淺析git

git是什麼

簡單來講,Git,它是一個快速的 分佈式版本控制系統 (Distributed Version Control System,簡稱 DVCS)git

同傳統的 集中式版本控制系統 (Centralized Version Control Systems,簡稱CVCS) 不一樣,Git的分佈式特性使得開發者間的協做變得更加靈活多樣。程序員

這時候咱們會想到:算法

  1. 什麼又是版本控制呢?
  2. 什麼是分佈式什麼是集中式?

咱們帶着問題往下走。數據庫

版本控制

版本控制是一種記錄一個或若干文件內容變化,以便未來查閱特定版本修訂狀況的系統。安全

好比:有一位程序員他可能須要保存一個代碼文件的全部的修訂版本,這樣就能夠服務器

  • 將某個文件回溯到以前的狀態
  • 甚至將整個項目都回退到過去某個時間點的狀態
  • 比較文件的變化細節,查出最後是誰修改了哪一個地方,從而找出致使怪異問題出現的緣由

這時候採用版本控制就是一個很是明智的選擇,使用版本控制系統一般還意味着,就算你亂來一氣把整個項目中的文件改的改刪的刪,你也照樣能夠輕鬆恢復到原先的樣子。 但額外增長的工做量卻微乎其微。app

版本控制的成長

兒童:人們經過複製整個項目的方式來保存不一樣的版本,或許還會更名加上備份時間以示區別。好處就是簡單,可是特別容易犯錯,一不當心會寫錯文件或者覆蓋意想外的文件。分佈式

少年:人們爲了上面的問題,好久之前就開發了許多種本地版本控制系統,大可能是採用某種簡單的數據庫來記錄文件的歷次更新差別,好比其中比較流行的 RCS網站

青年:人們又遇到一個問題,如何讓在不一樣系統上的開發者協同工做? 因而,集中化的版本控制系統( CVCS)應運而生。 這類系統,諸如 CVSSubversion ,都有一個單一的集中管理的服務器,保存全部文件的修訂版本,而協同工做的人們都經過客戶端連到這臺服務器,取出最新的文件或者提交更新。如今,每一個人均可以在必定程度上看到項目中的其餘人正在作些什麼。 而管理員也能夠輕鬆掌控每一個開發者的權限,而且管理一個 CVCS設計

事分兩面,有好有壞。 這麼作最顯而易見的缺點是中央服務器的單方面故障。 若是關機一小時,那麼在這一小時內,誰都沒法提交更新,也就沒法協同工做。 若是中心數據庫所在的磁盤發生損壞,又沒有作恰當備份,毫無疑問你將丟失全部數據——包括項目的整個變動歷史,只剩下人們在各自機器上保留的單獨快照。

壯年:因而分佈式版本控制系統面世了。 在這類系統中,像 GitMercurial 等,客戶端並不僅提取最新版本的文件快照,而是把代碼倉庫完整地鏡像下來。 這麼一來,任何一處協同工做用的服務器發生故障,過後均可以用任何一個鏡像出來的本地倉庫恢復。 由於每一次的克隆操做,實際上都是一次對代碼倉庫的完整備份。

許多這類系統均可以指定和若干不一樣的遠端代碼倉庫進行交互。籍此,你就能夠在同一個項目中,分別和不一樣工做小組的人相互協做。 你能夠根據須要設定不一樣的協做流程,好比層次模型式的工做流,而這在之前的集中式系統中是沒法實現的。

git誕生史記

不少人都知道, Linus 在1991年建立了開源的 Linux ,今後,Linux 系統不斷髮展,已經成爲最大的服務器系統軟件了。

Linus 雖然建立了 Linux,但 Linux 的壯大是靠全世界熱心的志願者參與的,這麼多人在世界各地爲 Linux 編寫代碼,那 Linux 的代碼是如何管理的呢?

事實是,在2002年之前,世界各地的志願者把源代碼文件經過 diff 的方式發給 Linus,而後由 Linus 本人經過手工方式合併代碼!

你也許會想,爲何 Linus 不把 Linux 代碼放到版本控制系統裏呢?不是有 CVSSVN這些免費的版本控制系統嗎?由於 Linus 堅決地反對 CVSSVN,這些集中式的版本控制系統不但速度慢,並且必須聯網才能使用。有一些商用的版本控制系統,雖然比 CVSSVN 好用,但那是付費的,和 Linux 的開源精神不符。

不過,到了2002年,Linux 系統已經發展了十年了,代碼庫之大讓 Linus 很難繼續經過手工方式管理了,社區的弟兄們也對這種方式表達了強烈不滿,因而 Linus 選擇了一個商業的版本控制系統 BitKeeperBitKeeper 的東家 BitMover 公司出於人道主義精神,受權 Linux 社區無償使用這個版本控制系統。

安定團結的大好局面在2005年就被打破了,緣由是 Linux 社區牛人彙集,難免沾染了一些梁山好漢的江湖習氣。開發 SambaAndrew 試圖破解 BitKeeper 的協議(這麼幹的其實也不僅他一個),被 BitMover 公司發現了(監控工做作得不錯!),因而 BitMover 公司怒了,要收回 Linux 社區的無償使用權。

Linus 能夠向 BitMover 公司道個歉,保證之後嚴格管教弟兄們,嗯,這是不可能的。實際狀況是這樣的:

Linus 花了兩週時間本身用 C 寫了一個分佈式版本控制系統,這就是 Git!一個月以內,Linux 系統的源碼已經由 Git 管理了!牛是怎麼定義的呢?你們能夠體會一下。

Git 迅速成爲最流行的分佈式版本控制系統,尤爲是2008年,GitHub 網站上線了,它爲開源項目免費提供 Git 存儲,無數開源項目開始遷移至 GitHub,包括 jQueryPHPRuby等等。

歷史就是這麼偶然,若是不是當年 BitMover 公司威脅 Linux 社區,可能如今咱們就沒有免費而超級好用的 Git 了。

git的優勢

在集中式系統中,每一個開發者就像是鏈接在集線器上的節點,彼此的工做方式大致相像。 而在 Git 中,每一個開發者同時扮演着節點和集線器的角色——也就是說,每一個開發者既能夠將本身的代碼貢獻到其餘的倉庫中,同時也能維護本身的公開倉庫,讓其餘人能夠在其基礎上工做並貢獻代碼。 由此,Git 的分佈式協做能夠爲你的項目和團隊衍生出種種不一樣的工做流程。

  • 速度快

  • 簡單的設計,易用

  • 對非線性開發模式的強力支持(容許成千上萬個並行開發的分支)

  • 徹底分佈式

  • 有能力高效管理相似 Linux 內核同樣的超大規模項目(速度和數據量)

git實現原理

從根本上來說 Git 是一個內容尋址 (content-addressable) 文件系統,並在此之上提供了一個版本控制系統的用戶界面,Git 的核心部分是一個簡單的鍵值對數據庫 (key-value data store) 。 你能夠向該數據庫插入任意類型的內容,它會返回一個鍵值,經過該鍵值能夠在任意時刻再次檢索 (retrieve) 該內容。

初始化的git目錄

當在一個新目錄或已有目錄執行 git init 時,Git 會建立一個 .git 目錄。 這個目錄包含了幾乎全部 Git 存儲和操做的對象。 如若想備份或複製一個版本庫,只需把這個目錄拷貝至另外一處便可。

$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/

這是一個全新的 git init 版本庫,這將是你看到的默認結構。

  • description 文件僅供 GitWeb 程序使用,咱們無需關心。
  • config 文件包含項目特有的配置選項。
  • info 目錄包含一個全局性排除(global exclude)文件,用以放置那些不但願被記錄在 .gitignore 文件中的忽略模式(ignored patterns)
  • hooks 目錄包含客戶端或服務端的鉤子腳本 (hook scripts)
  • objects 目錄存儲全部數據內容。
  • refs 目錄存儲指向數據(分支)的提交對象的指針
  • HEAD 文件指示目前被檢出的分支
  • index 文件保存暫存區信息。

git對象模型

全部用來表示項目歷史信息的文件,是經過一個40個字符的 (40-digit) 「對象名」來索引的,對象名看起來像這樣:

6ff87c4664981e4397625791c8ea3bbb5f2279a3

你會在Git裏處處看到這種「40個字符」字符串。每個「對象名」都是對「對象」內容作 SHA1 哈希計算得來的,( SHA1 是一種密碼學的哈希算法)。這樣就意味着兩個不一樣內容的對象不可能有相同的「對象名」。

這樣作會有幾個好處:

  • Git 只要比較對象名,就能夠很快的判斷兩個對象是否相同。
  • 由於在每一個倉庫 (repository) 的「對象名」的計算方法都徹底同樣,若是一樣的內容存在兩個不一樣的倉庫中,就會存在相同的「對象名」下。
  • Git 還能夠經過檢查對象內容的 SHA1 的哈希值和「對象名」是否相同,來判斷對象內容是否正確。

對象

每一個對象 (object) 包括三個部分:類型,大小和內容。大小就是指內容的大小,內容取決於對象的類型,有四種類型的對象:"blob""tree""commit""tag"

  • 「blob」 用來存儲文件數據,一般是一個文件。

  • 「tree」 有點像一個目錄,它管理一些「tree」或是 「blob」(就像文件和子目錄)。

  • 一個「commit」只指向一個"tree",它用來標記項目某一個特定時間點的狀態。它包括一些關於時間點的元數據,如時間戳、最近一次提交的做者、指向上次提交 (commits) 的指針等等。

  • 一個 「tag」 是來標記某一個提交 (commit) 的方法。

幾乎全部的 Git 功能都是使用這四個簡單的對象類型來完成的。它就像是在你本機的文件系統之上構建一個小的文件系統。

Blob對象

image

一個 blob 一般用來存儲文件的內容。

Tree 對象

image

一個 tree 對象能夠指向一個包含文件內容的 blob 對象, 也能夠是其它包含某個子目錄內容的其它 tree 對象,它通常用來表示內容之間的目錄層次關係。 Tree 對象、blob 對象和其它全部的對象同樣,都用其內容的 SHA1 哈希值來命名的;只有當兩個 tree 對象的內容徹底相同(包括其所指向全部子對象)時,它的名字纔會同樣,反之亦然。這樣就能讓Git 僅僅經過比較兩個相關的 tree 對象的名字是否相同,來快速的判斷其內容是否不一樣。

Commit對象

commit 對象指向一個 tree 對象,而且帶有相關的描述信息。

image

一個提交 commit 由如下的部分組成:

  • 一個 tree 對象:tree 對象的 `SHA1簽名, 表明着目錄在某一時間點的內容。

  • 父對象 (parent(s)): 提交 (commit) 的SHA1簽名表明着當前提交前一步的項目歷史。合併的提交 (merge commits) 可能會有不僅一個父對象。若是一個提交沒有父對象,那麼咱們就叫它「根提交" (root commit) ,它就表明着項目最初的一個版本 (revision)。 每一個項目必須有至少有一個「根提交"(root commit)。

  • 做者 (author) :作了這次修改的人的名字,還有修改日期。

  • 提交者(committer):實際建立提交(commit)的人的名字, 同時也帶有提交日期。
  • 註釋:用來描述這次提交。

注意:一個提交(commit)自己並無包括任何信息來講明其作了哪些修改; 全部的修改(changes)都是經過與父提交(parents)的內容比較而得出的。 值得一提的是, 儘管git能夠檢測到文件內容不變而路徑改變的狀況, 可是它不會去顯式(explicitly)的記錄文件的改名操做(能夠看一下 git diff )。

通常用 git commit 來建立一個提交 (commit), 這個提交 (commit) 的父對象通常是當前分支 (current HEAD) ,同時把存儲在當前索引 (index) 的內容所有提交。

對象模型:

若是咱們把它提交 (commit) 到一個 Git 倉庫中, 在 Git 中它們也許看起來就以下圖:

image

你能夠看到:每一個目錄都建立了 tree對象 (包括根目錄), 每一個文件都建立了一個對應的 blob對象。最後有一個 commit 對象 來指向根 tree 對象 (root of trees) , 這樣咱們就能夠追蹤項目每一項提交內容.

標籤對象:

image

一個標籤對象包括一個對象名(SHA1簽名), 對象類型, 標籤名, 標籤建立人的名字(tagger), 還有一條可能包含有簽名(signature)的消息.


回到咱們的問題


強大的git分支

有人把 Git 的分支模型稱爲它的必殺技特性,也正由於這一特性,使得它 從衆多版本控制系統中脫穎而出。

Git 保存的不是文件的變化或者差別,而是一系列不一樣時刻的文件快照。

在進行提交操做時,Git 會保存一個提交對象(commit object)。知道了 Git 保存數據的方式,該提交對象會包含一個指向暫存內容快照的指針。 但不只僅是這樣,該提交對象還包含了做者的姓名和郵箱、提交時輸入的信息以及指向它的父對象的指針。首次提交產生的提交對象沒有父對象,普通提交操做產生的提交對象有一個父對象,而由多個分支合併產生的提交對象有多個父對象,

當使用 git commit 新建一個提交對象前,Git 會先計算每個子目錄的校驗和(40 個字符長度 SHA-1 字串),而後在 Git 倉庫中將這些目錄保存爲樹(tree)對象。以後 Git 建立的提交對象,除了包含相關提交信息之外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就能夠在未來須要的時候,重現這次快照的內容了。

Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 做爲分支的默認名字。在若干次提交後,你其實已經有了一個指向最後一次提交對象的 master 分支,它在每次提交的時候都會自動向前移動。

Git 是如何知道你當前在哪一個分支上工做的呢?其實答案也很簡單,它保存着一個名爲 HEAD 的特別指針。在 Git 中,它是一個指向你正在工做中的本地分支的指針,咱們能夠將 HEAD 想象爲當前分支的別名。

因爲 Git 中的分支實際上僅是一個包含所指對象校驗和的文件,因此建立和銷燬一個分支就變得很是廉價。說白了,新建一個分支就是向一個文件寫入 41 個字節(外加一個換行符)那麼簡單,固然也就很快了。

大多數版本控制系統它們管理分支大多采起備份全部項目文件到特定目錄的方式,因此根據項目文件數量和大小不一樣,可能花費的時間也會有至關大的差異,快則幾秒,慢則數分鐘。

而 Git 的實現與項目複雜度無關,它永遠能夠在幾毫秒的時間內完成分支的建立和切換。同時,由於每次提交時都記錄了祖先信息(parent 對象),未來要合併分支時,尋找恰當的合併基礎(譯註:即共同祖先)的工做其實已經天然而然地擺在那裏了,因此實現起來很是容易。Git 鼓勵開發者頻繁使用分支,正是由於有着這些特性做保障。

分支的新建與合併

  1. 新建分支並進入

$ git checkout -b iss53

image

  1. 根據需求寫代碼並提交
$ git commit -a -m 'new text'

image

  1. 接到線上問題須要而且修改bug
$ git checkout master
$ git checkout -b hotfix
$ git commit -a -m 'fixed bug'

image

  1. 合併修改完bug的代碼進master(暫無衝突)
$ git checkout master
$ git merge hotfix

image

  1. 解決問題後刪除hotfix分支並返回原來的iss53分支繼續工做
$ git branch -d hotfix
$ git checkout iss53
$ git commit -a -m 'finished'

image

  1. 合併iss53分支進主分支
$ git checkout master
$ git merge iss53

請注意,此次合併操做的底層實現,並不一樣於以前 hotfix 的併入方式。由於此次你的開發歷史是從更早的地方開始分叉的。因爲當前 master 分支所指向的提交對象(C4)並非 iss53 分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。

此次,Git 沒有簡單地把分支指針右移,而是對三方合併後的結果從新作一個新的快照,並自動建立一個指向它的提交對象(C6)。這個提交對象比較特殊,它有兩個祖先(C4 和 C5)。

image

image

有時候合併操做並不會如此順利。若是在不一樣的分支中都修改了同一個文件的同一部分,Git 就沒法乾淨地把二者合到一塊兒。若是你在解決問題 #53 的過程當中修改了 hotfix 中修改的部分,將會出現問題。

Git 做了合併,但沒有提交,它會停下來等你解決衝突。

任何包含未解決衝突的文件都會以未合併 unmerged 的狀態列出。Git 會在有衝突的文件里加入標準的衝突解決標記,能夠經過它們來手工定位並解決這些衝突。

rebase 變基

最容易的整合分支的方法是 merge 命令,它會把兩個分支最新的快照(C3 和 C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)。:

image

可是,若是你想讓 experiment分支歷史看起來像沒有通過任何合併同樣,還有另一個選擇:你能夠把在 C3 裏產生的變化補丁在 C4 的基礎上從新打一遍。在 Git 裏,這種操做叫作變基 (rebase)。有了 rebase 命令,就能夠把在一個分支裏提交的改變移到另外一個分支裏重放一遍。

$ git checkout experiment
$ git rebase master

它的原理是回到兩個分支最近的共同祖先,根據當前分支(也就是要進行變基的分支 experiment )後續的歷次提交對象(這裏只有一個 C3),生成一系列文件補丁,而後以基底分支(也就是主幹分支 master)最後一個提交對象(C4)爲新的出發點,逐個應用以前準備好的補丁文件,最後會生成一個新的合併提交對象(C3'),從而改寫 experiment 的提交歷史,使它成爲 master 分支的直接下游

image

簡單講他就是把你的 experiment 分支裏的每一個提交 commit 取消掉,而且把它們臨時 保存爲補丁 patch (這些補丁放到".git/rebase"目錄中),而後把 experiment 分支更新 到最新的 origin 分支,最後把保存的這些補丁應用到 experiment 分支上。

如今的 C3' 對應的快照,其實和普通的三方合併,即上個例子中的 C5 對應的快照內容如出一轍了。雖然最後整合獲得的結果沒有任何區別,但變基能產生一個更爲整潔的提交歷史。若是視察一個變基過的分支的歷史記錄,看起來會更清楚:彷彿全部修改都是在一根線上前後進行的,儘管實際上它們本來是同時並行發生的。

rebase 的過程當中,也許會出現衝突 conflict。在這種狀況,Git 會中止 rebase 並會讓你去解決 衝突;在解決完衝突後,用 git-add 命令去更新這些內容的索引 index, 而後,你無需執行 git-commit ,只要執行:

$ git rebase --continue
這樣git會繼續應用 apply 餘下的補丁。在任什麼時候候,你能夠用 --abort 參數來終止 rebase 的行動,而且 experiment 分支會回到 rebase 開始前的狀態。

$ git rebase --abort

git merge 應該只用於爲了保留一個有用的,語義化的準確的歷史信息,而但願將一個分支的整個變動集成到另一個 branch 時使用 rebase。這樣造成的清晰版本變動圖有着重要的價值。

全部其餘的狀況都是以不一樣的方式使用 rebase 的適合場景:經典型方式,三點式,interactivecherry-picking

咱們使用變基的目的:是想要獲得一個能在遠程分支上乾淨應用的補丁 — 好比某些項目你不是維護者,但想幫點忙的話,最好用變基:先在本身的一個分支裏進行開發,當準備向主項目提交補丁的時候,根據最新的 origin/master 進行一次變基操做而後再提交,這樣維護者就不須要作任何整合工做(其實是把解決分支補丁同最新主幹代碼之間衝突的責任,化轉爲由提交補丁的人來解決。),只需根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的補丁。

須要注意,合併結果中最後一次提交所指向的快照,不管是經過變基,仍是三方合併,都會獲得相同的快照內容,只不過提交歷史不一樣罷了。變基是按照每行的修改次序重演一遍修改,而合併是把最終結果合在一塊兒。

有趣的變基

  • 我在不一樣的topic之間來回切換,這樣會致使個人歷史中不一樣topic互相交叉,邏輯上組織混亂;
  • 咱們可能須要多個連續的commit來解決一個bug;
  • 我可能會在commit中寫了錯別字,後來又作修改;
  • 甚至咱們在一次提交時純粹就是由於懶惰的緣由,我可能吧不少的變動都放在一個commit中作了提交。

  • rebase能夠合併commit

  • rebase能夠用來修改commit信息

  • rebase能夠用來拆分commit

git rebase -i HEAD~3

變基也能夠放到其餘分支進行,並不必定非得根據分化以前的分支。

image

image

image

變基的風險

要用它得遵照一條準則:

不要在公共分支上使用rebase。

「No one shall rebase a shared branch」 — Everyone about rebase

若是你遵循這條金科玉律,就不會出差錯。

在進行變基的時候,實際上拋棄了一些現存的提交對象而創造了一些相似但不一樣的新的提交對象。若是你把原來分支中的提交對象發佈出去,而且其餘人更新下載後在其基礎上開展工做,而稍後你又用 git rebase 拋棄這些提交對象,把新的重演後的提交對象發佈出去的話,你的合做者就不得不從新合併他們的工做,這樣當你再次從他們那裏獲取內容時,提交歷史就會變得一團糟。

注意rebase每每會重寫歷史,全部已經存在的commits雖然內容沒有改變,可是commit自己的hash都會改變。

結論:只要你的分支上須要rebase的全部commits歷史尚未被push過(好比上例中rebase時從分叉處開始有兩個commit歷史會被重寫),就能夠安全地使用git rebase來操做。

上述結論可能還須要修正:對於再也不有子分支的branch,而且由於rebase而會被重寫的commits都尚未push分享過,能夠比較安全地作rebase

思考下它的功能吧 git pull --rebase

相關文章
相關標籤/搜索