Git版本控制 —— 原理簡述

前言

這已是我第二次編寫Git原理方面的博文了。以前走了很多彎路,對於不少問題的理解也浮於表面,因此決定從新整理一次。其實以前的彎路很大一部分緣由是讀了劣質的資料致使的,因此強烈建議除了ProGit之外其餘的國產資料僅作參考。git

 

分佈式

版本控制的概念很少作解釋了,咱們經常使用的SVN屬於集中化的版本控制系統,而Git屬於分佈式版本控制系統(Distributed Version Control System,簡稱 DVCS)。在這類系統中客戶端並不僅提取最新版本的文件快照,而是把代碼倉庫完整地鏡像下來。 這麼一來,任何一處協同工做用的服務器發生故障,過後均可以用任何一個鏡像出來的本地倉庫恢復。 由於每一次的克隆操做,實際上都是一次對代碼倉庫的完整備份。數據庫

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

 

版本文件管理

Git在對待數據版本管理上的作法,與其餘版本控制系統不一樣。其餘系統存儲版本變動信息,就像用一個記事本記錄下了文件的變化狀況。而Git採用保存「快照流」的方式管理文件,就像是把原來的文件放到抽屜裏,而後在新的複印件上作出修改。服務器

存儲版本變動信息方式:網絡

快照流方式:分佈式

這裏要強調一下,只有被修改過的文件纔會在對應版本產生出新的快照。因此每次提交所產生的快照不是整個文件系統,而是被修改過的那部分文件。(上圖虛線圈出的文件表示沒有生成新的快照)svn

 

本地執行

Git在每一個用戶機器上都有一份完整的版本庫,因此咱們在集中式版本控制系統一般所作的提交代碼、比較代碼、分支合併等工做在本地就能夠完成,並且速度極快。工具

可是不一樣開發者之間的代碼協做,仍是須要經過網絡鏈接遠程倉庫進行交換的。學習

 

文件狀態(已修改和已暫存、已提交)

Git管理的文件存在三個狀態即已提交(committed)、已修改(modified)和已暫存(staged) 已提交表示數據已經安全的保存在本地數據庫中。 已修改表示修改了文件,但還沒保存到數據庫中。 已暫存表示對一個已修改文件的當前版本作了標記,使之包含在下次提交的快照中。測試

由此引入 Git 項目的三個工做區域的概念:Git 倉庫、工做目錄以及暫存區域。

Git 倉庫目錄是 Git 用來保存項目的元數據和對象數據庫的地方。 這是 Git 中最重要的部分,從其它計算機克隆倉庫時,拷貝的就是這裏的數據。

工做目錄是對項目的某個版本獨立提取出來的內容。 這些從 Git 倉庫的壓縮數據庫中提取出來的文件,放在磁盤上供你使用或修改。

暫存區域是一個文件,保存了下次將提交的文件列表信息,通常在 Git 倉庫目錄中。 有時候也被稱做`‘索引’',不過通常說法仍是叫暫存區域。

基本的 Git 工做流程以下:

  1. 在工做目錄中修改文件。

  2. 暫存文件,將文件的快照放入暫存區域。

  3. 提交更新,找到暫存區域的文件,將快照永久性存儲到 Git 倉庫目錄。

若是 Git 目錄中保存着的特定版本文件,就屬於已提交狀態。 若是做了修改並已放入暫存區域,就屬於已暫存狀態。 若是自上次取出後,做了修改但尚未放到暫存區域,就是已修改狀態。

 

標籤(tag)

像其餘版本控制系統(VCS)同樣,Git 能夠給歷史中的某一個提交打上標籤,以示重要。 比較有表明性的是人們會使用這個功能來標記發佈結點(v1.0 等等)。

Git 使用兩種主要類型的標籤:輕量標籤(lightweight)與附註標籤(annotated)。

一個輕量標籤很像一個不會改變的分支 - 它只是一個特定提交的引用。(這點與svn不一樣svn的標籤是會移動的)

然而,附註標籤是存儲在 Git 數據庫中的一個完整對象。 它們是能夠被校驗的;其中包含打標籤者的名字、電子郵件地址、日期時間;還有一個標籤信息;而且可使用 GNU Privacy Guard (GPG)簽名與驗證。 一般建議建立附註標籤,這樣你能夠擁有以上全部信息;可是若是你只是想用一個臨時的標籤,或者由於某些緣由不想要保存那些信息,輕量標籤也是可用的。

 

提交(commit)

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

從單次提交角度來看,提交(commit)指向了一個快照文件目錄樹(tree),目錄樹指向了多個文件快照(blob)。這樣咱們就能夠經過提交找到本次提交的全部文件快照了。

從提交流角度來看,提交對象會包含一個指向上次提交對象(父對象)的指針。

下圖展現了提交即關聯本身的快照又關聯父提交。

提交的這些模式最終組成了Git的提交樹結構。

每一個倉庫擁有惟一的提交樹結構,分支和標籤只不過是指向某個節點的指針而已。

 

分支(branch)

幾乎全部的版本控制系統都以某種形式支持分支。 使用分支意味着你能夠把你的工做從開發主線上分離開來,以避免影響開發主線。 在不少版本控制系統中(svn),這是一個略微低效的過程——經常須要徹底建立一個源代碼目錄的副本。對於大項目來講,這樣的過程會耗費不少時間。

結合提交部分的說明,咱們知道Git僅有一個提交樹。而Git 的分支其實本質上僅僅是指向提交對象的可變指針。 Git 的默認分支名字是 master。 在屢次提交操做以後,你其實已經有一個指向最後那個提交對象的 master 分支。 它會在每次的提交操做中自動向前移動。

Git 是怎麼建立新分支的呢? 很簡單,它只是爲你建立了一個能夠移動的新的指針。

那麼,Git 又是怎麼知道當前在哪個分支上呢? 也很簡單,它有一個名爲 HEAD 的特殊指針。 請注意它和許多其它版本控制系統(如 Subversion 或 CVS)裏的 HEAD 概念徹底不一樣。 在 Git 中,它是一個指針,指向當前所在的本地分支(譯註:將 HEAD 想象爲當前分支的別名)。 

切換分支就是將HEAD指針指向另外一另外一個本地分支。

若是咱們從一次提交建立不一樣分支,以後又分別在這些分支上作出了新的提交。這時項目將會產生分叉提交歷史,多個提交將指向同一個父提交。這些分叉若是想要最終合併回原來的分支,咱們就要經過合併或變基操做來解決。

遠程分支

遠程引用是對遠程倉庫的引用(指針),包括分支、標籤等等。
遠程跟蹤分支是遠程分支狀態的引用。 它們是你不能移動的本地引用,當你作任何網絡通訊操做時,它們會自動移動。 遠程跟蹤分支像是你上次鏈接到遠程倉庫時,那些分支所處狀態的書籤。
它們以 (remote)/(branch) 形式命名。 例如,若是你想要看你最後一次與遠程倉庫 origin 通訊時 master
分支的狀態,你能夠查看 origin/master 分支。

 

跟蹤分支

從一個遠程跟蹤分支檢出一個本地分支會自動建立一個叫作 「跟蹤分支」(有時候也叫作 「上游分支」)。 跟蹤分支是與遠程分支有直接關係的本地分支。 若是在一個跟蹤分支上輸入 git pullgit push,Git 能自動地識別去哪一個服務器上抓取、合併到哪一個分支。

當克隆一個倉庫時,它一般會自動地建立一個跟蹤 origin/master 的 master 分支。 然而,若是你願意的話能夠設置其餘的跟蹤分支 - 其餘遠程倉庫上的跟蹤分支,或者不跟蹤 master 分支。

跟蹤分支信息通常保存在local級別的配置文件當中

branch.master.remote=origin
branch.master.merge=refs/heads/master
branch.dev_5.2.remote=origin
branch.dev_5.2.merge=refs/heads/dev_5.2

 

合併(merge)

咱們想將出現分叉提交的分支整合在一塊兒時,可使用合併(merge)操做來完成。

Git 會使用兩個分支的末端所指的快照以及這兩個分支的工做祖先,作一個簡單的三方合併。

和以前將分支指針向前推動所不一樣的是,Git 將這次三方合併的結果作了一個新的快照而且自動建立一個新的提交指向它。 這個被稱做一次合併提交,它的特別之處在於他有不止一個父提交。

須要指出的是,Git 會自行決定選取哪個提交做爲最優的共同祖先,並以此做爲合併的基礎;這和更加古老的 CVS 系統或者 Subversion (1.5 版本以前)不一樣,在這些古老的版本管理系統中,用戶須要本身選擇最佳的合併基礎。 Git 的這個優點使其在合併操做上比其餘系統要簡單不少。

因此咱們能夠將合併理解爲,從分叉提交中提取所有快照整合在一塊兒,最後作一次新的提交。而且新提交擁有多個父提交。

 

快進合併(fast-forward

當你試圖合併兩個分支時,若是順着一個分支走下去可以到達另外一個分支,那麼 Git 在合併二者的時候,只會簡單的將指針向前推動(指針右移),由於這種狀況下的合併操做沒有須要解決的分歧——這就叫作 「快進(fast-forward)」。

master只要向前推動就能夠完成與iss53的合併,因此會使用快進合併。

 

合併衝突

有時候合併操做不會如此順利。 若是你在兩個不一樣的分支中,對同一個文件的同一個部分進行了不一樣的修改,Git 就無法乾淨的合併它們。在合併它們的時候就會產生合併衝突。

此時 Git 作了合併,可是沒有自動地建立一個新的合併提交。 Git 會暫停下來,等待你去解決合併產生的衝突。等你手動解決以後,Git 會詢問剛纔的合併是否成功。 若是你回答是,Git 會暫存那些文件以代表衝突已解決。

若是你對結果感到滿意,而且肯定以前有衝突的的文件都已經暫存了,這時你能夠輸入 git commit 來完成合並提交。 從而產生一次新的合併提交。

與無衝突合併的區別主要有:

一、須要手動解決衝突並標記已解決。

二、須要本身提交新的合併提交。

 

變基(rebase)

在 Git 中整合來自不一樣分支的修改主要有兩種方法:merge 以及 rebase

你能夠提取在一個分支中引入的補丁和修改,而後在另外一個分支的基礎上應用一次。 在 Git 中,這種操做就叫作 變基。 你可使用變基將提交到某一分支上的全部修改都移至另外一分支上,就好像「從新播放」同樣。

它的原理是首先找到這兩個分支(即當前分支、變基操做的目標基底分支)的最近共同祖先,而後對比當前分支相對於該祖先的歷次提交,提取相應的修改並存爲臨時文件,而後將當前分支指向目標基底, 最後以此將以前另存爲臨時文件的修改依序應用。

呃,奇妙的變基也並不是天衣無縫,要用它得遵照一條準則:

不要對在你的倉庫外有副本的分支執行變基。

若是你遵循這條金科玉律,就不會出差錯。 不然,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。

(也就是說已經推送到遠程分支的內容就不要進行變基了,不然會對別人的形成困擾)

 

變基衝突

咱們知道合併時有可能產生衝突,而變基時仍然有可能產生衝突問題。

咱們在基底分支和補丁分支修改了同一個文件時就要手動進行衝突處理。

若是在變基時發現衝突,git會中止變基操做要求手動解決衝突。

手動解決衝突後,git會使用手動解決衝突的文件從新創建補丁提交。

 

交互式變基(interactive)

交互式變基主要用於將屢次提交合併成一次提交。

咱們一般會在完成一個功能時,合併雜亂的提交從而使提交樹更加簡潔。

交互式變基容許咱們自由的選擇提交,而且從新編輯提交說明。

 

變基 vs. 合併

至此,你已在實戰中學習了變基和合並的用法,你必定會想問,到底哪一種方式更好。 在回答這個問題以前,讓咱們退後一步,想討論一下提交歷史到底意味着什麼。

有一種觀點認爲,倉庫的提交歷史便是 記錄實際發生過什麼。 它是針對歷史的文檔,自己就有價值,不能亂改。 從這個角度看來,改變提交歷史是一種褻瀆,你使用_謊話_掩蓋了實際發生過的事情。 若是由合併產生的提交歷史是一團糟怎麼辦? 既然事實就是如此,那麼這些痕跡就應該被保留下來,讓後人可以查閱。

另外一種觀點則正好相反,他們認爲提交歷史是 項目過程當中發生的事。 沒人會出版一本書的初版草稿,軟件維護手冊也是須要反覆修訂才能方便使用。 持這一觀點的人會使用 rebase 及 filter-branch 等工具來編寫故事,怎麼方便後來的讀者就怎麼寫。

如今,讓咱們回到以前的問題上來,到底合併仍是變基好?但願你能明白,這並無一個簡單的答案。 Git 是一個很是強大的工具,它容許你對提交歷史作許多事情,但每一個團隊、每一個項目對此的需求並不相同。 既然你已經分別學習了二者的用法,相信你可以根據實際狀況做出明智的選擇。

總的原則是,只對還沒有推送或分享給別人的本地修改執行變基操做清理歷史,從不對已推送至別處的提交執行變基操做,這樣,你才能享受到兩種方式帶來的便利。

 

應用提交(cherry-pick)

cherry-pick容許咱們提取一個或多個現有的提交,並使用這些提交的快照來建立新的提交。

也就是說咱們可以提取某一次提交的變動,應用在其餘分支當中。

這個功能在處理生產bug時將會很是有用。若是咱們在開發分支正在進行開發時出現了一個生產bug,就須要建立一個bug分支。可是bug分支即要合併到開發分支進行測試,又要合併到生產分支解決問題,顯然使用分支合併方式沒法完美的解決這個問題。

上面這種狀況使用cherry-pick正合適。在bug分支修改完以後,咱們能夠將修復bug的提交分別cherry-pick到生產和開發分支。因爲使用cherry-pick建立的提交標識名都是一致的,在生產上線時執行變基操做並不會產生衝突,會完美的合併成一次提交。

須要注意的是若是咱們對cherry-pick的提交進行了交互式變基,那麼在合併的時候就沒法確認兩次提交的關係,會要求咱們手動合併。因此若是考慮到未來要將cherry-pick的兩個分支進行合併的話,最好仍是不要在cherry-pick提交上進行交互式變基操做。

 

儲藏(stash)

有時,當你在項目的一部分上已經工做一段時間後,全部東西都進入了混亂的狀態,而這時你想要切換到另外一個分支作一點別的事情。 問題是,你不想僅僅由於過會兒回到這一點而爲作了一半的工做建立一次提交。 針對這個問題的答案是 git stash 命令。

儲藏會處理工做目錄的髒的狀態 - 即,修改的跟蹤文件與暫存改動 - 而後將未完成的修改保存到一個棧上,而你能夠在任什麼時候候從新應用這些改動。

相關文章
相關標籤/搜索