org.eclipse.jgit.dircache.DirCacheEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.dircache.DirCacheEditor.java

Source

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

package org.eclipse.jgit.dircache;

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

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

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

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

    private final List<PathEdit> edits;
    private int editIdx;

    /**
     * Construct a new editor.
     *
     * @param dc
     *            the cache this editor will eventually update.
     * @param ecnt
     *            estimated number of entries the editor will have upon
     *            completion. This sizes the initial entry table.
     */
    protected DirCacheEditor(DirCache dc, int ecnt) {
        super(dc, ecnt);
        edits = new ArrayList<>();
    }

    /**
     * Append one edit command to the list of commands to be applied.
     * <p>
     * Edit commands may be added in any order chosen by the application. They
     * are automatically rearranged by the builder to provide the most efficient
     * update possible.
     *
     * @param edit
     *            another edit command.
     */
    public void add(PathEdit edit) {
        edits.add(edit);
    }

    /** {@inheritDoc} */
    @Override
    public boolean commit() throws IOException {
        if (edits.isEmpty()) {
            // No changes? Don't rewrite the index.
            //
            cache.unlock();
            return true;
        }
        return super.commit();
    }

    /** {@inheritDoc} */
    @Override
    public void finish() {
        if (!edits.isEmpty()) {
            applyEdits();
            replace();
        }
    }

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

        final int maxIdx = cache.getEntryCount();
        int lastIdx = 0;
        while (editIdx < edits.size()) {
            PathEdit e = edits.get(editIdx++);
            int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
            final boolean missing = eIdx < 0;
            if (eIdx < 0)
                eIdx = -(eIdx + 1);
            final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
            if (cnt > 0)
                fastKeep(lastIdx, cnt);

            if (e instanceof DeletePath) {
                lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
                continue;
            }
            if (e instanceof DeleteTree) {
                lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
                continue;
            }

            if (missing) {
                DirCacheEntry ent = new DirCacheEntry(e.path);
                e.apply(ent);
                if (ent.getRawMode() == 0) {
                    throw new IllegalArgumentException(
                            MessageFormat.format(JGitText.get().fileModeNotSetForPath, ent.getPathString()));
                }
                lastIdx = e.replace ? deleteOverlappingSubtree(ent, eIdx) : eIdx;
                fastAdd(ent);
            } else {
                // Apply to all entries of the current path (different stages)
                lastIdx = cache.nextEntry(eIdx);
                for (int i = eIdx; i < lastIdx; i++) {
                    final DirCacheEntry ent = cache.getEntry(i);
                    e.apply(ent);
                    fastAdd(ent);
                }
            }
        }

        final int cnt = maxIdx - lastIdx;
        if (cnt > 0)
            fastKeep(lastIdx, cnt);
    }

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

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

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

        int maxEnt = cache.getEntryCount();
        if (eIdx >= maxEnt) {
            return maxEnt;
        }

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

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

    private int findEntry(byte[] p, int pLen) {
        int low = 0;
        int high = entryCnt;
        while (low < high) {
            int mid = (low + high) >>> 1;
            int cmp = cmp(p, pLen, entries[mid]);
            if (cmp < 0) {
                high = mid;
            } else if (cmp == 0) {
                while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
                    mid--;
                }
                return mid;
            } else {
                low = mid + 1;
            }
        }
        return -(low + 1);
    }

    private void insertEdit(DeleteTree d) {
        for (int i = editIdx; i < edits.size(); i++) {
            int cmp = EDIT_CMP.compare(d, edits.get(i));
            if (cmp < 0) {
                edits.add(i, d);
                return;
            } else if (cmp == 0) {
                return;
            }
        }
        edits.add(d);
    }

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

    private static int pdir(byte[] path, int e) {
        for (e--; e > 0; e--) {
            if (path[e] == '/') {
                return e;
            }
        }
        return 0;
    }

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

        /**
         * Create a new update command by path name.
         *
         * @param entryPath
         *            path of the file within the repository.
         */
        public PathEdit(String entryPath) {
            path = Constants.encode(entryPath);
        }

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

        /**
         * Create a new update command for an existing entry instance.
         *
         * @param ent
         *            entry instance to match path of. Only the path of this
         *            entry is actually considered during command evaluation.
         */
        public PathEdit(DirCacheEntry ent) {
            path = ent.path;
        }

        /**
         * Configure if a file can replace a directory (or vice versa).
         * <p>
         * Default is {@code true} as this is usually the desired behavior.
         *
         * @param ok
         *            if true a file can replace a directory, or a directory can
         *            replace a file.
         * @return {@code this}
         * @since 4.2
         */
        public PathEdit setReplace(boolean ok) {
            replace = ok;
            return this;
        }

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

        @Override
        public String toString() {
            String p = DirCacheEntry.toString(path);
            return getClass().getSimpleName() + '[' + p + ']';
        }
    }

    /**
     * Deletes a single file entry from the index.
     * <p>
     * This deletion command removes only a single file at the given location,
     * but removes multiple stages (if present) for that path. To remove a
     * complete subtree use {@link DeleteTree} instead.
     *
     * @see DeleteTree
     */
    public static final class DeletePath extends PathEdit {
        /**
         * Create a new deletion command by path name.
         *
         * @param entryPath
         *            path of the file within the repository.
         */
        public DeletePath(String entryPath) {
            super(entryPath);
        }

        /**
         * Create a new deletion command for an existing entry instance.
         *
         * @param ent
         *            entry instance to remove. Only the path of this entry is
         *            actually considered during command evaluation.
         */
        public DeletePath(DirCacheEntry ent) {
            super(ent);
        }

        @Override
        public void apply(DirCacheEntry ent) {
            throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
        }
    }

    /**
     * Recursively deletes all paths under a subtree.
     * <p>
     * This deletion command is more generic than {@link DeletePath} as it can
     * remove all records which appear recursively under the same subtree.
     * Multiple stages are removed (if present) for any deleted entry.
     * <p>
     * This command will not remove a single file entry. To remove a single file
     * use {@link DeletePath}.
     *
     * @see DeletePath
     */
    public static final class DeleteTree extends PathEdit {
        /**
         * Create a new tree deletion command by path name.
         *
         * @param entryPath
         *            path of the subtree within the repository. If the path
         *            does not end with "/" a "/" is implicitly added to ensure
         *            only the subtree's contents are matched by the command.
         *            The special case "" (not "/"!) deletes all entries.
         */
        public DeleteTree(String entryPath) {
            super(entryPath.isEmpty() || entryPath.charAt(entryPath.length() - 1) == '/' ? entryPath
                    : entryPath + '/');
        }

        DeleteTree(byte[] path) {
            super(appendSlash(path));
        }

        private static byte[] appendSlash(byte[] path) {
            int n = path.length;
            if (n > 0 && path[n - 1] != '/') {
                byte[] r = new byte[n + 1];
                System.arraycopy(path, 0, r, 0, n);
                r[n] = '/';
                return r;
            }
            return path;
        }

        @Override
        public void apply(DirCacheEntry ent) {
            throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
        }
    }
}