FileReftableDatabase.java

  1. /*
  2.  * Copyright (C) 2019 Google LLC and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.storage.file;

  11. import static org.eclipse.jgit.lib.Ref.UNDEFINED_UPDATE_INDEX;
  12. import static org.eclipse.jgit.lib.Ref.Storage.NEW;
  13. import static org.eclipse.jgit.lib.Ref.Storage.PACKED;

  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.util.ArrayList;
  17. import java.util.Collections;
  18. import java.util.HashSet;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Set;
  22. import java.util.TreeSet;
  23. import java.util.concurrent.locks.Lock;
  24. import java.util.concurrent.locks.ReentrantLock;
  25. import java.util.stream.Collectors;

  26. import org.eclipse.jgit.annotations.NonNull;
  27. import org.eclipse.jgit.events.RefsChangedEvent;
  28. import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
  29. import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate;
  30. import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase;
  31. import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
  32. import org.eclipse.jgit.lib.BatchRefUpdate;
  33. import org.eclipse.jgit.lib.Constants;
  34. import org.eclipse.jgit.lib.ObjectId;
  35. import org.eclipse.jgit.lib.ObjectIdRef;
  36. import org.eclipse.jgit.lib.PersonIdent;
  37. import org.eclipse.jgit.lib.Ref;
  38. import org.eclipse.jgit.lib.RefDatabase;
  39. import org.eclipse.jgit.lib.RefRename;
  40. import org.eclipse.jgit.lib.RefUpdate;
  41. import org.eclipse.jgit.lib.ReflogEntry;
  42. import org.eclipse.jgit.lib.ReflogReader;
  43. import org.eclipse.jgit.lib.Repository;
  44. import org.eclipse.jgit.lib.SymbolicRef;
  45. import org.eclipse.jgit.revwalk.RevObject;
  46. import org.eclipse.jgit.revwalk.RevTag;
  47. import org.eclipse.jgit.revwalk.RevWalk;
  48. import org.eclipse.jgit.transport.ReceiveCommand;
  49. import org.eclipse.jgit.util.FileUtils;
  50. import org.eclipse.jgit.util.RefList;
  51. import org.eclipse.jgit.util.RefMap;

  52. /**
  53.  * Implements RefDatabase using reftable for storage.
  54.  *
  55.  * This class is threadsafe.
  56.  */
  57. public class FileReftableDatabase extends RefDatabase {
  58.     private final ReftableDatabase reftableDatabase;

  59.     private final FileRepository fileRepository;

  60.     private final FileReftableStack reftableStack;

  61.     FileReftableDatabase(FileRepository repo) throws IOException {
  62.         this(repo, new File(new File(repo.getDirectory(), Constants.REFTABLE),
  63.                 Constants.TABLES_LIST));
  64.     }

  65.     FileReftableDatabase(FileRepository repo, File refstackName) throws IOException {
  66.         this.fileRepository = repo;
  67.         this.reftableStack = new FileReftableStack(refstackName,
  68.             new File(fileRepository.getDirectory(), Constants.REFTABLE),
  69.             () -> fileRepository.fireEvent(new RefsChangedEvent()),
  70.             () -> fileRepository.getConfig());
  71.         this.reftableDatabase = new ReftableDatabase() {

  72.             @Override
  73.             public MergedReftable openMergedReftable() throws IOException {
  74.                 return reftableStack.getMergedReftable();
  75.             }
  76.         };
  77.     }

  78.     ReflogReader getReflogReader(String refname) throws IOException {
  79.         return reftableDatabase.getReflogReader(refname);
  80.     }

  81.     /**
  82.      * @param repoDir
  83.      * @return whether the given repo uses reftable for refdb storage.
  84.      */
  85.     public static boolean isReftable(File repoDir) {
  86.         return new File(repoDir, Constants.REFTABLE).isDirectory();
  87.     }

  88.     /** {@inheritDoc} */
  89.     @Override
  90.     public boolean hasFastTipsWithSha1() throws IOException {
  91.         return reftableDatabase.hasFastTipsWithSha1();
  92.     }

  93.     /**
  94.      * Runs a full compaction for GC purposes.
  95.      * @throws IOException on I/O errors
  96.      */
  97.     public void compactFully() throws IOException {
  98.         Lock l = reftableDatabase.getLock();
  99.         l.lock();
  100.         try {
  101.             reftableStack.compactFully();
  102.             reftableDatabase.clearCache();
  103.         } finally {
  104.             l.unlock();
  105.         }
  106.     }

  107.     private ReentrantLock getLock() {
  108.         return reftableDatabase.getLock();
  109.     }

  110.     /** {@inheritDoc} */
  111.     @Override
  112.     public boolean performsAtomicTransactions() {
  113.         return true;
  114.     }

  115.     /** {@inheritDoc} */
  116.     @NonNull
  117.     @Override
  118.     public BatchRefUpdate newBatchUpdate() {
  119.         return new FileReftableBatchRefUpdate(this, fileRepository);
  120.     }

  121.     /** {@inheritDoc} */
  122.     @Override
  123.     public RefUpdate newUpdate(String refName, boolean detach)
  124.             throws IOException {
  125.         boolean detachingSymbolicRef = false;
  126.         Ref ref = exactRef(refName);

  127.         if (ref == null) {
  128.             ref = new ObjectIdRef.Unpeeled(NEW, refName, null);
  129.         } else {
  130.             detachingSymbolicRef = detach && ref.isSymbolic();
  131.         }

  132.         RefUpdate update = new FileReftableRefUpdate(ref);
  133.         if (detachingSymbolicRef) {
  134.             update.setDetachingSymbolicRef();
  135.         }
  136.         return update;
  137.     }

  138.     /** {@inheritDoc} */
  139.     @Override
  140.     public Ref exactRef(String name) throws IOException {
  141.         return reftableDatabase.exactRef(name);
  142.     }

  143.     /** {@inheritDoc} */
  144.     @Override
  145.     public List<Ref> getRefs() throws IOException {
  146.         return super.getRefs();
  147.     }

  148.     /** {@inheritDoc} */
  149.     @Override
  150.     public Map<String, Ref> getRefs(String prefix) throws IOException {
  151.         List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix);
  152.         RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size());
  153.         for (Ref r : refs) {
  154.             builder.add(r);
  155.         }
  156.         return new RefMap(prefix, builder.toRefList(), RefList.emptyList(),
  157.                 RefList.emptyList());
  158.     }

  159.     /** {@inheritDoc} */
  160.     @Override
  161.     public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
  162.             throws IOException {
  163.         return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes);
  164.     }

  165.     /** {@inheritDoc} */
  166.     @Override
  167.     public List<Ref> getAdditionalRefs() throws IOException {
  168.         return Collections.emptyList();
  169.     }

  170.     /** {@inheritDoc} */
  171.     @Override
  172.     public Ref peel(Ref ref) throws IOException {
  173.         Ref oldLeaf = ref.getLeaf();
  174.         if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) {
  175.             return ref;
  176.         }
  177.         return recreate(ref, doPeel(oldLeaf), hasVersioning());

  178.     }

  179.     private Ref doPeel(Ref leaf) throws IOException {
  180.         try (RevWalk rw = new RevWalk(fileRepository)) {
  181.             RevObject obj = rw.parseAny(leaf.getObjectId());
  182.             if (obj instanceof RevTag) {
  183.                 return new ObjectIdRef.PeeledTag(leaf.getStorage(),
  184.                         leaf.getName(), leaf.getObjectId(), rw.peel(obj).copy(),
  185.                         hasVersioning() ? leaf.getUpdateIndex()
  186.                                 : UNDEFINED_UPDATE_INDEX);
  187.             }
  188.             return new ObjectIdRef.PeeledNonTag(leaf.getStorage(),
  189.                     leaf.getName(), leaf.getObjectId(),
  190.                     hasVersioning() ? leaf.getUpdateIndex()
  191.                             : UNDEFINED_UPDATE_INDEX);

  192.         }
  193.     }

  194.     private static Ref recreate(Ref old, Ref leaf, boolean hasVersioning) {
  195.         if (old.isSymbolic()) {
  196.             Ref dst = recreate(old.getTarget(), leaf, hasVersioning);
  197.             return new SymbolicRef(old.getName(), dst,
  198.                     hasVersioning ? old.getUpdateIndex()
  199.                             : UNDEFINED_UPDATE_INDEX);
  200.         }
  201.         return leaf;
  202.     }

  203.     private class FileRefRename extends RefRename {
  204.         FileRefRename(RefUpdate src, RefUpdate dst) {
  205.             super(src, dst);
  206.         }

  207.         void writeRename(ReftableWriter w) throws IOException {
  208.             long idx = reftableDatabase.nextUpdateIndex();
  209.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin();
  210.             List<Ref> refs = new ArrayList<>(3);

  211.             Ref dest = destination.getRef();
  212.             Ref head = exactRef(Constants.HEAD);
  213.             if (head != null && head.isSymbolic()
  214.                     && head.getLeaf().getName().equals(source.getName())) {
  215.                 head = new SymbolicRef(Constants.HEAD, dest, idx);
  216.                 refs.add(head);
  217.             }

  218.             ObjectId objId = source.getRef().getObjectId();

  219.             // XXX should we check if the source is a Tag vs. NonTag?
  220.             refs.add(new ObjectIdRef.PeeledNonTag(Ref.Storage.NEW,
  221.                     destination.getName(), objId));
  222.             refs.add(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, source.getName(),
  223.                     null));

  224.             w.sortAndWriteRefs(refs);
  225.             PersonIdent who = destination.getRefLogIdent();
  226.             if (who == null) {
  227.                 who = new PersonIdent(fileRepository);
  228.             }

  229.             if (!destination.getRefLogMessage().isEmpty()) {
  230.                 List<String> refnames = refs.stream().map(r -> r.getName())
  231.                         .collect(Collectors.toList());
  232.                 Collections.sort(refnames);
  233.                 for (String s : refnames) {
  234.                     ObjectId old = (Constants.HEAD.equals(s)
  235.                             || s.equals(source.getName())) ? objId
  236.                                     : ObjectId.zeroId();
  237.                     ObjectId newId = (Constants.HEAD.equals(s)
  238.                             || s.equals(destination.getName())) ? objId
  239.                                     : ObjectId.zeroId();

  240.                     w.writeLog(s, idx, who, old, newId,
  241.                             destination.getRefLogMessage());
  242.                 }
  243.             }
  244.         }

  245.         @Override
  246.         protected RefUpdate.Result doRename() throws IOException {
  247.             Ref src = exactRef(source.getName());
  248.             if (exactRef(destination.getName()) != null || src == null
  249.                     || !source.getOldObjectId().equals(src.getObjectId())) {
  250.                 return RefUpdate.Result.LOCK_FAILURE;
  251.             }

  252.             if (src.isSymbolic()) {
  253.                 // We could support this, but this is easier and compatible.
  254.                 return RefUpdate.Result.IO_FAILURE;
  255.             }

  256.             if (!addReftable(this::writeRename)) {
  257.                 return RefUpdate.Result.LOCK_FAILURE;
  258.             }

  259.             return RefUpdate.Result.RENAMED;
  260.         }
  261.     }

  262.     /** {@inheritDoc} */
  263.     @Override
  264.     public RefRename newRename(String fromName, String toName)
  265.             throws IOException {
  266.         RefUpdate src = newUpdate(fromName, true);
  267.         RefUpdate dst = newUpdate(toName, true);
  268.         return new FileRefRename(src, dst);
  269.     }

  270.     /** {@inheritDoc} */
  271.     @Override
  272.     public boolean isNameConflicting(String name) throws IOException {
  273.         return reftableDatabase.isNameConflicting(name, new TreeSet<>(),
  274.                 new HashSet<>());
  275.     }

  276.     /** {@inheritDoc} */
  277.     @Override
  278.     public void close() {
  279.         reftableStack.close();
  280.     }

  281.     /** {@inheritDoc} */
  282.     @Override
  283.     public void create() throws IOException {
  284.         FileUtils.mkdir(
  285.                 new File(fileRepository.getDirectory(), Constants.REFTABLE),
  286.                 true);
  287.     }

  288.     private boolean addReftable(FileReftableStack.Writer w) throws IOException {
  289.         if (!reftableStack.addReftable(w)) {
  290.             reftableStack.reload();
  291.             reftableDatabase.clearCache();
  292.             return false;
  293.         }
  294.         reftableDatabase.clearCache();

  295.         return true;
  296.     }

  297.     private class FileReftableBatchRefUpdate extends ReftableBatchRefUpdate {
  298.         FileReftableBatchRefUpdate(FileReftableDatabase db,
  299.                 Repository repository) {
  300.             super(db, db.reftableDatabase, db.getLock(), repository);
  301.         }

  302.         @Override
  303.         protected void applyUpdates(List<Ref> newRefs,
  304.                 List<ReceiveCommand> pending) throws IOException {
  305.             if (!addReftable(rw -> write(rw, newRefs, pending))) {
  306.                 for (ReceiveCommand c : pending) {
  307.                     if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
  308.                         c.setResult(RefUpdate.Result.LOCK_FAILURE);
  309.                     }
  310.                 }
  311.             }
  312.         }
  313.     }

  314.     private class FileReftableRefUpdate extends RefUpdate {
  315.         FileReftableRefUpdate(Ref ref) {
  316.             super(ref);
  317.         }

  318.         @Override
  319.         protected RefDatabase getRefDatabase() {
  320.             return FileReftableDatabase.this;
  321.         }

  322.         @Override
  323.         protected Repository getRepository() {
  324.             return FileReftableDatabase.this.fileRepository;
  325.         }

  326.         @Override
  327.         protected void unlock() {
  328.             // nop.
  329.         }

  330.         private RevWalk rw;

  331.         private Ref dstRef;

  332.         @Override
  333.         public Result update(RevWalk walk) throws IOException {
  334.             try {
  335.                 rw = walk;
  336.                 return super.update(walk);
  337.             } finally {
  338.                 rw = null;
  339.             }
  340.         }

  341.         @Override
  342.         protected boolean tryLock(boolean deref) throws IOException {
  343.             dstRef = getRef();
  344.             if (deref) {
  345.                 dstRef = dstRef.getLeaf();
  346.             }

  347.             Ref derefed = exactRef(dstRef.getName());
  348.             if (derefed != null) {
  349.                 setOldObjectId(derefed.getObjectId());
  350.             }

  351.             return true;
  352.         }

  353.         void writeUpdate(ReftableWriter w) throws IOException {
  354.             Ref newRef = null;
  355.             if (rw != null && !ObjectId.zeroId().equals(getNewObjectId())) {
  356.                 RevObject obj = rw.parseAny(getNewObjectId());
  357.                 if (obj instanceof RevTag) {
  358.                     newRef = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED,
  359.                             dstRef.getName(), getNewObjectId(),
  360.                             rw.peel(obj).copy());
  361.                 }
  362.             }
  363.             if (newRef == null) {
  364.                 newRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED,
  365.                         dstRef.getName(), getNewObjectId());
  366.             }

  367.             long idx = reftableDatabase.nextUpdateIndex();
  368.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  369.                     .writeRef(newRef);

  370.             ObjectId oldId = getOldObjectId();
  371.             if (oldId == null) {
  372.                 oldId = ObjectId.zeroId();
  373.             }
  374.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
  375.                     getNewObjectId(), getRefLogMessage());
  376.         }

  377.         @Override
  378.         public PersonIdent getRefLogIdent() {
  379.             PersonIdent who = super.getRefLogIdent();
  380.             if (who == null) {
  381.                 who = new PersonIdent(getRepository());
  382.             }
  383.             return who;
  384.         }

  385.         void writeDelete(ReftableWriter w) throws IOException {
  386.             Ref newRef = new ObjectIdRef.Unpeeled(Ref.Storage.NEW,
  387.                     dstRef.getName(), null);
  388.             long idx = reftableDatabase.nextUpdateIndex();
  389.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  390.                     .writeRef(newRef);

  391.             ObjectId oldId = ObjectId.zeroId();
  392.             Ref old = exactRef(dstRef.getName());
  393.             if (old != null) {
  394.                 old = old.getLeaf();
  395.                 if (old.getObjectId() != null) {
  396.                     oldId = old.getObjectId();
  397.                 }
  398.             }

  399.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
  400.                     ObjectId.zeroId(), getRefLogMessage());
  401.         }

  402.         @Override
  403.         protected Result doUpdate(Result desiredResult) throws IOException {
  404.             if (isRefLogIncludingResult()) {
  405.                 setRefLogMessage(
  406.                         getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
  407.                         false);
  408.             }

  409.             if (!addReftable(this::writeUpdate)) {
  410.                 return Result.LOCK_FAILURE;
  411.             }

  412.             return desiredResult;
  413.         }

  414.         @Override
  415.         protected Result doDelete(Result desiredResult) throws IOException {

  416.             if (isRefLogIncludingResult()) {
  417.                 setRefLogMessage(
  418.                         getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
  419.                         false);
  420.             }

  421.             if (!addReftable(this::writeDelete)) {
  422.                 return Result.LOCK_FAILURE;
  423.             }

  424.             return desiredResult;
  425.         }

  426.         void writeLink(ReftableWriter w) throws IOException {
  427.             long idx = reftableDatabase.nextUpdateIndex();
  428.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  429.                     .writeRef(dstRef);

  430.             ObjectId beforeId = ObjectId.zeroId();
  431.             Ref before = exactRef(dstRef.getName());
  432.             if (before != null) {
  433.                 before = before.getLeaf();
  434.                 if (before.getObjectId() != null) {
  435.                     beforeId = before.getObjectId();
  436.                 }
  437.             }

  438.             Ref after = dstRef.getLeaf();
  439.             ObjectId afterId = ObjectId.zeroId();
  440.             if (after.getObjectId() != null) {
  441.                 afterId = after.getObjectId();
  442.             }

  443.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), beforeId,
  444.                     afterId, getRefLogMessage());
  445.         }

  446.         @Override
  447.         protected Result doLink(String target) throws IOException {
  448.             if (isRefLogIncludingResult()) {
  449.                 setRefLogMessage(
  450.                         getRefLogMessage() + ": " + Result.FORCED.toString(), //$NON-NLS-1$
  451.                         false);
  452.             }

  453.             boolean exists = exactRef(getName()) != null;
  454.             dstRef = new SymbolicRef(getName(),
  455.                     new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null),
  456.                     reftableDatabase.nextUpdateIndex());

  457.             if (!addReftable(this::writeLink)) {
  458.                 return Result.LOCK_FAILURE;
  459.             }
  460.             // XXX unclear if we should support FORCED here. Baseclass says
  461.             // NEW is OK ?
  462.             return exists ? Result.FORCED : Result.NEW;
  463.         }
  464.     }

  465.     private static void writeConvertTable(Repository repo, ReftableWriter w,
  466.             boolean writeLogs) throws IOException {
  467.         int size = 0;
  468.         List<Ref> refs = repo.getRefDatabase().getRefs();
  469.         if (writeLogs) {
  470.             for (Ref r : refs) {
  471.                 ReflogReader rlr = repo.getReflogReader(r.getName());
  472.                 if (rlr != null) {
  473.                     size = Math.max(rlr.getReverseEntries().size(), size);
  474.                 }
  475.             }
  476.         }
  477.         // We must use 1 here, nextUpdateIndex() on the empty stack is 1.
  478.         w.setMinUpdateIndex(1).setMaxUpdateIndex(size + 1).begin();

  479.         // The spec says to write the logs in the first table, and put refs in a
  480.         // separate table, but this complicates the compaction (when we can we drop
  481.         // deletions? Can we compact the .log table and the .ref table together?)
  482.         try (RevWalk rw = new RevWalk(repo)) {
  483.             List<Ref> toWrite = new ArrayList<>(refs.size());
  484.             for (Ref r : refs) {
  485.                 toWrite.add(refForWrite(rw, r));
  486.             }
  487.             w.sortAndWriteRefs(toWrite);
  488.         }

  489.         if (writeLogs) {
  490.             for (Ref r : refs) {
  491.                 long idx = size;
  492.                 ReflogReader reader = repo.getReflogReader(r.getName());
  493.                 if (reader == null) {
  494.                     continue;
  495.                 }
  496.                 for (ReflogEntry e : reader.getReverseEntries()) {
  497.                     w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(),
  498.                             e.getNewId(), e.getComment());
  499.                     idx--;
  500.                 }
  501.             }
  502.         }
  503.     }

  504.     private static Ref refForWrite(RevWalk rw, Ref r) throws IOException {
  505.         if (r.isSymbolic()) {
  506.             return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW,
  507.                     r.getTarget().getName(), null));
  508.         }
  509.         ObjectId newId = r.getObjectId();
  510.         RevObject obj = rw.parseAny(newId);
  511.         RevObject peel = null;
  512.         if (obj instanceof RevTag) {
  513.             peel = rw.peel(obj);
  514.         }
  515.         if (peel != null) {
  516.             return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId,
  517.                     peel.copy());
  518.         }
  519.         return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId);
  520.     }

  521.     /**
  522.      * @param repo
  523.      *            the repository
  524.      * @param writeLogs
  525.      *            whether to write reflogs
  526.      * @return a reftable based RefDB from an existing repository.
  527.      * @throws IOException
  528.      *             on IO error
  529.      */
  530.     public static FileReftableDatabase convertFrom(FileRepository repo,
  531.             boolean writeLogs) throws IOException {
  532.         FileReftableDatabase newDb = null;
  533.         File reftableList = null;
  534.         try {
  535.             File reftableDir = new File(repo.getDirectory(),
  536.                     Constants.REFTABLE);
  537.             reftableList = new File(reftableDir, Constants.TABLES_LIST);
  538.             if (!reftableDir.isDirectory()) {
  539.                 reftableDir.mkdir();
  540.             }

  541.             try (FileReftableStack stack = new FileReftableStack(reftableList,
  542.                     reftableDir, null, () -> repo.getConfig())) {
  543.                 stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs));
  544.             }
  545.             reftableList = null;
  546.         } finally {
  547.             if (reftableList != null) {
  548.                 reftableList.delete();
  549.             }
  550.         }
  551.         return newDb;
  552.     }
  553. }