首页 / 资讯中心 / 文章详情

Git 实用指南:从分支合并到代码回滚,一次理清核心原理

Git 实用指南:从分支合并到代码回滚,一次理清核心原理 ★ FEATURED ARTICLE
很多人一听到 Git 就头大。指令一大堆概念抽象出了问题还不知道怎么回滚。我刚开始接触 Git 的时候也差不多这样天天在那敲git status看文件状态敲完还是一脸懵只知道“能用”但完全不清楚背后发生了什么。后来在真实项目里踩过几次坑被迫把原理弄明白了才发现 Git 的学习曲线其实没有想象中陡峭关键是先建立一套正确的逻辑框架再往里填指令。这篇内容我打算用非常直白的方式把 Git 指令学习这件事从头到尾梳理一遍。不讲废话不背书就按一个一线开发者的实际使用习惯来讲。不管你是刚入行的新人还是用了很久但一直停留在“复制粘贴指令”阶段的老手这篇都能帮你把 Git 的知识体系补完整。1. 为什么每个写代码的人都躲不开 Git1.1 从“文件改名法”到版本控制的进化Git 本质上解决的是一个很原始的问题文件会变你需要能回到过去任意一个状态。没有版本控制的时候我们怎么管理代码最常见的操作是复制一份文件夹改个名字叫“项目备份_20231001”再过两天又复制一份叫“项目备份_最终版”最后再来一个“项目备份_最最终版_千万别删”。这套做法表面看没什么问题但只要你实际维护过项目超过一周就会体会到什么叫灾难。想对比一下昨天的改动和今天的改动你得开两个文件夹用比对工具慢慢看想找一段被删掉的代码你得翻遍所有备份文件想多人协作那更是不可能完成的任务你改我也改最后根本不知道谁覆盖了谁。Git 的出现把这一切变成了历史。它帮你做三件核心事情记录每一次改动的快照、允许你在任意时点之间来回穿梭、让多个人并行修改同一份代码而不会互相覆盖。用生活化一点的话说Git 就像代码世界的“时间机器”加“协作空间”。1.2 Git 的三个核心区域学习 Git 指令之前必须先搞懂一个概念Git 的“状态”不是只有一个而是分成了三个区域。第一个区域是工作区也就是你电脑上实实在在看到的文件目录你编辑的就是它。第二个区域是暂存区你可以理解成一个中转站放的是你已经告诉 Git“这些文件我改过了待会要一起提交”的改动记录。第三个区域是本地仓库这才是 Git 真正存储历史版本的地方每次提交就是把暂存区的内容打包成一次快照存进来。这三个区域的关系可以用一句话概括工作区是你干活的地方暂存区是确认干完的地方本地仓库是永久存档的地方。很多 Git 指令其实就是在操作这些区域之间的移动。比如git add就是把改动从工作区放到暂存区git commit就是把暂存区的内容固化到本地仓库。搞懂这三个区域后续所有指令都会变得非常顺。1.3 分布式和集中式的差异还有一个基础概念值得花点篇幅说清楚Git 是分布式版本控制系统这和以前常见的集中式版本控制系统比如 SVN有本质区别。集中式系统的逻辑是所有版本信息都存在一台中央服务器上每个人的本地只是工作副本你得联网才能提交、查看历史服务器挂了基本完蛋。分布式Git的逻辑则完全不同每个人clone仓库之后本地就拥有完整的版本历史提交、回滚、查看日志全都能在本地进行不需要联网。你可以把 Git 理解为每个开发者的电脑上都有一份自己的“服务器”。这个特性带来的实际好处非常明显坐飞机、地铁断网了依然可以正常提交代码中央仓库被删了随便找一个同事的本地仓库就能恢复所有的历史记录。理解了这一点你就能明白git push和git commit的本质区别commit 是写入本地的版本历史push 才是在同步到远程仓库。2. 起步必会初始化、提交与状态查看2.1 从零初始化一个仓库当你决定开始用 Git 管理项目第一步永远只有一个初始化仓库。在项目根目录执行git init这会在当前文件夹里创建一个隐藏的.git目录它就是 Git 的数据库所有版本信息、分支记录、配置项都存在这里面。初始化完成之后下一步就是把项目文件纳入 Git 管理。这里有一个新手很容易犯的误区git init之后直接敲git commit然后被系统告知没有可提交的内容。因为 Git 要求你先git add把文件加入暂存区再git commit才有效。一个完整的第一步流程是这样的# 进入项目目录 cd my-project # 初始化仓库 git init # 查看当前状态确认文件还没有被跟踪 git status # 添加所有文件到暂存区 git add . # 再次查看状态文件已经变成绿色代表进入了暂存区 git status # 提交到本地仓库 git commit -m init project这里建议每次git add和commit之间都看一眼git status这不是强迫症而是 Git 使用中最重要的安全习惯。你永远要知道自己把什么文件交出去了Git 的每一次提交都相当于一次存档存档内容错了后面回滚就很麻烦。2.2 status 和 log 的读法git status是使用频率最高的命令没有之一。它的输出信息非常丰富但新手往往不知道怎么读。简单拆解一下Untracked files意思是“没被跟踪的文件”也就是 Git 从来没记录过这些文件的任何版本通常是你新建的文件。Changes not staged for commit意思是“有改动但还没放进暂存区”你已经改了文件内容但还需要git add。Changes to be committed意思是“已经放进暂存区等待提交”的内容。git log则是查看提交历史的命令。默认情况下它按照从新到旧的顺序列出所有提交记录每条记录包含提交哈希值一串很长的十六进制数、作者、日期和提交说明。有一个比较好用的参数是--oneline会把每个提交压缩成一行看起来清爽很多git log --oneline git log --graph --oneline --all第二个命令里的--graph会把分支演化路径画出来--all会显示所有分支的提交记录。我建议新手把这句存成一个常用指令它能帮你快速建立对仓库演化的空间感。2.3 .gitignore 到底该怎么写还有一件非常容易忽略但非常关键的事.gitignore文件。.gitignore的作用是告诉 Git 忽略哪些文件。比如 Node.js 项目的node_modules目录Java 项目的target目录Python 项目的__pycache__还有.env这种存放密钥的环境变量文件全都应该被忽略不应该进入版本库。不写.gitignore的后果是很实际的依赖目录动不动几万个文件提交的时候效率低别人 clone 下来也是一大包垃圾。更严重的是如果误把本地配置、密钥文件提交上去多人协作时等于把敏感信息直接扩散出去了。我见过一次事故某次提交把.env里的数据库密码推到了公共仓库最后只能紧急改密码并清理全部历史。一个基本合格的.gitignore写法是这样的# 依赖目录 node_modules/ target/ vendor/ # 编译产物 dist/ build/ *.class *.pyc # 日志与临时文件 logs/ *.log .DS_Store # 环境变量与配置文件 .env .env.local写完.gitignore后执行git status确认相关目录不再出现。如果某个文件之前已经被跟踪了光加.gitignore是没用的还需要从 Git 的索引里移除# 从 Git 索引删除但保留本地文件 git rm -r --cached node_modules git commit -m remove node_modules from tracking提示先加.gitignore再git init是好习惯。语言模板可以直接去 GitHub 等平台搜对应语言的官方 ignore 模板比自己从零写省心不少。3. 分支操作并行开发的安全带3.1 分支是什么理解分支是 Git 学习过程中必须跨过的一道坎。用最简单的话说分支就是一个可移动的指针指向某一次提交。当你创建了一个新分支本质上是创造了一个新的指针而当你切换分支时Git 会把你工作区的文件恢复到那个分支指向的版本。我当年学 Git 时最震撼的就是明白了这个模型。在那之前我一直以为分支是把代码复制一份后来才知道复制的是指针代码文件只有一份。这也是 Git 分支创建和切换非常快的原因改的只是一个指针不是堆积数据。3.2 创建、切换和合并分支平时使用最多的几个分支操作基本可以归纳为# 创建新分支但不切换 git branch feature-login # 切换到某个分支 git checkout feature-login # 创建并立即切换最常用 git checkout -b feature-login # 新版写法效果等同于上面那句 git switch -c feature-login这里补充一句Git 新版提供了一套更语义化的指令git switch和git restore分别负责分支切换和文件恢复和checkout的职责分开了新手其实可以直接用这套新指令不容易混。把分支上的工作完成后需要合并回主干。合并的核心指令是git merge。假设你现在在main分支想把feature-login分支的改动合并过来操作是# 确认自己在 main 分支 git checkout main # 拉取最新代码远程协作时 git pull # 合并功能分支 git merge feature-login合并完成之后如果功能分支已经不再需要可以顺手删除git branch -d feature-login注意这里用小写的-d它只会在分支已经合并过时才允许删除。如果分支上还有未合并的提交Git 会拒绝删除并提醒你这是防呆设计。如果确实想强行丢掉一个分支才用-D。3.3 合并冲突的解决实战提到合并就绕不开冲突。所谓冲突就是两个分支修改了同一个文件的同一块位置Git 不知道怎么取舍只能把问题抛给开发者。遇到冲突时Git 会在冲突文件里写入特殊标记长这样 HEAD 这是当前分支的内容 这是被合并分支的内容 feature-login你需要手动把、、这三行标记删掉把最终想要保留的内容留下确保代码是完整的。处理完后执行git add 冲突文件 git commit这里我特别想提醒一个问题不要看到冲突就慌冲突是正常现象不是你能力不行。尤其是在团队合作的项目中合并时的冲突就像两个人改论文同一段话一样解决掉就好。关键是冲突文件不要硬着头皮乱改先打开上下文看看两边各写了什么分清哪些要保留、哪些要删除实在不确定就和写那段代码的同事当面确认一下。3.4 rebase 和 merge 的选择合并分支还有一种方式叫git rebase。它和merge的核心区别在于merge保留所有分支的交叉点提交历史会呈现分叉再合并的形状rebase会把你这条分支的提交重新放到目标分支的最新提交之后形成一条线性历史。merge和rebase的选择团队内部通常会有约定。我的建议是如果只是合并功能分支回主干用merge就行操作简单保留的过程完整。如果你在拉取远程最新代码、想把本地提交叠到远程提交上面用rebase会更整洁。不要在有协作功能分支上反复 rebase。因为 rebase 会重写提交哈希它会让你本地的提交“看起来像”新提交别人基于旧提交拉的分支会被打乱这很容易引发混乱。一句话总结rebase 很好用但别在共享分支上随便用。4. 后悔药与时间旅行reset、revert、reflog4.1 reset 的三种模式开发过程中改错代码、提交错内容是常有的事。Git 最让人安心的功能就是“后悔药”而git reset是实现后悔的核心工具。git reset有三种模式它们的力度逐级递增这个表格最好牢牢记在心里模式移动 HEAD重置暂存区重置工作区影响程度--soft是否否最轻改动还会保留在暂存区--mixed默认是是否中间改动保留在工作区但不在暂存区--hard是是是最重所有改动都会清掉实际使用中--soft最常见的用法是“撤回上一次提交但保留改动”比如你 commit 完了发现漏了一个文件可以git reset --soft HEAD~1 git add 漏掉的文件 git commit -m 重新提交--hard用得少但一用就是大动作。比如你本地改了一堆乱七八糟的东西不想要了想强制回到当前分支的最新提交git reset --hard HEAD4.2 revert 和 reset 的区别有人可能问那git revert是干嘛的它和reset有什么区别核心区别在于reset是“回退历史”它直接移动 HEAD原来的提交会被丢弃至少是游离。而revert是“反向提交”它保留原有的历史生成一次新的提交把要回退的改动抵消掉。什么时候必须用revert答案是当你想撤销的提交已经推到远程和大家共享时。因为此时再 reset 然后 force push会把仓库历史改得和同事不一样产生连锁问题。而revert相当于新增一个“取消上次改动”的提交其他人pull下来自然就会得到内容回退不需要任何强推操作。git revert a1b2c3d这里的a1b2c3d是你要撤销的提交哈希。执行后会打开一个编辑器让你填提交信息默认就写 Revert 相关直接保存退出即可。4.3 reflog 拯救误删的提交git reset --hard之后发现想要的东西没了怎么办不要慌Git 还有一个终极大招git reflog。reflog会记录 HEAD 指针的所有移动历史也就是说哪怕你 reset 掉了一个提交只要这个提交曾经存在过它都会留在 reflog 里。只要在仓库的本地把这个提交捡回来就行# 先查看 HEAD 的历史移动记录 git reflog # 找到目标提交的哈希比如 2f114a4 # 然后新建一个分支指向它 git branch recover 2f114a4 # 或者直接切换到那个提交 git checkout 2f114a4我用这个方法救回来过一次误删的功能分支当时同事以为自己两个星期的工作全丢了结果五分钟后从 reflog 里完整恢复。这个经验分享出来就一句话Git 里基本没有真正“删掉”的东西只要 reflog 还在记录就有救。但要注意reflog 默认只保留一定时间过期记录会被清理所以误删之后要尽快处理。5. 远程协作clone、push、pull 与分支保护5.1 从远程仓库开始参与项目进入团队工作后绝大多数场景不是从零初始化仓库而是参与一个已经存在的项目。这时你需要做的是把远程仓库复制到本地git clone gitexample.com:team/project.git执行完这行命令本地会生成一个项目目录同时 Git 会自动在你本地创建一个名为origin的远程仓库引用。这个origin只是约定俗成的名字代表“默认的远程源”。你后续所有的 push 和 pull默认都是跟它对话。如果你想看一下当前配置了哪些远程仓库可以执行git remote -v这个命令会列出远程仓库名字和对应的地址。要添加一个远程地址用git remote add upstream gitexample.com:upstream/project.gitupstream这个名字通常用来指“上游仓库”在多人贡献的开源项目中很常见。5.2 push、pull 和 fetch 之间到底差什么远程操作的三个核心命令git push、git pull、git fetch之间的区别很多用了好久 Git 的人也说不全我在这里用最直白的方式拆一下。git fetch是“只下载不改动”。它会从远程把新的提交记录拉到本地但不会更新你工作区的文件也不会改动当前分支。跑完之后远程仓库有什么新的提交你都看到了但本地代码还是旧样子。git pull是“下载并合并”。它本质上是git fetch加git merge的组合先从远程拉取然后自动把远程的改动合并到当前分支。git push则是反方向把你的本地提交推送到远程仓库。实际执行 push 的命令通常是git push origin main这个命令的意思是“把本地 main 分支推送到 origin 远程的 main 分支”。如果是第一次推送新分支需要加上-u参数建立关联git push -u origin feature-login加了-u之后后续直接git push就行Git 会记住这个分支对应远程的哪个分支。5.3 实操中的协作流程建议给新手一个非常实用的协作流程模板按照这个走不会出大问题# 1. 确保在主分支并拉取最新 git checkout main git pull # 2. 创建自己的功能分支 git checkout -b feature/xxx # 3. 开发过程中每天上班先同步远程 git pull origin main # 4. 完成功能提交到本地 git add . git commit -m feat: 完成登录功能 # 5. 推送并建立关联 git push -u origin feature/xxx # 6. 在代码托管平台发起合并请求等待审查合并这套流程的本质是功能都在独立分支开发主干始终保持可发布状态任何改动必须经过审查才能合并。这也是目前行业里最主流的协作方式适用于绝大多数软件开发项目。引入团队协作时还有一点要重视分支保护规则。在代码托管平台无论是自建还是第三方上建议给主分支开启保护不允许直接 push强制要求通过合并请求来合并代码。这样能显著降低错误代码直接进入主干的概率。6. 提效技巧配置、别名与良好习惯6.1 stash临时保存工作现场开发过程中经常会遇到一个场景正在分支 A 上写代码写到一半突然要切到分支 B 去处理一个紧急修补。这时如果直接git checkout切分支Git 会拒绝切换因为工作区有未提交的改动硬切的话会冲突。解决办法就是用git stash把当前未提交的改动暂存起来像“把桌面上的文件收进抽屉”一样让工作区变干净# 保存当前改动 git stash # 此时可以安全切换分支 git checkout -b hotfix/xxx # 处理完紧急任务后切回原分支 git checkout feature-a # 把暂存的改动恢复出来 git stash popgit stash的附加说明如果同时存了多份改动可以用git stash list看列表用git stash apply stash{1}来恢复指定的某一份。注意pop会恢复并删除对应的一条暂存记录apply不会删除保留记录可以继续用。6.2 配置 Git 别名有些指令很长敲起来费时费力。Git 支持自定义别名可以极大提升日常操作效率。配置方式是git config --global alias.st status git config --global alias.co checkout git config --global alias.br branch git config --global alias.cm commit -m git config --global alias.lg log --graph --oneline --all配置之后git st相当于git statusgit co相当于git checkout。建议别乱缩写自己用得习惯的关键指令配上就行另外这些配置都会写入全局配置文件。如果想直接编辑底层配置可以使用git config --global -e打开编辑器手动修改通常在~/.gitconfig里企业内部还常常在这里配置 user.name 和 user.email。6.3 提交信息的规范写法提交信息看着是小事但项目时间一长质量差别巨大。规范格式是“类型 冒号 简短描述”feat: 增加用户登录功能 fix: 修改订单列表为空时的报错问题 docs: 补充接口文档 refactor: 重构文件上传模块 style: 调整代码格式不影响功能 test: 增加登录接口单元测试 chore: 更新依赖版本我个人的体会是规范的提交信息不只是给 Git 看的更是给未来的自己看的。三个月后翻提交历史一眼就能定位某个改动是干嘛的效率翻倍。7. 常见问题与排查技巧实录7.1 处于 detached HEAD 状态怎么办这种情况一般是因为执行了git checkout加一个 commit 哈希而不是分支名。此时 HEAD 指向了一个具体的提交而不是某个分支名你当前的改动在这时会变得不好管理容易丢失。解决办法很简单# 如果没改东西直接切回原分支 git checkout main # 如果已经改了东西先建一个新分支把它保存 git switch -c save-my-work记住一个原则不要直接在游离状态下做开发。如果确实需要基于某个历史提交做修改先基于它建一个新分支再开始操作这样一切改动都有据可依。7.2 误删的分支还能找回来吗能找回来核心还是 reflog。当你删除一个分支时Git 并不立即清除这个分支指向的提交只是把这个分支的指针删除了。如果这个提交还在 reflog 里有记录就可以恢复分支。git branch recover 某提交哈希如果 reflog 里也找不到了比如时间太久被清理还有一种方法是在 Git 的文件存储目录里尝试找悬空提交。}运行git fsck --lost-found能列出未被任何分支引用的提交找到你需要的哈希后同样用git branch拉回来。7.3 push 被拒绝了怎么办git push被拒绝最常见的原因是远程仓库有本地没有的新提交直接推送会造成历史分叉Git 出于安全考虑拒绝执行。正确的解法是先把远程的改动拉下来合并然后再推# 推荐方式用 rebase 方式拉取保持线性历史 git pull --rebase origin main # 处理可能出现的冲突 # 重新推送 git push origin main如果这时强制推送到远程改动不一定是优势甚至可能是灾难。git push -f会把本地历史强行覆盖到远程这会直接破坏共享分支上的历史一致性所以不要轻易用--force。7.4 文件大小写引起的问题Git 默认不区分文件名大小写改动。也就是说你把readme.md改名为README.md直接提交的话 Git 感受不到变化。处理办法是先删掉原文件再添加新文件或者显式执行git mv Readme.md README.md否则到了他人机器上可能因为大小写不同导致编译错误这种问题在跨平台开发时非常浪费时间排查。写完这么多还是想补一句心得体会。Git 指令本身并不难背难的是头脑里要有一个清晰的心智模型。不管是reset还是merge还是rebase只要你能想象出“指针”和“提交节点”的关系这些指令就不再是一个个孤立的记忆点而是同一张图上的不同操作手法。我的建议是别怕在实际项目中尝试Git 的后悔药足够多大部分情况下你都不会把仓库搞到彻底损坏。真遇到不懂的时候先跑git status再跑git log --graph --oneline --all多半能判断个八九不离十。Git 这东西用多了就是肌肉记忆别把它当负担把它当副驾驶就好。
阅读完成 · 觉得有帮助?
咨询建站