Git 提交与 GitHub 推送:从四层模型到常见报错
Posted on Sat 14 November 2026 in 技术 • Sucre Liu
很多人用 Git 的方式是背命令:add、commit -m、push,三步走完就算完成。一旦遇到 rejected 或者冲突,就不知道从哪下手。
问题不在命令本身,而在于没有建立模型。这篇文章先把「文件在哪一层」讲清楚,再讲每条命令在移动什么,最后处理最常见的几类报错。
一、四层模型:理解 Git 的地基
Git 把代码的流转分成四个位置:
工作区 (Working Tree)
│ git add
▼
暂存区 (Index / Staging Area)
│ git commit
▼
本地仓库 (.git 目录)
│ git push
▼
远程仓库 (GitHub)
对应到日常操作:
| 层 | 是什么 | 怎么查看 |
|---|---|---|
| 工作区 | 你正在编辑的文件 | 直接打开文件 |
| 暂存区 | 下次提交要包含的快照 | git status / git diff --staged |
| 本地仓库 | 已经提交的历史 | git log |
| 远程仓库 | GitHub 上的副本 | GitHub 网页 / git log origin/main |
关键认知:commit 提交的是暂存区,不是工作区。这也是为什么改了三个文件、只 add 了两个,第三个不会进入这次提交。
三条命令分别在做什么
git add:把工作区的改动复制到暂存区。注意是复制,文件仍在工作区。git commit:把暂存区的内容打包成一个快照,写进本地仓库,并生成一个 commit id。git push:把本地仓库里多出来的提交同步到远程仓库。
前两步完全离线,只有 push 需要网络。理解这一点,就不会把「提交失败」和「推送失败」混为一谈。
二、从零开始:一次完整的提交流程
2.1 配置身份
第一次使用先配置,否则 commit 会报「Please tell me who you are」:
git config --global user.name "Sucre Liu"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
检查配置:
git config --global --list
--global 是全局配置,写在 ~/.gitconfig。单个仓库需要不同身份时,进目录后去掉 --global 再执行一次。
2.2 初始化仓库
mkdir demo && cd demo
git init
git init 只创建一个 .git 目录,不会碰你的文件。
2.3 查看状态
git status
git status -s
-s 是精简模式,输出里两个字符很关键:
M app.js # 工作区已修改,未暂存
M app.js # 已暂存,等待提交
MM app.js # 暂存后又改了一次
?? new.txt # 未跟踪文件
A added.js # 新文件已暂存
D old.js # 已暂存的删除
左边一列是暂存区状态,右边一列是工作区状态。看懂这两列,git status 就不会再是一堆看不懂的英文。
2.4 暂存与提交
git add app.js utils.js
git commit -m "feat: 新增订单详情页"
git log --oneline
到这一步,改动已经在本地仓库里了,但 GitHub 上还没有。
三、git add 的几个实用写法
| 命令 | 作用 |
|---|---|
git add <文件> |
暂存指定文件 |
git add <目录> |
暂存目录下所有改动 |
git add -A |
暂存全部改动(新增、修改、删除) |
git add -u |
只暂存已跟踪文件的修改和删除 |
git add -p |
交互式逐块选择,适合拆分提交 |
git add -n |
干跑,只显示会加什么,不实际执行 |
3.1 add -A 与 add . 的区别
老版本 Git 里 git add . 不会处理删除,-A 会。现在两者行为基本一致(都在当前目录及子目录范围),但 -A 的语义更明确:全部改动。在仓库根目录执行时推荐 -A。
3.2 用 add -p 拆出干净的提交
改了两个不相干的功能,却想分成两个 commit:
git add -p
Git 会逐块询问:y 暂存、n 跳过、s 拆分更小的块、e 手工编辑。这是做「一个提交只做一件事」最实用的工具。
3.3 .gitignore 的正确用法
新建 .gitignore,写入不需要版本控制的路径:
node_modules/
dist/
*.log
.env
.DS_Store
__pycache__/
.venv/
三个注意点:
.gitignore只对未跟踪文件生效。已经提交过的文件,加进去也不会自动消失。- 让已跟踪文件脱离版本控制:
bash git rm --cached config.local.js git commit -m "chore: 移除本地配置" - 空目录不会被跟踪,需要放一个
.gitkeep。
四、git commit:把快照写进历史
4.1 基本用法
git commit -m "fix: 修复登录态过期未跳转"
不写 -m 会打开编辑器,适合写多行提交信息。
4.2 提交信息怎么写
社区最通用的是 Conventional Commits:
<type>(<scope>): <subject>
<body>
<footer>
常用 type:
| type | 含义 |
|---|---|
| feat | 新功能 |
| fix | 修复缺陷 |
| docs | 文档 |
| style | 格式调整,不影响逻辑 |
| refactor | 重构 |
| perf | 性能优化 |
| test | 测试相关 |
| build | 构建系统或依赖 |
| ci | CI 配置 |
| chore | 杂项 |
几条经验:
- 首行控制在 50 字符内,空一行再写正文。
- 首行用祈使句、说清「做了什么」,不要写「update」这种无信息量的词。
- 正文解释为什么改,而不是重复改了什么——改了什么看 diff 就知道。
- 关联 issue 写在 footer:
Closes #42。
好的例子:
fix(order): 修正优惠券金额为 0 时的除零错误
结算页在优惠券面额为 0 时会触发除零,导致白屏。
改为先判断面额再计算折扣,并补充单元测试。
Closes #128
差的例子:update、fix bug、改了一下。
4.3 提交错了怎么办
| 场景 | 命令 | 说明 |
|---|---|---|
| 漏了文件,想并入上一次提交 | git add 漏掉的文件 + git commit --amend --no-edit |
会改写 commit id |
| 想改提交信息 | git commit --amend |
同上,只改信息 |
| 想撤销提交但保留改动 | git reset --soft HEAD~1 |
改动回到暂存区 |
| 想撤销提交且取消暂存 | git reset HEAD~1 |
改动回到工作区 |
| 想丢弃提交和改动 | git reset --hard HEAD~1 |
危险,改动不可恢复 |
已经 push 的提交不要 amend 或 reset,除非你确认只有自己在用这个分支。改写历史会让别人的本地分支和远程分叉。
--no-verify 可以跳过 pre-commit 钩子,只在确认钩子误报时用,不要养成习惯。
五、连接 GitHub
5.1 创建远程仓库
在 GitHub 上新建仓库,不要勾选初始化 README(本地已有代码时会产生无关的历史分叉)。拿到地址后:
git remote add origin git@github.com:username/demo.git
git remote -v
origin 只是远程仓库的别名,可以叫别的名字,但社区惯例是 origin。
5.2 分支名统一为 main
GitHub 默认分支早已从 master 改为 main。本地若是 master:
git branch -M main
5.3 认证方式
GitHub 从 2021 年 8 月起不再支持账号密码推送,两种方式:
方式一:HTTPS + Personal Access Token
在 GitHub「Settings → Developer settings → Personal access tokens」生成 token,推送时用户名填 GitHub 用户名,密码位置粘贴 token。token 只显示一次,注意保存。
方式二:SSH 密钥(推荐)
ssh-keygen -t ed25519 -C "you@example.com"
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
把公钥内容贴到 GitHub「Settings → SSH and GPG keys」,然后验证:
ssh -T git@github.com
看到 Hi username! You've successfully authenticated 即成功。
六、git push:把本地提交送上去
6.1 首次推送
git push -u origin main
-u 即 --set-upstream,把本地 main 和 origin/main 关联起来。设置之后,后续直接 git push 即可,不必再写远程名和分支名。
查看关联关系:
git branch -vv
6.2 日常推送
git push # 推当前分支到已关联的远程分支
git push origin dev # 推本地 dev 到远程 dev
git push origin --delete dev # 删除远程分支
git push --tags # 推送标签
6.3 关于 --force
--force 会覆盖远程历史,可能让别人已推送的提交消失。需要覆盖时优先用:
git push --force-with-lease
它会在远程分支与你上次拉取时一致的情况下才允许覆盖,相当于加了一道保险。
真正安全的顺序永远是:先 git fetch,看清差异,再决定是 merge、rebase 还是 force。
七、push 失败的常见原因
7.1 rejected: non-fast-forward
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs
含义:远程有你本地没有的提交,直接推会覆盖对方。
处理:
git pull --rebase origin main
# 解决冲突后
git rebase --continue
git push
用 --rebase 而不是默认 merge,提交历史会是一条直线,便于阅读。团队统一约定即可。
7.2 fatal: refusing to merge unrelated histories
本地和远程各自 init 过,历史没有共同祖先。确认远程内容可以合并后:
git pull origin main --allow-unrelated-histories
7.3 Permission denied (publickey)
SSH 密钥没配好,或 agent 里没有加载:
ssh -T git@github.com
ssh-add ~/.ssh/id_ed25519
仍失败就检查 ~/.ssh/config 是否指定了错误的 IdentityFile。
7.4 Authentication failed(HTTPS)
用密码代替了 token,或 token 过期/权限不足。重新生成带 repo 权限的 token,并在钥匙串里清除旧凭据:
# macOS
git credential-osxkeychain erase
# 或直接改用 SSH
git remote set-url origin git@github.com:username/demo.git
7.5 推送卡住或 443 超时
多为网络问题。检查代理:
git config --global --get http.proxy
git config --global --get https.proxy
需要时设置,不需要时清除:
git config --global http.proxy http://127.0.0.1:7890
git config --global --unset http.proxy
SSH 走 22 端口被墙时,可改用 GitHub 的 443 端口:
Host github.com
HostName ssh.github.com
Port 443
User git
7.6 文件过大被拒绝
GitHub 单文件上限 100MB,超过会直接拒绝推送。用 Git LFS 管理大文件:
git lfs install
git lfs track "*.psd"
git add .gitattributes
已经提交进历史的大文件,需要 git filter-repo 重写历史,操作前务必备份。
八、团队协作约定
8.1 分支模型
最轻量的做法:
| 分支 | 用途 |
|---|---|
| main | 随时可发布 |
| dev | 集成分支 |
| feature/xxx | 功能开发 |
| fix/xxx | 缺陷修复 |
| hotfix/xxx | 线上紧急修复 |
分支命名用小写加连字符,带上 issue 号更好:feature/128-order-detail。
8.2 提交粒度
一个提交只做一件事。判断标准:能否用一句话描述,并且回滚它不会牵连其他功能。
提交太碎会让历史噪音过多,提交太大则失去回滚价值。功能开发中按「可运行的阶段」提交是较好的平衡。
8.3 Pull Request 流程
git switch -c feature/128-order-detail
# 开发、提交
git push -u origin feature/128-order-detail
然后在 GitHub 发起 PR,走 review、CI,通过后合并。合并方式:
- Merge commit:保留完整分支历史,适合长期分支。
- Squash and merge:把多个提交压成一个,主干历史干净,适合功能分支。
- Rebase and merge:线性历史,但不保留合并节点。
小团队建议功能分支用 Squash,主干保持「一个 PR 一个提交」的清晰度。
8.4 推送前自检
git status # 有没有漏加的文件
git diff --staged # 暂存的内容是否正确
git log --oneline -5
git fetch && git status # 是否落后于远程
这四步花不了一分钟,能避免绝大多数「推上去才发现推错分支」的事故。
九、命令速查
# 配置
git config --global user.name "名字"
git config --global user.email "邮箱"
# 日常
git status
git diff
git add -A
git add -p
git commit -m "feat: xxx"
git log --oneline --graph --decorate
# 远程
git remote -v
git remote add origin <url>
git push -u origin main
git push
git fetch
git pull --rebase
# 撤销
git restore <file> # 丢弃工作区改动
git restore --staged <file> # 取消暂存
git commit --amend # 修改最后一次提交
git reset --soft HEAD~1 # 撤销提交保留改动
git revert <commit> # 生成反向提交,安全
# 分支
git switch -c feature/x
git switch main
git branch -d feature/x
小结
把 Git 想成一条流水线:add 决定「这次提交包含什么」,commit 决定「历史长什么样」,push 决定「什么时候让别人看到」。
大多数困惑来自跳过了前两步的确认:git status 看状态、git diff --staged 看内容、git log 看历史。命令本身不难,难的是知道此刻文件停在哪一层。
真正需要记住的只有一句:commit 提交的是暂存区。理解这一点,剩下的都是它的推论。
参考:Git 官方文档(git-scm.com)与 GitHub Docs。