Your First Git Repository: A Complete Hands-On Tutorial
This lesson builds a real Git repository from an empty directory, one command at a time. By the end you will have three commits, a readable history, and — more importantly — an accurate account of what each command did to the working tree, the index and the object database.
Every command and every output shown here was actually run. Your object IDs will differ from the ones printed below, because they are derived partly from your name, email and the current time. Everything else should match.
What you will build
Section titled “What you will build”A tiny project called hello-git with two files and three commits. The point is not the project — it
is watching state move through Git’s three areas and learning to read what Git tells you.
Step 1 — Create a project directory
Section titled “Step 1 — Create a project directory”mkdir hello-gitcd hello-gitmkdir hello-gitcd hello-gitNothing Git-related has happened yet. This is an ordinary empty folder.
Step 2 — Initialise the repository
Section titled “Step 2 — Initialise the repository”git initWhat it doesCreates a .git subdirectory containing an empty object database, an empty index, a config file, and a HEAD pointing at your initial branch.
Why we run itThis is the single step that turns an ordinary folder into a Git repository. Everything after it depends on .git existing.
Expected resultOne line confirming initialisation and the absolute path of the new .git directory.
Initialized empty Git repository in /home/you/hello-git/.git/If you have not set init.defaultBranch, Git also prints a hint about the initial branch name being
master. That hint is harmless, but setting the config removes it and matches modern convention:
git config --global init.defaultBranch mainYour project directory now looks like this:
Directoryhello-git/
Directory.git/ created by
git init— this is the repository- HEAD
- config
Directoryobjects/
- …
Directoryrefs/
- …
- …
Step 3 — Inspect the initial state
Section titled “Step 3 — Inspect the initial state”git statusWhat it doesCompares the working tree, the index and HEAD, then reports the differences in plain language.
Why we run itIt is the cheapest and most informative command in Git. Run it constantly — especially before add and before commit.
Expected resultYour branch name, a note that there are no commits yet, and confirmation there is nothing to commit.
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)Read that carefully — it is telling you three separate things:
On branch main— HEAD points at a branch calledmain.No commits yet— the branch does not point at a commit, because none exists. The branch ref will not exist as a file until the first commit is made.nothing to commit— the working tree is empty, so there is nothing to record.
Step 4 — Create a file
Section titled “Step 4 — Create a file”printf '# Hello Git\n\nA small project for learning Git from the ground up.\n' > README.md"# Hello Git`n`nA small project for learning Git from the ground up." | Set-Content README.mdYou can equally create the file in a text editor. The content does not matter.
Step 5 — See how Git reacts
Section titled “Step 5 — See how Git reacts”git statusOn 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)README.md is untracked: it exists in your working tree, but Git has no record of it in the index
or in any commit. Git is reporting it, not managing it.
Step 6 — Stage the file
Section titled “Step 6 — Stage the file”git add README.mdWhat it doesReads the current content of README.md, stores it in the object database as a blob, and points that file's index entry at the new blob.
Why we run itCommitting records the index, not the working tree. Staging is how you choose exactly what the next commit contains.
Expected resultNo output. Silence means success — a Unix convention Git follows throughout.
Confirm the effect:
git statusOn branch main
No commits yet
Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: README.mdThe file moved from Untracked files to Changes to be committed. It is now staged: its content sits in the index, ready to be committed.
Step 7 — Make your first commit
Section titled “Step 7 — Make your first commit”git commit -m "Add project README"What it doesBuilds tree objects from the index, writes a commit object recording that tree plus your identity, timestamp and message, and points the current branch at the new commit.
Why we run itThis is the operation that writes a permanent snapshot into history. Until you commit, nothing is recorded.
Expected resultA summary line with the branch name, the abbreviated commit ID, and your message, followed by a count of files and lines changed.
[main (root-commit) 695394d] Add project README 1 file changed, 3 insertions(+) create mode 100644 README.mdThat output is dense. Every part means something:
| Fragment | Meaning |
|---|---|
[main | The branch this commit was made on |
(root-commit) | This commit has no parent — it is the first in the repository |
695394d] | The abbreviated object ID of the new commit |
Add project README | Your commit message |
1 file changed, 3 insertions(+) | The change relative to the (empty) parent state |
create mode 100644 README.md | A new file was added, with regular non-executable permissions |
You will only ever see (root-commit) once per repository.
Step 8 — Read the history
Section titled “Step 8 — Read the history”git logWhat it doesWalks the commit graph backwards from HEAD, printing each commit's full ID, author, date and message.
Why we run itIt is how you read what has happened in a repository. Every commit's metadata is stored locally, so this needs no network.
Expected resultOne entry per commit, newest first. With one commit, one entry.
commit 695394d1a4e0b6f0c9b0f8e0d1c2b3a4e5f60718 (HEAD -> main)Author: Ada Lovelace <ada@example.com>Date: Sat Aug 22 13:20:41 2026 +0000
Add project README(HEAD -> main) tells you that HEAD points at the branch main, and main points at this commit.
That is the reference chain from Lesson 3
displayed in one line.
For a compact view:
git log --oneline695394d Add project READMEStep 9 — Confirm a clean state
Section titled “Step 9 — Confirm a clean state”git statusOn branch mainnothing to commit, working tree clean“Working tree clean” means the working tree, the index and the HEAD commit all agree. All three copies of your project are identical.
Three stacked boxes labelled working tree, index and repository. All three contain the same file, README.md, with the same content hash, showing that after committing there are no differences between them. A label reads: git status reports nothing to commit, working tree clean.
Step 10 — Add a second file and commit it
Section titled “Step 10 — Add a second file and commit it”Create another file:
printf 'Sun\nMon\nTue\n' > days.txt"Sun`nMon`nTue" | Set-Content days.txtStage and commit it in one go:
git add days.txtgit commit -m "Add list of days"[main c4dfb0b] Add list of days 1 file changed, 3 insertions(+) create mode 100644 days.txtNote what is missing this time: no (root-commit). This commit has a parent — the first commit — and
history is now a chain of two.
Step 11 — Modify a tracked file and inspect the change
Section titled “Step 11 — Modify a tracked file and inspect the change”Replace the README’s content with a longer version:
printf '# Hello Git\n\nA small project for learning Git from the ground up.\n\n## What this is\n\nA scratch repository used to practise the Git basics.\n' > README.md"# Hello Git`n`nA small project for learning Git from the ground up.`n`n## What this is`n`nA scratch repository used to practise the Git basics." | Set-Content README.mdNow check the status:
git statusOn branch mainChanges 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
no changes added to commit (use "git add" and/or "git commit -a")The file is modified, not untracked — Git already knows about it and can compare versions.
git diffWhat it doesShows the difference between your working tree and the index — that is, changes you have made but not yet staged.
Why we run itIt answers “what exactly did I change?” before you commit. Reviewing the diff is the most reliable way to avoid committing something unintended.
Expected resultA unified diff. Lines prefixed with + were added, lines prefixed with - were removed, and unprefixed lines are unchanged context.
diff --git a/README.md b/README.mdindex 42e11db..4d4fdc0 100644--- a/README.md+++ b/README.md@@ -1,3 +1,7 @@ # Hello Git
A small project for learning Git from the ground up.++## What this is++A scratch repository used to practise the Git basics.Reading the header pays off:
a/README.mdis the “before” version (the index);b/README.mdis the “after” (the working tree).index 42e11db..4d4fdc0gives the abbreviated blob IDs of the two versions. Those are real objects — the first is stored in the repository, the second exists only in your working tree so far.@@ -1,3 +1,7 @@is the hunk header: 3 lines starting at line 1 in the old version correspond to 7 lines starting at line 1 in the new.
For a summary rather than the full text:
git diff --stat README.md | 4 ++++ 1 file changed, 4 insertions(+)Step 12 — Stage and commit the change
Section titled “Step 12 — Stage and commit the change”Before staging, run the experiment promised in Step 6. Stage the file, then check status:
git add README.mdgit statusOn branch mainChanges to be committed: (use "git restore --staged <file>..." to unstage) modified: README.mdNow the two diff commands answer different questions:
git diff --stagedWhat it doesShows the difference between the index and HEAD — exactly what the next commit will contain.
Why we run itBare git diff shows only unstaged changes, so after staging it prints nothing. This is the command that reviews a commit before you make it.
Expected resultThe same diff you saw before staging, because the change moved from the working tree into the index.
Running plain git diff now produces no output, because the working tree and index agree. That is
correct behaviour, and it surprises nearly everyone the first time.
Commit:
git commit -m "Describe the project in the README"[main 2b1e0f8] Describe the project in the README 1 file changed, 4 insertions(+)No create mode line this time — no new file was added, an existing one was modified.
Step 13 — Read the resulting history
Section titled “Step 13 — Read the resulting history”git log --oneline --graph* 2b1e0f8 Describe the project in the README* c4dfb0b Add list of days* 695394d Add project READMEThree commits, newest first. --graph draws the branch structure; with a single linear branch it is
just a column of asterisks, but it becomes essential once branches diverge.
To see what changed in a single commit:
git show --stat HEAD2b1e0f8 Describe the project in the README README.md | 4 ++++ 1 file changed, 4 insertions(+)Drop --stat to see the full diff for that commit.
What you actually accomplished
Section titled “What you actually accomplished”You built a three-commit history, but the durable part is the mechanism you watched:
-
git initcreated a repository. An empty object database, an empty index, and a HEAD pointing atmain. -
Creating a file changed only the working tree. Git noticed and reported it as untracked.
-
git addwrote a blob and updated the index. The content — not the name — was recorded. -
git commitbuilt trees from the index and wrote a commit object, then movedmainforward to point at it. -
git statusreported the differences between the three areas at every step, and every message it printed maps to one of those differences. -
git diffandgit diff --stagedcompared different pairs of those areas — which is why they showed different things at different moments.
Each of the three commits is now a permanent, content-addressed snapshot in .git/objects, connected
to its parent, reachable from main, and reachable from HEAD through main.
Keeping files out of the repository
Section titled “Keeping files out of the repository”Real projects contain files that should never be committed: build output, dependency directories,
editor settings, and — critically — anything holding a secret. A .gitignore file tells Git to stop
reporting them.
Create one at the top of the repository:
# Build outputbuild/
# Local environment configuration.envWatch the effect. Before the .gitignore exists, git status --short reports both:
?? .env?? build/Afterwards, they disappear from the listing entirely, and the only untracked file is .gitignore
itself:
?? .gitignoreTo confirm Git is deliberately ignoring them rather than failing to see them, ask explicitly:
git status --short --ignored?? .gitignore!! .env!! build/The !! prefix marks ignored paths.
Commit the .gitignore itself — it is project configuration that every contributor should share:
git add .gitignoregit commit -m "Ignore build output and local environment file"Undoing things at this stage
Section titled “Undoing things at this stage”Two situations come up constantly in the first week. Both have safe answers.
You staged something you did not mean to. Unstage it without touching the file on disk:
git restore --staged days.txtWhat it doesCopies the file's content from HEAD back into the index, leaving the working tree untouched.
Why we run itIt reverses a git add. Your edits are preserved — only the staging decision is undone.
Expected resultNo output. git status --short then shows the file as modified but no longer staged: the status code moves from the first column to the second.
You want to throw away an uncommitted edit. This one destroys work, so read the warning:
git restore days.txtCommon mistakes
Section titled “Common mistakes”Running git commit without staging. If nothing is staged, Git tells you so and makes no commit.
Stage first, or use git commit -a to automatically stage tracked, modified files. -a never stages
untracked files.
Using git add . reflexively. It stages everything under the current directory, including files
you did not mean to include — build output, local configuration, or credentials. Run git status
first. A .gitignore file is the durable fix.
Committing without reading the diff. git diff --staged takes two seconds and catches debugging
statements, stray edits and accidental secrets before they enter history.
Expecting git diff to show everything. Bare git diff shows unstaged changes only. Use
git diff HEAD for everything since the last commit.
Vague commit messages. “update”, “fix”, “changes” tell a future reader nothing. The message is the only place the why can live.
Assuming a commit is shared. Everything in this tutorial was local. Nothing left your machine, and this repository has no remote at all.
Try It Yourself
Section titled “Try It Yourself”Do this in the same disposable repository. Predict each answer before running the command.
- Create two new files,
notes.txtandscratch.txt. - Stage only
notes.txt. - Predict what
git statuswill report about each file, then run it. - Now edit
notes.txtagain, without staging it. - Predict what
git statuswill show fornotes.txt, then run it. - Run
git diffandgit diff --stagedand explain why they differ. - Commit, then run
git log --onelineto see your fourth commit.
Step 5 is the payoff: notes.txt appears under both “Changes to be committed” and “Changes not
staged for commit”, because the index and the working tree hold two different versions of it. If that
makes sense, you have understood the index.
Cleaning up
Section titled “Cleaning up”Delete the practice directory whenever you like:
cd ..rm -rf hello-gitcd ..Remove-Item -Recurse -Force hello-gitWhat You Learned
Section titled “What You Learned”git initturns a directory into a repository by creating.git.- Files are untracked until staged, then staged, then committed.
git addrecords file content into the index;git committurns the index into a permanent snapshot.git statusreports the differences between the working tree, the index and HEAD.git diffcompares working tree to index;git diff --stagedcompares index to HEAD.- Commit output encodes real information: branch, root-commit status, object ID and file mode.
git log,git log --onelineandgit showread history — all locally, with no network.
Next Lesson
Section titled “Next Lesson”You have now used all three areas. The next four lessons take them one at a time and go deeper: the working tree, the index, HEAD, and finally the object database that stores it all.