As Mark Adelsberger noted, the database (the .git/objects/
and .git/objects/pack/
directories, in a non-bare repository) stores everything.
The point of Git is to store commits, generally forever. These commits aren't files, although they do (indirectly) contain files. The objects—files, which Git calls blobs; commits, and two more internal object types called trees and annotated tags—are all stored in a special, compressed (sometimes very compressed), Git-only format. These are only useful to Git itself, in general.
In order for you, or the rest of your computer, to work with files, they need to be in ordinary format, not some special Git-only format. Git lets you do this in an area where you will do your own work: a work-tree.
If you have a work-tree, though, you are likely to use it to do work. If a Git repository is supposed to receive and act upon push requests, what happens to the in-progress work in some poor fellow's work-tree when, out of the blue, bam! someone pushes new stuff into his repository? Bad things happen, is what.
The obvious solution is the one Git uses: if you're going to receive pushes, eliminate the work-tree. Now there's nowhere to put in-progress work, so there is never anything that gets overwritten. That's what a --bare
repository is: it eliminates the work-tree, so that there's no place to do work, so it can't be abruptly overwritten by someone else.
There are a number of ways to compromise, but like most compromises, they tend to have various problems. The bare repository can receive pushes to take commits in, and can accept fetch connection requests to send commits out, and the ony thing that gets overwritten by a push request is the bare repository's idea of which commit any particular branch name means—ideally, in a "fast forward" manner so that this merely adds new commits to the set of commits reachable from the name. (Remember that in Git, a branch name is just a moveable pointer to one specific commit. See also What exactly do we mean by "branch"?)