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