/*
 * Copyright (C) 2016, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.internal.storage.reftree;

import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.R_REFS;
import static org.eclipse.jgit.lib.Constants.encode;
import static org.eclipse.jgit.lib.FileMode.GITLINK;
import static org.eclipse.jgit.lib.FileMode.SYMLINK;
import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
import static org.eclipse.jgit.lib.Ref.Storage.NEW;
import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.DirCacheNameConflictException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.util.RawParseUtils;

Tree of references in the reference graph.

The root corresponds to the "refs/" subdirectory, for example the default reference "refs/heads/master" is stored at path "heads/master" in a RefTree.

Normal references are stored as FileMode.GITLINK tree entries. The ObjectId in the tree entry is the ObjectId the reference refers to.

Symbolic references are stored as FileMode.SYMLINK entries, with the blob storing the name of the target reference.

Annotated tags also store the peeled object using a GITLINK entry with the suffix " ^" (space carrot), for example "tags/v1.0" stores the annotated tag object, while "tags/v1.0 ^" stores the commit the tag annotates.

HEAD is a special case and stored as "..HEAD".

/** * Tree of references in the reference graph. * <p> * The root corresponds to the {@code "refs/"} subdirectory, for example the * default reference {@code "refs/heads/master"} is stored at path * {@code "heads/master"} in a {@code RefTree}. * <p> * Normal references are stored as {@link org.eclipse.jgit.lib.FileMode#GITLINK} * tree entries. The ObjectId in the tree entry is the ObjectId the reference * refers to. * <p> * Symbolic references are stored as * {@link org.eclipse.jgit.lib.FileMode#SYMLINK} entries, with the blob storing * the name of the target reference. * <p> * Annotated tags also store the peeled object using a {@code GITLINK} entry * with the suffix <code>" ^"</code> (space carrot), for example * {@code "tags/v1.0"} stores the annotated tag object, while * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates. * <p> * {@code HEAD} is a special case and stored as {@code "..HEAD"}. */
public class RefTree {
Suffix applied to GITLINK to indicate its the peeled value of a tag.
/** Suffix applied to GITLINK to indicate its the peeled value of a tag. */
public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$ static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$
Create an empty reference tree.
Returns:a new empty reference tree.
/** * Create an empty reference tree. * * @return a new empty reference tree. */
public static RefTree newEmptyTree() { return new RefTree(DirCache.newInCore()); }
Load a reference tree.
Params:
  • reader – reader to scan the reference tree with.
  • tree – the tree to read.
Throws:
Returns:the ref tree read from the commit.
/** * Load a reference tree. * * @param reader * reader to scan the reference tree with. * @param tree * the tree to read. * @return the ref tree read from the commit. * @throws java.io.IOException * the repository cannot be accessed through the reader. * @throws org.eclipse.jgit.errors.CorruptObjectException * a tree object is corrupt and cannot be read. * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * a tree object wasn't actually a tree. * @throws org.eclipse.jgit.errors.MissingObjectException * a reference tree object doesn't exist. */
public static RefTree read(ObjectReader reader, RevTree tree) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { return new RefTree(DirCache.read(reader, tree)); } private DirCache contents; private Map<ObjectId, String> pendingBlobs; private RefTree(DirCache dc) { this.contents = dc; }
Read one reference.

References are always returned peeled (Ref.isPeeled() is true). If the reference points to an annotated tag, the returned reference will be peeled and contain Ref.getPeeledObjectId().

If the reference is a symbolic reference and the chain depth is less than RefDatabase.MAX_SYMBOLIC_REF_DEPTH the returned reference is resolved. If the chain depth is longer, the symbolic reference is returned without resolving.

Params:
  • reader – to access objects necessary to read the requested reference.
  • name – name of the reference to read.
Throws:
  • IOException – cannot read a symbolic reference target.
Returns:the reference; null if it does not exist.
/** * Read one reference. * <p> * References are always returned peeled * ({@link org.eclipse.jgit.lib.Ref#isPeeled()} is true). If the reference * points to an annotated tag, the returned reference will be peeled and * contain {@link org.eclipse.jgit.lib.Ref#getPeeledObjectId()}. * <p> * If the reference is a symbolic reference and the chain depth is less than * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the * returned reference is resolved. If the chain depth is longer, the * symbolic reference is returned without resolving. * * @param reader * to access objects necessary to read the requested reference. * @param name * name of the reference to read. * @return the reference; null if it does not exist. * @throws java.io.IOException * cannot read a symbolic reference target. */
@Nullable public Ref exactRef(ObjectReader reader, String name) throws IOException { Ref r = readRef(reader, name); if (r == null) { return null; } else if (r.isSymbolic()) { return resolve(reader, r, 0); } DirCacheEntry p = contents.getEntry(peeledPath(name)); if (p != null && p.getRawMode() == TYPE_GITLINK) { return new ObjectIdRef.PeeledTag(PACKED, r.getName(), r.getObjectId(), p.getObjectId()); } return r; } private Ref readRef(ObjectReader reader, String name) throws IOException { DirCacheEntry e = contents.getEntry(refPath(name)); return e != null ? toRef(reader, e, name) : null; } private Ref toRef(ObjectReader reader, DirCacheEntry e, String name) throws IOException { int mode = e.getRawMode(); if (mode == TYPE_GITLINK) { ObjectId id = e.getObjectId(); return new ObjectIdRef.PeeledNonTag(PACKED, name, id); } if (mode == TYPE_SYMLINK) { ObjectId id = e.getObjectId(); String n = pendingBlobs != null ? pendingBlobs.get(id) : null; if (n == null) { byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes(); n = RawParseUtils.decode(bin); } Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null); return new SymbolicRef(name, dst); } return null; // garbage file or something; not a reference. } private Ref resolve(ObjectReader reader, Ref ref, int depth) throws IOException { if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) { Ref r = readRef(reader, ref.getTarget().getName()); if (r == null) { return ref; } Ref dst = resolve(reader, r, depth + 1); return new SymbolicRef(ref.getName(), dst); } return ref; }
Attempt a batch of commands against this RefTree.

The batch is applied atomically, either all commands apply at once, or they all reject and the RefTree is left unmodified.

On success (when this method returns true) the command results are left as-is (probably NOT_ATTEMPTED). Result fields are set only when this method returns false to indicate failure.

Params:
  • cmdList – to apply. All commands should still have result NOT_ATTEMPTED.
Returns:true if the commands applied; false if they were rejected.
/** * Attempt a batch of commands against this RefTree. * <p> * The batch is applied atomically, either all commands apply at once, or * they all reject and the RefTree is left unmodified. * <p> * On success (when this method returns {@code true}) the command results * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set * only when this method returns {@code false} to indicate failure. * * @param cmdList * to apply. All commands should still have result NOT_ATTEMPTED. * @return true if the commands applied; false if they were rejected. */
public boolean apply(Collection<Command> cmdList) { try { DirCacheEditor ed = contents.editor(); for (Command cmd : cmdList) { if (!isValidRef(cmd)) { cmd.setResult(REJECTED_OTHER_REASON, JGitText.get().funnyRefname); Command.abort(cmdList, null); return false; } apply(ed, cmd); } ed.finish(); return true; } catch (DirCacheNameConflictException e) { String r1 = refName(e.getPath1()); String r2 = refName(e.getPath2()); for (Command cmd : cmdList) { if (r1.equals(cmd.getRefName()) || r2.equals(cmd.getRefName())) { cmd.setResult(LOCK_FAILURE); break; } } Command.abort(cmdList, null); return false; } catch (LockFailureException e) { Command.abort(cmdList, null); return false; } } private static boolean isValidRef(Command cmd) { String n = cmd.getRefName(); return HEAD.equals(n) || Repository.isValidRefName(n); } private void apply(DirCacheEditor ed, Command cmd) { String path = refPath(cmd.getRefName()); Ref oldRef = cmd.getOldRef(); final Ref newRef = cmd.getNewRef(); if (newRef == null) { checkRef(contents.getEntry(path), cmd); ed.add(new DeletePath(path)); cleanupPeeledRef(ed, oldRef); return; } if (newRef.isSymbolic()) { final String dst = newRef.getTarget().getName(); ed.add(new PathEdit(path) { @Override public void apply(DirCacheEntry ent) { checkRef(ent, cmd); ObjectId id = Command.symref(dst); ent.setFileMode(SYMLINK); ent.setObjectId(id); if (pendingBlobs == null) { pendingBlobs = new HashMap<>(4); } pendingBlobs.put(id, dst); } }.setReplace(false)); cleanupPeeledRef(ed, oldRef); return; } ed.add(new PathEdit(path) { @Override public void apply(DirCacheEntry ent) { checkRef(ent, cmd); ent.setFileMode(GITLINK); ent.setObjectId(newRef.getObjectId()); } }.setReplace(false)); if (newRef.getPeeledObjectId() != null) { ed.add(new PathEdit(peeledPath(newRef.getName())) { @Override public void apply(DirCacheEntry ent) { ent.setFileMode(GITLINK); ent.setObjectId(newRef.getPeeledObjectId()); } }.setReplace(false)); } else { cleanupPeeledRef(ed, oldRef); } } private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) { if (!cmd.checkRef(ent)) { cmd.setResult(LOCK_FAILURE); throw new LockFailureException(); } } private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) { if (ref != null && !ref.isSymbolic() && (!ref.isPeeled() || ref.getPeeledObjectId() != null)) { ed.add(new DeletePath(peeledPath(ref.getName()))); } }
Convert a path name in a RefTree to the reference name known by Git.
Params:
  • path – name read from the RefTree structure, for example "heads/master".
Returns:reference name for the path, "refs/heads/master".
/** * Convert a path name in a RefTree to the reference name known by Git. * * @param path * name read from the RefTree structure, for example * {@code "heads/master"}. * @return reference name for the path, {@code "refs/heads/master"}. */
public static String refName(String path) { if (path.startsWith(ROOT_DOTDOT)) { return path.substring(2); } return R_REFS + path; } static String refPath(String name) { if (name.startsWith(R_REFS)) { return name.substring(R_REFS.length()); } return ROOT_DOTDOT + name; } private static String peeledPath(String name) { return refPath(name) + PEELED_SUFFIX; }
Write this reference tree.
Params:
  • inserter – inserter to use when writing trees to the object database. Caller is responsible for flushing the inserter before trying to read the objects, or exposing them through a reference.
Throws:
Returns:the top level tree.
/** * Write this reference tree. * * @param inserter * inserter to use when writing trees to the object database. * Caller is responsible for flushing the inserter before trying * to read the objects, or exposing them through a reference. * @return the top level tree. * @throws java.io.IOException * a tree could not be written. */
public ObjectId writeTree(ObjectInserter inserter) throws IOException { if (pendingBlobs != null) { for (String s : pendingBlobs.values()) { inserter.insert(OBJ_BLOB, encode(s)); } pendingBlobs = null; } return contents.writeTree(inserter); }
Create a deep copy of this RefTree.
Returns:a deep copy of this RefTree.
/** * Create a deep copy of this RefTree. * * @return a deep copy of this RefTree. */
public RefTree copy() { RefTree r = new RefTree(DirCache.newInCore()); DirCacheBuilder b = r.contents.builder(); for (int i = 0; i < contents.getEntryCount(); i++) { b.add(new DirCacheEntry(contents.getEntry(i))); } b.finish(); if (pendingBlobs != null) { r.pendingBlobs = new HashMap<>(pendingBlobs); } return r; } private static class LockFailureException extends RuntimeException { private static final long serialVersionUID = 1L; } }