Git - 在版本之間切換自如

Git 的使用說難也不難,對不少人而言,git 無外乎那麼幾種命令。使用 git 開發就如同一條流水線式的做業:pull 下來、checkout 分支、add 一下、commit 一下、push 完事。Git 用起來很是溫馨,由於靠着這麼一頓操做,大多數狀況下都能知足須要。git

可是一旦有一天中午,昏昏欲睡的你猛然發現本身好幾回 commit 錯了東西,或是一直工做在一個錯誤的分支上,那麼 git 對於你來講,忽然間就變成了洪水猛獸,日常使用的命令沒有一個派的上用場。json

痛定思痛,除了知足平常開發,仍是得掌握更多 git 的使用,才能讓在下一次犯錯的時候吃上一瓶後悔藥。app

基礎知識

學習 git 的一個重要的技巧就是結合類彈珠圖的 git 提交歷史來看,推薦使用一些圖形化界面好比 Sourcetree 來觀察每次執行命令以後 history 的變化。學習

提交對象

使用 git 做爲版本控制系統,首先要理解什麼是一個版本:每當進行一次 commit 操做時,Git 會保存一個提交對象(commit object),能夠理解這個提交對象就包含了此次改動的內容快照,根據不一樣提交對象的 commit id,能夠隨時訪問不一樣版本的內容。3d

舉例來說:版本控制

1)添加 README 文件,add -> commit -> git 生成 commit 98c27 2)更新 README 爲 v二、添加 app.js、package.json 文件,add -> commit -> git 生成 commit 2c9be 3)更新 README 爲 v三、更新 app.js 爲 v2,add -> commit -> git 生成 commit 1a35c指針

示意圖以下:code

時間軸是從左往右,可是 commit 的指向正好相反,由於每一個新建立的 commit,都之前一個 commit 爲父節點,因此指向前一個 commit。cdn

每個 commit 都包含了 當時版本的文件快照,也就是說,切換到某一個 commit,就能回到對應的版本上去。對象

分支

因此,咱們如何知道本身在哪一個 commit 上?通常來講,咱們都是工做在分支上的,好比當一個項目初始化時,都默認有一個 master 分支,分支本質上僅僅是指向提交對象的可變指針。好比以下的 master 分支上,README 已是 v3 版本了:

當在 master 分支上 checkout 一個分支出來時,僅僅是建立了一個新的指針,指向了當前的 commit。

因此此時 master 分支上和 feature 分支上的內容是同樣的。

咱們接着 commit,而後 master 的指針就會指向新的 commit:

爲何不是 feature 的指針改變?換句話說,git 怎麼知道咱們處在哪一個分支?這是由於有一個特殊的指針 HEAD,它指向當前所在的本地分支:

剛纔建立分支使用的是 git branch 命令,它只是建立一個分支,並不會自動切換過去,使用 git checkout 命令使得 HEAD 指向特定的分支:

繼續 commit,feature 將往前移動:

這時能夠發現兩個分支開始分叉了,也就是說這兩個分支基於同一個版本分別作了不一樣的改動。

三個集合

要理解如何操縱 commit,還須要理解 git 中的三個集合。在 git 中,文件有三種狀態:已修改(modified)、已暫存(staged)和已提交(committed),它們分別對應於三個區域:工做區(working directory)、暫存區(staging area) 和 版本庫(repository)。

git 經過比較三個區域的內容,來提示用戶須要作的操做。好比工做區與暫存區不一樣,意味着你須要 add;暫存區與版本庫不一樣,意味着你已經 add 但還沒有 commit。

咱們知道,基本的 Git 工做流程以下:

  1. 工做目錄 中修改文件。
  2. 暫存文件(add),將文件的快照放入 暫存區域
  3. 提交更新(commit),找到暫存區域的文件,將快照永久性存儲到 Git 倉庫目錄

假設咱們處在 feature 分支,當咱們進行以上操做後,commit 與三個區域的內容變化分別以下:

從左到右分別是工做區、暫存區和版本庫,版本庫上面標註了 HEAD,這表示咱們展現的是 HEAD 指針指向的內容,也就是當前分支所在的 commit。

接下來對工做區內的文件進行修改,換句話說就是修改磁盤上的文件:

修改磁盤上的文件只是改變了工做區的內容,接下來使用 git add 命令將修改提交到暫存區:

將修改提交到暫存區並不會新增一個 commit,接下來經過 git commit 來提交:

此時新的 commit 被建立,HEAD 指向的 commit 相應發生改變。

有了上面這些 git 操做的印象後,解釋如何切換版本就容易多了。

Git 中的撤銷

撤銷多是使用過程當中最須要的操做,你可能在任什麼時候候都須要撤銷。根據狀況不一樣,撤銷的命令也是不一樣的。

撤銷最近幾回 commit

要撤銷最近幾回的提交,可使用 git reset,下面介紹它的三種模式:softmixedhard

假設目前分支狀況以下,咱們須要撤銷到 98c27 commit 上去。

1)soft 模式

執行 git reset --soft 98c27,git 會首先修改 HEAD 的指向,它會連帶修改 HEAD 所在分支的指向:

如上圖所示,如今的暫存區和 HEAD 是不一樣的,這個操做本質上撤銷了 2c9be 這個 commit,如同回到了上次準備 commit 的時候。(git 中的時光機!)

此時你能夠進行後悔操做,繼續修改文件再 add,而後從新 commit,這時會提交一個新的 commit。

2)mixed 模式

回到一開始的時候,假如咱們執行的是 git reset --mixed 98c27,它也會首先修改 HEAD 的指向,使得 HEAD 上的 commit 爲 98c27

但還不夠,git 還會接着更新你的暫存區,如同回到了你準備 add 的時候。(時光機再向前!)

對你來講可能更方便了,繼續改就行,而後從新 add、commit。這其實是 reset 的默認模式,等同於 git reset 98c27

3)hard 模式

不用我多說你可能已經意識到 hard 是幹什麼用的了。這一次 git 摧枯拉朽,把你的 HEAD、暫存區、工做區全給幹掉了:

一會兒回到了你開始寫需求的時候。因此這個命令是 危險 的,除非你真的打算不要這些修改了,不然最好不要用。

不過即便你真的用了又後悔,那也是有辦法的,在 git 裏面,既然能回到過去,也能在過去穿越到將來。使用 git reflog 能夠查看你最近的修改,找到最前面的 commit id,能夠繼續使用 reset 穿回去。

合併 commit

有時候你可能發現本身剛纔提交的好幾個 commit 其實都是中間狀態,還不如把它們合併成一個。根據上面的 reset,實際上就能完成這件事情。

好比下面的場景,咱們多提交了一個 File V1.1 的中間版本,但願將其從 commit 歷史中去掉:

那其實能夠直接 reset 到 v1 版本:

而後從新進行 commit,這樣就會將 v1 以後的修改都提交到了新的版本,如同移除了中間的版本。

固然,這個場景也能用 rebase 解決,以後會提到。

挪動 commit

在多個分支上切換開發的時候,有時候會忘記切換分支就開始開發。當發現本身提交的 commit 放錯分支怎麼辦呢?在 git 中,這也不算個事,經過 git cherry-pick 就能解決。

cherry-pick 能夠將指定的 commit 「摘到」當前的分支上面,git 會爲你從新生成一個 commit,但內容與 pick 的 commit 一致。

若是你要 pick 好幾個 commit,它們之間有依賴關係,那須要根據前後順序依次進行 cherry pick。

當發生衝突時,此時須要修改文件解決衝突,可使用 git cherry-pick --abort 放棄這次 pick,或者解決完 add 進暫存區,而後使用 git cherry-pick --continue。注意這裏並非使用 git commit,若是你須要改變 commit 的信息,可使用 commit,不然 git 會默認使用 pick 的 commit 的信息。

相關文章
相關標籤/搜索