1

I made a branch (folders) that was deliberately experimental, did stuff on it, eventually liked it, rebased interactively to reduce the number of commits, and rebased or cherry-picked into master. I deleted the branch-name and waited for the commits on that "branch" to die of their own accord.

But then for some reason the nameless "branch" got itself merged into master. I have no idea how that happened; I think it had something to do with pushing and pulling.

So now I've got these two extraneous commits, as shown in this screenshot from Sourcetree (they are the red commits on the right-hand track):

enter image description here

And they really are extraneous; what the red commits do (be937ba and 33b3b01) is exactly the same as what the first commit at the bottom of the rebase does (4fc6b63), because the latter is squashed from the former.

For the sake of completeness, here's the relevant part of the reflog:

6464656 HEAD@{24}: commit: finished splitters, about to reorg again
a1fc825 HEAD@{25}: commit: finished writing switch, renamed splitter partitioners
d822ddc HEAD@{26}: pull --no-commit origin master: Fast-forward
1d2b83f HEAD@{27}: commit (merge): chugging some more
6a0d405 HEAD@{28}: rebase -i (finish): returning to refs/heads/master
6a0d405 HEAD@{29}: rebase -i (pick): a bunch more pages
0b4f305 HEAD@{30}: rebase -i (pick): chugging along once again
bd9aa4f HEAD@{31}: rebase -i (pick): okay reorg into folders, looks okay to me
1a80352 HEAD@{32}: rebase -i (pick): wrote a bunch more pages
4fc6b63 HEAD@{33}: rebase -i (squash): revise nextprevs and breadcrumbs so we use folders but maintain just one nextprevs organized hierarchically
be937ba HEAD@{34}: rebase -i (start): checkout 28c6f1c81b36790b212727adaf209f30c0c0a031
a8f44e6 HEAD@{35}: rebase finished: returning to refs/heads/master
a8f44e6 HEAD@{36}: rebase: a bunch more pages
e6468f0 HEAD@{37}: rebase: chugging along once again
5c32d8e HEAD@{38}: rebase: okay reorg into folders, looks okay to me
d323c8c HEAD@{39}: rebase: wrote a bunch more pages
33b3b01 HEAD@{40}: rebase: checkout folders
9937608 HEAD@{41}: checkout: moving from folders to master
33b3b01 HEAD@{42}: reset: moving to 33b3b01717af3845160ce5b576099264b538a016
db95dc0 HEAD@{43}: merge master: Merge made by the 'recursive' strategy.
33b3b01 HEAD@{44}: commit: fix atrocious previous implementation
be937ba HEAD@{45}: checkout: moving from master to folders
9937608 HEAD@{46}: rebase -i (finish): returning to refs/heads/master
9937608 HEAD@{47}: rebase -i (pick): a bunch more pages
f6e20ea HEAD@{48}: rebase -i (pick): chugging along once again
debb94b HEAD@{49}: rebase -i (pick): okay reorg into folders, looks okay to me
08b072e HEAD@{50}: rebase -i (pick): wrote a bunch more pages
be937ba HEAD@{51}: rebase -i (squash): revise nextprevs and breadcrumbs so we use folders but maintain just one nextprevs organized hierarchically
092014e HEAD@{52}: rebase -i (start): checkout 28c6f1c81b36790b212727adaf209f30c0c0a031
fbf96a0 HEAD@{53}: reset: moving to HEAD
fbf96a0 HEAD@{54}: commit: a bunch more pages
52f19d0 HEAD@{55}: commit: chugging along once again
3251531 HEAD@{56}: commit: okay reorg into folders, looks okay to me
0a8a991 HEAD@{57}: commit: wrote a bunch more pages
54fa472 HEAD@{58}: commit: modified breadcrumbs to go with modification of nextprevs of previous commit
092014e HEAD@{59}: commit: experiment: can we use folders but maintain just one nextprevs organized hierarchically
28c6f1c HEAD@{60}: commit: tweaks to css for iPhone

I'd like to get rid of 33b3b01 (HEAD@{44}) and be937ba (HEAD@{45}) and force that change up to the remote (github). I am the only consumer so this is safe; I use this repo on multiple machines but I'm happy to delete the work folder on the other machine and clone afresh.

So the question is, can I do that, is it safe to do, and how do I do it?

Supplementary question is, how did this happen? I think what I mean by "this" is, how did HEAD@{27} become a merge commit? I thought I had this Cunning Plan to use a temporary branch as an experimental world and let its commits die, and it went wrong somehow. I know for a fact that I didn't deliberately merge, but of course pull does merge so that's probably the answer.

EDIT Here's more info from a fuller printout of git log -g, showing the part of the graph where the unintended merge happened.

commit 6464656c1de2f127ae38836bc834382477beda9e
Reflog: HEAD@{24} Reflog message: commit: finished splitters, about to reorg again

    finished splitters, about to reorg again

commit a1fc8250746a5fce6b66272e4ac11bb1f337d29c
Reflog: HEAD@{25} 
Reflog message: commit: finished writing switch, renamed splitter partitioners

    finished writing switch, renamed splitter partitioners

commit d822ddc6372bb92d50155de83b8f570b52e8cc73
Reflog: HEAD@{26} 
Reflog message: pull --no-commit origin master: Fast-forward

    starting to write switchToLatest

commit 1d2b83f0f024ae8efdd9c4a90a99353b502ad532
Reflog: HEAD@{27} 
Reflog message: commit (merge): chugging some more
Merge: 6a0d405 33b3b01

    chugging some more

commit 6a0d405394fc7a172e4987bd785ad8d8d7fb7d5b
Reflog: HEAD@{28}
Reflog message: rebase -i (finish): returning to refs/heads/master

    a bunch more pages

commit 6a0d405394fc7a172e4987bd785ad8d8d7fb7d5b
Reflog: HEAD@{29} 
Reflog message: rebase -i (pick): a bunch more pages

    a bunch more pages

The mystery is how "chugging some more" became a merge commit (Merge: 6a0d405 33b3b01). I think it has something to do with pushing, pulling on another machine, and pushing there. But I assure you I never specifically asked 33b3b01 to be merged; there's no obvious way I could, as it had no branch name (I had deleted the name).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Can I do that? - **Yes** Is it safe to do? - **Generally no. But if you have a backup (origin in your case), it's fine.** How do I do it? - **I would start with removing the commit where they merged together: git reset --soft HEAD~4** – Šimon Kocúrek Mar 17 '20 at 19:32
  • Yes, `git pull` means *run `git fetch`, then run a second command* with `git merge` being the usual second command. I recommend *avoiding* `git pull`: it is supposed to be convenient but often, it does too much and is therefore *in*-convenient. – torek Mar 17 '20 at 22:22
  • @torek yes, but when I pulled `HEAD@{26}` I got a fast forward. What isn't clear is how `HEAD@{27}` got turned into a merge commit. It didn't start out life that way. – matt Mar 18 '20 at 00:12
  • OK, that does seem a bit odd. Based on the reflog entry, you ran `git commit`; it must have found leftover merge state (a `MERGE_HEAD` file) from a merge that was started and never finished, so it finished it as a merge commit. Note that `git pull` should not leave behind any such state: it either tried a merge, or did a fast-forward that's not a merge, and according to the reflog it did a fast-forward. You'd have to have run a second `git pull` or `git merge` that had a conflict, to have a left-over `MERGE_HEAD`. – torek Mar 18 '20 at 02:00
  • @torek It's possible, but I guess we'll never know. I solved the problem using `git replace` and things are chugging along fine now. Thanks! – matt Mar 19 '20 at 15:56

3 Answers3

0

You can use a interactive rebase, which you seem quite familiar with.

git rebase -i 638723a

An editor will come up with each commit in a line. Delete the lines with the offending commits, including the merge. Then save and exit.

If you want to undo that, use git reset --hard ORIG_HEAD, or remember the commit ID where your branch is at before the rebase and reset to that.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • I guess I'm reluctant to do that because the merge commit, which was never supposed to be a merge commit, does mark a historically real stage after the commit on `master` that precedes it. Perhaps I'm just looking at this wrong, though. – matt Mar 17 '20 at 23:57
  • @matt I'm not sure what you mean. Does 1d2b83f contain more content than just the merge? – Schwern Mar 18 '20 at 02:50
  • Oh yes, that’s the point. I don’t understand how it became a merge, that’s the secondary question. – matt Mar 18 '20 at 03:42
  • @matt That is a good question. As for untangling this, try comparing a clean merge with your dirty one. Make a branch at 6a0d405 and also at 33b3b01; that is reconstitute your branches before the merge. Merge red into blue. Diff the new merge with 1d2b83f. That should be your extra work. Save that diff. When you rebase to remove red and the bad merge, add the patch as a new commit between 6a0d405 and d822ddc. – Schwern Mar 18 '20 at 04:31
  • 1
    @matt My best guess is you merged, had a conflict, didn't realize it, continued editing, and committed the merge + edits. – Schwern Mar 18 '20 at 04:32
0

And the answer is:

  • Step 1. git replace --edit 1d2b83f

  • Step 2. A textual description of the commit opens in the editor. It has two parent lines. Delete the parent line you don't like; in this case, that's the one with the parent 33b3b01.... Save and exit the editor.

    The unwanted commits have vanished in a puff of smoke. The history is now a single straight line. If you do a git log --pretty=raw and walk through it, you can see that the history is now a single straight line from one commit to the next (meaning the previous one).

  • Step 3. git filter-branch — This (thanks, @torek) writes the replacement into the actual history, so that clones of this repo will see what I'm seeing.

I then deleted the remote and create a new remote and pushed to it, just to make sure we have a clean upstream.

This was an easy case because there was no other history to rewrite, and because no one else was sharing the work, there were no other branches, etc., and because there was nothing coming from the unwanted branch that I was trying to get rid of. I was trying to simplify the story of what happened, not to change what did happen.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I tested this. While the parent commits do go away, the content of the merge remains. Commit d822ddc and further commits will still be polluted by the content of the branch. Did you see otherwise? – Schwern Mar 19 '20 at 03:30
  • But that’s fine. The merge added nothing. That’s the whole point. If you diff 33b3 and 4fc6 you get nothing. That’s why I abandoned / orphaned 33b3 in the first place. The problem was it unorphaned itself against my will. – matt Mar 19 '20 at 03:33
  • I really don’t think downvoting is appropriate. The problem really is solved by what I did. – matt Mar 19 '20 at 03:36
  • No diff between 33b3 and 4fc6 is very odd. If there's really no change in the merge then I suppose this works for this very specific situation. Edit that important detail into the answer and I'll remove the DV. Glad you got it resolved and introduced me to `git replace`. – Schwern Mar 19 '20 at 08:32
  • Besides, let's keep in mind what the goal was. It was to get rid of the side-branch from the visible history of `master` (the red track in the diagram). The reason is that it has no historical significance. But even so, even if it had some effect of its own on d822, who cares? The resulting history would still be coherent. A git commit is a _snapshot_, not a _diff_. It doesn't matter how what's in a commit got there. If I want to rewrite history so as to deny the past existence of a branch, what's the harm? – matt Mar 19 '20 at 14:05
  • 1
    Note that the grafted replacement commit *will not* appear in clones: any clone you make will see the history that includes the merge. You can modify the clone to bring over `refs/replace/*` from `origin` so as to bring the graft as well, but you need to do that in each clone. – torek Mar 19 '20 at 17:48
  • @torek darn I better check that – matt Mar 19 '20 at 18:03
  • @torek argh, so can I fix that with some sort of filter-branch or similar? – matt Mar 19 '20 at 18:10
  • 1
    Yes: after grafting, you can run a no-op `git filter-branch` to recopy your repository in place to make the graft permanent. (It's usually easier to `git rebase -i` the original merge away instead, but using filter-branch here is an option.) Note that all commits from the graft point on will be copied to new commits with different hash IDs, so you'll need to `git push --force` and have all other users of the shared repository start using the new commits instead of the old ones. – torek Mar 19 '20 at 18:20
  • @matt Maybe I could have derived it, but it was not clear to me that both branches contain the same content; that is quite unusual. This answer is only appropriate with that caveat. Could you include it explicitly to avoid someone using this answer inappropriately? "This only works if the two branches contain the same content" or similar. The history being in a straight line is a red herring; the merged content remains. – Schwern Mar 19 '20 at 19:03
  • @torek Yup, that did it. There is no sharing of the repository so it's not an issue; the remote is just for backup and self-sharing so I simply made a new remote. – matt Mar 19 '20 at 19:20
  • @Schwern I do not know when it only works, so I'm not going to say that. I know that it did work in this case, but I am not aware of what makes this case special if anything. In fact, I _believe_ that it _always_ works, because I find exactly the same sort of thing being advised here: https://stackoverflow.com/a/37001417/341994. I did add a clarification that the merged branch contributes nothing, but I do not know that that's essential to the success of the procedure. – matt Mar 19 '20 at 19:31
  • @Schwern And again I repeat that I would not have cared if "the merged content remains". I only want this to look as if I thought in a straight line. It happens that the _reason_ I want it to look that way is that the same change is already present on `master` so it doesn't need to be repeated in the branch, but wanting to remove a merged branch and just making it look like what it does was done in a single straight-line commit is fine too. – matt Mar 19 '20 at 19:34
  • @matt [Here's a demonstration of my concern](https://gist.github.com/schwern/763b241b94be92b072abe9fa309f24a2). The branch appears to be gone, but the merged content remains. It's essentially converted the merge commit to a squash merge (perhaps a better way to describe what you want). Whereas an interactive rebase would remove both the branch and the merged content. I understand your answer solves your specific need, but for others wishing to get rid of a merged branch it needs to be very clear in the answer that ***while the branch is gone the content of the branch remains***. – Schwern Mar 19 '20 at 23:59
  • @Schwern Elegant gist! But behold, I put you a [countercase](https://gist.github.com/mattneub/46b193eed0089f0870386400e52ab19a). We must not stack the deck by assuming that all subsequent commits after the merge (and there are many, as you can see) will have left that one feature-added file untouched. That is most improbable. The interactive rebase will likely produce conflicts. The replace produces none (and we wound up with the same result, though there, perhaps, I stacked my own deck). – matt Mar 20 '20 at 00:44
  • @matt Yes, conflicts can happen when you change history. I'm not arguing one is better or worse, I'm saying your answer is appropriate ***if and only if you want to keep the content of the branch***, or in your special case where the branch introduced no changes, and that must be made clear. – Schwern Mar 20 '20 at 19:56
  • 1
    @Schwern Okay, added that clarification (at the end). – matt Mar 20 '20 at 20:03
0

I would simply manually recreate the history that you want via Reset and Cherry-Pick (and maybe Rebase), and then use git push --force to change your remote to the state of local repo.

  • Checkout the master branch
  • Hard Reset local master to 6a0d405 (the commit before the erroneous merge)
  • Cherry-pick the subsequent good commits.
  • Your new branch will not have the bad commits or the merge.
  • Force-push the branch to overwrite the bad merge version on the server.

I too use SourceTree quite a bit, and all of the above except the force push can be done via easy commands in the UI via your mouse. I like doing it in SourceTree for the visualization as you're working.

I often do the rebuilding work on a separate branch to prevent accidentally losing any work, then reset/push master only at the end.

IMO using the common commands and then force pushing seems much simpler and less error prone than the more esoteric route of git replace and git filter-branch to rewrite the history.

pkamb
  • 33,281
  • 23
  • 160
  • 191
  • Yes, I don't think you've quite understood the question. Reset and cherry-pick is exactly how I got _into_ this situation. – matt Mar 29 '20 at 23:01
  • @matt the situation is only bad because of that unintentional merge (due to `git pull`?). Do the resetting and cherry picking again, but this time don't merge in that branch. Then force push to overwrite the version on the server. – pkamb Mar 30 '20 at 01:01
  • But the problem is that I _didn't_ merge it. It is unclear how it got merged. The question then was just how to make the merge commit not be a merge commit. That's solved. — Now, if you can explain how it turned into a merge, that would be great. – matt Mar 30 '20 at 01:16
  • @matt Well the merge commit didn't come in from resets or cherry-picks. You performed a merge or a pull or something that you didn't need to. But you don't need to "fix" the current state of your branch or even know what happened... just go recreate the string of commits that you want, without the merge, then force push. – pkamb Mar 30 '20 at 05:01