-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Use work trees instead of copying the whole directory
This should speed up backports by not copying the repository but create a clean work tree with the required branches set. Signed-off-by: Ferdinand Thiessen <[email protected]>
- Loading branch information
Showing
5 changed files
with
182 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,175 +1,170 @@ | ||
import { existsSync, rmSync } from 'node:fs' | ||
import { Octokit } from '@octokit/rest' | ||
|
||
import { cherryPickCommits, cloneAndCacheRepo, hasDiff, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils' | ||
import { CherryPickResult, Task } from './constants' | ||
import { debug, error, info, warn } from './logUtils' | ||
import { Reaction, addReaction, getAuthToken, getAvailableLabels, getLabelsFromPR, getAvailableMilestones, requestReviewers, getReviewers, createBackportPullRequest, setPRLabels, setPRMilestone, getChangesFromPR, updatePRBody, commentOnPR, assignToPR } from './githubUtils' | ||
import { getBackportBody, getFailureCommentBody, getLabelsForPR, getMilestoneFromBase } from './nextcloudUtils' | ||
|
||
export const backport = (task: Task) => new Promise<void>((resolve, reject) => { | ||
getAuthToken(task.installationId).then(async token => { | ||
const octokit = new Octokit({ auth: token }) | ||
|
||
let tmpDir: string = '' | ||
let prNumber: number = 0 | ||
let conflicts: CherryPickResult|null = null | ||
const backportBranch = `backport/${task.prNumber}/${task.branch}` | ||
|
||
info(task, `Starting backport request`) | ||
|
||
// Add a reaction to the comment to indicate that we're processing it | ||
import { cherryPickCommits, cloneAndCacheRepo, hasDiff, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils.js' | ||
import { CherryPickResult, Task } from './constants.js' | ||
import { debug, error, info, warn } from './logUtils.js' | ||
import { Reaction, addReaction, getAuthToken, getAvailableLabels, getLabelsFromPR, getAvailableMilestones, requestReviewers, getReviewers, createBackportPullRequest, setPRLabels, setPRMilestone, getChangesFromPR, updatePRBody, commentOnPR, assignToPR } from './githubUtils.js' | ||
import { getBackportBody, getFailureCommentBody, getLabelsForPR, getMilestoneFromBase } from './nextcloudUtils.js' | ||
|
||
export async function backport(task: Task): Promise<void> { | ||
const token = await getAuthToken(task.installationId) | ||
const octokit = new Octokit({ auth: token }) | ||
|
||
let tmpDir: string = '' | ||
let prNumber: number = 0 | ||
let conflicts: CherryPickResult|null = null | ||
const backportBranch = `backport/${task.prNumber}/${task.branch}` | ||
|
||
info(task, `Starting backport request`) | ||
|
||
// Add a reaction to the comment to indicate that we're processing it | ||
try { | ||
await addReaction(octokit, task, Reaction.THUMBS_UP) | ||
} catch (e) { | ||
error(task, `Failed to add reaction to PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
try { | ||
// Clone and cache the repo | ||
try { | ||
await addReaction(octokit, task, Reaction.THUMBS_UP) | ||
tmpDir = await cloneAndCacheRepo(task, backportBranch) | ||
info(task, `Cloned to ${tmpDir}`) | ||
} catch (e) { | ||
error(task, `Failed to add reaction to PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
throw new Error(`Failed to clone repository: ${e.message}`) | ||
} | ||
|
||
// Cherry pick the commits | ||
try { | ||
// Clone and cache the repo | ||
try { | ||
tmpDir = await cloneAndCacheRepo(task, backportBranch) | ||
info(task, `Cloned to ${tmpDir}`) | ||
} catch (e) { | ||
throw new Error(`Failed to clone repository: ${e.message}`) | ||
conflicts = await cherryPickCommits(task, tmpDir) | ||
if (conflicts === CherryPickResult.CONFLICTS) { | ||
warn(task, `Cherry picking commits resulted in conflicts`) | ||
} else { | ||
info(task, `Cherry picking commits successful`) | ||
} | ||
} catch (e) { | ||
throw new Error(`Failed to cherry pick commits: ${e.message}`) | ||
} | ||
|
||
// Cherry pick the commits | ||
try { | ||
conflicts = await cherryPickCommits(task, tmpDir) | ||
if (conflicts === CherryPickResult.CONFLICTS) { | ||
warn(task, `Cherry picking commits resulted in conflicts`) | ||
} else { | ||
info(task, `Cherry picking commits successful`) | ||
} | ||
} catch (e) { | ||
throw new Error(`Failed to cherry pick commits: ${e.message}`) | ||
} | ||
// Check if there are any changes to backport | ||
const hasChanges = await hasDiff(tmpDir, task.branch, backportBranch, task) | ||
if (!hasChanges) { | ||
throw new Error(`No changes found in backport branch`) | ||
} | ||
|
||
// Check if there are any changes to backport | ||
const hasChanges = await hasDiff(tmpDir, task.branch, backportBranch, task) | ||
if (!hasChanges) { | ||
throw new Error(`No changes found in backport branch`) | ||
} | ||
// Push the branch | ||
try { | ||
await pushBranch(task, tmpDir, token, backportBranch) | ||
info(task, `Pushed branch ${backportBranch}`) | ||
} catch (e) { | ||
throw new Error(`Failed to push branch ${backportBranch}: ${e.message}`) | ||
} | ||
|
||
// Push the branch | ||
try { | ||
await pushBranch(task, tmpDir, token, backportBranch) | ||
info(task, `Pushed branch ${backportBranch}`) | ||
} catch (e) { | ||
throw new Error(`Failed to push branch ${backportBranch}: ${e.message}`) | ||
} | ||
// Create the pull request | ||
try { | ||
const reviewers = await getReviewers(octokit, task) | ||
const prCreationResult = await createBackportPullRequest(octokit, task, backportBranch, conflicts === CherryPickResult.CONFLICTS) | ||
prNumber = prCreationResult.data.number | ||
info(task, `Opened Pull Request #${prNumber} on ${prCreationResult.data.html_url}`) | ||
|
||
// Create the pull request | ||
try { | ||
const reviewers = await getReviewers(octokit, task) | ||
const prCreationResult = await createBackportPullRequest(octokit, task, backportBranch, conflicts === CherryPickResult.CONFLICTS) | ||
prNumber = prCreationResult.data.number | ||
info(task, `Opened Pull Request #${prNumber} on ${prCreationResult.data.html_url}`) | ||
|
||
try { | ||
// Ask for reviews from all reviewers of the original PR | ||
if (reviewers.length !== 0) { | ||
await requestReviewers(octokit, task, prNumber, reviewers) | ||
} | ||
|
||
// Also ask the author of the original PR for a review | ||
await requestReviewers(octokit, task, prNumber, [task.author]) | ||
info(task, `Requested reviews from ${[...reviewers, task.author].join(', ')}`) | ||
} catch (e) { | ||
error(task, `Failed to request reviews: ${e.message}`) | ||
// Ask for reviews from all reviewers of the original PR | ||
if (reviewers.length !== 0) { | ||
await requestReviewers(octokit, task, prNumber, reviewers) | ||
} | ||
} catch (e) { | ||
throw new Error(`Failed to create pull request: ${e.message}`) | ||
} | ||
|
||
// Get labels from original PR and set them on the new PR | ||
try { | ||
const availableLabels = await getAvailableLabels(octokit, task) | ||
const prLabels = await getLabelsFromPR(octokit, task) | ||
const labels = getLabelsForPR(prLabels, availableLabels) | ||
await setPRLabels(octokit, task, prNumber, labels) | ||
info(task, `Set labels: ${labels.join(', ')}`) | ||
// Also ask the author of the original PR for a review | ||
await requestReviewers(octokit, task, prNumber, [task.author]) | ||
info(task, `Requested reviews from ${[...reviewers, task.author].join(', ')}`) | ||
} catch (e) { | ||
error(task, `Failed to get and set labels: ${e.message}`) | ||
// continue, this is not a fatal error | ||
error(task, `Failed to request reviews: ${e.message}`) | ||
} | ||
} catch (e) { | ||
throw new Error(`Failed to create pull request: ${e.message}`) | ||
} | ||
|
||
// Find new appropriate Milestone and set it on the new PR | ||
try { | ||
const availableMilestone = await getAvailableMilestones(octokit, task) | ||
const milestone = await getMilestoneFromBase(task.branch, availableMilestone) | ||
await setPRMilestone(octokit, task, prNumber, milestone) | ||
info(task, `Set milestone: ${milestone.title}`) | ||
} catch (e) { | ||
error(task, `Failed to find appropriate milestone: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
// Get labels from original PR and set them on the new PR | ||
try { | ||
const availableLabels = await getAvailableLabels(octokit, task) | ||
const prLabels = await getLabelsFromPR(octokit, task) | ||
const labels = getLabelsForPR(prLabels, availableLabels) | ||
await setPRLabels(octokit, task, prNumber, labels) | ||
info(task, `Set labels: ${labels.join(', ')}`) | ||
} catch (e) { | ||
error(task, `Failed to get and set labels: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
// Assign the PR to the author of the original PR | ||
try { | ||
await assignToPR(octokit, task, prNumber, [task.author]) | ||
info(task, `Assigned original author: ${task.author}`) | ||
} catch (e) { | ||
error(task, `Failed to assign PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
// Find new appropriate Milestone and set it on the new PR | ||
try { | ||
const availableMilestone = await getAvailableMilestones(octokit, task) | ||
const milestone = await getMilestoneFromBase(task.branch, availableMilestone) | ||
await setPRMilestone(octokit, task, prNumber, milestone) | ||
info(task, `Set milestone: ${milestone.title}`) | ||
} catch (e) { | ||
error(task, `Failed to find appropriate milestone: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
// Assign the PR to the author of the original PR | ||
try { | ||
await assignToPR(octokit, task, prNumber, [task.author]) | ||
info(task, `Assigned original author: ${task.author}`) | ||
} catch (e) { | ||
error(task, `Failed to assign PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
// Compare the original PR with the new PR | ||
// Compare the original PR with the new PR | ||
try { | ||
const oldChanges = await getChangesFromPR(octokit, task, task.prNumber) | ||
const newChanges = await getChangesFromPR(octokit, task, prNumber) | ||
const diffChanges = oldChanges.additions !== newChanges.additions | ||
|| oldChanges.deletions !== newChanges.deletions | ||
|| oldChanges.changedFiles !== newChanges.changedFiles | ||
const skipCi = await hasSkipCiCommits(tmpDir, task.commits.length) | ||
const emptyCommits = await hasEmptyCommits(tmpDir, task.commits.length, task) | ||
const hasConflicts = conflicts === CherryPickResult.CONFLICTS | ||
|
||
debug(task, `hasConflicts: ${hasConflicts}, diffChanges: ${diffChanges}, emptyCommits: ${emptyCommits}, skipCi: ${skipCi}`) | ||
try { | ||
const oldChanges = await getChangesFromPR(octokit, task, task.prNumber) | ||
const newChanges = await getChangesFromPR(octokit, task, prNumber) | ||
const diffChanges = oldChanges.additions !== newChanges.additions | ||
|| oldChanges.deletions !== newChanges.deletions | ||
|| oldChanges.changedFiles !== newChanges.changedFiles | ||
const skipCi = await hasSkipCiCommits(tmpDir, task.commits.length) | ||
const emptyCommits = await hasEmptyCommits(tmpDir, task.commits.length, task) | ||
const hasConflicts = conflicts === CherryPickResult.CONFLICTS | ||
|
||
debug(task, `hasConflicts: ${hasConflicts}, diffChanges: ${diffChanges}, emptyCommits: ${emptyCommits}, skipCi: ${skipCi}`) | ||
try { | ||
if (hasConflicts || diffChanges || emptyCommits || skipCi) { | ||
const newBody = await getBackportBody(task.prNumber, hasConflicts, diffChanges, emptyCommits, skipCi) | ||
await updatePRBody(octokit, task, prNumber, newBody) | ||
} | ||
} catch (e) { | ||
error(task, `Failed to update PR body: ${e.message}`) | ||
// continue, this is not a fatal error | ||
if (hasConflicts || diffChanges || emptyCommits || skipCi) { | ||
const newBody = await getBackportBody(task.prNumber, hasConflicts, diffChanges, emptyCommits, skipCi) | ||
await updatePRBody(octokit, task, prNumber, newBody) | ||
} | ||
} catch (e) { | ||
error(task, `Failed to compare changes: ${e.message}`) | ||
error(task, `Failed to update PR body: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
// Success! We're done here | ||
addReaction(octokit, task, Reaction.HOORAY) | ||
} catch (e) { | ||
// Add a thumbs down reaction to the comment to indicate that we failed | ||
try { | ||
addReaction(octokit, task, Reaction.THUMBS_DOWN) | ||
const failureComment = getFailureCommentBody(task, backportBranch, e?.message) | ||
await commentOnPR(octokit, task, failureComment) | ||
} catch (e) { | ||
error(task, `Failed to comment failure on PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
error(task, `Failed to compare changes: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
reject(`Failed to backport: ${e.message}`) | ||
// Success! We're done here | ||
addReaction(octokit, task, Reaction.HOORAY) | ||
} catch (e) { | ||
// Add a thumbs down reaction to the comment to indicate that we failed | ||
try { | ||
addReaction(octokit, task, Reaction.THUMBS_DOWN) | ||
const failureComment = getFailureCommentBody(task, backportBranch, e?.message) | ||
await commentOnPR(octokit, task, failureComment) | ||
} catch (e) { | ||
error(task, `Failed to comment failure on PR: ${e.message}`) | ||
// continue, this is not a fatal error | ||
} | ||
|
||
// Remove the temp dir if it exists | ||
if (tmpDir !== '' && existsSync(tmpDir)) { | ||
try { | ||
rmSync(tmpDir, { recursive: true }) | ||
info(task, `Removed ${tmpDir}`) | ||
resolve() | ||
} catch (e) { | ||
reject(`Failed to remove ${tmpDir}: ${e.message}`) | ||
} | ||
throw new Error(`Failed to backport: ${e.message}`) | ||
} | ||
|
||
// Remove the temp dir if it exists | ||
if (tmpDir !== '' && existsSync(tmpDir)) { | ||
try { | ||
rmSync(tmpDir, { recursive: true }) | ||
info(task, `Removed ${tmpDir}`) | ||
} catch (e) { | ||
throw new Error(`Failed to remove ${tmpDir}: ${e.message}`) | ||
} | ||
}) | ||
}).catch(e => { | ||
error(task, e) | ||
throw e | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.