com.google.gerrit.server.notedb.ChangeUpdate.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.notedb.ChangeUpdate.java

Source

// Copyright (C) 2013 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.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.CommentsInNotesUtil.addCommentToMap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
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.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.Project;
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.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.LabelVote;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;

import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A delta to apply to a change.
 * <p>
 * This delta will become two unique commits: one in the AllUsers repo that will
 * contain the draft comments on this change and one in the notes branch that
 * will contain approvals, reviewers, change status, subject, submit records,
 * the change message, and published comments. There are limitations on the set
 * of modifications that can be handled in a single update. In particular, there
 * is a single author and timestamp for each update.
 * <p>
 * This class is not thread-safe.
 */
public class ChangeUpdate extends AbstractChangeUpdate {
    public interface Factory {
        ChangeUpdate create(ChangeControl ctl);

        ChangeUpdate create(ChangeControl ctl, Date when);

        @VisibleForTesting
        ChangeUpdate create(ChangeControl ctl, Date when, Comparator<String> labelNameComparator);
    }

    private final AccountCache accountCache;
    private final Map<String, Optional<Short>> approvals;
    private final Map<Account.Id, ReviewerState> reviewers;
    private Change.Status status;
    private String subject;
    private List<SubmitRecord> submitRecords;
    private final CommentsInNotesUtil commentsUtil;
    private List<PatchLineComment> comments;
    private Set<String> hashtags;
    private String changeMessage;
    private ChangeNotes notes;

    private final ChangeDraftUpdate.Factory draftUpdateFactory;
    private ChangeDraftUpdate draftUpdate;

    @AssistedInject
    private ChangeUpdate(@GerritPersonIdent PersonIdent serverIdent,
            @AnonymousCowardName String anonymousCowardName, GitRepositoryManager repoManager,
            NotesMigration migration, AccountCache accountCache, MetaDataUpdate.User updateFactory,
            ChangeDraftUpdate.Factory draftUpdateFactory, ProjectCache projectCache, @Assisted ChangeControl ctl,
            CommentsInNotesUtil commentsUtil) {
        this(serverIdent, anonymousCowardName, repoManager, migration, accountCache, updateFactory,
                draftUpdateFactory, projectCache, ctl, serverIdent.getWhen(), commentsUtil);
    }

    @AssistedInject
    private ChangeUpdate(@GerritPersonIdent PersonIdent serverIdent,
            @AnonymousCowardName String anonymousCowardName, GitRepositoryManager repoManager,
            NotesMigration migration, AccountCache accountCache, MetaDataUpdate.User updateFactory,
            ChangeDraftUpdate.Factory draftUpdateFactory, ProjectCache projectCache, @Assisted ChangeControl ctl,
            @Assisted Date when, CommentsInNotesUtil commentsUtil) {
        this(serverIdent, anonymousCowardName, repoManager, migration, accountCache, updateFactory,
                draftUpdateFactory, ctl, when,
                projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(), commentsUtil);
    }

    private static Project.NameKey getProjectName(ChangeControl ctl) {
        return ctl.getChange().getDest().getParentKey();
    }

    @AssistedInject
    private ChangeUpdate(@GerritPersonIdent PersonIdent serverIdent,
            @AnonymousCowardName String anonymousCowardName, GitRepositoryManager repoManager,
            NotesMigration migration, AccountCache accountCache, MetaDataUpdate.User updateFactory,
            ChangeDraftUpdate.Factory draftUpdateFactory, @Assisted ChangeControl ctl, @Assisted Date when,
            @Assisted Comparator<String> labelNameComparator, CommentsInNotesUtil commentsUtil) {
        super(migration, repoManager, updateFactory, ctl, serverIdent, anonymousCowardName, when);
        this.draftUpdateFactory = draftUpdateFactory;
        this.accountCache = accountCache;
        this.commentsUtil = commentsUtil;
        this.approvals = Maps.newTreeMap(labelNameComparator);
        this.reviewers = Maps.newLinkedHashMap();
        this.comments = Lists.newArrayList();
    }

    public void setStatus(Change.Status status) {
        checkArgument(status != Change.Status.MERGED, "use submit(Iterable<PatchSetApproval>)");
        this.status = status;
    }

    public void putApproval(String label, short value) {
        approvals.put(label, Optional.of(value));
    }

    public void removeApproval(String label) {
        approvals.put(label, Optional.<Short>absent());
    }

    public void merge(Iterable<SubmitRecord> submitRecords) {
        this.status = Change.Status.MERGED;
        this.submitRecords = ImmutableList.copyOf(submitRecords);
        checkArgument(!this.submitRecords.isEmpty(), "no submit records specified at submit time");
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public void setChangeMessage(String changeMessage) {
        this.changeMessage = changeMessage;
    }

    public void insertComment(PatchLineComment comment) throws OrmException {
        if (comment.getStatus() == Status.DRAFT) {
            insertDraftComment(comment);
        } else {
            insertPublishedComment(comment);
        }
    }

    public void upsertComment(PatchLineComment comment) throws OrmException {
        if (comment.getStatus() == Status.DRAFT) {
            upsertDraftComment(comment);
        } else {
            deleteDraftCommentIfPresent(comment);
            upsertPublishedComment(comment);
        }
    }

    public void updateComment(PatchLineComment comment) throws OrmException {
        if (comment.getStatus() == Status.DRAFT) {
            updateDraftComment(comment);
        } else {
            deleteDraftCommentIfPresent(comment);
            updatePublishedComment(comment);
        }
    }

    public void deleteComment(PatchLineComment comment) throws OrmException {
        if (comment.getStatus() == Status.DRAFT) {
            deleteDraftComment(comment);
        } else {
            throw new IllegalArgumentException("Cannot delete a published comment.");
        }
    }

    private void insertPublishedComment(PatchLineComment c) throws OrmException {
        verifyComment(c);
        if (notes == null) {
            notes = getChangeNotes().load();
        }
        if (migration.readChanges()) {
            checkArgument(!notes.containsComment(c),
                    "A comment already exists with the same key as the following comment,"
                            + " so we cannot insert this comment: %s",
                    c);
        }
        comments.add(c);
    }

    private void insertDraftComment(PatchLineComment c) throws OrmException {
        createDraftUpdateIfNull();
        draftUpdate.insertComment(c);
    }

    private void upsertPublishedComment(PatchLineComment c) throws OrmException {
        verifyComment(c);
        if (notes == null) {
            notes = getChangeNotes().load();
        }
        // This could allow callers to update a published comment if migration.write
        // is on and migration.readComments is off because we will not be able to
        // verify that the comment didn't already exist as a published comment
        // since we don't have a ReviewDb.
        if (migration.readChanges()) {
            checkArgument(!notes.containsCommentPublished(c),
                    "Cannot update a comment that has already been published and saved");
        }
        comments.add(c);
    }

    private void upsertDraftComment(PatchLineComment c) {
        createDraftUpdateIfNull();
        draftUpdate.upsertComment(c);
    }

    private void updatePublishedComment(PatchLineComment c) throws OrmException {
        verifyComment(c);
        if (notes == null) {
            notes = getChangeNotes().load();
        }
        // See comment above in upsertPublishedComment() about potential risk with
        // this check.
        if (migration.readChanges()) {
            checkArgument(!notes.containsCommentPublished(c),
                    "Cannot update a comment that has already been published and saved");
        }
        comments.add(c);
    }

    private void updateDraftComment(PatchLineComment c) throws OrmException {
        createDraftUpdateIfNull();
        draftUpdate.updateComment(c);
    }

    private void deleteDraftComment(PatchLineComment c) throws OrmException {
        createDraftUpdateIfNull();
        draftUpdate.deleteComment(c);
    }

    private void deleteDraftCommentIfPresent(PatchLineComment c) throws OrmException {
        createDraftUpdateIfNull();
        draftUpdate.deleteCommentIfPresent(c);
    }

    private void createDraftUpdateIfNull() {
        if (draftUpdate == null) {
            draftUpdate = draftUpdateFactory.create(ctl, when);
        }
    }

    private void verifyComment(PatchLineComment c) {
        checkArgument(c.getRevId() != null);
        checkArgument(c.getStatus() == Status.PUBLISHED,
                "Cannot add a draft comment to a ChangeUpdate. Use a ChangeDraftUpdate" + " for draft comments");
        checkArgument(c.getAuthor().equals(getUser().getAccountId()),
                "The author for the following comment does not match the author of"
                        + " this ChangeDraftUpdate (%s): %s",
                getUser().getAccountId(), c);

    }

    public void setHashtags(Set<String> hashtags) {
        this.hashtags = hashtags;
    }

    public void putReviewer(Account.Id reviewer, ReviewerState type) {
        checkArgument(type != ReviewerState.REMOVED, "invalid ReviewerType");
        reviewers.put(reviewer, type);
    }

    public void removeReviewer(Account.Id reviewer) {
        reviewers.put(reviewer, ReviewerState.REMOVED);
    }

    /** @return the tree id for the updated tree */
    private ObjectId storeCommentsInNotes() throws OrmException, IOException {
        ChangeNotes notes = ctl.getNotes().load();
        NoteMap noteMap = notes.getNoteMap();
        if (noteMap == null) {
            noteMap = NoteMap.newEmptyMap();
        }
        if (comments.isEmpty()) {
            return null;
        }

        Map<RevId, List<PatchLineComment>> allComments = Maps.newHashMap();
        for (Map.Entry<RevId, Collection<PatchLineComment>> e : notes.getComments().asMap().entrySet()) {
            List<PatchLineComment> comments = new ArrayList<>();
            for (PatchLineComment c : e.getValue()) {
                comments.add(c);
            }
            allComments.put(e.getKey(), comments);
        }
        for (PatchLineComment c : comments) {
            addCommentToMap(allComments, c);
        }
        commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter);
        return noteMap.writeTree(inserter);
    }

    public RevCommit commit() throws IOException {
        BatchMetaDataUpdate batch = openUpdate();
        try {
            writeCommit(batch);
            if (draftUpdate != null) {
                draftUpdate.commit();
            }
            RevCommit c = batch.commit();
            return c;
        } catch (OrmException e) {
            throw new IOException(e);
        } finally {
            batch.close();
        }
    }

    @Override
    public void writeCommit(BatchMetaDataUpdate batch) throws OrmException, IOException {
        CommitBuilder builder = new CommitBuilder();
        if (migration.writeChanges()) {
            ObjectId treeId = storeCommentsInNotes();
            if (treeId != null) {
                builder.setTreeId(treeId);
            }
        }
        batch.write(this, builder);
    }

    @Override
    protected String getRefName() {
        return ChangeNoteUtil.changeRefName(getChange().getId());
    }

    @Override
    protected boolean onSave(CommitBuilder commit) {
        if (isEmpty()) {
            return false;
        }
        commit.setAuthor(newIdent(getUser().getAccount(), when));
        commit.setCommitter(new PersonIdent(serverIdent, when));

        int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get();
        StringBuilder msg = new StringBuilder();
        if (subject != null) {
            msg.append(subject);
        } else {
            msg.append("Update patch set ").append(ps);
        }
        msg.append("\n\n");

        if (changeMessage != null) {
            msg.append(changeMessage);
            msg.append("\n\n");
        }

        addFooter(msg, FOOTER_PATCH_SET, ps);
        if (status != null) {
            addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
        }

        if (hashtags != null) {
            addFooter(msg, FOOTER_HASHTAGS, Joiner.on(",").join(hashtags));
        }

        for (Map.Entry<Account.Id, ReviewerState> e : reviewers.entrySet()) {
            Account account = accountCache.get(e.getKey()).getAccount();
            PersonIdent ident = newIdent(account, when);
            addFooter(msg, e.getValue().getFooterKey()).append(ident.getName()).append(" <")
                    .append(ident.getEmailAddress()).append(">\n");
        }

        for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
            if (!e.getValue().isPresent()) {
                addFooter(msg, FOOTER_LABEL, '-', e.getKey());
            } else {
                addFooter(msg, FOOTER_LABEL, LabelVote.create(e.getKey(), e.getValue().get()).formatWithEquals());
            }
        }

        if (submitRecords != null) {
            for (SubmitRecord rec : submitRecords) {
                addFooter(msg, FOOTER_SUBMITTED_WITH).append(rec.status);
                if (rec.errorMessage != null) {
                    msg.append(' ').append(sanitizeFooter(rec.errorMessage));
                }
                msg.append('\n');

                if (rec.labels != null) {
                    for (SubmitRecord.Label label : rec.labels) {
                        addFooter(msg, FOOTER_SUBMITTED_WITH).append(label.status).append(": ").append(label.label);
                        if (label.appliedBy != null) {
                            PersonIdent ident = newIdent(accountCache.get(label.appliedBy).getAccount(), when);
                            msg.append(": ").append(ident.getName()).append(" <").append(ident.getEmailAddress())
                                    .append('>');
                        }
                        msg.append('\n');
                    }
                }
            }
        }

        commit.setMessage(msg.toString());
        return true;
    }

    @Override
    protected Project.NameKey getProjectName() {
        return getProjectName(ctl);
    }

    private boolean isEmpty() {
        return approvals.isEmpty() && changeMessage == null && comments.isEmpty() && reviewers.isEmpty()
                && status == null && subject == null && submitRecords == null && hashtags == null;
    }

    private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
        return sb.append(footer.getName()).append(": ");
    }

    private static void addFooter(StringBuilder sb, FooterKey footer, Object... values) {
        addFooter(sb, footer);
        for (Object value : values) {
            sb.append(value);
        }
        sb.append('\n');
    }

    private static String sanitizeFooter(String value) {
        return value.replace('\n', ' ').replace('\0', ' ');
    }
}