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的各種命令。本文大多數內容來源於互聯網,是一個知識收集和理解總結性的文章,希望能真正幫助到大家。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM