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的一些特性:
- 可以存儲所有歷史。我們常聽到「Git是一個分散式的版本控制系統」,這個指的就是Git不需要中心化的伺服器,你就可以做完所有操作。因為本地存著所有的Line Diff,所以「查看昨天被改過的文件名列表」這個操作完全可以離線完成。
- 對二進位文件不友善。
二進位文件是沒法強行比Line Diff的。
所以假如用Git管理二進位文件,Git只會顯示一個Binary File Differ
。再把上面一條「存儲所有歷史」給疊加上,就會出現今天提交了一個200M的文件,明天後天我都修改覆蓋了這個文件,最後整個目錄就有600M大了…(也就是說一般不用Git來管理二進位大文件) - 能檢測文件重命名。假如在一個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
,
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
這些基本操作以外,
比如樣例的命令如下:
# 假設在伺服器上的 /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優秀的原因在於它:- 正交的設計:術語定義清晰,重疊概念少,表現張力強大。
- 紮實的實現:二級術語豐富,命令參數完善,貼合實際應用場景。
(完)
原文鏈接,作者 @蘇子岳
本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。
推薦閱讀: