1 /* 2 * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package org.openjdk.skara.webrev; 24 25 import org.openjdk.skara.vcs.*; 26 27 import java.util.*; 28 29 class Line { 30 private final int number; 31 private final String text; 32 33 public Line(int number, String text) { 34 this.number = number; 35 this.text = text; 36 } 37 38 public int number() { 39 return number; 40 } 41 42 public String text() { 43 return text; 44 } 45 } 46 47 class ContextHunk { 48 private List<Line> removed; 49 private List<Line> added; 50 private Context contextAfter; 51 52 public ContextHunk(List<Line> removed, List<Line> added, Context contextAfter) { 53 this.removed = removed; 54 this.added = added; 55 this.contextAfter = contextAfter; 56 } 57 58 public List<Line> removed() { 59 return removed; 60 } 61 62 public List<Line> added() { 63 return added; 64 } 65 66 public Context contextAfter() { 67 return contextAfter; 68 } 69 } 70 71 class Header { 72 private final Range source; 73 private final Range target; 74 75 public Header(Range source, Range target) { 76 this.source = source; 77 this.target = target; 78 } 79 80 public Range source() { 81 return source; 82 } 83 84 public Range target() { 85 return target; 86 } 87 } 88 89 class Context { 90 private final List<Line> sourceLines; 91 private final List<Line> destinationLines; 92 93 public Context(List<Line> sourceLines, List<Line> destinationLines) { 94 this.sourceLines = sourceLines; 95 this.destinationLines = destinationLines; 96 } 97 98 public List<Line> sourceLines() { 99 return sourceLines; 100 } 101 102 public List<Line> destinationLines() { 103 return destinationLines; 104 } 105 } 106 107 class HunkGroup { 108 private final Header header; 109 private Context contextBefore; 110 private List<ContextHunk> hunks; 111 112 public HunkGroup(Header header, Context contextBefore, List<ContextHunk> hunks) { 113 this.header = header; 114 this.contextBefore = contextBefore; 115 this.hunks = hunks; 116 } 117 118 Header header() { 119 return header; 120 } 121 122 Context contextBefore() { 123 return contextBefore; 124 } 125 126 List<ContextHunk> hunks() { 127 return hunks; 128 } 129 } 130 131 class HunkCoalescer { 132 private final int numContextLines; 133 private final List<String> sourceContent; 134 private final List<String> destContent; 135 136 public HunkCoalescer(int numContextLines, List<String> sourceContent, List<String> destContent) { 137 this.numContextLines = numContextLines; 138 this.sourceContent = sourceContent; 139 this.destContent = destContent; 140 } 141 142 public List<Hunk> nextGroup(LinkedList<Hunk> hunks) { 143 var hunksInRange = new ArrayList<Hunk>(); 144 hunksInRange.add(hunks.removeFirst()); 145 146 while (!hunks.isEmpty()) { 147 var next = hunks.peekFirst(); 148 var last = hunksInRange.get(hunksInRange.size() - 1); 149 var destEnd = last.target().range().end() + numContextLines; 150 var sourceEnd = last.source().range().end() + numContextLines; 151 var nextDestStart = next.target().range().start() - numContextLines; 152 var nextSourceStart = next.source().range().start() - numContextLines; 153 if (sourceEnd >= nextSourceStart || 154 destEnd >= nextDestStart) { 155 hunksInRange.add(hunks.removeFirst()); 156 } else { 157 break; 158 } 159 } 160 return hunksInRange; 161 } 162 163 private Header calculateCoalescedHeader(Hunk first, Hunk last) { 164 var sourceStart = first.source().range().start() - numContextLines; 165 sourceStart = Math.max(sourceStart, 1); 166 167 var destStart = first.target().range().start() - numContextLines; 168 destStart = Math.max(destStart, 1); 169 170 var sourceEnd = last.source().range().end() + numContextLines; 171 sourceEnd = Math.min(sourceEnd, sourceContent.size() + 1); 172 173 var destEnd = last.target().range().end() + numContextLines; 174 destEnd = Math.min(destEnd, destContent.size() + 1); 175 176 var sourceCount = sourceEnd - sourceStart; 177 var destCount = destEnd - destStart; 178 179 return new Header(new Range(sourceStart, sourceCount), 180 new Range(destStart, destCount)); 181 } 182 183 private Context createContextBeforeGroup(Header header, Hunk first) { 184 var sourceContextBeforeStart = header.source().start(); 185 var sourceContextBeforeEnd = first.source().range().start(); 186 var sourceBeforeContextCount = sourceContextBeforeEnd - sourceContextBeforeStart; 187 188 var destContextBeforeStart = header.target().start(); 189 var destContextBeforeEnd = first.target().range().start(); 190 var destBeforeContextCount = destContextBeforeEnd - destContextBeforeStart; 191 192 var beforeContextCount = Math.min(destBeforeContextCount, sourceBeforeContextCount); 193 assert beforeContextCount <= numContextLines; 194 195 sourceContextBeforeStart = sourceContextBeforeEnd - beforeContextCount; 196 destContextBeforeStart = destContextBeforeEnd - beforeContextCount; 197 198 var sourceContextBefore = new ArrayList<Line>(); 199 for (var lineNum = sourceContextBeforeStart; lineNum < sourceContextBeforeEnd; lineNum++) { 200 sourceContextBefore.add(new Line(lineNum, sourceContent.get(lineNum - 1))); 201 } 202 203 var destContextBefore = new ArrayList<Line>(); 204 for (var lineNum = destContextBeforeStart; lineNum < destContextBeforeEnd; lineNum++) { 205 destContextBefore.add(new Line(lineNum, destContent.get(lineNum - 1))); 206 } 207 208 return new Context(sourceContextBefore, destContextBefore); 209 } 210 211 private List<Line> removedLines(Hunk hunk) { 212 var removed = new ArrayList<Line>(); 213 214 var removedStart = hunk.source().range().start(); 215 var removedEnd = hunk.source().range().end(); 216 for (var lineNum = removedStart; lineNum < removedEnd; lineNum++) { 217 var text = sourceContent.get(lineNum - 1); 218 removed.add(new Line(lineNum, text)); 219 } 220 221 assert removed.size() == hunk.source().lines().size(); 222 223 return removed; 224 } 225 226 private List<Line> addedLines(Hunk hunk) { 227 var added = new ArrayList<Line>(); 228 var addedStart = hunk.target().range().start(); 229 var addedEnd = hunk.target().range().end(); 230 for (var lineNum = addedStart; lineNum < addedEnd; lineNum++) { 231 var text = destContent.get(lineNum - 1); 232 added.add(new Line(lineNum, text)); 233 } 234 235 assert added.size() == hunk.target().lines().size(); 236 237 return added; 238 } 239 240 private Context createContextAfterHunk(Hunk hunk, Hunk nextNonEmptySourceHunk, Hunk nextNonEmptyTargetHunk) { 241 var sourceAfterContextStart = hunk.source().range().end(); 242 var sourceAfterContextEnd = hunk.source().range().end() + numContextLines; 243 sourceAfterContextEnd = Math.min(sourceAfterContextEnd, sourceContent.size() + 1); 244 if (nextNonEmptySourceHunk != null) { 245 var nextNonEmptySourceHunkStart = nextNonEmptySourceHunk.source().range().start(); 246 sourceAfterContextEnd = sourceAfterContextEnd > nextNonEmptySourceHunkStart 247 ? Math.min(sourceAfterContextEnd, nextNonEmptySourceHunkStart) 248 : Math.max(sourceAfterContextEnd, nextNonEmptySourceHunkStart); 249 } 250 var sourceAfterContextCount = sourceAfterContextEnd - sourceAfterContextStart; 251 252 var destAfterContextStart = hunk.target().range().end(); 253 var destAfterContextEnd = hunk.target().range().end() + numContextLines; 254 destAfterContextEnd = Math.min(destAfterContextEnd, destContent.size() + 1); 255 if (nextNonEmptyTargetHunk != null) { 256 var nextNonEmptyTargetHunkStart = nextNonEmptyTargetHunk.target().range().start(); 257 destAfterContextEnd = destAfterContextEnd > nextNonEmptyTargetHunkStart 258 ? Math.min(destAfterContextEnd, nextNonEmptyTargetHunkStart) 259 : Math.max(destAfterContextEnd, nextNonEmptyTargetHunkStart); 260 } 261 var destAfterContextCount = destAfterContextEnd - destAfterContextStart; 262 263 var afterContextCount = Math.min(sourceAfterContextCount, destAfterContextCount); 264 265 var sourceLineNumStart = hunk.source().lines().isEmpty() && hunk.source().range().start() == 0 ? 266 sourceAfterContextStart + 1 : sourceAfterContextStart; 267 var sourceEndingLineNum = sourceLineNumStart + afterContextCount; 268 var sourceContextAfter = new ArrayList<Line>(); 269 for (var lineNum = sourceLineNumStart; lineNum < sourceEndingLineNum; lineNum++) { 270 var text = sourceContent.get(lineNum - 1); 271 sourceContextAfter.add(new Line(lineNum, text)); 272 } 273 274 var destLineNumStart = hunk.target().lines().isEmpty() && hunk.target().range().start() == 0 ? 275 destAfterContextStart + 1 : destAfterContextStart; 276 var destEndingLineNum = destLineNumStart + afterContextCount; 277 var destContextAfter = new ArrayList<Line>(); 278 for (var lineNum = destLineNumStart; lineNum < destEndingLineNum; lineNum++) { 279 var text = destContent.get(lineNum - 1); 280 destContextAfter.add(new Line(lineNum, text)); 281 } 282 283 return new Context(sourceContextAfter, destContextAfter); 284 } 285 286 public List<HunkGroup> coalesce(List<Hunk> originalHunks) { 287 var groups = new ArrayList<HunkGroup>(); 288 289 var worklist = new LinkedList<Hunk>(originalHunks); 290 while (!worklist.isEmpty()) { 291 var hunkGroup = nextGroup(worklist); 292 293 var first = hunkGroup.get(0); 294 var last = hunkGroup.get(hunkGroup.size() - 1); 295 var header = calculateCoalescedHeader(first, last); 296 297 var contextBefore = createContextBeforeGroup(header, first); 298 299 var hunksWithContext = new ArrayList<ContextHunk>(); 300 for (var i = 0; i < hunkGroup.size(); i++) { 301 var hunk = hunkGroup.get(i); 302 303 var removed = removedLines(hunk); 304 var added = addedLines(hunk); 305 306 Hunk nextNonEmptySourceHunk = null;; 307 for (var j = i + 1; j < hunkGroup.size(); j++) { 308 var next = hunkGroup.get(j); 309 if (next.source().range().count() > 0) { 310 nextNonEmptySourceHunk = next; 311 break; 312 } 313 } 314 Hunk nextNonEmptyTargetHunk = null; 315 for (var j = i + 1; j < hunkGroup.size(); j++) { 316 var next = hunkGroup.get(j); 317 if (next.target().range().count() > 0) { 318 nextNonEmptyTargetHunk = next; 319 break; 320 } 321 } 322 var contextAfter = createContextAfterHunk(hunk, nextNonEmptySourceHunk, nextNonEmptyTargetHunk); 323 324 hunksWithContext.add(new ContextHunk(removed, added, contextAfter)); 325 } 326 327 groups.add(new HunkGroup(header, contextBefore, hunksWithContext)); 328 } 329 330 return groups; 331 } 332 }