Skip to content

Commit e4bd134

Browse files
committed
Add option to always include branches, regardless of whether a pull request exists for those branches or not.
1 parent 4718b9b commit e4bd134

File tree

4 files changed

+208
-4
lines changed

4 files changed

+208
-4
lines changed

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTrait.java

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@
2929
import com.cloudbees.jenkins.plugins.bitbucket.Messages;
3030
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest;
3131
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository;
32+
import edu.umd.cs.findbugs.annotations.CheckForNull;
3233
import edu.umd.cs.findbugs.annotations.NonNull;
3334
import hudson.Extension;
35+
import hudson.Util;
36+
import hudson.util.FormValidation;
3437
import hudson.util.ListBoxModel;
3538
import java.io.IOException;
39+
import java.util.regex.Pattern;
40+
import java.util.regex.PatternSyntaxException;
3641
import jenkins.scm.api.SCMHead;
3742
import jenkins.scm.api.SCMHeadCategory;
3843
import jenkins.scm.api.SCMHeadOrigin;
@@ -49,6 +54,8 @@
4954
import org.kohsuke.accmod.Restricted;
5055
import org.kohsuke.accmod.restrictions.NoExternalUse;
5156
import org.kohsuke.stapler.DataBoundConstructor;
57+
import org.kohsuke.stapler.DataBoundSetter;
58+
import org.kohsuke.stapler.QueryParameter;
5259

5360
/**
5461
* A {@link Discovery} trait for bitbucket that will discover branches on the repository.
@@ -67,6 +74,17 @@ public class BranchDiscoveryTrait extends SCMSourceTrait {
6774
*/
6875
private final int strategyId;
6976

77+
/**
78+
* Regex of branches that should always be included regardless of whether a merge request exists or not.
79+
*/
80+
private String branchesAlwaysIncludedRegex;
81+
82+
/**
83+
* The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
84+
*/
85+
@CheckForNull
86+
private transient Pattern branchesAlwaysIncludedRegexPattern;
87+
7088
/**
7189
* Constructor for stapler.
7290
*
@@ -96,6 +114,36 @@ public int getStrategyId() {
96114
return strategyId;
97115
}
98116

117+
/**
118+
* Returns the branchesAlwaysIncludedRegex.
119+
*
120+
* @return the branchesAlwaysIncludedRegex.
121+
*/
122+
public String getBranchesAlwaysIncludedRegex() {
123+
return branchesAlwaysIncludedRegex;
124+
}
125+
126+
/**
127+
* Sets the branchesAlwaysIncludedRegex.
128+
*/
129+
@DataBoundSetter
130+
public void setBranchesAlwaysIncludedRegex(@CheckForNull String branchesAlwaysIncludedRegex) {
131+
this.branchesAlwaysIncludedRegex = Util.fixEmptyAndTrim(branchesAlwaysIncludedRegex);
132+
}
133+
134+
/**
135+
* Returns the compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
136+
*
137+
* @return the branchesAlwaysIncludedRegexPattern.
138+
*/
139+
public Pattern getBranchesAlwaysIncludedRegexPattern() {
140+
if (branchesAlwaysIncludedRegex != null && branchesAlwaysIncludedRegexPattern == null) {
141+
branchesAlwaysIncludedRegexPattern = Pattern.compile(branchesAlwaysIncludedRegex);
142+
}
143+
144+
return branchesAlwaysIncludedRegexPattern;
145+
}
146+
99147
/**
100148
* Returns {@code true} if building branches that are not filed as a PR.
101149
*
@@ -127,11 +175,11 @@ protected void decorateContext(SCMSourceContext<?, ?> context) {
127175
switch (strategyId) {
128176
case 1:
129177
ctx.wantOriginPRs(true);
130-
ctx.withFilter(new ExcludeOriginPRBranchesSCMHeadFilter());
178+
ctx.withFilter(new ExcludeOriginPRBranchesSCMHeadFilter(getBranchesAlwaysIncludedRegexPattern()));
131179
break;
132180
case 2:
133181
ctx.wantOriginPRs(true);
134-
ctx.withFilter(new OnlyOriginPRBranchesSCMHeadFilter());
182+
ctx.withFilter(new OnlyOriginPRBranchesSCMHeadFilter(getBranchesAlwaysIncludedRegexPattern()));
135183
break;
136184
case 3:
137185
default:
@@ -179,6 +227,20 @@ public ListBoxModel doFillStrategyIdItems() {
179227
result.add(Messages.BranchDiscoveryTrait_allBranches(), "3");
180228
return result;
181229
}
230+
231+
@NonNull
232+
@Restricted(NoExternalUse.class)
233+
public FormValidation doCheckBranchesAlwaysIncludedRegex(@QueryParameter String value) {
234+
if (value == null || value.isBlank()) {
235+
return FormValidation.ok();
236+
}
237+
try {
238+
Pattern.compile(value);
239+
return FormValidation.ok();
240+
} catch (PatternSyntaxException ex) {
241+
return FormValidation.error(ex.getMessage());
242+
}
243+
}
182244
}
183245

184246
/**
@@ -220,12 +282,41 @@ public boolean isApplicableToOrigin(@NonNull Class<? extends SCMHeadOrigin> orig
220282
* Filter that excludes branches that are also filed as a pull request.
221283
*/
222284
public static class ExcludeOriginPRBranchesSCMHeadFilter extends SCMHeadFilter {
285+
286+
/**
287+
* The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
288+
*/
289+
private final Pattern branchesAlwaysIncludedRegexPattern;
290+
291+
public ExcludeOriginPRBranchesSCMHeadFilter() {
292+
branchesAlwaysIncludedRegexPattern = null;
293+
}
294+
295+
/**
296+
* Constructor
297+
*
298+
* @param branchesAlwaysIncludedRegexPattern the branchesAlwaysIncludedRegexPattern.
299+
*/
300+
public ExcludeOriginPRBranchesSCMHeadFilter(Pattern branchesAlwaysIncludedRegexPattern) {
301+
this.branchesAlwaysIncludedRegexPattern = branchesAlwaysIncludedRegexPattern;
302+
}
303+
223304
/**
224305
* {@inheritDoc}
225306
*/
226307
@Override
227308
public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead head) {
228309
if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) {
310+
if (branchesAlwaysIncludedRegexPattern != null
311+
&& branchesAlwaysIncludedRegexPattern
312+
.matcher(head.getName())
313+
.matches()) {
314+
request.listener()
315+
.getLogger()
316+
.println("Include branch " + head.getName()
317+
+ " because branch name matches always included pattern");
318+
return false;
319+
}
229320
BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request;
230321
String fullName = req.getRepoOwner() + "/" + req.getRepository();
231322
try {
@@ -251,12 +342,42 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
251342
* Filter that excludes branches that are not also filed as a pull request.
252343
*/
253344
public static class OnlyOriginPRBranchesSCMHeadFilter extends SCMHeadFilter {
345+
346+
/**
347+
* The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
348+
*/
349+
private final Pattern branchesAlwaysIncludedRegexPattern;
350+
351+
public OnlyOriginPRBranchesSCMHeadFilter() {
352+
branchesAlwaysIncludedRegexPattern = null;
353+
}
354+
355+
/**
356+
* Constructor
357+
*
358+
* @param branchesAlwaysIncludedRegexPattern the branchesAlwaysIncludedRegexPattern.
359+
*/
360+
public OnlyOriginPRBranchesSCMHeadFilter(Pattern branchesAlwaysIncludedRegexPattern) {
361+
this.branchesAlwaysIncludedRegexPattern = branchesAlwaysIncludedRegexPattern;
362+
}
363+
254364
/**
255365
* {@inheritDoc}
256366
*/
257367
@Override
258368
public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead head) {
259369
if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) {
370+
if (branchesAlwaysIncludedRegexPattern != null
371+
&& branchesAlwaysIncludedRegexPattern
372+
.matcher(head.getName())
373+
.matches()) {
374+
request.listener()
375+
.getLogger()
376+
.println("Include branch " + head.getName()
377+
+ " because branch name matches always included pattern");
378+
return false;
379+
}
380+
260381
BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request;
261382
String fullName = req.getRepoOwner() + "/" + req.getRepository();
262383
try {
@@ -267,8 +388,11 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
267388
return false;
268389
}
269390
}
270-
request.listener().getLogger().println("Discard branch " + head.getName()
271-
+ " because current strategy excludes branches that are not also filed as a pull request");
391+
request.listener()
392+
.getLogger()
393+
.println(
394+
"Discard branch " + head.getName()
395+
+ " because current strategy excludes branches that are not also filed as a pull request");
272396
return true;
273397
} catch (IOException | InterruptedException e) {
274398
// should never happens because data in the requests has been already initialised

src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTrait/config.jelly

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
<f:entry title="${%Strategy}" field="strategyId">
66
<f:select default="1"/>
77
</f:entry>
8+
<f:entry title="${%Branches to always include}" field="branchesAlwaysIncludedRegex">
9+
<f:textbox/>
10+
</f:entry>
811
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div>
2+
Regular expression of branches that should always be included regardless of whether a pull request exists or not for those branches.
3+
</div>

src/test/java/com/cloudbees/jenkins/plugins/bitbucket/trait/BranchDiscoveryTraitTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.trait;
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext;
27+
import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceRequest;
28+
import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead;
29+
import hudson.model.TaskListener;
2730
import hudson.util.ListBoxModel;
31+
import java.io.PrintStream;
2832
import java.util.Collections;
33+
import jenkins.scm.api.SCMHead;
2934
import jenkins.scm.api.SCMHeadObserver;
3035
import jenkins.scm.api.trait.SCMHeadFilter;
3136
import jenkins.scm.api.trait.SCMHeadPrefilter;
@@ -37,10 +42,17 @@
3742
import static org.hamcrest.MatcherAssert.assertThat;
3843
import static org.hamcrest.Matchers.contains;
3944
import static org.hamcrest.Matchers.hasItem;
45+
import static org.hamcrest.Matchers.hasSize;
46+
import static org.hamcrest.Matchers.hasToString;
4047
import static org.hamcrest.Matchers.instanceOf;
4148
import static org.hamcrest.Matchers.is;
4249
import static org.hamcrest.Matchers.not;
50+
import static org.hamcrest.Matchers.nullValue;
4351
import static org.junit.Assume.assumeThat;
52+
import static org.mockito.Mockito.mock;
53+
import static org.mockito.Mockito.when;
54+
import static org.mockito.Mockito.verify;
55+
import static org.mockito.Mockito.verifyNoMoreInteractions;
4456

4557
public class BranchDiscoveryTraitTest {
4658
@ClassRule
@@ -120,4 +132,66 @@ public void given__descriptor__when__displayingOptions__then__allThreePresent()
120132
assertThat(options.get(2).value, is("3"));
121133
}
122134

135+
@Test
136+
public void given__context__with__AlwaysIncludePattern__shouldCompile() {
137+
BranchDiscoveryTrait instance = new BranchDiscoveryTrait(false, true);
138+
139+
assertThat(instance.getBranchesAlwaysIncludedRegexPattern(), is(nullValue()));
140+
instance.setBranchesAlwaysIncludedRegex("");
141+
assertThat(instance.getBranchesAlwaysIncludedRegexPattern(), is(nullValue()));
142+
143+
instance.setBranchesAlwaysIncludedRegex(".*myBranch");
144+
assertThat(instance.getBranchesAlwaysIncludedRegexPattern(), hasToString(".*myBranch"));
145+
}
146+
147+
@Test
148+
public void given__excludingPRs__branch__match__alwaysIncludedRegex__shouldNotBeExcluded() throws Exception {
149+
BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none());
150+
151+
BranchDiscoveryTrait instance = new BranchDiscoveryTrait(true, false);
152+
instance.setBranchesAlwaysIncludedRegex(".*release$");
153+
instance.decorateContext(ctx);
154+
assertThat(ctx.filters(), hasSize(1));
155+
SCMHeadFilter filter = ctx.filters().get(0);
156+
assertThat(filter, instanceOf(BranchDiscoveryTrait.ExcludeOriginPRBranchesSCMHeadFilter.class));
157+
158+
SCMHead head = mock(BranchSCMHead.class);
159+
when(head.getName()).thenReturn("feature/release");
160+
BitbucketSCMSourceRequest request = prepareRequest();
161+
162+
assertThat(filter.isExcluded(request, head), is(false));
163+
verify(request.listener().getLogger())
164+
.println("Include branch feature/release because branch name matches always included pattern");
165+
verifyNoMoreInteractions(request.listener().getLogger());
166+
}
167+
168+
@Test
169+
public void given__onlyPRs__branch__match__alwaysIncludedRegex__shouldNotBeExcluded() throws Exception {
170+
BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none());
171+
172+
BranchDiscoveryTrait instance = new BranchDiscoveryTrait(false, true);
173+
instance.setBranchesAlwaysIncludedRegex(".*release$");
174+
instance.decorateContext(ctx);
175+
assertThat(ctx.filters(), hasSize(1));
176+
SCMHeadFilter filter = ctx.filters().get(0);
177+
assertThat(filter, instanceOf(BranchDiscoveryTrait.OnlyOriginPRBranchesSCMHeadFilter.class));
178+
179+
SCMHead head = mock(BranchSCMHead.class);
180+
when(head.getName()).thenReturn("feature/release");
181+
BitbucketSCMSourceRequest request = prepareRequest();
182+
183+
assertThat(filter.isExcluded(request, head), is(false));
184+
verify(request.listener().getLogger())
185+
.println("Include branch feature/release because branch name matches always included pattern");
186+
verifyNoMoreInteractions(request.listener().getLogger());
187+
}
188+
189+
private BitbucketSCMSourceRequest prepareRequest() {
190+
BitbucketSCMSourceRequest request = mock(BitbucketSCMSourceRequest.class);
191+
TaskListener listener = mock(TaskListener.class);
192+
when(listener.getLogger()).thenReturn(mock(PrintStream.class));
193+
when(request.listener()).thenReturn(listener);
194+
return request;
195+
}
196+
123197
}

0 commit comments

Comments
 (0)