Skip to content

Commit 58111cf

Browse files
committed
Add ssh command to approve or fail a build
Fixes: QTBI-1546 Change-Id: I384c2c5c4d236d4b83add92b1cb74886427ec1eb Reviewed-by: Paul Wicking <[email protected]>
1 parent bc4a6ad commit 58111cf

File tree

5 files changed

+748
-0
lines changed

5 files changed

+748
-0
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//
2+
// Copyright (C) 2019 The Qt Company
3+
//
4+
5+
package com.googlesource.gerrit.plugins.qtcodereview;
6+
7+
import com.google.common.flogger.FluentLogger;
8+
import com.google.gerrit.extensions.restapi.AuthException;
9+
import com.google.gerrit.extensions.restapi.RestApiException;
10+
import com.google.gerrit.reviewdb.client.Branch;
11+
import com.google.gerrit.reviewdb.client.Change;
12+
import com.google.gerrit.reviewdb.client.PatchSet;
13+
import com.google.gerrit.reviewdb.client.Project;
14+
import com.google.gerrit.reviewdb.server.ReviewDb;
15+
import com.google.gerrit.server.ChangeMessagesUtil;
16+
import com.google.gerrit.server.IdentifiedUser;
17+
import com.google.gerrit.server.extensions.events.ChangeMerged;
18+
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
19+
import com.google.gerrit.server.git.GitRepositoryManager;
20+
import com.google.gerrit.server.mail.send.MergedSender;
21+
import com.google.gerrit.server.mail.send.RevertedSender;
22+
import com.google.gerrit.server.permissions.PermissionBackend;
23+
import com.google.gerrit.server.permissions.PermissionBackendException;
24+
import com.google.gerrit.server.permissions.ProjectPermission;
25+
import com.google.gerrit.server.permissions.RefPermission;
26+
import com.google.gerrit.server.project.NoSuchProjectException;
27+
import com.google.gerrit.server.project.NoSuchRefException;
28+
import com.google.gerrit.server.query.change.ChangeData;
29+
import com.google.gerrit.server.update.BatchUpdate;
30+
import com.google.gerrit.server.update.UpdateException;
31+
import com.google.gerrit.server.util.time.TimeUtil;
32+
import com.google.gerrit.sshd.SshCommand;
33+
import com.google.gerrit.sshd.CommandMetaData;
34+
35+
import com.google.gwtorm.server.OrmException;
36+
37+
import com.google.inject.Inject;
38+
import com.google.inject.Provider;
39+
40+
import org.eclipse.jgit.errors.MissingObjectException;
41+
import org.eclipse.jgit.errors.RepositoryNotFoundException;
42+
import org.eclipse.jgit.lib.ObjectId;
43+
import org.eclipse.jgit.lib.PersonIdent;
44+
import org.eclipse.jgit.lib.RefUpdate.Result;
45+
import org.eclipse.jgit.lib.Repository;
46+
import org.eclipse.jgit.revwalk.RevCommit;
47+
import org.kohsuke.args4j.Option;
48+
49+
import java.io.BufferedReader;
50+
import java.io.IOException;
51+
import java.io.InputStreamReader;
52+
import java.sql.Timestamp;
53+
import java.util.List;
54+
import java.util.Map.Entry;
55+
56+
57+
/**
58+
* A command to report pass or fail status for builds. When a build receives
59+
* pass status, the branch is updated with build ref and all open changes in
60+
* the build are marked as merged. When a build receives fail status, all
61+
* change in the build are marked as new and they need to be staged again.
62+
*
63+
* For example, how to approve a build
64+
* $ ssh -p 29418 localhost gerrit-plugin-qt-workflow staging-approve -p project -b master -i 123 -r=pass
65+
*/
66+
@CommandMetaData(name="staging-approve", description="Report pass or fail status for builds. If passed changed are merged into target branch.")
67+
class QtCommandBuildApprove extends SshCommand {
68+
69+
@Inject
70+
private PermissionBackend permissionBackend;
71+
72+
@Inject
73+
private GitRepositoryManager gitManager;
74+
75+
@Inject
76+
private Provider<ReviewDb> dbProvider;
77+
78+
@Inject
79+
private MergedSender.Factory mergedSenderFactory;
80+
81+
@Inject
82+
private RevertedSender.Factory revertedSenderFactory;
83+
84+
@Inject
85+
private BatchUpdate.Factory updateFactory;
86+
87+
@Inject
88+
private GitReferenceUpdated referenceUpdated;
89+
90+
@Inject
91+
private ChangeMerged changeMerged;
92+
93+
@Inject
94+
private QtUtil qtUtil;
95+
96+
@Inject
97+
private QtChangeUpdateOp.Factory qtUpdateFactory;
98+
99+
@Option(name = "--project", aliases = {"-p"},
100+
required = true, usage = "project name")
101+
private String project;
102+
103+
@Option(name = "--build-id", aliases = {"-i"},
104+
required = true, usage = "build branch containing changes, e.g. refs/builds/123 or 123")
105+
private String buildBranch;
106+
107+
@Option(name = "--result", aliases = {"-r"},
108+
required = true, usage = "pass or fail")
109+
private String result;
110+
111+
@Option(name = "--message", aliases = {"-m"}, metaVar ="-|MESSAGE",
112+
usage = "message added to all changes")
113+
private String message;
114+
115+
@Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH",
116+
required = true, usage = "destination branch, e.g. refs/heads/master or just master")
117+
private String destBranch;
118+
119+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
120+
121+
private Repository git;
122+
123+
/** Parameter value for pass result. */
124+
private static final String PASS = "pass";
125+
/** Parameter value for fail result. */
126+
private static final String FAIL = "fail";
127+
/** Parameter value for stdin message. */
128+
private static final String STDIN_MESSAGE = "-";
129+
130+
private Project.NameKey projectKey;
131+
private Branch.NameKey buildBranchKey;
132+
private Branch.NameKey destBranchKey;
133+
private Branch.NameKey stagingBranchKey;
134+
private Branch.NameKey destBranchShortKey;
135+
136+
private List<Entry<ChangeData,RevCommit>> affectedChanges = null;
137+
138+
@Override
139+
protected void run() throws UnloggedFailure {
140+
logger.atInfo().log("qtcodereview: staging-approve -p %s -i %s -r %s -m %s -b %s",
141+
project, buildBranch, result, message, destBranch);
142+
143+
readMessageParameter();
144+
145+
projectKey = QtUtil.getProjectKey(project);
146+
buildBranchKey = QtUtil.getNameKeyLong(project, QtUtil.R_BUILDS, buildBranch);
147+
destBranchKey = QtUtil.getNameKeyLong(project, QtUtil.R_HEADS, destBranch);
148+
stagingBranchKey = QtUtil.getNameKeyLong(project, QtUtil.R_STAGING, destBranch);
149+
destBranchShortKey = QtUtil.getNameKeyShort(project, QtUtil.R_HEADS, destBranch);
150+
151+
try {
152+
git = gitManager.openRepository(projectKey);
153+
154+
// Check required permissions
155+
permissionBackend.user(user).project(projectKey).ref(destBranchKey.get()).check(RefPermission.UPDATE);
156+
permissionBackend.user(user).project(projectKey).ref(stagingBranchKey.get()).check(RefPermission.UPDATE);
157+
permissionBackend.user(user).project(projectKey).ref(buildBranchKey.get()).check(RefPermission.READ);
158+
159+
if (git.resolve(destBranchKey.get()) == null) throw die("branch not found");
160+
if (git.resolve(buildBranchKey.get()) == null) throw die("build not found");
161+
162+
// Initialize and populate open changes list.
163+
affectedChanges = qtUtil.listChangesNotMerged(git, buildBranchKey, destBranchKey);
164+
165+
// Notify user that build did not have any open changes. The build has already been approved.
166+
if (affectedChanges.isEmpty()) {
167+
logger.atInfo().log("qtcodereview: staging-approve build %s already in project %s branch %s",
168+
buildBranch, projectKey, destBranchKey);
169+
throw die("No open changes in the build branch");
170+
}
171+
172+
if (result.toLowerCase().equals(PASS)) {
173+
approveBuildChanges();
174+
} else if (result.toLowerCase().equals(FAIL)) {
175+
rejectBuildChanges();
176+
} else {
177+
throw die("result argument accepts only value pass or fail.");
178+
}
179+
180+
} catch (AuthException e) {
181+
throw die("not authorized");
182+
} catch (PermissionBackendException e) {
183+
throw die("git permission error");
184+
} catch (RepositoryNotFoundException e) {
185+
throw die("project not found");
186+
} catch (IOException e) {
187+
throw die(e.getMessage());
188+
} catch (OrmException e) {
189+
throw die("Failed to access database");
190+
} catch (QtUtil.BranchNotFoundException e) {
191+
throw die("invalid branch " + e.getMessage());
192+
} catch (NoSuchRefException e) {
193+
throw die("invalid reference " + e.getMessage());
194+
} catch (UpdateException | RestApiException e) {
195+
logger.atSevere().log("qtcodereview: staging-napprove failed to update change status %s", e);
196+
throw die("Failed to update change status");
197+
} catch (QtUtil.MergeConflictException e) {
198+
String msg = String.format("Merge build %s to branch %s failed", buildBranch, destBranchKey);
199+
logger.atSevere().log("qtcodereview: %s", msg);
200+
throw die(String.format("Merge conflict! build branch %s into %s failed", buildBranch, destBranch));
201+
} finally {
202+
if (git != null) git.close();
203+
}
204+
}
205+
206+
private void approveBuildChanges() throws QtUtil.MergeConflictException, NoSuchRefException,
207+
IOException, UpdateException, RestApiException,
208+
OrmException {
209+
if (message == null) message = String.format("Change merged into branch %s", destBranchKey);
210+
211+
ObjectId oldId = git.resolve(destBranchKey.get());
212+
213+
QtUtil.mergeBranches(user.asIdentifiedUser(), git, buildBranchKey, destBranchKey);
214+
215+
updateChanges(affectedChanges, Change.Status.MERGED, null,
216+
message, ChangeMessagesUtil.TAG_MERGED, true);
217+
218+
logger.atInfo().log("qtcodereview: staging-approve build %s merged into branch %s",
219+
buildBranch, destBranchKey);
220+
221+
ObjectId newId = git.resolve(destBranchKey.get());
222+
// send ref updated event only if there are changes to build
223+
if (!newId.equals(oldId)) {
224+
referenceUpdated.fire(projectKey, destBranchKey.get(), oldId, newId, user.asIdentifiedUser().state());
225+
}
226+
}
227+
228+
private void rejectBuildChanges() throws QtUtil.MergeConflictException, UpdateException,
229+
RestApiException, OrmException {
230+
if (message == null) message = String.format("Change rejected for branch %s", destBranchKey);
231+
232+
updateChanges(affectedChanges, Change.Status.NEW, Change.Status.INTEGRATING,
233+
message, ChangeMessagesUtil.TAG_REVERT, false);
234+
235+
// need to rebuild the staging ref because the reject changes need to be removed from there
236+
qtUtil.rebuildStagingBranch(git, user.asIdentifiedUser(), projectKey, stagingBranchKey, destBranchShortKey);
237+
238+
logger.atInfo().log("qtcodereview: staging-approve build %s rejected for branch %s",
239+
buildBranch, destBranchKey);
240+
}
241+
242+
private void updateChanges(List<Entry<ChangeData,RevCommit>> list,
243+
Change.Status status,
244+
Change.Status oldStatus,
245+
String changeMessage,
246+
String tag,
247+
Boolean passed)
248+
throws UpdateException, RestApiException, OrmException {
249+
// do the db update
250+
QtChangeUpdateOp op = qtUpdateFactory.create(status, changeMessage, null, tag, null);
251+
try (BatchUpdate u = updateFactory.create(dbProvider.get(), projectKey, user, TimeUtil.nowTs())) {
252+
for (Entry<ChangeData,RevCommit> item : list) {
253+
Change change = item.getKey().change();
254+
if (oldStatus == null || change.getStatus() == oldStatus) {
255+
u.addOp(change.getId(), op);
256+
}
257+
}
258+
u.execute();
259+
}
260+
261+
// do rest
262+
for (Entry<ChangeData,RevCommit> item : list) {
263+
ChangeData cd = item.getKey();
264+
Change change = cd.change();
265+
if (passed) {
266+
sendMergeEvent(cd);
267+
logger.atInfo().log("qtcodereview: staging-approve change %s merged into %s",
268+
change, destBranchKey);
269+
} else {
270+
logger.atInfo().log("qtcodereview: staging-approve change %s rejected for %s",
271+
change, destBranchKey);
272+
}
273+
274+
sendResultEmail(change.getId(), passed);
275+
}
276+
}
277+
278+
private void sendMergeEvent(ChangeData changeData) throws OrmException {
279+
Timestamp ts = TimeUtil.nowTs();
280+
281+
try {
282+
PatchSet ps = changeData.currentPatchSet();
283+
changeMerged.fire(changeData.change(), ps, user.asIdentifiedUser().state(),
284+
ps.getRevision().get(), ts);
285+
} catch ( OrmException e) {
286+
logger.atInfo().log("qtcodereview: staging-approve sending merge event failed for %s",
287+
changeData.change());
288+
}
289+
}
290+
291+
private void readMessageParameter() throws UnloggedFailure {
292+
if (message == null) return;
293+
294+
try {
295+
// User will submit message through stdin.
296+
if (message.equals(STDIN_MESSAGE)) {
297+
// Clear stdin indicator.
298+
message = "";
299+
300+
// Read message from stdin.
301+
BufferedReader stdin = new BufferedReader(new InputStreamReader(in, "UTF-8"));
302+
String line;
303+
while ((line = stdin.readLine()) != null) {
304+
message += line + "\n";
305+
}
306+
}
307+
} catch (IOException e) {
308+
throw new UnloggedFailure(1, "fatal: " + e.getMessage(), e);
309+
}
310+
}
311+
312+
private void sendResultEmail(Change.Id changeId, Boolean passed) {
313+
try {
314+
if (passed) {
315+
MergedSender mcm = mergedSenderFactory.create(projectKey, changeId);
316+
mcm.send();
317+
} else {
318+
RevertedSender rcm = revertedSenderFactory.create(projectKey, changeId);
319+
rcm.send();
320+
}
321+
} catch (Exception e) {
322+
logger.atWarning().log("qtcodereview: staging-approve Cannot email notification for %s %s", changeId, e);
323+
}
324+
}
325+
326+
}

src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtSshModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class QtSshModule extends PluginCommandModule {
1111
@Override
1212
protected void configureCommands() {
1313
command(QtCommandPing.class);
14+
command(QtCommandBuildApprove.class);
1415
command(QtCommandNewBuild.class);
1516
command(QtCommandListStaging.class);
1617
command(QtCommandRebuildStaging.class);

0 commit comments

Comments
 (0)