-
1. 起步
-
2. Git 基礎
-
3. Git 分支
-
4. 伺服器上的 Git
- 4.1 協議
- 4.2 在伺服器上部署 Git
- 4.3 生成 SSH 公鑰
- 4.4 架設伺服器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方託管服務
- 4.10 小結
-
5. 分散式 Git
-
A1. 附錄 A: Git 在其他環境
- A1.1 圖形介面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小結
-
A2. 附錄 B: 在應用程式中嵌入 Git
-
A3. 附錄 C: Git 命令
7.6 Git 工具 - 重寫歷史
重寫歷史
在 Git 的使用過程中,你經常會想要修改你本地的提交歷史。Git 的一個優點是它允許你在最後一刻做出決定。你可以透過暫存區在提交前決定哪些檔案屬於哪個提交;你可以使用 git stash 來放棄暫時不用的工作;你也可以重寫已經發生的提交,讓它們看起來像是以不同的方式發生的。這可能包括更改提交的順序、修改提交資訊或檔案內容、合併或拆分提交,或者完全刪除提交——所有這些都可以在與他人分享你的工作之前完成。
在本節中,你將學習如何完成這些任務,以便在與他人分享之前,使你的提交歷史看起來是你想要的。
|
注意
|
在你滿意你的工作之前,不要推送
Git 的一個基本原則是,由於大部分工作都在你的本地克隆中完成,所以你有很大的自由度來*本地*重寫你的歷史。但是,一旦你推送了你的工作,情況就完全不同了,你應該將推送過的作為最終版本,除非有充分的理由去修改。簡而言之,在你對你的工作滿意並準備好與世界分享之前,你應該避免推送你的工作。 |
修改最後一次提交
修改你最近一次提交可能是你最常進行的歷史重寫。你經常會對最後一次提交做兩件基本的事情:簡單地修改提交資訊,或者透過新增、刪除和修改檔案來改變提交的實際內容。
如果你只想修改你最後一次提交的訊息,這很簡單
$ git commit --amend
上面的命令會將之前的提交訊息載入到編輯器會話中,你可以在其中修改訊息,儲存更改並退出。當你儲存並關閉編輯器時,編輯器會建立一個包含更新後提交訊息的新提交,並使其成為你最新的提交。
另一方面,如果你想改變最後一次提交的*內容*,過程基本相同——首先進行你認為遺漏的更改,暫存這些更改,然後隨後的 git commit --amend 會用你新的、改進的提交*替換*最後一次提交。
你需要小心使用此技術,因為修改會改變提交的 SHA-1 值。這就像一次非常小的 rebase——如果你已經推送了最後一次提交,請不要修改它。
|
提示
|
修改過的提交可能(或可能不需要)修改提交資訊
當你修改提交時,你有機會同時修改提交訊息和提交內容。如果你大幅修改了提交內容,你幾乎肯定應該更新提交訊息以反映修改後的內容。 另一方面,如果你的修改足夠瑣碎(例如,修復一個愚蠢的拼寫錯誤或添加了一個你忘記暫存的檔案),以至於之前的提交訊息仍然適用,你可以簡單地進行修改,暫存它們,然後使用以下命令完全避免不必要的編輯器會話:
|
修改多個提交訊息
要修改歷史中更靠前的提交,你必須使用更復雜的工具。Git 沒有一個修改歷史的工具,但你可以使用 rebase 工具將一系列提交 rebase 到它們最初基於的 HEAD,而不是將它們移動到另一個 HEAD。使用互動式 rebase 工具,你可以停在你想修改的每個提交之後,然後修改訊息、新增檔案,或者做任何你想做的事情。透過給 git rebase 新增 -i 選項,你可以互動式地執行 rebase。你必須透過告訴命令要 rebase 到哪個提交來指定你要重寫多遠的提交。
例如,如果你想修改最後三個提交的訊息,或者該組中的任何一個提交的訊息,你可以將你想要編輯的最後一個提交的父提交作為引數傳遞給 git rebase -i,即 HEAD~2^ 或 HEAD~3。記住 ~3 可能更容易,因為你想編輯最後三個提交,但要記住,你實際上是在指定四個提交之前的那個提交,即你想要編輯的最後一個提交的父提交。
$ git rebase -i HEAD~3
再次記住,這是一個 rebase 命令——範圍 HEAD~3..HEAD 內的每個提交,如果其訊息已更改,*及其所有後代*都將被重寫。不要包含任何你已經推送到中央伺服器的提交——這樣做會透過提供同一更改的替代版本來混淆其他開發者。
執行此命令會在你的文字編輯器中得到一個提交列表,看起來像這樣:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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 Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit
注意相反的順序。互動式 rebase 提供了一個它將要執行的指令碼。它將從你在命令列中指定的提交(HEAD~3)開始,並從上到下重放這些提交中的每個更改。它列出最舊的在最上面,而不是最新的,因為那是它將要重放的第一個。
你需要編輯指令碼,使其在你想編輯的提交處停止。為此,將“pick”替換為“edit”,用於你希望指令碼停止的每個提交。例如,如果你只想修改第三個提交的訊息,你可以將檔案修改成這樣:
edit f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
當你儲存並退出編輯器時,Git 會將你回溯到列表中最後一個提交,並在命令列上給你以下訊息:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... Change 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 都會停止,讓你修改提交,並在你完成後繼續。
重新排序提交
你也可以使用互動式 rebase 來重新排序或完全刪除提交。如果你想刪除“Add cat-file”提交併更改其他兩個提交的引入順序,你可以將 rebase 指令碼從這個:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
改為這個:
pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit
當你儲存並退出編輯器時,Git 會將你的分支回溯到這些提交的父提交,應用 310154e,然後是 f7f3f6d,然後停止。你有效地更改了這些提交的順序,並完全刪除了“Add cat-file”提交。
合併提交
使用互動式 rebase 工具,你也可以將一系列提交合併成一個提交。指令碼在 rebase 訊息中提供了有用的說明:
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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
如果,而不是“pick”或“edit”,你指定“squash”,Git 會應用該更改及其直接前面的更改,並讓你合併提交訊息。所以,如果你想將這三個提交合併成一個提交,你可以將指令碼修改成這樣:
pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file
當你儲存並退出編輯器時,Git 會應用所有三個更改,然後將你帶回編輯器以合併三個提交訊息:
# This is a combination of 3 commits.
# The first commit's message is:
Change my name a bit
# This is the 2nd commit message:
Update README formatting and add blame
# This is the 3rd commit message:
Add cat-file
當你儲存它後,你就擁有了一個提交,它包含了之前所有三個提交的更改。
拆分提交
拆分提交是撤銷一個提交,然後根據你想要的結果數量分多次暫存和提交。例如,假設你想拆分你三個提交中的中間一個。與其“Update README formatting and add blame”,不如將其拆分成兩個提交:“Update README formatting”作為第一個,然後“Add blame”作為第二個。你可以在 rebase -i 指令碼中透過將你想拆分的提交的指令更改為“edit”來做到這一點:
pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
然後,當指令碼將你帶到命令列時,你重置該提交,獲取已重置的更改,並從中建立多個提交。當你儲存並退出編輯器時,Git 會回溯到列表中第一個提交的父提交,應用第一個提交(f7f3f6d),應用第二個(310154e),並將你帶到控制檯。在那裡,你可以使用 git reset HEAD^ 進行混合重置,這實際上撤銷了該提交併使已修改的檔案處於未暫存狀態。現在你可以暫存並提交檔案,直到你有了幾個提交,並在完成後執行 git rebase --continue。
$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue
Git 應用指令碼中的最後一個提交(a5f4a0d),你的歷史看起來像這樣:
$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit
這會改變你列表中三個最近提交的 SHA-1 值,所以請確保列表中沒有你已經推送到共享倉庫的已更改提交。請注意,列表中最後一個提交(f7f3f6d)保持不變。儘管此提交顯示在指令碼中,但因為它被標記為“pick”並在任何 rebase 更改之前應用,Git 會將該提交保持不變。
刪除提交
如果你想擺脫一個提交,你可以使用 rebase -i 指令碼來刪除它。在提交列表中,在你想刪除的提交前加上“drop”字樣(或者直接從 rebase 指令碼中刪除該行):
pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken
由於 Git 構建提交物件的方式,刪除或修改一個提交會導致其後所有提交的重寫。你的倉庫歷史越靠後,需要重新建立的提交就越多。如果你在序列的後面有許多依賴於你剛剛刪除的提交,這可能會導致很多合併衝突。
如果你在進行 rebase 時遇到中途,並且決定這不是一個好主意,你可以隨時停止。輸入 git rebase --abort,你的倉庫將恢復到 rebase 開始之前的狀態。
如果你完成了一個 rebase 並決定它不是你想要的,你可以使用 git reflog 來恢復你分支的早期版本。有關 reflog 命令的更多資訊,請參閱資料恢復。
|
注意
|
Drew DeVault 建立了一個實用的實踐指南,其中包含學習如何使用 |
核選項:filter-branch
如果你需要以某種可指令碼化的方式重寫大量提交,還有一個歷史重寫選項——例如,全域性更改你的電子郵件地址或從每個提交中刪除一個檔案。該命令是 filter-branch,它可以重寫你的歷史的絕大部分,所以除非你的專案還沒有公開,並且其他人還沒有基於你即將重寫的提交進行工作,否則你不應該使用它。但是,它可能非常有用。你將學習一些常見用法,以便了解它的一些能力。
|
警告
|
|
從每個提交中刪除檔案
這種情況相當普遍。有人意外地透過一個不加思索的 git add . 提交了一個巨大的二進位制檔案,而你想將其從所有地方刪除。也許你不小心提交了一個包含密碼的檔案,而你想讓你的專案開源。filter-branch 是你可能想要用來清理你整個歷史的工具。要從你的整個歷史中刪除一個名為 passwords.txt 的檔案,你可以使用 filter-branch 的 --tree-filter 選項。
$ 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 重寫樹和提交,然後在最後移動分支指標。通常,最好在一個測試分支中執行此操作,然後在確定結果確實是你想要的之後,使用 hard-reset 你的 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="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
這會遍歷並重寫每個提交,使其包含你的新地址。由於提交包含其父提交的 SHA-1 值,此命令會更改你歷史中的每個提交 SHA-1,而不僅僅是那些具有匹配電子郵件地址的提交。