Branches, Forks, Remotes, Synchronization, and Contribution Workflows: Guided Hands-On Workflow and Core Operations
Build a disposable two-repository contribution topology, inspect origin/upstream before mutation, create and push a topic branch, synchronize explicitly, and verify every hosted and local ref transition with Git plus GitHub CLI/API evidence.
Learning objectives
- Create a disposable upstream/contributor topology that reproduces fork-style Git mechanics without requiring an organization or paid plan.
-
Clone the contributor repository, configure
origin/upstreamdeliberately, and verify URLs, refs, and permissions before any push. - Create a topic branch, push it only to the contributor repository, and prove the hosted branch OID matches the local commit.
- Introduce an upstream change, fetch it explicitly, and choose a transparent merge synchronization path for the topic branch.
-
Use
gh repo syncsafely for a fast-forwardable hosted branch and explain how that differs from local Git synchronization. -
Inspect an optional real fork path with
gh repo forkwithout making a paid/enterprise feature mandatory.
gh, Git, and two disposable public repositories
in your personal namespace. If account policy forbids public
repositories, use disposable private repositories; the two-repository
simulation still works because it does not require GitHub fork-network
metadata. Optional actual-fork extension requires a public upstream
that permits forking.
1. Scenario: Atlas Docs has a canonical repository and a contributor copy
You are going to create two repositories you own.
atlas-c04-upstream-lab acts as the canonical project.
atlas-c04-contrib-lab acts as a contributor-owned copy.
They deliberately share Git history, but the second repository is
not a GitHub fork. That distinction lets the lab
remain reproducible while teaching which mechanics are core Git and
which relationship metadata belongs to GitHub.
upstream at a real project
you maintain. Cleanup deletes only these disposable repositories.
2. Preflight: authentication, namespace, tools, and collision checks
gh auth status --active --hostname github.com
git --version
gh --version
OWNER=$(gh api user --jq .login)
UPSTREAM="atlas-c04-upstream-lab"
CONTRIB="atlas-c04-contrib-lab"
for R in "$UPSTREAM" "$CONTRIB"; do
gh repo view "$OWNER/$R" >/dev/null 2>&1 && {
echo "STOP: $OWNER/$R already exists" >&2
exit 2
}
done
printf 'owner=%s
upstream=%s
contrib=%s
' "$OWNER" "$UPSTREAM" "$CONTRIB"
The collision test intentionally stops instead of reusing a repository whose branches or settings you do not understand. A reproducible lab begins from known-empty hosted resources.
3. Predict the state transitions before creating anything
-
Prediction A: after seeding the canonical
repository,
mainwill exist in$OWNER/$UPSTREAMand have one specific OID. -
Prediction B: after creating the contributor
repository from the same local history, its
maincan initially point to the same OID even though GitHub reportsfork=false. -
Prediction C: pushing
topic/navigationtooriginwill create that branch only in the contributor repository; the canonical repository will remain unchanged. -
Prediction D: fetching upstream changes into the
local clone will update
upstream/mainbut will not automatically move the local topic branch or hosted contributor branch.
4. Create and seed the canonical repository
gh repo create "$OWNER/$UPSTREAM" --public --add-readme --clone
cd "$UPSTREAM"
git config user.name "Learner Example"
git config user.email "learner@example.invalid"
mkdir -p docs
printf '%s
' '# Atlas Docs' '' 'Canonical training repository.' > docs/architecture.md
git add docs/architecture.md
git commit -m "docs: add architecture baseline"
git push origin HEAD
CANONICAL_OID=$(git rev-parse HEAD)
CANONICAL_BRANCH=$(git branch --show-current)
printf 'canonical_branch=%s
canonical_oid=%s
' "$CANONICAL_BRANCH" "$CANONICAL_OID"
cd ..
Record both the branch name and OID. Do not assume every account
policy names the first branch main; the lesson uses the
effective branch where appropriate. In this controlled example, the
README-created repository will normally use your configured GitHub
default branch name.
5. Create the contributor repository from the same Git history
git clone "https://github.com/$OWNER/$UPSTREAM.git" "$CONTRIB"
cd "$CONTRIB"
git remote rename origin upstream
gh repo create "$OWNER/$CONTRIB" --public
git remote add origin "https://github.com/$OWNER/$CONTRIB.git"
git push -u origin "$CANONICAL_BRANCH"
cd ..
gh repo view "$OWNER/$CONTRIB" --json nameWithOwner,isFork,parent,defaultBranchRef --jq '{nameWithOwner,isFork,parent:(.parent.nameWithOwner // null),defaultBranch:(.defaultBranchRef.name // null)}'
The contributor repository now shares commit history because Git
pushed the same objects/refs. GitHub still reports
isFork=false because no hosted fork relationship was
created. This is a powerful separation:
same Git history does not imply same GitHub fork network.
6. Inspect origin and upstream before
creating a topic branch
cd "$CONTRIB"
git remote -v
git remote get-url origin
git remote get-url upstream
git branch -vv
git for-each-ref refs/remotes/ --format='%(refname:short) %(objectname:short)'
Expected intent: origin must resolve to
$OWNER/$CONTRIB; upstream must resolve to
$OWNER/$UPSTREAM. If the URLs are reversed, stop. Names
are not enough evidence.
pushurl for the
upstream remote while retaining its fetch URL. Lesson
04 demonstrates this as a guardrail.
7. Create a topic branch and prove only the contributor repository receives it
git switch -c topic/navigation
printf '%s
' '' 'Navigation ownership: docs-platform' >> docs/architecture.md
git add docs/architecture.md
git commit -m "docs: document navigation ownership"
TOPIC_OID=$(git rev-parse HEAD)
git push -u origin topic/navigation
ORIGIN_TOPIC=$(git ls-remote origin refs/heads/topic/navigation | cut -f1)
UPSTREAM_TOPIC=$(git ls-remote upstream refs/heads/topic/navigation | cut -f1)
printf 'local=%s
origin=%s
upstream=%s
' "$TOPIC_OID" "$ORIGIN_TOPIC" "${UPSTREAM_TOPIC:-<absent>}"
test "$TOPIC_OID" = "$ORIGIN_TOPIC"
test -z "$UPSTREAM_TOPIC"
Prediction C is now independently verified. The same commit object
may be reachable from several repositories after network transfer,
but a branch ref is owned by one repository. The canonical
repository has not received
refs/heads/topic/navigation.
8. Verify the hosted branch with GitHub CLI and REST
gh repo view "$OWNER/$CONTRIB" --branch topic/navigation --json nameWithOwner,defaultBranchRef,viewerPermission,url --jq '{nameWithOwner,defaultBranch:.defaultBranchRef.name,viewerPermission,url}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "/repos/$OWNER/$CONTRIB/branches/topic/navigation" --jq '{name,sha:.commit.sha,protected}'
The REST branch SHA should match $TOPIC_OID. This
proves causality with a GitHub-hosted resource rather than relying
on a local tracking ref that might be stale.
9. Make an independent upstream change
Use the canonical clone so the upstream default branch advances independently. This models a maintainer merging work while your topic branch is still in progress.
cd "../$UPSTREAM"
printf '%s
' '' 'Canonical policy: links require HTTPS.' >> docs/architecture.md
git add docs/architecture.md
git commit -m "docs: add canonical link policy"
git push origin HEAD
NEW_UPSTREAM_OID=$(git rev-parse HEAD)
printf 'new_upstream=%s
' "$NEW_UPSTREAM_OID"
cd "../$CONTRIB"
At this moment, your local upstream/* tracking ref is
still stale until you fetch. This intentionally creates the
observation Lesson 01 described.
10. Fetch first; inspect divergence before integrating
BEFORE_FETCH=$(git rev-parse "refs/remotes/upstream/$CANONICAL_BRANCH")
git fetch upstream --prune
AFTER_FETCH=$(git rev-parse "refs/remotes/upstream/$CANONICAL_BRANCH")
printf 'before=%s
after=%s
' "$BEFORE_FETCH" "$AFTER_FETCH"
git log --oneline --left-right --graph "topic/navigation...refs/remotes/upstream/$CANONICAL_BRANCH" -12
Prediction D is now visible. Fetch changed the local remote-tracking
ref from the old canonical OID to the new one. It did not alter
topic/navigation. Inspect the graph before choosing
integration.
11. Synchronize the private topic branch with an explicit merge
For this beginner-first path, use a merge so no already-published topic commits are rewritten. Rebase is analyzed in Lesson 03. Because both branches changed the same file, your exact content may cause a conflict; if it does, preserve the conflict evidence and resolve both intended lines.
git switch topic/navigation
git merge "refs/remotes/upstream/$CANONICAL_BRANCH"
If Git reports a conflict, stop and preserve that evidence. Run
git status, edit docs/architecture.md to
preserve both intended changes, then complete only that interrupted
merge:
# Run these commands only if the merge stopped for a conflict.
git status
git add docs/architecture.md
git commit
After the merge has completed—whether automatically or after the conflict resolution above—capture and publish the resulting topic commit:
SYNCED_TOPIC_OID=$(git rev-parse HEAD)
git log --oneline --graph --decorate -8
git push origin topic/navigation
git fetch origin topic/navigation
test "$SYNCED_TOPIC_OID" = "$(git rev-parse refs/remotes/origin/topic/navigation)"
The merge changes only local history until push; push then updates
the hosted contributor branch. The canonical branch stays at
$NEW_UPSTREAM_OID. You have synchronized a contribution
without pushing a topic branch into upstream.
12. Sync the contributor default branch as a separate operation
Your contributor repository’s default branch may still lag upstream
even though the topic branch contains a merge from upstream. To
demonstrate hosted synchronization separately, use the GitHub CLI
with an explicit source. This simulation is not a fork, so
--source makes the source relationship explicit.
cd ..
gh repo sync "$OWNER/$CONTRIB" --source "$OWNER/$UPSTREAM" --branch "$CANONICAL_BRANCH"
CONTRIB_DEFAULT_OID=$(gh api -H "X-GitHub-Api-Version: 2026-03-10" "/repos/$OWNER/$CONTRIB/commits/$CANONICAL_BRANCH" --jq .sha)
test "$CONTRIB_DEFAULT_OID" = "$NEW_UPSTREAM_OID"
printf 'hosted contributor default now=%s
' "$CONTRIB_DEFAULT_OID"
The command updated the hosted contributor repository. The existing
local clone is not automatically updated by that hosted mutation.
Return to the clone and fetch origin to observe the new
hosted branch.
cd "$CONTRIB"
LOCAL_TRACKING_BEFORE=$(git rev-parse "refs/remotes/origin/$CANONICAL_BRANCH")
git fetch origin "$CANONICAL_BRANCH"
LOCAL_TRACKING_AFTER=$(git rev-parse "refs/remotes/origin/$CANONICAL_BRANCH")
printf 'origin tracking before=%s
after=%s
' "$LOCAL_TRACKING_BEFORE" "$LOCAL_TRACKING_AFTER"
--force: current
gh repo sync documentation describes
--force as synchronizing with a hard reset. This lab
requires a fast-forwardable branch and stops instead of discarding
destination history.
13. Optional: inspect a real public fork with
gh repo fork
If you want the hosted fork-network metadata, fork a small public training repository that you do not already fork. The exact public repository can change over time; verify it is appropriate before use. A generic pattern is:
# OPTIONAL — choose a disposable public training upstream first.
gh repo fork ORIGINAL-OWNER/TRAINING-REPO --fork-name atlas-c04-real-fork-lab --clone
cd atlas-c04-real-fork-lab
git remote -v
gh repo view --json nameWithOwner,isFork,parent --jq '{nameWithOwner,isFork,parent:.parent.nameWithOwner}'
Current gh repo fork behavior may set the fork as
origin and configure the parent as
upstream; gh repo clone of a fork can also
add an upstream remote. Inspect the actual result rather than
memorizing it as an immutable convention.
14. Challenge: choose the right surface
For each requirement, name the operation before running anything:
| Requirement | Correct layer/control | Why |
|---|---|---|
| Learn new upstream commits locally without changing your topic | git fetch upstream |
Updates local objects/tracking refs only. |
| Publish your topic branch to your contributor repository | git push origin topic/navigation |
Moves the hosted contributor branch. |
| Make the hosted contributor default branch match canonical upstream | Hosted sync / gh repo sync |
Changes the destination repository branch on GitHub. |
| See whether the GitHub repository is a real fork |
gh repo view --json isFork,parent or REST
|
Fork relationship is hosted metadata. |
| Decide merge versus rebase | Inspect local graph/diff after fetch | It is a history/integration design choice, not a GitHub UI setting. |
15. Verification checklist
- Both disposable repository coordinates were recorded before mutation and collision-checked.
-
Contributor repository shared Git history but correctly reported
isFork=false. -
originandupstreamURLs were inspected before every push-sensitive phase. - Topic branch OID existed in contributor repository and was absent from canonical upstream before synchronization.
- Upstream change advanced canonical branch independently.
-
Fetch changed local
upstream/*tracking state without silently moving the topic branch. -
Topic synchronization used an explicit merge and pushed only to
origin. - Hosted default-branch synchronization was verified separately and local tracking state required its own fetch.
- No force sync, force push, branch deletion, or production repository was used.
Knowledge check
Why can two repositories have the same commit OID without one being a GitHub fork?
Git object identity is content/history based. GitHub fork-network membership is separate hosted relationship metadata.
After git fetch upstream, which ref should you
expect to move first?
A remote-tracking ref such as upstream/main, not
your current local topic branch.
You merged upstream into your topic locally but did not push. Has the hosted contributor branch changed?
No. The new merge commit exists locally until a push updates the hosted branch ref.
Why did the local origin/main remain stale
immediately after gh repo sync?
The CLI operation changed the hosted destination repository. Your local tracking ref updates only when the local clone fetches that remote.
What is the safest response if gh repo sync cannot
fast-forward?
Stop and inspect divergence/conflicts. Do not automatically use
--force, because it discards destination branch
history by hard-reset-style synchronization.
Authoritative references
Fork a repository
Configuring a remote repository for a fork
Syncing a fork
gh repo fork
gh repo sync
gh repo clone
gh repo view
REST API endpoints for branches
REST API versions
git-remote
git-fetch
git-merge
git-push
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.