1 /*******************************************************************************
2  * Copyright (c) 2006, 2018 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.compare.internal.core.patch;
15 
16 import java.io.BufferedReader;
17 import java.io.IOException;
18 import java.text.*;
19 import java.util.*;
20 import java.util.regex.Pattern;
21 
22 import org.eclipse.compare.patch.IFilePatch2;
23 import org.eclipse.core.runtime.*;
24 
25 public class PatchReader {
26 	private static final boolean DEBUG= false;
27 
28 	private static final String DEV_NULL= "/dev/null"; //$NON-NLS-1$
29 
30 	protected static final String MARKER_TYPE= "org.eclipse.compare.rejectedPatchMarker"; //$NON-NLS-1$
31 
32 	// diff formats
33 	//	private static final int CONTEXT= 0;
34 	//	private static final int ED= 1;
35 	//	private static final int NORMAL= 2;
36 	//	private static final int UNIFIED= 3;
37 
38 	// we recognize the following date/time formats
39 	private DateFormat[] fDateFormats= new DateFormat[] {
40 		new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy"), //$NON-NLS-1$
41 		new SimpleDateFormat("yyyy/MM/dd kk:mm:ss"), //$NON-NLS-1$
42 		new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy", Locale.US) //$NON-NLS-1$
43 	};
44 
45 	private boolean fIsWorkspacePatch;
46 	private boolean fIsGitPatch;
47 	private DiffProject[] fDiffProjects;
48 	private FilePatch2[] fDiffs;
49 
50 	// API for writing new multi-project patch format
51 	public static final String MULTIPROJECTPATCH_HEADER= "### Eclipse Workspace Patch"; //$NON-NLS-1$
52 
53 	public static final String MULTIPROJECTPATCH_VERSION= "1.0"; //$NON-NLS-1$
54 
55 	public static final String MULTIPROJECTPATCH_PROJECT= "#P"; //$NON-NLS-1$
56 
57 	private static final Pattern GIT_PATCH_PATTERN= Pattern.compile("^diff --git a/.+ b/.+[\r\n]+$"); //$NON-NLS-1$
58 
59 	/**
60 	 * Create a patch reader for the default date formats.
61 	 */
PatchReader()62 	public PatchReader() {
63 		// nothing here
64 	}
65 
66 	/**
67 	 * Create a patch reader for the given date formats.
68 	 *
69 	 * @param dateFormats
70 	 *            Array of <code>DateFormat</code>s to be used when
71 	 *            extracting dates from the patch.
72 	 */
PatchReader(DateFormat[] dateFormats)73 	public PatchReader(DateFormat[] dateFormats) {
74 		this();
75 		this.fDateFormats = dateFormats;
76 	}
77 
parse(BufferedReader reader)78 	public void parse(BufferedReader reader) throws IOException {
79 		List<FilePatch2> diffs= new ArrayList<>();
80 		HashMap<String, DiffProject> diffProjects= new HashMap<>(4);
81 		String line= null;
82 		boolean reread= false;
83 		String diffArgs= null;
84 		String fileName= null;
85 		// no project means this is a single patch,create a placeholder project for now
86 		// which will be replaced by the target selected by the user in the preview pane
87 		String projectName= ""; //$NON-NLS-1$
88 		this.fIsWorkspacePatch= false;
89 		this.fIsGitPatch = false;
90 
91 		LineReader lr= new LineReader(reader);
92 		lr.ignoreSingleCR(); // Don't treat single CRs as line feeds to be consistent with command line patch
93 		// Test for our format
94 		line= lr.readLine();
95 		if (line != null && line.startsWith(PatchReader.MULTIPROJECTPATCH_HEADER)) {
96 			this.fIsWorkspacePatch= true;
97 		} else {
98 			parse(lr, line);
99 			return;
100 		}
101 
102 		// read leading garbage
103 		while (true) {
104 			if (!reread)
105 				line= lr.readLine();
106 			reread= false;
107 			if (line == null)
108 				break;
109 			if (line.length() < 4)
110 				continue; // too short
111 
112 			if (line.startsWith(PatchReader.MULTIPROJECTPATCH_PROJECT)) {
113 				projectName= line.substring(2).trim();
114 				continue;
115 			}
116 
117 			if (line.startsWith("Index: ")) { //$NON-NLS-1$
118 				fileName= line.substring(7).trim();
119 				continue;
120 			}
121 			if (line.startsWith("diff")) { //$NON-NLS-1$
122 				diffArgs= line.substring(4).trim();
123 				continue;
124 			}
125 
126 			if (line.startsWith("--- ")) { //$NON-NLS-1$
127 				// if there is no current project or
128 				// the current project doesn't equal the newly parsed project
129 				// reset the current project to the newly parsed one, create a new DiffProject
130 				// and add it to the array
131 				DiffProject diffProject;
132 				if (!diffProjects.containsKey(projectName)) {
133 					diffProject= new DiffProject(projectName);
134 					diffProjects.put(projectName, diffProject);
135 				} else {
136 					diffProject= diffProjects.get(projectName);
137 				}
138 
139 				line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName, diffProject);
140 				diffArgs= fileName= null;
141 				reread= true;
142 			}
143 		}
144 
145 		lr.close();
146 
147 		this.fDiffProjects= diffProjects.values().toArray(new DiffProject[diffProjects.size()]);
148 		this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]);
149 	}
150 
createFileDiff(IPath oldPath, long oldDate, IPath newPath, long newDate)151 	protected FilePatch2 createFileDiff(IPath oldPath, long oldDate,
152 			IPath newPath, long newDate) {
153 		return new FilePatch2(oldPath, oldDate, newPath, newDate);
154 	}
155 
readUnifiedDiff(List<FilePatch2> diffs, LineReader lr, String line, String diffArgs, String fileName, DiffProject diffProject)156 	private String readUnifiedDiff(List<FilePatch2> diffs, LineReader lr, String line, String diffArgs, String fileName, DiffProject diffProject) throws IOException {
157 		List<FilePatch2> newDiffs= new ArrayList<>();
158 		String nextLine= readUnifiedDiff(newDiffs, lr, line, diffArgs, fileName);
159 		for (FilePatch2 diff : newDiffs) {
160 			diffProject.add(diff);
161 			diffs.add(diff);
162 		}
163 		return nextLine;
164 	}
165 
parse(LineReader lr, String line)166 	public void parse(LineReader lr, String line) throws IOException {
167 		List<FilePatch2> diffs= new ArrayList<>();
168 		boolean reread= false;
169 		String diffArgs= null;
170 		String fileName= null;
171 		List<String> headerLines = new ArrayList<>();
172 		boolean foundDiff= false;
173 
174 		// read leading garbage
175 		reread= line!=null;
176 		while (true) {
177 			if (!reread)
178 				line= lr.readLine();
179 			reread= false;
180 			if (line == null)
181 				break;
182 
183 			// remember some infos
184 			if (line.startsWith("Index: ")) { //$NON-NLS-1$
185 				fileName= line.substring(7).trim();
186 			} else if (line.startsWith("diff")) { //$NON-NLS-1$
187 				if (!foundDiff && GIT_PATCH_PATTERN.matcher(line).matches())
188 					this.fIsGitPatch= true;
189 				foundDiff= true;
190 				diffArgs= line.substring(4).trim();
191 			} else if (line.startsWith("--- ")) { //$NON-NLS-1$
192 				line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName);
193 				if (!headerLines.isEmpty())
194 					setHeader(diffs.get(diffs.size() - 1), headerLines);
195 				diffArgs= fileName= null;
196 				reread= true;
197 			} else if (line.startsWith("*** ")) { //$NON-NLS-1$
198 				line= readContextDiff(diffs, lr, line, diffArgs, fileName);
199 				if (!headerLines.isEmpty())
200 					setHeader(diffs.get(diffs.size() - 1), headerLines);
201 				diffArgs= fileName= null;
202 				reread= true;
203 			}
204 
205 			// Any lines we read here are header lines.
206 			// However, if reread is set, we will add them to the header on the next pass through
207 			if (!reread) {
208 				headerLines.add(line);
209 			}
210 		}
211 
212 		lr.close();
213 
214 		this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]);
215 	}
216 
setHeader(FilePatch2 diff, List<String> headerLines)217 	private void setHeader(FilePatch2 diff, List<String> headerLines) {
218 		String header = LineReader.createString(false, headerLines);
219 		diff.setHeader(header);
220 		headerLines.clear();
221 	}
222 
223 	/*
224 	 * Returns the next line that does not belong to this diff
225 	 */
readUnifiedDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName)226 	protected String readUnifiedDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName) throws IOException {
227 
228 		String[] oldArgs= split(line.substring(4));
229 
230 		// read info about new file
231 		line= reader.readLine();
232 		if (line == null || !line.startsWith("+++ ")) //$NON-NLS-1$
233 			return line;
234 
235 		String[] newArgs= split(line.substring(4));
236 
237 		FilePatch2 diff = createFileDiff(extractPath(oldArgs, 0, fileName),
238 				extractDate(oldArgs, 1), extractPath(newArgs, 0, fileName),
239 				extractDate(newArgs, 1));
240 		diffs.add(diff);
241 
242 		int[] oldRange= new int[2];
243 		int[] newRange= new int[2];
244 		int remainingOld= -1; // remaining old lines for current hunk
245 		int remainingNew= -1; // remaining new lines for current hunk
246 		List<String> lines= new ArrayList<>();
247 
248 		boolean encounteredPlus = false;
249 		boolean encounteredMinus = false;
250 		boolean encounteredSpace = false;
251 
252 		try {
253 			// read lines of hunk
254 			while (true) {
255 
256 				line= reader.readLine();
257 				if (line == null)
258 					return null;
259 
260 				if (reader.lineContentLength(line) == 0) {
261 					//System.out.println("Warning: found empty line in hunk; ignored");
262 					//lines.add(' ' + line);
263 					continue;
264 				}
265 
266 				char c= line.charAt(0);
267 				if (remainingOld == 0 && remainingNew == 0 && c != '@' && c != '\\') {
268 					return line;
269 				}
270 
271 				switch (c) {
272 					case '@':
273 						if (line.startsWith("@@ ")) { //$NON-NLS-1$
274 							// flush old hunk
275 							if (lines.size() > 0) {
276 								Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace);
277 								lines.clear();
278 							}
279 
280 							// format: @@ -oldStart,oldLength +newStart,newLength @@
281 							extractPair(line, '-', oldRange);
282 							extractPair(line, '+', newRange);
283 							remainingOld= oldRange[1];
284 							remainingNew= newRange[1];
285 							continue;
286 						}
287 						break;
288 					case ' ':
289 						encounteredSpace= true;
290 						remainingOld--;
291 						remainingNew--;
292 						lines.add(line);
293 						continue;
294 					case '+':
295 						encounteredPlus= true;
296 						remainingNew--;
297 						lines.add(line);
298 						continue;
299 					case '-':
300 						encounteredMinus= true;
301 						remainingOld--;
302 						lines.add(line);
303 						continue;
304 					case '\\':
305 						if (line.indexOf("newline at end") > 0) { //$NON-NLS-1$
306 							int lastIndex= lines.size();
307 							if (lastIndex > 0) {
308 								line= lines.get(lastIndex - 1);
309 								int end= line.length() - 1;
310 								char lc= line.charAt(end);
311 								if (lc == '\n') {
312 									end--;
313 									if (end > 0 && line.charAt(end) == '\r')
314 										end--;
315 								} else if (lc == '\r') {
316 									end--;
317 								}
318 								line= line.substring(0, end + 1);
319 								lines.set(lastIndex - 1, line);
320 							}
321 							continue;
322 						}
323 						break;
324 					case '#':
325 						break;
326 					case 'I':
327 						if (line.indexOf("Index:") == 0) //$NON-NLS-1$
328 							break;
329 						//$FALL-THROUGH$
330 					case 'd':
331 						if (line.indexOf("diff ") == 0) //$NON-NLS-1$
332 							break;
333 						//$FALL-THROUGH$
334 					case 'B':
335 						if (line.indexOf("Binary files differ") == 0) //$NON-NLS-1$
336 							break;
337 						//$FALL-THROUGH$
338 					default:
339 						break;
340 				}
341 				return line;
342 			}
343 		} finally {
344 			if (lines.size() > 0)
345 				Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace);
346 		}
347 	}
348 
349 	/*
350 	 * Returns the next line that does not belong to this diff
351 	 */
readContextDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName)352 	private String readContextDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName) throws IOException {
353 
354 		String[] oldArgs= split(line.substring(4));
355 
356 		// read info about new file
357 		line= reader.readLine();
358 		if (line == null || !line.startsWith("--- ")) //$NON-NLS-1$
359 			return line;
360 
361 		String[] newArgs= split(line.substring(4));
362 
363 		FilePatch2 diff = createFileDiff(extractPath(oldArgs, 0, fileName),
364 				extractDate(oldArgs, 1), extractPath(newArgs, 0, fileName),
365 				extractDate(newArgs, 1));
366 		diffs.add(diff);
367 
368 		int[] oldRange= new int[2];
369 		int[] newRange= new int[2];
370 		List<String> oldLines= new ArrayList<>();
371 		List<String> newLines= new ArrayList<>();
372 		List<String> lines= oldLines;
373 
374 
375 		boolean encounteredPlus = false;
376 		boolean encounteredMinus = false;
377 		boolean encounteredSpace = false;
378 
379 		try {
380 			// read lines of hunk
381 			while (true) {
382 
383 				line= reader.readLine();
384 				if (line == null)
385 					return line;
386 
387 				int l= line.length();
388 				if (l == 0)
389 					continue;
390 				if (l > 1) {
391 					switch (line.charAt(0)) {
392 					case '*':
393 						if (line.startsWith("***************")) {	// new hunk //$NON-NLS-1$
394 							// flush old hunk
395 							if (oldLines.size() > 0 || newLines.size() > 0) {
396 								Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace);
397 								oldLines.clear();
398 								newLines.clear();
399 							}
400 							continue;
401 						}
402 						if (line.startsWith("*** ")) {	// old range //$NON-NLS-1$
403 							// format: *** oldStart,oldEnd ***
404 							extractPair(line, ' ', oldRange);
405 							if (oldRange[0] == 0) {
406 								oldRange[1] = 0; // In case of the file addition
407 							} else {
408 								oldRange[1] = oldRange[1] - oldRange[0] + 1;
409 							}
410 							lines= oldLines;
411 							continue;
412 						}
413 						break;
414 					case ' ':	// context line
415 						if (line.charAt(1) == ' ') {
416 							lines.add(line);
417 							continue;
418 						}
419 						break;
420 					case '+':	// addition
421 						if (line.charAt(1) == ' ') {
422 							encounteredPlus = true;
423 							lines.add(line);
424 							continue;
425 						}
426 						break;
427 					case '!':	// change
428 						if (line.charAt(1) == ' ') {
429 							encounteredSpace = true;
430 							lines.add(line);
431 							continue;
432 						}
433 						break;
434 					case '-':
435 						if (line.charAt(1) == ' ') {	// deletion
436 							encounteredMinus = true;
437 							lines.add(line);
438 							continue;
439 						}
440 						if (line.startsWith("--- ")) {	// new range //$NON-NLS-1$
441 							// format: *** newStart,newEnd ***
442 							extractPair(line, ' ', newRange);
443 							if (newRange[0] == 0) {
444 								newRange[1] = 0; // In case of the file removal
445 							} else {
446 								newRange[1] = newRange[1] - newRange[0] + 1;
447 							}
448 							lines= newLines;
449 							continue;
450 						}
451 						break;
452 					default:
453 						break;
454 					}
455 				}
456 				return line;
457 			}
458 		} finally {
459 			// flush last hunk
460 			if (oldLines.size() > 0 || newLines.size() > 0)
461 				Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace);
462 		}
463 	}
464 
465 	/*
466 	 * Creates a List of lines in the unified format from
467 	 * two Lists of lines in the 'classic' format.
468 	 */
unifyLines(List<String> oldLines, List<String> newLines)469 	private List<String> unifyLines(List<String> oldLines, List<String> newLines) {
470 		List<String> result= new ArrayList<>();
471 
472 		String[] ol= oldLines.toArray(new String[oldLines.size()]);
473 		String[] nl= newLines.toArray(new String[newLines.size()]);
474 
475 		int oi= 0, ni= 0;
476 
477 		while (true) {
478 
479 			char oc= 0;
480 			String o= null;
481 			if (oi < ol.length) {
482 				o= ol[oi];
483 				oc= o.charAt(0);
484 			}
485 
486 			char nc= 0;
487 			String n= null;
488 			if (ni < nl.length) {
489 				n= nl[ni];
490 				nc= n.charAt(0);
491 			}
492 
493 			// EOF
494 			if (oc == 0 && nc == 0)
495 				break;
496 
497 			// deletion in old
498 			if (oc == '-') {
499 				do {
500 					result.add('-' + o.substring(2));
501 					oi++;
502 					if (oi >= ol.length)
503 						break;
504 					o= ol[oi];
505 				} while (o.charAt(0) == '-');
506 				continue;
507 			}
508 
509 			// addition in new
510 			if (nc == '+') {
511 				do {
512 					result.add('+' + n.substring(2));
513 					ni++;
514 					if (ni >= nl.length)
515 						break;
516 					n= nl[ni];
517 				} while (n.charAt(0) == '+');
518 				continue;
519 			}
520 
521 			// differing lines on both sides
522 			if (oc == '!' && nc == '!') {
523 				// remove old
524 				do {
525 					result.add('-' + o.substring(2));
526 					oi++;
527 					if (oi >= ol.length)
528 						break;
529 					o= ol[oi];
530 				} while (o.charAt(0) == '!');
531 
532 				// add new
533 				do {
534 					result.add('+' + n.substring(2));
535 					ni++;
536 					if (ni >= nl.length)
537 						break;
538 					n= nl[ni];
539 				} while (n.charAt(0) == '!');
540 
541 				continue;
542 			}
543 
544 			// context lines
545 			if (oc == ' ' && nc == ' ') {
546 				do {
547 					Assert.isTrue(o.equals(n), "non matching context lines"); //$NON-NLS-1$
548 					result.add(' ' + o.substring(2));
549 					oi++;
550 					ni++;
551 					if (oi >= ol.length || ni >= nl.length)
552 						break;
553 					o= ol[oi];
554 					n= nl[ni];
555 				} while (o.charAt(0) == ' ' && n.charAt(0) == ' ');
556 				continue;
557 			}
558 
559 			if (oc == ' ') {
560 				do {
561 					result.add(' ' + o.substring(2));
562 					oi++;
563 					if (oi >= ol.length)
564 						break;
565 					o= ol[oi];
566 				} while (o.charAt(0) == ' ');
567 				continue;
568 			}
569 
570 			if (nc == ' ') {
571 				do {
572 					result.add(' ' + n.substring(2));
573 					ni++;
574 					if (ni >= nl.length)
575 						break;
576 					n= nl[ni];
577 				} while (n.charAt(0) == ' ');
578 				continue;
579 			}
580 
581 			Assert.isTrue(false, "unexpected char <" + oc + "> <" + nc + ">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
582 		}
583 
584 		return result;
585 	}
586 
587 	/*
588 	 * @return the parsed time/date in milliseconds or IFilePatch.DATE_UNKNOWN
589 	 * (0) on error
590 	 */
extractDate(String[] args, int n)591 	private long extractDate(String[] args, int n) {
592 		if (n < args.length) {
593 			String line= args[n];
594 			for (DateFormat dateFormat : this.fDateFormats) {
595 				dateFormat.setLenient(true);
596 				try {
597 					Date date = dateFormat.parse(line);
598 					return date.getTime();
599 				} catch (ParseException ex) {
600 					// silently ignored
601 				}
602 			}
603 			// System.err.println("can't parse date: <" + line + ">");
604 		}
605 		return IFilePatch2.DATE_UNKNOWN;
606 	}
607 
608 	/*
609 	 * Returns null if file name is "/dev/null".
610 	 */
extractPath(String[] args, int n, String path2)611 	private IPath extractPath(String[] args, int n, String path2) {
612 		if (n < args.length) {
613 			String path= args[n];
614 			if (DEV_NULL.equals(path))
615 				return null;
616 			int pos= path.lastIndexOf(':');
617 			if (pos >= 0)
618 				path= path.substring(0, pos);
619 			if (path2 != null && !path2.equals(path)) {
620 				if (DEBUG) System.out.println("path mismatch: " + path2); //$NON-NLS-1$
621 				path= path2;
622 			}
623 			return new Path(path);
624 		}
625 		return null;
626 	}
627 
628 	/*
629 	 * Tries to extract two integers separated by a comma.
630 	 * The parsing of the line starts at the position after
631 	 * the first occurrence of the given character start an ends
632 	 * at the first blank (or the end of the line).
633 	 * If only a single number is found this is assumed to be the start of a one line range.
634 	 * If an error occurs the range -1,-1 is returned.
635 	 */
extractPair(String line, char start, int[] pair)636 	private void extractPair(String line, char start, int[] pair) {
637 		pair[0]= pair[1]= -1;
638 		int startPos= line.indexOf(start);
639 		if (startPos < 0) {
640 			if (DEBUG) System.out.println("parsing error in extractPair: couldn't find \'" + start + "\'"); //$NON-NLS-1$ //$NON-NLS-2$
641 			return;
642 		}
643 		line= line.substring(startPos+1);
644 		int endPos= line.indexOf(' ');
645 		if (endPos < 0) {
646 			if (DEBUG) System.out.println("parsing error in extractPair: couldn't find end blank"); //$NON-NLS-1$
647 			return;
648 		}
649 		line= line.substring(0, endPos);
650 		int comma= line.indexOf(',');
651 		if (comma >= 0) {
652 			pair[0]= Integer.parseInt(line.substring(0, comma));
653 			pair[1]= Integer.parseInt(line.substring(comma+1));
654 		} else {	// abbreviated form for one line patch
655 			pair[0]= Integer.parseInt(line);
656 			pair[1]= 1;
657 		}
658 	}
659 
660 	/*
661 	 * Breaks the given string into tab separated substrings.
662 	 * Leading and trailing whitespace is removed from each token.
663 	 */
split(String line)664 	private String[] split(String line) {
665 		List<String> l= new ArrayList<>();
666 		StringTokenizer st= new StringTokenizer(line, "\t"); //$NON-NLS-1$
667 		while (st.hasMoreElements()) {
668 			String token= st.nextToken().trim();
669 			if (token.length() > 0)
670 				l.add(token);
671 		}
672 		return l.toArray(new String[l.size()]);
673 	}
674 
isWorkspacePatch()675 	public boolean isWorkspacePatch() {
676 		return this.fIsWorkspacePatch;
677 	}
678 
isGitPatch()679 	public boolean isGitPatch() {
680 		return this.fIsGitPatch;
681 	}
682 
getDiffProjects()683 	public DiffProject[] getDiffProjects() {
684 		return this.fDiffProjects;
685 	}
686 
getDiffs()687 	public FilePatch2[] getDiffs() {
688 		return this.fDiffs;
689 	}
690 
getAdjustedDiffs()691 	public FilePatch2[] getAdjustedDiffs() {
692 		if (!isWorkspacePatch() || this.fDiffs.length == 0)
693 			return this.fDiffs;
694 		List<FilePatch2> result = new ArrayList<>();
695 		for (FilePatch2 diff : this.fDiffs) {
696 			result.add(diff.asRelativeDiff());
697 		}
698 		return result.toArray(new FilePatch2[result.size()]);
699 	}
700 
701 }
702