DirCacheEditor.java

  1. /*
  2.  * Copyright (C) 2008-2009, Google Inc.
  3.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  4.  * and other copyright owners as documented in the project's IP log.
  5.  *
  6.  * This program and the accompanying materials are made available
  7.  * under the terms of the Eclipse Distribution License v1.0 which
  8.  * accompanies this distribution, is reproduced below, and is
  9.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  10.  *
  11.  * All rights reserved.
  12.  *
  13.  * Redistribution and use in source and binary forms, with or
  14.  * without modification, are permitted provided that the following
  15.  * conditions are met:
  16.  *
  17.  * - Redistributions of source code must retain the above copyright
  18.  *   notice, this list of conditions and the following disclaimer.
  19.  *
  20.  * - Redistributions in binary form must reproduce the above
  21.  *   copyright notice, this list of conditions and the following
  22.  *   disclaimer in the documentation and/or other materials provided
  23.  *   with the distribution.
  24.  *
  25.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  26.  *   names of its contributors may be used to endorse or promote
  27.  *   products derived from this software without specific prior
  28.  *   written permission.
  29.  *
  30.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  31.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  32.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  33.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  34.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  35.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  36.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  37.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  38.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  39.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  40.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  41.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  42.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  43.  */

  44. package org.eclipse.jgit.dircache;

  45. import static org.eclipse.jgit.dircache.DirCache.cmp;
  46. import static org.eclipse.jgit.dircache.DirCacheTree.peq;
  47. import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;

  48. import java.io.IOException;
  49. import java.text.MessageFormat;
  50. import java.util.ArrayList;
  51. import java.util.Collections;
  52. import java.util.Comparator;
  53. import java.util.List;

  54. import org.eclipse.jgit.internal.JGitText;
  55. import org.eclipse.jgit.lib.Constants;
  56. import org.eclipse.jgit.util.Paths;

  57. /**
  58.  * Updates a {@link org.eclipse.jgit.dircache.DirCache} by supplying discrete
  59.  * edit commands.
  60.  * <p>
  61.  * An editor updates a DirCache by taking a list of
  62.  * {@link org.eclipse.jgit.dircache.DirCacheEditor.PathEdit} commands and
  63.  * executing them against the entries of the destination cache to produce a new
  64.  * cache. This edit style allows applications to insert a few commands and then
  65.  * have the editor compute the proper entry indexes necessary to perform an
  66.  * efficient in-order update of the index records. This can be easier to use
  67.  * than {@link org.eclipse.jgit.dircache.DirCacheBuilder}.
  68.  * <p>
  69.  *
  70.  * @see DirCacheBuilder
  71.  */
  72. public class DirCacheEditor extends BaseDirCacheEditor {
  73.     private static final Comparator<PathEdit> EDIT_CMP = (PathEdit o1,
  74.             PathEdit o2) -> {
  75.         final byte[] a = o1.path;
  76.         final byte[] b = o2.path;
  77.         return cmp(a, a.length, b, b.length);
  78.     };

  79.     private final List<PathEdit> edits;
  80.     private int editIdx;

  81.     /**
  82.      * Construct a new editor.
  83.      *
  84.      * @param dc
  85.      *            the cache this editor will eventually update.
  86.      * @param ecnt
  87.      *            estimated number of entries the editor will have upon
  88.      *            completion. This sizes the initial entry table.
  89.      */
  90.     protected DirCacheEditor(DirCache dc, int ecnt) {
  91.         super(dc, ecnt);
  92.         edits = new ArrayList<>();
  93.     }

  94.     /**
  95.      * Append one edit command to the list of commands to be applied.
  96.      * <p>
  97.      * Edit commands may be added in any order chosen by the application. They
  98.      * are automatically rearranged by the builder to provide the most efficient
  99.      * update possible.
  100.      *
  101.      * @param edit
  102.      *            another edit command.
  103.      */
  104.     public void add(PathEdit edit) {
  105.         edits.add(edit);
  106.     }

  107.     /** {@inheritDoc} */
  108.     @Override
  109.     public boolean commit() throws IOException {
  110.         if (edits.isEmpty()) {
  111.             // No changes? Don't rewrite the index.
  112.             //
  113.             cache.unlock();
  114.             return true;
  115.         }
  116.         return super.commit();
  117.     }

  118.     /** {@inheritDoc} */
  119.     @Override
  120.     public void finish() {
  121.         if (!edits.isEmpty()) {
  122.             applyEdits();
  123.             replace();
  124.         }
  125.     }

  126.     private void applyEdits() {
  127.         Collections.sort(edits, EDIT_CMP);
  128.         editIdx = 0;

  129.         final int maxIdx = cache.getEntryCount();
  130.         int lastIdx = 0;
  131.         while (editIdx < edits.size()) {
  132.             PathEdit e = edits.get(editIdx++);
  133.             int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
  134.             final boolean missing = eIdx < 0;
  135.             if (eIdx < 0)
  136.                 eIdx = -(eIdx + 1);
  137.             final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
  138.             if (cnt > 0)
  139.                 fastKeep(lastIdx, cnt);

  140.             if (e instanceof DeletePath) {
  141.                 lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
  142.                 continue;
  143.             }
  144.             if (e instanceof DeleteTree) {
  145.                 lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
  146.                 continue;
  147.             }

  148.             if (missing) {
  149.                 DirCacheEntry ent = new DirCacheEntry(e.path);
  150.                 e.apply(ent);
  151.                 if (ent.getRawMode() == 0) {
  152.                     throw new IllegalArgumentException(MessageFormat.format(
  153.                             JGitText.get().fileModeNotSetForPath,
  154.                             ent.getPathString()));
  155.                 }
  156.                 lastIdx = e.replace
  157.                     ? deleteOverlappingSubtree(ent, eIdx)
  158.                     : eIdx;
  159.                 fastAdd(ent);
  160.             } else {
  161.                 // Apply to all entries of the current path (different stages)
  162.                 lastIdx = cache.nextEntry(eIdx);
  163.                 for (int i = eIdx; i < lastIdx; i++) {
  164.                     final DirCacheEntry ent = cache.getEntry(i);
  165.                     e.apply(ent);
  166.                     fastAdd(ent);
  167.                 }
  168.             }
  169.         }

  170.         final int cnt = maxIdx - lastIdx;
  171.         if (cnt > 0)
  172.             fastKeep(lastIdx, cnt);
  173.     }

  174.     private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) {
  175.         byte[] entPath = ent.path;
  176.         int entLen = entPath.length;

  177.         // Delete any file that was previously processed and overlaps
  178.         // the parent directory for the new entry. Since the editor
  179.         // always processes entries in path order, binary search back
  180.         // for the overlap for each parent directory.
  181.         for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) {
  182.             int i = findEntry(entPath, p);
  183.             if (i >= 0) {
  184.                 // A file does overlap, delete the file from the array.
  185.                 // No other parents can have overlaps as the file should
  186.                 // have taken care of that itself.
  187.                 int n = --entryCnt - i;
  188.                 System.arraycopy(entries, i + 1, entries, i, n);
  189.                 break;
  190.             }

  191.             // If at least one other entry already exists in this parent
  192.             // directory there is no need to continue searching up the tree.
  193.             i = -(i + 1);
  194.             if (i < entryCnt && inDir(entries[i], entPath, p)) {
  195.                 break;
  196.             }
  197.         }

  198.         int maxEnt = cache.getEntryCount();
  199.         if (eIdx >= maxEnt) {
  200.             return maxEnt;
  201.         }

  202.         DirCacheEntry next = cache.getEntry(eIdx);
  203.         if (Paths.compare(next.path, 0, next.path.length, 0,
  204.                 entPath, 0, entLen, TYPE_TREE) < 0) {
  205.             // Next DirCacheEntry sorts before new entry as tree. Defer a
  206.             // DeleteTree command to delete any entries if they exist. This
  207.             // case only happens for A, A.c, A/c type of conflicts (rare).
  208.             insertEdit(new DeleteTree(entPath));
  209.             return eIdx;
  210.         }

  211.         // Next entry may be contained by the entry-as-tree, skip if so.
  212.         while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) {
  213.             eIdx++;
  214.         }
  215.         return eIdx;
  216.     }

  217.     private int findEntry(byte[] p, int pLen) {
  218.         int low = 0;
  219.         int high = entryCnt;
  220.         while (low < high) {
  221.             int mid = (low + high) >>> 1;
  222.             int cmp = cmp(p, pLen, entries[mid]);
  223.             if (cmp < 0) {
  224.                 high = mid;
  225.             } else if (cmp == 0) {
  226.                 while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
  227.                     mid--;
  228.                 }
  229.                 return mid;
  230.             } else {
  231.                 low = mid + 1;
  232.             }
  233.         }
  234.         return -(low + 1);
  235.     }

  236.     private void insertEdit(DeleteTree d) {
  237.         for (int i = editIdx; i < edits.size(); i++) {
  238.             int cmp = EDIT_CMP.compare(d, edits.get(i));
  239.             if (cmp < 0) {
  240.                 edits.add(i, d);
  241.                 return;
  242.             } else if (cmp == 0) {
  243.                 return;
  244.             }
  245.         }
  246.         edits.add(d);
  247.     }

  248.     private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) {
  249.         return e.path.length > pLen && e.path[pLen] == '/'
  250.                 && peq(path, e.path, pLen);
  251.     }

  252.     private static int pdir(byte[] path, int e) {
  253.         for (e--; e > 0; e--) {
  254.             if (path[e] == '/') {
  255.                 return e;
  256.             }
  257.         }
  258.         return 0;
  259.     }

  260.     /**
  261.      * Any index record update.
  262.      * <p>
  263.      * Applications should subclass and provide their own implementation for the
  264.      * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once
  265.      * for each record in the index which matches the path name. If there are
  266.      * multiple records (for example in stages 1, 2 and 3), the edit instance
  267.      * will be called multiple times, once for each stage.
  268.      */
  269.     public abstract static class PathEdit {
  270.         final byte[] path;
  271.         boolean replace = true;

  272.         /**
  273.          * Create a new update command by path name.
  274.          *
  275.          * @param entryPath
  276.          *            path of the file within the repository.
  277.          */
  278.         public PathEdit(String entryPath) {
  279.             path = Constants.encode(entryPath);
  280.         }

  281.         PathEdit(byte[] path) {
  282.             this.path = path;
  283.         }

  284.         /**
  285.          * Create a new update command for an existing entry instance.
  286.          *
  287.          * @param ent
  288.          *            entry instance to match path of. Only the path of this
  289.          *            entry is actually considered during command evaluation.
  290.          */
  291.         public PathEdit(DirCacheEntry ent) {
  292.             path = ent.path;
  293.         }

  294.         /**
  295.          * Configure if a file can replace a directory (or vice versa).
  296.          * <p>
  297.          * Default is {@code true} as this is usually the desired behavior.
  298.          *
  299.          * @param ok
  300.          *            if true a file can replace a directory, or a directory can
  301.          *            replace a file.
  302.          * @return {@code this}
  303.          * @since 4.2
  304.          */
  305.         public PathEdit setReplace(boolean ok) {
  306.             replace = ok;
  307.             return this;
  308.         }

  309.         /**
  310.          * Apply the update to a single cache entry matching the path.
  311.          * <p>
  312.          * After apply is invoked the entry is added to the output table, and
  313.          * will be included in the new index.
  314.          *
  315.          * @param ent
  316.          *            the entry being processed. All fields are zeroed out if
  317.          *            the path is a new path in the index.
  318.          */
  319.         public abstract void apply(DirCacheEntry ent);

  320.         @Override
  321.         public String toString() {
  322.             String p = DirCacheEntry.toString(path);
  323.             return getClass().getSimpleName() + '[' + p + ']';
  324.         }
  325.     }

  326.     /**
  327.      * Deletes a single file entry from the index.
  328.      * <p>
  329.      * This deletion command removes only a single file at the given location,
  330.      * but removes multiple stages (if present) for that path. To remove a
  331.      * complete subtree use {@link DeleteTree} instead.
  332.      *
  333.      * @see DeleteTree
  334.      */
  335.     public static final class DeletePath extends PathEdit {
  336.         /**
  337.          * Create a new deletion command by path name.
  338.          *
  339.          * @param entryPath
  340.          *            path of the file within the repository.
  341.          */
  342.         public DeletePath(String entryPath) {
  343.             super(entryPath);
  344.         }

  345.         /**
  346.          * Create a new deletion command for an existing entry instance.
  347.          *
  348.          * @param ent
  349.          *            entry instance to remove. Only the path of this entry is
  350.          *            actually considered during command evaluation.
  351.          */
  352.         public DeletePath(DirCacheEntry ent) {
  353.             super(ent);
  354.         }

  355.         @Override
  356.         public void apply(DirCacheEntry ent) {
  357.             throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
  358.         }
  359.     }

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

  388.         DeleteTree(byte[] path) {
  389.             super(appendSlash(path));
  390.         }

  391.         private static byte[] appendSlash(byte[] path) {
  392.             int n = path.length;
  393.             if (n > 0 && path[n - 1] != '/') {
  394.                 byte[] r = new byte[n + 1];
  395.                 System.arraycopy(path, 0, r, 0, n);
  396.                 r[n] = '/';
  397.                 return r;
  398.             }
  399.             return path;
  400.         }

  401.         @Override
  402.         public void apply(DirCacheEntry ent) {
  403.             throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
  404.         }
  405.     }
  406. }