diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fed0010..34d12011 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: check-latest: true cache: 'maven' - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 + uses: stCarolas/setup-maven@v5 with: maven-version: 3.9.6 - name: Build with Maven diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 05a25018..af74477f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: cache: maven - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 + uses: stCarolas/setup-maven@v5 with: maven-version: 3.9.6 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 97999c99..ea2ecbb0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,7 +23,7 @@ jobs: check-latest: true cache: 'maven' - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 + uses: stCarolas/setup-maven@v5 with: maven-version: 3.9.6 - name: Generate coverage with JaCoCo diff --git a/plugin/pom.xml b/plugin/pom.xml index 0e371933..1c83e0c6 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -4,7 +4,7 @@ org.jvnet.hudson.plugins analysis-pom - 6.14.0 + 7.0.0 @@ -30,6 +30,7 @@ -Djava.awt.headless=true -Xmx1024m -Djenkins.test.timeout=1000 --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED + 2.4.0 @@ -68,6 +69,7 @@ io.jenkins.plugins forensics-api + ${forensics-api.version} io.jenkins.plugins @@ -86,6 +88,7 @@ io.jenkins.plugins forensics-api + 2.3.0 test tests diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamer.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamer.java index c2792a83..a9ff3276 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamer.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamer.java @@ -124,7 +124,7 @@ static class BlameCallback extends AbstractRepositoryCallback invoke(final Repository repository, final VirtualChannel channel) throws InterruptedException { - try { + try (repository) { RemoteResultWrapper log = new RemoteResultWrapper<>(blames, "Errors while running Git blame:"); log.logInfo("-> Git commit ID = '%s'", headCommit.getName()); log.logInfo("-> Git working tree = '%s'", getWorkTree(repository)); @@ -148,9 +148,6 @@ public RemoteResultWrapper invoke(final Repository repository, final Vir return log; } - finally { - repository.close(); - } } /** diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/DeltaRepositoryCallback.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/DeltaRepositoryCallback.java index 216ed017..d5129918 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/DeltaRepositoryCallback.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/DeltaRepositoryCallback.java @@ -13,6 +13,7 @@ import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.lib.ObjectDatabase; diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculator.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculator.java index 9bdcb822..4b42f915 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculator.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculator.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; + import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -29,28 +31,29 @@ public class GitDeltaCalculator extends DeltaCalculator { static final String EMPTY_COMMIT_ERROR = "Calculating the Git code delta is not possible due to an unknown commit ID"; private final GitClient git; + private final String scmKey; /** * Constructor for an instance of {@link DeltaCalculator} which can be used for Git. * * @param git * The {@link GitClient} + * @param scmKey + * the key of the SCM repository (substring that must be part of the SCM key) */ - public GitDeltaCalculator(final GitClient git) { + public GitDeltaCalculator(final GitClient git, final String scmKey) { super(); this.git = git; + this.scmKey = scmKey; } @Override public Optional calculateDelta(final Run build, final Run referenceBuild, final String scmKeyFilter, final FilteredLog log) { - log.logInfo("-> Obtaining commits in SCM '%s' for current build '%s'", - scmKeyFilter, build.getFullDisplayName()); - Optional buildCommits = GitCommitsRecord.findRecordForScm(build, scmKeyFilter); - log.logInfo("-> Obtaining commits in SCM '%s' for reference build '%s'", - scmKeyFilter, referenceBuild.getFullDisplayName()); - Optional referenceCommits = GitCommitsRecord.findRecordForScm(referenceBuild, scmKeyFilter); + String scm = StringUtils.defaultIfEmpty(scmKeyFilter, scmKey); + Optional buildCommits = GitCommitsRecord.findRecordForScm(build, scm); + Optional referenceCommits = GitCommitsRecord.findRecordForScm(referenceBuild, scm); if (buildCommits.isPresent() && referenceCommits.isPresent()) { String currentCommit = getLatestCommit(build.getFullDisplayName(), buildCommits.get(), log); String referenceCommit = getLatestCommit(referenceBuild.getFullDisplayName(), referenceCommits.get(), log); @@ -77,11 +80,11 @@ public Optional calculateDelta(final Run build, final Run ref * Returns the latest commit of the {@link GitCommitsRecord commits record} of a Git repository. * * @param buildName - * The name of the build the commits record corresponds to + * the name of the build the commits record corresponds to * @param record - * The commits record + * the commits record * @param log - * The log + * the log * * @return the latest commit */ diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactory.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactory.java index 284cac90..6c01da98 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactory.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactory.java @@ -31,7 +31,7 @@ public Optional createDeltaCalculator(final SCM scm, final Run< GitClient client = validator.createClient(); logger.logInfo("-> Git delta calculator successfully created in working tree '%s'", new PathUtil().getAbsolutePath(client.getWorkTree().getRemote())); - return Optional.of(new GitDeltaCalculator(client)); + return Optional.of(new GitDeltaCalculator(client, scm.getKey())); } logger.logInfo("-> Git Delta Calculator could not be created for SCM '%s' in working tree '%s'", scm, workspace); diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidation.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidation.java deleted file mode 100644 index 43b66d96..00000000 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidation.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.jenkins.plugins.forensics.git.reference; - -import org.apache.commons.lang3.StringUtils; - -import edu.hm.hafner.util.VisibleForTesting; - -import hudson.util.ComboBoxModel; -import hudson.util.FormValidation; - -import io.jenkins.plugins.forensics.reference.ReferenceRecorder; -import io.jenkins.plugins.util.JenkinsFacade; - -/** - * Validates all properties of a configuration of a reference job. - * - * @author Ullrich Hafner - */ -class GitReferenceJobModelValidation { - private final JenkinsFacade jenkins; - - /** Creates a new descriptor. */ - GitReferenceJobModelValidation() { - this(new JenkinsFacade()); - } - - @VisibleForTesting - GitReferenceJobModelValidation(final JenkinsFacade jenkins) { - super(); - - this.jenkins = jenkins; - } - - /** - * Returns the model with the possible reference jobs. - * - * @return the model with the possible reference jobs - */ - public ComboBoxModel getAllJobs() { - ComboBoxModel model = new ComboBoxModel(jenkins.getAllJobNames()); - model.add(0, ReferenceRecorder.NO_REFERENCE_JOB); // make sure that no input is valid - return model; - } - - /** - * Performs on-the-fly validation of the reference job. - * - * @param referenceJobName - * the reference job - * - * @return the validation result - */ - public FormValidation validateJob(final String referenceJobName) { - if (ReferenceRecorder.NO_REFERENCE_JOB.equals(referenceJobName) - || StringUtils.isBlank(referenceJobName) - || jenkins.getJob(referenceJobName).isPresent()) { - return FormValidation.ok(); - } - return FormValidation.error(Messages.FieldValidator_Error_ReferenceJobDoesNotExist()); - } -} diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder.java index 5bffb4a4..1ca49187 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder.java @@ -7,18 +7,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.verb.POST; import org.jenkinsci.Symbol; import hudson.Extension; -import hudson.model.BuildableItem; -import hudson.model.Item; import hudson.model.Run; -import hudson.util.ComboBoxModel; -import hudson.util.FormValidation; import jenkins.scm.api.SCMHead; import io.jenkins.plugins.forensics.reference.ReferenceRecorder; @@ -50,8 +43,8 @@ public GitReferenceRecorder() { /** * Sets the maximal number of additional commits in the reference job that will be considered during the - * comparison with the current branch. In order to avoid an indefinite scanning of the build history until a - * matching reference has been found this value is used as a stop criteria. + * comparison with the current branch. To avoid an indefinite scanning of the build history until a + * matching reference has been found, this value is used as a stop criteria. * * @param maxCommits * maximal number of commits @@ -87,23 +80,19 @@ public boolean isSkipUnknownCommits() { final FilteredLog log) { Optional referenceCommit = GitCommitsRecord.findRecordForScm(lastCompletedBuildOfReferenceJob, getScm()); + + Optional> referenceBuild; if (referenceCommit.isPresent()) { - Optional thisCommit = GitCommitsRecord.findRecordForScm(owner, getScm()); - if (thisCommit.isPresent()) { - GitCommitsRecord commitsRecord = thisCommit.get(); - Optional> referencePoint = commitsRecord.getReferencePoint( - referenceCommit.get(), getMaxCommits(), isSkipUnknownCommits(), log); - if (referencePoint.isPresent()) { - log.logInfo("-> found build '%s' in reference job with matching commits", - referencePoint.get().getDisplayName()); - - return referencePoint; - } - } + referenceBuild = findByCommits(owner, referenceCommit.get(), log); } else { log.logInfo("-> selected build '%s' of reference job does not yet contain a `GitCommitsRecord`", lastCompletedBuildOfReferenceJob.getDisplayName()); + referenceBuild = Optional.empty(); + } + + if (referenceBuild.isPresent()) { + return referenceBuild; } Optional targetBranchHead = findTargetBranchHead(owner.getParent()); @@ -117,6 +106,30 @@ public boolean isSkipUnknownCommits() { return Optional.empty(); } + private Optional> findByCommits(final Run owner, final GitCommitsRecord referenceCommit, + final FilteredLog log) { + Optional ownerCommits = GitCommitsRecord.findRecordForScm(owner, getScm()); + if (ownerCommits.isPresent()) { + GitCommitsRecord commitsRecord = ownerCommits.get(); + Optional> referencePoint = commitsRecord.getReferencePoint( + referenceCommit, getMaxCommits(), isSkipUnknownCommits(), log); + if (referencePoint.isPresent()) { + Run referenceBuild = referencePoint.get(); + log.logInfo("-> found build '%s' in reference job with matching commits", + referenceBuild.getDisplayName()); + + return referencePoint; + } + else { + log.logInfo("-> found no build with matching commits"); + } + } + else { + log.logInfo("-> found no `GitCommitsRecord` in current build '%s'", owner.getDisplayName()); + } + return Optional.empty(); + } + @Override @SuppressFBWarnings("BC") public Descriptor getDescriptor() { @@ -129,22 +142,16 @@ public Descriptor getDescriptor() { @Extension @Symbol("discoverGitReferenceBuild") public static class Descriptor extends SimpleReferenceRecorderDescriptor { - private final JenkinsFacade jenkins; - /** - * Creates a new descriptor with the concrete services. + * Creates a new instance of {@link Descriptor}. */ - @SuppressWarnings("unused") // Required for extension point public Descriptor() { - this(new JenkinsFacade(), new GitReferenceJobModelValidation()); + this(new JenkinsFacade()); } @VisibleForTesting - Descriptor(final JenkinsFacade jenkins, final GitReferenceJobModelValidation model) { - super(); - - this.jenkins = jenkins; - this.model = model; + Descriptor(final JenkinsFacade jenkins) { + super(jenkins); } @NonNull @@ -152,43 +159,5 @@ public Descriptor() { public String getDisplayName() { return Messages.Recorder_DisplayName(); } - - private final GitReferenceJobModelValidation model; - - /** - * Returns the model with the possible reference jobs. - * - * @param project - * the project that is configured - * - * @return the model with the possible reference jobs - */ - @POST - public ComboBoxModel doFillReferenceJobItems(@AncestorInPath final BuildableItem project) { - if (jenkins.hasPermission(Item.CONFIGURE, project)) { - return model.getAllJobs(); - } - return new ComboBoxModel(); - } - - /** - * Performs on-the-fly validation of the reference job. - * - * @param project - * the project that is configured - * @param referenceJob - * the reference job - * - * @return the validation result - */ - @POST - @SuppressWarnings("unused") // Used in jelly validation - public FormValidation doCheckReferenceJob(@AncestorInPath final BuildableItem project, - @QueryParameter final String referenceJob) { - if (!jenkins.hasPermission(Item.CONFIGURE, project)) { - return FormValidation.ok(); - } - return model.validateJob(referenceJob); - } } } diff --git a/plugin/src/main/java/io/jenkins/plugins/forensics/git/util/GitCommitDecoratorFactory.java b/plugin/src/main/java/io/jenkins/plugins/forensics/git/util/GitCommitDecoratorFactory.java index 67f93582..0cdc839b 100644 --- a/plugin/src/main/java/io/jenkins/plugins/forensics/git/util/GitCommitDecoratorFactory.java +++ b/plugin/src/main/java/io/jenkins/plugins/forensics/git/util/GitCommitDecoratorFactory.java @@ -36,5 +36,4 @@ public Optional createCommitDecorator(final SCM scm, final Filt } return Optional.empty(); } - } diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.jelly b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.jelly index 69894608..586badde 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.jelly +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.jelly @@ -5,11 +5,11 @@ - + - + diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.properties b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.properties index 52476ae1..81e9216c 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.properties +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/config.properties @@ -1,11 +1,8 @@ title.referenceJob=Reference Job -description.referenceJob=A reference job defines the baseline for tools that want to create relative build reports. -title.scm=SCM Key -description.scm=Specify the key of your repository (substring is sufficient) if you are using multiple SCMs in your job. -title.maxCommits=#Commits -description.maxCommits=Defines how many commits of a job's Git history should be evaluated to find the reference build. -title.defaultBranch=Default Branch -description.defaultBranch=The default branch to use as reference job for multi-branch pipelines. If empty then `master` \ - will be used. -title.skipUnknownCommits=Ignore builds with commits that are not part of the current build -title.latestBuildIfNotFound=Fallback to latest build if no reference build has been found +description.referenceJob=Baseline job for plugins that want to create relative build reports +title.scm=Source Control Repository Key +title.maxCommits=# Commits +description.maxCommits=Defines how many commits of a job''s Git history should be evaluated to find the reference build. +title.targetBranch=Target Branch +title.skipUnknownCommits=Ignore reference job builds with commits that are not part of the current build +title.latestBuildIfNotFound=Fallback to the latest-completed build if no reference build has been found diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-latestBuildIfNotFound.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-latestBuildIfNotFound.html new file mode 100644 index 00000000..bc78af71 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-latestBuildIfNotFound.html @@ -0,0 +1,3 @@ +If enabled, then the last completed build of the reference job will be selected if no other reference build +can be found in the baseline, that contains the required commits or has the required build result. +This is useful if you want to always compute a delta, even if the exact reference build cannot be found. diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-maxCommits.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-maxCommits.html index fdb6ef84..701b5d55 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-maxCommits.html +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-maxCommits.html @@ -1,4 +1,4 @@ -The Default value is 100 commits, so if the branch has more than 100 commits since the last contact with the master branch -the reference finder will stop. If for some reason the finder will not be able to find an intersection between the current branch -and the master branch (for unknown reason) this check will ensure that the finder will not take forever to search for it. But -this scenario should not occur and usually developing branches should not be that far from the master branch. \ No newline at end of file +Defines the maximum number of commits that will be inspected to find the intersection between the current branch +and the target branch (main branch). The default value is 100 commits. If a branch has more than the specified number +of commits (since the last merge with the main branch), then the reference finder will stop and return an empty result. +This helps to skip computations of delta results that contain too many differences. diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-referenceJob.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-referenceJob.html new file mode 100644 index 00000000..85fe1581 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-referenceJob.html @@ -0,0 +1,14 @@ +

+ The reference job is the baseline used to determine the relative differences to the current build. + Several plugins that report build statistics (test results, code coverage, metrics, static + analysis warnings) typically show their reports in two different ways: either as absolute report + (e.g., total number of tests or warnings, overall code coverage) or as relative delta report (e.g., additional + tests, increased or decreased coverage, new or fixed warnings). To compute a relative delta report, a plugin needs + to select another build to compare the current results to (a so-called reference build). This reference build is + obtained from the specified reference job. +

+

+ In Multibranch Pipelines this parameter can be left empty: in these jobs, the plugin selects the build automatically + from the associated job that builds the main target branch. For all other jobs, the last completed build of the + current job will be used as reference build if the reference job is not specified. +

diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-requiredResult.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-requiredResult.html new file mode 100644 index 00000000..6f12e271 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-requiredResult.html @@ -0,0 +1,15 @@ +When a reference build is searched for in the baseline (see the parameter reference job) then it +sometimes makes sense to only consider builds that have a certain result. +For example, you may want to skip the computation of a delta coverage when the reference build has +failed tests. Or you may want to skip the computation of new warnings when the reference build has +a compilation error. + +The following enumeration shows the possible values for this parameter and the corresponding behavior: +
+
SUCCESS
+
The selected build must be successful
+
UNSTABLE
+
The selected build must be successful or unstable
+
FAILURE
+
The result of the build is not relevant (this is the default behavior if unset)
+
diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-scm.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-scm.html new file mode 100644 index 00000000..96d6a8dd --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-scm.html @@ -0,0 +1,5 @@ +
+ Specify the key of your repository (substring is sufficient) if you are using multiple SCMs in your job. + When your job is composed of several SCM checkouts (modules, pipeline libraries, etc.) then Jenkins stores + all those repositories in an unsorted way. +
diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-skipUnknownCommits.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-skipUnknownCommits.html new file mode 100644 index 00000000..d03c7064 --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-skipUnknownCommits.html @@ -0,0 +1,4 @@ +If enabled, then a build of the reference job will be skipped if one of the commits is unknown in the current +branch. This is useful if you want to compute the delta only with builds that contain all the commits that +are part of the current job. That means that the reference will not contain newer and unrelated commits that may +affect the build results. diff --git a/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-targetBranch.html b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-targetBranch.html new file mode 100644 index 00000000..b57ffa0c --- /dev/null +++ b/plugin/src/main/resources/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorder/help-targetBranch.html @@ -0,0 +1,5 @@ +
+ The target branch for this job is the branch you want to merge your changes into. + If left empty, the plugin will use the branch main as default value. + In MultiBranch Pipelines you can leave this field empty as the target branch will be computed automatically. +
diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/PluginArchitectureTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/PluginArchitectureTest.java index b11437e8..4ba7c949 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/PluginArchitectureTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/PluginArchitectureTest.java @@ -57,5 +57,4 @@ class PluginArchitectureTest { @ArchTest static final ArchRule USE_POST_FOR_LIST_MODELS_RULE = PluginArchitectureRules.USE_POST_FOR_LIST_AND_COMBOBOX_FILL; - } diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactoryTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactoryTest.java index 5762cdb8..d62d5e65 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactoryTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactoryTest.java @@ -34,7 +34,6 @@ * @author Florian Orendi */ class GitDeltaCalculatorFactoryTest { - private static final TaskListener NULL_LISTENER = TaskListener.NULL; @Test diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorITest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorITest.java index a36e2563..5ea7151d 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorITest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Test; import edu.hm.hafner.util.FilteredLog; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.model.FreeStyleProject; import hudson.model.Run; @@ -35,9 +34,6 @@ class GitDeltaCalculatorITest extends GitITest { private static final String EMPTY_SCM_KEY = ""; private static final String EMPTY_FILE_PATH = ""; - /** - * The delta result should be empty if there are invalid commits. - */ @Test void shouldCreateEmptyDeltaIfCommitsAreInvalid() { GitDeltaCalculator deltaCalculator = createDeltaCalculator(); @@ -46,11 +42,7 @@ void shouldCreateEmptyDeltaIfCommitsAreInvalid() { assertThat(deltaCalculator.calculateDelta(mock(Run.class), mock(Run.class), EMPTY_SCM_KEY, log)).isEmpty(); } - /** - * The Git diff file should be created properly. - */ @Test - @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "The cast is confirmed via an assertion") void shouldCreateDiffFile() { FreeStyleProject job = createJobWithReferenceRecorder(); @@ -83,6 +75,9 @@ void shouldCreateDiffFile() { + "@@ -0,0 +1 @@\n" + "+content\n" + "\\ No newline at end of file\n")); + assertThat(delta.getFileChangesMap().values()).hasSize(1) + .first().satisfies(fileChanges -> + assertThat(fileChanges.getModifiedLines()).containsExactly(1)); } /** @@ -108,6 +103,8 @@ void shouldDetermineAddedFile() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).containsExactly(1); + assertThat(fileChanges.getFileName()).isEqualTo(newFileName); assertThat(fileChanges.getOldFileName()).isEqualTo(EMPTY_FILE_PATH); assertThat(fileChanges.getFileEditType()).isEqualTo(FileEditType.ADD); @@ -134,6 +131,8 @@ void shouldDetermineModifiedFile() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).containsExactly(1); + assertThat(fileChanges.getFileName()).isEqualTo(INITIAL_FILE); assertThat(fileChanges.getOldFileName()).isEqualTo(INITIAL_FILE); assertThat(fileChanges.getFileEditType()).isEqualTo(FileEditType.MODIFY); @@ -161,6 +160,8 @@ void shouldDetermineDeletedFile() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).isEmpty(); + assertThat(fileChanges.getFileName()).isEqualTo(EMPTY_FILE_PATH); assertThat(fileChanges.getOldFileName()).isEqualTo(INITIAL_FILE); assertThat(fileChanges.getFileEditType()).isEqualTo(FileEditType.DELETE); @@ -188,6 +189,8 @@ void shouldDetermineRenamedFile() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).isEmpty(); + assertThat(fileChanges.getFileName()).isEqualTo(ADDITIONAL_FILE); assertThat(fileChanges.getOldFileName()).isEqualTo(INITIAL_FILE); assertThat(fileChanges.getFileEditType()).isEqualTo(FileEditType.RENAME); @@ -215,6 +218,8 @@ void shouldDetermineAddedLines() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).containsExactly(2, 3); + Change change = getSingleChangeOfType(fileChanges, ChangeEditType.INSERT); assertThat(change.getEditType()).isEqualTo(ChangeEditType.INSERT); assertThat(change.getChangedFromLine()).isEqualTo(1); @@ -244,6 +249,8 @@ void shouldDetermineModifiedLines() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).containsExactly(2, 3); + Change change = getSingleChangeOfType(fileChanges, ChangeEditType.REPLACE); assertThat(change.getEditType()).isEqualTo(ChangeEditType.REPLACE); assertThat(change.getChangedFromLine()).isEqualTo(2); @@ -273,6 +280,8 @@ void shouldDetermineDeletedLines() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).isEmpty(); + Change change = getSingleChangeOfType(fileChanges, ChangeEditType.DELETE); assertThat(change.getEditType()).isEqualTo(ChangeEditType.DELETE); assertThat(change.getChangedFromLine()).isEqualTo(2); @@ -302,6 +311,7 @@ void shouldDetermineAllChangeTypesTogether() { Delta delta = result.get(); FileChanges fileChanges = getSingleFileChanges(delta); + assertThat(fileChanges.getModifiedLines()).containsExactly(1, 3); Change replace = getSingleChangeOfType(fileChanges, ChangeEditType.REPLACE); assertThat(replace.getEditType()).isEqualTo(ChangeEditType.REPLACE); @@ -345,7 +355,7 @@ private Change getSingleChangeOfType(final FileChanges fileChanges, final Change /** * Gets the first {@link FileChanges} of a calculated code {@link Delta}, when exactly a single file has changed and - * checks if the found values are properly. + * checks if the found values are properly stored. * * @param delta * The code delta @@ -353,11 +363,12 @@ private Change getSingleChangeOfType(final FileChanges fileChanges, final Change * @return the found changes. */ private FileChanges getSingleFileChanges(final Delta delta) { - final Collection fileChangesList = delta.getFileChangesMap().values(); - assertThat(fileChangesList.size()).isEqualTo(1); - Optional fileChanges = fileChangesList.stream().findFirst(); - assertThat(fileChanges).isNotEmpty(); - return fileChanges.get(); + Collection fileChanges = delta.getFileChangesMap().values(); + assertThat(fileChanges).hasSize(1); + + Optional firstChanges = fileChanges.stream().findFirst(); + assertThat(firstChanges).isNotEmpty(); + return firstChanges.get(); } /** @@ -389,7 +400,7 @@ private FilteredLog createLog() { * @return the created delta calculator */ private GitDeltaCalculator createDeltaCalculator() { - return new GitDeltaCalculator(createGitClient()); + return new GitDeltaCalculator(createGitClient(), StringUtils.EMPTY); } /** diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorTest.java index 8637e418..9f5e9d4f 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorTest.java @@ -22,13 +22,12 @@ * @author Florian Orendi */ class GitDeltaCalculatorTest { - private static final String EMPTY_SCM_KEY = ""; @Test void shouldAbortIfCommitsAreEmpty() { GitClient gitClient = mock(GitClient.class); - GitDeltaCalculator deltaCalculator = new GitDeltaCalculator(gitClient); + GitDeltaCalculator deltaCalculator = new GitDeltaCalculator(gitClient, StringUtils.EMPTY); FilteredLog log = new FilteredLog(StringUtils.EMPTY); Optional result = deltaCalculator.calculateDelta(mock(Run.class), mock(Run.class), EMPTY_SCM_KEY, log); diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaTest.java index 6bea30e6..95c42025 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/delta/GitDeltaTest.java @@ -14,7 +14,6 @@ * @author Florian Orendi */ class GitDeltaTest { - private static final String DIFF_FILE = "testContent"; @Test diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitMergeITest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitMergeITest.java index 4e49ec12..54b3a884 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitMergeITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitMergeITest.java @@ -23,7 +23,6 @@ * @author Florian Orendi */ class GitMergeITest extends GitITest { - private static final String EMPTY_COMMIT = ObjectId.zeroId().name(); private static final String MAIN_JOB_NAME = "Main_Job"; diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidationTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidationTest.java deleted file mode 100644 index 0a258875..00000000 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceJobModelValidationTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.jenkins.plugins.forensics.git.reference; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; - -import org.junit.jupiter.api.Test; - -import hudson.model.Job; -import hudson.util.ComboBoxModel; -import hudson.util.FormValidation.Kind; - -import io.jenkins.plugins.util.JenkinsFacade; - -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests the class {@link GitReferenceJobModelValidation}. - * - * @author Arne Schöntag - * @author Stephan Plöderl - * @author Ullrich Hafner - */ -class GitReferenceJobModelValidationTest { - private static final String NO_REFERENCE_JOB = "-"; - - @Test - void doFillReferenceJobItemsShouldBeNotEmpty() { - JenkinsFacade jenkins = mock(JenkinsFacade.class); - when(jenkins.getAllJobNames()).thenReturn(new HashSet<>()); - - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(jenkins); - - assertThat(model.getAllJobs()).containsExactly(NO_REFERENCE_JOB); - } - - @Test - void shouldValidateToOkIfEmpty() { - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(); - - assertSoftly(softly -> { - softly.assertThat(model.validateJob(NO_REFERENCE_JOB).kind).isEqualTo(Kind.OK); - softly.assertThat(model.validateJob("").kind).isEqualTo(Kind.OK); - softly.assertThat(model.validateJob(null).kind).isEqualTo(Kind.OK); - }); - } - - @Test - void doCheckReferenceJobShouldBeOkWithValidValues() { - JenkinsFacade jenkins = mock(JenkinsFacade.class); - - Job job = mock(Job.class); - String jobName = "referenceJob"; - when(jenkins.getJob(jobName)).thenReturn(Optional.of(job)); - - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(jenkins); - - assertThat(model.validateJob(jobName).kind).isEqualTo(Kind.OK); - } - - @Test - void doCheckReferenceJobShouldBeNOkWithInvalidValue() { - String referenceJob = "referenceJob"; - JenkinsFacade jenkins = mock(JenkinsFacade.class); - when(jenkins.getJob(referenceJob)).thenReturn(Optional.empty()); - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(jenkins); - - assertThat(model.validateJob(referenceJob).kind).isEqualTo(Kind.ERROR); - assertThat(model.validateJob(referenceJob).getLocalizedMessage()).isEqualTo( - "There is no such job - maybe the job has been renamed?"); - } - - @Test - void shouldContainEmptyJobPlaceHolder() { - JenkinsFacade jenkins = mock(JenkinsFacade.class); - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(jenkins); - ComboBoxModel actualModel = model.getAllJobs(); - - assertThat(actualModel).hasSize(1); - assertThat(actualModel).containsExactly(NO_REFERENCE_JOB); - } - - @Test - void shouldContainSingleElementAndPlaceHolder() { - JenkinsFacade jenkins = mock(JenkinsFacade.class); - Job job = mock(Job.class); - String name = "Job Name"; - when(jenkins.getFullNameOf(job)).thenReturn(name); - when(jenkins.getAllJobNames()).thenReturn(Collections.singleton(name)); - - GitReferenceJobModelValidation model = new GitReferenceJobModelValidation(jenkins); - - ComboBoxModel actualModel = model.getAllJobs(); - - assertThat(actualModel).hasSize(2); - assertThat(actualModel).containsExactly(NO_REFERENCE_JOB, name); - } -} diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderITest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderITest.java index 0c4945d9..1d00b13f 100644 --- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderITest.java +++ b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderITest.java @@ -8,6 +8,9 @@ import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.Issue; import com.cloudbees.hudson.plugins.folder.computed.FolderComputation; @@ -18,6 +21,7 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import hudson.model.Result; import hudson.model.Run; import jenkins.branch.BranchProperty; import jenkins.branch.BranchSource; @@ -59,6 +63,7 @@ class GitReferenceRecorderITest extends GitITest { private static final String MULTI_BRANCH_PROJECT = "Found a `MultiBranchProject`, trying to resolve the target branch from the configuration"; private static final String MAIN_IS_TARGET = "-> using target branch 'main' as configured in step"; + private static final String NOT_FOUND_MESSAGE = "No reference build with required status found that contains matching commits"; /** * Runs a pipeline and verifies that the recorder does not break the build if Git is not configured. @@ -76,13 +81,12 @@ void shouldHandleJobsWithoutGitGracefully() { job.setDefinition(new CpsFlowDefinition("node {discoverGitReferenceBuild(referenceJob: 'no-git')}", true)); buildSuccessfully(job); - assertThat(getConsoleLog(job.getLastBuild())).contains( - "No reference build found that contains matching commits"); + assertThat(getConsoleLog(job.getLastBuild())).contains(NOT_FOUND_MESSAGE); } /** * Creates a pipeline for the main branch and another pipeline for the feature branch, builds them and checks if - * the correct reference build will be found. + * the correct reference build is found. *
      * {@code
      * M:  [M1]#1
@@ -95,7 +99,7 @@ void shouldFindCorrectBuildInPipelines() {
         WorkflowJob mainBranch = createPipeline(MAIN);
         mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN)));
 
-        Run masterBuild = buildSuccessfully(mainBranch);
+        Run mainBuild = buildSuccessfully(mainBranch);
 
         createFeatureBranchAndAddCommits();
 
@@ -104,7 +108,7 @@ void shouldFindCorrectBuildInPipelines() {
                 "discoverGitReferenceBuild(referenceJob: '" + MAIN + "')",
                 "gitDiffStat()"));
 
-        verifyPipelineResult(masterBuild, featureBranch);
+        verifyPipelineResult(mainBuild, featureBranch);
 
         assertThat(getCommitStatisticsOf(featureBranch.getLastBuild()))
                 .hasCommitCount(1)
@@ -113,9 +117,172 @@ void shouldFindCorrectBuildInPipelines() {
                 .hasDeletedLines(0);
     }
 
+    /**
+     * Creates a pipeline that checks if the reference build is found if the main is one commit and build ahead of the
+     * feature branch.
+     *
+     * 
+     * {@code
+     * M:  [M1]#1 - [M2]#2
+     *       \
+     *   F:  [F1]#1}
+     * 
+ * @see #shouldHandleExtraCommitsAfterBranchPointOnMain() + */ + @Test + void shouldFindCorrectBuildInPipelinesAfterBranchPointOnMain() { + WorkflowJob mainBranch = createPipeline(MAIN); + mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN))); + + Run mainBuild = buildSuccessfully(mainBranch); + + createFeatureBranchAndAddCommits(); + + addAdditionalFileTo(MAIN); + + buildAgain(mainBranch); + + WorkflowJob featureBranch = createPipeline(FEATURE); + featureBranch.setDefinition(asStage(createLocalGitCheckout(FEATURE), + "discoverGitReferenceBuild(referenceJob: '" + MAIN + "')")); + + Run featureBuild = buildSuccessfully(featureBranch); + assertThat(featureBuild.getNumber()).isEqualTo(1); + + assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull() + .hasOwner(featureBuild) + .hasReferenceBuildId(mainBuild.getExternalizableId()) + .hasReferenceBuild(Optional.of(mainBuild)) + .hasMessages("Configured reference job: 'main'", + "Found reference build '#1' for target branch"); + } + + /** + * Creates a pipeline that ignores failed builds. + * + *
+     * {@code
+     * M:  [M1]#1 - [M2]#2
+     *       \
+     *   F:  [F1]#1}
+     * 
+ * + * @param requiredResult + * the required result + * @param latestBuildIfNotFound + * determines whether the latest build should be used if the required result is not found + * + * @see #shouldHandleExtraCommitsAfterBranchPointOnMain() + */ + @Issue("JENKINS-72015") + @ParameterizedTest(name = "should skip failed builds: status: {0} - latestBuildIfNotFound: {1}") + @CsvSource({ + "SUCCESS, false", "UNSTABLE, false", ", false", + "SUCCESS, true ", "UNSTABLE, true ", ", true "}) + void shouldSkipFailedBuildsIfStatusIsWorseThanRequired(final String requiredResult, final boolean latestBuildIfNotFound) { + WorkflowJob mainBranch = createPipeline(MAIN); + mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN) + + "error('FAILURE')\n")); + + buildWithResult(mainBranch, Result.FAILURE); + + createFeatureBranchAndAddCommits(); + + addAdditionalFileTo(MAIN); + + var latestBuild = buildAgain(mainBranch); + + WorkflowJob featureBranch = createPipeline(FEATURE); + var requiredParameter = StringUtils.isBlank(requiredResult) ? StringUtils.EMPTY : ", requiredResult: '" + requiredResult + "'"; + featureBranch.setDefinition(asStage(createLocalGitCheckout(FEATURE), + "discoverGitReferenceBuild(" + + "referenceJob: '" + MAIN + "'" + + requiredParameter + + ", latestBuildIfNotFound: " + latestBuildIfNotFound + ")")); + + Run featureBuild = buildSuccessfully(featureBranch); + assertThat(featureBuild.getNumber()).isEqualTo(1); + + var expectedResult = StringUtils.isBlank(requiredResult) ? "UNSTABLE" : requiredResult; + + var referenceBuildAssert = assertThat(featureBuild.getAction(ReferenceBuild.class)) + .isNotNull() + .hasOwner(featureBuild) + .hasMessages("Configured reference job: 'main'", + "-> found build '#1' in reference job with matching commits", + "-> ignoring reference build '#1' or one of its predecessors since none have a result of " + + expectedResult + " or better", + NOT_FOUND_MESSAGE); + + if (latestBuildIfNotFound) { + referenceBuildAssert + .hasReferenceBuild(Optional.of(latestBuild)) + .hasReferenceBuildId(latestBuild.getExternalizableId()) + .hasMessages("Falling back to latest completed build of reference job: '#2'"); + } + else { + referenceBuildAssert + .hasReferenceBuild(Optional.empty()); + } + } + + /** + * Creates a pipeline that uses a previous build of the actual reference build, because the reference build failed. + * + *
+     * {@code
+     * M:  [M1]#1 - [M2]#2
+     *               \
+     *            F:  [F1]#1}
+     * 
+ * + * @param requiredResult + * the required result + * + * @see #shouldHandleExtraCommitsAfterBranchPointOnMain() + */ + @Issue("JENKINS-72015") + @ParameterizedTest(name = "should skip failed builds: status: {0}") + @ValueSource(strings = {"SUCCESS", "UNSTABLE", ""}) + void shouldUseSkipFailedBuildsIfStatusIsWorseThanRequired(final String requiredResult) { + WorkflowJob mainBranch = createPipeline(MAIN); + + mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN))); + + Run successful = buildSuccessfully(mainBranch); + + addAdditionalFileTo(MAIN); + mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN) + + "error('FAILURE')\n")); + + buildWithResult(mainBranch, Result.FAILURE); + + createFeatureBranchAndAddCommits(); + + WorkflowJob featureBranch = createPipeline(FEATURE); + var requiredParameter = StringUtils.isBlank(requiredResult) ? StringUtils.EMPTY : ", requiredResult: '" + requiredResult + "'"; + featureBranch.setDefinition(asStage(createLocalGitCheckout(FEATURE), + "discoverGitReferenceBuild(" + + "referenceJob: '" + MAIN + "'" + + requiredParameter + ")")); + + Run featureBuild = buildSuccessfully(featureBranch); + assertThat(featureBuild.getNumber()).isEqualTo(1); + + assertThat(featureBuild.getAction(ReferenceBuild.class)) + .isNotNull() + .hasOwner(featureBuild) + .hasMessages("Configured reference job: 'main'", + "-> found build '#2' in reference job with matching commits", + "Found reference build '#2' for target branch", + "-> Previous build '#1' has a result SUCCESS") + .hasReferenceBuild(Optional.of(successful)) + .hasReferenceBuildId(successful.getExternalizableId()); + } + /** * Creates a pipeline for the main branch and another pipeline for the feature branch, builds them and removes the - * commits action from the reference job. This simulates a setup where the last master build was finished before + * commits action from the reference job. This simulates a setup where the last main build was finished before * the Git forensics plugin has been added. */ @Test @@ -123,8 +290,8 @@ void shouldFindNoReferenceBuildIfReferenceJobHasNoCommitsAction() { WorkflowJob mainBranch = createPipeline(MAIN); mainBranch.setDefinition(asStage(createLocalGitCheckout(MAIN))); - Run masterBuild = buildSuccessfully(mainBranch); - masterBuild.removeAction(masterBuild.getAction(GitCommitsRecord.class)); // simulate a master build without records + Run mainBuild = buildSuccessfully(mainBranch); + mainBuild.removeAction(mainBuild.getAction(GitCommitsRecord.class)); // simulate a main build without records createFeatureBranchAndAddCommits(); WorkflowJob featureBranch = createPipeline(FEATURE); @@ -148,7 +315,7 @@ private CommitStatistics getCommitStatisticsOf(final WorkflowRun lastBuild) { /** * Creates a pipeline for the main branch and another pipeline for the feature branch, builds them and checks if - * the correct reference build will be found. The main branch contains an additional but unrelated SCM + * the correct reference build is found. The main branch contains an additional but unrelated SCM * repository. *
      * {@code
@@ -167,7 +334,7 @@ void shouldFindCorrectBuildInPipelinesWithMultipleReposInReference() {
                 createForensicsCheckoutStep(),
                 createLocalGitCheckout(MAIN)));
 
-        Run masterBuild = buildSuccessfully(mainBranch);
+        Run mainBuild = buildSuccessfully(mainBranch);
 
         createFeatureBranchAndAddCommits();
 
@@ -176,12 +343,12 @@ void shouldFindCorrectBuildInPipelinesWithMultipleReposInReference() {
                 createLocalGitCheckout(FEATURE),
                 "discoverGitReferenceBuild(referenceJob: '" + MAIN + "', scm: 'git file')"));
 
-        verifyPipelineResult(masterBuild, featureBranch);
+        verifyPipelineResult(mainBuild, featureBranch);
     }
 
     /**
      * Creates a pipeline for the main branch and another pipeline for the feature branch, builds them and checks if
-     * the correct reference build will be found. The main branch contains an additional but unrelated SCM
+     * the correct reference build is found. The main branch contains an additional but unrelated SCM
      * repository.
      * 
      * {@code
@@ -199,7 +366,7 @@ void shouldFindCorrectBuildInPipelinesWithMultipleReposInFeature() {
         mainBranch.setDefinition(asStage(
                 createLocalGitCheckout(MAIN)));
 
-        Run masterBuild = buildSuccessfully(mainBranch);
+        Run mainBuild = buildSuccessfully(mainBranch);
 
         createFeatureBranchAndAddCommits();
 
@@ -209,7 +376,7 @@ void shouldFindCorrectBuildInPipelinesWithMultipleReposInFeature() {
                 createLocalGitCheckout(FEATURE),
                 "discoverGitReferenceBuild(referenceJob: '" + MAIN + "', scm: 'git file')"));
 
-        verifyPipelineResult(masterBuild, featureBranch);
+        verifyPipelineResult(mainBuild, featureBranch);
     }
 
     private String createLocalGitCheckout(final String branch) {
@@ -217,7 +384,7 @@ private String createLocalGitCheckout(final String branch) {
                 + "branches: [[name: '" + branch + "' ]],\n"
                 + getUrl()
                 + "extensions: [[$class: 'RelativeTargetDirectory', \n"
-                + "            relativeTargetDir: 'forensics-api']]])";
+                + "            relativeTargetDir: 'forensics-api']]])\n";
     }
 
     private String createForensicsCheckoutStep() {
@@ -228,22 +395,22 @@ private String createForensicsCheckoutStep() {
                 + "            relativeTargetDir: 'forensics-api']]])";
     }
 
-    private void verifyPipelineResult(final Run masterBuild, final WorkflowJob featureBranch) {
+    private void verifyPipelineResult(final Run mainBuild, final WorkflowJob featureBranch) {
         Run featureBuild = buildSuccessfully(featureBranch);
         assertThat(featureBuild.getNumber()).isEqualTo(1);
 
         String featureCommit = getHead();
         checkout(MAIN);
-        String masterCommit = getHead();
+        String mainCommit = getHead();
 
         assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(featureBuild)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild))
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild))
                 .hasMessages("Configured reference job: 'main'",
                         String.format("-> detected 2 commits in current branch (last one: '%s')", DECORATOR.asText(featureCommit)),
-                        String.format("-> adding 1 commits from build '#1' of reference job (last one: '%s')", DECORATOR.asText(masterCommit)),
-                        String.format("-> found a matching commit in current branch and target branch: '%s'", DECORATOR.asText(masterCommit)),
+                        String.format("-> adding 1 commits from build '#1' of reference job (last one: '%s')", DECORATOR.asText(mainCommit)),
+                        String.format("-> found a matching commit in current branch and target branch: '%s'", DECORATOR.asText(mainCommit)),
                         "Found reference build '#1' for target branch");
     }
 
@@ -265,8 +432,8 @@ void shouldFindCorrectBuildForMultibranchPipeline() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         String mainCommit = getHead();
         String featureCommit = createFeatureBranchAndAddCommits();
@@ -277,8 +444,8 @@ void shouldFindCorrectBuildForMultibranchPipeline() {
 
         assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(featureBuild)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild))
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild))
                 .hasMessages(MULTI_BRANCH_PROJECT,
                         MAIN_IS_TARGET,
                         String.format("-> detected 3 commits in current branch (last one: '%s')", DECORATOR.asText(featureCommit)),
@@ -304,8 +471,8 @@ void shouldFindCorrectBuildForMultibranchPipelineWithComplexBranchNames() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyBuild(project, 1, target, "main content");
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyBuild(project, 1, target, "main content");
+        verifyRecordSize(mainBuild, 2);
 
         String feature = "bugfixes/hotfix-124";
         createBranchAndAddCommits(feature, "targetBranch: '" + target + "'");
@@ -316,8 +483,8 @@ void shouldFindCorrectBuildForMultibranchPipelineWithComplexBranchNames() {
 
         assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(featureBuild)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild));
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild));
     }
 
     /**
@@ -331,20 +498,20 @@ void shouldFindCorrectBuildForMultibranchPipelineWithComplexBranchNames() {
      * 
*/ @Test - void shouldHandleExtraCommitsAfterBranchPointOnMaster() { + void shouldHandleExtraCommitsAfterBranchPointOnMain() { WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject(); buildProject(project); - WorkflowRun masterBuild = verifyMasterBuild(project, 1); - verifyRecordSize(masterBuild, 2); + WorkflowRun mainBuild = verifyMainBuild(project, 1); + verifyRecordSize(mainBuild, 2); String initialMain = getHead(); String featureCommit = createFeatureBranchAndAddCommits(); String mainCommit = addAdditionalFileTo(MAIN); - buildAgain(masterBuild.getParent()); - WorkflowRun nextMaster = verifyMasterBuild(project, 2); + buildAgain(mainBuild.getParent()); + WorkflowRun nextMaster = verifyMainBuild(project, 2); verifyRecordSize(nextMaster, 1); buildProject(project); @@ -353,8 +520,8 @@ void shouldHandleExtraCommitsAfterBranchPointOnMaster() { assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull() .hasOwner(featureBuild) - .hasReferenceBuildId(masterBuild.getExternalizableId()) - .hasReferenceBuild(Optional.of(masterBuild)) + .hasReferenceBuildId(mainBuild.getExternalizableId()) + .hasReferenceBuild(Optional.of(mainBuild)) .hasMessages(MULTI_BRANCH_PROJECT, MAIN_IS_TARGET, String.format("-> detected 3 commits in current branch (last one: '%s')", DECORATOR.asText(featureCommit)), @@ -372,7 +539,7 @@ void shouldHandleExtraCommitsAfterBranchPointOnMaster() { } /** - * Checks if the reference build is found even if the reference build has commits that the feature build does not. + * Checks if the reference build is found even if the reference build contains commits that the feature build does not. * This also checks the algorithm if the config {@code skipUnknownCommits} is disabled. * *
@@ -387,8 +554,8 @@ void shouldFindBuildWithMultipleCommitsInReferenceBuild() throws IOException {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         addAdditionalFileTo(MAIN);
 
@@ -397,7 +564,7 @@ void shouldFindBuildWithMultipleCommitsInReferenceBuild() throws IOException {
         String mainCommit = changeContentOfAdditionalFile(MAIN, CHANGED_CONTENT);
 
         buildProject(project);
-        WorkflowRun nextMaster = verifyMasterBuild(project, 2);
+        WorkflowRun nextMaster = verifyMainBuild(project, 2);
         verifyRecordSize(nextMaster, 2);
 
         WorkflowRun firstFeature = verifyFeatureBuild(project, 1);
@@ -451,8 +618,8 @@ void shouldSkipBuildWithUnknownBuildsEnabled() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
         buildProject(project);
 
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         String firstMainCommit = getHead();
 
@@ -462,9 +629,9 @@ void shouldSkipBuildWithUnknownBuildsEnabled() {
 
         String mainCommit = changeContentOfAdditionalFile(MAIN, CHANGED_CONTENT);
 
-        buildAgain(masterBuild.getParent());
+        buildAgain(mainBuild.getParent());
 
-        WorkflowRun nextMaster = verifyMasterBuild(project, 2);
+        WorkflowRun nextMaster = verifyMainBuild(project, 2);
         verifyRecordSize(nextMaster, 2);
 
         buildProject(project);
@@ -473,8 +640,8 @@ void shouldSkipBuildWithUnknownBuildsEnabled() {
 
         assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(featureBuild)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild))
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild))
                 .hasMessages(MULTI_BRANCH_PROJECT,
                         MAIN_IS_TARGET,
                         String.format("-> detected 4 commits in current branch (last one: '%s')", DECORATOR.asText(featureCommit)),
@@ -500,8 +667,8 @@ void shouldNotFindBuildWithInsufficientMaxCommitsForMultibranchPipeline() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         createFeatureBranchAndAddCommits("maxCommits: 1");
 
@@ -509,8 +676,8 @@ void shouldNotFindBuildWithInsufficientMaxCommitsForMultibranchPipeline() {
 
         String mainHead = changeContentOfAdditionalFile(MAIN, CHANGED_CONTENT);
 
-        buildAgain(masterBuild.getParent());
-        WorkflowRun additionalMaster = verifyMasterBuild(project, 2);
+        buildAgain(mainBuild.getParent());
+        WorkflowRun additionalMaster = verifyMainBuild(project, 2);
         verifyRecordSize(additionalMaster, 1);
 
         buildProject(project);
@@ -545,13 +712,13 @@ void shouldUseNewestBuildIfNewestBuildIfNotFoundIsEnabled() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         addAdditionalFileTo(MAIN);
 
         buildProject(project);
-        WorkflowRun nextMaster = verifyMasterBuild(project, 2);
+        WorkflowRun nextMaster = verifyMainBuild(project, 2);
         verifyRecordSize(nextMaster, 1);
 
         createFeatureBranchAndAddCommits("maxCommits: 2", "latestBuildIfNotFound: true");
@@ -592,8 +759,8 @@ void shouldFindMasterReferenceIfBranchIsCheckedOutFromAnotherFeatureBranch() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun masterBuild = verifyMasterBuild(project, 1);
-        verifyRecordSize(masterBuild, 2);
+        WorkflowRun mainBuild = verifyMainBuild(project, 1);
+        verifyRecordSize(mainBuild, 2);
 
         createFeatureBranchAndAddCommits();
 
@@ -602,8 +769,8 @@ void shouldFindMasterReferenceIfBranchIsCheckedOutFromAnotherFeatureBranch() {
         verifyRecordSize(featureBuild, 3);
         assertThat(featureBuild.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(featureBuild)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild));
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild));
 
         checkoutNewBranch("feature2");
         addAdditionalFileTo("feature2");
@@ -614,8 +781,8 @@ void shouldFindMasterReferenceIfBranchIsCheckedOutFromAnotherFeatureBranch() {
 
         assertThat(anotherBranch.getAction(ReferenceBuild.class)).isNotNull()
                 .hasOwner(anotherBranch)
-                .hasReferenceBuildId(masterBuild.getExternalizableId())
-                .hasReferenceBuild(Optional.of(masterBuild));
+                .hasReferenceBuildId(mainBuild.getExternalizableId())
+                .hasReferenceBuild(Optional.of(mainBuild));
     }
 
     /**
@@ -633,7 +800,7 @@ void shouldNotFindIntersectionIfBuildWasDeleted() {
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun toDelete = verifyMasterBuild(project, 1);
+        WorkflowRun toDelete = verifyMainBuild(project, 1);
         verifyRecordSize(toDelete, 2);
 
         String toDeleteId = toDelete.getExternalizableId();
@@ -644,7 +811,7 @@ void shouldNotFindIntersectionIfBuildWasDeleted() {
         addAdditionalFileTo(MAIN);
 
         WorkflowRun nextMaster = buildAgain(toDelete.getParent());
-        verifyMasterBuild(project, 2);
+        verifyMainBuild(project, 2);
         verifyRecordSize(nextMaster, 1);
 
         // Now delete Build before the feature branch is build.
@@ -675,7 +842,7 @@ void shouldTakeNewestMasterBuildIfBuildWasDeletedAndNewestBuildIfNotFoundIsEnabl
         WorkflowMultiBranchProject project = initializeGitAndMultiBranchProject();
 
         buildProject(project);
-        WorkflowRun toDelete = verifyMasterBuild(project, 1);
+        WorkflowRun toDelete = verifyMainBuild(project, 1);
         verifyRecordSize(toDelete, 2);
 
         String toDeleteId = toDelete.getExternalizableId();
@@ -690,7 +857,7 @@ void shouldTakeNewestMasterBuildIfBuildWasDeletedAndNewestBuildIfNotFoundIsEnabl
         addAdditionalFileTo(MAIN);
 
         buildProject(project);
-        WorkflowRun nextMaster = verifyMasterBuild(project, 2);
+        WorkflowRun nextMaster = verifyMainBuild(project, 2);
         verifyRecordSize(nextMaster, 1);
 
         changeContentOfAdditionalFile(FEATURE, CHANGED_CONTENT);
@@ -770,7 +937,7 @@ private void delete(final String toDeleteId) {
         }
     }
 
-    private WorkflowRun verifyMasterBuild(final WorkflowMultiBranchProject project, final int buildNumber) {
+    private WorkflowRun verifyMainBuild(final WorkflowMultiBranchProject project, final int buildNumber) {
         return verifyBuild(project, buildNumber, GitITest.INITIAL_BRANCH, MAIN + " content");
     }
 
diff --git a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderTest.java b/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderTest.java
deleted file mode 100644
index 2fe65c95..00000000
--- a/plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitReferenceRecorderTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package io.jenkins.plugins.forensics.git.reference;
-
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-
-import hudson.model.BuildableItem;
-import hudson.model.FreeStyleProject;
-import hudson.model.Item;
-import hudson.util.ComboBoxModel;
-import hudson.util.FormValidation;
-
-import io.jenkins.plugins.forensics.git.reference.GitReferenceRecorder.Descriptor;
-import io.jenkins.plugins.util.JenkinsFacade;
-
-import static io.jenkins.plugins.forensics.git.assertions.Assertions.*;
-import static org.mockito.Mockito.*;
-
-/**
- * Tests the class {@link GitReferenceRecorder}.
- *
- * @author Ullrich Hafner
- */
-class GitReferenceRecorderTest {
-    private static final String JOB_NAME = "reference";
-    private static final FormValidation ERROR = FormValidation.error("error");
-    private static final FormValidation OK = FormValidation.ok();
-
-    @Nested
-    class DescriptorTest {
-        @Test
-        void shouldValidateJobName() {
-            GitReferenceJobModelValidation model = mock(GitReferenceJobModelValidation.class);
-            when(model.validateJob(JOB_NAME)).thenReturn(ERROR, OK);
-
-            JenkinsFacade jenkins = mock(JenkinsFacade.class);
-
-            Descriptor descriptor = new Descriptor(jenkins, model);
-
-            FreeStyleProject project = mock(FreeStyleProject.class);
-            assertThat(descriptor.doCheckReferenceJob((BuildableItem) project, JOB_NAME)).isEqualTo(OK);
-            verifyNoInteractions(model);
-
-            // Now enable permission
-            when(jenkins.hasPermission(Item.CONFIGURE, (BuildableItem)project)).thenReturn(true);
-            // first call stub returns ERROR
-            assertThat(descriptor.doCheckReferenceJob((BuildableItem) project, JOB_NAME)).isEqualTo(ERROR);
-            // second call stub returns ERROR
-            assertThat(descriptor.doCheckReferenceJob((BuildableItem) project, JOB_NAME)).isEqualTo(OK);
-        }
-
-        @Test
-        void shouldFillModel() {
-            GitReferenceJobModelValidation model = mock(GitReferenceJobModelValidation.class);
-            ComboBoxModel jobs = new ComboBoxModel();
-            jobs.add("A Job");
-            when(model.getAllJobs()).thenReturn(jobs);
-
-            JenkinsFacade jenkins = mock(JenkinsFacade.class);
-
-            Descriptor descriptor = new Descriptor(jenkins, model);
-
-            FreeStyleProject project = mock(FreeStyleProject.class);
-            assertThat(descriptor.doFillReferenceJobItems((BuildableItem) project)).isEqualTo(new ComboBoxModel());
-            verifyNoInteractions(model);
-
-            // Now enable permission
-            when(jenkins.hasPermission(Item.CONFIGURE, (BuildableItem)project)).thenReturn(true);
-            assertThat(descriptor.doFillReferenceJobItems((BuildableItem) project)).isEqualTo(jobs);
-        }
-    }
-}