Skip to content

Commit 5a2d6a2

Browse files
sjjarvilifeofguenterCarroll Chiou
authored
fix: call the changes API to make the commit hash available upstream (#548)
* addresses the problem raised in issue 469 regarding PRs based off of forks ref: https://github.com/jenkinsci/bitbucket-branch-source-plugin/issues/469 Co-authored-by: Günter Grodotzki <[email protected]> Co-authored-by: Carroll Chiou <[email protected]>
1 parent 559184b commit 5a2d6a2

File tree

4 files changed

+308
-1
lines changed

4 files changed

+308
-1
lines changed

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.io.ObjectStreamException;
6969
import java.util.ArrayList;
7070
import java.util.Arrays;
71+
import java.util.Collection;
7172
import java.util.Collections;
7273
import java.util.Date;
7374
import java.util.EnumSet;
@@ -563,7 +564,7 @@ protected Iterable<BitbucketPullRequest> create() {
563564
try {
564565
if (event instanceof HasPullRequests) {
565566
HasPullRequests hasPrEvent = (HasPullRequests) event;
566-
return hasPrEvent.getPullRequests(BitbucketSCMSource.this);
567+
return getBitbucketPullRequestsFromEvent(hasPrEvent, listener);
567568
}
568569

569570
return (Iterable<BitbucketPullRequest>) buildBitbucketClient().getPullRequests();
@@ -616,6 +617,24 @@ protected Iterable<BitbucketBranch> create() {
616617
}
617618
}
618619

620+
private Iterable<BitbucketPullRequest> getBitbucketPullRequestsFromEvent(@NonNull HasPullRequests incomingPrEvent, @NonNull TaskListener listener) {
621+
BitbucketApi bitBucket = buildBitbucketClient();
622+
Collection<BitbucketPullRequest> initializedPRs = new HashSet<>();
623+
try {
624+
Iterable<BitbucketPullRequest> pullRequests =
625+
incomingPrEvent.getPullRequests(BitbucketSCMSource.this);
626+
for (BitbucketPullRequest pr : pullRequests) {
627+
// ensure that the PR is properly initialized via /changes API
628+
// see BitbucketServerAPIClient.setupPullRequest()
629+
initializedPRs.add(bitBucket.getPullRequestById(Integer.parseInt(pr.getId())));
630+
listener.getLogger().format("Initialized PR: %s%n", pr.getLink());
631+
}
632+
} catch (IOException | InterruptedException e) {
633+
throw new BitbucketSCMSource.WrappedException(e);
634+
}
635+
return initializedPRs;
636+
}
637+
619638
private void retrievePullRequests(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException {
620639
final String fullName = repoOwner + "/" + repository;
621640

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.cloudbees.jenkins.plugins.bitbucket;
2+
3+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
4+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch;
5+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit;
6+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest;
7+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestDestination;
8+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent;
9+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestSource;
10+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository;
11+
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient;
12+
import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch;
13+
import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint;
14+
import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests;
15+
import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient;
16+
import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranch;
17+
import edu.umd.cs.findbugs.annotations.NonNull;
18+
import hudson.model.TaskListener;
19+
import hudson.scm.SCM;
20+
import java.net.URL;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
27+
import jenkins.model.Jenkins;
28+
import jenkins.scm.api.SCMHead;
29+
import jenkins.scm.api.SCMHeadEvent;
30+
import jenkins.scm.api.SCMHeadObserver;
31+
import jenkins.scm.api.SCMNavigator;
32+
import jenkins.scm.api.SCMRevision;
33+
import jenkins.scm.api.SCMSource;
34+
import jenkins.scm.api.SCMSourceCriteria;
35+
import org.hamcrest.Matchers;
36+
import org.junit.Before;
37+
import org.junit.ClassRule;
38+
import org.junit.Test;
39+
import org.junit.runner.RunWith;
40+
import org.jvnet.hudson.test.JenkinsRule;
41+
import org.mockito.Mock;
42+
import org.mockito.Mockito;
43+
import org.mockito.junit.MockitoJUnitRunner;
44+
45+
import static org.hamcrest.MatcherAssert.assertThat;
46+
import static org.hamcrest.Matchers.is;
47+
import static org.mockito.Mockito.mock;
48+
import static org.mockito.Mockito.verify;
49+
import static org.mockito.Mockito.when;
50+
51+
/**
52+
* Tests different scenarios of the
53+
* {@link BitbucketSCMSource#retrieve(SCMSourceCriteria, SCMHeadObserver, SCMHeadEvent, TaskListener)} method.
54+
*
55+
* This test was created to validate a fix for the issue described in:
56+
* https://github.com/jenkinsci/bitbucket-branch-source-plugin/issues/469
57+
*/
58+
@RunWith(MockitoJUnitRunner.class)
59+
public class BitbucketSCMSourceRetrieveTest {
60+
61+
private static final String CLOUD_REPO_OWNER = "cloudbeers";
62+
private static final String SERVER_REPO_OWNER = "DUB";
63+
private static final String SERVER_REPO_URL = "https://bitbucket.test";
64+
private static final String REPO_NAME = "stunning-adventure";
65+
private static final String BRANCH_NAME = "branch1";
66+
private static final String COMMIT_HASH = "e851558f77c098d21af6bb8cc54a423f7cf12147";
67+
private static final Integer PR_ID = 1;
68+
69+
@ClassRule
70+
public static JenkinsRule jenkinsRule = new JenkinsRule();
71+
72+
@Mock
73+
private BitbucketRepository repository;
74+
@Mock
75+
private BitbucketBranch sourceBranch;
76+
@Mock
77+
private BitbucketBranch destinationBranch;
78+
@Mock
79+
private BitbucketPullRequestDestination prDestination;
80+
@Mock
81+
private BitbucketPullRequestSource prSource;
82+
@Mock
83+
private BitbucketCommit commit;
84+
@Mock
85+
private BitbucketPullRequest pullRequest;
86+
@Mock
87+
private SCMSourceCriteria criteria;
88+
89+
@Before
90+
public void setUp() {
91+
when(prDestination.getRepository()).thenReturn(repository);
92+
when(prDestination.getBranch()).thenReturn(destinationBranch);
93+
when(destinationBranch.getName()).thenReturn("main");
94+
95+
when(sourceBranch.getName()).thenReturn(BRANCH_NAME);
96+
when(prSource.getRepository()).thenReturn(repository);
97+
when(prSource.getBranch()).thenReturn(sourceBranch);
98+
when(commit.getHash()).thenReturn(COMMIT_HASH);
99+
when(prSource.getCommit()).thenReturn(commit);
100+
101+
when(pullRequest.getSource()).thenReturn(prSource);
102+
when(pullRequest.getDestination()).thenReturn(prDestination);
103+
when(pullRequest.getId()).thenReturn(PR_ID.toString());
104+
}
105+
106+
@Test
107+
public void retrieveTriggersRequiredApiCalls_cloud() throws Exception {
108+
BitbucketSCMSource instance = load("retrieve_prs_test_cloud");
109+
assertThat(instance.getId(), is("retrieve_prs_test_cloud"));
110+
assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL));
111+
assertThat(instance.getRepoOwner(), is(CLOUD_REPO_OWNER));
112+
assertThat(instance.getRepository(), is(REPO_NAME));
113+
BitbucketCloudApiClient client = BitbucketClientMockUtils.getAPIClientMock(true, false);
114+
BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, client);
115+
116+
List<BitbucketCloudBranch> branches =
117+
Collections.singletonList(new BitbucketCloudBranch(BRANCH_NAME, COMMIT_HASH, 0));
118+
when(client.getBranches()).thenReturn(branches);
119+
120+
verifyExpectedClientApiCalls(instance, client);
121+
}
122+
123+
@Test
124+
public void retrieveTriggersRequiredApiCalls_server() throws Exception {
125+
BitbucketSCMSource instance = load("retrieve_prs_test_server");
126+
assertThat(instance.getServerUrl(), is(SERVER_REPO_URL));
127+
assertThat(instance.getRepoOwner(), is(SERVER_REPO_OWNER));
128+
assertThat(instance.getRepository(), is(REPO_NAME));
129+
130+
BitbucketServerAPIClient client = mock(BitbucketServerAPIClient.class);
131+
BitbucketMockApiFactory.add(SERVER_REPO_URL, client);
132+
133+
List<BitbucketServerBranch> branches =
134+
Collections.singletonList(new BitbucketServerBranch(BRANCH_NAME, COMMIT_HASH));
135+
when(client.getBranches()).thenReturn(branches);
136+
when(client.getRepository()).thenReturn(repository);
137+
138+
verifyExpectedClientApiCalls(instance, client);
139+
}
140+
141+
/**
142+
* Given a BitbucketSCMSource, call the retrieve(SCMSourceCriteria, SCMHeadObserver, SCMHeadEvent, TaskListener)
143+
* method with an event having a PR and verify the expected client API calls
144+
*
145+
* @param instance The BitbucketSCMSource instance that has been configured with the traits required
146+
* for testing this code path.
147+
*/
148+
private void verifyExpectedClientApiCalls(BitbucketSCMSource instance, BitbucketApi apiClient) throws Exception {
149+
String fullRepoName = instance.getRepoOwner() + '/' + instance.getRepository();
150+
when(repository.getFullName()).thenReturn(fullRepoName);
151+
when(repository.getRepositoryName()).thenReturn(instance.getRepository());
152+
153+
when(pullRequest.getLink()).thenReturn(instance.getServerUrl() + '/' + fullRepoName + "/pull-requests/" + PR_ID);
154+
when(apiClient.getPullRequestById(PR_ID)).thenReturn(pullRequest);
155+
156+
SCMHeadEvent<?> event = new HeadEvent(Collections.singleton(pullRequest));
157+
TaskListener taskListener = BitbucketClientMockUtils.getTaskListenerMock();
158+
SCMHeadObserver.Collector headObserver = new SCMHeadObserver.Collector();
159+
when(criteria.isHead(Mockito.any(), Mockito.same(taskListener))).thenReturn(true);
160+
161+
instance.retrieve(criteria, headObserver, event, taskListener);
162+
163+
// Expect the observer to collect the branch and the PR
164+
Set<String> heads =
165+
headObserver.result().keySet().stream().map(SCMHead::getName).collect(Collectors.toSet());
166+
assertThat(heads, Matchers.containsInAnyOrder("PR-1", BRANCH_NAME));
167+
168+
// Ensures PR is properly initialized, especially fork-based PRs
169+
// see BitbucketServerAPIClient.setupPullRequest()
170+
verify(apiClient, Mockito.times(1)).getPullRequestById(PR_ID);
171+
// The event is a HasPullRequests, so this call should be skipped in favor of getting PRs from the event itself
172+
verify(apiClient, Mockito.never()).getPullRequests();
173+
// Fetch tags trait was not enabled on the BitbucketSCMSource
174+
verify(apiClient, Mockito.never()).getTags();
175+
}
176+
177+
private static final class HeadEvent extends SCMHeadEvent<BitbucketPullRequestEvent> implements HasPullRequests {
178+
private final Collection<BitbucketPullRequest> pullRequests;
179+
180+
private HeadEvent(Collection<BitbucketPullRequest> pullRequests) {
181+
super(Type.UPDATED, 0, mock(BitbucketPullRequestEvent.class), "origin");
182+
this.pullRequests = pullRequests;
183+
}
184+
185+
@Override
186+
public Iterable<BitbucketPullRequest> getPullRequests(BitbucketSCMSource src) {
187+
return pullRequests;
188+
}
189+
190+
@Override
191+
public boolean isMatch(@NonNull SCMNavigator navigator) {
192+
return false;
193+
}
194+
195+
@NonNull
196+
@Override
197+
public String getSourceName() {
198+
return REPO_NAME;
199+
}
200+
201+
@NonNull
202+
@Override
203+
public Map<SCMHead, SCMRevision> heads(@NonNull SCMSource source) {
204+
return Collections.emptyMap();
205+
}
206+
207+
@Override
208+
public boolean isMatch(@NonNull SCM scm) {
209+
return false;
210+
}
211+
}
212+
213+
private BitbucketSCMSource load(String configuration) {
214+
String path = getClass().getSimpleName() + "/" + configuration + ".xml";
215+
URL url = getClass().getResource(path);
216+
return (BitbucketSCMSource) Jenkins.XSTREAM2.fromXML(url);
217+
}
218+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource>
2+
<id>retrieve_prs_test_cloud</id>
3+
<serverUrl>https://bitbucket.org</serverUrl>
4+
<credentialsId>bb-cloud-creds</credentialsId>
5+
<repoOwner>cloudbeers</repoOwner>
6+
<repository>stunning-adventure</repository>
7+
<traits>
8+
<jenkins.scm.impl.trait.RegexSCMHeadFilterTrait>
9+
<regex>.*</regex>
10+
</jenkins.scm.impl.trait.RegexSCMHeadFilterTrait>
11+
<com.cloudbees.jenkins.plugins.bitbucket.BranchDiscoveryTrait>
12+
<strategyId>3</strategyId>
13+
</com.cloudbees.jenkins.plugins.bitbucket.BranchDiscoveryTrait>
14+
<com.cloudbees.jenkins.plugins.bitbucket.WebhookRegistrationTrait>
15+
<mode>ITEM</mode>
16+
</com.cloudbees.jenkins.plugins.bitbucket.WebhookRegistrationTrait>
17+
<com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait>
18+
<strategyId>1</strategyId>
19+
</com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait>
20+
<jenkins.plugins.git.traits.RefSpecsSCMSourceTrait>
21+
<templates>
22+
<jenkins.plugins.git.traits.RefSpecsSCMSourceTrait_-RefSpecTemplate>
23+
<value>+refs/heads/*:refs/remotes/@{remote}/*</value>
24+
</jenkins.plugins.git.traits.RefSpecsSCMSourceTrait_-RefSpecTemplate>
25+
</templates>
26+
</jenkins.plugins.git.traits.RefSpecsSCMSourceTrait>
27+
<com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait>
28+
<strategyId>1</strategyId>
29+
<trust class="com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait$TrustEveryone"/>
30+
</com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait>
31+
</traits>
32+
</com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource>
2+
<id>retrieve_prs_test_server</id>
3+
<credentialsId>bb-server-creds</credentialsId>
4+
<checkoutCredentialsId>SAME</checkoutCredentialsId>
5+
<repoOwner>DUB</repoOwner>
6+
<repository>stunning-adventure</repository>
7+
<includes>*</includes>
8+
<excludes></excludes>
9+
<autoRegisterHook>false</autoRegisterHook>
10+
<bitbucketServerUrl>https://bitbucket.test</bitbucketServerUrl>
11+
<sshPort>7999</sshPort>
12+
<repositoryType>GIT</repositoryType>
13+
<traits>
14+
<jenkins.scm.impl.trait.RegexSCMHeadFilterTrait>
15+
<regex>.*</regex>
16+
</jenkins.scm.impl.trait.RegexSCMHeadFilterTrait>
17+
<com.cloudbees.jenkins.plugins.bitbucket.BranchDiscoveryTrait>
18+
<strategyId>3</strategyId>
19+
</com.cloudbees.jenkins.plugins.bitbucket.BranchDiscoveryTrait>
20+
<com.cloudbees.jenkins.plugins.bitbucket.WebhookRegistrationTrait>
21+
<mode>ITEM</mode>
22+
</com.cloudbees.jenkins.plugins.bitbucket.WebhookRegistrationTrait>
23+
<com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait>
24+
<strategyId>1</strategyId>
25+
</com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait>
26+
<jenkins.plugins.git.traits.RefSpecsSCMSourceTrait>
27+
<templates>
28+
<jenkins.plugins.git.traits.RefSpecsSCMSourceTrait_-RefSpecTemplate>
29+
<value>+refs/heads/*:refs/remotes/@{remote}/*</value>
30+
</jenkins.plugins.git.traits.RefSpecsSCMSourceTrait_-RefSpecTemplate>
31+
</templates>
32+
</jenkins.plugins.git.traits.RefSpecsSCMSourceTrait>
33+
<com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait>
34+
<strategyId>1</strategyId>
35+
<trust class="com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait$TrustEveryone"/>
36+
</com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait>
37+
</traits>
38+
</com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource>

0 commit comments

Comments
 (0)