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.
What Does git push Actually Do?
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.
To understand the mechanics, it is best to view git push as a multi-step negotiation between your local Git installation and the remote host server (such as GitHub, GitLab, or a private server):
- Query Remote State: Your local Git client contacts the remote server and asks: “Which branches do you have, and what commit hashes do they currently point to?”
- Determine Needed Commits: Git compares the hash of your local active branch (e.g.,
refs/heads/main) with the remote tracking branch hash (e.g.,refs/remotes/origin/main). It traces the lineage back to find the exact set of new commit objects that exist on your machine but are missing on the server. - Pack and Upload: Git packages the missing commit objects, directory tree objects, and file blobs (from /git-commit) into a compressed single archive called a packfile. It uploads this packfile to the server over HTTPS or SSH.
- Update Pointers: Once the server unpacks the files and verifies database integrity, the server moves its branch pointer to the new commit hash.
- Sync Cache: Your local Git client receives a confirmation message and immediately updates your local remote-tracking reference pointer (e.g.,
.git/refs/remotes/origin/main) to match.
+------------------+ Packfile Transfer +---------------------+
| Local Repository | ====================================================> | Remote Git Server |
| (Commit: C3) | | (e.g. GitHub) |
+------------------+ +---------------------+
| |
| Updates local ref pointer | Updates branch ref
v v
refs/remotes/origin/main <=============================================== refs/heads/main
(Updated to C3) Handshake Success Confirmation (Moved from C2 to C3)
The Fast-Forward Rule
Git enforces a strict policy for standard pushes: A push must result in a “fast-forward” merge on the server.
A fast-forward merge means the current branch pointer on the server exists as a direct ancestor in the commit history you are pushing. In other words, you are appending new commits to the existing history without altering, skipping, or diverging from any commits that already live on the server.
If someone else has pushed a commit to that branch since you last fetched, the histories have diverged. The server’s current commit is not an ancestor of your new commit. If the server accepted your push, it would replace its pointer with yours, effectively orphanizing (and deleting) your colleague’s commit.
To prevent this data loss, the server rejects the push, prompting you to run /git-pull or /git-fetch and merge the changes before trying again.
What Happens Inside .git/ (Hands-on exploration)
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 uploads the compressed packfile to the server. Let’s assume the server successfully receives it and updates its branch.
Once the server confirms the update, Git writes a tracking file for our remote branch under .git/refs/remotes/origin/main.
Let’s simulate this file system update:
$ 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
Both files are identical. They both contain the string 4d3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f. This means your local workspace is completely in sync with the remote repository.
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.
Detailed Command Options & Advanced Usage
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 tells the remote server: “Overwrite the remote branch pointer with my commit hash immediately. I do not care about any commits on the server that I don’t have.” If a teammate pushed a commit while you were rebasing, their work will be permanently deleted.
The Safe Way: git push --force-with-lease
This provides a crucial safety check. It tells the remote server: “I want to force update my branch, but ONLY if the current commit hash on the remote server matches my local cached copy of it (stored in refs/remotes/origin/branch).”
git push --force-with-lease origin feature-auth
If a colleague pushed updates to the server in the meantime, the server’s hash will not match your local refs/remotes/origin/feature-auth file. The push will fail, preventing you from accidentally destroying their work.
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).
Real-World Scenario: Cleaning History and Force-Pushing
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.
Common Pitfalls and Mistakes
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.
Command Quick Reference
| 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 |
FAQ (People Also Ask)
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
Test Your Knowledge
Loading quiz…