1. 程式人生 > >Git的原理簡介和常用命令

Git的原理簡介和常用命令

開發十年,就只剩下這套架構體系了! >>>   

Git和SVN是我們最常用的版本控制系(Version Control System, VCS),當然,除了這二者之外還有許多其他的VCS,例如早期的CVS等。顧名思義,版本控制系統主要就是控制、協調各個版本的文件內容的一致性,這些文件包括但不限於程式碼檔案、圖片檔案等等。早期SVN佔據了絕大部分市場,而後來隨著Git的出現,越來越多的人選擇將它作為版本控制工具,社群也越來越強大。相較於SVN,最核心的區別是Git是分散式的VCS,簡而言之,每一個你pull下來的Git倉庫都是主倉庫的一個分散式版本,倉庫的內容完全一樣,而SVN則不然,它需要一箇中央版本庫來進行集中控制。採用分散式模式的好處便是你不再依賴於網路,當有更改需要提交的時候而你又無法連線網路時,你只需要把更改提交到本地的Git倉庫,最後有網路的時候再把本地倉庫和遠端的主倉庫進行同步即可。當然,分散式和非分散式各有各的優缺點,但是目前來看,分散式的Git正逐漸被越來越多的人所接受並推廣。本文主要對Git的基本原理和常用命令進行簡介,試圖從底層來說明Git是如何工作的,從而幫助大家理解上層命令在執行的時候背後所產生的動作和變化。原理部分的內容可以參考

Pro Git做進一步的瞭解,而常用的命令可以參考其他的資料。本文的總結根據自己的理解進行描述,如果錯誤,請不吝賜教。

Git的基本原理

本質上,Git是一套內容定址(content-addressable)檔案系統,而和我們直接接觸的Git介面,只不過是封裝在其之上的一個應用層。這個關係頗有點類似於計算機網路中應用層和下屬層的關係。在Git中,那些和應用層相關的命令(也就是我們最常用的命令,如git commit、 git push等),我們稱之為porcelain命令(瓷器之意,意為成品、高階命令);而和底層相關的命令(幾乎不會在日常中使用,如git hash-object、git update-index等),則稱之為plumbing命令(管道之意,是連線git應用介面和git底層實現的一個管道,類似於shell,底層命令)。要了解Git的底層原理,就需要了解Git是如何利用底層命令來實現高層命令的。在此之前,讓我們先來看一下Git的目錄結構,和各個檔案在Git中的作用。

Git的目錄結構

在作業系統中,我們的倉庫就是一個資料夾。但是為什麼這些資料夾就是Git倉庫呢?這是因為Git在初始化的時候會生成一個.git的資料夾,而Git進行版本控制所需要的檔案,則都放在這個資料夾中。在桌面上新建一個目錄,然後利用命令列在該目錄下執行git init命令即可完成git倉庫的初始化。如果這個時候你看不到.git目錄,這是因為你的作業系統自動隱藏了該資料夾,需要在系統設定中設定隱藏檔案可見。進入.git目錄,便可以看到其中有很多的檔案和資料夾,這每一個檔案都有各自的作用,下面結合圖1來進行說明。

圖1 .git目錄結構示意圖

在上圖中,第一排的幾個檔案和資料夾是Git的核心,而第二排的則是一些不需要特別關注的。核心檔案包括:config檔案、objects資料夾、HEAD檔案、index檔案以及refs資料夾。下面依次對其進行說明。

  • config檔案:該檔案主要記錄針對該專案的一些配置資訊,例如是否以bare方式初始化、remote的資訊等,通過git remote add命令增加的遠端分支的資訊就儲存在這裡;

  • objects資料夾:該資料夾主要包含git物件。關於什麼是git物件,將會在下一節進行詳細介紹。Git中的檔案和一些操作都會以git物件來儲存,git物件分為BLOB、tree和commit三種類型,例如git commit便是git中的commit物件,而各個版本之間是通過版本樹來組織的,比如當前的HEAD會指向某個commit物件,而該commit物件又會指向幾個BLOB物件或者tree物件。objects資料夾中會包含很多的子資料夾,其中Git物件儲存在以其sha-1值的前兩位為子資料夾、後38位位檔名的檔案中;除此以外,Git為了節省儲存物件所佔用的磁碟空間,會定期對Git物件進行壓縮和打包,其中pack資料夾用於儲存打包壓縮的物件,而info資料夾用於從打包的檔案中查詢git物件;

  • HEAD檔案:該檔案指明瞭git branch(即當前分支)的結果,比如當前分支是master,則該檔案就會指向master,但是並不是儲存一個master字串,而是分支在refs中的表示,例如ref: refs/heads/master。

  • index檔案:該檔案儲存了暫存區域的資訊。該檔案某種程度就是緩衝區(staging area),內容包括它指向的檔案的時間戳、檔名、sha1值等;

  • Refs資料夾:該資料夾儲存指向資料(分支)的提交物件的指標。其中heads資料夾儲存本地每一個分支最近一次commit的sha-1值(也就是commit物件的sha-1值),每個分支一個檔案;remotes資料夾則記錄你最後一次和每一個遠端倉庫的通訊,Git會把你最後一次推送到這個remote的每個分支的值都記錄在這個資料夾中;tag資料夾則是分支的別名,這裡不需要對其有過多的瞭解;

除此以外,.git目錄下還有很多其他的檔案和資料夾,這些檔案和資料夾會額外支撐一些其他的功能,但是不是Git的核心部分,因此稍作了解即可。hooks主要定義了客戶端或服務端鉤子指令碼,這些指令碼主要用於在特定的命令和操作之前或者之後進行特定的處理,比如:當你把本地倉庫push到伺服器的遠端倉庫時,可以在伺服器倉庫的hooks資料夾下定義post_update指令碼,在該指令碼中可以通過指令碼程式碼將最新的程式碼部署到伺服器的web伺服器上,從而將版本控制和程式碼釋出無縫連線起來;description檔案僅供GitWeb程式使用,這裡不需要過多的關心;logs則記錄了本地倉庫和遠端倉庫的每一個分支的提交記錄,即所有的commit物件(包括時間、作者等資訊)都會被記錄在這個資料夾中,因此這個資料夾中的內容是我們檢視最頻繁的,不管是Git log命令還是tortoiseGit的show log,都需要從該資料夾中獲取提交日誌;info資料夾儲存了一份不希望在.gitignore 檔案中管理的忽略模式的全域性可執行檔案,基本也用不上;COMMIT_EDITMSG檔案則記錄了最後一次提交時的註釋資訊。從以上的描述中我們可以發現,.git資料夾中包含了眾多功能不一的資料夾和檔案,這些資料夾和檔案是描述Git倉庫所必不可少的資訊,不可以隨意更改或刪除;尤其需要注意的是,.git資料夾隨著專案的演進,可能會變得越來越大,因為任何檔案的任何一個變動,都需要Git在objects資料夾下將其重新儲存為一個新的物件檔案,因此如果一個檔案非常大,那麼你提交幾次改動就會造成.git資料夾容量成倍增長。因此,.git資料夾更像是一本書,每一個版本的每一個變動都儲存在這本書中,而且這本書還有一個目錄,指明瞭不同的版本的變動內容儲存在這本書的哪一頁上,這就是Git的最基本的原理。

從底層命令理解Git

上節中我們講到,Git分為porcelain命令和plumbing命令,而porcelain命令是基於plumbing來實現的。為了進一步的理解Git的底層原理,我們將在這一節中詳細的探討Git物件的儲存格式以及plumbing命令。如果把Git比作Linux作業系統,那plumbing命令就有點類似於shell命令,而上層的procelain命令便是利用shell命令編寫的一系列的系統功能或工具,如你自定義的自動化運維工具等。在接下來的介紹中,我們將試著如何利用plumbing命令,而不是porcelain命令,來完成Git的暫存和提交工作,並利用log檢視提交記錄。首先,我們從Git的物件介紹開始。

Git物件

在之前我們提到過,Git是一套內容定址(content-addressable)檔案系統,那麼Git是怎麼進行定址呢?其實,定址無非就是查詢,而Git採用HashTable的方式進行查詢,也就是說,Git只是通過簡單的儲存鍵值對(key-value pair)的方式來實現內容定址的,而key就是檔案(頭+內容)的雜湊值(採用sha-1的方式,40位),value就是經過壓縮後的檔案內容。因此,在接下來的實踐中,我們會經常通過40位的hash值來進行plumbing操作,幾乎每一個plumbing命令都需要通過key來指定所要操作的物件。

Git物件的型別包括:BLOB、tree物件、commit物件。BLOB物件可以儲存幾乎所有的檔案型別,全稱為binary large object,顧名思義,就是大的二進位制表示的物件,這種物件型別和資料庫中的BLOB型別(經常用來在資料庫中儲存圖片、視訊等)是一樣的,當作一種資料型別即可;tree物件是用來組織BLOB物件的一種資料型別,你完全可以把它想象成二叉樹中的樹節點,只不過Git中的樹不是二叉樹,而是"多叉樹";commit物件表示每一次的提交操作,由tree物件衍生,每一個commit物件表示一次提交,在建立的過程中可以指定該commit物件的父節點,這樣所有的commit操作便可以連線在一起,而這些commit物件便組成了提交樹,branch只不過是這個樹中的某一個子樹罷了。如果你能理解commit樹,那Git幾乎就已經理解了一半了。

Git物件的儲存方式也很簡單,基本可以用如下表達式來表示:

Key = sha1(file_header + file_content)

Value = zlib(file_content)

簡單來說,Git 將檔案頭與原始資料內容拼接起來,並計算拼接後的新內容的 40位的sha-1校驗和,將該校驗和的前2位作為object目錄中的子目錄的名稱,後38位作為子目錄中的檔名;然後,Git 用zlib的方式對資料內容進行壓縮,最後將用 zlib 壓縮後的內容寫入磁碟。檔案頭的格式為 "blob #{content.length}\0",例如"blob 16\000",這種檔案頭格式也是經常採用的格式。對於tree物件和commit物件,檔案頭的格式都是一樣的,但是其檔案資料卻是有固定格式的,鑑於本次只是Git原理的基本介紹,這裡不再詳細描述,有興趣的可以去Git的官網查詢相關文件進行了解;其實也可以自己按照理解構思一下,如果讓你來設計這種格式,應該如何設計:tree物件類似於樹中節點的定義,在tree物件中要包含對連線的BLOB物件的引用,而commit物件與tree物件類似,要包含提交的tree物件的引用,想到這裡,我覺得文件的閱讀大概也就可以省去了。

物件暫存區

在procelain命令中,為了將修改的檔案加入暫存區(也叫索引庫,將修改的檔案key-value化,.git根目錄下的index檔案記錄該暫存區中的檔案索引),我們會使用git add filename命令。那麼在git add這個命令的背後,Git是如何使用plumbing命令來完成檔案的索引操作呢?其實,git add命令對應著兩個基本的plumbing命令:

git hash-object #獲取指定檔案的key,如果帶上-w選項,則會將該物件的value進行儲存

git update-index #將指定的object加入索引庫,需要帶上—add選項

因此,git add命令在plumbing命令中其實是分成了兩步:首先,通過hash-object命令將需要暫存的檔案進行key-value化轉換成Git物件,並進行儲存,拿到這些檔案的key;然後,通過update-index命令將這些物件加入到索引庫進行暫存,這樣便完成了Git檔案的暫存操作。如果要根據Git物件的key來檢視檔案的資訊,還需要涉及下面的一個plumbing命令:

git cat-file –p/-t key #獲取指定key的物件資訊,-p列印詳細資訊,-t列印物件的型別

利用該命令可以檢視已經key-value化的Git物件的詳細資訊。

    接下來,我們利用plumbing命令來進行git add的實踐。首先,新建一個Git倉庫,通過在新建的資料夾中利用git init命令來初始化,這裡不再詳述,如下圖所示:

初始化之後,會在當前目錄下生成.git目錄,進入該目錄,就會發現我們上述的目錄結構。然後,我們新建一個version.txt檔案並在檔案中寫入"version 1"字串,這是version.txt的第一個版本,然後利用git hash-object –w命令將該檔案轉換為Git的物件並存儲,如下圖:

這裡hash-objec命令會返回該Git物件的key值,這時到.git目錄的objects目錄下會發現,多了一個6c子目錄,該目錄中的檔名稱為58b76a52188643965f3a6704166e8e0424b7fe,也就是該key值的後38位。記下該key值,因為我們要根據該key值將該物件加入索引庫。接著,我們利用update-index命令進行索引化操作,如下圖:

注意,這裡一定要帶上—add選項,而—cacheinfo選項則指出該檔案的檔案型別,100644表示普通檔案,與之相關的還有可執行檔案等等;並且,除了指定key值,還需要指定檔名,表明要把哪個檔案的哪個版本加入索引庫。該命令執行完成後,可以發現.git目錄下多了index檔案,並且在以後每次update-index命令執行之後,該index檔案的內容都會發生變化。至此,git add的主要過程也便完成了。

    這裡我們簡單談一下index檔案。index是一個索引檔案,存放的是暫存區的整個目錄樹的資訊,並且為目錄樹中的每個檔案都儲存了時間戳和長度。如果用UltraEdit開啟使用過程中的index檔案,可以發現index的格式為以下形式:

Index魔數(DIRC) + 版本號 + 暫存的檔案個數 + 每個檔案的時間戳和長度

Index索引庫記錄從專案初始化到目前為止,專案倉庫中所有檔案最後一次修改時刻的時間戳以及對應的長度資訊,因此隨著加入倉庫中的檔案不斷增多,index檔案也會不斷增大。每次呼叫git add命令,都會把add的檔案的索引資訊(時間戳和大小)進行更新,而我們所使用的git status命令,則會把每一個檔案的索引資訊和上次提交的索引資訊進行比較,如果發生了變化,就會顯示出來。Pro git 中是這樣描述暫存操作的:暫存操作會對每一個檔案計算校驗和(即第一章中提到的 SHA-1 雜湊字串),然後把當前版本的檔案快照儲存到 Git 倉庫中(Git 使用 blob 型別的物件儲存這些快照),並將校驗和加入暫存區域。意思很明確,也就是每個檔案對應的當前版本的key也會加入到index檔案中,這個我沒有進行驗證,不過理論上講應該是正確的。

建立樹節點

在Git中,所有的內容以tree或者BLOB物件進行儲存,如果把Git比作UNIX的檔案系統,則tree物件對應於UNIX檔案系統中的目錄,而BLOB物件則對應於inodes或檔案內容。在Git物件小節中,我們大致猜想了tree物件的儲存格式。其實,一個單獨的tree物件包含一條或多條tree記錄,每一條記錄含有一個指向BLOB物件或子tree物件的sha-1指標(也就是一個40位的key值),並附有該物件的許可權模式 、型別和檔名資訊,因此,我們的猜想也是八九不離十的。為什麼要建立tree物件呢?我們都知道,在Git中,我們add完已修改的檔案之後,一般就直接commit暫存區中的內容到本地倉庫了,似乎並沒有tree這個概念。其實,建立tree物件只是add和commit中間的一個緩衝步驟,因為commit物件要根據tree物件來建立。那麼如何建立tree物件呢?只需要如下命令即可:

git write-tree #根據索引庫中的資訊建立tree物件

該命令返回所建立的tree物件的key值,通過git cat-file可以檢視該物件的詳細資訊。建立過程如下圖:

從圖中可以看出,cat-file –t顯示該物件的型別為tree,表明該tree物件建立成功了,至此,樹節點便建立完成了。

    實際上,由於index暫存區包括了專案倉庫中所有的檔案,因此commit物件所對應的tree物件,永遠都是工作目錄的根tree物件。也就是說,每次commit,都是把工作目錄的根目錄所對應的tree物件,連結給此次的commit物件;而且,在Git中,每個子目錄都對應一個tree物件,每個檔案對應一個BLOB物件,因此整個工作目錄對應一棵Git物件樹,根節點就是commit物件所引用的tree節點,而每個子資料夾又分別對應一棵子樹。所以任何一個檔案的更改,都會導致其上層所有父物件的更改和重新儲存。這裡不再進行演示,你可以通過git add和git commit進行多次提交,並在每次提交之後使用git log檢視commit物件的key,使用cat-file獲取對應的tree物件的key,並再次使用cat-file獲取該tree物件下所有的子物件,這時你可以發現,子資料夾都對應一個tree節點,檔案都對應一個BLOB節點。

Commit物件

在Git中,每一次commit都對應一個commit物件,而一個commit物件對應一個tree物件。為了建立commit物件,需要使用如下命令:

git commit-tree key –p key2 #根據tree物件建立commit物件,-p表示前繼commit物件

該方法有點類似於資料結構中樹的增加節點操作:都是向父節點中增加子節點。其中,-p選項指明瞭前繼commit物件的key值,也就是父節點的key值,這樣,這兩個commit節點便連線在了一起,而不斷的連線便構成了一棵樹,也就是我們接下來要講的提交樹。Commit物件的建立過程如下所示:

在該命令中,我們只需要指定key的前六位即可,由於這是第一次提交,因此不需要帶上-p選項來指明父節點。通過cat-file命令可以看到,commit物件已建立成功,該commit物件中包含了與之關聯的tree物件的key值,以及author和committer的資訊。如果要檢視完整的提交記錄,可以通過git log –stat key命令,該命令會列印指定commit物件之前的所有提交記錄。至此,commit物件已經建立完成,而我們也利用plumbing命令,完整的實現了Git的add和commit操作,Cool。到目前為止,所建立的所有物件的關係如下圖所示:

圖2 第一次提交後Git物件關係圖

提交樹Commit Tree

接下來,我們在第一次提交的基礎上完成第二次提交和第三次提交。第二次提交我們會提交version.txt的第二個版本,並增加一個新的檔案;第三次提交會演示在tree物件中構造子tree物件並提交。在下面的每一次提交中,我們還需要指定每一次提交的前繼提交物件,這樣commit物件便連線在一起,形成一棵提交樹。首先,我們進行第二個版本的修改和提交。如下圖,修改version.txt並新增一個new.txt檔案,然後利用上面的方法進行key-value化和索引更新:

然後進行索引的更新:

然後我們利用暫存區建立tree物件,並根據該tree物件建立commit物件,如下圖所示。注意,本次commit需要利用-p選項指定此次commit物件的前繼commit物件,可以看到,通過git log命令打印出來的commit物件,連線在了一起。

本次提交完成後,Git中的物件關係如下圖所示:

圖3 第二次提交後Git物件關係圖

緊接著,我們來進行第三次提交。首先,利用read-tree命令將第一個版本中的tree物件讀入暫存區。如下圖所示:

注意,在讀取的過程中,需要加上—prefix選項,否則無法成功讀取,這是因為在index中相同路徑的檔案只能出現一次,由於version.txt已經存在於index索引庫了,因此如果想把第一個版本的tree物件讀取進來,需要將該版本的version.txt放在資料夾bak中。然後建立tree物件並進行第三次提交,如下圖所示:

通過git log可以檢視所有的commit物件。這個時候,通過cat-file命令檢視此次建立的tree物件所包含的內容:

可以看到,所建立的tree物件還不僅包括以上的兩個BLOB物件,還包括剛才讀取的子tree物件,這個時候如果把這個tree再匯出成工作目錄的話,則在根目錄會多出一個bak子資料夾。經過第三次提交後,Git中的所有物件的關係如圖4所示。

    注意,這裡加上這樣的步驟只是為了讓大家明白tree物件中的子tree物件的存在,正如上面上節所說的,整個工作目錄對應一個tree物件,並且其下的每一個子資料夾都是一個tree物件,每次的commit物件都對應著根tree物件,而任何一個物件的改變都會導致其上層所有tree物件的重新儲存。

    以上,便是我們利用plumbing命令完成的三次提交的過程,希望通過這幾個步驟,能讓你簡單的理解porcelain命令和plumbing命令之間的聯絡,為接下來的Git學習做鋪墊。

圖4 第三次提交後Git物件關係圖

Git的常用命令

本節的目的在於對Git中比較重要但是不太會經常使用的命令進行一個簡要的介紹,從而讓大家對Git中大部門命令都有一個整體的瞭解。Git中的基本命令的使用這裡不再贅述,整體的工作流程如圖所示。如果對Git的分支還不是很瞭解的話,建議去仔細閱讀下Pro Git的第三章。Git的基本工作流程如圖5所示。其中,git pull、git push、git fetch、git remote等基本命令的使用這裡不再進行贅述,這些基本的命令是最重要的命令,請務必牢牢掌握。建議通過以上的基本原理的講解和Pro Git的描述對各個基礎命令背後所發生的變化進行詳細的思考,以加深自己對Git應用層命令的認識。不要僅僅把自己侷限於tortoiseGit的GUI的使用中,只有深入的理解了工具,才有可能用好它。

本節重點對以下幾個git命令進行介紹,重點在於對這些命令的基本使用的普及,包括:git log、git fork、git rebase、git reset、git reverse和git stash。大多數情況下,我們在開發中小型專案的時候,如果團隊成員不是很多,則只需要開一個分支就夠了。在這種情況下,只要你操作規範,在push之前注意pull最新的程式碼,則基本不會出現比較嚴重的衝突或者問題,這時候以上命令基本都用不上,但是在多分支的情況下,我們可能會使用以上的命令來進行分支合併或者版本回退等,因此,我們有必要對這些命令做一個簡單的瞭解,知道在什麼時候去使用它們。

圖5 Git的基本工作流程圖

Git log

在提交了若干更新之後,又或者克隆了某個專案,想回顧下提交歷史,可以使用 git log 命令檢視。預設不用任何引數的話,git log會按提交時間列出所有的更新,最近的更新排在最上面。一般情況下,我會使用如下命令來列印log中的提交日誌記錄:

git log --pretty=format:"%h %s" --graph

其中。--pretty選項指定列印的格式,%h表示列出每個提交物件的短的sha1值(40位中的前6位);--graph選項表示使用圖的方式來列印日誌記錄。列印的結果如下圖所示:

也可以使用Git的GUI來顯示Git的提交歷史,在倉庫中右鍵選擇Git GUI,然後選擇選單欄上的 repository-->visual all branch history 選項,即可以顯示所有分支的提交記錄。如下圖所示:

Git fork

Git fork不是一個Git命令,而是一種工作流。它不是使用單個服務端倉庫作為『中央』程式碼基線,而讓各個開發者都有一個服務端倉庫,這意味著各個程式碼貢獻者有2個Git倉庫而不是1個:一個本地私有的,另一個服務端公開的,如下圖所示。

Forking工作流的一個主要優勢是,貢獻的程式碼可以被整合,而不需要所有人都能push程式碼到僅有的中央倉庫中。 開發者push到自己的服務端倉庫,而只有專案維護者才能push到正式倉庫。 這樣專案維護者可以接受任何開發者的提交,但無需給他正式程式碼庫的寫許可權。

Git rebase

把一個分支中的修改整合到另一個分支的辦法有兩種,第一種是我們常用的git merge操作,而第二種便是本節要講的rebase(中文翻譯為衍合)。該命令的原理是,回到兩個分支最近的共同祖先,根據當前分支(也就是要進行衍合的分支experiment)後續的歷次提交物件(這裡只有一個 C3),生成一系列檔案補丁,然後以基底分支(也就是主幹分支master)最後一個提交物件(C4)為新的出發點,逐個應用之前準備好的補丁檔案,最後會生成一個新的合併提交物件(C3'),從而改寫 experiment 的提交歷史,使它成為 master 分支的直接下游。如下圖所示:

一般我們使用rebase的目的,是想要得到一個能在遠端分支上乾淨應用的補丁,比如某些專案你不是維護者,但想幫點忙的話,最好用rebase:先在自己的一個分支裡進行開發,當準備向主專案提交補丁的時候,根據最新的 origin/master 進行一次衍合操作然後再提交,這樣維護者就不需要做任何整合工作(實際上是把解決分支補丁同最新主幹程式碼之間衝突的責任,化轉為由提交補丁的人來解決),只需根據你提供的倉庫地址作一次快進合併,或者直接採納你提交的補丁。

在rebase的過程中,也許會出現衝突。在這種情況,Git會停止rebase並會讓你去解決衝突;在解決完衝突後,用git add命令去更新這些內容的索引, 然後,你無需執行git-commit,只要執行git rebase –continue,這樣git會繼續應用(apply)餘下的補丁。如果要捨棄本次衍合,只需要git rebase --abort即可。切記,一旦分支中的提交物件釋出到公共倉庫,就千萬不要對該分支進行rebase操作

我們在使用git pull命令的時候,可以使用--rebase引數,即git pull --rebase。這裡表示把你的本地當前分支裡的每個提交取消掉,並且把它們臨時儲存為補丁(這些補丁放到.git/rebase目錄中),然後把本地當前分支更新為最新的origin分支,最後把儲存的這些補丁應用到本地當前分支上。在使用tortoise的pull的過程中,如果你留意tortoiseGit的日誌的話,你就會發現,它使用的就是這種方式來pull最新的提交的。

Git reset

在使用Git的過程中,由於操作不當,作為初學者的我們可能經常要去解決衝突。某些時候,當你不小心改錯了內容,或者錯誤地提交了某些commit,我們就需要進行版本的回退。版本回退最常用的命令包括git reset和git revert。這兩個命令允許我們在版本的歷史之間穿梭。

下面就幾種比較經典的場景進行總結:

  • 場景1:當你改亂了工作區某個檔案的內容,想直接丟棄工作區的修改時,用命git checkout -- filename;

  • 場景2:當你不但改亂了工作區某個檔案的內容,還新增到了暫存區時,想丟棄修改,分兩步,第一步用命令git reset HEAD file,就回到了場景1,第二步按場景1操作;

  • 場景3:已經提交了不合適的修改到版本庫時,想要撤銷本次提交,使用git reset --hard commit_id,不過前提是沒有推送到遠端庫。

穿梭前,用git log可以檢視提交歷史,以便確定要回退到哪個版本;要重返未來,用git reflog檢視命令歷史,以便確定要回到未來的哪個版本。

Git revert

Git revert用來撤銷某次操作,此次操作之前和之後的commit和history都會保留,並且把這次撤銷作為一次最新的提交。git revert是提交一個新的版本,將需要revert的版本的內容再反向修改回去,版本會遞增,不影響之前提交的內容。

Git revert和git reset都可以進行版本的回退,將工作區回退到歷史的某個狀態,二者有如下的區別:

  • git revert是用一次新的commit來回滾之前的commit,而git reset是直接刪除指定的commit(並沒有真正的刪除,通過git reflog可以找回),這是二者最顯著的區別;

  • git reset 是把HEAD向後移動了一下,而git revert是HEAD繼續前進,只是新的commit的內容和要revert的內容正好相反,能夠抵消要被revert的內容;

  • 在回滾這一操作上,效果差不多。但是在日後繼續merge以前的老版本時有區別。因為git revert是用一次逆向的commit"中和"之前的提交,因此日後合併老的branch時,導致這部分改變不會再次出現;但是git reset是之間把某些commit在某個branch上刪除,因而和老的branch再次merge時,這些被回滾的commit應該還會被引入。

Git stash

Git stash用來暫存當前正在進行的工作, 將工作區還沒加入索引庫的內容壓入本地的Git棧中,在需要應用的時候再彈出來。比如想pull 最新程式碼,又不想加新commit;或者為了修復一個緊急的bug,先stash,使返回到自己上一個commit,改完bug之後再stash pop,繼續原來的工作。Git stash可以讓本地倉庫返回到上一個提交狀態,而本地的還未提交的內容則被壓入Git棧。Git stash的基本使用流程如下:

git stash #暫存工作區尚未提交的內容

Do your work #在上一個提交的狀態之上完成你的操作

git stash pop #將暫存的內容彈出並應用

    當你多次使用git stash命令後,你的棧裡將充滿了未提交的程式碼,這時候你會對將哪個版本應用回來有些困惑,這時git stash list命令可以將當前的Git棧資訊打印出來,你只需要將找到對應的版本號,例如使用 git stash apply stash@{1} 就可以將你指定版本號為stash@{1}的暫存內容取出來,當你將所有的棧都應用回來的時候,可以使用git stash clear來將棧清空。TortoiseGit中的stash save選單就對應該命令。

總結

本文主要對Git的基本原理和常用命令進行介紹和知識普及。從Git的目錄結構,到porcelain命令和plumbing命令,到利用plumbing命令完成commit實踐,最後對一些比較重要的命令進行說明,希望閱讀完本文,你能對Git的原理有整體的認識,同時能夠靈活的使用Git的各種命令。本文大多數內容來源於網際網路,是一個知識收集和理解總結性的文章,希望能