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 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 }