View Javadoc
1   /*
2    * Copyright (C) 2008, 2009, Google Inc.
3    * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.dircache;
13  
14  import static org.eclipse.jgit.dircache.DirCache.cmp;
15  import static org.eclipse.jgit.dircache.DirCacheTree.peq;
16  import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
17  
18  import java.io.IOException;
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.List;
24  
25  import org.eclipse.jgit.internal.JGitText;
26  import org.eclipse.jgit.lib.Constants;
27  import org.eclipse.jgit.util.Paths;
28  
29  /**
30   * Updates a {@link org.eclipse.jgit.dircache.DirCache} by supplying discrete
31   * edit commands.
32   * <p>
33   * An editor updates a DirCache by taking a list of
34   * {@link org.eclipse.jgit.dircache.DirCacheEditor.PathEdit} commands and
35   * executing them against the entries of the destination cache to produce a new
36   * cache. This edit style allows applications to insert a few commands and then
37   * have the editor compute the proper entry indexes necessary to perform an
38   * efficient in-order update of the index records. This can be easier to use
39   * than {@link org.eclipse.jgit.dircache.DirCacheBuilder}.
40   * <p>
41   *
42   * @see DirCacheBuilder
43   */
44  public class DirCacheEditor extends BaseDirCacheEditor {
45  	private static final Comparator<PathEdit> EDIT_CMP = (PathEdit o1,
46  			PathEdit o2) -> {
47  		final byte[] a = o1.path;
48  		final byte[] b = o2.path;
49  		return cmp(a, a.length, b, b.length);
50  	};
51  
52  	private final List<PathEdit> edits;
53  	private int editIdx;
54  
55  	/**
56  	 * Construct a new editor.
57  	 *
58  	 * @param dc
59  	 *            the cache this editor will eventually update.
60  	 * @param ecnt
61  	 *            estimated number of entries the editor will have upon
62  	 *            completion. This sizes the initial entry table.
63  	 */
64  	protected DirCacheEditor(DirCache dc, int ecnt) {
65  		super(dc, ecnt);
66  		edits = new ArrayList<>();
67  	}
68  
69  	/**
70  	 * Append one edit command to the list of commands to be applied.
71  	 * <p>
72  	 * Edit commands may be added in any order chosen by the application. They
73  	 * are automatically rearranged by the builder to provide the most efficient
74  	 * update possible.
75  	 *
76  	 * @param edit
77  	 *            another edit command.
78  	 */
79  	public void add(PathEdit edit) {
80  		edits.add(edit);
81  	}
82  
83  	/** {@inheritDoc} */
84  	@Override
85  	public boolean commit() throws IOException {
86  		if (edits.isEmpty()) {
87  			// No changes? Don't rewrite the index.
88  			//
89  			cache.unlock();
90  			return true;
91  		}
92  		return super.commit();
93  	}
94  
95  	/** {@inheritDoc} */
96  	@Override
97  	public void finish() {
98  		if (!edits.isEmpty()) {
99  			applyEdits();
100 			replace();
101 		}
102 	}
103 
104 	private void applyEdits() {
105 		Collections.sort(edits, EDIT_CMP);
106 		editIdx = 0;
107 
108 		final int maxIdx = cache.getEntryCount();
109 		int lastIdx = 0;
110 		while (editIdx < edits.size()) {
111 			PathEdit e = edits.get(editIdx++);
112 			int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
113 			final boolean missing = eIdx < 0;
114 			if (eIdx < 0)
115 				eIdx = -(eIdx + 1);
116 			final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
117 			if (cnt > 0)
118 				fastKeep(lastIdx, cnt);
119 
120 			if (e instanceof DeletePath) {
121 				lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
122 				continue;
123 			}
124 			if (e instanceof DeleteTree) {
125 				lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
126 				continue;
127 			}
128 
129 			if (missing) {
130 				DirCacheEntry ent = new DirCacheEntry(e.path);
131 				e.apply(ent);
132 				if (ent.getRawMode() == 0) {
133 					throw new IllegalArgumentException(MessageFormat.format(
134 							JGitText.get().fileModeNotSetForPath,
135 							ent.getPathString()));
136 				}
137 				lastIdx = e.replace
138 					? deleteOverlappingSubtree(ent, eIdx)
139 					: eIdx;
140 				fastAdd(ent);
141 			} else {
142 				lastIdx = cache.nextEntry(eIdx);
143 				if (lastIdx > eIdx + 1) {
144 					// Apply to all entries of the current path (different
145 					// stages). If any apply() resets the stage to STAGE_0, take
146 					// only that entry and omit all others.
147 					DirCacheEntry[] tmp = new DirCacheEntry[lastIdx - eIdx];
148 					int n = 0;
149 					for (int i = eIdx; i < lastIdx; i++) {
150 						DirCacheEntry ent = cache.getEntry(i);
151 						e.apply(ent);
152 						if (ent.getStage() == DirCacheEntry.STAGE_0) {
153 							fastAdd(ent);
154 							n = 0;
155 							break;
156 						}
157 						tmp[n++] = ent;
158 					}
159 					for (int i = 0; i < n; i++) {
160 						fastAdd(tmp[i]);
161 					}
162 				} else {
163 					DirCacheEntry ent = cache.getEntry(eIdx);
164 					e.apply(ent);
165 					fastAdd(ent);
166 				}
167 			}
168 		}
169 
170 		final int cnt = maxIdx - lastIdx;
171 		if (cnt > 0)
172 			fastKeep(lastIdx, cnt);
173 	}
174 
175 	private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) {
176 		byte[] entPath = ent.path;
177 		int entLen = entPath.length;
178 
179 		// Delete any file that was previously processed and overlaps
180 		// the parent directory for the new entry. Since the editor
181 		// always processes entries in path order, binary search back
182 		// for the overlap for each parent directory.
183 		for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) {
184 			int i = findEntry(entPath, p);
185 			if (i >= 0) {
186 				// A file does overlap, delete the file from the array.
187 				// No other parents can have overlaps as the file should
188 				// have taken care of that itself.
189 				int n = --entryCnt - i;
190 				System.arraycopy(entries, i + 1, entries, i, n);
191 				break;
192 			}
193 
194 			// If at least one other entry already exists in this parent
195 			// directory there is no need to continue searching up the tree.
196 			i = -(i + 1);
197 			if (i < entryCnt && inDir(entries[i], entPath, p)) {
198 				break;
199 			}
200 		}
201 
202 		int maxEnt = cache.getEntryCount();
203 		if (eIdx >= maxEnt) {
204 			return maxEnt;
205 		}
206 
207 		DirCacheEntry next = cache.getEntry(eIdx);
208 		if (Paths.compare(next.path, 0, next.path.length, 0,
209 				entPath, 0, entLen, TYPE_TREE) < 0) {
210 			// Next DirCacheEntry sorts before new entry as tree. Defer a
211 			// DeleteTree command to delete any entries if they exist. This
212 			// case only happens for A, A.c, A/c type of conflicts (rare).
213 			insertEdit(new DeleteTree(entPath));
214 			return eIdx;
215 		}
216 
217 		// Next entry may be contained by the entry-as-tree, skip if so.
218 		while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) {
219 			eIdx++;
220 		}
221 		return eIdx;
222 	}
223 
224 	private int findEntry(byte[] p, int pLen) {
225 		int low = 0;
226 		int high = entryCnt;
227 		while (low < high) {
228 			int mid = (low + high) >>> 1;
229 			int cmp = cmp(p, pLen, entries[mid]);
230 			if (cmp < 0) {
231 				high = mid;
232 			} else if (cmp == 0) {
233 				while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
234 					mid--;
235 				}
236 				return mid;
237 			} else {
238 				low = mid + 1;
239 			}
240 		}
241 		return -(low + 1);
242 	}
243 
244 	private void insertEdit(DeleteTree d) {
245 		for (int i = editIdx; i < edits.size(); i++) {
246 			int cmp = EDIT_CMP.compare(d, edits.get(i));
247 			if (cmp < 0) {
248 				edits.add(i, d);
249 				return;
250 			} else if (cmp == 0) {
251 				return;
252 			}
253 		}
254 		edits.add(d);
255 	}
256 
257 	private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) {
258 		return e.path.length > pLen && e.path[pLen] == '/'
259 				&& peq(path, e.path, pLen);
260 	}
261 
262 	private static int pdir(byte[] path, int e) {
263 		for (e--; e > 0; e--) {
264 			if (path[e] == '/') {
265 				return e;
266 			}
267 		}
268 		return 0;
269 	}
270 
271 	/**
272 	 * Any index record update.
273 	 * <p>
274 	 * Applications should subclass and provide their own implementation for the
275 	 * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once
276 	 * for each record in the index which matches the path name. If there are
277 	 * multiple records (for example in stages 1, 2 and 3), the edit instance
278 	 * will be called multiple times, once for each stage. If any of these calls
279 	 * resets the stage to 0, only this entry will be taken and entries for
280 	 * other stages are discarded.
281 	 */
282 	public abstract static class PathEdit {
283 		final byte[] path;
284 		boolean replace = true;
285 
286 		/**
287 		 * Create a new update command by path name.
288 		 *
289 		 * @param entryPath
290 		 *            path of the file within the repository.
291 		 */
292 		public PathEdit(String entryPath) {
293 			path = Constants.encode(entryPath);
294 		}
295 
296 		PathEdit(byte[] path) {
297 			this.path = path;
298 		}
299 
300 		/**
301 		 * Create a new update command for an existing entry instance.
302 		 *
303 		 * @param ent
304 		 *            entry instance to match path of. Only the path of this
305 		 *            entry is actually considered during command evaluation.
306 		 */
307 		public PathEdit(DirCacheEntry ent) {
308 			path = ent.path;
309 		}
310 
311 		/**
312 		 * Configure if a file can replace a directory (or vice versa).
313 		 * <p>
314 		 * Default is {@code true} as this is usually the desired behavior.
315 		 *
316 		 * @param ok
317 		 *            if true a file can replace a directory, or a directory can
318 		 *            replace a file.
319 		 * @return {@code this}
320 		 * @since 4.2
321 		 */
322 		public PathEdit setReplace(boolean ok) {
323 			replace = ok;
324 			return this;
325 		}
326 
327 		/**
328 		 * Apply the update to a single cache entry matching the path.
329 		 * <p>
330 		 * After apply is invoked the entry is added to the output table, and
331 		 * will be included in the new index.
332 		 *
333 		 * @param ent
334 		 *            the entry being processed. All fields are zeroed out if
335 		 *            the path is a new path in the index.
336 		 */
337 		public abstract void apply(DirCacheEntry ent);
338 
339 		@Override
340 		public String toString() {
341 			String p = DirCacheEntry.toString(path);
342 			return getClass().getSimpleName() + '[' + p + ']';
343 		}
344 	}
345 
346 	/**
347 	 * Deletes a single file entry from the index.
348 	 * <p>
349 	 * This deletion command removes only a single file at the given location,
350 	 * but removes multiple stages (if present) for that path. To remove a
351 	 * complete subtree use {@link DeleteTree} instead.
352 	 *
353 	 * @see DeleteTree
354 	 */
355 	public static final class DeletePath extends PathEdit {
356 		/**
357 		 * Create a new deletion command by path name.
358 		 *
359 		 * @param entryPath
360 		 *            path of the file within the repository.
361 		 */
362 		public DeletePath(String entryPath) {
363 			super(entryPath);
364 		}
365 
366 		/**
367 		 * Create a new deletion command for an existing entry instance.
368 		 *
369 		 * @param ent
370 		 *            entry instance to remove. Only the path of this entry is
371 		 *            actually considered during command evaluation.
372 		 */
373 		public DeletePath(DirCacheEntry ent) {
374 			super(ent);
375 		}
376 
377 		@Override
378 		public void apply(DirCacheEntry ent) {
379 			throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
380 		}
381 	}
382 
383 	/**
384 	 * Recursively deletes all paths under a subtree.
385 	 * <p>
386 	 * This deletion command is more generic than {@link DeletePath} as it can
387 	 * remove all records which appear recursively under the same subtree.
388 	 * Multiple stages are removed (if present) for any deleted entry.
389 	 * <p>
390 	 * This command will not remove a single file entry. To remove a single file
391 	 * use {@link DeletePath}.
392 	 *
393 	 * @see DeletePath
394 	 */
395 	public static final class DeleteTree extends PathEdit {
396 		/**
397 		 * Create a new tree deletion command by path name.
398 		 *
399 		 * @param entryPath
400 		 *            path of the subtree within the repository. If the path
401 		 *            does not end with "/" a "/" is implicitly added to ensure
402 		 *            only the subtree's contents are matched by the command.
403 		 *            The special case "" (not "/"!) deletes all entries.
404 		 */
405 		public DeleteTree(String entryPath) {
406 			super(entryPath.isEmpty()
407 					|| entryPath.charAt(entryPath.length() - 1) == '/'
408 					? entryPath
409 					: entryPath + '/');
410 		}
411 
412 		DeleteTree(byte[] path) {
413 			super(appendSlash(path));
414 		}
415 
416 		private static byte[] appendSlash(byte[] path) {
417 			int n = path.length;
418 			if (n > 0 && path[n - 1] != '/') {
419 				byte[] r = new byte[n + 1];
420 				System.arraycopy(path, 0, r, 0, n);
421 				r[n] = '/';
422 				return r;
423 			}
424 			return path;
425 		}
426 
427 		@Override
428 		public void apply(DirCacheEntry ent) {
429 			throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
430 		}
431 	}
432 }