blob: ad0b1ad09a16ca6d122529e2e5f215621672b9ab [file] [log] [blame]
// Copyright (C) 2020 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.plugins.codeowners.restapi;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.plugins.codeowners.backend.ChangedFiles;
import com.google.gerrit.plugins.codeowners.restapi.CodeOwnersInChangeCollection.PathResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import java.io.IOException;
import java.nio.file.Path;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* REST collection that serves requests to {@code
* /changes/<change-id>/revisions/<revision-id>/code_owners/}.
*/
@Singleton
public class CodeOwnersInChangeCollection
implements ChildCollection<RevisionResource, PathResource> {
private final DynamicMap<RestView<PathResource>> views;
private final GitRepositoryManager repoManager;
private final ChangedFiles changedFiles;
@Inject
CodeOwnersInChangeCollection(
DynamicMap<RestView<PathResource>> views,
GitRepositoryManager repoManager,
ChangedFiles changedFiles) {
this.views = views;
this.repoManager = repoManager;
this.changedFiles = changedFiles;
}
@Override
public RestView<RevisionResource> list() throws ResourceNotFoundException {
// Listing paths that have code owners is not implemented.
throw new ResourceNotFoundException();
}
@Override
public PathResource parse(RevisionResource revisionResource, IdString id)
throws RestApiException, IOException, PatchListNotAvailableException {
// Check if the file exists in the revision only after creating the path resource. This way we
// get a more specific error response for invalid paths ('400 Bad Request' instead of a '404 Not
// Found').
PathResource pathResource =
PathResource.parse(
revisionResource, getDestBranchRevision(revisionResource.getChange()), id);
checkThatFileExists(revisionResource, pathResource, id);
return pathResource;
}
/**
* Gets the current revision of the destination branch of the given change.
*
* <p>This is the revision from which the code owner configs should be read when computing code
* owners for the files that are touched in the change.
*/
private ObjectId getDestBranchRevision(Change change) throws IOException {
try (Repository repository = repoManager.openRepository(change.getProject());
RevWalk rw = new RevWalk(repository)) {
Ref ref = repository.exactRef(change.getDest().branch());
checkNotNull(
ref,
"branch %s in repository %s not found",
change.getDest().branch(),
change.getProject().get());
return rw.parseCommit(ref.getObjectId());
}
}
private void checkThatFileExists(
RevisionResource revisionResource, PathResource pathResource, IdString id)
throws RestApiException, IOException, PatchListNotAvailableException {
if (!changedFiles.compute(revisionResource).stream()
.anyMatch(
changedFile ->
// Check whether the path matches any file in the change.
changedFile.hasNewPath(pathResource.getPath())
// For renamed and deleted files we also accept requests for the old path.
// Listing code owners for the old path of renamed/deleted files should be
// possible because these files require a code owner approval on the old path
// for submit and users need to know whom they need to add as reviewer for this.
// For copied files the old path is not modified and hence no code owner
// approval for the old path is required. This is why users do not need to get
// code owners for the old path in case of copy.
|| ((changedFile.isRename() || changedFile.isDeletion())
&& changedFile.hasOldPath(pathResource.getPath())))) {
// Throw the exception with the path we got as input.
throw new ResourceNotFoundException(id);
}
}
@Override
public DynamicMap<RestView<PathResource>> views() {
return views;
}
/**
* REST resource representing an arbitrary path in a branch under the {@link
* CodeOwnersInChangeCollection} REST collection.
*/
public static class PathResource extends AbstractPathResource {
/**
* The resource kind of the members in the {@link CodeOwnersInChangeCollection} REST collection.
*/
static final TypeLiteral<RestView<PathResource>> PATH_KIND =
new TypeLiteral<RestView<PathResource>>() {};
static PathResource parse(
RevisionResource revisionResource, ObjectId branchRevision, IdString pathId)
throws BadRequestException {
return new PathResource(revisionResource, branchRevision, parsePath(pathId));
}
private final RevisionResource revisionResource;
private PathResource(RevisionResource revisionResource, ObjectId branchRevision, Path path) {
super(revisionResource, branchRevision, path);
this.revisionResource = revisionResource;
}
public RevisionResource getRevisionResource() {
return revisionResource;
}
}
}