diff --git a/src/main/java/jenkins/branch/Branch.java b/src/main/java/jenkins/branch/Branch.java
index 05498c47..72c27b3b 100644
--- a/src/main/java/jenkins/branch/Branch.java
+++ b/src/main/java/jenkins/branch/Branch.java
@@ -141,8 +141,11 @@ public String getSourceId() {
* @return the name of the branch.
*/
public String getName() {
- // TODO this could include a uniquifying prefix defined in BranchSource
- return head.getName();
+ CustomNameBranchProperty customName = (CustomNameBranchProperty)this.properties.stream()
+ .filter(p -> CustomNameBranchProperty.class.isInstance(p)).findFirst().orElse(null);
+ return customName != null
+ ? customName.generateName(head.getName())
+ : head.getName();
}
/**
diff --git a/src/main/java/jenkins/branch/CustomNameBranchProperty.java b/src/main/java/jenkins/branch/CustomNameBranchProperty.java
new file mode 100644
index 00000000..6488e710
--- /dev/null
+++ b/src/main/java/jenkins/branch/CustomNameBranchProperty.java
@@ -0,0 +1,65 @@
+package jenkins.branch;
+
+import hudson.Extension;
+import hudson.model.Job;
+import hudson.model.Run;
+import hudson.util.FormValidation;
+
+import org.apache.commons.lang.StringUtils;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.verb.POST;
+
+/**
+ * @author Frédéric Laugier
+ */
+public class CustomNameBranchProperty extends BranchProperty {
+
+ private final String pattern;
+
+ @DataBoundConstructor
+ public CustomNameBranchProperty(String pattern) {
+ super();
+
+ if(!checkValidPattern(pattern)) {
+ throw new IllegalArgumentException(Messages.CustomNameBranchProperty_InvalidPattern());
+ }
+
+ this.pattern = StringUtils.trimToNull(pattern);
+ }
+
+
+ public String getPattern() {
+ return this.pattern;
+ }
+
+ @Override
+ public
, B extends Run
> JobDecorator
jobDecorator(Class
clazz) {
+ return null;
+ }
+
+ private static boolean checkValidPattern(String pattern) {
+ String value = StringUtils.trimToNull(pattern);
+ return value == null || value.contains("{}");
+ }
+
+ String generateName(String name) {
+ return this.pattern != null ? this.pattern.replaceAll("\\{\\}", name) : name;
+ }
+
+ @Extension
+ public static class DescriptorImpl extends BranchPropertyDescriptor {
+
+ @Override
+ public String getDisplayName() {
+ return Messages.CustomNameBranchProperty_DisplayName();
+ }
+
+ @POST
+ public FormValidation doCheckPattern(StaplerRequest request) {
+ return checkValidPattern(request.getParameter("value"))
+ ? FormValidation.ok()
+ : FormValidation.error(Messages.CustomNameBranchProperty_InvalidPattern());
+ }
+ }
+}
diff --git a/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.jelly b/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.jelly
new file mode 100644
index 00000000..4c5937c0
--- /dev/null
+++ b/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.jelly
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.properties b/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.properties
new file mode 100644
index 00000000..b50dc809
--- /dev/null
+++ b/src/main/resources/jenkins/branch/CustomNameBranchProperty/config.properties
@@ -0,0 +1,3 @@
+
+Pattern.Title = Pattern
+Pattern.Description = The required '{}' placeholder will be replaced with the remote branch name
diff --git a/src/main/resources/jenkins/branch/CustomNameBranchProperty/help.html b/src/main/resources/jenkins/branch/CustomNameBranchProperty/help.html
new file mode 100644
index 00000000..900e25cc
--- /dev/null
+++ b/src/main/resources/jenkins/branch/CustomNameBranchProperty/help.html
@@ -0,0 +1,3 @@
+
+ Used to disambiguate multiple sources that may have overlapping branch names.
+
diff --git a/src/main/resources/jenkins/branch/Messages.properties b/src/main/resources/jenkins/branch/Messages.properties
index ccd5f17d..c567e8b6 100644
--- a/src/main/resources/jenkins/branch/Messages.properties
+++ b/src/main/resources/jenkins/branch/Messages.properties
@@ -23,6 +23,8 @@
#
BaseEmptyView.displayName=Welcome
BranchStatusColumn.displayName=Status
+CustomNameBranchProperty.DisplayName=Customize local branch name
+CustomNameBranchProperty.InvalidPattern=Your pattern must include the '{}' placeholder
DefaultBranchPropertyStrategy.DisplayName=All branches get the same properties
DescriptionColumn.displayName=Project description
ItemColumn.DisplayName=Name
diff --git a/src/test/java/jenkins/branch/CustomNameBranchPropertyTest.java b/src/test/java/jenkins/branch/CustomNameBranchPropertyTest.java
new file mode 100644
index 00000000..8ddda365
--- /dev/null
+++ b/src/test/java/jenkins/branch/CustomNameBranchPropertyTest.java
@@ -0,0 +1,139 @@
+package jenkins.branch;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import hudson.model.FreeStyleProject;
+import hudson.model.TopLevelItem;
+import integration.harness.BasicBranchProperty;
+import integration.harness.BasicMultiBranchProject;
+import jenkins.scm.impl.mock.MockSCMController;
+import jenkins.scm.impl.mock.MockSCMDiscoverBranches;
+import jenkins.scm.impl.mock.MockSCMSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+/**
+ * @author Frédéric Laugier
+ */
+public class CustomNameBranchPropertyTest {
+ /**
+ * All tests in this class only create items and do not affect other global configuration, thus we trade test
+ * execution time for the restriction on only touching items.
+ */
+ @ClassRule
+ public static JenkinsRule r = new JenkinsRule();
+
+ @Before
+ public void cleanOutAllItems() throws Exception {
+ for (TopLevelItem i : r.getInstance().getItems()) {
+ i.delete();
+ }
+ }
+
+ @Test
+ public void patternValues() throws Exception {
+ assertThat(new CustomNameBranchProperty(null).getPattern(), nullValue());
+ assertThat(new CustomNameBranchProperty("").getPattern(), nullValue());
+ assertThat(new CustomNameBranchProperty(" ").getPattern(), nullValue());
+ assertThat(new CustomNameBranchProperty("app-{}").getPattern(), is("app-{}"));
+ assertThrows(IllegalArgumentException.class, () -> new CustomNameBranchProperty("foobar") );
+ }
+
+ @Test
+ public void defaultName() throws Exception {
+ try (final MockSCMController c = MockSCMController.create()) {
+ c.createRepository("foo");
+ BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
+ prj.setCriteria(null);
+ BranchSource source = new BranchSource(new MockSCMSource(c, "foo", new MockSCMDiscoverBranches()));
+ source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
+ new CustomNameBranchProperty(null)
+ }));
+ prj.getSourcesList().add(source);
+ prj.scheduleBuild2(0).getFuture().get();
+ r.waitUntilNoActivity();
+
+ FreeStyleProject master = prj.getItem("master");
+ assertNotNull(master);
+ assertNotNull(master.getProperty(BasicBranchProperty.class));
+
+ Branch branch = master.getProperty(BasicBranchProperty.class).getBranch();
+ assertNotNull(branch);
+ assertNotNull(branch.getProperty(CustomNameBranchProperty.class));
+ }
+ }
+
+ @Test
+ public void customName() throws Exception {
+ try (final MockSCMController c = MockSCMController.create()) {
+ c.createRepository("foo");
+ BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
+ prj.setCriteria(null);
+ BranchSource source = new BranchSource(new MockSCMSource(c, "foo", new MockSCMDiscoverBranches()));
+ source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
+ new CustomNameBranchProperty("app-{}")
+ }));
+ prj.getSourcesList().add(source);
+ prj.scheduleBuild2(0).getFuture().get();
+ r.waitUntilNoActivity();
+
+ assertNotNull(prj.getItem("app-master"));
+ assertNull(prj.getItem("master"));
+ }
+ }
+
+ @Test
+ public void multipleReplacement() throws Exception {
+ try (final MockSCMController c = MockSCMController.create()) {
+ c.createRepository("foo");
+ BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foo");
+ prj.setCriteria(null);
+ BranchSource source = new BranchSource(new MockSCMSource(c, "foo", new MockSCMDiscoverBranches()));
+ source.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
+ new CustomNameBranchProperty("app-{}/{}")
+ }));
+ prj.getSourcesList().add(source);
+ prj.scheduleBuild2(0).getFuture().get();
+ r.waitUntilNoActivity();
+
+ assertNotNull(prj.getItem("app-master/master"));
+ assertNull(prj.getItem("master"));
+ assertNull(prj.getItem("app-master"));
+ assertNull(prj.getItem("master/master"));
+ }
+ }
+ @Test
+ public void customNameWithMultipleSources() throws Exception {
+ try (final MockSCMController c = MockSCMController.create()) {
+ c.createRepository("foo");
+ c.createRepository("bar");
+ BasicMultiBranchProject prj = r.jenkins.createProject(BasicMultiBranchProject.class, "foobar");
+ prj.setCriteria(null);
+ BranchSource sourceFoo = new BranchSource(new MockSCMSource(c, "foo", new MockSCMDiscoverBranches()));
+ sourceFoo.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
+ new CustomNameBranchProperty("foo-{}")
+ }));
+ BranchSource sourceBar = new BranchSource(new MockSCMSource(c, "bar", new MockSCMDiscoverBranches()));
+ sourceBar.setStrategy(new DefaultBranchPropertyStrategy(new BranchProperty[]{
+ new CustomNameBranchProperty("bar-{}")
+ }));
+ prj.getSourcesList().add(sourceFoo);
+ prj.getSourcesList().add(sourceBar);
+ prj.scheduleBuild2(0).getFuture().get();
+ r.waitUntilNoActivity();
+
+ assertNotNull(prj.getItem("foo-master"));
+ assertNotNull(prj.getItem("bar-master"));
+ assertNull(prj.getItem("master"));
+ }
+ }
+}