How to Set Up Git Remotes and Push Pull Fetch Correctly
Set up git remotes and use push, pull and fetch correctly: add a remote, set the upstream, read the tracking status, and fix the common rejected push.

A remote is not a special server concept, only a stored URL with a short name. Most confusion comes from three verbs with similar results and different effects. The detail on the pull case is in fetch versus pull, and the examples here assume Git 2.47.
Add and inspect remotes
git remote add origin [email protected]:example/app.git
git remote add upstream [email protected]:original/app.git
git remote -vorigin [email protected]:example/app.git (fetch)
origin [email protected]:example/app.git (push)
upstream [email protected]:original/app.git (fetch)
upstream [email protected]:original/app.git (push)
origin is the conventional name for where you cloned from, set automatically by git clone. A second remote named upstream is the convention for the source repository when you work from a fork. Change a URL later with git remote set-url, which the SSH switch post covers.
Push and set the upstream
git push -u origin main
git push
git branch -vvbranch 'main' set up to track 'origin/main'.
* main 3a1b2c4 [origin/main] Fix redirect
The first push of a branch uses -u to link it to the remote branch. Afterwards plain git push and git pull know where to go. The bracket in branch -vv shows the upstream and whether you are ahead or behind, for example [origin/main: ahead 2, behind 1].
Fetch, then decide
git fetch origin
git log --oneline main..origin/main
git diff main origin/main --stat
git merge origin/mainfetch downloads new commits and moves your origin/* references but leaves your own branches and working files alone. Looking at main..origin/main shows what you are about to receive. This is the safe way to see changes before integrating; git pull does fetch and merge in one step.
A rejected push
! [rejected] main -> main (fetch first)
error: failed to push some refs to '[email protected]:example/app.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
Someone else pushed first. Fetch, then rebase your commits on top (git pull --rebase) or merge, as compared in merging versus rebasing, and push again. Avoid --force on shared branches. When you must overwrite your own branch after a rebase, use git push --force-with-lease, which refuses if the remote moved in the meantime.
What origin/main actually is
The most useful mental model is that origin/main is a bookmark in your own repository, not a live view of the server. It records where the remote's main pointed the last time you talked to it. Only fetch, pull and push move it. This explains the most common confusion: a branch can show "up to date with origin/main" while the server has twenty new commits, because Git has not asked the server since the last fetch.
These bookmarks are called remote-tracking branches, and they are read-only from your point of view. You cannot commit to origin/main directly; you commit to local main and push, and Git updates the bookmark when the server accepts the push. A branch's upstream setting is the link between the two, and it is what makes git status able to report that you are ahead of or behind the remote.
Working with more than one remote
Forks make two remotes ordinary. The usual layout has origin pointing at your fork and upstream pointing at the project you forked. You push to origin, and you fetch from upstream to learn what the maintainers have merged. Bringing your fork current is then a fetch followed by a merge or rebase of upstream/main into your local main, followed by a push to origin.
Two settings reduce mistakes here. git config push.default simple, the default since Git 2.0, pushes only the current branch to its same-named upstream, so a plain git push cannot publish unrelated work. And git remote set-url --push upstream DISABLE gives the upstream remote a push URL that cannot work, which turns an accidental push to the project into an error and not a pull request that was never intended.
Pruning and inspecting what the server has
Remote-tracking branches outlive the branches they track. After a pull request merges and the server deletes the branch, your repository still lists origin/feature-x until you prune. git fetch --prune removes the stale bookmarks, and git config fetch.prune true does it on every fetch. This keeps git branch -r an honest list rather than a history of everything anyone ever pushed.
To look at the server without changing anything locally, git ls-remote origin lists every ref and its commit id. It is also the quickest connectivity test, since it fails on authentication or network problems before any data transfer. For a closer comparison, git remote show origin lists which local branches are configured to push and pull where, and which remote branches are stale. When a script needs to know whether a push is needed, comparing the output of git rev-parse HEAD with the matching ls-remote line avoids a full fetch.
Authentication and the URL you chose
Many failed pushes are authentication problems that look like Git problems. A remote can use an HTTPS URL, which authenticates with a username and token through a credential helper, or an SSH URL, which authenticates with a key. The two do not mix: a key loaded in the agent does nothing for an HTTPS remote, and a stored token does nothing for an SSH one. git remote -v shows which kind a remote uses.
For HTTPS, set a credential helper once, such as the keychain helper on macOS or git-credential-libsecret on Linux, so the token is stored by the operating system and not in a plain text file. Hosting services no longer accept account passwords for Git operations, so a prompt that rejects what you type usually wants a personal access token. For SSH, confirm the key is loaded with ssh-add -l and test the connection with ssh -T git@host.
Switching an existing remote between the two is a one-line git remote set-url, covered in changing a remote URL to SSH. Nothing else about the repository changes, and every branch keeps its upstream.
If a push still fails after the checks above, read the full error before retrying. Git states the cause precisely: a rejected non-fast-forward, a protected branch that requires a pull request, a hook that refused the content, or a large file that exceeds the host's limit. Each has a different fix, and force-pushing through the first two is exactly what the protection exists to stop.
Where this does not apply
Some workflows never push to the shared branch: pull request flows push to a feature branch and merge on the server. Mirrors and backups use git push --mirror or git clone --mirror, which copy every ref and are not meant for daily work. Shallow clones lack history, so some operations need git fetch --unshallow. See the git-remote manual and the Pro Git chapter on remotes.


