简体   繁体   中英

Rebasing a tree (a commit/branch and all its children) to an unrelated branch?

Old subject: How to move a branch slice to an unrelated commit?

New subject: Rebasing a tree (a commit/branch and all its children) to an unrelated branch?

Example:

A - B - C - F - G - J - K
          \       \
           D - E   H - I

O - 

I would like to move B, C, D, E, F, G, H, I, J, K commits to O branch, while keeping the branch tree.

Result should be:

A -

O - B - C - F - G - J - K
          \       \
           D - E   H - I

How to do this?

Thanks!

EDIT1: I need a whole solution, which makes exact tree copy automatically, no matter if there are 10 branches or 100 or 1000.

EDIT2: Solution should work on both Linux (I use Debian server) & Windows (MSYS2 is acceptable, as on workstation as I use https://github.com/git-for-windows/git-sdk-64 )

EDIT3: The rebasing/moving a tree should be really basic part of git. Proposing, that conflicts are resolved between A & O, the remaining tree should be a simple copy tree from B to O & delete tree slice above A.

I imagine "Move tree" could work like this:

1) create O orphaned commit
2) Make diff between A & O. 
3) Commit diff between A & O onto O, named AO commit.
4) Rebase A and all child commits (B, C, etc.) of all branches onto AO
5) Delete child commits & branches of A

Is it really an utopistic request?

One thing that's not clear from your picture is what branches (or other refs) exist and need to be moved. You refer to "B, C, D, E, F, G, H, I, J, K branches" and "O branch", but in the diagram you've drawn those appear to be commits. I'll give directions assuming something like

A - B - C - F - G - J - K <--(master)
          \       \
           \       H - I <--(branch_X)
            \
             D - E <--(branch_Y)

O <--(new_root)

So here simply there is a branch for each commit that has no children.

Now there are a few ways to proceed, but before we start there is one important thing to understand. You cannot "move" a commit. You can create a new commit to apply, onto O , the same changes as were originally made over A , by an old commit. This is called rebasing. What you get is a new commit with a new ID.

I guess this can be important if you have tools or documentation that care about commit ID values, but more generally it means that you'll end up "rewriting" the histories of branches, so if you afterwards want to push to a remote you'll probably have to "force push" ( push -f ), and this would then require clean-up on any other clone of the remote (see "recovering from upstream rebase" in the git rebase docs).

So the closest thing you can get to what you described would be something like

A <--(old_root)

O - B' - C' - F' - G' - J' - K' <--(master)
           \         \
            \         H' - I' <--(branch_X)
             \
              D' - E' <--(branch_Y)

And actually, it can take a considerable number of extra steps to actually get rid of the original commits; by default you'd get something more like

A - B - C - F - G - J - K
          \       \
           \       H - I
            \
             D - E

O - B' - C' - F' - G' - J' - K' <--(master)
           \         \
            \         H' - I' <--(branch_X)
             \
              D' - E' <--(branch_Y)

where all of the commits in the A tree are unreachable and so don't show in default output, but they do still exist (for a while, at least). If this is a problem - for example, if the reason for all this is to remove some sensitive information or binary bloat in A - then extra steps are needed after the commits and refs have been rewritten.

But first, how to do the rewrite? There are basically two options.

You might be able to use git filter-branch . If there are many branches, if there are merges that need to be included in the rewritten history, or if there are tags, then this is possibly the easier approach. But the more difference there is between A and O , the harder it is to do this correctly.

The other option is to rebase. In a way this is more direct, because it calculates a patch for each commit and then applies that patch to the new base; so the differences between A and O are taken care of automatically. (Actually, I should say "as automatically as possible"; conflicts may arise as each patch is applied, based on the differences between A and O .) But you have to rebase each branch, and you have to move tags manually, and rebase doesn't handle merges well.

If you decide to try filter-branch

The thing you'll most definitely need is --parent-filter . The git filter-branch docs have several examples of how to re-parent a commit; in this case you'd re-parent B from A onto O .

But a parent filter isn't enough. You also have to transform the commit content ( TREE ) to account for differences between A and O . For example, if the point is that O omits a file, you could use an index-filter like

git filter-branch --parent-filter \
                    'test $GIT_COMMIT = <commit-id> && echo "-p <graft-id>" || cat' \
                  --index-filter \
                    'git rm --cached --ignore-unmatch path/to/unwanted/file` \
                  -- --all

(This assumes that the rest of the commits don't later introduce new content you want to keep at that path.)

For more complex transformations (like adding files) it might be easier to use tree-filter instead of index-filter , though this is slower.

If there are tags and you just want to move them, you can use --tag-name-filter cat

See the git filter-branch docs for more details. https://git-scm.com/docs/git-filter-branch

If you decide to try rebase

The easiest case is rebase ing a single branch that can reach no merge commits. For example, from our example you could say

git rebase --onto O A master

replacing each of O and A with either the SHA ID, or some other name or expression that resolves to the SHA ID, of the corresponding commit. (For example you could say the branch name new_root for O . You could tag A to make it easier to reference, or use something like master~6 .

Then you have

A - B - C - F - G - J - K
          \       \
           \       H - I <--(branch_X)
            \
             D - E <--(branch_Y)

O <--(new_root)
 \
  A` - B` - C` - F` - G` - J` - K` <--(master)

Note that J and K are unreachable so don't show up by default in log output, etc. You can refer to K as master@{1} (using the reflog to see where master was previously).

Then you would want to move branch_Y . So you need to get an identifier for C' as it will be the new base for the rewritten branch. In this example that could be an expression like master~4 .

git rebase --onto master~4 master@{1} branch_Y

gives us

A - B - C - F - G - J - K
          \       \
           \       H - I <--(branch_X)
            \
             D - E

O <--(new_root)
 \
  A` - B` - C` - F` - G` - J` - K` <--(master)
              \
               D' - E' <--(branch_Y)

and continue for each additional branch.

If there are tags you can move them manually

git checkout new-commit-to-tag
git tag -f tag-name

If this history contains commits, this is a bigger problem. By default rebase will try to make a linear history, You can override this with --preserve-merges , but then rebase will assume each merge is the natural product of its parents - ie it will assume no "evil merges". For this reason you should validate the result if you've rebased through a merge, or may want to "rebase in segments". For example, given

... A -- B -- C
                \
                 M -- G <--(master)
                /
... D .. E .. F

you might create temporary branches at C and F ; then rebase C , rebase F , manually recreate the merge (making sure any "evil" changes are accounted for), then finally rebase the commits after the merge.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM