# Git - 進階篇 ## 使用分支(branch) 在增加新功能、修正 bug,或是想嘗試一些新做法時,都可以另外開一個分支來進行,等開發完確認沒問題之後再合併回來,就不會影響正在運行的功能。簡而言之,branch 的作用就是讓開發過程各自獨立。 ![branch](https://imgur.com/wQmzfvH.png) ### 分支的本質 在 Git 裡⾯的分⽀,其實就跟**⼀張貼紙**⼀樣,它會貼在某個 Commit 上: ![branch](https://imgur.com/eAJMurC.png) 當完成了一個新的 Commit,新的 Commit 會指向它的前一個 Commit,而目前的分支,會貼到剛剛完成的新的 Commit 上。也就是說分支會隨著每次 Commit 的新增一起移動: ![branch on new commit](https://imgur.com/Edjec69.png) 在 Git 的分⽀並不是複製⽬錄或檔案來進⾏開發與修改,**分⽀就只是⼀個指標、⼀張貼紙,貼在某個 Commit 上⾯⽽已**。 ### 查看專案分支 ```bash $ git branch * master ``` ### 建立分支 開發新功能之前,養成建立新分支的好習慣。 ```bash $ git branch develop $ git branch develop * master ``` ### 切換分支 ```bash $ git checkout develop Switched to branch 'develop' $ git branch * develop master ``` #### 在切換分⽀的時候發⽣了什麼事? 1. 更新暫存區以及⼯作⽬錄 Git 在切換分⽀的時候,會⽤該分⽀指向的那個 Commit 的內容來「更新」暫存區以及⼯作⽬錄。但是**在切換分⽀之前所做的修改則還是會留在⼯作⽬錄**,也就是說換切分⽀並**不會影響已經在⼯作⽬錄的那些修改**。 2. 變更 HEAD 的位置 除了更新暫存區以及⼯作⽬錄的內容外,同時 HEAD 也會指向剛剛切換過去的那個分⽀,也就是說 `.git/HEAD` 這個檔案會跟著被修改。 ### 修改分支名稱 分支改名**不會影響到檔案或目錄**。 ```bash $ git branch -m <old branch-name> <new branch-name> ``` ### 刪除分支 ```bash $ git branch feature $ git branch * develop feature master $ git branch -d feature Deleted branch feature (was babb78d). ``` 如果要刪除的分支還沒被完全合併,Git 會有小提示: ```bash $ git branch -d feature error: The branch 'feature' is not fully merged. If you are sure you want to delete it, run 'git branch -D cat'. ``` 如果刪除了**尚未被合併完成**的分支,但事後反悔了想將檔案救回來,還是有方法可行的。 ![delete unmerged branch](https://imgur.com/BOWWGhq.png) 從上圖可以看到,即便刪除了分支,但在該分支上原先的 Commit 依然保存著,那是因為**分支只是一個指向某個 Commit 的指標,刪除這個指標並不會造成那些 Commit 消失**。既然 Commit 沒有消失,就意味著還是可以重新把它找回來: ```bash $ git branch return_dev b174a5 ``` 這個指令的意思是「建立一個名為 `return_dev` 的分支,讓它指向 `b174a5a` 這個 Commit」,簡單來說就是再去拿一張新的貼紙貼回去的意思。 :::warning **補充** 如果沒有把被刪除的分支的 SHA-1 值記下來,該怎麼復原? 可以使用 **`git reflog`** 指令去查找。當 **`HEAD`** 有移動的時候(例如:切換分支或是 reset,都會造成 **`HEAD`** 移動),Git 就會在 Reflog 裡記上一筆。 ::: ### 合併分支 當在 `develop` 分支開發完新功能,要將功能上線到穩定系統,也就是把 `develop` 分支合併到主要分支 `master`,需要以下步驟: 0. 使用 `git log` 指令查詢歷史紀錄: ```bash $ git log --oneline 87e0bb8 (HEAD -> develop) <create> product management feature a3664cb <create> product page 6b9bf18 (master) <create> main feature dbbcdd9 <create> home style 4c6862a <create> home page ``` 1. 切回主要分支 `master` ``` bash $ git checkout master Switched to branch 'master' ``` 2. 輸入要合併的分支名稱 ```bash $ git merge develop Updating 6b9bf18..87e0bb8 Fast-forward product.html | 0 product.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 product.html create mode 100644 product.js ``` 在 `develop` 分支新增的 `product.html` 與 `product.js` 兩個檔案,因為主要分支 `master` 現在已經合併了 `develop` 分支,所以現在在主要分支 `master` 上也有這兩個檔案了。 :::danger **注意** 所謂的 Merge 並不是指分支與分支之間的合併,而是去 Merge 「**該分支所指向的那些 Commit**」。 ::: #### 快轉模式(Fast Forward) 快轉模式是指當主要分支 `master` 合併 `feature` 分支時,`master` 當前的節點一直和 `feature` 的根節點相同,沒有發生改變,此時就會採取「快轉模式」,其實就是把 `master` 這張貼紙撕起來,往前貼到 `feature` 分支所指的那個 Commit 而已。 ![fast forward](https://imgur.com/gh2tuar.gif) #### 非快轉模式(No Fast Forward) 當主要分支 `master` 當前的節點與 `feature` 分支的根節點不同時,此時合併分支並不會觸發快轉模式,會額外產出一個 Commit。 ![no fast forward](https://imgur.com/kiAcFZ6.gif) ### 另一種合併方式:rebase rebase 這個英文單字的翻譯是「重新定義分支的**參考基準**」。Rebase 合併分支跟 Merge 合併分支最明顯的差異在於,使用 Rebase 合併分支,Git 不會特別產生一個專門用來合併的 Commit。 > A `git rebase` copies the commits from the current branch, and puts these copied commits on top of the specified branch. ![rebasing](https://imgur.com/OXgklx2.gif) #### rebase 參考資料 [另一種合併方式(使用 rebase)](https://gitbook.tw/chapters/branch/merge-with-rebase) ### 合併時發生衝突(Merge Conflict) Git 有能力幫忙檢查簡單的衝突,所以並不是改到同一個檔案就一定會發生衝突,但是改到**同一個檔案的同一行程式碼**就沒辦法了。Git 無法幫你選擇哪一個當作最終版本,所以當發生衝突時,只能「自己手動調整」。 假設現在有一個 `index.html` 檔案,主要分支 `master` 與 `develop` 分支都同時修改了這個檔案的同一行程式碼,如果想要把 `develop` 分支 Merge 到主要分支 `master` 上,會出現以下訊息: ```bash $ git merge develop Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. ``` 使用 `git status` 指令查詢目錄狀態: ```bash $ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: index.html no changes added to commit (use "git add" and/or "git commit -a") ``` > You have unmerged paths. 你有一個未合併的路徑。意思就是**目前的合併尚未完成**。 進入 `index.html` 檔案會顯示衝突發生的位置,需要手動調整檔案內容: ![merge conflict message](https://imgur.com/rSN5i9L.png) Git 會把有衝突的段落標記出來: * 上半部是 `HEAD`,也就是目前所在的主要分支 `master` * 中間是分隔線 * 下半部是被合併的分支,也就是 `develop` 分支 修改完發生衝突的段落後,還是要把檔案重新安置到暫存區,並完成 Commit Message,才算解決 Merge Conflict。 ```bash $ git add index.html $ git commit -m 'conflict fixed' [master d634e31] conflict fixed ``` ![merge conflict](https://imgur.com/UWOIgkf.gif) ## 使用 GitHub 遠端共同協作 Git 是一個分散式版本控制工具,藉由它可以產生一個儲存庫(Repository),裡面存放著被 Git 版本控制的專案。 GitHub 是目前全球最大的 Git Server,許多 open-source 的專案都是使用 Github 進行程式碼的管理,可以把 GitHub 想成「**提供存放 / 使用 Git 專案儲存庫(Repository)的服務**」。 ### 將本地端 Repository 推送到遠端 1. 在 Github 上建立一個 Repository ![create repository](https://imgur.com/zStUuVM.png) 2. 建立好遠端 Repository 後,有兩種方式將本地端 Repository 與 GitHub 的遠端 Repository 連結: * 如果是全新開始的專案,依照「create a new repository on the command line」指示進行 * 如果是要上傳現有專案,依照「push an existing repository from the command line」指示進行 ![remote guide](https://imgur.com/SXpZV0E.png) 3. 以全新開始的專案為例: ```bash $ cd /desktop $ mkdir git-practice $ cd git-practice $ echo '# Git Practice' >> README.md $ git init Initialized empty Git repository in /desktop/git-practice/.git/ $ git add README.md $ git commit -m 'initial commit' [master (root-commit) 542dc7e] initial commit 1 file changed, 1 insertion(+) create mode 100644 README.md ``` 4. 將本地端 Repository 推送到 GitHub ```bash $ git remote add origin https://github.com/avery210412/git-practice.git ``` 意思是「**將本地端 Repository 新增一個名為 `origin` 的遠端 Repository**」。 * `git remote`:主要是跟遠端有關的操作 * `add`:要加入一個遠端的節點 * `origin`:是一個「代名詞」,指的是後面那串 GitHub 伺服器的位置 ```bash $ git push -u origin master Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Writing objects: 100% (3/3), 229 bytes | 229.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To https://github.com/avery210412/git-practice.git * [new branch] master -> master Branch 'master' set up to track remote branch 'master' from 'origin'. ``` `git push` 指令其實做了幾件事: * 把 `master` 這個分支的內容,推向 `origin` 指向的 GitHub 伺服器位置 * 在 `origin` 指向的伺服器上,如果 `master` 分支不存在,就建立一個叫做 `master` 的同名分支。 * 如果伺服器上原本就存在 `master` 分支,便會移動伺服器上 `master` 分支的位置,使它指到目前最新的 Commit 上。 * `-u`:設定 `upstream`,使本地端分支開始追蹤指定的遠端分支,也就是說現在開始,「**本地端的 `master` 分支,會去追蹤遠端 origin 上的 `master` 分支**」 5. 回到瀏覽器上重新整理頁面: ![push repository finished](https://imgur.com/o7Jjyn1.png) 看到這個畫面,表示已經順利把本地端 Repository 的東西,推送到這個遠端 Repository 裡面了。 :::success **狀況題** 如何修改已經 Push 到遠端的 Commit? 假設現在已經完成一個 Commit,並且也推送到遠端 Repository 上,如果此時專案中的檔案做了一些小修改,但是又不想在這個 Commit 以後再新增一個 Commit 記錄此次的修改,只想要修改上一次的 Commit 並且重新 Push 到遠端 Repository,可以這樣操作: 1. 使用 **`--amend`** 參數進行 Commit,來修改上一次的 Commit,此時 SHA-1 值會重新計算 2. 使用 **`git push --force-with-lease origin <branch>`**,將 Commit 推送至遠端 Repository ::: #### `git push` 參考資料 [如何使用 git push 指令只 Push 部份的進度?](https://youtu.be/VShhhq_5sMc) [怎麼有時候推不上去](https://gitbook.tw/chapters/github/fail-to-push) [修改已經 Push 到遠端 Repository 的 Commit](https://tinyurl.com/y6gtw7s8) --- ### 將遠端 Repository 同步至本地端 #### Fetch 當本地端 Repository 完成推送到遠端 Repository 時,目前的分支圖如下: ![git branching diagram](https://imgur.com/TR4NUjH.png) 假使這個專案由多人協作開發,某位開發者 Push 了新的功能,此時遠端 Repository 就會新增一個 Commit: ![push a new feature](https://imgur.com/5Pngthu.png) 此時,若想要把遠端 Repository 的最新版本下載回本地端,但又擔心下載後,會與本地端的 Repository 發生合併衝突(Merge Conflict)的情形,此時可以使用 `git fetch` 指令。 當執行 Fetch 指令時,Git 會看一下遠端 Repository 的最新版本內容,再比較一下此時本地端 Repository 的內容,它會**把遠端有但是本地端沒有的內容**下載到本地端 Repository,但是**不會將這些內容合併到本地端 Repository**,因此分支圖還是會維持上面的情況: ![git fetch](https://imgur.com/5Pngthu.png) 由於 `origin/master` 這個遠端分支,是從本地端 `master` 分支推送到 GitHub 進而產生的,因此 `origin/master` 分支跟 `master` 分支**本是同根生**。如果要把 Fetch 得到的最新版本內容合併到本地端 Repository,會觸發快轉模式(Fast Forward),此時的分支圖如下: ![Fetch and Merge](https://imgur.com/QUQ5aAH.png) ![Fetch](https://imgur.com/tPCaQE5.gif) #### Pull > *git pull = git fetch + git merge* Pull 指令就是將遠端 Repository 的最新版本下載回本地端,並且將遠端分支合併到本地分支,也就是將 `origin/master` 這個遠端分支**直接合併**到本地端 `master` 分支。 ![Pull](https://imgur.com/RtefJ9S.gif) ### 從 GitHub 伺服器上取得 Repository 如果在 GitHub 上看到某個專案很有趣,想要下載到本地端研究,使用 Clone 指令就可以把整個專案複製一份到本地端了。 ```bash $ git clone https://github.com/sparanoid/chinese-copywriting-guidelines.git Cloning into 'chinese-copywriting-guidelines'... remote: Enumerating objects: 907, done. remote: Counting objects: 100% (212/212), done. remote: Compressing objects: 100% (98/98), done. remote: Total 907 (delta 137), reused 181 (delta 113), pack-reused 695 Receiving objects: 100% (907/907), 354.90 KiB | 2.25 MiB/s, done. Resolving deltas: 100% (529/529), done. ``` Clone 指令會把整個專案的內容複製一份到本地端,也就是你的電腦裡,這裡指的「內容」不是只有檔案,而是指**整個專案的歷史紀錄、分支、標籤等**內容都會複製一份下來。 :::danger **注意** 如果這個專案你是第一次看到,想要下載到你的電腦裡,請使用 **Clone** 指令;如果你已經下載過這個專案,只是想要把本地端的內容,更新成最新的線上版本內容,請使用 **Pull**(**Fetch**)指令。 **Clone 指令通常只會在第一次下載時使用,Clone 之後的更新,就是 Pull / Fetch 的事了。** ::: ## 使用 GitHub Flow 參與開源專案 Git 儲存庫並沒有**權限控管**的概念。當越多人參與同一個專案,每個人都有存取這個儲存庫的權限時,可能會遇到一些協作開發上的問題,例如:每個人都可以 Commit 到專案正式上線的分支(例如:`master`),在這種情況下,不同人彼此之間的程式碼會互相干擾。 GitHub 提供了 Fork 與 Pull Request 的機制,賦予儲存庫基本的權限控管。 ### GitHub Flow ![GitHub Flow](https://i.imgur.com/gns2luN.png) GitHub Flow 是一個基於分支(branch)的輕量化工作流程,藉由分支去管理功能的開發,以及來自社群的貢獻。 遵守 GitHub Flow 進行開發有一個規則:**`master` 分支必須保持隨時可以部屬正式環境(Production Ready)的狀態**。 ### GitHub Flow 步驟 #### Fork ![fork repository](https://imgur.com/Oq2TpJc.png) 如果想要參與一個你沒有推送(Push)權限的專案,可以先複製(**Fork**)一份原始專案的**副本**到自己的 GitHub 帳號底下,你對這個副本有全部的權限,之後的任何修改都在這個副本中執行。 所有人都可以 Fork 專案,對 Fork 出來的副本 Push 更新內容,然後去發送 Pull Request,來把這些更新內容貢獻回原始專案裡。 > Fork 在這邊翻譯成「複製」,但並不是這個詞的原意。在技術圈來說,這個詞使用的情境是「原作者做得不夠好,其它人覺得可以做得更好,或是想加入一些個人喜好的功能,而修改出另外的版本」。 #### Branch ![Create Branch](https://i.imgur.com/YxNhiqA.png) 將副本 Clone 到本地端,第一件事情就是**建立新分支**,之後的修改和討論都會**以這個分支為基準**。 在專案建立一個分支,代表建立了一個環境來開發新功能,在分支上所做的修改,都不會影響到 `master` 分支,所以可以自由的嘗試並提交修改。 #### Commits ![Add Commits](https://i.imgur.com/ehEeTyx.png) 在對專案進行功能開發、修改之前,執行 `git remote -v` 查看 Git 的遠端設定: ```bash $ git remote -v origin https://github.com/avery210412/chinese-copywriting-guidelines.git (fetch) origin https://github.com/avery210412/chinese-copywriting-guidelines.git (push) ``` 建議對 Git 的遠端設定進行調整,**將 fetch 的遠端位置,改成原始專案的位置**: ```bash $ git remote set-url origin https://github.com/sparanoid/chinese-copywriting-guidelines.git $ git remote -v origin https://github.com/sparanoid/chinese-copywriting-guidelines.git (fetch) origin https://github.com/sparanoid/chinese-copywriting-guidelines.git (push) $ git remote set-url --push origin https://github.com/avery210412/chinese-copywriting-guidelines.git $ git remote -v origin https://github.com/sparanoid/chinese-copywriting-guidelines.git (fetch) origin https://github.com/avery210412/chinese-copywriting-guidelines.git (push) ``` 這樣設定可以達到以下好處: * 之後會從原始專案中取得最新的專案資料(fetch),保持專案的一致性 * 所有修改的資料只會推送到自己的專案中(push),不會影響到其他人 完成設定後,就可以對專案進行功能開發。 #### Pull Request ![Open Pull Request](https://i.imgur.com/1XdLZa1.png) 功能開發完成後,將本地端的 Repository 推送到遠端的 GitHub Repository。 此時可以在 GitHub 上建立一個 Pull Request,Pull Request 提供了一個方式法來通知專案維護者,是否考慮使用你所做得修改。 這邊特別說明一件事,每個專案可能都有自己的規範,通常會寫在專案的 `README.md` 或是 `CONTRIBUTING.md`,請務必在修改或提交前,閱讀專案的規範,維持開源專案的品質。 #### Discuss And Review ![Discuss And Review](https://i.imgur.com/ser2wXQ.png) 專案維護者在檢視你所貢獻的程式碼後,可以在該 PR 中進行討論,討論程式碼內容或關於修改的建議。過程中如果有需要修改的地方,可以直接在該分支中進行修改,因為 **PR 是看分支的**,所以該分支於 PR 確認合併前,都可以新增 Commit,並納入該 PR 中。 #### Deploy ![Test Deploy](https://imgur.com/ibRwwDN.png) 正式合併到 `master` 分支之前,在這個階段可以先部署到測試環境,以進行合併前的最終測試。如果測試沒有通過或產生任何可以討論的議題,則可以取消合併,在通過一系列的測試後,才可以發佈到正式環境(Production Ready)上。 #### Merge ![Merge](https://i.imgur.com/IYJgbY5.png) 當所貢獻的程式碼經過一連串的檢閱、測試後,具有**原始專案合併權限的人**,就可以將你的貢獻合併到原始專案中了,你也成為原始專案的貢獻者(Contributions)之一了。 #### GitHub Flow 參考資料 [如何使用 GitHub Flow 來參與開源專案](https://blog.poychang.net/guide-to-use-github-flow/) [讓我們來了解 GitHub Flow 吧!](https://reurl.cc/1ZYDqp) ## 總結 ![Git Cheat Sheet](https://i.imgur.com/zzNkt5X.jpg) ### Git 版本控制參考資料 [為你自己學 Git](https://gitbook.tw/) [CS Visualized: Useful Git Commands](https://dev.to/lydiahallie/cs-visualized-useful-git-commands-37p1)