| /* |
| * 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.ObjectId; |
| 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<String>(); |
| |
| private boolean isReflogDisabled; |
| |
| /** |
| * |
| * @param repo |
| */ |
| public ResetCommand(Repository repo) { |
| super(repo); |
| } |
| |
| /** |
| * 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. |
| * |
| * @return the Ref after reset |
| * @throws GitAPIException |
| */ |
| 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(final 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); |
| } |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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; |
| else |
| return Constants.HEAD; |
| } |
| |
| private void resetIndexForPaths(ObjectId commitTree) { |
| DirCache dc = null; |
| try (final 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.getLastModified()); |
| 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); |
| 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); |
| } |
| |
| @SuppressWarnings("nls") |
| @Override |
| public String toString() { |
| return "ResetCommand [repo=" + repo + ", ref=" + ref + ", mode=" + mode |
| + ", isReflogDisabled=" + isReflogDisabled + ", filepaths=" |
| + filepaths + "]"; |
| } |
| |
| } |