Git 版本控制系統(2) 開 branch 分支和操作遠端 repo.

關於 Git 可以參考我的 Git 版本控制 課程資料

用 Git 就是要愛用 Branch 啊,Branch 很好用,開 Branch 不用錢。開 Branch 的情境除了在上一篇中提到因應產品 release 需求的 stable/production branch 之外,其他開 branch 情況有:

  • 帶有實驗性質的變更,例如想改寫新的演算法、重構程式碼等
  • 新功能 feature 開發
  • Bug fixes,你可能需要做些實驗才知道到底怎麼修

這些事情都可以先在本地開 local branch 做,而不需要立即 Push 分享給別人。


git branch <new_branch_name> 建立本地 local branch
git branch -m <old_name> <new_name> 改名字 (如果有同名會失敗,改用 -M 可以強制覆蓋)
git branch 列出目前有那些 branch 以及目前在那個 branch
git checkout <branch_name> 切換 branch (注意到如果你有檔案修改了卻還沒 commit,會不能切換 branch,解法稍後會談)
git checkout -b <new_branch_name> (<from_branch_name>) 本地建立 branch 並立即 checkout 切換過去
git branch -d <branch_name> 刪除 local branch

開 Branch 最大的好處除了可以不影響 stable 和其他分支版本的開發,另一個超棒的地方是”你可以決定 Merge 的方式”。Git 的 Merge 方式可以分成四種:

  • Straight merge 預設的合併模式,會有全部的被合併的 branch commits 記錄加上一個 merge-commit,看線圖會有兩條 Parents 線,並保留所有 commit log。
  • Squashed commit 壓縮成只有一個 merge-commit,不會有被合併的 log。SVN 的 merge 即是如此。
  • cherry-pick 只合併指定的 commit
  • rebase 變更 branch 的分支點:將目前 branch 的 commits,一個個重新重新 apply (或叫做patch) 到要被 rebase 的 branch 上。例如在 A branch rebase B branch,就會把原本分支點之後所有的 A commits 記錄,重新在 B branch 上再 commit 一遍,這時新分支點變成 B branch 的最新的 commit。這方式僅適合還沒分享給別人的 local branch,因為等於砍掉重練目前 branch 的 commits 記錄。

其中 rebase 比較難理解在下一篇有圖解。


git merge <branch_name> 合併另一個 branch,若沒有 conflict 衝突會直接 commit。若需要解決衝突則會再多一個 commit。
git merge --squash <branch_name> 將另一個 branch 的 commit 合併為一筆,特別適合需要做實驗的 fixes bug 或 new feature,最後只留結果。合併完不會幫你先 commit。
git cherry-pick 321d76f 只合併特定其中一個 commit。如果要合併多個,可以加上 -n 指令就不會先幫你 commit,這樣可以多 pick幾個要合併的 commit,最後再 git commit 即可。

使用 merge 可能會有部分程式碼會 conflict 衝突:簡單的情況只要編輯檔案處理 <<<< ===== >>>>> 即可,然後重新 add 到 staging area 並 commit (沒有像 SVN 的 resolve 指令)。複雜一點的可以再用 git mergetool 選檔案合併的 GUI 工具 (OS X 下面可以用 opendiff, linux 可以用 kdiff3 ),處理好後 git commit。

一旦 merge 好了,git branch -d <branch_name> 可以刪除 branch。但如果要刪除的 branch 還沒有合併,就會有錯誤訊息。如果真的要強制刪除可以用 -D

Git 的 working tree 是從 SVN 換過來一個不習慣的地方,因為它只是一個工作暫存區,在切換 Branch 時就會整個換掉。也因為如此,如果有檔案有修改還沒有 commit 出去,切換 branch 時就會出現 error 不能切換 (除非是新的 untracking 檔案),例如有修改還沒 add 會出現 error: Entry ‘ooxx’ not uptodate. Cannot merge. 有修改且已經add(還沒ci)會出現 error: Entry ‘ooxx’ would be overwritten by merge. Cannot merge.

最理想的處理當然是事情剛好做到一個段落,把東西 commit 出去才切換 branch 做事。不過事情總有臨時,如果要換 branch 的暫時的解決方式是使用 git stash 會先把修改暫存下來,要回復則執行 git stash pop。下一篇等你學會 git reset 之後,你會發現就算把還沒完成的東西 commit 也不會怎麼樣,只要還沒 push 出去一切 commit 紀錄都是可以改的。

Remote repo. 操作

首先要認識的是 Protocol,像在 Github 上面看自己的 Project,會有分 Public Clone URL 跟 Your Clone URL,這有什麼差?

  • 這種的是使用 Git 自己的 prototol,優點是速度快,但是沒有認證機制,只適合 read only (port:9418)
  • [email protected]/ihower/project.git 這種的是使用 SSH,可以有認證(SSH key)
  • Git 也可以透過 HTTPS 的方式,不過速度較慢,比較適合對 firewall 有限制的情況

其中 Github 就是同時用 SSH + Git protocol,兼顧認證需求及速度。


git clone <remote_address>
git checkout --track -b foobar origin/foobar 將遠端的 branch checkout 回來並建立一個新的 local branch,加上 --track 表示你之後還要pull、push回去,所以請 Git 記住對應關係。
git pull (<local_branch_name> origin/<remote_branch_name>) 去遠端 fetch 新版並 merge 進 local branch
git push 將 local branch 的 commit 紀錄更新到遠端

git pull 要注意的是,如果別人在你上次 pull 之後有 push 新東西上去(也就是說跟你的 branch 產生分岔了),此時有兩種情況: 一是 Git 可以順利 auto merge 的話,git 會自動多一次 merge commit,這也就為什麼常常 log 會跑出 Merge branch ‘master’ of [email protected]。二是如果有 conflict,這時候就需要你手動處理然後 commit。話說如果覺得這種 local branch 和 remote branch 的 merge commit log 很煩,建議可以改使用 git pull –rebase 指令來變成 fast-forward 形式 (就會變得像 svn up,而不會有 merge commit log)。rebase 的意思可能要下一篇才會詳細說明的清楚,簡單的說(?),就是先砍掉 local branch 分岔點之後自己的 commit,然後把遠端的 commit 先一個個 apply 進來,最後再把自己的 commit 再 apply 進去 (如果有 conflict 會中途停下來,等你修好才會繼續 apply),如此一來看線圖就會變成一條線而已,也就沒有所謂 merge 這個動作了。

git push 預設的遠端是 origin,並且會將所有有和 remote 有對應的 local branch 都 push 上去。如果要把新的 local branch push 上去,需要下 git push origin <local_vranch_name> 指令。

git push 也可能會失敗,例如出現 ! [rejected] master -> master (non-fast forward),這個 non-fast forward 的意思是你的 parent commit 和遠端的不相同,也就是線圖有分岔,需要先 pull 回來處理好 merge 才能 push 上去。

fast-forward 在 Git 是一種 merge 術語,當 B branch (例如一個 local branch) 是從 A branch (例如一個 remote branch) 的最新版(HEAD)分支出來的,那當 A 要把 B merge 進來時,因為 B 的 parent commit 是 A 的 HEAD,所以這兩個 branch 唯一的差異就是 B 後來的 commit 而已,而不會有任何 conflict。所以實際上的動作只要把 A 的 HEAD 改成 B 的 HEAD 就好了,線圖上這兩個 branch 根本是同一條線,此謂 fast-forward。

其他操作還有:


git fetch 把遠端的 branch 更新下載回來,但不會 merge 到 local branch
git branch -r 顯示 local 有追蹤的遠端 branch。注意到你不能直接修改這個 remote branch,一定要用一個 local branch 對應它。
git remote show origin 顯示遠端 server 的 branch
git remote add foobar git:// 可以新增別的 repo. 位置,於是 pull 的時候就可以指定要從哪一個遠端更新回來。
git push origin :foobar 刪除遠端的 branch

因為遠端的操作指令比較雜,所以也有人寫了 git_remote_branch 來簡化操作。

Mac OS X 瘦身軟體

MBP 硬碟不夠用,除了換大硬碟,就是努力砍沒用的資料了。首先找到的是小海介紹的 Monolingual,它會砍掉沒用的語系檔跟 Power PC Binary,大概可以省出快 5GB 吧。

不過還是不夠啊,最後拯救我的是 OmniDiskSweeper 這套磁碟清理工具,可以很方便地找出到底哪個檔案跟目錄是最佔空間,也發現原來 Mac 內建一堆沒用又佔空間的印表機驅動程式… :|

Git 版本控制系統 (1)

關於 Git 可以參考我的 Git 版本控制 課程資料

自從去年 Rails 改用 Github hosting 之後,就想學 Git 很久了(一直以來都只會像 svn 一樣操作 add, rm commit, push, pull 而已,如果你的程度也是如此,建議你可以再多學學)。今年許下的第一個目標就是認真學會 Git,而直到上個月和多開始陸陸續續把所有的 svn repo. 都換成 git,才開始非得天天用 git 的日子 :p

Git 是由 Linus Torvalds(Linux 發明人,發明 Git 的目的之一就是用來管理 Linux 原始碼) 發展出來的 content tracker 系統,它的內部設計像是檔案系統,而不是 SCM 系統,雖然它最主要的用處是拿來做 DVCS。

這裡最重要的理解就是:他追蹤的是資料內容,不是記錄版本間的差異。所以即使是改檔名或是在不同的 branch 裡面,相同的內容仍然只會有一份實體,其他都是利用 metadata 建立 reference 關聯。這也是為什麼它比起 SVN 又快又省空間又有效率,分支 branch 跟 tag 超快(只是建立 reference 而已),而且也可以改檔名(在 SVN 要刪除再新增,真是 sucks)。

因為 Git 比起 SVN 實在先進多了,除了比較難上手一些之外,我想這幾年應該會大規模的取代 SVN 成為新霸主吧(Why Git is Better than X),它目前最大的缺點是 Windows 上的支援還不是非常成熟。如果您想看看其他選擇,聽說 Hg 這套 DVCS 也是非常不錯 (快把 SVN 丟了吧)。

認真學會了一套分散式版本控制系統 DVCS,對專案開發真是幫助不少,以往我會對 branch 感到害怕,覺得是高級的技巧,學會 Git 之後這件事變成一個非常 easy 的開發技巧,不像 svn copy 跟 svn merge 令人抗拒。這對一個已經在上 production 的軟體來說幫助極大,因為必需同時維護 stable 的版本只做 bug fixes,以及數個 development branch 同時開發不同 topic/features。

另一好處是分散式 SCM 有本地端 Repo.,因此不需要網路就可以 commit 了,因此有時候做高鐵或等車想寫 Code 也就不需要網路了。而且本地端 Repo. 看 log 超快超方便,看 SVN Log 真是超級痛苦的網路延遲。另一個比較進階的長處是:只要在本地端還沒有 Push 分享給別人,你都可以修改 commit histroy。這是非常 powerful 的能力,例如我可以取消前一次或任一次的 commit、變更 commit 順序、移動 local branch 的分支點、將數個 commit 合併成一個、將一個 commit 打散成數個等等,因此可以在本地端都確認 okay 準備好才 Push 分享給別人。這種流程我自己也在試著改變,以往 svn 的經驗會讓你習慣一 commit 就馬上 push,但是使用 git 的話其實不需要這麼急著把還不完整的程式分享出去。

我的主要教材的是 Pragmatic Version Control Using Git 一書和 Git Internals PDF,前者很容易閱讀,後者比較概括性一些甚至開頭就先介紹了內部的運作。除了官方的說明文件,其他推薦的 Git 學習網頁有:

除了安裝 Git command,使用 GUI 軟體看分支線圖可是使用 Git 的樂趣之一。其中 MAC 上的 GitX 軟體超級推薦,內建的則有 gitk 和 git-gui。

Setup

使用 Git 的第一步是務必設定好你的名子跟email,該檔案會在 ~/.gitconfig,可以用以下指令直接加入:


git config --global user.name "ihower"
git config --global user.email "[email protected]"

接著是建立 repo.,有兩種方式:


git init <dir_name> 會在本地新建一個 repo.。
git clone <remote_location> 會從遠端複製一份 repo 回來。

以下介紹的操作都跟 remote repo. 沒有關係,都只是在本地 repo. 操作。(作為一個 DVCS,本地端目錄就會包含所有的 commit 紀錄)

add, status, commit (working tree and staging area)

Working tree 是你當下的工作目錄,就像 SVN checkout 出來的工作目錄。這裡要特別學習的新概念是 Staging area,這是 Git 獨有的功能。它是一塊暫存的 cached 區域,用來紀錄什麼是你待會要 commit 檔案。


git add . 加入所有檔案,包括所有還沒有被追蹤(untracked)的檔案
git add -i 進入互動模式,你甚至可以只 Add 檔案裡面其中的一段程式碼到 staging area 去(稱作patch)
git add -u 只加更新的檔案,不加入還沒有追蹤的檔案 (跟 git commit -a 涵蓋的範圍相同)

使用 git add 會將檔案加入 staging area 中,特別注意到待會的 commit 是 commit 你執行 add 時當下的檔案,而不是最後的版本。(也就是如果你 add 完之後又再次修改同個檔案,commit 出去的檔案是第二次修改前的版本)

接著也是最常用的 git status,會列出以下狀態:

  • Changed but not updated 有修改但是沒有加入 staging area 的檔案
  • Changes to be committed (staging area) 已經加入 staging area 的檔案
  • Untracked 還沒有被追蹤的新檔案

然後是 commit 指令,會把 Staging area 裡面的東西 commit 出去:


git commit -m "blah" 如果沒加 -m 的話, 會開編輯器輸入 commit log (可以在.gitconfig中設定編輯器)
git commit -a -m "foobar" 全部修改的檔案都 add 後 commit 出去 (不包括 untracked 的新檔案)
git commit -v 會開編輯器加上 diff 註解

其他常見操作有:


git rm foobar 刪除
git mv old_file new_file 改檔名

Git 沒有 git copy,因為既然 Git 不是 track file, 而是 track content,所以當你新建一個檔案把相同內容貼上去,Git 會知道你貼上去的東西從哪裡來的,也不需要你告訴 Git 你在 copy。

也因為多了一層 staging area 的存在,所以 diff 細分成三種


git diff 是比較 working tree 跟 staging area
git diff --cached 是比較 staging area 跟本來的 repo.
git diff HEAD 是比較 working tree 跟本來的 repo.

後幾篇還會提到更多如何做回復動作,這裡先介紹兩個指令:


git reset HEAD filename 會從 staging area 狀態回到 unstaging 或 untracked (檔案內容並不會改變)
git checkout filename 會從 unstaging 狀態回到最初 repo. 的檔案(檔案內容變回修改前)

最後一提的是 .gitignore,這個檔案可以編輯列出哪些檔案是要忽略不需要 tracked 的,像 Rails 就會加入 log/*.log 和 tmp/**/* 。

Git 還有項比較特立獨行的行為是空目錄是不會 commit 出去的,這在某些專案可能會是問題(例如 Rails 如果少了 log 或 tmp 目錄會不能啟動),解決辦法是在該目錄下 touch 一個空的檔案,慣例是 .gitkeep。

(請期待續集:開 Branch 分支、操作遠端 Repo.、還沒 push 前可以幹的壞事以及其他奇技淫巧)