分支

branch (分支)應該是 Git 最重要的技能了,在專案的開發過程中有時候要開發新功能,有時候是要修正某個Bug,這時候我們通常都會從主分支 (通常叫master)再開出一條新的分支來做,這支新開的分支會帶著你的master目前的最新狀態,當你完成要開發的新功能或 Bug 修正後確認沒問題就再把它 merge(合併)回master,如此便完成了新功能的開發或是 Bug 的修正,因此每個人都可以從master拉一條新的分支來做自己想做的事。整個過程就像玩RPG一樣,為了觸發不同的劇情又不覆蓋其他的劇情就會多開好幾個儲存點(commit),只是沒有合併這選項而已。

Gitk

為了明白branch的概念我們最好有圖像協助了解。這時我們輸入gitk --all&叫出內建的GUI圖形介面gitk,在指令後加上&是為了讓程式在背景執行,這樣我們才能在終端機輸入指令
如果沒有內建也可以輸入sudo apt install gitk安裝


Branch

輸入git branch可以列出所有的分支並告訴你目前正在哪個 branch(有*符號的),如果我想開新的分支John可以輸入git branch John

從gitk可以看到John和master在同一條水平線上,表示目前他們的狀態是一模一樣的。

HEAD

指向當前的游標。你現在位於哪,HEAD就指向哪,git也才移動到哪。HEAD就像玩RPG裡讀檔時的標記,當你在這個紀錄底下遊玩會標示它。所以別被HEAD字面意思搞錯認為只能留在分支最新的commit上,其實它能指向任一分支或是commit。

Checkout

當我們checkout的時候其實就是移動HEAD就會跳到另外一個commit或branch上。我們現在還在master,如果想切換到John要輸入git checkout

我正在John上做編輯,新增檔案John.txt,加上一些內容,將它 add 到 stage 後再 commit 它。接下來切換到原本的master,這時候你會發現剛剛在John新增的John.txt檔案不見了

這就是HEAD被移動的時候會造成工作目錄的檔案內容改變,所以如果有檔案被修改過但是沒有commit的檔案存在時會不能夠移動HEAD,也就是不能使用git checkout。
我們現在來用GUI來看

發現到與剛剛John和master在同一條水平線上不同,John現在已經比master再多出了一個 commit 的內容。如果這時候我們在 master 上繼續開發會怎樣呢?接下來我們在master創一個新檔案Alice.txt然後依然add後commit

現在master與John已經產生分歧了,因為兩個分支都有了各自往後開發的 commit ,而且由於master最後一次的 commit 時間較新因此排列在最前面。

Rebase

假設現在John已經開發完成要合併回去主分支master,一般來說開發人員都是從master拉出一條新的分支完成後合併回去,所以master總是保持最新的完成狀態,至於這裡要學的指令git rebase不只是把兩條branch合併而已,而是將某一支 branch 基於另一支 branch 的內容合併起來,舉例來說現在我們的John已經完成了但master已經和我們當時拉出來的狀態不同,多了一個檔案。但我們會希望現在John的內容就像是剛剛從master出來後才做編輯,也就是說讓John裡保有master最新的狀態,我們現在輸入git rebase master, git rebase 會基於 master branch 目前最後一次的 commit 內容再往後把你在John上commit 的內容加上去,過程中沒有發生衝突,現在我們看一下GUI上的圖

目前John的內容就像是剛從master所 checkout 出來然後再加上自己的 commit,因此不同於 git merge 的線圖會把John合併到master, 而是把原本的John接到master因此只有一條線,當一個專案有很多的分支在做開發的時候會避免很多分支的線接來接去難以辨認。可以使用rebase將你現在的分支整理過再merge主幹,這樣會產生較漂亮的線圖

Tips
想要看看目前的 branch 與其他 branch 有哪些差異,你可以使用git diff
像是git diff John master

Merge

如果我們開發完畢時,我們會把開發好的東西合併回master很自然的我們通常都會使用git merge這個指令來合併兩個分支,像是git merge John,那現在所位於的分支會把John的commit合到自己身上

Conflict

當我們在進行merge或rebase時經常會因為一個檔案兩條分支有不同的內容而產生衝突,例如我們在John和master上都對John.txt做編輯然後在merge它們,你會看到 Git 告訴你在你合併的過程中在John.txt這支檔案發生了衝突

Git 不知道該怎麼處理因此要你去處理它,這時候我們打開這支檔案會看到這樣的情況

<<<<<<<<<< HEAD 到 ========== 的上半部是目前你所在分支的 commit 內容(從HEAD就能看出),而從 =========== 到 >>>>>>>>>>> John 則是你要合併的John的內容,你必須做出決定看是要全部留下,或是二選一,或是改成你想要的內容,改好後記得要將 Git 自動產生的 <<< 、 =====、 <<< 的內容都刪除,修改完畢後存檔
將剛剛修改過的檔案再使用git add加入 stage ,將所有的衝突都修正完畢後就使用git commit提交一次 commit,由於這次的 commit 是在處理 merge 時的衝突,因此 Git 很聰明的已經幫我們加上了一些預設的訊息Merge branch ‘John’, commit 提交後就會看到合併成功的訊息了。


Reset

還記得檔案的三個狀態嗎?修改未追蹤的檔案(HEAD)、已add(stage)、commit後(工作目錄)。字典中可以查到reset意義為to set again, to clear,我們可以透過git reset這個指令來回復我們檔案但在此之前要先了解它的三個參數,
Mixed--(預設,不須輸入參數)會更改到HEAD和stage,不改變工作目錄
Soft--只更改HEAD
Hard--HEAD、stage和工作目錄全部更改
接下來透過一些功能我們會更了解這三個參數的意思
取消 merge:
輸入git reset --hard ORIG_HEAD
取消已暫存的檔案:
有時候我們會把還沒修改完的檔案'add'這時可以使用git reset HEAD <file> 把檔案取消 stage我們接下來可以用git stat來看剛剛的檔案現在已經在不在stage
取消修改過的檔案:
接續剛剛的情況,若是我想完全放棄這次修改 (將檔案狀態回復到最新的一次 commit 時的狀態),我可以使用git checkout -- <file> 來回復這支檔案,如果再用遊戲來比喻就像還沒存檔就跳出,再次進入檔案自然沒留下任何修改
修改上一次的commit:
手誤打太快, commit 訊息打錯時,我們可以使用git commit --amend來幫助我們重新修改 強制回復到上一次 commit 的版本:
有時候我們想要放棄所有修改回到 commit 時的狀態,這時候我們可以下git reset --hard HEAD來回復

TipsHEAD 參數可以加上一些變化,例如 HEAD^ 表示目前版本的上一個版本 HEAD~2 則是再上一個,因此你可以自由的跳回去之前的狀態。

我們此時再回來看看soft、mixed、hard:加上 —soft 參數,則會把做過的修改仍然會加入 stage ,不加參數的情況,只會把你做過的修改保留,若是加上 hard 參數的話則是把做過的修改完全刪除,回到那個版本原本的樣子。這樣有清楚了嗎?