diff --git a/docs/reference.md b/docs/reference.md index c77d7f39a..937258ceb 100755 --- a/docs/reference.md +++ b/docs/reference.md @@ -2131,7 +2131,7 @@ version_selector | `latestVersionSelector`

Select a custom version (tag)to Creates changes in a new pull request in the destination. -`gitHubPrDestination git.github_pr_destination(url, destination_ref="master", pr_branch=None, title=None, body=None, integrates=None, api_checker=None, update_description=False)` +`gitHubPrDestination git.github_pr_destination(url, destination_ref="master", pr_destination_url=None, pr_branch=None, title=None, body=None, integrates=None, api_checker=None, update_description=False)` #### Parameters: @@ -2140,6 +2140,7 @@ Parameter | Description --------- | ----------- url | `string`

Url of the GitHub project. For example "https://github.com/google/copybara'"

destination_ref | `string`

Destination reference for the change. By default 'master'

+pr_destination_url | `string`

Url of the GitHub project to create the PullRequest on. Set this if you want to push to a personal fork and create the PullRequest on the upstream project (e.g. because you don't have write access to the upstream repo).By default, `pr_destination_url` is the same as `url`.

pr_branch | `string`

Customize the pull request branch. Any variable present in the message in the form of ${CONTEXT_REFERENCE} will be replaced by the corresponding stable reference (head, PR number, Gerrit change number, etc.).

title | `string`

When creating (or updating if `update_description` is set) a pull request, use this title. By default it uses the change first line. This field accepts a template with labels. For example: `"Change ${CONTEXT_REFERENCE}"`

body | `string`

When creating (or updating if `update_description` is set) a pull request, use this body. By default it uses the change summary. This field accepts a template with labels. For example: `"Change ${CONTEXT_REFERENCE}"`

diff --git a/java/com/google/copybara/git/GitHubPrDestination.java b/java/com/google/copybara/git/GitHubPrDestination.java index 633a2d2c8..d0a0b5f23 100644 --- a/java/com/google/copybara/git/GitHubPrDestination.java +++ b/java/com/google/copybara/git/GitHubPrDestination.java @@ -54,6 +54,7 @@ import com.google.copybara.util.Identity; import com.google.copybara.util.console.Console; import java.io.IOException; +import java.util.Optional; import java.util.UUID; import javax.annotation.Nullable; @@ -64,6 +65,7 @@ public class GitHubPrDestination implements Destination { private final String url; private final String destinationRef; + private final Optional prDestinationUrl; private final String prBranch; private final GeneralOptions generalOptions; private final GitHubOptions gitHubOptions; @@ -82,6 +84,7 @@ public class GitHubPrDestination implements Destination { GitHubPrDestination( String url, String destinationRef, + Optional prDestinationUrl, @Nullable String prBranch, GeneralOptions generalOptions, GitHubOptions gitHubOptions, @@ -97,6 +100,7 @@ public class GitHubPrDestination implements Destination { boolean updateDescription) { this.url = Preconditions.checkNotNull(url); this.destinationRef = Preconditions.checkNotNull(destinationRef); + this.prDestinationUrl = Preconditions.checkNotNull(prDestinationUrl); this.prBranch = prBranch; this.generalOptions = Preconditions.checkNotNull(generalOptions); this.gitHubOptions = Preconditions.checkNotNull(gitHubOptions); @@ -176,6 +180,12 @@ public ImmutableList write( return result.build(); } + String prBranch = + getQualifiedPullRequestBranchName( + writerContext.getOriginalRevision(), + writerContext.getWorkflowName(), + writerContext.getWorkflowIdentityUser()); + if (!gitHubDestinationOptions.createPullRequest) { console.infoFmt( "Please create a PR manually following this link: %s/compare/%s...%s" @@ -188,9 +198,7 @@ public ImmutableList write( GitHubApi api = gitHubOptions.newGitHubApi(getProjectName()); ImmutableList pullRequests = api.getPullRequests( - getProjectName(), - PullRequestListParams.DEFAULT - .withHead(String.format("%s:%s", getUserNameFromUrl(url), prBranch))); + getProjectName(), PullRequestListParams.DEFAULT.withHead(prBranch)); ChangeMessage msg = ChangeMessage.parseMessage(transformResult.getSummary().trim()); @@ -275,7 +283,7 @@ private String asHttpsUrl() throws ValidationException { @VisibleForTesting String getProjectName() throws ValidationException { - return GitHubUtil.getProjectNameFromUrl(url); + return GitHubUtil.getProjectNameFromUrl(prDestinationUrl.orElse(url)); } @VisibleForTesting @@ -288,6 +296,16 @@ public Iterable getIntegrates() { return integrates; } + private String getQualifiedPullRequestBranchName( + @Nullable Revision changeRevision, String workflowName, String workflowIdentityUser) + throws ValidationException { + String branch = getPullRequestBranchName(changeRevision, workflowName, workflowIdentityUser); + if (prDestinationUrl.isPresent()) { + return String.format("%s:%s", getUserNameFromUrl(url), branch); + } + return branch; + } + private String getPullRequestBranchName( @Nullable Revision changeRevision, String workflowName, String workflowIdentityUser) throws ValidationException { diff --git a/java/com/google/copybara/git/GitModule.java b/java/com/google/copybara/git/GitModule.java index 0d5642347..679a34878 100644 --- a/java/com/google/copybara/git/GitModule.java +++ b/java/com/google/copybara/git/GitModule.java @@ -89,6 +89,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -1199,6 +1200,18 @@ public GitDestination gitHubDestination( named = true, doc = "Destination reference for the change. By default 'master'", defaultValue = "\"master\""), + @Param( + name = "pr_destination_url", + type = String.class, + defaultValue = "None", + noneable = true, + named = true, + positional = false, + doc = + "Url of the GitHub project to create the PullRequest on. Set this if you want to " + + "push to a personal fork and create the PullRequest on the upstream project " + + "(e.g. because you don't have write access to the upstream repo)." + + "By default, `pr_destination_url` is the same as `url`."), @Param( name = "pr_branch", type = String.class, @@ -1297,6 +1310,7 @@ public GitDestination gitHubDestination( public GitHubPrDestination githubPrDestination( String url, String destinationRef, + Object prDestinationUrl, Object prBranch, Object title, Object body, @@ -1309,12 +1323,20 @@ public GitHubPrDestination githubPrDestination( // This restricts to github.com, we will have to revisit this to support setups like GitHub // Enterprise. check(GitHubUtil.isGitHubUrl(url), "'%s' is not a valid GitHub url", url); + check(prDestinationUrl == Starlark.NONE || GitHubUtil.isGitHubUrl((String)prDestinationUrl), + "'%s' is not a valid GitHub url", prDestinationUrl); GitDestinationOptions destinationOptions = options.get(GitDestinationOptions.class); return new GitHubPrDestination( fixHttp( checkNotEmpty(firstNotNull(destinationOptions.url, url), "url"), thread.getCallerLocation()), destinationRef, + Optional.ofNullable( + prDestinationUrl == Starlark.NONE ? + null : + fixHttp( + checkNotEmpty((String)prDestinationUrl, "pr_destination_url"), + thread.getCallerLocation())), convertFromNoneable(prBranch, null), generalOptions, options.get(GitHubOptions.class), diff --git a/javatests/com/google/copybara/git/GitHubPrDestinationTest.java b/javatests/com/google/copybara/git/GitHubPrDestinationTest.java index 3550f0349..5184c94d8 100644 --- a/javatests/com/google/copybara/git/GitHubPrDestinationTest.java +++ b/javatests/com/google/copybara/git/GitHubPrDestinationTest.java @@ -438,6 +438,45 @@ public void testFindProject() throws ValidationException { .onceInLog(MessageType.ERROR, ".*'https://github.com' is not a valid GitHub url.*"); } + private void checkFindProject(String url, String project) throws ValidationException { + GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" + + " url = '" + url + "'," + + " destination_ref = 'other'," + + ")"); + + assertThat(d.getProjectName()).isEqualTo(project); + } + + @Test + public void testFindUpstreamProject() throws ValidationException { + checkFindProjectFromUpstreamUrl("https://github.com/foo", "foo"); + checkFindProjectFromUpstreamUrl("https://github.com/foo/bar", "foo/bar"); + checkFindProjectFromUpstreamUrl("https://github.com/foo.git", "foo"); + checkFindProjectFromUpstreamUrl("https://github.com/foo/", "foo"); + checkFindProjectFromUpstreamUrl("git+https://github.com/foo", "foo"); + checkFindProjectFromUpstreamUrl("git@github.com/foo", "foo"); + checkFindProjectFromUpstreamUrl( + "git@github.com:org/internal_repo_name.git", "org/internal_repo_name"); + ValidationException e = + assertThrows( + ValidationException.class, + () -> checkFindProjectFromUpstreamUrl("https://github.com", "foo")); + console + .assertThat() + .onceInLog(MessageType.ERROR, ".*'https://github.com' is not a valid GitHub url.*"); + } + + private void checkFindProjectFromUpstreamUrl( + String url, String project) throws ValidationException { + GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" + + " url = 'https://github.com/google/copybara'," + + " pr_destination_url = '" + url + "'," + + " destination_ref = 'other'," + + ")"); + + assertThat(d.getProjectName()).isEqualTo(project); + } + @Test public void testIntegratesCanBeRemoved() throws Exception { GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" @@ -464,15 +503,6 @@ public void testIntegratesCanBeRemoved() throws Exception { assertThat(d.getIntegrates()).isEmpty(); } - private void checkFindProject(String url, String project) throws ValidationException { - GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" - + " url = '" + url + "'," - + " destination_ref = 'other'," - + ")"); - - assertThat(d.getProjectName()).isEqualTo(project); - } - @Test public void testWriteNoMaster() throws ValidationException, IOException, RepoException { GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" @@ -530,6 +560,62 @@ public void testWriteNoMaster() throws ValidationException, IOException, RepoExc + "DummyOrigin-RevId: one\n"); } + @Test + public void testPrDestinationUrl() throws ValidationException, IOException, RepoException { + GitHubPrDestination d = skylark.eval("r", "r = git.github_pr_destination(" + + " url = 'https://github.com/foo'," + + " pr_destination_url = 'https://github.com/bar'," + + ")"); + DummyRevision dummyRevision = new DummyRevision("dummyReference", "feature"); + WriterContext writerContext = + new WriterContext("piper_to_github_pr", "TEST", false, dummyRevision, + Glob.ALL_FILES.roots()); + String branchName = + Identity.computeIdentity( + "OriginGroupIdentity", + dummyRevision.contextReference(), + writerContext.getWorkflowName(), + "copy.bara.sky", + writerContext.getWorkflowIdentityUser()); + + gitUtil.mockApi( + "GET", + "https://api.github.com/repos/bar/pulls?per_page=100&head=foo:" + branchName, + mockResponse("[]")); + gitUtil.mockApi( + "POST", + "https://api.github.com/repos/bar/pulls", + mockResponseAndValidateRequest( + "{\n" + + " \"id\": 1,\n" + + " \"number\": 12345,\n" + + " \"state\": \"open\",\n" + + " \"title\": \"test summary\",\n" + + " \"body\": \"test summary\"" + + "}", + req -> + req.equals( + "{\"base\":\"master\",\"body\":\"test summary\\n\",\"head\":\"" + + branchName + + "\",\"title\":\"test summary\"}"))); + + Writer writer = d.newWriter(writerContext); + GitRepository remote = gitUtil.mockRemoteRepo("github.com/foo"); + addFiles(remote, "master", "first change", ImmutableMap.builder() + .put("foo.txt", "").build()); + + writeFile(this.workdir, "test.txt", "some content"); + writer.write( + TransformResults.of(this.workdir, new DummyRevision("one")), Glob.ALL_FILES, console); + + assertThat(remote.refExists(branchName)).isTrue(); + assertThat(Iterables.transform(remote.log(branchName).run(), GitLogEntry::getBody)) + .containsExactly("first change\n", + "test summary\n" + + "\n" + + "DummyOrigin-RevId: one\n"); + } + @Test public void testBranchNameFromUserWithLabel() throws ValidationException, IOException, RepoException { testBranchNameFromUser("test${CONTEXT_REFERENCE}", "test_feature", "&feature"); diff --git a/third_party/BUILD b/third_party/BUILD index aff8566c7..d456baa8d 100644 --- a/third_party/BUILD +++ b/third_party/BUILD @@ -15,7 +15,7 @@ package( java_library( name = "guava", exports = [ - "@maven//:com_google_guava_failureaccess", + "@maven//:com_google_guava_failureaccess", "@maven//:com_google_guava_guava", ], ) @@ -95,18 +95,18 @@ java_library( name = "truth", testonly = 1, exports = [ - "@maven//:com_googlecode_java_diff_utils_diffutils", "@maven//:com_google_truth_truth", + "@maven//:com_googlecode_java_diff_utils_diffutils", ], ) java_library( name = "google_http_client", exports = [ + "@maven//:com_google_code_gson_gson", "@maven//:com_google_http_client_google_http_client", "@maven//:com_google_http_client_google_http_client_gson", - "@maven//:com_google_code_gson_gson", - "@maven//:commons_codec_commons_codec", + "@maven//:commons_codec_commons_codec", ], ) @@ -152,8 +152,8 @@ java_library( "@maven//:com_google_flogger_flogger", ], runtime_deps = [ - "@maven//:com_google_flogger_flogger_system_backend" - ] + "@maven//:com_google_flogger_flogger_system_backend", + ], ) java_library( @@ -161,11 +161,9 @@ java_library( testonly = 1, exports = [ "//java/com/google/copybara/hg/testing", - ] + ], ) # Required temporarily until @io_bazel//src/main/java/com/google/devtools/build/lib/syntax/... # is fixed exports_files(["bazel.patch"]) - -