Chapter 2 · Git and GitHub
GitHub: Remotes, push, pull and clone
- Page 9 of 22
- 18 min read
Everything you have done with Git so far lives in one folder on one laptop. If the laptop dies, the project dies with it; nobody else can see it, review it or run it; and you cannot work on it from a second machine. A remote fixes all of that: another copy of the repository, usually on GitHub, that you push your commits to and pull other people's commits from.
For AI work, GitHub is also your public portfolio, where recruiters look first, and the place where your CI tests run and your deployments start. This page connects your repository to a remote and teaches the daily commands: remote, push, clone, fetch and pull, plus how to log in to GitHub securely.
What you will learn
- How to create a repository on GitHub, and public vs private visibility.
- What a remote is:
git remote add,git remote -v,origin, and remote-tracking branches likeorigin/main. - Sending and receiving commits:
git push -u,git clone,git fetchandgit pull, and what to do when a push is rejected. - Authenticating to GitHub: HTTPS with a fine-grained personal access token, or SSH with an ed25519 key.
Set up the example project
A small review analyser with a .gitignore and two commits, as on the previous pages:
mkdir -p ~/projects/review-analyser
cd ~/projects/review-analyser
git init -q
cat > analyse.py <<'EOF'
"""Label product reviews as positive, negative or neutral."""
POSITIVE = {"good", "great", "fast", "love"}
NEGATIVE = {"bad", "slow", "broken", "late"}
def label(text):
words = text.lower().split()
score = sum(w in POSITIVE for w in words) - sum(w in NEGATIVE for w in words)
return "positive" if score > 0 else "negative" if score < 0 else "neutral"
if __name__ == "__main__":
for review in ["great phone and fast delivery", "the box was broken"]:
print(label(review), "|", review)
EOF
printf '.env\n.venv/\n__pycache__/\n' > .gitignore
git add .
git commit -q -m "Add keyword review analyser"
echo "# Review analyser" > README.md
git add README.md
git commit -q -m "Add README"
git log --oneline3c7a863 Add README
8d9d5fc Add keyword review analyserGit, GitHub and remotes
Git is the tool on your machine; GitHub is a website that hosts Git repositories and adds collaboration features around them: pull requests, issues, code review, Actions (CI/CD), and access control. GitLab, Bitbucket and Codeberg do the same job. From Git's point of view, a repository on GitHub is simply another repository it can exchange commits with: a remote.
your laptop GitHub teammate's laptop
+----------------+ git push +----------------+ git clone +----------------+
| review-analyser| --------------> | review-analyser| -----------> | review-analyser|
| (working tree | <-------------- | (bare repo: | <----------- | (working tree |
| + .git) | git fetch/pull | history only) | git push | + .git) |
+----------------+ +----------------+ +----------------+Nobody edits files "on GitHub's copy" directly with Git; everyone works in their own clone and exchanges commits through the shared one.
Create the repository on GitHub
On github.com (sign up first if needed; GitHub asks code contributors to turn on two-factor authentication, so have an authenticator app ready):
- Click + (top right) → New repository.
- Repository name:
review-analyser. Add a one-line description: "Keyword-based sentiment labels for product reviews". - Visibility: Public (anyone can see the code; good for a portfolio and course projects) or Private (only you and people you invite). You can change it later under Settings → General → Danger Zone.
- Because you already have a local repository, do not tick "Add a README", .gitignore or licence. Those create a first commit on GitHub that your local history does not have, and your first push is then rejected.
- Click Create repository. GitHub shows a page "…or push an existing repository from the command line" with the URL of your new repository, such as
https://github.com/nadia-rahman/review-analyser.git.
On the free plan you get unlimited public and private repositories. GitHub Actions minutes are free for public repositories and 2,000 minutes a month for private ones; public repositories also get secret scanning and push protection by default (.gitignore: Keeping Secrets and Junk Out explained why that matters).
A stand-in for GitHub on your own machine
To practise every command here without an account or network, create a bare repository in a folder. A bare repository has no working tree, only the history (what is normally inside .git). That is exactly what GitHub stores for each repository, so Git treats the two the same; the only difference is the URL.
git init --bare ~/remotes/review-analyser.gitInitialized empty Git repository in /home/nadia/remotes/review-analyser.git/In the rest of the page, read ~/remotes/review-analyser.git as "the repository on GitHub". Where the real GitHub behaves differently, the text says so.
Connect a remote and push
git remote add NAME URL stores a short name for a remote. By convention the main one is called origin. git remote -v lists remotes with their URLs:
git remote add origin ~/remotes/review-analyser.git
git remote -vorigin /home/nadia/remotes/review-analyser.git (fetch)
origin /home/nadia/remotes/review-analyser.git (push)With real GitHub you use the URL from the repository page, HTTPS or SSH form; everything below works the same:
git remote add origin https://github.com/nadia-rahman/review-analyser.git # HTTPS
git remote add origin git@github.com:nadia-rahman/review-analyser.git # or SSHNow send your commits. git push -u origin main pushes the local main branch to origin, and -u (--set-upstream) remembers that pairing, so later you can just type git push and git pull:
git push -u origin mainTo /home/nadia/remotes/review-analyser.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.[new branch] main -> main means the remote did not have a main yet and now has your commits. In a terminal Git also prints progress lines (Enumerating objects…, Writing objects…), and GitHub adds a few lines starting with remote:; that is normal. After the push, refresh the GitHub page and your files, commits and README are there.
Remote-tracking branches
Your repository now also keeps origin/main: a read-only bookmark of where main was on the remote the last time you talked to it. Git compares your branch against it:
git status
git branch -vv
git log --oneline --decorateOn branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
* main 3c7a863 [origin/main] Add README
3c7a863 (HEAD -> main, origin/main) Add README
8d9d5fc Add keyword review analyser[origin/main] in git branch -vv is the upstream that -u set. Important: origin/main is only updated when you push, fetch or pull. "Up to date with 'origin/main'" means "up to date with what I last saw", not "with GitHub right now".
Clone: a second copy
git clone URL downloads a whole repository, every commit and branch, sets up origin pointing back at the URL, and checks out the default branch. It is how you start working on any existing project, and how a teammate joins yours. Let's play Rafi, Nadia's teammate, on the same machine:
mkdir -p ~/rafi
cd ~/rafi
git clone ~/remotes/review-analyser.git
cd review-analyser
git config user.name "Rafi Ahmed"
git config user.email "rafi@example.com"
git remote -v
git log --onelineCloning into 'review-analyser'...
done.
origin /home/nadia/remotes/review-analyser.git (fetch)
origin /home/nadia/remotes/review-analyser.git (push)
3c7a863 Add README
8d9d5fc Add keyword review analyserRafi has the full history, and origin was set up by the clone. git config without --global sets his name only for this repository, so his commits are not signed as Nadia. (On GitHub, the green Code button gives you the URL to clone, and git clone https://github.com/OWNER/REPO.git creates a folder named after the repository.)
Rafi adds a usage section to the README and pushes. His clone already knows its upstream, so plain git push is enough:
printf '\nRun: python analyse.py\n' >> README.md
git commit -q -am "Explain how to run the analyser"
git pushTo /home/nadia/remotes/review-analyser.git
3c7a863..10178b3 main -> main3c7a863..10178b3 main -> main means the remote's main moved from Nadia's README commit to Rafi's new one.
fetch and pull: getting other people's commits
Back in Nadia's repository, Git does not know about Rafi's push yet, because it has not asked the remote:
cd ~/projects/review-analyser
git statusOn branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree cleangit fetch downloads new commits from the remote and updates origin/main, but does not touch your branch or your files. It is always safe:
git fetch
git status
git log --oneline --decorate --allFrom /home/nadia/remotes/review-analyser
3c7a863..10178b3 main -> origin/main
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
3c7a863 (HEAD -> main) Add README
10178b3 (origin/main) Explain how to run the analyser
8d9d5fc Add keyword review analyserNow Git knows main is one commit behind. You can inspect what arrived before taking it, for example with git diff main origin/main or git log main..origin/main. git pull is git fetch followed by merging the upstream into your current branch:
git pull
cat README.mdUpdating 3c7a863..10178b3
Fast-forward
README.md | 2 ++
1 file changed, 2 insertions(+)
# Review analyser
Run: python analyse.pygit fetch | git pull | |
|---|---|---|
| Downloads new commits | Yes | Yes |
Updates origin/main | Yes | Yes |
| Changes your branch and files | No | Yes: merges (or rebases) the upstream in |
| Can cause a merge conflict | Never | Yes, like any merge |
| When to use | To look before you take; in scripts | Start of the day, before starting a branch, before pushing |
When a push is rejected
The most common surprise with remotes: both of you commit, Rafi pushes first, then Nadia pushes.
cd ~/rafi/review-analyser
printf 'fast\nslow\n' > keywords.txt
git add keywords.txt
git commit -q -m "List keywords in a text file"
git push -q
cd ~/projects/review-analyser
echo "MIT licence" > LICENSE
git add LICENSE
git commit -q -m "Add licence"
git pushTo /home/nadia/remotes/review-analyser.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '/home/nadia/remotes/review-analyser.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.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.Git refuses because the remote has a commit Nadia does not have; accepting her push would throw Rafi's commit away. (The hint: lines are worded a little differently in newer Git versions; the meaning is the same.) The fix is always the same: pull, then push.
Because both sides have new commits, this pull needs a real merge. When git pull meets divergent branches, it needs to know whether to merge or rebase them, and since Git 2.33 it stops with an error until you choose a default once. Set it now (merging is the simplest choice for beginners; the advanced tutorial explains rebasing):
git config --global pull.rebase falseFrom now on git pull means "fetch, then merge". To see both halves clearly, do them one at a time this once: fetch, merge the remote-tracking branch into main, then push:
git fetch
git merge --no-edit origin/main
git push
git log --oneline --graph --decorate -4From /home/nadia/remotes/review-analyser
10178b3..bc13a4d main -> origin/main
Merge made by the 'ort' strategy.
keywords.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 keywords.txt
To /home/nadia/remotes/review-analyser.git
bc13a4d..415a422 main -> main
* 415a422 (HEAD -> main, origin/main) Merge remote-tracking branch 'origin/main'
|\
| * bc13a4d List keywords in a text file
* | cbdb7ef Add licence
|/
* 10178b3 Explain how to run the analyserThe merge combined Rafi's commit with Nadia's (a three-way merge, as on the previous page; --no-edit accepts the default message instead of opening an editor), and the push then succeeded. A plain git pull does the same, only the merge message reads Merge branch 'main' of github.com:…. Had they changed the same lines, the merge would have stopped with a conflict, resolved exactly as you learned. Never "solve" a rejected push with git push --force on a shared branch: it overwrites the remote with your history and deletes your teammate's commits there.
Pushing branches
Branches are pushed the same way. A new branch needs -u on its first push; after that, git push is enough. git branch -a lists local and remote-tracking branches:
git switch -q -c feature/negation
echo "# TODO: handle 'not good'" >> analyse.py
git commit -q -am "Note the negation problem"
git push -u origin feature/negation
git branch -aTo /home/nadia/remotes/review-analyser.git
* [new branch] feature/negation -> feature/negation
branch 'feature/negation' set up to track 'origin/feature/negation'.
* feature/negation
main
remotes/origin/feature/negation
remotes/origin/mainA pushed branch is also a backup of unfinished work and the starting point of a pull request, which is the next page.
Logging in to GitHub: HTTPS or SSH
The bare repository needed no login. GitHub does, the first time you push (and to clone a private repository). GitHub no longer accepts your account password for Git, so you choose one of two methods.
HTTPS with a credential manager or a fine-grained token
The easiest path: let a credential manager log you in through the browser and store the credential securely. Git for Windows installs Git Credential Manager, and on the first push a GitHub sign-in window appears. On any system, the GitHub CLI can set this up for you with gh auth login (choose GitHub.com → HTTPS → log in with a web browser).
Without a credential manager, Git asks for a username and password; for the password you paste a personal access token (PAT). GitHub recommends fine-grained tokens whenever possible:
- GitHub → your picture → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
- Give it a name and an expiration (the default is 30 days; short is safer).
- Repository access: "Only select repositories" →
review-analyser. - Permissions → Repository permissions → Contents: Read and write (enough to push and pull).
- Generate, copy it once, and store it in a password manager. Treat it exactly like an API key.
Never put a token into the remote URL (https://TOKEN@github.com/…): it is then saved in plain text in .git/config and shows up in git remote -v output and screenshots.
SSH with an ed25519 key
With SSH you create a key pair once: the private key stays on your machine, the public key goes to GitHub. These steps need your real machine and account, so they are shown, not run here:
ssh-keygen -t ed25519 -C "nadia@example.com" # Enter = default file; choose a passphrase
eval "$(ssh-agent -s)" # start the agent for this terminal
ssh-add ~/.ssh/id_ed25519 # load the key (asks for the passphrase once)
cat ~/.ssh/id_ed25519.pub # copy this one line: the PUBLIC keyOn GitHub: Settings → SSH and GPG keys → New SSH key, paste the .pub line, save. Then test the connection and point the remote at the SSH URL:
ssh -T git@github.com
git remote set-url origin git@github.com:nadia-rahman/review-analyser.gitHi nadia-rahman! You've successfully authenticated, but GitHub does not provide shell access.The first time, SSH asks whether to trust GitHub's host key; compare the fingerprint with the one in GitHub's documentation, then answer yes. Use ed25519: it is short, fast and what GitHub recommends; RSA keys are only for old systems that cannot use it.
| HTTPS | SSH | |
|---|---|---|
| URL | https://github.com/OWNER/REPO.git | git@github.com:OWNER/REPO.git |
| Credential | Browser login via a credential manager, or a fine-grained PAT | Key pair; public key added to GitHub |
| Setup effort | Lowest with Git Credential Manager or gh auth login | A few commands, once per machine |
| Works through strict firewalls | Yes (port 443) | Usually (port 22; GitHub also offers SSH over 443) |
| Good for | Beginners, Windows, CI tokens | Daily work on your own machines |
Never share or commit a private key (id_ed25519 without .pub) or a token. If one leaks, delete it on GitHub immediately and create a new one.
Where this shows up in real AI work
- Every collaboration starts with
git clone: open-source model libraries, your team's backend, a course project template. You will clone far more often than you create repositories. - Training servers and cloud GPUs pull code from GitHub: you push from your laptop, then
git pullon the GPU machine and run the experiment there. A deploy key or a fine-grained, read-only token is the safe way to give that machine access. - Portfolios: a public repository with a clear README and history shows how you work. Make sure no keys, private data or large datasets are in it before you flip it to public.
- CI and deployment (advanced tutorial) are triggered by pushes: pushing a branch runs the tests, merging to
maindeploys the model API.
Common mistakes
- Creating the GitHub repository with a README, then pushing an existing project. The push is rejected because the histories are unrelated. Fix: create it empty; if it is too late,
git pull --allow-unrelated-histories origin mainonce, resolve, push. - Forcing a rejected push.
git push --forcedeletes teammates' commits on the remote. Fix:git pull(resolve any conflict), thengit push. - Trusting
git statusabout the remote. "Up to date with 'origin/main'" only reflects the last fetch. Fix:git fetch(orgit pull) first. - Pasting a token into the URL or a script. It ends up in
.git/config, shell history and screenshots. Fix: a credential manager orgh auth login, fine-grained tokens with an expiry, SSH keys with a passphrase. - Making a repository public with secrets in its history. Every old commit becomes public too. Fix: check with
git log -p -S "sk-"(and a scanner) first; rotate anything found.
Try it yourself
Use Nadia's and Rafi's repositories from this page.
- Easy: In Rafi's repository, show the remotes and which upstream each local branch tracks. Then fetch, and show whether his
mainis behind. - Medium: Rafi wants to work on Nadia's
feature/negationbranch. Get it into his repository, switch to it, and show its last commit. - Hard: A repository can have several remotes. Create a second bare repository
~/remotes/backup.git, add it to Nadia's repository asbackup, push all her branches to it in one command, and prove it worked by cloning the backup into~/restore-testand listing its branches.
Answers
# Easy
cd ~/rafi/review-analyser
git remote -v
git branch -vv
git fetch -q
git statusorigin /home/nadia/remotes/review-analyser.git (fetch)
origin /home/nadia/remotes/review-analyser.git (push)
* main bc13a4d [origin/main] List keywords in a text file
On branch main
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree cleanRafi's main is behind by two commits: Nadia's licence commit and her merge commit.
# Medium
git switch feature/negation
git log --oneline -1Switched to a new branch 'feature/negation'
branch 'feature/negation' set up to track 'origin/feature/negation'.
30660f8 Note the negation problemThe fetch already brought origin/feature/negation. git switch with the name of a branch that exists only on the remote creates a local branch tracking it.
# Hard
cd ~/projects/review-analyser
git init -q --bare ~/remotes/backup.git
git remote add backup ~/remotes/backup.git
git push -q backup --all
git remote
git clone -q ~/remotes/backup.git ~/restore-test
git -C ~/restore-test branch -abackup
origin
* main
remotes/origin/HEAD -> origin/main
remotes/origin/feature/negation
remotes/origin/main--all pushes every local branch. git -C FOLDER runs a Git command in another folder without cd. The clone checked out main and knows feature/negation as a remote branch: the backup is complete.
Summary
- A remote is another copy of the repository; GitHub stores a bare repository (history only), and
originis the conventional name for the main remote. git remote add origin URLconnects;git push -u origin mainpushes and sets the upstream; afterwardsgit pushandgit pullare enough.git clone URLcopies a whole repository and sets uporigin.git fetchupdatesorigin/mainsafely;git pull= fetch + merge.- A rejected push means the remote has commits you lack: pull, resolve if needed, push. Never force-push a shared branch. Set
pull.rebase falseonce. - Log in with a credential manager /
gh auth login, a fine-grained token with an expiry, or an ed25519 SSH key. Keep tokens and private keys out of URLs and repositories.
Next: Pull Requests, README and Your First Published Project. With a remote in place, you will write a README people can use, work on a feature branch, open a pull request, get it reviewed and merged, and publish your first project.