Git Essential Concepts (part 3)
Table of Contents
Introduction
In the previous articles on Git we explored the basic concepts of this tool and you should now feel more confident in using it. In today’s post we will complete our understanding facing the last two situations that cover more than 90% of the use cases:
- Remote Repositories
- Conflict Resolution
Remote Repositories
The most common scenario includes a person working in a team of people and developing a small feature in a complex application. How do we collaborate effectively with our teammates?
The idea is to have a central repository of code with the current
status of the project. This is hosted on a server so we can call it
a remote repository as opposed to a local repository which is you
local copy of the project. In the first article of this series on Git,
we explained how to use the command git init
to create a new local
repository, however the most common case is to clone a remote
repository with git clone $REPO_URL
. The local copy is a full copy
of all the commits and branches. We typically have the master branch
that contains stable code and a development branch with all the new
features not yet released but tested.
If you run git br -a
you will see some green branches - at least
master - and some red branches with the prefix origin/ in their
name; origin is the default name of your remote repository, so
origin/master is the master branch on the server. In Git you
develop your work locally and create commits on local branches, then
you push the commits to the server and move the remote branch as well.
For example you can create a free account on GitHub (www.github.com) and create a repository from the web console. Then you can clone the repo and start working:
# replace "myuser" and "myproject" with your data
git clone https://github.com/myuser/myproject.git .
cd myproject
# create a text file called "data.txt"
git add data.txt
git commit -m "initial data"
You are on master since you didn’t switch the branch after cloning
the repository and
you have one commit in your local repository. The server doesn’t know
about your new commit until you explicitly let him know. It’s time to
use the command git push
to send our local work to the remote
repository. Let’s do this and then create a new local commit:
git push origin master
# add new data to "data.txt"
git add data.txt
git commit -m "Added additional data"
git lo
What do you see in the output of git lo
?
As you can predict, the green branch master is pointing to the last commit and, since we didn’t perform another push, origin/master (in red) is still pointing to the previous commit.
Now one of our teammates clones our repo (you can go ahead and clone
it again in another folder to simulate your colleague). If you ran
git lo
in the new folder you see master and origin/master are
aligned to the commit with the message “initial data”. Now add a new file
and run a push to the repo:
# create the file settings.json
git add settings.json
git commit -m "Added configuration"
git push origin/master
If you run git lo
you will see both master and origin/master
have moved to point to the most recent commit.
Now go back to your previous folder and try git lo
and git push origin master
. The first command showed the exact same state as
before since we didn’t tell the local repository to sync the new data
from the server. The push failed because it was trying to send a new
commit to the server and move the remote branch to point to it, however
the master branch of the server is already pointing to a different
commit and if our last push were successful we had overridden the
work of our teammate. So, we can peek at what is on the server with:
git fetch
git lo
The command git fetch
checks if the remote branches on the server
are changed and downloads all the new commits. It also moves the pointers
origin/. So git lo
will show a green master, which is our local
branch pointing to our newest commit, and origin/master pointing to
the commit with the message “Added configuration”. If we now try to push
master it will fail. To fix the situation we
can either run git merge origin/master -m "merge"
or git rebase origin/master
. Then you can check if now the graph of the commit
looks better and we can proceed with git push origin master
. In this
case the push is successful because the local master is a child of
origin/master. Go back to review the second article on Git if you
are still not too sure about the difference between merge and
rebase.
Instead of running git fetch
and then git merge
, we can run git pull origin master
, however keep in mind this can lead to unexpected
consequences if, like before, our local master is not a child of
origin/master. The behavior is configurable and I suggest making
the pull fail instead of configuring an automatic merge.
To avoid these ambiguous situations we usually don’t develop directly on remote branches, but it’s better to create a local branch and then pull the remote branch before merging our changes to it and then push. In particular let’s say we have two branches in our remote repo master and dev:
$ git clone https://github.com/myuser/myproject.git .
$ cd myproject
$ git br -a
master
origin/master
origin/dev
$ git lo
$ git co dev
# this will create a local branch "dev" which track "origin/dev"
$ git lo
We want to add a new shell script to run our application, test it and then merge the local branch into dev.
# let's start by creating a new local branch called "script"
git co -b script
# create the new shell script "run.sh"
git add run.sh
git commit -m "First version of run.sh"
# test your script and let's say you made a small fix
git add run.sh
git commit -m "Fixed path in run.sh"
We now want to push the new code to the server on the branch dev:
git co dev
git pull
git merge script -m "merge script file"
git push origin dev
Feel free to run git lo
before and after every command to
investigate changes of origin/dev and local branches during the
exercise.
Conflict Resolution
Let’s say I and another teammate make changes to the same file at
the same line. The first one who merges and pushes to the remote branch
will have no problem. But the second will see a failure when he tries to
merge his local branch. This is known as a “conflict”. The most common
scenario is when you have a file with constants and both you and your
teammate add a new constant at the bottom. When the second person tries
to merge the local repository will be stuck in an “ongoing merge” state where
whatever can be merged has been (and the file is added to the staging
area) while the file in conflict is marked by a double U in the output
of git st
:
git st
M run.sh
UU data.txt
If you are scared and want to restore the state of the repository
as it was before running the merge, you can abort the process with
git merge --abort
. If you want to see what went wrong in data.txt
and resolve the conflict you can open the file in a text editor:
data1
data2
>>> HEAD
data3a
---
data3b
<<< 39cd015
In this example you have added data3a in your local branch and your
teammate added and pushed data3b. Most of the time it is obvious
what is the correct thing to do to keep both changes in the final
commit (in this case we want both data3a and data3b), but
sometimes it requires contacting the other developer and discuss with
him what is the best way to proceed. In our example you can simply
remove the lines beginning with >>>
, ---
and <<<
and proceed
with git add data.txt
followed by git merge --continue
.
If you want a better tool to merge and compare the differences I
highly recommend installing Meld and running
git difftool
instead of opening the file in the text editor.
Conclusion
In this article we saw how to work in a team with remote repositories and how to solve conflicts in Git when we collaborate with other people.
Cheatsheet
Command | Alias | Explanation |
---|---|---|
git branch -a |
git br -a |
show all branches: local and remote |
git clone $REPO_URL |
clone a remote repository in a local folder | |
git fetch |
download all the new commits from the server and update remote pointers | |
git push origin $BRANCH |
push the local commits to the remote server | |
git merge --continue |
continue a merge after conflict resolution |