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.10.0-rc5
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.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_COMMIT;
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_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.NoteDbTable.CHANGES;

import com.google.common.base.Enums;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
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 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;

import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
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.Map.Entry;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

class ChangeNotesParser {
  // Sentinel RevId indicating a mutable field on a patch set was parsed, but
  // the parser does not yet know its commit SHA-1.
  private static final RevId PARTIAL_PATCH_SET =
      new RevId("INVALID PARTIAL PATCH SET");

  // Private final members initialized in the constructor.
  private final ChangeNoteUtil noteUtil;
  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 List allPastReviewers;
  private final List reviewerUpdates;
  private final List submitRecords;
  private final Multimap comments;
  private final TreeMap patchSets;
  private final Set deletedPatchSets;
  private final Map patchSetStates;
  private final Map, Optional>> approvals;
  private final List allChangeMessages;
  private final Multimap changeMessagesByPatchSet;

  // 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 changeId;
  private String subject;
  private String originalSubject;
  private String submissionId;
  private String tag;
  private PatchSet.Id currentPatchSetId;
  private RevisionNoteMap revisionNoteMap;

  ChangeNotesParser(Change.Id changeId, ObjectId tip, ChangeNotesRevWalk walk,
      ChangeNoteUtil noteUtil, NoteDbMetrics metrics) {
    this.id = changeId;
    this.tip = tip;
    this.walk = walk;
    this.noteUtil = noteUtil;
    this.metrics = metrics;
    approvals = new HashMap<>();
    reviewers = HashBasedTable.create();
    allPastReviewers = new ArrayList<>();
    reviewerUpdates = new ArrayList<>();
    submitRecords = Lists.newArrayListWithExpectedSize(1);
    allChangeMessages = new ArrayList<>();
    changeMessagesByPatchSet = LinkedListMultimap.create();
    comments = ArrayListMultimap.create();
    patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering());
    deletedPatchSets = new HashSet<>();
    patchSetStates = new HashMap<>();
  }

  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 (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
      ChangeNotesCommit commit;
      while ((commit = walk.next()) != null) {
        parse(commit);
      }
      parseNotes();
      allPastReviewers.addAll(reviewers.rowKeySet());
      pruneReviewers();
      updatePatchSetStates();
      checkMandatoryFooters();
    }

    return buildState();
  }

  RevisionNoteMap getRevisionNoteMap() {
    return revisionNoteMap;
  }

  private ChangeNotesState buildState() {
    return ChangeNotesState.create(
        id,
        new Change.Key(changeId),
        createdOn,
        lastUpdatedOn,
        ownerId,
        branch,
        currentPatchSetId,
        subject,
        topic,
        originalSubject,
        submissionId,
        status,

        hashtags,
        patchSets,
        buildApprovals(),
        ReviewerSet.fromTable(Tables.transpose(reviewers)),
        allPastReviewers,
        buildReviewerUpdates(),
        submitRecords,
        buildAllMessages(),
        buildMessagesByPatchSet(),
        comments);
  }

  private Multimap buildApprovals() {
    Multimap result =
        ArrayListMultimap.create(approvals.keySet().size(), 3);
    for (Table> curr : approvals.values()) {
      for (Optional psa : curr.values()) {
        if (psa.isPresent()) {
          result.put(psa.get().getPatchSetId(), psa.get());
        }
      }
    }
    for (Collection v : result.asMap().values()) {
      Collections.sort((List) v, 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 Multimap buildMessagesByPatchSet() {
    for (Collection v :
        changeMessagesByPatchSet.asMap().values()) {
      Collections.reverse((List) v);
    }
    return changeMessagesByPatchSet;
  }

  private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
    Timestamp ts =
        new Timestamp(commit.getCommitterIdent().getWhen().getTime());

    createdOn = ts;
    parseTag(commit);

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

    PatchSet.Id psId = parsePatchSetId(commit);
    if (currentPatchSetId == null || psId.get() > currentPatchSetId.get()) {
      currentPatchSetId = psId;
    }

    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;
    }

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

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

    parseChangeMessage(psId, accountId, commit, ts);
    if (topic == null) {
      topic = parseTopic(commit);
    }

    parseHashtags(commit);

    if (submissionId == null) {
      submissionId = parseSubmissionId(commit);
    }

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

    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));
    }

    for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
      parseApproval(psId, accountId, ts, line);
    }

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

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

  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 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());
    }
    PatchSet ps = patchSets.get(psId);
    if (ps == null) {
      ps = new PatchSet(psId);
      patchSets.put(psId, ps);
    } else if (!ps.getRevision().equals(PARTIAL_PATCH_SET)) {
      if (deletedPatchSets.contains(psId)) {
        // Do not update PS details as PS was deleted and this meta data is of
        // no relevance
        return;
      }
      throw new ConfigInvalidException(
          String.format(
              "Multiple revisions parsed for patch set %s: %s and %s",
              psId.get(), patchSets.get(psId).getRevision(), rev.name()));
    }
    ps.setRevision(new RevId(rev.name()));
    ps.setUploader(accountId);
    ps.setCreatedOn(ts);
  }

  private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
      throws ConfigInvalidException {
    String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
    if (groupsStr == null) {
      return;
    }
    PatchSet ps = patchSets.get(psId);
    if (ps == null) {
      ps = new PatchSet(psId);
      ps.setRevision(PARTIAL_PATCH_SET);
      patchSets.put(psId, ps);
    } else if (!ps.getGroups().isEmpty()) {
      return;
    }
    ps.setGroups(PatchSet.splitGroups(groupsStr));
  }

  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 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);
    }
    Optional status = Enums.getIfPresent(
        Change.Status.class, statusLines.get(0).toUpperCase());
    if (!status.isPresent()) {
      throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
    }
    return status.get();
  }

  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 new 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(")")) {
      Optional state = Enums.getIfPresent(PatchSetState.class,
          withParens.substring(1, withParens.length() - 1).toUpperCase());
      if (state.isPresent()) {
        return state.get();
      }
    }
    throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
  }

  private void parseChangeMessage(PatchSet.Id psId,
      Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
    byte[] raw = commit.getRawBuffer();
    int size = raw.length;
    Charset enc = RawParseUtils.parseEncoding(raw);

    int subjectStart = RawParseUtils.commitMessage(raw, 0);
    if (subjectStart < 0 || subjectStart >= size) {
      return;
    }

    int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
    if (subjectEnd == size) {
      return;
    }

    int changeMessageStart;

    if (raw[subjectEnd] == '\n') {
      changeMessageStart = subjectEnd + 2; //\n\n ends paragraph
    } else if (raw[subjectEnd] == '\r') {
      changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
    } else {
      return;
    }

    int ptr = size - 1;
    int changeMessageEnd = -1;
    while (ptr > changeMessageStart) {
      ptr = RawParseUtils.prevLF(raw, ptr, '\r');
      if (ptr == -1) {
        break;
      }
      if (raw[ptr] == '\n') {
        changeMessageEnd = ptr - 1;
        break;
      } else if (raw[ptr] == '\r') {
        changeMessageEnd = ptr - 3;
        break;
      }
    }

    if (ptr <= changeMessageStart) {
      return;
    }

    String changeMsgString = RawParseUtils.decode(enc, raw,
        changeMessageStart, changeMessageEnd + 1);
    ChangeMessage changeMessage = new ChangeMessage(
        new ChangeMessage.Key(psId.getParentKey(), commit.name()),
        accountId,
        ts,
        psId);
    changeMessage.setMessage(changeMsgString);
    changeMessage.setTag(tag);
    changeMessagesByPatchSet.put(psId, changeMessage);
    allChangeMessages.add(changeMessage);
  }

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

    for (Map.Entry e : rns.entrySet()) {
      for (PatchLineComment plc : e.getValue().comments) {
        comments.put(e.getKey(), plc);
      }
    }

    for (PatchSet ps : patchSets.values()) {
      RevisionNote rn = rns.get(ps.getRevision());
      if (rn != null && rn.pushCert != null) {
        ps.setPushCertificate(rn.pushCert);
      }
    }
  }

  private void parseApproval(PatchSet.Id psId, Account.Id accountId,
      Timestamp ts, String line) throws ConfigInvalidException {
    if (accountId == null) {
      throw parseException(
          "patch set %s requires an identified user as uploader", psId.get());
    }
    if (line.startsWith("-")) {
      parseRemoveApproval(psId, accountId, line);
    } else {
      parseAddApproval(psId, accountId, ts, line);
    }
  }

  private void parseAddApproval(PatchSet.Id psId, Account.Id committerId,
      Timestamp ts, String line) throws ConfigInvalidException {
    Account.Id accountId;
    String labelVoteStr;
    int s = line.indexOf(' ');
    if (s > 0) {
      labelVoteStr = line.substring(0, s);
      PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
      checkFooter(ident != null, FOOTER_LABEL, line);
      accountId = noteUtil.parseIdent(ident, id);
    } else {
      labelVoteStr = line;
      accountId = committerId;
    }

    LabelVote l;
    try {
      l = LabelVote.parseWithEquals(labelVoteStr);
    } catch (IllegalArgumentException e) {
      ConfigInvalidException pe =
          parseException("invalid %s: %s", FOOTER_LABEL, line);
      pe.initCause(e);
      throw pe;
    }

    Entry label = Maps.immutableEntry(l.label(), tag);
    Table, Optional> curr =
        getApprovalsTableIfNoVotePresent(psId, accountId, label);
    if (curr != null) {
      PatchSetApproval psa = new PatchSetApproval(
          new PatchSetApproval.Key(
              psId,
              accountId,
              new LabelId(l.label())),
          l.value(),
          ts);
      psa.setTag(tag);
      curr.put(accountId, label, Optional.of(psa));
    }
  }

  private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
      String line) throws ConfigInvalidException {
    Account.Id accountId;
    Entry label;
    int s = line.indexOf(' ');
    if (s > 0) {
      label = Maps.immutableEntry(line.substring(1, s), tag);
      PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
      checkFooter(ident != null, FOOTER_LABEL, line);
      accountId = noteUtil.parseIdent(ident, id);
    } else {
      label = Maps.immutableEntry(line.substring(1), tag);
      accountId = committerId;
    }

    try {
      LabelType.checkNameInternal(label.getKey());
    } catch (IllegalArgumentException e) {
      ConfigInvalidException pe =
          parseException("invalid %s: %s", FOOTER_LABEL, line);
      pe.initCause(e);
      throw pe;
    }

    Table, Optional> curr =
        getApprovalsTableIfNoVotePresent(psId, accountId, label);
    if (curr != null) {
      curr.put(accountId, label, Optional. absent());
    }
  }

  private Table, Optional>
      getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId,
        Entry label) {

    Table, Optional> curr =
        approvals.get(psId);
    if (curr != null) {
      if (curr.contains(accountId, label)) {
        return null;
      }
    } else {
      curr = Tables.newCustomTable(
          Maps., Optional>>
              newHashMapWithExpectedSize(2),
          new Supplier, Optional>>() {
            @Override
            public Map, Optional> get() {
              return new LinkedHashMap<>();
            }
          });
      approvals.put(psId, curr);
    }
    return curr;
  }

  private void parseSubmitRecords(List lines)
      throws ConfigInvalidException {
    SubmitRecord rec = null;

    for (String line : lines) {
      int c = line.indexOf(": ");
      if (c < 0) {
        rec = new SubmitRecord();
        submitRecords.add(rec);
        int s = line.indexOf(' ');
        String statusStr = s >= 0 ? line.substring(0, s) : line;
        Optional status =
            Enums.getIfPresent(SubmitRecord.Status.class, statusStr);
        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
        rec.status = status.get();
        if (s >= 0) {
          rec.errorMessage = line.substring(s);
        }
      } else {
        checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
        SubmitRecord.Label label = new SubmitRecord.Label();
        if (rec.labels == null) {
          rec.labels = new ArrayList<>();
        }
        rec.labels.add(label);

        Optional status = Enums.getIfPresent(
            SubmitRecord.Label.Status.class, line.substring(0, c));
        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
        label.status = status.get();
        int c2 = line.indexOf(": ", c + 2);
        if (c2 >= 0) {
          label.label = line.substring(c + 2, c2);
          PersonIdent ident =
              RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
          checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
          label.appliedBy = noteUtil.parseIdent(ident, id);
        } else {
          label.label = line.substring(c + 2);
        }
      }
    }
  }

  private Account.Id parseIdent(ChangeNotesCommit commit)
      throws ConfigInvalidException {
    // Check if the author name/email is the same as the committer name/email,
    // i.e. was the server ident at the time this commit was made.
    PersonIdent a = commit.getAuthorIdent();
    PersonIdent c = commit.getCommitterIdent();
    if (a.getName().equals(c.getName())
        && a.getEmailAddress().equals(c.getEmailAddress())) {
      return null;
    }
    return noteUtil.parseIdent(commit.getAuthorIdent(), id);
  }

  private void parseReviewer(Timestamp ts, ReviewerStateInternal state,
      String line) throws ConfigInvalidException {
    PersonIdent ident = RawParseUtils.parsePersonIdent(line);
    if (ident == null) {
      throw invalidFooter(state.getFooterKey(), line);
    }
    Account.Id accountId = noteUtil.parseIdent(ident, id);
    reviewerUpdates.add(
        ReviewerStatusUpdate.create(ts, ownerId, accountId, state));
    if (!reviewers.containsRow(accountId)) {
      reviewers.put(accountId, state, ts);
    }
  }

  private void pruneReviewers() {
    Iterator> rit =
        reviewers.cellSet().iterator();
    while (rit.hasNext()) {
      Table.Cell e = rit.next();
      if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
        rit.remove();
        for (Table curr : approvals.values()) {
          curr.rowKeySet().remove(e.getRowKey());
        }
      }
    }
  }

  private void updatePatchSetStates() throws ConfigInvalidException {
    for (PatchSet ps : patchSets.values()) {
      if (ps.getRevision().equals(PARTIAL_PATCH_SET)) {
        throw parseException("No %s found for patch set %s",
            FOOTER_COMMIT, ps.getPatchSetId());
      }
    }
    if (patchSetStates.isEmpty()) {
      return;
    }

    boolean deleted = false;
    for (Map.Entry e : patchSetStates.entrySet()) {
      switch (e.getValue()) {
        case PUBLISHED:
        default:
          break;

        case DELETED:
          deleted = true;
          patchSets.remove(e.getKey());
          break;

        case DRAFT:
          PatchSet ps = patchSets.get(e.getKey());
          if (ps != null) {
            ps.setDraft(true);
          }
          break;
      }
    }
    if (!deleted) {
      return;
    }

    // Post-process other collections to remove items corresponding to deleted
    // 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.
    NavigableSet all = patchSets.navigableKeySet();
    if (!all.isEmpty()) {
      currentPatchSetId = all.last();
    } else {
      currentPatchSetId = null;
    }
    approvals.keySet().retainAll(all);
    changeMessagesByPatchSet.keys().retainAll(all);

    for (Iterator it = allChangeMessages.iterator();
        it.hasNext();) {
      if (!all.contains(it.next().getPatchSetId())) {
        it.remove();
      }
    }
    for (Iterator it = comments.values().iterator();
        it.hasNext();) {
      PatchSet.Id psId = it.next().getKey().getParentKey().getParentKey();
      if (!all.contains(psId)) {
        it.remove();
      }
    }
  }

  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("Missing footers: " + Joiner.on(", ")
          .join(Lists.transform(missing, new Function() {
            @Override
            public String apply(FooterKey input) {
              return input.getName();
            }
          })));
    }
  }

  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 ConfigInvalidException parseException(String fmt, Object... args) {
    return ChangeNotes.parseException(id, fmt, args);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy