Remotes

Exercise 04 Remotes

Until now we have only worked with a local repository. Remotes are used to synchronize changes between your local repository and another location. This enables you to collaborate with other authors and can prevent loss of work in the event of disk failure on your local machine.

A graphic depicting a TV remote and a TV with the git logo

docker run -it gitforpragmatists/01-basics-04-remotes

Talking to Remotes

status

As usual, we can use git status to find out what’s going on.

$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

Our local repository is “1 commit behind origin/main”. What does that mean?

Typically you have one remote and it’s called origin, there’s nothing special about the name but it is the default that git will provide. origin/main is the main branch in the remote repository called origin.

We can get up to date with origin by merging

$ git merge
Updating a7a83a5..f2900e4
Fast-forward
 CakeRecipe.md | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 CakeRecipe.md

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

Invoking git merge without a branch argument has merged the configured remote branch into our local branch. By convention they have the same name. There’s nothing special about this merge, all the fast forward stuff from branch and merge still applies. It’s just like we have two local branches called main and origin/main and origin/main happens to be in whatever state main was on origin last time we synchronized.

fetch

So we’re actually not necessarily up to date with origin. Just the last time we synchronized with the remote. If someone else has pushed some work we might be out of date. Let’s fetch the latest state.

$ git fetch
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 947 bytes | 33.00 KiB/s, done.
From ../remote
   f2900e4..f520ee6  main              -> origin/main
 * [new branch]      add-cookie-recipe -> origin/add-cookie-recipe

$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

We’ve retrieved an updated HEAD for origin/main and a new branch add-cookie-recipe. If we check the status, we’re 1 behind again. Let’s catch up.

$ git merge
Updating f2900e4..f520ee6
Fast-forward
 CakeRecipe.md | 8 ++++++++
 1 file changed, 8 insertions(+)

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

pull

Fetching and then merging is such a common sequence that there is a command to combine the actions git pull

I’ll exit the exercise and restart it to demonstrate pull

$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

$ git pull
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 947 bytes | 63.00 KiB/s, done.
From ../remote
   f2900e4..f520ee6  main              -> origin/main
 * [new branch]      add-cookie-recipe -> origin/add-cookie-recipe
Updating a7a83a5..f520ee6
Fast-forward
 CakeRecipe.md | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 CakeRecipe.md

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

I’ve included pull for completeness but personally, I rarely use it. I like to have confidence about what will happen when I merge and give myself the opportunity to do something else if the remote state is surprising. In the above example my local branch got updated to a commit I’d never even seen before when I pulled.

If it’s a branch that I haven’t worked on that I just want to get up to date with then I’ll use pull.

push

So far we’ve been taking other people’s commits from remotes, let’s push some of our own.

I like the sound of that add-cookie-recipe branch. Let’s take a look.

git log origin/add-cookie-recipe

commit 089221cd0953b5efbaeb345334d512688f544423 (origin/add-cookie-recipe)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Fri Sep 16 19:48:27 2022 +0000

    Add basic cookie recipe

commit f520ee6d3d088b24d6798a8a93992b6b326c017f (HEAD -> main, origin/main)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Fri Sep 16 19:48:26 2022 +0000

    Add cake method
...

It’s one ahead of main so we can just do a fast-forward merge.

$ git merge origin/add-cookie-recipe
Updating f520ee6..089221c
Fast-forward
 CookieRecipe.md | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 CookieRecipe.md
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

$ git log
commit 089221cd0953b5efbaeb345334d512688f544423 (HEAD -> main, origin/add-cookie-recipe)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Fri Sep 16 19:48:27 2022 +0000

    Add basic cookie recipe

commit f520ee6d3d088b24d6798a8a93992b6b326c017f (origin/main)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Fri Sep 16 19:48:26 2022 +0000

    Add cake method

We’ve got 1 new commit on our branch and we’re now “ahead of ‘origin/main’ by 1 commit”.

Using git push as instructed

$ git push
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ../remote
   f520ee6..089221c  main -> main
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

Now origin is up to date with us and everyone can enjoy the new cookie recipe.

conflicts

Since merging remote branches is much the same as merging two local branches, you may be wondering what happens if we get a conflict.

I’ve provided a branch ex-add-pie in that situation, there’s some work that we don’t have on the local branch on the remote and vice versa.

$ git switch ex-add-pie
Switched to branch 'ex-add-pie'
Your branch and 'origin/ex-add-pie' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

If we try to push our change we get rejected.

$ git push
To ../remote
 ! [rejected]        ex-add-pie -> ex-add-pie (non-fast-forward)
error: failed to push some refs to '../remote'
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.

If we merge and resolve the conflict, we’re able to push

$ git merge
Auto-merging PieRecipe.md
CONFLICT (content): Merge conflict in PieRecipe.md
Automatic merge failed; fix conflicts and then commit the result.
$ vim PieRecipe.md
# In the editor I resolved the conflict as before in 01-03-branch-and-merge
$ git status
On branch ex-add-pie
Your branch and 'origin/ex-add-pie' 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:   PieRecipe.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git add PieRecipe.md
$ git commit
[ex-add-pie d94a5c9] Merge remote-tracking branch 'refs/remotes/origin/ex-add-pie' into ex-add-pie
$ git push
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 4 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 741 bytes | 92.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0), pack-reused 0
To ../remote
   b4c50e6..d94a5c9  ex-add-pie -> ex-add-pie

There is another option to resolving the conflict which is to erase the remote commits by forceing the push. It’s a destructive option so think carefully before you do it.

$ git switch ex-add-pie
Switched to branch 'ex-add-pie'
Your branch and 'origin/ex-add-pie' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)
$ git push --force
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 425 bytes | 53.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To ../remote
 + b4c50e6...a0ac42f ex-add-pie -> ex-add-pie (forced update)

We’ve forced the remote branch to align with our HEAD. You can usually assume that the previous HEAD is no longer referenced and therefore may be unrecoverable so use with caution. Typically I would only use this if I was overwriting an amended commit where it doesn’t make sense to merge the changes and I was confident nobody else had pulled the state I was overwriting.

For slightly more safety, I’d recommend --force-with-lease. This flag will cause a force push but only if the HEAD you are overwriting is the HEAD you last saw when you fetched. Using a lease protects you from overwriting what you think is work you are willing to throw away when someone else has checked in work after you last fetched.

$ git push --force-with-lease
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 425 bytes | 60.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To ../remote
 + b4c50e6...a0ac42f ex-add-pie -> ex-add-pie (forced update)

Managing Remotes

clone

To create a local copy of a remote repository you can use git clone. For example let’s clone this public repository from GitHub with git clone https://github.com/Git-For-Pragmastists/clone-me.git

$ cd /home/gitstudent
$ git clone https://github.com/Git-For-Pragmastists/clone-me.git
Cloning into 'clone-me'...
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (4/4), done.
$ cd clone-me
$ git remote -v
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)

clone sets up the remote for you with the default name of origin.

remote

If you don’t clone or have multiple remotes you’ll need to manage them with git remote

list

To list your remotes you can issue git remote. The output is not especially useful without the --verbose flag which can be abbreviated to -v

$ git remote
origin
$ git remote --verbose
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)
$ git remote -v
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)
add & remove

To add a remote to an existing git repository we can use git remote add. For example we can add this public repository as an additional remote.

$ git remote add additional https://github.com/Git-For-Pragmastists/add-me-as-a-remote.git
$ git remote -v
additional      https://github.com/Git-For-Pragmastists/add-me-as-a-remote.git (fetch)
additional      https://github.com/Git-For-Pragmastists/add-me-as-a-remote.git (push)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)
$ git fetch additional
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 701 bytes | 43.00 KiB/s, done.
From https://github.com/Git-For-Pragmastists/add-me-as-a-remote
 * [new branch]      main       -> additional/main
$ git merge additional/main
Updating 378261d..92eeab3
Fast-forward
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

It’s a copy of the repository we cloned earlier and it has a branch called main which is 1 commit ahead of origin/main. We can merge additional/main into our local main and see that we’re now 1 ahead of origin/main. We can’t push that change since the repository is read only.

If you want to remove a remote you can like so.

$ git remote -v
additional      https://github.com/Git-For-Pragmastists (fetch)
additional      https://github.com/Git-For-Pragmastists (push)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)
$ git remote remove additional
$ git remote -v
origin  https://github.com/Git-For-Pragmastists/clone-me.git (fetch)
origin  https://github.com/Git-For-Pragmastists/clone-me.git (push)

hosts

For this exercise the remote is just another directory inside the container. There are many services you can use to host a remote repository for you such as: GitHub, GitLab and Bitbucket.

They tend to offer a web UI which you can use to configure additional features such as read/write access control to your repository, interfaces to manage requests merge branches and perform code review, and running activities based on git events like running the automated test suite on push.

A screenshot of GitHub's pull request feature showing a request to merge the changes from add-me-as-a-remote