543 lines
22 KiB
Java
543 lines
22 KiB
Java
/*
|
|
* Copyright (C) 2018 Google Inc.
|
|
*
|
|
* 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.copybara.git;
|
|
|
|
import static com.google.copybara.config.SkylarkUtil.convertFromNoneable;
|
|
import static com.google.copybara.config.SkylarkUtil.stringToEnum;
|
|
import static com.google.copybara.exception.ValidationException.checkCondition;
|
|
|
|
import com.google.common.base.MoreObjects;
|
|
import com.google.common.base.Preconditions;
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSetMultimap;
|
|
import com.google.copybara.Endpoint;
|
|
import com.google.copybara.LazyResourceLoader;
|
|
import com.google.copybara.config.SkylarkUtil;
|
|
import com.google.copybara.exception.RepoException;
|
|
import com.google.copybara.exception.ValidationException;
|
|
import com.google.copybara.git.github.api.CheckRuns;
|
|
import com.google.copybara.git.github.api.CombinedStatus;
|
|
import com.google.copybara.git.github.api.CreateStatusRequest;
|
|
import com.google.copybara.git.github.api.GitHubApi;
|
|
import com.google.copybara.git.github.api.GitHubApi.PullRequestListParams;
|
|
import com.google.copybara.git.github.api.GitHubApi.PullRequestListParams.DirectionFilter;
|
|
import com.google.copybara.git.github.api.GitHubApi.PullRequestListParams.SortFilter;
|
|
import com.google.copybara.git.github.api.GitHubApi.PullRequestListParams.StateFilter;
|
|
import com.google.copybara.git.github.api.GitHubApiException;
|
|
import com.google.copybara.git.github.api.GitHubApiException.ResponseCode;
|
|
import com.google.copybara.git.github.api.GitHubCommit;
|
|
import com.google.copybara.git.github.api.PullRequest;
|
|
import com.google.copybara.git.github.api.PullRequestComment;
|
|
import com.google.copybara.git.github.api.Ref;
|
|
import com.google.copybara.git.github.api.Status;
|
|
import com.google.copybara.git.github.api.Status.State;
|
|
import com.google.copybara.git.github.api.UpdatePullRequest;
|
|
import com.google.copybara.git.github.api.UpdateReferenceRequest;
|
|
import com.google.copybara.git.github.api.User;
|
|
import com.google.copybara.git.github.util.GitHubUtil;
|
|
import com.google.copybara.util.console.Console;
|
|
import com.google.devtools.build.lib.skylarkinterface.Param;
|
|
import com.google.devtools.build.lib.skylarkinterface.StarlarkBuiltin;
|
|
import com.google.devtools.build.lib.skylarkinterface.StarlarkDocumentationCategory;
|
|
import com.google.devtools.build.lib.skylarkinterface.StarlarkMethod;
|
|
import com.google.devtools.build.lib.syntax.EvalException;
|
|
import com.google.devtools.build.lib.syntax.Sequence;
|
|
import com.google.devtools.build.lib.syntax.Starlark;
|
|
import com.google.devtools.build.lib.syntax.StarlarkList;
|
|
import com.google.devtools.build.lib.syntax.StarlarkValue;
|
|
import com.google.re2j.Pattern;
|
|
import javax.annotation.Nullable;
|
|
|
|
/** GitHub specific class used in feedback mechanism and migration event hooks to access GitHub */
|
|
@SuppressWarnings("unused")
|
|
@StarlarkBuiltin(
|
|
name = "github_api_obj",
|
|
category = StarlarkDocumentationCategory.BUILTIN,
|
|
doc = "GitHub API endpoint implementation for feedback migrations and after migration hooks.")
|
|
public class GitHubEndPoint implements Endpoint, StarlarkValue {
|
|
|
|
private final LazyResourceLoader<GitHubApi> apiSupplier;
|
|
private final String url;
|
|
private final Console console;
|
|
// This might not be complete but it is only used for filtering get_pull_requests. We can
|
|
// add more chars on demand.
|
|
private static final Pattern SAFE_BRANCH_NAME_PREFIX = Pattern.compile("[\\w_.-][\\w/_.-]*");
|
|
|
|
|
|
GitHubEndPoint(LazyResourceLoader<GitHubApi> apiSupplier, String url, Console console) {
|
|
this.apiSupplier = Preconditions.checkNotNull(apiSupplier);
|
|
this.url = Preconditions.checkNotNull(url);
|
|
this.console = Preconditions.checkNotNull(console);
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "create_status",
|
|
doc = "Create or update a status for a commit. Returns the status created.",
|
|
parameters = {
|
|
@Param(
|
|
name = "sha",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "The SHA-1 for which we want to create or update the status"),
|
|
@Param(
|
|
name = "state",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "The state of the commit status: 'success', 'error', 'pending' or 'failure'"),
|
|
@Param(
|
|
name = "context",
|
|
type = String.class,
|
|
doc =
|
|
"The context for the commit"
|
|
+ " status. Use a value like 'copybara/import_successful' or similar",
|
|
named = true),
|
|
@Param(
|
|
name = "description",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "Description about what happened"),
|
|
@Param(
|
|
name = "target_url",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "Url with expanded information about the event",
|
|
noneable = true,
|
|
defaultValue = "None"),
|
|
})
|
|
public Status createStatus(
|
|
String sha, String state, String context, String description, Object targetUrl)
|
|
throws EvalException {
|
|
try {
|
|
checkCondition(State.VALID_VALUES.contains(state),
|
|
"Invalid value for state. Valid values: %s", State.VALID_VALUES);
|
|
checkCondition(GitRevision.COMPLETE_SHA1_PATTERN.matcher(sha).matches(),
|
|
"Not a valid complete SHA-1: %s", sha);
|
|
checkCondition(!Strings.isNullOrEmpty(description), "description cannot be empty");
|
|
checkCondition(!Strings.isNullOrEmpty(context), "context cannot be empty");
|
|
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).createStatus(
|
|
project, sha, new CreateStatusRequest(State.valueOf(state.toUpperCase()),
|
|
convertFromNoneable(targetUrl, null),
|
|
description, context));
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling create_status: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_check_runs",
|
|
doc =
|
|
"Get the list of check runs for a sha. "
|
|
+ "https://developer.github.com/v3/checks/runs/#check-runs",
|
|
parameters = {
|
|
@Param(
|
|
name = "sha",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "The SHA-1 for which we want to get the check runs"),
|
|
})
|
|
public CheckRuns getCheckRuns(String sha) throws EvalException {
|
|
try {
|
|
checkCondition(GitRevision.COMPLETE_SHA1_PATTERN.matcher(sha).matches(),
|
|
"Not a valid complete SHA-1: %s", sha);
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).getCheckRuns(project, sha);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_check_runs: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_combined_status",
|
|
doc = "Get the combined status for a commit. Returns None if not found.",
|
|
parameters = {
|
|
@Param(
|
|
name = "ref",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "The SHA-1 or ref for which we want to get the combined status"),
|
|
},
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public CombinedStatus getCombinedStatus(String ref) throws EvalException {
|
|
try {
|
|
checkCondition(!Strings.isNullOrEmpty(ref), "Empty reference not allowed");
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).getCombinedStatus(project, ref);
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_combined_status: %s", e);
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_commit",
|
|
doc = "Get information for a commit in GitHub. Returns None if not found.",
|
|
parameters = {
|
|
@Param(
|
|
name = "ref",
|
|
type = String.class,
|
|
named = true,
|
|
// Works for refs too but we don't want to publicize since GH API docs refers to sha
|
|
doc = "The SHA-1 for which we want to get the combined status"),
|
|
},
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public GitHubCommit getCommit(String ref) throws EvalException {
|
|
try {
|
|
checkCondition(!Strings.isNullOrEmpty(ref), "Empty reference not allowed");
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).getCommit(project, ref);
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_commit: %s", e);
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "update_reference",
|
|
doc = "Update a reference to point to a new commit. Returns the info of the reference.",
|
|
parameters = {
|
|
@Param(name = "ref", type = String.class, named = true, doc = "The name of the reference."),
|
|
@Param(
|
|
name = "sha",
|
|
type = String.class,
|
|
doc = "The id for the commit" + " status.",
|
|
named = true),
|
|
@Param(
|
|
name = "force",
|
|
type = Boolean.class,
|
|
named = true,
|
|
doc =
|
|
"Indicates whether to force the update or to make sure the update is a"
|
|
+ " fast-forward update. Leaving this out or setting it to false will make"
|
|
+ " sure you're not overwriting work. Default: false")
|
|
})
|
|
public Ref updateReference(String sha, String ref, boolean force) throws EvalException {
|
|
try {
|
|
checkCondition(GitRevision.COMPLETE_SHA1_PATTERN.matcher(sha).matches(),
|
|
"Not a valid complete SHA-1: %s", sha);
|
|
checkCondition(!Strings.isNullOrEmpty(ref), "ref cannot be empty");
|
|
|
|
if (!ref.startsWith("refs/")) {
|
|
// TODO(malcon): Remove this functionality and use a check once library migrated.
|
|
console.warnFmt(
|
|
"Non-complete ref passed to update_reference '%s'. Assuming refs/heads/%s", ref, ref);
|
|
ref = "refs/heads/" + ref;
|
|
}
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).updateReference(
|
|
project, ref, new UpdateReferenceRequest(sha, force));
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling update_reference: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "delete_reference",
|
|
doc = "Delete a reference.",
|
|
parameters = {
|
|
@Param(name = "ref", type = String.class, named = true, doc = "The name of the reference."),
|
|
})
|
|
public void deleteReference(String ref) throws EvalException {
|
|
try {
|
|
checkCondition(!Strings.isNullOrEmpty(ref), "ref cannot be empty");
|
|
checkCondition(ref.startsWith("refs/"), "ref needs to be a complete reference."
|
|
+ " Example: refs/heads/foo");
|
|
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
apiSupplier.load(console).deleteReference(project, ref);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling delete_reference: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_reference",
|
|
doc = "Get a reference SHA-1 from GitHub. Returns None if not found.",
|
|
parameters = {
|
|
@Param(
|
|
name = "ref",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "The name of the reference. For example: \"refs/heads/branchName\".")
|
|
},
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public Ref getReference(String ref) throws EvalException {
|
|
try {
|
|
checkCondition(!Strings.isNullOrEmpty(ref), "Ref cannot be empty");
|
|
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).getReference(project, ref);
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_reference: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_pull_requests",
|
|
doc = "Get Pull Requests for a repo",
|
|
parameters = {
|
|
@Param(
|
|
name = "head_prefix",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "Only return PRs wher the branch name has head_prefix",
|
|
defaultValue = "None",
|
|
noneable = true),
|
|
@Param(
|
|
name = "base_prefix",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "Only return PRs where the destination branch name has base_prefix",
|
|
defaultValue = "None",
|
|
noneable = true),
|
|
@Param(
|
|
name = "state",
|
|
type = String.class,
|
|
doc = "State of the Pull Request. Can be `\"OPEN\"`, `\"CLOSED\"` or `\"ALL\"`",
|
|
defaultValue = "\"OPEN\"",
|
|
named = true),
|
|
@Param(
|
|
name = "sort",
|
|
type = String.class,
|
|
doc =
|
|
"Sort filter for retrieving the Pull Requests. Can be `\"CREATED\"`,"
|
|
+ " `\"UPDATED\"` or `\"POPULARITY\"`",
|
|
named = true,
|
|
defaultValue = "\"CREATED\""),
|
|
@Param(
|
|
name = "direction",
|
|
type = String.class,
|
|
doc = "Direction of the filter. Can be `\"ASC\"` or `\"DESC\"`",
|
|
defaultValue = "\"ASC\"",
|
|
named = true)
|
|
},
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public ImmutableList<PullRequest> getPullRequests(
|
|
Object headPrefixParam, Object basePrefixParam, String state, String sort, String direction)
|
|
throws EvalException {
|
|
try {
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
PullRequestListParams request = PullRequestListParams.DEFAULT;
|
|
String headPrefix = convertFromNoneable(headPrefixParam, null);
|
|
String basePrefix = convertFromNoneable(basePrefixParam, null);
|
|
if (!Strings.isNullOrEmpty(headPrefix)) {
|
|
checkCondition(SAFE_BRANCH_NAME_PREFIX.matches(headPrefix),
|
|
"'%s' is not a valid head_prefix (%s is used for validation)",
|
|
headPrefix, SAFE_BRANCH_NAME_PREFIX.pattern());
|
|
request = request.withHead(headPrefix);
|
|
}
|
|
if (!Strings.isNullOrEmpty(basePrefix)) {
|
|
checkCondition(SAFE_BRANCH_NAME_PREFIX.matches(basePrefix),
|
|
"'%s' is not a valid base_prefix (%s is used for validation)",
|
|
basePrefix, SAFE_BRANCH_NAME_PREFIX.pattern());
|
|
request = request.withHead(basePrefix);
|
|
}
|
|
|
|
return apiSupplier
|
|
.load(console)
|
|
.getPullRequests(
|
|
project,
|
|
request
|
|
.withState(stringToEnum("state", state, StateFilter.class))
|
|
.withDirection(stringToEnum("direction", direction, DirectionFilter.class))
|
|
.withSort(stringToEnum("sort", sort, SortFilter.class)));
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_pull_requests: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "update_pull_request",
|
|
doc = "Update Pull Requests for a repo. Returns None if not found",
|
|
parameters = {
|
|
@Param(name = "number", type = Integer.class, named = true, doc = "Pull Request number"),
|
|
@Param(
|
|
name = "title",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "New Pull Request title",
|
|
defaultValue = "None",
|
|
noneable = true),
|
|
@Param(
|
|
name = "body",
|
|
type = String.class,
|
|
named = true,
|
|
doc = "New Pull Request body",
|
|
defaultValue = "None",
|
|
noneable = true),
|
|
@Param(
|
|
name = "state",
|
|
type = String.class,
|
|
doc = "State of the Pull Request. Can be `\"OPEN\"`, `\"CLOSED\"`",
|
|
named = true,
|
|
noneable = true,
|
|
defaultValue = "None"),
|
|
},
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public PullRequest updatePullRequest(Integer number, Object title, Object body, Object state)
|
|
throws EvalException {
|
|
try {
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
|
|
return apiSupplier
|
|
.load(console)
|
|
.updatePullRequest(
|
|
project,
|
|
number,
|
|
new UpdatePullRequest(
|
|
convertFromNoneable(title, null),
|
|
convertFromNoneable(body, null),
|
|
stringToEnum(
|
|
"state", convertFromNoneable(state, null), UpdatePullRequest.State.class)));
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling update_pull_request: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_authenticated_user",
|
|
doc = "Get autenticated user info, return null if not found",
|
|
allowReturnNones = true)
|
|
@Nullable
|
|
public User getAuthenticatedUser() throws EvalException {
|
|
try {
|
|
return apiSupplier.load(console).getAuthenticatedUser();
|
|
} catch (GitHubApiException e) {
|
|
return returnNullOnNotFound(e);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_authenticated_user: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private <T> T returnNullOnNotFound(GitHubApiException e) throws EvalException {
|
|
SkylarkUtil.check(e.getResponseCode() == ResponseCode.NOT_FOUND, e.getMessage());
|
|
return null;
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_references",
|
|
doc =
|
|
"Get all the reference SHA-1s from GitHub. Note that Copybara only returns a maximum "
|
|
+ "number of 500.")
|
|
public Sequence<Ref> getReferences() throws EvalException {
|
|
try {
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return StarlarkList.immutableCopyOf(apiSupplier.load(console).getReferences(project));
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_references: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_pull_request_comment",
|
|
doc = "Get a pull request comment",
|
|
parameters = {
|
|
@Param(name = "comment_id", type = String.class, named = true, doc = "Comment identifier"),
|
|
})
|
|
public PullRequestComment getPullRequestComment(String commentId) throws EvalException {
|
|
try {
|
|
long commentIdLong;
|
|
try {
|
|
commentIdLong = Long.parseLong(commentId);
|
|
} catch (NumberFormatException e) {
|
|
throw Starlark.errorf("Invalid comment id %s: %s", commentId, e.getMessage());
|
|
}
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return apiSupplier.load(console).getPullRequestComment(project, commentIdLong);
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_pull_request_comment: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "get_pull_request_comments",
|
|
doc = "Get all pull request comments",
|
|
parameters = {
|
|
@Param(name = "number", type = Integer.class, named = true, doc = "Pull Request number"),
|
|
})
|
|
public Sequence<PullRequestComment> getPullRequestComments(Integer prNumber)
|
|
throws EvalException {
|
|
try {
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
return StarlarkList.immutableCopyOf(
|
|
apiSupplier.load(console).getPullRequestComments(project, prNumber));
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling get_pull_request_comments: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "url",
|
|
doc = "Return the URL of this endpoint.",
|
|
structField = true)
|
|
public String getUrl() {
|
|
return url;
|
|
}
|
|
|
|
@StarlarkMethod(
|
|
name = "add_label",
|
|
doc = "Add labels to a PR/issue",
|
|
parameters = {
|
|
@Param(name = "number", type = Integer.class, named = true, doc = "Pull Request number"),
|
|
@Param(name = "labels", type = Sequence.class, generic1 = String.class, named = true,
|
|
doc = "List of labels to add."),
|
|
})
|
|
public void addLabels(Integer prNumber, Sequence<?> labels) throws EvalException {
|
|
try {
|
|
String project = GitHubUtil.getProjectNameFromUrl(url);
|
|
// Swallow response, until a use-case for returning it surfaces.
|
|
apiSupplier.load(console).addLabels(project, prNumber,
|
|
SkylarkUtil.convertStringList(labels, "Expected list of GitHub label names."));
|
|
} catch (RepoException | ValidationException | RuntimeException e) {
|
|
throw Starlark.errorf("Error calling add_label: %s", e.getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
public GitHubEndPoint withConsole(Console console) {
|
|
return new GitHubEndPoint(this.apiSupplier, this.url, console);
|
|
}
|
|
|
|
@Override
|
|
public ImmutableSetMultimap<String, String> describe() {
|
|
return ImmutableSetMultimap.of("type", "github_api", "url", url);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return MoreObjects.toStringHelper(this)
|
|
.add("url", url)
|
|
.toString();
|
|
}
|
|
}
|