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