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

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

// 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_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_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;

import com.google.common.base.Enums;
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.ImmutableListMultimap;
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.SubmitRecord;
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.server.git.GitRepositoryManager;
import com.google.gerrit.server.util.LabelVote;

import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.RawParseUtils;

import java.io.IOException;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

class ChangeNotesParser implements AutoCloseable {
  final Map reviewers;
  final List allPastReviewers;
  final List submitRecords;
  final Multimap commentsForPs;
  final Multimap commentsForBase;
  NoteMap commentNoteMap;
  Change.Status status;
  Set hashtags;

  private final Change.Id changeId;
  private final ObjectId tip;
  private final RevWalk walk;
  private final Repository repo;
  private final Map>> approvals;
  private final Multimap changeMessages;

  ChangeNotesParser(Change change, ObjectId tip, RevWalk walk,
      GitRepositoryManager repoManager) throws RepositoryNotFoundException,
      IOException {
    this.changeId = change.getId();
    this.tip = tip;
    this.walk = walk;
    this.repo =
        repoManager.openMetadataRepository(ChangeNotes.getProjectName(change));
    approvals = Maps.newHashMap();
    reviewers = Maps.newLinkedHashMap();
    allPastReviewers = Lists.newArrayList();
    submitRecords = Lists.newArrayListWithExpectedSize(1);
    changeMessages = LinkedListMultimap.create();
    commentsForPs = ArrayListMultimap.create();
    commentsForBase = ArrayListMultimap.create();
  }

  @Override
  public void close() {
    repo.close();
  }

  void parseAll() throws ConfigInvalidException, IOException {
    walk.markStart(walk.parseCommit(tip));
    for (RevCommit commit : walk) {
      parse(commit);
    }
    parseComments();
    allPastReviewers.addAll(reviewers.keySet());
    pruneReviewers();
  }

  ImmutableListMultimap
      buildApprovals() {
    Multimap result =
        ArrayListMultimap.create(approvals.keySet().size(), 3);
    for (Table> curr
        : approvals.values()) {
      for (PatchSetApproval psa : Optional.presentInstances(curr.values())) {
        result.put(psa.getPatchSetId(), psa);
      }
    }
    for (Collection v : result.asMap().values()) {
      Collections.sort((List) v, ChangeNotes.PSA_BY_TIME);
    }
    return ImmutableListMultimap.copyOf(result);
  }

  ImmutableListMultimap buildMessages() {
    for (Collection v : changeMessages.asMap().values()) {
      Collections.sort((List) v, ChangeNotes.MESSAGE_BY_TIME);
    }
    return ImmutableListMultimap.copyOf(changeMessages);
  }

  private void parse(RevCommit commit) throws ConfigInvalidException {
    if (status == null) {
      status = parseStatus(commit);
    }
    PatchSet.Id psId = parsePatchSetId(commit);
    Account.Id accountId = parseIdent(commit);
    parseChangeMessage(psId, accountId, commit);
    parseHashtags(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.getFooterLines(FOOTER_SUBMITTED_WITH));
    }

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

    for (ReviewerState state : ReviewerState.values()) {
      for (String line : commit.getFooterLines(state.getFooterKey())) {
        parseReviewer(state, line);
      }
    }
  }

  private void parseHashtags(RevCommit 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.getFooterLines(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 Change.Status parseStatus(RevCommit commit)
      throws ConfigInvalidException {
    List statusLines = commit.getFooterLines(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(RevCommit commit)
      throws ConfigInvalidException {
    List psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
    if (psIdLines.size() != 1) {
      throw expectedOneFooter(FOOTER_PATCH_SET, psIdLines);
    }
    Integer psId = Ints.tryParse(psIdLines.get(0));
    if (psId == null) {
      throw invalidFooter(FOOTER_PATCH_SET, psIdLines.get(0));
    }
    return new PatchSet.Id(changeId, psId);
  }

  private void parseChangeMessage(PatchSet.Id psId, Account.Id accountId,
      RevCommit commit) {
    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,
        new Timestamp(commit.getCommitterIdent().getWhen().getTime()),
        psId);
    changeMessage.setMessage(changeMsgString);
    changeMessages.put(psId, changeMessage);
  }

  private void parseComments()
      throws IOException, ConfigInvalidException {
    commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
        ChangeNoteUtil.changeRefName(changeId), walk, changeId,
        commentsForBase, commentsForPs, PatchLineComment.Status.PUBLISHED);
  }

  private void parseApproval(PatchSet.Id psId, Account.Id accountId,
      RevCommit commit, String line) throws ConfigInvalidException {
    Table> curr =
        approvals.get(psId);
    if (curr == null) {
      curr = Tables.newCustomTable(
          Maps.>>
              newHashMapWithExpectedSize(2),
          new Supplier>>() {
            @Override
            public Map> get() {
              return Maps.newLinkedHashMap();
            }
          });
      approvals.put(psId, curr);
    }

    if (line.startsWith("-")) {
      String label = line.substring(1);
      if (!curr.contains(accountId, label)) {
        curr.put(accountId, label, Optional. absent());
      }
    } else {
      LabelVote l;
      try {
        l = LabelVote.parseWithEquals(line);
      } catch (IllegalArgumentException e) {
        ConfigInvalidException pe =
            parseException("invalid %s: %s", FOOTER_LABEL, line);
        pe.initCause(e);
        throw pe;
      }
      if (!curr.contains(accountId, l.label())) {
        curr.put(accountId, l.label(), Optional.of(new PatchSetApproval(
            new PatchSetApproval.Key(
                psId,
                accountId,
                new LabelId(l.label())),
            l.value(),
            new Timestamp(commit.getCommitterIdent().getWhen().getTime()))));
      }
    }
  }

  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 = Lists.newArrayList();
        }
        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 = parseIdent(ident);
        } else {
          label.label = line.substring(c + 2);
        }
      }
    }
  }

  private Account.Id parseIdent(RevCommit commit)
      throws ConfigInvalidException {
    return parseIdent(commit.getAuthorIdent());
  }

  private Account.Id parseIdent(PersonIdent ident)
      throws ConfigInvalidException {
    String email = ident.getEmailAddress();
    int at = email.indexOf('@');
    if (at >= 0) {
      String host = email.substring(at + 1, email.length());
      Integer id = Ints.tryParse(email.substring(0, at));
      if (id != null && host.equals(GERRIT_PLACEHOLDER_HOST)) {
        return new Account.Id(id);
      }
    }
    throw parseException("invalid identity, expected @%s: %s",
      GERRIT_PLACEHOLDER_HOST, email);
  }

  private void parseReviewer(ReviewerState state, String line)
      throws ConfigInvalidException {
    PersonIdent ident = RawParseUtils.parsePersonIdent(line);
    if (ident == null) {
      throw invalidFooter(state.getFooterKey(), line);
    }
    Account.Id accountId = parseIdent(ident);
    if (!reviewers.containsKey(accountId)) {
      reviewers.put(accountId, state);
    }
  }

  private void pruneReviewers() {
    Iterator> rit =
        reviewers.entrySet().iterator();
    while (rit.hasNext()) {
      Map.Entry e = rit.next();
      if (e.getValue() == ReviewerState.REMOVED) {
        rit.remove();
        for (Table curr : approvals.values()) {
          curr.rowKeySet().remove(e.getKey());
        }
      }
    }
  }

  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(changeId, fmt, args);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy