Git 分支與合併

1.  Git 對象

Git 的核心部分是一個簡單的鍵值對數據庫。可以向 Git 倉庫中插入任意類型的內容,它會返回一個唯一的鍵,通過該鍵可以在任意時刻再次取回該內容。

所有內容均以樹對象和數據對象的形式存儲,其中樹對象對應了 UNIX 中的目錄項,數據對象則大致上對應了 inodes 或文件內容。一個樹對象包含了一條或多條樹對象記錄(tree entry),每條記錄含有一個指向數據對象或者子樹對象的 SHA-1 指針,以及相應的模式、類型、文件名信息。

Git 保存的不是文件的變化或者差異,而是一系列不同時刻的快照

在進行提交操作時,Git 會保存一個提交對象(commit object),它包含指向樹對象的指針。

每次產生的提交對象會包含一個指向上次提交對象(父對象)的指針,首次提交產生的提交對象沒有父對象,普通提交操作產生的提交對象有一個父對象, 而由多個分支合併產生的提交對象有多個父對象。

2.  git branch

Git 的分支,其實本質上僅僅是指向提交對象的可變指針。 Git 的默認分支名字是 master。 在多次提交操作之後,你其實已經有一個指向最後那個提交對象的 master 分支。master 分支會在每次提交時自動向前移動。

注意:Git 的 master 分支並不是一個特殊分支。 它就跟其它分支完全沒有區別。 之所以幾乎每一個倉庫都有 master 分支,是因爲 git init 命令默認創建它,並且大多數人都懶得去改動它。

git branch 命令可以列出你所有的分支、創建新分支、刪除分支及重命名分支。比如,創建一個 testing 分支:

git branch testing

這會在當前所在的提交對象上創建一個指針。

在 Git 中,HEAD 是一個特殊指針,指向當前所在的本地分支(PS:將 HEAD 想象爲當前分支的別名)

由於 Git 的分支實質上僅是包含所指對象校驗和(長度爲 40 的 SHA-1 值字符串)的文件,所以它的創建和銷燬都異常高效。

3.  git checkout

git checkout 命令用來切換分支,或者檢出內容到工作目錄。

git checkout testing

這樣 HEAD 就指向 testing 分支了。

注意:分支切換會改變你工作目錄中的文件。如果是切換到一個較舊的分支,你的工作目錄會恢復到該分支最後一次提交時的樣子。 如果 Git 不能幹淨利落地完成這個任務,它將禁止切換分支。

使用 git checkout -- <file>  可以撤消對文件的修改

使用 git reset HEAD <file>...  可以取消文件的暫存

4.  git stash

git stash 命令用來臨時地保存一些還沒有提交的工作,以便在分支上不需要提交未完成工作就可以清理工作目錄。

當你在項目的一部分上已經工作一段時間後,所有東西都進入了混亂的狀態, 而這時你想要切換到另一個分支做一點別的事情。 問題是,你不想僅僅因爲過會兒回到這一點而爲做了一半的工作創建一次提交。 針對這個問題的答案是 git stash 命令。

貯藏(stash)會處理工作目錄的髒的狀態——即跟蹤文件的修改與暫存的改動——然後將未完成的修改保存到一個棧上, 而你可以在任何時候重新應用這些改動(甚至在不同的分支上)。

# 新的貯藏推送到棧上
git stash

# 查看貯藏的東西
git stash list

# 將你剛剛貯藏的工作重新應用
git stash apply

從貯藏創建一個分支

如果貯藏了一些工作,將它留在那兒了一會兒,然後繼續在貯藏的分支上工作,在重新應用工作時可能會有問題。 如果應用嘗試修改剛剛修改的文件,你會得到一個合併衝突並不得不解決它。這種情況下,可以運行 git stash branch <new branchname> 以你指定的分支名創建一個新分支,檢出貯藏工作時所在的提交,重新在那應用工作,然後在應用成功後丟棄貯藏。

5.  git clean

git clean 這個命令被設計爲從工作目錄中移除未被追蹤的文件

默認情況下,git clean 命令只會移除沒有忽略的未跟蹤文件

使用 git clean -f -d 命令來移除工作目錄中所有未追蹤的文件以及空的子目錄

如果只是想要看看它會做什麼,可以使用 git clean -d -n

6.  git reset

6.1.  三棵樹

“樹” 在這裏的實際意思是 “文件的集合”,而不是指特定的數據結構。

Git 作爲一個系統,是以它的一般操作來管理並操縱這三棵樹的:

  • HEAD : 上一次提交的快照,下一次提交的父結點
  • Index : 暫存區,預期的下一次提交的快照
  • Working Directory : 工作目錄

HEAD

HEAD 是當前分支引用的指針,它總是指向該分支上的最後一次提交。 這表示 HEAD 將是下一次提交的父結點。 通常,理解 HEAD 的最簡方式,就是將它看做 該分支上的最後一次提交 的快照。

Index(索引)

索引是你的 預期的下一次提交。 我們也會將這個概念引用爲 Git 的“暫存區”,這就是當你運行 git commit 時 Git 看起來的樣子。

Git 將上一次檢出到工作目錄中的所有文件填充到索引區,它們看起來就像最初被檢出時的樣子。 之後你會將其中一些文件替換爲新版本,接着通過 git commit 將它們轉換爲樹來用作新的提交。

工作目錄

工作目錄(通常也叫 工作區)。 另外兩棵樹以一種高效但並不直觀的方式,將它們的內容存儲在 .git 文件夾中。 工作目錄會將它們解包爲實際的文件以便編輯。 你可以把工作目錄當做 沙盒。在你將修改提交到暫存區並記錄到歷史之前,可以隨意更改。

6.2.  工作流程

經典的 Git 工作流程是通過操縱這三個區域來以更加連續的狀態記錄項目快照的。

6.3.  重置的效果

假設現在是這樣的

第 1 步:移動 HEAD

reset 做的第一件事是移動 HEAD 的指向。 這與改變 HEAD 自身不同(checkout 所做的);reset 移動 HEAD 指向的分支。 這意味着如果 HEAD 設置爲 master 分支(例如,你正在 master 分支上), 運行 git reset 9e5e6a4 將會使 master 指向 9e5e6a4。

無論你調用了何種形式的帶有一個提交的 reset,它首先都會嘗試這樣做。 使用 reset --soft,它將僅僅停在那兒。

現在看一眼上圖,理解一下發生的事情:它本質上是撤銷了上一次 git commit 命令。 當你在運行 git commit 時,Git 會創建一個新的提交,並移動 HEAD 所指向的分支來使其指向該提交。 當你將它 reset 回 HEAD~(HEAD 的父結點)時,其實就是把該分支移動回原來的位置,而不會改變索引和工作目錄。

第 2 步:更新索引(--mixed)

如果指定 --mixed 選項,reset 將會在這時停止。 這也是默認行爲,所以如果沒有指定任何選項(在本例中只是 git reset HEAD~),這就是命令將會停止的地方。

現在再看一眼上圖,理解一下發生的事情:它依然會撤銷一上次 提交,但還會 取消暫存 所有的東西。 於是,我們回滾到了所有 git addgit commit 的命令執行之前。

第 3 步:更新工作目錄(--hard)

reset 要做的的第三件事情就是讓工作目錄看起來像索引。 如果使用 --hard 選項,它將會繼續這一步。

注意:其他任何形式的 reset 調用都可以輕鬆撤消,但是 --hard 選項不能,因爲它強制覆蓋了工作目錄中的文件。 

回顧一下:

reset 命令會以特定的順序重寫這三棵樹,在你指定以下選項時停止:

  1. 移動 HEAD 分支的指向 (若指定了 --soft,則到此停止)
  2. 使索引看起來像 HEAD (若未指定 --hard,則到此停止)
  3. 使工作目錄看起來像索引

6.4.  通過路徑來重置

前面講述了 reset 基本形式的行爲,不過你還可以給它提供一個作用路徑。 若指定了一個路徑,reset 將會跳過第 1 步,並且將它的作用範圍限定爲指定的文件或文件集合。 這樣做自然有它的道理,因爲 HEAD 只是一個指針,你無法讓它同時指向兩個提交中各自的一部分。 不過索引和工作目錄 可以部分更新,所以重置會繼續進行第 2、3 步。

現在,假如我們運行 git reset file.txt (這其實是 git reset --mixed HEAD file.txt 的簡寫形式,因爲你既沒有指定一個提交的 SHA-1 或分支,也沒有指定 --soft--hard),它會:

  1. 移動 HEAD 分支的指向 (已跳過)
  2. 讓索引看起來像 HEAD (到此處停止)

所以它本質上只是將 file.txt 從 HEAD 複製到索引中。

它還有 取消暫存文件 的實際效果。 如果我們查看該命令的示意圖,然後再想想 git add 所做的事,就會發現它們正好相反。

可以不讓 Git 從 HEAD 拉取數據,而是通過具體指定一個提交來拉取該文件的對應版本。 我們只需運行類似於 git reset eb43bf file.txt 的命令即可。

它其實做了同樣的事情,也就是把工作目錄中的文件恢復到 v1 版本

6.5.  reset與checkout之間的區別

reset 一樣,checkout 也操縱三棵樹,不過它有一點不同,這取決於你是否傳給該命令一個文件路徑。

不帶路徑

運行 git checkout [branch] 與運行 git reset --hard [branch] 非常相似,它會更新所有三棵樹使其看起來像 [branch],不過有兩點重要的區別。

首先不同於 reset --hardcheckout 對工作目錄是安全的,它會通過檢查來確保不會將已更改的文件弄丟。 其實它還更聰明一些。它會在工作目錄中先試着簡單合併一下,這樣所有 還未修改過的 文件都會被更新。 而 reset --hard 則會不做檢查就全面地替換所有東西。

第二個重要的區別是 checkout 如何更新 HEAD。 reset 會移動 HEAD 分支的指向,而 checkout 只會移動 HEAD 自身來指向另一個分支。

帶路徑

運行 checkout 的另一種方式就是指定一個文件路徑,這會像 reset 一樣不會移動 HEAD。 它就像 git reset [branch] file 那樣用該次提交中的那個文件來更新索引,但是它也會覆蓋工作目錄中對應的文件。 它就像是 git reset --hard [branch] file(如果 reset 允許你這樣運行的話), 這樣對工作目錄並不安全,它也不會移動 HEAD。

git checkout [branch] file 用指定的某次提交中的那個文件來更新索引中的這個文件,因爲分支是一個指針,指向的是某一次提交,因此當我們說檢出分支的時候其實說的是將那個分支所指向的提交更新到暫存區和工作區中,所以說從某次提交中更新某個文件到當前工作目錄沒毛病。

 

https://git-scm.com/docs

https://git-scm.com/docs/git-reset

https://git-scm.com/docs/git-checkout

7.  git merge

git merge 工具用來合併一個或者多個分支到你已經檢出的分支中。 然後它將當前分支指針移動到合併結果上。

一般用法是 git merge <branch> 帶上一個你想合併進來的一個分支名稱。

7.1.  合併衝突

首先,在做一次可能有衝突的合併前儘可能保證工作目錄是乾淨的。 如果你有正在做的工作,要麼提交到一個臨時分支要麼儲藏它。

可以使用 git merge --abort 來中斷次合併

--abort  選項會嘗試恢復到你運行合併前的狀態。但當運行命令前,在工作目錄中有未暫存、未提交的修改時它不能完美處理,除此之外它都工作地很好。

使用 -Xignore-all-space-Xignore-space-change 選項可以忽略空白。第一個選項在比較行時 完全忽略 空白修改,第二個選項將一個空白符與多個連續的空白字符視作等價的。

7.2.  撤銷合併

假設現在在一個主題分支上工作,不小心將其合併到 master 中,現在提交歷史看起來是這樣:

對於這種意外的合併提交,有兩種方法來解決這個問題,這取決於你想要的結果是什麼。

第一種、修復引用

如果這個不想要的合併提交只存在於你的本地倉庫中,最簡單且最好的解決方案是移動分支到你想要它指向的地方。 大多數情況下,如果你在錯誤的 git merge 後運行 git reset --hard HEAD~,這會重置分支指向所以它們看起來像這樣:

回顧一下 git reset --hard 

  1. 移動 HEAD 指向的分支
  2. 使索引看起來像 HEAD
  3. 使工作目錄看起來像索引

這個方法的缺點是它會重寫歷史,在一個共享的倉庫中可能會造成一些問題。比如,假設有人在在合併之後又創建了新的提交,那麼移動指針實際上會丟失那些改動。

第二種、還原提交

如果移動分支指針並不適合你,Git 給你一個生成一個新提交的選項,提交將會撤消一個已存在提交的所有修改。 Git 稱這個操作爲“還原”,在這個特定的場景下,你可以像這樣調用它:

git revert -m 1 HEAD

-m 1 標記指出 “mainline” 需要被保留下來的父結點。 當你引入一個合併到 HEAD(git merge topic),新提交有兩個父結點:第一個是 HEADC6),第二個是將要合併入分支的最新提交(C4)。 在本例中,我們想要撤消所有由父結點 #2(C4)合併引入的修改,同時保留從父結點 #1(C6)開始的所有內容。

有還原提交的歷史看起來像這樣:

git revert -m 1 後,新的提交 ^MC6 有完全一樣的內容,所以從這兒開始就像合併從未發生過。

如果你在 topic 中增加工作然後再次合併,Git 只會引入被還原的合併 之後 的修改。

解決這個最好的方式是撤消還原原始的合併,因爲現在你想要引入被還原出去的修改,然後 創建一個新的合併提交:

git revert ^M

在本例中,M^M 抵消了。 ^^M 事實上合併入了 C3C4 的修改,C8 合併了 C7 的修改,所以現在 topic 已經完全被合併了。

8.  遠程倉庫

# 查看你已經配置的遠程倉庫服務器
git remote

# 顯示需要讀寫遠程倉庫使用的 Git 保存的簡寫與其對應的 URL
git remote -v

# 查看某一個遠程倉庫的更多信息
git remote show <remote>

# 添加一個新的遠程 Git 倉庫,同時指定一個方便使用的簡寫
git remote add <shortname> <url>

# 修改一個遠程倉庫的簡寫名
git remote rename

# 從遠程倉庫中獲得數據
git fetch <remote>

# 推送到遠程倉庫
git push <remote> <branch>

如果使用 clone 命令克隆了一個倉庫,命令會自動將其添加爲遠程倉庫並默認以 “origin” 爲簡寫。所以,git fetch origin 會抓取克隆(或上一次抓取)後新推送的所有工作。

必須注意 git fetch 命令只會將數據下載到你的本地倉庫——它並不會自動合併或修改你當前的工作。可以用 git pull 命令來自動抓取後合併該遠程分支到當前分支。

git pull 命令基本上就是 git fetchgit merge 命令的組合體,Git 從你指定的遠程倉庫中抓取內容,然後馬上嘗試將其合併進你所在的分支中。

git push 命令用來與另一個倉庫通信,計算你本地數據庫與遠程倉庫的差異,然後將差異推送到另一個倉庫中。它需要有另一個倉庫的寫權限,因此這通常是需要驗證的。

9.  補丁

每一次提交都是一個補丁

9.1.  git cherry-pick

git cherry-pick 命令用來獲得在單個提交中引入的變更,然後嘗試將作爲一個新的提交引入到你當前分支上。從一個分支單獨一個或者兩個提交而不是合併整個分支的所有變更是非常有用的。

Git 中的揀選類似於對特定的某次提交的變基。 它會提取該提交的補丁,之後嘗試將其重新應用到當前分支上。這種方式在你只想引入主題分支中的某個提交時很有用。 

如上圖所示,假設現在的提交時這樣(揀選之前)

如果你希望將提交 e43a6 拉取到 master 分支,你可以運行:

git cherry-pick e43a6

這樣會拉取和 e43a6 相同的更改,但是因爲應用的日期不同,你會得到一個新的提交 SHA-1 值。 現在你的歷史會變成這樣:

9.2.  git rebase

git rebase 命令基本是一個自動化的 cherry-pick 命令。它計算出一系列的提交,然後再以同樣的順序一個一個的 cherry-picks 出它們。

在 Git 中整合來自不同分支的修改主要有兩種方法:merge 以及 rebase

假設現在的提交歷史是這樣的:

之前介紹過,整合分支最容易的方法是 merge 命令。 它會把兩個分支的最新快照(C3C4)以及二者最近的共同祖先(C2)進行三方合併,合併的結果是生成一個新的快照(並提交)。

其實,還有一種方法:可以提取在 C4 中引入的補丁和修改,然後在 C3 的基礎上應用一次。 在 Git 中,這種操作就叫做 變基(rebase)。可以使用 rebase 命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。

在這個例子中,你可以檢出 experiment 分支,然後將它變基到 master 分支上:

git checkout experiment
git rebase master

這樣就將 C4 中的修改變基到 C3 上了

現在回到 master 分支,進行一次快進合併。

此時,C4' 指向的快照就和前面使用merge合併後的那個 C5 指向的快照一模一樣了。

這兩種整合方法的最終結果沒有任何區別,但是變基使得提交歷史更加整潔。 你在查看一個經過變基的分支的歷史記錄時會發現,儘管實際的開發工作是並行的, 但它們看上去就像是串行的一樣,提交歷史是一條直線沒有分叉。

金科玉律:“如果提交存在於你的倉庫之外,而別人可能基於這些提交進行開發,那麼不要執行變基。”

9.3.  git revert

git revert 命令本質上就是一個逆向的 git cherry-pick 操作。它將你提交中的變更的以完全相反的方式應用到一個新創建的提交中,本質上就是撤銷或者還原。

git revert 相當於撤銷/還原了上一次提交,就好像從當前這個提交中摘除上一次提交的內容,然後生成了一個新的提交

再舉個例子

假設現在的提交歷史是 C1 <--- C2 <--- C3,HEAD指向C3,此時執行 git revert -m 1 的話就生成一個新的提交 C4,C4的內容和C2是一樣的

reset是通過移動分支的指向來達到撤銷的目的,revert是通過挑出提交的內容重新生成一次新的提交來達到撤銷的目的

10.  選擇修訂版本

單個修訂版本

可以通過任意一個提交的 40 個字符的完整 SHA-1 散列值來指定它,不過還有很多更人性化的方式來做同樣的事情。

簡短的 SHA-1

Git 十分智能,你只需要提供 SHA-1 的前幾個字符就可以獲得對應的那次提交, 當然你提供的 SHA-1 字符數量不得少於 4 個,並且沒有歧義——也就是說, 當前對象數據庫中沒有其它對象以這段 SHA-1 開頭。

Git 可以爲 SHA-1 值生成出簡短且唯一的縮寫。如果在 git log 後加上 --abbrev-commit 參數,輸出結果裏就會顯示簡短且唯一的值。

git log --abbrev-commit --pretty=oneline

引用日誌

當你在工作時, Git 會在後臺保存一個引用日誌(reflog),引用日誌記錄了最近幾個月你的 HEAD 和分支引用所指向的歷史。

可以使用 git reflog 來查看引用日誌

每當你的 HEAD 所指向的位置發生了變化,Git 就會將這個信息存儲到引用日誌這個歷史記錄裏。 你也可以通過 reflog 數據來獲取之前的提交歷史。 如果你想查看倉庫中 HEAD 在五次前的所指向的提交,你可以使用 @{n} 來引用 reflog 中輸出的提交記錄。

git show HEAD@{5}

注意:引用日誌只存在於本地倉庫,它只是一個記錄你在 自己 的倉庫裏做過什麼的日誌。

祖先引用

祖先引用是另一種指明一個提交的方式。 如果你在引用的尾部加上一個 ^ , Git 會將其解析爲該引用的上一個提交。

可以使用 HEAD^ 來查看上一個提交,也就是 “HEAD 的父提交”

可以在 ^ 後面添加一個數字來指明想要 哪一個 父提交。例如 d921970^2 代表 “d921970 的第二父提交” 這個語法只適用於合併的提交,因爲合併提交會有多個父提交。 合併提交的第一父提交是你合併時所在分支(通常爲 master),而第二父提交是你所合併的分支(例如 topic

git show d921970^
git show d921970^2

另一種指明祖先提交的方法是 ~(波浪號)。同樣是指向第一父提交,因此 HEAD~HEAD^ 是等價的。而區別在於後面加數字的時候HEAD~2 代表“第一父提交的第一父提交”,也就是“祖父提交”,HEAD^2 代表“HEAD的第二父提交”。

HEAD~3 也可以寫成 HEAD~~~,表示“第一父提交的第一父提交的第一父提交”

可以組合使用這兩個語法,例如,可以通過 HEAD~3^2 來取得之前引用的第二父提交(假設它是一個合併提交)

補充

1、在 HEAD 後面加 ^ 或者 ~ 其實就是以 HEAD 爲基準,來表示之前的版本,因爲 HEAD 代表當前分支的最新版本,那麼 HEAD~ 和 HEAD^ 都是指次新版本,也就是倒數第二個版本,HEAD~~ 和 HEAD^^ 都是指次次新版本,也就是倒數第三個版本,以此類推。

2、HEAD~ 和 HEAD^ 的作用是相同的,它們本來的面貌是 HEAD~1 和 HEAD^1

3、如果後面跟的數字大於1的話就有區別了,比如:HEAD~2 代表後退兩步,每一步都後退到第一個父提交上,而 HEAD^2 代表後退一步,這一步退到第二個父提交上,如果沒有第二個父提交就會報錯.

提交區間

最常用的指明提交區間語法是雙點。 這種語法可以讓 Git 選出在一個分支中而不在另一個分支中的提交。

例如,現在的提交歷史是這樣的:

如果想要查看 experiment 分支中還有哪些提交尚未被合併入 master 分支。我們可以使用 master..experiment 來讓 Git 顯示這些提交。也就是“在 experiment 分支中而不在 master 分支中的提交”。

git log master..experiment

反過來,如果想查看在 master 分支中而不在 experiment 分支中的提交,你只要交換分支名即可。experiment..master 會顯示在 master 分支中而不在 experiment 分支中的提交。

雙點語法很好用,但有時候你可能需要兩個以上的分支才能確定你所需要的修訂, 比如查看哪些提交是被包含在某些分支中的一個,但是不在你當前的分支上。 Git 允許你在任意引用前加上 ^ 字符或者 --not 來指明你不希望提交被包含其中的分支。 因此下列三個命令是等價的:

git log refA..refB
git log ^refA refB
git log refB --not refA

比如,你想查看所有被 refA 或 refB 包含的但是不被 refC 包含的提交,你可以使用以下任意一個命令:

git log refA refB ^refC
git log refA refB --not refC

11.  查看提交歷史

不傳入任何參數的默認情況下,git log 會按時間先後順序列出所有的提交,最近的更新排在最上面。

# 顯示最近的兩次提交所引入的差異
git log -p -2

# 顯示簡短且唯一的值
git log --abbrev-commit

git log --pretty=oneline

git log --pretty=format:"%h %s" --graph

12.  演示

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章