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<>();
89  
90  	private String ourCommitName = null;
91  
92  	private List<Ref> revertedRefs = new LinkedList<>();
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 	@Override
124 	public RevCommit call() throws NoMessageException, UnmergedPathsException,
125 			ConcurrentRefUpdateException, WrongRepositoryStateException,
126 			GitAPIException {
127 		RevCommit newHead = null;
128 		checkCallable();
129 
130 		try (RevWalk revWalk = new RevWalk(repo)) {
131 
132 			// get the head commit
133 			Ref headRef = repo.exactRef(Constants.HEAD);
134 			if (headRef == null)
135 				throw new NoHeadException(
136 						JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported);
137 			RevCommit headCommit = revWalk.parseCommit(headRef.getObjectId());
138 
139 			newHead = headCommit;
140 
141 			// loop through all refs to be reverted
142 			for (Ref src : commits) {
143 				// get the commit to be reverted
144 				// handle annotated tags
145 				ObjectId srcObjectId = src.getPeeledObjectId();
146 				if (srcObjectId == null)
147 					srcObjectId = src.getObjectId();
148 				RevCommit srcCommit = revWalk.parseCommit(srcObjectId);
149 
150 				// get the parent of the commit to revert
151 				if (srcCommit.getParentCount() != 1)
152 					throw new MultipleParentsNotAllowedException(
153 							MessageFormat.format(
154 									JGitText.get().canOnlyRevertCommitsWithOneParent,
155 									srcCommit.name(),
156 									Integer.valueOf(srcCommit.getParentCount())));
157 
158 				RevCommit srcParent = srcCommit.getParent(0);
159 				revWalk.parseHeaders(srcParent);
160 
161 				String ourName = calculateOurName(headRef);
162 				String revertName = srcCommit.getId().abbreviate(7).name()
163 						+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$
164 
165 				ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
166 				merger.setWorkingTreeIterator(new FileTreeIterator(repo));
167 				merger.setBase(srcCommit.getTree());
168 				merger.setCommitNames(new String[] {
169 						"BASE", ourName, revertName }); //$NON-NLS-1$
170 
171 				String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$
172 						+ "\""; //$NON-NLS-1$
173 				String newMessage = shortMessage + "\n\n" //$NON-NLS-1$
174 						+ "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
175 						+ ".\n"; //$NON-NLS-1$
176 				if (merger.merge(headCommit, srcParent)) {
177 					if (AnyObjectId.equals(headCommit.getTree().getId(), merger
178 							.getResultTreeId()))
179 						continue;
180 					DirCacheCheckout dco = new DirCacheCheckout(repo,
181 							headCommit.getTree(), repo.lockDirCache(),
182 							merger.getResultTreeId());
183 					dco.setFailOnConflict(true);
184 					dco.checkout();
185 					try (Git git = new Git(getRepository())) {
186 						newHead = git.commit().setMessage(newMessage)
187 								.setReflogComment("revert: " + shortMessage) //$NON-NLS-1$
188 								.call();
189 					}
190 					revertedRefs.add(src);
191 					headCommit = newHead;
192 				} else {
193 					unmergedPaths = merger.getUnmergedPaths();
194 					Map<String, MergeFailureReason> failingPaths = merger
195 							.getFailingPaths();
196 					if (failingPaths != null)
197 						failingResult = new MergeResult(null,
198 								merger.getBaseCommitId(),
199 								new ObjectId[] { headCommit.getId(),
200 										srcParent.getId() },
201 								MergeStatus.FAILED, strategy,
202 								merger.getMergeResults(), failingPaths, null);
203 					else
204 						failingResult = new MergeResult(null,
205 								merger.getBaseCommitId(),
206 								new ObjectId[] { headCommit.getId(),
207 										srcParent.getId() },
208 								MergeStatus.CONFLICTING, strategy,
209 								merger.getMergeResults(), failingPaths, null);
210 					if (!merger.failed() && !unmergedPaths.isEmpty()) {
211 						String message = new MergeMessageFormatter()
212 						.formatWithConflicts(newMessage,
213 								merger.getUnmergedPaths());
214 						repo.writeRevertHead(srcCommit.getId());
215 						repo.writeMergeCommitMsg(message);
216 					}
217 					return null;
218 				}
219 			}
220 		} catch (IOException e) {
221 			throw new JGitInternalException(
222 					MessageFormat.format(
223 									JGitText.get().exceptionCaughtDuringExecutionOfRevertCommand,
224 							e), e);
225 		}
226 		return newHead;
227 	}
228 
229 	/**
230 	 * @param commit
231 	 *            a reference to a commit which is reverted into the current
232 	 *            head
233 	 * @return {@code this}
234 	 */
235 	public RevertCommand include(Ref commit) {
236 		checkCallable();
237 		commits.add(commit);
238 		return this;
239 	}
240 
241 	/**
242 	 * @param commit
243 	 *            the Id of a commit which is reverted into the current head
244 	 * @return {@code this}
245 	 */
246 	public RevertCommand include(AnyObjectId commit) {
247 		return include(commit.getName(), commit);
248 	}
249 
250 	/**
251 	 * @param name
252 	 *            a name given to the commit
253 	 * @param commit
254 	 *            the Id of a commit which is reverted into the current head
255 	 * @return {@code this}
256 	 */
257 	public RevertCommand include(String name, AnyObjectId commit) {
258 		return include(new ObjectIdRef.Unpeeled(Storage.LOOSE, name,
259 				commit.copy()));
260 	}
261 
262 	/**
263 	 * @param ourCommitName
264 	 *            the name that should be used in the "OURS" place for conflict
265 	 *            markers
266 	 * @return {@code this}
267 	 */
268 	public RevertCommand setOurCommitName(String ourCommitName) {
269 		this.ourCommitName = ourCommitName;
270 		return this;
271 	}
272 
273 	private String calculateOurName(Ref headRef) {
274 		if (ourCommitName != null)
275 			return ourCommitName;
276 
277 		String targetRefName = headRef.getTarget().getName();
278 		String headName = Repository.shortenRefName(targetRefName);
279 		return headName;
280 	}
281 
282 	/**
283 	 * @return the list of successfully reverted {@link Ref}'s. Never
284 	 *         <code>null</code> but maybe an empty list if no commit was
285 	 *         successfully cherry-picked
286 	 */
287 	public List<Ref> getRevertedRefs() {
288 		return revertedRefs;
289 	}
290 
291 	/**
292 	 * @return the result of the merge failure, <code>null</code> if no merge
293 	 *         failure occurred during the revert
294 	 */
295 	public MergeResult getFailingResult() {
296 		return failingResult;
297 	}
298 
299 	/**
300 	 * @return the unmerged paths, will be null if no merge conflicts
301 	 */
302 	public List<String> getUnmergedPaths() {
303 		return unmergedPaths;
304 	}
305 
306 	/**
307 	 * @param strategy
308 	 *            The merge strategy to use during this revert command.
309 	 * @return {@code this}
310 	 * @since 3.4
311 	 */
312 	public RevertCommand setStrategy(MergeStrategy strategy) {
313 		this.strategy = strategy;
314 		return this;
315 	}
316 }