Skip to content

Commit ab0857b

Browse files
sinchangjakebolam
authored andcommitted
fix: handle error when branch already exists (#48)
* refactor * Update createBranch method * Tweak * Add tests * wip * wip * lint
1 parent 4ce2016 commit ab0857b

File tree

8 files changed

+386
-119
lines changed

8 files changed

+386
-119
lines changed

src/tasks/processIssueComment/OptionsConfig/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ const ALL_CONTRIBUTORS_RC = '.all-contributorsrc'
22

33
const { addContributorWithDetails } = require('all-contributors-cli')
44

5+
const { AllContributorBotError } = require('../utils/errors')
6+
57
class OptionsConfig {
6-
constructor({ repository, commentReply }) {
8+
constructor({ repository }) {
79
this.repository = repository
8-
this.commentReply = commentReply
910
this.options
1011
this.originalOptionsSha
1112
}
@@ -22,12 +23,11 @@ class OptionsConfig {
2223
return optionsConfig
2324
} catch (error) {
2425
if (error instanceof SyntaxError) {
25-
this.commentReply.reply(
26+
throw new AllContributorBotError(
2627
`This project's configuration file has malformed JSON: ${ALL_CONTRIBUTORS_RC}. Error:: ${
2728
error.message
2829
}`,
2930
)
30-
error.handled = true
3131
}
3232
throw error
3333
}

src/tasks/processIssueComment/Repository/index.js

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,48 @@
1-
const { ResourceNotFoundError } = require('../utils/errors')
1+
const {
2+
BranchNotFoundError,
3+
ResourceNotFoundError,
4+
} = require('../utils/errors')
25

36
class Repository {
4-
constructor({ repo, owner, github }) {
7+
constructor({ repo, owner, github, defaultBranch }) {
58
this.github = github
69
this.repo = repo
710
this.owner = owner
11+
this.defaultBranch = defaultBranch
12+
this.baseBranch = defaultBranch
813
}
914

1015
getFullname() {
1116
return `${this.owner}/${this.repo}`
1217
}
1318

19+
setBaseBranch(branchName) {
20+
this.baseBranch = branchName
21+
}
22+
1423
async getFile(filePath) {
15-
// https://octokit.github.io/rest.js/#api-Repos-getContents
16-
let file
1724
try {
18-
file = await this.github.repos.getContents({
25+
// https://octokit.github.io/rest.js/#api-Repos-getContents
26+
const file = await this.github.repos.getContents({
1927
owner: this.owner,
2028
repo: this.repo,
2129
path: filePath,
30+
ref: this.baseBranch,
2231
})
32+
// Contents can be an array if its a directory, should be an edge case, and we can just crash
33+
const contentBinary = file.data.content
34+
const content = Buffer.from(contentBinary, 'base64').toString()
35+
return {
36+
content,
37+
sha: file.data.sha,
38+
}
2339
} catch (error) {
2440
if (error.status === 404) {
2541
throw new ResourceNotFoundError(filePath, this.full_name)
2642
} else {
2743
throw error
2844
}
2945
}
30-
31-
// Contents can be an array if its a directory, should be an edge case, and we can just crash
32-
const contentBinary = file.data.content
33-
const content = Buffer.from(contentBinary, 'base64').toString()
34-
return {
35-
content,
36-
sha: file.data.sha,
37-
}
3846
}
3947

4048
async getMultipleFiles(filePathsArray) {
@@ -61,17 +69,23 @@ class Repository {
6169
return multipleFilesByPath
6270
}
6371

64-
async getHeadRef(defaultBranch) {
65-
const result = await this.github.git.getRef({
66-
owner: this.owner,
67-
repo: this.repo,
68-
ref: `heads/${defaultBranch}`,
69-
})
70-
return result.data.object.sha
72+
async getRef(branchName) {
73+
try {
74+
const result = await this.github.git.getRef({
75+
owner: this.owner,
76+
repo: this.repo,
77+
ref: `heads/${branchName}`,
78+
})
79+
return result.data.object.sha
80+
} catch (error) {
81+
if (error.status === 404) {
82+
throw new BranchNotFoundError(branchName)
83+
}
84+
}
7185
}
7286

73-
async createBranch({ branchName, defaultBranch }) {
74-
const fromSha = await this.getHeadRef(defaultBranch)
87+
async createBranch(branchName) {
88+
const fromSha = await this.getRef(this.defaultBranch)
7589

7690
// https://octokit.github.io/rest.js/#api-Git-createRef
7791
await this.github.git.createRef({
@@ -140,27 +154,33 @@ class Repository {
140154
await Promise.all(createOrUpdateFilesMultiple)
141155
}
142156

143-
async createPullRequest({ title, body, branchName, defaultBranch }) {
144-
const result = await this.github.pulls.create({
145-
owner: this.owner,
146-
repo: this.repo,
147-
title,
148-
body,
149-
head: branchName,
150-
base: defaultBranch,
151-
maintainer_can_modify: true,
152-
})
153-
return result.data.html_url
157+
async createPullRequest({ title, body, branchName }) {
158+
try {
159+
const result = await this.github.pulls.create({
160+
owner: this.owner,
161+
repo: this.repo,
162+
title,
163+
body,
164+
head: branchName,
165+
base: this.defaultBranch,
166+
maintainer_can_modify: true,
167+
})
168+
return result.data.html_url
169+
} catch (error) {
170+
if (error.status === 422) {
171+
console.log('Pull request already open') // eslint-disable-line no-console
172+
return error.data.html_url
173+
} else {
174+
throw error
175+
}
176+
}
154177
}
155178

156-
async createPullRequestFromFiles({
157-
title,
158-
body,
159-
filesByPath,
160-
branchName,
161-
defaultBranch,
162-
}) {
163-
await this.createBranch({ branchName, defaultBranch })
179+
async createPullRequestFromFiles({ title, body, filesByPath, branchName }) {
180+
const branchNameExists = branchName === this.baseBranch
181+
if (!branchNameExists) {
182+
await this.createBranch(branchName)
183+
}
164184

165185
await this.createOrUpdateFiles({
166186
filesByPath,
@@ -171,9 +191,7 @@ class Repository {
171191
title,
172192
body,
173193
branchName,
174-
defaultBranch,
175194
})
176-
177195
return pullRequestURL
178196
}
179197
}

src/tasks/processIssueComment/probot-processIssueComment.js

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const parseComment = require('./utils/parse-comment')
88

99
const {
1010
AllContributorBotError,
11+
BranchNotFoundError,
1112
ResourceNotFoundError,
1213
} = require('./utils/errors')
1314

@@ -20,7 +21,7 @@ async function processAddContributor({
2021
optionsConfig,
2122
who,
2223
contributions,
23-
defaultBranch,
24+
branchName,
2425
}) {
2526
const { name, avatar_url, profile } = await getUserDetails({
2627
github: context.github,
@@ -49,31 +50,52 @@ async function processAddContributor({
4950
originalSha: optionsConfig.getOriginalSha(),
5051
}
5152

52-
const safeWho = getSafeRef(who)
5353
const pullRequestURL = await repository.createPullRequestFromFiles({
5454
title: `docs: add ${who} as a contributor`,
5555
body: `Adds @${who} as a contributor for ${contributions.join(
5656
', ',
5757
)}.\n\nThis was requested by ${commentReply.replyingToWho()} [in this comment](${commentReply.replyingToWhere()})`,
5858
filesByPath: filesByPathToUpdate,
59-
branchName: `all-contributors/add-${safeWho}`,
60-
defaultBranch,
59+
branchName,
6160
})
6261

6362
commentReply.reply(
6463
`I've put up [a pull request](${pullRequestURL}) to add @${who}! :tada:`,
6564
)
6665
}
6766

68-
async function probotProcessIssueComment({ context, commentReply }) {
67+
async function setupRepository({ context, branchName }) {
68+
const defaultBranch = context.payload.repository.default_branch
6969
const repository = new Repository({
7070
...context.repo(),
7171
github: context.github,
72+
defaultBranch,
7273
})
74+
75+
try {
76+
await repository.getRef(branchName)
77+
context.log.info(
78+
`Branch: ${branchName} EXISTS, will work from this branch`,
79+
)
80+
repository.setBaseBranch(branchName)
81+
} catch (error) {
82+
if (error instanceof BranchNotFoundError) {
83+
context.log.info(
84+
`Branch: ${branchName} DOES NOT EXIST, will work from default branch`,
85+
)
86+
} else {
87+
throw error
88+
}
89+
}
90+
91+
return repository
92+
}
93+
94+
async function setupOptionsConfig({ repository }) {
7395
const optionsConfig = new OptionsConfig({
7496
repository,
75-
commentReply,
7697
})
98+
7799
try {
78100
await optionsConfig.fetch()
79101
} catch (error) {
@@ -84,18 +106,31 @@ async function probotProcessIssueComment({ context, commentReply }) {
84106
}
85107
}
86108

109+
return optionsConfig
110+
}
111+
112+
async function probotProcessIssueComment({ context, commentReply }) {
87113
const commentBody = context.payload.comment.body
88-
const parsedComment = parseComment(commentBody)
114+
const { who, action, contributions } = parseComment(commentBody)
115+
116+
if (action === 'add') {
117+
const safeWho = getSafeRef(who)
118+
const branchName = `all-contributors/add-${safeWho}`
119+
120+
const repository = await setupRepository({
121+
context,
122+
branchName,
123+
})
124+
const optionsConfig = await setupOptionsConfig({ repository })
89125

90-
if (parsedComment.action === 'add') {
91126
await processAddContributor({
92127
context,
93128
commentReply,
94129
repository,
95130
optionsConfig,
96-
who: parsedComment.who,
97-
contributions: parsedComment.contributions,
98-
defaultBranch: context.payload.repository.default_branch,
131+
who,
132+
contributions,
133+
branchName,
99134
})
100135
return
101136
}
@@ -105,7 +140,7 @@ async function probotProcessIssueComment({ context, commentReply }) {
105140
`Basic usage: @all-contributors please add @jakebolam for code, doc and infra`,
106141
)
107142
commentReply.reply(
108-
`For other usage see the [documentation](https://github.com/all-contributors/all-contributors-bot#usage)`,
143+
`For other usages see the [documentation](https://github.com/all-contributors/all-contributors-bot#usage)`,
109144
)
110145
return
111146
}
@@ -115,9 +150,7 @@ async function probotProcessIssueCommentSafe({ context }) {
115150
try {
116151
await probotProcessIssueComment({ context, commentReply })
117152
} catch (error) {
118-
if (error.handled) {
119-
context.log.debug(error)
120-
} else if (error instanceof AllContributorBotError) {
153+
if (error instanceof AllContributorBotError) {
121154
context.log.info(error)
122155
commentReply.reply(error.message)
123156
} else {

src/tasks/processIssueComment/utils/errors.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ class ResourceNotFoundError extends AllContributorBotError {
1414
}
1515
}
1616

17+
class BranchNotFoundError extends AllContributorBotError {
18+
constructor(branchName) {
19+
super(`${branchName} does not exist`)
20+
this.name = this.constructor.name
21+
}
22+
}
23+
1724
class UserNotFoundError extends AllContributorBotError {
1825
constructor(username) {
1926
super(`Could not find the user ${username} on github.`)
@@ -23,6 +30,7 @@ class UserNotFoundError extends AllContributorBotError {
2330

2431
module.exports = {
2532
AllContributorBotError,
33+
BranchNotFoundError,
2634
ResourceNotFoundError,
2735
UserNotFoundError,
2836
}

test/tasks/processIssueComment/OptionsConfig/index.test.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ describe('ContentFiles', () => {
55
repo: 'all-contributors-bot-test',
66
owner: 'all-contributors',
77
}
8-
const mockCommentReply = {}
98

109
test(`Add's new contributor`, async () => {
1110
const optionsConfig = new OptionsConfig({
1211
repository: mockRepository,
13-
commentReply: mockCommentReply,
1412
})
1513

1614
optionsConfig.options = {
@@ -40,7 +38,6 @@ describe('ContentFiles', () => {
4038
test(`Add's new contributions for contributor`, async () => {
4139
const optionsConfig = new OptionsConfig({
4240
repository: mockRepository,
43-
commentReply: mockCommentReply,
4441
})
4542

4643
optionsConfig.options = {
@@ -81,7 +78,6 @@ describe('ContentFiles', () => {
8178
test(`If profile URL is missing protocol, add it for them`, async () => {
8279
const optionsConfig = new OptionsConfig({
8380
repository: mockRepository,
84-
commentReply: mockCommentReply,
8581
})
8682
optionsConfig.init()
8783

@@ -108,7 +104,6 @@ describe('ContentFiles', () => {
108104
test(`Inits the contributor file`, () => {
109105
const optionsConfig = new OptionsConfig({
110106
repository: mockRepository,
111-
commentReply: mockCommentReply,
112107
})
113108

114109
expect(optionsConfig.options).toBeUndefined()

test/tasks/processIssueComment/Repository/index.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('Repository', () => {
1313
repo: 'all-contributors-bot',
1414
owner: 'all-contributors',
1515
github: mockGithub,
16+
defaultBranch: 'master',
1617
})
1718

1819
const verifyBody = body => {
@@ -95,7 +96,6 @@ describe('Repository', () => {
9596
},
9697
},
9798
branchName: 'all-contributors/add-jakebolam',
98-
defaultBranch: 'master',
9999
})
100100
expect(pullRequestNumber).toEqual(
101101
'https://github.com/all-contributors/all-contributors-bot/pull/1347',

0 commit comments

Comments
 (0)