View Javadoc
1   /*
2    * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@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.assertNull;
49  import static org.junit.Assert.assertTrue;
50  
51  import java.io.ByteArrayOutputStream;
52  import java.io.File;
53  import java.io.FileInputStream;
54  import java.io.FileOutputStream;
55  import java.io.IOException;
56  import java.util.concurrent.Callable;
57  
58  import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
59  import org.eclipse.jgit.api.MergeResult.MergeStatus;
60  import org.eclipse.jgit.api.errors.NoHeadException;
61  import org.eclipse.jgit.junit.RepositoryTestCase;
62  import org.eclipse.jgit.lib.Constants;
63  import org.eclipse.jgit.lib.ObjectId;
64  import org.eclipse.jgit.lib.RefUpdate;
65  import org.eclipse.jgit.lib.Repository;
66  import org.eclipse.jgit.lib.RepositoryState;
67  import org.eclipse.jgit.lib.StoredConfig;
68  import org.eclipse.jgit.revwalk.RevCommit;
69  import org.eclipse.jgit.revwalk.RevSort;
70  import org.eclipse.jgit.revwalk.RevWalk;
71  import org.eclipse.jgit.transport.RefSpec;
72  import org.eclipse.jgit.transport.RemoteConfig;
73  import org.eclipse.jgit.transport.URIish;
74  import org.junit.Before;
75  import org.junit.Test;
76  
77  public class PullCommandTest extends RepositoryTestCase {
78  	/** Second Test repository */
79  	protected Repository dbTarget;
80  
81  	private Git source;
82  
83  	private Git target;
84  
85  	private File sourceFile;
86  
87  	private File targetFile;
88  
89  	@Test
90  	public void testPullFastForward() throws Exception {
91  		PullResult res = target.pull().call();
92  		// nothing to update since we don't have different data yet
93  		assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty());
94  		assertTrue(res.getMergeResult().getMergeStatus().equals(
95  				MergeStatus.ALREADY_UP_TO_DATE));
96  
97  		assertFileContentsEqual(targetFile, "Hello world");
98  
99  		// change the source file
100 		writeToFile(sourceFile, "Another change");
101 		source.add().addFilepattern("SomeFile.txt").call();
102 		source.commit().setMessage("Some change in remote").call();
103 
104 		res = target.pull().call();
105 
106 		assertFalse(res.getFetchResult().getTrackingRefUpdates().isEmpty());
107 		assertEquals(res.getMergeResult().getMergeStatus(),
108 				MergeStatus.FAST_FORWARD);
109 		assertFileContentsEqual(targetFile, "Another change");
110 		assertEquals(RepositoryState.SAFE, target.getRepository()
111 				.getRepositoryState());
112 
113 		res = target.pull().call();
114 		assertEquals(res.getMergeResult().getMergeStatus(),
115 				MergeStatus.ALREADY_UP_TO_DATE);
116 	}
117 
118 	@Test
119 	public void testPullMerge() throws Exception {
120 		PullResult res = target.pull().call();
121 		// nothing to update since we don't have different data yet
122 		assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty());
123 		assertTrue(res.getMergeResult().getMergeStatus()
124 				.equals(MergeStatus.ALREADY_UP_TO_DATE));
125 
126 		writeToFile(sourceFile, "Source change");
127 		source.add().addFilepattern("SomeFile.txt");
128 		RevCommit sourceCommit = source.commit()
129 				.setMessage("Source change in remote").call();
130 
131 		File targetFile2 = new File(dbTarget.getWorkTree(), "OtherFile.txt");
132 		writeToFile(targetFile2, "Unconflicting change");
133 		target.add().addFilepattern("OtherFile.txt").call();
134 		RevCommit targetCommit = target.commit()
135 				.setMessage("Unconflicting change in local").call();
136 
137 		res = target.pull().call();
138 
139 		MergeResult mergeResult = res.getMergeResult();
140 		ObjectId[] mergedCommits = mergeResult.getMergedCommits();
141 		assertEquals(targetCommit.getId(), mergedCommits[0]);
142 		assertEquals(sourceCommit.getId(), mergedCommits[1]);
143 		RevCommit mergeCommit = new RevWalk(dbTarget).parseCommit(mergeResult
144 				.getNewHead());
145 		String message = "Merge branch 'master' of "
146 				+ db.getWorkTree().getAbsolutePath();
147 		assertEquals(message, mergeCommit.getShortMessage());
148 	}
149 
150 	@Test
151 	public void testPullConflict() throws Exception {
152 		PullResult res = target.pull().call();
153 		// nothing to update since we don't have different data yet
154 		assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty());
155 		assertTrue(res.getMergeResult().getMergeStatus().equals(
156 				MergeStatus.ALREADY_UP_TO_DATE));
157 
158 		assertFileContentsEqual(targetFile, "Hello world");
159 
160 		// change the source file
161 		writeToFile(sourceFile, "Source change");
162 		source.add().addFilepattern("SomeFile.txt").call();
163 		source.commit().setMessage("Source change in remote").call();
164 
165 		// change the target file
166 		writeToFile(targetFile, "Target change");
167 		target.add().addFilepattern("SomeFile.txt").call();
168 		target.commit().setMessage("Target change in local").call();
169 
170 		res = target.pull().call();
171 
172 		String sourceChangeString = "Source change\n>>>>>>> branch 'master' of "
173 				+ target.getRepository().getConfig().getString("remote",
174 						"origin", "url");
175 
176 		assertFalse(res.getFetchResult().getTrackingRefUpdates().isEmpty());
177 		assertEquals(res.getMergeResult().getMergeStatus(),
178 				MergeStatus.CONFLICTING);
179 		String result = "<<<<<<< HEAD\nTarget change\n=======\n"
180 				+ sourceChangeString + "\n";
181 		assertFileContentsEqual(targetFile, result);
182 		assertEquals(RepositoryState.MERGING, target.getRepository()
183 				.getRepositoryState());
184 	}
185 
186 	@Test
187 	public void testPullLocalConflict() throws Exception {
188 		target.branchCreate().setName("basedOnMaster").setStartPoint(
189 				"refs/heads/master").setUpstreamMode(SetupUpstreamMode.TRACK)
190 				.call();
191 		target.getRepository().updateRef(Constants.HEAD).link(
192 				"refs/heads/basedOnMaster");
193 		PullResult res = target.pull().call();
194 		// nothing to update since we don't have different data yet
195 		assertNull(res.getFetchResult());
196 		assertTrue(res.getMergeResult().getMergeStatus().equals(
197 				MergeStatus.ALREADY_UP_TO_DATE));
198 
199 		assertFileContentsEqual(targetFile, "Hello world");
200 
201 		// change the file in master
202 		target.getRepository().updateRef(Constants.HEAD).link(
203 				"refs/heads/master");
204 		writeToFile(targetFile, "Master change");
205 		target.add().addFilepattern("SomeFile.txt").call();
206 		target.commit().setMessage("Source change in master").call();
207 
208 		// change the file in slave
209 		target.getRepository().updateRef(Constants.HEAD).link(
210 				"refs/heads/basedOnMaster");
211 		writeToFile(targetFile, "Slave change");
212 		target.add().addFilepattern("SomeFile.txt").call();
213 		target.commit().setMessage("Source change in based on master").call();
214 
215 		res = target.pull().call();
216 
217 		String sourceChangeString = "Master change\n>>>>>>> branch 'master' of local repository";
218 
219 		assertNull(res.getFetchResult());
220 		assertEquals(res.getMergeResult().getMergeStatus(),
221 				MergeStatus.CONFLICTING);
222 		String result = "<<<<<<< HEAD\nSlave change\n=======\n"
223 				+ sourceChangeString + "\n";
224 		assertFileContentsEqual(targetFile, result);
225 		assertEquals(RepositoryState.MERGING, target.getRepository()
226 				.getRepositoryState());
227 	}
228 
229 	@Test(expected = NoHeadException.class)
230 	public void testPullEmptyRepository() throws Exception {
231 		Repository empty = createWorkRepository();
232 		RefUpdate delete = empty.updateRef(Constants.HEAD, true);
233 		delete.setForceUpdate(true);
234 		delete.delete();
235 		Git.wrap(empty).pull().call();
236 	}
237 
238 	@Test
239 	public void testPullMergeProgrammaticConfiguration() throws Exception {
240 		// create another commit on another branch in source
241 		source.checkout().setCreateBranch(true).setName("other").call();
242 		sourceFile = new File(db.getWorkTree(), "file2.txt");
243 		writeToFile(sourceFile, "content");
244 		source.add().addFilepattern("file2.txt").call();
245 		RevCommit sourceCommit = source.commit()
246 				.setMessage("source commit on branch other").call();
247 
248 		File targetFile2 = new File(dbTarget.getWorkTree(), "OtherFile.txt");
249 		writeToFile(targetFile2, "Unconflicting change");
250 		target.add().addFilepattern("OtherFile.txt").call();
251 		RevCommit targetCommit = target.commit()
252 				.setMessage("Unconflicting change in local").call();
253 
254 		PullResult res = target.pull().setRemote("origin")
255 				.setRemoteBranchName("other")
256 				.setRebase(false).call();
257 
258 		MergeResult mergeResult = res.getMergeResult();
259 		ObjectId[] mergedCommits = mergeResult.getMergedCommits();
260 		assertEquals(targetCommit.getId(), mergedCommits[0]);
261 		assertEquals(sourceCommit.getId(), mergedCommits[1]);
262 		RevCommit mergeCommit = new RevWalk(dbTarget).parseCommit(mergeResult
263 				.getNewHead());
264 		String message = "Merge branch 'other' of "
265 				+ db.getWorkTree().getAbsolutePath();
266 		assertEquals(message, mergeCommit.getShortMessage());
267 	}
268 
269 	@Test
270 	public void testPullMergeProgrammaticConfigurationImpliedTargetBranch()
271 			throws Exception {
272 		// create another commit on another branch in source
273 		source.checkout().setCreateBranch(true).setName("other").call();
274 		sourceFile = new File(db.getWorkTree(), "file2.txt");
275 		writeToFile(sourceFile, "content");
276 		source.add().addFilepattern("file2.txt").call();
277 		RevCommit sourceCommit = source.commit()
278 				.setMessage("source commit on branch other").call();
279 
280 		target.checkout().setCreateBranch(true).setName("other").call();
281 		File targetFile2 = new File(dbTarget.getWorkTree(), "OtherFile.txt");
282 		writeToFile(targetFile2, "Unconflicting change");
283 		target.add().addFilepattern("OtherFile.txt").call();
284 		RevCommit targetCommit = target.commit()
285 				.setMessage("Unconflicting change in local").call();
286 
287 		// the source branch "other" matching the target branch should be
288 		// implied
289 		PullResult res = target.pull().setRemote("origin").setRebase(false)
290 				.call();
291 
292 		MergeResult mergeResult = res.getMergeResult();
293 		ObjectId[] mergedCommits = mergeResult.getMergedCommits();
294 		assertEquals(targetCommit.getId(), mergedCommits[0]);
295 		assertEquals(sourceCommit.getId(), mergedCommits[1]);
296 		RevCommit mergeCommit = new RevWalk(dbTarget).parseCommit(mergeResult
297 				.getNewHead());
298 		String message = "Merge branch 'other' of "
299 				+ db.getWorkTree().getAbsolutePath() + " into other";
300 		assertEquals(message, mergeCommit.getShortMessage());
301 	}
302 
303 	private enum TestPullMode {
304 		MERGE, REBASE, REBASE_PREASERVE
305 	}
306 
307 	@Test
308 	/** global rebase config should be respected */
309 	public void testPullWithRebasePreserve1Config() throws Exception {
310 		Callable<PullResult> setup = new Callable<PullResult>() {
311 			public PullResult call() throws Exception {
312 				StoredConfig config = dbTarget.getConfig();
313 				config.setString("pull", null, "rebase", "preserve");
314 				config.save();
315 				return target.pull().call();
316 			}
317 		};
318 		doTestPullWithRebase(setup, TestPullMode.REBASE_PREASERVE);
319 	}
320 
321 	@Test
322 	/** the branch-local config should win over the global config */
323 	public void testPullWithRebasePreserveConfig2() throws Exception {
324 		Callable<PullResult> setup = new Callable<PullResult>() {
325 			public PullResult call() throws Exception {
326 				StoredConfig config = dbTarget.getConfig();
327 				config.setString("pull", null, "rebase", "false");
328 				config.setString("branch", "master", "rebase", "preserve");
329 				config.save();
330 				return target.pull().call();
331 			}
332 		};
333 		doTestPullWithRebase(setup, TestPullMode.REBASE_PREASERVE);
334 	}
335 
336 	@Test
337 	/** the branch-local config should be respected */
338 	public void testPullWithRebasePreserveConfig3() throws Exception {
339 		Callable<PullResult> setup = new Callable<PullResult>() {
340 			public PullResult call() throws Exception {
341 				StoredConfig config = dbTarget.getConfig();
342 				config.setString("branch", "master", "rebase", "preserve");
343 				config.save();
344 				return target.pull().call();
345 			}
346 		};
347 		doTestPullWithRebase(setup, TestPullMode.REBASE_PREASERVE);
348 	}
349 
350 	@Test
351 	/** global rebase config should be respected */
352 	public void testPullWithRebaseConfig1() throws Exception {
353 		Callable<PullResult> setup = new Callable<PullResult>() {
354 			public PullResult call() throws Exception {
355 				StoredConfig config = dbTarget.getConfig();
356 				config.setString("pull", null, "rebase", "true");
357 				config.save();
358 				return target.pull().call();
359 			}
360 		};
361 		doTestPullWithRebase(setup, TestPullMode.REBASE);
362 	}
363 
364 	@Test
365 	/** the branch-local config should win over the global config */
366 	public void testPullWithRebaseConfig2() throws Exception {
367 		Callable<PullResult> setup = new Callable<PullResult>() {
368 			public PullResult call() throws Exception {
369 				StoredConfig config = dbTarget.getConfig();
370 				config.setString("pull", null, "rebase", "preserve");
371 				config.setString("branch", "master", "rebase", "true");
372 				config.save();
373 				return target.pull().call();
374 			}
375 		};
376 		doTestPullWithRebase(setup, TestPullMode.REBASE);
377 	}
378 
379 	@Test
380 	/** the branch-local config should be respected */
381 	public void testPullWithRebaseConfig3() throws Exception {
382 		Callable<PullResult> setup = new Callable<PullResult>() {
383 			public PullResult call() throws Exception {
384 				StoredConfig config = dbTarget.getConfig();
385 				config.setString("branch", "master", "rebase", "true");
386 				config.save();
387 				return target.pull().call();
388 			}
389 		};
390 		doTestPullWithRebase(setup, TestPullMode.REBASE);
391 	}
392 
393 	@Test
394 	/** without config it should merge */
395 	public void testPullWithoutConfig() throws Exception {
396 		Callable<PullResult> setup = new Callable<PullResult>() {
397 			public PullResult call() throws Exception {
398 				return target.pull().call();
399 			}
400 		};
401 		doTestPullWithRebase(setup, TestPullMode.MERGE);
402 	}
403 
404 	@Test
405 	/** the branch local config should win over the global config */
406 	public void testPullWithMergeConfig() throws Exception {
407 		Callable<PullResult> setup = new Callable<PullResult>() {
408 			public PullResult call() throws Exception {
409 				StoredConfig config = dbTarget.getConfig();
410 				config.setString("pull", null, "rebase", "true");
411 				config.setString("branch", "master", "rebase", "false");
412 				config.save();
413 				return target.pull().call();
414 			}
415 		};
416 		doTestPullWithRebase(setup, TestPullMode.MERGE);
417 	}
418 
419 	@Test
420 	/** the branch local config should win over the global config */
421 	public void testPullWithMergeConfig2() throws Exception {
422 		Callable<PullResult> setup = new Callable<PullResult>() {
423 			public PullResult call() throws Exception {
424 				StoredConfig config = dbTarget.getConfig();
425 				config.setString("pull", null, "rebase", "false");
426 				config.save();
427 				return target.pull().call();
428 			}
429 		};
430 		doTestPullWithRebase(setup, TestPullMode.MERGE);
431 	}
432 
433 	private void doTestPullWithRebase(Callable<PullResult> pullSetup,
434 			TestPullMode expectedPullMode) throws Exception {
435 		// simple upstream change
436 		writeToFile(sourceFile, "content");
437 		source.add().addFilepattern(sourceFile.getName()).call();
438 		RevCommit sourceCommit = source.commit().setMessage("source commit")
439 				.call();
440 
441 		// create a merge commit in target
442 		File loxalFile = new File(dbTarget.getWorkTree(), "local.txt");
443 		writeToFile(loxalFile, "initial\n");
444 		target.add().addFilepattern("local.txt").call();
445 		RevCommit t1 = target.commit().setMessage("target commit 1").call();
446 
447 		target.checkout().setCreateBranch(true).setName("side").call();
448 
449 		String newContent = "initial\n" + "and more\n";
450 		writeToFile(loxalFile, newContent);
451 		target.add().addFilepattern("local.txt").call();
452 		RevCommit t2 = target.commit().setMessage("target commit 2").call();
453 
454 		target.checkout().setName("master").call();
455 
456 		MergeResult mergeResult = target.merge()
457 				.setFastForward(MergeCommand.FastForwardMode.NO_FF).include(t2)
458 				.call();
459 		assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
460 		assertFileContentsEqual(loxalFile, newContent);
461 		ObjectId merge = mergeResult.getNewHead();
462 
463 		// pull
464 		PullResult res = pullSetup.call();
465 		assertNotNull(res.getFetchResult());
466 
467 		if (expectedPullMode == TestPullMode.MERGE) {
468 			assertEquals(MergeStatus.MERGED, res.getMergeResult()
469 					.getMergeStatus());
470 			assertNull(res.getRebaseResult());
471 		} else {
472 			assertNull(res.getMergeResult());
473 			assertEquals(RebaseResult.OK_RESULT, res.getRebaseResult());
474 		}
475 		assertFileContentsEqual(sourceFile, "content");
476 
477 		try (RevWalk rw = new RevWalk(dbTarget)) {
478 			rw.sort(RevSort.TOPO);
479 			rw.markStart(rw.parseCommit(dbTarget.resolve("refs/heads/master")));
480 
481 			RevCommit next;
482 			if (expectedPullMode == TestPullMode.MERGE) {
483 				next = rw.next();
484 				assertEquals(2, next.getParentCount());
485 				assertEquals(merge, next.getParent(0));
486 				assertEquals(sourceCommit, next.getParent(1));
487 				// since both parents are known do no further checks here
488 			} else {
489 				if (expectedPullMode == TestPullMode.REBASE_PREASERVE) {
490 					next = rw.next();
491 					assertEquals(2, next.getParentCount());
492 				}
493 				next = rw.next();
494 				assertEquals(t2.getShortMessage(), next.getShortMessage());
495 				next = rw.next();
496 				assertEquals(t1.getShortMessage(), next.getShortMessage());
497 				next = rw.next();
498 				assertEquals(sourceCommit, next);
499 				next = rw.next();
500 				assertEquals("Initial commit for source",
501 						next.getShortMessage());
502 				next = rw.next();
503 				assertNull(next);
504 			}
505 		}
506 	}
507 
508 	@Override
509 	@Before
510 	public void setUp() throws Exception {
511 		super.setUp();
512 		dbTarget = createWorkRepository();
513 		source = new Git(db);
514 		target = new Git(dbTarget);
515 
516 		// put some file in the source repo
517 		sourceFile = new File(db.getWorkTree(), "SomeFile.txt");
518 		writeToFile(sourceFile, "Hello world");
519 		// and commit it
520 		source.add().addFilepattern("SomeFile.txt").call();
521 		source.commit().setMessage("Initial commit for source").call();
522 
523 		// configure the target repo to connect to the source via "origin"
524 		StoredConfig targetConfig = dbTarget.getConfig();
525 		targetConfig.setString("branch", "master", "remote", "origin");
526 		targetConfig
527 				.setString("branch", "master", "merge", "refs/heads/master");
528 		RemoteConfig config = new RemoteConfig(targetConfig, "origin");
529 
530 		config
531 				.addURI(new URIish(source.getRepository().getWorkTree()
532 						.getAbsolutePath()));
533 		config.addFetchRefSpec(new RefSpec(
534 				"+refs/heads/*:refs/remotes/origin/*"));
535 		config.update(targetConfig);
536 		targetConfig.save();
537 
538 		targetFile = new File(dbTarget.getWorkTree(), "SomeFile.txt");
539 		// make sure we have the same content
540 		target.pull().call();
541 		assertFileContentsEqual(targetFile, "Hello world");
542 	}
543 
544 	private static void writeToFile(File actFile, String string)
545 			throws IOException {
546 		FileOutputStream fos = null;
547 		try {
548 			fos = new FileOutputStream(actFile);
549 			fos.write(string.getBytes("UTF-8"));
550 			fos.close();
551 		} finally {
552 			if (fos != null)
553 				fos.close();
554 		}
555 	}
556 
557 	private static void assertFileContentsEqual(File actFile, String string)
558 			throws IOException {
559 		ByteArrayOutputStream bos = new ByteArrayOutputStream();
560 		FileInputStream fis = null;
561 		byte[] buffer = new byte[100];
562 		try {
563 			fis = new FileInputStream(actFile);
564 			int read = fis.read(buffer);
565 			while (read > 0) {
566 				bos.write(buffer, 0, read);
567 				read = fis.read(buffer);
568 			}
569 			String content = new String(bos.toByteArray(), "UTF-8");
570 			assertEquals(string, content);
571 		} finally {
572 			if (fis != null)
573 				fis.close();
574 		}
575 	}
576 }