blob: cb3729b9b6f88e455fc4dc754d3d556c9f9a33e3 [file] [log] [blame]
// 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.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AcceptsDelete;
import com.google.gerrit.extensions.restapi.AcceptsPost;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
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.WebLinks;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.edit.ChangeEditJson;
import com.google.gerrit.server.edit.ChangeEditModifier;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.edit.UnchangedCommitMessageException;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.util.List;
@Singleton
public class ChangeEdits implements
ChildCollection<ChangeResource, ChangeEditResource>,
AcceptsCreate<ChangeResource>,
AcceptsPost<ChangeResource>,
AcceptsDelete<ChangeResource> {
private final DynamicMap<RestView<ChangeEditResource>> views;
private final Create.Factory createFactory;
private final DeleteFile.Factory deleteFileFactory;
private final Provider<Detail> detail;
private final ChangeEditUtil editUtil;
private final Post post;
@Inject
ChangeEdits(DynamicMap<RestView<ChangeEditResource>> views,
Create.Factory createFactory,
Provider<Detail> detail,
ChangeEditUtil editUtil,
Post post,
DeleteFile.Factory deleteFileFactory) {
this.views = views;
this.createFactory = createFactory;
this.detail = detail;
this.editUtil = editUtil;
this.post = post;
this.deleteFileFactory = deleteFileFactory;
}
@Override
public DynamicMap<RestView<ChangeEditResource>> views() {
return views;
}
@Override
public RestView<ChangeResource> list() {
return detail.get();
}
@Override
public ChangeEditResource parse(ChangeResource rsrc, IdString id)
throws ResourceNotFoundException, AuthException, IOException,
InvalidChangeOperationException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (!edit.isPresent()) {
throw new ResourceNotFoundException(id);
}
return new ChangeEditResource(rsrc, edit.get(), id.get());
}
@SuppressWarnings("unchecked")
@Override
public Create create(ChangeResource parent, IdString id)
throws RestApiException {
return createFactory.create(parent.getChange(), id.get());
}
@SuppressWarnings("unchecked")
@Override
public Post post(ChangeResource parent) throws RestApiException {
return post;
}
/**
* Create handler that is activated when collection element is accessed
* but doesn't exist, e. g. PUT request with a path was called but
* change edit wasn't created yet. Change edit is created and PUT
* handler is called.
*/
@SuppressWarnings("unchecked")
@Override
public DeleteFile delete(ChangeResource parent, IdString id)
throws RestApiException {
// It's safe to assume that id can never be null, because
// otherwise we would end up in dedicated endpoint for
// deleting of change edits and not a file in change edit
return deleteFileFactory.create(id.get());
}
public static class Create implements
RestModifyView<ChangeResource, Put.Input> {
interface Factory {
Create create(Change change, String path);
}
private final Provider<ReviewDb> db;
private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
private final Put putEdit;
private final Change change;
private final String path;
@Inject
Create(Provider<ReviewDb> db,
ChangeEditUtil editUtil,
ChangeEditModifier editModifier,
Put putEdit,
@Assisted Change change,
@Assisted @Nullable String path) {
this.db = db;
this.editUtil = editUtil;
this.editModifier = editModifier;
this.putEdit = putEdit;
this.change = change;
this.path = path;
}
@Override
public Response<?> apply(ChangeResource resource, Put.Input input)
throws AuthException, IOException, ResourceConflictException,
OrmException, InvalidChangeOperationException {
Optional<ChangeEdit> edit = editUtil.byChange(change);
if (edit.isPresent()) {
throw new ResourceConflictException(String.format(
"edit already exists for the change %s",
resource.getChange().getChangeId()));
}
edit = createEdit();
if (!Strings.isNullOrEmpty(path)) {
putEdit.apply(new ChangeEditResource(resource, edit.get(), path),
input);
}
return Response.none();
}
private Optional<ChangeEdit> createEdit() throws AuthException,
IOException, ResourceConflictException, OrmException {
editModifier.createEdit(change,
db.get().patchSets().get(change.currentPatchSetId()));
return editUtil.byChange(change);
}
}
public static class DeleteFile implements
RestModifyView<ChangeResource, DeleteFile.Input> {
public static class Input {
}
interface Factory {
DeleteFile create(String path);
}
private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
private final Provider<ReviewDb> db;
private final String path;
@Inject
DeleteFile(ChangeEditUtil editUtil,
ChangeEditModifier editModifier,
Provider<ReviewDb> db,
@Assisted String path) {
this.editUtil = editUtil;
this.editModifier = editModifier;
this.db = db;
this.path = path;
}
@Override
public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in)
throws IOException, AuthException, ResourceConflictException,
OrmException, InvalidChangeOperationException, BadRequestException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (edit.isPresent()) {
// Edit is wiped out
editUtil.delete(edit.get());
} else {
// Edit is created on top of current patch set by deleting path.
// Even if the latest patch set changed since the user triggered
// the operation, deleting the whole file is probably still what
// they intended.
editModifier.createEdit(rsrc.getChange(), db.get().patchSets().get(
rsrc.getChange().currentPatchSetId()));
edit = editUtil.byChange(rsrc.getChange());
editModifier.deleteFile(edit.get(), path);
}
return Response.none();
}
}
// TODO(davido): Turn the boolean options to ChangeEditOption enum,
// like it's already the case for ListChangesOption/ListGroupsOption
public static class Detail implements RestReadView<ChangeResource> {
private final ChangeEditUtil editUtil;
private final ChangeEditJson editJson;
private final FileInfoJson fileInfoJson;
private final Revisions revisions;
@Option(name = "--base", metaVar = "revision-id")
String base;
@Option(name = "--list")
boolean list;
@Option(name = "--download-commands")
boolean downloadCommands;
@Inject
Detail(ChangeEditUtil editUtil,
ChangeEditJson editJson,
FileInfoJson fileInfoJson,
Revisions revisions) {
this.editJson = editJson;
this.editUtil = editUtil;
this.fileInfoJson = fileInfoJson;
this.revisions = revisions;
}
@Override
public Response<EditInfo> apply(ChangeResource rsrc) throws AuthException,
IOException, ResourceNotFoundException, OrmException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (!edit.isPresent()) {
return Response.none();
}
EditInfo editInfo = editJson.toEditInfo(edit.get(), downloadCommands);
if (list) {
PatchSet basePatchSet = null;
if (base != null) {
RevisionResource baseResource = revisions.parse(
rsrc, IdString.fromDecoded(base));
basePatchSet = baseResource.getPatchSet();
}
try {
editInfo.files =
fileInfoJson.toFileInfoMap(
rsrc.getChange(),
edit.get().getRevision(),
basePatchSet);
} catch (PatchListNotAvailableException e) {
throw new ResourceNotFoundException(e.getMessage());
}
}
return Response.ok(editInfo);
}
}
/**
* Post to edit collection resource. Two different operations are
* supported:
* <ul>
* <li>Create non existing change edit</li>
* <li>Restore path in existing change edit</li>
* </ul>
* The combination of two operations in one request is supported.
*/
@Singleton
public static class Post implements
RestModifyView<ChangeResource, Post.Input> {
public static class Input {
public String restorePath;
public String oldPath;
public String newPath;
}
private final Provider<ReviewDb> db;
private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
@Inject
Post(Provider<ReviewDb> db,
ChangeEditUtil editUtil,
ChangeEditModifier editModifier) {
this.db = db;
this.editUtil = editUtil;
this.editModifier = editModifier;
}
@Override
public Response<?> apply(ChangeResource resource, Post.Input input)
throws AuthException, InvalidChangeOperationException, IOException,
ResourceConflictException, OrmException {
Optional<ChangeEdit> edit = editUtil.byChange(resource.getChange());
if (!edit.isPresent()) {
edit = createEdit(resource.getChange());
}
if (input != null) {
if (!Strings.isNullOrEmpty(input.restorePath)) {
editModifier.restoreFile(edit.get(), input.restorePath);
} else if (!Strings.isNullOrEmpty(input.oldPath)
&& !Strings.isNullOrEmpty(input.newPath)) {
editModifier.renameFile(edit.get(), input.oldPath, input.newPath);
}
}
return Response.none();
}
private Optional<ChangeEdit> createEdit(Change change)
throws AuthException, IOException, ResourceConflictException,
OrmException {
editModifier.createEdit(change,
db.get().patchSets().get(change.currentPatchSetId()));
return editUtil.byChange(change);
}
}
/**
* Put handler that is activated when PUT request is called on
* collection element.
*/
@Singleton
public static class Put implements
RestModifyView<ChangeEditResource, Put.Input> {
public static class Input {
@DefaultInput
public RawInput content;
}
private final ChangeEditModifier editModifier;
@Inject
Put(ChangeEditModifier editModifier) {
this.editModifier = editModifier;
}
@Override
public Response<?> apply(ChangeEditResource rsrc, Input input)
throws AuthException, ResourceConflictException {
String path = rsrc.getPath();
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
throw new ResourceConflictException("Invalid path: " + path);
}
try {
editModifier.modifyFile(
rsrc.getChangeEdit(),
rsrc.getPath(),
input.content);
} catch(InvalidChangeOperationException | IOException e) {
throw new ResourceConflictException(e.getMessage());
}
return Response.none();
}
}
/**
* Handler to delete a file.
* <p>
* This deletes the file from the repository completely. This is not the same
* as reverting or restoring a file to its previous contents.
*/
@Singleton
public static class DeleteContent implements
RestModifyView<ChangeEditResource, DeleteContent.Input> {
public static class Input {
}
private final ChangeEditModifier editModifier;
@Inject
DeleteContent(ChangeEditModifier editModifier) {
this.editModifier = editModifier;
}
@Override
public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input)
throws AuthException, ResourceConflictException {
try {
editModifier.deleteFile(rsrc.getChangeEdit(), rsrc.getPath());
} catch(InvalidChangeOperationException | IOException e) {
throw new ResourceConflictException(e.getMessage());
}
return Response.none();
}
}
@Singleton
public static class Get implements RestReadView<ChangeEditResource> {
private final FileContentUtil fileContentUtil;
@Inject
Get(FileContentUtil fileContentUtil) {
this.fileContentUtil = fileContentUtil;
}
@Override
public Response<?> apply(ChangeEditResource rsrc)
throws IOException {
try {
return Response.ok(fileContentUtil.getContent(
rsrc.getControl().getProjectControl().getProjectState(),
ObjectId.fromString(rsrc.getChangeEdit().getRevision().get()),
rsrc.getPath()));
} catch (ResourceNotFoundException rnfe) {
return Response.none();
}
}
}
@Singleton
public static class GetMeta implements RestReadView<ChangeEditResource> {
private final WebLinks webLinks;
@Inject
GetMeta(WebLinks webLinks) {
this.webLinks = webLinks;
}
@Override
public FileInfo apply(ChangeEditResource rsrc) {
FileInfo r = new FileInfo();
ChangeEdit edit = rsrc.getChangeEdit();
Change change = edit.getChange();
FluentIterable<DiffWebLinkInfo> links =
webLinks.getDiffLinks(change.getProject().get(),
change.getChangeId(),
edit.getBasePatchSet().getPatchSetId(),
edit.getBasePatchSet().getRefName(),
rsrc.getPath(),
0,
edit.getRefName(),
rsrc.getPath());
r.webLinks = links.isEmpty() ? null : links.toList();
return r;
}
public static class FileInfo {
public List<DiffWebLinkInfo> webLinks;
}
}
@Singleton
public static class EditMessage implements
RestModifyView<ChangeResource, EditMessage.Input> {
public static class Input {
@DefaultInput
public String message;
}
private final Provider<ReviewDb> db;
private final ChangeEditModifier editModifier;
private final ChangeEditUtil editUtil;
@Inject
EditMessage(Provider<ReviewDb> db,
ChangeEditModifier editModifier,
ChangeEditUtil editUtil) {
this.db = db;
this.editModifier = editModifier;
this.editUtil = editUtil;
}
@Override
public Object apply(ChangeResource rsrc, Input input) throws AuthException,
IOException, InvalidChangeOperationException, BadRequestException,
ResourceConflictException, OrmException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (!edit.isPresent()) {
editModifier.createEdit(rsrc.getChange(),
db.get().patchSets().get(rsrc.getChange().currentPatchSetId()));
edit = editUtil.byChange(rsrc.getChange());
}
if (input == null || Strings.isNullOrEmpty(input.message)) {
throw new BadRequestException("commit message must be provided");
}
try {
editModifier.modifyMessage(edit.get(), input.message);
} catch (UnchangedCommitMessageException ucm) {
throw new ResourceConflictException(ucm.getMessage());
}
return Response.none();
}
}
@Singleton
public static class GetMessage implements RestReadView<ChangeResource> {
private final ChangeEditUtil editUtil;
@Inject
GetMessage(ChangeEditUtil editUtil) {
this.editUtil = editUtil;
}
@Override
public BinaryResult apply(ChangeResource rsrc) throws AuthException,
IOException, ResourceNotFoundException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (edit.isPresent()) {
String msg = edit.get().getEditCommit().getFullMessage();
return BinaryResult.create(msg)
.setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
.base64();
}
throw new ResourceNotFoundException();
}
}
}