git-flight-rules/README_zh-TW.md

55 KiB
Raw Blame History

Git 飛行規則Flight Rules

🌍 EnglishEspañolРусский繁體中文簡體中文한국어Tiếng ViệtFrançais日本語

前言

  • 英文原版 README
  • 翻譯可能存在錯誤或不標準的地方,歡迎大家指正和修改,謝謝!

什麼是「飛行規則」?

這是一篇給太空人(這裡就是指使用 Git 的程式設計師們)的指南,用來指導問題出現後的應對之法。

飛行規則flight rules是記錄在手冊上的來之不易的一系列知識記錄了某個事情發生的原因以及怎樣一步一步的進行處理。本質上它們是特定場景的非常詳細的標準處理流程。[...]

自 20 世紀 60 年代初以來NASA 一直在捕捉capturing失誤、災難和解決方案。當時水星時代Mercury-era的地面小組首先開始將「經驗教訓」收集到一個綱要compendium該綱現在已經有上千個問題情景從發動機故障、到破損的艙口把手、再到計算機故障以及它們對應的解決方案。

——Chris Hadfield《一個太空人的生活指南》An Astronaut's Guide to Life

這篇文章的約定

為了清楚的表述,這篇文件裡的所有例子使用了自訂的 Bash 提示字元,以便指示目前分支和是否有暫存的更動。分支名用小括號括起來,分支名後面跟的 * 表示暫存的更動。

Join the chat at https://gitter.im/k88hudson/git-flight-rules

Table of Contents generated with DocToc

版本庫

我想建立本機版本庫

將現有目錄初始化為 Git 版本庫:

(my-folder)$ git init

我想複製遠端版本庫

要複製遠端版本庫,找到該版本庫的 URL然後執行

$ git clone [URL]

版本庫會複製到與其名稱相同的目錄中。確保你能連線到你要從之複製的遠端伺服器(通常來說,這表示你要連接上網際網路。)

如果想要將版本庫複製到不同名稱的目錄:

$ git clone [URL] [目錄名稱]

我設定了錯的遠端版本庫

有幾種可能的情況:

如果你複製錯了遠端版本庫,刪掉 git clone 創建的目錄,然後複製正確的版本庫就好。

如果你為本機版本庫設定了錯的遠端,你可以用以下命令變更 origin 的 URL

$ git remote set-url origin [正確的 URL]

詳見 此 StackOverflow

我想將程式碼加到其他人的版本庫中

Git 不允許沒有存取權的人將程式碼加到其他人的版本庫。GitHub一個 Git 版本庫的託管服務,也不行。不過你可以建議程式碼,透過修補,或者用 GitHub 的分叉與拉取請求。

關於分叉:分叉是版本庫的複本。這不是 Git 的功能,但是是 GitHub、Bitbucket、GitLab——或其他任何託管 Git 版本庫的地方——上很常見的動作。你可以透過其 UI 分叉版本庫。

透過拉取請求建議程式碼

在分叉版本庫後,你只需要一般地將其複製到你的機器上。你可以在 GitHub 上做一些小修改,而不需要複製,但這不是一份 GitHub 飛行規則,所以我們還是來談談該怎麼在本機上操作吧。

# 如果你用 SSH 的話:
$ git clone git@github.com:k88hudson/git-flight-rules.git

# 如果你用 HTTPS 的話:
$ git clone https://github.com/k88hudson/git-flight-rules.git

如果你 cd 進創建的目錄,然後輸入 git remote,你會看到遠端的列表。基本上,會有一個遠端——origin——指向 k88hudson/git-flight-rules。在這個情況下,我們也希望有一個指向你的分叉的遠端。

首先,按照 Git 的慣例,我們用 origin 表示你的分叉、upstream 表示原本的版本庫。因此,先將 origin 重新命名為 upstream

$ git remote rename origin upstream

你也可以用 git remote set-url 來達到同樣的結果,但會花費更多步驟。

接著,設定一個新的遠端指向你的版本庫:

$ git remote add origin git@github.com:YourName/git-flight-rules.git

現在有兩個遠端了:

  • origin 指向你分叉版本庫。
  • upstream 則指向原版版本庫。

origin 可以讀寫,upstream 則是唯讀的。

當你完成更動後,推送你(通常在某個分支上的)更動到遠端 origin。如果是在分支上,你可以用 --set-upstream 來避免每次推送都要指定遠端分支。例如:

(feature/my-feature)$ git push --set-upstream origin feature/my-feature

你沒辦法用 Git 在 CLI 中發起拉取請求(但有一些工具可以幫你做這件事,例如 hub)。所以當你準備好時,到 GitHub或其他 Git 託管服務)上創建拉取請求。

之後也別忘了回覆程式碼的檢閱回饋。

透過修補建議程式碼

另一個建議更動的方法是用 git format-patch,這不依賴於如 GitHub 的第三方服務。

format-patch 會為你的提交創建一個 .patch 檔案,本質上就是類似於能在 GitHub 上看到的提交差異的更動列表。

git am 可以用於查看、甚至編輯、套用修補檔。

例如,要基於前一個提交創建修補檔,你可以執行 git format-patch HEAD^,來創建一個名字類似於 0001-提交訊息.patch 的修補檔。

將修補檔套用至你的版本庫則需執行 git am ./0001-提交訊息.patch

修補檔也可以以 git send-email 命令透過 email 傳送。使用與組態資訊參見:https://git-send-email.io

我需要將分叉更新到原版的最新進度

在一段時間後,upstream 版本庫可能更新了,而這些更新需要拉取至你的 origin 版本庫。

你可能已經設定了指向原版的遠端。如果還沒的話,執行以下命令。通常我們用 upstream 作為名稱:

$ git remote add upstream [原版的 URL]

現在你可以從 upstream 抓取,並取得最新更新了。

(main)$ git fetch upstream
(main)$ git merge upstream/main

# 或者:
(main)$ git pull upstream main

編輯提交

我剛才提交了什麼?

如果你盲目地用 git commit -a 提交了更動,而不確定到底提交了哪些內容,可以用以下命令顯示目前 HEAD 上的最近一次的提交:

(main)$ git show

或者

$ git log -n1 -p

如果你想查看特定提交的特定檔案,你可以用:([提交] 是你想要的提交)

$ git show [提交]:filename

我的提交訊息寫錯了

如果你的提交訊息寫錯了,且這次提交還沒有推送,可以透過下面的方法來修改提交訊息:

$ git commit --amend --only

這會開啟你的預設編輯器來編輯訊息。你也可以選擇只靠一個命令來做這些事:

$ git commit --amend --only -m 'xxxxxxx'

如果你已經推送了提交,可以在修改後強制推送,但是不推薦這麼做。

我提交裡的使用者名稱和信箱不對

如果只是單個提交有錯,修正它:

$ git commit --amend --no-edit --author "New Authorname <authoremail@mydomain.com>"

另一個方法是在 git config --global author.(name|email) 正確配置你的作者資訊,然後用:

$ git commit --amend --no-edit --reset-author

如果你需要修改所有歷史記錄,參考 git filter-branch 的手冊頁。

我想從一個提交裡移除一個檔案

要從一個提交裡移除一個檔案:

$ git checkout HEAD^ filename
$ git add filename
$ git commit --amend --no-edit

如果該檔案是新加入的,而你想要(從 Git刪除它

$ git rm --cached filename
$ git commit --amend --no-edit

當你有一個開放的修補,而你往上面提交了一個不必要的檔案,需要強制推送去更新這個遠端修補時,這非常有用。--no-edit 選項將保留現有提交訊息。

我想將更動從一個提交移到另一個

如果你在一個提交作了一個更動,而它更符合另一個提交做的事,你可以用互動式重定基底將更動移動過去。這節來自 StackOverflow

假設你有三個提交,abcb 變更了 file1file2,你想要把 file1 的更動從 b 移到 a

首先,互動式重定基底:

$ git rebase -i HEAD~3

這會打開包含以下內容的編輯器:

pick a
pick b
pick c

ab 那行改為 edit

edit a
edit b
pick c

儲存並關閉編輯器後,你會被帶到 b。重設 file1 的更動:

$ git reset HEAD~1 file1

這會取消暫存 file1 的更動。貯存更動然後繼續重定基底:

$ git stash
$ git rebase --continue

現在要編輯 a。彈出貯存,將更動加入這個提交,然後繼續重定基底:

$ git stash pop
$ git add file1
$ git commit --amend --no-edit
$ git rebase --continue

現在你完成重定基底,並將更動從 b 移到 a 了。如果你要將更動從 b 移到 c,因為 cb 之前,你會需要重定基底兩次,一次將更動從 b 取出,一次將更動加入到 c

我想刪除我最後一次提交

如果你需要刪除推送了的提交,你可以使用以下方法。但是,這將不可逆的改變你的歷史記錄,也會搞亂那些已經從該版本庫拉取了的人的歷史記錄。簡而言之,如果你不是很確定,千萬不要這麼做。

$ git reset HEAD^ --hard
$ git push --force-with-lease [遠端] [分支]

如果你還沒有推送到遠端,重設到你最後一次提交前的狀態就可以了(同時保存暫存的更動):

(my-branch)$ git reset --soft HEAD^

這只能在推送之前使用。如果你已經推送了,唯一安全的做法是 git revert [不要的提交],那會創建一個新的提交來還原前一個提交的所有更動;或者,如果這個分支是重定基底安全的(即其他開發者不會從這個分支拉取),只需要使用 git push --force-with-lease,參見這一節前半部分。

刪除任意提交

同樣,除非必須,否則不要這麼做。

$ git rebase --onto [不要的提交]^ [不要的提交]
$ git push --force-with-lease [遠端] [分支]

或者使用互動式重定基底刪除那些你想要刪除的提交所對應的行。

我嘗試推送一個修正後的提交到遠端,但是報錯

To https://github.com/yourusername/repo.git
! [rejected]        mybranch -> mybranch (non-fast-forward)
error: failed to push some refs to 'https://github.com/tanay1337/webmaker.org.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

注意,重定基底和修正會用新的提交取代舊的,所以如果舊的提交已經推送到遠端上了,那你必須強制推送。注意:總是確保你指明一個分支!

(my-branch)$ git push --force-with-lease origin mybranch

一般來說,應該避免強制推送。最好是創建和推送一個新的提交,而不是強制推送一個修正後的提交。後者會使其他開發者該分支或其子分支的歷史記錄,與遠端歷史記錄產生衝突。--force-with-lease 仍然可能失敗,如果有人在同樣的分支上推送了提交,而你的推送會覆蓋他們的更動。

如果你完全確定沒有人在同一個分支上工作,或者你想要無條件更新分支的頂端,你可以用 --force-f),但通常應該避免。

我意外地硬性重設了,我想找回我的內容

如果你意外地做了 git reset --hard,你通常能找回你的提交,因為 Git 對每件事都會有日誌,且都會保存幾天。

注意,只適用於你的作業備份了的情況,即有提交或貯存。git reset --hard 會移除未提交的更動,所以請謹慎使用。(更安全的選擇是 git reset --keep。)

(main)$ git reflog

你將會看到一個你過去提交的列表,和一個重設的提交。選擇你想要回到的提交的 SHA再重設一次

(main)$ git reset --hard [SHA]

我意外地提交並推送了合併

如果你意外地將功能分支在準備好合併之前,合併進了主開發分支,你可以撤銷合併。但有個問題:合併提交有多於一個親代(通常是兩個)。

使用命令:

(feature-branch)$ git revert -m 1 [提交]

-m 1 選項的意思是選擇 1 號親代(合併進去的分支)作為要還原到的親代。

注意,親代編號不是提交識別符。合併提交有一行 Merge: 8e2ce2d 86ac2e7,親代編號是這行上的親代從 1 開始的索引,第一個識別符是 1 號親代,第二個是 2 號,以此類推。

我意外地提交並推送了敏感資料

如果你意外地推送了包含敏感或私人資料(密碼、金鑰等)的檔案,你可以修正該提交。記得,你應該認定其中的所有資料都外洩了。以下步驟可以從你的本機複本和公開版本庫移除敏感資料,但你不能從別人拉取的複本移除敏感資料。如果你提交了密碼,立刻變更,如果你提交了金鑰,立刻重新生成。修正推送的提交並不夠,任何人都可能拉取了包含敏感資料的原提交。

如果你編輯檔案移除了敏感資料,執行:

$ git add [編輯過的檔案]
$ git commit --amend --no-edit
$ git push --force-with-lease [遠端] [分支]

或是將敏感資料儲存在本機環境變數。

如果你希望移除整個檔案(但保留在本機),執行:

$ git rm --cached [敏感的檔案]
$ echo [敏感的檔案] >> .gitignore
$ git add .gitignore
$ git commit --amend --no-edit
$ git push --force-with-lease [遠端] [分支]

如果你同時做了其他提交(即,敏感資料不是在上一個提交引入的),你需要重定基底。

我想要從現有的版本庫歷史記錄中移除大檔案

如果你想移除的檔案是機密或敏感的,見〈我意外地提交並推送了敏感資料〉

即使你在最近的提交中刪除了大或不想要的檔案,它仍然存於 Git 歷史記錄,存於版本庫的 .git 目錄中,且 git clone 也會下載到不需要的檔案。

這節中提到的操作需要強制推送,且會重寫版本庫大部分的歷史,所以如果你與其他人在進行遠端協作,請先確認他們的本機複本都推送了。

有兩個方法可以重寫歷史記錄:內建的 git filter-branchBFG Repo-Cleaner。BFG 能以更好的效能顯著清理,但是是第三方,且需要 Java。這裡兩種選擇都會描述。最後一步是強制推送這需要比起一般強制推送更再三考慮因為這會永久變更版本庫大量的歷史記錄。

推薦的工具:第三方的 BFG

BFG Repo Cleaner 需要 Java。到[這裡]下載 BFG 的 JAR 檔。以下例子中將假設檔案名稱為 bfg.jar,位於 ~/Downloads/

刪除特定檔案:

$ git rm path/filename
$ git commit
$ java -jar ~/Downloads/bfg.jar --delete-files filename

注意,你應該直接給 BFG 檔案名稱,即使其在子目錄中。

你也可以用 glob 模式刪除檔案:

$ git rm *.jpg
$ git commit
$ java -jar ~/Downloads/bfg.jar --delete-files *.jpg

BFG 不會影響在最新提交中存在的檔案。例如,版本庫中有幾個大 .tga 檔,在之前的提交移除了其中一些,在最新的提交還存在的檔案則不會觸及。

注意,如果你重新命名了檔案,例如原本是 LargeFileFirstName.mp4 的檔案在一次提交中重新命名為 LargeFileSecondName.mp4java -jar ~/Downloads/bfg.jar --delete-files LargeFileSecondName.mp4 並不會從 Git 歷史記錄中刪除這個檔案。需要對兩個檔案名都執行一次,或使用匹配到兩個檔案名的模式。

內建的工具:git filter-branch

git filter-branch 較為麻煩且功能較少,但如果你不能安裝或執行 BFG你可以使用它。

將以下例子中的 filepattern 替換為某個檔案名稱或模式,例如 *.jpg。所有分支的歷史記錄中匹配到的檔案都會被移除。

$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch filepattern' --prune-empty --tag-name-filter cat -- --all

解釋:

--tag-name-filter cat 是使用 cat 命令將原本的標籤套用至新提交上,麻煩、但最簡單的方法。

--prune-empty 移除任何變為空的提交。

最後一步:推送你變更過的歷史

一旦你移除了檔案,細心地測試一下你沒有弄壞版本庫的任何東西——如果有,最簡單的方法是重新複製一個,然後重頭來過。最後,可以用 Git 垃圾收集來最小化你本機 .git 目錄的大小,然後強制推送。

$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
$ git push --force --tags [遠端]

由於你重寫了整個歷史記錄,git push 可能會需要推送太多資料,導致回傳錯誤 The remote end hung up unexpectedly(遠端意外掛斷了)。如果出現這個問題,可以嘗試增加 Git post 緩衝區大小:

$ git config http.postBuffer 524288000
$ git push --force

如果沒有用的話,你需要手動將提交分成多塊推送歷史記錄。試著增加以下命令中的 <數量> 直到成功推送:

(main)$ git push -u [遠端] HEAD~<數量>:refs/head/main --force

第一次成功推送後,逐步減少 <數量>,直到可以成功使用常規的 git push

我需要變更非最新者的提交的內容

如果你創建了好幾個提交,然後發現自己少做了一些應該在其中的第一個提交做的事。若創建新提交來放這些少做的更動,你會有乾淨的版本庫,但是你的提交便不是「原子」的(也就是說,應該在一起的更動沒有在同一個提交)。因此,你可能會想變更第一個提交,也就是這些更動應該在的地方,並保持其後的提交不變。這種情況下,重定基底或許能幫上忙。

假設你要變更第三新的提交。

$ git rebase -i HEAD~4

開始互動式重定基底後,你可以編輯最新的三個提交。啟動的文字編輯器會顯示類似這樣的內容:

pick 9e1d264 第三新的提交
pick 4b6e19a 第二新的提交
pick f4037ec 最新的提交

將其改為:

edit 9e1d264 第三新的提交
pick 4b6e19a 第二新的提交
pick f4037ec 最新的提交

然後儲存並退出編輯器。這表示你想要編輯第三新的提交,並保持其他兩者不變。重定基底會在你想編輯的提交停下,你可以做原本少了的更動,和往常一樣編輯、暫存,然後:

$ git commit --amend

這會修正這個提交,也就是用原本的更動和新做出的更動,重新創建一個提交來替換掉舊的。接著繼續重定基底就好了:

$ git rebase --continue

暫存

我需要把暫存的內容添加到上一次的提交

(my-branch*)$ git commit --amend

我想要暫存一個新檔案的一部分,而不是這個檔案的全部

一般來說,如果你想暫存一個檔案的一部分,你可以使用以下命令來開啟互動式介面,並使用 s 選項來選擇想要的行。

$ git add --patch filename.x # 或 `-p`。

然而,當這個檔案是新的,則需改用以下命令:

$ git add -N filename.x

然後,你需要用 e 選項來選擇需要添加的行,執行 git diff --cached 將會顯示哪些行暫存了、哪些行只是保存在本機了。

我想把在一個檔案裡的更動加到兩個提交裡

git add 會把整個檔案加入到一個提交。git add -p 則允許你互動式地選擇想要提交的部分。

我想把暫存的內容變成未暫存,把未暫存的內容暫存起來

多數情況下,你應該將所有的內容變為未暫存,然後再加入你想要的內容提交。但如果你就是想這麼做,你可以創建一個臨時的提交來儲存你已暫存的內容,然後加入未暫存的內容並貯存起來。最後,重設最後一個提交將原本暫存的內容變為未暫存,最後彈出貯存。

$ git commit -m "WIP"     # 將之前已暫存的內容提交。
$ git add .               # 加入未暫存的內容。
$ git stash               # 貯存剛剛加入的內容。
$ git reset HEAD^         # 重設到父提交。
$ git stash pop --index 0 # 彈出貯存。

註一:這裡使用 pop 僅僅是因為想盡可能保持冪等。

註二:假如不加上 --index,會把暫存的檔案標記為未暫存。這裡解釋得比較清楚。其大意是說這是一個較為底層的問題貯存時會創建兩個提交一個記錄索引狀態、暫存的內容等另一個紀錄工作區和其他的一些東西如果你不在套用時指定索引Git 會把兩個一起銷毀,所以暫存區裡就空了)。

未暫存的更動

我想把未暫存的更動移動到新分支

$ git checkout -b my-branch

我想把未暫存的更動移動到另一個已存在的分支

$ git stash
$ git checkout my-branch
$ git stash pop

我想丟棄本地未提交的更動

如果你只是想重設遠端和你本機之間的一些提交,你可以:

# one commit
(my-branch)$ git reset --hard HEAD^
# two commits
(my-branch)$ git reset --hard HEAD^^
# four commits
(my-branch)$ git reset --hard HEAD~4
# or
(main)$ git checkout -f

如果要重設某個特定檔案,可以用檔案名做為引數:

$ git reset filename

我想丟棄某些未暫存的更動

如果你想丟棄工作複本中的一部分內容,而不是全部。

簽出不需要的內容,保留需要的。

$ git checkout -p
# Answer y to all of the snippets you want to drop

另外一個方法是使用貯存,貯存所有要保留的更動,重設工作複本,然後把貯存彈出。

$ git stash -p
# Select all of the snippets you want to save
$ git reset --hard
$ git stash pop

或者,貯存你不需要的部分,然後捨棄貯存。

$ git stash -p
# Select all of the snippets you don't want to save
$ git stash drop

分支

我從錯誤的分支拉取了內容,或把內容拉取到了錯誤的分支

這是另外一種可以使用 git reflog 情況,找到在這次錯誤拉取之前 HEAD 的指向。

(main)$ git reflog
ab7555f HEAD@{0}: pull origin wrong-branch: Fast-forward
c5bc55a HEAD@{1}: checkout: checkout message goes here

然後,重設分支到所需的提交:

$ git reset --hard c5bc55a

完成。

我想丟棄本地的提交,以讓分支與遠端保持一致

首先,確認你沒有推送你的內容到遠端。

git status 會顯示本機領先遠端多少個提交:

(my-branch)$ git status
# On branch my-branch
# Your branch is ahead of 'origin/my-branch' by 2 commits.
#   (use "git push" to publish your local commits)
#

一種方法是:

(my-branch)$ git reset --hard origin/my-branch

我需要提交到一個新分支,但錯誤的提交到了 main

main 下創建一個新分支:

(main)$ git branch my-branch

main 重設到前一個提交:

(main)$ git reset --hard HEAD^

HEAD^HEAD^1 的縮寫,你可以指定數字來進一步重設。或者,如果你不想使用 HEAD^,你可以指定一個提交的雜湊值(可以使用 git log 查看),如 git reset --hard a13b85e

簽出到剛才新建的分支繼續工作:

(main)$ git checkout my-branch

我想保留來自另外一個 ref-ish 的整個檔案

假設你正在做一個探針解決方案(註),有成百上千的內容。當你提交到一個分支,儲存工作內容:

(solution)$ git add -A && git commit -m "Adding all changes from this spike into one big commit."

當你想要把它放到一個分支裡(假設是 develop),你希望保持整個檔案的完整,並將大的提交分割成數個小的。

假設這裡有:

  • 分支 solution,擁有原型方案,領先 develop 分支。
  • 分支 develop,應用原型方案的一些內容。

可以將內容放到那個分支中:

(develop)$ git checkout solution -- file1.txt
(develop)$ git status
# On branch develop
# Your branch is up-to-date with 'origin/develop'.
# Changes to be committed:
#  (use "git reset HEAD <file>..." to unstage)
#
#        modified:   file1.txt

然後,普通地提交。

探針解決方案spike solution旨在分析或解決問題。當所有人都清楚瞭解問題後這些方案將用於估計或被丟棄。參見 Wikipedia

我把幾個提交提交到了同一個分支,而這些提交應該在不同的分支上

假設在 main 分支,執行 git log 的結果如下:

(main)$ git log

commit e3851e817c451cc36f2e6f3049db528415e3c114
Author: Alex Lee <alexlee@example.com>
Date:   Tue Jul 22 15:39:27 2014 -0400

    Bug #21 - Added CSRF protection

commit 5ea51731d150f7ddc4a365437931cd8be3bf3131
Author: Alex Lee <alexlee@example.com>
Date:   Tue Jul 22 15:39:12 2014 -0400

    Bug #14 - Fixed spacing on title

commit a13b85e984171c6e2a1729bb061994525f626d14
Author: Aki Rose <akirose@example.com>
Date:   Tue Jul 21 01:12:48 2014 -0400

    First commit

要把 e3851e85ea5173 分別移到新的分支,首先,要把 main 分支重設到正確的提交(a13b85e

(main)$ git reset --hard a13b85e
HEAD is now at a13b85e

新增一個分支:

(main)$ git checkout -b 21

接著,然後揀選提交到正確的分支上。這意味著我們將直接在 HEAD 上面套用這個提交。

(21)$ git cherry-pick e3851e8

這可能會造成衝突,參見〈互動式重定基底衝突〉來解決衝突。

同樣地,為 5ea5173 也創建一個分支,並把提交揀選到其上:

(main)$ git checkout -b 14
(14)$ git cherry-pick 5ea5173

我想刪除上游刪除了的本地分支

比方說,在 GitHub 中,合併了拉取請求後,就可以刪除掉分支。如果不準備繼續在這個分支上工作,刪除這個分支會使工作複本更乾淨。

$ git fetch -p

我不小心刪除了分支

如果你定期推送到遠端,多數情況下應該是安全的,但有時可能刪除了還沒推送的分支。

為了模擬這種情況,首先,創建一個分支和一個檔案:

(main)$ git checkout -b my-branch
(my-branch)$ touch foo.txt
(my-branch)$ ls
README.md foo.txt

加入更動並提交:

(my-branch)$ git add .
(my-branch)$ git commit -m 'foo.txt added'
(my-branch)$ git log

commit 4e3cd85a670ced7cc17a2b5d8d3d809ac88d5012
Author: siemiatj <siemiatj@example.com>
Date:   Wed Jul 30 00:34:10 2014 +0200

    foo.txt added

commit 69204cdf0acbab201619d95ad8295928e7f411d5
Author: Kate Hudson <katehudson@example.com>
Date:   Tue Jul 29 13:14:46 2014 -0400

    Fixes #6: Force pushing after amending commits

現在,切回 main 分支,並「不小心」刪除了 my-branch

(my-branch)$ git checkout main
Switched to branch 'main'
Your branch is up-to-date with 'origin/main'.
(main)$ git branch -D my-branch
Deleted branch my-branch (was 4e3cd85).
(main)$ echo oh noes, deleted my branch!
oh noes, deleted my branch!

你應該想起了 reflog,它記錄了所有動作。

(main)$ git reflog
69204cd HEAD@{0}: checkout: moving from my-branch to main
4e3cd85 HEAD@{1}: commit: foo.txt added
69204cd HEAD@{2}: checkout: moving from main to my-branch

如你所見,其中包含了刪除分支的提交的雜湊值。可以藉此把提交找回來:

(main)$ git checkout -b my-branch-help
Switched to a new branch 'my-branch-help'
(my-branch-help)$ git reset --hard 4e3cd85
HEAD is now at 4e3cd85 foo.txt added
(my-branch-help)$ ls
README.md foo.txt

我們把遺失的檔案找回來了。Git 的 reflog 在重定基底出錯時也同樣有用。

我想刪除一個分支

刪除一個遠端分支:

(main)$ git push origin --delete my-branch

或:

(main)$ git push origin :my-branch

刪除一個本機分支:

(main)$ git branch -D my-branch

我想從別人正在工作的遠端分支簽出一個分支

首先,從遠端抓取所有分支:

(main)$ git fetch --all

假設你想要從遠端的 daves 分支簽出到本機的 daves

(main)$ git checkout --track origin/daves
Branch daves set up to track remote branch daves from origin.
Switched to a new branch 'daves'

--trackgit checkout -b [branch] [remotename]/[branch] 的縮寫。)

這樣就有 daves 的本機複本了。

重定基底與合併

撤銷重定基底或合併

你可能對一個錯誤的分支做了重定基底或合併或者無法完成重定基底或合併。Git 在進行危險操作時,會將原本的 HEAD 存成 ORIG_HEAD,因此可以很容易的恢復到之前的狀態。

(my-branch)$ git reset --hard ORIG_HEAD

我做了重定基底,但是我不想強制推送

不幸的是,如果你想把重定基底的結果反映在遠端分支上,你必須強制推送。因為你改變了歷史記錄,遠端不會接受使用快轉,而必須強制推送。這就是許多人使用合併工作流程、而不是重定基底工作流程的主要原因之一,開發者的強制推送會使大團隊陷入麻煩。

一種安全的方式是,不要推送到遠端:

(main)$ git checkout my-branch
(my-branch)$ git rebase -i main
(my-branch)$ git checkout main
(main)$ git merge --ff-only my-branch

參見此 StackOverflow 討論串

我需要組合幾個提交

假設你的工作分支將對 main 分支做拉取請求。

最簡單的情況下,不會關心提交的時間戳,只想將所有的提交組合成一個單獨的提交,你可以重設和重新提交。確保 main 是最新的,且你的更動都已經提交,然後:

(my-branch)$ git reset --soft main
(my-branch)$ git commit -am "New awesome feature"

如果你想保留更多控制、保留時間戳,你需要互動式重定基底:

(my-branch)$ git rebase -i main

如果沒有相對於其他分支,將不得不相對於 HEAD 重定基底。例如,要組合最近的兩次提交,需相對於 HEAD~2 重定基底,組合最近三次提交,則相對於 HEAD~3,以此類推。

(main)$ git rebase -i HEAD~2

在執行了互動式重定基底的命令後,你會在編輯器裡看到類似以下的內容:

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
pick b729ad5 fixup
pick e3851e8 another fix

# Rebase 8074d12..b729ad5 onto 8074d12
#
# 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

(以 # 開頭的行是註解,不影響重定基底。)

你可以以註解中提到的命令替換 pick也可以刪除一行來刪除對應的提交。例如如果要保留最舊first的提交並將其他組合成第二個提交應該將第二個提交之後所有提交的命令改為 f

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
f b729ad5 fixup
f e3851e8 another fix

如果要組合並重新命名這個提交,應該在第二個提交加上 r,或使用 s 取代 f

pick a9c8a1d Some refactoring
pick 01b2fd8 New awesome feature
s b729ad5 fixup
s e3851e8 another fix

你可以在接著彈出的文字提示中重新命名那個提交:

Newer, awesomer features

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto 8074d12
# You are currently editing a commit while rebasing branch 'main' on '8074d12'.
#
# Changes to be committed:
#	modified:   README.md
#

應該會看到如下的成功訊息:

(main)$ Successfully rebased and updated refs/heads/main.

安全合併的策略

--no-commit 選項會合併但不會自動提交,給使用者在提交前檢查和修改的機會。--no-ff 會留下功能分支存在過的證據,保持歷史記錄一致。

(main)$ git merge --no-ff --no-commit my-branch

我需要將一個分支合併成一個提交

(main)$ git merge --squash my-branch

我只想組合未推送的提交

假設在推送到上游前,你有幾個正在進行的工作提交,這時候不希望把已推送的提交也組合進來,因為其他人可能已經有提交引用它們了。

(main)$ git rebase -i @{u}

這會進行一次互動式重定基底,只會列出還沒推送的提交。對這些提交重新排序或做 squash、fixup 都是安全的。

檢查分支上的所有提交是否都合併了

要檢查一個分支上的所有提交是否都已經合併進了其它分支,應該在這些分支的 HEAD(或任何提交)之間檢查差異:

(main)$ git log --graph --left-right --cherry-pick --oneline HEAD...feature/120-on-scroll

這會顯示一個分支有而另一個分支沒有的提交,和分支之間不共享的提交的列表。

另一個方法是:

(main)$ git log main ^feature/120-on-scroll --no-merges

互動式重定基底可能出現的問題

編輯介面出現「noop」

如果你看到:

noop

表示重定基底的分支和目前分支在同一個提交,或領先目前分支。你可以嘗試:

  • 確保 main 分支沒有問題
  • HEAD~2 或更早的提交重定基底

有衝突的情況

如果不能成功的完成重定基底,你可能必須要解決衝突。

首先用 git status 檢查哪些檔案有衝突:

(my-branch)$ git status
On branch my-branch
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md

在這個例子中,README.md 有衝突。打開衝突的檔案會看到類似下面的內容:

   <<<<<<< HEAD
   some code
   =========
   some code
   >>>>>>> new-commit

你必須解決新提交的內容和 HEAD 中的內容的衝突。

有時候衝突非常複雜,你可以使用可視化差異編輯器:

(main*)$ git mergetool -t opendiff

解決所有衝突後,加入變更了的檔案,然後用 git rebase --continue 繼續重定基底:

(my-branch)$ git add README.md
(my-branch)$ git rebase --continue

如果在解決所有衝突過後,得到了與提交前一樣的結果,可以使用 git rebase --skip

如果想放棄重定基底,回到之前的狀態,可以在任何時候用:

(my-branch)$ git rebase --abort

貯存

貯存所有更動

貯存工作目錄下所有更動:

$ git stash

可以使用 -u 選項排除一些檔案:

$ git stash -u

貯存指定檔案

貯存一個檔案:

$ git stash push working-directory-path/filename.ext

貯存多個檔案:

$ git stash push working-directory-path/filename1.ext working-directory-path/filename2.ext

貯存時附加訊息

$ git stash save <message>

$ git stash push -m <message>

如此可以在使用 stash list 時看到訊息。

套用指定貯存

可以先列出擁有的貯存:

$ git stash list

然後,將以下命令的 n 替換成貯存在堆疊中的位置(最上方為 0),套用指定貯存:

$ git stash apply "stash@{n}"

除此之外,也可以使用時間標記(假如你能記住的話),如:

$ git stash apply "stash@{2.hours.ago}"

貯存時保留未暫存的內容

你需要先手動創建一個貯存提交,然後使用 git stash store

$ git stash create
$ git stash store -m "commit-message" CREATED_SHA1

雜項

複製所有子模組

$ git clone --recursive git://github.com/foo/bar.git

如果已經複製了:

$ git submodule update --init --recursive

刪除標籤

$ git tag -d <tag_name>
$ git push <remote> :refs/tags/<tag_name>

恢復已刪除標籤

如果想恢復一個已刪除標籤,首先,找到無法觸及的標籤:

$ git fsck --unreachable | grep tag

記下這個標籤的雜湊值,然後用 Git 的 update-ref

$ git update-ref refs/tags/<tag_name> <hash>

已刪除修補檔

如果有人在 GitHub 上向你提出了拉取請求,但他接著刪除了他的分叉,你無法複製他的提交或使用 git am。在這種情況下,最好手動的查看他們的提交,把它們拷貝到一個新的本機分支,然後提交。

最後,再修改作者,參見〈變更作者〉。然後,套用更動,再發起一個新的拉取請求。

追蹤檔案

我只想改變一個檔案名字的大小寫,而不修改內容

(main)$ git mv --force myfile MyFile

我想從 Git 刪除一個檔案,但保留該檔案

(main)$ git rm --cached log.txt

組態

我想為 Git 命令設定別名

在 OS X 和 Linux 下Git 的組態檔案儲存在 ~/.gitconfig。可以在 [alias] 部分設定一些快捷別名(以及容易拼錯的),如:

[alias]
    a = add
    amend = commit --amend
    c = commit
    ca = commit --amend
    ci = commit -a
    co = checkout
    d = diff
    dc = diff --changed
    ds = diff --staged
    f = fetch
    loll = log --graph --decorate --pretty=oneline --abbrev-commit
    m = merge
    one = log --pretty=oneline
    outstanding = rebase -i @{u}
    s = status
    unpushed = log @{u}
    wc = whatchanged
    wip = rebase -i @{u}
    zap = fetch -p

我想快取一個版本庫的使用者名稱和密碼

假設有一個版本庫需要授權,這時你可以快取使用者名稱和密碼,而不用每次推送和拉取時都輸入一次:

$ git config --global credential.helper cache
# Set Git to use the credential memory cache.
$ git config --global credential.helper 'cache --timeout=3600'
# Set the cache to timeout after 1 hour (setting is in seconds).

我不知道我做錯了什麼

如果你把事情搞砸了:你錯誤地重設、合併、或強制推送後,找不到自己的提交了;抑或你做得很好,但你想回到以前的某個狀態。

這時 git reflog 就派上用場了。reflog 記錄對分支頂端的任何改變,即使沒有任何分支或標籤參考那個頂端。基本上,只要 HEAD 改變,reflog 就會記錄下來。然而,這只能用於本機分支,且它只追蹤動作(例如,不會追蹤一個沒被記錄的檔案的任何改變)。

(main)$ git reflog
0a2e358 HEAD@{0}: reset: moving to HEAD~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to main
c10f740 HEAD@{2}: checkout: moving from main to 2.2

上面的 reflog 顯示了曾經從 main 分支簽出到 2.2 分支,然後再簽出回去,還有硬性重設到一個較舊的提交。最新的動作出現在最上面,並以 HEAD@{0} 標示。

如果你不小心移回了提交,reflog 會包含移回前 main 參考的提交(在這個例子中是 0254ea7)。只要硬性重設就能恢復到之前的狀態,這提供了歷史記錄不小心被變更時的安全網。

$ git reset --hard 0254ea7

摘自這裡

其他資源

書籍

  • Pro Git——Scott Chacon 的傑出書籍
  • Git Internals——Scott Chacon 的另一本傑出書籍

教學

腳本和工具

  • firstaidgit.io——一個可搜尋的 Git 常見問題集
  • git-extra-commands——一堆有用的額外 Git 腳本
  • git-extras——Git 工具集版本庫概要、repl、歷史記錄、提交百分比和更多
  • git-fire——git-fire 是一個 Git 插件,用於在緊急情況下幫助加入目前所有檔案、提交、推送到一個新分支(防止合併衝突)。
  • git-tips——Git 小撇步
  • git-town——通用、高級 Git 工作流程支援! http://www.git-town.com

GUI 客戶端

  • GitKraken——豪華的 Git 客戶端,適用於 Windows、Mac、Linux
  • git-cola——又一個 Git 客戶端,適用於 Windows、OS X
  • GitUp——一個新的 Git 客戶端,在解決 Git 的複雜問題上有自己的特點
  • gitx-dev——又一個圖形化的 Git 客戶端,適用於 OS X
  • Source Tree——簡單而強大的免費 Git GUI 客戶端,適用於 Windows、OS X
  • Tower——圖形化 Git 客戶端,適用於 OS X付費