private void postAdd(Change change, PostResult result) throws OrmException, EmailException { if (result.reviewers.isEmpty()) { return; } // Execute hook for added reviewers // PatchSet patchSet = db.get().patchSets().get(change.currentPatchSetId()); for (AccountInfo info : result.reviewers) { Account account = accountCache.get(info._id).getAccount(); hooks.doReviewerAddedHook(change, account, patchSet, db.get()); } // Email the reviewers // // The user knows they added themselves, don't bother emailing them. List<Account.Id> added = Lists.newArrayListWithCapacity(result.reviewers.size()); for (AccountInfo info : result.reviewers) { if (!info._id.equals(currentUser.getAccountId())) { added.add(info._id); } } if (!added.isEmpty()) { AddReviewerSender cm; cm = addReviewerSenderFactory.create(change); cm.setFrom(currentUser.getAccountId()); cm.addReviewers(added); cm.send(); } }
@Test public void reviewedBy() throws Exception { clockStepMs = MILLISECONDS.convert(2, MINUTES); TestRepository<Repo> repo = createProject("repo"); Change change1 = newChange(repo, null, null, null, null).insert(); Change change2 = newChange(repo, null, null, null, null).insert(); Change change3 = newChange(repo, null, null, null, null).insert(); gApi.changes().id(change1.getId().get()).current().review(new ReviewInput().message("comment")); Account.Id user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId(); requestContext.setContext(newRequestContext(user2)); gApi.changes().id(change2.getId().get()).current().review(new ReviewInput().message("comment")); PatchSet.Id ps3_1 = change3.currentPatchSetId(); change3 = newPatchSet(repo, change3); assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1); // Response to previous patch set still counts as reviewing. gApi.changes() .id(change3.getId().get()) .revision(ps3_1.get()) .review(new ReviewInput().message("comment")); List<ChangeInfo> actual; actual = assertQuery(newQuery("is:reviewed").withOption(REVIEWED), change3, change2); assertThat(actual.get(0).reviewed).isTrue(); assertThat(actual.get(1).reviewed).isTrue(); actual = assertQuery(newQuery("-is:reviewed").withOption(REVIEWED), change1); assertThat(actual.get(0).reviewed).isNull(); actual = assertQuery("reviewedby:" + userId.get()); actual = assertQuery(newQuery("reviewedby:" + user2.get()).withOption(REVIEWED), change3, change2); assertThat(actual.get(0).reviewed).isTrue(); assertThat(actual.get(1).reviewed).isTrue(); }
private ChangeMessage message(Change c, String body) { String uuid; try { uuid = ChangeUtil.messageUUID(db); } catch (OrmException e) { return null; } ChangeMessage m = new ChangeMessage( new ChangeMessage.Key(c.getId(), uuid), null, TimeUtil.nowTs(), c.currentPatchSetId()); m.setMessage(body); return m; }
protected Change newPatchSet(TestRepository<Repo> repo, Change c) throws Exception { // Add a new file so the patch set is not a trivial rebase, to avoid default // Code-Review label copying. int n = c.currentPatchSetId().get() + 1; RevCommit commit = repo.parseBody(repo.commit().message("message").add("file" + n, "contents " + n).create()); ChangeControl ctl = changeControlFactory.controlFor(c.getId(), user); return patchSetFactory .create(repo.getRepository(), repo.getRevWalk(), ctl, commit) .setSendMail(false) .setRunHooks(false) .setValidatePolicy(ValidatePolicy.NONE) .insert(); }
private void abandonOneChange(Change change) throws OrmException, NoSuchChangeException, IOException { db.changes().beginTransaction(change.getId()); // TODO(dborowitz): support InternalUser in ChangeUpdate ChangeControl control = changeControlFactory.controlFor(change, identifiedUserFactory.create(change.getOwner())); ChangeUpdate update = updateFactory.create(control); try { change = db.changes() .atomicUpdate( change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change change) { if (change.getStatus().isOpen()) { change.setStatus(Change.Status.ABANDONED); return change; } return null; } }); if (change != null) { ChangeMessage msg = new ChangeMessage( new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)), null, change.getLastUpdatedOn(), change.currentPatchSetId()); msg.setMessage("Project was deleted."); // TODO(yyonas): atomic change is not propagated. cmUtil.addChangeMessage(db, update, msg); db.commit(); indexer.index(db, change); } } finally { db.rollback(); } update.commit(); }
private Multimap<ObjectId, PatchSet.Id> getRevisions(OpenRepo or, Collection<ChangeData> cds) throws IntegrationException { try { List<String> refNames = new ArrayList<>(cds.size()); for (ChangeData cd : cds) { Change c = cd.change(); if (c != null) { refNames.add(c.currentPatchSetId().toRefName()); } } Multimap<ObjectId, PatchSet.Id> revisions = HashMultimap.create(cds.size(), 1); for (Map.Entry<String, Ref> e : or.repo .getRefDatabase() .exactRef(refNames.toArray(new String[refNames.size()])) .entrySet()) { revisions.put(e.getValue().getObjectId(), PatchSet.Id.fromRef(e.getKey())); } return revisions; } catch (IOException | OrmException e) { throw new IntegrationException("Failed to validate changes", e); } }
private void updateChangeStatus( List<ChangeData> submitted, Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller) throws NoSuchChangeException, IntegrationException, ResourceConflictException, OrmException { if (!dryRun) { logDebug("Updating change status for {} changes", submitted.size()); } else { logDebug("Checking change state for {} changes in a dry run", submitted.size()); } MergeTip mergeTip = mergeTips.get(destBranch); for (ChangeData cd : submitted) { Change c = cd.change(); CodeReviewCommit commit = commits.get(c.getId()); CommitMergeStatus s = commit != null ? commit.getStatusCode() : null; if (s == null) { // Shouldn't ever happen, but leave the change alone. We'll pick // it up on the next pass. // logDebug( "Submitted change {} did not appear in set of new commits" + " produced by merge strategy", c.getId()); continue; } if (!dryRun) { try { setApproval(cd, caller); } catch (IOException e) { throw new OrmException(e); } } String txt = s.getMessage(); logDebug("Status of change {} ({}) on {}: {}", c.getId(), commit.name(), c.getDest(), s); // If mergeTip is null merge failed and mergeResultRev will not be read. ObjectId mergeResultRev = mergeTip != null ? mergeTip.getMergeResults().get(commit) : null; // The change notes must be forcefully reloaded so that the SUBMIT // approval that we added earlier is visible commit.notes().reload(); try { ChangeMessage msg; switch (s) { case CLEAN_MERGE: if (!dryRun) { setMerged(c, message(c, txt + getByAccountName(commit)), mergeResultRev); } break; case CLEAN_REBASE: case CLEAN_PICK: if (!dryRun) { setMerged( c, message(c, txt + " as " + commit.name() + getByAccountName(commit)), mergeResultRev); } break; case ALREADY_MERGED: if (!dryRun) { setMerged(c, null, mergeResultRev); } break; case PATH_CONFLICT: case REBASE_MERGE_CONFLICT: case MANUAL_RECURSIVE_MERGE: case CANNOT_CHERRY_PICK_ROOT: case NOT_FAST_FORWARD: case INVALID_PROJECT_CONFIGURATION: case INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_PERMITTED: case INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_EDITABLE: case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND: case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT: case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: setNew(commit.notes(), message(c, txt)); throw new ResourceConflictException( "Cannot merge " + commit.name() + "\n" + s.getMessage()); case MISSING_DEPENDENCY: logDebug("Change {} is missing dependency", c.getId()); throw new IntegrationException("Cannot merge " + commit.name() + "\n" + s.getMessage()); case REVISION_GONE: logDebug("Commit not found for change {}", c.getId()); msg = new ChangeMessage( new ChangeMessage.Key(c.getId(), ChangeUtil.messageUUID(db)), null, TimeUtil.nowTs(), c.currentPatchSetId()); msg.setMessage("Failed to read commit for this patch set"); setNew(commit.notes(), msg); throw new IntegrationException(msg.getMessage()); default: msg = message(c, "Unspecified merge failure: " + s.name()); setNew(commit.notes(), msg); throw new IntegrationException(msg.getMessage()); } } catch (OrmException | IOException err) { logWarn("Error updating change status for " + c.getId(), err); } } }
private ListMultimap<SubmitType, ChangeData> validateChangeList(Collection<ChangeData> submitted) throws IntegrationException { logDebug("Validating {} changes", submitted.size()); ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create(); Map<String, Ref> allRefs; try { allRefs = repo.getRefDatabase().getRefs(ALL); } catch (IOException e) { throw new IntegrationException(e.getMessage(), e); } Set<ObjectId> tips = new HashSet<>(); for (Ref r : allRefs.values()) { tips.add(r.getObjectId()); } for (ChangeData cd : submitted) { ChangeControl ctl; Change chg; try { ctl = cd.changeControl(); // Reload change in case index was stale. chg = cd.reloadChange(); } catch (OrmException e) { throw new IntegrationException("Failed to validate changes", e); } Change.Id changeId = cd.getId(); if (chg.getStatus() != Change.Status.NEW) { logDebug("Change {} is not new: {}", changeId, chg.getStatus()); continue; } if (chg.currentPatchSetId() == null) { logError("Missing current patch set on change " + changeId); commits.put(changeId, CodeReviewCommit.noPatchSet(ctl)); continue; } PatchSet ps; Branch.NameKey destBranch = chg.getDest(); try { ps = cd.currentPatchSet(); } catch (OrmException e) { throw new IntegrationException("Cannot query the database", e); } if (ps == null || ps.getRevision() == null || ps.getRevision().get() == null) { logError("Missing patch set or revision on change " + changeId); commits.put(changeId, CodeReviewCommit.noPatchSet(ctl)); continue; } String idstr = ps.getRevision().get(); ObjectId id; try { id = ObjectId.fromString(idstr); } catch (IllegalArgumentException iae) { logError("Invalid revision on patch set " + ps.getId()); commits.put(changeId, CodeReviewCommit.noPatchSet(ctl)); continue; } if (!tips.contains(id)) { // TODO Technically the proper way to do this test is to use a // RevWalk on "$id --not --all" and test for an empty set. But // that is way slower than looking for a ref directly pointing // at the desired tip. We should always have a ref available. // // TODO this is actually an error, the branch is gone but we // want to merge the issue. We can't safely do that if the // tip is not reachable. // logError( "Revision " + idstr + " of patch set " + ps.getId() + " is not contained in any ref"); commits.put(changeId, CodeReviewCommit.revisionGone(ctl)); continue; } CodeReviewCommit commit; try { commit = rw.parseCommit(id); } catch (IOException e) { logError("Invalid commit " + idstr + " on patch set " + ps.getId(), e); commits.put(changeId, CodeReviewCommit.revisionGone(ctl)); continue; } // TODO(dborowitz): Consider putting ChangeData in CodeReviewCommit. commit.setControl(ctl); commit.setPatchsetId(ps.getId()); commits.put(changeId, commit); MergeValidators mergeValidators = mergeValidatorsFactory.create(); try { mergeValidators.validatePreMerge(repo, commit, destProject, destBranch, ps.getId()); } catch (MergeValidationException mve) { logDebug( "Revision {} of patch set {} failed validation: {}", idstr, ps.getId(), mve.getStatus()); commit.setStatusCode(mve.getStatus()); continue; } SubmitType submitType; submitType = getSubmitType(commit.getControl(), ps); if (submitType == null) { logError("No submit type for revision " + idstr + " of patch set " + ps.getId()); commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE); continue; } commit.add(canMergeFlag); toSubmit.put(submitType, cd); } logDebug("Submitting on this run: {}", toSubmit); return toSubmit; }
private void setNew(ChangeNotes notes, final ChangeMessage msg) throws NoSuchChangeException, IOException { Change c = notes.getChange(); Change change = null; ChangeUpdate update = null; try { db.changes().beginTransaction(c.getId()); try { change = db.changes() .atomicUpdate( c.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change c) { if (c.getStatus().isOpen()) { c.setStatus(Change.Status.NEW); ChangeUtil.updated(c); } return c; } }); ChangeControl control = changeControl(change); // TODO(yyonas): atomic change is not propagated. update = updateFactory.create(control, c.getLastUpdatedOn()); if (msg != null) { cmUtil.addChangeMessage(db, update, msg); } db.commit(); } finally { db.rollback(); } } catch (OrmException err) { logWarn("Cannot record merge failure message", err); } if (update != null) { update.commit(); } indexer.index(db, change); PatchSetApproval submitter = null; try { submitter = approvalsUtil.getSubmitter(db, notes, notes.getChange().currentPatchSetId()); } catch (Exception e) { logError("Cannot get submitter for change " + notes.getChangeId(), e); } if (submitter != null) { try { hooks.doMergeFailedHook( c, accountCache.get(submitter.getAccountId()).getAccount(), db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db); } catch (OrmException ex) { logError("Cannot run hook for merge failed " + c.getId(), ex); } } }
private BranchBatch validateChangeList(OpenRepo or, Collection<ChangeData> submitted) throws IntegrationException { logDebug("Validating {} changes", submitted.size()); List<ChangeData> toSubmit = new ArrayList<>(submitted.size()); Multimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted); SubmitType submitType = null; ChangeData choseSubmitTypeFrom = null; for (ChangeData cd : submitted) { Change.Id changeId = cd.getId(); ChangeControl ctl; Change chg; try { ctl = cd.changeControl(); chg = cd.change(); } catch (OrmException e) { logProblem(changeId, e); continue; } if (chg.currentPatchSetId() == null) { String msg = "Missing current patch set on change"; logError(msg + " " + changeId); problems.put(changeId, msg); continue; } PatchSet ps; Branch.NameKey destBranch = chg.getDest(); try { ps = cd.currentPatchSet(); } catch (OrmException e) { logProblem(changeId, e); continue; } if (ps == null || ps.getRevision() == null || ps.getRevision().get() == null) { logProblem(changeId, "Missing patch set or revision on change"); continue; } String idstr = ps.getRevision().get(); ObjectId id; try { id = ObjectId.fromString(idstr); } catch (IllegalArgumentException e) { logProblem(changeId, e); continue; } if (!revisions.containsEntry(id, ps.getId())) { // TODO this is actually an error, the branch is gone but we // want to merge the issue. We can't safely do that if the // tip is not reachable. // logProblem( changeId, "Revision " + idstr + " of patch set " + ps.getPatchSetId() + " does not match " + ps.getId().toRefName() + " for change"); continue; } CodeReviewCommit commit; try { commit = or.rw.parseCommit(id); } catch (IOException e) { logProblem(changeId, e); continue; } // TODO(dborowitz): Consider putting ChangeData in CodeReviewCommit. commit.setControl(ctl); commit.setPatchsetId(ps.getId()); commits.put(changeId, commit); MergeValidators mergeValidators = mergeValidatorsFactory.create(); try { mergeValidators.validatePreMerge(or.repo, commit, or.project, destBranch, ps.getId()); } catch (MergeValidationException mve) { problems.put(changeId, mve.getMessage()); continue; } SubmitType st = getSubmitType(cd); if (st == null) { logProblem(changeId, "No submit type for change"); continue; } if (submitType == null) { submitType = st; choseSubmitTypeFrom = cd; } else if (st != submitType) { problems.put( changeId, String.format( "Change has submit type %s, but previously chose submit type %s " + "from change %s in the same batch", st, submitType, choseSubmitTypeFrom.getId())); continue; } commit.add(or.canMergeFlag); toSubmit.add(cd); } logDebug("Submitting on this run: {}", toSubmit); return new AutoValue_MergeOp_BranchBatch(submitType, toSubmit); }