View Javadoc
1   /*
2    * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.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.LinkedList;
48  import java.util.List;
49  import java.util.Map;
50  
51  import org.eclipse.jgit.api.MergeResult.MergeStatus;
52  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
53  import org.eclipse.jgit.api.errors.GitAPIException;
54  import org.eclipse.jgit.api.errors.JGitInternalException;
55  import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
56  import org.eclipse.jgit.api.errors.NoHeadException;
57  import org.eclipse.jgit.api.errors.NoMessageException;
58  import org.eclipse.jgit.api.errors.UnmergedPathsException;
59  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
60  import org.eclipse.jgit.dircache.DirCacheCheckout;
61  import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
62  import org.eclipse.jgit.internal.JGitText;
63  import org.eclipse.jgit.lib.AnyObjectId;
64  import org.eclipse.jgit.lib.Constants;
65  import org.eclipse.jgit.lib.NullProgressMonitor;
66  import org.eclipse.jgit.lib.ObjectId;
67  import org.eclipse.jgit.lib.ObjectIdRef;
68  import org.eclipse.jgit.lib.ProgressMonitor;
69  import org.eclipse.jgit.lib.Ref;
70  import org.eclipse.jgit.lib.Ref.Storage;
71  import org.eclipse.jgit.lib.Repository;
72  import org.eclipse.jgit.merge.MergeMessageFormatter;
73  import org.eclipse.jgit.merge.MergeStrategy;
74  import org.eclipse.jgit.merge.ResolveMerger;
75  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
76  import org.eclipse.jgit.revwalk.RevCommit;
77  import org.eclipse.jgit.revwalk.RevWalk;
78  import org.eclipse.jgit.treewalk.FileTreeIterator;
79  
80  /**
81   * A class used to execute a {@code revert} command. It has setters for all
82   * supported options and arguments of this command and a {@link #call()} method
83   * to finally execute the command. Each instance of this class should only be
84   * used for one invocation of the command (means: one call to {@link #call()})
85   *
86   * @see <a
87   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-revert.html"
88   *      >Git documentation about revert</a>
89   */
90  public class RevertCommand extends GitCommand<RevCommit> {
91  	private List<Ref> commits = new LinkedList<>();
92  
93  	private String ourCommitName = null;
94  
95  	private List<Ref> revertedRefs = new LinkedList<>();
96  
97  	private MergeResult failingResult;
98  
99  	private List<String> unmergedPaths;
100 
101 	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
102 
103 	private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
104 
105 	/**
106 	 * <p>
107 	 * Constructor for RevertCommand.
108 	 * </p>
109 	 *
110 	 * @param repo
111 	 *            the {@link org.eclipse.jgit.lib.Repository}
112 	 */
113 	protected RevertCommand(Repository repo) {
114 		super(repo);
115 	}
116 
117 	/**
118 	 * {@inheritDoc}
119 	 * <p>
120 	 * Executes the {@code revert} command with all the options and parameters
121 	 * collected by the setter methods (e.g. {@link #include(Ref)} of this
122 	 * class. Each instance of this class should only be used for one invocation
123 	 * of the command. Don't call this method twice on an instance.
124 	 */
125 	@Override
126 	public RevCommit call() throws NoMessageException, UnmergedPathsException,
127 			ConcurrentRefUpdateException, WrongRepositoryStateException,
128 			GitAPIException {
129 		RevCommit newHead = null;
130 		checkCallable();
131 
132 		try (RevWalklk.html#RevWalk">RevWalk revWalk = new RevWalk(repo)) {
133 
134 			// get the head commit
135 			Ref headRef = repo.exactRef(Constants.HEAD);
136 			if (headRef == null)
137 				throw new NoHeadException(
138 						JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
139 			RevCommit headCommit = revWalk.parseCommit(headRef.getObjectId());
140 
141 			newHead = headCommit;
142 
143 			// loop through all refs to be reverted
144 			for (Ref src : commits) {
145 				// get the commit to be reverted
146 				// handle annotated tags
147 				ObjectId srcObjectId = src.getPeeledObjectId();
148 				if (srcObjectId == null)
149 					srcObjectId = src.getObjectId();
150 				RevCommit srcCommit = revWalk.parseCommit(srcObjectId);
151 
152 				// get the parent of the commit to revert
153 				if (srcCommit.getParentCount() != 1)
154 					throw new MultipleParentsNotAllowedException(
155 							MessageFormat.format(
156 									JGitText.get().canOnlyRevertCommitsWithOneParent,
157 									srcCommit.name(),
158 									Integer.valueOf(srcCommit.getParentCount())));
159 
160 				RevCommit srcParent = srcCommit.getParent(0);
161 				revWalk.parseHeaders(srcParent);
162 
163 				String ourName = calculateOurName(headRef);
164 				String revertName = srcCommit.getId().abbreviate(7).name()
165 						+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$
166 
167 				ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
168 				merger.setWorkingTreeIterator(new FileTreeIterator(repo));
169 				merger.setBase(srcCommit.getTree());
170 				merger.setCommitNames(new String[] {
171 						"BASE", ourName, revertName }); //$NON-NLS-1$
172 
173 				String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$
174 						+ "\""; //$NON-NLS-1$
175 				String newMessage = shortMessage + "\n\n" //$NON-NLS-1$
176 						+ "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
177 						+ ".\n"; //$NON-NLS-1$
178 				if (merger.merge(headCommit, srcParent)) {
179 					if (!merger.getModifiedFiles().isEmpty()) {
180 						repo.fireEvent(new WorkingTreeModifiedEvent(
181 								merger.getModifiedFiles(), null));
182 					}
183 					if (AnyObjectId.isEqual(headCommit.getTree().getId(),
184 							merger.getResultTreeId()))
185 						continue;
186 					DirCacheCheckout dco = new DirCacheCheckout(repo,
187 							headCommit.getTree(), repo.lockDirCache(),
188 							merger.getResultTreeId());
189 					dco.setFailOnConflict(true);
190 					dco.setProgressMonitor(monitor);
191 					dco.checkout();
192 					try (Gitit.html#Git">Git git = new Git(getRepository())) {
193 						newHead = git.commit().setMessage(newMessage)
194 								.setReflogComment("revert: " + shortMessage) //$NON-NLS-1$
195 								.call();
196 					}
197 					revertedRefs.add(src);
198 					headCommit = newHead;
199 				} else {
200 					unmergedPaths = merger.getUnmergedPaths();
201 					Map<String, MergeFailureReason> failingPaths = merger
202 							.getFailingPaths();
203 					if (failingPaths != null)
204 						failingResult = new MergeResult(null,
205 								merger.getBaseCommitId(),
206 								new ObjectId[] { headCommit.getId(),
207 										srcParent.getId() },
208 								MergeStatus.FAILED, strategy,
209 								merger.getMergeResults(), failingPaths, null);
210 					else
211 						failingResult = new MergeResult(null,
212 								merger.getBaseCommitId(),
213 								new ObjectId[] { headCommit.getId(),
214 										srcParent.getId() },
215 								MergeStatus.CONFLICTING, strategy,
216 								merger.getMergeResults(), failingPaths, null);
217 					if (!merger.failed() && !unmergedPaths.isEmpty()) {
218 						String message = new MergeMessageFormatter()
219 						.formatWithConflicts(newMessage,
220 								merger.getUnmergedPaths());
221 						repo.writeRevertHead(srcCommit.getId());
222 						repo.writeMergeCommitMsg(message);
223 					}
224 					return null;
225 				}
226 			}
227 		} catch (IOException e) {
228 			throw new JGitInternalException(
229 					MessageFormat.format(
230 									JGitText.get().exceptionCaughtDuringExecutionOfRevertCommand,
231 							e), e);
232 		}
233 		return newHead;
234 	}
235 
236 	/**
237 	 * Include a {@code Ref} to a commit to be reverted
238 	 *
239 	 * @param commit
240 	 *            a reference to a commit to be reverted into the current head
241 	 * @return {@code this}
242 	 */
243 	public RevertCommand include(Ref commit) {
244 		checkCallable();
245 		commits.add(commit);
246 		return this;
247 	}
248 
249 	/**
250 	 * Include a commit to be reverted
251 	 *
252 	 * @param commit
253 	 *            the Id of a commit to be reverted into the current head
254 	 * @return {@code this}
255 	 */
256 	public RevertCommand include(AnyObjectId commit) {
257 		return include(commit.getName(), commit);
258 	}
259 
260 	/**
261 	 * Include a commit to be reverted
262 	 *
263 	 * @param name
264 	 *            name of a {@code Ref} referring to the commit
265 	 * @param commit
266 	 *            the Id of a commit which is reverted into the current head
267 	 * @return {@code this}
268 	 */
269 	public RevertCommand include(String name, AnyObjectId commit) {
270 		return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
271 				commit.copy()));
272 	}
273 
274 	/**
275 	 * Set the name to be used in the "OURS" place for conflict markers
276 	 *
277 	 * @param ourCommitName
278 	 *            the name that should be used in the "OURS" place for conflict
279 	 *            markers
280 	 * @return {@code this}
281 	 */
282 	public RevertCommand setOurCommitName(String ourCommitName) {
283 		this.ourCommitName = ourCommitName;
284 		return this;
285 	}
286 
287 	private String calculateOurName(Ref headRef) {
288 		if (ourCommitName != null)
289 			return ourCommitName;
290 
291 		String targetRefName = headRef.getTarget().getName();
292 		String headName = Repository.shortenRefName(targetRefName);
293 		return headName;
294 	}
295 
296 	/**
297 	 * Get the list of successfully reverted {@link org.eclipse.jgit.lib.Ref}'s.
298 	 *
299 	 * @return the list of successfully reverted
300 	 *         {@link org.eclipse.jgit.lib.Ref}'s. Never <code>null</code> but
301 	 *         maybe an empty list if no commit was successfully cherry-picked
302 	 */
303 	public List<Ref> getRevertedRefs() {
304 		return revertedRefs;
305 	}
306 
307 	/**
308 	 * Get the result of a merge failure
309 	 *
310 	 * @return the result of a merge failure, <code>null</code> if no merge
311 	 *         failure occurred during the revert
312 	 */
313 	public MergeResult getFailingResult() {
314 		return failingResult;
315 	}
316 
317 	/**
318 	 * Get unmerged paths
319 	 *
320 	 * @return the unmerged paths, will be null if no merge conflicts
321 	 */
322 	public List<String> getUnmergedPaths() {
323 		return unmergedPaths;
324 	}
325 
326 	/**
327 	 * Set the merge strategy to use for this revert command
328 	 *
329 	 * @param strategy
330 	 *            The merge strategy to use for this revert command.
331 	 * @return {@code this}
332 	 * @since 3.4
333 	 */
334 	public RevertCommand setStrategy(MergeStrategy strategy) {
335 		this.strategy = strategy;
336 		return this;
337 	}
338 
339 	/**
340 	 * The progress monitor associated with the revert operation. By default,
341 	 * this is set to <code>NullProgressMonitor</code>
342 	 *
343 	 * @see NullProgressMonitor
344 	 * @param monitor
345 	 *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
346 	 * @return {@code this}
347 	 * @since 4.11
348 	 */
349 	public RevertCommand setProgressMonitor(ProgressMonitor monitor) {
350 		if (monitor == null) {
351 			monitor = NullProgressMonitor.INSTANCE;
352 		}
353 		this.monitor = monitor;
354 		return this;
355 	}
356 }