Git 沙盒模擬實戰
分支
現有一個主分支
建立分支
# 建立分支
$ git branch bugFix
# 切換到指定分支
$ git checkout bugFix
或者
# 建立分支,並切換到該分支
$ git checkout -b bugFix
切換到指定分支後,工作區也會更新。比如在 IDEA 中工程檔案會切換到該分支的儲存的版本資料。
合併分支
先要建立分支,切換到該分支,並進行提交
$ git checkout -b bugFix
$ git commit -m "c2"
$ git checkout master
$ git commit -m "c3"
# 合併指定分支到當前分支
$ git merge "需要合併的分支"
Rebase
第二種合併分支的方法是
git rebase
。Rebase 實際上就是取出一系列的提交記錄,“複製”它們,然後在另外一個地方逐個的放下去。Rebase 的優勢就是可以創造更線性的提交歷史,這聽上去有些難以理解。如果只允許使用 Rebase 的話,程式碼庫的提交歷史將會變得異常清晰。
$ git rebase master
其他關於分支命令:
# 列出所有本地分支
$ git branch
# 列出所有遠端分支
$ git branch -r
# 列出所有本地分支和遠端分支
$ git branch -a
# 新建一個分支,指向指定commit
$ git branch [branch] [commit]
# 選擇一個commit,合併進當前分支
$ git cherry-pick [commit]
# 新建一個分支,與指定的遠端分支建立追蹤關係
$ git branch --track [branch] [remote-branch]
# 建立追蹤關係,在現有分支與指定的遠端分支之間
$ git branch --set-upstream [branch] [remote-branch]
# 刪除分支
$ git branch -d [branch-name]
# 刪除遠端分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]
分離 HEAD
HEAD 是一個對當前檢出記錄的符號引用 —— 也就是指向你正在其基礎上進行工作的提交記錄。
HEAD 總是指向當前分支上最近一次提交記錄。大多數修改提交樹的 Git 命令都是從改變 HEAD 的指向開始的。
HEAD 通常情況下是指向分支名的(如 bugFix)。在你提交時,改變了 bugFix 的狀態,這一變化通過 HEAD 變得可見。
分離的 HEAD 就是讓其指向了某個具體的提交記錄而不是分支名。在命令執行之前的狀態如下所示:
HEAD -> bugFix-> C4
HEAD 指向 bugFix, bugFix 指向 C4
$ git checkout c4
相對引用
通過指定提交記錄雜湊值的方式在 Git 中移動不太方便。在實際應用時,並沒有像上圖中這麼漂亮的視覺化提交樹供你參考,所以你就不得不用 git log
來查檢視提交記錄的雜湊值。
例如上面的介紹中的提交記錄的雜湊值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8
。舌頭都快打結了吧...
比較令人欣慰的是,Git 對雜湊的處理很智慧。你只需要提供能夠唯一標識提交記錄的前幾個字元即可。因此我可以僅輸入fed2
而不是上面的一長串字元。
正如我前面所說,通過雜湊值指定提交記錄很不方便,所以 Git 引入了相對引用。這個就很厲害了!
使用相對引用的話,你就可以從一個易於記憶的地方(比如 bugFix
分支或 HEAD
)開始計算。
相對引用非常給力,這裡我介紹兩個簡單的用法:
- 使用
^
向上移動 1 個提交記錄 - 使用
~<num>
向上移動多個提交記錄,如~3
首先看看操作符 (^)。把這個符號加在引用名稱的後面,表示讓 Git 尋找指定提交記錄的父提交。
所以 master^
相當於“master
的父節點”。
master^^
是 master
的第二個父節點
$ git checkout bugFix^
如果你想在提交樹中向上移動很多步的話,敲那麼多 ^
貌似也挺煩人的,Git 當然也考慮到了這一點,於是又引入了操作符 ~
。
該操作符後面可以跟一個數字(可選,不跟數字時與 ^
相同,向上移動一次),指定向上移動多少次。我們們還是通過實際操作看一下吧
我使用相對引用最多的就是移動分支。可以直接使用 -f
選項讓分支指向另一個提交。例如:
$ git branch -f master HEAD~3
上面的命令會將 master 分支強制指向 HEAD 的第 3 級父提交。
移動 HEAD
和 bugFix
到目標所示的位置。
$ git checkout HEAD~1
$ git branch -f bugFix HEAD~1
撤銷變更
在 Git 裡撤銷變更的方法很多。和提交一樣,撤銷變更由底層部分(暫存區的獨立檔案或者片段)和上層部分(變更到底是通過哪種方式被撤銷的)組成。我們這個應用主要關注的是後者。
主要有兩種方法用來撤銷變更 —— 一是 git reset
,還有就是 git revert
。接下來我們們逐個進行講解。
git reset
通過把分支記錄回退幾個提交記錄來實現撤銷改動。你可以將這想象成“改寫歷史”。git reset
向上移動分支,原來指向的提交記錄就跟從來沒有提交過一樣。
雖然在你的本地分支中使用 git reset
很方便,但是這種“改寫歷史”的方法對大家一起使用的遠端分支是無效的哦!
為了撤銷更改並分享給別人,我們需要使用 git revert
。
分別撤銷
local
分支和pushed
分支上的最近一次提交。共需要撤銷兩個提交(每個分支一個)。記住
pushed
是遠端分支,local
是本地分支
$ git reset master
# 先切換到 pushed
$ git checkout pushed
$ git revert c2 # 需要用到提交的雜湊碼
移動(整理)提交記錄
到現在我們已經學習了 Git 的基礎知識 —— 提交、分支以及在提交樹上移動。 這些概念涵蓋了 Git 90% 的功能,同樣也足夠滿足開發者的日常需求
然而, 剩餘的 10% 在處理複雜的工作流時(或者當你陷入困惑時)可能就顯得尤為重要了。接下來要討論的這個話題是“整理提交記錄” —— 開發人員有時會說“我想要把這個提交放到這裡, 那個提交放到剛才那個提交的後面”, 而接下來就講的就是它的實現方式,非常清晰、靈活,還很生動。
看起來挺複雜, 其實是個很簡單的概念。
Git Cherry-pick
本小節的第一個命令是 git cherry-pick
, 命令形式為:
git cherry-pick <提交號>...
如果你想將一些提交複製到當前所在的位置(HEAD
)下面的話, Cherry-pick 是最直接的方式了。我個人非常喜歡 cherry-pick
,因為它特別簡單。
我們們還是通過例子來看一下!
需要簡單的將三個分支中的提交記錄複製到 master 上就可以了。
$ git cherry-pick C3 C4 C7
互動式 Rebase
當你知道你所需要的提交記錄(並且還知道這些提交記錄的雜湊值)時, 用 cherry-pick 再好不過了 —— 沒有比這更簡單的方式了。
但是如果你不清楚你想要的提交記錄的雜湊值呢? 幸好 Git 幫你想到了這一點, 我們可以利用互動式的 rebase —— 如果你想從一系列的提交記錄中找到想要的記錄, 這就是最好的方法了
我們們具體來看一下……
互動式 rebase 指的是使用帶引數 --interactive
的 rebase 命令, 簡寫為 -i
如果你在命令後增加了這個選項, Git 會開啟一個 UI 介面並列出將要被複制到目標分支的備選提交記錄,它還會顯示每個提交記錄的雜湊值和提交說明,提交說明有助於你理解這個提交進行了哪些更改。
當 rebase UI介面開啟時, 你能做3件事:
- 調整提交記錄的順序(通過滑鼠拖放來完成)
- 刪除你不想要的提交(通過切換
pick
的狀態來完成,關閉就意味著你不想要這個提交記錄) - 合併提交。 簡而言之,它允許你把多個提交記錄合併成一個。
做一次互動式的 rebase,整理成目標視窗中的提交順序。
$ git rebase -i overHere
一些提交技巧
只去一個提交記錄
來看一個在開發中經常會遇到的情況:我正在解決某個特別棘手的 Bug,為了便於除錯而在程式碼中新增了一些除錯命令並向控制檯列印了一些資訊。
這些除錯和列印語句都在它們各自的提交記錄裡。最後我終於找到了造成這個 Bug 的根本原因,解決掉以後覺得沾沾自喜!
最後就差把 bugFix
分支裡的工作合併回 master
分支了。你可以選擇通過 fast-forward 快速合併到 master
分支上,但這樣的話 master
分支就會包含我這些除錯語句了。你肯定不想這樣,應該還有更好的方式……
實際我們只要讓 Git 複製解決問題的那一個提交記錄就可以了。跟之前我們在“整理提交記錄”中學到的一樣,我們可以使用
git rebase -i
git cherry-pick
來達到目的。
$ git cherry-pick bugFix
提交的技巧 #1
接下來這種情況也是很常見的:你之前在 newImage
分支上進行了一次提交,然後又基於它建立了 caption
分支,然後又提交了一次。
此時你想對的某個以前的提交記錄進行一些小小的調整。比如設計師想修改一下 newImage
中圖片的解析度,儘管那個提交記錄並不是最新的了。
我們可以通過下面的方法來克服困難:
- 先用
git rebase -i
將提交重新排序,然後把我們想要修改的提交記錄挪到最前 - 然後用
commit --amend
來進行一些小修改 - 接著再用
git rebase -i
來將他們調回原來的順序 - 最後我們把 master 移到修改的最前端(用你自己喜歡的方法),就大功告成啦!
當然完成這個任務的方法不止上面提到的一種(我知道你在看 cherry-pick 啦),之後我們會多點關注這些技巧啦,但現在暫時只專注上面這種方法。 最後有必要說明一下目標狀態中的那幾個'
—— 我們把這個提交移動了兩次,每移動一次會產生一個 '
;而 C2 上多出來的那個是我們在使用了 amend 引數提交時產生的,所以最終結果就是這樣了。
也就是說,我在對比結果的時候只會對比提交樹的結構,對於 '
的數量上的不同,並不納入對比範圍內。只要你的 master
分支結構與目標結構相同,我就算你通過。
$ git rebase -i master
# 使用新的一次提交
$ git commit --amend
$ git rebase -i master
$ git checkout master
$ git merge caption
提交的技巧 #2
正如你在上小節所見到的,我們可以使用 rebase -i
對提交記錄進行重新排序。只要把我們想要的提交記錄挪到最前端,我們就可以很輕鬆的用 --amend
修改它,然後把它們重新排成我們想要的順序。
但這樣做就唯一的問題就是要進行兩次排序,而這有可能造成由 rebase 而導致的衝突。下面還是看看 git cherry-pick
是怎麼做的吧。
要在心裡牢記 cherry-pick 可以將提交樹上任何地方的提交記錄取過來追加到 HEAD 上(只要不是 HEAD 上游的提交就沒問題)。
同上一小節一樣的問題,我們是用 git cherry-pick
來解決。
$ git checkout master
$ git cherry-pick c2
$ git commit --amend
$ git cherry-pick c3
打標籤
相信你已經發現了:分支很容易被人為移動,並且當有新的提交時,它也會移動。分支很容易被改變,大部分分支還只是臨時的,並且還一直在變。
你可能會問了:有沒有什麼可以永遠指向某個提交記錄的標識呢,比如軟體釋出新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有沒有比分支更好的可以永遠指向這些提交的方法呢?
當然有了!Git 的 tag 就是幹這個用的啊,它們可以(在某種程度上 —— 因為標籤可以被刪除後重新在另外一個位置建立同名的標籤)永久地將某個特定的提交命名為里程碑,然後就可以像分支一樣引用了。
更難得的是,它們並不會隨著新的提交而移動。你也不能檢出到某個標籤上面進行修改提交,它就像是提交樹上的一個錨點,標識了某個特定的位置。
$ git tag v1 c2
$ git tag v0 c1
$ git checkout v1
如果你不指定提交記錄,Git 會用 HEAD
所指向的位置。進行標記 Tag。
$ git tag v2
# 此時 HEAD 指向 c2 就會為 c2增加一個名為 v2 的tag
獲取描述
由於標籤在程式碼庫中起著“錨點”的作用,Git 還為此專門設計了一個命令用來描述離你最近的錨點(也就是標籤),它就是 git describe
!
Git Describe 能幫你在提交歷史中移動了多次以後找到方向;當你用 git bisect
(一個查詢產生 Bug 的提交記錄的指令)找到某個提交記錄時,或者是當你坐在你那剛剛度假回來的同事的電腦前時, 可能會用到這個命令。
git describe
的語法是:
git describe <ref>
<ref>
可以是任何能被 Git 識別成提交記錄的引用,如果你沒有指定的話,Git 會以你目前所檢出的位置(HEAD
)。
它輸出的結果是這樣的:
<tag>_<numCommits>_g<hash>
tag
表示的是往上追溯,離ref
最近的標籤,numCommits
是表示這個ref
與tag
相差有多少個提交記錄,hash
表示的是你所給定的ref
所表示的提交記錄雜湊值的前幾位。
當 ref
提交記錄上有某個標籤時,則只輸出標籤名稱
$ git describe side
v1_1_gC4
$ git describe master
v0_2_gC2
高階操作
多分支 rebase
哥們兒,我們準備了很多分支!我們們把這些分支 rebase 到 master 上吧。
但是你的領導給你提了點要求 —— 他們希望得到有序的提交歷史,也就是我們最終的結果應該是 C6'
在 C7'
上面, C5'
在 C6'
上面,依此類推。
即使你搞砸了也沒關係,用 reset
命令就可以重新開始了。記得看看我們提供的答案,看你能否使用更少的命令來完成任務!
$ git rebase master bugFix
$ git rebase bugFix side
$ git rebase side another
$ git rebase another master
選擇父提交記錄
操作符 ^
與 ~
符一樣,後面也可以跟一個數字。
但是該操作符後面的數字與 ~
後面的不同,並不是用來指定向上返回幾代,而是指定合併提交記錄的某個父提交。還記得前面提到過的一個合併提交有兩個父提交吧,所以遇到這樣的節點時該選擇哪條路徑就不是很清晰了。
Git 預設選擇合併提交的“第一個”父提交,在操作符 ^
後跟一個數字可以改變這一預設行為。
使用 ^
和 ~
可以自由地在提交樹中移動,非常給力。
更厲害的是,這些操作符還支援鏈式操作!
$ git branch bugWork master^^2^
糾纏不清的分支
現在我們的 master
分支是比 one
、two
和 three
要多幾個提交。出於某種原因,我們需要把 master
分支上最近的幾次提交做不同的調整後,分別新增到各個的分支上。
one
需要重新排序並刪除 C5
,two
僅需要重排排序,而 three
只需要提交一次。
慢慢來,你會找到答案的 —— 記得通關之後用 show solution
看看我們的答案哦。
$ git checkout one
$ git cherry-pick C4 C3 C2
$ git checkout two
$ git cherry-pick C5 C4 C3 C2
$ git branch -f three C2