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

  1. 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?”
  2. 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.
  3. 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.
  4. Update Pointers: Once the server unpacks the files and verifies database integrity, the server moves its branch pointer to the new commit hash.
  5. 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 main or develop branch, 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 --force on 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 push on 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 .env file 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-repo or BFG Repo-Cleaner, and then force push to purge the object database on the server.

Command Quick Reference

CommandActionSafety Level
git pushPushes 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-leaseForce-pushes updates, but aborts if the remote contains untracked server commits.Moderate
git push --force / -fForce-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 --tagsPushes 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…