Git-深刻一點點

本文來自半個月前我在我司內部進行的分享。一直以爲 Git 是一個很是值得深刻學習的工具,準備此次內部分享用了好久的時間,不過關於Git的講解仍是有不少不足之處,你們有什麼建議,歡迎來本文的 githug地址討論,咱們一塊兒把 Git 學得更深一點。

Git是一個CLI(Common line interface),咱們與其的交互經常發生在命令行,(固然有時候也會使用GUI,如sourcetree,Github等等),因爲咱們的使用方式,咱們經常會忽略git倉庫自己是一個沒那麼複雜的文件系統,咱們輸入git命令時其實就是對這個文件系統進行操做。html

對 Git 文件系統的定義有一種更專業的說法,「從根本上來說 Git 是一個內容尋址(content-addressable)文件系統,並在此之上提供了一個版本控制系統的用戶界面」。git

…from [Git - 關於版本控制](https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6)

Git作爲文件系統長什麼樣子

找一個空文件夾,執行git init後咱們會發現其中會多出一個隱藏文件夾.git,其文件結構以下:github

➜ mkdir gitDemo && cd gitDemo && git init && tree -a
Initialized empty Git repository in /Users/zhangwang/Documents/personal/Test/gitDemo/.git/
.
└── .git
    ├── HEAD
    ├── branches
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

10 directories, 14 files

幾乎 Git 相關的全部操做都和這個文件夾相關,若是你是第一次見到這個文件系統,以爲陌生也很正常,不過讀完本文,每一項都會變得清晰了。算法

咱們先想另一個問題,作爲版本控制系統的 Git ,究竟會存儲那些內容在上述文件系統中,這些內容又是如何被存儲的呢?數據庫

「分支」,「commit」,「原始的文件」,「diff」…緩存

…from 熱烈的討論中

好吧,不賣關子,實際上在上述文件系統中 Git 爲咱們存儲了五種對象,這些對象存儲在/objects/refs文件夾中。安全

Git 中存儲的五種對象

  • Blobs, which are the most basic data type in Git. Essentially, a blob is just a bunch of bytes; usually a binary representation of a file.
Blobs是Git中最基礎的數據類型,一個 blob對象就是一堆字節,一般是一個文件的二進制表示
  • Tree objects, which are a bit like directories. Tree objects can contain pointers to blobs and other tree objects.
tree,有點相似於目錄,其內容由對其它 treeblobs的指向構成;
  • Commit objects, which point to a single tree object, and contain some metadata including the commit author and any parent commits.
commit,指向一個樹對象,幷包含一些代表做者及父 commit 的元數據
  • Tag objects, which point to a single commit object, and contain some metadata.
Tag,指向一個commit對象,幷包含一些元數據
  • References, which are pointers to a single object (usually a commit or tag object).
References,指向一個 commit或者 tag對象

blobs , tree , commit ,以及聲明式的 tag 這四種對象會存儲在 .git/object 文件夾中。這些對象的名稱是一段40位的哈希值,此名稱由其內容依據sha-1算法生成,具體到.git/object文件夾下,會取該hash值的前 2 位爲子文件夾名稱,剩餘 38 位爲文件名,這四類對象都是二進制文件,其內容格式依據類型有所不一樣。下面咱們一項項來看:bash

Blobs

咱們都經常使用git add這個命令,也都據說過,此命令會把文件添加到緩存區(index)。可是有沒有想過「把文件添加到緩存區」是一種很奇怪的說法,若是說這個文件咱們曾經add過,爲何咱們須要在修改事後再次添加到緩存區?服務器

咱們確實須要把文件從新添加到緩存區,其實每次修改後的文件,對 git 來講都是一個新文件,每次 add 一個文件,就會添加一個 Blob 對象。併發

blobs是二進制文件,咱們不能直接查看,不過經過 Git 提供的一些更底層的命令如 git show [hash] 或者 git cat-file -p [hash] 咱們就能夠查看 .git/object 文件夾下任一文件的內容。

➜ git cat-file -p 47ca
abc
456

從上面的內容中就能夠看出,blob 對象中僅僅存儲了文件的內容,若是咱們想要完整還原工做區的內容,咱們還須要把這些文件有序組合起來,這就涉及到 Git 中存儲的另一個重要的對象:tree

Tree objects

tree 對象記錄了咱們的文件結構,更形象的說法是,某個 tree 對象記錄了某個文件夾的結構,包含文件以及子文件夾。tree 對象的名稱也是一個40位的哈希值,文件名依據內容生成,所以若是一個文件夾中的結構有所改變,在 .git/object/ 中就會出現一個新的 tree object, 一個典型的 tree object 的內容以下:

➜ git ls-tree bb4a8638f1431e9832cfe149d7f32f31ebaa77ef
100644 blob 4be9cb419da86f9cbdc6d2ad4db763999a0b86f2    .gitignore
040000 tree dccea6a66df035ac506ab8ca6d2735f9b64f66c1    01_introduction_to_algorithms
040000 tree 363813a5406b072ec65867c6189e6894b152a7e5    02_selection_sort
040000 tree 5efc07910021b8a2de0291218cb1ec2555d06589    03_recursion
040000 tree cc15fd67f464c29495437aa81868be67cd9688b2    04_quicksort
040000 tree 9f09206e367567bf3fe0f9b96f3609eb929840f1    05_hash_tables
040000 tree c8b7b793b0318d13b25098548effde96fc9f1377    06_breadth-first_search
040000 tree 7f111006c8a37eab06a3d8931e83b00463ae0518    07_dijkstras_algorithm
040000 tree 9f6d831e5880716e0eda2d9312ea2689a8cc1439    08_greedy_algorithms
040000 tree 692a9b39721744730ad1b29c052e288aeb89c2ac    09_dynamic_programming
100644 blob 290689b29c24d3406a1ed863077a01393ae2aff3    LICENSE
100644 blob 9017b1121945799e97825f996bc0cefe3422cbaf    README.md
040000 tree ce710aa0b6c23b7f81dbd582aad6f9435988a8b4    images

咱們能夠看過,tree 中包含兩種類型的文件,treeblob,這就把文件有序的組合起來了,若是咱們知道了根 tree(能夠理解爲root文件夾對應的tree),咱們就有能力依據此tree還原整個工做區。

可能咱們很早就據說過 Git 中的每個 commit 存儲的都是一個「快照」。理解了tree對象,咱們就能夠較容易的理解「快照」這個詞了 ,接下來咱們看看 commit object

commit object

咱們知道,commit記錄了咱們的提交歷史,存儲着提交時的 message,Git 分支中的一個個的節點也是由 commit 構成。一個典型的 commit object 內容以下:

➜ git cat-file -p e655
tree 73aff116086bc78a29fd31ab3fbd7d73913cf958
parent 8da64ce1d90be7e40d6bad5dd1cb1a3c135806a2
author zhangwang <zhangwang2014@iCloud.com> 1521620446 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1521620446 +0800

bc

咱們來看看其中每一項的意義:

  • tree:告訴咱們當前 commit 對應的根 tree,依據此值咱們還原此 commit 對應的工做區;
  • parent:父 commit 的 hash 值,依據此值,咱們能夠記錄提交歷史;
  • author:記錄着此commit的修改內容由誰修改;
  • committer:記錄着當前 commit 由誰提交;
  • ...bc: commit message;

commit 經常位於 Git 分支上,分支每每也是由咱們主動添加的,Git 提供了一種名爲 References 的對象供咱們存儲「類分支」資源。

References

References 對象存儲在/git/refs/文件夾下,該文件夾結構以下:

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

其中 heads 文件夾中的每個文件其實就對應着一條本地分支,已咱們最熟悉的 master 分支爲例,咱們看看其中的內容:

➜ cat .git/refs/heads/master
603bdb03d7134bbcaf3f84b21c9dbe902cce0e79

有沒有發現,文件 master 中的內容看起來好眼熟,它實際上是就是一個指針,指向當前分支最新的 commit 對象。因此說 Git 中的分支是很是輕量級的,弄清分支在 Git 內部是這樣存儲以後,也許咱們能夠更容易理解相似下面這種圖了。

咱們再看看 .git/refs 文件夾中其它的內容:

  • .git/refs/remotes 中記錄着遠程倉庫分支的本地映射,其內容只讀;
  • .git/refs/stashgit stash 命令相關,後文會詳細講解;
  • .git/refs/tag, 輕量級的tag,與 git tag 命令相關,它也是一個指向某個commit 對象的指針;
tag是一種輔助 Git 作版本控制的對象,上面這種 tag 只是「輕量級tag」 ,此外還存在另外一種「聲明式tag」,聲明式 tag 對象能夠存儲更多的信息,其存在於 .git/object/下。

Tag objects

上文已經說過 Git 中存在兩種 tag

  • lightweight tags,輕量標籤很像一個不會改變的分支,其內容是對一個特定提交的引用,這種 tag 存儲在.git/refs/tag/文件夾下;
  • annotated tags: 聲明式的標籤會在object下添加tag object,此種 tag 能記錄更多的信息;

兩種 tag 的內容差異較大:

# lightweight tags
$ git tag 0.1
# 指向添加tag時的commit hash值
➜ cat 0.1 
e9f249828f3b6d31b895f7bc3588df7abe5cfeee

# annotated tags
$ git tag -a -m 'Tagged1.0' 1.0
➜ git cat-file -p 52c2
object e9f249828f3b6d31b895f7bc3588df7abe5cfeee
type commit
tag 1.0
tagger zhangwang <zhangwang2014@iCloud.com> 1521625083 +0800

Tagged1.0

對比能夠發現,聲明式的 tag 不只記錄了對應的 commit ,標籤號,額外還記錄了打標籤的人,並且還能夠額外添加 tag message(上面的-m 'Tagged1.0')。

值得額外說明的是,默認狀況下, git push 命令並不會推送標籤到遠程倉庫服務器上。 想要傳送,必須顯式地推送標籤到共享服務器上。 推送方法爲 git push origin [tagname],若是要推送全部的標籤,可使用 git push origin --tags

另外咱們也能夠在後期給某次 commit 打上標籤,如:git tag -a v1.2 9fceb02

至此,咱們已經理解了 Git 中的這幾類資源,接下來咱們看看 Git 命令是如何操做這些資源的。

常見git命令與上述資源間的映射

依據場景,咱們能夠粗略按照操做的是本地倉庫仍是遠程倉庫,把 Git 命令分爲本地命令和遠程命令,咱們先看本地命令,咱們本地可供操做的 Git 倉庫每每是經過 git clone 或者 git init 生成。咱們先看git init作了些什麼。

本地命令

git init && git init --bare

git init:在當前文件夾下新建一個本地倉庫,在文件系統上表現爲在當前文件夾中新增一個 .git 的隱藏文件夾
如:

gitDemo on  master 
➜ ls -a
.     ..    .git  a.txt data

Git 中還存在一種被稱爲裸倉庫的特殊倉庫,使用命令 git init --bare 能夠初始化一個裸倉庫

其目錄結構以下:

➜ mkdir gitDemoBear && cd gitDemoBear && git init --bare && tree
Initialized empty Git repository in some/path/gitDemoBear/
.
├── branches
├── hooks
├── info
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

9 directories, 14 files

和普通倉庫相比,裸倉庫沒有工做區,因此並不會存在在裸倉庫上直接提交變動的狀況,這種倉庫會直接把 .git 文件夾中的內容置於初始化的文件夾下。此外

在 config 文件下咱們會看到 bare = true 這代表當前倉庫是一個裸倉庫:

# normal
    bare = false
    logallrefupdates = true

# bare
    bare = true
普通的方法是不能修改裸倉庫中的內容的。裸倉庫只容許貢獻者 clone, push, pull

git add

咱們都知道 git add [file] 會把文件添加到緩存區。那緩存區本質上是什麼呢?

爲了理清這個問題,咱們先看下圖:

image.png

不少地方會說,git 命令操做的是三棵樹。三棵樹對應的就是上圖中的工做區( working directory )、緩存區( Index )、以及 HEAD。

工做區比較好理解,就是可供咱們直接修改的區域,HEAD 實際上是一個指針,指向最近的一次 commit 對象,這個咱們以後會詳述。Index 就是咱們說的緩存區了,它是下次 commit 涉及到的全部文件的列表。

回到git add [file],這個命令會依次作下面兩件事情:

  1. .git/object/ 文件夾中添加修改或者新增文件對應的 blob 對象;
  2. .git/index 文件夾中寫入該文件的名稱及對應的 blob 對象名稱;

經過命令 git ls-files -s 能夠查看全部位於.git/index中的文件,以下:

➜ git ls-files -s
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0    a.txt
100644 aceb8a25000b1c680a1a83c032daff4d800c8b95 0    b.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0    c.txt
100644 0932cc0d381ab943f3618e6125995f643cad4425 0    data/d.txt

其中各項的含義以下:

  • 100644100表明regular file,644表明文件權限
  • 8baef1b4abc478178b004d62031cf7fe6db6f903:blob對象的名稱;
  • 0:當前文件的版本,若是出現衝突,咱們會看到12
  • data/d.txt: 該文件的完整路徑

Git 還額外提供了一個命令來幫我咱們查看文件在這三棵樹中的狀態,git status

git status

git status有三個做用:

  1. 查看當前所在分支;
  2. 列出已經緩存,未緩存,未追蹤的文件(依據上文中的三棵樹生成);
  3. 給下一步的操做必定的提示;

通常來講 .git/HEAD 文件中存儲着 Git 倉庫當前位於的分支:

➜ cat .git/HEAD
ref: refs/heads/mate-school--encodeUri

當咱們 git add 某個文件後,git 下一步每每會提示咱們commit它。咱們接下來看看,commit過程發生了什麼。

git commit

對應到文件層面,git commit作了以下幾件事情:

  1. 新增tree對象,有多少個修改過的文件夾,就會添加多少個tree對象;
  2. 新增commit對象,其中的的tree指向最頂端的tree,此外還包含一些其它的元信息,commit對象中的內容,上文已經見到過, tree對象中會包含一級目錄下的子tree對象及blob對象,由此可構建當前commit的文檔快照;;
經過 git cat-file -p hash可查看某個對象中的內容
經過 git cat-file -t hash可查看某個對象的類型

當咱們 git add 某個文件後,下一步咱們每每須要執行 git commit 。接下來咱們看看,commit過程發生了什麼。

git branch

前文咱們提到過,分支在本質上僅僅是「指向提交對象的可變指針」,其內容爲所指對象校驗和(長度爲 40 的 SHA-1 值字符串)的文件(一個commit對象),因此分支的建立和銷燬都異常高效,建立一個新分支就至關於往一個文件中寫入 41 個字節(40 個字符和 1 個換行符),足見 Git 的分支多麼輕量級。
此外上文中提到的 HEAD 也能夠看作一個指向當前所在的本地分支的特殊指針。
在開發過程當中咱們會建立不少分支,全部的分支都存在於.git/refs文件夾中。

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

➜ cat heads/feature
0cdc9f42882f032c5a556d32ed4d8f9f5af182ed

存在兩種分支,本地分支遠程分支
本地分支:

對應存儲在 .git/refs/heads中;
還存在一種叫作「跟蹤分支」(也叫「上游分支」)的本地分支,此類分支從一個遠程跟蹤分支檢出,是與遠程分支有直接關係的本地分支。 若是在一個跟蹤分支上輸入 git pull,Git 能自動地識別去那個遠程倉庫上的那個分支抓取併合並代碼。

遠程分支:

對應存儲在 .git/refs/remotes中,能夠看作遠程倉庫的分支在本地的備份,其內容在本地是隻讀的。

.git/config文件中信息進一步指明瞭遠程分支與本地分支之間的關係:

➜ cat .git/config
...
[remote "origin"]
    url = git@git.in.zhihu.com:zhangwang/zhihu-lite.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[remote "wxa"]
    url = https://git.in.zhihu.com/wxa/zhihu-lite.git
    fetch = +refs/heads/*:refs/remotes/wxa/*

使用 git branch [newBranchName] 能夠建立新分支 newBranchName。不過一個更常見的用法是git checkout -b [newBranchName],此命令在本地建立了分支 newBranchName,並切換到了分支 newBranchName。咱們看看git checkout 究竟作了些什麼

git checkout

還記得前面咱們提到過的HEAD嗎?git checkout 實際上就是在操做HEAD
前文中咱們提到過通常狀況下 .git/HEAD 指向本地倉庫當前操做的分支。那只是通常狀況,更準確的說法是 .git/HEAD 直接或者間接指向某個 commit 對象。
咱們知道每個 commit 對象都對應着一個快照。可依據其恢復本地的工做目錄。 HEAD 指向的 commit 是判斷工做區有何更改的基礎。
Git 中有一個比較難理解的概念叫作「HEAD分離」,映射到文件層面,其實指的是 .git/HEAD 直接指向某個commit對象。
咱們來看git checkout的具體用法

  1. git checkout <file>:

此命令能夠用來清除未緩存的更改,它能夠看作是 git checkout HEAD <file> 的簡寫,
映射到文件層面,其操做爲恢復文件<file>的內容爲,HEAD對應的快照時的內容。其不會影響已經緩存的更改的緣由在於,其實緩存過的文件就是另一個文件啦。
相應的命令還有 git checkout <commit> <file> 能夠用來恢復某文件爲某個提交時的狀態。

  1. git checkout <branch>

切換分支到 <branch> 其其實是修改 .git/HEAD 中的內容爲 <branch>,更新工做區內容爲 <branch> 所指向的 commit 對象的內容。

➜ cat .git/HEAD
ref: refs/heads/master
  1. git checkout <hash|tag>

HEAD直接指向一個commit對象,更新工做區內容爲該commit對象對應的快照,此時爲HEAD分離狀態,切換到其它分支或者新建分支git branch -b new-branch|| git checkout branch可使得HEAD再也不分離。

➜ cat .git/HEAD
8e1dbd367283a34a57cb226d23417b95122e5754

在分支上進行了一些操做後,下一步咱們要作的就是合併不一樣分支上的代碼了,接下來咱們看看git merge 是如何工做的。

git merge

Git 中分支合併有兩種算法,快速向前合併三路合併

快速向前合併:

此種狀況下,主分支沒有改動,所以在基於主分支生成的分支上作的更改,必定不會和主分支上的代碼衝突,能夠直接合並,在底層至關於修改 .refs/heads/ 下主分支的內容爲最新的 commit 對象。

image.png

三路合併:

新的feature分支在開發過程當中,主分支上的代碼也作了修改並添加了新的 commit ,此時合併,須要對比 feature 分支上最新的 commit,feature 分支的 base commit 以及 master 分支上最新的 commit 這三個commit的快照。若是一切順利,這種合併會生成新的合併 commit ,格式以下:
➜ git cat-file -p 43cfbd24b7812b7cde0ca2799b5e3305bd66a9b3
tree 78f3bc25445be087a08c75ca62ca1708a9d2e33a
parent 51b45f5892f640b8e9b1fec2f91a99e0d855c077
parent 96e66a5b587b074d834f50d6f6b526395b1598e5
author zhangwang <zhangwang2014@iCloud.com> 1521714339 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1521714339 +0800

Merge branch 'feature'

和普通的 commit 對象的區別在於其有兩個parent,分別指向被合併的兩個commit

不過三路合併每每沒有那麼順利,每每會有衝突,此時須要咱們解決完衝突後,再合併,三路合併的詳細過程以下(爲了敘述便利,假設合併發生在 master 分支與 feature 分支之間):

  1. Git 將接收 commit 的哈希值寫入文件 .git/MERGE_HEAD。此文件的存在說明 Git 正在作合併操做。(記錄合併提交的狀態)
  2. Git 查找 base commit:被合併的兩個分支的第一個共有祖先 commit
  3. Git 基於 base commitmaster commitfeature commit 建立索引;
  4. Git 基於 base commit — master commitbase commit — feature commit 分別生成 diff,diff 是一個包含文件路徑的列表,其中包含添加、移除、修改或衝突等變化;
  5. Git 將 diff 應用到工做區;
  6. Git 將 diff 應用到 index,若是某文件有衝突,其在index中將存在三份;
  7. 若是存在衝突,須要手動解決衝突
  8. git add 以更新 index 被提交, git commit基於此 index 生成新的commit;
  9. 將主分支.git/refs/heads/master中的內容指向第8步中新生成的 commit,至此三路合併完成;

git cherry-pick(待進一步補充)

Git 中的一些命令是以引入的變動即提交這樣的概念爲中心的,這樣一系列的提交,就是一系列的補丁。 這些命令以這樣的方式來管理你的分支。
git cherry-pick作的事情是將一個或者多個commit應用到當前commit的頂部,複製commit,會保留對應的二進制文件,可是會修改parent信息。

image.png

在D commit上執行,git cherry-pick F 會將F複製一份到D上,複製的緣由在於,F的父commit變了,可是內容又須要保持不可變。

一個常見的工做流以下:

$ git checkout master
$ git checkout -b foo-tmp
$ git cherry-pick C D
# 將foo指向foo-tmp,reset將HEAD指向了某個特殊的commit
$ git checkout foo
$ git reset --hard foo-tmp
$ git branch -D foo-tmp

git revert 命令本質上就是一個逆向的 git cherry-pick 操做。 它將你提交中的變動的以徹底相反的方式的應用到一個新建立的提交中,本質上就是撤銷或者倒轉。

有時候咱們會想要撤銷一些commit,這時候咱們就會用到git reset

git reset

git reset 具備如下常見用法:

  1. git reset <file>:從緩存區移除特定文件,可是不會改變工做區的內容
  2. git reset : 重設緩存區,會取消全部文件的緩存
  3. git reset --hard : 重置緩存區和工做區,修改其內容對最新的一次 commit 對應的內容
  4. git reset <commit> : 移動當前分支的末端到指定的commit
  5. git reset --hard <commit>: 重置緩存區和工做區,修改其內容爲指定 commit 對應的內容

相對而言,git reset是一個相對危險的操做,其危險之處在於可能會讓本地的修改丟失,可能會讓分支歷史難以尋找。

咱們看看git reset的原理

  1. 移動HEAD所指向的分支的指向:若是你正在 master 分支上工做,執行 git reset 9e5e64a 將會修改 master 讓指向 哈希值爲 9e5e64acommit object
  • 不管你是怎麼使用的git reset,上述過程都會發生,不一樣用法的區別在於會如何修改工做區及緩存區的內容,若是你用的是 git reset --soft,將僅僅執行上述過程;
  • git reset本質上是撤銷了上一次的 git commit 命令。
執行 git commit ,Git 會建立一個新的 commit 對象,並移動 HEAD 所指向的分支指向該commit。 而執行 git reset會修改 HEAD 所指向的分支指向 HEAD~(HEAD 的父提交),也就是把該分支的指向修改成原來的指向,此過程不會改變 index和工做目錄的內容。
  1. 加上 —mixed 會更新索引:git reset --mixedgit reset 效果一致,這是git reset的默認選項,此命令除了會撤銷一上次提交外,還會重置index,至關於咱們回滾到了 git addgit commit 前的狀態。
  2. 添加—hard會修改工做目錄中的內容:除了發生上述過程外,還會恢復工做區爲 上一個 commit對應的快照的內容,換句話說,是會清空工做區所作的任何更改。
—hard 能夠算是 reset 命令惟一的危險用法,使用它會真的銷燬數據。

若是你給 git reset 指定了一個路徑,git reset 將會跳過第 1 步,將它的做用範圍限定爲指定的文件或文件夾。 此時分支指向不會移動,不過索引和工做目錄的內容則能夠完成局部的更改,會只針對這些內容執行上述的第 二、3 步。

git reset file.txt 實際上是 git reset --mixed HEAD file.txt 的簡寫形式,他會修改當前 index看起來像 HEAD 對應的 commit所依據的索引,所以能夠達到取消文件緩存的做用。

git stash

有時候,咱們在新分支上的feature開發到一半的時候接到通知須要去修復一個線上的緊急bug?,這時候新feature還達不到該提交的程度,命令git stash就派上了用場。

git stash被用來保存當前分支的工做狀態,便於再次切換回本分支時恢復。其具體用法以下:

  1. feature分支上執行git stash 或 git stash save,保存當前分支的工做狀態;
  2. 切換到其它分支,修復bug,並提交
  3. 切換回feature分支,執行git stash list,列出保存的全部stash,執行 git stash apply,恢復最新的stash到工做區;
也能夠覆蓋老一些的 stash, 用法如 git stash apply stash@{2};

關於git stash還有其它一些值得關注的點:

  1. 直接執行git stash會恢復全部以前的文件到工做區,也就是說以前添加到緩存區的文件不會再存在於緩存區,使用 git stash apply --index 命令,則能夠恢復工做區和緩存區與以前同樣;
  2. 默認狀況下,git stash 只會儲藏已經在索引中的文件。 使用 git stash —include-untrackedgit stash -u 命令,Git 纔會將任何未跟蹤的文件添加到stash;
  3. 使用命令git stash pop 命令能夠用來應用最新的stash,並當即從stash棧上扔掉它;
  4. 使用命令 git stash —patch ,可觸發交互式stash會提示哪些改動想要儲藏、哪些改動須要保存在工做目錄中。
➜ git stash --patch
diff --git a/src/pages/index/index.mina b/src/pages/index/index.mina
index 6e11ce3..038163c 100644
--- a/src/pages/index/index.mina
+++ b/src/pages/index/index.mina
@@ -326,6 +326,7 @@ Page<Props, Data, {}>({
   },

   onPageScroll({scrollTop}) {
+    // abc
 //    TODO: cover-view 的 fixed top 樣式和 pullDownRefresh 有嚴重衝突。
 //    當 bug 解決時,能夠在 TabNav 內使用 <cover-view> 配合滾動實現 iOS 的磁鐵效果

Stash this hunk [y,n,q,a,d,/,e,?]?
  1. 使用命令git stash branch <new branch>:構建一個名爲new branch的新分支,並將stash中的內容寫入該分支

說完了git stash的基本用法,咱們來看看,其在底層的實現原理:

上文中咱們提到過,Git 操做的是 工做區,緩存區及 HEAD 三棵文件樹,咱們也知道,commit 中包含的根 tree 對象指向,能夠看作文檔樹的快照。

當咱們執行git stash時,實際上咱們就是依據工做區,緩存區及HEAD這三棵文件樹分別生成commit對象,以後以這三個commit 爲 parent 生成新的 commit對象,表明這次stash,並把這個 commit 的 hash值存到.git/refs/stash中。

當咱們執行git stash apply時,就能夠依據存在 .git/refs/stash 文件中的 commit 對象找到 stash 時工做區,緩存區及HEAD這三棵文件樹的狀態,進而能夠恢復其內容。

gitDemo on  master [$]
➜ cat .git/refs/stash
68e5413895acd479daad0c96815cdb69a3c61bef

gitDemo on  master [$]
➜ git cat-file -p 68e5
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent aade8236c7c291f927f0be3f51ae57f5388eafcc
parent 408ef43aacaf7c255a0c3ea4f82196626a28a39b
parent 6bacdafcddf0685d8e4a0b364ea346ff209a87be
author zhangwang <zhangwang2014@iCloud.com> 1522397172 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1522397172 +0800

WIP on master: aade823 first commit
暫留的疑問?
.git/refs/stash文件中只存有最新的 stash commit值, git stash list是如何生效的。

git clean

使用git clean命令能夠去除冗餘文件或者清理工做目錄。 使用git clean -f -d命令能夠用來移除工做目錄中全部未追蹤的文件以及空的子目錄。

此命令真的會從工做目錄中移除未被追蹤的文件。 所以若是你改變主意了,不必定能找回來那些文件的內容。 一個更安全的命令是運行 git stash --all 來移除每一項更新,可是能夠從stash棧中找到並恢復它們。。

git clean -n 命令能夠告訴咱們git clean的結果是什麼,以下:

$ git clean -d -n
Would remove test.o
Would remove tmp/

全部在不知道 git clean 命令的後果是什麼的時候,不要使用-f,推薦先使用 -n 來看看會有什麼後果。

講到這裏,經常使用的操做本地倉庫的命令就基本上說完了,下面咱們看看 Git 提供的一些操做遠程倉庫的命令。

遠程命令

若是咱們是中途加入某個項目,每每咱們的開發會創建在已有的倉庫之上。若是使用github或者gitlab,像已有倉庫提交代碼的常見工做流是

  1. fork一份主倉庫的代碼到本身的遠程倉庫;
  2. clone 本身遠程倉庫代碼到本地;
  3. 添加主倉庫爲本地倉庫的遠程倉庫,git remote add ...,便於以後保持本地倉庫與主倉庫同步git pull
  4. 在本地分支上完成開發,推送本地分支到我的遠程倉庫某分支git push
  5. 基於我的遠程倉庫的分支向主倉庫對應分支提交MR,待review經過合併代碼到主倉庫;

這期間涉及不少遠程命令,咱們接觸到的第一個命令極可能是git clone,咱們先看這個命令作了些什麼

git clone

git clone的通常用法爲git clone <url>
<url>部分支持四種協議:本地協議(Local),HTTP 協議,SSH(Secure Shell)協議及 Git 協議。典型的用法以下:

$ git clone git://github.com/schacon/ticgit.git
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 193.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.

git clone作了如下三件事情

  1. 複製遠程倉庫objects/文件夾中的內容到本地倉庫; (對應Receiving objects);
  2. 爲所接收到的文件建立索引(對應Resolving deltas);
  3. 爲全部的遠程分支建立本地的跟蹤分支,存儲在.git/refs/remote/xxx/下;
  4. 檢測遠程分支上當前的活躍分支(.git/HEAD文件中存儲的內容);
  5. 在當前分支上執行git pull,保證當前分支和工做區與遠程分支一致;
參考 What is git actually doing when it says it is 「resolving deltas」? - Stack Overflow

除此以外,git會自動在.git/config文件中寫入部份內容,

[remote "origin"]
        url = git@git.in.zhihu.com:zhangwang/zhihu-lite.git
        fetch = +refs/heads/*:refs/remotes/origin/*

默認狀況下會把clone的源倉庫取名origin,在.git/config中存儲其對應的地址,本地分支與遠程分支的對應規則等。

除了git clone另外一個與遠程倉庫創建鏈接的命令爲git remote

git remote

git remote 爲咱們提供了管理遠程倉庫的途徑。
對遠程倉庫的管理包括,查看,添加,移除,對遠程分支的管理等等。

  1. 查看遠程倉庫 git remote
$ git remote
origin

# 添加 -v,可查看對應的連接
$ git remote -v
origin    https://github.com/schacon/ticgit (fetch)
origin    https://github.com/schacon/ticgit (push)

# git remote show [remote-name] 可查看更加詳細的信息
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
  1. 添加遠程倉庫 git remote add <shortname> <url>
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin    https://github.com/schacon/ticgit (fetch)
origin    https://github.com/schacon/ticgit (push)
pb    https://github.com/paulboone/ticgit (fetch)
pb    https://github.com/paulboone/ticgit (push)
  1. 遠程倉庫重命名 git remote rename
$ git remote rename pb paul
$ git remote
origin
paul
  1. 遠程倉庫的移除 git remote rm <name>
$ git remote rm paul
$ git remote
origin
上述示例代碼參照 Git - 遠程倉庫的使用

本地對遠程倉庫的記錄存在於.git/config文件中,在.git/config中咱們能夠看到以下格式的內容:

# .git/config
[remote "github"]
    url = https://github.com/zhangwang1990/weixincrawler.git
    fetch = +refs/heads/*:refs/remotes/github/*
[remote "zhangwang"]
    url = https://github.com/zhangwang1990/weixincrawler.git
    fetch = +refs/heads/*:refs/remotes/zhangwang/*
  • [remote] "github":表明遠程倉庫的名稱;
  • url:表明遠程倉庫的地址
  • fetch:表明遠程倉庫與本地倉庫的對應規則,這裏涉及到另一個 Git 命令,git fetch

git fetch

咱們先看git fetch的做用:

  1. git fetch <some remote branch> :同步某個遠程分支的改變到本地,會下載本地沒有的數據,更新本地數據庫,並移動本地對應分支的指向。
  2. git fetch --all會拉取全部的遠程分支的更改到本地

咱們繼續看看git fetch是如何工做的:

# config中的配置
[remote "origin"]
    url = /home/demo/bare-repo/
    fetch = +refs/heads/*:refs/remotes/origin/* #<remote-refs>:<local-refs> 遠程的對應本地的存儲位置

fetch的格式爲fetch = +<src>:<dst>,其中

  • +號是可選的,用來告訴 Git 即便在不能採用「快速向前合併」也要(強制)更新引用;
  • <src>表明遠程倉庫中分支的位置;
  • <dst> 遠程分支對應的本地位置。

咱們來看一個git fetch的實例,看看此命令是怎麼做用於本地倉庫的:

git fetch origin

  1. 會在本地倉庫中建立.git/refs/remotes/origin文件夾;
  2. 會建立一個名爲.git/FETCH_HEAD的特殊文件,其中記錄着遠程分支所指向的commit 對象;
  3. 若是咱們執行 git fetch origin feature-branch,Git並不會爲咱們建立一個對應遠程分支的本地分支,可是會更新本地對應的遠程分支的指向;
  4. 若是咱們再執行git checkout feature-branch,git 會基於記錄在.git/FETCH_HEA中的內容新建本地分支,並在.git/config中添加以下內容,用以保證本地分支與遠程分支future-branch的一致
[branch "feature-branch"]
    remote = origin
    merge = refs/heads/feature-branch
git 每次執行 git fetch都會重寫 .git/FETCH_HEA

上述fetch的格式也能幫咱們理解git push的一些用法

git push

咱們在本地某分支開發完成以後,會須要推送到遠程倉庫,這時候咱們會執行以下代碼:

git push origin featureBranch:featureBranch
此命令會幫咱們在遠程創建分支featureBranch,之因此要這樣作的緣由也在於上面定義的fetch模式。
由於引用規格(的格式)是 <src>:<dst>,因此其實會在遠程倉庫創建分支featureBranch,從這裏咱們也能夠看出,分支確實是很是輕量級的。

此外,若是咱們執行 git push origin :topic:,這裏咱們把 <src>留空,這意味着把遠程版本庫的 topic 分支定義爲空值,也就說會刪除對應的遠程分支。

回到git push,咱們從資源的角度看看發生了什麼?

  1. 從本地倉庫的.git/objects/目錄,上傳到遠程倉庫的/objects/下;
  2. 更新遠程倉庫的refs/heads/master內容,指向本地最新的commit;
  3. 更新文件.git/refs/remotes/delta/master內容,指向最新的commit;

說完git push,咱們再來看看 git pull

git pull

此命令的通用格式爲 git pull <remote> <branch>
它作了如下幾件事情:

  1. git fetch <remote>:下載最新的內容
  2. 查詢.git/FETCH_HEAD找到應該合併到的本地分支;
  3. 若是知足要求,沒有衝突,執行git merge

git pull 在大多數狀況下它的含義是一個 git fetch 緊接着一個 git merge 命令。

至此,經常使用的git命令原理咱們都基本講解完了。若是你們有一些其它想要了解的命令,咱們能夠再一塊兒探討,補充。

一些推薦的 git 資料

Home · geeeeeeeeek/git-recipes Wiki · GitHub
gitlet.js
git-from-the-inside-out
A Hacker’s Guide to Git | Wildly Inaccurate
githug

相關文章
相關標籤/搜索