Git 协作笔记:从一个人写代码到三个人一起改

约 4 分钟

个人项目的提交习惯,以及进入多人协作后遇到的主分支直改、push 被拒、冲突不会解、误提交文件等问题,附分支模型、.gitignore 与误操作补救命令。

我最早用 Git 是为了“有个地方存代码”,整个流程就是 add、commit、push 三条命令。直到数学建模和图书管理系统这两个项目需要三个人一起改同一个仓库,我才发现我原来会的那部分,只是 Git 里最简单的那部分。

这篇文章记录我从“一个人用”到“三个人用”之间补上的东西。

一个人写代码时的习惯

先说单人阶段我养成的两条习惯,它们后来救了我很多次。

一是小步提交。 我一度习惯攒一天再提交一次,提交信息写“更新代码”。后来有一次改坏了东西想退回去,发现上一个版本已经是三天前的状态,中间没有任何可回退的中间点。从那以后我改成完成一个可描述的小改动就提交一次,比如“完成借阅记录列表的分页查询”。

二是提交信息写清“做了什么和为什么”。 只写“修复 bug”没有价值,三个月后我自己都看不懂。现在我的格式是:

git commit -m "fix: 修正库存校验逻辑,避免并发借阅时出现负数"

前缀(fix: / feat: / docs: / refactor:)不是硬性规定,但它强迫我先想清楚这次改动属于哪一类。写“为什么”的部分尤其重要——避免并发借阅时出现负数 这个信息,看代码是看不出来的。

从单人走向多人时踩的坑

直接在主分支上改

我的默认习惯是拉下代码就在 main 上开工。三个人都这么干的时候,问题立刻出现:我改了一半的功能还没跑通,队友拉了最新代码,他的界面直接崩了。

push 被拒

第一次看到这个报错的时候我完全不知道怎么办:

! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.

原因很简单:队友先推了,我的本地落后于远程。正确做法是先 git pull 把远程的改动合进来,解决完冲突再推,而不是加 -f 强推。强推会把队友的提交直接抹掉,这是协作里最不能犯的错误之一。

冲突不会解

第一次遇到冲突时,我看到的是一堆 <<<<<<< 符号,第一反应是把文件删了重新写一遍。后来才知道冲突标记是有结构的:

<<<<<<< HEAD
available = available - 1
=======
available -= 1
>>>>>>> feature/borrow-lock

<<<<<<< HEAD 到 ======= 之间是我本地版本,======= 到 >>>>>>> 之间是对方版本。解决方式就是人工决定最终要什么,然后把三行标记全删掉。

把不该提交的文件提上去

我把虚拟环境目录 venv/ 和 IDE 的 .idea/ 一起提交了。仓库体积暴涨,队友拉代码变慢,而且每次他本地配置变一点点,git status 就多出一堆改动。更危险的一次是差点把含 API Key 的 .env 提交上去。

我们后来用的朴素分支模型

大厂用的 Git Flow、trunk-based 那一套对三人小项目来说太重了。我们后来只用最简单的三条规则:

main 永远保持可用。 任何时候拉下来的 main 都应该是能跑起来的。做不到这一点的代码不往 main 合。

功能在 feature 分支上开发。

git checkout main
git pull origin main              # 先同步
git checkout -b feature/reader-search   # 开分支

# ... 写代码,提交若干次 ...

git push -u origin feature/reader-search

合并前先拉取 main。 在 feature 分支上先把 main 的最新改动合进来,在自己这边把冲突解决掉,再提合并请求。这样 main 分支永远不需要处理冲突。

git checkout main && git pull origin main
git checkout feature/reader-search
git merge main                    # 在自己分支解决冲突
git push

冲突解决的实际步骤

真实场景里我是这么走的:

git pull origin main
# Auto-merging src/borrow.py
# CONFLICT (content): Merge conflict in src/borrow.py

git status                        # 看清哪些文件冲突了
# 打开文件,找到 <<<<<<< 标记,人工决定保留什么,删掉标记

git add src/borrow.py             # 标记为已解决
git commit                        # 完成这次合并提交

一条经验:冲突时不要慌着删对方的代码。 先看对方为什么改,很多时候两个人的改动是互补的,只是位置撞在了一起。如果实在不确定,直接问对方,比自己猜快得多。

.gitignore 该写什么

我现在每个新项目的第一件事就是建 .gitignore:

# Python
__pycache__/
*.py[cod]
.venv/
venv/
*.egg-info/

# 密钥与环境变量(最重要的一条)
.env
.env.local

# IDE 配置
.idea/
.vscode/
*.iml

# 系统与编辑器临时文件
.DS_Store
Thumbs.db
*.swp

.env 那一行永远是优先级的最高项。虚拟环境和 IDE 配置是“污染仓库”,密钥泄露是安全问题,不是一个量级。

已经提交过的文件,加进 .gitignore 也没用

这是我踩过的一个坑:.gitignore 只对没有被 Git 跟踪的文件生效。如果一个文件已经被 commit 过,后来才把它写进 .gitignore,Git 依然会继续跟踪它,改动照样出现在 git status 里。

解决办法是先把它从暂存区移除,但保留本地文件:

git rm --cached .env
git rm -r --cached .idea/         # 目录要加 -r
git commit -m "chore: 停止跟踪 .env 与 IDE 配置"

注意 --cached 这个参数。不加它,git rm 会把本地文件也删掉。加了,文件还在我磁盘上,只是 Git 不管它了。

需要提醒的是:如果泄露的是已经推送过的密钥,git rm --cached 并不能解决问题,历史提交里还留着。正确做法是立刻去服务商后台把那个 Key 作废并重新生成。

reset / revert 与误操作补救

这几个场景我都实际遇到过,列出来备查:

刚 commit 完发现提交信息写错了,还没推。

git commit --amend -m "fix: 正确的提交信息"

已经推送到远程了,想撤销这次提交。 用 revert,它生成一个新的反向提交,不改历史,对协作者安全。

git revert <commit-hash>

改乱了工作区文件,想丢掉所有未提交的修改。

git checkout -- src/borrow.py      # 单个文件
git restore .                       # 全部(新版 Git 的写法)

git add 错了文件,还没 commit。

git restore --staged wrong_file.py

这里有一条分界线要记住:只要提交已经推送到共享分支,就不要用 reset 改写历史。reset --hard 加 push -f 会把队友的提交一起干掉。没推送的本地提交随便折腾,推送过的用 revert。

比命令更重要的事

用了半年 Git 之后我的感受是:协作里最难的从来不是命令。

提前同步接口。 我和队友各自写模块,如果不在开工前约定好函数名、参数、返回值,最后合起来必然对不上,然后就是互相改代码。花十分钟把接口写下来,能省掉一天。

别改别人正在改的文件。 这条听起来是废话,但实际上只要提前分工时把“文件归属”也一起划分清楚,冲突能减少一大半。同一个文件两个人同时动,冲突是必然的。

Git 提供的是一套并发修改同一份代码的规则,它不能替你解决沟通问题,但如果你沟通到位了,它能让三个人改一个仓库这件事变得非常顺畅。