.git 目錄結構
|── HEAD|── branches // 分支|── config // 配置|── description // 項目的描述|── hooks // 鉤子| |── pre-commit.sample| |── pre-push.sample| └── ...|── info| └── exclude // 類似.gitignore 用于排除文件|── objects // 存儲了blob,tree,commit對象| |── info| └── pack // 用于優化倉庫體積,通過patch的方式└── refs |── heads└── tags // 標簽
復制代碼
blob,tree,commit對象
blob
blob對象是文件內容的快照
$ git cat-file -t e0f5c6
blob$ git cat-file -p e0f5c6
reademe
復制代碼
tree
tree對象描述了工作目錄,每個節點指向對應的blob或者子tree
$ git cat-file -t 443322
tree$ git cat-file -p 443322
100644 blob 723ef36f4e4f32c4560383aa5987c575a30c6535 .gitignore
100644 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de 1
100644 blob d218c7660f5672293d2b2241741f2e3f25008b9e 2
040000 tree 74080098daf8a1fa7368c2feac12cfab0e648d02 3
100644 blob e0f5c6d282792ef63ea012f200f5d7749b084fa0 README.md
復制代碼
commit
commit對象是對tree的封裝
$ git cat-file -t 186f17807d
commit$ git cat-file -p 186f17807d
tree 4433224cb7cbb72dae00b5138c8961522c531707
parent 45d0db32885c40a8c3244fa6ec24df2d7a631a3c
author 孫健 <jian.sun@ymm56.com> 1535621955 +0800
committer 孫健 <jian.sun@ymm56.com> 1535621955 +0800
復制代碼
擴展 - 手動創建 commit
mktree // 從標準格式文本中創建一個樹read-tree // 從倉庫中讀取到 index 文件ls-files -s // 檢查當前 index 文件的結構write-tree // 通過這個 index 在倉庫中創建一個樹commit-tree // 將一個 tree 包裝為 commit 對象-p 指定父commit-m 添加描述branch -f master HEAD // 更改分支指向
復制代碼
工作區和暫存區
工作區
工作區就是我們的工作目錄
暫存區
暫存區類似一個 tree 對象
$ git ls-files -s
100644 723ef36f4e4f32c4560383aa5987c575a30c6535 0 .gitignore
100644 56a6051ca2b02b04ef92d5150c9ef600403cb1de 0 1
100644 d218c7660f5672293d2b2241741f2e3f25008b9e 0 2
100644 00750edc07d6415dcc07ae0351e9397b0222b7ba 0 3/3
100644 e0f5c6d282792ef63ea012f200f5d7749b084fa0 0 README.md
復制代碼
當我們clone一個倉庫,或者檢出一個提交``的時候,此時HEAD == 暫存區 == 工作區
-
工作區修改,未添加到暫存區 - HEAD == 暫存區 != 工作區
-
工作區修改,添加到暫存區 - HEAD != 暫存區 == 工作區
-
工作區修改,添加到暫存區,提交到倉庫 - HEAD == 暫存區 == 工作區 -
nothing to commit, working tree clean
add的時候做了什么
-
從文件中創建 blob
-
將 blob 寫入倉庫
-
更新 index
Commit的時候 做了什么
- 從 index 文件創建 tree
- 將 tree 寫入倉庫
- 創建一個 commit 對象將樹封裝起來
- 將 HEAD 作為新創建 commit 的父 commit,并更新 HEAD 未新創建的 commit
擴展:從一個 tree 更新工作區
$ git read-tree $TREE_HASH // 從一個 tree 寫入到 index
$ git checkout-index -a // 從 index 檢出到工作區
復制代碼
分支,標簽,HEAD
branch
# refs/heads/dev
47c871bb634324cfcc41e5a5affee6aa35301e03 // branch總是指向最新的提交$ git cat-file -t 47c871b
commit
復制代碼
標簽
# refs/tags/dev
47c871bb634324cfcc41e5a5affee6aa35301e03 // tag指向固定的提交// 同上
復制代碼
HEAD
# HEAD
ref: refs/heads/dev 此時HEAD隨分支前進$ git checkout HEAD^
# HEAD
47c871bb634324cfcc41e5a5affee6aa35301e03 分離HEAD,不隨分支前進// 同上
復制代碼
merge
merge 常用于將兩條分支合并;
略過快速合并
標準的三方合并
上圖:
此時,你的開發歷史從一個更早的地方開始分叉開來(diverged)。 因為,master
分支所在提交并不是 iss53
分支所在提交的直接祖先,Git 不得不做一些額外的工作。 出現這種情況的時候,Git 會使用兩個分支的末端所指的快照(C4
和 C5
)以及這兩個分支的工作祖先(C2
),做一個簡單的三方合并。
我們分析一下兩條分支合并的過程
- 找到兩條分支對應的commit對象;
- 找到兩個commit對象共同的祖先commit對象;
- 通過兩個commit對象下的tree對象對比每個blob對象的差異;
- 如果不同并且其中一個blob對象與祖先相同,則默認自動合并;
- 如果不同并且都不與祖先相同
- 修改同一處地方 產生conflict,需要手動合并
- 沒有修改同一處地方 自動合并
查看合并基底
$ git merge-base master iss53
// C2的HASH_ID
復制代碼
合并
* master
$ git merge iss53
// 此時產生沖突$ git merge --abort // 撤銷合并
$ git commit -a // 解決沖突后,提交
復制代碼
查看沖突
$ git show :1:hello.rb > hello.common.rb // 祖先
$ git show :2:hello.rb > hello.ours.rb // 我
$ git show :3:hello.rb > hello.theirs.rb // 他$ git ls-files -u
復制代碼
小技巧
1.有時候我們格式化文件之后,在之后的合并中會產生很多沖突,有沒有辦法忽略空格上的更改嗎?
git merge [branch] --ignore-space-change
git merge [branch] -s recursive -X ignore-space-change-s 選擇策略-x 策略選項
復制代碼
2.通過revert撤銷合并,后面再次merge的時候提示已經合并過了?
再次revert,或者通過新建一個相同的commit,指定其父commit;
$ git commit-tree $TREE_HASH -p $PARENT_HASH // 需要合并的 commit 的tree hash 和其parent hash
$ git branch -f $BRANCH $NEW_COMMIT_HASH // 將分支指向新的 commit
$ git merge $branch // 此時合并就沒問題了
復制代碼
rebase
繼續上圖:
首先回到兩個分支最近的共同祖先,根據當前分支(也就是要進行衍合的分支 experiment)后續的歷次提交對象(這里只有一個 C4),生成一系列文件補丁;
然后以基底分支(也就是主干分支master)最后一個提交對象(C3)為新的出發點,逐個應用之前準備好的補丁文件;
最后會生成一個新的合并提交對象(C4'),從而改寫 experiment 的提交歷史,使它成為 master 分支的直接下游
需要注意的點
-
rebase是逐步應用補丁,可能會有多個rebase階段,每次解決沖突都需要:
$ git add . $ git rebase --continue 復制代碼
-
rebase完成之后會丟失之前的對C4的指向,導致C4無法再被找到,此時C4存在于
.git/objects
中,等待下次gc被回收; -
rebase類似于多個merge過程,比如已被應用的補丁產生的新的提交會與下一個補丁進行新的三方合并;
黃金準則 - 公用分支不可作為衍合分支
上圖:
交互式rebase
$ git rebase -i HEAD~6pick fb257ad9 某次提交說明
pick fb257ad9 某次提交說明
drop fb257ad9 某次提交說明# Rebase a0daba3d..fb257ad9 onto a0daba3d (1 command)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending // 修改某次commit
# s, squash <commit> = use commit, but meld into previous commit // 將commit合并到上一個commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit // 刪除某次commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
復制代碼
- 如果之前錯誤的合并了某次提交,可以通過drop刪除該提交
- 如果想修改某次提交的信息,可以將該提交對應的狀態改為edit
cherry-pick - 摘櫻桃
cherry-pick常用于將某些提交應用于其他的分支;
cherry-pick
可以理解為”挑揀”提交,它會獲取某一個分支的單筆提交,并作為一個新的提交引入到你當前分支上。 當我們需要在本地合入其他分支的提交時,如果我們不想對整個分支進行合并,而是只想將某一次提交合入到本地當前分支上,那么就要使用cherry-pick
了。
# cherry-pick的方式與 merge 有所不同,merge 的過程相當于兩個 tree 的差異對比,而cherry-pick更像是應用更改;C<---D<---E branch2/
master A<---B \F<---G<---H branch3|HEAD*** after ***C<---D<---E<---F'<---G'<---H' branch2/
master A<---B \F<---G<---H branch3|HEAD 復制代碼
當我們應用某個提交的時候,實際上會通過該提交與其父提交的差異得到發生的改變,并將這些改變應用到主分支上,cherry-pick產生的三方合并,其merge-base
是該提交的父提交;
* branch2
$ git cherry-pick B...H // 三點語法,前開后閉第一次 base:B our:E their: F 產生提交 F'
第二次 base:F our:F' their: G 產生提交 G'
第三次 base:G our:G' their: H 產生提交 H'
復制代碼