[檔案管理] git 教學:提交--保存每一次的更動 - 精華區

[檔案管理] git 教學:提交--保存每一次的更動

阿慶

阿慶圖像

2020-03-25 00:37:09

From:122.116.71.150

安裝與設定完 git 不代表你的系統已經開始受到 git 的版控

而是我們需要把資料夾與檔案加入 git 的控管當中

事實上我們現在還沒有開始讓 git 控管我們的任何一個檔案或資料夾

之後我們都會在 git bash 上作業,讓我們先進入 git bash 中,應該還沒忘吧。

讓資料夾被 git 版控

我們新增一個資料夾並讓這個資料夾被 git 版控,也就是被 git 追蹤

說明一下之後的體歷:

  • # 井號開頭是註解,不需要輸入,通常是用來說明與解釋指令
  • $ 錢號後面則是我們要輸入的指令
  • 沒有這些符號的就是下完指令按 enter 執行後的輸出結果
# 進入 git bash 後先看看自己在哪裡
$ pwd
 /c/Users/Administrator
# 切換到 D 槽,不一定要這樣,就看大家習慣,反正只是練習
$ cd /d
# 建立一個資料夾做為練習,就叫 practice 吧
$ mkdir practice
# 進入該資料夾
$ cd practice
# 初始化這個資料夾,讓他開始被 git 版控
$ git init
 Initialized empty Git repository in D:/practice/.git/

上面最後一個指令的執行結果是告訴我們 practice 開始被 git 版控了

他會在 practice 下新增一個 .git 的資料夾

這個資料夾就是在記錄 git 的版控資訊

其他加入追蹤的資料夾或檔案被刪被改了都救得回來,都是靠 .git 的紀錄資訊

所以這個 .git 很重要,被刪了就等於是解除 git 的版控了

.git 沒了就會讓該資料夾回到一般的狀態,改了就改了,刪了就刪了

提醒

因為 . 點開頭的是 linux 的隱藏檔或隱藏資料夾,輸入 lsls -l 會發現看不到他

這時候要加入 a 參數,也就是 ls -als -al 就可以看到他囉

其他已存在的資料夾想加入 git 就直接在該資料夾內輸入 git init 即可

查看 git 狀態

接下來要做什麼呢?先來看一下目前 practice 的 git 狀態

$ git status
 On branch master

 No commits yet

 nothing to commit (create/copy files and use "git add" to track)

上面是說,目前我們在 master 分支,這是一開始的主要分支,

沒有需要提交(commit) 的檔案,你可以輸入 git add 來將檔案加入追蹤

細節我們會在之後補充,目前先讓大家熟悉整個 git 的流程就好

把檔案交給 git 追蹤

我們在 practice 內建立一個新檔案,檔名為 welcome.txt 內容寫 你好

git bash 中可以用 vim 或 notepad 編輯器等方式完成這件事

$ vim welcome.txt
# 看到畫面一片空白,表示已經進入 vim

i 切換成插入模式

輸入 你好

按 esc 鍵切換回普通模式

輸入 :wq 並按 enter 存檔離開回到 $ 提示符號

如果不想用編輯器的話,可以直接用一行指令完成這件事

$ echo '你好' > welcome.txt

原本 echo 是把字印在畫面上,加上大於就是把前面的字串輸入到檔案中,

所以大於後面接了檔名就能夠把資訊輸入到檔案中了

小心,如果檔案存在的話會直接被蓋掉喔

編輯好後再來看看 git 狀態

$ git status
 On branch master

 No commits yet

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

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

重複的我們就不解釋了,看到 Untracked files 就表示有一些檔案還沒有被加入追蹤

其實他都有建議的下一步動作,訊息還滿清楚的

所以,git 是不會自動幫我們記錄的,需要我們手動加入追蹤

因為可能有不同的需求,自動記錄的話主動權就尚失了,這個之後再說

$ git add welcome.txt

加入後再來確認目前的 git 狀態

$ git status
 On branch master

 No commits yet

 Changes to be committed:
   (use "git rm --cached <file>..." to unstage)
         new file:   welcome.txt

沒有 Untracked 了,變成 new file 表示新建的檔案 welcome.txt 已經被追蹤了,只是還沒提交而已

他目前在暫存區(Staging Area) 如果提交了才會被放到儲存庫裡

提醒

git add 就跟 ls, copy 等這些指令一樣可以使用萬用字元

像是 git add *.txt 等一次加入多個檔案

使用 git add .git add --all 會加入該資料夾下包含所有子資料夾與檔案到追蹤清單

這兩個參數有一點點不同,--all 不管在哪個資料夾下,都會把 git init 初始化資料夾下含所有子資料夾之檔案加入追蹤

. 只會加入該資料夾下所有子資料夾之檔案到追蹤清單

所以在 git init 的那層為根目錄(資料夾)的話,輸入 git add . 就會加入該資料夾下所有檔案與資料夾到追蹤清單

這時候 .--all 是完全相同的

相對的如果只是在某個子資料夾用 . 加入追蹤的話,那麼上一層的資料就不會被加入了

狀況

做好 git add 的檔案再次編輯後還想加入追蹤則還需要再次 git add

否則直接提交的話會是前一次 git add 的內容而已,還沒 git add 的就不會被提交到儲存庫

我們再次編輯 welcome.txt 再第二行加入 我很好

# 變成兩個大於,就是直接在檔案後面添加資訊
$ echo '我很好' >> welcome.txt
# 再來看一下狀態
$ git status
 On branch master

 No commits yet

 Changes to be committed:
   (use "git rm --cached <file>..." to unstage)
         new file:   welcome.txt

 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   welcome.txt

看起來好像有兩個 welcome.txt 檔案,後面的狀態是 modified

並且提醒我們是要再 git add 還是想回到之前的版本等等

仔細看訊息其實就可以知道目前檔案的狀態,以及應該要做什麼事情

那就再次 git add . 加入追蹤

$ git add .
$ git status
 On branch master

 No commits yet

 Changes to be committed:
   (use "git rm --cached <file>..." to unstage)
         new file:   welcome.txt

好了,準備提交吧

提交到儲存庫裡存檔

想要將資訊從暫存區永久存放至儲存庫(倉庫)還需要最後一個動作,也就是提交

commit 就是提交指令,-m 參數後面用引號包住文字訊息

如果只輸入 git commit 應該會開啟預設編輯器讓你輸入提交的文字訊息

$  git commit -m '加入git並新增welcome.txt檔案'
 [master (root-commit) 0fdb52f] 加入git並新增welcome.txt檔案
  1 file changed, 2 insertions(+)
  create mode 100644 welcome.txt

從上面的資訊至少可以了解到:

  • 這是主要(master) 分支,產生的版號是 0fdb52f 接著是我們寫的提交訊息
  • 有 1 個檔案中的 2 行被提交
  • 看到 create 表示有新的檔案被建立,是之前沒有的,最後是權限跟檔名

從這些資訊我們也可以知道剛剛做了哪些事情

我覺得看到很多改變會突然覺得很有成就感

提交訊息的建議:

  1. 簡單易懂就好
  2. 避免情緒性文字,以免團隊合作時產生不必要的問題
  3. 避免不清楚或不完整的資訊,如:修改檔案或新增文字

提交時只會處理暫存區的資訊到儲存庫,這也就是我們剛剛提到的

沒有 git add 的資訊是不會被儲存下來的

完成提交才是完成整個 git 一般流程

提示

如果沒東西也想提交的話可以使用 --allow-empty 參數

# 看一下剛剛完成提交的狀態
$ git status
 On branch master
 nothing to commit, working tree clean
# 沒東西,來提交看看
$ git commit -m '沒東西'
 On branch master
 nothing to commit, working tree clean
# 沒東西硬是要提交
$ git commit --allow-empty -m '沒東西'
 [master 462682d] 沒東西

雖然提交空的沒什麼意義,不過在測試合併時可能會需要這樣做

還有一些特殊狀況,就是想要有一筆紀錄時也會這樣做

補充

想要在 git bash 貼上的話,可以直接按快顯鍵,並使用下方向鍵找到 paste 按 enter

後面也有列出快速鍵 shift+insert 如果 insert被設定成 NVDA 鍵那就要連按兩下囉

所以這個環境不能按 ctrl+v 貼上喔

結論

  • 初始化被 git 版控 git init 只要做一次就好
  • 讓 git 追蹤所有內容 git add .
  • 提交這次的追蹤內容到倉庫 git commit -m '訊息'
  • 經常使用 git status 來確認目前的 git 狀態

三個區域

在上一節,我們提到暫存區與儲存庫的概念。

事實上,以 git 的角度來看可以分成三個區域:

  • 工作目錄:就是我們處理檔案或資料夾的現行目錄,當然是有下過 git init 的才有 git 追蹤
  • 暫存區:就是我們下 git add . 會把這些工作目錄的資訊記錄在暫存區
  • 儲存庫:就是 git commit -m '訊息' 後把記錄由暫存區存放到永久的儲存庫中

如果覺得兩個步驟太麻煩,可以直接下 git commit -a -m '訊息'

但這個指令對於有新創建的檔案還沒被追蹤的狀況是無效的,因此有新加入的檔案還是先乖乖 git add . 比較保險。

如果只是修改原有的檔案,想偷懶一下就可以合併著用一個指令。

至於何時要下 commit 的時機,以下只是建議:

  1. 完成一個任務:就算只是改一個參數或新增一行程式碼都算。
  2. 完成一天的工作:雖然還沒完成任務,但要下班了還是先存到儲存庫比較保險,一方面是備份,另一方面也是讓人家知道你有進度有做事情。
  3. 其實隨時想 commit 都可以 commit 也沒什麼不行的,頂多就是比較長 commit 紀錄看起來會比較多比較零碎而已。

至於一個任務要怎麼界定也不好說,有時只改了一個數字對系統的影響就很大,

例如把每頁最多筆數由10改成1000筆,對使用者與系統來說就有很大的影響。

而加了好幾十行的註解對使用者來說完全無感,但對於要維護後續程式的人就有很大的幫助。

有時候一個功能的撰寫也沒那麼快,也許要一兩個星期,但我們最好不要完成整個功能才 commit

因為這樣中間想要回到之前的版本或修改就沒辦法了,也失去 git 的意義。

如果我們做一段修改就可以 commit 一下,這樣我們出錯隨時想回到哪個狀態就都很方便。

最重要的是紀錄最好可以寫清楚,但也不要當作寫心得或作文,條列式的記錄一下重點就行,

反正詳細檔案改了什麼地方也可以查得到,接下來我們就要來看紀錄囉。

更改提交的內容

有時候在 commit 時沒想清楚,漏掉了幾點要記錄的,或者發現打錯字了想修改,

甚至是工程師因為遇到澳客心情不好罵了幾句,這些都需要改 commit 的提交內容。

以下有幾種方法:

  1. 把 .git 整個資料夾刪掉
  2. 使用 git rebase 來修改歷史
  3. 先把 commit 用 git reset 拆掉,整理後再重新 commit
  4. 使用 --amend 參數來修改最後一次的 commit 紀錄

第一種最好不要做,因為這樣整個 git 的紀錄都沒了,有點因小失大。

第二和三種之後會介紹,雖然我們上個章節偷用了一下第三種方法。

這邊先介紹最簡單的第四種,不過如果要修改的 commit 不是前一個最新的,那就不行了。

先來看一下現在的紀錄:

$ git log --oneline
 93bb4ec (HEAD -> master) 特種兵要我改檔名,讓我很不爽,明明不懂還裝懂
 462682d 沒東西
 0fdb52f 加入git並新增welcome.txt檔案

看來最新一筆的 commit 內容有點不妙,讓我們加上 --amend 參數來修改

$ git commit --amend -m '將 welcome.txt 更名為 hello.txt'
 [master 9f2f6dd] 將 welcome.txt 更名為 hello.txt
  Date: Sun Feb 23 22:30:28 2020 +0800
  1 file changed, 0 insertions(+), 0 deletions(-)
  rename welcome.txt => hello.txt (100%)

如果沒加上 -m 就會跳出預設編輯器要你輸入提交的訊息。

檢查一下:

$ git log --oneline
 9f2f6dd (HEAD -> master) 將 welcome.txt 更名為 hello.txt
 462682d 沒東西
 0fdb52f 加入git並新增welcome.txt檔案

有沒有發現連原本的版本號都變了呢,這就表示雖然只是改 commit 內容,

但對 git 來說其實是一個新的 commit 蓋掉原本的而已。

至於想改更之前的 commit 就要使用 rebase 了,這個之後再說吧。

補充

如果有使用遠端倉庫,做這個動作後 push 上去都會發生錯誤,

目前我的做法是本地端做好後先從遠端倉庫把資料 pull 回來讓他自動 merge 接著再直接 push 回去,

這樣就會多一筆 merge 的 log 紀錄,目前先這樣處理,之後再看看有什麼更好的方式。

關於遠端倉庫,之後會說。

追加檔案到最近的提交

雖然說就再做一次 commit 就好,但有時會覺得這明明是之前那個 commit 應該做的事情,

為了想保持同一個 commit 的完整性,不想再做一次 commit 這樣,也是可以辦到。

我記得上次改內部系統的一個程式,提交後才發現有個檔案沒複製過去,

但是上一個 commit 內容已經有寫到這件事了,所以就會有這個需求。

至少有兩種方式:

  1. 使用 git reset 把最後一次的 commit 拆掉再加入檔案後重新 commit 一次
  2. 使用 --amend 參數進行 commit

第一種還是以後再說,先介紹第二種方式好了。

我們現在用 git bash 的指令複製一個檔案過來當作是新增的檔案

$ cp /d/test.py .
$ git status
 On branch master
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
         test.py

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

檔案加進來了但還沒有被 git 追蹤到(Untracked files),那就先加到暫存區吧

$ git add .

接著用 --amend 參數來 commit

$ git commit --amend --no-edit
 [master 25c6264] 將 welcome.txt 更名為 hello.txt
  Date: Sun Feb 23 22:30:28 2020 +0800
  2 files changed, 35 insertions(+)
  rename welcome.txt => hello.txt (100%)
  create mode 100644 test.py

如果覺得 commit 內容也要跟著改,那就不要使用 --no-edit 參數,

而是用上一節的方式來編輯提交訊息。

補充

如果有使用遠端倉庫,做這個動作後 push 上去都會發生錯誤,

目前我的做法是本地端做好後先從遠端倉庫把資料 pull 回來讓他自動 merge 接著再直接 push 回去,

這樣就會多一筆 merge 的 log 紀錄,目前先這樣處理,之後再看看有什麼更好的方式。

關於遠端倉庫,之後會說。

忽略

就是某些檔案雖然在 git init 的資料夾中,但不想要被 git 加入追蹤。

像是一些重要的帳號密碼資訊、在程式編譯中產生的中間的過渡檔案等,

此時,只要在 .gitignore 檔案新增略過的規則即可,沒有該檔案的話可以自行新增。

# 假設他不存在
$ touch .gitignore
# 編輯該檔案
# 忽略 password.ini 檔案
password.ini
# 忽略 database 資料夾下的 mysql.ini 檔案
database/mysql.ini
# 忽略所有 db 資料夾中附檔名為 txt 的檔案
/db/*.txt
# 忽略所有附檔名是 py 的檔案
*.py

之後在新增檔案時符合 .gitignore 檔案內的忽略規則就會被略過,

比較特別的是 .gitignore 一旦新增就算沒有存到暫存區或提交也都有效。

我們已經新增了忽略規則檔案,但還沒有提交,讓我們來做個實驗。

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

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

# 因為沒有 add 也沒有 commit 接著測試看看該忽略的檔案會不會被忽略
$ touch password.ini
# 已經新增應該被忽略的檔案
$ git status
 On branch master
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
         .gitignore

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

從上面的紀錄中可以發現,提醒 Untracked files 依然只有 .gitignore

這就表示 password.ini 完全被忽略了。

闖關

在存到暫存區時加上 -f 就可以強行闖關,也就是雖然 .gitignore 有忽略規則,

但他會忽略這些忽略規則。

$ git add -f .
$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         new file:   .gitignore
         new file:   password.ini

從上面的紀錄可以看出 password.ini 依然被存到暫存區了,闖關成功。

忽略無效

有一個狀況雖然已經加了忽略規則,但該檔案依然沒被忽略,

這是因為 git 跟一般法律一樣有不溯既往的特性,

也就是說原本已經存在的檔案,現在加入忽略規則也對那些檔案無效,

上面的範例有效是因為 password.ini 是在忽略規則建立後台新增的。

那怎麼辦呢?就利用之前學到的 git rm 檔名 --cached 手動移除該檔案的追蹤,

這樣 .gitignore 的忽略規則就會開始對他們有效了。

另外,使用 git clean -X 可以把所有不被追蹤的檔案刪除。

$ git rm password.ini --cached
 rm 'password.ini'
$ git clean -fX
 Removing password.ini
$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         new file:   .gitignore

$ ls
 hello.txt  test.py

password.ini 被刪掉了,加上 -f 是強制執行的意思。

沒看到 .gitignore 是因為他是 . 開頭的隱藏檔,要使用 -a 列出全部才看得到。

空資料夾自動忽略

最後,提醒一下,在新增空資料夾時就算 add 與 commit 後也都不會被追蹤,

我們一直在強調檔案檔案,就是在告訴大家,git 是以檔案為單位來追蹤的,

所以資料夾一定要放個檔案才會被加入追蹤喔。

部分提交

如果某個檔案只想提交某些內容上去,而不是整個檔案的話也做得到。

這個會發生在某個檔案的某個區塊還沒寫好,但其他區塊必須被使用的狀況。

例如修掉了 A bug 接著正在寫 B 功能還沒寫完,但主管說這個修掉的 bug 很棘手,

必須先推上去正式版讓大家使用,那為了怕還沒寫完的功能也一起上架了,就先提交已經修掉 bug 的那部分程式即可。

當然針對這個狀況會有更好的做法,但我們只是舉例。

先來看一下 hello.txt 這個檔的內容:

$ cat hello.txt
 你好
 我很好
 測試
 以下這兩行不要提交
 第一行
 第二行
 這邊是要提交的下半部

好,假設我們現在不想把那兩行提交(第4與5行),那該怎麼做呢?

此時需要使用參數 -p 來提交再進行編輯,把不想提交的部分刪掉。

# 目前是編輯完 hello.txt 還沒加到暫存區
$ git status
 On branch master
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   hello.txt

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

$ git add -p hello.txt
 diff --git a/hello.txt b/hello.txt
 index a660351..3f9701b 100644
 --- a/hello.txt
 +++ b/hello.txt
 @@ -1,3 +1,4 @@
 +
  你好
  我很好
  測試
 (1/1) Stage this hunk [y,n,q,a,d,e,?]? e
# 如果回答 y 就是整個檔案提交,等於是沒有加 -p 的效果
# 我們回答 e 就可以進行編輯
# 他會以預設編輯器把 hello.txt 打開
# 我們刪掉第4與5行之後存檔離開

# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,3 +1,6 @@
 你好
 我很好
+以下兩行不想提交
+第一行
+第二行
 測試
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again.  If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.

# 有加號的部分是我們最新新增的行但還沒提交的狀況
# 刪掉存檔離開後回到 git bash
$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   hello.txt

 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   hello.txt

$ cat hello.txt
 你好
 我很好
 以下兩行不想提交
 第一行
 第二行
 測試

# 在工作目錄的 hello.txt 檔案還是完整的
$ git commit -m '部分提交練習'
 [master 362f466] 部分提交練習
  1 file changed, 1 insertion(+)

$ git status
 On branch master
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   hello.txt

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

明明已經提交,但為什麼暫存區告訴我們 hello.txt 被修改了?沒錯,因為我們把不要提交的部分刪掉了,

所以不想提交的部分就被丟到暫存區囉,我們仔細看一下 hello.txt 的紀錄

$ git blame hello.txt
 ^0fdb52f welcome.txt (Logo Kuo          2020-02-16 13:01:25 +0800 1) 你好
 ^0fdb52f welcome.txt (Logo Kuo          2020-02-16 13:01:25 +0800 2) 我很好
 362f4669 hello.txt   (Logo Kuo          2020-03-08 23:06:35 +0800 3) 以下兩行不想提交
 00000000 hello.txt   (Not Committed Yet 2020-03-08 23:08:27 +0800 4) 第一行
 00000000 hello.txt   (Not Committed Yet 2020-03-08 23:08:27 +0800 5) 第二行
 b7df6113 hello.txt   (Logo Kuo          2020-02-28 21:31:22 +0800 6) 測試

由上面的紀錄可以看到最前面 00000000 開頭的那兩行,有標上 not commited yet 的字樣,

這樣就可以知道這幾行並沒有被提交上去,正合我們的意思。

修改歷史紀錄

之前我們分享過使用 --amend 來修改上一次的 commit 提交訊息。

但如果我們想修改的是更之前的提交訊息,且可能不只一筆的話該怎麼做?

我們要利用 rebase 指令的互動模式,這個指令可以用另一種方式來合併分支,在這裡,我們先把焦點放在互動模式上。

# 先看一下最近 5 個 commit
$ git log --oneline -5
 8436216 (HEAD -> master) 保留 new_test 的版本
 2a621b5 我也加了相同的 word 檔檔名進來
 0ceca24 (new_test) 增加一個 word 檔
 ca4c2fe Merge commit 'fa03700'
 fa03700 在 abc.txt 加了一行

我覺得第2個提交訊息不清楚,想要把當時加入哪個 word 檔的檔名補上去。

# 啟動 rebase 的互動模式,時間段是從現在到該版號之間的所有紀錄
# 他會跳出預設編輯器,列出這個區間段的所有 commit 紀錄
# 如果是 c 要合併到 b 那我們就設定不會更動到的 a
$ git rebase -i fa03700

pick 71188bf 在 ooqq.txt 新增第二與三行
pick 2a621b5 我也加了相同的 word 檔檔名進來
pick 0ceca24 增加一個 word 檔

# Rebase fa03700..8436216 onto fa03700 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

pick 是保持原本的 commit 與訊息

reword 是修改該條 commit 的訊息

其他指令後面章節會介紹。

這時候我把要修改訊息的 commit 前面 pick 換成 reword 也可以只打 r 就好:

pick 71188bf 在 ooqq.txt 新增第二與三行
reword 2a621b5 我也加了相同的 word 檔檔名進來
pick 0ceca24 增加一個 word 檔

然後存檔離開。接著還是會在編輯器中,要你修改 commit 的內容,

修改好後存檔離開,這樣就完成了。

$ git log --oneline -1
 d7de1ff (HEAD -> master) 我也加了生活班232小時課表--0610.doc

其實使用 rebase 修改 commit 並不只是改字而已,被修改及之後的 commit 版號都會改變,

對 git 來說其實是之後的 commit 都重做了一遍。

如果後悔了可以使用 git reset --hard ORIG_HEAD 來回到還沒 rebase 的狀態。

ORIG_HEAD 是每次我們在做較危險動作時會產生的一個紀錄點,例如合併分支,刪除分支等等。

有點像是準備做危險動作時先留一份備份的感覺。

如果是處理到一半想放棄,就輸入 git rebase --abort

合併 commit

有時候 commit 比較雜亂就必須合併一下,我們來看這個例子:

# 看最近 4 個 commit
$ git log --oneline -4
 82cdfe4 (HEAD -> master) 在 hello.txt 加入第3行
 41c1423 又刪掉 abc.txt
 b0a547f 刪掉生活班232小時課表--0610.doc
 d7de1ff 我也加了生活班232小時課表--0610.doc

我們想把那兩個刪掉檔案的 commit 合併成一個就好。

所以我們把 rebase 設定在更早一個的 commit 上:

注意,rebase -i 的交互模式顯示 commit 由上到下是由舊到新,

與我們的 log 顯示是由新到舊剛好相反。

$ git rebase -i d7de1ff

pick b0a547f 刪掉生活班232小時課表--0610.doc
pick 41c1423 又刪掉 abc.txt
pick 82cdfe4 在 hello.txt 加入第3行

我們列出前半部需要修改的部分就好,第一個和第二個 commit 需要合併,

所以我們在第二個 commit 把 pick 改成 quash 然後存檔離開

pick b0a547f 刪掉生活班232小時課表--0610.doc
squash 41c1423 又刪掉 abc.txt
pick 82cdfe4 在 hello.txt 加入第3行

接下來就看你是要把本來的註解掉再重新寫一個,還是用修改的方式變成你要的內容存檔離開。

[detached HEAD d78c4ff] 刪掉生活班232小時課表--0610.doc 與 abc.txt
  Date: Sat Apr 4 17:26:07 2020 +0800
  2 files changed, 2 deletions(-)
  delete mode 100644 abc.txt
  delete mode 100644 "\347\224\237\346\264\273\347\217\255232\345\260\217\346\231
 \202\350\252\262\350\241\250--0610.doc"
 Successfully rebased and updated refs/heads/master.

再觀察一下 log 是不是我們要的結果:

$ git log --oneline -4
 316ffbc (HEAD -> master) 在 hello.txt 加入第3行
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
 d7de1ff 我也加了生活班232小時課表--0610.doc
 fa03700 在 abc.txt 加了一行

因為兩個 commit 合併成一個,所以看起來比一開始多顯示了一筆 commit 資訊。

這邊附上 rebase -i 互動模式的其他指令說明:

  1. pick:保留该 commit 不變動
  2. reword:保留该 commit,但需要修改该 commit 的訊息
  3. edit:保留该 commit 但要停下来修改该提交的內容,不只是訊息而已
  4. squash:将该 commit 和前一个 commit 合并
  5. fixup:将该 commit 和前一个 commit 合并,但不需要保留该提交的訊息
  6. exec:执行 shell 命令
  7. drop:丢弃该 commit

拆解 commit

上一節是介紹把幾個 commit 合在一起,這一次要分享的是把一個 commit 拆成多個 commit 的狀況。

這個操作會比較複雜一點,原因在於他不是只有寫寫訊息那麼簡單而已。

需要實際改動這個 commit 操作的行為。我們需要回到要拆解的 commit 先把這個 commit 修改的檔案放回工作目錄,

接著在依照我們想要的方式分別做出新的 commit 這樣。

所以我們得先知道這個 commit 做了什麼,然後才知道該怎麼拆解 commit.

我先做了一個 commit 出來,裡面更改了一個檔案,又新增了另一個檔案,現在我們想把他拆開。

# 先來查看這個 commit 做了什麼事
$ git log -p 2a08291
 c9175348e5ad240 (HEAD -> master)
 Author: Logo Kuo <logo@forblind.org.tw>
 Date:   Sat Apr 4 19:51:54 2020 +0800

     忘記做了什麼事

 diff --git a/log.txt b/log.txt
 index 440e1f8..7403fbd 100644
 --- a/log.txt
 +++ b/log.txt
 @@ -27,3 +27,4 @@ index 0000000..65c6e5c
  @@ -0,0 +1,2 @@
  +你好
  +我很好
 +用不太習慣 windows 的 vim
 diff --git a/test.py b/test.py
 index 10ae50f..626aa3a 100644
 --- a/test.py
 +++ b/test.py
 @@ -1,35 +1,73 @@
 -class Bank:
 -       '''這是一個銀行類別,裡面有存款金額與存錢、提錢等功能'''

以下省略

簡單說,就是有兩個檔案有增刪,加號是增加的,減號是減掉的。

有興趣可以開一下原檔來比對一下。

好吧,那我們不旦覺得註解寫得很爛,也想把這兩個修改檔案事件拆開成兩個 commit.

# 看一下 log
$ git log --oneline -2
 2a08291 (HEAD -> master) 忘記做了什麼事
 316ffbc 在 hello.txt 加入第3行

# 把 rebase 設定在要拆解的 commit 的前一個 commit
$ git rebase -i 316ffbc

pick 2a08291 忘記做了什麼事

# 把 pick 改成 edit 並存檔離開
edit 2a08291 忘記做了什麼事

這時候 rebase 看到 edit 時就會停下來,我們看一下狀態

$ git status
 interactive rebase in progress; onto 316ffbc
 Last command done (1 command done):
    edit 2a08291 忘記做了什麼事MINGW64 /d/practice (master)
 No commands remaining.
 You are currently editing a commit while rebasing branch 'master' on '316ffbc'.
   (use "git commit --amend" to amend the current commit)
   (use "git rebase --continue" once you are satisfied with your changes)
 Administrator@DESKTOP-PI1KHDL MINGW64 /d/practice (master)
 nothing to commit, working tree clean
 hint: Waiting for your editor to close the file...

簡單說,就是我們還處在 rebase 這個狀態當中,接下來我們運用之前學過的 reset 先回到這個版本的前一個版本

$ git reset HEAD~
 Unstaged changes after reset:
 M       log.txt
 M       test.py

因為這樣做以後,git 會把 edit 所在的 commit 做的事情丟回工作目錄,

然後依照我們自己的需求 add 檔案再提交,所以我們這樣做。

# 一樣看一下狀態就知道自己接下來該怎麼做
$ git status
 interactive rebase in progress; onto 316ffbc
 Last command done (1 command done):
    edit 2a08291 忘記做了什麼事
 No commands remaining.
 You are currently splitting a commit while rebasing branch 'master' on '316ffbc'
 .
   (Once your working directory is clean, run "git rebase --continue")

 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   log.txt
         modified:   test.py

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

前面告訴我們一樣處在 rebase 狀態,後面的資訊就很熟悉了,要我們把工作目錄的狀況寫到暫存區,

這時候當然不能 git add . 因為這樣就跟原本的 commit 沒兩樣囉。

所以我們加入一個檔案就 commit 一下,這樣就可以分別做出兩個 commit 了:

# 加入 lot.txt 到暫存區
$ git add log.txt

# 先做一次提交
$ git commit -m '在 log.txt 加了第三行'

 [detached HEAD 6dd175d] 在 log.txt 加了第三行
  1 file changed, 1 insertion(+)

# 再觀察一下狀態
$ git status
 interactive rebase in progress; onto 316ffbc
 Last command done (1 command done):
    edit 2a08291 忘記做了什麼事
 No commands remaining.
 You are currently splitting a commit while rebasing branch 'master' on '316ffbc'
 .
   (Once your working directory is clean, run "git rebase --continue")

 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   test.py

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

# 再次將工作目錄的結果存到暫存區,這次可以用小數點偷懶了
$ git add .

# 將結果提交吧
$ git commit -m '複製一個 test.py 蓋掉原本的同名檔案'
 [detached HEAD 3d914e7] 複製一個 test.py 蓋掉原本的同名檔案
  1 file changed, 73 insertions(+), 35 deletions(-)
  rewrite test.py (99%)

事情還沒完喔,再看一下狀態:

$ git status
 interactive rebase in progress; onto 316ffbc
 Last command done (1 command done):
    edit 2a08291 忘記做了什麼事
 No commands remaining.
 You are currently editing a commit while rebasing branch 'master' on '316ffbc'.
   (use "git commit --amend" to amend the current commit)
   (use "git rebase --continue" once you are satisfied with your changes)

 nothing to commit, working tree clean

別忘了,我們還在 rebase 裡面呀,現在處理好了,就讓 rebase 繼續跑完就好:

# 讓 rebase 做完後面他該做的事情
$ git rebase --continue
 Successfully rebased and updated refs/heads/master.

# 成功了,再次確認狀態
$ git status
 On branch master
 nothing to commit, working tree clean

# 已經結捒離開 rebase 了且一切正常,那看一下 log 紀錄
$ git log --oneline -3
 3d914e7 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 6dd175d 在 log.txt 加了第三行
 316ffbc 在 hello.txt 加入第3行

如果要更仔細點就分別再來看一下這兩個 commit 的提交內容來確定是否正確。

做到這裡大家有沒有覺得我很龜毛,一直看狀態看狀態的,我想表達的其實只有一個觀念,

其實狀態裡的訊息很清楚,也有告訴我們可以怎麼做或該怎麼做,講誇張點,只要狀態訊息看得懂再加上查清楚他提到的指令,

再處理 git 根本不需要去死背什麼步驟,他會帶領我們前進,學 git 要這樣才會玩得活,

不是被這些流程跟指令參數綁死在那,而是根據這些提示來依據我們實際的狀況完成操作就好

插入 commit

想要在兩個 commit 之間再增加新的 commit 也可以

其實做法跟拆解 commit 的動作差不多

都是利用 rebase 的 edit 來進行的

# 看一下現在的前 5 個 log
$ git log --oneline -5
 3d914e7 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 6dd175d 在 log.txt 加了第三行
 316ffbc 在 hello.txt 加入第3行
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
 d7de1ff 我也加了生活班232小時課表--0610.doc

我想在 6dd175d316ffbc 之間再加兩個 commit

那我們就把 rebase 設在更早一個的 d78c4ff 上:

$ git rebase -i d78c4ff
# 並且把 316ffbc 的 pick 改成 edit 存檔離開
# 再次提醒, rebase -i 的順序跟 log 是相反的,我們要編輯的是比較早的那一個
edit 316ffbc 在 hello.txt 加入第3行
pick 6dd175d 在 log.txt 加了第三行
略

接著就是做要新增 commit 的事情:

# 刪掉 ooqq.txt
$ rm -f ooqq.txt

$ git status
 interactive rebase in progress; onto d78c4ff
 Last command done (1 command done):
    edit 316ffbc 在 hello.txt 加入第3行
 Next commands to do (2 remaining commands):
    pick 6dd175d 在 log.txt 加了第三行
    pick 3d914e7 複製一個 test.py 蓋掉原本的同名檔案
   (use "git rebase --edit-todo" to view and edit)
 You are currently splitting a commit while rebasing branch 'master' on 'd78c4ff'
 .
   (Once your working directory is clean, run "git rebase --continue")

 Changes not staged for commit:
   (use "git add/rm <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         deleted:    ooqq.txt

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

# 接下來的動作不用我多說了吧
$ git add ooqq.txt

$ git commit -m '刪除 ooqq.txt'
 [detached HEAD 13a95ad] 刪除 ooqq.txt
  1 file changed, 1 deletion(-)
  delete mode 100644 ooqq.txt

有看到 detached HEAD 這個斷頭關鍵字吧,因為現在我們算是處在 rebase 的其中一個臨時空間中,

並不是在一般的 HEAD 指向上,所以會有這個提示字樣。

# 再看一下狀態
$ git log --oneline -3
 13a95ad (HEAD) 刪除 ooqq.txt
 316ffbc 在 hello.txt 加入第3行
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt

# 新增一個 qqoo.txt 檔案再做一個 commit
$ touch qqoo.txt

$ git add .

$ git commit -m '新增 qqoo.txt'
 [detached HEAD 9a73d0c] 新增 qqoo.txt
  1 file changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 qqoo.txt

# 再看一下狀態
$ git log --oneline -3
 9a73d0c (HEAD) 新增 qqoo.txt
 13a95ad 刪除 ooqq.txt
 316ffbc 在 hello.txt 加入第3行

應該沒問題,那就完成後續的 rebase 吧

$ git rebase --continue
 Successfully rebased and updated refs/heads/master.

# 看到 Successfully 關鍵字就可以收工囉
$ git log --oneline -5
 aee4e62 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 0338589 在 log.txt 加了第三行
 9a73d0c 新增 qqoo.txt
 13a95ad 刪除 ooqq.txt
 316ffbc 在 hello.txt 加入第3行

沒問題,再次成功動了手腳。

刪除 commit

先來講一下怎麼調整 commit 的順序好了。

$ git log --oneline -7
 aee4e62 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 0338589 在 log.txt 加了第三行
 9a73d0c 新增 qqoo.txt
 13a95ad 刪除 ooqq.txt
 316ffbc 在 hello.txt 加入第3行
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
 d7de1ff 我也加了生活班232小時課表--0610.doc

我想要讓新增的資訊排在一起,修改的資訊排在一起這樣

簡單講就是直接用 rebase -i 然後把順序調一調就好了,

不過 rebase -i 呈現的順序本來就跟 log 相反,這點要注意一下別弄返了:

# 把 rebase 設定在 d7de1ff
$ git rebase -i d7de1ff

pick d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
pick 316ffbc 在 hello.txt 加入第3行
pick 13a95ad 刪除 ooqq.txt
pick 9a73d0c 新增 qqoo.txt
pick 0338589 在 log.txt 加了第三行
pick aee4e62 複製一個 test.py 蓋掉原本的同名檔案

# 其實就直接把順序搬一搬變這樣

pick d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
pick 13a95ad 刪除 ooqq.txt
pick 316ffbc 在 hello.txt 加入第3行
pick 0338589 在 log.txt 加了第三行
pick 9a73d0c 新增 qqoo.txt
pick aee4e62 複製一個 test.py 蓋掉原本的同名檔案

# 存檔離開就會自動繼續完成 rebase 作業了
 Successfully rebased and updated refs/heads/master.

# 查看一下紀錄
$ git log --oneline -7
 9a7c325 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 610ad12 新增 qqoo.txt
 1b697dc 在 log.txt 加了第三行
 181dbcc 在 hello.txt 加入第3行
 5bbbe37 刪除 ooqq.txt
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
 d7de1ff 我也加了生活班232小時課表--0610.doc

這邊要提醒一下,順序也不是想怎麼改就怎麼改,記得要注意合理性,

例如還沒新增 ooqq.txt 就不可能去修改這個檔案,檔案都不存在要怎麼修改?

放前面的就是先執行的,放後面的就是後執行的動作,在修改順序時要注意一下。

又如原本先修改某個檔案的 1-3 行,後來又改了 4-6 行,

當我們把這兩個順序對調以後,使用 reset 回到 修改 1-3 行的 commit 後,

因為修改 4-6 行這筆已經被我們挪到修改 1-3 行的前面了,

所以目前處在修改 1-3 行的 commit 時,此時這個檔案的 4-6 行是已經被修改的狀況,

如果沒有調動順序,reset 回來後,後面的 commit 都會暫時消失,所以 4-6 行是還沒修改的狀況,

希望你們可以聽得懂我在說什麼,要注意一下這些小細節就是了。

接著我們刪除 commit

有兩個方法,一是在 rebase -i 中把 pick 改成 drop

二是直接把那行 pick 的 commit 刪掉

# 列出前 5 筆記錄
$ git log --oneline -5
 9a7c325 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 610ad12 新增 qqoo.txt
 1b697dc 在 log.txt 加了第三行
 181dbcc 在 hello.txt 加入第3行
 5bbbe37 刪除 ooqq.txt

# 想把 `1b697dc` 與 `181dbcc` 刪掉
# 先把 rebase 設在 5bbbe37 上
$ git rebase -i 5bbbe37

pick 181dbcc 在 hello.txt 加入第3行
pick 1b697dc 在 log.txt 加了第三行
pick 610ad12 新增 qqoo.txt
pick 9a7c325 複製一個 test.py 蓋掉原本的同名檔案

# 將 pick 改成 drop
# 也可以直接用編輯器把那兩行刪掉

drop 181dbcc 在 hello.txt 加入第3行
drop 1b697dc 在 log.txt 加了第三行
pick 610ad12 新增 qqoo.txt
pick 9a7c325 複製一個 test.py 蓋掉原本的同名檔案

# 存檔離開
 Successfully rebased and updated refs/heads/master.

$ git log --oneline -5
 762ece1 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 580aaf0 新增 qqoo.txt
 5bbbe37 刪除 ooqq.txt
 d78c4ff 刪掉生活班232小時課表--0610.doc 與 abc.txt
 d7de1ff 我也加了生活班232小時課表--0610.doc

成功刪除了那兩個 commit 了。

提醒一下,刪除 commit 的意思就是該 commit 所做的動作都不算,所以跟上面的提醒一下要注意相依性就是合理性的問題。

例如你原本新增一個檔案,後來有在修改,結果你把新增該檔案的 commit 刪了,

那肯定會出問題。

另外,還有個指令可以刪除 commit 就是:

git revert HEAD --no-edit

--no-edit 就是不寫 commit 了,但這個指令會自己再新增一筆記錄,該筆記錄是刪除上一個 commit

就像是 merge 會產生 commit 一樣。

感覺上沒有像 reset 那麼強硬,revert 會新增一個 commit 紀錄,感覺上整份紀錄看起來會比較完整,

我自己的習慣是非必要不會去刪紀錄或資料,全部用新增的方式雖然會比較漸接,

但也保留了所有動作的紀錄,對我來說所有的資料都是無價的,不會隨便去刪他,

特別是與其他伙伴一起協做的話,直接去刪別人的紀錄總覺得不太禮貌。

刪除帳密

這篇要討論的是如果我們對於已經提交的資訊感到後悔,例如帳號密碼,那麼我們有幾種處理方式,

但在處理之前,最安全的就是先把實際的帳密改掉,這樣被看到了也無所謂。

刪除 git

這是很爛的方法但他是個有效的方式,整個 .git 資料夾都刪了就沒紀錄了。

然後看看現行是要把帳密的檔案刪除或搬走。

不過之前的那些修改紀錄與版本就都沒了。

重新提交

去修改或刪除帳密檔案,然後提交,不過有個缺點,

就是可以利用 git 回到之前有帳密的版本。

像是 reset 等這些退版指令。

忽略提交

可以利用之前學過的 .gitignore 讓 git 忽略該帳密檔,

如果是已經被追蹤的檔案記得要下 git rm filename --cached 取消追蹤才行。

修改紀錄

查出是哪個提交有帳密檔,使用之前所學的 rebase -i 然後把 commit 拆下來,

看是要刪檔案還是修改內容等再重新提交上去。

filter-branch

其實 git 提供一個 filter-branch 指令可以來處理這件事,

$ git filter-branch --tree-filter "rm -f mypassword.txt"
 Rewrite db3bbec63301d1c638e828c9a38a29314c8a0c44 (9/10) (1 seconds passed, remaining 0 predicted)
 Ref 'refs/heads/master' was rewritten

這個指令的好處有:

  1. 你不需要知道是從哪個提交開始上傳了這個密碼檔,他會找到最早有他的地方將他刪除並把後面的 commit 全部重做
  2. 因為 --tree-filter 所以當你 checkout 到分支執行這個指令就會幫我們自動搞定有這個檔的 commit

因為後面相關 commit 都會重做,所以如果是很早之前就有的,那就會看到一堆 rewrite 且會跑一陣子。

這個指令是針對這個 branch 所以如果每個 branch 都要刪這個檔案,那就要自己 checkout 到分支去執行這個指令。

如果後悔了就下指令退版回去:

$ git reset refs/original/refs/heads/master --hard

這是因為我們在執行 filter-branch 時 git 會先幫我們紀錄還沒執行的版本。

不然就是找出想回去的 sha1 出來退版回去也行。

刪乾淨

因為剛剛還可以回得去就表示其實並沒有真的刪乾淨,如果真的想完全從 git 把這個檔案刪乾淨,

就要加上 -f 參數

$ git filter-branch -f --tree-filter "rm -f mypassword.txt"

事情還沒完喔,因為可以用 reset 回去,所以也要處理一下 refs/original/refs/heads/master

$ rm .git/refs/original/refs/heads/master

還有之前提到的 reflog 他會記錄 30 天內的提交資訊,所以我們要讓他利刻過期:

$ git reflog expire --all --expire=now

檢查一下會看到很多 unreachable 物件:

$ git fsck --unreachable

那就啟動 git 回收機制把這些垃圾清掉:

$ git gc --prune=now

應該沒有垃圾了:

$ git fsck
 Checking object directories: 100% (256/256), done.
 Checking objects: 100% (14/14), done.

好了,這下真的回不去了,要徹底刪除一個檔案還真不容易,因為這一切都是 git 為我們著想的保護機制。


來源文章


最後更新:2020-04-29 23:27:43

From: 122.116.71.150

By: 阿慶