Git
https://learngitbranching.js.org/?locale=zh_CN

Git的三个区域
Git 的文件有三种状态区域:
| 区域 | 命令 | 说明 |
|---|---|---|
| 工作区 (Working Directory) | 你编辑的实际文件 | 改动还没 add |
| 暂存区 (Staging Area/index) | 用 git add 后进入这里 | 等待被 commit |
| 本地仓库 (Repository) | 用 git commit 后进入这里 | 已经记录在历史中 |
对应命令区别:
| 目标 | 用法 | 说明 |
|---|---|---|
❌ 撤销刚才的 git add(把文件从暂存区移回工作区) | git restore --staged <file> | ✅ 把文件移出暂存区,但保留修改内容 |
| ❌ 丢弃未暂存修改,恢复为暂存区版本 | git restore <file> | ⚠️ 彻底丢失工作区改动 |
| ❌ 既撤回暂存区,又丢弃修改 | git restore --staged <file> + git restore <file> | 等价于完全回到上一次提交的版本 |
流程
初始化
- 在本地初始化仓库并提交
git init
- 将所有当前文件添加到暂存区
git add .
- 提交到本地仓库
git commit -m "Initial commit"
- 重命名本地默认分支为
main(可选,但现在比较常见)
git branch -M main
关联本地仓库与远程仓库
添加远程仓库地址
git remote add origin <远程仓库URL>
或
git remote add origin git@192.168.10.28:pilinghui/test_project.git
推送本地分支到远程
git push -u origin main
-u选项会把本地的main与远程origin/main关联起来,之后推送可以直接用git push。- 在推送之前先拉取 git pull origin main
拉取远程仓库并允许合并无关历史
- git pull origin main –allow-unrelated-histories
代码的合并
下面是一次完整的真实流程(2026-08-12,eavs_mc6830 修 websocket recv 超时误判): 从 main 开 fix 分支 → 改代码 → 分批提交 → 切回 main 合并 → 推送 → 删分支。 分支该从哪个基点开,见下文 一句话黄金法则 的场景 A/B/C。
0. 动手前先看清楚现状
git status --porcelain # 工作区/暂存区都脏了什么
git branch -vv # 当前在哪个分支, 跟踪的是哪个远端分支
git log --oneline -5 # 最近提交, 顺便学仓库的 commit message 风格
关键:先把脏文件分成两类 —— 「这次要改的」和「别人/以前留下的 WIP」。
后面提交时只提交前者,绝不能 git add -A / git commit -a 一把梭。
1. 从 main 开分支
git checkout main
git pull # 确保基点是最新的
git checkout -b fix/ws-recv-timeout-misjudge
- 命名习惯:
fix/xxx(缺陷修复)、feature/xxx(新功能)、hotfix/xxx(线上紧急)。 - 在默认分支(main/master)上直接改代码是坏习惯:出问题不好回退,也挡不住别人的推送。
2. 在分支上改代码、分批提交
改完后按路径提交,不要提交无关文件:
git diff --stat # 先看改了哪些文件、增删多少行
git diff <file> # 逐个文件确认
git commit -m "fix(xxx): 一句话说明" -- <file1> <file2>
git commit -- <路径> 的两个坑:
| 坑 | 说明 |
|---|---|
| 路径是相对「当前目录」的,不是仓库根 | 若仓库根是 ~/proj,你人在 ~/proj/sub,git status 显示 sub/a.cpp,但提交要写 git commit -- a.cpp。写成 sub/a.cpp 会报 error: pathspec 'sub/a.cpp' did not match any file(s) known to git |
| 它会绕过暂存区 | 只提交你点名的路径的工作区内容;暂存区里别的已 git add 的东西不会被带进这次提交。这正是我们要的效果 —— 工作区里那 14 个 git rm --cached 的暂存删除原封不动留着 |
对比一下三种提交方式:
| 写法 | 提交什么 | 什么时候用 |
|---|---|---|
git commit -m "..." | 暂存区里的全部内容 | 你确定暂存区干净、只有本次改动 |
git commit -a -m "..." | 所有已跟踪文件的改动(不含新文件) | ⚠️ 工作区有别的 WIP 时会误伤 |
git commit -m "..." -- <路径> | 只有点名的路径 | ✅ 工作区混杂时最安全 |
提交完立刻复核:
git show --stat HEAD # 这次提交到底含了哪些文件
git status --porcelain | wc -l # 剩下的脏文件数量应该和预期一致
3. 合并回 main —— 先判断能不能 fast-forward
切回去之前,先搞清楚两个分支的拓扑关系:
git rev-parse main fix/ws-recv-timeout-misjudge # 两个分支各自指向哪个 commit
git merge-base main fix/ws-recv-timeout-misjudge # 共同祖先
git rev-list --left-right --count origin/main...main # 落后/领先各几个提交
判据:如果 merge-base == main 当前的 commit,说明 main 是分支的严格祖先、没有分叉,可以 fast-forward,零冲突风险。
git checkout main
git merge --ff-only fix/ws-recv-timeout-misjudge
--ff-only 是保险丝:能快进就快进,不能快进就直接报错退出,
绝不会偷偷给你造一个 merge commit、也不会把你拖进冲突解决。
–ff-only / –no-ff / 默认 三者怎么选:
| 方式 | 结果 | 适用 |
|---|---|---|
git merge --ff-only <br> | 只快进,历史保持线性;不能快进就报错 | ✅ 提交少、分支没分叉、想要干净线性历史 |
git merge --no-ff <br> | 强制产生一个 merge commit | 想在历史上保留「这几个提交是一个整体」的痕迹;长期 feature 分支 |
git merge <br>(默认) | 能快进就快进,否则造 merge commit | 不确定时容易得到意料之外的历史 |
选哪个看仓库习惯:
git log --oneline --graph -20看看历史是线性的还是一堆菱形。
4. 合并后核对:别人的 WIP 有没有被动过
合并会改工作区文件,所以合并前存一份快照,合并后 diff 一下:
git status --porcelain > /tmp/pre_merge.txt
git merge --ff-only fix/ws-recv-timeout-misjudge
git status --porcelain > /tmp/post_merge.txt
diff /tmp/pre_merge.txt /tmp/post_merge.txt && echo "工作区未受影响"
再确认改动确实落地了:
git log --oneline --decorate -3 # main 和分支应该指向同一个 commit
grep -c "你新加的关键字" <改过的文件>
5. 推送
git push origin main
输出形如 63cda5f..3a5c41b main -> main,左边是推之前远端的位置、右边是现在的。
- 推之前如果不确定远端有没有被别人推过:
git fetch origin main再git rev-list --left-right --count origin/main...main。 - 第一次推某个分支要建立跟踪关系用
-u:git push -u origin <branch>。
6. 删除已合并的分支
git branch -d fix/ws-recv-timeout-misjudge # 本地
git push origin --delete fix/ws-recv-timeout-misjudge # 远端(如果推过)
-d是安全删除:分支上若有未合并的提交,Git 会报错拦住你。-D是强制删除(--delete --force),会丢提交,确认不要了才用。
一页速查
# 1) 开分支
git checkout main && git pull
git checkout -b fix/xxx
# 2) 改完, 按路径提交(工作区有别的 WIP 时必须这样)
git diff --stat
git commit -m "fix(mod): 说明" -- path/to/file.cpp
git show --stat HEAD # 复核只含该文件
# 3) 判断能否快进
git merge-base main fix/xxx # == main 的 commit 即可快进
# 4) 合并
git checkout main
git merge --ff-only fix/xxx
# 5) 推送
git push origin main
# 6) 删分支
git branch -d fix/xxx
容易踩的几点
| 现象 | 原因 | 处理 |
|---|---|---|
error: pathspec 'a/b.cpp' did not match any file(s) known to git | pathspec 相对当前目录解析,而 git status 输出的是仓库根相对路径 | cd 到仓库根,或把路径改成相对当前目录的写法 |
git merge 意外产生了 merge commit | 用了默认行为,而分支已经分叉 | 想要线性历史就固定用 --ff-only,报错了再决定是 rebase 还是接受 merge |
fatal: Not possible to fast-forward, aborting. | main 上有分支里没有的提交(分叉了) | 要么 git rebase main 把分支挪到 main 顶上,要么老实用 --no-ff 合并 |
| 合并后发现把别人的改动一起提交了 | 用了 git add -A / git commit -a | 提交前 git status --porcelain 分类,提交后 git show --stat HEAD 复核 |
git branch -d 报错删不掉 | 分支上有未合并的提交 | 先确认这些提交要不要 —— 确实不要了才用 -D |
访问不了github
下面是曾经使用过的 hosts 配置记录,IP 可能已经变化,不应作为固定解决方案照抄。应先区分 DNS、代理和网络连通性问题:
- sudo vi /etc/hosts
- 在hosts文件末尾的ip地址如下:
#github
140.82.114.4 github.com
151.101.1.6 github.global.ssl.fastly.net
151.101.65.6 github.global.ssl.fastly.net
151.101.129.6 github.global.ssl.fastly.net
151.101.193.6 github.global.ssl.fastly.net
- 修改 /etc/hosts 通常不需要重启整个网络服务;用
getent hosts github.com检查系统解析结果。应用或 DNS 缓存仍使用旧结果时,再针对具体缓存处理。
git add
add 命令的作用就是将工作区的文件添加到暂存区
- -A 参数会监控工作区的状态树,它会把工作区的所有变化提交到暂存区,包括修改(modified)、新文件(Untracked files)、删除的文件(deleted)。git add . 只覆盖当前目录及其子目录;不带路径的 git add -A 覆盖整个工作区。仅在仓库根目录等相同作用域下两者效果相同
- -u
--update只更新已经被Git跟踪的文件,即只添加那些已经存在于版本库中且被修改或删除的文件,但不包括新创建的文件(未被Git跟踪的文件)
git branch
-M 参数是
--move --force的简写,它会强制将当前分支重命名为main,即使目标分支main已经存在。通常用于以下情况:你想将当前分支重命名为
main,而不是创建一个新的分支。你正在遵循社区中越来越普遍的做法,将默认分支从
master更改为main。
git branch -M main
如果你当前在 master 分支上,这将把 master 分支重命名为 main。
如果你在其他分支上,这将把该分支重命名为 main。
-r
作用: 显示你本地仓库知道的所有 远程分支 的列表
输出格式:
meian@ubuntu200406:~/work/MyProject/test_nvr/nvr_36chn$ git branch -r origin/HEAD -> origin/main origin/develop origin/mainorigin/HEAD -> origin/main: 这表示远程仓库
origin的 HEAD 指针指向origin/main分支。HEAD 指针通常表示远程仓库的默认分支(通常是main或master)。
-a
- 作用会列出所有的分支,包括本地分支和远程分支。
–contains
过滤器 查询到包含特定commitID的所有本地分支,加上–all 还会显示远程分支。
删除分支
- -d
git branch -d 新分支名- -d 要求分支已合并到其 upstream;没有 upstream 时检查是否已合并到当前 HEAD,并不是固定检查 main。
假设你在当前分支上运行以下命令:
一句话黄金法则
修复 bug 的分支永远基于出现 bug 的来源分支来决定。
也就是说:
- 如果 auth-server 还没合入 develop bug 修复必须在 feature/plh/auth-server 上进行
- 如果 auth-server 已经部分合入 develop bug 修复通常在 fix/plh/auth-server-xxx(新建一个 fix 分支)
- 如果 bug 出在主分支 main(线上环境) 必须从 main 开 hotfix 分支
⭐ 场景 A:auth-server 正在开发,还未完成 → bug 修复应该继续在 feature 分支做
你现在是这种情况(重构,分阶段开发)。
例如:
feature/plh/auth-server ← 你正在这里开发
突然发现一个 bug
那就继续在这个分支上修即可:
git checkout feature/plh/auth-server
# 修复代码
git commit -m "fix(auth): 修复认证流程死锁"
git push
不需要新建分支。这是最干净、最合理的方式。
⭐ 场景 B:auth-server 已经合入 develop,但你还要继续修复 → 创建 fix 分支(推荐)
此时你已经执行过:
git checkout develop
git merge feature/plh/auth-server
git push
并且 auth-server 已经部分进入 develop。 此时如果发现 bug:
🔧 必须基于 develop 创建 fix 分支:
git checkout develop
git pull
git checkout -b fix/plh/auth-server-deadlock
修复:
git add .
git commit -m "fix(auth): 修复死锁问题"
git push
修复完成后:
git checkout develop
git merge fix/plh/auth-server-deadlock
git push
最后可删除 fix 分支:
git branch -d fix/plh/auth-server-deadlock
git push origin --delete fix/plh/auth-server-deadlock
⭐ 场景 C:auth-server 已经合入 main(正式发布版本)→ bug 必须 hotfix
假设你已经发版:
v1.0.0(包含 auth-server)
用户反馈 bug 了。
🚑 必须从 main 开热修复分支:
git checkout main
git pull
git checkout -b hotfix/plh/auth-server-token-bug
修复完后必须合两个地方:
1)合回 main(用于发 v1.0.1)
git checkout main
git merge hotfix/plh/auth-server-token-bug
git push
2)合回 develop(保持一致)
git checkout develop
git merge hotfix/plh/auth-server-token-bug
git push
最后删除 hotfix 分支。
git checkout
切换到现有的分支
git checkout <branch-name>
创建并切换到新分支
git checkout -b <new-branch-name>
从指定的 commit 中取出某个文件或目录,覆盖到当前分支的工作区和暂存区
git checkout <commit> -- <文件路径>
- 只想恢复某个/某几个文件到某版本 → git checkout
– - 想切换整个工作区到某分支 → git checkout
- 只是想临时看看某个历史 commit 整体长什么样、不打算改动 → 才用 git checkout
,并且事后记得 git checkout main 切回来
git cherry-pick
对于多分支的代码库,将代码从一个分支转移到另一个分支是常见需求。 这时分两种情况。一种情况是,你需要另一个分支的所有代码变动,那么就采用合并(git merge)。另一种情况是,你只需要部分代码变动(某几个提交),这时可以采用(cherry pick)。
git commit
提交空消息
git commit -m "" --allow-empty-message
git config
- 本地仓库配置 –local:仅对当前git仓库生效
- 全局用户配置 –global:当前操作系统下用户的所有git仓库都生效
当通过git status 看到一串令人困惑的八进制转义字符,例如:"\345\237\272\346\234\254\345\212\237",只要字节值超过0x80的字符都被视为unusual字符
关闭路径转义
git config --global core.quotepath false
设置 HTTP/HTTPS 代理
git config --global http.proxy http://127.0.0.1:7897
# http.proxy 同时用于 Git 的 HTTP 与 HTTPS 传输;https.proxy 不是 Git 对应的标准配置项
git clone
浅拷贝
git clone --depth=1 <rep_url>
--shallow-submodules= 子模块也用浅克隆
--recurse-submodules
- 读取
.gitmodules - 自动 clone 每个 submodule
- checkout 到指定 commit
git diff
在git pull 完成后,Git 会在 reflog 里留下一个记录点
HEAD@{1}= 执行pull之前的 HEADHEAD= 执行pull之后的 HEAD
查看单个文件的改动
- git diff HEAD@{1} HEAD – path/to/your/file.cpp
前提:你刚刚执行的就是一次普通 git pull(没有用 --rebase),且还没再提交或切换分支,否则 HEAD@{1} 可能不是刚才的 pull 点。
git fetch
git fetch [<remote>] [<branch>]
<remote>:远程仓库的名称,默认为origin。<branch>:指定要获取的远程分支;省略时按该 remote 的 fetch refspec 获取,常见配置覆盖所有分支,但 single-branch 等配置可能只覆盖部分分支。
常见场景与实际例子:
获取远程仓库的所有更新:
git fetch origin
这条命令按 origin 的 fetch refspec 更新相应远程跟踪引用;默认只自动跟随与所获取历史相关的标签,需要所有标签时另用 –tags。注意,这并不会修改当前的工作分支或合并任何更改。
获取特定分支的更新:
git fetch origin feature-branch
这条命令只会从远程仓库 origin 获取 feature-branch 分支的更新信息,不会获取其他分支的内容。
参数:
–all
- 获取所有远程仓库的更新信息,而不仅仅是默认的远程仓库(
origin)。
- 获取所有远程仓库的更新信息,而不仅仅是默认的远程仓库(
–depth=
- 通过指定深度来获取一个浅拷贝。此选项会限制获取的历史记录的深度,适用于只关心最近提交的情况。
-p/--prune清理远端已删除分支对应的本地远程跟踪引用。当远程仓库删除了某个分支,而你本地的远程追踪分支仍然存在时,使用此选项可以同步删除本地不再存在的远程分支。
--tags- 获取远程仓库的所有标签信息。
git log
git log用于查询版本的历史,命令形式如下:
git log [<options>] [<since>..<until>] [[--] <path>...]
–oneline:每行显示缩写后的提交 ID 和提交主题;缩写长度会为消除歧义而增加,不保证固定 7 位。
–graph:在左侧以文本分支图的形式展示分支合并历史
–stat:是在git log 的基础上输出文件增删改的统计数据。
git merge
将两个分支的更改合并到一个分支中
git merge <branch-name>
- < branch-name > :指定要合并的目标分支。当前分支会与该目标分支合并。
命令的常用选项及参数:
–no-ff 选项:强制执行合并提交
用途:即使目标分支可以快进合并,使用
--no-ff选项仍然会生成一个新的合并提交。这个选项有助于保留合并的历史记录。如果目标提交已经是当前 HEAD 的祖先(Already up to date),即使指定 –no-ff,也不会凭空创建合并提交。
git push
提交到远程仓库,本地的代码已经add ,commit了
关联远程仓库
git remote add origin https://github.com/你的用户名/muduo.gitgit push [<remote>] <branch><remote>:远程仓库的名称,默认通常为origin,表示你与远程仓库的连接。<branch>:本地分支的名称,表示将当前分支推送到远程的哪个分支。-u 等同 –set-upstream
把当前本地分支与刚推上的远端分支建立跟踪关系(upstream)。这样以后在该分支上可以直接用
git pull/git push不带任何参数,它会默认对应到那个远端分支;git status也会显示 ahead/behind。
如果执行git push -u origin main失败:
error: 源引用规格 main 没有匹配 error: 无法推送一些引用到 ‘origin’
git branch -m 命令用于重命名 Git 分支
重命名当前所在的分支
- 如果你在某个分支上,并且希望将其名称更改为新的名字,只需执行:git branch -m 新的分支名称
重命名指定的分支
- git branch -m 旧的分支名称 新的分支名称
error:
fatal: 拒绝合并无关的历史
通常会出现这种情况的原因有
本地仓库与远程仓库独立初始化: 你可能在本地创建了一个仓库并做了一些提交,而远程仓库中也有独立的提交记录,两者没有共同的起始点。
仓库历史被重写或重新初始化: 如果远程仓库经历了历史重写(例如重写了根提交,或删除原 .git 后重新建库;对已有仓库单独执行 git init 不会清空提交历史),就可能导致两边的历史不再匹配。
git pull
先 fetch,再根据命令参数和配置,以 merge、rebase 或仅快进方式整合到当前分支;发生分叉且未指定策略时也可能拒绝继续。
git pull <remote> <branch>
<remote>:远程仓库的名称,通常默认为origin。<branch>:远程分支的名称,指定要拉取并合并的远程分支。如果不指定分支,git pull会默认拉取当前跟踪的远程分支。
常见场景与实际例子:
拉取远程仓库的更新并合并到当前分支:
git pull origin master
这条命令会从远程仓库 origin 的 master 分支拉取更新,并将其自动合并到当前分支。
拉取远程分支并合并到当前分支:
如果你当前在 develop 分支上,且远程仓库中有 develop 分支的更新,执行以下命令会拉取并合并这些更新:
git pull origin develop
这条命令会从远程仓库 origin 的 develop 分支拉取更新,并将其合并到本地的 develop 分支。
只拉取更新而不合并
只下载远程提交、更新远程跟踪信息而不整合当前分支,使用 fetch:
git fetch origin master
fetch 不修改当前分支的工作区内容。git pull --no-commit 仍会尝试整合;–no-commit 只阻止产生合并提交前的自动提交,不能阻止快进更新。
丢弃本地状态并对齐远程分支
以下操作不是合并:确认本地已跟踪文件的修改和当前分支独有提交都可以放弃后,先 fetch,再把当前分支重置到目标远程跟踪分支。
git reset --hard <commit>
<commit>是你要重置到的目标提交,可以是:- 提交的哈希值(如
a1b2c3d4) - 分支名(如
origin/main或master) HEAD~1(上一提交)HEAD(当前提交)
- 提交的哈希值(如
git fetch origin
git reset --hard origin/main # main 应替换为实际要对齐的远程分支
遇到build目录里的文件没有跟踪,但这是是临时文件并且不需要保留,所以直接删除这些未跟踪的文件。
error: The following untracked working tree files would be overwritten by merge: build/CMakeFiles/CMakeDirectoryInformation.cmake build/CMakeFiles/Makefile.cmake build/compile_commands.json
仅处理示例中的 build/ 时,先用 git clean -nd -- build/ 预览,再确认是否执行 git clean -fd -- build/;不带路径限制会影响整个工作区的未跟踪文件。
-f是强制删除未跟踪的文件。-d是删除未跟踪的目录。
在项目的根目录下创建.gitignore,并添加内容:build/
git remote
git remote add
git stash
用于临时保存当前工作目录和暂存区的改动,使得工作区能够恢复到上一次提交的干净状态。这对于在开发过程中需要切换分支但不想提交当前修改的情况非常有用。你可以将工作进度“存放”起来,稍后再回来继续工作。
用途:
- 切换分支时保存当前工作:当你在当前分支上有未提交的更改,并且需要临时切换到其他分支处理问题时,可以使用
git stash来保存这些更改。 - 避免提交不完整的工作:当你正在开发一个功能,途中遇到需要解决的紧急问题时,可以暂时将当前工作保存在 stash 中,待解决完紧急问题后再回来继续。
- 多任务处理:在一个任务的中间,切换到另一个任务时,你可以使用
git stash临时存储工作,以便稍后恢复。
git stash [<options>]
使用场景与实际例子
查看当前所有的 stash 条目
git stash list
这会列出所有的 stash 条目,每个条目都用一个标识符(例如 stash@{0})进行区分。
应用最近保存的 stash
git stash apply
应用特定的 stash
git stash apply stash@{1}
当你有多个 stash 条目时,可以指定特定的 stash 来应用。stash@{1} 指的是第二个保存的 stash。
常用选项:
- -u 或 –include-untracked
- 除了包含已跟踪文件的改动外,还会保存未跟踪的文件。
- -a 或 –all
- 除了未跟踪的文件外,还会将忽略文件(
.gitignore文件中列出的文件)保存到 stash 中。
- 除了未跟踪的文件外,还会将忽略文件(
- –keep-index
- 保存工作目录的改动,但不保存暂存区的内容。此选项会将暂存区的更改保持不变,仅保存工作目录的改动。
git tag
git tag [选项] [标签名称] [提交哈希]
-a . 附注标签(Annotated Tag)
-m 指定附注标签的注释信息。
常用的.gitignore
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
build/
bin/
objects/
.vscode/
如果不小心把 build 目录的内容提交到了 Git 仓库中,你可以按照下面的步骤将其移除,并让 Git 后续忽略该目录:
添加忽略规则
在项目根目录下的
.gitignore文件中添加一行,指定忽略 build 目录(如果没有该文件则新建一个)从 Git 暂存区移除 build 目录
使用下面的命令将 build 目录及其内容从 Git 的版本控制中移除,但本地文件不会被删除
git rm -r --cached build
- 提交更改
提交此次移除操作
git commit -m "Remove build directory from version control"
- 推送到远程仓库
如果你有远程仓库,还需要推送更新
git push
参考:git restore;git pull。