Java tutorial
// 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); } }