Git 版本控制教學 - 多人合作開發

Posted on  Oct 23, 2022  in  Git 版本控制  by  Amo Chen  ‐ 10 min read

經典的多人合作開發模式,是由 Git Server 提供中央的儲存庫(repository)儲存主線(master/main),由其他團隊成員複製(clone)到各自的開發環境,接著從主線分出分支(branch),在各自的環境內開發完成之後,最後將分支合併(merge)回主線。

軟體開發,就是如此重複循環作業。

建立遠端(remote) repository

Git 目前支援 SSH, HTTP 兩種協定進行網路協作。

以 SSH 協定複製 Git Server 上專案的指令為 git clone <user@remoteserver:project> ,例如:

$ git clone [email protected]:project.git

除了自己架設 Git Server 之外,其實也可以選擇更方便的 GitHub, GitLab, BitBucket 等線上服務。

為求方便,以下範例以 GitHub 進行。

首先,試著在 GitHub 建立 1 個 repository 。

在 GitHub 建立 1 個 repository 之後,請跟著 GitHub 提供的指令設定使用 GitHub 作為 Git Server, 就可以將程式碼傳到 GitHub 儲存。

此處,我們選擇建立新的建立新的 repository, 因此跟著輸入以下指令:

$ git init gittest
$ cd gittest
$ echo "# gittest" >> README.md
$ git add README.md
$ git commit -m "first commit"
$ git branch -M main
$ git remote add origin https://github.com/<GITHUB_ACCOUNT>/gittest.git
$ git push -u origin main

上述指令執行完畢之後,重新整理一下 GitHub 上 repository 的畫面,就能夠發現 README.md 已經被傳到 GitHub 儲存起來了:

接著,使用 git remote -vgit remote show origin 察看完整的遠端儲存庫資訊,例如:

$ git remote -v
origin	https://github.com/<GITHUB_ACCOUNT>/gittest.git (fetch)
origin	https://github.com/<GITHUB_ACCOUNT>/gittest.git (push)
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/<GITHUB_ACCOUNT>/gittest.git
  Push  URL: https://github.com/<GITHUB_ACCOUNT>/gittest.git
  HEAD branch: main
  Remote branch:
    main tracked
  Local branch configured for 'git pull':
    main merges with remote main
  Local ref configured for 'git push':
    main pushes to main (up to date)

上述的結果中的 fetch, push 分別代表 下載新版本的位置推送新版本的位置

若要新增遠端儲存庫,可使用 git remote add <shortname> <url> ,例如:

$ git remote add secondary https://github.com/<GITHUB_ACCOUNT>/gittest2.git
$ git remote -v
origin	https://github.com/<GITHUB_ACCOUNT>/gittest.git (fetch)
origin	https://github.com/<GITHUB_ACCOUNT>/gittest.git (push)
secondary	https://github.com/<GITHUB_ACCOUNT>/gittest2.git (fetch)
secondary	https://github.com/<GITHUB_ACCOUNT>/gittest2.git (push)

上述結果顯示,我們有 originsecondary 2 個位置可以推送或下載新版本。不過一般來說,遠端儲存庫的設定都十分單純,多數僅有 1 個預設的 origin

若是需要重新命名遠端儲存庫名稱,可使用 git remote rename <branch> <new_branch>

$ git remote rename secondary backup

Clone repository

建立好 repository 之後,團隊的成員就能夠使用 git clone 指令,將程式碼下載到各自的開發環境之中,大家都使用同樣的 repository 進行互動與開發:

$ git clone https://github.com/<GITHUB_ACCOUNT>/gittest.git

GitHub 上 URL 的取得方式如下圖:

除了 HTTPS 之外,也可以透過 SSH 進行 clone:

$ git clone [email protected]:<GITHUB_ACCOUNT>/gittest.git

切換至新分支

一般 clone 遠端 repository 回來之後,會以 git checkout -b <branch> 建立新的分支名稱,在該分支中進行開發,避免改壞原本的 mastermain 主線,新的分支通常會以 issue 編號或是新功能名稱命名,例如:

$ git checkout -b "feature-123/edit-user-info"

建立新的分支之後,可以使用 git branch 查看目前分支,米字號(*)的項目代表目前所在分支。 :

$ git branch
* feature-123/edit-user-info
  main

如果要切換分支可以使用 git checkout <branch> 進行,例如:

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

順帶一提,變更本地(local)分支名稱的指令為 git branch -m <oldname> <newname> ,例如:

$ git branch -m feature-123/edit-user-info feature-456/edit-user-info

p.s. 為免混淆,接下來 GitHub 上的 repository 通稱為遠端(remote) repository, GitHub 上的分支通稱為遠端(remote)分支

如果你已經在想重新命名的分支上時,還可以簡化成 git branch -m <newname> ,例如:

$ git checkout -b new-branch
Switched to a new branch 'new-branch'
$ git branch -m my-new-branch

刪除分支則是 git branch -d <branch> ,不過必須得先切換至其他分支,才能刪除,否則會出現類似以下的錯誤:

$ git branch -d my-new-branch
error: Cannot delete branch 'my-new-branch' checked out at ...

所以一般都會切換至 main 分支,再刪除其他分支:

$ git checkout main; git branch -d my-new-branch
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Deleted branch my-new-branch (was be73d43).

建立完分支之後,其實後續的作業模式就如同單人開發模式,唯一不同的是會在提交變更之後需要推送回遠端儲存庫,推送的指令為 git push <remote_branch_shortname> [local_branch] ,例如:

$ git push origin main

上述的指令為將本地的 main 分支推送(push)回 origin 的遠端儲存庫。

如果是初次使用 push 的人,可以額外使用以下指令查詢自己使用的 push.default 模式:

$ git config push.default
current

上述指令結果顯示 push.default=current ,該設定代表當我們只輸入 git push 時, Git 只會將我們當前所在的分支推送回遠端儲存庫,如果遠端儲存庫沒有相對應的分支存在,就會自動建立一個分支。 Git 2.0 之後, push.default 預設是 simple , 該模式會比較本地與遠端的分支名稱,如果遠端沒有與本地相同的分支,就會拒絕推送,出現類似以下的錯誤訊息:

fatal: The current branch feature-123/edit-user-info has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature-123/edit-user-info

在 simple 模式下,如要解決無法推送的問題,只要按照提示 git push 時加上 --set-upstream 參數設定即可。

Pull Request / Merge Request

一般在分支開發完成之後,並不會馬上與主線(master/main)合併。

而是會發出 Pull Request / Merge Request(簡稱 PR, MR) ,先請團隊成員協助進行程式碼審查(code review),審查都沒問題之後才會進到主線。

例如以下指令建立一個新的分支 feature/new-file, 並切換至該分支進行開發,最後推送到遠端儲存庫。

$ git checkout -b feature/new-file
$ # 開發再開發
$ git commit -m "feature/new-file"
$ git push origin feature/new-file

推送指令執行之後, GitHub 會出現下列訊息,其中一段訊息顯示,可以拜訪一段 URL https://github.com/<GITHUB_ACCOUNT>/gittest/pull/new/feature/new-file 以建立一個 pull request:

Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 290 bytes | 290.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'feature/new-file' on GitHub by visiting:
remote:      https://github.com/<GITHUB_ACCOUNT>/gittest/pull/new/feature/new-file
remote:
To github.com:GITHUB_ACCOUNT/gittest.git
 * [new branch]      feature/new-file -> feature/new-file

進入該網址之後畫面如下,只要填好 1. PR 相關資訊, 2. 指定 reviewers, 3. 按下 Create 按鈕即可建立一個 pull request:

接下來,就是按照 reviewers 的建議進行修改,待所有人都沒有問題之後,就可以將分支合併回主線。

合併回主線的指令:

$ git checkout main
$ git merge feature/new-file
$ git push origin main

或者也可以直接在 GitHub 上直接按下 Merge pull request 按鈕:

至此,你應該已經學會最基本的合作開發模式——切分支、開 PR、併回主線。

git pull 下載並合併版本

多人合作開發的情況下,推送的過程並不會毫無問題,十分有可能在你推送新版本回遠端儲存庫時,其他團隊成員就已經推送一版新的版本,導致你本地的版本並非最新的版本或是有版本衝突的問題。

在此假設成員A已經在一天前推送一版新的程式,並未通知你要進行版本更新,此時若你要進行推送新的版本,就有可能會得到類似以下的錯誤訊息。

$ git push origin main

錯誤訊息:

To https://github.com/GITHUB_ACCOUNT/gittest
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/GITHUB_ACCOUNT/gittest'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

上述的訊息明確地告知,現在的使用的版本並非是最新的,並且建議你使用 git pull 指令更新並合併最新的版本,例如將遠端儲存庫的 main 分支拉下來合併:

$ git pull origin main

但是我們並不建議使用 git pull 直接進行合併,在合併之前,可以用以下指令,了解哪些部份被更動了,多一些關注總是好的!

$ git diff origin/main...HEAD

p.s. HEAD 在 git 中是個專用關鍵字,代表當前分支的最新版本

上述指令代表將本地當前分支的最新版本,與 origin/main 遠端儲存庫 origin 的 main 分支進行比較,該指令會列出 2 者的差異部分,例如:

diff --git a/README.md b/README.md
index 249f346..2b4ed47 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,4 @@

 ## main

-## main
+#### main

上述顯示本地的與遠端的 README.md 有差異存在。

目前為止,不希望大家直接使用 git pull 的原因在於—— git pull 會直接從遠端儲存庫下載更新,並且直接合併到到當前的分支,在不了解變動的範圍與情況下,就很有可能搞砸你原本能夠正常運作的程式。

p.s. 如果是遠端 main/master 分支,也是可以直接 git pull 合併,不過最好是確保 main/master 分支是最穩定版本的情況下

另外,如果想把遠端的分支先暫存到本地另一個分支,先查看後再合併,也可以用下列指令更新版本:

$ git fetch origin main:tmp-main
$ git diff tmp-main
$ git merge tmp-main

上述的第 1 個指令為下載遠端儲存庫(origin)的 main 分支到本地的 tmp-main;第 2 個指令為比較與 tmp-main 分支的差異;第 3 個為合併 tmp-main 分支。

使用 git fetch 的不同點就是 git fetch 只抓取更新,並不會直接進行合併(merge)。

此外,上述 3 個指令也等同於以下 3 個指令:

$ git fetch origin main
$ git diff origin/main main
$ git merge origin/main

p.s. origin/main 是指遠端儲存庫的 main 主線

如果要看看本地分支與遠端分支之間的 log 差異,可以使用以下指令,若加上 -p 則可以分頁顯示:

$ git log origin/main...main

解決版本衝突(conflicts)

最後,在進行合併時,有可能會遇到衝突的情況,通常由於檔案的同一行內容不同所造成,這個情況就稱為 conflict 。由於 Git 無法為你決定哪些內容要保留,因此就會提示要你自行解決 conflict 的情況,例如刪去不要的內容,保留新版正確的部份,或者合併新舊版,這過程稱為解 conflict

例如,以下錯誤訊息是執行 git pull 時遇到 conflict 的情況:

$ git pull origin main
From https://github.com/<GITHUB_ACCOUNT>/gittest
 * branch            main       -> FETCH_HEAD
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

上述的訊息告訴我們 README.md 檔案存在衝突(conflict),因此我們必須修正衝突後再提交變更。

此處需要使用編輯器打開 README.md (或有衝突存在的檔案),打開該檔案之後,可能看到類似以下的畫面:

# gittest

## main

<<<<<<< HEAD
#### main
=======
### main
>>>>>>> f50b44fbf8f23fb7981b89566f5ba2dd3e538f24

只要是有衝突存在的區域,都會以上述的格式表示。 <<<<<<<<<< HEAD========== 就是我們所在的分支的檔案內容;而 ==========>>>>>>>>>> 則是要合併的分支內容。

修正的方法為保留需要的部份,或視需求合併兩者(所以通常會需要另一位協作者協助解決 conflicts),待修正完成並經過測試後,如果沒問題的話就提交(commit)變更,最後可以進行推送(push)。

例如我們選擇保留我們的版本,刪去遠端的內容:

# gittest

## main

#### main

接著提交變更:

$ git commit -a "resolve conflicts"

最後推送:

$ git push

用 git tag 上版本號

推送版本的過程中,可能也會有需要添加版本號的需求,可以為專案上標籤(tag)。

指令範例如下:

$ git tag -a v0.1 -m 'my version 0.1'
$ git tag
v0.1

上述第 1 個指令為新增 v0.1 的標籤,並附上 my version 0.1 做為說明。第 2 個指令為顯示現有的標籤。

下 tag 的當下, tag 會自動與當前 HEAD 的 commit id 綁定,代表這個 tag 鎖在這個版本,想知道 tag 詳細資訊可以使用 git show <tag 名稱> ,例如:

$ git show v0.1
tag v0.1
Tagger: Amo Chen < ... >
Date:   Sun Oct 23 16:36:10 2022 +0800

my version 0.1

commit da37f2b42125d380d1f7ffce980c8d2d95d9fb3e (HEAD -> main, tag: v0.1, origin/main, origin/HEAD)
Merge: 472e358 f50b44f
Author: Amo Chen < ... >
Date:   Sun Oct 23 16:18:20 2022 +0800

    resolve conflicts

上述結果顯示版本 v0.1 的 commit id 是 da37f2b42125d380d1f7ffce980c8d2d95d9fb3e

但是,推送版本時預設並不會一併推送標籤資訊,若要推送標籤則只要在 git push 加上 --tags 參數即可:

$ git push origin --tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 166 bytes | 166.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To https://github.com/<GITHUB_ACCOUNT>/gittest
 * [new tag]         v0.1 -> v0.1

推送到 GitHub 的話,標籤會出現在以下位置:

p.s. 實務上,通常會在推送完 tag 之後,自動觸發一系列的自動化測試、部署,不過,那又是另外的故事了⋯⋯

如果要刪除 tag 則輸入 git tag -d <tag 名稱>,例如:

$ git tag -d v0.1

如果要刪除不小心推送到遠端儲存庫的 tag, 則可以輸入 git push --delete origin <tag 名稱> ,例如:

$ git push --delete origin v0.1
To https://github.com/<GITHUB_ACCOUNT>/gittest
 - [deleted]         v0.1

刪除 Git 日誌中的資料

開發程式、系統時,難免會有利用設定檔管理的方式,此時設定檔中不免會存放帳號密碼等隱密資訊,如果不小心將這個設定檔加到版本控制中,即使將設定檔刪除,也會存在日誌檔中。此時最好的辦法就是將日誌中的相關紀錄也一併刪除,刪除的方法可以參考以下方法(git filter-repo , bfg ):

$ git filter-repo --invert-paths --path PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA

或者使用 bfg 指令:

$ bfg --delete-file YOUR-FILE-WITH-SENSITIVE-DATA
$ bfg --replace-text passwords.txt

詳情可以參照 GitHub Remove sensitive data

記得進行日誌資料刪除前,請先做好備份,以確保意外發生仍有機會復原。

Clone 遠端 repository 的分支

還有一種情況,我們想直接 clone 遠端 repository 上的分支在本地端的分支上,可以使用以下指令:

$ git checkout --track -b my-local-branch origin/<remote_branch>

如此一來,本地端就會新增一個與遠端分支一模一樣的新分支。

設定 SSH 金鑰認證

由於透過 SSH 協定進行合作開發需要團隊成員在 Git Server上有一組帳號,如此一來每一位成員在複製、推送或更新程式版本時都需要輸入帳號密碼,以 GitHub 來說預設就是使用 git 作為 SSH 預設帳號。

若設定 SSH 金鑰認證,就不需要一直輸入帳號密碼,也可加強 SSH 帳號的安全性。

以下為 SSH 金鑰認證的設定過程,分為 SSH Client 端SSH Serve r端

SSH Client 端要做的事

產生一對金鑰(公鑰與私鑰):

$ ssh-keygen -t rsa
$ ssh-keygen -t dsa

上述 2 種指令皆可。2 種指令會分別產生 (id_rsa, id_rsa.pub) 與 (id_dsa, id_dsa.pub) 的公私鑰檔案。

p.s. ssh-keygen 執行過程會詢問 Enter passphrase (empty for no passphrase) ,此處直接按 Enter 跳過即可,如果想每次存取私鑰都得輸入另一組密碼,則在此時需輸入 passphrase

把公鑰傳送到 SSH Server (以 .pub 為副檔名的檔案為公鑰),此處以 GitHub 為例,將 .pub 檔案的內容貼到 GitHub repo 內的 設定 即可:

最後,於家目錄下的 .ssh 資料夾內新增 SSH Config,設定 SSH 以金鑰認證:

$ touch ~/.ssh/config

添加並修改以下內容至 SSH Config 檔內 :

$ vim ~/.ssh/config
Host RemoteServer
	HostName 10.2.0.15
	Port 3333
	User git
	IdentityFile ~/.ssh/id_rsa

上述設定代表設定一組名稱為 RemoteServer 的 SSH Host 其位址在 10.2.0.15 的地方,通訊埠則為 3333 , 預設使用的使用者名稱為 git ,使用 ~/.ssh/id_rsa 私鑰進行認證。

如果是使用 GitHub 可以用以下設定:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa

SSH Server 端

如果你使用的是 GitHub 做為 Git Server, 則可以略過 server 端的設定步驟,因為 GitHub 都已經幫我們處理好了。只要輸入以下指令確定與 GitHub 連線順利即可:

$ ssh [email protected]
Hi ...! You've successfully authenticated, but GitHub does not provide shell access.
                                                                                               Shared connection to github.com closed.

如是自己架 Git Server, 就需要請該伺服器的 admin 將 client 端的公鑰寫到 ~git/.ssh/authorized_keys(此處假設開發團隊使用 git 做為 Git Server 的預設帳號,如果不是使用 git ,則須將 ~git/.ssh/authorized_keys 中的 ~git 換為該帳號:

$ cat id_rsa.pub >> ~git/.ssh/authorized_keys

並確認 /etc/ssh/sshd_config 有開啟以下選項,最後重新啟動 SSH Server :

RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile %h/.ssh/authorized_keys

測試 Client 端與 Server 端能否以金鑰認證

測試 SSH 能不能免輸入密碼即可登入 :

$ ssh RemoteServer

為 Git 新增遠端儲存庫,並測試是否可以免輸入密碼就推送 :

$ git remote add origin RemoteServer:path/to/repository.git
$ git push origin

總結

本篇僅介紹幾個必須懂的指令與概念,實際上仍有些指令可能會因為各團隊開發流程不同而需要額外學習。不過看完此文,大家應該都會有基本的素養能夠 clone, commit 以及 push 程式碼到遠端儲存庫。

以上, Happy Coding!

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

追蹤新知

看完這篇文章了嗎?還意猶未盡的話,追蹤粉絲專頁吧!

我們每天至少分享 1 篇文章/新聞或者實用的軟體/工具,讓你輕鬆增廣見聞提升專業能力!如果你喜歡我們的文章,或是想了解更多特定主題的教學,歡迎到我們的粉絲專頁按讚、留言讓我們知道。你的鼓勵,是我們的原力!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!