如果你写过项目代码,一定经历过这种场面:功能快做完了,想留个安全版本,于是老老实实复制一份文件夹,命名为project_final,过两天又复制成project_final_2024,再后来变成了project_final_真不改了。等到某天桌面上同时躺着四五个一模一样的目录时,你已经彻底分不清哪份能跑、哪份能删。Git 就是来解决这种乱象的,它是目前使用最广泛的分布式版本控制系统,全世界的开发者几乎都在用。这篇文章我打算用一条完整链路,把 Git 从安装配置、核心命令、分支合并到疑难杂症排查全部过一遍,适合刚起步想系统学习 Git 的新手,也适合用了很久但总在几个坑里反复打转的老同事。
1. Git 到底是什么:先把底层逻辑搞懂
1.1 从"备份"说到"分布式":Git 解决了什么问题
理解 Git,不能只学命令,得先明白它区别于传统备份方式的核心。你在文件夹后面加"v1""v2"本质上是在做版本管理,但这种方式只有最终版本有意义,过程全部丢失:谁改的、改了哪几行、为什么改,全都无从追溯。Git 把所有历史、作者、时间、备注都记录下来,任何一次改动都能回退、对比、追溯,这才是版本控制的意义。
Git 属于分布式版本控制,和 SVN 这类集中式方案不一样。集中式版本库有一台中央服务器,所有人提交都要连上它才能操作,服务器挂了或者网络断了,提交直接卡死。而 Git 在git clone的时候把整个仓库历史全部拉到本地,之后的提交、查看历史、创建分支、回退操作全部在本地完成,不需要网络,只有在推送和拉取时才需要和远程仓库通信。这个特性带来的最大好处是:本地就是一份完整备份,远程服务器哪天坏了,任何一个人手里的克隆都能完整恢复项目。
1.2 快照存储:Git 和普通文件夹备份的根本区别
Git 对文件历史的管理方式,不是记录"差异补丁",而是采用快照机制。每次提交时,Git 会保存当前所有文件状态的快照,如果某些文件自上次提交以来没有改动,Git 不会重复存储相同内容,而是用指针直接指向之前的文件对象。这就像一个高效的存档大师,只对发生变化的部分真实存放数据,没变的文件通过引用复用,既不浪费空间,还能保证任意时刻都能完整还原当时的项目状态。
你可以把快照想象成拍立得:每次提交就是给项目拍一张照片,存进一本相册(仓库)。想回到某个时间点,翻到那张照片照做就行,不需要从一堆补丁里倒推重建。由于每次提交都包含完整逻辑状态,Git 的分支成本也极低——新建分支只是创建一个可移动的指针,这也是后面讲分支合并时一切操作都很轻量的原因。
1.3 工作区、暂存区、仓库:三个区域必须分清
很多人初学 Git 卡壳,主要就是没分清三个区域:工作区(Working Directory)是你电脑上能看到的文件夹;暂存区(Index/Stage)是个中间缓冲地带,用git add把改动放进这里;本地仓库(Repository)存放已经提交的历史快照,由 HEAD 指针标记当前所在位置。
我常用一个日常类比来理解这三层:工作区是厨房灶台,食材(文件改动)摆在那里;暂存区是备菜台,你把要下锅的菜选定好放到备菜台;仓库是冰箱,做好一份菜就冻一份进去。这样设计的好处是,你可以一次性改动多个文件,但只挑其中有意义的几个提交,把无关紧要的临时改动留在工作区,不用每次提交都"一刀切"。后面所有命令,本质上都围绕这三个区域之间的移动展开。
2. 环境准备:安装、初始化、身份与密钥配置
2.1 不同系统下的安装方式与快速验证
Windows 上安装 Git 比较简单,去 Git 官网下载对应系统的安装包,一路 Next 即可。安装过程中有一个选项需要留意:在"Adjusting your PATH environment"这一步,建议选"Git from the command line and also from 3rd-party software",这样系统自带的命令提示符和 PowerShell 里也能直接用 git 命令,避免只能在专用小窗口里操作的尴尬。macOS 上如果装了 Homebrew,直接执行brew install git,否则下载官方 pkg 安装包也行。Linux 发行版则用对应包管理器,Ubuntu/Debian 是sudo apt install git,CentOS/RHEL 是sudo yum install git。
安装完成后打开终端输入git --version,只要能看到形如git version 2.40.0的输出,就说明安装成功。这里我额外提一句,Windows 环境下强烈建议把 Git Bash 作为日常操作终端,它模拟了 Linux 环境,很多路径和命令习惯跟服务器端保持一致,能少踩不少 shell 语法不一致的坑。我见过太多同事在 cmd 里拼命令拼到怀疑人生,换个 Git Bash 秒清爽。
2.2 初始化仓库与全局身份配置
装好后先做两件事:创建/克隆仓库,配置身份。
在一个空目录里执行git init,这个目录就变成了 Git 仓库,内部会生成一个隐藏的.git文件夹,所有版本历史都存在这里。如果是加入已有项目,更推荐git clone <远程仓库地址>,一条命令把远程仓库完整拉到本地,自动完成初始化和远程关联。
身份配置是一切的起点,Git 每次提交都会记录作者和提交者信息,未配置的话很多托管平台会拒绝操作或显示乱码名字。执行:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"--global表示对当前操作系统的所有仓库生效,属于个人级别配置。如果某个仓库需要单独身份(比如公司项目用公司邮箱),在该仓库目录下不带--global执行同样命令,仓库级配置会覆盖全局配置。个人项目、公司项目混用电脑时,这个技巧非常实用,相当于给不同仓库分别发了一张不同人名的工牌。查看当前配置用git config --list,随时确认自己的"身份"是否对得上。
2.3 SSH 密钥生成与免密配置
每次推送代码都要输一遍账号密码确实折磨人,配置 SSH 密钥后用密钥认证就能实现免密。在 Git Bash 里执行:
ssh-keygen -t ed25519 -C "你的邮箱"一路回车即可,会在~/.ssh目录生成一对文件:id_ed25519是私钥,绝不能外传;id_ed25519.pub是公钥,需要添加到代码托管平台。用编辑器打开公钥文件,把内容完整复制。然后登录 GitHub、Gitee(码云)或其他平台,在个人设置里找到"SSH keys",粘贴保存即可。
配置完成后测试连接:
ssh -T git@gitee.com ssh -T git@github.com如果看到提示"成功认证",就说明免密配置完成。认证失败的问题后面会专门讲,这里先记住一个认知:SSH 的验证对象是密钥对,不是账号密码。
2.4 多台电脑共用同一个托管平台账号
现在程序员基本都会遇到这个场景:家里一台电脑、公司一台电脑,都要往同一个 GitHub/Gitee 账号推送代码。有两种推荐做法。
第一种:每台电脑各自生成一对新密钥,然后把每台机器的公钥都添加进同一个账号的 SSH keys 列表。这是官方推荐的做法,因为私钥不需要离开各自机器,安全性和灵活性更好。第二种:把一台电脑上生成的私钥拷到另一台电脑的~/.ssh目录,配上对应权限即可,好处是省事,但私钥文件复制过程中存在泄露风险,除非你知道自己在做什么,否则不推荐。
实际操作中还有一个关键细节:如果公司电脑上已经配置过其他平台的 SSH 密钥(比如公司内网 GitLab),直接用默认文件名生成新密钥可能会互相覆盖。此时需要给不同平台的密钥指定不同文件名,并在~/.ssh下新建一个config文件,按域名匹配不同的私钥路径。这个进阶技巧属于"多账号 SSH 管理",等你碰到不同平台证书冲突时再回来看这段,能省下一大段排查时间。
3. 核心命令实操:每天都会用到的提交与回退
3.1 标准提交流程:status、add、commit 的配合
日常开发中最频繁的流程就是这三条命令的组合:git status查看变动、git add暂存改动、git commit提交快照。
git status git add . git commit -m "feat: 增加登录模块"git status永远是你操作前的第一道保险。它会明确告诉你哪个文件被修改了、哪个文件还在暂存区、当前分支落后远程多少提交。git add .表示把当前目录下所有变动文件加入暂存区;如果你只想暂存特定文件,用git add src/index.js指定路径,精准控制提交范围。git commit -m则是生成一次快照提交,-m后面的信息就是这次提交的备注。
关于提交备注,我强烈建议早点形成自己的规范。现在社区比较通用的写法是 Conventional Commits(约定式提交),在备注开头用类型前缀,比如:feat表示新功能,fix表示修复 bug,docs表示文档改动,refactor表示重构,style表示格式调整。举个例子,git commit -m "fix: 修复登录按钮在移动端无法点击的问题",别人一眼就能看出这次提交的意图。别再用"1""2""update"这种无意义备注了,你三天后回看历史会感谢当时的自己。
3.2 提交写错了怎么补救:commit --amend 的正确用法
提交完之后发现注释写错了,或者少提交了一个文件,怎么办?git commit --amend就是用来修改最近一次提交的。
如果你只是想换注释,执行:
git commit --amend -m "新注释"如果想补充文件,先git add那个遗漏的文件,再执行git commit --amend --no-edit,--no-edit表示保留原有注释,只将新的暂存文件合并进上一次提交。这个命令的本质是"用一个新的提交覆盖上一次提交",旧的提交会被替换掉,所以注意振幅范围:只适用于还没有推送到远程的本地提交。如果已经 push 到远程了再 amend,本地和远程历史就分叉了,后续覆盖会很麻烦。
真实场景里我经常这样用:写完代码提交后,自我 review 发现一行调试日志忘记删除。这时候 amend 比再补一个"remove debug log"的提交更干净,历史看起来就像这个调试日志根本没出现过。但补提交也并非坏事,关键看你所在团队对历史的洁癖程度,多人协作的公共分支上,任何改写历史的操作都要格外谨慎。
3.3 撤销操作全家桶:reset、revert 与后悔药分级
撤销操作是最容易把人绕晕的板块,因为后悔程度分好几级。我按从轻到重梳理成一张速查表:
| 撤销场景 | 命令 | 说明 |
|---|---|---|
| 工作区改乱了想放弃 | git restore <文件> | 用仓库版本覆盖工作区改动 |
| 文件已 add 想撤出暂存区 | git restore --staged <文件> | 从暂存区退回工作区 |
| 撤销最近一次本地提交,保留改动 | git reset --soft HEAD~1 | 提交回退,文件改动保留在暂存区 |
| 撤销提交且保留在工作区 | git reset --mixed HEAD~1 | 提交回退,改动放回工作区 |
| 撤销提交且连同改动一起丢弃 | git reset --hard HEAD~1 | 危险程度最高,务必先备份 |
| 撤销已推送到远程的提交 | git revert <commit> | 生成一个反向提交,不改历史 |
如果代码已经 push 到远程分支,我的建议非常明确:优先用git revert。git revert不是删除历史,而是生成一个"反向操作"的新提交,把之前的改动抵消掉。好处在于历史完整,其他人正常 pull 不会发生冲突。git reset --hard虽然也能配合git push --force把远程历史改写回旧状态,但这需要团队协作,一旦别人已经基于旧提交开发,就会产生大量冲突和混乱。公共分支上强制改写历史属于"高危操作",能不用就不用。
有个热词说得很具体:IDEA 里如何撤销已经提交到远程分支的代码。其实底层原理一样:在 IDEA 的 Git 面板选中那条提交记录,右键选择 Revert Commit,生成的提交推送到远程即可。注意不要选错了按钮,IDEA 里的 Reset 对应的是本地回退,不配合 force push 对远程没有影响。
3.4 分支管理:创建、切换、合并与冲突处理
分支是 Git 最值钱的特性。新建分支只是创建一个指针,成本极低,因此合理的使用习惯是"每个功能一个分支",开发完再合并回主分支,不要所有人挤在 master 上改来改去。
基础命令:
git branch 新分支名 # 创建分支 git checkout 新分支名 # 切换分支 git switch 新分支名 # 新版命令,语义更清晰 git checkout -b 新分支名 # 创建并切换 git merge 要合并进来的分支 # 把该分支合并到当前分支合并之后,代码历史的整合方式有merge和rebase两条路线。merge会产生一个真实的合并提交,保留两条分支的分叉历史,适合公共分支,操作安全。rebase会把当前分支的提交"重新摆放"到目标分支顶端,历史像一条直线,更整洁,但会改写提交,适合还没推送出去的本地分支。我的个人习惯是:自己的功能分支合入主分支前用 rebase 保持整洁,多人协作的共享分支用 merge 保证安全。
冲突是合并绕不过去的一道坎。比如两个分支都修改了config.js的同一行,Git 无法判断该采用谁的内容,就会在文件里插入冲突标记。打开冲突文件,你会看到形如:
<<<<<<< HEAD 当前分支的代码 ======= 被合并分支的代码 >>>>>>> feature/login=======上方是当前分支的内容,下方是被合并进来的内容。手动整理成最终想要的代码后,删除所有冲突标记,再执行git add和git commit,合并就完成了。如果改到一半发现冲突太难看,执行git merge --abort可以干净回到合并前状态。
日常协作另外一个高频场景是:代码不小心写在了错误的分支上,比如我在 master 上开发新功能,写了半截发现应该去 dev 分支开发。这时候不需要重新敲一遍代码,利用git stash就能完美转移。在 master 上执行git add(可选)然后git stash,切回 dev 分支后执行git stash pop,改动就原样回来了。这就是 stash 的场景价值,下文单独展开。
3.5 手头改到一半想切分支:stash 的魔法
场景很典型:你正在 dev 分支上实现一个功能,代码写了一上午还没完成,突然需要切回 master 修个紧急线上 bug。问题是,git checkout切分支的时候,如果工作区存在未提交的改动,Git 会拒绝切换,提醒你先处理这些改动。
此时git stash就派上用场了。执行git stash,Git 会把当前工作区和暂存区的改动临时保存到一个栈结构里,工作区恢复干净,你就可以愉快地切换分支干活。修完 bug 回到 dev 分支,执行git stash pop,改动立刻恢复。想看存了多少条记录,用git stash list;不想要某条存档了,用git stash drop。
这个命令群的使用频率远超很多人的预期。除了跨分支转移工作内容,它还能用于:实验性代码快速备份、pull 远程改动前临时避开本地冲突、在两个功能任务之间快速切换。我自己的习惯是:超过半小时的改动,在决定切换到另一个分支前一定先 stash 并写清楚备注——git stash push -m "登录模块 WIP",这样即使过了几天,我也能一眼认出这堆改动的来龙去脉。别问我是怎么知道需要这个备注的,经历过五六条"无题 stash"满屏飘以后,你会主动养成这个习惯的。
4. 进阶场景:IDE 集成、子模块与仓库安全
4.1 终端之外的图形工具与 IDE 接入方式
不少人是被命令行吓退的,其实 Git 的图形工具已经很成熟,日常操作完全可以用图形界面完成。
Windows 上常见的是小乌龟(TortoiseGit),安装后会在文件夹右键菜单上增加 Git 相关选项,提交、更新、换版本都能点点鼠标完成,中文界面做得也比较好,适合公司团队规范化使用。Git 自带的 Git GUI 在 Git Bash 里输入git gui也能打开,功能基础但胜在原生。如果你觉得自带 GUI 太简陋,SourceTree 和 Fork 也是口碑不错的跨平台客户端,提交历史可视化做得非常直观。
IDE 集成层面,最常用的是 VS Code 和 IDEA(IntelliJ 系列)。VS Code 自带 Git 面板,改动的文件会显示 M 标记,点击行号旁的加号就能直接暂存,提交按钮就在顶栏,配一个 GitLens 插件,每行代码的作者和提交信息直接悬浮显示,体验很好。IDEA 则把 Git 功能集中放在右上角的 Git 工具窗口,分支切换、提交、推送、更新都有图形按钮,还内置了冲突解决的可视化工具,比手动编辑冲突标记方便太多。
这里有一个热词问得比较多:IDEA 里修改提交的账户。如果你用 Git 命令行配置了用户名和邮箱,但 IDEA 提交时显示的还是旧账号,多半是因为 IDEA 启用了自己的 SSH 配置或者使用了凭据管理器里的缓存。处理方法有两种:一是在 IDEA 的 Settings → Version Control → Git 里检查 SSH executable 和 credential 设置;二是修改操作系统的凭据管理器(Windows 搜索"凭据管理器",找到 git 相关条目删除),下次推送会让重新输入账号密码。本质是身份信息缓存冲突,清掉缓存即可。
4.2 submodule 子模块的正确打开方式
当你需要在一个项目里引用另一个 Git 仓库时,submodule 是官方给出的解决方案。典型场景:公司有一个公共组件仓库,多个业务项目都要依赖它,而且希望组件升级时,业务项目能够按需选择跟随哪个版本。
添加子模块的命令:
git submodule add https://github.com/某公共仓库.git 子模块目录名执行后仓库里会出现一个子模块目录名的目录,以及一个.gitmodules文件,记录子模块的 URL 和路径。需要注意的是,子模块目录里不会直接放那个仓库的完整内容,它在 Git 看来只是一个 Gitlink 指针,记录了某个特定 commit 的引用。别人 clone 你的主仓库后,子模块目录是空的,需要执行:
git submodule update --init --recursive才能把子模块内容拉取下来。--recursive是处理嵌套子模块的,如果你的子模块自己也引用了子模块,必须带上它。更新子模块到最新提交,要进入子模块目录执行git pull,然后回到主仓库提交一次,记录新的指针位置。
submodule 使用中最大的坑是:很多人忘了子模块本质上是一个独立的 Git 仓库这个事实,导致子模块内部切换分支后主仓库的指针状态混乱。我的建议是,小团队内部如果可能,尽量用包管理工具(npm、Maven 等)替代 submodule,把公共代码发布成依赖包,引用双方都省心。只有在确实需要对公共代码进行本地联调、且团队对 submodule 机制都很熟悉时,再使用它。
4.3 提防 .git 目录泄露:开发者的底线意识
.git目录是整个仓库的大脑,里面存放着所有历史快照、对象数据和分支引用。如果项目被错误地部署到了服务器,并且 Web 服务器配置允许访问静态文件,攻击者就可能通过访问example.com/.git/config这类路径探测到这个目录的存在,进而利用工具慢慢还原出整个项目的源码,包括历史版本中可能存在的密钥、接口地址、内部注释等敏感信息。
对使用 Git 的开发者来说,这是一个必须刻进 DNA 的安全底线。部署项目时,一定不要把.git目录上传到生产环境的可访问路径;Docker 构建镜像时要通过.dockerignore排除它;写服务端代码时,要确认 Web 容器的访问规则不会暴露点开头的隐藏文件;如果使用一键部署工具,也要检查生成的文件列表里是否混入了.git。项目在本地无所谓,一旦涉及对外服务,这个细节就是高危项。
很多刚接触项目的同事会问我:"我 git push 前都得把 .git 目录删掉吗?"完全不用,.git 存在于项目目录里是 Git 正常工作的基础,部署不是把整个项目目录复制上服务器这个过程本身的问题,问题在于部署产物和服务配置。你可以使用打包构建产物部署,或者明确在 Web 服务器配置中拒绝以"点"开头的所有文件访问。从源头避免暴露,比事后补救轻松得多。
4.4 少踩 Git 环境细节:换行符、大小写与 token
Git 在一些细节上真的会让人挠头,这些细节单独写都够一篇篇排查长文,我在这里打包说三个。
换行符问题是 Windows 用户最经常遇到的。Windows 文本默认用 CRLF(回车换行),Linux/macOS 用 LF(换行),Git 默认有一个 autocrlf 机制,如果配置不一致,你会发现每次拉取代码后文件都被标记为已修改,烦不胜烦。推荐做法:Windows 仓库设置全局git config --global core.autocrlf true,提交时自动转成 LF,检出时转成 CRLF;macOS/Linux 设置为input,提交转 LF,检出不动。团队统一这个配置,能消灭一大批无效 diff。
文件大小写问题,Git 默认是彪悍的大小写不敏感追踪:你重命名文件时只改了大小写,Git 可能认不出来。在 Windows/macOS 上尤其常见。稳妥做法是执行git mv 旧文件名 新文件名显式告诉 Git 重命名,或者修改配置core.ignorecase false让它严格区分。
token 认证问题,主要出现在用 HTTPS 方式推送到 GitHub/Gitee 时,密码框要求填的不再是账号密码,而是一个个人访问令牌(Personal Access Token)。Git 提示认证失败时,回想一下你是不是把旧密码和这个 token 搞混了。到托管平台的个人设置里生成新 token,复制它粘贴到密码位置即可。如果 Windows 上 Git 老是用缓存旧的凭据,就在凭据管理器里删除对应条目,或者在命令中用带 token 的 URL 方式:
git remote set-url origin https://<token>@github.com/用户名/仓库名.git需要提醒的是,token 出现在 URL 里会留在.git/config中,明文的,用完记得换回无 token 的 URL。
5. 高频疑难杂症排障实录
5.1 ssh 认证失败:从日志到密钥的完整排查路线
SSH 认证失败是 Git 使用中最常见的报错,没有之一。现象通常是:git push时提示Permission denied (publickey),或者执行ssh -T git@gitee.com出现Permission denied。
按下面这个顺序排查,基本都能定位到问题:
第一步,测试链路连通性。执行ssh -vT git@github.com(带 -v 打印详细调试日志),观察输出中是否识别到了你的密钥文件。如果日志显示尝试了id_rsa、id_ed25519等路径但都被拒绝,说明密钥根本没有被采用。
第二步,确认本地密钥存在且路径正确。检查~/.ssh目录下是否有私钥文件,并用ssh-add -l查看当前会话加载了哪些密钥。如果你重新生成过密钥,系统可能缓存了旧路径,需要ssh-add 新私钥路径重新加载。
第三步,确认公钥是否添加到了平台。复制公钥内容,登录托管平台检查 SSH Keys 列表。这里有个隐蔽坑:Windows 用户复制公钥时可能会带入换行符或空格,粘贴时注意去掉多余字符。
第四步,检查远程仓库地址类型。执行git remote -v,如果 URL 是https://开头,那 SSH 密钥根本不参与认证,此时报错应该排查账号密码或 token。只有git@开头的 SSH 地址才走密钥验证。
我遇到过最多的案例是:同事在 Gitee 上配置了密钥,但项目 clone 时用的是 https 地址,之后一直报认证失败。换成 SSH 地址后问题秒解。先确认通信协议,再排查密钥,效率最高。
5.2 Git 提示 open /dev/null or dup failed 怎么办
这个报错看上去很诡异,实际却没那么复杂。/dev/null是类 Unix 系统中的特殊设备文件,用于丢弃写入的数据,Git 在执行某些子进程调用时,会尝试把子进程的标准输入输出重定向到/dev/null。如果当前环境里这个路径不存在或不可访问,就会报出open /dev/null or dup failed: no such file or directory。
最常见的触发场景是 Windows 下的 Git Bash 在某些受限环境(如部分终端模拟器、特殊 shell 环境)中运行时,环境变量或路径解析出了问题。排查和解决路线如下:
先确认/dev/null在你的终端环境中是否存在。在 Git Bash 里执行ls -l /dev/null,如果提示不存在,说明这个终端环境本身就不完整,可以尝试改用原生的 cmd 或 PowerShell 窗口执行 Git 命令,或者在 Git Bash 的 Settings 里检查启动 shell 是否被改成了奇怪的东西,恢复默认的 bash 路径。
其次,检查 Windows 上有没有安全软件在拦截进程句柄继承。有同事遇到过卡巴斯基导致 Git 子进程无法创建文件描述符,临时关闭防护后问题消失。再一个方向是 Git 版本过老的兼容性问题,升级到最新版 Git for Windows 往往能解决一批莫名其妙的句柄错误。
这个报错最大的特点是"心理冲击大于实际风险":它通常意味着某个 Git 内部操作(如差异计算、hook 触发)的子进程初始化失败,但一般不会破坏仓库数据。遇到后先重启 Git Bash,再按以上环境检查,绝大多数情况都能恢复正常。
5.3 其他常见小毛病速查
还有一些故障出现频率同样很高,单独整理成速查表,下次遇到直接对照:
| 症状 | 常见原因 | 处理方式 |
|---|---|---|
| 提交时提示 "Please tell me who you are" | 没有配置 user.name 和 user.email | 执行全局或仓库级 config 配置 |
| pull 时提示 "Your local changes would be overwritten" | 本地文件有改动且与远程冲突 | 先 stash 或 commit 本地改动,再 pull |
| push 被拒绝 "non-fast-forward" | 远程已有本地没有的提交 | 先 pull 合并,再 push |
| 文件太大时 push 失败 | 单个文件超过平台限制 | 使用 Git LFS 管理大文件 |
| 中文文件名乱码 | core.quotepath 默认转义 | git config --global core.quotepath false |
git clone时 SSL 证书错误 | 自签名证书或代理环境 | 检查 SSL 配置,必要时配置合适的 CA 证书 |
| 误删了分支但想找回 | reflog 记录了所有 HEAD 变动 | git reflog找到分支原指向的 commit,重新创建分支 |
git reflog是另一个值得特别保护的救命命令。它记录了所有 HEAD 引用的历史变动,相当于 Git 的"操作日志"。分支删除、reset 回退后,只要 commit 对象没有被 GC 清理,都能通过 reflog 找到那个 commit 的哈希值,然后重建分支。我把它比作后悔药的最后一层保险,比任何"高级恢复工具"都好使。
5.4 分支历史混乱时的救急思路
线上出过这么一个事故:一个同事在 master 上直接开发了两个星期,期间被其他人推送了好几次版本,他自己也在本地不断提交。最后他发现 dev 分支才是应该开发的地方,但本地 master 的提交历史已经和远程岔开了一大截。
如果直接 push,远程会拒绝,因为存在分叉且本地有不少别人没有的提交。这时候标准做法不是硬推,而是先把远程最新代码拉下来合并冲突,再提交推送。我给出的建议是:先git fetch origin把远程状态完整拉取,然后在本地 master 上执行git merge origin/master解决冲突,确认无误后再 push。整个过程不要用 force push 去覆盖远程,特别是共享分支上,强制覆盖意味着丢弃别人提交的代码,属于事故级别的操作。
另一个常见救急是"代码在错误分支想搬到另一个分支",我前面提到过 stash 方案,但 stash 只适用于改动未提交的情况。如果代码已经在错误分支提交过了,则用 cherry-pick 更干净:切到目标分支,执行git cherry-pick 那个提交的哈希,把指定提交复制过来。和 stash 相比,cherry-pick 不移动原分支,只是把提交的改动重新应用到新分支上,保留了提交作者和注释,适合记录较完整的正式提交。在这些操作之前,我都建议先看一眼git log --oneline --graph,图形化输出能让你搞清当前分支和各个引用之间的真实位置关系,定位准确再动手,比瞎试命令高效得多。
6. 最后分享几个我的使用习惯
写到这里,该聊的命令和坑都聊得差不多了,说几个我在实际工作中沉淀下来的使用习惯,供参考。
第一,提交时把改动拆小。哪怕一个功能涉及五个文件,我也尽量拆成两三次提交,比如先提交后端接口,再提交前端页面,最后提交测试用例。这样回退某个部分时不必连带撤销其他无关改动,git bisect定位问题也会更精准。
第二,功能分支保持短命。一个分支从创建到合并尽量控制在两天以内,分支活得越久,合并时冲突概率越大、context 丢失越严重。长时间不合并的分支,建议定期 rebase 主分支保持同步。
第三,公共分支多用git fetch+git rebase而不是直接 pull。git pull默认执行 merge,会在历史里产生大量的分叉合并记录。改用git pull --rebase把本地提交重新排到远程提交后面,历史看起来会清爽很多,这个习惯有几个团队同事一开始觉得多此一举,后来看自己的 log 图变直了,就再也回不去了。
Git 学了十年也不是全都记住,关键是建立"知其所以然"的脑子,遇到问题能判断出是协议问题、存储问题还是历史问题,然后命令自然就浮出来了。把上面这些内容吃透,不敢说你立刻变成 Git 高手,但至少在日常协作里能踏踏实实把代码管明白,不再被版本逼疯。