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

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

  60.     private final FileRepository fileRepository;

  61.     private final FileReftableStack reftableStack;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  179.     }

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

  193.         }
  194.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  296.         return true;
  297.     }

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

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

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

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

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

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

  331.         private RevWalk rw;

  332.         private Ref dstRef;

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

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

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

  352.             return true;
  353.         }

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

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

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

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

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

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

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

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

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

  413.             return desiredResult;
  414.         }

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

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

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

  425.             return desiredResult;
  426.         }

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

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

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

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

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

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

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

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

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

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

  505.     private static Ref refForWrite(RevWalk rw, Ref r) throws IOException {
  506.         if (r.isSymbolic()) {
  507.             return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW,
  508.                     r.getTarget().getName(), null));
  509.         }
  510.         ObjectId newId = r.getObjectId();
  511.         RevObject peel = null;
  512.         try {
  513.             RevObject obj = rw.parseAny(newId);
  514.             if (obj instanceof RevTag) {
  515.                 peel = rw.peel(obj);
  516.             }
  517.         } catch (MissingObjectException e) {
  518.             /* ignore this error and copy the dangling object ID into reftable too. */
  519.         }
  520.         if (peel != null) {
  521.                 return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId,
  522.                         peel.copy());
  523.             }

  524.         return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId);
  525.     }

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

  546.             try (FileReftableStack stack = new FileReftableStack(reftableList,
  547.                     reftableDir, null, () -> repo.getConfig())) {
  548.                 stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs));
  549.             }
  550.             reftableList = null;
  551.         } finally {
  552.             if (reftableList != null) {
  553.                 reftableList.delete();
  554.             }
  555.         }
  556.         return newDb;
  557.     }
  558. }