Mert Özen Further With Every Line
DevOps Git & GitHub — #11

Git Remote and Clone: origin, fetch, push and Upstream

Mert Özen Nov 6, 2026 9 min 2 views
Git Remote and Clone: origin, fetch, push and Upstream

We see with real output what a remote is, git remote add, push -u and the upstream setting with a local bare repo standing in for GitHub, remote-tracking branches like origin/main, what git clone does, the split between fetch and merge, sharing a new branch, git remote show/rename and a rejected push.

Version note: The commands in this post were tested with Git 2.54; a local bare repo stands in for GitHub as the remote.

Guess First

So far the bookstore repo has lived only on Ada's computer. We even made Alan's commit in the same repo, under his name. What happens if Ada types git push one day? Where does Git send the commits?

$ git remote -v
$ git push
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using

    git remote add <name> <url>

and then push using the remote name

    git push <name>

Nowhere. git remote -v came back empty, which means this repo knows nothing about any other repo. Git has no special concept of a "server"; the repo on GitHub is a repo just like the one on your computer. Until you introduce it, Git has no address to go to.

What Is a Remote?

A remote is a short name you give to another repo: a URL and a nickname. Think of an entry in your phone's contacts: instead of typing the number from memory every time, you say "Alan". The remote itself does not download or send anything; it just says "that repo is over there".

In real life this address is the URL of the repo on GitHub: https://github.com/ada/bookstore.git for HTTPS, git@github.com:ada/bookstore.git for SSH. We will cover creating a repo on GitHub and authentication in a separate post. To keep the commands in this post runnable, we use a local bare repo in place of the repo on GitHub. A bare repo is a repo without a working tree, made only of the .git contents; it is exactly what GitHub keeps on its servers:

$ git init --bare ../bookstore.git
Initialized empty Git repository in .../bookstore.git/
$ ls ../bookstore.git
HEAD
config
description
hooks
info
objects
refs

We shortened the long path in the output with .... Inside there is no src/ and no README, only objects and refs. Nobody will work in this repo directly; everyone will work in their own clone and push here.

Adding a Remote and the First Push

$ git remote add origin ../bookstore.git
$ git remote -v
origin	../bookstore.git (fetch)
origin	../bookstore.git (push)
$ git push -u origin main
To ../bookstore.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

origin is a convention, not a special word. Since git clone gives the first remote this name, everyone uses it. git push -u origin main means "send the local main to origin and pair the two from now on". In a terminal you will also see progress lines such as Enumerating objects; Git prints them only in an interactive terminal.

-u (long name --set-upstream) leaves a permanent setting behind. Let's look at .git/config:

$ cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = ../bookstore.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main

The [remote "origin"] section is the contact entry. The fetch line says that every branch on the remote will be copied locally under refs/remotes/origin/. The [branch "main"] section defines main's upstream: this branch is paired with main on origin. From now on, while on main, a plain git push and git pull know where to go.

Remote-tracking Branch: origin/main

$ git branch -vv
* main 85ae714 [origin/main] Merge branch 'spring-sale'
$ git branch -a
* main
  remotes/origin/main

git branch -a, which stayed empty in the branch post, now shows one more line: remotes/origin/main. This is a remote-tracking branch, a local pointer that says "the last time I talked to origin, its main was here". You cannot work on it or commit to it; Git updates it only during fetch, pull and push.

Think of it as a weather report. The report was taken in the morning; the weather may have changed by the afternoon, but the report shows the old information until it is refreshed. origin/main is not a live connection, it is a photo taken at the last fetch. The [origin/main] in git branch -vv shows main's upstream; when the local branch is ahead of or behind the remote, information like ahead 1 or behind 2 appears here too.

Clone: Alan's Copy

Alan joins the team. He clones the repo into his own folder:

# On Alan's computer
$ git clone ../bookstore.git
Cloning into 'bookstore'...
done.
$ cd bookstore
$ git remote -v
origin	.../bookstore.git (fetch)
origin	.../bookstore.git (push)
$ git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

git clone does four things at once: it creates a new folder, downloads the whole history, adds the origin remote, and creates the remote's default branch locally with its upstream set. Alan now has the entire history; even if Ada's computer goes down, all the commits are still with Alan. That is exactly what distributed version control means. remotes/origin/HEAD records that the remote's default branch is main.

Fetch: Getting the News

Alan adds Foundation to the book list and pushes:

# On Alan's computer
$ git commit -am "Add Foundation"
[main 3510c22] Add Foundation
 1 file changed, 1 insertion(+)
$ git status -sb
## main...origin/main [ahead 1]
$ git push
To .../bookstore.git
   85ae714..3510c22  main -> main

Nothing changes on Ada's side by itself. Git does not poll the server on its own; origin/main still points to the old commit. To get the news you need git fetch:

$ git log --oneline -1 origin/main
85ae714 Merge branch 'spring-sale'
$ git fetch
From ../bookstore
   85ae714..3510c22  main       -> origin/main
$ 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 log --oneline main..origin/main
3510c22 Add Foundation

fetch downloaded the new commits and moved origin/main forward, but it did not touch Ada's main or her working tree. That is why fetch is always safe; you can run it as often as you like. Bringing what arrived into your own branch, after you have looked at it, is a separate step:

$ git merge origin/main
Updating 85ae714..3510c22
Fast-forward
 src/books.js | 1 +
 1 file changed, 1 insertion(+)

The shortcut for these two steps, fetch followed by merge, is git pull. We will open it up in detail in the next post.

Sharing a New Branch

Ada develops the search feature on a feature branch and pushes it for the first time:

$ git switch -c feature/search
Switched to a new branch 'feature/search'
$ git add src/search.js
$ git commit -m "Add title search"
[feature/search dee3ed6] Add title search
 1 file changed, 7 insertions(+)
 create mode 100644 src/search.js
$ git push -u origin feature/search
To ../bookstore.git
 * [new branch]      feature/search -> feature/search
branch 'feature/search' set up to track 'origin/feature/search'.

When Alan fetches, he sees the new branch as a remote-tracking branch. To work on it he needs a local branch, but he does not have to set it up by hand:

# On Alan's computer
$ git fetch
From .../bookstore
 * [new branch]      feature/search -> origin/feature/search
$ git switch feature/search
Switched to a new branch 'feature/search'
branch 'feature/search' set up to track 'origin/feature/search'.

There was no local branch called feature/search, but there was exactly one branch with that name on origin. git switch guessed this, created the local branch and set its upstream too. This behaviour has been the default since Git 2.23; if you do not want it, you can turn it off with --no-guess or, since Git 2.30, with the checkout.guess setting.

Managing Remotes

$ git remote show origin
* remote origin
  Fetch URL: ../bookstore.git
  Push  URL: ../bookstore.git
  HEAD branch: main
  Remote branches:
    feature/search tracked
    main           tracked
  Local branches configured for 'git pull':
    feature/search merges with remote feature/search
    main           merges with remote main
  Local refs configured for 'git push':
    feature/search pushes to feature/search (up to date)
    main           pushes to main           (up to date)
$ git remote rename origin hub
$ git branch -vv
* feature/search dee3ed6 [hub/feature/search] Add title search
  main           3510c22 [hub/main] Add Foundation
$ git remote rename hub origin

git remote show gathers everything you know about a remote on one screen, and it actually connects to the remote to do so. git remote rename renames the remote-tracking branches and upstream settings along with the remote's name. When working with a fork in open source you usually have two remotes: your own fork as origin and the original project as upstream. We will come back to this setup in the pull request post. To delete a remote you use git remote remove, and to change its address git remote set-url.

Common Pitfalls

The first is thinking origin/main is live. git log origin/main shows you not the server's current state but the moment of the last fetch. Run git fetch before you look at "what is on the remote".

The second is the rejection you get when someone else pushed before you. Alan added a line to the README and pushed; Ada, back on main, unaware of this, tries to push her own commit:

$ git commit -am "Raise Snow Crash price"
[main d76d45c] Raise Snow Crash price
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
To ../bookstore.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '../bookstore.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Git refuses to move main on the remote in a way that is not a fast-forward; otherwise Alan's commit would be overwritten. The fix is to get the change on the remote first. Do not panic and reach for --force; that deletes Alan's work.

The third is forgetting -u on a new branch's first push. Without an upstream, a later plain git push and git pull do not know where to go and fail with an error. Pushing once with -u is enough.

The fourth is embedding a password or token in the URL. An address like https://ada:token@github.com/... is written to .git/config as plain text. The right way is a credential helper or SSH; we will set up both in the post on connecting to GitHub. On the CI/CD side, too, tokens go into the pipeline's secret mechanism, not into URLs.

A Small Experiment

★☆☆ Create a bare repo on your own computer, push bookstore to it and clone it into another folder. Compare the output of git branch -a and git remote -v on both sides.

★★☆ Make a commit in the clone and push it. In the original repo, run git log --oneline -1 origin/main before and after fetching. Explain at which step it changed and why main did not change.

★★★ Try committing to main on both sides at the same time and pushing from both. Why is the second push rejected? What does git status say on the rejected side after git fetch?

Ada's push was rejected, and git status is about to say that the two branches have diverged. In the next post we continue from exactly this point: what git pull really does, why the "divergent branches" error appears and how to choose between pull.rebase and pull.ff.