Git --fast-version-control

2.2 Git 基礎 - 提交更新到儲存庫

提交更新到儲存庫

讀者現在有一個貨真價實的Git儲存庫,而且有一份已放到工作複本的該專案的檔案。 讀者需要做一些修改並提交這些更動的快照到儲存庫,當這些修改到達讀者想要記錄狀態的情況。

記住工作目錄內的每個檔案可能為兩種狀態的任一種:追蹤或者尚未被追蹤。 被追蹤的檔案是最近的快照;它們可被復原、修改,或者暫存。 未被追蹤的檔案則是其它未在最近快照也未被暫存的任何檔案。 當讀者第一次複製儲存器時,讀者所有檔案都是被追蹤且未被修改的。 因為讀者剛取出它們而且尚未更改做任何修改。

只要讀者編輯任何已被追蹤的檔案。 Git將它們視為被更動的,因為讀者將它們改成與最後一次提交不同。 讀者暫存這些已更動檔案並提供所有被暫存的更新, 並重複此週期。 此生命週期如圖2-1所示。


圖2-1. 檔案狀態的生命週期。

檢視檔案的狀態

主要給讀者用來檢視檔案的狀態是 git status 命令。 若讀者在複製完複本後馬上執行此命令,會看到如下的文字:

$ git status
On branch master
nothing to commit, working directory clean

Wokring directory clean意謂著目前的工作目錄沒有未被追蹤或已被修改的檔案。Git未看到任何未被追蹤的檔案,否則會將它們列出。 最後,這個命令告訴讀者目前在哪一個分支(branch)。到目前為止,一直都是master,這是預設的。下一個章節會詳細介紹分支(branch),目前我們先不考慮它。

假設讀者新增一些檔案到專案,如README。 若該檔案先前並不存在,執行 git status 命令後,讀者會看到未被追蹤的檔案,如下:

$ vim README
$ git status
On branch 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不會主動將它加入。這樣可以避免加入一些二進位格式的檔案或其它使用者不想列入追蹤的檔案。 不過在這個例子中,我們的確是要將 README 檔案加入追蹤:

追蹤新檔案

要追蹤新增的檔案,我們可以使用git add命令。例如:要追蹤README檔案,可執行:

$ git add README

如此一來,我們重新檢查狀態(status)時,可看到README檔案已被列入追蹤並且已被暫存:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README

因為它被放在Changes to be commited文字下方,讀者可得知它已被暫存起來。 若讀者此時提交更新,剛才執行git add加進來的檔案就會被記錄在歷史的快照。 讀者可能可回想一下先前執行git init後也有執行過git add,開始追蹤目錄內的檔案。git add命令可接受檔名或者目錄名。 若是目錄名,Git會以遞迴(recursive)的方式會將整個目錄下所有檔案及子目錄都加進來。

暫存已修改檔案

讓我們修改已被追蹤的檔案。 若讀者修改先前已被追蹤的檔案,名為benchmarks.rb,並檢查目前儲存庫的狀態。我們會看到類似以下文字:

$ git status
On branch 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:   benchmarks.rb

benchmarks.rb檔案出現在 “Changes not staged for commit” 下方,代表著這個檔案已被追蹤,而且位於工作目錄的該檔案已被修改,但尚未暫存。 要暫存該檔案,可執行git add命令(這是一個多重用途的指令)。現在,讀者使用 git addbenchmarks.rb檔案暫存起來,並再度執行git status

$ git add benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

這兩個檔案目前都被暫存起來,而且會進入下一次的提交。 假設讀者記得仍需要對benchmarks.rb做一點修改後才要提交,可再度開啟並編輯該檔案。 然而,當我們再度執行git status

$ vim benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

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:   benchmarks.rb

到底發生了什麼事? 現在benchmarks.rb同時被列在已被暫存及未被暫存。 這怎麼可能? 這表示Git的確在讀者執行git add命令後,將檔案暫存起來。 若讀者現在提交更新,最近一次執行git add命令時暫存的benchmarks.rb會被提交。 若讀者在git add後修改檔案,需要再度執行git add將最新版的檔案暫存起來:

$ git add benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README
        modified:   benchmarks.rb

忽略某些檔案

通常讀者會有一類不想讓Git自動新增,也不希望它們被列入未被追蹤的檔案。 這些通常是自動產生的檔案,例如:記錄檔或者編譯系統產生的檔案。 在這情況下,讀者可建立一個名為.gitignore檔案,列出符合這些檔案檔名的特徵。 以下是一個範例:

$ cat .gitignore
*.[oa]
*~

第一列告訴Git忽略任何檔名為.o.a結尾的檔案,它們是可能是編譯系統建置讀者的程式碼時產生的目的檔及程式庫。 第二列告訴Git忽略所有檔名為~結尾的檔案,通常被很多文書編輯器,如:Emacs,使用的暫存檔案。 讀者可能會想一併將log、tmp、pid目錄及自動產生的文件等也一併加進來。 依據類推。在讀者要開始開發之前將.gitignore設定好,通常是一個不錯的點子。這樣子讀者不會意外地將真的不想追蹤的檔案提交到Git儲存庫。

編寫.gitignore檔案的規則如下:

  • 空白列或者以#開頭的列會被忽略。
  • 可使用標準的Glob pattern。
  • 可以/結尾,代表是目錄。
  • 可使用!符號將特徵反過來使用。

Glob pattern就像是shell使用的簡化版正規運算式。 星號(*)匹配零個或多個字元;[abc]匹配中括弧內的任一字元(此例為abc);問號(?)匹配單一個字元;中括孤內的字以連字符連接(如:[0-9]),用來匹配任何符合該範圍的字(此例為0到9)。

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

# 註解,會被忽略。
# 不要追蹤檔名為 .a 結尾的檔案
*.a
# 但是要追蹤 lib.a,即使上方已指定忽略所有的 .a 檔案
!lib.a
# 只忽略根目錄下的 TODO 檔案。 不包含子目錄下的 TODO
/TODO
# 忽略build/目錄下所有檔案
build/
# 忽略doc/notes.txt但不包含doc/server/arch.txt
doc/*.txt
# ignore all .txt files in the doc/ directory
doc/**/*.txt

A **/ pattern is available in Git since version 1.8.2.

檢視已暫存及尚未暫存的更動

在某些情況下,git status指令提供的資訊就太過簡要。 有的時候我們不只想知道那些檔案被更動,而是想更進一步知道被檔案的內容被做了那些修改,這時我們可以使用git diff命令。稍後我們會有更詳盡講解該命令。讀者使用它時通常會是為了瞭解兩個問題:目前已做的修改但尚未暫存的內容是哪些?以及將被提交的暫存資料有哪些?儘管git status指令可以大略回答這些問題,但git diff可顯示檔案裡的哪些列被加入或刪除,以修補檔(patch)方式表達。

假設讀者編輯並暫存(stage)README,接著修改benchmarks.rb檔案,卻未暫存。若讀者檢視目前的狀況,會看到類似下方文字:

$ git status
On branch 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:   benchmarks.rb

想瞭解尚未暫存的修改,執行git diff,不用帶任何參數:

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
           @commit.parents[0].parents[0].parents[0]
         end

+        run_code(x, 'commits 1') do
+          git.commits.size
+        end
+
         run_code(x, 'commits 2') do
           log = git.commits('master', 15)
           log.size

這命令會比對目前工作目錄(working directory)及暫存區域(stage area)的版本,然後顯示尚未被存入暫存區(stage area)的變更。

若讀者想比對暫存區域(stage)及最後一次提交(commit)的差異,可用git diff --cached指令(Git 1.6.1之後的版本,可用較易記的git diff --staged 指令):

$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README2
@@ -0,0 +1,5 @@
+grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+Grit is a Ruby library for extracting information from a Git repository

很重要的一點是git diff不會顯示最後一次commit後的所有變更;只會顯示尚未存入暫存區(即unstaged)的變更。這麼說可能會混淆,舉個例子來說,若讀者已暫存(stage)所有的變更,輸入git diff不會顯示任何資訊。

舉其它例子,若讀者暫存benchmarks.rb檔案後又編輯,可使用git diff看已暫存的版本與工作目錄內版本尚未暫存的變更:

$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   benchmarks.rb

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:   benchmarks.rb

現在讀者可使用git diff檢視哪些部份尚未被暫存:

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
 main()

 ##pp Grit::GitRuby.cache_client.stats
+# test line

以及使用git diff --cached檢視目前已暫存的變更:

$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
          @commit.parents[0].parents[0].parents[0]
        end

+        run_code(x, 'commits 1') do
+          git.commits.size
+        end
+
        run_code(x, 'commits 2') do
          log = git.commits('master', 15)
          log.size

提交修改

現在讀者的暫存區域已被更新為讀者想要的,可開始提交變更的部份。 要記得任何尚未被暫存的新建檔案或已被修改但尚未使用git add暫存的檔案將不會被記錄在本次的提交中。 它們仍會以被修改的檔案的身份存在磁碟中。 在這情況下,最後一次執行git status,讀者會看到所有已被暫存的檔案,讀者也準備好要提交修改。 最簡單的提交是執行git commit

$ git commit

執行此命令會叫出讀者指定的編輯器。(由讀者shell的$EDITOR環境變數指定,通常是vim或emacs。讀者也可以如同第1章介紹的,使用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
# Changes to be committed:
#       new file:   README
#       modified:   benchmarks.rb
#
~
~
~
".git/COMMIT_EDITMSG" 10L, 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, 3 insertions(+)
 create mode 100644 README

現在讀者已建立第一個提交! 讀者可從輸出的訊息看到此提交、放到哪個分支(master)、SHA-1查核碼(463dc4f)、有多少檔案被更動,以及統計此提交有多少列被新增及移除。

記得提交記錄讀者放在暫存區的快照。 任何讀者未暫存的仍然保持在已被修改狀態;讀者可進行其它的提交,將它增加到歷史。 每一次讀者執行提供,都是記錄專案的快照,而且以後可用來比對或者復原。

跳過暫存區域

雖然優秀好用的暫存區域能很有技巧且精確的提交讀者想記錄的資訊,有時候暫存區域也比讀者實際需要的工作流程繁瑣。 若讀者想跳過暫存區域,Git提供了簡易的使用方式。 在git commit命令後方加上-a參數,Git自動將所有已被追蹤且被修改的檔案送到暫存區域並開始提交程序,讓讀者略過git add的步驟:

$ git status
On branch 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:   benchmarks.rb

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

留意本次的提交之前,讀者並不需要執行git addbenchmarks.rb檔案加入。

刪除檔案

要從Git刪除檔案,讀者需要將它從已被追蹤檔案中移除(更精確的來說,是從暫存區域移除),並且提交。 git rm命令除了完成此工作外,也會將該檔案從工作目錄移除。 因此讀者以後不會在未被追蹤檔案列表看到它。

若讀者僅僅是將檔案從工作目錄移除,那麼在git status的輸出,可看見該檔案將會被視為“已被變更且尚未被更新”(也就是尚未存到暫存區域):

$ rm grit.gemspec
$ git status
On branch 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:    grit.gemspec

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

接著,若執行git rm,則會將暫存區域內的該檔案移除:

$ git rm grit.gemspec
rm 'grit.gemspec'
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        deleted:    grit.gemspec

下一次提交時,該檔案將會消失而且不再被追蹤。 若已更動過該檔案且將它記錄到暫存區域。 必須使用-f參數才能將它強制移除。 這是為了避免已被記錄的快照意外被移除且再也無法使用Git復原。

其它有用的技巧是保留工作目錄內的檔案,但從暫存區域移除。 換句話說,或許讀者想在磁碟機上的檔案且不希望Git繼續追蹤它。 這在讀者忘記將某些檔案記錄到.gitignore且不小心將它增加到暫存區域時特別有用。 比如說:巨大的記錄檔、或大量在編譯時期產生的.a檔案。 欲使用此功能,加上--cached參數:

$ git rm --cached readme.txt

除了檔名、目錄名以外,還可以指定簡化的正規運算式給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.txt README
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        renamed:    README.txt -> README

不過,這就相當於執行下列命令:

$ mv README.txt README
$ git rm README.txt
$ git add README

Git會在背後判斷檔案是否被更名,因此不管是用上述方法還是'mv'命令都沒有差別。 實際上唯一不同的是'mv'是一個命令,而不是三個。 使用上較方便。 更重要的是讀者可使用任何慣用的工具更名,再使用add/rm,接著才提交。