Chapter 04Lesson 02~145 minutes

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.

origin / upstreamgh repo syncTopic branchesVerification

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/upstream deliberately, 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 sync safely for a fast-forwardable hosted branch and explain how that differs from local Git synchronization.
  • Inspect an optional real fork path with gh repo fork without making a paid/enterprise feature mandatory.
Availability: Mandatory lab: GitHub.com, GitHub Free, current 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.

Safety: use the exact lab names below or add a unique suffix. Do not point 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

  1. Prediction A: after seeding the canonical repository, main will exist in $OWNER/$UPSTREAM and have one specific OID.
  2. Prediction B: after creating the contributor repository from the same local history, its main can initially point to the same OID even though GitHub reports fork=false.
  3. Prediction C: pushing topic/navigation to origin will create that branch only in the contributor repository; the canonical repository will remain unchanged.
  4. Prediction D: fetching upstream changes into the local clone will update upstream/main but 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.

Optional hardening: for workflows where contributors should never push upstream from this clone, teams sometimes configure an unusable 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"
No --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.

Optional cleanup: delete only the fork you created after ensuring no pull request, workflow, or other dependency uses its branches. A public fork is a real hosted repository, not a throwaway branch.

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.
  • origin and upstream URLs 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?

After git fetch upstream, which ref should you expect to move first?

You merged upstream into your topic locally but did not push. Has the hosted contributor branch changed?

Why did the local origin/main remain stale immediately after gh repo sync?

What is the safest response if gh repo sync cannot fast-forward?

Next lesson

Choose the collaboration policy deliberately

Lesson 03 compares fork and direct-branch workflows, merge versus rebase, synchronization cadence, remote naming, and cleanup policy.

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.

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