1

I have the following commit history:

  A--B--C branch1
 /     /
I      D
 \
  \
   J branch2

and I want to simply delete commit D, which isn't connected elsewhere.

I tried git rebase --onto B C, but for some reason this deletes C as well and produces

  A--B branch1
 /     
I      
 \
  \
   J branch2
Bananach
  • 2,016
  • 26
  • 51
  • Have a look at https://stackoverflow.com/questions/6975272/how-to-remove-unneeded-git-commits – floverdevel Mar 29 '18 at 19:53
  • @floverdevel D is a parent of C, but the linked question is about removing commits that are not within any branch, so if I'm not mistaken that won't help me – Bananach Mar 29 '18 at 19:57
  • Can you merge squash D into C ? And then delete D. – floverdevel Mar 29 '18 at 20:00
  • @floverdevel do you have a suggestion how I could try doing that? – Bananach Mar 29 '18 at 20:01
  • So `D` is a *root* commit? It's very odd to have a graph structure like that. – torek Mar 29 '18 at 20:53
  • @torek D was created as a child of J, but then J was `reset HEAD~1` – Bananach Mar 29 '18 at 20:56
  • Using `git reset` to change where the *branch name* `branch2` points does not change the parent linkage of `D`, so the graph drawing here is not right: `D` still connects right back to `J`. Fortunately that doesn't affect the answer. – torek Mar 29 '18 at 21:30

2 Answers2

2

TL;DR

Save the hash ID of C somewhere. The simplest method, other than using raw cut-and-pasted hash IDs, is to make a temporary branch or tag name, e.g.:

git branch save branch1

Then force the name branch1 to point to commit B. Assuming you are still on branch1—be sure you have nothing to commit too; use git status to check both of these:

git reset --hard <hash-of-B>

or similar. If branch1 still points to commit C you can use HEAD~1 or HEAD^1 to identify commit B, for instance.

Last, use git cherry-pick to make a copy of commit C, but in which this copy has only one parent. To do so you must tell which of the two parents git cherry-pick should think of as C's (single) parent for the purpose of this copying. The parent to choose will be #1; this is almost always true anyway; and it's definitely true in this case, where C was made by merging D:

git cherry-pick -m 1 save

You can then delete the name save at any point (it was just around to remember the hash ID for commit C).

Note that you could cherry-pick D directly. However, using git cherry-pick -m 1 <thing-to-locate-commit-C> means that if you had to resolve any conflicts when you did the merge, you don't have to re-resolve them now.

Long

Based on comments, the actual graph structure is this:

       A--B--C   <-- branch1 (HEAD)
      /     /
...--I     D
      \   /
       \ /
        J   <-- branch2

but this has no effect on the answer.

I tried git rebase --onto B C, but for some reason this deletes C as well ...

It doesn't technically delete commit C at all; it just arranges for the name branch1 to point to commit B (only). That is, we can redraw the graph like this:

       A---B   <-- branch1 [after rebase]
      /     \
...--I       C   <-- branch1 [before rebase]
      \     /
       \   D
        \ /
         J   <-- branch2

The reason for this is that:

git rebase --onto B C

tells Git:

  1. List all commits reachable from HEAD (using the name HEAD is in-built, part of git rebase itself) that are not reachable from C (from your argument C), minus some commits we can describe later. List them in "reverse topological" order, i.e., older required commits before newer ones.

    In this case, the list is empty: there are no commits starting from HEAD—attached to branch1 with branch1 pointing to commit C—that are not reachable from commit C as well.

  2. Set up a temporary branch pointing to B (from your argument B).

  3. For all commits listed in step 1, copy those commits, building up this new temporary branch.

  4. When done, change the name branch1 to point to the final commit copied.

    Since no commits are copied, the temporary branch still points to B, so the name branch1 is moved to point to B.

You could try to fix this by naming something other than C as "what not to copy". The problem is that you only get one commit ID for "what not to copy". You could specify B, but Git will then try to copy D and J (in the other order: D first, then J). You could specify D, but then Git will try to copy B, A, and I!

The other remaining issue is that we noted above that in step 1, Git omits certain commits. Specifically, it omits all merge commits—it won't try to copy C at all—and it also omits commits that are already in the upstream (through a mechanism I won't try to describe here; it doesn't apply in your case, so it's not important). What this means is that even if you could somehow say "copy C but don't copy B or D", Git would toss C out of the list and still copy nothing at all.

This is why we need an alternative mechanism: a "by hand" rebase, using git cherry-pick. We can cherry-pick D itself. This means re-doing any conflict resolution. Or, we can use git cherry-pick -m 1 <specifier-for-C> to cherry-pick the effect of merging D and doing any conflict resolution, and that's the method in the TL;DR section above.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
1

You can edit the commit object C to remove parent D.

First run:

git log

to see commit-id's of C and D. Then run

git replace --edit <id-of-commit-object-C>

to edit parents of C. An editor will be shown. In the editor, remove the line with parent <id-of-commit-object-C> and save.

After this you will have a replacement commit. To turn replacement commit into permanent, run:

git filter-branch --tag-name-filter cat -- --all