Search code examples
typescriptgithubgithub-apioctokit-js

Deleting folder from github using octokit/rest


I'm trying to use octokit/rest to remove a directory programaticcaly. I have the following code

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)
}

When I call deleteDirectoryFromGithubRepo, it commits an empty commit to github. That is, it created a commit but without any changes in it.

I read that passing the current commit might be repopulating the deleted files. But removing it results in an error Update is not a fast forward

Full reponse from 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": {}
  }
}

I'm using octokit-rest.js/18.12.0

Any idea how to fix this?


Solution

  • An answer was provided here here and a working example of deleting a directory can be found below

    const deleteFromRepo = async (
        github: Github,
        org: string,
        repo: string,
        directoryName: string
    ) => {
        const currentCommit = await getCurrentCommit(github, org, repo);
    
        const {data: repoTree} = await github.git.getTree({
            owner: org,
            repo,
            tree_sha: currentCommit.treeSha
        })
    
        const directorySha = await getDirectorySha(repoTree.tree, directoryName);
    
        if (!directorySha) {
            throw new Error(`Could not find an directory '${directoryName}'`)
        }
    
        const {data: directoryTree} = await github.git.getTree({
            owner: org,
            repo,
            tree_sha: directorySha
        })
    
        const blobs: Blob[] = directoryTree.tree.map((blob) => {
            return {'url': `${directoryName}/${blob.path}`, 'sha': null}
        });
    
        const newTree = await createNewTree(
            github,
            org,
            repo,
            blobs,
            currentCommit.treeSha
        )
    
        const commitMessage = `Deleting '${directoryName}' files.`
        const newCommit = await createNewCommit(
            github,
            org,
            repo,
            commitMessage,
            newTree.sha,
            currentCommit.commitSha
        )
        await setBranchToCommit(github, org, repo, newCommit.sha)
    }