1. 从一个真实的翻车现场说起
刚接触 Git 那会儿,我在一台内网服务器上建了个仓库,git init一把梭,然后回到本地git clone,结果死活克隆不下来,报错信息大意是"目标路径似乎不是一个 Git 仓库"。当时我第一反应是网络问题,折腾了半天防火墙和端口,最后才发现——问题出在服务器上那个仓库是用git init建的,而不是git init --bare。
这个坑我相信不少人都踩过,而且踩的时候往往一头雾水,因为两个命令看起来只差一个--bare参数,但背后的仓库结构和使用场景完全不同。这篇文章就把这两个命令掰开揉碎讲清楚:它们各自生成了什么样的目录结构、为什么会有这种差异、什么场景该用哪个、以及实际工作中怎么选才不给自己挖坑。不管你是刚学 Git 的新手,还是用了几年但一直没搞明白--bare到底干嘛的老手,看完应该都能有个清晰的认识。
核心关键词先摆出来:git init、git init --bare、裸仓库、工作区、远程仓库、push 报错。这几个词基本覆盖了这篇文章要讲的所有内容。
2. 先搞懂 git init 到底做了什么
2.1 一个普通仓库的目录长什么样
在空目录里执行git init,Git 会做一件事:创建一个名为.git的隐藏目录。这个目录就是仓库的"大脑",里面装着对象库、引用、配置、钩子等所有元数据。而.git所在的那个目录本身,就是我们日常写代码、改文件的工作区(working tree)。
结构大致是这样:
my-project/ ├── .git/ <- 仓库元数据,Git 的"大脑" │ ├── objects/ <- 所有提交对象、树对象、blob 对象 │ ├── refs/ <- 分支和标签的引用 │ ├── HEAD <- 当前指向哪个分支 │ ├── config <- 仓库级配置 │ └── hooks/ <- 各种钩子脚本 ├── src/ <- 你的代码 ├── README.md <- 你的文件 └── ... <- 其他工作区文件关键点在于:工作区和.git是并列的。你在工作区里改文件,Git 通过.git里的元数据来追踪这些改动。git status能告诉你哪些文件被修改了,git add能把改动暂存,git commit能把暂存内容写进对象库——这一切都依赖于"有一个工作区"这个前提。
2.2 为什么普通仓库不能直接当远程仓库用
现在问题来了。假设你在服务器上git init建了个普通仓库,然后本地想git push上去,会发生什么?
Git 会拒绝你,报错类似:
remote: error: refusing to update checked out branch: refs/heads/master remote: error: By default, updating the current branch in a non-bare repository remote: error: is denied, because it will make the index and work tree inconsistent翻译成人话就是:你正在往一个"有人正在里面干活"的仓库推代码,Git 怕把你的工作区搞乱,所以直接拒绝。
这个逻辑其实很合理。想象一下,服务器上那个普通仓库的master分支是"当前检出"状态,工作区里的文件对应着某个提交。这时候你从本地推了一个新提交上去,Git 如果接受了,那服务器上的工作区文件就和新的HEAD对不上了——工作区还是旧内容,但分支引用已经指向新提交。下次有人在服务器上执行git status,会看到一堆莫名其妙的"改动",因为工作区和索引、HEAD 全乱套了。
所以 Git 的设计者干脆一刀切:非裸仓库的当前分支,默认不允许被 push 更新。这不是 bug,是保护机制。
2.3 那 git init --bare 又是什么
git init --bare创建的仓库,没有工作区。它只有.git目录里的那些东西,而且这些内容直接放在仓库根目录下,不再套一层.git文件夹。
结构对比一下:
# 普通仓库 my-project/ ├── .git/ <- 元数据在这里 └── (工作区文件) # 裸仓库 my-project.git/ ├── objects/ <- 元数据直接放根目录 ├── refs/ ├── HEAD ├── config └── hooks/注意裸仓库的命名习惯:通常以.git结尾,比如project.git。这不是强制的,但约定俗成,一眼就能看出这是个裸仓库。
裸仓库没有工作区,意味着没有"当前检出的分支"这个概念(严格说HEAD还在,但指向的是一个不存在的工