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 }