如何将两个分支/仓库的提交/更改合并到同一个提交时间线中?

5

我有一个独特的需求,正在寻找解决方案...我看了很多其他在线解决方案,没有找到一个能满足我的期望...

有两个仓库-X和Y(如下图所示)。 我想永久删除Repo X。 但是有一个文件夹A,我想将其移动到第二个仓库(Y),并且要保留所有Git提交历史记录…可以改变历史记录,并且我希望Repo Y的历史记录声称文件夹A从一开始就存在于Repo Y中,并且对该文件夹A进行的所有更改都是在其内部的Repo Y中进行的。 我不想让Repo Y的历史记录显示文件夹A来自于从另一个分支/仓库合并之后。 我也不想执行rebase操作,因为所有文件夹A的提交(~1000)将被放置在Repo Y现有提交的顶部。

***** Original Commits on the timeline for Repo X and Repo Y *****
Repo Y: -------Cx--------Cy--------Cz----------
Repo X: -----------Ca----------Cb------Cc------  (only commits that touch Folder A)

我找到了一个命令来筛选分支并重写repo X的提交记录,使得根据git的记录,只有文件夹A的内容存在。
git filter-branch --subdirectory-filter ./A -- --all

然后使用以下脚本,将“文件夹 A”中的所有内容移动到名为“A”的文件夹中,位于 Repo X 中。
# https://gist.github.com/xkr47/f766f4082112c086af63ef8d378c4304
# placed the script above in PATH and executed the following command at Repo X
git filter-mv 's!^!A/!'

现在我有仓库Y,需要从仓库X中移动带有所有Git提交历史记录的文件夹A到该仓库。

请注意,所有对仓库X的提交都是针对文件夹A进行的,而这个文件夹在仓库Y中根本不存在,因此不可能存在冲突提交的情况。

现在,我可以将本地仓库X标记为远程仓库,并进行合并或变基。

# repo X lives in the same directory as repo Y
# commands run inside repo Y
git remote add repoX ../X

git pull repoX <branch> --allow-unrelated-histories
# or
git pull repoX <branch> --rebase

合并

***** MERGE's output *****
Repo Y: -------Cx--------Cy-------Cz-----(MERGE)----
                                         /
                                        /
              -----Ca---------Cb------Cc  (Folder A commits)

提交记录的时间轴将保留,但将分别在不同的轨道上进行,直到两者合并。在合并提交之前,我无法同时查看两个仓库中的内容,因为它们将在单独的提交线上。 (我希望有文件夹A,在存储库Y中具有其经历的所有更改以及其提交行)
(……这样,我可以回到存储库Y的任何时间点,并根据提交的时间看到Repo Y和Folder A的演变) REBASE:变基
***** REBASE's output *****
Repo Y: -------Cx--------Cy--------Cz------Ca-----Cb-----Cc----

但是我不希望来自Repo X的提交出现在Repo Y的最新提交之后... 在我的用例中,Repo X将有大约1000个提交,一次性对它们进行变基将破坏Repo Y的提交时间线。我希望根据提交发生的时间,将它们与Repo Y的提交合并在一起。
***** Expected Output *****
Repo Y: -------Cx--Ca----Cy----Cb--Cz--Cc------  (Repo Y commits fused with commits from repo X - only adds a folder (A) to repo Y)

我知道干涉Git历史记录并不是一件好事。但是针对我的使用场景,这可能是我能做的最好的事情...

1
“X/A” 的历史记录中修改的文件与 “Y” 的历史记录中修改的文件完全不同吗?还是有些文件会在两个仓库中都被修改过? - LeGEC
2
a. 仓库Y的历史记录完全是线性的吗?还是有多个分叉和合并点(例如:您是否使用合并请求来推进主分支)?b. 您只需要重写一个单独的分支(即master分支)吗?还是您还有从它派生的其他分支? - LeGEC
1
@HariR git rebase默认情况下不会保留合并提交。从两个仓库的根提交开始,将存储库历史记录完全线性化是否可行? - terrorrussia-keeps-killing
1
如果您的git历史记录不是线性的,而提交图很重要,那么从另一个存储库中选择提交仍然可以工作,但是手动从另一个历史记录中选择提交将非常繁琐,并且需要非常高的注意力以便按正确顺序移植提交并且不会丢失任何内容。我相信它也可以为git rebase --rebase-merges输出编写脚本(生成的待办事项序列似乎可以使用诸如awk之类的工具进行解析,而像sort这样的工具则会失败),但我不确定这样的脚本有多复杂。 - terrorrussia-keeps-killing
“正确的顺序”是指您期望在融合存储库中拥有的顺序(很可能是按时间顺序排列,这不能通过git rebase完成而需要额外的工作,正如您在问题中提到的)。我将尝试使用非线性存储库场景更新答案,但正如我上面提到的,我不知道如何编写脚本,因此最终可能需要进行极其繁琐的手动工作来编辑rebase TODO序列。 - terrorrussia-keeps-killing
显示剩余7条评论
2个回答

1

线性历史方案

如果您的“Repo Y”是单分支的线性历史(没有合并;但可以通过简单的 git rebase 将合并操作线性化),那么我能想到的一个解决方案是用包含正确顺序的合并提交的新的 rebase 序列替换生成的 rebase 序列。

  • 在您的“Repo Y”上,确保您正在 master 分支上,并标记当前的 master 分支提交以防出现问题:
git checkout master
git tag old-master-before-fuse
  • 将“Repo X”拉取到仓库中,使得该仓库能够识别两个历史记录中的对象:
git remote add X ...path-to-your-Repo-X...
git fetch X
  • 通过将两个仓库中的提交从最旧到最新排序,准备交互式变基序列:
git rev-list --format='%at %ct %H' refs/heads/master refs/remotes/X/master \ # produce a table of all commits from both repositories (fields: author timestamp, committer timestamp, commit hash)
    | grep -P '^\d+' \         # don't know to remove the "commit XXXX" lines from the output above otherway
    | sort -k1 -n \            # sort by the author timestamp
    | cut -d' ' -f3 \          # take hashes only
    | while read LINE; do      # prepare each hash for the interactive rebase sequencing
        echo "pick $LINE"
    done \
> .git-rebase-todo # save it to a temporary file to be used later
  • 从第一个提交开始启动交互式变基:
git rebase -i --root

一旦您的编辑器出现,使用单个"break"命令删除所有"pick"命令,然后退出编辑器。Git将在此停止(git rebase --continue不需要进一步操作即可使master分支为空白)。

  • 用先前生成的文件.git-rebase-todo替换当前的待办事项序列,并继续重新设置基础:
cp .git-rebase-todo .git/rebase-merge/git-rebase-todo
git rebase --continue

这将使得主分支包含融合后的提交历史记录。
检查主分支,如果一切顺利,删除检查点标签(当然要小心使用:git tag -d old-master-before-fuse)和远程链接(git remote remove X)。

以下是我用于测试的代码库转储:

"X 代码库"

blob
mark :1
data 0

reset refs/heads/master
commit refs/heads/master
mark :2
author - <-> 1577886449 +0200
committer - <-> 1606398449 +0200
data 3
Cx
M 100644 :1 Cx

commit refs/heads/master
mark :3
author - <-> 1580564850 +0200
committer - <-> 1606398450 +0200
data 3
Cy
from :2
M 100644 :1 Cy

commit refs/heads/master
mark :4
author - <-> 1583070451 +0200
committer - <-> 1606398451 +0200
data 3
Cz
from :3
M 100644 :1 Cz


"Repo Y"
blob
mark :1
data 0

reset refs/heads/master
commit refs/heads/master
mark :2
author - <-> 1577972852 +0200
committer - <-> 1606398452 +0200
data 3
Ca
M 100644 :1 Ca

commit refs/heads/master
mark :3
author - <-> 1580651253 +0200
committer - <-> 1606398453 +0200
data 3
Cb
from :2
M 100644 :1 Cb

commit refs/heads/master
mark :4
author - <-> 1583156854 +0200
committer - <-> 1606398454 +0200
data 3
Cc
from :3
M 100644 :1 Cc

可以使用git fast-import来恢复转储。

对于这两个示例存储库,fuse例程将产生以下git log --oneline结果:

7efcbbd (HEAD -> master) Cc
6cc905d Cz
a20f00e Cb
6650162 Cy
e8e9bf1 Ca
40678ea Cx

感谢您详细的回答。我会仔细查看并尽快回复。 - Hari R
由于我的仓库 Y 不是线性的,所以在变基操作期间我一直遇到合并冲突...提交图非常庞大,重新设置主分支确实非常繁琐,这也是最后的手段。 - Hari R

1

非线性历史情境

如果您的历史记录不是线性的,并且让它们变成线性的不是一个选项(您不想这样做或者这会导致许多冲突,这些冲突可能会因错误而解决),您也可以尝试通过手动编辑交互式变基序列来融合存储库。

假设您的存储库Y(主机)具有以下主分支图:

# Cx
touch Cx && git add Cx && git commit -m Cx Cx
git branch -m y/master
# Cy
git checkout -b y/master-Cy && touch Cy && git add Cy && git commit -m Cy \
    && git checkout - && git merge --no-edit --no-ff y/master-Cy && git branch -d y/master-Cy
# Cz
git checkout -b y/master-Cz && touch Cz && git add Cz && git commit -m Cz \
    && git checkout - && git merge --no-edit --no-ff y/master-Cz && git branch -d y/master-Cz

*   478d271 (HEAD -> y/master) - Merge branch 'y/master-Cz' into y/master 3 seconds ago 
|\  
| * 93fbaf8 - Cz 3 seconds ago 
|/  
*   e80b2ea - Merge branch 'y/master-Cy' into y/master 6 seconds ago 
|\  
| * a1842d1 - Cy 6 seconds ago 
|/  
* 9467260 - Cx 31 seconds ago 

为了简洁起见并省略了仓库X的源获取阶段,假设我们可以将其直接构建在同一仓库中。
git checkout --orphan x/master
git reset --hard
# Ca
touch Ca && git add Ca && git commit -m Ca Ca
# Cb
git checkout -b x/master-Cb && touch Cb && git add Cb && git commit -m Cb \
    && git checkout - && git merge --no-edit --no-ff x/master-Cb && git branch -d x/master-Cb
# Cc
git checkout -b x/master-Cc && touch Cc && git add Cc && git commit -m Cc \
    && git checkout - && git merge --no-edit --no-ff x/master-Cc && git branch -d x/master-Cc

*   e271022 (HEAD -> x/master) - Merge branch 'x/master-Cc' into x/master 1 second ago 
|\  
| * a403baf - Cc 1 second ago 
|/  
*   c34f424 - Merge branch 'x/master-Cb' into x/master 4 seconds ago 
|\  
| * 6107552 - Cb 4 seconds ago 
|/  
* 70d7b2d - Ca 7 seconds ago 

那时有两个分支:y/master代表您的主机存储库主分支,x/master代表您的外部存储库主分支。现在,让我们为两个主分支准备交互式变基TODO序列:
git checkout y/master
git rebase -i --rebase-merges --root

l onto

# Branch y-master-Cy
t [new root]
p 9467260 Cx
l branch-point
p a1842d1 Cy
l y-master-Cy

# Branch y-master-Cz
t branch-point # Cx
m -C e80b2ea y-master-Cy # Merge branch 'y/master-Cy' into y/master
l branch-point-2
p 93fbaf8 Cz
l y-master-Cz

t branch-point-2 # Merge branch 'y/master-Cy' into y/master
m -C 478d271 y-master-Cz # Merge branch 'y/master-Cz' into y/master

请将其保存为文本文件,命名为y-master.txt,并放在某个地方。

git checkout x/master # or git checkout to the foreign repository remote ref (that leads to a detached HEAD but it's definitely not an issue)
git rebase -i --rebase-merges --root
git checkout y/master

l onto

# Branch x-master-Cb
t [new root]
p 70d7b2d Ca
l branch-point
p 6107552 Cb
l x-master-Cb

# Branch x-master-Cc
t branch-point # Ca
m -C c34f424 x-master-Cb # Merge branch 'x/master-Cb' into x/master
l branch-point-2
p a403baf Cc
l x-master-Cc

t branch-point-2 # Merge branch 'x/master-Cb' into x/master
m -C e271022 x-master-Cc # Merge branch 'x/master-Cc' into x/master

请保存这个文件,x-master.txt
现在,假设您已经为y/master创建了一个快照标签(以避免崩溃原始分支),"唯一"的事情现在就是"编程"融合库序列。
git rebase -i --rebase-merges --root

(选项卡命令表示X存储库命令。FOREIGN-前缀是手动插入的。)
l onto

t [new root]
p 9467260 Cx

    # DON'T or the `9467260 Cx` will get lost: t [new root]
    p 70d7b2d Ca

l branch-point
p a1842d1 Cy
l y-master-Cy
t branch-point # Cx
m -C e80b2ea y-master-Cy # Merge branch 'y/master-Cy' into y/master

    l FOREIGN-branch-point
    p 6107552 Cb
    l FOREIGN-x-master-Cb
    t FOREIGN-branch-point # Ca
    m -C c34f424 FOREIGN-x-master-Cb # Merge branch 'x/master-Cb' into x/master

l branch-point-2
p 93fbaf8 Cz
l y-master-Cz
t branch-point-2 # Merge branch 'y/master-Cy' into y/master
m -C 478d271 y-master-Cz # Merge branch 'y/master-Cz' into y/master

    l FOREIGN-branch-point-2
    p a403baf Cc
    l FOREIGN-x-master-Cc
    t FOREIGN-branch-point-2 # Merge branch 'x/master-Cb' into x/master
    m -C e271022 FOREIGN-x-master-Cc # Merge branch 'x/master-Cc' into x/master

这将会导致类似以下的结果:
*   cbbf73b (HEAD -> y/master) - Merge branch 'x/master-Cc' into x/master 10 minutes ago 
|\  
| * b143f45 - Cc 10 minutes ago 
|/  
*   bd0c586 - Merge branch 'y/master-Cz' into y/master 3 hours ago 
|\  
| * dfb5e4e - Cz 3 hours ago 
|/  
*   01f078d - Merge branch 'x/master-Cb' into x/master 10 minutes ago 
|\  
| * 26b2f61 - Cb 10 minutes ago 
|/  
*   f1105a4 - Merge branch 'y/master-Cy' into y/master 3 hours ago 
|\  
| * aa67233 - Cy 3 hours ago 
|/  
* 7a23a6e - Ca 10 minutes ago 
* 9467260 - Cx 3 hours ago 

这看起来很像你在问题描述中所描述的提交顺序,并包括测试合并。它可能可以以某种方式脚本化,但是这种方法的脚本编写肯定超出了我的能力范围。
上述方法没有经过充分测试,可能包含错误或其他使得你的合并历史出错的问题。
即使存在文本冲突(这可以更轻松地解决),线性化两个历史记录并将它们合并在一起要容易得多。分歧的提交需要你介入到变基序列中。

这里还有一个 gawk 脚本,可以帮助您为生成的变基序列中出现的每个提交构建时间轴时间戳作为注释:

#!/usr/bin/awk

function print_with_extra(orig_line, object) {
    cmd = "git rev-list -1 --format='%ai%n%ci' " object
    author_date = ""
    committer_date = ""
    i = 0
    while ( (cmd | getline line) > 0 ) {
        switch ( ++i ) {
        case 2: author_date = line; break
        case 3: committer_date = line; break
        }
    }
    close(cmd)
    print orig_line " # (" author_date ") (" committer_date ")"
}

{
    switch ( $0 ) {
    case /^p [0-9a-f]+/: print_with_extra($0, $2); break
    case /^m -C [0-9a-f]+/: print_with_extra($0, $3); break
    default: print; break
    }
}

使用示例:gawk -f timestamps.awk y-master.txtgawk f- timestamps.awk x-master.txt。以下是示例输出:

l onto

# Branch x-master-Cb
t [new root]
p 9979de5 Ca # (2020-11-30 15:03:26 +0200) (2020-11-30 15:03:26 +0200)
l branch-point
p 4dcff42 Cb # (2020-11-30 15:03:26 +0200) (2020-11-30 15:03:26 +0200)
l x-master-Cb

# Branch x-master-Cc
t branch-point # Ca
m -C 4de8360 x-master-Cb # Merge branch 'x/master-Cb' into x/master # (2020-11-30 15:03:26 +0200) (2020-11-30 15:03:26 +0200)
l branch-point-2
p 206f5a8 Cc # (2020-11-30 15:03:26 +0200) (2020-11-30 15:03:26 +0200)
l x-master-Cc

t branch-point-2 # Merge branch 'x/master-Cb' into x/master
m -C c3ac7e5 x-master-Cc # Merge branch 'x/master-Cc' into x/master # (2020-11-30 15:03:26 +0200) (2020-11-30 15:03:26 +0200)

谢谢。这是一座信息宝藏。 - Hari R

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接