Line data Source code
1 : // Copyright (C) 2016 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.mail.send;
16 :
17 : import static com.google.common.collect.ImmutableList.toImmutableList;
18 : import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
19 :
20 : import com.google.common.base.Splitter;
21 : import com.google.common.collect.ImmutableList;
22 : import com.google.common.collect.ImmutableMap;
23 : import com.google.common.collect.ListMultimap;
24 : import com.google.common.flogger.FluentLogger;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.entities.Account;
27 : import com.google.gerrit.entities.AttentionSetUpdate;
28 : import com.google.gerrit.entities.Change;
29 : import com.google.gerrit.entities.ChangeSizeBucket;
30 : import com.google.gerrit.entities.NotifyConfig.NotifyType;
31 : import com.google.gerrit.entities.Patch;
32 : import com.google.gerrit.entities.PatchSet;
33 : import com.google.gerrit.entities.PatchSetInfo;
34 : import com.google.gerrit.entities.Project;
35 : import com.google.gerrit.exceptions.EmailException;
36 : import com.google.gerrit.exceptions.StorageException;
37 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
38 : import com.google.gerrit.extensions.api.changes.RecipientType;
39 : import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
40 : import com.google.gerrit.extensions.restapi.AuthException;
41 : import com.google.gerrit.mail.MailHeader;
42 : import com.google.gerrit.server.StarredChangesUtil;
43 : import com.google.gerrit.server.account.AccountState;
44 : import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
45 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
46 : import com.google.gerrit.server.patch.DiffNotAvailableException;
47 : import com.google.gerrit.server.patch.DiffOptions;
48 : import com.google.gerrit.server.patch.FilePathAdapter;
49 : import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
50 : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
51 : import com.google.gerrit.server.permissions.ChangePermission;
52 : import com.google.gerrit.server.permissions.GlobalPermission;
53 : import com.google.gerrit.server.permissions.PermissionBackendException;
54 : import com.google.gerrit.server.project.ProjectState;
55 : import com.google.gerrit.server.query.change.ChangeData;
56 : import java.io.IOException;
57 : import java.net.URI;
58 : import java.net.URISyntaxException;
59 : import java.text.MessageFormat;
60 : import java.time.Instant;
61 : import java.util.Collection;
62 : import java.util.HashMap;
63 : import java.util.HashSet;
64 : import java.util.Map;
65 : import java.util.Optional;
66 : import java.util.Set;
67 : import java.util.TreeMap;
68 : import java.util.TreeSet;
69 : import java.util.stream.Collectors;
70 : import org.apache.http.client.utils.URIBuilder;
71 : import org.apache.james.mime4j.dom.field.FieldName;
72 : import org.eclipse.jgit.diff.DiffFormatter;
73 : import org.eclipse.jgit.internal.JGitText;
74 : import org.eclipse.jgit.lib.ObjectId;
75 : import org.eclipse.jgit.lib.Repository;
76 : import org.eclipse.jgit.util.RawParseUtils;
77 : import org.eclipse.jgit.util.TemporaryBuffer;
78 :
79 : /** Sends an email to one or more interested parties. */
80 : public abstract class ChangeEmail extends NotificationEmail {
81 :
82 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
83 :
84 : protected static ChangeData newChangeData(
85 : EmailArguments ea, Project.NameKey project, Change.Id id) {
86 103 : return ea.changeDataFactory.create(project, id);
87 : }
88 :
89 : protected static ChangeData newChangeData(
90 : EmailArguments ea, Project.NameKey project, Change.Id id, ObjectId metaId) {
91 78 : return ea.changeDataFactory.create(ea.changeNotesFactory.createChecked(project, id, metaId));
92 : }
93 :
94 : private final Set<Account.Id> currentAttentionSet;
95 : protected final Change change;
96 : protected final ChangeData changeData;
97 : protected ListMultimap<Account.Id, String> stars;
98 : protected PatchSet patchSet;
99 : protected PatchSetInfo patchSetInfo;
100 : protected String changeMessage;
101 : protected Instant timestamp;
102 :
103 : protected ProjectState projectState;
104 : protected Set<Account.Id> authors;
105 : protected boolean emailOnlyAuthors;
106 : protected boolean emailOnlyAttentionSetIfEnabled;
107 :
108 : protected ChangeEmail(EmailArguments args, String messageClass, ChangeData changeData) {
109 103 : super(args, messageClass, changeData.change().getDest());
110 103 : this.changeData = changeData;
111 103 : change = changeData.change();
112 103 : emailOnlyAuthors = false;
113 103 : emailOnlyAttentionSetIfEnabled = true;
114 103 : currentAttentionSet = getAttentionSet();
115 103 : }
116 :
117 : @Override
118 : public void setFrom(Account.Id id) {
119 103 : super.setFrom(id);
120 :
121 : // Is the from user in an email squelching group?
122 : try {
123 103 : args.permissionBackend.absentUser(id).check(GlobalPermission.EMAIL_REVIEWERS);
124 0 : } catch (AuthException | PermissionBackendException e) {
125 0 : emailOnlyAuthors = true;
126 103 : }
127 103 : }
128 :
129 : public void setPatchSet(PatchSet ps) {
130 0 : patchSet = ps;
131 0 : }
132 :
133 : public void setPatchSet(PatchSet ps, PatchSetInfo psi) {
134 103 : patchSet = ps;
135 103 : patchSetInfo = psi;
136 103 : }
137 :
138 : public void setChangeMessage(String cm, Instant t) {
139 78 : changeMessage = cm;
140 78 : timestamp = t;
141 78 : }
142 :
143 : /** Format the message body by calling {@link #appendText(String)}. */
144 : @Override
145 : protected void format() throws EmailException {
146 103 : if (useHtml()) {
147 103 : appendHtml(soyHtmlTemplate("ChangeHeaderHtml"));
148 : }
149 103 : appendText(textTemplate("ChangeHeader"));
150 103 : formatChange();
151 103 : appendText(textTemplate("ChangeFooter"));
152 103 : if (useHtml()) {
153 103 : appendHtml(soyHtmlTemplate("ChangeFooterHtml"));
154 : }
155 103 : formatFooter();
156 103 : }
157 :
158 : /** Format the message body by calling {@link #appendText(String)}. */
159 : protected abstract void formatChange() throws EmailException;
160 :
161 : /**
162 : * Format the message footer by calling {@link #appendText(String)}.
163 : *
164 : * @throws EmailException if an error occurred.
165 : */
166 103 : protected void formatFooter() throws EmailException {}
167 :
168 : /** Setup the message headers and envelope (TO, CC, BCC). */
169 : @Override
170 : protected void init() throws EmailException {
171 103 : if (args.projectCache != null) {
172 103 : projectState = args.projectCache.get(change.getProject()).orElse(null);
173 : } else {
174 0 : projectState = null;
175 : }
176 :
177 103 : if (patchSet == null) {
178 : try {
179 64 : patchSet = changeData.currentPatchSet();
180 0 : } catch (StorageException err) {
181 0 : patchSet = null;
182 64 : }
183 : }
184 :
185 103 : if (patchSet != null) {
186 103 : setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.number() + "");
187 103 : if (patchSetInfo == null) {
188 : try {
189 64 : patchSetInfo = args.patchSetInfoFactory.get(changeData.notes(), patchSet.id());
190 0 : } catch (PatchSetInfoNotAvailableException | StorageException err) {
191 0 : patchSetInfo = null;
192 64 : }
193 : }
194 : }
195 103 : authors = getAuthors();
196 :
197 : try {
198 103 : stars = changeData.stars();
199 0 : } catch (StorageException e) {
200 0 : throw new EmailException("Failed to load stars for change " + change.getChangeId(), e);
201 103 : }
202 :
203 103 : super.init();
204 103 : if (timestamp != null) {
205 78 : setHeader(FieldName.DATE, timestamp);
206 : }
207 103 : setChangeSubjectHeader();
208 103 : setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get());
209 103 : setHeader(MailHeader.CHANGE_NUMBER.fieldName(), "" + change.getChangeId());
210 103 : setHeader(MailHeader.PROJECT.fieldName(), "" + change.getProject());
211 103 : setChangeUrlHeader();
212 103 : setCommitIdHeader();
213 :
214 103 : if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
215 : try {
216 103 : addByEmail(
217 103 : RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC));
218 103 : addByEmail(
219 : RecipientType.CC,
220 103 : changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
221 0 : } catch (StorageException e) {
222 0 : throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
223 103 : }
224 : }
225 103 : }
226 :
227 : private void setChangeUrlHeader() {
228 103 : final String u = getChangeUrl();
229 103 : if (u != null) {
230 103 : setHeader(MailHeader.CHANGE_URL.fieldName(), "<" + u + ">");
231 : }
232 103 : }
233 :
234 : private void setCommitIdHeader() {
235 103 : if (patchSet != null) {
236 103 : setHeader(MailHeader.COMMIT.fieldName(), patchSet.commitId().name());
237 : }
238 103 : }
239 :
240 : private void setChangeSubjectHeader() {
241 103 : setHeader(FieldName.SUBJECT, textTemplate("ChangeSubject"));
242 103 : }
243 :
244 : private int getInsertionsCount() {
245 103 : return listModifiedFiles().values().stream()
246 103 : .map(FileDiffOutput::insertions)
247 103 : .reduce(0, Integer::sum);
248 : }
249 :
250 : private int getDeletionsCount() {
251 103 : return listModifiedFiles().values().stream()
252 103 : .map(FileDiffOutput::deletions)
253 103 : .reduce(0, Integer::sum);
254 : }
255 :
256 : /**
257 : * Get a link to the change; null if the server doesn't know its own address or if the address is
258 : * malformed. The link will contain a usp parameter set to "email" to inform the frontend on
259 : * clickthroughs where the link came from.
260 : */
261 : @Nullable
262 : public String getChangeUrl() {
263 103 : Optional<String> changeUrl =
264 103 : args.urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId());
265 103 : if (!changeUrl.isPresent()) return null;
266 : try {
267 103 : URI uri = new URIBuilder(changeUrl.get()).addParameter("usp", "email").build();
268 103 : return uri.toString();
269 0 : } catch (URISyntaxException e) {
270 0 : return null;
271 : }
272 : }
273 :
274 : public String getChangeMessageThreadId() {
275 103 : return "<gerrit."
276 103 : + change.getCreatedOn().toEpochMilli()
277 : + "."
278 103 : + change.getKey().get()
279 : + "@"
280 103 : + getGerritHost()
281 : + ">";
282 : }
283 :
284 : /** Get the text of the "cover letter". */
285 : public String getCoverLetter() {
286 103 : if (changeMessage != null) {
287 78 : return changeMessage.trim();
288 : }
289 103 : return "";
290 : }
291 :
292 : /** Create the change message and the affected file list. */
293 : public String getChangeDetail() {
294 : try {
295 103 : StringBuilder detail = new StringBuilder();
296 :
297 103 : if (patchSetInfo != null) {
298 103 : detail.append(patchSetInfo.getMessage().trim()).append("\n");
299 : } else {
300 0 : detail.append(change.getSubject().trim()).append("\n");
301 : }
302 :
303 103 : if (patchSet != null) {
304 103 : detail.append("---\n");
305 : // Sort files by name.
306 103 : TreeMap<String, FileDiffOutput> modifiedFiles = new TreeMap<>(listModifiedFiles());
307 103 : for (FileDiffOutput fileDiff : modifiedFiles.values()) {
308 103 : if (fileDiff.newPath().isPresent() && Patch.isMagic(fileDiff.newPath().get())) {
309 103 : continue;
310 : }
311 90 : detail
312 90 : .append(fileDiff.changeType().getCode())
313 90 : .append(" ")
314 90 : .append(
315 90 : FilePathAdapter.getNewPath(
316 90 : fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType()))
317 90 : .append("\n");
318 90 : }
319 103 : detail.append(
320 103 : MessageFormat.format(
321 : "" //
322 : + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
323 : + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
324 : + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
325 : + "\n",
326 103 : modifiedFiles.size() - 1, //
327 103 : getInsertionsCount(), //
328 103 : getDeletionsCount()));
329 103 : detail.append("\n");
330 : }
331 103 : return detail.toString();
332 0 : } catch (Exception err) {
333 0 : logger.atWarning().withCause(err).log("Cannot format change detail");
334 0 : return "";
335 : }
336 : }
337 :
338 : /** Get the patch list corresponding to patch set patchSetId of this change. */
339 : protected Map<String, FileDiffOutput> listModifiedFiles(int patchSetId) {
340 : try {
341 : PatchSet ps;
342 23 : if (patchSetId == patchSet.number()) {
343 20 : ps = patchSet;
344 : } else {
345 5 : ps = args.patchSetUtil.get(changeData.notes(), PatchSet.id(change.getId(), patchSetId));
346 : }
347 23 : return args.diffOperations.listModifiedFilesAgainstParent(
348 23 : change.getProject(), ps.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
349 0 : } catch (StorageException | DiffNotAvailableException e) {
350 0 : logger.atSevere().withCause(e).log("Failed to get modified files");
351 0 : return new HashMap<>();
352 : }
353 : }
354 :
355 : /** Get the patch list corresponding to this patch set. */
356 : protected Map<String, FileDiffOutput> listModifiedFiles() {
357 103 : if (patchSet != null) {
358 : try {
359 103 : return args.diffOperations.listModifiedFilesAgainstParent(
360 103 : change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
361 0 : } catch (DiffNotAvailableException e) {
362 0 : logger.atSevere().withCause(e).log("Failed to get modified files");
363 0 : }
364 : } else {
365 0 : logger.atSevere().log("no patchSet specified");
366 : }
367 0 : return new HashMap<>();
368 : }
369 :
370 : /** Get the project entity the change is in; null if its been deleted. */
371 : protected ProjectState getProjectState() {
372 0 : return projectState;
373 : }
374 :
375 : /** TO or CC all vested parties (change owner, patch set uploader, author). */
376 : protected void rcptToAuthors(RecipientType rt) {
377 103 : for (Account.Id id : authors) {
378 103 : add(rt, id);
379 103 : }
380 103 : }
381 :
382 : /** BCC any user who has starred this change. */
383 : protected void bccStarredBy() {
384 84 : if (!NotifyHandling.ALL.equals(notify.handling())) {
385 20 : return;
386 : }
387 :
388 84 : for (Map.Entry<Account.Id, Collection<String>> e : stars.asMap().entrySet()) {
389 1 : if (e.getValue().contains(StarredChangesUtil.DEFAULT_LABEL)) {
390 1 : super.add(RecipientType.BCC, e.getKey());
391 : }
392 1 : }
393 84 : }
394 :
395 : @Override
396 : protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
397 103 : if (!NotifyHandling.ALL.equals(notify.handling())) {
398 16 : return new Watchers();
399 : }
400 :
401 103 : ProjectWatch watch = new ProjectWatch(args, branch.project(), projectState, changeData);
402 103 : return watch.getWatchers(type, includeWatchersFromNotifyConfig);
403 : }
404 :
405 : /** Any user who has published comments on this change. */
406 : protected void ccAllApprovals() {
407 74 : if (!NotifyHandling.ALL.equals(notify.handling())
408 18 : && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
409 16 : return;
410 : }
411 :
412 : try {
413 74 : for (Account.Id id : changeData.reviewers().all()) {
414 70 : add(RecipientType.CC, id);
415 70 : }
416 0 : } catch (StorageException err) {
417 0 : logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change");
418 74 : }
419 74 : }
420 :
421 : /** Users who were added as reviewers to this change. */
422 : protected void ccExistingReviewers() {
423 38 : if (!NotifyHandling.ALL.equals(notify.handling())
424 14 : && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
425 14 : return;
426 : }
427 :
428 : try {
429 32 : for (Account.Id id : changeData.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
430 31 : add(RecipientType.CC, id);
431 31 : }
432 0 : } catch (StorageException err) {
433 0 : logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change");
434 32 : }
435 32 : }
436 :
437 : @Override
438 : protected void add(RecipientType rt, Account.Id to) {
439 103 : addRecipient(rt, to, /* isWatcher= */ false);
440 103 : }
441 :
442 : /** This bypasses the EmailStrategy.ATTENTION_SET_ONLY strategy when adding the recipient. */
443 : @Override
444 : protected void addWatcher(RecipientType rt, Account.Id to) {
445 8 : addRecipient(rt, to, /* isWatcher= */ true);
446 8 : }
447 :
448 : private void addRecipient(RecipientType rt, Account.Id to, boolean isWatcher) {
449 103 : if (!isWatcher) {
450 103 : Optional<AccountState> accountState = args.accountCache.get(to);
451 103 : if (emailOnlyAttentionSetIfEnabled
452 103 : && accountState.isPresent()
453 103 : && accountState.get().generalPreferences().getEmailStrategy()
454 : == EmailStrategy.ATTENTION_SET_ONLY
455 1 : && !currentAttentionSet.contains(to)) {
456 1 : return;
457 : }
458 : }
459 103 : if (emailOnlyAuthors && !authors.contains(to)) {
460 0 : return;
461 : }
462 103 : super.add(rt, to);
463 103 : }
464 :
465 : @Override
466 : protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
467 103 : if (!projectState.statePermitsRead()) {
468 0 : return false;
469 : }
470 103 : return args.permissionBackend.absentUser(to).change(changeData).test(ChangePermission.READ);
471 : }
472 :
473 : /** Find all users who are authors of any part of this change. */
474 : protected Set<Account.Id> getAuthors() {
475 103 : Set<Account.Id> authors = new HashSet<>();
476 :
477 103 : switch (notify.handling()) {
478 : case NONE:
479 8 : break;
480 : case ALL:
481 : default:
482 103 : if (patchSet != null) {
483 103 : authors.add(patchSet.uploader());
484 : }
485 103 : if (patchSetInfo != null) {
486 103 : if (patchSetInfo.getAuthor().getAccount() != null) {
487 100 : authors.add(patchSetInfo.getAuthor().getAccount());
488 : }
489 103 : if (patchSetInfo.getCommitter().getAccount() != null) {
490 101 : authors.add(patchSetInfo.getCommitter().getAccount());
491 : }
492 : }
493 : // $FALL-THROUGH$
494 : case OWNER_REVIEWERS:
495 : case OWNER:
496 103 : authors.add(change.getOwner());
497 : break;
498 : }
499 :
500 103 : return authors;
501 : }
502 :
503 : @Override
504 : protected void setupSoyContext() {
505 103 : super.setupSoyContext();
506 :
507 103 : soyContext.put("changeId", change.getKey().get());
508 103 : soyContext.put("coverLetter", getCoverLetter());
509 103 : soyContext.put("fromName", getNameFor(fromId));
510 103 : soyContext.put("fromEmail", getNameEmailFor(fromId));
511 103 : soyContext.put("diffLines", getDiffTemplateData(getUnifiedDiff()));
512 :
513 103 : soyContextEmailData.put("unifiedDiff", getUnifiedDiff());
514 103 : soyContextEmailData.put("changeDetail", getChangeDetail());
515 103 : soyContextEmailData.put("changeUrl", getChangeUrl());
516 103 : soyContextEmailData.put("includeDiff", getIncludeDiff());
517 :
518 103 : Map<String, String> changeData = new HashMap<>();
519 :
520 103 : String subject = change.getSubject();
521 103 : String originalSubject = change.getOriginalSubject();
522 103 : changeData.put("subject", subject);
523 103 : changeData.put("originalSubject", originalSubject);
524 103 : changeData.put("shortSubject", shortenSubject(subject));
525 103 : changeData.put("shortOriginalSubject", shortenSubject(originalSubject));
526 :
527 103 : changeData.put("ownerName", getNameFor(change.getOwner()));
528 103 : changeData.put("ownerEmail", getNameEmailFor(change.getOwner()));
529 103 : changeData.put("changeNumber", Integer.toString(change.getChangeId()));
530 103 : changeData.put(
531 : "sizeBucket",
532 103 : ChangeSizeBucket.getChangeSizeBucket(getInsertionsCount() + getDeletionsCount()));
533 103 : soyContext.put("change", changeData);
534 :
535 103 : Map<String, Object> patchSetData = new HashMap<>();
536 103 : patchSetData.put("patchSetId", patchSet.number());
537 103 : patchSetData.put("refName", patchSet.refName());
538 103 : soyContext.put("patchSet", patchSetData);
539 :
540 103 : Map<String, Object> patchSetInfoData = new HashMap<>();
541 103 : patchSetInfoData.put("authorName", patchSetInfo.getAuthor().getName());
542 103 : patchSetInfoData.put("authorEmail", patchSetInfo.getAuthor().getEmail());
543 103 : soyContext.put("patchSetInfo", patchSetInfoData);
544 :
545 103 : footers.add(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
546 103 : footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + change.getChangeId());
547 103 : footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.number());
548 103 : footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
549 103 : if (change.getAssignee() != null) {
550 6 : footers.add(MailHeader.ASSIGNEE.withDelimiter() + getNameEmailFor(change.getAssignee()));
551 : }
552 103 : for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) {
553 73 : footers.add(MailHeader.REVIEWER.withDelimiter() + reviewer);
554 73 : }
555 103 : for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) {
556 27 : footers.add(MailHeader.CC.withDelimiter() + reviewer);
557 27 : }
558 103 : for (Account.Id attentionUser : currentAttentionSet) {
559 51 : footers.add(MailHeader.ATTENTION.withDelimiter() + getNameEmailFor(attentionUser));
560 51 : }
561 : // Since this would be user visible, only show it if attention set is enabled
562 103 : if (args.settings.isAttentionSetEnabled && !currentAttentionSet.isEmpty()) {
563 : // We need names rather than account ids / emails to make it user readable.
564 51 : soyContext.put(
565 : "attentionSet",
566 51 : currentAttentionSet.stream().map(this::getNameFor).sorted().collect(toImmutableList()));
567 : }
568 103 : }
569 :
570 : /**
571 : * A shortened subject is the subject limited to 72 characters, with an ellipsis if it exceeds
572 : * that limit.
573 : */
574 : private static String shortenSubject(String subject) {
575 103 : if (subject.length() < 73) {
576 103 : return subject;
577 : }
578 5 : return subject.substring(0, 69) + "...";
579 : }
580 :
581 : private Set<String> getEmailsByState(ReviewerStateInternal state) {
582 103 : Set<String> reviewers = new TreeSet<>();
583 : try {
584 103 : for (Account.Id who : changeData.reviewers().byState(state)) {
585 74 : reviewers.add(getNameEmailFor(who));
586 74 : }
587 0 : } catch (StorageException e) {
588 0 : logger.atWarning().withCause(e).log("Cannot get change reviewers");
589 103 : }
590 103 : return reviewers;
591 : }
592 :
593 : private Set<Account.Id> getAttentionSet() {
594 103 : Set<Account.Id> attentionSet = new TreeSet<>();
595 : try {
596 103 : attentionSet =
597 103 : additionsOnly(changeData.attentionSet()).stream()
598 103 : .map(AttentionSetUpdate::account)
599 103 : .collect(Collectors.toSet());
600 0 : } catch (StorageException e) {
601 0 : logger.atWarning().withCause(e).log("Cannot get change attention set");
602 103 : }
603 103 : return attentionSet;
604 : }
605 :
606 : public boolean getIncludeDiff() {
607 103 : return args.settings.includeDiff;
608 : }
609 :
610 : private static final int HEAP_EST_SIZE = 32 * 1024;
611 :
612 : /** Show patch set as unified difference. */
613 : public String getUnifiedDiff() {
614 : Map<String, FileDiffOutput> modifiedFiles;
615 103 : modifiedFiles = listModifiedFiles();
616 103 : if (modifiedFiles.isEmpty()) {
617 : // Octopus merges are not well supported for diff output by Gerrit.
618 : // Currently these always have a null oldId in the PatchList.
619 0 : return "[Empty change (potentially Octopus merge); cannot be formatted as a diff.]\n";
620 : }
621 :
622 103 : int maxSize = args.settings.maximumDiffSize;
623 103 : TemporaryBuffer.Heap buf = new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxSize), maxSize);
624 103 : try (DiffFormatter fmt = new DiffFormatter(buf)) {
625 103 : try (Repository git = args.server.openRepository(change.getProject())) {
626 : try {
627 103 : ObjectId oldId = modifiedFiles.values().iterator().next().oldCommitId();
628 103 : ObjectId newId = modifiedFiles.values().iterator().next().newCommitId();
629 103 : if (oldId.equals(ObjectId.zeroId())) {
630 : // DiffOperations returns ObjectId.zeroId if newCommit is a root commit, i.e. has no
631 : // parents.
632 30 : oldId = null;
633 : }
634 103 : fmt.setRepository(git);
635 103 : fmt.setDetectRenames(true);
636 103 : fmt.format(oldId, newId);
637 103 : return RawParseUtils.decode(buf.toByteArray());
638 1 : } catch (IOException e) {
639 1 : if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
640 1 : return "";
641 : }
642 0 : logger.atSevere().withCause(e).log("Cannot format patch");
643 0 : return "";
644 : }
645 103 : } catch (IOException e) {
646 0 : logger.atSevere().withCause(e).log("Cannot open repository to format patch");
647 0 : return "";
648 : }
649 103 : }
650 : }
651 :
652 : /**
653 : * Generate a list of maps representing each line of the unified diff. The line maps will have a
654 : * 'type' key which maps to one of 'common', 'add' or 'remove' and a 'text' key which maps to the
655 : * line's content.
656 : *
657 : * @param sourceDiff the unified diff that we're converting to the map.
658 : * @return map of 'type' to a line's content.
659 : */
660 : protected static ImmutableList<ImmutableMap<String, String>> getDiffTemplateData(
661 : String sourceDiff) {
662 103 : ImmutableList.Builder<ImmutableMap<String, String>> result = ImmutableList.builder();
663 103 : Splitter lineSplitter = Splitter.on(System.getProperty("line.separator"));
664 103 : for (String diffLine : lineSplitter.split(sourceDiff)) {
665 103 : ImmutableMap.Builder<String, String> lineData = ImmutableMap.builder();
666 103 : lineData.put("text", diffLine);
667 :
668 : // Skip empty lines and lines that look like diff headers.
669 103 : if (diffLine.isEmpty() || diffLine.startsWith("---") || diffLine.startsWith("+++")) {
670 103 : lineData.put("type", "common");
671 : } else {
672 90 : switch (diffLine.charAt(0)) {
673 : case '+':
674 90 : lineData.put("type", "add");
675 90 : break;
676 : case '-':
677 50 : lineData.put("type", "remove");
678 50 : break;
679 : default:
680 90 : lineData.put("type", "common");
681 : break;
682 : }
683 : }
684 103 : result.add(lineData.build());
685 103 : }
686 103 : return result.build();
687 : }
688 : }
|