aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch9
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch9
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java12
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java61
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java26
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java259
-rw-r--r--org.eclipse.jgit/.settings/.api_filters16
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java26
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java238
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java111
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java309
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java47
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java33
18 files changed, 1044 insertions, 149 deletions
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch
new file mode 100644
index 0000000000..802fa15465
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch
@@ -0,0 +1,9 @@
+diff --git a/.git/b b/.git/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/.git/b
+@@ -0,0 +1,3 @@
++a
++b
++c \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch
new file mode 100644
index 0000000000..03cacbaeed
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch
@@ -0,0 +1,9 @@
+diff --git a/.GIT/b b/.GIT/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/.git/b
+@@ -0,0 +1,3 @@
++a
++b
++c \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes
new file mode 100644
index 0000000000..b38f87f9e3
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes
@@ -0,0 +1 @@
+*.patch -crlf
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch
new file mode 100644
index 0000000000..a275c8593f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch
@@ -0,0 +1,9 @@
+diff --git a/a/b b/a/b
+new file mode 100644
+index 0000000..de98044
+--- /dev/null
++++ b/a/b
+@@ -0,0 +1,3 @@
++a
++b
++c \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index 8e84dfa318..01d1e0282d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009, 2020 Google Inc. and others
+ * Copyright (C) 2009, 2023 Google Inc. 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
@@ -46,6 +46,16 @@ public class DirCacheEntryTest {
assertFalse(isValidPath("a/"));
assertFalse(isValidPath("ab/cd/ef/"));
assertFalse(isValidPath("a\u0000b"));
+ assertFalse(isValidPath(".git"));
+ assertFalse(isValidPath(".GIT"));
+ assertFalse(isValidPath(".Git"));
+ assertFalse(isValidPath(".git/b"));
+ assertFalse(isValidPath(".GIT/b"));
+ assertFalse(isValidPath(".Git/b"));
+ assertFalse(isValidPath("x/y/.git/z/b"));
+ assertFalse(isValidPath("x/y/.GIT/z/b"));
+ assertFalse(isValidPath("x/y/.Git/z/b"));
+ assertTrue(isValidPath("git/b"));
}
@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
new file mode 100644
index 0000000000..e3bc85a512
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.dircache;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/**
+ * Tests for checking out with invalid paths.
+ */
+public class InvalidPathCheckoutTest extends RepositoryTestCase {
+
+ private DirCacheEntry brokenEntry(String fileName, RevBlob blob) {
+ DirCacheEntry entry = new DirCacheEntry("XXXX/" + fileName);
+ entry.path[0] = '.';
+ entry.path[1] = 'g';
+ entry.path[2] = 'i';
+ entry.path[3] = 't';
+ entry.setFileMode(FileMode.REGULAR_FILE);
+ entry.setObjectId(blob);
+ return entry;
+ }
+
+ @Test
+ public void testCheckoutIntoDotGit() throws Exception {
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ // DirCacheEntry does not allow any path component to contain
+ // ".git". C git also forbids this. But what if somebody creates
+ // such an entry explicitly?
+ RevCommit base = repo
+ .commit(repo.tree(brokenEntry("b", repo.blob("test"))));
+ try (Git git = new Git(db)) {
+ assertThrows(InvalidPathException.class, () -> git.reset()
+ .setMode(ResetType.HARD).setRef(base.name()).call());
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ }
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
index e2637257c5..92d47c2966 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
@@ -24,6 +24,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.attributes.FilterCommand;
@@ -892,5 +893,30 @@ public class PatchApplierTest {
FilterCommandRegistry.unregister("jgit://builtin/a2e/smudge");
}
}
+
+ private void dotGitTest(String fileName) throws Exception {
+ init(fileName, false, false);
+ Result result = null;
+ IOException ex = null;
+ try {
+ result = applyPatch();
+ } catch (IOException e) {
+ ex = e;
+ }
+ assertTrue(ex != null
+ || (result != null && !result.getErrors().isEmpty()));
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ }
+
+ @Test
+ public void testDotGit() throws Exception {
+ dotGitTest("dotgit");
+ }
+
+ @Test
+ public void testDotGit2() throws Exception {
+ dotGitTest("dotgit2");
+ }
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
new file mode 100644
index 0000000000..490c45b558
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 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.symlinks;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.api.Git;
+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.patch.Patch;
+import org.eclipse.jgit.patch.PatchApplier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+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;
+
+@RunWith(Parameterized.class)
+public class DirectoryTest extends RepositoryTestCase {
+
+ @BeforeClass
+ public static void checkPrecondition() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+ Path tempDir = Files.createTempDirectory("jgit");
+ try {
+ Path a = tempDir.resolve("a");
+ Files.writeString(a, "test");
+ Path b = tempDir.resolve("A");
+ Assume.assumeTrue(Files.exists(b));
+ } finally {
+ FileUtils.delete(tempDir.toFile(),
+ FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+ }
+ }
+
+ @Parameters(name = "core.symlinks={0}")
+ public static Boolean[] parameters() {
+ return new Boolean[] { Boolean.TRUE, Boolean.FALSE };
+ }
+
+ @Parameter(0)
+ public boolean useSymlinks;
+
+ private void checkFiles() throws Exception {
+ File a = new File(trash, "a");
+ assertTrue("a should be a directory",
+ Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File b = new File(a, "b");
+ assertTrue("a/b should exist", b.isFile());
+ File x = new File(trash, "x");
+ assertTrue("x should be a directory",
+ Files.isDirectory(x.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File y = new File(x, "y");
+ assertTrue("x/y should exist", y.isFile());
+ }
+
+ @Test
+ public void testCheckout() throws Exception {
+ 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 links directly in the git repo, then use a hard reset
+ // to get them into the workspace.
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.link("A", repo.blob(".git")),
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testCheckout2() throws Exception {
+ 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("A/B", repo.blob("../.git")),
+ repo.file("a/b/a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ boolean testFiles = true;
+ try {
+ git.reset().setMode(ResetType.HARD).setRef(base.name())
+ .call();
+ } catch (Exception e) {
+ if (!useSymlinks) {
+ // There is a file in the middle of the path where we'd
+ // expect a directory. This case is not handled
+ // anywhere. What would be a better reply than an IOE?
+ testFiles = false;
+ } else {
+ throw e;
+ }
+ }
+ File a = new File(new File(trash, ".git"), "a");
+ assertFalse(".git/a should not exist", a.exists());
+ if (testFiles) {
+ a = new File(trash, "a");
+ assertTrue("a should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File b = new File(a, "b");
+ assertTrue("a/b should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ a = new File(b, "a");
+ assertTrue("a/b/a should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ b = new File(a, "b");
+ assertTrue("a/b/a/b should exist", b.isFile());
+ File x = new File(trash, "x");
+ assertTrue("x should be a directory", Files.isDirectory(
+ x.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File y = new File(x, "y");
+ assertTrue("x/y should exist", y.isFile());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testMerge() throws Exception {
+ 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.file("q", repo.blob("test"))));
+ RevCommit side = repo.commit(
+ repo.tree(
+ repo.link("A", repo.blob(".git")),
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ git.merge().include(side)
+ .setMessage("merged").call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testMerge2() throws Exception {
+ 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.file("q", repo.blob("test")),
+ repo.link("A", repo.blob(".git"))));
+ RevCommit side = repo.commit(
+ repo.tree(
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ git.merge().include(side)
+ .setMessage("merged").call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testApply() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ // PatchApplier doesn't do symlinks yet.
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.file("x", repo.blob("test")),
+ repo.link("A", repo.blob(".git"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ Patch patch = new Patch();
+ try (InputStream patchStream = this.getClass()
+ .getResourceAsStream("dirtest.patch")) {
+ patch.parse(patchStream);
+ }
+ boolean testFiles = true;
+ try {
+ PatchApplier.Result result = new PatchApplier(db)
+ .applyPatch(patch);
+ assertNotNull(result);
+ } catch (IOException e) {
+ if (!useSymlinks) {
+ // There is a file there, so the patch won't apply.
+ // Unclear whether an IOE is the correct response,
+ // though. Probably some negative PatchApplier.Result is
+ // more appropriate.
+ testFiles = false;
+ } else {
+ throw e;
+ }
+ }
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ if (testFiles) {
+ File a = new File(trash, "a");
+ assertTrue("a should be a directory",
+ Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ b = new File(a, "b");
+ assertTrue("a/b should exist", b.isFile());
+ }
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 75697fd88a..35a3a0aeba 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component id="org.eclipse.jgit" version="2">
+ <resource path="src/org/eclipse/jgit/dircache/Checkout.java" type="org.eclipse.jgit.dircache.Checkout">
+ <filter id="1109393411">
+ <message_arguments>
+ <message_argument value="6.6.1"/>
+ <message_argument value="org.eclipse.jgit.dircache.Checkout"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/errors/PackMismatchException.java" type="org.eclipse.jgit.errors.PackMismatchException">
<filter id="1142947843">
<message_arguments>
@@ -22,6 +30,14 @@
</message_arguments>
</filter>
</resource>
+ <resource path="src/org/eclipse/jgit/lib/FileModeCache.java" type="org.eclipse.jgit.lib.FileModeCache">
+ <filter id="1109393411">
+ <message_arguments>
+ <message_argument value="6.6.1"/>
+ <message_argument value="org.eclipse.jgit.lib.FileModeCache"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/revwalk/RevCommit.java" type="org.eclipse.jgit.revwalk.RevCommit">
<filter id="1193279491">
<message_arguments>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index bc8144f1c9..3ac1fde6f0 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -20,6 +20,8 @@ applyBinaryPatchTypeNotSupported=Couldn't apply binary patch of type {0}
applyTextPatchCannotApplyHunk=Hunk cannot be applied
applyTextPatchSingleClearingHunk=Expected a single hunk for clearing all content
applyBinaryResultOidWrong=Result of binary patch for file {0} has wrong OID
+applyPatchDestInvalid=Destination path in patch is invalid
+applyPatchSourceInvalid==Source path in patch is invalid
applyPatchWithoutSourceOnAlreadyExistingSource=Cannot perform {0} action on an existing file
applyPatchWithCreationOverAlreadyExistingDestination=Cannot perform {0} action which overrides an existing file
applyPatchWithSourceOnNonExistentSource=Cannot perform {0} action on a non-existent file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 7319ff4b2f..8edae5a580 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
- * Copyright (C) 2011, 2020 Matthias Sohn <matthias.sohn@sap.com> and others
+ * Copyright (C) 2011, 2023 Matthias Sohn <matthias.sohn@sap.com> 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
@@ -28,6 +28,7 @@ import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.dircache.Checkout;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
@@ -55,7 +56,6 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
/**
@@ -412,8 +412,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
protected CheckoutCommand checkoutPaths() throws IOException,
RefNotFoundException {
actuallyModifiedPaths = new HashSet<>();
- WorkingTreeOptions options = repo.getConfig()
- .get(WorkingTreeOptions.KEY);
+ Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
DirCache dc = repo.lockDirCache();
try (RevWalk revWalk = new RevWalk(repo);
TreeWalk treeWalk = new TreeWalk(repo,
@@ -422,10 +421,10 @@ public class CheckoutCommand extends GitCommand<Ref> {
if (!checkoutAllPaths)
treeWalk.setFilter(PathFilterGroup.createFromStrings(paths));
if (isCheckoutIndex())
- checkoutPathsFromIndex(treeWalk, dc, options);
+ checkoutPathsFromIndex(treeWalk, dc, checkout);
else {
RevCommit commit = revWalk.parseCommit(getStartPointObjectId());
- checkoutPathsFromCommit(treeWalk, dc, commit, options);
+ checkoutPathsFromCommit(treeWalk, dc, commit, checkout);
}
} finally {
try {
@@ -443,7 +442,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
}
private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc,
- WorkingTreeOptions options)
+ Checkout checkout)
throws IOException {
DirCacheIterator dci = new DirCacheIterator(dc);
treeWalk.addTree(dci);
@@ -469,7 +468,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
if (stage > DirCacheEntry.STAGE_0) {
if (checkoutStage != null) {
if (stage == checkoutStage.number) {
- checkoutPath(ent, r, options,
+ checkoutPath(ent, r, checkout, path,
new CheckoutMetadata(eolStreamType,
filterCommand));
actuallyModifiedPaths.add(path);
@@ -480,7 +479,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
throw new JGitInternalException(e.getMessage(), e);
}
} else {
- checkoutPath(ent, r, options,
+ checkoutPath(ent, r, checkout, path,
new CheckoutMetadata(eolStreamType,
filterCommand));
actuallyModifiedPaths.add(path);
@@ -494,7 +493,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
}
private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc,
- RevCommit commit, WorkingTreeOptions options) throws IOException {
+ RevCommit commit, Checkout checkout) throws IOException {
treeWalk.addTree(commit.getTree());
final ObjectReader r = treeWalk.getObjectReader();
DirCacheEditor editor = dc.editor();
@@ -516,7 +515,7 @@ public class CheckoutCommand extends GitCommand<Ref> {
}
ent.setObjectId(blobId);
ent.setFileMode(mode);
- checkoutPath(ent, r, options,
+ checkoutPath(ent, r, checkout, path,
new CheckoutMetadata(eolStreamType, filterCommand));
actuallyModifiedPaths.add(path);
}
@@ -526,10 +525,9 @@ public class CheckoutCommand extends GitCommand<Ref> {
}
private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
- WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) {
+ Checkout checkout, String path, CheckoutMetadata checkoutMetadata) {
try {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
- checkoutMetadata, options);
+ checkout.checkout(entry, checkoutMetadata, reader, path);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 17036a9cd3..e4157286f1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012, 2021 GitHub Inc. and others
+ * Copyright (C) 2012, 2023 GitHub Inc. 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
@@ -23,6 +23,7 @@ import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.StashApplyFailureException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
+import org.eclipse.jgit.dircache.Checkout;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheCheckout;
@@ -48,7 +49,6 @@ import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.WorkingTreeOptions;
/**
* Command class to apply a stashed commit.
@@ -383,8 +383,7 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
private void resetUntracked(RevTree tree) throws CheckoutConflictException,
IOException {
Set<String> actuallyModifiedPaths = new HashSet<>();
- WorkingTreeOptions options = repo.getConfig()
- .get(WorkingTreeOptions.KEY);
+ Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
// TODO maybe NameConflictTreeWalk ?
try (TreeWalk walk = new TreeWalk(repo)) {
walk.addTree(tree);
@@ -408,17 +407,17 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
FileTreeIterator fIter = walk
.getTree(1, FileTreeIterator.class);
+ String gitPath = entry.getPathString();
if (fIter != null) {
if (fIter.isModified(entry, true, reader)) {
// file exists and is dirty
- throw new CheckoutConflictException(
- entry.getPathString());
+ throw new CheckoutConflictException(gitPath);
}
}
- checkoutPath(entry, reader, options,
+ checkoutPath(entry, gitPath, reader, checkout,
new CheckoutMetadata(eolStreamType, null));
- actuallyModifiedPaths.add(entry.getPathString());
+ actuallyModifiedPaths.add(gitPath);
}
} finally {
if (!actuallyModifiedPaths.isEmpty()) {
@@ -428,11 +427,11 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
}
}
- private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
- WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) {
+ private void checkoutPath(DirCacheEntry entry, String gitPath,
+ ObjectReader reader,
+ Checkout checkout, CheckoutMetadata checkoutMetadata) {
try {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
- checkoutMetadata, options);
+ checkout.checkout(entry, checkoutMetadata, reader, gitPath);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
new file mode 100644
index 0000000000..accf732dc7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023, 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.dircache;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.StandardCopyOption;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.FileModeCache;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
+import org.eclipse.jgit.lib.FileModeCache.CacheItem;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * An object that can be used to check out many files.
+ *
+ * @since 6.6.1
+ */
+public class Checkout {
+
+ private final FileModeCache cache;
+
+ private final WorkingTreeOptions options;
+
+ private boolean recursiveDelete;
+
+ /**
+ * Creates a new {@link Checkout} for checking out from the given
+ * repository.
+ *
+ * @param repo
+ * the {@link Repository} to check out from
+ */
+ public Checkout(@NonNull Repository repo) {
+ this(repo, null);
+ }
+
+ /**
+ * Creates a new {@link Checkout} for checking out from the given
+ * repository.
+ *
+ * @param repo
+ * the {@link Repository} to check out from
+ * @param options
+ * the {@link WorkingTreeOptions} to use; if {@code null},
+ * read from the {@code repo} config when this object is
+ * created
+ */
+ public Checkout(@NonNull Repository repo, WorkingTreeOptions options) {
+ this.cache = new FileModeCache(repo);
+ this.options = options != null ? options
+ : repo.getConfig().get(WorkingTreeOptions.KEY);
+ }
+
+ /**
+ * Retrieves the {@link WorkingTreeOptions} of the repository that are
+ * used.
+ *
+ * @return the {@link WorkingTreeOptions}
+ */
+ public WorkingTreeOptions getWorkingTreeOptions() {
+ return options;
+ }
+
+ /**
+ * Defines whether directories that are in the way of the file to be checked
+ * out shall be deleted recursively.
+ *
+ * @param recursive
+ * whether to delete such directories recursively
+ * @return {@code this}
+ */
+ public Checkout setRecursiveDeletion(boolean recursive) {
+ this.recursiveDelete = recursive;
+ return this;
+ }
+
+ /**
+ * Ensure that the given parent directory exists, and cache the information
+ * that gitPath refers to a file.
+ *
+ * @param gitPath
+ * of the file to be written
+ * @param parentDir
+ * directory in which the file shall be placed, assumed to be the
+ * parent of the {@code gitPath}
+ * @param makeSpace
+ * whether to delete a possibly existing file at
+ * {@code parentDir}
+ * @throws IOException
+ * if the directory cannot be created, if necessary
+ */
+ public void safeCreateParentDirectory(String gitPath, File parentDir,
+ boolean makeSpace) throws IOException {
+ cache.safeCreateParentDirectory(gitPath, parentDir, makeSpace);
+ }
+
+ /**
+ * Checks out the gitlink given by the {@link DirCacheEntry}.
+ *
+ * @param entry
+ * {@link DirCacheEntry} to check out
+ * @param gitPath
+ * the git path of the entry, if known already; otherwise
+ * {@code null} and it's read from the entry itself
+ * @throws IOException
+ * if the gitlink cannot be checked out
+ */
+ public void checkoutGitlink(DirCacheEntry entry, String gitPath)
+ throws IOException {
+ FS fs = cache.getRepository().getFS();
+ File workingTree = cache.getRepository().getWorkTree();
+ String path = gitPath != null ? gitPath : entry.getPathString();
+ File gitlinkDir = new File(workingTree, path);
+ File parentDir = gitlinkDir.getParentFile();
+ CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir,
+ false);
+ FileUtils.mkdirs(gitlinkDir, true);
+ cachedParent.insert(path.substring(path.lastIndexOf('/') + 1),
+ FileMode.GITLINK);
+ entry.setLastModified(fs.lastModifiedInstant(gitlinkDir));
+ }
+
+ /**
+ * Checks out the file given by the {@link DirCacheEntry}.
+ *
+ * @param entry
+ * {@link DirCacheEntry} to check out
+ * @param metadata
+ * {@link CheckoutMetadata} to use for CR/LF handling and
+ * smudge filtering
+ * @param reader
+ * {@link ObjectReader} to use
+ * @param gitPath
+ * the git path of the entry, if known already; otherwise
+ * {@code null} and it's read from the entry itself
+ * @throws IOException
+ * if the file cannot be checked out
+ */
+ public void checkout(DirCacheEntry entry, CheckoutMetadata metadata,
+ ObjectReader reader, String gitPath) throws IOException {
+ if (metadata == null) {
+ metadata = CheckoutMetadata.EMPTY;
+ }
+ FS fs = cache.getRepository().getFS();
+ ObjectLoader ol = reader.open(entry.getObjectId());
+ String path = gitPath != null ? gitPath : entry.getPathString();
+ File f = new File(cache.getRepository().getWorkTree(), path);
+ File parentDir = f.getParentFile();
+ CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir,
+ true);
+ if (entry.getFileMode() == FileMode.SYMLINK
+ && options.getSymLinks() == SymLinks.TRUE) {
+ byte[] bytes = ol.getBytes();
+ String target = RawParseUtils.decode(bytes);
+ if (recursiveDelete && Files.isDirectory(f.toPath(),
+ LinkOption.NOFOLLOW_LINKS)) {
+ FileUtils.delete(f, FileUtils.RECURSIVE);
+ }
+ fs.createSymLink(f, target);
+ cachedParent.insert(f.getName(), FileMode.SYMLINK);
+ entry.setLength(bytes.length);
+ entry.setLastModified(fs.lastModifiedInstant(f));
+ return;
+ }
+
+ String name = f.getName();
+ if (name.length() > 200) {
+ name = name.substring(0, 200);
+ }
+ File tmpFile = File.createTempFile("._" + name, null, parentDir); //$NON-NLS-1$
+
+ DirCacheCheckout.getContent(cache.getRepository(), path, metadata, ol,
+ options,
+ new FileOutputStream(tmpFile));
+
+ // The entry needs to correspond to the on-disk file size. If the
+ // content was filtered (either by autocrlf handling or smudge
+ // filters) ask the file system again for the length. Otherwise the
+ // object loader knows the size
+ if (metadata.eolStreamType == EolStreamType.DIRECT
+ && metadata.smudgeFilterCommand == null) {
+ entry.setLength(ol.getSize());
+ } else {
+ entry.setLength(tmpFile.length());
+ }
+
+ if (options.isFileMode() && fs.supportsExecute()) {
+ if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
+ if (!fs.canExecute(tmpFile))
+ fs.setExecute(tmpFile, true);
+ } else {
+ if (fs.canExecute(tmpFile))
+ fs.setExecute(tmpFile, false);
+ }
+ }
+ try {
+ if (recursiveDelete && Files.isDirectory(f.toPath(),
+ LinkOption.NOFOLLOW_LINKS)) {
+ FileUtils.delete(f, FileUtils.RECURSIVE);
+ }
+ FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
+ cachedParent.remove(f.getName());
+ } catch (IOException e) {
+ throw new IOException(
+ MessageFormat.format(JGitText.get().renameFileFailed,
+ tmpFile.getPath(), f.getPath()),
+ e);
+ } finally {
+ if (tmpFile.exists()) {
+ FileUtils.delete(tmpFile);
+ }
+ }
+ entry.setLastModified(fs.lastModifiedInstant(f));
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 1fb81b71e9..d54df8d20e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -5,7 +5,7 @@
* Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com>
- * Copyright (C) 2017, 2022, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2023, 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
@@ -19,11 +19,9 @@ package org.eclipse.jgit.dircache;
import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
@@ -49,7 +47,6 @@ import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
-import org.eclipse.jgit.lib.CoreConfig.SymLinks;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectChecker;
@@ -69,9 +66,7 @@ import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.IntList;
-import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.EolStreamTypeUtil;
import org.slf4j.Logger;
@@ -144,7 +139,7 @@ public class DirCacheCheckout {
private boolean performingCheckout;
- private WorkingTreeOptions options;
+ private Checkout checkout;
private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
@@ -495,9 +490,8 @@ public class DirCacheCheckout {
MissingObjectException, IncorrectObjectTypeException,
CheckoutConflictException, IndexWriteException, CanceledException {
toBeDeleted.clear();
- options = repo.getConfig()
- .get(WorkingTreeOptions.KEY);
try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) {
+ checkout = new Checkout(repo, null);
if (headCommitTree != null)
preScanTwoTrees();
else
@@ -564,10 +558,9 @@ public class DirCacheCheckout {
CheckoutMetadata meta = e.getValue();
DirCacheEntry entry = dc.getEntry(path);
if (FileMode.GITLINK.equals(entry.getRawMode())) {
- checkoutGitlink(path, entry);
+ checkout.checkoutGitlink(entry, path);
} else {
- checkoutEntry(repo, entry, objectReader, false, meta,
- options);
+ checkout.checkout(entry, meta, objectReader, path);
}
e = null;
@@ -602,8 +595,8 @@ public class DirCacheCheckout {
break;
}
if (entry.getStage() == DirCacheEntry.STAGE_3) {
- checkoutEntry(repo, entry, objectReader, false,
- null, options);
+ checkout.checkout(entry, null, objectReader,
+ conflict);
break;
}
++entryIdx;
@@ -626,14 +619,6 @@ public class DirCacheCheckout {
return toBeDeleted.isEmpty();
}
- private void checkoutGitlink(String path, DirCacheEntry entry)
- throws IOException {
- File gitlinkDir = new File(repo.getWorkTree(), path);
- FileUtils.mkdirs(gitlinkDir, true);
- FS fs = repo.getFS();
- entry.setLastModified(fs.lastModifiedInstant(gitlinkDir));
- }
-
private static ArrayList<String> filterOut(ArrayList<String> strings,
IntList indicesToRemove) {
int n = indicesToRemove.size();
@@ -1232,10 +1217,11 @@ public class DirCacheCheckout {
if (force) {
if (f == null || f.isModified(e, true, walk.getObjectReader())) {
kept.add(path);
- checkoutEntry(repo, e, walk.getObjectReader(), false,
+ checkout.checkout(e,
new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP),
walk.getFilterCommand(
- Constants.ATTR_FILTER_TYPE_SMUDGE)), options);
+ Constants.ATTR_FILTER_TYPE_SMUDGE)),
+ walk.getObjectReader(), path);
}
}
}
@@ -1494,83 +1480,16 @@ public class DirCacheCheckout {
* they are loaded from the repository config
* @throws java.io.IOException
* @since 6.3
+ * @deprecated since 6.6.1; use {@link Checkout} instead
*/
+ @Deprecated
public static void checkoutEntry(Repository repo, DirCacheEntry entry,
ObjectReader or, boolean deleteRecursive,
CheckoutMetadata checkoutMetadata, WorkingTreeOptions options)
throws IOException {
- if (checkoutMetadata == null) {
- checkoutMetadata = CheckoutMetadata.EMPTY;
- }
- ObjectLoader ol = or.open(entry.getObjectId());
- File f = new File(repo.getWorkTree(), entry.getPathString());
- File parentDir = f.getParentFile();
- if (parentDir.isFile()) {
- FileUtils.delete(parentDir);
- }
- FileUtils.mkdirs(parentDir, true);
- FS fs = repo.getFS();
- WorkingTreeOptions opt = options != null ? options
- : repo.getConfig().get(WorkingTreeOptions.KEY);
- if (entry.getFileMode() == FileMode.SYMLINK
- && opt.getSymLinks() == SymLinks.TRUE) {
- byte[] bytes = ol.getBytes();
- String target = RawParseUtils.decode(bytes);
- if (deleteRecursive && f.isDirectory()) {
- FileUtils.delete(f, FileUtils.RECURSIVE);
- }
- fs.createSymLink(f, target);
- entry.setLength(bytes.length);
- entry.setLastModified(fs.lastModifiedInstant(f));
- return;
- }
-
- String name = f.getName();
- if (name.length() > 200) {
- name = name.substring(0, 200);
- }
- File tmpFile = File.createTempFile(
- "._" + name, null, parentDir); //$NON-NLS-1$
-
- getContent(repo, entry.getPathString(), checkoutMetadata, ol, opt,
- new FileOutputStream(tmpFile));
-
- // The entry needs to correspond to the on-disk filesize. If the content
- // was filtered (either by autocrlf handling or smudge filters) ask the
- // filesystem again for the length. Otherwise the objectloader knows the
- // size
- if (checkoutMetadata.eolStreamType == EolStreamType.DIRECT
- && checkoutMetadata.smudgeFilterCommand == null) {
- entry.setLength(ol.getSize());
- } else {
- entry.setLength(tmpFile.length());
- }
-
- if (opt.isFileMode() && fs.supportsExecute()) {
- if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
- if (!fs.canExecute(tmpFile))
- fs.setExecute(tmpFile, true);
- } else {
- if (fs.canExecute(tmpFile))
- fs.setExecute(tmpFile, false);
- }
- }
- try {
- if (deleteRecursive && f.isDirectory()) {
- FileUtils.delete(f, FileUtils.RECURSIVE);
- }
- FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
- } catch (IOException e) {
- throw new IOException(
- MessageFormat.format(JGitText.get().renameFileFailed,
- tmpFile.getPath(), f.getPath()),
- e);
- } finally {
- if (tmpFile.exists()) {
- FileUtils.delete(tmpFile);
- }
- }
- entry.setLastModified(fs.lastModifiedInstant(f));
+ Checkout checkout = new Checkout(repo, options)
+ .setRecursiveDeletion(deleteRecursive);
+ checkout.checkout(entry, checkoutMetadata, or, null);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 518e0b7d9b..3b5cc3d1f5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -46,6 +46,8 @@ public class JGitText extends TranslationBundle {
/***/ public String applyBinaryOidTooShort;
/***/ public String applyBinaryPatchTypeNotSupported;
/***/ public String applyBinaryResultOidWrong;
+ /***/ public String applyPatchDestInvalid;
+ /***/ public String applyPatchSourceInvalid;
/***/ public String applyPatchWithoutSourceOnAlreadyExistingSource;
/***/ public String applyPatchWithCreationOverAlreadyExistingDestination;
/***/ public String applyPatchWithSourceOnNonExistentSource;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java
new file mode 100644
index 0000000000..073bf7a0ca
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023, 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.lib;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+
+/**
+ * A hierarchical cache of {@link FileMode}s per git path.
+ *
+ * @since 6.6.1
+ */
+public class FileModeCache {
+
+ @NonNull
+ private final CacheItem root = new CacheItem(FileMode.TREE);
+
+ @NonNull
+ private final Repository repo;
+
+ /**
+ * Creates a new {@link FileModeCache} for a {@link Repository}.
+ *
+ * @param repo
+ * {@link Repository} this cache is for
+ */
+ public FileModeCache(@NonNull Repository repo) {
+ this.repo = repo;
+ }
+
+ /**
+ * Retrieves the {@link Repository}.
+ *
+ * @return the {@link Repository} this {@link FileModeCache} was created for
+ */
+ @NonNull
+ public Repository getRepository() {
+ return repo;
+ }
+
+ /**
+ * Obtains the {@link CacheItem} for the working tree root.
+ *
+ * @return the {@link CacheItem}
+ */
+ @NonNull
+ public CacheItem getRoot() {
+ return root;
+ }
+
+ /**
+ * Ensure that the given parent directory exists, and cache the information
+ * that gitPath refers to a file.
+ *
+ * @param gitPath
+ * of the file to be written
+ * @param parentDir
+ * directory in which the file shall be placed, assumed to be the
+ * parent of the {@code gitPath}
+ * @param makeSpace
+ * whether to delete a possibly existing file at
+ * {@code parentDir}
+ * @throws IOException
+ * if the directory cannot be created, if necessary
+ */
+ public void safeCreateParentDirectory(String gitPath, File parentDir,
+ boolean makeSpace) throws IOException {
+ CacheItem cachedParent = safeCreateDirectory(gitPath, parentDir,
+ makeSpace);
+ cachedParent.remove(gitPath.substring(gitPath.lastIndexOf('/') + 1));
+ }
+
+ /**
+ * Ensures the given directory {@code dir} with the given git path exists.
+ *
+ * @param gitPath
+ * of a file to be written
+ * @param dir
+ * directory in which the file shall be placed, assumed to be the
+ * parent of the {@code gitPath}
+ * @param makeSpace
+ * whether to remove a file that already at that name
+ * @return A {@link CacheItem} describing the directory, which is guaranteed
+ * to exist
+ * @throws IOException
+ * if the directory cannot be made to exist at the given
+ * location
+ */
+ public CacheItem safeCreateDirectory(String gitPath, File dir,
+ boolean makeSpace) throws IOException {
+ FS fs = repo.getFS();
+ int i = gitPath.lastIndexOf('/');
+ String parentPath = null;
+ if (i >= 0) {
+ if ((makeSpace && dir.isFile()) || fs.isSymLink(dir)) {
+ FileUtils.delete(dir);
+ }
+ parentPath = gitPath.substring(0, i);
+ deleteSymlinkParent(fs, parentPath, repo.getWorkTree());
+ }
+ FileUtils.mkdirs(dir, true);
+ CacheItem cachedParent = getRoot();
+ if (parentPath != null) {
+ cachedParent = add(parentPath, FileMode.TREE);
+ }
+ return cachedParent;
+ }
+
+ private void deleteSymlinkParent(FS fs, String gitPath, File workingTree)
+ throws IOException {
+ if (!fs.supportsSymlinks()) {
+ return;
+ }
+ String[] parts = gitPath.split("/"); //$NON-NLS-1$
+ int n = parts.length;
+ CacheItem cached = getRoot();
+ File p = workingTree;
+ for (int i = 0; i < n; i++) {
+ p = new File(p, parts[i]);
+ CacheItem cachedChild = cached != null ? cached.child(parts[i])
+ : null;
+ boolean delete = false;
+ if (cachedChild != null) {
+ if (FileMode.SYMLINK.equals(cachedChild.getMode())) {
+ delete = true;
+ }
+ } else {
+ try {
+ Path nioPath = FileUtils.toPath(p);
+ BasicFileAttributes attributes = nioPath.getFileSystem()
+ .provider()
+ .getFileAttributeView(nioPath,
+ BasicFileAttributeView.class,
+ LinkOption.NOFOLLOW_LINKS)
+ .readAttributes();
+ if (attributes.isSymbolicLink()) {
+ delete = p.isDirectory();
+ } else if (attributes.isRegularFile()) {
+ break;
+ }
+ } catch (InvalidPathException | IOException e) {
+ // If we can't get the attributes the path does not exist,
+ // or if it does a subsequent mkdirs() will also throw an
+ // exception.
+ break;
+ }
+ }
+ if (delete) {
+ // Deletes the symlink
+ FileUtils.delete(p, FileUtils.SKIP_MISSING);
+ if (cached != null) {
+ cached.remove(parts[i]);
+ }
+ break;
+ }
+ cached = cachedChild;
+ }
+ }
+
+ /**
+ * Records the given {@link FileMode} for the given git path in the cache.
+ * If an entry already exists for the given path, the previously cached file
+ * mode is overwritten.
+ *
+ * @param gitPath
+ * to cache the {@link FileMode} for
+ * @param finalMode
+ * {@link FileMode} to cache
+ * @return the {@link CacheItem} for the path
+ */
+ @NonNull
+ private CacheItem add(String gitPath, FileMode finalMode) {
+ if (gitPath.isEmpty()) {
+ throw new IllegalArgumentException();
+ }
+ String[] parts = gitPath.split("/"); //$NON-NLS-1$
+ int n = parts.length;
+ int i = 0;
+ CacheItem curr = getRoot();
+ while (i < n) {
+ CacheItem next = curr.child(parts[i]);
+ if (next == null) {
+ break;
+ }
+ curr = next;
+ i++;
+ }
+ if (i == n) {
+ curr.setMode(finalMode);
+ } else {
+ while (i < n) {
+ curr = curr.insert(parts[i],
+ i + 1 == n ? finalMode : FileMode.TREE);
+ i++;
+ }
+ }
+ return curr;
+ }
+
+ /**
+ * An item from a {@link FileModeCache}, recording information about a git
+ * path (known from context).
+ */
+ public static class CacheItem {
+
+ @NonNull
+ private FileMode mode;
+
+ private Map<String, CacheItem> children;
+
+ /**
+ * Creates a new {@link CacheItem}.
+ *
+ * @param mode
+ * {@link FileMode} to cache
+ */
+ public CacheItem(@NonNull FileMode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * Retrieves the cached {@link FileMode}.
+ *
+ * @return the {@link FileMode}
+ */
+ @NonNull
+ public FileMode getMode() {
+ return mode;
+ }
+
+ /**
+ * Retrieves an immediate child of this {@link CacheItem} by name.
+ *
+ * @param childName
+ * name of the child to get
+ * @return the {@link CacheItem}, or {@code null} if no such child is
+ * known
+ */
+ public CacheItem child(String childName) {
+ if (children == null) {
+ return null;
+ }
+ return children.get(childName);
+ }
+
+ /**
+ * Inserts a new cached {@link FileMode} as an immediate child of this
+ * {@link CacheItem}. If there is already a child with the same name, it
+ * is overwritten.
+ *
+ * @param childName
+ * name of the child to create
+ * @param childMode
+ * {@link FileMode} to cache
+ * @return the new {@link CacheItem} created for the child
+ */
+ public CacheItem insert(String childName, @NonNull FileMode childMode) {
+ if (!FileMode.TREE.equals(mode)) {
+ throw new IllegalArgumentException();
+ }
+ if (children == null) {
+ children = new HashMap<>();
+ }
+ CacheItem newItem = new CacheItem(childMode);
+ children.put(childName, newItem);
+ return newItem;
+ }
+
+ /**
+ * Removes the immediate child with the given name.
+ *
+ * @param childName
+ * name of the child to remove
+ * @return the previously cached {@link CacheItem}, if any
+ */
+ public CacheItem remove(String childName) {
+ if (children == null) {
+ return null;
+ }
+ return children.remove(childName);
+ }
+
+ void setMode(@NonNull FileMode mode) {
+ this.mode = mode;
+ if (!FileMode.TREE.equals(mode)) {
+ children = null;
+ }
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index e56513d4e9..04c17be392 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -3,8 +3,8 @@
* Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2012, Research In Motion Limited
* Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
- * Copyright (C) 2018, 2022 Thomas Wolf <twolf@apache.org>
- * Copyright (C) 2022, Google Inc. and others
+ * Copyright (C) 2018, 2023 Thomas Wolf <twolf@apache.org>
+ * Copyright (C) 2023, Google Inc. 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
@@ -47,6 +47,7 @@ import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.Checkout;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuildIterator;
import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -79,7 +80,6 @@ import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
-import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.LfsFactory;
import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
import org.eclipse.jgit.util.TemporaryBuffer;
@@ -205,6 +205,12 @@ public class ResolveMerger extends ThreeWayMerger {
private boolean indexChangesWritten;
/**
+ * {@link Checkout} to use for actually checking out files if
+ * {@link #inCore} is {@code false}.
+ */
+ private Checkout checkout;
+
+ /**
* @param repo
* the {@link Repository}.
* @param dirCache
@@ -223,6 +229,7 @@ public class ResolveMerger extends ThreeWayMerger {
this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config);
this.checkoutMetadataByPath = new HashMap<>();
this.cleanupMetadataByPath = new HashMap<>();
+ this.checkout = new Checkout(nonNullRepo(), workingTreeOptions);
}
/**
@@ -350,9 +357,8 @@ public class ResolveMerger extends ThreeWayMerger {
}
// All content operations are successfully done. If we can now write
- // the
- // new index we are on quite safe ground. Even if the checkout of
- // files coming from "theirs" fails the user can work around such
+ // the new index we are on quite safe ground. Even if the checkout
+ // of files coming from "theirs" fails the user can work around such
// failures by checking out the index again.
if (!builder.commit()) {
revertModifiedFiles();
@@ -518,14 +524,14 @@ public class ResolveMerger extends ThreeWayMerger {
for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
.entrySet()) {
DirCacheEntry dirCacheEntry = entry.getValue();
+ String gitPath = entry.getKey();
if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
- new File(nonNullRepo().getWorkTree(), entry.getKey())
- .mkdirs();
+ checkout.checkoutGitlink(dirCacheEntry, gitPath);
} else {
- DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader,
- false, checkoutMetadataByPath.get(entry.getKey()),
- workingTreeOptions);
- result.modifiedFiles.add(entry.getKey());
+ checkout.checkout(dirCacheEntry,
+ checkoutMetadataByPath.get(gitPath), reader,
+ gitPath);
+ result.modifiedFiles.add(gitPath);
}
}
}
@@ -550,9 +556,8 @@ public class ResolveMerger extends ThreeWayMerger {
for (String path : result.modifiedFiles) {
DirCacheEntry entry = dirCache.getEntry(path);
if (entry != null) {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, false,
- cleanupMetadataByPath.get(path),
- workingTreeOptions);
+ checkout.checkout(entry, cleanupMetadataByPath.get(path),
+ reader, path);
}
}
}
@@ -586,6 +591,8 @@ public class ResolveMerger extends ThreeWayMerger {
if (inCore) {
return;
}
+ checkout.safeCreateParentDirectory(path, file.getParentFile(),
+ false);
CheckoutMetadata metadata = new CheckoutMetadata(streamType,
smudgeCommand);
@@ -1576,15 +1583,11 @@ public class ResolveMerger extends ThreeWayMerger {
Attributes attributes)
throws IOException {
File workTree = nonNullRepo().getWorkTree();
- FS fs = nonNullRepo().getFS();
- File of = new File(workTree, tw.getPathString());
- File parentFolder = of.getParentFile();
+ String gitPath = tw.getPathString();
+ File of = new File(workTree, gitPath);
EolStreamType eol = workTreeUpdater.detectCheckoutStreamType(attributes);
- if (!fs.exists(parentFolder)) {
- parentFolder.mkdirs();
- }
workTreeUpdater.updateFileWithContent(rawMerged::openInputStream,
- eol, tw.getSmudgeCommand(attributes), of.getPath(), of);
+ eol, tw.getSmudgeCommand(attributes), gitPath, of);
return of;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
index da698d6bf6..04300a9765 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022, Google Inc. and others
+ * Copyright (C) 2023, Google Inc. 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
@@ -52,6 +52,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IndexWriteException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
@@ -59,6 +60,7 @@ import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.FileModeCache;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader;
@@ -81,6 +83,7 @@ import org.eclipse.jgit.util.LfsFactory;
import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
import org.eclipse.jgit.util.io.BinaryDeltaInputStream;
@@ -258,6 +261,7 @@ public class PatchApplier {
DirCache dirCache = inCore() ? DirCache.read(reader, beforeTree)
: repo.lockDirCache();
+ FileModeCache directoryCache = new FileModeCache(repo);
DirCacheBuilder dirCacheBuilder = dirCache.builder();
Set<String> modifiedPaths = new HashSet<>();
for (FileHeader fh : p.getFiles()) {
@@ -270,7 +274,8 @@ public class PatchApplier {
switch (type) {
case ADD: {
if (dest != null) {
- FileUtils.mkdirs(dest.getParentFile(), true);
+ directoryCache.safeCreateParentDirectory(fh.getNewPath(),
+ dest.getParentFile(), false);
FileUtils.createNewFile(dest);
}
apply(fh.getNewPath(), dirCache, dirCacheBuilder, dest, fh, result);
@@ -295,7 +300,8 @@ public class PatchApplier {
* apply() will write a fresh stream anyway, which will
* overwrite if there were hunks in the patch.
*/
- FileUtils.mkdirs(dest.getParentFile(), true);
+ directoryCache.safeCreateParentDirectory(fh.getNewPath(),
+ dest.getParentFile(), false);
FileUtils.rename(src, dest,
StandardCopyOption.ATOMIC_MOVE);
}
@@ -306,7 +312,8 @@ public class PatchApplier {
}
case COPY: {
if (!inCore()) {
- FileUtils.mkdirs(dest.getParentFile(), true);
+ directoryCache.safeCreateParentDirectory(fh.getNewPath(),
+ dest.getParentFile(), false);
Files.copy(src.toPath(), dest.toPath());
}
apply(fh.getOldPath(), dirCache, dirCacheBuilder, dest, fh, result);
@@ -401,9 +408,27 @@ public class PatchApplier {
fh.getPatchType()), fh.getNewPath(), null);
isValid = false;
}
+ if (srcShouldExist && !validGitPath(fh.getOldPath())) {
+ result.addError(JGitText.get().applyPatchSourceInvalid,
+ fh.getOldPath(), null);
+ isValid = false;
+ }
+ if (destShouldNotExist && !validGitPath(fh.getNewPath())) {
+ result.addError(JGitText.get().applyPatchDestInvalid,
+ fh.getNewPath(), null);
+ isValid = false;
+ }
return isValid;
}
+ private boolean validGitPath(String path) {
+ try {
+ SystemReader.getInstance().checkPath(path);
+ return true;
+ } catch (CorruptObjectException e) {
+ return false;
+ }
+ }
private static final int FILE_TREE_INDEX = 1;
/**