簡單來講,Git,它是一個快速的 分佈式版本控制系統 (Distributed Version Control System,簡稱 DVCS)
。git
同傳統的 集中式版本控制系統 (Centralized Version Control Systems,簡稱CVCS)
不一樣,Git的分佈式特性使得開發者間的協做變得更加靈活多樣。程序員
這時候咱們會想到:算法
咱們帶着問題往下走。數據庫
版本控制是一種記錄一個或若干文件內容變化,以便未來查閱特定版本修訂狀況的系統。安全
好比:有一位程序員他可能須要保存一個代碼文件的全部的修訂版本,這樣就能夠服務器
這時候採用版本控制就是一個很是明智的選擇,使用版本控制系統一般還意味着,就算你亂來一氣把整個項目中的文件改的改刪的刪,你也照樣能夠輕鬆恢復到原先的樣子。 但額外增長的工做量卻微乎其微。app
兒童:人們經過複製整個項目的方式來保存不一樣的版本,或許還會更名加上備份時間以示區別。好處就是簡單,可是特別容易犯錯,一不當心會寫錯文件或者覆蓋意想外的文件。分佈式
少年:人們爲了上面的問題,好久之前就開發了許多種本地版本控制系統,大可能是採用某種簡單的數據庫來記錄文件的歷次更新差別,好比其中比較流行的 RCS
。網站
青年:人們又遇到一個問題,如何讓在不一樣系統上的開發者協同工做? 因而,集中化的版本控制系統( CVCS)
應運而生。 這類系統,諸如 CVS
、 Subversion
,都有一個單一的集中管理的服務器,保存全部文件的修訂版本,而協同工做的人們都經過客戶端連到這臺服務器,取出最新的文件或者提交更新。如今,每一個人均可以在必定程度上看到項目中的其餘人正在作些什麼。 而管理員也能夠輕鬆掌控每一個開發者的權限,而且管理一個 CVCS
。設計
事分兩面,有好有壞。 這麼作最顯而易見的缺點是中央服務器的單方面故障。 若是關機一小時,那麼在這一小時內,誰都沒法提交更新,也就沒法協同工做。 若是中心數據庫所在的磁盤發生損壞,又沒有作恰當備份,毫無疑問你將丟失全部數據——包括項目的整個變動歷史,只剩下人們在各自機器上保留的單獨快照。
壯年:因而分佈式版本控制系統面世了。 在這類系統中,像 Git
、Mercurial
等,客戶端並不僅提取最新版本的文件快照,而是把代碼倉庫完整地鏡像下來。 這麼一來,任何一處協同工做用的服務器發生故障,過後均可以用任何一個鏡像出來的本地倉庫恢復。 由於每一次的克隆操做,實際上都是一次對代碼倉庫的完整備份。
許多這類系統均可以指定和若干不一樣的遠端代碼倉庫進行交互。籍此,你就能夠在同一個項目中,分別和不一樣工做小組的人相互協做。 你能夠根據須要設定不一樣的協做流程,好比層次模型式的工做流,而這在之前的集中式系統中是沒法實現的。
不少人都知道, Linus
在1991年建立了開源的 Linux
,今後,Linux
系統不斷髮展,已經成爲最大的服務器系統軟件了。
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
了。
在集中式系統中,每一個開發者就像是鏈接在集線器上的節點,彼此的工做方式大致相像。 而在 Git
中,每一個開發者同時扮演着節點和集線器的角色——也就是說,每一個開發者既能夠將本身的代碼貢獻到其餘的倉庫中,同時也能維護本身的公開倉庫,讓其餘人能夠在其基礎上工做並貢獻代碼。 由此,Git
的分佈式協做能夠爲你的項目和團隊衍生出種種不一樣的工做流程。
速度快
簡單的設計,易用
對非線性開發模式的強力支持(容許成千上萬個並行開發的分支)
徹底分佈式
有能力高效管理相似 Linux 內核同樣的超大規模項目(速度和數據量)
從根本上來說 Git
是一個內容尋址 (content-addressable)
文件系統,並在此之上提供了一個版本控制系統的用戶界面,Git 的核心部分是一個簡單的鍵值對數據庫 (key-value data store)
。 你能夠向該數據庫插入任意類型的內容,它會返回一個鍵值,經過該鍵值能夠在任意時刻再次檢索 (retrieve)
該內容。
當在一個新目錄或已有目錄執行 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
文件保存暫存區信息。全部用來表示項目歷史信息的文件,是經過一個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
一般用來存儲文件的內容。
一個 tree
對象能夠指向一個包含文件內容的 blob
對象, 也能夠是其它包含某個子目錄內容的其它 tree
對象,它通常用來表示內容之間的目錄層次關係。 Tree
對象、blob
對象和其它全部的對象同樣,都用其內容的 SHA1
哈希值來命名的;只有當兩個 tree
對象的內容徹底相同(包括其所指向全部子對象)時,它的名字纔會同樣,反之亦然。這樣就能讓Git
僅僅經過比較兩個相關的 tree
對象的名字是否相同,來快速的判斷其內容是否不一樣。
commit
對象指向一個 tree
對象,而且帶有相關的描述信息。
一個提交 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
中它們也許看起來就以下圖:
你能夠看到:每一個目錄都建立了 tree
對象 (包括根目錄), 每一個文件都建立了一個對應的 blob
對象。最後有一個 commit
對象 來指向根 tree
對象 (root of trees)
, 這樣咱們就能夠追蹤項目每一項提交內容.
一個標籤對象包括一個對象名(SHA1簽名)
, 對象類型, 標籤名, 標籤建立人的名字(tagger)
, 還有一條可能包含有簽名(signature)
的消息.
有人把 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 鼓勵開發者頻繁使用分支,正是由於有着這些特性做保障。
$ git checkout -b iss53
$ git commit -a -m 'new text'
$ git checkout master $ git checkout -b hotfix $ git commit -a -m 'fixed bug'
$ git checkout master $ git merge hotfix
$ git branch -d hotfix $ git checkout iss53 $ git commit -a -m 'finished'
$ git checkout master $ git merge iss53
請注意,此次合併操做的底層實現,並不一樣於以前 hotfix
的併入方式。由於此次你的開發歷史是從更早的地方開始分叉的。因爲當前 master
分支所指向的提交對象(C4)並非 iss53
分支的直接祖先,Git
不得不進行一些額外處理。就此例而言,Git
會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。
此次,Git
沒有簡單地把分支指針右移,而是對三方合併後的結果從新作一個新的快照,並自動建立一個指向它的提交對象(C6)。這個提交對象比較特殊,它有兩個祖先(C4 和 C5)。
有時候合併操做並不會如此順利。若是在不一樣的分支中都修改了同一個文件的同一部分,Git
就沒法乾淨地把二者合到一塊兒。若是你在解決問題 #53
的過程當中修改了 hotfix
中修改的部分,將會出現問題。
Git
做了合併,但沒有提交,它會停下來等你解決衝突。
任何包含未解決衝突的文件都會以未合併 unmerged
的狀態列出。Git
會在有衝突的文件里加入標準的衝突解決標記,能夠經過它們來手工定位並解決這些衝突。
最容易的整合分支的方法是 merge
命令,它會把兩個分支最新的快照(C3 和 C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)。:
可是,若是你想讓 experiment
分支歷史看起來像沒有通過任何合併同樣,還有另一個選擇:你能夠把在 C3 裏產生的變化補丁在 C4 的基礎上從新打一遍。在 Git
裏,這種操做叫作變基 (rebase)
。有了 rebase
命令,就能夠把在一個分支裏提交的改變移到另外一個分支裏重放一遍。
$ git checkout experiment $ git rebase master
它的原理是回到兩個分支最近的共同祖先,根據當前分支(也就是要進行變基的分支 experiment
)後續的歷次提交對象(這裏只有一個 C3),生成一系列文件補丁,而後以基底分支(也就是主幹分支 master)最後一個提交對象(C4)爲新的出發點,逐個應用以前準備好的補丁文件,最後會生成一個新的合併提交對象(C3'),從而改寫 experiment
的提交歷史,使它成爲 master
分支的直接下游
簡單講他就是把你的 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
的適合場景:經典型方式,三點式,interactive
和 cherry-picking
。
咱們使用變基的目的:是想要獲得一個能在遠程分支上乾淨應用的補丁 — 好比某些項目你不是維護者,但想幫點忙的話,最好用變基:先在本身的一個分支裏進行開發,當準備向主項目提交補丁的時候,根據最新的 origin/master
進行一次變基操做而後再提交,這樣維護者就不須要作任何整合工做(其實是把解決分支補丁同最新主幹代碼之間衝突的責任,化轉爲由提交補丁的人來解決。),只需根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的補丁。
須要注意,合併結果中最後一次提交所指向的快照,不管是經過變基,仍是三方合併,都會獲得相同的快照內容,只不過提交歷史不一樣罷了。變基是按照每行的修改次序重演一遍修改,而合併是把最終結果合在一塊兒。
甚至咱們在一次提交時純粹就是由於懶惰的緣由,我可能吧不少的變動都放在一個commit中作了提交。
rebase能夠合併commit
rebase能夠用來修改commit信息
rebase能夠用來拆分commit
git rebase -i HEAD~3
變基也能夠放到其餘分支進行,並不必定非得根據分化以前的分支。
要用它得遵照一條準則:
不要在公共分支上使用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