Merge topic 'inline-3'

* changes:
  InlineEdit: Add change edit collection and resource
  InlineEdit: Add acceptance test for edit operations
  InlineEdit: Factor out get content method for file resource
  InlineEdit: Add utility class to modify edits
This commit is contained in:
Dave Borowitz
2014-08-08 23:48:20 +00:00
committed by Gerrit Code Review
10 changed files with 898 additions and 49 deletions

View File

@@ -0,0 +1,67 @@
// 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.change;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
public class ChangeEditResource implements RestResource {
public static final TypeLiteral<RestView<ChangeEditResource>> CHANGE_EDIT_KIND =
new TypeLiteral<RestView<ChangeEditResource>>() {};
private final ChangeResource change;
private final ChangeEdit edit;
public ChangeEditResource(ChangeResource change, ChangeEdit edit) {
this.change = change;
this.edit = edit;
}
// TODO(davido): Make this cacheable.
// Should just depend on the SHA-1 of the edit itself.
public boolean isCacheable() {
return false;
}
public ChangeResource getChangeResource() {
return change;
}
public ChangeControl getControl() {
return getChangeResource().getControl();
}
public Change getChange() {
return edit.getChange();
}
public ChangeEdit getChangeEdit() {
return edit;
}
Account.Id getAccountId() {
return getUser().getAccountId();
}
IdentifiedUser getUser() {
return edit.getUser();
}
}

View File

@@ -0,0 +1,48 @@
// 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.change;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
class ChangeEdits implements
ChildCollection<ChangeResource, ChangeEditResource> {
private final DynamicMap<RestView<ChangeEditResource>> views;
@Inject
ChangeEdits(DynamicMap<RestView<ChangeEditResource>> views) {
this.views = views;
}
@Override
public DynamicMap<RestView<ChangeEditResource>> views() {
return views;
}
@Override
public RestView<ChangeResource> list() {
throw new IllegalStateException("not yet implemented");
}
@Override
public ChangeEditResource parse(ChangeResource change, IdString id) {
throw new IllegalStateException("not yet implemented");
}
}

View File

@@ -0,0 +1,71 @@
// 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.change;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import java.io.IOException;
import java.io.OutputStream;
@Singleton
public class FileContentUtil {
private final GitRepositoryManager repoManager;
@Inject
FileContentUtil(GitRepositoryManager repoManager) {
this.repoManager = repoManager;
}
public BinaryResult getContent(Project.NameKey project, String revstr,
String path) throws ResourceNotFoundException, IOException {
Repository repo = repoManager.openRepository(project);
try {
RevWalk rw = new RevWalk(repo);
try {
RevCommit commit = rw.parseCommit(repo.resolve(revstr));
TreeWalk tw =
TreeWalk.forPath(rw.getObjectReader(), path,
commit.getTree().getId());
if (tw == null) {
throw new ResourceNotFoundException();
}
final ObjectLoader object = repo.open(tw.getObjectId(0));
@SuppressWarnings("resource")
BinaryResult result = new BinaryResult() {
@Override
public void writeTo(OutputStream os) throws IOException {
object.copyTo(os);
}
};
return result.setContentLength(object.getSize()).base64();
} finally {
rw.release();
}
} finally {
repo.close();
}
}
}

View File

@@ -17,65 +17,26 @@ package com.google.gerrit.server.change;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import java.io.IOException;
import java.io.OutputStream;
@Singleton
public class GetContent implements RestReadView<FileResource> {
private final GitRepositoryManager repoManager;
private final FileContentUtil fileContentUtil;
@Inject
GetContent(GitRepositoryManager repoManager) {
this.repoManager = repoManager;
GetContent(FileContentUtil fileContentUtil) {
this.fileContentUtil = fileContentUtil;
}
@Override
public BinaryResult apply(FileResource rsrc)
throws ResourceNotFoundException, IOException {
return apply(rsrc.getRevision().getControl().getProject().getNameKey(),
return fileContentUtil.getContent(
rsrc.getRevision().getControl().getProject().getNameKey(),
rsrc.getRevision().getPatchSet().getRevision().get(),
rsrc.getPatchKey().get());
}
public BinaryResult apply(Project.NameKey project, String revstr, String path)
throws ResourceNotFoundException, IOException {
Repository repo = repoManager.openRepository(project);
try {
RevWalk rw = new RevWalk(repo);
try {
RevCommit commit =
rw.parseCommit(repo.resolve(revstr));
TreeWalk tw =
TreeWalk.forPath(rw.getObjectReader(), path,
commit.getTree().getId());
if (tw == null) {
throw new ResourceNotFoundException();
}
final ObjectLoader object = repo.open(tw.getObjectId(0));
@SuppressWarnings("resource")
BinaryResult result = new BinaryResult() {
@Override
public void writeTo(OutputStream os) throws IOException {
object.copyTo(os);
}
};
return result.setContentLength(object.getSize()).base64();
} finally {
rw.release();
}
} finally {
repo.close();
}
}
}

View File

@@ -20,6 +20,7 @@ import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
import static com.google.gerrit.server.change.FileResource.FILE_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
import static com.google.gerrit.server.change.ChangeEditResource.CHANGE_EDIT_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -44,6 +45,7 @@ public class Module extends RestApiModule {
DynamicMap.mapOf(binder(), FILE_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
get(CHANGE_KIND).to(GetChange.class);
get(CHANGE_KIND, "detail").to(GetDetail.class);

View File

@@ -0,0 +1,336 @@
// 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.gerrit.server.edit.ChangeEditUtil.editRefName;
import com.google.gerrit.extensions.restapi.AuthException;
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.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.project.InvalidChangeOperationException;
import com.google.gerrit.server.util.TimeUtil;
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.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
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.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import java.io.IOException;
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,
RESTORE_ENTRY
}
private final TimeZone tz;
private final GitRepositoryManager gitManager;
private final Provider<CurrentUser> currentUser;
private final ChangeEditUtil editUtil;
@Inject
ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent,
GitRepositoryManager gitManager,
Provider<ReviewDb> dbProvider,
Provider<CurrentUser> currentUser,
ChangeEditUtil editUtil) {
this.gitManager = gitManager;
this.currentUser = currentUser;
this.editUtil = editUtil;
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();
Repository repo = gitManager.openRepository(change.getProject());
String refName = editRefName(me.getAccountId(), change.getId());
try {
if (repo.getRefDatabase().getRef(refName) != null) {
throw new ResourceConflictException("edit already exists");
}
RevWalk rw = new RevWalk(repo);
ObjectInserter inserter = repo.newObjectInserter();
try {
RevCommit base = rw.parseCommit(ObjectId.fromString(
ps.getRevision().get()));
ObjectId commit = createCommit(me, inserter, base, base, base.getTree());
inserter.flush();
return update(repo, me, refName, rw, base, ObjectId.zeroId(), commit);
} finally {
rw.release();
inserter.release();
}
} finally {
repo.close();
}
}
/**
* 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, byte[] content) throws AuthException,
InvalidChangeOperationException, IOException {
return modify(TreeOperation.CHANGE_ENTRY, edit, file, 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);
}
/**
* 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);
}
private RefUpdate.Result modify(TreeOperation op,
ChangeEdit edit, String file, byte[] content)
throws AuthException, IOException, InvalidChangeOperationException {
if (!currentUser.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
IdentifiedUser me = (IdentifiedUser) currentUser.get();
Repository repo = gitManager.openRepository(edit.getChange().getProject());
try {
RevWalk rw = new RevWalk(repo);
ObjectInserter inserter = repo.newObjectInserter();
ObjectReader reader = repo.newObjectReader();
try {
String refName = edit.getRefName();
RevCommit prevEdit = rw.parseCommit(edit.getRef().getObjectId());
PatchSet basePs = editUtil.getBasePatchSet(edit, prevEdit);
RevCommit base = rw.parseCommit(ObjectId.fromString(
basePs.getRevision().get()));
ObjectId oldObjectId = prevEdit;
if (prevEdit == null) {
prevEdit = base;
oldObjectId = ObjectId.zeroId();
}
ObjectId newTree = writeNewTree(op, repo, rw, inserter,
prevEdit, reader, file, content, base);
if (ObjectId.equals(newTree, prevEdit.getTree())) {
throw new InvalidChangeOperationException("no changes were made");
}
ObjectId commit = createCommit(me, inserter, prevEdit, base, newTree);
inserter.flush();
return update(repo, me, refName, rw, base, oldObjectId, commit);
} finally {
rw.release();
inserter.release();
reader.release();
}
} finally {
repo.close();
}
}
private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
RevCommit prevEdit, RevCommit base, ObjectId tree) throws IOException {
CommitBuilder builder = new CommitBuilder();
builder.setTreeId(tree);
builder.setParentIds(base);
builder.setAuthor(prevEdit.getAuthorIdent());
builder.setCommitter(getCommitterIdent(me));
builder.setMessage(prevEdit.getFullMessage());
return inserter.insert(builder);
}
private RefUpdate.Result update(Repository repo, IdentifiedUser me,
String refName, RevWalk rw, RevCommit base,
ObjectId oldObjectId, ObjectId newEdit) throws IOException {
RefUpdate ru = repo.updateRef(refName);
ru.setExpectedOldObjectId(oldObjectId);
ru.setNewObjectId(newEdit);
ru.setRefLogIdent(getRefLogIdent(me));
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, Repository repo, RevWalk rw,
ObjectInserter ins, RevCommit prevEdit, ObjectReader reader,
String fileName, byte[] content, RevCommit base)
throws IOException, InvalidChangeOperationException {
DirCache newTree = createTree(reader, prevEdit);
editTree(
op,
repo,
rw,
base,
newTree.editor(),
ins,
fileName,
content);
return newTree.writeTree(ins);
}
private static void editTree(TreeOperation op, Repository repo, RevWalk rw,
RevCommit base, DirCacheEditor dce, ObjectInserter ins, String path,
byte[] content) throws IOException, InvalidChangeOperationException {
switch (op) {
case CHANGE_ENTRY:
case RESTORE_ENTRY:
dce.add(getPathEdit(op, repo, rw, base, path, ins, content));
break;
case DELETE_ENTRY:
dce.add(new DeletePath(path));
break;
default:
throw new IllegalStateException("unknown tree operation");
}
dce.finish();
}
private static PathEdit getPathEdit(TreeOperation op, Repository repo, RevWalk rw,
RevCommit base, String path, ObjectInserter ins, byte[] content)
throws IOException, InvalidChangeOperationException {
final ObjectId oid = op == TreeOperation.CHANGE_ENTRY
? ins.insert(Constants.OBJ_BLOB, content)
: getObjectIdForRestoreOperation(repo, rw, base, path);
return new PathEdit(path) {
@Override
public void apply(DirCacheEntry ent) {
ent.setFileMode(FileMode.REGULAR_FILE);
ent.setObjectId(oid);
}
};
}
private static ObjectId getObjectIdForRestoreOperation(Repository repo,
RevWalk rw, RevCommit base, String path)
throws IOException, InvalidChangeOperationException {
TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path,
base.getTree().getId());
// If the file does not exist in the base commit, try to restore it
// from the base's parent commit.
if (tw == null && base.getParentCount() == 1) {
tw = TreeWalk.forPath(rw.getObjectReader(), path,
rw.parseCommit(base.getParent(0)).getTree().getId());
}
if (tw == null) {
throw new InvalidChangeOperationException(String.format(
"cannot restore path %s: missing in base revision %s",
path, base.abbreviate(8)));
}
return tw.getObjectId(0);
}
private static DirCache createTree(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()
.getId());
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);
}
}

View File

@@ -53,6 +53,7 @@ import java.util.List;
* Utility functions to manipulate change edits.
* <p>
* This class contains methods to retrieve, publish and delete edits.
* For changing edits see {@link ChangeEditModifier}.
*/
@Singleton
public class ChangeEditUtil {

View File

@@ -17,25 +17,25 @@ package com.google.gerrit.server.project;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class GetContent implements RestReadView<FileResource> {
private final Provider<com.google.gerrit.server.change.GetContent> getContent;
private final FileContentUtil fileContentUtil;
@Inject
GetContent(Provider<com.google.gerrit.server.change.GetContent> getContent) {
this.getContent = getContent;
GetContent(FileContentUtil fileContentUtil) {
this.fileContentUtil = fileContentUtil;
}
@Override
public BinaryResult apply(FileResource rsrc)
throws ResourceNotFoundException, IOException {
return getContent.get().apply(rsrc.getProject(), rsrc.getRev(),
return fileContentUtil.getContent(rsrc.getProject(), rsrc.getRev(),
rsrc.getPath());
}
}