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

com.google.gerrit.server.notedb.CommentsInNotesUtil 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.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.GitDateFormatter;
import org.eclipse.jgit.util.GitDateFormatter.Format;
import org.eclipse.jgit.util.GitDateParser;
import org.eclipse.jgit.util.MutableInteger;
import org.eclipse.jgit.util.QuotedString;
import org.eclipse.jgit.util.RawParseUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * Utility functions to parse PatchLineComments out of a note byte array and
 * store a list of PatchLineComments in the form of a note (in a byte array).
 **/
@Singleton
public class CommentsInNotesUtil {
  private static final String AUTHOR = "Author";
  private static final String BASE_PATCH_SET = "Base-for-patch-set";
  private static final String COMMENT_RANGE = "Comment-range";
  private static final String FILE = "File";
  private static final String LENGTH = "Bytes";
  private static final String PARENT = "Parent";
  private static final String PATCH_SET = "Patch-set";
  private static final String REVISION = "Revision";
  private static final String UUID = "UUID";
  private static final int MAX_NOTE_SZ = 25 << 20;

  public static NoteMap parseCommentsFromNotes(Repository repo, String refName,
      RevWalk walk, Change.Id changeId,
      Multimap commentsForBase,
      Multimap commentsForPs,
      Status status)
      throws IOException, ConfigInvalidException {
    Ref ref = repo.getRef(refName);
    if (ref == null) {
      return null;
    }

    ObjectReader reader = walk.getObjectReader();
    RevCommit commit = walk.parseCommit(ref.getObjectId());
    NoteMap noteMap = NoteMap.read(reader, commit);

    for (Note note: noteMap) {
      byte[] bytes =
          reader.open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
      List result = parseNote(bytes, changeId, status);
      if (result == null || result.isEmpty()) {
        continue;
      }
      PatchSet.Id psId = result.get(0).getKey().getParentKey().getParentKey();
      short side = result.get(0).getSide();
      if (side == 0) {
        commentsForBase.putAll(psId, result);
      } else {
        commentsForPs.putAll(psId, result);
      }
    }
    return noteMap;
  }

  public static List parseNote(byte[] note,
      Change.Id changeId, Status status) throws ConfigInvalidException {
    List result = Lists.newArrayList();
    int sizeOfNote = note.length;
    Charset enc = RawParseUtils.parseEncoding(note);
    MutableInteger curr = new MutableInteger();
    curr.value = 0;

    boolean isForBase =
        (RawParseUtils.match(note, curr.value, PATCH_SET.getBytes(UTF_8))) < 0;

    PatchSet.Id psId = parsePsId(note, curr, changeId, enc,
        isForBase ? BASE_PATCH_SET : PATCH_SET);

    RevId revId =
        new RevId(parseStringField(note, curr, changeId, enc, REVISION));

    PatchLineComment c = null;
    while (curr.value < sizeOfNote) {
      String previousFileName = c == null ?
          null : c.getKey().getParentKey().getFileName();
      c = parseComment(note, curr, previousFileName, psId, revId,
          isForBase, enc, status);
      result.add(c);
    }
    return result;
  }

  public static String formatTime(PersonIdent ident, Timestamp t) {
    GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
    // TODO(dborowitz): Use a ThreadLocal or use Joda.
    PersonIdent newIdent = new PersonIdent(ident, t);
    return dateFormatter.formatDate(newIdent);
  }

  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
    return plc.getKey().getParentKey().getParentKey();
  }

  private static PatchLineComment parseComment(byte[] note, MutableInteger curr,
      String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
      Charset enc, Status status)
          throws ConfigInvalidException {
    Change.Id changeId = psId.getParentKey();

    // Check if there is a new file.
    boolean newFile =
        (RawParseUtils.match(note, curr.value, FILE.getBytes(UTF_8))) != -1;
    if (newFile) {
      // If so, parse the new file name.
      currentFileName = parseFilename(note, curr, changeId, enc);
    } else if (currentFileName == null) {
      throw parseException(changeId, "could not parse %s", FILE);
    }

    CommentRange range = parseCommentRange(note, curr);
    if (range == null) {
      throw parseException(changeId, "could not parse %s", COMMENT_RANGE);
    }

    Timestamp commentTime = parseTimestamp(note, curr, changeId, enc);
    Account.Id aId = parseAuthor(note, curr, changeId, enc);

    boolean hasParent =
        (RawParseUtils.match(note, curr.value, PARENT.getBytes(enc))) != -1;
    String parentUUID = null;
    if (hasParent) {
      parentUUID = parseStringField(note, curr, changeId, enc, PARENT);
    }

    String uuid = parseStringField(note, curr, changeId, enc, UUID);
    int commentLength = parseCommentLength(note, curr, changeId, enc);

    String message = RawParseUtils.decode(
        enc, note, curr.value, curr.value + commentLength);
    checkResult(message, "message contents", changeId);

    PatchLineComment plc = new PatchLineComment(
        new PatchLineComment.Key(new Patch.Key(psId, currentFileName), uuid),
        range.getEndLine(), aId, parentUUID, commentTime);
    plc.setMessage(message);
    plc.setSide((short) (isForBase ? 0 : 1));
    if (range.getStartCharacter() != -1) {
      plc.setRange(range);
    }
    plc.setRevId(revId);
    plc.setStatus(status);

    curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
    curr.value = RawParseUtils.nextLF(note, curr.value);
    return plc;
  }

  private static String parseStringField(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc, String fieldName)
      throws ConfigInvalidException {
    int endOfLine = RawParseUtils.nextLF(note, curr.value);
    checkHeaderLineFormat(note, curr, fieldName, enc, changeId);
    int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
    curr.value = endOfLine;
    return RawParseUtils.decode(enc, note, startOfField, endOfLine - 1);
  }

  /**
   * @return a comment range. If the comment range line in the note only has
   *    one number, we return a CommentRange with that one number as the end
   *    line and the other fields as -1. If the comment range line in the note
   *    contains a whole comment range, then we return a CommentRange with all
   *    fields set. If the line is not correctly formatted, return null.
   */
  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
    CommentRange range = new CommentRange(-1, -1, -1, -1);

    int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
    if (startLine == 0) {
      range.setEndLine(0);
      ptr.value += 1;
      return range;
    }

    if (note[ptr.value] == '\n') {
      range.setEndLine(startLine);
      ptr.value += 1;
      return range;
    } else if (note[ptr.value] == ':') {
      range.setStartLine(startLine);
      ptr.value += 1;
    } else {
      return null;
    }

    int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
    if (startChar == 0) {
      return null;
    }
    if (note[ptr.value] == '-') {
      range.setStartCharacter(startChar);
      ptr.value += 1;
    } else {
      return null;
    }

    int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
    if (endLine == 0) {
      return null;
    }
    if (note[ptr.value] == ':') {
      range.setEndLine(endLine);
      ptr.value += 1;
    } else {
      return null;
    }

    int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
    if (endChar == 0) {
      return null;
    }
    if (note[ptr.value] == '\n') {
      range.setEndCharacter(endChar);
      ptr.value += 1;
    } else {
      return null;
    }
    return range;
  }

  private static PatchSet.Id parsePsId(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc, String fieldName)
      throws ConfigInvalidException {
    checkHeaderLineFormat(note, curr, fieldName, enc, changeId);
    int startOfPsId =
        RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
    MutableInteger i = new MutableInteger();
    int patchSetId =
        RawParseUtils.parseBase10(note, startOfPsId, i);
    int endOfLine = RawParseUtils.nextLF(note, curr.value);
    if (i.value != endOfLine - 1) {
      throw parseException(changeId, "could not parse %s", fieldName);
    }
    checkResult(patchSetId, "patchset id", changeId);
    curr.value = endOfLine;
    return new PatchSet.Id(changeId, patchSetId);
  }

  private static String parseFilename(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc) throws ConfigInvalidException {
    checkHeaderLineFormat(note, curr, FILE, enc, changeId);
    int startOfFileName =
        RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
    int endOfLine = RawParseUtils.nextLF(note, curr.value);
    curr.value = endOfLine;
    curr.value = RawParseUtils.nextLF(note, curr.value);
    return QuotedString.GIT_PATH.dequote(
        RawParseUtils.decode(enc, note, startOfFileName, endOfLine - 1));
  }

  private static Timestamp parseTimestamp(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc)
      throws ConfigInvalidException {
    int endOfLine = RawParseUtils.nextLF(note, curr.value);
    Timestamp commentTime;
    String dateString =
        RawParseUtils.decode(enc, note, curr.value, endOfLine - 1);
    try {
      commentTime =
          new Timestamp(GitDateParser.parse(dateString, null).getTime());
    } catch (ParseException e) {
      throw new ConfigInvalidException("could not parse comment timestamp", e);
    }
    curr.value = endOfLine;
    return checkResult(commentTime, "comment timestamp", changeId);
  }

  private static Account.Id parseAuthor(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc) throws ConfigInvalidException {
    checkHeaderLineFormat(note, curr, AUTHOR, enc, changeId);
    int startOfAccountId =
        RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
    PersonIdent ident =
        RawParseUtils.parsePersonIdent(note, startOfAccountId);
    Account.Id aId = parseIdent(ident, changeId);
    curr.value = RawParseUtils.nextLF(note, curr.value);
    return checkResult(aId, "comment author", changeId);
  }

  private static int parseCommentLength(byte[] note, MutableInteger curr,
      Change.Id changeId, Charset enc) throws ConfigInvalidException {
    checkHeaderLineFormat(note, curr, LENGTH, enc, changeId);
    int startOfLength =
        RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
    MutableInteger i = new MutableInteger();
    int commentLength =
        RawParseUtils.parseBase10(note, startOfLength, i);
    int endOfLine = RawParseUtils.nextLF(note, curr.value);
    if (i.value != endOfLine-1) {
      throw parseException(changeId, "could not parse %s", PATCH_SET);
    }
    curr.value = endOfLine;
    return checkResult(commentLength, "comment length", changeId);
  }

  private static  T checkResult(T o, String fieldName,
      Change.Id changeId) throws ConfigInvalidException {
    if (o == null) {
      throw parseException(changeId, "could not parse %s", fieldName);
    }
    return o;
  }

  private static int checkResult(int i, String fieldName, Change.Id changeId)
      throws ConfigInvalidException {
    if (i <= 0) {
      throw parseException(changeId, "could not parse %s", fieldName);
    }
    return i;
  }

  private PersonIdent newIdent(Account author, Date when) {
    return new PersonIdent(
        new AccountInfo(author).getName(anonymousCowardName),
        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
        when, serverIdent.getTimeZone());
  }

  private static Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
      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(changeId, "invalid identity, expected @%s: %s",
      GERRIT_PLACEHOLDER_HOST, email);
  }

  private void appendHeaderField(PrintWriter writer,
      String field, String value) {
    writer.print(field);
    writer.print(": ");
    writer.print(value);
    writer.print('\n');
  }

  private static void checkHeaderLineFormat(byte[] note, MutableInteger curr,
      String fieldName, Charset enc, Change.Id changeId)
      throws ConfigInvalidException {
    boolean correct =
        RawParseUtils.match(note, curr.value, fieldName.getBytes(enc)) != -1;
    correct &= (note[curr.value + fieldName.length()] == ':');
    correct &= (note[curr.value + fieldName.length() + 1] == ' ');
    if (!correct) {
      throw parseException(changeId, "could not parse %s", fieldName);
    }
  }

  private final AccountCache accountCache;
  private final PersonIdent serverIdent;
  private final String anonymousCowardName;

  @Inject
  public CommentsInNotesUtil(AccountCache accountCache,
      @GerritPersonIdent PersonIdent serverIdent,
      @AnonymousCowardName String anonymousCowardName) {
    this.accountCache = accountCache;
    this.serverIdent = serverIdent;
    this.anonymousCowardName = anonymousCowardName;
  }

  /**
   * Build a note that contains the metadata for and the contents of all of the
   * comments in the given list of comments.
   *
   * @param comments
   *            A list of the comments to be written to the returned note
   *            byte array.
   *            All of the comments in this list must have the same side and
   *            must share the same PatchSet.Id.
   *            This list must not be empty because we cannot build a note
   *            for no comments.
   * @return the note. Null if there are no comments in the list.
   */
  public byte[] buildNote(List comments) {
    ByteArrayOutputStream buf = new ByteArrayOutputStream();
    OutputStreamWriter streamWriter = new OutputStreamWriter(buf, UTF_8);
    PrintWriter writer = new PrintWriter(streamWriter);
    PatchLineComment first = comments.get(0);

    short side = first.getSide();
    PatchSet.Id psId = getCommentPsId(first);
    appendHeaderField(writer, side == 0
        ? BASE_PATCH_SET
        : PATCH_SET,
        Integer.toString(psId.get()));
    appendHeaderField(writer, REVISION, first.getRevId().get());

    String currentFilename = null;

    for (PatchLineComment c : comments) {
      PatchSet.Id currentPsId = getCommentPsId(c);
      checkArgument(psId.equals(currentPsId),
          "All comments being added must all have the same PatchSet.Id. The"
          + "comment below does not have the same PatchSet.Id as the others "
          + "(%s).\n%s", psId.toString(), c.toString());
      checkArgument(side == c.getSide(),
          "All comments being added must all have the same side. The"
          + "comment below does not have the same side as the others "
          + "(%s).\n%s", side, c.toString());
      String commentFilename =
          QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());

      if (!commentFilename.equals(currentFilename)) {
        currentFilename = commentFilename;
        writer.print("File: ");
        writer.print(commentFilename);
        writer.print("\n\n");
      }

      // The CommentRange field for a comment is allowed to be null.
      // If it is indeed null, then in the first line, we simply use the line
      // number field for a comment instead. If it isn't null, we write the
      // comment range itself.
      CommentRange range = c.getRange();
      if (range != null) {
        writer.print(range.getStartLine());
        writer.print(':');
        writer.print(range.getStartCharacter());
        writer.print('-');
        writer.print(range.getEndLine());
        writer.print(':');
        writer.print(range.getEndCharacter());
      } else {
        writer.print(c.getLine());
      }
      writer.print("\n");

      writer.print(formatTime(serverIdent, c.getWrittenOn()));
      writer.print("\n");

      PersonIdent ident =
          newIdent(accountCache.get(c.getAuthor()).getAccount(),
              c.getWrittenOn());
      String nameString = ident.getName() + " <" + ident.getEmailAddress()
          + ">";
      appendHeaderField(writer, AUTHOR, nameString);

      String parent = c.getParentUuid();
      if (parent != null) {
        appendHeaderField(writer, PARENT, parent);
      }

      appendHeaderField(writer, UUID, c.getKey().get());

      byte[] messageBytes = c.getMessage().getBytes(UTF_8);
      appendHeaderField(writer, LENGTH,
          Integer.toString(messageBytes.length));

      writer.print(c.getMessage());
      writer.print("\n\n");
    }
    writer.close();
    return buf.toByteArray();
  }

  public void writeCommentsToNoteMap(NoteMap noteMap,
      List allComments, ObjectInserter inserter)
        throws IOException {
    checkArgument(!allComments.isEmpty(),
        "No comments to write; to delete, use removeNoteFromNoteMap().");
    ObjectId commit =
        ObjectId.fromString(allComments.get(0).getRevId().get());
    Collections.sort(allComments, ChangeNotes.PLC_ORDER);
    noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(allComments)));
  }

  public void removeNote(NoteMap noteMap, RevId commitId)
      throws IOException {
    noteMap.remove(ObjectId.fromString(commitId.get()));
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy