eclipse-jgit/jgit-CVE-2023-4759.patch

1695 lines
58 KiB
Diff
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties 2023-10-10 15:45:07.523229821 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties 2023-10-10 16:05:28.178175915 +0200
@@ -13,6 +13,8 @@
aNewObjectIdIsRequired=A NewObjectId is required.
anExceptionOccurredWhileTryingToAddTheIdOfHEAD=An exception occurred while trying to add the Id of HEAD
anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
+applyPatchDestInvalid=Destination path in patch is invalid
+applyPatchSourceInvalid==Source path in patch is invalid
applyingCommit=Applying {0}
archiveFormatAlreadyAbsent=Archive format already absent: {0}
archiveFormatAlreadyRegistered=Archive format already registered with different implementation: {0}
@@ -522,6 +524,8 @@
packWriterStatistics=Total {0,number,#0} (delta {1,number,#0}), reused {2,number,#0} (delta {3,number,#0})
panicCantRenameIndexFile=Panic: index file {0} must be renamed to replace {1}; until then repository is corrupt
patchApplyException=Cannot apply: {0}
+patchApplyErrorWithHunk=Error applying patch in {0}, hunk {1}: {2}
+patchApplyErrorWithoutHunk=Error applying patch in {0}: {1}
patchFormatException=Format error: {0}
pathNotConfigured=Submodule path is not configured
peeledLineBeforeRef=Peeled line before ref.
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/.settings/.api_filters 2023-10-10 15:45:07.523229821 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/.settings/.api_filters 2023-10-10 16:24:34.812579919 +0200
@@ -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="5.11.0"/>
+ <message_argument value="org.eclipse.jgit.dircache.Checkout"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
<filter id="338755678">
<message_arguments>
@@ -8,6 +16,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="5.11.0"/>
+ <message_argument value="org.eclipse.jgit.lib.FileModeCache"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/revwalk/ObjectWalk.java" type="org.eclipse.jgit.revwalk.ObjectWalk">
<filter id="421654647">
<message_arguments>
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java 2023-10-10 15:45:07.523229821 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java 2023-10-10 16:37:00.354302669 +0200
@@ -17,21 +17,34 @@
import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.api.errors.PatchFormatException;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.FileModeCache;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.patch.HunkHeader;
import org.eclipse.jgit.patch.Patch;
import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.SystemReader;
+
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
/**
* Apply a patch to files and/or to the index.
@@ -78,6 +91,7 @@
@Override
public ApplyResult call() throws GitAPIException, PatchFormatException,
PatchApplyException {
+ Result result = new Result();
checkCallable();
ApplyResult r = new ApplyResult();
try {
@@ -89,29 +103,33 @@
}
if (!p.getErrors().isEmpty())
throw new PatchFormatException(p.getErrors());
+ FileModeCache directoryCache = new FileModeCache(repo);
for (FileHeader fh : p.getFiles()) {
ChangeType type = fh.getChangeType();
File f = null;
+ if (!verifyExistence(fh, new File(repo.getWorkTree(), fh.getOldPath()), new File(repo.getWorkTree(), fh.getNewPath()), result)) {
+ continue;
+ }
switch (type) {
case ADD:
- f = getFile(fh.getNewPath(), true);
+ f = getFile(fh.getNewPath(), true, directoryCache);
apply(f, fh);
break;
case MODIFY:
- f = getFile(fh.getOldPath(), false);
+ f = getFile(fh.getOldPath(), false, directoryCache);
apply(f, fh);
break;
case DELETE:
- f = getFile(fh.getOldPath(), false);
+ f = getFile(fh.getOldPath(), false, directoryCache);
if (!f.delete())
throw new PatchApplyException(MessageFormat.format(
JGitText.get().cannotDeleteFile, f));
break;
case RENAME:
- f = getFile(fh.getOldPath(), false);
- File dest = getFile(fh.getNewPath(), false);
+ f = getFile(fh.getOldPath(), false, directoryCache);
+ File dest = getFile(fh.getNewPath(), false, directoryCache);
try {
- FileUtils.mkdirs(dest.getParentFile(), true);
+ directoryCache.safeCreateParentDirectory(fh.getNewPath(), dest.getParentFile(), false);
FileUtils.rename(f, dest,
StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
@@ -121,9 +139,9 @@
apply(dest, fh);
break;
case COPY:
- f = getFile(fh.getOldPath(), false);
- File target = getFile(fh.getNewPath(), false);
- FileUtils.mkdirs(target.getParentFile(), true);
+ f = getFile(fh.getOldPath(), false, directoryCache);
+ File target = getFile(fh.getNewPath(), false, directoryCache);
+ directoryCache.safeCreateParentDirectory(fh.getNewPath(), target.getParentFile(), false);
Files.copy(f.toPath(), target.toPath());
apply(target, fh);
}
@@ -137,13 +155,122 @@
return r;
}
- private File getFile(String path, boolean create)
+ /**
+ * A wrapper for returning both the applied tree ID and the applied files
+ * list, as well as file specific errors.
+ *
+ * @since 6.3
+ */
+ public static class Result {
+
+ /**
+ * A wrapper for a patch applying error that affects a given file.
+ *
+ * @since 6.6
+ */
+ // TODO(ms): rename this class in next major release
+ @SuppressWarnings("JavaLangClash")
+ public static class Error {
+
+ private String msg;
+ private String oldFileName;
+ private @Nullable HunkHeader hh;
+
+ private Error(String msg, String oldFileName,
+ @Nullable HunkHeader hh) {
+ this.msg = msg;
+ this.oldFileName = oldFileName;
+ this.hh = hh;
+ }
+
+ @Override
+ public String toString() {
+ if (hh != null) {
+ return MessageFormat.format(JGitText.get().patchApplyErrorWithHunk,
+ oldFileName, hh, msg);
+ }
+ return MessageFormat.format(JGitText.get().patchApplyErrorWithoutHunk,
+ oldFileName, msg);
+ }
+
+ }
+
+ private ObjectId treeId;
+
+ private List<String> paths;
+
+ private List<Error> errors = new ArrayList<>();
+
+ /**
+ * Get modified paths
+ *
+ * @return List of modified paths.
+ */
+ public List<String> getPaths() {
+ return paths;
+ }
+
+ /**
+ * Get tree ID
+ *
+ * @return The applied tree ID.
+ */
+ public ObjectId getTreeId() {
+ return treeId;
+ }
+
+ /**
+ * Get errors
+ *
+ * @return Errors occurred while applying the patch.
+ *
+ * @since 6.6
+ */
+ public List<Error> getErrors() {
+ return errors;
+ }
+
+ private void addError(String msg,String oldFileName, @Nullable HunkHeader hh) {
+ errors.add(new Error(msg, oldFileName, hh));
+ }
+ }
+
+ private boolean verifyExistence(FileHeader fh, File src, File dest,
+ Result result) throws IOException {
+ boolean isValid = true;
+ boolean srcShouldExist = Arrays.asList(new ChangeType[]{MODIFY, DELETE, RENAME, COPY})
+ .contains(fh.getChangeType());
+ boolean destShouldNotExist = Arrays.asList(new ChangeType[]{ADD, RENAME, COPY})
+ .contains(fh.getChangeType());
+ 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 File getFile(String path, boolean create, FileModeCache directoryCache)
throws PatchApplyException {
File f = new File(getRepository().getWorkTree(), path);
if (create)
try {
File parent = f.getParentFile();
- FileUtils.mkdirs(parent, true);
+ directoryCache.safeCreateParentDirectory(path, parent, false);
FileUtils.createNewFile(f);
} catch (IOException e) {
throw new PatchApplyException(MessageFormat.format(
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java 2023-10-10 15:45:07.523229821 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java 2023-10-10 16:04:27.644432299 +0200
@@ -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;
@@ -411,6 +412,7 @@
protected CheckoutCommand checkoutPaths() throws IOException,
RefNotFoundException {
actuallyModifiedPaths = new HashSet<>();
+ Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
DirCache dc = repo.lockDirCache();
try (RevWalk revWalk = new RevWalk(repo);
TreeWalk treeWalk = new TreeWalk(repo,
@@ -419,10 +421,10 @@
if (!checkoutAllPaths)
treeWalk.setFilter(PathFilterGroup.createFromStrings(paths));
if (isCheckoutIndex())
- checkoutPathsFromIndex(treeWalk, dc);
+ checkoutPathsFromIndex(treeWalk, dc, checkout);
else {
RevCommit commit = revWalk.parseCommit(getStartPointObjectId());
- checkoutPathsFromCommit(treeWalk, dc, commit);
+ checkoutPathsFromCommit(treeWalk, dc, commit, checkout);
}
} finally {
try {
@@ -439,7 +441,8 @@
return this;
}
- private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc)
+ private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc,
+ Checkout checkout)
throws IOException {
DirCacheIterator dci = new DirCacheIterator(dc);
treeWalk.addTree(dci);
@@ -465,8 +468,9 @@
if (stage > DirCacheEntry.STAGE_0) {
if (checkoutStage != null) {
if (stage == checkoutStage.number) {
- checkoutPath(ent, r, new CheckoutMetadata(
- eolStreamType, filterCommand));
+ checkoutPath(ent, r, checkout, path,
+ new CheckoutMetadata(eolStreamType,
+ filterCommand));
actuallyModifiedPaths.add(path);
}
} else {
@@ -475,7 +479,8 @@
throw new JGitInternalException(e.getMessage(), e);
}
} else {
- checkoutPath(ent, r, new CheckoutMetadata(eolStreamType,
+ checkoutPath(ent, r, checkout, path,
+ new CheckoutMetadata(eolStreamType,
filterCommand));
actuallyModifiedPaths.add(path);
}
@@ -488,7 +493,7 @@
}
private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc,
- RevCommit commit) throws IOException {
+ RevCommit commit, Checkout checkout) throws IOException {
treeWalk.addTree(commit.getTree());
final ObjectReader r = treeWalk.getObjectReader();
DirCacheEditor editor = dc.editor();
@@ -510,7 +515,7 @@
}
ent.setObjectId(blobId);
ent.setFileMode(mode);
- checkoutPath(ent, r,
+ checkoutPath(ent, r, checkout, path,
new CheckoutMetadata(eolStreamType, filterCommand));
actuallyModifiedPaths.add(path);
}
@@ -520,10 +525,9 @@
}
private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
- CheckoutMetadata checkoutMetadata) {
+ Checkout checkout, String path, CheckoutMetadata checkoutMetadata) {
try {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
- checkoutMetadata);
+ checkout.checkout(entry, checkoutMetadata, reader, path);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java 2023-10-10 15:45:07.526563177 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java 2023-10-10 16:04:27.644432299 +0200
@@ -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;
@@ -345,6 +346,7 @@
private void resetUntracked(RevTree tree) throws CheckoutConflictException,
IOException {
Set<String> actuallyModifiedPaths = new HashSet<>();
+ Checkout checkout = new Checkout(repo).setRecursiveDeletion(true);
// TODO maybe NameConflictTreeWalk ?
try (TreeWalk walk = new TreeWalk(repo)) {
walk.addTree(tree);
@@ -368,17 +370,17 @@
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,
+ checkoutPath(entry, gitPath, reader, checkout,
new CheckoutMetadata(eolStreamType, null));
- actuallyModifiedPaths.add(entry.getPathString());
+ actuallyModifiedPaths.add(gitPath);
}
} finally {
if (!actuallyModifiedPaths.isEmpty()) {
@@ -388,11 +390,11 @@
}
}
- private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
- CheckoutMetadata checkoutMetadata) {
+ private void checkoutPath(DirCacheEntry entry, String gitPath,
+ ObjectReader reader,
+ Checkout checkout, CheckoutMetadata checkoutMetadata) {
try {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
- checkoutMetadata);
+ checkout.checkout(entry, checkoutMetadata, reader, gitPath);
} catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().checkoutConflictWithFile,
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java 2023-10-10 16:04:27.647765655 +0200
@@ -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
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java 2023-10-10 15:45:07.529896533 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java 2023-10-10 16:39:28.708642256 +0200
@@ -18,10 +18,8 @@
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.OutputStream;
-import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
@@ -47,7 +45,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;
@@ -67,7 +64,6 @@
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;
@@ -142,6 +138,8 @@
private boolean performingCheckout;
+ private Checkout checkout;
+
private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
/**
@@ -492,6 +490,7 @@
CheckoutConflictException, IndexWriteException, CanceledException {
toBeDeleted.clear();
try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) {
+ checkout = new Checkout(repo, null);
if (headCommitTree != null)
preScanTwoTrees();
else
@@ -558,9 +557,9 @@
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);
+ checkout.checkout(entry, meta, objectReader, path);
}
e = null;
@@ -595,8 +594,8 @@
break;
}
if (entry.getStage() == DirCacheEntry.STAGE_3) {
- checkoutEntry(repo, entry, objectReader, false,
- null);
+ checkout.checkout(entry, null, objectReader,
+ conflict);
break;
}
++entryIdx;
@@ -619,14 +618,6 @@
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();
@@ -1225,10 +1216,11 @@
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)));
+ Constants.ATTR_FILTER_TYPE_SMUDGE)),
+ walk.getObjectReader(), path);
}
}
}
@@ -1453,76 +1445,9 @@
public static void checkoutEntry(Repository repo, DirCacheEntry entry,
ObjectReader or, boolean deleteRecursive,
CheckoutMetadata checkoutMetadata) 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 = 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, null)
+ .setRecursiveDeletion(deleteRecursive);
+ checkout.checkout(entry, checkoutMetadata, or, null);
}
/**
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java 2023-10-10 15:45:07.533229888 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java 2023-10-10 16:18:44.090214524 +0200
@@ -41,6 +41,8 @@
/***/ public String aNewObjectIdIsRequired;
/***/ public String anExceptionOccurredWhileTryingToAddTheIdOfHEAD;
/***/ public String anSSHSessionHasBeenAlreadyCreated;
+ /***/ public String applyPatchDestInvalid;
+ /***/ public String applyPatchSourceInvalid;
/***/ public String applyingCommit;
/***/ public String archiveFormatAlreadyAbsent;
/***/ public String archiveFormatAlreadyRegistered;
@@ -550,6 +552,8 @@
/***/ public String packWriterStatistics;
/***/ public String panicCantRenameIndexFile;
/***/ public String patchApplyException;
+ /***/ public String patchApplyErrorWithHunk;
+ /***/ public String patchApplyErrorWithoutHunk;
/***/ public String patchFormatException;
/***/ public String pathNotConfigured;
/***/ public String peeledLineBeforeRef;
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java 2023-10-10 16:04:27.647765655 +0200
@@ -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;
+ }
+ }
+ }
+
+}
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java 2023-10-10 15:45:07.539896600 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java 2023-10-10 16:04:27.647765655 +0200
@@ -43,10 +43,10 @@
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;
-import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.BinaryBlobException;
@@ -75,7 +75,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;
@@ -85,6 +84,13 @@
* A three-way merger performing a content-merge if necessary
*/
public class ResolveMerger extends ThreeWayMerger {
+
+ /**
+ * {@link Checkout} to use for actually checking out files if
+ * {@link #inCore} is {@code false}.
+ */
+ private Checkout checkout;
+
/**
* If the merge fails (means: not stopped because of unresolved conflicts)
* this enum is used to explain why it failed
@@ -314,6 +320,7 @@
implicitDirCache = true;
workingTreeOptions = local.getConfig().get(WorkingTreeOptions.KEY);
}
+ checkout = new Checkout(nonNullRepo(), workingTreeOptions);
}
/**
@@ -380,12 +387,15 @@
for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
.entrySet()) {
DirCacheEntry cacheEntry = entry.getValue();
+ String gitPath = entry.getKey();
if (cacheEntry.getFileMode() == FileMode.GITLINK) {
new File(nonNullRepo().getWorkTree(), entry.getKey()).mkdirs();
+ checkout.checkoutGitlink(cacheEntry, gitPath);
} else {
- DirCacheCheckout.checkoutEntry(db, cacheEntry, reader, false,
- checkoutMetadata.get(entry.getKey()));
- modifiedFiles.add(entry.getKey());
+ checkout.checkout(cacheEntry,
+ checkoutMetadata.get(entry.getKey()), reader,
+ gitPath);
+ modifiedFiles.add(gitPath);
}
}
}
@@ -415,8 +425,8 @@
String mpath = mpathsIt.next();
DirCacheEntry entry = dc.getEntry(mpath);
if (entry != null) {
- DirCacheCheckout.checkoutEntry(db, entry, reader, false,
- checkoutMetadata.get(mpath));
+ checkout.checkout(entry, checkoutMetadata.get(mpath),
+ reader, mpath);
}
mpathsIt.remove();
}
@@ -1009,15 +1019,12 @@
Attributes attributes)
throws FileNotFoundException, IOException {
File workTree = nonNullRepo().getWorkTree();
- FS fs = nonNullRepo().getFS();
- File of = new File(workTree, tw.getPathString());
- File parentFolder = of.getParentFile();
- if (!fs.exists(parentFolder)) {
- parentFolder.mkdirs();
- }
+ String gitPath = tw.getPathString();
+ File of = new File(workTree, gitPath);
EolStreamType streamType = EolStreamTypeUtil.detectStreamType(
OperationType.CHECKOUT_OP, workingTreeOptions,
attributes);
+ checkout.safeCreateParentDirectory(tw.getPathString(), of.getParentFile(), false);
try (OutputStream os = EolStreamTypeUtil.wrapOutputStream(
new BufferedOutputStream(new FileOutputStream(of)),
streamType)) {
@@ -1295,9 +1302,9 @@
// go into the new index.
checkout();
- // All content-merges 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
+ // All content-merges 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
// failures by checking out the index again.
if (!builder.commit()) {
cleanUp();
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java 2023-10-10 15:45:07.469896126 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java 2023-10-10 16:04:27.641098943 +0200
@@ -276,6 +276,25 @@
}
/**
+ * Construct a symlink mode tree entry.
+ *
+ * @param path
+ * path of the symlink.
+ * @param blob
+ * a blob, previously constructed in the repository.
+ * @return the entry.
+ * @throws Exception
+ * if an error occurred
+ * @since 5.13.3
+ */
+ public DirCacheEntry link(String path, RevBlob blob) throws Exception {
+ DirCacheEntry e = new DirCacheEntry(path);
+ e.setFileMode(FileMode.SYMLINK);
+ e.setObjectId(blob);
+ return e;
+ }
+
+ /**
* Construct a tree from a specific listing of file entries.
*
* @param entries
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java 2023-10-10 15:45:07.509896397 +0200
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java 2023-10-10 16:04:27.644432299 +0200
@@ -46,6 +46,16 @@
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")
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java 2023-10-10 16:04:27.644432299 +0200
@@ -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());
+ }
+ }
+ }
+
+}
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java 2023-10-10 16:04:27.644432299 +0200
@@ -0,0 +1,254 @@
+/*
+ * 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.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.api.ApplyResult;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.PatchApplyException;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.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.write(a, "test".getBytes(StandardCharsets.UTF_8));
+ 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)) {
+ boolean testFiles = true;
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ try (InputStream patchStream = this.getClass()
+ .getResourceAsStream("dirtest.patch")) {
+ ApplyResult result = git.apply().setPatch(patchStream).call();
+ assertNotNull(result);
+ } catch (PatchApplyException 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());
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit2.patch 2023-10-10 16:04:27.641098943 +0200
@@ -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
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/dotgit.patch 2023-10-10 16:04:27.641098943 +0200
@@ -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
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/dirtest.patch 2023-10-10 16:04:27.644432299 +0200
@@ -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
--- jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes 1970-01-01 01:00:00.000000000 +0100
+++ jgit-5.11.0.202103091610-r/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/symlinks/.gitattributes 2023-10-10 16:04:27.641098943 +0200
@@ -0,0 +1 @@
+*.patch -crlf