Headers examples
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.
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 | ||
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:
- fix the issue by hand,
- let Git know the issue is fixed, and
- make a commit to remember how the conflict was solved
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:
- The text between
<<<<<<<
and=======
is what Richard's version looks like. - The text between
=======
and>>>>>>>
is what Sarah's version looks like. You can identify it by its commit hash at the end.
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:
- Sarah edits her recipe, but it's not ready yet to commit her changes
- Richard tells her to pull because he added a change they both need
- Sarah pulls Richard's changes without committing her changes first.
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:
- It saves her modifications to the
recipes.txt
file since her last commit in the so-calledstash
- 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
.