blob: 2f46b7faf3ccb47ffc5b3300ee5f49e767b5dcd2 [file] [log] [blame]
/*
* Copyright (C) 2011, 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.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
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.FileMode;
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.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
}
private String ref = Constants.HEAD;
private ResetType mode;
private Collection<String> filepaths = new LinkedList<String>();
/**
*
* @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
*/
public Ref call() throws GitAPIException {
checkCallable();
Ref r;
RevCommit commit;
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);
// resolve the ref to a commit
final ObjectId commitId;
try {
commitId = repo.resolve(ref + "^{commit}");
if (commitId == null) {
// @TODO throw an InvalidRefNameException. We can't do that
// now because this would break the API
throw new JGitInternalException("Invalid ref " + ref
+ " specified");
}
} catch (IOException e) {
throw new JGitInternalException(
MessageFormat.format(JGitText.get().cannotRead, ref),
e);
}
RevWalk rw = new RevWalk(repo);
try {
commit = rw.parseCommit(commitId);
} catch (IOException e) {
throw new JGitInternalException(
MessageFormat.format(
JGitText.get().cannotReadCommit, commitId.toString()),
e);
} finally {
rw.release();
}
if (!filepaths.isEmpty()) {
// reset [commit] -- paths
resetIndexForPaths(commit);
setCallable(false);
return repo.getRef(Constants.HEAD);
}
// write the ref
final RefUpdate ru = repo.updateRef(Constants.HEAD);
ru.setNewObjectId(commitId);
String refName = Repository.shortenRefName(ref);
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()));
switch (mode) {
case HARD:
checkoutIndex(commit);
break;
case MIXED:
resetIndex(commit);
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();
}
setCallable(false);
r = ru.getRef();
} catch (IOException e) {
throw new JGitInternalException(
JGitText.get().exceptionCaughtDuringExecutionOfResetCommand,
e);
}
return r;
}
/**
* @param ref
* the ref to reset to
* @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>..."));
this.mode = mode;
return this;
}
/**
* @param file
* the file to add
* @return this instance
*/
public ResetCommand addPath(String file) {
if (mode != null)
throw new JGitInternalException(MessageFormat.format(
JGitText.get().illegalCombinationOfArguments, "<paths>...",
"[--mixed | --soft | --hard]"));
filepaths.add(file);
return this;
}
private void resetIndexForPaths(RevCommit commit) {
DirCache dc = null;
final DirCacheEditor edit;
try {
dc = repo.lockDirCache();
edit = dc.editor();
final TreeWalk tw = new TreeWalk(repo);
tw.addTree(new DirCacheIterator(dc));
tw.addTree(commit.getTree());
tw.setFilter(PathFilterGroup.createFromStrings(filepaths));
tw.setRecursive(true);
while (tw.next()) {
final String path = tw.getPathString();
// DirCacheIterator dci = tw.getTree(0, DirCacheIterator.class);
final CanonicalTreeParser tree = tw.getTree(1,
CanonicalTreeParser.class);
if (tree == null)
// file is not in the commit, remove from index
edit.add(new DirCacheEditor.DeletePath(path));
else { // revert index to commit
// it seams that there is concurrent access to tree
// variable, therefore we need to keep references to
// entryFileMode and entryObjectId in local
// variables
final FileMode entryFileMode = tree.getEntryFileMode();
final ObjectId entryObjectId = tree.getEntryObjectId();
edit.add(new DirCacheEditor.PathEdit(path) {
@Override
public void apply(DirCacheEntry ent) {
ent.setFileMode(entryFileMode);
ent.setObjectId(entryObjectId);
ent.setLastModified(0);
}
});
}
}
edit.commit();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (dc != null)
dc.unlock();
}
}
private void resetIndex(RevCommit commit) throws IOException {
DirCache dc = repo.lockDirCache();
TreeWalk walk = null;
try {
DirCacheEditor editor = dc.editor();
walk = new TreeWalk(repo);
walk.addTree(commit.getTree());
walk.addTree(new DirCacheIterator(dc));
walk.setRecursive(true);
while (walk.next()) {
AbstractTreeIterator cIter = walk.getTree(0,
AbstractTreeIterator.class);
if (cIter == null) {
editor.add(new DeletePath(walk.getPathString()));
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());
}
editor.add(new PathEdit(entry) {
@Override
public void apply(DirCacheEntry ent) {
ent.copyMetaData(entry);
}
});
}
editor.commit();
} finally {
dc.unlock();
if (walk != null)
walk.release();
}
}
private void checkoutIndex(RevCommit commit) throws IOException {
DirCache dc = repo.lockDirCache();
try {
DirCacheCheckout checkout = new DirCacheCheckout(repo, dc,
commit.getTree());
checkout.setFailOnConflict(false);
checkout.checkout();
} 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);
}
}