Nothing you commit leaves your machine until you push it. Until then the work is yours alone, and the push is what turns it into shared history.
The remote will take a branch update only if it is a fast-forward, meaning the tip it holds right now is an ancestor of the commit you are pushing (Git documentation, June 2026).
So a push never creates a merge commit on the server. If the branch has diverged, Git rejects the update and you fix it locally. Fetch the remote history, integrate it, then push again.
What Does git push Do?

It uploads commits from a local branch to a remote repository, then moves the matching remote ref to the new tip. Git sends only the data the remote doesn’t have yet, and the files in your working directory stay as they are.
Private work becomes shared work at the push. Recording a commit with git commit touches only your local repository, so nobody else sees it until a push carries it out.
| Command | Direction | What it changes | Touches the remote |
|---|---|---|---|
| git commit | None | Local branch history | No |
| git push | Local to remote | Remote branch ref and objects | Yes, writes |
| git fetch | Remote to local | Remote-tracking branches | Yes, reads |
| git pull | Remote to local | Remote-tracking branch and current branch | Yes, reads |
Of the three network commands, only push uploads. According to the Pro Git book, git pull is essentially a fetch immediately followed by a merge.
Uncommitted and unstaged changes never go along, since only commits travel. Local branches you didn’t name stay behind too, because Git does not synchronize branches automatically (Pro Git). Existing remote history is left alone unless you force the update.
What Gets Sent to the Remote, and How the Remote Decides

A push sends the objects the remote lacks in a single packfile, along with a request to move one or more refs. The remote applies the update only if it passes validation.
What Is Transferred
For each ref, the client lists the object ID the server holds now, the ID it wants, and the ref name. The packfile carries the commits the server lacks, plus the trees and blobs those commits reference.
The server unpacks the pack, confirms each ref still matches the old ID, and runs any update hooks. Then it answers “unpack ok”, followed by “ok” or “ng” for every ref.
Two edge cases come straight from the pack-protocol specification. A push that only deletes refs sends no packfile, while a push that creates a branch at an existing commit still sends an empty one.
Transfers are thin by default, which cuts the data sent when both sides already share many objects (git-push documentation, –thin option).
The receiving end is normally a bare repository, a repository with no working tree to overwrite, as the Git user manual puts it.
The Fast-Forward Check
The remote accepts a branch update only if its current tip is an ancestor of the pushed commit. Anything else would drop history.
The Git documentation uses a standard case. You and a teammate start from commit X. They push commit A, and you build commit B on top of X.
Your push of B is not a fast-forward, because A is not an ancestor of B. Accepting it would discard A, so Git refuses the update unless you force it.
Some tutorials, Atlassian’s push guide among them, describe a push as the same thing as running git merge inside the remote repository. The Git documentation describes a ref update restricted to fast-forwards, so the push itself creates no merge commit and a diverged branch gets rejected instead of merged.
What counts as safe depends on the destination, at least in the Git 2.55 documentation (June 2026).
- Branches under refs/heads take fast-forward updates only.
- Tags under refs/tags reject every update to a tag that already exists.
- Creating or deleting a ref is always allowed, unless configuration or hooks forbid it.
The –force flag skips the fast-forward check and the other safety checks, so the remote can lose commits.
git push Syntax, Upstream Tracking and Default Behavior

The basic form is git push <remote> <branch>. git push origin main sends the local main branch to main on the remote named origin.
Run a bare git push and Git fills in the missing pieces from upstream tracking and the push.default setting.
A first push of a new branch goes like this.
- Commit the work on the local branch.
- Check the remote name and URL with git remote -v.
- Run git push -u origin branch-name.
- Look for the new-branch line in the output, which marks a created remote ref.
- Confirm tracking with git branch -vv. Later pushes need only git push.
Refspec and Remote Arguments
A refspec has the form [+]<src>[:<dst>]. The branch name main expands to main:refs/heads/main.
- git push origin serverfix:awesomebranch pushes the local serverfix branch to a remote branch called awesomebranch (Pro Git).
- git push origin HEAD pushes the current branch to the same name on the remote.
- Leave the remote out and Git uses the upstream of the current branch, or origin when none is configured.
With no refspec, Git decides what to push by precedence. The command line comes first (a refspec, –all, –mirror or –tags), then the remote.<name>.push setting, then push.default.
What -u Stores

git push -u origin feature-login pushes the branch and records an upstream in .git/config, as a remote field and a merge field.
After that, a plain git push and a plain git pull know which remote branch to use, because the local branch tracks an upstream branch. Git also sets an upstream in a few other situations.
- Cloning sets it for the default branch.
- Checking out a remote-tracking branch creates a tracking local branch.
- With push.autoSetupRemote=true, Git assumes –set-upstream on the first push of a branch.
Without an upstream, a bare git push can fail, depending on push.default. One push with the remote and branch spelled out fixes that.
push.default Values
push.default decides what a push does when no refspec is given. In the Git 2.55 documentation (June 2026), the default is simple, and it has been the default since Git 2.0.
| Value | What gets pushed | Catch |
|---|---|---|
| simple | Current branch to the same-named remote branch | Pushing to the remote you pull from needs an upstream with the same name |
| current | Current branch to a same-named branch | Works in central and non-central workflows |
| upstream | Current branch to its upstream | Only makes sense when you push where you pull |
| matching | Every branch with the same name on both ends | Default before Git 2.0, unsuitable for a shared central repository |
| nothing | Nothing | Errors out unless a refspec is given |
The value tracking is a deprecated synonym for upstream. Changing the setting takes one git config call, for example git config –global push.default current.
Why Is git push Rejected, and How Do You Fix It?

A push gets rejected when the remote branch contains commits your local branch lacks, so the update would not be a fast-forward. Fetch the remote history, integrate it with a merge or a rebase, and push again.
The usual culprit is a teammate who pushed first, which moves the remote branch after your last fetch. Rewritten history causes it too, when you amend or rebase commits that were already pushed.
Then there is the push race. Someone updates the ref between the server’s ref listing and your update, and the server catches it by comparing the old object ID (pack-protocol specification).
Git’s hint text varies, and each variant is named after an advice setting in the git-config documentation. pushNonFFCurrent covers a non-fast-forward update to the current branch, and pushNonFFMatching covers a non-fast-forward error after you pushed matching refs explicitly. pushFetchFirst covers an overwrite of a remote ref that points at an object you do not have, and pushAlreadyExists covers an update that cannot fast-forward, such as moving an existing tag. pushNeedsForce covers an update that involves an object that is not a commit-ish, and pushRefNeedsUpdate covers a forced update of a branch whose remote-tracking ref has updates you do not have locally. Setting advice.pushUpdateRejected to false switches all six off at once.
A status of rejected means Git did not even try to send the ref, typically because the update is not a fast-forward and was not forced. A status of remote rejected means the remote end refused the update, usually through a hook or a safety option such as receive.denyNonFastForwards.
To fix a rejected push:
- Run git fetch origin to update the remote-tracking branch.
- Run git log main..origin/main to see the commits you lack.
- Integrate them with git merge origin/main, or with git pull –rebase.
- Resolve any conflicts and finish the merge or rebase.
- Run git push. The update is now a fast-forward.
Conflicts appear while integrating in step 4, never at the push, so resolving merge conflicts in Git comes before the push can succeed.
Rewritten history is the one cause where integrating doesn’t help. After you rebase commits that were already pushed, the new commits have new IDs and the remote branch has diverged from yours.
The Git documentation reserves –force for cases where losing history is the goal, and only when nobody has fetched the earlier commit.
Force Push Versus force-with-lease
git push –force overwrites the remote ref with no check, while –force-with-lease overwrites it only if the remote ref still matches what your repository last saw. Use the lease form whenever the branch could hold someone else’s commits.
| Option | Safety check | Main risk | Fits |
|---|---|---|---|
| –force | None, and it skips the other push safety checks too | Drops commits the remote has and you lack | A branch only you push to |
| –force-with-lease | Remote ref must equal your remote-tracking branch | A background fetch makes the check pass wrongly | Default after a rebase or amend |
| –force-with-lease=ref:expect | Remote ref must equal the ID you name | You must know the expected ID | Setups with automatic fetches |
| + before a refspec | None, for that one ref | Same as –force, on one branch | Forcing one branch when several refs are pushed |
The Git documentation notes that –force applies to every ref in the push. With push.default set to matching, it can overwrite refs beyond the current branch.
Plain –force is one flag and no setup, but a teammate’s push can disappear without a warning. –force-with-lease fails when the remote ref moved since your last fetch. Its weak spot is anything that fetches in the background, because that resets the comparison.
The lease compares the remote ref with your remote-tracking branch, and every git fetch moves that branch. An editor or cron job that fetches behind your back makes the check pass even though you never saw the new commits.
The documentation offers a few ways around that. Add –force-if-includes, which rejects the forced update unless the remote-tracking tip is reachable from your local branch’s reflog (push.useForceIfIncludes sets it by default). Or name the expected ID yourself with –force-with-lease=ref:expect.
You can also fetch into a second remote such as origin-push, and push to that remote with the lease. The second-remote trick fails against anything that runs git fetch –all.
Here is the documented recipe for the explicit form, adapted to main.
- git fetch
- git tag base main
- git rebase -i main
- git push –force-with-lease=main:base main:main
The push succeeds only if the remote main still sits at base, whatever your remote-tracking branch says by then.
As of Git 2.55, the git-push documentation marks every lease form except the explicit expected-value one as experimental, with semantics that can still change.
I’d default to –force-with-lease. Keep plain –force for a branch only you push to.
Pushing Tags, Deleting Remote Branches and Other Options
A handful of options cover the remaining jobs. –tags and –follow-tags send tags, –delete or an empty source removes a remote ref, –dry-run previews a push, and a remote group pushes to several remotes in turn.
Tags
A plain git push does not transfer tags, according to Pro Git. A git tag marks a point in history, usually a release such as v1.0, so it stays local until you push it by name or in bulk.
| Form | What is sent | Tag types |
|---|---|---|
| git push origin v1.5 | That one tag | Lightweight and annotated |
| git push origin –tags | Every tag the remote lacks | Lightweight and annotated |
| git push origin –follow-tags | Annotated tags that point at commits being pushed | Annotated only |
Setting push.followTags to true turns –follow-tags on by default, and –no-follow-tags overrides it for a single push.
Deleting a Remote Branch
Any of these removes a remote branch.
- git push origin –delete serverfix
- git push -d origin serverfix, the short flag
- git push origin :serverfix, which pushes nothing to the remote ref name
Deleting removes only the pointer on the server. The server usually keeps the data until garbage collection runs, so an accidental delete is often recoverable (Pro Git).
Deleting a tag locally does not touch the remotes. git push origin –delete v1.4-lw removes the remote copy.
–prune is the most destructive flag in this group, because it removes every remote branch that has no local counterpart. Run it with –dry-run first.
Previewing and Multi-Remote Pushes

–dry-run (or -n) does everything except send the updates.
Run it before –prune, before a force push, and before the first push to a new remote.
A remote group pushes to several remotes in turn. Remotes get their names and URLs from git remote, and a group is a named list of those names in config, for example git config remotes.all-remotes “origin gitlab backup” followed by git push all-remotes.
- Git runs a separate git push for each member, with the same flags and refspecs.
- A failure on one remote is reported and the push carries on to the rest, ending with a non-zero exit code.
- –atomic is refused when the target is a group.
A single remote with several push URLs behaves differently. A push goes to all of them, while a fetch reads only the first URL (git-push documentation).
When Does git push Fail Even With a Clean History?
A fast-forward push still fails when the transport rejects your credentials, when you lack write access, when a hook exits non-zero, or when branch protection blocks the update. None of these has anything to do with the history check.
Authentication Failures
The Git protocol has no authentication of its own.
The pack-protocol specification leaves it to the transport, such as SSH, before receive-pack starts. Write access is a separate requirement, since Pro Git describes a push as going to a remote where you have it.
| Transport | Credential | Catch |
|---|---|---|
| HTTPS | Personal access token in place of a password | GitHub rejects account passwords for Git operations |
| SSH | Key pair registered with the host | The server must let your user run receive-pack through the SSH shell |
| git:// | None | Anyone who can reach port 9418 can write if receive-pack is allowed |
GitHub stopped accepting account passwords for Git operations on August 13, 2021, and requires token-based authentication instead: a personal access token, OAuth token, SSH key or GitHub App installation token (GitHub Changelog, 2021). The error text names that date and ends with an authentication failure.
Creating a GitHub access token and pasting it where Git asks for the password fixes it.
Over HTTPS, Git prompts for credentials on the terminal. Setting credential.helper to cache keeps them in memory for 15 minutes by default (Pro Git).
Hooks and Protected Branches
Hooks run at two points. pre-push runs on your machine before the upload, and the server-side hooks run inside receive-pack once the packfile arrives (githooks documentation, unchanged through Git 2.56).
- pre-push is client side. It receives the remote name and location as arguments plus one stdin line per ref, and a non-zero exit aborts the push before anything is sent.
- pre-receive runs once per push on the server, and a non-zero exit updates none of the refs.
- update runs once per ref, and a non-zero exit blocks only that ref. It is the documented place to enforce a fast-forward-only policy.
- post-receive runs after the updates and cannot change the outcome. Commit emails are the sample use.
Output from the server-side hooks is forwarded to the client, so a hook can explain its refusal in your terminal. That refusal appears as the remote rejected status.
–no-verify bypasses pre-push and nothing else. The server-side hooks run on the remote, so the client has no flag for them.
On GitHub, a branch protection rule blocks force pushes and branch deletion by default, and it can require approved pull requests, passing status checks or a linear history. The rule does not bind admins unless the “do not allow bypassing” setting is on (GitHub Docs, October 2026).
GitLab protects the default branch from the start, and force push is a separate “Allowed to force push” toggle. A protected branch can be deleted only through the web interface or the API, not with local Git commands (GitLab Docs, October 2026).
Git itself permits deletions and, with –force, overwrites unless a hook or configuration forbids them. Hosts add their own rules on top, so the same command can pass on one remote and fail on another.
GitHub reports a blocked update as “GH006: Protected branch update failed”, followed by the reason. When a rule requires reviews, the route is a feature branch and a pull request, because collaborators can only push to that branch through an approved one.
git push FAQ
What happens to existing files on the remote after a push?
A normal push adds commits and moves the branch ref forward, so earlier history stays in place. Only a forced update replaces the remote tip, and the commits it leaves unreachable are removed when git gc runs on the remote (Git documentation).
Why are my uncommitted edits missing from a push?
Because git push sends commits, not working-directory changes.
Staged work is skipped too. Record it with git commit first, then push.
Why does git push print “Everything up-to-date”?
The remote ref already points at your local commit, so Git has nothing to send.
The status line for an up-to-date ref carries the = flag, shown only with –verbose or –porcelain.
How do I undo a push?
Add a revert commit and push it, which leaves history intact for everyone who already fetched.
The alternative is resetting the branch and pushing with –force-with-lease. That removes the commit from the remote, but GitHub blocks force pushes on protected branches by default (GitHub Docs).
Does git push need an internet connection?
No. A push needs a network path to the remote, not the internet.
The remote can be a server on your LAN, a local path such as /path/to/repo.git/, or a file:// URL (git-push documentation).
What Happens to a Branch After the Push

Once git push publishes a branch, the remote holds the commits, but the shared branch does not change until a pull request merges them.
On GitHub, a later push that changes the pull request’s diff dismisses that approval when the stale-approval setting is on (GitHub Docs, October 2026).
A rebase followed by –force-with-lease replaces the commits reviewers saw, so a changed diff sends the pull request back for a fresh approval.
To keep approvals intact, push once with -u, open the pull request, and push fixes as new commits.
Rewrite history only before review starts. After approval, merging the branch on GitHub is the next step.
- MongoDB Cheat Sheet - October 20, 2026
- Adding AI Video Generation to a Software Product: Architecture, Cost Guardrails and QA - October 20, 2026
- Show Hidden Characters, Spaces, and Line Endings in Notepad++ - October 19, 2026




