原創(chuàng) @shhp 轉(zhuǎn)載請(qǐng)注明作者和出處
這篇文章將通過場(chǎng)景分析的方式講解一些git的實(shí)用技巧。這些技巧的使用頻率或許不是很高捆蜀,但真正需要用到的時(shí)候研底,你可能會(huì)感慨相見恨晚畜吊。
場(chǎng)景一踱阿、罪魁禍?zhǔn)?/h3>
程序員小A發(fā)現(xiàn)源文件B.java中的某一行引發(fā)了一個(gè)bug」芮現(xiàn)在小A要找出是誰在什么時(shí)間提交了這次修改。那么小A應(yīng)該怎么做呢软舌?
程序員小A發(fā)現(xiàn)源文件B.java中的某一行引發(fā)了一個(gè)bug」芮現(xiàn)在小A要找出是誰在什么時(shí)間提交了這次修改。那么小A應(yīng)該怎么做呢软舌?
最直接的想法應(yīng)該是察看B.java這個(gè)文件的提交歷史才漆,在一個(gè)個(gè)commit中找出導(dǎo)致bug的修改。高級(jí)一點(diǎn)的話可以使用git bisect
進(jìn)行二分查找葫隙。其實(shí)git提供了一個(gè)簡(jiǎn)單直接的命令git blame
可以幫助我們快速地找到那個(gè)“罪魁禍?zhǔn)住薄?code>git blame命令會(huì)輸出指定文件中每一行的最后一次修改的作者栽烂、時(shí)間以及commit hash. 例如執(zhí)行git blame B.java
的輸出如下:
^87fe8e2 (little A 2016-06-18 14:21:23 +0800 1) public class B {
^87fe8e2 (little A 2016-06-18 14:21:23 +0800 2)
^87fe8e2 (little A 2016-06-18 14:21:23 +0800 3) public static void main(String[] args) {
51497e8a (someone 2016-06-18 14:22:21 +0800 4) System.out.println("It is me! Haha!");
^87fe8e2 (little A 2016-06-18 14:21:23 +0800 5) }
^87fe8e2 (little A 2016-06-18 14:21:23 +0800 6) }
可以看到someone就是那個(gè)“罪魁禍?zhǔn)住保躏仇。ê冒闪到牛鋵?shí)我們還是不知道他是誰)
場(chǎng)景二腺办、亡羊補(bǔ)牢1.0
小A剛剛提交了一個(gè)commit,但發(fā)現(xiàn)提交信息寫錯(cuò)了糟描。小A該如何修改這次的提交信息呢怀喉?
補(bǔ)救的方法有不少,這里列舉兩個(gè):
執(zhí)行命令
git commit --amend
, 在vim編輯界面修改提交信息后保存即可船响。這算是最簡(jiǎn)單的方法了躬拢。-
依次執(zhí)行
git reset HEAD~1 git commit -am "new message"
這個(gè)方法首先將當(dāng)前的working tree還原到最后一次commit之前的狀態(tài),然后再進(jìn)行一次新的commit.
場(chǎng)景三见间、亡羊補(bǔ)牢2.0
現(xiàn)在難度升級(jí)聊闯,如果小A想修改中間某個(gè)commit的提交信息,該怎么辦呢米诉?
這時(shí)可以使用強(qiáng)大的git rebase -i
命令菱蔬。輸入git rebase -i
加一個(gè)commit hash之后回車,進(jìn)入一個(gè)vim編輯界面史侣。界面上按序列出了指定commit之后的所有commit拴泌,類似下面這樣:
pick 383fd55 llal pick 1ddcdab update new.txt
pick 1519f1f update new.txt 2
pick fdc6370 delete new.txt
pick 0a8bcf8 add back new.txt
# Rebase 2aa6567..0a8bcf8 onto 2aa6567 (5 command(s))
# # Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
在每一行的開頭可以指定一個(gè)你想執(zhí)行的命令。默認(rèn)是pick
, 也就是不做任何修改【鳎現(xiàn)在我們想修改中間的某個(gè)commit信息蚪腐,只需把該commit所在行的pick
替換成reword
或者簡(jiǎn)寫r
,然后保存税朴。接下來會(huì)出現(xiàn)新的編輯界面回季,然后我們就可以修改提交信息了。
可以看到除了reword
還有其他一些命令掉房,后面會(huì)用到其中的某幾個(gè)茧跋。
場(chǎng)景四、追悔莫及
小A發(fā)現(xiàn)當(dāng)前分支test上的最后N個(gè)commit有問題卓囚,想丟棄它們瘾杭,于是執(zhí)行了
git reset --hard HEAD~N
. 過了一段時(shí)間,小A突然想起之前丟棄的N個(gè)commit里面還有一些重要的修改哪亿!那么小A有什么辦法找回丟棄的那N個(gè)commit呢粥烁?
這時(shí)可以求助于git提供的ORIG_HEAD
這個(gè)變量。在進(jìn)行pull
蝇棉、merge
或者reset
等操作后讨阻,HEAD
一般都會(huì)移動(dòng)好幾個(gè)commit。而ORIG_HEAD
就是指向這些操作之前HEAD
所指向的那個(gè)commit篡殷《鬯保可以認(rèn)為ORIG_HEAD
是git提供的一劑后悔藥。小A要找回之前丟棄的N個(gè)commit,可以這么做:
git checkout -b temp ORIG_HEAD
git checkout test
git merge temp
首先創(chuàng)建一個(gè)分支temp指向ORIG_HEAD
指向的commit奇瘦。然后切回test分支棘催,merge一下temp,就可以讓那N個(gè)commit重新回到test分支耳标。
但是醇坝,如果小A在丟棄了那幾個(gè)commit之后又做了pull
、merge
或者reset
等操作次坡,改變了ORIG_HEAD
的值呼猪,那么上面這個(gè)方法就無效了。不過辦法總比困難多砸琅,這時(shí)使用git reflog
可以解決這個(gè)問題宋距。git會(huì)記錄HEAD
的改變歷史,而git reflog
就是把這個(gè)歷史記錄輸出症脂。這個(gè)命令的輸出如下:
87fe8e2 HEAD@{0}: reset: moving to HEAD~1
d42533e HEAD@{1}: checkout: moving from master to test
d42533e HEAD@{2}: commit: another
小A只需找到執(zhí)行reset命令的那一行乡革,那么它下面一行最前面的commit hash就是“解藥”了。找到了這個(gè)commit摊腋,之后再仿照上面的步驟操作就可以了沸版。
場(chǎng)景五、挑三揀四1.0
小A對(duì)一個(gè)文件B做了多處修改兴蒸。在提交的時(shí)候發(fā)現(xiàn)有一些修改是需要排除在這次提交之外的视粮。
那么怎么做可以對(duì)同一個(gè)文件的多處修改進(jìn)行選擇性提交?
這時(shí)可以使用git add -p
命令。輸入此命令(后面可以指定文件名)后橙凳,git會(huì)對(duì)每一個(gè)修改區(qū)塊(一般來講一段連續(xù)的修改算是一個(gè)區(qū)塊)詢問需要執(zhí)行的指令:
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]?
其中各指令的含義如下:
y - 將此區(qū)塊加入index
n - 不加入index蕾殴,并跳到下一個(gè)區(qū)塊
q - 不加入index,同時(shí)結(jié)束操作
a - 將此區(qū)塊以及當(dāng)前文件剩余區(qū)塊都加入index岛啸,跳到下一個(gè)文件
d - 與a相反
g - 選擇具體的區(qū)塊進(jìn)行操作
/ - 搜索能夠匹配給定正則表達(dá)式的區(qū)塊
j - 將此區(qū)塊標(biāo)記成“暫定”钓觉,跳到下一個(gè)“暫定”的區(qū)塊
J - 將此區(qū)塊標(biāo)記成“暫定”,跳到下一個(gè)區(qū)塊
k - 將此區(qū)塊標(biāo)記成“暫定”坚踩,跳到前一個(gè)“暫定”的區(qū)塊
K - 將此區(qū)塊標(biāo)記成“暫定”荡灾,跳到前一個(gè)區(qū)塊
s - 將當(dāng)前區(qū)塊劃分成更小的區(qū)塊
e - 進(jìn)入編輯界面進(jìn)行更細(xì)的選擇
? - 打印幫助
如果選擇執(zhí)行指令e
則會(huì)進(jìn)入一個(gè)新的vim編輯頁(yè)面,如下所示:
# Manual hunk edit mode -- see bottom for a quick guide
@@ -1,3 +1,3 @@
origin
-1
-2
+abc
+xyz
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
在這個(gè)界面可以進(jìn)行更細(xì)致的操作:將某一行開頭的-
號(hào)換成空格瞬铸,該行的刪除修改則不會(huì)加入index批幌,但這個(gè)刪除的修改仍然保留;將以+
號(hào)開頭的某一行刪除嗓节,該行則不會(huì)加入index荧缘,但添加的這一行仍然保留在文件中。
場(chǎng)景六拦宣、挑三揀四2.0
如果上面那個(gè)B文件是一個(gè)Untracked file截粗,那又該如何進(jìn)行選擇性提交呢信姓?
可以先執(zhí)行git add -N B
命令。現(xiàn)在B里面的內(nèi)容都變成了unstaged的修改绸罗。之后再用上面的方法進(jìn)行選擇性提交财破。
場(chǎng)景七、萬源歸一
小A開發(fā)出了一款可以自動(dòng)編程并且提交代碼的AI工具从诲。不過有一天小A發(fā)現(xiàn)該工具的一個(gè)bug引發(fā)了一個(gè)問題:
它每修改一處就會(huì)進(jìn)行一次commit,這樣導(dǎo)致一次功能修改就提交了N多個(gè)commit靡羡。有什么辦法可以合并這些commit變成一個(gè)commit系洛?
可以再一次使用git rebase -i
. 回顧場(chǎng)景三可以看到vim編輯界面的提示中有兩條指令:
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
這兩條指令都可以把當(dāng)前的commit歸并到前一個(gè)commit中,區(qū)別就在于是否保留當(dāng)前commit的提交信息略步。