View Javadoc
1   /*
2    * Copyright (C) 2008-2013, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.diff;
12  
13  import java.io.IOException;
14  import java.util.ArrayList;
15  import java.util.Arrays;
16  import java.util.List;
17  
18  import org.eclipse.jgit.attributes.Attribute;
19  import org.eclipse.jgit.internal.JGitText;
20  import org.eclipse.jgit.lib.AbbreviatedObjectId;
21  import org.eclipse.jgit.lib.AnyObjectId;
22  import org.eclipse.jgit.lib.Constants;
23  import org.eclipse.jgit.lib.FileMode;
24  import org.eclipse.jgit.lib.MutableObjectId;
25  import org.eclipse.jgit.lib.ObjectId;
26  import org.eclipse.jgit.treewalk.TreeWalk;
27  import org.eclipse.jgit.treewalk.filter.TreeFilter;
28  import org.eclipse.jgit.treewalk.filter.TreeFilterMarker;
29  
30  /**
31   * A value class representing a change to a file
32   */
33  public class DiffEntry {
34  	/** Magical SHA1 used for file adds or deletes */
35  	static final AbbreviatedObjectIdorg/eclipse/jgit/lib/AbbreviatedObjectId.html#AbbreviatedObjectId">AbbreviatedObjectId A_ZERO = AbbreviatedObjectId
36  			.fromObjectId(ObjectId.zeroId());
37  
38  	/** Magical file name used for file adds or deletes. */
39  	public static final String DEV_NULL = "/dev/null"; //$NON-NLS-1$
40  
41  	/** General type of change a single file-level patch describes. */
42  	public enum ChangeType {
43  		/** Add a new file to the project */
44  		ADD,
45  
46  		/** Modify an existing file in the project (content and/or mode) */
47  		MODIFY,
48  
49  		/** Delete an existing file from the project */
50  		DELETE,
51  
52  		/** Rename an existing file to a new location */
53  		RENAME,
54  
55  		/** Copy an existing file to a new location, keeping the original */
56  		COPY;
57  	}
58  
59  	/** Specify the old or new side for more generalized access. */
60  	public enum Side {
61  		/** The old side of a DiffEntry. */
62  		OLD,
63  
64  		/** The new side of a DiffEntry. */
65  		NEW;
66  	}
67  
68  	/**
69  	 * Create an empty DiffEntry
70  	 */
71  	protected DiffEntry(){
72  		// reduce the visibility of the default constructor
73  	}
74  
75  	/**
76  	 * Convert the TreeWalk into DiffEntry headers.
77  	 *
78  	 * @param walk
79  	 *            the TreeWalk to walk through. Must have exactly two trees.
80  	 * @return headers describing the changed files.
81  	 * @throws java.io.IOException
82  	 *             the repository cannot be accessed.
83  	 * @throws java.lang.IllegalArgumentException
84  	 *             When given TreeWalk doesn't have exactly two trees.
85  	 */
86  	public static List<DiffEntry> scan(TreeWalk walk) throws IOException {
87  		return scan(walk, false);
88  	}
89  
90  	/**
91  	 * Convert the TreeWalk into DiffEntry headers, depending on
92  	 * {@code includeTrees} it will add tree objects into result or not.
93  	 *
94  	 * @param walk
95  	 *            the TreeWalk to walk through. Must have exactly two trees and
96  	 *            when {@code includeTrees} parameter is {@code true} it can't
97  	 *            be recursive.
98  	 * @param includeTrees
99  	 *            include tree objects.
100 	 * @return headers describing the changed files.
101 	 * @throws java.io.IOException
102 	 *             the repository cannot be accessed.
103 	 * @throws java.lang.IllegalArgumentException
104 	 *             when {@code includeTrees} is true and given TreeWalk is
105 	 *             recursive. Or when given TreeWalk doesn't have exactly two
106 	 *             trees
107 	 */
108 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees)
109 			throws IOException {
110 		return scan(walk, includeTrees, null);
111 	}
112 
113 	/**
114 	 * Convert the TreeWalk into DiffEntry headers, depending on
115 	 * {@code includeTrees} it will add tree objects into result or not.
116 	 *
117 	 * @param walk
118 	 *            the TreeWalk to walk through. Must have exactly two trees and
119 	 *            when {@code includeTrees} parameter is {@code true} it can't
120 	 *            be recursive.
121 	 * @param includeTrees
122 	 *            include tree objects.
123 	 * @param markTreeFilters
124 	 *            array of tree filters which will be tested for each entry. If
125 	 *            an entry matches, the entry will later return true when
126 	 *            queried through {{@link #isMarked(int)} (with the index from
127 	 *            this passed array).
128 	 * @return headers describing the changed files.
129 	 * @throws java.io.IOException
130 	 *             the repository cannot be accessed.
131 	 * @throws java.lang.IllegalArgumentException
132 	 *             when {@code includeTrees} is true and given TreeWalk is
133 	 *             recursive. Or when given TreeWalk doesn't have exactly two
134 	 *             trees
135 	 * @since 2.3
136 	 */
137 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees,
138 			TreeFilter[] markTreeFilters)
139 			throws IOException {
140 		if (walk.getTreeCount() != 2)
141 			throw new IllegalArgumentException(
142 					JGitText.get().treeWalkMustHaveExactlyTwoTrees);
143 		if (includeTrees && walk.isRecursive())
144 			throw new IllegalArgumentException(
145 					JGitText.get().cannotBeRecursiveWhenTreesAreIncluded);
146 
147 		TreeFilterMarker treeFilterMarker;
148 		if (markTreeFilters != null && markTreeFilters.length > 0)
149 			treeFilterMarker = new TreeFilterMarker(markTreeFilters);
150 		else
151 			treeFilterMarker = null;
152 
153 		List<DiffEntry> r = new ArrayList<>();
154 		MutableObjectId idBuf = new MutableObjectId();
155 		while (walk.next()) {
156 			DiffEntry entry = new DiffEntry();
157 
158 			walk.getObjectId(idBuf, 0);
159 			entry.oldId = AbbreviatedObjectId.fromObjectId(idBuf);
160 
161 			walk.getObjectId(idBuf, 1);
162 			entry.newId = AbbreviatedObjectId.fromObjectId(idBuf);
163 
164 			entry.oldMode = walk.getFileMode(0);
165 			entry.newMode = walk.getFileMode(1);
166 			entry.newPath = entry.oldPath = walk.getPathString();
167 
168 			if (walk.getAttributesNodeProvider() != null) {
169 				entry.diffAttribute = walk.getAttributes()
170 						.get(Constants.ATTR_DIFF);
171 			}
172 
173 			if (treeFilterMarker != null)
174 				entry.treeFilterMarks = treeFilterMarker.getMarks(walk);
175 
176 			if (entry.oldMode == FileMode.MISSING) {
177 				entry.oldPath = DiffEntry.DEV_NULL;
178 				entry.changeType = ChangeType.ADD;
179 				r.add(entry);
180 
181 			} else if (entry.newMode == FileMode.MISSING) {
182 				entry.newPath = DiffEntry.DEV_NULL;
183 				entry.changeType = ChangeType.DELETE;
184 				r.add(entry);
185 
186 			} else if (!entry.oldId.equals(entry.newId)) {
187 				entry.changeType = ChangeType.MODIFY;
188 				if (RenameDetector.sameType(entry.oldMode, entry.newMode))
189 					r.add(entry);
190 				else
191 					r.addAll(breakModify(entry));
192 			} else if (entry.oldMode != entry.newMode) {
193 				entry.changeType = ChangeType.MODIFY;
194 				r.add(entry);
195 			}
196 
197 			if (includeTrees && walk.isSubtree())
198 				walk.enterSubtree();
199 		}
200 		return r;
201 	}
202 
203 	static DiffEntry add(String path, AnyObjectId id) {
204 		DiffEntry e = new DiffEntry();
205 		e.oldId = A_ZERO;
206 		e.oldMode = FileMode.MISSING;
207 		e.oldPath = DEV_NULL;
208 
209 		e.newId = AbbreviatedObjectId.fromObjectId(id);
210 		e.newMode = FileMode.REGULAR_FILE;
211 		e.newPath = path;
212 		e.changeType = ChangeType.ADD;
213 		return e;
214 	}
215 
216 	static DiffEntry delete(String path, AnyObjectId id) {
217 		DiffEntry e = new DiffEntry();
218 		e.oldId = AbbreviatedObjectId.fromObjectId(id);
219 		e.oldMode = FileMode.REGULAR_FILE;
220 		e.oldPath = path;
221 
222 		e.newId = A_ZERO;
223 		e.newMode = FileMode.MISSING;
224 		e.newPath = DEV_NULL;
225 		e.changeType = ChangeType.DELETE;
226 		return e;
227 	}
228 
229 	static DiffEntry modify(String path) {
230 		DiffEntry e = new DiffEntry();
231 		e.oldMode = FileMode.REGULAR_FILE;
232 		e.oldPath = path;
233 
234 		e.newMode = FileMode.REGULAR_FILE;
235 		e.newPath = path;
236 		e.changeType = ChangeType.MODIFY;
237 		return e;
238 	}
239 
240 	/**
241 	 * Breaks apart a DiffEntry into two entries, one DELETE and one ADD.
242 	 *
243 	 * @param entry
244 	 *            the DiffEntry to break apart.
245 	 * @return a list containing two entries. Calling {@link #getChangeType()}
246 	 *         on the first entry will return ChangeType.DELETE. Calling it on
247 	 *         the second entry will return ChangeType.ADD.
248 	 */
249 	static List<DiffEntry> breakModify(DiffEntry entry) {
250 		DiffEntry del = new DiffEntry();
251 		del.oldId = entry.getOldId();
252 		del.oldMode = entry.getOldMode();
253 		del.oldPath = entry.getOldPath();
254 
255 		del.newId = A_ZERO;
256 		del.newMode = FileMode.MISSING;
257 		del.newPath = DiffEntry.DEV_NULL;
258 		del.changeType = ChangeType.DELETE;
259 		del.diffAttribute = entry.diffAttribute;
260 
261 		DiffEntry add = new DiffEntry();
262 		add.oldId = A_ZERO;
263 		add.oldMode = FileMode.MISSING;
264 		add.oldPath = DiffEntry.DEV_NULL;
265 
266 		add.newId = entry.getNewId();
267 		add.newMode = entry.getNewMode();
268 		add.newPath = entry.getNewPath();
269 		add.changeType = ChangeType.ADD;
270 		add.diffAttribute = entry.diffAttribute;
271 		return Arrays.asList(del, add);
272 	}
273 
274 	static DiffEntry="../../../../org/eclipse/jgit/diff/DiffEntry.html#DiffEntry">DiffEntryse/jgit/diff/DiffEntry.html#DiffEntry">DiffEntry pair(ChangeType changeType, DiffEntry="../../../../org/eclipse/jgit/diff/DiffEntry.html#DiffEntry">DiffEntry src, DiffEntry dst,
275 			int score) {
276 		DiffEntry r = new DiffEntry();
277 
278 		r.oldId = src.oldId;
279 		r.oldMode = src.oldMode;
280 		r.oldPath = src.oldPath;
281 
282 		r.newId = dst.newId;
283 		r.newMode = dst.newMode;
284 		r.newPath = dst.newPath;
285 		r.diffAttribute = dst.diffAttribute;
286 
287 		r.changeType = changeType;
288 		r.score = score;
289 
290 		r.treeFilterMarks = src.treeFilterMarks | dst.treeFilterMarks;
291 
292 		return r;
293 	}
294 
295 	/** File name of the old (pre-image). */
296 	protected String oldPath;
297 
298 	/** File name of the new (post-image). */
299 	protected String newPath;
300 
301 	/**
302 	 * diff filter attribute
303 	 *
304 	 * @since 4.11
305 	 */
306 	protected Attribute diffAttribute;
307 
308 	/** Old mode of the file, if described by the patch, else null. */
309 	protected FileMode oldMode;
310 
311 	/** New mode of the file, if described by the patch, else null. */
312 	protected FileMode newMode;
313 
314 	/** General type of change indicated by the patch. */
315 	protected ChangeType changeType;
316 
317 	/** Similarity score if {@link #changeType} is a copy or rename. */
318 	protected int score;
319 
320 	/** ObjectId listed on the index line for the old (pre-image) */
321 	protected AbbreviatedObjectId oldId;
322 
323 	/** ObjectId listed on the index line for the new (post-image) */
324 	protected AbbreviatedObjectId newId;
325 
326 	/**
327 	 * Bitset for marked flags of tree filters passed to
328 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}
329 	 */
330 	private int treeFilterMarks = 0;
331 
332 	/**
333 	 * Get the old name associated with this file.
334 	 * <p>
335 	 * The meaning of the old name can differ depending on the semantic meaning
336 	 * of this patch:
337 	 * <ul>
338 	 * <li><i>file add</i>: always <code>/dev/null</code></li>
339 	 * <li><i>file modify</i>: always {@link #getNewPath()}</li>
340 	 * <li><i>file delete</i>: always the file being deleted</li>
341 	 * <li><i>file copy</i>: source file the copy originates from</li>
342 	 * <li><i>file rename</i>: source file the rename originates from</li>
343 	 * </ul>
344 	 *
345 	 * @return old name for this file.
346 	 */
347 	public String getOldPath() {
348 		return oldPath;
349 	}
350 
351 	/**
352 	 * Get the new name associated with this file.
353 	 * <p>
354 	 * The meaning of the new name can differ depending on the semantic meaning
355 	 * of this patch:
356 	 * <ul>
357 	 * <li><i>file add</i>: always the file being created</li>
358 	 * <li><i>file modify</i>: always {@link #getOldPath()}</li>
359 	 * <li><i>file delete</i>: always <code>/dev/null</code></li>
360 	 * <li><i>file copy</i>: destination file the copy ends up at</li>
361 	 * <li><i>file rename</i>: destination file the rename ends up at</li>
362 	 * </ul>
363 	 *
364 	 * @return new name for this file.
365 	 */
366 	public String getNewPath() {
367 		return newPath;
368 	}
369 
370 	/**
371 	 * Get the path associated with this file.
372 	 *
373 	 * @param side
374 	 *            which path to obtain.
375 	 * @return name for this file.
376 	 */
377 	public String getPath(Side side) {
378 		return side == Side.OLD ? getOldPath() : getNewPath();
379 	}
380 
381 	/**
382 	 * @return the {@link Attribute} determining filters to be applied.
383 	 * @since 4.11
384 	 */
385 	public Attribute getDiffAttribute() {
386 		return diffAttribute;
387 	}
388 
389 	/**
390 	 * Get the old file mode
391 	 *
392 	 * @return the old file mode, if described in the patch
393 	 */
394 	public FileMode getOldMode() {
395 		return oldMode;
396 	}
397 
398 	/**
399 	 * Get the new file mode
400 	 *
401 	 * @return the new file mode, if described in the patch
402 	 */
403 	public FileMode getNewMode() {
404 		return newMode;
405 	}
406 
407 	/**
408 	 * Get the mode associated with this file.
409 	 *
410 	 * @param side
411 	 *            which mode to obtain.
412 	 * @return the mode.
413 	 */
414 	public FileMode getMode(Side side) {
415 		return side == Side.OLD ? getOldMode() : getNewMode();
416 	}
417 
418 	/**
419 	 * Get the change type
420 	 *
421 	 * @return the type of change this patch makes on {@link #getNewPath()}
422 	 */
423 	public ChangeType getChangeType() {
424 		return changeType;
425 	}
426 
427 	/**
428 	 * Get similarity score
429 	 *
430 	 * @return similarity score between {@link #getOldPath()} and
431 	 *         {@link #getNewPath()} if {@link #getChangeType()} is
432 	 *         {@link org.eclipse.jgit.diff.DiffEntry.ChangeType#COPY} or
433 	 *         {@link org.eclipse.jgit.diff.DiffEntry.ChangeType#RENAME}.
434 	 */
435 	public int getScore() {
436 		return score;
437 	}
438 
439 	/**
440 	 * Get the old object id from the <code>index</code>.
441 	 *
442 	 * @return the object id; null if there is no index line
443 	 */
444 	public AbbreviatedObjectId getOldId() {
445 		return oldId;
446 	}
447 
448 	/**
449 	 * Get the new object id from the <code>index</code>.
450 	 *
451 	 * @return the object id; null if there is no index line
452 	 */
453 	public AbbreviatedObjectId getNewId() {
454 		return newId;
455 	}
456 
457 	/**
458 	 * Whether the mark tree filter with the specified index matched during scan
459 	 * or not, see {@link #scan(TreeWalk, boolean, TreeFilter...)}. Example:
460 	 * <p>
461 	 *
462 	 * <pre>
463 	 * TreeFilter filterA = ...;
464 	 * TreeFilter filterB = ...;
465 	 * List&lt;DiffEntry&gt; entries = DiffEntry.scan(walk, false, filterA, filterB);
466 	 * DiffEntry entry = entries.get(0);
467 	 * boolean filterAMatched = entry.isMarked(0);
468 	 * boolean filterBMatched = entry.isMarked(1);
469 	 * </pre>
470 	 * <p>
471 	 * Note that 0 corresponds to filterA because it was the first filter that
472 	 * was passed to scan.
473 	 * <p>
474 	 * To query more than one flag at once, see {@link #getTreeFilterMarks()}.
475 	 *
476 	 * @param index
477 	 *            the index of the tree filter to check for (must be between 0
478 	 *            and {@link java.lang.Integer#SIZE}).
479 	 * @since 2.3
480 	 * @return a boolean.
481 	 */
482 	public boolean isMarked(int index) {
483 		return (treeFilterMarks & (1L << index)) != 0;
484 	}
485 
486 	/**
487 	 * Get the raw tree filter marks, as set during
488 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}. See
489 	 * {@link #isMarked(int)} to query each mark individually.
490 	 *
491 	 * @return the bitset of tree filter marks
492 	 * @since 2.3
493 	 */
494 	public int getTreeFilterMarks() {
495 		return treeFilterMarks;
496 	}
497 
498 	/**
499 	 * Get the object id.
500 	 *
501 	 * @param side
502 	 *            the side of the id to get.
503 	 * @return the object id; null if there is no index line
504 	 */
505 	public AbbreviatedObjectId getId(Side side) {
506 		return side == Side.OLD ? getOldId() : getNewId();
507 	}
508 
509 	/** {@inheritDoc} */
510 	@SuppressWarnings("nls")
511 	@Override
512 	public String toString() {
513 		StringBuilder buf = new StringBuilder();
514 		buf.append("DiffEntry[");
515 		buf.append(changeType);
516 		buf.append(" ");
517 		switch (changeType) {
518 		case ADD:
519 			buf.append(newPath);
520 			break;
521 		case COPY:
522 			buf.append(oldPath + "->" + newPath);
523 			break;
524 		case DELETE:
525 			buf.append(oldPath);
526 			break;
527 		case MODIFY:
528 			buf.append(oldPath);
529 			break;
530 		case RENAME:
531 			buf.append(oldPath + "->" + newPath);
532 			break;
533 		}
534 		buf.append("]");
535 		return buf.toString();
536 	}
537 }