| /* |
| * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| package org.eclipse.jgit.lib; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.InvalidPathException; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Path; |
| import java.nio.file.attribute.BasicFileAttributeView; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.eclipse.jgit.annotations.NonNull; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.FileUtils; |
| |
| /** |
| * A hierarchical cache of {@link FileMode}s per git path. |
| * |
| * @since 6.6.1 |
| */ |
| public class FileModeCache { |
| |
| @NonNull |
| private final CacheItem root = new CacheItem(FileMode.TREE); |
| |
| @NonNull |
| private final Repository repo; |
| |
| /** |
| * Creates a new {@link FileModeCache} for a {@link Repository}. |
| * |
| * @param repo |
| * {@link Repository} this cache is for |
| */ |
| public FileModeCache(@NonNull Repository repo) { |
| this.repo = repo; |
| } |
| |
| /** |
| * Retrieves the {@link Repository}. |
| * |
| * @return the {@link Repository} this {@link FileModeCache} was created for |
| */ |
| @NonNull |
| public Repository getRepository() { |
| return repo; |
| } |
| |
| /** |
| * Obtains the {@link CacheItem} for the working tree root. |
| * |
| * @return the {@link CacheItem} |
| */ |
| @NonNull |
| public CacheItem getRoot() { |
| return root; |
| } |
| |
| /** |
| * Ensure that the given parent directory exists, and cache the information |
| * that gitPath refers to a file. |
| * |
| * @param gitPath |
| * of the file to be written |
| * @param parentDir |
| * directory in which the file shall be placed, assumed to be the |
| * parent of the {@code gitPath} |
| * @param makeSpace |
| * whether to delete a possibly existing file at |
| * {@code parentDir} |
| * @throws IOException |
| * if the directory cannot be created, if necessary |
| */ |
| public void safeCreateParentDirectory(String gitPath, File parentDir, |
| boolean makeSpace) throws IOException { |
| CacheItem cachedParent = safeCreateDirectory(gitPath, parentDir, |
| makeSpace); |
| cachedParent.remove(gitPath.substring(gitPath.lastIndexOf('/') + 1)); |
| } |
| |
| /** |
| * Ensures the given directory {@code dir} with the given git path exists. |
| * |
| * @param gitPath |
| * of a file to be written |
| * @param dir |
| * directory in which the file shall be placed, assumed to be the |
| * parent of the {@code gitPath} |
| * @param makeSpace |
| * whether to remove a file that already at that name |
| * @return A {@link CacheItem} describing the directory, which is guaranteed |
| * to exist |
| * @throws IOException |
| * if the directory cannot be made to exist at the given |
| * location |
| */ |
| public CacheItem safeCreateDirectory(String gitPath, File dir, |
| boolean makeSpace) throws IOException { |
| FS fs = repo.getFS(); |
| int i = gitPath.lastIndexOf('/'); |
| String parentPath = null; |
| if (i >= 0) { |
| if ((makeSpace && dir.isFile()) || fs.isSymLink(dir)) { |
| FileUtils.delete(dir); |
| } |
| parentPath = gitPath.substring(0, i); |
| deleteSymlinkParent(fs, parentPath, repo.getWorkTree()); |
| } |
| FileUtils.mkdirs(dir, true); |
| CacheItem cachedParent = getRoot(); |
| if (parentPath != null) { |
| cachedParent = add(parentPath, FileMode.TREE); |
| } |
| return cachedParent; |
| } |
| |
| private void deleteSymlinkParent(FS fs, String gitPath, File workingTree) |
| throws IOException { |
| if (!fs.supportsSymlinks()) { |
| return; |
| } |
| String[] parts = gitPath.split("/"); //$NON-NLS-1$ |
| int n = parts.length; |
| CacheItem cached = getRoot(); |
| File p = workingTree; |
| for (int i = 0; i < n; i++) { |
| p = new File(p, parts[i]); |
| CacheItem cachedChild = cached != null ? cached.child(parts[i]) |
| : null; |
| boolean delete = false; |
| if (cachedChild != null) { |
| if (FileMode.SYMLINK.equals(cachedChild.getMode())) { |
| delete = true; |
| } |
| } else { |
| try { |
| Path nioPath = FileUtils.toPath(p); |
| BasicFileAttributes attributes = nioPath.getFileSystem() |
| .provider() |
| .getFileAttributeView(nioPath, |
| BasicFileAttributeView.class, |
| LinkOption.NOFOLLOW_LINKS) |
| .readAttributes(); |
| if (attributes.isSymbolicLink()) { |
| delete = p.isDirectory(); |
| } else if (attributes.isRegularFile()) { |
| break; |
| } |
| } catch (InvalidPathException | IOException e) { |
| // If we can't get the attributes the path does not exist, |
| // or if it does a subsequent mkdirs() will also throw an |
| // exception. |
| break; |
| } |
| } |
| if (delete) { |
| // Deletes the symlink |
| FileUtils.delete(p, FileUtils.SKIP_MISSING); |
| if (cached != null) { |
| cached.remove(parts[i]); |
| } |
| break; |
| } |
| cached = cachedChild; |
| } |
| } |
| |
| /** |
| * Records the given {@link FileMode} for the given git path in the cache. |
| * If an entry already exists for the given path, the previously cached file |
| * mode is overwritten. |
| * |
| * @param gitPath |
| * to cache the {@link FileMode} for |
| * @param finalMode |
| * {@link FileMode} to cache |
| * @return the {@link CacheItem} for the path |
| */ |
| @NonNull |
| private CacheItem add(String gitPath, FileMode finalMode) { |
| if (gitPath.isEmpty()) { |
| throw new IllegalArgumentException(); |
| } |
| String[] parts = gitPath.split("/"); //$NON-NLS-1$ |
| int n = parts.length; |
| int i = 0; |
| CacheItem curr = getRoot(); |
| while (i < n) { |
| CacheItem next = curr.child(parts[i]); |
| if (next == null) { |
| break; |
| } |
| curr = next; |
| i++; |
| } |
| if (i == n) { |
| curr.setMode(finalMode); |
| } else { |
| while (i < n) { |
| curr = curr.insert(parts[i], |
| i + 1 == n ? finalMode : FileMode.TREE); |
| i++; |
| } |
| } |
| return curr; |
| } |
| |
| /** |
| * An item from a {@link FileModeCache}, recording information about a git |
| * path (known from context). |
| */ |
| public static class CacheItem { |
| |
| @NonNull |
| private FileMode mode; |
| |
| private Map<String, CacheItem> children; |
| |
| /** |
| * Creates a new {@link CacheItem}. |
| * |
| * @param mode |
| * {@link FileMode} to cache |
| */ |
| public CacheItem(@NonNull FileMode mode) { |
| this.mode = mode; |
| } |
| |
| /** |
| * Retrieves the cached {@link FileMode}. |
| * |
| * @return the {@link FileMode} |
| */ |
| @NonNull |
| public FileMode getMode() { |
| return mode; |
| } |
| |
| /** |
| * Retrieves an immediate child of this {@link CacheItem} by name. |
| * |
| * @param childName |
| * name of the child to get |
| * @return the {@link CacheItem}, or {@code null} if no such child is |
| * known |
| */ |
| public CacheItem child(String childName) { |
| if (children == null) { |
| return null; |
| } |
| return children.get(childName); |
| } |
| |
| /** |
| * Inserts a new cached {@link FileMode} as an immediate child of this |
| * {@link CacheItem}. If there is already a child with the same name, it |
| * is overwritten. |
| * |
| * @param childName |
| * name of the child to create |
| * @param childMode |
| * {@link FileMode} to cache |
| * @return the new {@link CacheItem} created for the child |
| */ |
| public CacheItem insert(String childName, @NonNull FileMode childMode) { |
| if (!FileMode.TREE.equals(mode)) { |
| throw new IllegalArgumentException(); |
| } |
| if (children == null) { |
| children = new HashMap<>(); |
| } |
| CacheItem newItem = new CacheItem(childMode); |
| children.put(childName, newItem); |
| return newItem; |
| } |
| |
| /** |
| * Removes the immediate child with the given name. |
| * |
| * @param childName |
| * name of the child to remove |
| * @return the previously cached {@link CacheItem}, if any |
| */ |
| public CacheItem remove(String childName) { |
| if (children == null) { |
| return null; |
| } |
| return children.remove(childName); |
| } |
| |
| void setMode(@NonNull FileMode mode) { |
| this.mode = mode; |
| if (!FileMode.TREE.equals(mode)) { |
| children = null; |
| } |
| } |
| } |
| |
| } |