版本控制系统这东西,几乎每个前端都天天在用,但真要让人用一句话说清楚它是什么、为什么前端工程化绕不开它,很多人会卡住。我见过太多项目,代码目录里躺着一堆index.js、index-备份.js、index-最终版.js、index-最终版-真的最终版.js,还有人把整个项目打包成压缩包按日期排好放在共享盘里,靠“谁最后改的谁负责”来维持秩序。这套玩法在单人、短周期的小项目里勉强能跑,一旦进入多人协作、需求反复、线上要回滚的场景,立刻就塌。版本控制系统就是用来终结这种混乱的基础设施,它记录每一次改动的来龙去脉,让任意一个历史时刻的代码都能被完整还原,让多个人在同一份代码上并行工作而不互相踩脚。这篇内容适合三类人看:刚入行、还在用复制文件夹管理代码的新人;写了两年代码、add和commit用得很溜但说不清分支和合并原理的中级开发;以及需要为团队定规范、搭流程的技术负责人。我会从它解决的问题讲到内部模型,再落到前端项目里的具体操作和踩坑记录,尽量把每个“为什么这么设计”都讲透。
1. 版本控制系统到底在解决什么问题
1.1 从“文件名后缀地狱”说起
先不谈工具,谈谈没有版本控制时,我们是怎么管理代码的。最常见的是复制法:改一版就另存一份,于是项目里出现了login.js、login-new.js、login-修改.js、login-修复bug.js。这套方法有三个致命缺陷。第一,你无法知道两份文件之间到底差了什么,只能靠肉眼逐行比对,几十行的改动还能看,几百行的重构就是灾难。第二,你无法知道某一行代码是谁、在什么时候、为什么改的,等线上出问题需要追溯,只能去问人,而人往往记不清。第三,也是最能说明问题的,你无法安全地回退。假设昨天那版是好的,今天改坏了,你得手工把改动一个个撤回来,撤的过程中还可能引入新的错误。
版本控制系统把这三点一次性解决了。它把每次改动打包成一个带元信息的记录,包含改了什么、谁改的、什么时候改的、为什么改;它保存完整的历史链条,让任意一个历史节点都能被取出来;它用一套算法保证内容完整性,改动一旦落库就很难被悄悄篡改。你可以把它想成一个只增不改的流水账本,每次记账都会盖一个时间戳和签名,账本本身不接受涂改,要修正只能再记一笔新的。
这里有个容易被忽略的点:版本控制的核心价值不是“备份”,而是“可解释的历史”。备份只解决数据不丢,版本控制解决的是“这份数据是怎么一步步变成现在这样的”。这个区别决定了你在设计提交粒度、写提交信息时该投入多少精力。
1.2 三种形态:本地式、集中式、分布式
版本控制系统在发展过程中演化出三种形态,理解它们的分野,能帮你判断团队当前方案处于什么水平。
| 形态 | 典型特征 | 优点 | 明显短板 |
|---|---|---|---|
| 本地式 | 数据只存在本机,单机记录改动 | 结构简单,无需网络 | 无法协作,硬盘坏了历史全没 |
| 集中式 | 有一台中央服务器存全部历史,客户端只存当前版本 | 权限集中管理,历史唯一 | 服务器不可用就全部停摆,本地没有完整历史 |
| 分布式 | 每个克隆都是一份完整仓库,含全部历史 | 离线可提交,容灾强,分支成本极低 | 概念更多,初学者容易搞混远程与本地 |
现在主流的是分布式形态。它的关键特征是“每个副本都是完整仓库”,你在本地做的任何操作——提交、切分支、看历史、合并——全都不需要联网。只有跟远端同步时才需要网络。这个设计带来两个直接好处:一是网络抖动不会打断你的工作节奏,二是分支操作变成了几乎零成本的事情,因为分支只是一个指针,不涉及任何文件复制。
很多人用了几年分布式工具,思维还停留在集中式:每次改一点就赶紧推上去,生怕本地丢东西。这种习惯带来的副作用是提交历史变得又碎又乱,回滚时根本找不到一个语义完整的点。我在团队里反复强调的一句话是:本地提交是你的草稿纸,随便写;推到远端才是发表,要有格式。
1.3 前端工程化为什么把它当作地基
后端项目对版本控制的依赖很好理解,前端这两年的工程化演进让这个依赖变得更重。原因在于前端项目的产物链条变长了:源码经过转译、打包、压缩、资源指纹化,最后产出一堆带哈希的文件。这个链条里每一个环节的配置都需要被版本化——构建脚本、依赖清单、锁文件、环境变量模板、部署配置。少了任何一样,别人拉下代码就跑不起来。
尤其关键的是依赖锁文件。前端项目的依赖树动辄上千个包,间接依赖的版本漂移足以让“昨天能构建、今天构建失败”成为常态。锁文件的作用是把整棵依赖树的精确版本固定下来,它必须进版本库,而且必须跟package.json的改动一起提交。我见过有人把锁文件加进忽略列表,理由是本机生成的跟别人不一样、有冲突。这个做法等于主动放弃了构建的可复现性,代价会在某次线上紧急发版时一次性还清。
另一个前端特有的点是构建产物。dist、build、.cache这类目录不该进仓库,因为它们完全可以从源码和配置重建,放进去只会让仓库体积爆炸、每次提交产生巨大的无意义差异、代码评审时噪声盖过真正的改动。这条规则听起来是常识,但每年都有项目因为把产物提交上去,导致仓库涨到几个 G、克隆一次要等半小时。
还有一点值得单独说:前端团队普遍采用持续集成,每次推送触发自动检查与构建。这套机制的前提是“远端仓库的某一次提交能独立、完整地构建出产物”。如果历史里有依赖缺失、有环境变量硬编码、有产物混入,持续集成就会变成一个反复报假警的摆设。版本控制的规范性,直接决定了自动化流水线能不能立起来。
2. 把核心概念掰开:仓库、提交、分支、合并
2.1 三个区域:工作区、暂存区、版本库
刚接触这套工具的人最容易卡在“为什么我改了文件,还要再add一次才能提交”。这个设计不是多余,它对应三个明确的概念。
工作区是你眼前看到的目录,随便改,改坏了也不要紧,只要没提交就能还原。暂存区是一个中间层,用来挑选“这一次要提交哪些改动”。版本库是最终落库的历史记录,一旦进去就基本不可变。
暂存区的存在解决了一个真实痛点:你改了五个文件,其中三个是这次功能的改动,两个是顺手改的格式或调试代码。没有暂存区,你只能一次性全提交,历史就脏了;有了暂存区,你可以精细地挑,只提交那三个。这个能力在代码评审时价值极高——一个提交只做一件事,评审的人能看懂,回滚的时候也不会连带撤掉无关改动。
日常高频的几个动作,本质都是在三个区域之间搬运内容:
git status # 看三个区域的差异概览 git diff # 工作区 vs 暂存区 git diff --cached # 暂存区 vs 版本库 git add src/App.vue # 工作区改动搬进暂存区 git restore src/App.vue # 工作区改动丢弃,回到暂存区版本 git restore --staged src/App.vue # 暂存区退回工作区 git commit -m "feat: 新增登录态校验" # 暂存区落库我一般建议新手养成一个习惯:提交前先跑git status和git diff --cached,把要提交的内容从头到尾看一遍。这个动作平均花二十秒,能拦下绝大多数“误把调试代码、密钥、临时文件提交上去”的事故。
2.2 一次提交到底存了什么
很多人以为提交存的是“差异”,像打补丁一样一层层叠上去。实际上现代分布式版本控制系统存的是快照——每次提交时,它会为当前所有文件的内容生成一份完整的树状结构,只对内容没变的文件做复用引用。这个设计的精妙之处在于:取任意一个历史版本,都不需要从头重放所有补丁,直接拿到那棵完整的树就行,速度极快。
内容完整性靠哈希保证。每个文件内容算一个哈希值,目录结构也算一个哈希值,提交对象再把这些哈希值和作者、时间、说明、父提交串起来算一个总哈希。总哈希是四十位十六进制的字符串,理论上任何一处内容变化都会导致哈希完全不同。这条链条让历史无法被悄悄篡改:你改了中间某个提交,它后面所有提交的哈希都会变,一眼就能看出来。
由此引出两条实操心得。第一,提交哈希是可靠的定位手段,出问题时记下出问题版本的哈希前七位,就能精确描述“哪个状态有问题”。第二,不要在提交信息里写没用的东西,因为提交对象一旦生成就改不了,写错字只能靠重建历史来修,成本很高。宁可提交前多打几个字,也别事后纠结。
另外提一句内容寻址带来的好处:两个文件内容完全相同时只存一份。前端项目里模板文件、样式文件重复度很高,这个特性让仓库体积比想象中小得多。
2.3 分支的本质:一个可移动的指针
分支是初学者最迷糊的概念,因为它听起来像是“把代码复制了一份”。实际完全不是。一条分支本质上就是一个指向某个提交的指针,存在.git/refs/heads/下面,内容只有一行哈希值。创建一个分支,就是在那个目录里新建一个文件、写个哈希,耗时在毫秒级,跟项目大小无关。
这个认知一旦建立,很多操作就通了。切分支之所以快,是因为它只改 HEAD 的指向,然后把工作区文件更新成目标提交的样子。删除分支之所以危险,是因为它删掉的只是一个指针,如果没有别的分支或标签指向那些提交,它们就会变成不可达对象,被定期清理掉。
头部指针 HEAD 的作用是标记“你现在在哪”。它通常指向某条分支,分支再指向提交。所以一次提交实际做了三步:把暂存区内容做成新提交,新提交的父节点是当前提交,然后把当前分支指针往前挪。这个链条理解了,detached HEAD状态也就不神秘了——HEAD 不指向分支而直接指向某个提交时,你就处于游离状态,此时提交的内容不属于任何分支,切走之后就很难找回。
git switch -c feature/user-center # 新建并切换到特性分支 git switch main # 切回主干 git branch -v # 看每条分支指向的提交 git branch -d feature/user-center # 删除已合并的分支注意:删除分支前务必确认它已经被合并,或者你确实不再需要那些提交。删除后虽然可以通过查看引用日志找回,但过程对新手并不友好,别赌。
2.4 合并与变基:两种整合策略的取舍
把一条分支的改动整合回主干,有两种方式:合并和变基。两者的区别在于是否保留分叉的历史。
合并会生成一个新的“合并提交”,它的特殊之处是有两个父提交,一条来自主干、一条来自特性分支。历史图会呈现出一个分叉再汇合的形状。好处是真实反映了并行开发的事实,任何时候都能看出“这批改动是从哪条分支合进来的”。坏处是分支多的时候历史图会变成一团乱麻。
变基的做法不一样:它把特性分支上的每个提交,依次在目标分支的最新提交上“重放”一遍,生成一批内容相同但哈希全新的提交。历史变成一条干净的直线,看不出分叉。代价是原始提交被复制了,哈希变了,如果这条分支已经被别人拉下去用,就会造成两边历史不一致。
我的实践原则很朴素:只对还没推送到远端、只有自己用的本地分支做变基。这类分支是草稿,随便整理,把提交压缩、改写说明、消除分叉都没问题,推上去的时候历史干干净净。已经共享出去的分支一律用合并,哪怕历史图上多条线,也比让别人解决历史冲突强。
| 维度 | 合并 | 变基 |
|---|---|---|
| 历史形态 | 保留分叉,有合并提交 | 拉成直线,无合并提交 |
| 提交哈希 | 原提交不变 | 全部重算,哈希改变 |
| 冲突处理 | 一次性集中处理 | 每个提交都可能触发冲突 |
| 适用场景 | 共享分支、长期分支 | 个人本地分支、提交整理 |
还有一个实际操作的细节:主干合并特性分支时,我倾向加上--no-ff参数,强制生成合并提交。这样即使特性分支只提交了一次,历史里也能看出“这里有一组完整的改动被合进来”,将来回滚时可以直接撤掉整个合并,而不是逐个提交去逆操作。
git switch main git merge --no-ff feature/user-center git log --oneline --graph --all3. 在一个前端项目里落地:从初始化到日常提交
3.1 项目初始化与首次提交
新项目起步时,我通常先把骨架和忽略规则准备好,再提交第一次。顺序很重要:先提交忽略规则,能避免依赖目录被误加进去。
git init # 先写好 .gitignore,再执行下面的 git add .gitignore git commit -m "chore: 初始化忽略规则" git add . git status git commit -m "feat: 初始化项目骨架"中间那次git status不能省。它会明确告诉你有哪些文件即将进入仓库,特别是有没有把锁文件之外的奇怪东西加进去。我见过一个新同事把本地的.env直接提交上去,里面带着数据库连接串,虽然演示环境影响有限,但这个事故形态在真实项目里足够疼。
首次提交完成后,应该立刻建立远端关联并推送,让仓库有一个可靠的中心副本。这一步之后,日常开发就进入“本地提交 → 推送 → 评审 → 合并”的循环。
3.2 忽略规则怎么写才不出事
忽略规则不是随便凑几条,它应该按“必然可重建”和“含敏感信息”两个标准来写。前端项目常见的条目大致如下:
# 依赖 node_modules/ .pnpm-store/ # 构建产物 dist/ build/ .output/ *.tsbuildinfo # 缓存 .cache/ .eslintcache .parcel-cache/ # 环境变量 .env .env.local .env.*.local # 编辑器与系统 .idea/ .vscode/* !.vscode/extensions.json .DS_Store # 日志 *.log npm-debug.log*这里有几个判断依据值得说清楚。依赖目录不进仓库,是因为它可以从锁文件完整重建,而且体积巨大、文件数极多,放进去会让任何涉及它的操作都变慢。构建产物不进仓库,是因为它是源码的派生物,提交它等于同时维护两份真相,迟早不一致。环境变量文件不进仓库,是因为里面通常有密钥、令牌,一旦进了历史就很难彻底清除,只能靠重建历史。
!.vscode/extensions.json这种反向规则是刻意保留的:团队统一推荐插件列表对协作有正向价值,而个人设置不该共享。写忽略规则时用反向规则要谨慎,因为它会把已经被忽略的目录里的特定文件重新纳入,容易造成意料之外的结果。我的建议是先保守,遇到实际问题再补,不要一开始写一大坨自己都解释不清的规则。
注意:如果某个文件已经被提交进历史,之后再把它写进忽略规则是无效的。必须先用
git rm --cached 文件名把它从索引里移除,再提交一次,忽略规则才会生效。这个坑非常高频,尤其是.env和构建产物。
3.3 分支模型:主干开发加短生命周期特性分支
分支模型的选择直接决定了团队的协作效率。我见过两种极端:一种是所有人都在主干上直接提交,结果主干天天处于半坏状态,谁都不敢基于它发版;另一种是分支开得比需求还多,develop、release、hotfix、feature层层嵌套,合并路径像立交桥,出了事没人说得清哪条线是线上状态。
对大多数中小规模的前端团队,我更推荐主干开发加短生命周期特性分支。主干始终保持可发布状态,每个需求或修复开一条特性分支,命名带语义前缀,开发完尽快合并,分支存活时间控制在两三天以内。
git switch main git pull --rebase git switch -c feature/order-list-filter # 开发若干次提交 git fetch origin git rebase origin/main git push -u origin feature/order-list-filter分支存活时间短的好处很实际:时间越长,主干漂移越多,合并冲突越复杂;时间越短,冲突范围越小,解决成本越低。我自己的经验是,一条分支超过五天没合,冲突处理的时间就会开始超过开发本身。
命名规范上我建议统一前缀:feature/表示新功能,fix/表示缺陷修复,refactor/表示重构,chore/表示工程配置。这套前缀不只是给人看,更重要的是能配自动化规则——比如fix/开头的分支合并时自动打补丁版本号,feature/开头的自动升次版本号。
3.4 提交粒度与提交信息规范
提交粒度是我在团队里花最多口水的地方。判断标准只有一条:这个提交能不能被一句话描述清楚,并且回滚它不会误伤别的功能。如果一个提交同时改了登录逻辑、调整了样式、升级了依赖,那它就是不合格的,因为它无法被安全回滚。
提交信息我推行的是约定式格式,结构是“类型(范围): 描述”。
git commit -m "feat(auth): 增加登录态过期自动刷新" git commit -m "fix(list): 修复分页切换后筛选条件丢失" git commit -m "refactor(utils): 抽离日期格式化逻辑" git commit -m "chore(deps): 升级构建工具到最新稳定版" git commit -m "docs(readme): 补充本地启动说明"这套格式的价值在半年后才体现出来。当你要查“上次改动分页逻辑是什么时候”,一条命令就能筛出来:
git log --oneline --grep="list" --since="3 months ago"如果提交信息都是“修改”“更新”“修复一下”,这个能力就等于没有。我还建议在描述里写清楚“为什么改”,而不是“改了什么”——改了什么看差异就行,为什么改只有当时的人知道,而这个问题三个月后一定会有人问。
3.5 冲突是怎么产生的,怎么解
冲突的本质是“同一个文件的同一片区域,被两条分支改成了不同内容”。工具无法判断谁对,只能交给人裁决。它会在文件里插入冲突标记:
<<<<<<< HEAD const timeout = 3000; ======= const timeout = 5000; >>>>>>> feature/request-retry处理流程是固定的:先读懂两边各自想干什么,再决定保留哪一边、还是融合成第三种写法,然后删掉标记,git add标记为已解决,最后完成合并或变基。这里的关键不是命令,而是判断。我一般的判断顺序是:先看两边改动的意图是否冲突,如果只是数值不同,找业务方确认;如果是逻辑不同,看哪边更符合当前需求;如果两边都在解决不同问题,就融合。
减少冲突的办法比解决冲突更重要。第一,把文件拆小,一个组件一个文件,别搞几千行的巨型文件;第二,避免大范围的格式化提交,一次格式化会把整文件的每一行都标记为改动,让后续所有对比失效,这类提交应该单独成一次并提前通知团队;第三,勤同步,每天开工先拉一次主干,别攒一周再合。
git fetch origin git rebase origin/main # 冲突逐个解决后 git add 已解决的文件 git rebase --continue # 想放弃这次变基 git rebase --abort提示:变基过程中遇到冲突,解决完千万别用
git commit,要用git rebase --continue,否则会多出一个不该存在的提交,历史会变得很奇怪。
4. 团队协作里的几个关键动作
4.1 代码评审:合并请求该怎么提
合并请求不只是“请求合并”,它是一次知识传递和风险拦截的机会。我评审时最先看的不是代码风格,而是三件事:这个改动的范围是否和描述一致、有没有附带无关改动、有没有测试或验证说明。
提交合并请求时,描述模板我建议固定包含几块:改了什么、为什么改、怎么验证、影响范围。这四块看起来繁琐,但能省掉大量来回追问。特别是“影响范围”,前端改动经常波及公共组件,写清楚“本次改动影响所有使用列表组件的页面”,评审人就能有针对性地检查。
规模上,我强烈建议单个合并请求控制在四百行以内。超过这个量级,评审质量会断崖式下滑,人会开始“扫一眼就过”。如果需求确实大,就拆成多个有依赖顺序的请求,一个接一个合。这个习惯推行起来有阻力,但收益很直接:缺陷发现得更早,回滚粒度更细。
4.2 标签与版本号:给可发布状态打个记号
提交哈希虽然精确,但不适合人读,也不适合描述“这是几版”。标签就是给人用的记号。我一般只在真正可发布的状态上打标签,格式用语义化版本号。
git tag -a v1.4.0 -m "订单筛选功能上线" git push origin v1.4.0 git tag -l "v1.*"带注释的标签会存成一个独立对象,包含打标签的人、时间、说明,比轻量标签信息完整。什么时候升主版本、什么时候升次版本、什么时候只升补丁号,最好在团队里写死一条规则,别每次凭感觉。我的规则是:不兼容的接口或行为变更升主版本,新增功能升次版本,只修缺陷升补丁号。这条规则一旦定下来,前端包的版本号和仓库标签就能对上,排查线上问题时能直接定位到代码状态。
4.3 钩子做本地校验:把问题拦在提交前
等持续集成报错再回头改,成本比本地拦下来高一个数量级。本地钩子就是在提交或推送前自动跑一段脚本,常见的是格式检查、类型检查、提交信息校验。
# 安装 husky 与 lint-staged 后,在 package.json 里配置 { "lint-staged": { "*.{js,ts,vue}": ["eslint --fix", "prettier --write"], "*.{css,scss}": ["stylelint --fix"] } }这里有个经验:钩子里只跑快的检查,控制在几秒内。全量类型检查、全量构建这类耗时几十秒到几分钟的任务放到持续集成里,不要塞进提交钩子。否则开发会觉得每次提交都在等进度条,很快就会养成加--no-verify跳过的习惯,钩子就等于废了。
提交信息校验也值得加,用一条正则约束格式,能保证历史始终可检索。规则不要太苛刻,比如限制描述长度在七十二字符内、类型必须从固定集合里选,这就够了。
4.4 大文件与构建产物:为什么不该进仓库
仓库的本质是“存源码和历史”,不是“存文件”。任何能从源码重建的东西进去都是负担。构建产物进去的直接后果是:每次构建都产生成千上万行差异,代码评审页面卡死,合并冲突变成不可能完成的任务;同时仓库体积持续膨胀,克隆和拉取越来越慢。
真正的二进制素材,比如设计稿、图标集、字体文件,如果确实需要版本化,用专门的大文件存储方案,而不是直接塞进普通仓库。我见过一个项目把几十兆的视频素材提交进去,半年后仓库涨到接近两个 G,新人克隆一次要等二十分钟,最后只能重建历史把那些提交剔掉,代价是所有人的本地仓库都得重新克隆一遍。
判断标准很简单:这个文件能不能用一条命令重新生成?能,就不进仓库;不能,且是项目必需素材,考虑专门方案;不能,但只是本地配置,那就进忽略规则。
5. 踩坑记录与排查手册
5.1 提交到了错误的分支
场景很典型:在主干上改了几行,直接提交了,忘了先开分支。这时候千万别慌,也别急着重置。正确做法是新开一条分支,把提交带过去,然后把主干退回到原来的位置。
# 当前在 main,已经提交了,但这次提交应该在特性分支上 git switch -c feature/xxx # 新分支指向当前提交 git switch main git reset --hard origin/main # 主干退回到远端状态 git switch feature/xxx # 继续在正确分支上开发判断用哪种重置很关键:--soft只移动分支指针,改动留在暂存区;--mixed是默认行为,改动留在工作区;--hard连工作区一起清掉。用--hard之前一定先确认没有未提交的改动,因为它不可逆地丢弃工作区内容。
如果提交已经推到远端,就不能用重置了,因为远端历史还在。这种情况用反向提交,生成一个新提交来抵消原提交的改动,历史保持只增不改。
git revert <commit-hash>5.2 误删分支、误重置之后怎么找回
这类操作听起来很吓人,实际上绝大多数都能救回来,因为引用日志会记录 HEAD 的每一次移动,默认保留九十天。
git reflog # 找到目标提交的哈希,例如 a1b2c3d git switch -c rescue a1b2c3d引用日志是本地的,不随推送同步,所以只能在自己机器上救自己丢的东西。这也是为什么我建议遇到“代码好像没了”先别做任何写操作,第一件事是打开引用日志看记录,越早越容易找。
还有一个常见情况是误提交了一个不该进仓库的大文件。如果只是刚提交、还没推送,用git reset --soft HEAD~1撤销提交,再从暂存区移除该文件,重新提交即可。如果已经推送并且历史里带着大文件,普通删除无效,因为历史里那份对象还在,必须重建历史才能彻底清理。这类操作影响所有协作者,动手前一定要先跟团队对齐。
5.3 冲突反复出现、越解越多
有一种情况很折磨人:明明刚解完冲突,一合并又冒出一批新的,甚至同样的文件反复冲突。根源通常是两条分支长期并行、互相改同一片区域,或者历史里混进了大范围的格式化提交。
排查的第一步是看清两边各自改了什么范围,而不是急着改文件:
git log --oneline main..feature/xxx # 特性分支比主干多的提交 git log --oneline feature/xxx..main # 主干比特性分支多的提交 git diff main...feature/xxx # 特性分支相对分叉点的全部改动如果发现是格式化提交导致的整文件冲突,正确做法是先把格式化单独提交、合并进主干,再让特性分支变基到最新主干上,冲突就会大幅减少。如果是两条分支都在改同一个公共组件,那说明拆分粒度有问题,应该考虑把公共改动先抽出来合并,再各自开发。
处理冲突时我有个习惯:先把冲突文件备份一份到临时目录,再动手改。听起来笨,但在复杂冲突里,改到一半发现思路错了,能一键还原,这个保险很值。
5.4 常见问题速查表
| 现象 | 大概率原因 | 处理动作 |
|---|---|---|
| 提示本地有未提交改动无法切换分支 | 工作区不干净 | 提交、暂存或丢弃后再切 |
| 推送被拒绝 | 远端有新提交 | 先拉取并整合,再推 |
| 忽略规则不生效 | 文件已被纳入索引 | 从索引移除后再提交 |
| 历史图上出现重复提交 | 对已推送分支做了变基 | 避免对共享分支变基,必要时统一重置 |
| 合并后功能丢失 | 冲突解决时误删一侧代码 | 查看合并提交差异,逐段核对 |
| 克隆速度极慢 | 仓库体积过大 | 检查历史中的大文件,考虑精简 |
| 提交信息写错 | 已生成不可改 | 未推送可重建提交,已推送则接受 |
最后分享一个我自己一直在用的小习惯:每天开工第一件事是拉取主干并变基当前分支,收工前把当天的工作提交并推送到远端。这个习惯看起来很基础,但它把“本地有大量未提交改动”这个最常见的风险源直接消掉了。我踩过最疼的一次坑,是连续两天没提交,机器硬盘出问题,两天的改动全丢,只能凭记忆重写。从那以后我就认了一条原则:哪怕代码还很丑、测试还没跑通,只要是一个逻辑完整的中间状态,就先提交到本地分支。草稿留在自己机器上不值钱,丢了才值钱。