mcpField
A
awesome-git-author-rewrite

awesome-git-author-rewrite

github.com/khasky开发2639 次安装2 周前更新
代码仓库

关于 awesome-git-author-rewrite

Git Author Rewrite 是用于修改 Git 提交的作者或提交者身份并推送改写后历史的技能。它通过验证关卡、新克隆和镜像备份明确这项不可逆操作的范围与风险。

主要功能

  1. 01改写作者和提交者身份
  2. 02支持单个提交或按身份处理
  3. 03统计受影响的提交及后代
  4. 04验证账户归属和写入权限
  5. 05要求备份并取得确认

适用场景

  1. 01更正提交中的邮箱
  2. 02更新匹配提交中的身份
  3. 03为共享历史的改写做好验证

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/khasky/awesome-agent-skills --skill awesome-git-author-rewrite

安装后在 Claude Code 中输入/awesome-git-author-rewrite确认已加载。

附带文件evals/eval_queries.json

详细介绍

Git 作者信息重写

将提交上记录的身份——author 和 committer 名称/邮箱标头——替换为用户实际拥有的身份,然后强制推送。文件树、提交消息和日期都会逐字节保留;只有身份标头会改变,并因此导致所有受影响的提交哈希发生变化。

为何要如此谨慎:身份存储在提交对象内部,因此更改身份会改变提交的哈希,并连带改变其所有后代提交的哈希。这不是编辑,而是对共享远程仓库进行不可逆的重写。以下步骤分别防范几种具体风险:重写的哈希范围远超用户预期、没有写入权限却推送、把一个不属于用户的邮箱换成另一个不属于用户的邮箱导致归属仍未生效、悄然使签名失效,或强制推送覆盖了用户从未见过的队友提交。若另一个克隆未重置,第二天就可能把旧身份推回去。

核心原则

只有在以下五项条件全部满足后,才能执行不可逆操作:已证明新身份属于用户的账户、已确认有写入权限、已创建并验证镜像备份、已统计并展示哈希影响范围,且用户已明确确认。缺少任何一项,都必须停在该关卡。

全程须遵守四项不变原则:

  • 绝不在用户现有的检出目录中操作。应在临时目录中新建一个全新克隆。如果重写有误,该临时克隆可以丢弃,用户的工作副本也从未被触碰。
  • 作者和提交者是两个不同字段。只重写作者会让旧地址继续留在对象中,并显示在托管平台的提交页面上(“X authored and Y committed”)。除非用户只要求修改其中一项,否则两者都要重写。
  • 一个提交绝不只影响一个提交。重写提交 C 会重写所有以 C 为祖先的提交。询问用户确认前,必须统计并展示数量。
  • 未经询问,绝不删除备份,也绝不删除远程引用。备份是唯一的回滚途径。

调用方式

/awesome-git-author-rewrite <commit-url> --name '<Git Author Name>' --email '<git@email>' [--scope commit|identity] [--field author|committer|both]
  • <commit-url> — 必填。任意托管平台(GitHub、GitLab、Bitbucket、Gitea/Forgejo、Azure DevOps)上提交的网页 URL;如果无法识别托管平台的 URL 格式,则使用 <repository-url>#<sha>。
  • --name / --email — 必填。要替换为的身份。缺少任一项时,必须先询问。
  • --scope — 可选。commit(默认):仅处理 URL 中的提交。identity:处理仓库中作者或提交者邮箱与该提交上找到的邮箱相匹配的所有提交——当多个提交都有误时,这通常是用户的意图。
  • --field — 可选,默认为 both。

如果用户调用此技能时没有提供 URL,必须先索取 URL,再进行任何其他操作。

工具检查(先运行)

  • git --version — 必需。
  • git filter-repo --version — 首选的重写引擎。Git 官方文档也推荐使用它;它会保留作者和提交者日期,并且会移除因重写而失效的签名,而不是留下损坏的签名。安装方式:pip install git-filter-repo(所有平台)、brew install git-filter-repo(macOS)、Linux 上的发行版软件包(apt install git-filter-repo、dnf install git-filter-repo)、scoop install git-filter-repo(Windows)。
  • git filter-branch — 始终可用的备用方案,仅在没有 filter-repo 且用户不想安装它时使用。它已弃用、速度慢,而且会保留现已失效的 gpgsig 标头——使用前要说明这一点。
  • 托管平台 CLI — 可选,但强烈建议使用:它会在执行破坏性步骤前验证新邮箱是否属于用户的账户,以及写入权限、分支保护和未合并的拉取请求/合并请求。没有 CLI 就无法检查这些事项——请明确说明,并且仅在用户接受这一盲点后继续。

依赖某个工具前,先确认它位于 PATH 中(退出码为 0)。绝不要假定已安装 filter-repo 或托管平台 CLI。

使用哪个托管平台 CLI。根据 URL 检测托管平台,然后使用对应工具:

托管平台CLI检查是否存在通用替代方式
GitHubghgh --versiongh api <endpoint>
GitLab(SaaS 或自托管)glabglab --versionglab api <endpoint>(自托管时使用 --hostname)
Bitbucket、Gitea/Forgejo、Azure DevOps、普通 SSH 远程仓库不假定有 CLI—如果用户提供令牌,则使用 curl 调用托管平台的 REST API

使用 curl 作为替代方式时,从环境变量读取令牌,并通过环境变量传递。绝不要在命令、日志行或报告中打印令牌;如果用户把令牌粘贴到了聊天中,要告知用户并建议撤销该令牌后重新签发。

对于没有 CLI 且没有令牌的托管平台,以下所有需要 CLI 或令牌的检查都视为不可用,而非已通过:列出无法执行的检查,并在进入阶段 1 前取得用户的明确同意。无法执行的检查属于必须披露的盲点,绝不可悄悄跳过。

Shell。运行任何操作前先检测平台(uname -s,或在 PowerShell 中使用 $IsWindows),并根据检测结果选择 Shell,而不是凭习惯选择。在 Windows 上,filter-branch 备用方案(多行 POSIX Shell 脚本)和阶段 6 中的 xargs 清理操作都需要 Git Bash;它随 Git for Windows 一起提供。PowerShell 的引号处理会破坏前者,而且没有后者所需的 xargs。在任一 Shell 中都可以安全运行 filter-repo 调用和普通的 git 命令。


阶段 0 — 输入、身份和访问权限验证(停止门槛)

使用现有工具执行所有可执行的检查。任何一项检查失败都必须立即停止,不得当作警告继续操作。

  1. 将提交 URL 解析为克隆 URL、owner/repo 路径和 SHA:

    托管平台URL 格式说明
    GitHubhttps://github.com/<owner>/<repo>/commit/<sha>也可以是 /pull/<n>/commits/<sha> — 同一提交的 PR 视图
    GitLabhttps://<host>/<group>/<subgroup>/<repo>/-/commit/<sha>/-/ 之前的所有部分都是项目路径
    Bitbuckethttps://bitbucket.org/<workspace>/<repo>/commits/<sha>commits 为复数
    Gitea / Forgejohttps://<host>/<owner>/<repo>/commit/<sha>自托管平台的主机因情况而异
    Azure DevOpshttps://dev.azure.com/<org>/<project>/_git/<repo>/commit/<sha>可能带有 ?refName= 查询参数

    去掉查询字符串和片段。URL 中的 SHA 通常是缩写——在步骤 4 中将其解析为完整哈希。指向分叉仓库的提交 URL 指向的是分叉仓库,而不是上游仓库:URL 中的仓库路径就是即将重写的仓库。如果路径与用户指定的路径不一致,请向用户确认。

  2. 读取权限 + 存在性——成本最低的实际检查:

    git ls-remote <clone-url>
    

    退出码非零或出现身份验证提示 → 停止。URL 错误、仓库为私有且你未通过身份验证,或网络已断开。

  3. 克隆到临时目录——绝不使用用户的工作副本:

    git clone <clone-url> <scratch>/<repo>-rewrite
    cd <scratch>/<repo>-rewrite
    
  4. 提交确实存在,且这是提交上的身份信息:

    git rev-parse <sha>^{commit}                                   # full SHA; fails if the commit isn't here
    git show -s --format='A: %an <%ae>%nC: %cn <%ce>%nD: %ad / %cd' <sha>
    

    将完整 SHA 记录为 TARGET_SHA,将旧邮箱记录为 OLD_EMAIL。向用户展示这两行:作者和提交者经常是不同的身份信息,而哪些信息有误决定了操作范围。此处出现 fatal: bad object 意味着该提交无法从任何已获取的引用访问——它可能只存在于 PR 引用或另一个分叉中;停止并询问,不要猜测。

  5. 验证替换身份信息。邮箱中没有 @、把显示名称粘贴进邮箱字段,或使用占位符,都应停止。如果用户想使用 GitHub 的私有格式,必须准确写成 <id>+<login>@users.noreply.github.com:

    gh api user -q '"\(.id)+\(.login)@users.noreply.github.com"'
    
  6. 硬性规则——新邮箱必须属于用户的账户。这是决定整个操作是否有用的关卡:只有作者邮箱是该账户上的已验证地址(或其 noreply 格式)时,托管平台才会将提交关联到个人资料。若改写为未经验证的地址,提交仍会显示为未关联的纯文本名称——改写历史记录完全白费。

    gh api user/emails -q '.[] | select(.verified) | .email'    # GitHub; needs the user:email scope
    glab api user                                               # GitLab — read `email`; /user/emails lists the rest
    

    glab api 没有字段选择标志:它会输出 JSON,因此请自行读取字段,不要通过尚未确认已安装的 jq 进行管道处理。如果调用因权限范围返回 403(运行 gh auth refresh -s user:email 可修复),或系统中没有 CLI,请说明该关卡无法检查,并告知用户手动确认地址(GitHub:Settings → Emails;GitLab:Preferences → Emails)。地址不在已验证列表中 → 停止并报告;不要改写为账户不拥有的地址。

  7. 硬性规则——仓库所有者必须与您的 git 身份信息匹配。改写他人的仓库几乎总是意味着克隆 URL 错误或误用了同事的项目:

    git config user.name
    git config user.email
    gh api user -q .login                 # GitHub — the account that will actually push
    glab api user                         # GitLab — read `username`
    

    对于由组织或群组拥有的仓库,登录名不会按名称匹配——请改用第 10 步中的写入权限检查作为证明。没有托管平台 CLI 时,将 <owner> 与 git config user.name 或 git config user.email 的本地部分进行匹配。不匹配 → 停止并报告,同时并列列出仓库所有者和本地身份信息;只有在用户明确确认后才能继续。

  8. 确定操作范围并统计代价。这两个数字都必须出现在确认关卡中。

    git log --all --format='%H %ae %ce' | grep -c '<OLD_EMAIL>'      # commits carrying the old identity
    git log --all --format='%ae%n%ce' | sort -u                       # every identity in the repo
    git rev-list --all --count                                        # total commits, for the after-check
    
    • --scope commit → 改写集合仅包含 TARGET_SHA。
    • --scope identity → 改写集合包含以上所有提交。如果计数大于 1,而用户要求的是 commit,请告知数量并询问——保留十个有问题的提交中的九个,通常不是用户的本意。
  9. 影响范围——有多少个哈希会变化。最早被改写提交的所有后续提交都会重新计算哈希;用户真正同意的是这个数字,而不是有问题的提交数量:

    git branch -a --contains <earliest-rewritten-sha>                       # branches affected
    git tag --contains <earliest-rewritten-sha>                             # tags that move — or go stale
    git rev-list --count --ancestry-path <earliest-rewritten-sha>..<branch> # descendants, per branch
    

    --ancestry-path 可确保第三条命令给出精确结果:没有它,范围还会统计后来合并进来的侧分支提交,而这些提交的哈希不会改变。计数不包含提交本身——请加 1。对第一条命令中的每个分支都运行此命令;不在该列表中的分支上不可能存在被改写的提交,因此该列表就是完整的推送集合。将结果表述为 N of M commits change hash。

  10. 写入 / 管理员权限(需要托管平台 CLI):

    gh repo view <owner>/<repo> --json viewerPermission,isFork,forkCount
    glab api projects/<url-encoded-path>        # 读取权限、forked_from_project、forks_count
    

    GitHub:viewerPermission 必须是 WRITE、MAINTAIN 或 ADMIN;READ / null → 停止,你无法推送。GitLab:permissions.project_access(或 permissions.group_access)中的有效 access_level 必须 ≥ 40(Maintainer)——30(Developer)无法强制推送受保护分支。对 GitLab 路径进行 URL 编码:group/sub/repo → group%2Fsub%2Frepo。 Token 权限范围。gh auth status 会打印这些范围。GitHub 的 repo / GitLab 的 write_repository 足以执行强制推送。如果重写范围内的任何提交涉及 .github/workflows/**,还需要 GitHub 的 workflow:此类提交在推送时都会作为新对象重新推送,没有该权限范围时,推送会被拒绝,并显示 refusing to allow an OAuth App to create or update workflow。现在就检查,不要等到推送时再检查。强制执行 SSO 的组织还要求 Token 已获授权;如果 403 响应带有 SSO 标头,就表示尚未授权。

  11. 分支保护(需要主机 CLI)——对受保护分支执行强制推送会在推送时被拒绝,此时重写已经完成:

    gh api repos/<owner>/<repo>/branches/<branch>/protection
    glab api projects/<url-encoded-path>/protected_branches/<branch>
    

    如果返回 200 且不允许强制推送 → 停止;用户需先解除保护或授予绕过权限(GitHub:Settings → Branches;GitLab:Settings → Repository → Protected branches,字段 allow_force_push)。返回 404 表示未受保护——可以继续。

    推送保护(密钥扫描)是另一项规则,也会在推送时拒绝操作;每个重写后的提交都会作为新对象发送到主机:

    gh api repos/<owner>/<repo> --jq .security_and_analysis
    

    如果 secret_scanning_push_protection 显示为 enabled,重写范围内任何位置出现密钥都会导致推送被拒绝。没有管理员权限时不会显示此字段,组织级策略也可能不会显示在其中:字段缺失表示未知,绝不表示未启用。此技能自身不会运行扫描;如果 gitleaks 在 PATH 中,则在克隆仓库上运行 gitleaks git .,如果在重写范围内发现密钥,需提前说明推送会被拒绝。没有 gitleaks 时,需说明该范围未经扫描。

  12. 未合并的拉取请求 / 合并请求(需要主机 CLI)——它们引用旧哈希,因此会失效。这两条命令默认每页返回 30 行,因此先获取完整计数;如果列表到达上限就停止,则视为已截断:

    gh api "search/issues?q=repo:<owner>/<repo>+is:pr+is:open&per_page=1" --jq .total_count
    gh pr list --repo <owner>/<repo> --state open --limit 200
    glab mr list --repo <owner>/<repo> --per-page 100      # 未合并的 MR;添加 --page N,直到某一页返回空结果
    

    如果任何未合并的 PR/MR 所含提交位于影响范围内 → 展示列表,并给出其数量占总数的比例;用户需先关闭或合并这些 PR/MR。如果行数等于上限,或行数少于总数,就表示列表已截断:提高上限或继续翻页;在此之前,将输出标记为已截断,不要说它是完整列表。

  13. 签名——重写会使涉及的每个签名失效,因为签名涵盖身份标头:

    git log --format='%h %G?' <earliest-rewritten-sha>~1..HEAD      # G 表示有效,U 表示不受信任,N 表示无签名,B 表示无效
    

    如果范围内有任何 G/U → 告知用户这些提交将不带签名(filter-repo 会剥除失效签名;filter-branch 会保留它,主机随后会显示“Unverified”)。重新签名意味着之后使用 -S 重新提交,不属于此技能的范围。

  14. Fork 和其他克隆(第 10 步获取 Fork 数量)。重写无法影响它们。明确说明两点:每个 Fork 都会保留旧身份;任何其他机器上的克隆如果不重置,就会直接推回旧历史——准确命令会在第 7 阶段的报告中给出。

  15. 确认关卡。准确说明将要发生什么,然后明确征求同意:

    这将把 <owner>/<repo> 中 N 个提交的 <field> 从 <Old Name> <OLD_EMAIL> 改为 <New Name> <new@email>,更改分支 <list>(以及标签 <list>)上 T 个提交中的 M 个提交的哈希,然后执行强制推送。旧哈希将无法从远程尖端访问(本地会保留经过验证的备份)。这些提交的签名会丢失。未合并的 PR 会失效;Fork 和其他克隆会保留旧身份。继续吗? 未明确确认 → 到此停止。此前所有操作均为只读操作。


阶段 1 — 备份(强制,需验证)

镜像克隆是回滚路径。修改任何内容之前,先创建镜像克隆。

git clone --mirror <clone-url> <scratch>/<repo>-backup-<shortsha>.git
cd <scratch>/<repo>-backup-<shortsha>.git
git rev-list --all --count            # must be > 0 — record as OLD_COUNT
git show-ref --heads                  # record each branch tip: OLD_SHA per branch
git show -s --format=%T <OLD_SHA>     # record OLD_TREE per branch — Phase 3 compares against it
git fsck --full                       # no missing or broken objects

Git LFS:git lfs ls-files 输出非空 → 镜像克隆保存的是指针,而非文件内容。在备份中运行 git lfs fetch --all,否则备份无法还原这些文件。

这里从备份中记录 OLD_TREE,因为工作克隆会在阶段 2 的清理过程中丢失旧对象:之后,git diff <OLD_SHA> HEAD 会返回 fatal: bad object,而对比树哈希仍能证明内容未被更改。

记录每个分支的顶端提交。如需回滚:

git push --force <clone-url> <OLD_SHA>:refs/heads/<branch>

报告备份的绝对路径。切勿将其作为此技能的一部分删除。


阶段 2 — 重写身份信息

在阶段 0 第 3 步创建的工作克隆中运行,不要在镜像克隆中运行。

首选:git filter-repo

范围为 identity — mailmap 正是处理“这个地址实际上属于那个人”的工具,而且一次就能覆盖作者、提交者和标签创建者:

printf '%s\n' 'New Name <new@email> <OLD_EMAIL>' > ../identity.mailmap
git filter-repo --mailmap ../identity.mailmap

每个错误地址添加一行,即可一次处理多个地址。mailmap 对名称采用宽松匹配;邮箱才是可靠的键,因此应使用 OLD_EMAIL 作为匹配键。

范围为 commit — 目标是单个原始哈希:

git filter-repo --commit-callback '
if commit.original_id == b"<TARGET_SHA>":
    commit.author_name     = b"New Name"
    commit.author_email    = b"new@email"
    commit.committer_name  = b"New Name"
    commit.committer_email = b"new@email"
'

对于 --field author,删除两行 committer_*;对于 --field committer,删除两行 author_*。

以下两种行为都应预料并妥善处理,而不是感到意外:

  • filter-repo 会在重写后有意移除 origin 远程仓库,避免意外推送。请在阶段 4 中重新添加。
  • 除非使用强制选项,否则它会拒绝在新克隆以外的目录中运行。这种拒绝是有意设计的 — 如果遇到这种情况,说明你所在的目录不对。不要为了绕过拒绝而使用 --force。
后备方案:git filter-branch(仅在没有 filter-repo 时使用)

在 Git Bash 中运行。-- 用于分隔 filter-branch 的选项和用于选择引用的 rev-list 参数;--branches --tags 可确保操作覆盖整个仓库,而不只是 HEAD。

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f --env-filter '
NEW_NAME="New Name"
NEW_EMAIL="new@email"
OLD_EMAIL="old@email"
# --scope commit: guard on the hash instead — [ "$GIT_COMMIT" = "<TARGET_SHA>" ]
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
  export GIT_AUTHOR_NAME="$NEW_NAME";    export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
fi
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
  export GIT_COMMITTER_NAME="$NEW_NAME"; export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
fi
' -- --branches --tags

然后删除仍指向旧对象的两组引用。漏掉任何一组,下面的每项验证都会继续报告旧地址 — 因为历史中确实还保留着这些地址:

git for-each-ref --format='%(refname)' refs/original | xargs -n1 git update-ref -d
git remote remove origin                       # refs/remotes/origin/* still hold the old identity
git reflog expire --expire=now --all && git gc --prune=now

filter-branch 只会重写 refs/heads 和 refs/tags,因此普通克隆会在 refs/remotes/origin/* 下完整保留旧历史。移除远程仓库正是 filter-repo 自行执行的操作;阶段 4 会重新添加远程仓库。

对地址使用精确的 = 匹配。只有当用户明确要求重写整个域名时,子字符串匹配(case "$GIT_AUTHOR_EMAIL" in *acme*)才是合适的工具 — 而且必须明确说明,因为这种匹配会捕获用户未列出的地址。


阶段 3 — 推送前验证

这里的操作都不会触及远程端。运行以下五项检查;每项都能发现不同的问题。

git log --all --format='%ae%n%ce' | sort -u          # 1. OLD_EMAIL must be gone
git show -s --format='A: %an <%ae>%nC: %cn <%ce>%nD: %ad / %cd' <NEW_SHA>   # 2. target commit
git rev-list --all --count                            # 3. must equal OLD_COUNT
git show -s --format=%T <branch>                      # 4. must equal OLD_TREE, per branch
git log --format='%h %an <%ae>' -5                    # 5. eyeball the tip
  1. 仍有 OLD_EMAIL → 仍有某个引用指向旧历史:运行 filter-branch 时未使用 --branches --tags、未清理 refs/original,或仍保留着 origin 远程跟踪引用。重新运行 git log --branches --tags --format='%ae%n%ce' | sort -u — 如果这项检查结果干净,那么残留内容只存在于你不会推送的引用中,且你跳过了阶段 2 的清理。修复后重新检查;如果 --all 仍显示旧地址,就不要推送。
  2. 身份已换成新的,日期保持不变。作者日期发生变化,说明使用了错误的工具(交互式变基,而非过滤器)——从备份重新开始。
  3. 提交数量不同,说明有提交被丢弃或合并被压平。停止;过滤操作绝不能改变提交图的结构。
  4. 树哈希不同 → 文件树发生了变化。改写身份时绝不能出现这种情况:树哈希相同、提交哈希不同,这正是正确结果的全部特征。停止并从备份恢复。
  5. 在进行任何不可逆操作之前,目视检查一遍。

另请确认阶段 0 第 9 步中的每个分支和标签都仍在本地(git branch -a、git tag)。未进入工作克隆的分支不会在远程端被改写,旧身份会通过它继续保留。


阶段 4 — 强制推送(不可逆步骤)

此时远程端已经移除(filter-repo 会移除它;备用方案的清理操作也会移除)。重新添加远程端,然后为每个改写后的分支使用 lease 推送,而不是直接使用 --force,这样克隆完成后新增的提交会导致推送中止,而不会被销毁:

git remote add origin <clone-url>                                  # removed during Phase 2
git push --force-with-lease=<branch>:<OLD_SHA> origin <branch>     # one line per branch
  • 因信息过期而被拒绝 → 有人在你备份后进行了推送。停止。用户是在确认关卡看到的数字后给出确认的,而这些数字所描述的提交位置已发生变化:返回阶段 0,重新克隆,重新计算第 8、9、12 和 13 步(改写范围、影响范围、开放的 PR、签名)以及针对新提交位置的分支保护,展示变化后的数字,并在阶段 1 开始前重新询问第 15 步。绝不要改用 --force 强行覆盖。
  • 因受保护分支而被拒绝 → 跳过了第 11 步,或之后添加了保护;解除保护后重试。
  • 因推送保护而被拒绝(在推送的提交中发现了密钥)→ 停止。不要通过修改或变基来清除密钥:这会改变文件树,并破坏阶段 3 中树哈希相同的证明。拒绝提示中的绕过链接应由用户决定,绝不能代替用户操作;报告拒绝情况、密钥所在位置,以及轮换密钥才是关键解决办法。
  • 因 refusing to allow an OAuth App to create or update workflow 而被拒绝 → 令牌缺少 workflow 范围(第 10 步);用户刷新令牌后再重试。
  • 包含改写后提交的标签也发生了变化。lease 不适用于这些标签;应明确推送它们,且只推送用户指定的标签:git push --force origin refs/tags/<tag>。未推送的标签会使旧身份仍可访问,导致改写不完整;发布后强制推送标签则会改变该发布所指向的内容。逐个说明标签对应的取舍,绝不要批量处理。

阶段 5 — 在托管平台上验证

本地仓库正确,并不能证明远程端和个人资料页面显示的内容也正确。

git ls-remote origin <branch>                        # tip matches the new local HEAD
gh api repos/<owner>/<repo>/commits/<NEW_SHA> -q '.commit.author.email, .commit.committer.email, .author.login'
glab api projects/<url-encoded-path>/repository/commits/<NEW_SHA>    # read author_email, committer_email
  • 两个电子邮件地址都必须是新地址。
  • .author.login 不能是 null。该字段是托管平台对“此提交是否关联到账户”的直接答复 — 登录名非空就是归属关联成功的证明,null 则表示该电子邮件地址尚未在账户上验证(返回阶段 0 第 6 步)。这是本指南中最有用的单项检查,因为其他一切都可能完美无误,但提交仍可能显示为未关联的姓名。
  • 没有托管平台 CLI → 在浏览器中打开提交页面,检查头像和账户链接是否显示,并在报告中说明验证是通过目视完成的,而非通过程序完成。

阶段 6 — 修复导致身份错误的原因

错误的邮箱来自 git 配置;如果它还在那里,下次提交时就会再次引入错误。只修复历史记录而不修复源头,只是在治标。

git config --show-origin --get user.email          # in the user's real checkout — which file set it
git config --global user.email                     # the usual culprit
  • 全局配置有误 → 在与用户确认后,运行 git config --global user.email '<new@email>' 和 user.name。
  • 全局配置正确,但某个仓库中的配置有误 → 仓库本地值优先;请在该仓库中修正。
  • 有意使用多个身份(工作和个人)→ 长久的解决办法是在 ~/.gitconfig 中添加条件包含项,而不是逐个编辑仓库,免得下次克隆时忘记:
    [includeIf "gitdir:~/work/"]
        path = ~/.gitconfig-work
    
  • 如果还涉及第二台机器,那台机器上的配置也有误。请说明这一点;你无法从这里修复它。

阶段 7 — 汇报

总结发生了什么、有哪些证据,以及用户仍需手动完成哪些事项:

Repository:   <owner>/<repo>
Rewrite:      <Old Name> <old@email> → <New Name> <new@email>   (author + committer)
Scope:        N commit(s) rewritten; M of T commits changed hash
Branches:     <branch> <OLD_SHA> → <NEW_SHA>   (+ one line per branch)
Tags:         <tag> force-pushed | left at old hash (old identity still reachable)
Content:      tree <OLD_TREE> unchanged — same tree hash before and after, no files changed
Dates:        author/committer dates unchanged
Identities:   git log --all: only <new@email> remains | also <bot@address> (expected)
Backup:       <absolute path to *-backup-*.git>   (verified: OLD_COUNT commits, fsck clean)
Host check:   commit.author.email = <new@email>; author.login = <login> (attribution linked)
Signatures:   N commits lost their signature | none were signed
Config fix:   git config --global user.email now <new@email> | unchanged (was correct)

手动处理的遗留事项(无法通过命令完成):

  • 其他所有克隆 — 无论在哪台机器上,下次有人在那里提交时,旧历史都会被推送回来。每个克隆都必须在下次推送前执行:
    git fetch origin && git reset --hard origin/<branch>
    
    过期的克隆是这种重写被悄无声息地撤销的最常见原因。
  • 未关闭的 PR — 关闭或重新创建;它们引用的提交已不复存在。
  • Fork — 其中仍保留旧身份;重写无法影响它们。
  • 在每个主要托管平台上,旧提交在垃圾回收运行前仍可通过 SHA 直接访问,且平台不会承诺何时运行。如果必须让旧地址真正消失,可靠的途径是提交支持请求(GitHub Support 可以清除悬空提交),或删除并重新创建仓库——如果仓库即将公开,应在发布前而不是发布后进行。
  • .mailmap 文件无法解决这个问题。它只会改写本地 git 自身 输出所显示的内容;托管平台会读取提交对象中的原始邮箱并显示出来。这就是为什么必须更改历史记录。
  • 贡献图 — GitHub 只有在提交位于默认分支(或 gh-pages)上,且邮箱与账户关联时,才会将提交计入个人资料。提交页面上的归属信息可能已经正确显示,但贡献图需要一段时间才会更新。
  • 共同作者标记 — Co-Authored-By: 行位于提交 消息 中,重写身份不会触及它。如果旧地址也出现在那里,请说明这一点;修改它需要重写消息(filter-repo --replace-message),这是单独的决定。
  • 备份 — 告诉用户备份的位置,并说明远程端确认无误后可以删除。此技能绝不会删除备份。

安全准则

  • 不得触碰用户自己的工作副本。所有操作都在新的临时克隆和镜像备份中进行。
  • 在通过阶段 0 的确认关卡之前,只读操作。此前的所有操作都可以安全放弃。
  • 每个停止关卡都必须真正停止:新邮箱未经验证、所有者与身份不匹配、没有写入权限、分支受保护、提交数量发生变化、树差异非空,或未得到用户确认——出现任一情况,技能都必须停止。
  • 两条硬性规则绝对不可违反:新邮箱必须已在推送账户上验证,且仓库所有者必须与本地身份匹配。这两项都不能由 Agent 单方面决定。
  • 除非用户选择只重写其中一个,否则作者和提交者信息会一起重写;影响范围必须在操作前统计并告知,而不是操作后才告知。
  • 对分支使用 --force-with-lease,绝不使用裸 --force;标签必须一次强推一个,且只能推送用户指定的标签。
  • 此技能绝不会删除备份或用户未指定的任何远程引用。
  • 此操作会将用户控制的仓库中的提交重新归属到用户拥有的身份。它不能用于将他人的工作归属到另一个人;账户所有权关卡正是为了确保这一点。

评论

支持 Markdown