Remotes and GitHub
Everything so far happens on one machine. A remote is a named URL pointing at another copy of the same repository, and Git gives you a small set of commands to move commits between copies. GitHub is one popular place to host that other copy, plus a layer of collaboration features on top.
Remotes
Section titled “Remotes”A remote is nothing more than a name and a URL stored in .git/config.
git remote -vorigin git@github.com:ada/parser.git (fetch)origin git@github.com:ada/parser.git (push)-v shows URLs; without it you get just the names. Two lines per remote because fetch and push URLs
can differ (rare, but supported).
git remote add origin git@github.com:ada/parser.gitgit remote add upstream https://github.com/original/parser.gitgit remote rename origin githubgit remote remove upstreamgit remote set-url origin git@github.com:ada/parser.gitgit remote show origin # detailed: branches, tracking, what push/pull would doorigin is a convention, not a keyword — it is simply the name git clone assigns to the URL you
cloned from. upstream is the conventional name for the original repository when you are working from
a fork.
Here is the relevant chunk of .git/config:
[remote "origin"] url = git@github.com:ada/parser.git fetch = +refs/heads/*:refs/remotes/origin/*[branch "main"] remote = origin merge = refs/heads/mainThe fetch line is a refspec: “copy every branch under refs/heads/ on the remote into
refs/remotes/origin/ locally”. The [branch] section is what makes main a tracking branch.
Remote-tracking branches
Section titled “Remote-tracking branches”origin/main is a remote-tracking branch: a local, read-only bookmark recording where main was
on origin the last time you talked to it. It lives in .git/refs/remotes/origin/main.
Three distinct things share the name “main”:
| Ref | What it is |
|---|---|
main |
Your local branch. You commit to it. |
origin/main |
Your cached copy of the remote’s main. Updated only by fetch/pull/push. |
main on the server |
The actual remote branch. Only changes when someone pushes. |
origin/main never moves on its own. If a colleague pushes and you do not fetch, your origin/main
is stale — and git status will confidently tell you that you are “up to date” because it is comparing
against that stale cache.
git branch -vv* main 9c1f0a3 [origin/main: ahead 2] Add pagination feature/api 4e2b7d1 [origin/feature/api: ahead 1, behind 3] Extract client local-only 1a2b3c4 Experiment-vv shows each branch’s upstream and how far ahead/behind it is. “Ahead 2” means you have two
commits origin/main does not; “behind 3” means the reverse.
Setting an upstream
Section titled “Setting an upstream”The upstream (or tracking) relationship is what lets you type bare git pull and git push.
git push -u origin feature/api # push and set upstream in one gogit branch -u origin/feature/api # set upstream for the current branchgit branch --unset-upstreamSince Git 2.37 you can make -u unnecessary:
git config --global push.autoSetupRemote trueFetch vs pull
Section titled “Fetch vs pull”This distinction causes more confusion than anything else in this area, and it is simple:
git fetchdownloads new commits and updates your remote-tracking branches. It never touches your working directory or your local branches. It is always safe.git pullrunsgit fetchand then immediately integrates the result into your current branch.
git fetch origingit fetch --all # all remotesgit fetch --prune # also delete origin/* refs for branches deleted on the serverA good habit: fetch, look, then decide.
git fetchgit log --oneline HEAD..origin/main # what did they add that I don't have?git log --oneline origin/main..HEAD # what do I have that they don't?git diff origin/main # what would changegit merge origin/main # integrate when readygit pull, and its two modes
Section titled “git pull, and its two modes”git pull # fetch + merge (default)git pull --rebase # fetch + rebase your local commits on topgit pull --ff-only # fetch, and only integrate if it fast-forwards; otherwise stopWith the default merge behaviour, if you have local commits and the remote also moved, you get a merge commit reading “Merge branch ‘main’ of github.com:…”. A history full of those is noisy and tells you nothing.
--rebase instead replays your local commits on top of the fetched ones, producing a linear history.
Because those commits were only local, this does not violate the golden rule of rebasing.
Pick a default so git pull never surprises you:
git config --global pull.rebase true # always rebase# orgit config --global pull.ff only # refuse to auto-merge; you decidegit push sends commits from a local branch to a remote branch.
git push # to the upstream of the current branchgit push origin main # explicitgit push -u origin feature/api # first push of a new branch, sets upstreamgit push origin --delete feature/api # delete the branch on the servergit push origin HEAD # push current branch to a same-named remote branchA push succeeds only if it is a fast-forward — that is, the remote branch’s tip is an ancestor of what you are pushing. If someone else pushed since your last fetch, Git rejects it:
! [rejected] main -> main (fetch first)error: failed to push some refs to 'github.com:ada/parser.git'hint: Updates were rejected because the remote contains work that you dohint: not have locally.The fix is never --force. Integrate first:
git pull --rebase # or: git fetch && git merge origin/maingit push--force overwrites the remote branch and destroys whatever commits were there. When you genuinely
need to overwrite (after amending or rebasing your own feature branch), use
--force-with-lease, which refuses if the remote moved since your last fetch — see
rewriting published history.
Tags mark a specific commit permanently — almost always a release. Unlike branches, they do not move.
git tag # listgit tag -l "v1.*" # filtergit tag v1.0.0 # lightweight: just a name pointing at HEADgit tag -a v1.0.0 -m "Release 1.0.0" # annotated: a real object with author, date, messagegit tag -a v1.0.0 9c1f0a3 # tag an older commitgit show v1.0.0git tag -d v1.0.0 # delete locallyUse annotated tags for releases. A lightweight tag is just a ref; an annotated tag is a full Git
object storing who tagged it, when, and why, and it can be GPG-signed with -s. git describe also
prefers annotated tags.
Tags are not pushed by default:
git push origin v1.0.0 # one taggit push origin --tags # all tagsgit push origin --follow-tags # annotated tags reachable from what you're pushinggit push origin --delete v1.0.0Authentication
Section titled “Authentication”Two ways to prove who you are to GitHub. GitHub removed password authentication for Git operations in 2021, so plain passwords are not an option.
SSH keys (recommended)
Section titled “SSH keys (recommended)”Generate a key pair, add the public half to GitHub, keep the private half secret.
ssh-keygen -t ed25519 -C "ada@example.com"# Accept the default path (~/.ssh/id_ed25519) and set a passphrase.# Start the agent and load the key so you type the passphrase once per sessioneval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519
# Copy the PUBLIC key and paste it into GitHub → Settings → SSH and GPG keyscat ~/.ssh/id_ed25519.pubVerify:
ssh -T git@github.com# => Hi ada! You've successfully authenticated, but GitHub does not provide shell access.Use SSH URLs: git@github.com:user/repo.git.
Ed25519 is the modern default; use ssh-keygen -t rsa -b 4096 only if you must support something
that predates it.
HTTPS with a personal access token
Section titled “HTTPS with a personal access token”Use HTTPS URLs (https://github.com/user/repo.git) and supply a personal access token (PAT) where
Git asks for a password. Create one under GitHub → Settings → Developer settings → Personal access
tokens.
Fine-grained tokens let you scope to specific repositories and permissions, and always have an expiry — prefer them over classic tokens.
Store it so you are not pasting it constantly, using a credential helper:
git config --global credential.helper cache # in memory, 15 min by defaultgit config --global credential.helper 'cache --timeout=3600'git config --global credential.helper osxkeychain # macOSgit config --global credential.helper manager # Windows (Git Credential Manager)git config --global credential.helper libsecret # Linux with libsecretThe gh CLI
Section titled “The gh CLI”GitHub’s official CLI handles authentication for you and adds commands for the parts of GitHub that are not Git.
gh auth login # interactive; can also configure git to use gh as a credential helpergh repo clone ada/parsergh pr create --fillgh pr checkout 142 # check out someone else's PR branch locallygh pr view --webgh issue list| Choice | Use when |
|---|---|
| SSH | Your normal machine. Set it up once, never think about it again. |
| HTTPS + PAT | Firewalls block port 22, CI systems, short-lived environments. |
gh auth login |
You want the least setup and already use GitHub features. |
GitHub concepts
Section titled “GitHub concepts”Git is the version control system. GitHub adds a shared server plus:
- Repository — a hosted copy of a Git repo, with settings, access control, and a web UI.
- Fork — your own server-side copy of someone else’s repository, under your account. It is a clone that GitHub tracks the relationship of. You need one when you cannot push to the original.
- Pull request (PR) — a request to merge one branch into another, with a diff, a discussion thread, review tools, and CI status. Despite the name, nothing is “pulled” until someone merges it. A PR is not a Git concept; it is a GitHub feature built on top of branches.
- Review — line-by-line comments plus an overall verdict: comment, approve, or request changes.
- Issue — a tracked bug, task, or discussion. Writing
Fixes #142in a commit message or PR description makes GitHub close issue 142 automatically when the PR merges. - Protected branch — server-side rules on
main: require reviews, require passing CI, forbid force pushes. This is how teams stop the mistakes in history and recovery from happening at all. - Tag and release — a release is a GitHub object attached to a Git tag, with release notes and optional binary assets. The tag is Git; the release page is GitHub.
- Actions — CI/CD triggered by pushes, PRs, tags, and schedules.
Merge strategies GitHub offers
Section titled “Merge strategies GitHub offers”When merging a PR you pick one of three, each with a Git equivalent:
| Button | Equivalent | Result on main |
|---|---|---|
| Create a merge commit | git merge --no-ff |
All branch commits plus a merge commit. |
| Squash and merge | git merge --squash |
One commit containing the whole branch. |
| Rebase and merge | git rebase then fast-forward |
Each branch commit, replayed, no merge commit. |
Squash is the most common default: PR-sized commits on main, and the messy branch history stays in
the PR where it is still viewable.
End to end: contributing to a project
Section titled “End to end: contributing to a project”The complete fork-and-pull-request flow, which is how essentially all open source contribution works.
1. Fork on GitHub. Click Fork on github.com/original/parser. You now have
github.com/ada/parser.
2. Clone your fork and add the original as upstream.
git clone git@github.com:ada/parser.gitcd parsergit remote add upstream https://github.com/original/parser.gitgit remote -vorigin git@github.com:ada/parser.git (fetch)origin git@github.com:ada/parser.git (push)upstream https://github.com/original/parser.git (fetch)upstream https://github.com/original/parser.git (push)origin is yours and you can push to it. upstream is theirs and you cannot.
3. Sync with upstream before starting. Forks do not update themselves.
git fetch upstreamgit switch maingit merge upstream/main # or: git rebase upstream/maingit push origin main4. Branch. Never work on main in a fork — it makes syncing painful and PRs unclear.
git switch -c fix/parser-off-by-one5. Work, in reviewable commits.
git add -pgit commit -m "Fix off-by-one when the header ends at a buffer boundary"6. Push to your fork.
git push -u origin fix/parser-off-by-one7. Open the pull request. From the web UI, or:
gh pr create --base main --head ada:fix/parser-off-by-one \ --title "Fix off-by-one in header parsing" \ --body "Fixes #142. The loop read one byte past the buffer when the header ended exactly at the boundary."8. Respond to review. Push more commits to the same branch; the PR updates automatically.
git add src/parser.jsgit commit -m "Add regression test for boundary case"git push9. Keep the branch current if main moves.
git fetch upstreamgit rebase upstream/maingit push --force-with-leaseForce-pushing is acceptable here because the branch is yours and everyone expects PR branches to be
rewritten. Use --force-with-lease, never bare --force.
10. After it merges, clean up.
git switch maingit fetch upstreamgit merge upstream/maingit push origin maingit branch -d fix/parser-off-by-onegit push origin --delete fix/parser-off-by-onegit fetch --pruneKey points
Section titled “Key points”- A remote is a name plus a URL;
originandupstreamare conventions, not built-ins. origin/mainis a cached bookmark of the server’s branch — it is only as fresh as your last fetch.fetchis always safe and never changes your files;pullisfetchplusmergeorrebase.- Push rejections mean the remote moved: integrate and push again, do not force.
- Prefer SSH keys; if you use HTTPS, use a fine-grained PAT with a credential helper.
- Tags need an explicit push; use annotated tags for releases.
- The fork model:
origin= your fork (push here),upstream= the original (fetch from here), one branch per change, PR from that branch.