Collaborating - Remotes, Clone, Push, and Pull
Context
A remote is a copy of your repository living somewhere other than your machine. Bringing that copy down for the first time, sending your new snapshots up to it, and pulling down snapshots someone else sent are three named operations, and each one moves history in exactly one direction.
This is what makes a project survive its author's laptop and what makes more than one person able to work on it. It is also how the same code reaches a machine you do not sit in front of, which is the normal arrangement in machine learning, where you write on a laptop and train on a rented machine with a graphics card, and getting your code onto that machine is a pull rather than a copied folder.
History that exists in only one place is one failed disk away from being gone, and sending work up without first taking down what changed underneath you is refused outright, which is a mercy rather than an obstacle. Everything in this section so far happened inside one machine, and this lesson is where the repository stops being local.
Goal
- Connect your machine to GitHub over SSH once, and prove the connection works before anything depends on it.
- Add a remote to a local repository and list it, naming the address your commits will travel to and from.
- Clone a remote repository to a second directory and confirm the
originremote came with it. - Push local commits up to a remote and verify they arrived, then pull commits made elsewhere down and confirm your history advanced.
- Trace any sync operation as a direction, up (push) or down (clone and pull), and state exactly what each one moves.
Prerequisites
Neither resource below is required, the lesson stands on its own. They are here if you would rather see the picture before you build it.
- YouTube - "Complete Git and GitHub Tutorial" (Kunal Kushwaha), the chapters from 25:26 to 28:43 and the clone at 39:00 only. A repository created on GitHub, connected to a local one as its remote and pushed to, then a project cloned into a new folder, so you see commits travel up and down before you move them yourself.
- GitHub Docs - "Generating a new SSH key and adding it to the ssh-agent" and "Adding a new SSH key to your GitHub account". The openly published version of the setup below, useful if a step on your machine looks different from the one printed here.
Table of Contents
- Remote
- Clone
- Push
- Pull
Theory
CONNECT THIS MACHINE TO GITHUB, OVER SSH, ONCE. Every example in this bootcamp uses the account name goku and the address goku@gmail.com. Substitute your own wherever you see them.
Git reaches GitHub with an SSH key pair. SSH is the standard protocol for a secure connection to another machine over the network, the same network layer Section 2 introduced, and a key pair is two files generated together and mathematically linked. The private key stays on your disk forever and is the thing that proves you are you. The public key is the half you hand to GitHub, and it can prove nothing on its own, which is precisely why handing it over is safe. Once GitHub holds the public half, every push, pull and clone authenticates silently, with no secret typed and nothing to expire.
Notice what this means for the user.name and user.email you set in Lesson 0.3.1. They are labels stamped onto commits, not credentials. Anyone can set any string there and nothing checks them. Authentication is entirely the key's job, so the label says who wrote a commit and the key decides whether the push is allowed. Confusing the two is why people set their email correctly, watch a push get denied, and go looking in the wrong place for the fault.
Step 1 - Create the account. On the GitHub website, sign up. Use the same email address you set as user.email, goku@gmail.com here, so the commits you publish are attached to that profile instead of showing up as an unrecognized author. Your account name becomes the middle of every address you type, git@github.com:goku/repo-name.git, so choose one you are content to keep.
Step 2 - Look for a key you already have, before making a new one.
# 'ls -al' lists everything, hidden entries included, in long form.
ls -al ~/.ssh # look for a pair named 'id_ed25519' and 'id_ed25519.pub'~/.ssh is where SSH keeps keys. On a machine that has never used it, the directory does not exist at all, so ls reports No such file or directory, which is expected rather than a problem, and Step 3 creates it. If a pair named id_ed25519 and id_ed25519.pub is already there, you have a key, so skip Step 3 and go on from Step 4.
Step 3 - Generate the pair.
# '-t' picks the key type, '-C' attaches a comment that labels the key.
ssh-keygen -t ed25519 -C "goku@gmail.com"
# Prompt 1, 'file in which to save the key': press Enter for the default,
# '/home/goku/.ssh/id_ed25519', which is where Git and SSH already look.
# Prompt 2, 'Enter passphrase': type one and remember it, or press Enter
# twice for none. It encrypts the PRIVATE key file on disk.Two files land in ~/.ssh. id_ed25519, with no extension, is the private half, and it never leaves the machine, never gets emailed, never gets committed. id_ed25519.pub is the public half, a single line of text, and that line is what GitHub receives. The ed25519 type is modern, short and fast, and it is the right default. The -C comment is written inside the public key purely so you can tell one key from another later, it takes no part in authentication. The passphrase encrypts the private key while it sits on disk, so a stolen laptop does not hand over your identity along with the hardware, and Step 4 is what saves you from typing it on every single push.
Step 4 - Load the key into the agent.
# 'ssh-agent' is a small program that holds unlocked keys in memory for a session.
eval "$(ssh-agent -s)" # starts it and prints Agent pid 1234
# 'ssh-add' hands the PRIVATE key to the running agent, asking the passphrase once.
ssh-add ~/.ssh/id_ed25519 # Identity added: /home/goku/.ssh/id_ed25519 (goku@gmail.com)Two things happen inside eval "$(ssh-agent -s)". ssh-agent -s starts the agent and then prints a couple of shell variable assignments rather than doing anything else, and those variables are how any later command finds the running agent. The $( ) captures that printed text, and eval executes it as if you had typed it, which is what puts the variables into your shell. Drop the eval and the agent still starts, but your shell never learns where it is, so ssh-add fails with Could not open a connection to your authentication agent. The agent keeps running after the terminal closes, but the variables that point at it die with that shell, so a new terminal needs both lines again, and each run of the first line starts one more agent. Run them in the terminal you push from, and keep them out of ~/.bashrc, the startup file Section 2 covered, where every new shell would start one more agent and ask for the passphrase again.
Step 5 - Give GitHub the public half.
# 'cat' prints the PUBLIC key, one long line that begins 'ssh-ed25519'.
cat ~/.ssh/id_ed25519.pub # copy the ENTIRE lineOn the website, go to Settings → SSH and GPG keys → New SSH key. The title is a note to yourself, "WSL laptop" for instance, so that you can later delete the right key when a machine is retired. Leave the key type as Authentication Key, paste the whole line into the key box with no line breaks introduced and no characters trimmed, and save. GitHub now knows that whoever can prove they hold the matching private key is you. Upload the .pub file only. The other file is the one that has to stay secret, and nothing on any website ever needs it.
Step 6 - Prove the connection works before you depend on it.
# '-T' asks for an authentication test instead of an interactive shell.
ssh -T git@github.com # first run: match the fingerprint below, then answer 'yes'
# Hi goku! You've successfully authenticated, but GitHub does not provide shell access.The first connection prints the server's fingerprint and asks whether to continue. GitHub publishes its ED25519 fingerprint as SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU, so compare the ED25519 key fingerprint is line against it and answer yes only when the two match. Answering yes records it in ~/.ssh/known_hosts, so the question is asked once per machine and any later change to that fingerprint is treated as suspicious rather than waved through. The greeting names your account, which is the proof that the key reached GitHub and matched. The "does not provide shell access" half of that same line is not a failure, GitHub deliberately refuses interactive logins, and Git never wanted one. If you get Permission denied (publickey) instead, GitHub accepted none of the keys this machine offered, which almost always means the public half never landed, so redo Step 5, and ssh -vT git@github.com prints the negotiation step by step when you need to see which key was actually offered.
That greeting is where the setup ends. Nothing has been published, no repository exists on the server yet, and no address has been typed into Git. Every one of those is a subtitle below.
WHAT TO AVOID IN THIS SETUP:
- NEVER share, paste, email or upload the PRIVATE key,
~/.ssh/id_ed25519with no.pubon the end. Only the.pubhalf goes to GitHub. Anyone holding the private half can act as you until you delete that key from the account and generate a fresh pair. - NEVER let a key into a repository. Keys live in
~/.sshand never inside a project folder. A private key that reaches a public repository is compromised the instant it lands, not when somebody eventually notices it. - DON'T answer
ywhenssh-keygenreports that/home/goku/.ssh/id_ed25519already exists and asksOverwrite (y/n)?. Pressing Enter there keeps the old key, andyreplaces it, which breaks every service that trusted it. - DON'T paste a public key that has picked up a line break or lost characters. It must arrive as one unbroken line, exactly as
catprinted it, and a key damaged anywhere before its closing comment never authenticates. - DON'T lose your passphrase. There is no recovery for it, and the only way forward is a new key pair and a fresh upload.
Remote
What. A remote is a copy of the repository hosted somewhere else, typically on a server like GitHub. Your local Git refers to it by a short name (almost always origin) paired with an address (where it actually lives). The name is an alias so you never type the address after the first setup.
Why. A repository on your machine alone is fragile and unshareable: if the disk dies it is gone, and no one else can see it. A remote fixes both at once, it is an off-machine copy and the single point every other copy syncs against. This is the extension of the repository idea from Lesson 0.3.1: the same project, the same commit history, now existing in two places that must be kept aligned.
When. You add a remote when you want to publish a local project for the first time, back it up off your machine, or let others contribute. You receive a remote automatically when you clone (next subtitle). You inspect remotes whenever you are unsure where a push or pull will actually go.
A remote has to exist before you can name it. On the GitHub website, use New repository, name it model-training, and leave it completely empty, no README, no .gitignore, no license. An empty repository accepts your first upload, while a pre-filled one already holds a commit yours knows nothing about and rejects it, which is a refusal you will meet on purpose in the Pull subtitle.
# The laptop copy: a local repository
# '-q' keeps init quiet
cd ~ && mkdir model-training && cd model-training && git init -q
echo "lr = 0.01" > config.txt # the training configuration this project is about
# one LOCAL snapshot
git add config.txt && git commit -q -m "Add training config"
# 'git remote -v' lists the remotes this repository knows, with their addresses.
git remote -v # prints NOTHING, this repository is purely localNow record where the empty GitHub repository lives:
# 'remote add' records a name and an address. 'origin' is the conventional name.
# sends nothing
git remote add origin git@github.com:goku/model-training.git
# '-v' prints the address behind the name, once for fetching and once for pushing.
git remote -v
# origin git@github.com:goku/model-training.git (fetch)
# origin git@github.com:goku/model-training.git (push)Note the shape of the address. git@github.com:goku/model-training.git is the SSH form, and it is the form your key authenticates. An address beginning https:// is a different door that expects a token (a long secret string GitHub issues in place of a password), so mixing the two produces a password prompt on a machine you were certain was set up.
DON'T:
- DON'T confuse the remote's name with its address.
originis an alias, the address is the real destination. Renaming or removingorigindoes not touch the server, it only changes what your local repository calls it. - DON'T add a remote and assume your commit is now safe there.
git remote addrecords where, it moves zero commits, and the repository on GitHub is still empty. The next subtitle proves it.
Clone
What. Clone creates a local copy of a remote repository. git clone <address> brings down the whole repository, every file and the full commit history, into a new folder, and automatically sets that source up as the origin remote. It is the down direction used to start from nothing.
Why. Clone is how you get onto a project that already exists somewhere else: an open-source repository, a teammate's work, or your own project on a new machine. You do not rebuild the history by hand, clone brings the whole .git with it, so the moment it finishes you have a complete working repository with its past intact and its remote already wired up. It is the single command that turns "a project on a server" into "a project I can run and edit here".
When. You clone exactly once per copy, the first time you bring a remote project onto a machine. After that you use pull, not clone, to get updates. Clone when joining a project or setting up a new machine, pull to stay current on a copy you already have.
The second copy in this lesson stands in for the training machine, the rented box with the graphics card that runs the code your laptop writes. It lives in a second directory on the same computer, which changes nothing about the mechanics, because Git does not care whether the other copy is across a room or across a network.
# Clone the remote into a second directory. The last argument names the new folder.
cd ~ && git clone git@github.com:goku/model-training.git trainbox
# Cloning into 'trainbox'...
# warning: You appear to have cloned an empty repository.
# The clone is a real repository, with 'origin' already pointing back where it came from.
cd trainbox && git remote -v # the same two lines the laptop copy printed
# And it is EMPTY. The laptop's commit never left the laptop.
git log --oneline # fatal: your current branch 'main' does not have any commits yet
ls -a # . .. .git, not a single fileThat warning is the proof the previous subtitle promised. git remote add recorded an address and nothing else, so the server holds a repository with no commits, and a clone of it is a repository with no commits. Clone copied everything that was there, which was the shape of a repository and none of its contents. The commit sitting on the laptop reaches this directory only after it reaches the server, and that is the next subtitle.
DON'T:
- DON'T clone the same project repeatedly to get updates. Clone is for the first copy, and once you have it,
git pullbrings new commits into the folder you already have. Re-cloning makes a second, disconnected copy. - DON'T clone inside an existing repository. Run
git clonefrom a plain folder, because cloning into another repository's working tree nests one repository inside another and leaves it unclear which.gitis in charge.
Push
What. Push sends your local commits up to the remote. git push origin main takes the commits you have made locally on the main branch and uploads them to origin, so the remote's history advances to match yours.
Why. Committing only records a snapshot in your local .git, and the remote knows nothing about it until you push. Push is what publishes your work, backs it up off-machine, and makes it visible to every other copy. Without it, your repository and its remote silently drift apart, your machine holding commits the server has never seen, which is exactly the state the empty clone just showed you.
When. You push after committing, when your local work is ready to share or simply to back up, typically at the end of a chunk of work. You push for the first time with -u to link your branch to the remote's, and after that a bare git push knows where to go.
# Back to the laptop copy, where the commit is.
cd ~/model-training
# '-u' records 'origin/main' as this branch's upstream, so later pushes need no names.
git push -u origin main
# ...
# To github.com:goku/model-training.git
# * [new branch] main -> main
# branch 'main' set up to track 'origin/main'.branch 'main' set up to track 'origin/main' is the line to remember. That pairing is stored in .git/config, and it is why every later git push and git pull in this repository works with no arguments at all. Without -u, Git knows the remote exists but not which remote branch yours corresponds to, and a bare git push stops with fatal: The current branch main has no upstream branch. Refresh the repository page on GitHub and config.txt and its commit are there.
# After the first '-u' push, syncing up is a single word.
echo "epochs = 10" >> config.txt # a second setting, appended
# another LOCAL snapshot
git add config.txt && git commit -q -m "Set epoch count"
git push # uploads to the remembered 'origin/main'
# ...
# To github.com:goku/model-training.git
# 5ad986d..07ec57a main -> main <- the ids are yoursDON'T:
- DON'T expect push to move uncommitted work. Push only transmits commits, and staged-but-uncommitted or unstaged changes stay on your machine. Commit first, then push.
- DON'T push to a remote whose recent commits you do not have. If another copy pushed since your last sync, Git rejects your push, and the fix is the next subtitle, never a flag that forces it through.
Pull
What. Pull brings commits from the remote down into your local repository and integrates them. git pull does two things in one step: it fetches the new commits from origin and then merges them into your current branch, the same merge Lesson 0.3.3 taught, so your local history advances to match the remote's.
Why. A remote moves on without you, other copies push, or you push from another machine, and your local copy goes stale. Pull is how you catch up: it is the down counterpart to push, the operation that folds the remote's new commits into yours so the two copies converge again. Collaboration is this rhythm repeated, pull down what changed, do your work, push up what you added. Pulling before you start work is what keeps your push from being rejected for being out of date.
When. You pull before beginning a session of work, to start from the latest, and any time you know or suspect the remote has commits you do not. You especially pull right after a push is rejected: that rejection means the remote advanced, and pull is the fix that lets you push afterward.
# The training machine, which has been empty all along, catches up.
cd ~/trainbox
git pull
# ...
# From github.com:goku/model-training
# * [new branch] main -> origin/main
git log --oneline # both commits, in the order the laptop made them
# 07ec57a (HEAD -> main, origin/main) Set epoch count
# 5ad986d Add training config
cat config.txt # lr = 0.01 then epochs = 10, the file itself arrived with the history
# Nothing new on the remote, so pull brings nothing.
git pull # Already up to date.Already up to date. is pull succeeding, having looked at the remote and confirmed this copy is not behind. It is the answer you want to see at the start of every working session.
The rhythm breaks in one specific situation, and you meet it here on purpose. The training machine records something the laptop does not know about, the laptop records something the training machine does not know about and pushes first, and then the training machine tries to push.
# The training machine commits a note about itself. LOCAL only.
echo "gpu: A100" > machine.txt
git add machine.txt && git commit -q -m "Record the training machine"
# Meanwhile the laptop commits a different change to a different file, and pushes first.
cd ~/model-training
echo "batch_size = 32" >> config.txt
git add config.txt && git commit -q -m "Set batch size"
git push # accepted: the laptop was not behind
# Now the training machine tries. It is behind, so the server refuses.
cd ~/trainbox
git push
# ...
# ! [rejected] main -> main (fetch first)
# error: failed to push some refs to 'github.com:goku/model-training.git'
# hint: Updates were rejected because the remote contains work that you do not
# hint: have locally. This is usually caused by another repository pushing to
# hint: the same ref. If you want to integrate the remote changes, use
# hint: 'git pull' before pushing again.
# ...Read what the rejection actually says. It is not that your files collided, machine.txt and config.txt are different files that could not possibly conflict. It is that the remote contains work you do not have, so accepting your push would discard a commit that already exists there, and Git refuses to do that silently. The hint names the fix.
# Pull, as the hint says. Both sides have a commit the other lacks, and Git will not guess.
git pull
# ...
# hint: You have divergent branches and need to specify how to reconcile them.
# ...
# hint: git config pull.rebase false # merge
# ...
# fatal: Need to specify how to reconcile divergent branches.
# Tell THIS repository, once, to reconcile by merging. 'false' = merge, not rebase.
git config pull.rebase false
# Now the pull proceeds. '--no-edit' accepts Git's own merge message instead of opening an editor.
git pull --no-edit
# Merge made by the 'ort' strategy.
# config.txt | 1 +
# 1 file changed, 1 insertion(+)
# The two lines of work are joined by a merge commit, exactly as in Lesson 0.3.3.
git log --oneline --graph
# * f8ce8e8 (HEAD -> main) Merge branch 'main' of github.com:goku/model-training
# |\
# | * 88036e0 (origin/main) Set batch size
# * | 1197c83 Record the training machine
# |/
# * 07ec57a Set epoch count
# * 5ad986d Add training config
# The training machine's history now contains the laptop's commit, so the push is accepted.
git push # 88036e0..f8ce8e8 main -> mainGit calls that first state divergent branches: your local branch has a commit the remote does not, and the remote has a commit you do not, at the same time. Current Git refuses to pick between merging and rebasing (a different technique that rewrites your commits on top of the remote's) on your behalf, so it stops with that fatal line instead of guessing. git config pull.rebase false is a one-time, per-repository setting, and choosing false is deliberate: it produces the same fetch-then-merge behavior, and the same kind of two-parent merge commit, that Lesson 0.3.3 already taught. A pull that lands as a fast-forward never asks the question, which is why the first git pull in this subtitle sailed through.
DON'T:
- DON'T forget to pull before pushing when another copy may have contributed. Pushing onto a remote that has advanced past you is rejected, so pull first so your history includes theirs, then push.
- DON'T pull with local uncommitted edits to files the incoming commits also touch. Pull can refuse or collide, so commit your local changes first so the merge has a clean snapshot to integrate into.
- DON'T read
Need to specify how to reconcile divergent branchesas a broken command. It is current Git behavior, andgit config pull.rebase falseonce per repository is the answer this lesson uses.
Additional - one picture: up vs. down
Every command in this lesson is a direction. Down brings the remote's commits to you: clone the first time (the whole repository, empty or not), pull every time after (only the new commits). Up sends your commits to the remote: push. origin is the name of the remote, main is the branch you sync, and git remote -v is how you check where up and down actually point. Any project you put on GitHub goes online and stays in sync through exactly this machinery: clone it once, then loop pull → commit → push for as long as the project lives.
Lab Exercises
Lab Exercise 1: Wire Up a Remote and Prove It Moved Nothing
Create a fresh local repository with one commit, create an empty repository named ml-pipeline on GitHub, then attach it as origin and list your remotes to confirm the address. Then clone that same remote into a second directory and read what Git prints. In comments, state how many commits git remote add moved to the server, which direction each listed address serves, and what the clone's warning proves.
Lab Exercise 2: Push Up, Pull Down, and Account for What Did Not Travel
Part A: From the original repository in Exercise 1, push your commit up to origin/main for the first time, then make a new commit and push again with no arguments. Before that second commit, also create a scratch file in the working tree that you do not stage.
Part B: In the clone, pull. Count the commits in the clone and compare against the original. Then list both working trees and account for anything present in one and absent in the other. In comments, label which command moved commits up, which brought them down, and state what a clone or a pull brings across besides the current files, and what it never brings.
Solution 1: Wire Up a Remote and Prove It Moved Nothing
# A fresh local repository with exactly one commit.
cd ~ && mkdir ml-pipeline && cd ml-pipeline && git init -q
echo "stage: load" > pipeline.txt # the first stage of a pipeline
# one LOCAL snapshot
git add pipeline.txt && git commit -q -m "Add load stage"
# Attach the EMPTY GitHub repository as 'origin' (create it on the website first).
git remote add origin git@github.com:goku/ml-pipeline.git
git remote -v # the address twice, '(fetch)' for down and '(push)' for up
# Clone the same remote into a second directory.
cd ~ && git clone git@github.com:goku/ml-pipeline.git ml-pipeline-clone
# ...
# warning: You appear to have cloned an empty repository.
cd ml-pipeline-clone && ls -a # . .. .git, nothing elsegit remote add moved zero commits, it stored an address. The (fetch) line is the address for the down direction and (push) for the up direction. The clone's warning proves it: the server holds an empty repository, because nothing has been pushed, and a clone of nothing is nothing.
DON'T:
- DON'T expect your commit to be on GitHub after
git remote add. Adding a remote is pure bookkeeping, and the repository on the server is still empty. - DON'T type the full address again after this. That is the whole point of the name
origin.
Solution 2: Push Up, Pull Down, and Account for What Did Not Travel
# PART A - the original pushes.
cd ~/ml-pipeline
git push -u origin main # UP: the commit, then branch 'main' set up to track 'origin/main'.
echo "tmp" > scratch.log # a working-tree file that is never staged
echo "stage: train" >> pipeline.txt
# LOCAL, only 'pipeline.txt'
git add pipeline.txt && git commit -q -m "Add train stage"
git push # UP, no arguments needed: <id>..<id> main -> main
git log --oneline | wc -l # 2
ls # pipeline.txt scratch.log# PART B - the clone pulls.
cd ~/ml-pipeline-clone
git pull # DOWN: * [new branch] main -> origin/main
git log --oneline | wc -l # 2, the same count as the original
ls # pipeline.txt, and no 'scratch.log'git push -u origin main and git push moved commits up, git pull brought them down. What came across is the repository: every commit, and the files as they stand in the newest one. What never came across is scratch.log, because it was never staged or committed, and Git moves commits, not folders. The same holds for ignored files, and for empty directories, which Git does not record at all.
DON'T:
- DON'T skip the
-uon the first push and then wonder why a baregit pushlater reports no upstream branch. The-uis what records the pairing. - DON'T expect a clone or a pull to deliver your untracked or ignored files. It delivers what was committed, which is the point of deciding carefully what to commit.
Theory Assessment
Conceptual reinforcement, not a coding section. All four questions with their hints first, then all four answers.
Q1: What wiring up a remote actually moves
State what git remote add origin <address> changes, and how many commits it uploads to the server. Then explain the relationship between origin and the address it points at, and why Git lets you use the name instead of the address afterwards.
Hint: Ask whether anything left your machine. For the second half, one of the two is a convenience and the other is the real destination.
Q2: Committed is not shared
A student commits their work, closes the laptop, and assumes it is now on GitHub. Explain why it is not, and name the missing command. Then describe the everyday collaboration loop using pull, commit and push, stating the direction of each.
Hint: Ask which repository a commit writes to. For the loop, one step goes down, one stays put, one goes up, and the order is not arbitrary.
Q3: Two ways to bring things down
Both git clone and git pull bring data down. Explain when you use each, and why you would not use clone to get updates. Then explain why cloning a repository straight after git remote add produced an empty directory, and what had to happen before a pull into that directory brought anything.
Hint: One of them starts something that does not exist yet, and the other advances something that does. For the second half, follow the commit: where was it, and which direction had it not yet traveled?
Q4: Two refusals, and what each one protects
Your git push fails with Updates were rejected because the remote contains work that you do not have locally. Explain the cause and the fix, and say what the message is not telling you. Then explain the difference between git fetch and git pull, and give one reason you would fetch rather than pull. Finally: a different push fails with Permission denied (publickey), and the student checks that user.email is set correctly and finds it is. Explain why that check was the wrong place to look, and say what the two refusals have in common and where they differ.
Hint: The first rejection is about history, not about your files colliding. For the last part, ask which of the two, the label or the key, the server ever reads.
Answer 1:
git remote add origin <address> records an address in your local repository's configuration, storing where the off-machine copy lives and naming that location origin. It uploads zero commits. It is pure bookkeeping, and afterwards the remote repository is exactly as empty or as full as it was. Commits leave your machine only when you git push.
origin is a name your local repository maps to an address, the real destination. The address is where commits actually travel, and the name is shorthand Git stores so you need not retype it, which is why git push origin main resolves origin automatically.
DON'T assume your work is backed up because a remote is configured. DON'T think renaming or removing origin affects the server, it only changes what your local repository calls it.
Answer 2:
A git commit records the snapshot in the local repository's .git only. The remote has no knowledge of it and learns of a commit exactly when you push. The laptop holds the work and GitHub does not, so the missing command is git push.
The loop is git pull (DOWN, to start current), then edit and git commit (LOCAL, recording in your own .git), then git push (UP, sending those commits out). Down to catch up, local to record, up to share. Pulling first is what keeps the eventual push from being rejected.
DON'T equate "committed" with "backed up" or "shared", commit is local and push is what sends it off the machine.
Answer 3:
git clone is used once, to create a brand-new local copy of a remote you do not have yet, wiring up origin for you. git pull is used repeatedly, to bring new commits down into a copy you already have and merge them into your current branch. Re-cloning for updates would create a second, separate folder rather than advancing the one you are working in.
The clone was empty because the commit was still only on the laptop. git remote add had given the laptop an address and moved nothing, so the server held a repository with no commits, and a clone copies what the server holds. The commit had to travel up first, by git push from the laptop, before a git pull in the clone had anything to bring down.
DON'T clone over and over for updates. DON'T expect a clone to contain a commit that was never pushed, no matter how many times it was committed.
Answer 4:
The push was rejected because the remote holds commits your local repository does not, so its history has advanced past yours. Completing the push would mean discarding those commits, and Git refuses. The fix is the down operation first: git pull to bring the remote's commits into your branch, then git push. What the message is not telling you is that your files collided. It is about the shape of the history, not the content of any file, and it appears even when the two sides changed entirely different files.
git fetch performs only the download half, bringing the remote's commits down and updating your origin/main reference without touching your working branch. git pull is a fetch followed by a merge into your current branch. You would fetch when you want to inspect what arrived, with git log origin/main, before deciding to integrate it, rather than merging blindly.
user.email is a label stamped onto commits, and the server never reads it to decide anything. Permission denied (publickey) is the SSH layer saying no key it recognizes was offered, so the fault is in the key, Step 5 of the setup, and the label could be anything at all without changing the outcome.
The through-line is that Git is protecting something different in each case. The rejected push protects history from being silently discarded. The denied key protects the server's data from an unidentified writer. Neither is an obstacle to work around, and the fix for the first is never to force it.
DON'T force a push to bypass a rejection, that risks discarding the commits you are missing. DON'T go looking at user.name or user.email when authentication fails, they are never consulted.
Assignment
Assignment: Resolve a Real Sync Conflict Between Two Collaborators
Simulate two collaborators with two clones of one remote. Create an empty repository named ml-report on GitHub, clone it as yourself, seed it with one commit and push, then clone it a second time as vegeta. From your clone, commit and push a change. In vegeta's clone, which has not pulled, commit a different change to a different file, then attempt to push and observe the rejection. Fetch to inspect what the remote has that vegeta's clone does not, then pull to integrate your commit, and finally push vegeta's commit successfully. In comments, explain the rejection, why the remote had to hold a commit before vegeta's clone was made, given that vegeta commits before ever pulling, and why fetch-then-inspect is safer than pulling blindly.
Hints:
- Edit different files in the two clones so the merge is clean, with no line-level conflict. The push rejection happens regardless, because vegeta's history is simply behind the remote.
- Sequence in vegeta's clone after the rejection:
git fetch origin→git log --oneline origin/main(inspect) →git config pull.rebase false(both sides have a commit the other lacks, so Git needs to be told once) →git pull --no-edit(integrate) →git push(now accepted).
Solution:
# Vegeta commits before ever pulling, so the remote must hold one commit BEFORE
# vegeta's clone is made. A clone of an EMPTY repository that commits first starts
# its own root commit, shares no ancestor with yours, and the pull below dies on
# 'fatal: refusing to merge unrelated histories'.
# your clone, DOWN, and the empty-repository warning is expected
cd ~ && git clone git@github.com:goku/ml-report.git ml-report
cd ml-report && echo "# ML Report" > report.md
git add report.md && git commit -q -m "Start the report"
git push -u origin main # UP: the remote now has a commit both clones descend from
# The second collaborator. A second clone on the same machine stands in for vegeta.
# DOWN
cd ~ && git clone git@github.com:goku/ml-report.git vegeta-ml-report
# YOU: commit to one file and push UP.
cd ~/ml-report
echo "accuracy: 0.91" > results.txt
git add results.txt && git commit -q -m "Add results"
git push # UP, accepted: the remote now has your commit
# VEGETA (has NOT pulled): commit to a DIFFERENT file, then try to push.
cd ~/vegeta-ml-report
# No '--global': vegeta's name and email go into THIS clone only, so the commit is vegeta's.
git config user.name vegeta
git config user.email vegeta@gmail.com
echo "notes from vegeta" > notes.txt
git add notes.txt && git commit -q -m "Add notes"
git push # ! [rejected] main -> main (fetch first), vegeta is behind
# Inspect before integrating: fetch downloads your commit WITHOUT merging it.
git fetch origin # DOWN, download only: <id>..<id> main -> origin/main
git log --oneline origin/main # your 'Add results' commit, waiting on the remote
git log --oneline # vegeta's own history, which still lacks it
# Vegeta has a commit the remote never saw, AND the remote has one vegeta never pulled.
# The histories have diverged, so tell this repository once how to reconcile.
git config pull.rebase false # merge, the strategy Lesson 0.3.3 taught
# Integrate, then the push is accepted.
git pull --no-edit # Merge made by the 'ort' strategy. Clean, because the files differ
git push # UP, accepted: the remote holds both commitsThe rejection happened because vegeta's history was behind, the remote held your commit that vegeta never integrated, and Git refuses to push on top of commits it would skip. The seed commit had to exist before vegeta's clone because vegeta commits before ever pulling: a clone of an empty repository that commits first invents its own first commit, and Git will not merge two histories that share no ancestor. git fetch downloaded your commit without touching vegeta's branch, so git log origin/main could show exactly what was about to be merged before anything changed, which is safer than a pull that merges whatever it finds.
DON'T assume a rejected push means a file conflict. The rejection is about being behind the remote, not about overlapping edits, and even unrelated files in different commits trigger it until you pull the missing commits in.
DON'T let a clone taken while the remote was still empty commit anything before it pulls the seed. Its first commit would be a second root commit, sharing no ancestor with anything pushed later.
Additional Resources
Optional, and nothing later depends on either. Worth a look only if this lesson left you curious.
- YouTube - "Git and GitHub for Beginners - Crash Course" (freeCodeCamp.org), the push chapters from 20:20 to 31:40 and the
git pullat 47:44. Reinforces the up/down directional model, pushing commits up to a real remote and pulling merged work back down. - Pro Git book (Scott Chacon and Ben Straub) - Chapter 2, "Working with Remotes". The canonical, openly licensed explanation of
remote,fetch,pushandpull.
Summary
This lesson is the point where a repository stops being a thing that exists on one disk and becomes something shared, backed up, and alive in more than one place. Everything you learned earlier about commits, staging a change and recording a snapshot, was preparation for the moment those snapshots needed to travel, and the one-time setup at the top is what lets them travel without a password ever being typed. Once a project exists both on your machine and on a server, the entire job of collaboration reduces to a single, almost embarrassingly simple idea: keeping two copies of the same history in agreement. There is no deeper magic underneath. There is up and there is down.
The four operations slot cleanly into that picture. A remote names the other copy and tells your Git where it lives, and names it so cheaply that the empty clone you took straight afterward was the whole proof of what it had not done. Clone is the one-time descent that brings the whole project, history and all, onto a new machine. Push lifts your local commits up to the remote so every other copy can see them, and pull lowers the remote's newer commits down into yours so you stay current. The rejections and the "already up to date" messages that look like errors at first are the system reporting, precisely, which copy is ahead of which, and the fix is never a fight, only a sync in the correct direction.
Any project you put online later will not need a new set of tools, it will use exactly these: a remote created once, a clone wherever you need one, and the pull-commit-push loop lived inside until the project is done. The vocabulary is already yours, and so is the one mental model that makes all of it legible: anything moving toward the remote is going up, anything moving toward you is coming down, and collaboration is just doing both in turn.
Cheat Sheet
Part A - Reference Table
| Operation | Command | Direction | Moves | When |
|---|---|---|---|---|
| Test the key | ssh -T git@github.com |
- | nothing (prints Hi goku!) |
once, after uploading the .pub |
| Inspect remotes | git remote -v |
- | nothing (lists) | unsure where push/pull go |
| Add a remote | git remote add origin <address> |
- | nothing (records address) | publishing a local repo |
| Clone | git clone <address> [dir] |
down | whole repo + history + sets origin |
first copy of a project |
| Push (first) | git push -u origin main |
up | local commits + sets upstream | first publish of a branch |
| Push | git push |
up | local commits | after committing, to share |
| Pull | git pull |
down | remote commits (fetch + merge) | catch up, before pushing |
| Fetch | git fetch origin |
down | remote commits (no merge) | inspect before integrating |
Part B - Compact Code Block
# One-time SSH setup
ssh-keygen -t ed25519 -C "goku@gmail.com" # make the pair, Enter for the default path
# load the private half
eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub # paste this ONE line into GitHub -> SSH keys
ssh -T git@github.com # Hi goku! = the key works# Publish a local repo (up)
# name + address, moves nothing
git remote add origin git@github.com:goku/repo.git
git push -u origin main # first push: commits up + set upstream# The daily loop: down -> local -> up
git pull # DOWN: fetch + merge remote commits
git add . && git commit -m "msg" # LOCAL: record snapshot
git push # UP: send commits to origin# When both sides moved: rejected push -> pull refuses to guess -> merge once
git config pull.rebase false # this repo reconciles by merging
git pull --no-edit && git push # integrate theirs, then send yours