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

com.google.gerrit.server.restapi.change.ChangeEdits Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
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.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 {
  private final DynamicMap> views;
  private final Provider detail;
  private final ChangeEditUtil editUtil;

  @Inject
  ChangeEdits(
      DynamicMap> views,
      Provider detail,
      ChangeEditUtil editUtil) {
    this.views = views;
    this.detail = detail;
    this.editUtil = editUtil;
  }

  @Override
  public DynamicMap> views() {
    return views;
  }

  @Override
  public RestView list() {
    return detail.get();
  }

  @Override
  public ChangeEditResource parse(ChangeResource rsrc, IdString id)
      throws ResourceNotFoundException, AuthException, IOException {
    Optional 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 {
    private final Put putEdit;

    @Inject
    Create(Put putEdit) {
      this.putEdit = putEdit;
    }

    @Override
    public Response 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 {
    private final DeleteContent deleteContent;

    @Inject
    DeleteFile(DeleteContent deleteContent) {
      this.deleteContent = deleteContent;
    }

    @Override
    public Response 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 {
    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 apply(ChangeResource rsrc)
        throws AuthException, IOException, ResourceNotFoundException, ResourceConflictException,
            PermissionBackendException {
      Optional 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:
   *
   * 
    *
  • Create non existing change edit *
  • Restore path in existing change edit *
* * The combination of two operations in one request is supported. */ @Singleton public static class Post implements RestCollectionModifyView { 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 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 { 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 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 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. * *

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 { private final ChangeEditModifier editModifier; private final GitRepositoryManager repositoryManager; @Inject DeleteContent(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) { this.editModifier = editModifier; this.repositoryManager = repositoryManager; } @Override public Response apply(ChangeEditResource rsrc, Input input) throws AuthException, BadRequestException, ResourceConflictException, IOException, PermissionBackendException { return apply(rsrc.getChangeResource(), rsrc.getPath()); } public Response 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 { 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 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 { private final WebLinks webLinks; @Inject GetMeta(WebLinks webLinks) { this.webLinks = webLinks; } @Override public Response apply(ChangeEditResource rsrc) { FileInfo r = new FileInfo(); ChangeEdit edit = rsrc.getChangeEdit(); Change change = edit.getChange(); ImmutableList 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 webLinks; } } @Singleton public static class EditMessage implements RestModifyView { 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 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 { 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 apply(ChangeResource rsrc) throws AuthException, IOException, ResourceNotFoundException { Optional 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(); } } }