diff --git a/pom.xml b/pom.xml
index 69e68824..35ced172 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,10 +81,16 @@
io.jenkins.tools.bom
bom-${jenkins.baseline}.x
- 5043.v855ff4819a_0f
+ 5659.vecf9e2dc5a_ed
pom
import
+
+
+ org.jenkins-ci.plugins
+ scm-api
+ 721.v54b_c43b_da_1db_
+
diff --git a/src/main/java/jenkins/branch/MultiBranchProject.java b/src/main/java/jenkins/branch/MultiBranchProject.java
index f49ca2c0..0f1b5b87 100644
--- a/src/main/java/jenkins/branch/MultiBranchProject.java
+++ b/src/main/java/jenkins/branch/MultiBranchProject.java
@@ -204,14 +204,7 @@ public void onLoad(ItemGroup extends Item> parent, String name) throws IOExcep
state.reset();
}
// optimize lookup of sources by building a temporary map that is equivalent to getSCMSource(id) in results
- Map sourceMap = new HashMap<>();
- for (BranchSource source : sources) {
- SCMSource s = source.getSource();
- String id = s.getId();
- if (!sourceMap.containsKey(id)) { // only the first match should win
- sourceMap.put(id, s);
- }
- }
+ Map sourceMap = buildSourceMap();
for (P item : getItems(factory::isProject)) {
Branch oldBranch = factory.getBranch(item);
SCMSource source = sourceMap.get(oldBranch.getSourceId());
@@ -250,6 +243,28 @@ public void onLoad(ItemGroup extends Item> parent, String name) throws IOExcep
}
}
+ private Map buildSourceMap() {
+ Map sourceMap = new HashMap<>();
+ int count = 1;
+ for (BranchSource source : sources) {
+ SCMSource s = source.getSource();
+ String id = s.getId();
+ if (id.isBlank()) {
+ // Generate ids of the form "1", "2", … on demand
+ while (sourceMap.containsKey(Integer.toString(count))) {
+ count++;
+ }
+ id = Integer.toString(count);
+ s.setId(id);
+ LOGGER.fine(() -> "assigned id to " + s + " in " + this);
+ }
+ if (!sourceMap.containsKey(id)) { // only the first match should win
+ sourceMap.put(id, s);
+ }
+ }
+ return sourceMap;
+ }
+
/**
* Consolidated initialization code.
*/
@@ -426,6 +441,7 @@ public void setSourcesList(List sources) throws IOException {
if (this.sources.isEmpty() || sources.isEmpty()) {
// easy
this.sources.replaceBy(sources);
+ buildSourceMap();
return;
}
Set oldIds = sourceIds(this.sources);
@@ -477,12 +493,29 @@ public void setSourcesList(List sources) throws IOException {
private Set sourceIds(List sources) {
Set result = new HashSet<>();
+ int count = 1;
for (BranchSource s : sources) {
- result.add(s.getSource().getId());
+ var id = s.getSource().getId();
+ if (id.isBlank()) {
+ // Generate ids of the form "1", "2", … on demand
+ while (result.contains(Integer.toString(count))) {
+ count++;
+ }
+ id = Integer.toString(count);
+ s.getSource().setId(id);
+ LOGGER.fine(() -> "assigned id to " + s.getSource() + " in " + this);
+ }
+ result.add(id);
}
return result;
}
+ @Override
+ public synchronized void save() throws IOException {
+ buildSourceMap();
+ super.save();
+ }
+
private boolean equalButForId(SCMSource a, SCMSource b) {
if (!a.getClass().equals(b.getClass())) {
return false;
diff --git a/src/test/java/integration/BrandingTest.java b/src/test/java/integration/BrandingTest.java
index 5265f172..61980fba 100644
--- a/src/test/java/integration/BrandingTest.java
+++ b/src/test/java/integration/BrandingTest.java
@@ -692,6 +692,14 @@ protected List retrieveActions(@NonNull SCMRevision revision, SCMHeadEve
}
@Extension()
- public static class DescriptorImpl extends MockSCMSource.DescriptorImpl {}
+ public static class DescriptorImpl extends MockSCMSource.DescriptorImpl {
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "Branded Mock SCM";
+ }
+
+ }
}
}
diff --git a/src/test/java/jenkins/branch/MultiBranchProjectSourcesTest.java b/src/test/java/jenkins/branch/MultiBranchProjectSourcesTest.java
new file mode 100644
index 00000000..a27d678f
--- /dev/null
+++ b/src/test/java/jenkins/branch/MultiBranchProjectSourcesTest.java
@@ -0,0 +1,81 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2025 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package jenkins.branch;
+
+import integration.harness.BasicMultiBranchProject;
+import java.util.List;
+import java.util.logging.Level;
+import jenkins.scm.impl.mock.MockSCMSource;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.LoggerRule;
+import org.jvnet.hudson.test.recipes.LocalData;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+
+public final class MultiBranchProjectSourcesTest {
+
+ @Rule public final JenkinsRule r = new JenkinsRule();
+ @Rule public final LoggerRule logging = new LoggerRule().record(MultiBranchProject.class, Level.FINE);
+
+ @LocalData
+ @Test public void oldMBPWithIds() throws Exception {
+ assertThat(idsOf(r.jenkins.getItemByFullName("mbp", BasicMultiBranchProject.class)), contains("30d15538-4f60-437f-81cd-9c46ee6e7ec6", "01d26901-3b39-45ac-915e-e65d806fbcba"));
+ }
+
+ @LocalData
+ @Test public void oldMBPWithoutIds() throws Exception {
+ assertThat(idsOf(r.jenkins.getItemByFullName("mbp", BasicMultiBranchProject.class)), contains("1", "2"));
+ }
+
+ @Test public void setSourcesList() throws Exception {
+ var mbp = r.createProject(BasicMultiBranchProject.class, "mbp");
+ var bs1 = new BranchSource(new MockSCMSource("c", "r1"));
+ var bs2 = new BranchSource(new MockSCMSource("c", "r2"));
+ mbp.setSourcesList(List.of(bs1, bs2));
+ assertThat(idsOf(mbp), contains("1", "2"));
+ mbp.setSourcesList(List.of(bs2));
+ assertThat(idsOf(mbp), contains("2"));
+ var bs3 = new BranchSource(new MockSCMSource("c", "r3"));
+ mbp.setSourcesList(List.of(bs1, bs2, bs3));
+ assertThat(idsOf(mbp), contains("1", "2", "3"));
+ }
+
+ @Test public void getSourcesListAdd() throws Exception {
+ var mbp = r.createProject(BasicMultiBranchProject.class, "mbp");
+ var bs1 = new BranchSource(new MockSCMSource("c", "r1"));
+ var bs2 = new BranchSource(new MockSCMSource("c", "r2"));
+ mbp.getSourcesList().add(bs1);
+ assertThat(idsOf(mbp), contains("1"));
+ mbp.getSourcesList().add(bs2);
+ assertThat(idsOf(mbp), contains("1", "2"));
+ }
+
+ private static List idsOf(MultiBranchProject, ?> mbp) {
+ return mbp.getSources().stream().map(bs -> bs.getSource().getId()).toList();
+ }
+
+}
diff --git a/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithIds/jobs/mbp/config.xml b/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithIds/jobs/mbp/config.xml
new file mode 100644
index 00000000..c33852b7
--- /dev/null
+++ b/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithIds/jobs/mbp/config.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ -1
+ -1
+ false
+
+
+ false
+
+
+
+
+ 30d15538-4f60-437f-81cd-9c46ee6e7ec6
+ c6fb4b23-2907-4f7c-9337-e07d687ecccb
+ xxx
+
+
+
+
+
+
+
+
+
+
+ 01d26901-3b39-45ac-915e-e65d806fbcba
+ c6fb4b23-2907-4f7c-9337-e07d687ecccb
+ yyy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithoutIds/jobs/mbp/config.xml b/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithoutIds/jobs/mbp/config.xml
new file mode 100644
index 00000000..a73a9795
--- /dev/null
+++ b/src/test/resources/jenkins/branch/MultiBranchProjectSourcesTest/oldMBPWithoutIds/jobs/mbp/config.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ -1
+ -1
+ false
+
+
+ false
+
+
+
+
+ c6fb4b23-2907-4f7c-9337-e07d687ecccb
+ xxx
+
+
+
+
+
+
+
+
+
+
+ c6fb4b23-2907-4f7c-9337-e07d687ecccb
+ yyy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file