Git basics · Lesson 2 of 6 · about 25 minutes

Your first repository

Create a repository, learn the three places a change can be, make your first commits, read the history, and keep files that don't belong out of Git.

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:

BASH
mkdir git-practice
cd git-practice
git init
TEXT
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:

BASH
ls
ls -a

The first shows nothing, because the folder looks empty. The second shows the hidden folder Git created:

TEXT
.	..	.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:

BASH
git status
TEXT
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:

BASH
echo "# My practice project" > README.md
git status
TEXT
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.

Think of packing a box to ship. Your desk is covered in things (your working folder). You choose what goes into the box and put it in (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).
The three places a change can beYour working folder holds the files as you edit them. git add copies a chosen change into the staging area, which holds what will go into the next commit. git commit saves everything in the staging area as a commit in the repository, which keeps every commit. git status shows what's in the working folder and the staging area.Working folderyour files, as you edit themREADME.md (changed)todo.txt (new)git addStaging areawhat goes in the next commitREADME.md (changed)git commitRepository (.git)Describe the projectAdd a READMEevery commit, keptgit status shows these twoYou choose what goes in each commit. todo.txt stays out until you add it too.
git add puts a change in the staging area; git commit saves what's staged as a commit. Anything you haven't added, like todo.txt here, stays out of the commit.

Your first commit

Stage the file, and check:

BASH
git add README.md
git status
TEXT
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:

BASH
git commit -m "Add a README"
TEXT
[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.

BASH
echo "A place to practice Git." >> README.md
echo "- learn git status" > todo.txt
git status
TEXT
Changes not staged for commit:
	modified:   README.md

Untracked files:
	todo.txt

Before staging anything, see exactly what changed in tracked files:

BASH
git diff
TEXT
@@ -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:

BASH
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

BASH
git log --oneline
TEXT
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:

BASH
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...":

WeakBetter
stuffAdd a to-do list
fixed itFix the broken link on the contact page
changesChange the button color to match the logo
updateUpdate 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:

BASH
echo "API_KEY=not-a-real-key" > .env
git status --short
TEXT
?? .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:

BASH
echo ".env" > .gitignore
git status --short
TEXT
?? .gitignore

.env has disappeared from Git's view. The .gitignore itself should be committed, so everyone working on the project ignores the same things:

BASH
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:

BASH
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:

BASH
echo "*.log" >> .gitignore
echo "debug: more" >> debug.log
git status --short
TEXT
 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:

BASH
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:

BASH
echo "- learn git add" >> todo.txt
git commit -m "Update the to-do list"
TEXT
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:

BASH
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

  1. You create a new file in a repository. What does git status call it?

  2. What does git add do?

  3. You changed two unrelated things. Why make two commits instead of one?

  4. Which is the most useful commit message?

  5. You add *.log to .gitignore, but debug.log still shows as modified. Why?

  6. Why is git add . risky without a good .gitignore?

Key terms

  • git init: turns a folder into a repository, creating the hidden .git folder.
  • 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; --staged shows what's about to be committed.
  • git log: shows the history; --oneline for 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