我用四個命令概括了 Git 的所有套路
-----------
我先開一會兒吐槽大會,Git 這東西我用了兩年,根本尼瑪用不明白。
我搞不明白的一個重要原因就是,命令的功能太雜,有時候一個需求可以用好幾種命令解決,而且有的命令還 tm 有別名。這導致什麼問題呢,我在網上找到的答案五花八門,竟然都能達成目的,難以找到規律,毫無套路可言。對於我這種不喜歡動腦子,只喜歡玩套路的人來說,簡直不能接受。
以前我用 Git,就知道 add .
,然後 commit -m
,最後 push origin master
一套帶走,或者就是把 Git 作為下載器,去 clone
別人的專案。但是在工作中呢,和別人一起開發程式碼,就需要處理一些複雜情況,比如解決衝突,比如手殘恢復,等等等實用場景,這些我在後文都會列舉。
對於工具的學習,我認為應該多做減法,只撿最有用的學,那些奇技淫巧不學也罷,應該把時間投入更有價值的事情中。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。
所以本文不是一個大而全 Git 命令的使用手冊,而是根據實際工作中最常見問題,提供小而美的解決方案,僅僅涉及四個命令:add
,commit
,reset
,checkout
。
一、前言
首先,在進入 Git 的各種神仙操作之前,一定要明白 git 的三個「分割槽」是什麼,否則的話你一定沒辦法真正理解 Git 的原理
Git 的三個分割槽分別是:working directory
,stage/index area
,commit history
。
working directory
是「工作目錄」,也就是我們肉眼能夠看到的檔案,後文我們稱其為 work dir
區。
當我們在 work dir
中執行 git add
相關命令後,就會把 work dir
中的修改新增到「暫存區」stage area
(或者叫 index area
)中去,後文我們稱暫存區為 stage
區。
當 stage
中存在修改時,我們使用 git commit
相關命令之後,就會把 stage
中的修改儲存到「提交歷史」 commit history
HEAD
指標指向的位置。後文我們稱「提交歷史」為 history
區。
關於 commit history
我們多說幾句,任何修改只要進入 commit history
,基本可以認為永遠不會丟失了。每個 commit
都有一個唯一的 Hash 值,我們經常說的 HEAD
或者 master
分支,都可以理解為一個指向某個 commit
的指標。
work dir
和 stage
區域的狀態,可以通過命令 git status
來檢視,history
區域的提交歷史可以通過 git log
命令來檢視。
好的,如果上面的內容你都能夠理解,那麼本文就完全圍繞這三個概念展開,下面就是一個「狀態轉移圖」:
二、本地 Git 極簡教程
需求一,如何把 work dir
中的修改加入 stage
。
這個是最簡單,使用 git add
相關的命令就行了。順便一提,add
有個別名叫做 stage
,也就是說你可能見到 git stage
相關的命令,這個命令和 git add
命令是完全一樣的。
風險等級:無風險。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。
理由:不會改變任或撤銷任何已作出的修改,而且還會將 work dir
中未追蹤的修改(Untracked file)新增到暫存區 stage
中進行追蹤。
需求二,如何把 stage
中的修改還原到 work dir
中。
這個需求很常見,也很重要,比如我先將當前 work dir
中的修改新增到 stage
中,然後又對 work dir
中的檔案進行了修改,但是又後悔了,如何把 work dir
中的全部或部分檔案還原成 stage
中的樣子呢?
來個實際場景,我先新建兩個檔案,然後把他們都加到 stage
:
$ touch a.txt b.txt
$ git add .
$ git status
On branch master
Changes to be committed:
new file: a.txt
new file: b.txt
然後我又修改了 a.txt
檔案:
$ echo hello world >> a.txt
$ git status
On branch master
Changes to be committed:
new file: a.txt
new file: b.txt
Changes not staged for commit:
modified: a.txt
現在,我後悔了,我認為不應該修改 a.txt
,我想把它還原成 stage
中的空檔案,怎麼辦?
答案是,使用 checkout
命令:
$ git checkout a.txt
Updated 1 path from the index
$ git status
On branch master
Changes to be committed:
new file: a.txt
new file: b.txt
看到了麼,輸出顯示從 index
區(也就是 stage
區)更新了一個檔案,也就是把 work dir
中 a.txt
檔案還原成了 stage
中的狀態(一個空檔案)。
當然,如果 work dir
中被修改的檔案很多,可以使用萬用字元全部恢復成 stage
:
$ git checkout .
有一點需要指出的是,checkout
命令只會把被「修改」的檔案恢復成 stage
的狀態,如果 work dir
中新增了新檔案,你使用 git checkout .
是不會刪除新檔案的。
風險等級:中風險。
理由:在 work dir
做出的「修改」會被 stage
覆蓋,無法恢復。所以使用該命令你應該確定 work dir
中的修改可以拋棄。
需求三,將 stage
區的檔案新增到 history
區。
很簡單,就是 git commit
相關的命令,一般我們就是這樣用的:
$ git commit -m '一些描述'
再簡單提一些常見場景, 比如說 commit
完之後,突然發現一些錯別字需要修改,又不想為改幾個錯別字而新開一個 commit
到 history
區,那麼就可以使用下面這個命令:
$ git commit --amend
這樣就是把錯別字的修改和之前的那個 commit
中的修改合併,作為一個 commit
提交到 history
區。
風險等級:無風險。
理由:不會改變任或撤銷任何已作出的修改,而且還會將 stage
區的修改加入 history
區並分配一個 Hash 值。只要不亂動本地的 .git
資料夾,進入 history
的修改就永遠不會丟失。
需求四,將 history
區的檔案還原到 stage
區。
這個需求很常見,比如說我用了一個 git add .
一股腦把所有修改加入 stage
,但是突然想起來檔案 a.txt
中的程式碼我還沒寫完,不應該把它 commit
到 history
區,所以我得把它從 stage
中撤銷,等後面我寫完了再提交。
$ echo aaa >> a.txt; echo bbb >> b.txt;
$ git add .
$ git status
On branch master
Changes to be committed:
modified: a.txt
modified: b.txt
如何把 a.txt
從 stage
區還原出來呢?可以使用 git reset
命令:
$ git reset a.txt
$ git status
On branch master
Changes to be committed:
modified: b.txt
Changes not staged for commit:
modified: a.txt
你看,這樣就可以把 a.txt
檔案從 stage
區移出,這時候進行 git commit
相關的操作就不會把這個檔案一起提交到 history
區了。
上面的這個命令是一個簡寫,實際上 reset
命令的完整寫法如下:
$ git reset --mixed HEAD a.txt
其中,mixed
是一個模式(mode)引數,如果 reset
省略這個選項的話預設是 mixed
模式;HEAD
指定了一個歷史提交的 hash 值;a.txt
指定了一個或者多個檔案。
該命令的自然語言描述是:不改變 work dir
中的任何資料,將 stage
區域中的 a.txt
檔案還原成 HEAD
指向的 commit history
中的樣子。就相當於把對 a.txt
的修改從 stage
區撤銷,但依然儲存在 work dir
中,變為 unstage
的狀態。
風險等級:低風險。
理由:不會改變 work dir
中的資料,會改變 stage
區的資料,所以應確保 stage
中被改動資料是可以拋棄的。
需求五,將 work dir
的修改提交到 history
區。
這個需求很簡單啦,先 git add
然後 git commit
就行了,或者一個快捷方法是使用命令 git commit -a
。
風險等級:無風險。
理由:顯而易見。
需求六,將 history
區的歷史提交還原到 work dir
中。
這個場景,我說一個極端一點的例子:比如我從 GitHub 上 clone
了一個專案,然後亂改了一通程式碼,結果發現我寫的程式碼根本跑不通,於是後悔了,乾脆不改了,我想恢復成最初的模樣,怎麼辦?
依然是使用 checkout
命令,但是和之前的使用方式有一些不同:
$ git checkout HEAD .
Updated 12 paths from d480c4f
這樣,work dir
和 stage
中所有的「修改」都會被撤銷,恢復成 HEAD
指向的那個 history commit
。
注意,類似之前通過 stage
恢復 work dir
的 checkout
命令,這裡撤銷的也只是修改,新增的檔案不會被撤銷。
當然,只要找到任意一個 commit
的 HASH 值,checkout
命令可就以將檔案恢復成任一個 history commit
中的樣子:
$ git checkout 2bdf04a some_test.go
Updated 1 path from 2bdf04a
# 前文的用法顯示 update from index
比如,我改了某個測試檔案,結果發現測試跑不過了,所以就把該檔案恢復到了它能跑過的那個歷史版本……
風險等級:高風險。
理由:這個操作會將指定檔案在 work dir
的資料恢復成指定 commit
的樣子,且會刪除該檔案在 stage
中的資料,都無法恢復,所以應該慎重使用。
三、其他技巧
需求一,合併多個 commit
。
比如說我本地從 17bd20c
到 HEAD
有多個 commit
,但我希望把他們合併成一個 commit
推到遠端倉庫,這時候就可以使用 reset
命令:
$ git reset 17bd20c
$ git add .
$ git commit -m 'balabala'
回顧一下剛才說的 reset
命令的作用,相當於把 HEAD 移到了 17bd20c
這個 commit
,而且不會修改 work dir
中的資料,所以只要 add
再 commit
,就相當於把中間的多個 commit
合併到一個了。
需求二,由於 HEAD
指標的回退,導致有的 commit
在 git log
命令中無法看到,怎麼得到它們的 Hash 值呢?
再重複一遍,只要你不亂動本地的 .git
資料夾,任何修改只要提交到 commit history
中,都永遠不會丟失,看不到某些 commit
只是因為它們不是我們當前 HEAD
位置的「歷史」提交,我們可以使用如下命令檢視操作記錄:
$ git reflog
比如 reset
,checkout
等等關鍵操作都會在這裡留下記錄,所有 commit
的 Hash 值都能在這裡找到,所以如果你發現有哪個 commit
突然找不到了,一定都可以在這裡找到。
需求三,怎麼解決衝突?
記住,Git 雖然高大上,但也不要迷戀,一定要懂得藉助先進的工具。
比較流行的程式碼編輯器或者 IDE 都會整合方便的視覺化 Git 工具,至於解決衝突,視覺化的表現方式不是比你在命令列裡 git diff
看半天要清晰明瞭得多?只需要點點點就行了。
所以說,只要明白本文講的這些基本操作,夠你用的了,平時能用圖形化工具就多用圖形化工具,畢竟工具都是為人服務的。
前文 Git/SQL/正則表示式的線上練習平臺 介紹了一個優秀的 Git 線上練習平臺,值得一刷。
_____________
我的 線上電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 演算法倉庫 已經獲得了 70k star,歡迎標星!