Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public String getCommitsModel() {
@SuppressWarnings("unused") //called by jelly view
public Object getDynamic(final String link, final StaplerRequest2 request, final StaplerResponse2 response) {
try {
CommitDecorator decorator = CommitDecoratorFactory.findCommitDecorator(owner);
CommitDecorator decorator = CommitDecoratorFactory.findCommitDecorator(owner, scmKey);

return new FileDetailsView(owner, link, repositoryStatistics, decorator);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.forensics.util;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -63,6 +64,29 @@
return findCommitDecorator(new ScmResolver().getScm(run), new FilteredLog("ignored"));
}

/**
* Returns a commit decorator for the specified {@link Run build} and SCM key. The SCM key is used to identify the
* correct SCM in case multiple SCMs are used (e.g., when Global Libraries are configured). This prevents using the
* wrong SCM's commit URLs (e.g., the Global Library's repository URLs instead of the actual project's URLs).
*
* @param run
* the current build
* @param scmKey
* the key of the SCM to use (a substring of the full SCM key)
*
* @return a commit decorator for the matching SCM of the specified build or a {@link NullDecorator} if no matching
* SCM is found, the SCM is not supported, or the SCM does not provide a {@link RepositoryBrowser}
* implementation
*/
public static CommitDecorator findCommitDecorator(final Run<?, ?> run, final String scmKey) {
var logger = new FilteredLog("ignored");
Collection<? extends SCM> matchingScms = new ScmResolver().getScms(run, scmKey);
if (matchingScms.isEmpty()) {
return findCommitDecorator(new ScmResolver().getScm(run), logger);
}
return findCommitDecorator(matchingScms.iterator().next(), logger);

Check warning on line 87 in src/main/java/io/jenkins/plugins/forensics/util/CommitDecoratorFactory.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 82-87 are not covered by tests
}

private static List<CommitDecoratorFactory> findAllExtensions() {
return new JenkinsFacade().getExtensionsFor(CommitDecoratorFactory.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import hudson.model.Run;

import io.jenkins.plugins.forensics.miner.FileStatistics.FileStatisticsBuilder;
import io.jenkins.plugins.forensics.util.CommitDecorator;
import io.jenkins.plugins.forensics.util.CommitDecorator.NullDecorator;
import io.jenkins.plugins.forensics.util.CommitDecoratorFactory;

import static io.jenkins.plugins.forensics.assertions.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
Expand Down Expand Up @@ -105,10 +107,42 @@ void shouldThrowIOExceptionInGetDynamic() throws IOException {
});
}

/**
* Verifies that {@link ForensicsViewModel#getDynamic} uses the stored {@code scmKey} when resolving the
* commit decorator. This prevents Global Libraries from hijacking commit URLs by ensuring only the
* SCM matching the stored key is consulted for link generation.
*
* @see <a href="https://github.com/jenkinsci/forensics-api-plugin/issues/682">Issue #682</a>
*/
@Test
void shouldUseScmKeyToResolveCorrectCommitDecoratorForGlobalLibraries() {
var repositoryStatistics = new RepositoryStatistics();
var fileStatistics = new FileStatisticsBuilder().build(FILE_NAME);
repositoryStatistics.add(fileStatistics);

Run<?, ?> owner = mock(Run.class);
var model = new ForensicsViewModel(owner, repositoryStatistics, SCM_KEY);

CommitDecorator expectedDecorator = mock(CommitDecorator.class);
when(expectedDecorator.asLink(anyString())).thenReturn("<a href='https://correct-repo/commit/abc'>abc</a>");

try (MockedStatic<CommitDecoratorFactory> commitDecoratorFactory = mockStatic(CommitDecoratorFactory.class)) {
commitDecoratorFactory
.when(() -> CommitDecoratorFactory.findCommitDecorator(eq(owner), eq(SCM_KEY)))
.thenReturn(expectedDecorator);

Object result = model.getDynamic(createLink(), mock(StaplerRequest2.class), mock(StaplerResponse2.class));

assertThat(result).isInstanceOf(FileDetailsView.class);
commitDecoratorFactory.verify(() -> CommitDecoratorFactory.findCommitDecorator(eq(owner), eq(SCM_KEY)));
commitDecoratorFactory.verify(() -> CommitDecoratorFactory.findCommitDecorator(any(Run.class)), never());
}
}

private void runWithNullDecorator(final ForensicsViewModel model, final Consumer<ForensicsViewModel> modelConsumer) {
try (MockedStatic<CommitDecoratorFactory> commitDecoratorFactory = mockStatic(CommitDecoratorFactory.class)) {
commitDecoratorFactory
.when(() -> CommitDecoratorFactory.findCommitDecorator(any(Run.class)))
.when(() -> CommitDecoratorFactory.findCommitDecorator(any(Run.class), anyString()))
.thenReturn(new NullDecorator());

modelConsumer.accept(model);
Expand Down
Loading