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).

Anatomy of git push origin main git push origin main the command always literal — never changes remote name set by git clone, not a keyword branch name defaults to current branch origin is a convention, not a Git keyword — run git remote -v to see what it points to
git push origin main names all three parts explicitly — origin is git clone's naming convention, not special syntax, so git remote -v always confirms what it actually points to.

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.

Upstream tracking: one stored link, four commands read it Local branch main exists only here Remote-tracking branch origin/main mirrors the remote's tip tracks .git/config git push git pull git fetch git status All four commands resolve through this one stored reference — nothing here is guessed
Upstream tracking is one link stored in .git/config; git push, git pull, git fetch and git status all read it instead of asking every time.

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.

From new branch to tracked remote branch LOCAL ONLY ON THE REMOTE checkout -b create + switch add . stage changes commit -m commit locally first push git push -u origin <branch> publish + start tracking repeats git push every push after this one Only the first push needs -u — every push after that can drop back to plain git push
Only the first push on a new branch needs -u — everything after that collapses back to a bare git push once tracking is set.

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.

Reviewing a push before it lands origin/main: src/api/client.ts git show /tmp/remote-version.ts local working copy src/api/client.ts on disk working directory, as-is Side-by-side diff viewer Smart Diff · syntax highlighting · Show Diff Only Run this before any push — the stakes are highest right before a force-push
git show extracts the remote's version as text without touching the working directory — read the diff before any push, and especially before a force-push.

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.

Local and origin/main have diverged C0 shared ancestor C1 local main not pushed yet C2 origin/main already pushed git push origin main ! rejected — not a fast-forward Pull first, then push succeeds C1 + C2 local + remote commits pull C3 (merge/rebase) builds on both parents push push origin main accepted — fast-forward now
A straight push fails the moment local and origin/main hold different commits off the same ancestor; pulling first turns two histories into one the remote will accept.

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.

--force vs --force-with-lease on the same branch origin/feature-branch has commit T you haven't fetched --force --force-with-lease git push --force overwrites unconditionally no check against the remote commit T is now unreachable gone from the branch history git push --force-with-lease checks the remote-tracking ref first — ref no longer matches refuses — push rejected commit T stays safe Same starting point, same intent — only the lease check notices the remote moved
Same remote branch, same missing commit T — --force overwrites blind, --force-with-lease checks first and refuses when the remote moved.

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.