| // Copyright (C) 2016 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.mail.send; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly; | |
| import com.google.auto.factory.AutoFactory; | |
| import com.google.auto.factory.Provided; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Address; | |
| import com.google.gerrit.entities.AttentionSetUpdate; | |
| import com.google.gerrit.entities.BranchNameKey; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.ChangeSizeBucket; | |
| import com.google.gerrit.entities.NotifyConfig.NotifyType; | |
| import com.google.gerrit.entities.Patch; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.PatchSetInfo; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.exceptions.EmailException; | |
| import com.google.gerrit.exceptions.StorageException; | |
| import com.google.gerrit.extensions.api.changes.NotifyHandling; | |
| import com.google.gerrit.extensions.api.changes.RecipientType; | |
| import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.mail.MailHeader; | |
| import com.google.gerrit.server.account.AccountState; | |
| import com.google.gerrit.server.mail.send.ProjectWatch.Watchers; | |
| import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList; | |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; | |
| import com.google.gerrit.server.patch.DiffNotAvailableException; | |
| import com.google.gerrit.server.patch.DiffOptions; | |
| import com.google.gerrit.server.patch.FilePathAdapter; | |
| import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; | |
| import com.google.gerrit.server.patch.filediff.FileDiffOutput; | |
| import com.google.gerrit.server.permissions.ChangePermission; | |
| import com.google.gerrit.server.permissions.GlobalPermission; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.project.ProjectState; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import java.io.IOException; | |
| import java.text.MessageFormat; | |
| import java.time.Instant; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import java.util.TreeMap; | |
| import java.util.TreeSet; | |
| import java.util.stream.Collectors; | |
| import org.apache.james.mime4j.dom.field.FieldName; | |
| import org.eclipse.jgit.diff.DiffFormatter; | |
| import org.eclipse.jgit.internal.JGitText; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.util.RawParseUtils; | |
| import org.eclipse.jgit.util.TemporaryBuffer; | |
| /** Populates an email for change related notifications. */ | |
| @AutoFactory | |
| public class ChangeEmailImpl implements ChangeEmail { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| // Available after construction | |
| protected final EmailArguments args; | |
| protected final Set<Account.Id> currentAttentionSet; | |
| protected final Change change; | |
| protected final ChangeData changeData; | |
| protected final BranchNameKey branch; | |
| protected final ChangeEmailDecorator changeEmailDecorator; | |
| // Available after init or after being explicitly set. | |
| protected OutgoingEmail email; | |
| private List<Account.Id> stars; | |
| protected PatchSet patchSet; | |
| protected PatchSetInfo patchSetInfo; | |
| private String changeMessage; | |
| private String changeMessageThreadId; | |
| private Instant timestamp; | |
| private ProjectState projectState; | |
| private Set<Account.Id> authors; | |
| private boolean emailOnlyAuthors; | |
| private boolean emailOnlyAttentionSetIfEnabled; | |
| // Watchers ignore attention set rules. | |
| private Set<Account.Id> watcherAccounts = new HashSet<>(); | |
| // Watcher can only be an email if it's specified in notify section of ProjectConfig. | |
| private Set<Address> watcherEmails = new HashSet<>(); | |
| private boolean isThreadReply = false; | |
| public ChangeEmailImpl( | |
| @Provided EmailArguments args, | |
| Project.NameKey project, | |
| Change.Id changeId, | |
| ChangeEmailDecorator changeEmailDecorator) { | |
| this.args = args; | |
| this.changeData = args.newChangeData(project, changeId); | |
| change = changeData.change(); | |
| emailOnlyAuthors = false; | |
| emailOnlyAttentionSetIfEnabled = true; | |
| currentAttentionSet = getAttentionSet(); | |
| branch = changeData.change().getDest(); | |
| this.changeEmailDecorator = changeEmailDecorator; | |
| } | |
| @Override | |
| public void markAsReply() { | |
| isThreadReply = true; | |
| } | |
| @Override | |
| public Change getChange() { | |
| return change; | |
| } | |
| @Override | |
| public ChangeData getChangeData() { | |
| return changeData; | |
| } | |
| @Override | |
| @Nullable | |
| public Instant getTimestamp() { | |
| return timestamp; | |
| } | |
| @Override | |
| public void setPatchSet(PatchSet ps) { | |
| patchSet = ps; | |
| } | |
| @Override | |
| @Nullable | |
| public PatchSet getPatchSet() { | |
| return patchSet; | |
| } | |
| @Override | |
| public void setPatchSet(PatchSet ps, PatchSetInfo psi) { | |
| patchSet = ps; | |
| patchSetInfo = psi; | |
| } | |
| @Override | |
| public void setChangeMessage(String cm, Instant t) { | |
| changeMessage = cm; | |
| timestamp = t; | |
| } | |
| @Override | |
| public void setEmailOnlyAttentionSetIfEnabled(boolean value) { | |
| emailOnlyAttentionSetIfEnabled = value; | |
| } | |
| @Override | |
| public boolean shouldSendMessage() { | |
| return changeEmailDecorator.shouldSendMessage(); | |
| } | |
| @Override | |
| public void init(OutgoingEmail email) throws EmailException { | |
| this.email = email; | |
| changeMessageThreadId = | |
| String.format( | |
| "<gerrit.%s.%s@%s>", | |
| change.getCreatedOn().toEpochMilli(), change.getKey().get(), email.getGerritHost()); | |
| if (email.getFrom() != null) { | |
| // Is the from user in an email squelching group? | |
| try { | |
| args.permissionBackend.absentUser(email.getFrom()).check(GlobalPermission.EMAIL_REVIEWERS); | |
| } catch (AuthException | PermissionBackendException e) { | |
| emailOnlyAuthors = true; | |
| } | |
| } | |
| if (args.projectCache != null) { | |
| projectState = args.projectCache.get(change.getProject()).orElse(null); | |
| } else { | |
| projectState = null; | |
| } | |
| if (patchSet == null) { | |
| try { | |
| patchSet = changeData.currentPatchSet(); | |
| } catch (StorageException err) { | |
| patchSet = null; | |
| } | |
| } | |
| if (patchSet != null) { | |
| email.setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.number() + ""); | |
| if (patchSetInfo == null) { | |
| try { | |
| patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.id()); | |
| } catch (PatchSetInfoNotAvailableException | StorageException err) { | |
| patchSetInfo = null; | |
| } | |
| } | |
| } | |
| try { | |
| stars = changeData.stars(); | |
| } catch (StorageException e) { | |
| throw new EmailException("Failed to load stars for change " + change.getChangeId(), e); | |
| } | |
| BranchEmailUtils.setListIdHeader(email, branch); | |
| if (timestamp != null) { | |
| email.setHeader(FieldName.DATE, timestamp); | |
| } | |
| email.setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get()); | |
| email.setHeader(MailHeader.CHANGE_NUMBER.fieldName(), "" + change.getChangeId()); | |
| email.setHeader(MailHeader.PROJECT.fieldName(), "" + change.getProject()); | |
| setChangeUrlHeader(); | |
| setCommitIdHeader(); | |
| changeEmailDecorator.init(email, this); | |
| } | |
| private void setChangeUrlHeader() { | |
| final String u = getChangeUrl(); | |
| if (u != null) { | |
| email.setHeader(MailHeader.CHANGE_URL.fieldName(), "<" + u + ">"); | |
| } | |
| } | |
| private void setCommitIdHeader() { | |
| if (patchSet != null) { | |
| email.setHeader(MailHeader.COMMIT.fieldName(), patchSet.commitId().name()); | |
| } | |
| } | |
| protected void setChangeSubjectHeader() { | |
| email.setHeader(FieldName.SUBJECT, email.textTemplate("ChangeSubject")); | |
| } | |
| @Override | |
| public int getInsertionsCount() { | |
| return listModifiedFiles().entrySet().stream() | |
| .filter(e -> !Patch.COMMIT_MSG.equals(e.getKey())) | |
| .map(Map.Entry::getValue) | |
| .map(FileDiffOutput::insertions) | |
| .reduce(0, Integer::sum); | |
| } | |
| @Override | |
| public int getDeletionsCount() { | |
| return listModifiedFiles().values().stream() | |
| .map(FileDiffOutput::deletions) | |
| .reduce(0, Integer::sum); | |
| } | |
| /** | |
| * Get a link to the change; null if the server doesn't know its own address or if the address is | |
| * malformed. The link will contain a usp parameter set to "email" to inform the frontend on | |
| * clickthroughs where the link came from. | |
| */ | |
| @Nullable | |
| protected String getChangeUrl() { | |
| return args.urlFormatter | |
| .get() | |
| .getChangeViewUrl(change.getProject(), change.getId()) | |
| .map(EmailArguments::addUspParam) | |
| .orElse(null); | |
| } | |
| /** Sets headers for conversation grouping */ | |
| protected void setThreadHeaders() { | |
| if (isThreadReply) { | |
| email.setHeader("In-Reply-To", changeMessageThreadId); | |
| } | |
| email.setHeader("References", changeMessageThreadId); | |
| } | |
| /** Get the text of the "cover letter". */ | |
| @Override | |
| public String getCoverLetter() { | |
| if (changeMessage != null) { | |
| return changeMessage.trim(); | |
| } | |
| return ""; | |
| } | |
| /** Create the change message and the affected file list. */ | |
| protected String getChangeDetail() { | |
| try { | |
| StringBuilder detail = new StringBuilder(); | |
| if (patchSetInfo != null) { | |
| detail.append(patchSetInfo.getMessage().trim()).append("\n"); | |
| } else { | |
| detail.append(change.getSubject().trim()).append("\n"); | |
| } | |
| if (patchSet != null) { | |
| detail.append("---\n"); | |
| // Sort files by name. | |
| TreeMap<String, FileDiffOutput> modifiedFiles = new TreeMap<>(listModifiedFiles()); | |
| for (FileDiffOutput fileDiff : modifiedFiles.values()) { | |
| if (fileDiff.newPath().isPresent() && Patch.isMagic(fileDiff.newPath().get())) { | |
| continue; | |
| } | |
| detail | |
| .append(fileDiff.changeType().getCode()) | |
| .append(" ") | |
| .append( | |
| FilePathAdapter.getNewPath( | |
| fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType())) | |
| .append("\n"); | |
| } | |
| detail.append( | |
| MessageFormat.format( | |
| "" // | |
| + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " // | |
| + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " // | |
| + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" // | |
| + "\n", | |
| modifiedFiles.size() - 1, // -1 to account for the commit message | |
| getInsertionsCount(), | |
| getDeletionsCount())); | |
| detail.append("\n"); | |
| } | |
| return detail.toString(); | |
| } catch (Exception err) { | |
| logger.atWarning().withCause(err).log("Cannot format change detail"); | |
| return ""; | |
| } | |
| } | |
| /** Get the patch list corresponding to patch set patchSetId of this change. */ | |
| @Override | |
| public Map<String, FileDiffOutput> listModifiedFiles(int patchSetId) { | |
| try { | |
| PatchSet ps; | |
| if (patchSetId == patchSet.number()) { | |
| ps = patchSet; | |
| } else { | |
| ps = args.patchSetUtil.get(changeData.notes(), PatchSet.id(change.getId(), patchSetId)); | |
| } | |
| return args.diffOperations.listModifiedFilesAgainstParent( | |
| change.getProject(), ps.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS); | |
| } catch (StorageException | DiffNotAvailableException e) { | |
| logger.atSevere().withCause(e).log("Failed to get modified files"); | |
| return new HashMap<>(); | |
| } | |
| } | |
| /** Get the patch list corresponding to this patch set. */ | |
| @Override | |
| public Map<String, FileDiffOutput> listModifiedFiles() { | |
| if (patchSet != null) { | |
| try { | |
| return args.diffOperations.listModifiedFilesAgainstParent( | |
| change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS); | |
| } catch (DiffNotAvailableException e) { | |
| logger.atSevere().withCause(e).log("Failed to get modified files"); | |
| } | |
| } else { | |
| logger.atSevere().log("no patchSet specified"); | |
| } | |
| return new HashMap<>(); | |
| } | |
| /** Get the project entity the change is in; null if its been deleted. */ | |
| @Override | |
| public ProjectState getProjectState() { | |
| return projectState; | |
| } | |
| /** TO or CC all vested parties (change owner, patch set uploader, author). */ | |
| @Override | |
| public void addAuthors(RecipientType rt) { | |
| for (Account.Id id : getAuthors()) { | |
| email.addByAccountId(rt, id); | |
| } | |
| } | |
| /** BCC any user who has starred this change. */ | |
| @Override | |
| public void bccStarredBy() { | |
| if (!NotifyHandling.ALL.equals(email.getNotify().handling())) { | |
| return; | |
| } | |
| stars.forEach(accountId -> email.addByAccountId(RecipientType.BCC, accountId)); | |
| } | |
| /** Include users and groups that want notification of events. */ | |
| @Override | |
| public void includeWatchers(NotifyType type) { | |
| includeWatchers(type, true); | |
| } | |
| /** Include users and groups that want notification of events. */ | |
| @Override | |
| public void includeWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) { | |
| try { | |
| Watchers matching = getWatchers(type, includeWatchersFromNotifyConfig); | |
| addWatchers(RecipientType.TO, matching.to); | |
| addWatchers(RecipientType.CC, matching.cc); | |
| addWatchers(RecipientType.BCC, matching.bcc); | |
| } catch (StorageException err) { | |
| // Just don't CC everyone. Better to send a partial message to those | |
| // we already have queued up then to fail deliver entirely to people | |
| // who have a lower interest in the change. | |
| logger.atWarning().withCause(err).log("Cannot BCC watchers for %s", type); | |
| } | |
| } | |
| /** Add users or email addresses to the TO, CC, or BCC list. */ | |
| private void addWatchers(RecipientType type, WatcherList watcherList) { | |
| watcherAccounts.addAll(watcherList.accounts); | |
| for (Account.Id user : watcherList.accounts) { | |
| email.addByAccountId(type, user); | |
| } | |
| watcherEmails.addAll(watcherList.emails); | |
| for (Address addr : watcherList.emails) { | |
| email.addByEmail(type, addr); | |
| } | |
| } | |
| private final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) { | |
| if (!NotifyHandling.ALL.equals(email.getNotify().handling())) { | |
| return new Watchers(); | |
| } | |
| ProjectWatch watch = new ProjectWatch(args, branch.project(), projectState, changeData); | |
| return watch.getWatchers(type, includeWatchersFromNotifyConfig); | |
| } | |
| /** Any user who has published comments on this change. */ | |
| @Override | |
| public void ccAllApprovals() { | |
| if (!NotifyHandling.ALL.equals(email.getNotify().handling()) | |
| && !NotifyHandling.OWNER_REVIEWERS.equals(email.getNotify().handling())) { | |
| return; | |
| } | |
| try { | |
| for (Account.Id id : changeData.reviewers().all()) { | |
| email.addByAccountId(RecipientType.CC, id); | |
| } | |
| } catch (StorageException err) { | |
| logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change"); | |
| } | |
| } | |
| /** Users who were added as reviewers to this change. */ | |
| @Override | |
| public void ccExistingReviewers() { | |
| if (!NotifyHandling.ALL.equals(email.getNotify().handling()) | |
| && !NotifyHandling.OWNER_REVIEWERS.equals(email.getNotify().handling())) { | |
| return; | |
| } | |
| try { | |
| for (Account.Id id : changeData.reviewers().byState(ReviewerStateInternal.REVIEWER)) { | |
| email.addByAccountId(RecipientType.CC, id); | |
| } | |
| } catch (StorageException err) { | |
| logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change"); | |
| } | |
| } | |
| @Override | |
| public boolean isRecipientAllowed(Address addr) throws PermissionBackendException { | |
| if (!projectState.statePermitsRead()) { | |
| return false; | |
| } | |
| if (emailOnlyAuthors) { | |
| return false; | |
| } | |
| // If the email is a watcher email, skip permission check. An email can only be a watcher if | |
| // it is specified in notify section of ProjectConfig, so we trust that the recipient is | |
| // allowed. | |
| if (watcherEmails.contains(addr)) { | |
| return true; | |
| } | |
| return args.permissionBackend | |
| .user(args.anonymousUser.get()) | |
| .change(changeData) | |
| .test(ChangePermission.READ); | |
| } | |
| @Override | |
| public boolean isRecipientAllowed(Account.Id to) throws PermissionBackendException { | |
| if (!projectState.statePermitsRead()) { | |
| return false; | |
| } | |
| if (emailOnlyAuthors && !getAuthors().contains(to)) { | |
| return false; | |
| } | |
| // Watchers ignore AttentionSet rules. | |
| if (!watcherAccounts.contains(to)) { | |
| Optional<AccountState> accountState = args.accountCache.get(to); | |
| if (emailOnlyAttentionSetIfEnabled | |
| && accountState.isPresent() | |
| && accountState.get().generalPreferences().getEmailStrategy() | |
| == EmailStrategy.ATTENTION_SET_ONLY | |
| && !currentAttentionSet.contains(to)) { | |
| return false; | |
| } | |
| } | |
| return args.permissionBackend.absentUser(to).change(changeData).test(ChangePermission.READ); | |
| } | |
| /** Lazily finds all users who are authors of any part of this change. */ | |
| private Set<Account.Id> getAuthors() { | |
| if (this.authors != null) { | |
| return this.authors; | |
| } | |
| Set<Account.Id> authors = new HashSet<>(); | |
| switch (email.getNotify().handling()) { | |
| case NONE: | |
| break; | |
| case ALL: | |
| default: | |
| if (patchSet != null) { | |
| authors.add(patchSet.uploader()); | |
| } | |
| if (patchSetInfo != null) { | |
| if (patchSetInfo.getAuthor().getAccount() != null) { | |
| authors.add(patchSetInfo.getAuthor().getAccount()); | |
| } | |
| if (patchSetInfo.getCommitter().getAccount() != null) { | |
| authors.add(patchSetInfo.getCommitter().getAccount()); | |
| } | |
| } | |
| // $FALL-THROUGH$ | |
| case OWNER_REVIEWERS: | |
| case OWNER: | |
| authors.add(change.getOwner()); | |
| break; | |
| } | |
| return this.authors = authors; | |
| } | |
| @Override | |
| public void populateEmailContent() throws EmailException { | |
| BranchEmailUtils.addBranchData(email, args, branch); | |
| setThreadHeaders(); | |
| email.addSoyParam("changeId", change.getKey().get()); | |
| email.addSoyParam("coverLetter", getCoverLetter()); | |
| email.addSoyParam("fromName", email.getNameFor(email.getFrom())); | |
| email.addSoyParam("fromEmail", email.getNameEmailFor(email.getFrom())); | |
| email.addSoyParam("diffLines", ChangeEmail.getDiffTemplateData(getUnifiedDiff())); | |
| email.addSoyEmailDataParam("unifiedDiff", getUnifiedDiff()); | |
| email.addSoyEmailDataParam("changeDetail", getChangeDetail()); | |
| email.addSoyEmailDataParam("changeUrl", getChangeUrl()); | |
| email.addSoyEmailDataParam("includeDiff", getIncludeDiff()); | |
| Map<String, String> changeData = new HashMap<>(); | |
| String subject = change.getSubject(); | |
| String originalSubject = change.getOriginalSubject(); | |
| changeData.put("subject", subject); | |
| changeData.put("originalSubject", originalSubject); | |
| changeData.put("shortSubject", shortenSubject(subject)); | |
| changeData.put("shortOriginalSubject", shortenSubject(originalSubject)); | |
| changeData.put("ownerName", email.getNameFor(change.getOwner())); | |
| changeData.put("ownerEmail", email.getNameEmailFor(change.getOwner())); | |
| changeData.put("changeNumber", Integer.toString(change.getChangeId())); | |
| changeData.put( | |
| "sizeBucket", | |
| ChangeSizeBucket.getChangeSizeBucket(getInsertionsCount() + getDeletionsCount())); | |
| email.addSoyParam("change", changeData); | |
| Map<String, Object> patchSetData = new HashMap<>(); | |
| patchSetData.put("patchSetId", patchSet.number()); | |
| patchSetData.put("refName", patchSet.refName()); | |
| email.addSoyParam("patchSet", patchSetData); | |
| Map<String, Object> patchSetInfoData = new HashMap<>(); | |
| patchSetInfoData.put("authorName", patchSetInfo.getAuthor().getName()); | |
| patchSetInfoData.put("authorEmail", patchSetInfo.getAuthor().getEmail()); | |
| email.addSoyParam("patchSetInfo", patchSetInfoData); | |
| email.addFooter(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get()); | |
| email.addFooter(MailHeader.CHANGE_NUMBER.withDelimiter() + change.getChangeId()); | |
| email.addFooter(MailHeader.PATCH_SET.withDelimiter() + patchSet.number()); | |
| email.addFooter(MailHeader.OWNER.withDelimiter() + email.getNameEmailFor(change.getOwner())); | |
| for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) { | |
| email.addFooter(MailHeader.REVIEWER.withDelimiter() + reviewer); | |
| } | |
| for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) { | |
| email.addFooter(MailHeader.CC.withDelimiter() + reviewer); | |
| } | |
| for (Account.Id attentionUser : currentAttentionSet) { | |
| email.addFooter(MailHeader.ATTENTION.withDelimiter() + email.getNameEmailFor(attentionUser)); | |
| } | |
| // We need names rather than account ids / emails to make it user readable. | |
| email.addSoyParam( | |
| "attentionSet", | |
| currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList())); | |
| setChangeSubjectHeader(); | |
| if (email.getNotify().handling().equals(NotifyHandling.OWNER_REVIEWERS) | |
| || email.getNotify().handling().equals(NotifyHandling.ALL)) { | |
| try { | |
| this.changeData.reviewersByEmail().byState(ReviewerStateInternal.CC).stream() | |
| .forEach(address -> email.addByEmail(RecipientType.CC, address)); | |
| this.changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER).stream() | |
| .forEach(address -> email.addByEmail(RecipientType.CC, address)); | |
| } catch (StorageException e) { | |
| throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e); | |
| } | |
| } | |
| if (email.useHtml()) { | |
| email.appendHtml(email.soyHtmlTemplate("ChangeHeaderHtml")); | |
| } | |
| email.appendText(email.textTemplate("ChangeHeader")); | |
| changeEmailDecorator.populateEmailContent(); | |
| email.appendText(email.textTemplate("ChangeFooter")); | |
| if (email.useHtml()) { | |
| email.appendHtml(email.soyHtmlTemplate("ChangeFooterHtml")); | |
| } | |
| } | |
| /** | |
| * A shortened subject is the subject limited to 72 characters, with an ellipsis if it exceeds | |
| * that limit. | |
| */ | |
| protected static String shortenSubject(String subject) { | |
| if (subject.length() < 73) { | |
| return subject; | |
| } | |
| return subject.substring(0, 69) + "..."; | |
| } | |
| protected Set<String> getEmailsByState(ReviewerStateInternal state) { | |
| Set<String> reviewers = new TreeSet<>(); | |
| try { | |
| for (Account.Id who : changeData.reviewers().byState(state)) { | |
| reviewers.add(email.getNameEmailFor(who)); | |
| } | |
| } catch (StorageException e) { | |
| logger.atWarning().withCause(e).log("Cannot get change reviewers"); | |
| } | |
| return reviewers; | |
| } | |
| private Set<Account.Id> getAttentionSet() { | |
| Set<Account.Id> attentionSet = new TreeSet<>(); | |
| try { | |
| attentionSet = | |
| additionsOnly(changeData.attentionSet()).stream() | |
| .map(AttentionSetUpdate::account) | |
| .collect(Collectors.toSet()); | |
| } catch (StorageException e) { | |
| logger.atWarning().withCause(e).log("Cannot get change attention set"); | |
| } | |
| return attentionSet; | |
| } | |
| protected boolean getIncludeDiff() { | |
| return args.settings.includeDiff; | |
| } | |
| private static final int HEAP_EST_SIZE = 32 * 1024; | |
| /** Show patch set as unified difference. */ | |
| @Override | |
| public String getUnifiedDiff() { | |
| Map<String, FileDiffOutput> modifiedFiles; | |
| modifiedFiles = listModifiedFiles(); | |
| if (modifiedFiles.isEmpty()) { | |
| // Octopus merges are not well supported for diff output by Gerrit. | |
| // Currently these always have a null oldId in the PatchList. | |
| return "[Empty change (potentially Octopus merge); cannot be formatted as a diff.]\n"; | |
| } | |
| int maxSize = args.settings.maximumDiffSize; | |
| TemporaryBuffer.Heap buf = new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxSize), maxSize); | |
| try (DiffFormatter fmt = new DiffFormatter(buf)) { | |
| try (Repository git = args.server.openRepository(change.getProject())) { | |
| try { | |
| ObjectId oldId = modifiedFiles.values().iterator().next().oldCommitId(); | |
| ObjectId newId = modifiedFiles.values().iterator().next().newCommitId(); | |
| if (oldId.equals(ObjectId.zeroId())) { | |
| // DiffOperations returns ObjectId.zeroId if newCommit is a root commit, i.e. has no | |
| // parents. | |
| oldId = null; | |
| } | |
| fmt.setRepository(git); | |
| fmt.setDetectRenames(true); | |
| fmt.format(oldId, newId); | |
| return RawParseUtils.decode(buf.toByteArray()); | |
| } catch (IOException e) { | |
| if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) { | |
| return ""; | |
| } | |
| logger.atSevere().withCause(e).log("Cannot format patch"); | |
| return ""; | |
| } | |
| } catch (IOException e) { | |
| logger.atSevere().withCause(e).log("Cannot open repository to format patch"); | |
| return ""; | |
| } | |
| } | |
| } | |
| } |