Git 工作流与速查手册:日常开发实用参考
Git 工作流与速查手册:日常开发实用参考
Git 有上百个子命令,但日常开发其实只靠其中十五个左右。这篇文章讲的是我真正在用的工作流——再附一份值得放在手边的命令速查表。不讲内部原理,不讲理论,只讲你真正要敲的那些键。
心智模型
在 Git 里,一处改动可以待在三个地方,几乎每个命令都是在这三者之间搬运工作成果:
工作区 → 暂存区(Index) → 版本库(提交)
(你在这里改文件) (git add) (git commit)
还有第四个地方——远程仓库,它是版本库在服务器上的一份副本。push 把提交推上去,fetch 把提交拉下来。把这张图记在脑子里,Git 的大部分神秘感就消失了。
一次性配置
git config --global user.name "Charles Tang"
git config --global user.email "you@example.com"
# 新仓库默认使用 main 作为分支名
git config --global init.defaultBranch main
# 更好的差异显示和一个顺手的默认编辑器
git config --global core.editor "code --wait"
git config --global pull.rebase true # pull 时用 rebase 而非 merge
# 查看当前配置
git config --list设置 pull.rebase true 是一个有倾向性的选择,但值得:它能让历史保持线性,而不是被一堆 “Merge branch ‘main’” 的噪音塞满。
每天的循环
九成的日子都长这样:
git status # 改了什么?
git pull # 拉取最新
git checkout -b feat/login # 为这次工作开分支
# ……改文件……
git add -p # 交互式地按代码块暂存
git commit -m "Add login form validation"
git push -u origin feat/logingit add -p 被严重低估了。它会带你逐个代码块地过一遍改动,让你提交的恰好是你想提交的内容——而不是那行你忘了删的 console.log。
暂存与提交
git add file.js # 暂存单个文件
git add . # 暂存当前目录下的所有改动
git add -p # 交互式暂存,逐块进行
git restore --staged file.js # 取消暂存(保留改动)
git commit -m "Message" # 提交暂存区的改动
git commit -am "Message" # 暂存已跟踪文件 + 提交,一步到位
git commit --amend # 重写上一次提交
git commit --amend --no-edit # 把改动追加到上次提交,保留原消息--amend 是「糟糕,漏了个文件」时的好帮手——但只在 push 之前用。修改已发布的历史会逼着其他人一起处理这次重写。
写一条好的提交消息
Add rate limiting to login endpoint
Brute-force attempts were unthrottled. Cap at 5 tries
per IP per minute, returning 429 after that.
标题用祈使句(“Add”,不是 “Added”),控制在 50 个字符以内,然后空一行,再写为什么。做了什么,diff 已经说清楚了。
分支
git branch # 列出本地分支
git branch -a # 包含远程分支
git checkout -b feat/x # 创建并切换
git switch feat/x # 切换(更现代、更清晰)
git switch -c feat/x # 创建并切换(现代写法)
git branch -d feat/x # 删除(安全——未合并会拒绝)
git branch -D feat/x # 删除(强制)
git branch -m old new # 重命名switch 和 restore 是把功能过载的 checkout 拆开后的现代命令。switch 切分支,restore 改文件。用它们——更不容易误操作。
查看历史
git log --oneline --graph --all # 我一直开着的视图
git log -p file.js # 单个文件的历史,带 diff
git log --since="2 weeks ago"
git show <commit> # 某次提交的完整细节
git diff # 工作区 vs 暂存区
git diff --staged # 暂存区 vs 上次提交
git diff main..feat/x # 两个分支之间的差异
git blame file.js # 每一行最后是谁改的给上面第一条命令配个别名吧——你会反复敲它:
git config --global alias.lg "log --oneline --graph --all --decorate"与远程同步
git fetch # 下载远程改动,但不应用
git pull # fetch + 整合进当前分支
git push # 上传提交
git push -u origin feat/x # 推送并设置上游(第一次)
git push --force-with-lease # rebase 之后的安全强推永远不要用裸的 git push --force。 改用 --force-with-lease:如果别人推了你还没看到的提交,它会拒绝推送,这样你就不会悄无声息地覆盖掉队友的工作。
撤销操作
这是人人都怕的部分。其实大多数操作都是可逆的。
# 丢弃某个文件未提交的改动
git restore file.js
# 取消暂存某个文件(保留改动)
git restore --staged file.js
# 撤销上一次提交,改动保留在暂存区
git reset --soft HEAD~1
# 撤销上一次提交,改动退回工作区(未暂存)
git reset HEAD~1
# 抹掉上一次提交连同它的改动——破坏性操作
git reset --hard HEAD~1
# 通过新建一个反向提交来撤销(在共享历史上是安全的)
git revert <commit>经验法则:reset 重写历史,revert 追加历史。 在共享分支上,永远用 revert。只有 reset --hard 会真正丢掉成果——对它保持敬畏。
安全网:reflog
即便执行了 reset --hard,你的提交在大约 30 天内也不会真的消失。reflog 会列出 HEAD 到过的每一个位置:
git reflog
# 挑出犯错之前那个位置的哈希
git reset --hard HEAD@{2}这东西救我的次数多到我都不好意思承认。如果你觉得自己弄丢了一个提交,先查 reflog,别急着慌。
储藏(Stash)
适用于手头工作只做了一半、却需要切换上下文的时候:
git stash # 把改动收起来,工作区变干净
git stash -u # 连未跟踪文件一起收
git stash list # 查看所有储藏
git stash pop # 重新应用最近一条,并从栈中移除
git stash apply # 重新应用,但保留在栈中
git stash drop # 删除某条储藏Stash 是一个临时抽屉,不是档案柜。如果某样东西在 stash 里待了超过一天,它就该是一个分支。
合并 vs 变基
把一个分支更新到与 main 同步,有两种方式:
# 合并——原样保留历史,多一个合并提交
git switch feat/x
git merge main
# 变基——把你的提交重放到 main 之上,历史保持线性
git switch feat/x
git rebase main合并忠实记录了真实发生的事,代价是图谱会变得错综复杂。变基给你一条干净的直线,代价是重写了你的提交哈希。
我的原则:对自己的特性分支变基,让它在评审前保持整洁;绝不对别人已经拉取过的东西变基。重写共享历史,是毁掉一个周五的标准做法。
评审前整理提交
交互式变基让你能压缩、重排、改写提交:
git rebase -i HEAD~4你会进入一个编辑器,里面是你最近 4 次提交:
pick a1b2c3d Add login form
squash e4f5g6h Fix typo
squash h7i8j9k Fix another typo
reword l0m1n2o Wire up validation
squash 把一个提交并入上面那个;reword 让你修改提交消息。最终交付一个干净的提交,而不是五个乱糟糟的。
解决冲突
冲突不是错误——是 Git 在请你做一个它替你做不了的决定:
<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feat/x
把文件改成你想要的版本,删掉 <<<、===、>>> 这些标记,然后:
git add file.js # 标记这处冲突已解决
git rebase --continue # 或者:git merge --continue
git rebase --abort # 放弃,回到起点拿不准的时候,--abort 永远是安全的。它会把一切恢复原状。
团队特性分支工作流
我在真实团队里遵循的流程:
# 1. 从 main 全新开始
git switch main
git pull
# 2. 为这次工作开分支
git switch -c feat/user-export
# 3. 以小提交推进工作
git add -p && git commit -m "Add CSV export service"
# 4. 推送并发起 PR
git push -u origin feat/user-export
# 5. 工作期间保持与 main 同步
git fetch origin
git rebase origin/main
# 6. 评审前整理历史
git rebase -i origin/main
# 7. PR 合并后清理
git switch main
git pull
git branch -d feat/user-export短命的分支、小颗粒的提交、用变基保持同步、合并前压缩。分支活得越久,冲突越糟糕——让它们以「天」计,而不是以「周」计。
常见的坑
提交到了错误的分支。 别慌。git reset --soft HEAD~1 取消提交,git stash,切到正确的分支,git stash pop,再提交一次。
把密钥推上去了。 在后面的提交里删掉是不够的——它还在历史里。你需要 git filter-repo(或 BFG)把它彻底擦掉、强推一次,而且无论如何都该轮换这个密钥。就当它已经泄露了。
detached HEAD(游离 HEAD)。 你检出的是一个提交而不是分支。如果只是看看,无害。如果你在那里做了提交,先 git switch -c rescue-branch 把它们保住,再切走。
pull 老是产生合并提交。 你没有设置 pull.rebase true。设上它,或者用 git pull --rebase。
不小心提交了大文件。 一旦进了历史,它会让之后每一次克隆都永久变臃肿。要么趁早用 reset 拦下,要么用 filter-repo 擦掉。更好的做法:从第一个提交起就有一份正确的 .gitignore。
.gitignore 必备项
# 依赖
node_modules/
# 构建产物
dist/
public/
# 环境变量与密钥
.env
.env.local
# 编辑器与系统垃圾文件
.vscode/
.DS_Store
在第一个提交里就把它加上。事后取消跟踪(git rm --cached)也行,但文件仍然会留在历史里。
速查表(浓缩版)
| 任务 | 命令 |
|---|---|
| 查看状态 | git status |
| 交互式暂存 | git add -p |
| 提交 | git commit -m "..." |
| 修补上次提交 | git commit --amend |
| 新建分支 | git switch -c feat/x |
| 切换分支 | git switch main |
| 从远程更新 | git pull |
| 首次推送 | git push -u origin feat/x |
| 安全强推 | git push --force-with-lease |
| 漂亮的历史图 | git log --oneline --graph --all |
| 丢弃文件改动 | git restore file.js |
| 取消暂存 | git restore --staged file.js |
| 撤销提交、保留成果 | git reset --soft HEAD~1 |
| 撤销共享分支上的提交 | git revert <commit> |
| 临时储藏 | git stash / git stash pop |
| 找回丢失的提交 | git reflog |
| 变基到 main | git rebase main |
| 整理提交 | git rebase -i HEAD~N |
写在最后
比起对 Git 底层管道的深入了解,它更回报一小撮稳定一致的习惯。频繁提交、颗粒小一点。任何工作都开分支。动手前先 pull。写未来的自己读得懂的提交消息。还要记得 reflog 的存在——Git 里几乎没有什么是真正丢得掉的,这也意味着几乎没有哪个错误值得你慌张。