We closed module 1 with Ana's laptop ready to go: Git installed, her identity configured, init.defaultBranch set to main and the editor sorted. All the scaffolding was in place, but task-manager was still exactly what it had been on day one: an ordinary folder with four files inside it. No history, no versions, no way back.
This lesson takes the step that changes everything. With a single command, git init, that folder becomes a repository: Git starts watching it, the .git/ directory appears with the object database we studied in The Git Data Model, and from then on every change can be recorded for good.
We will see exactly what git init creates, what git status says in a brand-new repository (and why it talks about "no commits yet"), how the project's first commit is made, how starting from scratch differs from converting a project that already exists, and how to undo a git init fired off in the wrong place — something that happens to nearly everyone sooner or later.
Contents
- The starting point: Ana's folder
git init: the command that creates the repository- What it actually creates inside
.git/ git statusin a brand-new repository- What "No commits yet" means
- The project's first commit
- Converting an existing project versus starting from scratch
- Bare repositories: what they are and what they are for
- How to undo an accidental
git init - Useful checks before moving on
- The starting point: Ana's folder
Ana works on Ubuntu and keeps her project in ~/projects/task-manager. She wrote it over a couple of evenings and it contains four files:
total 16 -rw-rw-r-- 1 ana ana 486 Jul 20 18:12 app.js -rw-rw-r-- 1 ana ana 602 Jul 20 18:04 index.html -rw-rw-r-- 1 ana ana 198 Jul 20 18:20 README.md -rw-rw-r-- 1 ana ana 312 Jul 20 17:55 styles.css
The content is straightforward. index.html builds the page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Task Manager</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Task Manager</h1>
<form id="new-task">
<input type="text" id="text" placeholder="What needs to be done?">
<button type="submit">Add</button>
</form>
<ul id="list"></ul>
<script src="app.js"></script>
</body>
</html>And app.js holds the bare minimum logic for adding tasks:
// task-manager — main logic
const tasks = [];
function addTask(text) {
tasks.push({ id: Date.now(), text: text, done: false });
renderList();
}
function renderList() {
const list = document.querySelector('#list');
list.innerHTML = '';
for (const task of tasks) {
const li = document.createElement('li');
li.textContent = task.text;
list.appendChild(li);
}
}
document.querySelector('#new-task').addEventListener('submit', function (event) {
event.preventDefault();
const field = document.querySelector('#text');
if (field.value.trim() !== '') {
addTask(field.value.trim());
field.value = '';
}
});These files will stay with us for the whole course. Notice one detail that will matter later: there is no hidden folder anywhere. Let us check:
Only . (the current directory) and .. (its parent). Not a trace of .git. This is what Git accurately calls a directory that is not under version control. If Ana deleted app.js right now, nothing could be done about it.
git init: the command that creates the repository
git init: the command that creates the repositoryAna moves into the project folder and runs:
That is it. Nothing more. Let us take that message apart slowly, because every word carries information:
Initialized: the structure has been created from scratch.empty: the repository is empty in the sense that it contains no commits. Careful, though: Ana's four files are still sitting there untouched. "Empty" refers to the history, not to the folder.- The path
/home/ana/projects/task-manager/.git/: it tells you precisely where the repository has landed. It is always worth reading, because it is the fastest way to spot that you ran the command in the wrong place.
The two ways of invoking it
| Form | What it does | When to use it |
|---|---|---|
git init |
Turns the current directory into a repository | You already have a folder with files in it (Ana's case) |
git init <name> |
Creates the directory <name> if it does not exist and initialises it |
You are starting a project from scratch |
The second form saves a step. If Ana were starting a new project called team-notes:
Git created the folder and initialised it, but put nothing inside: the project is still to be written.
Options worth knowing about
# Force the name of the initial branch in this particular repository
git init --initial-branch=main
git init -b main # short form
# Initialise with minimal output
git init --quietAna does not need -b main because she already set init.defaultBranch=main in her global configuration back in Initial Configuration. But if you work on a team where not everyone has it configured, git init -b main guarantees the same starting point for everybody.
A note about older versions. The
-b/--initial-branchoption has existed since Git 2.28. Before that the initial branch was always calledmaster, with no way to change it at creation time. If yourgit --versionis below 2.28, this is a good reason to upgrade.
- What it actually creates inside
.git/
.git/Now let us look at what has appeared:
The only visible change is the .git folder. Let us look inside:
This connects straight back to what we saw in The Git Data Model. Let us go over each piece in a freshly initialised repository:
| Item | What it is | State when just created |
|---|---|---|
HEAD |
Reference to the current branch | Points at refs/heads/main, which does not exist yet |
config |
Configuration at the --local level |
Only a handful of default values |
description |
Repository name for GitWeb | Filler text; irrelevant today |
objects/ |
The object database | Empty: no blobs, no trees, no commits |
refs/ |
Branches (heads/) and tags (tags/) |
Both subdirectories empty |
hooks/ |
Scripts triggered by certain events | Only disabled samples (.sample) |
info/ |
Auxiliary information, such as exclude |
Practically empty |
branches/ |
Legacy directory, no longer used | Empty; you can ignore it |
Notice what is not there: there is no index file. The staging area is a binary file (.git/index) that Git creates the first time you stage something. Before that, it simply does not exist.
Looking at the key files
HEAD is a one-line text file:
It says: "the current branch is main". Right now, though, that branch is a promise, not a fact:
The directory is empty. main does not exist as a reference file because a branch is, literally, a file containing the hash of a commit, and there is no commit yet for it to point at. This situation — HEAD pointing at a branch that does not exist — is called an unborn branch, and it resolves itself the moment the first commit is created.
The local config is short too:
Four technical values and nothing else. Remember from Configuring Git that this file is the --local level, the one with the highest precedence: Ana's identity is not here because she set it at --global, and from there it applies just the same.
And the object database is strictly empty:
Two empty subdirectories waiting for the first blob. This illustrates the underlying idea nicely: git init does not save any of your files. It only prepares the store. Saving is the job of git add and git commit.
git status in a brand-new repository
git status in a brand-new repositorygit status is the command you will type more often than any other in your life. Let us see what it says now:
On branch main No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) README.md app.js index.html styles.css nothing added to commit but untracked files present (use "git add" to track)
Four blocks of information, and every one of them matters:
On branch main— which branch you are on. It matches what we read in.git/HEAD.No commits yet— the history is empty. We unpack this in the next section.Untracked files— the files without tracking. Git can see them in the working tree, but it has never recorded them, so they are not part of the project yet. This is the state we introduced in Basic Git Terminology.- The last line — a summary: nothing is staged, but there are untracked files.
Note how Git suggests the next command in brackets: use "git add <file>...". Git is relentlessly didactic in its output. Reading those hints instead of skipping past them flattens the learning curve enormously.
Tip. If those hints start to feel like noise once the workflow is second nature, you can turn them down with
git config --global advice.statusHints false. At the beginning, leave them on.
- What "No commits yet" means
It is a phrase that confuses beginners because it sounds like "you have done nothing", when in fact it carries a very precise technical meaning:
HEAD points at the branch main, but main points at no commit.
It is a transient and perfectly valid state, but it has practical consequences. Many commands need a commit to work from and fail until the first one exists:
You have done nothing wrong: HEAD simply resolves to nothing. As soon as there is a commit, all of these commands will behave normally.
We can see it with the plumbing we learned in module 1:
This diagram sums up the state of Ana's repository at this moment:
graph LR
HEAD["HEAD"] -->|ref: refs/heads/main| MAIN["main<br/>(does not exist yet)"]
MAIN -.->|will point to| C["first commit<br/>(not created yet)"]
OBJ["objects/<br/>empty"]
WT["Working tree<br/>4 untracked files"]
style MAIN stroke-dasharray: 5 5
style C stroke-dasharray: 5 5
- The project's first commit
Let us close the loop. In The Basic Git Workflow, and above all in Staging and Committing Changes, we will go through these commands in full detail; here we use them in their simplest form so that the repository stops being empty.
Step 1: stage the four files.
The command prints nothing. In Git, silence means success. Let us check the effect:
On branch main No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: README.md new file: app.js new file: index.html new file: styles.css
The four files have moved from Untracked files to Changes to be committed: they are in the staging area. Each one is marked as new file because they existed in no previous commit (there are none).
And now the index does exist:
And the database now has content:
Four objects: one blob per file. There is still no tree and no commit; those are created by git commit.
Step 2: commit.
[main (root-commit) 1a4c8d6] Add initial task manager structure 4 files changed, 68 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 index.html create mode 100644 styles.css
Let us read that first line, which is the most informative one:
[main— the branch the commit was made on.(root-commit)— this marker appears only on the first commit of a history: it is a commit with no parent. Remember from the data model that a normal commit stores the hash of its parent; this one has none.1a4c8d6— the abbreviated hash of the new commit.- The message we wrote.
The lines that follow summarise what went in: 4 files, 68 lines added, and mode 100644 (a regular file) for each of them, exactly the notation we saw when studying trees.
Step 3: check the result.
Both No commits yet and the list of files are gone. working tree clean means the working tree matches the last commit exactly: there is nothing outstanding. It is the most reassuring message Git produces.
And the branch now genuinely exists:
A file with a hash inside it. That is what a branch is in Git: no more and no less.
HEAD -> main tells us we are sitting on the branch main and that the branch points at this commit. Ana's project now has a history.
- Converting an existing project versus starting from scratch
Both scenarios use the same command, but the precautions differ.
| Starting from scratch | Converting an existing project | |
|---|---|---|
| Typical command | git init <name> |
cd project && git init |
| Initial state | Empty folder | Files already there, untracked |
| Main risk | None | Committing junk: dependencies, secrets, binaries |
| Recommended first step | Create a README.md |
Write a .gitignore before the first git add |
| First commit | Almost empty | The whole project in one go |
The risk in the second case is real and very common. Imagine Ana had installed dependencies before initialising the repository:
If she ran git add . without thinking, she would push thousands of node_modules/ files into the history (files that a single command reinstalls and that add nothing) and, far worse, .env with her keys in it. And as we learned in The Git Data Model, history is immutable: getting a file full of secrets out of it is not "deleting" it, it means rewriting the history and rotating the keys.
The rule is simple: in an existing project, write the .gitignore before the first git add. A minimal .gitignore for Ana's case:
With that in place, git status goes back to showing only what matters. We will cover patterns, exceptions and the awkward cases in Ignoring Files with .gitignore; for now, hold on to the idea that this file exists and that its place is before the first commit.
One important detail: existing files are not lost
It is worth saying out loud because it causes real anxiety: git init does not touch your files. It does not move them, modify them or delete them. All it creates is .git/. If you run git init in a folder holding months of work, that work is exactly as it was a second later. The only thing that changes is that now you can start versioning it.
- Bare repositories: what they are and what they are for
There is a variant you will see mentioned constantly:
A bare repository is a repository with no working tree. It does not hold index.html or app.js as files you can work on: it holds nothing but the object database and the references — that is, the contents of what in a normal repository would be .git/, but sitting directly at the root.
Look familiar? It is exactly the same content we saw inside .git/, but without the containing folder and without the project files alongside it.
| Normal repository | Bare repository | |
|---|---|---|
| Working tree | Yes | No |
| Can you edit files | Yes | No |
| Can you commit in it | Yes | Not directly |
| Naming convention | task-manager |
task-manager.git |
| Typical use | Your working copy | Shared central server |
What it is for: it is the correct way to set up the "central" repository that the whole team pushes to and pulls from. When you clone from GitHub or GitLab, what sits on the other side is a bare repository. It is done this way because, if the central repository had a working tree, receiving changes could leave it in a state inconsistent with whatever its files contained.
We will not take this any further here: bare repositories start to make sense once we talk about remotes in module 4. Hold on to the definition — a repository with no working copy, meant for sharing — and to the fact that in the next lesson Bruno will clone from precisely one of them.
- How to undo an accidental
git init
git initIt happens to everybody: you run git init, look at the message and discover a path you were not expecting. The classic case:
You have just turned your entire home folder into a repository. git status will try to list tens of thousands of untracked files and, worse still, any project you have inside it will start behaving oddly, because Git looks for the nearest .git on the way up and now it finds that one.
The fix is reassuringly simple: delete the .git folder.
On Windows, from PowerShell:
That is all. Since git init had done nothing beyond creating that folder, deleting it puts the system back exactly as it was. Your files are not affected in the slightest.
Precautions before deleting
This is where you want to tread carefully, because rm -rf .git is harmless in a freshly initialised repository and catastrophic in one with a history: you would wipe out every commit, branch and tag, with no way to get them back (unless a copy exists somewhere else). Before running it, ask yourself three questions:
1. Am I where I think I am?
2. Which repository am I about to delete?
This command returns the root of the current repository, which is precisely what you are about to remove. If the path is not the one you expected, delete nothing yet.
3. Does it have a history?
If it answers does not have any commits yet, deleting is safe. If it lists commits, stop and work out which repository it is before touching anything.
How to avoid it next time
- Always read the path in the
Initialized...message. One second of attention avoids the whole problem. - Prefer
git init <name>when starting from scratch: it creates the folder itself, so you cannot get the location wrong. - Check before initialising whether you are already inside a repository:
In this case that fatal is the answer you want: it means you are not inside any repository and that git init really will create a new one.
A warning about nested repositories. If you run
git initinside a folder that is already under version control, Git does not complain: you end up with a repository inside another repository. The inner one is invisible to the outer one and it usually ends in confusion and lost work. If you genuinely need to nest projects, the right tool is submodules, not a hand-rolledgit init.
- Useful checks before moving on
A handful of diagnostic commands worth keeping to hand:
# Am I inside a repository?
git rev-parse --is-inside-work-tree # → true
# What is the root of the repository?
git rev-parse --show-toplevel # → /home/ana/projects/task-manager
# Where is the .git directory?
git rev-parse --git-dir # → .git
# Is it a bare repository?
git rev-parse --is-bare-repository # → false
# Which branch am I on?
git branch --show-current # → main
# Which identity will I commit with here?
git config user.name && git config user.emailThat last one is especially valuable when you use conditional profiles with includeIf, as we saw at the end of Configuring Git: it confirms which identity will apply in this specific repository before you create a commit with the wrong address on it.
Common Mistakes and Tips
- Running
git initwithout looking at where you are. This is mistake number one. Read the path in the confirmation message; if it is not the one you expected, delete.gitimmediately, before adding anything. - Believing
git initalready saves your files. It saves nothing. A freshly initialised repository has an empty database and every one of your files in the untracked state. Until the firstgit committhere is no backup of anything. - Running
git add .as your first command in an existing project. Without a.gitignoreyou will drag in dependencies, temporary files, build artefacts and, in the worst case, secrets. Write the.gitignorefirst and reviewgit statusbefore staging. - Confusing "empty repository" with "empty folder". The message
Initialized empty Git repositoryalarms plenty of people who fear they have lost their work. "Empty" refers to the history, not to the files. - Initialising a repository inside another one. Git allows it silently and the result is baffling. Check with
git rev-parse --show-toplevelfirst. - Committing nothing for days on end. A repository with no commits protects you from nothing. The first commit, imperfect as it may be, is already a safety net.
- Tip: get into the habit of naming a project's first commit something like "Add initial project structure". It is the most widespread convention and it helps you spot the start of a history at a glance.
- Tip: if you are unsure whether a folder is a repository,
ls -aon Linux/macOS ordir /aon Windows tells you in a second: if.gitshows up, it is.
Exercises
Exercise 1: Create a repository from scratch and observe it
Create a new repository called team-notes without using mkdir, and answer these four questions with commands — not from memory:
- Which branch does
HEADpoint at? - Does that branch already exist as a file in
refs/heads? - How many objects are there in the database?
- Does the file
.git/indexexist?
Then create a README.md with one line of text in it, make the first commit and answer the four questions again. Explain what has changed and why.
Exercise 2: Convert an existing project without dragging in junk
Set up this starting situation:
mkdir -p ~/practice/online-shop/node_modules/library
cd ~/practice/online-shop
echo "<h1>Shop</h1>" > index.html
echo "body { margin: 0; }" > styles.css
echo "API_KEY=abc123secret" > .env
echo "noise" > node_modules/library/index.js
echo "thursday's error" > debug.logTurn the folder into a repository and make a first commit containing only index.html, styles.css and the .gitignore. Use the output of git status to prove that .env, node_modules/ and debug.log do not appear even as untracked files.
Exercise 3: Diagnose and repair an accidental git init
A colleague writes to you: "I ran git init somewhere, I don't know where, and now git status takes forever and lists thousands of files that have nothing to do with my project. On top of that, when I go into my ~/projects/client-api folder and run git log, it tells me there are no commits, when I know for a fact I have twenty."
- What has happened, exactly?
- Which commands should they run to confirm the diagnosis before touching anything?
- How do they fix it without losing the history of
client-api? - Why did
git loginsideclient-apisay there were no commits if that project's.gitwas still intact?
Solutions
Solution to Exercise 1
Initial state:
cat .git/HEAD
# → ref: refs/heads/main
ls .git/refs/heads
# → (empty)
find .git/objects -type f | wc -l
# → 0
ls .git/index
# → ls: cannot access '.git/index': No such file or directoryAnswers: (1) HEAD points at refs/heads/main; (2) no, the branch does not exist yet; (3) zero objects; (4) no, there is no index.
After the first commit:
[main (root-commit) 3f7b2e9] Add initial project structure 1 file changed, 1 insertion(+) create mode 100644 README.md
cat .git/HEAD
# → ref: refs/heads/main (unchanged)
cat .git/refs/heads/main
# → 3f7b2e9c1a5d8b4f2e6a9c3d7b1f5e8a4c2d9b6f (now it does exist)
find .git/objects -type f | wc -l
# → 3
ls .git/index
# → .git/index (now it does exist)What changed and why:
HEADis the same. It always pointed atmain; what was missing was the branch, not the reference.mainnow exists as a file with a hash inside it: the commit gave the branch something to point at. Here you can see literally that a branch is a text file with a hash in it.- There are 3 objects, not 1. They are the three objects the data model of 01-04 demands: a blob with the content of
README.md, a tree for the root directory (one entry: the nameREADME.mdand the blob's hash) and a commit that points at that tree and has no parent. .git/indexexists becausegit addcreated it when staging the file.
Solution to Exercise 2
The order is what solves the exercise: .gitignore first, git add afterwards.
Before staging anything, we write the exclusions:
Check:
.env, node_modules/ and debug.log have vanished from the list: Git sees them on disk but ignores them, so it does not even count them as untracked. That is the proof the exercise asked for.
Now it is safe to stage everything:
[main (root-commit) 8c2f5a1] Add initial shop structure 3 files changed, 5 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 styles.css
Three files, all three of them the right ones.
Had anyone run git add . before creating the .gitignore, the .env file would already be in the staging area and adding the .gitignore afterwards would not take it out: .gitignore only affects files without tracking. It would have to be removed from the index explicitly, something we will see in Staging and Committing Changes. And if it had already been committed, the secret would sit in the history permanently and the key would have to be rotated.
Solution to Exercise 3
1. What has happened. They ran git init in their home directory (~) or in ~/projects — that is, in a folder above their projects. There is now a .git at that upper level.
Both symptoms explain themselves:
git statuslists thousands of files because the repository spans the whole home folder and everything in it is untracked.- Inside
client-apithey see an empty history because… they are not actually seeing it. See point 4.
2. Confirm the diagnosis without modifying anything:
cd ~/projects/client-api
git rev-parse --show-toplevel
# → /home/colleague ← the root is NOT client-api: there is the problem
git rev-parse --git-dir
# → /home/colleague/.git
ls -d ~/.git
# → /home/colleague/.git ← it exists, and it should notAnd check that the accidental repository really is empty before deleting it:
That fatal is the green light: there is no history to lose.
3. The repair:
And verify that everything is back where it belongs:
cd ~/projects/client-api
git rev-parse --show-toplevel
# → /home/colleague/projects/client-api
git log --oneline | head -3
# → their twenty commits, intact4. Why git log was failing. Because Git looks for the .git directory by walking up the directory tree from wherever you are, and uses the first one it finds. You would expect the first one to be the one in client-api… and it was. Here is the crux of the scenario: the colleague was not inside client-api when they ran git log, or else their project's .git did not exist because they never created one there and had been working, unknowingly, against the upper-level repository.
This nuance is what makes an accidental git init dangerous: the .git above does not hide the one below, but it does capture any command you run in folders that have no repository of their own. If the colleague had spent days committing from ~ in the belief that the commits were going to client-api, their entire history would be inside ~/.git and deleting it with rm -rf would destroy it. Hence the insistence in section 9: always check git log --oneline before deleting. Had it listed commits, the fix would not be to delete but to move that .git somewhere safe and recover the work from it.
Conclusion
Ana's project is no longer a folder: it is a repository. In this lesson we have seen that:
git initturns the current directory into a repository, andgit init <name>creates the folder as well. Its only effect is to create.git/; it neither touches nor saves your files.- What it creates is the empty scaffolding we already knew from the data model:
HEADpointing atmain,objects/with no objects in it,refs/heads/with no branches and not even anindexfile. git statusis your compass from the very first second: it tells you the branch, that there are no commits yet and which files are untracked, always suggesting the next command.- "No commits yet" has a technical meaning:
HEADpoints at a branch that does not exist yet. That is whygit logandgit diff HEADfail until the first commit. - The first commit is a
root-commit: the one commit in the history with no parent. Creating it brings the blobs, the tree and the commit into being, andrefs/heads/mainis born as a file with a hash inside it. - Converting an existing project calls for one extra precaution: write the
.gitignorebefore the firstgit add, because history is immutable and getting a secret out of it costs far more than never putting it in. - Bare repositories are repositories with no working copy, designed to serve as a shared central point.
- An accidental
git initis undone by deleting.git, but only after checking withgit rev-parse --show-toplevelandgit log --onelinethat the repository holds nothing of value.
Ana has her repository and her first commit. But a team project does not live on a single laptop: Bruno joins next week and needs his own copy, with the whole history, on his MacBook. git init is no use for that — it would create a new, empty repository with no relationship to Ana's — so a different command is needed.
In the next lesson, Cloning a Repository, we will see what git clone really does: how it copies the entire object database, creates the working tree, registers the origin remote and leaves Bruno with a history identical to Ana's. We will compare the available protocols (HTTPS, SSH and a local path), the most useful options such as --branch and --depth, and settle once and for all the conceptual difference between init and clone.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
