Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Git & GitHub  Module 5 – GitHub and remotes

Sync work with fetch, pull and push

How git fetch, git pull and git push differ, how to inspect incoming commits, fix a rejected push and replace --force with a lease that is really safe.

  • Intermediate
  • 20 minutes
  • Examples run with git 2.54.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain what git fetch changes, and inspect incoming commits before you integrate them
  • Choose how git pull joins diverged branches, and act on the divergent-branches message
  • Publish a branch with upstream tracking, delete a remote branch and prune stale names
  • Recover from a rejected push without losing anyone's commits
  • Use --force-with-lease with --force-if-includes instead of a bare --force

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what git fetch changes, and inspect incoming commits before you integrate them
  • Choose how git pull joins diverged branches, and act on the divergent-branches message
  • Publish a branch with upstream tracking, delete a remote branch and prune stale names
  • Recover from a rejected push without losing anyone's commits
  • Use --force-with-lease with --force-if-includes instead of a bare --force

Before you start

On this page

Ravi’s git push is refused: his classmate Asha pushed 1 commit to main first, and his repository has never seen it. A forum answer tells him to add --force, which would drop Asha’s commit from main on the server. Three commands move work between your repository and a remote, and each changes something different. git fetch downloads the commits you do not have and moves only your remote-tracking branches, such as origin/main. git pull is a fetch followed by one integration step into the branch you are on. git push uploads your commits and moves the branch on the remote, by default only when no commit already there would be lost.

Every session below ran in Git 2.54.0, with a folder standing in for GitHub and two clones of it, Asha’s and Ravi’s. You will look at incoming work before it touches yours, choose how a pull joins diverged branches, publish and delete branches, get a refused push through safely, and rewrite your own pushed branch without overwriting anyone.

git fetch changes only your remote-tracking branches

Asha has pushed two commits. Ravi’s clone does not know yet: it still records the remote as it was at his last fetch. Step through what one git fetch changes on his side, and what it leaves alone:

Interactive · message by message What one git fetch changes, and what it leaves alone Step through Asha's push and Ravi's fetch, and watch which of Ravi's names move.
What one git fetch changes, and what it leaves alone, step 6 of 6. Messages so far: Asha's clone to origin.git: push: 2 commits; Ravi's clone to origin.git: git fetch; origin.git to Ravi's clone: 2 commits.Asha's cloneorigin.gitmain at 782c007Ravi's clonemain at 782c007push: 2 commitsgit fetch2 commits

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Asha pushes. Asha pushes two commits, Plan week 2 and Add the class rules. The shared repository's main moves from 7cd9f44 to 782c007.
  2. Ravi has not looked. Ravi's clone still records the remote as it was at his last fetch, origin/main at 7cd9f44, the same commit as his own main. git status cannot know about the new commits yet.
  3. git fetch. Ravi runs git fetch. His Git asks the remote where its branches point now, and downloads the commits it does not have.
  4. Only origin/main moves. The two commits arrive and origin/main moves to 782c007. Ravi's own main, his files and his staging area stay exactly as they were.
  5. Look before joining. git log HEAD..@{upstream} lists the two commits a pull would bring in, and git diff --stat HEAD...@{upstream} shows what they change. Nothing of Ravi's has moved yet.
  6. Integrate. git merge --ff-only @{upstream} moves main forward to origin/main, the step git pull --ff-only would take after its fetch. Both names now point at 782c007.

The fetch moved one name, origin/main, the remote-tracking branch: A reference such as origin/main that records where a branch of a remote pointed the last time your repository talked to that remote. It moves only when you fetch, pull or push, and you never commit to it. More in “Remotes and cloning: origin and tracking branches” that records where the remote’s main was when Ravi last talked to it. His own main, his files and his staging area did not change, so a fetch is safe to run at any moment, even in the middle of uncommitted work. Here is the same session in real Git:

Fetch, inspect, then integrate Python · fetch_session.py
from session import run, setup

# Set-up: one shared repository and two clones of it, Asha's and Ravi's. Asha then pushes two commits.
setup("""
git init --bare -q origin.git
git clone -q origin.git asha
cd asha
git config user.name Asha
git config user.email asha@example.com
echo "Week 1: install Git" > plan.md
git add plan.md
git commit -q -m "Add the study plan"
git push -q origin main
cd ..
git clone -q origin.git ravi
cd asha
echo "Week 2: branches" >> plan.md
git commit -q -am "Plan week 2"
echo "Bring a laptop to every class" > rules.md
git add rules.md
git commit -q -m "Add the class rules"
git push -q origin main
cd ..
""")

run("""
cd ravi
git --version
git fetch
git status
# What the fetch brought: the commits on origin/main that main does not have yet.
git log --oneline HEAD..@{upstream}
git diff --stat HEAD...@{upstream}
# Nothing in Ravi's files has changed so far. Now he integrates:
git merge --ff-only @{upstream}
git log --oneline
""")

Output

$ cd ravi
$ git --version
git version 2.54.0 (Apple Git-157)
$ git fetch
From ~/demo/origin
   7cd9f44..782c007  main       -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean
# What the fetch brought: the commits on origin/main that main does not have yet.
$ git log --oneline HEAD..@{upstream}
782c007 Add the class rules
df7d107 Plan week 2
$ git diff --stat HEAD...@{upstream}
 plan.md  | 1 +
 rules.md | 1 +
 2 files changed, 2 insertions(+)
# Nothing in Ravi's files has changed so far. Now he integrates:
$ git merge --ff-only @{upstream}
Updating 7cd9f44..782c007
Fast-forward
 plan.md  | 1 +
 rules.md | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 rules.md
$ git log --oneline
782c007 Add the class rules
df7d107 Plan week 2
7cd9f44 Add the study plan

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fetch_session.py

  • git fetch printed one line per remote-tracking branch it moved: origin/main went from 7cd9f44 to 782c007.
  • git status now knew that main was 2 commits behind and could be fast-forwarded. Before the fetch it could not know: its counts are only as fresh as the last fetch.
  • @{upstream} (short form @{u}) means the upstream branch: The remote-tracking branch that a local branch follows, set by git clone, git push -u or git branch --set-upstream-to. git status compares the branch with it, and git pull and git push use it when given no arguments. More in “Remotes and cloning: origin and tracking branches” of the current branch, here origin/main. git log HEAD..@{upstream} lists the commits that are on the upstream but not on HEAD: exactly what an integration would bring in. The reverse, git log @{upstream}..HEAD, lists what a push would send.
  • git diff --stat HEAD...@{upstream}, with three dots, shows what changed on the upstream since the point where the two histories split, without the changes that are only on your side.
  • git merge --ff-only @{upstream} then moved main forward to origin/main, exactly the step that git pull --ff-only would have taken after its fetch.

The habit pays for itself the first time a teammate pushes something you did not expect: you see it while it is still only on origin/main, where it changes nothing of yours.

Git Patch and Diff Viewer (Apply, Reverse, Split) Save the incoming changes with git diff HEAD...@{upstream} > incoming.diff, then read them file by file, side by side.

Checkpoint

Check: What git fetch changes

  1. Question 1 of 2 You run git fetch while you have uncommitted edits. Which of these can it change? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • The list of remote-tracking branches, when the remote has a new branch
    • Where your remote-tracking branches, such as origin/main, point

    A fetch downloads commits and moves remote-tracking branches, adding new ones for new remote branches. Your own branches, files and staging area stay exactly as they were, so it is safe at any moment.

  2. Question 2 of 2 After git fetch, Ravi wants to list the commits that a pull would bring into his main. Which command lists exactly those?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: git log HEAD..@{upstream}

    HEAD..@{upstream} subtracts what HEAD already reaches from what the upstream reaches, leaving the incoming commits, the two of the fetch session. Swap the sides and you get what a push would send.

git pull is a fetch plus one integration step

When your branch has no commits of its own, every pull is a Moving a branch forward to a commit that already contains its current commit. No merge commit is made and no commit is lost, because the old commit is part of the new history. and needs no decision. When both sides have new commits, the branches have diverged branches: Two branches that each have commits the other lacks since their last common commit. Neither can fast-forward to the other, so joining them takes a merge or a rebase., and pull has to know how you want them joined. The figure shows the diverged history of the rejected push later in this lesson, X with Asha’s commit A on origin/main and Ravi’s commit B on main, and the three things that can happen next:

A diverged branch and three ways out: a merge commit, a rebase that copies your commit onto theirs, or a forced push that drops theirs.1. Diverged2. Merge3. Rebase4. Forced pushXAorigin/mainBmainXABMmainXAB′mainXBmainAlost

A diverged branch, and three ways out of it (merge, rebase, forced push)

Text description of the diagram

Four panels show the same history. The last three are alternatives, not steps.

  1. Diverged: commit X has two children. A is on origin/main (Asha's commit) and B is on main (Ravi's commit), so each side has a commit the other lacks.
  2. Merge, what git pull --no-rebase does: a new merge commit M has both A and B as its parents, and main points at M. No commit is rewritten.
  3. Rebase, what git pull --rebase does: Ravi's change is replayed on top of A as a new commit, B′, and main points at it. History is a single line, and B′ has a new ID.
  4. Forced push, what git push --force does: the remote's main is set to B. Commit A, drawn dashed and marked lost, is no longer on any branch of the remote. This is the outcome a rejected push protects you from.

A merge and a rebase are the two ways to join the histories; the forced push in the last panel is what a pull never does, and what the rest of this lesson keeps you from doing. Each of the three pull policies is a setting, or an option for one pull:

Three ways git pull can integrate
Criterion Fast-forwardRebaseMerge
Setting pull.ff onlypull.rebase truepull.rebase false
One pull --ff-only--rebase--no-rebase
Diverged StopsReplays your commits on theirsAdds a merge commit
History SameOne line, new IDs for yoursTwo lines, joined
Rewrites NoneUnpushed commitsNone
When to choose Deciding each timeShort-lived branchesKeeping merge records

Each policy has a cost. A rebase rewrites only the commits you have not pushed yet, which is usually safe, but test the replayed commits again, because they now sit on top of code they were never written against. A merge rewrites nothing, but a team that merges on every pull fills its history with commits named “Merge branch ‘main’ of …”. Fast-forward only never surprises you, and it hands every real decision back to you.

If you have set none of these, Git 2.54 pulls without a word as long as a fast-forward is possible, and stops with a hint that lists all three settings as soon as the branches have diverged; the rejected-push session below prints that hint. A pull that rebases also refuses to start while you have uncommitted changes, because, as the git config documentation puts it, a rebasing pull “refuses to work with any local changes”. pull.autoStash puts them aside first and brings them back afterwards.

One setting that never surprises you

Run git config --global pull.ff only once. Every pull that can fast-forward still just works, and a pull that would have to join diverged branches stops, so you choose git pull --rebase or git pull --no-rebase knowing what it will do.

Checkpoint

Check: Pull policies for diverged branches

  1. Question 1 of 2 Ravi has set git config pull.ff only. His main has 1 commit that the remote lacks, and the remote has 2 commits his main lacks. What does git pull do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It fetches, then stops without changing his branch, because no fast-forward is possible

    pull.ff only allows only fast-forwards. Diverged branches need a merge or a rebase, so the pull stops and Ravi chooses, for example with git pull --rebase or git pull --no-rebase.

  2. Question 2 of 2 Ravi's unpushed commit "Add notes" is 1323c7e, and Asha's commit e0db801 is on origin/main. He runs git pull --rebase. What becomes of his commit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It is replayed on top of e0db801 as a new commit with a new ID, c2c8d98 in the session

    git pull --rebase fetches, then replays the commits that only your branch has on top of the upstream. Their changes stay; their IDs change, which is why a rebase rewrites only commits you have not pushed yet.

The first push of a branch sets its upstream

A new branch exists only on your computer until you push it, and its first push decides where later plain pushes and pulls go:

Publishing, deleting and pruning branches Python · branch_session.py
from session import run, setup

# Set-up: Asha's clone of a shared repository with one commit, and Ravi's clone of the same repository.
setup("""
git init --bare -q origin.git
git clone -q origin.git asha
cd asha
echo "Week 1: install Git" > plan.md
git add plan.md
git commit -q -m "Add the study plan"
git push -q origin main
cd ..
git clone -q origin.git ravi
cd asha
""")

run("""
git switch -c quiz
echo "Q1: what does git fetch change?" > quiz.md
git add quiz.md
git commit -q -m "Start the quiz"
git push
git push -u origin quiz
git branch -vv
# With push.autoSetupRemote, the first plain git push of a new branch sets its upstream:
git config push.autoSetupRemote true
git switch -q -c answers
echo "A1: only remote-tracking branches" > answers.md
git add answers.md
git commit -q -m "Answer Q1"
git push
# Ravi fetches and sees both branches.
cd ../ravi
git fetch
# Later the quiz branch has been merged, and Asha deletes it on the server.
cd ../asha
git push origin --delete quiz
cd ../ravi
git fetch
git branch -r
git fetch --prune
git branch -r
""")

Output

$ git switch -c quiz
Switched to a new branch 'quiz'
$ echo "Q1: what does git fetch change?" > quiz.md
$ git add quiz.md
$ git commit -q -m "Start the quiz"
$ git push
fatal: The current branch quiz has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin quiz

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.

[exit status 128]
$ git push -u origin quiz
To ~/demo/origin.git
 * [new branch]      quiz -> quiz
branch 'quiz' set up to track 'origin/quiz'.
$ git branch -vv
  main 60b930d [origin/main] Add the study plan
* quiz 5c021e8 [origin/quiz] Start the quiz
# With push.autoSetupRemote, the first plain git push of a new branch sets its upstream:
$ git config push.autoSetupRemote true
$ git switch -q -c answers
$ echo "A1: only remote-tracking branches" > answers.md
$ git add answers.md
$ git commit -q -m "Answer Q1"
$ git push
To ~/demo/origin.git
 * [new branch]      answers -> answers
branch 'answers' set up to track 'origin/answers'.
# Ravi fetches and sees both branches.
$ cd ../ravi
$ git fetch
From ~/demo/origin
 * [new branch]      answers    -> origin/answers
 * [new branch]      quiz       -> origin/quiz
# Later the quiz branch has been merged, and Asha deletes it on the server.
$ cd ../asha
$ git push origin --delete quiz
To ~/demo/origin.git
 - [deleted]         quiz
$ cd ../ravi
$ git fetch
$ git branch -r
  origin/HEAD -> origin/main
  origin/answers
  origin/main
  origin/quiz
$ git fetch --prune
From ~/demo/origin
 - [deleted]         (none)     -> origin/quiz
$ git branch -r
  origin/HEAD -> origin/main
  origin/answers
  origin/main

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 branch_session.py

  • A plain git push of a new branch failed. With the default setting push.default=simple, Git pushes the current branch to the branch of the same name on its upstream, and the new branch quiz had no upstream yet. The error names both fixes.
  • git push -u origin quiz created the branch on the remote and set origin/quiz as its upstream, so later pushes and pulls on it need no arguments; git branch -vv shows the upstream in brackets.
  • push.autoSetupRemote=true makes the first plain git push of any new branch do the same, as it did for answers. Set it once with git config --global push.autoSetupRemote true.
  • git push origin --delete quiz removed the branch from the remote, for example after its pull request was merged.
  • git fetch kept origin/quiz although the branch had gone from the remote: Git deletes nothing unless told to. git fetch --prune removed it, and git config --global fetch.prune true makes every fetch prune.

Tags are not pushed by a plain git push; the tags and releases lesson of this track covers them.

A rejected push is protecting someone’s commit

Here Asha and Ravi both commit to main, and Asha pushes first. Step through Ravi’s pushes, what his Git learns from the remote each time, and why it stops twice before one goes through:

Interactive · message by message A push that stops twice, then goes through Step through Ravi's pushes and read what his Git learns from the remote each time, and why it stops.
A push that stops twice, then goes through, step 8 of 8. Messages so far: Asha's clone to origin.git: push: Plan week 2; origin.git to Ravi's clone: main is at e0db801; origin.git to Ravi's clone: fetch: e0db801; Ravi's clone to origin.git: push: e0db801..c2c8d98.Asha's cloneorigin.gitmain at c2c8d98Ravi's clonemain at c2c8d98push: Plan week 2main is at e0db801fetch: e0db801push: e0db801..c2c8d98

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Asha pushes first. Asha commits Plan week 2 and pushes. The remote's main moves from 7cd9f44 to her commit, e0db801, as a fast-forward.
  2. Ravi commits. Ravi has not fetched. He commits Add notes, 1323c7e, on top of 7cd9f44, the last commit he knows.
  3. Where is main now?. Ravi runs git push. His Git first reads where the remote's branches point, and main is at e0db801, a commit his repository has never seen.
  4. Refused, fetch first. Without that commit, Git cannot check that the push would keep it, so it stops with fetch first and sends nothing.
  5. git fetch. After git fetch, Ravi's Git has e0db801. git status counts 1 and 1 different commits on each side, so the branches have diverged.
  6. Refused again. Now Git can see that Ravi's main does not contain e0db801. Moving the remote's main to 1323c7e would drop Asha's commit, so the push stops as non-fast-forward.
  7. Rebase. git pull --rebase replays Add notes on top of Asha's commit as a new commit, c2c8d98. The history is one line again and holds both commits.
  8. Accepted. Now the push is a fast-forward from e0db801 to c2c8d98, and it goes through. Asha's commit and Ravi's are both on the remote's main.

A push moves the remote’s branch to your commit, and git push refuses any update that is not a fast-forward: your commit must already contain the one the branch points at now, so no history is lost. Ravi’s first push stopped with “(fetch first)”: the remote’s main pointed at a commit his repository had never seen, so Git could not even check. After git fetch, the same push stopped with “(non-fast-forward)”: now Git had the commit, and could see that his main did not contain it.

Forcing a refused push on a shared branch

git push --force after “(fetch first)” would also have “worked”: it moves the remote’s main to Ravi’s commit, and Asha’s commit drops off the branch for everyone, as the last panel of the diverged figure shows. Fetch, rebase or merge, test, then push without any force.

Here is the whole session, the pulls included:

A rejected push, and the safe way through Python · rejected_session.py
from session import run, setup

# Set-up: Asha and Ravi share one repository; both start from the same commit.
setup("""
git init --bare -q origin.git
git clone -q origin.git asha
cd asha
git config user.name Asha
git config user.email asha@example.com
echo "Week 1: install Git" > plan.md
git add plan.md
git commit -q -m "Add the study plan"
git push -q origin main
cd ..
git clone -q origin.git ravi
cd ravi
git config user.name Ravi
git config user.email ravi@example.com
cd ..
""")

run("""
# Asha pushes first.
cd asha
echo "Week 2: branches" >> plan.md
git commit -q -am "Plan week 2"
git push -q
# Ravi committed a different file, without fetching.
cd ../ravi
echo "Bring a laptop" > notes.md
git add notes.md
git commit -q -m "Add notes"
git push
git fetch
git status
git push
git log --oneline --graph --all
git pull
git config pull.ff only
git pull
git pull --rebase
git log --oneline --graph --all
git push
""")

Output

# Asha pushes first.
$ cd asha
$ echo "Week 2: branches" >> plan.md
$ git commit -q -am "Plan week 2"
$ git push -q
# Ravi committed a different file, without fetching.
$ cd ../ravi
$ echo "Bring a laptop" > notes.md
$ git add notes.md
$ git commit -q -m "Add notes"
$ git push
To ~/demo/origin.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '~/demo/origin.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
[exit status 1]
$ git fetch
From ~/demo/origin
   7cd9f44..e0db801  main       -> origin/main
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

nothing to commit, working tree clean
$ git push
To ~/demo/origin.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '~/demo/origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
[exit status 1]
$ git log --oneline --graph --all
* 1323c7e Add notes
| * e0db801 Plan week 2
|/
* 7cd9f44 Add the study plan
$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
[exit status 128]
$ git config pull.ff only
$ git pull
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: 	git merge --no-ff
hint:
hint: or:
hint:
hint: 	git rebase
hint:
hint: Disable this message with "git config set advice.diverging false"
fatal: Not possible to fast-forward, aborting.
[exit status 128]
$ git pull --rebase
Rebasing (1/1)
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph --all
* c2c8d98 Add notes
* e0db801 Plan week 2
* 7cd9f44 Add the study plan
$ git push
To ~/demo/origin.git
   e0db801..c2c8d98  main -> main

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rejected_session.py

git status explained the situation in numbers: the branches had diverged, with 1 different commit on each side. Without a policy, git pull stopped and listed the three settings. With pull.ff only it refused again, because no fast-forward was possible. git pull --rebase replayed Ravi’s commit “Add notes” on top of Asha’s, which gave it a new ID, c2c8d98, and the graph became one line. Only then did git push go through, as a plain fast-forward. The recipe is always the same: fetch, integrate, test, push. If someone pushes again while you test, the next push is refused again, and you run the same four steps once more.

Key idea

A push is refused only when it would drop commits that are already on the remote: bring them into your branch first, test, and push again, never with --force on a branch other people commit to.

Checkpoint

Check: Publishing and deleting branches · Getting a rejected push through

  1. Question 1 of 2 You created the branch quiz and committed to it. git push fails with "The current branch quiz has no upstream branch". Which command publishes it and sets its upstream?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: git push -u origin quiz

    With push.default=simple, a plain push goes to the same-named branch of the upstream, and a new branch has none. git push -u origin quiz creates it and sets the upstream; push.autoSetupRemote true does that for every new branch.

  2. Question 2 of 2 git push answers "! [rejected] main -> main (fetch first)". What does it tell you, and what do you do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The remote's main points at a commit you have never fetched; fetch, integrate it, test and push again

    "fetch first" means the remote's branch holds a commit your repository does not have. After a fetch the same push says "non-fast-forward" until you merge or rebase. Forcing it would drop the other person's commit.

A lease makes a forced push conditional

Sometimes rewriting what you pushed is right: you fixed a commit message with git commit --amend, or moved your own feature branch onto a newer main with git rebase. The remote then holds the old commits, your branch holds their replacements, and only a forced push can swap them. --force replaces whatever the branch holds, including commits someone added since you last looked. --force-with-lease: A git push option that forces the update only if the remote branch still points where your remote-tracking branch says it does, so it refuses to overwrite commits pushed since your last fetch. adds a condition: the remote branch must still point where your remote-tracking branch says it does. Step through a lease that works once and is then defeated by a fetch nobody noticed:

Interactive · message by message A lease that holds, and a background fetch that defeats it Step through Asha's two forced pushes and watch what her remote-tracking branch says before each one.
A lease that holds, and a background fetch that defeats it, step 7 of 7. Messages so far: Ravi's clone to origin.git: push: add a test; origin.git to Asha's clone: search is at cf2f0ba; origin.git to Asha's clone: fetch: search cf2f0ba; Asha's clone to origin.git: push --force-with-lease.Asha's cloneorigin/search cf2f0baorigin.gitsearch at cf7a26eRavi's clonepush: add a testsearch is at cf2f0bafetch: search cf2f0bapush --force-with-lease

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Ravi adds a test. Ravi pushes Search, add a test to the shared search branch. The remote's search moves from Asha's first draft, 154b934, to cf2f0ba.
  2. Asha rewrites. Asha has not fetched. She amends her commit into cf7a26e, and her origin/search still says 154b934, the draft she pushed.
  3. The lease is checked. git push --force-with-lease replaces the remote's search only if it still points at 154b934, where Asha's origin/search says it is. Her Git first reads where it points now.
  4. Refused, stale info. cf2f0ba is not 154b934, so the push stops with stale info and sends nothing. The lease has saved Ravi's test.
  5. A background fetch. Asha's editor runs git fetch on its own, as many editors and prompt scripts do. Her origin/search becomes cf2f0ba, and she never looks at it.
  6. Accepted, test dropped. The same command now matches the lease, and the remote's search is replaced by cf7a26e. Ravi's test is no longer on the branch, and nothing warned anyone.
  7. The second condition. --force-if-includes adds a check the background fetch cannot pass. The commit about to be overwritten must once have been part of Asha's own branch, and cf2f0ba never was.

In the session, Ravi has added a test to Asha’s search branch, and Asha, who has not fetched since, amends her commit:

A lease that refuses, and a background fetch that defeats it Python · lease_session.py
from session import run, setup

# Set-up: Asha's feature branch is on the shared repository, Ravi has added a test to it, and Asha, who has not
# fetched since, rewrites her commit with --amend.
setup("""
git init --bare -q origin.git
git clone -q origin.git asha
cd asha
git config user.name Asha
git config user.email asha@example.com
echo "v1" > app.md
git add app.md
git commit -q -m "Start the app"
git push -q origin main
git switch -q -c search
echo "search: first draft" > search.md
git add search.md
git commit -q -m "Search: first draft"
git push -q -u origin search
cd ..
git clone -q origin.git ravi
cd ravi
git config user.name Ravi
git config user.email ravi@example.com
git switch -q search
echo "test: search finds one result" > search_test.md
git add search_test.md
git commit -q -m "Search: add a test"
git push -q
cd ../asha
echo "search: first version" > search.md
git commit -q -a --amend -m "Search: first version"
""")

run("""
git log --oneline --graph --all
# Asha's origin/search still points at her first draft, but the branch on the server has moved on:
git push --force-with-lease
# An editor or a prompt script fetches in the background, and the lease is renewed without Asha seeing it:
git fetch -q
git push --force-with-lease
# Ravi's test is no longer on the branch:
git log --oneline origin/search
""")

Output

$ git log --oneline --graph --all
* cf7a26e Search: first version
| * 154b934 Search: first draft
|/
* e9014a6 Start the app
# Asha's origin/search still points at her first draft, but the branch on the server has moved on:
$ git push --force-with-lease
To ~/demo/origin.git
 ! [rejected]        search -> search (stale info)
error: failed to push some refs to '~/demo/origin.git'
[exit status 1]
# An editor or a prompt script fetches in the background, and the lease is renewed without Asha seeing it:
$ git fetch -q
$ git push --force-with-lease
To ~/demo/origin.git
 + cf2f0ba...cf7a26e search -> search (forced update)
# Ravi's test is no longer on the branch:
$ git log --oneline origin/search
cf7a26e Search: first version
e9014a6 Start the app

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lease_session.py

The first --force-with-lease was refused with “(stale info)”: Asha’s origin/search still pointed at her first draft, 154b934, but the server’s branch had moved on to Ravi’s test. That refusal saved his commit. Then git fetch -q ran, the way an editor or a shell prompt script fetches every few minutes without telling anyone, and renewed the lease. The same command was accepted, and the last line shows that “Search: add a test” was gone from the branch. The git push documentation warns about exactly this: a lease without an expected value “interacts very badly” with anything that runs git fetch in the background.

--force-if-includes, added in Git 2.30, closes that gap. Together with --force-with-lease, it also checks that the tip of your remote-tracking branch can be reached from an entry of your local branch’s reflog: that the commit you are about to overwrite was at some point part of your own branch, so you have actually seen it. Without a lease it does nothing. In this session a background fetch has already brought in Ravi’s test and a new commit on main, and Asha rebases onto main without looking:

--force-if-includes refuses, then the safe rewrite Python · includes_session.py
from session import run, setup

# Set-up: Asha's search branch is on the shared repository, Ravi has added a test to it, main has a new commit, and
# a background fetch has already brought all of that into Asha's remote-tracking branches.
setup("""
git init --bare -q origin.git
git clone -q origin.git asha
cd asha
git config user.name Asha
git config user.email asha@example.com
echo "v1" > app.md
git add app.md
git commit -q -m "Start the app"
git push -q origin main
git switch -q -c search
echo "search: first draft" > search.md
git add search.md
git commit -q -m "Search: first draft"
git push -q -u origin search
cd ..
git clone -q origin.git ravi
cd ravi
git config user.name Ravi
git config user.email ravi@example.com
git switch -q search
echo "test: search finds one result" > search_test.md
git add search_test.md
git commit -q -m "Search: add a test"
git push -q
git switch -q main
echo "v2" > app.md
git commit -q -am "App: version 2"
git push -q
cd ../asha
git fetch -q
""")

run("""
# Asha moves her branch onto the new main, then force-pushes with both checks:
git rebase -q origin/main
git push --force-with-lease --force-if-includes
# Her rebased copy holds nothing new, so she starts again from the branch as the server has it:
git switch -q -C search origin/search
git rebase -q origin/main
git log --oneline --graph search
git push --force-with-lease --force-if-includes
git log --oneline origin/search
""")

Output

# Asha moves her branch onto the new main, then force-pushes with both checks:
$ git rebase -q origin/main
$ git push --force-with-lease --force-if-includes
To ~/demo/origin.git
 ! [rejected]        search -> search (remote ref updated since checkout)
error: failed to push some refs to '~/demo/origin.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
[exit status 1]
# Her rebased copy holds nothing new, so she starts again from the branch as the server has it:
$ git switch -q -C search origin/search
$ git rebase -q origin/main
$ git log --oneline --graph search
* 0a7775c Search: add a test
* eefbac9 Search: first draft
* 4c4115a App: version 2
* e9014a6 Start the app
$ git push --force-with-lease --force-if-includes
To ~/demo/origin.git
 + cf2f0ba...0a7775c search -> search (forced update)
$ git log --oneline origin/search
0a7775c Search: add a test
eefbac9 Search: first draft
4c4115a App: version 2
e9014a6 Start the app

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 includes_session.py

The lease alone would have passed, because the background fetch had updated origin/search. With --force-if-includes the push was refused with “(remote ref updated since checkout)”, because Ravi’s commit had never been part of Asha’s branch. Her rebased copy held nothing that was not already on the server, so she started again from the server’s branch with git switch -C search origin/search, rebased that onto main and pushed. The log shows both commits, hers and Ravi’s, on top of the new main.

To make the check the default, run git config --global push.useForceIfIncludes true; after that, git push --force-with-lease checks both conditions. Keep forced pushes for branches that are yours, and tell anyone who shares one before you rewrite it.

Checkpoint

Check: Leases and --force-if-includes

  1. Question 1 of 2 Your editor runs git fetch in the background every few minutes. A teammate pushed a commit to your feature branch after you rebased it. Which push refuses to overwrite their commit?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: git push --force-with-lease --force-if-includes

    A background fetch renews the lease without you looking, which is how the lease session lost Ravi's test. --force-if-includes also requires that the remote tip was once in your branch's reflog, which a commit you never integrated is not.

  2. Question 2 of 2 git push --force-with-lease answers "(stale info)". What does it mean?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The remote branch has moved since your remote-tracking branch last saw it, so someone pushed after your last fetch

    A lease is a condition, not a time: the remote must still be where your remote-tracking branch says. "(stale info)" means it is not, as when Ravi's test arrived after Asha's last fetch. Fetch, look at what arrived, and integrate it.

Practise: count ahead and behind

git status decides “behind”, “ahead” or “diverged” by counting commits on each side. Write the same count for a commit graph of your own, then the advice that follows from it; the sample tests try a straight line, a fork, a merge and a history of 2,000 commits:

Exercise · Medium · Python

Count ahead and behind, then say what to run next

git status compares a branch with its upstream by counting commits. Do the same in sync.py, on a commit graph given as a dictionary from each commit's ID to the list of its parents' IDs (a first commit has none, a merge commit has two).

ahead_behind(parents, local, upstream) returns a pair (ahead, behind): how many commits can be reached from local but not from upstream, and how many can be reached from upstream but not from local. A commit can be reached from itself and from every commit that has it as a parent, a grandparent and so on.

next_step(ahead, behind) says what to do next, as one of four strings:

  • "nothing" when both counts are 0;
  • "push" when the branch is only ahead;
  • "pull" when it is only behind (a pull is then a fast-forward);
  • "integrate, then push" when it is both, which means the two have diverged.

With parents = {"a": [], "b": ["a"], "c": ["b"], "x": ["b"]}, ahead_behind(parents, "c", "x") is (1, 1) and next_step(1, 1) is "integrate, then push". The sample tests also try a merge commit and longer histories.

Starter code · sync.py

def ahead_behind(parents, local, upstream):
    """Return (ahead, behind): commits only `local` can reach, and commits only `upstream` can reach."""
    # Replace this line with your code.
    return (0, 0)


def next_step(ahead, behind):
    """Return "nothing", "push", "pull" or "integrate, then push"."""
    # Replace this line with your code.
    return "nothing"
The sample tests · test_sync.py
from sync import ahead_behind, next_step

LINE = {"a": [], "b": ["a"], "c": ["b"], "d": ["c"]}
FORK = {"a": [], "b": ["a"], "c": ["b"], "x": ["b"], "y": ["x"]}
# m merges y into c: the local branch at m has integrated the upstream branch at y.
MERGED = {"a": [], "b": ["a"], "c": ["b"], "x": ["b"], "y": ["x"], "m": ["c", "y"]}


def test_same_commit():
    """finds nothing to do on the same commit"""
    assert ahead_behind(LINE, "d", "d") == (0, 0)
    assert next_step(0, 0) == "nothing"


def test_only_ahead_or_only_behind():
    """counts a straight line both ways"""
    assert ahead_behind(LINE, "d", "b") == (2, 0)
    assert ahead_behind(LINE, "b", "d") == (0, 2)
    assert next_step(2, 0) == "push"
    assert next_step(0, 2) == "pull"


def test_diverged():
    """counts the commits on each side of a fork"""
    assert ahead_behind(FORK, "c", "y") == (1, 2)
    assert ahead_behind(FORK, "y", "c") == (2, 1)
    assert next_step(1, 2) == "integrate, then push"


def test_after_a_merge():
    """counts the merge commit and the local commit as ahead, and nothing as behind"""
    assert ahead_behind(MERGED, "m", "y") == (2, 0)
    assert ahead_behind(MERGED, "y", "m") == (0, 2)


def test_long_history():
    """stays correct on a history of 2,000 commits that splits after 1,000"""
    parents = {"0": []}
    for i in range(1, 1000):
        parents[str(i)] = [str(i - 1)]
    for side in ("L", "U"):
        previous = "999"
        for i in range(1000, 1500 if side == "L" else 2000):
            parents[f"{side}{i}"] = [previous]
            previous = f"{side}{i}"
    assert ahead_behind(parents, "L1499", "U1999") == (500, 1000)
A hint

Write a helper that collects every commit reachable from one commit: start a list with that commit, and while the list is not empty, take a commit off it, skip it if you have seen it, and otherwise add it to a set and put its parents on the list. Then ahead is the size of one set minus the other, and behind the other way round.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

To replay any session of this page on your own computer, put its file next to this helper and run it with Python, for example python3 lease_session.py. The helper makes the remote and both clones in a throwaway folder, fixes every name and time so the commit IDs come out as shown here, and deletes everything at the end:

session.py, which types the commands of every session Python · session.py
"""Shows a terminal session with Git: each line of a script is typed at a prompt (after "$"), and what the command
printed appears under it.

Every session starts in a new, empty folder, shown as ~/demo, with a home folder of its own, so your own Git settings
take no part: its ~/.gitconfig holds only a name, an email address and init.defaultBranch = main. Standard output and
standard error appear together, as in a terminal, and a command that fails adds a line with its exit status, after
which the session goes on. "cd <folder>" changes the folder of the commands that follow, as in a shell; a line that
starts with "#" is printed as a comment. Nothing uses the network: the "remote" repositories are folders.

So that every run prints the same text:
  * each command runs one minute after the one before (GIT_AUTHOR_DATE and GIT_COMMITTER_DATE), so the commit IDs
    are the same on every run (yours will differ, because your commits carry your own times);
  * the session's folder is shown as ~/demo and its home folder as ~, and spaces at the ends of lines are dropped;
  * mask= rewrites text that is new on every run, such as a freshly made key, as a description of it.
On a Mac, the Command Line Tools' own git is used directly when it is there (the /usr/bin/git launcher in front of it
can print cache warnings when it runs without a writable home folder).
"""

import atexit
import os
import re
import shlex
import shutil
import subprocess
import tempfile

# The example's own folder (files() copies from it) and the session's folders, which are removed at the end.
HERE = os.getcwd()
_BASE = os.path.realpath(tempfile.mkdtemp(prefix="git-session-"))
atexit.register(shutil.rmtree, _BASE, ignore_errors=True)
ROOT = os.path.join(_BASE, "demo")
HOME = os.path.join(_BASE, "home")
for folder in (ROOT, HOME, os.path.join(_BASE, "tmp")):
    os.makedirs(folder)
with open(os.path.join(HOME, ".gitconfig"), "w", encoding="utf-8") as f:
    f.write("[user]\n\tname = Learner\n\temail = learner@example.com\n[init]\n\tdefaultBranch = main\n")

_MAC_GIT = "/Library/Developer/CommandLineTools/usr/bin"
_path = os.environ.get("PATH", "/usr/bin:/bin")
if os.path.exists(os.path.join(_MAC_GIT, "git")):
    _path = _MAC_GIT + os.pathsep + _path
ENV = {
    "PATH": _path,
    "HOME": HOME,
    "XDG_CONFIG_HOME": os.path.join(HOME, ".config"),
    "TMPDIR": os.path.join(_BASE, "tmp"),
    "GIT_CONFIG_NOSYSTEM": "1",
    "GIT_TERMINAL_PROMPT": "0",
    "LANG": os.environ.get("LANG", "C.UTF-8"),
    "LC_ALL": os.environ.get("LC_ALL", os.environ.get("LANG", "C.UTF-8")),
    "TZ": "UTC",
}
# A fixed clock: the first command runs at this second (India Standard Time), each later one a minute after.
_state = {"clock": 1767240000, "cwd": ROOT}


def _tidy(line: str) -> str:
    """The line with the session's folders written the way the lesson shows them."""
    for real, shown in ((ROOT, "~/demo"), (HOME, "~"), (_BASE, "…")):
        line = line.replace(real, shown)
    return line.rstrip()


def files(*names: str) -> None:
    """Copies files of the example's folder into ~/demo, the folder the session starts in."""
    for name in names:
        shutil.copy(os.path.join(HERE, name), os.path.join(ROOT, name))


def setup(script: str) -> None:
    """Runs the commands of `script` without showing them (the lesson shows them in the example's code); a command that
    fails stops the example."""
    run(script, show=False)


def run(script: str, mask: tuple = (), show: bool = True) -> None:
    """Types each line of `script` at the prompt and prints what it printed.

    mask: (pattern, replacement) pairs applied to each line of output; a replacement of None leaves the line out.
    """
    for command in script.strip("\n").splitlines():
        command = command.strip()
        if not command or command.startswith("#"):
            if show:
                print(command)
            continue
        if show:
            print(f"$ {command}")
        words = shlex.split(command)
        if words[0] == "cd" and len(words) == 2:
            where = HOME + words[1][1:] if words[1].startswith("~") else words[1]
            target = os.path.normpath(os.path.join(_state["cwd"], where))
            if os.path.isdir(target):
                _state["cwd"] = target
            elif show:
                print(f"cd: no such file or directory: {words[1]}")
                print("[exit status 1]")
            else:
                raise SystemExit(f"set-up command failed: {command}")
            continue
        _state["clock"] += 60
        stamp = f"{_state['clock']} +0530"
        env = {**ENV, "GIT_AUTHOR_DATE": stamp, "GIT_COMMITTER_DATE": stamp}
        result = subprocess.run(command, shell=True, cwd=_state["cwd"], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)
        if not show:
            if result.returncode != 0:
                raise SystemExit(f"set-up command failed: {command}\n{_tidy(result.stdout)}")
            continue
        for line in result.stdout.splitlines():
            line = _tidy(line)
            for pattern, replacement in mask:
                if replacement is not None:
                    line = re.sub(pattern, replacement, line)
                elif re.search(pattern, line):
                    break
            else:
                print(line)
        if result.returncode != 0:
            print(f"[exit status {result.returncode}]")

Finish with the self-check on fetching, pulling, publishing, refused pushes and leases: every answer comes with its reason, and the questions keep circling back to the idea you have got wrong most often.

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Why can you run git fetch safely in the middle of uncommitted work?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It changes only remote-tracking branches, never your branches, files or staging area

    Fetching moves remote-tracking branches and nothing else, so uncommitted edits, staged changes and every local branch stay exactly as they were.

  2. Question 2 of 15 Ravi's main and origin/main have diverged. Which command shows only the changes that arrived on origin/main since the two histories split, without his own changes?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: git diff HEAD...@{upstream}

    git diff A...B shows the changes on B since the merge base of A and B. With HEAD and @{upstream}, that is exactly what the upstream brought in, which is what to read before integrating it.

  3. Question 3 of 15 git status says "Your branch is up to date with 'origin/main'", yet a teammate pushed to main ten minutes ago. What explains it, and what do you run?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: git status compares with origin/main as of your last fetch; run git fetch, then git status again

    Remote-tracking branches move only when you fetch, pull or push. Fetch first, and git status can then say that you are behind, as it did with 2 commits in the fetch session.

  4. Question 4 of 15 What does git pull do?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: A fetch, then one integration of the current branch's upstream into it, by fast-forward, merge or rebase

    git pull fetches, then joins the upstream into the current branch: a fast-forward when it can, otherwise a merge or a rebase as pull.rebase, pull.ff or an option says.

  5. Question 5 of 15 With Git 2.54 and no pull settings at all, Ravi's main and origin/main have 1 different commit each. What does a plain git pull do?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: It stops with a hint that lists pull.rebase false, pull.rebase true and pull.ff only, and leaves main as it was

    A pull that can fast-forward needs no decision. Once the branches have diverged, Git 2.54 with no policy stops and lists the three settings, which you can also pass for one pull as --no-rebase, --rebase or --ff-only.

  6. Question 6 of 15 With pull.rebase true, git pull refuses because you have uncommitted edits in one file. Which setting lets such pulls put the edits aside first and bring them back afterwards?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: pull.autoStash true

    A merging pull tolerates local changes that do not get in its way, but a rebasing pull refuses to work with any. pull.autoStash puts them in a temporary stash entry and restores them when the pull is done.

  7. Question 7 of 15 The pull request of the branch quiz has been merged. Which command deletes the branch from the remote?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: git push origin --delete quiz

    Deleting on the remote is a push, git push origin --delete quiz, as Asha did in the branch session. Other clones keep their origin/quiz until they fetch with --prune.

  8. Question 8 of 15 You set push.autoSetupRemote true, create the branch answers, commit and run a plain git push. What happens?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: answers is created on the remote, and origin/answers becomes its upstream

    push.autoSetupRemote assumes --set-upstream on a plain push when the current branch has no upstream yet, so the first git push of every new branch creates it on the remote and tracks it.

  9. Question 9 of 15 A teammate deleted the remote branch quiz. After git fetch, git branch -r still lists origin/quiz. Why, and what removes it?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: A fetch keeps remote-tracking branches unless it prunes; git fetch --prune, or fetch.prune true, removes it

    git fetch --prune deletes remote-tracking branches whose branch no longer exists on the remote, and git config --global fetch.prune true makes every fetch do it.

  10. Question 10 of 15 Your push to main was refused as non-fast-forward. Put the safe way through in order.

    Give each item its position, from 1 (first).

    Show the answer: Question 10 of 15

    Answer:

    1. git fetch
    2. git rebase origin/main
    3. Run the tests
    4. git push

    Fetch, integrate, test, push. The fetch brings in the commits you lack, the rebase (or a merge) puts yours on top, the tests check your change on its new base, and the push is then a plain fast-forward.

  11. Question 11 of 15 In the rejected-push session, what would git push --force have done when Ravi's first push stopped with "fetch first"?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Moved the remote's main to Ravi's 1323c7e, so Asha's e0db801 would be on no branch of the remote

    A forced push replaces whatever the branch holds. That is the fourth panel of the diverged figure: A drops off the remote's branch, for everyone who has not fetched it yet.

  12. Question 12 of 15 Ravi has fetched, rebased and is running the tests when Asha pushes another commit to main. What happens when he pushes?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The push is refused again, because the remote's main has a commit his branch lacks; he fetches, integrates, tests and pushes once more

    Each push is checked against the remote as it is right then, so a busy branch can refuse you more than once. The answer is the same each time: fetch, integrate, test, push, never --force.

  13. Question 13 of 15 What condition does git push --force-with-lease add to a forced push?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: The remote branch must still point where your remote-tracking branch says it does

    --force-with-lease rewrites the remote branch only if it is still where you last saw it. That protects commits pushed after your last fetch, as long as nothing renews the lease behind your back.

  14. Question 14 of 15 What does git push --force --force-if-includes do, without --force-with-lease?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: The same as git push --force, because without a lease --force-if-includes checks nothing

    --force-if-includes is an addition to a lease: with --force-with-lease it also checks that the remote tip was once part of your branch. Alone, or with an explicit --force-with-lease=<ref>:<expect>, it does nothing.

  15. Question 15 of 15 Which of these is a good reason to force-push?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: You amended the last commit of your own pull-request branch, which nobody else pushes to

    Force only branches that are yours, after an amend or a rebase, with --force-with-lease --force-if-includes (or push.useForceIfIncludes), and tell anyone who shares the branch before you rewrite it.

Interview questions

Warm-up (fresher): What is the difference between git fetch and git pull?

git fetch downloads new commits from a remote and updates your remote-tracking branches, such as origin/main, and nothing else: your local branches, your working tree and the staging area stay as they were. It is always safe, and afterwards you can look at what arrived, for example with git log HEAD..@{upstream}. git pull runs that fetch and then integrates the upstream of the current branch into it, by a fast-forward, a merge or a rebase, depending on pull.ff, pull.rebase or the option you give. So pull can change your branch and your files, and when the histories have diverged it can create a merge commit, rewrite your unpushed commits or stop with a conflict. Fetch first when you want to see incoming work before it touches yours; pull when you already know what you will get.

Two harder questions start from the sessions of this lesson: a refused push with a colleague urging --force, and a lease that a background fetch has renewed.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Your git push to main is rejected as non-fast-forward, and a colleague suggests git push --force. Explain what the rejection protects, why the suggestion is wrong, and the safe sequence of commands, including what you check before you push again.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Explain what git push --force-with-lease protects against, describe a situation in which it still overwrites a colleague's commit, and say what --force-if-includes adds. Then give the commands you would use to update a rebased feature branch that a colleague also pushes to.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • git fetch moves only remote-tracking branches such as origin/main, so it is always safe, and git log HEAD..@{upstream} then lists exactly what an integration would bring in.
  • git pull is a fetch plus one integration step, and diverged branches need a policy: fast-forward only, rebase or merge, set once with pull.ff or pull.rebase, or chosen for one pull with an option.
  • The first push of a branch sets its upstream with git push -u or push.autoSetupRemote; git push origin --delete removes a remote branch and git fetch --prune removes the stale name.
  • A rejected push means the remote has commits you lack: fetch, integrate, test and push again, never with --force on a shared branch.
  • Rewrite only branches that are yours, and push them with --force-with-lease --force-if-includes, so neither a stale view nor a background fetch can make you overwrite a commit you never saw.

Terms in this lesson

--force-with-lease
A git push option that forces the update only if the remote branch still points where your remote-tracking branch says it does, so it refuses to overwrite commits pushed since your last fetch.
diverged branches
Two branches that each have commits the other lacks since their last common commit. Neither can fast-forward to the other, so joining them takes a merge or a rebase.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.