日拱一卒,麻省理工教你學Git,工程師必備技能

語言: CN / TW / HK

大家好,日拱一卒,我是梁唐。

今天我們繼續聊聊麻省理工的missing smester,消失的學期,講述課堂上不會涉及,但又非常重要的知識和技能。

這一節課主要講的內容是git的基本原理以及常見命令,git對於工程師的重要性相信不用我多說,絕對是所有程式設計師必學的技能之一。屬於不一定要很精通,但至少得懂一點的領域之一。

B站影片連結

和之前一樣,這節課的note質量同樣非常高。

note筆記

本文是基於本節課note以及老師上課演示的內容,還有我個人的一些理解做的翻譯整理版本。日拱一卒,歡迎大家打卡一起學習。

前言

版本控制系統(VCS)是用來追蹤原始碼(或其他檔案、資料夾的集合)變更的工具。正如其名,這些工具幫助我們維護一個變更的歷史。不僅如此,還讓協同開發變得更加方便。VCS通過建立一系列快照的方式追蹤一個資料夾和它當中所有內容的變更,每個快照都包含了檔案/資料夾的完整的狀態。VCS同樣維護一些元資訊,比如誰建立了快照,每個快照的備註資訊等。

為什麼我們要用版本控制呢?即使你獨自工作,它也可以讓你檢視專案的歷史版本,維護改動的歷史,允許我們並行開發。當我們和其他人合作的時候,它更是無價之寶,因為我們可以看到其他人的修改,並且可以解決並行開發導致的衝突。

現代VCS同樣讓你能夠很輕易地回答下列問題:

  • 當前模組是誰編寫的?
  • 這個檔案的這一行是什麼時候被編輯的?是誰作出的修改?修改原因是什麼呢?
  • 最近的1000個版本中,何時/為什麼導致了單元測試失敗?

雖然還有其他的VCS,但事實上的標準是git。這裡有一篇關於git的漫畫,很有意思:

因為git的介面過於抽象(leaky abstraction),通過自頂向下的方式學習git充滿了困惑。很多時候只能死記硬背一些命令,像是魔法一樣使用它們。一旦遇到問題,就只能像是漫畫裡說的那樣去處理了。

儘管git的介面有些撿漏,它底層的設計和思想卻非常出彩。醜陋的介面只能死記硬背,而優秀的設計值得花時間理解。因此,我們將提供一個自底向上的對於git的解釋,從它的資料模型開始,然後再學習它的命令列介面。當資料模型被理解了之後,再理解命令以及它們是如何生成底層資料模型的就非常容易了。

Git 資料模型

進行版本控制的方法有很多,git擁有一個深思熟慮的模型是的它支援版本控制當中許多出彩的特性。比如維護歷史、支援分支以及允許協同合作。

Snapshots

git將歷史變更抽象成頂級目錄下的一系列檔案和資料夾的快照。在git術語中,檔案被稱為blob,會被視為是一系列位元組(byte)。一個資料夾被叫做tree,它儲存一系列blob和tree和名稱的對映(資料夾可以包含資料夾)。快照是被追蹤的最頂層的樹,比如一個樹看起來可能是這樣的:

頂層的樹包含兩個元素,一個叫做foo的tree(foo當中又包含一個叫做bar.txt的blob),和一個叫做baz.txt的blob。

歷史變更建模:關聯快照

版本控制系統是如何關聯快照的呢?一種簡單的方式是線性歷史。一個歷史變更是由一系列快照按照時間順序排列組成的。因為種種原因,git沒有使用這樣的模型。

在git當中,歷史變更是一個快照構成的DAG(有向無環圖)。這看起來似乎很高大上,但不用害怕只是一個很簡單的概念。這表明了git當中的每一個快照可能有多個父節點。注意,快照有多個父節點而非一個,因為某一個快照可能是由多個父節點生成的,比如由於合併了兩個並行開發的分支而建立的節點,就會有多個父節點。

git將這些快照叫做commit。將一個commit的歷史視覺化,看起來可能是這樣的:

上面是一個由ASCII構成的簡圖,o表示一個獨立的commit(快照)。箭頭指向了每個commit的父節點(箭頭方向是時間更早的方向)。第三個commit之後,歷史記錄分岔成了兩個不同的分治。這可能由於兩個獨立的特性被並行開發。未來這些分支會被合併成一個新的快照,它會包含所有的特性。新的提交會建立一個新的歷史記錄,看起來像是這樣,新建立的節點被加粗顯示:

git中的commit是不可修改的。這不意味著錯誤不能被修改,而是我們修改變更歷史實際上是建立的新的commit,而引用(參考下文)則被更新並指向這些新節點。

資料模型的虛擬碼展示

可能用虛擬碼的形式表示git中的資料模型更加清晰:

非常簡潔易懂

Objects 和內容定址

object可以是一個blob、tree或commit:

在git資料儲存當中,所有的objects都會基於它們SHA-1 雜湊之後的結果進行定址,SHA-1是一種雜湊演算法。會將傳入的結果對映成一個字串,演算法會盡可能保證對映之後的字串唯一。

blob、tree、commit就以這種方式被整合在了一起:它們都是object。當它們引用其他object時,它並沒有真正將這些值儲存下來,僅僅是引用了它們的hash值。

舉個例子,剛才例子當中的tree可以通過git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d來進行視覺化,看上去是這樣的:

樹本身會包含它當中內容的指標,baz.txt(blobl)和foo(tree)。如果我們檢視baz.txt這個hash值中對應的內容,命令git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85,我們可以看到如下結果:

引用

現在,所有的快照都可以通過它們的hash值來定位。這並不方便,因為對人類來說記住長度40的16進位制字串是非常不方便的。

git的做法是給這些hash值賦予人類能理解的名字,叫做引用。引用是指向commit的指標。和object不可變不同,引用是可變的,可以指向新的commit。比如,master是一個引用,通常用來指向主分支中最新的commit。

通過引用,git就使用了人類可讀的諸如master這樣的名字來指代歷史中的快照了。

一個細節是,我們經常想要知道我們當前所在的位置。這樣當我們建立新的快照時,我們就知道它關聯哪些快照。在git當中,我們現在所在的位置也是一個特殊的引用,叫做HEAD。

倉庫

最後,我們可以粗略地定義git倉庫了:資料object和引用。

在磁碟上,所有記錄都以object和引用的方式儲存:因為資料模型當中只有這兩個概念。所有的git命令都對應commit DAG上的一些操作,比如新增object,新增或更新引用。

當你輸入命令的時候,思考一下命令背後對於底層資料結構進行的操作。相反,如果你做出對commit DAG進行具體的修改,比如拋棄未提交的變更,或者是讓master引用指向5d83f9e commit,這通常都是有辦法的。(上述的例子當中,可以使用git checkout master; git reset --hard 5d83f9e

暫存區(Staging area)

git當中還包含一個和資料模型不相關的概念,但它是建立commit介面的一部分。

你可能覺得上面說的建立快照的命令類似於create snapshot,一些VCS的確是這樣,但git不是。我們希望乾淨的快照,每次都從當前狀態建立快照在一些情況並不理想。比如,想象一個場景,你開發完了兩個功能,你想要建立兩個不同的分支。第一個分支包含第一個功能,第二個分支包含第二個。或者,你在程式碼當中加入了一些debug資訊,在提交的時候你希望能不要帶上這些debug程式碼。

git處理這些場景的方式是使用一種叫做暫存區(staing area)的機制,它允許你指定下一次快照會包含的內容。

Git 命令列

為了避免重複資訊,我們將不會詳細解釋下面的命令。強烈推薦閱讀一下Pro Git這本書獲得更多資訊,或者觀看課程影片。

Basics

  • git help : 獲取 git 命令的幫助資訊
  • git init: 建立一個新的 git 倉庫,其資料會存放在一個名為 .git 的目錄下
  • git status: 顯示當前的倉庫狀態
  • git add : 新增檔案到暫存區
  • git commit: 建立一個新的提交

  • 為何要 編寫良好的提交資訊

  • git log: 顯示歷史日誌

  • git log --all --graph --decorate: 視覺化歷史記錄(有向無環圖)
  • git diff : 顯示與暫存區檔案的差異
  • git diff : 顯示某個檔案兩個版本之間的差異
  • git checkout : 更新 HEAD 和目前的分支

分支和合並

  • git branch: 顯示分支
  • git branch : 建立分支
  • git checkout -b : 建立分支並切換到該分支

  • 相當於 git branch ; git checkout

  • git merge : 合併到當前分支

  • git mergetool: 使用工具來處理合並衝突
  • git rebase: 將一系列補丁變基(rebase)為新的基線

遠端操作

  • git remote: 列出遠端
  • git remote add : 新增一個遠端
  • git push :: 將物件傳送至遠端並更新遠端引用
  • git branch --set-upstream-to=/: 建立本地和遠端分支的關聯關係
  • git fetch: 從遠端獲取物件/索引
  • git pull: 相當於 git fetch; git merge
  • git clone: 從遠端下載倉庫

撤銷

  • git commit --amend: 編輯提交的內容或資訊
  • git reset HEAD : 恢復暫存的檔案
  • git checkout -- : 丟棄修改
  • git restore: git2.32版本後取代git reset 進行許多撤銷操作

Git 高階操作

  • git config: Git 是一個 高度可定製的 工具
  • git clone --depth=1: 淺克隆(shallow clone),不包括完整的版本歷史資訊
  • git add -p: 互動式暫存
  • git rebase -i: 互動式變基
  • git blame: 檢視最後修改某行的人
  • git stash: 暫時移除工作目錄下的修改內容
  • git bisect: 通過二分查詢搜尋歷史記錄
  • .gitignore: 指定 故意不追蹤的檔案

雜項

  • 圖形使用者介面: Git 的 圖形使用者介面客戶端 有很多,但是我們自己並不使用這些圖形使用者介面的客戶端,我們選擇使用命令列介面
  • Shell 整合: 將 Git 狀態整合到您的 shell 中會非常方便。(zsh, bash)。Oh My Zsh這樣的框架中一般以及集成了這一功能
  • 編輯器整合: 和上面一條類似,將 Git 整合到編輯器中好處多多。fugitive.vim 是 Vim 中整合 GIt 的常用外掛
  • 工作流: 我們已經講解了資料模型與一些基礎命令,但還沒討論到進行大型專案時的一些慣例 ( 有很多 不同的 處理方法)
  • GitHub: Git 並不等同於 GitHub。 在 GitHub 中您需要使用一個被稱作拉取請求(pull request)的方法來向其他專案貢獻程式碼
  • 其他 Git 提供商: GitHub 並不是唯一的。還有像 GitLabBitBucket 這樣的平臺。

資源

  • Pro Git強烈推薦!學習前五章的內容可以教會您流暢使用 Git 的絕大多數技巧,因為您已經理解了 Git 的資料模型。後面的章節提供了很多有趣的高階主題。(Pro Git 中文版:http://git-scm.com/book/zh/v2);
  • Oh Shit, Git!?! :http://ohshitgit.com/,簡短的介紹瞭如何從 Git 錯誤中恢復;
  • Git for Computer Scientists :http://eagain.net/articles/git-for-computer-scientists/,簡短的介紹了 Git 的資料模型,與本文相比包含較少量的虛擬碼以及大量的精美圖片;
  • Git from the Bottom Up:http://jwiegley.github.io/git-from-the-bottom-up/詳細的介紹了 Git 的實現細節,而不僅僅侷限於資料模型。好奇的同學可以看看;
  • How to explain git in simple words:http://smusamashah.github.io/blog/2017/10/14/explain-git-in-simple-words
  • Learn Git Branching:http://learngitbranching.js.org/通過基於瀏覽器的遊戲來學習 Git ;

練習

  1. 如果您之前從來沒有用過 Git,推薦您閱讀 Pro Git 的前幾章,或者完成像 Learn Git Branching這樣的教程。重點關注 Git 命令和資料模型相關內容;
  2. Fork 本課程網站的倉庫:http://github.com/missing-semester/missing-semester

    1. 將版本歷史視覺化並進行探索
  3. 是誰最後修改了 README.md檔案?(提示:使用 git log 命令並新增合適的引數)
  4. 最後一次修改_config.yml 檔案中 collections: 行時的提交資訊是什麼?(提示:使用 git blame 和 git show)

  5. 使用 Git 時的一個常見錯誤是提交本不應該由 Git 管理的大檔案,或是將含有敏感資訊的檔案提交給 Git 。嘗試向倉庫中新增一個檔案並新增提交資訊,然後將其從歷史中刪除 ( 這篇文章也許會有幫助:http://help.github.com/articles/removing-sensitive-data-from-a-repository/);

  6. 從 GitHub 上克隆某個倉庫,修改一些檔案。當您使用 git stash 會發生什麼?當您執行 git log --all --oneline 時會顯示什麼?通過 git stash pop 命令來撤銷 git stash 操作,什麼時候會用到這一技巧?
  7. 與其他的命令列工具一樣,Git 也提供了一個名為 ~/.gitconfig 配置檔案 (或 dotfile)。請在 ~/.gitconfig 中建立一個別名,使您在執行 git graph 時,您可以得到 git log --all --graph --decorate --oneline 的輸出結果;
  8. 您可以通過執行 git config --global core.excludesfile ~/.gitignore_global 在 ~/.gitignore_global 中建立全域性忽略規則。配置您的全域性 gitignore 檔案來自動忽略系統或編輯器的臨時檔案,例如 .DS_Store;
  9. 克隆 本課程網站的倉庫:http://github.com/missing-semester/missing-semester,找找有沒有錯別字或其他可以改進的地方,在 GitHub 上發起拉取請求(Pull Request);