(Previously: Part 2: Branching)

Having Git providing a source control repository for local development is great, but there’s more. Git repositories can also reference other remote repositories out on the network. The network might be our corporate LAN, or perhaps the Internet.

Remote references allow us to get changes made by other developers. They also allow us to share our changes with other developers.

Two popular repository vendors are GitHub and Atlassian BitBucket. Either of them will allow us set up our own public or private git repositories for free.

Some conceptual stuff

What can we do with remote references?

  • We can fetch the latest change history from the remote repository;
  • We can reference the state of remote branches called remote-tracking branches, e.g. “alias/name”, or “origin/master”;
  • We can create a local tracking branch that replicates the contents of a remote branch;
  • We can merge the changes into our local tracking branch;
  • We can make local source code edits in a tracking branch; and push the committed changes to the remote repository.

I found the terminology of “remote-tracking branches” versus “tracking branches” confusing at first. Let’s re-iterate:

  • A branch is a pointer to a specific commit in the change history;
  • A remote-tracking branch represents the current state of a remote branch (updated automatically when connected to the server)
  • A tracking branch is a local branch checked out from a remote branch (or “upstream branch”).

I’m going to move my local repository into a remote, shareable version, and then pretend to be a second developer collaborating on the project from a different machine.

I’m going to do it step-by-step, with no shortcuts.

Creating a remote repository on GitHub

I’ve already created a GitHub account, so I log in and create a new empty repository called cow-tipper.

CAUTION: GitHub prompts us to initialize the repository with README, .gitignore, and license files. Don’t be tempted to do this! We require the repository to be empty in order to upload our local repository history into it. Leave the checkboxes un-ticked.

Another thing to note is that GitHub uses a default branch name of “main” instead of the traditional “master”. We could have changed it back at the time of repository creation, but let’s leave it as-is for now.

Remote URLs and Protocols

Git repository can store a remote URL that represents a remote repository. Usually, the URL will use one of two common network protocols: HTTPS or SSH. Either works well but each has its own quirks of user authentication.

GitHub is helpful and allows us to select which protocol we want to use, showing us the appropriate URL, along with some commands for uploading a local repo:

  • SSH: git@github.com:buster-kitten/cow-tipper.git
  • HTTPS: https://github.com/buster-kitten/cow-tipper.git

Connecting our repository to the remote one

Here’s what we need to do:

  • Add a remote reference to the new empty GitHub repository in our local repository;
  • Redefine the default branch name (“master”) so that it matches the remote one (“main”);
  • Initialize the remote repository with our local repos’ change history

Let’s double-check where we are:

$ cd ~/Projects/cow_tipper 
$ git switch master 
$ git status
On branch master nothing to commit, working tree clean

Add remote reference

Let’s add a reference named “skippy” to our remote repository to our local one, using the git remote command:

$ cd ~/Projects/cow_tipper
$ git remote add skippy git@github.com:buster-kitten/cow-tipper.git

Most of the time in literature you’ll see remote references use the name “origin”, but I want to underline the fact that “origin” is not a magic word, it is just a label, and could be anything. Ours is “skippy”.

We can see what remote references our local repository knows about:

$ git remote --verbose
skippy git@github.com:buster-kitten/cow-tipper.git (fetch) 
skippy git@github.com:buster-kitten/cow-tipper.git (push)

…but that’s not important right now.

Synchronize the default branch name

Now we rename the local repository’s default branch from “master” to “main”:

$ git branch --move master main
$ git status 
On branch main
nothing to commit, working tree clean

Two observations:

  • It is not required that the local branch name match the tracked remote branch name, but I think you’ll agree that it is a good idea to reduce confusion.
  • If we had created our local repository to use “main” as the name of the default branch, this rename step would not have been necessary, obviously.

Make it a remote-tracking branch (attempt 1)

$ git branch --set-upstream-to skippy/main
error: the requested upstream branch 'skippy/main' does not exist
hint: If you are planning on basing your work on an upstream
hint: branch that already exists at the remote, you may need to
hint: run "git fetch" to retrieve it.
hint: If you are planning to push out a new local branch that
hint: will track its remote counterpart, you may want to use
hint: "git push -u" to set the upstream config as you push.

My first instinct was “oh, of course skippy/main doesn’t exist – we don’t have any remote-tracking branches until we fetch from the server”.

This is true, but not appropriate in our situation: there’s no actual history to fetch from our brand-new empty remote repository. It has no commit history, no branches. We could perform a Fetch, but it wouldn’t help.

Let’s try it anyway:

Attempting to fetch the remote repository commit history

A fetch operation will need to authenticate against the remote server, which doesn’t know who we are. We’ll get a “Permission denied (publickey)” error. We need to authenticate ourselves over SSH by creating a public+private key pair and uploading our public key to the server.

For more detail on authentication with remote repositories,
see Appendix C: Petra Rabbit’s Authentication-palooza.

Having set up my RSA key pair and uploaded my public key to GitHub, I’ll try the fetch:

$ git fetch skippy

That took a few seconds before displaying the prompt, so I think this means it worked. No error, anyway. Now let’s try setting up the remote-tracking branch again:

$ git branch --set-upstream-to skippy/main
error: the requested upstream branch 'skippy/main' does not exist

Well, I said fetching wouldn’t help. The second hint that Git echoes to the console is more relevant:

hint: If you are planning to push out a new local branch that
hint: will track its remote counterpart, you may want to use
hint: "git push -u" to set the upstream config as you push.

Okay. Because no branches exist in our target empty repository, we have to push from our local repo in order to create them remotely.

Push to create the remote branch

This is a one-time thing: we push our work to the empty remote repository:

$ git push skippy main:main

Further reading on this, see: https://stackoverflow.com/questions/1519006/how-do-you-create-a-remote-git-branch/1519032#1519032

If you’re following along, but omitted the attempt to fetch, then you might still get the “permission denied (publickey)” error at this point. Ensure your public key is uploaded to your GitHub account.

$ git push skippy main:main
Enumerating objects: 44, done.
Counting objects: 100% (44/44), done.
Delta compression using up to 8 threads
Compressing objects: 100% (41/41), done.
Writing objects: 100% (44/44), 4.53 KiB | 928.00 KiB/s, done.
Total 44 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), done.
To github.com:buster-kitten/cow-tipper.git
 * [new branch]      main -> main

At this point, we can go to the GitHub site and select our cow-tipper repository and see that there are now two files, a branch “main”, and 15 commits. Nice! We’ve successfully uploaded our local repository into a remote one.

Let’s recap:

  • we have a remote called “skippy”
  • Our “master” branch was renamed “main” and is now a tracking branch
  • It tracks the remote-tracking branch “skippy/main”
  • All our locally-created source code change history is available on the remote repository on GitHub, available for cloning.

Remote Workflow

Now that our repository is replicated on the remote server, it is available to other developers. I’ve sent out invites using the tools available on GitHub.com, and I’m pretty sure there’s been some activity.


Now that we’re working with changes from other developers, we need to refresh our local remote-tracking branches with any new changes they may have saved to the server.

The fetch command retrieves from the remote repository all the source file changes that we don’t already have in our local repo:

$ git fetch skippy
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 7 (delta 3), reused 7 (delta 3), pack-reused 0
Unpacking objects: 100% (7/7), 777 bytes | 388.00 KiB/s, done.
From github.com:buster-kitten/cow-tipper
   84b9985..5e9ae78  main       -> skippy/main

Fetch downloads the changes into our remote-tracking branch. Our “main” branch is unchanged at this point:

$ git log --pretty="format:%h %ce %ci %d %n        %s" -1 
84b9985 buster@spacefold.com 2021-09-07 15:40:32 -0700  (HEAD -> main) 
        Merge branch 'secundo'

Yeah, that’s right – the last code change I made was to merge in my “secundo” branch. However we can ask git to show the log for the remote-tracking branch:

$ git log --pretty="format:%h %ce %ci %d %n        %s" -3 skippy/main
5e9ae78 zach@litterbox.com 2021-09-11 20:25:34 -0700  (skippy/main) 
        Zach The Cat was here and added some code
48e6c91 petra@mcgregor_garden.co.uk 2021-09-11 17:54:59 -0700  
        Additional description about stuff to do
84b9985 buster@spacefold.com 2021-09-07 15:40:32 -0700  (HEAD -> main) 
        Merge branch 'secundo'

We see Zach and Petra have been busy, and committed a couple of changes.


Let’s merge their changes into our local branch:

$ git merge skippy/main
Updating 84b9985..5e9ae78
 README.md     | 3 ++-
 cow_tipper.py | 2 ++
 2 files changed, 4 insertions(+), 1 deletion(-)

No conflicts, that’s good. Now our local source files on our file system (in our “main” branch) are up-to-date with the latest changes from the remote repo.

The Next Day…

This morning I’ve made some changes to the code: I moved a function into a separate module. I’ve tested my code change, and I’m ready to commit.

But I suspect there may be some changes from my offshore developer. I’ll fetch and check the remote log:

$ git fetch skippy 
$ git log --pretty="format:%h %ce %ci %d %n %s" -2 skippy/main
cee496c petra@mcgregor_garden.co.uk 2021-09-13 17:00:39 -0700 (skippy/main)
        Testing negative tax rates 
5e9ae78 zach@litterbox.com 2021-09-11 20:25:34 -0700 
        Zach The Cat was here and added some code

Yup, Petra added a test. What should we do now? Should I:

  • commit my changes locally
  • merge from remote and deal with potential conflicts


  • stash my current working tree
  • merge from remote
  • un-stash and commit (and deal with potential conflicts)

Aside: I haven’t talked about stashing in this tutorial yet, and it is already epic in size. So go here if you’re curious:
See https://git-scm.com/book/en/v2/Git-Tools-Stashing-and-Cleaning.)

What’s best? I’m not sure. I’m going to go with strategy A:

$ git add *
$ git commit -m "moving function to library module"
$ git log --pretty="format:%h %ce %ci %d %n        %s" -1 
3e60b54 buster@spacefold.com 2021-09-15 17:14:39 -0700  (HEAD -> main) 
        moving function to library module

No errors, and all our work is safe in commit “3e60b54” in branch “main”.

What horrors await us in the remote branch?

$ git diff main skippy/main

Aside: To avoid derailing the narrative, I’ve put a the introduction and examples of DIFF into Appendix D: What’s the DIFF? Check it out if you wish.

This gets messy, because the diff shows us what we need to do to make the source in “main” look like the source in “skippy/main”, but we don’t want to accept all those changes, because it would undo the work we did this morning. We have to be selective.

Worst case, we can roll back to our “safe” commit using git reset –hard 3e60b54. Let’s merge and see what we get:

$ git merge skippy/main
Auto-merging cow_tipper.py
CONFLICT (content): Merge conflict in cow_tipper.py
Automatic merge failed; fix conflicts and then commit the result.

The results are actually pretty good. We do need to manually resolve (i.e., edit) cow_tipper.py because blindly accepting either the remote or local version would be incorrect: I need to keep Petra’s addition (from the remote version), whilst changing it to invoke the function from the new library I added (in the local version).

You do not need to see the code – it’s a trivial and contrived example.

Having resolved the conflicts, and tested it (hey, Petra had a bug in her code!), we can commit and push:

$ git add cow_tipper.py
$ git commit -m "resolving merge conflicts; fixing Petra's bug"
$ git push skippy
fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream skippy main

Uh.. what? Has it forgotten? Do we have to remind it? Okay:

$ git branch --set-upstream-to skippy/main
Branch 'main' set up to track remote branch 'main' from 'skippy'.
$ git push skippy
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (10/10), 1.03 KiB | 1.03 MiB/s, done.
Total 10 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 1 local object.
To github.com:buster-kitten/cow-tipper.git
   cee496c..d9ac925  main -> main

Yeah, I don’t know what happened, but we’re back now. The remote repo has our combined changes.


The Git pull command combines fetch and merge for remote-tracking branches. As Petra enjoys her first coffee of the day, she executes:

$ git pull origin

(No, that’s not a typo. In Petra’s local repository,the remote reference is named “origin”. Only in my local repository is the remote ref called “skippy”.)

And Petra now has the latest source files in her local “main” branch.

How do I…?

Show branches with upstream remote-tracking branches:

$ git branch -vv

Show detailed information about remotes and remote-tracking branches:

$ git remote show <remote>

Further reading:

That’s all for this section. Next, Part 4: Remotes II – Reverse the Polarity!