Git簡單原理

weixin_34253539發表於2018-04-01

版本控制

在我的學生時代,我最常遇到的就是程式碼版本管理問題,每完成一版程式碼,就會存一個資料夾,然後做修改,如果某個時候忘記了,恰恰又改出了問題,簡直是一場噩夢。

3979148-3f361660460838f7.png
image.png
本地版本控制工具

相信有不少人遇到了與我相似的困境,於是出現了本地版本控制工具。例如rcs就是一個本地版本控制工具,它的工作原理基本上就是儲存並管理檔案補丁(patch)。檔案補丁是一種特定格式的文字檔案,記錄著對應檔案修訂前後的內容變化。所以,根據每次修訂後的補丁,rcs 可以通過不斷打補丁,計算出各個版本的檔案內容。

集中化的版本控制系統

本地版本控制工具可以解決那些個人的文件、檔案的版本管理,但是對於團隊協作的工作還是有很大的侷限性,一個團隊中的成員沒法進行程式碼或者文件的快速共享和整體的版本管理。由此出現了集中化的版本控制系統( Centralized Version Control Systems,簡稱 CVCS )。這類系統,諸如 CVS,Subversion 以及 Perforce 等,都有一個單一的集中管理的伺服器,儲存所有檔案的修訂版本,而協同工作的人們都通過客戶端連到這臺伺服器,取出最新的檔案或者提交更新。

這類的版本控制系統和本地的版本控制工具都有一個缺陷:容錯率低。如果儲存檔案的中央伺服器出現資料丟失、磁碟損壞、備份不及時等問題的時候,歷史更改紀錄就有可能丟失。如果中央伺服器出現當機等故障,那麼在這段時間內,誰都無法提交更新,也就無法協同工作。只要整個專案的歷史記錄被儲存在單一位置,就有丟失所有歷史更新記錄的風險。這個時候,被客戶端偶然提取出來的儲存在本地的某些快照資料就成了恢復資料的希望。由此出現了分散式版本控制系統。

分散式版本控制系統

分散式版本控制系統就是為了解決由於專案歷史記錄被儲存在單一位置而可能引起的丟失所有歷史更新記錄問題而出現的。

分散式版本控制系統中,像 Git,Mercurial,Bazaar 以及 Darcs 等,客戶端並不只提取最新版本的檔案快照,而是把程式碼倉庫完整地映象下來。這麼一來,任何一處協同工作用的伺服器發生故障,事後都可以用任何一個映象出來的本地倉庫恢復。因為每一次的提取操作,實際上都是一次對程式碼倉庫的完整備份。

更進一步,許多這類系統都可以指定和若干不同的遠端程式碼倉庫進行互動。籍此,你就可以在同一個專案中,分別和不同工作小組的人相互協作。你可以根據需要設定不同的協作流程,比如層次模型式的工作流,而這在以前的集中式系統中是無法實現的。

Git的誕生

Git是由Linux開源社群進行開發和維護的。由於Linux 核心開源專案有著為數眾廣的參與者,在1991年~2002年間,絕大多數的 Linux 核心維護工作都花在了提交補丁和儲存歸檔的繁瑣事務上。

到2002年,整個專案組開始啟用分散式版本控制系統 BitKeeper 來管理和維護程式碼。

一直到2005年,開發 BitKeeper 的商業公司同 Linux 核心開源社群的合作關係結束,他們收回了免費使用 BitKeeper 的權力。這就迫使 Linux 開源社群(特別是 Linux 的締造者 Linus Torvalds )不得不吸取教訓,只有開發一套屬於自己的版本控制系統才不至於重蹈覆轍。由此,Git在2005年正式誕生。

3979148-49c9990df7031f59.png
image.png

Git的管理方式

在很多版本管理系統中,通過記錄各個檔案的具體差異來進行版本管理,像CVS,Subversion,Perforce,Bazaar 等都是這樣進行版本管理。

3979148-78f47f4e4f5fb4fb.png
image.png

然而Git 並不儲存這些前後變化的差異資料。實際上,Git 更像是把變化的檔案作快照後,記錄在一個微型的檔案系統中。每次提交更新時,它會縱覽一遍所有檔案的指紋資訊並對檔案作一快照,然後儲存一個指向這次快照的索引。為提高效能,若檔案沒有變化,Git 不會再次儲存,而只對上次儲存的快照作一連結。

3979148-b093c90978675353.png
image.png

這也是Git進行版本回退,分支切換等快速的原因,只用切換到相應的快照即可,不需要做其他的操作。

Git本地工作流程

對於任何一個檔案,在 Git 內都只有三種狀態:已修改(modified)、已暫存(staged)和 已提交(committed)。已提交表示該檔案已經被安全地儲存在本地資料庫中了;已修改表示修改了某個檔案,但還沒有提交儲存;已暫存表示把已修改的檔案放在下次提交時要儲存的清單中。

3979148-9009affde6fb3be6.png
image.png

如圖所示,基本的 Git 工作流程如下:
在工作目錄中修改某些檔案。
對修改後的檔案進行快照,然後儲存到暫存區域。
提交更新,將儲存在暫存區域的檔案快照永久轉儲到 Git 目錄中。

關於分支

前面已經提到Git的每一次提交是儲存一個類似於快照的東西,有一個指向這個快找的索引。而分支可以理解為一個指標,如圖所示。master分支是一個指標,這個指標會隨著提交的增加而跟著進行後移。

3979148-83dbbebbf911af41.png
image.png

在圖中,只有快照A的時候,master指標指向98ca9,當有新的提交出現,出現快照B的時候,master指標跟著進行後移,指向34ac2。同理,當出現快照C時,就如圖上的狀態所示。這個便是Git分支實現方式。

當使用 $ git branch <branchName> 命令建立一個新的分支的時候就如下圖所示,出現了一個新的分支指標。圖中出現的 HEAD 也是一個指標,表示當前所在的位置。由於 branch 命令只是建立分支,並沒有切換到該分支上,因此 HEAD 指標指向 master 上。

$ git branch testing

3979148-260b8c34554ce627.png
image.png

如果使用 $ git checkout <branchName> 命令,則 HEAD 指標酒會指向對應的分支指標上。

$ git checkout testing

3979148-299d227215f48103.png
image.png

如果在有兩條分支的情況下進行提交,那麼就會出現幾個分支指標指向不同快照索引的情況。如圖所示。

3979148-1a1794149671e809.png
image.png

如果在此時切換分支,就會發現只有 HEAD 指標發生了改變。

$ git checkout master

3979148-13f0b0d5266e2cfc.png
image.png

如果再繼續在master的分支上修改,就會出現分叉的情況。

3979148-0bf153876bb01d54.png
image.png
Merge

瞭解完分支的實現方式,就可以更好的理解 $ git merge 這條命令。如圖所示,當想要將hotfix分支合併到master分支上時,hotfix 和 master的指標同時指向同一個快照。此時就可以刪掉hotfix分支。由於git的分支實現方式是使用指標的方式,因此刪除分支和建立分支都是一件非常快速的事情,只需要刪除該指標就可以了。

3979148-3a74afa3888e030b.png
image.png

而對於並不只是移動一下指標,而是需要處理程式碼合併/衝突的情況,如下圖。Git會在合併程式碼或衝突處理之後建立一個新的快照,即圖中的C6,使master分支指向這個新的快照。

3979148-8a28b655ef06f926.png
image.png
幫助理解一些異常情況

瞭解一下Git分支的實現方式可以很好的幫助我們理解一些似乎看起來很詭異的情況。今天我在給別人講git的時候就遇到了這樣一種情況。他的命令是這樣的:

$ git init
$ git:(master) git branch test
$ git:(master) git checkout test
$ git:(test) git add .
$ git:(test) git commit -m "first commit"
$ git:(test) git checkout master

最後一條命令他是希望可以回到 master 分支,但是一直在提示他找不到這個分支。乍一看就是很奇怪,最開始是有master分支的,而且他並沒有對master分支執行任何刪除操作,但是這個分支就莫名其妙的找不到了。

那麼通過Git分支的實現方式想一下,在 git init 之後,這個初始化了一個本地倉庫,並且有一個master分支,但是此時並沒有任何提交,因此此時沒有任何快照產生,這個master指標也就是一個沒有指向的指標。

之後直接建立了test分支,並且在test分支上建立了快照,test指標指向了這個快照,那麼想要切回master分支的時候,就會發現對於一個沒有快照的指標,沒法正常的切換回去,因為這個指標沒有依託(在初始化倉庫後會有一個.git資料夾,會記錄git的資訊,可以找到有專門儲存分支的檔案,裡邊是儲存了指向的索引的),所以可以理解為這個指標就已經丟了。

Git的好處

  • 近乎所有操作都是本地執行

    在 Git 中的絕大多數操作都只需要訪問本地檔案和資源,不用連網。但如果用 CVCS 的話,差不多所有操作都需要連線網路。因為 Git 在本地磁碟上就儲存著所有當前專案的歷史更新,所以處理起來速度飛快。

    舉個例子,如果要瀏覽專案的歷史更新摘要,Git 不用跑到外面的伺服器上去取資料回來,而直接從本地資料庫讀取後展示給你看。所以任何時候你都可以馬上翻閱,無需等待。如果想要看當前版本的檔案和一個月前的版本之間有何差異,Git 會取出一個月前的快照和當前檔案作一次差異運算,而不用請求遠端伺服器來做這件事,或是把老版本的檔案拉到本地來作比較。
    用 CVCS 的話,沒有網路或者斷開 VPN 你就無法做任何事情。但用 Git 的話,就算你在飛機或者火車上,都可以非常愉快地頻繁提交更新,等到了有網路的時候再上傳到遠端倉庫。同樣,在回家的路上,不用連線 VPN 你也可以繼續工作。換作其他版本控制系統,這麼做幾乎不可能,抑或非常麻煩。比如 Perforce,如果不連到伺服器,幾乎什麼都做不了(譯註:預設無法發出命令 p4 edit file 開始編輯檔案,因為 Perforce 需要聯網通知系統宣告該檔案正在被誰修訂。但實際上手工修改檔案許可權可以繞過這個限制,只是完成後還是無法提交更新。);如果是 Subversion 或 CVS,雖然可以編輯檔案,但無法提交更新,因為資料庫在網路上。看上去好像這些都不是什麼大問題,但實際體驗過之後,你就會驚喜地發現,這其實是會帶來很大不同的。

  • 時刻保持資料完整性

    在儲存到 Git 之前,所有資料都要進行內容的校驗和(checksum)計算,並將此結果作為資料的唯一標識和索引。換句話說,不可能在你修改了檔案或目錄之後,Git 一無所知。這項特性作為 Git 的設計哲學,建在整體架構的最底層。所以如果檔案在傳輸時變得不完整,或者磁碟損壞導致檔案資料缺失,Git 都能立即察覺。
    Git 使用 SHA-1 演算法計算資料的校驗和,通過對檔案的內容或目錄的結構計算出一個 SHA-1 雜湊值,作為指紋字串。該字串由 40 個十六進位制字元(0-9 及 a-f)組成,看起來就像是:

    24b9da6552252987aa493b52f8696cd6d3b00373

    Git 的工作完全依賴於這類指紋字串,所以你會經常看到這樣的雜湊值。實際上,所有儲存在 Git 資料庫中的東西都是用此雜湊值來作索引的,而不是靠檔名。

Git常用命令

  • Config:
    • 配置的你個人的使用者名稱稱和電子郵件地址
      • $git config --global user.name "John Doe”
      • $ git config --global user.email johndoe@example.com
    • 配置差異分析工具
      • $ git config --global merge.tool vimdiff (使用vimdiff)
    • 配置使用的文字編輯器
      • git config --global core.editor emacs # 預設會使用作業系統指定的預設編輯器,一般可能會是 Vi 或者 Vim
    • 檢查已有的配置資訊
      • git config –list
    • 直接查閱某個環境變數的設定
      • $ git config user.name
  • 查詢用法:
    • $ git help<verb> ($ git help config)
    • $ git<verb>--help
    • $ man git-<verb>
  • 新增倉庫
    • Init: 初始化本地倉庫
    • Clone:從現有倉庫克隆
      • $ git clone git://github.com/schacon/grit.git
      • $ git clone git://github.com/schacon/grit.git mygrit #重新命名資料夾名
  • 一些常用命令
    • Status: 要確定哪些檔案當前處於什麼狀態
    • Add :
      • 開始跟蹤一個新檔案
      • 暫存已修改檔案
      • 合併時把有衝突的檔案標記為已解決狀態等
    • .gitignore 檔案:忽略檔案
    • Diff: 檢視區別
    • Log:檢視提交
  • 撤銷操作:
    • $ git commit –amend # 修改最後一次提交
    • $ git reset HEAD<file>... # 取消已經暫存的檔案
    • $ git checkout --<file>... # 取消對檔案的修改
  • 遠端倉庫:
    • $ git remote –v # 顯示對應的克隆地址
    • $ git remote add [shortname] [url] # 新增一個新的遠端倉庫
    • $ git fetch [remote-name] # 到遠端倉庫中拉取所有你本地倉庫中還沒有的資料
    • $ git pull # 自動抓取資料下來,然後將遠端分支自動合併到本地倉庫中當前分支
    • $ git push [remote-name] [branch-name] # 將本地倉庫中的資料推送到遠端倉庫

使用Git進行團隊協作--持續整合

提到git和敏捷,那就不得不提到持續整合。持續整合是指頻繁地(一天多次)將程式碼整合到主幹。一般主張是一天至少要提交1次以上。

持續整合的好處:

  • 快速發現錯誤: 每完成一點更新,就整合到主幹,可以快速發現錯誤,定位錯誤也比較容易。

  • 防止分支大幅偏離主幹:如果不是經常整合,主幹又在不斷更新,會導致以後整合的難度變大,甚至難以整合。

3979148-8f81d51c64f9e94c.png
image.png

上圖就是一個很好的例子,兩個分支都只是拉程式碼,而沒有將自己的程式碼合併上去,因此要處理的衝突還不算多,當其中一個分支團隊將改動很大的檔案進行提交之後,另一個分支團隊幾乎可以崩潰了。舉個不太恰當的例子,有兩個人最開始拿到的都是圓,兩個人沒有溝通,很有可能一個人就做成了一個蘋果,另一個人做成了玫瑰。在無法頻繁溝通的情況下,頻繁讓別人看到自己做的時候,並且拿到了自己做到一半的東西繼續做,從某種意義上來說也是一種溝通,而且能夠方便兩個人一點一點的達成統一。

參考資料 & 本文圖片來源: git官網教程 https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5

相關文章