blob: 2827f5976f8cc8b3b629aefd37e4e45dcb983d8d [file] [log] [blame]
// Copyright (C) 2010 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.events;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.UserIdentity;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountAttributeLoader;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.data.AccountAttribute;
import com.google.gerrit.server.data.ApprovalAttribute;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.DependencyAttribute;
import com.google.gerrit.server.data.MessageAttribute;
import com.google.gerrit.server.data.PatchAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.PatchSetCommentAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
import com.google.gerrit.server.data.SubmitLabelAttribute;
import com.google.gerrit.server.data.SubmitRecordAttribute;
import com.google.gerrit.server.data.SubmitRequirementAttribute;
import com.google.gerrit.server.data.TrackingIdAttribute;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
import com.google.gerrit.server.patch.FilePathAdapter;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
public class EventFactory {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AccountCache accountCache;
private final DynamicItem<UrlFormatter> urlFormatter;
private final DiffOperations diffOperations;
private final Emails emails;
private final Provider<PersonIdent> myIdent;
private final ChangeData.Factory changeDataFactory;
private final ApprovalsUtil approvalsUtil;
private final ChangeKindCache changeKindCache;
private final Provider<InternalChangeQuery> queryProvider;
private final IndexConfig indexConfig;
private final AccountTemplateUtil accountTemplateUtil;
@Inject
EventFactory(
AccountCache accountCache,
Emails emails,
DynamicItem<UrlFormatter> urlFormatter,
DiffOperations diffOperations,
@GerritPersonIdent Provider<PersonIdent> myIdent,
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
ChangeKindCache changeKindCache,
Provider<InternalChangeQuery> queryProvider,
IndexConfig indexConfig,
AccountTemplateUtil accountTemplateUtil) {
this.accountCache = accountCache;
this.urlFormatter = urlFormatter;
this.emails = emails;
this.diffOperations = diffOperations;
this.myIdent = myIdent;
this.changeDataFactory = changeDataFactory;
this.approvalsUtil = approvalsUtil;
this.changeKindCache = changeKindCache;
this.queryProvider = queryProvider;
this.indexConfig = indexConfig;
this.accountTemplateUtil = accountTemplateUtil;
}
public ChangeAttribute asChangeAttribute(Change change, AccountAttributeLoader accountLoader) {
ChangeAttribute a = new ChangeAttribute();
a.project = change.getProject().get();
a.branch = change.getDest().shortName();
a.topic = change.getTopic();
a.id = change.getKey().get();
a.number = change.getId().get();
a.subject = change.getSubject();
a.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner(), accountLoader);
a.status = change.getStatus();
a.createdOn = change.getCreatedOn().getEpochSecond();
a.wip = change.isWorkInProgress() ? true : null;
a.isPrivate = change.isPrivate() ? true : null;
a.cherryPickOfChange =
change.getCherryPickOf() != null ? change.getCherryPickOf().changeId().get() : null;
a.cherryPickOfPatchSet =
change.getCherryPickOf() != null ? change.getCherryPickOf().get() : null;
return a;
}
/** Create a {@link ChangeAttribute} instance from the specified change. */
public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
ChangeAttribute a = asChangeAttribute(change, (AccountAttributeLoader) null);
addHashTags(a, notes);
addCommitMessage(a, notes);
return a;
}
/**
* Create a {@link RefUpdateAttribute} for the given old ObjectId, new ObjectId, and branch that
* is suitable for serialization to JSON.
*/
public RefUpdateAttribute asRefUpdateAttribute(
ObjectId oldId, ObjectId newId, BranchNameKey refName) {
RefUpdateAttribute ru = new RefUpdateAttribute();
ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
ru.project = refName.project().get();
ru.refName = refName.branch();
return ru;
}
/** Extend the existing {@link ChangeAttribute} with additional fields. */
public void extend(ChangeAttribute a, Change change) {
a.lastUpdated = change.getLastUpdatedOn().getEpochSecond();
a.open = change.isNew();
}
/** Add allReviewers to an existing {@link ChangeAttribute}. */
public void addAllReviewers(
ChangeAttribute a, ChangeNotes notes, AccountAttributeLoader accountLoader) {
ImmutableSet<Account.Id> reviewers = approvalsUtil.getReviewers(notes).all();
if (!reviewers.isEmpty()) {
a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
for (Account.Id id : reviewers) {
a.allReviewers.add(asAccountAttribute(id, accountLoader));
}
}
}
/** Add submitRecords to an existing {@link ChangeAttribute}. */
public void addSubmitRecords(
ChangeAttribute ca, List<SubmitRecord> submitRecords, AccountAttributeLoader accountLoader) {
ca.submitRecords = new ArrayList<>();
for (SubmitRecord submitRecord : submitRecords) {
SubmitRecordAttribute sa = new SubmitRecordAttribute();
sa.status = submitRecord.status.name();
if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
addSubmitRecordLabels(submitRecord, sa, accountLoader);
addSubmitRecordRequirements(submitRecord, sa);
}
ca.submitRecords.add(sa);
}
// Remove empty lists so a confusing label won't be displayed in the output.
if (ca.submitRecords.isEmpty()) {
ca.submitRecords = null;
}
}
private void addSubmitRecordLabels(
SubmitRecord submitRecord, SubmitRecordAttribute sa, AccountAttributeLoader accountLoader) {
if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) {
sa.labels = new ArrayList<>();
for (SubmitRecord.Label lbl : submitRecord.labels) {
SubmitLabelAttribute la = new SubmitLabelAttribute();
la.label = lbl.label;
la.status = lbl.status.name();
if (lbl.appliedBy != null) {
la.by = asAccountAttribute(lbl.appliedBy, accountLoader);
}
sa.labels.add(la);
}
}
}
private void addSubmitRecordRequirements(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
if (submitRecord.requirements != null && !submitRecord.requirements.isEmpty()) {
sa.requirements = new ArrayList<>();
for (LegacySubmitRequirement req : submitRecord.requirements) {
SubmitRequirementAttribute re = new SubmitRequirementAttribute();
re.fallbackText = req.fallbackText();
re.type = req.type();
sa.requirements.add(re);
}
}
}
public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs) {
if (change == null || currentPs == null) {
return;
}
ca.dependsOn = new ArrayList<>();
ca.neededBy = new ArrayList<>();
try {
addDependsOn(rw, ca, change, currentPs);
addNeededBy(rw, ca, change, currentPs);
} catch (StorageException | IOException e) {
// Squash DB exceptions and leave dependency lists partially filled.
}
// Remove empty lists so a confusing label won't be displayed in the output.
if (ca.dependsOn.isEmpty()) {
ca.dependsOn = null;
}
if (ca.neededBy.isEmpty()) {
ca.neededBy = null;
}
}
private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
throws IOException {
RevCommit commit = rw.parseCommit(currentPs.commitId());
final List<String> parentNames = new ArrayList<>(commit.getParentCount());
for (RevCommit p : commit.getParents()) {
parentNames.add(p.name());
}
// Find changes in this project having a patch set matching any parent of
// this patch set's revision.
for (ChangeData cd : queryProvider.get().byProjectCommits(change.getProject(), parentNames)) {
for (PatchSet ps : cd.patchSets()) {
for (String p : parentNames) {
if (!ps.commitId().name().equals(p)) {
continue;
}
ca.dependsOn.add(newDependsOn(requireNonNull(cd.change()), ps));
}
}
}
// Sort by original parent order.
ca.dependsOn.sort(
comparing(
d -> {
for (int i = 0; i < parentNames.size(); i++) {
if (parentNames.get(i).equals(d.revision)) {
return i;
}
}
return parentNames.size() + 1;
}));
}
private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
throws IOException {
if (currentPs.groups().isEmpty()) {
return;
}
String rev = currentPs.commitId().name();
// Find changes in the same related group as this patch set, having a patch
// set whose parent matches this patch set's revision.
for (ChangeData cd :
InternalChangeQuery.byProjectGroups(
queryProvider, indexConfig, change.getProject(), currentPs.groups())) {
PATCH_SETS:
for (PatchSet ps : cd.patchSets()) {
RevCommit commit = rw.parseCommit(ps.commitId());
for (RevCommit p : commit.getParents()) {
if (!p.name().equals(rev)) {
continue;
}
ca.neededBy.add(newNeededBy(requireNonNull(cd.change()), ps));
continue PATCH_SETS;
}
}
}
}
private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
DependencyAttribute d = newDependencyAttribute(c, ps);
d.isCurrentPatchSet = ps.id().equals(c.currentPatchSetId());
return d;
}
private DependencyAttribute newNeededBy(Change c, PatchSet ps) {
return newDependencyAttribute(c, ps);
}
private DependencyAttribute newDependencyAttribute(Change c, PatchSet ps) {
DependencyAttribute d = new DependencyAttribute();
d.number = c.getId().get();
d.id = c.getKey().toString();
d.revision = ps.commitId().name();
d.ref = ps.refName();
return d;
}
public void addTrackingIds(ChangeAttribute a, ListMultimap<String, String> set) {
if (!set.isEmpty()) {
a.trackingIds = new ArrayList<>(set.size());
for (Map.Entry<String, Collection<String>> e : set.asMap().entrySet()) {
for (String id : e.getValue()) {
TrackingIdAttribute t = new TrackingIdAttribute();
t.system = e.getKey();
t.id = id;
a.trackingIds.add(t);
}
}
}
}
public void addCommitMessage(ChangeAttribute a, String commitMessage) {
a.commitMessage = commitMessage;
}
private void addCommitMessage(ChangeAttribute changeAttribute, ChangeNotes notes) {
try {
addCommitMessage(changeAttribute, changeDataFactory.create(notes).commitMessage());
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Error while getting full commit message for change %d", changeAttribute.number);
}
}
public void addPatchSets(
RevWalk revWalk,
ChangeAttribute ca,
Collection<PatchSet> ps,
Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
LabelTypes labelTypes,
AccountAttributeLoader accountLoader) {
addPatchSets(revWalk, ca, ps, approvals, false, null, labelTypes, accountLoader);
}
public void addPatchSets(
RevWalk revWalk,
ChangeAttribute ca,
Collection<PatchSet> ps,
Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
boolean includeFiles,
Change change,
LabelTypes labelTypes,
AccountAttributeLoader accountLoader) {
if (!ps.isEmpty()) {
ca.patchSets = new ArrayList<>(ps.size());
for (PatchSet p : ps) {
PatchSetAttribute psa = asPatchSetAttribute(revWalk, change, p, accountLoader);
if (approvals != null) {
addApprovals(psa, p.id(), approvals, labelTypes, accountLoader);
}
ca.patchSets.add(psa);
if (includeFiles) {
addPatchSetFileNames(psa, change, p);
}
}
}
}
public void addPatchSetComments(
PatchSetAttribute patchSetAttribute,
Collection<HumanComment> comments,
AccountAttributeLoader accountLoader) {
for (HumanComment comment : comments) {
if (comment.key.patchSetId == patchSetAttribute.number) {
if (patchSetAttribute.comments == null) {
patchSetAttribute.comments = new ArrayList<>();
}
patchSetAttribute.comments.add(asPatchSetLineAttribute(comment, accountLoader));
}
}
}
public void addPatchSetFileNames(
PatchSetAttribute patchSetAttribute, Change change, PatchSet patchSet) {
try {
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
for (FileDiffOutput diff : modifiedFiles.values()) {
if (patchSetAttribute.files == null) {
patchSetAttribute.files = new ArrayList<>();
}
PatchAttribute p = new PatchAttribute();
p.file = FilePathAdapter.getNewPath(diff.oldPath(), diff.newPath(), diff.changeType());
p.fileOld = FilePathAdapter.getOldPath(diff.oldPath(), diff.changeType());
p.type = diff.changeType();
p.deletions -= diff.deletions();
p.insertions = diff.insertions();
patchSetAttribute.files.add(p);
}
} catch (DiffNotAvailableException e) {
logger.atSevere().withCause(e).log("Cannot get patch list");
}
}
public void addComments(
ChangeAttribute ca,
Collection<ChangeMessage> messages,
AccountAttributeLoader accountLoader) {
if (!messages.isEmpty()) {
ca.comments = new ArrayList<>();
for (ChangeMessage message : messages) {
ca.comments.add(asMessageAttribute(message, accountLoader));
}
}
}
public PatchSetAttribute asPatchSetAttribute(RevWalk revWalk, Change change, PatchSet patchSet) {
return asPatchSetAttribute(revWalk, change, patchSet, null);
}
/** Create a PatchSetAttribute for the given patchset suitable for serialization to JSON. */
public PatchSetAttribute asPatchSetAttribute(
RevWalk revWalk, Change change, PatchSet patchSet, AccountAttributeLoader accountLoader) {
PatchSetAttribute p = new PatchSetAttribute();
p.revision = patchSet.commitId().name();
p.number = patchSet.number();
p.ref = patchSet.refName();
p.uploader = asAccountAttribute(patchSet.uploader(), accountLoader);
p.createdOn = patchSet.createdOn().getEpochSecond();
PatchSet.Id pId = patchSet.id();
try {
p.parents = new ArrayList<>();
RevCommit c = revWalk.parseCommit(ObjectId.fromString(p.revision));
for (RevCommit parent : c.getParents()) {
p.parents.add(parent.name());
}
UserIdentity author = emails.toUserIdentity(c.getAuthorIdent());
if (author.getAccount() == null) {
p.author = new AccountAttribute();
p.author.email = author.getEmail();
p.author.name = author.getName();
p.author.username = "";
} else {
p.author = asAccountAttribute(author.getAccount(), accountLoader);
}
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
for (FileDiffOutput fileDiff : modifiedFiles.values()) {
p.sizeDeletions += fileDiff.deletions();
p.sizeInsertions += fileDiff.insertions();
}
p.kind = changeKindCache.getChangeKind(change, patchSet);
} catch (IOException | StorageException e) {
logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.id());
} catch (DiffNotAvailableException e) {
logger.atSevere().withCause(e).log("Cannot get size information for %s.", pId);
}
return p;
}
public void addApprovals(
PatchSetAttribute p,
PatchSet.Id id,
Map<PatchSet.Id, Collection<PatchSetApproval>> all,
LabelTypes labelTypes,
AccountAttributeLoader accountLoader) {
Collection<PatchSetApproval> list = all.get(id);
if (list != null) {
addApprovals(p, list, labelTypes, accountLoader);
}
}
public void addApprovals(
PatchSetAttribute p,
Collection<PatchSetApproval> list,
LabelTypes labelTypes,
AccountAttributeLoader accountLoader) {
if (!list.isEmpty()) {
p.approvals = new ArrayList<>(list.size());
for (PatchSetApproval a : list) {
if (a.value() != 0) {
p.approvals.add(asApprovalAttribute(a, labelTypes, accountLoader));
}
}
if (p.approvals.isEmpty()) {
p.approvals = null;
}
}
}
public AccountAttribute asAccountAttribute(Account.Id id, AccountAttributeLoader accountLoader) {
return accountLoader != null ? accountLoader.get(id) : asAccountAttribute(id);
}
/** Create an AuthorAttribute for the given account suitable for serialization to JSON. */
@Nullable
public AccountAttribute asAccountAttribute(Account.Id id) {
if (id == null) {
return null;
}
return accountCache.get(id).map(this::asAccountAttribute).orElse(null);
}
/** Create an AuthorAttribute for the given account suitable for serialization to JSON. */
public AccountAttribute asAccountAttribute(AccountState accountState) {
AccountAttribute who = new AccountAttribute();
who.name = accountState.account().fullName();
who.email = accountState.account().preferredEmail();
who.username = accountState.userName().orElse(null);
return who;
}
/** Create an AuthorAttribute for the given person ident suitable for serialization to JSON. */
public AccountAttribute asAccountAttribute(PersonIdent ident) {
AccountAttribute who = new AccountAttribute();
who.name = ident.getName();
who.email = ident.getEmailAddress();
return who;
}
/**
* Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
*
* @param labelTypes label types for the containing project
* @return object suitable for serialization to JSON
*/
public ApprovalAttribute asApprovalAttribute(
PatchSetApproval approval, LabelTypes labelTypes, AccountAttributeLoader accountLoader) {
ApprovalAttribute a = new ApprovalAttribute();
a.type = approval.labelId().get();
a.value = Short.toString(approval.value());
a.by = asAccountAttribute(approval.accountId(), accountLoader);
a.grantedOn = approval.granted().getEpochSecond();
a.oldValue = null;
Optional<LabelType> lt = labelTypes.byLabel(approval.labelId());
lt.ifPresent(l -> a.description = l.getName());
return a;
}
public MessageAttribute asMessageAttribute(
ChangeMessage message, AccountAttributeLoader accountLoader) {
MessageAttribute a = new MessageAttribute();
a.timestamp = message.getWrittenOn().getEpochSecond();
a.reviewer =
message.getAuthor() != null
? asAccountAttribute(message.getAuthor(), accountLoader)
: asAccountAttribute(myIdent.get());
a.message = accountTemplateUtil.replaceTemplates(message.getMessage());
return a;
}
public PatchSetCommentAttribute asPatchSetLineAttribute(
HumanComment c, AccountAttributeLoader accountLoader) {
PatchSetCommentAttribute a = new PatchSetCommentAttribute();
a.reviewer = asAccountAttribute(c.author.getId(), accountLoader);
a.file = c.key.filename;
a.line = c.lineNbr;
a.message = c.message;
return a;
}
/** Get a link to the change; null if the server doesn't know its own address. */
@Nullable
private String getChangeUrl(Change change) {
if (change != null) {
return urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
}
return null;
}
private void addHashTags(ChangeAttribute changeAttribute, ChangeNotes notes) {
ImmutableSet<String> hashtags = notes.load().getHashtags();
if (!hashtags.isEmpty()) {
changeAttribute.hashtags = new ArrayList<>(hashtags.size());
changeAttribute.hashtags.addAll(hashtags);
}
}
}