The previous lesson made clear what a remote is: a short name for a URL. Now we have to create one. And we get to do it with the case we have been postponing for three modules: Ana is going to publish task-manager.
Until today the project has lived entirely on her Ubuntu laptop. It has a respectable history — four base commits, two merges, a squash, a resolved conflict — and main sits at c2a8f1e. In lesson 02-02 we watched Bruno clone the project and saw that the clone registered a remote called origin without anybody asking it to. That lesson explained half the story: how a remote is inherited. This one explains the other half: how to create one from scratch, which is what Ana needs now, because her repository was born with git init and has never spoken to anybody in its life.
We will also open .git/config and pick apart the line fetch = +refs/heads/*:refs/remotes/origin/*. It is called a refspec, it appears in every repository in the world and hardly anyone knows what it says. Understanding it is what turns fetch and push from magic commands into predictable ones.
Contents
- The starting point: a repository with no remotes
git remote add: registering the first onegit remote -v: which remotes do I havegit remote show: the full X-ray- How it ends up in
.git/config - The refspec, explained calmly
- Renaming, removing and changing the URL
set-url --push: read from one place, write to another- Working with several remotes
initversusclone: two ways of having a remote
- The starting point: a repository with no remotes
Ana is in her project, exactly as we left it at the end of module 3:
c2a8f1e (HEAD -> main) Merge the empty-list message 6d3f8b2 Show a message when the list is empty 3b9e7d1 Add CSV export of the task list
And now the question of the day:
Silence. There is no remote configured, which is exactly what you would expect in a repository created with git init. We can confirm it by looking at the disk:
Four lines of basic configuration and no [remote …] section at all. Compared with Bruno's .git/config that we saw in lesson 02-02, both the remote section and the branch tracking section are missing here.
Before we go on, somebody has to have created the destination repository. In a real case, Ana would go into the web interface of GitHub, GitLab or the company's Gitea and click "New repository", getting a URL back. To practise without depending on anything, we set up the exact equivalent with what we learnt in the previous lesson:
Important: the destination repository is empty. Not one commit, not one branch. That is normal and desirable: publishing a project means pouring your history into a freshly created repository.
In the narrative we will use the team's real URL, https://git.example.com/team/task-manager.git; in the commands you can reproduce, the local path. They are interchangeable: as far as Git is concerned, both are valid URLs.
git remote add: registering the first one
git remote add: registering the first oneThe syntax is as simple as the concept:
Ana runs it:
Absolute silence, which in Git means success. And rightly so: nothing has happened beyond writing three lines into a text file. It has not connected anywhere, has not checked that the URL exists, has not downloaded anything and has not asked for credentials.
Let us prove it in the most emphatic way possible:
Git accepts it happily. git remote add is a purely local, offline operation. Only when you run fetch or push against that name will Git discover that the URL leads nowhere. This detail explains a lot of bafflement: people believe "the remote must be fine because git remote add did not complain", when that command never complains about a bad URL.
Let us clean up the experiment:
Rules for the name
- It must be unique within the repository: if
originalready exists,git remote add origin …fails witherror: remote origin already exists. - It accepts letters, numbers, hyphens and underscores. Avoid spaces, slashes and accented characters.
- There are no reserved words.
originis pure convention, as we saw in 04-01.
The -f option
With -f (fetch), Git registers the remote and immediately runs a git fetch against it. It is a handy shortcut when you add a remote that already has content and you want it straight away. In Ana's case it adds nothing, because the destination repository is empty.
git remote -v: which remotes do I have
git remote -v: which remotes do I haveThe two basic query commands:
Names only. And the useful version:
origin https://git.example.com/team/task-manager.git (fetch) origin https://git.example.com/team/task-manager.git (push)
Why does the same remote appear twice? Because Git stores the read URL (fetch) and the write URL (push) separately. They are usually identical, which is why the line repeats, but they can differ: we will see that in section 8, and there the output of -v stops being redundant and becomes informative.
git remote -v should be your first reflex when you land in somebody else's repository, along with git status and git log --oneline -5. In three seconds it tells you who that repository talks to.
git remote show: the full X-ray
git remote show: the full X-rayThis command is in another category, and it is worth understanding why:
This one does hit the network. Unlike git remote -v, which only reads your .git/config, git remote show queries the server to ask which branches it has right now. With no connection or with failing credentials, this command fails.
Since Ana's repository is still empty, the output is sparse. Let us look instead at the one Bruno gets later on, once there is content, because that is the one that teaches you something:
* remote origin
Fetch URL: https://git.example.com/team/task-manager.git
Push URL: https://git.example.com/team/task-manager.git
HEAD branch: main
Remote branches:
docs/update-notes tracked
fix/focus-after-delete tracked
main tracked
experiment/pwa new (next fetch will store in remotes/origin)
feature/old-idea stale (use 'git remote prune' to remove)
Local branches configured for 'git pull':
main merges with remote main
Local refs configured for 'git push':
main pushes to main (up to date)It is worth reading line by line, because it packs in five separate pieces of information:
| Section | What it means |
|---|---|
| Fetch URL / Push URL | The two configured URLs. If they differ, you see it here |
| HEAD branch | The server's default branch. It determines which branch a git clone leaves you on |
| Remote branches | The branches that exist right now on the server, with their status |
| Local branches configured for 'git pull' | Which local branch integrates with which remote one (lesson 04-06) |
| Local refs configured for 'git push' | Which local branch goes where when pushing, and whether it is up to date |
The three possible states of a remote branch deserve an explanation:
tracked: it exists on the server and you hold its local reference. The normal situation.new: it exists on the server but you do not have it yet. Somebody created it since your lastfetch.stale: you hold the reference but it no longer exists on the server. Somebody deleted it there. You clear it out withgit remote prune, which we will see in lesson 04-04.
If you want the information without going out to the network:
With -n (no query), Git shows only what it knows from its local configuration. It is fast and works offline, but the list of remote branches will be the one from your last synchronisation, not the real one.
- How it ends up in
.git/config
.git/configLet us go back to the file, which is where you really see what has happened:
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://git.example.com/team/task-manager.git
fetch = +refs/heads/*:refs/remotes/origin/*That is everything git remote add did. A new section with two keys. You can edit it by hand in any editor and it will work just the same; the git remote commands are nothing more than a convenient interface for writing into this file.
The same keys can be queried and modified with git config, because they are perfectly ordinary configuration:
Remember from lesson 01-05 that these keys live at the local level (.git/config), the most specific of the three. A remote belongs to one particular repository, and configuring it with --global would make no sense at all.
And now, that second line.
- The refspec, explained calmly
This line appears in every Git repository on the planet and hardly anyone could explain it. That is a shame, because it is straightforward and because understanding it lights up the whole module. It is called a refspec: a specification of how the other repository's references are translated into references of your own.
The structure
A refspec has three parts:
+refs/heads/*:refs/remotes/origin/* │└─────────┬┘ └────────────┬─────┘ │ SOURCE DESTINATION │ (over there, in (over here, in my │ the remote repository) │ repository) │ └── optional modifier
| Part | Value | Meaning |
|---|---|---|
+ |
Modifier | Allows non-fast-forward updates: writes the new value even when it is not a clean advance |
refs/heads/* |
Source | All the branches of the remote repository |
: |
Separator | "translates to" |
refs/remotes/origin/* |
Destination | They are stored here, under the remote's namespace |
Read out in plain English:
"Take every branch from the remote and store it on my disk under
refs/remotes/origin/, overwriting whatever is there."
Concrete examples of the translation the asterisk performs:
| Branch on the server | Stored on your disk as | Short name you use |
|---|---|---|
refs/heads/main |
refs/remotes/origin/main |
origin/main |
refs/heads/docs/update-notes |
refs/remotes/origin/docs/update-notes |
origin/docs/update-notes |
refs/heads/fix/focus-after-delete |
refs/remotes/origin/fix/focus-after-delete |
origin/fix/focus-after-delete |
Here is where the slash in origin/main comes from, the question left open at the end of module 3. It is not an arbitrary separator or a special syntax: it is the real path of the file inside .git/refs/. origin/main is called that because it sits at refs/remotes/origin/main.
flowchart LR
subgraph REMOTE["Server: refs/heads/"]
R1["main"]
R2["docs/<br/>update-notes"]
R3["fix/<br/>focus-after-delete"]
end
subgraph LOCAL["Your disk: refs/remotes/origin/"]
L1["origin/main"]
L2["origin/docs/<br/>update-notes"]
L3["origin/fix/<br/>focus-after-delete"]
end
R1 -->|refspec| L1
R2 -->|refspec| L2
R3 -->|refspec| L3
Why that translation exists
It might look like an unnecessary detour. Would it not be simpler for the server's branches to be copied straight onto your local branches?
No, and for a fundamental reason: they would be the same references and would trample each other. If the server's refs/heads/main were copied over your refs/heads/main, every fetch would destroy your unpushed local work. The separate namespace is what allows three things to coexist:
- Your
main, which you control and where you commit. - Your photograph of their
main(origin/main), which Git updates. - Their
main, which is on another machine.
It is also what makes it possible for Git to tell you "you are 2 commits ahead": it holds the two references separately and can compare them.
The leading +
The + modifier means "update this reference even when the change is not a fast-forward".
Remember from module 3 that a fast-forward is a clean advance: the new value is a descendant of the old one. Without the +, Git would refuse to update origin/main if somebody had rewritten history on the server (with a push --force, say), because the new commit would not be a descendant of the one you had recorded.
For remote references we want the +: their job is to faithfully reflect the server's state, whatever it happens to be. If the server changed in a strange way, your photograph must show that strange state; you can then decide what to do with your local branches. Protection against rewrites applies in the other direction, when pushing, and it is the subject of lesson 04-05.
Custom refspecs
The default refspec brings all the branches, but it can be narrowed down. If a repository has hundreds of branches and you only care about main:
No wildcard: that branch only. This is in fact what git clone --single-branch does internally, as mentioned in lesson 02-02.
You can stack several lines to bring in several groups:
git config --add remote.origin.fetch '+refs/heads/main:refs/remotes/origin/main'
git config --add remote.origin.fetch '+refs/heads/release/*:refs/remotes/origin/release/*'[remote "origin"]
url = https://git.example.com/team/task-manager.git
fetch = +refs/heads/main:refs/remotes/origin/main
fetch = +refs/heads/release/*:refs/remotes/origin/release/*Now git fetch origin will bring main and every release branch, and ignore the rest. In a corporate repository with seven hundred open branches, that is the difference between a two-second fetch and a thirty-second one.
The same concept comes back when pushing. When you write git push origin main:main in lesson 04-05, you will be supplying a refspec by hand, in the same source:destination syntax. The consistency is complete: the two sides of : always mean the same thing, "from here" and "to here".
- Renaming, removing and changing the URL
Three maintenance operations, all local, all instant and none of them dangerous.
Renaming
central https://git.example.com/team/task-manager.git (fetch) central https://git.example.com/team/task-manager.git (push)
And this is not just a change of label. Renaming a remote forces Git to rewrite everything that depended on the name:
- The remote references move from
refs/remotes/origin/*torefs/remotes/central/*. Yourorigin/mainbecomescentral/main. - The refspec is updated to point at the new namespace.
- The branches' tracking configuration (
branch.main.remote) is rewritten so thatmainkeeps pointing at the right remote.
remote.central.url https://git.example.com/team/task-manager.git remote.central.fetch +refs/heads/*:refs/remotes/central/* branch.main.remote central branch.main.merge refs/heads/main
All consistent. Git has done the complete job. Let us go back to the conventional name:
Removing
Removing a remote deletes its section from .git/config, all its remote references (refs/remotes/<name>/*) and the tracking configuration of the branches that used it.
What it does not delete: not a single commit. The objects stay in .git/objects/. If you remove a remote by mistake, just add it again and run fetch; the only thing you lose is time.
Changing the URL
This is the most frequent case in real life: the company migrates servers, or you move from HTTPS to SSH after setting up your keys.
git remote set-url origin [email protected]:team/task-manager.git
git remote -vorigin [email protected]:team/task-manager.git (fetch) origin [email protected]:team/task-manager.git (push)
Same repository, same history, same references: only the route to get there changes. Nothing else is affected, because a commit's identity lies in its hash, not in the URL it arrived from.
This is where that idea from lesson 04-01 becomes tangible: the "official" repository is official by convention. Migrating a whole team to another server is one set-url per person.
| Command | What it touches | Needs network |
|---|---|---|
git remote add |
Creates the section in .git/config |
No |
git remote -v |
Only reads .git/config |
No |
git remote show <n> |
Reads config and asks the server | Yes |
git remote rename |
Config + remote references + tracking | No |
git remote remove |
Deletes config + remote references + tracking | No |
git remote set-url |
The URL only | No |
git remote prune <n> |
Deletes references to branches that no longer exist | Yes |
set-url --push: read from one place, write to another
set-url --push: read from one place, write to anotherA twist that explains why git remote -v distinguishes (fetch) from (push):
git remote set-url --push origin [email protected]:team/task-manager.git
git remote -vorigin https://git.example.com/team/task-manager.git (fetch) origin [email protected]:team/task-manager.git (push)
Ana now reads over HTTPS and writes over SSH. Both paths lead to the same repository, but by different routes.
What is this good for in practice?
- Reading without credentials, writing with them. A public repository is cloned over HTTPS without authenticating; pushing requires an identity, and SSH supplies it without typing anything (lesson 04-03).
- Corporate firewalls. On some networks only port 443 for HTTPS is open outbound, but pushing goes by another route.
- Read-only mirrors. Read from a fast, nearby replica, always write to the original.
- Preventing accidental pushes. There is a well-known trick: point the
pushURL at something invalid so that no accidental push ever gets through.
fatal: 'DO_NOT_PUSH' does not appear to be a git repository fatal: Could not read from remote repository.
Good insurance when you clone somebody else's project purely to read it.
To undo any of these settings:
And the related options, to round out the picture:
# Add an extra URL (pushes to both: handy for mirrors)
git remote set-url --add --push origin [email protected]:team/task-manager.git
# See every URL of every remote, including the multiple ones
git remote -vWith two push URLs, a single git push sends to both places. It is the simplest way to keep a mirror in sync without external tools.
- Working with several remotes
A repository can have as many remotes as you like, and that is no oddity: it is the norm as soon as a project grows a little.
Suppose Bruno also keeps a personal copy of the project and the team deploys by pushing to a staging server:
git remote add personal [email protected]:bruno/task-manager.git
git remote add staging [email protected]:apps/task-manager.git
git remote -vorigin https://git.example.com/team/task-manager.git (fetch) origin https://git.example.com/team/task-manager.git (push) personal [email protected]:bruno/task-manager.git (fetch) personal [email protected]:bruno/task-manager.git (push) staging [email protected]:apps/task-manager.git (fetch) staging [email protected]:apps/task-manager.git (push)
Each remote has its own reference space, completely separate:
origin/HEAD -> origin/main origin/main origin/docs/update-notes personal/main personal/experiment/pwa staging/main
Now origin/main, personal/main and staging/main are three different references that can point at three different commits. And you can compare them like any other reference:
# What does my personal copy have that the team repository does not?
git log --oneline origin/main..personal/main
# Is staging behind the team?
git log --oneline staging/main..origin/main
# Bring a branch over from my personal copy
git switch -c experiment/pwa personal/experiment/pwaWhat having several is good for
| Situation | Typical setup |
|---|---|
| Contributing to somebody else's project | origin = your personal copy; upstream = the original project (module 7) |
Deploying by push |
origin = code; production = deployment server (module 10) |
| Mirror or backup | origin = the main one; mirror = the replica |
| Server migration | origin = the old one; new = the destination, while the transition lasts |
| Direct exchange between colleagues | origin = server; bruno = his laptop, for occasional four-handed work |
That last case illustrates what we were saying in 04-01 about Git's distributed nature. If Ana and Bruno are sitting in the same room working together:
# Ana registers Bruno's repository, reachable over SSH on the local network
git remote add bruno [email protected]:/home/bruno/projects/task-manager
git fetch bruno
git log --oneline bruno/feature/new-ideaNo server in the middle, nothing published. It is Git doing exactly what it was designed for.
And one useful command when you have several:
init versus clone: two ways of having a remote
init versus clone: two ways of having a remoteWe close with the comparison that makes sense of the whole lesson, because it is the difference between Ana's path and Bruno's.
Repository from git init (Ana) |
Repository from git clone (Bruno) |
|
|---|---|---|
| Remote at the start | None | origin, automatically |
Must you run remote add |
Yes | No |
| Remote references at the start | None | All the server's |
| Local branch to start from | Whichever you create | The server's default branch |
| Tracking configured | No: you have to set it up | Yes, automatically |
| First push | git push -u origin main |
Plain git push |
| Starting history | Yours | The server's |
flowchart TB
Q{"Does the project already<br/>exist on a server?"}
Q -->|"No: I am starting it"| I["git init<br/>work, commit<br/><b>git remote add origin URL</b><br/>git push -u origin main"]
Q -->|"Yes: I am joining"| C["git clone URL<br/><b>origin already registered</b><br/>tracking already configured<br/>get to work"]
I --> F["Repository with a remote<br/>and tracking"]
C --> F
Both paths arrive at the same place. clone is nothing more than init + remote add + fetch + switch, packed into a single command, just as we broke it down in lesson 02-02. You now have the loose pieces and can assemble them by hand:
# A "manual" clone, step by step, equivalent to git clone
mkdir task-manager && cd task-manager
git init
git remote add origin https://git.example.com/team/task-manager.git
git fetch origin
git switch mainThat last git switch main deserves a note: even though no local branch called main exists yet, Git sees that origin/main does, works out what you mean and creates the local branch with its tracking already configured. It is called DWIM (Do What I Mean) and it is the subject of lesson 04-06.
Ana is ready
Ana's repository now has its remote:
origin https://git.example.com/team/task-manager.git (fetch) origin https://git.example.com/team/task-manager.git (push)
But she has not pushed anything yet. And if she tried right now against a real server over HTTPS, she would run into this:
A question she cannot answer, because her web-interface password probably will not do. Authentication has to be sorted out before pushing, and that is exactly the subject of the next lesson.
Common Mistakes and Tips
Mistake 1: believing that git remote add validates the URL. It does not: it only writes to a file. You can register https://this.does.not.exist/nothing.git without a single warning. The first real check comes with git fetch, git push or git ls-remote.
Mistake 2: error: remote origin already exists. This happens when you try to add a remote that is already there, very typical in cloned repositories where origin comes preset. If what you want is to change its address, the command is git remote set-url origin <url>, not add.
Mistake 3: confusing git remote -v with git remote show. The first reads your configuration and works offline; the second asks the server and can fail because of the network or credentials. To diagnose "which URL do I have configured?" use -v; for "what is on the server right now?", show.
Mistake 4: not understanding the refspec, and therefore not understanding why origin/main is not a branch. The line +refs/heads/*:refs/remotes/origin/* says literally where the remote's references are stored: in a namespace separate from your branches. Without that separation, every fetch would flatten your work.
Mistake 5: trying to git push to a non-bare remote. If you set up the server yourself with git init instead of git init --bare, the push will be rejected with branch is currently checked out. Remember lesson 04-01: repositories that receive pushes must be bare.
Mistake 6: letting dead remotes pile up. After a server migration, plenty of people add the new one and leave the old one in place. Months later, git fetch --all takes forever waiting for a switched-off server to time out. Clean up with git remote remove.
Tip 1: check a freshly added remote with git ls-remote. It is the cheapest way to verify that the URL, the network and the credentials work, without downloading any objects:
If it returns the server's list of references, everything is in order. If it fails, you get the specific error before having invested anything.
Tip 2: always use origin for the main repository. The convention is so entrenched that any documentation, script or colleague will assume it. Save your creativity for the secondary remotes.
Tip 3: build git remote -v into your arrival routine for a repository. That command and git log --oneline --graph -10 give you the complete map of where you are in five seconds.
Exercises
Exercise 1: publishing an existing project
Simulate Ana's case from start to finish, locally:
- Create a normal repository with two or three commits and one extra branch.
- Check that it has no remote.
- Create a bare repository to act as the server.
- Register it as
origin. - Show the contents of
.git/configbefore and after, pointing out exactly which lines the command added. - Verify with
git ls-remotethat the remote responds, and explain what it returns while the server is empty.
Exercise 2: decoding refspecs
Translate each of these refspecs into plain English and say what a git fetch would do with each one:
+refs/heads/*:refs/remotes/origin/*+refs/heads/main:refs/remotes/origin/mainrefs/heads/main:refs/remotes/origin/main(without the+)+refs/heads/release/*:refs/remotes/origin/release/*+refs/tags/*:refs/tags/*
Then configure number 4 in a test repository and demonstrate with commands that a fetch no longer brings branches that do not start with release/.
Exercise 3: two remotes and a comparison
Set up this situation and solve it with commands:
- A bare
team.gitand another barepersonal.git. - A working repository with both registered as
originandpersonal. - Push a different commit to each one, so that their histories diverge.
- Answer: which commits does
personalhave thatorigindoes not? And the other way round? - Configure the repository to read from
originbut push topersonal, and demonstrate that it works.
Solutions
Solution 1:
mkdir -p /tmp/ex1 && cd /tmp/ex1
# 1. Repository with a history
git init -b main project
cd project
echo "<h1>Task manager</h1>" > index.html
git add . && git commit -m "Add initial task manager structure"
echo "body { font-family: sans-serif; }" > styles.css
git add . && git commit -m "Add base styles for the list"
git switch -c docs/update-notes
echo "# Task manager" > README.md
git add . && git commit -m "Add the project README"
git switch main# 3. The server
git init --bare /tmp/ex1/server.git
# 4. Register it
git remote add origin /tmp/ex1/server.git[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = /tmp/ex1/server.git
fetch = +refs/heads/*:refs/remotes/origin/*The two new lines are the [remote "origin"] section with its url and its fetch. Nothing more. No references, no objects, no connection.
The absence of output is the correct answer: the server replied and its list of references is empty, because the bare repository does not have a single commit yet. What matters is that the exit code is 0: it means Git got there and was able to talk. Compare it with a bad URL:
Solution 2:
-
+refs/heads/*:refs/remotes/origin/*— "Bring every branch from the remote and store them underrefs/remotes/origin/, forcing the update even when it is not a clean advance." This is the default behaviour of any clone. -
+refs/heads/main:refs/remotes/origin/main— "Bring only themainbranch and store it asorigin/main." The server's other branches are ignored entirely; they will not even show up ingit branch -r. Equivalent to--single-branch. -
refs/heads/main:refs/remotes/origin/main— The same, but without the+: if history were rewritten on the server'smain, thefetchwould fail instead of updating the reference, because the new value would not be a descendant of the old one. You rarely want this on a remote reference, whose job is to mirror the server as it is. -
+refs/heads/release/*:refs/remotes/origin/release/*— "Bring only the branches whose name starts withrelease/and keep them with the same structure underorigin/release/." Useful in huge repositories where only the release branches matter to you. -
+refs/tags/*:refs/tags/*— "Bring every tag from the remote and store them as local tags, under the same name." Notice that here there is no namespace translation: source and destination are the same. Tags are not separated per remote the way branches are, and that is why a fetched tag is indistinguishable from one you created yourself. Tags are the subject of lesson 05-05.
Checking case 4:
cd /tmp/ex1/project
# Set the server up with several branches
git push origin main
git switch -c release/1.0 && git commit --allow-empty -m "Version 1.0" && git push origin release/1.0
git switch -c release/1.1 && git commit --allow-empty -m "Version 1.1" && git push origin release/1.1
git switch main
git push origin docs/update-notes
# A fresh clone with a restricted refspec
cd /tmp/ex1
git clone server.git restricted
cd restricted
git config remote.origin.fetch '+refs/heads/release/*:refs/remotes/origin/release/*'
# Delete the references it already had, to start from scratch
git branch -r | grep -v HEAD | sed 's/^ *//' | xargs -r -n1 git branch -rd
git fetch origin
git branch -rorigin/main and origin/docs/update-notes do not appear. The restricted refspec has left them out: Git did not even bother to fetch them.
Solution 3:
mkdir -p /tmp/ex3 && cd /tmp/ex3
# 1. The two servers
git init --bare team.git
git init --bare personal.git
# 2. The working repository with both remotes
git init -b main work
cd work
echo "<h1>Task manager</h1>" > index.html
git add . && git commit -m "Add initial task manager structure"
git remote add origin /tmp/ex3/team.git
git remote add personal /tmp/ex3/personal.git
git remote -vorigin /tmp/ex3/team.git (fetch) origin /tmp/ex3/team.git (push) personal /tmp/ex3/personal.git (fetch) personal /tmp/ex3/personal.git (push)
# 3. A common base and then divergence
git push origin main
git push personal main
echo "console.log('team');" > app.js
git add . && git commit -m "Add the skeleton of the task logic"
git push origin main
git reset --hard HEAD~1
echo "console.log('personal');" > experiment.js
git add . && git commit -m "Try out a loose idea"
git push personal main
git fetch --allEach one has a commit the other does not: they have diverged from the common initial commit. You can see it all at once:
One exclusive commit on each side. This three-dot syntax and how to read it are the subject of lesson 04-06.
# 5. Read from origin, push to personal
git remote set-url --push origin /tmp/ex3/personal.git
git remote -vorigin /tmp/ex3/team.git (fetch) origin /tmp/ex3/personal.git (push) personal /tmp/ex3/personal.git (fetch) personal /tmp/ex3/personal.git (push)
# Demonstration: a fetch from origin brings the team's work…
git fetch origin
git log --oneline -1 origin/main# …but a push to origin writes into personal
git commit --allow-empty -m "Check the push destination"
git push origin main
git --git-dir=/tmp/ex3/personal.git log --oneline -1 mainConfirmed: origin reads from team.git and writes to personal.git. It is the mechanism used by people who clone somebody else's project read-only and push their changes to their own copy.
Conclusion
In this lesson Ana took the step she had been putting off for three modules: connecting her repository to the world. What you have learnt:
git remote add <name> <url>registers a remote, and it is a purely local operation: it writes three lines into.git/config, does not validate the URL and does not touch the network. The first real check comes withfetch,pushorgit ls-remote.git remote -vlists the remotes with their read and write URLs, offline.git remote show <name>does query the server and gives you the full X-ray: default branch, remote branches with their status (tracked,new,stale) and thepullandpushconfiguration.- Everything lives in
.git/config, in a[remote "origin"]section with two keys:urlandfetch. - The refspec
+refs/heads/*:refs/remotes/origin/*says: "bring every branch from the remote and store them underrefs/remotes/origin/, forcing the update". The+allows non-fast-forward updates; the:separates source from destination. That is where the slash inorigin/maincomes from: it is the real path of the file. And that namespace separation is what stops afetchfrom flattening your local branches. - Renaming, removing and changing the URL are local, harmless operations. Renaming also moves the remote references and rewrites the tracking configuration; removing deletes no commit.
set-url --pushlets you read from one place and write to another, or block pushes by pointing at an invalid URL.- Several remotes coexist without any trouble, each with its own reference space, and they are compared with one another like any other commit name.
initversusclone: the first requires registering the remote by hand; the second inherits it. Both arrive at the same place, becausecloneisinit+remote add+fetch+switch.
What comes next
Ana's remote is configured, but if she tried to push right now she would hit an uncomfortable question: Username for 'https://git.example.com':. Registering a URL does not grant permission to write to it.
In lesson 04-03: Authenticating with Remote Repositories we take down that wall. We will see why cloning a public repository asks for nothing while pushing does, what a personal access token is and why it replaced the password, how to generate and use an SSH key with ssh-keygen -t ed25519, and how each operating system stores credentials so that you do not have to type them forty times a day. It matters especially for Carla, who is about to join from Windows 11 and needs this sorted out before writing her first line of code.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
