Git原理與高階使用(2)

Desmonddai583發表於2018-04-11

上一篇中我們介紹了git的基礎概念,這篇我們就來說說分支

分支

git中的分支其實只是一個指標指向一個commit物件,而不是像傳統的版本控制系統一樣把整個當前版本複製一份出來。它背後其實就是一個檔案,我們可以去.git/refs/heads資料夾下面檢視,裡面的每個檔案其實就是一個分支,而內容其實就是一串SHA1值,而這個SHA1值又是什麼呢,其實就是一個commit id,以下圖為例,其實此時master分支檔案存放的就是master分支上的最後一次commit id,而分支背後代表的就是一條commit物件鏈。

Git原理與高階使用(2)

Git原理與高階使用(2)

而HEAD又是什麼呢,其實它也是一個指標,指向著是當前分支,而不是commit物件。在.git目錄下有一個HEAD檔案,裡面就是記錄著HEAD指向的分支。

Git原理與高階使用(2)

Git原理與高階使用(2)

分支合併

接下來就用一個例子來介紹一下git分支合併的過程,首先假設我們處於master分支上

Git原理與高階使用(2)
接著我們從當前master上建立一個新分支dev,並且換到當前分支上git checkout -b dev
Git原理與高階使用(2)
下一步我們在dev分支上修改並提交一個新的commit
Git原理與高階使用(2)
接著我們回到master分支並執行git merge dev合併分支
Git原理與高階使用(2)
這裡要注意的是merge預設採用的是fast-forward快進模式,它代表著就是直接將master指標指向最新的commit id,中間master分支不會做任何的修改,而沒有建立任何新的commit,但是實際上不是任何情況下我們都可以直接這樣快進的,合併時修改的不是同一個檔案的同一個內容是可以直接fast-forward合併,但是因為兩個分支都有過commit,所以合併的時候就一定會產生一個新的commit物件,同時當我們修改的是同一個檔案同一個內容時,合併是會產生衝突的,這是後我們就必須要手動修復衝突,解決後通過git add標示衝突已解決並用git commit提交,也一樣不可避免會產生一個新的commit。在合併之後,反過來如果被merge的分支想要去merge已經merge完的分支,就直接fast-forward就可以了,以為兩條分支已經有了一個交匯點,所以直接快進至交匯點即可,所以這裡其實是可以總結一條規律的,什麼情況下可以快進呢,就是噹噹前的commit物件鏈可以從當前想要merge的分支走到要merge到的commit點,那就可以直接快進了。
Git原理與高階使用(2)
那說完快進模式後,自然也就有非快進模式,非快進模式模式在上面提到的情況時即便沒有衝突,兩個分支也沒有同時進行修改也會產生一個新的commit,在我們merge時我們只要加上一個--no-ff即可,git merge --no-ff dev,那非快進模式的好處是什麼呢,其實就是如同下圖所示,非快進模式可以幫我們保留分支的樣子,在樹狀結構中可以完整的看出分支的情況
Git原理與高階使用(2)

Reset

git reset其實有3中模式,mixed, soft和hard,預設不寫的話會呼叫mixed模式,那麼3者的區別是什麼呢,mixed其實就是會將reset之後的commit與原commit之間的修改轉到工作區,而soft則是轉到暫存區,最後hard就是直接丟棄掉,所以可以看出其實reset與它的字面意思有些不同的,我們經常會誤會它是重新設定把之後的commit砍掉,其實它只是回到之前的狀態。

Stash

git stash的作用是什麼呢,假設當我們在feature1上做事時,突然需要緊急去feature2上做事,此時就需要將feature1上的事情暫時用git stash先存起來然後去feature2上做事,做完再回feature1上通過git stash pop將之前儲存的修改取出。需要注意的是git stash會記住目前的commit id,所以如果同一個commit下stash兩次對於同一檔案同一行內容的修改,恢復完第一次並且提交過後,在執行第二次恢復就會有衝突。

Tag

tag就是我們之前提到的其實就是一個標籤物件,它也是指向某一個commit物件,它與分支的區別就在於當有commit發生時,分支會跟著一起向前移,但是標籤一旦定下來就不會再移動了,所以它通常是用於我們專案到了一個milestone,需要釋出一個新版本時使用。git標籤分為兩種輕量級標籤(lightweight)和附註標籤(annotated),就如同字面上的意思,輕量級於附註標籤的區別就是一個有描述資訊一個沒有。

Diff

git diff背後其實就是利用了linux自帶的diff模組用來比較檔案之間的差別,有興趣的可以去了解一下

指令

  1. 檢視分支列表

    git branch

  2. 建立分支

    git branch 分支名

  3. 切換分支

    git checkout 分支名

  4. 切換到上次處於的分支中

    git checkout -

  5. 刪除分支()

    git branch -d 分支名

    這裡需要注意不能刪除當前處於的分支,如果非master分支有改動但還未merge的話也不可以,除非使用git branch -D 分支名

  6. 建立新分支並切換到新分支

    git checkout -b 分支名

  7. 顯示當前分支最近的一條提交訊息

    git branch -v

  8. 將分支合併到當前分支

    git merge 分支名

  9. 禁用fast-forward,會多一個commit id

    git merge --no-ff 分支名

  10. 回退到上一次提交(基於當前的commit)

    git reset --hard HEAD^

  11. 回退到上上一次提交(都是基於當前的commit)

    git reset --hard HEAD^^

  12. 回退到當前分支上的前n次(從當前commit往前n次)的提交

    git reset --hard HEAD~n

  13. 回退到指定commit

    git reset --hard commit資訊的前幾位

  14. 修改分支名

    git branch -m 原分支名 新分支名

  15. 將工作區的修改儲存

    git stash

  16. 列出所有的儲存

    git stash list

  17. 手動設定stash描述

    預設執行git stash返回的描述資訊是

    Git原理與高階使用(2)
    其實就是包含了當前最新commit的訊息,那麼我們可以通過以下命令來修改描述資訊 git stash save 'hello basic'
    Git原理與高階使用(2)

  18. 恢復最近一次的儲存,並且會把這次儲存在列表中刪除

    git stash pop

  19. 恢復最近一次的儲存,但是不會在列表中刪除

    git stash apply

  20. apply特定一個版,並且會把這次儲存在列表中刪除

    git stash apply stash@{0}

  21. 手動刪除指定的一個儲存版本

    git stash drop stash@{0}

  22. 建立輕量標籤

    git tag v1.0.1

  23. 建立附註標籤

    git tag -a v1.0.2 -m 'release 1.0.2'

  24. 檢視標籤

    git tag可以檢視所有的標籤,同時也可以通過git tag show 標籤名來看某個標籤

  25. 查詢標籤

    git tag -l 'v1.0' 裡面可以使用pattern,例如'v*', 代表v開頭的所有標籤

  26. 刪除標籤

    git tag -d 標籤名

  27. 列出每一行都是誰在什麼時間哪個commit修改的

    git blame 檔名

  28. 比較算入暫存區修改的當前檔案與工作區檔案之間的區別

    git diff

  29. 比較當前最新commit與工作區的區別

    git diff HEAD

  30. 比較某個commit與工作區的區別

    git diff commit_id

  31. 比較最新提交與暫存區的區別

    git diff --cached

  32. 比較某個commit與暫存區的區別

    git diff --cached commit_id

  33. 一次把本地還未提交的標籤推送上去

    git push origin --tags

  34. 刪除遠端標籤

    可以通過呼叫git push origin --delete tag 標籤名,或者使用git push origin :refs/tags/v6.0推送一個空標籤上去

  35. 裁剪掉當前遠端分支

    有時候可能使用者a執行了命令刪除了遠端的a1分支,那麼對於使用者b來說當他執行git remote show origin時,會發現遠端的a1分支是處於stale狀態,也就是說遠端已經沒有了,但是本地還在,這時候就可以執行git remote prune origin,這樣就會把那個stale狀態下的遠端分支刪掉

場景

  1. 當我們使用git reset --hard指令回退到之前的commit時,執行git log只能看到當前commit及其之前的commit,那如果我想回到之後的某個commit,我要怎麼獲取commit id呢?

    我們可以通過執行git reflog來檢視操作日誌,看到我們使用git的指令的歷史,這樣就可以找回我們之前的commit id了,這裡說下reflog背後其實就是記錄著HEAD指標的改變歷史,所以平常都會說不要手動去修改.git目錄下的檔案,因為手工修改之後是不會記錄在reflog中的

  2. 我不小心把一個分支用git branch -D 分支名強制刪除掉了,還救得回來嗎?

    是救得回來的,這裡要先強調的是,分支其實只是一個指標,所以即便刪除掉了這個指標,我們的commit物件還在,所以當然是可以救回來的,只要我們通過git reflog找到原本分支指向的commit-id,然後執行git branch 分支名 commit-id就可以重新建立回我們的分支了。這裡在延伸說一下,其實在git merge時背後其實也是在合併commit物件,所以我們其實也可以通過git merge commit-id來做合併的,分支只是給我們一個類似貼紙一樣簡單標註了這串分支commit鏈做的是什麼。

Q&A

  1. git checkout commit-idgit reset --hard commit-id 有什麼區別呢?

    兩者都可以回到對應的commit點,但是checkout與reset不同的是它會處於遊離狀態,任何的修改如果不做提交就會有警告不允許我們跳去其他的commit,同時修改完我們可以通過git branch 分支名 當前checkout的commit-id來建立一個新分支。

相關文章