\n");
+ output.write("| " + line + " | \n");
+ output.write(" | \n");
+ }
+ output.write(""
+ + content.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\n", "")
+ .replace("\r", "")
+ .replace(" ",
+ " ")
+ .replace("\t", " ") + " | \n");
+ output.write("
\n");
+ }
+
+ private String selectColor(final int covered, final int missed, final int survived) {
+ // TODO: what colors should be used for the different cases: Mutations or Branches?
+ if (covered == 0) {
+ return "coverNone";
+ }
+ else if (missed == 0 && survived == 0) {
+ return "coverFull";
+ }
+ else {
+ return "coverPart";
+ }
+ }
+
+ private String getTooltip(final PaintedNode paint,
+ final int missed, final int covered, final int survived, final int killed) {
+ var tooltip = getTooltipValue(paint, missed, covered, survived, killed);
+ if (StringUtils.isBlank(tooltip)) {
+ return StringUtils.EMPTY;
+ }
+ return "tooltip=\"" + tooltip + "\"";
+ }
+
+ // TODO: Extract into classes so that we can paint the mutations as well
+ private String getTooltipValue(final PaintedNode paint,
+ final int missed, final int covered, final int survived, final int killed) {
+
+ if (survived + killed > 1) {
+ return String.format("Mutations survived: %d, mutations killed: %d", survived, killed);
+ }
+ if (survived == 1) {
+ return "One survived mutation";
+ }
+ if (killed == 1) {
+ return "One killed mutation";
+ }
+
+ if (covered + missed > 1) {
+ if (missed == 0) {
+ return "All branches covered";
+ }
+ return String.format("Partially covered, branch coverage: %d/%d", covered, covered + missed);
+ }
+ else if (covered == 1) {
+ return "Covered at least once";
+ }
+ else {
+ return "Not covered";
+ }
+ }
+}
diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java
new file mode 100644
index 000000000..a9f9031a9
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/SourceViewModel.java
@@ -0,0 +1,83 @@
+package io.jenkins.plugins.coverage.metrics.source;
+
+import java.io.IOException;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import edu.hm.hafner.coverage.FileNode;
+import edu.hm.hafner.coverage.Node;
+
+import hudson.model.ModelObject;
+import hudson.model.Run;
+
+/**
+ * Server side model that provides the data for the source code view of the coverage results. The layout of the
+ * associated view is defined corresponding jelly view 'index.jelly'.
+ *
+ * @author Ullrich Hafner
+ */
+public class SourceViewModel implements ModelObject {
+ private static final SourceCodeFacade SOURCE_CODE_FACADE = new SourceCodeFacade();
+
+ private final Run, ?> owner;
+ private final String id;
+ private final FileNode fileNode;
+
+ /**
+ * Creates a new source view model instance.
+ *
+ * @param owner
+ * the owner of this view
+ * @param id
+ * the ID that is used to store the coverage sources
+ * @param fileNode
+ * the selected file node of the coverage tree
+ */
+ public SourceViewModel(final Run, ?> owner, final String id, final FileNode fileNode) {
+ this.owner = owner;
+ this.id = id;
+ this.fileNode = fileNode;
+ }
+
+ public Run, ?> getOwner() {
+ return owner;
+ }
+
+ public Node getNode() {
+ return fileNode;
+ }
+
+
+ /**
+ * Returns the source file rendered in HTML.
+ *
+ * @return the colored source code as HTML document
+ */
+ @SuppressWarnings("unused") // Called by jelly view
+ public String getSourceFileContent() {
+ try {
+ return SOURCE_CODE_FACADE.read(getOwner().getRootDir(), id, getNode().getPath());
+ }
+ catch (IOException | InterruptedException exception) {
+ return ExceptionUtils.getStackTrace(exception);
+ }
+ }
+
+ /**
+ * Returns whether the source file is available in Jenkins build folder.
+ *
+ * @param coverageNode
+ * The {@link Node} which is checked if there is a source file available
+ *
+ * @return {@code true} if the source file is available, {@code false} otherwise
+ */
+ @SuppressWarnings("unused") // Called by jelly view
+ public boolean isSourceFileAvailable(final Node coverageNode) {
+ return SOURCE_CODE_FACADE.canRead(getOwner().getRootDir(), id, coverageNode.getPath());
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Messages.Coverage_Title(getNode().getName());
+ }
+}
diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java
new file mode 100644
index 000000000..4109ce802
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/source/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Contains logic and models for visualizing the colored HTML source code files.
+ */
+@DefaultAnnotation(NonNull.class)
+package io.jenkins.plugins.coverage.metrics.source;
+
+import edu.umd.cs.findbugs.annotations.DefaultAnnotation;
+import edu.umd.cs.findbugs.annotations.NonNull;
diff --git a/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java
new file mode 100644
index 000000000..1375a4391
--- /dev/null
+++ b/plugin/src/main/java/io/jenkins/plugins/coverage/metrics/steps/ChangesTableModel.java
@@ -0,0 +1,84 @@
+package io.jenkins.plugins.coverage.metrics.steps;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import edu.hm.hafner.coverage.Coverage;
+import edu.hm.hafner.coverage.FileNode;
+import edu.hm.hafner.coverage.Metric;
+import edu.hm.hafner.coverage.Node;
+
+import hudson.Functions;
+
+import io.jenkins.plugins.coverage.metrics.color.ColorProvider;
+import io.jenkins.plugins.datatables.DetailedCell;
+
+/**
+ * A base class for coverage table models that handle the changes to a result of a reference build.
+ */
+abstract class ChangesTableModel extends CoverageTableModel {
+ private final Node changeRoot;
+
+ ChangesTableModel(final String id, final Node root, final Node changeRoot,
+ final RowRenderer renderer, final ColorProvider colorProvider) {
+ super(id, root, renderer, colorProvider);
+
+ this.changeRoot = changeRoot;
+ }
+
+ @Override
+ public List