/* * Copyright (C) 2019 Google LLC and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.lib.Ref.UNDEFINED_UPDATE_INDEX; import static org.eclipse.jgit.lib.Ref.Storage.NEW; import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.internal.storage.reftable.MergedReftable; import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate; import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefRename; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogReader; 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.transport.ReceiveCommand; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.RefList; import org.eclipse.jgit.util.RefMap; /** * Implements RefDatabase using reftable for storage. * * This class is threadsafe. */ public class FileReftableDatabase extends RefDatabase { private final ReftableDatabase reftableDatabase; private final FileRepository fileRepository; private final FileReftableStack reftableStack; FileReftableDatabase(FileRepository repo, File refstackName) throws IOException { this.fileRepository = repo; this.reftableStack = new FileReftableStack(refstackName, new File(fileRepository.getDirectory(), Constants.REFTABLE), () -> fileRepository.fireEvent(new RefsChangedEvent()), () -> fileRepository.getConfig()); this.reftableDatabase = new ReftableDatabase() { @Override public MergedReftable openMergedReftable() throws IOException { return reftableStack.getMergedReftable(); } }; } ReflogReader getReflogReader(String refname) throws IOException { return reftableDatabase.getReflogReader(refname); } /** * @param repoDir * @return whether the given repo uses reftable for refdb storage. */ public static boolean isReftable(File repoDir) { return new File(repoDir, "refs").isFile() //$NON-NLS-1$ && new File(repoDir, Constants.REFTABLE).isDirectory(); } /** {@inheritDoc} */ @Override public boolean hasFastTipsWithSha1() throws IOException { return reftableDatabase.hasFastTipsWithSha1(); } /** * Runs a full compaction for GC purposes. * @throws IOException on I/O errors */ public void compactFully() throws IOException { reftableDatabase.getLock().lock(); try { reftableStack.compactFully(); reftableDatabase.clearCache(); } finally { reftableDatabase.getLock().unlock(); } } private ReentrantLock getLock() { return reftableDatabase.getLock(); } /** {@inheritDoc} */ @Override public boolean performsAtomicTransactions() { return true; } /** {@inheritDoc} */ @NonNull @Override public BatchRefUpdate newBatchUpdate() { return new FileReftableBatchRefUpdate(this, fileRepository); } /** {@inheritDoc} */ @Override public RefUpdate newUpdate(String refName, boolean detach) throws IOException { boolean detachingSymbolicRef = false; Ref ref = exactRef(refName); if (ref == null) { ref = new ObjectIdRef.Unpeeled(NEW, refName, null); } else { detachingSymbolicRef = detach && ref.isSymbolic(); } RefUpdate update = new FileReftableRefUpdate(ref); if (detachingSymbolicRef) { update.setDetachingSymbolicRef(); } return update; } /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { return reftableDatabase.exactRef(name); } /** {@inheritDoc} */ @Override public List getRefs() throws IOException { return super.getRefs(); } /** {@inheritDoc} */ @Override public Map getRefs(String prefix) throws IOException { List refs = reftableDatabase.getRefsByPrefix(prefix); RefList.Builder builder = new RefList.Builder<>(refs.size()); for (Ref r : refs) { builder.add(r); } return new RefMap(prefix, builder.toRefList(), RefList.emptyList(), RefList.emptyList()); } /** {@inheritDoc} */ @Override public List getAdditionalRefs() throws IOException { return Collections.emptyList(); } /** {@inheritDoc} */ @Override public Ref peel(Ref ref) throws IOException { Ref oldLeaf = ref.getLeaf(); if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) { return ref; } return recreate(ref, doPeel(oldLeaf), hasVersioning()); } private Ref doPeel(Ref leaf) throws IOException { try (RevWalk rw = new RevWalk(fileRepository)) { RevObject obj = rw.parseAny(leaf.getObjectId()); if (obj instanceof RevTag) { return new ObjectIdRef.PeeledTag(leaf.getStorage(), leaf.getName(), leaf.getObjectId(), rw.peel(obj).copy(), hasVersioning() ? leaf.getUpdateIndex() : UNDEFINED_UPDATE_INDEX); } return new ObjectIdRef.PeeledNonTag(leaf.getStorage(), leaf.getName(), leaf.getObjectId(), hasVersioning() ? leaf.getUpdateIndex() : UNDEFINED_UPDATE_INDEX); } } private static Ref recreate(Ref old, Ref leaf, boolean hasVersioning) { if (old.isSymbolic()) { Ref dst = recreate(old.getTarget(), leaf, hasVersioning); return new SymbolicRef(old.getName(), dst, hasVersioning ? old.getUpdateIndex() : UNDEFINED_UPDATE_INDEX); } return leaf; } private class FileRefRename extends RefRename { FileRefRename(RefUpdate src, RefUpdate dst) { super(src, dst); } void writeRename(ReftableWriter w) throws IOException { long idx = reftableDatabase.nextUpdateIndex(); w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin(); List refs = new ArrayList<>(3); Ref dest = destination.getRef(); Ref head = exactRef(Constants.HEAD); if (head != null && head.isSymbolic() && head.getLeaf().getName().equals(source.getName())) { head = new SymbolicRef(Constants.HEAD, dest, idx); refs.add(head); } ObjectId objId = source.getRef().getObjectId(); // XXX should we check if the source is a Tag vs. NonTag? refs.add(new ObjectIdRef.PeeledNonTag(Ref.Storage.NEW, destination.getName(), objId)); refs.add(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, source.getName(), null)); w.sortAndWriteRefs(refs); PersonIdent who = destination.getRefLogIdent(); if (who == null) { who = new PersonIdent(fileRepository); } if (!destination.getRefLogMessage().isEmpty()) { List refnames = refs.stream().map(r -> r.getName()) .collect(Collectors.toList()); Collections.sort(refnames); for (String s : refnames) { ObjectId old = (Constants.HEAD.equals(s) || s.equals(source.getName())) ? objId : ObjectId.zeroId(); ObjectId newId = (Constants.HEAD.equals(s) || s.equals(destination.getName())) ? objId : ObjectId.zeroId(); w.writeLog(s, idx, who, old, newId, destination.getRefLogMessage()); } } } @Override protected RefUpdate.Result doRename() throws IOException { Ref src = exactRef(source.getName()); if (exactRef(destination.getName()) != null || src == null || !source.getOldObjectId().equals(src.getObjectId())) { return RefUpdate.Result.LOCK_FAILURE; } if (src.isSymbolic()) { // We could support this, but this is easier and compatible. return RefUpdate.Result.IO_FAILURE; } if (!addReftable(this::writeRename)) { return RefUpdate.Result.LOCK_FAILURE; } return RefUpdate.Result.RENAMED; } } /** {@inheritDoc} */ @Override public RefRename newRename(String fromName, String toName) throws IOException { RefUpdate src = newUpdate(fromName, true); RefUpdate dst = newUpdate(toName, true); return new FileRefRename(src, dst); } /** {@inheritDoc} */ @Override public boolean isNameConflicting(String name) throws IOException { return reftableDatabase.isNameConflicting(name, new TreeSet<>(), new HashSet<>()); } /** {@inheritDoc} */ @Override public void close() { reftableStack.close(); } /** {@inheritDoc} */ @Override public void create() throws IOException { FileUtils.mkdir( new File(fileRepository.getDirectory(), Constants.REFTABLE), true); } private boolean addReftable(FileReftableStack.Writer w) throws IOException { if (!reftableStack.addReftable(w)) { reftableStack.reload(); reftableDatabase.clearCache(); return false; } reftableDatabase.clearCache(); return true; } private class FileReftableBatchRefUpdate extends ReftableBatchRefUpdate { FileReftableBatchRefUpdate(FileReftableDatabase db, Repository repository) { super(db, db.reftableDatabase, db.getLock(), repository); } @Override protected void applyUpdates(List newRefs, List pending) throws IOException { if (!addReftable(rw -> write(rw, newRefs, pending))) { for (ReceiveCommand c : pending) { if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { c.setResult(RefUpdate.Result.LOCK_FAILURE); } } } } } private class FileReftableRefUpdate extends RefUpdate { FileReftableRefUpdate(Ref ref) { super(ref); } @Override protected RefDatabase getRefDatabase() { return FileReftableDatabase.this; } @Override protected Repository getRepository() { return FileReftableDatabase.this.fileRepository; } @Override protected void unlock() { // nop. } private RevWalk rw; private Ref dstRef; @Override public Result update(RevWalk walk) throws IOException { try { rw = walk; return super.update(walk); } finally { rw = null; } } @Override protected boolean tryLock(boolean deref) throws IOException { dstRef = getRef(); if (deref) { dstRef = dstRef.getLeaf(); } Ref derefed = exactRef(dstRef.getName()); if (derefed != null) { setOldObjectId(derefed.getObjectId()); } return true; } void writeUpdate(ReftableWriter w) throws IOException { Ref newRef = null; if (rw != null && !ObjectId.zeroId().equals(getNewObjectId())) { RevObject obj = rw.parseAny(getNewObjectId()); if (obj instanceof RevTag) { newRef = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, dstRef.getName(), getNewObjectId(), rw.peel(obj).copy()); } } if (newRef == null) { newRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, dstRef.getName(), getNewObjectId()); } long idx = reftableDatabase.nextUpdateIndex(); w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() .writeRef(newRef); ObjectId oldId = getOldObjectId(); if (oldId == null) { oldId = ObjectId.zeroId(); } w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId, getNewObjectId(), getRefLogMessage()); } @Override public PersonIdent getRefLogIdent() { PersonIdent who = super.getRefLogIdent(); if (who == null) { who = new PersonIdent(getRepository()); } return who; } void writeDelete(ReftableWriter w) throws IOException { Ref newRef = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dstRef.getName(), null); long idx = reftableDatabase.nextUpdateIndex(); w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() .writeRef(newRef); ObjectId oldId = ObjectId.zeroId(); Ref old = exactRef(dstRef.getName()); if (old != null) { old = old.getLeaf(); if (old.getObjectId() != null) { oldId = old.getObjectId(); } } w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId, ObjectId.zeroId(), getRefLogMessage()); } @Override protected Result doUpdate(Result desiredResult) throws IOException { if (isRefLogIncludingResult()) { setRefLogMessage( getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$ false); } if (!addReftable(this::writeUpdate)) { return Result.LOCK_FAILURE; } return desiredResult; } @Override protected Result doDelete(Result desiredResult) throws IOException { if (isRefLogIncludingResult()) { setRefLogMessage( getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$ false); } if (!addReftable(this::writeDelete)) { return Result.LOCK_FAILURE; } return desiredResult; } void writeLink(ReftableWriter w) throws IOException { long idx = reftableDatabase.nextUpdateIndex(); w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() .writeRef(dstRef); ObjectId beforeId = ObjectId.zeroId(); Ref before = exactRef(dstRef.getName()); if (before != null) { before = before.getLeaf(); if (before.getObjectId() != null) { beforeId = before.getObjectId(); } } Ref after = dstRef.getLeaf(); ObjectId afterId = ObjectId.zeroId(); if (after.getObjectId() != null) { afterId = after.getObjectId(); } w.writeLog(dstRef.getName(), idx, getRefLogIdent(), beforeId, afterId, getRefLogMessage()); } @Override protected Result doLink(String target) throws IOException { if (isRefLogIncludingResult()) { setRefLogMessage( getRefLogMessage() + ": " + Result.FORCED.toString(), //$NON-NLS-1$ false); } boolean exists = exactRef(getName()) != null; dstRef = new SymbolicRef(getName(), new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null), reftableDatabase.nextUpdateIndex()); if (!addReftable(this::writeLink)) { return Result.LOCK_FAILURE; } // XXX unclear if we should support FORCED here. Baseclass says // NEW is OK ? return exists ? Result.FORCED : Result.NEW; } } private static void writeConvertTable(Repository repo, ReftableWriter w, boolean writeLogs) throws IOException { int size = 0; List refs = repo.getRefDatabase().getRefs(); if (writeLogs) { for (Ref r : refs) { ReflogReader rlr = repo.getReflogReader(r.getName()); if (rlr != null) { size = Math.max(rlr.getReverseEntries().size(), size); } } } // We must use 1 here, nextUpdateIndex() on the empty stack is 1. w.setMinUpdateIndex(1).setMaxUpdateIndex(size + 1).begin(); // The spec says to write the logs in the first table, and put refs in a // separate table, but this complicates the compaction (when we can we drop // deletions? Can we compact the .log table and the .ref table together?) try (RevWalk rw = new RevWalk(repo)) { List toWrite = new ArrayList<>(refs.size()); for (Ref r : refs) { toWrite.add(refForWrite(rw, r)); } w.sortAndWriteRefs(toWrite); } if (writeLogs) { for (Ref r : refs) { long idx = size; ReflogReader reader = repo.getReflogReader(r.getName()); if (reader == null) { continue; } for (ReflogEntry e : reader.getReverseEntries()) { w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(), e.getNewId(), e.getComment()); idx--; } } } } private static Ref refForWrite(RevWalk rw, Ref r) throws IOException { if (r.isSymbolic()) { return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW, r.getTarget().getName(), null)); } ObjectId newId = r.getObjectId(); RevObject obj = rw.parseAny(newId); RevObject peel = null; if (obj instanceof RevTag) { peel = rw.peel(obj); } if (peel != null) { return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId, peel.copy()); } return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId); } /** * @param repo * the repository * @param refstackName * the filename for the stack * @param writeLogs * whether to write reflogs * @return a reftable based RefDB from an existing repository. * @throws IOException * on IO error */ public static FileReftableDatabase convertFrom(FileRepository repo, File refstackName, boolean writeLogs) throws IOException { FileReftableDatabase newDb = null; try { File reftableDir = new File(repo.getDirectory(), Constants.REFTABLE); if (!reftableDir.isDirectory()) { reftableDir.mkdir(); } try (FileReftableStack stack = new FileReftableStack(refstackName, reftableDir, null, () -> repo.getConfig())) { stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs)); } refstackName = null; } finally { if (refstackName != null) { refstackName.delete(); } } return newDb; } }