View Javadoc
1   /*
2    * Copyright (C) 2011-2013, Chris Aniszczyk <caniszczyk@gmail.com>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.api;
44  
45  import java.io.IOException;
46  import java.text.MessageFormat;
47  import java.util.Collection;
48  import java.util.LinkedList;
49  
50  import org.eclipse.jgit.api.errors.CheckoutConflictException;
51  import org.eclipse.jgit.api.errors.GitAPIException;
52  import org.eclipse.jgit.api.errors.JGitInternalException;
53  import org.eclipse.jgit.dircache.DirCache;
54  import org.eclipse.jgit.dircache.DirCacheBuildIterator;
55  import org.eclipse.jgit.dircache.DirCacheBuilder;
56  import org.eclipse.jgit.dircache.DirCacheCheckout;
57  import org.eclipse.jgit.dircache.DirCacheEntry;
58  import org.eclipse.jgit.dircache.DirCacheIterator;
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.lib.Constants;
61  import org.eclipse.jgit.lib.NullProgressMonitor;
62  import org.eclipse.jgit.lib.ObjectId;
63  import org.eclipse.jgit.lib.ProgressMonitor;
64  import org.eclipse.jgit.lib.Ref;
65  import org.eclipse.jgit.lib.RefUpdate;
66  import org.eclipse.jgit.lib.Repository;
67  import org.eclipse.jgit.lib.RepositoryState;
68  import org.eclipse.jgit.revwalk.RevCommit;
69  import org.eclipse.jgit.revwalk.RevWalk;
70  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
71  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
72  import org.eclipse.jgit.treewalk.EmptyTreeIterator;
73  import org.eclipse.jgit.treewalk.TreeWalk;
74  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
75  
76  /**
77   * A class used to execute a {@code Reset} command. It has setters for all
78   * supported options and arguments of this command and a {@link #call()} method
79   * to finally execute the command. Each instance of this class should only be
80   * used for one invocation of the command (means: one call to {@link #call()})
81   *
82   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-reset.html"
83   *      >Git documentation about Reset</a>
84   */
85  public class ResetCommand extends GitCommand<Ref> {
86  
87  	/**
88  	 * Kind of reset
89  	 */
90  	public enum ResetType {
91  		/**
92  		 * Just change the ref, the index and workdir are not changed.
93  		 */
94  		SOFT,
95  
96  		/**
97  		 * Change the ref and the index, the workdir is not changed.
98  		 */
99  		MIXED,
100 
101 		/**
102 		 * Change the ref, the index and the workdir
103 		 */
104 		HARD,
105 
106 		/**
107 		 * Resets the index and updates the files in the working tree that are
108 		 * different between respective commit and HEAD, but keeps those which
109 		 * are different between the index and working tree
110 		 */
111 		MERGE, // TODO not implemented yet
112 
113 		/**
114 		 * Change the ref, the index and the workdir that are different between
115 		 * respective commit and HEAD
116 		 */
117 		KEEP // TODO not implemented yet
118 	}
119 
120 	// We need to be able to distinguish whether the caller set the ref
121 	// explicitly or not, so we apply the default (HEAD) only later.
122 	private String ref = null;
123 
124 	private ResetType mode;
125 
126 	private Collection<String> filepaths = new LinkedList<>();
127 
128 	private boolean isReflogDisabled;
129 
130 	private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
131 
132 	/**
133 	 * <p>
134 	 * Constructor for ResetCommand.
135 	 * </p>
136 	 *
137 	 * @param repo
138 	 *            the {@link org.eclipse.jgit.lib.Repository}
139 	 */
140 	public ResetCommand(Repository repo) {
141 		super(repo);
142 	}
143 
144 	/**
145 	 * {@inheritDoc}
146 	 * <p>
147 	 * Executes the {@code Reset} command. Each instance of this class should
148 	 * only be used for one invocation of the command. Don't call this method
149 	 * twice on an instance.
150 	 */
151 	@Override
152 	public Ref call() throws GitAPIException, CheckoutConflictException {
153 		checkCallable();
154 
155 		try {
156 			RepositoryState state = repo.getRepositoryState();
157 			final boolean merging = state.equals(RepositoryState.MERGING)
158 					|| state.equals(RepositoryState.MERGING_RESOLVED);
159 			final boolean cherryPicking = state
160 					.equals(RepositoryState.CHERRY_PICKING)
161 					|| state.equals(RepositoryState.CHERRY_PICKING_RESOLVED);
162 			final boolean reverting = state.equals(RepositoryState.REVERTING)
163 					|| state.equals(RepositoryState.REVERTING_RESOLVED);
164 
165 			final ObjectId commitId = resolveRefToCommitId();
166 			// When ref is explicitly specified, it has to resolve
167 			if (ref != null && commitId == null) {
168 				// @TODO throw an InvalidRefNameException. We can't do that
169 				// now because this would break the API
170 				throw new JGitInternalException(MessageFormat
171 						.format(JGitText.get().invalidRefName, ref));
172 			}
173 
174 			final ObjectId commitTree;
175 			if (commitId != null)
176 				commitTree = parseCommit(commitId).getTree();
177 			else
178 				commitTree = null;
179 
180 			if (!filepaths.isEmpty()) {
181 				// reset [commit] -- paths
182 				resetIndexForPaths(commitTree);
183 				setCallable(false);
184 				return repo.exactRef(Constants.HEAD);
185 			}
186 
187 			final Ref result;
188 			if (commitId != null) {
189 				// write the ref
190 				final RefUpdate ru = repo.updateRef(Constants.HEAD);
191 				ru.setNewObjectId(commitId);
192 
193 				String refName = Repository.shortenRefName(getRefOrHEAD());
194 				if (isReflogDisabled) {
195 					ru.disableRefLog();
196 				} else {
197 					String message = refName + ": updating " + Constants.HEAD; //$NON-NLS-1$
198 					ru.setRefLogMessage(message, false);
199 				}
200 				if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE)
201 					throw new JGitInternalException(MessageFormat.format(
202 							JGitText.get().cannotLock, ru.getName()));
203 
204 				ObjectId origHead = ru.getOldObjectId();
205 				if (origHead != null)
206 					repo.writeOrigHead(origHead);
207 			}
208 			result = repo.exactRef(Constants.HEAD);
209 
210 			if (mode == null)
211 				mode = ResetType.MIXED;
212 
213 			switch (mode) {
214 				case HARD:
215 					checkoutIndex(commitTree);
216 					break;
217 				case MIXED:
218 					resetIndex(commitTree);
219 					break;
220 				case SOFT: // do nothing, only the ref was changed
221 					break;
222 				case KEEP: // TODO
223 				case MERGE: // TODO
224 					throw new UnsupportedOperationException();
225 
226 			}
227 
228 			if (mode != ResetType.SOFT) {
229 				if (merging)
230 					resetMerge();
231 				else if (cherryPicking)
232 					resetCherryPick();
233 				else if (reverting)
234 					resetRevert();
235 				else if (repo.readSquashCommitMsg() != null)
236 					repo.writeSquashCommitMsg(null /* delete */);
237 			}
238 
239 			setCallable(false);
240 			return result;
241 		} catch (IOException e) {
242 			throw new JGitInternalException(MessageFormat.format(
243 					JGitText.get().exceptionCaughtDuringExecutionOfResetCommand,
244 					e.getMessage()), e);
245 		}
246 	}
247 
248 	private RevCommit parseCommit(ObjectId commitId) {
249 		try (RevWalkRevWalk.html#RevWalk">RevWalk rw = new RevWalk(repo)) {
250 			return rw.parseCommit(commitId);
251 		} catch (IOException e) {
252 			throw new JGitInternalException(MessageFormat.format(
253 					JGitText.get().cannotReadCommit, commitId.toString()), e);
254 		}
255 	}
256 
257 	private ObjectId resolveRefToCommitId() {
258 		try {
259 			return repo.resolve(getRefOrHEAD() + "^{commit}"); //$NON-NLS-1$
260 		} catch (IOException e) {
261 			throw new JGitInternalException(
262 					MessageFormat.format(JGitText.get().cannotRead, getRefOrHEAD()),
263 					e);
264 		}
265 	}
266 
267 	/**
268 	 * Set the name of the <code>Ref</code> to reset to
269 	 *
270 	 * @param ref
271 	 *            the ref to reset to, defaults to HEAD if not specified
272 	 * @return this instance
273 	 */
274 	public ResetCommand setRef(String ref) {
275 		this.ref = ref;
276 		return this;
277 	}
278 
279 	/**
280 	 * Set the reset mode
281 	 *
282 	 * @param mode
283 	 *            the mode of the reset command
284 	 * @return this instance
285 	 */
286 	public ResetCommand setMode(ResetType mode) {
287 		if (!filepaths.isEmpty())
288 			throw new JGitInternalException(MessageFormat.format(
289 					JGitText.get().illegalCombinationOfArguments,
290 					"[--mixed | --soft | --hard]", "<paths>...")); //$NON-NLS-1$ //$NON-NLS-2$
291 		this.mode = mode;
292 		return this;
293 	}
294 
295 	/**
296 	 * Repository relative path of file or directory to reset
297 	 *
298 	 * @param path
299 	 *            repository-relative path of file/directory to reset (with
300 	 *            <code>/</code> as separator)
301 	 * @return this instance
302 	 */
303 	public ResetCommand addPath(String path) {
304 		if (mode != null)
305 			throw new JGitInternalException(MessageFormat.format(
306 					JGitText.get().illegalCombinationOfArguments, "<paths>...", //$NON-NLS-1$
307 					"[--mixed | --soft | --hard]")); //$NON-NLS-1$
308 		filepaths.add(path);
309 		return this;
310 	}
311 
312 	/**
313 	 * Whether to disable reflog
314 	 *
315 	 * @param disable
316 	 *            if {@code true} disables writing a reflog entry for this reset
317 	 *            command
318 	 * @return this instance
319 	 * @since 4.5
320 	 */
321 	public ResetCommand disableRefLog(boolean disable) {
322 		this.isReflogDisabled = disable;
323 		return this;
324 	}
325 
326 	/**
327 	 * Whether reflog is disabled
328 	 *
329 	 * @return {@code true} if writing reflog is disabled for this reset command
330 	 * @since 4.5
331 	 */
332 	public boolean isReflogDisabled() {
333 		return this.isReflogDisabled;
334 	}
335 
336 	private String getRefOrHEAD() {
337 		if (ref != null)
338 			return ref;
339 		else
340 			return Constants.HEAD;
341 	}
342 
343 	/**
344 	 * The progress monitor associated with the reset operation. By default,
345 	 * this is set to <code>NullProgressMonitor</code>
346 	 *
347 	 * @see NullProgressMonitor
348 	 * @param monitor
349 	 *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
350 	 * @return {@code this}
351 	 * @since 4.11
352 	 */
353 	public ResetCommand setProgressMonitor(ProgressMonitor monitor) {
354 		if (monitor == null) {
355 			monitor = NullProgressMonitor.INSTANCE;
356 		}
357 		this.monitor = monitor;
358 		return this;
359 	}
360 
361 	private void resetIndexForPaths(ObjectId commitTree) {
362 		DirCache dc = null;
363 		try (TreeWalkTreeWalk.html#TreeWalk">TreeWalk tw = new TreeWalk(repo)) {
364 			dc = repo.lockDirCache();
365 			DirCacheBuilder builder = dc.builder();
366 
367 			tw.addTree(new DirCacheBuildIterator(builder));
368 			if (commitTree != null)
369 				tw.addTree(commitTree);
370 			else
371 				tw.addTree(new EmptyTreeIterator());
372 			tw.setFilter(PathFilterGroup.createFromStrings(filepaths));
373 			tw.setRecursive(true);
374 
375 			while (tw.next()) {
376 				final CanonicalTreeParser tree = tw.getTree(1,
377 						CanonicalTreeParser.class);
378 				// only keep file in index if it's in the commit
379 				if (tree != null) {
380 				    // revert index to commit
381 					DirCacheEntry entry = new DirCacheEntry(tw.getRawPath());
382 					entry.setFileMode(tree.getEntryFileMode());
383 					entry.setObjectId(tree.getEntryObjectId());
384 					builder.add(entry);
385 				}
386 			}
387 
388 			builder.commit();
389 		} catch (IOException e) {
390 			throw new RuntimeException(e);
391 		} finally {
392 			if (dc != null)
393 				dc.unlock();
394 		}
395 	}
396 
397 	private void resetIndex(ObjectId commitTree) throws IOException {
398 		DirCache dc = repo.lockDirCache();
399 		try (TreeWalkeeWalk.html#TreeWalk">TreeWalk walk = new TreeWalk(repo)) {
400 			DirCacheBuilder builder = dc.builder();
401 
402 			if (commitTree != null)
403 				walk.addTree(commitTree);
404 			else
405 				walk.addTree(new EmptyTreeIterator());
406 			walk.addTree(new DirCacheIterator(dc));
407 			walk.setRecursive(true);
408 
409 			while (walk.next()) {
410 				AbstractTreeIterator cIter = walk.getTree(0,
411 						AbstractTreeIterator.class);
412 				if (cIter == null) {
413 					// Not in commit, don't add to new index
414 					continue;
415 				}
416 
417 				final DirCacheEntryEntry.html#DirCacheEntry">DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
418 				entry.setFileMode(cIter.getEntryFileMode());
419 				entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
420 
421 				DirCacheIterator dcIter = walk.getTree(1,
422 						DirCacheIterator.class);
423 				if (dcIter != null && dcIter.idEqual(cIter)) {
424 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
425 					entry.setLastModified(indexEntry.getLastModified());
426 					entry.setLength(indexEntry.getLength());
427 				}
428 
429 				builder.add(entry);
430 			}
431 
432 			builder.commit();
433 		} finally {
434 			dc.unlock();
435 		}
436 	}
437 
438 	private void checkoutIndex(ObjectId commitTree) throws IOException,
439 			GitAPIException {
440 		DirCache dc = repo.lockDirCache();
441 		try {
442 			DirCacheCheckout checkout = new DirCacheCheckout(repo, dc,
443 					commitTree);
444 			checkout.setFailOnConflict(false);
445 			checkout.setProgressMonitor(monitor);
446 			try {
447 				checkout.checkout();
448 			} catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
449 				throw new CheckoutConflictException(checkout.getConflicts(),
450 						cce);
451 			}
452 		} finally {
453 			dc.unlock();
454 		}
455 	}
456 
457 	private void resetMerge() throws IOException {
458 		repo.writeMergeHeads(null);
459 		repo.writeMergeCommitMsg(null);
460 	}
461 
462 	private void resetCherryPick() throws IOException {
463 		repo.writeCherryPickHead(null);
464 		repo.writeMergeCommitMsg(null);
465 	}
466 
467 	private void resetRevert() throws IOException {
468 		repo.writeRevertHead(null);
469 		repo.writeMergeCommitMsg(null);
470 	}
471 
472 	/** {@inheritDoc} */
473 	@SuppressWarnings("nls")
474 	@Override
475 	public String toString() {
476 		return "ResetCommand [repo=" + repo + ", ref=" + ref + ", mode=" + mode
477 				+ ", isReflogDisabled=" + isReflogDisabled + ", filepaths="
478 				+ filepaths + "]";
479 	}
480 
481 }