2018(農曆年)封山之做,和我一塊兒嚼爛Git(兩萬字長文)

本文是『horseshoe·Git專題』系列文章之一,後續會有更多專題推出
GitHub地址(持續更新): https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
若是以爲對你有幫助,歡迎來GitHub點Star或者來個人博客親口告訴我

我剛開始接觸git的時候,徹底搞不清楚爲何這個操做要用這個命令,而那個操做要用那個命令。git

由於git不是一套注重用戶體驗的工具,git有本身的哲學。你首先要理解它的哲學,才能真正理解它是如何運做的。github

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

git有多強大,想必你們都有所耳聞。git有多使人困惑,想必你們也親身經歷過吧。數據庫

總而言之,學習git有兩板斧:其一,理解git的哲學;其二,在複雜實踐中積累處理問題的經驗。缺一不可。服務器

這篇文章就是第一板斧。markdown

做者我本身也還在路上,畢竟,這篇文章也只是個人學習心得,仍然須要大量的實踐。網絡

寫git有多個角度,反覆權衡,我最終仍是決定從命令的角度鋪陳,閱讀體驗也不至於割裂。app

想分章節閱讀請移步個人GitHub或者我的博客編輯器

困難年歲,共勉。分佈式

01) add

git是一個數據庫系統,git是一個內容尋址文件系統,git是一個版本管理系統。

沒錯,它都是。

不過咱們不糾結於git是什麼,咱們單刀直入,介紹git命令。

要將未跟蹤的文件和已跟蹤文件的改動加入暫存區,咱們能夠使用git add命令。

不過不少人嫌git add命令不夠語義化,畢竟這一步操做是加入暫存區呀。因此git又增長了另一個命令git stage,它們的效果是如出一轍的。

git倉庫、工做區和暫存區

進入主題以前,咱們先要介紹一下git倉庫、工做區和暫存區的概念。

git倉庫

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

能夠使用git init命令初始化一個git倉庫。

$ git init

也能夠使用git clone命令從服務器上克隆倉庫到本地。

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

而後你的本地就有了一個和服務器上如出一轍的git倉庫。

這裏要說明的是,clone操做並非將整個倉庫下載下來,而是隻下載.git目錄。由於關於git的一切祕密都在這個目錄裏面,只要有了它,git就能復原到倉庫的任意版本。

工做區(working directory)

工做區,又叫工做目錄,就是不包括.git目錄的項目根目錄。咱們要在這個目錄下進行手頭的工做,它就是版本管理的素材庫。你甚至能夠稱任何與工做有關的目錄爲工做區,只不過沒有.git目錄git是不認的。

暫存區(stage或者index)

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

那它爲何又被稱做index呢?由於暫存區在物理上僅僅是.git目錄下的index二進制文件。它就是一個索引文件,將工做區中的文件和暫存區中的備份一一對應起來。

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

你能夠把暫存區理解爲一個豬豬儲錢罐。咱們仍是孩子的時候,手裏有一毛錢就會丟進儲錢罐裏。等到儲錢罐搖晃的聲音變的渾厚時,或者咱們有一個心願急需用錢時,咱們就砸開儲錢罐,一次性花完。

類比到軟件開發,每當咱們寫完一個小模塊,就能夠將它放入暫存區。等到一個完整的功能開發完,咱們就能夠從暫存區一次性提交到版本庫裏。

這樣作的好處是明顯的:

  • 它能夠實現更小顆粒度的撤銷。
  • 它能夠實現批量提交到版本庫。

另外,添加到暫存區其實包含兩種操做。一種是將還未被git跟蹤過的文件放入暫存區;一種是已經被git跟蹤的文件,將有改動的內容放入暫存區。

放入暫存區

git默認是不會把工做區的文件放入暫存區的。

$ git status

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

咱們看到文件如今被標註爲Untracked files。表示git目前還沒法追蹤它們的變化,也就是說它們還不在暫存區裏。

那麼咱們如何手動將文件或文件夾放入暫存區呢?

$ git add .

上面的命令表示將工做目錄全部未放入暫存區的文件都放入暫存區。這時文件的狀態已經變成了Changes to be committed,表示文件已經放入暫存區,等待下一步提交。每一次add操做其實就是爲加入的文件或內容生成一份備份。

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

$ git add -A

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

$ git add README.md

暫存整個文件夾也是同樣的道理。由於git會遞歸暫存文件夾下的全部文件。

$ git add src

把歷來沒有被標記過的文件放入暫存區的命令是git add,暫存區中的文件有改動也須要使用git add命令將改動放入暫存區。

這時狀態變成了Changes not staged for commit

$ git status

On 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 add -a

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

跟蹤內容

假設咱們已經將文件加入暫存區,如今咱們往文件中添加內容,再次放入暫存區,而後查看狀態。

$ git status

On 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

哎,忽然變的有意思了。爲何一個文件會同時存在兩種狀態,它是薛定諤的貓麼?

想象一下,我想在一個文件中先修復一個bug而後增長一個feather,我確定但願分兩次放入暫存區,這樣能夠實現顆粒度更細的撤銷和提交。可是若是git是基於文件作版本管理的,它就沒法作到。

因此git只能是基於內容作版本管理,而不是基於文件。版本管理的最小單位叫作hunk,所謂的hunk就是一段連續的改動。一個文件同時有兩種狀態也就不稀奇了。

objects

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

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

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

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

SHA-1是一種哈希加密算法,它的特色是隻要加密的內容相同,獲得的校驗和也相同。固然這種說法是不許確的,可是碰撞的機率極低。

git除了用內容來計算校驗和以外,還加入了一些其餘信息,目的也是爲了進一步下降碰撞的機率。

重點是,SHA-1算法是根據內容來計算校驗和的,跟前面講的git跟蹤內容相呼應。git被稱爲一個內容尋址文件系統不是沒有道理的。

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

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

若是你真的作了實驗,你會發現即使兩個文件的文件名和文件格式都不同,只要內容同樣,它們的校驗和就是同樣的,而且就是上面列出的校驗和。

如今你們應該對git跟蹤內容這句話有更深的理解了。

相同內容引用一個對象

雖然開發者要極力避免這種狀況,可是若是一個倉庫有多個內容相同的文件,git會如何處理呢?

咱們初始化一個本地倉庫,新建兩個不一樣名的文件,但文件內容都是## git is awesome。運行git add .命令以後看看神祕的objects目錄下會發生什麼?

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f

只有一個目錄,並且校驗和跟以前如出一轍。

其實你們確定早就想到了,git這麼優秀的工具,怎麼可能會讓浪費磁盤空間的事情發生呢?既然多個文件的內容相同,確定只保存一個對象,讓它們引用到這裏來就行了。

文件改動對應新對象

如今咱們猜想工做區的文件和objects目錄中的對象是一一對應起來的。但事實真的是這樣嗎?

咱們初始化一個本地倉庫,新建一個markdown文件,運行git add .命令。如今objects目錄中已經有了一個對象。而後往文件中添加內容## git is awesome。再次運行git add .命令。

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

哎,objects目錄中出現了兩個對象。第一個對象確定對應空文件。第二個對象咱們太熟悉了,對應的是添加內容後的文件。

再次強調,git是一個版本管理系統,文件在它這裏不是主角,版本纔是。剛纔咱們暫存了兩次,能夠認爲暫存區如今已經有了兩個版本(暫存區的版本其實是內容備份,並非真正的版本)。固然就須要兩個對象來保存。

文件改動全量保存

初始化一個本地倉庫,往工做區添加lodash.js未壓縮版本,版本號是4.17.11,體積大約是540KB。運行git add .命令後objects目錄下面出現一個對象,體積大約是96KB

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70

咱們對lodash.js文件內容做一個小小的改動,將版本號從4.17.11改成4.17.10,再次運行git add .命令。而後你們會驚奇的發現objects目錄下有兩個對象了。驚奇的不是這個,而是第二個對象的體積也是大約96KB

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

明明只改了一個數字而已,第二個對象卻仍是這麼大。

前面剛誇git會精打細算,怎麼到這裏就不知深淺了?這是由於多個文件內容相同的狀況,引用到同一個對象並不會形成查詢效率的下降,而暫存區的多個對象之間若是隻保存增量的話,版本之間的查詢和切換須要花費額外的時間,這樣作是不划算的。

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

重命名會拆分紅刪除和新建兩個動做

初始化一個本地倉庫,新建一個文件,運行git add .命令。而後重命名該文件,查看狀態信息。

$ git status

On 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的內部機制致使的。生成對象的時候,它發現倉庫中叫這個名字的文件不見了,因而標記爲已刪除,又發現有一個新的文件名是以前沒有標記過的,因而標記爲未跟蹤。由於它只是重命名而已,文件內容並無改變,因此能夠共享對象,並不會影響效率。

blob對象

git的一切祕密都在.git目錄裏。由於它擁有項目的完整信息,因此git必定是把備份存在了某個地方。git把它們存在了哪裏,又是如何存儲它們的呢?

這些備份信息,git統一稱它們爲對象。git總共有四種對象類型,都存在.git/objects目錄下。

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

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

由於它只存儲內容和大小,因此兩個文件即使文件名和格式徹底不同,只要內容相同,就能夠共享一個blob對象。

注意blob對象和工做目錄的文件並非一一對應的,由於工做目錄的文件幾乎會被屢次添加到暫存區,這時一個文件會對應多個blob對象。

index

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

是的,暫存區並非一塊區域,只是一個文件,確切的說,是一個索引文件。

它保存了項目結構、文件名、時間戳以及blob對象的引用。

工做區的文件和blob對象之間就是經過這個索引文件關聯起來的。

打包

還記得咱們在文件改動全量保存小節裏講到,git魚和熊掌想兼得麼?

又想全量保存,不下降檢索和切換速度,又想盡量壓榨體積。git是怎麼作到的呢?

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

打包的時候保存文件最新的全量版本,基於該文件的歷史版本的改動則只保存diff信息。由於開發者不多會切換到較早的版本中,因此這時候效率就能夠部分犧牲。

須要注意的是,全部的git對象都會被打包,而不只僅是blob對象。

git也有一個git gc命令能夠手動執行打包。

$ git gc

Counting 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對象文件的實際內容。

.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是一個版本管理系統。它的終極目的就是將項目特定時間的信息保留成一個版本,以便未來的回退和查閱。

咱們已經介紹了暫存區,暫存區的下一步就是版本庫,而促成這一步操做的是git commit命令。

提交

暫存區有待提交內容的狀況下,若是直接運行git 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 nothing
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a.md

若是我就是不寫提交說明呢?

Aborting commit due to empty commit message.

看到沒有,提交信息在git中時必填的。

若是提交說明很少,能夠加參數-m直接在命令後面填寫提交說明。

$ git commit -m "commit for nothing"

你甚至能夠將加入暫存區和提交一併作了。

$ git commit -am "commit for nothing"

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

重寫提交

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

$ git log --oneline

8274473 (HEAD -> master) commit for nothing

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

這時我就能夠使用git commit --amend命令。

首先修改高曉鬆高圓圓

而後執行git add a.md命令。

最後重寫提交。git會跳往默認或者自定義編輯器提示你修改commit說明。固然你也能夠不改。

$ git commit --amend

commit 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 --oneline

8a71ae1 (HEAD -> master) commit for nothing

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

tree對象和commit對象

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

第一是tree對象。

它存儲子目錄和子文件的引用。若是隻有blob對象,那版本庫將是一團散沙。正由於有tree對象將它們的關係登記在冊,才能構成一個有結構的版本庫。

添加到暫存區操做並不會生成tree對象,這時項目的結構信息存儲在index文件中,直到提交版本庫操做,纔會爲每個目錄分別生成tree對象。

第二是commit對象。

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

做者和提交者一般是一我的,但也存在不一樣人的狀況。

objects

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

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

首先運行git add命令。咱們清楚,這會在.git/objects目錄下生成一個blob對象,由於目前兩個文件都是空文件,共享一個blob對象。

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

如今咱們運行git 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>查看這些對象的類型發現,其中有一個blob對象,三個tree對象,一個commit對象。

這是第一個tree對象。

$ git cat-file -t 93810bb

tree
$ git cat-file -p 93810bb

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.md

這是第二個tree對象。

$ git cat-file -t 520c9f9

tree
$ git cat-file -p 520c9f9

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    a.md

這是第三個tree對象。

$ git cat-file -t 0b785fa

tree
$ git cat-file -p 0b785fa

040000 tree 93810bbde0f994d41ef550324a2c1ad5f9278e19    lib
040000 tree 520c9f9f61657ca1e65a288ea77d229a27a8171b    src

能夠看到,提交時每一個目錄都會生成對應的tree對象。

而後咱們再來看commit對象。

$ git cat-file -t 4911ff6

commit
$ git cat-file -p 4911ff6

tree 0b785fa11cd93f95b1cab8b9cbab188edc7e04df
parent c4731cfab38f036c04de93facf07cae496a124a2
author veedrin <veedrin@qq.com> 1546395770 +0800
committer veedrin <veedrin@qq.com> 1546395770 +0800
commit for nothing

能夠看到,commit會關聯根目錄的tree對象,由於關聯它就能夠關聯到全部的項目結構信息,所謂擒賊先擒王嘛。它也要關聯父commit,也就是它的上一個commit,這樣才能組成版本歷史。固然,若是是第一個commit那就沒有父commit了。而後就是commit說明和一些參與者信息。

咱們總結一下,git add命令會爲加入暫存區的內容或文件生成blob對象,git commit命令會爲加入版本庫的內容或文件生成tree對象和commit對象。至此,四種git對象咱們見識了三種。

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

所謂暫存區,就是不必定會保存爲版本的信息,只是一個準備的臨時場所。git認爲在git add的時候生成tree對象是不夠高效的,徹底能夠等版本定型時再生成。而版本定型以前的結構信息存在index文件中就行了。

03) branch

分支是使得git如此靈活的強大武器,正是由於有巧妙的分支設計,衆多的git工做流才成爲可能。

如今咱們已經知道commit對象其實就是git中的版本。那咱們要在版本之間切換難道只能經過指定commit對象毫無心義的SHA-1值嗎?

固然不是。

在git中,咱們能夠經過將一些指針指向commit對象來方便操做,這些指針即是分支。

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

你能夠認爲它僅僅是一個指針,指向一個commit對象節點。

你也能夠認爲它是指針指向的commit對象節點追溯到某個交叉節點之間的commit歷史。

嚴格的來講,一種叫分支指針,一種叫分支歷史。不過實際使用中,它們在名字上經常不做區分。

因此咱們須要意會文字背後的意思,它究竟說的是分支指針仍是分支歷史。

大多數時候,它指的都是分支指針。

master分支

剛剛初始化的git倉庫,會發現.git/refs/heads目錄下面是空的。這是由於目前版本庫裏尚未任何commit對象,而分支必定是指向commit對象的。

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

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

咱們看一下master文件到底有什麼。

$ cat .git/refs/heads/master

6b5a94158cc141286ac98f30bb189b8a83d61347

40個字符,明顯是某個git對象的引用。再識別一下它的類型,發現是一個commit對象。

$ git cat-file -t 6b5a941

commit

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

HEAD指針

形象的講,HEAD就是景區地圖上標註你當前在哪裏的一個圖標。

你當前在哪裏,HEAD就在哪裏。它通常指向某個分支,由於通常咱們都會在某個分支之上。

由於HEAD是用來標註當前位置的,因此一旦HEAD的位置被改變,工做目錄就會切換到HEAD指向的分支。

$ git log --oneline

f53aaa7 (HEAD -> master) commit for nothing

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

$ git log --oneline

cb64064 (HEAD -> master) commit for nothing again
324a3c0 commit for nothing
$ git checkout 324a3c0

Note: 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 --oneline

324a3c0 commit for nothing

這個時候的HEAD就叫作detached HEAD

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

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

若是不當心這麼作了,要麼在原地新建一個分支,要麼將已有的分支強行移動過來。確保它不會被遺忘。

死亡不是終結,遺忘纔是。——尋夢環遊記

建立

除了默認的master分支,咱們能夠隨意建立新的分支。

$ git branch dev

一個dev分支就建立好了。

查看

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

$ git branch

  dev
* master

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

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

$ git branch -a

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

刪除

通常分支合併完以後就再也不須要了,這時就要將它刪除。

$ git branch -d dev

Deleted branch dev (was 657142d).

有時候咱們會獲得不同的提示。

$ git branch -d dev

error: 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是針對當前分支來講的,意思是你要刪除的分支還有內容沒有合併進當前分支,你肯定要刪除它嗎?

大多數時候,固然是要的。

$ git branch -D dev

Deleted branch dev (was 657142d).

-D--delete --force的縮寫,你也能夠寫成-df

須要注意的是,刪除分支僅僅是刪除一個指針而已,並不會刪除對應的commit對象。不過有可能刪除分支之後,這一串commit對象就沒法再被引用了,從而被垃圾回收機制刪除。

04) checkout

在git中,暫存區裏有若干備份,版本庫裏有若干版本。留着這些東西確定是拿來用的對吧,怎麼用呢?當我須要哪一份的時候我就切換到哪一份。

git checkout命令就是用來幹這個的,官方術語叫作簽出

怎麼理解checkout這個詞呢?checkout本來指的是消費結束服務員要與你覈對一下帳單,結完帳以後你就能夠走了。在git中核對指的是diff,比較兩份版本的差別,若是發現沒有衝突那就能夠切換過來了。

底層

咱們知道HEAD指針指向當前版本,而git checkout命令的做用是切換版本,它們確定有所關聯。

目前HEAD指針指向master分支。

$ cat .git/HEAD

ref: refs/heads/master

若是我切換到另外一個分支,會發生什麼?

$ git checkout dev

Switched to branch 'dev'
$ cat .git/HEAD

ref: refs/heads/dev

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

符號

在進入正題以前,咱們要先聊聊git中的兩個符號~^

若是咱們要從一個分支切換到另外一個分支,那還好說,足夠語義化。可是若是咱們要切換到某個commit,除了兢兢業業的找到它的SHA-1值,還有什麼辦法快速的引用到它呢?

好比說咱們能夠根據commit之間的譜系關係快速定位。

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

4e76510fe8bb3c69de12068ab354ef37bba6da9d

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

$ git rev-parse HEAD~0

4e76510fe8bb3c69de12068ab354ef37bba6da9d

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

$ git rev-parse HEAD^0

4e76510fe8bb3c69de12068ab354ef37bba6da9d

~符號數量的堆砌或者~數量的寫法定位第幾代父commit

$ git rev-parse HEAD~~

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b
$ git rev-parse HEAD~2

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b

^數量的寫法定位第幾個父commit

注意,^定位的是當前基礎的父commit。

$ git rev-parse HEAD^

2ec837440051af433677f786e502d1f6cdeb0a4a
$ git rev-parse HEAD^1

2ec837440051af433677f786e502d1f6cdeb0a4a

由於當前commit只有一個父commit,因此定位第二個父commit會失敗。

$ git rev-parse HEAD^2

HEAD^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>...]'

~數量^數量的寫法或者^數量^數量的寫法定位第幾代父commit的第幾個父commit

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

$ git rev-parse HEAD~^0

2ec837440051af433677f786e502d1f6cdeb0a4a

好比這裏定位的是當前commit的第一代父commit的第一個父commit。再次注意,^定位的是當前基礎的父commit。

$ git rev-parse HEAD~^1

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b

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

$ git rev-parse HEAD~^2

7c0a8e3a325ce1b5a1cdeb8c89bef1ecf17c10c9

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

$ git rev-parse HEAD~^3

HEAD~^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命令若是不帶任何參數,默認會加上HEAD參數。而HEAD指針指向的就是當前commit。因此它並不會有任何簽出動做。

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

因此git checkout命令不帶任何參數,意思就是比較當前commit和暫存區之間的差別。

$ git checkout

A   b.md
$ git checkout HEAD

A   b.md

切換到commit

開發者用的最多的固然是切換分支。其實checkout後面不只能夠跟分支名,也能夠跟commit的校驗和,還能夠用符號定位commit。

$ git checkout dev

Switched to branch 'dev'
$ git checkout acb71fe

Note: 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~2

Note: 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 branch <branch>是作不到的。這時git checkout命令能夠提供一個快捷操做,建立分支和切換分支一步到位。

$ git checkout -b dev

Switched to a new branch 'dev'

暫存區文件覆蓋工做區文件

git checkout不只能夠執行切換commit這種全量切換,它還能以文件爲單位執行微觀切換。

$ git status

On 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 status

On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   a.md

由於暫存區覆蓋了工做區,因此工做區的改動就被撤銷了,如今只剩下暫存區的改動等待提交。其實至關於撤銷文件在工做區的改動,只不過它的語義是覆蓋。這個命令沒有任何提示,直接撤銷工做區改動,要謹慎使用。

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

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

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

獨立的--參數在Linux命令行中指的是:視後面的參數爲文件名。當後面跟的是文件名的時候,最好加上獨立的--參數,以避免有歧義。

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

若是你以爲僅僅撤銷一個文件在工做區的改動不過癮,你不是針對誰,你是以爲工做區的改動都是垃圾。那麼還有一個更危險的命令。

$ git checkout -- .

.表明當前目錄下的全部文件和子目錄。這條命令會撤銷全部工做區的改動。

當前commit文件覆蓋暫存區文件和工做區文件

若是執行git checkout -- <file>的時候加上一個分支名或者commit的校驗和,效果就是該文件的當前版本會同時覆蓋暫存區和工做區。至關於同時撤銷文件在暫存區和工做區的改動。

$ git status

On 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 status

On branch master
nothing to commit, working tree clean

最後再提醒一下,運行git checkout命令做用於文件時,即使覆蓋內容與被覆蓋內容有衝突,也會直接覆蓋,因此這真的是悶聲打雷式的git命令,必定要抽本身幾個耳刮子方可放心食用。

05) merge

能夠方便的建立分支是git如此受歡迎的重要緣由,利用git checkout <branch>也讓開發者在分支之間穿梭自如。然而百川終入海,其餘分支上完成的工做終究是要合併到主分支上去的。

因此咱們來看看git中的合併操做。

首先說明,執行git merge命令以前須要一些準備工做。

$ git merge dev

error: 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

合併操做以前必須保證暫存區內沒有待提交內容,不然git會阻止合併。這是由於合併以後,git會將合併後的版本覆蓋暫存區。因此會有丟失工做成果的危險。

至於工做區有待添加到暫存區的內容,git倒不會阻止你。可能git以爲它不重要吧。

不過最好仍是保持一個乾淨的工做區再執行合併操做。

不一樣分支的合併

不一樣分支指的是要合併的兩個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實際上會建立一個新的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,當前分支指針向右移。若是有那就要求開發者自行解決。

因此在三方合併中,合併主客體的共同祖先commit只是一個參照物。

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

它指的是開發者當前在一個commit節點上,要將同一個分支上更新的commit節點合併進來。

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

這時候會發生什麼呢?

這至關於更新當前分支指針,因此只須要將當前分支指針向下遊移動,讓合併主體與合併客體指向同一個commit便可。這時並不會產生一個新的commit。

用三方合併的概念來理解,合併主體commit合併主客體的共同祖先commit是同一個commit,合併主體commit相對於合併主客體的共同祖先commit的diff爲空,合併客體commit相對於合併主客體的共同祖先commit的diff與空diff合併仍是它本身,因此移動過去就好了,並不須要生成一個新的commit。

$ git merge dev

Updating 9242078..631ef3a
Fast-forward
 a.md | 2 ++
 1 file changed, 2 insertions(+)
C0 -- C1 -- C2 -- C3(HEAD -> master, dev)

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

好比說git pull的時候常常發生這種狀況。一般由於遠端有更新的commit咱們才須要執行git pull命令,這時遠端就是合併客體,本地就是合併主體,遠端的分支指針在下游,也會觸發Fast forward

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

若是合併主體在合併客體的下游,那合併主體自己就包含合併客體,合併操做並不會產生任何效果。

C0 -- C1 -- C2(dev) -- C3(HEAD -> master)
$ git merge dev

Already 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 ccc

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

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

合併有衝突

git merge操做並不老是如此順利的。由於有時候要合併的兩個分支不是同一我的的,就會有很大的機率遇到兩人同時修改文件某一行的狀況。git不知道該用誰的版本,它認爲兩個分支遇到了衝突。

這時就須要開發者手動的解決衝突,才能讓git繼續合併。

$ git merge dev

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

咱們來看一下有衝突的文件是什麼樣的。

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

運行git status命令。

$ git status

On 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.

誒,被拒絕了。是否是想起了本身的情場故事?

當咱們解決衝突的時候,工做區已經有改動,因此須要先提交到暫存區。

$ git add a.md
$ git commit -m "fix merge conflict"

[master 9b32d4d] fix merge conflict

運行git add 命令以後你也能夠用git merge --continue來替代git commit命令。它會讓後面的行爲跟沒有衝突時的行爲表現的同樣。

若是你遇到衝突之後不知道如何解決,由於你要去詢問你的合做夥伴爲何這樣改。這時你確定想回到合併之前的狀態。

這對git來講很容易。只須要運行git merge --abort命令便可。

$ git merge --abort

該命令沒法保證恢復工做區的修改,因此最好是在合併以前先讓工做區保持乾淨。

06) rebase

git merge命令會生成一個新的合併commit。若是你有強迫症,不喜歡這個新的合併commit,git也有更加清爽的方案能夠知足你,它就是git rebase命令。

git就是哆啦A夢的口袋。

rebase翻譯過來是變基。意思就是將全部要合併進來的commit在新的基礎上從新提交一次。

基礎用法

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

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

mergerebase後跟的分支名是不同的。合併是合併進來,變基是變基過去,大家感覺一下。

$ git rebase master

First, 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

如今最近共同祖先與當前分支之間的全部commit都被複制到master分支以後,而且將HEAD指針與當前分支指針切換過去。這招移花接木玩的很溜啊,若是你置身其中根本分不出區別。

原來的commit還在嗎?還在,若是你記得它的commit校驗和,仍然能夠切換過去,git會提示你當前處於detached HEAD狀態下。只不過沒有任何分支指針指向它們,它們已經被拋棄了,剩餘的時光就是等待git垃圾回收命令清理它們。

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

git rebase完並無結束,由於我變基的目標分支是master,而當前分支是dev。我須要切換到master分支上,而後再合併一次。

$ git checkout master
$ git merge dev

誒,說來講去,仍是要合併啊?

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

若是我要變基的本體分支不是當前分支行不行?也是能夠的。

$ git rebase master dev

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

裁剪commit變基

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

有了精確的基因編輯技術,媽媽不再用擔憂你長的 啦。
C0 -- C1 -- C2 -- C3(master)
       \
        C4 -- C5 -- C6(dev)
         \
          C7 -- C8(HEAD -> hotfix)
$ git rebase --onto master dev hotfix

First, 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參數就是那把基因編輯的剪刀。

它會把hotfix分支hotfix分支與dev分支的最近共同祖先之間的commit裁剪下來,複製到目標基礎點上。注意,所謂的之間指的都是不包括最近共同祖先commit的範圍,好比這裏就不會複製C4commit。

$ git rebase --onto master dev

First, 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 dev

First, 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的某一行,引起衝突。git已經給咱們提示了,大致上和merge的操做一致。

咱們能夠手動解決衝突,而後執行git addgit rebase --continue來完成變基。

若是你不想覆蓋目標commit的內容,也能夠跳過這個commit,執行git rebase --skip。可是注意,這會跳過有衝突的整個commit,而不只僅是有衝突的部分。

後悔藥也是有的,執行git rebase --abort,乾脆就放棄變基了。

cherry-pick

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

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

git cherry-pick命令能夠挑選任意commit變基到目標commit上。你負責挑,它負責基。

用法

只須要在git cherry-pick命令後跟commit校驗和,就能夠將它應用到目標commit上。

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

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

$ git cherry-pick C6

[master dc342e0] c6
 Date: Mon Dec 24 09:13:57 2018 +0800
 1 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] c4
 Date: Mon Dec 24 09:12:58 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c4.md
[master 161d993] c7
 Date: Mon Dec 24 09:14:12 2018 +0800
 1 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正好是連續的呢?

$ git cherry-pick C3...C7

[master d16c42e] c4
 Date: Mon Dec 24 09:12:58 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c4.md
[master d16c42e] c6
 Date: Mon Dec 24 09:13:57 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c6.md
[master a4d5976] c7
 Date: Mon Dec 24 09:14:12 2018 +0800
 1 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所謂的從某某開始,通常都是不包括某某的,這裏也同樣。

有沒有發現操做連續commit的git cherry-pickgit rebase的功能已經很是接近了?因此呀,git cherry-pick也是變基,只不過一邊變基一邊喂櫻桃給你吃。

衝突

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

C0 -- C1(HEAD -> master)
 \
  C2(dev)
$ git cherry-pick C2

error: 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'

手動解決衝突,執行git add命令而後執行git cherry-pick --continue命令。

若是被唬住了想還原,執行git cherry-pick --abort便可。

變基仍是合併

這是一個哲學問題。

有一種觀點認爲,倉庫的commit歷史應該記錄實際發生過什麼。因此若是你將一個分支合併進另外一個分支,commit歷史中就應該有這一次合併的痕跡,由於它是實實在在發生過的。

另外一種觀點則認爲,倉庫的commit歷史應該記錄項目過程當中發生過什麼。合併非項目開發自己帶來的,它是一種額外的操做,會使commit歷史變的冗長。

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

07) reset

git checkout命令能夠在版本之間隨意切換,它的本質是移動HEAD指針。

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

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

底層

git reset命令與git checkout命令的區別在於,它會把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)

同時重置暫存區和工做區的改動

當你在 git reset 命令後面加 --hard 參數時,暫存區和工做區的內容都會重置爲重置後的commit內容。也就是說暫存區和工做區的改動都會清空,至關於撤銷暫存區和工做區的改動。

並且是沒有確認操做的喲。

$ git status

On 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 status

On branch master
nothing to commit, working tree clean

僅重置暫存區的改動

git reset 命令後面加 --mixed 參數,或者不加參數,由於--mixed參數是默認值,暫存區的內容會重置爲重置後的commit內容,工做區的改動不會清空,至關於撤銷暫存區的改動。

一樣也是沒有確認操做的喲。

$ git status

On 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 status

On 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命令不加參數默認就是--mixed,不加操做對象默認就是HEAD,因此單純的git reset命令至關於git reset --mixed HEAD命令。

那這又意味着什麼呢?

這意味着從當前commit重置到當前commit,沒有變化對吧?可是--mixed參數會撤銷暫存區的改動對不對,這就是它的效果。

同時保留暫存區和工做區的改動

若是 git reset 命令後面加 --soft 參數,鋼鐵直男的溫柔,你懂的。僅僅是重置commit而已,暫存區和工做區的改動都會保留下來。

更溫柔的是,重置前的commit內容與重置後的commit內容的diff也會放入暫存區。

$ git status

On 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 --staged

diff --git a/a.md b/a.md
index 4a77268..fde8dcd 100644
--- a/a.md
+++ b/a.md
@@ -1,2 +1,3 @@
 apple
 banana
+cherry
$ git reset --soft HEAD^
$ git status

On 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 --staged

diff --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內容與重置後的commit內容的diff,能夠看到,它已經在暫存區了。

文件暫存區改動撤回工做區

git reset命令後面也能夠跟文件名,它的做用是將暫存區的改動重置爲工做區的改動,是git add -- <file>的反向操做。

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

它並不會撤銷工做區原有的改動。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
$ git reset -- a.md

Unstaged changes after reset:
M   a.md
$ git status

On 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命令後面也能夠跟文件名,它的做用是撤銷工做區的改動,須要注意區分。

文件若干commit版本撤回工做區

若是git reset命令後跟一個commit校驗和,它會把該commit與全部後代commit的diff重置到工做區。

意思就是將該文件重置回你指定的commit版本,可是在你指定的commit以後的改動我也給你留着,就放到工做區裏吧。

$ git diff --staged

# 空
git reset HEAD~4 -- a.md

Unstaged changes after reset:
M   a.md
$ git diff --staged

diff --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命令比較工做區和暫存區的內容。能夠看到初始工做區和暫存區是一致的,重置文件到4個版本以前,發現工做區比暫存區多了不少改動,這些都是指定commit以後的提交被重置到工做區了。

08) revert

有時候咱們想撤回一個commit,可是這個commit已經在公共的分支上。若是直接修改分支歷史,可能會引發一些沒必要要的混亂。這個時候,git revert命令就派上用場了。

revert翻譯成中文是還原。我以爲稱它爲對衝更合理。對衝指的是同時進行兩筆行情相關、方向相反、數量至關、盈虧相抵的交易,這麼理解git revert命令一針見血。

由於它的做用就是生成一個新的、徹底相反的commit。

命令

git revert後跟你想要對衝的commit便可。

$ git revert HEAD

Revert "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.mdrevert操做就會執行刪除命令。在工做目錄看起來就像添加文件操做被撤銷了同樣,實際上是被對衝了。

它不會改變commit歷史,只會增長一個新的對衝commit。這是它最大的優勢。

衝突

反向操做也會有衝突?你逗個人吧。

若是你操做的是最新的commit,那固然不會有衝突了。

那要操做的是之前的commit呢?

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

好比a.mdC0內容爲空,C1修改文件內容爲appleC2修改文件內容爲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'

咱們看一下文件內容。

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

手動解決衝突,執行git add命令而後執行git revert --continue命令完成對衝操做。

取消revert操做只須要執行git revert --abort便可。

09) stash

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

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

儲藏

想要儲藏手頭的工做,只需運行git stash命令。

$ git status

On 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 stash

Saved working directory and index state WIP on master: 974a2f2 update

WIPwork in progress的縮寫,指的是進行中的工做。

$ git status

On 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跟蹤的文件以外,工做區和暫存區的內容都會被儲藏起來。如今你能夠切換到其餘分支進行下一步工做了。

查看

咱們看一下儲藏列表。

$ git stash list

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

恢復

等咱們完成其餘工做,確定要回到這裏,繼續進行中斷的任務。

$ git stash apply

On 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
    modified:   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的變動也跑到工做區了?是的,git stash默認會將暫存區和工做區的儲藏所有恢復到工做區。若是我就是想原樣恢復呢?

$ git stash apply --index

On 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

加一個參數--index就會讓工做區的歸工做區,讓暫存區的歸暫存區。

還有一點須要注意,恢復儲藏的操做能夠應用在任何分支,它也不關心即將恢復儲藏的分支上,工做區和暫存區是否乾淨。若是有衝突,自行解決就是了。

咱們瀏覽過儲藏列表,說明git stash apply僅僅是恢復了最新的那一次儲藏。

$ git stash apply stash@{1}

指定儲藏的名字,咱們就能夠恢復列表中的任意儲藏了。

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

$ git stash list

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

誒,發現仍是兩條。我不是已經恢復了一條麼?

apply這個詞很巧妙,它只是應用,它可不會清理。

清理

想要清理儲藏列表,我們得顯式的運行git stash drop命令。

$ git stash drop stash@{1}
$ git stash list

stash@{0}: WIP on master: 974a2f2 apple

如今就真的沒有了。但願你沒有喝酒🙃。

git還給咱們提供了一個快捷操做,運行git stash pop命令,同時恢復儲藏和清理儲藏。

$ git stash pop

10) view

有四個git命令能夠用來查看git倉庫相關信息。

status

git status命令的做用是同時展現工做區和暫存區的diff、暫存區和當前版本的diff、以及沒有被git追蹤的文件。

$ git status

On 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命令之一了,每次提交以前都要看一下。

git status -v命令至關於git status命令和git diff --staged之和。

$ git status -v

On 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 -vv

On 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參數,給出的結果頗有意思。

$ git status -s

M  a.md
 M b.md
?? c.md

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

第一個位置是該文件在暫存區的狀態,第二個位置是該文件在工做區的狀態。好比,如下信息顯示a.md文件在暫存區有改動待提交,在工做區也有改動待暫存。

MM a.md

縮寫的狀態碼主要有這麼幾種:

狀態碼 含義
M 文件內容有改動
A 文件被添加
D 文件被刪除
R 文件被重命名
C 文件被複制
U 文件衝突未解決
? 文件未被git追蹤
! 文件被git忽略
?!所表明的狀態由於沒有進入git版本系統,因此任什麼時候候兩個位置都是同樣的。就像 ??或者 !!這樣。

show

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

$ git show

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

git show至關於git show HEAD,顯示當前HEAD指向的commit對象的信息。

固然,你也能夠查看某個git對象的信息,後面跟上git對象的校驗和就行。

$ git show 38728d8

tree 38728d8
README.md

diff

git diff命令能夠顯示兩個主體之間的差別。

工做區與暫存區的差別

單純的git diff命令顯示工做區與暫存區之間的差別。

$ git diff

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

由於是兩個主體之間的比較,git永遠將兩個主體分別命名爲ab

也能夠只查看某個文件的diff。固然這裏依然是工做區與暫存區之間的差別。

$ git diff a.md

暫存區與當前commit的差別

git diff --staged命令顯示暫存區與當前commit的差別。

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

$ git diff --staged

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

一樣,顯示某個文件暫存區與當前commit的差別。

$ git diff --staged a.md

兩個commit之間的差別

咱們還能夠用git diff查看兩個commit之間的差別。

$ git diff C1 C2

diff --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 C1

diff --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.md

diff --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 log

commit 7e2514419ec0f75d1557d3d8165a7e7969f08349
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 cde34665b49033d7b8aed3a334c3e2db2200b4dd
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:54:59 2018 +0800
    a.md

若是要查看每一個commit具體的改動,添加-p參數,它是--patch的縮寫。

$ git log -p

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

你還能夠控制顯示最近幾條。

$ git log -p -1

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

-p有點過於冗餘,只是想查看文件修改的統計信息的話,能夠使用--stat參數。

$ git log --stat

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

還以爲冗餘?只想看提交說明,有一個--oneline能夠幫到你。

$ git log --oneline

4ad50f6 (HEAD -> master) 添加c.md文件
4d34677 添加b.md文件
cde3466 添加a.md文件

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

$ 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 cde34665b49033d7b8aed3a334c3e2db2200b4dd
  Author: veedrin <veedrin@qq.com>
  Date:   Sat Dec 29 11:54:59 2018 +0800
      a.md

我知道大家確定又以爲冗餘,--graph--oneline食用更佳喲。

$ git log --graph --oneline

* 7e25144 (HEAD -> master) c.md
* 4d34677 b.md
* cde3466 a.md

11) position

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

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

bisect

有時候咱們發現程序有bug,可是回退幾個版本都不解決問題。說明這個bug是一次很老的提交致使的,也不知道當時怎麼就沒察覺。

那怎麼辦呢?繼續一個一個版本的回退?

估計Linus Torvalds會鄙視你吧。

爲了專一於工做,不分心來鄙視你,Linus Torvalds在git中內置了一套定位bug的命令。

你們都玩過猜數字遊戲吧。主持人悄悄寫下一個數,給你們一個數字區間,而後你們輪流開始切割,誰切到主持人寫的那個數就要自罰三杯了。

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

使用

假設目前的git項目歷史是這樣的。

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

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

運行git bisect start命令,後跟你要定位的區間中最新的commit和最老的commit。

$ git bisect start HEAD C0

Bisecting: 4 revisions left to test after this (roughly 2 steps)
[ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4

而後你就發現HEAD指針自動的指向了C4commit。若是範圍是奇數位,那取中間就好了,若是範圍是偶數位,則取中間更偏老的那個commit,就好比這裏的C4commit。

$ git bisect good

Bisecting: 2 revisions left to test after this (roughly 1 step)
[97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6

HEAD指針指向C4commit後,你應該運行一下程序,若是沒問題,那說明有bug的提交在它以後。咱們只須要告訴git當前commit以及更老的commit都是好的。

而後HEAD指針就自動指向C6commit。

繼續在C6commit運行程序,結果復現了bug。說明問題就出在C6commit和C4commit之間。

$ git bisect bad

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[a7e09bd3eab7d1e824c0338233f358cafa682af0] C5

C6commit標記爲bad以後,HEAD指針自動指向C5commit。再次運行程序,依然能復現bug。話很少說,標記C5commit爲bad

$ git bisect bad

a7e09bd3eab7d1e824c0338233f358cafa682af0 is the first bad commit

由於C4commit和C5commit之間已經不須要二分了,git會告訴你,C5commit是你標記爲bad的最先的commit。問題就應該出在C5commit上。

git bisect reset

Previous HEAD position was a7e09bd... C5
Switched to branch 'master'

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

另外,git bisect oldgit bisect good的效果相同,git bisect newgit bisect bad的效果相同,這是由於git考慮到,有時候開發者並非想定位bug,只是想定位某個commit,這時候用good bad就會有點彆扭。

後悔

git bisect確實很強大,但若是我已經bisect若干次,結果不當心把一個goodcommit標記爲bad,或者相反,難道我要reset重來麼?

git bisect還有一個log命令,咱們只須要保存bisect日誌到一個文件,而後擦除文件中標記錯誤的日誌,而後按新的日誌從新開始bisect就行了。

git bisect log > log.txt

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

看看log.txt文件中的內容。

# 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

將標記錯誤的內容去掉。

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

而後運行git bisect replay log.txt命令。

$ git bisect replay log.txt

Previous 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會根據log從頭開始從新bisect,錯誤的標記就被擦除了。

而後就是從新作人啦。

blame

一個充分協做的項目,每一個文件可能都被多我的改動過。當出現問題的時候,你們但願快速的知道,某個文件的某一行是誰最後改動的,以便釐清責任。

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

git blame只能做用於單個文件。

$ git blame a.md

705d9622 (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哈希值,表示這一行的最近一次修改屬於該次提交。

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

第三部分是行的內容。

若是文件太長,咱們能夠截取部分行。

$ git blame -L 1,5 a.md

705d9622 (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.md

705d9622 (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) 第四行

可是結果不是你預期的那樣是吧。1,+4的確切意思是從1開始,顯示4行。

若是有人重名,能夠顯示郵箱來區分。添加參數-e或者--show-email便可。

$ git blame -e a.md

705d9622 (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是一個版本管理工具,但在衆多版本中,確定有一些版本是比較重要的,這時候咱們但願給這些特定的版本打上標籤。好比發佈一年之後,程序的各項功能都趨於穩定,能夠在聖誕節發佈v1.0版本。這個v1.0在git中就能夠經過標籤實現。

而git標籤又分爲兩種,輕量級標籤和含附註標籤。

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

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

建立

建立輕量級標籤的命令很簡單,運行git tag <tag name>

$ git tag v0.3

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

.git/refs/tags/v0.3

建立含附註標籤要加一個參數-a,它是--annotated的縮寫。

$ git tag -a v1.0

git commit同樣,若是不加-m參數,則會彈出默認或者自定義的編輯器,要求你寫標籤說明。

不寫呢?

fatal: no tag message?

建立完含附註標籤後,.git目錄會多出兩個文件。

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

含附註標籤不只會建立一個指針,還會建立一個tag對象。

咱們瞭解過git有四種對象類型,tag類型是咱們認識的最後一種。

咱們看看該對象的類型。

$ git cat-file -t 80e79e9

tag

再來看看該對象的內容。

$ git cat-file -p 80e79e9

object 359fd95229532cd352aec43aada8e6cea68d87a9
type commit
tag v1.0
tagger veedrin <veedrin@qq.com> 1545878480 +0800
版本 v1.0

它關聯的是一個commit對象,包含標籤的名稱,打標籤的人,打標籤的時間以及標籤說明。

我可不能夠給歷史commit打標籤呢?固然能夠。

$ git tag -a v1.0 36ff0f5

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

查看

查看當前git項目的標籤列表,運行git tag命令不帶任何參數便可。

$ git tag

v0.3
v1.0

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

並且我並無找到分別查看輕量級標籤和含附註標籤的方法。

查看標籤詳情能夠使用git show <tag name>

$ git show v0.3

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

tag 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 +0800
    add b.md
diff --git a/b.md b/b.md
new file mode 100644
index 0000000..e69de29

刪除

雖然git標籤不能移動對吧,但咱們能夠刪除它呀。

$ git tag -d v0.3

Deleted tag 'v0.3' (was 36ff0f5)

若是標籤已經推送到了遠端,也是能夠刪除的。

$ git push origin -d v0.3

To github.com:veedrin/git.git
 - [deleted]         v0.3

推送

默認狀況下,git push推送到遠端倉庫並不會將標籤也推送上去。若是想將標籤推送到遠端與別人共享,咱們得顯式的運行命令git push origin <tag name>

$ git push origin v1.0

Counting 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

這裏並不區分輕量級標籤和含附註標籤。

一次性將本地標籤推送到遠端倉庫也是能夠的。

$ git push origin --tags

13) remote

git是分佈式版本管理工具,它沒有中央倉庫。但多人協做時,咱們依然須要一個集散地,讓協做成員之間統一往集散地推送和拉取更新。不然,點對點的溝通,效率會很低。

因此就引出了git中遠端倉庫的概念。

概念

咱們以前全部的操做都是在本地倉庫完成的,和本地倉庫對應的是遠端倉庫。那麼本地有若干分支,遠端倉庫是否是也有對應的若干分支呢?

固然。

咱們探討一個問題,在離線狀態下,git是否是無從知道遠端倉庫的任何狀態?

我讓網絡下線,查詢從github克隆下來的本地倉庫的狀態,結果它告訴我本地倉庫的master分支是up to date with 'origin/master'

$ git status

On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

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

  • 本地分支,咱們能夠經過<branch>寫法訪問它。
  • 遠端分支,咱們能夠經過<remote branch>寫法訪問它。
  • 遠端分支引用,咱們能夠經過<remote/branch>寫法訪問它。實際上它也是本地分支,只不過咱們沒法操做它,只有git的網絡操做才能夠更新它。離線狀態下,git給的狀態就是本地分支和遠端分支引用的比較結果。
git官方把我所說的 遠端分支引用稱爲 遠端分支。知道誰是誰就好了,名字不重要🤔

我是馬蹄疾

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

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

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

拉取

若是遠端倉庫有新的提交或者新的分支,咱們須要運行git fetch命令來拉取更新。

$ git fetch

remote: 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/git
   3893459..0f80eeb  master     -> origin/master

這個命令是git fetch origin的縮寫。由於origin是遠端倉庫的默認名稱,因此能夠省略。若是有手動添加的遠端倉庫,那就必須指定遠端倉庫的名稱了。

這個命令作了什麼呢?

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

git fetch僅僅是將遠端的更新拉取下來,同步本地的遠端分支引用,不會對本地分支有任何影響。咱們須要手動執行合併操做才能更新本地分支。

$ git merge origin/master

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

固然,有一個更簡單的操做。

$ git pull

remote: 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/git
   4fbd1d4..d9785d7  master     -> origin/master
Updating 4fbd1d4..d9785d7
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

git pull就是git fetchgit merge的一鍵操做。

推送

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

$ git push origin master

Counting 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.git
   3eaa1ae..2bd3c9d  master -> master

若是當前分支對遠端分支設置了追蹤的話,也能夠省略分支名。

$ git push

Counting 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.git
   3eaa1ae..2bd3c9d  master -> master

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

$ git push

To 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 -f

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

實際開發時咱們會建不少特性分支,推送到遠端,經過測試後再合入主分支。使用git push <remote-name> <remote-branch-name>每次都要指定遠端分支名,若是會有屢次推送,咱們能夠在推送時設置本地分支追蹤遠端分支,這樣下次就能夠直接推送了。

也能夠簡寫成git push -u <remote-name> <remote-branch-name>

$ git push --set-upstream origin dev

Counting 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 = origin
    merge = refs/heads/dev

查看

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

$ git remote

origin

-v參數能夠查看更爲詳細的信息,-v--verbose的縮寫。

$ git remote -v

origin  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 origin
  Fetch URL: git@github.com:veedrin/git-1.git
  Push  URL: git@github.com:veedrin/git-1.git
  HEAD branch: master
  Remote branches:
    dev    tracked
    master tracked
  Local branches configured for 'git pull':
    dev    merges with remote dev
    master merges with remote master
  Local 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 remote

horseshoe
origin

除了添加遠端倉庫,咱們還能夠添加本地分支對遠端分支的追蹤。

$ git checkout -b dev origin/dev

Branch 'dev' set up to track remote branch 'dev' from 'origin'.
Switched to a new branch 'dev'

建立dev分支的同時,也設置了對遠端分支dev的追蹤,這樣下次推送的時候就不須要指定了。

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

$ git checkout -b dev origin/dev

fatal: 'origin/dev' is not a commit and a branch 'dev' cannot be created from it

git也提供了快捷方式。

$ git checkout --track origin/dev

Branch 'dev' set up to track remote branch 'dev' from 'origin'.
Switched to a new branch 'dev'

重命名

有時候你想修改遠端倉庫的簡寫名。好比你將女友的名字命名爲遠端倉庫的簡寫名,而後大家分手了。這真是一個使人悲傷(欣喜)的故事。

$ git remote rename nvpengyou gaoyuanyuan

查看遠端倉庫列表。

$ git remote

gaoyuanyuan
origin

刪除

通常來講,一個git項目有一個遠端倉庫就好了,其他的大可能是臨時性的。因此總有一天要刪除它。

$ git remote rm horseshoe

查看遠端倉庫列表。

$ git remote

origin
本文是『horseshoe·Git專題』系列文章之一,後續會有更多專題推出
GitHub地址(持續更新): https://github.com/veedrin/horseshoe
博客地址(文章排版真的很漂亮): https://veedrin.com
若是以爲對你有幫助,歡迎來GitHub點Star或者來個人博客親口告訴我

Git專題一覽

🎖 add

🎖 commit

🎖 branch

🎖 checkout

🎖 merge

🎖 rebase

🎖 reset

🎖 revert

🎖 stash

🎖 view

🎖 position

🎖 tag

🎖 remote

相關文章
相關標籤/搜索