做开发这些年,有个工具几乎天天都在用,就是 Git。不管是个人写点小工具,还是团队协作做项目,第一步基本都是同一个动作:创建项目仓库。很多人觉得这事简单,无非就是 git init 加 git push,但真到实际操作的时候,忽略文件怎么写、远程仓库怎么关联、分支命名怎么选、权限怎么配,每一步都有讲究。这篇文章就把 Git 项目创建这件事从头到尾拆开讲一遍,适合刚接触版本控制的同学,也适合用过一段时间但一直靠搜索引擎抄命令、没系统捋过一遍的人。看完之后,你不仅知道命令怎么敲,还能理解每条命令背后到底发生了什么。
1. 创建Git项目的整体思路:本地仓库与远程仓库的配合
1.1 为什么强调"创建"这一步
不少人在开始一个新项目时,习惯先写代码,写了几百行才想起来"哦对,要用 Git 管起来"。然后匆匆初始化仓库,把整个目录一股脑提交上去。结果就是 .git 目录里塞满了临时文件、编译产物、本机配置,后续每次提交都在跟这些垃圾文件纠缠,甚至可能把含密码的配置文件泄漏到团队仓库里。
我个人的习惯是:任何项目,落盘的第一件事就是初始化 Git 仓库,哪怕还没有一行代码。 理由很简单,版本控制的粒度越细,回退越灵活。你写第一行代码之前先创建仓库,就能把每个小改动都存成一个独立的提交;如果等写了一堆再初始化,第一个提交就是一大坨"初始版本",后面想定位某个改动具体发生在哪一步,根本无从下手。
"创建"这个词,在 Git 语境下其实包含两件事:
- 本地仓库的初始化:
git init,在本地目录生成.git内部文件,让这个目录进入版本控制状态。 - 远程仓库的建立:在托管平台上创建空仓库,再把本地仓库关联上去,实现异地备份和团队协作。
这两件事的顺序可以灵活调整,但底层关系必须搞清楚,否则容易出各种莫名其妙的问题。
1.2 本地仓库和远程仓库的关系
我见过不少初学者把远程仓库当成"主仓库",本地只是一个"草稿区",每次改完代码都小心翼翼地推上去,生怕本地丢失。这个认知是反的。Git 是一个分布式版本控制系统,本地仓库和远程仓库在结构上是平级的,每个本地克隆都是完整的历史副本,不是你上传文件的网盘。
打个比方:本地仓库就像你办公桌上摊开的笔记本,远程仓库是存放在档案柜里的复印本。你在笔记本上随时记录、涂改、撕页重写,这些都是本地操作,不打扰别人;等一个阶段的记录稳定下来了,才复印一份放进档案柜,供大家一起查阅和更新。
这种设计带来的实际影响是:
- 绝大多数操作都在本地完成,
git add、git commit、git log都不需要联网。 - 只有
push(推送)和pull(拉取)才涉及远程,推送是把本地的提交上传,拉取是把远程的新提交下载到本地。 - 远程仓库不是唯一真相,如果远程挂了,只要你本地还有克隆,历史就还在,可以重新推到一个新仓库。
理解了这个关系,后面创建远程仓库时你就不会纠结"应该先建远程还是先建本地"——其实都可以,只要最终把两边关联起来就行。但如果远程仓库里已经有内容(比如托管平台自动生成了 README),本地又是从零初始化的,关联时就会遇到"两个不相关历史合并"的问题,这个我在第 5 节会详细讲怎么处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 本地仓库创建:从 git init 到首次提交
2.1 初始化前的准备工作
真正敲 git init 之前,我建议先做三件事,能省不少后续的麻烦。
第一,确认当前目录是对的。这事听起来很蠢,但我自己就干过在项目上级目录初始化的事,结果整个父目录下的所有子项目全被一个仓库管起来了,后面拆分时折腾了半天。执行前用 pwd 看一眼当前路径,确认你正站在项目根目录下。
第二,先写好 .gitignore。这可能是整个创建过程中最重要也最容易被跳过的一步。很多语言的生态都有固定的"不该进仓库"的文件类型,比如 Python 的 __pycache__、venv,Node 的 node_modules,Java 的 target,还有各类 IDE 的配置目录 .idea、.vscode。我的经验是,哪怕你还没想好用哪个语言写,先把通用的那几条写上,比如:
gitignore复制.DS_Store
*.log
.idea/
.vscode/
后续用到什么加什么。宁可多忽略,不要少忽略。代码入库了可以删,但垃圾文件一旦进了 Git 的历史,清理起来要动 filter-branch 或者 filter-repo 这种高级手段,性价比极低。
第三,设置身份信息。提交记录里包含的用户名和邮箱,来自 Git 的全局配置或者当前仓库的局部配置。全局没设过的机器上直接提交,Git 会报"Please tell me who you are"的错误。提前配好:
bash复制git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
如果公司项目和个人项目用不同身份,可以在某个仓库目录下执行不带 --global 的版本,只对当前仓库生效。
2.2 git init 到底做了什么
执行 git init 之后,最快确认结果的方式是 ls -a,你会发现目录下多了一个 .git 文件夹。这个文件夹就是 Git 仓库的"大脑",里面存放了所有版本信息:提交对象、分支指针、配置、对象数据库、暂存区索引等等。
有几点值得了解:
- 初始化成功后,你所在的分支默认叫什么,取决于 Git 的配置。 新版本的 Git 默认分支名是
main,老版本是master。如果你不太确定,可以用git branch --show-current看看当前分支名。现在主流托管平台的新仓库默认分支普遍已经是main,所以本地分支名最好跟远程保持一致,省得后面改名或者出现歧义。 git init是幂等的,也就是说在已经初始化的目录里再执行一次,不会破坏现有数据,但也不会重复创建。如果看到 "Reinitialized existing Git repository" 之类的提示,不用慌,这是正常输出。- 初始化之后,仓库是空的,还没有任何提交。这个状态叫" unborn branch"(未诞生的分支),后面第一次 commit 之后,分支才真正被创建出来。
另外有一个容易被忽略的变体:git init -b main。这个参数可以直接指定初始分支名,如果你想统一团队规范,用这一步把分支名定死,比初始化完再 git branch -m 改名要干净。
2.3 首次提交:add、commit、status 的配合
初始化完成后的第一个提交,是整个仓库历史的起点。这个起点好不好,直接决定后面看 git log 的体验。
操作序列很简单:
bash复制git add README.md .gitignore
git commit -m "项目初始化:添加 README 和 gitignore"
这里我说一下 git add 的粒度问题。很多人喜欢 git add .,把当前目录所有变更一次性全部暂存。对于首次提交,你确实想把项目所有基础文件都纳进来,git add . 也没什么大问题。但有一个前提:** .gitignore 必须已经写好并且生效了**,否则你会把一堆垃圾文件一起提交进去。建议先 git add .gitignore,再 git add .,确保忽略规则先到位。
git status 是这块的核心检查工具。初次提交前,git status 会显示已暂存的变更、未暂存的变更、未跟踪的文件三大块。我一般会先跑 git status --short(缩写为 git status -s),这个格式更紧凑:
bash复制git status -s
输出形如:
code复制A README.md
A .gitignore
?? src/
A 表示已加入暂存区,?? 表示未跟踪的文件。看到这个状态,你就很清楚哪些文件会被提交进去了。如果列表里有不该出现的文件,趁现在从暂存区撤下来:git reset HEAD 文件名,或者直接在 .gitignore 里补上忽略规则。
首次提交建议至少包含两个文件:一个是 README.md,写明这个项目是干什么的、怎么运行;另一个就是 .gitignore。前者是给协作的人看的,后者是给 Git 看的,两个文件一个管"人",一个管"机",都是基础设施。
2.4 忽略文件 .gitignore 的配置要点
.gitignore 写得好不好,能直接影响团队日常协作的体验。我说几个实际项目中反复踩过的点。
规则是按行匹配的,从文件所在目录开始生效。重点记住几种写法:
gitignore复制# 注释:以 # 开头
node_modules/ # 忽略这个目录及其下所有内容
*.log # 忽略所有 .log 结尾的文件
!important.log # 但保留 important.log
build/ # 忽略 build 目录
/build/ # 只忽略根目录下的 build
这里很多人的困惑点是 node_modules/ 和 /node_modules/ 的区别。前者会命中项目中任意层级的 node_modules 目录,后者只命中仓库根目录下的那个。默认推荐用前者,因为依赖目录一般出现在多个子目录里。
空目录不需要忽略。Git 本来就不跟踪空目录,只有里面有了文件才会纳入版本控制。如果你需要保留一个空目录的结构(比如为了占位),常规做法是在里面放一个 .gitkeep 文件,再在 .gitignore 里写 !.gitkeep 把它特殊保留下来。
换行和编码。.gitignore 文件本身要保留 UTF-8 编码、LF 换行,否则在跨平台协作时可能出现匹配异常。Windows 上如果用记事本编辑,保存时注意选编码;推荐直接用代码编辑器的默认设置。
验证规则。写完 .gitignore 后,可以用 git check-ignore -v 文件名 来检查某个文件到底是不是被忽略的、被哪一条规则忽略的。这个命令在排查"为什么这个文件没被跟踪"时非常好用。
还有一点经验:.gitignore 只影响未被跟踪的文件。如果一个文件已经被 git add 或者提交过了,之后再往 .gitignore 里写规则,是不会让它自动消失的。必须先删除并提交一次移除操作,然后忽略规则才会对后续生效。
3. 远程仓库的创建与关联
3.1 在托管平台创建远程仓库时的选择
本地仓库创建好,下一步是到代码托管平台建一个远程仓库。不同平台的界面可能有差异,但新建仓库时让你做的几个选择基本是一致的,逐个说下我的建议。
仓库名称:通常要求是小写字母、数字、连字符的组合,比如 blog-system、data-analysis-tool。平台一般不支持驼峰命名,也不建议用下划线。仓库名最好能一眼看出是干什么的。
可见性:公开仓库(public)对所有人可见,私有仓库(private)只有授权成员可见。个人学习项目和开源项目选公开没问题,但要注意,公开仓库里的所有历史提交都会被看到——你不小心提交进去的密钥文件,即使后来删掉了,在历史里依然找得到。所以涉及公司业务或者任何不想公开的东西,一律用私有仓库。
是否初始化 README / .gitignore / License:这里是个关键岔路口。很多平台在新建仓库时给你勾选项:"Initialize repository with a README"。如果选了,远程仓库就会自动生成一个初始提交;如果你本地已经是初始化好的仓库,这个"初始提交"会导致两个仓库的历史完全不相关,推送时就会撞上"两个根"的问题。
我的建议是:本地已经创建好仓库的情况下,远程这边统统不要初始化任何文件,保持空仓库状态。让平台生成 README 这步省事,但后面处理历史冲突的麻烦远大于省下的那几秒钟。
3.2 本地仓库与远程仓库建立关联
远程仓库建好后,平台会给你一段命令提示,通常是:
bash复制git remote add origin https://某托管平台/用户名/仓库名.git
git branch -M main
git push -u origin main
这里逐步解释一下这三行的含义。
第一行给本地仓库添加一个名叫 origin 的远程仓库引用。origin 跟"原始"没关系,它只是一个约定俗成的名字,叫任何名字都行,但业界统一用 origin,别搞特殊。远程地址可以是 HTTPS 地址,也可以是 SSH 地址,区别在 3.3 节详述。
第二行 git branch -M main 是把当前分支重命名为 main。-M 是 --move --force 的缩写,不管原来叫什么,强制改成 main。如果你初始化时用了 -b main,这行可以不执行。
第三行是首次推送的核心。-u 是 --set-upstream 的简写,它做了两件事:推送当前分支到远程的 main 分支,同时建立跟踪关系——以后你在本地敲 git push 或 git pull 时,Git 自动知道该跟远程的哪个分支对话,不需要每次带参数。
关联好之后,可以用 git remote -v 验证,会列出远程仓库的地址(fetch 和 push 两个),确认无误后,本地和远程就算接通了。
3.3 HTTPS 与 SSH 两种认证方式怎么选
远程仓库的地址有 HTTPS 和 SSH 两种格式,这个选择直接影响你日常推送的体验。
HTTPS 方式:克隆地址形如 https://某托管平台/用户名/仓库名.git。优点是简单,浏览器能打开的地址就能克隆,不额外配任何东西;缺点是每次推送都要输入用户名和密码(或者个人访问令牌)。可以配置凭证存储避免重复输入:
bash复制git config --global credential.helper store
但这种方式是把凭证明文存在本地磁盘的文件里,有一定风险。相对折中的方案是配置为 cache,只缓存在内存里一段时间,过期后重新输入。
SSH 方式:克隆地址形如 git@某托管平台:用户名/仓库名.git。省去每次输账号密码的麻烦,但需要提前生成密钥对并把公钥添加到托管平台账号里。
生成密钥:
bash复制ssh-keygen -t ed25519 -C "你的邮箱"
一路回车后,默认会在 ~/.ssh/id_ed25519 生成私钥、~/.ssh/id_ed25519.pub 生成公钥。公钥内容是纯文本,把它整段复制到托管平台的 SSH Keys 设置里就行。
我的个人建议:个人日常使用,选 SSH,配一次后面长期省事;在别人电脑上临时操作或者用公有云开发环境,选 HTTPS,因为不想在陌生设备上留密钥。只要记住,地址的前缀就已经注定了认证方式——HTTPS 前缀就是账号密码认证,SSH 前缀就是密钥认证,中途换方式的话需要把远程地址一起换掉:
bash复制git remote set-url origin 新地址
4. 实操全过程:一个模拟项目从创建到首次推送
4.1 项目初始化的完整命令序列
前面理论讲了不少,现在走一遍完整流程。假设我在开发一个叫"某跨平台系统"的模拟项目,整个流程是标准化的,你完全可以照着抄。
第一步,创建项目目录并进入:
bash复制mkdir some-cross-platform-system
cd some-cross-platform-system
第二步,初始化 Git 仓库并指定初始分支名:
bash复制git init -b main
第三步,写好 .gitignore,以这个模拟项目为例:
gitignore复制# 系统生成
.DS_Store
# 构建产物
dist/
build/
*.tsbuildinfo
# 依赖和缓存
node_modules/
.pnpm-store/
# 日志
*.log
# IDE 配置
.idea/
.vscode/
# 环境变量
.env
.env.local
第四步,创建 README 并提交:
bash复制touch README.md
git add .gitignore README.md
git commit -m "chore: 初始化项目结构与基础配置"
注意 touch 在 Windows 的 cmd 下不可用,可以改成在编辑器里新建文件,或者用 PowerShell 的 New-Item。
第五步,添加远程:
bash复制git remote add origin git@某托管平台:某用户名/some-cross-platform-system.git
第六步,推送:
bash复制git push -u origin main
这条流程走完,本地和远程就打通了。后面日常开发就是编码、git add、git commit、git push 的循环,不会再涉及"创建"层面的操作。
4.2 分支命名与默认分支设置
分支策略这件事,我建议在项目创建初期就定下来,因为后续所有分支都从默认分支拉出来,改起来成本高。
业界现在基本达成共识:主分支默认叫 main,不叫 master。原因很简单,托管平台的新仓库默认分支就是 main,本地格式统一最省事。共享存储仓库切到新分支的完整命令:
bash复制git symbolic-ref HEAD refs/heads/main
git branch -M main
如果你管着一个老仓库,目前还在 master 分支上,想切换到 main 作为主干,最稳妥的步骤是:
bash复制git branch -m master main
git remote set-head origin main
但这会牵涉到远程仓库的默认分支设置,得去托管平台的管理后台把默认分支改成 main,还要让所有协作者同步这个变化。所以我更推荐在新项目的创建阶段就定好 main,省得后面折腾。
团队项目的话,建议额外约定一套分支命名规则。常见的做法是:
feature/xxx:新功能分支fix/xxx:缺陷修复分支release/xxx:发布分支docs/xxx:文档变更分支
分支名用斜杠分层,这样在托管平台的分支列表里会自动折叠成目录结构,找起来非常方便。这个规范最好写入仓库的贡献指南文档里,和 README 一起作为首次提交的内容。
4.3 首次推送的注意事项
首次推送是整个创建流程中出错概率最高的一个环节,主要问题集中在三种情况。
情况一:本地有提交,远程也是非空仓库。比如你在远程创建时勾选了"添加 README",推送时 Git 会拒绝并提示 "failed to push some refs"。因为两边都有独立的历史,Git 不知道如何把两条没有共同祖先的历史拼接起来。处理办法有两个:
- 如果远程确实只有 README 这类你不需要的文件,可以先把远程仓库清空,或者重新建一个不带初始化的仓库。
- 如果远程内容想保留,本地先执行
git pull --rebase origin main,把远程的提交作为基线,再把本地提交"叠加"上去。
第二种方案处理起来要细心,有冲突就得逐个解决,不建议新手在项目初期就陷入这种局面。最好的策略还是防患于未然——远程创建时别勾任何初始化选项。
情况二:SSH 密钥不对。推送时报 "Permission denied (publickey)",多半是本地 SSH 私钥没有被加载,或者公钥没添加到托管平台。先用 ssh -T git@某托管平台 测试连通性,能提示欢迎信息就说明密钥认证已经通了。如果测试正常但推送还是失败,查一下远程地址是不是 HTTPS 开头的——地址不一致,认证方式自然不匹配。
情况三:推错远程仓库。复制命令时看错地址,把代码推到了不相干的仓库,虽然可以 git push --force 覆盖,但后果不可控。首次推送前,用 git remote -v 把地址核对一遍,重点看仓库名是否正确。这步检查 10 秒钟,比出错后修复省太多时间。
推送成功后再跑一次 git status,如果显示 "Your branch is up to date with 'origin/main'",说明跟踪关系已经建立好,本地和远程处于同步状态,整个创建流程正式完成。
5. 常见问题与排查实录
5.1 "两个不相关历史"的合并冲突
这个问题我在前面提到过,是新手创建仓库时遇到最多的一个。报错长这样:
code复制error: failed to push some refs to 'git@某托管平台:某用户名/仓库名.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
核心原因就是本地和远程各有一个初始提交,它们没有共同的祖先。解决思路是让 Git 把两个历史"嫁接"到一起。
先拉取远程历史,用 rebase 方式让本地提交叠在远程提交之后:
bash复制git pull origin main --rebase
如果两边改动了同一个文件(比如远程的 README 和本地新建的 README),会报冲突。Git 会在冲突文件里标记出两边的不同版本,你要做的就是打开文件,决定保留哪边,然后:
bash复制git add README.md
git rebase --continue
rebase 完成后再推送就正常了。这种方案需要一个心态准备:在项目创建阶段就要处理冲突,这本身就是个信号,说明流程上该改一改——远程创建时就不要勾选初始化文件。以后所有新项目,本地 git init 完直接空仓库对接,就不会再有"联姻"问题。
5.2 提交成功但分支推不上去
有一个很隐蔽的场景:你在本地提交了代码,git log 也看得到提交记录,但推送时提示 "src refspec main does not match any"。
这个报错的原因是:本地不存在 main 分支。为什么会这样?很可能你初始化的时候用的旧版 Git,默认分支叫 master,你推的却是 main。
排查方法:
bash复制git branch # 查看当前有哪些分支
git branch --show-current # 查看当前所在分支
如果是 master,重命名即可:
bash复制git branch -M main
还有个更隐蔽的情况:你错误地在某个子目录里执行了 git init,导致子目录是个独立的仓库,外层仓库根本追踪不到你的提交。这时看着都正常,但推的就是空内容。用 git rev-parse --show-toplevel 查看当前仓库的根目录,确认你操作的位置正确。这个问题我在帮别人排查时遇到过不止一次,通常是因为平时打开终端的默认路径不对,稀里糊涂在子目录里敲了 git init。
5.3 换行符与权限问题
跨平台协作的项目,创建阶段就有两个隐性坑。
换行符(CRLF/LF)。代码文件在 Windows 上默认用 CRLF 换行,Linux 和 macOS 用 LF。如果没有任何配置,同一个文件在不同人之间来回提交时会疯狂产生 "whole file changed" 的假性差异,严重影响代码评审。
Git 提供了一个属性配置来解决这个问题,在仓库根目录创建一个 .gitattributes 文件并加入:
gitattributes复制* text=auto eol=lf
*.bat text eol=crlf
这样强制代码文件统一用 LF 换行,批量脚本类文件保留 CRLF。.gitattributes 应该和 .gitignore 一样,在项目创建初期就提交进仓库,所有协作者都会自动遵循。
文件权限位(file mode)。在 Linux 上开发、Windows 上协作的项目,偶尔会遇到 "old mode 100644 / new mode 100755" 这种提示——这是文件权限位变化导致的。如果项目里不需要跟踪执行权限的差异,可以关掉:
bash复制git config core.fileMode false
这条命令建议在本地执行后,同步给团队成员。不过需要注意,这只是忽略权限变化,真正可执行的文件权限还是要靠部署脚本去设置,git config 只是让你提交历史干净一些。
5.4 误提交了大文件
大文件进 Git 仓库是另一种高频问题。一个简单的模拟场景:项目初期,你随手把一个几十 MB 的测试数据文件提交进了仓库。当时没什么感觉,随着提交次数增多,每次 git clone 或者 pull 都要把这个对象的历史全部拉下来,仓库体积膨胀得很厉害,性能肉眼可见地下降。
确认哪个文件占了空间,可以用下面这条命令列出历史中体积最大的对象:
bash复制git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -nr | head -10
处理思路有两个方向:
- 如果文件还没被推送到远程,可以直接修改提交历史里的大文件,推荐用
git filter-repo这类工具重写历史,把大文件从所有历史提交中剥除。这个过程会改写仓库的全部提交哈希,只适合在团队规模很小、没有其他人拉取过该仓库的时候操作。 - 如果涉及的文件确实需要长期保存,应当改用 Git LFS(Large File Storage)来跟踪。LFS 会把大文件的内容存放到独立的存储服务中,仓库里只保留文件指针,克隆和拉取时按需下载。
我的习惯是:项目创建阶段就先规定好文件大小上限,超过阈值的二进制文件一律走 LFS 或者外部存储。把规则写进 README 的约定说明里,省得以后靠人肉盯。
5.5 误操作的恢复
创建流程中还有一个高频失误:初始化仓库后发现不对,想全部撤销。一个很简单直接的命令:
bash复制rm -rf .git
删掉 .git 目录,这个目录就完全退出版本控制了——之前提交的历史、分支、配置全都没了,但只要工作区文件还在,一切都可以重新初始化。这个操作适合"推倒重来"的场景,但务必确认你删的目录是对的,别把别人项目的 .git 删了。
如果只是想撤销暂存区的暂存操作,而不是彻底删除仓库:
bash复制git reset # 默认混合模式,撤销暂存,保留工作区变更
git reset --hard HEAD # 危险操作,会丢弃工作区的所有未提交修改
--hard 这个选项我是建议新手尽量少碰的,项目初期代码量少还好,等积累了几天工作量的代码,一个手滑就全没了。真要用之前,先 git stash 暂存变更,给自己留个后悔药。
6. 一些实操体会
项目创建这件事,在这些年的开发里重复了太多次,有一个体会是反复被验证的:前期多花五分钟,后面省五小时。.gitignore 提前写好、远程仓库别勾初始化、分支命名一次性统一,这些都是在创建时顺手就做的事,但能避免的麻烦远远超过那几分钟的成本。
我在实际踩过几次坑之后,给自己定了一套固定的创建节奏:先建目录,再 git init -b main,然后立刻写 .gitignore 和 .gitattributes,提交首个基础配置,最后才去托管平台建空远程仓库并关联。顺序固定下来之后,几乎不再遇到 "failed to push" 或者历史冲突这类问题。
如果这个项目是团队协作的,我还会在首次提交里放一份简短的 CONTRIBUTING 说明,把分支命名规范、提交信息格式、大文件约束写清楚。版本控制工具本身不产生协作规范,但它在项目第一天就能为规范落地打下底子。
创建仓库只是 Git 万里长征的第一步,但这一步走稳了,后面所有提交、评审、回滚、发布,都会在一个干净的地基上展开,值得花点心思。
最后再分享一个小技巧:对开发环境做一点微调,把部分常用交互命令的默认分支统一成 main,同时设置 git pull 默认采用 rebase 方式,可以让日常操作更顺手。具体配置因人而异,但方向是让 Git 尽量少打断你,多帮你干活。
