日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

2018(农历年)封山之作,和我一起嚼烂Git(两万字长文)

發(fā)布時間:2024/3/13 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 2018(农历年)封山之作,和我一起嚼烂Git(两万字长文) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
本文是『horseshoe·Git專題』系列文章之一,后續(xù)會有更多專題推出
GitHub地址(持續(xù)更新): https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
如果覺得對你有幫助,歡迎來GitHub點Star或者來我的博客親口告訴我

我剛開始接觸git的時候,完全搞不清楚為什么這個操作要用這個命令,而那個操作要用那個命令。

因為git不是一套注重用戶體驗的工具,git有自己的哲學(xué)。你首先要理解它的哲學(xué),才能真正理解它是如何運作的。

我也是看了前輩寫的文章才在某一刻醍醐灌頂。

git有多強大,想必大家都有所耳聞。git有多令人困惑,想必大家也親身經(jīng)歷過吧。

總而言之,學(xué)習(xí)git有兩板斧:其一,理解git的哲學(xué);其二,在復(fù)雜實踐中積累處理問題的經(jīng)驗。缺一不可。

這篇文章就是第一板斧。

作者我自己也還在路上,畢竟,這篇文章也只是我的學(xué)習(xí)心得,仍然需要大量的實踐。

寫git有多個角度,反復(fù)權(quán)衡,我最終還是決定從命令的角度鋪陳,閱讀體驗也不至于割裂。

想分章節(jié)閱讀請移步我的GitHub或者個人博客。

困難年歲,共勉。

01) add

git是一個數(shù)據(jù)庫系統(tǒng),git是一個內(nèi)容尋址文件系統(tǒng),git是一個版本管理系統(tǒng)。

沒錯,它都是。

不過我們不糾結(jié)于git是什么,我們單刀直入,介紹git命令。

要將未跟蹤的文件和已跟蹤文件的改動加入暫存區(qū),我們可以使用git add命令。

不過很多人嫌git add命令不夠語義化,畢竟這一步操作是加入暫存區(qū)呀。所以git又增加了另外一個命令git stage,它們的效果是一模一樣的。

git倉庫、工作區(qū)和暫存區(qū)

進入主題之前,我們先要介紹一下git倉庫、工作區(qū)和暫存區(qū)的概念。

git倉庫

所謂的git倉庫就是一個有.git目錄的文件夾。它是和git有關(guān)的一切故事開始的地方。

可以使用git init命令初始化一個git倉庫。

$ git init

也可以使用git clone命令從服務(wù)器上克隆倉庫到本地。

$ git clone git@github.com:veedrin/horseshoe.git

然后你的本地就有了一個和服務(wù)器上一模一樣的git倉庫。

這里要說明的是,clone操作并不是將整個倉庫下載下來,而是只下載.git目錄。因為關(guān)于git的一切秘密都在這個目錄里面,只要有了它,git就能復(fù)原到倉庫的任意版本。

工作區(qū)(working directory)

工作區(qū),又叫工作目錄,就是不包括.git目錄的項目根目錄。我們要在這個目錄下進行手頭的工作,它就是版本管理的素材庫。你甚至可以稱任何與工作有關(guān)的目錄為工作區(qū),只不過沒有.git目錄git是不認的。

暫存區(qū)(stage或者index)

stage在英文中除了有舞臺、階段之意外,還有作為動詞的準備、籌劃之意,所謂的暫存區(qū)就是一個為提交到版本庫做準備的地方。

那它為什么又被稱作index呢?因為暫存區(qū)在物理上僅僅是.git目錄下的index二進制文件。它就是一個索引文件,將工作區(qū)中的文件和暫存區(qū)中的備份一一對應(yīng)起來。

stage是表意的,index是表形的。

你可以把暫存區(qū)理解為一個豬豬儲錢罐。我們還是孩子的時候,手里有一毛錢就會丟進儲錢罐里。等到儲錢罐搖晃的聲音變的渾厚時,或者我們有一個心愿急需用錢時,我們就砸開儲錢罐,一次性花完。

類比到軟件開發(fā),每當我們寫完一個小模塊,就可以將它放入暫存區(qū)。等到一個完整的功能開發(fā)完,我們就可以從暫存區(qū)一次性提交到版本庫里。

這樣做的好處是明顯的:

  • 它可以實現(xiàn)更小顆粒度的撤銷。
  • 它可以實現(xiàn)批量提交到版本庫。

另外,添加到暫存區(qū)其實包含兩種操作。一種是將還未被git跟蹤過的文件放入暫存區(qū);一種是已經(jīng)被git跟蹤的文件,將有改動的內(nèi)容放入暫存區(qū)。

放入暫存區(qū)

git默認是不會把工作區(qū)的文件放入暫存區(qū)的。

$ git statusOn branch master No commits yet Untracked files:(use "git add <file>..." to include in what will be committed)a.md nothing added to commit but untracked files present (use "git add" to track)

我們看到文件現(xiàn)在被標注為Untracked files。表示git目前還無法追蹤它們的變化,也就是說它們還不在暫存區(qū)里。

那么我們?nèi)绾问謩訉⑽募蛭募A放入暫存區(qū)呢?

$ git add .

上面的命令表示將工作目錄所有未放入暫存區(qū)的文件都放入暫存區(qū)。這時文件的狀態(tài)已經(jīng)變成了Changes to be committed,表示文件已經(jīng)放入暫存區(qū),等待下一步提交。每一次add操作其實就是為加入的文件或內(nèi)容生成一份備份。

下面的命令也能達到相同的效果。

$ git add -A

假如我只想暫存單個文件呢?后跟相對于當前目錄的文件名即可。

$ git add README.md

暫存整個文件夾也是一樣的道理。因為git會遞歸暫存文件夾下的所有文件。

$ git add src

把從來沒有被標記過的文件放入暫存區(qū)的命令是git add,暫存區(qū)中的文件有改動也需要使用git add命令將改動放入暫存區(qū)。

這時狀態(tài)變成了Changes not staged for commit。

$ git statusOn branch master Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md no changes added to commit (use "git add" and/or "git commit -a")

針對已經(jīng)加入暫存區(qū)的文件,要將文件改動加入暫存區(qū),還有一個命令。

$ git add -a

它和git add -A命令的區(qū)別在于,它只能將已加入暫存區(qū)文件的改動放入暫存區(qū),而git add -A通吃兩種情況。

跟蹤內(nèi)容

假設(shè)我們已經(jīng)將文件加入暫存區(qū),現(xiàn)在我們往文件中添加內(nèi)容,再次放入暫存區(qū),然后查看狀態(tài)。

$ git statusOn branch master No commits yet Changes to be committed:(use "git rm --cached <file>..." to unstage)new file: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md

哎,突然變的有意思了。為什么一個文件會同時存在兩種狀態(tài),它是薛定諤的貓么?

想象一下,我想在一個文件中先修復(fù)一個bug然后增加一個feather,我肯定希望分兩次放入暫存區(qū),這樣可以實現(xiàn)顆粒度更細的撤銷和提交。但是如果git是基于文件做版本管理的,它就無法做到。

所以git只能是基于內(nèi)容做版本管理,而不是基于文件。版本管理的最小單位叫做hunk,所謂的hunk就是一段連續(xù)的改動。一個文件同時有兩種狀態(tài)也就不稀奇了。

objects

git項目的.git目錄下面有一個目錄objects,一開始這個目錄下面只有兩個空目錄:info和pack。

一旦我們執(zhí)行了git add命令,objects目錄下面就會多出一些東西。

.git/ .git/objects/ .git/objects/e6/ .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391

它多出了一個2個字符命名的目錄和一個38個字符命名的文件。加起來正好是40個字符。增加一個2個字符的目錄是為了提高檢索效率。

SHA-1是一種哈希加密算法,它的特點是只要加密的內(nèi)容相同,得到的校驗和也相同。當然這種說法是不準確的,但是碰撞的概率極低。

git除了用內(nèi)容來計算校驗和之外,還加入了一些其他信息,目的也是為了進一步降低碰撞的概率。

重點是,SHA-1算法是根據(jù)內(nèi)容來計算校驗和的,跟前面講的git跟蹤內(nèi)容相呼應(yīng)。git被稱為一個內(nèi)容尋址文件系統(tǒng)不是沒有道理的。

我們可以做個實驗。初始化本地倉庫兩次,每次都新建一個markdown文件,里面寫## git is awesome,記下完整的40個字符的校驗和,看看它們是否一樣。

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

如果你真的做了實驗,你會發(fā)現(xiàn)即便兩個文件的文件名和文件格式都不一樣,只要內(nèi)容一樣,它們的校驗和就是一樣的,并且就是上面列出的校驗和。

現(xiàn)在大家應(yīng)該對git跟蹤內(nèi)容這句話有更深的理解了。

相同內(nèi)容引用一個對象

雖然開發(fā)者要極力避免這種情況,但是如果一個倉庫有多個內(nèi)容相同的文件,git會如何處理呢?

我們初始化一個本地倉庫,新建兩個不同名的文件,但文件內(nèi)容都是## git is awesome。運行g(shù)it add .命令之后看看神秘的objects目錄下會發(fā)生什么?

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

只有一個目錄,而且校驗和跟之前一模一樣。

其實大家肯定早就想到了,git這么優(yōu)秀的工具,怎么可能會讓浪費磁盤空間的事情發(fā)生呢?既然多個文件的內(nèi)容相同,肯定只保存一個對象,讓它們引用到這里來就好了。

文件改動對應(yīng)新對象

現(xiàn)在我們猜測工作區(qū)的文件和objects目錄中的對象是一一對應(yīng)起來的。但事實真的是這樣嗎?

我們初始化一個本地倉庫,新建一個markdown文件,運行g(shù)it add .命令。現(xiàn)在objects目錄中已經(jīng)有了一個對象。然后往文件中添加內(nèi)容## git is awesome。再次運行g(shù)it add .命令。

.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 .git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

哎,objects目錄中出現(xiàn)了兩個對象。第一個對象肯定對應(yīng)空文件。第二個對象我們太熟悉了,對應(yīng)的是添加內(nèi)容后的文件。

再次強調(diào),git是一個版本管理系統(tǒng),文件在它這里不是主角,版本才是。剛才我們暫存了兩次,可以認為暫存區(qū)現(xiàn)在已經(jīng)有了兩個版本(暫存區(qū)的版本實際上是內(nèi)容備份,并不是真正的版本)。當然就需要兩個對象來保存。

文件改動全量保存

初始化一個本地倉庫,往工作區(qū)添加lodash.js未壓縮版本,版本號是4.17.11,體積大約是540KB。運行g(shù)it add .命令后objects目錄下面出現(xiàn)一個對象,體積大約是96KB。

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70

我們對lodash.js文件內(nèi)容作一個小小的改動,將版本號從4.17.11改為4.17.10,再次運行g(shù)it add .命令。然后大家會驚奇的發(fā)現(xiàn)objects目錄下有兩個對象了。驚奇的不是這個,而是第二個對象的體積也是大約96KB。

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70 .git/objects/bf/c087eec7e61f106df8f5149091b8790e6f3636

明明只改了一個數(shù)字而已,第二個對象卻還是這么大。

前面剛夸git會精打細算,怎么到這里就不知深淺了?這是因為多個文件內(nèi)容相同的情況,引用到同一個對象并不會造成查詢效率的降低,而暫存區(qū)的多個對象之間如果只保存增量的話,版本之間的查詢和切換需要花費額外的時間,這樣做是不劃算的。

但是全量保存也不是個辦法吧。然而git魚和熊掌想兼得,它也做到了。后面會講到。

重命名會拆分成刪除和新建兩個動作

初始化一個本地倉庫,新建一個文件,運行g(shù)it add .命令。然后重命名該文件,查看狀態(tài)信息。

$ git statusOn branch master No commits yet Changes to be committed:(use "git rm --cached <file>..." to unstage)new file: a.md Changes not staged for commit:(use "git add/rm <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)deleted: a.md Untracked files:(use "git add <file>..." to include in what will be committed)b.md

這是由于git的內(nèi)部機制導(dǎo)致的。生成對象的時候,它發(fā)現(xiàn)倉庫中叫這個名字的文件不見了,于是標記為已刪除,又發(fā)現(xiàn)有一個新的文件名是之前沒有標記過的,于是標記為未跟蹤。因為它只是重命名而已,文件內(nèi)容并沒有改變,所以可以共享對象,并不會影響效率。

blob對象

git的一切秘密都在.git目錄里。因為它擁有項目的完整信息,所以git一定是把備份存在了某個地方。git把它們存在了哪里,又是如何存儲它們的呢?

這些備份信息,git統(tǒng)一稱它們?yōu)閷ο蟆it總共有四種對象類型,都存在.git/objects目錄下。

這一次我們只介紹blob對象。

它存儲文件的內(nèi)容和大小。當開發(fā)者把未跟蹤的文件或跟蹤文件的改動加入暫存區(qū),就會生成若干blob對象。git會對blob對象進行zlib壓縮,以減少空間占用。

因為它只存儲內(nèi)容和大小,所以兩個文件即便文件名和格式完全不一樣,只要內(nèi)容相同,就可以共享一個blob對象。

注意blob對象和工作目錄的文件并不是一一對應(yīng)的,因為工作目錄的文件幾乎會被多次添加到暫存區(qū),這時一個文件會對應(yīng)多個blob對象。

index

倉庫的.git目錄下面有一個文件,它就是大名鼎鼎的暫存區(qū)。

是的,暫存區(qū)并不是一塊區(qū)域,只是一個文件,確切的說,是一個索引文件。

它保存了項目結(jié)構(gòu)、文件名、時間戳以及blob對象的引用。

工作區(qū)的文件和blob對象之間就是通過這個索引文件關(guān)聯(lián)起來的。

打包

還記得我們在文件改動全量保存小節(jié)里講到,git魚和熊掌想兼得么?

又想全量保存,不降低檢索和切換速度,又想盡可能壓榨體積。git是怎么做到的呢?

git會定期或者在推送到遠端之前對git對象進行打包處理。

打包的時候保存文件最新的全量版本,基于該文件的歷史版本的改動則只保存diff信息。因為開發(fā)者很少會切換到較早的版本中,所以這時候效率就可以部分犧牲。

需要注意的是,所有的git對象都會被打包,而不僅僅是blob對象。

git也有一個git gc命令可以手動執(zhí)行打包。

$ git gcCounting objects: 11, done. Delta compression using up to 4 threads. Compressing objects: 100% (9/9), done. Writing objects: 100% (11/11), done. Total 11 (delta 3), reused 0 (delta 0)

之前的git對象文件都不見了,pack文件夾多了兩個文件。其中 .pack 后綴文件存儲的就是打包前git對象文件的實際內(nèi)容。

.git/objects/ .git/objects/info/ .git/objects/info/packs .git/objects/pack/ .git/objects/pack/pack-99b4704a207ea3cc4924c9f0febb6ea45d4cdfd2.idx .git/objects/pack/pack-99b4704a207ea3cc4924c9f0febb6ea45d4cdfd2.pack

只能說,git gc的語義化不夠好。它的功能不僅僅是垃圾回收,還有打包。

02) commit

git是一個版本管理系統(tǒng)。它的終極目的就是將項目特定時間的信息保留成一個版本,以便將來的回退和查閱。

我們已經(jīng)介紹了暫存區(qū),暫存區(qū)的下一步就是版本庫,而促成這一步操作的是git commit命令。

提交

暫存區(qū)有待提交內(nèi)容的情況下,如果直接運行g(shù)it commit命令,git會跳往默認編輯器要求你輸入提交說明,你也可以自定義要跳往的編輯器。

# Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Initial commit # Changes to be committed: # new file: a.md

提交之后我們就看到這樣的信息。

[master (root-commit) 99558b4] commit for nothing1 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 a.md

如果我就是不寫提交說明呢?

Aborting commit due to empty commit message.

看到?jīng)]有,提交信息在git中時必填的。

如果提交說明不多,可以加參數(shù)-m直接在命令后面填寫提交說明。

$ git commit -m "commit for nothing"

你甚至可以將加入暫存區(qū)和提交一并做了。

$ git commit -am "commit for nothing"

但是要注意,和git add -a命令一樣,未跟蹤的文件是無法提交上去的。

重寫提交

amend翻譯成中文是修改的意思。git commit --amend命令允許你修改最近的一次commit。

$ git log --oneline8274473 (HEAD -> master) commit for nothing

目前項目提交歷史中只有一個commit。我突然想起來這次提交中有一個筆誤,我把高圓圓寫成了高曉松(真的是筆誤)。但是呢,我又不想為了這個筆誤增加一個commit,畢竟它僅僅是一個小小的筆誤而已。最重要的是我想悄無聲息的改正它,以免被別人笑話。

這時我就可以使用git commit --amend命令。

首先修改高曉松成高圓圓。

然后執(zhí)行g(shù)it add a.md命令。

最后重寫提交。git會跳往默認或者自定義編輯器提示你修改commit說明。當然你也可以不改。

$ git commit --amendcommit for nothing # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # Date: Thu Jan 3 09:33:56 2019 +0800 # On branch master # Initial commit # Changes to be committed: # new file: a.md

我們再來看提交歷史。

$ git log --oneline8a71ae1 (HEAD -> master) commit for nothing

提交歷史中同樣只有一個commit。但是注意喲,commit已經(jīng)不是之前的那個commit了,它們的校驗和是不一樣的。這就是所謂的重寫。

tree對象和commit對象

commit操作涉及到兩個git對象。

第一是tree對象。

它存儲子目錄和子文件的引用。如果只有blob對象,那版本庫將是一團散沙。正因為有tree對象將它們的關(guān)系登記在冊,才能構(gòu)成一個有結(jié)構(gòu)的版本庫。

添加到暫存區(qū)操作并不會生成tree對象,這時項目的結(jié)構(gòu)信息存儲在index文件中,直到提交版本庫操作,才會為每一個目錄分別生成tree對象。

第二是commit對象。

它存儲每個提交的信息,包括當前提交的根tree對象的引用,父commit對象的引用,作者和提交者,還有提交信息。所謂的版本,其實指的就是這個commit對象。

作者和提交者通常是一個人,但也存在不同人的情況。

objects

初始化一個git項目,新建一些文件和目錄。

src/ src/a.md lib/ lib/b.md

首先運行g(shù)it add命令。我們清楚,這會在.git/objects目錄下生成一個blob對象,因為目前兩個文件都是空文件,共享一個blob對象。

.git/objects/info/ .git/objects/pack/ .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391

現(xiàn)在我們運行g(shù)it commit命令,看看有什么變化。

.git/objects/info/ .git/objects/pack/ .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 .git/objects/93/810bbde0f994d41ef550324a2c1ad5f9278e19 .git/objects/52/0c9f9f61657ca1e65a288ea77d229a27a8171b .git/objects/0b/785fa11cd93f95b1cab8b9cbab188edc7e04df .git/objects/49/11ff67189d8d5cc2f94904fdd398fc16410d56

有意思。剛剛只有一個blob對象,怎么突然蹦出來這么多git對象呢?想一想之前說的commit操作涉及到兩個git對象這句話,有沒有可能多出來的幾個,分別是tree對象和commit對象?

我們使用git底層命令git cat-file -t <commit>查看這些對象的類型發(fā)現(xiàn),其中有一個blob對象,三個tree對象,一個commit對象。

這是第一個tree對象。

$ git cat-file -t 93810bbtree $ git cat-file -p 93810bb100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b.md

這是第二個tree對象。

$ git cat-file -t 520c9f9tree $ git cat-file -p 520c9f9100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a.md

這是第三個tree對象。

$ git cat-file -t 0b785fatree $ git cat-file -p 0b785fa040000 tree 93810bbde0f994d41ef550324a2c1ad5f9278e19 lib 040000 tree 520c9f9f61657ca1e65a288ea77d229a27a8171b src

可以看到,提交時每個目錄都會生成對應(yīng)的tree對象。

然后我們再來看commit對象。

$ git cat-file -t 4911ff6commit $ git cat-file -p 4911ff6tree 0b785fa11cd93f95b1cab8b9cbab188edc7e04df parent c4731cfab38f036c04de93facf07cae496a124a2 author veedrin <veedrin@qq.com> 1546395770 +0800 committer veedrin <veedrin@qq.com> 1546395770 +0800 commit for nothing

可以看到,commit會關(guān)聯(lián)根目錄的tree對象,因為關(guān)聯(lián)它就可以關(guān)聯(lián)到所有的項目結(jié)構(gòu)信息,所謂擒賊先擒王嘛。它也要關(guān)聯(lián)父commit,也就是它的上一個commit,這樣才能組成版本歷史。當然,如果是第一個commit那就沒有父commit了。然后就是commit說明和一些參與者信息。

我們總結(jié)一下,git add命令會為加入暫存區(qū)的內(nèi)容或文件生成blob對象,git commit命令會為加入版本庫的內(nèi)容或文件生成tree對象和commit對象。至此,四種git對象我們見識了三種。

為啥不在git add的時候就生成tree對象呢?

所謂暫存區(qū),就是不一定會保存為版本的信息,只是一個準備的臨時場所。git認為在git add的時候生成tree對象是不夠高效的,完全可以等版本定型時再生成。而版本定型之前的結(jié)構(gòu)信息存在index文件中就好了。

03) branch

分支是使得git如此靈活的強大武器,正是因為有巧妙的分支設(shè)計,眾多的git工作流才成為可能。

現(xiàn)在我們已經(jīng)知道commit對象其實就是git中的版本。那我們要在版本之間切換難道只能通過指定commit對象毫無意義的SHA-1值嗎?

當然不是。

在git中,我們可以通過將一些指針指向commit對象來方便操作,這些指針便是分支。

分支在git中是一個模棱兩可的概念。

你可以認為它僅僅是一個指針,指向一個commit對象節(jié)點。

你也可以認為它是指針指向的commit對象節(jié)點追溯到某個交叉節(jié)點之間的commit歷史。

嚴格的來說,一種叫分支指針,一種叫分支歷史。不過實際使用中,它們在名字上常常不作區(qū)分。

所以我們需要意會文字背后的意思,它究竟說的是分支指針還是分支歷史。

大多數(shù)時候,它指的都是分支指針。

master分支

剛剛初始化的git倉庫,會發(fā)現(xiàn).git/refs/heads目錄下面是空的。這是因為目前版本庫里還沒有任何commit對象,而分支一定是指向commit對象的。

一旦版本庫里有了第一個commit對象,git都會在.git/refs/heads目錄下面自動生成一個master文件,它就是git的默認分支。不過它并不特殊,只是它充當?shù)氖且粋€默認角色而已。

剛剛初始化的git倉庫會顯示目前在master分支上,其實這個master分支是假的,.git/refs/heads目錄下根本沒有這個文件。只有等提交歷史不為空時才有會真正的默認分支。

我們看一下master文件到底有什么。

$ cat .git/refs/heads/master6b5a94158cc141286ac98f30bb189b8a83d61347

40個字符,明顯是某個git對象的引用。再識別一下它的類型,發(fā)現(xiàn)是一個commit對象。

$ git cat-file -t 6b5a941commit

就這么簡單,所謂的分支(分支指針)就是一個指向某個commit對象的指針。

HEAD指針

形象的講,HEAD就是景區(qū)地圖上標注你當前在哪里的一個圖標。

你當前在哪里,HEAD就在哪里。它一般指向某個分支,因為一般我們都會在某個分支之上。

因為HEAD是用來標注當前位置的,所以一旦HEAD的位置被改變,工作目錄就會切換到HEAD指向的分支。

$ git log --onelinef53aaa7 (HEAD -> master) commit for nothing

但是也有例外,比如我直接簽出到某個沒有分支引用的commit。

$ git log --onelinecb64064 (HEAD -> master) commit for nothing again 324a3c0 commit for nothing $ git checkout 324a3c0Note: checking out '324a3c0'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:git checkout -b <new-branch-name> HEAD is now at 324a3c0... commit for nothing $ git log --oneline324a3c0 commit for nothing

這個時候的HEAD就叫做detached HEAD。

要知道,只有在初始提交和某個分支之間的commit才是有效的。當你的HEAD處于detached HEAD狀態(tài)時,在它之上新建的commit沒有被任何分支包裹。一旦你切換到別的分支,這個commit(可能)再也不會被引用到,最終會被垃圾回收機制刪除。因此這是很危險的操作。

324a3c0 -- cb64064(master)\3899a24(HEAD)

如果不小心這么做了,要么在原地新建一個分支,要么將已有的分支強行移動過來。確保它不會被遺忘。

死亡不是終結(jié),遺忘才是。——尋夢環(huán)游記

創(chuàng)建

除了默認的master分支,我們可以隨意創(chuàng)建新的分支。

$ git branch dev

一個dev分支就創(chuàng)建好了。

查看

或許有時我們也想要查看本地倉庫有多少個分支,因為在git中新建分支實在是太容易了。

$ git branchdev * master

當前分支的前面會有一個*號標注。

同時查看本地分支和遠端分支引用,添加-a參數(shù)。

$ git branch -a* masterremotes/origin/HEAD -> origin/masterremotes/origin/master

刪除

一般分支合并完之后就不再需要了,這時就要將它刪除。

$ git branch -d devDeleted branch dev (was 657142d).

有時候我們會得到不一樣的提示。

$ git branch -d deverror: The branch 'dev' is not fully merged. If you are sure you want to delete it, run 'git branch -D dev'.

這是git的一種保護措施。is not fully merged是針對當前分支來說的,意思是你要刪除的分支還有內(nèi)容沒有合并進當前分支,你確定要刪除它嗎?

大多數(shù)時候,當然是要的。

$ git branch -D devDeleted branch dev (was 657142d).

-D是--delete --force的縮寫,你也可以寫成-df。

需要注意的是,刪除分支僅僅是刪除一個指針而已,并不會刪除對應(yīng)的commit對象。不過有可能刪除分支以后,這一串commit對象就無法再被引用了,從而被垃圾回收機制刪除。

04) checkout

在git中,暫存區(qū)里有若干備份,版本庫里有若干版本。留著這些東西肯定是拿來用的對吧,怎么用呢?當我需要哪一份的時候我就切換到哪一份。

git checkout命令就是用來干這個的,官方術(shù)語叫做簽出。

怎么理解checkout這個詞呢?checkout原本指的是消費結(jié)束服務(wù)員要與你核對一下賬單,結(jié)完賬之后你就可以走了。在git中核對指的是diff,比較兩份版本的差異,如果發(fā)現(xiàn)沒有沖突那就可以切換過來了。

底層

我們知道HEAD指針指向當前版本,而git checkout命令的作用是切換版本,它們肯定有所關(guān)聯(lián)。

目前HEAD指針指向master分支。

$ cat .git/HEADref: refs/heads/master

如果我切換到另一個分支,會發(fā)生什么?

$ git checkout devSwitched to branch 'dev' $ cat .git/HEADref: refs/heads/dev

果然,git checkout命令的原理就是改變了HEAD指針。而一旦HEAD指針改變,git就會取出HEAD指針指向的版本作為當前工作目錄的版本。簽出到一個沒有分支引用的commit也是一樣的。

符號

在進入正題之前,我們要先聊聊git中的兩個符號~和^。

如果我們要從一個分支切換到另一個分支,那還好說,足夠語義化。但是如果我們要切換到某個commit,除了兢兢業(yè)業(yè)的找到它的SHA-1值,還有什么辦法快速的引用到它呢?

比如說我們可以根據(jù)commit之間的譜系關(guān)系快速定位。

$ git log --graph --oneline* 4e76510 (HEAD -> master) c4 * 2ec8374 c3 |\ | * 7c0a8e3 c2 * | fb60f51 c1 |/ * dc96a29 c0 ~的作用是在縱向上定位。它可以一直追溯到最早的祖先commit。如果commit歷史有分叉,那它就選第一個,也就是主干上的那個。

^的作用是在橫向上定位。它無法向上追溯,但是如果commit歷史有分叉,它能定位所有分叉中的任意一支。

HEAD不加任何符號、加~0 符號或者加^0符號時,定位的都是當前版本

這個不用說,定位當前commit。

$ git rev-parse HEAD4e76510fe8bb3c69de12068ab354ef37bba6da9d

它表示定位第零代父commit,也就是當前commit。

$ git rev-parse HEAD~04e76510fe8bb3c69de12068ab354ef37bba6da9d

它表示定位當前commit的第零個父commit,也就是當前commit。

$ git rev-parse HEAD^04e76510fe8bb3c69de12068ab354ef37bba6da9d

用~符號數(shù)量的堆砌或者~數(shù)量的寫法定位第幾代父commit

$ git rev-parse HEAD~~fb60f519a59e9ceeef039f7efd2a8439aa7efd4b $ git rev-parse HEAD~2fb60f519a59e9ceeef039f7efd2a8439aa7efd4b

用^數(shù)量的寫法定位第幾個父commit

注意,^定位的是當前基礎(chǔ)的父commit。

$ git rev-parse HEAD^2ec837440051af433677f786e502d1f6cdeb0a4a $ git rev-parse HEAD^12ec837440051af433677f786e502d1f6cdeb0a4a

因為當前commit只有一個父commit,所以定位第二個父commit會失敗。

$ git rev-parse HEAD^2HEAD^2 fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]'

用~數(shù)量^數(shù)量的寫法或者^數(shù)量^數(shù)量的寫法定位第幾代父commit的第幾個父commit

當前commit的第一代父commit的第零個父commit,意思就是第一代父commit咯。

$ git rev-parse HEAD~^02ec837440051af433677f786e502d1f6cdeb0a4a

比如這里定位的是當前commit的第一代父commit的第一個父commit。再次注意,^定位的是當前基礎(chǔ)的父commit。

$ git rev-parse HEAD~^1fb60f519a59e9ceeef039f7efd2a8439aa7efd4b

這里定位的是當前commit的第一代父commit的第二個父commit。

$ git rev-parse HEAD~^27c0a8e3a325ce1b5a1cdeb8c89bef1ecf17c10c9

同樣,定位到一個不存在的commit會失敗。

$ git rev-parse HEAD~^3HEAD~^3 fatal: ambiguous argument 'HEAD~^3': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]'

和~不同,^2和^^的效果是不一樣的。^2指的是第二個父commit,^^指的是第一個父commit的第一個父commit。

切換到HEAD

git checkout命令如果不帶任何參數(shù),默認會加上HEAD參數(shù)。而HEAD指針指向的就是當前commit。所以它并不會有任何簽出動作。

前面沒有提到的是,git checkout命令會有一個順帶效果:比較簽出后的版本和暫存區(qū)之間的差異。

所以git checkout命令不帶任何參數(shù),意思就是比較當前commit和暫存區(qū)之間的差異。

$ git checkoutA b.md $ git checkout HEADA b.md

切換到commit

開發(fā)者用的最多的當然是切換分支。其實checkout后面不僅可以跟分支名,也可以跟commit的校驗和,還可以用符號定位commit。

$ git checkout devSwitched to branch 'dev' $ git checkout acb71feNote: checking out 'acb71fe11f78d230b860692ea6648906153f3d27'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:git checkout -b <new-branch-name> HEAD is now at acb71fe... null $ git checkout HEAD~2Note: checking out 'acb71fe11f78d230b860692ea6648906153f3d27'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:git checkout -b <new-branch-name> HEAD is now at acb71fe... null

創(chuàng)建分支并切換

有時候我們在創(chuàng)建分支時希望同時切換到創(chuàng)建后的分支,僅僅git branch <branch>是做不到的。這時git checkout命令可以提供一個快捷操作,創(chuàng)建分支和切換分支一步到位。

$ git checkout -b devSwitched to a new branch 'dev'

暫存區(qū)文件覆蓋工作區(qū)文件

git checkout不僅可以執(zhí)行切換commit這種全量切換,它還能以文件為單位執(zhí)行微觀切換。

$ git statusOn branch master No commits yet Changes to be committed:(use "git rm --cached <file>..." to unstage)new file: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git checkout -- a.md $ git statusOn branch master No commits yet Changes to be committed:(use "git rm --cached <file>..." to unstage)new file: a.md

因為暫存區(qū)覆蓋了工作區(qū),所以工作區(qū)的改動就被撤銷了,現(xiàn)在只剩下暫存區(qū)的改動等待提交。其實相當于撤銷文件在工作區(qū)的改動,只不過它的語義是覆蓋。這個命令沒有任何提示,直接撤銷工作區(qū)改動,要謹慎使用。

我們看到git提示語中有一個git checkout -- <file>命令,這又是干嘛用的呢?

提醒一下,這個參數(shù)的寫法不是git checkout --<file>,而是git checkout -- <file>。

其實它和git checkout <file>的效果是一樣的。但是別急,我是說這兩個命令想要達到的效果是一樣的,但實際效果卻有略微的差別。

獨立的--參數(shù)在Linux命令行中指的是:視后面的參數(shù)為文件名。當后面跟的是文件名的時候,最好加上獨立的--參數(shù),以免有歧義。

也就是說,如果該項目正好有一個分支名為a.md(皮一下也不是不行對吧),那加獨立的--參數(shù)就不會操作分支,而是操作文件。

如果你覺得僅僅撤銷一個文件在工作區(qū)的改動不過癮,你不是針對誰,你是覺得工作區(qū)的改動都是垃圾。那么還有一個更危險的命令。

$ git checkout -- .

.代表當前目錄下的所有文件和子目錄。這條命令會撤銷所有工作區(qū)的改動。

當前commit文件覆蓋暫存區(qū)文件和工作區(qū)文件

如果執(zhí)行g(shù)it checkout -- <file>的時候加上一個分支名或者commit的校驗和,效果就是該文件的當前版本會同時覆蓋暫存區(qū)和工作區(qū)。相當于同時撤銷文件在暫存區(qū)和工作區(qū)的改動。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git checkout HEAD -- a.md $ git statusOn branch master nothing to commit, working tree clean

最后再提醒一下,運行g(shù)it checkout命令作用于文件時,即便覆蓋內(nèi)容與被覆蓋內(nèi)容有沖突,也會直接覆蓋,所以這真的是悶聲打雷式的git命令,一定要抽自己幾個耳刮子方可放心食用。

05) merge

可以方便的創(chuàng)建分支是git如此受歡迎的重要原因,利用git checkout <branch>也讓開發(fā)者在分支之間穿梭自如。然而百川終入海,其他分支上完成的工作終究是要合并到主分支上去的。

所以我們來看看git中的合并操作。

首先說明,執(zhí)行g(shù)it merge命令之前需要一些準備工作。

$ git merge deverror: Your local changes to the following files would be overwritten by merge:a.md Please commit your changes or stash them before you merge. Aborting

合并操作之前必須保證暫存區(qū)內(nèi)沒有待提交內(nèi)容,否則git會阻止合并。這是因為合并之后,git會將合并后的版本覆蓋暫存區(qū)。所以會有丟失工作成果的危險。

至于工作區(qū)有待添加到暫存區(qū)的內(nèi)容,git倒不會阻止你。可能git覺得它不重要吧。

不過最好還是保持一個干凈的工作區(qū)再執(zhí)行合并操作。

不同分支的合并

不同分支指的是要合并的兩個commit在某個祖先commit之后開始分叉。

C0 -- C1 -- C2(HEAD -> master)\C3(dev)

git merge后跟合并客體,表示要將它合并進來。

$ git merge dev

進行到這里,如果沒有沖突,git會彈出默認或者自定義的編輯器,讓你填寫commit說明。當然它會給你填寫一個默認的commit說明。

Merge branch 'dev'# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.

為什么要你填寫commit說明?因為這種情況的git merge實際上會創(chuàng)建一個新的commit對象,記錄此次合并的信息,并將當前分支指針移動到它上面來。

C0 -- C1 -- C2 -- C4(HEAD -> master)(merge commit)\ /\ /C3(dev)

大家常說不同分支的git merge操作是一個三方合并,這里的三方指的是合并主體commit、合并客體commit以及合并主客體的共同祖先commit。

所謂的三方和并到底是什么意思呢?

git會提取出合并主體commit相對于合并主客體的共同祖先commit的diff與合并客體commit相對于合并主客體的共同祖先commit的diff,再去比較這兩份diff有沒有修改同一個地方,這里同一個地方的單位是文件的行。如果沒有,那就將這兩份diff合并生成一個新的commit,當前分支指針向右移。如果有那就要求開發(fā)者自行解決。

所以在三方合并中,合并主客體的共同祖先commit只是一個參照物。

合并主體在合并客體的上游

它指的是開發(fā)者當前在一個commit節(jié)點上,要將同一個分支上更新的commit節(jié)點合并進來。

C0 -- C1 -- C2(HEAD -> master) -- C3(dev)

這時候會發(fā)生什么呢?

這相當于更新當前分支指針,所以只需要將當前分支指針向下游移動,讓合并主體與合并客體指向同一個commit即可。這時并不會產(chǎn)生一個新的commit。

用三方合并的概念來理解,合并主體commit與合并主客體的共同祖先commit是同一個commit,合并主體commit相對于合并主客體的共同祖先commit的diff為空,合并客體commit相對于合并主客體的共同祖先commit的diff與空diff合并還是它自己,所以移動過去就行了,并不需要生成一個新的commit。

$ git merge devUpdating 9242078..631ef3a Fast-forwarda.md | 2 ++1 file changed, 2 insertions(+) C0 -- C1 -- C2 -- C3(HEAD -> master, dev)

這種操作在git中有一個專有名詞,叫Fast forward。

比如說git pull的時候經(jīng)常發(fā)生這種情況。通常因為遠端有更新的commit我們才需要執(zhí)行g(shù)it pull命令,這時遠端就是合并客體,本地就是合并主體,遠端的分支指針在下游,也會觸發(fā)Fast forward。

合并主體在合并客體的下游

如果合并主體在合并客體的下游,那合并主體本身就包含合并客體,合并操作并不會產(chǎn)生任何效果。

C0 -- C1 -- C2(dev) -- C3(HEAD -> master) $ git merge devAlready up to date. C0 -- C1 -- C2(dev) -- C3(HEAD -> master)

依然用三方合并的概念來理解,這時合并客體commit與合并主客體的共同祖先commit是同一個commit,合并客體commit相對于合并主客體的共同祖先commit的diff為空,合并主體commit相對于合并主客體的共同祖先commit的diff與空diff合并還是它自己。但是這回它都不用移動,因為合并后的diff就是它自己原有的diff。

注意,這時候dev分支指針會不會動呢?

當然不會,git merge操作對合并客體是沒有任何影響的。

同時合并多個客體

如果你在git merge后面跟不止一個分支,這意味著你想同時將它們合并進當前分支。

$ git merge aaa bbb cccFast-forwarding to: aaa Trying simple merge with bbb Trying simple merge with ccc Merge made by the 'octopus' strategy.aaa.md | 0bbb.md | 0ccc.md | 03 files changed, 0 insertions(+), 0 deletions(-)create mode 100644 aaa.mdcreate mode 100644 bbb.mdcreate mode 100644 ccc.md

git合并有多種策略,上面使用的是'octopus' strategy章魚策略,因為同時合并的多個分支最終都會指向新的commit,看起來像章魚的觸手。

合并有沖突

git merge操作并不總是如此順利的。因為有時候要合并的兩個分支不是同一個人的,就會有很大的概率遇到兩人同時修改文件某一行的情況。git不知道該用誰的版本,它認為兩個分支遇到了沖突。

這時就需要開發(fā)者手動的解決沖突,才能讓git繼續(xù)合并。

$ git merge devAuto-merging a.md CONFLICT (content): Merge conflict in a.md Automatic merge failed; fix conflicts and then commit the result.

我們來看一下有沖突的文件是什么樣的。

<<<<<<< HEAD apple ======= banana >>>>>>> dev

運行g(shù)it status命令。

$ git statusOn branch master You have unmerged paths.(fix conflicts and run "git commit")(use "git merge --abort" to abort the merge) Unmerged paths:(use "git add <file>..." to mark resolution)both modified: a.md no changes added to commit (use "git add" and/or "git commit -a")

解決完沖突之后,你需要再提交,告訴git可以完成合并了。

$ git commit -m "fix merge conflict"U a.md error: Committing is not possible because you have unmerged files. hint: Fix them up in the work tree, and then use 'git add/rm <file>' hint: as appropriate to mark resolution and make a commit. fatal: Exiting because of an unresolved conflict.

誒,被拒絕了。是不是想起了自己的情場故事?

當我們解決沖突的時候,工作區(qū)已經(jīng)有改動,所以需要先提交到暫存區(qū)。

$ git add a.md $ git commit -m "fix merge conflict"[master 9b32d4d] fix merge conflict

運行g(shù)it add 命令之后你也可以用git merge --continue來替代git commit命令。它會讓后面的行為跟沒有沖突時的行為表現(xiàn)的一樣。

如果你遇到?jīng)_突以后不知道如何解決,因為你要去詢問你的合作伙伴為什么這樣改。這時你肯定想回到合并以前的狀態(tài)。

這對git來說很容易。只需要運行g(shù)it merge --abort命令即可。

$ git merge --abort

該命令無法保證恢復(fù)工作區(qū)的修改,所以最好是在合并之前先讓工作區(qū)保持干凈。

06) rebase

git merge命令會生成一個新的合并commit。如果你有強迫癥,不喜歡這個新的合并commit,git也有更加清爽的方案可以滿足你,它就是git rebase命令。

git就是哆啦A夢的口袋。

rebase翻譯過來是變基。意思就是將所有要合并進來的commit在新的基礎(chǔ)上重新提交一次。

基礎(chǔ)用法

git rebase <branch>會計算當前分支和目標分支的最近共同祖先,然后將最近共同祖先與當前分支之間的所有commit都變基到目標分支上,使得提交歷史變成一條直線。

C0 -- C1 -- C2 -- C3(master)\C4 -- C5 -- C6(HEAD -> dev)

merge與rebase后跟的分支名是不一樣的。合并是合并進來,變基是變基過去,你們感受一下。

$ git rebase masterFirst, rewinding head to replay your work on top of it... Applying: C4.md Applying: C5.md Applying: C6.md C0 -- C1 -- C2 -- C3(master) -- C4' -- C5' -- C6'(HEAD -> dev)\C4 -- C5 -- C6

現(xiàn)在最近共同祖先與當前分支之間的所有commit都被復(fù)制到master分支之后,并且將HEAD指針與當前分支指針切換過去。這招移花接木玩的很溜啊,如果你置身其中根本分不出區(qū)別。

原來的commit還在嗎?還在,如果你記得它的commit校驗和,仍然可以切換過去,git會提示你當前處于detached HEAD狀態(tài)下。只不過沒有任何分支指針指向它們,它們已經(jīng)被拋棄了,剩余的時光就是等待git垃圾回收命令清理它們。

好在,還有人記得它們,不是么?

git rebase完并沒有結(jié)束,因為我變基的目標分支是master,而當前分支是dev。我需要切換到master分支上,然后再合并一次。

$ git checkout master $ git merge dev

誒,說來說去,還是要合并啊?

別急,這種合并是Fast forward的,并不會生成一個新的合并commit。

如果我要變基的本體分支不是當前分支行不行?也是可以的。

$ git rebase master dev

你在任何一個分支上,這種寫法都可以將dev分支變基到master分支上,變基完成當前分支會變成dev分支。

裁剪commit變基

變基有點像基因編輯,git有更精確的工具達到你想要的效果。

有了精確的基因編輯技術(shù),媽媽再也不用擔(dān)心你長的 啦。 C0 -- C1 -- C2 -- C3(master)\C4 -- C5 -- C6(dev)\C7 -- C8(HEAD -> hotfix) $ git rebase --onto master dev hotfixFirst, rewinding head to replay your work on top of it... Applying: C7.md Applying: C8.md C0 -- C1 -- C2 -- C3(master) -- C7' -- C8'(HEAD -> hotfix)\C4 -- C5 -- C6(dev)\C7 -- C8

--onto參數(shù)就是那把基因編輯的剪刀。

它會把hotfix分支到hotfix分支與dev分支的最近共同祖先之間的commit裁剪下來,復(fù)制到目標基礎(chǔ)點上。注意,所謂的之間指的都是不包括最近共同祖先commit的范圍,比如這里就不會復(fù)制C4commit。

$ git rebase --onto master devFirst, rewinding head to replay your work on top of it... Applying: C7.md Applying: C8.md

如果--onto后面只寫兩個分支(或者commit)名,第三個分支(或者commit)默認就是HEAD指針指向的分支(或者commit)。

變基沖突解決

變基也會存在沖突的情況,我們看看沖突怎么解決。

C0 -- C1 -- C2(HEAD -> master)\C3 -- C4(dev) $ git rebase master devFirst, rewinding head to replay your work on top of it... Applying: c.md Applying: a.md add banana Using index info to reconstruct a base tree... M a.md Falling back to patching base and 3-way merge... Auto-merging a.md CONFLICT (content): Merge conflict in a.md error: Failed to merge in the changes. Patch failed at 0002 a.md dev The copy of the patch that failed is found in: .git/rebase-apply/patch Resolve all conflicts manually, mark them as resolved with "git add/rm <conflicted_files>", then run "git rebase --continue". You can instead skip this commit: run "git rebase --skip". To abort and get back to the state before "git rebase", run "git rebase --abort".

C2和C4同時修改了a.md的某一行,引發(fā)沖突。git已經(jīng)給我們提示了,大體上和merge的操作一致。

我們可以手動解決沖突,然后執(zhí)行g(shù)it add和git rebase --continue來完成變基。

如果你不想覆蓋目標commit的內(nèi)容,也可以跳過這個commit,執(zhí)行g(shù)it rebase --skip。但是注意,這會跳過有沖突的整個commit,而不僅僅是有沖突的部分。

后悔藥也是有的,執(zhí)行g(shù)it rebase --abort,干脆就放棄變基了。

cherry-pick

git rebase --onto命令可以裁剪分支以變基到另一個分支上。但它依然是挑選連續(xù)的一段commit,只是允許你指定頭和尾罷了。

別急,git cherry-pick命令雖然是一個獨立的git命令,它的效果卻還是變基,而且是commit級別的變基。

git cherry-pick命令可以挑選任意commit變基到目標commit上。你負責(zé)挑,它負責(zé)基。

用法

只需要在git cherry-pick命令后跟commit校驗和,就可以將它應(yīng)用到目標commit上。

C0 -- C1 -- C2(HEAD -> master)\C3 -- C4 -- C5(dev)\C6 -- C7(hotfix)

將當前分支切換到master分支。

$ git cherry-pick C6[master dc342e0] c6Date: Mon Dec 24 09:13:57 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c6.md C0 -- C1 -- C2 -- C6'(HEAD -> master)\C3 -- C4 -- C5(dev)\C6 -- C7(hotfix)

C6commit就按原樣重新提交到master分支上了。cherry-pick并不會修改原有的commit。

同時挑選多個commit也很方便,往后面疊加就行。

$ git cherry-pick C4 C7[master ab1e7c7] c4Date: Mon Dec 24 09:12:58 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c4.md [master 161d993] c7Date: Mon Dec 24 09:14:12 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c7.md C0 -- C1 -- C2 -- C4' -- C7'(HEAD -> master)\C3 -- C4 -- C5(dev)\C6 -- C7(hotfix)

如果這多個commit正好是連續(xù)的呢?

$ git cherry-pick C3...C7[master d16c42e] c4Date: Mon Dec 24 09:12:58 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c4.md [master d16c42e] c6Date: Mon Dec 24 09:13:57 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c6.md [master a4d5976] c7Date: Mon Dec 24 09:14:12 2018 +08001 file changed, 0 insertions(+), 0 deletions(-)create mode 100644 c7.md C0 -- C1 -- C2 -- C4' -- C6' -- C7'(HEAD -> master)\C3 -- C4 -- C5(dev)\C6 -- C7(hotfix)

需要注意,git所謂的從某某開始,一般都是不包括某某的,這里也一樣。

有沒有發(fā)現(xiàn)操作連續(xù)commit的git cherry-pick和git rebase的功能已經(jīng)非常接近了?所以呀,git cherry-pick也是變基,只不過一邊變基一邊喂櫻桃給你吃。

沖突

git各種命令解決沖突的方法都大同小異。

C0 -- C1(HEAD -> master)\C2(dev) $ git cherry-pick C2error: could not apply 051c24c... banana hint: after resolving the conflicts, mark the corrected paths hint: with 'git add <paths>' or 'git rm <paths>' hint: and commit the result with 'git commit'

手動解決沖突,執(zhí)行g(shù)it add命令然后執(zhí)行g(shù)it cherry-pick --continue命令。

如果被唬住了想還原,執(zhí)行g(shù)it cherry-pick --abort即可。

變基還是合并

這是一個哲學(xué)問題。

有一種觀點認為,倉庫的commit歷史應(yīng)該記錄實際發(fā)生過什么。所以如果你將一個分支合并進另一個分支,commit歷史中就應(yīng)該有這一次合并的痕跡,因為它是實實在在發(fā)生過的。

另一種觀點則認為,倉庫的commit歷史應(yīng)該記錄項目過程中發(fā)生過什么。合并不是項目開發(fā)本身帶來的,它是一種額外的操作,會使commit歷史變的冗長。

我是一個極簡主義者,所以我支持首選變基。

07) reset

git checkout命令可以在版本之間隨意切換,它的本質(zhì)是移動HEAD指針。

那git有沒有辦法移動分支指針呢?

當然有,這就是git reset命令。

底層

git reset命令與git checkout命令的區(qū)別在于,它會把HEAD指針和分支指針一起移動,如果HEAD指針指向的是一個分支指針的話。

我們前面說過使用git checkout命令從有分支指向的commit切換到一個沒有分支指向的commit上,這個時候的HEAD指針被稱為detached HEAD。這是非常危險的。

C0 -- C1 -- C2(HEAD -> master) $ git checkout C1 C0 -- C1(HEAD) -- C2(master)

但是git reset命令沒有這個問題,因為它會把當前的分支指針也帶過去。

C0 -- C1 -- C2(HEAD -> master) $ git reset C1 C0 -- C1(HEAD -> master) -- C2

這就是重置的含義所在。它可以重置分支。

看另一種情況。如果是從一個沒有分支指向的commit切換到另一個沒有分支指向的commit上,那它們就是兩個韓國妹子,傻傻分不清楚了。

這是git checkout命令的效果。

C0 -- C1 -- C2(HEAD) -- C3(master) $ git checkout C1 C0 -- C1(HEAD) -- C2 -- C3(master)

這是git reset命令的效果。

C0 -- C1 -- C2(HEAD) -- C3(master) $ git reset C1 C0 -- C1(HEAD) -- C2 -- C3(master)

同時重置暫存區(qū)和工作區(qū)的改動

當你在 git reset 命令后面加 --hard 參數(shù)時,暫存區(qū)和工作區(qū)的內(nèi)容都會重置為重置后的commit內(nèi)容。也就是說暫存區(qū)和工作區(qū)的改動都會清空,相當于撤銷暫存區(qū)和工作區(qū)的改動。

而且是沒有確認操作的喲。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git reset --hard HEAD^HEAD is now at 58b0040 commit for nothing $ git statusOn branch master nothing to commit, working tree clean

僅重置暫存區(qū)的改動

git reset 命令后面加 --mixed 參數(shù),或者不加參數(shù),因為--mixed參數(shù)是默認值,暫存區(qū)的內(nèi)容會重置為重置后的commit內(nèi)容,工作區(qū)的改動不會清空,相當于撤銷暫存區(qū)的改動。

同樣也是沒有確認操作的喲。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git reset HEAD^Unstaged changes after reset: M a.md $ git statusOn branch master Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md no changes added to commit (use "git add" and/or "git commit -a")

打個趣,如果git reset命令什么都不加會怎樣呢?

你可以腦補一下,git reset命令不加參數(shù)默認就是--mixed,不加操作對象默認就是HEAD,所以單純的git reset命令相當于git reset --mixed HEAD命令。

那這又意味著什么呢?

這意味著從當前commit重置到當前commit,沒有變化對吧?但是--mixed參數(shù)會撤銷暫存區(qū)的改動對不對,這就是它的效果。

同時保留暫存區(qū)和工作區(qū)的改動

如果 git reset 命令后面加 --soft 參數(shù),鋼鐵直男的溫柔,你懂的。僅僅是重置commit而已,暫存區(qū)和工作區(qū)的改動都會保留下來。

更溫柔的是,重置前的commit內(nèi)容與重置后的commit內(nèi)容的diff也會放入暫存區(qū)。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git diff --stageddiff --git a/a.md b/a.md index 4a77268..fde8dcd 100644 --- a/a.md +++ b/a.md @@ -1,2 +1,3 @@applebanana +cherry $ git reset --soft HEAD^ $ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md $ git diff --stageddiff --git a/a.md b/a.md index 4a77268..fde8dcd 100644 --- a/a.md +++ b/a.md @@ -1 +1,3 @@apple +banana +cherry

banana就是重置前的commit內(nèi)容與重置后的commit內(nèi)容的diff,可以看到,它已經(jīng)在暫存區(qū)了。

文件暫存區(qū)改動撤回工作區(qū)

git reset命令后面也可以跟文件名,它的作用是將暫存區(qū)的改動重置為工作區(qū)的改動,是git add -- <file>的反向操作。

git reset -- <file>命令是git reset HEAD --mixed -- <file>的簡寫。在操作文件時,參數(shù)只有默認的--mixed一種。

它并不會撤銷工作區(qū)原有的改動。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md $ git reset -- a.mdUnstaged changes after reset: M a.md $ git statusOn branch master Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.md no changes added to commit (use "git add" and/or "git commit -a")

git checkout命令后面也可以跟文件名,它的作用是撤銷工作區(qū)的改動,需要注意區(qū)分。

文件若干commit版本撤回工作區(qū)

如果git reset命令后跟一個commit校驗和,它會把該commit與所有后代commit的diff重置到工作區(qū)。

意思就是將該文件重置回你指定的commit版本,但是在你指定的commit之后的改動我也給你留著,就放到工作區(qū)里吧。

$ git diff --staged# 空 git reset HEAD~4 -- a.mdUnstaged changes after reset: M a.md $ git diff --stageddiff --git a/a.md b/a.md index 6f195b4..72943a1 100644 --- a/a.md +++ b/a.md @@ -1,5 +1 @@aaa -bbb -ccc -ddd -eee

git diff --staged命令比較工作區(qū)和暫存區(qū)的內(nèi)容。可以看到初始工作區(qū)和暫存區(qū)是一致的,重置文件到4個版本之前,發(fā)現(xiàn)工作區(qū)比暫存區(qū)多了很多改動,這些都是指定commit之后的提交被重置到工作區(qū)了。

08) revert

有時候我們想撤回一個commit,但是這個commit已經(jīng)在公共的分支上。如果直接修改分支歷史,可能會引起一些不必要的混亂。這個時候,git revert命令就派上用場了。

revert翻譯成中文是還原。我覺得稱它為對沖更合理。對沖指的是同時進行兩筆行情相關(guān)、方向相反、數(shù)量相當、盈虧相抵的交易,這么理解git revert命令一針見血。

因為它的作用就是生成一個新的、完全相反的commit。

命令

git revert后跟你想要對沖的commit即可。

$ git revert HEADRevert "add c.md" This reverts commit 8a23dad059b60ba847a621b6058fb32fa531b20a. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # deleted: c.md

git會彈出默認或者自定義的編輯器要求你輸入commit信息。然后一個新的commit就生成了。

[master a8c4205] Revert "add c.md"1 file changed, 0 insertions(+), 0 deletions(-)delete mode 100644 c.md

可以看到,原本我添加了一個文件a.md,revert操作就會執(zhí)行刪除命令。在工作目錄看起來就像添加文件操作被撤銷了一樣,其實是被對沖了。

它不會改變commit歷史,只會增加一個新的對沖commit。這是它最大的優(yōu)點。

沖突

反向操作也會有沖突?你逗我的吧。

如果你操作的是最新的commit,那當然不會有沖突了。

那要操作的是以前的commit呢?

C0 -- C1 -- C2(HEAD -> master)

比如a.md在C0內(nèi)容為空,C1修改文件內(nèi)容為apple,C2修改文件內(nèi)容為banana。這時候你想撤銷C1的修改。

$ git revert HEAD~error: could not revert 483b537... apple hint: after resolving the conflicts, mark the corrected paths hint: with 'git add <paths>' or 'git rm <paths>' hint: and commit the result with 'git commit'

我們看一下文件內(nèi)容。

<<<<<<< HEAD banana ======= >>>>>>> parent of 483b537... apple

手動解決沖突,執(zhí)行g(shù)it add命令然后執(zhí)行g(shù)it revert --continue命令完成對沖操作。

取消revert操作只需要執(zhí)行g(shù)it revert --abort即可。

09) stash

你在一個分支上開展了一半的工作,突然有一件急事要你去處理。這時候你得切換到一個新的分支,可是手頭上的工作你又不想立即提交。

這種場景就需要用到git的儲藏功能。

儲藏

想要儲藏手頭的工作,只需運行g(shù)it stash命令。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md $ git stashSaved working directory and index state WIP on master: 974a2f2 update

WIP是work in progress的縮寫,指的是進行中的工作。

$ git statusOn branch master Untracked files:(use "git add <file>..." to include in what will be committed)c.md nothing added to commit but untracked files present (use "git add" to track)

可以看到,除了未被git跟蹤的文件之外,工作區(qū)和暫存區(qū)的內(nèi)容都會被儲藏起來。現(xiàn)在你可以切換到其他分支進行下一步工作了。

查看

我們看一下儲藏列表。

$ git stash liststash@{0}: WIP on master: 974a2f2 apple stash@{1}: WIP on master: c27b351 banana

恢復(fù)

等我們完成其他工作,肯定要回到這里,繼續(xù)進行中斷的任務(wù)。

$ git stash applyOn branch master Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: a.mdmodified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md no changes added to commit (use "git add" and/or "git commit -a")

誒,等等。怎么a.md的變更也跑到工作區(qū)了?是的,git stash默認會將暫存區(qū)和工作區(qū)的儲藏全部恢復(fù)到工作區(qū)。如果我就是想原樣恢復(fù)呢?

$ git stash apply --indexOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md

加一個參數(shù)--index就會讓工作區(qū)的歸工作區(qū),讓暫存區(qū)的歸暫存區(qū)。

還有一點需要注意,恢復(fù)儲藏的操作可以應(yīng)用在任何分支,它也不關(guān)心即將恢復(fù)儲藏的分支上,工作區(qū)和暫存區(qū)是否干凈。如果有沖突,自行解決就是了。

我們?yōu)g覽過儲藏列表,說明git stash apply僅僅是恢復(fù)了最新的那一次儲藏。

$ git stash apply stash@{1}

指定儲藏的名字,我們就可以恢復(fù)列表中的任意儲藏了。

這個時候我們再看一下儲藏列表。

$ git stash liststash@{0}: WIP on master: 974a2f2 apple stash@{1}: WIP on master: c27b351 banana

誒,發(fā)現(xiàn)還是兩條。我不是已經(jīng)恢復(fù)了一條么?

apply這個詞很巧妙,它只是應(yīng)用,它可不會清理。

清理

想要清理儲藏列表,咱們得顯式的運行g(shù)it stash drop命令。

$ git stash drop stash@{1} $ git stash liststash@{0}: WIP on master: 974a2f2 apple

現(xiàn)在就真的沒有了。希望你沒有喝酒?。

git還給我們提供了一個快捷操作,運行g(shù)it stash pop命令,同時恢復(fù)儲藏和清理儲藏。

$ git stash pop

10) view

有四個git命令可以用來查看git倉庫相關(guān)信息。

status

git status命令的作用是同時展示工作區(qū)和暫存區(qū)的diff、暫存區(qū)和當前版本的diff、以及沒有被git追蹤的文件。

$ git statusOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md

這個命令應(yīng)該是最常用的git命令之一了,每次提交之前都要看一下。

git status -v命令相當于git status命令和git diff --staged之和。

$ git status -vOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md diff --git a/a.md b/a.md index 5646a65..4c479de 100644 --- a/a.md +++ b/a.md @@ -1 +1 @@ -apple +banana

git status -vv命令相當于git status命令和git diff之和。

$ git status -vvOn branch master Changes to be committed:(use "git reset HEAD <file>..." to unstage)modified: a.md Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git checkout -- <file>..." to discard changes in working directory)modified: b.md Untracked files:(use "git add <file>..." to include in what will be committed)c.md Changes to be committed: diff --git c/a.md i/a.md index 5646a65..4c479de 100644 --- c/a.md +++ i/a.md @@ -1 +1 @@ -apple +banana -------------------------------------------------- Changes not staged for commit: diff --git i/b.md w/b.md index e69de29..637a09b 100644 --- i/b.md +++ w/b.md @@ -0,0 +1 @@ +## git is awesome

還有一個-s參數(shù),給出的結(jié)果很有意思。

$ git status -sM a.mdM b.md ?? c.md

注意看,前面的字母位置是不一樣的。

第一個位置是該文件在暫存區(qū)的狀態(tài),第二個位置是該文件在工作區(qū)的狀態(tài)。比如,以下信息顯示a.md文件在暫存區(qū)有改動待提交,在工作區(qū)也有改動待暫存。

MM a.md

縮寫的狀態(tài)碼主要有這么幾種:

狀態(tài)碼含義
M文件內(nèi)容有改動
A文件被添加
D文件被刪除
R文件被重命名
C文件被復(fù)制
U文件沖突未解決
?文件未被git追蹤
!文件被git忽略
?和 !所代表的狀態(tài)因為沒有進入git版本系統(tǒng),所以任何時候兩個位置都是一樣的。就像 ??或者 !!這樣。

show

git show命令show的是什么呢?git對象。

$ git showcommit 2bd3c9d7de54cec10f0896db9af04c90a41a8160 Author: veedrin <veedrin@qq.com> Date: Fri Dec 28 11:23:27 2018 +0800update diff --git a/README.md b/README.md index e8ab145..75625ce 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ onetwothree + +four

git show相當于git show HEAD,顯示當前HEAD指向的commit對象的信息。

當然,你也可以查看某個git對象的信息,后面跟上git對象的校驗和就行。

$ git show 38728d8tree 38728d8 README.md

diff

git diff命令可以顯示兩個主體之間的差異。

工作區(qū)與暫存區(qū)的差異

單純的git diff命令顯示工作區(qū)與暫存區(qū)之間的差異。

$ git diffdiff --git a/a.md b/a.md index e69de29..5646a65 100644 --- a/a.md +++ b/a.md @@ -0,0 +1 @@ +## git is awesome

因為是兩個主體之間的比較,git永遠將兩個主體分別命名為a和b。

也可以只查看某個文件的diff。當然這里依然是工作區(qū)與暫存區(qū)之間的差異。

$ git diff a.md

暫存區(qū)與當前commit的差異

git diff --staged命令顯示暫存區(qū)與當前commit的差異。

git diff --cached也可以達到相同的效果,它比較老,不如--staged語義化。

$ git diff --stageddiff --git a/b.md b/b.md index e69de29..4c479de 100644 --- a/b.md +++ b/b.md @@ -0,0 +1 @@ +apple

同樣,顯示某個文件暫存區(qū)與當前commit的差異。

$ git diff --staged a.md

兩個commit之間的差異

我們還可以用git diff查看兩個commit之間的差異。

$ git diff C1 C2diff --git a/a.md b/a.md index e69de29..5646a65 100644 --- a/a.md +++ b/a.md @@ -0,0 +1 @@ +## git is awesome diff --git a/b.md b/b.md new file mode 100644 index 0000000..e69de29

注意先后順序很重要,假如我改一下順序。

$ git diff C2 C1diff --git a/a.md b/a.md index 5646a65..e69de29 100644 --- a/a.md +++ b/a.md @@ -1 +0,0 @@ -## git is awesome diff --git a/b.md b/b.md deleted file mode 100644 index e69de29..0000000

比較兩個commit之間某個文件的差異。

$ git diff C1:a.md C2:a.mddiff --git a/a.md b/a.md index e69de29..5646a65 100644 --- a/a.md +++ b/a.md @@ -0,0 +1 @@ +## git is awesome

log

git log命令顯示提交歷史。

$ git logcommit 7e2514419ec0f75d1557d3d8165a7e7969f08349 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:53 2018 +0800c.md commit 4d346773212b208380f71885979f93da65f07ea6 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:41 2018 +0800b.md commit cde34665b49033d7b8aed3a334c3e2db2200b4dd Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:54:59 2018 +0800a.md

如果要查看每個commit具體的改動,添加-p參數(shù),它是--patch的縮寫。

$ git log -pcommit 7e2514419ec0f75d1557d3d8165a7e7969f08349 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:53 2018 +0800c.md diff --git a/c.md b/c.md new file mode 100644 index 0000000..e69de29 commit 4d346773212b208380f71885979f93da65f07ea6 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:41 2018 +0800b.md diff --git a/b.md b/b.md new file mode 100644 index 0000000..e69de29 commit cde34665b49033d7b8aed3a334c3e2db2200b4dd Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:54:59 2018 +0800a.md diff --git a/a.md b/a.md new file mode 100644 index 0000000..e69de29

你還可以控制顯示最近幾條。

$ git log -p -1commit 7e2514419ec0f75d1557d3d8165a7e7969f08349 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:53 2018 +0800c.md diff --git a/c.md b/c.md new file mode 100644 index 0000000..e69de29

-p有點過于冗余,只是想查看文件修改的統(tǒng)計信息的話,可以使用--stat參數(shù)。

$ git log --statcommit 7e2514419ec0f75d1557d3d8165a7e7969f08349 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:53 2018 +0800c.mdc.md | 01 file changed, 0 insertions(+), 0 deletions(-) commit 4d346773212b208380f71885979f93da65f07ea6 Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:56:41 2018 +0800b.mdb.md | 01 file changed, 0 insertions(+), 0 deletions(-) commit cde34665b49033d7b8aed3a334c3e2db2200b4dd Author: veedrin <veedrin@qq.com> Date: Sat Dec 29 11:54:59 2018 +0800a.mda.md | 01 file changed, 0 insertions(+), 0 deletions(-)

還覺得冗余?只想看提交說明,有一個--oneline可以幫到你。

$ git log --oneline4ad50f6 (HEAD -> master) 添加c.md文件 4d34677 添加b.md文件 cde3466 添加a.md文件

想在命令行工具看git提交歷史的樹形圖表,用--graph參數(shù)。

$ git log --graph* commit 7e2514419ec0f75d1557d3d8165a7e7969f08349 (HEAD -> master) | Author: veedrin <veedrin@qq.com> | Date: Sat Dec 29 11:56:53 2018 +0800 | c.md * commit 4d346773212b208380f71885979f93da65f07ea6 | Author: veedrin <veedrin@qq.com> | Date: Sat Dec 29 11:56:41 2018 +0800 | b.md * commit cde34665b49033d7b8aed3a334c3e2db2200b4ddAuthor: veedrin <veedrin@qq.com>Date: Sat Dec 29 11:54:59 2018 +0800a.md

我知道你們肯定又覺得冗余,--graph和--oneline食用更佳喲。

$ git log --graph --oneline* 7e25144 (HEAD -> master) c.md * 4d34677 b.md * cde3466 a.md

11) position

程序遇到bug的時候,我們需要快速定位。

定位有兩種,第一種是定位bug在哪個提交上,第二種是定位特定文件的某一行是誰最近提交的。

bisect

有時候我們發(fā)現(xiàn)程序有bug,但是回退幾個版本都不解決問題。說明這個bug是一次很老的提交導(dǎo)致的,也不知道當時怎么就沒察覺。

那怎么辦呢?繼續(xù)一個一個版本的回退?

估計Linus Torvalds會鄙視你吧。

為了專注于工作,不分心來鄙視你,Linus Torvalds在git中內(nèi)置了一套定位bug的命令。

大家都玩過猜數(shù)字游戲吧。主持人悄悄寫下一個數(shù),給大家一個數(shù)字區(qū)間,然后大家輪流開始切割,誰切到主持人寫的那個數(shù)就要自罰三杯了。

對,這就是二分法。git利用二分法定位bug的命令是git bisect。

使用

假設(shè)目前的git項目歷史是這樣的。

C0 -- C1 -- C2 -- C3 -- C4 -- C5 -- C6 -- C7 -- C8 -- C9(HEAD -> master)

這里面有一次commit藏了一個bug,但幸運的是,你不知道是哪一次。

運行g(shù)it bisect start命令,后跟你要定位的區(qū)間中最新的commit和最老的commit。

$ git bisect start HEAD C0Bisecting: 4 revisions left to test after this (roughly 2 steps) [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4

然后你就發(fā)現(xiàn)HEAD指針自動的指向了C4commit。如果范圍是奇數(shù)位,那取中間就行了,如果范圍是偶數(shù)位,則取中間更偏老的那個commit,就比如這里的C4commit。

$ git bisect goodBisecting: 2 revisions left to test after this (roughly 1 step) [97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6

HEAD指針指向C4commit后,你應(yīng)該運行一下程序,如果沒問題,那說明有bug的提交在它之后。我們只需要告訴git當前commit以及更老的commit都是好的。

然后HEAD指針就自動指向C6commit。

繼續(xù)在C6commit運行程序,結(jié)果復(fù)現(xiàn)了bug。說明問題就出在C6commit和C4commit之間。

$ git bisect badBisecting: 0 revisions left to test after this (roughly 0 steps) [a7e09bd3eab7d1e824c0338233f358cafa682af0] C5

將C6commit標記為bad之后,HEAD指針自動指向C5commit。再次運行程序,依然能復(fù)現(xiàn)bug。話不多說,標記C5commit為bad。

$ git bisect bada7e09bd3eab7d1e824c0338233f358cafa682af0 is the first bad commit

因為C4commit和C5commit之間已經(jīng)不需要二分了,git會告訴你,C5commit是你標記為bad的最早的commit。問題就應(yīng)該出在C5commit上。

git bisect resetPrevious HEAD position was a7e09bd... C5 Switched to branch 'master'

既然找到問題了,那就可以退出git bisect工具了。

另外,git bisect old和git bisect good的效果相同,git bisect new和git bisect bad的效果相同,這是因為git考慮到,有時候開發(fā)者并不是想定位bug,只是想定位某個commit,這時候用good bad就會有點別扭。

后悔

git bisect確實很強大,但如果我已經(jīng)bisect若干次,結(jié)果不小心把一個goodcommit標記為bad,或者相反,難道我要reset重來么?

git bisect還有一個log命令,我們只需要保存bisect日志到一個文件,然后擦除文件中標記錯誤的日志,然后按新的日志重新開始bisect就好了。

git bisect log > log.txt

該命令的作用是將日志保存到log.txt文件中。

看看log.txt文件中的內(nèi)容。

# bad: [4d5e75c7a9e6e65a168d6a2663e95b19da1e2b21] C9 # good: [c2fa7ca426cac9990ba27466520677bf1780af97] add a.md git bisect start 'HEAD' 'c2fa7ca426cac9990ba27466520677bf1780af97' # good: [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4 git bisect good ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd # good: [97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6 git bisect good 97cc0e879dc09796bd56cfd7c3a54deb41e447f6

將標記錯誤的內(nèi)容去掉。

# bad: [4d5e75c7a9e6e65a168d6a2663e95b19da1e2b21] C9 # good: [c2fa7ca426cac9990ba27466520677bf1780af97] add a.md git bisect start 'HEAD' 'c2fa7ca426cac9990ba27466520677bf1780af97' # good: [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4 git bisect good ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd

然后運行g(shù)it bisect replay log.txt命令。

$ git bisect replay log.txtPrevious HEAD position was ad95ae3... C8 Switched to branch 'master' Bisecting: 4 revisions left to test after this (roughly 2 steps) [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4 Bisecting: 2 revisions left to test after this (roughly 1 step) [97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6

git會根據(jù)log從頭開始重新bisect,錯誤的標記就被擦除了。

然后就是重新做人啦。

blame

一個充分協(xié)作的項目,每個文件可能都被多個人改動過。當出現(xiàn)問題的時候,大家希望快速的知道,某個文件的某一行是誰最后改動的,以便厘清責(zé)任。

git blame就是這樣一個命令。blame翻譯成中文是歸咎于,這個命令就是用來甩鍋的。

git blame只能作用于單個文件。

$ git blame a.md705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行 74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行 a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行 ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行 a7e09bd3 (veedrin 2018-12-25 10:19:19 +0800 5) 第五行 97cc0e87 (veedrin 2018-12-25 10:21:55 +0800 6) 第六行 67029a81 (veedrin 2018-12-25 10:22:15 +0800 7) 第七行 ad95ae3f (zhangsan 2018-12-25 10:23:20 +0800 8) 第八行 4d5e75c7 (lisi 2018-12-25 10:23:37 +0800 9) 第九行

它會把每一行的修改者信息都列出來。

第一部分是commit哈希值,表示這一行的最近一次修改屬于該次提交。

第二部分是作者以及修改時間。

第三部分是行的內(nèi)容。

如果文件太長,我們可以截取部分行。

$ git blame -L 1,5 a.md705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行 74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行 a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行 ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行 a7e09bd3 (veedrin 2018-12-25 10:19:19 +0800 5) 第五行

或者這樣寫。

$ git blame -L 1,+4 a.md705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行 74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行 a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行 ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行

但是結(jié)果不是你預(yù)期的那樣是吧。1,+4的確切意思是從1開始,顯示4行。

如果有人重名,可以顯示郵箱來區(qū)分。添加參數(shù)-e或者--show-email即可。

$ git blame -e a.md705d9622 (veedrin@qq.com 2018-12-25 10:09:04 +0800 1) 第一行 74eff2ee (abby@qq.com 2018-12-25 10:16:44 +0800 2) 第二行 a65b29bd (bob@qq.com 2018-12-25 10:17:02 +0800 3) 第三行 ee27077f (veedrin@qq.com 2018-12-25 10:19:05 +0800 4) 第四行 a7e09bd3 (veedrin@qq.com 2018-12-25 10:19:19 +0800 5) 第五行 97cc0e87 (veedrin@qq.com 2018-12-25 10:21:55 +0800 6) 第六行 67029a81 (veedrin@qq.com 2018-12-25 10:22:15 +0800 7) 第七行 ad95ae3f (zhangsan@qq.com 2018-12-25 10:23:20 +0800 8) 第八行 4d5e75c7 (lisi@qq.com 2018-12-25 10:23:37 +0800 9) 第九行

12) tag

git是一個版本管理工具,但在眾多版本中,肯定有一些版本是比較重要的,這時候我們希望給這些特定的版本打上標簽。比如發(fā)布一年以后,程序的各項功能都趨于穩(wěn)定,可以在圣誕節(jié)發(fā)布v1.0版本。這個v1.0在git中就可以通過標簽實現(xiàn)。

而git標簽又分為兩種,輕量級標簽和含附注標簽。

輕量級標簽和分支的表現(xiàn)形式是一樣的,僅僅是一個指向commit的指針而已。只不過它不能切換,一旦貼上就無法再挪動了。

含附注標簽才是我們理解的那種標簽,它是一個獨立的git對象。包含標簽的名字,電子郵件地址和日期,以及標簽說明。

創(chuàng)建

創(chuàng)建輕量級標簽的命令很簡單,運行g(shù)it tag <tag name>。

$ git tag v0.3

在.git目錄中就多了一個指針文件。

.git/refs/tags/v0.3

創(chuàng)建含附注標簽要加一個參數(shù)-a,它是--annotated的縮寫。

$ git tag -a v1.0

和git commit一樣,如果不加-m參數(shù),則會彈出默認或者自定義的編輯器,要求你寫標簽說明。

不寫呢?

fatal: no tag message?

創(chuàng)建完含附注標簽后,.git目錄會多出兩個文件。

.git/refs/tags/v0.3 .git/objects/80/e79e91ce192e22a9fd860182da6649c4614ba1

含附注標簽不僅會創(chuàng)建一個指針,還會創(chuàng)建一個tag對象。

我們了解過git有四種對象類型,tag類型是我們認識的最后一種。

我們看看該對象的類型。

$ git cat-file -t 80e79e9tag

再來看看該對象的內(nèi)容。

$ git cat-file -p 80e79e9object 359fd95229532cd352aec43aada8e6cea68d87a9 type commit tag v1.0 tagger veedrin <veedrin@qq.com> 1545878480 +0800 版本 v1.0

它關(guān)聯(lián)的是一個commit對象,包含標簽的名稱,打標簽的人,打標簽的時間以及標簽說明。

我可不可以給歷史commit打標簽?zāi)?#xff1f;當然可以。

$ git tag -a v1.0 36ff0f5

只需在后面加上commit的校驗和。

查看

查看當前git項目的標簽列表,運行g(shù)it tag命令不帶任何參數(shù)即可。

$ git tagv0.3 v1.0

注意git標簽是按字母順序排列的,而不是按時間順序排列。

而且我并沒有找到分別查看輕量級標簽和含附注標簽的方法。

查看標簽詳情可以使用git show <tag name>。

$ git show v0.3commit 36ff0f58c8e6b6a441733e909dc95a6136a4f91b (tag: v0.3) Author: veedrin <veedrin@qq.com> Date: Thu Dec 27 11:08:09 2018 +0800add a.md diff --git a/a.md b/a.md new file mode 100644 index 0000000..e69de29 $ git show v1.0tag v1.0 Tagger: veedrin <veedrin@qq.com> Date: Thu Dec 27 11:08:39 2018 +0800 版本 v1.0 commit 6dfdb65ce65b782a6cb57566bcc1141923059d2b (HEAD -> master, tag: v1.0) Author: veedrin <veedrin@qq.com> Date: Thu Dec 27 11:08:33 2018 +0800add b.md diff --git a/b.md b/b.md new file mode 100644 index 0000000..e69de29

刪除

雖然git標簽不能移動對吧,但我們可以刪除它呀。

$ git tag -d v0.3Deleted tag 'v0.3' (was 36ff0f5)

如果標簽已經(jīng)推送到了遠端,也是可以刪除的。

$ git push origin -d v0.3To github.com:veedrin/git.git- [deleted] v0.3

推送

默認情況下,git push推送到遠端倉庫并不會將標簽也推送上去。如果想將標簽推送到遠端與別人共享,我們得顯式的運行命令git push origin <tag name>。

$ git push origin v1.0Counting objects: 1, done. Writing objects: 100% (1/1), 160 bytes | 160.00 KiB/s, done. Total 1 (delta 0), reused 0 (delta 0) To github.com:veedrin/git.git* [new tag] v1.0 -> v1.0

這里并不區(qū)分輕量級標簽和含附注標簽。

一次性將本地標簽推送到遠端倉庫也是可以的。

$ git push origin --tags

13) remote

git是分布式版本管理工具,它沒有中央倉庫。但多人協(xié)作時,我們依然需要一個集散地,讓協(xié)作成員之間統(tǒng)一往集散地推送和拉取更新。否則,點對點的溝通,效率會很低。

所以就引出了git中遠端倉庫的概念。

概念

我們之前所有的操作都是在本地倉庫完成的,和本地倉庫對應(yīng)的是遠端倉庫。那么本地有若干分支,遠端倉庫是不是也有對應(yīng)的若干分支呢?

當然。

我們探討一個問題,在離線狀態(tài)下,git是不是無從知道遠端倉庫的任何狀態(tài)?

我讓網(wǎng)絡(luò)下線,查詢從github克隆下來的本地倉庫的狀態(tài),結(jié)果它告訴我本地倉庫的master分支是up to date with 'origin/master'。

$ git statusOn branch master Your branch is up to date with 'origin/master'.nothing to commit, working tree clean

實際上,git的分支有三種:

  • 本地分支,我們可以通過<branch>寫法訪問它。
  • 遠端分支,我們可以通過<remote branch>寫法訪問它。
  • 遠端分支引用,我們可以通過<remote/branch>寫法訪問它。實際上它也是本地分支,只不過我們無法操作它,只有g(shù)it的網(wǎng)絡(luò)操作才可以更新它。離線狀態(tài)下,git給的狀態(tài)就是本地分支和遠端分支引用的比較結(jié)果。
git官方把我所說的 遠端分支引用稱為 遠端分支。知道誰是誰就行了,名字不重要?

我是馬蹄疾

我們看一下本地的遠端分支引用。

.git/ .git/refs/ .git/refs/remotes/ .git/refs/remotes/origin/ .git/refs/remotes/origin/HEAD .git/refs/remotes/origin/master

默認的遠端倉庫名就叫origin。它也有master分支指針,也有HEAD指針。

拉取

如果遠端倉庫有新的提交或者新的分支,我們需要運行g(shù)it fetch命令來拉取更新。

$ git fetchremote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), done. From github.com:veedrin/git3893459..0f80eeb master -> origin/master

這個命令是git fetch origin的縮寫。因為origin是遠端倉庫的默認名稱,所以可以省略。如果有手動添加的遠端倉庫,那就必須指定遠端倉庫的名稱了。

這個命令做了什么呢?

它會把新的提交和新的分支拉取到本地,然后更新本地的遠端分支引用到最新的提交。

git fetch僅僅是將遠端的更新拉取下來,同步本地的遠端分支引用,不會對本地分支有任何影響。我們需要手動執(zhí)行合并操作才能更新本地分支。

$ git merge origin/masterOn branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean

當然,有一個更簡單的操作。

$ git pullremote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. Unpacking objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 From github.com:veedrin/git4fbd1d4..d9785d7 master -> origin/master Updating 4fbd1d4..d9785d7 Fast-forwardREADME.md | 2 ++1 file changed, 2 insertions(+)

git pull就是git fetch和git merge的一鍵操作。

推送

推送到遠端的命令是git push <remote-name> <remote-branch-name>。

$ git push origin masterCounting objects: 3, done. Writing objects: 100% (3/3), 261 bytes | 261.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0) To github.com:veedrin/git.git3eaa1ae..2bd3c9d master -> master

如果當前分支對遠端分支設(shè)置了追蹤的話,也可以省略分支名。

$ git pushCounting objects: 3, done. Writing objects: 100% (3/3), 261 bytes | 261.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0) To github.com:veedrin/git.git3eaa1ae..2bd3c9d master -> master

有時候本地分支和遠端分支同時有新的提交,直接push是不行的。

$ git pushTo github.com:veedrin/git.git! [rejected] master -> master (fetch first) error: failed to push some refs to 'git@github.com:veedrin/git.git' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.

有兩種方式解決。

第一是先把遠端的更新拉下來,有沖突則解決沖突,沒沖突則再推送。

第二是強推。有時候我們就是想覆蓋遠端對吧,也不是不行,但是必須十分謹慎。而且不要在公共分支上強制推送。

$ git push -fCounting objects: 24, done. Delta compression using up to 4 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (24/24), 3.72 KiB | 1.24 MiB/s, done. Total 24 (delta 0), reused 3 (delta 0) To github.com:veedrin/git.git+ 54d741b...2db10e0 master -> master (forced update)

實際開發(fā)時我們會建很多特性分支,推送到遠端,通過測試后再合入主分支。使用git push <remote-name> <remote-branch-name>每次都要指定遠端分支名,如果會有多次推送,我們可以在推送時設(shè)置本地分支追蹤遠端分支,這樣下次就可以直接推送了。

也可以簡寫成git push -u <remote-name> <remote-branch-name>。

$ git push --set-upstream origin devCounting objects: 3, done. Delta compression using up to 4 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 255 bytes | 255.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0) remote: remote: Create a pull request for 'dev' on GitHub by visiting: remote: https://github.com/veedrin/git/pull/new/dev remote: To github.com:veedrin/git.git* [new branch] dev -> dev Branch 'dev' set up to track remote branch 'dev' from 'origin'.

然后我們在.git/config文件中能看到多了一條配置。

[branch "dev"]remote = originmerge = refs/heads/dev

查看

查看遠端倉庫的命令是git remote。

$ git remoteorigin

加-v參數(shù)可以查看更為詳細的信息,-v是--verbose的縮寫。

$ git remote -vorigin git@github.com:veedrin/git.git (fetch) origin git@github.com:veedrin/git.git (push)

查看某個遠端倉庫的信息,可以使用命令git remote show <remote-name>。

$ git remote show origin* remote originFetch URL: git@github.com:veedrin/git-1.gitPush URL: git@github.com:veedrin/git-1.gitHEAD branch: masterRemote branches:dev trackedmaster trackedLocal branches configured for 'git pull':dev merges with remote devmaster merges with remote masterLocal refs configured for 'git push':master pushes to master (up to date)

添加

添加新的遠端倉庫,使用git remote add <shortname> <url>命令。

$ git remote add horseshoe https://github.com/veedrin/horseshoe

然后本地就多了一個遠端倉庫。

$ git remotehorseshoe origin

除了添加遠端倉庫,我們還可以添加本地分支對遠端分支的追蹤。

$ git checkout -b dev origin/devBranch 'dev' set up to track remote branch 'dev' from 'origin'. Switched to a new branch 'dev'

創(chuàng)建dev分支的同時,也設(shè)置了對遠端分支dev的追蹤,這樣下次推送的時候就不需要指定了。

當然,遠端分支引用必須得存在才行。

$ git checkout -b dev origin/devfatal: 'origin/dev' is not a commit and a branch 'dev' cannot be created from it

git也提供了快捷方式。

$ git checkout --track origin/devBranch 'dev' set up to track remote branch 'dev' from 'origin'. Switched to a new branch 'dev'

重命名

有時候你想修改遠端倉庫的簡寫名。比如你將女朋友的名字命名為遠端倉庫的簡寫名,然后你們分手了。這真是一個令人悲傷(欣喜)的故事。

$ git remote rename nvpengyou gaoyuanyuan

查看遠端倉庫列表。

$ git remotegaoyuanyuan origin

刪除

一般來說,一個git項目有一個遠端倉庫就行了,其余的大多是臨時性的。所以總有一天要刪除它。

$ git remote rm horseshoe

查看遠端倉庫列表。

$ git remoteorigin 本文是『horseshoe·Git專題』系列文章之一,后續(xù)會有更多專題推出
GitHub地址(持續(xù)更新): https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
如果覺得對你有幫助,歡迎來GitHub點Star或者來我的博客親口告訴我

Git專題一覽

? add

? commit

? branch

? checkout

? merge

? rebase

? reset

? revert

? stash

? view

? position

? tag

? remote

總結(jié)

以上是生活随笔為你收集整理的2018(农历年)封山之作,和我一起嚼烂Git(两万字长文)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。