深刻理解Git - 一切皆commit

在對 git 有了基本理解和知道常規操做以後,如何對 git 的使用有進一步的理解?
一切皆 commit 或許是個不錯的理解思路。html

本文將從『一切皆 commit 』的角度,經過 git 中常見的名詞,如 commit, branch, tag, HEAD 和動詞,如 cherry-pick, rebase, reset, revert, stash 來理解 git。經過這些理解,指望可以更好地處理使用 git 中遇到的問題。git

好比:shell

  • 1 作了兩個提交的修改,而後刪掉分支了,過會發現剛纔兩個提交有價值,怎麼找回來?
  • 2 基於當前 release 分支開發功能,中途誤合併了 dev 分支,
    而後又進行了幾回提交,怎麼取消合併dev的操做?
  • 3 rebase(變基)到底是什麼意思?
    等等。

配合希沃白板課件食用,效果更佳:
【希沃白板5】課件分享 : 《Git 進階 - 從使用角度深刻理解Git》
https://r302.cc/ke8XdO?platform=enpc&channel=copylink
點擊連接直接預覽課件fetch

一切皆 commit

1 commit 的原子性

在 git 中有工做區,暫存區和代碼倉庫三個概念,那爲何要有暫存區呢?爲了保證提交的原子性,在 git 的應用層面上,提交(commit,名詞)是 git 主要命令的操做的最小單位了。翻譯

關於此,能夠查看這篇知乎貼:爲何要先 git add 才能 git commit ? - Ivony的回答 - 知乎指針

本文中的內容不多涉及工做區和暫存區的操做,有了 commit 是 git 操做的基本單位這個概念,接下來將從『一切皆 commit』來理解 git。code


2 一切皆 commit :名詞部分

2.1 本地倉庫

如上圖,其實比較好理解,咱們知道 commit 有一個 commit id,另外仍是 branch(分支),tag(標籤),HEAD(當前分支頭結點)這些概念。他們都是指向某個提交的引用(或者理解爲指針)。orm

  • branch(分支):指向你當前工做分支的最新的那個提交,當在當前分支有了新的提交,則 git 自動更新這個分支指針,以指向最新的提交。
  • tag(標籤):對某個提交或者分支打 tag 以後,將固定指向那個提交,後續即便分支有更新甚至刪除,tag 所指向的提交不變,且一直存在。
  • HEAD(頭結點):指向當前工做的分支,即 HEAD 是當前分支的一個引用,若是切換了分支,HEAD 隨之更新。

如此,便理解了,branch,tag,HEAD 這些,本質上都是指向某個提交的引用,即:一切都是 commit 。htm

2.2 遠端倉庫

有一個引用,須要單獨說明,就是 origin/branch ,一般稱之爲遠程分支,那這個遠程分支指向哪裏呢?
如何在 『一切皆commit』 這句咒語下理解遠程倉庫?對象

以 master 分支爲例,origin/master 指向的,就是當前遠端 master 分支最新的那個提交。等等,其實這句話有點小問題,應該是最後一次更新本地倉庫時,遠端 master 分支最新的那個提交。那何時會更新遠程倉庫?在執行 pull push fetch 時更新。

你或許據說過 git pull = git fetch + git merge 的說法。
當執行 git fetch 命令時,只更新 origin/master 分支(包括全部其它的 origin 遠端分支),但並不會影響本地的任何分支。

那要更新本地的 master 分支怎麼辦? git merge origin/master ,將遠端的分支合併到本地分支,即完成了對本地 master 分支的更新。因此,實際上,git pull = git fetch + git merge 。
(@master)git pull = git fetch & git merge origin/master

案例

你在 f/table 分支開發功能,如今須要合併最新dev,能夠怎麼作?

剛學 git 時,可能會這麼作:

(@f/table) git checkout dev
(@dev) git pull 
(@dev) git checkout f/table
(@f/table) git merge dev

實際上,不須要切到 dev 分支,先更新 dev,則合併。如下命令便可:

(@f/table) git fetch
(@f/table) git merge origin/dev

小結:origin/branch 是指向此分支雲端最新提交的引用(最新=最後一次更新),在執行 fetch pull push 指令時自動更新。

可使用 git show 命令查看一個提交的詳細信息,
由於 commitId/HEAD/branch/tag/origin-branch 這些都是指向一個提交,因此 show 命令後面寫任意一個均可以。
另外,還可使用其餘參數控制顯示內容,這裏不展開。

git show commitId/HEAD/branch/tag/origin-branch --format=short

3 一切皆 commit :動詞部分

3.1 cherry-pick

cherry-pick 比較好理解,就是將一個指定提交的修改摘取過來,舉例:

如圖,6 提交是增長一個有用的 helper 類(間接說明,一個 commit 最好功能獨立),但你不想將整個分支合併過來,就可使用 cherry-pick 命令。使用任何一個指向 6 提交的引用均可以。
須要說明的是,cherry-pick 過來的提交,只是內容與以前的提交同樣,他們是兩個不一樣的提交。

案例

作了兩個提交的修改,而後刪掉分支了,過會發現剛纔兩個提交有價值,怎麼找回來?

Step1 使用 git reflog 查看以前的提交歷史,找到須要找回的提交ID。

Step2 使用 cherry-pick 命令將須要的提交摘取出來便可。

如何丟失的提交比較多,除了能夠批量 cherry-pick 以外,根據實際狀況,能夠直接在那些提交的最新提交上,新建一個分支,那些提交在此以前的全部提交,都在新的分支上了。

新建分支(03620f1 指提交號/commit id):

git branch newbranch 03620f1
git checkout -b newbranch 03620f1

3.2 rebase

若是用一句話理解 rebase 的話,就是:rebase = 一連串自動的 cherry-pick 。

關於 rebase ,須要回答三個問題:

  • 爲何推薦使用 rebase 而不是 merge?
  • 爲何據說過使用 rebase 會被打?
  • 使用 rebase 有什麼問題(什麼狀況不用 rebase )?

rebase 到底是什麼意思?

如上圖,假設 dev 上的提交是 1-2-3-4-5,f/table 分支上的提交是 1-2-3-6-7。如今咱們須要合併 dev,一般,會使用 (@f/table)git merge dev 的方式合併。這裏,咱們使用 rebase 來合併 dev 。

首先,rebase 會找到 dev 和 f/table 共同的父提交,即 3 提交。而後以 dev 最新的提交爲基礎,把 f/table 分支上新的提交(這裏就是 6 和 7),逐個 cherry-pick 過來。造成新的 f/table 分支。

注意,整個過程當中,對 dev 分支不會有任何影響,由於你是在 f/table 上進行的操做。全部,rebase 的中文翻譯,變基,就能夠理解爲:變基:用 cherry-pick 的方式,給 f/table 上的新提交,換一個基,將基從以前的 3 換到了 dev 所指的提交 5 上。

問題1 爲何推薦使用 rebase 而不是 merge?

當使用 merge 時,提交歷史如右側所示,使用 rebase 的提交歷史以下側所示。
提交歷史更清晰,當分支很是多時,回溯提交與查找問題更容易。

問題2 爲何據說過使用 rebase 會被打

使用 rebase 會修改提交歷史,上面的例子中,6和7提交將不在 f/table 分支上存在,取而代之的是8和9分支,在協做分支上,若是6和7已經存在於遠端倉庫(即別人可能已經基於此有了新的修改),再將6和7移除,將帶來諸多衝突與合併的麻煩。(這是,你 push 時,也須要強推,在協做分支上強推,是很危險的行爲。)
因此:rebase只對本地未推送的commit上或本身的分支上進行。

問題3 使用 rebase 有什麼問題(什麼狀況不用 rebase )

使用 rebase 的收益:更簡潔清晰易回溯的提交歷史。

使用 rebase 的代價:逐個 cherry-pick ,若是有衝突,須要逐個解衝突,使合併變複雜。

以合併 dev 分支爲例,當工做分支已經作了大量修改(有不少提交,預期有許多衝突),或者以前 merge 過 dev。則建議使用 merge 的方式合併 dev。

rebase 小結:
rebase : 一連串的 cherry-pick。(移花接木)

3.3 reset

reset,重置,將當前分支的狀態(這裏指工做區,暫存區,代碼倉庫)重置到指定的狀態。reset 的語法以下圖,第一個參數是重置方式,後面是一個指向提交的引用(能夠是提交ID,分支,tag,HEAD~1等等)。

與 rebase 同樣,reset 只對當前分支和工做區,暫存區的數據有影響,對參數中指定的引用沒有影響。即 (@f/table)git reset --hard dev 這句命令,影響的是 f/table 分支,對 dev 沒有任何影響。

具體來看:

git reset --hard

從參數名能夠猜到,這個重置方式比較「強硬」,實際上就是,將當前分支,重置到與指定引用同樣的狀態,丟棄在這以後的提交,以及工做區和暫存區的提交。

未追蹤的文件是不受影響的,PS:git clean 命令會清除掉未追蹤的文件。

案例1

(@f/table)git reset --hard f/table~2 的含義?

當前在 f/table 分支,將其重置到 f/table~2 ,結果就是:丟棄掉 f/table 最新的兩個提交。

案例2

將當前分支重置到遠端最新 dev 的狀態,怎麼作?

(@f/table)git fetch
(@f/table)git reset --hard origin/dev
注意,這裏須要先 fetch 一下遠程倉庫,更新 origin/dev 分支。

git reset --soft / --mixed

理解了 --hard 的含義,--soft 和 --mixed 就很好理解了,這兩個參數,不會丟棄任何內容。

--soft 會將指定提交以後的提交內容,都放到 暫存區,同理,--mixed 會將指定提交以後的提交內容,以及暫存區中的內容,放到工做區。

因此,git reset --mixed HEAD (能夠簡寫爲 git reset),實現的效果就是:將暫存區中的內容,回退到工做區。
git reset --hard HEAD (能夠簡寫爲 git reset --hard),實現的效果就是:將工做區和暫存區中的所有內容。

案例1 將圖中的 2 3 4合併爲一個提交

案例2 移除誤合併

3.4 revert

reset 用於修改錯誤,一般會修改提交歷史,
這在團隊協做分支上是危險且不容許的(如不少倉庫的 master 分支)。
這時可使用 revert 命令。

revert 很好理解,就是新建一個提交,用於撤銷以前的修改。

有個問題,revert 一個 merge 提交會怎麼樣?

如圖,若是執行 (@f/table)git revert 6
會獲得相似這樣的提示:

這時,使用 -m 參數能夠指定保留那邊的提交,可選內容只有 1 和 2 (對於一般的兩兩合併的狀況而言),
1 指代當前分支的那些提交,若是不是很肯定,可使用 git show 命令查看那個合併提交,在前的那個父節點爲 1 。


留兩個思考題:
1 如何在一切皆 commit 的語境下理解 git commit --amend
2 如何在一切皆 commit 的語境下理解 git stash

後篇:
深刻理解Git - Git底層對象


原文連接: http://www.javashuo.com/article/p-zzlopvae-kq.html

END

相關文章
相關標籤/搜索