blob: 3cdc8da34ef125ebbe145461e80f040d03876f1d [file] [log] [blame]
/*
* Copyright (C) 2022 Thomas Wolf <twolf@apache.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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.MergeResult.MergeStatus;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
/**
* Tests for merges involving symlinks.
*/
@RunWith(Parameterized.class)
public class SymlinkMergeTest extends RepositoryTestCase {
@Parameters(name = "target={0}, core.symlinks={1}")
public static Object[][] parameters() {
return new Object[][] {
{ Target.NONE, Boolean.TRUE },
{ Target.FILE, Boolean.TRUE },
{ Target.DIRECTORY, Boolean.TRUE },
{ Target.NONE, Boolean.FALSE },
{ Target.FILE, Boolean.FALSE },
{ Target.DIRECTORY, Boolean.FALSE },
};
}
public enum Target {
NONE, FILE, DIRECTORY
}
@Parameter(0)
public Target target;
@Parameter(1)
public boolean useSymLinks;
private void setTargets() throws IOException {
switch (target) {
case DIRECTORY:
assertTrue(new File(trash, "target").mkdir());
assertTrue(new File(trash, "target1").mkdir());
assertTrue(new File(trash, "target2").mkdir());
break;
case FILE:
writeTrashFile("target", "t");
writeTrashFile("target1", "t1");
writeTrashFile("target2", "t2");
break;
default:
break;
}
}
private void checkTargets() throws IOException {
File t = new File(trash, "target");
File t1 = new File(trash, "target1");
File t2 = new File(trash, "target2");
switch (target) {
case DIRECTORY:
assertTrue(t.isDirectory());
assertTrue(t1.isDirectory());
assertTrue(t2.isDirectory());
break;
case FILE:
checkFile(t, "t");
checkFile(t1, "t1");
checkFile(t2, "t2");
break;
default:
assertFalse(Files.exists(t.toPath(), LinkOption.NOFOLLOW_LINKS));
assertFalse(Files.exists(t1.toPath(), LinkOption.NOFOLLOW_LINKS));
assertFalse(Files.exists(t2.toPath(), LinkOption.NOFOLLOW_LINKS));
break;
}
}
private void assertSymLink(File link, String content) throws Exception {
if (useSymLinks) {
assertTrue(Files.isSymbolicLink(link.toPath()));
assertEquals(content, db.getFS().readSymLink(link));
} else {
assertFalse(Files.isSymbolicLink(link.toPath()));
assertTrue(link.isFile());
checkFile(link, content);
}
}
// Link/link conflict: C git records the conflict but leaves the link in the
// working tree unchanged.
@Test
public void mergeWithSymlinkConflict() throws Exception {
assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
config.save();
try (TestRepository<Repository> repo = new TestRepository<>(db)) {
db.incrementOpen();
// Create the links directly in the git repo, then use a hard reset
// to get them into the workspace. This enables us to run these
// tests also with core.symLinks = false.
RevCommit base = repo
.commit(repo.tree(repo.link("link", repo.blob("target"))));
RevCommit side = repo.commit(
repo.tree(repo.link("link", repo.blob("target1"))), base);
RevCommit head = repo.commit(
repo.tree(repo.link("link", repo.blob("target2"))), base);
try (Git git = new Git(db)) {
setTargets();
git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
File link = new File(trash, "link");
assertSymLink(link, "target2");
MergeResult result = git.merge().include(side)
.setMessage("merged").call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
// Link should be unmodified
assertSymLink(link, "target2");
checkTargets();
assertEquals("[link, mode:120000, stage:1, content:target]"
+ "[link, mode:120000, stage:2, content:target2]"
+ "[link, mode:120000, stage:3, content:target1]",
indexState(CONTENT));
}
}
}
// In file/link conflicts, C git never does a content merge. It records the
// stages in the index, and always puts the file into the workspace.
@Test
public void mergeWithFileSymlinkConflict() throws Exception {
assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
config.save();
try (TestRepository<Repository> repo = new TestRepository<>(db)) {
db.incrementOpen();
RevCommit base = repo.commit(repo.tree());
RevCommit side = repo.commit(
repo.tree(repo.link("link", repo.blob("target1"))), base);
RevCommit head = repo.commit(
repo.tree(repo.file("link", repo.blob("not a link"))),
base);
try (Git git = new Git(db)) {
setTargets();
git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
File link = new File(trash, "link");
assertFalse(Files.isSymbolicLink(link.toPath()));
checkFile(link, "not a link");
MergeResult result = git.merge().include(side)
.setMessage("merged").call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
// File should be unmodified
assertFalse(Files.isSymbolicLink(link.toPath()));
checkFile(link, "not a link");
checkTargets();
assertEquals("[link, mode:100644, stage:2, content:not a link]"
+ "[link, mode:120000, stage:3, content:target1]",
indexState(CONTENT));
}
}
}
@Test
public void mergeWithSymlinkFileConflict() throws Exception {
assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
config.save();
try (TestRepository<Repository> repo = new TestRepository<>(db)) {
db.incrementOpen();
RevCommit base = repo.commit(repo.tree());
RevCommit side = repo.commit(
repo.tree(repo.file("link", repo.blob("not a link"))),
base);
RevCommit head = repo.commit(
repo.tree(repo.link("link", repo.blob("target2"))), base);
try (Git git = new Git(db)) {
setTargets();
git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
File link = new File(trash, "link");
assertSymLink(link, "target2");
MergeResult result = git.merge().include(side)
.setMessage("merged").call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
// Should now be a file!
assertFalse(Files.isSymbolicLink(link.toPath()));
checkFile(link, "not a link");
checkTargets();
assertEquals("[link, mode:120000, stage:2, content:target2]"
+ "[link, mode:100644, stage:3, content:not a link]",
indexState(CONTENT));
}
}
}
// In Delete/modify conflicts with the non-deleted side a link, C git puts
// the link into the working tree.
@Test
public void mergeWithSymlinkDeleteModify() throws Exception {
assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
config.save();
try (TestRepository<Repository> repo = new TestRepository<>(db)) {
db.incrementOpen();
RevCommit base = repo
.commit(repo.tree(repo.link("link", repo.blob("target"))));
RevCommit side = repo.commit(
repo.tree(repo.link("link", repo.blob("target1"))), base);
RevCommit head = repo.commit(repo.tree(), base);
try (Git git = new Git(db)) {
setTargets();
git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
File link = new File(trash, "link");
assertFalse(
Files.exists(link.toPath(), LinkOption.NOFOLLOW_LINKS));
MergeResult result = git.merge().include(side)
.setMessage("merged").call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
// Link should have the content from side
assertSymLink(link, "target1");
checkTargets();
assertEquals("[link, mode:120000, stage:1, content:target]"
+ "[link, mode:120000, stage:3, content:target1]",
indexState(CONTENT));
}
}
}
@Test
public void mergeWithSymlinkModifyDelete() throws Exception {
assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
config.save();
try (TestRepository<Repository> repo = new TestRepository<>(db)) {
db.incrementOpen();
RevCommit base = repo
.commit(repo.tree(repo.link("link", repo.blob("target"))));
RevCommit side = repo.commit(repo.tree(), base);
RevCommit head = repo.commit(
repo.tree(repo.link("link", repo.blob("target2"))), base);
try (Git git = new Git(db)) {
setTargets();
git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
File link = new File(trash, "link");
assertSymLink(link, "target2");
MergeResult result = git.merge().include(side)
.setMessage("merged").call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
// Link should be unmodified
assertSymLink(link, "target2");
checkTargets();
assertEquals("[link, mode:120000, stage:1, content:target]"
+ "[link, mode:120000, stage:2, content:target2]",
indexState(CONTENT));
}
}
}
}