尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Git提交入门到实践:从commit信息规范到撤销与分支协作

Git提交入门到实践:从commit信息规范到撤销与分支协作 我见过太多团队的提交历史git 提交记录是一团乱麻一眼望去全是“update”“fix”“111”“test”再往下翻还有把一天的改动堆成一个大提交、把密钥和本地配置一起带上去的“名场面”。说实话git commit这个命令恐怕是开发者每天敲得最频繁的操作之一但也是被误解得最深的一个。很多人把它当成“保存代码”——CtrlS 式的本能操作改两行就存一下心里才踏实。可 Git 里的提交远远不是存档那么简单每一次 commit 都是在为整个项目拍一张快照同时给未来的自己写一张便签。这张便签如果写的是“update”一年后回看历史跟没写没什么两样。这篇文章我从实操角度把“提交”这件事拆开讲透提交到底存了什么、提交信息怎么写才值钱、怎么控制提交粒度、提交错了怎么撤、提交之后如何配合分支协作以及我这些年亲手踩过的坑。无论你是刚学会git add .的新手还是已经提交过上千万次的老开发大概率都能在里面找到一两个值得改掉的坏习惯。如果你还没装 Git也没关系Windows 装 Git for WindowsmacOS 用brew install gitLinux 直接走包管理器几分钟就能就绪我们直接进入正题。1. 提交的真实含义Git 到底在提交什么1.1 工作区、暂存区、版本库提交前的“三段路由”要真正理解提交得先搞清楚 Git 的三个区域工作区Working Directory就是你正在编辑的目录代码改没改、加了哪行删了哪行都在这里体现。暂存区Index / Staging Area可以理解为“待提交区”。git add把你选中的改动放进这个区它决定了“这一次提交包含哪些变化”。版本库Repository真正存放历史快照的地方。所有 commit 对象、分支指针、HEAD 引用都落在.git目录里。这三个区域对应了 Git 最经典的日常流程改代码 →git add→git commit。很多初学者习惯性地直接敲git commit然后发现提示 nothing to commit其实就是因为 Git 强制要求你“先选货、再付款”。“选货”是add“付款”才是commit。git status之所以永远是你最好的朋友就是因为它能告诉你当前工作区哪些文件改了、哪些已经暂存、哪些还没被跟踪。每次提交前看一眼 status是成本最低的保险。注意git commit提交的是暂存区的内容不是你工作区里所有改动的总和。这个区别是后面所有提交技巧的根基。1.2 为什么 Git 要设计成“先暂存再提交”这个设计经常被拿来和 SVN 对比。SVN 的commit是把工作区里的改动一股脑提交上去只要你改了提交就一定会带上而 Git 特意在“改动”和“提交”之间加了一道“挑选”的工序。这一步看似冗余实际价值非常大。举一个我日常遇到的场景一个页面需求你改了 Vue 组件的逻辑、调通了接口、顺手把样式表里的缩进统一了。这三类改动混在一起时如果直接一个提交上去review 的人会非常痛苦——他分不清哪些代码是解决业务逻辑的哪些只是格式化噪音。有了暂存区你可以只把组件逻辑的改动git add进去先提交“fix: 修复列表页加载异常”再把样式相关的改动单独提交成 “style: 统一样式缩进”。同一个工作阶段拆成两个逻辑清晰的提交。用生活化一点的说法提交像打包发货add就是往快递盒子里放东西commit是封箱、贴单号。如果两种完全不同类的东西比如衣服和盘子硬塞进一个箱子里收货人拆开时就会一脸问号。暂存区就是给你“分箱”的机会不要浪费它。1.3 一次 commit 里到底藏着什么很多人以为 commit 保存的是“改动的补丁”比如“这次改了文件 A 的第 12 行、文件 B 的第 3 行”。这个理解是错的。Git 的每个 commit 保存的是整个项目的完整快照外加一堆元信息。你可以用git cat-file -p HEAD看一个提交的内部结构里面大致长这样tree 3b18e512dba79e4c8300dd08aeb37f9e9b7e0fb1 parent 4b9b4b0f0e0b2f9f0b7a0c8a1b2c3d4e5f6a7b8c author Zhang San zhangsanexample.com 1681300000 0800 committer Zhang San zhangsanexample.com 1681300000 0800 feat(user): add login by phone number 手机号验证码登录替代原有账号密码登录入口。其中的 tree 对象指向当前项目的完整目录结构快照parent 指向上一个提交。因为每个提交都持有完整树快照的引用Git 切换分支、回滚版本才会那么快——它不是在“算差异”而是在“换快照”。这个机制也解释了为什么“改写历史”是个敏感操作任何对提交内容或提交信息的修改都会改变 commit 对象的哈希值导致它的所有后代提交哈希全部跟着变。所以本地还没推送的提交你可以随便改一旦推送到了共享分支改动就会影响所有人。这个道理后面第四章和第五章都会反复用到。2. 提交信息怎么写从“看不懂”到“一眼明白”2.1 你其实是在给谁写提交信息git log --oneline面前坐着的大概率不是别人而是半年后的你自己。我见过不少人抱怨“这个模块当年谁写的完全看不懂”然后git log一看提交信息写着“update”“12.3 改的”“ffff”。这种情况你不能怪别人只能怪那个人当时没把话说清楚。提交信息有三大读者第一个是未来的你第二个是帮你做代码评审的同事第三个是自动化工具。现在的 CI/CD、CHANGELOG 生成器、语义化版本检查工具很多都依赖提交信息的结构化格式。你随手写个“fix stuff”人看着费劲工具更是毫无办法。所以写提交信息不是在“应付流程”而是在“沟通”。一次提交就是一个最小单位的变更说明说明越清晰协作效率越高。2.2 一个合格的提交信息长什么样目前团队里最主流的写法是Conventional Commits约定式提交。核心结构是type(scope): subject 空行 body 空行 footertype提交类型。常见的有feat新功能、fix修复、docs文档、refactor重构不修 bug 不加功能、perf性能优化、test补测试、chore构建或辅助工具变动、style格式调整不改变逻辑、ciCI 配置变动。scope影响范围可选比如feat(user)表示用户模块的新功能。subject一行摘要尽量控制在 50 个字符以内用动词开头、祈使句不要句号结尾。body补充说明“为什么这么做”。很多人只写“做了什么”但真正值钱的是“为什么做这个选择”。比如修复一个 bug你可以在 body 里写清楚根因是什么、为什么用这种修法而不是另一种。footer关联的 Issue 编号、破坏性变更说明BREAKING CHANGE等。举个例子一个烂提交信息和一个合格提交信息的对比# 烂的 fix# 合格的 fix(order): 修复订单金额计算在折扣码叠加时溢出 满减和折扣码同时生效时原实现按 order.total * discount 直接计算 在极端数值下会出现浮点溢出。改成先对折扣码做金额上限封顶 再叠加满减并补了对应的边界测试用例。 Closes #1234看完第二种即使不看代码你也能知道这个提交解决了什么问题、为什么那么改。这就是提交信息的价值。2.3 实操从一行命令到完整提交说明最简单的情况下git commit -m就够了git commit -m fix(order): 修复折扣码叠加时金额溢出如果你想要在 message 里写多段可以用多个-m参数Git 会用空行把它们连接成不同段落git commit -m fix(order): 修复折扣码叠加时金额溢出 -m 满减和折扣码同时生效时原实现会产生浮点溢出改为先封顶再叠加并补充边界测试。如果信息比较复杂我更推荐直接不带-m敲git commitGit 会打开默认编辑器Vim、VS Code、Sublime 都行你可以舒舒服服地写多行、校对、再保存退出。很多人觉得麻烦但其实这才是正经工作流。另外git commit -v会额外在编辑窗口里显示这次的 diff 内容相当于边看改动边写说明对防止“提交信息和实际改动对不上”很有帮助。提示提交信息里的 subject 不要以.结尾不要用“修复了”这种过去式统一用“修复”“添加”“更新”这种祈使句开头。理由很简单一条提交记录在语义上描述的应该是“应用这个提交之后会发生什么”而不是“这个提交之前发生了什么”。3. 提交的时机与拆分让每一次提交都有意义3.1 该提交什么不该提交什么提交的黄金法则是每个提交代表一个完整的、可编译的、有意义的逻辑单元。具体量化的话可以这样判断如果把这个提交单独拿出来给同事 review他能看懂你在做什么如果单独checkout到这个提交项目依然能正常编译运行。满足这两点这个提交的粒度就是合理的。不建议提交的东西也很明确调试代码console.log、print_r、临时打点、写死的测试变量。临时注释掉的代码块如果你怕以后找回请交给 Git 历史去记而不是在提交里留一堆“死代码”。密钥和本机配置.env、config.local.php、application.yml里的数据库密码、各类 token。这些一旦进了提交历史删掉当前文件也没用历史记录里永远有。构建产物node_modules、dist、vendor、*.class、target等。这些东西应该通过.gitignore在提交前就挡在外面。.gitignore是提交纪律的第一道防线。一个新项目初始化的那一刻就应该把 IDE 配置.idea/、.vscode/、系统文件.DS_Store、依赖目录、构建目录全部写进去。否则迟早有一天你会把本地的.env推到远端然后花一下午改密。3.2 改动混在一起时如何拆成多个提交代码写嗨了的时候一个文件里往往同时存在“修 bug”“加功能”“改格式”三种改动。硬拆其实不难关键在于你要会用 Git 的“精细暂存”能力。第一种按文件拆。这个最简单git add src/domain/Order.php只暂存指定文件然后提交再继续下一个文件。第二种按文件内的代码块hunk拆。同一个文件里有两类改动时用git add -p进入交互式拆分模式。Git 会把改动按逻辑块逐个展示你可以用y暂存当前块n跳过当前块s把大块拆成小块甚至e手动编辑这个块的范围。这个命令我强烈建议花十分钟练熟它是把“一团乱改动”整理成“一串清晰提交”的核心工具。第三种临时藏起改动。有些改动你暂时不想提交但又要切分支去处理别的事可以用git stash push -m wip: 用户列表分页把改动暂存起来切走处理完后再git stash pop恢复。注意 stash 是“整份改动”的临时仓库不是精细提交的替代品适合短期过渡不适合长期堆着。拆分提交的实际流程通常是这样的先git status看整体改动心里把改动分成几个逻辑组然后逐组git add -p选中对应代码块git commit写清楚说明重复这个过程直到所有改动都变成了有名字的提交。最后git log --oneline一看整整齐齐那种感觉比直接一个大提交舒坦得多。3.3 大需求如何保持提交节奏做一个小需求还好一两个提交就结束了。但碰到那种需要开发两周的大功能怎么提交就成了难题。两个极端都不可取一是“一天一提交”不管改了什么反正下班前 commit 一下二是“憋大招”两周的改动攒到最后一次性提交。我的做法是“小步提交 阶段整理”。开发过程中每完成一个可运行的小里程碑就提交一次哪怕信息写得简单一点也要保证功能是完整的。比如“feat(import): 添加 Excel 解析模块”“feat(import): 实现数据校验逻辑”“feat(import): 接入入库流程”。这些提交之间的颗粒度不需要完美因为本地分支上没人看到。等到功能全部完成、准备合并到主干之前再用git rebase -i交互式变基去“整理房间”把多个“wip”小提交squash合并成一个像样的提交把提交顺序调整成“从搭建到收尾”的逻辑顺序。这一步是很多团队保持主干历史干净的核心技巧。记住本地的提交可以很随意共享的提交必须很规整。这是项目历史管理的核心心法。4. 提交错了怎么办三种撤销方式的选择4.1 改最近一次提交git commit --amend刚提交完突然发现漏了一个文件或者提交信息里打错了一个字这是最常遇到的情况。正确的做法是git commit --amendgit add src/utils/format.js git commit --amend --no-edit--no-edit表示沿用原来的提交信息不加改动的话就不用重新打开编辑器。如果你连提交信息也想改直接git commit --amend打开编辑器改就行。它的原理是把你的新改动“追加”到最近一次提交里而不是产生一条新提交。代价是这个提交的哈希值会变。如果这个提交已经 push 到了共享分支amend 会让远端和本地对不上下次 push 就会被拒绝。所以 amend 的使用原则很简单本地随便用推送后慎用。4.2 回退历史提交reset 的三种模式如果错误不是“最近一次提交漏文件”而是“回退到某个历史节点”你会用到git reset。它有三种模式很多人一直分不清其实用一张表就能说明白模式命令HEAD 位置暂存区工作区典型用途softgit reset --soft commit回退到指定提交保留保留只想撤销 commit但保留所有改动和暂存状态重新提交mixed默认git reset commit回退到指定提交清空保留撤销 commit 和暂存状态改动回到工作区可重新挑选提交hardgit reset --hard commit回退到指定提交清空清空彻底丢弃提交和所有改动慎用举个例子你提交了一个包含大量不可用改动的提交想拆成两个更小的提交。这时git reset --soft HEAD~1最合适HEAD 回到上一次提交但那个提交里的所有改动还稳稳地待在暂存区你重新分拣、重新提交就行。git reset不带参数更温和它只撤销“暂存”不删改动适合“我 add 错了文件”的场景。而git reset --hard是最危险的它会直接丢弃工作区和暂存区里的所有改动一旦执行没有后悔药。警告reset同样属于“改写历史”操作。任何已经 push 到共享分支的提交都不要用 reset 去回退否则和你协作的同事会遭遇一堆莫名其妙的冲突。4.3 不改写历史的撤销git revert那已经 push 的提交怎么撤答案是git revert。它的原理不是抹掉历史记录而是生成一条“反向提交”把之前那个提交做的事情倒着做一遍产生一个新的提交记录。git revert 3b18e51这条命令会创建一个“撤销提交”的新 commit把3b18e51造成的改动全部回滚。这样一来历史没有被改写只是多了一条“我把之前的提交撤掉了”的记录。所有同事 pull 下来后都能看到这条清晰的撤销轨迹不会出现哈希错乱。所以选择逻辑很简单未推送的提交用 amend/reset 随手整理已推送的提交老老实实用 revert 追加一条撤销记录。前者是你自己的私有领地后者是大家的公共历史改公共历史的代价远大于收益。4.4 密钥和敏感信息进了提交怎么办严格来说这已经不是“撤销”能解决的问题了。如果你不小心把.env、密码、token 提交到了 Git 历史里即使立刻删除文件再提交那个密钥也已经永远留在历史记录中了。此时的处理步骤应该是立刻到对应的平台云厂商、代码托管平台、第三方服务吊销并更换密钥这一步不能拖旧密钥必须作废。把当前分支上的敏感文件从工作区删除或重置为占位内容并提交一次。如果需要彻底清洗历史方向是git filter-repo官方推荐替代已经停止维护的 filter-branch或 BFG Repo-Cleaner。这类操作很硬核而且会改写所有提交哈希通常需要协调团队统一操作所以能不做就不做最好的策略是提交前靠.gitignore和检查清单防住。5. 提交之后的协作从本地仓库到团队主干5.1 推送前的提交检查清单提交写完了距离推到远端还有一步。我每次git push前会习惯性过一遍这几件事git status确认没有未暂存的改动被漏掉也没有不该提交的文件混进来。git diff --cached逐行看暂存区里到底有什么这一步能拦住“密钥进提交”“调试代码进提交”这类事故。git log --oneline -3看一眼最近几条提交信息确认没有临时的 “wip”“test” 垃圾记录。git pull --rebase在推送前拉取远端最新代码把你的本地提交变基到最新的远端提交之后避免出现一堆无意义的 merge commit。第四点值得多说一句。很多人习惯git pull然后自动生成 “Merge remote-tracking branch” 这样的合并提交用git pull --rebase则是先把本地未推送的提交挪到远端最新提交之后得到一条更线性的历史。团队里如果约定都用 rebase 方式拉取主干历史会干净得多。5.2 提交与分支合并MR/PR 里的纪律提交不是终点它最终要合并到主干。现在的团队大多走 MR/PRMerge Request / Pull Request流程你推送自己的功能分支然后在平台上发起合并请求由同事 review 你的提交。这里就涉及权限管理。有些团队在 GitLab 上允许 Developer 角色直接推代码到 master但我强烈建议开启分支保护直接推送 master 应该被禁用所有改动都必须走 MR 合入。原因很简单在 master 上直接提交就像是“绕过评审直接修改公共资产”一旦出错影响的是所有人。关于合并方式有两个方向merge commit和rebase 合并。merge 会在主干上留下一个“合并提交”节点保留功能分支的完整历史适合大型功能分支rebase 则是把功能分支的提交一个个排到主干后面形成一条直线历史最清爽适合小型改动。具体用哪种看团队和代码托管平台的能力但无论哪种都不要在 MR 里堆“一天一个 update”的垃圾提交。5.3 强制推送什么时候能“强行提交”很多开发新人第一次遇到git push被拒绝时第一反应是加-f强行推送。这不是不可以但要有严格的边界意识。git push -f的作用是用本地历史强行覆盖远端历史。危险在于如果远端有同事已经基于旧历史提交了新代码你的强推会把他的提交“抹掉”。最常见的允许场景是你在自己的功能分支上做了一次rebase -i整理把 5 个垃圾提交合并成了 1 个此时远端分支只有你自己在推强推是安全的。更好的做法是使用--force-with-leasegit push --force-with-lease这个参数比-f多了一层保护它会检查远端分支是否还是你上次 pull 时的状态如果别人已经推进了它就直接拒绝避免误覆盖。我的原则是不用裸-f一律--force-with-lease不在共享主干上强推只在私有特性分支上使用。6. 提交这件事我踩过的坑和改掉的习惯6.1 我见过最糟的几种提交习惯带过几年团队、review 过无数提交记录之后我总结了三种“反面典型”第一种是“永远在 update”。整条历史全是Update xx.md、update file没有任何有效信息。这种分支合并时reviewer 根本没法通过提交历史理解开发脉络只能硬看代码 diff。第二种是“一天一个大提交”。把从早到晚的所有改动堆成一个提交。乍一看代码能跑但你要是想回滚到“上午刚实现的某个功能”那个状态根本无从下手因为所有改动全混在一起了。第三种是“混入无关改动”。明明提交信息写的是修复登录 bug但 diff 里有大量格式化、变量重命名的无关改动。这类提交让 blame 失真——以后查“这行代码是谁改的、为什么改”只会得到一堆错误答案。这三种习惯的共同问题是没有把提交当成交互记录来经营。对个人是偷懒对团队是负资产。6.2 三个真实事故复盘第一件我年轻时候在一个功能分支上git commit --amend修改了提交信息然后直接git push -f推了上去。当天下午同事告诉我他的代码被“覆盖”了——因为他已经在我的旧提交基础上拉过分支、做过改动。那次之后我把--force-with-lease和“分支私有”的原则刻在了脑子里。第二件git reset --hard丢了两天工作。当时想回退一个实验性改动没仔细确认就把 HEAD 回退了三个提交然后发现工作区里两天的新代码全没了。从那以后我在使用 hard 模式前一定会先git stash或者直接备份一份整个目录也建议你养成这个习惯。第三件把本地application.yml提交到仓库里面带着我本机数据库的账号密码。同事一拉下来就直接用了我的配置连到我的本机整个联调环境乱了半天。那次之后.gitignore我必须放在项目初始化第一优先级敏感配置文件永远用.env.example 本地复制的方式管理。6.3 让提交体验变好的小技巧最后分享几个我日常高频使用的小工具和习惯git commit --fixup commitgit rebase -i --autosquash如果你在某次提交之后又发现它有问题不要急着新增一个“fix xx bug”的补救提交而是用 fixup 标注“这个补丁应该插入到那次提交里”然后 autosquash 自动帮你合并。这是整理本地长分支的利器。GitHub/GitLab 上的按块提交面板VS Code 和 IDEA 的 Git 界面里都能直接在 diff 视图里暂存单个代码块日常拆分比命令行add -p更直观值得用起来。修改提交者身份如果你发现某次提交的用户名、邮箱不对很多人问“IDEA 里怎么修改 git 提交的账户”——本质上就是改user.name和user.email。全局或仓库级配置用git config --global user.name 你的名字 git config --global user.email 你的邮箱改当前仓库就去掉--global。已经产生的历史提交要改身份用git filter-repo或交互式 rebase 处理这个我会单独写一篇讲透。最后再分享一个小习惯我每次提交前都会下意识跑一遍git status和git diff --cached确认“我要提交的和我以为我要提交的”完全一致。这个方法救了我不知道多少次建议你也试两周之后会发现自己的提交质量会上一个台阶。
返回列表