Line data Source code
1 : // Copyright (C) 2014 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.change;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
20 : import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
21 : import static java.util.Comparator.comparing;
22 : import static java.util.Objects.requireNonNull;
23 :
24 : import com.google.auto.value.AutoValue;
25 : import com.google.common.collect.Collections2;
26 : import com.google.common.collect.ImmutableList;
27 : import com.google.common.collect.Iterables;
28 : import com.google.common.collect.MultimapBuilder;
29 : import com.google.common.collect.SetMultimap;
30 : import com.google.common.flogger.FluentLogger;
31 : import com.google.gerrit.common.Nullable;
32 : import com.google.gerrit.entities.Change;
33 : import com.google.gerrit.entities.PatchSet;
34 : import com.google.gerrit.entities.Project;
35 : import com.google.gerrit.entities.SubmissionId;
36 : import com.google.gerrit.exceptions.StorageException;
37 : import com.google.gerrit.extensions.api.changes.FixInput;
38 : import com.google.gerrit.extensions.common.ProblemInfo;
39 : import com.google.gerrit.extensions.common.ProblemInfo.Status;
40 : import com.google.gerrit.extensions.registration.DynamicItem;
41 : import com.google.gerrit.extensions.restapi.RestApiException;
42 : import com.google.gerrit.server.ChangeUtil;
43 : import com.google.gerrit.server.CurrentUser;
44 : import com.google.gerrit.server.GerritPersonIdent;
45 : import com.google.gerrit.server.PatchSetUtil;
46 : import com.google.gerrit.server.account.Accounts;
47 : import com.google.gerrit.server.config.UrlFormatter;
48 : import com.google.gerrit.server.git.GitRepositoryManager;
49 : import com.google.gerrit.server.notedb.ChangeNotes;
50 : import com.google.gerrit.server.notedb.PatchSetState;
51 : import com.google.gerrit.server.patch.PatchSetInfoFactory;
52 : import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
53 : import com.google.gerrit.server.plugincontext.PluginItemContext;
54 : import com.google.gerrit.server.update.BatchUpdate;
55 : import com.google.gerrit.server.update.BatchUpdateOp;
56 : import com.google.gerrit.server.update.ChangeContext;
57 : import com.google.gerrit.server.update.RepoContext;
58 : import com.google.gerrit.server.update.RetryHelper;
59 : import com.google.gerrit.server.update.UpdateException;
60 : import com.google.gerrit.server.util.time.TimeUtil;
61 : import com.google.inject.Inject;
62 : import com.google.inject.Provider;
63 : import java.io.IOException;
64 : import java.util.ArrayList;
65 : import java.util.Collection;
66 : import java.util.Collections;
67 : import java.util.HashSet;
68 : import java.util.List;
69 : import java.util.Locale;
70 : import java.util.Map;
71 : import java.util.Set;
72 : import java.util.TreeSet;
73 : import org.eclipse.jgit.errors.ConfigInvalidException;
74 : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
75 : import org.eclipse.jgit.errors.MissingObjectException;
76 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
77 : import org.eclipse.jgit.lib.ObjectId;
78 : import org.eclipse.jgit.lib.ObjectInserter;
79 : import org.eclipse.jgit.lib.PersonIdent;
80 : import org.eclipse.jgit.lib.Ref;
81 : import org.eclipse.jgit.lib.RefUpdate;
82 : import org.eclipse.jgit.lib.Repository;
83 : import org.eclipse.jgit.revwalk.RevCommit;
84 : import org.eclipse.jgit.revwalk.RevWalk;
85 :
86 : /**
87 : * Checks changes for various kinds of inconsistency and corruption.
88 : *
89 : * <p>A single instance may be reused for checking multiple changes, but not concurrently.
90 : */
91 : public class ConsistencyChecker {
92 5 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
93 :
94 : @AutoValue
95 5 : public abstract static class Result {
96 : private static Result create(ChangeNotes notes, List<ProblemInfo> problems) {
97 5 : return new AutoValue_ConsistencyChecker_Result(
98 5 : notes.getChangeId(), notes.getChange(), ImmutableList.copyOf(problems));
99 : }
100 :
101 : public abstract Change.Id id();
102 :
103 : @Nullable
104 : public abstract Change change();
105 :
106 : public abstract ImmutableList<ProblemInfo> problems();
107 : }
108 :
109 : private final ChangeNotes.Factory notesFactory;
110 : private final Accounts accounts;
111 : private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
112 : private final GitRepositoryManager repoManager;
113 : private final PatchSetInfoFactory patchSetInfoFactory;
114 : private final PatchSetInserter.Factory patchSetInserterFactory;
115 : private final PatchSetUtil psUtil;
116 : private final Provider<CurrentUser> user;
117 : private final Provider<PersonIdent> serverIdent;
118 : private final RetryHelper retryHelper;
119 : private final DynamicItem<UrlFormatter> urlFormatter;
120 :
121 : private BatchUpdate.Factory updateFactory;
122 : private FixInput fix;
123 : private ChangeNotes notes;
124 : private Repository repo;
125 : private RevWalk rw;
126 : private ObjectInserter oi;
127 :
128 : private RevCommit tip;
129 : private SetMultimap<ObjectId, PatchSet> patchSetsBySha;
130 : private PatchSet currPs;
131 : private RevCommit currPsCommit;
132 :
133 : private List<ProblemInfo> problems;
134 :
135 : @Inject
136 : ConsistencyChecker(
137 : @GerritPersonIdent Provider<PersonIdent> serverIdent,
138 : ChangeNotes.Factory notesFactory,
139 : Accounts accounts,
140 : PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
141 : GitRepositoryManager repoManager,
142 : PatchSetInfoFactory patchSetInfoFactory,
143 : PatchSetInserter.Factory patchSetInserterFactory,
144 : PatchSetUtil psUtil,
145 : Provider<CurrentUser> user,
146 : RetryHelper retryHelper,
147 5 : DynamicItem<UrlFormatter> urlFormatter) {
148 5 : this.accounts = accounts;
149 5 : this.accountPatchReviewStore = accountPatchReviewStore;
150 5 : this.notesFactory = notesFactory;
151 5 : this.patchSetInfoFactory = patchSetInfoFactory;
152 5 : this.patchSetInserterFactory = patchSetInserterFactory;
153 5 : this.psUtil = psUtil;
154 5 : this.repoManager = repoManager;
155 5 : this.retryHelper = retryHelper;
156 5 : this.serverIdent = serverIdent;
157 5 : this.user = user;
158 5 : this.urlFormatter = urlFormatter;
159 5 : reset();
160 5 : }
161 :
162 : private void reset() {
163 5 : updateFactory = null;
164 5 : notes = null;
165 5 : repo = null;
166 5 : rw = null;
167 5 : problems = new ArrayList<>();
168 5 : }
169 :
170 : private Change change() {
171 5 : return notes.getChange();
172 : }
173 :
174 : public Result check(ChangeNotes notes, @Nullable FixInput f) {
175 5 : requireNonNull(notes);
176 : try {
177 5 : return retryHelper
178 5 : .changeUpdate(
179 : "checkChangeConsistency",
180 : buf -> {
181 : try {
182 5 : reset();
183 5 : this.updateFactory = buf;
184 5 : this.notes = notes;
185 5 : fix = f;
186 5 : checkImpl();
187 5 : return result();
188 : } finally {
189 5 : if (rw != null) {
190 5 : rw.getObjectReader().close();
191 5 : rw.close();
192 5 : oi.close();
193 : }
194 5 : if (repo != null) {
195 5 : repo.close();
196 : }
197 : }
198 : })
199 5 : .call();
200 0 : } catch (RestApiException e) {
201 0 : return logAndReturnOneProblem(e, notes, "Error checking change: " + e.getMessage());
202 0 : } catch (UpdateException e) {
203 0 : return logAndReturnOneProblem(e, notes, "Error checking change");
204 : }
205 : }
206 :
207 : private Result logAndReturnOneProblem(Exception e, ChangeNotes notes, String problem) {
208 0 : logger.atWarning().withCause(e).log("Error checking change %s", notes.getChangeId());
209 0 : return Result.create(notes, ImmutableList.of(problem(problem)));
210 : }
211 :
212 : private void checkImpl() {
213 5 : checkOwner();
214 5 : checkCurrentPatchSetEntity();
215 :
216 : // All checks that require the repo.
217 5 : if (!openRepo()) {
218 0 : return;
219 : }
220 5 : if (!checkPatchSets()) {
221 1 : return;
222 : }
223 5 : checkMerged();
224 5 : }
225 :
226 : private void checkOwner() {
227 : try {
228 5 : if (!accounts.get(change().getOwner()).isPresent()) {
229 1 : problem("Missing change owner: " + change().getOwner());
230 : }
231 0 : } catch (IOException | ConfigInvalidException e) {
232 0 : ProblemInfo problem = problem("Failed to look up owner");
233 0 : logger.atWarning().withCause(e).log(
234 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
235 5 : }
236 5 : }
237 :
238 : private void checkCurrentPatchSetEntity() {
239 : try {
240 5 : currPs = psUtil.current(notes);
241 5 : if (currPs == null) {
242 0 : problem(
243 0 : String.format("Current patch set %d not found", change().currentPatchSetId().get()));
244 : }
245 0 : } catch (StorageException e) {
246 0 : ProblemInfo problem = problem("Failed to look up current patch set");
247 0 : logger.atWarning().withCause(e).log(
248 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
249 5 : }
250 5 : }
251 :
252 : private boolean openRepo() {
253 5 : Project.NameKey project = change().getDest().project();
254 : try {
255 5 : repo = repoManager.openRepository(project);
256 5 : oi = repo.newObjectInserter();
257 5 : rw = new RevWalk(oi.newReader());
258 5 : return true;
259 0 : } catch (RepositoryNotFoundException e) {
260 0 : ProblemInfo problem = problem("Destination repository not found: " + project);
261 0 : logger.atWarning().withCause(e).log(
262 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
263 0 : return false;
264 0 : } catch (IOException e) {
265 0 : ProblemInfo problem = problem("Failed to open repository: " + project);
266 0 : logger.atWarning().withCause(e).log(
267 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
268 0 : return false;
269 : }
270 : }
271 :
272 : private boolean checkPatchSets() {
273 : List<PatchSet> all;
274 : try {
275 : // Iterate in descending order.
276 5 : all = PS_ID_ORDER.sortedCopy(psUtil.byChange(notes));
277 0 : } catch (StorageException e) {
278 0 : ProblemInfo problem = problem("Failed to look up patch sets");
279 0 : logger.atWarning().withCause(e).log(
280 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
281 0 : return false;
282 5 : }
283 5 : patchSetsBySha = MultimapBuilder.hashKeys(all.size()).treeSetValues(PS_ID_ORDER).build();
284 :
285 : Map<String, Ref> refs;
286 : try {
287 5 : refs =
288 5 : repo.getRefDatabase()
289 5 : .exactRef(all.stream().map(ps -> ps.id().toRefName()).toArray(String[]::new));
290 0 : } catch (IOException e) {
291 0 : ProblemInfo problem = problem("Error reading refs");
292 0 : logger.atWarning().withCause(e).log(
293 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
294 0 : refs = Collections.emptyMap();
295 5 : }
296 :
297 5 : List<DeletePatchSetFromDbOp> deletePatchSetOps = new ArrayList<>();
298 5 : for (PatchSet ps : all) {
299 : // Check revision format.
300 5 : int psNum = ps.id().get();
301 5 : String refName = ps.id().toRefName();
302 5 : ObjectId objId = ps.commitId();
303 5 : patchSetsBySha.put(objId, ps);
304 :
305 : // Check ref existence.
306 5 : ProblemInfo refProblem = null;
307 5 : Ref ref = refs.get(refName);
308 5 : if (ref == null) {
309 1 : refProblem = problem("Ref missing: " + refName);
310 5 : } else if (!objId.equals(ref.getObjectId())) {
311 0 : String actual = ref.getObjectId() != null ? ref.getObjectId().name() : "null";
312 0 : refProblem =
313 0 : problem(
314 0 : String.format(
315 0 : "Expected %s to point to %s, found %s", ref.getName(), objId.name(), actual));
316 : }
317 :
318 : // Check object existence.
319 5 : RevCommit psCommit = parseCommit(objId, String.format("patch set %d", psNum));
320 5 : if (psCommit == null) {
321 1 : if (fix != null && fix.deletePatchSetIfCommitMissing) {
322 1 : deletePatchSetOps.add(new DeletePatchSetFromDbOp(lastProblem(), ps.id()));
323 : }
324 : continue;
325 5 : } else if (refProblem != null && fix != null) {
326 1 : fixPatchSetRef(refProblem, ps);
327 : }
328 5 : if (ps.id().equals(change().currentPatchSetId())) {
329 5 : currPsCommit = psCommit;
330 : }
331 5 : }
332 :
333 : // Delete any bad patch sets found above, in a single update.
334 5 : deletePatchSets(deletePatchSetOps);
335 :
336 : // Check for duplicates.
337 5 : for (Map.Entry<ObjectId, Collection<PatchSet>> e : patchSetsBySha.asMap().entrySet()) {
338 5 : if (e.getValue().size() > 1) {
339 1 : problem(
340 1 : String.format(
341 : "Multiple patch sets pointing to %s: %s",
342 1 : e.getKey().name(), Collections2.transform(e.getValue(), PatchSet::number)));
343 : }
344 5 : }
345 :
346 5 : return currPs != null && currPsCommit != null;
347 : }
348 :
349 : private void checkMerged() {
350 5 : String refName = change().getDest().branch();
351 : Ref dest;
352 : try {
353 5 : dest = repo.getRefDatabase().exactRef(refName);
354 0 : } catch (IOException e) {
355 0 : problem("Failed to look up destination ref: " + refName);
356 0 : return;
357 5 : }
358 5 : if (dest == null) {
359 1 : problem("Destination ref not found (may be new branch): " + refName);
360 1 : return;
361 : }
362 5 : tip = parseCommit(dest.getObjectId(), "destination ref " + refName);
363 5 : if (tip == null) {
364 0 : return;
365 : }
366 :
367 5 : if (fix != null && fix.expectMergedAs != null) {
368 2 : checkExpectMergedAs();
369 : } else {
370 : boolean merged;
371 : try {
372 5 : merged = rw.isMergedInto(currPsCommit, tip);
373 0 : } catch (IOException e) {
374 0 : problem("Error checking whether patch set " + currPs.id().get() + " is merged");
375 0 : return;
376 5 : }
377 5 : checkMergedBitMatchesStatus(currPs.id(), currPsCommit, merged);
378 : }
379 5 : }
380 :
381 : private ProblemInfo wrongChangeStatus(PatchSet.Id psId, RevCommit commit) {
382 2 : String refName = change().getDest().branch();
383 2 : return problem(
384 2 : formatProblemMessage(
385 : "Patch set %d (%s) is merged into destination ref %s (%s), but change"
386 : + " status is %s",
387 2 : psId.get(), commit.name(), refName, tip.name()));
388 : }
389 :
390 : private void checkMergedBitMatchesStatus(PatchSet.Id psId, RevCommit commit, boolean merged) {
391 5 : String refName = change().getDest().branch();
392 5 : if (merged && !change().isMerged()) {
393 2 : ProblemInfo p = wrongChangeStatus(psId, commit);
394 2 : if (fix != null) {
395 1 : fixMerged(p);
396 : }
397 5 : } else if (!merged && change().isMerged()) {
398 1 : problem(
399 1 : formatProblemMessage(
400 : "Patch set %d (%s) is not merged into"
401 : + " destination ref %s (%s), but change status is %s",
402 1 : currPs.id().get(), commit.name(), refName, tip.name()));
403 : }
404 5 : }
405 :
406 : private String formatProblemMessage(
407 : String message, int psId, String commitName, String refName, String tipName) {
408 2 : return String.format(
409 : message,
410 2 : psId,
411 : commitName,
412 : refName,
413 : tipName,
414 2 : ChangeUtil.status(change()).toUpperCase(Locale.US));
415 : }
416 :
417 : private void checkExpectMergedAs() {
418 2 : if (!ObjectId.isId(fix.expectMergedAs)) {
419 0 : problem("Invalid revision on expected merged commit: " + fix.expectMergedAs);
420 0 : return;
421 : }
422 2 : ObjectId objId = ObjectId.fromString(fix.expectMergedAs);
423 2 : RevCommit commit = parseCommit(objId, "expected merged commit");
424 2 : if (commit == null) {
425 0 : return;
426 : }
427 :
428 : try {
429 2 : if (!rw.isMergedInto(commit, tip)) {
430 1 : problem(
431 1 : String.format(
432 : "Expected merged commit %s is not merged into destination ref %s (%s)",
433 1 : commit.name(), change().getDest().branch(), tip.name()));
434 1 : return;
435 : }
436 :
437 2 : List<PatchSet.Id> thisCommitPsIds = new ArrayList<>();
438 2 : for (Ref ref : repo.getRefDatabase().getRefsByPrefix(REFS_CHANGES)) {
439 2 : if (!ref.getObjectId().equals(commit)) {
440 2 : continue;
441 : }
442 2 : PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
443 2 : if (psId == null) {
444 0 : continue;
445 : }
446 : try {
447 2 : Change c = notesFactory.createChecked(change().getProject(), psId.changeId()).getChange();
448 2 : if (!c.getDest().equals(change().getDest())) {
449 0 : continue;
450 : }
451 0 : } catch (StorageException e) {
452 0 : logger.atWarning().withCause(e).log(
453 0 : "Error in consistency check of change %s", notes.getChangeId());
454 : // Include this patch set; should cause an error below, which is good.
455 2 : }
456 2 : thisCommitPsIds.add(psId);
457 2 : }
458 2 : switch (thisCommitPsIds.size()) {
459 : case 0:
460 : // No patch set for this commit; insert one.
461 2 : rw.parseBody(commit);
462 2 : String changeId =
463 2 : Iterables.getFirst(
464 2 : ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get()), null);
465 : // Missing Change-Id footer is ok, but mismatched is not.
466 2 : if (changeId != null && !changeId.equals(change().getKey().get())) {
467 1 : problem(
468 1 : String.format(
469 : "Expected merged commit %s has Change-Id: %s, but expected %s",
470 1 : commit.name(), changeId, change().getKey().get()));
471 1 : return;
472 : }
473 2 : insertMergedPatchSet(commit, null, false);
474 2 : break;
475 :
476 : case 1:
477 : // Existing patch set ref pointing to this commit.
478 2 : PatchSet.Id id = thisCommitPsIds.get(0);
479 2 : if (id.equals(change().currentPatchSetId())) {
480 : // If it's the current patch set, we can just fix the status.
481 2 : fixMerged(wrongChangeStatus(id, commit));
482 1 : } else if (id.get() > change().currentPatchSetId().get()) {
483 : // If it's newer than the current patch set, reuse this patch set
484 : // ID when inserting a new merged patch set.
485 1 : insertMergedPatchSet(commit, id, true);
486 : } else {
487 : // If it's older than the current patch set, just delete the old
488 : // ref, and use a new ID when inserting a new merged patch set.
489 1 : insertMergedPatchSet(commit, id, false);
490 : }
491 1 : break;
492 :
493 : default:
494 1 : problem(
495 1 : String.format(
496 : "Multiple patch sets for expected merged commit %s: %s",
497 1 : commit.name(),
498 1 : thisCommitPsIds.stream()
499 1 : .sorted(comparing(PatchSet.Id::get))
500 1 : .collect(toImmutableList())));
501 : break;
502 : }
503 0 : } catch (IOException e) {
504 0 : ProblemInfo problem =
505 0 : problem("Error looking up expected merged commit " + fix.expectMergedAs);
506 0 : logger.atWarning().withCause(e).log(
507 0 : "Error in consistency check of change %s: %s", notes.getChangeId(), problem);
508 2 : }
509 2 : }
510 :
511 : private void insertMergedPatchSet(
512 : final RevCommit commit, @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) {
513 2 : ProblemInfo notFound = problem("No patch set found for merged commit " + commit.name());
514 2 : if (!user.get().isIdentifiedUser()) {
515 0 : notFound.status = Status.FIX_FAILED;
516 0 : notFound.outcome = "Must be called by an identified user to insert new patch set";
517 0 : return;
518 : }
519 : ProblemInfo insertPatchSetProblem;
520 : ProblemInfo deleteOldPatchSetProblem;
521 :
522 2 : if (psIdToDelete == null) {
523 2 : insertPatchSetProblem =
524 2 : problem(
525 2 : String.format(
526 2 : "Expected merged commit %s has no associated patch set", commit.name()));
527 2 : deleteOldPatchSetProblem = null;
528 : } else {
529 1 : String msg =
530 1 : String.format(
531 : "Expected merge commit %s corresponds to patch set %s,"
532 : + " not the current patch set %s",
533 1 : commit.name(), psIdToDelete.get(), change().currentPatchSetId().get());
534 : // Maybe an identical problem, but different fix.
535 1 : deleteOldPatchSetProblem = reuseOldPsId ? null : problem(msg);
536 1 : insertPatchSetProblem = problem(msg);
537 : }
538 :
539 2 : List<ProblemInfo> currProblems = new ArrayList<>(3);
540 2 : currProblems.add(notFound);
541 2 : if (deleteOldPatchSetProblem != null) {
542 1 : currProblems.add(deleteOldPatchSetProblem);
543 : }
544 2 : currProblems.add(insertPatchSetProblem);
545 :
546 : try {
547 : PatchSet.Id psId =
548 2 : (psIdToDelete != null && reuseOldPsId)
549 1 : ? psIdToDelete
550 2 : : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
551 2 : PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, commit);
552 2 : try (BatchUpdate bu = newBatchUpdate()) {
553 2 : bu.setRepository(repo, rw, oi);
554 :
555 2 : if (psIdToDelete != null) {
556 : // Delete the given patch set ref. If reuseOldPsId is true,
557 : // PatchSetInserter will reinsert the same ref, making it a no-op.
558 1 : bu.addOp(
559 1 : notes.getChangeId(),
560 1 : new BatchUpdateOp() {
561 : @Override
562 : public void updateRepo(RepoContext ctx) throws IOException {
563 1 : ctx.addRefUpdate(commit, ObjectId.zeroId(), psIdToDelete.toRefName());
564 1 : }
565 : });
566 1 : if (!reuseOldPsId) {
567 1 : bu.addOp(
568 1 : notes.getChangeId(),
569 1 : new DeletePatchSetFromDbOp(requireNonNull(deleteOldPatchSetProblem), psIdToDelete));
570 : }
571 : }
572 :
573 2 : bu.setNotify(NotifyResolver.Result.none());
574 2 : bu.addOp(
575 2 : notes.getChangeId(),
576 : inserter
577 2 : .setValidate(false)
578 2 : .setFireRevisionCreated(false)
579 2 : .setAllowClosed(true)
580 2 : .setMessage("Patch set for merged commit inserted by consistency checker"));
581 2 : bu.addOp(notes.getChangeId(), new FixMergedOp(notFound));
582 2 : bu.execute();
583 : }
584 2 : notes = notesFactory.createChecked(inserter.getChange());
585 2 : insertPatchSetProblem.status = Status.FIXED;
586 2 : insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get();
587 0 : } catch (StorageException | IOException | UpdateException | RestApiException e) {
588 0 : logger.atWarning().withCause(e).log(
589 0 : "Error in consistency check of change %s", notes.getChangeId());
590 0 : for (ProblemInfo pi : currProblems) {
591 0 : pi.status = Status.FIX_FAILED;
592 0 : pi.outcome = "Error inserting merged patch set";
593 0 : }
594 0 : return;
595 2 : }
596 2 : }
597 :
598 : private static class FixMergedOp implements BatchUpdateOp {
599 : private final ProblemInfo p;
600 :
601 2 : private FixMergedOp(ProblemInfo p) {
602 2 : this.p = p;
603 2 : }
604 :
605 : @Override
606 : public boolean updateChange(ChangeContext ctx) {
607 2 : ctx.getChange().setStatus(Change.Status.MERGED);
608 2 : ctx.getUpdate(ctx.getChange().currentPatchSetId())
609 2 : .fixStatusToMerged(new SubmissionId(ctx.getChange()));
610 2 : p.status = Status.FIXED;
611 2 : p.outcome = "Marked change as merged";
612 2 : return true;
613 : }
614 : }
615 :
616 : private void fixMerged(ProblemInfo p) {
617 2 : try (BatchUpdate bu = newBatchUpdate()) {
618 2 : bu.setRepository(repo, rw, oi);
619 2 : bu.addOp(notes.getChangeId(), new FixMergedOp(p));
620 2 : bu.execute();
621 0 : } catch (UpdateException | RestApiException e) {
622 0 : logger.atWarning().withCause(e).log("Error marking %s as merged", notes.getChangeId());
623 0 : p.status = Status.FIX_FAILED;
624 0 : p.outcome = "Error updating status to merged";
625 2 : }
626 2 : }
627 :
628 : private BatchUpdate newBatchUpdate() {
629 5 : return updateFactory.create(change().getProject(), user.get(), TimeUtil.now());
630 : }
631 :
632 : private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
633 : try {
634 1 : RefUpdate ru = repo.updateRef(ps.id().toRefName());
635 1 : ru.setForceUpdate(true);
636 1 : ru.setNewObjectId(ps.commitId());
637 1 : ru.setRefLogIdent(newRefLogIdent());
638 1 : ru.setRefLogMessage("Repair patch set ref", true);
639 1 : RefUpdate.Result result = ru.update();
640 1 : switch (result) {
641 : case NEW:
642 : case FORCED:
643 : case FAST_FORWARD:
644 : case NO_CHANGE:
645 1 : p.status = Status.FIXED;
646 1 : p.outcome = "Repaired patch set ref";
647 1 : return;
648 : case IO_FAILURE:
649 : case LOCK_FAILURE:
650 : case NOT_ATTEMPTED:
651 : case REJECTED:
652 : case REJECTED_CURRENT_BRANCH:
653 : case RENAMED:
654 : case REJECTED_MISSING_OBJECT:
655 : case REJECTED_OTHER_REASON:
656 : default:
657 0 : p.status = Status.FIX_FAILED;
658 0 : p.outcome = "Failed to update patch set ref: " + result;
659 0 : return;
660 : }
661 0 : } catch (IOException e) {
662 0 : String msg = "Error fixing patch set ref";
663 0 : logger.atWarning().withCause(e).log("%s %s", msg, ps.id().toRefName());
664 0 : p.status = Status.FIX_FAILED;
665 0 : p.outcome = msg;
666 : }
667 0 : }
668 :
669 : private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) {
670 5 : try (BatchUpdate bu = newBatchUpdate()) {
671 5 : bu.setRepository(repo, rw, oi);
672 5 : for (DeletePatchSetFromDbOp op : ops) {
673 1 : checkArgument(op.psId.changeId().equals(notes.getChangeId()));
674 1 : bu.addOp(notes.getChangeId(), op);
675 1 : }
676 5 : bu.addOp(notes.getChangeId(), new UpdateCurrentPatchSetOp(ops));
677 5 : bu.execute();
678 1 : } catch (NoPatchSetsWouldRemainException e) {
679 1 : for (DeletePatchSetFromDbOp op : ops) {
680 1 : op.p.status = Status.FIX_FAILED;
681 1 : op.p.outcome = e.getMessage();
682 1 : }
683 0 : } catch (UpdateException | RestApiException e) {
684 0 : String msg = "Error deleting patch set";
685 0 : logger.atWarning().withCause(e).log("%s of change %s", msg, ops.get(0).psId.changeId());
686 0 : for (DeletePatchSetFromDbOp op : ops) {
687 : // Overwrite existing statuses that were set before the transaction was
688 : // rolled back.
689 0 : op.p.status = Status.FIX_FAILED;
690 0 : op.p.outcome = msg;
691 0 : }
692 5 : }
693 5 : }
694 :
695 : private class DeletePatchSetFromDbOp implements BatchUpdateOp {
696 : private final ProblemInfo p;
697 : private final PatchSet.Id psId;
698 :
699 1 : private DeletePatchSetFromDbOp(ProblemInfo p, PatchSet.Id psId) {
700 1 : this.p = p;
701 1 : this.psId = psId;
702 1 : }
703 :
704 : @Override
705 : public boolean updateChange(ChangeContext ctx) throws PatchSetInfoNotAvailableException {
706 : // Delete dangling key references.
707 1 : accountPatchReviewStore.run(s -> s.clearReviewed(psId));
708 :
709 : // For NoteDb setting the state to deleted is sufficient to filter everything out.
710 1 : ctx.getUpdate(psId).setPatchSetState(PatchSetState.DELETED);
711 :
712 1 : p.status = Status.FIXED;
713 1 : p.outcome = "Deleted patch set";
714 1 : return true;
715 : }
716 : }
717 :
718 : private static class NoPatchSetsWouldRemainException extends RestApiException {
719 : private static final long serialVersionUID = 1L;
720 :
721 : private NoPatchSetsWouldRemainException() {
722 1 : super("Cannot delete patch set; no patch sets would remain");
723 1 : }
724 : }
725 :
726 : private class UpdateCurrentPatchSetOp implements BatchUpdateOp {
727 : private final Set<PatchSet.Id> toDelete;
728 :
729 5 : private UpdateCurrentPatchSetOp(List<DeletePatchSetFromDbOp> deleteOps) {
730 5 : toDelete = new HashSet<>();
731 5 : for (DeletePatchSetFromDbOp op : deleteOps) {
732 1 : toDelete.add(op.psId);
733 1 : }
734 5 : }
735 :
736 : @Override
737 : public boolean updateChange(ChangeContext ctx)
738 : throws PatchSetInfoNotAvailableException, NoPatchSetsWouldRemainException {
739 5 : if (!toDelete.contains(ctx.getChange().currentPatchSetId())) {
740 5 : return false;
741 : }
742 1 : TreeSet<PatchSet.Id> all = new TreeSet<>(comparing(PatchSet.Id::get));
743 : // Doesn't make any assumptions about the order in which deletes happen
744 : // and whether they are seen by this op; we are already given the full set
745 : // of patch sets that will eventually be deleted in this update.
746 1 : for (PatchSet ps : psUtil.byChange(ctx.getNotes())) {
747 1 : if (!toDelete.contains(ps.id())) {
748 1 : all.add(ps.id());
749 : }
750 1 : }
751 1 : if (all.isEmpty()) {
752 1 : throw new NoPatchSetsWouldRemainException();
753 : }
754 1 : ctx.getChange().setCurrentPatchSet(patchSetInfoFactory.get(ctx.getNotes(), all.last()));
755 1 : return true;
756 : }
757 : }
758 :
759 : private PersonIdent newRefLogIdent() {
760 1 : CurrentUser u = user.get();
761 1 : if (u.isIdentifiedUser()) {
762 1 : return u.asIdentifiedUser().newRefLogIdent();
763 : }
764 0 : return serverIdent.get();
765 : }
766 :
767 : @Nullable
768 : private RevCommit parseCommit(ObjectId objId, String desc) {
769 : try {
770 5 : return rw.parseCommit(objId);
771 1 : } catch (MissingObjectException e) {
772 1 : problem(String.format("Object missing: %s: %s", desc, objId.name()));
773 0 : } catch (IncorrectObjectTypeException e) {
774 0 : problem(String.format("Not a commit: %s: %s", desc, objId.name()));
775 0 : } catch (IOException e) {
776 0 : problem(String.format("Failed to look up: %s: %s", desc, objId.name()));
777 1 : }
778 1 : return null;
779 : }
780 :
781 : private ProblemInfo problem(String msg) {
782 2 : ProblemInfo p = new ProblemInfo();
783 2 : p.message = requireNonNull(msg);
784 2 : problems.add(p);
785 2 : return p;
786 : }
787 :
788 : private ProblemInfo lastProblem() {
789 1 : return problems.get(problems.size() - 1);
790 : }
791 :
792 : private Result result() {
793 5 : return Result.create(notes, problems);
794 : }
795 : }
|