Git的騷操作你都會嘛? 教會你遠離Git噩夢

正式開始之前先講個親身經歷經歷的故事,在上一個公司工作有個女同事,晚上和男朋友提前說好了約會看電影,快五點了,就想着趕緊提交代碼,好下班,可是她push了好幾次,都被遠端拒絕了,這時候她一狠心一跺腳用了-f,然後就下班去了,她倒是下班了,由於她對之前提交的代碼用了rebase,而其他同事又基於她之前的代碼開發,直接導致了其他同事的commit id出現了混亂,而導致當天的上線被擱置了,這是一個真實的案例,而其中的問題就在於這個女同事對rebase不熟悉,並採用了危險的git操作。因此,git的操作雖然簡單,但在開發過程中卻非常重要,如果用不好,自己的工作白費了是輕的,更糟糕的是有可能讓自己成爲同事厭惡的人。因此git用的好,將會是一個神器,用不好就是噩夢!

前言

首先問一下自己以下幾個問題:

  • 如何合併幾個歷史commit?

  • 如何修改歷史提交的message?

  • 如何將某個分支的某次commit移到另一個分支尼?

要是你都會,那恭喜你,可以不用閱讀本文了,要是還有疑惑,那就跟着我一起來學習學習吧。

一、基本配置

配置用戶信息

git config --global user.name "FrontDream"

git config --global user.email [email protected]

以上配置過程中用到了--global,其實還有--local--system,下面對此做一個對比,問題不大,可以不用着重考慮。

git config --local  // local只對某個倉庫有效

git config --global // global對當前用戶所有倉庫有效

git config --system // 不常用,對系統所有登陸用戶有效

如果想要顯示 config 的配置,可以加上--list:

git config --list --local

git config --list --global

git config --list --system

配置文本編輯器

當用戶信息設置完畢,你可以配置默認文本編輯器了,當 Git 需要你輸入信息時會調用它。如果沒有配置,Git 會使用操作系統默認的文本編輯器,通常是 Vim。如果你想使用不同的文本編輯器,例如 Emacs,可以這樣做:

git config --global core.editor emacs

配置命令的別名

git config --global alias.ci commit

可以通過 git config 文件來輕鬆地爲每一個命令設置一個別名,如上配置後,當要輸入 git commit 時,只需要輸入 git ci

二、建立倉庫

主要有以下幾種場景:

把已有的項目代碼納入到 Git 管理

$cd 項目代碼所在文件夾
$git init

新建的項目(沒有代碼)直接用 Git 管理

$ cd 某個文件夾
$ git init your_project #會在當前路徑下創建和項目名稱同名的文件夾
$ cd your_project

克隆倉庫

git clone https://github.com/libgit2/libgit2

這會在當前目錄下創建一個名爲 “libgit2” 的目錄,並在這個目錄下初始化一個 .git 文件夾, 從遠程倉庫拉取下所有數據放入 .git 文件夾,然後從中讀取最新版本的文件的拷貝。如果你進入到這個新建的 libgit2 文件夾,你會發現所有的項目文件已經在裏面了,準備就緒等待後續的開發和使用。

如果你想在克隆遠程倉庫的時候,自定義本地倉庫的名字,你可以通過額外的參數指定新的目錄名:

git clone https://github.com/libgit2/libgit2 mylibgit

這會執行與上一條命令相同的操作,但目標目錄名變爲了 mylibgit

添加遠程倉庫

當你本地已經有了代碼,在 GitHub 上新建了一個倉庫,這時候需要將遠程的倉庫與本地進行合併與關聯。

首先你需要在倉庫中獲取到自己的 SSH:

通過命令:

git remote add origin [email protected]:FrontDream/FrontDream.github.io.git

這樣就將本地的項目與遠程的項目進行了關聯,如果這時候遠程是有文件的如 readme 文件,直接git push --all會失敗,這個時候,需要將遠程的代碼拉下來git fetch,然後再git merge --allow-unrelated-histories origin/master,再git push或者git push -u origin master就好了。

如果想要在某個的倉庫下配置local的用戶名和郵箱,可以在當前倉庫的路徑下用以下設置:

git config --local user.name "FrontDream"

git config --local user.email [email protected]

這樣就達到了只對某個倉庫進行配置的目的。如下圖所示

三、深入探索.git 文件

HEAD 文件

如上圖所示,現在 master 分支上,通過cd .git進行git文件,然後通過cat命令將 HEAD 文件的內容輸出,然後切換到 204-分支, 再次進入.git文件,並將 HEAD 文件的內容輸入。通過對比,我們可以發現,HEAD 文件存放的是當前所在分支的引用。

Config 文件

如上圖所示,通過在.git目錄下,通過cat命令將 congig 文件的內容輸出,我們可以看到,這個文件存儲的就是我們之前配置的用戶名和郵箱,我們可以通過文本編輯器打開這個文件,修改用戶名和郵箱,這個修改的效果和我們用git config 的命令相同。

refs 文件

還記得上面 HEAD 文件中存放的是refs/heads/master嘛?指的就是 refs 文件夾下的 heads 中的 master, 如上圖所示,通過在.git目錄下,通過cd命令進入 refs,可以看到有 heads 文件夾和 tags 文件夾,繼續進入 heads,我們可以看到有很多分支名稱的文件,我們通過cat命令輸出內容,可以發現,各個分支名的文件存儲的其實是 commit 的哈希。可以通過命令git cat-file -t 跟上輸出內容的前面一部分查看其是什麼類型,輸出是commit類型。

回到 refs 進入 tags,發現有很多的版本號的名稱,我們通過cat輸出其中某個版本號文件的內容,同樣發現,tag 同樣也是某個 commit 的哈希值。但是通過git cat-file -t 跟上輸出內容的前面一部分查看其是什麼類型,輸出是tag類型。繼續通過git cat-file -p跟上哈希值(一部分就行),發現其實他是一個object。不要停,繼續通過git cat-file -t 跟上object後面的一部分哈希值,輸出了commit類型。這裏需要注意的是git cat-file後面跟-t是查看類型,-p是查看內容

結論: heads中存放的是各個分支名,而各個分支名其實是commit的哈希值,而tags我們也是在某個 commit 後打的標籤,其是通過一個tag類型,其實是一個object對象,包裹着一個commit哈希值。

objects 文件

首先從.git目錄下進入objects目錄,我們發現目錄下面有很多兩位數的文件夾,進入其中一個如6b,我們發現有很多哈希文件,通過6b,與哈希值進行拼接,並用git cat-file -t 查看類型,發現爲tree類型(tree類型是git中重要的類型,其他類型還有commitblob等);用git cat-file -p查看內容,我們發現又是一堆哈希,繼續用-t查看類型,-p查看內容,我們發現這是我們的新增文件呀,爲blob類型。

四、基本用法

我們將在此部分中用提問的方式,引出每一條命令。

Q1: 如何查看工作區、暫存區的狀態?

git status

命令的輸出十分詳細,但其用語有些繁瑣。 Git 有一個選項可以幫你縮短狀態命令的輸出,這樣可以以簡潔的方式查看更改。如果你使用 git status -s 命令或 git status --short 命令,你將得到一種格式更爲緊湊的輸出。

git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt

新添加的未跟蹤文件前面有 ?? 標記,新添加到暫存區中的文件前面有A 標記,修改過的文件前面有 M 標記。輸出中有兩欄,左欄指明瞭暫存區的狀態,右欄指明瞭工作區的狀態。例如,上面的狀態報告顯示: README 文件在工作區已修改但尚未暫存,而 lib/simplegit.rb 文件已修改且已暫存。 Rakefile 文件已修,暫存後又作了修改,因此該文件的修改中既有已暫存的部分,又有未暫存的部分。

Q2: 如何查看工作區、暫存區的修改後的差異?

git diff

此比較的是工作目錄中當前文件和暫存區域快照之間的差異。也就是修改之後還沒有暫存起來的變化內容。

git diff --staged

這條命令將比對已暫存文件與最後一次提交的文件差異。

git diff 分支一 分支二 -- 要比較的文件

此命令對兩個分支的某個文件進行比較差異。

git diff #hash1 #hash2

此命令對比 commit1 和 commit2 的差異。

git diff HEAD HEAD~2

對比 HEAD(其實指向的也是一個commit的哈希)與 HEAD 的父親的父親(上兩次 commit)的差異。

Q3: 如何跳過添加到暫存區直接提交?

git commit -a -m 'added new benchmarks'
git commit -am 'added new benchmarks'  // 簡化版

此命令將跳過使用暫存區域,儘管使用暫存區域的方式可以精心準備要提交的細節,但有時候這麼做略顯繁瑣。Git 提供了一個跳過使用暫存區域的方式, 只要在提交的時候,給 git commit 加上 -a 選項,Git 就會自動把所有已經跟蹤過的文件暫存起來一併提交,從而跳過 git add 步驟。

Q4: 如何將連續的多個commit合併成一個?

以下圖爲例,需要將前三個commit合併成一個,首先複製第三個commit後面的哈希值。

然後運行命令:

git rebase -i #複製的哈希

出現如下界面:

將第二、第三的pick修改爲s(squash 簡寫),如下圖所示,保存按esc,輸入:wq!保存退出。

繼續出現如下界面:

添加一條message,如下圖所示:

保存按esc輸入:wq!退出。成功界面如下圖所示:

squash是指將該commit合併到相鄰的pickcommit中。

Q5: 如何重命名或者移動文件?

重命名文件或者移動文件。如果是移動根目錄下的文件,要有相對路徑。

git mv file_from file_to

Q6: 如何刪除指定文件?

git rm 待刪除文件

Q7: 如何保存文件,不提交到暫存區?

這個問題可以用一個場景來解釋,如果在開發過程中,工作區已經有了修改內容,這時需要立馬修復一個 bug,這時是不可能直接跳到某個分支去修改的,我們需要先提交到暫存區,但是我其實不滿意與現在的修改,後面還需要再大改,我不想提交到暫存區。這時就可以通過命令:

git stash

此命令將工作區的內容存放在堆棧中,當我們修改好了 bug,就重新用:

git stash apply
or
git stash pop

恢復工作區的內容,需要注意的是,apply會在堆棧中保存原有的stash信息,而pop將會把stash中的內容清空。

Q8: 如何查看歷史提交記錄?

git log

我相信小孩都知道,在不傳入任何參數的默認情況下,git log 會按時間先後順序列出所有的提交,最近的更新排在最上面。

其中一個比較有用的選項是 -p 或 --patch ,它會顯示每次提交所引入的差異(按 補丁 的格式輸出)。你也可以限制顯示的日誌條目數量,例如使用 -2 選項來只顯示最近的兩次提交。

git log -p -2

如果我希望在一行顯示需要的信息,可以進行相應的配置如:

git log --pretty=oneline
git log --oneline //簡化版

oneline 會將每個提交放在一行顯示,在瀏覽大量的提交時非常有用。另外還有 shortfull 和 fuller 選項,它們展示信息的格式基本一致,但是詳盡程度不一。

git log --pretty=format:"%h - %an, %ar : %s"

可以定製記錄的顯示格式:

當 oneline 或 format 與另一個 log 選項 --graph 結合使用時尤其有用。這個選項添加了一些 ASCII 字符串來形象地展示你的分支、合併歷史。

git log --pretty=format:"%h %s" --graph

限制輸出長度

示例如下:

$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \
   --before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:\$current_branch" into an unborn branch

以上是通過命令行git log查看版本歷史信息,當然也可以通過gitk命令調出圖形化界面,查看歷史信息。在命令行中輸入gitk,調出如下界面:

Q9: 如何修改最近的提交信息?

有時候我們提交完了才發現漏掉了幾個文件沒有添加,或者說上一次的提交信息寫錯了,我們想修改當前分支最近的 commit 的 message。此時,可以運行帶有 --amend 選項的提交命令來重新提交,第二次的提交會替代掉第一次的提交。

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend

兩次提交只有一次提交信息。

Q10: 如何修改歷史的提交信息尼?

這裏需要用到rebase了。如下圖,我們想修改歷史的提交信息“ant4.0 升級”爲“升級 ant4.0”。

首先需要找到需要修改的提交信息的上一個(父級)的哈希值,記住是需要修改的上一次提交的哈希值,並複製,重要的事情說三遍!

然後通過命令:

git rebase -i #你複製的哈希值

然後將出現如下界面:

如下圖所示,修改你需要修改的提交信息前面的pickr(reward 的簡寫),保存,然後點擊一下esc,輸入:wq!回車退出。

接着出現如下界面,供你修改提交信息。

編輯修改,並保存,在英文輸入法下點擊一下esc,輸入:wq!回車退出。出現如下界面,即成功修改了歷史的提交信息。

Q11: 如何取消暫存中的文件?

可以通過如下命令實現取消暫存中的文件:

git reset HEAD <file>

Q12: 如何取消工作區中的文件?

如果我們現在正在工作區中工作,發現現在的寫法還不如已經提交的寫法,想把還沒添加到暫存區的文件取消修改,可以通過如下命令:

git checkout -- <file>

Q13: 如何回退到某個 commit?

有時候我們提交了一些改動,後來又不想要了。有可能是WIP提交,也可能是某個引入了 bug 的提交。這種情況,我們可以執行git resetgit reset會丟棄當前所有暫存的文件,並讓我們決定 HEAD 應該指向哪裏。

git reset --soft #哈希

soft reset 將HEAD 移動到指定的提交(或者相對於HEAD 的位置索引),同時不會丟棄這些提交帶來的改動。執行git status,你會看到我們依然能夠查看之前提交所做的改動。這很有用,因爲這樣我們就能繼續修改文件內容,後續再次提交了。

git reset --hard #哈希

有時候,我們不想保留某些提交帶來的改動。跟 soft reset 不一樣,我們不再需要訪問這些變動了。Git 應該簡單地重置到指定的提交,並且會重置工作區和暫存區的文件。

Q13: 如何刪除不需要的分支?

git branch -d you-branch

Q14: 如何查看分支?

git branch -v
or
git branch -av

-v表示的是查看本地有哪些分支,而-av查看的是本地和遠程的分支。

還有一種情況是爲了查看設置的所有跟蹤分支可以用-vv。將所有的本地分支列出來並且包含更多的信息,如每一個分支正在跟蹤哪個遠程分支與本地分支是否是領先、落後或是都有。

git branch -vv

Q15: 如何查看哪些分支已經合併到當前分支?

git branch --merged
git branch --no-merged

--merged查看有哪些分支已經合併到當前分支,而--no-merged查看所有包含未合併工作的分支,因爲它包含了還未合併的工作,嘗試使用 git branch -d 命令刪除它時會失敗(可以使用 -D 選項強制刪除它)。

Q16: 如何跟遠程分支關聯?

如果遠程已經有了feature-branch,通過git fetch拉到本地,並通過以下命令在本地新建了feature-branch,並同遠程分支關聯。

git checkout -b feature-branch origin/feature-branch

Q17: 如何跟將本地代碼推送到遠程的某個分支?

git push origin local-branch:feature-branch

推送本地的 local-branch(冒號前面的)分支到遠程 origin 的 feature-branch(冒號後面的)分支(沒有會自動創建)

下一次其他協作者從服務器上抓取數據時,他們會在本地生成一個遠程分支 origin/feature-branch,指向服務器的 feature-branch 分支的引用。本地不會自動生成一份可編輯的副本(拷貝)。換一句話說,這種情況下,不會有一個新的 feature-branch 分支,只有一個不可以修改的 origin/feature-branch 指針。

可以運行 git merge origin/feature-branch 將這些工作合併到當前所在的分支。

如果想要在自己的 my_branch 分支上工作,可以將其建立在遠程跟蹤分支之上:

git checkout -b my_branch origin/feature-branch

這樣就新開了一個本地分支 my_branch 用戶跟蹤遠程的 feature-branch。看起來是挺複雜的,但是可以簡化:

git checkout --track origin/feature-branch

這樣的本地的 feature-branch 分支就可以跟蹤遠程的 feature-branch 分支了。還可以繼續簡化:

git checkout feature-branch

當然簡化的化就無法重命名了,如果需要重命名還是得使用:

git checkout -b my_branch origin/feature-branch

Q18: 如何獲取某個分支的某次提交內容?

當活動分支需要某個分支的某個提交包含的改動時,我們可以用cherry-pick命令。通過cherry-pick某個提交,在當前活動分支上會創建一個新提交,包含了前者帶來的改動。

如下圖所示(盜來的),假設 dev 分支上的提交76d12改動了index.js文件,我們在master分支上也需要。我們不需要整個分支上的改動,只要這個提交。

git cherry-pick 76d12

Q19: pull 與 fetch 有什麼區別?

用 git fetch 把這些改動獲取到本地。這不會影響本地分支,fetch只是下載數據。git pull實際上是兩個命令合而爲一:git fetchgit merge。當我們從 origin 拉取改動時,先是像git fetch一樣獲取所有數據,然後最新改動會自動合併到本地分支。

Q20: 如何變基?

如上圖所示,我們從C2拉出了特性分支experiment並進行了開發到C4,同時master繼續開發到C3,現在我們需要將C3,與C4進行合併,通過git merge是其中一種策略:

git checkout master

git merge experiment

另外一種策略就是通過變基:

git checkout experiment

git rebase master

git checkout master

git merge experiment

這兩種整合方法的最終結果都是將C3C4進行合併到主分支,結果沒有任何區別,主要是歷史記錄的區別,rebase是一種直線型的,提交歷史非常清晰整潔,而merge相對來說分支比較複雜。

儘管變基會使得我們的提交歷史變得更加簡潔,但是變基是有風險的,但是當rebase出現衝突時,處理過程比較麻煩,同時當對已經push到遠端,同事也基於你的push進行開發,然後你又對之前的push執行變基操作,就會出問題,本文開始的故事就是真實的案例。總的原則是,只對尚未推送或分享給別人的本地修改執行變基操作清理歷史, 從不對已推送至別處的提交執行變基操作

總結

git操作總的來說並不是很難,難點在於對命令的理解和記住,好好掌握,讓git成爲我們的利器而不是我們的噩夢!

最後

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 歡迎加我微信「qianyu443033099」拉你進技術羣,長期交流學習...

  3. 關注公衆號「前端下午茶」,持續爲你推送精選好文,也可以加我爲好友,隨時聊騷。

點個在看支持我吧,轉發就更好了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章