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

标签 git branch rebase

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

问题描述

虽然许多 SO 问题都会询问并回答如何拆分子目录(例如 The Easy Way ),但这不是我需要做的。相反,我需要将存储库提交拆分为某个提交之前的所有提交,以及之后的所有提交。虽然我的存储库很大,在十年的历史中包含数千次提交和数百个分支,但问题可以归结为一个简单的存储库,其中包含 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

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

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

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

创造历史
  • 找到要拆分存储库的提交的 SHA
  • 创建 history在那一点上的分支
  • 将历史分支及其附属分支推送到新的项目历史仓库

  • 这一切都很好。

    重新设置最近的提交

    但是下一部分并不能完全发挥作用:
  • 创建一个孤儿提交,它会产生提交 aaf5c36
  • git commit-tree 8e3dbc5^{tree}
  • 将 master 重新设置为 aaf5c36从拆分提交的父级开始
  • git rebase --preserve-merges --onto aaf5c36 8e3dbc5
  • 将这个新的 master 和分支 B 推送到一个新的项目最近的 repo

  • 问题:分支 B 与新项目最近存储库中的 master 断开连接。生成的存储库如下所示:
    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 有重复(原始的,加上从 rebase )。这是项目最近 repo 的最终状态:
    $ 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 的提交,正确链接到 rebase commit 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产生大量 merge 冲突(自历史开始以来的每次提交都有一个),因为重写的第五次提交不与原始分支 B 共享任何历史记录。所以这里有很多手动冲突解决,它会我的真实存储库需要太长时间。所以仍然在寻找一个可行的解决方案,其中 rebase 可以在没有 merge 冲突的情况下工作。

    最佳答案

    我终于想通了,所以在这里记录程序,希望它有用。可以使用嫁接来拆分存储库,然后使用 filter-branch 重写树以使嫁接永久化,而不是 rebase 。所以,鉴于 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分支到master project-history 的分支repo,然后将分支 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 的结果拆分内容 repo 。
    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,它的提交日志可以包含一条消息,将用户指向带有项目历史的存储库的位置,允许 future 用户使用 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 
    

    关于git - 如何在特定提交点将 git 存储库拆分为最近和较旧的提交,保留分支?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48179834/

    相关文章:

    assembly - MIPS 什么十六进制字编码分支指令?

    svn - 防止Subversion更改分支的最佳方法

    git - COPY 本地提交到另一个分支

    git - 获取 git 当前分支/标签名称

    node.js - 更改 git 分支时切换 node_modules 文件夹

    javascript - 权限被拒绝,forever.js 作为 bash 脚本?

    Git rebase 并自动更新分支指针?

    git - 有谁知道用于 windows 的 git bash 程序用于其 shell 的是什么?

    svn - Subversion 将未提交的更改提交到新分支

    git - `git rebase` 没有更新分支?