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 工做流程以下:
假設咱們處在 feature 分支,當咱們進行以上操做後,commit 與三個區域的內容變化分別以下:
從左到右分別是工做區、暫存區和版本庫,版本庫上面標註了 HEAD,這表示咱們展現的是 HEAD 指針指向的內容,也就是當前分支所在的 commit。
接下來對工做區內的文件進行修改,換句話說就是修改磁盤上的文件:
修改磁盤上的文件只是改變了工做區的內容,接下來使用 git add
命令將修改提交到暫存區:
將修改提交到暫存區並不會新增一個 commit,接下來經過 git commit
來提交:
此時新的 commit 被建立,HEAD 指向的 commit 相應發生改變。
有了上面這些 git 操做的印象後,解釋如何切換版本就容易多了。
撤銷多是使用過程當中最須要的操做,你可能在任什麼時候候都須要撤銷。根據狀況不一樣,撤銷的命令也是不一樣的。
要撤銷最近幾回的提交,可使用 git reset
,下面介紹它的三種模式:soft
、mixed
和 hard
。
假設目前分支狀況以下,咱們須要撤銷到 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 其實都是中間狀態,還不如把它們合併成一個。根據上面的 reset,實際上就能完成這件事情。
好比下面的場景,咱們多提交了一個 File V1.1 的中間版本,但願將其從 commit 歷史中去掉:
那其實能夠直接 reset 到 v1 版本:
而後從新進行 commit,這樣就會將 v1 以後的修改都提交到了新的版本,如同移除了中間的版本。
固然,這個場景也能用 rebase 解決,以後會提到。
在多個分支上切換開發的時候,有時候會忘記切換分支就開始開發。當發現本身提交的 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 的信息。