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 	 *
130 	 * @param repo
131 	 */
132 	public ResetCommand(Repository repo) {
133 		super(repo);
134 	}
135 
136 	/**
137 	 * Executes the {@code Reset} command. Each instance of this class should
138 	 * only be used for one invocation of the command. Don't call this method
139 	 * twice on an instance.
140 	 *
141 	 * @return the Ref after reset
142 	 * @throws GitAPIException
143 	 */
144 	@Override
145 	public Ref call() throws GitAPIException, CheckoutConflictException {
146 		checkCallable();
147 
148 		try {
149 			RepositoryState state = repo.getRepositoryState();
150 			final boolean merging = state.equals(RepositoryState.MERGING)
151 					|| state.equals(RepositoryState.MERGING_RESOLVED);
152 			final boolean cherryPicking = state
153 					.equals(RepositoryState.CHERRY_PICKING)
154 					|| state.equals(RepositoryState.CHERRY_PICKING_RESOLVED);
155 			final boolean reverting = state.equals(RepositoryState.REVERTING)
156 					|| state.equals(RepositoryState.REVERTING_RESOLVED);
157 
158 			final ObjectId commitId = resolveRefToCommitId();
159 			// When ref is explicitly specified, it has to resolve
160 			if (ref != null && commitId == null) {
161 				// @TODO throw an InvalidRefNameException. We can't do that
162 				// now because this would break the API
163 				throw new JGitInternalException(MessageFormat
164 						.format(JGitText.get().invalidRefName, ref));
165 			}
166 
167 			final ObjectId commitTree;
168 			if (commitId != null)
169 				commitTree = parseCommit(commitId).getTree();
170 			else
171 				commitTree = null;
172 
173 			if (!filepaths.isEmpty()) {
174 				// reset [commit] -- paths
175 				resetIndexForPaths(commitTree);
176 				setCallable(false);
177 				return repo.exactRef(Constants.HEAD);
178 			}
179 
180 			final Ref result;
181 			if (commitId != null) {
182 				// write the ref
183 				final RefUpdate ru = repo.updateRef(Constants.HEAD);
184 				ru.setNewObjectId(commitId);
185 
186 				String refName = Repository.shortenRefName(getRefOrHEAD());
187 				if (isReflogDisabled) {
188 					ru.disableRefLog();
189 				} else {
190 					String message = refName + ": updating " + Constants.HEAD; //$NON-NLS-1$
191 					ru.setRefLogMessage(message, false);
192 				}
193 				if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE)
194 					throw new JGitInternalException(MessageFormat.format(
195 							JGitText.get().cannotLock, ru.getName()));
196 
197 				ObjectId origHead = ru.getOldObjectId();
198 				if (origHead != null)
199 					repo.writeOrigHead(origHead);
200 			}
201 			result = repo.exactRef(Constants.HEAD);
202 
203 			if (mode == null)
204 				mode = ResetType.MIXED;
205 
206 			switch (mode) {
207 				case HARD:
208 					checkoutIndex(commitTree);
209 					break;
210 				case MIXED:
211 					resetIndex(commitTree);
212 					break;
213 				case SOFT: // do nothing, only the ref was changed
214 					break;
215 				case KEEP: // TODO
216 				case MERGE: // TODO
217 					throw new UnsupportedOperationException();
218 
219 			}
220 
221 			if (mode != ResetType.SOFT) {
222 				if (merging)
223 					resetMerge();
224 				else if (cherryPicking)
225 					resetCherryPick();
226 				else if (reverting)
227 					resetRevert();
228 				else if (repo.readSquashCommitMsg() != null)
229 					repo.writeSquashCommitMsg(null /* delete */);
230 			}
231 
232 			setCallable(false);
233 			return result;
234 		} catch (IOException e) {
235 			throw new JGitInternalException(MessageFormat.format(
236 					JGitText.get().exceptionCaughtDuringExecutionOfResetCommand,
237 					e.getMessage()), e);
238 		}
239 	}
240 
241 	private RevCommit parseCommit(final ObjectId commitId) {
242 		try (RevWalk rw = new RevWalk(repo)) {
243 			return rw.parseCommit(commitId);
244 		} catch (IOException e) {
245 			throw new JGitInternalException(MessageFormat.format(
246 					JGitText.get().cannotReadCommit, commitId.toString()), e);
247 		}
248 	}
249 
250 	private ObjectId resolveRefToCommitId() {
251 		try {
252 			return repo.resolve(getRefOrHEAD() + "^{commit}"); //$NON-NLS-1$
253 		} catch (IOException e) {
254 			throw new JGitInternalException(
255 					MessageFormat.format(JGitText.get().cannotRead, getRefOrHEAD()),
256 					e);
257 		}
258 	}
259 
260 	/**
261 	 * @param ref
262 	 *            the ref to reset to, defaults to HEAD if not specified
263 	 * @return this instance
264 	 */
265 	public ResetCommand setRef(String ref) {
266 		this.ref = ref;
267 		return this;
268 	}
269 
270 	/**
271 	 * @param mode
272 	 *            the mode of the reset command
273 	 * @return this instance
274 	 */
275 	public ResetCommand setMode(ResetType mode) {
276 		if (!filepaths.isEmpty())
277 			throw new JGitInternalException(MessageFormat.format(
278 					JGitText.get().illegalCombinationOfArguments,
279 					"[--mixed | --soft | --hard]", "<paths>...")); //$NON-NLS-1$ //$NON-NLS-2$
280 		this.mode = mode;
281 		return this;
282 	}
283 
284 	/**
285 	 * @param path
286 	 *            repository-relative path of file/directory to reset (with
287 	 *            <code>/</code> as separator)
288 	 * @return this instance
289 	 */
290 	public ResetCommand addPath(String path) {
291 		if (mode != null)
292 			throw new JGitInternalException(MessageFormat.format(
293 					JGitText.get().illegalCombinationOfArguments, "<paths>...", //$NON-NLS-1$
294 					"[--mixed | --soft | --hard]")); //$NON-NLS-1$
295 		filepaths.add(path);
296 		return this;
297 	}
298 
299 	/**
300 	 * @param disable
301 	 *            if {@code true} disables writing a reflog entry for this reset
302 	 *            command
303 	 * @return this instance
304 	 * @since 4.5
305 	 */
306 	public ResetCommand disableRefLog(boolean disable) {
307 		this.isReflogDisabled = disable;
308 		return this;
309 	}
310 
311 	/**
312 	 * @return {@code true} if writing reflog is disabled for this reset command
313 	 * @since 4.5
314 	 */
315 	public boolean isReflogDisabled() {
316 		return this.isReflogDisabled;
317 	}
318 
319 	private String getRefOrHEAD() {
320 		if (ref != null)
321 			return ref;
322 		else
323 			return Constants.HEAD;
324 	}
325 
326 	private void resetIndexForPaths(ObjectId commitTree) {
327 		DirCache dc = null;
328 		try (final TreeWalk tw = new TreeWalk(repo)) {
329 			dc = repo.lockDirCache();
330 			DirCacheBuilder builder = dc.builder();
331 
332 			tw.addTree(new DirCacheBuildIterator(builder));
333 			if (commitTree != null)
334 				tw.addTree(commitTree);
335 			else
336 				tw.addTree(new EmptyTreeIterator());
337 			tw.setFilter(PathFilterGroup.createFromStrings(filepaths));
338 			tw.setRecursive(true);
339 
340 			while (tw.next()) {
341 				final CanonicalTreeParser tree = tw.getTree(1,
342 						CanonicalTreeParser.class);
343 				// only keep file in index if it's in the commit
344 				if (tree != null) {
345 				    // revert index to commit
346 					DirCacheEntry entry = new DirCacheEntry(tw.getRawPath());
347 					entry.setFileMode(tree.getEntryFileMode());
348 					entry.setObjectId(tree.getEntryObjectId());
349 					builder.add(entry);
350 				}
351 			}
352 
353 			builder.commit();
354 		} catch (IOException e) {
355 			throw new RuntimeException(e);
356 		} finally {
357 			if (dc != null)
358 				dc.unlock();
359 		}
360 	}
361 
362 	private void resetIndex(ObjectId commitTree) throws IOException {
363 		DirCache dc = repo.lockDirCache();
364 		try (TreeWalk walk = new TreeWalk(repo)) {
365 			DirCacheBuilder builder = dc.builder();
366 
367 			if (commitTree != null)
368 				walk.addTree(commitTree);
369 			else
370 				walk.addTree(new EmptyTreeIterator());
371 			walk.addTree(new DirCacheIterator(dc));
372 			walk.setRecursive(true);
373 
374 			while (walk.next()) {
375 				AbstractTreeIterator cIter = walk.getTree(0,
376 						AbstractTreeIterator.class);
377 				if (cIter == null) {
378 					// Not in commit, don't add to new index
379 					continue;
380 				}
381 
382 				final DirCacheEntry entry = new DirCacheEntry(walk.getRawPath());
383 				entry.setFileMode(cIter.getEntryFileMode());
384 				entry.setObjectIdFromRaw(cIter.idBuffer(), cIter.idOffset());
385 
386 				DirCacheIterator dcIter = walk.getTree(1,
387 						DirCacheIterator.class);
388 				if (dcIter != null && dcIter.idEqual(cIter)) {
389 					DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
390 					entry.setLastModified(indexEntry.getLastModified());
391 					entry.setLength(indexEntry.getLength());
392 				}
393 
394 				builder.add(entry);
395 			}
396 
397 			builder.commit();
398 		} finally {
399 			dc.unlock();
400 		}
401 	}
402 
403 	private void checkoutIndex(ObjectId commitTree) throws IOException,
404 			GitAPIException {
405 		DirCache dc = repo.lockDirCache();
406 		try {
407 			DirCacheCheckout checkout = new DirCacheCheckout(repo, dc,
408 					commitTree);
409 			checkout.setFailOnConflict(false);
410 			try {
411 				checkout.checkout();
412 			} catch (org.eclipse.jgit.errors.CheckoutConflictException cce) {
413 				throw new CheckoutConflictException(checkout.getConflicts(),
414 						cce);
415 			}
416 		} finally {
417 			dc.unlock();
418 		}
419 	}
420 
421 	private void resetMerge() throws IOException {
422 		repo.writeMergeHeads(null);
423 		repo.writeMergeCommitMsg(null);
424 	}
425 
426 	private void resetCherryPick() throws IOException {
427 		repo.writeCherryPickHead(null);
428 		repo.writeMergeCommitMsg(null);
429 	}
430 
431 	private void resetRevert() throws IOException {
432 		repo.writeRevertHead(null);
433 		repo.writeMergeCommitMsg(null);
434 	}
435 
436 	@SuppressWarnings("nls")
437 	@Override
438 	public String toString() {
439 		return "ResetCommand [repo=" + repo + ", ref=" + ref + ", mode=" + mode
440 				+ ", isReflogDisabled=" + isReflogDisabled + ", filepaths="
441 				+ filepaths + "]";
442 	}
443 
444 }