<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Git on heyaohua's Blog</title><link>https://blog.heyaohua.com/tags/git/</link><description>Recent content in Git on heyaohua's Blog</description><image><title>heyaohua's Blog</title><url>https://blog.heyaohua.com/og-image.png</url><link>https://blog.heyaohua.com/og-image.png</link></image><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Sat, 22 Aug 2026 23:24:00 +0800</lastBuildDate><atom:link href="https://blog.heyaohua.com/tags/git/index.xml" rel="self" type="application/rss+xml"/><item><title>Git 使用手册 · 终极篇：工程化、安全与团队规范</title><link>https://blog.heyaohua.com/posts/2026/08/git-handbook-ultimate/</link><pubDate>Sat, 22 Aug 2026 23:24:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/08/git-handbook-ultimate/</guid><description>Git 终极手册，覆盖 monorepo 策略、签名提交与密钥扫描等安全实践、CI/CD 中的 Git 技巧、灾难恢复 SOP，以及一套可直接落地的团队 Git 规范。</description><content:encoded><![CDATA[<p>这是 Git 手册系列的最后一篇。前三篇解决了&quot;会用 → 用得对 → 懂原理&quot;，本篇解决最后一件事：<strong>让 Git 在团队和工程体系里成为可靠的基础设施</strong>，并在灾难发生时救得回来。</p>
<hr>
<h2 id="目录">目录</h2>
<ul>
<li>Monorepo vs Polyrepo：仓库拓扑决策</li>
<li>大型仓库的性能优化</li>
<li>提交规范：Conventional Commits</li>
<li>签名提交与提交者验证</li>
<li>密钥防护：从预防到泄漏处置</li>
<li>CI/CD 中的 Git 技巧</li>
<li>灾难恢复 SOP</li>
<li>一套可直接落地的团队 Git 规范</li>
<li>终极速查表</li>
</ul>
<hr>
<h2 id="monorepo-vs-polyrepo仓库拓扑决策">Monorepo vs Polyrepo：仓库拓扑决策</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Monorepo（一个仓库存所有项目）</th>
					<th>Polyrepo（每项目一个仓库）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>跨项目改动</td>
					<td>一次提交原子完成</td>
					<td>要发版、升依赖、多 PR 联动</td>
			</tr>
			<tr>
					<td>代码共享</td>
					<td>直接 import</td>
					<td>发包 / submodule</td>
			</tr>
			<tr>
					<td>CI</td>
					<td>需要增量构建，配置复杂</td>
					<td>简单直接</td>
			</tr>
			<tr>
					<td>权限边界</td>
					<td>靠 CODEOWNERS，粒度粗</td>
					<td>天然隔离</td>
			</tr>
			<tr>
					<td>克隆体积</td>
					<td>大（可用 sparse checkout 缓解）</td>
					<td>小</td>
			</tr>
			<tr>
					<td>代表</td>
					<td>Google、Meta、Vue、React 生态</td>
					<td>多数中小团队</td>
			</tr>
	</tbody>
</table>
<p><strong>决策建议</strong>：</p>
<ul>
<li>项目间强耦合、频繁联动发布（如前端组件库 + 应用）→ monorepo</li>
<li>团队/项目独立、发布节奏不同、需要强权限边界 → polyrepo</li>
<li>不确定就先 polyrepo；拆容易，合回去很难</li>
</ul>
<p>monorepo 配套工具：Nx、Turborepo、Bazel（构建缓存与增量）、changesets（多包版本管理）。</p>
<hr>
<h2 id="大型仓库的性能优化">大型仓库的性能优化</h2>
<p>仓库几万提交、几百 MB 之后，这些手段能救体验：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 浅克隆：只拿最近的历史（CI 必备）</span>
</span></span><span style="display:flex;"><span>git clone --depth <span style="color:#bd93f9">1</span> &lt;url&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 部分克隆：不下载全部 blob，按需懒加载（本地开发大仓库神器）</span>
</span></span><span style="display:flex;"><span>git clone --filter<span style="color:#ff79c6">=</span>blob:none &lt;url&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 稀疏检出：只 checkout 需要的目录</span>
</span></span><span style="display:flex;"><span>git sparse-checkout init --cone
</span></span><span style="display:flex;"><span>git sparse-checkout <span style="color:#8be9fd;font-style:italic">set</span> apps/web packages/ui
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 让 status / diff 在大仓库里更快</span>
</span></span><span style="display:flex;"><span>git config core.fsmonitor <span style="color:#8be9fd;font-style:italic">true</span>
</span></span><span style="display:flex;"><span>git config core.untrackedCache <span style="color:#8be9fd;font-style:italic">true</span>
</span></span><span style="display:flex;"><span>git maintenance start        <span style="color:#6272a4"># 后台自动做 gc、打包、提交图等维护</span>
</span></span></code></pre></div><p>提交图（commit-graph）单独提一句：它让 <code>git log --graph</code>、合并基计算在大仓库里快一个数量级，<code>git maintenance start</code> 会自动开启。</p>
<hr>
<h2 id="提交规范conventional-commits">提交规范：Conventional Commits</h2>
<p>格式：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;body&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;footer&gt;
</span></span></code></pre></div><p>常用 type：</p>
<table>
	<thead>
			<tr>
					<th>type</th>
					<th>含义</th>
					<th>影响版本</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>feat</code></td>
					<td>新功能</td>
					<td>次版本 +1</td>
			</tr>
			<tr>
					<td><code>fix</code></td>
					<td>修 bug</td>
					<td>修订号 +1</td>
			</tr>
			<tr>
					<td><code>perf</code></td>
					<td>性能优化</td>
					<td>修订号 +1</td>
			</tr>
			<tr>
					<td><code>refactor</code></td>
					<td>重构（不改行为）</td>
					<td>不升</td>
			</tr>
			<tr>
					<td><code>docs</code> / <code>style</code> / <code>test</code> / <code>chore</code> / <code>ci</code> / <code>build</code></td>
					<td>文档 / 格式 / 测试 / 杂务</td>
					<td>不升</td>
			</tr>
			<tr>
					<td>带 <code>!</code> 或 footer 写 <code>BREAKING CHANGE:</code></td>
					<td>破坏性变更</td>
					<td>主版本 +1</td>
			</tr>
	</tbody>
</table>
<p>示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>feat(auth): 支持手机号验证码登录
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- 新增 /auth/sms/send 与 /auth/sms/verify 接口
</span></span><span style="display:flex;"><span>- 验证码 5 分钟有效，限频 1 条/分钟
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Closes #128
</span></span></code></pre></div><p>为什么值得推行：<strong>版本号和 CHANGELOG 可以全自动生成</strong>。配套工具链：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># commitizen：交互式生成规范提交信息</span>
</span></span><span style="display:flex;"><span>pnpm add -D commitizen cz-conventional-changelog
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># commitlint + husky：不合规的提交直接拒绝</span>
</span></span><span style="display:flex;"><span>pnpm add -D @commitlint/cli @commitlint/config-conventional
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;npx commitlint --edit \$1&#34;</span> &gt; .husky/commit-msg
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># semantic-release / release-please：按提交记录自动升版本、发 CHANGELOG</span>
</span></span></code></pre></div><hr>
<h2 id="签名提交与提交者验证">签名提交与提交者验证</h2>
<p>Git 的 <code>user.name</code> / <code>user.email</code> 可以随便填——<code>git commit --author=&quot;Linus &lt;linus@kernel.org&gt;&quot;</code> 就能伪装任何人。要证明&quot;这个提交真的是我提交的&quot;，用签名：</p>
<h3 id="ssh-签名推荐零门槛复用现有密钥">SSH 签名（推荐，零门槛复用现有密钥）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git config --global gpg.format ssh
</span></span><span style="display:flex;"><span>git config --global user.signingkey ~/.ssh/id_ed25519.pub
</span></span><span style="display:flex;"><span>git config --global commit.gpgsign <span style="color:#8be9fd;font-style:italic">true</span>      <span style="color:#6272a4"># 之后每次提交自动签名</span>
</span></span></code></pre></div><p>GitHub 设置里把同一个公钥添加为 <strong>Signing Key</strong>，你的提交就会显示 <code>Verified</code> 徽章。</p>
<h3 id="验证别人的提交">验证别人的提交</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git log --show-signature         <span style="color:#6272a4"># 查看签名验证结果</span>
</span></span><span style="display:flex;"><span>git merge --verify-signatures main
</span></span></code></pre></div><h3 id="为什么这很重要">为什么这很重要</h3>
<ul>
<li>供应链安全：防止有人伪装核心维护者提交恶意代码</li>
<li>审计合规：金融、政企场景要求提交可追溯真人</li>
<li>配合分支保护规则&quot;要求签名提交&quot;，从平台侧强制</li>
</ul>
<hr>
<h2 id="密钥防护从预防到泄漏处置">密钥防护：从预防到泄漏处置</h2>
<p>密钥进仓库是最高频的安全事故。三层防御：</p>
<h3 id="第一层本地预防">第一层：本地预防</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># gitleaks：提交/推送前扫描</span>
</span></span><span style="display:flex;"><span>brew install gitleaks
</span></span><span style="display:flex;"><span>gitleaks git --pre-commit --staged --verbose   <span style="color:#6272a4"># 接入 husky pre-commit</span>
</span></span></code></pre></div><p><code>.gitignore</code> 里永远有：<code>.env</code>、<code>*.pem</code>、<code>*.key</code>、<code>credentials*</code>。</p>
<h3 id="第二层服务端拦截">第二层：服务端拦截</h3>
<ul>
<li>GitHub：开启 <strong>Push Protection</strong>（推送时扫描，疑似密钥直接拒绝）</li>
<li>GitLab：开启 Secret Detection</li>
<li>自建：pre-receive hook 跑 gitleaks</li>
</ul>
<h3 id="第三层泄漏后的处置-sop">第三层：泄漏后的处置 SOP</h3>
<p>密钥一旦进过任何一次提交，就<strong>视为已泄漏</strong>，即使后来的提交删了它：</p>
<ol>
<li><strong>立即吊销 / 轮换密钥</strong>（这是唯一有效的补救，改历史只是清理痕迹）</li>
<li>用 <code>git filter-repo</code> 从全历史删除该文件（见高级篇）</li>
<li>全员重新克隆</li>
<li>排查密钥使用日志，评估影响面</li>
<li>复盘：为什么绕过了第一层和第二层</li>
</ol>
<hr>
<h2 id="cicd-中的-git-技巧">CI/CD 中的 Git 技巧</h2>
<h3 id="浅克隆--按需加深">浅克隆 + 按需加深</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#6272a4"># GitHub Actions</span>
</span></span><span style="display:flex;"><span>- <span style="color:#ff79c6">uses</span>: actions/checkout@v4
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">fetch-depth</span>: <span style="color:#bd93f9">0</span>        <span style="color:#6272a4"># 需要 tag / 全历史（如算版本号）时才用 0，否则保持默认 1</span>
</span></span></code></pre></div><h3 id="只跑受影响范围的测试">只跑受影响范围的测试</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 与合并基比较，得到改动文件列表</span>
</span></span><span style="display:flex;"><span>git diff --name-only origin/main...HEAD | grep -q <span style="color:#f1fa8c">&#39;^apps/web/&#39;</span> <span style="color:#ff79c6">&amp;&amp;</span> run_web_tests
</span></span></code></pre></div><h3 id="版本号来自-git">版本号来自 Git</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git describe --tags --always --dirty
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># v1.2.0-5-ga1b2c3d  → 距 v1.2.0 之后第 5 个提交，哈希 a1b2c3d</span>
</span></span></code></pre></div><p>这是给构建产物打版本标识的零维护方案，前端塞进 <code>__VERSION__</code>、后端塞进 <code>/healthz</code> 返回值。</p>
<h3 id="自动打-tag--生成-changelog">自动打 tag / 生成 CHANGELOG</h3>
<p>release-please 或 semantic-release 会读取 Conventional Commits，自动算出版本号、生成 CHANGELOG、创建 release PR 和 tag。从此&quot;发版&quot;不是一个手工动作。</p>
<h3 id="保护分支--必需检查">保护分支 + 必需检查</h3>
<p>平台侧配置（GitHub Branch Protection / GitLab Protected Branches）：</p>
<ul>
<li>禁止直接 push main，必须走 PR</li>
<li>必须 ≥1 人 approve</li>
<li>必须通过 CI 检查</li>
<li>禁止 force push 和删除</li>
</ul>
<hr>
<h2 id="灾难恢复-sop">灾难恢复 SOP</h2>
<h3 id="场景-1误删了远程分支">场景 1：误删了远程分支</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 任何拉过该分支的人本地都有完整历史</span>
</span></span><span style="display:flex;"><span>git switch -c feature/xxx &lt;已知的最新提交id 或 reflog 里找&gt;
</span></span><span style="display:flex;"><span>git push -u origin feature/xxx
</span></span></code></pre></div><h3 id="场景-2对-main-执行了-force-push覆盖掉别人的提交">场景 2：对 main 执行了 force push，覆盖掉别人的提交</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 找到被覆盖前的 main 位置（任何同事本地、CI 缓存、或平台 Events API）</span>
</span></span><span style="display:flex;"><span>git reflog show main           <span style="color:#6272a4"># 在还没 pull 的机器上</span>
</span></span><span style="display:flex;"><span>git push --force origin &lt;旧提交id&gt;:main
</span></span></code></pre></div><p>教训：<strong>在平台设置里禁掉 main 的 force push</strong>，从制度上消灭这个场景。</p>
<h3 id="场景-3仓库本地损坏对象丢失索引损坏">场景 3：仓库本地损坏（对象丢失、索引损坏）</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git fsck --full                <span style="color:#6272a4"># 诊断</span>
</span></span><span style="display:flex;"><span>git clone &lt;remote-url&gt; rescue  <span style="color:#6272a4"># 最省事：远程还在就直接重克隆</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 远程也没了但同事机器上有：任何人的 clone 都是完整备份</span>
</span></span></code></pre></div><h3 id="场景-4rebase--amend-错了想回到操作前">场景 4：rebase / amend 错了，想回到操作前</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git reflog                     <span style="color:#6272a4"># 找到 rebase 之前的 HEAD 位置</span>
</span></span><span style="display:flex;"><span>git reset --hard HEAD@<span style="color:#ff79c6">{</span>5<span style="color:#ff79c6">}</span>
</span></span></code></pre></div><h3 id="场景-5整个-git-目录被删了">场景 5：整个 .git 目录被删了</h3>
<p>未推送的提交彻底丢失——<strong>这就是为什么&quot;每天至少 push 一次&quot;写进了初级篇</strong>。能做的只有从远程重新克隆，工作区文件还在的话手动对比恢复。</p>
<p><strong>通用原则</strong>：Git 里 99% 的&quot;丢失&quot;都能用 <code>git reflog</code> + <code>git fsck</code> 找回；剩下 1% 是&quot;从未 commit / 从未 push&quot;，只能靠备份习惯预防。</p>
<hr>
<h2 id="一套可直接落地的团队-git-规范">一套可直接落地的团队 Git 规范</h2>
<p>以下是本篇的浓缩，可以直接贴进团队 Wiki：</p>
<h3 id="分支">分支</h3>
<ol>
<li><code>main</code> 永远可发布，受保护，禁止直推、禁止 force push</li>
<li>功能分支命名：<code>&lt;type&gt;/&lt;简述&gt;</code>，如 <code>feat/sms-login</code>、<code>fix/pay-timeout</code></li>
<li>功能分支活不过 3 天，合并后删除</li>
</ol>
<h3 id="提交">提交</h3>
<ol start="4">
<li>遵循 Conventional Commits，commitlint 强制</li>
<li>一次提交做一件事；<code>wip</code>、<code>tmp</code> 类提交合并前必须 squash</li>
<li>开启 SSH 提交签名</li>
</ol>
<h3 id="同步">同步</h3>
<ol start="7">
<li>每天开工先 <code>git rebase origin/main</code>（功能分支上）</li>
<li>每天至少 push 一次</li>
<li>公共分支只用 merge，功能分支同步用 rebase，永不 rebase 公共历史</li>
</ol>
<h3 id="评审">评审</h3>
<ol start="10">
<li>一切代码经 PR 合入 main，CI 通过 + 至少 1 人 approve</li>
<li>PR 保持小：≤400 行变更为佳，大了拆</li>
</ol>
<h3 id="发布">发布</h3>
<ol start="12">
<li>tag 遵循 semver，由 release-please 自动生成</li>
<li>线上 bug 走 <code>hotfix/*</code> 分支，修完同时回合 main</li>
</ol>
<h3 id="安全">安全</h3>
<ol start="14">
<li>pre-commit 跑 gitleaks，服务端开 Push Protection</li>
<li>密钥泄漏 = 立即轮换，其次才是清历史</li>
</ol>
<hr>
<h2 id="终极速查表">终极速查表</h2>
<table>
	<thead>
			<tr>
					<th>场景</th>
					<th>方案</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>大仓库克隆</td>
					<td><code>--filter=blob:none</code> + sparse-checkout</td>
			</tr>
			<tr>
					<td>提交规范</td>
					<td>Conventional Commits + commitlint</td>
			</tr>
			<tr>
					<td>自动发版</td>
					<td>release-please / semantic-release</td>
			</tr>
			<tr>
					<td>构建版本号</td>
					<td><code>git describe --tags --always --dirty</code></td>
			</tr>
			<tr>
					<td>提交防伪</td>
					<td>SSH 签名 + 平台 Verified</td>
			</tr>
			<tr>
					<td>密钥扫描</td>
					<td>gitleaks（本地）+ Push Protection（服务端）</td>
			</tr>
			<tr>
					<td>密钥泄漏</td>
					<td>轮换密钥 → filter-repo 清历史 → 全员重克隆</td>
			</tr>
			<tr>
					<td>误删/覆盖找回</td>
					<td><code>git reflog</code> → <code>reset --hard</code> / 强推恢复</td>
			</tr>
			<tr>
					<td>仓库损坏</td>
					<td><code>git fsck</code> 诊断，重新克隆</td>
			</tr>
			<tr>
					<td>协作红线</td>
					<td>保护 main、禁 force push、一切走 PR</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="系列回顾">系列回顾</h2>
<ul>
<li><strong>初级篇</strong>：四个区域、三板斧、远程同步——能用</li>
<li><strong>中级篇</strong>：分支模型、merge/rebase、reflog——用得对</li>
<li><strong>高级篇</strong>：对象模型、bisect、worktree、submodule、LFS——懂原理</li>
<li><strong>终极篇</strong>：工程化、安全、SOP、团队规范——成体系</li>
</ul>
<p>Git 的学习曲线不在命令数量，而在心智模型。四篇读完，剩下的就是每天在真实项目里踩坑、翻 reflog、读 graph——祝你的每一次 <code>git push</code> 都心安理得。</p>
]]></content:encoded></item><item><title>Git 使用手册 · 高级篇：原理、调试与仓库工程</title><link>https://blog.heyaohua.com/posts/2026/08/git-handbook-advanced/</link><pubDate>Sat, 22 Aug 2026 23:23:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/08/git-handbook-advanced/</guid><description>Git 高级手册，深入对象模型与引用、bisect 二分排错、worktree 多工作区、submodule 与 subtree、hooks 自动化、大文件 LFS 与仓库瘦身。</description><content:encoded><![CDATA[<p>这是 Git 手册系列的第三篇。前两篇讲&quot;怎么用&quot;，本篇讲&quot;为什么&quot;：理解了 Git 的内部模型，所有命令都会变成推理题而非背诵题；再配上一批工程化工具，解决&quot;什么时候出的 bug&quot;、&ldquo;仓库太大&rdquo;、&ldquo;多仓库依赖&quot;这类真实难题。</p>
<hr>
<h2 id="目录">目录</h2>
<ul>
<li>Git 对象模型：blob、tree、commit、tag</li>
<li>引用：分支、HEAD、tag 都只是指针</li>
<li>彻底理解 merge / rebase / cherry-pick</li>
<li>bisect：二分查找&quot;哪次提交引入了 bug&rdquo;</li>
<li>worktree：一个仓库多个工作区</li>
<li>submodule 与 subtree：仓库套仓库</li>
<li>hooks：在关键时刻自动执行脚本</li>
<li>Git LFS 与大文件</li>
<li>仓库瘦身：清理历史中的大文件</li>
<li>高级调试与诊断</li>
</ul>
<hr>
<h2 id="git-对象模型blobtreecommittag">Git 对象模型：blob、tree、commit、tag</h2>
<p>Git 本质是一个<strong>内容寻址的文件系统</strong> + 一个版本控制外壳。<code>.git/objects/</code> 下只有四种对象：</p>
<table>
	<thead>
			<tr>
					<th>对象</th>
					<th>内容</th>
					<th>类比</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>blob</strong></td>
					<td>文件的内容（不含文件名）</td>
					<td>文件快照</td>
			</tr>
			<tr>
					<td><strong>tree</strong></td>
					<td>一个目录：文件名 → blob/tree 的指针列表</td>
					<td>文件夹快照</td>
			</tr>
			<tr>
					<td><strong>commit</strong></td>
					<td>指向一个 tree + 父 commit 指针 + 作者/时间/信息</td>
					<td>一次版本</td>
			</tr>
			<tr>
					<td><strong>tag</strong>（附注）</td>
					<td>指向一个 commit + 标签信息</td>
					<td>里程碑</td>
			</tr>
	</tbody>
</table>
<p>所有对象用内容算 SHA-1（新版支持 SHA-256）哈希作为 id：<strong>内容相同 → id 相同</strong>，这就是 Git 去重和完整性校验的原理。</p>
<p>一次提交的结构示意：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>commit a1b2c3d
</span></span><span style="display:flex;"><span>├── tree     e4f5g6h          ← 项目根目录快照
</span></span><span style="display:flex;"><span>│   ├── src/        → tree ...
</span></span><span style="display:flex;"><span>│   ├── README.md   → blob ...
</span></span><span style="display:flex;"><span>│   └── package.json→ blob ...
</span></span><span style="display:flex;"><span>├── parent   9i0j1k2          ← 上一次提交
</span></span><span style="display:flex;"><span>├── author   heyaohua &lt;...&gt; 1700000000 +0800
</span></span><span style="display:flex;"><span>└── message  修复登录页按钮错位
</span></span></code></pre></div><p>自己动手看一眼（plumbing 命令）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git cat-file -t a1b2c3d    <span style="color:#6272a4"># 对象类型：commit</span>
</span></span><span style="display:flex;"><span>git cat-file -p a1b2c3d    <span style="color:#6272a4"># 打印内容</span>
</span></span><span style="display:flex;"><span>git cat-file -p HEAD^<span style="color:#ff79c6">{</span>tree<span style="color:#ff79c6">}</span> <span style="color:#6272a4"># 当前提交的根 tree</span>
</span></span></code></pre></div><p>关键推论：</p>
<ul>
<li><strong>&ldquo;快照&quot;其实很省空间</strong>：没改的文件，新 tree 直接复用旧 blob 指针。</li>
<li><strong>commit 是不可变的</strong>：任何修改历史（amend/rebase）都是生成新对象，旧的靠 reflog 留着。</li>
</ul>
<hr>
<h2 id="引用分支headtag-都只是指针">引用：分支、HEAD、tag 都只是指针</h2>
<p><code>.git/refs/</code> 和 <code>.git/HEAD</code> 揭开了分支的全部秘密：</p>
<ul>
<li><strong>分支</strong>：一个文件，内容就是某个 commit 的哈希。<code>git switch -c foo</code> 就是写了一个 41 字节的文件——所以 Git 开分支快到零成本。</li>
<li><strong>HEAD</strong>：一个文件，内容是&quot;当前指向哪个分支&rdquo;（如 <code>ref: refs/heads/main</code>）。</li>
<li><strong>detached HEAD</strong>：HEAD 直接指向 commit 而不是分支（如 <code>git switch a1b2c3d</code>）。此时提交的改动没有分支名挂着，切走后会变成&quot;孤儿&quot;，解法是立刻 <code>git switch -c rescue</code> 给它安个分支名。</li>
<li><strong>远程跟踪分支</strong> <code>origin/main</code>：也是本地的一个指针，记录&quot;上次和远程同步时远程在哪&quot;，只有 <code>fetch/pull/push</code> 时才更新。</li>
</ul>
<p>合并提交 = 有<strong>两个 parent</strong> 的 commit。fast-forward 合并 = 只是把分支指针往前挪，不产生新 commit。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git log --oneline --graph --all    <span style="color:#6272a4"># 现在你应该能完全读懂这张图了</span>
</span></span><span style="display:flex;"><span>git rev-parse HEAD main origin/main  <span style="color:#6272a4"># 看各引用实际指向的哈希</span>
</span></span></code></pre></div><hr>
<h2 id="彻底理解-merge--rebase--cherry-pick">彻底理解 merge / rebase / cherry-pick</h2>
<p>有了对象模型，三者的区别一句话说清：</p>
<ul>
<li><strong>merge</strong>：新建一个双 parent 的 commit，tree 取三方合并（base、我方、对方）的结果。</li>
<li><strong>rebase</strong>：把每条提交的 diff 依次&quot;重放&quot;到新 base 上，生成一串<strong>新 commit</strong>（旧提交还在，靠 reflog 可见）。</li>
<li><strong>cherry-pick</strong>：等价于对单条提交做一次 rebase——算出它的 diff，重放到当前 HEAD。</li>
</ul>
<p>所以&quot;rebase 会改历史&quot;的准确说法是：<strong>commit id（哈希）会变，因为 parent 变了 → 内容变了 → 哈希变了</strong>。这也是为什么不能 rebase 公共分支：别人手里还是旧 id，你们的对象图从此分叉。</p>
<hr>
<h2 id="bisect二分查找哪次提交引入了-bug">bisect：二分查找&quot;哪次提交引入了 bug&quot;</h2>
<p>场景：线上报 bug，上次发版还是好的，中间隔了 200 条提交，不知道哪次改坏了。人肉翻要半天，bisect 用二分查找只要约 8 步（log₂200 ≈ 7.6）。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git bisect start
</span></span><span style="display:flex;"><span>git bisect bad                 <span style="color:#6272a4"># 当前版本是坏的</span>
</span></span><span style="display:flex;"><span>git bisect good v1.2.0         <span style="color:#6272a4"># 上次发版是好的</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Git 自动 checkout 到中间的提交，你测试后回答：</span>
</span></span><span style="display:flex;"><span>git bisect good   <span style="color:#6272a4"># 或 git bisect bad</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># …重复几轮…</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>git bisect reset   <span style="color:#6272a4"># 结束后回到原分支</span>
</span></span></code></pre></div><p>最后 Git 会告诉你：<code>a1b2c3d is the first bad commit</code>。</p>
<p><strong>自动化 bisect</strong>——有现成测试脚本时全程不用动手：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git bisect start HEAD v1.2.0
</span></span><span style="display:flex;"><span>git bisect run npm <span style="color:#8be9fd;font-style:italic">test</span>        <span style="color:#6272a4"># 退出码非 0 即 bad</span>
</span></span><span style="display:flex;"><span>git bisect reset
</span></span></code></pre></div><hr>
<h2 id="worktree一个仓库多个工作区">worktree：一个仓库多个工作区</h2>
<p>场景：正在 feature 分支开发到一半（一堆未提交改动），需要紧急在 main 上验证一个问题。stash 来回倒腾很烦，worktree 直接给仓库开第二个目录：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git worktree add ../my-project-hotfix main   <span style="color:#6272a4"># 在旁边目录开一个 main 的工作区</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> ../my-project-hotfix                      <span style="color:#6272a4"># 独立目录，独立工作区，共享 .git</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># …修 bug、提交、push…</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> ../my-project
</span></span><span style="display:flex;"><span>git worktree remove ../my-project-hotfix     <span style="color:#6272a4"># 用完删掉</span>
</span></span><span style="display:flex;"><span>git worktree list
</span></span></code></pre></div><p>注意：同一分支不能同时被两个 worktree 检出。这也是 Code Review 时快速跑别人 PR 的利器。</p>
<hr>
<h2 id="submodule-与-subtree仓库套仓库">submodule 与 subtree：仓库套仓库</h2>
<h3 id="submodule嵌一个独立仓库的固定版本">submodule：嵌一个独立仓库的固定版本</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git submodule add https://github.com/org/shared-lib.git vendor/shared-lib
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#f1fa8c">&#34;引入 shared-lib 子模块&#34;</span>
</span></span></code></pre></div><p>主仓库存的只是&quot;子模块的仓库地址 + commit id&quot;。克隆含子模块的项目：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone --recurse-submodules &lt;url&gt;
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 或事后补：</span>
</span></span><span style="display:flex;"><span>git submodule update --init --recursive
</span></span></code></pre></div><p>更新子模块：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> vendor/shared-lib <span style="color:#ff79c6">&amp;&amp;</span> git pull origin main <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#8be9fd;font-style:italic">cd</span> -
</span></span><span style="display:flex;"><span>git add vendor/shared-lib <span style="color:#ff79c6">&amp;&amp;</span> git commit -m <span style="color:#f1fa8c">&#34;升级 shared-lib&#34;</span>
</span></span></code></pre></div><p>痛点：每个协作者都要记得 <code>--recurse-submodules</code>，子模块内部的修改提交流程也绕。<strong>适合：外部依赖、发布节奏完全独立的组件。</strong></p>
<h3 id="subtree把另一个仓库的内容直接并入">subtree：把另一个仓库的内容直接并入</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git subtree add --prefix vendor/shared-lib https://github.com/org/shared-lib.git main --squash
</span></span><span style="display:flex;"><span>git subtree pull --prefix vendor/shared-lib https://github.com/org/shared-lib.git main --squash
</span></span></code></pre></div><p>对协作者完全透明（就是一个普通目录），但拉取更新稍慢、命令冗长。<strong>适合：想 vendoring 一份代码且偶尔同步上游。</strong></p>
<p>选择建议：依赖关系强、需要锁版本 → submodule；只想拷一份进来偶尔同步 → subtree；能走包管理器（npm/pip/go mod）的一律别用这两个。</p>
<hr>
<h2 id="hooks在关键时刻自动执行脚本">hooks：在关键时刻自动执行脚本</h2>
<p><code>.git/hooks/</code> 里的可执行脚本会在对应时机触发。常用：</p>
<table>
	<thead>
			<tr>
					<th>hook</th>
					<th>时机</th>
					<th>典型用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>pre-commit</code></td>
					<td>提交前</td>
					<td>lint、格式化、跑单测</td>
			</tr>
			<tr>
					<td><code>commit-msg</code></td>
					<td>提交信息生成后</td>
					<td>校验 message 格式（conventional commits）</td>
			</tr>
			<tr>
					<td><code>pre-push</code></td>
					<td>推送前</td>
					<td>跑完整测试套件</td>
			</tr>
			<tr>
					<td><code>post-merge</code></td>
					<td>合并后</td>
					<td>检测 package.json 变了就提醒装依赖</td>
			</tr>
	</tbody>
</table>
<p>示例 <code>pre-commit</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#ff79c6">#!/bin/sh
</span></span></span><span style="display:flex;"><span>npm run lint-staged <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">exit</span> <span style="color:#bd93f9">1</span>
</span></span></code></pre></div><p>实践中不要手写裸 hook，用 <strong>husky</strong>（Node 项目）或 <strong>pre-commit 框架</strong>（Python/多语言）管理，它们把 hooks 纳入版本库、团队共享：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pnpm add -D husky lint-staged
</span></span><span style="display:flex;"><span>npx husky init
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;pnpm lint-staged&#34;</span> &gt; .husky/pre-commit
</span></span></code></pre></div><hr>
<h2 id="git-lfs-与大文件">Git LFS 与大文件</h2>
<p>Git 对二进制的差量存储很差，一个 100MB 的 PSD 改 10 次，仓库就胖 1GB。Git LFS（Large File Storage）把大文件内容存到专门服务器，仓库里只留指针：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git lfs install
</span></span><span style="display:flex;"><span>git lfs track <span style="color:#f1fa8c">&#34;*.psd&#34;</span> <span style="color:#f1fa8c">&#34;*.zip&#34;</span> <span style="color:#f1fa8c">&#34;assets/videos/**&#34;</span>
</span></span><span style="display:flex;"><span>git add .gitattributes        <span style="color:#6272a4"># 必须提交，团队才会共享规则</span>
</span></span><span style="display:flex;"><span>git add design.psd <span style="color:#ff79c6">&amp;&amp;</span> git commit -m <span style="color:#f1fa8c">&#34;添加设计稿&#34;</span>
</span></span></code></pre></div><p>注意：LFS 只对<strong>新提交</strong>生效；历史里已有的大家伙要用下节的方法清理。另外 LFS 服务器（GitHub 等）有流量配额，重度使用前先算账。</p>
<hr>
<h2 id="仓库瘦身清理历史中的大文件">仓库瘦身：清理历史中的大文件</h2>
<p>场景：早期误提交了构建产物 / 视频，仓库 clone 要 10 分钟。目标是<strong>改写全部历史</strong>，把大文件从每个 commit 里抠掉。</p>
<p>推荐工具 <strong>git-filter-repo</strong>（官方推荐的第三方工具，替代已废弃的 <code>filter-branch</code>）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install git-filter-repo
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 按路径删除（历史上所有提交）</span>
</span></span><span style="display:flex;"><span>git filter-repo --path dist/ --invert-paths
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 按大小：干掉所有超过 50M 的 blob</span>
</span></span><span style="display:flex;"><span>git filter-repo --strip-blobs-bigger-than 50M
</span></span></code></pre></div><p>⚠️ 这是<strong>破坏性重写历史</strong>：</p>
<ol>
<li>操作前全量备份（<code>cp -r</code> 或推一个备份 remote）。</li>
<li>所有协作者必须重新克隆，旧本地仓库作废。</li>
<li>推送用 <code>git push --force</code> 全分支全标签。</li>
<li>如果只是为了去掉误提交的<strong>密钥</strong>，改完历史后<strong>密钥照样要换</strong>——它可能已经泄露。</li>
</ol>
<p>查一下仓库里最占空间的历史对象：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git rev-list --objects --all <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | git cat-file --batch-check<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;%(objecttype) %(objectname) %(objectsize) %(rest)&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | awk <span style="color:#f1fa8c">&#39;/^blob/ {print substr($0,6)}&#39;</span> | sort -k2 -n | tail -20
</span></span></code></pre></div><hr>
<h2 id="高级调试与诊断">高级调试与诊断</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 仓库统计与磁盘占用</span>
</span></span><span style="display:flex;"><span>git count-objects -vH
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 某行代码的演化史：谁在什么时候为什么改的</span>
</span></span><span style="display:flex;"><span>git log -L 10,20:src/app.js        <span style="color:#6272a4"># 跟踪文件第 10-20 行的变更历史</span>
</span></span><span style="display:flex;"><span>git log -S <span style="color:#f1fa8c">&#34;functionName&#34;</span>           <span style="color:#6272a4"># 找&#34;某段字符串被增删&#34;的提交（pickaxe）</span>
</span></span><span style="display:flex;"><span>git log -G <span style="color:#f1fa8c">&#34;regex&#34;</span>                  <span style="color:#6272a4"># 按正则找 diff 内容变化的提交</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 分支拓扑</span>
</span></span><span style="display:flex;"><span>git log --graph --oneline --all --decorate
</span></span><span style="display:flex;"><span>git branch --contains &lt;commit&gt;      <span style="color:#6272a4"># 哪些分支包含这个提交（修没修进发布版？）</span>
</span></span><span style="display:flex;"><span>git branch --no-merged main         <span style="color:#6272a4"># 还没合并进 main 的分支</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 比较</span>
</span></span><span style="display:flex;"><span>git diff main...feature/xxx         <span style="color:#6272a4"># 三点：feature 从 main 分叉后引入的改动</span>
</span></span><span style="display:flex;"><span>git range-diff main..old main..new  <span style="color:#6272a4"># 比较 rebase 前后两个提交序列的差异</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 数据完整性</span>
</span></span><span style="display:flex;"><span>git fsck                            <span style="color:#6272a4"># 检查对象库损坏 / 悬空对象</span>
</span></span></code></pre></div><p><code>-S</code>（pickaxe）是排查&quot;这行代码什么时候没的&quot;的终极武器，配合 <code>--reverse</code> 还能看一段逻辑从出生到现在的完整演化。</p>
<hr>
<h2 id="本篇速查表">本篇速查表</h2>
<table>
	<thead>
			<tr>
					<th>目的</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>看对象内容</td>
					<td><code>git cat-file -p &lt;id&gt;</code></td>
			</tr>
			<tr>
					<td>看引用指向</td>
					<td><code>git rev-parse &lt;ref&gt;</code></td>
			</tr>
			<tr>
					<td>二分排错</td>
					<td><code>git bisect start/bad/good/run</code></td>
			</tr>
			<tr>
					<td>多工作区</td>
					<td><code>git worktree add &lt;path&gt; &lt;branch&gt;</code></td>
			</tr>
			<tr>
					<td>子模块</td>
					<td><code>git submodule update --init --recursive</code></td>
			</tr>
			<tr>
					<td>并入外部仓库</td>
					<td><code>git subtree pull --prefix=... --squash</code></td>
			</tr>
			<tr>
					<td>提交前自动化</td>
					<td>husky / pre-commit 框架</td>
			</tr>
			<tr>
					<td>大文件</td>
					<td><code>git lfs track &quot;*.bin&quot;</code></td>
			</tr>
			<tr>
					<td>历史瘦身</td>
					<td><code>git filter-repo --invert-paths --path ...</code></td>
			</tr>
			<tr>
					<td>找代码何时被删</td>
					<td><code>git log -S &quot;...&quot;</code></td>
			</tr>
			<tr>
					<td>检查仓库健康</td>
					<td><code>git fsck</code> / <code>git count-objects -vH</code></td>
			</tr>
	</tbody>
</table>
<hr>
<p>到这里，Git 对你已经没有黑盒了。下一篇《终极篇》转向工程之巅：monorepo 策略、安全合规（签名提交、密钥扫描）、CI/CD 中的 Git 技巧、灾难恢复 SOP，以及一套可以直接落地的团队 Git 规范。</p>
]]></content:encoded></item><item><title>Git 使用手册 · 中级篇：分支协作、冲突与历史修改</title><link>https://blog.heyaohua.com/posts/2026/08/git-handbook-intermediate/</link><pubDate>Sat, 22 Aug 2026 23:22:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/08/git-handbook-intermediate/</guid><description>Git 中级手册，讲清分支协作模型、合并冲突的正确解法、reset 三种模式、stash、rebase 与 cherry-pick，让你从会用 Git 到用得对。</description><content:encoded><![CDATA[<p>这是 Git 手册系列的第二篇。初级篇解决了&quot;能用&quot;，本篇解决&quot;用得对&quot;：多人协作不打架、冲突不慌、历史能改得干净。</p>
<hr>
<h2 id="目录">目录</h2>
<ul>
<li>分支模型：选一个然后坚持</li>
<li>merge 与 rebase：到底用哪个</li>
<li>冲突的正确解法</li>
<li>reset 的三种模式（soft / mixed / hard）</li>
<li>stash：临时收摊子</li>
<li>cherry-pick：挑一个提交带走</li>
<li>修改历史：amend 与交互式 rebase</li>
<li>reflog：你的后悔药</li>
<li>tag 与发布</li>
<li>中级常见错误</li>
</ul>
<hr>
<h2 id="分支模型选一个然后坚持">分支模型：选一个然后坚持</h2>
<h3 id="个人--小团队trunk-based主干开发">个人 / 小团队：Trunk-Based（主干开发）</h3>
<ul>
<li><code>main</code> 是唯一长期分支，随时可发布</li>
<li>功能分支从 main 切出，活 1~3 天，合并后立即删除</li>
<li>配合 PR/MR 做代码评审</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git switch -c feature/xxx main
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 开发、提交</span>
</span></span><span style="display:flex;"><span>git push -u origin feature/xxx   <span style="color:#6272a4"># 提 PR，评审通过后合并</span>
</span></span><span style="display:flex;"><span>git switch main <span style="color:#ff79c6">&amp;&amp;</span> git pull
</span></span><span style="display:flex;"><span>git branch -d feature/xxx
</span></span></code></pre></div><h3 id="需要同时维护多个发布版本git-flow">需要同时维护多个发布版本：Git Flow</h3>
<ul>
<li><code>main</code>：线上版本；<code>develop</code>：集成分支</li>
<li><code>feature/*</code>、<code>release/*</code>、<code>hotfix/*</code> 各司其职</li>
<li>重，只有确实需要多版本并行（如 SaaS + 私有化部署）才用</li>
</ul>
<p><strong>结论</strong>：90% 的团队用 Trunk-Based 就够了。分支模型越简单，协作成本越低。</p>
<hr>
<h2 id="merge-与-rebase到底用哪个">merge 与 rebase：到底用哪个</h2>
<p>两者都用来&quot;把 B 分支的改动合进 A 分支&quot;，区别在于历史形态：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>merge（保留真实历史，产生合并提交）：
</span></span><span style="display:flex;"><span>main:   A---B-------M
</span></span><span style="display:flex;"><span>              \   /
</span></span><span style="display:flex;"><span>feature:       C---D
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rebase（把 feature 的提交&#34;搬家&#34;到 main 最新处，历史成一条直线）：
</span></span><span style="display:flex;"><span>main:   A---B
</span></span><span style="display:flex;"><span>feature:         C&#39;--D&#39;   （C、D 被重放为新提交）
</span></span></code></pre></div><p>经验法则：</p>
<ul>
<li><strong>公共分支（main、develop）上用 merge</strong>，保留协作痕迹。</li>
<li><strong>自己的功能分支同步 main 最新代码时用 rebase</strong>，保持历史干净：</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git switch feature/xxx
</span></span><span style="display:flex;"><span>git fetch origin
</span></span><span style="display:flex;"><span>git rebase origin/main      <span style="color:#6272a4"># 解决冲突后 git rebase --continue</span>
</span></span></code></pre></div><p>⚠️ <strong>黄金规则：永远不要 rebase 已经 push 且别人在用的分支</strong>。rebase 会改写提交 id，别人的本地历史会对不上。</p>
<hr>
<h2 id="冲突的正确解法">冲突的正确解法</h2>
<p>冲突的本质：同一文件的同一区域，两边都改了，Git 不知道该听谁的。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git merge feature/xxx
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># CONFLICT (content): Merge conflict in src/app.js</span>
</span></span></code></pre></div><p>处理流程：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git status                 <span style="color:#6272a4"># 1. 看哪些文件冲突</span>
</span></span></code></pre></div><p>打开冲突文件，会看到标记：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
</span></span><span style="display:flex;"><span>const timeout = 3000;
</span></span><span style="display:flex;"><span>=======
</span></span><span style="display:flex;"><span>const timeout = 5000;
</span></span><span style="display:flex;"><span>&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/xxx
</span></span></code></pre></div><p><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD</code> 到 <code>=======</code> 是当前分支的内容，<code>=======</code> 到 <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code> 是合入方的内容。手动编辑成正确结果，删掉标记：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>const timeout = 5000;
</span></span></code></pre></div><p>然后：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git add src/app.js         <span style="color:#6272a4"># 2. 标记为已解决</span>
</span></span><span style="display:flex;"><span>git commit                 <span style="color:#6272a4"># 3. 完成合并提交</span>
</span></span></code></pre></div><p>中途想放弃：<code>git merge --abort</code>（rebase 对应 <code>git rebase --abort</code>），一切回到合并前。</p>
<p>降低冲突概率的三个习惯：</p>
<ol>
<li>功能分支短命，尽快合并</li>
<li>开始干活前先 <code>git pull</code> / <code>git rebase origin/main</code></li>
<li>公共文件（配置文件、路由表）的改动尽早沟通</li>
</ol>
<hr>
<h2 id="reset-的三种模式soft--mixed--hard">reset 的三种模式（soft / mixed / hard）</h2>
<p><code>git reset</code> 把 HEAD 移动到某个提交，三种模式决定&quot;工作区和暂存区怎么办&quot;：</p>
<table>
	<thead>
			<tr>
					<th>模式</th>
					<th>提交历史</th>
					<th>暂存区</th>
					<th>工作区</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--soft</code></td>
					<td>回退</td>
					<td><strong>保留</strong></td>
					<td>保留</td>
			</tr>
			<tr>
					<td><code>--mixed</code>（默认）</td>
					<td>回退</td>
					<td>清空</td>
					<td><strong>保留</strong></td>
			</tr>
			<tr>
					<td><code>--hard</code></td>
					<td>回退</td>
					<td>清空</td>
					<td><strong>清空</strong></td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git reset --soft HEAD~1      <span style="color:#6272a4"># 撤销上次提交，改动还在暂存区（改提交内容用）</span>
</span></span><span style="display:flex;"><span>git reset HEAD~1             <span style="color:#6272a4"># 撤销上次提交，改动回到工作区（重新组织提交用）</span>
</span></span><span style="display:flex;"><span>git reset --hard HEAD~1      <span style="color:#6272a4"># 撤销上次提交，改动全部丢弃（危险！）</span>
</span></span></code></pre></div><p><code>HEAD~1</code> 表示&quot;上一条提交&quot;，<code>HEAD~3</code> 表示往前数 3 条。</p>
<p><strong>安全准则</strong>：<code>--hard</code> 之前先 <code>git stash</code> 或确认改动有备份；只 reset 还没 push 的提交。</p>
<hr>
<h2 id="stash临时收摊子">stash：临时收摊子</h2>
<p>场景：正在 feature 分支改一半，线上突然有 bug 要你切分支修。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git stash                    <span style="color:#6272a4"># 把未提交的改动收起来，工作区变干净</span>
</span></span><span style="display:flex;"><span>git switch hotfix/urgent     <span style="color:#6272a4"># 去修 bug</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># …修完、提交…</span>
</span></span><span style="display:flex;"><span>git switch feature/xxx       <span style="color:#6272a4"># 回来</span>
</span></span><span style="display:flex;"><span>git stash pop                <span style="color:#6272a4"># 恢复刚才收起的改动</span>
</span></span></code></pre></div><p>常用操作：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git stash list               <span style="color:#6272a4"># 看有几个 stash</span>
</span></span><span style="display:flex;"><span>git stash show -p            <span style="color:#6272a4"># 看最近一次 stash 的内容</span>
</span></span><span style="display:flex;"><span>git stash push -m <span style="color:#f1fa8c">&#34;登录页改了一半&#34;</span>   <span style="color:#6272a4"># 带备注存</span>
</span></span><span style="display:flex;"><span>git stash apply stash@<span style="color:#ff79c6">{</span>1<span style="color:#ff79c6">}</span>    <span style="color:#6272a4"># 恢复指定的（pop 会删掉，apply 保留）</span>
</span></span><span style="display:flex;"><span>git stash drop stash@<span style="color:#ff79c6">{</span>0<span style="color:#ff79c6">}</span>     <span style="color:#6272a4"># 删除</span>
</span></span></code></pre></div><hr>
<h2 id="cherry-pick挑一个提交带走">cherry-pick：挑一个提交带走</h2>
<p>场景：在 develop 上修的一个 bug，main 上也需要，但不想合并整个 develop。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git log --oneline develop        <span style="color:#6272a4"># 找到那个提交的 id，比如 a1b2c3d</span>
</span></span><span style="display:flex;"><span>git switch main
</span></span><span style="display:flex;"><span>git cherry-pick a1b2c3d          <span style="color:#6272a4"># 把它复制过来（生成新提交 id）</span>
</span></span></code></pre></div><p>可以连续挑多个：<code>git cherry-pick A B C</code>，或挑一个区间 <code>git cherry-pick A..B</code>（不含 A）。</p>
<hr>
<h2 id="修改历史amend-与交互式-rebase">修改历史：amend 与交互式 rebase</h2>
<h3 id="amend修补最近一次提交">amend：修补最近一次提交</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 刚提交完发现漏了个文件 / 提交信息写错了</span>
</span></span><span style="display:flex;"><span>git add forgotten.js
</span></span><span style="display:flex;"><span>git commit --amend               <span style="color:#6272a4"># 编辑器里顺便改 message</span>
</span></span><span style="display:flex;"><span>git commit --amend --no-edit     <span style="color:#6272a4"># 只补内容不改 message</span>
</span></span></code></pre></div><h3 id="交互式-rebase整理最近-n-条提交">交互式 rebase：整理最近 N 条提交</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git rebase -i HEAD~4
</span></span></code></pre></div><p>编辑器里列出最近 4 条提交，把每行开头的 <code>pick</code> 改成：</p>
<table>
	<thead>
			<tr>
					<th>指令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>pick</code></td>
					<td>保留这条提交</td>
			</tr>
			<tr>
					<td><code>reword</code></td>
					<td>保留，但改提交信息</td>
			</tr>
			<tr>
					<td><code>squash</code></td>
					<td>合并进上一条（保留两条的信息供编辑）</td>
			</tr>
			<tr>
					<td><code>fixup</code></td>
					<td>合并进上一条（丢弃本条信息）</td>
			</tr>
			<tr>
					<td><code>drop</code></td>
					<td>删除这条提交</td>
			</tr>
	</tbody>
</table>
<p>典型场景：PR 之前把 <code>修复 typo</code>、<code>再改一下</code>、<code>真的改好了</code> 这类碎提交压成一条干净的。</p>
<p>⚠️ 同样的黄金规则：<strong>只改写未 push 的历史</strong>。如果已 push 且确认只有自己在用，改写后需要 <code>git push --force-with-lease</code>（比 <code>--force</code> 安全：远程有别人新提交时会拒绝）。</p>
<hr>
<h2 id="reflog你的后悔药">reflog：你的后悔药</h2>
<p>几乎所有&quot;我完了&quot;的瞬间，<code>git reflog</code> 都能救：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git reflog
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>a1b2c3d HEAD@{0}: reset: moving to HEAD~1      ← 手滑的操作
</span></span><span style="display:flex;"><span>e4f5g6h HEAD@{1}: commit: 重要的提交            ← 以为丢了的提交
</span></span></code></pre></div><p>reflog 记录 HEAD 的每一次移动（commit、reset、rebase、切换分支……），保留约 90 天。找回方式：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git reset --hard e4f5g6h     <span style="color:#6272a4"># 直接回到那个状态</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 或</span>
</span></span><span style="display:flex;"><span>git branch rescue e4f5g6h    <span style="color:#6272a4"># 开个分支把那个提交捞回来</span>
</span></span></code></pre></div><p><strong>只要不是 <code>git clean</code> 删掉的未跟踪新文件，Git 里几乎没有真正丢失的提交。</strong></p>
<hr>
<h2 id="tag-与发布">tag 与发布</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git tag v1.2.0                   <span style="color:#6272a4"># 轻量标签</span>
</span></span><span style="display:flex;"><span>git tag -a v1.2.0 -m <span style="color:#f1fa8c">&#34;发布 1.2.0：新增支付模块&#34;</span>   <span style="color:#6272a4"># 附注标签（推荐）</span>
</span></span><span style="display:flex;"><span>git push origin v1.2.0           <span style="color:#6272a4"># 推送单个标签</span>
</span></span><span style="display:flex;"><span>git push --tags                  <span style="color:#6272a4"># 推送所有标签</span>
</span></span><span style="display:flex;"><span>git tag -d v1.2.0                <span style="color:#6272a4"># 删本地</span>
</span></span><span style="display:flex;"><span>git push origin :v1.2.0          <span style="color:#6272a4"># 删远程</span>
</span></span></code></pre></div><p>版本号遵循语义化版本 <code>主.次.修</code>（semver）：破坏性变更升主版本，新功能升次版本，修 bug 升修订号。</p>
<hr>
<h2 id="中级常见错误">中级常见错误</h2>
<p><strong>1. 在公共分支上 rebase / amend 了已 push 的提交</strong>
队友 pull 时会历史错乱。解：公共分支只用 merge；改写前先确认没有别人基于它工作。</p>
<p><strong>2. 解决冲突时直接用&quot;接受我方/接受对方&quot;整文件覆盖</strong>
可能把别人无关的改动一起吞掉。逐块（hunk）检查。</p>
<p><strong>3. <code>git reset --hard</code> 之后发现误删了改动</strong>
先别慌，<code>git reflog</code> 找回。养成 hard 之前 <code>git stash</code> 的习惯。</p>
<p><strong>4. stash 堆成山从来不清理</strong>
<code>git stash list</code> 定期看一眼，用完就 pop/drop。</p>
<p><strong>5. cherry-pick 之后又 merge 原分支，出现重复改动</strong>
偶尔会发生，合并时 Git 通常能识别；冲突了耐心解一次即可。</p>
<hr>
<h2 id="本篇速查表">本篇速查表</h2>
<table>
	<thead>
			<tr>
					<th>目的</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>同步 main 到功能分支</td>
					<td><code>git rebase origin/main</code></td>
			</tr>
			<tr>
					<td>放弃合并</td>
					<td><code>git merge --abort</code></td>
			</tr>
			<tr>
					<td>撤销提交保留改动</td>
					<td><code>git reset --soft/--mixed HEAD~1</code></td>
			</tr>
			<tr>
					<td>彻底回退（危险）</td>
					<td><code>git reset --hard &lt;id&gt;</code></td>
			</tr>
			<tr>
					<td>暂存半成品</td>
					<td><code>git stash</code> / <code>git stash pop</code></td>
			</tr>
			<tr>
					<td>复制单个提交</td>
					<td><code>git cherry-pick &lt;id&gt;</code></td>
			</tr>
			<tr>
					<td>改最近提交</td>
					<td><code>git commit --amend</code></td>
			</tr>
			<tr>
					<td>整理历史</td>
					<td><code>git rebase -i HEAD~n</code></td>
			</tr>
			<tr>
					<td>找回丢失提交</td>
					<td><code>git reflog</code></td>
			</tr>
			<tr>
					<td>安全强推</td>
					<td><code>git push --force-with-lease</code></td>
			</tr>
			<tr>
					<td>打标签</td>
					<td><code>git tag -a v1.0.0 -m &quot;...&quot;</code></td>
			</tr>
	</tbody>
</table>
<hr>
<p>到这一层，你已经能应付团队协作的全部日常。下一篇《高级篇》进入 Git 的内部世界：对象模型、引用、HEAD 的本质、bisect 二分排错、worktree、submodule/subtree 与钩子——理解了&quot;为什么&quot;，命令就再也背不丢。</p>
]]></content:encoded></item><item><title>Git 使用手册 · 初级篇：从第一次提交开始</title><link>https://blog.heyaohua.com/posts/2026/08/git-handbook-beginner/</link><pubDate>Sat, 22 Aug 2026 23:21:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/08/git-handbook-beginner/</guid><description>Git 入门手册，用最少的心智负担讲清仓库、工作区、暂存区、提交、分支、远程的基本概念与日常命令，适合零基础的第一次上手。</description><content:encoded><![CDATA[<p>这是 Git 手册系列的第一篇。目标只有一个：让你今天就能用 Git 管好自己的代码，并且知道每条命令在干什么。不理解原理没关系，先把工作流跑通。</p>
<hr>
<h2 id="目录">目录</h2>
<ul>
<li>Git 是什么，解决什么问题</li>
<li>安装与初始配置</li>
<li>四个区域：工作区、暂存区、本地仓库、远程仓库</li>
<li>第一次提交</li>
<li>日常三板斧：status / add / commit</li>
<li>查看历史</li>
<li>撤销与回退（安全版）</li>
<li>连接远程仓库（GitHub / GitLab）</li>
<li>分支是什么（先会用最基础的）</li>
<li>初级常见错误</li>
</ul>
<hr>
<h2 id="git-是什么解决什么问题">Git 是什么，解决什么问题</h2>
<p>没有 Git 的时候，你是这样管理代码的：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>project/
</span></span><span style="display:flex;"><span>project-final/
</span></span><span style="display:flex;"><span>project-final-v2/
</span></span><span style="display:flex;"><span>project-final-v2-真的最终版/
</span></span></code></pre></div><p>Git 做的事：把项目的每一次修改记录成&quot;快照&quot;（commit），你可以随时查看任何历史版本、回到任何历史版本、同时维护多条并行的修改线（分支），并和别人协作而互不覆盖。</p>
<hr>
<h2 id="安装与初始配置">安装与初始配置</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># macOS</span>
</span></span><span style="display:flex;"><span>brew install git
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Ubuntu / Debian</span>
</span></span><span style="display:flex;"><span>sudo apt install git
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Windows：安装 Git for Windows（自带 Git Bash）</span>
</span></span></code></pre></div><p>第一次使用必须先配置身份，否则提交会带上机器默认的匿名信息：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git config --global user.name <span style="color:#f1fa8c">&#34;你的名字&#34;</span>
</span></span><span style="display:flex;"><span>git config --global user.email <span style="color:#f1fa8c">&#34;you@example.com&#34;</span>
</span></span><span style="display:flex;"><span>git config --global init.defaultBranch main
</span></span></code></pre></div><p>验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git config --list
</span></span></code></pre></div><hr>
<h2 id="四个区域工作区暂存区本地仓库远程仓库">四个区域：工作区、暂存区、本地仓库、远程仓库</h2>
<p>这是理解 Git 所有命令的钥匙：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>工作区          暂存区          本地仓库          远程仓库
</span></span><span style="display:flex;"><span>(working dir)   (staging)      (repository)     (remote)
</span></span><span style="display:flex;"><span>    |               |               |                |
</span></span><span style="display:flex;"><span>    |  git add  →   |  git commit → |  git push  →   |
</span></span><span style="display:flex;"><span>    |               |               |                |
</span></span><span style="display:flex;"><span>    |  ← checkout   |  ← reset      |  ← git pull    |
</span></span></code></pre></div><ul>
<li><strong>工作区</strong>：你正在编辑的文件</li>
<li><strong>暂存区</strong>：下一次提交要包含的内容（一个&quot;待打包区&quot;）</li>
<li><strong>本地仓库</strong>：已经提交的历史快照</li>
<li><strong>远程仓库</strong>：GitHub / GitLab / 自建服务器上的副本</li>
</ul>
<p>为什么要多一个暂存区？因为你可以改 10 个文件，但只把其中 3 个相关的改动打成一次干净的提交。</p>
<hr>
<h2 id="第一次提交">第一次提交</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 在项目目录初始化仓库</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> my-project
</span></span><span style="display:flex;"><span>git init
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 查看状态（最重要的命令，有事没事敲一下）</span>
</span></span><span style="display:flex;"><span>git status
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 把文件放进暂存区</span>
</span></span><span style="display:flex;"><span>git add README.md        <span style="color:#6272a4"># 单个文件</span>
</span></span><span style="display:flex;"><span>git add .                <span style="color:#6272a4"># 所有变更（初级可以先这样用）</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 提交到本地仓库</span>
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#f1fa8c">&#34;初始化项目：添加 README&#34;</span>
</span></span></code></pre></div><p>提交信息（commit message）的约定：<strong>一句话说清&quot;做了什么&quot;，用动词开头</strong>。例如 <code>修复登录页按钮错位</code>、<code>新增用户注册接口</code>。不要写 <code>update</code>、<code>fix</code>、<code>123</code>。</p>
<h3 id="gitignore告诉-git-别管某些文件">.gitignore：告诉 Git 别管某些文件</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span># 依赖与构建产物
</span></span><span style="display:flex;"><span>node_modules/
</span></span><span style="display:flex;"><span>dist/
</span></span><span style="display:flex;"><span>*.log
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># 环境与密钥（永远不要提交）
</span></span><span style="display:flex;"><span>.env
</span></span><span style="display:flex;"><span>*.pem
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># 系统文件
</span></span><span style="display:flex;"><span>.DS_Store
</span></span></code></pre></div><p>新建仓库第一件事就是写 <code>.gitignore</code>。如果已经把 <code>node_modules</code> 提交了，用 <code>git rm -r --cached node_modules</code> 把它移出版本库再提交。</p>
<hr>
<h2 id="日常三板斧status--add--commit">日常三板斧：status / add / commit</h2>
<p>你的日常循环就是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git status                    <span style="color:#6272a4"># 现在有哪些改动？哪些已暂存？</span>
</span></span><span style="display:flex;"><span>git add &lt;file&gt;                <span style="color:#6272a4"># 挑选改动放入暂存区</span>
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#f1fa8c">&#34;做了什么&#34;</span>      <span style="color:#6272a4"># 打包成一次快照</span>
</span></span></code></pre></div><p>辅助命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git diff                      <span style="color:#6272a4"># 工作区 vs 暂存区：还没 add 的改动细节</span>
</span></span><span style="display:flex;"><span>git diff --staged             <span style="color:#6272a4"># 暂存区 vs 上次提交：这次将要提交什么</span>
</span></span></code></pre></div><p><strong>提交频率建议</strong>：完成一个&quot;说得清的小目标&quot;就提交一次。一天 3~10 次提交很正常；一周一次提交等于没用 Git。</p>
<hr>
<h2 id="查看历史">查看历史</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git log                       <span style="color:#6272a4"># 完整历史</span>
</span></span><span style="display:flex;"><span>git log --oneline             <span style="color:#6272a4"># 一行一条，最常用</span>
</span></span><span style="display:flex;"><span>git log --oneline --graph     <span style="color:#6272a4"># 带分支图形</span>
</span></span><span style="display:flex;"><span>git show &lt;commit-id&gt;          <span style="color:#6272a4"># 看某次提交的具体改动</span>
</span></span><span style="display:flex;"><span>git blame &lt;file&gt;              <span style="color:#6272a4"># 文件每一行是谁在哪次提交写的</span>
</span></span></code></pre></div><p>每次提交有一个 40 位的哈希 id，平时用前 7 位即可指代（如 <code>a1b2c3d</code>）。</p>
<hr>
<h2 id="撤销与回退安全版">撤销与回退（安全版）</h2>
<p>初级只需要记住三句，按&quot;还没提交到哪一步&quot;选：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1. 改了文件但还没 add：丢弃工作区修改</span>
</span></span><span style="display:flex;"><span>git restore &lt;file&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. 已经 add 但还没 commit：退回暂存区（改动保留在工作区）</span>
</span></span><span style="display:flex;"><span>git restore --staged &lt;file&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3. 已经 commit：再提交一次&#34;反向提交&#34;来撤销，不改历史，最安全</span>
</span></span><span style="display:flex;"><span>git revert &lt;commit-id&gt;
</span></span></code></pre></div><p>⚠️ <code>git reset --hard</code> 和 <code>git push --force</code> 会<strong>永久丢数据或改写共享历史</strong>，初级阶段先别碰，等中级篇再学。</p>
<hr>
<h2 id="连接远程仓库github--gitlab">连接远程仓库（GitHub / GitLab）</h2>
<h3 id="情形-a本地已有项目推到新建的远程仓库">情形 A：本地已有项目，推到新建的远程仓库</h3>
<p>在 GitHub 上创建一个<strong>空仓库</strong>（不要勾选初始化 README），然后：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git remote add origin git@github.com:yourname/my-project.git
</span></span><span style="display:flex;"><span>git branch -M main
</span></span><span style="display:flex;"><span>git push -u origin main
</span></span></code></pre></div><h3 id="情形-b从远程克隆一个已有项目">情形 B：从远程克隆一个已有项目</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone git@github.com:someone/some-project.git
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> some-project
</span></span></code></pre></div><h3 id="日常同步">日常同步</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git pull        <span style="color:#6272a4"># 拉取远程最新提交并合并到本地</span>
</span></span><span style="display:flex;"><span>git push        <span style="color:#6272a4"># 推送本地提交到远程</span>
</span></span></code></pre></div><p>认证推荐配置 SSH key（<code>ssh-keygen</code> 生成后把公钥贴到 GitHub 设置里），一次配置永久免密。</p>
<hr>
<h2 id="分支是什么先会用最基础的">分支是什么（先会用最基础的）</h2>
<p>分支 = 一条独立的修改线，互不影响。默认分支是 <code>main</code>。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git branch                      <span style="color:#6272a4"># 列出本地分支</span>
</span></span><span style="display:flex;"><span>git switch -c feature-login     <span style="color:#6272a4"># 新建并切换到新分支</span>
</span></span><span style="display:flex;"><span>git switch main                 <span style="color:#6272a4"># 切回 main</span>
</span></span></code></pre></div><p>典型用法：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git switch -c fix-button        <span style="color:#6272a4"># 开分支做修复</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># …改代码、add、commit…</span>
</span></span><span style="display:flex;"><span>git switch main                 <span style="color:#6272a4"># 回主线</span>
</span></span><span style="display:flex;"><span>git merge fix-button            <span style="color:#6272a4"># 把修复合并进来</span>
</span></span><span style="display:flex;"><span>git branch -d fix-button        <span style="color:#6272a4"># 删掉用完的分支</span>
</span></span><span style="display:flex;"><span>git push                        <span style="color:#6272a4"># 同步到远程</span>
</span></span></code></pre></div><p>初级记住一个原则：<strong>永远别直接在 main 上干活，做任何改动都先开分支</strong>。合并冲突暂时不用怕，中级篇会专门讲。</p>
<hr>
<h2 id="初级常见错误">初级常见错误</h2>
<p><strong>1. 提交信息写 &ldquo;update&rdquo;</strong>
一个月后你自己都不知道这次提交干了什么。写具体点。</p>
<p><strong>2. 把 <code>.env</code>、密钥提交了</strong>
先 <code>git rm --cached .env</code> 移除并写进 <code>.gitignore</code>。注意：历史里还有，密钥必须立即更换。</p>
<p><strong>3. 在错误的目录 <code>git init</code></strong>
比如在家目录 <code>~</code> 下执行了 init。删掉误建的 <code>.git</code> 目录即可：<code>rm -rf ~/.git</code>（确认路径后再执行）。</p>
<p><strong>4. pull 下来一堆冲突，慌了</strong>
冲突不是错误，是 Git 在问你&quot;两边都改了，听谁的&quot;。先 <code>git status</code> 看哪些文件冲突，打开文件找 <code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code> 标记，手动改对后 <code>add</code> + <code>commit</code>。</p>
<p><strong>5. 半年不 push</strong>
本地仓库丢了硬盘就没了。每天至少 push 一次。</p>
<hr>
<h2 id="本篇速查表">本篇速查表</h2>
<table>
	<thead>
			<tr>
					<th>目的</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>初始化</td>
					<td><code>git init</code></td>
			</tr>
			<tr>
					<td>看状态</td>
					<td><code>git status</code></td>
			</tr>
			<tr>
					<td>暂存</td>
					<td><code>git add &lt;file&gt;</code></td>
			</tr>
			<tr>
					<td>提交</td>
					<td><code>git commit -m &quot;...&quot;</code></td>
			</tr>
			<tr>
					<td>看历史</td>
					<td><code>git log --oneline</code></td>
			</tr>
			<tr>
					<td>撤销未暂存修改</td>
					<td><code>git restore &lt;file&gt;</code></td>
			</tr>
			<tr>
					<td>撤销已暂存</td>
					<td><code>git restore --staged &lt;file&gt;</code></td>
			</tr>
			<tr>
					<td>安全撤销某次提交</td>
					<td><code>git revert &lt;id&gt;</code></td>
			</tr>
			<tr>
					<td>克隆</td>
					<td><code>git clone &lt;url&gt;</code></td>
			</tr>
			<tr>
					<td>拉取 / 推送</td>
					<td><code>git pull</code> / <code>git push</code></td>
			</tr>
			<tr>
					<td>开分支</td>
					<td><code>git switch -c &lt;name&gt;</code></td>
			</tr>
			<tr>
					<td>合并</td>
					<td><code>git merge &lt;name&gt;</code></td>
			</tr>
	</tbody>
</table>
<hr>
<p>掌握这些，你已经覆盖了个人项目 90% 的场景。下一篇《中级篇》讲分支协作模型、合并冲突的正确解法、stash、reset 的三种模式和 rebase 入门——也就是从&quot;会用&quot;到&quot;用得对&quot;。</p>
]]></content:encoded></item></channel></rss>