diff --git a/assets/drawio/git-bare-after.drawio b/assets/drawio/git-bare-after.drawio new file mode 100644 index 0000000..0d50fc4 --- /dev/null +++ b/assets/drawio/git-bare-after.drawio @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/drawio/git-bare-before.drawio b/assets/drawio/git-bare-before.drawio new file mode 100644 index 0000000..6c46b42 --- /dev/null +++ b/assets/drawio/git-bare-before.drawio @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/drawio/git-bare-overwrite.drawio b/assets/drawio/git-bare-overwrite.drawio new file mode 100644 index 0000000..664ede4 --- /dev/null +++ b/assets/drawio/git-bare-overwrite.drawio @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/git-bare-after.png b/assets/images/git-bare-after.png new file mode 100644 index 0000000..78e1fd3 Binary files /dev/null and b/assets/images/git-bare-after.png differ diff --git a/assets/images/git-bare-before.png b/assets/images/git-bare-before.png new file mode 100644 index 0000000..7e0a557 Binary files /dev/null and b/assets/images/git-bare-before.png differ diff --git a/assets/images/git-bare-overwrite.png b/assets/images/git-bare-overwrite.png new file mode 100644 index 0000000..056e7fc Binary files /dev/null and b/assets/images/git-bare-overwrite.png differ diff --git a/chapters/02-building-blocks.md b/chapters/02-building-blocks.md index edabfbe..46a7148 100644 --- a/chapters/02-building-blocks.md +++ b/chapters/02-building-blocks.md @@ -37,19 +37,36 @@ A repository can be **local** (on your machine) or **remote** (on a server like GitHub). Git treats both as equals — you can push to and pull from any repository you have access to. -Git supports two repository layouts: +### Repository Layouts -### Bare Repository +Typically, the layout of a **local** repository has a `.git` +folder with all the internals, and a working tree with your project. This type +of layout is called a **non-bare repository**. The `.git` folder stores the +full history and configuration. When you clone a repository, you get a +non-bare repository by default. -A **bare repository** is a Git repository without a working tree — it -contains only the `.git` internals (objects, refs, config) and no -checked-out files. Hosting services like GitHub and GitLab store repositories as bare -on the server. When you edit a file through GitHub's web interface, -GitHub creates a commit directly — it does not use a working tree. +```text +git clone project.git -> **Note:** "bare" and "remote" are not the same thing. A remote is any -> repository you connect to via URL. Remotes are *usually* bare, but a -> remote can also be a regular repository on another machine. +PROJECT/ +│ readme.md # Working tree — your editable files +└───.git # Repository internals + ├───hooks # Event scripts + ├───info # Repository metadata + ├───objects # All Git objects + │ ├───info # Object storage metadata + │ └───pack # Compressed object packs + └───refs # Named references + ├───heads # Branch tips + └───tags # Tag references +``` + +In contrast, the layout of remote repositories on hosting services like GitHub +and GitLab is different — they are **bare repositories**. A +**bare repository** has no working tree — only the Git internals +(objects, refs, config) and no editable files. When you edit a file through +GitHub's web interface, GitHub creates a commit directly — it does not use a +working tree. ```text git init --bare project.git @@ -65,32 +82,88 @@ PROJECT.GIT/ └───tags # Tag references ``` -### Non-bare Repository +As a rule of thumb, if you see a `.git` folder, it's a regular repository with +a working tree. If you see a folder that ends with `.git` but has no `.git` +inside it, it's a bare repository. -A **non-bare repository** (also called a regular or working repository) -is what you get when you clone or run `git init`. It has a working tree -where you create, edit and delete files, plus a hidden `.git` folder -that stores the full history and configuration. +Both `git clone` and `git init` can create either type of repository. By default, +they create regular repositories with a working tree. To create a bare +repository, use the `--bare` flag: ```text -git clone project.git +# creates a bare repository from a remote +$ git clone --bare -PROJECT/ -│ readme.md # Working tree — your editable files -└───.git # Repository internals (same as bare) - ├───hooks # Event scripts - ├───info # Repository metadata - ├───objects # All Git objects - │ ├───info # Object storage metadata - │ └───pack # Compressed object packs - └───refs # Named references - ├───heads # Branch tips - └───tags # Tag references +# creates a new bare repository locally +$ git init --bare ``` -The `.git` folder contains the same structure as a bare repository. -The difference is that a non-bare repository also has a working tree -next to it — the place where you do your actual work. +### Why bare repositories exist + +Imagine two developers — Alice and Bob — working on the same project +over a local network. Each has a non-bare repository with a working +tree. There is no central server — Bob pushes his changes directly +into Alice's `.git/` directory. + +![Direct push without a bare repo](../assets/images/git-bare-before.png) + +The problem: when Bob pushes his changes, Git updates the branch +inside Alice's `.git/` to point to Bob's latest commit immediately. +But Alice's working tree is **not** updated — her files still reflect +the old commit, plus whatever edits she has in progress. Alice's +branch and her working tree are now out of sync. If she commits at +this point, she unknowingly **reverts Bob's changes** without any +error or warning — because her files do not contain them. + +![Silent overwrite of commit B](../assets/images/git-bare-overwrite.png) + +What if the two developers never touch each other's repositories +directly? A popular solution to this kind of synchronization problem +is to introduce an intermediary repository that both developers push +to and pull from. + +![Bare repo as a shared hub](../assets/images/git-bare-after.png) + +This is called a **shared hub**. The shared hub is a bare repository — it has no +working tree, only the Git internals. Its purpose is to hold commits and +references, not to be edited directly. Because nobody edits files in it, +updating a branch is always safe. + +With a bare repository in the middle, each developer works +independently. Bob pushes his changes to the bare repository whenever +he is ready. Alice commits her own work first, then pulls Bob's +changes from the bare repository when **she** is ready. No one +reaches into anyone else's working tree. This is exactly how +hosting services like GitHub and GitLab work — every repository on +the server is bare. + +### The core.bare flag + +Git controls this with a configuration flag called `core.bare`, stored +inside the repository's config file: + +```text +$ cat project.git/config +[core] + repositoryformatversion = 0 + filemode = true + bare = true +``` + +In a non-bare repository, `bare = false` — the default. Git checks +this flag when receiving a push: if it is `false`, the push is +rejected to protect the working tree. Only bare repositories have +`bare = true`, which tells Git there is no working tree and pushes +are safe to accept. The `git init --bare` command sets this flag +automatically. + +Now, when Bob pushes to Alice's repository, Git checks `core.bare` and +rejects the push because it is not a bare repository. This prevents +Alice from silently overwriting Bob's changes on her next commit. + +Bob must push to a shared hub (a bare repository) instead, and Alice must +pull from it. This way, both developers have control over when they receive +each other's changes, and there are no surprises. ## 3. Object Model diff --git a/chapters/07-playbook.md b/chapters/07-playbook.md index 758e197..cfaa6b5 100644 --- a/chapters/07-playbook.md +++ b/chapters/07-playbook.md @@ -39,6 +39,7 @@ For definitions, see [Glossary](09-glossary.md). | [Remote Operations](playbook/remote-operations.md) | Push, pull, force push safely, sync forks | | [Remote Management](playbook/remote-management.md) | Add, rename, remove remotes, switch URL, SSH setup | | [SSH Setup](playbook/ssh-setup.md) | Key generation, agent, GitHub registration, troubleshooting | +| [Bare Repositories](playbook/bare-repositories.md) | Create, clone, convert, and use bare repos as local remotes | ## Project Structure diff --git a/chapters/recipes/bare-repositories.md b/chapters/recipes/bare-repositories.md new file mode 100644 index 0000000..8471cb2 --- /dev/null +++ b/chapters/recipes/bare-repositories.md @@ -0,0 +1,147 @@ +--- +title: "Bare Repositories" +description: "Git recipes for creating, cloning, and working with bare repositories — the standard layout for central and shared repositories." +section: "playbook/bare-repositories" +order: 93 +--- + +## Bare Repositories + +A bare repository has no working tree — only the Git internals (objects, +refs, config). It is the standard layout for central repositories that +multiple developers push to. Hosting services like GitHub and GitLab +store every repository as bare on the server. + +For the theory behind bare vs non-bare repositories, see +[Building Blocks](../02-building-blocks.md#2-repository). + +### Create a bare repository + +```text +$ git init --bare project.git +Initialized empty Git repository in /home/user/project.git/ +``` + +The `.git` suffix is a convention, not a requirement — it signals that +the directory is a bare repository. + +### Compare the layouts + +```text +# Bare — no working tree, internals at the top level +project.git/ +├── HEAD +├── config +├── hooks/ +├── objects/ +└── refs/ + +# Non-bare — working tree + .git folder +project/ +├── .git/ +│ ├── HEAD +│ ├── config +│ ├── hooks/ +│ ├── objects/ +│ └── refs/ +└── README.md +``` + +In a bare repository, what normally lives inside `.git/` sits at the +top level. There is no place to check out files. + +### Use a bare repository as a local remote + +This is the most common use case — simulate a central server on your +own machine for practice or local collaboration. + +```text +# 1. Create the bare (central) repository +$ git init --bare /tmp/central.git + +# 2. Clone it into a working copy +$ git clone /tmp/central.git /tmp/dev-alice +$ cd /tmp/dev-alice + +# 3. Make a commit and push +$ echo "Hello" > greeting.txt +$ git add greeting.txt +$ git commit -m "Add greeting" +$ git push origin main + +# 4. Clone again to simulate a second developer +$ git clone /tmp/central.git /tmp/dev-bob +$ cd /tmp/dev-bob +$ cat greeting.txt +Hello +``` + +Both clones push to and pull from the same bare repository, exactly +like working with GitHub. + +### Convert a non-bare repository to bare + +```text +$ git clone --bare project project.git +``` + +This copies only the Git data — no working tree files. The result is +a bare repository you can use as a central remote. + +You can also convert in place: + +```text +$ cd project +$ mv .git ../project.git +$ cd .. +$ rm -rf project +$ cd project.git +$ git config --bool core.bare true +``` + +### Clone a bare repository + +```text +$ git clone --bare https://github.com/user/project.git +``` + +Useful for creating mirrors or backup copies that do not need a +working tree. + +### Push to a non-bare repository (and why it fails) + +Pushing to a non-bare repository is rejected by default: + +```text +$ git push /tmp/dev-alice main +remote: error: refusing to update checked out branch: refs/heads/main +``` + +Git refuses because a push updates the branch reference but not the +working tree — this would leave the two out of sync and could cause +the recipient to unknowingly revert the pushed changes on their next +commit. For a full walkthrough of the problem, see +[Building Blocks — Why bare repositories exist](../02-building-blocks.md#why-bare-repositories-exist). + +If you need to accept pushes on a non-bare repository (rare), you +can enable it: + +```text +$ git config receive.denyCurrentBranch updateInstead +``` + +This tells Git to update both the branch and the working tree on push. +Use this only for special setups like deployment targets — not for +regular development. + +### Gotchas + +- **You cannot run `git add` or `git commit` in a bare repository** — + there is no working tree to stage files from. All changes must arrive + via `git push` from another repository. +- **The `.git` suffix is convention only.** Git does not require it, + but omitting it makes the directory harder to identify. +- **Bare does not mean read-only.** A bare repository accepts pushes, + runs hooks, and stores the same history as a non-bare one. +- **`git clone` of a bare repository produces a non-bare clone by + default.** Use `git clone --bare` if you want another bare copy.