圖解git原理與平常實用指南

緣起

讀了「扔物線」老師的小冊《Git 原理詳解及實用指南》感受收穫良多,因而想寫點東西作一個總結,即加深本身的印象也但願能給社區小夥伴一點幫助,寫的不對的地方還請多多指導。身爲一個初入前端半年的菜鳥,由伊始的只知道git是用來託管代碼的工具到逐步瞭解中央版本控制系統與分佈式版本控制系統(git)的原理與區別;從以前只會基本的add、commit、pull、push操做到使用stash、merge、reset方便得不亦樂乎,都得益於對git原理的深刻理解,逼話少說,咋們直接進入正題。前方長篇預警...前端

從瞭解版本控制系統開始

所謂版本控制,就是在文件修改的歷程中保留修改歷史,能夠方便的撤銷(如同文本編輯的撤銷操做通常,只是版本控制會複雜的多)以前對文件的修改。一個版本控制系統的三個核心內容:版本控制(最基本的功能),主動提交(commit歷史)和遠程倉庫(協同開發)。git

中央式版本控制系統(VCS)

工做模型安全

  1. 主工程師搭好項目框架
  2. 在公司服務器建立一個遠程倉庫,並提交代碼
  3. 其餘人拉取代碼,並行開發
  4. 每一個人獨立負責一個功能,開發完成提交代碼
  5. 其餘人隨時拉取代碼,保持同步

分佈式版本控制系統(DVCS)

分佈式與中央式的區別主要在於,分佈式除了遠程倉庫以外團隊中每個成員的機器上都有一份本地倉庫,每一個人在本身的機器上就能夠進行提交代碼,查看版本,切換分支等操做而不須要徹底依賴網絡環境。
工做模型服務器

  1. 主工程師搭好項目框架 ,並提交代碼到本地倉庫
  2. 在公司服務器建立一個遠程倉庫,並將1的提交推送到遠程倉庫
  3. 其餘人把遠程倉庫全部內容克隆到本地,擁有了各自的本地倉庫,開始並行開發
  4. 每一個人獨立負責一個功能,能夠把每個小改動提交到本地(因爲本地提交無需當即上傳到遠程倉庫,因此每一步提交沒必要是一個完整功能,而能夠是功能中的一個步驟或塊)
  5. 功能開發完畢,將和這個功能相關的全部提交從本地推送到遠程倉庫
  6. 每次當有人把新的提交推送到遠程倉庫的時候,其餘人就能夠選擇把這些提交同步到本身的機器上,並把它們和本身的本地代碼合併

分佈式版本管理系統的優缺點:

優勢

  • 大多數操做本地進行,數度更快,不受網絡與物理位置限制,不聯網也能夠提交代碼、查看歷史、切換分支等等
  • 分佈提交代碼,提交更細利於review

缺點

  • 初次clone時間較長
  • 本地佔用存儲高於中央式系統

繼續深刻git原理

假設你已經安裝好了git並將代碼clone到了本地,新手移步git安裝與代碼拷貝指南markdown

git最基本的工做模型

首先理解三個基本概念:
網絡

  • 工做區:就是你在電腦裏能看到的目錄
  • 版本庫:工做區有一個隱藏目錄.git,這個不算工做區,而是Git的本地版本庫,你的全部版本信息都會存在這裏
  • 暫存區:英文叫stage, 或index。通常存放在 ".git目錄下" 下的index文件(.git/index)中,因此咱們把暫存區有時也叫做索引(index)

工做模型
1.首先新建一個test.txt文件並對其進行修改,經過status能夠查看工做目錄當前狀態,此時test.txt對git來講是不存在的(Untracked)

2.而後經過add命令將修改放入暫存區(git開始追蹤它)

能夠看到,test.txt 的文字變成了綠色,它的前面多了「new file:」的標記,而它的描述也從 "Untracked files" 變成了 "Changes to be commited"。這些都說明一點:test.txt 這個文件的狀態從 "untracked"(未跟蹤)變成了 "staged"(已暫存),意思是這個文件中被改動的部分(也就是這整個文件)被記錄進了 staging area(暫存區)

stage 這個詞在 Git 裏,是「集中收集改動以待提交」的意思;而 staging area ,就是一個「聚集待提交的文件改動的地方」。簡稱「暫存」和「暫存區」。至於 staged 表示「已暫存」,就不用再解釋了吧?
3.如今文件已經放入暫存區,能夠用commit命令提交:

在這裏你也能夠直接commit提交會進入commit信息編輯頁面,而加上-m參數能夠快捷輸入簡短的提交備註信息,這樣你就完成了一次提交(能夠經過 git log查看提交歷史)

接着對該文件再次進行修改,輸入 git status能夠看到,該文件 又變紅了,不過此次它左邊的文字不是 "New file:" 而是 "modified:",並且上方顯示它的狀態也不是 "Untracked" 而是 "not staged for commit",意思很明確:Git 已經認識這個文件了,它不是個新文件,但它有了一些改動。因此雖然狀態的顯示有點不一樣,但處理方式仍是同樣的:
接下來再次將該文件add、commit,查看log能夠看到已經存在兩條提交記錄

4.最後經過push把本地的全部commit上傳到遠程倉庫:

團隊工做基本模型

工做模型
1.在上面基本操做的基礎上,同事 commit 代碼到他的本地,並 push 到遠程倉庫
2.你把遠程倉庫新的提交經過 pull指令拉取到你的本地

經過這個流程,你和同事就能夠簡單地合做了:你寫了代碼,commit,push 到遠程倉庫,而後他 pull 到他的本地;他再寫代碼,commit, push 到遠程倉庫,而後你再 pull 到你的本地。你來我往,配合得不亦樂乎。(可是有時候push會失敗)app

爲何會失敗?
由於 Git 的push 實際上是用本地倉庫的commit記錄去覆蓋遠程倉庫的commit記錄(注:這是簡化概念後的說法,push 的實質和這個說法略有不一樣),而若是在遠程倉庫含有本地沒有的commit的時候,push (若是成功)將會致使遠端的commit被擦掉。這種結果固然是不可行的,所以 Git 會在 push 的時候進行檢查,若是出現這樣的狀況,push 就會失敗框架

這時只須要先經過git pull(實爲fetch和merge的組合操做)將本地倉庫的提交和遠程倉庫的提交進行合併,而後再push就能夠了分佈式

Feature Branching:最流行的工做流

核心:
(1)任何新的功能(feature)或 bug 修復全都新建一個 branch 來寫;
(2)branch 寫完後,合併到 master,而後刪掉這個 branch(可以使用git origin -d 分支名刪除遠程倉庫的分支)。 工具

優點:
(1)代碼分享:寫完以後能夠在開發分支review以後再merge到master分支
(2)一人多任務:當正在開發接到更重要的新任務時,你只要稍微把目前未提交的代碼簡單收尾一下,而後作一個帶有「未完成」標記的提交(例如,在提交信息裏標上「TODO」),而後回到 master 去建立一個新的 branch 進行開發就行了。

HEAD、branch、引用的本質以及push的本質

HEAD:當前commit的引用

當前 commit 在哪裏,HEAD 就在哪裏,這是一個永遠自動指向當前 commit 的引用,因此你永遠能夠用 HEAD 來操做當前 commit,

branch:

HEAD 是 Git 中一個獨特的引用,它是惟一的。而除了 HEAD 以外,Git 還有一種引用,叫作 branch(分支)。HEAD 除了能夠指向 commit,還能夠指向一個branch,當指向一個branch時,HEAD會經過branch間接指向當前commit,HEAD移動會帶着branch一塊兒移動:

branch 包含了從初始 commit 到它的全部路徑,而不是一條路徑。而且,這些路徑之間也是彼此平等的。

像上圖這樣,master 在合併了 branch1 以後,從初始 commit 到 master 有了兩條路徑。這時,master 的串就包含了 1 2 3 4 7 和 1 2 5 6 7 這兩條路徑。並且,這兩條路徑是平等的,1 2 3 4 7 這條路徑並不會由於它是「原生路徑」而擁有任何的特別之處

建立branch: git branch 名稱
切換branch: git checkout 名稱(將HEAD指向該branch)
建立+切換: git checkout -b 名稱
在切換到新的 branch 後,再次 commit 時 HEAD 就會帶着新的 branch 移動了:
而這個時候,若是你再切換到 master 去 commit,就會真正地出現分叉了:
刪除branch: git branch -d 名稱
注意:
(1)HEAD 指向的 branch 不能刪除。若是要刪除 HEAD 指向的 branch,須要先用 checkout 把 HEAD 指向其餘地方。
(2)因爲 Git 中的 branch 只是一個引用,因此刪除 branch 的操做也只會刪掉這個引用,並不會刪除任何的 commit。(不過若是一個 commit 不在任何一個 branch 的「路徑」上,或者換句話說,若是沒有任何一個 branch 能夠回溯到這條 commit(也許能夠稱爲野生 commit?),那麼在必定時間後,它會被 Git 的回收機制刪除掉)
(3)出於安全考慮,沒有被合併到 master 過的 branch 在刪除時會失敗(怕誤刪未完成branch)把-d換成-D能夠強制刪除

引用的本質

所謂引用,其實就是一個個的字符串。這個字符串能夠是一個 commit 的 SHA-1 碼(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也能夠是一個 branch(例:ref: refs/heads/feature3)。

Git 中的 HEAD 和每個 branch 以及其餘的引用,都是以文本文件的形式存儲在本地倉庫 .git 目錄中,而 Git 在工做的時候,就是經過這些文本文件的內容來判斷這些所謂的「引用」是指向誰的。

push的本質:把 branch 上傳到遠程倉庫

(1)把當前branch位置上傳到遠程倉庫,並把它路徑上的commits一併上傳
(2)git中(2.0及之後版本),git push不加參數只能上傳到從遠程倉庫clone或者pull下來的分支,如需push在本地建立的分支則需使用git push origin 分支名的命令
(3)遠端倉庫的HEAD並不隨push與本地一致,遠端倉庫HEAD永遠指向默認分支(master),並隨之移動(可使用git br -r查看遠程分支的HEAD指向)。

開啓git操做之旅

merge:合併

含義:從目標 commit 和當前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目標 commit 的路徑上的全部 commit 的內容一併應用到當前 commit,而後自動生成一個新的 commit。

當執行 git merge branch1操做,Git 會把 5 和 6 這兩個 commit 的內容一併應用到 4 上,而後生成一個新的提交 7 。
merge的特殊狀況:
(1)merge衝突:你的兩個分支改了相同的內容,Git 不知道應該以哪一個爲準。若是在 merge 的時候發生了這種狀況,Git 就會把問題交給你來決定。具體地,它會告訴你 merge 失敗,以及失敗的緣由;這時候你只須要手動解決掉衝突並從新add、commit(改動不一樣文件或同一文件的不一樣行都不會產生衝突);或者使用 git merge --abort放棄解決衝突,取消merge
(2)HEAD 領先於目標 commit:merge是一個空操做:
此時merge不會有任何反應。
(3)HEAD 落後於 目標 commit且不存在分支(fast-forward):
git會直接把HEAD與其指向的branch(若是有的話)一塊兒移動到目標commit。

rebase:給commit序列從新設置基礎點

有些人不喜歡 merge,由於在 merge 以後,commit 歷史就會出現分叉,這種分叉再匯合的結構會讓有些人以爲混亂而難以管理。若是你不但願 commit 歷史出現分叉,能夠用 rebase 來代替 merge。

能夠看出,經過 rebase,5 和 6 兩條 commits 把基礎點從 2 換成了 4 。經過這樣的方式,就讓原本分叉了的提交歷史從新回到了一條線。這種「從新設置基礎點」的操做,就是 rebase 的含義。另外,在 rebase 以後,記得切回 master 再 merge 一下,把 master 移到最新的 commit。

爲何要從 branch1 來 rebase,而後再切回 master 再 merge 一下這麼麻煩,而不是直接在 master 上執行 rebase?

從圖中能夠看出,rebase 後的每一個 commit 雖然內容和 rebase 以前相同,但它們已是不一樣的 commit 了(每一個commit有惟一標誌)。若是直接從 master 執行 rebase 的話,就會是下面這樣:

這就致使 master 上以前的兩個最新 commit (3和4)被剔除了。若是這兩個 commit 以前已經在遠程倉庫存在,這就會致使無法 push :
因此,爲了不和遠程倉庫發生衝突,通常不要從 master 向其餘 branch 執行 rebase 操做。而若是是 master 之外的 branch 之間的 rebase(好比 branch1 和 branch2 之間),就沒必要這麼多費一步,直接 rebase 就好。

須要說明的是,rebase 是站在須要被 rebase 的 commit 上進行操做,這點和 merge 是不一樣的。

stash:臨時存放工做目錄的改動

stash 指令能夠幫你把工做目錄的內容所有放在你本地的一個獨立的地方,它不會被提交,也不會被刪除,你把東西放起來以後就能夠去作你的臨時工做了,作完之後再來取走,就能夠繼續以前手頭的事了。
操做步驟:
(1)git stash能夠加上save參數後面帶備註信息(git stash save '備註信息'
(2)此時工做目錄已經清空,能夠切換到其餘分支幹其餘事情了
(3)git stash pop彈出第一個stash(該stash從歷史stash中移除);或者使用git stash apply達到相同的效果(該stash仍存在stash list中),同時可使用git stash list查看stash歷史記錄並在apply後面加上指定的stash返回到該stash。
注意:沒有被track的文件會被git忽略而不被stash,若是想一塊兒stash,加上-u參數。

reflog:引用記錄的log

能夠查看git的引用記錄,不指定參數,默認顯示HEAD的引用記錄;若是不當心把分支刪掉了,可使用該命令查看引用記錄,而後使用checkout切到該記錄處重建分支便可。

注意:再也不被引用直接或間接指向的 commits 會在必定時間後被 Git 回收,因此使用 reflog 來找回被刪除的 branch 的操做必定要及時,否則有可能會因爲 commit 被回收而再也找不回來。

看看我都改了什麼

log:查看已提交內容

git log -p能夠查看每一個commit的改動細節(到改動文件的每一行)
git log --stat查看簡要統計(哪幾個文件改動了)
git show 指定commit 指定文件名查看指定commit的指定文件改動細節

diff:查看未提交內容

git diff --staged能夠顯示暫存區和上一條提交之間的不一樣。換句話說,這條指令可讓你看到「若是你當即輸入 git commit,你將會提交什麼」
git diff能夠顯示工做目錄和暫存區之間的不一樣。換句話說,這條指令可讓你看到「若是你如今把全部文件都 add,你會向暫存區中增長哪些內容」
git diff HEAD能夠顯示工做目錄和上一條提交之間的不一樣,它是上面這兩者的內容相加。換句話說,這條指令可讓你看到「若是你如今把全部文件都 add 而後 git commit,你將會提交什麼」(不過須要注意,沒有被 Git 記錄在案的文件(即歷來沒有被 add 過的文件,untracked files 並不會顯示出來。由於對 Git 來講它並不存在)實質上,若是你把 HEAD 換成別的commit,也能夠顯示當前工做目錄和這條 commit 的區別。

剛剛提交的代碼發現寫錯了怎麼辦?

再提一個修復了錯誤的commit?能夠是能夠,不過還有一個更加優雅和簡單的解決方法:commit --amend。
具體作法:
(1)修改好問題
(2)將修改add到暫存區
(3)使用git commit --amend提交修改,結果以下圖:

減小了一次無謂的commit。

錯誤不是最新的提交而是倒數第二個?

使用rebase -i(交互式rebase):
所謂「交互式 rebase」,就是在 rebase 的操做執行以前,你能夠指定要 rebase 的 commit 鏈中的每個 commit 是否須要進一步修改,那麼你就能夠利用這個特色,進行一次「原地 rebase」。

操做過程:
(1)git rebase -i HEAD^^

說明:在 Git 中,有兩個「偏移符號」: ^ 和 ~。
^ 的用法:在 commit 的後面加一個或多個 ^ 號,能夠把 commit 往回偏移,偏移的數量是 ^ 的數量。例如:master^ 表示 master 指向的 commit 以前的那個 commit; HEAD^^ 表示 HEAD 所指向的 commit 往前數兩個 commit。
~ 的用法:在 commit 的後面加上 ~ 號和一個數,能夠把 commit 往回偏移,偏移的數量是 ~ 號後面的數。例如:HEAD~5 表示 HEAD 指向的 commit往前數 5 個 commit。

上面這行代碼表示,把當前 commit ( HEAD 所指向的 commit) rebase 到 HEAD 以前 2 個的 commit 上:

(2)進入編輯頁面,選擇commit對應的操做,commit爲正序排列,舊的在上,新的在下,前面黃色的爲如何操做該commit,默認pick(直接應用該commit不作任何改變),修改第一個commit爲edit(應用這個 commit,而後停下來等待繼續修正)而後:wq退出編輯頁面,此時rebase停在第二個commit的位置,此時能夠對內容進行修改:
(3)修改完後使用add,commit --amend將修改提交
(4) git rebase --continue繼續 rebase 過程,把後面的 commit 直接應用上去,此次交互式 rebase 的過程就完美結束了,你的那個倒數第二個寫錯的 commit 就也被修正了:

想直接丟棄某次提交?

reset --hard 丟棄最新的提交

git reset --hard HEAD^

HEAD^ 表示 HEAD 往回數一個位置的 commit ,上節剛說過,記得吧?

用交互式 rebase 撤銷歷史提交

操做步驟與修改歷史提交相似,第二步把須要撤銷的commit修改成drop,其餘步驟再也不贅述。

用 rebase --onto 撤銷提交

git rebase --onto HEAD^^ HEAD^ branch1
上面這行代碼的意思是:以倒數第二個 commit 爲起點(起點不包含在 rebase 序列裏),branch1 爲終點,rebase 到倒數第三個 commit 上。

錯誤代碼已經push?

有的時候,代碼 push 到了遠程倉庫,才發現有個 commit 寫錯了。這種問題的處理分兩種狀況:

出錯內容在本身的分支

假如是某個你本身獨立開發的 branch 出錯了,不會影響到其餘人,那不要緊用前面幾節講的方法把寫錯的 commit 修改或者刪除掉,而後再 push 上去就行了。可是此時會push報錯,由於遠程倉庫包含本地沒有的 commits(在本地已經被替換或被刪除了),此時直接使用git push origin 分支名 -f強制push。

問題內容已合併到master

(1)增長新提交覆蓋以前內容
(2)使用git revert 指定commit 它的用法很簡單,你但願撤銷哪一個 commit,就把它填在後面。如:git revert HEAD^
上面這行代碼就會增長一條新的 commit,它的內容和倒數第二個 commit 是相反的,從而和倒數第二個 commit 相互抵消,達到撤銷的效果。在 revert 完成以後,把新的 commit 再 push 上去,這個 commit 的內容就被撤銷了。它和前面所介紹的撤銷方式相比,最主要的區別是,此次改動只是被「反轉」了,並無在歷史中消失掉,你的歷史中會存在兩條 commit :一個原始 commit ,一個對它的反轉 commit。

reset:不止能夠撤銷提交

git reset --hard 指定commit你的工做目錄裏的內容會被徹底重置爲和指定commit位置相同的內容。換句話說,就是你的未提交的修改會被所有擦掉。
git reset --soft 指定commit會在重置 HEAD 和 branch 時,保留工做目錄和暫存區中的內容,並把重置 HEAD 所帶來的新的差別放進暫存區。
什麼是「重置 HEAD 所帶來的新的差別」?就是這裏:

git reset --mixed(或者不加參數) 指定commit保留工做目錄,而且清空暫存區。也就是說,工做目錄的修改、暫存區的內容以及由 reset 所致使的新的文件差別,都會被放進工做區。簡而言之,就是「把全部差別都混合(mixed)放在工做區中」。

checkout:簽出指定commit

checkout的本質是簽出指定的commit,不止能夠切換branch還能夠指定commit做爲參數,把HEAD移動到指定的commit上;與reset的區別在於只移動HEAD不改變綁定的branch;git checkout --detach能夠把 HEAD 和 branch 脫離,直接指向當前 commit。

最後

但願個人總結能給你們帶來些許幫助,也但願和你們一塊兒學以至用,一塊兒成長。最後,萬分感謝扔老師的小冊,強勢安利《git原理詳解與實用指南》,認準扔物線。

相關文章
相關標籤/搜索