git push
git push is the command that uploads your local commits to a remote repository such as GitHub, updating a branch there so teammates (and your other machines) can see your work. Until you run git push, every commit you make lives only on your machine — the remote repository has no idea it exists. Understanding exactly what push does, and does not do, is essential for working safely on a shared branch.
Overview: How git push Works
Git repositories are decentralized: your local repository and the copy on GitHub are two full, independent repositories that happen to share history. A remote is just a named URL pointing at another repository — origin is the conventional name Git gives the remote you cloned from, but you can add others with git remote add. Your local repo also keeps remote-tracking branches like origin/main, which are read-only bookmarks recording where the remote’s main branch was the last time you talked to it. They only move when you fetch, pull, or push — never by committing locally.
Recall Git’s object model: every commit is an object containing a pointer to a tree (a snapshot of your project’s files and directories), a pointer to its parent commit(s), and metadata (author, message, timestamp). A branch such as main is nothing more than a small file holding the 40-character SHA-1 (or SHA-256, on newer repos) hash of its latest commit — a lightweight, movable pointer. When you run git push origin main, Git does three things: it walks the commit graph from your local main tip backward and figures out which commit, tree, and blob objects the remote doesn’t already have; it transfers only those missing objects over the network as a compressed \”packfile\”; and then it asks the remote to move its own main ref to point at your new commit. That last step only succeeds if it’s a fast-forward — meaning the remote’s current commit is a direct ancestor of the commit you’re pushing. If someone else has pushed commits to main that you don’t have locally, the remote’s tip is no longer an ancestor of yours, and Git rejects the push rather than silently discard those commits.
The first time you push a new local branch, Git doesn’t know it should be linked to a same-named branch on the remote. The -u (--set-upstream) flag establishes that link, called an upstream or tracking relationship. Once set, a bare git push and git pull on that branch automatically know which remote branch to talk to, and commands like git status can tell you \”your branch is 2 commits ahead of origin/main\”.
Syntax
The general form is git push [options] [remote] [refspec], where the refspec describes which local branch maps to which remote branch. Common flags:
| Flag | Meaning |
|---|---|
-u, --set-upstream |
Link the local branch to the remote branch so future plain push/pull know the target |
--force |
Overwrite the remote branch even if it isn’t a fast-forward — dangerous, can discard others’ commits |
--force-with-lease |
Like --force, but fails if the remote has commits you haven’t fetched yet — the safer default for rewriting shared-but-yours history |
--tags |
Push all local tags that aren’t already on the remote |
--delete (or a leading : before the branch name) |
Delete a branch on the remote |
--all |
Push all local branches |
--dry-run |
Show what would be pushed without actually pushing |
-n |
Shorthand for --dry-run |
You can also push a local branch to a differently-named remote branch with a full refspec, local-branch:remote-branch, or delete a remote branch by pushing an empty local side, :remote-branch.
Examples
Example 1: A basic push to an existing branch
git push origin main
Output:
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:yourname/recipe-box.git
a1b2c3d..e4f5g6h main -> main
Git enumerated the objects reachable from your local main, figured out the remote was missing 3 of them, uploaded a small packfile, and then fast-forwarded the remote’s main ref from commit a1b2c3d to e4f5g6h. Because this was a fast-forward, no merge or rewrite happened on the remote.
Example 2: Pushing a brand-new branch and setting upstream
git switch -c feature/login-page
git add src/login.js
git commit -m "feat: add login page skeleton"
git push -u origin feature/login-page
Output:
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 612 bytes | 612.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'feature/login-page' on GitHub by visiting:
remote: https://github.com/yourname/recipe-box/pull/new/feature/login-page
remote:
To github.com:yourname/recipe-box.git
* [new branch] feature/login-page -> feature/login-page
branch 'feature/login-page' set up to track 'origin/feature/login-page'.
feature/login-page didn’t exist on GitHub before this push, so Git creates it there and points it at your new commit. GitHub also prints a handy link to open a pull request straight away. The final line confirms the upstream link was set — from now on, git push and git pull on this branch need no extra arguments.
Example 3: A rejected push, and recovering safely
git push origin main
Output:
To github.com:yourname/recipe-box.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:yourname/recipe-box.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
A teammate pushed to main after your last fetch, so your local main is no longer an ancestor of the remote’s main — a fast-forward is impossible. The fix is to integrate their work first, then push again:
git pull --rebase origin main
git push origin main
git pull --rebase fetches the remote commits and replays your local commits on top of them, keeping history linear. Only after your branch is caught up does the second push succeed as a fast-forward.
How It Works Step by Step
When you run git push origin main, Git performs roughly these steps internally:
- It looks up the URL associated with the remote name
origin(stored in.git/config). - It contacts the remote and asks for the current SHA of its
mainref. - It compares that SHA to your local remote-tracking ref
origin/main. If they differ, someone else has moved the remote branch since your last fetch, and Git will reject a non-forced push. - Assuming the check passes, Git walks backward from your local
maintip through parent commits, collecting every commit, tree, and blob object the remote doesn’t already have. - It compresses those objects into a single packfile and transfers it over the network (SSH or HTTPS).
- The remote unpacks the objects into its own object database and, if the update is a fast-forward (or you passed
--force/--force-with-lease), moves itsmainref to your new commit SHA. - Your local repository updates its own
origin/mainremote-tracking ref to match, sogit statusandgit log origin/mainreflect reality without another fetch.
Common Mistakes
Mistake 1: Force-pushing over a shared branch with bare –force
git push --force origin main
Bare --force overwrites whatever is on the remote unconditionally, even if a teammate pushed commits you’ve never seen — those commits simply vanish from main (they’re still recoverable from the reflog on the teammate’s machine for a while, but that’s a bad day for everyone). Prefer --force-with-lease, which refuses to push if the remote has moved since your last fetch:
git fetch origin
git push --force-with-lease origin main
This is only ever appropriate on a branch that’s genuinely yours (like your own feature branch after an interactive rebase) — never on a shared branch like main that others are actively committing to.
Mistake 2: Forgetting to set the upstream on a new branch
git switch -c feature/search-filters
git commit -am "feat: add search filter UI"
git push
Output:
fatal: The current branch feature/search-filters has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/search-filters
Git has no way to know which remote branch a brand-new local branch should map to, so it refuses to guess. Push with -u the first time (Git even prints the exact command to copy):
git push -u origin feature/search-filters
Mistake 3: Reacting to a rejected push by force-pushing instead of integrating
After seeing a [rejected] message, it’s tempting to \”make the error go away\” with git push --force. On a shared branch this is the rebase/force-push golden-rule violation: it discards a colleague’s history without warning. Always git fetch and either merge or rebase first, resolve any conflicts, and only then push normally (or with --force-with-lease if you rebased your own already-pushed feature branch).
Best Practices
- Never rebase or force-push a branch that others have already pulled or built work on top of — the golden rule of rebasing. Force-pushing is only safe on branches you alone own.
- Default to
--force-with-leaseinstead of bare--forcewhenever you must rewrite a branch’s history that’s already on the remote. - Use
-uthe first time you push a new branch so subsequentgit push/git pullcalls need no arguments. - Run
git push --dry-runwhen you’re unsure exactly what a push will do, especially before a force-push. - Write Conventional Commits style messages (
feat: ...,fix: ...,chore: ...) so pushed history stays readable for reviewers. - Push small, frequent commits on feature branches rather than one giant commit — it makes code review and any future
git bisectfar easier. - Authenticate over SSH or with a Personal Access Token — GitHub no longer accepts plain password auth over HTTPS.
- Delete remote feature branches after their pull request merges (
git push origin --delete feature/login-page) to keep the branch list clean.
Practice Exercises
- Create a new local branch named
feature/dark-mode, make a commit on it, and push it tooriginso that a plaingit pushwould work on your next commit without extra flags. Check GitHub to confirm the branch appears. - On that same branch, make a second commit, then use
git rebase -ito squash your two commits into one. Push the rewritten branch up — plaingit pushwill be rejected; figure out which flag lets you update the remote branch safely, and explain why it’s safer than the alternative. - Create an annotated tag
v1.0.0on your latest commit onmain, then push only that tag toorigin(not with--tags). Verify it shows up under your repository’s Tags/Releases page on GitHub.
Summary
git pushuploads local commits (and the objects they reference) to a remote branch, moving that branch’s ref forward on the remote.- A push only succeeds without extra flags when it’s a fast-forward; otherwise Git rejects it to avoid silently discarding commits.
-u/--set-upstreamlinks a local branch to a remote one so future pushes and pulls need no arguments.--force-with-leaseis the safe way to overwrite remote history you rewrote yourself; bare--forcecan destroy teammates’ work.- Never rewrite (rebase or force-push) a branch other people have already pulled from.
- Use
git push origin --delete <branch>to remove a remote branch, andgit push --tagsorgit push origin <tag>to publish tags.
