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