掃描下方海報二維碼,試聽課程:
html
(課程詳細大綱,請參見文末)git
前言: 以前聽過公司大佬分享過 Git 原理以後就想來本身總結一下,最近一忙起來就拖得久了,原本想塞更多的乾貨,可是不喜歡拖過久,因此先出一版足夠入門的;github
Git 是當前流行的分佈式版本控制管理工具,最初由 Linux Torvalds (Linux 之父) 創造,於 2005 年發佈。面試
Git,這個詞其實源自英國俚語,意思大約是 「混帳」。Linux 爲何會以這樣自嘲的名字來命名呢?這其中還有一段兒有趣的歷史能夠說一說:算法
如下摘自:安全
https://www.liaoxuefeng.com/wiki/896043488029600/896202815778784bash
Git 的誕生:服務器
不少人都知道,Linus 在 1991 年建立了開源的 Linux,今後,Linux 系統不斷髮展,已經成爲最大的服務器系統軟件了。app
Linus 雖然建立了 Linux,但 Linux 的壯大是靠全世界熱心的志願者參與的,這麼多人在世界各地爲 Linux 編寫代碼,那 Linux 的代碼是如何管理的呢?分佈式
事實是,在 2002 年之前,世界各地的志願者把源代碼文件經過 diff 的方式發給 Linus,而後由 Linus 本人經過手工方式合併代碼!
你也許會想,爲何 Linus 不把 Linux 代碼放到版本控制系統裏呢?不是有 CVS、SVN 這些免費的版本控制系統嗎?
由於 Linus 堅決地反對 CVS 和 SVN,這些集中式的版本控制系統不但速度慢,並且必須聯網才能使用。
有一些商用的版本控制系統,雖然比 CVS、SVN 好用,但那是付費的,和 Linux 的開源精神不符。
不過,到了 2002 年,Linux 系統已經發展了十年了,代碼庫之大讓 Linus 很難繼續經過手工方式管理了,社區的弟兄們也對這種方式表達了強烈不滿
因而 Linus 選擇了一個商業的版本控制系統 BitKeeper,BitKeeper 的東家 BitMover 公司出於人道主義精神,受權 Linux 社區無償使用這個版本控制系統。
安定團結的大好局面在 2005 年就被打破了,緣由是 Linux 社區牛人彙集,難免沾染了一些梁山好漢的江湖習氣。
開發 Samba 的 Andrew 試圖破解 BitKeeper 的協議(這麼幹的其實也不僅他一個),被 BitMover 公司發現了(監控工做作得不錯!),因而 BitMover 公司怒了,要收回 Linux 社區的無償使用權。
Linus 能夠向 BitMover 公司道個歉,保證之後嚴格管教弟兄們,嗯,這是不可能的。
然而實際狀況是:Linus 花了兩週時間本身用 C 寫了一個分佈式版本控制系統,這就是 Git!
一個月以內,Linux 系統的源碼已經由 Git 管理了!牛是怎麼定義的呢?你們能夠體會一下。
Git 迅速成爲最流行的分佈式版本控制系統,尤爲是 2008 年,GitHub 網站上線了,它爲開源項目免費提供 Git 存儲,無數開源項目開始遷移至 GitHub,包括 jQuery,PHP,Ruby 等等。
歷史就是這麼偶然,若是不是當年 BitMover 公司威脅 Linux 社區,可能如今咱們就沒有免費而超級好用的 Git 了。
無論是集中式的 CVS、SVN 仍是分佈式的 Git 工具,實際上都是一種版本控制系統,咱們能夠經過他們很方便的管理咱們的文件、代碼等,咱們能夠先來暢想一下若是本身來設計這麼一個系統,你會怎麼設計?
摁,這不由讓我想起了以前寫畢業論文的日子,我先在一個開闊的空間建立了一個文件夾用於保存個人各類版本,而後開始了個人 「畢業論文版本管理」,參考下圖:
這好像暴露了我寫畢業論文愉快的經歷..但無論怎麼樣,我在用一個粗粒度版本的制度,在對個人畢業論文進行着管理
摁,我經過不停在原基礎上迭代出新的版本的方式,不只保存了我各個版本的畢業論文,還有這清晰的一個路徑
完美?NO!問題是:
每一次的迭代都更改了什麼東西,我如今徹底看不出來了!
當我在迭代個人超級無敵怎麼樣都不改的版本的時候,忽然回想起好像以前版本 1.0 的第一節內容和 2.0 版本第三節的內容加起來纔是最棒的
我須要打開多個文檔並建立一個新的文檔,仔細對比文檔中的不一樣併爲個人新文檔添加新的東西,好麻煩啊…
到最後文件多起來的時候,我甚至都不知道是個人 「超級無敵版」 是最終版,仍是 「打死都不改版」 是最終版了;
更爲要命的是,我保存在個人桌面上,沒有備份,意味着我本地文件手滑刪除了,那我就…我就…就…
而且可能問題還遠不止於此,因此往往想起,就不自覺對 Linux 膜拜了起來。
Git 採用與 CSV/SVN 徹底不一樣的處理方式,前者採用分佈式,然後面兩個都是集中式的版本管理。
先說集中式版本控制系統,版本庫是集中存放在中央服務器的,而幹活的時候,用的都是本身的電腦,因此要先從中央服務器取得最新的版本,而後開始幹活,幹完活了,再把本身的活推送給中央服務器。
中央服務器就比如是一個圖書館,你要改一本書,必須先從圖書館借出來,而後回到家本身改,改完了,再放回圖書館。
集中式版本控制系統最大的毛病就是必須聯網才能工做,若是在局域網內還好,帶寬夠大,速度夠快
可若是在互聯網上,遇到網速慢的話,可能提交一個10M的文件就須要5分鐘,這還不得把人給憋死啊。
那分佈式版本控制系統與集中式版本控制系統有何不一樣呢?
首先,分佈式版本控制系統根本沒有 「中央服務器」,每一個人的電腦上都是一個完整的版本庫
這樣,你工做的時候,就不須要聯網了,由於版本庫就在你本身的電腦上。
既然每一個人電腦上都有一個完整的版本庫,那多我的如何協做呢?
比方說你在本身電腦上改了文件 A,你的同事也在他的電腦上改了文件 A,這時,大家倆之間只需把各自的修改推送給對方,就能夠互相看到對方的修改了。
和集中式版本控制系統相比,分佈式版本控制系統的安全性要高不少,由於每一個人電腦裏都有完整的版本庫
某一我的的電腦壞掉了沒關係,隨便從其餘人那裏複製一個就能夠了。而集中式版本控制系統的中央服務器要是出了問題,全部人都無法幹活了。
在實際使用分佈式版本控制系統的時候,其實不多在兩人之間的電腦上推送版本庫的修改,由於可能大家倆不在一個局域網內,兩臺電腦互相訪問不了,也可能今天你的同事病了,他的電腦壓根沒有開機。
所以,分佈式版本控制系統一般也有一臺充當 「中央服務器」 的電腦,但這個服務器的做用僅僅是用來方便 「交換」 你們的修改,沒有它你們也同樣幹活,只是交換修改不方便而已。
固然,Git 的強大還遠不止此。
首先,讓咱們來建立一個空的項目目錄,並進入該目錄。
$ mkdir git-demo-project$ cd git-demo-project複製代碼
若是咱們打算對該項目進行版本管理,第一件事就是使用 git init
命令,進行初始化。
$ git init複製代碼
git init
命令只會作一件事,就是在項目的根目錄下建立一個 .git
的子目錄,用來保存當前項目的一些版本信息,咱們能夠繼續使用 tree -a
命令查看該目錄的完整結構,以下:
$ tree -a.└── .git ├── HEAD ├── branches ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ ├── commit-msg.sample │ ├── fsmonitor-watchman.sample │ ├── post-update.sample │ ├── pre-applypatch.sample │ ├── pre-commit.sample │ ├── pre-push.sample │ ├── pre-rebase.sample │ ├── pre-receive.sample │ ├── prepare-commit-msg.sample │ └── update.sample ├── index ├── info │ └── exclude ├── objects │ ├── .DS_Store │ ├── info │ └── pack └── refs ├── heads └── tags複製代碼
config 是倉庫的配置文件,一個典型的配置文件以下,咱們建立的遠端,分支都在等信息都在配置文件裏有表現;fetch
操做的行爲也是在這裏配置的:
[core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true[remote "origin"] url = git@github.com:yanhaijing/zepto.fullpage.git fetch = +refs/heads/*:refs/remotes/origin/*[branch "master"] remote = origin merge = refs/heads/master[branch "dev"] remote = origin merge = refs/heads/dev複製代碼
Git 能夠經過一種算法能夠獲得任意文件的 「指紋」(40 位 16 進制數字),而後經過文件指紋存取數據,存取的數據都位於 objects 目錄。
例如咱們能夠手動建立一個測試文本文件並使用 git add .
命令來觀察 .git
文件夾出現的變化:
$ touch test.txt$ git add .複製代碼
git add .
命令就是用於把當前新增的變化添加進 Git 本地倉庫的,在咱們使用後,咱們驚奇的發現 .git
目錄下的 objects/
目錄下多了一個目錄:
$ tree -a.├── .git│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 節省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── objects│ │ ├── .DS_Store│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ └── tags└── test.txt複製代碼
咱們可使用 git hash-object test.txt
命令來看看剛纔咱們建立的 test.txt
的 「文件指紋」:
$ git hash-object test.txte69de29bb2d1d6434b8b29ae775ad8c2e48c5391複製代碼
這時候咱們能夠發現,新建立的目錄 e6
實際上是該文件哈希值的前兩位,這實際上是 Git 作的一層相似於索引同樣的東西,而且默認採用 16 進制的兩位數來當索引,是很是合適的。
objects 目錄下有 3 種類型的數據:
Blob;
Tree;
Commit;
文件都被存儲爲 blob 類型的文件,文件夾被存儲爲 tree 類型的文件,建立的提交節點被存儲爲 Commit 類型的數據;
通常咱們系統中的目錄(tree),在 Git 會像下面這樣存儲:
而 Commit 類型的數據則整合了 tree 和 blob 類型,保存了當前的全部變化,例如咱們能夠再在剛纔的目錄下新建一個目錄,並添加一些文件試試:
$ mkdir test$ touch test/test.file$ tree -a.├── .git│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 節省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── objects│ │ ├── .DS_Store│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ └── tags├── test│ └── test.file└── test.txt複製代碼
提交一個 Commit 再觀察變化:
$ git commit -a -m "test: 新增測試文件夾和測試文件觀察.git文件的變化"[master (root-commit) 30d51b1] test: 新增測試文件夾和測試文件觀察.git文件的變化 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.txt$ tree -a.├── .git│ ├── COMMIT_EDITMSG│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 節省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── logs│ │ ├── HEAD│ │ └── refs│ │ └── heads│ │ └── master│ ├── objects│ │ ├── .DS_Store│ │ ├── 30│ │ │ └── d51b1edd2efd551dd6bd52d4520487b5708c0e│ │ ├── 5e│ │ │ └── fb9bc29c482e023e40e0a2b3b7e49cec842034│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ │ └── master│ └── tags├── test│ └── test.file└── test.txt複製代碼
首先咱們能夠觀察到咱們提交了一個 Commit 的時候在第一句話裏面返回了一個短的像是哈希值同樣的東西:[master (root-commit) 30d51b1]
中 的 30d51b1
對應的咱們也能夠在 objects 找到剛纔 commit 的對象,咱們可使用 git cat-file -p
命令輸出一下當前文件的內容:
$ git cat-file -p 30d5tree 5efb9bc29c482e023e40e0a2b3b7e49cec842034author 我沒有三顆心臟 <wmyskxz@wmyskxzdemacbook-pro.local> 1565742122 +0800committer 我沒有三顆心臟 <wmyskxz@wmyskxzdemacbook-pro.local> 1565742122 +0800test: 新增測試文件夾和測試文件觀察.git文件的變化</wmyskxz@wmyskxzdemacbook-pro.local></wmyskxz@wmyskxzdemacbook-pro.local>複製代碼
咱們發現這裏面有提交的內容信息、做者信息、提交者信息以及 commit message,固然咱們能夠進一步看到提交的內容具體有哪些:
$ git cat-file -p 5efb100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 test.txt複製代碼
咱們再試着提交一個 commit 來觀察變化:
$ touch test/test2.file$ git commit -a -m "test: 新增長一個 commit 以觀察變化."[master 9dfabac] test: 新增長一個 commit 以觀察變化. 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test.file create mode 100644 test/test2.file$ git cat-file -p 9dfabactree c562bfb9441352f4c218b0028148289f1ea7d7cdparent 30d51b1edd2efd551dd6bd52d4520487b5708c0eauthor 龍滔 <longtao@longtaodemacbook-pro.local> 1565878699 +0800committer 龍滔 <longtao@longtaodemacbook-pro.local> 1565878699 +0800test: 新增長一個 commit 以觀察變化.</longtao@longtaodemacbook-pro.local></longtao@longtaodemacbook-pro.local>複製代碼
能夠觀察到這一次的 commit 多了一個 parent 的行,其中的 「指紋」 和上一次的 commit 如出一轍
當咱們提交兩個 commit 以後咱們的 Git 倉庫能夠簡化爲下圖:
說明:其中由於咱們 test 文件夾新增了文件,也就是出現了變化,因此就被標識成了新的 tree 類型的對象;
refs 目錄存儲都是引用文件,如本地分支,遠端分支,標籤等
refs/heads/xxx 本地分支
refs/remotes/origin/xxx 遠端分支
refs/tags/xxx 本地tag
引用文件的內容都是 40 位長度的 commit
$ cat .git/refs/heads/master9dfabac68470a588a4b4a78742249df46438874a複製代碼
這就像是一個指針同樣,它指向了你的最後一次提交(例如這裏就指向了第二次提交的 commit),咱們補充上分支信息,如今的 Git 倉庫就會像下圖所示:
HEAD 目錄下存儲的是當前所在的位置,其內容是分支的名稱:
$ cat HEADref: refs/heads/master複製代碼
咱們再補充上 HEAD 的信息,如今的 Git 倉庫以下圖所示:
您也在上面瞭解到了,在 Git 中分支是一種十分輕便的存在,僅僅是一個指針罷了
咱們在普遍的使用分支中,不可避免的會遇到新建立分支的合併,這時候不管是選擇 merge 仍是 rebase,都有可能發生衝突,咱們先來看一下衝突是如何產生的:
圖上的狀況,並非移動分支指針就可以解決問題的,它須要一種合併策略。
首先咱們須要明確的是誰與誰的合併,是 2,3 與 4, 5, 6 兩條線的合併嗎?
其實並非的,真實合併的其實只有 3 和 6,由於每一次的提交都包含了項目完整的快照,即合併只是 tree 與 tree 的合併。
這可能提及來有點繞,咱們能夠先來想一個簡單的算法,用來比較 3 和 6 的不一樣。
若是咱們只是單純的比較 3 和 6 的信息,其實並無意義,由於它們之間並不能確切的表達出當前的衝突狀態。
所以咱們須要選取它們兩個分支的分歧點(merge base)做爲參考點,進行比較。
首先咱們把 1 做爲基礎,而後把 一、三、6 中全部的文件作一個列表,而後依次遍歷這個列表中的文件。
咱們如今拿列表中的一個文件進行舉例,把在提交在 一、三、6 中的該文件分別稱爲版本一、版本三、版本6,可能出現以下幾種狀況:
1. 版本 一、版本 三、版本 6 的 「指紋」 值都相同:這種狀況則說明沒有衝突;
2. 版本 3 or 版本 6 至少有一個與版本 1 狀態相同(指的是指紋值相同或都不存在):
這種狀況能夠自動合併,好比版本 1 中存在一個文件,在版本 3 中沒有對該文件進行修改,而版本 6 中刪除了這個文件,則以版本 6 爲準就能夠了;
3. 版本 3 or 版本 6 都與版本 1 的狀態不一樣:
這種狀況複雜一些,自動合併策略很難生效了,因此須要手動解決;
在解決完衝突後,咱們能夠將修改的內容提交爲一個新的提交,這就是 merge。
能夠看到 merge 是一種不修改分支歷史提交記錄的方式,這也是咱們經常使用的方式。
可是這種方式在某些狀況下使用起來不太方便,好比咱們建立了一些提交發送給管理者,管理者在合併操做中產生了衝突,還須要去解決衝突,這無疑增長了他人的負擔。
而咱們使用 rebase 能夠解決這種問題。
假設咱們的分支結構以下:
rebase 會把從 Merge Base 以來的全部提交,以補丁的形式一個一個從新打到目標分支上。
這使得目標分支合併該分支的時候會直接 Fast Forward(能夠簡單理解爲直接後移指針),即不會產生任何衝突。提交歷史是一條線,這對強迫症患者可謂是一大福音。
其實 rebase 主要是在 .git/rebase-merge 下生成了兩個文件,分別爲 git-rebase-todo 和 done 文件,這兩個文件的做用光看名字就大概可以看得出來。
git-rebase-todo 中存放了 rebase 將要操做的 commit,而 done 存放正操做或已操做完畢的 commit
好比咱們這裏,git-rebase-todo 存放了 四、五、6 三個提交。
首先 Git 會把 4 這個 commit 放入 done,表示正在操做 4,而後將 4 以補丁的方式打到 3 上,造成了新的 4
這一步是可能產生衝突的,若是有衝突,須要解決衝突以後才能繼續操做。 接着按一樣的方式把 五、6 都放入 done,最後把指針移動到最新的提交 6
上,就完成了 rebase 的操做。
從剛纔的圖中,咱們就能夠看到 rebase 的一個缺點,那就是修改了分支的歷史提交。
若是已經將分支推送到了遠程倉庫,會致使沒法將修改後的分支推送上去,必須使用 -f 參數(force)強行推送。
因此使用 rebase 最好不要在公共分支上進行操做。
簡單說就是壓縮提交,把屢次的提交融合到一個 commit 中,這樣的好處不言而喻,咱們着重來討論一下實現的技術細節
仍是以咱們上面最開始的分支狀況爲例,首先,Git 會建立一個臨時分支,指向當前 feature 的最新 commit。
而後按照上面 rebase 的方式,變基到 master 的最新 commit 處。
接着用 rebase 來 squash 之,壓縮這些提交爲一個提交。
最後以 fast forward 的方式合併到 master 中。
可見此時 master 分支多且只多了一個描述了此次改動的提交,這對於大型工程,保持主分支的簡潔易懂有很大的幫助。
說明:想要了解更多的諸如 checkout、cherry-pick 等操做的話能夠看看參考文章的第三篇,這裏就不作細緻描述了。
經過上面的瞭解,其實咱們已經大體的掌握了 Git 中的基本原理
咱們的 Commit 就像是一個鏈表節點同樣,不只有自身的節點信息,還保存着上一個節點的指針
而後咱們以 Branch 這樣輕量的指針保存着一條又一條的 commit 鏈條
不過值得注意的是,objects 目錄下的文件是不會自動刪除的,除非你手動 GC,否則本地的 objects 目錄下就保留着你當前項目完整的變化信息
因此咱們一般都會看到 Git 上面的項目一般是沒有 .git 目錄的,否則僅僅經過 .git 目錄理論上就能夠還原出你的完整項目!
https://www.liaoxuefeng.com/wiki/896043488029600/896202780297248
https://yanhaijing.com/git/2017/02/08/deep-git-3
https://coding.net/help/doc/practice/git-principle.html
END
《21天互聯網Java進階面試訓練營(分佈式篇)》詳細目錄,掃描圖片末尾的二維碼,試聽課程