On This Page
git push
TL;DR: The git push command uploads your local commit history (objects like commits, trees, and blobs) to a remote repository defined via git remote. It updates the branch pointers on the remote server to synchronize them with your local branches. To protect collaboration, Git servers reject pushes that would overwrite someone else’s work (non-fast-forward errors) unless explicitly forced.
How a push negotiates objects and refs
While /git-fetch brings data down from the cloud, git push is the primary command for uploading data. It connects to the URL designated in your remote configuration (set up using /git-remote) and securely transfers files.
git push is a negotiated ref update, not a copy of the whole repository. In a typical push:
- Compare refs: The remote advertises its current refs, and your client proposes the ref update. A local remote-tracking ref can help you understand prior state, but it may be stale; it is not the remote’s live answer.
- Transfer missing objects: Git sends the commits, trees, and blobs the remote needs, commonly in a packfile. The protocol negotiates which objects are missing; it does not upload every object in your local repository. See Git objects for the object model.
- Validate and update: The server checks the objects and proposed update. Fast-forward rules, hooks, or branch protections can reject it; only an accepted update moves the remote ref. See Git refs for how those references work.
- Record the result locally: After a successful push, Git typically updates the applicable remote-tracking ref. Its on-disk representation can vary, so it is not necessarily a standalone file under
.git/refs/.
Exact packet order and server behavior vary by protocol and implementation, but these steps describe the useful distinction: object transfer supplies data; the ref update changes which commit the remote branch names.
The Fast-Forward Rule
By default, Git rejects a push that would move a branch backward or discard commits on the server: the update must be a fast-forward. Server configuration and hosting rules can impose additional restrictions.
A fast-forward update means the server’s current commit is an ancestor of the commit you are pushing. In other words, you are appending commits to the existing history without replacing the server’s current tip.
If someone else has pushed a commit to that branch since you last fetched, the histories may have diverged. If the server accepted a non-fast-forward update, the other commit could become unreachable from that branch, though it may still exist through another ref or reflog.
To prevent this, the server rejects the push. Fetch and inspect the remote changes, then integrate them with git merge or git rebase before trying again. Avoid force-pushing unless you deliberately intend to rewrite the remote history.
Watching refs/remotes update after a push
This walkthrough assumes the default files ref backend, unpacked refs, and an initial branch named main. Git manages refs itself; other backends or packed refs can store them differently.
Let’s watch how the files inside .git/ change before and after running a push command.
Step 1: Baseline Local Setup
We will initialize a local repository, write a commit, and add a remote pointing to origin.
$ mkdir push-demo
$ cd push-demo
$ git init
Initialized empty Git repository in /home/admin/Code/push-demo/.git/
$ echo "Project Roadmap" > roadmap.md
$ git add roadmap.md
$ git commit -m "C1: Add roadmap draft"
[main (root-commit) 4d3e2a1] C1: Add roadmap draft
Let’s look at the commit hash stored inside our local main branch ref file:
$ cat .git/refs/heads/main
4d3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f
We configure the remote connection:
$ git remote add origin https://github.com/developer/project.git
At this stage, we have no remote tracking files:
$ find .git/refs/
.git/refs/
.git/refs/tags
.git/refs/heads
.git/refs/heads/main
Step 2: Simulate the Push
During a real push, Git negotiates the needed objects and proposes a ref update. Let’s assume the server accepts it.
After confirmation, Git updates the applicable remote-tracking ref. For this simplified files-backend example, we represent that ref as .git/refs/remotes/origin/main.
The following commands simulate that filesystem representation only; do not create or edit ref files manually in a real repository:
$ mkdir -p .git/refs/remotes/origin
$ echo "4d3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f" > .git/refs/remotes/origin/main
Let’s inspect our refs folder now:
$ find .git/refs/
.git/refs/
.git/refs/tags
.git/refs/heads
.git/refs/heads/main
.git/refs/remotes
.git/refs/remotes/origin
.git/refs/remotes/origin/main
And check what commits are tracked using git show-ref:
$ git show-ref
4d3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f refs/heads/main
4d3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f refs/remotes/origin/main
In this simplified example, both refs name the same commit. That shows the local branch tip matches the remote branch tip; it does not prove that every working-tree file is committed or that other branches are synchronized.
Step 3: Write Config Tracking
When you push with the upstream flag (git push -u origin main), Git writes config metadata inside .git/config indicating that the local main branch tracks the remote main branch.
Let’s inspect what changes in .git/config after a tracking configuration is saved:
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/developer/project.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
The new [branch "main"] block creates the association. It tells Git: “When I am checked out on the local branch ‘main’, my default remote is ‘origin’ and my remote tracking counterpart is ‘refs/heads/main’.”
This configuration enables you to type git push or git pull without specifying the remote and branch names every time.
Refspecs, upstreams, and force-with-lease
1. git push -u (Set Upstream tracking)
When working with new branches (created via /git-branch), the remote server does not know they exist. If you run a plain git push, Git will warn you:
fatal: The current branch feature-auth has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature-auth
Using the -u (or --set-upstream) flag during your first push maps your local branch to the remote branch:
git push -u origin feature-auth
This creates the remote branch on the server, uploads your commits, and writes the [branch "feature-auth"] association in .git/config. Subsequent commands on this branch require only git push or git pull.
2. Safeguarding Force Pushes: --force-with-lease
If you rewrite commit history locally (using git commit --amend or git rebase), your commit hashes change. If you attempt a normal push, Git rejects it because it is a non-fast-forward update.
To overwrite the server’s history with yours, you must “force” the push. There are two ways to do this:
The Dangerous Way: git push --force (-f)
This asks the remote server to overwrite the branch pointer even if the update is not a fast-forward. If a teammate pushed a commit while you were rebasing, their commit can become unreachable from that branch and harder to recover.
A safer option: git push --force-with-lease
This adds a check that the remote ref still has the value Git expects, but it is safer—not universally safe. The default lease usually compares against your local remote-tracking ref; if that ref is stale or updated by a background fetch, it may not reflect the value you meant to protect.
git fetch origin
git push --force-with-lease=refs/heads/feature-auth:<expected-object-id> origin feature-auth
Inspect the fetched branch before rewriting, and use an explicit expected object ID when you need to guard against one exact previous value. Branch protection or server hooks may still reject the push.
3. Deleting Remote Branches
You can use the push command to delete branches on the remote server:
git push origin --delete feature-obsolete
This tells Git: “Connect to origin, navigate to the refs directory on the server, and delete the branch reference file named feature-obsolete.” (It does not delete the branch in your local repository).
Rewriting history on a shared branch safely
You are working on a feature branch feature-payment. You made three messy commits during testing. Before opening a Pull Request for code review, you want to clean up your history by “squashing” your commits into a single, clean commit using interactive rebase, and then upload it.
Step 1: View Current Commits
Review the commits on your branch (using /git-log):
$ git log --oneline
4b3a2c1 Fix typo in webhook config
9e8d7c6 Try debugging api response
a1b2c3d Add Stripe checkout route
Step 2: Squash Commits Locally
Run an interactive rebase to squash the two debugging commits into the main feature commit:
$ git rebase -i HEAD~3
This opens your editor. You change the instruction verbs to squash (or s) for the top two commits:
pick a1b2c3d Add Stripe checkout route
squash 9e8d7c6 Try debugging api response
squash 4b3a2c1 Fix typo in webhook config
Save and exit. Write a clean commit message: "Implement Stripe checkout webhook integrations".
The rebase completes. Let’s look at the log now:
$ git log --oneline
d4e3f2a Implement Stripe checkout webhook integrations
Your history is clean, but the commit hash has changed from 4b3a2c1... to d4e3f2a....
Step 3: Push Safely Using Force-With-Lease
Because the commit hashes changed, a standard push will be rejected:
$ git push origin feature-payment
To https://github.com/developer/project.git
! [rejected] feature-payment -> feature-payment (non-fast-forward)
error: failed to push some refs to...
Execute a safe force push to update the PR on GitHub:
$ git push --force-with-lease origin feature-payment
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 342 bytes | 342.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/developer/project.git
+ 4b3a2c1...d4e3f2a feature-payment -> feature-payment (forced update)
The branch on GitHub has been updated cleanly.
Reading a rejected push
1. Blindly Force-Pushing on Shared Branches
- The Problem: A developer force pushes changes to the shared
mainordevelopbranch, destroying commits pushed by colleagues. - The Cause: Using
git push --force(or-f) without verifying if other work was present on the server. - The Fix: Never use
--forceon shared trunk branches. Set up branch protection rules on GitHub to disable force-pushes on primary branches. If you must force push, always use--force-with-lease.
2. Missing Untracked Branch Errors
- The Problem: Running
git pushon a new branch fails with “no upstream branch”. - The Cause: Git requires explicit instructions on where to push a new local branch on its first upload.
- The Fix: Always push with the upstream flag first:
git push -u origin branch-name
3. Pushing Sensitive Data (API Keys / Credentials)
- The Problem: You committed an
.envfile containing database passwords and pushed it to GitHub. - The Cause: Staging files without verifying exclusions.
- The Fix: Merely deleting the file and making a new commit will not remove the credentials; the secret remains in the commit history database. You must rewrite history using tools like
git-filter-repoorBFG Repo-Cleaner, and then force push to purge the object database on the server.
Push commands at a glance
| Command | Action | Safety Level |
|---|---|---|
git push | Pushes the current branch to its configured tracking upstream. | Safe |
git push origin <branch> | Pushes a specific branch to origin without setting tracking. | Safe |
git push -u origin <branch> | Pushes a branch and configures it as the tracking target. | Safe |
git push --force-with-lease | Force-pushes updates, but aborts if the remote contains untracked server commits. | Moderate |
git push --force / -f | Force-pushes updates, overwriting whatever commits are on the server. | Dangerous |
git push origin --delete <branch> | Deletes a branch on the remote server. | Moderate |
git push origin --tags | Pushes all local tags (references under .git/refs/tags/). | Safe |
Push questions, answered
Why is my git push rejected?
A push is rejected if the remote branch contains commits that do not exist in your local branch. This typically happens when a teammate pushes updates. To fix this, run git pull or git fetch followed by git merge or git rebase to integrate their updates, and then attempt your push again.
What is the difference between git push --force and git push --force-with-lease?
git push --force tells the remote server to overwrite the remote branch pointer with your local commit immediately, regardless of what exists on the server. git push --force-with-lease is a safety check: it only allows the push to succeed if the server’s branch pointer matches your local cached version of it. If someone has pushed new work to the server that you haven’t fetched yet, the push is safely blocked.
What does -u mean in git push -u origin main?
The -u (short for --set-upstream) flag configures tracking associations in .git/config. It links your local branch directly to the remote branch. Once configured, you can simply type git push or git pull without specifying the remote or branch names.
How do I push a local tag to the remote server?
By default, running git push does not upload tags. To push a single tag, specify it:
git push origin v1.0.0
To push all local tags at once, use the --tags flag:
git push origin --tags
Check your understanding
Loading quiz…