skip to content

Why can't you commit an empty directory in Git?

level: juniorimportance: should knowfreq 42%

answer

  1. think about what a tree entry can name
  2. blobs have no path of their own
  3. nothing left to hash
  4. the fix is a convention, not a feature

basics

~20 s

Git records files, not directories. A directory exists only as a tree object listing entries, and each entry must name a blob or a subtree, so a folder with nothing in it has nothing to record.

solid answer

~40 s

Git's snapshot is built from **tree** objects, and a tree entry is a mode, a name, and the hash of either a blob (a file) or another tree. There is no entry type that means "empty directory", and the index only ever lists file paths. So a directory appears in history purely as a side effect of containing at least one tracked file, and Git recreates it on checkout only because it has to place files inside it. The usual workaround is a placeholder file — commonly named `.gitkeep`, which is a convention with no special meaning to Git, or a `.gitignore` inside the folder. Deleting the last tracked file in a directory also makes Git remove the now-empty directory from the working tree.

code

console · 2 lines
console
$ git cat-file -p HEAD^{tree}
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	f.txt

go deeper

for a junior

Recall the one-liner: Git snapshots files, and a directory only shows up because a tracked file lives in it. Know the .gitkeep placeholder trick and that it is a convention.

for a middle

Explain the mechanics: a tree entry is mode plus name plus object hash, and the index is a flat list of file paths, so there is no representation for an empty directory.

for a senior

Show judgment about the workaround: prefer creating runtime directories in the application or build rather than committing placeholders that later rot, and know that Git prunes emptied directories on checkout.

for a principal

Frame it as a repository-hygiene rule: placeholder files are unowned artifacts, so bake required directory structure into build or deployment automation instead of relying on version control to carry it.

## Git tracks content, not folders A Git commit points at exactly one **tree** object, which represents the root directory. A tree is a list of entries, and each entry has three parts: a mode, a name, and the hash of the object it points at. That object is either a **blob** (the raw bytes of one file version) or another tree (a subdirectory). Nothing else can appear. Because there is no "directory with no children" representation, a folder can only exist in a commit as the parent of something tracked. ## What a tree entry looks like Printing a tree shows the shape clearly: `100644 blob <hash> README.md` for a regular file, `100755` for an executable file, `120000` for a symlink, `040000 tree <hash>` for a subdirectory, and `160000 commit <hash>` for a submodule reference. Note what is *not* there: no owner, no group, no ordinary Unix permission bits beyond the executable flag, and no modification time. Git deliberately records only what it can reproduce portably. ## The index agrees The staging area (the index) is likewise a flat list of file paths with their blob hashes and mode bits. `git add somedir/` adds the files it finds underneath; if it finds none, nothing is staged and `git status` shows nothing to commit. There is no command that stages a directory as an entity. ## Practical consequences A build that requires `logs/` or `tmp/` to exist will not get it from a clone. Teams solve this in three ways: commit a placeholder file (`.gitkeep` is a pure naming convention — Git gives that filename no special treatment); commit a real file the directory needs anyway, such as a `.gitignore` that ignores the directory's generated contents while keeping itself tracked; or create the directory at application or build startup, which is usually the cleanest. The reverse also surprises people: when you delete or move the last tracked file out of a directory, Git prunes the empty directory from your working tree on the next checkout, because from Git's point of view that directory never existed. ## In an interview The expected answer is one sentence about trees and blobs, followed by the placeholder-file convention. Saying "you use `.gitkeep`" alone is a weak answer, because it does not explain *why* the workaround is needed, and it invites the follow-up "is `.gitkeep` a Git feature?" — it is not.

  • Is `.gitkeep` a feature of Git?
    No. Git assigns that filename no special meaning; it is purely a community convention for "this file exists only so the directory does". Any tracked file works, and a `.gitignore` placed inside the directory is often better because it does real work as well as holding the directory open.
  • What file metadata does a tree entry actually preserve?
    The name, the object hash, and a mode that distinguishes a regular file, an executable file, a symlink, a subdirectory, and a submodule reference. Ordinary permission bits, ownership and timestamps are not stored — which is why a checkout gives everyone the same result regardless of the machine that made the commit.

saying these in an interview costs you the question

  • Claims Git stores directories but hides empty ones
  • Says .gitkeep is a built-in Git feature
  • Thinks git add on a folder stages the folder itself
  • Believes file ownership and mtime are recorded in the tree

context