如何用 Git 合併兩個庫(合併歷史記錄,解決衝突/改寫路徑)

原本已經不寫文字博客了,通常心得都錄成了視頻(這在我看來是更好的方式),可是今天遇到一個關於 Git 的問題不太好重現也不便於錄製視頻,加上它自己很具備表明性也頗有用,因此仍是記錄於此。前端

背景

一箇中型規模項目,開始規劃時就打算採用 C/S 架構,後端是單純的 API 服務,前端在 Web 上搞一個 SPA,以後再搞其餘端也就瓜熟蒂落了。只能夠第一次弄沒經驗,有些細節最初沒有考慮到。git

建立項目的時候先後端真是徹底分離的,分紅了兩個目錄,建立了兩個 repos。一開始只有一我的乾的時候倒也沒什麼,開兩個窗口切來切去也就罷了,後來一是部署起來麻煩,二來主要是其餘開發者加入後,代碼的版本管理、提交、合併、審覈等等等等都變得愈來愈繁瑣。後端

後來一想:架構上分離而已,幹嗎非要兩個目錄兩個 repos?真是自找麻煩!因而就開始考慮整合。緩存

要求

把兩個目錄併成一個倒不難,可是要完整保留雙方的歷史記錄就有些麻煩了,這也是惟一一個必需要實現的目標。bash

過程

首先爲了便於描述,約定整合前兩個目錄分別叫作 frontendbackend,合併後的結構與名稱應當以下:架構

- project/                  => 即最開始的 frontend,整合完後改名
  - .gitignore              => 合併兩個 repos 的忽略文件
  - .git/                   => 最終僅餘一個 repo
  + client/                 => 對應 frontend
  + server/                 => 對應 backend

如下步驟是以 frontend 爲基點,把 backend 移進來,實際上反過來也是同樣的,自行替換對應的名稱便可。在開始以前先清理兩個 repos 裏的工做記錄,該提交的提交,該備份的備份,保持乾淨。app

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 '遷移整合完成!'

以上是完整的步驟先列出來方便參考,下面作一個詳細的解釋。frontend

整個過程當中主要用到的工具是 mergeread-tree,前者用於合併歷史記錄而且中斷在最後提交以前,所產生的文件衝突不會被寫入硬盤;而後利用後者重寫整個文件樹並把讀取到的內容(讀取的目標是 backend)寫入新的路徑下。最後提交以結束合併。工具

2步裏,咱們把 backend 做爲 remote server 添加到 frontend 庫中。-f 的做用是在添加後馬上 fetch。要注意必定得使用絕對路徑來引用 backend 庫。測試

3步裏,--strategy ours 比較難以理解,且聽我詳細道來:通常來講當合並兩個文件樹時,若是遇到衝突咱們是須要手動去解決它的,可是目前咱們要作的不是解決衝突,而是在引入 backend 歷史記錄的前提下完整保留 frontend 的內容。衝突確定是會有的,即便兩個不一樣的項目也是如此,比方說兩邊都有 README.mdapp/config/ 等文件或目錄,可是咱們不關心衝突,咱們只要保留 frontend 的文件樹而且把 backend 的歷史記錄合併進來。

--strategy ours 會完成所有的合併解析,可是全部的衝突都以「我」爲準,不容許外來的衝突覆蓋「我」的文件內容。最終的結果就是:

  1. backend 的歷史記錄被合併到 frontend 的歷史記錄中
  2. backend 的文件樹被讀取並和 frontend 的文件樹比對進行衝突解析:

    • 若是發現衝突,以 frontend 爲準,丟棄全部內容變動
    • 沒衝突的則保留(可是咱們也不要的,見後面的內容)

這也是後面緊接着使用 --no-commit 的緣由,該選項會在合併解析完成後中斷,停留在最後的提交步驟以前。咱們知道,只要你還沒 commit,那麼 merge 的結果就暫時保存在緩存區中,只有完成提交步驟合併纔算完全完成(文件樹被正式改變)。這就給咱們一個機會來從新讀取 backend 的文件樹,並改寫其保存的位置。不過在此以前,第4步先要建立目標子目錄(很重要!)。

5步開始 read-tree 了,--prefix 用於指定文件樹讀取後保存的路徑,相對於當前路徑而且必定要追加 /-u 是說在讀取後更新 index,使得 working treeindex 保持同步。若是你不當心忘了加 -u,能夠在這一步以後執行 git add --update,同樣的效果。

這一步在背後有些細節比較抽象,以前的 merge 也曾讀取過 backend 的文件樹,但通過沖突解析以後已經面目全非,分析以下:

  • 有衝突的被丟棄,所以一部分文件/目錄其實已經不存在了
  • 沒衝突的被保留,可是路徑還在 frontend 的根路徑下

通過再次 read-tree,上面的「遺蹟」得以修復,結果以下:

  • 有衝突的由於已被丟棄,因此直接從本次讀取中得到,且路徑前面追加 --prefix 選項的值
  • 沒衝突的雖然被保留,可是因爲本次讀取追加了 prefix,因此它們的路徑也被改變,至關於在緩存裏作了一次 git mv

好了,重點就是這些,以後的步驟都很尋常,只要當心操做就沒什麼難理解的。

相關文章
相關標籤/搜索