/* * Copyright (C) 2017 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.common.truth.Truth.assertThat; import static com.google.copybara.git.GitHubPROrigin.GITHUB_BASE_BRANCH; import static com.google.copybara.git.GitHubPROrigin.GITHUB_BASE_BRANCH_SHA1; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_ASSIGNEE; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_BODY; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_HEAD_SHA; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_NUMBER_LABEL; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_TITLE; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_URL; import static com.google.copybara.git.GitHubPROrigin.GITHUB_PR_USER; import static com.google.copybara.testing.git.GitTestUtil.getGitEnv; import static com.google.copybara.testing.git.GitTestUtil.mockResponse; import static com.google.copybara.testing.git.GitTestUtil.mockResponseAndValidateRequest; import static com.google.copybara.util.CommandRunner.DEFAULT_TIMEOUT; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.gson.GsonFactory; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.copybara.Change; import com.google.copybara.Origin.Baseline; import com.google.copybara.Origin.Reader; import com.google.copybara.Workflow; import com.google.copybara.authoring.Author; import com.google.copybara.authoring.Authoring; import com.google.copybara.authoring.Authoring.AuthoringMappingMode; import com.google.copybara.config.MapConfigFile; import com.google.copybara.exception.CannotResolveRevisionException; import com.google.copybara.exception.EmptyChangeException; import com.google.copybara.exception.RepoException; import com.google.copybara.exception.ValidationException; import com.google.copybara.git.github.util.GitHubUtil; import com.google.copybara.testing.FileSubjects; import com.google.copybara.testing.OptionsBuilder; import com.google.copybara.testing.SkylarkTestExecutor; import com.google.copybara.testing.git.GitTestUtil; import com.google.copybara.testing.git.GitTestUtil.CompleteRefValidator; import com.google.copybara.testing.git.GitTestUtil.MockRequestAssertion; import com.google.copybara.util.Glob; import com.google.copybara.util.console.testing.TestingConsole; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class GitHubPrOriginTest { private Path repoGitDir; private OptionsBuilder options; private TestingConsole console; private SkylarkTestExecutor skylark; private final Authoring authoring = new Authoring(new Author("foo", "[email protected]"), AuthoringMappingMode.PASS_THRU, ImmutableSet.of()); private Path workdir; private GitTestUtil gitUtil; @Before public void setup() throws Exception { repoGitDir = Files.createTempDirectory("GitHubPrDestinationTest-repoGitDir"); workdir = Files.createTempDirectory("workdir"); git("init", "--bare", repoGitDir.toString()); console = new TestingConsole(); options = new OptionsBuilder() .setConsole(console) .setOutputRootToTmpDir(); gitUtil = new GitTestUtil(options); gitUtil.mockRemoteGitRepos(new CompleteRefValidator()); Path credentialsFile = Files.createTempFile("credentials", "test"); Files.write(credentialsFile, "https://user:[email protected]".getBytes(UTF_8)); options.git.credentialHelperStorePath = credentialsFile.toString(); skylark = new SkylarkTestExecutor(options); } private String git(String... argv) throws RepoException { return repo() .git(repoGitDir, argv) .getStdout(); } private GitRepository repo() { return repoForPath(repoGitDir); } private GitRepository repoForPath(Path path) { return GitRepository .newBareRepo(path, getGitEnv(), /*verbose=*/true, DEFAULT_TIMEOUT, /*noVerify*/ false); } @Test public void testNoCommandLineReference() throws Exception { ValidationException thrown = assertThrows( ValidationException.class, () -> githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']") .resolve(null)); assertThat(thrown).hasMessageThat().contains("A pull request reference is expected"); } @Test public void testGitResolvePullRequest() throws Exception { mockPullRequestAndIssue(123, "open", "foo: yes", "bar: yes"); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']"), "https://github.com/google/example/pull/123", 123); } @Test public void testGitResolveWithGitDescribe() throws Exception { mockPullRequestAndIssue(123, "open", "foo: yes", "bar: yes"); GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); addFiles(remote, "first change", ImmutableMap.<String, String>builder() .put(123 + ".txt", "").build()); String sha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), sha1); remote.simpleCommand("tag", "-m", "This is a tag", "1.0"); GitRevision rev = githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']") .resolve("https://github.com/google/example/pull/123"); assertThat(rev.associatedLabels().get("GIT_DESCRIBE_REQUESTED_VERSION")).containsExactly("1.0"); } @Test public void testGitResolvePullRequestNumber() throws Exception { mockPullRequestAndIssue(123, "open", "foo: yes", "bar: yes"); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']"), "123", 123); } @Test public void testEmptyUrl() { skylark.evalFails("git.github_pr_origin( url = '')", "Invalid empty field 'url'"); } @Test public void testGitResolvePullRequestRawRef() throws Exception { mockPullRequestAndIssue(123, "open", "foo: yes", "bar: yes"); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']"), "refs/pull/123/head", 123); } @Test public void testGitResolveSha1() throws Exception { mockPullRequestAndIssue(123, "open"); GitHubPROrigin origin = githubPrOrigin( "url = 'https://github.com/google/example'"); checkResolve(origin, "refs/pull/123/head", 123); // Test that we can resolve SHA-1 as long as they were fetched by the PR + base branch fetch. String sha1 = gitUtil.mockRemoteRepo("github.com/google/example").parseRef("HEAD"); GitRevision rev = origin .resolve(sha1 + " not important review data"); assertThat(rev.getSha1()).isEqualTo(sha1); } @Test public void testGitResolveNoLabelsRequired() throws Exception { mockPullRequestAndIssue(125, "open", "bar: yes"); checkResolve( githubPrOrigin("url = 'https://github.com/google/example'", "required_labels = []"), "125", 125); mockPullRequestAndIssue(126, "open"); checkResolve( githubPrOrigin("url = 'https://github.com/google/example'", "required_labels = []"), "126", 126); } @Test public void testGitResolveRequiredLabelsNotFound() throws Exception { mockPullRequestAndIssue(125, "open", "bar: yes"); EmptyChangeException thrown = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']"), "125", 125)); assertThat(thrown) .hasMessageThat() .contains( "Cannot migrate http://github.com/google/example/pull/125 because it is missing the" + " following labels: [foo: yes]"); } @Test public void testGitResolveRequiredLabelsNotFound_forceMigrate() throws Exception { options.githubPrOrigin.forceImport = true; mockPullRequestAndIssue(125, "open", "bar: yes"); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']"), "125", 125); } @Test public void testLimitByBranch() throws Exception { // This should work since it returns a PR for master. mockPullRequestAndIssue(125, "open", "bar: yes"); checkResolve( githubPrOrigin("url = 'https://github.com/google/example'", "branch = 'master'"), "125", 125); mockPullRequestAndIssue(126, "open", "bar: yes"); EmptyChangeException e = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin("url = 'https://github.com/google/example'", "branch = 'other'"), "126", 126)); assertThat(e) .hasMessageThat() .contains( "because its base branch is 'master', but the workflow is configured to only migrate" + " changes for branch 'other'"); } @Test public void testGitResolveRequiredLabelsRetried() throws Exception { mockPullRequest(125, "open"); mockIssue( 125, issueResponse(125, "open"), issueResponse(125, "open", "foo: yes"), issueResponse(125, "open", "foo: yes", "bar: yes")); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']", "retryable_labels = ['foo: yes', 'bar: yes']"), "125", 125); verify(gitUtil.httpTransport(), times(3)) .buildRequest("GET", "https://api.github.com/repos/google/example/issues/125"); } @Test public void testGitResolveRequiredLabelsNotRetryable() throws Exception { mockPullRequestAndIssue(125, "open"); EmptyChangeException thrown = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes']"), "125", 125)); assertThat(thrown) .hasMessageThat() .contains( "Cannot migrate http://github.com/google/example/pull/125 because it is missing the" + " following labels: [foo: yes]"); } @Test public void testAlreadyClosed_default() throws Exception { mockPullRequestAndIssue(125, "closed", "foo: yes"); EmptyChangeException thrown = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin("url = 'https://github.com/google/example'"), "125", 125)); assertThat(thrown).hasMessageThat().contains("Pull Request 125 is not open"); } @Test public void testAlreadyClosed_only_open() throws Exception { mockPullRequestAndIssue(125, "closed", "foo: yes"); EmptyChangeException thrown = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin("url = 'https://github.com/google/example', state = 'OPEN'"), "125", 125)); assertThat(thrown).hasMessageThat().contains("Pull Request 125 is not open"); } @Test public void testAlreadyClosed_only_open_forceMigration() throws Exception { options.githubPrOrigin.forceImport = true; mockPullRequestAndIssue(125, "closed", "foo: yes"); checkResolve( githubPrOrigin("url = 'https://github.com/google/example', state = 'OPEN'"), "125", 125); } @Test public void testAlreadyClosed_only_closed() throws Exception { mockPullRequestAndIssue(125, "open", "foo: yes"); EmptyChangeException thrown = assertThrows( EmptyChangeException.class, () -> checkResolve( githubPrOrigin("url = 'https://github.com/google/example', state = 'CLOSED'"), "125", 125)); assertThat(thrown).hasMessageThat().contains("Pull Request 125 is open"); } @Test public void testGitResolveRequiredLabelsMixed() throws Exception { mockPullRequestAndIssue(125, "open", "foo: yes", "bar: yes"); checkResolve( githubPrOrigin( "url = 'https://github.com/google/example'", "required_labels = ['foo: yes', 'bar: yes']", "retryable_labels = ['foo: yes']"), "125", 125); } @Test public void testGitResolveInvalidReference() throws Exception { ValidationException thrown = assertThrows( ValidationException.class, () -> checkResolve( githubPrOrigin("url = 'https://github.com/google/example'"), "master", 125)); assertThat(thrown) .hasMessageThat() .contains("'master' is not a valid reference for a GitHub Pull Request"); } @Test public void testChanges() throws Exception { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); addFiles(remote, "base", ImmutableMap.<String, String>builder() .put("test.txt", "a").build()); String base = remote.parseRef("HEAD"); addFiles(remote, "one", ImmutableMap.<String, String>builder() .put("test.txt", "b").build()); addFiles(remote, "two", ImmutableMap.<String, String>builder() .put("test.txt", "c").build()); String prHeadSha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), prHeadSha1); withTmpWorktree(remote).simpleCommand("reset", "--hard", "HEAD~2"); // master = base commit. addFiles(remote, "master change", ImmutableMap.<String, String>builder() .put("other.txt", "").build()); remote.simpleCommand("update-ref", GitHubUtil.asMergeRef(123), remote.parseRef("HEAD")); mockPullRequestAndIssue(123, "open"); GitHubPROrigin origin = githubPrOrigin( "url = 'https://github.com/google/example'"); Reader<GitRevision> reader = origin.newReader(Glob.ALL_FILES, authoring); GitRevision prHead = origin.resolve("123"); assertThat(prHead.getSha1()).isEqualTo(prHeadSha1); ImmutableList<Change<GitRevision>> changes = reader.changes(origin.resolve(base), prHead).getChanges(); assertThat(Lists.transform(changes, Change::getMessage)) .isEqualTo(Lists.newArrayList("one\n", "two\n")); // Non-found baseline. We return all the changes between baseline and PR head. changes = reader.changes(origin.resolve(remote.parseRef("HEAD")), prHead).getChanges(); // Even if the PR is outdated it should return only the changes in the PR by finding the // common ancestor. assertThat(Lists.transform(changes, Change::getMessage)) .isEqualTo(Lists.newArrayList("one\n", "two\n")); assertThat(changes.stream() .map(c -> c.getRevision().getUrl()) .allMatch(c -> c.startsWith("https://github.com/"))) .isTrue(); } @Test public void testCheckout() throws Exception { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); String baseline1 = addFiles(remote, "base", ImmutableMap.<String, String>builder() .put("test.txt", "a").build()); addFiles(remote, "one", ImmutableMap.<String, String>builder() .put("test.txt", "b").build()); addFiles(remote, "two", ImmutableMap.<String, String>builder() .put("test.txt", "c").build()); String prHeadSha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), prHeadSha1); withTmpWorktree(remote).simpleCommand("reset", "--hard", "HEAD~2"); // master = base commit. String baselineMerge = addFiles(remote, "master change", ImmutableMap.<String, String>builder() .put("other.txt", "").build()); remote.simpleCommand("update-ref", GitHubUtil.asMergeRef(123), remote.parseRef("HEAD")); mockPullRequestAndIssue(123, "open"); GitHubPROrigin origin = githubPrOrigin( "url = 'https://github.com/google/example'", "baseline_from_branch = True"); GitRevision headPrRevision = origin.resolve("123"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_BASE_BRANCH, "master"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_BASE_BRANCH_SHA1, baseline1); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_NUMBER_LABEL, "123"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_TITLE, "test summary"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_USER, "some_user"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_ASSIGNEE, "assignee1"); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_ASSIGNEE, "assignee2"); assertThat(headPrRevision.associatedLabels()) .containsEntry(GITHUB_PR_HEAD_SHA, headPrRevision.getSha1()); assertThat(headPrRevision.associatedLabels()).containsEntry(GITHUB_PR_BODY, "test summary\n\nMore text"); Reader<GitRevision> reader = origin.newReader(Glob.ALL_FILES, authoring); Optional<Baseline<GitRevision>> baselineObj = reader.findBaseline(headPrRevision, "RevId"); assertThat(baselineObj.isPresent()).isTrue(); assertThat(baselineObj.get().getBaseline()) .isEqualTo(baselineObj.get().getOriginRevision().getSha1()); assertThat(baselineObj.get().getBaseline()).isEqualTo(baseline1); assertThat( reader .changes(baselineObj.get().getOriginRevision(), headPrRevision).getChanges() .size()) .isEqualTo(2); assertThat(reader.findBaselinesWithoutLabel(headPrRevision, /*limit=*/ 1).get(0).getSha1()) .isEqualTo(baseline1); reader.checkout(headPrRevision, workdir); FileSubjects.assertThatPath(workdir) .containsFile("test.txt", "c") .containsNoMoreFiles(); // Now try with merge ref origin = githubPrOrigin( "url = 'https://github.com/google/example'", "use_merge = True", "baseline_from_branch = True"); mockPullRequestAndIssue(123, "open"); GitRevision mergePrRevision = origin.resolve("123"); assertThat(mergePrRevision.associatedLabels()).containsEntry(GITHUB_BASE_BRANCH, "master"); assertThat(mergePrRevision.associatedLabels()) .containsEntry(GITHUB_BASE_BRANCH_SHA1, baselineMerge); assertThat(mergePrRevision.associatedLabels()).containsEntry(GITHUB_PR_NUMBER_LABEL, "123"); assertThat(mergePrRevision.associatedLabels()).containsEntry(GITHUB_PR_TITLE, "test summary"); assertThat(mergePrRevision.associatedLabels()).containsEntry(GITHUB_PR_BODY, "test summary\n\nMore text"); assertThat(mergePrRevision.associatedLabels()).containsEntry(GITHUB_PR_URL, "http://some/pr/url/123"); reader = origin.newReader(Glob.ALL_FILES, authoring); baselineObj = reader.findBaseline(mergePrRevision, "RevId"); assertThat(baselineObj.isPresent()).isTrue(); assertThat(baselineObj.get().getBaseline()) .isEqualTo(baselineObj.get().getOriginRevision().getSha1()); assertThat(baselineObj.get().getBaseline()).isEqualTo(baselineMerge); assertThat( reader .changes(baselineObj.get().getOriginRevision(), headPrRevision).getChanges() .size()) .isEqualTo(2); assertThat(reader.findBaselinesWithoutLabel(mergePrRevision, /*limit=*/ 1).get(0).getSha1()) .isEqualTo(baselineMerge); reader.checkout(mergePrRevision, workdir); FileSubjects.assertThatPath(workdir) .containsFile("other.txt", "") .containsNoMoreFiles(); } @Test public void testHookForGitHubPr() throws Exception { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); GitRepository destination = gitUtil.mockRemoteRepo("github.com/destination"); addFiles(remote, "base", ImmutableMap.<String, String>builder().put("test.txt", "a").build()); String lastRev = remote.parseRef("HEAD"); addFiles(remote, "one", ImmutableMap.<String, String>builder().put("test.txt", "b").build()); addFiles(remote, "two", ImmutableMap.<String, String>builder().put("test.txt", "c").build()); String prHeadSha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), prHeadSha1); mockPullRequestAndIssue(123, "open"); gitUtil.mockApi( eq("POST"), startsWith("https://api.github.com/repos/google/example/statuses/"), mockResponseAndValidateRequest( "{ state : 'success', context : 'the_context' }", MockRequestAssertion.contains("Migration success at"))); Path dest = Files.createTempDirectory(""); options.folderDestination.localFolder = dest.toString(); options.setWorkdirToRealTempDir(); options.setForce(true); options.setLastRevision(lastRev); options.gitDestination.committerEmail = "[email protected]"; options.gitDestination.committerName = "Bara Kopi"; Workflow<GitRevision, ?> workflow = workflow( "" + "def update_commit_status(ctx):\n" + " for effect in ctx.effects:\n" + " for origin_change in effect.origin_refs:\n" + " if effect.type == 'CREATED' or effect.type == 'UPDATED':\n" + " status = ctx.origin.create_status(\n" + " sha = origin_change.ref,\n" + " state = 'success',\n" + " context = 'copybara/import',\n" + " description = 'Migration success at ' " + "+ effect.destination_ref.id,\n" + " )\n" + "core.workflow(\n" + " name = 'default',\n" + " origin = git.github_pr_origin(\n" + " url = 'https://github.com/google/example',\n" + " ),\n" + " authoring = authoring.pass_thru('foo <[email protected]>'),\n" + " destination = git.destination(\n" + " url = '" + destination.getGitDir() + "'\n" + " ),\n" + " after_migration = [\n" + " update_commit_status" + " ]" + ")"); workflow.run(workdir, ImmutableList.of("123")); verify(gitUtil.httpTransport(), times(2)) .buildRequest( eq("POST"), startsWith("https://api.github.com/repos/google/example/statuses/")); } @Test public void testReviewApproversDescription() throws ValidationException { assertThat(createGitHubPrOrigin().describe(Glob.ALL_FILES)).containsExactly( "type", "git.github_pr_origin", "url", "https://github.com/google/example" ); assertThat(createGitHubPrOrigin( "review_state = 'ANY'" ).describe(Glob.ALL_FILES)).containsExactly( "type", "git.github_pr_origin", "url", "https://github.com/google/example", "review_state", "ANY", "review_approvers", "MEMBER", "review_approvers", "OWNER", "review_approvers", "COLLABORATOR" ); assertThat(createGitHubPrOrigin( "review_state = 'HEAD_COMMIT_APPROVED'", "review_approvers = ['MEMBER', 'OWNER']" ).describe(Glob.ALL_FILES)).containsExactly( "type", "git.github_pr_origin", "url", "https://github.com/google/example", "review_state", "HEAD_COMMIT_APPROVED", "review_approvers", "MEMBER", "review_approvers", "OWNER" ); } @Test public void testReviewApprovers() throws Exception { GitRevision noReviews = checkReviewApprovers(); assertThat(noReviews.associatedLabels()) .doesNotContainKey(GitHubPROrigin.GITHUB_PR_REVIEWER_APPROVER); assertThat(noReviews.associatedLabels()) .doesNotContainKey(GitHubPROrigin.GITHUB_PR_REVIEWER_OTHER); GitRevision any = checkReviewApprovers("review_state = 'ANY'"); assertThat(any.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_APPROVER)) .containsExactly("APPROVED_MEMBER", "COMMENTED_OWNER", "APPROVED_COLLABORATOR"); assertThat(any.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_OTHER)) .containsExactly("COMMENTED_OTHER"); EmptyChangeException e = assertThrows( EmptyChangeException.class, () -> checkReviewApprovers( "review_state = 'HEAD_COMMIT_APPROVED'", "review_approvers = [\"MEMBER\", \"OWNER\"]")); assertThat(e).hasMessageThat().contains("missing the required approvals"); GitRevision hasReviewers = checkReviewApprovers("review_state = 'ANY_COMMIT_APPROVED'", "review_approvers = [\"MEMBER\", \"OWNER\"]"); assertThat(hasReviewers.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_APPROVER)) .containsExactly("APPROVED_MEMBER", "COMMENTED_OWNER"); assertThat(hasReviewers.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_OTHER)) .containsExactly("COMMENTED_OTHER", "APPROVED_COLLABORATOR"); GitRevision anyCommitApproved = checkReviewApprovers("review_state = 'HAS_REVIEWERS'", "review_approvers = [\"OWNER\"]"); assertThat(anyCommitApproved.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_APPROVER)) .containsExactly("COMMENTED_OWNER"); assertThat(anyCommitApproved.associatedLabels().get(GitHubPROrigin.GITHUB_PR_REVIEWER_OTHER)) .containsExactly("APPROVED_MEMBER", "COMMENTED_OTHER", "APPROVED_COLLABORATOR"); } @Test public void testHttprUrl() throws Exception { GitHubPROrigin val = skylark.eval( "origin", "origin = git.github_pr_origin(url = 'http://github.com/google/example')\n"); assertThat(val.describe(Glob.ALL_FILES).get("url")) .contains("https://github.com/google/example"); } @Test public void testDescribeBranch() throws Exception { GitHubPROrigin val = skylark.eval( "origin", "origin = git.github_pr_origin(" + "url = 'http://github.com/google/example', branch = 'dev')\n"); assertThat(val.describe(Glob.ALL_FILES).get("branch")) .contains("dev"); } private GitRevision checkReviewApprovers(String... configLines) throws RepoException, IOException, ValidationException { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); addFiles(remote, "base", ImmutableMap.<String, String>builder().put("test.txt", "a").build()); addFiles(remote, "one", ImmutableMap.<String, String>builder().put("test.txt", "b").build()); addFiles(remote, "two", ImmutableMap.<String, String>builder().put("test.txt", "c").build()); String prHeadSha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), prHeadSha1); mockPullRequestAndIssue(123, "open"); gitUtil.mockApi( "GET", "https://api.github.com/repos/google/example/pulls/123/reviews?per_page=100", mockResponse( toJson( ImmutableList.of( ImmutableMap.of( "user", ImmutableMap.of("login", "APPROVED_COLLABORATOR"), "state", "APPROVED", "author_association", "COLLABORATOR", "commit_id", prHeadSha1), ImmutableMap.of( "user", ImmutableMap.of("login", "APPROVED_MEMBER"), "state", "APPROVED", "author_association", "MEMBER", "commit_id", Strings.repeat("0", 40)), ImmutableMap.of( "user", ImmutableMap.of("login", "COMMENTED_OWNER"), "state", "COMMENTED", "author_association", "OWNER", "commit_id", prHeadSha1), ImmutableMap.of( "user", ImmutableMap.of("login", "COMMENTED_OTHER"), "state", "COMMENTED", "author_association", "NONE", "commit_id", prHeadSha1))))); GitHubPROrigin origin = createGitHubPrOrigin(configLines); return origin.resolve("123"); } private GitHubPROrigin createGitHubPrOrigin(String... configLines) throws ValidationException { return skylark.eval("origin", "origin = " + "git.github_pr_origin(\n" + " url = 'https://github.com/google/example',\n" + (configLines.length == 0 ? "" : " " + Joiner.on(",\n ").join(configLines) + ",\n") + ")\n"); } private String toJson(Object obj) { try { return GsonFactory.getDefaultInstance().toPrettyString(obj); } catch (IOException e) { // Unexpected throw new IllegalStateException(e); } } @SuppressWarnings("unchecked") private Workflow<GitRevision, ?> workflow(String config) throws IOException, ValidationException { return (Workflow<GitRevision, ?>) skylark.loadConfig( new MapConfigFile(ImmutableMap.of("copy.bara.sky", config.getBytes()), "copy.bara.sky")) .getMigration("default"); } @Test public void testMerge() throws Exception { GitRepository remote = withTmpWorktree(gitUtil.mockRemoteRepo("github.com/google/example")); addFiles(remote, "base", ImmutableMap.<String, String>builder() .put("a.txt", "").build()); remote.simpleCommand("branch", "foo"); remote.forceCheckout("foo"); addFiles(remote, "one", ImmutableMap.<String, String>builder() .put("a.txt", "").put("b.txt", "").build()); addFiles(remote, "two", ImmutableMap.<String, String>builder() .put("a.txt", "").put("b.txt", "").put("c.txt", "").build()); remote.forceCheckout("master"); addFiles(remote, "master change", ImmutableMap.<String, String>builder() .put("a.txt", "").put("d.txt", "").build()); remote.simpleCommand("merge", "foo"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), remote.parseRef("foo")); remote.simpleCommand("update-ref", GitHubUtil.asMergeRef(123), remote.parseRef("master")); GitHubPROrigin origin = githubPrOrigin( "url = 'https://github.com/google/example'", "use_merge = True"); mockPullRequestAndIssue(123, "open"); origin.newReader(Glob.ALL_FILES, authoring).checkout(origin.resolve("123"), workdir); FileSubjects.assertThatPath(workdir) .containsFiles("a.txt", "b.txt", "c.txt", "d.txt") .containsNoMoreFiles(); GitRevision mergeRevision = origin.resolve("123"); // integrate SHA needs to be HEAD ref of the PR, not the (moving) merge sha-1. This is // going to be used for doing a merge later, so at best it would do a double-merge and // in the worse case it wouldn't find the merge sha-1 since baseline branch could have // already moved. assertThat(mergeRevision.associatedLabels().get(GitModule.DEFAULT_INTEGRATE_LABEL)) .contains(String.format( "https://github.com/google/example/pull/123 from googletestuser:example-branch %s", remote.resolveReference(GitHubUtil.asHeadRef(123)).getSha1())); Reader<GitRevision> reader = origin.newReader(Glob.ALL_FILES, authoring); assertThat( Lists.transform( reader.changes(/*fromRef=*/ null, mergeRevision).getChanges(), Change::getMessage)) .isEqualTo(Lists.newArrayList("base\n", "one\n", "two\n", "Merge branch 'foo'\n")); // Simulate fast-forward remote.simpleCommand("update-ref", GitHubUtil.asMergeRef(123), remote.parseRef("foo")); assertThat(Lists.transform( reader.changes(/*fromRef=*/null, origin.resolve("123")).getChanges(), Change::getMessage)) .isEqualTo(Lists.newArrayList("base\n", "one\n", "two\n")); } @Test public void testCheckout_noMergeRef() throws Exception { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); addFiles(remote, "base", ImmutableMap.<String, String>builder() .put("test.txt", "a").build()); String prHeadSha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(123), prHeadSha1); mockPullRequestAndIssue(123, "open"); // Now try with merge ref GitHubPROrigin origin = githubPrOrigin( "url = 'https://github.com/google/example'", "use_merge = True"); CannotResolveRevisionException thrown = assertThrows( CannotResolveRevisionException.class, () -> origin .newReader(Glob.ALL_FILES, authoring) .checkout(origin.resolve("123"), workdir)); assertThat(thrown) .hasMessageThat() .contains("Cannot find a merge reference for Pull Request 123"); } private void checkResolve(GitHubPROrigin origin, String reference, int prNumber) throws RepoException, IOException, ValidationException { GitRepository remote = gitUtil.mockRemoteRepo("github.com/google/example"); addFiles(remote, "first change", ImmutableMap.<String, String>builder() .put(prNumber + ".txt", "").build()); String sha1 = remote.parseRef("HEAD"); remote.simpleCommand("update-ref", GitHubUtil.asHeadRef(prNumber), sha1); GitRevision rev = origin.resolve(reference); assertThat(rev.asString()).hasLength(40); assertThat(rev.contextReference()).isEqualTo(GitHubUtil.asHeadRef(prNumber)); assertThat(rev.associatedLabels()).containsEntry(GITHUB_PR_NUMBER_LABEL, Integer.toString(prNumber)); assertThat(rev.associatedLabels()).containsEntry(GitModule.DEFAULT_INTEGRATE_LABEL, "https://github.com/google/example/pull/" + prNumber + " from googletestuser:example-branch " + sha1); } private String addFiles(GitRepository remote, String msg, Map<String, String> files) throws IOException, RepoException { GitRepository tmpRepo = withTmpWorktree(remote); for (Entry<String, String> entry : files.entrySet()) { Path file = tmpRepo.getWorkTree().resolve(entry.getKey()); Files.createDirectories(file.getParent()); Files.write(file, entry.getValue().getBytes(UTF_8)); } tmpRepo.add().all().run(); tmpRepo.simpleCommand("commit", "-m", msg); return Iterables.getOnlyElement(tmpRepo.log("HEAD").withLimit(1).run()).getCommit().getSha1(); } private GitRepository withTmpWorktree(GitRepository remote) throws IOException { return remote.withWorkTree(Files.createTempDirectory("temp")); } private GitHubPROrigin githubPrOrigin(String... lines) throws ValidationException { return skylark.eval("r", "r = git.github_pr_origin(" + " " + Joiner.on(",\n ").join(lines) + ",\n)"); } public void mockPullRequestAndIssue(int prNumber, String state, String... labels) throws IOException { mockPullRequest(prNumber, state); mockIssue(prNumber, issueResponse(prNumber, state, labels)); } private void mockPullRequest(int prNumber, String state) throws IOException { mockPullRequest(gitUtil, prNumber, state); } /** Used internally */ public static void mockPullRequest(GitTestUtil gitUtil, int prNumber, String state) throws IOException { String content = "{\n" + " \"id\": 1,\n" + " \"number\": " + prNumber + ",\n" + " \"state\": \"" + state + "\",\n" + " \"title\": \"test summary\",\n" + " \"body\": \"test summary\n\nMore text\",\n" + " \"html_url\": \"http://some/pr/url/" + prNumber + "\",\n" + " \"head\": {\n" + " \"label\": \"googletestuser:example-branch\",\n" + " \"ref\": \"example-branch\"\n" + " },\n" + " \"base\": {\n" + " \"label\": \"google:master\",\n" + " \"ref\": \"master\"\n" + " },\n" + " \"user\": {\n" + " \"login\": \"some_user\"\n" + " },\n" + " \"assignees\": [\n" + " {\n" + " \"login\": \"assignee1\"\n" + " },\n" + " {\n" + " \"login\": \"assignee2\"\n" + " }\n" + " ]\n" + "}"; gitUtil.mockApi( eq("GET"), eq("https://api.github.com/repos/google/example/pulls/" + prNumber), mockResponse(content)); } private void mockIssue(int number, LowLevelHttpRequest first, LowLevelHttpRequest... rest) throws IOException { gitUtil.mockApi( eq("GET"), eq("https://api.github.com/repos/google/example/issues/" + number), first, rest); } private LowLevelHttpRequest issueResponse(int number, String state, String... labels) { StringBuilder result = new StringBuilder( "{\n" + " \"id\": 1,\n" + " \"number\": " + number + ",\n" + " \"state\": \"" + state + "\",\n" + " \"title\": \"test summary\",\n" + " \"body\": \"test summary\"\n," + " \"labels\": [\n"); for (String label : labels) { result .append( " {\n" + " \"id\": 111111,\n" + " \"url\": " + "\"https://api.github.com/repos/google/example/labels/foo:%20yes\",\n" + " \"name\": \"") .append(label) .append("\",\n") .append(" \"color\": \"009800\",\n") .append(" \"default\": false\n") .append(" },\n"); } result.append(" ]\n" + "}"); return mockResponse(result.toString()); } }