git push origin main uploads your local commits on the main branch
to the main branch on the remote named origin — it's the single most
common git push command developers type, and also the one most guides gloss over. This guide
covers the full mechanics of how to push git branches: what git push actually does under the
hood, how to push a new local
branch to remote for the first time, what the -u flag sets up and why you only need
it once per branch, how to push to a remote branch with a different name, and exactly what to do
when the push gets rejected. It also covers ground most tutorials skip — how to actually review
what you're about to push before you do, especially before a force-push. If you've already got a
branch on the remote and just need to switch to it locally, checking
out a remote branch is the mirror-image operation.
The Fast Answer: Push a Branch in Two Commands
Most of what people search for as "how to push git" — every git push command, really — boils down to one of two forms, and the only variable is whether the remote has ever heard of your branch before.
# Branch is already tracked — remote knows about it
git push
# First push of this branch — set upstream tracking at the same time
git push -u origin <branch> git push origin main is the fully spelled-out version of the first form:
origin is the remote name, main is the branch. Once a branch has
upstream tracking configured, plain git push does the identical thing without naming
either — that's covered in the section on the -u flag below. For a branch nobody has
pushed yet, run git push -u origin <branch> once; every push after that can drop
back to the bare command.
Not sure which situation you're in? git status says so directly: "Your branch is up
to date with 'origin/main'" means tracking exists; a failed push with "fatal: The current branch
<branch> has no upstream branch" means it doesn't yet. The rest of this guide expands on
exactly why, plus what to do the moment a push gets rejected instead of accepted.
What git push Actually Does
What does git push do? It uploads commits from a local branch to a branch on a remote
repository — it's the only step in the standard git add, git commit, git push sequence that
actually leaves your machine. Everything before it only touches your local .git
directory; nothing is visible to anyone else until git push runs. Mechanically: Git
compares the commit history on your local branch to the commit history the remote-tracking branch
(origin/main, for example) last recorded, figures out which commits the remote is
missing, and transfers those commit objects — plus every tree and blob they reference — over the
network. Once the objects land, Git asks the remote to move its main ref forward to
point at your new tip commit.
The remote only accepts that ref update if it's a fast-forward: your new tip has to build directly on top of what the remote already has. That's what makes a git push to a remote repository predictable — the remote never silently loses a commit that was already there, because a non-fast-forward update is rejected outright by default, covered fully below. Every git push to a remote repository follows that same contract, whether the far end is GitHub, GitLab, or a bare repo on a box you own. If a push ever lands somewhere unexpected, the remote itself is usually the culprit; changing the remote URL covers fixing one pointed at the wrong place.
Reading the Syntax: git push <remote> <branch>
The full git push example worth memorizing is git push <remote> <branch>
— three tokens, each with a specific job:
git push origin main
# | | |
# | | +-- local branch to push (defaults to the current branch)
# | +-- remote name (origin is the default remote's conventional name)
# +-- the command itself origin isn't special to Git — it's just the default name git clone
assigns to the remote you cloned from. A repository can have several remotes (origin,
upstream, a colleague's fork), and the second argument to git push picks
which one receives the push. Run git remote -v any time to see what origin
actually points to.
The third token can be a local branch name (git push origin main pushes your local
main), or a refspec in the form local:remote for pushing under a different
name — its own section further down. Naming the branch is what turns a bare git push command into
a push to a specific branch, rather than whatever's checked out. Drop the branch argument entirely
and Git falls back to push.default (simple, the default since Git 2.0,
pushes only the current branch, and only if it already tracks a remote branch of the same name — a
safety rail against an accidental single-branch git push turning into a push of everything).
Pushing a New Local Branch to Remote for the First Time
Pushing a new local branch to remote for the first time is one of the most common things people search for on this topic — sometimes phrased as pushing a local branch to remote, sometimes as creating a local branch and pushing it to remote, sometimes as publishing a local branch to remote. All of it describes the same moment: a branch that exists only on your machine, about to exist somewhere else too.
git checkout -b feature/retry-logic
# ...make commits...
git push -u origin feature/retry-logic
That single command does two things at once: it creates a new branch named
feature/retry-logic on the remote — Git creates remote branches implicitly, on first
push, there's no separate "create branch on remote" step — and it sets your local branch to track
that new remote branch, so every push and pull after this one can omit both arguments. Before the
first push, only your machine knows this branch exists; the moment the push succeeds,
origin/feature/retry-logic exists too, and anyone who fetches from that remote can see
it.
Haven't cloned the repository yet? See cloning a remote repository first. And if the push fails immediately with an identity error rather than a rejection, Git hasn't been told who you are on this machine; configuring your username and email is a one-time, unrelated fix.
The -u Flag and Upstream Tracking, Explained
-u is shorthand for --set-upstream; git push upstream configuration is
what it establishes. Upstream tracking is a persistent link, stored in .git/config,
between a local branch and a specific remote branch — once it's set, git pull,
git push, git status, and git fetch all know which remote
branch to compare against without you naming it every time.
git push -u origin main
# equivalent, spelled out:
git push --set-upstream origin main
# check what a branch currently tracks
git branch -vv
Do you have to set git push upstream tracking every time? No — that's the entire point of
-u. It's a
one-time configuration per local branch, not a per-push flag; run it once, and every subsequent
git push on that branch reads the stored reference and needs nothing else. Confusion
usually starts with re-cloning: a fresh clone tracks whatever branch was checked out automatically,
but any branch you create afterward starts with no tracking reference — exactly why the first push
of a new branch needs -u and later ones don't.
Skip -u on a git push new branch and the push still succeeds — it creates the remote
branch fine — but the next plain git push on it fails with "fatal: The current branch
<branch> has no upstream branch," along with a suggestion containing the exact -u
command needed. Less a real error than Git refusing to guess where an untracked branch should go.
Creating a Branch and Pushing It: The Full Sequence
Put together, the whole sequence for pushing a local branch to remote — whether on GitHub, GitLab, or any other host; the Git side is identical — is five commands, start to finish:
git checkout -b feature/login-retry # create + switch to the new branch
git add . # stage changes
git commit -m "Add retry logic" # commit locally
git push -u origin feature/login-retry # publishes the branch — first push
git push # every push after this one That's the real answer to "how do I create a new branch and push it" as a general question, and to "how do I publish a local branch to remote" as a description of the same thing — Git has no separate publish-branch command hiding anywhere; publishing a branch just means pushing it somewhere for the first time. GitHub, GitLab, Bitbucket and every other host built on Git use this exact sequence under the hood — pushing a local branch to GitLab follows these same five commands — and what differs between them is the web UI that appears afterward, not the commands, covered fully in its own section below.
The same five commands cover every branch type — push a feature branch to remote, a hotfix branch,
a release branch; Git sees no difference between them. To create a local branch and push it to
remote from a specific point in history rather than off main, branch from wherever you
need first — git checkout -b hotfix/security-patch v2.3.1, for instance — and the rest
of the sequence doesn't change. And if a commit slips into the sequence that shouldn't have,
catching it before the push is far cheaper than catching it after;
undoing the last commit covers the local-only fix.
Pushing to a Remote Branch With a Different Name
A git push to a remote branch whose name differs from your local one uses the refspec form
local:remote instead of a single branch name:
git push origin local-branch-name:remote-branch-name
# example: local branch "fix/login" published as "hotfix/login-retry"
git push origin fix/login:hotfix/login-retry
Git reads local-branch-name:remote-branch-name as two separate refs joined by a
colon — push whatever's at the local ref onto the remote ref, creating the remote branch if it
doesn't already exist, which is exactly what pushing a new remote branch under a different name
needs. This is the same refspec mechanism behind branch deletion, just with the local side left
empty: git push origin :remote-branch-name pushes "nothing" onto that remote ref,
Git's oldest syntax for deleting it — most people now use the clearer
git push origin --delete remote-branch-name instead, but both do the same thing.
Renaming on push is common for naming-convention differences between a personal fork and a shared
repository, or for splitting one local working branch into several purpose-named remote branches
without touching local branch names at all. Set up tracking for the renamed branch explicitly,
since -u alone can't infer a different name on each side:
git push -u origin local-branch-name:remote-branch-name sets
local-branch-name to track origin/remote-branch-name correctly, matching
by the remote side of the colon.
Reviewing What You're About to Push
Before any push — and especially before a force-push — it's worth actually looking at what's about
to change on the remote, not just trusting git status and git log to
summarize it correctly. git log origin/main..HEAD lists which commits are about to
move, and git diff origin/main shows the combined content difference, but for a
specific file it's often faster to look at both full versions side by side.
# Pull the remote version of a file as plain text
git show origin/main:src/api/client.ts > /tmp/remote-version.ts
# Your local working copy is already sitting on disk as-is git show origin/main:path/to/file extracts exactly what's on the remote right now, as
text, without touching your working directory. Paste that alongside your local file in a
side-by-side viewer with syntax highlighting, and the actual difference takes seconds to read
instead of reconstructing it mentally from a commit list. Diff Checker, a free Chrome extension
(also usable at diffchecker.pro), is built for exactly this: paste the remote version into one pane
and your local file into the other, pick from Smart Diff, Ignore Whitespace, or Classic (LCS)
comparison, and it renders a live side-by-side or unified diff with syntax highlighting across 17
languages — no "Compare" button needed, and the comparison itself runs entirely in the browser.
This matters most right before a force-push, where the cost of pushing the wrong version is
highest — the case covered two sections below. It's not a git client; git show does
the extraction, the extension does the reading. "Show Diff Only" collapses everything that didn't
change, with a context-lines picker (0, 1, 2, 3, or 5 lines), so a long file doesn't bury the two or three
lines actually about to overwrite the remote's copy. For comparing arbitrary file versions more
generally, the fuller rundown is in
diffing two files between branches.
When the Push Is Rejected: Non-Fast-Forward Errors
A rejected git push to a remote branch almost always means the same thing: the remote has at least one commit your local branch doesn't have yet. Here's the git push example everyone hits sooner or later:
git push origin main
# ! [rejected] main -> main (fetch first)
# error: failed to push some refs to 'https://github.com/you/repo.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. You may want to first integrate the remote changes... That's what non-fast-forward means concretely: your branch tip and the remote branch tip have diverged — someone else pushed in between your last fetch and this push, and Git refuses to silently overwrite commits it's never seen locally. The fix is to bring those commits in first, then push again:
git pull origin main
# resolves via merge or rebase, depending on pull.rebase config, then:
git push origin main git pull is fetch plus integrate in one step; a real conflict resolves the same way
as any other merge conflict — and if that merge turns messy,
aborting the merge puts you back where you started.
Prefer a linear history? git pull --rebase origin main
replays your local commits on top of the remote's instead of merging — same rejection, same
recovery, cleaner resulting log.
One case looks identical but needs a different answer: if the branch diverged because you rewrote your own already-pushed commits — an amend, a rebase, a squash — pulling would merge two versions of the same work into a mess. That's not a case to reconcile; it's a case to overwrite deliberately, covered next. If the safer move is actually to discard local commits and match the remote exactly, git reset --hard against origin/main does that — but only after confirming, via the review step above, that nothing local is worth keeping.
Force Push Without Losing Work: --force-with-lease
When the divergence is deliberate — you amended a commit, rewrote history with an interactive rebase, or squashed several commits into one, all on a branch already pushed once — pulling isn't the right move; overwriting the remote to match your rewritten history is. That's what a force push does, and there are two ways to do it with very different risk profiles.
git push --force origin feature/login-retry
# overwrites the remote branch unconditionally,
# even if it has commits you've never fetched
git push --force-with-lease origin feature/login-retry
# refuses if origin/feature/login-retry has moved since your last fetch --force overwrites the remote branch to match your local one, full stop — it doesn't
check whether the remote has changed since you last looked at it. If a teammate pushed a commit to
that branch five minutes ago and you --force over it without fetching first, their
commit is gone from the branch's reachable history the instant your push lands — recoverable from
their reflog, maybe, but not something to rely on.
--force-with-lease adds one check: before overwriting, Git compares the remote's
actual current tip to what your local origin/feature/login-retry remote-tracking ref
last recorded. If they match, nobody else has pushed since your last fetch, so the force proceeds.
If they don't, Git refuses with a rejection instead of silently destroying a commit you've never
seen. Run git fetch immediately before a force-push if there's any doubt — that keeps
the lease check meaningful rather than stale. For the strictest version,
--force-with-lease=<branch>:<expected-commit> pins the exact hash it must
match; newer Git (2.30+) also supports --force-if-includes for the same purpose.
Neither flag protects against your own mistakes on a branch nobody else touches — force-pushing over history you actually needed is just as final either way. That's the argument for reviewing the diff before force-pushing, covered above, and for knowing how to amend a commit locally before it ever reaches a force-push at all.
GitHub, GitLab and Other Hosts: What Changes
The push mechanics above are identical on GitHub, GitLab, Bitbucket, self-hosted Gitea, or any other Git host — any git push to a remote repository talks plain Git protocol over HTTPS or SSH, and no host changes that wire format. What differs is authentication, UI feedback, and what happens to the branch afterward: pushing a branch to GitHub and pushing a branch to GitLab diverge only in the credential each host expects and the screen it shows you once the push lands.
Pushing a branch to GitHub over HTTPS needs a personal access token in place of a password —
GitHub stopped accepting account passwords for Git operations in 2021, as
GitHub's
own docs on pushing commits to a remote spell out — while over SSH it needs a key on
your account, checked with ssh -T git@github.com. Pushing a local branch to GitLab
works the same way — a PAT or deploy token for HTTPS, an SSH key for SSH — and it fails with the
same 403-style error as GitHub when the credential is missing, not because the push command itself
differs.
Push a branch to GitHub that doesn't exist yet, and the web UI shows a banner offering to open a pull request from it — a UI convenience, not a separate step; the branch already exists on the remote the moment the push finished. Pushing to a new remote branch on GitHub behaves identically whether that banner appears or not. GitLab's equivalent is the merge request prompt — same idea, different name.
Where hosts genuinely diverge is branch protection: both let admins block direct pushes — and
separately, force-pushes — to branches like main, requiring a pull or merge request
with required reviews instead. That's a server-side policy rejection, not a Git-level non-fast-forward
check, covered in the table next. Before pushing to a branch you're not sure is protected,
checking which branch you're
actually on avoids finding out the hard way.
Push Errors and Fixes, Side by Side
Every rejection this guide covers, in one table — what the message actually means, the fix, and how much risk the fix carries.
| Error message | What it means | Fix | Risk |
|---|---|---|---|
fatal: The current branch <branch> has no upstream branch | No tracking reference set for this branch yet | git push -u origin <branch> once | None — just sets tracking |
! [rejected] main -> main (fetch first) | Non-fast-forward — the remote has commits you don't have locally | git pull (or fetch + rebase), then push again | Low — may need to resolve a merge or rebase conflict |
error: src refspec main does not match any | Local branch named main doesn't exist — often no commits yet, or the default branch is master | Check with git branch; commit first, or git branch -M main to rename | None |
remote: Permission denied (403) | Auth failure — wrong or missing credentials, expired token, or the wrong account entirely | Check git remote -v; re-add the SSH key or refresh the token | None, but a silent wrong-account push is worth double-checking |
Updates were rejected because the tip of your current branch is behind | The specific hint text behind a non-fast-forward rejection — someone else pushed since your last fetch | git pull --rebase origin <branch>, then push | Low — rebase can surface the same conflicts a merge would |
remote: error: GH006: Protected branch update failed | Branch protection rules block direct or force pushes to this branch | Open a pull or merge request instead of pushing directly | None — protection is working as designed |
error: failed to push some refs right after git commit --amend or a rebase | Local history was rewritten and no longer builds on the remote's tip | git push --force-with-lease, only once you've confirmed nobody else pushed | Moderate — rewrites shared history even when the lease check passes |
Everything up-to-date | Nothing to push — as much a source of confusion as an actual error | Check git status/git log -1: uncommitted changes never staged, or the wrong branch checked out | None — but easy to mistake for a completed push when nothing moved |
For the canonical reference on every flag mentioned here, see the official git-push documentation. Once a branch is pushed, reviewed, and merged, cleaning up is a separate step — deleting the local branch covers removing it from your machine after the remote copy has done its job.
Frequently Asked Questions
What does git push origin main actually do?
It uploads commits that exist on your local main but not yet on the remote's
main, then moves origin's main ref forward to match — assuming it's a
fast-forward. origin is just the conventional name for the remote you cloned from;
nothing about the command is special beyond spelling out both arguments explicitly instead of
relying on stored tracking.
How do I push a new branch to a remote for the first time?
Create it locally with git checkout -b <branch>, make at least one commit,
then run git push -u origin <branch>. That single command creates the branch
on the remote — Git does it implicitly on first push, no separate create step — and sets up
tracking, so every push after the first can drop back to a plain git push.
What's the difference between plain git push and git push origin main?
Once a branch has upstream tracking configured — via -u on an earlier push, or
automatically for the branch checked out at clone time — plain git push and
git push origin main do the identical thing: naming both arguments explicitly is
redundant, not different, once Git already knows the target. Before tracking exists, plain
git push fails outright, while the explicit form still needs -u the
first time to establish that link.
What does the -u flag do, and do I need it every time?
-u (--set-upstream) links a local branch to a remote branch, stored in
.git/config, so push, pull, fetch, and status commands on that branch know what to
compare against without being told. It's a one-time setup per branch, not a per-push flag.
How do I fix "fatal: the current branch has no upstream branch"?
Run the exact command Git suggests in the same error message:
git push --set-upstream origin <branch> (or the -u shorthand).
That both pushes the branch and sets tracking in one step, and every push after it can be a
plain git push.
Does git push send all my branches, or just the current one?
Just the current one. What does git push do with no arguments? Under
push.default = simple, the default since Git 2.0, a bare git push command is a
single-branch push: it sends the branch you have checked out, and only if that branch already
tracks a remote branch of the same name. To make it a push to a specific branch instead, name
the branch — git push origin <branch>. Pushing every branch at once takes
an explicit --all, which is rarely what you want.
What's the difference between --force and --force-with-lease?
--force overwrites the remote branch unconditionally, even if it has commits you've
never fetched — those become unreachable the moment the push lands.
--force-with-lease checks that the remote's current tip still matches your local
remote-tracking ref first, and refuses instead of silently discarding someone else's work. It's
the safer default on any branch someone else might touch.