Repository: jenkinsci/ansicolor-plugin (java)

Issue:
Logs not showing colors 
### Test environment
```
- Jenkins Version:2.263.1
- AnsiColor Version:0.7.3
- Operating System running Jenkins: Ubuntu 14.04.6 LTS
```

### Expected behavior
see logs similar to full log screen 
![image](https://user-images.githubusercontent.com/73224983/102991861-c0c0d680-4522-11eb-81a9-098b2404dd6a.png)


### Actual behavior
Logs not showing colors (do show in Full log mode/screen),
![image](https://user-images.githubusercontent.com/73224983/102991778-8f480b00-4522-11eb-91e3-ebea651a162f.png)

### Steps to reproduce the behavior
free style project, 
execute shell 
echo '\033[42m\033[97mWhite letters, green background\033[0m'
 
![image](https://user-images.githubusercontent.com/73224983/102915533-a6d3b500-448a-11eb-9e7b-2d0f197ad34e.png)
![image](https://user-images.githubusercontent.com/73224983/102987085-805d5a80-451a-11eb-98b8-2da48c2cf04a.png)
![image](https://user-images.githubusercontent.com/73224983/102987244-b995ca80-451a-11eb-81ae-72c1716d1149.png)

### Issue checklist

- [x] Jenkins version
- [x] AnsiColor version
- [x] Operating System name and version
- [x] Expected behavior
- [x] Actual behavior
- [x] Steps to reproduce the behavior


Pull request description:
212 partial log output
Fixes #212

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cb14e7..3cc20b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
 0.7.4 (Next)
 ============
 
+* [#213](https://github.com/jenkinsci/ansicolor-plugin/pull/213): Remove tabular markup on new Jenkins core - [@timja](https://github.com/timja).
+* [#214](https://github.com/jenkinsci/ansicolor-plugin/pull/214): Render logs correctly also in Jenkins versions greater than 2.260 - [@tszmytka](https://github.com/tszmytka).
 * Your contribution here.
 
 
diff --git a/src/main/java/hudson/plugins/ansicolor/action/ShortlogActionCreator.java b/src/main/java/hudson/plugins/ansicolor/action/ShortlogActionCreator.java
index 57fcc23..49da97f 100644
--- a/src/main/java/hudson/plugins/ansicolor/action/ShortlogActionCreator.java
+++ b/src/main/java/hudson/plugins/ansicolor/action/ShortlogActionCreator.java
@@ -4,15 +4,14 @@ import hudson.Extension;
 import hudson.console.ConsoleNote;
 import hudson.model.Run;
 import hudson.model.listeners.RunListener;
+import hudson.util.VersionNumber;
+import jenkins.model.Jenkins;
 
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.function.Function;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
@@ -32,8 +31,8 @@ public class ShortlogActionCreator {
         this.eol = eol.getBytes(UTF_8);
     }
 
-    public ColorizedAction createActionForShortlog(File logFile, Map<String, ColorizedAction> actions, int shortlogLimit) {
-        final ActionContext lastAction = findLastActionBefore(logFile, actions.keySet(), shortlogLimit);
+    public ColorizedAction createActionForShortlog(File logFile, Map<String, ColorizedAction> actions, int shortlogLimit, boolean keepLinesWhole) {
+        final ActionContext lastAction = findLastActionBefore(logFile, actions.keySet(), shortlogLimit, keepLinesWhole);
         if (!lastAction.isEmpty()) {
             final ColorizedAction colorizedAction = actions.get(lastAction.serializedAction);
             if (ColorizedAction.Command.START.equals(colorizedAction.getCommand())) {
@@ -43,7 +42,7 @@ public class ShortlogActionCreator {
         return null;
     }
 
-    private ActionContext findLastActionBefore(File logFile, Collection<String> serializedActions, int shortlogLimit) {
+    private ActionContext findLastActionBefore(File logFile, Collection<String> serializedActions, int shortlogLimit, boolean keepLinesWhole) {
         try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(logFile))) {
             final long shortlogStart = logFile.length() - shortlogLimit * 1024L;
             if (shortlogStart > 0) {
@@ -60,11 +59,14 @@ public class ShortlogActionCreator {
                     }
                     if (totalRead + read >= shortlogStart) {
                         final int eolPos = indexOfEol(buf, startInBuff);
-                        if (eolPos != -1 && !lastAction.isEmpty()) {
-                            return new ActionContext(lastAction, partialLine + new String(buf, startInBuff, eolPos - startInBuff + eol.length, UTF_8));
+                        final int[] beginLength = calculateBeginLength(buf, startInBuff, eolPos, partialLine.isEmpty() && keepLinesWhole);
+                        final int begin = beginLength[0];
+                        final int length = beginLength[1];
+                        if (length != -1 && !lastAction.isEmpty()) {
+                            return new ActionContext(lastAction, partialLine + new String(buf, begin, length, UTF_8));
                         } else {
                             // line extends to the next buffer
-                            partialLine = new String(Arrays.copyOfRange(buf, startInBuff, buf.length), UTF_8);
+                            partialLine = new String(Arrays.copyOfRange(buf, begin, buf.length), UTF_8);
                         }
                     }
                     totalRead += read;
@@ -88,7 +90,7 @@ public class ShortlogActionCreator {
     }
 
     private int indexOfEol(byte[] buf, int after) {
-        for (int i = after; i < buf.length; i++) {
+        for (int i = after + 1; i < buf.length; i++) {
             if (Arrays.equals(Arrays.copyOfRange(buf, i, i + eol.length), eol)) {
                 return i;
             }
@@ -96,6 +98,14 @@ public class ShortlogActionCreator {
         return -1;
     }
 
+    private int[] calculateBeginLength(byte[] buf, int startInBuff, int eolPos, boolean keepLinesWhole) {
+        if (keepLinesWhole) {
+            final int begin = eolPos != -1? eolPos + eol.length: startInBuff;
+            return new int[]{begin, eolPos != -1 ? indexOfEol(buf, eolPos) - begin + eol.length : -1};
+        }
+        return new int[]{startInBuff, eolPos != -1 ? eolPos - startInBuff + eol.length : -1};
+    }
+
     @Extension
     public static class Listener extends RunListener<Run<?, ?>> {
         @Override
@@ -116,8 +126,14 @@ public class ShortlogActionCreator {
                 final File logFile = new File(run.getRootDir(), "log");
                 if (logFile.isFile()) {
                     final ShortlogActionCreator shortlogActionCreator = new ShortlogActionCreator(new LineIdentifier(), System.lineSeparator());
+                    final VersionNumber keepLinesWholeVersion = new VersionNumber("2.260");
                     final String consoleTail = System.getProperty("hudson.consoleTailKB");
-                    final ColorizedAction action = shortlogActionCreator.createActionForShortlog(logFile, actions, consoleTail != null ? Integer.parseInt(consoleTail) : CONSOLE_TAIL_DEFAULT);
+                    final ColorizedAction action = shortlogActionCreator.createActionForShortlog(
+                        logFile,
+                        actions,
+                        consoleTail != null ? Integer.parseInt(consoleTail) : CONSOLE_TAIL_DEFAULT,
+                        Optional.ofNullable(Jenkins.getVersion()).orElse(keepLinesWholeVersion).isNewerThan(keepLinesWholeVersion)
+                    );
                     if (action != null) {
                         run.addAction(action);
                     }


Test patch:
diff --git a/src/test/java/hudson/plugins/ansicolor/action/ShortlogActionCreatorTest.java b/src/test/java/hudson/plugins/ansicolor/action/ShortlogActionCreatorTest.java
index 8b419ec..fa694a1 100644
--- a/src/test/java/hudson/plugins/ansicolor/action/ShortlogActionCreatorTest.java
+++ b/src/test/java/hudson/plugins/ansicolor/action/ShortlogActionCreatorTest.java
@@ -28,16 +28,39 @@ public class ShortlogActionCreatorTest {
 
     @Test
     public void canCreateActionForShortlog() {
-        canCreateActionForShortlog(shortlogActionCreator, "[Pipeline] echo\n", "testlog.log");
+        final String shortlogLine = "\u001B[3B\u001B[2A\u001B[2K \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold\n";
+        canCreateActionForShortlog(shortlogActionCreator, shortlogLine, "testlog.log", true);
+    }
+
+    @Test
+    public void canCreateActionForShortlogBreakLines() {
+        canCreateActionForShortlog(shortlogActionCreator, "[Pipeline] echo\n", "testlog.log", false);
     }
 
     @Test
     public void canCreateActionForShortlogForWindows() {
+        final String shortlogLine = "\u001B[3B\u001B[2A\u001B[2K \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold \u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mlightgreen bold " +
+            "\u001B[92m\u001B[22mlightgreen normal\u001B[0m \u001B[92m\u001B[1mligh";
+        final String eol = "\r\n";
+        canCreateActionForShortlog(new ShortlogActionCreator(lineIdentifier, eol), shortlogLine + eol, "testlog-crlf.log", true);
+    }
+
+    @Test
+    public void canCreateActionForShortlogForWindowsBreakLines() {
         final String eol = "\r\n";
-        canCreateActionForShortlog(new ShortlogActionCreator(lineIdentifier, eol), "[Pipeline] echo" + eol, "testlog-crlf.log");
+        canCreateActionForShortlog(new ShortlogActionCreator(lineIdentifier, eol), "[Pipeline] echo" + eol, "testlog-crlf.log", false);
     }
 
-    private void canCreateActionForShortlog(ShortlogActionCreator shortlogActionCreator, String shortlogLine, String logFile) {
+    private void canCreateActionForShortlog(ShortlogActionCreator shortlogActionCreator, String shortlogLine, String logFile, boolean keepLinesWhole) {
         final String lineHash = "mock-line-hash";
         final ColorizedAction colorizedAction = new ColorizedAction("xterm", ColorizedAction.Command.START);
         final String serializedNote = "<mock-serialized-note-start>";
@@ -50,7 +73,7 @@ public class ShortlogActionCreatorTest {
         startActions.put(ConsoleNote.PREAMBLE_STR + serializedNote, colorizedAction);
         startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-stop>", new ColorizedAction("xterm", ColorizedAction.Command.STOP));
         startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start1>", new ColorizedAction("gnome-terminal", ColorizedAction.Command.START));
-        final ColorizedAction shortlogAction = shortlogActionCreator.createActionForShortlog(file, startActions, 3);
+        final ColorizedAction shortlogAction = shortlogActionCreator.createActionForShortlog(file, startActions, 3, keepLinesWhole);
         assertEquals(colorizedAction.getColorMapName(), shortlogAction.getColorMapName());
         assertEquals(colorizedAction.getCommand(), shortlogAction.getCommand());
         assertNotEquals(colorizedAction.getId(), shortlogAction.getId());
@@ -59,56 +82,77 @@ public class ShortlogActionCreatorTest {
 
     @Test
     public void wontCreateActionForLogFileShorterThanShortlogLimit() {
-        final String serializedNote = "<mock-serialized-note-start>";
-        final ColorizedAction colorizedAction = new ColorizedAction("xterm", ColorizedAction.Command.START);
-        final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog.log")).getFile());
-        final HashMap<String, ColorizedAction> startActions = new HashMap<>();
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
-        startActions.put(ConsoleNote.PREAMBLE_STR + serializedNote, colorizedAction);
-        assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 256));
-        verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        final boolean[] keepLinesWholeOptions = {true, false};
+        for (boolean keepLinesWhole : keepLinesWholeOptions) {
+            final String serializedNote = "<mock-serialized-note-start>";
+            final ColorizedAction colorizedAction = new ColorizedAction("xterm", ColorizedAction.Command.START);
+            final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog.log")).getFile());
+            final HashMap<String, ColorizedAction> startActions = new HashMap<>();
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
+            startActions.put(ConsoleNote.PREAMBLE_STR + serializedNote, colorizedAction);
+            assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 256, keepLinesWhole));
+            verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        }
     }
 
     @Test
     public void wontCreateActionIfBuildHasNoStartActions() {
-        final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog.log")).getFile());
-        assertNull(shortlogActionCreator.createActionForShortlog(file, new HashMap<>(), 3));
-        verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        final boolean[] keepLinesWholeOptions = {true, false};
+        for (boolean keepLinesWhole : keepLinesWholeOptions) {
+            final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog.log")).getFile());
+            assertNull(shortlogActionCreator.createActionForShortlog(file, new HashMap<>(), 3, keepLinesWhole));
+            verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        }
     }
 
     @Test
     public void wontCreateActionIfNoCorrespondingNotesArePresent() {
-        final HashMap<String, ColorizedAction> startActions = new HashMap<>();
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-stop0>", new ColorizedAction("css", ColorizedAction.Command.STOP));
-        final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog-no-notes.log")).getFile());
-        assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3));
-        verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        final boolean[] keepLinesWholeOptions = {true, false};
+        for (boolean keepLinesWhole : keepLinesWholeOptions) {
+            final HashMap<String, ColorizedAction> startActions = new HashMap<>();
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-stop0>", new ColorizedAction("css", ColorizedAction.Command.STOP));
+            final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog-no-notes.log")).getFile());
+            assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3, keepLinesWhole));
+            verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        }
     }
 
     @Test
     public void wontCreateActionIfNoLogFileIsPresent() {
-        final HashMap<String, ColorizedAction> startActions = new HashMap<>();
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
-        final File file = new File("non-existing.log");
-        assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3));
-        verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        final boolean[] keepLinesWholeOptions = {true, false};
+        for (boolean keepLinesWhole : keepLinesWholeOptions) {
+            final HashMap<String, ColorizedAction> startActions = new HashMap<>();
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start0>", new ColorizedAction("css", ColorizedAction.Command.START));
+            final File file = new File("non-existing.log");
+            assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3, keepLinesWhole));
+            verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        }
     }
 
     @Test
     public void wontCreateActionIfActionIsNotActiveAtShortlogLimit() {
-        final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog-action-not-active.log")).getFile());
-        final HashMap<String, ColorizedAction> startActions = new HashMap<>();
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start>", new ColorizedAction("css", ColorizedAction.Command.START));
-        startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-stop>", new ColorizedAction("css", ColorizedAction.Command.STOP));
-
-        assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3));
-        verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        final boolean[] keepLinesWholeOptions = {true, false};
+        for (boolean keepLinesWhole : keepLinesWholeOptions) {
+            final File file = new File(getClass().getResource(String.join("/", "", getClass().getName().replace('.', '/'), "testlog-action-not-active.log")).getFile());
+            final HashMap<String, ColorizedAction> startActions = new HashMap<>();
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-start>", new ColorizedAction("css", ColorizedAction.Command.START));
+            startActions.put(ConsoleNote.PREAMBLE_STR + "<mock-serialized-note-stop>", new ColorizedAction("css", ColorizedAction.Command.STOP));
+
+            assertNull(shortlogActionCreator.createActionForShortlog(file, startActions, 3, keepLinesWhole));
+            verify(lineIdentifier, never()).hash(anyString(), anyLong());
+        }
     }
 
     @Test
     public void canCreateActionForShortlogOnLogLineExceedingBufferSize() {
         final String s = "[Pipeline]  echo a very very very long line,a very very very long line,a very very very long line,a very very very long line,a very very very long line";
-        canCreateActionForShortlog(shortlogActionCreator, s + "\n", "testlog-long.log");
+        canCreateActionForShortlog(shortlogActionCreator, s + "\n", "testlog-long.log", true);
+    }
+
+    @Test
+    public void canCreateActionForShortlogOnLogLineExceedingBufferSizeBreakLines() {
+        final String s = "[Pipeline]  echo a very very very long line,a very very very long line,a very very very long line,a very very very long line,a very very very long line";
+        canCreateActionForShortlog(shortlogActionCreator, s + "\n", "testlog-long.log", false);
     }
 }