Throughout this module, messages have kept appearing that we kept putting off:

Your branch is ahead of 'origin/main' by 2 commits.
branch 'main' set up to track 'origin/main'.
  main  b4d7e93 [origin/main: behind 3] Adjust the footer style

And behaviours Git worked out on its own: plain git push knowing where to go, git pull with no arguments, and that git switch docs/update-notes which created a local branch that did not exist.

All of it points at the same concept, and it is the one that closes the module: tracking branches. A simple mechanism — two lines of configuration per branch — on which half of Git's conveniences and most of its status messages depend.

Along the way we are going to settle once and for all the confusion we have been carrying since lesson 04-01: main, origin/main and the server's main are three different things with similar names that people mix up constantly. By the end of this lesson that distinction will be automatic, and with it the whole module falls into place.

Contents

  1. What a tracking branch is
  2. Where it lives: branch.<name>.remote and branch.<name>.merge
  3. The three things called main
  4. Where "ahead of 'origin/main' by 2 commits" comes from
  5. git branch -vv: every branch's state at a glance
  6. Setting, changing and removing the upstream
  7. Creating a local branch from a remote one: Git's DWIM
  8. The ambiguous case of several remotes
  9. @{upstream} and @{push}: referring to the tracking branch
  10. Closing the module

  1. What a tracking branch is

The definition:

A tracking branch is a local branch that has a recorded association with one specific remote reference. That associated reference is called its upstream.

Put plainly: your main branch "knows" that its counterpart on the server is origin/main. And with that one fact, Git can:

  • Push with no arguments: git push knows which remote and which branch.
  • Fetch and pull with no arguments: git pull too.
  • Report the gap: "you are 2 commits ahead and 3 behind".
  • Offer shortcuts: @{upstream} to refer to the counterpart without typing its name.

Without that association everything still works, but you have to spell it all out:

# Without tracking
git push origin feature/alphabetical-order
git pull origin feature/alphabetical-order
git log feature/alphabetical-order..origin/feature/alphabetical-order

# With tracking
git push
git pull
git log @{u}..

One important note on terminology, because Git's own documentation is not always consistent. There are two uses of "tracking branch" that are worth keeping apart:

Expression What it refers to
Tracking branch A local branch that has an upstream configured. Your main, for instance
Remote-tracking branch The remote reference itself: origin/main. It is not a branch of yours

And remember too the other sense of the word upstream, the one from lesson 04-01: the conventional name for the remote pointing at the original project you forked from (module 7). Three uses of two words. Here, whenever we say upstream, we mean the remote reference associated with a local branch.

  1. Where it lives: branch.<name>.remote and branch.<name>.merge

As with everything in Git, this is not magic: it is two lines in a text file.

cat .git/config
[remote "origin"]
	url = https://git.example.com/team/task-manager.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main
[branch "fix/focus-after-delete"]
	remote = origin
	merge = refs/heads/fix/focus-after-delete

Every branch with tracking has its own section:

Key Value What it means
branch.main.remote origin Which remote this branch talks to
branch.main.merge refs/heads/main Which branch on that remote is its counterpart

One detail throws people the first time they read it: merge = refs/heads/main is the name of the branch on the server, not refs/remotes/origin/main. There is logic to it: the configuration says "my counterpart is the main branch of the remote repository"; it is the refspec's job to know that this branch is stored locally at refs/remotes/origin/main.

You query them like any other setting:

git config branch.main.remote
git config branch.main.merge
origin
refs/heads/main

And there is an optional key you may come across:

git config branch.main.rebase

If it is true, git pull on that branch will use --rebase instead of merging (lesson 05-01).

Take this in: tracking is local configuration in your repository. It does not travel with a push, nobody else can see it and every person on the team has their own. Bruno can have his main following origin/main while Carla has hers following mirror/main, and neither of them will ever know.

  1. The three things called main

This is the key section of the lesson, and it deserves your full attention. These three things have similar names and are not the same:

main origin/main the server's main
What it is Your local branch Your photograph of the server The real branch in the remote repository
Where it lives .git/refs/heads/main, on your disk .git/refs/remotes/origin/main, on your disk On another machine
Who moves it You, by committing or merging Git, on fetch/pull/push Anyone on the team, by pushing
Can you commit on it Yes No Not directly
Can it go out of date It is yours: it is where you left it Yes, constantly It is the truth of the moment
You see it with git branch git branch -r git ls-remote origin
flowchart TB
    subgraph YOURS["YOUR DISK"]
        M["<b>main</b><br/>refs/heads/main<br/>You move it by committing"]
        OM["<b>origin/main</b><br/>refs/remotes/origin/main<br/>Photograph of the server.<br/>Moved by fetch/pull/push"]
    end
    subgraph SRV["ANOTHER MACHINE"]
        SM["<b>main</b><br/>refs/heads/main<br/>Moved by whoever pushes"]
    end

    M -.->|"tracking configured:<br/>branch.main.remote = origin<br/>branch.main.merge = refs/heads/main"| OM
    OM <-->|"fetch: updates the photograph<br/>push: updates the server"| SM

    style M fill:#fff0e8
    style OM fill:#e8f0ff
    style SM fill:#e8f4e8

The rule of thumb

Whenever something about remotes baffles you, ask yourself: which of the three am I talking about?

Examples you can apply straight away:

  • "My colleague says it is already up there but I cannot see it." → You are both talking about the server's main; what you are looking at is your origin/main, which is out of date. Fix: git fetch.
  • "I ran fetch and my files have not changed." → A fetch updates origin/main; your files depend on main. That is correct behaviour.
  • "I ran git reset --hard origin/main and lost my commits." → You moved your main to where your photograph of the server sits. The commits are still in the reflog (lesson 09-04), but the branch no longer reaches them.
  • "git push says Everything up-to-date but my change is not there." → Your main is where your origin/main is; the change was never committed.

One experiment fixes the idea better than any explanation:

# 1. Commit something: only main moves
git commit --allow-empty -m "Test"
git rev-parse main origin/main
d5e9b2f8a3c1e6b4d7f2a9c5e8b1d4f6a3c7e9b2   ← main has moved on
b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7   ← origin/main has not
# 2. Push: now both move (and the server's too)
git push
git rev-parse main origin/main
d5e9b2f8a3c1e6b4d7f2a9c5e8b1d4f6a3c7e9b2
d5e9b2f8a3c1e6b4d7f2a9c5e8b1d4f6a3c7e9b2

A successful push updates your origin/main as well, and that makes sense: Git has just spoken to the server and knows for certain where its reference ended up.

  1. Where "ahead of 'origin/main' by 2 commits" comes from

git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

That message does not come from anywhere magical: Git compares your branch against its upstream and counts the commits on each side. It can only do that because tracking is configured; without it, git status would say nothing about the server at all.

The command that does the arithmetic

git rev-list --left-right --count main...origin/main
2	0

Let us take it apart, because every piece matters:

  • main...origin/mainthree dots, not two. This is the symmetric difference: every commit reachable from one of the two references but not from both. In other words, what is exclusive to each side.
  • --left-right — marks each commit according to which side it came from: < for the left one (main), > for the right one (origin/main).
  • --count — instead of listing the commits, it counts how many there are on each side.

The result reads like this:

2	0
│   └── commits origin/main has and main does not   → "behind"
└────── commits main has and origin/main does not   → "ahead"

Two dots versus three dots, which is the most common source of confusion:

Syntax Meaning Typical use
main..origin/main What origin/main has and main does not "What have I still to fetch?"
origin/main..main What main has and origin/main does not "What have I still to push?"
main...origin/main What is exclusive to each side "How far have we diverged?"

Without --count, you see it commit by commit:

git rev-list --left-right --oneline main...origin/main
<d5e9b2f Return focus to the field after deleting a task
<8a3f7c1 Fix the focus after deleting a task
>b4d7e93 Adjust the footer style

Two of mine (<) and one of theirs (>): we have diverged, and git status will put it like this:

Your branch and 'origin/main' have diverged,
and have 2 and 1 different commits each, respectively.

The four possible states

rev-list returns Situation What git status says What to do
0 0 In sync "up to date with 'origin/main'" Nothing
N 0 Ahead "ahead of 'origin/main' by N commits" git push
0 N Behind "behind 'origin/main' by N commits, and can be fast-forwarded" git pull
N M Diverged "have diverged" Integrate (see 09-03)
flowchart TB
    B["Common base"]
    B --> A1["A"] --> A2["B<br/><b>main</b>"]
    B --> C1["C"] --> C2["D<br/><b>origin/main</b>"]

    N["main...origin/main → 2 and 2<br/>THEY HAVE DIVERGED"]

    style A2 fill:#fff0e8
    style C2 fill:#e8f0ff

A crucial warning about these numbers

The gap is measured against your origin/main, which is a photograph. Not against the server.

If you have not run a fetch for two days, that "2 commits ahead" may be completely false: the server may have moved on fifteen times since. git status never touches the network.

That is why the first command of the day is git fetch: without it, all these numbers describe the past.

And that is why this combination exists, doing both things at once:

git fetch && git status

There is a git status option that sounds as though it does the job by itself:

git status -uno --ahead-behind

But --ahead-behind only controls whether the gap is calculated at all; it does not fetch anything from the server. The only way to refresh the photograph is still fetch.

  1. git branch -vv: every branch's state at a glance

In lesson 03-06 we mentioned -vv and said it would make sense once remotes were in play. That moment has arrived.

git branch -v
  fix/focus-after-delete  d5e9b2f Return focus to the field after deleting a task
  docs/update-notes       7f3c9a2 Update the internal notes with the new flow
* main                    b4d7e93 Adjust the footer style
  experiment/local        3a7e9c2 Try an idea without publishing it

With the double v:

git branch -vv
  fix/focus-after-delete  d5e9b2f [origin/fix/focus-after-delete] Return focus to the field after deleting a task
  docs/update-notes       7f3c9a2 [origin/docs/update-notes: ahead 1] Update the internal notes with the new flow
* main                    b4d7e93 [origin/main: behind 3] Adjust the footer style
  experiment/local        3a7e9c2 Try an idea without publishing it

The square brackets are the new information, and each line tells a different story:

Branch Brackets Reading
fix/focus-after-delete [origin/fix/…] It has an upstream and is in sync
docs/update-notes [origin/…: ahead 1] One local commit not yet pushed
main [origin/main: behind 3] Three commits on the server not yet fetched
experiment/local (nothing) No upstream: it is purely local

That last case is the one to learn to recognise. A branch with no brackets does not exist on any server: lose the disk and you lose that work. Having branches like that is perfectly legitimate (experiments, trials), but it pays to know which ones they are.

And this can turn up too:

  feature/old-idea        9d1e4b7 [origin/feature/old-idea: gone] Mark tasks as done

gone means the branch had an upstream but that upstream no longer exists: somebody deleted the branch on the server and you have run fetch --prune. It is the usual sign of "this branch has been integrated and cleaned up; you can delete yours".

A very useful command for periodic tidying:

# List the branches whose upstream has disappeared
git branch -vv | grep ': gone]'
  feature/old-idea        9d1e4b7 [origin/feature/old-idea: gone] Mark tasks as done
  feature/csv-export      e9a2c5f [origin/feature/csv-export: gone] now it works

These are the natural candidates for git branch -d, and it complements the --merged of lesson 03-06 beautifully (which failed on branches integrated by squash: if the platform deleted it after integrating, it will show up here even though --merged cannot detect it).

Other ways to query the tracking:

# Just the name of the current branch's upstream
git rev-parse --abbrev-ref @{upstream}
origin/main
# With a custom format
git branch --format='%(refname:short) → %(upstream:short) [%(upstream:track)]'
fix/focus-after-delete → origin/fix/focus-after-delete []
docs/update-notes → origin/docs/update-notes [ahead 1]
main → origin/main [behind 3]
experiment/local →  []

The %(upstream:short) and %(upstream:track) placeholders are the same git for-each-ref ones we used in lesson 03-06 for the pretty branch listing.

  1. Setting, changing and removing the upstream

There are four routes to configuring tracking, and it is worth knowing them all because each one turns up at a different moment.

  1. git push -u (the most common)

git push -u origin feature/alphabetical-order

It pushes and configures the tracking in one step. It is the natural route when you publish a new branch, as we saw in lesson 04-05.

  1. Automatically, with push.autoSetupRemote

git config --global push.autoSetupRemote true

We have had this configured since lesson 01-06. With it, a plain git push on a branch with no upstream creates it on the server and configures the tracking, with no -u:

git switch -c feature/alphabetical-order
git commit -am "Show the tasks in alphabetical order"
git push
 * [new branch]      feature/alphabetical-order -> feature/alphabetical-order
branch 'feature/alphabetical-order' set up to track 'origin/feature/alphabetical-order'.

Without that setting, the same git push would have failed with the classic:

fatal: The current branch feature/alphabetical-order has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/alphabetical-order

A message you have seen a thousand times if you have used Git without that setting.

  1. git branch --set-upstream-to (after the fact)

For a branch that already exists in both places but is not paired up:

git branch --set-upstream-to=origin/main main
branch 'main' set up to track 'origin/main'.

There is a short form with -u (which here means the same as it does in push):

git branch -u origin/main

With no branch name, it applies to the current one. It is the command you need when you created the repository with init and added the remote afterwards, or when the configuration has been lost for some reason.

It can point at a branch with a different name, which is useful in migrations:

git branch --set-upstream-to=origin/develop main

Now your local main follows origin/develop. It is unusual, but perfectly valid: the association does not require the names to match.

  1. When creating the branch

# Explicit
git switch -c my-copy --track origin/main

# Or straight from the remote reference (the DWIM of section 7)
git switch main

Removing the tracking

git branch --unset-upstream
git status
On branch main
nothing to commit, working tree clean

Notice what has disappeared: the line about the gap. With no upstream, Git has nothing to compare against and git status goes silent about the server. It is the best demonstration that the message depends entirely on this configuration.

Let us put it back:

git branch -u origin/main

Summary

Command When
git push -u origin <branch> Publishing a branch for the first time
git push (with push.autoSetupRemote) The same, but automatic
git branch -u origin/<branch> The branch already exists on both sides, unpaired
git switch -c <local> --track origin/<branch> Creating a local one from a remote one, with a different name
git switch <branch> (DWIM) Creating a local one from a remote one, with the same name
git branch --unset-upstream To unlink it

  1. Creating a local branch from a remote one: Git's DWIM

Carla has just cloned and wants to work on Ana's documentation branch. She has only one local branch:

git branch
* main
git branch -r
  origin/HEAD -> origin/main
  origin/docs/update-notes
  origin/fix/focus-after-delete
  origin/main

Remote references are not branches of hers, as we have known since lesson 04-01: they are read-only and she cannot commit on them. She needs a local branch.

The explicit route would be this:

git switch -c docs/update-notes --track origin/docs/update-notes

Long and repetitive. Which is why Git has a shortcut:

git switch docs/update-notes
branch 'docs/update-notes' set up to track 'origin/docs/update-notes'.
Switched to a new branch 'docs/update-notes'

This is Git's DWIM (Do What I Mean). The internal reasoning goes:

  1. Is there a local branch called docs/update-notes? No.
  2. Is there exactly one remote reference with that name? Yes, origin/docs/update-notes.
  3. Then this person wants to work on that branch: create the local one from it and configure the tracking.

And the result is exactly the same as the long form:

git branch -vv
* docs/update-notes       7f3c9a2 [origin/docs/update-notes] Update the internal notes with the new flow
  main                    b4d7e93 [origin/main] Adjust the footer style

The same works with checkout for compatibility, although as we know from lesson 03-02 switch is preferable:

git checkout docs/update-notes    # equivalent, but prefer switch

The limits of DWIM

It does not apply if the local name already exists:

git switch main

If you already have a local main, this simply switches to your existing branch. It creates nothing and reconfigures nothing.

It does not work if you have not fetched:

git switch feature/just-created
fatal: invalid reference: feature/just-created

If a colleague has only just created that branch and you have not fetched the references, Git does not know about it. Fix: git fetch first. It is a very common error when joining work already in progress.

It does not work with several ambiguous remotes: that is the next section.

Turning it off

There is a setting that always demands the explicit form:

git config --global checkout.guess false

With it, git switch <branch> works only with existing local branches. Most people prefer DWIM switched on, but it is worth knowing the setting exists if the automatic behaviour makes you uneasy.

  1. The ambiguous case of several remotes

When more than one remote has a branch of the same name, DWIM cannot guess and gives up.

Suppose Bruno has two remotes, as in lesson 04-02:

git remote -v
origin		https://git.example.com/team/task-manager.git (fetch)
origin		https://git.example.com/team/task-manager.git (push)
personal	git@git.example.com:bruno/task-manager.git (fetch)
personal	git@git.example.com:bruno/task-manager.git (push)
git branch -r
  origin/main
  origin/feature/alphabetical-order
  personal/main
  personal/feature/alphabetical-order

And now:

git switch feature/alphabetical-order
fatal: 'feature/alphabetical-order' matched multiple (2) remote tracking branches

Git refuses, and rightly so: choosing for you would mean configuring tracking towards a repository that might not be the one you want, with the risk of pushing work to the wrong place.

The three ways to resolve it

1. Be explicit with the remote reference (the clearest):

git switch -c feature/alphabetical-order origin/feature/alphabetical-order

Or with --track, which also puts the intention on record:

git switch -c feature/alphabetical-order --track origin/feature/alphabetical-order

2. Use the disambiguating syntax:

git switch --track origin/feature/alphabetical-order

With --track and no -c, Git creates a local branch with the same short name as the remote one.

3. Configure a preferred remote:

git config checkout.defaultRemote origin

From then on, faced with ambiguity Git will always choose origin without asking:

git switch feature/alphabetical-order
branch 'feature/alphabetical-order' set up to track 'origin/feature/alphabetical-order'.
Switched to a new branch 'feature/alphabetical-order'

It is the recommended solution if you routinely work with several remotes — for example in the fork workflow of module 7, where having origin and upstream side by side is the norm.

Different local names

With several remotes it sometimes pays to decouple the names so you do not lose track:

git switch -c team-order      origin/feature/alphabetical-order
git switch -c personal-order  personal/feature/alphabetical-order
git branch -vv
  team-order      a1e5c93 [origin/feature/alphabetical-order] Show the tasks in alphabetical order
* personal-order  4f8c2a1 [personal/feature/alphabetical-order] Try another way of sorting

Two local branches with clear names, each following a different remote. Remember that tracking does not require the names to match.

  1. @{upstream} and @{push}: referring to the tracking branch

Git offers a syntax for referring to a branch's upstream without typing its name. It is a small saving but a very comfortable one, and it turns up constantly in aliases and scripts.

# Three ways of saying the same thing
git log origin/main..main
git log @{upstream}..
git log @{u}..

@{upstream} (shortened to @{u}) resolves to the current branch's upstream:

git rev-parse --abbrev-ref @{u}
origin/main

And it can be applied to another branch:

git rev-parse --abbrev-ref docs/update-notes@{u}
origin/docs/update-notes

The most practical uses, all of them independent of whichever branch you happen to be on:

# What have I got unpushed?
git log --oneline @{u}..

# What have I still to fetch?
git log --oneline ..@{u}

# How far have we diverged?
git rev-list --left-right --count @{u}...

# What differs from the server?
git diff @{u}

Notice the elegance of @{u}..: by omitting the right-hand side, Git uses HEAD. And ..@{u} omits the left-hand one, to the same effect. These are the ranges of module 2 making the most of their defaults.

@{push}, the lesser-known sibling

There is a second reference, @{push}, pointing at where a git push would go. Normally it coincides with @{upstream}, but not always: if you work with remote.pushDefault configured, or with push refspecs different from the fetch ones, they can differ.

git rev-parse --abbrev-ref @{push}

It is the right reference for answering "which commits would go out if I pushed right now?":

git log --oneline @{push}..

In the fork workflow of module 7 — where you read from upstream and write to origin — that distinction becomes very useful.

And a couple of aliases worth their weight in gold:

git config --global alias.unpushed "log --oneline @{u}.."
git config --global alias.incoming "log --oneline ..@{u}"
git config --global alias.gap "rev-list --left-right --count @{u}..."
git unpushed
d5e9b2f Return focus to the field after deleting a task
8a3f7c1 Fix the focus after deleting a task

Aliases in depth, in lesson 06-04.

  1. Closing the module

Let us recap what we have built across these six lessons, because module 4 is a conceptual leap and it helps to see it whole.

We began by demolishing the myth: a remote is not a magic server, it is a short name for a URL stored in .git/config. We saw that in a distributed system there is technically no central server: every repository is equal and the "official" one is only official by team agreement. We finally understood what a bare repository is — the contents of .git/ with no working tree — and why it is the correct way to set up a repository that receives pushes. And we discovered that remote references live in .git/refs/remotes/, on your disk, and that origin/main is a photograph with a date on it, not a live window.

Then Ana published the project. We registered the remote with git remote add — a purely local operation that does not even validate the URL — learned to inspect it, rename it and change its address, and took apart the refspec +refs/heads/*:refs/remotes/origin/*, that line hardly anyone can read and which explains where the slash in origin/main comes from.

We sorted out authentication: why reading a public repository needs no credentials while writing always does, what a personal access token is and why it replaced the password, how an SSH key pair is generated and used, and how each operating system stores credentials — with Git Credential Manager smoothing Carla's way on Windows 11.

We tackled Git's most widespread confusion: fetch versus pull. The first downloads and updates remote references without touching a single file, and is absolutely safe; the second adds an integration that does modify your work and can produce conflicts. We learned to look into the gap between the two, to clear out ghost references with --prune, and Carla finally cloned a repository with the whole of module 3's history inside it.

We turned the channel around with git push: what it actually sends, what -u does, how a push refspec is written, why the server rejects non-fast-forward pushes — so as not to destroy someone else's work — and why --force-with-lease is almost always the right answer when forcing really is necessary.

And we have finished by tying the threads together: tracking branches, two lines of configuration per branch on which git push with no arguments, git pull with no arguments, the git status messages and the DWIM of git switch all depend.

The idea that holds the whole module up, and the one to take away: remotes have changed nothing of what you already knew. Merging Bruno's work is the same git merge from module 3. Conflicts are resolved the same way. Branches are still 41-byte files. All that has been added is the transport: how objects manage to travel from one .git/ to another. Once they have arrived, everything works exactly as you already knew.

The problem still left open

The team is collaborating now. All three push, fetch and integrate, and the project moves forward. But if Ana looks at main's history with a critical eye, she starts to see things she does not like:

git log --oneline --graph -14
*   4f8c2a1 Merge branch 'main' of https://git.example.com/team/task-manager
|\
| * d5e9b2f Return focus to the field after deleting a task
* | 8a3f7c1 Fix the focus after deleting a task
|/
*   e7b3f2a Merge branch 'main' of https://git.example.com/team/task-manager
|\
| * b4d7e93 Adjust the footer style
* | 3e9c2a5 fix stuff
|/
* 9e2f4a7 Add the pending task counter
* c2a8f1e Merge the empty-list message

Two distinct problems living side by side:

  • Merge commits that say nothing. Those Merge branch 'main' of https://… are not design decisions: they are the trace of two people pulling at the same time. They clutter the graph and add no information.
  • Commits that should not exist in that shape. fix stuff describes nothing. And on the working branches there are wip, wip 2, now it works and the odd forgotten console.log that got pushed by accident.

There is also a whole repertoire of operations that team work makes indispensable: taking one specific commit from one branch to another without dragging the rest along; putting half-finished work aside to deal with an emergency and picking it up afterwards; marking versions with tags that get published on the server; and undoing a commit already pushed without rewriting the history everyone else already has.

Module 5: Advanced Git Operations takes us into that territory. We will look at git rebase — which rewrites commits to produce a linear history, and which we have mentioned three times without developing — interactive rebase for reordering, combining and rewriting commits before publishing them, git cherry-pick for transplanting individual commits, git stash for setting work aside temporarily, tags for marking versions, and git revert for undoing without deleting.

It is a module about precision, and it arrives at the right moment: now that the history is shared, rewriting it has consequences for other people. Everything you have learned here about push, about the non-fast-forward rejection and about --force-with-lease is exactly what will let you tell what can be rewritten from what cannot.

Common Mistakes and Tips

Mistake 1: confusing main, origin/main and the server's main. It is the mistake that contains almost all the others. Whenever in doubt, ask yourself which of the three you are talking about. The first two are on your disk.

Mistake 2: trusting the "ahead/behind" without having fetched. git status never touches the network: it compares against your photograph. If you have not fetched for two days, those numbers describe the past.

Mistake 3: confusing .. with .... Two dots is "what B has and A does not"; three dots is "what is exclusive to each side". For the gap you use ... with --left-right --count.

Mistake 4: fatal: The current branch X has no upstream branch. The branch has no tracking. Fix it with git push -u origin X, or once and for all with push.autoSetupRemote true.

Mistake 5: DWIM failing because you have not fetched. If a colleague has just created the branch and you have not fetched the references, git switch <branch> will give invalid reference. A git fetch sorts it out.

Mistake 6: ignoring branches marked gone. They mean their branch on the server has disappeared, usually because it was integrated. They are the clearest candidates for local deletion.

Mistake 7: not noticing that a branch has no upstream. With no brackets in git branch -vv, that branch exists only on your disk. Fine for an experiment, dangerous for work that matters.

Mistake 8: believing tracking is shared with the team. It is local configuration in your .git/config. Everyone has their own and nobody sees anybody else's.

Tip 1: git fetch && git status as your start-up routine. Two commands that tell you the truth instead of showing you an old photograph.

Tip 2: use git branch -vv for the weekly review. At a glance you see what is unpushed, what is unfetched, what is local only and what can be deleted.

Tip 3: turn on push.autoSetupRemote true. It eliminates the "no upstream branch" error for good. You have had it since lesson 01-06.

Tip 4: learn @{u}. git log @{u}.. for what you have not pushed and git log ..@{u} for what you have not fetched work on any branch without changing a character. Save them as aliases.

Tip 5: with several remotes, set checkout.defaultRemote origin. It avoids the ambiguity error and saves you typing the full reference every time.

Exercises

Exercise 1: the three things

Set up a bare server and two clones. Then, with commands, prove each statement:

  1. Committing locally moves only main.
  2. Pushing moves main, origin/main and the branch on the server.
  3. When the other clone pushes something, the first one's origin/main does not change until it fetches.
  4. git status works out the gap without touching the network: engineer a situation in which the message is objectively false with respect to the server, and explain why.
  5. Reproduce git status's calculation by hand with git rev-list.

Exercise 2: managing the tracking

In a repository connected to a bare:

  1. Create a local branch without an upstream and prove with two different commands that it has none.
  2. Check what git push says on that branch if you switch push.autoSetupRemote off.
  3. Configure its upstream all three possible ways (undoing between each) and verify the result in .git/config every time.
  4. Remove the upstream and observe what disappears from git status.
  5. Create a local branch that follows a remote one with a different name and prove that it works.

Exercise 3: DWIM and ambiguity

  1. Set up two bare servers and a working repository with both registered.
  2. Create a branch of the same name on both, with different content.
  3. Try to use DWIM and capture the error.
  4. Resolve it in the three ways explained in the lesson.
  5. Prove with git branch -vv that in each case the local branch follows the right remote.
  6. Check with git log that the content really does differ depending on the remote chosen.

Solutions

Solution 1:

mkdir -p /tmp/ex-track && cd /tmp/ex-track
git init --bare server.git

git clone server.git ana
cd ana
echo "base" > f.txt && git add . && git commit -m "Add initial task manager structure"
git push -u origin main
cd ..
git clone server.git bruno
# 1. Committing moves ONLY main
cd /tmp/ex-track/ana
git rev-parse main origin/main
5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3
5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3
echo "counter" >> f.txt
git commit -am "Add the pending task counter"
git rev-parse main origin/main
9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9   ← main has moved on
5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3   ← origin/main has NOT

And nothing has changed on the server either:

git --git-dir=/tmp/ex-track/server.git rev-parse main
5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3
# 2. Push moves all three
git push
git rev-parse main origin/main
git --git-dir=/tmp/ex-track/server.git rev-parse main
9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9
9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9
9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9

All three aligned. The push updates your origin/main because Git has just spoken to the server and knows for certain where it ended up.

# 3. What Bruno pushes does not reach Ana by itself
cd /tmp/ex-track/bruno
git pull
echo "filter" >> f.txt
git commit -am "Add the pending task filter"
git push

# In Ana's repository, WITHOUT fetching:
cd /tmp/ex-track/ana
git rev-parse origin/main
git --git-dir=/tmp/ex-track/server.git rev-parse main
9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9   ← Ana's photograph, out of date
c3b8f5d2a9e4f7b1c6d3a8e5b2f9c4d7a1e6b3f8   ← the server's reality
# 4. git status lies (without knowing it)
git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

The message is objectively false. The server has a commit Ana does not have, so her branch is behind. But git status compares against origin/main, a photograph taken before Bruno's push, and it never touches the network. It is not lying: it is telling the truth about information that has expired.

git fetch
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)

That is more like it. The only way for those numbers to be true is to fetch first.

# 5. The calculation by hand
git rev-list --left-right --count main...origin/main
0	1

Zero commits exclusive to main (nothing to push), one exclusive to origin/main (something to fetch). That is exactly "behind by 1 commit". And in detail:

git rev-list --left-right --oneline main...origin/main
>c3b8f5d Add the pending task filter

The > shows it comes from the right-hand side, origin/main.

Solution 2:

mkdir -p /tmp/ex-up && cd /tmp/ex-up
git init --bare server.git
git clone server.git work
cd work
echo "base" > f.txt && git add . && git commit -m "Base"
git push -u origin main
# 1. Local branch with no upstream
git switch -c feature/alphabetical-order
echo "order" >> f.txt
git commit -am "Show the tasks in alphabetical order"

# Test a: no brackets in branch -vv
git branch -vv
* feature/alphabetical-order a1e5c93 Show the tasks in alphabetical order
  main                       5f8b2e1 [origin/main] Base
# Test b: @{u} does not resolve
git rev-parse --abbrev-ref @{u}
fatal: no upstream configured for branch 'feature/alphabetical-order'
# Test c: there is no section in the configuration either
git config --get-regexp '^branch\.'
branch.main.remote origin
branch.main.merge refs/heads/main

Only main shows up. The new branch has no entry at all.

# 2. What push says without autoSetupRemote
git config --local push.autoSetupRemote false
git push
fatal: The current branch feature/alphabetical-order has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/alphabetical-order

Git does not guess: it insists you say where the branch is going the first time.

# 3a. Way 1: push -u
git push -u origin feature/alphabetical-order
git config --get-regexp '^branch\.feature'
branch.feature/alphabetical-order.remote origin
branch.feature/alphabetical-order.merge refs/heads/feature/alphabetical-order
# Undo it to try the next one
git branch --unset-upstream
git config --get-regexp '^branch\.feature'
(no output)
# 3b. Way 2: branch --set-upstream-to
git branch --set-upstream-to=origin/feature/alphabetical-order
branch 'feature/alphabetical-order' set up to track 'origin/feature/alphabetical-order'.
git branch --unset-upstream

# 3c. Way 3: automatically, with autoSetupRemote
git config --local push.autoSetupRemote true
git commit --allow-empty -m "Another commit"
git push
branch 'feature/alphabetical-order' set up to track 'origin/feature/alphabetical-order'.

All three produce exactly the same configuration. What changes is the moment and the convenience, not the result.

# 4. Removing the upstream
git status
On branch feature/alphabetical-order
Your branch is up to date with 'origin/feature/alphabetical-order'.

nothing to commit, working tree clean
git branch --unset-upstream
git status
On branch feature/alphabetical-order
nothing to commit, working tree clean

The gap line has disappeared. With no upstream, Git has nothing to compare against. It is the proof that the message depends entirely on this configuration.

# 5. Local branch with a name different from its upstream
git switch main
git switch -c local-order --track origin/feature/alphabetical-order
branch 'local-order' set up to track 'origin/feature/alphabetical-order'.
Switched to a new branch 'local-order'
git branch -vv
  feature/alphabetical-order a7c2e94 Another commit
  main                       5f8b2e1 [origin/main] Base
* local-order                a7c2e94 [origin/feature/alphabetical-order] Another commit
git commit --allow-empty -m "Work from the differently named branch"
git push
To /tmp/ex-up/server.git
   a7c2e94..b3f8d1c  local-order -> feature/alphabetical-order

Look at that last line: local-order -> feature/alphabetical-order. The local branch is called one thing and the server's another, and the push works perfectly well. Tracking does not require the names to match.

Solution 3:

mkdir -p /tmp/ex-dwim && cd /tmp/ex-dwim

# 1. Two servers and a working repository
git init --bare team.git
git init --bare personal.git

git init -b main work
cd work
echo "base" > f.txt && git add . && git commit -m "Base"
git remote add origin /tmp/ex-dwim/team.git
git remote add personal /tmp/ex-dwim/personal.git
git push origin main
git push personal main
# 2. The same branch, with different content, on each server
git switch -c feature/alphabetical-order
echo "team order" >> f.txt
git commit -am "Show the tasks in alphabetical order"
git push origin feature/alphabetical-order

git reset --hard HEAD~1
echo "another way of sorting" >> f.txt
git commit -am "Try another way of sorting"
git push personal feature/alphabetical-order

# Back to a clean state and delete the local branch
git switch main
git branch -D feature/alphabetical-order
git fetch --all
git branch -r
  origin/feature/alphabetical-order
  origin/main
  personal/feature/alphabetical-order
  personal/main
# 3. DWIM fails
git switch feature/alphabetical-order
fatal: 'feature/alphabetical-order' matched multiple (2) remote tracking branches

Git cannot guess which one you want, and guessing wrong would mean pushing work to the wrong repository.

# 4a. Way 1: explicit remote reference
git switch -c team-order origin/feature/alphabetical-order
branch 'team-order' set up to track 'origin/feature/alphabetical-order'.
Switched to a new branch 'team-order'
# 4b. Way 2: --track with no -c (takes the remote's short name)
git switch main
git switch --track personal/feature/alphabetical-order
branch 'feature/alphabetical-order' set up to track 'personal/feature/alphabetical-order'.
Switched to a new branch 'feature/alphabetical-order'
# 4c. Way 3: preferred remote
git switch main
git branch -D feature/alphabetical-order
git config checkout.defaultRemote origin
git switch feature/alphabetical-order
branch 'feature/alphabetical-order' set up to track 'origin/feature/alphabetical-order'.
Switched to a new branch 'feature/alphabetical-order'

The ambiguity is gone: when in doubt, Git picks origin.

# 5. Each branch follows its own remote
git branch -vv
* feature/alphabetical-order a1e5c93 [origin/feature/alphabetical-order] Show the tasks in alphabetical order
  main                       5f8b2e1 Base
  team-order                 a1e5c93 [origin/feature/alphabetical-order] Show the tasks in alphabetical order
# 6. The content really is different
git log --oneline -1 origin/feature/alphabetical-order
git log --oneline -1 personal/feature/alphabetical-order
a1e5c93 Show the tasks in alphabetical order
4f8c2a1 Try another way of sorting
git diff origin/feature/alphabetical-order personal/feature/alphabetical-order
diff --git a/f.txt b/f.txt
index 8c3d5a1..2e7b9f4 100644
--- a/f.txt
+++ b/f.txt
@@ -1,2 +1,2 @@
 base
-team order
+another way of sorting

Two different commits under the same branch name on two different servers. This is where you see why Git prefers to fail rather than guess: choosing wrong would have meant working on the wrong base and, worse still, pushing the result to the wrong place.

Conclusion

With this lesson we close module 4. What we learned here:

  • A tracking branch is a local branch associated with a remote reference, its upstream. That association is what makes git push and git pull work with no arguments, what produces the gap messages in git status and what powers the @{u} shortcut.
  • It lives in two lines of .git/config: branch.<name>.remote (which remote) and branch.<name>.merge (which branch on that remote). It is local configuration: it does not travel with a push and everybody has their own.
  • main, origin/main and the server's main are three different things. The first two are on your disk: you move the first by committing, and Git moves the second when it talks to the server. The third is on another machine and anyone on the team can move it. Almost every bewilderment with remotes is settled by asking which of the three is being talked about.
  • The "ahead/behind" is worked out with git rev-list --left-right --count main...origin/main, using the three-dot symmetric difference. And it is worked out against your photograph, not against the server: without a prior fetch, those numbers can be false.
  • git branch -vv shows every branch's upstream and gap in square brackets. No brackets = no upstream, the branch is purely local. With gone = its branch on the server no longer exists, usually because it was integrated: a candidate for deletion.
  • The upstream is set with git push -u, automatically with push.autoSetupRemote true, after the fact with git branch -u origin/<branch>, or when creating the branch with --track. It is removed with --unset-upstream, and git status then stops reporting the gap.
  • The DWIM of git switch <branch> creates a local branch from a remote one of the same name and configures the tracking, provided it does not already exist locally and exactly one remote reference matches. With several remotes it fails: resolve it by being explicit, with --track, or by setting checkout.defaultRemote.
  • @{upstream} (or @{u}) refers to the current branch's upstream without naming it: git log @{u}.. (what you have not pushed) and git log ..@{u} (what you have not fetched) work on any branch.

What comes next

The project has left Ana's laptop and circulates between three machines and three operating systems. The team genuinely collaborates: all three push, fetch and integrate, and task-manager moves forward.

But the history is starting to show the strain. It is filling up with merge commits that say nothing, with messages like fix stuff and wip 2, and with half-finished work published by accident. And new needs are appearing that solo work never raised: taking one specific commit from one branch to another, setting unfinished work aside to deal with an emergency, marking a released version or undoing something already on the server.

In module 5: Advanced Git Operations we will learn to manipulate the history with precision: git rebase to rewrite commits and obtain a linear history, interactive rebase to reorder, combine and correct them before publishing, git cherry-pick to transplant individual commits, git stash to set work aside temporarily, tags to mark versions, and git revert to undo without deleting.

And everything you have learned in this module is precisely what will let you use them with judgement. Because from now on the history is shared, and rewriting something another person has already downloaded is not a local operation: it is a decision that affects the whole team.

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