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