By the end you'll know
- How to turn a folder into a Git repository
- The three places a change can be, and how it moves between them
- How to make commits, and write messages your future self will thank you for
- How to see what changed, and read the history
- How to keep files out of Git with
.gitignore, and what it can't do
Create a repository
Make a folder for practicing, and turn it into a repository. mkdir ("make directory") creates a folder, and cd ("change directory") moves you into it, so the next commands run there:
mkdir git-practice
cd git-practice
git init
Initialized empty Git repository in /Users/you/git-practice/.git/
That's all it takes. git init is short for "initialize": it turns the folder you're in into a repository, so Git can start keeping its history. Here's what it actually did, and didn't do:
- It created one hidden folder,
.git, inside your folder. That's where Git keeps every commit and all its records: the whole history lives in there. It's hidden because its name starts with a dot: macOS, Linux and most tools treat names that start with a dot as hidden, so you don't see them unless you ask. - It didn't touch anything else. Any files already in the folder stay exactly as they were, and Git doesn't start tracking them until you tell it to (that's the next section).
- You run it once per project, at the very start. Run it again by mistake and nothing breaks: Git says it reinitialized the existing repository and leaves your history alone.
See it for yourself. ls (short for "list") shows what's in the folder you're in, and -a (short for "all") tells it to include hidden things too. Try both:
ls
ls -a
The first shows nothing, because the folder looks empty. The second shows the hidden folder Git created:
. .. .git
Ignore the . and ..: every folder has them, as shortcuts meaning "this folder" and "the folder above it". (If you use Finder on a Mac, it hides .git too. Press Cmd+Shift+. in a Finder window to show hidden files, and the same keys again to hide them.)
You'll never need to open .git, and you shouldn't edit anything in it. Delete it, and the folder is just a folder again, with no history at all.
You won't always start with git init. When a project already exists on GitHub, you download it with git clone instead, which brings its .git folder, and the full history, with it. Lesson 5 covers that.
Now ask Git what's going on. You'll type this command more than any other:
git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
An empty repository, on a branch called main (Lesson 4 explains branches).
The three places a change can be
Create a file, and ask again. echo prints text, and > sends that text into a file instead of the screen, creating the file:
echo "# My practice project" > README.md
git status
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
Untracked means Git can see the file but isn't keeping track of it yet. Before you can commit it, you have to tell Git it belongs in the next commit. That's because Git has three places a change can be:
- Your working folder: the files as they are on your disk, which you edit normally.
- The staging area: the changes you've chosen to go into the next commit. Git also calls it the index.
- The repository: every commit you've made, safe in
.git.
git add moves a change from your folder to the staging area. git commit takes everything in the staging area and saves it as a commit.
git add); nothing's sent yet, and you can still take things out. When the box holds exactly what you want, you seal it and write a label saying what's inside (git commit with a message). Once it's sealed and labeled, it goes on the shelf with every box you've ever packed (the repository).Your first commit
Stage the file, and check:
git add README.md
git status
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
"Changes to be committed": it's in the box. Now seal it. -m gives the commit its message:
git commit -m "Add a README"
[main (root-commit) 4156b52] Add a README
1 file changed, 1 insertion(+)
create mode 100644 README.md
Your first commit. 4156b52 is the start of its ID, a long, unique code Git gives every commit; yours will be different. root-commit means it's the first one, with nothing before it.
Why the staging area is worth it
Make two unrelated changes: add a line to the README, and start a to-do list. Watch the arrows: >> adds a line to the end of a file, while a single > replaces everything in it. Mix them up, and > wipes out what was there.
echo "A place to practice Git." >> README.md
echo "- learn git status" > todo.txt
git status
Changes not staged for commit:
modified: README.md
Untracked files:
todo.txt
Before staging anything, see exactly what changed in tracked files:
git diff
@@ -1 +1,2 @@
# My practice project
+A place to practice Git.
Lines starting with + were added, lines starting with - were removed. Now, here's what the staging area is for. These are two different changes, so they deserve two commits, each with its own message:
git add README.md
git commit -m "Describe the project in the README"
git add todo.txt
git commit -m "Add a to-do list"
Each commit now does one thing, which makes the history easy to read, and lets you undo one change without the other (Lesson 3). Before a commit, git diff --staged shows what's in the box, which is what you're actually about to commit.
Read the history
git log --oneline
a698b99 Add a to-do list
f6b60d9 Describe the project in the README
4156b52 Add a README
Newest first, one line each. Without --oneline, git log also shows the author and date of each commit. To see everything about the latest commit, including what it changed:
git show
Writing good commit messages
Your messages are how you, and anyone else, will understand the history later. A good one says what the commit does, in a few words, as if finishing the sentence "This commit will...":
| Weak | Better |
|---|---|
stuff | Add a to-do list |
fixed it | Fix the broken link on the contact page |
changes | Change the button color to match the logo |
update | Update Flask to 3.1.3 |
Six months from now, "fixed it" tells you nothing. "Fix the broken link on the contact page" tells you exactly where to look.
Keep files out with .gitignore
Some files should never be committed: passwords, settings for your laptop only, logs, and folders a tool can rebuild. Make a file that looks like a secret:
echo "API_KEY=not-a-real-key" > .env
git status --short
?? .env
--short is a compact git status: ?? means untracked. One careless git add . (which stages everything in the folder) and that key is in your history forever (Lesson 1). Tell Git to ignore it. A file called .gitignore lists names and patterns Git should pretend it can't see:
echo ".env" > .gitignore
git status --short
?? .gitignore
.env has disappeared from Git's view. The .gitignore itself should be committed, so everyone working on the project ignores the same things:
git add .gitignore
git commit -m "Ignore the .env file"
Patterns work too: *.log ignores every file ending in .log, and node_modules/ ignores a whole folder. If you've done the Docker track, this is the same idea as .dockerignore, which keeps files out of images.
In my real setup
On my laptop, the repository for this website always has two files with local changes I deliberately keep out of my commits. So I never use git add .. I add files by name, every time: git add content/lessons/git/your-first-repository.mdx. And git status before each commit shows those two files as modified, a reminder that they're there and not going in.
This site's .gitignore ignores node_modules, the build folder .next, macOS's .DS_Store files, and anything ending in .env, .pem or .key. While writing this lesson, I tested it, and found a gap: *.env matches .env, but not .env.local, the file this site's framework uses for local secrets by convention. Nothing had leaked, because no such file existed yet, but the first one would have been committed without a warning. I added .env*, which catches all of them. Test your .gitignore with git check-ignore -v filename: it tells you whether a file is ignored, and which line is doing it.
Break it on purpose
Ignore a file that's already tracked. Create a log file, and commit it with the careless shortcut:
echo "debug: started" > debug.log
git add .
git commit -m "Add debug notes"
Now decide logs shouldn't be in Git, and ignore them. Then change the log and check:
echo "*.log" >> .gitignore
echo "debug: more" >> debug.log
git status --short
M .gitignore
M debug.log
debug.log still shows as modified, even though *.log is ignored. Here's the rule: .gitignore only affects files Git isn't tracking yet. Once a file has been committed, Git keeps tracking it. To stop, remove it from Git without deleting it from your disk:
git rm --cached debug.log
git add .gitignore
git commit -m "Stop tracking log files"
git status --short
Nothing listed: debug.log is still in your folder (ls shows it), but Git now ignores it. One warning: the earlier commit still contains the file. For a log, that's fine. For a password, it isn't: change the password, because anyone with the history can read the old one.
Commit with nothing staged. Change a file, forget to add it, and commit:
echo "- learn git add" >> todo.txt
git commit -m "Update the to-do list"
no changes added to commit (use "git add" and/or "git commit -a")
Nothing was committed: the change is in your folder, not the box. git status would have shown it. Add it, and commit again:
git add todo.txt
git commit -m "Update the to-do list"
Clean up
Keep the git-practice folder: Lessons 3 and 4 use it.
Check yourself
You create a new file in a repository. What does git status call it?
What does git add do?
You changed two unrelated things. Why make two commits instead of one?
Which is the most useful commit message?
You add *.log to .gitignore, but debug.log still shows as modified. Why?
Why is git add . risky without a good .gitignore?
Key terms
- git init: turns a folder into a repository, creating the hidden
.gitfolder. - git status: shows what's changed, what's staged and what's untracked. Use it constantly.
- Untracked: a file Git can see but isn't keeping track of.
- Staging area (index): the changes chosen to go into the next commit.
- git add / git commit: put a change in the staging area / save what's staged as a commit.
- git diff: shows changes not yet staged;
--stagedshows what's about to be committed. - git log: shows the history;
--onelinefor one line per commit. - .gitignore: a file listing names and patterns Git should ignore. It only affects untracked files.
- git rm --cached: stops tracking a file without deleting it.
Go deeper
- Chapter 2.2 of Pro Git (opens in a new tab) covers recording changes, the staging area and
.gitignorein more depth. - gitignore templates (opens in a new tab) on GitHub has ready-made
.gitignorefiles for most languages and tools.