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 .model .Item ;
37+ import hudson .util .FormValidation ;
3438import hudson .util .ListBoxModel ;
3539import java .io .IOException ;
40+ import java .util .regex .Pattern ;
41+ import java .util .regex .PatternSyntaxException ;
42+ import jenkins .model .Jenkins ;
3643import jenkins .scm .api .SCMHead ;
3744import jenkins .scm .api .SCMHeadCategory ;
3845import jenkins .scm .api .SCMHeadOrigin ;
4855import org .jenkinsci .Symbol ;
4956import org .kohsuke .accmod .Restricted ;
5057import org .kohsuke .accmod .restrictions .NoExternalUse ;
58+ import org .kohsuke .stapler .AncestorInPath ;
5159import org .kohsuke .stapler .DataBoundConstructor ;
60+ import org .kohsuke .stapler .DataBoundSetter ;
61+ import org .kohsuke .stapler .QueryParameter ;
62+ import org .kohsuke .stapler .interceptor .RequirePOST ;
5263
5364/**
5465 * A {@link Discovery} trait for bitbucket that will discover branches on the repository.
@@ -67,6 +78,17 @@ public class BranchDiscoveryTrait extends SCMSourceTrait {
6778 */
6879 private final int strategyId ;
6980
81+ /**
82+ * Regex of branches that should always be included regardless of whether a merge request exists or not.
83+ */
84+ private String branchesAlwaysIncludedRegex ;
85+
86+ /**
87+ * The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
88+ */
89+ @ CheckForNull
90+ private transient Pattern branchesAlwaysIncludedRegexPattern ;
91+
7092 /**
7193 * Constructor for stapler.
7294 *
@@ -96,6 +118,36 @@ public int getStrategyId() {
96118 return strategyId ;
97119 }
98120
121+ /**
122+ * Returns the branchesAlwaysIncludedRegex.
123+ *
124+ * @return the branchesAlwaysIncludedRegex.
125+ */
126+ public String getBranchesAlwaysIncludedRegex () {
127+ return branchesAlwaysIncludedRegex ;
128+ }
129+
130+ /**
131+ * Sets the branchesAlwaysIncludedRegex.
132+ */
133+ @ DataBoundSetter
134+ public void setBranchesAlwaysIncludedRegex (@ CheckForNull String branchesAlwaysIncludedRegex ) {
135+ this .branchesAlwaysIncludedRegex = Util .fixEmptyAndTrim (branchesAlwaysIncludedRegex );
136+ }
137+
138+ /**
139+ * Returns the compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
140+ *
141+ * @return the branchesAlwaysIncludedRegexPattern.
142+ */
143+ public Pattern getBranchesAlwaysIncludedRegexPattern () {
144+ if (branchesAlwaysIncludedRegex != null && branchesAlwaysIncludedRegexPattern == null ) {
145+ branchesAlwaysIncludedRegexPattern = Pattern .compile (branchesAlwaysIncludedRegex );
146+ }
147+
148+ return branchesAlwaysIncludedRegexPattern ;
149+ }
150+
99151 /**
100152 * Returns {@code true} if building branches that are not filed as a PR.
101153 *
@@ -127,11 +179,11 @@ protected void decorateContext(SCMSourceContext<?, ?> context) {
127179 switch (strategyId ) {
128180 case 1 :
129181 ctx .wantOriginPRs (true );
130- ctx .withFilter (new ExcludeOriginPRBranchesSCMHeadFilter ());
182+ ctx .withFilter (new ExcludeOriginPRBranchesSCMHeadFilter (getBranchesAlwaysIncludedRegexPattern () ));
131183 break ;
132184 case 2 :
133185 ctx .wantOriginPRs (true );
134- ctx .withFilter (new OnlyOriginPRBranchesSCMHeadFilter ());
186+ ctx .withFilter (new OnlyOriginPRBranchesSCMHeadFilter (getBranchesAlwaysIncludedRegexPattern () ));
135187 break ;
136188 case 3 :
137189 default :
@@ -179,6 +231,27 @@ public ListBoxModel doFillStrategyIdItems() {
179231 result .add (Messages .BranchDiscoveryTrait_allBranches (), "3" );
180232 return result ;
181233 }
234+
235+ @ NonNull
236+ @ Restricted (NoExternalUse .class )
237+ @ RequirePOST
238+ public FormValidation doCheckBranchesAlwaysIncludedRegex (@ CheckForNull @ AncestorInPath Item context , @ QueryParameter String value ) {
239+ if (context == null ) {
240+ Jenkins .get ().checkPermission (Jenkins .MANAGE );
241+ } else {
242+ context .checkPermission (Item .CONFIGURE );
243+ }
244+
245+ if (value == null || value .isBlank ()) {
246+ return FormValidation .ok ();
247+ }
248+ try {
249+ Pattern .compile (value );
250+ return FormValidation .ok ();
251+ } catch (PatternSyntaxException ex ) {
252+ return FormValidation .error (ex .getMessage ());
253+ }
254+ }
182255 }
183256
184257 /**
@@ -220,12 +293,41 @@ public boolean isApplicableToOrigin(@NonNull Class<? extends SCMHeadOrigin> orig
220293 * Filter that excludes branches that are also filed as a pull request.
221294 */
222295 public static class ExcludeOriginPRBranchesSCMHeadFilter extends SCMHeadFilter {
296+
297+ /**
298+ * The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
299+ */
300+ private final Pattern branchesAlwaysIncludedRegexPattern ;
301+
302+ public ExcludeOriginPRBranchesSCMHeadFilter () {
303+ branchesAlwaysIncludedRegexPattern = null ;
304+ }
305+
306+ /**
307+ * Constructor
308+ *
309+ * @param branchesAlwaysIncludedRegexPattern the branchesAlwaysIncludedRegexPattern.
310+ */
311+ public ExcludeOriginPRBranchesSCMHeadFilter (Pattern branchesAlwaysIncludedRegexPattern ) {
312+ this .branchesAlwaysIncludedRegexPattern = branchesAlwaysIncludedRegexPattern ;
313+ }
314+
223315 /**
224316 * {@inheritDoc}
225317 */
226318 @ Override
227319 public boolean isExcluded (@ NonNull SCMSourceRequest request , @ NonNull SCMHead head ) {
228320 if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest ) {
321+ if (branchesAlwaysIncludedRegexPattern != null
322+ && branchesAlwaysIncludedRegexPattern
323+ .matcher (head .getName ())
324+ .matches ()) {
325+ request .listener ()
326+ .getLogger ()
327+ .println ("Include branch " + head .getName ()
328+ + " because branch name matches always included pattern" );
329+ return false ;
330+ }
229331 BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest ) request ;
230332 String fullName = req .getRepoOwner () + "/" + req .getRepository ();
231333 try {
@@ -251,12 +353,42 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
251353 * Filter that excludes branches that are not also filed as a pull request.
252354 */
253355 public static class OnlyOriginPRBranchesSCMHeadFilter extends SCMHeadFilter {
356+
357+ /**
358+ * The compiled {@link Pattern} of the branchesAlwaysIncludedRegex.
359+ */
360+ private final Pattern branchesAlwaysIncludedRegexPattern ;
361+
362+ public OnlyOriginPRBranchesSCMHeadFilter () {
363+ branchesAlwaysIncludedRegexPattern = null ;
364+ }
365+
366+ /**
367+ * Constructor
368+ *
369+ * @param branchesAlwaysIncludedRegexPattern the branchesAlwaysIncludedRegexPattern.
370+ */
371+ public OnlyOriginPRBranchesSCMHeadFilter (Pattern branchesAlwaysIncludedRegexPattern ) {
372+ this .branchesAlwaysIncludedRegexPattern = branchesAlwaysIncludedRegexPattern ;
373+ }
374+
254375 /**
255376 * {@inheritDoc}
256377 */
257378 @ Override
258379 public boolean isExcluded (@ NonNull SCMSourceRequest request , @ NonNull SCMHead head ) {
259380 if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest ) {
381+ if (branchesAlwaysIncludedRegexPattern != null
382+ && branchesAlwaysIncludedRegexPattern
383+ .matcher (head .getName ())
384+ .matches ()) {
385+ request .listener ()
386+ .getLogger ()
387+ .println ("Include branch " + head .getName ()
388+ + " because branch name matches always included pattern" );
389+ return false ;
390+ }
391+
260392 BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest ) request ;
261393 String fullName = req .getRepoOwner () + "/" + req .getRepository ();
262394 try {
@@ -267,8 +399,11 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
267399 return false ;
268400 }
269401 }
270- request .listener ().getLogger ().println ("Discard branch " + head .getName ()
271- + " because current strategy excludes branches that are not also filed as a pull request" );
402+ request .listener ()
403+ .getLogger ()
404+ .println (
405+ "Discard branch " + head .getName ()
406+ + " because current strategy excludes branches that are not also filed as a pull request" );
272407 return true ;
273408 } catch (IOException | InterruptedException e ) {
274409 // should never happens because data in the requests has been already initialised
0 commit comments