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
- What a tracking branch is
- Where it lives:
branch.<name>.remoteandbranch.<name>.merge - The three things called
main - Where "ahead of 'origin/main' by 2 commits" comes from
git branch -vv: every branch's state at a glance- Setting, changing and removing the upstream
- Creating a local branch from a remote one: Git's DWIM
- The ambiguous case of several remotes
@{upstream}and@{push}: referring to the tracking branch- Closing the module
- 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 pushknows which remote and which branch. - Fetch and pull with no arguments:
git pulltoo. - 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.
- Where it lives:
branch.<name>.remote and branch.<name>.merge
branch.<name>.remote and branch.<name>.mergeAs with everything in Git, this is not magic: it is two lines in a text file.
[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-deleteEvery 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:
And there is an optional key you may come across:
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.
- The three things called
main
mainThis 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 yourorigin/main, which is out of date. Fix:git fetch. - "I ran
fetchand my files have not changed." → Afetchupdatesorigin/main; your files depend onmain. That is correct behaviour. - "I ran
git reset --hard origin/mainand lost my commits." → You moved yourmainto 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 pushsaysEverything up-to-datebut my change is not there." → Yourmainis where yourorigin/mainis; 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/maind5e9b2f8a3c1e6b4d7f2a9c5e8b1d4f6a3c7e9b2 ← main has moved on b4d7e935f1c8a2e6d4b7f9a3c1e5b8d2f4a6c9e7 ← origin/main has not
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.
- Where "ahead of 'origin/main' by 2 commits" comes from
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
Let us take it apart, because every piece matters:
main...origin/main— three 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:
<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:
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:
There is a git status option that sounds as though it does the job by itself:
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.
git branch -vv: every branch's state at a glance
git branch -vv: every branch's state at a glanceIn lesson 03-06 we mentioned -vv and said it would make sense once remotes were in play. That moment has arrived.
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:
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:
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:
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:
# 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.
- 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.
git push -u (the most common)
git push -u (the most common)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.
- Automatically, with
push.autoSetupRemote
push.autoSetupRemoteWe 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-orderA message you have seen a thousand times if you have used Git without that setting.
git branch --set-upstream-to (after the fact)
git branch --set-upstream-to (after the fact)For a branch that already exists in both places but is not paired up:
There is a short form with -u (which here means the same as it does in push):
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:
Now your local main follows origin/develop. It is unusual, but perfectly valid: the association does not require the names to match.
- 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 mainRemoving the tracking
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:
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 |
- 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:
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:
Long and repetitive. Which is why Git has a shortcut:
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:
- Is there a local branch called
docs/update-notes? No. - Is there exactly one remote reference with that name? Yes,
origin/docs/update-notes. - 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:
* 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:
The limits of DWIM
It does not apply if the local name already exists:
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:
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:
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.
- 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:
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)
And now:
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):
Or with --track, which also puts the intention on record:
2. Use the disambiguating syntax:
With --track and no -c, Git creates a local branch with the same short name as the remote one.
3. Configure a preferred remote:
From then on, faced with ambiguity Git will always choose origin without asking:
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 -vvteam-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.
@{upstream} and @{push}: referring to the tracking branch
@{upstream} and @{push}: referring to the tracking branchGit 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:
And it can be applied to another branch:
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.
It is the right reference for answering "which commits would go out if I pushed right now?":
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}..."Aliases in depth, in lesson 06-04.
- 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:
* 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 stuffdescribes nothing. And on the working branches there arewip,wip 2,now it worksand the odd forgottenconsole.logthat 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:
- Committing locally moves only
main. - Pushing moves
main,origin/mainand the branch on the server. - When the other clone pushes something, the first one's
origin/maindoes not change until it fetches. git statusworks 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.- Reproduce
git status's calculation by hand withgit rev-list.
Exercise 2: managing the tracking
In a repository connected to a bare:
- Create a local branch without an upstream and prove with two different commands that it has none.
- Check what
git pushsays on that branch if you switchpush.autoSetupRemoteoff. - Configure its upstream all three possible ways (undoing between each) and verify the result in
.git/configevery time. - Remove the upstream and observe what disappears from
git status. - Create a local branch that follows a remote one with a different name and prove that it works.
Exercise 3: DWIM and ambiguity
- Set up two bare servers and a working repository with both registered.
- Create a branch of the same name on both, with different content.
- Try to use DWIM and capture the error.
- Resolve it in the three ways explained in the lesson.
- Prove with
git branch -vvthat in each case the local branch follows the right remote. - Check with
git logthat 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 brunoecho "counter" >> f.txt
git commit -am "Add the pending task counter"
git rev-parse main origin/main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 ← main has moved on 5a1d8f39c2e7b4a6f1d8c3e9b2a5f7d4c6e8b1a3 ← origin/main has NOT
And nothing has changed on the server either:
# 2. Push moves all three
git push
git rev-parse main origin/main
git --git-dir=/tmp/ex-track/server.git rev-parse main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 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 main9e2f4a78c3b1d6f4a2e7c5b9d8f1a3c6e4b2d7f9 ← Ana's photograph, out of date c3b8f5d2a9e4f7b1c6d3a8e5b2f9c4d7a1e6b3f8 ← the server's reality
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.
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.
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:
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
Only main shows up. The new branch has no entry at all.
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-orderGit 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'# 3b. Way 2: branch --set-upstream-to
git branch --set-upstream-to=origin/feature/alphabetical-ordergit branch --unset-upstream
# 3c. Way 3: automatically, with autoSetupRemote
git config --local push.autoSetupRemote true
git commit --allow-empty -m "Another commit"
git pushAll three produce exactly the same configuration. What changes is the moment and the convenience, not the result.
On branch feature/alphabetical-order Your branch is up to date with 'origin/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-orderbranch 'local-order' set up to track 'origin/feature/alphabetical-order'. Switched to a new branch 'local-order'
feature/alphabetical-order a7c2e94 Another commit main 5f8b2e1 [origin/main] Base * local-order a7c2e94 [origin/feature/alphabetical-order] Another commit
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 -rGit cannot guess which one you want, and guessing wrong would mean pushing work to the wrong repository.
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-orderbranch '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-orderbranch '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.
* 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-orderdiff --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 sortingTwo 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 pushandgit pullwork with no arguments, what produces the gap messages ingit statusand what powers the@{u}shortcut. - It lives in two lines of
.git/config:branch.<name>.remote(which remote) andbranch.<name>.merge(which branch on that remote). It is local configuration: it does not travel with apushand everybody has their own. main,origin/mainand the server'smainare 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 priorfetch, those numbers can be false. git branch -vvshows every branch's upstream and gap in square brackets. No brackets = no upstream, the branch is purely local. Withgone= 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 withpush.autoSetupRemote true, after the fact withgit branch -u origin/<branch>, or when creating the branch with--track. It is removed with--unset-upstream, andgit statusthen 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 settingcheckout.defaultRemote. @{upstream}(or@{u}) refers to the current branch's upstream without naming it:git log @{u}..(what you have not pushed) andgit 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
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
