-3

Working in branch temp, I created a new branch, temp2. I rearranged a file heavily and then did some cleanup on it. I committed.

Later it occurred to me that I'd have a clearer history if I had done the rearrangement, committed, then did the cleanup, then committed.

In other words, I just want to turn this commit into two stages, two commits.

Well, I remembered the rearrangement part. So I switched to branch temp and did the rearrangement again, without the cleanup. My idea was that I could then bring the commit from temp2 onto temp.

What I have:

------ before ----- rearrangement <-- temp
              \
               \ 
                \
                 ------- rearrangement plus cleanup <-- temp2

What I want:

------ before ----- rearrangement ----- rearrangement plus cleanup <-- temp

First I tried rebasing temp2 onto temp, but I got a merge conflict. This surprised me because wasn't asking to merge anything. I thought rebasing just changed where a commit was attached. So I aborted that.

Then I tried cherrypicking "rearrangement plus cleanup" from temp2 into temp. But I still got a merge conflict. That really surprised me because I thought it was completely impossible. So I aborted that too.

I'm stuck. I don't understand what's going on here. git is not based on diffs. Commits are just snapshots. So why is there a conflict? All I'm asking to do is arrange these snapshots in a certain order. Why is that so difficult, and how can I do it?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I think you are looking for an interactive rebase. https://stackoverflow.com/questions/2740537/reordering-of-commits – intboolstring Jul 27 '18 at 20:10
  • @intboolstring I don't see why. I don't see why it needs to be interactive, and I don't see what it has to do with the interactive rebasing I've done in the past, which involved the _opposite_ of what I'm describing, i.e. squashing. – matt Jul 27 '18 at 20:12
  • Interactive rebasing has many more features than just squashes (reword, reorder, fixup, drop, etc.) – intboolstring Jul 27 '18 at 20:13
  • @intboolstring And none of those names means anything to me. Which do you say I need here? – matt Jul 27 '18 at 20:23
  • Reorder I think... – intboolstring Jul 27 '18 at 20:25
  • @intboolstring But they are on different branches. The challenge that I'm failing to meet is getting them into the same branch to begin with. – matt Jul 27 '18 at 20:29
  • @matt in fact git is Really based on diff, each commit is just a patch, snapshot is recreated by applying series of patches in that branch – William Chong Jul 27 '18 at 20:37
  • @WilliamChong My understanding is, that's how svn works, not how git works. In git, a commit is the full content of every file that has changed (plus a hash). – matt Jul 27 '18 at 20:45
  • It seems conceptually you are correct, but in this particular context, you are better off thinking them as diff: https://stackoverflow.com/questions/40617288/a-commit-in-git-is-it-a-snapshot-state-image-or-is-it-a-change-diff-patch-delta & https://stackoverflow.com/questions/27760257/cherry-picking-commit-is-commit-a-snapshot-or-patch – William Chong Jul 27 '18 at 20:58

2 Answers2

2

Haven't tried it in a command line, but the steps should be similar:

  1. Hard reset to temp2. Just to make sure you are on temp2 without extra diff.
  2. Mixed reset to temp1. Now you are on temp1, but with temp2's data as a diff.
  3. Commit the current diff as a new commit rearrangement plus cleanup on temp1.
  4. Viola.
William Chong
  • 2,107
  • 13
  • 24
  • Mmmm, okay, but there's something I didn't tell you. "rearrangement plus cleanup" is now several commits in temp2. So it's not enough to have "the diff" hanging around in index space; I actually want the commits themselves. – matt Jul 27 '18 at 20:23
  • baseline approach, you could repeat above step1 for each commit in temp2, repeat a few times and recreate the whole commit tree onto temp1. However you could also just do these steps for conflict inducing commits, and simply cherrypick other clean commits onto temp1 – William Chong Jul 27 '18 at 20:39
  • Okay, I understand what you're saying, thanks. I still don't understand why what I'm doing needs to be "conflict-inducing", though. I'm not trying to _blend_ two commits, I'm just trying to move one commit from one branch onto the end of another. – matt Jul 27 '18 at 20:55
  • cherrypicking and rebasing, etc are all diff-based, despite commits conceptually are `snapshots` – William Chong Jul 27 '18 at 20:59
  • This seemed promising, and I tried it, but for some reason I ended up with two copies of all the rearranged material. In the end I just gave up. – matt Jul 28 '18 at 01:31
2

TL;DR

Use:

git checkout temp
hash=$(git log --no-walk --pretty=format:%B temp2 | git commit-tree -p temp temp2^{tree})
git merge --ff-only $hash

to copy commit C to a new commit C' that re-uses the log message and tree of C, and then adjust the branch name temp to point to the new commit.

Long

I thought rebasing just changed where a commit was attached ...

That's the key error!

If I take your diagram and shrink the names down, we start with:

A--B  <-- temp
 \
  C   <-- temp2

What git checkout temp2; git rebase temp does—or tries to do—is to copy commit C to new commit C' that is "like C" in some ways, but has B as its parent. The final result, if it had been successful, would be:

     C'  <-- temp2
    /
A--B   <-- temp
 \
  C   [abandoned]

You already knew that, I think; where you went wrong is the idea of how the copying takes place.

What git rebase does is, in essence, to run git cherry-pick. What git cherry-pick does is—well, from a logical point of view; the underlying mechanism is to use Git's merge machinery—to turn a commit into a change, and then apply that change elsewhere. So Git will compare (diff) commit C vs commit A, and then also diff commit B vs A, and attempt to combine the two change-sets to produce commit C' atop B. These two change-sets overlap, of course, and that's why you get a merge conflict.

You don't have to use git rebase to achieve this. Since C is already the final result you want, you can simply copy C's snapshot—and perhaps its commit message as well—to a new commit C'. There is no user-oriented Git command to do this, but there is a low-level command that does exactly this. These low level commands are what Git calls plumbing commands.

In particular, git commit-tree writes a commit object. To do so, it needs an existing tree object (snapshot) plus some set of parent hashes. Normally we might make a tree with the plumbing command git write-tree, which writes out the current index, but we already have a perfectly suitable final result in existing commit C. We just need to obtain its tree hash ID. The gitrevisions syntax for this is temp2^{tree}, since the name temp2 points to commit C.

The parent we want for our copy of commit C is of course commit B. The name temp suffices since it points to commit B at the moment. We also must supply to git commit-tree the commit log message, on standard input, or as an argument or a file. git log --no-walk --pretty=format:%B will obtain the log message from the given commit.

Once we have made the new commit object, we must hurry up and make some name remember that object, within our grace period (for git gc / git prune), which is 14 days by default. To do that, we can use git merge --ff-only to advance the current branch name (i.e., temp).

(Note that you can do this whole thing as a one line expression, provided you're sure the git log ... | git commit-tree is all going to work.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks for the explanation! I was too scared to do what you suggested but I'm sure it will help others. – matt Jul 28 '18 at 01:32
  • @matt: except for the fast-forward operation at the end, this is just copying a commit. If you like, instead of doing the fast-forward, point a branch name at the copy: `git branch temp3 $hash`. Then `git log temp3` and see if you like the result. – torek Jul 28 '18 at 04:29
  • it has taken a while but I finally understand what you're telling me. As you rightly say, my problem was really that I wasn't understanding that both cherry-picking and rebasing do not _move_ commits (i.e. they do not rearrange the object graph) — they _replay_ commits, i.e. they try to patch the same change onto the end of a different spot in the graph as a completely different commit. I never really knew what "replay" means. – matt Dec 02 '18 at 20:39