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.junit.Assert.assertEquals;
13  import static org.junit.Assert.assertFalse;
14  import static org.junit.Assert.assertNotNull;
15  import static org.junit.Assert.assertTrue;
16  import static org.junit.Assert.fail;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.util.Iterator;
21  
22  import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
23  import org.eclipse.jgit.api.ResetCommand.ResetType;
24  import org.eclipse.jgit.api.errors.GitAPIException;
25  import org.eclipse.jgit.api.errors.JGitInternalException;
26  import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
27  import org.eclipse.jgit.dircache.DirCache;
28  import org.eclipse.jgit.events.ChangeRecorder;
29  import org.eclipse.jgit.events.ListenerHandle;
30  import org.eclipse.jgit.junit.RepositoryTestCase;
31  import org.eclipse.jgit.lib.ConfigConstants;
32  import org.eclipse.jgit.lib.Constants;
33  import org.eclipse.jgit.lib.FileMode;
34  import org.eclipse.jgit.lib.ObjectId;
35  import org.eclipse.jgit.lib.ReflogReader;
36  import org.eclipse.jgit.lib.RepositoryState;
37  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
38  import org.eclipse.jgit.revwalk.RevCommit;
39  import org.junit.Test;
40  
41  /**
42   * Test cherry-pick command
43   */
44  public class CherryPickCommandTest extends RepositoryTestCase {
45  	@Test
46  	public void testCherryPick() throws IOException, JGitInternalException,
47  			GitAPIException {
48  		doTestCherryPick(false);
49  	}
50  
51  	@Test
52  	public void testCherryPickNoCommit() throws IOException,
53  			JGitInternalException, GitAPIException {
54  		doTestCherryPick(true);
55  	}
56  
57  	private void doTestCherryPick(boolean noCommit) throws IOException,
58  			JGitInternalException,
59  			GitAPIException {
60  		try (Git git = new Git(db)) {
61  			writeTrashFile("a", "first line\nsec. line\nthird line\n");
62  			git.add().addFilepattern("a").call();
63  			RevCommit firstCommit = git.commit().setMessage("create a").call();
64  
65  			writeTrashFile("b", "content\n");
66  			git.add().addFilepattern("b").call();
67  			git.commit().setMessage("create b").call();
68  
69  			writeTrashFile("a", "first line\nsec. line\nthird line\nfourth line\n");
70  			git.add().addFilepattern("a").call();
71  			git.commit().setMessage("enlarged a").call();
72  
73  			writeTrashFile("a",
74  					"first line\nsecond line\nthird line\nfourth line\n");
75  			git.add().addFilepattern("a").call();
76  			RevCommit fixingA = git.commit().setMessage("fixed a").call();
77  
78  			git.branchCreate().setName("side").setStartPoint(firstCommit).call();
79  			checkoutBranch("refs/heads/side");
80  
81  			writeTrashFile("a", "first line\nsec. line\nthird line\nfeature++\n");
82  			git.add().addFilepattern("a").call();
83  			git.commit().setMessage("enhanced a").call();
84  
85  			CherryPickResult pickResult = git.cherryPick().include(fixingA)
86  					.setNoCommit(noCommit).call();
87  
88  			assertEquals(CherryPickStatus.OK, pickResult.getStatus());
89  			assertFalse(new File(db.getWorkTree(), "b").exists());
90  			checkFile(new File(db.getWorkTree(), "a"),
91  					"first line\nsecond line\nthird line\nfeature++\n");
92  			Iterator<RevCommit> history = git.log().call().iterator();
93  			if (!noCommit)
94  				assertEquals("fixed a", history.next().getFullMessage());
95  			assertEquals("enhanced a", history.next().getFullMessage());
96  			assertEquals("create a", history.next().getFullMessage());
97  			assertFalse(history.hasNext());
98  		}
99  	}
100 
101     @Test
102     public void testSequentialCherryPick() throws IOException, JGitInternalException,
103             GitAPIException {
104         try (Git git = new Git(db)) {
105 	        writeTrashFile("a", "first line\nsec. line\nthird line\n");
106 	        git.add().addFilepattern("a").call();
107 	        RevCommit firstCommit = git.commit().setMessage("create a").call();
108 
109 	        writeTrashFile("a", "first line\nsec. line\nthird line\nfourth line\n");
110 	        git.add().addFilepattern("a").call();
111 	        RevCommit enlargingA = git.commit().setMessage("enlarged a").call();
112 
113 	        writeTrashFile("a",
114 	                "first line\nsecond line\nthird line\nfourth line\n");
115 	        git.add().addFilepattern("a").call();
116 	        RevCommit fixingA = git.commit().setMessage("fixed a").call();
117 
118 	        git.branchCreate().setName("side").setStartPoint(firstCommit).call();
119 	        checkoutBranch("refs/heads/side");
120 
121 	        writeTrashFile("b", "nothing to do with a");
122 	        git.add().addFilepattern("b").call();
123 	        git.commit().setMessage("create b").call();
124 
125 	        CherryPickResult result = git.cherryPick().include(enlargingA).include(fixingA).call();
126 	        assertEquals(CherryPickResult.CherryPickStatus.OK, result.getStatus());
127 
128 	        Iterator<RevCommit> history = git.log().call().iterator();
129 	        assertEquals("fixed a", history.next().getFullMessage());
130 	        assertEquals("enlarged a", history.next().getFullMessage());
131 	        assertEquals("create b", history.next().getFullMessage());
132 	        assertEquals("create a", history.next().getFullMessage());
133 	        assertFalse(history.hasNext());
134         }
135     }
136 
137 	@Test
138 	public void testCherryPickDirtyIndex() throws Exception {
139 		try (Git git = new Git(db)) {
140 			RevCommit sideCommit = prepareCherryPick(git);
141 
142 			// modify and add file a
143 			writeTrashFile("a", "a(modified)");
144 			git.add().addFilepattern("a").call();
145 			// do not commit
146 
147 			doCherryPickAndCheckResult(git, sideCommit,
148 					MergeFailureReason.DIRTY_INDEX);
149 		}
150 	}
151 
152 	@Test
153 	public void testCherryPickDirtyWorktree() throws Exception {
154 		try (Git git = new Git(db)) {
155 			RevCommit sideCommit = prepareCherryPick(git);
156 
157 			// modify file a
158 			writeTrashFile("a", "a(modified)");
159 			// do not add and commit
160 
161 			doCherryPickAndCheckResult(git, sideCommit,
162 					MergeFailureReason.DIRTY_WORKTREE);
163 		}
164 	}
165 
166 	@Test
167 	public void testCherryPickConflictResolution() throws Exception {
168 		try (Git git = new Git(db)) {
169 			RevCommit sideCommit = prepareCherryPick(git);
170 
171 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
172 					.call();
173 
174 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
175 			assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
176 			assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg());
177 			assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
178 					.exists());
179 			assertEquals(sideCommit.getId(), db.readCherryPickHead());
180 			assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
181 
182 			// Resolve
183 			writeTrashFile("a", "a");
184 			git.add().addFilepattern("a").call();
185 
186 			assertEquals(RepositoryState.CHERRY_PICKING_RESOLVED,
187 					db.getRepositoryState());
188 
189 			git.commit().setOnly("a").setMessage("resolve").call();
190 
191 			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
192 		}
193 	}
194 
195 	@Test
196 	public void testCherryPickConflictResolutionNoCOmmit() throws Exception {
197 		Git git = new Git(db);
198 		RevCommit sideCommit = prepareCherryPick(git);
199 
200 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
201 				.setNoCommit(true).call();
202 
203 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
204 		assertTrue(db.readDirCache().hasUnmergedPaths());
205 		String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
206 		assertEquals(expected, read("a"));
207 		assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
208 		assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg());
209 		assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
210 				.exists());
211 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
212 
213 		// Resolve
214 		writeTrashFile("a", "a");
215 		git.add().addFilepattern("a").call();
216 
217 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
218 
219 		git.commit().setOnly("a").setMessage("resolve").call();
220 
221 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
222 	}
223 
224 	@Test
225 	public void testCherryPickConflictReset() throws Exception {
226 		try (Git git = new Git(db)) {
227 			RevCommit sideCommit = prepareCherryPick(git);
228 
229 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
230 					.call();
231 
232 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
233 			assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
234 			assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
235 					.exists());
236 
237 			git.reset().setMode(ResetType.MIXED).setRef("HEAD").call();
238 
239 			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
240 			assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
241 					.exists());
242 		}
243 	}
244 
245 	@Test
246 	public void testCherryPickOverExecutableChangeOnNonExectuableFileSystem()
247 			throws Exception {
248 		try (Git git = new Git(db)) {
249 			File file = writeTrashFile("test.txt", "a");
250 			assertNotNull(git.add().addFilepattern("test.txt").call());
251 			assertNotNull(git.commit().setMessage("commit1").call());
252 
253 			assertNotNull(git.checkout().setCreateBranch(true).setName("a").call());
254 
255 			writeTrashFile("test.txt", "b");
256 			assertNotNull(git.add().addFilepattern("test.txt").call());
257 			RevCommit commit2 = git.commit().setMessage("commit2").call();
258 			assertNotNull(commit2);
259 
260 			assertNotNull(git.checkout().setName(Constants.MASTER).call());
261 
262 			DirCache cache = db.lockDirCache();
263 			cache.getEntry("test.txt").setFileMode(FileMode.EXECUTABLE_FILE);
264 			cache.write();
265 			assertTrue(cache.commit());
266 			cache.unlock();
267 
268 			assertNotNull(git.commit().setMessage("commit3").call());
269 
270 			db.getFS().setExecute(file, false);
271 			git.getRepository()
272 					.getConfig()
273 					.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
274 							ConfigConstants.CONFIG_KEY_FILEMODE, false);
275 
276 			CherryPickResult result = git.cherryPick().include(commit2).call();
277 			assertNotNull(result);
278 			assertEquals(CherryPickStatus.OK, result.getStatus());
279 		}
280 	}
281 
282 	@Test
283 	public void testCherryPickConflictMarkers() throws Exception {
284 		try (Git git = new Git(db)) {
285 			RevCommit sideCommit = prepareCherryPick(git);
286 
287 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
288 					.call();
289 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
290 
291 			String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
292 			checkFile(new File(db.getWorkTree(), "a"), expected);
293 		}
294 	}
295 
296 	@Test
297 	public void testCherryPickConflictFiresModifiedEvent() throws Exception {
298 		ListenerHandle listener = null;
299 		try (Git git = new Git(db)) {
300 			RevCommit sideCommit = prepareCherryPick(git);
301 			ChangeRecorder recorder = new ChangeRecorder();
302 			listener = db.getListenerList()
303 					.addWorkingTreeModifiedListener(recorder);
304 			CherryPickResult result = git.cherryPick()
305 					.include(sideCommit.getId()).call();
306 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
307 			recorder.assertEvent(new String[] { "a" }, ChangeRecorder.EMPTY);
308 		} finally {
309 			if (listener != null) {
310 				listener.remove();
311 			}
312 		}
313 	}
314 
315 	@Test
316 	public void testCherryPickNewFileFiresModifiedEvent() throws Exception {
317 		ListenerHandle listener = null;
318 		try (Git git = new Git(db)) {
319 			writeTrashFile("test.txt", "a");
320 			git.add().addFilepattern("test.txt").call();
321 			git.commit().setMessage("commit1").call();
322 			git.checkout().setCreateBranch(true).setName("a").call();
323 
324 			writeTrashFile("side.txt", "side");
325 			git.add().addFilepattern("side.txt").call();
326 			RevCommit side = git.commit().setMessage("side").call();
327 			assertNotNull(side);
328 
329 			assertNotNull(git.checkout().setName(Constants.MASTER).call());
330 			writeTrashFile("test.txt", "b");
331 			assertNotNull(git.add().addFilepattern("test.txt").call());
332 			assertNotNull(git.commit().setMessage("commit2").call());
333 
334 			ChangeRecorder recorder = new ChangeRecorder();
335 			listener = db.getListenerList()
336 					.addWorkingTreeModifiedListener(recorder);
337 			CherryPickResult result = git.cherryPick()
338 					.include(side.getId()).call();
339 			assertEquals(CherryPickStatus.OK, result.getStatus());
340 			recorder.assertEvent(new String[] { "side.txt" },
341 					ChangeRecorder.EMPTY);
342 		} finally {
343 			if (listener != null) {
344 				listener.remove();
345 			}
346 		}
347 	}
348 
349 	@Test
350 	public void testCherryPickOurCommitName() throws Exception {
351 		try (Git git = new Git(db)) {
352 			RevCommit sideCommit = prepareCherryPick(git);
353 
354 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
355 					.setOurCommitName("custom name").call();
356 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
357 
358 			String expected = "<<<<<<< custom name\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
359 			checkFile(new File(db.getWorkTree(), "a"), expected);
360 		}
361 	}
362 
363 	private RevCommit prepareCherryPick(Git git) throws Exception {
364 		// create, add and commit file a
365 		writeTrashFile("a", "a");
366 		git.add().addFilepattern("a").call();
367 		RevCommit firstMasterCommit = git.commit().setMessage("first master")
368 				.call();
369 
370 		// create and checkout side branch
371 		createBranch(firstMasterCommit, "refs/heads/side");
372 		checkoutBranch("refs/heads/side");
373 		// modify, add and commit file a
374 		writeTrashFile("a", "a(side)");
375 		git.add().addFilepattern("a").call();
376 		RevCommit sideCommit = git.commit().setMessage("side").call();
377 
378 		// checkout master branch
379 		checkoutBranch("refs/heads/master");
380 		// modify, add and commit file a
381 		writeTrashFile("a", "a(master)");
382 		git.add().addFilepattern("a").call();
383 		git.commit().setMessage("second master").call();
384 		return sideCommit;
385 	}
386 
387 	private void doCherryPickAndCheckResult(final Git git,
388 			final RevCommit sideCommit, final MergeFailureReason reason)
389 			throws Exception {
390 		// get current index state
391 		String indexState = indexState(CONTENT);
392 
393 		// cherry-pick
394 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
395 				.call();
396 		assertEquals(CherryPickStatus.FAILED, result.getStatus());
397 		// staged file a causes DIRTY_INDEX
398 		assertEquals(1, result.getFailingPaths().size());
399 		assertEquals(reason, result.getFailingPaths().get("a"));
400 		assertEquals("a(modified)", read(new File(db.getWorkTree(), "a")));
401 		// index shall be unchanged
402 		assertEquals(indexState, indexState(CONTENT));
403 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
404 
405 		if (reason == null) {
406 			ReflogReader reader = db.getReflogReader(Constants.HEAD);
407 			assertTrue(reader.getLastEntry().getComment()
408 					.startsWith("cherry-pick: "));
409 			reader = db.getReflogReader(db.getBranch());
410 			assertTrue(reader.getLastEntry().getComment()
411 					.startsWith("cherry-pick: "));
412 		}
413 	}
414 
415 	/**
416 	 * Cherry-picking merge commit M onto T
417 	 * <pre>
418 	 *    M
419 	 *    |\
420 	 *    C D
421 	 *    |/
422 	 * T  B
423 	 * | /
424 	 * A
425 	 * </pre>
426 	 * @throws Exception
427 	 */
428 	@Test
429 	public void testCherryPickMerge() throws Exception {
430 		try (Git git = new Git(db)) {
431 			commitFile("file", "1\n2\n3\n", "master");
432 			commitFile("file", "1\n2\n3\n", "side");
433 			checkoutBranch("refs/heads/side");
434 			RevCommit commitD = commitFile("file", "1\n2\n3\n4\n5\n", "side2");
435 			commitFile("file", "a\n2\n3\n", "side");
436 			MergeResult mergeResult = git.merge().include(commitD).call();
437 			ObjectId commitM = mergeResult.getNewHead();
438 			checkoutBranch("refs/heads/master");
439 			RevCommit commitT = commitFile("another", "t", "master");
440 
441 			try {
442 				git.cherryPick().include(commitM).call();
443 				fail("merges should not be cherry-picked by default");
444 			} catch (MultipleParentsNotAllowedException e) {
445 				// expected
446 			}
447 			try {
448 				git.cherryPick().include(commitM).setMainlineParentNumber(3).call();
449 				fail("specifying a non-existent parent should fail");
450 			} catch (JGitInternalException e) {
451 				// expected
452 				assertTrue(e.getMessage().endsWith(
453 						"does not have a parent number 3."));
454 			}
455 
456 			CherryPickResult result = git.cherryPick().include(commitM)
457 					.setMainlineParentNumber(1).call();
458 			assertEquals(CherryPickStatus.OK, result.getStatus());
459 			checkFile(new File(db.getWorkTree(), "file"), "1\n2\n3\n4\n5\n");
460 
461 			git.reset().setMode(ResetType.HARD).setRef(commitT.getName()).call();
462 
463 			CherryPickResult result2 = git.cherryPick().include(commitM)
464 					.setMainlineParentNumber(2).call();
465 			assertEquals(CherryPickStatus.OK, result2.getStatus());
466 			checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n");
467 		}
468 	}
469 }