14 Keep Your Work
You have written scripts, startup files, and notes. Right now they are protected by nothing: an editor slip, a wrong rm, or a bad idea at midnight replaces the good version with no way back. Version control records each version you choose to keep, shows you what changed between them, and gets an old one back. Git is the version control system almost everyone uses, and its basics fit in one chapter.
The concepts in this chapter are:
- a repository and the commits that make up its history;
- the working tree, the staging area, and what
statusreports about them; diff: seeing what changed before you commit it;addandcommit: choosing what to record and describing it;logandshow: reading history;- restoring a file from the last commit or an older one;
- ignore rules, and keeping passwords and keys out of the repository.
Git is a separate program. Linux distributions and Homebrew install it with a package manager, and Git for Windows installs it with Git Bash; git --version confirms it is on PATH. The commands are identical in every shell, because git is an external program in all of them.
14.1 Repositories and Commits
A repository is a directory that Git tracks, plus a hidden .git directory inside it where Git keeps the history. A commit is a snapshot of the tracked files at one moment, with a message saying why, the author, and the time. Commits form a chain, each pointing at the one before, and that chain is the history.
Nothing is recorded until you ask. Editing a file changes the working tree, the ordinary files on disk. git add copies a file’s current state into the staging area, also called the index, which is the set of changes the next commit will contain. git commit turns the staging area into a commit. The staging step is what lets you commit one fix while leaving unrelated edits for later.
Tell Git who you are once, so that commits carry your name:
$ git config --global user.name 'Student Name' $ git config --global user.email 'student@example.com'
--global writes to ~/.gitconfig, which applies to every repository on this account. [1]
14.2 Start a Repository
Turn the practice directory into a repository:
$ cd ~/hax0r-practice $ git init Initialized empty Git repository in /home/student/hax0r-practice/.git/ $ git status On branch main No commits yet
git init creates the .git directory and nothing else. git status is the command you will run most: it reports which files are changed, staged, or unknown to Git. If your Git names the first branch master, git config --global init.defaultBranch main changes the default for new repositories; the name does not matter for this chapter. [2], [3]
14.3 Record a Change
Create a file, then look at what Git says:
$ printf 'hello\n' > notes.txt $ git status --short ?? notes.txt
?? marks an untracked file: Git sees it and is not recording it. Stage it and commit:
$ git add notes.txt $ git status --short A notes.txt $ git commit -m 'Add notes' [main (root-commit) 5177c52] Add notes 1 file changed, 1 insertion(+)
A means added to the staging area. The commit message after -m should say what the change does and, when it is not obvious, why. git commit without -m opens the editor from Chapter 5, which is where --wait and EDITOR matter. [4], [5]
Now change the file and follow the same path, with one stop to look at the difference:
$ printf 'hello\nworld\n' > notes.txt $ git status --short M notes.txt $ git diff diff --git a/notes.txt b/notes.txt --- a/notes.txt +++ b/notes.txt @@ -1 +1,2 @@ hello +world
M in the second column means modified in the working tree and not staged. git diff shows the change as lines removed with - and added with +, and reading it before staging is the habit that catches stray edits. After git add, git diff shows nothing, because it compares the working tree with the staging area; git diff --staged shows what the next commit will contain. [6]
$ git add notes.txt $ git commit -m 'Add a second line'
git add . stages everything under the current directory, and git commit -a stages every modified tracked file and commits in one step. Both are convenient and both are how unrelated changes and unwanted files end up in a commit; run git status first.
14.4 Read the History
$ git log $ git log --oneline 1553927 Add a second line 5177c52 Add notes $ git log --oneline --stat $ git show HEAD
git log lists commits, newest first, with their hash, author, date, and message. --oneline compresses each to one line, and --stat adds which files changed. git show prints one commit with its diff; HEAD is the current commit, HEAD~1 the one before, and any hash prefix names a specific one. git show HEAD~1:notes.txt prints the file as it was in that commit, without touching the working tree. [7], [8]
14.5 Get an Old Version Back
Restoration is why you kept the history.
$ printf 'oops\n' > notes.txt $ git restore notes.txt $ cat notes.txt hello world
git restore file replaces the working-tree file with the version in the staging area, or the last commit if nothing is staged. The bad edit is gone, so be sure that is what you want; the command does not ask. [9]
Two related situations:
- Staged by mistake.
git restore --staged fileremoves the file from the staging area and leaves the working tree alone. - Need a version from several commits ago.
git restore --source HEAD~2 filetakes that commit’s version.
Git also has git checkout for the same jobs, which older instructions use. git restore was added to make the two meanings of checkout separate; prefer it in new work. [9]
Commits themselves are not deleted by any command in this chapter. Even a commit you regret stays in the history, and that is the property the next section depends on.
14.6 Ignore What Should Not Be Tracked
Some files belong in the directory and not in the history: build output, editor backups, downloaded dependencies, and anything containing a secret. A .gitignore file lists patterns for Git to skip:
build/
*.log
.env
.DS_Store
A pattern ending in / matches a directory, * works as in a glob, and a leading ! re-includes something an earlier pattern excluded. Commit .gitignore itself, so that everyone who clones the repository ignores the same things. git status no longer lists ignored files, and git add . skips them. [10]
Ignoring stops Git from adding a file. It does not remove a file that is already tracked; for that, git rm --cached file stops tracking it while leaving it on disk.
14.7 Keep Secrets Out
A secret is a password, an API token, an SSH private key, or anything else that grants access. Once committed, a secret is in the history, and the history is what gets copied when a repository is shared or pushed. Removing the file in a later commit does not help:
$ git rm --cached secret.txt $ git commit -m 'Stop tracking secret' $ git show HEAD~1:secret.txt TOKEN=abc
The token is still there for anyone with the repository. Treat a committed secret as leaked: change the token, then clean the history if the repository was shared, using tools such as git filter-repo, which are outside this chapter. [11]
Prevention is cheaper:
- Put secrets in a file such as
.envand list it in.gitignorebefore creating it. - Read
git statusandgit diff --stagedbefore every commit, especially aftergit add .. - Keep keys in
~/.sshand configuration in~/.config, outside any repository. - Consider a pre-commit scanner such as
gitleaks, which refuses commits containing things that look like credentials.
Trap: git add . inside your home directory, or in a directory containing .ssh, .env, or a browser profile, stages everything there. Initialize repositories in project directories, never in your home directory itself. If you did, rm -r .git in the home directory removes the repository without touching the files.
14.8 Key Points
- A repository holds commits, each a snapshot with a message;
git initstarts one andgit statusreports its state. - Changes move from the working tree to the staging area with
git add, and into history withgit commit -m. git diffshows unstaged changes,git diff --stagedshows what the next commit contains, andgit log --onelinelists history.git restore filediscards working-tree changes,git restore --staged fileunstages, and--sourcefetches an older version..gitignorekeeps build output and secrets from being added;git rm --cachedstops tracking a file already added.- A committed secret is in the history until the history is rewritten; rotate it and prevent the next one with ignore rules and a look at
git diff --staged.
14.9 Level Up
n00bs finish the chapter here. You can record versions of your work, see what changed, and get an old version back. Hackers use history as a tool, not just a backup.
Learn branches: git switch -c experiment starts one, and git merge brings it back. Explain what a branch is in terms of commits pointing at commits, and try git log --graph --oneline --all after a merge.
Put your repository on a remote: GitHub, GitLab, or a bare repository on the lab machine from Chapter 12 over SSH. Learn git remote, git push, git pull, and git clone, and explain what origin is.
Investigate git stash, git bisect, and git blame, and describe a situation where each one saved you time. Then read about git reflog and explain why a commit is hard to lose even after a mistake.
Look at how other people write commit messages: read the log of a project you use. Read about Conventional Commits and decide whether the structure helps.
Start the dotfiles repository suggested in Chapter 10, and use git diff the next time a shell setting breaks.
14.10 Exercises
- What does this do? Predict the output of
git status --shortafter creating a file, then aftergit add, then after editing it again without adding, and check each step. - What does this do? Predict what
git diffandgit diff --stagedeach show aftergit add notes.txtfollowed by one more edit tonotes.txt. - What is the problem? A teammate’s
.gitignorelists*.log, butgit statusstill showsdebug.logas modified. Explain why and give the command that fixes it. - What is the problem? A commit contains a
.envfile with an API key, and the author deleted it in the next commit. Explain why the key must still be changed. - Why is it like this? Git has a staging area between editing and committing, when many tools just save. Explain one thing the staging area makes possible.
- Why is it like this?
git restore notes.txtdoes not ask for confirmation, whilerm -idoes. Explain what Git assumes about the version it restores from. - How do you see the version of a file from three commits ago without changing your working tree?
- How do you set up a new project so that build output and secrets are ignored before the first commit?