com.google.gerrit.server.edit.ChangeEditModifier.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.edit.ChangeEditModifier.java

Source

// 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.edit;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
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.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.treewalk.TreeWalk;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.TimeZone;

/**
 * Utility functions to manipulate change edits.
 * <p>
 * This class contains methods to modify edit's content.
 * For retrieving, publishing and deleting edit see
 * {@link ChangeEditUtil}.
 * <p>
 */
@Singleton
public class ChangeEditModifier {

    private static enum TreeOperation {
        CHANGE_ENTRY, DELETE_ENTRY, RENAME_ENTRY, RESTORE_ENTRY
    }

    private final TimeZone tz;
    private final GitRepositoryManager gitManager;
    private final ChangeIndexer indexer;
    private final Provider<ReviewDb> reviewDb;
    private final Provider<CurrentUser> currentUser;

    @Inject
    ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent, GitRepositoryManager gitManager,
            ChangeIndexer indexer, Provider<ReviewDb> reviewDb, Provider<CurrentUser> currentUser) {
        this.gitManager = gitManager;
        this.indexer = indexer;
        this.reviewDb = reviewDb;
        this.currentUser = currentUser;
        this.tz = gerritIdent.getTimeZone();
    }

    /**
     * Create new change edit.
     *
     * @param change to create change edit for
     * @param ps patch set to create change edit on
     * @return result
     * @throws AuthException
     * @throws IOException
     * @throws ResourceConflictException When change edit already
     * exists for the change
     */
    public RefUpdate.Result createEdit(Change change, PatchSet ps)
            throws AuthException, IOException, ResourceConflictException {
        if (!currentUser.get().isIdentifiedUser()) {
            throw new AuthException("Authentication required");
        }

        IdentifiedUser me = (IdentifiedUser) currentUser.get();
        String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());

        try (Repository repo = gitManager.openRepository(change.getProject())) {
            Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix);
            if (!refs.isEmpty()) {
                throw new ResourceConflictException("edit already exists");
            }

            try (RevWalk rw = new RevWalk(repo)) {
                ObjectId revision = ObjectId.fromString(ps.getRevision().get());
                String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(), ps.getId());
                Result res = update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
                indexer.index(reviewDb.get(), change);
                return res;
            }
        }
    }

    /**
     * Rebase change edit on latest patch set
     *
     * @param edit change edit that contains edit to rebase
     * @param current patch set to rebase the edit on
     * @throws AuthException
     * @throws ResourceConflictException thrown if rebase fails due to merge conflicts
     * @throws InvalidChangeOperationException
     * @throws IOException
     */
    public void rebaseEdit(ChangeEdit edit, PatchSet current)
            throws AuthException, ResourceConflictException, InvalidChangeOperationException, IOException {
        if (!currentUser.get().isIdentifiedUser()) {
            throw new AuthException("Authentication required");
        }

        Change change = edit.getChange();
        IdentifiedUser me = (IdentifiedUser) currentUser.get();
        String refName = RefNames.refsEdit(me.getAccountId(), change.getId(), current.getId());
        try (Repository repo = gitManager.openRepository(change.getProject());
                RevWalk rw = new RevWalk(repo);
                ObjectInserter inserter = repo.newObjectInserter()) {
            BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
            RevCommit editCommit = edit.getEditCommit();
            if (editCommit.getParentCount() == 0) {
                throw new InvalidChangeOperationException("Rebase edit against root commit not supported");
            }
            RevCommit tip = rw.parseCommit(ObjectId.fromString(current.getRevision().get()));
            ThreeWayMerger m = MergeStrategy.RESOLVE.newMerger(repo, true);
            m.setObjectInserter(inserter);
            m.setBase(ObjectId.fromString(edit.getBasePatchSet().getRevision().get()));

            if (m.merge(tip, editCommit)) {
                ObjectId tree = m.getResultTreeId();

                CommitBuilder commit = new CommitBuilder();
                commit.setTreeId(tree);
                for (int i = 0; i < tip.getParentCount(); i++) {
                    commit.addParentId(tip.getParent(i));
                }
                commit.setAuthor(editCommit.getAuthorIdent());
                commit.setCommitter(new PersonIdent(editCommit.getCommitterIdent(), TimeUtil.nowTs()));
                commit.setMessage(editCommit.getFullMessage());
                ObjectId newEdit = inserter.insert(commit);
                inserter.flush();

                ru.addCommand(new ReceiveCommand(ObjectId.zeroId(), newEdit, refName));
                ru.addCommand(
                        new ReceiveCommand(edit.getRef().getObjectId(), ObjectId.zeroId(), edit.getRefName()));
                ru.execute(rw, NullProgressMonitor.INSTANCE);
                for (ReceiveCommand cmd : ru.getCommands()) {
                    if (cmd.getResult() != ReceiveCommand.Result.OK) {
                        throw new IOException("failed: " + cmd);
                    }
                }
            } else {
                // TODO(davido): Allow to resolve conflicts inline
                throw new ResourceConflictException("merge conflict");
            }
        }
    }

    /**
     * Modify commit message in existing change edit.
     *
     * @param edit change edit
     * @param msg new commit message
     * @return result
     * @throws AuthException
     * @throws InvalidChangeOperationException
     * @throws IOException
     * @throws UnchangedCommitMessageException
     */
    public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg)
            throws AuthException, InvalidChangeOperationException, IOException, UnchangedCommitMessageException {
        checkState(!Strings.isNullOrEmpty(msg), "message cannot be null");
        if (!currentUser.get().isIdentifiedUser()) {
            throw new AuthException("Authentication required");
        }

        RevCommit prevEdit = edit.getEditCommit();
        if (prevEdit.getFullMessage().equals(msg)) {
            throw new UnchangedCommitMessageException();
        }

        IdentifiedUser me = (IdentifiedUser) currentUser.get();
        Project.NameKey project = edit.getChange().getProject();
        try (Repository repo = gitManager.openRepository(project);
                RevWalk rw = new RevWalk(repo);
                ObjectInserter inserter = repo.newObjectInserter()) {
            String refName = edit.getRefName();
            ObjectId commit = createCommit(me, inserter, prevEdit, prevEdit.getTree(), msg);
            inserter.flush();
            return update(repo, me, refName, rw, prevEdit, commit);
        }
    }

    /**
     * Modify file in existing change edit from its base commit.
     *
     * @param edit change edit
     * @param file path to modify
     * @param content new content
     * @return result
     * @throws AuthException
     * @throws InvalidChangeOperationException
     * @throws IOException
     */
    public RefUpdate.Result modifyFile(ChangeEdit edit, String file, RawInput content)
            throws AuthException, InvalidChangeOperationException, IOException {
        return modify(TreeOperation.CHANGE_ENTRY, edit, file, null, content);
    }

    /**
     * Delete file in existing change edit.
     *
     * @param edit change edit
     * @param file path to delete
     * @return result
     * @throws AuthException
     * @throws InvalidChangeOperationException
     * @throws IOException
     */
    public RefUpdate.Result deleteFile(ChangeEdit edit, String file)
            throws AuthException, InvalidChangeOperationException, IOException {
        return modify(TreeOperation.DELETE_ENTRY, edit, file, null, null);
    }

    /**
     * Rename file in existing change edit.
     *
     * @param edit change edit
     * @param file path to rename
     * @param newFile path to rename the file to
     * @return result
     * @throws AuthException
     * @throws InvalidChangeOperationException
     * @throws IOException
     */
    public RefUpdate.Result renameFile(ChangeEdit edit, String file, String newFile)
            throws AuthException, InvalidChangeOperationException, IOException {
        return modify(TreeOperation.RENAME_ENTRY, edit, file, newFile, null);
    }

    /**
     * Restore file in existing change edit.
     *
     * @param edit change edit
     * @param file path to restore
     * @return result
     * @throws AuthException
     * @throws InvalidChangeOperationException
     * @throws IOException
     */
    public RefUpdate.Result restoreFile(ChangeEdit edit, String file)
            throws AuthException, InvalidChangeOperationException, IOException {
        return modify(TreeOperation.RESTORE_ENTRY, edit, file, null, null);
    }

    private RefUpdate.Result modify(TreeOperation op, ChangeEdit edit, String file, @Nullable String newFile,
            @Nullable RawInput content) throws AuthException, IOException, InvalidChangeOperationException {
        if (!currentUser.get().isIdentifiedUser()) {
            throw new AuthException("Authentication required");
        }
        IdentifiedUser me = (IdentifiedUser) currentUser.get();
        Project.NameKey project = edit.getChange().getProject();
        try (Repository repo = gitManager.openRepository(project);
                RevWalk rw = new RevWalk(repo);
                ObjectInserter inserter = repo.newObjectInserter();
                ObjectReader reader = repo.newObjectReader()) {
            String refName = edit.getRefName();
            RevCommit prevEdit = edit.getEditCommit();
            ObjectId newTree = writeNewTree(op, rw, inserter, prevEdit, reader, file, newFile,
                    toBlob(inserter, content));
            if (ObjectId.equals(newTree, prevEdit.getTree())) {
                throw new InvalidChangeOperationException("no changes were made");
            }

            ObjectId commit = createCommit(me, inserter, prevEdit, newTree);
            inserter.flush();
            return update(repo, me, refName, rw, prevEdit, commit);
        }
    }

    private static ObjectId toBlob(ObjectInserter ins, @Nullable RawInput content) throws IOException {
        if (content == null) {
            return null;
        }

        long len = content.getContentLength();
        InputStream in = content.getInputStream();
        if (len < 0) {
            return ins.insert(OBJ_BLOB, ByteStreams.toByteArray(in));
        }
        return ins.insert(OBJ_BLOB, len, in);
    }

    private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter, RevCommit revision, ObjectId tree)
            throws IOException {
        return createCommit(me, inserter, revision, tree, revision.getFullMessage());
    }

    private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter, RevCommit revision, ObjectId tree,
            String msg) throws IOException {
        CommitBuilder builder = new CommitBuilder();
        builder.setTreeId(tree);
        builder.setParentIds(revision.getParents());
        builder.setAuthor(revision.getAuthorIdent());
        builder.setCommitter(getCommitterIdent(me));
        builder.setMessage(msg);
        return inserter.insert(builder);
    }

    private RefUpdate.Result update(Repository repo, IdentifiedUser me, String refName, RevWalk rw,
            ObjectId oldObjectId, ObjectId newEdit) throws IOException {
        RefUpdate ru = repo.updateRef(refName);
        ru.setExpectedOldObjectId(oldObjectId);
        ru.setNewObjectId(newEdit);
        ru.setRefLogIdent(getRefLogIdent(me));
        ru.setRefLogMessage("inline edit (amend)", false);
        ru.setForceUpdate(true);
        RefUpdate.Result res = ru.update(rw);
        if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
            throw new IOException("update failed: " + ru);
        }
        return res;
    }

    private static ObjectId writeNewTree(TreeOperation op, RevWalk rw, ObjectInserter ins, RevCommit prevEdit,
            ObjectReader reader, String fileName, @Nullable String newFile, @Nullable final ObjectId content)
            throws IOException {
        DirCache newTree = readTree(reader, prevEdit);
        DirCacheEditor dce = newTree.editor();
        switch (op) {
        case DELETE_ENTRY:
            dce.add(new DeletePath(fileName));
            break;

        case RENAME_ENTRY:
            rw.parseHeaders(prevEdit);
            TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), fileName, prevEdit.getTree());
            if (tw != null) {
                dce.add(new DeletePath(fileName));
                addFileToCommit(newFile, dce, tw);
            }
            break;

        case CHANGE_ENTRY:
            checkNotNull(content, "new content required");
            dce.add(new PathEdit(fileName) {
                @Override
                public void apply(DirCacheEntry ent) {
                    if (ent.getRawMode() == 0) {
                        ent.setFileMode(FileMode.REGULAR_FILE);
                    }
                    ent.setObjectId(content);
                }
            });
            break;

        case RESTORE_ENTRY:
            if (prevEdit.getParentCount() == 0) {
                dce.add(new DeletePath(fileName));
                break;
            }

            RevCommit base = prevEdit.getParent(0);
            rw.parseHeaders(base);
            tw = TreeWalk.forPath(rw.getObjectReader(), fileName, base.getTree());
            if (tw == null) {
                dce.add(new DeletePath(fileName));
                break;
            }

            addFileToCommit(fileName, dce, tw);
            break;
        }
        dce.finish();
        return newTree.writeTree(ins);
    }

    private static void addFileToCommit(String newFile, DirCacheEditor dce, TreeWalk tw) {
        final FileMode mode = tw.getFileMode(0);
        final ObjectId oid = tw.getObjectId(0);
        dce.add(new PathEdit(newFile) {
            @Override
            public void apply(DirCacheEntry ent) {
                ent.setFileMode(mode);
                ent.setObjectId(oid);
            }
        });
    }

    private static DirCache readTree(ObjectReader reader, RevCommit prevEdit) throws IOException {
        DirCache dc = DirCache.newInCore();
        DirCacheBuilder b = dc.builder();
        b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, prevEdit.getTree());
        b.finish();
        return dc;
    }

    private PersonIdent getCommitterIdent(IdentifiedUser user) {
        return user.newCommitterIdent(TimeUtil.nowTs(), tz);
    }

    private PersonIdent getRefLogIdent(IdentifiedUser user) {
        return user.newRefLogIdent(TimeUtil.nowTs(), tz);
    }
}