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