Git: Saving and Sharing Your Work
The idea
Think of a video game with a save system. Before a risky boss fight you save, and if it goes badly you reload. Git is that system for your files, and it keeps every save forever.
A folder that Git is watching, together with its saved history, is a repository (repo). One named checkpoint is a commit. Git does not save everything you touched. You choose what goes into each checkpoint, so the next save holds only what you picked.
Why bother? Git lets you undo mistakes, see exactly what changed and when, and share work without emailing files around. Nearly every software team uses it.
Setup: on Windows, work in the Ubuntu (WSL2) app from the first lesson. On macOS, running git --version offers to install Git. On Linux, use your package manager or see https://git-scm.com/downloads. Run git --version to check; you need 2.28 or newer. If Ubuntu says command not found, install it with sudo apt update && sudo apt install -y git. (sudo runs one command as the administrator; it asks for your password, and nothing shows as you type.)
git init, add, and commit
Starting a repo is switching the save system on for one folder. Staging is choosing which changes go into the next save, and committing is pressing the save button. First tell Git who you are, then create a folder and start a repository in it:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global core.editor nano
mkdir gonomicon-git
cd gonomicon-git
git init -b main
Use your own name and email. git config --global stores a setting once for all repos. The first two are your identity, which is stamped on every save. init.defaultBranch main makes new repos start on a branch called main, and core.editor nano picks the text editor Git opens when it needs you to type a message (nano is the friendly one from the terminal lesson: Ctrl+O then Enter saves, Ctrl+X exits). Without it Git may open vim, which is hard to escape. git init -b main creates a hidden .git folder, which holds the history, and names the first branch main. It prints Initialized empty Git repository in .../gonomicon-git/.git/. Never run git init inside a folder that is already a repo. GitHub also offers a private "no-reply" email address in its settings if you would rather not publish your real one.
Now make your first checkpoint:
echo "# GoNomicon practice" > README.md
git status
git add README.md
git commit -m "Start practice"
git status
The echo ... > line is the redirect from the terminal lessons: it creates README.md. git status reports where things stand. The first one prints:
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
nothing added to commit but untracked files present (use "git add" to track)
Untracked files means Git has never saved it. git add puts the file on the list for the next save; this is called staging. git commit -m writes the save with a short label (-m is the message). It prints something like [main (root-commit) 4f949b6] Start practice, then 1 file changed, 1 insertion(+); the id after main is this save's name, and yours will differ. After git add and before the commit, git status shows Changes to be committed: new file: README.md, the staged list. The second git status says nothing to commit, working tree clean: every change is saved. If you leave -m off, Git opens the text editor you chose (nano): type the message on the first line, press Ctrl+O, Enter, then Ctrl+X to save and leave. If you ever land in vim instead (a screen you can't escape), press Esc, then type :q! and press Enter.
Reading git log and diff
A save list is only useful if you can read it. git diff compares your files with the last save, git diff --staged compares the next save with the last one, and git log lists every save.
echo "Git remembers every save." >> README.md
git diff
git add README.md
git diff --staged
git commit -m "Explain checkpoints"
git log --oneline
>> appends, as before. git diff shows edits you have not staged yet. git diff --staged shows edits waiting for the next commit. Both print the same kind of report:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
# GoNomicon practice
+Git remembers every save.
A line starting with + was added, a line starting with - was removed, and a line starting with a space is unchanged context. (There is also an index line you can ignore. @@ -1 +1,2 @@ says "the old file had 1 line here; the new one has 2".) git diff goes quiet after you stage the change, because there is nothing left unstaged; that is why you use --staged next. git log --oneline lists saves newest first, one per line: a short id like a1b2c3d, then the message. Your two lines look like 21c8a0f Explain checkpoints and 4f949b6 Start practice, but the ids on your machine will differ. git show prints the newest save with its full message and changes (git show a1b2c3d shows any save by id). git commit -am "message" stages every change to files Git already tracks and commits, in one step; it skips brand-new files. For long histories, git log opens in the less pager, so press q to leave.
.gitignore
Some files should never be saved: temporary files, logs, and above all secrets. A .gitignore file lists the names Git should skip:
echo "notes.tmp" > .gitignore
echo "scratch" > notes.tmp
git status --short
git add .gitignore
git commit -m "Ignore temporary notes"
git status --short prints one line per file. ?? means untracked: Git has never seen the file. So .gitignore shows as ?? .gitignore, while notes.tmp does not appear at all because it is ignored. Patterns work too: *.log skips every file ending in .log, .env skips a secrets file, and build/ skips a whole folder. Ignoring a file only affects future saves. If a password was already committed, ignoring it does not remove it from history, so treat it as leaked and change it.
Undoing mistakes
Git offers a different undo for each situation. Pick by asking: has this change been saved yet, and has it been pushed?
Not saved yet. git restore throws away edits you have not committed, and git restore --staged takes a file off the next-save list while keeping your edit:
echo "oops" >> README.md
git restore README.md # throw away the unsaved edit
echo "oops" >> README.md
git add README.md
git restore --staged README.md # take it off the next-save list, keep the edit
git restore README.md # now throw the edit away too
git restore on an unsaved edit is permanent, because Git never saved that edit. Use it carefully.
Saved, and you want it undone safely: git revert. It adds a NEW save that does the opposite of an earlier one, so history stays honest and nothing is destroyed. This is the right undo for anything you have already pushed:
echo "bad line" >> README.md
git commit -am "Add bad line"
git revert --no-edit HEAD
git log --oneline
git revert HEAD undoes the newest save. Without --no-edit it opens your editor so you can adjust the message; with it, Git writes Revert "Add bad line" for you. git log --oneline now shows both saves, Revert "Add bad line" on top, and the bad line is gone from the file. To undo an older save, give its id from the log: git revert a1b2c3d.
Saved, but not pushed, and you want to un-save it: git reset. git reset --soft HEAD~1 removes the newest save but keeps its changes staged, so you can commit again properly. git reset with no options takes everything off the staged list. git reset --hard also throws your edits away, permanently: they are not recoverable by normal means. Never use reset on saves you have pushed; use revert.
A file you saved by mistake. Suppose you committed a .env file with a password in it. git rm --cached .env removes it from Git's tracking while leaving the file on your disk; then add .env to .gitignore and commit both changes:
echo "password=hunter2" > .env
git add .env
git commit -m "Add settings" # the mistake: the secret is now saved
git rm --cached .env
echo ".env" >> .gitignore
git add .gitignore
git commit -m "Stop tracking .env"
git rm --cached prints rm '.env'. The old save still contains the file, so if it was pushed, the password is leaked: change the password.
Branches and merging
A branch is a parallel save slot. You can play out a risky idea in one slot while your main slot stays untouched. Merging copies the progress from one slot into another.
git branch experiment
git switch experiment
git branch
echo "Experiment work" > experiment.txt
git add experiment.txt
git commit -m "Save experiment"
git switch main
ls
git merge experiment
ls
git branch -d experiment
git branch lists your slots, with * marking the one you are in. git switch moves to another slot (it needs Git 2.23 or newer). When you switch back to main, ls does not show experiment.txt, because that file only exists in the experiment slot. After git merge experiment it appears. Git reports a "Fast-forward" because main had not changed in the meantime, so it simply caught up. git branch -d deletes the finished slot. It prints Deleted branch experiment (was cc43919). git switch -c NAME creates a branch and switches to it in one step.
A fast-forward only happens when main has not moved. If both branches gained saves, Git makes a new merge commit that joins them, prints Merge made by the 'ort' strategy., and opens your editor with a ready-made message such as Merge branch 'experiment'. Save and close it (Ctrl+O, Enter, Ctrl+X in nano) to finish. git log --oneline --graph draws the two lines of work joining back together. If the same line changed on both sides, you get a merge conflict, which the next lesson covers.
Remotes, push, and pull
A remote is a cloud backup copy of your repo, usually on GitHub (a website that hosts remotes). Five commands connect you to it:
git remote add origin ADDRESSregisters a remote under the nicknameorigin.git pushuploads your commits to it. Git refuses the push when the remote has commits you do not have yet; the next lesson shows how to fix that.git fetchdownloads new commits from it without touching your files, so you can look first.git pulldownloads new commits from it and merges them into your branch (a fetch followed by a merge).git clonedownloads a whole repo with its history, which is how you start on a project that already exists.
Commits stay on your computer until you push them. You will run all four against a real GitHub repo in the steps below, after setting up an account and an SSH key.
Common mistakes
- Forgetting
git add. A commit only contains what you staged. - Using
git add .(it means "stage everything here") without checkinggit status. It stages every file that isn't ignored, including files you did not mean to save. - Believing a commit uploads anything. Commits stay on your computer until you
git push. - Saving work on the wrong branch. Run
git branchbefore you start. - Committing secrets such as passwords, tokens, or the private SSH key. Once pushed, assume they are public.
- Running
git initin your home folder or inside another repo. - Using
git reset --hardorgit restoreon work you wanted: they cannot be undone. Usegit revertfor saves, and checkgit statusfirst. - Committing a file and adding it to
.gitignoreafterwards. Ignoring does not untrack it; usegit rm --cached.
Try it
- Run the local examples. Inside
gonomicon-git, run the sections above in order if you have not yet. You should see:README.mduntracked, thenworking tree cleanafter the first commit; a+Git remembers every save.line ingit diffand again ingit diff --staged;git log --onelinelisting two saves;.gitignorebut notnotes.tmpingit status --short; andFast-forwardwhen you mergeexperiment. Finish withgit log --onelineandls: if you ran every example, including the undo ones, you have eight saves (Start practice, Explain checkpoints, Ignore temporary notes, Add bad line, Revert "Add bad line", Add settings, Stop tracking .env, Save experiment), andexperiment.txtis in the folder. Check that.envis still on disk (ls -a) but missing fromgit status. - Create a GitHub account and check for SSH keys. GitHub hosts remote repos. Sign up at https://github.com. SSH (Secure Shell) is a way to prove who you are using a key pair: a private key that stays on your computer, and a public key you give to GitHub. First see what keys you already have:
ls -al ~/.ssh
~/.ssh is the hidden folder where SSH keeps keys (-a shows hidden names). If you see id_ed25519 and id_ed25519.pub, skip to step 4. If the folder is missing or empty, continue. (If SSH is blocked on your network, use HTTPS instead; see the box after step 5.)
- Create a key, only if you have none. Run this once:
ssh-keygen -t ed25519 -C "you@example.com"
-t ed25519 picks a modern key type and -C adds a label. It asks where to save the key and for a passphrase. Press Enter to accept the default location. A passphrase is optional; Enter skips it. If it asks whether to overwrite an existing key, answer n. You now have a private key (id_ed25519) and a public key (id_ed25519.pub). Never share the private one.
- Give GitHub your public key and test it.
cat ~/.ssh/id_ed25519.pub
This prints one long line starting with ssh-ed25519. Copy all of it. On GitHub, open Settings, then SSH and GPG keys, then New SSH key, paste it, and save. Then test:
ssh -T git@github.com
-T means "just test, don't open a session". The first time, it asks whether to continue connecting. Type yes. You should see Hi YOUR-USERNAME! You've successfully authenticated, but GitHub does not provide shell access. The command still ends with a failure status of 1; that is normal here. If you see Permission denied (publickey), the key on GitHub does not match the one on your computer, so repeat the copy and paste.
- Create a remote repo and connect to it. On GitHub, click New repository, name it
gonomicon-git, and leave "Add a README" and ".gitignore" unchecked, because you already made them locally. Back in your terminal, in thegonomicon-gitfolder:
echo "Type your GitHub username, then press Enter:"
read -r GITHUB_USERNAME
git remote add origin "git@github.com:$GITHUB_USERNAME/gonomicon-git.git"
git remote -v
read -r stores what you type in the variable GITHUB_USERNAME, as in the shell lessons. git remote add origin registers the GitHub repo under the nickname origin, and git remote -v shows its two addresses (one to fetch from, one to push to).
If you use HTTPS instead of SSH. Skip steps 3 and 4. GitHub no longer accepts your account password for Git. Create a personal access token, which is a long generated password that you can limit and delete: on GitHub open Settings, Developer settings, Personal access tokens, Fine-grained tokens, Generate new token; pick your gonomicon-git repository and give it "Contents: Read and write". (GitHub moves these menus around sometimes; the docs at https://docs.github.com/en/authentication have the current clicks.) Copy the token once, because GitHub never shows it again. Use the HTTPS address https://github.com/$GITHUB_USERNAME/gonomicon-git.git in the commands of this lesson instead of the git@github.com: one, and when Git asks for a password, paste the token. Ask Git to remember it with git config --global credential.helper store (it keeps the token in a plain file in your home folder, so only do this on your own computer). Treat the token exactly like a password.
- Upload your checkpoints.
git push -u origin main
git push sends your commits to the remote, and -u remembers that main is paired with origin/main, so later you can type just git push. Refresh the repo page on GitHub. You should see README.md, .gitignore, and experiment.txt, but not notes.tmp.
- Download a change made elsewhere. On GitHub, open
README.md, click the pencil icon, add the lineEdited on GitHub., and commit it tomain. Then in your terminal:
git pull
cat README.md
You should see "Fast-forward" and your README now contains the new line.
- Clone a second copy. Staying in the same terminal window:
cd ..
git clone "git@github.com:$GITHUB_USERNAME/gonomicon-git.git" gonomicon-git-copy
cd gonomicon-git-copy
git log --oneline
git remote -v
The history matches the original, and origin is already set up. If you opened a new terminal, type your username in place of $GITHUB_USERNAME. When you are done, run cd ../gonomicon-git to return to your original repo. The next lesson starts there.