首页 WordPress .deploy-git 部署方案-Docusaurus 本地构建 · Git 增量传输 · 直推腾讯轻量服务器
.deploy-git 部署方案 — 本地构建 · Git 增量传输 · 直推服务器

🚀 .deploy-git 部署方案

Docusaurus 本地构建 · Git 增量传输 · 直推腾讯轻量服务器
Git 增量传输 无需 CI/CD 直推服务器

📖 概述

传统 Docusaurus 部署流程通常是:源码推 GitHub → GitHub Actions 构建 → rsync 到服务器。这套流程依赖外部 CI、排队等待、链路长、排查麻烦。

.deploy-git 方案 的思路完全不同:在项目根目录下创建一个独立的 Git 裸仓库 .deploy-git,以 build/ 为其工作目录(work-tree),专门用于追踪构建产物。每次本地 npm run build 后,将 build/ 的内容提交到 .deploy-git,然后通过 git push 直接推送到服务器上的 bare repo,服务器通过 post-receive hook 自动检出到网站根目录。

这套方案的核心优势在于:构建在本地完成,传输走 Git 协议且只传增量,部署结果即时反馈,不依赖任何外部服务。

🏗️ 整体架构

本地开发机 (Windows) 源码 build/ npm run build .deploy-git/ git push 增量传输 ✓ 腾讯轻量服务器 (Linux) /var/repo/xxx.git 网站根目录 hook Build = 构建 · Git = 版本控制+传输 · Server = 部署

整个流程只有 3 步

  1. 本地构建npm run build → 生成 build/
  2. Git 提交+推送build/ 的内容提交到 .deploy-git,然后 git push 到服务器
  3. 服务器自动部署post-receive hook 将文件检出到网站根目录

📁 项目目录结构

项目根目录/
├── .git/              ← 日常开发的 Git 仓库(跟踪源码)
├── .deploy-git/部署专用 Git 裸仓库
│   ├── objects/       ← Git 对象存储(commit / tree / blob)
│   ├── refs/          ← 分支引用
│   └── config         ← 配置(含 remote 地址)
├── build/             ← 构建产物(.deploy-git 的 work-tree)
│   ├── index.html
│   ├── assets/
│   └── ...
├── docs/              ← 源码文档
├── src/               ← 源码
└── package.json

💡 关键理解

.deploy-git.git 是完全独立的两个 Git 仓库。一个管源码,一个管部署产物。互不干扰。

⚖️ 方案对比

维度 传统 GitHub Actions .deploy-git 直推
构建位置 GitHub Runner(需排队) 本地(即时执行)
传输方式 rsync 全量对比 Git 增量(只传变化)
部署反馈 等 CI 跑完才知道结果 秒级反馈,终端直接看
外部依赖 依赖 GitHub + Actions 零依赖,SSH + Git 即可
版本回滚 重跑 Action / 手动 rsync git revert 一键回退
源码仓库 可能混入 build 产物 完全分离,干干净净
部署失败排障 翻 CI 日志 本地终端直接看

🔧 配置步骤

1. 服务器端 — 创建 bare repo + hook

SSH 登录到服务器,执行以下命令:

# 创建 bare 仓库
mkdir -p /var/repo/docusite.git
cd /var/repo/docusite.git
git init --bare

# 创建 post-receive hook(自动部署到网站目录)
cat > hooks/post-receive << 'EOF'
#!/bin/bash
TARGET="/www/wwwroot/haoyelaiga.com/docusite"
git --work-tree="$TARGET" checkout -f
EOF
chmod +x hooks/post-receive

# 修正 HEAD 指向 main(如果推送的是 main 分支)
git symbolic-ref HEAD refs/heads/main

⚠️ 注意 CRLF 换行符

hook 文件在 Windows 上创建后 scp 到 Linux 会带 CRLF 结尾,导致 #!/bin/bash\r 无法识别。需要用 sed -i 's/\r$//' 转换,或者直接在服务器上用 vi/nano 创建。

2. 本地 — 初始化 .deploy-git

# 在项目根目录执行
git init --bare .deploy-git
git --git-dir=.deploy-git remote add origin \
  root@111.230.81.144:/var/repo/docusite.git

# 关闭自动 CRLF 转换(消除 LF→CRLF warning)
git --git-dir=.deploy-git config core.autocrlf false

3. 本地 — 添加 npm scripts

package.jsonscripts 中添加:

"upload": "git --git-dir=.deploy-git --work-tree=build add -A && git --git-dir=.deploy-git --work-tree=build commit -m deploy --allow-empty && git --git-dir=.deploy-git --work-tree=build push -f origin HEAD:main",
"ship":   "npm run build && npm run upload"

4. 本地 — .gitignore

.gitignore 中添加,防止源码仓库误追踪:

# Deploy git repo
.deploy-git

🧪 增量传输测试 — 完整过程与结果

为了验证 Git 是否真的只传输有变化的文件,我们设计了一套三阶段测试。

测试设计

测试 操作 预期
测试 1 正常构建(有博客文章更新) 有一定量的变更
测试 2 不修改任何源文件,再次构建 几乎无变更
测试 3 只修改 1 个 markdown 文档,构建 观察实际影响范围

测试结果数据

指标 测试 1
有文章更新
测试 2
不改源码
测试 3
改 1 个 doc
变更文件数 162 0 313
新增 Git 对象 +319 +1(仅 commit) +612
新增存储大小 +1,363 KB +0 KB +2,173 KB
Tree hash 变化 完全一致 ✅ 变化

测试 2 的关键发现

✅ 证明:不修改源码时,零增量传输

连续两次 git commit(源码未改),tree hash 完全一致。Git 识别出文件内容没有变化,只创建了一个空的 commit 对象(~180 字节),没有创建任何新的 blob(文件内容对象)。push 时也只传输了这个 tiny commit。

测试 3 的变更构成分析

文件类型 数量 变化原因
JS 文件 16 webpack content hash 重新计算,文件名变化
HTML 文件 294 引用的 JS/CSS hash 变了,路径更新
XML 文件 2 atom.xml / rss.xml 博客 feed 更新
其他 1 sitemap.xml
实际内容变化 2 仅中/英文版 getting-started 页面 ← 我们修改的那个 doc

🔍 验证方法大全

以下是验证 Git 增量传输的各种方法:

方法 1:对比 tree hash(最直接)

# 查看最近两次提交的 tree hash
$t1 = git --git-dir=.deploy-git rev-parse "HEAD^{tree}"
$t2 = git --git-dir=.deploy-git rev-parse "HEAD~1^{tree}"
Write-Host "当前 tree: $t1"
Write-Host "上一个 tree: $t2"
# 如果相同 → 文件内容没变,只传了 commit

方法 2:查看 Git 对象统计

# 查看松散对象数量和大小
git --git-dir=.deploy-git count-objects -v

# 输出示例:
count: 152        ← 松散对象数
size: 1849        ← 松散对象大小 (KB)
in-pack: 5545     ← 已打包对象数
size-pack: 4711   ← 打包后大小 (KB)

# 对比两次 count 的变化:
# 如果只 +1 → 只新增了 commit 对象,文件内容没变 👍

方法 3:查看 push 传输量

# push 输出中会显示传输了多少对象
npm run upload
# 输出示例:
Enumerating objects: 1, done.     ← 只枚举了 1 个对象
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 178 bytes  ← 只传了 178 字节
Total 1 (delta 0)                      ← delta 为 0
To 111.230.81.144:/var/repo/xxx.git
   abc123..def456  HEAD -> main

方法 4:查看变更文件数

# 查看与上一次提交相比,哪些文件变了
git --git-dir=.deploy-git diff --stat HEAD~1 HEAD

# 如果不改源码重新构建,应该是空的(0 files changed)

方法 5:服务器端查看

# 在服务器上查看接收的对象
ssh root@服务器IP "cat /var/repo/xxx.git/objects/info/packs"

❓ 常见问题

Q: 为什么服务器上所有文件的时间戳都变成了当前时间?

这是因为 post-receive hook 中的 git checkout -f 会把 tree 中的所有文件全部写出到磁盘。即使文件内容没变,也会重新写入,导致时间戳更新。这是 checkout 命令的特性,不代表文件内容被重新传输了。传输阶段(git push)确实是增量的。

Q: 为什么要用 --allow-empty

因为 git commit 在内容没变化时会拒绝创建新 commit。但部署需要一个新 commit 来触发推送到服务器。--allow-empty 确保即使 build/ 的内容和上次一样,也能生成一个 commit 来推动部署流程。

Q: build/ 下的 .gitattributes 要加吗?

不需要。只需在 .deploy-git 中设置 core.autocrlf false 即可消除 LF→CRLF 的 warning。构建产物最终部署到 Linux 服务器,LF 换行符完全没问题。

Q: 如果某次部署有问题,怎么回滚?

在本地执行:

# 查看部署历史
git --git-dir=.deploy-git log --oneline -10

# 回退到指定版本并强制推送
git --git-dir=.deploy-git reset --hard <commit-hash>
git --git-dir=.deploy-git --work-tree=build push -f origin HEAD:main

Q: 第一次推送到服务器时,网站目录需要先清空吗?

不需要。git checkout -f 会覆盖已有文件,但不会删除不在 tree 中的额外文件(除非用 git clean -fd)。如果之前有旧文件残留,建议先清空一次:rm -rf /网站目录/*

⚠️ 注意事项

🎯 最终总结

核心结论

Git 确实只传输内容有变化的文件。

虽然 Docusaurus 每次构建会因 content hash 策略导致少量 JS/CSS 文件名变化(从而连锁引起 HTML 更新),但 如果源文件没改,重新构建出来的内容完全一致,Git 不会重复存储也不会重复传输

验证结果:不修改任何源文件的情况下连续两次构建并 push,tree hash 完全一致,新增 Git 对象数为 0(仅一个空的 commit 对象)。

这套方案的核心设计理念:

  1. 构建本地化 — 不依赖外部 CI,即时构建,即时反馈
  2. 传输增量 — Git 原生增量协议,只传变化的内容
  3. 部署自动化 — 服务器 hook 自动检出,无需额外脚本
  4. 版本可追溯 — 每个部署都是一个 Git commit,可随时回滚

整个流程只需一条命令:npm run ship → 构建 + 增量传输 + 服务器自动部署,一步到位。