推薦學習網站:https://learngitbranching.js.org/
🧚♀️ 主要
1. Git Commit
Git 倉庫中的提交記錄保存的是你的目錄下所有文件的快照,就像是把整個目錄複製,然後再粘貼一樣,但比複製粘貼優雅許多!
Git 希望提交記錄儘可能地輕量,因此在你每次進行提交時,它並不會盲目地複製整個目錄。條件允許的情況下,它會將當前版本與倉庫中的上一個版本進行對比,並把所有的差異打包到一起作爲一個提交記錄。
Git 還保存了提交的歷史記錄。這也是爲什麼大多數提交記錄的上面都有父節點的原因 —— 我們會在圖示中用箭頭來表示這種關係。對於項目組的成員來說,維護提交歷史對大家都有好處。
關於提交記錄太深入的東西咱們就不再繼續探討了,現在你可以把提交記錄看作是項目的快照。提交記錄非常輕量,可以快速地在這些提交記錄之間切換!
git commit
2. Git Branch
Git 的分支也非常輕量。它們只是簡單地指向某個提交紀錄 —— 僅此而已。所以許多 Git 愛好者傳頌:早建分支!多用分支!
這是因爲即使創建再多分的支也不會造成儲存或內存上的開銷,並且按邏輯分解工作到不同的分支要比維護那些特別臃腫的分支簡單多了。
在將分支和提交記錄結合起來後,我們會看到兩者如何協作。現在只要記住使用分支其實就相當於在說:“我想基於這個提交以及它所有的父提交進行新的工作。”
- 要創建一個到名爲 newImage 的分支:
git branch newImage
- 在提交修改之前先切換到新的分支上:
git checkout newImage
如果你想創建一個新的分支同時切換到新創建的分支的話,可以通過
git checkout -b <your-branch-name>
來實現。
3. Git Merge
如何將兩個分支合併到一起?就是說我們新建一個分支,在其上開發某個新功能,開發完成後再合併回主線。
先看第一種方法 —— git merge。在 Git 中合併兩個分支時會產生一個特殊的提交記錄,它有兩個父節點。翻譯成自然語言相當於:“我要把這兩個父節點本身及它們所有的祖先都包含進來。”
- 我們要把 bugFix 合併到 master 裏:
git merge bugFix
- 再把 master 分支合併到 bugFix:
git checkout bugFix; git merge master
4. Git Rebase
第二種合併分支的方法是 git rebase。Rebase 實際上就是取出一系列的提交記錄,“複製”它們,然後在另外一個地方逐個的放下去。
Rebase 的優勢就是可以創造更線性的提交歷史,這聽上去有些難以理解。如果只允許使用 Rebase 的話,代碼庫的提交歷史將會變得異常清晰。
-
我們想要把 bugFix 分支裏的工作直接移到 master 分支上:
git rebase master
移動以後會使得兩個分支的功能看起來像是按順序開發,但實際上它們是並行開發的。
-
切換到 master 上。把它 rebase 到 bugFix 分支上:
git rebase bugFix
5. 分離 HEAD
HEAD 是一個對當前檢出記錄的符號引用 —— 也就是指向你正在其基礎上進行工作的提交記錄。
HEAD 總是指向當前分支上最近一次提交記錄。大多數修改提交樹的 Git 命令都是從改變 HEAD 的指向開始的。
HEAD 通常情況下是指向分支名的(如 bugFix)。在你提交時,改變了 bugFix 的狀態,這一變化通過 HEAD 變得可見。
分離的 HEAD
分離的 HEAD 就是讓其指向了某個具體的提交記錄而不是分支名。在命令執行之前的狀態如下所示:
HEAD -> master -> C1
HEAD 指向 master, master 指向 C1
git checkout C1
6. 相對引用(^)
通過指定提交記錄哈希值的方式在 Git 中移動不太方便。在實際應用時,並沒有像本程序中這麼漂亮的可視化提交樹供你參考,所以你就不得不用 git log 來查查看提交記錄的哈希值。
比較令人欣慰的是,Git 對哈希的處理很智能。你只需要提供能夠唯一標識提交記錄的前幾個字符即可。因此我可以僅輸入fed2 而不是上面的一長串字符。
相對引用非常給力,兩個簡單的用法:
① 使用 ^
向上移動 1 個提交記錄
② 使用 ~<num>
向上移動多個提交記錄,如 ~3
所以 master^
相當於“master
的父節點”。master^^
是 master
的第二個父節點
- 現在咱們切換到 master 的父節點:
git checkout master^
7. 相對引用(~)
如果你想在提交樹中向上移動很多步的話,敲那麼多 ^ 貌似也挺煩人的,Git 當然也考慮到了這一點,於是又引入了操作符 ~。
該操作符後面可以跟一個數字(可選,不跟數字時與 ^ 相同,向上移動一次),指定向上移動多少次。
- 用 ~ 一次後退四步:
git checkout HEAD~4
使用相對引用最多的就是移動分支。可以直接使用 -f
選項讓分支指向另一個提交。
- 將 master 分支強制指向 HEAD 的第 3 級父提交:
git branch -f master HEAD~3
8. 撤銷變更
在 Git 裏撤銷變更的方法很多。和提交一樣,撤銷變更由底層部分(暫存區的獨立文件或者片段)和上層部分(變更到底是通過哪種方式被撤銷的)組成。我們這個應用主要關注的是後者。
主要有兩種方法用來撤銷變更 —— 一是 git reset
,還有就是 git revert
。
git reset
通過把分支記錄回退幾個提交記錄來實現撤銷改動。你可以將這想象成“改寫歷史”。git reset
向上移動分支,原來指向的提交記錄就跟從來沒有提交過一樣。
git reset HEAD~1
Git 把 master 分支移回到 C1;現在我們的本地代碼庫根本就不知道有 C2 這個提交了。
(注:在reset後, C2 所做的變更還在,但是處於未加入暫存區狀態。)
雖然在你的本地分支中使用 git reset 很方便,但是這種“改寫歷史”的方法對大家一起使用的遠程分支是無效的哦!
- 爲了撤銷更改並分享給別人,我們需要使用 git revert:
git revert HEAD
9. Git Cherry-pick
git cherry-pick <提交號>...
如果你想將一些提交複製到當前所在的位置(HEAD)下面的話, Cherry-pick 是最直接的方式了。
這裏有一個倉庫, 我們想將 side 分支上的工作複製到 master 分支,你立刻想到了之前學過的 rebase 了吧?但是咱們還是看看 cherry-pick 有什麼本領吧。
- 我們只需要提交記錄 C2 和 C4,到當前分支下:
git cherry-pick C2 C4
10. 交互式 rebase
當你知道你所需要的提交記錄(並且還知道這些提交記錄的哈希值)時, 用 cherry-pick 再好不過了 —— 沒有比這更簡單的方式了。
但是如果你不清楚你想要的提交記錄的哈希值呢? 幸好 Git 幫你想到了這一點, 我們可以利用交互式的 rebase —— 如果你想從一系列的提交記錄中找到想要的記錄, 這就是最好的方法了。
交互式 rebase 指的是使用帶參數 --interactive
的 rebase 命令, 簡寫爲 -i
如果你在命令後增加了這個選項, Git 會打開一個 UI 界面並列出將要被複制到目標分支的備選提交記錄,它還會顯示每個提交記錄的哈希值和提交說明,提交說明有助於你理解這個提交進行了哪些更改。
在實際使用時,所謂的 UI 窗口一般會在文本編輯器 —— 如 Vim —— 中打開一個文件。
當 rebase UI界面打開時, 你能做 3 件事:
- 調整提交記錄的順序(通過鼠標拖放來完成)
- 刪除你不想要的提交(通過切換 pick 的狀態來完成,關閉就意味着你不想要這個提交記錄)
- 合併提交。它允許你把多個提交記錄合併成一個。
git rebase -i HEAD~4
11. 只取一個提交記錄
本地棧式提交
🌰:
我正在解決某個特別棘手的 Bug,爲了便於調試而在代碼中添加了一些調試命令並向控制檯打印了一些信息。
這些調試和打印語句都在它們各自的提交記錄裏。最後我終於找到了造成這個 Bug 的根本原因,解決掉以後覺得沾沾自喜!
最後就差把 bugFix 分支裏的工作合併回 master 分支了。
你可以選擇通過 fast-forward 快速合併到 master 分支上,但這樣的話 master 分支就會包含我這些調試語句了。你肯定不想這樣,應該還有更好的方式……
實際我們只要讓 Git 複製解決問題的那一個提交記錄就可以了。可以使用:
git rebase -i
git cherry-pick
12. 提交的技巧(1)
🌰:
你之前在 newImage 分支上進行了一次提交,然後又基於它創建了 caption 分支,然後又提交了一次。
此時你想對的某個以前的提交記錄進行一些小小的調整。比如設計師想修改一下 newImage 中圖片的分辨率,儘管那個提交記錄並不是最新的了。
可以通過下面的方法來克服困難:
- 先用
git rebase -i
將提交重新排序,然後把我們想要修改的提交記錄挪到最前; - 然後用
commit --amend
來進行一些小修改; - 接着再用
git rebase -i
來將他們調回原來的順序; - 最後我們把
master
移到修改的最前端(用你自己喜歡的方法),就大功告成啦!
13. 提交的技巧(2)
我們可以使用 rebase -i
對提交記錄進行重新排序。只要把我們想要的提交記錄挪到最前端,我們就可以很輕鬆的用 --amend
修改它,然後把它們重新排成我們想要的順序。
但這樣做就唯一的問題就是要進行兩次排序,而這有可能造成由 rebase 而導致的衝突。
cherry-pick
可以將提交樹上任何地方的提交記錄取過來追加到 HEAD 上(只要不是 HEAD 上游的提交就沒問題)。
git cherry-pick C2
14. Git Tag
Git 的 tag 可以(在某種程度上 —— 因爲標籤可以被刪除後重新在另外一個位置創建同名的標籤)永久地將某個特定的提交命名爲里程碑,然後就可以像分支一樣引用了。
更難得的是,它們並不會隨着新的提交而移動。你也不能檢出到某個標籤上面進行修改提交,它就像是提交樹上的一個錨點,標識了某個特定的位置。
- 建立一個標籤,指向提交記錄 C1,表示這是我們 1.0 版本:
git tag v1 C1
如果你不指定提交記錄,Git 會用
HEAD
所指向的位置。
15. Git Describe
由於標籤在代碼庫中起着“錨點”的作用,Git 還爲此專門設計了一個命令用來描述離你最近的錨點(也就是標籤),它就是 git describe
!
Git Describe 能幫你在提交歷史中移動了多次以後找到方向;當你用 git bisect
(一個查找產生 Bug 的提交記錄的指令)找到某個提交記錄時,或者是當你坐在你那剛剛度假回來的同事的電腦前時, 可能會用到這個命令。
git describe
的語法是:
git describe <ref>
<ref>
可以是任何能被 Git 識別成提交記錄的引用,如果你沒有指定的話,Git 會以你目前所檢出的位置(HEAD
)。
它輸出的結果是這樣的:
<tag>_<numCommits>_g<hash>
tag
表示的是離 ref
最近的標籤, numCommits
是表示這個 ref
與 tag
相差有多少個提交記錄, hash
表示的是你所給定的 ref
所表示的提交記錄哈希值的前幾位。
當 ref
提交記錄上有某個標籤時,則只輸出標籤名稱。
-
git describe master
會輸出:v1_2_gC2
-
git describe side
會輸出:v2_1_gC4
16. 多次 rebase
這裏準備了很多分支,把這些分支 rebase 到 master 上。
但是你的領導給你提了點要求 —— 他們希望得到有序的提交歷史,也就是我們最終的結果應該是 C6'
在 C7'
上面, C5'
在 C6'
上面,依此類推。
git rebase master bugFix
git rebase bugFix side
git rebase side another
git rebase another master
17. 兩個父節點
選擇父提交記錄
操作符 ^
與 ~
符一樣,後面也可以跟一個數字。
但是該操作符後面的數字與 ~
後面的不同,並不是用來指定向上返回幾代,而是指定合併提交記錄的某個父提交。還記得前面提到過的一個合併提交有兩個父提交吧,所以遇到這樣的節點時該選擇哪條路徑就不是很清晰了。
Git 默認選擇合併提交的“第一個”父提交,在操作符 ^
後跟一個數字可以改變這一默認行爲。
git checkout master^
git checkout master^2
git checkout HEAD~; git checkout HEAD^2; git chekcout HEAD~2
- 支持鏈式操作:
git checkout HEAD~^2~2
18. 糾纏不清的分支
現在我們的 master
分支是比 one
、two
和 three
要多幾個提交。出於某種原因,我們需要把 master
分支上最近的幾次提交做不同的調整後,分別添加到各個的分支上。
one
需要重新排序並刪除 C5
,two
僅需要重排排序,而 three
只需要提交一次。
git checkout one
git cherry-pick C4 C3 C2
git checkout two
git cherry-pick C5 C4 C3 C2
git branch -f three C2
🧙♂️ 遠程
Push & Pull —— Git 遠程倉庫
1. Git Clone
遠程倉庫:
遠程倉庫有一系列強大的特性:
- 首先也是最重要的的點, 遠程倉庫是一個強大的備份。本地倉庫也有恢復文件到指定版本的能力, 但所有的信息都是保存在本地的。有了遠程倉庫以後,即使丟失了本地所有數據, 你仍可以通過遠程倉庫拿回你丟失的數據。
- 還有就是, 遠程讓代碼社交化了! 既然你的項目被託管到別的地方了, 你的朋友可以更容易地爲你的項目做貢獻(或者拉取最新的變更)。
git clone
命令的作用是在本地創建一個遠程倉庫的拷貝(比如從 github.com)。
2. 遠程分支
遠程分支有一個命名規範 —— 它們的格式是:<remote name>/<branch name>
因此,如果你看到一個名爲 o/master
的分支,那麼這個分支就叫 master
,遠程倉庫的名稱就是 o
。
3. Git Fetch
git fetch
—— 從遠程倉庫獲取數據。
遠程倉庫有兩個我們本地倉庫中沒有的提交。git fetch 之後,C2、C3 被下載到了本地倉庫,同時遠程分支 o/master 也被更新,反映到了這一變化。
git fetch
完成了僅有的但是很重要的兩步:
- 從遠程倉庫下載本地倉庫中缺失的提交記錄
- 更新遠程分支指針(如
o/master
)
git fetch
實際上將本地倉庫中的遠程分支更新成了遠程倉庫相應分支最新的狀態。
遠程分支反映了遠程倉庫在你最後一次與它通信時的狀態,git fetch
就是你與遠程倉庫通信的方式了!
git fetch
通常通過互聯網(使用 http://
或 git://
協議) 與遠程倉庫通信。
git fetch
並不會改變你本地倉庫的狀態。它不會更新你的 master
分支,也不會修改你磁盤上的文件。
許多開發人員誤以爲執行了 git fetch 以後,他們本地倉庫就與遠程倉庫同步了。它可能已經將進行這一操作所需的所有數據都下載了下來,但是並沒有修改你本地的文件。所以,你可以將 git fetch
的理解爲單純的下載操作。
4. Git Pull
既然我們已經知道了如何用 git fetch
獲取遠程的數據, 現在我們學習如何將這些變化更新到我們的工作當中。
其實有很多方法的 —— 當遠程分支中有新的提交時,你可以像合併本地分支那樣來合併遠程分支。也就是說就是你可以執行以下命令:
git cherry-pick o/master
git rebase o/master
git merge o/master
- 等等
實際上,由於先抓取更新再合併到本地分支這個流程很常用,因此 Git 提供了一個專門的命令來完成這兩個操作。它就是 git pull
。
git fetch; git merge o/master
==git pull
5. 模擬團隊合作
git clone
git fakeTeamwork 2
git commit
git pull
6. Git Push
git push
負責將你的變更上傳到指定的遠程倉庫,並在遠程倉庫上合併你的新提交記錄。
7. 偏離的提交歷史
假設你週一克隆了一個倉庫,然後開始研發某個新功能。到週五時,你新功能開發測試完畢,可以發佈了。但是 —— 天啊!你的同事這周寫了一堆代碼,還改了許多你的功能中使用的 API,這些變動會導致你新開發的功能變得不可用。但是他們已經將那些提交推送到遠程倉庫了,因此你的工作就變成了基於項目舊版的代碼,與遠程倉庫最新的代碼不匹配了。
這種情況(歷史偏離)有許多的不確定性,Git 是不會允許你 push
變更的。實際上它會強制你先合併遠程最新的代碼,然後才能分享你的工作。
- 先更新本地倉庫中的遠程分支,然後將我們的工作移動到最新的提交記錄下,最後再推送到遠程倉庫:
git fetch; git rebase o/master; git push
- 或者:使用
merge
儘管git merge
不會移動你的工作(它會創建新的合併提交),但是它會告訴 Git 你已經合併了遠程倉庫的所有變更。這是因爲遠程分支現在是你本地分支的祖先,也就是說你的提交已經包含了遠程分支的所有變化。- 更新本地倉庫中的遠程分支,然後合併了新變更到我們的本地分支(爲了包含遠程倉庫的變更),最後把工作推送到遠程倉庫:
git fetch; git merge o/master; git push
- 更新本地倉庫中的遠程分支,然後合併了新變更到我們的本地分支(爲了包含遠程倉庫的變更),最後把工作推送到遠程倉庫:
git pull --rebase
就是fetch
和rebase
的簡寫!
克隆你的倉庫
模擬一次遠程提交(fakeTeamwork)
完成一次本地提交
用 rebase 發佈你的工作
git clone
git fakeTeamwork
git commit
git pull --rebase
git push
關於 origin 和它的周邊 —— Git 遠程倉庫高級操作
1. 推送主分支
合併特性分支
- 將特性分支集成到
master
上:git pull --rebase
- 推送並更新遠程分支:
git push
這裏共有三個特性分支 —— side1 side2 和 side3
需要將這三分支按順序推送到遠程倉庫
因爲遠程倉庫已經被更新過了,所以我們還要把那些工作合並過來
git fetch
git rebase o/master side1
git rebase side1 side2
git rebase side2 side3
git rebase side3 master
git push
2. 合併遠程倉庫
關於 rebase 的優缺點:
- 優點:Rebase 使你的提交樹變得很乾淨, 所有的提交都在一條線上
- 缺點:Rebase 修改了提交樹的歷史(🌰:提交 C1 可以被 rebase 到 C3 之後。這看起來 C1 中的工作是在 C3 之後進行的,但實際上是在 C3 之前。)
一些開發人員喜歡保留提交歷史,因此更偏愛 merge
。而其他人可能更喜歡乾淨的提交樹,於是偏愛 rebase
。
利用 merge 完成上面 1 中的例子:
git checkout master
git pull
git merge side1
git merge side2
git merge side3
git push
3. 遠程追蹤
直接了當地講,master
和 o/master
的關聯關係就是由分支的“remote tracking”屬性決定的。master
被設定爲跟蹤 o/master
—— 這意味着爲 master
分支指定了推送的目的地以及拉取後合併的目標。
當你克隆時, Git 會爲遠程倉庫中的每個分支在本地倉庫中創建一個遠程分支(比如 o/master
)。然後再創建一個跟蹤遠程倉庫中活動分支的本地分支,默認情況下這個本地分支會被命名爲 master
。
在克隆的時候會看到下面的輸出:local branch "master" set to track remote branch "o/master"
你可以讓任意分支跟蹤 o/master,然後該分支會像 master 分支一樣得到隱含的 push 目的地以及 merge 的目標。 這意味着你可以在分支 totallyNotMaster 上執行 git push,將工作推送到遠程倉庫的 master 分支上。
有兩種方法設置這個屬性(設置遠程追蹤分支的方法):
- 通過遠程分支檢出一個新的分支。
- 創建一個名爲 totallyNotMaster 的分支,它跟蹤遠程分支 o/master:
git checkout -b totallyNotMaster o/master
- 創建一個名爲 totallyNotMaster 的分支,它跟蹤遠程分支 o/master:
- 使用
git branch -u
命令。- 讓 foo 跟蹤 o/master :
git branch -u o/master foo
- 如果當前就在 foo 分支上,還可以省略 foo:
git branch -u o/master
- 讓 foo 跟蹤 o/master :
🌰:
👇
git checkout -b side o/master
git commit
git pull --rabase
git push
4. Git push 的參數(1)
git push <remote> <place>
<place>
參數是什麼意思呢?🌰:git push origin master
把這個命令翻譯過來就是:切到本地倉庫中的“master”分支,獲取所有的提交,再到遠程倉庫“origin”中找到“master”分支,將遠程倉庫中沒有的提交記錄都添加上去,搞定之後告訴我。
我們通過“place”參數來告訴 Git 提交記錄來自於 master,要推送到遠程倉庫中的 master。它實際就是要同步的兩個倉庫的位置。
5. Git push 的參數(2)
如果來源和去向分支的名稱不同呢?比如你想把本地的 foo 分支推送到遠程倉庫中的 bar 分支。
要同時爲源和目的地指定 <place>
的話,只需要用冒號 :
將二者連起來就可以了:git push origin <source>:<destination>
如果你要推送到的目的分支不存在,Git 會在遠程倉庫中根據你提供的名稱幫你創建這個分支!
git push origin foo^:master
🌰:
👇
git push origin master^:foo
git push origin foo:master
6. Git fetch 的參數
git fetch
的參數和 git push
極其相似。他們的概念是相同的,只是方向相反罷了。
<place>
參數
如果你 git fetch origin foo
這樣爲 git fetch
設置 <place>
的話,Git 會到遠程倉庫的 foo
分支上,然後獲取所有本地不存在的提交,放到本地的 o/foo
上。
指定 <source>:<destination>
—— source
現在指的是遠程倉庫中的位置,而 <destination>
纔是要放置提交的本地倉庫的位置。
如果執行命令前目標分支不存在,跟 git push 一樣,Git 會在 fetch 前自己創建立本地分支。
git fetch origin foo~1:bar
如果 git fetch
沒有參數,它會下載所有的提交記錄到各個遠程分支……
7. 沒有 source 的 source
古怪的 <source>
Git 有兩種關於 <source>
的用法是比較詭異的,即你可以在 git push
或 git fetch
時不指定任何 source
,方法就是僅保留冒號和 destination 部分,source 部分留空。
- 如果 push 空
<source>
到遠程倉庫,會刪除遠程倉庫中的分支:git push origin :side
(本地和遠程 side 分支都被刪除了) - 如果 fetch 空
<source>
到本地,會在本地創建一個新分支:git fetch origin :bugFix
8. Git Pull 的參數
git pull 實際上就是 fetch + merge 的縮寫,git pull 唯一關注的是提交最終合併到哪裏(也就是爲 git fetch 所提供的 destination 參數)。
git pull origin foo
==git fetch origin foo; git merge o/foo
git pull origin bar~1:bugFix
==git fetch origin bar~1:bugFix; git merge bugFix
🌰:
👇
git pull origin bar:foo
git pull origin master:side