Git 命令速查

按"我现在想做什么"组织的命令表。撤销部分建议先读,因为大多数 Git 事故都发生在分不清工作区、暂存区和提交这三个位置的时候。

一次性配置

新机器上先做这几件事。配置写在 ~/.gitconfig,对所有仓库生效。

git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global core.editor "vim"
git config --global pull.ff only          # 拉取时只允许快进,避免意外的合并提交
git config --global core.autocrlf input   # macOS / Linux;Windows 上用 true

pull.ff only 值得单独说一句:默认的 git pull 在历史分叉时会直接生成一个合并提交,而且不给你看合并结果。设成 only 之后,分叉时它会报错停下,逼你显式选择 git pull --rebase 还是 git merge。这个"失败"比默默合并有用。

常用别名可以省不少打字,但别把别名写进团队文档——别人机器上没有。

git config --global alias.st status
git config --global alias.co checkout
git config --global alias.lg "log --oneline --graph --decorate --all"

每天用到的那几条

git status -s                 # 短格式:?? 未跟踪, M 已修改,M  已暂存
git add -p                    # 逐块挑选,而不是整个文件一起提交
git diff                      # 工作区 vs 暂存区
git diff --staged             # 暂存区 vs 最后一次提交
git commit -m "fix: 修正空指针判断"
git log --oneline -10
git show HEAD --stat

git add -p 是让提交历史保持干净的最省力办法:一个文件里混着两件不相关的事时,它让你只挑其中一块进本次提交。它给出的 y/n/s/q 里,s 是再拆小块,e 是直接手改要提交的行。

撤销:先判断改动在哪一层

几乎所有"怎么撤销"的问题,答案都取决于改动目前处在哪一层。按下面的表从上往下找。

现状目的命令
工作区已改,未暂存 丢弃这些改动 git restore <文件>
已 add,未提交 退回工作区,保留改动 git restore --staged <文件>
已提交,未推送 改提交信息 / 补一个漏掉的文件 git commit --amend
已提交,未推送 整个提交不要了,改动也丢 git reset --hard HEAD~1
已推送 撤销这次提交的影响 git revert <sha>

三条 reset 的区别只有一句话:--soft 只移动分支指针,改动全留在暂存区;--mixed(默认)连暂存区一起清,改动留在工作区;--hard 三处一起清。想"把最近三次提交合成一次重新写",用 git reset --soft HEAD~3 然后重新提交,比交互式变基快。

git revert 是唯一适合处理已推送历史的方式。它生成一个新提交来抵消旧提交,历史保持不变,别人拉取时不会出问题。reset --hard 加 push --force 会改写别人已经拿到的历史,除非你确认这个分支只有你一个人在用,否则不要这么做。

撤销一个合并提交要指定保留哪一边,否则 Git 不知道该沿哪条线继续:

git revert -m 1 <merge-commit-sha>   # 保留第一父提交(即合并前所在的分支)

分支

git switch -c feature/login          # 新建并切换(Git 2.23+)
git switch main
git branch -vv                       # 列出分支及其上游、领先/落后数量
git branch -d feature/login          # 已合并才能删;未合并用 -D 强删
git branch --merged                  # 哪些分支已经合进当前分支

checkout 同时负责切分支和还原文件,容易误操作,所以 2.23 之后拆成了 switch(切分支)和 restore(还原文件)。旧脚本里还能用 checkout,新写的命令建议用新的两个。

合并还是变基

mergerebase
历史形状保留分叉,多一个合并提交线性,像从来没分叉过
提交哈希不变全部重写
适合已推送的公共分支、需要保留合并点本地未推送的整理、跟进上游
风险历史较乱冲突要逐个提交解决

经验规则:只对还没推送的提交做变基。已经推送的分支上做变基,等于要求所有协作者重置本地分支。

冲突处理

git status                 # 冲突文件列在 "Unmerged paths"
# 打开文件,处理 <<<<<<< ======= >>>>>>> 三段
git add <冲突文件>
git merge --continue       # 或 git rebase --continue
git merge --abort          # 放弃,回到合并前

冲突标记的三段分别是"当前分支的版本"、"分隔线"、"被合入分支的版本"。如果只是想让某一侧赢,git checkout --ours <文件> 或 --theirs 可以整文件取一边,然后再 git add。注意在变基过程中 ours 和 theirs 的含义是反的——这是新手最容易搞混的一处。

找历史

知道"这句话是哪次提交加进来的",比逐条翻 log 快得多。

git log -S'retry_limit' --oneline          # 某段字符串出现或消失的提交
git log -G'def .*handler' -p -- '*.py'     # 按正则匹配 diff 内容
git log --follow -p -- path/to/file        # 跟随重命名
git blame -L 20,40 -- path/to/file         # 指定行范围的最后修改者
git log -L 20,40:path/to/file              # 这几行的演变过程

找"哪次提交弄坏了"用二分查找,不要靠猜:

git bisect start
git bisect bad                 # 当前版本有问题
git bisect good v1.4.0         # 这个 tag 是好的
# Git 每次检出中间版本,测完标记:
git bisect good                # 或 git bisect bad
git bisect reset               # 结束后回到原分支

如果有能自动判断的测试脚本,可以全自动跑完:git bisect run ./test.sh。脚本退出码 0 视为 good,1 视为 bad,125 表示跳过这个版本。

后悔药:reflog

分支被 reset --hard 掉、提交"消失"了,只要没被垃圾回收,都还能找回来。reflog 记录的是 HEAD 的每一次移动:

git reflog                 # 最近 90 天的 HEAD 移动记录
git reset --hard HEAD@{3}  # 回到 reflog 里的第 3 条状态
git switch -c rescue HEAD@{3}   # 更安全的做法:新建分支接住它

默认保留 90 天(不可达对象 30 天),gc 之后才真正删除。所以误删分支后的第一件事是别再乱敲命令,先看 reflog。

远程

git remote -v
git fetch --prune              # 拉取并清掉远端已删除的分支引用
git switch -c feature origin/feature
git push -u origin HEAD        # 首次推送并设置上游
git push --force-with-lease    # 必须强推时用它,而不是 --force

--force-with-lease 会在远端有你没见过的提交时拒绝推送,避免把别人的工作覆盖掉。--force 什么都不检查,只在"这个分支确实只有我一个人用"时才可以考虑。

临时存放

git stash push -m "wip: 表单校验"
git stash list
git stash pop              # 取出并删除这条
git stash apply stash@{1}  # 取出但保留
git stash show -p stash@{0}

stash 默认不包含未跟踪文件,需要时加 -u。它也不是长期存储:stash 里放了几周的东西基本会被忘掉,超过一天的改动更适合开一个临时分支提交上去。

常见坑

1. .gitignore 对已跟踪的文件无效

文件一旦进过版本库,再加进 .gitignore 也不会被忽略。要先从索引里移除(本地文件保留):

git rm --cached config.local.json
git commit -m "chore: 停止跟踪本地配置"

2. 分离头指针(detached HEAD)

直接 git checkout <某个提交> 会进入分离头指针状态,这时的提交不属于任何分支,切走之后很容易丢。在这个状态下想保留工作,先 git switch -c <新分支名>。

3. 空目录不会被跟踪

Git 只跟踪文件。需要保留目录结构时,放一个空的 .gitkeep 进去(名字没有特殊含义,只是约定)。

4. 换行符

Windows 用 CRLF,其他平台用 LF。core.autocrlf 设错会让整个文件显示为"全部修改"。仓库里放一个 .gitattributes 更可靠:

* text=auto eol=lf
*.png binary

5. 把密钥提交上去了

即使下一次提交删掉,密钥仍然留在历史里,git log -p 能翻出来。正确顺序是:先去作废并重新生成那个凭据,再考虑清理历史(filter-repo 之类的工具会重写全部哈希,需要所有人重新克隆)。历史清理不能替代作废。

6. 大文件进了历史

仓库体积只增不减,删掉文件也不会变小,因为对象还在。要么用 git gc --prune=now 配合历史重写,要么一开始就用 .gitignore 挡住构建产物和数据集。