Git is distributed: every clone is a complete repository with its own history, and nothing is shared until you explicitly exchange commits. This page shows that exchange step by step. The short version is in Version Control with Git; how branches work locally is on Branches.

Remotes and origin

A remote is a short name for the address of another repository: a URL on GitHub or GitLab, an SSH path on a server, or a folder on the same disk. origin is simply the name git clone gives to the repository you cloned from. It is a convention, not a special status: you can rename it, and a repository can have several remotes.

In the examples on this page the "server" is a bare repository called shared.git in the same folder, so everything runs without a network. Sam connects an existing local repository to it:

git init -q --bare shared.git
git init -q net-tools && cd net-tools
echo "# Network tools" > README.md
git add README.md && git commit -q -m "Add README"
git remote add origin ../shared.git
git remote -v
git push -u origin main
git branch -vv
origin	../shared.git (fetch)
origin	../shared.git (push)
To ../shared.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
* main e9f93d2 [origin/main] Add README

git remote -v lists each remote twice, because the fetch and push addresses can in principle differ. The push created the branch main on the remote, and -u linked the local main to it, which git branch -vv shows in square brackets.

One shared bare repository with a main branch, and two clones, amara and joost. Each clone has its own main branch and a remote-tracking branch origin/main. Push sends commits from a clone to the shared repository; fetch brings commits from the shared repository into a clone's origin/main. shared.git (bare, on the server) main amara (clone) main origin/main joost (clone) main origin/main push fetch push fetch
The clones never talk to each other directly. Every exchange goes through the shared repository, and each clone keeps a read-only record (origin/main) of what it last saw there.

Other useful remote commands (not run here):

git remote rename origin github      # give a remote a different name
git remote remove old-server          # forget a remote (nothing on the server changes)
git remote set-url origin <new-url>   # the repository moved
git remote show origin                # branches, tracking and status in detail

Cloning

git clone <url> <folder> creates a folder, copies the complete history into it, adds a remote called origin, and checks out the remote's default branch. Here Amara has already pushed a first commit, and Joost clones the repository:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
cd joost
git branch -a
git branch -vv
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
* main 755311e [origin/main] Add inventory

Two details in these examples deserve a word:

  • Cloning an empty repository works but prints warning: You appear to have cloned an empty repository. Amara's clone hides that warning with 2>/dev/null.
  • For a clone from a local folder, Git stores the folder's absolute path as the URL. The examples replace it with a relative path (git remote set-url origin ../shared.git) so that the output shows ../shared.git rather than the name of a temporary folder. With a real server you would not need this step.

git branch -a shows Joost's single local branch main plus the remote-tracking branches, which the next section explains. origin/HEAD records which branch is the default branch on the remote.

Remote-tracking branches and upstreams

A remote-tracking branch such as origin/main is Git's local record of where a branch on the remote was the last time you talked to it. You never commit on it; only fetch, pull and push update it. That is why it can be out of date: origin/main does not change when a colleague pushes, only when you next fetch.

The upstream branch of a local branch is the remote-tracking branch it is linked to, for example main → origin/main. The link lets a plain git push and git pull work without arguments, and lets git status and git branch -vv report ahead (you have commits the remote lacks) and behind (the remote has commits you lack). A clone sets the upstream of the default branch automatically; for a new branch, git push -u origin <branch> sets it on the first push.

Fetch and pull

git fetch downloads commits and branch positions from a remote and updates your remote-tracking branches. It never changes your own branches, your staging area or your files, so it is always safe to run. git pull is two steps in one: a fetch, followed by integrating the upstream branch into your current branch, by merge (the classic default) or by rebase.

After Amara pushes a second router, Joost first looks before he leaps:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
git -C joost remote set-url origin ../shared.git
cd amara
printf '  r2: 192.0.2.2\n' >> inventory.yml
git commit -q -am "Add router r2"
git push -q
cd ../joost
git fetch
git status -sb
git log --oneline main..origin/main
git pull
From ../shared
   755311e..cf25f2e  main       -> origin/main
## main...origin/main [behind 1]
cf25f2e Add router r2
Updating 755311e..cf25f2e
Fast-forward
 inventory.yml | 1 +
 1 file changed, 1 insertion(+)
  • The fetch moved origin/main from 755311e to cf25f2e; Joost's own main stayed where it was.
  • git status -sb (short, with branch) reports [behind 1].
  • git log main..origin/main lists exactly the commits that are on the remote but not yet in his branch, so he can review them first.
  • Because Joost had no commits of his own, the pull was a fast-forward: main simply moved forward to cf25f2e.

Push

git push uploads your commits to a remote and moves the remote branch to point at them. A normal push is only allowed if it is a fast-forward on the remote: your branch must already contain everything the remote branch has, so that the push only adds commits. That rule is what keeps one person from silently overwriting another's work.

Command What it does
git push -u origin feature/ntp First push of a new branch: create it on the remote and set it as upstream
git push Push the current branch to its upstream
git push origin --delete feature/ntp Delete a branch on the remote
git push origin v1.2.0 Push a tag (tags are not pushed by default; see Branching Strategies and Releases)
git push --force-with-lease Overwrite the remote branch, but only if it is still where you last saw it

Since Git 2.37 you can set git config --global push.autoSetupRemote true, and the first plain git push of a new branch sets the upstream by itself.

Two people, one branch

Amara Okafor and Joost Bakker both work on main of the team's tools repository. Amara adds router r2 to the inventory and pushes. Meanwhile Joost, without fetching, commits a README and tries to push:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
cd joost
git remote set-url origin ../shared.git
git config user.name "Joost Bakker"
git config user.email joost@example.com
cd ../amara
printf '  r2: 192.0.2.2\n' >> inventory.yml
git commit -q -am "Add router r2"
git push
cd ../joost
echo "# Network tools" > README.md
git add README.md && git commit -q -m "Add README"
git push
To ../shared.git
   1c37dbd..c2f9c2f  main -> main
To ../shared.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '../shared.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Amara's push succeeded (1c37dbd..c2f9c2f: the remote main moved forward). Joost's was rejected: the remote now has c2f9c2f, which his branch does not contain, so accepting his push would throw Amara's commit away. Nothing is broken; Git is asking him to integrate first.

Fix 1: fetch and merge

The same situation, now resolved. Joost fetches, looks at the two diverged histories, merges, and pushes:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
cd joost
git remote set-url origin ../shared.git
git config user.name "Joost Bakker"
git config user.email joost@example.com
cd ../amara
printf '  r2: 192.0.2.2\n' >> inventory.yml
git commit -q -am "Add router r2"
git push
cd ../joost
echo "# Network tools" > README.md
git add README.md && git commit -q -m "Add README"
git fetch
git status
git branch -vv
git log --oneline --graph --all --format='%h %an: %s'
git merge --no-edit origin/main
git log --oneline --graph --format='%h %an: %s'
git push
To ../shared.git
   1c37dbd..c2f9c2f  main -> main
From ../shared
   1c37dbd..c2f9c2f  main       -> origin/main
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

nothing to commit, working tree clean
* main 02ccd83 [origin/main: ahead 1, behind 1] Add README
* 02ccd83 Joost Bakker: Add README
| * c2f9c2f Amara Okafor: Add router r2
|/
* 1c37dbd Amara Okafor: Add inventory
Merge made by the 'ort' strategy.
 inventory.yml | 1 +
 1 file changed, 1 insertion(+)
*   22b5851 Joost Bakker: Merge remote-tracking branch 'origin/main'
|\
| * c2f9c2f Amara Okafor: Add router r2
* | 02ccd83 Joost Bakker: Add README
|/
* 1c37dbd Amara Okafor: Add inventory
To ../shared.git
   c2f9c2f..22b5851  main -> main

"Ahead 1, behind 1" is the signature of diverged branches. The merge commit 22b5851 has both 02ccd83 and c2f9c2f as parents, so it contains everything the remote has, and the push is now a fast-forward on the server. Had both edited the same lines, the merge would have stopped with a conflict to resolve first (see Merging and Conflicts).

Fix 2: pull with rebase

Many teams prefer not to have a merge commit for every "someone pushed before me". git pull --rebase fetches, then replays Joost's local commit on top of Amara's, giving a straight line:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
cd joost
git remote set-url origin ../shared.git
git config user.name "Joost Bakker"
git config user.email joost@example.com
cd ../amara
printf '  r2: 192.0.2.2\n' >> inventory.yml
git commit -q -am "Add router r2"
git push
cd ../joost
echo "# Network tools" > README.md
git add README.md && git commit -q -m "Add README"
git pull --rebase --quiet
git log --oneline --format='%h %an: %s'
git push
To ../shared.git
   1c37dbd..c2f9c2f  main -> main
ba580ab Joost Bakker: Add README
c2f9c2f Amara Okafor: Add router r2
1c37dbd Amara Okafor: Add inventory
To ../shared.git
   c2f9c2f..ba580ab  main -> main

The history is a straight line. Joost's commit is on top, but with a new hash: ba580ab instead of 02ccd83. Rebasing replayed the same change on a new parent, and a commit with a different parent is a different commit. Rebasing your own, unpushed commits like this is safe; rebasing commits others already have is not. Rebasing and Rewriting History explains why.

"You have divergent branches"

If Joost runs a plain git pull in the same situation without having chosen merge or rebase, Git refuses to guess:

git init -q --bare shared.git
git clone -q shared.git amara 2>/dev/null
cd amara
git remote set-url origin ../shared.git
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf 'routers:\n  r1: 192.0.2.1\n' > inventory.yml
git add inventory.yml && git commit -q -m "Add inventory"
git push -q -u origin main
cd ..
git clone -q shared.git joost
cd joost
git remote set-url origin ../shared.git
git config user.name "Joost Bakker"
git config user.email joost@example.com
cd ../amara
printf '  r2: 192.0.2.2\n' >> inventory.yml
git commit -q -am "Add router r2"
git push
cd ../joost
echo "# Network tools" > README.md
git add README.md && git commit -q -m "Add README"
git pull
To ../shared.git
   1c37dbd..c2f9c2f  main -> main
From ../shared
   1c37dbd..c2f9c2f  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

Git 2.27 began warning about pulling without a configured strategy; since Git 2.33.1 (including the 2.39 used here) the pull stops with this message whenever the histories have diverged. The fetch part already happened, so nothing is lost. Choose once and Git remembers:

Setting On a diverged pull Good for
pull.rebase false Merge, creating a merge commit Keeping an exact record of parallel work
pull.rebase true Rebase your local commits on top A linear history on shared branches (popular)
pull.ff only Refuse; you decide each time People who always fetch and inspect first

Forks and the upstream remote

On a hosting platform you often cannot push to a project you want to contribute to. A fork is your own copy of that repository on the platform, under your account or organization, with its full history and a recorded link to the original. You push to your fork and then open a pull request asking the original project to merge your branch. Forks are a platform feature (GitHub, GitLab, Bitbucket), not a Git command.

A typical GitHub fork workflow for Mei Chen (not run here; it needs the network):

# 1. Click "Fork" on github.com/example-org/net-tools, then clone YOUR fork
git clone git@github.com:mei-chen/net-tools.git
cd net-tools

# 2. Add the original project as a second remote, by convention named "upstream"
git remote add upstream https://github.com/example-org/net-tools.git

# 3. Work on a branch and push it to your fork
git switch -c fix-ntp-servers
git commit -am "Fix NTP server addresses"
git push -u origin fix-ntp-servers
# 4. Open a pull request from mei-chen:fix-ntp-servers to example-org:main

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

Inside a company, teams usually skip forks and push branches directly to the shared repository, because everyone already has write access; branch rules and pull request reviews provide the control instead (see Pull Requests and Code Review).

Bare repositories

A bare repository is a repository without a working tree: it contains only what normally sits inside .git. Nobody edits files in it, so nobody's uncommitted work can get in the way of a push. That is why the repositories on a Git server are bare, and why the "server" on this page is one:

git init -q --bare shared.git
ls shared.git
git -C shared.git rev-parse --is-bare-repository
HEAD
branches
config
description
hooks
info
objects
refs
true

The .git suffix is only a naming convention for bare repositories. Git normally refuses a push to the checked-out branch of a non-bare repository, because it would make that repository's working tree silently out of date. For a quick shared repository on a file server or a Linux host you reach over SSH, git init --bare is all you need.

SSH or HTTPS URLs

Hosting platforms offer each repository under two kinds of URL:

# SSH: authenticates with your SSH key pair
git remote add origin git@github.com:example-org/net-tools.git

# HTTPS: authenticates with a token or a credential helper
git remote set-url origin https://github.com/example-org/net-tools.git
SSH (Secure Shell) HTTPS (HTTP Secure)
URL form git@github.com:owner/repo.git https://github.com/owner/repo.git
Port 22 443
You authenticate with An SSH key pair; the public key is registered in your account settings A personal access token or a credential manager that signs in through the browser, not your account password
Strength No secret typed or stored per repository; one key for all repositories Works through strict corporate firewalls and proxies that block port 22

For SSH, GitHub's documentation recommends an Ed25519 key (ssh-keygen -t ed25519 -C "you@example.com"); creating keys and loading them into ssh-agent is covered in SSH and Remote Work. You can switch an existing clone between the two forms at any time with git remote set-url; the history does not change.