UNPKG

octokit-plugin-create-pull-request

Version:

Octokit plugin to create a pull request with multiple file changes

367 lines (360 loc) 9.57 kB
// pkg/dist-src/value-to-tree-object.js async function valueToTreeObject(octokit, owner, repo, path, value) { const defaultMode = "100644"; if (typeof value === "string") { return { path, mode: defaultMode, content: value }; } const mode = value.mode ?? defaultMode; if (value.encoding === "utf-8") { return { path, mode, content: value.content }; } const { data } = await octokit.request( "POST /repos/{owner}/{repo}/git/blobs", { owner, repo, ...value } ); const blobSha = data.sha; return { path, mode, sha: blobSha }; } // pkg/dist-src/constants.js var DELETE_FILE = Symbol("DELETE_FILE"); // pkg/dist-src/create-tree.js 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) { try { 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; } } 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 { 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; } } } catch (error) { if (error.status !== 404) throw error; 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; } tree.push(await valueToTreeObject(octokit, ownerOrFork, repo, path, value)); continue; } tree = tree.filter(Boolean); if (tree.length === 0) { return null; } const { data: { sha: newTreeSha } } = await octokit.request("POST /repos/{owner}/{repo}/git/trees", { owner: ownerOrFork, repo, base_tree: latestCommitTreeSha, tree }); return newTreeSha; } // pkg/dist-src/create-commit.js 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] }; const { data: latestCommit } = await octokit.request( "POST /repos/{owner}/{repo}/git/commits", { owner: ownerOrFork, repo, ...commit, signature: changes.signature ? await changes.signature(commit) : void 0 } ); return latestCommit.sha; } // pkg/dist-src/compose-create-pull-request.js 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 }; 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) { const user = await octokit.request("GET /user"); const forks = await octokit.request("GET /repos/{owner}/{repo}/forks", { owner, repo }); const hasFork = forks.data.find( /* v8 ignore next - fork owner can be null, but we don't test that */ (fork) => fork.owner && fork.owner.login === user.data.login ); if (!hasFork) { await octokit.request("POST /repos/{owner}/{repo}/forks", { owner, repo }); } state.ownerOrFork = user.data.login; } 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 getPullRequestsForBranch($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) { await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", { owner: state.ownerOrFork, repo, sha: state.latestCommitSha, ref: `heads/${head}`, force: true }); } else { 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) { res = await octokit.request( "PATCH /repos/{owner}/{repo}/pulls/{pull_number}", { pull_number: existingPullRequest.number, ...pullRequestOptions } ); } else { 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 } ); 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) { 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; } if (error.status !== 403) throw error; } } return res; } // pkg/dist-src/version.js var VERSION = "0.0.0-development"; // pkg/dist-src/index.js function createPullRequest(octokit) { return { createPullRequest: composeCreatePullRequest.bind(null, octokit) }; } createPullRequest.VERSION = VERSION; export { DELETE_FILE, composeCreatePullRequest, createPullRequest };