RefTreeDatabase.java
/*
* 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.Ref.Storage.LOOSE;
import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Ref.Storage;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.RefList;
import org.eclipse.jgit.util.RefMap;
/**
* Reference database backed by a
* {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
* <p>
* The storage for RefTreeDatabase has two parts. The main part is a native Git
* tree object stored under the {@code refs/txn} namespace. To avoid cycles,
* references to {@code refs/txn} are not stored in that tree object, but
* instead in a "bootstrap" layer, which is a separate
* {@link org.eclipse.jgit.lib.RefDatabase} such as
* {@link org.eclipse.jgit.internal.storage.file.RefDirectory} using local
* reference files inside of {@code $GIT_DIR/refs}.
*/
public class RefTreeDatabase extends RefDatabase {
private final Repository repo;
private final RefDatabase bootstrap;
private final String txnCommitted;
@Nullable
private final String txnNamespace;
private volatile Scanner.Result refs;
/**
* Create a RefTreeDb for a repository.
*
* @param repo
* the repository using references in this database.
* @param bootstrap
* bootstrap reference database storing the references that
* anchor the
* {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
*/
public RefTreeDatabase(Repository repo, RefDatabase bootstrap) {
Config cfg = repo.getConfig();
String committed = cfg.getString("reftree", null, "committedRef"); //$NON-NLS-1$ //$NON-NLS-2$
if (committed == null || committed.isEmpty()) {
committed = "refs/txn/committed"; //$NON-NLS-1$
}
this.repo = repo;
this.bootstrap = bootstrap;
this.txnNamespace = initNamespace(committed);
this.txnCommitted = committed;
}
/**
* Create a RefTreeDb for a repository.
*
* @param repo
* the repository using references in this database.
* @param bootstrap
* bootstrap reference database storing the references that
* anchor the
* {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
* @param txnCommitted
* name of the bootstrap reference holding the committed RefTree.
*/
public RefTreeDatabase(Repository repo, RefDatabase bootstrap,
String txnCommitted) {
this.repo = repo;
this.bootstrap = bootstrap;
this.txnNamespace = initNamespace(txnCommitted);
this.txnCommitted = txnCommitted;
}
private static String initNamespace(String committed) {
int s = committed.lastIndexOf('/');
if (s < 0) {
return null;
}
return committed.substring(0, s + 1); // Keep trailing '/'.
}
Repository getRepository() {
return repo;
}
/**
* Get the bootstrap reference database
*
* @return the bootstrap reference database, which must be used to access
* {@link #getTxnCommitted()}, {@link #getTxnNamespace()}.
*/
public RefDatabase getBootstrap() {
return bootstrap;
}
/**
* Get name of bootstrap reference anchoring committed RefTree.
*
* @return name of bootstrap reference anchoring committed RefTree.
*/
public String getTxnCommitted() {
return txnCommitted;
}
/**
* Get namespace used by bootstrap layer.
*
* @return namespace used by bootstrap layer, e.g. {@code refs/txn/}. Always
* ends in {@code '/'}.
*/
@Nullable
public String getTxnNamespace() {
return txnNamespace;
}
/** {@inheritDoc} */
@Override
public void create() throws IOException {
bootstrap.create();
}
/** {@inheritDoc} */
@Override
public boolean performsAtomicTransactions() {
return true;
}
/** {@inheritDoc} */
@Override
public void refresh() {
bootstrap.refresh();
}
/** {@inheritDoc} */
@Override
public void close() {
refs = null;
bootstrap.close();
}
/** {@inheritDoc} */
@Override
public Ref exactRef(String name) throws IOException {
if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
// Pass through names like MERGE_HEAD, ORIG_HEAD, FETCH_HEAD.
return bootstrap.exactRef(name);
} else if (conflictsWithBootstrap(name)) {
return null;
}
boolean partial = false;
Ref src = bootstrap.exactRef(txnCommitted);
Scanner.Result c = refs;
if (c == null || !c.refTreeId.equals(idOf(src))) {
c = Scanner.scanRefTree(repo, src, prefixOf(name), false);
partial = true;
}
Ref r = c.all.get(name);
if (r != null && r.isSymbolic()) {
r = c.sym.get(name);
if (partial && r.getObjectId() == null) {
// Attempting exactRef("HEAD") with partial scan will leave
// an unresolved symref as its target e.g. refs/heads/master
// was not read by the partial scan. Scan everything instead.
return getRefs(ALL).get(name);
}
}
return r;
}
private static String prefixOf(String name) {
int s = name.lastIndexOf('/');
if (s >= 0) {
return name.substring(0, s);
}
return ""; //$NON-NLS-1$
}
/** {@inheritDoc} */
@Override
public Map<String, Ref> getRefs(String prefix) throws IOException {
if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '/') {
return new HashMap<>(0);
}
Ref src = bootstrap.exactRef(txnCommitted);
Scanner.Result c = refs;
if (c == null || !c.refTreeId.equals(idOf(src))) {
c = Scanner.scanRefTree(repo, src, prefix, true);
if (prefix.isEmpty()) {
refs = c;
}
}
return new RefMap(prefix, RefList.<Ref> emptyList(), c.all, c.sym);
}
private static ObjectId idOf(@Nullable Ref src) {
return src != null && src.getObjectId() != null
? src.getObjectId()
: ObjectId.zeroId();
}
/** {@inheritDoc} */
@Override
public List<Ref> getAdditionalRefs() throws IOException {
Collection<Ref> txnRefs;
if (txnNamespace != null) {
txnRefs = bootstrap.getRefsByPrefix(txnNamespace);
} else {
Ref r = bootstrap.exactRef(txnCommitted);
if (r != null && r.getObjectId() != null) {
txnRefs = Collections.singleton(r);
} else {
txnRefs = Collections.emptyList();
}
}
List<Ref> otherRefs = bootstrap.getAdditionalRefs();
List<Ref> all = new ArrayList<>(txnRefs.size() + otherRefs.size());
all.addAll(txnRefs);
all.addAll(otherRefs);
return all;
}
/** {@inheritDoc} */
@Override
public Ref peel(Ref ref) throws IOException {
Ref i = ref.getLeaf();
ObjectId id = i.getObjectId();
if (i.isPeeled() || id == null) {
return ref;
}
try (RevWalk rw = new RevWalk(repo)) {
RevObject obj = rw.parseAny(id);
if (obj instanceof RevTag) {
ObjectId p = rw.peel(obj).copy();
i = new ObjectIdRef.PeeledTag(PACKED, i.getName(), id, p);
} else {
i = new ObjectIdRef.PeeledNonTag(PACKED, i.getName(), id);
}
}
return recreate(ref, i);
}
private static Ref recreate(Ref old, Ref leaf) {
if (old.isSymbolic()) {
Ref dst = recreate(old.getTarget(), leaf);
return new SymbolicRef(old.getName(), dst);
}
return leaf;
}
/** {@inheritDoc} */
@Override
public boolean isNameConflicting(String name) throws IOException {
return conflictsWithBootstrap(name)
|| !getConflictingNames(name).isEmpty();
}
/** {@inheritDoc} */
@Override
public BatchRefUpdate newBatchUpdate() {
return new RefTreeBatch(this);
}
/** {@inheritDoc} */
@Override
public RefUpdate newUpdate(String name, boolean detach) throws IOException {
if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
return bootstrap.newUpdate(name, detach);
}
if (conflictsWithBootstrap(name)) {
return new AlwaysFailUpdate(this, name);
}
Ref r = exactRef(name);
if (r == null) {
r = new ObjectIdRef.Unpeeled(Storage.NEW, name, null);
}
boolean detaching = detach && r.isSymbolic();
if (detaching) {
r = new ObjectIdRef.Unpeeled(LOOSE, name, r.getObjectId());
}
RefTreeUpdate u = new RefTreeUpdate(this, r);
if (detaching) {
u.setDetachingSymbolicRef();
}
return u;
}
/** {@inheritDoc} */
@Override
public RefRename newRename(String fromName, String toName)
throws IOException {
RefUpdate from = newUpdate(fromName, true);
RefUpdate to = newUpdate(toName, true);
return new RefTreeRename(this, from, to);
}
boolean conflictsWithBootstrap(String name) {
if (txnNamespace != null && name.startsWith(txnNamespace)) {
return true;
} else if (txnCommitted.equals(name)) {
return true;
}
if (name.indexOf('/') < 0 && !HEAD.equals(name)) {
return true;
}
if (name.length() > txnCommitted.length()
&& name.charAt(txnCommitted.length()) == '/'
&& name.startsWith(txnCommitted)) {
return true;
}
return false;
}
}