Collaborating with Git

Share your work with Git through remote repositories, such as those on GitHub, and resolve the conflicts that come up when two people change the same thing. This is a condensed version of the Git Basics - Working with Remotes chapter of the Git Book, and the appendix lists where to read more.

Presentation

You will need

  • Git
  • A free GitHub account, with your SSH key
  • A Unix CLI

Recommended reading

Remotes

A remote is another copy of your repository, usually on a server. It holds the whole commit graph, not only the latest version of your files. Every copy holds everything: this is what makes Git a distributed version control system. No copy is more important than another, except by agreement.

Collaborating means pushing your commits to a remote, and pulling (or fetching) the commits others have pushed there. Git never does either by itself: nothing is shared until you ask for it, and two remotes of the same repository know nothing about each other.

GitHub is a service that hosts Git repositories, so that you and others can push to them and fetch from them. It adds features Git does not have, such as access control, issues and pull requests, but the repositories themselves are ordinary Git repositories.

git remote: name your remotes

A remote is known to your repository by a name and a URL:

# add a remote
$> git remote add origin git@github.com:jdoe/git-calculator.git
$> git remote -v # list the remotes and their URLs
$> git remote rename origin github # rename a remote
$> git remote rm github # remove a remote

git clone automatically adds a remote named origin, pointing to the repository it cloned. “origin” is only a conventional name: a repository can have as many remotes as you want, each under a name of your choice.

Use the SSH URL GitHub shows for a repository, git@github.com:<owner>/<repo>. Git connects to GitHub with SSH, and authenticates you with your SSH key.

💎Tip

When you create a repository on GitHub to push an existing project to it, create it without a README, a license or a .gitignore file. Any of them makes a first commit on GitHub, which your project does not have, and your first push is then refused, as described below.

git push: send your commits

git push <remote> <branch> sends the commit your branch points to, with every commit behind it that the remote does not have yet, and asks the remote to move its own branch there:

$> git push -u origin main
...
To github.com:jdoe/git-calculator.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.

-u (--set-upstream) makes origin/main the upstream of your main: the branch git status compares yours to, and the one git push and git pull use when you give them no arguments. You only need it for the first push of a branch.

A rejected push

A remote only accepts a push that moves its branch forward with a fast-forward. If someone else has pushed commits you do not have, moving the remote’s branch to your commit would throw their work away, so the remote refuses:

$> git push team main
To github.com:ArchiDep/git-calculator-team.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:ArchiDep/git-calculator-team.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. ...

The reason Git gives says why:

  • (fetch first): the remote’s branch points to a commit your repository does not know at all. Fetch it to see the latest changes or pull it to integrate them.
  • (non-fast-forward): your repository knows that commit, but your branch does not include it, because the histories have diverged. Merge it or pull it.

A rejected push changes nothing, on either side. Fetch the others’ commits and merge them into your branch (or do both with a pull): your branch then contains their work, and pushing it is a fast-forward again.

Remote-tracking branches

Each time your Git talks to a remote, it records where the remote’s branches were, as remote-tracking branches named after the remote: origin/main is your record of where main is on origin. git graph shows them next to your own branches:

$> git graph
* e0711c3 (HEAD -> main, origin/main) Merge branch 'sub'
...

A remote-tracking branch is not a live view of the remote. It only moves when you push, fetch or pull. When git status says that your branch is “up to date with ‘origin/main’”, it compares your branch to that record, not to what is on the server now. You never commit on a remote-tracking branch yourself.

git fetch: get the others’ commits

git fetch <remote> downloads the commits of a remote that you do not have, and moves your remote-tracking branches to where the remote’s branches are:

$> git fetch team
...
From github.com:ArchiDep/git-calculator-team
* [new branch] main -> team/main

A fetch changes neither your branches nor your files. It only lets you see what the others have done, with git graph, git log team/main or git diff main team/main, before you decide to merge it:

$> git merge team/main

Merging a remote-tracking branch works exactly like merging any other branch: a fast-forward if your branch has no commits of its own, a three-way merge if the histories have diverged.

git pull: fetch and merge

git pull fetches, then merges what it fetched into the current branch. git pull team main is the same as git fetch team followed by git merge team/main. On a branch with an upstream, git pull without arguments fetches from the upstream’s remote and merges the upstream.

Fetching and merging in two steps lets you look at what the others did before you merge it. git pull is the shortcut once you know what to expect.

Merge conflicts

A three-way merge combines the changes of both branches since their common ancestor. When both branches changed the same lines of the same file differently, Git cannot choose between the two versions, and stops in the middle of the merge. This is a merge conflict:

$> git merge team/main
Auto-merging subtraction.js
CONFLICT (content): Merge conflict in subtraction.js
Automatic merge failed; fix conflicts and then commit the result.

A conflict is about lines, not whole files. Git merges everything it can, and git status lists what is left: the files it merged on its own are already staged, and the files in conflict are unmerged:

$> git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: index.html
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: subtraction.js

Resolving a conflict

In each file in conflict, Git has written both versions of the conflicting lines, between conflict markers:

/**
* Takes two numbers, a and b, and returns their subtraction.
*/
function subtract(a, b) {
<<<<<<< HEAD
return a - b;
=======
return -b + a;
>>>>>>> team/main
}
  • Between <<<<<<< HEAD and ======= is your version, the one of the branch you are on.
  • Between ======= and >>>>>>> team/main is theirs, the one of the branch you are merging.

To resolve the conflict:

  1. Edit the file until it is what it should be: keep one version, the other, or write a third that combines them, and remove the markers. Git does not check that the result makes sense, so test it.
  2. Stage it with git add, which marks the conflict as resolved.
  3. When git status says that all conflicts are fixed, commit. The commit concludes the merge, with the two commits as parents. git commit --no-edit keeps the message Git generated, and git commit -m "Your message" allows you to provide a custom message.

At any point before that commit, git merge --abort gives up on the merge and puts everything back as it was before it.

Conflicts are normal when several people work on the same code, and small ones are easy to resolve. Pulling often, and committing small changes, keeps them small.

Appendix: going further

This subject stops at what a small team sharing one repository needs day to day. These pages of the Pro Git book go further:

  • Distributed Workflows

    The ways to organize a team’s work with Git, which differ in who pushes to which repository and who merges what. The Hello GitHub exercise uses the simplest one, a centralized workflow where everyone pushes to one shared repository. Most open source projects use an integration manager workflow instead, where contributors push to their own copy and a maintainer merges their work.

  • Remote Branches

    Remote-tracking branches and upstreams in more detail: pushing a branch under another name, tracking other branches than main, and deleting a branch on a remote.

  • Contributing to a Project and Maintaining a Project

    Both sides of each workflow: how to prepare and share your work in a small team, in a larger one, or in a public project you cannot push to; and how to review and integrate the work of others.

  • GitHub - Contributing to a Project

    Forks and pull requests, the way most projects on GitHub take in contributions.

  • Basic Merge Conflicts and Advanced Merging

    More ways to understand and resolve a conflict: graphical merge tools with git mergetool, showing the common ancestor’s version of the conflicting lines, taking one side for a whole file, and undoing a merge.

  • Rerere

    “Reuse recorded resolution”: Git remembers how you resolved a conflict, and resolves the same conflict the same way the next time it comes up.

  • Rebasing

    The other way to integrate the others’ work: git pull --rebase replays your commits on top of theirs instead of merging, which keeps the history linear. Read “The Perils of Rebasing” before you rebase commits you have already pushed.

  • The Refspec

    How git fetch and git push decide which remote branch goes to which local branch, and how to change it.