1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.DefaultExtractAuthor = exports.NoOpExtractAuthor = exports.AutofixProgressReporter = exports.AutofixProgressTests = exports.generateCommitMessageForAutofix = exports.filterImmediateAutofixes = exports.executeAutofixes = void 0;
|
19 | const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
|
20 | const editModes_1 = require("@atomist/automation-client/lib/operations/edit/editModes");
|
21 | const projectEditorOps_1 = require("@atomist/automation-client/lib/operations/edit/projectEditorOps");
|
22 | const logger_1 = require("@atomist/automation-client/lib/util/logger");
|
23 | const slack_messages_1 = require("@atomist/slack-messages");
|
24 | const _ = require("lodash");
|
25 | const types_1 = require("../../typings/types");
|
26 | const confirmEditedness_1 = require("../command/transform/confirmEditedness");
|
27 | const minimalClone_1 = require("../goal/minimalClone");
|
28 | const progress_1 = require("../goal/progress/progress");
|
29 | const handlerRegistrations_1 = require("../machine/handlerRegistrations");
|
30 | const child_process_1 = require("../misc/child_process");
|
31 | const createPushImpactListenerInvocation_1 = require("./createPushImpactListenerInvocation");
|
32 | const relevantCodeActions_1 = require("./relevantCodeActions");
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | function executeAutofixes(registrations, transformPresentation, extractAuthor = exports.NoOpExtractAuthor) {
|
40 | return async (goalInvocation) => {
|
41 | const { id, configuration, goalEvent, credentials, context, progressLog } = goalInvocation;
|
42 | progressLog.write("Evaluating to %d configured autofixes", registrations.length);
|
43 | try {
|
44 | if (registrations.length === 0) {
|
45 | return HandlerResult_1.Success;
|
46 | }
|
47 | const push = goalEvent.push;
|
48 | const appliedAutofixes = [];
|
49 | let editMode;
|
50 | const editResult = await configuration.sdm.projectLoader.doWithProject({
|
51 | credentials,
|
52 | id,
|
53 | context,
|
54 | readOnly: false,
|
55 | cloneOptions: minimalClone_1.minimalClone(push),
|
56 | }, async (project) => {
|
57 | if ((await project.gitStatus()).sha !== id.sha) {
|
58 | return {
|
59 | success: true,
|
60 | edited: false,
|
61 | target: project,
|
62 | description: "Autofixes not executing",
|
63 | phase: "new commit on branch",
|
64 | };
|
65 | }
|
66 | const cri = await createPushImpactListenerInvocation_1.createPushImpactListenerInvocation(goalInvocation, project);
|
67 | if (!!transformPresentation) {
|
68 | editMode = transformPresentation(Object.assign(Object.assign({}, cri), { parameters: {
|
69 | goalInvocation,
|
70 | } }), project);
|
71 | if (editModes_1.isBranchCommit(editMode)) {
|
72 | if (await project.hasBranch(editMode.branch)) {
|
73 | await project.checkout(editMode.branch);
|
74 | }
|
75 | else {
|
76 | await project.createBranch(editMode.branch);
|
77 | }
|
78 | }
|
79 | }
|
80 | const relevantAutofixes = filterImmediateAutofixes(await relevantCodeActions_1.relevantCodeActions(registrations, cri), goalInvocation);
|
81 | progressLog.write("Applying %d relevant autofixes of %d to %s/%s: '%s' of configured '%s'", relevantAutofixes.length, registrations.length, cri.id.owner, cri.id.repo, relevantAutofixes.map(a => a.name).join(", "), registrations.map(a => a.name).join(", "));
|
82 | let cumulativeResult = {
|
83 | target: cri.project,
|
84 | success: true,
|
85 | edited: false,
|
86 | };
|
87 | for (const autofix of _.flatten(relevantAutofixes)) {
|
88 | const thisEdit = await runOne(goalInvocation, cri, autofix, extractAuthor);
|
89 | if (thisEdit.edited) {
|
90 | appliedAutofixes.push(autofix);
|
91 | }
|
92 | cumulativeResult = projectEditorOps_1.combineEditResults(cumulativeResult, thisEdit);
|
93 | }
|
94 | if (cumulativeResult.edited) {
|
95 | await cri.project.push();
|
96 | if (!!editMode && editModes_1.isPullRequest(editMode)) {
|
97 | const targetBranch = editMode.targetBranch || goalEvent.branch;
|
98 | let body = `${editMode.body}
|
99 |
|
100 | Applied autofixes:
|
101 | ${appliedAutofixes.map(af => ` * ${slack_messages_1.codeLine(af.name)}`).join("\n")}
|
102 |
|
103 | [atomist:generated] [atomist:autofix]`.trim();
|
104 | if (editMode.autoMerge) {
|
105 | body = `${body} ${editMode.autoMerge.mode} ${editMode.autoMerge.method ? editMode.autoMerge.method : ""}`.trim();
|
106 | }
|
107 | await cri.project.raisePullRequest(editMode.title, body, targetBranch);
|
108 | }
|
109 | }
|
110 | return cumulativeResult;
|
111 | });
|
112 | if (editResult.edited) {
|
113 |
|
114 | return {
|
115 | code: 0,
|
116 | message: "Edited",
|
117 | description: goalInvocation.goal.stoppedDescription,
|
118 | state: isNewBranch(editMode, goalEvent.branch) ? types_1.SdmGoalState.success : types_1.SdmGoalState.stopped,
|
119 | phase: detailMessage(appliedAutofixes),
|
120 | };
|
121 | }
|
122 | return {
|
123 | code: 0,
|
124 | description: editResult.description,
|
125 | };
|
126 | }
|
127 | catch (err) {
|
128 | logger_1.logger.warn("Autofixes failed with '%s':\n%s", err.message, err.stack);
|
129 | progressLog.write("Autofixes failed with '%s'", err.message);
|
130 | if (err.stdout) {
|
131 | progressLog.write(err.stdout);
|
132 | }
|
133 | if (err.stderr) {
|
134 | progressLog.write(err.stderr);
|
135 | }
|
136 | return {
|
137 | code: 1,
|
138 | message: err.message,
|
139 | };
|
140 | }
|
141 | };
|
142 | }
|
143 | exports.executeAutofixes = executeAutofixes;
|
144 |
|
145 |
|
146 |
|
147 | function isNewBranch(editMode, branch) {
|
148 | if (!!editMode && editModes_1.isBranchCommit(editMode)) {
|
149 | return editMode.branch !== branch;
|
150 | }
|
151 | return false;
|
152 | }
|
153 | function detailMessage(appliedAutofixes) {
|
154 |
|
155 | if (appliedAutofixes.length <= 2) {
|
156 | return `${appliedAutofixes.map(af => af.name).join(", ")}`;
|
157 | }
|
158 | else {
|
159 | return `${appliedAutofixes.length} autofixes`;
|
160 | }
|
161 | }
|
162 | async function runOne(gi, cri, autofix, extractAuthor) {
|
163 | const { progressLog, configuration } = gi;
|
164 | const project = cri.project;
|
165 | progressLog.write("About to transform %s with autofix '%s'", project.id.url, autofix.name);
|
166 | try {
|
167 | const arg2 = Object.assign(Object.assign(Object.assign({}, cri.context), cri), { push: cri, progressLog });
|
168 | const tentativeEditResult = await handlerRegistrations_1.toScalarProjectEditor(autofix.transform, configuration.sdm)(project, arg2, autofix.parametersInstance);
|
169 | const editResult = await confirmEditedness_1.confirmEditedness(tentativeEditResult);
|
170 | if (!editResult.success) {
|
171 | await project.revert();
|
172 | logger_1.logger.warn("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
|
173 | progressLog.write("Edited %s with autofix %s and success=false, edited=%d", project.id.url, autofix.name, editResult.edited);
|
174 | if (!!autofix.options && autofix.options.ignoreFailure) {
|
175 |
|
176 | return { target: project, edited: false, success: false };
|
177 | }
|
178 | }
|
179 | else if (editResult.edited) {
|
180 | progressLog.write("Autofix '%s' made changes", autofix.name);
|
181 | await project.commit(generateCommitMessageForAutofix(autofix));
|
182 | const author = await extractAuthor(gi);
|
183 | if (!!author && !!author.name && !!author.email) {
|
184 | await child_process_1.spawnLog("git", ["commit", "--amend", `--author="${author.name} <${author.email}>"`, "--no-edit"], {
|
185 | cwd: project.baseDir,
|
186 | log: progressLog,
|
187 | });
|
188 | }
|
189 | }
|
190 | else {
|
191 | progressLog.write("Autofix '%s' made no changes", autofix.name);
|
192 | logger_1.logger.debug("No changes were made by autofix %s", autofix.name);
|
193 | }
|
194 | return editResult;
|
195 | }
|
196 | catch (err) {
|
197 | if (!autofix.options || !autofix.options.ignoreFailure) {
|
198 | throw err;
|
199 | }
|
200 | await project.revert();
|
201 | logger_1.logger.warn("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
|
202 | progressLog.write("Ignoring autofix failure %s on %s with autofix %s", err.message, project.id.url, autofix.name);
|
203 | return { target: project, success: false, edited: false };
|
204 | }
|
205 | }
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 | function filterImmediateAutofixes(autofixes, gi) {
|
213 | return autofixes.filter(af => !(gi.goalEvent.push.commits || [])
|
214 | .some(c => c.message === generateCommitMessageForAutofix(af)));
|
215 | }
|
216 | exports.filterImmediateAutofixes = filterImmediateAutofixes;
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 | function generateCommitMessageForAutofix(autofix) {
|
223 | const name = autofix.name.toLowerCase().replace(/ /g, "_");
|
224 | return `Autofix: ${autofix.name}\n\n[atomist:generated] [atomist:autofix=${name}]`;
|
225 | }
|
226 | exports.generateCommitMessageForAutofix = generateCommitMessageForAutofix;
|
227 | exports.AutofixProgressTests = [{
|
228 | test: /About to transform .* autofix '(.*)'/i,
|
229 | phase: "$1",
|
230 | }];
|
231 |
|
232 |
|
233 |
|
234 | exports.AutofixProgressReporter = progress_1.testProgressReporter(...exports.AutofixProgressTests);
|
235 | const NoOpExtractAuthor = async () => {
|
236 | return undefined;
|
237 | };
|
238 | exports.NoOpExtractAuthor = NoOpExtractAuthor;
|
239 | const DefaultExtractAuthor = async (gi) => {
|
240 | const { goalEvent } = gi;
|
241 | const name = _.get(goalEvent, "push.after.author.name");
|
242 | const email = _.get(goalEvent, "push.after.author.emails[0].address");
|
243 | if (!!name && !!email) {
|
244 | return {
|
245 | name,
|
246 | email,
|
247 | };
|
248 | }
|
249 | else {
|
250 | return undefined;
|
251 | }
|
252 | };
|
253 | exports.DefaultExtractAuthor = DefaultExtractAuthor;
|
254 |
|
\ | No newline at end of file |