在對 git 有了基本理解和知道常規操作之后,如何對 git 的使用有進一步的理解?
一切皆 commit 或許是個不錯的理解思路。
本文將從『一切皆 commit 』的角度,通過 git 中常見的名詞,如 commit, branch, tag, HEAD 和動詞,如 cherry-pick, rebase, reset, revert, stash 來理解 git。通過這些理解,期望能夠更好地處理使用 git 中遇到的問題。
比如:
- 1 做了兩個提交的修改,然后刪掉分支了,過會發現剛才兩個提交有價值,怎么找回來?
- 2 基於當前 release 分支開發功能,中途誤合並了 dev 分支,
然后又進行了幾次提交,怎么取消合並dev的操作? - 3 rebase(變基)究竟是什么意思?
等等。
配合希沃白板課件食用,效果更佳:
【希沃白板5】課件分享 : 《Git 進階 - 從使用角度深入理解Git》
https://r302.cc/ke8XdO?platform=enpc&channel=copylink
點擊鏈接直接預覽課件
一切皆 commit
1 commit 的原子性
在 git 中有工作區,暫存區和代碼倉庫三個概念,那為什么要有暫存區呢?為了保證提交的原子性,在 git 的應用層面上,提交(commit,名詞)是 git 主要命令的操作的最小單位了。
關於此,可以查看這篇知乎貼:為什么要先 git add 才能 git commit ? - Ivony的回答 - 知乎
本文中的內容很少涉及工作區和暫存區的操作,有了 commit 是 git 操作的基本單位這個概念,接下來將從『一切皆 commit』來理解 git。
2 一切皆 commit :名詞部分
2.1 本地倉庫
如上圖,其實比較好理解,我們知道 commit 有一個 commit id,另外還是 branch(分支),tag(標簽),HEAD(當前分支頭結點)這些概念。他們都是指向某個提交的引用(或者理解為指針)。
- branch(分支):指向你當前工作分支的最新的那個提交,當在當前分支有了新的提交,則 git 自動更新這個分支指針,以指向最新的提交。
- tag(標簽):對某個提交或者分支打 tag 之后,將固定指向那個提交,后續即使分支有更新甚至刪除,tag 所指向的提交不變,且一直存在。
- HEAD(頭結點):指向當前工作的分支,即 HEAD 是當前分支的一個引用,如果切換了分支,HEAD 隨之更新。
如此,便理解了,branch,tag,HEAD 這些,本質上都是指向某個提交的引用,即:一切都是 commit 。
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
原文鏈接: https://www.cnblogs.com/jasongrass/p/10582449.html
END