Checkout: better directory handling

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>
This commit is contained in:
Thomas Wolf 2023-08-11 21:40:13 +02:00 committed by Matthias Sohn
parent b4c3a5da0d
commit 9072103f3b
18 changed files with 1017 additions and 149 deletions

View File

@ -0,0 +1 @@
*.patch -crlf

View 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 void testIsValidPath() {
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")

View File

@ -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());
}
}
}
}

View File

@ -24,6 +24,7 @@
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 void testFiltering() throws Exception {
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");
}
}
}

View File

@ -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());
}
}
}
}
}

View 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>
@ -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>

View 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

View 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.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.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 CheckoutCommand setAllPaths(boolean all) {
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 @@ protected CheckoutCommand checkoutPaths() throws IOException,
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 @@ protected CheckoutCommand checkoutPaths() throws IOException,
}
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 void apply(DirCacheEntry ent) {
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 void apply(DirCacheEntry ent) {
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 void apply(DirCacheEntry ent) {
}
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 void apply(DirCacheEntry ent) {
}
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 void apply(DirCacheEntry ent) {
}
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,

View 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.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.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 @@ private void resetIndex(RevTree tree) throws IOException {
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 @@ private void resetUntracked(RevTree tree) throws CheckoutConflictException,
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 @@ private void resetUntracked(RevTree tree) throws CheckoutConflictException,
}
}
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,

View File

@ -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));
}
}

View 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 @@
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.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.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 CheckoutMetadata(EolStreamType eolStreamType,
private boolean performingCheckout;
private WorkingTreeOptions options;
private Checkout checkout;
private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
@ -495,9 +490,8 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
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 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
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 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
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 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
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 @@ private void keep(String path, DirCacheEntry e, WorkingTreeIterator f)
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 static void checkoutEntry(Repository repo, DirCacheEntry entry,
* 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);
}
/**

View File

@ -46,6 +46,8 @@ public static JGitText get() {
/***/ public String applyBinaryOidTooShort;
/***/ public String applyBinaryPatchTypeNotSupported;
/***/ public String applyBinaryResultOidWrong;
/***/ public String applyPatchDestInvalid;
/***/ public String applyPatchSourceInvalid;
/***/ public String applyPatchWithoutSourceOnAlreadyExistingSource;
/***/ public String applyPatchWithCreationOverAlreadyExistingDestination;
/***/ public String applyPatchWithSourceOnNonExistentSource;

View File

@ -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;
}
}
}
}

View 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.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.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 List<String> getModifiedFiles() {
*/
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 @@ private WorkTreeUpdater(Repository repo, DirCache dirCache) {
this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config);
this.checkoutMetadataByPath = new HashMap<>();
this.cleanupMetadataByPath = new HashMap<>();
this.checkout = new Checkout(nonNullRepo(), workingTreeOptions);
}
/**
@ -350,9 +357,8 @@ public void writeWorkTreeChanges(boolean shouldCheckoutTheirs)
}
// 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 @@ private void checkout() throws NoWorkTreeException, IOException {
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 void revertModifiedFiles() throws IOException {
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 void updateFileWithContent(StreamSupplier inputStream,
if (inCore) {
return;
}
checkout.safeCreateParentDirectory(path, file.getParentFile(),
false);
CheckoutMetadata metadata = new CheckoutMetadata(streamType,
smudgeCommand);
@ -1576,15 +1583,11 @@ private File writeMergedFile(TemporaryBuffer rawMerged,
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;
}

View 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.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.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.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 Result applyPatch(Patch p) throws IOException {
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 Result applyPatch(Patch p) throws IOException {
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 Result applyPatch(Patch p) throws IOException {
* 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 Result applyPatch(Patch p) throws IOException {
}
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 @@ private boolean verifyExistence(FileHeader fh, File src, File dest,
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;
/**