| // 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.restapi.change; | |
| import static com.google.gerrit.entities.Patch.FileMode.EXECUTABLE_FILE; | |
| import static com.google.gerrit.entities.Patch.FileMode.REGULAR_FILE; | |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.io.ByteStreams; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.RawInputUtil; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.Patch; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.extensions.api.changes.FileContentInput; | |
| import com.google.gerrit.extensions.common.DiffWebLinkInfo; | |
| import com.google.gerrit.extensions.common.EditInfo; | |
| import com.google.gerrit.extensions.common.Input; | |
| import com.google.gerrit.extensions.registration.DynamicMap; | |
| 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.RestCollectionCreateView; | |
| import com.google.gerrit.extensions.restapi.RestCollectionDeleteMissingView; | |
| import com.google.gerrit.extensions.restapi.RestCollectionModifyView; | |
| 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.server.WebLinks; | |
| import com.google.gerrit.server.change.ChangeEditResource; | |
| import com.google.gerrit.server.change.ChangeResource; | |
| import com.google.gerrit.server.change.FileContentUtil; | |
| import com.google.gerrit.server.change.FileInfoJson; | |
| import com.google.gerrit.server.change.RevisionResource; | |
| 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.git.GitRepositoryManager; | |
| import com.google.gerrit.server.patch.PatchListNotAvailableException; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.project.InvalidChangeOperationException; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.Singleton; | |
| import java.io.IOException; | |
| import java.util.List; | |
| import java.util.Optional; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.util.Base64; | |
| import org.kohsuke.args4j.Option; | |
| @Singleton | |
| public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditResource> { | |
| private final DynamicMap<RestView<ChangeEditResource>> views; | |
| private final Provider<Detail> detail; | |
| private final ChangeEditUtil editUtil; | |
| @Inject | |
| ChangeEdits( | |
| DynamicMap<RestView<ChangeEditResource>> views, | |
| Provider<Detail> detail, | |
| ChangeEditUtil editUtil) { | |
| this.views = views; | |
| this.detail = detail; | |
| this.editUtil = editUtil; | |
| } | |
| @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 { | |
| Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser()); | |
| if (!edit.isPresent()) { | |
| throw new ResourceNotFoundException(id); | |
| } | |
| return new ChangeEditResource(rsrc, edit.get(), id.get()); | |
| } | |
| /** | |
| * 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. | |
| */ | |
| @Singleton | |
| public static class Create | |
| implements RestCollectionCreateView<ChangeResource, ChangeEditResource, FileContentInput> { | |
| private final Put putEdit; | |
| @Inject | |
| Create(Put putEdit) { | |
| this.putEdit = putEdit; | |
| } | |
| @Override | |
| public Response<Object> apply( | |
| ChangeResource resource, IdString id, FileContentInput fileContentInput) | |
| throws AuthException, BadRequestException, ResourceConflictException, IOException, | |
| PermissionBackendException { | |
| putEdit.apply(resource, id.get(), fileContentInput); | |
| return Response.none(); | |
| } | |
| } | |
| @Singleton | |
| public static class DeleteFile | |
| implements RestCollectionDeleteMissingView<ChangeResource, ChangeEditResource, Input> { | |
| private final DeleteContent deleteContent; | |
| @Inject | |
| DeleteFile(DeleteContent deleteContent) { | |
| this.deleteContent = deleteContent; | |
| } | |
| @Override | |
| public Response<Object> apply(ChangeResource rsrc, IdString id, Input input) | |
| throws IOException, AuthException, BadRequestException, ResourceConflictException, | |
| PermissionBackendException { | |
| return deleteContent.apply(rsrc, id.get()); | |
| } | |
| } | |
| // 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; | |
| private String base; | |
| private boolean list; | |
| private boolean downloadCommands; | |
| @Option(name = "--base", metaVar = "revision-id") | |
| public void setBase(String base) { | |
| this.base = base; | |
| } | |
| @Option(name = "--list") | |
| public void setList(boolean list) { | |
| this.list = list; | |
| } | |
| @Option(name = "--download-commands") | |
| public void setDownloadCommands(boolean downloadCommands) { | |
| this.downloadCommands = 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, ResourceConflictException, | |
| PermissionBackendException { | |
| Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser()); | |
| 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.getFileInfoMap( | |
| rsrc.getChange(), edit.get().getEditCommit(), 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>Restore path in existing change edit | |
| * </ul> | |
| * | |
| * The combination of two operations in one request is supported. | |
| */ | |
| @Singleton | |
| public static class Post | |
| implements RestCollectionModifyView<ChangeResource, ChangeEditResource, Post.Input> { | |
| public static class Input { | |
| public String restorePath; | |
| public String oldPath; | |
| public String newPath; | |
| } | |
| private final ChangeEditModifier editModifier; | |
| private final GitRepositoryManager repositoryManager; | |
| @Inject | |
| Post(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) { | |
| this.editModifier = editModifier; | |
| this.repositoryManager = repositoryManager; | |
| } | |
| @Override | |
| public Response<Object> apply(ChangeResource resource, Post.Input postInput) | |
| throws AuthException, BadRequestException, IOException, ResourceConflictException, | |
| PermissionBackendException { | |
| Project.NameKey project = resource.getProject(); | |
| try (Repository repository = repositoryManager.openRepository(project)) { | |
| if (isRestoreFile(postInput)) { | |
| editModifier.restoreFile(repository, resource.getNotes(), postInput.restorePath); | |
| } else if (isRenameFile(postInput)) { | |
| editModifier.renameFile( | |
| repository, resource.getNotes(), postInput.oldPath, postInput.newPath); | |
| } else { | |
| editModifier.createEdit(repository, resource.getNotes()); | |
| } | |
| } catch (InvalidChangeOperationException e) { | |
| throw new ResourceConflictException(e.getMessage()); | |
| } | |
| return Response.none(); | |
| } | |
| private static boolean isRestoreFile(Post.Input postInput) { | |
| return postInput != null && !Strings.isNullOrEmpty(postInput.restorePath); | |
| } | |
| private static boolean isRenameFile(Post.Input postInput) { | |
| return postInput != null | |
| && !Strings.isNullOrEmpty(postInput.oldPath) | |
| && !Strings.isNullOrEmpty(postInput.newPath); | |
| } | |
| } | |
| /** Put handler that is activated when PUT request is called on collection element. */ | |
| @Singleton | |
| public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> { | |
| private static final Pattern BINARY_DATA_PATTERN = | |
| Pattern.compile("data:([\\w/.-]*);([\\w]+),(.*)"); | |
| private static final String BASE64 = "base64"; | |
| private final ChangeEditModifier editModifier; | |
| private final GitRepositoryManager repositoryManager; | |
| private final EditMessage editMessage; | |
| @Inject | |
| Put( | |
| ChangeEditModifier editModifier, | |
| GitRepositoryManager repositoryManager, | |
| EditMessage editMessage) { | |
| this.editModifier = editModifier; | |
| this.repositoryManager = repositoryManager; | |
| this.editMessage = editMessage; | |
| } | |
| @Override | |
| public Response<Object> apply(ChangeEditResource rsrc, FileContentInput fileContentInput) | |
| throws AuthException, BadRequestException, ResourceConflictException, IOException, | |
| PermissionBackendException { | |
| return apply(rsrc.getChangeResource(), rsrc.getPath(), fileContentInput); | |
| } | |
| @Nullable | |
| private Integer decimalAsOctal(Integer inputMode) throws BadRequestException { | |
| if (inputMode == null) { | |
| return null; | |
| } | |
| switch (inputMode) { | |
| case 100755: | |
| return EXECUTABLE_FILE.getMode(); | |
| case 100644: | |
| return REGULAR_FILE.getMode(); | |
| } | |
| throw new BadRequestException( | |
| "file_mode (" + inputMode + ") was invalid: supported values are 100644 or 100755."); | |
| } | |
| public Response<Object> apply( | |
| ChangeResource rsrc, String path, FileContentInput fileContentInput) | |
| throws AuthException, BadRequestException, ResourceConflictException, IOException, | |
| PermissionBackendException { | |
| if (fileContentInput.content == null && fileContentInput.binary_content == null) { | |
| throw new BadRequestException("either content or binary_content is required"); | |
| } | |
| RawInput newContent; | |
| if (fileContentInput.binary_content != null) { | |
| Matcher m = BINARY_DATA_PATTERN.matcher(fileContentInput.binary_content); | |
| if (m.matches() && BASE64.equals(m.group(2))) { | |
| newContent = RawInputUtil.create(Base64.decode(m.group(3))); | |
| } else { | |
| throw new BadRequestException("binary_content must be encoded as base64 data uri"); | |
| } | |
| } else { | |
| newContent = fileContentInput.content; | |
| } | |
| if (Patch.COMMIT_MSG.equals(path) && fileContentInput.binary_content == null) { | |
| EditMessage.Input editMessageInput = new EditMessage.Input(); | |
| editMessageInput.message = | |
| new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8); | |
| return editMessage.apply(rsrc, editMessageInput); | |
| } | |
| if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') { | |
| throw new ResourceConflictException("Invalid path: " + path); | |
| } | |
| try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) { | |
| editModifier.modifyFile( | |
| repository, | |
| rsrc.getNotes(), | |
| path, | |
| newContent, | |
| decimalAsOctal(fileContentInput.fileMode)); | |
| } catch (InvalidChangeOperationException 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, Input> { | |
| private final ChangeEditModifier editModifier; | |
| private final GitRepositoryManager repositoryManager; | |
| @Inject | |
| DeleteContent(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) { | |
| this.editModifier = editModifier; | |
| this.repositoryManager = repositoryManager; | |
| } | |
| @Override | |
| public Response<Object> apply(ChangeEditResource rsrc, Input input) | |
| throws AuthException, BadRequestException, ResourceConflictException, IOException, | |
| PermissionBackendException { | |
| return apply(rsrc.getChangeResource(), rsrc.getPath()); | |
| } | |
| public Response<Object> apply(ChangeResource rsrc, String filePath) | |
| throws AuthException, BadRequestException, IOException, ResourceConflictException, | |
| PermissionBackendException { | |
| try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) { | |
| editModifier.deleteFile(repository, rsrc.getNotes(), filePath); | |
| } catch (InvalidChangeOperationException e) { | |
| throw new ResourceConflictException(e.getMessage()); | |
| } | |
| return Response.none(); | |
| } | |
| } | |
| public static class Get implements RestReadView<ChangeEditResource> { | |
| private final FileContentUtil fileContentUtil; | |
| private final ProjectCache projectCache; | |
| private final GetMessage getMessage; | |
| @Option( | |
| name = "--base", | |
| aliases = {"-b"}, | |
| usage = "whether to load the content on the base revision instead of the change edit") | |
| private boolean base; | |
| @Inject | |
| Get(FileContentUtil fileContentUtil, ProjectCache projectCache, GetMessage getMessage) { | |
| this.fileContentUtil = fileContentUtil; | |
| this.projectCache = projectCache; | |
| this.getMessage = getMessage; | |
| } | |
| @Override | |
| public Response<BinaryResult> apply(ChangeEditResource rsrc) throws AuthException, IOException { | |
| try { | |
| if (Patch.COMMIT_MSG.equals(rsrc.getPath())) { | |
| return getMessage.apply(rsrc.getChangeResource()); | |
| } | |
| ChangeEdit edit = rsrc.getChangeEdit(); | |
| Project.NameKey project = rsrc.getChangeResource().getProject(); | |
| return Response.ok( | |
| fileContentUtil.getContent( | |
| projectCache.get(project).orElseThrow(illegalState(project)), | |
| base ? edit.getBasePatchSet().commitId() : edit.getEditCommit(), | |
| rsrc.getPath(), | |
| null)); | |
| } catch (ResourceNotFoundException | BadRequestException e) { | |
| return Response.none(); | |
| } | |
| } | |
| } | |
| @Singleton | |
| public static class GetMeta implements RestReadView<ChangeEditResource> { | |
| private final WebLinks webLinks; | |
| @Inject | |
| GetMeta(WebLinks webLinks) { | |
| this.webLinks = webLinks; | |
| } | |
| @Override | |
| public Response<FileInfo> apply(ChangeEditResource rsrc) { | |
| FileInfo r = new FileInfo(); | |
| ChangeEdit edit = rsrc.getChangeEdit(); | |
| Change change = edit.getChange(); | |
| ImmutableList<DiffWebLinkInfo> links = | |
| webLinks.getDiffLinks( | |
| change.getProject().get(), | |
| change.getChangeId(), | |
| edit.getBasePatchSet().number(), | |
| edit.getBasePatchSet().refName(), | |
| rsrc.getPath(), | |
| 0, | |
| edit.getRefName(), | |
| rsrc.getPath()); | |
| r.webLinks = links.isEmpty() ? null : links; | |
| return Response.ok(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 ChangeEditModifier editModifier; | |
| private final GitRepositoryManager repositoryManager; | |
| @Inject | |
| EditMessage(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) { | |
| this.editModifier = editModifier; | |
| this.repositoryManager = repositoryManager; | |
| } | |
| @Override | |
| public Response<Object> apply(ChangeResource rsrc, EditMessage.Input editMessageInput) | |
| throws AuthException, IOException, BadRequestException, ResourceConflictException, | |
| PermissionBackendException { | |
| if (editMessageInput == null || Strings.isNullOrEmpty(editMessageInput.message)) { | |
| throw new BadRequestException("commit message must be provided"); | |
| } | |
| Project.NameKey project = rsrc.getProject(); | |
| try (Repository repository = repositoryManager.openRepository(project)) { | |
| editModifier.modifyMessage(repository, rsrc.getNotes(), editMessageInput.message); | |
| } catch (InvalidChangeOperationException e) { | |
| throw new ResourceConflictException(e.getMessage()); | |
| } | |
| return Response.none(); | |
| } | |
| } | |
| public static class GetMessage implements RestReadView<ChangeResource> { | |
| private final GitRepositoryManager repoManager; | |
| private final ChangeEditUtil editUtil; | |
| @Option( | |
| name = "--base", | |
| aliases = {"-b"}, | |
| usage = "whether to load the message on the base revision instead of the change edit") | |
| private boolean base; | |
| @Inject | |
| GetMessage(GitRepositoryManager repoManager, ChangeEditUtil editUtil) { | |
| this.repoManager = repoManager; | |
| this.editUtil = editUtil; | |
| } | |
| @Override | |
| public Response<BinaryResult> apply(ChangeResource rsrc) | |
| throws AuthException, IOException, ResourceNotFoundException { | |
| Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser()); | |
| String msg; | |
| if (edit.isPresent()) { | |
| if (base) { | |
| try (Repository repo = repoManager.openRepository(rsrc.getProject()); | |
| RevWalk rw = new RevWalk(repo)) { | |
| RevCommit commit = rw.parseCommit(edit.get().getBasePatchSet().commitId()); | |
| msg = commit.getFullMessage(); | |
| } | |
| } else { | |
| msg = edit.get().getEditCommit().getFullMessage(); | |
| } | |
| return Response.ok( | |
| BinaryResult.create(msg) | |
| .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE) | |
| .base64()); | |
| } | |
| throw new ResourceNotFoundException(); | |
| } | |
| } | |
| } |