/* * 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 { boolean isDir = Files.isDirectory(f.toPath(), LinkOption.NOFOLLOW_LINKS); if (recursiveDelete && isDir) { FileUtils.delete(f, FileUtils.RECURSIVE); } if (cache.getRepository().isWorkTreeCaseInsensitive() && !isDir) { // We cannot rely on rename via Files.move() to work correctly // if the target exists in a case variant. For instance with JDK // 17 on Mac OS, the existing case-variant name is kept. On // Windows 11 it would work and use the name given in 'f'. FileUtils.delete(f, FileUtils.SKIP_MISSING); } 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)); } }