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.TreeSet;
  22. import java.util.concurrent.locks.ReentrantLock;
  23. import java.util.stream.Collectors;

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

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

  57.     private final FileRepository fileRepository;

  58.     private final FileReftableStack reftableStack;

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

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

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

  76.     ReflogReader getReflogReader(String refname) throws IOException {
  77.         return reftableDatabase.getReflogReader(refname);
  78.     }

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

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

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

  104.     private ReentrantLock getLock() {
  105.         return reftableDatabase.getLock();
  106.     }

  107.     /** {@inheritDoc} */
  108.     @Override
  109.     public boolean performsAtomicTransactions() {
  110.         return true;
  111.     }

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

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

  124.         if (ref == null) {
  125.             ref = new ObjectIdRef.Unpeeled(NEW, refName, null);
  126.         } else {
  127.             detachingSymbolicRef = detach && ref.isSymbolic();
  128.         }

  129.         RefUpdate update = new FileReftableRefUpdate(ref);
  130.         if (detachingSymbolicRef) {
  131.             update.setDetachingSymbolicRef();
  132.         }
  133.         return update;
  134.     }

  135.     /** {@inheritDoc} */
  136.     @Override
  137.     public Ref exactRef(String name) throws IOException {
  138.         return reftableDatabase.exactRef(name);
  139.     }

  140.     /** {@inheritDoc} */
  141.     @Override
  142.     public List<Ref> getRefs() throws IOException {
  143.         return super.getRefs();
  144.     }

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

  156.     /** {@inheritDoc} */
  157.     @Override
  158.     public List<Ref> getAdditionalRefs() throws IOException {
  159.         return Collections.emptyList();
  160.     }

  161.     /** {@inheritDoc} */
  162.     @Override
  163.     public Ref peel(Ref ref) throws IOException {
  164.         Ref oldLeaf = ref.getLeaf();
  165.         if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) {
  166.             return ref;
  167.         }
  168.         return recreate(ref, doPeel(oldLeaf), hasVersioning());

  169.     }

  170.     private Ref doPeel(Ref leaf) throws IOException {
  171.         try (RevWalk rw = new RevWalk(fileRepository)) {
  172.             RevObject obj = rw.parseAny(leaf.getObjectId());
  173.             if (obj instanceof RevTag) {
  174.                 return new ObjectIdRef.PeeledTag(leaf.getStorage(),
  175.                         leaf.getName(), leaf.getObjectId(), rw.peel(obj).copy(),
  176.                         hasVersioning() ? leaf.getUpdateIndex()
  177.                                 : UNDEFINED_UPDATE_INDEX);
  178.             }
  179.             return new ObjectIdRef.PeeledNonTag(leaf.getStorage(),
  180.                     leaf.getName(), leaf.getObjectId(),
  181.                     hasVersioning() ? leaf.getUpdateIndex()
  182.                             : UNDEFINED_UPDATE_INDEX);

  183.         }
  184.     }

  185.     private static Ref recreate(Ref old, Ref leaf, boolean hasVersioning) {
  186.         if (old.isSymbolic()) {
  187.             Ref dst = recreate(old.getTarget(), leaf, hasVersioning);
  188.             return new SymbolicRef(old.getName(), dst,
  189.                     hasVersioning ? old.getUpdateIndex()
  190.                             : UNDEFINED_UPDATE_INDEX);
  191.         }
  192.         return leaf;
  193.     }

  194.     private class FileRefRename extends RefRename {
  195.         FileRefRename(RefUpdate src, RefUpdate dst) {
  196.             super(src, dst);
  197.         }

  198.         void writeRename(ReftableWriter w) throws IOException {
  199.             long idx = reftableDatabase.nextUpdateIndex();
  200.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin();
  201.             List<Ref> refs = new ArrayList<>(3);

  202.             Ref dest = destination.getRef();
  203.             Ref head = exactRef(Constants.HEAD);
  204.             if (head != null && head.isSymbolic()
  205.                     && head.getLeaf().getName().equals(source.getName())) {
  206.                 head = new SymbolicRef(Constants.HEAD, dest, idx);
  207.                 refs.add(head);
  208.             }

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

  210.             // XXX should we check if the source is a Tag vs. NonTag?
  211.             refs.add(new ObjectIdRef.PeeledNonTag(Ref.Storage.NEW,
  212.                     destination.getName(), objId));
  213.             refs.add(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, source.getName(),
  214.                     null));

  215.             w.sortAndWriteRefs(refs);
  216.             PersonIdent who = destination.getRefLogIdent();
  217.             if (who == null) {
  218.                 who = new PersonIdent(fileRepository);
  219.             }

  220.             if (!destination.getRefLogMessage().isEmpty()) {
  221.                 List<String> refnames = refs.stream().map(r -> r.getName())
  222.                         .collect(Collectors.toList());
  223.                 Collections.sort(refnames);
  224.                 for (String s : refnames) {
  225.                     ObjectId old = (Constants.HEAD.equals(s)
  226.                             || s.equals(source.getName())) ? objId
  227.                                     : ObjectId.zeroId();
  228.                     ObjectId newId = (Constants.HEAD.equals(s)
  229.                             || s.equals(destination.getName())) ? objId
  230.                                     : ObjectId.zeroId();

  231.                     w.writeLog(s, idx, who, old, newId,
  232.                             destination.getRefLogMessage());
  233.                 }
  234.             }
  235.         }

  236.         @Override
  237.         protected RefUpdate.Result doRename() throws IOException {
  238.             Ref src = exactRef(source.getName());
  239.             if (exactRef(destination.getName()) != null || src == null
  240.                     || !source.getOldObjectId().equals(src.getObjectId())) {
  241.                 return RefUpdate.Result.LOCK_FAILURE;
  242.             }

  243.             if (src.isSymbolic()) {
  244.                 // We could support this, but this is easier and compatible.
  245.                 return RefUpdate.Result.IO_FAILURE;
  246.             }

  247.             if (!addReftable(this::writeRename)) {
  248.                 return RefUpdate.Result.LOCK_FAILURE;
  249.             }

  250.             return RefUpdate.Result.RENAMED;
  251.         }
  252.     }

  253.     /** {@inheritDoc} */
  254.     @Override
  255.     public RefRename newRename(String fromName, String toName)
  256.             throws IOException {
  257.         RefUpdate src = newUpdate(fromName, true);
  258.         RefUpdate dst = newUpdate(toName, true);
  259.         return new FileRefRename(src, dst);
  260.     }

  261.     /** {@inheritDoc} */
  262.     @Override
  263.     public boolean isNameConflicting(String name) throws IOException {
  264.         return reftableDatabase.isNameConflicting(name, new TreeSet<>(),
  265.                 new HashSet<>());
  266.     }

  267.     /** {@inheritDoc} */
  268.     @Override
  269.     public void close() {
  270.         reftableStack.close();
  271.     }

  272.     /** {@inheritDoc} */
  273.     @Override
  274.     public void create() throws IOException {
  275.         FileUtils.mkdir(
  276.                 new File(fileRepository.getDirectory(), Constants.REFTABLE),
  277.                 true);
  278.     }

  279.     private boolean addReftable(FileReftableStack.Writer w) throws IOException {
  280.         if (!reftableStack.addReftable(w)) {
  281.             reftableStack.reload();
  282.             reftableDatabase.clearCache();
  283.             return false;
  284.         }
  285.         reftableDatabase.clearCache();

  286.         return true;
  287.     }

  288.     private class FileReftableBatchRefUpdate extends ReftableBatchRefUpdate {
  289.         FileReftableBatchRefUpdate(FileReftableDatabase db,
  290.                 Repository repository) {
  291.             super(db, db.reftableDatabase, db.getLock(), repository);
  292.         }

  293.         @Override
  294.         protected void applyUpdates(List<Ref> newRefs,
  295.                 List<ReceiveCommand> pending) throws IOException {
  296.             if (!addReftable(rw -> write(rw, newRefs, pending))) {
  297.                 for (ReceiveCommand c : pending) {
  298.                     if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
  299.                         c.setResult(RefUpdate.Result.LOCK_FAILURE);
  300.                     }
  301.                 }
  302.             }
  303.         }
  304.     }

  305.     private class FileReftableRefUpdate extends RefUpdate {
  306.         FileReftableRefUpdate(Ref ref) {
  307.             super(ref);
  308.         }

  309.         @Override
  310.         protected RefDatabase getRefDatabase() {
  311.             return FileReftableDatabase.this;
  312.         }

  313.         @Override
  314.         protected Repository getRepository() {
  315.             return FileReftableDatabase.this.fileRepository;
  316.         }

  317.         @Override
  318.         protected void unlock() {
  319.             // nop.
  320.         }

  321.         private RevWalk rw;

  322.         private Ref dstRef;

  323.         @Override
  324.         public Result update(RevWalk walk) throws IOException {
  325.             try {
  326.                 rw = walk;
  327.                 return super.update(walk);
  328.             } finally {
  329.                 rw = null;
  330.             }
  331.         }

  332.         @Override
  333.         protected boolean tryLock(boolean deref) throws IOException {
  334.             dstRef = getRef();
  335.             if (deref) {
  336.                 dstRef = dstRef.getLeaf();
  337.             }

  338.             Ref derefed = exactRef(dstRef.getName());
  339.             if (derefed != null) {
  340.                 setOldObjectId(derefed.getObjectId());
  341.             }

  342.             return true;
  343.         }

  344.         void writeUpdate(ReftableWriter w) throws IOException {
  345.             Ref newRef = null;
  346.             if (rw != null && !ObjectId.zeroId().equals(getNewObjectId())) {
  347.                 RevObject obj = rw.parseAny(getNewObjectId());
  348.                 if (obj instanceof RevTag) {
  349.                     newRef = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED,
  350.                             dstRef.getName(), getNewObjectId(),
  351.                             rw.peel(obj).copy());
  352.                 }
  353.             }
  354.             if (newRef == null) {
  355.                 newRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED,
  356.                         dstRef.getName(), getNewObjectId());
  357.             }

  358.             long idx = reftableDatabase.nextUpdateIndex();
  359.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  360.                     .writeRef(newRef);

  361.             ObjectId oldId = getOldObjectId();
  362.             if (oldId == null) {
  363.                 oldId = ObjectId.zeroId();
  364.             }
  365.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
  366.                     getNewObjectId(), getRefLogMessage());
  367.         }

  368.         @Override
  369.         public PersonIdent getRefLogIdent() {
  370.             PersonIdent who = super.getRefLogIdent();
  371.             if (who == null) {
  372.                 who = new PersonIdent(getRepository());
  373.             }
  374.             return who;
  375.         }

  376.         void writeDelete(ReftableWriter w) throws IOException {
  377.             Ref newRef = new ObjectIdRef.Unpeeled(Ref.Storage.NEW,
  378.                     dstRef.getName(), null);
  379.             long idx = reftableDatabase.nextUpdateIndex();
  380.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  381.                     .writeRef(newRef);

  382.             ObjectId oldId = ObjectId.zeroId();
  383.             Ref old = exactRef(dstRef.getName());
  384.             if (old != null) {
  385.                 old = old.getLeaf();
  386.                 if (old.getObjectId() != null) {
  387.                     oldId = old.getObjectId();
  388.                 }
  389.             }

  390.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId,
  391.                     ObjectId.zeroId(), getRefLogMessage());
  392.         }

  393.         @Override
  394.         protected Result doUpdate(Result desiredResult) throws IOException {
  395.             if (isRefLogIncludingResult()) {
  396.                 setRefLogMessage(
  397.                         getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
  398.                         false);
  399.             }

  400.             if (!addReftable(this::writeUpdate)) {
  401.                 return Result.LOCK_FAILURE;
  402.             }

  403.             return desiredResult;
  404.         }

  405.         @Override
  406.         protected Result doDelete(Result desiredResult) throws IOException {

  407.             if (isRefLogIncludingResult()) {
  408.                 setRefLogMessage(
  409.                         getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$
  410.                         false);
  411.             }

  412.             if (!addReftable(this::writeDelete)) {
  413.                 return Result.LOCK_FAILURE;
  414.             }

  415.             return desiredResult;
  416.         }

  417.         void writeLink(ReftableWriter w) throws IOException {
  418.             long idx = reftableDatabase.nextUpdateIndex();
  419.             w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin()
  420.                     .writeRef(dstRef);

  421.             ObjectId beforeId = ObjectId.zeroId();
  422.             Ref before = exactRef(dstRef.getName());
  423.             if (before != null) {
  424.                 before = before.getLeaf();
  425.                 if (before.getObjectId() != null) {
  426.                     beforeId = before.getObjectId();
  427.                 }
  428.             }

  429.             Ref after = dstRef.getLeaf();
  430.             ObjectId afterId = ObjectId.zeroId();
  431.             if (after.getObjectId() != null) {
  432.                 afterId = after.getObjectId();
  433.             }

  434.             w.writeLog(dstRef.getName(), idx, getRefLogIdent(), beforeId,
  435.                     afterId, getRefLogMessage());
  436.         }

  437.         @Override
  438.         protected Result doLink(String target) throws IOException {
  439.             if (isRefLogIncludingResult()) {
  440.                 setRefLogMessage(
  441.                         getRefLogMessage() + ": " + Result.FORCED.toString(), //$NON-NLS-1$
  442.                         false);
  443.             }

  444.             boolean exists = exactRef(getName()) != null;
  445.             dstRef = new SymbolicRef(getName(),
  446.                     new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null),
  447.                     reftableDatabase.nextUpdateIndex());

  448.             if (!addReftable(this::writeLink)) {
  449.                 return Result.LOCK_FAILURE;
  450.             }
  451.             // XXX unclear if we should support FORCED here. Baseclass says
  452.             // NEW is OK ?
  453.             return exists ? Result.FORCED : Result.NEW;
  454.         }
  455.     }

  456.     private static void writeConvertTable(Repository repo, ReftableWriter w,
  457.             boolean writeLogs) throws IOException {
  458.         int size = 0;
  459.         List<Ref> refs = repo.getRefDatabase().getRefs();
  460.         if (writeLogs) {
  461.             for (Ref r : refs) {
  462.                 ReflogReader rlr = repo.getReflogReader(r.getName());
  463.                 if (rlr != null) {
  464.                     size = Math.max(rlr.getReverseEntries().size(), size);
  465.                 }
  466.             }
  467.         }
  468.         // We must use 1 here, nextUpdateIndex() on the empty stack is 1.
  469.         w.setMinUpdateIndex(1).setMaxUpdateIndex(size + 1).begin();

  470.         // The spec says to write the logs in the first table, and put refs in a
  471.         // separate table, but this complicates the compaction (when we can we drop
  472.         // deletions? Can we compact the .log table and the .ref table together?)
  473.         try (RevWalk rw = new RevWalk(repo)) {
  474.             List<Ref> toWrite = new ArrayList<>(refs.size());
  475.             for (Ref r : refs) {
  476.                 toWrite.add(refForWrite(rw, r));
  477.             }
  478.             w.sortAndWriteRefs(toWrite);
  479.         }

  480.         if (writeLogs) {
  481.             for (Ref r : refs) {
  482.                 long idx = size;
  483.                 ReflogReader reader = repo.getReflogReader(r.getName());
  484.                 if (reader == null) {
  485.                     continue;
  486.                 }
  487.                 for (ReflogEntry e : reader.getReverseEntries()) {
  488.                     w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(),
  489.                             e.getNewId(), e.getComment());
  490.                     idx--;
  491.                 }
  492.             }
  493.         }
  494.     }

  495.     private static Ref refForWrite(RevWalk rw, Ref r) throws IOException {
  496.         if (r.isSymbolic()) {
  497.             return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW,
  498.                     r.getTarget().getName(), null));
  499.         }
  500.         ObjectId newId = r.getObjectId();
  501.         RevObject obj = rw.parseAny(newId);
  502.         RevObject peel = null;
  503.         if (obj instanceof RevTag) {
  504.             peel = rw.peel(obj);
  505.         }
  506.         if (peel != null) {
  507.             return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId,
  508.                     peel.copy());
  509.         }
  510.         return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId);
  511.     }

  512.     /**
  513.      * @param repo
  514.      *            the repository
  515.      * @param writeLogs
  516.      *            whether to write reflogs
  517.      * @return a reftable based RefDB from an existing repository.
  518.      * @throws IOException
  519.      *             on IO error
  520.      */
  521.     public static FileReftableDatabase convertFrom(FileRepository repo,
  522.             boolean writeLogs) throws IOException {
  523.         FileReftableDatabase newDb = null;
  524.         File reftableList = null;
  525.         try {
  526.             File reftableDir = new File(repo.getDirectory(),
  527.                     Constants.REFTABLE);
  528.             reftableList = new File(reftableDir, Constants.TABLES_LIST);
  529.             if (!reftableDir.isDirectory()) {
  530.                 reftableDir.mkdir();
  531.             }

  532.             try (FileReftableStack stack = new FileReftableStack(reftableList,
  533.                     reftableDir, null, () -> repo.getConfig())) {
  534.                 stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs));
  535.             }
  536.             reftableList = null;
  537.         } finally {
  538.             if (reftableList != null) {
  539.                 reftableList.delete();
  540.             }
  541.         }
  542.         return newDb;
  543.     }
  544. }