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.
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
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
-
You want to see the remote's current
trunkOID without updating local refs. Which command? -
You want to refresh
origin/trunkbut leave localtrunkuntouched. Which command? -
You want to know what a no-refspec
pushwould target. Which config/upstream inspections come first? - 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
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?
ls-remote asks the remote live;
origin/trunk is a local remote-tracking ref that may
not have been fetched since the remote changed.
Question 2. After git fetch origin, which ref
should normally move first: trunk or
origin/trunk?
origin/trunk. Fetch updates configured
remote-tracking refs; local trunk moves only through
a later local operation such as merge/rebase/reset/switch
behavior.
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.
Authoritative references
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.