Git 之旅——不那麼深入理解 Git

初期形態

代碼管理的最早形態是使用類似現在百度同步盤的方式同步、管理代碼。但是代碼衝突問題時有發生,只能人工解決。

之後出現了改善工具:

  • diff:比較文本文件、目錄差異
  • patch:相當於 diff 反向操作
# 生成差異文件
diff -u hello1 hello2 > diff.txt

# 根據 hello1、diff 生成 hello2
cp hello1 hello3
patch hello3 < diff.txt

# 根據 hello2、diff 生成 hello1
cp hello2 hello4
patch -R hello4 < diff.txt

Linus 在 1991~2002 年使用 diff、patch 維護 Linux 代碼差異。

diff、patch 缺陷:無法處理二進制文件。Git 解決了此問題。

開山鼻祖 CVS Concurrent Versions System

CVS(ConcurrentVersionsSystem)誕生於1985年,是由荷蘭阿姆斯特丹 VU 大學的 DickGrune 教授實現的。當時 DickGrune 和兩個學生共同開發一個項目,但是三個人的工作時間無法協調到一起,迫切需要一個記錄和協同開發的工具軟件。於是 DickGrune 通過腳本語言對 RCS(Revision Control System)(一個針對單獨文件的版本管理工具)進行封裝,設計出有史以來第一個被大規模使用的版本控制工具。

—— 蔣鑫. Git權威指南 (Chinese Edition) (Kindle 位置 559-562). 機械工業出版社. Kindle 版本.

CVS 存在的問題:

  • 創建 tag、branch 效率低
  • tag、branch 分散在 RCS 文件中,不可見
  • 缺乏對合並的追蹤,會導致重複合併
  • 不支持原子提交,會導致提交數據不完整

CVS 成功地爲後來的版本控制系統確立了標準,像提交說明(commitlog)、檢入(checkin)、檢出(checkout)、里程碑(tag)、分支(branch)等概念在CVS中早就已經確立。

—— 蔣鑫. Git權威指南 (Chinese Edition) (Kindle 位置 578-579). 機械工業出版社.

SVN Subversion

SVN 優化了很多特性,如:

  • 實現了原子提交:SVN 不會像 CVS 那樣出現文件的部分內容被提交而其餘的內容沒有被提交的情況
  • 全局版本號:和 CVS 每個文件都擁有一個版本號相比,便捷許多
  • 文件輕拷貝:里程碑和分支創建速度加快很多

這些優化也使得它在版本控制工具中成爲最佳選擇之一。但 SVN 本質上是一種集中式版本管理工具,這種版本控制太依賴於服務器,如果服務器出現問題,版本控制將不可用;如果網絡較差,提交代碼將變得十分漫長。

除了以上集中式版本控制系統固有問題之外,再加上 SVN 本身設計的一些問題,使用其進行版本管理也並存在很多不如意之處。比如:

  • 項目文件在版本庫中必須按照一定的目錄結構進行部署,否則就可能無法建立里程碑和分支
  • 創建里程碑和分支會破壞精心設計的授權
  • 分支太隨意從而導致混亂。SVN 的分支創建非常隨意:可以基於 /trunk 目錄創建分支,也可以基於其他任何目錄創建分支,因此 SVN 很難畫出一個有意義的分支圖。再加上一次提交可以同時包含針對不同分支的文件變更,使得事情變得更糟

Git

BitKeeper 和 Git 由來

BitKeeper 是一種分佈式版本控制系統。分佈式版本控制系統優勢:不要中央版本庫,每個人都可以自己本地查看提交日誌、提交、創建里程碑和分支、合併分支、回退等操作,而不需要網絡連接。

分佈式版本控制系統和集中式版本控制系統區別粗略的講,就像是分封制和郡縣制的區別。

假設現在要書寫史書,集中式即郡縣制,郡縣制背後是強大的中央集權,相當於中央的史書絕對權威,各個郡縣對史書進行修改後必須上報中央才能生效。距離首都近的城市還好,提交修改十分便捷,如果海南要提交對史書的修改,得數月才能跑到首都,進行修改。要想拉取新的分支,寫寫野史,必須先上報中央,批准後才能進行。這裏的離首都的距離,就相當於網速,任何修改、提交、拉分支必須經過網絡和中央倉庫交互後才能進行。

分佈式則相當於春秋戰國時期,諸侯做大,諸侯每個人都有一份史書,都可以在自己的國家進行修改。任何修改的提交,甚至想要創建新的分支,寫寫野史,也無須上報周天子,在自家就可以完成。想同步到周天子時再派人去周天子那裏同步一下即可。

這就是集中式和分佈式版本控制系統的根本區別。

2005 年發生的一件事最終導致了 Git 的誕生。在 2005 年 4 月,AndrewTridgell(即大名鼎鼎的 Samba - Wikipedia 的作者)試圖對 BitKeeper 進行反向工程,以開發一個能與 BitKeeper 交互的開源工具。這激怒了 BitKeeper 軟件的所有者 BitMover 公司,要求收回對 Linux 社區免費使用 BitKeeper 的授權。迫不得已,Linus 選擇了自己開發一個分佈式版本控制工具以替代 BitKeeper。

—— 蔣鑫. Git權威指南 (Chinese Edition) (Kindle 位置 663-666). 機械工業出版社. Kindle 版本.

基本知識

  • 工作區 Workspace
  • 暫存區 Index/Stage
  • 倉庫 Repository
  • 遠程倉庫 Remote

目錄結構

mkdir demo
cd demo
git init
ls -l .git

# output
# 
# config
# description
# HEAD
# hooks/
# info/
# objects/
# refs/
  • description 文件僅供 GitWeb 程序使用,我們無需關心
  • config 文件包含項目特有的配置選項
  • info 目錄包含一個全局性排除(global exclude)文件, 用以放置那些不希望被記錄在 .gitignore 文件中的忽略模式(ignored patterns)
  • hooks 目錄包含客戶端或服務端的鉤子腳本(hook scripts)

剩下的四個文件很重要,是 Git 的核心組成部分:

  • HEAD 文件:指向目前被檢出的分支
  • index 文件:保存暫存區信息
  • objects 目錄:存儲所有數據內容
  • refs 目錄:存儲指向數據(分支 heads、遠程倉庫 remotes/origin 和標籤 tags 等)的提交對象的指針

接下來通過拆解 git addgit commitgit checkout 命令,結合 HEADrefs/heads/masterobjects/ 文件變化,探索一下 Git 的背後。

實踐操作

git checkout : HEAD

cat .git/HEAD
# output
# ref: refs/heads/master

git checkout -b test

cat .git/HEAD
# output
# ref: refs/heads/test

git add filename : 工作區文件添加到暫存區


# 生成 object
git hash-object -w filename

# 添加到暫存區
git update-index --add filename

# 上述兩個命令相當於
git add filename
# 查看暫存區
git ls-files --stage

# 上述命令相當於
git status

git commit : 將暫存區文件提交


# 保存當前目錄結構
git write-tree

# 保存快照 commit,-p 可指定父快照
echo "first commit" | git commit-tree 90f3b20385d2b20cf85477a65e4ef7e2eff71353 [-p id]

# git log 無內容,因爲當前 HEAD 沒有綁定到剛剛提交的快照
git log

# HEAD 對應 refs/head/master,將快照 id 放到該文件即可
echo 785f188674ef3c6ddc5b516307884e1d551f53ca > .git/refs/heads/master

git log

# 以上命令相當於
git commit -m "first commit"

總結

objects/ 中的對象類型

  • blob: git hash-object 生成的爲 blob object。命名取自文件的 SHA-1 值
  • tree: wirte-tree 生成的是 tree object。該對象將多個文件組織到一起
  • commit: commit-tree 生成的是 commit object。該對象表示一次 commit。

tree 對象相當於 Linux 中的目錄,blob 對象相當於 Linux 中的文件。tree 和 blob 關係如下圖:

refs/heads/master.git/objects 各個類型對象之間的關係:

  • 灰色爲 refs/heads/master 文件
  • 綠色爲 .git/objects/ 中的 commit object 文件
  • 紫色爲 .git/objects/ 中的 tree object 文件
  • 紅色爲 .git/objects/ 中的 blob object 文件

命令拆解與文件轉換關係

git checkout -b test

git checkout 切換分支,本質上是對 HEAD 文件的內容修改,令其指向 refs/ 中的不同文件。

git add

git add 分爲兩步:

  1. 將工作區中的文件轉爲 objects/ 中的 blob 對象,並以 SHA1 命名
  2. 將該對象的 SHA1 記錄到 index 文件中
git commit

git commit 分爲三步:

  1. 根據 index 中的記錄,生成 objects/ 中的 tree 對象
  2. 根據生成的 tree 對象創建 commit 對象
  3. 把 commit 對象的 SHA1 放入 refs/heads/master

HEADrefs/heads/masterobjects/ 關係

  • HEAD 指向 refs/ 中的文件
  • refs/ 中的文件都是存有某個 commit 對象的 id
  • commit 對象文件中存有 tree 對象的 id、提交作者、提交日誌
  • tree 對象中存有其下的 blob 對象、tree 對象 id 列表和對應文件名
  • blob 對象中則存有對應文件的內容

問題

Git 是保存差異嗎?

經過實踐操作和搜索發現,Git 每次修改文件都會生成一份完整的 blob 文件,而非保存差異。只是會在和遠程倉庫交互的時候,會進行壓縮和差異處理來決定上傳差異文件還是完整的文件。

就是說平常 Git 存儲的完整文件——鬆散 loose 對象格式,但是 Git 會時不時對這些文件進行打包,刪除原始文件,當向遠程服務器推送的時候也會執行這個操作。自己可以執行 git gc 主動觸發這一操作。

詳細解答可以看:Git 內部原理 - 包文件

談談學習

  1. 以終爲始:任何學的東西都得落到產出
  2. 實踐:要想掌握一種方法,實際上是擁有對應的強大的神經元通路,其實就是通過神經元通路不斷沖刷來形成的。正如魯迅先生所說:世界上本來是沒有路的,走的多了才產生了路。神經元通路也是如此
    1. 多種形式學習:畫圖、寫文字梳理邏輯、講出來、看視頻
      1. 主動學習
      2. 被動學習


    2. 刻意練習:練習中感覺不舒服,正是建立新的神經通路的時候
  3. 反饋
  4. 總結、聯想:神經連接越多,記憶越深刻

練車:每個動作都是刺激新的通路產生,並且實踐的時候反饋非常及時,比如說倒車入庫,壓線就是壓線了,對就是,錯就是錯,能夠馬上知道行爲的正確與否,並做出糾正動作的決策。

重構:不僅要讀重構之法,也要親手寫代碼去實踐。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章