此篇文章將就一名新進工程師的日常,試圖將 Git 常見的操作情境依序串接起來,期望可以在情境中直覺地學習使用 clone, commit, push, pull, rebase, stash, branch, merge 等指令功能,協助讀者掌握 Git 基本的版控操作技巧。
前言
筆者在工作上習慣使用 Windows 作業系統,因此多數使用 TortoiseGit 作為 Git 圖形化操作介面,而使用這類工具的好處是可以提示使用者常用的操作模式做參考,本文將會使用 Git 工具作為說明,此外筆者也會在文章中列出相關指令,畢竟有時候直接敲指令操作 Git 真的比較快,就看讀者習慣哪種方式囉!
故事開始 - 從遠端 clone 程式
開發者剛加入新團隊的第一件事情,就是需要從 Git 遠端檔案庫 (remote repository) 中 clone 檔案庫回本地端,在此以 Bitbucket 為範例;進入到 repository 頁後就可以看到上方有一段 git clone 語法,先把它複製起來,這就是該 repository 的存放位置。
接著在本地資料夾上點選滑鼠右鍵,選擇 git clone 功能並完成以下設定即可。
$ git clone https://.../.../gittester.git git-for-fun
- 設定 remote repository 位置,也就是剛剛複製的 url 位置。
- 設定 clone 回來的資料存放位置 (可以任意命名資料夾)。
最後輸入 Bitbucket 帳號及密碼,驗證完畢就開始從遠端 clone 檔案庫到本地端了。
開發結束 - 將變更 commit 至檔案庫
每完成一個階段的開發,就需要將異動資料 commit 到本地檔案庫 (local repository) 中存放;操作方式一樣是在資料夾上點選滑鼠右鍵,選擇 commit 開啟以下設定畫面。
$ git add -A .
$ git commit -m "完成公佈欄功能"
- 輸入代表此次異動的說明文字
- 勾選所需的異動檔案 (自動列入 staged 檔案 )
- 執行 commit 將被勾選的異動檔案存入 local repository
同事看不到我的變更?需將變更 push 至遠端檔案庫
因為目前所有變更紀錄都僅存放在 local repository 中,同事當然無法從 Bitbucket 取得異動後的資料,所以必須將 local repository 中的變更紀錄推送 (push) 至 remote repository 中。
推送成功 (幸運)
為什麼這樣說呢?因為推送成功表示「遠端最新版本」仍與你最近一次「取回本地端的版本」相同,換句話說就是從你取得這個版本後,就再也沒有其他人 push 變更至遠端檔案庫了,我想在多人開發情境下這種情況應該滿還幸運的吧!
推送失敗 (常態)
專案通常都是由多位工程師共同開發,因此在你的變更尚未推送前,同時間都會有其他開發者推送變更至遠端檔案庫,所以本地端最近一次向遠端取回的版本有很大的機會與遠端最新的版本不一致,當版本不一致時就會造成推送失敗,因此這種情況才會是常態。
推送失敗處理 - 向遠端檔案庫 pull 最新檔案
推送失敗的主因多數是遠端檔案版本比本地端最近一次從遠端取回的版本還要新,所以要解決這個問題就是要從遠端取回最新的資料,跟目前本地端的異動進行合併,擬平版本上的差異後再推送。
舉個簡單的例子來說明,假設筆者最後一次取得的遠端檔案庫版本如下
此時另位開發者完成「行事曆功能」任務後,推送變更至遠端檔案庫中存放。
最後筆者完成「修正權限錯誤」任務後 commit 一筆異動到本地端檔案庫中。
接著將變更 push 到遠端檔案庫時,會因版本不一致而發生推送失敗的情況。
$ git push
此時需要透過 pull 指令取回遠端最新的資料,而執行 pull
可拆解為 fetch
+ merge
兩個步驟,簡單來說就是透過 fetch 將資料從遠端取回,然後再跟本地端目前的資料做 merge 動作;另外在 pull 時也可以考慮使用 rebase
選項來換取清晰直覺的歷史線圖,詳細說明如下。
方法1:使用 pull 將資料從遠端拉回
直接使用 pull 取回遠端最新的資料,此時若沒衝突發生就會自動建立一個 merge 的 commit 節點;但若有衝突發生,表示你要自行解決 (resolve) 所有衝突檔案 (conflict) ,並在排除衝突後自行 commit 此次 merge 結果。
$ git pull
執行後會產生一個 merge 用的 commit 節點,表示本地端已經 merge 遠端所有異動,此時本地端最近一次從遠端取回的版本已經與遠端一致,所以在這情況下 push 可以順利完成。
完成以上動作就可以順利執行 push 將本地變更推送至遠端檔案庫囉!
image from Resistance Against London Tube Map Commit History
方法2:使用 Pull + Rebase 將資料從遠端拉回
有時為換取較直覺的版本異動軌跡,會在 pull 過程中使用 rebase 選項,簡單來說就是將本地端「最近一次從遠端取回的檔案版本」直接平移接到「fetch 回來的遠端最新檔案版本」上,而先前在本地端曾經建立的每個 commit 節點也須同步平移,因此需要逐一地與遠端新版本檔案進行合併;如果對這部分有疑問的話,可以參考筆者 Rebase - 合併的另種選擇 文章有詳細的說明。
$ git pull --rebase
就在 fetch 遠端最新檔案版本後,接著進行 rebase 作業,這個階段會逐一將本地端所有 commit 的變更與遠端新版本檔案進行合併,且「重新建立」相同名稱的新 commit 節點,稍微注意一下 commit id 是會變動的喔!
在經過 rebase 後,這個歷史線圖是不是非常清晰呢?具有較為直覺的異動軌跡;此時本地端最近一次從遠端取回的版本已經與遠端一致,所以在這情況下 push 可以順利完成。
完成以上動作就可以順利執行 push 將本地變更推送至遠端檔案庫囉!
緊急任務出現
有時候開發進行到一半,總是會有不預期的工作插入,有可能是某個 bug 被客戶發現,需要緊急修改,若這時的你正在開發檔案異動範圍滿天下的功能,當下就會有種騎虎難下的感覺;好在我們可以利用 stash 將目前變動暫存起來,讓目前檔案庫回到最近一次 commit 的節點版本上。以下說明。
將尚未 commit 的變動暫存至 stash
$ git stash save "some message"
輸入 message 來識別這次 stash 紀錄,由於預設不包括「未追蹤」及「設為 gitignore 」的檔案,所以可以透過以下兩個選項來擴大暫存變更的範圍。
- include untracked: 包含 unstaged 新檔案
$ git stash save -u "some message"
- all: 全部,包含設定為 gitignore 的檔案
$ git stash save -a "some message"
儲存後所有未 commit 的 staged 檔案異動都會消失,並且回復到最近一次 commit 的節點,此時就可以對分支進行任何操作,也許是切換 bug 分支進行錯誤修正作業。
取出 stash 後將變更 apply 到目前檔案庫中
當緊急任務完成後,需要接續先前中斷的開發工作,因此要把先前存入 stash 的變更取回;首先我們可以透過 stash list 功能取出所有 stash 暫存紀錄。
$ git stash list
在特定 stash 紀錄上點選 stash apply 功能,表示將此暫存的變更套用在目前本地端檔案庫上;只要有合併就有機會發生衝突,因此若有衝突會顯示 stash apply failed 訊息,提示你要去逐一解決衝突才能完整 apply 這個 stash 變更。
$ git stash apply [--index] [<stash>
妥善建立多個任務型分支
如果你專案目前有好幾個任務同時在執行,然後每個新功能的上線時間又還不確定,此時就可以為每個新功能開創新的分支;我們可以同時擁有數個分支,隨意切換分支來接續執行不同的開發任務,最終等待時機成熟再將此功能併回主要分支發行即可。
假設現在要開發權限功能,因此建立 feature-auth
分支
這邊表示建立 feature-auth
分支,並且直接切換到該分支中
$ git checkout -b feature-auth
可以利用 checkout 來隨意切換分支
$ git checkout master
合併任務型分支
任務型分支最終都將回歸到主幹分支上,而合併分支的步驟約略如下。
- 切換分支到「主幹 」上。
- 使用 merge 功能將「指定分支」的所有變動合併至「主幹」上。
- 刪除任務結束的分支
在開發單一功能的情況下,有些人習慣會將 commit 時機點切得很細,這都是個人習慣沒有什麼對錯,而本地端分支作業本來就是可以自己掌控,只要能夠提升效率怎樣都行,並不會影響到團隊其他成員;但當功能開發完畢,準備將異動 push 到遠端檔案庫的時候,就需要考慮這些 commit 紀錄是否對團隊具有意義性,是否會因這些變更紀錄造成線圖混亂難以追蹤,這些都是需要考量的部分。
模擬個情境來說明,目前兩分支的 base 都是從 「張四也來修改」節點開始,在開發人員完成「權限設定功能」期間,總共在 feature-auth
分支 commit 四個變更(黃色區塊);同時在 master
分支也同步進行開發,共建立一個變更,而歷史線圖將如下圖所示。
最終我們需將 feature-auth
分支的所有異動併回 master
分支,此時可以選擇的方式如下。
方法1:直接合併所有 commit 節點 (保留所有異動紀錄)
$ git merge feature-auth
merge 往往伴隨著 conflict 檔案的產生,需要逐一手動合併這些無法透過自動機制合併的檔案,並且記得在排除所有 conflict 檔案後,將所有異動 commit 進檔案庫 (建立 merge 節點)。
$ git commit
從以下歷史線圖可以發現 feature-auth
分支上的節點都併入 master
分支上,當 master
分支 push 變動到遠端檔案庫後,所有人將會看到開發「權限設定功能」過程中所有建立的節點資訊,因為多屬個人開發細節,如果有線圖潔癖的人可能會受不了。
方法2:使用 squash 壓縮合併所有變動至單個新 commit 節點 (拋棄所有異動紀錄)
這種合併方式就像是把 feature-auth
分支中所有 commit 取出,壓縮變動後 apply 到 master
分支中,最後在為此變動建立新 commit 節點。
$ git merge --squash feature-auth
因此在壓縮 feature-auth
分支的所有 commit 的檔案異動,並且排除所有 confict 檔案後,需將這些濃縮後的檔案異動 commit 進 master
分支中 (建立 squash 變更的節點)。
$ git commit -m "搞定權限設定"
這樣的線圖就非常直覺,明確地標示這個 commit 節點就是「完成權限設定」功能,不會有其他個人開發細節干擾,更沒有無謂的分支線圖出現,因此筆者認為在此情境下使用 squash 合併較為恰當。
特別注意兩分支並無合併特徵,就只是在 master
中 commit 這個壓縮後的變動。
刪除分支
針對任務型分支來說,只要該分支已經順利被 master
合併後,就可以直接刪除了;在 TortoiseGit 中可以先點選 browser references 功能來列出所有 branch 清單,接著選定目標後刪除即可。
$ git branch -d feature-auth
$ git push origin :feature-auth
刪除遠端分支
解決衝突檔案 (conflict)
在合併檔案時會先進行 auto merge 處理,當有同一行程式資料在兩個版本中呈現不同面貌時,工具無法自動處理就會列入衝突檔案清單中,此時就只能透過人工作業來進行接續的檔案合併作業;許多剛接觸版控的朋友總是對衝突相當畏懼,但其實真的還滿常會遇到這種情況,所以還是來說明一下怎麼排除這種情況,我們必須正面勇敢地去面對它,這樣才能夠在多人同時開發下發揮版控真正的作用。
以下就舉實際使用 squash 合併分支發生衝突的案例,首先可以點選 resolve 列出衝突檔案清單。
在檔案清單上點選右鍵,選擇 edit confilicts 功能來編輯衝突檔案。
工具如下,反正目標就是要搞到所有標示衝突的紅色區塊都消失就對了。
- 啟用編輯來直接修改 Merged 區塊文字
- 跳至下一個衝突區塊
- 直接使用左方區塊文字解決衝突區塊 (後續再依現況微調)
- 等待被合併進來的資料
- 目前本地的資料
- 合併後每行狀態 (減: 移除該行 加: 新增該行 三角驚嘆號: 衝突)
- 合併後所使用的檔案
稍微操作一下比較有感覺
解決完所有衝突後,就可以執行 commit 建立出 merge 節點。
後悔 commit 怎麼辦?
在還沒有 push 變更到遠端檔案庫前,什麼事情都好說,因此若不小心 commit 錯資料時,可以先透過 log 或 reflog 查詢操作紀錄,然後從這些紀錄中挑選 commit 前的版本進行 reset 回復即可。
後記
Git 的使用上還算簡單,但有時面對複雜情境也是滿燒腦的,因此仍需對 Git 運作原理及不同指令功能間的差異(指令選項一定有其目的性) 加以理解,這些我想就不是三言兩語可以完整概括,所以誠心建議大家還是找本書來看看,有系統地瀏覽過一遍全貌,這樣在面對各式各樣版控需求時,才有能力考量現況做出最合適的操作,筆者也還在持續學習中,咱們共勉之。
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !