30分鐘讓你掌握Git的黑魔法

技術瑣話發表於2019-05-27

 

30分鐘讓你掌握Git的黑魔法

本文轉載自雲效公眾號

在Git Rev News #48期的LightReading 中有一篇文章寫的不錯,不僅乾貨滿滿而且還附帶了操作影片。其中的內容不僅覆蓋了很多git使用上的基礎知識,也從使用角度上解答了很多剛接觸git的開發者的疑問。為了便於讀者理解,我在翻譯的同時也新增了一些內容。以下為正文部分。

**注:本文內容較長,建議收藏慢慢學習。

擔憂

很多人怕使用git,我個人覺得主要可能是兩部分的原因:

 

  • 沒接觸過:平時接觸的程式碼還託管在SVN或CVS等工具上。

  • 不太熟悉:可能對git的使用還不太熟悉和全面,導致了在使用git時步步為營。

Never Be Afraid To Try Something New.

程式碼對於開發者是勞作成果的結晶,對於公司而言是核心資產,有一些擔憂也是正常的。但git也並沒有我們想象中的那麼複雜,需要讓我們每次使用都心有餘悸,其實我們只需要稍微花一點時間嘗試多多瞭解它,在很多時候你會發現,非但git不會讓你產生擔憂,而且會讓自己的交付過程更加高效。

Version Control

談及git就不得不提到版本控制,我們不妨先來看下版本控制是做什麼的,這將有助於後續對git的理解。

當你在工作中面對的是一些經常變化的文件、程式碼等交付物的時候,考慮如何去追蹤和記錄這些changes就變得非常重要,原因可能是:

  • 對於頻繁改動和改進的交付物,非常有必要去記錄下每次變更的內容,每次記錄的內容匯成了一段修改的歷史,有了歷史我們才知道我們曾經做了什麼

  • 記錄的歷史中必須要包含一些重要的資訊,這樣追溯才變得有意義,比如:

    Who: 是誰執行的變更?

    When:什麼時候做出的變更?

    What:這次變更做了什麼事情?

  • 最好可以支援撤銷變更,不讓某一個提交的嚴重問題,去汙染整個提交歷史。

版本控制系統(VCS: Version Control System),正會為你提供這種記錄和追溯變更的能力。

30分鐘讓你掌握Git的黑魔法

 

大多數的VCS支援在多個使用者之間共享變更的提交歷史,這從實質上讓團隊協同變為了可能,簡單說來就是:

  •  你可以看到我的變更提交。

  • 我也可以看到你的變更提交。

  • 如果雙方都進行了變更提交,也可以以某種方式方法進行比對和合並,最終作出統一的變更版本。

VCS歷經多年的發展,目前業界中有許多VCS工具可供我們選擇。在本文中,我們將會針對目前最流行的 git 來介紹。

git是黑魔法麼?

剛接觸git時,git確實有讓人覺得有點像黑魔法一樣神秘,但是又有哪個技術不是這樣呢?當我們瞭解其基本的資料結構結構後,會發現git從使用角度來講其實並不複雜,我們甚至可以更進一步的學習git的一些優良的軟體設計理論,從中獲益。首先,讓我們先從commit說起。

git object commit

  • 提交物件(git commit object): 每一個提交在git中都透過git commit object儲存,物件具有一個全域性唯一的名稱,叫做 revision hash。它的名字是由 SHA-1 演算法生成,形如"998622294a6c520db718867354bf98348ae3c7e2",我們通常會取其縮寫方便使用,如"9986222"。

  • 物件構成: commit物件包含了author + commit message 的基本資訊。

  • 物件儲存:git commit object儲存一次變更提交內的所有變更內容,而不是增量變化的資料delta(很多人都理解錯了這一點),所以git對於每次改動儲存的都是全部狀態的資料。

  • 大物件儲存:因對於大檔案的修改和儲存,同樣也是儲存全部狀態的資料,所以可能會影響git使用時的效能(glfs可以改進這一點)。

  • 提交樹: 多個commit物件會組成一個提交樹,它讓我們可以輕鬆的追溯commit的歷史,也能對比樹上commit與commit之間的變更差異。

git commit 練習

讓我們透過實戰來幫助理解,第一步我們來初始化一個repository(git倉庫),預設初始化之後倉庫是空的,其中既沒有儲存任何文字內容也沒有附帶任何提交:

$ git init hackers$ cd hackers$ git status

第二步,讓我們來看下執行過後git給出的輸出內容,它會指引我們進行進一步的瞭解:

➜  hackers git:(master) git status
On branch master
No commits yet
nothing to commit (create/copy files anduse "git add" to track)

output 1: On branch master

  • 對於剛剛建立空倉庫來說,master是我們的預設分支,一個git倉庫下可以有很多分支(branches),具體某一個分支的命名可以完全由你自己決定,通常會起便於理解的名字,如果用hash號的話肯定不是一個好主意。

  • branches是一種引用(ref),他們指向了一個確定的commit hash號,這樣我們就可以明確我們的分支當前的內容

  • 除了branches引用以外,還有一種引用叫做tags,相信大家也不會陌生。

  • master通常被我們更加熟知,因為大多數的分支開發模式都是用master來指向“最新”的commit。

  • On branch master代表著我們當前是在master分支下操作,所以每次當我們在提交新的commit時,git會自動將master指向我們新的commit,當工作在其他分支上時,同理。

  • 有一個很特殊的ref名稱叫做"HEAD",它指向我們當前正在操作的branches或tags(正常工作時),其命名上非常容易理解,表示當前的引用狀態。

  • 透過 git branch(或 gittag) 命令你可以靈活的操作和修改branches或tags。

output 2: No commits yet

  • 對於空倉庫來說,目前我們還沒有進行任意的提交

nothing to commit (create/copy files anduse "git add" to track)

output中提示我們需要使用 gitadd 命令,說到這裡就必須要提到暫存或索引(stage),那麼如何去理解暫存呢?

一個檔案從改動到提交到git倉庫,需要經歷三個狀態:

  • 工作區:工作區指的是我們本地工作的目錄,比如我們可以在剛才建立的hackers目錄下新增一個readme檔案,readme檔案這時只是本地檔案系統上的修改,還未儲存到git。

  • 暫存(索引)區: 暫存實際上是將我們本地檔案系統的改動轉化為git的物件儲存的過程

  • 倉庫:git commit後將提交物件儲存到git倉庫

30分鐘讓你掌握Git的黑魔法


git add 的幫助文件中很詳細的解釋了暫存這一過程:

DESCRIPTION

This command updates the index using thecurrent content found in the

working tree, to prepare the content stagedfor the next commit.

( git add命令將更新暫存區,為接下來的提交做準備)

It typically adds the current content ofexisting paths as a whole, but

with some options it can also be used toadd content with only part of

the changes made to the working tree filesapplied, or remove paths

that do not exist in the working tree anymore.

The "index" holds a snapshot ofthe content of the working tree, and it

is this snapshot that is taken as thecontents of the next commit.

(暫存區的index儲存的是改動的完整檔案和目錄的快照(非delta))


Thus after making any changes to theworking tree, and before running the

commit command, you must use the addcommand to add any new or modified

 files to the index.

暫存是我們將改動提交到git倉庫之前必須經歷的狀態

對git暫存有一定了解後,其相關操作的使用其實也非常簡單,簡要的說明如下:

1、暫存區操作

  • 透過 git add 命令將改動暫存

  • 可以使用 git add -p 來依次暫存每一個檔案改動,過程中我們可以靈活選擇檔案中的變更內容,從而決定哪些改動暫存

  • 如果 git add 不會暫存被ignore 的檔案改動

  • 透過 git rm 命令,我們可以刪除檔案的同時將其從暫存區中剔除

2、暫存區修正

  • 透過 git reset 命令進行修正,可以先將暫存區的內容清空,在使用 git add -p 命令對改動review和暫存

  • 這個過程不會對你的檔案進行任何修改操作,只是git會認為目前沒有改動需要被提交

  •  如果我們想分階段(or 分檔案)進行reset,可以使用 git reset FILE or git reset -p 命令

3、暫存區狀態

  • 可以用 git diff --staged 依次檢查暫存區內每一個檔案的修改

  • 用 git diff 檢視剩餘的還未暫存內容的修改

4、Just Commit!

  • 當你對需要修改的內容和範圍滿意時,你就可以將暫存區的內容進行commit了,命令為: git commit

  • 如果你覺得需要把所有當前工作空間的修改全部commit,可以執行 git commit -a ,這相當於先執行 git add後執行 git commit,將暫存和提交的指令合二為一,這對於一些開發者來說是很高效的,但是如果提交過大這樣做通常不合適。

  • 我們建議一個提交中只做一件事,這在符合單一職責的同時,也可以讓我們明確的知道每一個commit中做了一件什麼事情而不是多個事情。所以通常我們的使用習慣都是執行 git add -p 來review我們將要暫存內容是否合理?是否需要更細的拆分提交?這些優秀的工程實踐,將會讓程式碼庫中的commits更加優雅☕️

ok,我們已經在不知不覺中瞭解了很多內容,我們來回顧下,它們包括了:

  • commit包含的資訊?

  • commit是如何表示的?

  • 暫存區是什麼?如何全部新增、一次新增、刪除、查詢和修正?

  • 如何將暫存區的改動內容commit?

  • 不要做大提交,一個提交只做一件事

附帶的,在瞭解commit過程中我們知道了從本地改動到提交到git倉庫,經歷的幾個關鍵的狀態:

  • 工作區(Working Directory)

  • 暫存區(Index)

  • Git倉庫(Git Repo)

下圖為上述過程中各個狀態的轉換過程:

  • 本地改動檔案時,此時還僅僅是工作區內的改動

  • 當執行 git add 之後,工作區內的改動被索引在暫存區

  • 當執行 git commit 之後,暫存區的內容物件將會儲存在git倉庫中,並執行更新HEAD指向等後續操作,這樣就完成了引用與提交、提交與改動快照的一一對應了。

30分鐘讓你掌握Git的黑魔法


正是因為git本身對於這幾個區域(狀態)的設計,為git在本地開發過程帶來了靈活的管理空間。我們可以根據自己的情況,自由的選擇哪些改動暫存、哪些暫存的改動可以commit、commit可以關聯到那個引用,從而進一步與其他人進行協同。

提交之後

我們已經有了一個commit,現在我們可以圍繞commit做更多有趣的事情:

  • 檢視commit歷史: git log(or git log --oneline)

  • 在commit中檢視改動的diff:git log -p

  • 檢視ref與提交的關聯關係,如當前master指向的commit: git show master

  • 檢出覆蓋: git checkout NAME(如果NAME是一個具體的提交雜湊值時,git會認為狀態時“detached(分離的)”,因為gitcheckout過程中重要的一步是將HEAD指向那個分支的最後一次commit。所以如果這樣做,將意味著沒有分支在引用此提交,所以若我們這時候進行提交的話,沒有人會知道它們的存在)

  • 使用 git revert NAME 來對commit進行反轉操作。

  • 使用 git diff NAME.. 將舊版本與當前版本進行比較,檢視diff

  • 使用 git log NAME, 檢視指定區間的提交

  • 使用 git reset NAME 進行提交重置操作

  • 使用 git reset --hard NAME:將所有檔案的狀態強制重置為NAME的狀態,使用上需要小心

引用基本操作

引用(refs)包含兩種分別是branches 和 tags, 我們接下來簡單介紹下相關操作:

  

  • git branch b 命令可以讓我們建立一個名稱為 b 的分支

  • 當我們建立了一個 b 分支後,這也相當於意味著 b 的指向就是 HEAD 對應的commit

  • 我們可以先在 b 分支上建立一個新的commitA ,然後假如切回 master 分支上,這時再提交了一個新的commitB,那麼 master 和 HEAD 將會指向了新的commit __B,而b分支指向的還是原來的commit A

  • git checkout b 可以切換到b分支上,切換後新的提交都會在b分支上,理所應當

  • git checkout master 切換回master後,b分支的提交也不會帶回master上,分支隔離

分支上提交隔離的設計,可以讓我們非常輕鬆的切換我們的修改,非常方便的做各類測試

tags 的名稱不會改變,而且它們有自己的描述資訊(比如可以作為release note以及標記釋出的版本號等)。

做好你的提交

可能很多人的提交歷史是長這個樣子的:

commit 14add feature x – maybe even witha commit message about x!
commit 13: forgot to add file
commit 12: fix bug 
commit 11: typo
commit 10: typo2
commit 9: actually fix
commit 8: actually actually fix
commit 7: tests pass
commit 6: fix example code
commit 5: typo
commit 4: x
commit 3: x
commit 2: x
commit 1: x

單就git而言,這看上去是沒有問題而且合法的,但對於那些對你修改感興趣的人(很可能是未來的你!),這樣的提交在資訊在追溯歷史時可能並沒有多大幫助。但是如果你的提交已經長成這個樣子,我們該怎麼辦?

沒關係,git有辦法可以彌補這一些:

git commit --amend 

我們可以將新的改動提交到當前最近的提交上,比如你發現少改了什麼,但是又不想多出一個提交時會很有用。

如果我們認為我們的提交資訊寫的並不好,我要修改修改,這也是一種辦法,但是並不是最好的辦法。

這個操作會更改先前的提交,併為其提供新的hash值。

git rebase -i HEAD~13 

這個命令非常強大,可以說是git提交管理的神器,此命令含義是我們可以針對之前的13次的提交在VI環境中進行重新修改設計:

  • 操作選項 p 意味著保持原樣什麼都不做,我們可以透過vim中編輯提交的順序,使其在提交樹上生效

  • 操作選項 r: 我們可以修改提交資訊,這種方式比commit --amend要好的多,因為不會新生成一個commit

  • 操作選項 e: 我們可以修改commit,比如新增或者刪除某些檔案改動

  • 操作選項 s: 我們可以將這個提交與其上一次的提交進行合併,並重新編輯提交資訊

  • 操作選項 f: f代表著"fixup"。例如我們如果想針對之前一個老的提交進行fixup,又不想做一次新的提交破壞提交樹的歷史的邏輯含義,可以採用這種方式,這種處理方式非常優雅

關於git

版本控制的一個常見功能是允許多個人對一組檔案進行更改,而不會互相影響。或者更確切地說,為了確保如果他們不會踩到彼此的腳趾,不會在提交程式碼到服務端時偷偷的覆蓋彼此的變化。

在git中我們如何保證這一點呢?

git與svn不同,git不存在本地檔案存在lock的情況,這是一種避免出現寫作問題的方式,但是並不方便,而git與svn最大的不同在於它是一個分散式VCS,這意味著:

  • 每個人都有整個儲存庫的本地副本(其中不僅包含了自己的,也包含了其他人的提交到倉庫的所有內容)。

  • 一些VCS是集中式的(例如,svn):伺服器具有所有提交,而客戶端只有他們“已檢出”的檔案。所以基本上在本地我們只有當前檔案,每次涉及本地不存在的檔案操作時,都需要訪問服務端進行進一步互動。

  • 每一個本地副本都可以當作服務端對外提供git服務

  • 我們可以用git push推送本地內容到任意我們有許可權的git遠端倉庫

  • 不管是集團的force、github、gitlab等工具,其實本質上都是提供的git倉庫儲存的相關服務,在這一點上其實並沒有特別之處,針對git本身和其協議上是透明的。


30分鐘讓你掌握Git的黑魔法

svn,圖片出自git-scm

 

30分鐘讓你掌握Git的黑魔法

git,圖片出自git-scm

 

 git衝突解決

衝突的產生幾乎是不可避免的,當衝突產生時你需要將一個分支中的更改與另一個分支中的更改合併,對應git的命令為 git merge NAME ,一般過程如下:

  • 找到HEAD和NAME的一個共同祖先(common base)

  • 嘗試將這些NAME到共同祖先之間的修改合併到HEAD上

  • 新建立一個merge commit物件,包含所有的這些變更內容

  • HEAD指向這個新的mergecommit

git將會保證這個過程改動不會丟失,另外一個命令你可能會比較熟悉,那就是 git pull 命令,git pull 命令實際上包含了 git merge 的過程,具體過程為:

  • git fetch REMOTE

  • git merge REMOTE/BRANCH

  • 和 git push一樣,有的時候需要先設定 "tracking"(-u) ,這樣可以將本地和遠端的分支一一對應。

如果每次merge都如此順利,那肯定是非常完美的,但有時候你會發現在合併時產生了衝突檔案,這時候也不用擔心,如何處理衝突的簡要介紹如下:

  • 衝突只是因為git不清楚你最終要合併後的文字是什麼樣子,這是很正常的情況

  • 產生衝突時,git會中斷合併操作,並指導你解決好所有的衝突檔案

  • 開啟你的衝突檔案,找到 <<<<<<< ,這是你需要開始處理衝突的地方,然後找到=======,等號上面的內容是HEAD到共同祖先之間的改動,等號下面是NAME到共同祖先之間的改動。用 git mergetool 通常是比較好的選擇,當然現在大多數IDE都整合了不錯的衝突解決工具

  • 當你把衝突全部解決完畢,請用 git add . 來暫存這些改動吧

  • 最後進行git commit,如果你想放棄當前修改重新解決可以使用 git merge --abort ,非常方便

  • 當你完成了以上這些艱鉅的任務,最後 git push 吧!

push失敗?

排除掉遠端的git服務存在問題以外,我們push失敗的大多數原因都是因為我們在工作的內容其他人也在工作的關係。

Git是這樣判斷的:

1、會判斷REMOTE的當前commit是不是你當前正在pushing commit的祖先。

2、如果是的話,代表你的提交是相對比較新的,push是可以成功的(fast-forwarding)

3、否則push失敗並提示你其他人已經在你push之前執行更新(push is rejected)。

當發生push is rejected 後我們的幾個處理方法如下:

  • 使用git pull合併遠端的最新更改(git pull相當於 git fetch + git merge) 

  • 使用 --force 強制推送本地變化到遠端飲用進行覆蓋,需要注意的是 這種覆蓋操作可能會丟失其他人的提交內容

  • 可以使用 --force-with-lease 引數,這樣只有遠端的ref自上次從fetch後沒有改變時才會強制進行更改,否則reject the push,這樣的操作更安全,是一種非常推薦使用的方式。

  • 如果rebase操作了本地的一些提交,而這些提交之前已經push過了的話,你可能需要進行force push了,可以想象看為什麼?

本文只是選取部分Git基本命令進行介紹,目的是拋磚引玉,讓大家對git有一個基本的認識。當我們深入挖掘Git時,你會發現它本身有著如此多優秀的設計理念,值得我們學習和探究。

不要讓Git成為你認知領域的黑魔法,而是讓Git成為你掌握的魔法。

原文:

譯者:滕龍(花名澳明),阿里巴巴研發效能部技術專家

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562044/viewspace-2645757/,如需轉載,請註明出處,否則將追究法律責任。

相關文章