Git之原理篇[懂了?]

Get Started

安裝完Git,讓咱們開始第一次Git之旅。在終端運行下面的命令前端

$git config --global user.name  "gaopo"
$git config --global user.email "gaopo@blook.me"
  
$git init git-demo
$cd git-demo
$echo "Git學習" > README.md
$git add README.md
$git commit -m 'init repo and add README.md'
$git log
複製代碼

執行完以上命令,咱們就已經創建了一個Git倉庫,而且將文件README.md交由Git管理。 鏈接遠程版本庫,把這個README.md分享給別人git

$git remote add origin git@gitlab.blook.me:fe/git-demo.git
$git push origin master
複製代碼
  • git config 設置 user.name user.email 只需在首次使用git時設置,以後就無需再設置了。 若是是首次鏈接使用遠程版本庫的話,須要把當前用戶的公鑰傳到遠程版本庫上,不然沒法使用ssh協議

基本使用

團隊開發中使用Git的基本流程:bash

  1. 克隆遠程版本庫
  2. 基於遠程develop分支創建本地develop分支
  3. 基於develop分支創建本地特性分支feature
  4. 在feature分支編寫程序
  5. 切換到develop分支,合併feature的修改
  6. 把本地develop分支的修改推到遠程develop

實際代碼ssh

$git clone git@gitlab.blook.me:fe/git-demo.git

cd git-demo
# 基於遠程develop分支創建本地develop分支
$git checkout -b develop origin/develop

# 基於develop分支創建本地特性分支feature
$git branch feature
$git checkout feature

# 在feature分支編寫程序 後提交
$git add README.md a.txt
$git commit -m 'add a.txt , change README.md'

# 切換到develop分支,合併feature的修改
$git checkout develop
$git pull
$git merge feature


# 把本地develop分支的修改推到遠程develop
$git push
複製代碼

Git中的對象及背後原理

目錄分類

  • Git最重要的概念就是工做區,暫存區,版本庫,Git對象。
  • 執行 git init 或 git clone 以後會生成一個目錄,這個目錄叫作項目目錄。
  • 在項目目錄下有一個Git目錄,除了Git目錄以外的都是工做目錄

在咱們的例子中對應關係以下。gitlab

項目目錄:git-demo學習

Git目錄:git-demo/.gitfetch

工做目錄:git-demo下除了.git目錄以外的所有ui

.git目錄詳解

'Git目錄'是項目存儲全部歷史和元信息的目錄 - 包括全部的對象(commits,trees,blobs,tags).url

$cd .git ; tree -L 1
|-- HEAD         # 記錄當前處在哪一個分支裏
|-- config       # 項目的配置信息,git config命令會改動它
|-- description  # 項目的描述信息
|-- hooks/       # 系統默認鉤子腳本目錄
|-- index        # 索引文件
|-- logs/        # 各個refs的歷史信息
|-- objects/     # Git本地倉庫的全部對象 (commits, trees, blobs, tags)
|-- refs/        # 標識每一個分支指向了哪一個提交(commit)。
複製代碼

這個.git目錄中還有幾個其餘的文件和目錄,但都不是很重要。不用太關注。spa

HEAD文件

HEAD文件就是一個只有一行信息的純文本文件。這行內容記錄的是當前頭指針的引用,一般是指向一個分支的引用 ,有時也是一個提交(commit)的SHA值

$ cat .git/HEAD
ref: refs/heads/master  #HEAD文件的內容只有這一行,代表當前處於master分支
$ git checkout dd98199
Note: checking out 'dd98199'.
  
You are in 'detached HEAD' state.
...
  
$ cat .git/HEAD
dd981999876726a1d31110479807e71bba979c44 #這種狀況是」頭指針分離「模式,不處於任何分支下。HEAD的值就是某一次commit的SHA
複製代碼

config文件

config文件記錄着項目的配置信息,也是一個普通的純文本文件。git config命令會改動它(固然也能夠手工編輯)。

這個文件裏面配置了當前這個版本庫的基本屬性信息,遠程版本庫信息,本地分支與遠程的映射關係,命令別名等。

總之是一個頗有用的文件。在你的.git目錄裏看到的config文件內容基本上是下面的樣子。

#基本配置
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
#上游版本庫
[remote "origin"]
    url = http://git.blook.me/fe/git-demo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
#本地分支與上游版本庫分支的映射
[branch "master"]
    remote = origin
    merge = refs/heads/master
#當前倉庫Git命令別名
[alias]
    st = status
複製代碼
  • 若是沒有添加遠程版本庫,[remote "origin"]和[branch "master"]是不存在的;若是沒有設置alias那麼[alias]也是不存在的。
  • 因此若是僅僅是git init以後的一個本地倉庫,那麼只有[core]配置項

hooks目錄

鉤子(hooks)是一些在.git/hooks目錄的腳本, 在被特定的事件觸發後被調用。當git init命令被 調用後, 一些很是有用的示例鉤子腳本被拷到新倉庫的hooks目錄中; 可是在默認狀況下它們是不生效的。 把這些鉤 子文件的".sample"文件名後綴去掉就可使它們生效。

  • 在前端提交前進行eslint,就是pre-commit
  • commit時對說明信息進行格式校驗,就是commit-msg
  • 若是沒有添加遠程版本庫,[remote "origin"]和[branch "master"]是不存在的;若是沒有設置alias那麼[alias]也是不存在的。
  • 因此若是僅僅是git init以後的一個本地倉庫,那麼只有[core]配置項

index文件

git暫存區存放index文件中,因此咱們把暫存區有時也叫做索引(index)。索引是一個二進制格式的文件,裏面存放了與當前暫存內容相關的信息,包括暫存的文件名、文件內容的SHA1哈希串值和文件訪問權限。暫存區是貫穿於整個Git使用流程的重要概念,因此index文件就很重要。因爲是二進制因此咱們沒法查看具體內容,可是能夠用git ls-files --stage 命令查看暫存區裏面的文件

$git ls-files --stage
100644 44601d12328ea8e04367337184dcccb85859610e 0    README.md
複製代碼

objects目錄

Git對象(blob,tree,commit,tag)都保存在objects目錄裏面,因此objects目錄就是真正的倉庫。objects裏面的目錄結構組織的頗有特色,是以SHA值的前2位做爲目錄,後38位做爲這個目錄下的文件名。

咱們的工做目錄裏的全部文件,代碼、庫文件、圖片等都會變成git對象存在這個objects目錄下。每一個文件都是一個二進制文件。能夠經過 git cat-file -p SHA值來查看文件的內容。

refs目錄

refs目錄下面是一些純文本文件,分別記錄着本地分支和遠程分支的SHA哈希值。文件的數量取決於分支的數量。

$tree refs
refs
├── heads
│   ├── develop     # 記錄本地develop分支的SHA哈希值
│   └── master      # 記錄本地master分支的SHA哈希值
├── remotes
│   └── origin
│       ├── develop # 記錄遠程版本庫develop分支的SHA哈希值
│       └── master  # 記錄遠程版本庫master分支的SHA哈希值
└── tags
    └── v1.0         # 記錄里程碑V1.0的SHA哈希值
複製代碼
  • 回想前面介紹的 HEAD文件, HEAD文件的內容記錄了當前處於哪一個分支,值是 ref: refs/heads/master 。
  • 而 refs/heads/master文件 記錄了master分支的最新提交的SHA哈希值 ,Git就是經過HEAD文件和refs/heads下面的文件來判斷當前分支及分支最新提交的。
$cat HEAD
ref: refs/heads/master                      # 說明當前處於master分支
  
$cat refs/heads/master
dd981999876726a1d31110479807e71bba979c44    # master分支的最新提交SHA哈希值
複製代碼

logs目錄

logs目錄下面是幾個純文本文件,分別保存着HEAD文件和refs文件內容的歷史變化。因爲HEAD文件和refs文件的內容就是SHA值,因此log文件的內容就是這些SHA值的變化歷史。

  • logs目錄的結構和refs幾乎同樣,都是分支名

  • 在實際使用中,咱們常常須要把代碼總體回滾到一個歷史狀態,這是須要用到 git reflog 命令,這個命令其實就是讀取的logs目錄裏的日誌文件。就是這麼簡單。

每次 commit 加一條記錄,發現是個鏈表結構,首尾相連。 到目前爲止 .git目錄 裏的重要文件目錄已經都介紹完了,你們掌握這些就能夠了,

Git對象存儲及管理

SHA

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

dd981999876726a1d31110479807e71bba979c44

你會在Git裏處處看到這種「40個字符」字符串。每個「對象名」都是對「對象」內容作SHA1哈希計算得來的。

這樣就意味着兩個不一樣內容的對象不可能有相同的「對象名」。

對象分類

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

BLOB

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

可使用 git show 或 git cat-file -p 命令來查看一個blob對象裏的內容。在咱們的例子中 README.md文件對應的 blob對象的SHA1哈希值是 44601d12328ea8e04367337184dcccb85859610e,咱們能夠經過下面的的命令來查看blob文件內容:

$ git show 44601d1
Git學習
複製代碼

能夠經過 git hash-object 命令生成文件的SHA哈希值,若是加上 -w 參數,會把這個文件生成blob對象並寫入對象庫。 hash-object 命令是個Git比較底層的命令,平時正常使用Git幾乎用不到。

$git hash-object README.md
44601d12328ea8e04367337184dcccb85859610e
#這隻會顯示READMD.md文件blob對象的SHA值,並不會生成blob文件。
#git hash-object -w README.md ,則會真正把README.md生blob對象並寫入對象庫
複製代碼
  • 總之,被Git管理的全部文件都會生成一個blob對象
  • 不須要寫完整40位的SHA哈希值,只寫前7位就能夠

TREE

「tree」有點像一個目錄,它管理一些「tree」或是 「blob」(就像文件和子目錄) git ls-tree 或 git cat-file -p 命令還能夠用來查看tree對象,如今咱們查看剛剛最新提交對應的Tree對象 咱們能夠像下面同樣來查看它:

$git ls-tree HEAD^{tree}
100644 blob 44601d12328ea8e04367337184dcccb85859610e    README.md
040000 tree 16a87dbed191bcfb19a4af9d0cc569f6448a01cc    script
複製代碼

就如同你所見,一個tree對象包括一串(list)條目,每個條目包括:mode、對象類型、SHA1值 和名字(這串條目是按名字排序的)。它用來表示一個目錄樹的內容。 一個tree對象能夠指向(reference): 一個包含文件內容的blob對象, 也能夠是其它包含某個子目錄內容的其它tree對象. Tree對象、blob對象和其它全部的對象同樣,都用其內容的SHA1哈希值來命名的;只有當兩個tree對象的內容徹底相同(包括其所指向全部子對象)時,它的名字纔會同樣,反之亦然。這樣就能讓Git僅僅經過比較兩個相關的tree對象的名字是否相同,來快速的判斷其內容是否不一樣。tree對象存儲的是指針(tree和blob的SHA哈希值),不存儲真正的對象。tree對象能夠理解爲就是一個目錄,目錄裏包含子目錄(tree的SHA值)和文件(blob的SHA值).而SHA值所對應的真正的對象文件存在 .git/objects下面。

COMMIT

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

  • 有時也叫作快照。"commit對象"還帶有相關的描述信息.

能夠用 git log -1 --pretty=raw 或 git show -s --pretty=raw 或 git cat-file -p <commit>

➜  rrc git:(develop) git cat-file -p HEAD^^
tree f829a37e520fde788a93067d5de87f53e39ffbda
parent 604c39395fbcd4d6ada81207d359974196741085
author gaopo <gaopo@renrenche.com> 1557138527 +0800
committer gaopo <gaopo@renrenche.com> 1557138527 +0800

feat(form): 添加password input
複製代碼
  • 提交(commit)由如下的部分組成: 一個 tree對象: tree對象的SHA1簽名, 表明着目錄在某一時間點的內容.

父提交 (parent(s)): 提交(commit)的SHA1簽名表明着當前提交前一步的項目歷史. 上面的那個例子就只有一個父對象; 合併的提交(merge commits)可能會有不僅一個父對象. 若是一個提交沒有父對象, 那麼咱們就叫它「根提交"(root commit), 它就表明着項目最初的一個版本(revision). 每一個項目必須有至少有一個「根提交"(root commit)。Git就是經過父提交把每一個提交聯繫起來,也就是咱們通常所說的提交歷史。父提交就是當前提交上一版本。

做者 : 作了這次修改的人的名字, 還有修改日期.

提交者(committer): 實際建立提交(commit)的人的名字, 同時也帶有提交日期. TA可能會和做者不是同一我的; 例如做者寫一個補丁(patch)並把它用郵件發給提交者, 由他來建立提交(commit).

提交說明 :用來描述這次提交.

一個提交(commit)自己並無包括任何信息來講明其作了哪些修改; 全部的修改(changes)都是經過與父提交(parents)的內容比較而得出的。 通常用 git commit 來建立一個提交(commit), 這個提交(commit)的父對象通常是當前分支(current HEAD), 同時把存儲在當前索引(index)的內容所有提交.

commit是使用頻率最高的對象,通常在使用Git時,咱們直接接觸的就是commit。咱們 commit代碼, merge代碼, pull / push代碼,重置版本庫,查看歷史,切換分支這些在開發流程中的基本操做都是直接和commit對象打交道。

TAG

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

一個標籤對象包括一個對象名, 對象類型, 標籤名, 標籤建立人的名字("tagger"), 還有一條可能包含有簽名(signature)的消息. 你能夠用 git cat-file -p 命令來查看這些信息。

  • 如今咱們的git對象庫裏尚未一個tag對象,咱們先用 git tag -m [] 命令建立一個tag。
$git tag -m 'create tag from demo' v1.0  #基於當前HEAD創建一個tag,因此tag指向的就是HEAD的引用
$git tag
v1.0
$git cat-file -p v1.0
object e6361ed35aa40f5bae8bd52867885a2055d60ea2
type commit
tag v1.0
tagger gp <gp@demo.com> 1494406971 +0800
  
create tag from demo

複製代碼

Tag對象就是里程碑的做用,通常在咱們正式發佈代碼是須要創建一個里程碑。

對象模型組合關係

如今咱們已經瞭解了3種主要對象類型(blob, tree 和 commit), 好如今就讓咱們大概瞭解一下它們怎麼組合到一塊兒的.

回憶一下如今項目的目錄結構:

$tree
.
 
├── README.md
 
└── script
 
    ├── perl
 
    │   └── test2.pl
 
    └── test1.sh
 
 
 
2 directories, 3 files
複製代碼

在Git中它們的存儲結構看起來就以下圖:

每一個目錄都建立了 tree對象, 每一個文件都建立了一個對應的 blob對象 . 最後有一個 commit對象 來指向根tree對象(root of trees), 這樣咱們就能夠追蹤項目每一項提交內容。除了第一個commit,每一個commit對象都有一個父commit對象,父commit就是上一次的提交(歷史 history),這樣就造成了一條提交歷史鏈。Git就是經過這種方式組成了git版本庫

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

工做區、暫存區、版本庫

Git對於咱們的代碼管理分了3個區域,分別是工做區,暫存區和版本庫。這是Git徹底不一樣於SVN的地方。Git之因此強大很大程度上就是由於它設計了3個區域,但同時也是由於這個設計讓Git學習起來比較難,上手也比較難,比較難理解。凡事都是有利有弊。

工做區(Working Directory):

就是電腦上的一個目錄,裏面是正在開發的工程代碼。執行git init 命令後,生成的這個目錄除了.git目錄,就是工做區。

暫存區(Stage / Index):

暫存區最很差理解的一個概念,能夠先認爲須要提交的文件要先放到暫存區才能提交。因此暫存區能夠理解爲「提交任務」。是代碼提交到版本庫前的一個緩衝區域。

  • 暫存區其實就是 .git/index文件

歷史庫(History):

Git的版本庫指的是本地倉庫,這裏面存放着文件的各個版本的數據。其實就是 .git/objects 目錄。Git對象都存在這個目錄裏

歷史庫,版本庫,Git倉庫,History,叫法不一樣而已,其實指的是同一回事。就是執行了 git commit 後,生成了commit對象。如今咱們知道commit對象包含了提交時間,提交人,提交說明以及提交時的目錄樹和父提交。那麼上面這些都是構成版本庫的要素。因爲Git管理的全部對象文件都在 .git/objects 目錄中,因此版本庫概念能夠具體形象的理解成 .git/objects 目錄裏面的對象文件。.git/objects 目錄就是版本庫,雖然這麼說不是十分的準確,可是很是便於記憶和理解。

暫存區,Stage,Cached,Index ,叫法不一樣而已,其實指的是同一回事。暫存區就是 .git/index 這個二進制文件,記錄着目錄和文件的引用(SHA1值),而不是真正的文件對象。真正的文件對象存在於 .git/objects目錄裏面。

工做區其實就沒啥可說的了,就是真實的,看的見摸獲得的,正在寫的代碼。就是你的IDE裏面打開的這一堆東西。

再次增強一下記憶: 暫存區就是 .git/index文件 ,版本庫就是 .git/objects目錄

三區關係

下面的圖例展現了3個區域的關係以及涉及到的主要命令 :git add , git commit , git reset , git checkout

由於有了這3個區域,在使用Git時,文件常常處於不一樣的狀態:

  • 未被跟蹤的文件(untracked file)
  • 已被跟蹤的文件(tracked file)
    • 被修改但未被暫存的文件(changed but not updated或modified)
    • 已暫存能夠被提交的文件(changes to be committed 或staged)
    • 自上次提交以來,未修改的文件(clean 或 unmodified)

命令 git diff 用來進行具體文件的變更對比,一般用來進行工做區與暫存區之間的對比,實質上是用 git objects 庫中的快照與工做區文件的內容的對比。

add,commit和暫存區的關係

git add 把當前工做目錄中的文件放入暫存區域。

準確的說法是 git add files 作了兩件事:

將本地文件的時間戳、長度,當前文檔對象的id等信息保存到一個樹形目錄中去(.git/index,即暫存區)

將本地文件的內容作快照並保存到Git 的對象庫(.git/object) 。

從命令的角度來看,git add 能夠分兩條底層命令實現:

  1. git hash-object
  2. git update-index --add
$git hash-object a.txt
$git update-index --add a.txt
#以上兩條命令等價於 git add a.txt
複製代碼

git commit 命令就是生成一個新的提交,主要乾了這麼幾件事:

  1. 生成一個commit對象
  2. 把暫存區的目錄樹寫到版本庫中,也就是生成一個tree對象,這個tree裏面的引用和暫存區index裏的引用同樣
  3. 跟新當前分支ref文件的引用,指向最新生成的這個commit的SHA1哈希值

git reset HEAD 命令,暫存區的目錄樹會被重寫,被最新提交的目錄樹所替換,可是工做區不受影響。

「git checkout .」 或者 「git checkout -- <file>」 命令,會用暫存區所有或指定的文件替換工做區的文件。這個操做很危險,會清除工做區中未添加到暫存區的改動。

「git checkout HEAD .」 或者 「git checkout HEAD <file>」 命令,會用最新提交的所有或者部分文件替換暫存區和以及工做區中的文件。這個命令也是極具危險性的,由於不但會清除工做區中未提交的改動,也會清除暫存區中未提交的改動。

恭喜!Git的原理篇就到此。

敬請期待……

  • 命令詳解及實戰
相關文章
相關標籤/搜索