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