如何在特定提交点将 git 存储库拆分为最近和较旧的提交,同时保留分支?

How to split a git repository into recent and older commits at a specific commit point, preserving branches?

如何在特定提交 SHA 中将我的 git 存储库拆分为两个存储库(最近的和历史的),同时保留每个分支中的分支,正确链接到它们在 master 上的提交?

问题描述

虽然许多 SO 问题询问并回答了如何拆分子目录(例如,The Easy Way),但这不是我需要做的。相反,我需要将存储库提交拆分为特定提交之前的所有提交以及之后的所有提交。虽然我的存储库很大,在十年的历史中有数千次提交和数百个分支,但问题可以归结为一个简单的存储库,有 8 次提交 (1-8) 和三个分支 (master、A、B):

1 - 2 - 3 - 4 - 5 - 6 - master
     \           \
      7           8
       \           \
        A           B

转换后,我想要的是两个仓库。第一个(project-history)应该包含分支 A 上的历史提交 1、2、3 和 4 以及关联的提交 7。第二个(project-recent)应该包含分支 B 上的提交 4、5、6 和关联的提交 8 . 这些看起来像:

project-history                     project-recent
1 - 2 - 3 - 4 -master               4 - 5 - 6 - master
     \                                   \
      7                                   8
       \                                   \
        A                                   B

Split a Git Repository into Two 中描述了类似的问题,但是没有一个答案被接受,也没有产生我需要的结果,我在下面描述了这个问题,以及一个测试脚本。

可能的方法:分支,然后使用孤立的提交进行变基

Pro Git 一书 Chapter 7.13 Git-Tools-Replace 提供了一种非常接近的方法。在这种方法中,您首先创建历史记录,然后将最近的提交变基到一个新的孤立提交上。

创造历史

  1. 找到要拆分存储库的提交的 SHA
  2. 在那一点创建一个 history 分支
  3. 将历史分支及其附属分支推送到一个新的项目历史回购中

这一切都很好。

Rebase 最近的提交

但是下一部分不能完全工作:

  1. 创建一个孤儿提交,它会产生提交 aaf5c36
    • git commit-tree 8e3dbc5^{tree}
  2. 从拆分提交的父级开始,将 master 重新设置为 aaf5c36
    • git rebase --preserve-merges --onto aaf5c36 8e3dbc5
  3. 将这个新的 master 和分支 B 推送到一个新的项目最近的 repo

问题:分支 B 与新项目最近存储库中的主服务器断开连接。生成的存储库如下所示:

project-history                     project-recent
1 - 2 - 3 - 4 -master               4 - 5 - 6 - master
     \                                   
      7                             1 - 2 - 3 - 4 - 5 - 8- B
       \                            
        A                                   

说明问题的脚本

repo-split-example.sh 脚本创建一个示例存储库 (repo-split-example),然后使用此技术将其拆分为 repo-split-historyrepo-split-recent 存储库,但分支 B 未附加在后者。此外,通过将分支 B 推入最近的版本库,历史提交也被推入版本库(提交 1、2、3),并且有重复的提交 4 和 5(原始的,加上来自重写的)变基)。这是项目最新回购的最终状态:

$ git log --graph --all --oneline --decorate
* c29649c (HEAD -> master) sixth
* e8545fd fifth
* 8e3dbc5 fourth
* aaf5c36 Get history from historical repository at file:///Users/jones/development/git-svn-migrate/repo-split-history
* 7a98d11 (B) branchB
* 1f620ac fifth
* 1853778 fourth
* 14ab901 third
* 8dd0189 second
* bb1fc8d first

而我想要的是:

$ git log --graph --all --oneline --decorate
* c29649c (HEAD -> master) sixth
| * 7a98d11 (B) branchB
|/
* e8545fd fifth
* 8e3dbc5 fourth
* aaf5c36 Get history from historical repository at file:///Users/jones/development/git-svn-migrate/repo-split-history

repo-split-example.sh 脚本是重现问题的简单方法。我怎样才能让项目最近的存储库包含来自 master 的最近提交加上来自分支 B 的提交,正确链接到重新设置的提交 5 (fifth)?

谢谢指教!

更新

四处查看后,我确定我可以手动将最近的分支重新设置为新重写的树。为此,对于最近树中的每个分支,我会做:

# Rebase branch B onto the newly rewritten fifth commit
git branch temp e8545fd # the SHA of the rewritten fifth commit
git checkout B
git rebase temp # This works, but will (always?) have conflicts because it starts 
                # from the beginning because there is no common merge base for the commit
git branch -d temp

所以,这行得通,并产生了预期的结果。 Bit git rebase temp 会产生大量的合并冲突(从历史开始以来的每个提交都有一个冲突),因为重写的第五次提交不与原始分支 B 共享任何历史。所以有很多手动冲突在这里解决,这对我的真实存储库来说会花费太长时间。因此,仍在寻找一个可行的解决方案,其中 rebase 可以在没有合并冲突的情况下工作。

我终于弄明白了,所以把这个过程记录在这里,希望它有用。可以使用移植来拆分存储库,然后使用 filter-branch 重写树以使移植永久化,而不是变基。因此,鉴于 TRUNCPOINT 是要拆分存储库的提交的 SHA,TRUNCPARENT 是其父级的 SHA,并且 project-historyproject-recent 都是新初始化的存储库为了接收历史提交和最近的提交,将存储库分成两半的最终过程如下:

首先为历史提交创建一个分支

这只需在 $TRUNCPOINT 处创建一个分支,并将该分支和所有源于它的分支推送到 project-history

git branch history $TRUNCPOINT
git push project-history history:master
git push project-history A

将本地 hisotry 分支上的历史提交推送到 project-history 仓库的 master 分支,然后将分支 A 推送到 project-history 仓库出色地。结果如下:

git log --graph --oneline --decorate --all
* fdc8f84 (A) branchA a1
| * 7237a3e (HEAD -> master) fourth
| * 55be55d third
|/  
* 26555d8 second
* 5a68ca2 first

到目前为止很好,因为历史上最近的提交是第四次提交。

现在我们需要拆分 repo 以获取从 TRUNCPOINT 到 master 的 HEAD 的最新提交。

创建一个基本提交作为最近提交的父提交

接下来的这些命令会创建一个空提交,它将成为最近提交树的新根。

MESSAGE="Get history from historical repository"
BASECOMMIT=`echo $MESSAGE | git commit-tree ${TRUNCPARENT}^{tree}`

通过将 TRUNCPARENT 移植到 BASECOMMIT 来拆分存储库

最后,我们 graft 存储库,告诉它 $TRUNCPOINT 的父级现在是 $BASECOMMIT 而不是它原来的父级。这有效地截断了 $TRUNCPOINT 处的历史记录。然后我们使用filter-branch重写历史让嫁接永久化,然后将master及其关联的分支B推送到project-recent仓库

echo "${TRUNCPOINT} ${BASECOMMIT}" > .git/info/grafts
git filter-branch -- --all
git push project-recent master
git push project-recent B

这是 project-recent 存储库的结果拆分内容。

git log --graph --oneline --decorate --all
* 2335aeb (B) branchB b2
* 2bb7ea3 branchB b1
| * 83c3ae9 (HEAD -> master) sixth
|/  
* 25931c5 fifth
* 1e1e201 fourth
* a7f3373 Get history from historical repository

请注意,根提交 a7f3373 是我们人为创建的 BASECOMMIT,它的提交日志可以包含一条消息,将用户指向具有项目历史的存储库的位置,以便将来如果需要,用户可以使用 git replace 重新加入两个存储库。 可以下载 reproducible script 的完整过程,但也包含在下面以供参考。

我们唯一遇到的另一个主要问题是,在我们的真实案例中,我们试图确定哪些分支应该被推送到历史回购,哪些应该被推送到最近的回购。但是这个答案显示了拆分本身是如何完成的以创建两个存储库。

完全复制示例 bash 脚本

#!/bin/bash
WORKDIR=${PWD}

create_repos () {
    rm -rf repo-split-example repo-split-recent repo-split-history
    # Create the repo to be split
    example_repo

    # Create the repo to contain the historical commits
    HISTREPO="file://${WORKDIR}/repo-split-history"
    mkdir ../repo-split-history
    cd ../repo-split-history/
    git init --bare
    cd ../repo-split-example
    git remote add project-history $HISTREPO

    # Create the repo to contain the recent commits
    RECEREPO="file://${WORKDIR}/repo-split-recent"
    mkdir ../repo-split-recent
    cd ../repo-split-recent/
    git init --bare
    cd ../repo-split-example
    git remote add project-recent $RECEREPO
}

example_repo () {
    # Part I: set up a test repo with our example commits
    mkdir repo-split-example
    cd repo-split-example/
    git init
    echo "We want to split the repository into project-recent and project-history portions, following the instructions at https://git-scm.com/book/en/v2/Git-Tools-Replace., but also including branches." > README.md
    echo " "
    echo "First commit." >> README.md
    git add README.md
    git commit -m "first"
    echo "Second commit." >> README.md
    git add README.md
    git commit -m "second"

    git checkout -b A HEAD
    echo "Add Branch A change." >> README.md
    git add README.md
    git commit -m "branchA a1"

    git checkout master
    echo "Third commit." >> README.md
    git add README.md
    git commit -m "third"
    TRUNCPARENT=`git rev-parse HEAD`

    echo "Fourth commit." >> README.md 
    git add README.md
    git commit -m "fourth"
    TRUNCPOINT=`git rev-parse HEAD`

    echo "Fifth commit." >> README.md
    git add README.md
    git commit -m "fifth"
    FIFTH=`git rev-parse HEAD`

    git checkout -b B HEAD
    echo "Add Branch B change. b1" >> README.md
    git add README.md
    git commit -m "branchB b1"
    B1=`git rev-parse HEAD`

    echo "Add Branch B change. b2" >> README.md
    git add README.md
    git commit -m "branchB b2"
    B2=`git rev-parse HEAD`

    git checkout master
    echo "Sixth commit." >> README.md
    git add README.md
    git commit -m "sixth"

    # Now we have a repo with the requisite structure, ready to be split
    git log --graph --all --oneline --decorate
}


split_repo () {
    # Part II: Split the git repo into historical and current halves at $TRUNCPOINT
    # Following guidelines at https://git-scm.com/book/en/v2/Git-Tools-Replace

    # First create a branch for the historical commits
    echo "Branching history at $TRUNCPOINT"
    git branch history $TRUNCPOINT
    git log --graph --oneline --decorate history A

    # Now copy the history repo to the remote HISTREPO repository
    git push project-history history:master
    git push project-history A

    # Now to split the repo to get the recent history from TRUNCPOINT to HEAD of master
    # Create a base commit for the new new recent history
    MESSAGE="Get history from historical repository at $HISTREPO"
    BASECOMMIT=`echo $MESSAGE | git commit-tree ${TRUNCPARENT}^{tree}`

    # Split the repository by grafting the TRUNCPARENT onto BASECOMMIT
    echo "${TRUNCPOINT} ${BASECOMMIT}" > .git/info/grafts
    git filter-branch -- --all

    # Finally, push the current rewritten master and associated branches to a new repository
    git push project-recent master
    git push project-recent B
}

create_repos
split_repo