async function valueToTreeObject(octokit, owner, repo, path, value) { const defaultMode = "100644"; // Text files can be changed through the .content key if (typeof value === "string") { return { path, mode: defaultMode, content: value, }; } const mode = value.mode ?? defaultMode; // UTF-8 files can be treated as text files // https://github.com/gr2m/octokit-plugin-create-pull-request/pull/133 if (value.encoding === "utf-8") { return { path, mode: mode, content: value.content, }; } // Binary files need to be created first using the git blob API, // then changed by referencing in the .sha key const { data } = await octokit.request("POST /repos/{owner}/{repo}/git/blobs", { owner, repo, ...value, }); const blobSha = data.sha; return { path, mode: mode, sha: blobSha, }; } const DELETE_FILE = Symbol("DELETE_FILE"); async function createTree(state, changes) { const { octokit, owner, repo, ownerOrFork, latestCommitSha, latestCommitTreeSha, } = state; let tree = []; for (const path of Object.keys(changes.files)) { const value = changes.files[path]; if (value === DELETE_FILE) { // Deleting a non-existent file from a tree leads to an "GitRPC::BadObjectState" error, // so we only attempt to delete the file if it exists. try { // https://developer.github.com/v3/repos/contents/#get-contents await octokit.request("HEAD /repos/{owner}/{repo}/contents/:path", { owner: ownerOrFork, repo, ref: latestCommitSha, path, }); tree.push({ path, mode: "100644", sha: null, }); continue; } catch (error) { continue; } } // When passed a function, retrieve the content of the file, pass it // to the function, then return the result if (typeof value === "function") { let result; try { const { data: file } = await octokit.request("GET /repos/{owner}/{repo}/contents/:path", { owner: ownerOrFork, repo, ref: latestCommitSha, path, }); result = await value(Object.assign(file, { exists: true })); if (result === DELETE_FILE) { try { // https://developer.github.com/v3/repos/contents/#get-contents await octokit.request("HEAD /repos/{owner}/{repo}/contents/:path", { owner: ownerOrFork, repo, ref: latestCommitSha, path, }); tree.push({ path, mode: "100644", sha: null, }); continue; } catch (error) { // istanbul ignore next continue; } } } catch (error) { // @ts-ignore // istanbul ignore if if (error.status !== 404) throw error; // @ts-ignore result = await value({ exists: false }); } if (result === null || typeof result === "undefined" || typeof result === "symbol") { continue; } tree.push( // @ts-expect-error - Argument result can never be of type Symbol at this branch // because the above condition will catch it and move on to the next iteration cycle await valueToTreeObject(octokit, ownerOrFork, repo, path, result)); continue; } // @ts-expect-error - Argument value can never be of type Symbol at this branch // because the above condition will catch it and initiate a file deletion operation tree.push(await valueToTreeObject(octokit, ownerOrFork, repo, path, value)); continue; } tree = tree.filter(Boolean); if (tree.length === 0) { return null; } // https://developer.github.com/v3/git/trees/#create-a-tree const { data: { sha: newTreeSha }, } = await octokit.request("POST /repos/{owner}/{repo}/git/trees", { owner: ownerOrFork, repo, base_tree: latestCommitTreeSha, tree, }); return newTreeSha; } async function createCommit(state, treeCreated, changes) { const { octokit, repo, ownerOrFork, latestCommitSha } = state; const message = treeCreated ? changes.commit : typeof changes.emptyCommit === "string" ? changes.emptyCommit : changes.commit; const commit = { message, author: changes.author, committer: changes.committer, tree: state.latestCommitTreeSha, parents: [latestCommitSha], }; // https://developer.github.com/v3/git/commits/#create-a-commit const { data: latestCommit } = await octokit.request("POST /repos/{owner}/{repo}/git/commits", { owner: ownerOrFork, repo, ...commit, signature: changes.signature ? await changes.signature(commit) : undefined, }); return latestCommit.sha; } async function composeCreatePullRequest(octokit, { owner, repo, title, body, base, head, createWhenEmpty, changes: changesOption, draft = false, labels = [], forceFork = false, update = false, }) { if (head === base) { throw new Error('[octokit-plugin-create-pull-request] "head" cannot be the same value as "base"'); } const changes = Array.isArray(changesOption) ? changesOption : [changesOption]; if (changes.length === 0) throw new Error('[octokit-plugin-create-pull-request] "changes" cannot be an empty array'); const state = { octokit, owner, repo }; // https://developer.github.com/v3/repos/#get-a-repository const { data: repository, headers } = await octokit.request("GET /repos/{owner}/{repo}", { owner, repo, }); const isUser = !!headers["x-oauth-scopes"]; if (!repository.permissions) { throw new Error("[octokit-plugin-create-pull-request] Missing authentication"); } if (!base) { base = repository.default_branch; } state.ownerOrFork = owner; if (forceFork || (isUser && !repository.permissions.push)) { // https://developer.github.com/v3/users/#get-the-authenticated-user const user = await octokit.request("GET /user"); // https://developer.github.com/v3/repos/forks/#list-forks const forks = await octokit.request("GET /repos/{owner}/{repo}/forks", { owner, repo, }); const hasFork = forks.data.find( /* istanbul ignore next - fork owner can be null, but we don't test that */ (fork) => fork.owner && fork.owner.login === user.data.login); if (!hasFork) { // https://developer.github.com/v3/repos/forks/#create-a-fork await octokit.request("POST /repos/{owner}/{repo}/forks", { owner, repo, }); } state.ownerOrFork = user.data.login; } // https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository const { data: [latestCommit], } = await octokit.request("GET /repos/{owner}/{repo}/commits", { owner, repo, sha: base, per_page: 1, }); state.latestCommitSha = latestCommit.sha; state.latestCommitTreeSha = latestCommit.commit.tree.sha; const baseCommitTreeSha = latestCommit.commit.tree.sha; for (const change of changes) { let treeCreated = false; if (change.files && Object.keys(change.files).length) { const latestCommitTreeSha = await createTree(state, change); if (latestCommitTreeSha) { state.latestCommitTreeSha = latestCommitTreeSha; treeCreated = true; } } if (treeCreated || change.emptyCommit !== false) { state.latestCommitSha = await createCommit(state, treeCreated, change); } } const hasNoChanges = baseCommitTreeSha === state.latestCommitTreeSha; if (hasNoChanges && createWhenEmpty === false) { return null; } const branchInfo = await octokit.graphql(` query ($owner: String!, $repo: String!, $head: String!) { repository(name: $repo, owner: $owner) { ref(qualifiedName: $head) { associatedPullRequests(first: 1, states: OPEN) { edges { node { id number url } } } } } }`, { owner: state.ownerOrFork, repo, head, }); const branchExists = !!branchInfo.repository.ref; const existingPullRequest = branchInfo.repository.ref?.associatedPullRequests?.edges?.[0]?.node; if (existingPullRequest && !update) { throw new Error(`[octokit-plugin-create-pull-request] Pull request already exists: ${existingPullRequest.url}. Set update=true to enable updating`); } if (branchExists) { // https://docs.github.com/en/rest/git/refs#update-a-reference await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", { owner: state.ownerOrFork, repo, sha: state.latestCommitSha, ref: `heads/${head}`, force: true, }); } else { // https://developer.github.com/v3/git/refs/#create-a-reference await octokit.request("POST /repos/{owner}/{repo}/git/refs", { owner: state.ownerOrFork, repo, sha: state.latestCommitSha, ref: `refs/heads/${head}`, }); } const pullRequestOptions = { owner, repo, head: `${state.ownerOrFork}:${head}`, base, title, body, draft, }; let res; if (existingPullRequest) { // https://docs.github.com/en/rest/pulls/pulls#update-a-pull-request res = await octokit.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", { pull_number: existingPullRequest.number, ...pullRequestOptions, }); } else { // https://developer.github.com/v3/pulls/#create-a-pull-request res = await octokit.request("POST /repos/{owner}/{repo}/pulls", pullRequestOptions); } if (labels.length) { try { const labelRes = await octokit.request("POST /repos/{owner}/{repo}/issues/{number}/labels", { owner, repo, number: res.data.number, labels, }); // istanbul ignore if if (labelRes.data.length > labels.length) { octokit.log.warn("The pull request already contains more labels than the ones provided. This could be due to the presence of previous labels."); } } catch (error) { // @ts-ignore // istanbul ignore if if (error.status === 403) { octokit.log.warn("You do not have permissions to apply labels to this pull request. However, the pull request has been successfully created without the requested labels."); return res; } // @ts-ignore if (error.status !== 403) throw error; } } return res; } const VERSION = "5.1.1"; /** * @param octokit Octokit instance */ function createPullRequest(octokit) { return { createPullRequest: composeCreatePullRequest.bind(null, octokit), }; } createPullRequest.VERSION = VERSION; export { DELETE_FILE, composeCreatePullRequest, createPullRequest }; //# sourceMappingURL=index.js.map