1. 这不是“高级命令合集”而是日常开发中每天都在用的生存技巧Git 的学习曲线有个特别真实的现象刚入门时觉得add、commit、push就够用了等项目跑起来团队协作一上规模突然发现每天有大量时间花在“怎么把刚才那个改错的文件撤回来”“为什么这个文件老是被我误提交”“上次我改的那个函数到底动了哪几行”“为什么每次打长命令都要敲三遍”这类琐事上。标题里列的这六项——引用日志reflog、忽略临时性本地修改、检查文本文件修改、命令别名、为临时提交建分支、移动提交到另一分支——根本不是什么“炫技彩蛋”而是我在带多个跨地域团队、维护五个以上主干分支、平均每天处理 20 次本地操作的实战中反复验证过、删减过、最终沉淀下来的六条“防手抖保命线”。它们共同解决一个核心问题如何让 Git 从一个冷冰冰的版本记录工具变成你本地工作流的呼吸节奏本身。比如 reflog 不是让你去考古的是当你reset --hard HEAD~3错了之后30 秒内原路找回的保险绳忽略临时修改不是为了偷懒是防止.env.local或config-debug.php这类敏感/环境专属文件意外进仓库的物理隔离层而“为临时指向的提交创建分支”本质上是你在代码审查中途突然想验证一个补丁效果时不用中断当前工作流就能开个沙盒的快捷键。这些技巧之所以被归为“十一”不是因为它们排在第十一章而是因为前十个章节教你怎么把车开起来而这一章教你如何在暴雨夜、急弯道、没导航的情况下不撞护栏、不熄火、还能顺手给后座乘客递杯水。如果你现在还在靠git log --oneline | head -20翻历史、靠手动复制粘贴改命令、靠删掉整个node_modules来解决冲突那这篇就是为你写的——它不讲原理推导只讲你明天早上打开终端第一件事该怎么做。2. 核心技巧逐层拆解与底层逻辑还原2.1 引用日志reflogGit 的本地操作黑匣子不是历史是操作轨迹很多人第一次听说 reflog是在git reset --hard后发现代码没了慌乱中搜到“Git 恢复已删除提交”。但 reflog 的真正价值远不止于“后悔药”。它的本质是Git 在你本地仓库中对每一个“HEAD 移动”动作的不可篡改式快照记录。注意关键词本地、HEAD 移动、不可篡改。它不记录文件内容变化不记录分支合并逻辑只忠实地记下“X 月 X 日 X 时 X 分HEAD 从 commit abc123 移到了 def456原因是执行了reset --hard HEAD~1”。为什么它比git log可靠因为log展示的是当前分支可到达的所有提交即 DAG 图中的可达路径一旦你reset --hard或rebase那些被移出路径的提交就从log里消失了而 reflog 是独立日志只要没执行git reflog expire默认90天自动清理它就永远躺在.git/logs/HEAD里连gc都不会动它。我曾在一个客户现场遇到极端案例某开发者误将生产环境配置 commit 推到了 develop 分支紧急revert后又push --force覆盖了远程历史。结果第二天发现 revert 的逻辑有缺陷需要重新基于原始错误 commit 做修复。此时git log origin/develop里早已找不到那个 commit 的任何痕迹但git reflog show develop里清清楚楚写着“2 days ago develop{0}: reset: moving to HEAD~1”、“2 days ago develop{1}: commit: [prod] accidentally pushed config”。我们直接git checkout develop{1}拿回原始 commit再基于它做正确修复——整个过程不到两分钟。提示reflog 的索引是相对的develop{0}是当前 develop 指向的提交develop{1}是上一次 develop 指向的提交。它不依赖 commit hash所以即使你rebase后 hash 全变{n}依然有效。这是它作为“操作轨迹”而非“提交历史”的关键特征。2.2 忽略临时性的本地修改.gitignore的兄弟.git/info/exclude新手常混淆两个概念.gitignore是项目级规则会随仓库一起传播适合忽略node_modules/、__pycache__/这类通用产物而.git/info/exclude是纯本地、不进仓库、专为“我这次调试要改的临时文件”设计的隔离区。它的存在就是为了解决那个经典困境你正在调试一个数据库连接超时问题需要临时把config/database.php里的 host 改成127.0.0.1但你绝不想把这个改动提交——它既不是功能变更也不是配置优化纯粹是此刻的调试副产品。.git/info/exclude的威力在于其“绝对本地性”。它位于每个克隆副本的.git/目录下永远不会被git add、git commit或git push触及。你往里面加一行config/database.php下次git status就彻底看不见这个文件的修改git diff也不会显示它git add .更不会把它带上。更重要的是它支持和.gitignore完全一致的语法通配符、取反!、注释#你可以把它当成个人开发环境的“免打扰模式开关”。我习惯在新项目初始化后立刻在.git/info/exclude里加上# 本地调试专用 .env.local config/debug-*.php *.log .DS_Store这样无论团队里谁改了.env模板我的.env.local永远安静如鸡。它不像git update-index --skip-worktree那样需要记忆复杂命令也不像git stash那样有状态管理成本就是一行配置永久生效。2.3 检查对文本文件的修改git diff的七种武器不是只有--word-diff当同事说“我只改了一个变量名”而你git diff看到满屏红色绿色怀疑人生时你需要的不是更宽的显示器而是git diff的精准打击模式。git diff默认展示的是“按行”差异但文本文件的语义单位其实是“词”甚至“符号”。--word-diff是入门级武器它把差异粒度缩小到单词级别比如const MAX_RETRY 3;改成const MAX_RETRY 5;diff 会高亮3和5而不是整行。但这还不够。真正的高手会组合使用git diff --word-diff-regex[a-zA-Z0-9_]|[^[:space:]]自定义“词”的定义。默认 regex 把当作分隔符导致MAX_RETRY 3被切成MAX_RETRY和3两部分。这个自定义 regex 明确告诉 Git“词”是字母数字下划线组合或任意非空白字符即、{、}都算独立“词”这样就不会被切开语义更准。git diff --ignore-space-change忽略空格数量变化。前端同学改 HTML把div classa b拆成多行写空格多了diff却疯狂报红。加这个参数Git 只关心标签和属性名是否变不关心缩进。git diff --ignore-all-space更激进连空格位置都忽略。适合对比被不同编辑器格式化过的 JSON。git diff --no-index file1 file2对比两个完全不在 Git 管理下的文件。比如你本地有个config-prod.json想和线上拉下来的config-prod.json.bak对比不用先git add直接git diff --no-index。git diff --color-words在终端里用颜色高亮“词”级差异比--word-diff更直观尤其适合快速扫读。我每天必用的是git diff --word-diff-regex[a-zA-Z0-9_]|[^[:space:]] --ignore-space-change把它 alias 成gdw见后文审 PR 时一眼锁定真实业务逻辑变更过滤掉 80% 的噪音。2.4 Git 命令别名把 12 个按键压缩成 3 个且永不遗忘git status -sb简短格式 分支信息是我每天敲 20 次的命令但它要按 15 个键g-i-t-空格-s-t-a-t-u-s-空格--s-b。而git co -b feature/login创建并切换分支要按 18 个键。这些高频操作如果每次都要完整输入不仅慢而且极易因手滑输错checkout成checkou导致报错。Git 别名alias就是你的键盘宏。关键在于别名不是简单的字符串替换而是可以嵌入 shell 逻辑的脚本载体。比如# 最基础的缩写 co checkout br branch ci commit st status # 带参数的智能别名 # gd file只显示指定文件的 word-diff gd !f() { git diff --word-diff-regex[a-zA-Z0-9_]|[^[:space:]] --ignore-space-change \$\; }; f # gcamcommit all modified, with auto-generated message gcam !f() { git add -u git commit -m \chore: update $(git status -s | grep ^ M | cut -d -f3 | xargs | sed s/ /, /g)\; }; f # gbv列出所有本地分支并标出当前分支*号 gbv !f() { git for-each-ref --format%(if:equalsHEAD)%(refname:short)%(then)* %(else) %(end)%(refname:short) %(committerdate:relative) refs/heads/; }; f看gcam这个别名!f() { ... }; f是 shell 函数定义语法$代表所有传入参数。它先git add -u只添加已跟踪文件的修改再生成一条 commit messagemessage 里自动提取所有被修改的文件名git status -s | grep ^ M找出修改行cut -d -f3取第三列文件名xargs | sed把它们串成逗号分隔列表。这样你只需gcam就完成“暂存所有修改生成描述性消息”的全流程且消息内容是真实的、可追溯的。注意别名定义在~/.gitconfig的[alias]段下全局生效。不要用git config --global alias.xxx逐条添加容易遗漏和混乱。直接编辑文件结构清晰备份方便。2.5 为临时指向的提交创建分支git switch -c branch commit比checkout -b更安全场景很常见你在main分支上看到一个 PR 的 commita1b2c3d很有意思想拉下来本地跑一下测试但又不想污染当前工作区。传统做法是git checkout -b temp-test a1b2c3d但这里有个隐藏风险checkout命令在旧版 Git 中如果目标 commit 不在任何分支上它会进入“分离 HEAD”状态而-b参数在这种状态下可能行为异常尤其在 Windows 上。更现代、更明确的方式是git switchGit 2.23 引入它是checkout的语义拆分版switch专管分支切换restore专管文件恢复。git switch -c temp-test a1b2c3d的含义是“创建一个名为temp-test的新分支让它指向 commita1b2c3d然后立即切换过去”。它强制要求你明确指定“创建分支”-c和“目标提交”a1b2c3d两个动作杜绝了歧义。执行后temp-test分支诞生HEAD 稳稳地落在a1b2c3d上你可以放心npm test、python manage.py runserver所有操作都在这个干净的分支里。测试完git switch main回去git branch -d temp-test删除即可全程无副作用。我把它 alias 成gtcgo-to-commit配合git log --oneline -n 20使用形成“看日志 - 复制 hash - gtc ”的三步闪电流程。比checkout少一个思维负担你不需要判断当前是“在分支上”还是“在 detached HEAD”switch总是按你字面意思执行。2.6 将提交移动到另一分支git cherry-pick与git rebase --onto的抉择“把 A 分支上的某个提交挪到 B 分支上”——这句话听起来简单实操中陷阱密布。核心在于你要的到底是“复制”还是“迁移”git cherry-pick commit是复制它把commit的变更内容patch应用到当前分支生成一个全新的、hash 不同的提交。适合场景A 分支有个 bugfixB 分支也需要但 A 和 B 已经分叉很久直接 merge 会带入大量无关变更。cherry-pick就像复印机只印你需要的那一页。git rebase --onto new-base old-base branch是迁移它把branch分支上从old-base不含到branchtip含之间的所有提交整体“拔”起来重新安放到new-base之上。适合场景你在feature/login分支上开发了 5 个提交但后来发现它们应该基于develop而不是main这时rebase --onto develop main feature/login就能把这 5 个提交整体迁移到develop顶端。关键区别在于cherry-pick操作单个提交rebase --onto操作提交序列。但rebase --onto的参数顺序极其反直觉--onto new-base old-base branch极易输错。我总结了一个口诀“--onto后跟‘要去哪’中间是‘从哪起’最后是‘搬谁’”。比如git rebase --onto develop main feature/login意思是“把feature/login分支上从main不含之后的所有提交搬到develop上面去”。实操心得永远先用git log --oneline old-base..branch确认要迁移的提交范围再执行rebase --onto。执行前git branch backup-before-rebase创建备份分支以防万一。rebase是重写历史没有后悔键。3. 实操过程与核心环节实现3.1 reflog 的完整恢复流程从“完了”到“搞定”的 4 步假设你执行了git reset --hard HEAD~2删掉了最近两次提交现在git log里只剩三天前的记录。别慌reflog 是你的锚点。步骤 1定位丢失的提交# 查看 HEAD 的 reflog找到 reset 前的状态 git reflog show HEAD # 输出类似 # a1b2c3d HEAD{0}: reset: moving to HEAD~2 # d4e5f6g HEAD{1}: commit: fix user login timeout # h7i8j9k HEAD{2}: commit: add password strength validation # ... # 你需要的是 HEAD{1} 或 HEAD{2}即 reset 前的提交步骤 2验证提交内容可选但强烈推荐# 检出 HEAD{1} 到临时目录确认是你要的 git checkout HEAD{1} -- . # 此时工作区恢复了 HEAD{1} 的全部文件 # 用 IDE 或 git status 确认无误 # 如果不对git checkout main 回来再试 HEAD{2}步骤 3创建新分支或重置当前分支# 方案 A创建新分支保留原状最安全 git switch -c recovery-branch HEAD{1} # 方案 B直接重置当前分支如果确定要覆盖 git reset --hard HEAD{1} # 此时 git log 会重新显示那两个提交步骤 4推送如果已推送到远程# 如果之前 git push 过现在 reset --hard 后需要 force push git push --force-with-lease origin main # --force-with-lease 比 --force 安全它会检查远程分支是否被别人更新过如果被更新会拒绝 force避免覆盖他人工作注意reflog默认只记录 90 天长期未操作的仓库可能已过期。所以git reflog expire --expirenever --all可以永久保存不推荐磁盘空间有限更务实的做法是养成git switch -c backup-$(date %Y%m%d) HEAD的习惯每天开工前一键备份当前状态。我把它 alias 成gbk每天早上gbk心里踏实。3.2.git/info/exclude的配置与验证三行代码建立信任配置.git/info/exclude不是写完就完事必须验证它是否真的生效。步骤 1创建并编辑文件# 进入项目根目录 cd /path/to/your/project # 编辑本地排除文件 nano .git/info/exclude # 添加内容示例 .env.local config/*.debug.php *.tmp步骤 2制造一个被排除的文件# 创建一个 .env.local 文件 echo DB_HOST127.0.0.1 .env.local # 创建一个 debug 配置 echo ?php return [debug true]; ? config/app.debug.php步骤 3验证git status是否忽略# 执行 status观察输出 git status # 正确输出应为 # On branch main # Your branch is up to date with origin/main. # nothing to commit, working tree clean # 即使你刚创建了 .env.local 和 app.debug.php它们也不会出现在 untracked files 列表里 # 对比如果删掉 .git/info/exclude 里的对应行再执行 git status它们就会出现步骤 4验证git add是否无效# 尝试强制添加测试边界 git add .env.local # 此时不会报错但 git status 仍显示 clean且 git commit 不会包含它 # git ls-files --others --exclude-standard 可以列出所有被忽略的未跟踪文件确认它们在列表中实操心得.git/info/exclude的规则优先级高于.gitignore但低于git update-index --skip-worktree。如果一个文件同时被exclude和skip-worktree标记skip-worktree生效。所以对于绝对不能提交的敏感文件如私钥用skip-worktree对于只是“这次调试用”的临时文件用exclude。两者不冲突可共存。3.3git diff高级组合技一份配置三种场景我把最常用的git diff组合封装成三个别名覆盖 95% 的日常需求别名 1gdw—— 精准词级差异审 PR 专用# ~/.gitconfig [alias] gdw !f() { git diff --word-diff-regex[a-zA-Z0-9_]|[^[:space:]] --ignore-space-change --color-words \$\; }; f使用场景Code Review 时快速扫描 PR 修改。gdw HEAD~1对比上一个 commitgdw origin/main对比远程 main。效果函数名、变量名、数字、符号,!,{,}都独立高亮空格缩进变化被忽略一眼看出逻辑变更。别名 2gdf—— 文件级差异摘要快速定位gdf !f() { git diff --name-only --diff-filterAMR \$\ | head -20; }; f使用场景git pull后想知道这次更新改了哪些文件但不想看详细 diff。gdf origin/main列出所有被修改/新增/重命名的文件名最多 20 行。效果输出类似src/utils/date.js,package.json,README.md帮你快速聚焦到关键文件。别名 3gdc—— 提交间内容差异追溯 Buggdc !f() { git diff \$1\ \$2\ --word-diff-regex[a-zA-Z0-9_]|[^[:space:]] --ignore-space-change; }; f使用场景线上发现一个 Bug怀疑是某次发布引入的。git log --oneline v1.2.0..v1.3.0找出两个版本间的 commit然后gdc v1.2.0 v1.3.0直接对比这两个 tag 的全部内容变更。效果跳过中间所有 commit直接看到“从 v1.2.0 到 v1.3.0代码库到底变了什么”极大缩短排查路径。注意所有别名中的--word-diff-regex都经过实测在 JavaScript、PHP、Python、HTML、CSS 中表现稳定。正则[a-zA-Z0-9_]|[^[:space:]]的含义是匹配连续的字母数字下划线即标识符或者任意一个非空白字符即运算符、括号、分号等。这样user.name admin会被拆成user、.、name、、admin七个“词”每个词独立高亮语义清晰。3.4 别名系统的构建与维护从零开始的 10 分钟搭建别名系统不是一蹴而就的它应该随着你的工作流进化。以下是我在新机器上搭建的标准化流程步骤 1初始化基础别名5 分钟# 创建 ~/.gitconfig如果不存在 touch ~/.gitconfig # 写入基础框架 cat ~/.gitconfig EOF [alias] # 基础缩写 co checkout br branch ci commit st status lg log --graph --prettyformat:%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)%an%Creset --abbrev-commit --daterelative # lg 是图形化日志带颜色和相对时间一目了然 EOF步骤 2添加高频生产力别名3 分钟# 追加到 ~/.gitconfig cat ~/.gitconfig EOF # 高频操作 gdw !f() { git diff --word-diff-regex[a-zA-Z0-9_]|[^[:space:]] --ignore-space-change --color-words \$\; }; f gdf !f() { git diff --name-only --diff-filterAMR \$\ | head -20; }; f gtc !f() { git switch -c \temp-$(date %s)\ \$1\; }; f # gtc 创建临时分支名字带时间戳避免重名 gbk !f() { git switch -c \backup-$(date %Y%m%d-%H%M%S)\ HEAD; }; f # gbk 创建带完整时间戳的备份分支 EOF步骤 3验证与同步2 分钟# 重新加载配置或新开终端 git config --list | grep alias # 应该看到所有 alias 都在列表中 # 同步到其他机器如果用 dotfiles 管理 # cp ~/.gitconfig ~/dotfiles/gitconfig # git -C ~/dotfiles add gitconfig git -C ~/dotfiles commit -m update git aliases git -C ~/dotfiles push实操心得别名名尽量用小写字母和短横线避免下划线某些 shell 有兼容性问题。别名值用双引号包裹内部$用双引号确保参数传递正确。定期git config --get-regexp alias检查别名是否生效比git help -a更直接。3.5git switch创建临时分支的完整链路git switch -c branch commit看似简单但结合 reflog能形成强大工作流。场景在 Code Review 中发现一个可疑 commit# 1. 查看 PR 的提交历史 git log --oneline origin/main..origin/pr-123 -n 10 # 输出a1b2c3d fix: handle null pointer in payment service # d4e5f6g feat: add webhook retry logic # 2. 想单独测试 a1b2c3d git switch -c pr123-fix-test a1b2c3d # 3. 此时已在新分支运行测试 npm test # 或 python -m pytest tests/test_payment.py # 4. 测试通过想把这个 fix 合并到 main git switch main git cherry-pick a1b2c3d # 生成新 commithash 不同但内容一致 # 5. 清理临时分支 git branch -d pr123-fix-test进阶用 reflog switch 快速回溯# 你昨天在某个分支上做了实验今天忘了分支名 git reflog --dateiso | grep switch: # 输出2023-10-05 14:23:01 0800 HEAD{0}: switch: from feature/user-profile # 2023-10-04 09:15:22 0800 HEAD{1}: switch: from temp-test-1696400122 # 发现 temp-test-1696400122 是你昨天建的直接切回去 git switch temp-test-1696400122注意git switch创建的分支和git branch创建的分支完全一样没有任何特殊标记。它的优势在于命令语义清晰且switch命令本身不会触发任何危险操作如checkout在 detached HEAD 下可能意外修改文件。3.6cherry-pick与rebase --onto的实操对比用一个具体例子说明何时用哪个。初始状态main分支A-B-C-Dfeature/login分支A-B-C-D-E-F-G基于 main 开发需求提交E是一个重要的安全修复需要尽快上线到main但F和G是未完成的功能不能上。方案 1cherry-pick推荐git switch main git cherry-pick E # main 变成 A-B-C-D-EE 是 E 的副本hash 不同 # 安全修复上线F/G 未引入完美方案 2rebase --onto错误选择git rebase --onto main C feature/login # 这会把 feature/login 上 C 之后的提交E,F,G全部搬到 main 之后 # 结果main 变成 A-B-C-D-E-F-G把未完成功能也带上了 # 这违背了需求正确使用rebase --onto的场景需求feature/login分支其实应该基于develop而不是main开发因为develop包含了最新的 API 兼容层。操作# 查看分支关系 git log --oneline main # A-B-C-D git log --oneline develop # A-B-C-D-X-Y-Z # 确认 feature/login 是从 D 开始的 git log --oneline main..feature/login # E-F-G # 执行 onto把 feature/login 上 mainD之后的提交搬到 developZ之后 git rebase --onto develop main feature/login # 结果feature/login 变成 X-Y-Z-E-F-G完美对接 develop关键口诀cherry-pick用于“摘一个果子”rebase --onto用于“换一棵树”。前者单点后者批量前者复制后者迁移。4. 常见问题与排查技巧实录4.1 reflog 相关问题为什么我的 reflog 里没有想要的记录问题现象执行git reflog输出为空或找不到前几天的操作记录。排查思路确认 reflog 是否启用git config --get core.logAllRefUpdates。如果输出falsereflog 被禁用。修复git config --local core.logAllRefUpdates true当前仓库或git config --global core.logAllRefUpdates true全局。检查过期时间git config --get gc.reflogExpire。默认90.days.ago如果操作超过 90 天已被git gc清理。修复git reflog expire --expirenever --all慎用占磁盘。确认操作对象git reflog默认显示HEAD的日志。如果你在feature分支上操作但git reflog没反应试试git reflog show feature查看该分支的 reflog。检查是否在 bare 仓库bare 仓库如远程服务器上的默认不开启 reflog。本地克隆的仓库都是 non-bare无需担心。独家技巧git reflog --all可以显示所有引用HEAD、所有分支、所有 remote的日志有时HEAD{n}找不到但在main{m}里能找到相同提交。4.2.git/info/exclude失效文件依然出现在git status中问题现象在.git/info/exclude里加了*.log但app.log依然在git status的 untracked 列表里。排查步骤确认文件未被git add过git ls-files --cached app.log。如果输出app.log说明它已被 Git 跟踪.git/info/exclude对已跟踪文件无效。解决git rm --cached app.log从暂存区移除保留工作区文件再git status就会忽略它。检查.gitignore冲突.gitignore里是否有!*.log这样的取反规则取反规则优先级高于exclude。解决检查.gitignore删除或注释掉冲突行。确认路径正确exclude中的路径是相对于.git/info/exclude文件所在目录即项目根目录的。如果app.log在logs/子目录下exclude里要写logs/*.log而不是*.log。检查 Git 版本极老版本 Git 1.7.0可能不支持.git/info/exclude。升级 Git 即可。实操心得用git check-ignore -v app.log命令可以精确追踪一个文件被哪个规则忽略。它会输出类似.git/info/exclude:1:*.log:app.log明确告诉你规则来源和行号是排查忽略失效的终极武器。4.3git diff颜色不显示或乱码问题现象git diff输出是黑白的或中文显示为M-oM-?M-?。解决方案颜色不显示git config --global color.ui auto。auto表示终端支持颜色时才启用。如果终端确实支持如 iTerm2、Windows Terminal但依然不显示检查TERM环境变量echo $TERM应为xterm-256color或类似。修复export TERMxterm-256color加到~/.bashrc。中文乱码Git 默认用 UTF-8