
每个写代码的人早晚都要面对版本管理这件事。刚开始我不太在意直到有一次熬夜改了三天代码因为一次误操作把整个项目覆盖才真正体会到版本管理的分量。后来把Git当成每日必用工具才发现真正高频的“常用命令”就二三十条但每一条用对了都能省下大把时间。这篇东西适合刚接触Git的新人也适合那些已经会用图形界面但说不清底层逻辑、遇到问题只能靠搜索引擎的人。我会把命令背后的“为什么”也讲清楚这样以后遇到再奇怪的报错你也能有个判断方向。1. 先搞清楚Git的设计逻辑再动手记命令1.1 三个区域工作区、暂存区、版本库刚开始学Git最容易懵的就是三个名词工作区、暂存区、版本库。我见过不少新人把 git add 和 git commit 混在一起理解以为 add 就是保存。其实整套模型可以拿写文章投稿来类比。工作区就是你电脑里能看到、能编辑的那些文件夹相当于你的草稿纸暂存区是一个临时存放清单的区域相当于你把已经修改好的段落贴到“待发表”栏版本库则是已经保存下来、带编号的历史记录相当于正式发布存档。git add 是把改动放入暂存区git commit 才是把暂存区的改动正式写入版本库生成一个不可篡改的历史快照。这套设计不是平白无故多出来的环节。有了暂存区你可以把一次改动拆成多个逻辑提交。比如我改了一个页面样式顺带修了一个接口参数两个改动混在一起时就可以分别 add 不同的文件分两次 commit。提交记录干净了后期排查问题和回滚都会轻松很多。1.2 一条命令的完整执行路径看透一次提交流程对理解很多疑难杂症很有帮助。假设你改了一个文件从最开始的编辑动作到最后被保存进版本库实际经历了下面这几步。工作区里的文件被编辑器修改但Git还感知不到具体内容变化只能看出“有改动”。随后执行 git add这相当于把文件内容的一次快照放进了暂存区。最后 git commitGit 会把暂存区的这份快照连同提交时间、提交人、提交信息一起打包成一个 commit 对象挂到当前分支上。提交对象里还记录了“父提交”的指针指向本次提交发生前的那个历史节点这样就形成了一条链式历史。很多人对 Git 的理解停留在“保存版本”的层面觉得 commit 就像文件另存为。其实它更像是在一条时间轴上不断追加节点。每个节点都有自己的哈希值节点之间通过父子关系串联。理解了这条链之后接触 reset、rebase、cherry-pick 时就能明白它们到底在“动”什么。1.3 .git目录里到底存了什么在你的项目根目录下有个隐藏文件夹叫.git那是仓库的心脏。平时操作无非是在生成、调整和读取这个目录里的数据。它里面有几个关键部分HEAD文件记录了当前处于哪个分支config文件保存了仓库级配置refs目录下面存放分支和标签的引用信息objects目录是真正的数据仓库各种对象都压在里面。理解了.git目录你就理解了一个经典问题为什么直接用系统复制粘贴一个项目文件夹到另一台电脑上就不是一个仓库了因为你拷贝的可能只有业务代码并没有涵盖.git里面的全部元数据。合理的迁移方式是直接复制整个项目或者用git clone从远程仓库拉取这样才是一个完整仓库。1.4 注意不是所有文件都在版本控制里这里要说个容易踩的坑。.git只跟踪它看得见的东西默认情况下新建目录和文件并不会自动纳入版本控制。很多新人编译运行项目后发现一堆中间产物被误提交到仓库里就是因为忽略了这一点。后面讲.gitignore的时候我会再展开但先记住一个结论Git 的跟踪对象是你明确 add 过的文件或匹配规则的文件不存在的文件它根本不管。2. 环境准备与命令行基础配置2.1 安装Git与初识命令行Git 本身是跨平台的工具。在 Windows 上官方安装包或者常见的包管理器都可以装macOS 和 Linux 也基本都能通过系统自带包管理器快速搞定。装完之后先在终端里执行git --version确认一下版本太老的版本可能会导致某些命令不兼容尤其是分支切换相关的命令。说句实在话图形客户端确实容易上手但命令行才是理解 Git 的最佳途径。因为每个命令在做什么图形界面通常只显示了结果而命令行会把完整的操作逻辑暴露给你。我也不是让你完全抛弃图形工具而是建议先尽量用命令行把这条路走通等到心中有模型之后再借助工具提升效率。2.2 全局配置身份、编辑器与默认行为刚装好 Git第一件事是配置身份信息否则 commit 时会报错或者提示无法确认提交人。在我的机器上写代码的机器和提交代码的机器可能会分开所以全局身份信息用工作常用的那个邮箱和昵称就好。git config --global user.name 你的昵称 git config --global user.email youexample.com除了身份还有几个推荐设置。可以指定默认编辑器比如 Vim、VS Code 或者你习惯用的任意编辑器这样当 git 需要你输入多行提交信息时会自动弹出来。还有很多人会设置默认分支名比如把init.defaultBranch设为main避免每次手动去改名。核对配置用git config --list它会显示所有生效的配置项。配置优先级从高到低大致是仓库级、全局级、系统级。也就是说如果某个项目里单独设置了 user.email那么在这个项目里提交时会优先用项目级的配置。这个优先级我记得很清楚因为曾经有同事在仓库里配了一个错误的邮箱导致所有提交都记录在了错误的身份下排查了半天才找到原因。2.3 SSH公钥配置与远程认证如果你打算把代码同步到云端仓库推荐使用 SSH 方式连接。相比每次输账号密码SSH 的体验稳定很多身份认证也更安全。生成密钥的时候直接敲下面这条命令ssh-keygen -t ed25519 -C youexample.com生成后公钥默认在~/.ssh/id_ed25519.pub文件里。打开这个文件复制全部内容去你的代码托管平台个人设置里找到 SSH Keys 配置页粘贴保存。验证是否连通可以执行ssh -T gitexample.com如果连接成功会返回一段欢迎提示。这里我要提醒一句托管平台返回的标识信息可能与你注册的用户名不一致只要能看到成功提示说明密钥已经生效。千万不要把这个验证流程误解为登录它只是检查认证是否通过具体操作还是要靠git命令完成。3. 日常提交最开始会用到的那几条命令3.1 提交前的体检status 与 diff写代码时最好养成一个习惯提交前先看一眼仓库当前状态。git status是最常用的命令它告诉你当前分支、暂存区状态、哪些文件被修改但还没 add以及哪些文件完全没有被跟踪。我把它理解成“提交前的体检报告”。光是 status 只能看出文件级别有没有变化具体的改动内容要靠git diff来看。不带参数时git diff比较的是工作区和暂存区之间的差异也就是你上一次 add 之后又改了哪些内容。想看暂存区与版本库之间的差异用git diff --cached。我每次提交前都会执行一遍git diff --cached逐行确认这次将进入历史的改动确实是自己想要的而不是把调试日志、临时代码一起提交进去。3.2 add 的讲究从整包加法到逐块挑选git add最朴素的使用方式是添加指定文件或目录比如git add src/main.js。偷懒一点可以直接git add .把当前目录所有改动加入暂存区。我建议在项目较小、改动集中时用git add .没问题但改动特别多、包含临时文件时就不要无脑全加了。更精细的做法是git add -p它会把文件里的改动按“块”拆开让你逐个决定要不要暂存。比如同一个文件里既有需求代码又有格式化改动你想把后者排除掉用-p就能干净地把两者拆开提交。这个过程刚开始会觉得繁琐但一旦养成习惯你的提交历史会非常漂亮每一笔 commit 都是完整且语义明确的。如果你不小心 add 了不该加入的文件可以用git rm --cached file把它从暂存区移除同时保留工作区文件。切记不要直接git rm那会连磁盘上的文件一起删掉这不是你想达到的效果。3.3 commit 的规范信息写清楚比写多更重要提交信息的质量直接决定日后你以及你的同事排查问题的效率。我见过太多人提交信息只写“update”、“fix”等到一个月后回看历史完全不知道那次修改在干嘛。写提交信息并不需要长篇大论但至少要让人一眼看懂这次改动做了什么。如果只是简短的描述可以直接git commit -m 修复订单金额计算精度问题。如果需要补充背景信息不要带-m直接执行git commit进入编辑器写一行标题空一行再写正文说明原因和影响。如果发现上一次提交的说明写错了或者漏了一个文件可以用git commit --amend修正上一次提交。它会把当前暂存区的内容与上一次提交合并成一个新提交并允许你重新编辑说明。这里有一条铁律只对还没有推送的提交使用 amend。如果已经推到了公共分支改写历史会严重影响其他人的工作。3.4 查看历史log 与 show 的正确打开方式git log是查阅提交历史的基础命令默认输出完整哈希、作者、日期和提交信息。信息量太大时会刷屏我常用的组合是git log --oneline --graph --all一行一个提交并带上分支图形。加-n 5可以只看最近五条记录。想回顾某个提交到底改了哪些文件、哪些行可以执行git show hash。这比盲目点开图形界面去翻版本树要快得多。如果想知道某一行代码最近一次是哪个提交引入的用git blame file就能逐行显示来源。过去排查线上问题时git blame帮我快速锁定过不少“罪魁祸首”这是 Linux 服务端开发里非常刚需的操作。4. 分支操作多人协作的地基4.1 分支的模型与常用命令分支是 Git 里最重要的概念之一简单说它只是一个指向某个提交的可移动指针。开发新功能时从主线拉一条分支出来在主线上继续发布其他内容功能开发完再合并回去彼此互不干扰。创建分支可以用git branch name切换分支可以用git checkout name。更推荐新版本的用git switch name因为 switch 语义明确只做切换。创建并切换一步到位用git switch -c name等价于过去的git checkout -b name。删除分支时git branch -d name只删除已经合并进当前分支或主线上的分支是个安全操作git branch -D name则是强制删除不管分支内容是否合并。我的建议是本地分支删除前先用git branch -d如果报错说分支未合并那就先确认该分支的内容确实不需要保留再考虑用大写-D强制删除。4.2 合并与变基merge 和 rebase 的选择逻辑合并分支有两个主流方式merge 和 rebase。两者的目标都是把另一条分支的提交内容整合到当前分支但结果的历史形态不一样。merge 会保留分支分叉的痕迹执行后继提交时会在历史里增加一个“合并提交”节点。适合在共享分支上汇合持续开发的功能分支因为保留真实的分叉和汇合关系让团队能清晰看到“什么时候做了什么合并”。rebase 则相反它会把当前分支的提交“搬移”到目标分支的最新提交之后历史变成一条直线看起来干净工整。适合场景可以参考这张表对比维度mergerebase历史形态保留分叉和合并节点线性推进无合并节点适用分支公共分支、长期分支个人开发分支、推送前的整理安全性公共分支上使用安全推送到公共分支的提交不要用冲突处理一次性处理多提交可以整体合并逐提交重放冲突可能反复出现我的个人习惯是个人功能分支在合并到主线之前先用 rebase 整理掉那些“补个逗号”“修正上一条提交”的临时提交让主线历史尽量干净。可是提交一旦推送到公共分支我绝不再对这个分支做 rebase否则所有人本地仓库的历史就会分叉后续 pull 会出现一堆莫名其妙的冲突。4.3 解决冲突的标准流程冲突并不少见也并不可怕。当两个分支修改了同一文件同一块区域时Git 不知道谁是对的于是把决定权交给你并在文件里标出冲突段落。一段典型的冲突标记长这样 HEAD 这边是当前分支的代码 这边是合并进来的分支的代码 feature/login处理冲突时我自己的标准流程是先执行git status看看有哪些文件处于 unmerged 状态然后逐个打开文件根据自己的业务判断保留需要的代码。直接粗暴地全选“ours”或“theirs”往往不是好主意因为两边可能各自改了一半逻辑只有完整理解双方意图才能真正解决冲突。改完之后执行git add file标记为已解决最后再提交。rebase 过程中的冲突解决方案略有不同。git rebase遇到冲突会暂停在出问题的那个提交上处理完文件后要执行git rebase --continue继续重放下一个提交。某次实验发现整个 rebase 思路错了想放弃时执行git rebase --abort就能回到操作前的状态。5. 远程仓库让代码真正“上云”5.1 remote、fetch、pull、push 的关系本地仓库是你的工作现场远程仓库才是团队协作的公共枢纽。要让本地仓库和远程仓库建立联系先添加一个 remotegit remote add origin 仓库地址remote其实是远程仓库地址的别名“origin”是默认命名。用git remote -v可以查看当前配置的远程地址。第一次推送时如果远程分支还没建立要加-u参数git push -u origin main-u表示设置上游关联之后直接git push或git pull就可以不必每次都带上远程仓库名和分支名。fetch和pull的差别也很关键。fetch 只是从远程把最新提交下载到本地但不会动你当前的工作区pull 则相当于 fetch 加 merge会直接把远程分支合并进当前分支。我遇到一些把握不准的场景时会先git fetch看两个分支的差异再决定怎么合并避免 pull 直接合并带来的意外冲突。5.2 push 报错的常规解法推送时最典型的报错是“failed to push some refs”意思是本地和远程分支出现了分叉远程有本地没有的提交。这个报错的根源是因为你把历史的“时间轴”改动了通常是远程仓库里已经有别人推过新代码本地却基于旧节点直接提交。正确做法是先把远程更新拉到本地再尝试推送。用git pull --rebase origin main把本地提交先“搁置”起来拉取远程新提交再把本地提交重放到远程最新节点的后面。如果本地改动和远程改动没有冲突整个过程一气呵成如果冲突了按前面讲过的冲突解决流程处理。这里提醒两句一是不到万不得已不要用git push --force。强制推送会覆盖远程历史一旦操作失误后果很难挽回建议用git push --force-with-lease代替它会在推送前检查远程是否发生了预期外的变化安全性高很多。二是你在本地做了 rebase、reset 等改写历史的操作后推送到远程通常要用强推才能生效这种情况下更要谨慎确认目标分支上只有你一个人在工作。5.3 为什么更推荐 pull --rebase我在团队里推荐默认使用git pull --rebase。原因是本地分支频繁从远程拉取更新时如果用默认的 pullfetchmerge每次拉取都可能生成一个合并提交时间久了历史里全是“Merge branch xxx into xxx”这类噪音。而使用 rebase 方式你所有的本地提交就像排在你拉取到的远程提交之后历史干净接近一条直线。有个边界要反复提醒rebase 适合在你自己的私有分支上做不适合在共享的固定分支上做。如果团队里大家都遵守这条规则历史就会非常清楚。如果已经推上去的提交还在处理中那宁可先用 merge也别为了“历史好看”去改写公共记录。6. 回滚与修复犯错了怎么办6.1 reset 的三种模式软、硬、混合怎么选代码写错了、提交打错了第一时间想到的是回滚。Git 里最基础的回滚命令是git reset它有三种模式对应你希望保留多少工作成果。参数模式作用范围工作区保留与否暂存区保留与否典型场景--soft仅回退提交记录保留保留所有改动在暂存区想重新拆分提交但内容不想丢--mixed默认回退提交记录并清暂存区保留清空到工作区想重写提交内容重新 add--hard回退提交并清空暂存区和工作区不保留不保留确认改动彻底作废举例说git reset --soft HEAD~1会把最近一次提交从分支上摘掉但你的改动全部保留且在暂存区git reset --hard HEAD~1则是把最近一次提交连同工作区改动全部丢掉慎用。进行 reset 操作前我建议先git log --oneline确认你要回退到哪个节点哈希错了后果会很麻烦。reset 严格来说是针对尚未推到远程的提交的。如果你已经把提交推送到了公共分支那么 reset 之后其他人那里还是旧历史推上去会乱套。这种场景下应该用下一节说的 revert。6.2 revert 与 reset 的本质区别git revert不是“回退到某个节点”而是“生成一个与目标提交相反的新提交”。比如你提交了 a 这个提交revert a 会生成一个提交 bb 的内容恰好把 a 的改动整体撤销。历史里同时保留 a 和 b长期记录看起来更完整也不会破坏其他协作成员对历史共同的认知。什么时候用哪个我总结得很简单还没推送出去的错误用 reset 清理已经推送到公共分支的错误用 revert 撤销。revert 的本质是向前走一步不是把历史折返因此团队协作场景里安全很多。6.3 误删分支、误改文件后的自救误删分支或者误执行了git reset --hard不用立刻陷入绝望git reflog往往能把你的操作记录打捞回来。reflog 是“引用日志”记录了 HEAD 和各种分支在本地仓库里的每一次变更包括你曾经切换过的分支、reset 之前所在的位置。比如你误删了一个分支先用git reflog找到那个分支最后一次提交的哈希再执行git branch 分支名 哈希就能恢复。同理误 reset 之后也可以用同样方法找回旧版本。要注意 reflog 的内容通常只会保留一段时间一般默认是90天而且本地 clone 下来的仓库 reflog 为空所以发现问题要尽快处理。git clean也是一个危险命令。它用来清理未跟踪文件但git clean -fd会把未跟踪的文件和目录直接删掉而且不能通过 Git 命令恢复。我在使用 clean 前会先加-n预演一遍确认要删除的清单里没有重要文件才真正执行。6.4 stash临时切换现场的救命稻草正在改功能 A突然线上来了紧急 bug 要切分支修改动还没提交直接切换分支会报错或者把改动带过去。这时git stash就是最顺手的方案。git stash # 把当前改动暂时收起 git stash list # 查看保存的 stash 列表 git stash pop # 恢复最近一次 stash 并删除该记录 git stash apply # 恢复最近一次 stash 但保留记录默认情况下 stash 不会保存未跟踪文件如果你需要连新建的文件一起收起用git stash -u。恢复 stash 时如果和目标分支有冲突会像 merge 冲突一样要求手动解决。我一般会在一天内恢复 stash放太久容易忘记当时改到哪一步恢复时反而混乱。7. 高频技巧让命令组合出花来7.1 cherry-pick精准移植提交有时候只想把其他分支上的一个或几个提交弄到当前分支不想全局合并。比如紧急修复提交打在 release 分支上希望 master 分支也修复同样的 buggit cherry-pick hash就是干这个的。它可以指定单个或多个提交哈希把对应的改动在新分支上重放。如果害怕冲突最好先确认目标提交与当前分支改动重叠不大。cherry-pick 产生的新提交会生成新的哈希值时间、摘要都会标记为当前操作完成。如果你希望保留原提交的信息可以在提交流程里不要填写新的信息直接沿用即可。遇到冲突时处理方式和 merge 类似解决完git cherry-pick --continue想放弃就git cherry-pick --abort。7.2 tag 打标签与版本发布tag 是对某个提交做的一个固定标记常用于版本发布的里程碑。比如你完成了 v1.2.0 版本的代码希望以后任何时候都能快速切到这个版本就可以打一个标签。git tag v1.2.0 git tag -a v1.2.0 -m 发布 1.2.0 版本轻量标签只是指向某个提交的指针附注标签会额外保存打标签的时间、作者和说明。我推荐用附注标签因为发布版本通常需要记录责任人、发布说明轻量标签在多少天后往往被遗忘。标签需要单独推送git push origin v1.2.0如果要一次推送所有本地标签可用git push origin --tags但我的习惯是只推送明确需要的标签避免把零散的本地实验标签传到公共仓库。7.3 alias 别名配置高频命令加上别名之后日常操作舒服很多。Git 支持在配置里给命令起别名也可以用一个别名触发一串组合命令。我维护的.gitconfig里有这样几个git config --global alias.st status git config --global alias.ci commit git config --global alias.lg log --graph --prettyformat:%h - %an, %ar : %s --dateshort配好之后git lg就能看到一张非常直观的提交关系图。别名本质上是字符串替换它能帮你把冗余参数收敛成简短指令。新人阶段不要反着来——先熟悉完整命令再配置别名否则看到别人的别名配置会一头雾水。7.4 .gitignore 的常见坑.gitignore是仓库里的忽略规则文件用来告诉 Git 哪些文件不纳入版本控制。编译产物、依赖目录、本地配置、IDE 配置等都应该忽略否则仓库会越来越臃肿还会把每个人的本地差异反复提交上去。这里有两个高频踩坑点。第一已经被 Git 跟踪的文件不会因为加了 ignore 规则就自动停止跟踪你必须先git rm --cached file把它从版本控制里移除。第二忽略规则的匹配方式和普通通配符不完全一样比如build/表示忽略 build 目录*.log表示忽略所有 .log 文件但/foo只匹配根目录下的 foo。配置后建议用git status检查一下是否生效有些目录结构复杂时需要调整规则。8. 常见问题与排查实录8.1 提交错分支怎么办有次我在主线分支上写开发功能写完才发现当前分支切错了所有提交都落在了主线。这个问题可以按三步抢救。先基于当前 HEAD 创建一个新分支把刚才的提交留在新分支里然后回退主线分支的提交最后切换到新分支去继续工作。git branch feature/temp git reset --hard HEAD~1 # 根据提交数量调整 git checkout feature/temp这么做的本质是让“错误提交”从一个火坑挪到正确的位置。重要的是不要在回退之前 push 到远程否则公共历史已经被污染处理起来会复杂很多。8.2 冲突中途想退出merge 或 rebase 过程中如果发现冲突过多、这条路走错了不必硬着头皮一个个解决。merge 冲突可以用git merge --abort回到合并前的状态rebase 冲突对应git rebase --abort。两者都会把工作区还原到操作开始前的样子如果你在冲突期间手动改了好几个文件这些改动也会一并回滚所以执行前一定要确认自己没有想留的修改。还有一个更温和的方案是查看冲突文件数量不多时直接读完所有标记后一次性处理。只要流程熟练大多数冲突其实十分钟内都能解决abort 主要还是用在“方案选错”的场景。8.3 代码被覆盖了还能恢复吗git reset --hard之后心里最慌的往往是“改动的代码丢了”。不要先急着重建文件先去查git reflog。reflog 里记录了旧位置你可以从那里把丢失的提交或分支找回来。我曾经在一个迭代收尾时误操作 reset 了刚写半天的代码后来正是靠 reflog 恢复了现场。真正没有退路的情况是同时执行了git clean -fd把未跟踪文件也删了Git 完全不知道这些文件的内容恢复基本无望。所以在玩 clean 之前一定要预演在玩 hard reset 之前一定要确认能用 revert 就别用 reset。8.4 常见错误信息速查表平时被问到最多的问题我整理成了一份速查表遇到报错时先对照定位原因。报错信息原因处理方式fatal: Not a git repository当前目录不是 Git 仓库先 git init 或进入仓库目录refusing to merge unrelated histories两个仓库没有共同历史确认分支正确后可加 --allow-unrelated-histories但慎用failed to push some refs本地落后于远程或历史分叉先 git pull --rebase再重新 pushAuthentication failed认证方式错误检查 SSH 密钥是否添加或 token 是否过期You have unmerged paths冲突尚未解决处理冲突文件后 git add再提交fatal: destination path already existsclone 目标目录已存在同名项目更换目录或者确认已有仓库是否可以继续使用再补充一个易误操作的点遇到refusing to merge unrelated histories时立刻去搞清楚两个仓库是怎么变成“无关联”的盲目允许合并可能会让历史里凭空多出两棵互不相干的树后续排查很麻烦。如果这个报错来自你故意把两个独立项目合并一起那使用对应参数就是合理场景否则更建议重新建仓库或者用 cherry-pick 精确迁移。最后再分享一个小技巧不管遇到什么问题先git status总不会错。它会把当前工作区、暂存区、分支信息完完整整地展示出来大多数问题在这个环节就能定位。我自己平时很少背完整命令手册但每一条命令执行前都会想清楚它会影响哪个区域也就不会因为误操作把仓库搞得一团糟。Git 这东西真用熟了以后会觉得它像一把趁手的瑞士军刀用得越多越能体会到那些设计细节的妙处。