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



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

问题描述

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

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

转换后,我想要的是两个存储库。 第一个(项目历史记录)应包含历史提交 1、2、3 和 4,以及分支 A 上的关联提交 7。 第二个(项目最近)应包含分支 B 上的提交 4、5、6 和关联的提交 8。 这些看起来像:

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

在将 Git 存储库一分为二中描述了一个类似的问题,但这两个答案都没有被接受,也不会产生我需要的结果,我在下面描述,以及一个测试脚本。

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

Pro Git书籍第 7.13 章 Git-Tools-Replace 提供了一种非常接近的方法。 在这种方法中,首先创建历史记录,然后将最近的提交重新设置为新的孤立提交。

创建历史记录

  1. 查找要拆分存储库的提交的 SHA,以查找要在其上拆分的提交的 SHA。
  2. 此时创建history分支
  3. 将历史记录分支
  4. 及其附加的分支推送到新的项目历史记录存储库中

这一切都很好用。

重定最近提交的基准

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

  1. 创建一个孤立提交,这将产生提交aaf5c36
    • git commit-tree 8e3dbc5^{tree}
  2. 主服务器重定为从拆分提交的父级开始的aaf5c36
    • git rebase --preserve-merges --onto aaf5c36 8e3dbc5
  3. 将此新的主节点和分支 B 推送到新的项目最近的存储库中

问题:分支 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-history存储库和repo-split-recent存储库,但分支 B 在后者中未附加。 此外,通过将分支 B 推送到最近的存储库中,历史提交也会被推送到存储库中(提交 1,2,3),并且存在重复的提交 4 和 5(原始提交,加上来自 rebase 的重写提交)。 下面是项目最近的存储库的最终状态:

$ 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

因此,这有效,并产生所需的结果。 位git rebase temp会产生大量合并冲突(自历史记录开始以来的每个提交一个),因为重写的第五次提交不与原始分支 B 共享任何历史记录。 所以这里有很多手动冲突解决,对于我真正的存储库来说,这需要太长时间。 因此,仍然在寻找一种可行的解决方案,其中变基工作而不会发生合并冲突。

我终于想通了这一点,所以在这里记录这个过程,希望它有用。 可以使用移植物来拆分存储库,然后使用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

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

现在我们需要拆分存储库,以获取最近的提交从 TRUNCPOINT 到 HEAD of master。

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

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

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

通过将 TRUNCPARENT 嫁接到 BASECOMMIT 来拆分存储库

最后,我们嫁接存储库,告诉它$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重新加入两个存储库。 可以下载作为可重现脚本的完整过程,但也包含在下面以供参考。

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

完全复制的示例 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 

最新更新