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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

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

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

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

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

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

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

這篇文章就是第一板斧。

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

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

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

困難年歲,共勉。

01) add

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

沒錯(cuò),它都是。

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

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

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

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

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

git倉庫

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

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

$ git init

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

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

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

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

工作區(qū)(working directory)

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

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

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

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

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

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

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

這樣做的好處是明顯的:

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

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

放入暫存區(qū)

git默認(rèn)是不會(huì)把工作區(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)在被標(biāo)注為Untracked files。表示git目前還無法追蹤它們的變化,也就是說它們還不在暫存區(qū)里。

那么我們?nèi)绾问謩?dòng)將文件或文件夾放入暫存區(qū)呢?

$ git add .

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

下面的命令也能達(dá)到相同的效果。

$ git add -A

假如我只想暫存單個(gè)文件呢?后跟相對(duì)于當(dāng)前目錄的文件名即可。

$ git add README.md

暫存整個(gè)文件夾也是一樣的道理。因?yàn)間it會(huì)遞歸暫存文件夾下的所有文件。

$ git add src

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

這時(shí)狀態(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")

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

$ git add -a

它和git add -A命令的區(qū)別在于,它只能將已加入暫存區(qū)文件的改動(dòng)放入暫存區(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

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

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

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

objects

git項(xiàng)目的.git目錄下面有一個(gè)目錄objects,一開始這個(gè)目錄下面只有兩個(gè)空目錄:info和pack。

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

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

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

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

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

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

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

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

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

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

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

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

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

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

只有一個(gè)目錄,而且校驗(yàn)和跟之前一模一樣。

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

文件改動(dòng)對(duì)應(yīng)新對(duì)象

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

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

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

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

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

文件改動(dòng)全量保存

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

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70

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

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

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

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

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

重命名會(huì)拆分成刪除和新建兩個(gè)動(dòng)作

初始化一個(gè)本地倉庫,新建一個(gè)文件,運(yùn)行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)部機(jī)制導(dǎo)致的。生成對(duì)象的時(shí)候,它發(fā)現(xiàn)倉庫中叫這個(gè)名字的文件不見了,于是標(biāo)記為已刪除,又發(fā)現(xiàn)有一個(gè)新的文件名是之前沒有標(biāo)記過的,于是標(biāo)記為未跟蹤。因?yàn)樗皇侵孛?#xff0c;文件內(nèi)容并沒有改變,所以可以共享對(duì)象,并不會(huì)影響效率。

blob對(duì)象

git的一切秘密都在.git目錄里。因?yàn)樗鼡碛许?xiàng)目的完整信息,所以git一定是把備份存在了某個(gè)地方。git把它們存在了哪里,又是如何存儲(chǔ)它們的呢?

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

這一次我們只介紹blob對(duì)象。

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

因?yàn)樗淮鎯?chǔ)內(nèi)容和大小,所以兩個(gè)文件即便文件名和格式完全不一樣,只要內(nèi)容相同,就可以共享一個(gè)blob對(duì)象。

注意blob對(duì)象和工作目錄的文件并不是一一對(duì)應(yīng)的,因?yàn)楣ぷ髂夸浀奈募缀鯐?huì)被多次添加到暫存區(qū),這時(shí)一個(gè)文件會(huì)對(duì)應(yīng)多個(gè)blob對(duì)象。

index

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

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

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

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

打包

還記得我們?cè)谖募膭?dòng)全量保存小節(jié)里講到,git魚和熊掌想兼得么?

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

git會(huì)定期或者在推送到遠(yuǎn)端之前對(duì)git對(duì)象進(jìn)行打包處理。

打包的時(shí)候保存文件最新的全量版本,基于該文件的歷史版本的改動(dòng)則只保存diff信息。因?yàn)殚_發(fā)者很少會(huì)切換到較早的版本中,所以這時(shí)候效率就可以部分犧牲。

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

git也有一個(gè)git gc命令可以手動(dòng)執(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對(duì)象文件都不見了,pack文件夾多了兩個(gè)文件。其中 .pack 后綴文件存儲(chǔ)的就是打包前git對(duì)象文件的實(shí)際內(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是一個(gè)版本管理系統(tǒng)。它的終極目的就是將項(xiàng)目特定時(shí)間的信息保留成一個(gè)版本,以便將來的回退和查閱。

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

提交

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

# 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í)必填的。

如果提交說明不多,可以加參數(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

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

這時(shí)我就可以使用git commit --amend命令。

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

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

最后重寫提交。git會(huì)跳往默認(rèn)或者自定義編輯器提示你修改commit說明。當(dāng)然你也可以不改。

$ 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

我們?cè)賮砜刺峤粴v史。

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

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

tree對(duì)象和commit對(duì)象

commit操作涉及到兩個(gè)git對(duì)象。

第一是tree對(duì)象。

它存儲(chǔ)子目錄和子文件的引用。如果只有blob對(duì)象,那版本庫將是一團(tuán)散沙。正因?yàn)橛衪ree對(duì)象將它們的關(guān)系登記在冊(cè),才能構(gòu)成一個(gè)有結(jié)構(gòu)的版本庫。

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

第二是commit對(duì)象。

它存儲(chǔ)每個(gè)提交的信息,包括當(dāng)前提交的根tree對(duì)象的引用,父commit對(duì)象的引用,作者和提交者,還有提交信息。所謂的版本,其實(shí)指的就是這個(gè)commit對(duì)象。

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

objects

初始化一個(gè)git項(xiàng)目,新建一些文件和目錄。

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

首先運(yùn)行g(shù)it add命令。我們清楚,這會(huì)在.git/objects目錄下生成一個(gè)blob對(duì)象,因?yàn)槟壳皟蓚€(gè)文件都是空文件,共享一個(gè)blob對(duì)象。

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

現(xiàn)在我們運(yù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

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

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

這是第一個(gè)tree對(duì)象。

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

這是第二個(gè)tree對(duì)象。

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

這是第三個(gè)tree對(duì)象。

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

可以看到,提交時(shí)每個(gè)目錄都會(huì)生成對(duì)應(yīng)的tree對(duì)象。

然后我們?cè)賮砜碿ommit對(duì)象。

$ 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會(huì)關(guān)聯(lián)根目錄的tree對(duì)象,因?yàn)殛P(guān)聯(lián)它就可以關(guān)聯(lián)到所有的項(xiàng)目結(jié)構(gòu)信息,所謂擒賊先擒王嘛。它也要關(guān)聯(lián)父commit,也就是它的上一個(gè)commit,這樣才能組成版本歷史。當(dāng)然,如果是第一個(gè)commit那就沒有父commit了。然后就是commit說明和一些參與者信息。

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

為啥不在git add的時(shí)候就生成tree對(duì)象呢?

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

03) branch

分支是使得git如此靈活的強(qiáng)大武器,正是因?yàn)橛星擅畹姆种гO(shè)計(jì),眾多的git工作流才成為可能。

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

當(dāng)然不是。

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

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

你可以認(rèn)為它僅僅是一個(gè)指針,指向一個(gè)commit對(duì)象節(jié)點(diǎn)。

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

嚴(yán)格的來說,一種叫分支指針,一種叫分支歷史。不過實(shí)際使用中,它們?cè)诿稚铣32蛔鲄^(qū)分。

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

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

master分支

剛剛初始化的git倉庫,會(huì)發(fā)現(xiàn).git/refs/heads目錄下面是空的。這是因?yàn)槟壳鞍姹編炖镞€沒有任何commit對(duì)象,而分支一定是指向commit對(duì)象的。

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

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

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

$ cat .git/refs/heads/master6b5a94158cc141286ac98f30bb189b8a83d61347

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

$ git cat-file -t 6b5a941commit

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

HEAD指針

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

你當(dāng)前在哪里,HEAD就在哪里。它一般指向某個(gè)分支,因?yàn)橐话阄覀兌紩?huì)在某個(gè)分支之上。

因?yàn)镠EAD是用來標(biāo)注當(dāng)前位置的,所以一旦HEAD的位置被改變,工作目錄就會(huì)切換到HEAD指向的分支。

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

但是也有例外,比如我直接簽出到某個(gè)沒有分支引用的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

這個(gè)時(shí)候的HEAD就叫做detached HEAD。

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

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

如果不小心這么做了,要么在原地新建一個(gè)分支,要么將已有的分支強(qiáng)行移動(dòng)過來。確保它不會(huì)被遺忘。

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

創(chuàng)建

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

$ git branch dev

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

查看

或許有時(shí)我們也想要查看本地倉庫有多少個(gè)分支,因?yàn)樵趃it中新建分支實(shí)在是太容易了。

$ git branchdev * master

當(dāng)前分支的前面會(huì)有一個(gè)*號(hào)標(biāo)注。

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

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

刪除

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

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

有時(shí)候我們會(huì)得到不一樣的提示。

$ 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的一種保護(hù)措施。is not fully merged是針對(duì)當(dāng)前分支來說的,意思是你要?jiǎng)h除的分支還有內(nèi)容沒有合并進(jìn)當(dāng)前分支,你確定要?jiǎng)h除它嗎?

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

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

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

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

04) checkout

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

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

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

底層

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

目前HEAD指針指向master分支。

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

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

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

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

符號(hào)

在進(jìn)入正題之前,我們要先聊聊git中的兩個(gè)符號(hào)~和^。

如果我們要從一個(gè)分支切換到另一個(gè)分支,那還好說,足夠語義化。但是如果我們要切換到某個(gè)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歷史有分叉,那它就選第一個(gè),也就是主干上的那個(gè)。

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

HEAD不加任何符號(hào)、加~0 符號(hào)或者加^0符號(hào)時(shí),定位的都是當(dāng)前版本

這個(gè)不用說,定位當(dāng)前commit。

$ git rev-parse HEAD4e76510fe8bb3c69de12068ab354ef37bba6da9d

它表示定位第零代父commit,也就是當(dāng)前commit。

$ git rev-parse HEAD~04e76510fe8bb3c69de12068ab354ef37bba6da9d

它表示定位當(dāng)前commit的第零個(gè)父commit,也就是當(dāng)前commit。

$ git rev-parse HEAD^04e76510fe8bb3c69de12068ab354ef37bba6da9d

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

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

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

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

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

因?yàn)楫?dāng)前commit只有一個(gè)父commit,所以定位第二個(gè)父commit會(huì)失敗。

$ 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的第幾個(gè)父commit

當(dāng)前commit的第一代父commit的第零個(gè)父commit,意思就是第一代父commit咯。

$ git rev-parse HEAD~^02ec837440051af433677f786e502d1f6cdeb0a4a

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

$ git rev-parse HEAD~^1fb60f519a59e9ceeef039f7efd2a8439aa7efd4b

這里定位的是當(dāng)前commit的第一代父commit的第二個(gè)父commit。

$ git rev-parse HEAD~^27c0a8e3a325ce1b5a1cdeb8c89bef1ecf17c10c9

同樣,定位到一個(gè)不存在的commit會(huì)失敗。

$ 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指的是第二個(gè)父commit,^^指的是第一個(gè)父commit的第一個(gè)父commit。

切換到HEAD

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

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

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

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

切換到commit

開發(fā)者用的最多的當(dāng)然是切換分支。其實(shí)checkout后面不僅可以跟分支名,也可以跟commit的校驗(yàn)和,還可以用符號(hào)定位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)建分支并切換

有時(shí)候我們?cè)趧?chuàng)建分支時(shí)希望同時(shí)切換到創(chuàng)建后的分支,僅僅git branch <branch>是做不到的。這時(shí)git checkout命令可以提供一個(gè)快捷操作,創(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

因?yàn)闀捍鎱^(qū)覆蓋了工作區(qū),所以工作區(qū)的改動(dòng)就被撤銷了,現(xiàn)在只剩下暫存區(qū)的改動(dòng)等待提交。其實(shí)相當(dāng)于撤銷文件在工作區(qū)的改動(dòng),只不過它的語義是覆蓋。這個(gè)命令沒有任何提示,直接撤銷工作區(qū)改動(dòng),要謹(jǐn)慎使用。

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

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

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

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

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

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

$ git checkout -- .

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

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

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

$ 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

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

05) merge

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

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

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

$ 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會(huì)阻止合并。這是因?yàn)楹喜⒅?#xff0c;git會(huì)將合并后的版本覆蓋暫存區(qū)。所以會(huì)有丟失工作成果的危險(xiǎn)。

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

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

不同分支的合并

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

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

git merge后跟合并客體,表示要將它合并進(jìn)來。

$ git merge dev

進(jìn)行到這里,如果沒有沖突,git會(huì)彈出默認(rèn)或者自定義的編輯器,讓你填寫commit說明。當(dāng)然它會(huì)給你填寫一個(gè)默認(rèn)的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說明?因?yàn)檫@種情況的git merge實(shí)際上會(huì)創(chuàng)建一個(gè)新的commit對(duì)象,記錄此次合并的信息,并將當(dāng)前分支指針移動(dòng)到它上面來。

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

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

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

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

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

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

它指的是開發(fā)者當(dāng)前在一個(gè)commit節(jié)點(diǎn)上,要將同一個(gè)分支上更新的commit節(jié)點(diǎn)合并進(jìn)來。

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

這時(shí)候會(huì)發(fā)生什么呢?

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

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

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

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

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

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

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

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

依然用三方合并的概念來理解,這時(shí)合并客體commit與合并主客體的共同祖先commit是同一個(gè)commit,合并客體commit相對(duì)于合并主客體的共同祖先commit的diff為空,合并主體commit相對(duì)于合并主客體的共同祖先commit的diff與空diff合并還是它自己。但是這回它都不用移動(dòng),因?yàn)楹喜⒑蟮膁iff就是它自己原有的diff。

注意,這時(shí)候dev分支指針會(huì)不會(huì)動(dòng)呢?

當(dāng)然不會(huì),git merge操作對(duì)合并客體是沒有任何影響的。

同時(shí)合并多個(gè)客體

如果你在git merge后面跟不止一個(gè)分支,這意味著你想同時(shí)將它們合并進(jìn)當(dāng)前分支。

$ 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章魚策略,因?yàn)橥瑫r(shí)合并的多個(gè)分支最終都會(huì)指向新的commit,看起來像章魚的觸手。

合并有沖突

git merge操作并不總是如此順利的。因?yàn)橛袝r(shí)候要合并的兩個(gè)分支不是同一個(gè)人的,就會(huì)有很大的概率遇到兩人同時(shí)修改文件某一行的情況。git不知道該用誰的版本,它認(rèn)為兩個(gè)分支遇到了沖突。

這時(shí)就需要開發(fā)者手動(dòng)的解決沖突,才能讓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

運(yùn)行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.

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

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

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

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

如果你遇到?jīng)_突以后不知道如何解決,因?yàn)槟阋ピ儐柲愕暮献骰锇闉槭裁催@樣改。這時(shí)你肯定想回到合并以前的狀態(tài)。

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

$ git merge --abort

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

06) rebase

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

git就是哆啦A夢(mèng)的口袋。

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

基礎(chǔ)用法

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

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

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

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

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

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

git rebase完并沒有結(jié)束,因?yàn)槲易兓哪繕?biāo)分支是master,而當(dāng)前分支是dev。我需要切換到master分支上,然后再合并一次。

$ git checkout master $ git merge dev

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

別急,這種合并是Fast forward的,并不會(huì)生成一個(gè)新的合并commit。

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

$ git rebase master dev

你在任何一個(gè)分支上,這種寫法都可以將dev分支變基到master分支上,變基完成當(dāng)前分支會(huì)變成dev分支。

裁剪commit變基

變基有點(diǎn)像基因編輯,git有更精確的工具達(dá)到你想要的效果。

有了精確的基因編輯技術(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ù)就是那把基因編輯的剪刀。

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

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

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

變基沖突解決

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

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同時(shí)修改了a.md的某一行,引發(fā)沖突。git已經(jīng)給我們提示了,大體上和merge的操作一致。

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

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

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

cherry-pick

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

別急,git cherry-pick命令雖然是一個(gè)獨(dú)立的git命令,它的效果卻還是變基,而且是commit級(jí)別的變基。

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

用法

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

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

將當(dāng)前分支切換到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并不會(huì)修改原有的commit。

同時(shí)挑選多個(gè)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)

如果這多個(gè)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'

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

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

變基還是合并

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

有一種觀點(diǎn)認(rèn)為,倉庫的commit歷史應(yīng)該記錄實(shí)際發(fā)生過什么。所以如果你將一個(gè)分支合并進(jìn)另一個(gè)分支,commit歷史中就應(yīng)該有這一次合并的痕跡,因?yàn)樗菍?shí)實(shí)在在發(fā)生過的。

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

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

07) reset

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

那git有沒有辦法移動(dòng)分支指針呢?

當(dāng)然有,這就是git reset命令。

底層

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

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

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

但是git reset命令沒有這個(gè)問題,因?yàn)樗鼤?huì)把當(dāng)前的分支指針也帶過去。

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

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

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

這是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)

同時(shí)重置暫存區(qū)和工作區(qū)的改動(dòng)

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

而且是沒有確認(rèn)操作的喲。

$ 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ū)的改動(dòng)

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

同樣也是沒有確認(rèn)操作的喲。

$ 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")

打個(gè)趣,如果git reset命令什么都不加會(huì)怎樣呢?

你可以腦補(bǔ)一下,git reset命令不加參數(shù)默認(rèn)就是--mixed,不加操作對(duì)象默認(rèn)就是HEAD,所以單純的git reset命令相當(dāng)于git reset --mixed HEAD命令。

那這又意味著什么呢?

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

同時(shí)保留暫存區(qū)和工作區(qū)的改動(dòng)

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

更溫柔的是,重置前的commit內(nèi)容與重置后的commit內(nèi)容的diff也會(huì)放入暫存區(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ū)改動(dòng)撤回工作區(qū)

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

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

它并不會(huì)撤銷工作區(qū)原有的改動(dòng)。

$ 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ū)的改動(dòng),需要注意區(qū)分。

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

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

意思就是將該文件重置回你指定的commit版本,但是在你指定的commit之后的改動(dòng)我也給你留著,就放到工作區(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個(gè)版本之前,發(fā)現(xiàn)工作區(qū)比暫存區(qū)多了很多改動(dòng),這些都是指定commit之后的提交被重置到工作區(qū)了。

08) revert

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

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

因?yàn)樗淖饔镁褪巧梢粋€(gè)新的、完全相反的commit。

命令

git revert后跟你想要對(duì)沖的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會(huì)彈出默認(rèn)或者自定義的編輯器要求你輸入commit信息。然后一個(gè)新的commit就生成了。

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

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

它不會(huì)改變commit歷史,只會(huì)增加一個(gè)新的對(duì)沖commit。這是它最大的優(yōu)點(diǎn)。

沖突

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

如果你操作的是最新的commit,那當(dāng)然不會(huì)有沖突了。

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

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

比如a.md在C0內(nèi)容為空,C1修改文件內(nèi)容為apple,C2修改文件內(nèi)容為banana。這時(shí)候你想撤銷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

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

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

09) stash

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

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

儲(chǔ)藏

想要儲(chǔ)藏手頭的工作,只需運(yùn)行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的縮寫,指的是進(jìn)行中的工作。

$ 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)容都會(huì)被儲(chǔ)藏起來。現(xiàn)在你可以切換到其他分支進(jìn)行下一步工作了。

查看

我們看一下儲(chǔ)藏列表。

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

恢復(fù)

等我們完成其他工作,肯定要回到這里,繼續(xù)進(jìn)行中斷的任務(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默認(rèn)會(huì)將暫存區(qū)和工作區(qū)的儲(chǔ)藏全部恢復(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

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

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

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

$ git stash apply stash@{1}

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

這個(gè)時(shí)候我們?cè)倏匆幌聝?chǔ)藏列表。

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

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

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

清理

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

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

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

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

$ git stash pop

10) view

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

status

git status命令的作用是同時(shí)展示工作區(qū)和暫存區(qū)的diff、暫存區(qū)和當(dāng)前版本的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

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

git status -v命令相當(dāng)于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命令相當(dāng)于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

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

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

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

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

MM a.md

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

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

show

git show命令show的是什么呢?git對(duì)象。

$ 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相當(dāng)于git show HEAD,顯示當(dāng)前HEAD指向的commit對(duì)象的信息。

當(dāng)然,你也可以查看某個(gè)git對(duì)象的信息,后面跟上git對(duì)象的校驗(yàn)和就行。

$ git show 38728d8tree 38728d8 README.md

diff

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

工作區(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

因?yàn)槭莾蓚€(gè)主體之間的比較,git永遠(yuǎn)將兩個(gè)主體分別命名為a和b。

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

$ git diff a.md

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

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

git diff --cached也可以達(dá)到相同的效果,它比較老,不如--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

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

$ git diff --staged a.md

兩個(gè)commit之間的差異

我們還可以用git diff查看兩個(gè)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

比較兩個(gè)commit之間某個(gè)文件的差異。

$ 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

如果要查看每個(gè)commit具體的改動(dòng),添加-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有點(diǎn)過于冗余,只是想查看文件修改的統(tǒng)計(jì)信息的話,可以使用--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(-)

還覺得冗余?只想看提交說明,有一個(gè)--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的時(shí)候,我們需要快速定位。

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

bisect

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

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

估計(jì)Linus Torvalds會(huì)鄙視你吧。

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

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

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

使用

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

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

這里面有一次commit藏了一個(gè)bug,但幸運(yùn)的是,你不知道是哪一次。

運(yùn)行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指針自動(dòng)的指向了C4commit。如果范圍是奇數(shù)位,那取中間就行了,如果范圍是偶數(shù)位,則取中間更偏老的那個(gè)commit,就比如這里的C4commit。

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

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

然后HEAD指針就自動(dòng)指向C6commit。

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

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

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

$ git bisect bada7e09bd3eab7d1e824c0338233f358cafa682af0 is the first bad commit

因?yàn)镃4commit和C5commit之間已經(jīng)不需要二分了,git會(huì)告訴你,C5commit是你標(biāo)記為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的效果相同,這是因?yàn)間it考慮到,有時(shí)候開發(fā)者并不是想定位bug,只是想定位某個(gè)commit,這時(shí)候用good bad就會(huì)有點(diǎn)別扭。

后悔

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

git bisect還有一個(gè)log命令,我們只需要保存bisect日志到一個(gè)文件,然后擦除文件中標(biāo)記錯(cuò)誤的日志,然后按新的日志重新開始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

將標(biāo)記錯(cuò)誤的內(nèi)容去掉。

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

然后運(yùn)行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會(huì)根據(jù)log從頭開始重新bisect,錯(cuò)誤的標(biāo)記就被擦除了。

然后就是重新做人啦。

blame

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

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

git blame只能作用于單個(gè)文件。

$ 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) 第九行

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

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

第二部分是作者以及修改時(shí)間。

第三部分是行的內(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是一個(gè)版本管理工具,但在眾多版本中,肯定有一些版本是比較重要的,這時(shí)候我們希望給這些特定的版本打上標(biāo)簽。比如發(fā)布一年以后,程序的各項(xiàng)功能都趨于穩(wěn)定,可以在圣誕節(jié)發(fā)布v1.0版本。這個(gè)v1.0在git中就可以通過標(biāo)簽實(shí)現(xiàn)。

而git標(biāo)簽又分為兩種,輕量級(jí)標(biāo)簽和含附注標(biāo)簽。

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

含附注標(biāo)簽才是我們理解的那種標(biāo)簽,它是一個(gè)獨(dú)立的git對(duì)象。包含標(biāo)簽的名字,電子郵件地址和日期,以及標(biāo)簽說明。

創(chuàng)建

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

$ git tag v0.3

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

.git/refs/tags/v0.3

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

$ git tag -a v1.0

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

不寫呢?

fatal: no tag message?

創(chuàng)建完含附注標(biāo)簽后,.git目錄會(huì)多出兩個(gè)文件。

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

含附注標(biāo)簽不僅會(huì)創(chuàng)建一個(gè)指針,還會(huì)創(chuàng)建一個(gè)tag對(duì)象。

我們了解過git有四種對(duì)象類型,tag類型是我們認(rèn)識(shí)的最后一種。

我們看看該對(duì)象的類型。

$ git cat-file -t 80e79e9tag

再來看看該對(duì)象的內(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)的是一個(gè)commit對(duì)象,包含標(biāo)簽的名稱,打標(biāo)簽的人,打標(biāo)簽的時(shí)間以及標(biāo)簽說明。

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

$ git tag -a v1.0 36ff0f5

只需在后面加上commit的校驗(yàn)和。

查看

查看當(dāng)前git項(xiàng)目的標(biāo)簽列表,運(yùn)行g(shù)it tag命令不帶任何參數(shù)即可。

$ git tagv0.3 v1.0

注意git標(biāo)簽是按字母順序排列的,而不是按時(shí)間順序排列。

而且我并沒有找到分別查看輕量級(jí)標(biāo)簽和含附注標(biāo)簽的方法。

查看標(biāo)簽詳情可以使用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標(biāo)簽不能移動(dòng)對(duì)吧,但我們可以刪除它呀。

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

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

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

推送

默認(rèn)情況下,git push推送到遠(yuǎn)端倉庫并不會(huì)將標(biāo)簽也推送上去。如果想將標(biāo)簽推送到遠(yuǎn)端與別人共享,我們得顯式的運(yùn)行命令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ū)分輕量級(jí)標(biāo)簽和含附注標(biāo)簽。

一次性將本地標(biāo)簽推送到遠(yuǎn)端倉庫也是可以的。

$ git push origin --tags

13) remote

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

所以就引出了git中遠(yuǎn)端倉庫的概念。

概念

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

當(dāng)然。

我們探討一個(gè)問題,在離線狀態(tài)下,git是不是無從知道遠(yuǎn)端倉庫的任何狀態(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

實(shí)際上,git的分支有三種:

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

我是馬蹄疾

我們看一下本地的遠(yuǎn)端分支引用。

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

默認(rèn)的遠(yuǎn)端倉庫名就叫origin。它也有master分支指針,也有HEAD指針。

拉取

如果遠(yuǎn)端倉庫有新的提交或者新的分支,我們需要運(yùn)行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

這個(gè)命令是git fetch origin的縮寫。因?yàn)閛rigin是遠(yuǎn)端倉庫的默認(rèn)名稱,所以可以省略。如果有手動(dòng)添加的遠(yuǎn)端倉庫,那就必須指定遠(yuǎn)端倉庫的名稱了。

這個(gè)命令做了什么呢?

它會(huì)把新的提交和新的分支拉取到本地,然后更新本地的遠(yuǎn)端分支引用到最新的提交。

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

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

當(dāng)然,有一個(gè)更簡單的操作。

$ 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的一鍵操作。

推送

推送到遠(yuǎn)端的命令是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

如果當(dāng)前分支對(duì)遠(yuǎn)端分支設(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

有時(shí)候本地分支和遠(yuǎn)端分支同時(shí)有新的提交,直接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.

有兩種方式解決。

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

第二是強(qiáng)推。有時(shí)候我們就是想覆蓋遠(yuǎn)端對(duì)吧,也不是不行,但是必須十分謹(jǐn)慎。而且不要在公共分支上強(qiáng)制推送。

$ 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)

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

也可以簡寫成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'.

然后我們?cè)?git/config文件中能看到多了一條配置。

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

查看

查看遠(yuǎn)端倉庫的命令是git remote。

$ git remoteorigin

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

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

查看某個(gè)遠(yuǎn)端倉庫的信息,可以使用命令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)

添加

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

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

然后本地就多了一個(gè)遠(yuǎn)端倉庫。

$ git remotehorseshoe origin

除了添加遠(yuǎn)端倉庫,我們還可以添加本地分支對(duì)遠(yuǎn)端分支的追蹤。

$ 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í),也設(shè)置了對(duì)遠(yuǎn)端分支dev的追蹤,這樣下次推送的時(shí)候就不需要指定了。

當(dāng)然,遠(yuǎn)端分支引用必須得存在才行。

$ 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'

重命名

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

$ git remote rename nvpengyou gaoyuanyuan

查看遠(yuǎn)端倉庫列表。

$ git remotegaoyuanyuan origin

刪除

一般來說,一個(gè)git項(xiàng)目有一個(gè)遠(yuǎn)端倉庫就行了,其余的大多是臨時(shí)性的。所以總有一天要?jiǎng)h除它。

$ git remote rm horseshoe

查看遠(yuǎn)端倉庫列表。

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

Git專題一覽

? add

? commit

? branch

? checkout

? merge

? rebase

? reset

? revert

? stash

? view

? position

? tag

? remote

總結(jié)

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

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