View Javadoc
1   /*
2    * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.api;
44  
45  import static org.junit.Assert.assertEquals;
46  import static org.junit.Assert.assertFalse;
47  import static org.junit.Assert.assertNotNull;
48  import static org.junit.Assert.assertTrue;
49  import static org.junit.Assert.fail;
50  
51  import java.io.File;
52  import java.io.IOException;
53  import java.util.Iterator;
54  
55  import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
56  import org.eclipse.jgit.api.ResetCommand.ResetType;
57  import org.eclipse.jgit.api.errors.GitAPIException;
58  import org.eclipse.jgit.api.errors.JGitInternalException;
59  import org.eclipse.jgit.api.errors.MultipleParentsNotAllowedException;
60  import org.eclipse.jgit.dircache.DirCache;
61  import org.eclipse.jgit.junit.RepositoryTestCase;
62  import org.eclipse.jgit.lib.ConfigConstants;
63  import org.eclipse.jgit.lib.Constants;
64  import org.eclipse.jgit.lib.FileMode;
65  import org.eclipse.jgit.lib.ObjectId;
66  import org.eclipse.jgit.lib.ReflogReader;
67  import org.eclipse.jgit.lib.RepositoryState;
68  import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
69  import org.eclipse.jgit.revwalk.RevCommit;
70  import org.junit.Test;
71  
72  /**
73   * Test cherry-pick command
74   */
75  public class CherryPickCommandTest extends RepositoryTestCase {
76  	@Test
77  	public void testCherryPick() throws IOException, JGitInternalException,
78  			GitAPIException {
79  		doTestCherryPick(false);
80  	}
81  
82  	@Test
83  	public void testCherryPickNoCommit() throws IOException,
84  			JGitInternalException, GitAPIException {
85  		doTestCherryPick(true);
86  	}
87  
88  	private void doTestCherryPick(boolean noCommit) throws IOException,
89  			JGitInternalException,
90  			GitAPIException {
91  		Git git = new Git(db);
92  
93  		writeTrashFile("a", "first line\nsec. line\nthird line\n");
94  		git.add().addFilepattern("a").call();
95  		RevCommit firstCommit = git.commit().setMessage("create a").call();
96  
97  		writeTrashFile("b", "content\n");
98  		git.add().addFilepattern("b").call();
99  		git.commit().setMessage("create b").call();
100 
101 		writeTrashFile("a", "first line\nsec. line\nthird line\nfourth line\n");
102 		git.add().addFilepattern("a").call();
103 		git.commit().setMessage("enlarged a").call();
104 
105 		writeTrashFile("a",
106 				"first line\nsecond line\nthird line\nfourth line\n");
107 		git.add().addFilepattern("a").call();
108 		RevCommit fixingA = git.commit().setMessage("fixed a").call();
109 
110 		git.branchCreate().setName("side").setStartPoint(firstCommit).call();
111 		checkoutBranch("refs/heads/side");
112 
113 		writeTrashFile("a", "first line\nsec. line\nthird line\nfeature++\n");
114 		git.add().addFilepattern("a").call();
115 		git.commit().setMessage("enhanced a").call();
116 
117 		CherryPickResult pickResult = git.cherryPick().include(fixingA)
118 				.setNoCommit(noCommit).call();
119 
120 		assertEquals(CherryPickStatus.OK, pickResult.getStatus());
121 		assertFalse(new File(db.getWorkTree(), "b").exists());
122 		checkFile(new File(db.getWorkTree(), "a"),
123 				"first line\nsecond line\nthird line\nfeature++\n");
124 		Iterator<RevCommit> history = git.log().call().iterator();
125 		if (!noCommit)
126 			assertEquals("fixed a", history.next().getFullMessage());
127 		assertEquals("enhanced a", history.next().getFullMessage());
128 		assertEquals("create a", history.next().getFullMessage());
129 		assertFalse(history.hasNext());
130 	}
131 
132     @Test
133     public void testSequentialCherryPick() throws IOException, JGitInternalException,
134             GitAPIException {
135         Git git = new Git(db);
136 
137         writeTrashFile("a", "first line\nsec. line\nthird line\n");
138         git.add().addFilepattern("a").call();
139         RevCommit firstCommit = git.commit().setMessage("create a").call();
140 
141         writeTrashFile("a", "first line\nsec. line\nthird line\nfourth line\n");
142         git.add().addFilepattern("a").call();
143         RevCommit enlargingA = git.commit().setMessage("enlarged a").call();
144 
145         writeTrashFile("a",
146                 "first line\nsecond line\nthird line\nfourth line\n");
147         git.add().addFilepattern("a").call();
148         RevCommit fixingA = git.commit().setMessage("fixed a").call();
149 
150         git.branchCreate().setName("side").setStartPoint(firstCommit).call();
151         checkoutBranch("refs/heads/side");
152 
153         writeTrashFile("b", "nothing to do with a");
154         git.add().addFilepattern("b").call();
155         git.commit().setMessage("create b").call();
156 
157         CherryPickResult result = git.cherryPick().include(enlargingA).include(fixingA).call();
158         assertEquals(CherryPickResult.CherryPickStatus.OK, result.getStatus());
159 
160         Iterator<RevCommit> history = git.log().call().iterator();
161         assertEquals("fixed a", history.next().getFullMessage());
162         assertEquals("enlarged a", history.next().getFullMessage());
163         assertEquals("create b", history.next().getFullMessage());
164         assertEquals("create a", history.next().getFullMessage());
165         assertFalse(history.hasNext());
166     }
167 
168 	@Test
169 	public void testCherryPickDirtyIndex() throws Exception {
170 		Git git = new Git(db);
171 		RevCommit sideCommit = prepareCherryPick(git);
172 
173 		// modify and add file a
174 		writeTrashFile("a", "a(modified)");
175 		git.add().addFilepattern("a").call();
176 		// do not commit
177 
178 		doCherryPickAndCheckResult(git, sideCommit,
179 				MergeFailureReason.DIRTY_INDEX);
180 	}
181 
182 	@Test
183 	public void testCherryPickDirtyWorktree() throws Exception {
184 		Git git = new Git(db);
185 		RevCommit sideCommit = prepareCherryPick(git);
186 
187 		// modify file a
188 		writeTrashFile("a", "a(modified)");
189 		// do not add and commit
190 
191 		doCherryPickAndCheckResult(git, sideCommit,
192 				MergeFailureReason.DIRTY_WORKTREE);
193 	}
194 
195 	@Test
196 	public void testCherryPickConflictResolution() throws Exception {
197 		Git git = new Git(db);
198 		RevCommit sideCommit = prepareCherryPick(git);
199 
200 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
201 				.call();
202 
203 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
204 		assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
205 		assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg());
206 		assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
207 				.exists());
208 		assertEquals(sideCommit.getId(), db.readCherryPickHead());
209 		assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
210 
211 		// Resolve
212 		writeTrashFile("a", "a");
213 		git.add().addFilepattern("a").call();
214 
215 		assertEquals(RepositoryState.CHERRY_PICKING_RESOLVED,
216 				db.getRepositoryState());
217 
218 		git.commit().setOnly("a").setMessage("resolve").call();
219 
220 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
221 	}
222 
223 	@Test
224 	public void testCherryPickConflictResolutionNoCOmmit() throws Exception {
225 		Git git = new Git(db);
226 		RevCommit sideCommit = prepareCherryPick(git);
227 
228 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
229 				.setNoCommit(true).call();
230 
231 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
232 		assertTrue(db.readDirCache().hasUnmergedPaths());
233 		String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
234 		assertEquals(expected, read("a"));
235 		assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists());
236 		assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg());
237 		assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
238 				.exists());
239 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
240 
241 		// Resolve
242 		writeTrashFile("a", "a");
243 		git.add().addFilepattern("a").call();
244 
245 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
246 
247 		git.commit().setOnly("a").setMessage("resolve").call();
248 
249 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
250 	}
251 
252 	@Test
253 	public void testCherryPickConflictReset() throws Exception {
254 		Git git = new Git(db);
255 
256 		RevCommit sideCommit = prepareCherryPick(git);
257 
258 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
259 				.call();
260 
261 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
262 		assertEquals(RepositoryState.CHERRY_PICKING, db.getRepositoryState());
263 		assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
264 				.exists());
265 
266 		git.reset().setMode(ResetType.MIXED).setRef("HEAD").call();
267 
268 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
269 		assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD)
270 				.exists());
271 	}
272 
273 	@Test
274 	public void testCherryPickOverExecutableChangeOnNonExectuableFileSystem()
275 			throws Exception {
276 		Git git = new Git(db);
277 		File file = writeTrashFile("test.txt", "a");
278 		assertNotNull(git.add().addFilepattern("test.txt").call());
279 		assertNotNull(git.commit().setMessage("commit1").call());
280 
281 		assertNotNull(git.checkout().setCreateBranch(true).setName("a").call());
282 
283 		writeTrashFile("test.txt", "b");
284 		assertNotNull(git.add().addFilepattern("test.txt").call());
285 		RevCommit commit2 = git.commit().setMessage("commit2").call();
286 		assertNotNull(commit2);
287 
288 		assertNotNull(git.checkout().setName(Constants.MASTER).call());
289 
290 		DirCache cache = db.lockDirCache();
291 		cache.getEntry("test.txt").setFileMode(FileMode.EXECUTABLE_FILE);
292 		cache.write();
293 		assertTrue(cache.commit());
294 		cache.unlock();
295 
296 		assertNotNull(git.commit().setMessage("commit3").call());
297 
298 		db.getFS().setExecute(file, false);
299 		git.getRepository()
300 				.getConfig()
301 				.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
302 						ConfigConstants.CONFIG_KEY_FILEMODE, false);
303 
304 		CherryPickResult result = git.cherryPick().include(commit2).call();
305 		assertNotNull(result);
306 		assertEquals(CherryPickStatus.OK, result.getStatus());
307 	}
308 
309 	@Test
310 	public void testCherryPickConflictMarkers() throws Exception {
311 		Git git = new Git(db);
312 		RevCommit sideCommit = prepareCherryPick(git);
313 
314 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
315 				.call();
316 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
317 
318 		String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
319 		checkFile(new File(db.getWorkTree(), "a"), expected);
320 	}
321 
322 	@Test
323 	public void testCherryPickOurCommitName() throws Exception {
324 		Git git = new Git(db);
325 		RevCommit sideCommit = prepareCherryPick(git);
326 
327 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
328 				.setOurCommitName("custom name").call();
329 		assertEquals(CherryPickStatus.CONFLICTING, result.getStatus());
330 
331 		String expected = "<<<<<<< custom name\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n";
332 		checkFile(new File(db.getWorkTree(), "a"), expected);
333 	}
334 
335 	private RevCommit prepareCherryPick(final Git git) throws Exception {
336 		// create, add and commit file a
337 		writeTrashFile("a", "a");
338 		git.add().addFilepattern("a").call();
339 		RevCommit firstMasterCommit = git.commit().setMessage("first master")
340 				.call();
341 
342 		// create and checkout side branch
343 		createBranch(firstMasterCommit, "refs/heads/side");
344 		checkoutBranch("refs/heads/side");
345 		// modify, add and commit file a
346 		writeTrashFile("a", "a(side)");
347 		git.add().addFilepattern("a").call();
348 		RevCommit sideCommit = git.commit().setMessage("side").call();
349 
350 		// checkout master branch
351 		checkoutBranch("refs/heads/master");
352 		// modify, add and commit file a
353 		writeTrashFile("a", "a(master)");
354 		git.add().addFilepattern("a").call();
355 		git.commit().setMessage("second master").call();
356 		return sideCommit;
357 	}
358 
359 	private void doCherryPickAndCheckResult(final Git git,
360 			final RevCommit sideCommit, final MergeFailureReason reason)
361 			throws Exception {
362 		// get current index state
363 		String indexState = indexState(CONTENT);
364 
365 		// cherry-pick
366 		CherryPickResult result = git.cherryPick().include(sideCommit.getId())
367 				.call();
368 		assertEquals(CherryPickStatus.FAILED, result.getStatus());
369 		// staged file a causes DIRTY_INDEX
370 		assertEquals(1, result.getFailingPaths().size());
371 		assertEquals(reason, result.getFailingPaths().get("a"));
372 		assertEquals("a(modified)", read(new File(db.getWorkTree(), "a")));
373 		// index shall be unchanged
374 		assertEquals(indexState, indexState(CONTENT));
375 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
376 
377 		if (reason == null) {
378 			ReflogReader reader = db.getReflogReader(Constants.HEAD);
379 			assertTrue(reader.getLastEntry().getComment()
380 					.startsWith("cherry-pick: "));
381 			reader = db.getReflogReader(db.getBranch());
382 			assertTrue(reader.getLastEntry().getComment()
383 					.startsWith("cherry-pick: "));
384 		}
385 	}
386 
387 	/**
388 	 * Cherry-picking merge commit M onto T
389 	 * <pre>
390 	 *    M
391 	 *    |\
392 	 *    C D
393 	 *    |/
394 	 * T  B
395 	 * | /
396 	 * A
397 	 * </pre>
398 	 * @throws Exception
399 	 */
400 	@Test
401 	public void testCherryPickMerge() throws Exception {
402 		Git git = new Git(db);
403 
404 		commitFile("file", "1\n2\n3\n", "master");
405 		commitFile("file", "1\n2\n3\n", "side");
406 		checkoutBranch("refs/heads/side");
407 		RevCommit commitD = commitFile("file", "1\n2\n3\n4\n5\n", "side2");
408 		commitFile("file", "a\n2\n3\n", "side");
409 		MergeResult mergeResult = git.merge().include(commitD).call();
410 		ObjectId commitM = mergeResult.getNewHead();
411 		checkoutBranch("refs/heads/master");
412 		RevCommit commitT = commitFile("another", "t", "master");
413 
414 		try {
415 			git.cherryPick().include(commitM).call();
416 			fail("merges should not be cherry-picked by default");
417 		} catch (MultipleParentsNotAllowedException e) {
418 			// expected
419 		}
420 		try {
421 			git.cherryPick().include(commitM).setMainlineParentNumber(3).call();
422 			fail("specifying a non-existent parent should fail");
423 		} catch (JGitInternalException e) {
424 			// expected
425 			assertTrue(e.getMessage().endsWith(
426 					"does not have a parent number 3."));
427 		}
428 
429 		CherryPickResult result = git.cherryPick().include(commitM)
430 				.setMainlineParentNumber(1).call();
431 		assertEquals(CherryPickStatus.OK, result.getStatus());
432 		checkFile(new File(db.getWorkTree(), "file"), "1\n2\n3\n4\n5\n");
433 
434 		git.reset().setMode(ResetType.HARD).setRef(commitT.getName()).call();
435 
436 		CherryPickResult result2 = git.cherryPick().include(commitM)
437 				.setMainlineParentNumber(2).call();
438 		assertEquals(CherryPickStatus.OK, result2.getStatus());
439 		checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n");
440 	}
441 }