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(final PathEdit o1, final 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(final DirCache dc, final 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(final 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(final 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(final 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(final 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(final DirCacheEntry ent) {
389 super(ent);
390 }
391
392 @Override
393 public void apply(final 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(final DirCacheEntry ent) {
444 throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
445 }
446 }
447 }