ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Git常用命令实战:提交、分支、回滚与远程同步全解析

Git常用命令实战:提交、分支、回滚与远程同步全解析

上周帮一位朋友排查代码回滚的问题,他提交完一个“紧急修复”后发现改错了文件,对着终端一脸茫然,翻出收藏夹里的命令笔记,试了reset、又试了checkout,最后好不容易把代码弄回来,分支却乱成一团。这种场景我见过太多次了。很多人对 Git 命令并不陌生,真正的问题在于记了一堆命令,却不知道它们背后的逻辑,到用的时候不敢下手。

这篇文章不打算搞成一本命令词典,而是围绕“常用 Git 命令清单”这个主题,把日常工作最高频、最容易踩坑的那批命令拆开揉碎讲清楚。核心内容覆盖提交、分支、回滚、远程同步、临时保存这几大类,每个命令都会说清楚“它解决什么问题”“底层到底发生了什么”“什么场景别用它”。无论你是刚入职的学生,还是写了两三年业务代码的开发者,这份清单都能帮你把 Git 用得更有底气。

1. 先理解 Git 的三个区域,命令才不会背串

在用任何命令之前,我强烈建议先建立一张内存中的地图:Git 的所有操作,本质上是文件在三个区域之间流转——工作区、暂存区、版本库。

工作区就是你电脑上能看到的项目目录,你改文件、新建文件,这些变动只存在于工作区。暂存区在 .git 目录内部,你可以把它想象成一个“候车室”,你用git add把文件从工作区送进候车室,但还没真正上车。版本库就是 .git 目录里的提交历史,git commit负责把暂存区里的内容打包成一个快照,永久记录下来。

1.1 理解三个区域之间的关系

这三个区域的流转方向是单向的,理解了这个,很多命令就自然记住了:

  • 工作区 -> 暂存区:git add
  • 暂存区 -> 版本库:git commit
  • 版本库 -> 工作区:git checkout、git reset、git restore

很多命令搞混,是因为没意识到它们的作用层不同。比如git checkout -- file是从版本库把文件恢复到工作区,直接覆盖工作区的改动;而git restore --staged file是只把暂存区的改动退回到工作区,不影响工作区内容。两者看起来都叫“撤销”,动作完全不同。

1.2 远程仓库在命令链条里的位置

远程仓库(origin)是额外的一层,它不在你本地,涉及的命令主要是push、pull、fetch、clone。很多人把本地分支和远程分支的关系理解成“同步”,其实更准确的说法是“交换提交记录”:

  • git fetch只把远程的提交记录下载到本地,不合并到你的工作分支
  • git pull是fetch + merge的组合,把远程改动拉下来并自动合并
  • git push把本地分支上的新提交推送到远程仓库

这三个命令之所以容易出问题,根源在于本地分支和远程分支的对应关系。第一次推送一个本地新分支时,Git 会提示你设置上游分支,常见做法是git push -u origin 分支名,-u就是--set-upstream,建立本地分支和远程分支的关联,之后就能直接git push/git pull,不用再带完整参数。

2. 按场景梳理常用命令清单

光理解区域还不够,我平时更习惯把命令按“场景”而不是按“字母序”分类记忆。以下是整理后的常用清单,每类都标注了使用频率和注意事项。

2.1 初始化与提交类

这是使用频率最高的一类命令,包括初始化仓库、查看状态、提交、追加提交等。

git init # 初始化当前目录为 Git 仓库 git status # 查看工作区和暂存区的状态 git add <file> # 将文件加入暂存区 git add . # 将所有改动加入暂存区(注意别误加无关文件) git commit -m "提交说明" # 提交暂存区的内容 git commit --amend -m "新说明" # 修改上一次提交的信息或内容

git status是使用频率最高的命令,强烈建议每次操作前后都跑一遍。它用红绿色标出文件状态,红色代表未暂存,绿色代表已暂存,还有一个Untracked files区,表示 Git 从未跟踪过的新文件。

git commit --amend是个好东西,但要警惕一个场景:如果你已经 push 过一次,然后 amend 修改了这次提交,本地和远程的提交哈希会不一致,下次 push 会被拒绝,只能强制推送,这在团队协作中是有风险的。经验法则是:push 之前可以用 amend 随便改,push 之后别用它。

2.2 分支管理类

Git 分支是日常协作的核心,日常开发中基本离不开以下几处:

git branch # 查看本地分支列表,当前分支前有 * git branch <name> # 创建新分支 git checkout <name> # 切换到目标分支 git switch <name> # 新版命令,切换分支更语义化(Git 2.23+) git checkout -b <name> # 创建并切换到新分支 git switch -c <name> # 等价于上面的命令 git branch -d <name> # 删除已合并的分支 git branch -D <name> # 强制删除未合并的分支

git checkout -b是创建分支最常用的组合命令,很多人把它当成“快捷方式”,其实拆开来理解更清楚:git branch <name>负责创建,git checkout <name>负责切换,组合起来就是一步到位。新版 Git 推出了git switch,语义上更清晰,推荐新项目直接用git switch而不是git checkout,因为 checkout 还承担着“恢复文件”的职责,一个命令两个含义,容易混淆。

删除分支的-d和-D区别很大,很多人在这上面吃过亏。-d只允许删除已经被合并过的分支,如果分支有未合并的提交,Git 会拒绝删除,这是保护机制。-D是无条件强制删除,一旦删除,该分支上的提交如果没有其他引用,会被直接丢进回收站,某种意义上等于找不回来了。我建议日常只用-d,确需-D时一定要先检查分支上有没有不得不保留的提交。

2.3 查看与对比类

不夸张地说,排查问题的时间占了日常工作的一大部分。有几个查看命令我几乎天天用:

git log --oneline --graph --decorate # 简化日志,显示分支图 git log -p <file> # 查看某个文件的历史改动细节 git diff # 工作区 vs 暂存区的差异 git diff --cached # 暂存区 vs 版本库的差异 git show <commit-hash> # 查看某次提交的具体改动

git log --oneline --graph是我的固定搭配,--graph会用*和|画出分支合并的拓扑结构,非常直观。如果日志太多,可以加上--decorate查看分支标签指向的提交。git log --all --graph可以查看所有分支的提交图,排查合并问题时很好用。

git diff的两个变体值得多说一句。不加参数时,git diff比较的是工作区和暂存区,也就是说,你改了文件但还没git add,这时候看到的差异就是它。加了--cached,比较的是暂存区和版本库,也就是你已git add但还没git commit的内容。这两个命令经常被搞反,我记忆的方式是:--cached就是“已经缓存起来的内容”,对应暂存区。

2.4 撤销与回退类

这是所有 Git 命令里最需要谨慎对待的一类,因为涉及历史操作,稍有不慎就会影响全组同事。

git restore <file> # 丢弃工作区的改动,回到当前版本库状态 git restore --staged <file> # 将暂存区的改动移回工作区(取消 add) git reset --soft HEAD~1 # 撤销最近一次 commit,保留改动在暂存区 git reset --mixed HEAD~1 # 撤销最近一次 commit,保留改动在工作区 git reset --hard HEAD~1 # 撤销最近一次 commit,丢弃所有改动(危险) git revert <commit-hash> # 生成一个新的反向提交,撤销指定提交

git reset有三种模式,--soft、--mixed、--hard,它们决定改动去向哪里。--soft把这次提交的改动全部留在暂存区,适合你发现提交信息写错了、想重新提交的场景。--mixed是默认模式,改动回退到工作区,适合提交后发现还需要追加修改的场景。--hard最狠,直接把这次提交的所有改动全丢弃,文件回到提交前的状态,一旦执行,工作区里未提交的改动也会一并消失。

git revert是另一个思路,它不会动历史,而是生成一个完全相反的新提交。比如提交 A 删除了某行代码,revert A 会生成一个新增这行代码的新提交。这个命令在生产分支回滚时非常安全,因为它保留了完整的操作痕迹,所有同事都能从历史里看到发生了什么。团队协作时,reset --hard应该被视为“仅在未推送或单分支场景才能用”的高危命令,凡是已经 push 到远程的分支,一律用revert。

2.5 远程同步类

协作离不开远程仓库,涉及的命令在团队中出错率高居榜首:

git clone <url> # 克隆远程仓库到本地 git remote -v # 查看远程仓库地址 git fetch origin # 下载远程最新状态,不合并 git pull origin <branch> # 拉取远程分支并尝试合并 git push origin <branch> # 推送本地分支到远程 git push -u origin <branch> # 首次推送新分支并设置上游关联 git push origin --delete <branch> # 删除远程分支

git pull导致冲突的情况很常见,本质原因是它自动执行了 merge,而 merge 的前提是本地分支和远程分支的修改没有交集。如果两个分支都改了同一个文件的同一行代码,Git 会停下来,让你手工解决冲突。这个过程很多人第一次遇到时很慌,其实不慌,Git 会产生一个冲突状态文件,里面用<<<<<<<、=======、>>>>>>>标记出两边内容,你把文件改成最终想要的样子,git add后git commit即可解决。

我自己的习惯是尽量用git pull --rebase而不是git pull,这样本地未推送的提交会被垫到远程最新提交的后面,提交历史是一条直线,不会生成一坨 merge commit。这个偏好后面在讲 merge 与 rebase 的部分详细展开。

2.6 临时保存与清理类

这个场景经常被忽略,但实际开发中特别好用,比如你在一个分支上改到一半,临时要切到别的分支看个东西:

git stash # 保存当前工作区和暂存区的改动,恢复干净工作区 git stash list # 查看保存的临时改动列表 git stash pop # 恢复最近一次 stash 并删除该记录 git stash apply # 恢复最近一次 stash 但不删除记录 git stash drop # 手动删除某条 stash 记录

git stash本质上是一个栈,你把当前改动压栈,可以随时弹出来恢复。比如周五下班前代码改到一半,又不想提交一个残缺的 commit,stash就很合适。stash pop可能会产生冲突,因为工作区的目标文件可能已经被改过了,这种冲突正常解决即可,不用慌。

3. 高频场景的完整实操链路

光有命令清单还不够,我更想分享几个完整场景的实操过程。以下是我平时在真实项目中走了一遍又一遍的流程,每段都是直接能用的。

3.1 日常提交的标准操作链路

正常改需求的流程可以这样走,我一般会刻意改成一种机械执行的状态:

  1. 开工前先git pull拉一下远程最新代码,减少后面冲突的可能
  2. 新建并切换分支:git switch -c feature/user-login
  3. 写完一阶段代码,git status看下改动情况,重点确认有没有把敏感文件或临时文件加进来
  4. git add具体的文件,我习惯显式列出文件名,避免git add .误加依赖目录或构建产物
  5. git diff --cached检查暂存区内容,确认改动确实是自己想要的
  6. git commit -m "feat: 新增用户登录页"提交并写上清晰的说明
  7. 重复第3-6步,直到功能完成
  8. git push -u origin feature/user-login推到远程,后续通过git push继续提交

这个流程看起来平平无奇,但我见过不少人跳过第5步,结果把调试代码提交进仓库,后面花更长时间处理。提交前看一眼差异,是性价比最高的防范手段。

3.2 分支合并:merge 与 rebase 的选择问题

合并分支是协作开发的高频操作。Git 提供merge和rebase两种思路,很多人的困惑在于不知道区别和适用边界。

先看命令本身:

git merge <branch> # 把 branch 合入当前分支,生成合并提交 git rebase <branch> # 把当前分支的提交历史变基到 branch 之上

merge保留两个人各自的提交历史,然后产生一个新的 merge commit,历史看起来有分支分叉又交汇的痕迹,相对真实和完整。rebase的机制是把当前分支的提交从公共基点开始的这段“摘下来”,重新按顺序拼接到另一个分支的顶端,历史是一条线,非常清爽。

结合上面的场景说明一下,假设你在 feature 分支上开发了三天,主分支 main 推进了两个提交。在 feature 上执行git rebase main后,feature 的三个提交会重新排列在 main 最新提交的后面,像一个串一样。好处是历史清晰,缺点是 rebase 会改写提交哈希,如果 force push 了,工作区的人都要重新同步,自己一个人用没问题,共享分支上需要谨慎。

我的经验法则:个人分支用 rebase 保持干净,公共分支用 merge 保持真实。比如老板要看合入记录,merge 的提交图表能说明哪天、哪个分支、合入了哪些内容。而自己开发的分支,把多个琐碎的 commit 用 rebase 整理成几个有意义的提交,再合入主干,是最舒服的节奏。

3.3 回滚操作的三类场景

回滚是最容易出事的场景,我把经验拆成三类:

第一类,提交后发现改错了,但还没推送,优先用git reset。比如刚提交了一个错误的文件,可以git reset --soft HEAD~1,把这次的改动退回到暂存区,重新git add正确文件后再次提交。

第二类,代码已经推送到远程的公共分支,比如 main 分支上有个错误提交被其他人拉取过了,这时不要用 reset,而要用git revert。它生成一个反向提交,代码回到正确状态,但历史里原错误提交仍然存在,其他同事 pull 这个新提交就能自动同步,不需要任何强制操作。

第三类,本地分支被自己搞乱,但远端有一个可用的版本,可以直接用git reset --hard origin/main,把本地状态强行对齐到远程。这个操作很暴力,执行前必须确认本地没有想保留的未提交改动。

分享一个记忆口诀:reset是“回到过去”,revert是“否定过去”。回到过去适合私密操作,否定过去适合公开操作——凡是已经 push 且被他人拉取过的提交,只用 revert。

3.4 解决冲突的标准流程

冲突没法完全避免,但可以按流程优雅处理:

  1. 冲突发生后,Git 会把冲突标记写入文件,git status会列出both modified状态的文件
  2. 打开冲突文件,搜索<<<<<<<、=======、>>>>>>>,上半部分是当前分支内容,下半部分是待合入分支内容
  3. 逐处判断保留哪些内容,编辑后删除这些标记行
  4. 处理完所有冲突文件后,git add这些文件
  5. 如果冲突来自 merge,执行git commit(Git 已准备好提交信息);如果来自 rebase,则git rebase --continue;来自 stash pop 的话,直接继续操作即可
  6. 用git status确认状态干净后,再继续下一步

处理冲突时有一个容易忽视的点:编辑器里对上下的内容进行取舍时,要防止把两边的重复行全保留,导致逻辑重复。比如一个人重命名了函数,另一个人新增了这个函数的调用,合入后可能同时存在新旧两个函数名。解决冲突时最好像我们处理合并代码那样,结合上下文整体审视,而不是只盯着几行冲突标记。

3.5 临时切换分支的 stash 操作

我在改一个功能时被要求紧急处理线上 bug 是常态。改到一半的代码不能提交,又不能覆盖,处理方式是这样的:

git stash # 保存当前改动 git switch -c hotfix/urgent-fix # 切去修 bug # 修复完并提交、推送 git switch feature/user-login # 切回原分支 git stash pop # 恢复之前的改动

stash pop恢复后如果报冲突,通常是因为恢复时分支代码和 stash 里的代码基于的版本差距较大,耐心按冲突流程处理即可。另外,git stash默认不包含未跟踪文件,如果你还新建了文件,要git stash -u才把它们也保存进去,这个细节经常被遗忘。

4. 常见问题与踩坑排查

这部分是多年实战中积累的排雷经验,基本上每一个我都亲手踩过。

4.1 detached HEAD(游离头指针)状态是咋回事

有时候执行完git checkout <commit-hash>后,会看到提示You are in 'detached HEAD' state。这说明你不在任何分支上,而是直接停在某个提交上。此时继续提交,提交会挂在半空,切换分支后可能找不到。

解决办法是立刻创建一个分支来承接:git switch -c tmp-branch,把当前的游离提交保存下来。养成习惯的话,不要直接 checkout 到某个 hash 浏览代码,用git log --oneline查看后复制 hash 再 checkout,看完就切回原分支。如果发现自己的改动在游离状态下意外丢失,可以执行git reflog找到那一串操作记录,从提交 hash 找回。git reflog记录了 HEAD 的所有移动历史,是本地操作的“后悔药”。

4.2 误用了 reset --hard,改动还能找回来吗

可以,但前提是动作要快。当你执行了git reset --hard,被丢弃的提交在短时间内仍留在本地对象库里,通过git reflog能看到 release 前的 HEAD 哈希,然后git reset --hard <hash>恢复。

如果git reflog里也找不到,但改动曾被提交过且被 push 到了远程,可以通过git reflog找到那个哈希后,git push origin 本地分支:远程分支覆盖回远程。任何 force push 都可能导致他人本地历史不一致,需要同步告知协作方执行git pull --rebase刷新。

这里想强调一个原则:在分支上动手脚之前,先考虑这个分支是否共享。共享分支上尽量不 reset,更不 force push。尊重共享历史,是对团队协作负责的基本素养。

4.3 大小写敏感、换行符等文件相关的坑

Windows 和 macOS/Linux 协作时,换行符问题很常见。Git 默认会智能处理,但遇到混用可能导致 diff 显示整个文件被改动。应对方案有两点:

  • 仓库根目录加.gitattributes文件,统一配置换行符规则
  • 统一团队 IDE / 编辑器的换行符设置为 LF

还有一个小细节:macOS 的文件系统默认大小写不敏感,但 Git 本身区分大小写。一个文件从README.md改成readme.md,在 macOS 上看起来是重命名了,但 Git 可能显示为删除了旧文件、新增了新文件。如果遇到大小写导致的问题,可以在仓库里用git mv先把文件移到一个临时名,再移到目标名,两次 mv 触发 Git 感知重命名。

4.4 提交了不该提交的大文件

常见的是把编译产物、安装包、日志、数据库备份等大文件直接 commit 进了仓库。Git 仓库会越来越臃肿,clone 速度直线下降。处理思路分两步:

  1. 从文件历史中移除:使用git filter-repo或git filter-branch重写历史(这一步比较深,建议在有测试环境的情况下操作)
  2. 从源头禁止:新建.gitignore文件,把大文件对应的目录、后缀写进去,比如*.log、/build、/target等

比“不小心提交”更省心的做法是从第一天就写一份合理的.gitignore。这里列一个常见模板:

# 依赖目录 node_modules/ vendor/ # 构建产物 dist/ build/ target/ # 日志 *.log # 系统与编辑器 .DS_Store .idea/ .vscode/

4.5 强推(force push)的连锁反应

如果因为 pull 到一半改了历史,引发别人提交不同步,面不改色地进行如下检查:

  • git push --force会覆盖远程分支到本地状态。如果别人基于旧记录新提交了代码,这些提交会直接丢失,触发同事标题党的“谁把我的代码删了”。
  • git push --force-with-lease更谨慎,它先比对远程分支当前状态与你上次 fetch 的状态,不一致就拒绝 push,保护他人提交不被误覆盖。原则是:团队分支上的任何强推,必须先跟成员打招呼,并优先用--force-with-lease。

5. 几个值得养成的 Git 使用习惯

命令清单再完整,最终还是要靠日常习惯来撑。最后分享几个我一直在用、且真实改善效率的做法。

5.1 配置简写命令,减少重复敲击

在~/.gitconfig里设置 alias,是性价比最高的提升操作。以下是我一直在用的配置:

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 --oneline --graph --decorate --all" git config --global alias.last "log -1 HEAD --stat"

有些人会配置更激进的缩写,比如把 checkout 缩成 co、commit 缩成 ci,但我建议 alias 不要过于抽象,否则换台机器会想不起来。常用的这几个,键盘敲击量和记忆成本都能降到最低。

5.2 提交信息写清楚,三个月后你还会感谢自己

git commit -m支持多行信息,别只写一行模糊的描述。推荐遵循约定式提交(Conventional Commits)的格式:

feat: 新增用户登录接口 fix: 修复登录状态下刷新页面会退出问题 docs: 更新部署文档 refactor: 重构数据校验模块

这样的提交信息天然适合阅读git log --oneline,看到前缀就知道这次提交的类型。团队如果统一规范,后续做 changelog、代码审查、回溯问题都会很轻松。

5.3 用工作树(worktree)处理多分支并行

如果你和我一样经常陷入“A 分支改一半,B 分支要紧急修 bug”的困境,常规方案是 stash 切分支,但这有一个缺点:stash 只能保存一次状态,多次 stash 之后找起来很乱。

Git worktree 是更优雅的方案,它允许同一个仓库建出多个工作目录,每个目录对应一个分支互不干扰:

git worktree add ../project-fix -b hotfix/urgent-fix

这样你原有的项目目录还在 A 分支保持原样,另一个目录../project-fix是 B 分支的工作空间,两边同时打开,随时切换零开销。等修复完成后,删除 worktree 再执行git worktree prune清理即可。

5.4 遇到问题时先看 status 和 reflog

踩过太多坑之后,我现在面对任何 Git 异常,第一反应不是去网上搜一条命令直接执行,而是先git status看当前状态,再用git reflog看历史移动记录。

reflog的正确打开方式示例:

$ git reflog --oneline -10 abc1234 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2 def5678 HEAD@{1}: commit: feat: 用户模块开发 1234567 HEAD@{2}: checkout: moving from feature/user-login to main

结合我刚才聊的“reset 找回”“stash 找回”“脱离分支找回”,你会发现reflog才是真正的保险丝。所以不妨平时就记住这个命令,关键时候能救命。

说实话,Git 并不难学,难的是第一次遇到异常时保持冷静,不盲敲命令。把常用命令清单转换成“出了问题先看状态、再定位阶段、最后选命令”的思维方式,才是真正掌握 Git 的标志。如果你能把这篇文章的核心内容消化掉,再遇到回滚、冲突、意外删除这类问题,心态基本可以稳住了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进