Git Push Explained: Upload Commits, Set Upstream, and Push to Deploy

`git push` is the command that moves your work off your laptop. Everything you commit lives only in your local repository until you push it; pushing uploads those commits to a remote — a shared copy that teammates, CI pipelines, or a production server can read. If you commit but never push, your collaborators see nothing. The commit is the save; the push is the share.

This guide walks through `git push` the way I’d explain it to a new hire on my team: the basic invocation, setting an upstream so future pushes are one word, why a push gets rejected and what to actually do about it, the real difference between `–force` and `–force-with-lease`, pushing tags, deleting remote branches, and finally pushing code straight to a server to deploy a live site.

Key Takeaways
`git push` uploads local commits to a remote repository; nothing is shared until you push.
`git push -u origin ` sets the upstream so later you can just type `git push`.
A “non-fast-forward” rejection is Git protecting you — the remote has commits you don’t. Integrate first (`git pull`), then push.
Never reach for `–force` to “make it work.” Prefer `–force-with-lease`, and never force a shared branch casually.
Push-to-deploy lets you `git push` to a server remote over SSH and have your live site update automatically.

What does `git push` actually do?

When you run `git push`, Git takes the commits on your current local branch that the remote doesn’t have yet and transfers them, along with the objects (trees, blobs) they reference, then moves the remote branch pointer forward to your latest commit. It is a one-directional sync: local to remote. It does not touch your working tree, and it does not pull anything down.

The simplest form, once a branch is tracking a remote, is just:

“`bash git push “`

The fully explicit form names the remote and the branch:

“`bash git push origin main “`

Here `origin` is the conventional name for your default remote (the URL you cloned from), and `main` is the branch you’re pushing. You can confirm what `origin` points at any time:

“`bash git remote -v

“`

The everyday workflow is two steps: commit locally to record your work, then push to share it.

“`bash git add . git commit -m “Add rate limiting to the API gateway” git push origin main “`

If `origin/main` already knows about everything up to your new commit, Git fast-forwards the remote pointer and you’re done.

How do you push a new branch and set its upstream?

When you create a branch locally, the remote has never heard of it. Your first push needs to both create the remote branch and link the two so Git knows they belong together. That link is the upstream (also called the tracking branch).

“`bash git checkout -b feature/oauth-login

git push -u origin feature/oauth-login “`

The `-u` flag (long form `–set-upstream`) does two things in one shot: it pushes the branch to `origin`, and it records that your local `feature/oauth-login` tracks `origin/feature/oauth-login`. After that, the bare command works:

“`bash git push # Git already knows where this goes git pull # …and so does pull git status # now shows “ahead/behind” relative to the upstream “`

Without an upstream set, a plain `git push` on a fresh branch errors out and tells you to specify the remote and branch. Setting it once with `-u` is the fix. If you forget and want to set it after the fact, the same command works on a subsequent push.

The single most common `git push` failure is the “rejected — non-fast-forward” error, and it trips up nearly every beginner because the instinct it triggers is exactly the wrong one. The error is not Git breaking. It is Git *protecting* you. The remote branch has commits that you don’t have locally — someone else pushed, or you pushed from another machine — since you last synced. If Git let your push through, it would have to move the remote pointer to your commit and orphan the commits that are already there, silently erasing work. So it refuses. The wrong reflex is to reach for `–force`, which says “throw away whatever’s on the remote and use mine” — and on a shared branch that deletes your teammates’ commits. The right move is always to *integrate first*: pull (or fetch and merge/rebase) so your local branch contains their commits too, resolve any conflicts, and *then* push as a clean fast-forward. Force-push is not a “make it work” button. It is an “I am certain I want to erase what’s on the remote” button. When in doubt, never force — pull, then push.

Why is my `git push` rejected, and how do I fix it?

You’ll see something like this:

“`bash $ git push origin main ! [rejected] main -> main (non-fast-forward) error: failed to push some refs to ‘[email protected]:yourname/yourrepo.git’ hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: ‘git pull …’) before pushing again. “`

The fix is to bring the remote’s commits into your local branch first. There are two clean ways to do that.

Option A — pull (fetch + merge):

“`bash git pull origin main # downloads remote commits and merges them in

git push origin main # now a fast-forward; accepted “`

Option B — fetch and rebase (keeps history linear, no merge commit):

“`bash git fetch origin git rebase origin/main # replays your commits on top of theirs

git push origin main “`

Rebase gives you a tidier, linear history; merge preserves the exact shape of what happened. Both are legitimate — pick the one your team agreed on. The point is identical: you cannot push commits on top of a remote you’ve fallen behind without first incorporating what’s there. For the mechanics of bringing those changes down, see and the related .

When is force push safe, and what’s the difference between `–force` and `–force-with-lease`?

Sometimes you genuinely need to overwrite the remote — for example, after you rebased your own feature branch and the remote still has the old, pre-rebase commits. Your branch and the remote have diverged on purpose, so a normal push is rejected and integrating would be wrong (you’d merge the old commits back in).

There are two ways to force, and the difference matters enormously.

“`bash

git push –force origin feature/oauth-login

git push –force-with-lease origin feature/oauth-login “`

`–force` overwrites the remote branch no matter what. If a teammate pushed a commit two minutes ago that you never fetched, `–force` deletes it without warning. `–force-with-lease` first checks that the remote is still exactly where you last saw it; if someone else pushed in the meantime, the push is rejected and you’re saved from clobbering their work. Always prefer `–force-with-lease`. It gives you the override you need on your own branch while keeping the safety net for the case you didn’t anticipate.

Two firm rules:

  1. Never force-push a shared branch like `main` or a long-lived `develop`. Rewriting history that others have already pulled creates painful divergences for the whole team.
  2. Only force a branch you own and know no one else is building on — typically your personal feature branch before it’s reviewed.
Command What it does
`git push` Push current branch to its configured upstream
`git push origin main` Push the `main` branch to the `origin` remote
`git push -u origin ` Push a new branch and set its upstream (tracking)
`git push –force-with-lease` Overwrite the remote *only if* no one pushed since your last fetch
`git push –force` Overwrite the remote unconditionally (dangerous on shared branches)
`git push –tags` Push all local tags to the remote
`git push origin ` Push a single tag
`git push origin –delete ` Delete a branch on the remote

How do you push tags and delete a remote branch?

Tags don’t travel with a normal push — `git push` moves branches, not tags. You push them explicitly.

“`bash git tag -a v2.1.0 -m “Release 2.1.0” # create an annotated tag git push origin v2.1.0 # push just that tag git push –tags # or push every local tag at once “`

This matters for releases and for any deploy pipeline that triggers on a tag. Pushing the tag is what makes the remote — and your CI — aware that a release exists.

Deleting a branch on the remote uses the same `push` command with `–delete`:

“`bash git push origin –delete feature/oauth-login

“`

That removes the branch from the remote only; your local copy stays until you delete it with `git branch -d`. If you manage many branches and want to understand the local/remote relationship better, covers the full lifecycle.

How do you use `git push` to deploy a live site?

Here’s where `git push` stops being just collaboration plumbing and becomes a deployment mechanism. If you control your own server, you can add it as a Git remote and push your code straight to it over SSH — no FTP, no manual file copying, no build artifacts to drag around.

The classic setup is a bare repository on the server plus a `post-receive` hook that checks out the pushed code into your web root.

On the server:

“`bash

mkdir -p /var/repos/mysite.git cd /var/repos/mysite.git git init –bare

cat > hooks/post-receive <<'EOF'

#!/bin/bash git –work-tree=/var/www/mysite –git-dir=/var/repos/mysite.git checkout -f main echo “Deployed to /var/www/mysite” EOF chmod +x hooks/post-receive “`

On your laptop, add the server as a remote and push:

“`bash git remote add production ssh://deploy@your-server-ip/var/repos/mysite.git git push production main “`

Every time you push to `production`, the hook fires and checks the latest `main` into the live web root. Your deploy is now literally one command. You can extend the hook to run `npm install`, build assets, run migrations, or restart a service — whatever your stack needs. To manage multiple targets like staging and production cleanly, it helps to understand .

This pattern only works when you have real shell access and control over the box — which is exactly what a proper developer hosting environment gives you. For the bigger picture of running your own controlled stack, see the complete guide to hosting for developers.

Deploy with a single `git push` on infrastructure you control. DarazHost VPS and dedicated servers let you push code straight to the server over SSH — set up a Git remote and push-to-deploy your live site, or run a bare repository with hooks for a full custom pipeline. You get root access, guaranteed resources, and 24/7 support, so your deploy workflow is yours to shape end to end.

Frequently asked questions

What’s the difference between `git commit` and `git push`? `git commit` records your changes into your *local* repository’s history. `git push` uploads those committed changes to a *remote* repository so others can see them. Commit is local and private; push makes it shared. See for the recording step.

Why does `git push` say “everything up-to-date”? Your local branch has no commits that the remote is missing. Either you haven’t committed since your last push, or you committed on a different branch than the one you’re pushing. Run `git status` and `git log origin/main..HEAD` to see what, if anything, is ahead.

Does `git push` upload my uncommitted changes? No. `git push` only moves *committed* history. Files you’ve edited but not committed, and staged-but-not-committed changes, stay on your machine. Commit first, then push.

Is `git push –force-with-lease` always safe? It’s much safer than `–force` because it refuses to overwrite if the remote moved since your last fetch. But it still rewrites history, so avoid it on shared branches. It’s safe on a personal feature branch you alone are working on.

Can I push to multiple remotes at once? Not in a single command by default — you push to one remote at a time (`git push origin main`, then `git push production main`). You can script it, or configure a remote with multiple push URLs if you genuinely need fan-out.

About the Author

Leave a Reply