作者 | 梁唐
出品 | 公眾號(hào):Coder梁(ID:Coder_LT)
大家好,日拱一卒,我是梁唐。
今天我們繼續(xù)聊聊麻省理工的missing smester,消失的學(xué)期,講述課堂上不會(huì)涉及,但又非常重要的知識(shí)和技能。
這一節(jié)課主要講的內(nèi)容是git的基本原理以及常見命令,git對(duì)于工程師的重要性相信不用我多說,絕對(duì)是所有程序員必學(xué)的技能之一。屬于不一定要很精通,但至少得懂一點(diǎn)的領(lǐng)域之一。
B站視頻鏈接:https://www.bilibili.com/video/BV1x7411H7wa?p=6
和之前一樣,這節(jié)課的note質(zhì)量同樣非常高。大家可以點(diǎn)擊「閱讀原文」跳轉(zhuǎn)英文原文。
本文是基于本節(jié)課note以及老師上課演示的內(nèi)容,還有我個(gè)人的一些理解做的翻譯整理版本。日拱一卒,歡迎大家打卡一起學(xué)習(xí)。
版本控制系統(tǒng)(VCS)是用來追蹤源代碼(或其他文件、文件夾的集合)變更的工具。正如其名,這些工具幫助我們維護(hù)一個(gè)變更的歷史。不僅如此,還讓協(xié)同開發(fā)變得更加方便。VCS通過創(chuàng)建一系列快照的方式追蹤一個(gè)文件夾和它當(dāng)中所有內(nèi)容的變更,每個(gè)快照都包含了文件/文件夾的完整的狀態(tài)。VCS同樣維護(hù)一些元信息,比如誰創(chuàng)建了快照,每個(gè)快照的備注信息等。
為什么我們要用版本控制呢?即使你獨(dú)自工作,它也可以讓你查看項(xiàng)目的歷史版本,維護(hù)改動(dòng)的歷史,允許我們并行開發(fā)。當(dāng)我們和其他人合作的時(shí)候,它更是無價(jià)之寶,因?yàn)槲覀兛梢钥吹狡渌说男薷?,并且可以解決并行開發(fā)導(dǎo)致的沖突。
現(xiàn)代VCS同樣讓你能夠很輕易地回答下列問題:
雖然還有其他的VCS,但事實(shí)上的標(biāo)準(zhǔn)是git。這里有一篇關(guān)于git的漫畫,很有意思:
因?yàn)間it的界面過于抽象(leaky abstraction),通過自頂向下的方式學(xué)習(xí)git充滿了困惑。很多時(shí)候只能死記硬背一些命令,像是魔法一樣使用它們。一旦遇到問題,就只能像是漫畫里說的那樣去處理了。
盡管git的界面有些簡陋,它底層的設(shè)計(jì)和思想?yún)s非常出彩。丑陋的接口只能死記硬背,而優(yōu)秀的設(shè)計(jì)值得花時(shí)間理解。因此,我們將提供一個(gè)自底向上的對(duì)于git的解釋,從它的數(shù)據(jù)模型開始,然后再學(xué)習(xí)它的命令行接口。當(dāng)數(shù)據(jù)模型被理解了之后,再理解命令以及它們是如何生成底層數(shù)據(jù)模型的就非常容易了。
進(jìn)行版本控制的方法有很多,git擁有一個(gè)深思熟慮的模型是的它支持版本控制當(dāng)中許多出彩的特性。比如維護(hù)歷史、支持分支以及允許協(xié)同合作。
git將歷史變更抽象成頂級(jí)目錄下的一系列文件和文件夾的快照。在git術(shù)語中,文件被稱為blob,會(huì)被視為是一系列字節(jié)(byte)。一個(gè)文件夾被叫做tree,它存儲(chǔ)一系列blob和tree和名稱的映射(文件夾可以包含文件夾)??煺帐潜蛔粉櫟淖铐攲拥臉?,比如一個(gè)樹看起來可能是這樣的:
頂層的樹包含兩個(gè)元素,一個(gè)叫做foo的tree(foo當(dāng)中又包含一個(gè)叫做bar.txt的blob),和一個(gè)叫做baz.txt的blob。
版本控制系統(tǒng)是如何關(guān)聯(lián)快照的呢?一種簡單的方式是線性歷史。一個(gè)歷史變更是由一系列快照按照時(shí)間順序排列組成的。因?yàn)榉N種原因,git沒有使用這樣的模型。
在git當(dāng)中,歷史變更是一個(gè)快照構(gòu)成的DAG(有向無環(huán)圖)。這看起來似乎很高大上,但不用害怕只是一個(gè)很簡單的概念。這表明了git當(dāng)中的每一個(gè)快照可能有多個(gè)父節(jié)點(diǎn)。注意,快照有多個(gè)父節(jié)點(diǎn)而非一個(gè),因?yàn)槟骋粋€(gè)快照可能是由多個(gè)父節(jié)點(diǎn)生成的,比如由于合并了兩個(gè)并行開發(fā)的分支而創(chuàng)建的節(jié)點(diǎn),就會(huì)有多個(gè)父節(jié)點(diǎn)。
git將這些快照叫做commit。將一個(gè)commit的歷史可視化,看起來可能是這樣的:
上面是一個(gè)由ASCII構(gòu)成的簡圖,o表示一個(gè)獨(dú)立的commit(快照)。箭頭指向了每個(gè)commit的父節(jié)點(diǎn)(箭頭方向是時(shí)間更早的方向)。第三個(gè)commit之后,歷史記錄分岔成了兩個(gè)不同的分支。這可能由于兩個(gè)獨(dú)立的特性被并行開發(fā)。未來這些分支會(huì)被合并成一個(gè)新的快照,它會(huì)包含所有的特性。新的提交會(huì)創(chuàng)建一個(gè)新的歷史記錄,看起來像是這樣,新創(chuàng)建的節(jié)點(diǎn)被加粗顯示:
git中的commit是不可修改的。這不意味著錯(cuò)誤不能被修改,而是我們修改變更歷史實(shí)際上是創(chuàng)建的新的commit,而引用(參考下文)則被更新并指向這些新節(jié)點(diǎn)。
可能用偽代碼的形式表示git中的數(shù)據(jù)模型更加清晰:
非常簡潔易懂
object可以是一個(gè)blob、tree或commit:
在git數(shù)據(jù)存儲(chǔ)當(dāng)中,所有的objects都會(huì)基于它們SHA-1 哈希之后的結(jié)果進(jìn)行尋址,SHA-1是一種哈希算法。會(huì)將傳入的結(jié)果映射成一個(gè)字符串,算法會(huì)盡可能保證映射之后的字符串唯一。
blob、tree、commit就以這種方式被整合在了一起:它們都是object。當(dāng)它們引用其他object時(shí),它并沒有真正將這些值存儲(chǔ)下來,僅僅是引用了它們的hash值。
舉個(gè)例子,剛才例子當(dāng)中的tree可以通過git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
來進(jìn)行可視化,看上去是這樣的:
樹本身會(huì)包含它當(dāng)中內(nèi)容的指針,baz.txt(blobl)和foo(tree)。如果我們查看baz.txt這個(gè)hash值中對(duì)應(yīng)的內(nèi)容,命令git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,我們可以看到如下結(jié)果:
現(xiàn)在,所有的快照都可以通過它們的hash值來定位。這并不方便,因?yàn)閷?duì)人類來說記住長度40的16進(jìn)制字符串是非常不方便的。
git的做法是給這些hash值賦予人類能理解的名字,叫做引用。引用是指向commit的指針。和object不可變不同,引用是可變的,可以指向新的commit。比如,master是一個(gè)引用,通常用來指向主分支中最新的commit。
通過引用,git就使用了人類可讀的諸如master這樣的名字來指代歷史中的快照了。
一個(gè)細(xì)節(jié)是,我們經(jīng)常想要知道我們當(dāng)前所在的位置。這樣當(dāng)我們創(chuàng)建新的快照時(shí),我們就知道它關(guān)聯(lián)哪些快照。在git當(dāng)中,我們現(xiàn)在所在的位置也是一個(gè)特殊的引用,叫做HEAD。
最后,我們可以粗略地定義git倉庫了:數(shù)據(jù)object和引用。
在磁盤上,所有記錄都以object和引用的方式存儲(chǔ):因?yàn)閿?shù)據(jù)模型當(dāng)中只有這兩個(gè)概念。所有的git命令都對(duì)應(yīng)commit DAG上的一些操作,比如添加object,添加或更新引用。
當(dāng)你輸入命令的時(shí)候,思考一下命令背后對(duì)于底層數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。相反,如果你做出對(duì)commit DAG進(jìn)行具體的修改,比如拋棄未提交的變更,或者是讓master引用指向5d83f9e commit,這通常都是有辦法的。(上述的例子當(dāng)中,可以使用git checkout master; git reset --hard 5d83f9e
)
git當(dāng)中還包含一個(gè)和數(shù)據(jù)模型不相關(guān)的概念,但它是創(chuàng)建commit接口的一部分。
你可能覺得上面說的創(chuàng)建快照的命令類似于create snapshot,一些VCS的確是這樣,但git不是。我們希望干凈的快照,每次都從當(dāng)前狀態(tài)創(chuàng)建快照在一些情況并不理想。比如,想象一個(gè)場景,你開發(fā)完了兩個(gè)功能,你想要?jiǎng)?chuàng)建兩個(gè)不同的分支。第一個(gè)分支包含第一個(gè)功能,第二個(gè)分支包含第二個(gè)。或者,你在代碼當(dāng)中加入了一些debug信息,在提交的時(shí)候你希望能不要帶上這些debug代碼。
git處理這些場景的方式是使用一種叫做暫存區(qū)(staing area)的機(jī)制,它允許你指定下一次快照會(huì)包含的內(nèi)容。
為了避免重復(fù)信息,我們將不會(huì)詳細(xì)解釋下面的命令。強(qiáng)烈推薦閱讀一下Pro Git這本書獲得更多信息,或者觀看課程視頻。
git help: 獲取 git 命令的幫助信息
git init: 創(chuàng)建一個(gè)新的 git 倉庫,其數(shù)據(jù)會(huì)存放在一個(gè)名為 .git 的目錄下
git status: 顯示當(dāng)前的倉庫狀態(tài)
git add
git commit: 創(chuàng)建一個(gè)新的提交
git log: 顯示歷史日志
git log --all --graph --decorate: 可視化歷史記錄(有向無環(huán)圖)
git diff
git diff
git checkout
git branch: 顯示分支
git branch
git checkout -b
git merge
git mergetool: 使用工具來處理合并沖突
git rebase: 將一系列補(bǔ)丁變基(rebase)為新的基線
如果您之前從來沒有用過 Git,推薦您閱讀 Pro Git 的前幾章,或者完成像 Learn Git Branching這樣的教程。重點(diǎn)關(guān)注 Git 命令和數(shù)據(jù)模型相關(guān)內(nèi)容;
Fork 本課程網(wǎng)站的倉庫:https://github.com/missing-semester/missing-semester
使用 Git 時(shí)的一個(gè)常見錯(cuò)誤是提交本不應(yīng)該由 Git 管理的大文件,或是將含有敏感信息的文件提交給 Git 。嘗試向倉庫中添加一個(gè)文件并添加提交信息,然后將其從歷史中刪除 ( 這篇文章也許會(huì)有幫助:https://help.github.com/articles/removing-sensitive-data-from-a-repository/);
從 GitHub 上克隆某個(gè)倉庫,修改一些文件。當(dāng)您使用 git stash 會(huì)發(fā)生什么?當(dāng)您執(zhí)行 git log --all --oneline 時(shí)會(huì)顯示什么?通過 git stash pop 命令來撤銷 git stash 操作,什么時(shí)候會(huì)用到這一技巧?
與其他的命令行工具一樣,Git 也提供了一個(gè)名為 ~/.gitconfig 配置文件 (或 dotfile)。請?jiān)?~/.gitconfig 中創(chuàng)建一個(gè)別名,使您在運(yùn)行 git graph 時(shí),您可以得到 git log --all --graph --decorate --oneline 的輸出結(jié)果;
您可以通過執(zhí)行 git config --global core.excludesfile ~/.gitignore_global 在 ~/.gitignore_global 中創(chuàng)建全局忽略規(guī)則。配置您的全局 gitignore 文件來自動(dòng)忽略系統(tǒng)或編輯器的臨時(shí)文件,例如 .DS_Store;
克隆 本課程網(wǎng)站的倉庫:https://github.com/missing-semester/missing-semester,找找有沒有錯(cuò)別字或其他可以改進(jìn)的地方,在 GitHub 上發(fā)起拉取請求(Pull Request);
喜歡本文的話不要忘記三連~
聯(lián)系客服