DirCacheEditor.java

  1. /*
  2.  * Copyright (C) 2008, 2009, Google Inc.
  3.  * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. package org.eclipse.jgit.dircache;

  12. import static org.eclipse.jgit.dircache.DirCache.cmp;
  13. import static org.eclipse.jgit.dircache.DirCacheTree.peq;
  14. import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;

  15. import java.io.IOException;
  16. import java.text.MessageFormat;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.Comparator;
  20. import java.util.List;

  21. import org.eclipse.jgit.internal.JGitText;
  22. import org.eclipse.jgit.lib.Constants;
  23. import org.eclipse.jgit.util.Paths;

  24. /**
  25.  * Updates a {@link org.eclipse.jgit.dircache.DirCache} by supplying discrete
  26.  * edit commands.
  27.  * <p>
  28.  * An editor updates a DirCache by taking a list of
  29.  * {@link org.eclipse.jgit.dircache.DirCacheEditor.PathEdit} commands and
  30.  * executing them against the entries of the destination cache to produce a new
  31.  * cache. This edit style allows applications to insert a few commands and then
  32.  * have the editor compute the proper entry indexes necessary to perform an
  33.  * efficient in-order update of the index records. This can be easier to use
  34.  * than {@link org.eclipse.jgit.dircache.DirCacheBuilder}.
  35.  * <p>
  36.  *
  37.  * @see DirCacheBuilder
  38.  */
  39. public class DirCacheEditor extends BaseDirCacheEditor {
  40.     private static final Comparator<PathEdit> EDIT_CMP = (PathEdit o1,
  41.             PathEdit o2) -> {
  42.         final byte[] a = o1.path;
  43.         final byte[] b = o2.path;
  44.         return cmp(a, a.length, b, b.length);
  45.     };

  46.     private final List<PathEdit> edits;
  47.     private int editIdx;

  48.     /**
  49.      * Construct a new editor.
  50.      *
  51.      * @param dc
  52.      *            the cache this editor will eventually update.
  53.      * @param ecnt
  54.      *            estimated number of entries the editor will have upon
  55.      *            completion. This sizes the initial entry table.
  56.      */
  57.     protected DirCacheEditor(DirCache dc, int ecnt) {
  58.         super(dc, ecnt);
  59.         edits = new ArrayList<>();
  60.     }

  61.     /**
  62.      * Append one edit command to the list of commands to be applied.
  63.      * <p>
  64.      * Edit commands may be added in any order chosen by the application. They
  65.      * are automatically rearranged by the builder to provide the most efficient
  66.      * update possible.
  67.      *
  68.      * @param edit
  69.      *            another edit command.
  70.      */
  71.     public void add(PathEdit edit) {
  72.         edits.add(edit);
  73.     }

  74.     /** {@inheritDoc} */
  75.     @Override
  76.     public boolean commit() throws IOException {
  77.         if (edits.isEmpty()) {
  78.             // No changes? Don't rewrite the index.
  79.             //
  80.             cache.unlock();
  81.             return true;
  82.         }
  83.         return super.commit();
  84.     }

  85.     /** {@inheritDoc} */
  86.     @Override
  87.     public void finish() {
  88.         if (!edits.isEmpty()) {
  89.             applyEdits();
  90.             replace();
  91.         }
  92.     }

  93.     private void applyEdits() {
  94.         Collections.sort(edits, EDIT_CMP);
  95.         editIdx = 0;

  96.         final int maxIdx = cache.getEntryCount();
  97.         int lastIdx = 0;
  98.         while (editIdx < edits.size()) {
  99.             PathEdit e = edits.get(editIdx++);
  100.             int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
  101.             final boolean missing = eIdx < 0;
  102.             if (eIdx < 0)
  103.                 eIdx = -(eIdx + 1);
  104.             final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
  105.             if (cnt > 0)
  106.                 fastKeep(lastIdx, cnt);

  107.             if (e instanceof DeletePath) {
  108.                 lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
  109.                 continue;
  110.             }
  111.             if (e instanceof DeleteTree) {
  112.                 lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
  113.                 continue;
  114.             }

  115.             if (missing) {
  116.                 DirCacheEntry ent = new DirCacheEntry(e.path);
  117.                 e.apply(ent);
  118.                 if (ent.getRawMode() == 0) {
  119.                     throw new IllegalArgumentException(MessageFormat.format(
  120.                             JGitText.get().fileModeNotSetForPath,
  121.                             ent.getPathString()));
  122.                 }
  123.                 lastIdx = e.replace
  124.                     ? deleteOverlappingSubtree(ent, eIdx)
  125.                     : eIdx;
  126.                 fastAdd(ent);
  127.             } else {
  128.                 lastIdx = cache.nextEntry(eIdx);
  129.                 if (lastIdx > eIdx + 1) {
  130.                     // Apply to all entries of the current path (different
  131.                     // stages). If any apply() resets the stage to STAGE_0, take
  132.                     // only that entry and omit all others.
  133.                     DirCacheEntry[] tmp = new DirCacheEntry[lastIdx - eIdx];
  134.                     int n = 0;
  135.                     for (int i = eIdx; i < lastIdx; i++) {
  136.                         DirCacheEntry ent = cache.getEntry(i);
  137.                         e.apply(ent);
  138.                         if (ent.getStage() == DirCacheEntry.STAGE_0) {
  139.                             fastAdd(ent);
  140.                             n = 0;
  141.                             break;
  142.                         }
  143.                         tmp[n++] = ent;
  144.                     }
  145.                     for (int i = 0; i < n; i++) {
  146.                         fastAdd(tmp[i]);
  147.                     }
  148.                 } else {
  149.                     DirCacheEntry ent = cache.getEntry(eIdx);
  150.                     e.apply(ent);
  151.                     fastAdd(ent);
  152.                 }
  153.             }
  154.         }

  155.         final int cnt = maxIdx - lastIdx;
  156.         if (cnt > 0)
  157.             fastKeep(lastIdx, cnt);
  158.     }

  159.     private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) {
  160.         byte[] entPath = ent.path;
  161.         int entLen = entPath.length;

  162.         // Delete any file that was previously processed and overlaps
  163.         // the parent directory for the new entry. Since the editor
  164.         // always processes entries in path order, binary search back
  165.         // for the overlap for each parent directory.
  166.         for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) {
  167.             int i = findEntry(entPath, p);
  168.             if (i >= 0) {
  169.                 // A file does overlap, delete the file from the array.
  170.                 // No other parents can have overlaps as the file should
  171.                 // have taken care of that itself.
  172.                 int n = --entryCnt - i;
  173.                 System.arraycopy(entries, i + 1, entries, i, n);
  174.                 break;
  175.             }

  176.             // If at least one other entry already exists in this parent
  177.             // directory there is no need to continue searching up the tree.
  178.             i = -(i + 1);
  179.             if (i < entryCnt && inDir(entries[i], entPath, p)) {
  180.                 break;
  181.             }
  182.         }

  183.         int maxEnt = cache.getEntryCount();
  184.         if (eIdx >= maxEnt) {
  185.             return maxEnt;
  186.         }

  187.         DirCacheEntry next = cache.getEntry(eIdx);
  188.         if (Paths.compare(next.path, 0, next.path.length, 0,
  189.                 entPath, 0, entLen, TYPE_TREE) < 0) {
  190.             // Next DirCacheEntry sorts before new entry as tree. Defer a
  191.             // DeleteTree command to delete any entries if they exist. This
  192.             // case only happens for A, A.c, A/c type of conflicts (rare).
  193.             insertEdit(new DeleteTree(entPath));
  194.             return eIdx;
  195.         }

  196.         // Next entry may be contained by the entry-as-tree, skip if so.
  197.         while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) {
  198.             eIdx++;
  199.         }
  200.         return eIdx;
  201.     }

  202.     private int findEntry(byte[] p, int pLen) {
  203.         int low = 0;
  204.         int high = entryCnt;
  205.         while (low < high) {
  206.             int mid = (low + high) >>> 1;
  207.             int cmp = cmp(p, pLen, entries[mid]);
  208.             if (cmp < 0) {
  209.                 high = mid;
  210.             } else if (cmp == 0) {
  211.                 while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
  212.                     mid--;
  213.                 }
  214.                 return mid;
  215.             } else {
  216.                 low = mid + 1;
  217.             }
  218.         }
  219.         return -(low + 1);
  220.     }

  221.     private void insertEdit(DeleteTree d) {
  222.         for (int i = editIdx; i < edits.size(); i++) {
  223.             int cmp = EDIT_CMP.compare(d, edits.get(i));
  224.             if (cmp < 0) {
  225.                 edits.add(i, d);
  226.                 return;
  227.             } else if (cmp == 0) {
  228.                 return;
  229.             }
  230.         }
  231.         edits.add(d);
  232.     }

  233.     private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) {
  234.         return e.path.length > pLen && e.path[pLen] == '/'
  235.                 && peq(path, e.path, pLen);
  236.     }

  237.     private static int pdir(byte[] path, int e) {
  238.         for (e--; e > 0; e--) {
  239.             if (path[e] == '/') {
  240.                 return e;
  241.             }
  242.         }
  243.         return 0;
  244.     }

  245.     /**
  246.      * Any index record update.
  247.      * <p>
  248.      * Applications should subclass and provide their own implementation for the
  249.      * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once
  250.      * for each record in the index which matches the path name. If there are
  251.      * multiple records (for example in stages 1, 2 and 3), the edit instance
  252.      * will be called multiple times, once for each stage. If any of these calls
  253.      * resets the stage to 0, only this entry will be taken and entries for
  254.      * other stages are discarded.
  255.      */
  256.     public abstract static class PathEdit {
  257.         final byte[] path;
  258.         boolean replace = true;

  259.         /**
  260.          * Create a new update command by path name.
  261.          *
  262.          * @param entryPath
  263.          *            path of the file within the repository.
  264.          */
  265.         public PathEdit(String entryPath) {
  266.             path = Constants.encode(entryPath);
  267.         }

  268.         PathEdit(byte[] path) {
  269.             this.path = path;
  270.         }

  271.         /**
  272.          * Create a new update command for an existing entry instance.
  273.          *
  274.          * @param ent
  275.          *            entry instance to match path of. Only the path of this
  276.          *            entry is actually considered during command evaluation.
  277.          */
  278.         public PathEdit(DirCacheEntry ent) {
  279.             path = ent.path;
  280.         }

  281.         /**
  282.          * Configure if a file can replace a directory (or vice versa).
  283.          * <p>
  284.          * Default is {@code true} as this is usually the desired behavior.
  285.          *
  286.          * @param ok
  287.          *            if true a file can replace a directory, or a directory can
  288.          *            replace a file.
  289.          * @return {@code this}
  290.          * @since 4.2
  291.          */
  292.         public PathEdit setReplace(boolean ok) {
  293.             replace = ok;
  294.             return this;
  295.         }

  296.         /**
  297.          * Apply the update to a single cache entry matching the path.
  298.          * <p>
  299.          * After apply is invoked the entry is added to the output table, and
  300.          * will be included in the new index.
  301.          *
  302.          * @param ent
  303.          *            the entry being processed. All fields are zeroed out if
  304.          *            the path is a new path in the index.
  305.          */
  306.         public abstract void apply(DirCacheEntry ent);

  307.         @Override
  308.         public String toString() {
  309.             String p = DirCacheEntry.toString(path);
  310.             return getClass().getSimpleName() + '[' + p + ']';
  311.         }
  312.     }

  313.     /**
  314.      * Deletes a single file entry from the index.
  315.      * <p>
  316.      * This deletion command removes only a single file at the given location,
  317.      * but removes multiple stages (if present) for that path. To remove a
  318.      * complete subtree use {@link DeleteTree} instead.
  319.      *
  320.      * @see DeleteTree
  321.      */
  322.     public static final class DeletePath extends PathEdit {
  323.         /**
  324.          * Create a new deletion command by path name.
  325.          *
  326.          * @param entryPath
  327.          *            path of the file within the repository.
  328.          */
  329.         public DeletePath(String entryPath) {
  330.             super(entryPath);
  331.         }

  332.         /**
  333.          * Create a new deletion command for an existing entry instance.
  334.          *
  335.          * @param ent
  336.          *            entry instance to remove. Only the path of this entry is
  337.          *            actually considered during command evaluation.
  338.          */
  339.         public DeletePath(DirCacheEntry ent) {
  340.             super(ent);
  341.         }

  342.         @Override
  343.         public void apply(DirCacheEntry ent) {
  344.             throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
  345.         }
  346.     }

  347.     /**
  348.      * Recursively deletes all paths under a subtree.
  349.      * <p>
  350.      * This deletion command is more generic than {@link DeletePath} as it can
  351.      * remove all records which appear recursively under the same subtree.
  352.      * Multiple stages are removed (if present) for any deleted entry.
  353.      * <p>
  354.      * This command will not remove a single file entry. To remove a single file
  355.      * use {@link DeletePath}.
  356.      *
  357.      * @see DeletePath
  358.      */
  359.     public static final class DeleteTree extends PathEdit {
  360.         /**
  361.          * Create a new tree deletion command by path name.
  362.          *
  363.          * @param entryPath
  364.          *            path of the subtree within the repository. If the path
  365.          *            does not end with "/" a "/" is implicitly added to ensure
  366.          *            only the subtree's contents are matched by the command.
  367.          *            The special case "" (not "/"!) deletes all entries.
  368.          */
  369.         public DeleteTree(String entryPath) {
  370.             super(entryPath.isEmpty()
  371.                     || entryPath.charAt(entryPath.length() - 1) == '/'
  372.                     ? entryPath
  373.                     : entryPath + '/');
  374.         }

  375.         DeleteTree(byte[] path) {
  376.             super(appendSlash(path));
  377.         }

  378.         private static byte[] appendSlash(byte[] path) {
  379.             int n = path.length;
  380.             if (n > 0 && path[n - 1] != '/') {
  381.                 byte[] r = new byte[n + 1];
  382.                 System.arraycopy(path, 0, r, 0, n);
  383.                 r[n] = '/';
  384.                 return r;
  385.             }
  386.             return path;
  387.         }

  388.         @Override
  389.         public void apply(DirCacheEntry ent) {
  390.             throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
  391.         }
  392.     }
  393. }