All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.events.EventFactory Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.GerritPersonIdent;
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.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.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
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 UrlFormatter urlFormatter;
  private final Emails emails;
  private final PatchListCache patchListCache;
  private final Provider myIdent;
  private final ChangeData.Factory changeDataFactory;
  private final ApprovalsUtil approvalsUtil;
  private final ChangeKindCache changeKindCache;
  private final Provider queryProvider;
  private final SchemaFactory schema;
  private final IndexConfig indexConfig;

  @Inject
  EventFactory(
      AccountCache accountCache,
      Emails emails,
      UrlFormatter urlFormatter,
      PatchListCache patchListCache,
      @GerritPersonIdent Provider myIdent,
      ChangeData.Factory changeDataFactory,
      ApprovalsUtil approvalsUtil,
      ChangeKindCache changeKindCache,
      Provider queryProvider,
      SchemaFactory schema,
      IndexConfig indexConfig) {
    this.accountCache = accountCache;
    this.urlFormatter = urlFormatter;
    this.emails = emails;
    this.patchListCache = patchListCache;
    this.myIdent = myIdent;
    this.changeDataFactory = changeDataFactory;
    this.approvalsUtil = approvalsUtil;
    this.changeKindCache = changeKindCache;
    this.queryProvider = queryProvider;
    this.schema = schema;
    this.indexConfig = indexConfig;
  }

  /**
   * Create a ChangeAttribute for the given change suitable for serialization to JSON.
   *
   * @param change
   * @return object suitable for serialization to JSON
   */
  public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
    try (ReviewDb db = schema.open()) {
      return asChangeAttribute(db, change, notes);
    } catch (OrmException e) {
      logger.atSevere().withCause(e).log("Cannot open database connection");
      return new ChangeAttribute();
    }
  }

  /**
   * Create a ChangeAttribute for the given change suitable for serialization to JSON.
   *
   * @param db Review database
   * @param change
   * @return object suitable for serialization to JSON
   */
  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change) {
    ChangeAttribute a = new ChangeAttribute();
    a.project = change.getProject().get();
    a.branch = change.getDest().getShortName();
    a.topic = change.getTopic();
    a.id = change.getKey().get();
    a.number = change.getId().get();
    a.subject = change.getSubject();
    try {
      a.commitMessage = changeDataFactory.create(db, change).commitMessage();
    } catch (Exception e) {
      logger.atSevere().withCause(e).log(
          "Error while getting full commit message for change %d", a.number);
    }
    a.url = getChangeUrl(change);
    a.owner = asAccountAttribute(change.getOwner());
    a.assignee = asAccountAttribute(change.getAssignee());
    a.status = change.getStatus();
    a.createdOn = change.getCreatedOn().getTime() / 1000L;
    a.wip = change.isWorkInProgress() ? true : null;
    a.isPrivate = change.isPrivate() ? true : null;
    return a;
  }

  /**
   * Create a ChangeAttribute for the given change suitable for serialization to JSON.
   *
   * @param db Review database
   * @param change
   * @param notes
   * @return object suitable for serialization to JSON
   */
  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change, ChangeNotes notes)
      throws OrmException {
    ChangeAttribute a = asChangeAttribute(db, change);
    Set hashtags = notes.load().getHashtags();
    if (!hashtags.isEmpty()) {
      a.hashtags = new ArrayList<>(hashtags.size());
      a.hashtags.addAll(hashtags);
    }
    return a;
  }
  /**
   * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and branch that is
   * suitable for serialization to JSON.
   *
   * @param oldId
   * @param newId
   * @param refName
   * @return object suitable for serialization to JSON
   */
  public RefUpdateAttribute asRefUpdateAttribute(
      ObjectId oldId, ObjectId newId, Branch.NameKey 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.getParentKey().get();
    ru.refName = refName.get();
    return ru;
  }

  /**
   * Extend the existing ChangeAttribute with additional fields.
   *
   * @param a
   * @param change
   */
  public void extend(ChangeAttribute a, Change change) {
    a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
    a.open = change.getStatus().isOpen();
  }

  /**
   * Add allReviewers to an existing ChangeAttribute.
   *
   * @param a
   * @param notes
   */
  public void addAllReviewers(ReviewDb db, ChangeAttribute a, ChangeNotes notes)
      throws OrmException {
    Collection reviewers = approvalsUtil.getReviewers(db, notes).all();
    if (!reviewers.isEmpty()) {
      a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
      for (Account.Id id : reviewers) {
        a.allReviewers.add(asAccountAttribute(id));
      }
    }
  }

  /**
   * Add submitRecords to an existing ChangeAttribute.
   *
   * @param ca
   * @param submitRecords
   */
  public void addSubmitRecords(ChangeAttribute ca, List submitRecords) {
    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);
        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) {
    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);
        }
        sa.labels.add(la);
      }
    }
  }

  private void addSubmitRecordRequirements(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
    if (submitRecord.requirements != null && !submitRecord.requirements.isEmpty()) {
      sa.requirements = new ArrayList<>();
      for (SubmitRequirement req : submitRecord.requirements) {
        SubmitRequirementAttribute re = new SubmitRequirementAttribute();
        re.fallbackText = req.fallbackText();
        re.type = req.type();
        re.data = req.data();
        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 (OrmException | 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 OrmException, IOException {
    RevCommit commit = rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
    final List 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.getRevision().get().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 OrmException, IOException {
    if (currentPs.getGroups().isEmpty()) {
      return;
    }
    String rev = currentPs.getRevision().get();
    // 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.getGroups())) {
      PATCH_SETS:
      for (PatchSet ps : cd.patchSets()) {
        RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
        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.getId().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.getRevision().get();
    d.ref = ps.getRefName();
    return d;
  }

  public void addTrackingIds(ChangeAttribute a, ListMultimap set) {
    if (!set.isEmpty()) {
      a.trackingIds = new ArrayList<>(set.size());
      for (Map.Entry> 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;
  }

  public void addPatchSets(
      ReviewDb db,
      RevWalk revWalk,
      ChangeAttribute ca,
      Collection ps,
      Map> approvals,
      LabelTypes labelTypes) {
    addPatchSets(db, revWalk, ca, ps, approvals, false, null, labelTypes);
  }

  public void addPatchSets(
      ReviewDb db,
      RevWalk revWalk,
      ChangeAttribute ca,
      Collection ps,
      Map> approvals,
      boolean includeFiles,
      Change change,
      LabelTypes labelTypes) {
    if (!ps.isEmpty()) {
      ca.patchSets = new ArrayList<>(ps.size());
      for (PatchSet p : ps) {
        PatchSetAttribute psa = asPatchSetAttribute(db, revWalk, change, p);
        if (approvals != null) {
          addApprovals(psa, p.getId(), approvals, labelTypes);
        }
        ca.patchSets.add(psa);
        if (includeFiles) {
          addPatchSetFileNames(psa, change, p);
        }
      }
    }
  }

  public void addPatchSetComments(
      PatchSetAttribute patchSetAttribute, Collection comments) {
    for (Comment comment : comments) {
      if (comment.key.patchSetId == patchSetAttribute.number) {
        if (patchSetAttribute.comments == null) {
          patchSetAttribute.comments = new ArrayList<>();
        }
        patchSetAttribute.comments.add(asPatchSetLineAttribute(comment));
      }
    }
  }

  public void addPatchSetFileNames(
      PatchSetAttribute patchSetAttribute, Change change, PatchSet patchSet) {
    try {
      PatchList patchList = patchListCache.get(change, patchSet);
      for (PatchListEntry patch : patchList.getPatches()) {
        if (patchSetAttribute.files == null) {
          patchSetAttribute.files = new ArrayList<>();
        }

        PatchAttribute p = new PatchAttribute();
        p.file = patch.getNewName();
        p.fileOld = patch.getOldName();
        p.type = patch.getChangeType();
        p.deletions -= patch.getDeletions();
        p.insertions = patch.getInsertions();
        patchSetAttribute.files.add(p);
      }
    } catch (PatchListObjectTooLargeException e) {
      logger.atWarning().log("Cannot get patch list: %s", e.getMessage());
    } catch (PatchListNotAvailableException e) {
      logger.atSevere().withCause(e).log("Cannot get patch list");
    }
  }

  public void addComments(ChangeAttribute ca, Collection messages) {
    if (!messages.isEmpty()) {
      ca.comments = new ArrayList<>();
      for (ChangeMessage message : messages) {
        ca.comments.add(asMessageAttribute(message));
      }
    }
  }

  /**
   * Create a PatchSetAttribute for the given patchset suitable for serialization to JSON.
   *
   * @param revWalk
   * @param patchSet
   * @return object suitable for serialization to JSON
   */
  public PatchSetAttribute asPatchSetAttribute(RevWalk revWalk, Change change, PatchSet patchSet) {
    try (ReviewDb db = schema.open()) {
      return asPatchSetAttribute(db, revWalk, change, patchSet);
    } catch (OrmException e) {
      logger.atSevere().withCause(e).log("Cannot open database connection");
      return new PatchSetAttribute();
    }
  }

  /**
   * Create a PatchSetAttribute for the given patchset suitable for serialization to JSON.
   *
   * @param db Review database
   * @param patchSet
   * @return object suitable for serialization to JSON
   */
  public PatchSetAttribute asPatchSetAttribute(
      ReviewDb db, RevWalk revWalk, Change change, PatchSet patchSet) {
    PatchSetAttribute p = new PatchSetAttribute();
    p.revision = patchSet.getRevision().get();
    p.number = patchSet.getPatchSetId();
    p.ref = patchSet.getRefName();
    p.uploader = asAccountAttribute(patchSet.getUploader());
    p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
    PatchSet.Id pId = patchSet.getId();
    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 = 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());
      }

      List list = patchListCache.get(change, patchSet).toPatchList(pId);
      for (Patch pe : list) {
        if (!Patch.isMagic(pe.getFileName())) {
          p.sizeDeletions -= pe.getDeletions();
          p.sizeInsertions += pe.getInsertions();
        }
      }
      p.kind = changeKindCache.getChangeKind(db, change, patchSet);
    } catch (IOException | OrmException e) {
      logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.getId());
    } catch (PatchListObjectTooLargeException e) {
      logger.atWarning().log("Cannot get size information for %s: %s", pId, e.getMessage());
    } catch (PatchListNotAvailableException e) {
      logger.atSevere().withCause(e).log("Cannot get size information for %s.", pId);
    }
    return p;
  }

  // TODO: The same method exists in PatchSetInfoFactory, find a common place
  // for it
  private UserIdentity toUserIdentity(PersonIdent who) throws IOException, OrmException {
    UserIdentity u = new UserIdentity();
    u.setName(who.getName());
    u.setEmail(who.getEmailAddress());
    u.setDate(new Timestamp(who.getWhen().getTime()));
    u.setTimeZone(who.getTimeZoneOffset());

    // If only one account has access to this email address, select it
    // as the identity of the user.
    //
    Set a = emails.getAccountFor(u.getEmail());
    if (a.size() == 1) {
      u.setAccount(a.iterator().next());
    }

    return u;
  }

  public void addApprovals(
      PatchSetAttribute p,
      PatchSet.Id id,
      Map> all,
      LabelTypes labelTypes) {
    Collection list = all.get(id);
    if (list != null) {
      addApprovals(p, list, labelTypes);
    }
  }

  public void addApprovals(
      PatchSetAttribute p, Collection list, LabelTypes labelTypes) {
    if (!list.isEmpty()) {
      p.approvals = new ArrayList<>(list.size());
      for (PatchSetApproval a : list) {
        if (a.getValue() != 0) {
          p.approvals.add(asApprovalAttribute(a, labelTypes));
        }
      }
      if (p.approvals.isEmpty()) {
        p.approvals = null;
      }
    }
  }

  /**
   * Create an AuthorAttribute for the given account suitable for serialization to JSON.
   *
   * @param id
   * @return object suitable for serialization to JSON
   */
  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.
   *
   * @param accountState the account state
   * @return object suitable for serialization to JSON
   */
  public AccountAttribute asAccountAttribute(AccountState accountState) {
    AccountAttribute who = new AccountAttribute();
    who.name = accountState.getAccount().getFullName();
    who.email = accountState.getAccount().getPreferredEmail();
    who.username = accountState.getUserName().orElse(null);
    return who;
  }

  /**
   * Create an AuthorAttribute for the given person ident suitable for serialization to JSON.
   *
   * @param ident
   * @return object 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 approval
   * @param labelTypes label types for the containing project
   * @return object suitable for serialization to JSON
   */
  public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval, LabelTypes labelTypes) {
    ApprovalAttribute a = new ApprovalAttribute();
    a.type = approval.getLabelId().get();
    a.value = Short.toString(approval.getValue());
    a.by = asAccountAttribute(approval.getAccountId());
    a.grantedOn = approval.getGranted().getTime() / 1000L;
    a.oldValue = null;

    LabelType lt = labelTypes.byLabel(approval.getLabelId());
    if (lt != null) {
      a.description = lt.getName();
    }
    return a;
  }

  public MessageAttribute asMessageAttribute(ChangeMessage message) {
    MessageAttribute a = new MessageAttribute();
    a.timestamp = message.getWrittenOn().getTime() / 1000L;
    a.reviewer =
        message.getAuthor() != null
            ? asAccountAttribute(message.getAuthor())
            : asAccountAttribute(myIdent.get());
    a.message = message.getMessage();
    return a;
  }

  public PatchSetCommentAttribute asPatchSetLineAttribute(Comment c) {
    PatchSetCommentAttribute a = new PatchSetCommentAttribute();
    a.reviewer = asAccountAttribute(c.author.getId());
    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. */
  private String getChangeUrl(Change change) {
    if (change != null) {
      return urlFormatter.getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
    }
    return null;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy