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\nConflicts:\n\ta\n", db.readMergeCommitMsg());
179 			assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
180 					.exists());
181 			assertEquals(sideCommit.getId(), db.readCherryPickHead());
182 			assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
183 
184 			// Resolve
185 			writeTrashFile("a", "a");
186 			git.add().addFilepattern("a").call();
187 
188 			assertEquals(RepositoryState.CHERRY_PICKING_RESOLVED,
189 					db.getRepositoryState());
190 
191 			git.commit().setOnly("a").setMessage("resolve").call();
192 
193 			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
194 		}
195 	}
196 
197 	@Test
198 	public void testCherryPickConflictResolutionNoCommit() throws Exception {
199 		Git git = new Git(db);
200 		RevCommit sideCommit = prepareCherryPick(git);
201 
202 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
203 				.setNoCommit(true).call();
204 
205 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
206 		assertTrue(db.readDirCache().hasUnmergedPaths());
207 		String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
208 		assertEquals(expected, read("a"));
209 		assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
210 		assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg());
211 		assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
212 				.exists());
213 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
214 
215 		// Resolve
216 		writeTrashFile("a", "a");
217 		git.add().addFilepattern("a").call();
218 
219 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
220 
221 		git.commit().setOnly("a").setMessage("resolve").call();
222 
223 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
224 	}
225 
226 	@Test
227 	public void testCherryPickConflictReset() throws Exception {
228 		try (Git git = new Git(db)) {
229 			RevCommit sideCommit = prepareCherryPick(git);
230 
231 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
232 					.call();
233 
234 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
235 			assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
236 			assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
237 					.exists());
238 
239 			git.reset().setMode(ResetType.MIXED).setRef("HEAD").call();
240 
241 			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
242 			assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
243 					.exists());
244 		}
245 	}
246 
247 	@Test
248 	public void testCherryPickOverExecutableChangeOnNonExectuableFileSystem()
249 			throws Exception {
250 		try (Git git = new Git(db)) {
251 			File file = writeTrashFile("test.txt", "a");
252 			assertNotNull(git.add().addFilepattern("test.txt").call());
253 			assertNotNull(git.commit().setMessage("commit1").call());
254 
255 			assertNotNull(git.checkout().setCreateBranch(true).setName("a").call());
256 
257 			writeTrashFile("test.txt", "b");
258 			assertNotNull(git.add().addFilepattern("test.txt").call());
259 			RevCommit commit2 = git.commit().setMessage("commit2").call();
260 			assertNotNull(commit2);
261 
262 			assertNotNull(git.checkout().setName(Constants.MASTER).call());
263 
264 			DirCache cache = db.lockDirCache();
265 			cache.getEntry("test.txt").setFileMode(FileMode.EXECUTABLE_FILE);
266 			cache.write();
267 			assertTrue(cache.commit());
268 			cache.unlock();
269 
270 			assertNotNull(git.commit().setMessage("commit3").call());
271 
272 			db.getFS().setExecute(file, false);
273 			git.getRepository()
274 					.getConfig()
275 					.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
276 							ConfigConstants.CONFIG_KEY_FILEMODE, false);
277 
278 			CherryPickResult result = git.cherryPick().include(commit2).call();
279 			assertNotNull(result);
280 			assertEquals(CherryPickStatus.OK, result.getStatus());
281 		}
282 	}
283 
284 	@Test
285 	public void testCherryPickOurs() throws Exception {
286 		try (Git git = new Git(db)) {
287 			RevCommit sideCommit = prepareCherryPick(git);
288 
289 			CherryPickResult result = git.cherryPick()
290 					.include(sideCommit.getId())
291 					.setStrategy(MergeStrategy.OURS)
292 					.call();
293 			assertEquals(CherryPickStatus.OK, result.getStatus());
294 
295 			String expected = "a(master)";
296 			checkFile(new File(db.getWorkTree(), "a"), expected);
297 		}
298 	}
299 
300 	@Test
301 	public void testCherryPickTheirs() throws Exception {
302 		try (Git git = new Git(db)) {
303 			RevCommit sideCommit = prepareCherryPick(git);
304 
305 			CherryPickResult result = git.cherryPick()
306 					.include(sideCommit.getId())
307 					.setStrategy(MergeStrategy.THEIRS)
308 					.call();
309 			assertEquals(CherryPickStatus.OK, result.getStatus());
310 
311 			String expected = "a(side)";
312 			checkFile(new File(db.getWorkTree(), "a"), expected);
313 		}
314 	}
315 
316 	@Test
317 	public void testCherryPickXours() throws Exception {
318 		try (Git git = new Git(db)) {
319 			RevCommit sideCommit = prepareCherryPickStrategyOption(git);
320 
321 			CherryPickResult result = git.cherryPick()
322 					.include(sideCommit.getId())
323 					.setContentMergeStrategy(ContentMergeStrategy.OURS)
324 					.call();
325 			assertEquals(CherryPickStatus.OK, result.getStatus());
326 
327 			String expected = "a\nmaster\nc\nd\n";
328 			checkFile(new File(db.getWorkTree(), "a"), expected);
329 		}
330 	}
331 
332 	@Test
333 	public void testCherryPickXtheirs() throws Exception {
334 		try (Git git = new Git(db)) {
335 			RevCommit sideCommit = prepareCherryPickStrategyOption(git);
336 
337 			CherryPickResult result = git.cherryPick()
338 					.include(sideCommit.getId())
339 					.setContentMergeStrategy(ContentMergeStrategy.THEIRS)
340 					.call();
341 			assertEquals(CherryPickStatus.OK, result.getStatus());
342 
343 			String expected = "a\nside\nc\nd\n";
344 			checkFile(new File(db.getWorkTree(), "a"), expected);
345 		}
346 	}
347 
348 	@Test
349 	public void testCherryPickConflictMarkers() throws Exception {
350 		try (Git git = new Git(db)) {
351 			RevCommit sideCommit = prepareCherryPick(git);
352 
353 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
354 					.call();
355 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
356 
357 			String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
358 			checkFile(new File(db.getWorkTree(), "a"), expected);
359 		}
360 	}
361 
362 	@Test
363 	public void testCherryPickConflictFiresModifiedEvent() throws Exception {
364 		ListenerHandle listener = null;
365 		try (Git git = new Git(db)) {
366 			RevCommit sideCommit = prepareCherryPick(git);
367 			ChangeRecorder recorder = new ChangeRecorder();
368 			listener = db.getListenerList()
369 					.addWorkingTreeModifiedListener(recorder);
370 			CherryPickResult result = git.cherryPick()
371 					.include(sideCommit.getId()).call();
372 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
373 			recorder.assertEvent(new String[] { "a" }, ChangeRecorder.EMPTY);
374 		} finally {
375 			if (listener != null) {
376 				listener.remove();
377 			}
378 		}
379 	}
380 
381 	@Test
382 	public void testCherryPickNewFileFiresModifiedEvent() throws Exception {
383 		ListenerHandle listener = null;
384 		try (Git git = new Git(db)) {
385 			writeTrashFile("test.txt", "a");
386 			git.add().addFilepattern("test.txt").call();
387 			git.commit().setMessage("commit1").call();
388 			git.checkout().setCreateBranch(true).setName("a").call();
389 
390 			writeTrashFile("side.txt", "side");
391 			git.add().addFilepattern("side.txt").call();
392 			RevCommit side = git.commit().setMessage("side").call();
393 			assertNotNull(side);
394 
395 			assertNotNull(git.checkout().setName(Constants.MASTER).call());
396 			writeTrashFile("test.txt", "b");
397 			assertNotNull(git.add().addFilepattern("test.txt").call());
398 			assertNotNull(git.commit().setMessage("commit2").call());
399 
400 			ChangeRecorder recorder = new ChangeRecorder();
401 			listener = db.getListenerList()
402 					.addWorkingTreeModifiedListener(recorder);
403 			CherryPickResult result = git.cherryPick()
404 					.include(side.getId()).call();
405 			assertEquals(CherryPickStatus.OK, result.getStatus());
406 			recorder.assertEvent(new String[] { "side.txt" },
407 					ChangeRecorder.EMPTY);
408 		} finally {
409 			if (listener != null) {
410 				listener.remove();
411 			}
412 		}
413 	}
414 
415 	@Test
416 	public void testCherryPickOurCommitName() throws Exception {
417 		try (Git git = new Git(db)) {
418 			RevCommit sideCommit = prepareCherryPick(git);
419 
420 			CherryPickResult result = git.cherryPick().include(sideCommit.getId())
421 					.setOurCommitName("custom name").call();
422 			assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
423 
424 			String expected = "<<<<<<< custom name\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
425 			checkFile(new File(db.getWorkTree(), "a"), expected);
426 		}
427 	}
428 
429 	private RevCommit prepareCherryPick(Git git) throws Exception {
430 		// create, add and commit file a
431 		writeTrashFile("a", "a");
432 		git.add().addFilepattern("a").call();
433 		RevCommit firstMasterCommit = git.commit().setMessage("first master")
434 				.call();
435 
436 		// create and checkout side branch
437 		createBranch(firstMasterCommit, "refs/heads/side");
438 		checkoutBranch("refs/heads/side");
439 		// modify, add and commit file a
440 		writeTrashFile("a", "a(side)");
441 		git.add().addFilepattern("a").call();
442 		RevCommit sideCommit = git.commit().setMessage("side").call();
443 
444 		// checkout master branch
445 		checkoutBranch("refs/heads/master");
446 		// modify, add and commit file a
447 		writeTrashFile("a", "a(master)");
448 		git.add().addFilepattern("a").call();
449 		git.commit().setMessage("second master").call();
450 		return sideCommit;
451 	}
452 
453 	private RevCommit prepareCherryPickStrategyOption(Git git)
454 			throws Exception {
455 		// create, add and commit file a
456 		writeTrashFile("a", "a\nb\nc\n");
457 		git.add().addFilepattern("a").call();
458 		RevCommit firstMasterCommit = git.commit().setMessage("first master")
459 				.call();
460 
461 		// create and checkout side branch
462 		createBranch(firstMasterCommit, "refs/heads/side");
463 		checkoutBranch("refs/heads/side");
464 		// modify, add and commit file a
465 		writeTrashFile("a", "a\nside\nc\nd\n");
466 		git.add().addFilepattern("a").call();
467 		RevCommit sideCommit = git.commit().setMessage("side").call();
468 
469 		// checkout master branch
470 		checkoutBranch("refs/heads/master");
471 		// modify, add and commit file a
472 		writeTrashFile("a", "a\nmaster\nc\n");
473 		git.add().addFilepattern("a").call();
474 		git.commit().setMessage("second master").call();
475 		return sideCommit;
476 	}
477 
478 	private void doCherryPickAndCheckResult(final Git git,
479 			final RevCommit sideCommit, final MergeFailureReason reason)
480 			throws Exception {
481 		// get current index state
482 		String indexState = indexState(CONTENT);
483 
484 		// cherry-pick
485 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
486 				.call();
487 		assertEquals(CherryPickStatus.FAILED, result.getStatus());
488 		// staged file a causes DIRTY_INDEX
489 		assertEquals(1, result.getFailingPaths().size());
490 		assertEquals(reason, result.getFailingPaths().get("a"));
491 		assertEquals("a(modified)", read(new File(db.getWorkTree(), "a")));
492 		// index shall be unchanged
493 		assertEquals(indexState, indexState(CONTENT));
494 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
495 
496 		if (reason == null) {
497 			ReflogReader reader = db.getReflogReader(Constants.HEAD);
498 			assertTrue(reader.getLastEntry().getComment()
499 					.startsWith("cherry-pick: "));
500 			reader = db.getReflogReader(db.getBranch());
501 			assertTrue(reader.getLastEntry().getComment()
502 					.startsWith("cherry-pick: "));
503 		}
504 	}
505 
506 	/**
507 	 * Cherry-picking merge commit M onto T
508 	 * <pre>
509 	 *    M
510 	 *    |\
511 	 *    C D
512 	 *    |/
513 	 * T  B
514 	 * | /
515 	 * A
516 	 * </pre>
517 	 * @throws Exception
518 	 */
519 	@Test
520 	public void testCherryPickMerge() throws Exception {
521 		try (Git git = new Git(db)) {
522 			commitFile("file", "1\n2\n3\n", "master");
523 			commitFile("file", "1\n2\n3\n", "side");
524 			checkoutBranch("refs/heads/side");
525 			RevCommit commitD = commitFile("file", "1\n2\n3\n4\n5\n", "side2");
526 			commitFile("file", "a\n2\n3\n", "side");
527 			MergeResult mergeResult = git.merge().include(commitD).call();
528 			ObjectId commitM = mergeResult.getNewHead();
529 			checkoutBranch("refs/heads/master");
530 			RevCommit commitT = commitFile("another", "t", "master");
531 
532 			try {
533 				git.cherryPick().include(commitM).call();
534 				fail("merges should not be cherry-picked by default");
535 			} catch (MultipleParentsNotAllowedException e) {
536 				// expected
537 			}
538 			try {
539 				git.cherryPick().include(commitM).setMainlineParentNumber(3).call();
540 				fail("specifying a non-existent parent should fail");
541 			} catch (JGitInternalException e) {
542 				// expected
543 				assertTrue(e.getMessage().endsWith(
544 						"does not have a parent number 3."));
545 			}
546 
547 			CherryPickResult result = git.cherryPick().include(commitM)
548 					.setMainlineParentNumber(1).call();
549 			assertEquals(CherryPickStatus.OK, result.getStatus());
550 			checkFile(new File(db.getWorkTree(), "file"), "1\n2\n3\n4\n5\n");
551 
552 			git.reset().setMode(ResetType.HARD).setRef(commitT.getName()).call();
553 
554 			CherryPickResult result2 = git.cherryPick().include(commitM)
555 					.setMainlineParentNumber(2).call();
556 			assertEquals(CherryPickStatus.OK, result2.getStatus());
557 			checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n");
558 		}
559 	}
560 }