user2568374
user2568374

Reputation: 1306

Delete Git Commits - most recent one and an older one

I have seen multiple answers on deleting most recent commit but can't find any where an older commit is deleted as well.

I have the last four commits as:

bc8354f Revert Merge branch 'repo/develop' into M372046"
edce31a Good commit2 M372046
926b7de Good commit1 M372046
422f6cb Merge branch 'repo/develop' into M372046

I want to delete bc8354f and 422f6cb without affecting others.

I have tried : git rebase -i HEAD~4 but it only shows bc8354f and not the other one. If i delete that commit using d, drop= remove commit, it puts me to a HEAD reference of that original branch and don't know how to make it take effect, nor do I know how to delete the 422f6cb commit.

If I try to push this HEAD ref to the original (M372046) I get : This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.

error: failed to push some refs to ...

I have also tried deleting the entire line that starts with "pick" when doing a rebase but when I save it it says "Nothing to do" and no changes are made.

Upvotes: 0

Views: 84

Answers (2)

torek
torek

Reputation: 487735

TL;DR

git branch tmp
git reset --hard HEAD~4
git cherry-pick tmp~2
git cherry-pick tmp~1

after which you should inspect the result carefully, and if all looks good, delete branch tmp. You must then force-push (git push -f or equivalent) to discard all the work everyone (including you) has done since the commit you git reset to, in favor of the copies you just made. Be sure no one else has done work and pushed commits that must be kept.

(Check the long answer below, and make sure the graph really is the way I have drawn it, before blindly doing this.)

Long

Technically, you cannot delete a commit at all. You can only stop using it (and if you've removed all names by which Git can find it, it will eventually be recycled via git gc, the Grim Reaper Collector).

I want to delete bc8354f and 422f6cb without affecting others.

You cannot do that at all, for the technical reason above. Each existing commit is quite unchangeable. Meanwhile, commit 926b7de, which is one you do not want to affect, lists commit 422f6cb as its parent.

If 422f6cb did not exist—if you somehow deleted it—commit 926b7de would become generally unusable, as it would list a parent that does not exist. (Git can do a few things with a broken commit like this, but the repository would be defective.)

What this means is that in order to delete the merge and its reversion, you must be willing to copy the two commits you would like to retain. The copies will be different in at least one respect: the copy of 926b7de (which will have some different hash-ID) will list some other parent than 422f6cb.

There are some bits-and-pieces missing here, that we (or you) will need to find out / fill in, in order to do what you want. The first step in almost anything like this, though, is to draw, on paper or a whiteboard or some such, the commit graph, or enough of it to matter. Draw each commit as a node with arrows coming out of it. The arrows should point to the previous node(s): the parent(s) of that commit.

I like to put the commits right (latest) to left (oldest), with the arrows pointing leftward. You can choose some other orientation: for instance, git log --graph starts with the newest commits at the top, and the arrows point downward.

An ordinary commit has only a single parent. The last three of your commits here are ordinary:

             <-926b7de  <-edce31a  <-bc8354f

Let's replace these with single uppercase letters since the hash IDs are clunky. We can use E and B for the last two, since that's the first character of their actual hash IDs, and B can stand for Bad. Let's use D for the first of these three, since it ends in DE:

             <-D        <-E        <-B

Now we have 422f6cb Merge branch 'repo/develop' into M372046, though. This one is a merge commit. A merge commit is special in that it has two (or more, but ordinary merges only have two) parents.

I don't know their hash IDs so I'll just draw M, the Merge commit, this way:

...
   \
    M        <-D        <-E        <-B
   /
...

and then I'll tweak the drawing a bit for reasons we'll see in a moment, and add some branch names as well:

...--o
      \
       M--D--E--B   <-- yourbranch (HEAD)
      /
...--o   <-- repo/develop

Note that the branch names, yourbranch—whatever branch this is; it's the one that your HEAD is attached to—and repo/develop, point to one specific commit. This is the tip commit of the corresponding branch. This is how Git finds the commits: it starts at the tip, as found by the branch name, and then uses the commit hashes embedded in each commit to work backwards through this graph.

Now, you want to get rid of commit M (the merge) and commit B (the bad commit that undoes the merge) entirely. You can do this, sort of, by simply forgetting about them by making the name yourbranch point directly to whichever commit comes before M on the top row:

...--o   <-- yourbranch (HEAD)
      \
       M--D--E--B   [abandoned]
      /
...--o   <-- repo/develop

There is now no name for commit B, so you won't find it. But since you won't find B, you won't find E and D either. Of course that also means you won't find M, but that's OK: you wanted that! But dropping D and E is the actual problem. So we will need to fix that.

First, though, let's learn how to move the branch name, yourbranch (to which your HEAD is attached), so that it points to the commit just before M along the top row. The command that does this is git reset. The git reset command does a lot of things—too many things, in my opinion—but this is one of the things it will do. We make sure that we have no unsaved work and run:

git reset --hard <hash>

to change the branch name to which our HEAD is attached so that it points to the given <hash> commit. So we must find the appropriate parent, and do the git reset, and we'll have the picture we want as our intermediate step.

Now—well, technically, before we do this—we must save the hash IDs of the commits we want to copy, or attach a name to the last one, or anything that comes after the last one. The easier method, in general, is to attach a name, because then we can use git rebase -i --onto to copy commits en-masse. Let's do it the harder way instead, though, because that's more instructive.

The command that copies a commit is git cherry-pick. It turns a commit—a snapshot—into a set of changes, by comparing the commit to its parent. That is, given commit E, it will compare D vs E to see what you changed. It will save those changes away, and then look at your current commit—the one you have checked-out right now—and apply the same changes and then make a new commit.

Now, before we run git reset, let's attach a second branch name tmp here, using git branch:

git branch tmp

...--o
      \
       M--D--E--B   <-- yourbranch (HEAD), tmp
      /
...--o   <-- repo/develop

Then when we do the git reset --hard, tmp will remain:

...--o   <-- yourbranch (HEAD)
      \
       M--D--E--B   <-- tmp
      /
...--o   <-- repo/develop

The name tmp will keep the commits alive.

There's a fancy way to name the commit on the top row. We know that tmp and, before the git reset command anyway, yourbranch aka HEAD, both identify commit B. If we step back one parent from B, we get to E. If we step back 2 parents, we get to D. If we step back three, we get to M. If we step back four ... well, now we have a problem: which direction do we step back: left-and-up, or left-and-down?

The answer Git gives is that we can choose which way to go, at a merge. The default answer is to move to the "first parent". The first parent of any merge is the commit that was at the tip of the branch before you did the merge. In our case, that's the one that's also upwards, which is what we want.

The special syntax ~number means count back that many first-parent links. So, HEAD~4 means start at HEAD and go back four first-parent links, which arrives at the commit we want.

Now we can use git cherry-pick to make copies of the good commits that we wish to keep. Note that we should do them in what Git thinks of as reverse order, i.e., do the older one first, instead of the newer one first, in case commit E depends on commit D.

To copy commit E, simply name it in a git cherry-pick command. Note that tmp names commit B, tmp~1 names E, and tmp~2 names D, so:

git cherry-pick tmp~2
git cherry-pick tmp~1

will copy D and then E, giving:

...--o--D'-E'  <-- yourbranch (HEAD)
      \
       M--D--E--B   <-- tmp
      /
...--o   <-- repo/develop

where D' and E' are the copies you just made.

It's now safe to delete tmp:

git branch -D tmp

(well, as long as everything looks good—you can delete tmp later, of course).

Upvotes: 1

knittl
knittl

Reputation: 265131

git rebase -i HEAD~4 should show you all 4 commits. But beware, rebase will usually not show merge commits. If you then delete the lines containing the commits you want to delete (or change them to drop) you should get the expected result.

Another way would be to reset your branch to 422f6cb^ (the 5th last commit) and then cherry pick both edce31a and 926b7de

Upvotes: 1

Related Questions