| /* |
| * Copyright (C) 2011, 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.storage.dht; |
| |
| import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; |
| import static org.eclipse.jgit.lib.Ref.Storage.NEW; |
| import static org.eclipse.jgit.storage.dht.RefDataUtil.NONE; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.generated.storage.dht.proto.GitStore.RefData; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectIdSubclassMap; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefDatabase; |
| import org.eclipse.jgit.lib.RefRename; |
| 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.storage.dht.RefDataUtil.IdWithChunk; |
| import org.eclipse.jgit.storage.dht.spi.Context; |
| import org.eclipse.jgit.storage.dht.spi.Database; |
| import org.eclipse.jgit.util.RefList; |
| import org.eclipse.jgit.util.RefMap; |
| |
| /** Repository references stored on top of a DHT database. */ |
| public class DhtRefDatabase extends RefDatabase { |
| private final DhtRepository repository; |
| |
| private final Database db; |
| |
| private final AtomicReference<RefCache> cache; |
| |
| DhtRefDatabase(DhtRepository repository, Database db) { |
| this.repository = repository; |
| this.db = db; |
| this.cache = new AtomicReference<RefCache>(); |
| } |
| |
| DhtRepository getRepository() { |
| return repository; |
| } |
| |
| ChunkKey findChunk(AnyObjectId id) { |
| RefCache c = cache.get(); |
| if (c != null) { |
| IdWithChunk i = c.hints.get(id); |
| if (i != null) |
| return i.getChunkKey(); |
| } |
| return null; |
| } |
| |
| @Override |
| public Ref getRef(String needle) throws IOException { |
| RefCache curr = readRefs(); |
| for (String prefix : SEARCH_PATH) { |
| DhtRef ref = curr.ids.get(prefix + needle); |
| if (ref != null) { |
| ref = resolve(ref, 0, curr.ids); |
| return ref; |
| } |
| } |
| return null; |
| } |
| |
| private DhtRef getOneRef(String refName) throws IOException { |
| RefCache curr = readRefs(); |
| DhtRef ref = curr.ids.get(refName); |
| if (ref != null) |
| return resolve(ref, 0, curr.ids); |
| return ref; |
| } |
| |
| @Override |
| public List<Ref> getAdditionalRefs() { |
| return Collections.emptyList(); |
| } |
| |
| @Override |
| public Map<String, Ref> getRefs(String prefix) throws IOException { |
| RefCache curr = readRefs(); |
| RefList<DhtRef> packed = RefList.emptyList(); |
| RefList<DhtRef> loose = curr.ids; |
| RefList.Builder<DhtRef> sym = new RefList.Builder<DhtRef>(curr.sym.size()); |
| |
| for (int idx = 0; idx < curr.sym.size(); idx++) { |
| DhtRef ref = curr.sym.get(idx); |
| String name = ref.getName(); |
| ref = resolve(ref, 0, loose); |
| if (ref != null && ref.getObjectId() != null) { |
| sym.add(ref); |
| } else { |
| // A broken symbolic reference, we have to drop it from the |
| // collections the client is about to receive. Should be a |
| // rare occurrence so pay a copy penalty. |
| int toRemove = loose.find(name); |
| if (0 <= toRemove) |
| loose = loose.remove(toRemove); |
| } |
| } |
| |
| return new RefMap(prefix, packed, loose, sym.toRefList()); |
| } |
| |
| private DhtRef resolve(DhtRef ref, int depth, RefList<DhtRef> loose) |
| throws IOException { |
| if (!ref.isSymbolic()) |
| return ref; |
| |
| DhtRef dst = (DhtRef) ref.getTarget(); |
| |
| if (MAX_SYMBOLIC_REF_DEPTH <= depth) |
| return null; // claim it doesn't exist |
| |
| dst = loose.get(dst.getName()); |
| if (dst == null) |
| return ref; |
| |
| dst = resolve(dst, depth + 1, loose); |
| if (dst == null) |
| return null; |
| |
| return new DhtSymbolicRef( |
| ref.getName(), |
| dst, |
| ((DhtSymbolicRef) ref).getRefData()); |
| } |
| |
| @Override |
| public Ref peel(Ref ref) throws IOException { |
| final Ref oldLeaf = ref.getLeaf(); |
| if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) |
| return ref; |
| |
| DhtRef newLeaf = doPeel(oldLeaf); |
| |
| RefCache cur = readRefs(); |
| int idx = cur.ids.find(oldLeaf.getName()); |
| if (0 <= idx && cur.ids.get(idx) == oldLeaf) { |
| RefList<DhtRef> newList = cur.ids.set(idx, newLeaf); |
| if (cache.compareAndSet(cur, new RefCache(newList, cur))) |
| cachePeeledState(oldLeaf, newLeaf); |
| } |
| |
| return recreate(ref, newLeaf); |
| } |
| |
| private void cachePeeledState(Ref oldLeaf, Ref newLeaf) { |
| // TODO(spearce) Use an ExecutorService here |
| try { |
| RepositoryKey repo = repository.getRepositoryKey(); |
| RefKey key = RefKey.create(repo, newLeaf.getName()); |
| RefData oldData = ((DhtRef) oldLeaf).getRefData(); |
| RefData newData = ((DhtRef) newLeaf).getRefData(); |
| db.ref().compareAndPut(key, oldData, newData); |
| } catch (TimeoutException e) { |
| // Ignore a timeout here, we were only trying to update |
| // a cached value to save peeling costs in the future. |
| |
| } catch (DhtException e) { |
| // Ignore a database error, this was only an attempt to |
| // fix a value that could be cached to save time later. |
| } |
| } |
| |
| private DhtRef doPeel(final Ref leaf) throws MissingObjectException, |
| IOException { |
| RevWalk rw = new RevWalk(getRepository()); |
| try { |
| DhtReader ctx = (DhtReader) rw.getObjectReader(); |
| RevObject obj = rw.parseAny(leaf.getObjectId()); |
| RefData.Builder d = RefData.newBuilder(((DhtRef) leaf).getRefData()); |
| |
| ChunkKey oKey = ctx.findChunk(leaf.getObjectId()); |
| if (oKey != null) |
| d.getTargetBuilder().setChunkKey(oKey.asString()); |
| else |
| d.getTargetBuilder().clearChunkKey(); |
| |
| if (obj instanceof RevTag) { |
| ObjectId pId = rw.peel(obj); |
| d.getPeeledBuilder().setObjectName(pId.name()); |
| |
| ChunkKey pKey = ctx.findChunk(pId); |
| if (pKey != null) |
| d.getPeeledBuilder().setChunkKey(pKey.asString()); |
| else |
| d.getPeeledBuilder().clearChunkKey(); |
| } else { |
| d.clearPeeled(); |
| } |
| |
| d.setIsPeeled(true); |
| d.setSequence(d.getSequence() + 1); |
| return new DhtObjectIdRef(leaf.getName(), d.build()); |
| } finally { |
| rw.release(); |
| } |
| } |
| |
| private static Ref recreate(final Ref old, final Ref leaf) { |
| if (old.isSymbolic()) { |
| Ref dst = recreate(old.getTarget(), leaf); |
| return new SymbolicRef(old.getName(), dst); |
| } |
| return leaf; |
| } |
| |
| @Override |
| public DhtRefUpdate newUpdate(String refName, boolean detach) |
| throws IOException { |
| boolean detachingSymbolicRef = false; |
| DhtRef ref = getOneRef(refName); |
| if (ref == null) |
| ref = new DhtObjectIdRef(refName, NONE); |
| else |
| detachingSymbolicRef = detach && ref.isSymbolic(); |
| |
| if (detachingSymbolicRef) { |
| RefData src = ((DhtRef) ref.getLeaf()).getRefData(); |
| RefData.Builder b = RefData.newBuilder(ref.getRefData()); |
| b.clearSymref(); |
| b.setTarget(src.getTarget()); |
| ref = new DhtObjectIdRef(refName, b.build()); |
| } |
| |
| RepositoryKey repo = repository.getRepositoryKey(); |
| DhtRefUpdate update = new DhtRefUpdate(this, repo, db, ref); |
| if (detachingSymbolicRef) |
| update.setDetachingSymbolicRef(); |
| return update; |
| } |
| |
| @Override |
| public RefRename newRename(String fromName, String toName) |
| throws IOException { |
| DhtRefUpdate src = newUpdate(fromName, true); |
| DhtRefUpdate dst = newUpdate(toName, true); |
| return new DhtRefRename(src, dst); |
| } |
| |
| @Override |
| public boolean isNameConflicting(String refName) throws IOException { |
| RefList<DhtRef> all = readRefs().ids; |
| |
| // Cannot be nested within an existing reference. |
| int lastSlash = refName.lastIndexOf('/'); |
| while (0 < lastSlash) { |
| String needle = refName.substring(0, lastSlash); |
| if (all.contains(needle)) |
| return true; |
| lastSlash = refName.lastIndexOf('/', lastSlash - 1); |
| } |
| |
| // Cannot be the container of an existing reference. |
| String prefix = refName + '/'; |
| int idx = -(all.find(prefix) + 1); |
| if (idx < all.size() && all.get(idx).getName().startsWith(prefix)) |
| return true; |
| return false; |
| } |
| |
| @Override |
| public void create() { |
| // Nothing to do. |
| } |
| |
| @Override |
| public void close() { |
| clearCache(); |
| } |
| |
| void clearCache() { |
| cache.set(null); |
| } |
| |
| void stored(String refName, RefData newData) { |
| DhtRef ref = fromData(refName, newData); |
| RefCache oldCache, newCache; |
| do { |
| oldCache = cache.get(); |
| if (oldCache == null) |
| return; |
| |
| RefList<DhtRef> ids = oldCache.ids.put(ref); |
| RefList<DhtRef> sym = oldCache.sym; |
| |
| if (ref.isSymbolic()) { |
| sym.put(ref); |
| } else { |
| int p = sym.find(refName); |
| if (0 <= p) |
| sym = sym.remove(p); |
| } |
| |
| newCache = new RefCache(ids, sym, oldCache.hints); |
| } while (!cache.compareAndSet(oldCache, newCache)); |
| } |
| |
| void removed(String refName) { |
| RefCache oldCache, newCache; |
| do { |
| oldCache = cache.get(); |
| if (oldCache == null) |
| return; |
| |
| int p; |
| |
| RefList<DhtRef> ids = oldCache.ids; |
| p = ids.find(refName); |
| if (0 <= p) |
| ids = ids.remove(p); |
| |
| RefList<DhtRef> sym = oldCache.sym; |
| p = sym.find(refName); |
| if (0 <= p) |
| sym = sym.remove(p); |
| |
| newCache = new RefCache(ids, sym, oldCache.hints); |
| } while (!cache.compareAndSet(oldCache, newCache)); |
| } |
| |
| private RefCache readRefs() throws DhtException { |
| RefCache c = cache.get(); |
| if (c == null) { |
| try { |
| c = read(); |
| } catch (TimeoutException e) { |
| throw new DhtTimeoutException(e); |
| } |
| cache.set(c); |
| } |
| return c; |
| } |
| |
| private RefCache read() throws DhtException, TimeoutException { |
| RefList.Builder<DhtRef> id = new RefList.Builder<DhtRef>(); |
| RefList.Builder<DhtRef> sym = new RefList.Builder<DhtRef>(); |
| ObjectIdSubclassMap<IdWithChunk> hints = new ObjectIdSubclassMap<IdWithChunk>(); |
| |
| for (Map.Entry<RefKey, RefData> e : scan()) { |
| DhtRef ref = fromData(e.getKey().getName(), e.getValue()); |
| |
| if (ref.isSymbolic()) |
| sym.add(ref); |
| id.add(ref); |
| |
| if (ref.getObjectId() instanceof IdWithChunk |
| && !hints.contains(ref.getObjectId())) |
| hints.add((IdWithChunk) ref.getObjectId()); |
| if (ref.getPeeledObjectId() instanceof IdWithChunk |
| && !hints.contains(ref.getPeeledObjectId())) |
| hints.add((IdWithChunk) ref.getPeeledObjectId()); |
| } |
| |
| id.sort(); |
| sym.sort(); |
| |
| return new RefCache(id.toRefList(), sym.toRefList(), hints); |
| } |
| |
| static DhtRef fromData(String name, RefData data) { |
| if (data.hasSymref()) |
| return new DhtSymbolicRef(name, data); |
| else |
| return new DhtObjectIdRef(name, data); |
| } |
| |
| private static ObjectId idFrom(RefData.Id src) { |
| ObjectId id = ObjectId.fromString(src.getObjectName()); |
| if (!src.hasChunkKey()) |
| return id; |
| return new IdWithChunk(id, ChunkKey.fromString(src.getChunkKey())); |
| } |
| |
| private Set<Map.Entry<RefKey, RefData>> scan() throws DhtException, |
| TimeoutException { |
| // TODO(spearce) Do we need to perform READ_REPAIR here? |
| RepositoryKey repo = repository.getRepositoryKey(); |
| return db.ref().getAll(Context.LOCAL, repo).entrySet(); |
| } |
| |
| private static class RefCache { |
| final RefList<DhtRef> ids; |
| |
| final RefList<DhtRef> sym; |
| |
| final ObjectIdSubclassMap<IdWithChunk> hints; |
| |
| RefCache(RefList<DhtRef> ids, RefList<DhtRef> sym, |
| ObjectIdSubclassMap<IdWithChunk> hints) { |
| this.ids = ids; |
| this.sym = sym; |
| this.hints = hints; |
| } |
| |
| RefCache(RefList<DhtRef> ids, RefCache old) { |
| this(ids, old.sym, old.hints); |
| } |
| } |
| |
| static interface DhtRef extends Ref { |
| RefData getRefData(); |
| } |
| |
| private static class DhtSymbolicRef extends SymbolicRef implements DhtRef { |
| private final RefData data; |
| |
| DhtSymbolicRef(String refName,RefData data) { |
| super(refName, new DhtObjectIdRef(data.getSymref(), NONE)); |
| this.data = data; |
| } |
| |
| DhtSymbolicRef(String refName, Ref target, RefData data) { |
| super(refName, target); |
| this.data = data; |
| } |
| |
| public RefData getRefData() { |
| return data; |
| } |
| } |
| |
| private static class DhtObjectIdRef implements DhtRef { |
| private final String name; |
| private final RefData data; |
| private final ObjectId objectId; |
| private final ObjectId peeledId; |
| |
| DhtObjectIdRef(String name, RefData data) { |
| this.name = name; |
| this.data = data; |
| this.objectId = data.hasTarget() ? idFrom(data.getTarget()) : null; |
| this.peeledId = data.hasPeeled() ? idFrom(data.getPeeled()) : null; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| |
| public boolean isSymbolic() { |
| return false; |
| } |
| |
| public Ref getLeaf() { |
| return this; |
| } |
| |
| public Ref getTarget() { |
| return this; |
| } |
| |
| public ObjectId getObjectId() { |
| return objectId; |
| } |
| |
| public Ref.Storage getStorage() { |
| return data.hasTarget() ? LOOSE : NEW; |
| } |
| |
| public boolean isPeeled() { |
| return data.getIsPeeled(); |
| } |
| |
| public ObjectId getPeeledObjectId() { |
| return peeledId; |
| } |
| |
| public RefData getRefData() { |
| return data; |
| } |
| } |
| } |