changed 5 years ago
Linked with GitHub

0506 Learn Git Branching 攻略

7.8 Learn Git Branching
https://learngitbranching.js.org/

Git 命令

  1. commit
  2. branch
  3. checkout
  4. merge
  5. rebase
  6. reset
  7. revert
  8. cherry-pick
使用 export tree 和 import tree 與朋友分享 Git樹

用 build level 創建一個關卡,
或者 import level 試試朋友的。

言歸正傳,讓我們先從 levels 開始……

基礎篇: 循序漸進地介紹 git 主要命令

1. git commit

git commit

一個 commit 在 git repo 中會記錄目錄下所有文件的快照。
感覺像是大量的複製和貼上,但 git 的速度更快!

git 希望 commit 儘可能地不占空間,所以每次進行 commit 的時候,它不會單純地複製整個目錄。實際上它把每次 commit 視為從目前的版本到下一個版本的變化量,或者說一個 "(delta)"。

git 會保存 commit 的歷史紀錄,所以,絕大部分的 commit 的上面都會有 parent commit,在我們的圖形表示中,箭頭方向表示從 parent commit 到所對應的 child commit,保存這樣子的一個歷史紀錄是非常有用的。

要學的東西有很多,但現在你可以把 commit 當作是當下的 project 的快照。commit 不佔空間且可以快速切換!

Eg.

在實例中學習 commit。右邊是一個(小)git repository。
當前有兩個 commit,
一開始的 commit C0 以及 C1 這一個 commit 
表示之後可能的一些修改。
  • git commit
我們對於文件做了一些修改,並且把這些修改表示成一個 commit。
剛剛做的 commit C2 有一個 parent commit C1,
代表此次修改是從那裡過來的。

2. git branch

git 的 branch 非常不佔空間。
它們只是一個指向某個 commit 的 reference,就這麼簡單。

所以許多 git 的愛好者會建議:

早點建立 branch!經常建立 branch!

因為建立 branch 不怎麼會佔用到硬碟空間或者是記憶體,
所以你可以把你目前的工作分成好幾個 branch,
這比只用一個 branch 要來的好。

同時使用 branch 和 commit 時,我們待會可以看到兩者如何配合。現在,只要記住
使用 branch 其實就是在說:
「我想要包含這一次的 commit 以及它的所有 parent 的 commit。」

Eg.

舉一個例子來看看 branch 到底是什麼。

這裡,我們建立一個名稱為 newImage 的新的 branch。
  • git branch newImage
看吧!這就是建立 branch 所需的操作啦! 
newImage branch 現在指向 commit C1。

現在讓我們這個新的 branch 做一些操作。
  • git commit
master branch 前進了,但 newImage branch 沒有前進!
這是因為我們沒有「在」這個新的 branch 上,
這也是為什麼星號(*)會在 master 上。

3. checkout

使用如下指令告訴 git 我們想要切換到新的 branch

git checkout [name]

這可以讓我們在 commit 之前切換到新的 branch。
  • git checkout newImage
  • git commit
太好了!新的 branch 已經記錄了我們的修改。

Q. 建立一個叫 bugFix 的新的 branch,然後切換過去。

A.

  1. git branch bugFix
  2. git checkout bugFix

4. git merge

branch 以及 merge
太好了! 我們已經知道怎麼使用 commit 和 branch 了。

接下來要學的一招是如何合併(merge)兩個不同 branch 的工作。
這讓我們可以建立一個新的 branch ,並且在上面開發新功能,
然後合併回 master branch。

git merge 是我們要學習 merge 的第一個方法。該 merge 會產生一個特殊的 commit,它包含兩個唯一 parent commit。
一個 commit 如果有兩個 parent commit 的話,那就表示:
「我想把這兩個 parent commit 本身及它們的 所有的 parent commit 都包含進來。」

有圖有真相,看看下面的圖就明白了
在這裡,我們有兩個 branch:各自都有一個唯一的 commit。
這意味著沒有一個 branch 包含我們對文件的所有修改。
讓我們 merge 這兩個 branch 來解決這個問題。

Eg.

我們要 merge bugFix 到 master
  • git merge bugFix
哇!看見了沒有?首先,master 現在指向一個 commit,
這個 commit 有兩個 parent commit。

假如從 master 開始沿著箭頭向上走,
在到達起點的路上會經過所有的 commit。
這說明了現在 master 紀錄了對文件的所有修改。
還有,看見各個 commit 的顏色變化了嗎?為了幫助學習,我混合了顏色。每個 branch 都有特定的顏色。每個 commit 的顏色都變成了含有此 commit 的所有 branch 的混合色。

Eg.

master branch 的顏色被混入到所有的 commit,但 bugFix 沒有。接下來就改一下這裡吧。
讓我們 merge master branch 到 bugFix 吧。
  1. git checkout bugFix
  2. git merge master
因為 bugFix branch只是 master branch 的 parent,
git 什麼都不用做,
只是簡單地把 bugfix branch 移動到 master 指向的 commit。

現在所有的 commit 的顏色都是一樣的啦,
這表示每一個 branch 都包含了所有文件的修改!太厲害了啦!

Q. merge bugFix 到 master

想完成這一關,執行以下的操作:

1. 建立新的 branch,叫做 bugFix
2. 用 git checkout bugFix 切換到 bugFix branch
3. commit 一次
4. 用 git checkout 切換回 master branch
5. 再 commit 一次
6. 用 git merge 將 bugFix merge 到 master

記住,你可以用 "objective" 指令來重新顯示這個對話框!

A.

  1. git branch bugFix
  2. git checkout bugFix
  3. git commit -m "c2"
  4. git checkout master
  5. git commit -m "master c3"
  6. git merge bugFix

1 & 2 步驟 可以結合成 git checkout -b bugFix

5. git rebase

rebasing 是 merge branch 的第二種方法。
rebasing 就是取出一連串的 commit,"複製"它們,然後把它們接在別的地方。

雖然聽起來難以理解,rebasing 的優點是可以建立更線性的 commit history。假如只允許使用 rebasing 的話,則我們的 repo 中的 commit log 或者是 commit history 會更加簡潔好看。

讓我們親身體會一下...

Eg.

這裡,還是有兩個 branch;注意目前我們所在的 branch 是 bugFix(看那顆星啦)

我們想要把在 bugfix 所做的修改直接移到 master branch上。
使用 rebasing 的話,兩個 branch 看起來像是依序按順序進行修改,
實際上它們的修改是平行進行的。

用 git rebase 來實現吧
  • git rebase master
很厲害吧!現在 bugFix branch 上的工作在 master branch 的最前端,同時我們也得到了一個更加線性的 commit 順序。

注意,本來的 commit C3 沒有消失(在圖上面呈現陰影),
而我們"複製" C3,將它的副本 C3'接在 master branch 的後面。

現在唯一的問題是 master branch 還沒有更新...
我們接下來就更新它吧!

現在,切換到 master branch。接下來就把它 rebase 到 bugFix 上面吧...
  • git rebase bugFix
完成!因為 master branch 是 bugFix 的 parent,所以 git 只是把 master branch 往前移動到 bugFix 上。 
(註:染色前面的parent commit)

Q.

想完成這一關,執行以下操作:

建立 bugFix branch
commit 一次
切換回 master branch 再 commit 一次
再次切換到 bugFix branch,接著 rebase bugFix 這個 branch 到 master branch 上
祝你好運啦!

A.

  1. git checkout -b bugFix
  2. git commit -m "c2"
  3. git checkout master
  4. git commit -m "master c3"
  5. git checkout bugFix
  6. git rebase master

進階篇

HEAD: 在git tree 前後移動

在 git 中前後移動
在接觸 git 的更多進階的主題之前,我們先學習用不同的方法在你的 project 中的 commit tree 上面移動。

一旦能夠熟練地在 commit tree 中隨意地移動,
你使用其它的 git 指令也會更厲害!

HEAD: 指向目前所 checkout 的 commit (目前所在的commit)

HEAD
我們首先看一下 "HEAD",
HEAD 是一個 reference,
它是指向目前所 checkout 的 commit,
基本上,其實就是你目前所在的 commit。

在 commit tree 中,HEAD 總是指向最近的一次commit。
大部份 git 的指令如果要修改 commit tree 的狀態的話,
都會先改變 HEAD 所指向的 commit。

HEAD 通常指向一個 branch 的名稱(比如 bugFix)。
當你 commit 的時候,改變了 bugFix 的狀態,
這一個變化可以從 HEAD 的改變中看到。

Eg.

 Git示範
在實際的例子中。我們將會觀察 commit 前後 HEAD 的位置。
  1. git checkout C1
  2. git checkout master
  3. git commit
  4. git checkout C2
看吧!HEAD 一直藏在 master 分支的後面。

分離 HEAD: 指向commit

分離 HEAD 就是
讓其指向一個 commit 而不是 branch 的名稱。

這是指令執行之前的樣子:
HEAD -> master -> C1
  • git checkout C1
現在變成了

HEAD -> C1 (跟分支名稱master脫鉤)

Q.

想要完成這一個關卡,從 bugFix 分離出 HEAD 並且讓它指向一個 commit。

通過 hash 值可以指定 commit。每個 commit 的 hash 值顯示在各自的圓圈中。

A. git checkout C4

相對引用(^)

相對引用
如果要在 git 中移動,透過指定 commit 的 hash 值的方式會變得比較麻煩。
在實際例子中,你的終端機上面不會出現漂亮且具備視覺效果的 commit tree,
所以你不得不用 git log 來查詢 hash 值。

另外,hash 值的長度在真實的 git 環境中很長。
舉個例子,前一個關卡的介紹中的 commit 的 
hash 值是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌頭不要打結了...

幸運的是,git 對於處理 hash 值很有一套。
你只需要提供能夠唯一辨識出該 commit 的前幾個字元就可以了。
所以,我可以只輸入 fed2 而不是上面的一長串字元。
我說過,透過 hash 值來指定 commit 不是很方便,
所以 git 加入了相對引用。這個就很厲害了!

使用相對引用,你可以從一個易於記憶的地方
(比如說 branch 名稱 bugFix 或 HEAD)開始工作。

相對引用非常好用,這裡我介紹兩個簡單的用法:

使用 ^ 向上移動一個 commit
使用 ~<num> 向上移動多個 commit

Eg.

首先看看插入(^)這一個符號。
把這個符號接在某一個 reference 後面,
就表示你告訴 git 去找到該 reference 所指向的 commit 的 parent commit。

所以 master^ 相當於 "master 的 parent commit"。

master^^ 是 master 的 grandparent commit(往前推兩代)

切換到 master的 parent commit
  • git checkout master^

Eg.

你也可以把 HEAD 當作相對引用。以下指令使用 HEAD 在 commit tree 中向上移動數次。

簡單吧!我們可以一直使用 HEAD^ 向上移動。
  1. git checkout C3
  2. git checkout HEAD^
  3. git checkout HEAD^
  4. git checkout HEAD^

Q.

要完成這一關,切換到 bugFix 的 parent commit。這會分離出 HEAD。

如果你願意的話,透過直接指定 hash 值的方式也可以過關,
但是還是試試看相對引用吧!

A. git checkout C4^

相對引用(~)

"~" 符號
假設需要在 commit tree 中向上移動多個 commit。使用太多 ^ 會非常討人厭,所以 Git 也加入了波浪(~)符號。

波浪符號後面可以選擇一個數字(你也可以不選擇),
該數字可以告訴 Git 我要向上移動多少個 commit 。舉個例子

使用 ~ 一次往上移動多個 commit。
哇!太簡潔了 -- 相對引用真的很好用!
  • git checkout HEAD~4

Branch forcing

git branch -f master HEAD~3

你現在是相對引用的高手了,現在用它來實際做點事情。

我使用相對引用最多的就是移動分支。
你可以使用 -f 選項直接讓分支指向另一個 commit。

舉個例子:

git branch -f master HEAD~3

(強制)移動 master 指向從 HEAD 往上數的第三個 parent commit。

Q.

要完成這一關,移動 HEAD,master 和 bugFix 到目標所示的位置。

A. 將貼紙撕起來再重貼,合成一個步驟

  1. git branch -f master C6 (強制把master移到C6)
  2. git branch -f bugFix C0 (強制把bugFix移到C0)
    (注意:如果HEAD在屬於C0, C6的分支上,就無法重貼)
  3. git checkout C1 (最後把HEAD移到C1)
    (注意:checkout移動的是HEAD)

取消分支的修改

取消 git 的修改
在 git 裡面取消修改的方法很多。
和 commit 一樣,
在 git 裡面取消修改同時具有底層的部份(暫存一些獨立的文件或者片段)和高層的部份(修改是如何被取消)。
我們主要講的重點是後者。

在 git 裡主要用兩種方法來取消修改,
一種是 git reset,另外一種是 git revert。
讓我們在下一個對話視窗中逐一瞭解它們。

6. git reset

Git Reset
git reset 把分支的參考點退回到上一個 commit 來取消修改。
你可以認為這是在"重寫歷史"。

git reset 往回移動 branch,
原來的 branch 所指向的 commit 好像從來沒有存在過一樣。

讓我們來看看要怎麼操作:
  • git reset HEAD~1
太好了! Git 把 master branch 簡單地移回到 C1;現在在我們的 local 已經退回到沒有 commit 過 C2 的狀態了。

7. git revert

Git Revert
雖然在你的 local branch 中使用 git reset 很方便,
但是這種「改寫歷史」的方法對別人的 remote branch 是無效的哦!

為了取消修改並且把這個狀態分享給別人,我們需要使用 git revert。舉個例子
  • git revet HEAD
很奇怪吧!在我們要取消的 commit 後面居然多了一個新的 commit!

這是因為新的 commit C2' 引入了修改
——用來表示我們取消 C2 這個 commit 的修改。
(C2 和 C2' 互相抵消)

多虧了 revert,現在可以把你的修改分享給別人啦。

Q.

要完成這一關,
分別取消 local branch 和 pushed branch 上的最近的一次 commit。

記住 pushed 是一個 remote branch,
local 是一個 local branch,

有了這麼明顯的提示應該知道要用哪種方法了吧?

A.

  1. git reset HEAD^
  2. git checkout pushed
  3. gut revert HEAD

移動 commit

移動 commit

目前為止我們已經講了 git 的基礎,
這些基礎包括 commit、branch 以及在 commit tree 中移動,

只要有這些概念你就能發揮 git 90% 的功力,而且對於程式設計師來說,這樣就很夠了。
而剩下的 10%,在很複雜的專案上面,是非常有用的(或者當你陷入困惑時)

我們下一個要講的概念是 "移動 commit",
換句話說,當你會這個非常有彈性的招數之後,
你就可以說"我想要把這個 commit 放這裡,而那個 commit 放在那裡"。

這看起來很複雜,但其實它很簡單。

8. git cherry-pick

git cherry-pick
我們要講的第一個指令叫作 git cherry-pick,它的用法如下:

git cherry-pick <Commit1> <Commit2> <...>
當你想要複製幾個 commit 並且接在你目前的位置(HEAD)下面的時候,這會是一個非常直接的方式。我個人非常喜歡用 cherry-pick,因為它並不複雜,很容易就可以了解。

讓我們來看一個例子!

Eg.

這裡有一個 repo,在 side branch 中,
我們有一些 commit 想要複製到 master branch 上,
這可以透過一個 rebase 來完成(我們之前已經學到了),
但是讓我們看看 git cherry-pick 怎麼做。
  • git cherry-pick C2 C4
就是那樣!我們複製了 C2 以及 C4 並且把它們放到我們的後面,很簡單吧!

Q.

要完成這個關卡,只需要從三個 branch 複製幾個 commit 到 master 下面,你可以從視覺化的目標看到我們需要哪些 commit。

A. git cherry-pick C3 C4 C7

git interactive rebase

「介紹互動式的 rebase」

git interactive rebase
當你知道你要複製哪些 commit(而且你也知道他們所對應的 hash 值),那麼 git cherry-pick 很適合你。

但是如果你不知道你要的是哪些 commit 呢? 很幸運的是,git 也有考慮到這個問題喔!我們可以用互動式的 rebase 來做到,當你想要檢查你想要的 commit 的時候,這會是最好的方法。
Select a repo