Git 中那些容易混淆和忽略的指令都做了些神馬

dzone發表於2019-02-27

整理下git中不是很清晰的一些指令的背後做了什麼,有說的不對的地方,歡迎指正。

git merge & git rebase

這節緣起於最近給公司的優秀的開源元件庫提pr,在開發說明文件中看到,推薦在提交pr前建議用git rebase 清理下commit再提pr~。恩,平時合併改動的時候git merge是老朋友,常用到,介個git rebase有聽說,但是用的不多。特此瞭解下兩者的區別,簡單記錄下吧。

首先git merge 和 git rebase 都可以合併兩個分支的commit。

git merge

舉個栗子:

git checkout feature
git merge master
複製程式碼

feature上除了合併了master分支上改動的commit外會多一個merge的commit說明這次merge,此外不會生成新的commit

git rebase

舉個栗子:

git checkout feature
git rebase master
複製程式碼

feature上的新的commit會全部重新以master最新的commit為base,效果就是feature上的新的commit就像是在最新的master上checkout出來的分支上後續新增的commit,這些commit是新生成的並且不會有那個專門說明合併的commit。【有些人會覺得這種commit汙染了commit的記錄~,但是也有人覺得這個merge commit可以幫忙理解到每次合併操作的發生的時間和合並的分支,這些資訊是有用的。恩,我都好~

引用很漂亮的圖來解釋如下【圖來自《Merging vs. Rebasing》】:

git

git

git

劃重點,其實需要注意的點在於:

  • git merge 除了merge commit外,合併到feature上的commit不是生成的新的commit。
  • git rebase 則是線性的將feature的commit接到master的最新的commit後面,以新生成的commit的形式。

這有個問題,舉個栗子:

就是git rebase的feature分支如果是一個多人開發的分支,那麼rebase的是你本地的分支,合作的小夥伴的分支和你的這個分支進行同步的話,會有一些內容重複的但是commit的hash值不一樣的多餘的commit,整個commit就冗餘不乾淨了~

因此比較好的使用git rebase的場景是單人開發的分支,或者使用一個臨時的分支進行git rebase 然後使用git merge到master分支上,merge master的時候是fast forward的,並且不會有master到feature的merge commit。

此外

  • git rebase 的互動模式,即:git rebase -i 可以進行更細粒度的rebase過程中的commit的挑選和合並,也很方便。
  • 你也可以rebase分支本身,比如3個commit之前為base進行rebase,這樣相當於整理最近的3個commit的效果。

在知道了這些重要的點後,就可以自行判斷何時應該使用git merge 還是 git rebase。git rebase還有很多用法,這裡不詳細說明,要用時再瞭解就好了。


git checkout & git reset & git revert

【本節插圖來自Resetting, Checking Out & Reverting

這三個指令都可以做到撤銷改動的作用,但是背後的行為是不一樣的,結果也有所不同,其中git checkout肯定是大家最熟悉的。這裡分別按順序介紹下三個指令的作用以及背後做了什麼。

git的本地的三個區域

git

分別稱為:

  • 工作區
  • 暫存區
  • 版本區

這三個區分別管理著git專案中的改動的不同階段的狀態。

git checkout

git checkout是工作中常見的一個操作。

commit操作

在對commit操作的時候,簡單來說就是將HEAD(HEAD我的理解是一個指向當前活躍狀態或者說當前活躍commit的一個指標,表示的是現在做出的版本狀態)移動到對應的commit,當你checkout一個分支的時候,則是將HEAD指標移至這個分支的最新的一個commit。git checkout並不會影響分支上的commit,而只是切到對應的commit的版本狀態,在切換之前,需要儲存當前的改動並且commit,因為你一旦切走了,雖然沒有改動commit的歷史,之後也可以切回來,但是checkout走了之後,你的HEAD就不指向當前分支最新的狀態了,這時候需要將改動儲存,之後最為一個commit版本來進行管理。用圖來表示如下:

git

只是改變了HEAD的指向,並沒有改動到commit的歷史,這個時候工作區和暫存區的狀態保持一致為checkout到的這個版本的狀態。

也就是說,git checkout對commit操作的時候的作用為檢視歷史版本,當然你也可以在這時候進行改動並且commit,這樣操作的結果就是,在checkout到的commit的基礎上多了一個沒有歸屬任何分支的commit。當你這時checkout回其他分支的時候,git會提醒你給這個剛才在'detached HEAD' state時新增的commit的那個改動的分叉建立一個分支,這樣方便之後切到這個狀態,而不是一個沒有branch歸屬的commit改動。

檔案操作

git checkout對檔案進行操作的時候,則是將工作區的指定的檔案或者目錄的狀態切換成指定的版本的狀態,對暫存區和版本區沒有影響。如果你git checkout一個檔案的時候預設是HEAD,產生的效果就是放棄工作區當前的改動,這個也是小夥伴們使用git checkout比較多的操作之一。

git revert

git revert只能操作commit,不能對檔案進行操作,git revert的撤銷背後的原理,就是用一次新的反向的commit將工作區、暫存區、版本區的狀態全部回退到指定的版本。也就是你的commit歷史會多一個commit,這個commit的操作就是指定的那些撤銷改動。git revert不會對歷史的commit進行改動,因此常用與公共分支的回滾。保留了commit歷史,同時也完成了版本回滾,並且其他小夥伴在同步這次的回滾的時候只是相當於fast forward了一個commit版本。用圖來表示如下:

git

git reset

git reset這個操作可以對檔案也可以對commit進行操作,git reset需要慎用,因為git reset是會改動到commit的歷史的。

檔案操作

git reset指定檔案的時候會將快取區同步到你指定的那個提交。git reset預設reset到HEAD,所以可以用來移除暫存區的指定檔案。檔案層面只支援--mixed引數,作用就是unstaged對應的暫存區中的檔案。

commit操作

git reset的撤銷操作是“真 · 撤銷”操作,git reset 將一個分支的末端指向另一個提交。這可以用來移除當前分支的一些提交。被移除的commit在下次 git 執行垃圾回收的時候會被刪除。換句話說,如果你想徹底的扔掉提交,你可以這麼做。用圖來表示如下:

git

git reset會改寫當前分支的commit的歷史,所以最好不要在公共分支上進行這個操作。git reset 操作有三個選項來指定這個操作的影響範圍或者說作用域:

  • --soft – 快取區和工作目錄都不會被改變
  • --mixed – 預設選項。快取區和你指定的提交同步,但工作目錄不受影響
  • --hard – 快取區和工作目錄都同步到你指定的提交

《git reset soft,hard,mixed之區別深解》中的解釋,我覺得挺清晰的,參考總結如下:

  • --soft引數告訴Git重置HEAD到另外一個commit,但也到此為止。所有的在original HEAD和你重置到的那個commit之間的所有變更集都放在暫存區中。

  • --hard引數將會blow out everything.它將重置HEAD返回到另外一個commit,重置暫存區以便反映版本區的變化,並且重置工作區也使得其完全匹配起來。這是一個比較危險的動作,具有破壞性,資料因此可能會丟失(makes everything matching the commit you have reset to.)。如果真是發生了資料丟失又希望找回來,那麼只有使用:git reflog命令了。

  • --mixed是reset的預設引數,也就是當你不指定任何引數時的引數。它將重置HEAD到另外一個commit,並且重置暫存區以便和版本區相匹配,但是也到此為止。工作區不會被更改,所有該branch上從original HEAD(commit)到你重置到的那個commit之間的所有變更將作為local modifications儲存在工作區中,(被標示為local modification or untracked via git status),但是並未staged的狀態,你可以重新檢視然後再做修改和commit。

總結表格

【圖表來自參考資料文章】

git


fast forward & non fast forward

在合併分支的時候git merge是老朋友,一般的小夥伴都是直接git merge 巴拉巴拉吧就解決了,這種情況下預設使用的是fast forward模式進行分支的合併。另外我們可以在git merge的時候加上--no-ff引數來切換成non fast forward模式進行分支的合併。這兩種合併方式的結果不同,適用於不同的場景。

fast forward

fast forward簡單來說,就是將分支改動的commit按照時間順序依次併入到(舉個栗子)master分支上,合併的分支和master分支是一個扁平的關係。這裡有個問題就是,commit可能和master上新增的commit穿插到一起,並且,在branch tree上,沒辦法直觀的看到一個分支的所有改動都有哪一些,因為這些改動被插入到master的commit歷史中了。

non fast forward

non fast forward簡單來說,合併分支的時候一定會多產生一個merge commit來說明這次合併的操作。並且在branch tree上被合併的分支和master分支的關係不是扁平的,是可以清晰的看到這個合併的操作合併了哪些commit。此外,non fast forward的合併的回滾也比較方便,只要revert到對應的這個merge commit就好了。不會有fast forwar的模式下回滾的時候影響的commit穿插在其他正常改動的commit中間,這時候,回滾就很難辦,可能會回滾掉正常的commit,在公共開發的分支上進行rebase整理commit歷史也不好(原因在git rebase中已經說明了),整個處理起來就比較麻煩。

用圖來解釋如下:

git


參考資料:


相關文章