使用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对象不会生效。
修复方案
- 拉取Tree时添加
recursive: "true"参数,获取仓库全量路径条目 - 修正路径匹配规则:匹配以
${directoryName}/开头的路径,避免误删同名文件 - 调用
git.createTree接口,基于当前基准Tree生成包含删除变更的新Tree,拿到新Tree的SHA后再创建提交 - 保留父提交参数,确保提交链连续,避免快进报错
修复后的完整可运行代码:
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
相关产品推荐
相关产品推荐

