本來已經不寫文字部落格了,一般心得都錄成了視訊(這在我看來是更好的方式),但是今天遇到一個關於 Git 的問題不太好重現也不便於錄製視訊,加上它本身很具有代表性也很有用,所以還是記錄於此。
背景
一箇中型規模專案,開始規劃時就打算採用 C/S 架構,後端是單純的 API 服務,前端在 Web 上搞一個 SPA,之後再搞其他端也就順理成章了。只可以第一次弄沒經驗,有些細節最初沒有考慮到。
建立專案的時候前後端真是完全分離的,分成了兩個目錄,建立了兩個 repos。一開始只有一個人乾的時候倒也沒什麼,開兩個視窗切來切去也就罷了,後來一是部署起來麻煩,二來主要是其他開發者加入後,程式碼的版本管理、提交、合併、稽核等等等等都變得越來越繁瑣。
後來一想:架構上分離而已,幹嘛非要兩個目錄兩個 repos?真是自找麻煩!於是就開始考慮整合。
要求
把兩個目錄併成一個倒不難,但是要完整保留雙方的歷史記錄就有些麻煩了,這也是唯一一個必須要實現的目標。
過程
首先為了便於描述,約定整合前兩個目錄分別叫做 frontend
和 backend
,合併後的結構與名稱應當如下:
- project/ => 即最開始的 frontend,整合完後更名
- .gitignore => 合併兩個 repos 的忽略檔案
- .git/ => 最終僅餘一個 repo
+ client/ => 對應 frontend
+ server/ => 對應 backend
以下步驟是以 frontend
為基點,把 backend
移進來,實際上反過來也是一樣的,自行替換對應的名稱即可。在開始之前先清理兩個 repos 裡的工作記錄,該提交的提交,該備份的備份,保持乾淨。
1. $ [~] cd frontend
2. $ [frontend] git remote add -f backend /fullpath/to/backend
3. $ [frontend] git merge --strategy ours --no-commit backend/master
4. $ [frontend] mkdir -p server
5. $ [frontend] git read-tree --prefix=server/ -u backend/master
6. $ [frontend] git commit --message `完成 backend 的遷移,新目錄為 server`
7. $ [frontend] mkdir -p client
8. # 拷貝 frontend 的原始專案檔案(除了 .git/ 和 .gitignore 以外)至 client/
9. $ [frontend] cd ..; mv frontend/ project/; cd project
10. $ [project] cat server/.gitignore >> .gitignore
11. # 整理合並後的 .gitignore,修復其中的路徑缺失並儲存;修復各種專案依賴的缺失,本地測試。
12. $ [project] git add --all; git commit --message `遷移整合完成!`
以上是完整的步驟先列出來方便參考,下面做一個詳細的解釋。
整個過程中主要用到的工具是 merge 和 read-tree,前者用於合併歷史記錄並且中斷在最後提交之前,所產生的檔案衝突不會被寫入硬碟;然後利用後者重寫整個檔案樹並把讀取到的內容(讀取的目標是 backend
)寫入新的路徑下。最後提交以結束合併。
第2
步裡,我們把 backend
作為 remote server 新增到 frontend
庫中。-f
的作用是在新增後立刻 fetch
。要注意一定得使用絕對路徑來引用 backend
庫。
第3
步裡,--strategy ours
比較難以理解,且聽我詳細道來:一般來說當合並兩個檔案樹時,如果遇到衝突我們是需要手動去解決它的,但是目前我們要做的不是解決衝突,而是在引入 backend
歷史記錄的前提下完整保留 frontend
的內容。衝突肯定是會有的,即使兩個不同的專案也是如此,比方說兩邊都有 README.md
、app/
、config/
等檔案或目錄,但是我們不關心衝突,我們只要保留 frontend
的檔案樹並且把 backend
的歷史記錄合併進來。
--strategy ours
會完成全部的合併解析,但是所有的衝突都以“我”為準,不允許外來的衝突覆蓋“我”的檔案內容。最終的結果就是:
backend
的歷史記錄被合併到frontend
的歷史記錄中backend
的檔案樹被讀取並和frontend
的檔案樹比對進行衝突解析:- 如果發現衝突,以
frontend
為準,丟棄所有內容變更 - 沒衝突的則保留(但是我們也不要的,見後面的內容)
- 如果發現衝突,以
這也是後面緊接著使用 --no-commit
的原因,該選項會在合併解析完成後中斷,停留在最後的提交步驟之前。我們知道,只要你還沒 commit,那麼 merge 的結果就暫時儲存在快取區中,只有完成提交步驟合併才算徹底完成(檔案樹被正式改變)。這就給我們一個機會來重新讀取 backend
的檔案樹,並改寫其儲存的位置。不過在此之前,第4
步先要建立目標子目錄(很重要!)。
第5
步開始 read-tree 了,--prefix
用於指定檔案樹讀取後儲存的路徑,相對於當前路徑並且一定要追加 /
。-u
是說在讀取後更新 index,使得 working tree 與 index 保持同步。如果你不小心忘了加 -u
,可以在這一步之後執行 git add --update
,一樣的效果。
這一步在背後有些細節比較抽象,之前的 merge 也曾讀取過 backend
的檔案樹,但經過沖突解析之後已經面目全非,分析如下:
- 有衝突的被丟棄,因此一部分檔案/目錄其實已經不存在了
- 沒衝突的被保留,但是路徑還在
frontend
的根路徑下
經過再次 read-tree,上面的“遺蹟”得以修復,結果如下:
- 有衝突的因為已被丟棄,所以直接從本次讀取中獲得,且路徑前面追加
--prefix
選項的值 - 沒衝突的雖然被保留,但是由於本次讀取追加了 prefix,所以它們的路徑也被改變,相當於在快取裡做了一次
git mv
好了,重點就是這些,之後的步驟都很尋常,只要小心操作就沒什麼難理解的。