| /* |
| * Copyright (C) 2012, Robin Stocker <robin@nibor.org> and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| package org.eclipse.jgit.merge; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.time.Instant.EPOCH; |
| import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.time.Instant; |
| import java.util.Arrays; |
| import java.util.Map; |
| |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.api.MergeResult; |
| import org.eclipse.jgit.api.MergeResult.MergeStatus; |
| import org.eclipse.jgit.api.RebaseResult; |
| import org.eclipse.jgit.api.errors.CheckoutConflictException; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.api.errors.JGitInternalException; |
| import org.eclipse.jgit.dircache.DirCache; |
| import org.eclipse.jgit.dircache.DirCacheEditor; |
| import org.eclipse.jgit.dircache.DirCacheEntry; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.NoMergeBaseException; |
| import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; |
| import org.eclipse.jgit.junit.RepositoryTestCase; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.ConfigConstants; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.FileMode; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.ObjectLoader; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.ObjectStream; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.revwalk.RevTree; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.storage.file.FileBasedConfig; |
| import org.eclipse.jgit.treewalk.FileTreeIterator; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.FileUtils; |
| import org.junit.Assert; |
| import org.junit.experimental.theories.DataPoints; |
| import org.junit.experimental.theories.Theories; |
| import org.junit.experimental.theories.Theory; |
| import org.junit.runner.RunWith; |
| |
| @RunWith(Theories.class) |
| public class MergerTest extends RepositoryTestCase { |
| |
| @DataPoints |
| public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] { |
| MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE }; |
| |
| @Theory |
| public void failingDeleteOfDirectoryWithUntrackedContent( |
| MergeStrategy strategy) throws Exception { |
| File folder1 = new File(db.getWorkTree(), "folder1"); |
| FileUtils.mkdir(folder1); |
| File file = new File(folder1, "file1.txt"); |
| write(file, "folder1--file1.txt"); |
| file = new File(folder1, "file2.txt"); |
| write(file, "folder1--file2.txt"); |
| |
| try (Git git = new Git(db)) { |
| git.add().addFilepattern(folder1.getName()).call(); |
| RevCommit base = git.commit().setMessage("adding folder").call(); |
| |
| recursiveDelete(folder1); |
| git.rm().addFilepattern("folder1/file1.txt") |
| .addFilepattern("folder1/file2.txt").call(); |
| RevCommit other = git.commit() |
| .setMessage("removing folders on 'other'").call(); |
| |
| git.checkout().setName(base.name()).call(); |
| |
| file = new File(db.getWorkTree(), "unrelated.txt"); |
| write(file, "unrelated"); |
| |
| git.add().addFilepattern("unrelated.txt").call(); |
| RevCommit head = git.commit().setMessage("Adding another file").call(); |
| |
| // Untracked file to cause failing path for delete() of folder1 |
| // but that's ok. |
| file = new File(folder1, "file3.txt"); |
| write(file, "folder1--file3.txt"); |
| |
| ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false); |
| merger.setCommitNames(new String[] { "BASE", "HEAD", "other" }); |
| merger.setWorkingTreeIterator(new FileTreeIterator(db)); |
| boolean ok = merger.merge(head.getId(), other.getId()); |
| assertTrue(ok); |
| assertTrue(file.exists()); |
| } |
| } |
| |
| /** |
| * Merging two conflicting subtrees when the index does not contain any file |
| * in that subtree should lead to a conflicting state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("d/1", "master"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "side"); |
| git.commit().setAll(true).setMessage("modified d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two different but mergeable subtrees when the index does not |
| * contain any file in that subtree should lead to a merged state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "1\n2\n3"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("d/1", "1master\n2\n3"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "1\n2\n3side"); |
| git.commit().setAll(true).setMessage("modified d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals("[d/1, mode:100644, content:1master\n2\n3side]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * An existing directory without tracked content should not prevent merging |
| * a tree where that directory exists. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkUntrackedFolderIsNotAConflict( |
| MergeStrategy strategy) throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "1"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("e/1", "4"); |
| git.add().addFilepattern("e/1").call(); |
| RevCommit masterCommit = git.commit().setMessage("added e/1").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("f/1", "5"); |
| git.add().addFilepattern("f/1").call(); |
| git.commit().setAll(true).setMessage("added f/1") |
| .call(); |
| |
| // Untracked directory e shall not conflict with merged e/1 |
| writeTrashFile("e/2", "d two"); |
| |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * A tracked file is replaced by a folder in THEIRS. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("sub", "file"); |
| git.add().addFilepattern("sub").call(); |
| RevCommit first = git.commit().setMessage("initial").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| |
| git.rm().addFilepattern("sub").call(); |
| writeTrashFile("sub/file", "subfile"); |
| git.add().addFilepattern("sub/file").call(); |
| RevCommit masterCommit = git.commit().setMessage("file -> folder") |
| .call(); |
| |
| git.checkout().setName("master").call(); |
| writeTrashFile("noop", "other"); |
| git.add().addFilepattern("noop").call(); |
| git.commit().setAll(true).setMessage("noop").call(); |
| |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * A tracked file is replaced by a folder in OURS. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkFileReplacedByFolderInOurs(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("sub", "file"); |
| git.add().addFilepattern("sub").call(); |
| RevCommit first = git.commit().setMessage("initial").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("noop", "other"); |
| git.add().addFilepattern("noop").call(); |
| RevCommit sideCommit = git.commit().setAll(true).setMessage("noop") |
| .call(); |
| |
| git.checkout().setName("master").call(); |
| git.rm().addFilepattern("sub").call(); |
| writeTrashFile("sub/file", "subfile"); |
| git.add().addFilepattern("sub/file").call(); |
| git.commit().setMessage("file -> folder") |
| .call(); |
| |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(sideCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * An existing directory without tracked content should not prevent merging |
| * a file with that name. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkUntrackedEmpytFolderIsNotAConflictWithFile( |
| MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "1"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("e", "4"); |
| git.add().addFilepattern("e").call(); |
| RevCommit masterCommit = git.commit().setMessage("added e").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("f/1", "5"); |
| git.add().addFilepattern("f/1").call(); |
| git.commit().setAll(true).setMessage("added f/1").call(); |
| |
| // Untracked empty directory hierarcy e/1 shall not conflict with merged |
| // e/1 |
| FileUtils.mkdirs(new File(trash, "e/1"), true); |
| |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]", |
| indexState(CONTENT)); |
| } |
| |
| @Theory |
| public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException, |
| GitAPIException { |
| Git git = Git.wrap(db); |
| db.getConfig().setString("core", null, "autocrlf", "false"); |
| db.getConfig().save(); |
| writeTrashFile("crlf.txt", "some\r\ndata\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("base").call(); |
| |
| git.branchCreate().setName("brancha").call(); |
| |
| writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("on master").call(); |
| |
| git.checkout().setName("brancha").call(); |
| writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("on brancha").call(); |
| |
| db.getConfig().setString("core", null, "autocrlf", "input"); |
| db.getConfig().save(); |
| |
| MergeResult mergeResult = git.merge().setStrategy(strategy) |
| .include(db.resolve("master")) |
| .call(); |
| assertEquals(MergeResult.MergeStatus.MERGED, |
| mergeResult.getMergeStatus()); |
| } |
| |
| @Theory |
| public void mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy) |
| throws IOException, GitAPIException { |
| Git git = Git.wrap(db); |
| db.getConfig().setString("core", null, "autocrlf", "true"); |
| db.getConfig().save(); |
| writeTrashFile("crlf.txt", "a crlf file\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("base").call(); |
| |
| git.branchCreate().setName("brancha").call(); |
| |
| writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("on master").call(); |
| |
| git.checkout().setName("brancha").call(); |
| File testFile = writeTrashFile("crlf.txt", |
| "a first line\r\na crlf file\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("on brancha").call(); |
| |
| MergeResult mergeResult = git.merge().setStrategy(strategy) |
| .include(db.resolve("master")).call(); |
| assertEquals(MergeResult.MergeStatus.MERGED, |
| mergeResult.getMergeStatus()); |
| checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n"); |
| assertEquals( |
| "[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]", |
| indexState(CONTENT)); |
| } |
| |
| @Theory |
| public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy) |
| throws IOException, GitAPIException { |
| Git git = Git.wrap(db); |
| db.getConfig().setString("core", null, "autocrlf", "true"); |
| db.getConfig().save(); |
| writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| RevCommit first = git.commit().setMessage("base").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("brancha").call(); |
| |
| File testFile = writeTrashFile("crlf.txt", |
| "line 1\r\nmodified line\r\nline 3\r\n"); |
| git.add().addFilepattern("crlf.txt").call(); |
| git.commit().setMessage("on brancha").call(); |
| |
| git.checkout().setName("master").call(); |
| File otherFile = writeTrashFile("otherfile.txt", "a line\r\n"); |
| git.add().addFilepattern("otherfile.txt").call(); |
| git.commit().setMessage("on master").call(); |
| |
| git.checkout().setName("brancha").call(); |
| checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n"); |
| assertFalse(otherFile.exists()); |
| |
| RebaseResult rebaseResult = git.rebase().setStrategy(strategy) |
| .setUpstream(db.resolve("master")).call(); |
| assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus()); |
| checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n"); |
| checkFile(otherFile, "a line\r\n"); |
| assertEquals( |
| "[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]" |
| + "[otherfile.txt, mode:100644, content:a line\n]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two equal subtrees when the index does not contain any file in |
| * that subtree should lead to a merged state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("d/1", "modified"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "modified"); |
| git.commit().setAll(true).setMessage("modified d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals("[d/1, mode:100644, content:modified]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two equal subtrees with an incore merger should lead to a merged |
| * state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeEqualTreesInCore(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("d/1", "modified"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "modified"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| |
| ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db, |
| true); |
| boolean noProblems = resolveMerger.merge(masterCommit, sideCommit); |
| assertTrue(noProblems); |
| } |
| |
| /** |
| * Merging two equal subtrees with an incore merger should lead to a merged |
| * state, without using a Repository (the 'Gerrit' use case). |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit first = git.commit().setMessage("added d/1").call(); |
| |
| writeTrashFile("d/1", "modified"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "modified"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| |
| try (ObjectInserter ins = db.newObjectInserter()) { |
| ThreeWayMerger resolveMerger = |
| (ThreeWayMerger) strategy.newMerger(ins, db.getConfig()); |
| boolean noProblems = resolveMerger.merge(masterCommit, sideCommit); |
| assertTrue(noProblems); |
| } |
| } |
| |
| /** |
| * Merging two equal subtrees when the index and HEAD does not contain any |
| * file in that subtree should lead to a merged state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeEqualNewTrees(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("2", "orig"); |
| git.add().addFilepattern("2").call(); |
| RevCommit first = git.commit().setMessage("added 2").call(); |
| |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("added d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "orig"); |
| git.add().addFilepattern("d/1").call(); |
| git.commit().setAll(true).setMessage("added d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[2, mode:100644, content:orig][d/1, mode:100644, content:orig]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two conflicting subtrees when the index and HEAD does not contain |
| * any file in that subtree should lead to a conflicting state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeConflictingNewTrees(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("2", "orig"); |
| git.add().addFilepattern("2").call(); |
| RevCommit first = git.commit().setMessage("added 2").call(); |
| |
| writeTrashFile("d/1", "master"); |
| git.add().addFilepattern("d/1").call(); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("added d/1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("d/1", "side"); |
| git.add().addFilepattern("d/1").call(); |
| git.commit().setAll(true).setMessage("added d/1 on side").call(); |
| |
| git.rm().addFilepattern("d/1").call(); |
| git.rm().addFilepattern("d").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus()); |
| assertEquals( |
| "[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]", |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two conflicting files when the index contains a tree for that |
| * path should lead to a failed state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("0", "orig"); |
| git.add().addFilepattern("0").call(); |
| RevCommit first = git.commit().setMessage("added 0").call(); |
| |
| writeTrashFile("0", "master"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified 0 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("0", "side"); |
| git.commit().setAll(true).setMessage("modified 0 on side").call(); |
| |
| git.rm().addFilepattern("0").call(); |
| writeTrashFile("0/0", "side"); |
| git.add().addFilepattern("0/0").call(); |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus()); |
| } |
| |
| /** |
| * Merging two equal files when the index contains a tree for that path |
| * should lead to a failed state. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("0", "orig"); |
| writeTrashFile("1", "1\n2\n3"); |
| git.add().addFilepattern("0").addFilepattern("1").call(); |
| RevCommit first = git.commit().setMessage("added 0, 1").call(); |
| |
| writeTrashFile("1", "1master\n2\n3"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified 1 on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("1", "1\n2\n3side"); |
| git.commit().setAll(true).setMessage("modified 1 on side").call(); |
| |
| git.rm().addFilepattern("0").call(); |
| writeTrashFile("0/0", "modified"); |
| git.add().addFilepattern("0/0").call(); |
| try { |
| git.merge().setStrategy(strategy).include(masterCommit).call(); |
| Assert.fail("Didn't get the expected exception"); |
| } catch (CheckoutConflictException e) { |
| assertEquals(1, e.getConflictingPaths().size()); |
| assertEquals("0/0", e.getConflictingPaths().get(0)); |
| } |
| } |
| |
| @Theory |
| public void checkContentMergeNoConflict(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("file", "1\n2\n3"); |
| git.add().addFilepattern("file").call(); |
| RevCommit first = git.commit().setMessage("added file").call(); |
| |
| writeTrashFile("file", "1master\n2\n3"); |
| git.commit().setAll(true).setMessage("modified file on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("file", "1\n2\n3side"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified file on side").call(); |
| |
| git.checkout().setName("master").call(); |
| MergeResult result = |
| git.merge().setStrategy(strategy).include(sideCommit).call(); |
| assertEquals(MergeStatus.MERGED, result.getMergeStatus()); |
| String expected = "1master\n2\n3side"; |
| assertEquals(expected, read("file")); |
| } |
| |
| @Theory |
| public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("file", "1\n2\n3"); |
| git.add().addFilepattern("file").call(); |
| RevCommit first = git.commit().setMessage("added file").call(); |
| |
| writeTrashFile("file", "1master\n2\n3"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified file on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("file", "1\n2\n3side"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified file on side").call(); |
| |
| try (ObjectInserter ins = db.newObjectInserter()) { |
| ResolveMerger merger = |
| (ResolveMerger) strategy.newMerger(ins, db.getConfig()); |
| boolean noProblems = merger.merge(masterCommit, sideCommit); |
| assertTrue(noProblems); |
| assertEquals("1master\n2\n3side", |
| readBlob(merger.getResultTreeId(), "file")); |
| } |
| } |
| |
| |
| /** |
| * Merging a change involving large binary files should short-circuit reads. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception { |
| Git git = Git.wrap(db); |
| final int LINELEN = 72; |
| |
| // setup a merge that would work correctly if we disconsider the stray '\0' |
| // that the file contains near the start. |
| byte[] binary = new byte[LINELEN * 2000]; |
| for (int i = 0; i < binary.length; i++) { |
| binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x'); |
| } |
| binary[50] = '\0'; |
| |
| writeTrashFile("file", new String(binary, UTF_8)); |
| git.add().addFilepattern("file").call(); |
| RevCommit first = git.commit().setMessage("added file").call(); |
| |
| // Generate an edit in a single line. |
| int idx = LINELEN * 1200 + 1; |
| byte save = binary[idx]; |
| binary[idx] = '@'; |
| writeTrashFile("file", new String(binary, UTF_8)); |
| |
| binary[idx] = save; |
| git.add().addFilepattern("file").call(); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified file l 1200").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call(); |
| binary[LINELEN * 1500 + 1] = '!'; |
| writeTrashFile("file", new String(binary, UTF_8)); |
| git.add().addFilepattern("file").call(); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified file l 1500").call(); |
| |
| try (ObjectInserter ins = db.newObjectInserter()) { |
| // Check that we don't read the large blobs. |
| ObjectInserter forbidInserter = new ObjectInserter.Filter() { |
| @Override |
| protected ObjectInserter delegate() { |
| return ins; |
| } |
| |
| @Override |
| public ObjectReader newReader() { |
| return new BigReadForbiddenReader(super.newReader(), 8000); |
| } |
| }; |
| |
| ResolveMerger merger = |
| (ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig()); |
| boolean noProblems = merger.merge(masterCommit, sideCommit); |
| assertFalse(noProblems); |
| } |
| } |
| |
| /** |
| * Throws an exception if reading beyond limit. |
| */ |
| static class BigReadForbiddenStream extends ObjectStream.Filter { |
| long limit; |
| |
| BigReadForbiddenStream(ObjectStream orig, long limit) { |
| super(orig.getType(), orig.getSize(), orig); |
| this.limit = limit; |
| } |
| |
| @Override |
| public long skip(long n) throws IOException { |
| limit -= n; |
| if (limit < 0) { |
| throw new IllegalStateException(); |
| } |
| |
| return super.skip(n); |
| } |
| |
| @Override |
| public int read() throws IOException { |
| int r = super.read(); |
| limit--; |
| if (limit < 0) { |
| throw new IllegalStateException(); |
| } |
| return r; |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| int n = super.read(b, off, len); |
| limit -= n; |
| if (limit < 0) { |
| throw new IllegalStateException(); |
| } |
| return n; |
| } |
| } |
| |
| static class BigReadForbiddenReader extends ObjectReader.Filter { |
| ObjectReader delegate; |
| int limit; |
| |
| @Override |
| protected ObjectReader delegate() { |
| return delegate; |
| } |
| |
| BigReadForbiddenReader(ObjectReader delegate, int limit) { |
| this.delegate = delegate; |
| this.limit = limit; |
| } |
| |
| @Override |
| public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException { |
| ObjectLoader orig = super.open(objectId, typeHint); |
| return new ObjectLoader.Filter() { |
| @Override |
| protected ObjectLoader delegate() { |
| return orig; |
| } |
| |
| @Override |
| public ObjectStream openStream() throws IOException { |
| ObjectStream os = orig.openStream(); |
| return new BigReadForbiddenStream(os, limit); |
| } |
| }; |
| } |
| } |
| |
| @Theory |
| public void checkContentMergeConflict(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("file", "1\n2\n3"); |
| git.add().addFilepattern("file").call(); |
| RevCommit first = git.commit().setMessage("added file").call(); |
| |
| writeTrashFile("file", "1master\n2\n3"); |
| git.commit().setAll(true).setMessage("modified file on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("file", "1side\n2\n3"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified file on side").call(); |
| |
| git.checkout().setName("master").call(); |
| MergeResult result = |
| git.merge().setStrategy(strategy).include(sideCommit).call(); |
| assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); |
| String expected = "<<<<<<< HEAD\n" |
| + "1master\n" |
| + "=======\n" |
| + "1side\n" |
| + ">>>>>>> " + sideCommit.name() + "\n" |
| + "2\n" |
| + "3"; |
| assertEquals(expected, read("file")); |
| } |
| |
| @Theory |
| public void checkContentMergeConflict_noTree(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("file", "1\n2\n3"); |
| git.add().addFilepattern("file").call(); |
| RevCommit first = git.commit().setMessage("added file").call(); |
| |
| writeTrashFile("file", "1master\n2\n3"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified file on master").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("file", "1side\n2\n3"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified file on side").call(); |
| |
| try (ObjectInserter ins = db.newObjectInserter()) { |
| ResolveMerger merger = |
| (ResolveMerger) strategy.newMerger(ins, db.getConfig()); |
| boolean noProblems = merger.merge(masterCommit, sideCommit); |
| assertFalse(noProblems); |
| assertEquals(Arrays.asList("file"), merger.getUnmergedPaths()); |
| |
| MergeFormatter fmt = new MergeFormatter(); |
| merger.getMergeResults().get("file"); |
| try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { |
| fmt.formatMerge(out, merger.getMergeResults().get("file"), |
| "BASE", "OURS", "THEIRS", UTF_8); |
| String expected = "<<<<<<< OURS\n" |
| + "1master\n" |
| + "=======\n" |
| + "1side\n" |
| + ">>>>>>> THEIRS\n" |
| + "2\n" |
| + "3"; |
| assertEquals(expected, new String(out.toByteArray(), UTF_8)); |
| } |
| } |
| } |
| |
| /** |
| * Merging after criss-cross merges. In this case we merge together two |
| * commits which have two equally good common ancestors |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeCrissCross(MergeStrategy strategy) throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("1", "1\n2\n3"); |
| git.add().addFilepattern("1").call(); |
| RevCommit first = git.commit().setMessage("added 1").call(); |
| |
| writeTrashFile("1", "1master\n2\n3"); |
| RevCommit masterCommit = git.commit().setAll(true) |
| .setMessage("modified 1 on master").call(); |
| |
| writeTrashFile("1", "1master2\n2\n3"); |
| git.commit().setAll(true) |
| .setMessage("modified 1 on master again").call(); |
| |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("1", "1\n2\na\nb\nc\n3side"); |
| RevCommit sideCommit = git.commit().setAll(true) |
| .setMessage("modified 1 on side").call(); |
| |
| writeTrashFile("1", "1\n2\n3side2"); |
| git.commit().setAll(true) |
| .setMessage("modified 1 on side again").call(); |
| |
| MergeResult result = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| assertEquals(MergeStatus.MERGED, result.getMergeStatus()); |
| result.getNewHead(); |
| git.checkout().setName("master").call(); |
| result = git.merge().setStrategy(strategy).include(sideCommit).call(); |
| assertEquals(MergeStatus.MERGED, result.getMergeStatus()); |
| |
| // we have two branches which are criss-cross merged. Try to merge the |
| // tips. This should succeed with RecursiveMerge and fail with |
| // ResolveMerge |
| try { |
| MergeResult mergeResult = git.merge().setStrategy(strategy) |
| .include(git.getRepository().exactRef("refs/heads/side")) |
| .call(); |
| assertEquals(MergeStrategy.RECURSIVE, strategy); |
| assertEquals(MergeResult.MergeStatus.MERGED, |
| mergeResult.getMergeStatus()); |
| assertEquals("1master2\n2\n3side2", read("1")); |
| } catch (JGitInternalException e) { |
| assertEquals(MergeStrategy.RESOLVE, strategy); |
| assertTrue(e.getCause() instanceof NoMergeBaseException); |
| assertEquals(((NoMergeBaseException) e.getCause()).getReason(), |
| MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); |
| } |
| } |
| |
| @Theory |
| public void checkLockedFilesToBeDeleted(MergeStrategy strategy) |
| throws Exception { |
| Git git = Git.wrap(db); |
| |
| writeTrashFile("a.txt", "orig"); |
| writeTrashFile("b.txt", "orig"); |
| git.add().addFilepattern("a.txt").addFilepattern("b.txt").call(); |
| RevCommit first = git.commit().setMessage("added a.txt, b.txt").call(); |
| |
| // modify and delete files on the master branch |
| writeTrashFile("a.txt", "master"); |
| git.rm().addFilepattern("b.txt").call(); |
| RevCommit masterCommit = git.commit() |
| .setMessage("modified a.txt, deleted b.txt").setAll(true) |
| .call(); |
| |
| // switch back to a side branch |
| git.checkout().setCreateBranch(true).setStartPoint(first) |
| .setName("side").call(); |
| writeTrashFile("c.txt", "side"); |
| git.add().addFilepattern("c.txt").call(); |
| git.commit().setMessage("added c.txt").call(); |
| |
| // Get a handle to the file so on windows it can't be deleted. |
| try (FileInputStream fis = new FileInputStream( |
| new File(db.getWorkTree(), "b.txt"))) { |
| MergeResult mergeRes = git.merge().setStrategy(strategy) |
| .include(masterCommit).call(); |
| if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) { |
| // probably windows |
| assertEquals(1, mergeRes.getFailingPaths().size()); |
| assertEquals(MergeFailureReason.COULD_NOT_DELETE, |
| mergeRes.getFailingPaths().get("b.txt")); |
| } |
| assertEquals( |
| "[a.txt, mode:100644, content:master]" |
| + "[c.txt, mode:100644, content:side]", |
| indexState(CONTENT)); |
| } |
| } |
| |
| @Theory |
| public void checkForCorrectIndex(MergeStrategy strategy) throws Exception { |
| File f; |
| Instant lastTs4, lastTsIndex; |
| Git git = Git.wrap(db); |
| File indexFile = db.getIndexFile(); |
| |
| // Create initial content and remember when the last file was written. |
| f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); |
| lastTs4 = FS.DETECTED.lastModifiedInstant(f); |
| |
| // add all files, commit and check this doesn't update any working tree |
| // files and that the index is in a new file system timer tick. Make |
| // sure to wait long enough before adding so the index doesn't contain |
| // racily clean entries |
| fsTick(f); |
| git.add().addFilepattern(".").call(); |
| RevCommit firstCommit = git.commit().setMessage("initial commit") |
| .call(); |
| checkConsistentLastModified("0", "1", "2", "3", "4"); |
| checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index"); |
| assertEquals("Commit should not touch working tree file 4", lastTs4, |
| FS.DETECTED |
| .lastModifiedInstant(new File(db.getWorkTree(), "4"))); |
| lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); |
| |
| // Do modifications on the master branch. Then add and commit. This |
| // should touch only "0", "2 and "3" |
| fsTick(indexFile); |
| f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master", |
| null); |
| fsTick(f); |
| git.add().addFilepattern(".").call(); |
| RevCommit masterCommit = git.commit().setMessage("master commit") |
| .call(); |
| checkConsistentLastModified("0", "1", "2", "3", "4"); |
| checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" |
| + lastTsIndex, "<0", "2", "3", "<.git/index"); |
| lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); |
| |
| // Checkout a side branch. This should touch only "0", "2 and "3" |
| fsTick(indexFile); |
| git.checkout().setCreateBranch(true).setStartPoint(firstCommit) |
| .setName("side").call(); |
| checkConsistentLastModified("0", "1", "2", "3", "4"); |
| checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" |
| + lastTsIndex, "<0", "2", "3", ".git/index"); |
| lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); |
| |
| // This checkout may have populated worktree and index so fast that we |
| // may have smudged entries now. Check that we have the right content |
| // and then rewrite the index to get rid of smudged state |
| assertEquals("[0, mode:100644, content:orig]" // |
| + "[1, mode:100644, content:orig]" // |
| + "[2, mode:100644, content:1\n2\n3]" // |
| + "[3, mode:100644, content:orig]" // |
| + "[4, mode:100644, content:orig]", // |
| indexState(CONTENT)); |
| fsTick(indexFile); |
| f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); |
| lastTs4 = FS.DETECTED.lastModifiedInstant(f); |
| fsTick(f); |
| git.add().addFilepattern(".").call(); |
| checkConsistentLastModified("0", "1", "2", "3", "4"); |
| checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3", |
| "4", "<.git/index"); |
| lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); |
| |
| // Do modifications on the side branch. Touch only "1", "2 and "3" |
| fsTick(indexFile); |
| f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null); |
| fsTick(f); |
| git.add().addFilepattern(".").call(); |
| git.commit().setMessage("side commit").call(); |
| checkConsistentLastModified("0", "1", "2", "3", "4"); |
| checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*" |
| + lastTsIndex, "<1", "2", "3", "<.git/index"); |
| lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile); |
| |
| // merge master and side. Should only touch "0," "2" and "3" |
| fsTick(indexFile); |
| git.merge().setStrategy(strategy).include(masterCommit).call(); |
| checkConsistentLastModified("0", "1", "2", "4"); |
| checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*" |
| + lastTsIndex, "<0", "2", "3", ".git/index"); |
| assertEquals( |
| "[0, mode:100644, content:master]" // |
| + "[1, mode:100644, content:side]" // |
| + "[2, mode:100644, content:1master\n2\n3side]" // |
| + "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" // |
| + "[4, mode:100644, content:orig]", // |
| indexState(CONTENT)); |
| } |
| |
| /** |
| * Merging two conflicting submodules when the index does not contain any |
| * entry for that submodule. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeConflictingSubmodulesWithoutIndex( |
| MergeStrategy strategy) throws Exception { |
| Git git = Git.wrap(db); |
| writeTrashFile("initial", "initial"); |
| git.add().addFilepattern("initial").call(); |
| RevCommit initial = git.commit().setMessage("initial").call(); |
| |
| writeSubmodule("one", ObjectId |
| .fromString("1000000000000000000000000000000000000000")); |
| git.add().addFilepattern(Constants.DOT_GIT_MODULES).call(); |
| RevCommit right = git.commit().setMessage("added one").call(); |
| |
| // a second commit in the submodule |
| |
| git.checkout().setStartPoint(initial).setName("left") |
| .setCreateBranch(true).call(); |
| writeSubmodule("one", ObjectId |
| .fromString("2000000000000000000000000000000000000000")); |
| |
| git.add().addFilepattern(Constants.DOT_GIT_MODULES).call(); |
| git.commit().setMessage("a different one").call(); |
| |
| MergeResult result = git.merge().setStrategy(strategy).include(right) |
| .call(); |
| |
| assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus()); |
| Map<String, int[][]> conflicts = result.getConflicts(); |
| assertEquals(1, conflicts.size()); |
| assertNotNull(conflicts.get("one")); |
| } |
| |
| /** |
| * Merging two non-conflicting submodules when the index does not contain |
| * any entry for either submodule. |
| * |
| * @param strategy |
| * @throws Exception |
| */ |
| @Theory |
| public void checkMergeNonConflictingSubmodulesWithoutIndex( |
| MergeStrategy strategy) throws Exception { |
| Git git = Git.wrap(db); |
| writeTrashFile("initial", "initial"); |
| git.add().addFilepattern("initial").call(); |
| |
| writeSubmodule("one", ObjectId |
| .fromString("1000000000000000000000000000000000000000")); |
| |
| // Our initial commit should include a .gitmodules with a bunch of |
| // comment lines, so that |
| // we don't have a content merge issue when we add a new submodule at |
| // the top and a different |
| // one at the bottom. This is sort of a hack, but it should allow |
| // add/add submodule merges |
| String existing = read(Constants.DOT_GIT_MODULES); |
| String context = "\n# context\n# more context\n# yet more context\n"; |
| write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES), |
| existing + context + context + context); |
| |
| git.add().addFilepattern(Constants.DOT_GIT_MODULES).call(); |
| RevCommit initial = git.commit().setMessage("initial").call(); |
| |
| writeSubmodule("two", ObjectId |
| .fromString("1000000000000000000000000000000000000000")); |
| git.add().addFilepattern(Constants.DOT_GIT_MODULES).call(); |
| |
| RevCommit right = git.commit().setMessage("added two").call(); |
| |
| git.checkout().setStartPoint(initial).setName("left") |
| .setCreateBranch(true).call(); |
| |
| // we need to manually create the submodule for three for the |
| // .gitmodules hackery |
| addSubmoduleToIndex("three", ObjectId |
| .fromString("1000000000000000000000000000000000000000")); |
| new File(db.getWorkTree(), "three").mkdir(); |
| |
| existing = read(Constants.DOT_GIT_MODULES); |
| String three = "[submodule \"three\"]\n\tpath = three\n\turl = " |
| + db.getDirectory().toURI() + "\n"; |
| write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES), |
| three + existing); |
| |
| git.add().addFilepattern(Constants.DOT_GIT_MODULES).call(); |
| git.commit().setMessage("a different one").call(); |
| |
| MergeResult result = git.merge().setStrategy(strategy).include(right) |
| .call(); |
| |
| assertNull(result.getCheckoutConflicts()); |
| assertNull(result.getFailingPaths()); |
| for (String dir : Arrays.asList("one", "two", "three")) { |
| assertTrue(new File(db.getWorkTree(), dir).isDirectory()); |
| } |
| } |
| |
| private void writeSubmodule(String path, ObjectId commit) |
| throws IOException, ConfigInvalidException { |
| addSubmoduleToIndex(path, commit); |
| new File(db.getWorkTree(), path).mkdir(); |
| |
| StoredConfig config = db.getConfig(); |
| config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, |
| ConfigConstants.CONFIG_KEY_URL, |
| db.getDirectory().toURI().toString()); |
| config.save(); |
| |
| FileBasedConfig modulesConfig = new FileBasedConfig( |
| new File(db.getWorkTree(), Constants.DOT_GIT_MODULES), |
| db.getFS()); |
| modulesConfig.load(); |
| modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, |
| ConfigConstants.CONFIG_KEY_PATH, path); |
| modulesConfig.save(); |
| |
| } |
| |
| private void addSubmoduleToIndex(String path, ObjectId commit) |
| throws IOException { |
| DirCache cache = db.lockDirCache(); |
| DirCacheEditor editor = cache.editor(); |
| editor.add(new DirCacheEditor.PathEdit(path) { |
| |
| @Override |
| public void apply(DirCacheEntry ent) { |
| ent.setFileMode(FileMode.GITLINK); |
| ent.setObjectId(commit); |
| } |
| }); |
| editor.commit(); |
| } |
| |
| // Assert that every specified index entry has the same last modification |
| // timestamp as the associated file |
| private void checkConsistentLastModified(String... pathes) |
| throws IOException { |
| DirCache dc = db.readDirCache(); |
| File workTree = db.getWorkTree(); |
| for (String path : pathes) |
| assertEquals( |
| "IndexEntry with path " |
| + path |
| + " has lastmodified which is different from the worktree file", |
| FS.DETECTED.lastModifiedInstant(new File(workTree, path)), |
| dc.getEntry(path) |
| .getLastModifiedInstant()); |
| } |
| |
| // Assert that modification timestamps of working tree files are as |
| // expected. You may specify n files. It is asserted that every file |
| // i+1 is not older than file i. If a path of file i+1 is prefixed with "<" |
| // then this file must be younger then file i. A path "*<modtime>" |
| // represents a file with a modification time of <modtime> |
| // E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt |
| private void checkModificationTimeStampOrder(String... pathes) { |
| Instant lastMod = EPOCH; |
| for (String p : pathes) { |
| boolean strong = p.startsWith("<"); |
| boolean fixed = p.charAt(strong ? 1 : 0) == '*'; |
| p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0)); |
| Instant curMod = fixed ? Instant.parse(p) |
| : FS.DETECTED |
| .lastModifiedInstant(new File(db.getWorkTree(), p)); |
| if (strong) { |
| assertTrue("path " + p + " is not younger than predecesssor", |
| curMod.compareTo(lastMod) > 0); |
| } else { |
| assertTrue("path " + p + " is older than predecesssor", |
| curMod.compareTo(lastMod) >= 0); |
| } |
| } |
| } |
| |
| private String readBlob(ObjectId treeish, String path) throws Exception { |
| try (TestRepository<?> tr = new TestRepository<>(db); |
| RevWalk rw = tr.getRevWalk()) { |
| RevTree tree = rw.parseTree(treeish); |
| RevObject obj = tr.get(tree, path); |
| if (obj == null) { |
| return null; |
| } |
| return new String( |
| rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(), UTF_8); |
| } |
| } |
| } |