GIT 學習筆記(4) —— 儲藏/搜索/重寫歷史

交互式暫存

git add -i | git add --interactive

例:

$ git add -i
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now>

暫存補丁

從交互式提示符中,輸入 5 或 p(補丁)。 Git 會詢問你想要部分暫存哪些文件;然後,對已選擇文件的每一個部分,它都會一個個地顯示文件區別並詢問你是否想要暫存它們:

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index dd5ecc4..57399e0 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -22,7 +22,7 @@ class SimpleGit
   end

   def log(treeish = 'master')
-    command("git log -n 25 #{treeish}")
+    command("git log -n 30 #{treeish}")
   end

   def blame(path)
Stage this hunk [y,n,a,d,/,j,J,g,e,?]?

這時有很多選項。 輸入 ? 顯示所有可以使用的命令列表:

Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help

儲藏與清理

儲藏修改

git stash | git stash save

查看儲藏

git stash list

應用儲藏

git stash apply

移除儲藏

git stash drop

參考:

$ git stash list //查看儲藏
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log
$ git stash drop stash@{0} //移除儲藏stash@{0} 
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)

創造性的儲藏

Git 不儲藏任何你通過 git add 命令已暫存的東西

git stash --keep-index

$ git status -s
M  index.html
 M lib/simplegit.rb

$ git stash --keep-index
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file

$ git status -s
M  index.html

git stash --include-untrackedgit stash -u

默認情況下,git stash只會儲藏已經在索引中的文件。 如果指定 --include-untracked 或 -u 標記,Git 也會儲藏任何創建的未跟蹤文件。

$ git status -s
M  index.html
 M lib/simplegit.rb
?? new-file.txt

$ git stash -u
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file

$ git status -s
$

git stash --patch

如果指定了 --patch 標記,Git 不會儲藏所有修改過的任何東西,但是會交互式地提示哪些改動想要儲藏、哪些改動需要保存在工作目錄中。

從儲藏創建一個分支

如果儲藏了一些工作,將它留在那兒了一會兒,然後繼續在儲藏的分支上工作,在重新應用工作時可能會有問題。 如果應用嘗試修改剛剛修改的文件,你會得到一個合併衝突並不得不解決它。 如果想要一個輕鬆的方式來再次測試儲藏的改動,可以運行 git stash branch 創建一個新分支,檢出儲藏工作時所在的提交,重新在那應用工作,然後在應用成功後扔掉儲藏:

$ git stash branch testchanges
Switched to a new branch "testchanges"
# On branch testchanges
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)

這是在新分支輕鬆恢復儲藏工作並繼續工作的一個很不錯的途徑。

清理工作目錄

git clean

你需要謹慎地使用這個命令,因爲它被設計爲從工作目錄中移除未被追蹤的文件。 如果你改變主意了,你也不一定能找回來那些文件的內容。 一個更安全的選項是運行 git stash --all 來移除每一樣東西並存放在棧中。

git clean -f -d #用來移除工作目錄中所有未追蹤的文件以及空的子目錄。-f 意味着 強制 或 “確定移除”。

git clean -d -n #如果只是想要看看它會做什麼,可以使用 -n 選項來運行命令,這意味着 “做一次演習然後告訴你將要 移除什麼”。

git clean -n -d -x

默認情況下,git clean 命令只會移除沒有忽略的未跟蹤文件。 任何與 .gitiignore 或其他忽略文件中的模式匹配的文件都不會被移除。 如果你也想要移除那些文件,例如爲了做一次完全乾淨的構建而移除所有由構建生成的 .o 文件,可以給 clean 命令增加一個 -x 選項。

git clean -x -i | git clean -x --interactive

這將會以交互模式運行 clean 命令。

例:

$ git clean -x -i
Would remove the following items:
  build.TMP  test.o
*** Commands ***
    1: clean  2: filter by pattern  3: select by numbers  4: ask each  5: quit
    6: help
What now>

Git 搜索

git grep -n [abc]

-n #參數來輸出 Git 所找到的匹配行行號。

[abc] #搜索內容

git grep --count [abc] # Git 輸出概述的信息,僅僅包括哪些文件包含匹配以及每個文件包含了多少個匹配

$ git grep -p [abc] [文件名] # 查看匹配的行是屬於哪一個方法或者函數

Git 日誌搜索

$ git log -S[abc] --oneline

例如,如果我們想找到 ZLIB_BUF_MAX 常量是什麼時候引入的,我們可以使用 -S 選項來顯示新增和刪除該字符串的提交。

$ git log -SZLIB_BUF_MAX --oneline
e01503b zlib: allow feeding more than 4GB in one go
ef49a7a zlib: zlib can only process 4GB at a time

-G 使用正則表達式搜索。

行日誌搜索

git log -L :[abc]:[文件名]

例如,假設我們想查看 zlib.c 文件中git_deflate_bound 函數的每一次變更,我們可以執行git log -L :git_deflate_bound:zlib.c。 Git 會嘗試找出這個函數的範圍,然後查找歷史記錄,並且顯示從函數創建之後一系列變更對應的補丁。

如果 Git 無法計算出如何匹配你代碼中的函數或者方法,你可以提供一個正則表達式。 例如,這個命令和上面的是等同的:git log -L '/unsigned long git_deflate_bound/',/^}/:zlib.c。 你也可以提供單行或者一個範圍的行號來獲得相同的輸出。

重寫歷史

修改最後一次提交

$ git commit --amend

修改多個提交信息

$ git rebase -i [HEAD~n]

例如,如果想要修改最近三次提交信息,或者那組提交中的任意一個提交信息,將想要修改的最近一次提交的父提交作爲參數傳遞給 git rebase -i命令,即 HEAD~2^HEAD~3。 記住 ~3 可能比較容易,因爲你正嘗試修改最後三次提交;但是注意實際上指定了以前的四次提交,即想要修改提交的父提交:$ git rebase -i HEAD~3

再次記住這是一個變基命令 - 在 HEAD~3..HEAD 範圍內的每一個提交都會被重寫,無論你是否修改信息。 不要涉及任何已經推送到中央服務器的提交 - 這樣做會產生一次變更的兩個版本,因而使他人困惑。

運行這個命令會在文本編輯器上給你一個提交的列表,看起來像下面這樣:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

需要重點注意的是相對於正常使用的 log 命令,這些提交顯示的順序是相反的。 運行一次 log 命令,會看到類似這樣的東西:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

注意其中的反序顯示。 交互式變基給你一個它將會運行的腳本。 它將會從你在命令行中指定的提交(HEAD~3)開始,從上到下的依次重演每一個提交引入的修改。 它將最舊的而不是最新的列在上面,因爲那會是第一個將要重演的。

你需要修改腳本來讓它停留在你想修改的變更上。 要達到這個目的,你只要將你想修改的每一次提交前面的 ‘pick’ 改爲 ‘edit’。 例如,只想修改第三次提交信息,可以像下面這樣修改文件:

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

當保存並退出編輯器時,Git 將你帶回到列表中的最後一次提交,把你送回命令行並提示以下信息:

$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed my name a bit
You can amend the commit now, with

       git commit --amend

Once you’re satisfied with your changes, run

       git rebase --continue

這些指令準確地告訴你該做什麼。 輸入

$ git commit --amend

修改提交信息,然後退出編輯器。 然後,運行

$ git rebase --continue

這個命令將會自動地應用另外兩個提交,然後就完成了。 如果需要將不止一處的 pick 改爲 edit,需要在每一個修改爲 edit 的提交上重複這些步驟。 每一次,Git 將會停止,讓你修正提交,然後繼續直到完成。

重新排序提交

也可以使用交互式變基來重新排序或完全移除提交。 如果想要移除 “added cat-file” 提交然後修改另外兩個提交引入的順序,可以將變基腳本從這樣:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

改爲這樣:

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

當保存並退出編輯器時,Git 將你的分支帶回這些提交的父提交,應用 310154e 然後應用 f7f3f6d,最後停止。 事實修改了那些提交的順序並完全地移除了 “added cat-file” 提交。

壓縮提交

通過交互式變基工具,也可以將一連串提交壓縮成一個單獨的提交。 在變基信息中腳本給出了有用的指令:

# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

如果,指定 “squash” 而不是 “pick” 或 “edit”,Git 將應用兩者的修改併合並提交信息在一起。 所以,如果想要這三次提交變爲一個提交,可以這樣修改腳本:

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

當保存並退出編輯器時,Git 應用所有的三次修改然後將你放到編輯器中來合併三次提交信息:

# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit

# This is the 2nd commit message:

updated README formatting and added blame

# This is the 3rd commit message:

added cat-file

當你保存之後,你就擁有了一個包含前三次提交的全部變更的提交。

拆分提交

拆分一個提交會撤消這個提交,然後多次地部分地暫存與提交直到完成你所需次數的提交。 例如,假設想要拆分三次提交的中間那次提交。 想要將它拆分爲兩次提交:第一個 “updated README formatting”,第二個 “added blame” 來代替原來的 “updated README formatting and added blame”。 可以通過修改 rebase -i 的腳本來做到這點,將要拆分的提交的指令修改爲 “edit”:

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

然後,當腳本將你進入到命令行時,重置那個提交,拿到被重置的修改,從中創建幾次提交。 當保存並退出編輯器時,Git 帶你到列表中第一個提交的父提交,應用第一個提交(f7f3f6d),應用第二個提交(310154e),然後讓你進入命令行。 那裏,可以通過 git reset HEAD^ 做一次針對那個提交的混合重置,實際上將會撤消那次提交併將修改的文件未暫存。 現在可以暫存並提交文件直到有幾個提交,然後當完成時運行 git rebase --continue

$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

Git 在腳本中應用最後一次提交(a5f4a0d),歷史記錄看起來像這樣:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

再次強調,這些改動了所有在列表中的提交的 SHA-1 校驗和,所以要確保列表中的提交還沒有推送到共享倉庫中。

核武器級選項:filter-branch

有另一個歷史改寫的選項,如果想要通過腳本的方式改寫大量提交的話可以使用它 - 例如,全局修改你的郵箱地址或從每一個提交中移除一個文件。 這個命令是 filter-branch,它可以改寫歷史中大量的提交,除非你的項目還沒有公開並且其他人沒有基於要改寫的工作的提交做的工作,你不應當使用它。

從每一個提交移除一個文件 有人粗心地通過 git add . 提交了一個巨大的二進制文件,你想要從所有地方刪除它。 可能偶然地提交了一個包括一個密碼的文件,然而你想要開源項目。 filter-branch 是一個可能會用來擦洗整個提交歷史的工具。 爲了從整個提交歷史中移除一個叫做 passwords.txt 的文件,可以使用 --tree-filter 選項給 filter-branch

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

--tree-filter 選項在檢出項目的每一個提交後運行指定的命令然後重新提交結果。 在本例中,你從每一個快照中移除了一個叫作 passwords.txt 的文件,無論它是否存在。 如果想要移除所有偶然提交的編輯器備份文件,可以運行類似 git filter-branch --tree-filter 'rm -f *~' HEAD 的命令。

最後將可以看到 Git 重寫樹與提交然後移動分支指針。 通常一個好的想法是在一個測試分支中做這件事,然後當你決定最終結果是真正想要的,可以硬重置 master 分支。 爲了讓 filter-branch 在所有分支上運行,可以給命令傳遞 --all 選項。

使一個子目錄做爲新的根目錄 假設已經從另一個源代碼控制系統中導入,並且有幾個沒意義的子目錄(trunk、tags 等等)。 如果想要讓 trunk 子目錄作爲每一個提交的新的項目根目錄,filter-branch 也可以幫助你那麼做:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

現在新項目根目錄是 trunk 子目錄了。 Git 會自動移除所有不影響子目錄的提交。

全局修改郵箱地址 另一個常見的情形是在你開始工作時忘記運行 git config 來設置你的名字與郵箱地址,或者你想要開源一個項目並且修改所有你的工作郵箱地址爲你的個人郵箱地址。 任何情形下,你也可以通過 filter-branch 來一次性修改多個提交中的郵箱地址。 需要小心的是隻修改你自己的郵箱地址,所以你使用 --commit-filter

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="[email protected]";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

這會遍歷並重寫每一個提交來包含你的新郵箱地址。 因爲提交包含了它們父提交的 SHA-1 校驗和,這個命令會修改你的歷史中的每一個提交的 SHA-1 校驗和,而不僅僅只是那些匹配郵箱地址的提交。

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