全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/12966398.html, 多謝,=。=~
(如果對你有幫助的話請幫我點個贊啦)
日常項目管理中我們最常使用的git命令有
add、commit、push、pull,但其他不常使用的命令往往容易誤操作,所以想深入的學習一下git操作命令底層原理到底是怎么樣的,在阮一峰大大的日志里面看到了《Git from the inside out》,全文通過樹狀圖的方式表示各分支節點之間的關系,以示例的方式闡述每種操作命令后底層文件及索引的變化。然而是全英文的,於是乎我只能每天抽點時間來翻譯加學習,前前后后經歷了一周,終於完成了,大家一起學起來吧。
git init
初始化git倉庫(該操作會在當前目錄下創建一個.git目錄,里面可以放git配置或者項目歷史記錄:.git/objects)。
例如:
~ $ mkdir alpha
~ $ cd alpha
~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt
~/alpha $ printf '1234' > data/number.txt
~/alpha $ git init
目錄結構如下:
alpha
├── data
| └── letter.txt
| └── number.txt
└── .git
├── objects
etc...
.git目錄及其內容是git相關的文件,除此之外所有其他文件統稱為工作副本,為用戶文件。
git add
在git倉庫中添加一些文件。
例如:
~/alpha $ git add data/letter.txt
第一步:在.git/objects目錄中創建一個新的blob文件(創建的blob文件包含data/letter.txt的壓縮內容,文件名是由它的內容哈希得到)
-
git將
data/letter.txt中的內容ahash計算得到2e65efe2a145dda7ee51d1741299f848e5bf752e,前兩個字符被用作對象數據庫中的目錄名:.git/objects/2e/; -
hash散列值的剩余部分用作blob文件(被添加的文件中需要保存的內容)的名稱:
.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e;
第二步:將文件添加到索引中
-
索引是一個列表,其中包含Git要跟蹤的每個文件,它以
.git/index文件的形式存儲,其中每行指向跟蹤的blob文件,包含文件內容的hash散列值。例如:data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e; -
注意:
git add data命令執行,索引中只列出data目錄中的文件,並不會列出data目錄; -
以同樣的方法我們將
data/number.txt文件添加到git倉庫后,當用戶修改data/number.txt文件中內容,並重新執行git add命令時,git會根據更新后的內容創建一個新的blob,同時更新data/number.txt的索引條目以指向新的blob。
例如:
~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data
git commit
通過git commit命令創建a1提交。
~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1
第一步:創建一個樹狀圖來表示提交的項目版本的內容(git通過索引創建樹狀圖來記錄項目的當前狀態,這個樹狀圖記錄了項目中每個文件的位置和內容)
樹狀圖由兩類對象組成:blobs和trees
-
blobs:通過git add存儲,表示文件內容; -
trees:通過git commit存儲,表示工作副本中的目錄;
例如:分別對應文件權限、條目類型、blob文件的hash散列值、文件名稱;
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt
040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data;
a1提交的樹狀圖:
root→data→a(data/letter.txt) and 1(data/number.txt)
第二步:創建一個提交對象(git commit在創建樹狀圖之后就會創建一個提交對象,提交對象是.git/objects中的另一個文本文件)
例如:
tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
a1
- first line:指向樹狀圖,其中hash散列值由工作副本的根目錄生成(也就是
alpha); - last line:提交信息;
a1的提交對象,指向它的樹狀圖:
a1→root→data→a(data/letter.txt) and 1(data/number.txt)
第三步:將當前分支指向新的提交對象(git在.git/HEAD的頭文件中查找當前分支)
-
例如:
ref: refs/heads/master,表明HEAD指向master,所以master為當前分支; -
注意:首次提交時
master的ref是不存在的,git會創建.git/refs/heads/master,並將其內容設置為提交對象的hash散列:74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd;
當前分支指向提交對象
a1:
HEAD→master→a1→root→data→a(data/letter.txt) and 1(data/number.txt)
git commit(非初次提交)
下面為a1提交后的結構圖,工作副本及索引已存在,此時三方的data/letter.txt 和data/number.txt內容一致:

修改data/number.txt的內容,工作副本更新:
~/alpha $ printf '2' > data/number.txt

執行git add命令,在.git/objects目錄中創建一個新的blob文件,並將文件添加到索引中:
~/alpha $ git add data/number.txt

執行git commit命令:
~/alpha $ git commit -m 'a2'
創建一個新的樹狀圖來表示索引的內容:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt
040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data
然后創建一個新的提交對象:
tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
a2
- first line:指向新的
roottree對象; - second line:指向
a1(為了找到父提交,git轉到HEAD,跟着它轉到master,最終找到a1提交的hash散列); - last line:提交信息;
將當前分支指向新的提交對象:

從結構圖可以看出以下特性:
- 文件內容是以對象樹存儲的。這意味着只有差異會存儲在對象數據庫中,上圖中
a2提交時復用了a1提交之前生成的blob(根據內容a生成)。同理,如果整個目錄從一個commit到另一個commit時並沒有發生改變,那么它的對象樹以及下面的所有blobs對象和trees對象都是可以復用的。通常,從一個commit到另一個commit的內容變更較少,這就是git可以在很小的空間中存儲大量提交歷史的原因。 - 每個
commit都會有一個parent。這意味着一個倉庫可以存儲一個項目的所有歷史記錄。 refs是入口,指向了commit歷史的一部分。每項commit都有自己獨特的標識,用戶通過類似樹狀結構的“族譜”將他們的工作組織起來,例如:refs具體為fix-for-bug-376。git則使用特殊的符號例如HEAD、MERGE_HEAD、FETCH_HEAD來支持通過用命令行操作提交歷史。.git/objects目錄下的節點是不變的。也就是說,內容只能被編輯,不能被刪除。添加的每個文件內容和創建的每個提交都能在.git/objects目錄中找到。refs是可變的。因此,ref的含義可以改變,master所指向的commit可能是目前項目的最佳版本,但是很快,它就會被更新更好的commit所取代。- 通過
ref指向的工作副本和commit很容易回索,但其他的commit就不是。意思是最近的歷史記錄更容易找到,但也經常改變。換句話說就是git比較健忘,如果想要查找比較久遠的提交記錄就需要深度索引。
git checkout(檢出commit)
通過git checkout命令+a2提交的hash散列值檢出a2commit。
例如:
~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...
第一步:git獲取到a2提交及它所指向的樹狀圖。
第二步:git將樹狀圖中的文件條目寫入工作副本。
這時內容並不會發生改變。因為此時HEAD就是通過master指向a2提交,所以a2對應的樹狀圖內容也已經被寫入工作副本中。
第三步:git將樹圖中的文件條目寫入索引。
這也不會導致任何變化。因為索引已經包含了a2提交的內容。
第四步:HEAD的內容被設置為a2提交的hash散列值。
例如:f0af7e62679e144bb28c627ee3e8f7bdb235eee9
通過設置HEAD的內容為hash散列值,會使Head直接指向a2而不是原本的master(倉庫將被至於分離的HEAD):

此時提交的commit很容易丟失。比如修改number.txt文件內容為3並提交修改,git會通過HEAD去獲取a3提交的parent,而不是像之前一樣利用ref實現跟蹤和查找。最終由HEAD直接指向a3的提交對象(倉庫仍處於分離的HEAD中,無論是a3還是之后的commit都沒有在任何分支上)。
例如:
~/alpha $ printf '3' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a3'
[detached HEAD 3645a0e] a3
樹狀圖結構如下:

git branch(創建分支)
通過git branch命令創建一個名為deputy的分支,實際就是在.git/refs/heads/deputy目錄下創建了一個新文件,其中包含了HEAD所指向的hash散列值(a3提交的hash散列)。
~/alpha $ git branch deputy
樹狀圖結構如下(分支deputy的創建使得a3提交被添加到該分支,安全性就有了,不至於會丟失。但HEAD還是在分離狀態,因為它仍然指向了commit):

git checkout(檢出分支)
通過git checkout命令檢出master分支。
~/alpha $ git checkout master
Switched to branch 'master'
第一步:git獲取到master指向的a2提交及a2所指向的樹狀圖。
第二步:git將樹狀圖中的文件條目寫入工作副本。
這時會將data/number.txt文件內容寫為2。
第三步:git將樹圖中的文件條目寫入索引。
這時會將data/number.txt文件的條目更新為2blob文件的hash散列。
第四步:git將HEAD的內容由hash散列值修改為ref: refs/heads/master,使得HEAD重新指向master。
樹狀圖結構如下:

git checkout(檢出與工作副本不兼容的分支)
本地修改文件data/number.txt內容后,通過git checkout命令檢出deputy分支。
~/alpha $ printf '789' > data/number.txt
~/alpha $ git checkout deputy
Your changes to these files would be overwritten
by checkout:
data/number.txt
Commit your changes or stash them before you
switch branches.
很顯然,checkout被git無情拒絕,原因是此時三方的文件內容不一致,必須先解決差異(git如果做覆蓋操作會使信息丟失,如果做合並又太復雜):
HEAD指向master,master指向a2,而a2中data/number.txt文件的內容為2;deputy指向a3,而a3中data/number.txt文件的內容為3;- 本地工作空間中
data/number.txt文件的內容為789;
所以將誤修改復原就可以解決了(假設不是誤修改,那你需要將修改先提交到原分支):
~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
Switched to branch 'deputy'
樹狀圖結構如下:

git merge(合並父分支到子分支)
通過git merge命令將master分支合並到deputy,合並兩個分支其實就是合並兩個commit,對於這樣的合並git什么也不做。
~/alpha $ git merge master
Already up-to-date.
結構圖中一系列的提交其實就是對倉庫文件內容做的一系列修改,所以,如果是將父提交合並至子提交,git什么也不做,因為這些改變其實已經被合並了。
git merge(合並子分支到父分支)
先將分支切換回master:
~/alpha $ git checkout master
Switched to branch 'master'

通過git merge命令將deputy分支合並到master:
~/alpha $ git merge deputy
Fast-forward
git獲取到子提交及它所指向的樹狀圖,git將樹狀圖中的文件條目寫入工作副本和索引,git的fast-forwards操作將master指向了a3(如前面所說的,結構圖中一系列的提交其實就是對倉庫文件內容做的一系列修改,合並時,如果是將子提交合並至父提交,提交歷史是不會改變的,只是合並雙方之間差了一些修改,所以最終改變的是被合並分支的指向)。

git merge(合並非直接關聯分支)
本地修改文件data/number.txt內容為4,並提交為a4至master分支:
~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4
切換至deputy分支,本地修改文件data/letter.txt內容為b,並提交為b3至deputy分支:
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
[deputy 982dffb] b3

從結構圖可以看出以下特性:
commit可以共享parent。所以在提交歷史中可以創建新的“族譜”。commit可以包含多個parent。所以不同的“族譜”可以通過一個包含兩個parent的commit來連接(commit有兩個parent的情況是通過merge實現)。
例如:將master分支合並至deputy分支(由於git發現這兩個分支對應的commit屬於不同的“族譜”,所以需要合並commit,總共分為8步)
~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy.
第一步:git將giver commit(也就是a4)的hash散列值寫入alpha/.git/MERGE_HEAD文件
這個文件的存在就是告訴git正在做合並操作。
第二步:git查找base commit
是receiver commit(也就是b3)和giver commit(也就是a4)在歷史記錄中最近的共同祖先(通俗的說:兩個“族譜”分道揚鑣的節點,也就是a3)。
第三步:git基於樹狀圖為base commit、receiver commit、giver commit生成索引
第四步:git生成一個diff
diff可以理解為差異文件,其中組合了receiver commit和giver commit對base commit的更改,diff是一個指向被修改文件的路徑列表(文件修改包括:add、remove、modify、conflict)。
git通過獲取出現在
base commit、receiver commit、giver commit索引中的所有文件列表,對於每一個都進行比較,確定文件被修改后就向diff寫入一個對應的條目,在本例中,diff有兩個條目。
- 一個是
data/letter.txt,base commit中是a,receiver commit中是b,giver commit中是a。git可以看到內容是由
receiver修改的,而不是giver,所以diff中data/letter.txt對應的條目是一個modify,而不是conflict。- 另一個是
data/number.txt,base commit中是3,receiver commit中是3,giver commit中是4,所以diff中data/number.txt對應的條目也是一個modify。
第五步:git將diff中的修改應用於工作副本
data/letter.txt中的內容被設置成b,data/number.txt中的內容被設置成4。
第六步:git將diff中的修改寫入索引
data/letter.txt對應的條目指向b的blob文件,data/number.txt對應的條目指向4的blob文件。
第七步:git提交更新后的索引
可以看到此時的提交就有兩個parent。
tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
b4
第八步:git將當前分支deputy指向最新的提交b4(將a4合並到b3的遞歸合並結果)

git merge(合並非直接關聯分支,且修改了相同文件)
先切換至master分支,將deputy分支合並至master分支(也就是前面的將子分支合並到父分支,其實只是修改了master分支的commit指向):
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge deputy
Fast-forward

此時切換至deputy分支,本地修改文件data/number.txt內容為5,並提交為b5至deputy分支:
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
[deputy bd797c2] b5
然后切換至master分支,本地修改文件data/number.txt內容為6,並提交為b6至master分支:
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
[master 4c3ce18] b6

最終,將deputy分支合並到master分支,很顯然被git拒絕,原因是data/number.txt文件中的內容沖突了,自動合並失敗:
~/alpha $ git merge deputy
CONFLICT in data/number.txt
Automatic merge failed; fix conflicts and
commit the result.
整個過程與上面合並時的前6步是一致的:alpha/.git/MERGE_HEAD文件設置;查找base commit;為base commit、receiver commit、giver commit生成索引;生成diff文件;將diff中的修改應用於工作副本;git將diff中的修改寫入索引;但是由於沖突第7步的提交和第8步的ref更新不能正常執行。
下面詳細說明一下前面6步到底發生了什么導致最終的結果:
第一步:git將giver commit(也就是b5)的hash散列值寫入alpha/.git/MERGE_HEAD文件
同樣的,這個文件的存在就是告訴git正在做合並操作。

第二步:git查找base commit
是receiver commit(也就是b6)和giver commit(也就是b5)在歷史記錄中最近的共同祖先(通俗的說:兩個“族譜”分道揚鑣的節點,也就是b4)。
第三步:git基於樹狀圖為base commit、receiver commit、giver commit生成索引
第四步:git生成一個diff
diff可以理解為差異文件,其中組合了receiver commit和giver commit對base commit的更改,diff是一個指向被修改文件的路徑列表(文件修改包括:add、remove、modify、conflict)。
git通過獲取出現在
base commit、receiver commit、giver commit索引中的所有文件列表,對於每一個都進行比較,確定文件被修改后就向diff寫入一個對應的條目,在本例中,diff只有一個條目。
- 也就是
data/number.txt,base commit中是4,receiver commit中是6,giver commit中是5。條目被標記為conflict,因為data/number.txt的內容在receiver、giver和base中是不同的。
第五步:git將diff中的修改應用於工作副本
對於沖突的部分,git會將兩個版本都寫入到工作副本的文件中。data/number.txt中的內容被設置成:
<<<<<<< HEAD
6
=======
5
>>>>>>> deputy
第六步:git將diff中的修改寫入索引
索引中的條目由其文件路徑和stage共同組成唯一標識,對於沒有沖突的文件,stage為0。合並前的索引如下(前面的0就是stage):
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
合並的diff被寫入索引后:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61
stage為0的data/letter.txt條目與合並之前的條目相同,但是stage為0的data/number.txt條目已經沒有了,在該位置上新增了3個新的條目。stage1條目包含了base(data/number.txt)內容的hash散列,stage2條目包含了receiver(data/number.txt)內容的hash散列,stage3條目包含了giver(data/number.txt)內容的hash散列。這三個條目的存在就是在告訴gitdata/number.txt是沖突的,所以合並就被中斷了。
此時,如果用戶通過將data/number.txt的內容設置為11來整合兩個沖突版本的內容,並通過git add將文件添加至索引中:
~/alpha $ printf '11' > data/number.txt
~/alpha $ git add data/number.txt
整體過程就是:git add命令創建了一個包含11的blob文件,該操作就是就是告訴git沖突解決了,此時git就會從索引中移除stage為1、2、3的條目,並使用新blob文件的散列為data/number.txt添加一個stage為0的條目:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
第七步:用戶通過git commit命令提交最新修改
~/alpha $ git commit -m 'b11'
[master 251a513] b11
git在倉庫中看到.git/MERGE_HEAD,就知道合並正在進行中,它會檢查索引並查看是否存在沖突,如果沒有就會創建一個新的提交b11來記錄已解決的合並內容,然后刪除.git/MERGE_HEAD中的文件,此時合並就完成了。
第八步:git將當前分支master指向新的提交。

git rm(刪除文件)
下面是當前狀態下最新的結構圖:

通過git rm命令刪除data/letter.txt文件,文件首先會從本地的工作副本移除,接着文件條目會從索引中移除:
~/alpha $ git rm data/letter.txt
rm 'data/letter.txt'
此時結構圖就變成了:

通過git commit命令提交變更:
~/alpha $ git commit -m '11'
[master d14c7d2] 11
和之前一樣,作為提交的一部分,git會構建一個表示索引內容的樹狀圖,data/letter.txt不包括在樹圖中,因為它不在索引中。

復制倉庫
~/alpha $ cd ..
~ $ cp -R alpha bravo
用戶將alpha/倉庫的內容復制到bravo/目錄,目錄結構就變成了:
~
├── alpha
| └── data
| └── number.txt
└── bravo
└── data
└── number.txt
而此時bravo/目錄中也會有一個與之對應的git結構圖:

建立兩個倉庫的鏈接
用戶首先返回到alpha倉庫:
~ $ cd alpha
~/alpha $ git remote add bravo ../bravo
如果要將bravo設置為alpha的遠程倉庫,需要在alpha/.git/config文件中添加一些代碼:
[remote "bravo"]
url = ../bravo/
指定在../bravo目錄中有一個名為bravo的遠程倉庫。
從遠程倉庫上fetch分支
用戶首先進入bravo倉庫,將data/number.txt的內容設置為12,並將修改提交給bravo上的master:
~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
[master 94cd04d] 12
此時結構圖如下:

然后用戶進入alpha倉庫,想要把分支master從bravo取過來:
~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD
這個過程git有四個步驟:
第一步:獲取master在bravo上所指向的commit的散列
也就是12 commit提交的散列。
第二步:將12 commit依賴的所有對象(去除alpha倉庫中已存在的)復制到alpha/.git/objects/中
包括提交對象本身、樹圖中指向的對象、12 commit的父提交以及它在樹圖中指向的對象。
第三步:alpha/.git/refs/remotes/bravo/master中的ref被設置成12 commit提交的散列值
第四步:alpha/.git/FETCH_HEAD的內容被設置成:
94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
這表明剛剛的fetch命令從bravo獲取了master的12 commit,此時結構圖就變成了:

從結構圖可以看出以下特性:
- 對象是可以被拷貝的。也就是說歷史記錄可以在倉庫之間共享。
- 一個倉庫可以存儲遠程倉庫分支的
ref,例如alpha/.git/refs/remotes/bravo/master。這意味着一個倉庫可以在本地記錄遠程倉庫上分支的狀態。它在獲取時是正確的,但是如果遠程分支發生更改,它就會過期。
合並FETCH_HEAD
用戶通過git merge命令合並FETCH_HEAD:
~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward
FETCH_HEAD只是一個ref,它解析為12 commit(giver),HEAD指向11 commit(receiver)。git執行合並后將指向master→12 commit:

從遠程倉庫上pull分支
用戶將master分支從bravo pull到alpha,pull是“fetch FETCH_HEAD和 merge FETCH_HEAD”的縮寫,所以最終git執行兩個命令並反饋master已經是最新的了。
~/alpha $ git pull bravo master
Already up-to-date.
clone倉庫
用戶移動到上層目錄並clone alpha到charlie:
~/alpha $ cd ..
~ $ git clone alpha charlie
Cloning into 'charlie'
clone到charlie的結果與之前用戶為了生成bravo倉庫所使用的cp類似,git創建一個名為charlie的新目錄,並將它初始化為git倉庫,將alpha作為一個名為origin的遠程倉庫,fetch origin並合並FETCH_HEAD。
將分支push到從遠程倉庫中checkout的分支上
用戶回到alpha倉庫,修改data/number.txt的值為13並將修改提交到master分支。
~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
[master 3238468] 13
將charlie設置為alpha的遠程倉庫:
~/alpha $ git remote add charlie ../charlie
將master分支push到charlie:
~/alpha $ git push charlie master
Writing objects: 100%
remote error: refusing to update checked out
branch: refs/heads/master because it will make
the index and work tree inconsistent
13 commit關聯的所有對象將被復制到charlie。但從上面的命令行反饋可以看到,push過程被中斷,git拒絕push到遠程檢出(checkout)的分支。其實這是可以理解的,因為這樣的push將更新遠程索引和HEAD,如果有人正在編輯遠程上的工作副本,就會導致混亂。
此時,用戶可以創建一個新分支,將13 commit合並到其中,並將該分支push到charlie。但實際上,我們是想要一個可以隨時可以push的倉庫,一個中央存儲庫,用於push和pull,但是沒有人直接進行commit提交,類似GitHub的遠程倉庫,想要一個裸(bare)存儲庫。
clone一個裸(bare)倉庫
用戶進入到上層目錄,clone delta作為裸倉庫:
~/alpha $ cd ..
~ $ git clone alpha delta --bare
Cloning into bare repository 'delta'
跟普通的clone有兩個不同之處,首先config文件表明存儲庫是裸倉庫,而原本存儲在.git目錄下的文件則存儲在倉庫的根目錄下:
delta
├── HEAD
├── config
├── objects
└── refs
此時的結構圖如下:

將分支push到裸(bare)倉庫
用戶回到alpha倉庫並設置delta作為它的遠程倉庫:
~ $ cd alpha
~/alpha $ git remote add delta ../delta
修改data/number.txt的值為14並將修改提交到master分支:
~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
[master cb51da8] 14
提交后結構圖如下:

接下來將master push到delta:
~/alpha $ git push delta master
Writing objects: 100%
To ../delta
3238468..cb51da8 master -> master
整個過程有3步:
第一步:從alpha/.git/objects/拷貝14 commit提交相關的所有對象至delta/objects/
第二步:delta/refs/heads/master的指向更新為14 commit
第三步:alpha/.git/refs/remotes/delta/master的指向更新為14 commit,alpha擁有了delta狀態的最新記錄
現在的結構圖如下:

參考文獻
Git from the inside out:https://codewords.recurse.com/issues/two/git-from-the-inside-out
