Git
章節 ▾ 第二版

2.2 Git 基礎 - 記錄變更至儲存庫

記錄變更至儲存庫

此時,您應該在本機上擁有一個真正的 Git 儲存庫,並且已取出或建立其所有檔案的工作副本。通常,您會想要開始進行變更,並在每次專案達到您想要記錄的狀態時,將這些變更的快照提交到儲存庫中。

請記住,您工作目錄中的每個檔案都可能處於兩種狀態之一:已追蹤未追蹤。已追蹤的檔案是指上次快照中包含的檔案,以及任何新暫存的檔案;它們可以是未修改、已修改或已暫存的。簡而言之,已追蹤的檔案是 Git 知道的檔案。

未追蹤的檔案是指其他所有檔案,也就是您工作目錄中未包含在上次快照中且不在暫存區中的任何檔案。當您第一次複製儲存庫時,所有檔案都會被追蹤且未修改,因為 Git 剛剛取出它們,並且您尚未編輯任何內容。

當您編輯檔案時,Git 會將它們視為已修改,因為您自上次提交以來已變更它們。當您工作時,您會選擇性地暫存這些已修改的檔案,然後提交所有這些已暫存的變更,而這個週期會重複進行。

The lifecycle of the status of your files
圖 8. 您檔案狀態的生命週期

檢查檔案的狀態

您用來判斷哪些檔案處於哪種狀態的主要工具是 git status 命令。如果您在複製後直接執行此命令,您應該會看到類似如下的內容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

這表示您有一個乾淨的工作目錄;換句話說,您所有已追蹤的檔案都沒有被修改。Git 也沒有看到任何未追蹤的檔案,否則它們會列在這裡。最後,該命令會告訴您您目前處於哪個分支,並告知您它尚未與伺服器上的同一分支分歧。目前,該分支始終是 master,這是預設值;您無需在此擔心它。Git 分支將詳細介紹分支與參考。

注意

GitHub 在 2020 年中期將預設分支名稱從 master 變更為 main,其他 Git 主機也紛紛效仿。因此,您可能會發現某些新建立的儲存庫中的預設分支名稱為 main 而不是 master。此外,預設分支名稱可以變更(如您在您的預設分支名稱中所見),因此您可能會看到預設分支的不同名稱。

但是,Git 本身仍然使用 master 作為預設值,因此我們將在整本書中使用它。

假設您在您的專案中新增一個新的檔案,一個簡單的 README 檔案。如果該檔案之前不存在,並且您執行 git status,您會看到您的未追蹤檔案,如下所示

$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

nothing added to commit but untracked files present (use "git add" to track)

您可以看到您的新 README 檔案處於未追蹤狀態,因為它在您的狀態輸出中的「未追蹤的檔案」標題下。未追蹤基本上意味著 Git 看到一個您在上一個快照(提交)中沒有的檔案,並且尚未被暫存;Git 不會開始將其包含在您的提交快照中,除非您明確告知它這樣做。這樣做的目的是為了防止您不小心開始包含產生的二進位檔案或其他您不希望包含的檔案。您希望開始包含 README,所以讓我們開始追蹤該檔案。

追蹤新檔案

為了開始追蹤新檔案,您可以使用 git add 命令。要開始追蹤 README 檔案,您可以執行以下指令

$ git add README

如果您再次執行您的狀態命令,您可以看到您的 README 檔案現在已被追蹤並已暫存以準備提交

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    new file:   README

您可以判斷它已暫存,因為它在「要提交的變更」標題下。如果您在這一點提交,則在您執行 git add 時的檔案版本將會被包含在後續的歷史快照中。您可能還記得當您之前執行 git init 時,您接著執行了 git add <files> — 那是為了開始追蹤您目錄中的檔案。git add 命令接受檔案或目錄的路徑名稱;如果是目錄,則該命令會遞迴地新增該目錄中的所有檔案。

暫存已修改的檔案

讓我們變更一個已被追蹤的檔案。如果您變更一個先前追蹤的檔案,例如 CONTRIBUTING.md,然後再次執行您的 git status 命令,您會得到如下所示的內容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README

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:   CONTRIBUTING.md

CONTRIBUTING.md 檔案出現在名為「尚未暫存以提交的變更」的區塊下 — 這表示一個已追蹤的檔案在工作目錄中已被修改,但尚未暫存。要暫存它,您需要執行 git add 命令。git add 是一個多用途命令 — 您可以使用它來開始追蹤新檔案、暫存檔案,以及執行其他操作,例如將合併衝突的檔案標記為已解決。將其視為「將此內容精確地新增至下一個提交」,而不是「將此檔案新增至專案」,可能會更有幫助。現在讓我們執行 git add 來暫存 CONTRIBUTING.md 檔案,然後再次執行 git status

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

這兩個檔案都已暫存,將會被包含在您的下一個提交中。此時,假設您想起在提交 CONTRIBUTING.md 之前,還想做一個小小的修改。您再次開啟它並進行修改,然後準備提交。但是,讓我們再次執行 git status

$ vim CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

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:   CONTRIBUTING.md

搞什麼鬼?現在 CONTRIBUTING.md 同時被列為已暫存 *和* 未暫存。這怎麼可能?事實證明,Git 會在您執行 git add 命令時,完全按照當時的狀態暫存檔案。如果您現在提交,則在您上次執行 git add 命令時的 CONTRIBUTING.md 版本將會被包含在提交中,而不是當您執行 git commit 時在工作目錄中的檔案版本。如果您在執行 git add 後修改了檔案,則必須再次執行 git add 才能暫存檔案的最新版本

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

簡短狀態

雖然 git status 的輸出相當完整,但也相當冗長。Git 也有一個簡短狀態的標誌,讓您可以用更精簡的方式查看您的變更。如果您執行 git status -sgit 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 已修改、已暫存,然後再次修改,因此它既有已暫存的變更,也有未暫存的變更。

忽略檔案

通常,您會有一些類型的檔案不希望 Git 自動新增,甚至不希望將它們顯示為未追蹤。這些通常是自動產生的檔案,例如記錄檔或您的建置系統產生的檔案。在這種情況下,您可以建立一個名為 .gitignore 的檔案,其中列出要比對的模式。以下是一個 .gitignore 檔案的範例

$ cat .gitignore
*.[oa]
*~

第一行告訴 Git 忽略所有以「.o」或「.a」結尾的檔案 — 這些可能是建置程式碼產生的物件檔和封存檔。第二行告訴 Git 忽略所有名稱以波浪符號 (~) 結尾的檔案,許多文字編輯器 (例如 Emacs) 使用波浪符號來標記暫存檔。您也可以包含 log、tmp 或 pid 目錄;自動產生的文件等等。在開始之前為您的新儲存庫設定 .gitignore 檔案通常是一個好主意,這樣您就不會不小心提交您真的不希望在 Git 儲存庫中的檔案。

您可以在 .gitignore 檔案中使用的模式規則如下

  • 空白行或以 # 開頭的行會被忽略。

  • 標準的 glob 模式可以運作,並且會遞迴地套用至整個工作樹。

  • 您可以使用正斜線 (/) 開頭模式以避免遞迴。

  • 您可以使用正斜線 (/) 結尾模式以指定目錄。

  • 您可以使用驚嘆號 (!) 開頭模式來否定模式。

Glob 模式類似於 shell 使用的簡化正規表示式。星號 (*) 比對零個或多個字元;[abc] 比對方括號內的任何字元 (在此範例中為 a、b 或 c);問號 (?) 比對單一字元;方括號括住以連字號分隔的字元 ([0-9]) 比對它們之間的任何字元 (在此範例中為 0 到 9)。您也可以使用兩個星號來比對巢狀目錄;a/**/z 會比對 a/za/b/za/b/c/z 等等。

以下是另一個 .gitignore 檔案的範例

# ignore all .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in any directory named build
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory and any of its subdirectories
doc/**/*.pdf
提示

GitHub 在 https://github.com/github/gitignore 維護了一個相當完整的 .gitignore 檔案範例清單,適用於數十個專案和語言,如果您想為您的專案找到起點,可以在此參考。

注意

在簡單的情況下,儲存庫可能會在其根目錄中只有一個 .gitignore 檔案,該檔案會遞迴地套用至整個儲存庫。但是,也可以在子目錄中擁有額外的 .gitignore 檔案。這些巢狀 .gitignore 檔案中的規則僅適用於它們所在目錄下的檔案。Linux 核心原始碼儲存庫有 206 個 .gitignore 檔案。

本書的範圍不包含多個 .gitignore 檔案的詳細資訊;有關詳細資訊,請參閱 man gitignore

檢視已暫存和未暫存的變更

如果 git status 命令對您來說太模糊 — 您想確切知道您變更了什麼,而不僅僅是哪些檔案已變更 — 您可以使用 git diff 命令。我們稍後會更詳細地介紹 git diff,但您可能會最常使用它來回答以下兩個問題:您已變更但尚未暫存的內容是什麼?您已暫存但即將提交的內容是什麼?雖然 git status 會透過列出檔案名稱非常概略地回答這些問題,但 git diff 會顯示您新增和移除的確切行 — 也就是補丁。

假設您再次編輯並暫存 README 檔案,然後編輯 CONTRIBUTING.md 檔案而不暫存它。如果您執行 git status 命令,您會再次看到類似以下的內容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README

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:   CONTRIBUTING.md

若要查看您已變更但尚未暫存的內容,請輸入 git diff,不帶其他引數

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's

該命令會比較您的工作目錄中的內容和暫存區中的內容。結果會告訴您您已進行但尚未暫存的變更。

如果您想查看您已暫存且將被包含在您下一個提交中的內容,您可以使用 git diff --staged。此命令會將您已暫存的變更與您上次的提交進行比較

$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project

請務必注意,git diff 本身不會顯示自您上次提交以來的所有變更 — 只會顯示尚未暫存的變更。如果您已暫存所有變更,則 git diff 不會產生任何輸出。

另一個範例,如果您暫存 CONTRIBUTING.md 檔案然後編輯它,您可以使用 git diff 來查看檔案中已暫存的變更和未暫存的變更。如果我們的環境看起來像這樣

$ git add CONTRIBUTING.md
$ echo '# test line' >> CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   CONTRIBUTING.md

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:   CONTRIBUTING.md

現在您可以使用 git diff 來查看仍然未暫存的內容

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
 ## Starter Projects

 See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+# test line

以及使用 git diff --cached 來查看您目前已暫存的內容 (--staged--cached 是同義詞)

$ git diff --cached
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's
注意
在外部工具中使用 Git Diff

我們將在本書的其餘部分繼續以各種方式使用 git diff 命令。如果您喜歡使用圖形化或外部 diff 檢視程式,則可以使用另一種方式來查看這些差異。如果您執行 git difftool 而不是 git diff,您可以使用 emerge、vimdiff 等軟體 (包括商業產品) 來檢視任何這些差異。執行 git difftool --tool-help 以查看您的系統上可用的內容。

提交您的變更

現在您的暫存區已設定為您想要的方式,您可以提交您的變更。請記住,任何仍然未暫存的內容 — 您已建立或修改但自編輯後未執行 git add 的任何檔案 — 都不會被包含在本次提交中。它們會作為您磁碟上的已修改檔案保留。在這種情況下,假設您上次執行 git status 時,您看到所有內容都已暫存,因此您已準備好提交您的變更。提交的最簡單方式是輸入 git commit

$ git commit

這樣做會啟動您選擇的編輯器。

注意

這由您 shell 的 EDITOR 環境變數設定 — 通常是 vim 或 emacs,但您可以使用 git config --global core.editor 命令將其設定為您想要的任何編輯器,如您在開始中所見。

編輯器會顯示以下文字 (此範例為 Vim 畫面)

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

您可以看到預設提交訊息包含 git status 命令的最新輸出 (以註解形式) 和頂部的一行空白行。您可以移除這些註解並輸入您的提交訊息,也可以將它們保留在那裡,以幫助您記住您正在提交的內容。

注意

為了更明確地提醒您修改過的內容,您可以將 -v 選項傳遞給 git commit。這樣做也會將您的變更差異放入編輯器中,以便您確切地看到您正在提交的變更。

當您退出編輯器時,Git 會建立您的提交,並帶有該提交訊息 (其中已移除註解和差異)。

或者,您也可以在 commit 命令中,在 -m 標誌後面指定提交訊息,如下所示

$ git commit -m "Story 182: fix benchmarks for speed"
[master 463dc4f] Story 182: fix benchmarks for speed
 2 files changed, 2 insertions(+)
 create mode 100644 README

現在你已經建立了你的第一個提交(commit)!你可以看到這個提交提供了一些關於它自身的輸出資訊:你提交到哪個分支(master)、這個提交的 SHA-1 校验碼是什麼(463dc4f)、有多少檔案被更改,以及提交中新增和移除的程式碼行數統計。

請記住,提交會記錄你在暫存區中設定的快照。任何你沒有暫存的東西仍然會在那裡被修改;你可以執行另一個提交,將其加入你的歷史記錄。每次執行提交時,你都在記錄你的專案快照,你可以稍後還原或比較這些快照。

跳過暫存區

雖然暫存區在精確地建構你想要的提交時非常有用,但有時它在你的工作流程中會顯得有點過於複雜。如果你想要跳過暫存區,Git 提供了一個簡單的快捷方式。在 git commit 命令中加入 -a 選項,會讓 Git 在執行提交之前,自動暫存所有已經追蹤的檔案,讓你跳過 git add 的步驟。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
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:   CONTRIBUTING.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'Add new benchmarks'
[master 83e38c7] Add new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

請注意,在這種情況下,你不需要在提交之前對 CONTRIBUTING.md 檔案執行 git add。這是因為 -a 旗標會包含所有變更過的檔案。這很方便,但請小心;有時這個旗標會導致你包含不想要的變更。

移除檔案

要從 Git 中移除一個檔案,你必須將它從你追蹤的檔案中移除(更準確地說,是從你的暫存區中移除),然後提交。git rm 命令會執行此操作,也會將檔案從你的工作目錄中移除,這樣你下次就不會再看到它作為未追蹤的檔案。

如果你只是從工作目錄中移除檔案,它會顯示在 git status 輸出的「Changes not staged for commit」(即,未暫存)區域中

$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    PROJECTS.md

no changes added to commit (use "git add" and/or "git commit -a")

然後,如果你執行 git rm,它會暫存檔案的移除

$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md

下次你提交時,該檔案將會被移除且不再被追蹤。如果你修改了該檔案或已經將其加入暫存區,你必須使用 -f 選項強制移除。這是一個安全功能,可防止意外移除尚未記錄在快照中且無法從 Git 復原的資料。

你可能想做的另一個有用的事情是,保留工作樹中的檔案,但將其從你的暫存區中移除。換句話說,你可能希望將檔案保留在你的硬碟上,但不再讓 Git 追蹤它。如果你忘記將某些東西加入你的 .gitignore 檔案,而意外地將其暫存(例如,一個大型的日誌檔或一堆 .a 編譯檔案),這會特別有用。要做到這一點,請使用 --cached 選項

$ git rm --cached README

你可以將檔案、目錄和檔案模式傳遞給 git rm 命令。這表示你可以執行以下操作,例如

$ git rm log/\*.log

請注意 * 前面的反斜線 (\)。這是必要的,因為 Git 除了你的 shell 的檔案名稱擴展之外,還會進行自己的檔案名稱擴展。此命令會移除 log/ 目錄中所有副檔名為 .log 的檔案。或者,你可以執行類似的操作,如下所示

$ git rm \*~

此命令會移除所有名稱以 ~ 結尾的檔案。

移動檔案

與許多其他版本控制系統不同,Git 不會明確追蹤檔案移動。如果你在 Git 中重新命名檔案,Git 中不會儲存任何告訴你重新命名檔案的中繼資料。但是,Git 在事後判斷這一點非常聰明——我們稍後會處理偵測檔案移動的問題。

因此,Git 有一個 mv 命令有點令人困惑。如果你想在 Git 中重新命名檔案,你可以執行類似的操作

$ git mv file_from file_to

它會正常運作。事實上,如果你執行類似的操作並查看狀態,你會看到 Git 認為它是一個重新命名的檔案

$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

但是,這相當於執行類似的操作

$ mv README.md README
$ git rm README.md
$ git add README

Git 會隱含地判斷出它是重新命名,因此無論你以這種方式還是使用 mv 命令重新命名檔案都沒關係。唯一真正的區別是 git mv 是一個命令而不是三個——它是一個便利函數。更重要的是,你可以使用任何你喜歡的工具來重新命名檔案,然後在提交之前處理 add/rm

scroll-to-top