Remotes and Collaboration
Working with others through shared repositories: remotes and origin, fetch, pull and push, remote-tracking branches, upstreams and forks.
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.
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 with2>/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.gitrather 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/mainfrom755311etocf25f2e; Joost's ownmainstayed where it was. -
git status -sb(short, with branch) reports[behind 1]. -
git log main..origin/mainlists 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:
mainsimply moved forward tocf25f2e.
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.