diff --git a/eclipse-jgit.changes b/eclipse-jgit.changes index e507a99..b633fab 100644 --- a/eclipse-jgit.changes +++ b/eclipse-jgit.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Tue Oct 10 15:09:41 UTC 2023 - Fridrich Strba + +- Added patch: + * jgit-CVE-2023-4759.patch + + backport of upstream fix for bsc#1215298 (CVE-2023-4759), + arbitrary file overwrite + ------------------------------------------------------------------- Fri Oct 6 11:04:41 UTC 2023 - Fridrich Strba @@ -5,6 +13,13 @@ Fri Oct 6 11:04:41 UTC 2023 - Fridrich Strba * 0001-Ensure-the-correct-classpath-is-set-for-the-jgit-com.patch + no need to patch the jgit.sh launcher that we do not use +------------------------------------------------------------------- +Fri Oct 6 11:00:40 UTC 2023 - Fridrich Strba + +- Craft the jgit script from the real Main class of the jar file + instead of using some superfluous jar launcher. + Fixes bsc#1209646 + ------------------------------------------------------------------- Wed May 31 19:51:51 UTC 2023 - Fridrich Strba @@ -20,6 +35,13 @@ Fri May 5 08:24:40 UTC 2023 - Fridrich Strba - Add _multibuild to define 2nd spec file as additional flavor. Eliminates the need for source package links in OBS. +------------------------------------------------------------------- +Mon Mar 27 08:18:14 UTC 2023 - Fridrich Strba + +- Require xz-java because the jgit script that we install is + expecting it to be present when composing the classpath + (bsc#1209646) + ------------------------------------------------------------------- Wed Nov 16 11:24:53 UTC 2022 - Fridrich Strba diff --git a/eclipse-jgit.spec b/eclipse-jgit.spec index f2671ba..b9bc19b 100644 --- a/eclipse-jgit.spec +++ b/eclipse-jgit.spec @@ -36,6 +36,7 @@ Patch2: jgit-shade.patch Patch3: jgit-5.11.0-java8.patch Patch4: jgit-apache-sshd-2.7.0.patch Patch5: jgit-jsch.patch +Patch6: jgit-CVE-2023-4759.patch # For main build BuildRequires: ant BuildRequires: apache-commons-compress @@ -95,6 +96,7 @@ A pure Java implementation of the Git version control system. %patch3 -p1 %patch4 -p1 %patch5 -p1 +%patch6 -p1 # Disable multithreaded build rm .mvn/maven.config diff --git a/jgit-CVE-2023-4759.patch b/jgit-CVE-2023-4759.patch new file mode 100644 index 0000000..3b41ddb --- /dev/null +++ b/jgit-CVE-2023-4759.patch @@ -0,0 +1,1694 @@ +--- 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 @@ + + ++ ++ ++ ++ ++ ++ ++ ++ + + + +@@ -8,6 +16,14 @@ + + + ++ ++ ++ ++ ++ ++ ++ ++ + + + +--- 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 paths; ++ ++ private List errors = new ArrayList<>(); ++ ++ /** ++ * Get modified paths ++ * ++ * @return List of modified paths. ++ */ ++ public List 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 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 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 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 filterOut(ArrayList 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 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 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 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 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 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 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 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 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 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 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 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 diff --git a/jgit.changes b/jgit.changes index 57ab59b..b633fab 100644 --- a/jgit.changes +++ b/jgit.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Tue Oct 10 15:09:41 UTC 2023 - Fridrich Strba + +- Added patch: + * jgit-CVE-2023-4759.patch + + backport of upstream fix for bsc#1215298 (CVE-2023-4759), + arbitrary file overwrite + ------------------------------------------------------------------- Fri Oct 6 11:04:41 UTC 2023 - Fridrich Strba @@ -21,6 +29,12 @@ Wed May 31 19:51:51 UTC 2023 - Fridrich Strba + allows building with 0.2.x (which is backward compatible with 0.1.x) +------------------------------------------------------------------- +Fri May 5 08:24:40 UTC 2023 - Fridrich Strba + +- Add _multibuild to define 2nd spec file as additional flavor. + Eliminates the need for source package links in OBS. + ------------------------------------------------------------------- Mon Mar 27 08:18:14 UTC 2023 - Fridrich Strba @@ -35,6 +49,12 @@ Wed Nov 16 11:24:53 UTC 2022 - Fridrich Strba * jgit-apache-sshd-2.7.0.patch + Allow building against apache-sshd 2.8.x and 2.9.x +------------------------------------------------------------------- +Tue Mar 29 14:06:34 UTC 2022 - Fridrich Strba + +- Force building with Java 11, since tycho is not knowing about any + Java >= 15 + ------------------------------------------------------------------- Fri Jul 30 12:24:56 UTC 2021 - Fridrich Strba @@ -54,6 +74,11 @@ Fri Jul 30 12:24:56 UTC 2021 - Fridrich Strba * 0003-Remove-requirement-on-assertj-core.patch + Not needed anymore +------------------------------------------------------------------- +Thu Nov 19 13:00:00 UTC 2020 - Fridrich Strba + +- Fix provides + ------------------------------------------------------------------- Thu Jul 16 21:23:15 UTC 2020 - Fridrich Strba diff --git a/jgit.spec b/jgit.spec index 194c4fd..94567e4 100644 --- a/jgit.spec +++ b/jgit.spec @@ -36,6 +36,7 @@ Patch2: jgit-shade.patch Patch3: jgit-5.11.0-java8.patch Patch4: jgit-apache-sshd-2.7.0.patch Patch5: jgit-jsch.patch +Patch6: jgit-CVE-2023-4759.patch # For main build BuildRequires: ant BuildRequires: fdupes @@ -104,6 +105,7 @@ Group: Documentation/HTML %patch3 -p1 %patch4 -p1 %patch5 -p1 +%patch6 -p1 # Disable multithreaded build rm .mvn/maven.config @@ -160,7 +162,7 @@ done %fdupes -s %{buildroot}%{_javadocdir} # Binary -%jpackage_script org.eclipse.jgit.pgm.Main "" "" javaewah:jzlib:jsch:jgit/org.eclipse.jgit:slf4j/api:slf4j/simple:args4j:commons-compress:httpcomponents/httpcore:httpcomponents/httpclient:commons-logging:commons-codec:eddsa:apache-sshd/sshd-osgi:apache-sshd/sshd-sftp %{name} +%jpackage_script org.eclipse.jgit.pgm.Main "" "" javaewah:jzlib:jsch:jgit:slf4j/api:slf4j/simple:args4j:commons-compress:httpcomponents/httpcore:httpcomponents/httpclient:commons-logging:commons-codec:eddsa:apache-sshd/sshd-osgi:apache-sshd/sshd-sftp %{name} # Ant task configuration install -dm 755 %{buildroot}%{_sysconfdir}/ant.d