View Javadoc
1   /*
2    * Copyright (C) 2016, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.internal.storage.reftree;
45  
46  import static org.eclipse.jgit.lib.Constants.HEAD;
47  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
48  import static org.eclipse.jgit.lib.Constants.R_REFS;
49  import static org.eclipse.jgit.lib.Constants.encode;
50  import static org.eclipse.jgit.lib.FileMode.GITLINK;
51  import static org.eclipse.jgit.lib.FileMode.SYMLINK;
52  import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
53  import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
54  import static org.eclipse.jgit.lib.Ref.Storage.NEW;
55  import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
56  import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
57  import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
58  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
59  
60  import java.io.IOException;
61  import java.util.Collection;
62  import java.util.HashMap;
63  import java.util.Map;
64  
65  import org.eclipse.jgit.annotations.Nullable;
66  import org.eclipse.jgit.dircache.DirCache;
67  import org.eclipse.jgit.dircache.DirCacheBuilder;
68  import org.eclipse.jgit.dircache.DirCacheEditor;
69  import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
70  import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
71  import org.eclipse.jgit.dircache.DirCacheEntry;
72  import org.eclipse.jgit.errors.CorruptObjectException;
73  import org.eclipse.jgit.errors.DirCacheNameConflictException;
74  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
75  import org.eclipse.jgit.errors.MissingObjectException;
76  import org.eclipse.jgit.internal.JGitText;
77  import org.eclipse.jgit.lib.FileMode;
78  import org.eclipse.jgit.lib.ObjectId;
79  import org.eclipse.jgit.lib.ObjectIdRef;
80  import org.eclipse.jgit.lib.ObjectInserter;
81  import org.eclipse.jgit.lib.ObjectReader;
82  import org.eclipse.jgit.lib.Ref;
83  import org.eclipse.jgit.lib.Repository;
84  import org.eclipse.jgit.lib.SymbolicRef;
85  import org.eclipse.jgit.revwalk.RevTree;
86  import org.eclipse.jgit.util.RawParseUtils;
87  
88  /**
89   * Tree of references in the reference graph.
90   * <p>
91   * The root corresponds to the {@code "refs/"} subdirectory, for example the
92   * default reference {@code "refs/heads/master"} is stored at path
93   * {@code "heads/master"} in a {@code RefTree}.
94   * <p>
95   * Normal references are stored as {@link FileMode#GITLINK} tree entries. The
96   * ObjectId in the tree entry is the ObjectId the reference refers to.
97   * <p>
98   * Symbolic references are stored as {@link FileMode#SYMLINK} entries, with the
99   * blob storing the name of the target reference.
100  * <p>
101  * Annotated tags also store the peeled object using a {@code GITLINK} entry
102  * with the suffix <code>" ^"</code> (space carrot), for example
103  * {@code "tags/v1.0"} stores the annotated tag object, while
104  * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates.
105  * <p>
106  * {@code HEAD} is a special case and stored as {@code "..HEAD"}.
107  */
108 public class RefTree {
109 	/** Suffix applied to GITLINK to indicate its the peeled value of a tag. */
110 	public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$
111 	static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$
112 
113 	/**
114 	 * Create an empty reference tree.
115 	 *
116 	 * @return a new empty reference tree.
117 	 */
118 	public static RefTree newEmptyTree() {
119 		return new RefTree(DirCache.newInCore());
120 	}
121 
122 	/**
123 	 * Load a reference tree.
124 	 *
125 	 * @param reader
126 	 *            reader to scan the reference tree with.
127 	 * @param tree
128 	 *            the tree to read.
129 	 * @return the ref tree read from the commit.
130 	 * @throws IOException
131 	 *             the repository cannot be accessed through the reader.
132 	 * @throws CorruptObjectException
133 	 *             a tree object is corrupt and cannot be read.
134 	 * @throws IncorrectObjectTypeException
135 	 *             a tree object wasn't actually a tree.
136 	 * @throws MissingObjectException
137 	 *             a reference tree object doesn't exist.
138 	 */
139 	public static RefTree read(ObjectReader reader, RevTree tree)
140 			throws MissingObjectException, IncorrectObjectTypeException,
141 			CorruptObjectException, IOException {
142 		return new RefTree(DirCache.read(reader, tree));
143 	}
144 
145 	private DirCache contents;
146 	private Map<ObjectId, String> pendingBlobs;
147 
148 	private RefTree(DirCache dc) {
149 		this.contents = dc;
150 	}
151 
152 	/**
153 	 * Read one reference.
154 	 * <p>
155 	 * References are always returned peeled ({@link Ref#isPeeled()} is true).
156 	 * If the reference points to an annotated tag, the returned reference will
157 	 * be peeled and contain {@link Ref#getPeeledObjectId()}.
158 	 * <p>
159 	 * If the reference is a symbolic reference and the chain depth is less than
160 	 * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the
161 	 * returned reference is resolved. If the chain depth is longer, the
162 	 * symbolic reference is returned without resolving.
163 	 *
164 	 * @param reader
165 	 *            to access objects necessary to read the requested reference.
166 	 * @param name
167 	 *            name of the reference to read.
168 	 * @return the reference; null if it does not exist.
169 	 * @throws IOException
170 	 *             cannot read a symbolic reference target.
171 	 */
172 	@Nullable
173 	public Ref exactRef(ObjectReader reader, String name) throws IOException {
174 		Ref r = readRef(reader, name);
175 		if (r == null) {
176 			return null;
177 		} else if (r.isSymbolic()) {
178 			return resolve(reader, r, 0);
179 		}
180 
181 		DirCacheEntry p = contents.getEntry(peeledPath(name));
182 		if (p != null && p.getRawMode() == TYPE_GITLINK) {
183 			return new ObjectIdRef.PeeledTag(PACKED, r.getName(),
184 					r.getObjectId(), p.getObjectId());
185 		}
186 		return r;
187 	}
188 
189 	private Ref readRef(ObjectReader reader, String name) throws IOException {
190 		DirCacheEntry e = contents.getEntry(refPath(name));
191 		return e != null ? toRef(reader, e, name) : null;
192 	}
193 
194 	private Ref toRef(ObjectReader reader, DirCacheEntry e, String name)
195 			throws IOException {
196 		int mode = e.getRawMode();
197 		if (mode == TYPE_GITLINK) {
198 			ObjectId id = e.getObjectId();
199 			return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
200 		}
201 
202 		if (mode == TYPE_SYMLINK) {
203 			ObjectId id = e.getObjectId();
204 			String n = pendingBlobs != null ? pendingBlobs.get(id) : null;
205 			if (n == null) {
206 				byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes();
207 				n = RawParseUtils.decode(bin);
208 			}
209 			Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null);
210 			return new SymbolicRef(name, dst);
211 		}
212 
213 		return null; // garbage file or something; not a reference.
214 	}
215 
216 	private Ref resolve(ObjectReader reader, Ref ref, int depth)
217 			throws IOException {
218 		if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) {
219 			Ref r = readRef(reader, ref.getTarget().getName());
220 			if (r == null) {
221 				return ref;
222 			}
223 			Ref dst = resolve(reader, r, depth + 1);
224 			return new SymbolicRef(ref.getName(), dst);
225 		}
226 		return ref;
227 	}
228 
229 	/**
230 	 * Attempt a batch of commands against this RefTree.
231 	 * <p>
232 	 * The batch is applied atomically, either all commands apply at once, or
233 	 * they all reject and the RefTree is left unmodified.
234 	 * <p>
235 	 * On success (when this method returns {@code true}) the command results
236 	 * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set
237 	 * only when this method returns {@code false} to indicate failure.
238 	 *
239 	 * @param cmdList
240 	 *            to apply. All commands should still have result NOT_ATTEMPTED.
241 	 * @return true if the commands applied; false if they were rejected.
242 	 */
243 	public boolean apply(Collection<Command> cmdList) {
244 		try {
245 			DirCacheEditor ed = contents.editor();
246 			for (Command cmd : cmdList) {
247 				if (!isValidRef(cmd)) {
248 					cmd.setResult(REJECTED_OTHER_REASON,
249 							JGitText.get().funnyRefname);
250 					Command.abort(cmdList, null);
251 					return false;
252 				}
253 				apply(ed, cmd);
254 			}
255 			ed.finish();
256 			return true;
257 		} catch (DirCacheNameConflictException e) {
258 			String r1 = refName(e.getPath1());
259 			String r2 = refName(e.getPath2());
260 			for (Command cmd : cmdList) {
261 				if (r1.equals(cmd.getRefName())
262 						|| r2.equals(cmd.getRefName())) {
263 					cmd.setResult(LOCK_FAILURE);
264 					break;
265 				}
266 			}
267 			Command.abort(cmdList, null);
268 			return false;
269 		} catch (LockFailureException e) {
270 			Command.abort(cmdList, null);
271 			return false;
272 		}
273 	}
274 
275 	private static boolean isValidRef(Command cmd) {
276 		String n = cmd.getRefName();
277 		return HEAD.equals(n) || Repository.isValidRefName(n);
278 	}
279 
280 	private void apply(DirCacheEditor ed, final Command cmd) {
281 		String path = refPath(cmd.getRefName());
282 		Ref oldRef = cmd.getOldRef();
283 		final Ref newRef = cmd.getNewRef();
284 
285 		if (newRef == null) {
286 			checkRef(contents.getEntry(path), cmd);
287 			ed.add(new DeletePath(path));
288 			cleanupPeeledRef(ed, oldRef);
289 			return;
290 		}
291 
292 		if (newRef.isSymbolic()) {
293 			final String dst = newRef.getTarget().getName();
294 			ed.add(new PathEdit(path) {
295 				@Override
296 				public void apply(DirCacheEntry ent) {
297 					checkRef(ent, cmd);
298 					ObjectId id = Command.symref(dst);
299 					ent.setFileMode(SYMLINK);
300 					ent.setObjectId(id);
301 					if (pendingBlobs == null) {
302 						pendingBlobs = new HashMap<>(4);
303 					}
304 					pendingBlobs.put(id, dst);
305 				}
306 			}.setReplace(false));
307 			cleanupPeeledRef(ed, oldRef);
308 			return;
309 		}
310 
311 		ed.add(new PathEdit(path) {
312 			@Override
313 			public void apply(DirCacheEntry ent) {
314 				checkRef(ent, cmd);
315 				ent.setFileMode(GITLINK);
316 				ent.setObjectId(newRef.getObjectId());
317 			}
318 		}.setReplace(false));
319 
320 		if (newRef.getPeeledObjectId() != null) {
321 			ed.add(new PathEdit(peeledPath(newRef.getName())) {
322 				@Override
323 				public void apply(DirCacheEntry ent) {
324 					ent.setFileMode(GITLINK);
325 					ent.setObjectId(newRef.getPeeledObjectId());
326 				}
327 			}.setReplace(false));
328 		} else {
329 			cleanupPeeledRef(ed, oldRef);
330 		}
331 	}
332 
333 	private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) {
334 		if (!cmd.checkRef(ent)) {
335 			cmd.setResult(LOCK_FAILURE);
336 			throw new LockFailureException();
337 		}
338 	}
339 
340 	private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) {
341 		if (ref != null && !ref.isSymbolic()
342 				&& (!ref.isPeeled() || ref.getPeeledObjectId() != null)) {
343 			ed.add(new DeletePath(peeledPath(ref.getName())));
344 		}
345 	}
346 
347 	/**
348 	 * Convert a path name in a RefTree to the reference name known by Git.
349 	 *
350 	 * @param path
351 	 *            name read from the RefTree structure, for example
352 	 *            {@code "heads/master"}.
353 	 * @return reference name for the path, {@code "refs/heads/master"}.
354 	 */
355 	public static String refName(String path) {
356 		if (path.startsWith(ROOT_DOTDOT)) {
357 			return path.substring(2);
358 		}
359 		return R_REFS + path;
360 	}
361 
362 	static String refPath(String name) {
363 		if (name.startsWith(R_REFS)) {
364 			return name.substring(R_REFS.length());
365 		}
366 		return ROOT_DOTDOT + name;
367 	}
368 
369 	private static String peeledPath(String name) {
370 		return refPath(name) + PEELED_SUFFIX;
371 	}
372 
373 	/**
374 	 * Write this reference tree.
375 	 *
376 	 * @param inserter
377 	 *            inserter to use when writing trees to the object database.
378 	 *            Caller is responsible for flushing the inserter before trying
379 	 *            to read the objects, or exposing them through a reference.
380 	 * @return the top level tree.
381 	 * @throws IOException
382 	 *             a tree could not be written.
383 	 */
384 	public ObjectId writeTree(ObjectInserter inserter) throws IOException {
385 		if (pendingBlobs != null) {
386 			for (String s : pendingBlobs.values()) {
387 				inserter.insert(OBJ_BLOB, encode(s));
388 			}
389 			pendingBlobs = null;
390 		}
391 		return contents.writeTree(inserter);
392 	}
393 
394 	/** @return a deep copy of this RefTree. */
395 	public RefTree copy() {
396 		RefTree r = new RefTree(DirCache.newInCore());
397 		DirCacheBuilder b = r.contents.builder();
398 		for (int i = 0; i < contents.getEntryCount(); i++) {
399 			b.add(new DirCacheEntry(contents.getEntry(i)));
400 		}
401 		b.finish();
402 		if (pendingBlobs != null) {
403 			r.pendingBlobs = new HashMap<>(pendingBlobs);
404 		}
405 		return r;
406 	}
407 
408 	private static class LockFailureException extends RuntimeException {
409 		private static final long serialVersionUID = 1L;
410 	}
411 }