章節 ▾ 第二版

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 檔案是未跟蹤的,因為它位於你的狀態輸出中的“Untracked files”(未跟蹤檔案)標題下。未跟蹤基本上意味著 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

你可以透過它在“Changes to be committed”(要提交的更改)標題下來判斷它是否已暫存。如果你此時提交,那麼在你執行 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 檔案出現在名為“Changes not staged for commit”(未暫存以待提交的更改)的部分下——這意味著一個已跟蹤檔案在工作目錄中已被修改但尚未暫存。要暫存它,請執行 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 add 命令時,Git 會精確地暫存檔案當時的版本。如果你現在提交,CONTRIBUTING.md 檔案在你上次執行 git add 命令時的版本將被提交,而不是你在執行 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。這樣做還會將你的更改的 diff 放入編輯器中,這樣你就可以清楚地看到你正在提交哪些更改。

當你退出編輯器時,Git 會使用該提交資訊(並去除註釋和 diff 內容)建立你的提交。

或者,你也可以透過在 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

現在你已經建立了你的第一次提交!你可以看到提交給出了一些關於它自身的資訊:你提交到的分支 (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

你可以將檔案、目錄和檔案 glob 模式傳遞給 git rm 命令。這意味著你可以執行以下操作

$ git rm log/\*.log

請注意 * 前面的反斜槓 (\)。這是必要的,因為除了 shell 的檔名擴充套件外,Git 還會進行自己的檔名擴充套件。此命令會刪除 log/ 目錄中所有具有 .log 副檔名的檔案。或者,你可以執行類似這樣的操作

$ git rm \*~

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

移動檔案

與許多其他版本控制系統(VCS)不同,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