
学 Git 和用 Git完全是两码事。刚开始接触版本控制的时候我也和大多数人一样把git init、git add、git commit、git push背得滚瓜烂熟感觉只要能把代码推上远端就算会用 Git了。直到有一次我在项目里误操作把分支搞乱提交历史一塌糊涂又碰上团队成员同时改了同一份配置文件那一整天才真的让我意识到不理解 Git 背后到底在干什么你就永远只能在能用和好用之间反复挣扎。这篇系统性使用指南不是我临时整理的命令手册而是这些年真正在项目里摸爬滚打之后沉淀下来的一套完整用法。覆盖了 Git 的底层存储模型、日常提交链路、分支管理、远程协作、撤销与救回以及最终怎么把这些串成一套适合自己的工作流。无论你是刚接触 Git 的初学者还是已经用了一阵子但总觉得哪里不对劲的开发者这篇内容都能帮你把之前零散的知识拼成一张完整的图。1. 先想清楚Git 到底解决了我的什么痛点1.1 没有 Git 时的版本管理噩梦聊 Git 之前先回想一下没有版本控制的日子。最典型的就是项目文件夹里堆满了项目_最终版.zip、项目_最终版2.zip、项目_真最终版_别动.zip这种命名。改坏一处代码想回退只能靠记忆和运气。团队协作更麻烦每个人电脑里各改各的最后靠 U 盘和聊天软件互传文件能不能合成一个完整版本全看缘分。Git 解决的问题本质上是三个可以随时回到任意历史状态、多人并行修改同一份代码而不互相覆盖、知道每一行代码是谁在什么时候为什么改的。这三点听起来简单但真正做好需要你先理解它的存储模型而不是只背命令。1.2 Git 的三个仓库模型工作区、暂存区、版本库Git 和其他版本控制工具最大的区别在于它多了一个暂存区Staging Area。很多人第一次学 Git 都会困惑为什么我git add之后还要git commit这不是多此一举吗实际上这个设计非常巧妙。工作区Working Directory是你电脑里看得见的文件目录版本库Repository是每次提交形成的不可变历史快照而暂存区是你主动挑选这一次提交里要包含哪些改动的中间区域。你可以把三者理解成工作区是厨房操作台暂存区是备菜盘版本库是已经出锅上桌的菜。你不需要把厨房里所有食材一股脑全倒进锅里而是先挑出这顿饭要用的洗好切好放进备菜盘确认无误再下锅。这个中间层最大的价值在于让提交变得有意识。你可以只暂存某个文件里的部分改动把同一文件的不同修改拆成多个逻辑独立的提交。我见过太多人git add .一把梭结果一次提交里混着日志调试代码、临时改动、格式调整出了问题回溯起来非常痛苦。1.3 第一次初始化项目时的合理姿势初始化虽然就一条命令但很多细节影响后续使用。最基本的是git init git add . git commit -m Initial commit但如果在初始化前就先把这些事想清楚能省很多麻烦确认全局身份git config --global user.name和git config --global user.email必须设置否则提交记录的归属会乱。公司项目和私人项目建议分开设置仓库级配置。写好 .gitignore 再提交第一笔node_modules/、target/、dist/、.env这类目录和文件一旦被提交进版本库后面再想彻底移除会很麻烦。先写好.gitignore再 init是成本最低的时机。选对分支名master还是main无所谓但团队要统一。现在很多平台默认用main你要是用了旧习惯后期想改名也不是不行只是要同步更新所有远端设置。2. 每天都会用到的提交链路从 add 到 commit 再到 log2.1 add 的几种用法以及为什么不要无脑 add all日常开发里提交链路是使用频率最高的操作也是最容易养成坏习惯的地方。git add有三种常见用法git add . # 暂存所有改动包含删除和新增 git add src/main.js # 暂存指定文件 git add -p src/main.js # 进入交互模式逐个 hunk 决定是否暂存git add .最省事但也最危险。它把所有改动不加区分地塞进暂存区。我曾经有一次在调试时往代码里加了一堆console.log顺手把某个和业务无关的配置也改了然后git add .加git commit一条指令提交上去后来要定位一个线上问题时不得不从一堆混杂的改动里摘线索。我的习惯是只暂存和当前提交主题相关的文件。改动涉及多个逻辑模块时先git add第一组文件并提交再git add第二组文件提交。git add -p这个交互式命令适合处理同一文件里包含多处不同目的改动的情况。它会把文件按 hunk 拆分逐个问你要不要暂存是拆提交的利器。2.2 写提交信息这件事值得认真对待很多人提交信息写得非常随意比如fix bug、update something、123。问题是Git 历史是要给别人看、给未来的自己看的。三个月后回看fix bug你完全想不起修的是什么 bug。规范的提交信息通常遵循这样一个模板type(scope): subject bodytype常用的是feat新功能、fix修 bug、refactor重构、docs文档、test测试、chore杂务。scope是可选的表示影响范围比如feat(login): 增加短信验证码登录。标题尽量短能说清是什么正文里写清为什么这样改、影响范围是什么。比如fix(cart): 修复购物车数量为 0 时仍可结算的问题 原因是结算接口没有对商品数量做非空校验 后端在数量为 0 时会抛出 500。 前端在数量为 0 时禁用结算按钮并在提交前二次校验。这种提交信息配合git log --oneline浏览历史的时候一眼就能看出每一次提交干了什么回退和排查的效率完全不一样。2.3 用 log 回看历史用 diff 看清变化提交得多了就要学会看历史和变化。git log最常用的几个形式git log --oneline --graph --all # 横向树状展示所有分支历史 git log -p src/main.js # 查看某个文件的完整修改历史 git log --author某开发者 # 看某个人的提交记录 git log --since2 weeks ago # 按时间过滤git diff是另一个高频工具。它展示的是还没暂存的变化git diff # 工作区 vs 暂存区 git diff --cached # 暂存区 vs 版本库 git diff HEAD # 工作区 暂存区 vs 最近一次提交我在排查问题时经常用git log -p定位某一行代码是被哪次提交改掉的再用git blame看对应的提交作者和修改原因。这一套组合拳下来绝大多数这个变量怎么没人赋初值之类的问题都能快速溯源。3. 分支不是玄学理解指针理解分支的本质3.1 分支的底层本质与 HEAD 切换Git 的分支本质上就是一个指向提交对象的指针。每次提交Git 会生成一个不可变的提交对象包含tree目录快照、parent父提交、作者信息和本次提交信息。分支指针就指向这个对象提交时分支指针自动前移。之所以强调指针这一点是因为很多初学者想不通分支操作为什么那么快。git branch dev只是新建了一个指针不会复制任何文件git checkout dev本质上是把HEAD这个特殊指针从当前分支切到dev同时更新工作区里的文件内容。HEAD可以理解成一个你当前站在哪个分支上的标记。理解了这个概念后面所有关于分支合并、rebase 的操作就都很容易想通。3.2 合并分支时 merge 与 rebase 的取舍这是 Git 里最有争议的话题之一。merge和rebase都能把另一个分支的改动整合到当前分支但方式完全不同。git merge dev会生成一个合并提交merge commit它有两个父提交历史是分叉然后汇合的。好处是保留了真实的开发轨迹谁在哪条分支上做了什么清清楚楚缺点是历史会出现交叉网络git log --graph看起来会比较杂乱。git rebase dev做的事是把当前分支的提交一个个摘下来重新接到dev的最新提交之后。结果是历史变成一条直线非常干净但代价是重写了提交对象因为 parent 变了原来的提交在效果上消失了。我个人的原则是公共分支上的提交绝不 rebase。因为 rebase 会改写历史一旦其他人已经基于旧提交继续开发你重写后会导致他们的仓库和你不一致产生非常难解的合并问题。本地未推送的分支可以放心 rebase。为了保持历史整洁在推到远端之前用 rebase 把一条功能分支整理成线性历史是推荐做法。简单场景用 merge复杂场景用 rebase。比如dev合并回主分支时我倾向于 merge保留一个清晰的合并节点开发分支同步主分支最新代码时用 rebase 避免产生大量无意义的 merge commit。3.3 分支命名与工作流约定分支命名不是小事。一个团队如果各起各的test、aaa、fix满天飞光看分支列表就头皮发麻。比较通用的模式是主分支main/master始终可部署的状态。开发分支develop集成各个功能分支的地方。功能分支feature/xxx比如feature/sso-login。修复分支fix/xxx比如fix/payment-token-expired。试验区experiment/xxx随便折腾用完即删。命名里带feature/或fix/前缀配合平台的自动清理规则可以在合并后自动删除远端分支避免分支越积越多。我自己推进新需求时一定是新建feature/需求名分支开发、自查、合并然后删除分支一条龙。这样做最大的好处是即使两个功能并行开发也不会互相干扰每个分支的提交历史都聚焦在它自己的主题上。4. 远程协作从 clone 到 pull 的那些坑4.1 SSH 还是 HTTPS远程连接的选型与配置推送代码到远端仓库之前先要把远程连接配好。主流的两种方式SSH和HTTPS各有各的使用场景。HTTPS的优点是简单克隆地址直接复制输账号密码或凭据管理器就能推送适合偶尔在临时机器上操作。缺点是每次操作都要走一套认证流程虽然可以用凭据缓存但配置体验始终差一点。SSH更推荐日常主力使用。生成密钥的方式是ssh-keygen -t ed25519 -C your_emailexample.com执行后会在~/.ssh/下生成一对密钥把.pub结尾的公钥添加到托管平台账户里私有密钥留在本地。验证是否生效ssh -T gitgithub.com # 不同平台域名不同看到欢迎信息就说明连接成功了。SSH 的好处是一劳永逸之后所有推送都不需要再输密码。配置远程仓库地址的命令也很基础git remote add origin gitxxx:yourname/project.git git remote -v # 查看当前远程配置4.2 fetch、pull、push 的完整链路很多人以为git pull就是把远端代码拉下来实际上它是一条复合命令等于git fetchgit merge。这两个步骤拆开理解非常关键。git fetch的作用是把远端仓库的最新提交和分支信息下载到本地的一个隐藏区域origin/main这种远程跟踪分支但不会动你当前的工作区和分支。也就是说fetch 之后你可以先看看别人改了什么再决定怎么处理。git pull则在 fetch 之后立刻把远端跟踪分支合并进你当前的分支。如果本地没有未提交的修改且远端没有和你本地产生分叉一切顺滑但如果两边同时改了一个文件的同一区域就会立刻触发冲突。git push更直接。把当前分支推送到远端并和远端对应分支建立关联git push -u origin main # -u 指定默认上游分支 git push # 之后直接使用简写这里有一个高频报错non-fast-forward或fetch first。含义是远端已经有你本地没有的提交Git 拒绝直接覆盖。有人看到这个报错的第一反应是git push --force这是非常危险的操作会直接覆盖远端的提交历史——如果远端那部分提交是别人刚合作完成的等于把别人的工作抹掉了。正确做法是先git pull --rebase或git fetch git rebase把远端新增的提交整合进本地再git push。这样能确保远端历史是累积式的而不是被强行覆盖。4.3 多人协作冲突的完整处理流程冲突大概是 Git 里最吓人的报错。但别慌冲突的本质并不复杂两个人改了同一个文件的同一行Git 不知道应该听谁的只能把人肉决策留给你。假设你在feature/login分支上修改了auth.js同事在fix/session分支上也改了同一个文件。合并时出现 HEAD const timeout 3000; const timeout 5000; fix/session处理流程是打开冲突文件逐段看和之间的内容。决定保留哪一边还是两边都保留还是写一个全新的值。删掉所有冲突标记符号只保留最终代码。对每个冲突文件逐一处理完后git add标记为已解决。继续执行git merge --continue或git rebase --continue。这里最大的技巧是冲突产生的时候先看上下文不要只看那几行。有时候双方改的目标不一样表面看是一处冲突实际逻辑上根本不能二选一而是要把两边思路整合起来重写这一段逻辑。我遇到很多复杂冲突都是两个提交各自依赖了一段对方的旧代码这时候就得到文件里通读一遍甚至要拉出两个提交的完整 diff 来对比。5. 后悔药怎么吃reset、revert、checkout 的差别5.1 三种撤销场景还没提交、已提交未推送、已推送版本控制系统给了我们后悔药但很多人分不清该吃哪一颗。我先按场景直接给结论再展开讲参数。场景一工作区改动还没 add。想撤销文件的修改回到最近一次提交的状态git restore src/main.js git checkout -- src/main.js # 旧一点的写法场景二已经 add 了但还没 commit。想取消暂存但保留工作区改动git restore --staged src/main.js git reset HEAD src/main.js # 旧写法场景三已经 commit 了但还没 push。想撤销这次提交回到提交前状态git reset --hard HEAD~1场景四已经 push 到远端。这时绝对不能用reset而是应该用git revertgit revert commit-id5.2 reset 的 soft、mixed、hard 到底怎么选git reset的核心作用是移动 HEAD 指针它有三个参数很多人背了又忘忘在没理解它们控制的是三个不同的区域暂存区和工作区。--soft只移动 HEAD 指针暂存区和工作区内容原封不动。也就是说撤销一个 commit 后改动还留在暂存区里可以重新整理后再次提交。--mixed默认移动 HEAD重置暂存区但工作区不动。撤销 commit 后改动回到工作区变成未暂存状态。--hard移动 HEAD重置暂存区同时把工作区文件也恢复到目标提交的状态。这个参数会丢弃所有未提交的本地修改必须慎用。举一个实际场景。提交之后发现这条提交内容不对想重来一遍但不丢失改动的代码git reset --soft HEAD~1 # 修改和补充代码 git add . git commit -m 重新提交如果要抽掉某次提交但不保留它的改动用--hard。注意--hard之前先确认没有未提交的、不想丢的文件。我就有过一次在git stash和git reset --hard之间搞混结果把一整个下午写的新功能全部丢掉的经历从那以后我养成习惯动--hard之前开个新分支git checkout -b backup-branch做个保险。5.3 revert 在协作分支上的正确用法git revert和 reset 的最大区别是它会生成一个新的提交新提交的内容恰好是反向应用目标提交。也就是说历史是往前走的之前的提交记录还在只是在最上面多了一个撤销提交。这个特性让 revert 成为协作分支上的安全撤销手段。因为它不改写历史其他人正常 pull 就能拿到对等的结果不会出现因为历史分叉导致的同步问题。revert还能批量撤销一段提交git revert A..B # 撤销 A不含到 B含之间的所有提交如果你 revert 之后发现后悔了可以再 revert 那个 revert就能恢复到原来的状态。这一点在多人协作时特别有用——你不需要和别人解释我改了历史只需要追加提交。6. 系统化之后的个人协作流程6.1 我的个人分支模型把上面这些零散操作拼成流程才算真正系统化了。这里分享一下我个人在中小规模项目里的实践。项目核心分支只有main始终处于可部署状态。所有功能开发都在feature/xxx分支上进行git switch -c feature/user-profile # 从 main 切出新分支 # 开发、提交、推送到远端 git push -u origin feature/user-profile # 合并到 main git switch main git pull git merge --no-ff feature/user-profile # 保留合并节点 git push git branch -d feature/user-profile # 本地删除 git push origin --delete feature/user-profile # 远端删除这个流程的好处是历史在main上是一条有清晰合并节点的链每一个功能都有一个明显的入口回溯的时候特别快。两条功能并行时各自的分支互不干扰顶多在合并时遇到一些小冲突。6.2 团队约定与自动化检查一个人用 Git 自由散漫无所谓一旦组队约定比能力重要。我参与过的团队里效果最好的三条规定是提交信息统一前缀规范feat:、fix:、refactor:等配合提交信息检查工具不满足就直接拦截。合并到 main 之前必须跑完测试。本地跑一遍有条件的话接一个自动化流水线在远端跑完测试和代码检查再允许合并。禁止直接往 main 上提交。所有变更都走功能分支和合并请求哪怕只改一个标点符号。自动化方面可以配置一个简单的pre-commit钩子在提交前自动跑代码格式化#!/bin/sh npx prettier --write src/ npm run lint这类钩子能在一开始就把低级问题拦在门外比事后在合并请求里反复挑刺要省力得多。6.3 一些容易忽略但很实用的配置系统的最后一个环节是打磨日常操作的体验。下面这几个配置项是我用了很久之后才发现早该设置的git config --global core.autocrlf input git config --global pull.rebase true git config --global alias.co checkout git config --global alias.br branch git config --global alias.st statuscore.autocrlf input解决的是 Windows 和 Linux/macOS 换行符不一致的问题。这个配置让 Git 在提交时把CRLF转成LF检出时按系统需要再转换能避免一大票整个文件全被标红的诡异 diff 问题。pull.rebase true让git pull默认用 rebase 而不是 merge配合我前面说的未推送的提交可以自由 rebase原则能保持本地提交历史的线性整洁。设置别名不是为了装酷而是为了让自己能用最少的按键完成最频繁的操作。我甚至把git checkout -b配置成了git cob开发体验好了不是一个档次。最后再说一个很多人忽略的命令git reflog。它会记录 HEAD 指针的每一次移动就算是执行了git reset --hard只要你知道 commit id或者从 reflog 里找到曾经的记录都能把丢失的提交找回来。把这个命令放在心里的某个角落遇到再大的操作事故也有底气的存在。