記錄每次更新到倉庫 —— Git 學習筆記 10

ARM的程式設計師敲著詩歌的夢發表於2018-09-02

記錄每次更新到倉庫


我們們接著很多天以前的 取得Git倉庫 這篇文章繼續說。

檔案的狀態

不管是通過哪種方法,現在我們已經有了一個倉庫,並從這個倉庫中取出了所有檔案的拷貝。接下來,對這些檔案作些修改,在完成了一個階段的目標之後,提交本次更新到倉庫。

需要說明的是,工作目錄下面的所有檔案都不外乎這兩種狀態:已知(已跟蹤)的和未知的。已跟蹤的檔案是指已經被納入版本控制的檔案,未知的檔案又分為兩種:未跟蹤和和已忽略(這個以後再說)。

已跟蹤的檔案可分為以下幾種狀態:

  1. 已提交(或未修改)
  2. 已修改
  3. 已暫存

這裡寫圖片描述

初次克隆某個倉庫時,工作目錄中的所有檔案都屬於已跟蹤檔案,且狀態為未修改(unmodified)。在編輯過某些檔案之後,Git 將這些檔案標記為已修改(modified)。然後可以用"git add"命令把這些檔案新增到暫存區,這時候它們的狀態就是已暫存(staged);再然後用“git commit”把這些暫存的檔案提交到本地倉庫後,它們的狀態又變成了未修改(unmodified)。

這裡寫圖片描述

三個區域

上面是圍繞著檔案狀態變化來說的,也可以圍繞著工作目錄、暫存區、本地版本庫來說明。

Git 系統跟蹤的檔案一般有 2 種狀態,未修改(或已提交)和已修改。未修改意味著工作目錄下的檔案內容和最近一次提交的修訂內容一致,很安全地存放在版本庫中;如果工作目錄下的檔案和最近一次提交的版本存在差異,則被認為是已修改的檔案。

不過,在 Git 系統內部,還有一個被稱為索引(index)或暫存區(staging area)的區域,它用來儲存將要提交的資訊。git add 命令用來把已修改的檔案加入索引,這將導致 Git 為其生成當前版本的快照。此時這個檔案的狀態就是已暫存(staged)。
這裡寫圖片描述

檢查當前檔案狀態

要確定哪些檔案當前處於什麼狀態,可以用 git status 命令。如果在克隆倉庫之後立即執行此命令,會看到類似這樣的輸出:

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

這說明你現在的工作目錄是乾淨的。換句話說,所有已跟蹤檔案在上次提交後都未被修改過。此外,上面的資訊還表明,當前目錄下沒有任何未跟蹤的新檔案,否則 Git 會在這裡列出來。最後,還顯示了當前所在的分支是 master,這是預設的分支名稱(分支以後再說,這裡先不用管)。

現在讓我們建立一個新檔案 README,內容是什麼無所謂,儲存後執行 git status

$ 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 不會自動將其納入跟蹤範圍,除非你明明白白地告訴它“我需要跟蹤該檔案”。

跟蹤新檔案

使用命令 git add 開始跟蹤一個新檔案。所以,要跟蹤 README 檔案,只需執行:

$ git add README

此時再執行 git status 命令,會看到:

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

只要在"Changes to be committed"這行下面列出的檔案,就說明是已暫存狀態。如果此時提交,那麼該檔案此時此刻的版本將被存入倉庫。在 git add 後面可以指明要跟蹤的檔案或目錄。如果是目錄,就說明要遞迴跟蹤該目錄下的所有檔案及子目錄。

其實 git add 的潛臺詞就是把目標檔案的快照放入暫存區域,同時未曾跟蹤過的檔案標記為已跟蹤。

取消跟蹤(un-tracking)檔案

假如想忽略某個已經跟蹤的檔案,可以用命令

 git rm --cached  <file>

注意,把 <file > 替換成具體的檔名。

舉例:

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

        modified:   world.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        oops

可以看到,world.c 是一個已經被跟蹤的檔案。

$ git rm --cached world.c
rm 'world.c'

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

        deleted:    world.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        oops
        world.c
$ ls
oops  world.c

當不再跟蹤 world.c 後, 工作區中的 world.c 不受影響。

重新跟蹤(re-tracking)檔案

如果想跟蹤一個已經被忽略的檔案,可以用

 git add -f <file>

例如:

$ git add -f world.c
$ git status --ignored
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   world.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        oops

暫存已修改檔案

我們修改一個已跟蹤的檔案(我這裡是 world.c),再執行 git status

$ echo "hello world" >>world.c

$ 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:   world.c

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

檔案 world.c 出現在“Changes not staged for commit”下面,說明已跟蹤檔案的內容發生了變化,但還沒有放到暫存區。現在讓我們執行 git add 將 world.c 放到暫存區,然後再看看 git status 的輸出。

注意: git add 命令是個多功能命令,根據目標檔案的狀態不同,此命令的效果也不同:可以用它開始跟蹤新檔案,或者把已跟蹤的檔案放到暫存區,還能用於合併時把有衝突的檔案標記為已解決狀態(這個以後再說)等。

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

        modified:   world.c

現在 world.c 已暫存,下次提交時其快照就會永久儲存在倉庫。假設此時,你突然想起來world.c還需要再改一下,比如加一行註釋。重新編輯儲存後,準備提交。不過且慢,在提交之前再執行一遍 git status 看看。

$ echo "//this is comment" >>world.c

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

        modified:   world.c

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:   world.c

怎麼回事?world.c 檔案出現了兩次!一次已暫存,一次未暫存,是不是 Git 搞錯了?

實際上 Git 只不過暫存了你執行 git add 命令時的版本,如果現在提交,那麼提交的是新增註釋前的版本,而非當前工作目錄中的版本。所以,執行了 git add 之後又作了修訂的檔案,需要重新執行 git add 把最新版本暫存起來

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

        modified:   world.c

忽略某些檔案

對於某些檔案,我們不希望把它們納入 Git 的管理,也不希望它們總出現在未跟蹤檔案列表。通常它們都是些自動生成的檔案,比如日誌檔案、編譯過程中建立的臨時檔案等。我們可以建立一個名為 .gitignore 的檔案,在裡面列出要忽略的檔案模式。來看一個實際的例子:

$ cat .gitignore
*.[oa]
*~

*.[oa]告訴 Git 忽略所有以 .o 或 .a 結尾的檔案。*~告訴 Git 忽略所有以波浪符(~)結尾的檔案,許多文字編輯軟體(比如 Emacs)都用這樣的檔名儲存副本。

檔案 .gitignore 的格式規範如下:
• 所有空行或者以 # 開頭的行都會被 Git 忽略。
• 可以使用標準的 glob 模式匹配。
• 匹配模式可以以(/)開頭防止遞迴。
• 匹配模式可以以(/)結尾指定目錄。
• 要忽略指定模式以外的檔案或目錄,可以在模式前加上驚歎號(!)取反。

所謂的 glob 模式是指 shell 所使用的簡化了的正規表示式。

  1. 星號(*)匹配零個或多個任意字元;
  2. [abc] 匹配任何一個列在方括號中的字元(這個例子要麼匹配一個 a,要麼匹配一個 b,要麼匹配一個 c);
  3. 問號(?)只匹配一個任意字元;
  4. 如果在方括號中使用短劃線分隔兩個字元,表示所有在這兩個字元範圍內的都可以匹配 (比如 [0-9] 表示匹配所有 0 到 9 的數字)。
  5. 使用兩個星號(**) 表示匹配任意中間目錄,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。

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

# 忽略.a檔案
*.a
# 但是跟蹤 lib.a
!lib.a
# 忽略當前目錄下的 TODO,但是不忽略子目錄下的 TODO
/TODO
# 忽略 build 目錄下的所有檔案
build/
# 例如忽略 doc/notes.txt, 但是不忽略 doc/server/arch.txt
doc/*.txt
# 忽略 doc 目錄下的所有 .pdf 
doc/**/*.pdf

注意:Git 允許在版本庫中任何目錄下有.gitignore檔案。每個 .gitignore 檔案都隻影響該目錄及其所有子目錄。

由於本文是針對初學者的,所以說得略簡單。如果你想了解關於 Git 忽略檔案的更多內容,可以參考我的博文: 忽略某些檔案

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

如果 git status 命令的輸出對於你來說過於籠統,你想知道具體修改了什麼地方,可以用 git diff 命令。git diff 命令可以幫我們回答兩個問題:

  1. 有哪些更新未暫存?
  2. 有哪些更新已暫存?

儘管 git status 已經通過在相應欄下列出檔名的方式回答了這2個問題,但是 git diff 將通過檔案補丁的格式(合併格式)顯示修改了哪些行。

比如我的工作目錄下有一個 README.md 檔案,裡面已經有一些內容了,它的狀態是未修改。現在修改它,但是不暫存。

$ echo 1234 >> README.md

執行 git diff 命令

$ git diff
diff --git a/README.md b/README.md
index 26573a9..78ea874 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
 my_repo
+1234

可以清楚看到,增加了一行“1234”。要了解合併格式的 diff,可以參考我的博文 diff命令輸出格式解讀

如果執行

$ git add README.md

也就是把修改提交到暫存區,這時候再次執行 git diff

$ git diff

什麼輸出都沒有,也就說明工作區的更新都已經暫存了。若要檢視已暫存的更新,可以用 git diff --cached 命令。(Git 1.6.1 及更高版本還允許使用 git diff --staged,效果是相同的。)

$ git diff --cached
diff --git a/README.md b/README.md
index 26573a9..78ea874 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
 my_repo
+1234


$ git diff --staged
diff --git a/README.md b/README.md
index 26573a9..78ea874 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
 my_repo
+1234

你也許會問,能不能比較當前工作目錄和最近一次提交之間的差異。當然可以,用命令git diff HEAD,這裡就不舉例了。

下圖列出了這三個命令的區別。

這裡寫圖片描述

提交更新

現在的暫存區已經準備妥當可以提交了。 在此之前,請一定要確認還有什麼修改過的或新建的檔案還沒有 git add 過,否則提交的時候不會記錄這些還沒暫存起來的變化。 這些修改過的檔案只保留在本地磁碟。所以,每次準備提交前,先用 git status 檢視下,是不是都已暫存起來了,然後再執行提交命令 git commit

$ git commit

這種方式會啟動文字編輯器以便輸入本次提交的說明。預設會啟用 shell 的環境變數 $EDITOR 所指定的軟體,一般都是 vim 或 emacs。當然也可以使用 git config --global core.editor 命令設定你喜歡的編輯器。比如

$ git config --global core.editor "emacs"

我是Windows作業系統,用命令 git config --list查了一下,我的配置是

core.editor='d:\PF\Notepad++\notepad++.exe' -multiInst -notabbar -nosession -noPlugin

可見,我用的軟體是 notepad++.

當我執行 git commit命令後,編輯器會顯示類似下面的文字資訊:

這裡寫圖片描述

可以看到,預設的提交訊息包含最後一次執行 git status 的輸出,放在註釋行裡,另外開頭還有一空行,供我們輸入提交說明。退出編輯器時,Git 會丟掉註釋行,用輸入的資訊生成一次提交。

另外,也可以在 git commit 命令後新增 -m 選項,將提交資訊與命令放在同一行,如下所示:

$ git commit -m "initialize"
[master 0b6ab8c] initialize
 1 file changed, 1 deletion(-)
 delete mode 100644 hello.c

可以看到,Git 會告訴我們,當前是在哪個分支(master)提交的,本次提交的 SHA-1 校驗和是什麼(0b6ab8c),以及在本次提交中,有多少檔案修訂過,多少行新增和刪改過。

請記住,提交時寫入倉庫的是放在暫存區的快照。任何還未暫存的修改仍然在磁碟上,可以在以後暫存並提交。

每一次進行提交操作,都是對專案作一次快照,以後可以退回到這個狀態,或者把某兩個提交進行比較。

跳過暫存區

儘管使用暫存區可以精心準備每一次提交,但有時候顯得麻煩。 Git 提供了一個跳過使用暫存區的方式, 只要在提交的時候,給 git commit 加上 -a 選項,Git 就會自動把所有已經跟蹤過的檔案暫存起來一併提交,從而跳過 git add 步驟:

例如,我修改了一個檔案 change_log.md,但是沒有暫存

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

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

我直接提交:

$ git commit -a -m "commit change log skip the index"
[master 32ee0ac] commit change log skip the index
 1 file changed, 1 insertion(+)

提交成功了。

注意,這種方法僅對已經跟蹤的檔案有效,如果是 untracked 的檔案,是無法提交的。比如

$ touch change_log.md
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        change_log.md

nothing added to commit but untracked files present (use "git add" to track)

這時候,我試著提交:

$ git commit -a -m "add change log"
On branch master
Untracked files:
        change_log.md

nothing added to commit but untracked files present

你瞧,提交失敗了,Git 說沒有東西可提交。

刪除檔案

在 Git中,刪除也是一個修改操作。我們先新增一個新檔案test.txt到Git並且提交:

$ touch test.txt
$ git add test.txt
$ git commit -m "add test.txt"
[master 92c6e9d] add test.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

一般情況下,我們會用rm命令刪除:

$ rm test.txt

這個時候,Git 知道你刪除了該檔案。因此,工作區和版本庫就不一致了,用git status命令檢視一下:

$ 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:    test.txt

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


現在你有兩個選擇:

  1. 確實要從版本庫中刪除該檔案
  2. 誤刪了,想把這個檔案找回來

對於1,用命令git rm刪掉,並且git commit

$ git rm test.txt
rm 'test.txt'

$ git commit -m "rm test.txt"
[master 6c0b39a] rm test.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 test.txt


現在,檔案就從版本庫中被刪除了。

對於2,可以用git checkout -- <file>把檔案從版本庫裡找回來。例如:

$ git checkout -- test.txt


這裡的git checkout其實是用版本庫裡的版本來更新索引,同時覆蓋工作目錄中對應的檔案,無論工作區的檔案是被修改還是被刪除,都可以“一鍵還原”。

其實,如果確實要從版本庫刪除某個檔案,只需要2個步驟。

git rm <file>
git commit

git rm表示從工作區刪除檔案,並且把這個變更暫存(就像 git add 一樣);git commit表示在版本庫實現這個變更。

需要說明的是,如果檔案修改了,且修改未提交,刪除就會失敗。例如:

$ echo 11 >>oops
$ 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:   oops

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

$ git rm oops
error: the following file has local modifications:
    oops
(use --cached to keep the file, or -f to force removal)
# 以上是未暫存的情況



$ git add oops

$ git rm oops
error: the following file has changes staged in the index:
    oops
(use --cached to keep the file, or -f to force removal)
# 以上是暫存了但是未提交的情況

這時候需要加 -f 強行刪除。

$ git rm oops -f
rm 'oops'

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

        deleted:    oops

$ ls


移動檔案

在 Git 中可以執行下面的命令重新命名檔案:

git mv <old_file> <new_file>

例如:

$ git mv banana.c grape.c

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

        renamed:    banana.c -> grape.c


$ git commit -m xx
[master 0c89153] xx
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename banana.c => grape.c (100%)


參考資料

【1】《精通Git(第2版)》,Scott Chacon & Ben Straub,人民郵電出版社

【2】 https://www.liaoxuefeng.com/

相關文章