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