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

com.google.gerrit.server.notedb.ChangeNotesParser Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// Copyright (C) 2014 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.notedb;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COPIED_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REVERT_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;

import com.google.common.base.Enums;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.FormatMethod;
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.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.AssigneeStatusUpdate;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.LabelVote;
import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.util.RawParseUtils;

class ChangeNotesParser {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  // Private final members initialized in the constructor.
  private final ChangeNoteJson changeNoteJson;
  private final NoteDbMetrics metrics;
  private final Change.Id id;
  private final ObjectId tip;
  private final ChangeNotesRevWalk walk;

  // Private final but mutable members initialized in the constructor and filled
  // in during the parsing process.
  private final Table reviewers;
  private final Table reviewersByEmail;
  private final List allPastReviewers;
  private final List reviewerUpdates;
  /** Holds only the most recent update per user. Older updates are discarded. */
  private final Map latestAttentionStatus;
  /** Holds all updates to attention set. */
  private final List allAttentionSetUpdates;

  private final List assigneeUpdates;
  private final List submitRecords;
  private final ListMultimap humanComments;
  private final List submitRequirementResults;
  private final Map patchSets;
  private final Set deletedPatchSets;
  private final Map patchSetStates;
  private final List currentPatchSets;
  private final Map approvals;
  private final List bufferedApprovals;
  private final List allChangeMessages;

  // Non-final private members filled in during the parsing process.
  private String branch;
  private Change.Status status;
  private String topic;
  private Set hashtags;
  private Timestamp createdOn;
  private Timestamp lastUpdatedOn;
  private Account.Id ownerId;
  private String serverId;
  private String changeId;
  private String subject;
  private String originalSubject;
  private String submissionId;
  private String tag;
  private RevisionNoteMap revisionNoteMap;
  private Boolean isPrivate;
  private Boolean workInProgress;
  private Boolean previousWorkInProgressFooter;
  private Boolean hasReviewStarted;
  private ReviewerSet pendingReviewers;
  private ReviewerByEmailSet pendingReviewersByEmail;
  private Change.Id revertOf;
  private int updateCount;
  // Null indicates that the field was not parsed (yet).
  // We only set the value once, based on the latest update (the actual value or Optional.empty() if
  // the latest record unsets the field).
  private Optional cherryPickOf;
  private Timestamp mergedOn;

  ChangeNotesParser(
      Change.Id changeId,
      ObjectId tip,
      ChangeNotesRevWalk walk,
      ChangeNoteJson changeNoteJson,
      NoteDbMetrics metrics) {
    this.id = changeId;
    this.tip = tip;
    this.walk = walk;
    this.changeNoteJson = changeNoteJson;
    this.metrics = metrics;
    approvals = new LinkedHashMap<>();
    bufferedApprovals = new ArrayList<>();
    reviewers = HashBasedTable.create();
    reviewersByEmail = HashBasedTable.create();
    pendingReviewers = ReviewerSet.empty();
    pendingReviewersByEmail = ReviewerByEmailSet.empty();
    allPastReviewers = new ArrayList<>();
    reviewerUpdates = new ArrayList<>();
    latestAttentionStatus = new HashMap<>();
    allAttentionSetUpdates = new ArrayList<>();
    assigneeUpdates = new ArrayList<>();
    submitRecords = Lists.newArrayListWithExpectedSize(1);
    allChangeMessages = new ArrayList<>();
    humanComments = MultimapBuilder.hashKeys().arrayListValues().build();
    submitRequirementResults = new ArrayList<>();
    patchSets = new HashMap<>();
    deletedPatchSets = new HashSet<>();
    patchSetStates = new HashMap<>();
    currentPatchSets = new ArrayList<>();
  }

  ChangeNotesState parseAll() throws ConfigInvalidException, IOException {
    // Don't include initial parse in timer, as this might do more I/O to page
    // in the block containing most commits. Later reads are not guaranteed to
    // avoid I/O, but often should.
    walk.reset();
    walk.markStart(walk.parseCommit(tip));

    try (Timer0.Context timer = metrics.parseLatency.start()) {
      ChangeNotesCommit commit;
      while ((commit = walk.next()) != null) {
        parse(commit);
      }
      if (hasReviewStarted == null) {
        if (previousWorkInProgressFooter == null) {
          hasReviewStarted = true;
        } else {
          hasReviewStarted = !previousWorkInProgressFooter;
        }
      }
      parseNotes();
      allPastReviewers.addAll(reviewers.rowKeySet());
      pruneReviewers();
      pruneReviewersByEmail();

      updatePatchSetStates();
      checkMandatoryFooters();
    }

    return buildState();
  }

  RevisionNoteMap getRevisionNoteMap() {
    return revisionNoteMap;
  }

  private ChangeNotesState buildState() throws ConfigInvalidException {
    return ChangeNotesState.create(
        tip.copy(),
        id,
        Change.key(changeId),
        createdOn,
        lastUpdatedOn,
        ownerId,
        serverId,
        branch,
        buildCurrentPatchSetId(),
        subject,
        topic,
        originalSubject,
        submissionId,
        status,
        firstNonNull(hashtags, ImmutableSet.of()),
        buildPatchSets(),
        buildApprovals(),
        ReviewerSet.fromTable(Tables.transpose(reviewers)),
        ReviewerByEmailSet.fromTable(Tables.transpose(reviewersByEmail)),
        pendingReviewers,
        pendingReviewersByEmail,
        allPastReviewers,
        buildReviewerUpdates(),
        ImmutableSet.copyOf(latestAttentionStatus.values()),
        allAttentionSetUpdates,
        assigneeUpdates,
        submitRecords,
        buildAllMessages(),
        humanComments,
        submitRequirementResults,
        firstNonNull(isPrivate, false),
        firstNonNull(workInProgress, false),
        firstNonNull(hasReviewStarted, true),
        revertOf,
        cherryPickOf != null ? cherryPickOf.orElse(null) : null,
        updateCount,
        mergedOn);
  }

  private Map buildPatchSets() throws ConfigInvalidException {
    Map result = Maps.newHashMapWithExpectedSize(patchSets.size());
    for (Map.Entry e : patchSets.entrySet()) {
      try {
        PatchSet ps = e.getValue().build();
        result.put(ps.id(), ps);
      } catch (Exception ex) {
        ConfigInvalidException cie = parseException("Error building patch set %s", e.getKey());
        cie.initCause(ex);
        throw cie;
      }
    }
    return result;
  }

  private PatchSet.Id buildCurrentPatchSetId() {
    // currentPatchSets are in parse order, i.e. newest first. Pick the first
    // patch set that was marked as current, excluding deleted patch sets.
    for (PatchSet.Id psId : currentPatchSets) {
      if (patchSetCommitParsed(psId)) {
        return psId;
      }
    }
    return null;
  }

  private ListMultimap buildApprovals() {
    ListMultimap result =
        MultimapBuilder.hashKeys().arrayListValues().build();
    for (PatchSetApproval.Builder a : approvals.values()) {
      if (!patchSetCommitParsed(a.key().patchSetId())) {
        continue; // Patch set deleted or missing.
      } else if (allPastReviewers.contains(a.key().accountId())
          && !reviewers.containsRow(a.key().accountId())) {
        continue; // Reviewer was explicitly removed.
      }
      result.put(a.key().patchSetId(), a.build());
    }
    result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
    return result;
  }

  private List buildReviewerUpdates() {
    List result = new ArrayList<>();
    HashMap lastState = new HashMap<>();
    for (ReviewerStatusUpdate u : Lists.reverse(reviewerUpdates)) {
      if (!Objects.equals(ownerId, u.reviewer()) && lastState.get(u.reviewer()) != u.state()) {
        result.add(u);
        lastState.put(u.reviewer(), u.state());
      }
    }
    return result;
  }

  private List buildAllMessages() {
    return Lists.reverse(allChangeMessages);
  }

  private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
    Timestamp commitTimestamp = getCommitTimestamp(commit);

    createdOn = commitTimestamp;
    parseTag(commit);

    if (branch == null) {
      branch = parseBranch(commit);
    }

    PatchSet.Id psId = parsePatchSetId(commit);
    PatchSetState psState = parsePatchSetState(commit);
    if (psState != null) {
      if (!patchSetStates.containsKey(psId)) {
        patchSetStates.put(psId, psState);
      }
      if (psState == PatchSetState.DELETED) {
        deletedPatchSets.add(psId);
      }
    }

    Account.Id accountId = parseIdent(commit);
    if (accountId != null) {
      ownerId = accountId;
      PersonIdent personIdent = commit.getAuthorIdent();
      serverId = NoteDbUtil.extractHostPartFromPersonIdent(personIdent);
    } else {
      serverId = "UNKNOWN_SERVER_ID";
    }
    Account.Id realAccountId = parseRealAccountId(commit, accountId);

    if (changeId == null) {
      changeId = parseChangeId(commit);
    }

    String currSubject = parseSubject(commit);
    if (currSubject != null) {
      if (subject == null) {
        subject = currSubject;
      }
      originalSubject = currSubject;
    }

    boolean hasChangeMessage =
        parseChangeMessage(psId, accountId, realAccountId, commit, commitTimestamp);
    if (topic == null) {
      topic = parseTopic(commit);
    }

    parseHashtags(commit);
    parseAttentionSetUpdates(commit);
    parseAssigneeUpdates(commitTimestamp, commit);

    parseSubmission(commit, commitTimestamp);

    if (lastUpdatedOn == null || commitTimestamp.after(lastUpdatedOn)) {
      lastUpdatedOn = commitTimestamp;
    }

    if (deletedPatchSets.contains(psId)) {
      // Do not update PS details as PS was deleted and this meta data is of no relevance.
      return;
    }

    // Parse mutable patch set fields first so they can be recorded in the PendingPatchSetFields.
    parseDescription(psId, commit);
    parseGroups(psId, commit);

    ObjectId currRev = parseRevision(commit);
    if (currRev != null) {
      parsePatchSet(psId, currRev, accountId, commitTimestamp);
    }
    parseCurrentPatchSet(psId, commit);

    if (status == null) {
      status = parseStatus(commit);
    }

    // Parse approvals after status to treat approvals in the same commit as
    // "Status: merged" as non-post-submit.
    for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
      parseApproval(psId, accountId, realAccountId, commitTimestamp, line);
    }
    for (String line : commit.getFooterLineValues(FOOTER_COPIED_LABEL)) {
      parseCopiedApproval(psId, commitTimestamp, line);
    }

    for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
      for (String line : commit.getFooterLineValues(state.getFooterKey())) {
        parseReviewer(commitTimestamp, state, line);
      }
      for (String line : commit.getFooterLineValues(state.getByEmailFooterKey())) {
        parseReviewerByEmail(commitTimestamp, state, line);
      }
      // Don't update timestamp when a reviewer was added, matching RevewDb
      // behavior.
    }

    if (isPrivate == null) {
      parseIsPrivate(commit);
    }

    if (revertOf == null) {
      revertOf = parseRevertOf(commit);
    }

    if (cherryPickOf == null) {
      cherryPickOf = parseCherryPickOf(commit);
    }

    previousWorkInProgressFooter = null;
    parseWorkInProgress(commit);
    if (countTowardsMaxUpdatesLimit(commit, hasChangeMessage)) {
      updateCount++;
    }
  }

  private void parseSubmission(ChangeNotesCommit commit, Timestamp commitTimestamp)
      throws ConfigInvalidException {
    // Only parse the most recent sumbit commit (there should be exactly one).
    if (submissionId == null) {
      submissionId = parseSubmissionId(commit);
    }

    if (submissionId != null && mergedOn == null) {
      mergedOn = commitTimestamp;
    }

    if (submitRecords.isEmpty()) {
      // Only parse the most recent set of submit records; any older ones are
      // still there, but not currently used.
      parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
    }
  }

  private String parseSubmissionId(ChangeNotesCommit commit) throws ConfigInvalidException {
    return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
  }

  private String parseBranch(ChangeNotesCommit commit) throws ConfigInvalidException {
    String branch = parseOneFooter(commit, FOOTER_BRANCH);
    return branch != null ? RefNames.fullName(branch) : null;
  }

  private String parseChangeId(ChangeNotesCommit commit) throws ConfigInvalidException {
    return parseOneFooter(commit, FOOTER_CHANGE_ID);
  }

  private String parseSubject(ChangeNotesCommit commit) throws ConfigInvalidException {
    return parseOneFooter(commit, FOOTER_SUBJECT);
  }

  private Account.Id parseRealAccountId(ChangeNotesCommit commit, Account.Id effectiveAccountId)
      throws ConfigInvalidException {
    String realUser = parseOneFooter(commit, FOOTER_REAL_USER);
    if (realUser == null) {
      return effectiveAccountId;
    }
    PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
    return parseIdent(ident);
  }

  private String parseTopic(ChangeNotesCommit commit) throws ConfigInvalidException {
    return parseOneFooter(commit, FOOTER_TOPIC);
  }

  private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
      throws ConfigInvalidException {
    List footerLines = commit.getFooterLineValues(footerKey);
    if (footerLines.isEmpty()) {
      return null;
    } else if (footerLines.size() > 1) {
      throw expectedOneFooter(footerKey, footerLines);
    }
    return footerLines.get(0);
  }

  private String parseExactlyOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
      throws ConfigInvalidException {
    String line = parseOneFooter(commit, footerKey);
    if (line == null) {
      throw expectedOneFooter(footerKey, Collections.emptyList());
    }
    return line;
  }

  private ObjectId parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
    String sha = parseOneFooter(commit, FOOTER_COMMIT);
    if (sha == null) {
      return null;
    }
    try {
      return ObjectId.fromString(sha);
    } catch (InvalidObjectIdException e) {
      ConfigInvalidException cie = invalidFooter(FOOTER_COMMIT, sha);
      cie.initCause(e);
      throw cie;
    }
  }

  private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Timestamp ts)
      throws ConfigInvalidException {
    if (accountId == null) {
      throw parseException("patch set %s requires an identified user as uploader", psId.get());
    }
    if (patchSetCommitParsed(psId)) {
      ObjectId commitId = patchSets.get(psId).commitId().orElseThrow(IllegalStateException::new);
      throw new ConfigInvalidException(
          String.format(
              "Multiple revisions parsed for patch set %s: %s and %s",
              psId.get(), commitId.name(), rev.name()));
    }
    patchSets
        .computeIfAbsent(psId, id -> PatchSet.builder())
        .id(psId)
        .commitId(rev)
        .uploader(accountId)
        .createdOn(ts);
    // Fields not set here:
    // * Groups, parsed earlier in parseGroups.
    // * Description, parsed earlier in parseDescription.
    // * Push certificate, parsed later in parseNotes.
  }

  private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
      throws ConfigInvalidException {
    String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
    if (groupsStr == null) {
      return;
    }
    checkPatchSetCommitNotParsed(psId, FOOTER_GROUPS);
    PatchSet.Builder pending = patchSets.computeIfAbsent(psId, id -> PatchSet.builder());
    if (pending.groups().isEmpty()) {
      pending.groups(PatchSet.splitGroups(groupsStr));
    }
  }

  private void parseCurrentPatchSet(PatchSet.Id psId, ChangeNotesCommit commit)
      throws ConfigInvalidException {
    // This commit implies a new current patch set if either it creates a new
    // patch set, or sets the current field explicitly.
    boolean current = false;
    if (parseOneFooter(commit, FOOTER_COMMIT) != null) {
      current = true;
    } else {
      String currentStr = parseOneFooter(commit, FOOTER_CURRENT);
      if (Boolean.TRUE.toString().equalsIgnoreCase(currentStr)) {
        current = true;
      } else if (currentStr != null) {
        // Only "true" is allowed; unsetting the current patch set makes no
        // sense.
        throw invalidFooter(FOOTER_CURRENT, currentStr);
      }
    }
    if (current) {
      currentPatchSets.add(psId);
    }
  }

  private void parseHashtags(ChangeNotesCommit commit) throws ConfigInvalidException {
    // Commits are parsed in reverse order and only the last set of hashtags
    // should be used.
    if (hashtags != null) {
      return;
    }
    List hashtagsLines = commit.getFooterLineValues(FOOTER_HASHTAGS);
    if (hashtagsLines.isEmpty()) {
      return;
    } else if (hashtagsLines.size() > 1) {
      throw expectedOneFooter(FOOTER_HASHTAGS, hashtagsLines);
    } else if (hashtagsLines.get(0).isEmpty()) {
      hashtags = ImmutableSet.of();
    } else {
      hashtags = Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
    }
  }

  private void parseAttentionSetUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
    List attentionStrings = commit.getFooterLineValues(FOOTER_ATTENTION);
    for (String attentionString : attentionStrings) {

      Optional attentionStatus =
          ChangeNoteUtil.attentionStatusFromJson(
              Instant.ofEpochSecond(commit.getCommitTime()), attentionString);
      if (!attentionStatus.isPresent()) {
        throw invalidFooter(FOOTER_ATTENTION, attentionString);
      }
      // Processing is in reverse chronological order. Keep only the latest update.
      latestAttentionStatus.putIfAbsent(attentionStatus.get().account(), attentionStatus.get());

      // Keep all updates as well.
      allAttentionSetUpdates.add(attentionStatus.get());
    }
  }

  private void parseAssigneeUpdates(Timestamp ts, ChangeNotesCommit commit)
      throws ConfigInvalidException {
    String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
    if (assigneeValue != null) {
      Optional parsedAssignee;
      if (assigneeValue.equals("")) {
        // Empty footer found, assignee deleted
        parsedAssignee = Optional.empty();
      } else {
        PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
        parsedAssignee = Optional.ofNullable(parseIdent(ident));
      }
      assigneeUpdates.add(AssigneeStatusUpdate.create(ts, ownerId, parsedAssignee));
    }
  }

  private void parseTag(ChangeNotesCommit commit) throws ConfigInvalidException {
    tag = null;
    List tagLines = commit.getFooterLineValues(FOOTER_TAG);
    if (tagLines.isEmpty()) {
      return;
    } else if (tagLines.size() == 1) {
      tag = tagLines.get(0);
    } else {
      throw expectedOneFooter(FOOTER_TAG, tagLines);
    }
  }

  private Change.Status parseStatus(ChangeNotesCommit commit) throws ConfigInvalidException {
    List statusLines = commit.getFooterLineValues(FOOTER_STATUS);
    if (statusLines.isEmpty()) {
      return null;
    } else if (statusLines.size() > 1) {
      throw expectedOneFooter(FOOTER_STATUS, statusLines);
    }
    Change.Status status =
        Enums.getIfPresent(Change.Status.class, statusLines.get(0).toUpperCase()).orNull();
    if (status == null) {
      throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
    }
    // All approvals after MERGED and before the next status change get the postSubmit
    // bit. (Currently the state can't change from MERGED to something else, but just in case.) The
    // exception is the legacy SUBM approval, which is never considered post-submit, but might end
    // up sorted after the submit during rebuilding.
    if (status == Change.Status.MERGED) {
      for (PatchSetApproval.Builder psa : bufferedApprovals) {
        if (!psa.key().isLegacySubmit()) {
          psa.postSubmit(true);
        }
      }
    }
    bufferedApprovals.clear();
    return status;
  }

  private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit) throws ConfigInvalidException {
    String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
    int s = psIdLine.indexOf(' ');
    String psIdStr = s < 0 ? psIdLine : psIdLine.substring(0, s);
    Integer psId = Ints.tryParse(psIdStr);
    if (psId == null) {
      throw invalidFooter(FOOTER_PATCH_SET, psIdStr);
    }
    return PatchSet.id(id, psId);
  }

  private PatchSetState parsePatchSetState(ChangeNotesCommit commit) throws ConfigInvalidException {
    String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
    int s = psIdLine.indexOf(' ');
    if (s < 0) {
      return null;
    }
    String withParens = psIdLine.substring(s + 1);
    if (withParens.startsWith("(") && withParens.endsWith(")")) {
      PatchSetState state =
          Enums.getIfPresent(
                  PatchSetState.class,
                  withParens.substring(1, withParens.length() - 1).toUpperCase())
              .orNull();
      if (state != null) {
        return state;
      }
    }
    throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
  }

  private void parseDescription(PatchSet.Id psId, ChangeNotesCommit commit)
      throws ConfigInvalidException {
    List descLines = commit.getFooterLineValues(FOOTER_PATCH_SET_DESCRIPTION);
    if (descLines.isEmpty()) {
      return;
    }

    checkPatchSetCommitNotParsed(psId, FOOTER_PATCH_SET_DESCRIPTION);
    if (descLines.size() == 1) {
      String desc = descLines.get(0).trim();
      PatchSet.Builder pending = patchSets.computeIfAbsent(psId, p -> PatchSet.builder());
      if (!pending.description().isPresent()) {
        pending.description(Optional.of(desc));
      }
    } else {
      throw expectedOneFooter(FOOTER_PATCH_SET_DESCRIPTION, descLines);
    }
  }

  private boolean parseChangeMessage(
      PatchSet.Id psId,
      Account.Id accountId,
      Account.Id realAccountId,
      ChangeNotesCommit commit,
      Timestamp ts) {
    Optional changeMsgString = getChangeMessageString(commit);
    if (!changeMsgString.isPresent()) {
      return false;
    }

    ChangeMessage changeMessage =
        ChangeMessage.create(
            ChangeMessage.key(psId.changeId(), commit.name()),
            accountId,
            ts,
            psId,
            changeMsgString.get(),
            realAccountId,
            tag);
    return allChangeMessages.add(changeMessage);
  }

  public static Optional getChangeMessageString(ChangeNotesCommit commit) {
    byte[] raw = commit.getRawBuffer();
    Charset enc = RawParseUtils.parseEncoding(raw);

    Optional range = parseCommitMessageRange(commit);
    return range.map(
        commitMessageRange ->
            commitMessageRange.hasChangeMessage()
                ? RawParseUtils.decode(
                    enc,
                    raw,
                    commitMessageRange.changeMessageStart(),
                    commitMessageRange.changeMessageEnd() + 1)
                : null);
  }

  private void parseNotes() throws IOException, ConfigInvalidException {
    ObjectReader reader = walk.getObjectReader();
    ChangeNotesCommit tipCommit = walk.parseCommit(tip);
    revisionNoteMap =
        RevisionNoteMap.parse(
            changeNoteJson, reader, NoteMap.read(reader, tipCommit), HumanComment.Status.PUBLISHED);
    Map rns = revisionNoteMap.revisionNotes;

    for (Map.Entry e : rns.entrySet()) {
      for (HumanComment c : e.getValue().getEntities()) {
        humanComments.put(e.getKey(), c);
      }
      for (SubmitRequirementResult sr : e.getValue().getSubmitRequirementsResult()) {
        submitRequirementResults.add(sr);
      }
    }

    for (PatchSet.Builder b : patchSets.values()) {
      ObjectId commitId =
          b.commitId()
              .orElseThrow(
                  () ->
                      new IllegalStateException("never parsed commit ID for patch set " + b.id()));
      ChangeRevisionNote rn = rns.get(commitId);
      if (rn != null && rn.getPushCert() != null) {
        b.pushCertificate(Optional.of(rn.getPushCert()));
      }
    }
  }

  // Footer example: Copied-Label: 

Don't use {@link org.eclipse.jgit.revwalk.RevCommit#getCommitTime} directly because it * returns int and would overflow. * * @param commit the commit to return commit time. * @return the timestamp when the commit was applied. */ private Timestamp getCommitTimestamp(ChangeNotesCommit commit) { return new Timestamp(commit.getCommitterIdent().getWhen().getTime()); } private void pruneReviewers() { Iterator> rit = reviewers.cellSet().iterator(); while (rit.hasNext()) { Table.Cell e = rit.next(); if (e.getColumnKey() == ReviewerStateInternal.REMOVED) { rit.remove(); } } } private void pruneReviewersByEmail() { Iterator> rit = reviewersByEmail.cellSet().iterator(); while (rit.hasNext()) { Table.Cell e = rit.next(); if (e.getColumnKey() == ReviewerStateInternal.REMOVED) { rit.remove(); } } } private void updatePatchSetStates() { Set missing = new TreeSet<>(comparing(PatchSet.Id::get)); patchSets.keySet().stream().filter(p -> !patchSetCommitParsed(p)).forEach(p -> missing.add(p)); for (Map.Entry e : patchSetStates.entrySet()) { switch (e.getValue()) { case PUBLISHED: default: break; case DELETED: patchSets.remove(e.getKey()); break; } } // Post-process other collections to remove items corresponding to deleted // (or otherwise missing) patch sets. This is safer than trying to prevent // insertion, as it will also filter out items racily added after the patch // set was deleted. int pruned = pruneEntitiesForMissingPatchSets(allChangeMessages, ChangeMessage::getPatchSetId, missing); pruned += pruneEntitiesForMissingPatchSets( humanComments.values(), c -> PatchSet.id(id, c.key.patchSetId), missing); pruned += pruneEntitiesForMissingPatchSets( approvals.values(), psa -> psa.key().patchSetId(), missing); if (!missing.isEmpty()) { logger.atWarning().log( "ignoring %s additional entities due to missing patch sets: %s", pruned, missing); } } private int pruneEntitiesForMissingPatchSets( Iterable ents, Function psIdFunc, Set missing) { int pruned = 0; for (Iterator it = ents.iterator(); it.hasNext(); ) { PatchSet.Id psId = psIdFunc.apply(it.next()); if (!patchSetCommitParsed(psId)) { pruned++; missing.add(psId); it.remove(); } else if (deletedPatchSets.contains(psId)) { it.remove(); // Not an error we need to report, don't increment pruned. } } return pruned; } private void checkMandatoryFooters() throws ConfigInvalidException { List missing = new ArrayList<>(); if (branch == null) { missing.add(FOOTER_BRANCH); } if (changeId == null) { missing.add(FOOTER_CHANGE_ID); } if (originalSubject == null || subject == null) { missing.add(FOOTER_SUBJECT); } if (!missing.isEmpty()) { throw parseException( "%s", "Missing footers: " + missing.stream().map(FooterKey::getName).collect(joining(", "))); } } private ConfigInvalidException expectedOneFooter(FooterKey footer, List actual) { return parseException("missing or multiple %s: %s", footer.getName(), actual); } private ConfigInvalidException invalidFooter(FooterKey footer, String actual) { return parseException("invalid %s: %s", footer.getName(), actual); } private void checkFooter(boolean expr, FooterKey footer, String actual) throws ConfigInvalidException { if (!expr) { throw invalidFooter(footer, actual); } } private void checkPatchSetCommitNotParsed(PatchSet.Id psId, FooterKey footer) throws ConfigInvalidException { if (patchSetCommitParsed(psId)) { throw parseException( "%s field found for patch set %s before patch set was originally defined", footer.getName(), psId.get()); } } private boolean patchSetCommitParsed(PatchSet.Id psId) { PatchSet.Builder pending = patchSets.get(psId); return pending != null && pending.commitId().isPresent(); } @FormatMethod private ConfigInvalidException parseException(String fmt, Object... args) { return ChangeNotes.parseException(id, fmt, args); } private Account.Id parseIdent(PersonIdent ident) throws ConfigInvalidException { return NoteDbUtil.parseIdent(ident) .orElseThrow( () -> parseException("cannot retrieve account id: %s", ident.getEmailAddress())); } protected boolean countTowardsMaxUpdatesLimit( ChangeNotesCommit commit, boolean hasChangeMessage) { return !commit.isAttentionSetCommitOnly(hasChangeMessage); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy