The previous lesson made clear what a remote is: a short name for a URL. Now we have to create one. And we get to do it with the case we have been postponing for three modules: Ana is going to publish task-manager.

Until today the project has lived entirely on her Ubuntu laptop. It has a respectable history — four base commits, two merges, a squash, a resolved conflict — and main sits at c2a8f1e. In lesson 02-02 we watched Bruno clone the project and saw that the clone registered a remote called origin without anybody asking it to. That lesson explained half the story: how a remote is inherited. This one explains the other half: how to create one from scratch, which is what Ana needs now, because her repository was born with git init and has never spoken to anybody in its life.

We will also open .git/config and pick apart the line fetch = +refs/heads/*:refs/remotes/origin/*. It is called a refspec, it appears in every repository in the world and hardly anyone knows what it says. Understanding it is what turns fetch and push from magic commands into predictable ones.

Contents

  1. The starting point: a repository with no remotes
  2. git remote add: registering the first one
  3. git remote -v: which remotes do I have
  4. git remote show: the full X-ray
  5. How it ends up in .git/config
  6. The refspec, explained calmly
  7. Renaming, removing and changing the URL
  8. set-url --push: read from one place, write to another
  9. Working with several remotes
  10. init versus clone: two ways of having a remote

  1. The starting point: a repository with no remotes

Ana is in her project, exactly as we left it at the end of module 3:

cd ~/projects/task-manager
git log --oneline -3
c2a8f1e (HEAD -> main) Merge the empty-list message
6d3f8b2 Show a message when the list is empty
3b9e7d1 Add CSV export of the task list
git branch
  docs/update-notes
  fix/focus-after-delete
* main

And now the question of the day:

git remote
(no output)

Silence. There is no remote configured, which is exactly what you would expect in a repository created with git init. We can confirm it by looking at the disk:

cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true

Four lines of basic configuration and no [remote …] section at all. Compared with Bruno's .git/config that we saw in lesson 02-02, both the remote section and the branch tracking section are missing here.

Before we go on, somebody has to have created the destination repository. In a real case, Ana would go into the web interface of GitHub, GitLab or the company's Gitea and click "New repository", getting a URL back. To practise without depending on anything, we set up the exact equivalent with what we learnt in the previous lesson:

mkdir -p /tmp/server
git init --bare /tmp/server/task-manager.git
Initialized empty Git repository in /tmp/server/task-manager.git/

Important: the destination repository is empty. Not one commit, not one branch. That is normal and desirable: publishing a project means pouring your history into a freshly created repository.

In the narrative we will use the team's real URL, https://git.example.com/team/task-manager.git; in the commands you can reproduce, the local path. They are interchangeable: as far as Git is concerned, both are valid URLs.

  1. git remote add: registering the first one

The syntax is as simple as the concept:

git remote add <name> <url>

Ana runs it:

git remote add origin https://git.example.com/team/task-manager.git
(no output)

Absolute silence, which in Git means success. And rightly so: nothing has happened beyond writing three lines into a text file. It has not connected anywhere, has not checked that the URL exists, has not downloaded anything and has not asked for credentials.

Let us prove it in the most emphatic way possible:

git remote add nonexistent https://this.server.does.not.exist/nothing.git
(no output)

Git accepts it happily. git remote add is a purely local, offline operation. Only when you run fetch or push against that name will Git discover that the URL leads nowhere. This detail explains a lot of bafflement: people believe "the remote must be fine because git remote add did not complain", when that command never complains about a bad URL.

Let us clean up the experiment:

git remote remove nonexistent

Rules for the name

  • It must be unique within the repository: if origin already exists, git remote add origin … fails with error: remote origin already exists.
  • It accepts letters, numbers, hyphens and underscores. Avoid spaces, slashes and accented characters.
  • There are no reserved words. origin is pure convention, as we saw in 04-01.

The -f option

git remote add -f origin https://git.example.com/team/task-manager.git

With -f (fetch), Git registers the remote and immediately runs a git fetch against it. It is a handy shortcut when you add a remote that already has content and you want it straight away. In Ana's case it adds nothing, because the destination repository is empty.

  1. git remote -v: which remotes do I have

The two basic query commands:

git remote
origin

Names only. And the useful version:

git remote -v
origin	https://git.example.com/team/task-manager.git (fetch)
origin	https://git.example.com/team/task-manager.git (push)

Why does the same remote appear twice? Because Git stores the read URL (fetch) and the write URL (push) separately. They are usually identical, which is why the line repeats, but they can differ: we will see that in section 8, and there the output of -v stops being redundant and becomes informative.

git remote -v should be your first reflex when you land in somebody else's repository, along with git status and git log --oneline -5. In three seconds it tells you who that repository talks to.

  1. git remote show: the full X-ray

This command is in another category, and it is worth understanding why:

git remote show origin

This one does hit the network. Unlike git remote -v, which only reads your .git/config, git remote show queries the server to ask which branches it has right now. With no connection or with failing credentials, this command fails.

Since Ana's repository is still empty, the output is sparse. Let us look instead at the one Bruno gets later on, once there is content, because that is the one that teaches you something:

git remote show origin
* remote origin
  Fetch URL: https://git.example.com/team/task-manager.git
  Push  URL: https://git.example.com/team/task-manager.git
  HEAD branch: main
  Remote branches:
    docs/update-notes               tracked
    fix/focus-after-delete          tracked
    main                            tracked
    experiment/pwa                  new (next fetch will store in remotes/origin)
    feature/old-idea                stale (use 'git remote prune' to remove)
  Local branches configured for 'git pull':
    main                            merges with remote main
  Local refs configured for 'git push':
    main                            pushes to main            (up to date)

It is worth reading line by line, because it packs in five separate pieces of information:

Section What it means
Fetch URL / Push URL The two configured URLs. If they differ, you see it here
HEAD branch The server's default branch. It determines which branch a git clone leaves you on
Remote branches The branches that exist right now on the server, with their status
Local branches configured for 'git pull' Which local branch integrates with which remote one (lesson 04-06)
Local refs configured for 'git push' Which local branch goes where when pushing, and whether it is up to date

The three possible states of a remote branch deserve an explanation:

  • tracked: it exists on the server and you hold its local reference. The normal situation.
  • new: it exists on the server but you do not have it yet. Somebody created it since your last fetch.
  • stale: you hold the reference but it no longer exists on the server. Somebody deleted it there. You clear it out with git remote prune, which we will see in lesson 04-04.

If you want the information without going out to the network:

git remote show -n origin

With -n (no query), Git shows only what it knows from its local configuration. It is fast and works offline, but the list of remote branches will be the one from your last synchronisation, not the real one.

  1. How it ends up in .git/config

Let us go back to the file, which is where you really see what has happened:

cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "origin"]
	url = https://git.example.com/team/task-manager.git
	fetch = +refs/heads/*:refs/remotes/origin/*

That is everything git remote add did. A new section with two keys. You can edit it by hand in any editor and it will work just the same; the git remote commands are nothing more than a convenient interface for writing into this file.

The same keys can be queried and modified with git config, because they are perfectly ordinary configuration:

git config remote.origin.url
https://git.example.com/team/task-manager.git
git config remote.origin.fetch
+refs/heads/*:refs/remotes/origin/*

Remember from lesson 01-05 that these keys live at the local level (.git/config), the most specific of the three. A remote belongs to one particular repository, and configuring it with --global would make no sense at all.

And now, that second line.

  1. The refspec, explained calmly

fetch = +refs/heads/*:refs/remotes/origin/*

This line appears in every Git repository on the planet and hardly anyone could explain it. That is a shame, because it is straightforward and because understanding it lights up the whole module. It is called a refspec: a specification of how the other repository's references are translated into references of your own.

The structure

A refspec has three parts:

+refs/heads/*:refs/remotes/origin/*
│└─────────┬┘ └────────────┬─────┘
│      SOURCE           DESTINATION
│   (over there, in     (over here, in my
│    the remote          repository)
│    repository)
│
└── optional modifier
Part Value Meaning
+ Modifier Allows non-fast-forward updates: writes the new value even when it is not a clean advance
refs/heads/* Source All the branches of the remote repository
: Separator "translates to"
refs/remotes/origin/* Destination They are stored here, under the remote's namespace

Read out in plain English:

"Take every branch from the remote and store it on my disk under refs/remotes/origin/, overwriting whatever is there."

Concrete examples of the translation the asterisk performs:

Branch on the server Stored on your disk as Short name you use
refs/heads/main refs/remotes/origin/main origin/main
refs/heads/docs/update-notes refs/remotes/origin/docs/update-notes origin/docs/update-notes
refs/heads/fix/focus-after-delete refs/remotes/origin/fix/focus-after-delete origin/fix/focus-after-delete

Here is where the slash in origin/main comes from, the question left open at the end of module 3. It is not an arbitrary separator or a special syntax: it is the real path of the file inside .git/refs/. origin/main is called that because it sits at refs/remotes/origin/main.

flowchart LR
    subgraph REMOTE["Server: refs/heads/"]
        R1["main"]
        R2["docs/<br/>update-notes"]
        R3["fix/<br/>focus-after-delete"]
    end
    subgraph LOCAL["Your disk: refs/remotes/origin/"]
        L1["origin/main"]
        L2["origin/docs/<br/>update-notes"]
        L3["origin/fix/<br/>focus-after-delete"]
    end
    R1 -->|refspec| L1
    R2 -->|refspec| L2
    R3 -->|refspec| L3

Why that translation exists

It might look like an unnecessary detour. Would it not be simpler for the server's branches to be copied straight onto your local branches?

No, and for a fundamental reason: they would be the same references and would trample each other. If the server's refs/heads/main were copied over your refs/heads/main, every fetch would destroy your unpushed local work. The separate namespace is what allows three things to coexist:

  • Your main, which you control and where you commit.
  • Your photograph of their main (origin/main), which Git updates.
  • Their main, which is on another machine.

It is also what makes it possible for Git to tell you "you are 2 commits ahead": it holds the two references separately and can compare them.

The leading +

The + modifier means "update this reference even when the change is not a fast-forward".

Remember from module 3 that a fast-forward is a clean advance: the new value is a descendant of the old one. Without the +, Git would refuse to update origin/main if somebody had rewritten history on the server (with a push --force, say), because the new commit would not be a descendant of the one you had recorded.

For remote references we want the +: their job is to faithfully reflect the server's state, whatever it happens to be. If the server changed in a strange way, your photograph must show that strange state; you can then decide what to do with your local branches. Protection against rewrites applies in the other direction, when pushing, and it is the subject of lesson 04-05.

Custom refspecs

The default refspec brings all the branches, but it can be narrowed down. If a repository has hundreds of branches and you only care about main:

git config remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'

No wildcard: that branch only. This is in fact what git clone --single-branch does internally, as mentioned in lesson 02-02.

You can stack several lines to bring in several groups:

git config --add remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'
git config --add remote.origin.fetch '+refs/heads/release/*:refs/remotes/origin/release/*'
[remote "origin"]
	url = https://git.example.com/team/task-manager.git
	fetch = +refs/heads/main:refs/remotes/origin/main
	fetch = +refs/heads/release/*:refs/remotes/origin/release/*

Now git fetch origin will bring main and every release branch, and ignore the rest. In a corporate repository with seven hundred open branches, that is the difference between a two-second fetch and a thirty-second one.

The same concept comes back when pushing. When you write git push origin main:main in lesson 04-05, you will be supplying a refspec by hand, in the same source:destination syntax. The consistency is complete: the two sides of : always mean the same thing, "from here" and "to here".

  1. Renaming, removing and changing the URL

Three maintenance operations, all local, all instant and none of them dangerous.

Renaming

git remote rename <old> <new>
git remote rename origin central
git remote -v
central	https://git.example.com/team/task-manager.git (fetch)
central	https://git.example.com/team/task-manager.git (push)

And this is not just a change of label. Renaming a remote forces Git to rewrite everything that depended on the name:

  • The remote references move from refs/remotes/origin/* to refs/remotes/central/*. Your origin/main becomes central/main.
  • The refspec is updated to point at the new namespace.
  • The branches' tracking configuration (branch.main.remote) is rewritten so that main keeps pointing at the right remote.
git config --get-regexp '^(remote|branch)\.'
remote.central.url https://git.example.com/team/task-manager.git
remote.central.fetch +refs/heads/*:refs/remotes/central/*
branch.main.remote central
branch.main.merge refs/heads/main

All consistent. Git has done the complete job. Let us go back to the conventional name:

git remote rename central origin

Removing

git remote remove <name>     # the recommended form
git remote rm <name>         # equivalent alias

Removing a remote deletes its section from .git/config, all its remote references (refs/remotes/<name>/*) and the tracking configuration of the branches that used it.

What it does not delete: not a single commit. The objects stay in .git/objects/. If you remove a remote by mistake, just add it again and run fetch; the only thing you lose is time.

Changing the URL

This is the most frequent case in real life: the company migrates servers, or you move from HTTPS to SSH after setting up your keys.

git remote set-url origin [email protected]:team/task-manager.git
git remote -v
origin	[email protected]:team/task-manager.git (fetch)
origin	[email protected]:team/task-manager.git (push)

Same repository, same history, same references: only the route to get there changes. Nothing else is affected, because a commit's identity lies in its hash, not in the URL it arrived from.

This is where that idea from lesson 04-01 becomes tangible: the "official" repository is official by convention. Migrating a whole team to another server is one set-url per person.

Command What it touches Needs network
git remote add Creates the section in .git/config No
git remote -v Only reads .git/config No
git remote show <n> Reads config and asks the server Yes
git remote rename Config + remote references + tracking No
git remote remove Deletes config + remote references + tracking No
git remote set-url The URL only No
git remote prune <n> Deletes references to branches that no longer exist Yes

  1. set-url --push: read from one place, write to another

A twist that explains why git remote -v distinguishes (fetch) from (push):

git remote set-url --push origin [email protected]:team/task-manager.git
git remote -v
origin	https://git.example.com/team/task-manager.git (fetch)
origin	[email protected]:team/task-manager.git (push)

Ana now reads over HTTPS and writes over SSH. Both paths lead to the same repository, but by different routes.

What is this good for in practice?

  • Reading without credentials, writing with them. A public repository is cloned over HTTPS without authenticating; pushing requires an identity, and SSH supplies it without typing anything (lesson 04-03).
  • Corporate firewalls. On some networks only port 443 for HTTPS is open outbound, but pushing goes by another route.
  • Read-only mirrors. Read from a fast, nearby replica, always write to the original.
  • Preventing accidental pushes. There is a well-known trick: point the push URL at something invalid so that no accidental push ever gets through.
# Turn a remote into a strictly read-only one
git remote set-url --push origin DO_NOT_PUSH
git push origin main
fatal: 'DO_NOT_PUSH' does not appear to be a git repository
fatal: Could not read from remote repository.

Good insurance when you clone somebody else's project purely to read it.

To undo any of these settings:

git remote set-url --delete --push origin DO_NOT_PUSH

And the related options, to round out the picture:

# Add an extra URL (pushes to both: handy for mirrors)
git remote set-url --add --push origin [email protected]:team/task-manager.git

# See every URL of every remote, including the multiple ones
git remote -v

With two push URLs, a single git push sends to both places. It is the simplest way to keep a mirror in sync without external tools.

  1. Working with several remotes

A repository can have as many remotes as you like, and that is no oddity: it is the norm as soon as a project grows a little.

Suppose Bruno also keeps a personal copy of the project and the team deploys by pushing to a staging server:

git remote add personal [email protected]:bruno/task-manager.git
git remote add staging [email protected]:apps/task-manager.git
git remote -v
origin		https://git.example.com/team/task-manager.git (fetch)
origin		https://git.example.com/team/task-manager.git (push)
personal	[email protected]:bruno/task-manager.git (fetch)
personal	[email protected]:bruno/task-manager.git (push)
staging		[email protected]:apps/task-manager.git (fetch)
staging		[email protected]:apps/task-manager.git (push)

Each remote has its own reference space, completely separate:

git fetch --all
git branch -r
  origin/HEAD -> origin/main
  origin/main
  origin/docs/update-notes
  personal/main
  personal/experiment/pwa
  staging/main

Now origin/main, personal/main and staging/main are three different references that can point at three different commits. And you can compare them like any other reference:

# What does my personal copy have that the team repository does not?
git log --oneline origin/main..personal/main

# Is staging behind the team?
git log --oneline staging/main..origin/main

# Bring a branch over from my personal copy
git switch -c experiment/pwa personal/experiment/pwa

What having several is good for

Situation Typical setup
Contributing to somebody else's project origin = your personal copy; upstream = the original project (module 7)
Deploying by push origin = code; production = deployment server (module 10)
Mirror or backup origin = the main one; mirror = the replica
Server migration origin = the old one; new = the destination, while the transition lasts
Direct exchange between colleagues origin = server; bruno = his laptop, for occasional four-handed work

That last case illustrates what we were saying in 04-01 about Git's distributed nature. If Ana and Bruno are sitting in the same room working together:

# Ana registers Bruno's repository, reachable over SSH on the local network
git remote add bruno [email protected]:/home/bruno/projects/task-manager
git fetch bruno
git log --oneline bruno/feature/new-idea

No server in the middle, nothing published. It is Git doing exactly what it was designed for.

And one useful command when you have several:

# Fetch from every remote at once
git fetch --all
Fetching origin
Fetching personal
Fetching staging

  1. init versus clone: two ways of having a remote

We close with the comparison that makes sense of the whole lesson, because it is the difference between Ana's path and Bruno's.

Repository from git init (Ana) Repository from git clone (Bruno)
Remote at the start None origin, automatically
Must you run remote add Yes No
Remote references at the start None All the server's
Local branch to start from Whichever you create The server's default branch
Tracking configured No: you have to set it up Yes, automatically
First push git push -u origin main Plain git push
Starting history Yours The server's
flowchart TB
    Q{"Does the project already<br/>exist on a server?"}
    Q -->|"No: I am starting it"| I["git init<br/>work, commit<br/><b>git remote add origin URL</b><br/>git push -u origin main"]
    Q -->|"Yes: I am joining"| C["git clone URL<br/><b>origin already registered</b><br/>tracking already configured<br/>get to work"]
    I --> F["Repository with a remote<br/>and tracking"]
    C --> F

Both paths arrive at the same place. clone is nothing more than init + remote add + fetch + switch, packed into a single command, just as we broke it down in lesson 02-02. You now have the loose pieces and can assemble them by hand:

# A "manual" clone, step by step, equivalent to git clone
mkdir task-manager && cd task-manager
git init
git remote add origin https://git.example.com/team/task-manager.git
git fetch origin
git switch main

That last git switch main deserves a note: even though no local branch called main exists yet, Git sees that origin/main does, works out what you mean and creates the local branch with its tracking already configured. It is called DWIM (Do What I Mean) and it is the subject of lesson 04-06.

Ana is ready

Ana's repository now has its remote:

git remote -v
origin	https://git.example.com/team/task-manager.git (fetch)
origin	https://git.example.com/team/task-manager.git (push)

But she has not pushed anything yet. And if she tried right now against a real server over HTTPS, she would run into this:

Username for 'https://git.example.com':

A question she cannot answer, because her web-interface password probably will not do. Authentication has to be sorted out before pushing, and that is exactly the subject of the next lesson.

Common Mistakes and Tips

Mistake 1: believing that git remote add validates the URL. It does not: it only writes to a file. You can register https://this.does.not.exist/nothing.git without a single warning. The first real check comes with git fetch, git push or git ls-remote.

Mistake 2: error: remote origin already exists. This happens when you try to add a remote that is already there, very typical in cloned repositories where origin comes preset. If what you want is to change its address, the command is git remote set-url origin <url>, not add.

Mistake 3: confusing git remote -v with git remote show. The first reads your configuration and works offline; the second asks the server and can fail because of the network or credentials. To diagnose "which URL do I have configured?" use -v; for "what is on the server right now?", show.

Mistake 4: not understanding the refspec, and therefore not understanding why origin/main is not a branch. The line +refs/heads/*:refs/remotes/origin/* says literally where the remote's references are stored: in a namespace separate from your branches. Without that separation, every fetch would flatten your work.

Mistake 5: trying to git push to a non-bare remote. If you set up the server yourself with git init instead of git init --bare, the push will be rejected with branch is currently checked out. Remember lesson 04-01: repositories that receive pushes must be bare.

Mistake 6: letting dead remotes pile up. After a server migration, plenty of people add the new one and leave the old one in place. Months later, git fetch --all takes forever waiting for a switched-off server to time out. Clean up with git remote remove.

Tip 1: check a freshly added remote with git ls-remote. It is the cheapest way to verify that the URL, the network and the credentials work, without downloading any objects:

git ls-remote origin

If it returns the server's list of references, everything is in order. If it fails, you get the specific error before having invested anything.

Tip 2: always use origin for the main repository. The convention is so entrenched that any documentation, script or colleague will assume it. Save your creativity for the secondary remotes.

Tip 3: build git remote -v into your arrival routine for a repository. That command and git log --oneline --graph -10 give you the complete map of where you are in five seconds.

Exercises

Exercise 1: publishing an existing project

Simulate Ana's case from start to finish, locally:

  1. Create a normal repository with two or three commits and one extra branch.
  2. Check that it has no remote.
  3. Create a bare repository to act as the server.
  4. Register it as origin.
  5. Show the contents of .git/config before and after, pointing out exactly which lines the command added.
  6. Verify with git ls-remote that the remote responds, and explain what it returns while the server is empty.

Exercise 2: decoding refspecs

Translate each of these refspecs into plain English and say what a git fetch would do with each one:

  1. +refs/heads/*:refs/remotes/origin/*
  2. +refs/heads/main:refs/remotes/origin/main
  3. refs/heads/main:refs/remotes/origin/main (without the +)
  4. +refs/heads/release/*:refs/remotes/origin/release/*
  5. +refs/tags/*:refs/tags/*

Then configure number 4 in a test repository and demonstrate with commands that a fetch no longer brings branches that do not start with release/.

Exercise 3: two remotes and a comparison

Set up this situation and solve it with commands:

  1. A bare team.git and another bare personal.git.
  2. A working repository with both registered as origin and personal.
  3. Push a different commit to each one, so that their histories diverge.
  4. Answer: which commits does personal have that origin does not? And the other way round?
  5. Configure the repository to read from origin but push to personal, and demonstrate that it works.

Solutions

Solution 1:

mkdir -p /tmp/ex1 && cd /tmp/ex1

# 1. Repository with a history
git init -b main project
cd project
echo "<h1>Task manager</h1>" > index.html
git add . && git commit -m "Add initial task manager structure"
echo "body { font-family: sans-serif; }" > styles.css
git add . && git commit -m "Add base styles for the list"
git switch -c docs/update-notes
echo "# Task manager" > README.md
git add . && git commit -m "Add the project README"
git switch main
# 2. No remotes
git remote -v
(no output)
# 5a. The configuration before
cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
# 3. The server
git init --bare /tmp/ex1/server.git

# 4. Register it
git remote add origin /tmp/ex1/server.git
# 5b. The configuration after
cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "origin"]
	url = /tmp/ex1/server.git
	fetch = +refs/heads/*:refs/remotes/origin/*

The two new lines are the [remote "origin"] section with its url and its fetch. Nothing more. No references, no objects, no connection.

# 6. Check that it responds
git ls-remote origin
(no output, and exit code 0)
echo $?
0

The absence of output is the correct answer: the server replied and its list of references is empty, because the bare repository does not have a single commit yet. What matters is that the exit code is 0: it means Git got there and was able to talk. Compare it with a bad URL:

git ls-remote /tmp/ex1/does-not-exist.git
fatal: '/tmp/ex1/does-not-exist.git' does not appear to be a git repository

Solution 2:

  1. +refs/heads/*:refs/remotes/origin/* — "Bring every branch from the remote and store them under refs/remotes/origin/, forcing the update even when it is not a clean advance." This is the default behaviour of any clone.

  2. +refs/heads/main:refs/remotes/origin/main — "Bring only the main branch and store it as origin/main." The server's other branches are ignored entirely; they will not even show up in git branch -r. Equivalent to --single-branch.

  3. refs/heads/main:refs/remotes/origin/main — The same, but without the +: if history were rewritten on the server's main, the fetch would fail instead of updating the reference, because the new value would not be a descendant of the old one. You rarely want this on a remote reference, whose job is to mirror the server as it is.

  4. +refs/heads/release/*:refs/remotes/origin/release/* — "Bring only the branches whose name starts with release/ and keep them with the same structure under origin/release/." Useful in huge repositories where only the release branches matter to you.

  5. +refs/tags/*:refs/tags/* — "Bring every tag from the remote and store them as local tags, under the same name." Notice that here there is no namespace translation: source and destination are the same. Tags are not separated per remote the way branches are, and that is why a fetched tag is indistinguishable from one you created yourself. Tags are the subject of lesson 05-05.

Checking case 4:

cd /tmp/ex1/project

# Set the server up with several branches
git push origin main
git switch -c release/1.0 && git commit --allow-empty -m "Version 1.0" && git push origin release/1.0
git switch -c release/1.1 && git commit --allow-empty -m "Version 1.1" && git push origin release/1.1
git switch main
git push origin docs/update-notes

# A fresh clone with a restricted refspec
cd /tmp/ex1
git clone server.git restricted
cd restricted
git config remote.origin.fetch '+refs/heads/release/*:refs/remotes/origin/release/*'

# Delete the references it already had, to start from scratch
git branch -r | grep -v HEAD | sed 's/^ *//' | xargs -r -n1 git branch -rd

git fetch origin
git branch -r
  origin/release/1.0
  origin/release/1.1

origin/main and origin/docs/update-notes do not appear. The restricted refspec has left them out: Git did not even bother to fetch them.

Solution 3:

mkdir -p /tmp/ex3 && cd /tmp/ex3

# 1. The two servers
git init --bare team.git
git init --bare personal.git

# 2. The working repository with both remotes
git init -b main work
cd work
echo "<h1>Task manager</h1>" > index.html
git add . && git commit -m "Add initial task manager structure"

git remote add origin /tmp/ex3/team.git
git remote add personal /tmp/ex3/personal.git
git remote -v
origin	/tmp/ex3/team.git (fetch)
origin	/tmp/ex3/team.git (push)
personal	/tmp/ex3/personal.git (fetch)
personal	/tmp/ex3/personal.git (push)
# 3. A common base and then divergence
git push origin main
git push personal main

echo "console.log('team');" > app.js
git add . && git commit -m "Add the skeleton of the task logic"
git push origin main

git reset --hard HEAD~1
echo "console.log('personal');" > experiment.js
git add . && git commit -m "Try out a loose idea"
git push personal main

git fetch --all
# 4. Compare the two remotes
git log --oneline origin/main..personal/main
9c4e2b7 Try out a loose idea
git log --oneline personal/main..origin/main
5a1d8f3 Add the skeleton of the task logic

Each one has a commit the other does not: they have diverged from the common initial commit. You can see it all at once:

git rev-list --left-right --count origin/main...personal/main
1	1

One exclusive commit on each side. This three-dot syntax and how to read it are the subject of lesson 04-06.

# 5. Read from origin, push to personal
git remote set-url --push origin /tmp/ex3/personal.git
git remote -v
origin	/tmp/ex3/team.git (fetch)
origin	/tmp/ex3/personal.git (push)
personal	/tmp/ex3/personal.git (fetch)
personal	/tmp/ex3/personal.git (push)
# Demonstration: a fetch from origin brings the team's work…
git fetch origin
git log --oneline -1 origin/main
5a1d8f3 Add the skeleton of the task logic
# …but a push to origin writes into personal
git commit --allow-empty -m "Check the push destination"
git push origin main
git --git-dir=/tmp/ex3/personal.git log --oneline -1 main
e7b3f2a Check the push destination
# And the team repository has NOT received it
git --git-dir=/tmp/ex3/team.git log --oneline -1 main
5a1d8f3 Add the skeleton of the task logic

Confirmed: origin reads from team.git and writes to personal.git. It is the mechanism used by people who clone somebody else's project read-only and push their changes to their own copy.

Conclusion

In this lesson Ana took the step she had been putting off for three modules: connecting her repository to the world. What you have learnt:

  • git remote add <name> <url> registers a remote, and it is a purely local operation: it writes three lines into .git/config, does not validate the URL and does not touch the network. The first real check comes with fetch, push or git ls-remote.
  • git remote -v lists the remotes with their read and write URLs, offline. git remote show <name> does query the server and gives you the full X-ray: default branch, remote branches with their status (tracked, new, stale) and the pull and push configuration.
  • Everything lives in .git/config, in a [remote "origin"] section with two keys: url and fetch.
  • The refspec +refs/heads/*:refs/remotes/origin/* says: "bring every branch from the remote and store them under refs/remotes/origin/, forcing the update". The + allows non-fast-forward updates; the : separates source from destination. That is where the slash in origin/main comes from: it is the real path of the file. And that namespace separation is what stops a fetch from flattening your local branches.
  • Renaming, removing and changing the URL are local, harmless operations. Renaming also moves the remote references and rewrites the tracking configuration; removing deletes no commit.
  • set-url --push lets you read from one place and write to another, or block pushes by pointing at an invalid URL.
  • Several remotes coexist without any trouble, each with its own reference space, and they are compared with one another like any other commit name.
  • init versus clone: the first requires registering the remote by hand; the second inherits it. Both arrive at the same place, because clone is init + remote add + fetch + switch.

What comes next

Ana's remote is configured, but if she tried to push right now she would hit an uncomfortable question: Username for 'https://git.example.com':. Registering a URL does not grant permission to write to it.

In lesson 04-03: Authenticating with Remote Repositories we take down that wall. We will see why cloning a public repository asks for nothing while pushing does, what a personal access token is and why it replaced the password, how to generate and use an SSH key with ssh-keygen -t ed25519, and how each operating system stores credentials so that you do not have to type them forty times a day. It matters especially for Carla, who is about to join from Windows 11 and needs this sorted out before writing her first line of code.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved