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"])
-
-