現(xiàn)在我們手上已經(jīng)有了一個(gè)真實(shí)項(xiàng)目的 Git 倉(cāng)庫(kù),并從這個(gè)倉(cāng)庫(kù)中取出了所有文件的工作拷貝。接下來,對(duì)這些文件作些修改,在完成了一個(gè)階段的目標(biāo)之后,提交本次更新到倉(cāng)庫(kù)。
請(qǐng)記住,工作目錄下面的所有文件都不外乎這兩種狀態(tài):已跟蹤或未跟蹤。已跟蹤的文件是指本來就被納入版本控制管理的文件,在上次快照中有它們的記錄,工作一段時(shí)間后,它們的狀態(tài)可能是未更新,已修改或者已放入暫存區(qū)。而所有其他文件都屬于未跟蹤文件。它們既沒有上次更新時(shí)的快照,也不在當(dāng)前的暫存區(qū)域。初次克隆某個(gè)倉(cāng)庫(kù)時(shí),工作目錄中的所有文件都屬于已跟蹤文件,且狀態(tài)為未修改。
在編輯過某些文件之后,Git 將這些文件標(biāo)為已修改。我們逐步把這些修改過的文件放到暫存區(qū)域,直到最后一次性提交所有這些暫存起來的文件,如此重復(fù)。所以使用 Git 時(shí)的文件狀態(tài)變化周期如圖 2-1 所示。
圖 2-1. 文件的狀態(tài)變化周期
要確定哪些文件當(dāng)前處于什么狀態(tài),可以用 git status
命令。如果在克隆倉(cāng)庫(kù)之后立即執(zhí)行此命令,會(huì)看到類似這樣的輸出:
$ git status
On branch master
nothing to commit, working directory clean
這說明你現(xiàn)在的工作目錄相當(dāng)干凈。換句話說,所有已跟蹤文件在上次提交后都未被更改過。此外,上面的信息還表明,當(dāng)前目錄下沒有出現(xiàn)任何處于未跟蹤的新文件,否則 Git 會(huì)在這里列出來。最后,該命令還顯示了當(dāng)前所在的分支是 master
,這是默認(rèn)的分支名稱,實(shí)際是可以修改的,現(xiàn)在先不用考慮。下一章我們就會(huì)詳細(xì)討論分支和引用。
現(xiàn)在讓我們用 vim 創(chuàng)建一個(gè)新文件 README,保存退出后運(yùn)行 git status
會(huì)看到該文件出現(xiàn)在未跟蹤文件列表中:
$ 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)
在狀態(tài)報(bào)告中可以看到新建的README
文件出現(xiàn)在“Untracked files”下面。未跟蹤的文件意味著Git在之前的快照(提交)中沒有這些文件;Git 不會(huì)自動(dòng)將之納入跟蹤范圍,除非你明明白白地告訴它“我需要跟蹤該文件”,因而不用擔(dān)心把臨時(shí)文件什么的也歸入版本管理。不過現(xiàn)在的例子中,我們確實(shí)想要跟蹤管理 README 這個(gè)文件。
使用命令 git add
開始跟蹤一個(gè)新文件。所以,要跟蹤 README 文件,運(yùn)行:
$ git add README
此時(shí)再運(yùn)行 git status
命令,會(huì)看到 README 文件已被跟蹤,并處于暫存狀態(tài):
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
只要在 “Changes to be committed” 這行下面的,就說明是已暫存狀態(tài)。如果此時(shí)提交,那么該文件此時(shí)此刻的版本將被留存在歷史記錄中。你可能會(huì)想起之前我們使用 git init
后就運(yùn)行了 git add
命令,開始跟蹤當(dāng)前目錄下的文件。在 git add
后面可以指明要跟蹤的文件或目錄路徑。如果是目錄的話,就說明要遞歸跟蹤該目錄下的所有文件。(譯注:其實(shí) git add
的潛臺(tái)詞就是把目標(biāo)文件快照放入暫存區(qū)域,也就是 add file into staged area,同時(shí)未曾跟蹤過的文件標(biāo)記為需要跟蹤。這樣就好理解后續(xù) add 操作的實(shí)際意義了。)
現(xiàn)在我們修改下之前已跟蹤過的文件 benchmarks.rb
,然后再次運(yùn)行 status
命令,會(huì)看到這樣的狀態(tài)報(bào)告:
$ 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
出現(xiàn)在 “Changes not staged for commit” 這行下面,說明已跟蹤文件的內(nèi)容發(fā)生了變化,但還沒有放到暫存區(qū)。要暫存這次更新,需要運(yùn)行 git add
命令(這是個(gè)多功能命令,根據(jù)目標(biāo)文件的狀態(tài)不同,此命令的效果也不同:可以用它開始跟蹤新文件,或者把已跟蹤的文件放到暫存區(qū),還能用于合并時(shí)把有沖突的文件標(biāo)記為已解決狀態(tài)等)?,F(xiàn)在讓我們運(yùn)行 git add
將 benchmarks.rb 放到暫存區(qū),然后再看看 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
現(xiàn)在兩個(gè)文件都已暫存,下次提交時(shí)就會(huì)一并記錄到倉(cāng)庫(kù)。假設(shè)此時(shí),你想要在 benchmarks.rb
里再加條注釋,重新編輯存盤后,準(zhǔn)備好提交。不過且慢,再運(yùn)行 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
文件出現(xiàn)了兩次!一次算未暫存,一次算已暫存,這怎么可能呢?好吧,實(shí)際上 Git 只不過暫存了你運(yùn)行 git add
命令時(shí)的版本,如果現(xiàn)在提交,那么提交的是添加注釋前的版本,而非當(dāng)前工作目錄中的版本。所以,運(yùn)行了 git add
之后又作了修訂的文件,需要重新運(yùn)行 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
一般我們總會(huì)有些文件無需納入 Git 的管理,也不希望它們總出現(xiàn)在未跟蹤文件列表。通常都是些自動(dòng)生成的文件,比如日志文件,或者編譯過程中創(chuàng)建的臨時(shí)文件等。我們可以創(chuàng)建一個(gè)名為 .gitignore
的文件,列出要忽略的文件模式。來看一個(gè)實(shí)際的例子:
$ cat .gitignore
*.[oa]
*~
第一行告訴 Git 忽略所有以 .o
或 .a
結(jié)尾的文件。一般這類對(duì)象文件和存檔文件都是編譯過程中出現(xiàn)的,我們用不著跟蹤它們的版本。第二行告訴 Git 忽略所有以波浪符(~
)結(jié)尾的文件,許多文本編輯軟件(比如 Emacs)都用這樣的文件名保存副本。此外,你可能還需要忽略 log
,tmp
或者 pid
目錄,以及自動(dòng)生成的文檔等等。要養(yǎng)成一開始就設(shè)置好 .gitignore
文件的習(xí)慣,以免將來誤提交這類無用的文件。
文件 .gitignore
的格式規(guī)范如下:
#
開頭的行都會(huì)被 Git 忽略。/
)說明要忽略的是目錄。!
)取反。所謂的 glob 模式是指 shell 所使用的簡(jiǎn)化了的正則表達(dá)式。星號(hào)(*
)匹配零個(gè)或多個(gè)任意字符;[abc]
匹配任何一個(gè)列在方括號(hào)中的字符(這個(gè)例子要么匹配一個(gè) a,要么匹配一個(gè) b,要么匹配一個(gè) c);問號(hào)(?
)只匹配一個(gè)任意字符;如果在方括號(hào)中使用短劃線分隔兩個(gè)字符,表示所有在這兩個(gè)字符范圍內(nèi)的都可以匹配(比如 [0-9]
表示匹配所有 0 到 9 的數(shù)字)。
我們?cè)倏匆粋€(gè) .gitignore
文件的例子:
# 此為注釋 – 將被 Git 忽略
# 忽略所有 .a 結(jié)尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 僅僅忽略項(xiàng)目根目錄下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目錄下的所有文件
build/
# 會(huì)忽略 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.
實(shí)際上 git status
的顯示比較簡(jiǎn)單,僅僅是列出了修改過的文件,如果要查看具體修改了什么地方,可以用 git diff
命令。稍后我們會(huì)詳細(xì)介紹 git diff
,不過現(xiàn)在,它已經(jīng)能回答我們的兩個(gè)問題了:當(dāng)前做的哪些更新還沒有暫存?有哪些更新已經(jīng)暫存起來準(zhǔn)備好了下次提交? git diff
會(huì)使用文件補(bǔ)丁的格式顯示具體添加和刪除的行。
假如再次修改 README
文件后暫存,然后編輯 benchmarks.rb
文件后先別暫存,運(yùn)行 status
命令將會(huì)看到:
$ 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
要查看尚未暫存的文件更新了哪些部分,不加參數(shù)直接輸入 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
此命令比較的是工作目錄中當(dāng)前文件和暫存區(qū)域快照之間的差異,也就是修改之后還沒有暫存起來的變化內(nèi)容。
若要看已經(jīng)暫存起來的文件和上次提交時(shí)的快照之間的差異,可以用 git diff --cached
命令。(Git 1.6.1 及更高版本還允許使用 git diff --staged
,效果是相同的,但更好記些。)來看看實(shí)際的效果:
$ 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
請(qǐng)注意,單單 git diff
不過是顯示還沒有暫存起來的改動(dòng),而不是這次工作和上次提交之間的差異。所以有時(shí)候你一下子暫存了所有更新過的文件后,運(yùn)行 git diff
后卻什么也沒有,就是這個(gè)原因。
像之前說的,暫存 benchmarks.rb 后再編輯,運(yùn)行 git status
會(huì)看到暫存前后的兩個(gè)版本:
$ 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
現(xiàn)在運(yùn)行 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
查看已經(jīng)暫存起來的變化:
$ 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
現(xiàn)在的暫存區(qū)域已經(jīng)準(zhǔn)備妥當(dāng)可以提交了。在此之前,請(qǐng)一定要確認(rèn)還有什么修改過的或新建的文件還沒有 git add
過,否則提交的時(shí)候不會(huì)記錄這些還沒暫存起來的變化。所以,每次準(zhǔn)備提交前,先用 git status
看下,是不是都已暫存起來了,然后再運(yùn)行提交命令 git commit
:
$ git commit
這種方式會(huì)啟動(dòng)文本編輯器以便輸入本次提交的說明。(默認(rèn)會(huì)啟用 shell 的環(huán)境變量 $EDITOR
所指定的軟件,一般都是 vim 或 emacs。當(dāng)然也可以按照第一章介紹的方式,使用 git config --global core.editor
命令設(shè)定你喜歡的編輯軟件。)
編輯器會(huì)顯示類似下面的文本信息(本例選用 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
可以看到,默認(rèn)的提交消息包含最后一次運(yùn)行 git status
的輸出,放在注釋行里,另外開頭還有一空行,供你輸入提交說明。你完全可以去掉這些注釋行,不過留著也沒關(guān)系,多少能幫你回想起這次更新的內(nèi)容有哪些。(如果覺得這還不夠,可以用 -v
選項(xiàng)將修改差異的每一行都包含到注釋中來。)退出編輯器時(shí),Git 會(huì)丟掉注釋行,將說明內(nèi)容和本次更新提交到倉(cāng)庫(kù)。
另外也可以用 -m 參數(shù)后跟提交說明的方式,在一行命令中提交更新:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Fix benchmarks for speed
2 files changed, 3 insertions(+)
create mode 100644 README
好,現(xiàn)在你已經(jīng)創(chuàng)建了第一個(gè)提交!可以看到,提交后它會(huì)告訴你,當(dāng)前是在哪個(gè)分支(master)提交的,本次提交的完整 SHA-1 校驗(yàn)和是什么(463dc4f
),以及在本次提交中,有多少文件修訂過,多少行添改和刪改過。
記住,提交時(shí)記錄的是放在暫存區(qū)域的快照,任何還未暫存的仍然保持已修改狀態(tài),可以在下次提交時(shí)納入版本管理。每一次運(yùn)行提交操作,都是對(duì)你項(xiàng)目作一次快照,以后可以回到這個(gè)狀態(tài),或者進(jìn)行比較。
盡管使用暫存區(qū)域的方式可以精心準(zhǔn)備要提交的細(xì)節(jié),但有時(shí)候這么做略顯繁瑣。Git 提供了一個(gè)跳過使用暫存區(qū)域的方式,只要在提交的時(shí)候,給 git commit
加上 -a
選項(xiàng),Git 就會(huì)自動(dòng)把所有已經(jīng)跟蹤過的文件暫存起來一并提交,從而跳過 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 add
文件 benchmarks.rb 了。
要從 Git 中移除某個(gè)文件,就必須要從已跟蹤文件清單中移除(確切地說,是從暫存區(qū)域移除),然后提交??梢杂?nbsp;git rm
命令完成此項(xiàng)工作,并連帶從工作目錄中刪除指定的文件,這樣以后就不會(huì)出現(xiàn)在未跟蹤文件清單中了。
如果只是簡(jiǎn)單地從工作目錄中手工刪除文件,運(yùn)行 git status
時(shí)就會(huì)在 “Changes not staged for commit” 部分(也就是未暫存清單)看到:
$ 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")
然后再運(yùn)行 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
最后提交的時(shí)候,該文件就不再納入版本管理了。如果刪除之前修改過并且已經(jīng)放到暫存區(qū)域的話,則必須要用強(qiáng)制刪除選項(xiàng) -f
(譯注:即 force 的首字母),以防誤刪除文件后丟失修改的內(nèi)容。
另外一種情況是,我們想把文件從 Git 倉(cāng)庫(kù)中刪除(亦即從暫存區(qū)域移除),但仍然希望保留在當(dāng)前工作目錄中。換句話說,僅是從跟蹤清單中刪除。比如一些大型日志文件或者一堆 .a
編譯文件,不小心納入倉(cāng)庫(kù)后,要移除跟蹤但不刪除文件,以便稍后在 .gitignore
文件中補(bǔ)上,用 --cached
選項(xiàng)即可:
$ git rm --cached readme.txt
后面可以列出文件或者目錄的名字,也可以使用 glob 模式。比方說:
$ git rm log/\*.log
注意到星號(hào) *
之前的反斜杠 \
,因?yàn)?Git 有它自己的文件模式擴(kuò)展匹配方式,所以我們不用 shell 來幫忙展開(譯注:實(shí)際上不加反斜杠也可以運(yùn)行,只不過按照 shell 擴(kuò)展的話,僅僅刪除指定目錄下的文件而不會(huì)遞歸匹配。上面的例子本來就指定了目錄,所以效果等同,但下面的例子就會(huì)用遞歸方式匹配,所以必須加反斜杠。)。此命令刪除所有 log/
目錄下擴(kuò)展名為 .log
的文件。類似的比如:
$ git rm \*~
會(huì)遞歸刪除當(dāng)前目錄及其子目錄中所有 ~
結(jié)尾的文件。
不像其他的 VCS 系統(tǒng),Git 并不跟蹤文件移動(dòng)操作。如果在 Git 中重命名了某個(gè)文件,倉(cāng)庫(kù)中存儲(chǔ)的元數(shù)據(jù)并不會(huì)體現(xiàn)出這是一次改名操作。不過 Git 非常聰明,它會(huì)推斷出究竟發(fā)生了什么,至于具體是如何做到的,我們稍后再談。
既然如此,當(dāng)你看到 Git 的 mv
命令時(shí)一定會(huì)困惑不已。要在 Git 中對(duì)文件改名,可以這么做:
$ git mv file_from file_to
它會(huì)恰如預(yù)期般正常工作。實(shí)際上,即便此時(shí)查看狀態(tài)信息,也會(huì)明白無誤地看到關(guān)于重命名操作的說明:
$ 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
其實(shí),運(yùn)行 git mv
就相當(dāng)于運(yùn)行了下面三條命令:
$ mv README.txt README
$ git rm README.txt
$ git add README
如此分開操作,Git 也會(huì)意識(shí)到這是一次改名,所以不管何種方式都一樣。當(dāng)然,直接用 git mv
輕便得多,不過有時(shí)候用其他工具批處理改名的話,要記得在提交前刪除老的文件名,再添加新的文件名。
更多建議: