A group of updates can be applied by updating the tree in one step, writing out a new root tree, and storing its SHA-1. If references are stored in RefTrees, comparing two repositories is a matter of checking if two SHA-1s are identical. Without RefTrees comparing two repositories requires listing all references and comparing the sets. Track the "refs/" directory as a root tree by storing references that point directly at an object as a GITLINK entry in the tree. For example "refs/heads/master" is written as "heads/master". Annotated tags also store their peeled value with ^{} suffix, using "tags/v1.0" and "tags/v1.0^{}" GITLINK entries. Symbolic references are written as SYMLINK entries with the blob of the symlink carrying the name of the symbolic reference target. HEAD is outside of "refs/" namespace so it is stored as a special "..HEAD" entry. This name is chosen because ".." is not valid in a reference name and it almost looks like "../HEAD" which names HEAD if the reader was inside of the "refs/" directory. A new Command type is required to handle symbolic references and peeled references. Change-Id: Id47e5d4d32149a9e500854147edd7d93c1041a39tags/v4.2.0.201601211800-r
org.eclipse.jgit.internal.storage.dfs;version="[4.2.0,4.3.0)", | org.eclipse.jgit.internal.storage.dfs;version="[4.2.0,4.3.0)", | ||||
org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)", | org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)", | ||||
org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)", | org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)", | ||||
org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)", | |||||
org.eclipse.jgit.junit;version="[4.2.0,4.3.0)", | org.eclipse.jgit.junit;version="[4.2.0,4.3.0)", | ||||
org.eclipse.jgit.lib;version="[4.2.0,4.3.0)", | org.eclipse.jgit.lib;version="[4.2.0,4.3.0)", | ||||
org.eclipse.jgit.merge;version="[4.2.0,4.3.0)", | org.eclipse.jgit.merge;version="[4.2.0,4.3.0)", |
/* | |||||
* 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.R_HEADS; | |||||
import static org.eclipse.jgit.lib.Constants.R_TAGS; | |||||
import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; | |||||
import static org.eclipse.jgit.lib.Ref.Storage.NEW; | |||||
import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; | |||||
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; | |||||
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; | |||||
import static org.junit.Assert.assertEquals; | |||||
import static org.junit.Assert.assertFalse; | |||||
import static org.junit.Assert.assertNotNull; | |||||
import static org.junit.Assert.assertNull; | |||||
import static org.junit.Assert.assertSame; | |||||
import static org.junit.Assert.assertTrue; | |||||
import java.io.IOException; | |||||
import java.util.Arrays; | |||||
import java.util.Collections; | |||||
import org.eclipse.jgit.errors.MissingObjectException; | |||||
import org.eclipse.jgit.internal.JGitText; | |||||
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; | |||||
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; | |||||
import org.eclipse.jgit.junit.TestRepository; | |||||
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.SymbolicRef; | |||||
import org.eclipse.jgit.revwalk.RevBlob; | |||||
import org.eclipse.jgit.revwalk.RevTag; | |||||
import org.eclipse.jgit.revwalk.RevWalk; | |||||
import org.eclipse.jgit.transport.ReceiveCommand; | |||||
import org.junit.Before; | |||||
import org.junit.Test; | |||||
public class RefTreeTest { | |||||
private static final String R_MASTER = R_HEADS + "master"; | |||||
private InMemoryRepository repo; | |||||
private TestRepository<InMemoryRepository> git; | |||||
@Before | |||||
public void setUp() throws IOException { | |||||
repo = new InMemoryRepository(new DfsRepositoryDescription("RefTree")); | |||||
git = new TestRepository<>(repo); | |||||
} | |||||
@Test | |||||
public void testEmptyTree() throws IOException { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
try (ObjectReader reader = repo.newObjectReader()) { | |||||
assertNull(HEAD, tree.exactRef(reader, HEAD)); | |||||
assertNull("master", tree.exactRef(reader, R_MASTER)); | |||||
} | |||||
} | |||||
@Test | |||||
public void testApplyThenReadMaster() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob id = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, id)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
assertSame(NOT_ATTEMPTED, cmd.getResult()); | |||||
try (ObjectReader reader = repo.newObjectReader()) { | |||||
Ref m = tree.exactRef(reader, R_MASTER); | |||||
assertNotNull(R_MASTER, m); | |||||
assertEquals(R_MASTER, m.getName()); | |||||
assertEquals(id, m.getObjectId()); | |||||
assertTrue("peeled", m.isPeeled()); | |||||
} | |||||
} | |||||
@Test | |||||
public void testUpdateMaster() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob id1 = git.blob("A"); | |||||
Command cmd1 = new Command(null, ref(R_MASTER, id1)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd1))); | |||||
assertSame(NOT_ATTEMPTED, cmd1.getResult()); | |||||
RevBlob id2 = git.blob("B"); | |||||
Command cmd2 = new Command(ref(R_MASTER, id1), ref(R_MASTER, id2)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd2))); | |||||
assertSame(NOT_ATTEMPTED, cmd2.getResult()); | |||||
try (ObjectReader reader = repo.newObjectReader()) { | |||||
Ref m = tree.exactRef(reader, R_MASTER); | |||||
assertNotNull(R_MASTER, m); | |||||
assertEquals(R_MASTER, m.getName()); | |||||
assertEquals(id2, m.getObjectId()); | |||||
assertTrue("peeled", m.isPeeled()); | |||||
} | |||||
} | |||||
@Test | |||||
public void testHeadSymref() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob id = git.blob("A"); | |||||
Command cmd1 = new Command(null, ref(R_MASTER, id)); | |||||
Command cmd2 = new Command(null, symref(HEAD, R_MASTER)); | |||||
assertTrue(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); | |||||
assertSame(NOT_ATTEMPTED, cmd1.getResult()); | |||||
assertSame(NOT_ATTEMPTED, cmd2.getResult()); | |||||
try (ObjectReader reader = repo.newObjectReader()) { | |||||
Ref m = tree.exactRef(reader, HEAD); | |||||
assertNotNull(HEAD, m); | |||||
assertEquals(HEAD, m.getName()); | |||||
assertTrue("symbolic", m.isSymbolic()); | |||||
assertNotNull(m.getTarget()); | |||||
assertEquals(R_MASTER, m.getTarget().getName()); | |||||
assertEquals(id, m.getTarget().getObjectId()); | |||||
} | |||||
// Writing flushes some buffers, re-read from blob. | |||||
ObjectId newId = write(tree); | |||||
try (ObjectReader reader = repo.newObjectReader(); | |||||
RevWalk rw = new RevWalk(reader)) { | |||||
tree = RefTree.read(reader, rw.parseTree(newId)); | |||||
Ref m = tree.exactRef(reader, HEAD); | |||||
assertEquals(R_MASTER, m.getTarget().getName()); | |||||
} | |||||
} | |||||
@Test | |||||
public void testTagIsPeeled() throws Exception { | |||||
String name = "v1.0"; | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob id = git.blob("A"); | |||||
RevTag tag = git.tag(name, id); | |||||
String ref = R_TAGS + name; | |||||
Command cmd = create(ref, tag); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
assertSame(NOT_ATTEMPTED, cmd.getResult()); | |||||
try (ObjectReader reader = repo.newObjectReader()) { | |||||
Ref m = tree.exactRef(reader, ref); | |||||
assertNotNull(ref, m); | |||||
assertEquals(ref, m.getName()); | |||||
assertEquals(tag, m.getObjectId()); | |||||
assertTrue("peeled", m.isPeeled()); | |||||
assertEquals(id, m.getPeeledObjectId()); | |||||
} | |||||
} | |||||
@Test | |||||
public void testApplyAlreadyExists() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob a = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, a)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
ObjectId treeId = write(tree); | |||||
RevBlob b = git.blob("B"); | |||||
Command cmd1 = create(R_MASTER, b); | |||||
Command cmd2 = create(R_MASTER, b); | |||||
assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); | |||||
assertSame(LOCK_FAILURE, cmd1.getResult()); | |||||
assertSame(REJECTED_OTHER_REASON, cmd2.getResult()); | |||||
assertEquals(JGitText.get().transactionAborted, cmd2.getMessage()); | |||||
assertEquals(treeId, write(tree)); | |||||
} | |||||
@Test | |||||
public void testApplyWrongOldId() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob a = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, a)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
ObjectId treeId = write(tree); | |||||
RevBlob b = git.blob("B"); | |||||
RevBlob c = git.blob("C"); | |||||
Command cmd1 = update(R_MASTER, b, c); | |||||
Command cmd2 = create(R_MASTER, b); | |||||
assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); | |||||
assertSame(LOCK_FAILURE, cmd1.getResult()); | |||||
assertSame(REJECTED_OTHER_REASON, cmd2.getResult()); | |||||
assertEquals(JGitText.get().transactionAborted, cmd2.getMessage()); | |||||
assertEquals(treeId, write(tree)); | |||||
} | |||||
@Test | |||||
public void testApplyWrongOldIdButAlreadyCurrentIsNoOp() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob a = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, a)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
ObjectId treeId = write(tree); | |||||
RevBlob b = git.blob("B"); | |||||
cmd = update(R_MASTER, b, a); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
assertEquals(treeId, write(tree)); | |||||
} | |||||
@Test | |||||
public void testApplyCannotCreateSubdirectory() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob a = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, a)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
ObjectId treeId = write(tree); | |||||
RevBlob b = git.blob("B"); | |||||
Command cmd1 = create(R_MASTER + "/fail", b); | |||||
assertFalse(tree.apply(Collections.singletonList(cmd1))); | |||||
assertSame(LOCK_FAILURE, cmd1.getResult()); | |||||
assertEquals(treeId, write(tree)); | |||||
} | |||||
@Test | |||||
public void testApplyCannotCreateParentRef() throws Exception { | |||||
RefTree tree = RefTree.newEmptyTree(); | |||||
RevBlob a = git.blob("A"); | |||||
Command cmd = new Command(null, ref(R_MASTER, a)); | |||||
assertTrue(tree.apply(Collections.singletonList(cmd))); | |||||
ObjectId treeId = write(tree); | |||||
RevBlob b = git.blob("B"); | |||||
Command cmd1 = create("refs/heads", b); | |||||
assertFalse(tree.apply(Collections.singletonList(cmd1))); | |||||
assertSame(LOCK_FAILURE, cmd1.getResult()); | |||||
assertEquals(treeId, write(tree)); | |||||
} | |||||
private static Ref ref(String name, ObjectId id) { | |||||
return new ObjectIdRef.PeeledNonTag(LOOSE, name, id); | |||||
} | |||||
private static Ref symref(String name, String dest) { | |||||
Ref d = new ObjectIdRef.PeeledNonTag(NEW, dest, null); | |||||
return new SymbolicRef(name, d); | |||||
} | |||||
private Command create(String name, ObjectId id) | |||||
throws MissingObjectException, IOException { | |||||
return update(name, ObjectId.zeroId(), id); | |||||
} | |||||
private Command update(String name, ObjectId oldId, ObjectId newId) | |||||
throws MissingObjectException, IOException { | |||||
try (RevWalk rw = new RevWalk(repo)) { | |||||
return new Command(rw, new ReceiveCommand(oldId, newId, name)); | |||||
} | |||||
} | |||||
private ObjectId write(RefTree tree) throws IOException { | |||||
try (ObjectInserter ins = repo.newObjectInserter()) { | |||||
ObjectId id = tree.writeTree(ins); | |||||
ins.flush(); | |||||
return id; | |||||
} | |||||
} | |||||
} |
org.eclipse.jgit.pgm.test, | org.eclipse.jgit.pgm.test, | ||||
org.eclipse.jgit.pgm", | org.eclipse.jgit.pgm", | ||||
org.eclipse.jgit.internal.storage.pack;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", | org.eclipse.jgit.internal.storage.pack;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", | ||||
org.eclipse.jgit.internal.storage.reftree;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", | |||||
org.eclipse.jgit.lib;version="4.2.0"; | org.eclipse.jgit.lib;version="4.2.0"; | ||||
uses:="org.eclipse.jgit.revwalk, | uses:="org.eclipse.jgit.revwalk, | ||||
org.eclipse.jgit.treewalk.filter, | org.eclipse.jgit.treewalk.filter, |
NB.encodeInt16(info, infoOffset + P_FLAGS, flags); | NB.encodeInt16(info, infoOffset + P_FLAGS, flags); | ||||
} | } | ||||
/** | |||||
* Duplicate DirCacheEntry with same path and copied info. | |||||
* <p> | |||||
* The same path buffer is reused (avoiding copying), however a new info | |||||
* buffer is created and its contents are copied. | |||||
* | |||||
* @param src | |||||
* entry to clone. | |||||
* @since 4.2 | |||||
*/ | |||||
public DirCacheEntry(DirCacheEntry src) { | |||||
path = src.path; | |||||
info = new byte[INFO_LEN]; | |||||
infoOffset = 0; | |||||
System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN); | |||||
} | |||||
void write(final OutputStream os) throws IOException { | void write(final OutputStream os) throws IOException { | ||||
final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN; | final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN; | ||||
final int pathLen = path.length; | final int pathLen = path.length; |
/* | |||||
* 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.OBJ_BLOB; | |||||
import static org.eclipse.jgit.lib.Constants.encode; | |||||
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.NETWORK; | |||||
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; | |||||
import java.io.IOException; | |||||
import org.eclipse.jgit.annotations.Nullable; | |||||
import org.eclipse.jgit.dircache.DirCacheEntry; | |||||
import org.eclipse.jgit.errors.MissingObjectException; | |||||
import org.eclipse.jgit.lib.ObjectId; | |||||
import org.eclipse.jgit.lib.ObjectIdRef; | |||||
import org.eclipse.jgit.lib.ObjectInserter; | |||||
import org.eclipse.jgit.lib.Ref; | |||||
import org.eclipse.jgit.revwalk.RevObject; | |||||
import org.eclipse.jgit.revwalk.RevTag; | |||||
import org.eclipse.jgit.revwalk.RevWalk; | |||||
import org.eclipse.jgit.transport.ReceiveCommand; | |||||
import org.eclipse.jgit.transport.ReceiveCommand.Result; | |||||
/** | |||||
* Command to create, update or delete an entry inside a {@link RefTree}. | |||||
* <p> | |||||
* Unlike {@link ReceiveCommand} (which can only update a reference to an | |||||
* {@link ObjectId}), a RefTree Command can also create, modify or delete | |||||
* symbolic references to a target reference. | |||||
* <p> | |||||
* RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to | |||||
* process an existing ReceiveCommand against a RefTree. | |||||
* <p> | |||||
* Commands should be passed into {@link RefTree#apply(java.util.Collection)} | |||||
* for processing. | |||||
*/ | |||||
public class Command { | |||||
private final Ref oldRef; | |||||
private final Ref newRef; | |||||
private final ReceiveCommand cmd; | |||||
private Result result; | |||||
/** | |||||
* Create a command to create, update or delete a reference. | |||||
* <p> | |||||
* At least one of {@code oldRef} or {@code newRef} must be supplied. | |||||
* | |||||
* @param oldRef | |||||
* expected value. Null if the ref should not exist. | |||||
* @param newRef | |||||
* desired value, must be peeled if not null and not symbolic. | |||||
* Null to delete the ref. | |||||
*/ | |||||
public Command(@Nullable Ref oldRef, @Nullable Ref newRef) { | |||||
this.oldRef = oldRef; | |||||
this.newRef = newRef; | |||||
this.cmd = null; | |||||
this.result = NOT_ATTEMPTED; | |||||
if (oldRef == null && newRef == null) { | |||||
throw new IllegalArgumentException(); | |||||
} | |||||
if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) { | |||||
throw new IllegalArgumentException(); | |||||
} | |||||
if (oldRef != null && newRef != null | |||||
&& !oldRef.getName().equals(newRef.getName())) { | |||||
throw new IllegalArgumentException(); | |||||
} | |||||
} | |||||
/** | |||||
* Construct a RefTree command wrapped around a ReceiveCommand. | |||||
* | |||||
* @param rw | |||||
* walk instance to peel the {@code newId}. | |||||
* @param cmd | |||||
* command received from a push client. | |||||
* @throws MissingObjectException | |||||
* {@code oldId} or {@code newId} is missing. | |||||
* @throws IOException | |||||
* {@code oldId} or {@code newId} cannot be peeled. | |||||
*/ | |||||
public Command(RevWalk rw, ReceiveCommand cmd) | |||||
throws MissingObjectException, IOException { | |||||
this.oldRef = toRef(rw, cmd.getOldId(), cmd.getRefName(), false); | |||||
this.newRef = toRef(rw, cmd.getNewId(), cmd.getRefName(), true); | |||||
this.cmd = cmd; | |||||
} | |||||
private static Ref toRef(RevWalk rw, ObjectId id, String name, | |||||
boolean mustExist) throws MissingObjectException, IOException { | |||||
if (ObjectId.zeroId().equals(id)) { | |||||
return null; | |||||
} | |||||
try { | |||||
RevObject o = rw.parseAny(id); | |||||
if (o instanceof RevTag) { | |||||
RevObject p = rw.peel(o); | |||||
return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy()); | |||||
} | |||||
return new ObjectIdRef.PeeledNonTag(NETWORK, name, id); | |||||
} catch (MissingObjectException e) { | |||||
if (mustExist) { | |||||
throw e; | |||||
} | |||||
return new ObjectIdRef.Unpeeled(NETWORK, name, id); | |||||
} | |||||
} | |||||
/** @return name of the reference affected by this command. */ | |||||
public String getRefName() { | |||||
if (cmd != null) { | |||||
return cmd.getRefName(); | |||||
} else if (newRef != null) { | |||||
return newRef.getName(); | |||||
} | |||||
return oldRef.getName(); | |||||
} | |||||
/** | |||||
* Set the result of this command. | |||||
* | |||||
* @param result | |||||
* the command result. | |||||
*/ | |||||
public void setResult(Result result) { | |||||
setResult(result, null); | |||||
} | |||||
/** | |||||
* Set the result of this command. | |||||
* | |||||
* @param result | |||||
* the command result. | |||||
* @param why | |||||
* optional message explaining the result status. | |||||
*/ | |||||
public void setResult(Result result, @Nullable String why) { | |||||
if (cmd != null) { | |||||
cmd.setResult(result, why); | |||||
} else { | |||||
this.result = result; | |||||
} | |||||
} | |||||
/** @return result of executing this command. */ | |||||
public Result getResult() { | |||||
return cmd != null ? cmd.getResult() : result; | |||||
} | |||||
/** @return optional message explaining command failure. */ | |||||
@Nullable | |||||
public String getMessage() { | |||||
return cmd != null ? cmd.getMessage() : null; | |||||
} | |||||
/** | |||||
* Old peeled reference. | |||||
* | |||||
* @return the old reference; null if the command is creating the reference. | |||||
*/ | |||||
@Nullable | |||||
public Ref getOldRef() { | |||||
return oldRef; | |||||
} | |||||
/** | |||||
* New peeled reference. | |||||
* | |||||
* @return the new reference; null if the command is deleting the reference. | |||||
*/ | |||||
@Nullable | |||||
public Ref getNewRef() { | |||||
return newRef; | |||||
} | |||||
@Override | |||||
public String toString() { | |||||
StringBuilder s = new StringBuilder(); | |||||
append(s, oldRef, "CREATE"); //$NON-NLS-1$ | |||||
s.append(' '); | |||||
append(s, newRef, "DELETE"); //$NON-NLS-1$ | |||||
s.append(' ').append(getRefName()); | |||||
s.append(' ').append(getResult()); | |||||
if (getMessage() != null) { | |||||
s.append(' ').append(getMessage()); | |||||
} | |||||
return s.toString(); | |||||
} | |||||
private static void append(StringBuilder s, Ref r, String nullName) { | |||||
if (r == null) { | |||||
s.append(nullName); | |||||
} else if (r.isSymbolic()) { | |||||
s.append(r.getTarget().getName()); | |||||
} else { | |||||
ObjectId id = r.getObjectId(); | |||||
if (id != null) { | |||||
s.append(id.name()); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Check the entry is consistent with either the old or the new ref. | |||||
* | |||||
* @param entry | |||||
* current entry; null if the entry does not exist. | |||||
* @return true if entry matches {@link #getOldRef()} or | |||||
* {@link #getNewRef()}; otherwise false. | |||||
*/ | |||||
boolean checkRef(@Nullable DirCacheEntry entry) { | |||||
if (entry != null && entry.getRawMode() == 0) { | |||||
entry = null; | |||||
} | |||||
return check(entry, oldRef) || check(entry, newRef); | |||||
} | |||||
private static boolean check(@Nullable DirCacheEntry cur, | |||||
@Nullable Ref exp) { | |||||
if (cur == null) { | |||||
// Does not exist, ok if oldRef does not exist. | |||||
return exp == null; | |||||
} else if (exp == null) { | |||||
// Expected to not exist, but currently exists, fail. | |||||
return false; | |||||
} | |||||
if (exp.isSymbolic()) { | |||||
String dst = exp.getTarget().getName(); | |||||
return cur.getRawMode() == TYPE_SYMLINK | |||||
&& cur.getObjectId().equals(symref(dst)); | |||||
} | |||||
return cur.getRawMode() == TYPE_GITLINK | |||||
&& cur.getObjectId().equals(exp.getObjectId()); | |||||
} | |||||
static ObjectId symref(String s) { | |||||
@SuppressWarnings("resource") | |||||
ObjectInserter.Formatter fmt = new ObjectInserter.Formatter(); | |||||
return fmt.idFor(OBJ_BLOB, encode(s)); | |||||
} | |||||
} |
/* | |||||
* 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.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.NOT_ATTEMPTED; | |||||
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.FileMode; | |||||
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.SymbolicRef; | |||||
import org.eclipse.jgit.revwalk.RevTree; | |||||
import org.eclipse.jgit.util.RawParseUtils; | |||||
/** | |||||
* 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 FileMode#GITLINK} tree entries. The | |||||
* ObjectId in the tree entry is the ObjectId the reference refers to. | |||||
* <p> | |||||
* Symbolic references are stored as {@link 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>, 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. */ | |||||
public static final String PEELED_SUFFIX = "^{}"; //$NON-NLS-1$ | |||||
static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$ | |||||
/** | |||||
* Create an empty reference tree. | |||||
* | |||||
* @return a new empty reference tree. | |||||
*/ | |||||
public static RefTree newEmptyTree() { | |||||
return new RefTree(DirCache.newInCore()); | |||||
} | |||||
/** | |||||
* 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 IOException | |||||
* the repository cannot be accessed through the reader. | |||||
* @throws CorruptObjectException | |||||
* a tree object is corrupt and cannot be read. | |||||
* @throws IncorrectObjectTypeException | |||||
* a tree object wasn't actually a tree. | |||||
* @throws 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. | |||||
* <p> | |||||
* References are always returned peeled ({@link Ref#isPeeled()} is true). | |||||
* If the reference points to an annotated tag, the returned reference will | |||||
* be peeled and contain {@link 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 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. | |||||
* <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) { | |||||
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; | |||||
} | |||||
} | |||||
return abort(cmdList); | |||||
} catch (LockFailureException e) { | |||||
return abort(cmdList); | |||||
} | |||||
} | |||||
private void apply(DirCacheEditor ed, final 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()))); | |||||
} | |||||
} | |||||
private static boolean abort(Iterable<Command> cmdList) { | |||||
for (Command cmd : cmdList) { | |||||
if (cmd.getResult() == NOT_ATTEMPTED) { | |||||
reject(cmd, JGitText.get().transactionAborted); | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
private static void reject(Command cmd, String msg) { | |||||
cmd.setResult(REJECTED_OTHER_REASON, msg); | |||||
} | |||||
/** | |||||
* 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; | |||||
} | |||||
private 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. | |||||
* | |||||
* @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 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); | |||||
} | |||||
/** @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; | |||||
} | |||||
} |
* <p> | * <p> | ||||
* If the reference is nested deeper than this depth, the implementation | * If the reference is nested deeper than this depth, the implementation | ||||
* should either fail, or at least claim the reference does not exist. | * should either fail, or at least claim the reference does not exist. | ||||
* | |||||
* @since 4.2 | |||||
*/ | */ | ||||
protected static final int MAX_SYMBOLIC_REF_DEPTH = 5; | |||||
public static final int MAX_SYMBOLIC_REF_DEPTH = 5; | |||||
/** Magic value for {@link #getRefs(String)} to return all references. */ | /** Magic value for {@link #getRefs(String)} to return all references. */ | ||||
public static final String ALL = "";//$NON-NLS-1$ | public static final String ALL = "";//$NON-NLS-1$ |