一步步拆分 Git 資料夾歷史並轉換為 Submodule 指南

16 分鐘閱讀

我後來使用 git filter-repo 搞定部落格的 content 拆分,使得內容部落格更新和部落格程式碼的歷史不會混在一起。

琳 avatar

生成一篇指南,如何將 git commit 記錄拆開,將涉及變更了某一個指定的資料夾下檔案的 commit 全部分開到另一個樹? 我的目的是將某一個資料夾分開為 git submodule

Felo Search avatar
Felo Search

本指南旨在詳細說明如何將 Git 專案中特定資料夾的變更歷史記錄分離出來,並將該資料夾轉換為一個獨立的 Git Submodule。

這個過程主要包含兩個階段:首先,將目標資料夾及其完整的提交歷史提取到一個全新的 Git 倉庫;其次,在原始專案中移除該資料夾,並將新建立的倉庫作為 Submodule 引入。透過這種方式,可以實現更模組化的專案管理,讓主專案和子模組各自獨立發展和版本控制。

第 1 步:將目標資料夾的歷史記錄分離到獨立的 Git 倉庫

此步驟的目的是為您想要分離的資料夾建立一個全新的 Git 倉庫,同時完整保留其在原專案中的提交歷史記錄。

選項 A:使用 git subtree split

git subtree split 命令能夠從現有倉庫中提取一個子目錄的歷史記錄,並在一個新的分支中建立一個以該子目錄為根的全新專案歷史 12

  1. 在您的主專案倉庫根目錄執行以下命令。請將 <folder_path> 替換成您要分離的資料夾路徑(例如 src/my_module),並將 <new_branch_name> 替換為新分支的自訂名稱(例如 feature/my_module_history):

    git subtree split -P <folder_path> -b <new_branch_name>
    
  2. 建立一個新的資料夾,用於存放即將分離出來的子模組程式碼,並將其初始化為一個 Git 倉庫:

    mkdir ../<submodule_repo_name>
    cd ../<submodule_repo_name>
    git init
    

    請將 <submodule_repo_name> 替換為您子模組倉庫的名稱,<original_repo_name> 則是您原始主專案資料夾的名稱。

  3. 從原專案剛剛建立的 <new_branch_name> 分支,將程式碼拉取到這個新的子模組倉庫中:

    git pull ../<original_repo_name> <new_branch_name>
    

    完成此步驟後,新的 <submodule_repo_name> 倉庫將擁有原資料夾的完整歷史記錄 1

  4. 在您的 Git 代管服務(如 GitHub、GitLab 或 Bitbucket)上,為這個子模組建立一個新的遠端倉庫。

  5. 將本地新建立的子模組倉庫連接到遠端倉庫,並推送程式碼:

    git remote add origin <remote_repository_url>
    git push -u origin main # 或 master,取決於您的預設分支名稱
    

    如果在執行 git push 時遇到 refusing to merge unrelated histories 的錯誤訊息,可以嘗試先執行 git pull origin main --allow-unrelated-histories(將 main 替換成您的遠端預設分支名),然後再重新推送 1

選項 B:使用 git filter-repo (功能更強大,但需額外安裝)

git filter-repo是 Git 官方推薦用於重寫歷史的工具,相較於舊有的 git filter-branch,它執行速度更快且使用上更安全 3。使用前需先安裝此工具。

  1. 重要:在進行任何歷史重寫操作之前,強烈建議您先備份原始倉庫。

  2. 為了安全起見,可以複製一份原始倉庫到新的位置,然後在新位置進行操作:

    cp -r <original_repo_name> <submodule_repo_name>_temp
    cd <submodule_repo_name>_temp
    
  3. 執行 git filter-repo 命令,將指定的資料夾提升為倉庫的根目錄,並移除所有其他檔案及資料夾的歷史。請將 <folder_path> 替換成您要分離的資料夾路徑:

    git filter-repo --subdirectory-filter <folder_path>
    

    此命令會改寫當前操作倉庫的歷史記錄,使其只包含 <folder_path> 的內容及其相關的提交歷史,並將 <folder_path> 作為新倉庫的根目錄。

  4. 同樣地,為這個子模組在您的 Git 代管服務上建立一個新的遠端倉庫。

  5. 將這個經過歷史重寫的本地倉庫連接到遠端倉庫,並推送程式碼:

    git remote add origin <remote_repository_url>
    git push -u origin main # 或 master
    

第 2 步:在原專案中移除舊資料夾並添加 Submodule

在成功將目標資料夾的歷史分離到獨立倉庫後,接下來的步驟是在原始主專案中,用新建立的 Submodule 來取代原有的資料夾。

  1. 返回到您的原始主專案倉庫的根目錄。

  2. 從主專案中移除舊的資料夾。建議先從 Git 的追蹤中移除,然後再實際刪除資料夾檔案 4567

    git rm -r --cached <folder_path> # 從 Git 追蹤中移除,但保留本地檔案
    rm -rf <folder_path>             # 刪除實際的資料夾和檔案
    git commit -m "Removed <folder_path> to replace with submodule"
    

    另一種方式是使用 git rm -rf <folder_path>,這會直接刪除檔案並將刪除操作加入到 Git 的暫存區 8

  3. 添加新的 Submodule。請將 <remote_repository_url> 替換成您在第 1 步中為子模組建立的遠端倉庫 URL,<folder_path> 通常應保持和原資料夾的名稱及路徑一致 9107

    git submodule add <remote_repository_url> <folder_path>
    

    這個命令會執行以下操作:

    • 克隆子模組的遠端倉庫到主專案中指定的 <folder_path> 路徑。
    • 在主專案的根目錄下建立或更新 .gitmodules 檔案,此檔案會記錄子模組的相關資訊(如路徑和 URL)1151012
    • .gitmodules 檔案的變更和新加入的子模組資料夾(它本身是一個指向特定 commit 的 Git "link")加入到 Git 的暫存區。
  4. 提交將 Submodule 添加到專案的變更:

    git commit -m "Added <folder_path> as a submodule"
    
  5. 推送主專案的變更到其遠端倉庫:

    git push
    

    在 GitHub 等平台上,您會注意到子模組資料夾的圖示與一般資料夾不同,點擊它可以直接跳轉到該子模組的獨立倉庫頁面 7

第 3 步:Submodule 的日常協作與管理

克隆包含 Submodule 的專案

當團隊成員或其他使用者克隆包含 Submodule 的主專案時,有以下幾種方式來獲取 Submodule 的實際內容:

更新 Submodule

主專案本身記錄的是 Submodule 在某個特定 commit 的參照。如果 Submodule 的遠端倉庫有了新的提交,主專案需要手動更新其對 Submodule 的參照。

在 Submodule 中進行開發

在 Submodule 中進行開發工作,並將變更推送回 Submodule 的遠端倉庫:

  1. 進入 Submodule 的目錄:cd <folder_path> 13

  2. 重要:執行 git submodule update 後,Submodule 通常會處於 "detached HEAD" (分離頭) 狀態,即 HEAD 直接指向一個 commit 而非分支。在此狀態下直接提交的變更,若未被特定分支引用,則在下次執行 git submodule update 時可能會遺失。因此,強烈建議在開始開發前先切換到一個已存在的分支,或建立一個新的分支 101819

    git checkout main  # 或其他您想在其上開發的分支
    # 或建立並切換到新分支
    # git checkout -b new_feature_branch
    
  3. 進行程式碼修改,然後像在一般 Git 倉庫中一樣執行 git addgit commit

  4. 將變更推送到 Submodule 自身的遠端倉庫:git push

  5. 返回主專案的根目錄。

  6. 主專案會偵測到其追蹤的 Submodule commit ID 已不再是 Submodule 最新 HEAD 所指向的 commit。執行 git add <folder_path> 將此更新(即 Submodule 指向了新的 commit)加入暫存區。

  7. 提交此變更到主專案:git commit -m "Update submodule <folder_path> with new features"

  8. 推送主專案的變更。

指定 Submodule 追蹤特定分支

在添加 Submodule 時,可以透過 -b 參數指定希望它追蹤的分支 2021

git submodule add -b <branch_name> <remote_repository_url> <folder_path>

如果 Submodule 已經存在,也可以編輯 .gitmodules 檔案,在對應的 Submodule 區塊中添加 branch = <branch_name> 欄位,然後執行 git submodule syncgit submodule update --remote 來使設定生效 20

注意事項

  1. git多模块项目拆分原创 - CSDN博客 ↩2 ↩3

  2. Git 将子文件夹分离为一个新的库转载 - CSDN博客

  3. 如何使用git submodule和git filter-repo从仓库中分离特定目录

  4. 将git文件夹转换为子模块- 瑞诺拉- 博客园

  5. Git: submodule 子模块简明教程 ↩2

  6. 将git文件夹转换为子模块 - 瑞诺拉 - 博客园

  7. Git: submodule 子模块简明教程 | A Quest After Perspectives ↩2 ↩3 ↩4 ↩5 ↩6

  8. 如何将现有git 仓库中的子目录分离为独立仓库并保留其提交历史

  9. git submodule add子模块的添加 - CSDN博客

  10. 子模組(Submodules) | GitPro - GitBook ↩2 ↩3 ↩4 ↩5

  11. Git中submodule的使用原创 - CSDN博客 ↩2

  12. Git: submodule 子模块简明教程

  13. Git Submodules 介绍(通俗易懂,总结了工作完全够用的 ... ↩2 ↩3 ↩4

  14. git submodule 子模块的基本使用原创 - CSDN博客

  15. Git 版本控制教學- submodule - MyApollo

  16. git submodule - 网易数帆- 博客园 ↩2

  17. Git Submodules 介绍(通俗易懂,总结了工作完全够用的 ...

  18. 子模块— git-reference 0.1 文档 ↩2

  19. 子模組(Submodules) | GitPro - GitBook ↩2 ↩3

  20. git 使用submodule 如何指定分支原创 - CSDN博客 ↩2

  21. git 使用submodule 如何指定分支原创 - CSDN博客