Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How did I cross the streams, and how can I uncross them?

Tags:

git

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:

Crossed Streams GitEx

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:

Crossed Streams GitHub

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:

  1. 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?

  2. 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.

like image 762
Aaronaught Avatar asked Jul 10 '13 23:07

Aaronaught


People also ask

What happens if you cross streams?

This can cause a chain reaction, which may lead to total protonic reversal (or destruction at the cellular level). Crossing streams is like Russian roulette, protonic reversal may never happen.

What does dont cross the streams mean?

Roughly translated, it's along the lines of "that's a really bad thing you're about to start doing/are in the process of doing" Specifically, early on in the film there's a scene and some dialog which introduces the concept of "crossing the streams" as a Very Bad Thing.


2 Answers

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:

    1. release-1.3 was merged into develop
    2. 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:

    1. 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.
    2. 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.
    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.
    4. Some time passes while the user edits some files.
    5. 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".
    6. 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."
    7. The user runs git checkout release-1.3, this time remembering the correct command.
    8. The user creates a commit and runs git push.
    9. 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.
    10. 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:

  1. Run git checkout release-1.3.
  2. Find the sha1 of that middle commit (where the two branches come together). Let's call it X.
  3. 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.

  4. Run git checkout develop.
  5. Run git branch temp to act as a temporary placeholder for this commit.
  6. 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.
  7. 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.
  8. Run git cherry-pick temp to bring back the tip commit that was removed in step #6.
  9. 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...
    
  10. 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.

like image 128
Richard Hansen Avatar answered Nov 07 '22 15:11

Richard Hansen


  1. Not sure, beyond pure speculation. If you can provide a git log --decorate --graph --all --oneline and a git reflog release-1.3 (and perhaps a shell history) we might be able to help.

  2. This does involve a force push. Talk to your teammates about this. I don't know your hashes of these commits since you haven't provided them. Replace <blah> with the corresponding hash.

    git branch -m release-1.3 old-release-1.3
    git checkout -b release-1.3 <Un-ign> # the last good commit on the release branch
    # This will replay those five commits onto our new release-1.3 branch
    git rebase --onto release-1.3 <Merge> old-release-1.3 # the merge just above "Un-ign"
    # Verify that release-1.3 now looks correct
    # You'll need the -f. Be careful with force pushes, run -n first
    git push [-n|-f] origin release-1.3
    
like image 28
Peter Lundgren Avatar answered Nov 07 '22 17:11

Peter Lundgren