You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用octokit/rest删除GitHub目录时空提交、非快进错误如何解决

问题描述

我尝试使用octokit/rest以编程方式删除GitHub仓库中的目录,实现代码如下:

import {Octokit as Github} from '@octokit/rest';

const githubToken = "read from vault"

const getCurrentCommit = async (
    github: Github,
    org: string,
    repo: string,
    branch: string = 'main'
) => {
  const {data: refData} = await github.git.getRef({
    owner: org,
    repo,
    ref: `heads/${branch}`,
  })
  const commitSha = refData.object.sha
  const {data: commitData} = await github.git.getCommit({
    owner: org,
    repo,
    commit_sha: commitSha,
  })
  return {
    commitSha,
    treeSha: commitData.tree.sha,
  }
}

const createNewCommit = async (
    github: Github,
    org: string,
    repo: string,
    message: string,
    currentTreeSha: string,
    currentCommitSha: string
) => {
  const {data} = await github.git.createCommit({
    owner: org,
    repo,
    message,
    tree: currentTreeSha,
    parents: [currentCommitSha],
  });

  return data;
}

const setBranchToCommit = (
    github: Github,
    org: string,
    repo: string,
    commitSha: string,
    branch: string = `main`
) => github.git.updateRef({
  owner: org,
  repo,
  ref: `heads/${branch}`,
  sha: commitSha,
});

const getdirectorySha = async (
    tree: { path?: string, sha?: string }[],
    directoryName: string
) => {

  return tree
  .filter(({path: directoryPath}) => directoryPath ? directoryPath.includes(directoryName) : false)
  .map(({sha}) => sha)
  .filter(sha => sha !== undefined).values().next().value
};

const deleteFromRepo = async (
    github: Github,
    org: string,
    repo: string,
    directoryName: string,
    branch: string = `main`
) => {
  const currentCommit = await getCurrentCommit(github, org, repo, branch);

  const {data: treeData} = await github.git.getTree({
    owner: org,
    repo,
    tree_sha: currentCommit.treeSha
  })

  const directorySha = await getdirectorySha(treeData.tree, directoryName);

  if (!directorySha) {
    throw new Error(`Could not find an directory '${directoryName}'`)
  }
  
  // Skip the files in the directory I need to remove
  treeData.tree = treeData.tree
  .filter(({path: directoryPath}) => directoryPath && !(directoryPath.includes(directoryName)))
  .map(({path: directoryPath, sha}) => {
    return {path: directoryPath, sha}
  });

  // For some reason, this repopulates the files I just skipped
  const commitMessage = `Deleting '${directoryName}' files.`
  const newCommit = await createNewCommit(
      github,
      org,
      repo,
      commitMessage,
      treeData.sha,
      currentCommit.commitSha
  )
  await setBranchToCommit(github, org, repo, newCommit.sha, branch)
}

const checkRepositoryExists = (
    organisationRepos: any,
    repoName: string) => {
  if (!organisationRepos.data.map((repo: { name: any }) => repo.name).includes(repoName)) {
    let errorMessage = `Repo '${repoName}' does not exist`;
    console.log(errorMessage);
    throw new Error(errorMessage);
  }
};

export const deletedirectoryFromGithubRepo = async (
    directoryName: string
) => {
  const github = new Github({auth: githubToken})
  const organization = `myOrg`
  const repoName = `myRepo`
  const organisationRepos = await github.repos.listForOrg({org: organization})

  checkRepositoryExists(organisationRepos, repoName);

  await deleteFromRepo(github, organization, repoName, directoryName)
}

调用deleteDirectoryFromGithubRepo方法时,GitHub上仅生成了无任何变更的空提交。
查阅资料得知传入当前提交可能导致已删除的文件被重新填充,但移除当前提交参数后会触发Update is not a fast forward错误。GitHub返回的完整错误响应如下:

{
  "name": "HttpError",
  "status": 422,
  "response": {
    "url": "https://api.github.com/repos/myOrg/myRepo/git/refs/heads%2Fmain",
    "status": 422,
    "headers": {
      "header1": "*",
      "header2": "*"
    },
    "data": {
      "message": "Update is not a fast forward",
      "documentation_url": "https://docs.github.com/rest/reference/git#update-a-reference"
    }
  },
  "request": {
    "method": "PATCH",
    "url": "https://api.github.com/repos/myOrg/myRepo/git/refs/heads%2Fmain",
    "headers": {
      "accept": "application/vnd.github.v3+json",
      "user-agent": "octokit-rest.js/18.12.0 octokit-core.js/3.6.0 Node.js/14.19.3 (linux; x64)",
      "authorization": "token [REDACTED]",
      "content-type": "application/json; charset=utf-8"
    },
    "body": "{\"sha\":\"75dd3**************ee3da188d4\"}",
    "request": {}
  }
}

当前使用的版本为octokit-rest.js/18.12.0。

问题根因
  • 空提交问题:代码直接复用了旧提交的Tree SHA创建新提交,没有调用createTree接口生成包含删除变更的新Tree,提交内容和之前完全一致。同时默认调用getTree时未加递归参数,只能拿到根目录一级条目,无法获取目标目录下的嵌套文件。
  • 路径匹配逻辑错误:用字符串includes匹配目录名会误判同名文件,比如删除test目录时会把test.md这类同级同名文件也纳入删除范围,且无法精准匹配目录下的嵌套路径。
  • 快进报错问题:创建提交时未正确关联最新父提交,或新提交的提交链断裂,GitHub更新分支引用时会拒绝非快进的推送。
  • 删除逻辑不符合API规则:GitHub Git Tree API删除文件/目录时,要么显式给要删除的路径传sha: null,要么基于基准Tree传入过滤后的全量有效路径生成新Tree,直接修改接口返回的Tree对象不会生效。
修复方案
  1. 拉取Tree时添加recursive: "true"参数,获取仓库全量路径条目
  2. 修正路径匹配规则:匹配以${directoryName}/开头的路径,避免误删同名文件
  3. 调用git.createTree接口,基于当前基准Tree生成包含删除变更的新Tree,拿到新Tree的SHA后再创建提交
  4. 保留父提交参数,确保提交链连续,避免快进报错

修复后的完整可运行代码:

import {Octokit as Github} from '@octokit/rest';

const githubToken = "read from vault"

const getCurrentCommit = async (
    github: Github,
    org: string,
    repo: string,
    branch: string = 'main'
) => {
  const {data: refData} = await github.git.getRef({
    owner: org,
    repo,
    ref: `heads/${branch}`,
  })
  const commitSha = refData.object.sha
  const {data: commitData} = await github.git.getCommit({
    owner: org,
    repo,
    commit_sha: commitSha,
  })
  return {
    commitSha,
    treeSha: commitData.tree.sha,
  }
}

const deleteFromRepo = async (
    github: Github,
    org: string,
    repo: string,
    directoryName: string,
    branch: string = `main`
) => {
  const currentCommit = await getCurrentCommit(github, org, repo, branch);
  // 拉取全量递归tree
  const {data: treeData} = await github.git.getTree({
    owner: org,
    repo,
    tree_sha: currentCommit.treeSha,
    recursive: "true"
  })

  // 精准匹配目标目录下的所有路径,避免误判
  const dirPathPrefix = `${directoryName}/`;
  const dirRelatedEntries = treeData.tree.filter(entry => entry.path?.startsWith(dirPathPrefix));
  if (dirRelatedEntries.length === 0) {
    throw new Error(`Could not find directory '${directoryName}'`)
  }

  // 构造删除操作的tree条目:要删除的路径sha设为null
  const deleteTreeEntries = dirRelatedEntries.map(entry => ({
    path: entry.path!,
    sha: null
  }));

  // 生成新的tree
  const {data: newTreeData} = await github.git.createTree({
    owner: org,
    repo,
    base_tree: currentCommit.treeSha,
    tree: deleteTreeEntries
  });

  // 创建新提交
  const commitMessage = `Delete directory '${directoryName}'`;
  const {data: newCommit} = await github.git.createCommit({
    owner: org,
    repo,
    message: commitMessage,
    tree: newTreeData.sha,
    parents: [currentCommit.commitSha],
  });

  // 更新分支指向
  await github.git.updateRef({
    owner: org,
    repo,
    ref: `heads/${branch}`,
    sha: newCommit.sha,
  });
}

const checkRepositoryExists = async (
    github: Github,
    org: string,
    repoName: string
) => {
  try {
    await github.repos.get({owner: org, repo: repoName});
  } catch (e) {
    throw new Error(`Repo '${repoName}' does not exist`);
  }
};

export const deleteDirectoryFromGithubRepo = async (
    directoryName: string
) => {
  const github = new Github({auth: githubToken})
  const organization = `myOrg`
  const repoName = `myRepo`

  await checkRepositoryExists(github, organization, repoName);
  await deleteFromRepo(github, organization, repoName, directoryName)
}

注意:如果目录下文件超过1000个,递归拉取tree会截断,需要分批处理,常规场景下上述代码可直接使用。

内容的提问来源于stack exchange,提问作者Matto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 05:55:15