org.eclipse.jgit.api.ResetCommand.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.api.ResetCommand.java

Source

/*
 * Copyright (C) 2011-2013, Chris Aniszczyk <caniszczyk@gmail.com>
 * 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.api;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.LinkedList;

import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuildIterator;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;

/**
 * A class used to execute a {@code Reset} command. It has setters for all
 * supported options and arguments of this command and a {@link #call()} method
 * to finally execute the command. Each instance of this class should only be
 * used for one invocation of the command (means: one call to {@link #call()})
 *
 * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-reset.html"
 *      >Git documentation about Reset</a>
 */
public class ResetCommand extends GitCommand<Ref> {

    /**
     * Kind of reset
     */
    public enum ResetType {
        /**
         * Just change the ref, the index and workdir are not changed.
         */
        SOFT,

        /**
         * Change the ref and the index, the workdir is not changed.
         */
        MIXED,

        /**
         * Change the ref, the index and the workdir
         */
        HARD,

        /**
         * Resets the index and updates the files in the working tree that are
         * different between respective commit and HEAD, but keeps those which
         * are different between the index and working tree
         */
        MERGE, // TODO not implemented yet

        /**
         * Change the ref, the index and the workdir that are different between
         * respective commit and HEAD
         */
        KEEP // TODO not implemented yet
    }

    // We need to be able to distinguish whether the caller set the ref
    // explicitly or not, so we apply the default (HEAD) only later.
    private String ref = null;

    private ResetType mode;

    private Collection<String> filepaths = new LinkedList<>();

    private boolean isReflogDisabled;

    private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;

    /**
     * <p>
     * Constructor for ResetCommand.
     * </p>
     *
     * @param repo
     *            the {@link org.eclipse.jgit.lib.Repository}
     */
    public ResetCommand(Repository repo) {
        super(repo);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Executes the {@code Reset} command. Each instance of this class should
     * only be used for one invocation of the command. Don't call this method
     * twice on an instance.
     */
    @Override
    public Ref call() throws GitAPIException, CheckoutConflictException {
        checkCallable();

        try {
            RepositoryState state = repo.getRepositoryState();
            final boolean merging = state.equals(RepositoryState.MERGING)
                    || state.equals(RepositoryState.MERGING_RESOLVED);
            final boolean cherryPicking = state.equals(RepositoryState.CHERRY_PICKING)
                    || state.equals(RepositoryState.CHERRY_PICKING_RESOLVED);
            final boolean reverting = state.equals(RepositoryState.REVERTING)
                    || state.equals(RepositoryState.REVERTING_RESOLVED);

            final ObjectId commitId = resolveRefToCommitId();
            // When ref is explicitly specified, it has to resolve
            if (ref != null && commitId == null) {
                // @TODO throw an InvalidRefNameException. We can't do that
                // now because this would break the API
                throw new JGitInternalException(MessageFormat.format(JGitText.get().invalidRefName, ref));
            }

            final ObjectId commitTree;
            if (commitId != null)
                commitTree = parseCommit(commitId).getTree();
            else
                commitTree = null;

            if (!filepaths.isEmpty()) {
                // reset [commit] -- paths
                resetIndexForPaths(commitTree);
                setCallable(false);
                return repo.exactRef(Constants.HEAD);
            }

            final Ref result;
            if (commitId != null) {
                // write the ref
                final RefUpdate ru = repo.updateRef(Constants.HEAD);
                ru.setNewObjectId(commitId);

                String refName = Repository.shortenRefName(getRefOrHEAD());
                if (isReflogDisabled) {
                    ru.disableRefLog();
                } else {
                    String message = refName + ": updating " + Constants.HEAD; //$NON-NLS-1$
                    ru.setRefLogMessage(message, false);
                }
                if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE)
                    throw new JGitInternalException(MessageFormat.format(JGitText.get().cannotLock, ru.getName()));

                ObjectId origHead = ru.getOldObjectId();
                if (origHead != null)
                    repo.writeOrigHead(origHead);
            }
            result = repo.exactRef(Constants.HEAD);

            if (mode == null)
                mode = ResetType.MIXED;

            switch (mode) {
            case HARD:
                checkoutIndex(commitTree);
                break;
            case MIXED:
                resetIndex(commitTree);
                break;
            case SOFT: // do nothing, only the ref was changed
                break;
            case KEEP: // TODO
            case MERGE: // TODO
                throw new UnsupportedOperationException();

            }

            if (mode != ResetType.SOFT) {
                if (merging)
                    resetMerge();
                else if (cherryPicking)
                    resetCherryPick();
                else if (reverting)
                    resetRevert();
                else if (repo.readSquashCommitMsg() != null)
                    repo.writeSquashCommitMsg(null /* delete */);
            }

            setCallable(false);
            return result;
        } catch (IOException e) {
            throw new JGitInternalException(MessageFormat
                    .format(JGitText.get().exceptionCaughtDuringExecutionOfResetCommand, e.getMessage()), e);
        }
    }

    private RevCommit parseCommit(ObjectId commitId) {
        try (RevWalk rw = new RevWalk(repo)) {
            return rw.parseCommit(commitId);
        } catch (IOException e) {
            throw new JGitInternalException(
                    MessageFormat.format(JGitText.get().cannotReadCommit, commitId.toString()), e);
        }
    }

    private ObjectId resolveRefToCommitId() {
        try {
            return repo.resolve(getRefOrHEAD() + "^{commit}"); //$NON-NLS-1$
        } catch (IOException e) {
            throw new JGitInternalException(MessageFormat.format(JGitText.get().cannotRead, getRefOrHEAD()), e);
        }
    }

    /**
     * Set the name of the <code>Ref</code> to reset to
     *
     * @param ref
     *            the ref to reset to, defaults to HEAD if not specified
     * @return this instance
     */
    public ResetCommand setRef(String ref) {
        this.ref = ref;
        return this;
    }

    /**
     * Set the reset mode
     *
     * @param mode
     *            the mode of the reset command
     * @return this instance
     */
    public ResetCommand setMode(ResetType mode) {
        if (!filepaths.isEmpty())
            throw new JGitInternalException(MessageFormat.format(JGitText.get().illegalCombinationOfArguments,
                    "[--mixed | --soft | --hard]", "<paths>...")); //$NON-NLS-1$ //$NON-NLS-2$
        this.mode = mode;
        return this;
    }

    /**
     * Repository relative path of file or directory to reset
     *
     * @param path
     *            repository-relative path of file/directory to reset (with
     *            <code>/</code> as separator)
     * @return this instance
     */
    public ResetCommand addPath(String path) {
        if (mode != null)
            throw new JGitInternalException(
                    MessageFormat.format(JGitText.get().illegalCombinationOfArguments, "<paths>...", //$NON-NLS-1$
                            "[--mixed | --soft | --hard]")); //$NON-NLS-1$
        filepaths.add(path);
        return this;
    }

    /**
     * Whether to disable reflog
     *
     * @param disable
     *            if {@code true} disables writing a reflog entry for this reset
     *            command
     * @return this instance
     * @since 4.5
     */
    public ResetCommand disableRefLog(boolean disable) {
        this.isReflogDisabled = disable;
        return this;
    }

    /**
     * Whether reflog is disabled
     *
     * @return {@code true} if writing reflog is disabled for this reset command
     * @since 4.5
     */
    public boolean isReflogDisabled() {
        return this.isReflogDisabled;
    }

    private String getRefOrHEAD() {
        if (ref != null) {
            return ref;
        }
        return Constants.HEAD;
    }

    /**
     * The progress monitor associated with the reset operation. By default,
     * this is set to <code>NullProgressMonitor</code>
     *
     * @see NullProgressMonitor
     * @param monitor
     *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
     * @return {@code this}
     * @since 4.11
     */
    public ResetCommand setProgressMonitor(ProgressMonitor monitor) {
        if (monitor == null) {
            monitor = NullProgressMonitor.INSTANCE;
        }
        this.monitor = monitor;
        return this;
    }

    private void resetIndexForPaths(ObjectId commitTree) {
        DirCache dc = null;
        try (TreeWalk tw = new TreeWalk(repo)) {
            dc = repo.lockDirCache();
            DirCacheBuilder builder = dc.builder();

            tw.addTree(new DirCacheBuildIterator(builder));
            if (commitTree != null)
                tw.addTree(commitTree);
            else
                tw.addTree(new EmptyTreeIterator());
            tw.setFilter(PathFilterGroup.createFromStrings(filepaths));
            tw.setRecursive(true);

            while (tw.next()) {
                final CanonicalTreeParser tree = tw.getTree(1, CanonicalTreeParser.class);
                // only keep file in index if it's in the commit
                if (tree != null) {
                    // revert index to commit
                    DirCacheEntry entry = new DirCacheEntry(tw.getRawPath());
                    entry.setFileMode(tree.getEntryFileMode());
                    entry.setObjectId(tree.getEntryObjectId());
                    builder.add(entry);
                }
            }

            builder.commit();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (dc != null)
                dc.unlock();
        }
    }

    private void resetIndex(ObjectId commitTree) throws IOException {
        DirCache dc = repo.lockDirCache();
        try (TreeWalk walk = new TreeWalk(repo)) {
            DirCacheBuilder builder = dc.builder();

            if (commitTree != null)
                walk.addTree(commitTree);
            else
                walk.addTree(new EmptyTreeIterator());
            walk.addTree(new DirCacheIterator(dc));
            walk.setRecursive(true);

            while (walk.next()) {
                AbstractTreeIterator cIter = walk.getTree(0, AbstractTreeIterator.class);
                if (cIter == null) {
                    // Not in commit, don't add to new index
                    continue;
                }

                final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
                entry.setFileMode(cIter.getEntryFileMode());
                entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());

                DirCacheIterator dcIter = walk.getTree(1, DirCacheIterator.class);
                if (dcIter != null && dcIter.idEqual(cIter)) {
                    DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
                    entry.setLastModified(indexEntry.getLastModifiedInstant());
                    entry.setLength(indexEntry.getLength());
                }

                builder.add(entry);
            }

            builder.commit();
        } finally {
            dc.unlock();
        }
    }

    private void checkoutIndex(ObjectId commitTree) throws IOException, GitAPIException {
        DirCache dc = repo.lockDirCache();
        try {
            DirCacheCheckout checkout = new DirCacheCheckout(repo, dc, commitTree);
            checkout.setFailOnConflict(false);
            checkout.setProgressMonitor(monitor);
            try {
                checkout.checkout();
            } catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
                throw new CheckoutConflictException(checkout.getConflicts(), cce);
            }
        } finally {
            dc.unlock();
        }
    }

    private void resetMerge() throws IOException {
        repo.writeMergeHeads(null);
        repo.writeMergeCommitMsg(null);
    }

    private void resetCherryPick() throws IOException {
        repo.writeCherryPickHead(null);
        repo.writeMergeCommitMsg(null);
    }

    private void resetRevert() throws IOException {
        repo.writeRevertHead(null);
        repo.writeMergeCommitMsg(null);
    }

    /** {@inheritDoc} */
    @SuppressWarnings("nls")
    @Override
    public String toString() {
        return "ResetCommand [repo=" + repo + ", ref=" + ref + ", mode=" + mode + ", isReflogDisabled="
                + isReflogDisabled + ", filepaths=" + filepaths + "]";
    }

}