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

com.google.gerrit.server.edit.ChangeEditModifier Maven / Gradle / Ivy

There is a newer version: 3.11.1
Show newest version
// 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.change.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
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.errors.InvalidObjectIdException;
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.sql.Timestamp;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Utility functions to manipulate change edits.
 * 

* This class contains methods to modify edit's content. * For retrieving, publishing and deleting edit see * {@link ChangeEditUtil}. *

*/ @Singleton public class ChangeEditModifier { private 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; private final Provider currentUser; private final ChangeControl.GenericFactory changeControlFactory; @Inject ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent, GitRepositoryManager gitManager, ChangeIndexer indexer, Provider reviewDb, Provider currentUser, ChangeControl.GenericFactory changeControlFactory) { this.gitManager = gitManager; this.indexer = indexer; this.reviewDb = reviewDb; this.currentUser = currentUser; this.tz = gerritIdent.getTimeZone(); this.changeControlFactory = changeControlFactory; } /** * 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 * @throws OrmException */ public RefUpdate.Result createEdit(Change change, PatchSet ps) throws AuthException, IOException, ResourceConflictException, OrmException { if (!currentUser.get().isIdentifiedUser()) { throw new AuthException("Authentication required"); } IdentifiedUser me = currentUser.get().asIdentifiedUser(); String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId()); try { ChangeControl c = changeControlFactory.controlFor(reviewDb.get(), change, me); if (!c.canAddPatchSet(reviewDb.get())) { return RefUpdate.Result.REJECTED; } } catch (NoSuchChangeException e) { return RefUpdate.Result.NO_CHANGE; } try (Repository repo = gitManager.openRepository(change.getProject())) { Map 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, TimeUtil.nowTs()); 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 = currentUser.get().asIdentifiedUser(); 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 { msg = msg.trim() + "\n"; 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 = currentUser.get().asIdentifiedUser(); 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(); Timestamp now = TimeUtil.nowTs(); ObjectId commit = createCommit(me, inserter, prevEdit, prevEdit.getTree(), msg, now); inserter.flush(); return update(repo, me, refName, rw, prevEdit, commit, now); } } /** * 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 = currentUser.get().asIdentifiedUser(); 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, content); if (ObjectId.equals(newTree, prevEdit.getTree())) { throw new InvalidChangeOperationException("no changes were made"); } Timestamp now = TimeUtil.nowTs(); ObjectId commit = createCommit(me, inserter, prevEdit, newTree, now); inserter.flush(); return update(repo, me, refName, rw, prevEdit, commit, now); } } 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, Timestamp when) throws IOException { return createCommit(me, inserter, revision, tree, revision.getFullMessage(), when); } private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter, RevCommit revision, ObjectId tree, String msg, Timestamp when) throws IOException { CommitBuilder builder = new CommitBuilder(); builder.setTreeId(tree); builder.setParentIds(revision.getParents()); builder.setAuthor(revision.getAuthorIdent()); builder.setCommitter(getCommitterIdent(me, when)); builder.setMessage(msg); return inserter.insert(builder); } private RefUpdate.Result update(Repository repo, IdentifiedUser me, String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit, Timestamp when) throws IOException { RefUpdate ru = repo.updateRef(refName); ru.setExpectedOldObjectId(oldObjectId); ru.setNewObjectId(newEdit); ru.setRefLogIdent(getRefLogIdent(me, when)); 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, final ObjectInserter ins, RevCommit prevEdit, ObjectReader reader, String fileName, @Nullable String newFile, @Nullable final RawInput content) throws InvalidChangeOperationException, 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"); final AtomicReference ioe = new AtomicReference<>(null); final AtomicReference icoe = new AtomicReference<>(null); dce.add(new PathEdit(fileName) { @Override public void apply(DirCacheEntry ent) { try { if (ent.getFileMode() == FileMode.GITLINK) { ent.setLength(0); ent.setLastModified(0); ent.setObjectId(ObjectId.fromString( ByteStreams.toByteArray(content.getInputStream()), 0)); } else { if (ent.getRawMode() == 0) { ent.setFileMode(FileMode.REGULAR_FILE); } ent.setObjectId(toBlob(ins, content)); } } catch (IOException e) { ioe.set(e); } catch (InvalidObjectIdException e) { icoe.set(new InvalidChangeOperationException( "Invalid object id in submodule link: " + e.getMessage())); icoe.get().initCause(e); } } }); if (ioe.get() != null) { throw ioe.get(); } if (icoe.get() != null) { throw icoe.get(); } 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, Timestamp when) { return user.newCommitterIdent(when, tz); } private PersonIdent getRefLogIdent(IdentifiedUser user, Timestamp when) { return user.newRefLogIdent(when, tz); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy