Git的理念

Git作為一個極其靈活的工具,

從修改單機遊戲數據文件的版本管理,

到多人協作一起堆屎的協作開發,

使用起來都是十分趁手。

那麼Git靈活的奧秘在哪呢?

大概是因為Git設計正交、實現紮實吧。

總覽

Git裡面的術語/命令很多,

但是它們可以歸併成幾個大類,

每個大類的概念都是正交的,

也就是說交叉概念很少,

不會有模糊的概念定義。

基於這樣的設計,

Git與之對應地實現了一套紮實的命令系統。

Git里的概念有些難以準確翻譯,

本文涉及概念詞的地方盡量用術語表達 。

比如經常用到的概念會有這些:

  • Line Diff
  • Commit
  • Branch
  • Repository
  • Remote

Line Diff

Git實現版本控制的方法是根據Line Diff,

推算出每個Commit具體改了哪些東西,

然後用多個Commit(實則是多份Line Diff)構建出所有歷史。

這個基於Line Diff的先天設計決定了Git的一些特性:

  1. 可以存儲所有歷史。

    我們常聽到「Git是一個分散式的版本控制系統」,

    這個指的就是Git不需要中心化的伺服器,

    你就可以做完所有操作。

    因為本地存著所有的Line Diff,

    所以「查看昨天被改過的文件名列表」這個操作完全可以離線完成。
  2. 對二進位文件不友善。

    二進位文件是沒法強行比Line Diff的。

    所以假如用Git管理二進位文件,

    Git只會顯示一個Binary File Differ

    再把上面一條「存儲所有歷史」給疊加上,

    就會出現今天提交了一個200M的文件,

    明天後天我都修改覆蓋了這個文件,

    最後整個目錄就有600M大了…

    (也就是說一般不用Git來管理二進位大文件)
  3. 能檢測文件重命名。

    假如在一個Commit中,

    從Line Diff的視角看,

    刪除的文件和增加的文件相似度很高,

    Git就會判定這是一個重命名的操作。

Commit

Line Diff組成了Commit,

Commit是大部分Git操作的最小單位。

這個詞既是動詞,也是名詞。

一個Commit包含了多種信息:

  • SHA hash:是根據line diff + 精確到秒的時間戳生成的一串唯一標識符
  • Author:寫Line Diff的人
  • Committer:一個隱藏的屬性,代表Commit的人
  • Date:包括AuthorDate和CommitDate
  • Message:Commit文本描述,Git會取Message第一行作為Subject,所以一般會遵循一定規範
  • Line Diffs:改動了哪些內容

這裡還可以說的概念包括RootCommit、MergeCommit,

不過它們特殊之處不影響實際使用,

所以跳過它們,繼續往下說。

Branch

多個Commit會組成一個Branch,

最初的Branch默認叫master(主幹分支)。

Branch和Commit在很多命令里都是可以作為等價的操作對象的。

舉個例子:

小成寫了一天代碼,

他在wechat這個分支上commit了很多次,

快下班了,小成想回顧一下今天的改動。

假設他的log長這樣子:

> git log --oneline --graph* f01c8d1 (HEAD -> wechat) refactor: improve project layout* 2f9c867 feat: add rest api to create card* 5d5242b feat: custom wechat card background* 873e6ca fix: wechat card slow query* 0dd06a9 fix: 500 when user unsubscribe* fb91f98 (origin/master, master) feat: implement wechat card* 176b4f0 feat: implement membership level* 2727226 migration: add Settings.enable_level...

那麼以下命令是完全等價的:

# 查看從master到wechat的diff> git diff master..wechat# 查看從master到當前的diff(HEAD代表當前位置,也就是wechat分支)> git diff master..HEAD# 查看從master到當前的diff(HEAD是默認值,可省略)> git diff master# 查看master的commit到當前的diff> git diff fb91f98# 查看五個Commit以前倒當前的diff(master分支在五個Commit以前)> git diff HEAD~5

模糊地理解的話,

就是「Branch是特殊的Commit」

(評論里有更詳實的解釋~)

理解了這一點以後,

再去看大部分的Git命令,

發現它們都是git <operation> <range> -- <files>...這樣的形式。

比如查看今天發布哪些內容就是git diff master..release

把某個文件回滾到200個Commit以前就是git checkout HEAD~200 -- some/path/some/file.txt

查看單個文件的改動歷史就是git log -- some/path/some/file.txt

Repository

Repository包含了所有的操作歷史。

git init命令可以初始化一個Repository。

一個Git Repository結構可能是這樣的:

- .git/ - hooks/ - objects/ - refs/ - HEAD - config- ForgiveDB/- README.md- requirements.txt

這裡的.git目錄就存儲著上面講的Line Diff、Commit、Branch的所有歷史,

就像上面二進位大文件的那個例子,

這裡可能存了幾百M的文件歷史。

Remote

Remote就是放在別的地方的Repository。

同一個Repository可以添加多個Remote。

除了push/pull/fetch這些基本操作以外,

關於Remote還有一個很騷的設定:

Git支持本地Remote。

比如樣例的命令如下:

# 假設在伺服器上的 /home/lirian/chinese-calendar 路徑下有一個 Repository> cd /home/lirian# 把它 clone 到某一個地方> git clone chinese-calendar /opt/git/repo/chinese-calendar --bare# 同個伺服器上的另一個用戶就可以 clone 這個 Repository> cd /home/ldsink && git clone file:///opt/git/repo/chinese-calendar && git remote -vorigin file:///opt/git/repo/chinese-calendar (fetch)origin file:///opt/git/repo/chinese-calendar (push)

這樣的設計之下,

Remote/Repository是完全分離的,

不會因為斷網就修改不了歷史。

我們甚至可以把Remote當成一種特殊的Branch,

比如fork - pull request就是這種模式的一種應用。

尾言

文中講到的不少例子有一些淺嘗輒止,

讀者有興趣的話可以嘗試思考實現一下這幾個拓展問題:

  • 關於Line Diff:改動的兩個文件相似度多高,Git才會識別為重命名呢?
  • 關於Commit:如何修改Commit的Author?GitHub上能看出來Committer么?
  • 關於Branch:如何刪除遠程分支?git stash產生的Commit可以像Branch一樣操作么?
  • 關於Repository:刪除分支以後,Git目錄會變小嗎?
  • 關於Remote:文中用到的--bare參數是什麼意思?

Git的設計理念中還有很強大的一部分是它關於歷史(History)的管理,

那又是一個值得細說的話題。

總的來說,筆者眼中Git是一個科學且強大的工具。

Git優秀的原因在於它:

  • 正交的設計:術語定義清晰,重疊概念少,表現張力強大。
  • 紮實的實現:二級術語豐富,命令參數完善,貼合實際應用場景。

(完)

原文鏈接,作者 @蘇子岳

本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。

推薦閱讀:

用Python對斗圖網站的分析以及抓取

TAG:Git | 軟體開發 | 計算機技術 |