Skip to main content

Git Remotes: Push, Fetch, Pull and Tracking Branches

Connect a Git repository to a remote, push and pull, fix rejected pushes, set a pull strategy, track branches, force-push safely and set up SSH keys.

So far the project lives in one folder on one computer. A remote is another copy of the repository that you exchange commits with, usually on GitHub, GitLab, Bitbucket or a server of your own. This part connects the project to a remote, adds a second developer, Sam, and goes through what happens when you both push: rejected pushes, git fetch versus git pull, tracking branches, force pushes that do not destroy other people's work, and cleaning up branches. At the end you set up SSH keys and HTTPS logins for a real hosting service.

Prerequisites

  • The status-page repository from Part 5, with two tags.
  • Merging and rebasing (Part 5), because pulling is fetching plus one of them.

How Remotes Work

  • git push sends your commits to the remote and moves the branch there.
  • git fetch downloads new commits from the remote and updates your remote-tracking branches (origin/main). It never changes your own branches or files.
  • git pull is git fetch followed by a merge or rebase of the remote-tracking branch into your current branch.

origin/main is your local, read-only record of where main was on origin the last time you talked to it. It only moves when you fetch, pull or push. That is why git status can say "up to date" while a teammate pushed a minute ago.

Set Up a Remote

A remote repository is a bare repository: only the .git contents, with no working tree, because nobody edits files on the server. On GitHub or GitLab you create one in the web interface (or with gh repo create) and get its URL. In this guide, a bare repository in ~/server plays the server, so everything runs on one machine:

On a hosting service, only the URL changes:

If you created the repository on the platform with a README, it has a commit your local repository does not have, and your first push is rejected. Create it empty, or fetch and merge that commit first (unrelated histories).

Push

git push origin main sends main to origin. -u (--set-upstream) also records origin/main as the upstream of your local main, so from now on a plain git push or git pull on main knows where to go, and git status compares your branch with it. You need -u once per branch.

The remote name and the branch name are two separate arguments. Git 2.56 recognizes the common slip of writing them as one:

Push Tags

git push does not send tags unless you ask:

git push origin v0.2.0 pushes one tag. git push --follow-tags pushes the annotated tags that point at the commits you are pushing, and git config --global push.followTags true makes that the default. How to push a tag and how to delete a remote tag have the details.

A Teammate Clones and Pushes

Sam clones the repository, sets the identity for this clone, adds a service and pushes:

(Sam's name and email are set in this repository's .git/config because both clones share one home directory in this guide. On Sam's own computer, they would be global settings.)

When Your Push Is Rejected

Meanwhile, you commit a change of your own without knowing about Sam's push:

The remote's main has a commit (Sam's) that is not in your main. If Git accepted your push, Sam's commit would be lost, so the push is refused. This is not an error to work around; it means "integrate first". Fetch, and look at the situation before doing anything:

The branches have diverged: one commit on each side since the shared one. A pull has to either merge them or rebase your commit on top of Sam's, and Git does not pick for you:

Your commit is local and unpublished, so rebasing it is safe and gives a straight history:

Pull: Merge or Rebase?

Set your preference once, globally, and override it per command when needed:

Setting git pull does Override for one pull
git config --global pull.rebase true Rebases your local commits onto the remote branch --no-rebase
git config --global pull.rebase false Merges, creating a merge commit when the branches diverged --rebase
git config --global pull.ff only Only fast-forwards; refuses when your branch and the remote one have diverged --rebase or --no-ff

pull.rebase true is a good default for day-to-day work: your unpublished commits move on top of what others pushed, and the history stays linear. Add rebase.autoStash true so a pull also works with uncommitted changes (Git stashes and re-applies them). pull.ff only is the strictest choice: nothing happens without your decision. Whatever you pick, the rules from Part 5 apply: never rebase commits that others have already pulled. The difference between git pull and git fetch goes deeper.

This guide uses pull.rebase true and rebase.autoStash true from here on:

Tracking Branches

A feature branch that you push has to get an upstream too. Without one, git push does not know where to send it. Start a runbook for the page in docs/runbook.md:

# Runbook: status page

## The page shows an old list

1. Run `scripts/build.sh` and check its output.
2. Deploy the new `build/` folder.

GitHub and GitLab also print a link to open a pull request for the new branch at this point. To skip the -u dance for new branches, let Git set the upstream on the first push, as the hint says:

git config --global push.autoSetupRemote true

Sam fetches and starts working on the same branch. git switch with the name of a branch that exists only on the remote creates a local branch that tracks it:

git branch -vv shows each branch's upstream in brackets, plus ahead N and behind N counts when they differ (as of the last fetch). Fetch a remote branch and check it out, check out a remote branch and make an existing branch track a remote branch cover the variations, such as git branch -u origin/main to set an upstream later.

Force Push Safely

You, on your copy of docs/runbook, add a line about who is on call below the title, and push:

# Runbook: status page

On call: the person listed in the team calendar.

Sam's commit is on the remote branch, so the push is rejected, the same way as on main:

It is tempting to "fix" this with git push --force. That would replace the remote branch with yours and delete Sam's commit from it. A force push is sometimes right, for example after you rebased your own branch, but it should only overwrite what you expect. --force-with-lease does that check: it only overwrites the remote branch if it still points where your origin/docs/runbook says it does:

"stale info": the remote branch moved since your last fetch, so the lease does not match. Now the trap: many editors fetch in the background. After a fetch, your origin/docs/runbook includes Sam's commit, the lease matches, and --force-with-lease alone would delete it. --force-if-includes adds a second check: the remote commit you are about to overwrite must be one your branch has actually included at some point:

The right move here is to integrate Sam's commit, not to force:

When you do need to force-push (after rebasing your own pull request branch onto main, for example), use git push --force-with-lease --force-if-includes, or make it an alias. Never force-push to main or other shared branches; most teams block it there with branch protection (Part 7).

Delete Remote Branches

The runbook is finished. You merge it into main (on a hosting service, this is the pull request's merge button) and delete the branch on the remote:

Sam still has origin/docs/runbook and a local docs/runbook. A normal fetch does not remove remote-tracking branches that are gone on the remote; --prune does:

[origin/docs/runbook: gone] marks a branch whose upstream was deleted, usually because its pull request was merged. Set git config --global fetch.prune true to prune on every fetch. Delete a branch locally and remotely has more examples. (Git 2.56 also added git branch --delete-merged <upstream>, which deletes local branches whose upstream is, for example, origin/main and whose work is already in it. It is meant for setups where topic branches track the main branch; with the usual one-upstream-per-branch setup above, look for : gone] instead.)

Inspect and Change Remotes

Command Does
git remote -v List remotes with their URLs
git remote add <name> <url> Add another remote, for example upstream for a fork
git remote set-url origin <new-url> Change a URL, for example from HTTPS to SSH (more)
git remote rename <old> <new> Rename a remote and its remote-tracking branches
git remote remove <name> Remove a remote and its remote-tracking branches
git ls-remote origin List the branches and tags on the remote without fetching

A Read-Only Server with git daemon

For sharing a repository on a trusted local network, Git includes a small server for the git:// protocol. It has no authentication and no encryption, so it is read-only by default and not something to expose to the internet. (--detach does not work in Git for Windows: there, run the same git daemon command without --detach and --pid-file in a second terminal, run the clone and push below in the first, then stop the daemon with Ctrl+C and skip the kill step.)

The push fails because git daemon only serves fetches unless you start it with --enable=receive-pack; the error message does not say so. --base-path maps git://host/status-page.git to ~/server/status-page.git, and --export-all serves every repository under it (without it, a repository is served only if it contains a file called git-daemon-export-ok). For anything beyond a LAN, use SSH or HTTPS.

Authenticate to GitHub, GitLab and Others

Hosting services accept two kinds of Git URLs, and each needs its own setup once per computer.

HTTPS with a credential helper

GitHub stopped accepting account passwords for Git over HTTPS in 2021, and GitLab and Bitbucket also expect a token instead of your password. A credential helper handles the login (usually through the browser) and stores the token safely, so you never type it:

If you use a personal access token, prefer fine-grained tokens limited to the repositories you need, and never put a token in the remote URL: it ends up in plain text in .git/config and in your shell history.

SSH keys

SSH uses a key pair: the private key stays on your computer, the public key goes to your account on the hosting service. Create an Ed25519 key, and give it a passphrase when asked:

If you already have a key at ~/.ssh/id_ed25519, ssh-keygen asks before overwriting it: answer n and use the key you have. The passphrase encrypts the private key on disk. To avoid typing it on every push, load the key into the SSH agent once per login (desktop systems usually start an agent for you, so ssh-add alone is enough there):

On macOS, ssh-add --apple-use-keychain ~/.ssh/id_ed25519 stores the passphrase in the keychain. In Git Bash on Windows, use the same commands as above: Git for Windows brings its own OpenSSH. (Windows also has a built-in "OpenSSH Authentication Agent" service, but only Windows' own ssh.exe talks to it; to use it from Git, set git config --global core.sshCommand C:/Windows/System32/OpenSSH/ssh.exe.)

Copy the whole ssh-ed25519 ... line into your account's SSH key settings (GitHub: Settings, SSH and GPG keys; or gh ssh-key add ~/.ssh/id_ed25519.pub --title devbox). Then test the connection:

ssh -T git@github.com

The first time, SSH asks you to confirm the server's host key. Compare the fingerprint with the one the service publishes; GitHub's Ed25519 key is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU (GitHub's list). After that, GitHub answers with a greeting like this (example output, from a run against github.com):

Hi OWNER! You've successfully authenticated, but GitHub does not provide shell access.

To use different keys for different accounts or hosts, add entries to ~/.ssh/config (how to choose the SSH key Git uses):

Host github.com
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

If you still use an old RSA key, read GitHub is removing ssh-rsa signatures before it breaks.

Host a Repository on Your Own Server

Any Linux server with SSH can host Git repositories; there is nothing to install beyond Git. On the server, create a bare repository in a directory your SSH user owns; on your computer, add it as a remote:

# on the server, as the user you log in with over SSH
git init --bare /srv/git/status-page.git

# on your computer
git remote add server deploy@SERVER:/srv/git/status-page.git
git push -u server main
git clone deploy@SERVER:/srv/git/status-page.git

For a shared server, create a dedicated git user whose login shell is git-shell (it only allows Git commands) and put each developer's public key in its ~/.ssh/authorized_keys. If you need a server for this, any small cloud VM works; on DigitalOcean, you can create a Droplet with Ubuntu 24.04 and the smallest size. That is a referral link: new accounts that sign up through it get DigitalOcean's standard signup credit ($5, valid for 90 days, checked in October 2026) and must add a payment method before creating a Droplet, and if you later spend $25 there, DevOps Daily gets $25 in credit (referral program). A bare repository has no web interface, pull requests or access control per branch; for a team, a hosting service or a self-hosted Gitea, Forgejo or GitLab is usually the better choice.

Forks: origin and upstream

For open source, you usually cannot push to the project's repository. You fork it (your own copy on the platform), clone your fork as origin, and add the original as upstream to stay in sync:

git clone git@github.com:YOU/project.git && cd project
git remote add upstream https://github.com/ORIGINAL-OWNER/project.git

# later: bring your fork's main up to date with the original
git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main

Your changes go on branches in your fork, and you open a pull request from there. gh repo sync does the update in one command. How to sync a forked repository covers the web button and conflicts.

GitHub Push Protection

GitHub scans pushes for known secret formats (cloud keys, tokens, private keys) and can reject them before they reach the repository. Push protection for users is on by default for every account and blocks secrets in your pushes to public repositories. Repository administrators can turn on push protection for a repository, so that it applies to everyone who pushes; that is free for public repositories, and private repositories need GitHub Secret Protection. GitHub runs secret scanning on public repositories automatically (GitHub's documentation).

A blocked push fails with GH013: Repository rule violations found and Push cannot contain secrets, followed by the type of secret, the commit and the file and line. Deleting the secret in a new commit does not help, because the earlier commit still contains it: remove it from the commit that introduced it (git commit --amend for the last commit, an interactive rebase for older ones, Part 8), then push again. Only use the bypass for a real false positive, and if the secret was real, revoke it anyway: it has left your machine in at least one commit. Part 9 adds checks that catch secrets before you commit.

Common Mistakes

  • Expecting git fetch to change your files. It only updates origin/*. Merge, rebase or pull to bring the changes in.
  • git push --force on a shared branch. Use --force-with-lease --force-if-includes, and only on branches you own.
  • Pulling with local commits and no strategy set. Git stops with "Need to specify how to reconcile divergent branches". Set pull.rebase once.
  • Forgetting to push tags. Use --follow-tags, or push the tag by name.
  • Tokens in remote URLs. Use a credential helper or SSH.
  • Stale branches. Turn on fetch.prune and delete branches marked gone.

Summary

  • A remote is another copy of the repository; origin/main is your last view of its main.
  • git push -u sends a branch and sets its upstream; push.autoSetupRemote does it automatically.
  • A rejected push means "integrate first": git fetch, look, then git pull --rebase (or merge) and push again.
  • --force-with-lease --force-if-includes is the recommended way to force-push your own branch.
  • git push origin --delete and git fetch --prune clean up merged branches.
  • Use a credential helper for HTTPS or an Ed25519 key with an agent for SSH.

Next: Collaboration Workflows, where pull requests, review and team rules come in.

Keep Learning

Updated:

Tags

GitVersion ControlGitHubBranchingCollaborationDevOps

Found an issue?