2929import com .cloudbees .jenkins .plugins .bitbucket .Messages ;
3030import com .cloudbees .jenkins .plugins .bitbucket .api .BitbucketPullRequest ;
3131import com .cloudbees .jenkins .plugins .bitbucket .api .BitbucketRepository ;
32+ import edu .umd .cs .findbugs .annotations .CheckForNull ;
3233import edu .umd .cs .findbugs .annotations .NonNull ;
3334import hudson .Extension ;
35+ import hudson .Util ;
36+ import hudson .util .FormValidation ;
3437import hudson .util .ListBoxModel ;
3538import java .io .IOException ;
39+ import java .util .regex .Pattern ;
40+ import java .util .regex .PatternSyntaxException ;
3641import jenkins .scm .api .SCMHead ;
3742import jenkins .scm .api .SCMHeadCategory ;
3843import jenkins .scm .api .SCMHeadOrigin ;
4954import org .kohsuke .accmod .Restricted ;
5055import org .kohsuke .accmod .restrictions .NoExternalUse ;
5156import 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
0 commit comments