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.ObjectId;
62  import org.eclipse.jgit.lib.Ref;
63  import org.eclipse.jgit.lib.RefUpdate;
64  import org.eclipse.jgit.lib.Repository;
65  import org.eclipse.jgit.lib.RepositoryState;
66  import org.eclipse.jgit.revwalk.RevCommit;
67  import org.eclipse.jgit.revwalk.RevWalk;
68  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
69  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
70  import org.eclipse.jgit.treewalk.EmptyTreeIterator;
71  import org.eclipse.jgit.treewalk.TreeWalk;
72  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
73  
74  /**
75   * A class used to execute a {@code Reset} command. It has setters for all
76   * supported options and arguments of this command and a {@link #call()} method
77   * to finally execute the command. Each instance of this class should only be
78   * used for one invocation of the command (means: one call to {@link #call()})
79   *
80   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-reset.html"
81   *      >Git documentation about Reset</a>
82   */
83  public class ResetCommand extends GitCommand<Ref> {
84  
85  	/**
86  	 * Kind of reset
87  	 */
88  	public enum ResetType {
89  		/**
90  		 * Just change the ref, the index and workdir are not changed.
91  		 */
92  		SOFT,
93  
94  		/**
95  		 * Change the ref and the index, the workdir is not changed.
96  		 */
97  		MIXED,
98  
99  		/**
100 		 * Change the ref, the index and the workdir
101 		 */
102 		HARD,
103 
104 		/**
105 		 * Resets the index and updates the files in the working tree that are
106 		 * different between respective commit and HEAD, but keeps those which
107 		 * are different between the index and working tree
108 		 */
109 		MERGE, // TODO not implemented yet
110 
111 		/**
112 		 * Change the ref, the index and the workdir that are different between
113 		 * respective commit and HEAD
114 		 */
115 		KEEP // TODO not implemented yet
116 	}
117 
118 	// We need to be able to distinguish whether the caller set the ref
119 	// explicitly or not, so we apply the default (HEAD) only later.
120 	private String ref = null;
121 
122 	private ResetType mode;
123 
124 	private Collection<String> filepaths = new LinkedList<>();
125 
126 	private boolean isReflogDisabled;
127 
128 	/**
129 	 * <p>
130 	 * Constructor for ResetCommand.
131 	 * </p>
132 	 *
133 	 * @param repo
134 	 *            the {@link org.eclipse.jgit.lib.Repository}
135 	 */
136 	public ResetCommand(Repository repo) {
137 		super(repo);
138 	}
139 
140 	/**
141 	 * {@inheritDoc}
142 	 * <p>
143 	 * Executes the {@code Reset} command. Each instance of this class should
144 	 * only be used for one invocation of the command. Don't call this method
145 	 * twice on an instance.
146 	 */
147 	@Override
148 	public Ref call() throws GitAPIException, CheckoutConflictException {
149 		checkCallable();
150 
151 		try {
152 			RepositoryState state = repo.getRepositoryState();
153 			final boolean merging = state.equals(RepositoryState.MERGING)
154 					|| state.equals(RepositoryState.MERGING_RESOLVED);
155 			final boolean cherryPicking = state
156 					.equals(RepositoryState.CHERRY_PICKING)
157 					|| state.equals(RepositoryState.CHERRY_PICKING_RESOLVED);
158 			final boolean reverting = state.equals(RepositoryState.REVERTING)
159 					|| state.equals(RepositoryState.REVERTING_RESOLVED);
160 
161 			final ObjectId commitId = resolveRefToCommitId();
162 			// When ref is explicitly specified, it has to resolve
163 			if (ref != null && commitId == null) {
164 				// @TODO throw an InvalidRefNameException. We can't do that
165 				// now because this would break the API
166 				throw new JGitInternalException(MessageFormat
167 						.format(JGitText.get().invalidRefName, ref));
168 			}
169 
170 			final ObjectId commitTree;
171 			if (commitId != null)
172 				commitTree = parseCommit(commitId).getTree();
173 			else
174 				commitTree = null;
175 
176 			if (!filepaths.isEmpty()) {
177 				// reset [commit] -- paths
178 				resetIndexForPaths(commitTree);
179 				setCallable(false);
180 				return repo.exactRef(Constants.HEAD);
181 			}
182 
183 			final Ref result;
184 			if (commitId != null) {
185 				// write the ref
186 				final RefUpdate ru = repo.updateRef(Constants.HEAD);
187 				ru.setNewObjectId(commitId);
188 
189 				String refName = Repository.shortenRefName(getRefOrHEAD());
190 				if (isReflogDisabled) {
191 					ru.disableRefLog();
192 				} else {
193 					String message = refName + ": updating " + Constants.HEAD; //$NON-NLS-1$
194 					ru.setRefLogMessage(message, false);
195 				}
196 				if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE)
197 					throw new JGitInternalException(MessageFormat.format(
198 							JGitText.get().cannotLock, ru.getName()));
199 
200 				ObjectId origHead = ru.getOldObjectId();
201 				if (origHead != null)
202 					repo.writeOrigHead(origHead);
203 			}
204 			result = repo.exactRef(Constants.HEAD);
205 
206 			if (mode == null)
207 				mode = ResetType.MIXED;
208 
209 			switch (mode) {
210 				case HARD:
211 					checkoutIndex(commitTree);
212 					break;
213 				case MIXED:
214 					resetIndex(commitTree);
215 					break;
216 				case SOFT: // do nothing, only the ref was changed
217 					break;
218 				case KEEP: // TODO
219 				case MERGE: // TODO
220 					throw new UnsupportedOperationException();
221 
222 			}
223 
224 			if (mode != ResetType.SOFT) {
225 				if (merging)
226 					resetMerge();
227 				else if (cherryPicking)
228 					resetCherryPick();
229 				else if (reverting)
230 					resetRevert();
231 				else if (repo.readSquashCommitMsg() != null)
232 					repo.writeSquashCommitMsg(null /* delete */);
233 			}
234 
235 			setCallable(false);
236 			return result;
237 		} catch (IOException e) {
238 			throw new JGitInternalException(MessageFormat.format(
239 					JGitText.get().exceptionCaughtDuringExecutionOfResetCommand,
240 					e.getMessage()), e);
241 		}
242 	}
243 
244 	private RevCommit parseCommit(final ObjectId commitId) {
245 		try (RevWalk rw = new RevWalk(repo)) {
246 			return rw.parseCommit(commitId);
247 		} catch (IOException e) {
248 			throw new JGitInternalException(MessageFormat.format(
249 					JGitText.get().cannotReadCommit, commitId.toString()), e);
250 		}
251 	}
252 
253 	private ObjectId resolveRefToCommitId() {
254 		try {
255 			return repo.resolve(getRefOrHEAD() + "^{commit}"); //$NON-NLS-1$
256 		} catch (IOException e) {
257 			throw new JGitInternalException(
258 					MessageFormat.format(JGitText.get().cannotRead, getRefOrHEAD()),
259 					e);
260 		}
261 	}
262 
263 	/**
264 	 * Set the name of the <code>Ref</code> to reset to
265 	 *
266 	 * @param ref
267 	 *            the ref to reset to, defaults to HEAD if not specified
268 	 * @return this instance
269 	 */
270 	public ResetCommand setRef(String ref) {
271 		this.ref = ref;
272 		return this;
273 	}
274 
275 	/**
276 	 * Set the reset mode
277 	 *
278 	 * @param mode
279 	 *            the mode of the reset command
280 	 * @return this instance
281 	 */
282 	public ResetCommand setMode(ResetType mode) {
283 		if (!filepaths.isEmpty())
284 			throw new JGitInternalException(MessageFormat.format(
285 					JGitText.get().illegalCombinationOfArguments,
286 					"[--mixed | --soft | --hard]", "<paths>...")); //$NON-NLS-1$ //$NON-NLS-2$
287 		this.mode = mode;
288 		return this;
289 	}
290 
291 	/**
292 	 * Repository relative path of file or directory to reset
293 	 *
294 	 * @param path
295 	 *            repository-relative path of file/directory to reset (with
296 	 *            <code>/</code> as separator)
297 	 * @return this instance
298 	 */
299 	public ResetCommand addPath(String path) {
300 		if (mode != null)
301 			throw new JGitInternalException(MessageFormat.format(
302 					JGitText.get().illegalCombinationOfArguments, "<paths>...", //$NON-NLS-1$
303 					"[--mixed | --soft | --hard]")); //$NON-NLS-1$
304 		filepaths.add(path);
305 		return this;
306 	}
307 
308 	/**
309 	 * Whether to disable reflog
310 	 *
311 	 * @param disable
312 	 *            if {@code true} disables writing a reflog entry for this reset
313 	 *            command
314 	 * @return this instance
315 	 * @since 4.5
316 	 */
317 	public ResetCommand disableRefLog(boolean disable) {
318 		this.isReflogDisabled = disable;
319 		return this;
320 	}
321 
322 	/**
323 	 * Whether reflog is disabled
324 	 *
325 	 * @return {@code true} if writing reflog is disabled for this reset command
326 	 * @since 4.5
327 	 */
328 	public boolean isReflogDisabled() {
329 		return this.isReflogDisabled;
330 	}
331 
332 	private String getRefOrHEAD() {
333 		if (ref != null)
334 			return ref;
335 		else
336 			return Constants.HEAD;
337 	}
338 
339 	private void resetIndexForPaths(ObjectId commitTree) {
340 		DirCache dc = null;
341 		try (final TreeWalk tw = new TreeWalk(repo)) {
342 			dc = repo.lockDirCache();
343 			DirCacheBuilder builder = dc.builder();
344 
345 			tw.addTree(new DirCacheBuildIterator(builder));
346 			if (commitTree != null)
347 				tw.addTree(commitTree);
348 			else
349 				tw.addTree(new EmptyTreeIterator());
350 			tw.setFilter(PathFilterGroup.createFromStrings(filepaths));
351 			tw.setRecursive(true);
352 
353 			while (tw.next()) {
354 				final CanonicalTreeParser tree = tw.getTree(1,
355 						CanonicalTreeParser.class);
356 				// only keep file in index if it's in the commit
357 				if (tree != null) {
358 				    // revert index to commit
359 					DirCacheEntry entry = new DirCacheEntry(tw.getRawPath());
360 					entry.setFileMode(tree.getEntryFileMode());
361 					entry.setObjectId(tree.getEntryObjectId());
362 					builder.add(entry);
363 				}
364 			}
365 
366 			builder.commit();
367 		} catch (IOException e) {
368 			throw new RuntimeException(e);
369 		} finally {
370 			if (dc != null)
371 				dc.unlock();
372 		}
373 	}
374 
375 	private void resetIndex(ObjectId commitTree) throws IOException {
376 		DirCache dc = repo.lockDirCache();
377 		try (TreeWalk walk = new TreeWalk(repo)) {
378 			DirCacheBuilder builder = dc.builder();
379 
380 			if (commitTree != null)
381 				walk.addTree(commitTree);
382 			else
383 				walk.addTree(new EmptyTreeIterator());
384 			walk.addTree(new DirCacheIterator(dc));
385 			walk.setRecursive(true);
386 
387 			while (walk.next()) {
388 				AbstractTreeIterator cIter = walk.getTree(0,
389 						AbstractTreeIterator.class);
390 				if (cIter == null) {
391 					// Not in commit, don't add to new index
392 					continue;
393 				}
394 
395 				final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
396 				entry.setFileMode(cIter.getEntryFileMode());
397 				entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
398 
399 				DirCacheIterator dcIter = walk.getTree(1,
400 						DirCacheIterator.class);
401 				if (dcIter != null && dcIter.idEqual(cIter)) {
402 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
403 					entry.setLastModified(indexEntry.getLastModified());
404 					entry.setLength(indexEntry.getLength());
405 				}
406 
407 				builder.add(entry);
408 			}
409 
410 			builder.commit();
411 		} finally {
412 			dc.unlock();
413 		}
414 	}
415 
416 	private void checkoutIndex(ObjectId commitTree) throws IOException,
417 			GitAPIException {
418 		DirCache dc = repo.lockDirCache();
419 		try {
420 			DirCacheCheckout checkout = new DirCacheCheckout(repo, dc,
421 					commitTree);
422 			checkout.setFailOnConflict(false);
423 			try {
424 				checkout.checkout();
425 			} catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
426 				throw new CheckoutConflictException(checkout.getConflicts(),
427 						cce);
428 			}
429 		} finally {
430 			dc.unlock();
431 		}
432 	}
433 
434 	private void resetMerge() throws IOException {
435 		repo.writeMergeHeads(null);
436 		repo.writeMergeCommitMsg(null);
437 	}
438 
439 	private void resetCherryPick() throws IOException {
440 		repo.writeCherryPickHead(null);
441 		repo.writeMergeCommitMsg(null);
442 	}
443 
444 	private void resetRevert() throws IOException {
445 		repo.writeRevertHead(null);
446 		repo.writeMergeCommitMsg(null);
447 	}
448 
449 	/** {@inheritDoc} */
450 	@SuppressWarnings("nls")
451 	@Override
452 	public String toString() {
453 		return "ResetCommand [repo=" + repo + ", ref=" + ref + ", mode=" + mode
454 				+ ", isReflogDisabled=" + isReflogDisabled + ", filepaths="
455 				+ filepaths + "]";
456 	}
457 
458 }