代码与仓库:多远程、组织所有权、镜像与归档

代码只有一个远程仓库,就等于只有一个副本。本篇讲清工作副本、远程仓库、CI 配置与发布产物四层资产,给出多远程、镜像、git bundle 归档、组织所有权、发布产物保留,以及一次在没有原平台账号时从零恢复的演练。

大多数人把「代码在 GitHub 上」当成「代码有备份」。其实那只是「代码有一个远程副本」,而这个副本和你的账号、平台政策、计费状态绑在一起。

一、仓库消失的那天

这是为解释机制构造的假设案例,人物、引语、收入和耗时均为教学设定,不是对真实事件的调查记录。

一位开发者的产品代码放在一个私有仓库里,仓库属于他个人账号。某天他收到平台通知:账号因「异常活动」被临时限制。限制期间,他无法登录、无法克隆、无法推送,也无法导出仓库。

他有本地工作副本,所以代码本身没丢。但接下来他发现:

  • CI 配置里的部署密钥只在平台 secrets 里,本地没有记录。
  • 发布用的签名证书密码,存在平台的环境变量里。
  • 上一个版本的构建产物只存在 Releases 页面。
  • 问题追踪、讨论、发布说明,全在平台上。

账号在第九天恢复。代码没丢,但九天里他无法发布任何修复。他后来把这次经历总结为一句话:

「我有代码的副本,但我没有产品的副本。」

二、代码资产的四层

层内容常见唯一副本失效后的表现
工作副本你电脑上的 .git 目录通常有,但可能只有一台机器电脑损坏或未推送,丢失最近改动
远程仓库GitHub / GitLab / Gitee 等往往只有一个账号受限或仓库误删,无法协作与发布
CI 与配置流水线、环境变量、部署密钥只在平台里无法构建、无法部署
发布产物二进制、安装包、校验和、签名只在 Releases 或 CDN无法回滚、无法给用户重装

四层里,远程仓库最容易被当成「已经备份」,发布产物最容易被完全忽略。

三、多远程与镜像

Git 本身支持多个远程。最轻量的韧性方案,是让同一次推送落到两个地方。

3.1 手动双远程

# 查看现有远程
git remote -v

# 添加一个镜像远程
git remote add mirror git@backup.example.com:you/project.git

# 推到两个远程
git push origin main
git push mirror main

3.2 一次推送两个远程

# 给 origin 增加第二个 push URL
git remote set-url --add --push origin git@primary.example.com:you/project.git
git remote set-url --add --push origin git@backup.example.com:you/project.git

之后 git push origin main 会依次尝试推送到配置的地址,不是跨平台原子事务;一个成功另一个失败时需分别核对。fetch URL 不因增加 push URL 自动改变,备用远程仍需验证实际 refs。

3.3 平台侧镜像

主流代码托管平台大多支持「推送镜像」或「导入仓库」:把仓库定时同步到另一个平台。优点是无需改本地配置,缺点是依赖平台功能,且同步可能有延迟。

3.4 镜像不是备份的全部

镜像能防「平台账号受限」,不能防「误删分支后镜像也同步删除」。所以还需要一份不会被同步删除的归档。

四、git bundle:离线归档的最小单位

git bundle 可以把一个仓库(包括所有分支和标签)打包成单个文件。它不依赖任何平台,能在任何有 Git 的机器上恢复。

# 打包所有分支和标签
git bundle create project-2026-10.bundle --all

# 校验包是否完整
git bundle verify project-2026-10.bundle

# 计算校验和
shasum -a 256 project-2026-10.bundle

恢复时,先区分两种目标。只想查看默认分支可普通 git clone,但它不等于完整恢复所有分支。要完整恢复 Git 分支和标签,在隔离目录使用镜像克隆;目标必须是已确认的空仓库:

# backup.bundle 已校验,以下在新的隔离目录执行
# 镜像克隆保留 bundle 中的引用;不使用 push --mirror 覆盖已有远端
git clone --mirror backup.bundle restored.git
git -C restored.git for-each-ref --format='%(refname) %(objectname)' refs/heads refs/tags
# 将 origin 改为自己控制且已确认为空的新仓库,再分别推分支和标签
git -C restored.git remote set-url origin git@example.com:you/empty-restored.git
git -C restored.git config remote.origin.mirror false
git -C restored.git push origin --all
git -C restored.git push origin --tags

普通 clone 只把默认分支建成本地分支;其他分支常在 origin/*。这时删除 origin 会移除其远程跟踪引用,push --all 也不会推回它们。镜像 clone 默认设置镜像推送,所以上面在分别推分支和标签前显式关闭 remote.origin.mirror。恢复验收应比较原始 refs/heads、refs/tags 的名字及对象 ID,而不是只看 clone 退出码。

--all 打包的是当前仓库已有引用能到达的 Git 对象。未提交与未跟踪文件、Git LFS 实体、子模块各自的仓库、平台 Issues/PR、CI secrets、外部包和发布产物需要独立清单与备份;浅克隆或漏 fetch 的分支不能凭 bundle 补出来。

4.1 归档节奏

项目类型建议频率保留
有付费用户的商业项目每月一次最近 12 个月 + 每年一份
活跃开发的开源项目每季度一次最近 4 个季度
维护模式项目每半年一次最近 2 份
已归档项目关停时一次永久

4.2 归档存放

  • 一份放在与代码平台无关的云存储(不同供应商)。
  • 一份放在离线硬盘或 NAS。
  • 记录 SHA-256 校验和,恢复前先校验。
  • 归档文件加密,密钥与文件分开存放。

五、组织所有权:别把产品绑在个人账号上

个人账号适合起步,但不适合承载有付费用户的产品。原因不是「个人账号不安全」,而是个人账号的恢复路径和所有权转移路径更窄。

结构优点风险适用阶段
个人账号下的私有仓库简单、免费账号受限即全部不可用;难以转让原型、实验
组织/团队账号可设多个所有者;可转移;权限清晰配置略复杂有付费用户
组织 + 服务账号自动化不依赖个人需要管理密钥有 CI/CD
自建托管完全可控运维成本高有明确合规需求

5.1 组织的最小配置

  • 至少两个 owner:你 + 一个可信任的人(或一个长期保留的应急账号)。
  • 开启组织级 2FA 要求。
  • 关键仓库开启分支保护:禁止直接 push 到主分支,要求 PR。
  • 部署密钥、CI token 使用最小权限,不用个人长期 token。
  • 记录「谁是 owner、谁有 admin 权限」,写进资产清单。

5.2 服务账号与机器用户

如果 CI 需要一个长期身份,不要用你的个人账号 token。优先使用平台支持的应用身份、OIDC 或服务账号;确需机器用户时先核对平台条款与席位成本,再授予最小权限,并把它的凭据存进密码管理器与离线恢复包。

六、CI 配置与密钥

CI 是最容易被忽略的一层,因为它「看不见」。但它决定了你能不能发布。

6.1 需要备份的内容

内容存在哪备份方式
流水线定义仓库内的 YAML 文件随代码一起归档
环境变量与 secrets平台设置页加密清单,不含明文
部署密钥平台或云厂商密码管理器 + 离线恢复包
签名证书与口令本地或平台加密存储,双份异地
构建脚本仓库内随代码归档

6.2 原则

  • secrets 的「清单」要备份,「值」要单独加密存放。 出事时至少知道缺什么。
  • 不要把唯一一份签名证书放在 CI 平台。 平台账号受限时,你连重签都做不到。
  • 部署流程要有一份可手动执行的脚本。 CI 挂了不等于发布流程必须停。

七、发布产物的保留

用户下载的是发布产物,不是源码。发布产物丢了,用户无法重装,你也无法回滚。

产物保留策略校验
安装包 / 二进制每个公开版本永久保留SHA-256 + 代码签名
容器镜像至少保留最近 10 个版本digest
前端静态资源保留最近 3 个版本构建哈希
数据库迁移脚本永久保留随代码归档
发布说明永久保留与版本号对应

每个版本发布时,把产物、校验和、签名、发布说明一起复制到独立存储。 不要只依赖平台的 Releases 页面。

八、一次从零恢复演练

目标:在没有原平台账号的情况下,用归档恢复出一个能构建的仓库。

8.1 准备

  • 选一个非核心仓库,打一个 git bundle。
  • 导出 CI 变量清单(只记名字,不记值)。
  • 下载最近一个发布产物与校验和。

8.2 恢复

  • 在一台干净机器上 git clone bundle。
  • 校验 bundle 的 SHA-256。
  • 在新的代码托管平台创建空仓库,推送所有分支与标签。
  • 按清单重建 CI 变量(值从密码管理器取)。
  • 跑一次构建,确认产物可生成。
  • 对比新产物与旧产物的校验和(如果构建可复现)。

8.3 记录

  • 记录恢复耗时。
  • 记录缺失的配置、密钥、依赖。
  • 把缺失项补进资产清单。
  • 更新归档频率或存放位置。

演练的价值不在于「恢复成功」,而在于发现你以为备份了、其实没备份的东西。

九、常见错误

错误后果更好的做法
只有一个远程仓库账号受限即无法发布加镜像远程或平台镜像
把 secrets 只存在平台平台不可用时无法构建备份加密的 secrets 清单与值
发布产物只留在 Releases平台清理或账号受限后无法回滚每个版本复制到独立存储
组织只有一个 owner本人无法操作时无人能接管至少两个 owner
用个人 token 做 CI权限过大,离职或轮换麻烦服务账号 + 最小权限
归档从不校验真恢复时发现包损坏每次归档计算并记录 SHA-256

十、行动清单

  • 列出所有代码仓库,标出「只有一个远程」的。
  • 为商业项目添加镜像远程或平台镜像。
  • 做一次 git bundle 归档,存到独立存储。
  • 评估商业仓库是否需要组织所有权与第二位真实授权 owner;组织设置不代替独立备份。
  • 组织至少设两个 owner,开启 2FA 要求。
  • 导出 CI 变量清单,把值存进密码管理器。
  • 每个发布版本保留产物、校验和、签名与说明。
  • 做一次从 bundle 恢复的演练,记录耗时与缺失项。

使用边界与实操验收

本节恢复验证的是 Git 引用与对象完整性,不认证所有 CI、LFS、依赖和签名资产已恢复。下面夹具仅创建临时本地仓库,无远端网络。

将下面保存为 verify_bundle_restore.py,在有 Git 和 Python 3 的机器执行。它创建 main、feature 与标签,恢复后比较全部分支和标签,还检查 feature 的独有内容:

import os
import subprocess
import tempfile
from pathlib import Path

env = os.environ.copy()
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)

def git(cwd, *args):
    return subprocess.check_output(
        ["git", "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null",
         "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", *args],
        cwd=cwd, env=env, text=True, stderr=subprocess.STDOUT
    )

def refs_at(cwd):
    return git(cwd, "for-each-ref", "--format=%(refname) %(objectname)",
               "refs/heads", "refs/tags").splitlines()

with tempfile.TemporaryDirectory() as d:
    d = Path(d)
    git(d, "init", "-b", "main", "src")
    src = d / "src"
    (src / "main.txt").write_text("main\n")
    git(src, "add", ".")
    git(src, "commit", "-m", "main")
    git(src, "switch", "-c", "feature")
    (src / "feature.txt").write_text("feature\n")
    git(src, "add", ".")
    git(src, "commit", "-m", "feature")
    git(src, "tag", "v1")
    git(src, "switch", "main")
    expected = refs_at(src)
    git(src, "bundle", "create", str(d / "backup.bundle"), "--all")
    git(d, "clone", "--mirror", str(d / "backup.bundle"), "restored.git")
    restored = d / "restored.git"
    git(d, "init", "--bare", "destination.git")
    git(restored, "remote", "set-url", "origin", str(d / "destination.git"))
    git(restored, "config", "remote.origin.mirror", "false")
    git(restored, "push", "origin", "--all")
    git(restored, "push", "origin", "--tags")
    assert refs_at(restored) == expected == refs_at(d / "destination.git")
    assert git(d / "destination.git", "show", "feature:feature.txt") == "feature\n"
    print("PASS: main, feature, v1 and feature content restored")

关键参考与核验范围

以下链接支持对应的机制或规则;本文的案例、成本和经营评分是说明方法的假设,不能当作平台统计或收益承诺。核验日期:2026-10-22。

  • Git bundle :离线传输 Git 对象和引用,不含完整项目的所有外部资产。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 定价复盘:价格、转化、留存与退款的一张表
  2. 低流量定价实验:样本不够时怎么测
  3. 结账页:币种、税费、失败重试与拒付