blob: 073bf7a0ca4eb42d1844d7a4756f8cac7a5f9da8 [file] [log] [blame]
/*
* 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;
}
}
}
}