Skip to content

Commit

Permalink
Merge pull request #303 from jenkinsci/reference-forensics
Browse files Browse the repository at this point in the history
Add delta forensics for PR and branch builds
  • Loading branch information
uhafner committed Jul 4, 2021
2 parents 6f7ccdc + d56e1ba commit efc2658
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 112 deletions.
23 changes: 7 additions & 16 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<url>https://github.com/jenkinsci/git-forensics-plugin</url>

<properties>
<revision>1.0.1</revision>
<revision>1.1.0</revision>
<changelist>-SNAPSHOT</changelist>

<module.name>${project.groupId}.git.forensics</module.name>
Expand All @@ -27,24 +27,20 @@
<eclipse-collections.version>9.2.0</eclipse-collections.version>

<!-- Jenkins Plugin Dependencies Versions -->
<forensics-api-plugin.version>1.0.0</forensics-api-plugin.version>
<forensics-api-plugin.version>1.2.0</forensics-api-plugin.version>
<plugin-util-api.version>2.3.0</plugin-util-api.version>
<data-tables-api.version>1.10.23-3</data-tables-api.version>
<echarts-api.version>5.1.0-2</echarts-api.version>
<popper-api.version>1.16.1-2</popper-api.version>
<data-tables-api.version>1.10.25-1</data-tables-api.version>
<jquery3-api.version>3.6.0-1</jquery3-api.version>
<bootstrap4-api.version>4.6.0-3</bootstrap4-api.version>
<font-awesome-api.version>5.15.3-3</font-awesome-api.version>
<echarts-api.version>5.1.2-2</echarts-api.version>
<bootstrap5-api.version>5.0.1-2</bootstrap5-api.version>

<git-plugin.version>4.6.0</git-plugin.version>
<git-client.version>3.6.0</git-client.version>
<scm-api.version>2.6.4</scm-api.version>

<j2html.version>1.4.0</j2html.version>
<streamex.version>0.7.3</streamex.version>

<bootstrap4-api.version>4.6.0-2</bootstrap4-api.version>
<font-awesome-api.version>5.15.2-2</font-awesome-api.version>
</properties>

<licenses>
Expand Down Expand Up @@ -86,19 +82,14 @@
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>bootstrap4-api</artifactId>
<version>${bootstrap4-api.version}</version>
<artifactId>bootstrap5-api</artifactId>
<version>${bootstrap5-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>font-awesome-api</artifactId>
<version>${font-awesome-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>popper-api</artifactId>
<version>${popper-api.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>jquery3-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package io.jenkins.plugins.forensics.git.miner;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;

import edu.hm.hafner.util.FilteredLog;
import edu.umd.cs.findbugs.annotations.NonNull;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.gitclient.GitClient;
import org.jenkinsci.plugins.workflow.steps.Step;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.scm.SCM;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import jenkins.tasks.SimpleBuildStep;

import io.jenkins.plugins.forensics.git.reference.GitCommitsRecord;
import io.jenkins.plugins.forensics.git.util.GitCommitTextDecorator;
import io.jenkins.plugins.forensics.git.util.GitRepositoryValidator;
import io.jenkins.plugins.forensics.git.util.RemoteResultWrapper;
import io.jenkins.plugins.forensics.miner.CommitDiffItem;
import io.jenkins.plugins.forensics.miner.CommitStatistics;
import io.jenkins.plugins.forensics.miner.CommitStatisticsBuildAction;
import io.jenkins.plugins.forensics.miner.ForensicsBuildAction;
import io.jenkins.plugins.forensics.miner.RepositoryStatistics;
import io.jenkins.plugins.forensics.reference.ReferenceFinder;
import io.jenkins.plugins.forensics.util.ScmResolver;
import io.jenkins.plugins.util.LogHandler;

/**
* A pipeline {@link Step} or Freestyle or Maven {@link Recorder} that obtains statistics for all repository files. The
* following statistics are computed:
* <ul>
* <li>total number of commits</li>
* <li>total number of different authors</li>
* <li>creation time</li>
* <li>last modification time</li>
* <li>lines of code (from the commit details)</li>
* <li>code churn (changed lines since created)</li>
* </ul>
* Stores the created statistics in a {@link RepositoryStatistics} instance. The result is attached to
* a {@link Run} by registering a {@link ForensicsBuildAction}.
*
* @author Ullrich Hafner
*/
@SuppressWarnings({"checkstyle:ClassFanOutComplexity", "PMD.ExcessiveImports"})
public class CommitStatisticsStep extends Recorder implements SimpleBuildStep {
private static final GitCommitTextDecorator RENDERER = new GitCommitTextDecorator();

private String scm = StringUtils.EMPTY;

/**
* Creates a new instance of {@link CommitStatisticsStep}.
*/
@DataBoundConstructor
public CommitStatisticsStep() {
super();

// empty constructor required for Stapler
}

/**
* Sets the SCM that should be used to find the reference build for. The reference recorder will select the SCM
* based on a substring comparison, there is no need to specify the full name.
*
* @param scm
* the ID of the SCM to use (a substring of the full ID)
*/
@DataBoundSetter
public void setScm(final String scm) {
this.scm = scm;
}

public String getScm() {
return scm;
}

@Override
public void perform(@NonNull final Run<?, ?> run, @NonNull final FilePath workspace, @NonNull final EnvVars env,
@NonNull final Launcher launcher, @NonNull final TaskListener listener) throws InterruptedException {
LogHandler logHandler = new LogHandler(listener, "Git DiffStats");
FilteredLog logger = new FilteredLog("Errors while computing diff statistics");

logger.logInfo("Analyzing commits to obtain diff statistics for affected repository files");

for (SCM repository : new ScmResolver().getScms(run, getScm())) {
logger.logInfo("-> checking SCM '%s'", repository.getKey());
logHandler.log(logger);

GitRepositoryValidator validator = new GitRepositoryValidator(repository, run, workspace, listener, logger);
if (validator.isGitRepository()) {
try {
computeStats(run, logger, repository, validator);
}
catch (IOException exception) {
logger.logInfo("-> skipping due to exception: %s", exception);
}
}
else {
logger.logInfo("-> skipping not supported repository");
}

logHandler.log(logger);
}
}

private void computeStats(final Run<?, ?> run, final FilteredLog logger, final SCM repository,
final GitRepositoryValidator validator) throws IOException, InterruptedException {
Optional<Run<?, ?>> possibleReferenceBuild = new ReferenceFinder().findReference(run, logger);
if (possibleReferenceBuild.isPresent()) {
Run<?, ?> referenceBuild = possibleReferenceBuild.get();
logger.logInfo("-> found reference build '%s'", referenceBuild);
GitCommitsRecord commitsRecord = referenceBuild.getAction(GitCommitsRecord.class);
if (commitsRecord == null) {
logger.logInfo("-> skipping since reference build '%s' has no recorded commits", referenceBuild);
}
else {
computeStatsBasedOnReferenceBuild(run, logger, repository, validator, commitsRecord);
}
}
else {
Run<?, ?> previousCompletedBuild = run.getPreviousCompletedBuild();
if (previousCompletedBuild == null) {
logger.logInfo("-> skipping step since no previous build has been completed yet");
}
else {
computeStatsBasedOnPreviousBuild(run, logger, repository, validator, previousCompletedBuild);
}
}
}

private void computeStatsBasedOnReferenceBuild(final Run<?, ?> run, final FilteredLog logger, final SCM repository,
final GitRepositoryValidator validator, final GitCommitsRecord commitsRecord)
throws IOException, InterruptedException {
String latestCommit = commitsRecord.getLatestCommit();
String ancestor = validator.createClient().withRepository(new MergeBaseSelector(latestCommit));
if (StringUtils.isNotEmpty(ancestor)) {
logger.logInfo("-> found best common ancestor '%s' between HEAD and target branch commit '%s'",
renderCommit(ancestor), renderCommit(latestCommit));
extractStats(run, repository, validator.createClient(), logger, ancestor);

return;
}

logger.logInfo("-> No common ancestor between HEAD and target branch commit '%s' found", latestCommit);
}

private void computeStatsBasedOnPreviousBuild(final Run<?, ?> run, final FilteredLog logger, final SCM repository,
final GitRepositoryValidator validator, final Run<?, ?> previousCompletedBuild)
throws IOException, InterruptedException {
logger.logInfo("-> no reference build found, using previous build '%s' as baseline",
previousCompletedBuild);
GitCommitsRecord commitsRecord = previousCompletedBuild.getAction(GitCommitsRecord.class);
if (commitsRecord != null) {
String latestCommit = commitsRecord.getLatestCommit();
if (StringUtils.isNotEmpty(latestCommit)) {
logger.logInfo("-> found latest previous commit '%s'", renderCommit(latestCommit));

extractStats(run, repository, validator.createClient(), logger, latestCommit);

return;
}
}
logger.logInfo("-> skipping since previous completed build '%s' has no recorded commits",
previousCompletedBuild);
}

private void extractStats(final Run<?, ?> run, final SCM repository, final GitClient gitClient,
final FilteredLog logger, final String ancestor) throws IOException, InterruptedException {
RemoteResultWrapper<ArrayList<CommitDiffItem>> wrapped = gitClient.withRepository(
new RepositoryStatisticsCallback(ancestor));
List<CommitDiffItem> commits = wrapped.getResult();
CommitStatistics.logCommits(commits, logger);

RepositoryStatistics repositoryStatistics = new RepositoryStatistics(ancestor);
repositoryStatistics.addAll(commits);
run.addAction(new CommitStatisticsBuildAction(run, repository.getKey(),
repositoryStatistics.getLatestStatistics()));
}

private String renderCommit(final String ancestor) {
return RENDERER.asText(ancestor);
}

@Override
public Descriptor getDescriptor() {
return (Descriptor) super.getDescriptor();
}

/**
* Descriptor for this step: defines the context and the UI elements.
*/
@Extension
@Symbol("gitDiffStat")
public static class Descriptor extends BuildStepDescriptor<Publisher> {
@NonNull
@Override
public String getDisplayName() {
return "Git Diff Statistics";
}

@Override
public boolean isApplicable(final Class<? extends AbstractProject> jobType) {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@
import java.util.Collections;
import java.util.List;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;

import edu.hm.hafner.util.FilteredLog;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import org.jenkinsci.plugins.gitclient.GitClient;
import hudson.remoting.VirtualChannel;

import io.jenkins.plugins.forensics.git.util.AbstractRepositoryCallback;
import io.jenkins.plugins.forensics.git.util.RemoteResultWrapper;
import io.jenkins.plugins.forensics.miner.CommitDiffItem;
import io.jenkins.plugins.forensics.miner.CommitStatistics;
Expand Down Expand Up @@ -43,7 +37,6 @@ public class GitRepositoryMiner extends RepositoryMiner {
this.gitClient = gitClient;
}

// TODO: we need to create the new results separately to compute the added and deleted lines per build
@Override
public RepositoryStatistics mine(final RepositoryStatistics previous, final FilteredLog logger)
throws InterruptedException {
Expand Down Expand Up @@ -78,41 +71,4 @@ public RepositoryStatistics mine(final RepositoryStatistics previous, final Filt
return new RepositoryStatistics();
}
}

private static class RepositoryStatisticsCallback
extends AbstractRepositoryCallback<RemoteResultWrapper<ArrayList<CommitDiffItem>>> {
private static final long serialVersionUID = 7667073858514128136L;

private final String previousCommitId;

RepositoryStatisticsCallback(final String previousCommitId) {
super();

this.previousCommitId = previousCommitId;
}

@Override @SuppressWarnings("PMD.UseTryWithResources")
public RemoteResultWrapper<ArrayList<CommitDiffItem>> invoke(
final Repository repository, final VirtualChannel channel) {
ArrayList<CommitDiffItem> commits = new ArrayList<>();
RemoteResultWrapper<ArrayList<CommitDiffItem>> wrapper = new RemoteResultWrapper<>(
commits, "Errors while mining the Git repository:");

try {
try (Git git = new Git(repository)) {
CommitAnalyzer commitAnalyzer = new CommitAnalyzer();
commits.addAll(commitAnalyzer.run(repository, git, previousCommitId, wrapper));
}
catch (IOException | GitAPIException exception) {
wrapper.logException(exception,
"Can't analyze commits for the repository " + repository.getIdentifier());
}
}
finally {
repository.close();
}

return wrapper;
}
}
}
Loading

0 comments on commit efc2658

Please sign in to comment.