公司动态
Git 本地版本管理与分支管理:从零理解工作区、回退、冲突与开发流程
这是一篇写给 Git 初学者的图解复习笔记。重点不是背命令而是先弄清楚“文件现在在哪个区域”“分支指针现在指向哪里”再决定应该执行什么命令。第一次接触 Git 时我经常遇到三种困惑明明保存了文件为什么 Git 还说没有可提交的内容add、commit和reset到底在移动什么分支合并后自己的代码为什么“不见了”或者出现一堆奇怪符号这些问题看似零散实际上都能放进同一张知识地图里。知识框架整篇内容分成三条主线先理解工作区、暂存区和版本库之间的数据流再学习撤销与回退最后进入分支、合并、冲突和开发策略。读到任意一节迷路时都可以回到这张图重新定位。目录一、为什么需要 Git二、安装 Git、创建仓库与身份配置三、先理解 Git 最重要的三个区域四、提交对象与提交 ID 到底是什么五、用 status、diff、log 和 reflog 看清现场六、版本回退reset 的三种模式七、撤销修改先判断内容位于哪个区域八、删除文件系统删除与 Git 删除不是一回事九、分支、HEAD 与提交先把指针关系画清楚十、创建、开发、合并与删除一条完整分支流程十一、合并冲突Git 为什么不替我做决定十二、Fast-forward 与 --no-ff结果相似历史形状不同十三、稳定主分支的开发策略十四、开发到一半出现紧急 bug使用 stash 暂存现场十五、删除未合并分支-d 与 -D 不只是大小写区别十六、常见错误与排查顺序十七、一套适合初学者的安全操作习惯十八、总结一、为什么需要 Git1. 手动复制文件为什么不够没有版本管理工具时我们很容易把项目保存成这样project ├── project_最终版 ├── project_最终版2 ├── project_真的最终版 └── project_真的最终版_不改了这种方法短期内能用项目稍微变大就会出现问题不知道每个版本改了什么不知道哪个版本可以正常运行多人修改时很难合并想撤销某一次修改只能靠记忆和手工对比文件越复制越多却仍然不敢删除。Git 的价值就是把“版本、差异、恢复和协作”交给一个可靠的版本控制系统。可以先把 Git 理解成一个会记账的时间机器我修改文件我挑选这一次想保存的内容Git 把这些内容保存为一个提交以后可以查看、比较或回到某个提交。2. 文本文件与二进制文件的差别Git 最擅长管理文本文件例如C/C 源代码Markdown 文档配置文件HTML、CSS、JavaScript 文件。对于文本文件Git 能按行显示“删除了什么、增加了什么”。图片、音频、压缩包等二进制文件也可以交给 Git 保存但 Git 通常只能判断“文件变了”不容易展示内部每一处变化。因此Git 不是不能管理二进制文件而是对文本差异的展示更直观、更有价值。二、安装 Git、创建仓库与身份配置1. 在 Linux 中安装 Git不同 Linux 发行版使用的包管理命令不同。# CentOS / RHEL 系列sudoyuminstallgit# Ubuntu / Debian 系列sudoaptinstallgit# 查看是否安装成功并显示版本号git--version2. 初始化本地仓库先进入准备交给 Git 管理的目录再执行# 把当前目录初始化为 Git 仓库gitinit执行成功后当前目录会多出一个隐藏目录.git。它不是普通项目文件而是 Git 保存提交对象、分支引用、配置等内部数据的地方。# Linux 下显示包含隐藏目录在内的全部内容ls-la不要随意修改或删除.git。删除它不会删除工作目录中的源文件但会让当前目录失去原有的本地版本历史。3. 配置提交者姓名和邮箱Git 会在每次提交中记录作者信息。# 只对当前仓库生效gitconfig user.nameYour Namegitconfig user.emailyouexample.com# 对当前用户的所有仓库生效gitconfig--globaluser.nameYour Namegitconfig--globaluser.emailyouexample.com查看配置# 查看所有能够读取到的配置gitconfig--list# 单独查看某个配置项gitconfig user.namegitconfig user.email删除配置时要注意作用域# 删除当前仓库中的姓名配置gitconfig--unsetuser.name# 删除全局姓名配置gitconfig--global--unsetuser.name如果本地配置和全局配置同时存在本地仓库配置的优先级更高。三、先理解 Git 最重要的三个区域很多命令记不住是因为只看到了命令没有看到数据在什么位置。Git 的本地操作可以先分成三个主要区域工作区Working Tree我正在直接编辑的文件暂存区Index / Staging Area我挑选出来、准备放进下一次提交的内容版本库Repository已经提交、可以长期追踪的历史。版本库内部还有对象库。文件内容、目录结构和提交说明最终会以对象的形式保存进去。最常见的数据流只有两步工作区 --git add-- 暂存区 --git commit-- 本地版本库反过来思考也很重要从工作区撤销修改让工作区重新采用暂存区中的内容取消暂存让暂存区重新采用某个提交中的内容回退版本移动当前分支指针并按模式决定是否同步暂存区和工作区。1. 完成第一次提交假设创建了一个ReadMe文件# 查看当前状态先确认 Git 发现了哪些变化gitstatus# 把 ReadMe 当前内容放入暂存区gitaddReadMe# 再看一次状态此时应看到它处于“待提交”状态gitstatus# 把暂存区快照保存成一个提交gitcommit-madd ReadMe这里最容易产生的误解是git add并不是简单地给文件贴一个“已选择”标签。它会把执行命令时的文件内容放入暂存区。例如第一次修改ReadMe执行git add ReadMe又修改一次ReadMe直接执行git commit。这次提交只会包含第 2 步时进入暂存区的内容第 3 步的新修改仍留在工作区。2. 一次添加多个文件# 只添加指定文件范围最明确gitaddmain.c util.c# 添加当前目录及子目录中的变化gitadd.# 提交暂存区中的全部内容gitcommit-mimplement basic functions对初学者而言提交前固定执行一次git status很有帮助。它相当于提交前的清单核对。四、提交对象与提交 ID 到底是什么每次提交都会得到一个很长的十六进制 ID例如4f7c1f2e0a...它常被称为提交哈希或提交 ID。实际操作时不一定要输入完整 ID只要缩写在当前仓库中能够唯一确定目标即可。一次提交可以先这样理解commit 对象 ├── 指向一次目录快照tree ├── 指向父提交第一次提交没有父提交 ├── 作者与提交者信息 └── 提交说明 tree 对象 ├── 文件名与目录结构 └── 指向文件内容对象blob也就是说提交对象并不是把每个文件随意堆在一起而是把“目录结构、文件内容、提交关系和说明”组织成可追踪的历史。查看提交历史# 显示较完整的提交记录gitlog# 每个提交压缩成一行适合快速查看gitlog--oneline# 同时显示分支关系后面学习分支时非常有用gitlog--oneline--graph--decorate--all查看对象类型和内容# 判断某个对象是 commit、tree 还是 blobgitcat-file-t对象ID# 以可读形式显示对象内容gitcat-file-p对象IDblob保存的是文件内容不直接保存原始文件名文件名和目录关系由tree对象组织。五、用 status、diff、log 和 reflog 看清现场动手修复问题之前先看清现场通常比立刻执行命令更安全。1.git status现在有哪些变化# 查看未跟踪、已修改、已暂存等状态gitstatus# 使用更精简的两列状态格式gitstatus--shortstatus回答的是“现在有哪些文件处于什么状态”2.git diff具体改了什么# 比较“工作区”和“暂存区”# 适合检查还没有 add 的修改gitdiff# 比较“暂存区”和“HEAD 所指提交”# 适合检查下一次 commit 准备提交什么gitdiff--cached# 比较两个提交之间的变化gitdiff旧提交ID新提交ID可以用一句话记忆git diff 看还没暂存的内容 git diff --cached 看已经暂存、还没提交的内容3.git log与git reflog的区别# 查看正式提交历史gitlog--oneline# 查看 HEAD 和分支引用近期移动过的位置gitrefloglog关注提交历史reflog关注本地引用如何移动执行错误回退后reflog常能帮助找回先前的提交 ID。reflog是本地恢复线索不应被当作永久备份。六、版本回退reset 的三种模式git reset最核心的动作是移动当前分支指针。三个常用模式的差别是移动指针后还会不会继续更新暂存区和工作区。1.--soft只移动分支指针# 回到上一个提交但把撤回的内容保留在暂存区gitreset--softHEAD^结果分支指针回到上一个提交暂存区保留原提交内容工作区也保留原内容。适合提交说明写错了或者想把最近几次提交重新整理。2.--mixed再重置暂存区# --mixed 是默认模式gitreset--mixedHEAD^# 等价的简写gitreset HEAD^结果分支指针回退暂存区恢复到目标提交工作区文件仍保留修改。适合想撤销提交同时重新选择哪些内容应该add。3.--hard连工作区一起重置# 高风险工作区和暂存区都会变成目标提交的状态gitreset--hardHEAD^结果分支指针回退暂存区恢复工作区也恢复未提交修改可能直接丢失。执行前至少检查# 第一步查看是否有未提交修改gitstatus# 第二步确认要回到哪个提交gitlog--oneline# 第三步确认无误后再使用 --hardgitreset--hard目标提交ID4. HEAD、HEAD^ 与 HEAD~nHEAD 当前所在位置 HEAD^ 当前提交的第一个父提交 HEAD~2 沿第一个父提交方向向前走两步在没有合并提交的直线历史中HEAD^^和HEAD~2通常指向同一位置。出现合并提交后^还可以用于选择不同父提交因此不能只把它机械理解成“减一”。5. 回退错了怎么找回来# 找到 reset 前 HEAD 曾经指向的提交gitreflog# 假设找到了目标 ID再移动回去gitreset--hard找回的提交ID前提是相关提交仍能通过本地对象和引用日志找到。越早恢复成功机会通常越高。七、撤销修改先判断内容位于哪个区域“我想撤销”并不是一个完整问题。真正需要问的是这份错误内容只在工作区已经进入暂存区还是已经提交场景一只修改了工作区还没有 add# 放弃 ReadMe 在工作区中的修改# 恢复来源是暂存区中的版本gitcheckout -- ReadMe这里的--用于把命令选项与文件名分开。如果文件名恰好和分支名相同它也能避免歧义。执行前建议先看差异# 先确认准备放弃哪些内容gitdiffReadMe# 确认后再撤销gitcheckout -- ReadMe场景二已经 add但还没有 commit要分两步处理。# 第一步把 ReadMe 从暂存区撤回工作区# 文件内容不会因此消失gitreset HEAD ReadMe# 第二步如果连工作区修改也不要再执行gitcheckout -- ReadMe第一步只是“取消暂存”第二步才是“放弃工作区修改”。场景三已经 commit如果提交还只存在于本地并且确认可以改写这段历史可以使用# 回到上一个提交同时把撤回内容留在工作区gitreset HEAD^如果只想完全丢弃最近提交及未提交变化# 高风险请先确认这些内容确实不再需要gitreset--hardHEAD^一个稳妥的选择顺序是能用--soft就先保留暂存内容需要重新挑选内容时用默认的--mixed只有明确要丢弃工作区变化时才用--hard。八、删除文件系统删除与 Git 删除不是一回事假设test.c已经被 Git 跟踪。1. 先用系统命令删除# 只删除工作区文件rmtest.c# Git 会发现“工作区少了一个文件”gitstatus此时删除操作还没有进入暂存区。如果删错了# 从暂存区中的版本恢复工作区文件gitcheckout -- test.c如果确认要删除# 把“删除 test.c”这个变化放入暂存区gitaddtest.c# 保存删除记录gitcommit-mremove test.c2. 使用git rm# 同时删除工作区文件并把删除操作加入暂存区gitrmtest.c# 提交删除记录gitcommit-mremove test.cgit rm相当于“删除文件 暂存删除变化”。它不会省略最后的commit。九、分支、HEAD 与提交先把指针关系画清楚分支并不是复制出一整套项目文件。它本质上是一个轻量的引用指向某个提交。HEAD通常指向当前分支当前分支再指向当前提交HEAD → master → 提交 C本文统一用master表示主分支。如果自己的仓库显示的是main只需要把示例命令中的master换成main指针模型和操作逻辑完全相同。有新提交时当前分支指针会向前移动提交前HEAD → master → C 提交后HEAD → master → D C → D切换分支本质上是改变HEAD所关联的分支并让工作区匹配目标分支对应的内容。1. 查看分支# 查看本地分支gitbranch输出中带*的分支就是当前分支。2. 创建和切换分支# 只创建 dev不切换gitbranch dev# 切换到 devgitcheckout dev# 创建 dev2 并立即切换过去gitcheckout-bdev2git checkout -b dev2可以理解为gitbranch dev2gitcheckout dev23. 在分支上提交# 确认当前位于哪个分支gitbranch# 修改文件后把变化放入暂存区gitaddReadMe# 提交后只有当前分支指针向前移动gitcommit-mupdate ReadMe on dev其他分支不会自动跟着移动这正是分支可以隔离开发工作的原因。十、创建、开发、合并与删除一条完整分支流程假设要在dev中开发功能完成后合入master# 1. 从当前位置创建并切换到 devgitcheckout-bdev# 2. 在 dev 中完成修改gitadd.gitcommit-mfinish feature# 3. 切回接收合并结果的 mastergitcheckout master# 4. 把 dev 的历史合入当前 mastergitmerge dev# 5. 确认不再需要 dev 后安全删除分支gitbranch-ddev要特别注意合并方向先切到“接收结果”的分支再 merge“提供结果”的分支因此gitcheckout mastergitmerge dev含义是“把dev合入master”不是反过来。提交、切换、合并前都可以用下面的命令检查# 检查工作区是否干净gitstatus# 检查当前分支gitbranch# 检查历史形状和各分支位置gitlog--oneline--graph--decorate--all十一、合并冲突Git 为什么不替我做决定如果两个分支修改了不同文件或者修改了同一文件的不同位置Git 通常可以自动合并。如果两个分支修改了同一个文件的同一部分Git 无法判断哪一边才是正确业务结果于是停止合并并标记冲突。冲突文件中常见的标记如下 HEAD 当前分支中的内容 被合入分支中的内容 dev这些符号的含义 HEAD到当前分支的内容到 dev准备合入的dev内容Git 只负责指出两边差异不会替我理解业务需求。正确解决步骤# 1. 查看哪些文件发生冲突gitstatus# 2. 打开冲突文件人工决定最终内容# 删除 、、 这些冲突标记# 3. 把解决后的文件加入暂存区gitaddReadMe# 4. 创建合并提交结束这次合并gitcommit-mresolve merge conflict只编辑文件还不够。必须再次git add再git commitGit 才知道冲突已经解决并完成合并。如果暂时不想继续本次合并可以使用# 放弃当前未完成的合并尽量回到合并开始前gitmerge--abort十二、Fast-forward 与 --no-ff结果相似历史形状不同1. Fast-forward快进合并如果master指向的提交正好是dev的祖先那么gitcheckout mastergitmerge devGit 可以直接把master指针移动到dev所在位置不需要创建新的合并提交。优点历史简洁。不足从提交图上不容易看出某一段开发曾经属于独立分支。2.--no-ff明确创建合并提交# 即使可以快进也创建一个合并提交gitmerge --no-ff-mmerge devdev这样会保留一个明确的合并节点历史中更容易看出功能分支的边界。两种模式不一定会产生不同的最终文件内容但会产生不同的提交历史形状。十三、稳定主分支的开发策略当所有人都直接在主分支上修改任何一个未完成功能都可能影响稳定版本。更清晰的做法是分层管理master目标是保持稳定、可发布dev用于日常开发和集成feature/*每个功能单独开发完成并验证后再合入dev。一个功能分支可以这样完成# 1. 先进入开发分支gitcheckout dev# 2. 从 dev 创建独立功能分支gitcheckout-bfeature/login# 3. 完成功能并提交gitadd.gitcommit-mimplement login# 4. 回到 dev接收功能结果gitcheckout devgitmerge feature/login# 5. 功能分支已合入可以安全删除gitbranch-dfeature/login当dev中多个功能完成整体验证后再把它合入mastergitcheckout mastergitmerge dev更稳妥的集成顺序假设master在开发期间增加了一个紧急修复而dev也有自己的开发提交。直接把dev合入master可能到最后一步才暴露冲突。更稳妥的流程是先在dev中合入最新master在dev中解决冲突并完成验证再把验证后的dev合入master。对应命令# 第一步让 dev 先吸收 master 的最新变化gitcheckout devgitmerge master# 第二步如有冲突在 dev 中解决、测试并提交gitstatus# 第三步确认 dev 已经可用再合回 mastergitcheckout mastergitmerge dev这样可以把冲突处理和验证工作留在开发分支减少主分支长时间处于不完整状态的机会。十四、开发到一半出现紧急 bug使用 stash 暂存现场假设我正在dev2开发修改还不适合提交但此时必须立刻回master修复紧急问题。直接切换分支可能被 Git 拒绝也可能把未提交变化带到另一个分支。可以先使用stash临时收起工作现场。完整流程如下# 1. 确认当前状态和分支gitstatusgitbranch# 2. 临时保存已跟踪文件的工作区修改和暂存内容gitstash# 3. 回到 mastergitcheckout master# 4. 创建紧急修复分支gitcheckout-bfix_bug# 5. 修复问题并提交gitadd.gitcommit-mfix bug# 6. 把修复结果合回 mastergitcheckout mastergitmerge fix_bug# 7. 回到原来的开发分支gitcheckout dev2# 8. 恢复工作现场并移除对应 stash 记录gitstash popstash 的几个常用查看命令# 查看保存过的工作现场gitstash list# 恢复最近一次现场但保留 stash 记录gitstash apply# 恢复最近一次现场并在成功后移除记录gitstash pop默认情况下未跟踪文件不会被git stash收起。如果确实需要一起保存# -u 表示同时包含未跟踪文件gitstash-ustash适合临时切换任务不应长期代替清晰的提交。十五、删除未合并分支-d 与 -D 不只是大小写区别1. 优先使用-d# 安全删除如果分支存在未合并提交Git 通常会拒绝gitbranch-ddev-d会帮我做一次保护性检查。若分支内容已合并删除的主要是分支引用提交仍可从合入后的历史访问。2. 强制删除前先看独有提交# 查看 dev 中存在、master 中不存在的提交gitlog--onelinemaster..dev如果这里仍有输出说明dev还有独有提交。只有明确决定放弃它们时才考虑# 高风险即使尚未合并也强制删除 dev 分支引用gitbranch-Ddev删除分支引用后独有提交不一定立刻从对象库消失但寻找它们的直接入口可能丢失。不要把“暂时可能找回”当成安全保障。最容易记住的原则是能用 -d 就不用 -D 只有明确放弃独有提交时才强制删除。十六、常见错误与排查顺序1.git commit后没有包含刚改的内容可能原因修改发生在最后一次git add之后。排查# 查看哪些内容还留在工作区gitdiff# 查看暂存区准备提交什么gitdiff--cached2. 合并方向弄反错误思路看到git merge dev就以为当前会切到dev。正确理解merge不会替我切换分支它把指定分支合入当前分支。# 先确认当前分支gitbranch# 要把 dev 合入 master必须先切到 mastergitcheckout mastergitmerge dev3. 冲突文件已经修改但 Git 仍说正在合并原因只删除了冲突标记没有重新暂存并提交。gitadd已解决的文件gitcommit-mresolve merge conflict4.reset --hard后修改不见了--hard本来就会同步重置工作区。可以尽快查看# 寻找 reset 前的提交位置gitreflog未提交的工作区修改通常没有对应提交恢复难度远高于已经提交的内容。5. 切换分支时 Git 拒绝操作常见原因当前未提交修改会被目标分支内容覆盖。可以根据实际情况选择# 方案一修改已经完整正常提交gitadd.gitcommit-msave current work# 方案二修改还不适合提交临时收起gitstash不要为了强行切换而随手使用会丢失内容的命令。十七、一套适合初学者的安全操作习惯执行普通操作前# 我在哪个分支gitbranch# 当前有哪些变化gitstatus# 具体改了什么gitdiffgitdiff--cached执行回退或删除前# 提交历史是什么样gitlog--oneline--graph--decorate--all# 目标分支是否有独有提交gitlog--onelinemaster..dev# HEAD 最近移动过哪些位置gitreflog可以把安全优先级记成四句话先status再操作先看diff再放弃修改先看分支图再回退能保留内容就不要急着使用--hard或-D。十八、总结Git 本地版本管理的核心不是命令数量而是三组关系三个区域工作区、暂存区、版本库三个指针层次HEAD → 当前分支 → 当前提交三类安全判断内容在哪里、分支指向哪里、操作会覆盖什么。最后用一组最小命令表完成复习# 初始化与配置gitinitgitconfig user.nameYour Namegitconfig user.emailyouexample.com# 保存版本gitstatusgitadd.gitcommit-mmessage# 查看变化与历史gitdiffgitdiff--cachedgitlog--oneline--graph--decorate--allgitreflog# 撤销与回退gitcheckout --文件gitreset HEAD文件gitreset--softHEAD^gitreset HEAD^gitreset--hardHEAD^# 分支gitbranchgitcheckout-bdevgitcheckout mastergitmerge devgitbranch-ddev# 临时保存现场gitstashgitstash listgitstash pop当我能先在脑中回答“这条命令会移动哪个指针、更新哪个区域”Git 就不再是一堆容易混淆的咒语而会变成一套可以推理、可以验证的工具。