天天看點

2.2 Git 基礎 - 記錄每次更新到倉庫

記錄每次更新到倉庫

現在我們手上有了一個真實項目的 Git 倉庫,并從這個倉庫中取出了所有檔案的工作拷貝。 接下來,對這些檔案做些修改,在完成了一個階段的目标之後,送出本次更新到倉庫。

請記住,你工作目錄下的每一個檔案都不外乎這兩種狀态:已跟蹤或未跟蹤。 已跟蹤的檔案是指那些被納入了版本控制的檔案,在上一次快照中有它們的記錄,在工作一段時間後,它們的狀态可能處于未修改,已修改或已放入暫存區。 工作目錄中除已跟蹤檔案以外的所有其它檔案都屬于未跟蹤檔案,它們既不存在于上次快照的記錄中,也沒有放入暫存區。 初次克隆某個倉庫的時候,工作目錄中的所有檔案都屬于已跟蹤檔案,并處于未修改狀态。

編輯過某些檔案之後,由于自上次送出後你對它們做了修改,Git 将它們标記為已修改檔案。 我們逐漸将這些修改過的檔案放入暫存區,然後送出所有暫存了的修改,如此反複。是以使用 Git 時檔案的生命周期如下:

2.2 Git 基礎 - 記錄每次更新到倉庫

Figure 2-1. 檔案的狀态變化周期

檢查目前檔案狀态

要檢視哪些檔案處于什麼狀态,可以用

git status

指令。 如果在克隆倉庫後立即使用此指令,會看到類似這樣的輸出:

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

複制

這說明你現在的工作目錄相當幹淨。換句話說,所有已跟蹤檔案在上次送出後都未被更改過。 此外,上面的資訊還表明,目前目錄下沒有出現任何處于未跟蹤狀态的新檔案,否則 Git 會在這裡列出來。 最後,該指令還顯示了目前所在分支,并告訴你這個分支同遠端伺服器上對應的分支沒有偏離。 現在,分支名是 “master”,這是預設的分支名。 我們在 Git 分支 會詳細讨論分支和引用。

現在,讓我們在項目下建立一個新的 README 檔案。 如果之前并不存在這個檔案,使用

git status

指令,你将看到一個新的未跟蹤檔案:

$ echo 'My Project' > 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 在之前的快照(送出)中沒有這些檔案;Git 不會自動将之納入跟蹤範圍,除非你明明白白地告訴它“我需要跟蹤該檔案”, 這樣的處理讓你不必擔心将生成的二進制檔案或其它不想被跟蹤的檔案包含進來。 不過現在的例子中,我們确實想要跟蹤管理 README 這個檔案。

跟蹤新檔案

使用指令

git add

開始跟蹤一個檔案。 是以,要跟蹤 README 檔案,運作:

$ git add README           

複制

此時再運作

git status

指令,會看到 README 檔案已被跟蹤,并處于暫存狀态:

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

    new file:   README           

複制

隻要在

Changes to be committed

這行下面的,就說明是已暫存狀态。 如果此時送出,那麼該檔案此時此刻的版本将被留存在曆史記錄中。 你可能會想起之前我們使用

git init

後就運作了

git add (files)

指令,開始跟蹤目前目錄下的檔案。

git add

指令使用檔案或目錄的路徑作為參數;如果參數是目錄的路徑,該指令将遞歸地跟蹤該目錄下的所有檔案。

暫存已修改檔案

現在我們來修改一個已被跟蹤的檔案。 如果你修改了一個名為

CONTRIBUTING.md

的已被跟蹤的檔案,然後運作

git status

指令,會看到下面内容:

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

複制

檔案

CONTRIBUTING.md

出現在

Changes not staged for commit

這行下面,說明已跟蹤檔案的内容發生了變化,但還沒有放到暫存區。 要暫存這次更新,需要運作

git add

指令。 這是個多功能指令:可以用它開始跟蹤新檔案,或者把已跟蹤的檔案放到暫存區,還能用于合并時把有沖突的檔案标記為已解決狀态等。 将這個指令了解為“添加内容到下一次送出中”而不是“将一個檔案添加到項目中”要更加合适。 現在讓我們運作

git add

将"CONTRIBUTING.md"放到暫存區,然後再看看

git status

的輸出:

$ git add CONTRIBUTING.md
$ git status
On branch 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
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

指令時的版本, 如果你現在送出,

CONTRIBUTING.md

的版本是你最後一次運作

git add

指令時的那個版本,而不是你運作

git commit

時,在工作目錄中的目前版本。 是以,運作了

git add

之後又作了修訂的檔案,需要重新運作

git add

把最新版本重新暫存起來:

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

    new file:   README
    modified:   CONTRIBUTING.md           

複制

狀态簡覽

git status

指令的輸出十分詳細,但其用語有些繁瑣。 如果你使用

git status -s

指令或

git status --short

指令,你将得到一種更為緊湊的格式輸出。 運作

git status -s

,狀态報告輸出如下:

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt           

複制

新添加的未跟蹤檔案前面有

??

标記,新添加到暫存區中的檔案前面有

A

标記,修改過的檔案前面有

M

标記。 你可能注意到了

M

有兩個可以出現的位置,出現在右邊的

M

表示該檔案被修改了但是還沒放入暫存區,出現在靠左邊的

M

表示該檔案被修改了并放入了暫存區。 例如,上面的狀态報告顯示:

README

檔案在工作區被修改了但是還沒有将修改後的檔案放入暫存區,

lib/simplegit.rb

檔案被修改了并将修改後的檔案放入了暫存區。 而

Rakefile

在工作區被修改并送出到暫存區後又在工作區中被修改了,是以在暫存區和工作區都有該檔案被修改了的記錄。

忽略檔案

一般我們總會有些檔案無需納入 Git 的管理,也不希望它們總出現在未跟蹤檔案清單。 通常都是些自動生成的檔案,比如日志檔案,或者編譯過程中建立的臨時檔案等。 在這種情況下,我們可以建立一個名為

.gitignore

的檔案,列出要忽略的檔案模式。 來看一個實際的例子:

$ cat .gitignore
*.[oa]
*~           

複制

第一行告訴 Git 忽略所有以

.o

.a

結尾的檔案。一般這類對象檔案和存檔檔案都是編譯過程中出現的。 第二行告訴 Git 忽略所有以波浪符(~)結尾的檔案,許多文本編輯軟體(比如 Emacs)都用這樣的檔案名儲存副本。 此外,你可能還需要忽略 log,tmp 或者 pid 目錄,以及自動生成的文檔等等。 要養成一開始就設定好 .gitignore 檔案的習慣,以免将來誤送出這類無用的檔案。

檔案

.gitignore

的格式規範如下:

  • 所有空行或者以

    開頭的行都會被 Git 忽略。
  • 可以使用标準的 glob 模式比對。
  • 比對模式可以以(

    /

    )開頭防止遞歸。
  • 比對模式可以以(

    /

    )結尾指定目錄。
  • 要忽略指定模式以外的檔案或目錄,可以在模式前加上驚歎号(

    !

    )取反。

所謂的 glob 模式是指 shell 所使用的簡化了的正規表達式。 星号(

*

)比對零個或多個任意字元;

[abc]

比對任何一個列在方括号中的字元(這個例子要麼比對一個 a,要麼比對一個 b,要麼比對一個 c);問号(

?

)隻比對一個任意字元;如果在方括号中使用短劃線分隔兩個字元,表示所有在這兩個字元範圍内的都可以比對(比如

[0-9]

表示比對所有 0 到 9 的數字)。 使用兩個星号(

*

) 表示比對任意中間目錄,比如

a/**/z

可以比對

a/z

,

a/b/z

a/b/c/z

等。

我們再看一個 .gitignore 檔案的例子:

# no .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 the build/ directory
build/

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

# ignore all .pdf files in the doc/ directory
doc/**/*.pdf           

複制

TIP

GitHub 有一個十分詳細的針對數十種項目及語言的

.gitignore

檔案清單,你可以在https://github.com/github/gitignore 找到它.

檢視已暫存和未暫存的修改

如果

git status

指令的輸出對于你來說過于模糊,你想知道具體修改了什麼地方,可以用

git diff

指令。 稍後我們會詳細介紹

git diff

,你可能通常會用它來回答這兩個問題:目前做的哪些更新還沒有暫存? 有哪些更新已經暫存起來準備好了下次送出? 盡管

git status

已經通過在相應欄下列出檔案名的方式回答了這個問題,

git diff

将通過檔案更新檔的格式顯示具體哪些行發生了改變。

假如再次修改 README 檔案後暫存,然後編輯

CONTRIBUTING.md

檔案後先不暫存, 運作

status

指令将會看到:

$ git status
On branch 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 --cached

指令。(Git 1.6.1 及更高版本還允許使用

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 status

會看到暫存前後的兩個版本。 如果我們的環境(終端輸出)看起來如下:

$ git add CONTRIBUTING.md
$ echo '# test line' >> CONTRIBUTING.md
$ git status
On branch 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           

複制

NOTE

Git Diff 的插件版本

在本書中,我們使用

git diff

來分析檔案差異。 但是,如果你喜歡通過圖形化的方式或其它格式輸出方式的話,可以使用

git difftool

指令來用 Araxis ,emerge 或 vimdiff 等軟體輸出 diff 分析結果。 使用

git difftool --tool-help

指令來看你的系統支援哪些 Git Diff 插件。

送出更新

現在的暫存區域已經準備妥當可以送出了。 在此之前,請一定要确認還有什麼修改過的或建立的檔案還沒有

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
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C           

複制

可以看到,預設的送出消息包含最後一次運作

git status

的輸出,放在注釋行裡,另外開頭還有一空行,供你輸入送出說明。 你完全可以去掉這些注釋行,不過留着也沒關系,多少能幫你回想起這次更新的内容有哪些。 (如果想要更詳細的對修改了哪些内容的提示,可以用

-v

選項,這會将你所做的改變的 diff 輸出放到編輯器中進而使你知道本次送出具體做了哪些修改。) 退出編輯器時,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           

複制

好,現在你已經建立了第一個送出! 可以看到,送出後它會告訴你,目前是在哪個分支(

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

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 file changed, 5 insertions(+), 0 deletions(-)           

複制

看到了嗎?送出之前不再需要

git add

檔案“CONTRIBUTING.md”了。

移除檔案

要從 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
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md           

複制

下一次送出時,該檔案就不再納入版本管理了。 如果删除之前修改過并且已經放到暫存區域的話,則必須要用強制删除選項

-f

(譯注:即 force 的首字母)。 這是一種安全特性,用于防止誤删還沒有添加到快照的資料,這樣的資料不能被 Git 恢複。

另外一種情況是,我們想把檔案從 Git 倉庫中删除(亦即從暫存區域移除),但仍然希望保留在目前工作目錄中。 換句話說,你想讓檔案保留在磁盤,但是并不想讓 Git 繼續跟蹤。 當你忘記添加

.gitignore

檔案,不小心把一個很大的日志檔案或一堆

.a

這樣的編譯生成檔案添加到暫存區時,這一做法尤其有用。 為達到這一目的,使用

--cached

選項:

$ git rm --cached README           

複制

git rm

指令後面可以列出檔案或者目錄的名字,也可以使用

glob

模式。 比方說:

$ git rm log/\*.log           

複制

注意到星号

*

之前的反斜杠

\

, 因為 Git 有它自己的檔案模式擴充比對方式,是以我們不用 shell 來幫忙展開。 此指令删除

log/

目錄下擴充名為

.log

的所有檔案。 類似的比如:

$ git rm \*~           

複制

該指令為删除以

~

結尾的所有檔案。

移動檔案

不像其它的 VCS 系統,Git 并不顯式跟蹤檔案移動操作。 如果在 Git 中重命名了某個檔案,倉庫中存儲的中繼資料并不會展現出這是一次改名操作。 不過 Git 非常聰明,它會推斷出究竟發生了什麼,至于具體是如何做到的,我們稍後再談。

既然如此,當你看到 Git 的

mv

指令時一定會困惑不已。 要在 Git 中對檔案改名,可以這麼做:

$ git mv file_from file_to           

複制

它會恰如預期般正常工作。 實際上,即便此時檢視狀态資訊,也會明白無誤地看到關于重命名操作的說明:

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

    renamed:    README.md -> README           

複制

其實,運作

git mv

就相當于運作了下面三條指令:

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

複制

如此分開操作,Git 也會意識到這是一次改名,是以不管何種方式結果都一樣。 兩者唯一的差別是,

mv

是一條指令而另一種方式需要三條指令,直接用

git mv

輕便得多。 不過有時候用其他工具批處理改名的話,要記得在送出前删除老的檔案名,再添加新的檔案名。

上一篇: go-runtime/debug
下一篇: 一些MIDI音樂