octokit-plugin-create-pull-request
Version:
Octokit plugin to create a pull request with multiple file changes
367 lines (360 loc) • 9.57 kB
JavaScript
// 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
};