图解git原理与日常实用指南
緣起
讀了“扔物線”老師的小冊《Git 原理詳解及實用指南》感覺收獲良多,于是想寫點東西做一個總結,即加深自己的印象也希望能給社區小伙伴一點幫助,寫的不對的地方還請多多指導。身為一個初入前端半年的菜鳥,由伊始的只知道git是用來托管代碼的工具到逐步了解中央版本控制系統與分布式版本控制系統(git)的原理與區別;從之前只會基本的add、commit、pull、push操作到使用stash、merge、reset方便得不亦樂乎,都得益于對git原理的深入理解,逼話少說,咋們直接進入正題。前方長篇預警...
從了解版本控制系統開始
所謂版本控制,就是在文件修改的歷程中保留修改歷史,可以方便的撤銷(如同文本編輯的撤銷操作一般,只是版本控制會復雜的多)之前對文件的修改。一個版本控制系統的三個核心內容:版本控制(最基本的功能),主動提交(commit歷史)和遠程倉庫(協同開發)。
中央式版本控制系統(VCS)
工作模型
分布式版本控制系統(DVCS)
分布式與中央式的區別主要在于,分布式除了遠程倉庫之外團隊中每一個成員的機器上都有一份本地倉庫,每個人在自己的機器上就可以進行提交代碼,查看版本,切換分支等操作而不需要完全依賴網絡環境。
工作模型
分布式版本管理系統的優缺點:
優點
- 大多數操作本地進行,數度更快,不受網絡與物理位置限制,不聯網也可以提交代碼、查看歷史、切換分支等等
- 分布提交代碼,提交更細利于review
缺點
- 初次clone時間較長
- 本地占用存儲高于中央式系統
繼續深入git原理
假設你已經安裝好了git并將代碼clone到了本地,新手移步git安裝與代碼拷貝指南。
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(暫存區)
<blockquote> stage 這個詞在 Git 里,是「集中收集改動以待提交」的意思;而 staging area ,就是一個「匯集待提交的文件改動的地方」。簡稱「暫存」和「暫存區」。至于 staged 表示「已暫存」,就不用再解釋了吧?</blockquote>
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會失敗)
因為 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。
從圖中可以看出,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^^
^ 的用法:在 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原理詳解與實用指南》,認準扔物線。
總結
以上是生活随笔為你收集整理的图解git原理与日常实用指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Javascript代码优化的8个知识点
- 下一篇: leetcode 28. Impleme