快轉到主要內容

軟體開發 基本的 git 流程操作

簡記在 git 開 feature branch 開發的時候,一些跟 git 相關的流程跟指令

簡單整體流程 #

  1. 從主分支開出自己的 branch
# In main branch
$ git pull origin main
$ git checkout -b feature/ML-1234-add-dropout
  1. 開發/修bug/本地測試, 中間可能會有多個 commits

  2. (視情況) squash commits, 把多個 commit 合併成幾個

$ git rebase -i HEAD~3
# into interaction mode
  1. 更新 main branch
# In main branch
$ git checkout main
$ git pull origin main

# Back to feature branch
$ git checkout -

或者不切到 main 而更新 main branch

# Still in feature branch
$ git fetch origin main:main
  1. Rebase: 把你的 commits 接到 main 之後
$ git rebase main feature/ML-1234-add-dropout
  1. 把你的 feature branch 推到遠端,測試,發 pull request / merge request
$ git push origin feature/ML-1234-add-dropout
  1. 可能一些原因,你要再一次更改本地的 feature branch。重複步驟 2~6。不過如果 main 也更新的話,第 6 步推到遠端可能會有下面錯誤 – 就算是你自己的 feature branch
$ git push
To github.com:myname/test-repo.git
 ! [rejected]        feature/ML-1234-add-dropout -> feature/ML-1234-add-dropout (non-fast-forward)
error: failed to push some refs to 'github.com:nyname/test-repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

因為 rebase main 以後,有可能你本地的 feature 跟遠端的 feature 已經在不同的路上。這時 不要用 –force,請用 --force-with-lease

$ git push --force-with-lease

利用互動式 rebase -i 來合併 commits #

假設你做了下面的 commits

* 3022ae2 (HEAD -> feature/ML-1234-add-dropout) readme -1 +3
* 006fa59 readme +2
* 452872f README +1
* b91f843 (origin/main, origin/HEAD, main) Initial commit

你想要合併最後三個 commits – 因為太瑣碎,知道其歷程也沒用,都是在更新 README, 那可以用 git rebase -i HEAD~<數量> 或是 git rebase -i <根基commit>,表示你要把<數量>個 commit 合併,或是把 <根基commit> 之後的合併(不包括他自己)

指令下了以後,會進入編輯器,你更進一步去編輯互動

pick 452872f README +1
pick 006fa59 readme +2
pick 3022ae2 readme -1 +3

# Rebase b91f843..3022ae2 onto b91f843 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
...
# s, squash <commit> = use commit, but meld into previous commit
...

注意上面的順序,舊的 commit 在上,新的在下。邏輯是除了第一個 commit 維持 pick 以外,其他後來新的 commit 都改成 s 表示合併到前面(最舊的那個)

pick 452872f README +1
s 006fa59 readme +2
s 3022ae2 readme -1 +3

然後儲存,就會有新的編輯視窗跑出來,跟你在 git commit 的時候一樣,你可以放心更改裡面的 commit 訊息

# This is a combination of 3 commits.
# This is the 1st commit message:

README +1

# This is the commit message #2:

readme +2

# This is the commit message #3:

readme -1 +3

儲存後,新的 commit 就會生效,你會看到在根基後面有一個,而且只有一個新的 commit

* fb8befe (HEAD -> feature/ML-1234-add-dropout) Update README
* b91f843 (origin/main, origin/HEAD, main) Initial commit

要不要合併 squash commit #

每個 commit 代表了開發的記錄、心路歷程,就算是沒有推到遠端給別人知道,仍然有其意義(推到遠端的就更不用說了)

所以哪些 commit 要合併哪些不要,就要靠判斷力了。我覺得重點在「意義」,以及「commit 訊息」要表達出這個意義

如果看到十連發 commit 訊息都是 “bug fix”, “bug fix”, 天知道你在 fix 什麼

相反地,如果每個 bug fix 都有其意義 (“fix cache reading due to inconsistency”),而且重要,要讓別人知道,那這歷程也值得留著

最終 squash commit 是為了讓 commit 歷史乾淨做個整理,但不應該失去過多的思考歷程

Feature branch 取名慣例 #

branch name convention 通常由公司或部門決定,主要需兼具「清楚」與「簡要」,例如

  • 是新功能還是修 bug ? feature/fix/
  • 是誰的 (比較不大有意義)
  • ticket (問題單?) 的編號
  • 簡述目的或處理動作

例如 feature/ML-1234-add-dropout 大概猜得出來是 新功能, 要加 dropout, 而且這個問題追蹤在 “ML-1234” 這張 ticket 上

更新 + rebase main branch #

版本控制就是「你有你的工作,我有我的工作,追蹤版本,然後合併成一個可信、真理的來源」

通常 main 是那個真理的 branch,雖然你的 feature branch 是從 main 分岔出去,為了是不要立即影響到 main,但最後還是要合併回去

所以要先更新 main

$ git fetch origin main:main
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 298 bytes | 74.00 KiB/s, done.
From github.com:myname/test-repo
   b91f843..3e4a27a  main       -> main
   b91f843..3e4a27a  main       -> origin/main

$ git l
* 3e4a27a (origin/main, origin/HEAD, main) add my file
| * fb8befe (HEAD -> feature/ML-1234-add-dropout) Update README
|/  
* b91f843 Initial commit

然後把你的內容跟 main 合併:可以 pull merge, 但比較簡潔漂亮的會是用 rebase

git rebase A B 白話的意思是把 B 的內容加在 A 上面

$ git l
* 3e4a27a (origin/main, origin/HEAD, main) add my file
| * fb8befe (HEAD -> feature/ML-1234-add-dropout) Update README
|/  
* b91f843 Initial commit

$ git rebase main feature/ML-1234-add-dropout
Successfully rebased and updated refs/heads/feature/ML-1234-add-dropout.

$ git l
* ac00862 (HEAD -> feature/ML-1234-add-dropout) Update README
* 3e4a27a (origin/main, origin/HEAD, main) add my file
* b91f843 Initial commit

git pull push 不想打 branch 名字 #

如果每次都要打 git push origin feature/ML-1234-add-dropout 很煩,可以做這一次性的指令,表示你本地的分支是對應遠端的某個分支

# In local feature branch
$ git branch -u origin/feature/ML-1234-add-dropout

-u--set-upstream-to= 的意思

git push –force-with-lease #

Git push 把你更改的內容推給其他人(更精確講,這邊的「其他人」是一個 remote 的 branch. remote 通常是 origin 代表大家可相信的來源),但其他人也有可能更改同一份 (branch) 的內容。你的內容可能會蓋過他們的或者有衝突。

Git push 會看那個遠端的內容跟你宣稱的內容是否在同一條歷史線上(remote ref is an ancestor of the local ref),代表沒人跟你搶,就可以把你的內容「快轉」上去

如果你要推過去的遠端 branch 已經有其他改變,歷史線分岔,一種方式是 pull merge 合併遠端別人的改變到自己的,然後再 push 上去。

然而文章前面的例子是,遠端跟本地 feature branch 都是你的,如果只有你在改變內容(也就是遠端的內容跟你記憶中的一樣),只是因為 rebase main (以別人的內容為根基加上去) 導致分岔,就可以用 --force-with-lease

# 本來本地跟遠端的 ML-1234-add-dropout 在同一條線上

$ git l
* 5412270 (HEAD -> feature/ML-1234-add-dropout) readme +4
* ac00862 (origin/feature/ML-1234-add-dropout) Update README
| * d9c4b49 (origin/main, origin/HEAD, main) Add more content in sl.txt
|/  
* 3e4a27a add my file
* b91f843 Initial commit

$ git rebase main feature/ML-1234-add-dropout
Successfully rebased and updated refs/heads/feature/ML-1234-add-dropout.

# 本地 rebase main 以後反而分岔了 -- 因為 main 也走出自己的路

$ git l
* 5c4d18a (HEAD -> feature/ML-1234-add-dropout) readme +4
* 755b237 Update README
* d9c4b49 (origin/main, origin/HEAD, main) Add more content in sl.txt
| * ac00862 (origin/feature/ML-1234-add-dropout) Update README
|/  
* 3e4a27a add my file
* b91f843 Initial commit

# 直接 push 會出錯, 因為遠端的 ac00862 跟本地的 5c4d18a 已經在不同的路上

$ git push
To github.com:myname/test-repo.git
 ! [rejected]        feature/ML-1234-add-dropout -> feature/ML-1234-add-dropout (non-fast-forward)
error: failed to push some refs to 'github.com:myname/test-repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

# 為了 commit 歷史簡潔,其實可以不用 merge, 而用 push --force-with-lease

$ git push --force-with-lease
...
...
To github.com:myname/test-repo.git
 + ac00862...5c4d18a feature/ML-1234-add-dropout -> feature/ML-1234-add-dropout (forced update)

$ git l
* 5c4d18a (HEAD -> feature/ML-1234-add-dropout, origin/feature/ML-1234-add-dropout) readme +4
* 755b237 Update README
* d9c4b49 (origin/main, origin/HEAD, main) Add more content in sl.txt
* 3e4a27a add my file
* b91f843 Initial commit

# 舊的遠端 feature branch 本來指向 ac00862, 現在就更新成跟本地一樣的 5c4d18a

當然,如果是小團體而且用 Trunk based development 就不用這麼多奇怪的東西 – 那是一個完全不同的心態。不過就算是 trunk based 開發,還是可能會有「短命 feature branch」的情況。總之這邊記錄一下 feature branch 開發 在 git 操作上的流程步驟

若您覺得有趣, 請 追蹤我的Facebook 或  Linkedin, 讓你獲得更多資訊!