章節 ▾ 第二版

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 會在你執行 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 自動新增,甚至不想讓 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 檢視程式,還有另一種檢視這些 diff 的方法。如果你執行 git difftool 而不是 git diff,你可以在 emerge、vimdiff 等軟體(包括商業產品)中檢視任何 diff。執行 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 提供了一個簡單的快捷方式。將 -a 選項新增到 git commit 命令,可以讓 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(-)

注意在這種情況下,你不需要在提交之前執行 git add on the CONTRIBUTING.md 檔案。這是因為 -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

注意 * 前面的反斜槓 (\)。這是必需的,因為 Git 除了 shell 的檔名擴充套件之外,還會進行自己的檔名擴充套件。此命令會刪除 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