com.google.gerrit.server.change.Submit.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.change.Submit.java

Source

// Copyright (C) 2012 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.change;

import static com.google.gerrit.common.data.SubmitRecord.Status.OK;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.change.Submit.Input;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.errors.RepositoryNotFoundException;

import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class Submit implements RestModifyView<RevisionResource, Input> {
    public static class Input {
        public boolean waitForMerge;
    }

    public enum Status {
        SUBMITTED, MERGED;
    }

    public static class Output {
        public Status status;
        transient Change change;

        private Output(Status s, Change c) {
            status = s;
            change = c;
        }
    }

    private final Provider<ReviewDb> dbProvider;
    private final GitRepositoryManager repoManager;
    private final MergeQueue mergeQueue;
    private final ChangeIndexer indexer;

    @Inject
    Submit(Provider<ReviewDb> dbProvider, GitRepositoryManager repoManager, MergeQueue mergeQueue,
            ChangeIndexer indexer) {
        this.dbProvider = dbProvider;
        this.repoManager = repoManager;
        this.mergeQueue = mergeQueue;
        this.indexer = indexer;
    }

    @Override
    public Output apply(RevisionResource rsrc, Input input) throws AuthException, ResourceConflictException,
            RepositoryNotFoundException, IOException, OrmException {
        ChangeControl control = rsrc.getControl();
        IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
        Change change = rsrc.getChange();
        if (!control.canSubmit()) {
            throw new AuthException("submit not permitted");
        } else if (!change.getStatus().isOpen()) {
            throw new ResourceConflictException("change is " + status(change));
        } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
            throw new ResourceConflictException(
                    String.format("destination branch \"%s\" not found.", change.getDest().get()));
        } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) {
            // TODO Allow submitting non-current revision by changing the current.
            throw new ResourceConflictException(
                    String.format("revision %s is not current revision", rsrc.getPatchSet().getRevision().get()));
        }

        checkSubmitRule(rsrc);
        change = submit(rsrc, caller);
        if (change == null) {
            throw new ResourceConflictException(
                    "change is " + status(dbProvider.get().changes().get(rsrc.getChange().getId())));
        }

        if (input.waitForMerge) {
            mergeQueue.merge(change.getDest());
            change = dbProvider.get().changes().get(change.getId());
        } else {
            mergeQueue.schedule(change.getDest());
        }

        if (change == null) {
            throw new ResourceConflictException("change is deleted");
        }
        switch (change.getStatus()) {
        case SUBMITTED:
            return new Output(Status.SUBMITTED, change);
        case MERGED:
            return new Output(Status.MERGED, change);
        case NEW:
            ChangeMessage msg = getConflictMessage(rsrc);
            if (msg != null) {
                throw new ResourceConflictException(msg.getMessage());
            }
        default:
            throw new ResourceConflictException("change is " + status(change));
        }
    }

    /**
     * If the merge was attempted and it failed the system usually writes a
     * comment as a ChangeMessage and sets status to NEW. Find the relevant
     * message and return it.
     */
    public ChangeMessage getConflictMessage(RevisionResource rsrc) throws OrmException {
        final Timestamp before = rsrc.getChange().getLastUpdatedOn();
        ChangeMessage msg = Iterables.getFirst(Iterables.filter(
                Lists.reverse(dbProvider.get().changeMessages().byChange(rsrc.getChange().getId()).toList()),
                new Predicate<ChangeMessage>() {
                    @Override
                    public boolean apply(ChangeMessage input) {
                        return input.getAuthor() == null && input.getWrittenOn().getTime() >= before.getTime();
                    }
                }), null);
        return msg;
    }

    public Change submit(RevisionResource rsrc, IdentifiedUser caller) throws OrmException {
        final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        Change change = rsrc.getChange();
        ReviewDb db = dbProvider.get();
        db.changes().beginTransaction(change.getId());
        try {
            approve(rsrc.getPatchSet(), caller, timestamp);
            change = db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
                @Override
                public Change update(Change change) {
                    if (change.getStatus().isOpen()) {
                        change.setStatus(Change.Status.SUBMITTED);
                        change.setLastUpdatedOn(timestamp);
                        ChangeUtil.computeSortKey(change);
                        return change;
                    }
                    return null;
                }
            });
            if (change == null) {
                return null;
            }
            db.commit();
        } finally {
            db.rollback();
        }
        indexer.index(change);
        return change;
    }

    private void approve(PatchSet rev, IdentifiedUser caller, Timestamp timestamp) throws OrmException {
        PatchSetApproval submit = Iterables.getFirst(Iterables.filter(
                dbProvider.get().patchSetApprovals().byPatchSetUser(rev.getId(), caller.getAccountId()),
                new Predicate<PatchSetApproval>() {
                    @Override
                    public boolean apply(PatchSetApproval input) {
                        return input.isSubmit();
                    }
                }), null);
        if (submit == null) {
            submit = new PatchSetApproval(
                    new PatchSetApproval.Key(rev.getId(), caller.getAccountId(), LabelId.SUBMIT), (short) 1);
        }
        submit.setValue((short) 1);
        submit.setGranted(timestamp);
        dbProvider.get().patchSetApprovals().upsert(Collections.singleton(submit));
    }

    private void checkSubmitRule(RevisionResource rsrc) throws ResourceConflictException {
        List<SubmitRecord> results = rsrc.getControl().canSubmit(dbProvider.get(), rsrc.getPatchSet());
        Optional<SubmitRecord> ok = findOkRecord(results);
        if (ok.isPresent()) {
            // Rules supplied a valid solution.
            return;
        } else if (results.isEmpty()) {
            throw new IllegalStateException(
                    String.format("ChangeControl.canSubmit returned empty list for %s in %s",
                            rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get()));
        }

        for (SubmitRecord record : results) {
            switch (record.status) {
            case CLOSED:
                throw new ResourceConflictException("change is closed");

            case RULE_ERROR:
                throw new ResourceConflictException(String.format("rule error: %s", record.errorMessage));

            case NOT_READY:
                StringBuilder msg = new StringBuilder();
                for (SubmitRecord.Label lbl : record.labels) {
                    switch (lbl.status) {
                    case OK:
                    case MAY:
                        continue;

                    case REJECT:
                        if (msg.length() > 0)
                            msg.append("; ");
                        msg.append("blocked by " + lbl.label);
                        continue;

                    case NEED:
                        if (msg.length() > 0)
                            msg.append("; ");
                        msg.append("needs " + lbl.label);
                        continue;

                    case IMPOSSIBLE:
                        if (msg.length() > 0)
                            msg.append("; ");
                        msg.append("needs " + lbl.label + " (check project access)");
                        continue;

                    default:
                        throw new IllegalStateException(
                                String.format("Unsupported SubmitRecord.Label %s for %s in %s", lbl.toString(),
                                        rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get()));
                    }
                }
                throw new ResourceConflictException(msg.toString());

            default:
                throw new IllegalStateException(String.format("Unsupported SubmitRecord %s for %s in %s", record,
                        rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get()));
            }
        }
    }

    private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
        return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
            @Override
            public boolean apply(SubmitRecord input) {
                return input.status == OK;
            }
        });
    }

    private static String status(Change change) {
        return change != null ? change.getStatus().name().toLowerCase() : "deleted";
    }

    public static class CurrentRevision implements RestModifyView<ChangeResource, Input> {
        private final Provider<ReviewDb> dbProvider;
        private final Submit submit;
        private final ChangeJson json;

        @Inject
        CurrentRevision(Provider<ReviewDb> dbProvider, Submit submit, ChangeJson json) {
            this.dbProvider = dbProvider;
            this.submit = submit;
            this.json = json;
        }

        @Override
        public Object apply(ChangeResource rsrc, Input input) throws AuthException, ResourceConflictException,
                RepositoryNotFoundException, IOException, OrmException {
            PatchSet ps = dbProvider.get().patchSets().get(rsrc.getChange().currentPatchSetId());
            if (ps == null) {
                throw new ResourceConflictException("current revision is missing");
            } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
                throw new AuthException("current revision not accessible");
            }
            Output out = submit.apply(new RevisionResource(rsrc, ps), input);
            return json.format(out.change);
        }
    }
}