Headers examples

Git: An opinionated tutorial

Part 3: Conflicts

In Part 2 we learned how to work in sync with other people over the internet.

Git is designed to collaborate with as many people as possible, and that means disagreements: the more collaborators a project has, the higher the chances that two of them disagree about a specific change. In Git, just like in real life, we call this a conflict, and this lesson explains how to deal with them.

Living a conflict-free life

Remember the recipe we wrote up until now?

Poached egg
===========
Ingredients:
  * 1 egg
  * 1 spoon vinegar
  * Salt

Instructions:
  * Heat the water to 80°C
  * Add salt and vinegar to the water
  * Submerge the egg for 3-4 minutes.
  * Add salt to taste.

If Richard and Sarah want to work together, the easiest way to cooperate is to ensure that they are not standing on each other’s toes.

Let’s say we all agree that the vinegar is unnecessary, and we split the work of fixing the recipe. We also agree on who fixes what: Sarah will take care of the ingredients while Richard takes care of the instructions. Therefore, Sarah's local version ends up looking like this:

Poached egg
===========
Ingredients:
  * 1 egg
  * Salt

Instructions:
  * Heat the water to 80°C
  * Add salt and vinegar to the water
  * Submerge the egg for 3-4 minutes.
  * Add salt to taste.
$ git commit -a -m "Removes vinegar from the ingredients"
[master 5cf6c04] Removes vinegar from the ingredients
 1 file changed, 1 deletion(-)

While Richard's version ends up looking like this:

Poached egg
===========
Ingredients:
  * 1 egg
  * 1 spoon vinegar
  * Salt

Instructions:
  * Heat the water to 80°C.
  * Add salt to the water.
  * Submerge the egg for 3-4 minutes.
  * Add salt to taste.
$ git commit -a -m "Removes vinegar from the description"
[master 91f6b7c] Removes vinegar from the description
 1 file changed, 1 insertion(+), 1 deletion(-)

Now each version of the repository (Sarah's, Richard's, and the central one in GitLab) looks different from each other.

Sarah GitLab Richard

Without Git this would spell trouble, but fear not! This is exactly the type of situation where Git shines.

Let’s assume that Sarah completed her part first. When she pushes her changes, nothing bad happens - this is the same situation we have seen in the previous lesson:

$ git push
Username for 'https://gitlab.com': sarah
Password for 'https://sarah@gitlab.com': 
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 290 bytes | 290.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://gitlab.com/sarah/recipes.git
   fbe14a8..5cf6c04  master -> master
Sarah GitLab Richard

Now when Richard tries to push his work, he finds himself with a surprise

$ git push
Username for 'https://gitlab.com': richard
Password for 'https://richard@gitlab.com': 
To https://gitlab.com/sarah/recipes.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://gitlab.com/sarah/recipes.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The problem here is that Richard is not up to date, but he doesn’t know it because he didn’t pull the latest changes first. In Git terms, his HEAD label is pointing to a commit that's older than the one in the central repository. Note that Sarah also forgot to pull, but this was not an issue for her because she was ahead of the central repository, and you can always push to a repository when you have the newest version. Sarah can tell everyone "I have the latest version, everyone follow me" (which she does when she pushes her code to the central repository), but Richard cannot say "I have an older version, everyone throw away what you've done and follow me". Since Richard is the one who's not up-to-date, it's his job to get in sync.

To do that, Richard needs to pull the newest version of the repository. He does that with git pull, but if we did that right now a lot would happen at once and we might get lost. So let's go step by step.

In the previous lesson I said that git pull is actually two commands, git fetch followed by git merge. When Richard calls git fetch, nothing happens to his code - Git brings all pending commits in the background, but it doesn't do anything with them yet.

When Richard calls git merge next, Git realizes that the code has diverged: if we now insert Sarah's commit 5cf6c04 on top of commit fbe14a8 the end result is a chain with two top-most nodes instead of just one, and that's not what we want. Bringing those two top-most nodes back into one is called merging, and Git will automatically try to do it for us next.

Luckily for Sarah and Richard, their agreement not to stand on each other's toes pays off: since their modifications don't overlap, Git can easily deduct what the combined file should look like. The final version, with vinegar removed from both sections, ends up looking like this:

Poached egg
===========
Ingredients:
  * 1 egg
  * Salt

Instructions:
  * Heat the water to 80°C.
  * Add salt to the water.
  * Submerge the egg for 3-4 minutes.
  * Add salt to taste.

Next, Git creates a new commit to store the successful result of this merge. Richard is therefore presented with yet another window asking him to provide a meaningful description of what just happened.


# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

Merge recipes
$ git merge
Auto-merging recipe.txt
Merge made by the 'recursive' strategy.
 recipe.txt | 1 -
 1 file changed, 1 deletion(-)

And now that Richard has unified his local version with the remote one, he can finally push his changes back.

Sarah GitLab Richard
Note: If you resolve a conflict once, you have resolved it for everyone else too. Sarah can therefore pull the changes from the central repository without issues - when she does, she'll get both Richard's original commit plus the merge commit where he fixed the conflict.

Solving conflicts

Whenever two commits cannot be automatically merged, we say that there's a conflict. Git works one line at the time - as long as two people don't edit the same line at the same time, their changes will usually be merged without them even noticing, as it happened in our last example. But now that we’ve seen how to avoid conflicts, let’s do the exact opposite and create one.

Let’s say that both Sarah and Richard have independently decided that the recipe could use some improvements. Sarah believes that eggs taste better when adding some mustard to the water:

Poached egg
===========
Ingredients:
  * 1 egg
  * 1 spoonful mustard
  * Salt

Instructions:
  * Heat the water to 80°C.
  * Add salt and mustard to the water.
  * Submerge the egg for 3-4 minutes.
  * Add salt to taste.
$ git commit -a -m "Adds mustard"
[master 9b39da1] Adds mustard
 1 file changed, 2 insertions(+), 1 deletion(-)

Richard, on the other hand, believes that the temperature description is too vague:

Poached egg
===========
Ingredients:
  * 1 egg
  * Salt

Instructions:
  * Bring the water to a boil and then lower the temperature to 80°C.
  * Add salt to the almost-boiling water.
  * Submerge the egg for 3-4 minutes, and then carefully take it out.
  * Add salt to taste.
$ git commit -a -m "Adds details about water temperature"
[master 0e5ca24] Adds details about water temperature
 1 file changed, 3 insertions(+), 3 deletions(-)

Sarah pushes her changes first. Seeing as all repositories were until now in agreement with each other, nothing bad happens to her. But when Richard pulls, he is in trouble: not only is there a merge needed, but the conflict we have created is one that Git cannot solve by itself:

$ git pull
Username for 'https://gitlab.com': richard
Password for 'https://richard@gitlab.com': 
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://gitlab.com/sarah/recipes
   b7709d4..9b39da1  master     -> origin/master
Auto-merging recipe.txt
CONFLICT (content): Merge conflict in recipe.txt
Automatic merge failed; fix conflicts and then commit the result.

Once again, since Richard is the one who pulled last, it's therefore his job to solve the conflict. He needs to follow three steps:

For the sake of the exercise we’ll see two different ways of doing it, once from the command line and once using a helper tool.

From the command line, we’ll start by asking Git which conflicts still remain unsolved. You don’t always need to do this (since Git already told us which files need our intervention), but it’s useful in those cases when you have too many conflicts to solve at once and forgot which ones are still pending.

$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

	both modified:   recipe.txt

no changes added to commit (use "git add" and/or "git commit -a")

We edit our recipes.txt file, and we are welcome with the following word salad:

Poached egg
===========
Ingredients:
  * 1 egg
  * 1 spoonful mustard
  * Salt

Instructions:
<<<<<<< HEAD
  * Bring the water to a boil and then lower the temperature to 80°C.
  * Add salt to the almost-boiling water.
  * Submerge the egg for 3-4 minutes, and then carefully take it out.
=======
  * Heat the water to 80°C.
  * Add salt and mustard to the water.
  * Submerge the egg for 3-4 minutes.
>>>>>>> 9b39da1454986a7bf72c3766cbc6e3ac237a0174
  * Add salt to taste.

This type of text always looks daunting at first, but stay with me and we’ll pull through together.

The Ingredients section of the file looks fine - Sarah modified the ingredients list but Richard didn’t, and therefore Git assumes that we are both in agreement here that the mustard should be added.

The instructions, on the other hand, is where the conflict happened. The strange format we see here is Git’s way of showing what the conflict is:

This is the section of the file that Git cannot solve because both Sarah and Richard modified the same section and Git doesn’t want to pick favorites. It is therefore our job to use our text editor and put this file into shape. Some edits later, our file looks like this:

Poached egg
===========
Ingredients:
  * 1 egg
  * 1 spoonful mustard
  * Salt

Instructions:
  * Bring the water to a boil and then lower the temperature to 80°C.
  * Add salt and mustard to the almost-boiling water.
  * Submerge the egg for 3-4 minutes, and then carefully take it out.
  * Add salt to taste.

As you can see, we kept parts of both files - Git may not be able to decide who is right and who isn’t, but we are. Our next step is to mark this conflict as solved with the git add command.

$ git add recipe.txt

$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

	modified:   recipe.txt

And since we are done with all of our conflicts (git status says so), Richard can commit and push this version the same way we did in the previous section.

$ git commit -a -m "Resolves mustard conflict"
[master 8f50765] Resolves mustard conflict

The second way of solving a conflict invokes the use of a graphical tool: instead of editing a file with our regular editor, an editor like Meld can make the entire process easier to understand:

While all tools look slightly different, the basic concept is the same: you get a side-by-side comparison between the two versions, plus a third one where you decide how the conflict is to be solved. You can either click on specific lines that you’d like to keep, or you can jump in and edit the text yourself.

In this example, Sarah's version is on the left and Richard's version is on the right. We can see that the line "1 spoonful mustard" came from Sarah's version, and the green color indicates that it can be added with zero conflicts to the final version, which is shown in the middle.

We then jump to the conflicting part. If one of this versions were absolutely correct and the other one were absolutely wrong, we could simply pick the correct version with the small arrow and decide for that one. If we were to keep Richard's version here, it would look like this, with Meld highlighting how the final version differs from the one we didn't keep.

Our example, however, is more complicated because we want to keep data from both versions at the same time. Therefore, instead of picking from one version or the other, we just edit the final version directly. Note that this makes Meld unhappy and it paints everything red, but this is fine: Meld is designed for cases when you simply cherry-pick specific lines, but our case requires finer work.

Once we are done solving all conflicts, we save our changes and exit the program. Normally we would have to go and mark the conflict as solved, but in this case Meld graciously offers to do it by itself.

All that remains now is to commit our changes, push, and our conflict is once again resolved.

Note that Git doesn’t care about whose version is "better" - we could have resolved the conflict by deleting all of Sarah's changes and Git wouldn’t have complained. If this is a problem for you, or if you'd prefer to review all of little Timmy's changes before they are saved forever, there are mechanisms for limiting who can contribute to a project. We won't be covering them in this tutorial, but keep in mind that they exist.

Stashing changes

Before we move on, I'd like to dedicate a couple paragraphs to talk about the following problem:

What happens to Sarah's uncommitted changes? The short answer is that Git doesn't let Sarah pull - if she tries to pull, Git will show an error and cancel the merge step:

$ git pull
(...)
error: Your local changes to the following files would be overwritten by merge:
	recipe.txt
Please commit your changes or stash them before you merge.
Aborting

What Git is telling is simple: if Git merges the new changes on top of the current file, Sarah risks losing her uncommitted changes forever if there's an overlap with what Richard did. Git suggests two solutions and the first one should be clear by now: if Sarah commits her current version first, merging Richard's changes will follow the steps we've seen before and nothing will be lost.

But Sarah has a second alternative: stashing her changes. The stash is a pile where you can keep changes you are not ready to commit yet. If Sarah stashes her changes, Git does two things:

  1. It saves her modifications to the recipes.txt file since her last commit in the so-called stash
  2. It then reverts the modified files back to their last committed version

Once her changes are safely stored in the stash, Sarah can merge Richard's changes (as we've seen before) and re-apply her pending changes on top of the merged file. The commands would look like this:

$ git stash
Saved working directory and index state WIP on master: <last commit hash and description>

$ git pull
(...)

$ git stash pop
Auto-merging recipe.txt
CONFLICT (content): Merge conflict in recipe.txt

The last message can be different depending on whether the changes Sarah stashed away conflict with the newer version of the file or not. If they do, she needs to follow the steps we've seen in this lesson to merge her older, uncommitted changes with those she just pulled. Once she has successfully merged both versions, she keeps working until she's ready to commit.

There are plenty of edge cases when dealing with git stash: whether you want the changes to remain in the stash or not (in which case you call git stash with apply instead of pop), whether you want to stash a single file, using multiple stashes, and more. We won't be seeing them in this tutorial, but I encourage you to look into more detail as your usage of Git becomes more advanced.

Part 3 quick review


merge conflict
A situation that arises when two versions of a repository have diverged and cannot be merged back together automatically.
git add
In Part 1 we used this command to start keeping track of previously-untracked files. We have now seen a second function: it can also mark that the conflicts on a file have been fixed.
git stash
Store local changes to our repo on the side so we can pull some remote changes.
git stash pop
Bring our previously-stashed changes back into our local version.

This ends the third part of this tutorial. In Part 4 we'll finally take a look at that curious master label we’ve been ignoring all this time when we take a look at branches.