How did I cross the streams, and how can I uncross them?
git
Solution
The possibilities
I can think of a couple of ways this could have happened.
Fast-forward merge. I believe this to be the most likely cause. This is what I believe happened:
- `release-1.3` was merged into `develop`
- immediately after, `develop` was fast-forward merged into `release-1.3`
That's all. `release-1.3` was merged to `develop`, then before any additional commits were made on `release-1.3` someone merged `develop` into `release-1.3`. When possible, `git` does a fast-forward merge by default (a "feature" that does the wrong thing half the time), which is why the graph looks confusing.
Note that a fast-forward merge leaves no direct evidence in the resulting graph. Unlike a regular merge, a fast-forward merge does not create a new commit object, and no existing commit is modified. A fast-forward merge simply adjusts the branch reference to point to an existing commit.
Even though there is no direct evidence of a fast-forward merge, the fact that you can reach that one merge commit by following each branch's first-parent path strongly indicates that this was a result of a fast-forward merge.
Accidental `git branch` instead of `git checkout`. On a project I was working on, another developer that was new to Git made the mistake of typing `git branch foo` in an attempt to switch to branch `foo`. This is a perfectly natural mistake, and one that even an expert can make when tired. Anyway, this mistake eventually resulted in something that looked just like a fast-forward merge even though `git merge` was never typed. A similar thing could have happened here. This is how the scenario plays out:
- The user has the `develop` branch checked out, and `develop` happens to be pointing to the merge commit that is at the center of this mystery.
- The user wishes to switch to the `release-1.3` branch to do some work, but accidentally types `git branch release-1.3` instead of `git checkout release-1.3`.
- Because it is a fresh clone, there is no local `release-1.3` branch yet (only `origin/release-1.3`). Thus, Git happily creates a new local branch named `release-1.3` that is pointing at that same merge commit.
- Some time passes while the user edits some files.
- Preparing to commit, the user runs `git status`. Because the current branch is still `develop` and not `release-1.3`, Git prints "On branch develop".
- The user is caught by surprise, and thinks, "Didn't I switch to release-1.3 a long time ago? Oh well, I must have forgotten to switch branches."
- The user runs `git checkout release-1.3`, this time remembering the correct command.
- The user creates a commit and runs `git push`.
- Git's default push behavior (see `push.default` in `git help config`) is `matching`, so even though `release-1.3` doesn't have a configured upstream branch, `git push` chooses the upstream's `release-1.3` branch to push to.
- The new version of `release-1.3` is a descendant of the previous `release-1.3`, so the remote repository happily accepts the push.
This is all it would take to produce the graph you provided in your question.
Assuming `develop` was intentionally merged into `release-1.3`
If the merge of `develop` into `release-1.3` was intentional (someone made a conscious decision that `develop` was good enough to ship), this is perfectly normal and correct. Despite the visual differences, the blue and black lines are both on the `release-1.3` branch; the blue line just happens to also be on the `develop` branch.
The only thing wrong is that it's a little awkward for someone reviewing the history to figure out what's going on (you wouldn't have this question otherwise). To prevent this from happening again, follow these rules of thumb:
- If you are merging two differently-named branches (e.g., `develop` and `release-1.3`, then always do `git merge --no-ff`.
- If you are merging two versions of the same branch (e.g., `develop` and `origin/develop`), then always do `git merge --ff-only`. If that fails because you can't fast-forward, then it's time for `git rebase`.
If you had followed the above rules of thumb, then the graph would have looked like this:
* (develop)
| * (release-1.3)
* | Merge...
|\|
| * Added...
| * using ...
* | adding...
| * Hide s...
* | Date ...
* | updati...
* | Candi...
| * Locali...
| * <---- merge commit that would have been created by
|/| 'git merge' had you used the '--no-ff' option
* | Merge...
|\|
| * Un-ign...
| * Added...
* | Merge...
|\|
| * Remov...
| * Move...
* | Fixed...
Notice how that extra merge commit makes the history much more readable.
If the merge of `develop` into `release-1.3` was a mistake
Doh! Looks like you have some rebasing and force-pushing to do. This will not make the other users of this repository happy.
Here's how you can fix it:
- Run `git checkout release-1.3`.
- Find the sha1 of that middle commit (where the two branches come together). Let's call it `X`.
Run `git rebase --onto X^2 X`. The resulting graph will look like this:
* (develop)
| * (release-1.3)
* | Merge...
|\ |
| * | Added...
| | * Added...
| * | using ...
| | * using ...
* | | adding...
| * | Hide s...
| | * Hide s...
* | | Date ...
* | | updati...
* | | Candi...
| * | Locali...
|/ * Locali...
* / Merge...
|\|
| * Un-ign...
| * Added...
* | Merge...
|\|
| * Remov...
| * Move...
* | Fixed...
This fixes the `release-1.3` branch, but notice how you now have two versions of the `release-1.3` commits. The next steps will remove these duplicates from the `develop` branch.
- Run `git checkout develop`.
- Run `git branch temp` to act as a temporary placeholder for this commit.
- Run `git reset --hard HEAD^^` to remove two commits from the `develop` branch: the tip `develop` commit and the commit merging the old version of `release-1.3` into `develop`. We'll restore that tip commit later.
- Run `git merge --no-ff release-1.3^` to merge the second commit on the new `release-1.3` branch and its ancestors into `develop`.
- Run `git cherry-pick temp` to bring back the tip commit that was removed in step #6.
Run `git branch -D temp` to get rid of the temporary placeholder branch. Your graph should now look like this:
* (develop)
| * (release-1.3)
* | Merge...
|\|
| * Added...
| * using ...
* | adding...
| * Hide s...
* | Date ...
* | updati...
* | Candi...
| * Locali...
* | Merge...
|\|
| * Un-ign...
| * Added...
* | Merge...
|\|
| * Remov...
| * Move...
* | Fixed...
Run `git push -f origin release-1.3 develop` to force-update the upstream branches.
Preventing this from happening again in the future
If you have control over the upstream repository and can install some hooks, you can create a hook that rejects any pushes where the old version of the branch isn't reachable by starting at the new version of the branch and walking the first-parent path. This also has the advantage of rejecting those stupid commits created by `git pull`.
Problem
I've been using git happily for the better part of a year, and our team is using the git-flow model (although not git-flow itself). Last week I created a release branch and have been happily merging the changes back into the development/unstable branch - that is until something very strange happened today. Somehow, the release branch got hyper-merged, or something. I honestly can't think of a better way to describe it than "crossing the streams". Here's a visual from Git Extensions: I'm obfuscating most of the commit comments, sorry. But starting from the bottom, you can clearly see the development branch on the left and the release branch on the right being periodically merged into it. Until about halfway through, when I must have done something wrong with the merge, because at that point the release branch literally merged with the development branch - they actually appear to share a merge commit that should only have been on the development branch. GitHub presents a slightly clearer view: This doesn't look so unusual, except for the fact that the blue and black lines are supposed to be the same branch. Just to tick off the obvious: There are commits from other contributors, but the specific commits in question are from me. I know I can trust the commit messages. The commit messages all say the same thing: "Merge branch 'release-1.3' into develop." So I know I didn't, for example, accidentally merge the development branch into the release branch instead. I never created any new branches or used any commands other than `git pull --rebase`, `git commit`, and `git merge`. I wasn't experimenting or trying to do anything creative with the repository. It looks like I dropped the original `release-1.3` branch and created a new one from the `develop` branch near the middle. But I didn't do that. The `release-1.3` branch has been active the whole time. I swear that the blue and black commits really are the same branch and that I never did any merges other than merging them both into the development (pink) branch. So I have two questions: How is this even possible? I understand technically how it is possible, with git commits being graph nodes and all that, but I can't quite figure out how this happened from a procedural point of view. What mistake could I have made that would cause this, and how can I avoid doing it again? How can I get these branches untangled, so that I don't have a bunch of half-finished code from the unstable branch in my squeaky-clean release branch? It should just look like two parallel lines occasionally connected by merges. Preferably, I'd like to do this without a forced push or destructive rebase, since this is a shared repository, although it's a small team and a private repository so I can get people to re-clone if absolutely necessary.