skip to content

In Git, what makes a repository bare, and why do servers host bare repositories?

level: middleimportance: must knowfreq 55%

answer

  1. Ask which of the two halves of a clone is missing
  2. No working tree means no index either
  3. Contents sit at the top level, not inside .git
  4. Pushing to a checked-out branch causes a mismatch
  5. There is a config key that refuses exactly that push

basics

~20 s

A bare repository has no working tree and no index: the contents normally found in .git sit directly in the repository directory, with core.bare set to true. Servers use them because nothing should be checked out there, and pushing to a checked-out branch is refused by default.

solid answer

~50 s

`git init --bare` creates a repository whose directory holds `HEAD`, `config`, `objects/`, `refs/`, `hooks/` and `info/` at the top level instead of nested inside a `.git` folder. There is no working tree, no `index`, and `core.bare` is `true` — by convention such a directory is named `something.git`. It exists purely to receive and serve objects and refs. That matters because a push updates refs on the remote but does not touch a working tree, so pushing to the branch that is currently checked out in a non-bare repository would leave the index and files disagreeing with HEAD. Git refuses that by default via `receive.denyCurrentBranch`. Bare repositories sidestep the problem entirely, which is why every server-side repository is bare. Server-side hooks still run there, and you can create one locally with `git clone --bare` or `git clone --mirror`.

code

console · 8 lines
console
$ git init --bare repo.git
Initialized empty Git repository in /srv/git/repo.git/
$ ls repo.git
HEAD  config  description  hooks  info  objects  refs
$ git -C repo.git config core.bare
true
$ git -C repo.git rev-parse --is-bare-repository
true

go deeper

for a junior

Know that a bare repository has no checked-out files and that it is what a server hosts; git init --bare is the command that creates one.

for a middle

Explain the layout difference, the absent index, core.bare, and why pushing into a non-bare repository's checked-out branch is refused by default.

for a senior

Discuss operating one: server-side hooks for policy, --mirror for migrations and backups, and the failure mode when someone pushes into a working repository anyway.

for a principal

Own the topology decision — where authoritative repositories live, how they are backed up and mirrored, and what policy is enforced at the receiving end rather than on developer machines.

## What "bare" actually means An ordinary clone has two parts: the repository (`.git`) and the working tree (your files). A **bare** repository is the first part without the second. Run `git init --bare repo.git` and list the directory: you see `HEAD`, `config`, `description`, `hooks/`, `info/`, `objects/` and `refs/` — exactly what would normally live inside `.git`, promoted to the top level. Two things are absent: the working tree and the `index`. There is nothing checked out, so there is nothing to stage. The repository's `config` records `core.bare = true`, which is how Git knows not to look for a working tree, and `git rev-parse --is-bare-repository` reports it. The `.git` suffix on the directory name (`project.git`) is a naming convention, not a mechanism. ## Why servers want it A push transfers objects and asks the receiving repository to update refs. It does **not** touch anyone's files. If the receiving repository were non-bare and you pushed to the branch it currently has checked out, its `HEAD` would move while its working tree and index stayed on the old commit — the next `git status` there would report a confusing mass of "changes" that are really the difference between two commits, and a careless `git reset --hard` would destroy work. Git protects against this: `receive.denyCurrentBranch` defaults to refusing such a push. Values include `refuse`, `warn`, `ignore` and `updateInstead`, the last of which makes the receiving repository update its working tree when it is clean. But the clean answer for anything acting as a shared hub is simply to have no working tree at all. There are secondary benefits: no working tree means less disk use and no risk of somebody editing files on the server, and it makes the repository's role unambiguous — it is storage, not a workspace. ## What still works in a bare repository Plenty. Object and ref inspection commands work normally: `git log`, `git show`, `git cat-file`, `git rev-parse`, `git for-each-ref`. Server-side hooks — `pre-receive`, `update`, `post-receive` — live in its `hooks/` directory and run on every push, which is where server-side policy is enforced. What does not work is anything that needs files on disk: `git status`, `git add`, `git checkout`, `git merge` into a working tree. Those need an index and a working tree, and there are none. ## Creating and converting - `git init --bare <dir>` — a new, empty hub. - `git clone --bare <url>` — a copy with the source's branches, without a working tree. - `git clone --mirror <url>` — like `--bare` but sets up a refspec that mirrors *all* refs and configures `remote.origin.mirror`, so a later `git fetch` updates them wholesale. This is the form to use for backups and migrations. Going the other way, you can point a normal repository at a bare one as a remote and push; there is nothing special about the client side. ## Common misconceptions That a bare repository is "read-only" — it is not; it is the normal target of pushes. That it lacks history or objects — it has exactly the same object database. That it cannot run hooks — server-side hooks are precisely what it runs. And that `--mirror` and `--bare` are synonyms — mirror additionally mirrors every ref namespace rather than just the branches a normal clone would take.

  • What happens if you push to a branch that is currently checked out in a non-bare repository?
    By default Git refuses the push, because `receive.denyCurrentBranch` is set to refuse. Accepting it would move HEAD while leaving the index and working tree on the old commit, so the receiving repository would show a phantom set of reverse changes. Setting the key to `updateInstead` makes Git update the working tree when it is clean.
  • How does git clone --mirror differ from git clone --bare?
    Both produce a repository with no working tree. `--mirror` additionally configures a refspec that maps all refs, and marks the remote as a mirror, so fetching updates every ref namespace rather than just the branches a normal clone would take. It is the right choice for backups and repository migrations.
  • Which Git commands still work inside a bare repository?
    Everything that only reads objects and refs — `git log`, `git show`, `git cat-file`, `git for-each-ref`, `git rev-parse` — plus receiving pushes and running server-side hooks. Anything needing files on disk, such as `git status`, `git add` or `git checkout`, does not, because there is no index and no working tree.

A normal clone is a warehouse with a shop floor attached; a bare repository is the warehouse alone — goods come and go, but nobody browses the shelves in place.

saying these in an interview costs you the question

  • Thinks a bare repository is read-only storage
  • Believes it holds no objects or history
  • Assumes hooks cannot run without a working tree
  • Says the .git suffix is what makes it bare
  • Confuses --mirror with --bare

context