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