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