ReftableBatchRefUpdate.java

  1. /*
  2.  * Copyright (C) 2019, Google Inc. 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.reftable;

  11. import org.eclipse.jgit.annotations.Nullable;
  12. import org.eclipse.jgit.errors.MissingObjectException;
  13. import org.eclipse.jgit.internal.JGitText;
  14. import org.eclipse.jgit.lib.AnyObjectId;
  15. import org.eclipse.jgit.lib.BatchRefUpdate;
  16. import org.eclipse.jgit.lib.ObjectId;
  17. import org.eclipse.jgit.lib.ObjectIdRef;
  18. import org.eclipse.jgit.lib.PersonIdent;
  19. import org.eclipse.jgit.lib.ProgressMonitor;
  20. import org.eclipse.jgit.lib.Ref;
  21. import org.eclipse.jgit.lib.RefDatabase;
  22. import org.eclipse.jgit.lib.ReflogEntry;
  23. import org.eclipse.jgit.lib.Repository;
  24. import org.eclipse.jgit.lib.SymbolicRef;
  25. import org.eclipse.jgit.revwalk.RevObject;
  26. import org.eclipse.jgit.revwalk.RevTag;
  27. import org.eclipse.jgit.revwalk.RevWalk;
  28. import org.eclipse.jgit.transport.ReceiveCommand;

  29. import java.io.IOException;
  30. import java.util.ArrayList;
  31. import java.util.Collections;
  32. import java.util.HashMap;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Set;
  36. import java.util.TreeSet;
  37. import java.util.concurrent.locks.Lock;
  38. import java.util.stream.Collectors;

  39. import static org.eclipse.jgit.lib.Ref.Storage.NEW;
  40. import static org.eclipse.jgit.lib.Ref.Storage.PACKED;

  41. import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
  42. import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
  43. import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
  44. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
  45. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
  46. import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
  47. import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;

  48. /**
  49.  * {@link org.eclipse.jgit.lib.BatchRefUpdate} for Reftable based RefDatabase.
  50.  */
  51. public abstract class ReftableBatchRefUpdate extends BatchRefUpdate {
  52.     private final Lock lock;

  53.     private final ReftableDatabase refDb;

  54.     private final Repository repository;

  55.     /**
  56.      * Initialize.
  57.      *
  58.      * @param refdb
  59.      *            The RefDatabase
  60.      * @param reftableDb
  61.      *            The ReftableDatabase
  62.      * @param lock
  63.      *            A lock protecting the refdatabase's state
  64.      * @param repository
  65.      *            The repository on which this update will run
  66.      */
  67.     protected ReftableBatchRefUpdate(RefDatabase refdb, ReftableDatabase reftableDb, Lock lock,
  68.             Repository repository) {
  69.         super(refdb);
  70.         this.refDb = reftableDb;
  71.         this.lock = lock;
  72.         this.repository = repository;
  73.     }

  74.     /** {@inheritDoc} */
  75.     @Override
  76.     public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
  77.         List<ReceiveCommand> pending = getPending();
  78.         if (pending.isEmpty()) {
  79.             return;
  80.         }
  81.         if (options != null) {
  82.             setPushOptions(options);
  83.         }
  84.         try {
  85.             if (!checkObjectExistence(rw, pending)) {
  86.                 return;
  87.             }
  88.             // if we are here, checkObjectExistence might have flagged some problems
  89.             // but the transaction is not atomic, so we should proceed with the other
  90.             // pending commands.
  91.             pending = getPending();
  92.             if (!checkNonFastForwards(rw, pending)) {
  93.                 return;
  94.             }
  95.             pending = getPending();

  96.             lock.lock();
  97.             try {
  98.                 if (!checkExpected(pending)) {
  99.                     return;
  100.                 }
  101.                 pending = getPending();
  102.                 if (!checkConflicting(pending)) {
  103.                     return;
  104.                 }
  105.                 pending = getPending();
  106.                 if (!blockUntilTimestamps(MAX_WAIT)) {
  107.                     return;
  108.                 }

  109.                 List<Ref> newRefs = toNewRefs(rw, pending);
  110.                 applyUpdates(newRefs, pending);
  111.                 for (ReceiveCommand cmd : pending) {
  112.                     if (cmd.getResult() == NOT_ATTEMPTED) {
  113.                         // XXX this is a bug in DFS ?
  114.                         cmd.setResult(OK);
  115.                     }
  116.                 }
  117.             } finally {
  118.                 lock.unlock();
  119.             }
  120.         } catch (IOException e) {
  121.             pending.get(0).setResult(LOCK_FAILURE, "io error"); //$NON-NLS-1$
  122.             ReceiveCommand.abort(pending);
  123.         }
  124.     }

  125.     /**
  126.      * Implements the storage-specific part of the update.
  127.      *
  128.      * @param newRefs
  129.      *            the new refs to create
  130.      * @param pending
  131.      *            the pending receive commands to be executed
  132.      * @throws IOException
  133.      *             if any of the writes fail.
  134.      */
  135.     protected abstract void applyUpdates(List<Ref> newRefs,
  136.             List<ReceiveCommand> pending) throws IOException;

  137.     private List<ReceiveCommand> getPending() {
  138.         return ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
  139.     }

  140.     private boolean checkObjectExistence(RevWalk rw,
  141.             List<ReceiveCommand> pending) throws IOException {
  142.         for (ReceiveCommand cmd : pending) {
  143.             try {
  144.                 if (!cmd.getNewId().equals(ObjectId.zeroId())) {
  145.                     rw.parseAny(cmd.getNewId());
  146.                 }
  147.             } catch (MissingObjectException e) {
  148.                 // ReceiveCommand#setResult(Result) converts REJECTED to
  149.                 // REJECTED_NONFASTFORWARD, even though that result is also
  150.                 // used for a missing object. Eagerly handle this case so we
  151.                 // can set the right result.
  152.                 cmd.setResult(REJECTED_MISSING_OBJECT);
  153.                 if (isAtomic()) {
  154.                     ReceiveCommand.abort(pending);
  155.                     return false;
  156.                 }
  157.             }
  158.         }
  159.         return true;
  160.     }

  161.     private boolean checkNonFastForwards(RevWalk rw,
  162.             List<ReceiveCommand> pending) throws IOException {
  163.         if (isAllowNonFastForwards()) {
  164.             return true;
  165.         }
  166.         for (ReceiveCommand cmd : pending) {
  167.             cmd.updateType(rw);
  168.             if (cmd.getType() == UPDATE_NONFASTFORWARD) {
  169.                 cmd.setResult(REJECTED_NONFASTFORWARD);
  170.                 if (isAtomic()) {
  171.                     ReceiveCommand.abort(pending);
  172.                     return false;
  173.                 }
  174.             }
  175.         }
  176.         return true;
  177.     }

  178.     private boolean checkConflicting(List<ReceiveCommand> pending)
  179.             throws IOException {
  180.         TreeSet<String> added = new TreeSet<>();
  181.         Set<String> deleted =
  182.                 pending.stream()
  183.                         .filter(cmd -> cmd.getType() == DELETE)
  184.                         .map(c -> c.getRefName())
  185.                         .collect(Collectors.toSet());

  186.         boolean ok = true;
  187.         for (ReceiveCommand cmd : pending) {
  188.             if (cmd.getType() == DELETE) {
  189.                 continue;
  190.             }

  191.             String name = cmd.getRefName();
  192.             if (refDb.isNameConflicting(name, added, deleted)) {
  193.                 if (isAtomic()) {
  194.                     cmd.setResult(
  195.                             ReceiveCommand.Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted);
  196.                 } else {
  197.                     cmd.setResult(LOCK_FAILURE);
  198.                 }

  199.                 ok = false;
  200.             }
  201.             added.add(name);
  202.         }

  203.         if (isAtomic()) {
  204.             if (!ok) {
  205.                 pending.stream()
  206.                         .filter(cmd -> cmd.getResult() == NOT_ATTEMPTED)
  207.                         .forEach(cmd -> cmd.setResult(LOCK_FAILURE));
  208.             }
  209.             return ok;
  210.         }

  211.         for (ReceiveCommand cmd : pending) {
  212.             if (cmd.getResult() == NOT_ATTEMPTED) {
  213.                 return true;
  214.             }
  215.         }

  216.         return false;
  217.     }

  218.     private boolean checkExpected(List<ReceiveCommand> pending)
  219.             throws IOException {
  220.         for (ReceiveCommand cmd : pending) {
  221.             if (!matchOld(cmd, refDb.exactRef(cmd.getRefName()))) {
  222.                 cmd.setResult(LOCK_FAILURE);
  223.                 if (isAtomic()) {
  224.                     ReceiveCommand.abort(pending);
  225.                     return false;
  226.                 }
  227.             }
  228.         }
  229.         return true;
  230.     }

  231.     private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) {
  232.         if (ref == null) {
  233.             return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId())
  234.                 && cmd.getOldSymref() == null;
  235.         } else if (ref.isSymbolic()) {
  236.             return ref.getTarget().getName().equals(cmd.getOldSymref());
  237.         }
  238.         ObjectId id = ref.getObjectId();
  239.         if (id == null) {
  240.             id = ObjectId.zeroId();
  241.         }
  242.         return cmd.getOldId().equals(id);
  243.     }

  244.     /**
  245.      * Writes the refs to the writer, and calls finish.
  246.      *
  247.      * @param writer
  248.      *            the writer on which we should write.
  249.      * @param newRefs
  250.      *            the ref data to write..
  251.      * @param pending
  252.      *            the log data to write.
  253.      * @throws IOException
  254.      *            in case of problems.
  255.      */
  256.     protected void write(ReftableWriter writer, List<Ref> newRefs,
  257.             List<ReceiveCommand> pending) throws IOException {
  258.         long updateIndex = refDb.nextUpdateIndex();
  259.         writer.setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex)
  260.                 .begin().sortAndWriteRefs(newRefs);
  261.         if (!isRefLogDisabled()) {
  262.             writeLog(writer, updateIndex, pending);
  263.         }
  264.     }

  265.     private void writeLog(ReftableWriter writer, long updateIndex,
  266.             List<ReceiveCommand> pending) throws IOException {
  267.         Map<String, ReceiveCommand> cmds = new HashMap<>();
  268.         List<String> byName = new ArrayList<>(pending.size());
  269.         for (ReceiveCommand cmd : pending) {
  270.             cmds.put(cmd.getRefName(), cmd);
  271.             byName.add(cmd.getRefName());
  272.         }
  273.         Collections.sort(byName);

  274.         PersonIdent ident = getRefLogIdent();
  275.         if (ident == null) {
  276.             ident = new PersonIdent(repository);
  277.         }
  278.         for (String name : byName) {
  279.             ReceiveCommand cmd = cmds.get(name);
  280.             if (isRefLogDisabled(cmd)) {
  281.                 continue;
  282.             }
  283.             String msg = getRefLogMessage(cmd);
  284.             if (isRefLogIncludingResult(cmd)) {
  285.                 String strResult = toResultString(cmd);
  286.                 if (strResult != null) {
  287.                     msg = msg.isEmpty() ? strResult : msg + ": " + strResult; //$NON-NLS-1$
  288.                 }
  289.             }
  290.             writer.writeLog(name, updateIndex, ident, cmd.getOldId(),
  291.                     cmd.getNewId(), msg);
  292.         }
  293.     }

  294.     private String toResultString(ReceiveCommand cmd) {
  295.         switch (cmd.getType()) {
  296.         case CREATE:
  297.             return ReflogEntry.PREFIX_CREATED;
  298.         case UPDATE:
  299.             // Match the behavior of a single RefUpdate. In that case, setting
  300.             // the force bit completely bypasses the potentially expensive
  301.             // isMergedInto check, by design, so the reflog message may be
  302.             // inaccurate.
  303.             //
  304.             // Similarly, this class bypasses the isMergedInto checks when the
  305.             // force bit is set, meaning we can't actually distinguish between
  306.             // UPDATE and UPDATE_NONFASTFORWARD when isAllowNonFastForwards()
  307.             // returns true.
  308.             return isAllowNonFastForwards() ? ReflogEntry.PREFIX_FORCED_UPDATE
  309.                     : ReflogEntry.PREFIX_FAST_FORWARD;
  310.         case UPDATE_NONFASTFORWARD:
  311.             return ReflogEntry.PREFIX_FORCED_UPDATE;
  312.         default:
  313.             return null;
  314.         }
  315.     }

  316.     // Extracts and peels the refs out of the ReceiveCommands
  317.     private static List<Ref> toNewRefs(RevWalk rw, List<ReceiveCommand> pending)
  318.         throws IOException {
  319.         List<Ref> refs = new ArrayList<>(pending.size());
  320.         for (ReceiveCommand cmd : pending) {
  321.             if (cmd.getResult() != NOT_ATTEMPTED) {
  322.                 continue;
  323.             }

  324.             String name = cmd.getRefName();
  325.             ObjectId newId = cmd.getNewId();
  326.             String newSymref = cmd.getNewSymref();
  327.             if (AnyObjectId.isEqual(ObjectId.zeroId(), newId)
  328.                 && newSymref == null) {
  329.                 refs.add(new ObjectIdRef.Unpeeled(NEW, name, null));
  330.                 continue;
  331.             } else if (newSymref != null) {
  332.                 refs.add(new SymbolicRef(name,
  333.                     new ObjectIdRef.Unpeeled(NEW, newSymref, null)));
  334.                 continue;
  335.             }

  336.             RevObject obj = rw.parseAny(newId);
  337.             RevObject peel = null;
  338.             if (obj instanceof RevTag) {
  339.                 peel = rw.peel(obj);
  340.             }
  341.             if (peel != null) {
  342.                 refs.add(new ObjectIdRef.PeeledTag(PACKED, name, newId,
  343.                     peel.copy()));
  344.             } else {
  345.                 refs.add(new ObjectIdRef.PeeledNonTag(PACKED, name, newId));
  346.             }
  347.         }
  348.         return refs;
  349.     }
  350. }