Skip to content

Commit f8adcf9

Browse files
jakebolamtenshiAMD
andauthored
feat: support adding multiple users in a single message (#127)
Co-authored-by: Angel Aviel Domaoan <[email protected]>
1 parent bad58c8 commit f8adcf9

File tree

5 files changed

+776
-12
lines changed

5 files changed

+776
-12
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
const Analytics = require('../../utils/Analytics')
2+
const CommentReply = require('./CommentReply')
3+
const Repository = require('./Repository')
4+
const OptionsConfig = require('./OptionsConfig')
5+
const ContentFiles = require('./ContentFiles')
6+
7+
const getUserDetails = require('./utils/getUserDetails')
8+
const parseComment = require('./utils/parse-comment')
9+
10+
const {
11+
AllContributorBotError,
12+
BranchNotFoundError,
13+
ResourceNotFoundError,
14+
} = require('./utils/errors')
15+
16+
const getSafeRef = require('./utils/git/getSafeRef')
17+
18+
async function processAddContributors({
19+
context,
20+
commentReply,
21+
repository,
22+
optionsConfig,
23+
contributors,
24+
branchName,
25+
}) {
26+
27+
const usersAdded = []
28+
const usersMissed = []
29+
30+
async function addContributor(who, contributions) {
31+
if (contributions.length === 0) {
32+
context.log.debug(`No contributions for ${who}`)
33+
usersMissed.push(who)
34+
return
35+
}
36+
37+
// TODO: wrap this blog in a try catch, if one user fails, don't fail the whole pull request
38+
const { name, avatar_url, profile } = await getUserDetails({
39+
github: context.github,
40+
username: who,
41+
})
42+
43+
await optionsConfig.addContributor({
44+
login: who,
45+
contributions,
46+
name,
47+
avatar_url,
48+
profile,
49+
})
50+
51+
usersAdded.push(who)
52+
}
53+
54+
// TODO: throttle max paralle requests
55+
const addContributorPromises = contributors.map(function(contributor) {
56+
return addContributor((contributor.who, contributor.contributions))
57+
})
58+
59+
await Promise.all(addContributorPromises)
60+
61+
if (usersAdded === 0) {
62+
return commentReply.reply(
63+
`I couldn't determine any contributions for ${usersAdded.join(', ')}.
64+
Did you specify any contributions? Please make sure to use [valid contribution names](https://allcontributors.org/docs/en/emoji-key).`,
65+
)
66+
67+
const contentFiles = new ContentFiles({
68+
repository,
69+
})
70+
await contentFiles.fetch(optionsConfig)
71+
if (optionsConfig.getOriginalSha() === undefined) {
72+
contentFiles.init()
73+
}
74+
contentFiles.generate(optionsConfig)
75+
const filesByPathToUpdate = contentFiles.get()
76+
filesByPathToUpdate[optionsConfig.getPath()] = {
77+
content: optionsConfig.getRaw(),
78+
originalSha: optionsConfig.getOriginalSha(),
79+
}
80+
81+
const pullRequestBodyAdds = `Adds the following contributors:
82+
- ${usersAdded.join('\n- ')}\n\n`
83+
84+
const pullRequestBodyMissed = `Unable to determine contributions for the following contributors, these were excluded from this PR:
85+
- ${usersMissed.join('\\n- ')}\n\n`
86+
87+
const pullRequestBodyRequester = `This was requested by ${commentReply.replyingToWho()} [in this comment](${commentReply.replyingToWhere()})`
88+
89+
const {
90+
pullRequestURL,
91+
pullCreated,
92+
} = await repository.createPullRequestFromFiles({
93+
title: `docs: add new contributors`,
94+
body: `${pullRequestBodyAdds}.\n\n
95+
${usersMissed > 0 ? pullRequestBodyMissed : ''}
96+
${pullRequestBodyRequester}
97+
`,
98+
filesByPath: filesByPathToUpdate,
99+
branchName,
100+
})
101+
102+
if (pullCreated) {
103+
commentReply.reply(
104+
`I've put up [a pull request](${pullRequestURL}) to add new contributors! :tada:`,
105+
)
106+
return
107+
}
108+
// Updated
109+
commentReply.reply(
110+
`I've updated [the pull request](${pullRequestURL}) to add contributors! :tada:`,
111+
)
112+
}
113+
114+
async function setupRepository({ context, branchName }) {
115+
const defaultBranch = context.payload.repository.default_branch
116+
const repository = new Repository({
117+
...context.repo(),
118+
github: context.github,
119+
defaultBranch,
120+
log: context.log,
121+
})
122+
123+
try {
124+
await repository.getRef(branchName)
125+
context.log.info(
126+
`Branch: ${branchName} EXISTS, will work from this branch`,
127+
)
128+
repository.setBaseBranch(branchName)
129+
} catch (error) {
130+
if (error instanceof BranchNotFoundError) {
131+
context.log.info(
132+
`Branch: ${branchName} DOES NOT EXIST, will work from default branch`,
133+
)
134+
} else {
135+
throw error
136+
}
137+
}
138+
139+
return repository
140+
}
141+
142+
async function setupOptionsConfig({ repository }) {
143+
const optionsConfig = new OptionsConfig({
144+
repository,
145+
})
146+
147+
try {
148+
await optionsConfig.fetch()
149+
} catch (error) {
150+
if (error instanceof ResourceNotFoundError) {
151+
optionsConfig.init()
152+
} else {
153+
throw error
154+
}
155+
}
156+
157+
return optionsConfig
158+
}
159+
160+
async function probotProcessIssueComment({ context, commentReply, analytics }) {
161+
const commentBody = context.payload.comment.body
162+
analytics.track('processComment', {
163+
commentBody: commentBody,
164+
})
165+
const { action, contributors } = parseComment(commentBody)
166+
167+
if (action === 'add') {
168+
analytics.track('addContributors', {
169+
who: commentBody,
170+
contributors,
171+
})
172+
173+
const whos = contributors.map(({ who }) => who).join('--')
174+
const safeWho = getSafeRef(whos)
175+
// TODO: max length on branch name?
176+
// TODO: should this be the branch name
177+
const branchName = `all-contributors/add-${safeWho}`
178+
179+
const repository = await setupRepository({
180+
context,
181+
branchName,
182+
})
183+
const optionsConfig = await setupOptionsConfig({ repository })
184+
185+
await processAddContributors({
186+
context,
187+
commentReply,
188+
repository,
189+
optionsConfig,
190+
contributors,
191+
branchName,
192+
})
193+
analytics.track('processCommentSuccess')
194+
return
195+
}
196+
197+
analytics.track('unknownIntent', {
198+
action,
199+
})
200+
201+
commentReply.reply(`I could not determine your intention.`)
202+
commentReply.reply(
203+
`Basic usage: @all-contributors please add @jakebolam for code, doc and infra`,
204+
)
205+
commentReply.reply(
206+
`For other usages see the [documentation](https://allcontributors.org/docs/en/bot/usage)`,
207+
)
208+
return
209+
}
210+
211+
async function probotProcessIssueCommentSafe({ context }) {
212+
const analytics = new Analytics({
213+
...context.repo(),
214+
user: context.payload.sender.login,
215+
log: context.log,
216+
})
217+
const commentReply = new CommentReply({ context })
218+
try {
219+
await probotProcessIssueComment({ context, commentReply, analytics })
220+
} catch (error) {
221+
if (error instanceof AllContributorBotError) {
222+
context.log.info(error)
223+
commentReply.reply(error.message)
224+
analytics.track('errorKnown', {
225+
errorMessage: error.message,
226+
})
227+
} else {
228+
context.log.error(error)
229+
commentReply.reply(
230+
`We had trouble processing your request. Please try again later.`,
231+
)
232+
analytics.track('errorUnKnown', {
233+
errorMessage: error.message,
234+
})
235+
throw error
236+
}
237+
} finally {
238+
await commentReply.send()
239+
await analytics.finishQueue()
240+
}
241+
}
242+
243+
function processIssueCommentApp(app) {
244+
// issueComment.edited
245+
// Issue comments and PR comments both create issue_comment events
246+
app.on('issue_comment.created', async context => {
247+
app.log.trace(context)
248+
await probotProcessIssueCommentSafe({ context })
249+
})
250+
}
251+
252+
module.exports = processIssueCommentApp

0 commit comments

Comments
 (0)