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 java.io.IOException; 48 import java.text.MessageFormat; 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.Comparator; 52 import java.util.List; 53 54 import org.eclipse.jgit.internal.JGitText; 55 import org.eclipse.jgit.lib.Constants; 56 57 /** 58 * Updates a {@link DirCache} by supplying discrete edit commands. 59 * <p> 60 * An editor updates a DirCache by taking a list of {@link PathEdit} commands 61 * and executing them against the entries of the destination cache to produce a 62 * new cache. This edit style allows applications to insert a few commands and 63 * then have the editor compute the proper entry indexes necessary to perform an 64 * efficient in-order update of the index records. This can be easier to use 65 * than {@link DirCacheBuilder}. 66 * <p> 67 * 68 * @see DirCacheBuilder 69 */ 70 public class DirCacheEditor extends BaseDirCacheEditor { 71 private static final Comparator<PathEdit> EDIT_CMP = new Comparator<PathEdit>() { 72 public int compare(final PathEdit o1, final PathEdit o2) { 73 final byte[] a = o1.path; 74 final byte[] b = o2.path; 75 return DirCache.cmp(a, a.length, b, b.length); 76 } 77 }; 78 79 private final List<PathEdit> edits; 80 81 /** 82 * Construct a new editor. 83 * 84 * @param dc 85 * the cache this editor will eventually update. 86 * @param ecnt 87 * estimated number of entries the editor will have upon 88 * completion. This sizes the initial entry table. 89 */ 90 protected DirCacheEditor(final DirCache dc, final int ecnt) { 91 super(dc, ecnt); 92 edits = new ArrayList<PathEdit>(); 93 } 94 95 /** 96 * Append one edit command to the list of commands to be applied. 97 * <p> 98 * Edit commands may be added in any order chosen by the application. They 99 * are automatically rearranged by the builder to provide the most efficient 100 * update possible. 101 * 102 * @param edit 103 * another edit command. 104 */ 105 public void add(final PathEdit edit) { 106 edits.add(edit); 107 } 108 109 @Override 110 public boolean commit() throws IOException { 111 if (edits.isEmpty()) { 112 // No changes? Don't rewrite the index. 113 // 114 cache.unlock(); 115 return true; 116 } 117 return super.commit(); 118 } 119 120 public void finish() { 121 if (!edits.isEmpty()) { 122 applyEdits(); 123 replace(); 124 } 125 } 126 127 private void applyEdits() { 128 Collections.sort(edits, EDIT_CMP); 129 130 final int maxIdx = cache.getEntryCount(); 131 int lastIdx = 0; 132 for (final PathEdit e : edits) { 133 int eIdx = cache.findEntry(e.path, e.path.length); 134 final boolean missing = eIdx < 0; 135 if (eIdx < 0) 136 eIdx = -(eIdx + 1); 137 final int cnt = Math.min(eIdx, maxIdx) - lastIdx; 138 if (cnt > 0) 139 fastKeep(lastIdx, cnt); 140 lastIdx = missing ? eIdx : cache.nextEntry(eIdx); 141 142 if (e instanceof DeletePath) 143 continue; 144 if (e instanceof DeleteTree) { 145 lastIdx = cache.nextEntry(e.path, e.path.length, eIdx); 146 continue; 147 } 148 149 if (missing) { 150 final DirCacheEntry ent = new DirCacheEntry(e.path); 151 e.apply(ent); 152 if (ent.getRawMode() == 0) 153 throw new IllegalArgumentException(MessageFormat.format(JGitText.get().fileModeNotSetForPath 154 , ent.getPathString())); 155 fastAdd(ent); 156 } else { 157 // Apply to all entries of the current path (different stages) 158 for (int i = eIdx; i < lastIdx; i++) { 159 final DirCacheEntry ent = cache.getEntry(i); 160 e.apply(ent); 161 fastAdd(ent); 162 } 163 } 164 } 165 166 final int cnt = maxIdx - lastIdx; 167 if (cnt > 0) 168 fastKeep(lastIdx, cnt); 169 } 170 171 /** 172 * Any index record update. 173 * <p> 174 * Applications should subclass and provide their own implementation for the 175 * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once 176 * for each record in the index which matches the path name. If there are 177 * multiple records (for example in stages 1, 2 and 3), the edit instance 178 * will be called multiple times, once for each stage. 179 */ 180 public abstract static class PathEdit { 181 final byte[] path; 182 183 /** 184 * Create a new update command by path name. 185 * 186 * @param entryPath 187 * path of the file within the repository. 188 */ 189 public PathEdit(final String entryPath) { 190 path = Constants.encode(entryPath); 191 } 192 193 /** 194 * Create a new update command for an existing entry instance. 195 * 196 * @param ent 197 * entry instance to match path of. Only the path of this 198 * entry is actually considered during command evaluation. 199 */ 200 public PathEdit(final DirCacheEntry ent) { 201 path = ent.path; 202 } 203 204 /** 205 * Apply the update to a single cache entry matching the path. 206 * <p> 207 * After apply is invoked the entry is added to the output table, and 208 * will be included in the new index. 209 * 210 * @param ent 211 * the entry being processed. All fields are zeroed out if 212 * the path is a new path in the index. 213 */ 214 public abstract void apply(DirCacheEntry ent); 215 } 216 217 /** 218 * Deletes a single file entry from the index. 219 * <p> 220 * This deletion command removes only a single file at the given location, 221 * but removes multiple stages (if present) for that path. To remove a 222 * complete subtree use {@link DeleteTree} instead. 223 * 224 * @see DeleteTree 225 */ 226 public static final class DeletePath extends PathEdit { 227 /** 228 * Create a new deletion command by path name. 229 * 230 * @param entryPath 231 * path of the file within the repository. 232 */ 233 public DeletePath(final String entryPath) { 234 super(entryPath); 235 } 236 237 /** 238 * Create a new deletion command for an existing entry instance. 239 * 240 * @param ent 241 * entry instance to remove. Only the path of this entry is 242 * actually considered during command evaluation. 243 */ 244 public DeletePath(final DirCacheEntry ent) { 245 super(ent); 246 } 247 248 public void apply(final DirCacheEntry ent) { 249 throw new UnsupportedOperationException(JGitText.get().noApplyInDelete); 250 } 251 } 252 253 /** 254 * Recursively deletes all paths under a subtree. 255 * <p> 256 * This deletion command is more generic than {@link DeletePath} as it can 257 * remove all records which appear recursively under the same subtree. 258 * Multiple stages are removed (if present) for any deleted entry. 259 * <p> 260 * This command will not remove a single file entry. To remove a single file 261 * use {@link DeletePath}. 262 * 263 * @see DeletePath 264 */ 265 public static final class DeleteTree extends PathEdit { 266 /** 267 * Create a new tree deletion command by path name. 268 * 269 * @param entryPath 270 * path of the subtree within the repository. If the path 271 * does not end with "/" a "/" is implicitly added to ensure 272 * only the subtree's contents are matched by the command. 273 * The special case "" (not "/"!) deletes all entries. 274 */ 275 public DeleteTree(final String entryPath) { 276 super( 277 (entryPath.endsWith("/") || entryPath.length() == 0) ? entryPath //$NON-NLS-1$ 278 : entryPath + "/"); //$NON-NLS-1$ 279 } 280 281 public void apply(final DirCacheEntry ent) { 282 throw new UnsupportedOperationException(JGitText.get().noApplyInDelete); 283 } 284 } 285 }