前言
Git是目前世界上最先進(jìn)的分布式版本控制系統(tǒng)。
工作原理 / 流程:

Workspace:工作區(qū)
Index / Stage:暫存區(qū)
Repository:倉庫區(qū)(或本地倉庫)
Remote:遠(yuǎn)程倉庫
SVN與Git的最主要的區(qū)別?
SVN是集中式版本控制系統(tǒng),版本庫是集中放在中央服務(wù)器的,而干活的時(shí)候,用的都是自己的電腦,所以首先要從中央服務(wù)器哪里得到最新的版本,然后干活,干完后,需要把自己做完的活推送到中央服務(wù)器。集中式版本控制系統(tǒng)是必須聯(lián)網(wǎng)才能工作,如果在局域網(wǎng)還可以,帶寬夠大,速度夠快,如果在互聯(lián)網(wǎng)下,如果網(wǎng)速慢的話,就納悶了。
Git是分布式版本控制系統(tǒng),那么它就沒有中央服務(wù)器的,每個(gè)人的電腦就是一個(gè)完整的版本庫,這樣,工作的時(shí)候就不需要聯(lián)網(wǎng)了,因?yàn)榘姹径际窃谧约旱碾娔X上。既然每個(gè)人的電腦都有一個(gè)完整的版本庫,那多個(gè)人如何協(xié)作呢?比如說自己在電腦上改了文件A,其他人也在電腦上改了文件A,這時(shí),你們兩之間只需把各自的修改推送給對(duì)方,就可以互相看到對(duì)方的修改了
一,基礎(chǔ)篇
1,提交代碼
$ git add . //添加分支代碼到暫存區(qū)域
$ git commit -m "fix :-----" //提交分支代碼---填寫提交信息
2,創(chuàng)建分支
$ git branch newImage // 創(chuàng)建新分支 newImage
$ git checkout newImage 或 git switch newImage //切換到本地newImage分支
$ git checkout -b newImage 創(chuàng)建新分支并切換到新分支
3,合并分支
$ git merge bugFix //合并bugFix 分支到當(dāng)前分支
$ git rebase bugFix //合并bugFix 分支到當(dāng)前分支
區(qū)別:
rebase 一個(gè)兩個(gè)分支 就各位了一個(gè)分支,test合并前所有的 patch也就是commit消失了
merge 則還是兩個(gè)分支,只不過在merge后這個(gè)點(diǎn)交匯
圖解:


建議使用merge合并代碼
二,高級(jí)篇
1,分離HEAD
HEAD 是一個(gè)對(duì)當(dāng)前檢出記錄的符號(hào)引用 —— 也就是指向你正在其基礎(chǔ)上進(jìn)行工作的提交記錄。HEAD 總是指向當(dāng)前分支上最近一次提交記錄。大多數(shù)修改提交樹的 Git 命令都是從改變 HEAD 的指向開始的。HEAD 通常情況下是指向分支名的。
將其總結(jié)為以下幾種情況:
·如果臨時(shí)想基于某個(gè)commit做變更,試試新方案是否可行,就可以采用分離頭指針的方式。測(cè)試后發(fā)現(xiàn)新方案不成熟,直接reset回其他分支即可,省去了建、刪分支的麻煩,git會(huì)忽略管理這個(gè)沒有分支commit。
·進(jìn)行到某個(gè)commit,試試新方案,為該commit建立新的branch,可以進(jìn)行特性開發(fā)。
·分離頭指針對(duì)應(yīng)的commit,你如果認(rèn)為有用需要保留,那就用新分支把它保留。
·如果當(dāng)下無master分支,可以在分離頭指針狀態(tài)下,git checkout -b master HEAD
2,相對(duì)引用
通過哈希值指定提交記錄很不方便,所以 Git 引入了相對(duì)引用。使用相對(duì)引用的話,你就可以從一個(gè)易于記憶的地方(比如bugFix 分支或 HEAD)開始計(jì)算。
下面介紹兩個(gè)簡(jiǎn)單的用法:
· 使用 ^ 向上移動(dòng) 1 個(gè)提交記錄
· 使用 ~<num> 向上移動(dòng)多個(gè)提交記錄,如~3
<1>.操作符 (^)
把這個(gè)符號(hào)加在引用名稱的后面,表示讓 Git 尋找指定提交記錄的父提交。
所以 master^ 相當(dāng)于“master 的父節(jié)點(diǎn)”。
master^^ 是 master 的第二個(gè)父節(jié)點(diǎn)
演示操作:切換到 master 的父節(jié)點(diǎn)
$ git checkout master^

也可以將 HEAD 作為相對(duì)引用的參照。下面咱們就用 HEAD 在提交樹中向上移動(dòng)幾次。
我們可以一直使用 HEAD^ 向上移動(dòng)。
$ git checkout C3
$ git checkout HEAD^
$ git checkout HEAD^
$ git checkout HEAD^

<2>.操作符(~)
如果想在提交樹中向上移動(dòng)很多步的話,敲那么多 ^ 貌似也挺煩人的,Git 當(dāng)然也考慮到了這一點(diǎn),于是又引入了操作符 ~。
該操作符后面可以跟一個(gè)數(shù)字(可選,不跟數(shù)字時(shí)與 ^ 相同,向上移動(dòng)一次),指定向上移動(dòng)多少次。下面實(shí)際操作
用 ~<num> 一次后退四步,這種方法比前面的簡(jiǎn)單了不少。
$ git checkout HEAD~4

3,撤銷變更
主要有兩種方法用來撤銷變更 —— 一是 git reset,還有就是 git revert。
·git reset 通過把分支記錄回退幾個(gè)提交記錄來實(shí)現(xiàn)撤銷改動(dòng)。你可以將這想象成“改寫歷史”。git reset 向上移動(dòng)分支,原來指向的提交記錄就跟從來沒有提交過一樣。
$ git reset HEAD~1

Git 把 master 分支移回到 C1;現(xiàn)在我們的本地代碼庫根本就不知道有C2 這個(gè)提交了。
(譯者注:在reset后, C2 所做的變更還在,但是處于未加入暫存區(qū)狀態(tài)。)
·雖然在你的本地分支中使用 git reset 很方便,但是這種“改寫歷史”的方法對(duì)大家一起使用的遠(yuǎn)程分支是無效的哦!
為了撤銷更改并分享給別人,我們需要使用 git revert。
$ git revert HEAD

在我們要撤銷的提交記錄后面居然多了一個(gè)新提交!這是因?yàn)樾绿峤挥涗?C2' 引入了更改 —— 這些更改剛好是用來撤銷 C2 這個(gè)提交的。也就是說 C2' 的狀態(tài)與 C1 是相同的。
revert 之后就可以把你的更改推送到遠(yuǎn)程倉庫與別人分享啦。
三,移動(dòng)提交記錄
1,git cherry-pick <提交號(hào)>...
如果你想將一些提交復(fù)制到當(dāng)前所在的位置(HEAD)下面的話, Cherry-pick 是最直接的方式了。
$ git cherry-pick c2 c4

2,交互式的 rebase
當(dāng)你知道你所需要的提交記錄(并且還知道這些提交記錄的哈希值)時(shí), 用 cherry-pick 再好不過了 —— 沒有比這更簡(jiǎn)單的方式了。
但是如果你不清楚你想要的提交記錄的哈希值呢? 幸好 Git 幫你想到了這一點(diǎn), 我們可以利用交互式的 rebase —— 如果你想從一系列的提交記錄中找到想要的記錄, 這就是最好的方法了
交互式 rebase 指的是使用帶參數(shù) --interactive 的 rebase 命令, 簡(jiǎn)寫為 -i
如果你在命令后增加了這個(gè)選項(xiàng), Git 會(huì)打開一個(gè) UI 界面并列出將要被復(fù)制到目標(biāo)分支的備選提交記錄,它還會(huì)顯示每個(gè)提交記錄的哈希值和提交說明,提交說明有助于你理解這個(gè)提交進(jìn)行了哪些更改。
在實(shí)際使用時(shí),所謂的 UI 窗口一般會(huì)在文本編輯器 —— 如 Vim —— 中打開一個(gè)文件。
當(dāng) rebase UI界面打開時(shí), 你能做3件事:
調(diào)整提交記錄的順序(通過鼠標(biāo)拖放來完成)
刪除你不想要的提交(通過切換 pick 的狀態(tài)來完成,關(guān)閉就意味著你不想要這個(gè)提交記錄)
合并提交。 遺憾的是由于某種邏輯的原因,我們的課程不支持此功能,因此我不會(huì)詳細(xì)介紹這個(gè)操作。簡(jiǎn)而言之,它允許你把多個(gè)提交記錄合并成一個(gè)。
實(shí)例操作:



要通過本關(guān), 做一次交互式的 rebase,整理成目標(biāo)窗口中的提交順序。 記住,你隨時(shí)都可以用 undo、reset 修正錯(cuò)誤,這是不會(huì)記入步數(shù)的 ??

$ git rebase -i HEAD~4 //刪除c2,調(diào)整順序c3,c5,c4

四,雜項(xiàng)
1, 只取一個(gè)提交記錄
$ git cherry-pick c2 // 只需要其中一個(gè)代碼提交記錄
2,Git tag
git tag可以(在某種程度上 —— 因?yàn)闃?biāo)簽可以被刪除后重新在另外一個(gè)位置創(chuàng)建同名的標(biāo)簽)永久地將某個(gè)特定的提交命名為里程碑,然后就可以像分支一樣引用了。
更難得的是,它們并不會(huì)隨著新的提交而移動(dòng)。你也不能切換到某個(gè)標(biāo)簽上面進(jìn)行修改提交,它就像是提交樹上的一個(gè)錨點(diǎn),標(biāo)識(shí)了某個(gè)特定的位置。
$ git tag v1 c1

我們將這個(gè)標(biāo)簽命名為 v1,并且明確地讓它指向提交記錄 C1,如果你不指定提交記錄,Git 會(huì)用 HEAD 所指向的位置。
3,Git Describe
由于標(biāo)簽在代碼庫中起著“錨點(diǎn)”的作用,Git 還為此專門設(shè)計(jì)了一個(gè)命令用來描述離你最近的錨點(diǎn)(也就是標(biāo)簽),它就是 git describe!
Git Describe 能幫你在提交歷史中移動(dòng)了多次以后找到方向;當(dāng)你用 git bisect(一個(gè)查找產(chǎn)生 Bug 的提交記錄的指令)找到某個(gè)提交記錄時(shí),或者是當(dāng)你坐在你那剛剛度假回來的同事的電腦前時(shí), 可能會(huì)用到這個(gè)命令。
git describe 的語法是:
git describe <ref>
<ref> 可以是任何能被 Git 識(shí)別成提交記錄的引用,如果你沒有指定的話,Git 會(huì)以你目前所檢出的位置(HEAD)。
它輸出的結(jié)果是這樣的:
<tag>_<numCommits>_g<hash>
tag 表示的是離 ref 最近的標(biāo)簽, numCommits 是表示這個(gè) ref 與 tag 相差有多少個(gè)提交記錄, hash 表示的是你所給定的 ref 所表示的提交記錄哈希值的前幾位。
當(dāng) ref 提交記錄上有某個(gè)標(biāo)簽時(shí),則只輸出標(biāo)簽名稱

五,高級(jí)話題
1,多分支 rebase
咱們把這些分支 rebase 到 main 上,希望得到有序的提交歷史,也就是我們最終的結(jié)果應(yīng)該是 C6' 在 C7' 上面, C5' 在 C6' 上面,依此類推

2,兩個(gè)父節(jié)點(diǎn)




3 ,糾纏不清的分支
現(xiàn)在我們的 main 分支是比 one、two 和 three 要多幾個(gè)提交。出于某種原因,我們需要把 main 分支上最近的幾次提交做不同的調(diào)整后,分別添加到各個(gè)的分支上。
one 需要重新排序并刪除 C5,two 僅需要重排排序,而 three 只需要提交一次。


六,Push & Pull —— Git 遠(yuǎn)程倉庫!
1,git init & git clone
Git 使用 git init 命令來初始化一個(gè) Git 倉庫,Git 的很多命令都需要在 Git 的倉庫中運(yùn)行,所以 git init 是使用 Git 的第一個(gè)命令。
在執(zhí)行完成 git init 命令后,Git 倉庫會(huì)生成一個(gè) .git 目錄,該目錄包含了資源的所有元數(shù)據(jù),其他的項(xiàng)目目錄保持不變。
遠(yuǎn)程倉庫并不復(fù)雜, 在如今的云計(jì)算盛行的世界很容易把遠(yuǎn)程倉庫想象成一個(gè)富有魔力的東西, 但實(shí)際上它們只是你的倉庫在另個(gè)一臺(tái)計(jì)算機(jī)上的拷貝。你可以通過因特網(wǎng)與這臺(tái)計(jì)算機(jī)通信 —— 也就是增加或是獲取提交記錄
話雖如此, 遠(yuǎn)程倉庫卻有一系列強(qiáng)大的特性
首先也是最重要的的點(diǎn), 遠(yuǎn)程倉庫是一個(gè)強(qiáng)大的備份。本地倉庫也有恢復(fù)文件到指定版本的能力, 但所有的信息都是保存在本地的。有了遠(yuǎn)程倉庫以后,即使丟失了本地所有數(shù)據(jù), 你仍可以通過遠(yuǎn)程倉庫拿回你丟失的數(shù)據(jù)。
還有就是, 遠(yuǎn)程讓代碼社交化了! 既然你的項(xiàng)目被托管到別的地方了, 你的朋友可以更容易地為你的項(xiàng)目做貢獻(xiàn)(或者拉取最新的變更)

2,遠(yuǎn)程分支





3,Git Fetch
Git 遠(yuǎn)程倉庫相當(dāng)?shù)牟僮鲗?shí)際可以歸納為兩點(diǎn):向遠(yuǎn)程倉庫傳輸數(shù)據(jù)以及從遠(yuǎn)程倉庫獲取數(shù)據(jù)。既然我們能與遠(yuǎn)程倉庫同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代碼、文件、想法、情書等等)。
git fetch 完成了僅有的但是很重要的兩步:
<1>從遠(yuǎn)程倉庫下載本地倉庫中缺失的提交記錄
<2>更新遠(yuǎn)程分支指針(如 o/main)
git fetch 實(shí)際上將本地倉庫中的遠(yuǎn)程分支更新成了遠(yuǎn)程倉庫相應(yīng)分支最新的狀態(tài)。
如果你還記得上一節(jié)課程中我們說過的,遠(yuǎn)程分支反映了遠(yuǎn)程倉庫在你最后一次與它通信時(shí)的狀態(tài),git fetch 就是你與遠(yuǎn)程倉庫通信的方式了!希望我說的夠明白了,你已經(jīng)了解 git fetch 與遠(yuǎn)程分支之間的關(guān)系了吧。
git fetch 通常通過互聯(lián)網(wǎng)(使用 http:// 或 git:// 協(xié)議) 與遠(yuǎn)程倉庫通信。
git fetch 并不會(huì)改變你本地倉庫的狀態(tài)。它不會(huì)更新你的 main 分支,也不會(huì)修改你磁盤上的文件。
4,Git Pull
既然我們已經(jīng)知道了如何用 git fetch 獲取遠(yuǎn)程的數(shù)據(jù), 現(xiàn)在我們學(xué)習(xí)如何將這些變化更新到我們的工作當(dāng)中。
其實(shí)有很多方法的 —— 當(dāng)遠(yuǎn)程分支中有新的提交時(shí),你可以像合并本地分支那樣來合并遠(yuǎn)程分支。也就是說就是你可以執(zhí)行以下命令:
git cherry-pick o/main
git rebase o/main
git merge o/main
等等
實(shí)際上,由于先抓取更新再合并到本地分支這個(gè)流程很常用,因此 Git 提供了一個(gè)專門的命令來完成這兩個(gè)操作。它就是我們要講的 git pull。
git pull 就是 git fetch 和 git merge 的縮寫!
5,fakeTeamwork


6,Git Push
git push 負(fù)責(zé)將你的變更上傳到指定的遠(yuǎn)程倉庫,并在遠(yuǎn)程倉庫上合并你的新提交記錄。
注意 —— git push 不帶任何參數(shù)時(shí)的行為與 Git 的一個(gè)名為 push.default 的配置有關(guān)。它的默認(rèn)值取決于你正使用的 Git 的版本,但是在教程中我們使用的是 upstream。 這沒什么太大的影響,但是在你的項(xiàng)目中進(jìn)行推送之前,最好檢查一下這個(gè)配置。
7,偏離的工作
多個(gè)小伙伴同時(shí)開發(fā),因代碼提交順序?qū)е耮it push失敗


還有其它的方法可以在遠(yuǎn)程倉庫變更了以后更新我的工作嗎? 當(dāng)然有,我們還可以使用 merge
盡管 git merge 不會(huì)移動(dòng)你的工作(它會(huì)創(chuàng)建新的合并提交),但是它會(huì)告訴 Git 你已經(jīng)合并了遠(yuǎn)程倉庫的所有變更。這是因?yàn)檫h(yuǎn)程分支現(xiàn)在是你本地分支的祖先,也就是說你的提交已經(jīng)包含了遠(yuǎn)程分支的所有變化。

很好!但是要敲那么多命令,有沒有更簡(jiǎn)單一點(diǎn)的?
當(dāng)然 —— 前面已經(jīng)介紹過 git pull 就是 fetch 和 merge 的簡(jiǎn)寫,類似的 git pull --rebase 就是 fetch 和 rebase 的簡(jiǎn)寫!


8,遠(yuǎn)程服務(wù)器拒絕!(Remote Rejected)
如果你是在一個(gè)大的合作團(tuán)隊(duì)中工作, 很可能是main被鎖定了, 需要一些Pull Request流程來合并修改。如果你直接提交(commit)到本地main, 然后試圖推送(push)修改, 你將會(huì)收到這樣類似的信息:
! [遠(yuǎn)程服務(wù)器拒絕] main -> main (TF402455: 不允許推送(push)這個(gè)分支; 你必須使用pull request來更新這個(gè)分支.)
為什么會(huì)被拒絕?
遠(yuǎn)程服務(wù)器拒絕直接推送(push)提交到main, 因?yàn)椴呗耘渲靡?pull requests 來提交更新.
你應(yīng)該按照流程,新建一個(gè)分支, 推送(push)這個(gè)分支并申請(qǐng)pull request,但是你忘記并直接提交給了main.現(xiàn)在你卡住并且無法推送你的更新.
解決辦法
新建一個(gè)分支feature, 推送到遠(yuǎn)程服務(wù)器. 然后reset你的main分支和遠(yuǎn)程服務(wù)器保持一致, 否則下次你pull并且他人的提交和你沖突的時(shí)候就會(huì)有問題.

七,關(guān)于 origin 和它的周邊 —— Git 遠(yuǎn)程倉庫高級(jí)操作
1,合并特性分支

實(shí)例:
這里共有三個(gè)特性分支 —— side1 side2 和 side3
我需要將這三分支按順序推送到遠(yuǎn)程倉庫
因?yàn)檫h(yuǎn)程倉庫已經(jīng)被更新過了,所以我們還要把那些工作合并過來
圖示:


2,合并遠(yuǎn)程倉庫
rebase 的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):Rebase 使你的提交樹變得很干凈, 所有的提交都在一條線上
缺點(diǎn):Rebase 修改了提交樹的歷史
比如, 提交 C1 可以被 rebase 到 C3 之后。這看起來 C1 中的工作是在 C3 之后進(jìn)行的,但實(shí)際上是在 C3 之前。
一些開發(fā)人員喜歡保留提交歷史,因此更偏愛 merge。而其他人(比如我自己)可能更喜歡干凈的提交樹,于是偏愛 rebase。
3,遠(yuǎn)程跟蹤分支
pull 操作時(shí), 提交記錄會(huì)被先下載到 o/main 上,之后再合并到本地的 main 分支。隱含的合并目標(biāo)由這個(gè)關(guān)聯(lián)確定的。
push 操作時(shí), 我們把工作從 main 推到遠(yuǎn)程倉庫中的 main 分支(同時(shí)會(huì)更新遠(yuǎn)程分支 o/main) 。這個(gè)推送的目的地也是由這種關(guān)聯(lián)確定的!

你可以讓任意分支跟蹤 o/main, 然后該分支會(huì)像 main 分支一樣得到隱含的 push 目的地以及 merge 的目標(biāo)。 這意味著你可以在分支 totallyNotMain 上執(zhí)行 git push,將工作推送到遠(yuǎn)程倉庫的 main 分支上。
有兩種方法設(shè)置這個(gè)屬性:
第一種就是通過遠(yuǎn)程分支檢出一個(gè)新的分支,執(zhí)行:
git checkout -b totallyNotMain o/main
就可以創(chuàng)建一個(gè)名為 totallyNotMain 的分支,它跟蹤遠(yuǎn)程分支 o/main


另一種設(shè)置遠(yuǎn)程追蹤分支的方法就是使用:git branch -u 命令,執(zhí)行:
git branch -u o/main foo
這樣 foo 就會(huì)跟蹤 o/main 了。如果當(dāng)前就在 foo 分支上, 還可以省略 foo:
git branch -u o/main

4,Git Push 的參數(shù)
$ git push <remote> <place>



如果來源和去向分支的名稱不同呢?比如你想把本地的 foo 分支推送到遠(yuǎn)程倉庫中的 bar 分支



5,Git fetch 的參數(shù)
<place> 參數(shù)
如果你像如下命令這樣為 git fetch 設(shè)置 的話:
$ git fetch origin foo
Git 會(huì)到遠(yuǎn)程倉庫的 foo 分支上,然后獲取所有本地不存在的提交,放到本地的 o/foo 上。

你可能會(huì)好奇 —— 為何 Git 會(huì)將新提交放到 o/foo 而不是放到我本地的 foo 分支呢?之前不是說這樣的 參數(shù)就是同時(shí)應(yīng)用于本地和遠(yuǎn)程的位置嗎?
好吧, 本例中 Git 做了一些特殊處理,因?yàn)槟憧赡茉?foo 分支上的工作還未完成,你也不想弄亂它。還記得在 git fetch 課程里我們講到的嗎 —— 它不會(huì)更新你的本地的非遠(yuǎn)程分支, 只是下載提交記錄(這樣, 你就可以對(duì)遠(yuǎn)程分支進(jìn)行檢查或者合并了)。
“如果我們指定 <source>:<destination> 會(huì)發(fā)生什么呢?”
如果你覺得直接更新本地分支很爽,那你就用冒號(hào)分隔的 refspec 吧。不過,你不能在當(dāng)前檢出的分支上干這個(gè)事,但是其它分支是可以的。
這里有一點(diǎn)是需要注意的 —— source 現(xiàn)在指的是遠(yuǎn)程倉庫中的位置,而 <destination> 才是要放置提交的本地倉庫的位置。它與 git push 剛好相反,這是可以講的通的,因?yàn)槲覀冊(cè)谕喾吹姆较騻魉蛿?shù)據(jù)。



6,沒有source的source
Git 有兩種關(guān)于 <source> 的用法是比較詭異的,即你可以在 git push 或 git fetch 時(shí)不指定任何 source,方法就是僅保留冒號(hào)和 destination 部分,source 部分留空。
$ git push origin :side
$ git fetch origin :bugFix


7,Git pull 參數(shù)
$ git pull origin foo // 相當(dāng)于
$ git fetch origin foo;
$ git merge o/foo
$ git pull origin bar~1:bugFix // 相當(dāng)于
$ git fetch origin bar~1:bugFix;
$ git merge bugFix
看到了? git pull 實(shí)際上就是 fetch + merge 的縮寫, git pull 唯一關(guān)注的是提交最終合并到哪里(也就是為 git fetch 所提供的 destination 參數(shù))


