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