Chapter 07Lesson 02~115 minutes

Remotes, Refspecs, Fetch, Pull, Push, SSH, HTTPS, and Protocol Behavior: Guided Hands-On Workflow and Core Operations

Use a local bare repository and multiple clones to observe live remote refs, stale tracking refs, fetch/pull/push behavior, upstream setup, dry-run pushes, and a guarded force-with-lease rewrite.

Bare remoteFetch / pullPush dry-runForce-with-lease

Learning objectives

  • Create a no-account bare remote and inspect it with remote, ls-remote, branch -vv, and config.
  • Prove fetch moves remote-tracking refs without automatically moving the local branch.
  • Use explicit fast-forward integration and explicit pull policy after inspecting incoming work.
  • Set upstream tracking during a first push and inspect the recorded branch configuration.
  • Perform a controlled non-fast-forward update with an explicit expected-OID lease.

1. Build a remote laboratory with no hosted account

A bare repository has Git objects and refs but no normal working tree, which makes it a useful stand-in for a central server. Create it beside a seed repository.

Git Bash, Bash, or zsh

mkdir git-remote-lab
cd git-remote-lab
git init --bare --initial-branch=trunk remote.git
git init -b trunk seed
cd seed
git config user.name "Seed User"
git config user.email "seed@example.invalid"
printf "baseline
" > service.txt
git add service.txt
git commit -m "Create remote baseline"
git remote add origin ../remote.git
git push -u origin trunk
cd ..
git clone remote.git client
git clone remote.git worker

PowerShell

New-Item -ItemType Directory git-remote-lab | Out-Null
Set-Location git-remote-lab
git init --bare --initial-branch=trunk remote.git
git init -b trunk seed
Set-Location seed
git config user.name "Seed User"
git config user.email "seed@example.invalid"
Set-Content service.txt 'baseline'
git add service.txt
git commit -m "Create remote baseline"
git remote add origin ../remote.git
git push -u origin trunk
Set-Location ..
git clone remote.git client
git clone remote.git worker

Configure fake identities in client and worker before creating commits. No credential helper, SSH key, or network service is needed because the remote is a local path.

2. Inspect the remote relationship before changing it

git -C client remote -v
git -C client branch -vv
git -C client config --get-regexp '^(remote\.origin|branch\.trunk)'
git -C client ls-remote --symref origin HEAD refs/heads/trunk
git -C client remote show origin

After clone, local trunk normally tracks origin/trunk. The fetch refspec should map remote branches into refs/remotes/origin/*. ls-remote asks the bare repository for its live refs; it does not change client/trunk.

3. Push a normal fast-forward update with a dry-run preflight

First configure client and add one commit:

Git Bash / Bash / zsh

git -C client config user.name "Client User"
git -C client config user.email "client@example.invalid"
printf "baseline
client change
" > client/service.txt
git -C client status --short
git -C client diff
git -C client add service.txt
git -C client diff --staged
git -C client commit -m "Add client service behavior"
git -C client push --dry-run origin trunk
git -C client push origin trunk

The commit and local branch change before the push. The push then sends any missing objects and requests refs/heads/trunk on the bare remote to advance. A dry run verifies the proposed ref update without performing it.

4. Let another clone advance the remote independently

git -C worker config user.name "Worker User"
git -C worker config user.email "worker@example.invalid"
git -C worker pull --ff-only

Now create and push worker.txt from the worker clone. Before fetching in client, compare two different observations:

git -C client rev-parse origin/trunk
git -C client ls-remote origin refs/heads/trunk

If the worker has pushed since the client's last communication, the first command can be stale while ls-remote reports the current remote value.

5. Fetch updates remote-tracking refs, not the local branch

git -C client status --short --branch
git -C client rev-parse trunk
git -C client rev-parse origin/trunk
git -C client fetch origin
git -C client rev-parse trunk
git -C client rev-parse origin/trunk
git -C client log --graph --decorate --oneline --all -8
git -C client rev-list --left-right --count trunk...origin/trunk

After fetch, origin/trunk should move to the remote tip. Local trunk should remain at the same OID until an integration command moves it. This is the safest moment to inspect incoming work.

6. Integrate after inspection

If trunk is only behind, the least surprising integration is a fast-forward-only merge:

git -C client merge --ff-only origin/trunk
git -C client status --short --branch
git -C client log --graph --decorate --oneline -5

No network communication is required for this merge: fetch already brought the needed objects into the local repository. The command changes the local branch/working tree, not the remote.

7. Use pull only when you want transfer and integration in one command

Have worker create one more remote commit, then in client run:

git -C client status --short --branch
git -C client pull --ff-only
git -C client status --short --branch

Because trunk has an upstream, pull knows which remote/branch to fetch. --ff-only states the integration policy explicitly: if the local branch has diverged, the pull fails rather than silently creating a merge or rewriting local commits.

8. Create a topic branch and set its upstream during first push

git -C client switch -c topic/remote-inspection
git -C client branch -vv
git -C client push --dry-run --set-upstream origin topic/remote-inspection
git -C client push --set-upstream origin topic/remote-inspection
git -C client branch -vv
git -C client config --get-regexp '^branch\.topic/remote-inspection\.'

--set-upstream records the tracking relationship after the remote branch is created. The branch name itself is not transmitted as a directory or copy; it is a ref update.

9. Controlled rewrite — guard the remote ref with an explicit lease

Disposable branch only. Amending a pushed commit changes its OID and requires a non-fast-forward remote update. Never make this a routine shared-branch operation.

Create a dedicated branch, push two versions, record the exact remote value you believe you are replacing, then amend locally:

Git Bash / Bash / zsh

git -C client switch -c rewrite-demo trunk
printf "version 1
" > client/rewrite.txt
git -C client add rewrite.txt
git -C client commit -m "Add rewrite demonstration"
git -C client push -u origin rewrite-demo
printf "version 2
" > client/rewrite.txt
git -C client add rewrite.txt
git -C client commit -m "Update rewrite demonstration"
git -C client push origin rewrite-demo
EXPECTED=$(git -C client rev-parse origin/rewrite-demo)
printf "version 2 corrected
" > client/rewrite.txt
git -C client add rewrite.txt
git -C client commit --amend -m "Correct rewrite demonstration"
git -C client push --dry-run --force-with-lease=refs/heads/rewrite-demo:$EXPECTED origin HEAD:refs/heads/rewrite-demo
git -C client push --force-with-lease=refs/heads/rewrite-demo:$EXPECTED origin HEAD:refs/heads/rewrite-demo

PowerShell lease variable

$EXPECTED = git -C client rev-parse origin/rewrite-demo
git -C client push --dry-run "--force-with-lease=refs/heads/rewrite-demo:$EXPECTED" origin HEAD:refs/heads/rewrite-demo
git -C client push "--force-with-lease=refs/heads/rewrite-demo:$EXPECTED" origin HEAD:refs/heads/rewrite-demo

The explicit lease says the remote branch must still equal $EXPECTED. If another actor moved it, Git refuses. Plain --force lacks that expected-value guard.

10. Challenge — choose the command from the state you need

  1. You want to see the remote's current trunk OID without updating local refs. Which command?
  2. You want to refresh origin/trunk but leave local trunk untouched. Which command?
  3. You want to know what a no-refspec push would target. Which config/upstream inspections come first?
  4. You need to propose a force update only if the remote still points at an exact known OID. Which lease form is appropriate?

11. Cleanup

Preflight: confirm your terminal is in the parent of this disposable lab before recursive deletion.

Git Bash / Bash / zsh

git -C client status --short --branch
cd ..
pwd
rm -rf git-remote-lab

PowerShell

git -C client status --short --branch
Set-Location ..
Get-Location
Remove-Item -Recurse -Force git-remote-lab

12. Knowledge check

Question 1. Why can git ls-remote origin refs/heads/trunk disagree with git rev-parse origin/trunk?

Question 2. After git fetch origin, which ref should normally move first: trunk or origin/trunk?

Question 3. What does git push --dry-run protect against?

Question 4. Why is the explicit --force-with-lease=ref:expected form stronger than plain --force?

13. Summary

You used a local bare repository to separate live remote refs from local remote-tracking refs, inspected upstream configuration, fetched before integration, used explicit pull policy, previewed pushes, and performed a narrowly guarded rewrite only on a disposable branch.

Next

Choose remote and transport policy deliberately

Lesson 3 turns those mechanics into configuration decisions: fetch and push URLs, refspecs, upstream/push destinations, pull policy, push defaults, SSH/HTTPS trust, and wire-protocol troubleshooting.

Authoritative references

 git-remote
 git-ls-remote
 git-fetch
 git-push
 git-pull

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.