0

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?

klor
  • 1,237
  • 4
  • 12
  • 37
  • 3
    Do you want to change the parent commit of all commits in the branch, or do you just want to change the name of the branch? You've used the word "branch" many times to refer to what appear to be commits, based on your diagrams. – user229044 Jan 09 '18 at 13:41
  • Possible duplicate of [Rebasing a tree (a commit/branch and all its children)](https://stackoverflow.com/questions/17315285/rebasing-a-tree-a-commit-branch-and-all-its-children) – Melebius Jan 09 '18 at 13:51
  • Do `A` and `O` share any common history? – larsks Jan 09 '18 at 13:53
  • No. A and O is not related, no common history. Of course as a result of rebase conflicts has be to solved. – klor Jan 09 '18 at 14:08
  • In the Rebasing a tree (a commit/branch and all its children) - https://stackoverflow.com/questions/17315285/rebasing-a-tree-a-commit-branch-and-all-its-children question the A & H are related. Thus it is different from my question. – klor Jan 09 '18 at 14:11
  • So you're saying you tried the solutions there and they definitely don't work? Or you're just assuming they won't work because your situation is different? – Useless Jan 09 '18 at 14:18
  • The answer to the other question assumes rebase is the right tool, which it might or might not be. Whether with rebase or with filter-branch, the process isn't really affected by whether there is common history. – Mark Adelsberger Jan 09 '18 at 14:37

1 Answers1

0

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 rebaseing 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 - i.e. 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.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • Really detailed answer! Thank you! My problem with rebase is, that I have to repeat the rebase onto for each branch. I learned this from your answer. But I need a whole solution, which makes exact tree copy automatically, no matter if there are 10 branches or 100 or 1000. – klor Jan 09 '18 at 16:54
  • @klor - Right, the desire to automate across many branches/refs and arbitrarily complex histories is where the `filter-branch` option comes in, but then the problem becomes making sure the differences between `A` and `O` stay in place throughout the rewritten tree. There's really no great, easy solution. You could get close with a lot of custom programming, but even so (potentially lots of) conflicts requiring manual intervention might be possible (depending on a lot of things). It takes a significant problem for this to be worth attempting... – Mark Adelsberger Jan 09 '18 at 17:30
  • @klor - ... In the specific cases where it's likely to be really important to rewrite history (sensitive info and/or binary bloat in `A`), there are tools like the BFG Repo Cleaner designed to address those cases, which may be of help. In most other cases, it may be better to just apply the changes from `A` to `O` in a new branch, and merge that to each of your existing branches. The result history won't be as "neat", but it's a far less troublesome procedure. – Mark Adelsberger Jan 09 '18 at 17:32
  • 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. Is it really an utopistic request? – klor Jan 09 '18 at 17:48
  • I will try to send a feature request to the git developers. – klor Jan 09 '18 at 19:12
  • @klor - I disagree with the assumption that this is a basic function. As to your assessment of how difficult it would be, it seems to rest on the idea that a commit consists of a set of changes; this is incorrect. A commit is a snapshot (which in some cases is compared to its parent to yield a set of changes). To rebase a commit tree, every snapshot must be modified to reflect the diff between the old base and the new base. – Mark Adelsberger Jan 10 '18 at 01:27