Your First Git Repo: git init, the Staging Area and the First Commit
We build the bookstore repo from scratch with git init and take a first look inside the .git folder. We walk through the working tree, staging area and repository with git status, git add and git commit, and learn to read the two columns of status -s and why an MM file commits its staged version.
Version note: The commands in this post were tested with Git 2.54.
Guess First
You added a line to README.md and ran git add README.md. Then, before committing, you added one more line to the same file. If you run git commit now, which version of the file goes into the commit: the latest one with both lines, or the one from the moment you ran git add?
The answer rests on Git's most important idea, the three areas. In this post we build the bookstore repo from scratch and make its first two commits, looking at those three areas one by one along the way.
Creating the Repo
If you made the settings from the previous post (user.name, user.email, init.defaultBranch main), everything is ready. Let Git create the project folder:
$ git init bookstore
Initialized empty Git repository in /.../bookstore/.git/
$ cd bookstore
$ ls -A
.git
git init created a folder with the name you gave and put a hidden folder called .git inside it. If you are working on an existing project, go into its folder and run git init with no argument; it leaves your files alone and only adds .git. I shortened the start of the path in the output to ...; you will see the full path.
What makes a folder a repo is nothing more than this .git folder. Delete it and the folder becomes an ordinary folder again, with all its history gone. Copy it and the history comes along.
A First Look Inside .git
$ ls -1F .git
HEAD
config
description
hooks/
info/
objects/
refs/
$ cat .git/HEAD
ref: refs/heads/main
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)
It is nearly empty for now, but the skeleton is in place. objects/ is where all content is stored: files, folders and commits. refs/ records which commit each branch and tag points to. config is the local settings file we saw in the previous post. hooks/ holds sample scripts that run on events such as a commit; we will come back to hooks later in the series.
The most interesting one is HEAD. It contains a single line: ref: refs/heads/main. That means "I am on the main branch right now". The odd part is that main does not exist yet, because a branch points to a commit and there are no commits. git status says so too: "No commits yet". main will be born with the first commit. It is healthiest not to touch anything inside this folder by hand; we will do everything with Git commands.
Three Areas: Working Tree, Staging Area, Repository
In Git a file can live in three places. The working tree (also called the working directory) is the files you see on disk and edit in your editor. The staging area is the in-between area where the changes for the next commit are prepared; Git's own documentation calls it the index, and it lives in the .git/index file. The repository is .git/objects, where commits are stored for good.
Think of packing a parcel. The items on your desk are the working tree. The items you put into the box are the staging area: you have decided what goes, but the box is not sealed yet, so you can take something out and put something else in. The moment you tape the box shut and stick the label on is the commit: the contents are now fixed, and the label says who sent it and when. git add puts an item into the box, git commit seals it.
Why does this in-between area exist? Because in one working session you often do more than one thing. While fixing a bug, you also spot a typo in the README. Thanks to the staging area you can put the two into separate commits, so each commit tells one story.
Talking to git status
Let's add two files to the project. README.md:
# Bookstore
A tiny bookstore app for the Git & GitHub series.
And src/books.js:
export const books = [
{ id: 1, title: "Dune", price: 12.5 },
{ id: 2, title: "Neuromancer", price: 9.99 },
];
Now let's ask Git what it sees:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
src/
nothing added to commit but untracked files present (use "git add" to track)
Both files are "untracked": Git sees them but does not track them. It shows the folder as a single src/ line because nothing inside it is tracked. Pay attention to the parts of the messages in parentheses; git status always tells you the next step as well. When you are starting out, reading these hints teaches you more than memorizing commands.
$ git add README.md
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/
README.md is now under "Changes to be committed", that is, in the staging area. src/ is still outside. The hint line changed too: it now tells you how to take the file back out of the staging area. We will cover undoing changes in a separate post later.
The First Commit
$ git add src/
$ git status -s
A README.md
A src/books.js
$ git commit -m "Add README and book list"
[main (root-commit) 29f27c8] Add README and book list
2 files changed, 7 insertions(+)
create mode 100644 README.md
create mode 100644 src/books.js
$ git status
On branch main
nothing to commit, working tree clean
$ git log --oneline
29f27c8 (HEAD -> main) Add README and book list
git status -s gives the same information in a short form; we will read its two columns in detail shortly. A means "added", a file newly added to the staging area.
The first line of the commit output says a lot. main is the branch the commit was made on. root-commit says this is the first commit, with nothing before it. 29f27c8 is the abbreviated hash of the commit, its identity. If you make a commit with the same content, the same author and the same date, you get the same hash; mine was made with a fixed date, so you will see a different hash. create mode 100644 below it shows that a new file was created and that it is a regular (non-executable) file.
After the commit, git status says "working tree clean": the files on disk, the staging area and the last commit are identical. git log --oneline shows the first commit with the HEAD -> main label. main exists now and points to this commit.
The Change Cycle and the Two Columns of status -s
Everyday work is this cycle on repeat: edit, add, commit. Let's do another round. A new file for the cart, src/cart.js:
import { books } from "./books.js";
export function addToCart(cart, bookId) {
const book = books.find((b) => b.id === bookId);
return book ? [...cart, book] : cart;
}
And let's append the line Books live in src/books.js. to the README:
# src/cart.js is new, a line was appended to README.md
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/cart.js
no changes added to commit (use "git add" and/or "git commit -a")
$ git status -s
M README.md
?? src/cart.js
The long output shows two separate groups: a file that was committed before and has changed ("modified"), and a new, untracked file. The short output has two columns. The left column is the state of the staging area compared to the last commit; the right column is the state of the working tree compared to the staging area. M has an empty left column and an M on the right: the file has changed, but the change is not in the staging area yet. ?? means untracked. Let's add both:
$ git add README.md src/cart.js
$ git status -s
M README.md
A src/cart.js
The M moved to the left column: the change is in the staging area, ready for the commit. Now for the opening question. Before committing, we add one more line to the README:
# After appending one more line to README.md
$ git status -s
MM README.md
A src/cart.js
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
new file: src/cart.js
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
MM: there is an M in both columns. The long output shows the same file in both groups at once. The staging area holds the README with the first added line; the working tree holds the version with both lines. If you commit now, the commit gets the staging area version, the one with a single new line. The second line stays in the working tree. That is the answer to the opening question: a commit takes a snapshot of the staging area, not of the working tree.
We want the second line in the commit too, so we add the file again and commit:
$ git add README.md
$ git status -s
M README.md
A src/cart.js
$ git commit -m "Add shopping cart"
[main fae4454] Add shopping cart
2 files changed, 9 insertions(+)
create mode 100644 src/cart.js
$ git log --oneline
fae4454 (HEAD -> main) Add shopping cart
29f27c8 Add README and book list
We now have a history of two commits. The HEAD -> main label moved to the new commit; main moves forward with every commit. Look inside .git again and you will see a few new things:
$ ls -1F .git
COMMIT_EDITMSG
HEAD
config
description
hooks/
index
info/
logs/
objects/
refs/
index is the staging area itself; it was created by the first git add. COMMIT_EDITMSG holds the last commit message, and logs/ keeps a record of which commit each branch moved from and to. The objects/ folder is full now as well. In the next post we open it up and see exactly what is in there.
The Forms of git add
You can give git add a file, a folder or a pattern. git add . adds everything under the current folder, and git add -A adds new, modified and deleted files across the whole repo. Both are convenient but dangerous: you throw everything into the box without looking at what goes in. git add -p shows the changes in a file piece by piece and asks "stage this?" for each one; it is the cleanest way to split two separate pieces of work in the same file into two commits.
git commit -a skips the staging step: it automatically adds tracked files that have changed and commits them. The "tracked" part matters; -a does not add a new, untracked file. It is handy for small, single-file fixes, but if you make it a habit you lose the control the staging area gives you.
Common Pitfalls
The first pitfall is running git init in the wrong folder. Run git init in your home folder by accident and your entire home folder becomes a repo, with every project under it looking like part of that repo. If git status shows far more untracked files than you expect, first check where you are; git rev-parse --show-toplevel tells you the root of the repo.
The second is continuing to edit a file after git add and assuming its latest version was committed. As the MM example showed, the commit takes the staging area. A quick look at git status before committing prevents that surprise.
The third is expecting empty folders to be committed. Git tracks files, not folders. A folder with no files in it does not show up in git status and does not go into a commit. If the folder needs to exist in the repo, putting a small file inside it is a common solution.
The fourth is pulling password files, node_modules or build output into the repo with git add .. The remedy is .gitignore; we will get to it two posts from now.
A Small Experiment
★☆☆ Build the bookstore repo on your own machine with the steps in this post and make the two commits. After each step, put the output of git status and git status -s side by side and compare how they show the same information.
★★☆ Add a new book to src/books.js and run git add. Then change the price of a book in the same file. What does git status -s show? Before committing, guess which change will go into the commit; after committing, check with git status.
★★★ Create an empty docs folder and see whether it shows up in git status. Then put a file inside it. After that, create a new file and try git commit -a -m "...": did the new file get into the commit?
In this post we looked at a commit from the outside. In the next post we go inside: is a commit really a diff, or a snapshot? We open blob, tree and commit objects with git cat-file, see where the hash comes from, and talk about how to write a good commit message and about git commit --amend.