GitHub 关闭 PR 后,如何找回 Pull Request 会话历史

约 7 分钟 #开发 #GitHub #数据恢复
杨秋逸
杨秋逸

最近回头找自己提交过的一个 Obsidian 插件 PR:obsidianmd/obsidian-releases#5839。这个仓库后来关闭了 Pull requests 模块,网页中的 PR 链接、REST API,甚至按编号的 GraphQL 查询都失效了。

我原本打算从 GH Archive 的事件归档里慢慢拼回对话。最后发现,更有效的路径是:先从第三方索引找回 PR 的 GitHub 全局对象 ID(node_id),再通过 GraphQL 的 node(id:) 查询读取原始 PR 对象。

这篇文章记录这个排查过程,也说明 GH Archive 在其中真正适合做什么。

现象:PR 编号还在,PR 却“消失”了

代码提交还保留在仓库历史中:

0ee35c9f chore: Add plugin Nutstore Sync (#5839)

但常规 GitHub 接口已经不可用:

curl https://api.github.com/repos/obsidianmd/obsidian-releases/pulls/5839
# {"message":"Not Found", ...}

curl https://api.github.com/repos/obsidianmd/obsidian-releases/issues/5839/comments
# {"message":"Pull requests are disabled for this repo", ...}

gh api graphql 中的 repository.pullRequest(number: 5839) 也是同样的结果:Pull requests are disabled for this repository.

这时不要因为 URL 失效就认定数据被删除了。关闭模块更像是让“按仓库 + 编号”的入口不可访问;GitHub 内部的对象是否还存在,是另一回事。

第一步:从 Git 历史确定锚点

我最开始并不知道 PR 编号,只知道当时使用的 GitHub 用户名是 nutstore-dev,于是可以先从 Git 历史反查:

git log --all --author='你的名字或邮箱' --oneline

然后再对目标提交查看完整元数据:

git show --format=fuller --stat 0ee35c9f

这一步从 commit message 中找到了 (#5839),并得到如下最小事实集:

  • PR:#5839
  • 标题:Add plugin: Nutstore Sync
  • 合入提交:0ee35c9f3c86a4d027daaa1f611f0227d345d029
  • 合入时间:2025-11-28

这些信息一方面可用于在归档中缩小时间范围,另一方面可避免把相邻编号或同名提交误当成目标。

第二步:从 Ecosyste.ms 找回 node_id

Ecosyste.ms 是一个独立于 GitHub 的开源数据服务集合。它索引公共开源项目的仓库、Issue、PR 和活动元数据;其中的 issues.ecosyste.ms 并不等同于 GitHub 的实时 API,数据可能有延迟,但在本例中恰好保留了目标 PR 的元数据。

curl -sS \
  'https://issues.ecosyste.ms/api/v1/hosts/GitHub/repositories/obsidianmd%2Fobsidian-releases/issues/5839' \
  | jq '{number, title, user, created_at, node_id, comments_count}'

输出中最关键的一项是:

{
    "number": 5839,
    "title": "Add plugin: Nutstore Sync",
    "node_id": "PR_kwDOD6MHws6QfHNP"
}

node_id 是 GitHub GraphQL 使用的全局对象 ID。PR_ 前缀表明对象类型是 Pull Request;其余字符是 GitHub 的不透明内部标识,不应尝试自行编码或解码。

这里的 Ecosyste.ms 只是提供了线索,不能把它当作完整备份:它当时只显示 6 条评论,而且状态仍是旧的 open。这说明第三方索引适合恢复定位信息,不能替代原始数据源。

第三步:改用 GraphQL 的全局对象查询

拿到 node_id 后,不再按仓库和编号查找,而是直接查询全局对象:

gh api graphql \
  -f query='query($id: ID!) {
    node(id: $id) {
      __typename
      ... on PullRequest {
        number
        title
        state
        merged
        createdAt
        updatedAt
        mergedAt
        body
        author { login }
        comments(first: 100) {
          totalCount
          nodes {
            author { login }
            body
            createdAt
            updatedAt
            url
          }
        }
        reviews(first: 100) {
          totalCount
          nodes {
            author { login }
            body
            state
            submittedAt
            url
          }
        }
        reviewThreads(first: 100) {
          totalCount
          nodes {
            isResolved
            comments(first: 100) {
              nodes {
                author { login }
                body
                path
                line
                createdAt
                url
              }
            }
          }
        }
      }
    }
  }' \
  -F id='PR_kwDOD6MHws6QfHNP'

这次成功返回了对象。它确认 #5839 已合并,并找回了 PR 描述和 24 条普通会话评论;该 PR 没有正式 review 或行级 review thread。

这条路的关键不是“绕过权限”。查询使用的是 GitHub 正常的 GraphQL 全局对象接口,只是仓库设置让按编号的入口失效了,而目标对象在查询时仍可通过其 ID 解析。GitHub 随时可能改变这一行为,因此应在成功后立即导出结果并保存。

GH Archive 应该放在什么位置?

GH Archive 按小时保存公开 GitHub 事件。它非常适合做事件取证和交叉验证,相关类型包括:

  • PullRequestEvent:创建、关闭、合入、打标签等 PR 生命周期事件
  • IssueCommentEvent:PR Conversation 中的普通评论
  • PullRequestReviewEvent:Review 的正文与状态
  • PullRequestReviewCommentEvent:代码行级评论

例如已经知道 PR 开始活动的日期时,可以先只查询单日,避免 BigQuery 扫描太多数据:

SELECT
  type,
  created_at,
  actor.login AS actor,
  JSON_VALUE(payload, '$.action') AS action,
  COALESCE(
    JSON_VALUE(payload, '$.number'),
    JSON_VALUE(payload, '$.issue.number'),
    JSON_VALUE(payload, '$.pull_request.number')
  ) AS pr_number,
  JSON_VALUE(payload, '$.comment.body') AS comment_body,
  JSON_VALUE(payload, '$.review.body') AS review_body
FROM `githubarchive.day.20250328`
WHERE repo.name = 'obsidianmd/obsidian-releases'
  AND type IN (
    'PullRequestEvent',
    'IssueCommentEvent',
    'PullRequestReviewEvent',
    'PullRequestReviewCommentEvent'
  )
  AND COALESCE(
    JSON_VALUE(payload, '$.number'),
    JSON_VALUE(payload, '$.issue.number'),
    JSON_VALUE(payload, '$.pull_request.number')
  ) = '5839'
ORDER BY created_at;

GH Archive 的优势是事件在当时已经被归档,不依赖 GitHub 现在是否还展示对象;不足是它是事件流,不是 PR 的完整快照。要重建一段很长的会话,需要知道所有活跃日期、逐日查询,并处理创建、编辑和删除事件。它适合作为“最后的取证来源”,不应是已经能访问 GitHub 原始对象时的首选。

参考