Skip to content

Commit 48e0945

Browse files
authored
Merge pull request #553 from nextcloud/feat/use-work-trees
2 parents 6c1ba1b + 4bc27eb commit 48e0945

File tree

5 files changed

+182
-191
lines changed

5 files changed

+182
-191
lines changed

src/backport.ts

Lines changed: 139 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,170 @@
11
import { existsSync, rmSync } from 'node:fs'
22
import { Octokit } from '@octokit/rest'
33

4-
import { cherryPickCommits, cloneAndCacheRepo, hasDiff, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils'
5-
import { CherryPickResult, Task } from './constants'
6-
import { debug, error, info, warn } from './logUtils'
7-
import { Reaction, addReaction, getAuthToken, getAvailableLabels, getLabelsFromPR, getAvailableMilestones, requestReviewers, getReviewers, createBackportPullRequest, setPRLabels, setPRMilestone, getChangesFromPR, updatePRBody, commentOnPR, assignToPR } from './githubUtils'
8-
import { getBackportBody, getFailureCommentBody, getLabelsForPR, getMilestoneFromBase } from './nextcloudUtils'
9-
10-
export const backport = (task: Task) => new Promise<void>((resolve, reject) => {
11-
getAuthToken(task.installationId).then(async token => {
12-
const octokit = new Octokit({ auth: token })
13-
14-
let tmpDir: string = ''
15-
let prNumber: number = 0
16-
let conflicts: CherryPickResult|null = null
17-
const backportBranch = `backport/${task.prNumber}/${task.branch}`
18-
19-
info(task, `Starting backport request`)
20-
21-
// Add a reaction to the comment to indicate that we're processing it
4+
import { cherryPickCommits, cloneAndCacheRepo, hasDiff, hasEmptyCommits, hasSkipCiCommits, pushBranch } from './gitUtils.js'
5+
import { CherryPickResult, Task } from './constants.js'
6+
import { debug, error, info, warn } from './logUtils.js'
7+
import { Reaction, addReaction, getAuthToken, getAvailableLabels, getLabelsFromPR, getAvailableMilestones, requestReviewers, getReviewers, createBackportPullRequest, setPRLabels, setPRMilestone, getChangesFromPR, updatePRBody, commentOnPR, assignToPR } from './githubUtils.js'
8+
import { getBackportBody, getFailureCommentBody, getLabelsForPR, getMilestoneFromBase } from './nextcloudUtils.js'
9+
10+
export async function backport(task: Task): Promise<void> {
11+
const token = await getAuthToken(task.installationId)
12+
const octokit = new Octokit({ auth: token })
13+
14+
let tmpDir: string = ''
15+
let prNumber: number = 0
16+
let conflicts: CherryPickResult|null = null
17+
const backportBranch = `backport/${task.prNumber}/${task.branch}`
18+
19+
info(task, `Starting backport request`)
20+
21+
// Add a reaction to the comment to indicate that we're processing it
22+
try {
23+
await addReaction(octokit, task, Reaction.THUMBS_UP)
24+
} catch (e) {
25+
error(task, `Failed to add reaction to PR: ${e.message}`)
26+
// continue, this is not a fatal error
27+
}
28+
29+
try {
30+
// Clone and cache the repo
2231
try {
23-
await addReaction(octokit, task, Reaction.THUMBS_UP)
32+
tmpDir = await cloneAndCacheRepo(task, backportBranch)
33+
info(task, `Cloned to ${tmpDir}`)
2434
} catch (e) {
25-
error(task, `Failed to add reaction to PR: ${e.message}`)
26-
// continue, this is not a fatal error
35+
throw new Error(`Failed to clone repository: ${e.message}`)
2736
}
2837

38+
// Cherry pick the commits
2939
try {
30-
// Clone and cache the repo
31-
try {
32-
tmpDir = await cloneAndCacheRepo(task, backportBranch)
33-
info(task, `Cloned to ${tmpDir}`)
34-
} catch (e) {
35-
throw new Error(`Failed to clone repository: ${e.message}`)
40+
conflicts = await cherryPickCommits(task, tmpDir)
41+
if (conflicts === CherryPickResult.CONFLICTS) {
42+
warn(task, `Cherry picking commits resulted in conflicts`)
43+
} else {
44+
info(task, `Cherry picking commits successful`)
3645
}
46+
} catch (e) {
47+
throw new Error(`Failed to cherry pick commits: ${e.message}`)
48+
}
3749

38-
// Cherry pick the commits
39-
try {
40-
conflicts = await cherryPickCommits(task, tmpDir)
41-
if (conflicts === CherryPickResult.CONFLICTS) {
42-
warn(task, `Cherry picking commits resulted in conflicts`)
43-
} else {
44-
info(task, `Cherry picking commits successful`)
45-
}
46-
} catch (e) {
47-
throw new Error(`Failed to cherry pick commits: ${e.message}`)
48-
}
50+
// Check if there are any changes to backport
51+
const hasChanges = await hasDiff(tmpDir, task.branch, backportBranch, task)
52+
if (!hasChanges) {
53+
throw new Error(`No changes found in backport branch`)
54+
}
4955

50-
// Check if there are any changes to backport
51-
const hasChanges = await hasDiff(tmpDir, task.branch, backportBranch, task)
52-
if (!hasChanges) {
53-
throw new Error(`No changes found in backport branch`)
54-
}
56+
// Push the branch
57+
try {
58+
await pushBranch(task, tmpDir, token, backportBranch)
59+
info(task, `Pushed branch ${backportBranch}`)
60+
} catch (e) {
61+
throw new Error(`Failed to push branch ${backportBranch}: ${e.message}`)
62+
}
5563

56-
// Push the branch
57-
try {
58-
await pushBranch(task, tmpDir, token, backportBranch)
59-
info(task, `Pushed branch ${backportBranch}`)
60-
} catch (e) {
61-
throw new Error(`Failed to push branch ${backportBranch}: ${e.message}`)
62-
}
64+
// Create the pull request
65+
try {
66+
const reviewers = await getReviewers(octokit, task)
67+
const prCreationResult = await createBackportPullRequest(octokit, task, backportBranch, conflicts === CherryPickResult.CONFLICTS)
68+
prNumber = prCreationResult.data.number
69+
info(task, `Opened Pull Request #${prNumber} on ${prCreationResult.data.html_url}`)
6370

64-
// Create the pull request
6571
try {
66-
const reviewers = await getReviewers(octokit, task)
67-
const prCreationResult = await createBackportPullRequest(octokit, task, backportBranch, conflicts === CherryPickResult.CONFLICTS)
68-
prNumber = prCreationResult.data.number
69-
info(task, `Opened Pull Request #${prNumber} on ${prCreationResult.data.html_url}`)
70-
71-
try {
72-
// Ask for reviews from all reviewers of the original PR
73-
if (reviewers.length !== 0) {
74-
await requestReviewers(octokit, task, prNumber, reviewers)
75-
}
76-
77-
// Also ask the author of the original PR for a review
78-
await requestReviewers(octokit, task, prNumber, [task.author])
79-
info(task, `Requested reviews from ${[...reviewers, task.author].join(', ')}`)
80-
} catch (e) {
81-
error(task, `Failed to request reviews: ${e.message}`)
72+
// Ask for reviews from all reviewers of the original PR
73+
if (reviewers.length !== 0) {
74+
await requestReviewers(octokit, task, prNumber, reviewers)
8275
}
83-
} catch (e) {
84-
throw new Error(`Failed to create pull request: ${e.message}`)
85-
}
8676

87-
// Get labels from original PR and set them on the new PR
88-
try {
89-
const availableLabels = await getAvailableLabels(octokit, task)
90-
const prLabels = await getLabelsFromPR(octokit, task)
91-
const labels = getLabelsForPR(prLabels, availableLabels)
92-
await setPRLabels(octokit, task, prNumber, labels)
93-
info(task, `Set labels: ${labels.join(', ')}`)
77+
// Also ask the author of the original PR for a review
78+
await requestReviewers(octokit, task, prNumber, [task.author])
79+
info(task, `Requested reviews from ${[...reviewers, task.author].join(', ')}`)
9480
} catch (e) {
95-
error(task, `Failed to get and set labels: ${e.message}`)
96-
// continue, this is not a fatal error
81+
error(task, `Failed to request reviews: ${e.message}`)
9782
}
83+
} catch (e) {
84+
throw new Error(`Failed to create pull request: ${e.message}`)
85+
}
9886

99-
// Find new appropriate Milestone and set it on the new PR
100-
try {
101-
const availableMilestone = await getAvailableMilestones(octokit, task)
102-
const milestone = await getMilestoneFromBase(task.branch, availableMilestone)
103-
await setPRMilestone(octokit, task, prNumber, milestone)
104-
info(task, `Set milestone: ${milestone.title}`)
105-
} catch (e) {
106-
error(task, `Failed to find appropriate milestone: ${e.message}`)
107-
// continue, this is not a fatal error
108-
}
87+
// Get labels from original PR and set them on the new PR
88+
try {
89+
const availableLabels = await getAvailableLabels(octokit, task)
90+
const prLabels = await getLabelsFromPR(octokit, task)
91+
const labels = getLabelsForPR(prLabels, availableLabels)
92+
await setPRLabels(octokit, task, prNumber, labels)
93+
info(task, `Set labels: ${labels.join(', ')}`)
94+
} catch (e) {
95+
error(task, `Failed to get and set labels: ${e.message}`)
96+
// continue, this is not a fatal error
97+
}
10998

110-
// Assign the PR to the author of the original PR
111-
try {
112-
await assignToPR(octokit, task, prNumber, [task.author])
113-
info(task, `Assigned original author: ${task.author}`)
114-
} catch (e) {
115-
error(task, `Failed to assign PR: ${e.message}`)
116-
// continue, this is not a fatal error
117-
}
99+
// Find new appropriate Milestone and set it on the new PR
100+
try {
101+
const availableMilestone = await getAvailableMilestones(octokit, task)
102+
const milestone = await getMilestoneFromBase(task.branch, availableMilestone)
103+
await setPRMilestone(octokit, task, prNumber, milestone)
104+
info(task, `Set milestone: ${milestone.title}`)
105+
} catch (e) {
106+
error(task, `Failed to find appropriate milestone: ${e.message}`)
107+
// continue, this is not a fatal error
108+
}
109+
110+
// Assign the PR to the author of the original PR
111+
try {
112+
await assignToPR(octokit, task, prNumber, [task.author])
113+
info(task, `Assigned original author: ${task.author}`)
114+
} catch (e) {
115+
error(task, `Failed to assign PR: ${e.message}`)
116+
// continue, this is not a fatal error
117+
}
118118

119-
// Compare the original PR with the new PR
119+
// Compare the original PR with the new PR
120+
try {
121+
const oldChanges = await getChangesFromPR(octokit, task, task.prNumber)
122+
const newChanges = await getChangesFromPR(octokit, task, prNumber)
123+
const diffChanges = oldChanges.additions !== newChanges.additions
124+
|| oldChanges.deletions !== newChanges.deletions
125+
|| oldChanges.changedFiles !== newChanges.changedFiles
126+
const skipCi = await hasSkipCiCommits(tmpDir, task.commits.length)
127+
const emptyCommits = await hasEmptyCommits(tmpDir, task.commits.length, task)
128+
const hasConflicts = conflicts === CherryPickResult.CONFLICTS
129+
130+
debug(task, `hasConflicts: ${hasConflicts}, diffChanges: ${diffChanges}, emptyCommits: ${emptyCommits}, skipCi: ${skipCi}`)
120131
try {
121-
const oldChanges = await getChangesFromPR(octokit, task, task.prNumber)
122-
const newChanges = await getChangesFromPR(octokit, task, prNumber)
123-
const diffChanges = oldChanges.additions !== newChanges.additions
124-
|| oldChanges.deletions !== newChanges.deletions
125-
|| oldChanges.changedFiles !== newChanges.changedFiles
126-
const skipCi = await hasSkipCiCommits(tmpDir, task.commits.length)
127-
const emptyCommits = await hasEmptyCommits(tmpDir, task.commits.length, task)
128-
const hasConflicts = conflicts === CherryPickResult.CONFLICTS
129-
130-
debug(task, `hasConflicts: ${hasConflicts}, diffChanges: ${diffChanges}, emptyCommits: ${emptyCommits}, skipCi: ${skipCi}`)
131-
try {
132-
if (hasConflicts || diffChanges || emptyCommits || skipCi) {
133-
const newBody = await getBackportBody(task.prNumber, hasConflicts, diffChanges, emptyCommits, skipCi)
134-
await updatePRBody(octokit, task, prNumber, newBody)
135-
}
136-
} catch (e) {
137-
error(task, `Failed to update PR body: ${e.message}`)
138-
// continue, this is not a fatal error
132+
if (hasConflicts || diffChanges || emptyCommits || skipCi) {
133+
const newBody = await getBackportBody(task.prNumber, hasConflicts, diffChanges, emptyCommits, skipCi)
134+
await updatePRBody(octokit, task, prNumber, newBody)
139135
}
140136
} catch (e) {
141-
error(task, `Failed to compare changes: ${e.message}`)
137+
error(task, `Failed to update PR body: ${e.message}`)
142138
// continue, this is not a fatal error
143139
}
144-
145-
// Success! We're done here
146-
addReaction(octokit, task, Reaction.HOORAY)
147140
} catch (e) {
148-
// Add a thumbs down reaction to the comment to indicate that we failed
149-
try {
150-
addReaction(octokit, task, Reaction.THUMBS_DOWN)
151-
const failureComment = getFailureCommentBody(task, backportBranch, e?.message)
152-
await commentOnPR(octokit, task, failureComment)
153-
} catch (e) {
154-
error(task, `Failed to comment failure on PR: ${e.message}`)
155-
// continue, this is not a fatal error
156-
}
141+
error(task, `Failed to compare changes: ${e.message}`)
142+
// continue, this is not a fatal error
143+
}
157144

158-
reject(`Failed to backport: ${e.message}`)
145+
// Success! We're done here
146+
addReaction(octokit, task, Reaction.HOORAY)
147+
} catch (e) {
148+
// Add a thumbs down reaction to the comment to indicate that we failed
149+
try {
150+
addReaction(octokit, task, Reaction.THUMBS_DOWN)
151+
const failureComment = getFailureCommentBody(task, backportBranch, e?.message)
152+
await commentOnPR(octokit, task, failureComment)
153+
} catch (e) {
154+
error(task, `Failed to comment failure on PR: ${e.message}`)
155+
// continue, this is not a fatal error
159156
}
160157

161-
// Remove the temp dir if it exists
162-
if (tmpDir !== '' && existsSync(tmpDir)) {
163-
try {
164-
rmSync(tmpDir, { recursive: true })
165-
info(task, `Removed ${tmpDir}`)
166-
resolve()
167-
} catch (e) {
168-
reject(`Failed to remove ${tmpDir}: ${e.message}`)
169-
}
158+
throw new Error(`Failed to backport: ${e.message}`)
159+
}
160+
161+
// Remove the temp dir if it exists
162+
if (tmpDir !== '' && existsSync(tmpDir)) {
163+
try {
164+
rmSync(tmpDir, { recursive: true })
165+
info(task, `Removed ${tmpDir}`)
166+
} catch (e) {
167+
throw new Error(`Failed to remove ${tmpDir}: ${e.message}`)
170168
}
171-
})
172-
}).catch(e => {
173-
error(task, e)
174-
throw e
175-
})
169+
}
170+
}

src/gitUtils.ts

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { cpSync, existsSync, mkdirSync} from 'node:fs'
1+
import { existsSync, mkdirSync} from 'node:fs'
22
import { join } from 'node:path'
33
import { simpleGit } from 'simple-git'
44

5-
import { CACHE_DIRNAME, CherryPickResult, ROOT_DIR, Task, WORK_DIRNAME } from './constants'
6-
import { debug, error } from './logUtils'
5+
import { CACHE_DIRNAME, CherryPickResult, ROOT_DIR, Task, WORK_DIRNAME } from './constants.js'
6+
import { debug, error } from './logUtils.js'
7+
import { randomBytes } from 'node:crypto'
78

89
export const setGlobalGitConfig = async (user: string): Promise<void> => {
910
const git = simpleGit()
@@ -27,11 +28,18 @@ export const cloneAndCacheRepo = async (task: Task, backportBranch: string): Pro
2728
// Clone the repo into the cache dir or make sure it already exists
2829
const cachedRepoRoot = join(ROOT_DIR, CACHE_DIRNAME, owner, repo)
2930
try {
30-
if (!existsSync(cachedRepoRoot + '/.git')) {
31+
// Create repo path if needed
32+
if (!existsSync(cachedRepoRoot)) {
3133
mkdirSync(cachedRepoRoot, { recursive: true })
32-
const git = simpleGit(cachedRepoRoot)
34+
}
35+
36+
const git = simpleGit(cachedRepoRoot)
37+
if (!existsSync(cachedRepoRoot + '/.git')) {
38+
// Is not a repository, so clone
3339
await git.clone(`https://github.com/${owner}/${repo}`, '.')
3440
} else {
41+
// Is already a repository so make sure it is clean and follows the default branch
42+
await git.clean(['-X', '-d', '-f'])
3543
debug(task, `Repo already cached at ${cachedRepoRoot}`)
3644
}
3745
} catch (e) {
@@ -52,36 +60,19 @@ export const cloneAndCacheRepo = async (task: Task, backportBranch: string): Pro
5260
// }
5361

5462
// Init a new temp repo in the work dir
55-
const tmpDirName = Math.random().toString(36).substring(7)
63+
const tmpDirName = randomBytes(7).toString('hex')
5664
const tmpRepoRoot = join(ROOT_DIR, WORK_DIRNAME, tmpDirName)
5765
try {
5866
// Copy the cached repo to the temp repo
5967
mkdirSync(join(ROOT_DIR, WORK_DIRNAME), { recursive: true })
60-
cpSync(cachedRepoRoot, tmpRepoRoot, { recursive: true })
61-
} catch (e) {
62-
throw new Error(`Failed to copy cached repo: ${e.message}`)
63-
}
64-
65-
try {
66-
// Checkout all the branches
67-
const git = simpleGit(tmpRepoRoot)
68-
// TODO: We could do that to the cached repo, but
69-
// this seem to create some concurrency issues.
70-
await git.raw(['fetch', '--all'])
71-
await git.raw(['pull', '--prune'])
72-
73-
// reset and clean the repo
74-
await git.raw(['reset', '--hard', `origin/${branch}`])
75-
await git.raw(['clean', '--force', '-dfx'])
76-
77-
// Checkout the branch we want to backport from
78-
await git.checkout(branch)
79-
await git.checkoutBranch(
80-
backportBranch,
81-
branch
82-
)
68+
// create worktree
69+
const git = simpleGit(cachedRepoRoot)
70+
// fetch upstream version of the branch - well we need to fetch all because we do not know where the commits are located we need to cherry-pick
71+
await git.fetch(['-p', '--all'])
72+
// create work tree with up-to-date content of that branch
73+
await git.raw(['worktree', 'add', '-b', backportBranch, tmpRepoRoot, `origin/${branch}`])
8374
} catch (e) {
84-
throw new Error(`Failed to checkout branches: ${e.message}`)
75+
throw new Error(`Failed to create working tree: ${e.message}`)
8576
}
8677

8778
return tmpRepoRoot

0 commit comments

Comments
 (0)