]> source.dussan.org Git - jgit.git/commitdiff
Checkout: better directory handling 42/204042/1
authorThomas Wolf <twolf@apache.org>
Fri, 11 Aug 2023 19:40:13 +0000 (21:40 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Sat, 2 Sep 2023 22:16:26 +0000 (00:16 +0200)
When checking out a file into the working tree ensure that all parent
directories of the file below the working tree root are actually
directories and do exist before we try to create the file.

When multiple files are to be checked out (or even a whole tree), this
may check the same directories over and over again. Asking the file
system every time for file attributes is a potentially expensive
operation. As a remedy, introduce an in-memory cache of directory
states for a particular check-out operation.

Apply the same fix also in the ResolveMerger, which may also check out
files, and also in the PatchApplier. In PatchApplier, also validate
paths.

Change-Id: Ie12864c54c9f901a2ccee7caddec73027f353111
Signed-off-by: Thomas Wolf <twolf@apache.org>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
18 files changed:
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java [new file with mode: 0644]
org.eclipse.jgit/.settings/.api_filters
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java

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 (file)
index 0000000..802fa15
--- /dev/null
@@ -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 (file)
index 0000000..03cacba
--- /dev/null
@@ -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 (file)
index 0000000..b38f87f
--- /dev/null
@@ -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 (file)
index 0000000..a275c85
--- /dev/null
@@ -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
index 8e84dfa31860be55ebf865d58c7585f969779a41..01d1e0282dbb7f11ada1dff5641fedfb84b6c435 100644 (file)
@@ -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 (file)
index 0000000..e3bc85a
--- /dev/null
@@ -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());
+                       }
+               }
+       }
+
+}
index e2637257c5220f54bd26a36b532d793d93a6f683..92d47c296677246de6b6562eaa5aefd88eab7294 100644 (file)
@@ -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 (file)
index 0000000..490c45b
--- /dev/null
@@ -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());
+                               }
+                       }
+               }
+       }
+}
index 75697fd88a882eebb6fb1f50973acb6fc2b8f086..35a3a0aebafeeb677fb43692b3921140eb09fc8d 100644 (file)
@@ -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>
             </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>
index bc8144f1c9a71e2664de5e3eafea1465cac1c380..3ac1fde6f0f6559eee833c970fc96fa8bd17ac6e 100644 (file)
@@ -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
index 7319ff4b2f0d646a3f0584730a36ceb48830dd24..8edae5a580fe80d750b1bfa5836f94582aa549e3 100644 (file)
@@ -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,
index 17036a9cd328741a03bbb20c19f44e09f812d018..e4157286f1158992a0280901045a9cc57d9f4009 100644 (file)
@@ -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 (file)
index 0000000..accf732
--- /dev/null
@@ -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
index 1fb81b71e9381dec1a152284468ba8269d0a2b10..d54df8d20e66b9b94c712f9486e37a03c2592f2f 100644 (file)
@@ -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);
        }
 
        /**
index 518e0b7d9b76e4ce3091c993b04f9371e059459f..3b5cc3d1f5244034e4583eed9f9e5116f0852bd9 100644 (file)
@@ -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 (file)
index 0000000..073bf7a
--- /dev/null
@@ -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;
+                       }
+               }
+       }
+
+}
index e56513d4e936c856bf81c6130be1aa503dfc2efb..04c17be392767f24dd316067c09f78e0d4970fbf 100644 (file)
@@ -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;
@@ -204,6 +204,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}.
@@ -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;
        }
 
index da698d6bf675eb16e91f1f704cbd26aec7d37a48..04300a9765ea4098451a8a84f0d12a241a36aec8 100644 (file)
@@ -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;
 
        /**