aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org
diff options
context:
space:
mode:
authorThomas Wolf <twolf@apache.org>2025-02-07 21:25:08 +0100
committerThomas Wolf <twolf@apache.org>2025-02-09 13:44:01 +0100
commitf41253804068768c1d44a01808805e15a7b63eb6 (patch)
treeb9109c1364fce6f20907c3f370508d1a07025848 /org.eclipse.jgit/src/org
parent523878915ccf6f013d084c552d6b7219d0dfc02a (diff)
downloadjgit-f41253804068768c1d44a01808805e15a7b63eb6.tar.gz
jgit-f41253804068768c1d44a01808805e15a7b63eb6.zip
Merge: improve handling of case-variants
Ensure that on a case-insensitive filesystem a merge that includes a rename of a file from one case variant to another does not delete the file. Basically make sure that we don't delete files that we had marked under a case variant as "keep" before, and ensure that when checking out a file, it is written to the file system with the exact casing recorded in the git tree. Bug: egit-76 Change-Id: Ibbc9ba97c70971ba3e83381b41364a5529f5a5dc
Diffstat (limited to 'org.eclipse.jgit/src/org')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java12
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java68
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java40
3 files changed, 78 insertions, 42 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
index accf732dc7..de02aecdb9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
@@ -217,10 +217,18 @@ public class Checkout {
}
}
try {
- if (recursiveDelete && Files.isDirectory(f.toPath(),
- LinkOption.NOFOLLOW_LINKS)) {
+ 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) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 4f78404f48..d61a453973 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -5,7 +5,7 @@
* Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com>
- * Copyright (C) 2017, 2023, Thomas Wolf <twolf@apache.org> and others
+ * Copyright (C) 2017, 2025, 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
@@ -31,6 +31,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.FilterFailedException;
@@ -66,7 +67,6 @@ import org.eclipse.jgit.treewalk.WorkingTreeOptions;
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.IntList;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.EolStreamTypeUtil;
import org.slf4j.Logger;
@@ -113,9 +113,11 @@ public class DirCacheCheckout {
private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>();
+ private Set<String> existing;
+
private ArrayList<String> conflicts = new ArrayList<>();
- private ArrayList<String> removed = new ArrayList<>();
+ private TreeSet<String> removed;
private ArrayList<String> kept = new ArrayList<>();
@@ -185,7 +187,7 @@ public class DirCacheCheckout {
* @return a list of all files removed by this checkout
*/
public List<String> getRemoved() {
- return removed;
+ return new ArrayList<>(removed);
}
/**
@@ -214,6 +216,14 @@ public class DirCacheCheckout {
this.mergeCommitTree = mergeCommitTree;
this.workingTree = workingTree;
this.initialCheckout = !repo.isBare() && !repo.getIndexFile().exists();
+ boolean caseInsensitive = !repo.isBare()
+ && repo.isWorkTreeCaseInsensitive();
+ this.removed = caseInsensitive
+ ? new TreeSet<>(String::compareToIgnoreCase)
+ : new TreeSet<>();
+ this.existing = caseInsensitive
+ ? new TreeSet<>(String::compareToIgnoreCase)
+ : null;
}
/**
@@ -521,6 +531,13 @@ public class DirCacheCheckout {
// update our index
builder.finish();
+ // On case-insensitive file systems we may have a case variant kept
+ // and another one removed. In that case, don't remove it.
+ if (existing != null) {
+ removed.removeAll(existing);
+ existing.clear();
+ }
+
// init progress reporting
int numTotal = removed.size() + updated.size() + conflicts.size();
monitor.beginTask(JGitText.get().checkingOutFiles, numTotal);
@@ -531,9 +548,9 @@ public class DirCacheCheckout {
// when deleting files process them in the opposite order as they have
// been reported. This ensures the files are deleted before we delete
// their parent folders
- IntList nonDeleted = new IntList();
- for (int i = removed.size() - 1; i >= 0; i--) {
- String r = removed.get(i);
+ Iterator<String> iter = removed.descendingIterator();
+ while (iter.hasNext()) {
+ String r = iter.next();
file = new File(repo.getWorkTree(), r);
if (!file.delete() && repo.getFS().exists(file)) {
// The list of stuff to delete comes from the index
@@ -542,7 +559,7 @@ public class DirCacheCheckout {
// to delete it. A submodule is not empty, so it
// is safe to check this after a failed delete.
if (!repo.getFS().isDirectory(file)) {
- nonDeleted.add(i);
+ iter.remove();
toBeDeleted.add(r);
}
} else {
@@ -560,8 +577,6 @@ public class DirCacheCheckout {
if (file != null) {
removeEmptyParents(file);
}
- removed = filterOut(removed, nonDeleted);
- nonDeleted = null;
Iterator<Map.Entry<String, CheckoutMetadata>> toUpdate = updated
.entrySet().iterator();
Map.Entry<String, CheckoutMetadata> e = null;
@@ -633,36 +648,6 @@ public class DirCacheCheckout {
return toBeDeleted.isEmpty();
}
- private static ArrayList<String> filterOut(ArrayList<String> strings,
- IntList indicesToRemove) {
- int n = indicesToRemove.size();
- if (n == strings.size()) {
- return new ArrayList<>(0);
- }
- switch (n) {
- case 0:
- return strings;
- case 1:
- strings.remove(indicesToRemove.get(0));
- return strings;
- default:
- int length = strings.size();
- ArrayList<String> result = new ArrayList<>(length - n);
- // Process indicesToRemove from the back; we know that it
- // contains indices in descending order.
- int j = n - 1;
- int idx = indicesToRemove.get(j);
- for (int i = 0; i < length; i++) {
- if (i == idx) {
- idx = (--j >= 0) ? indicesToRemove.get(j) : -1;
- } else {
- result.add(strings.get(i));
- }
- }
- return result;
- }
- }
-
private static boolean isSamePrefix(String a, String b) {
int as = a.lastIndexOf('/');
int bs = b.lastIndexOf('/');
@@ -1233,6 +1218,9 @@ public class DirCacheCheckout {
if (!FileMode.TREE.equals(e.getFileMode())) {
builder.add(e);
}
+ if (existing != null) {
+ existing.add(path);
+ }
if (force) {
if (f == null || f.isModified(e, true, walk.getObjectReader())) {
kept.add(path);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index 757473878b..c9dc6da4ba 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -26,6 +26,8 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
@@ -33,10 +35,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import org.eclipse.jgit.annotations.NonNull;
@@ -132,6 +136,8 @@ public abstract class Repository implements AutoCloseable {
private final String initialBranch;
+ private final AtomicReference<Boolean> caseInsensitiveWorktree = new AtomicReference<>();
+
/**
* Initialize a new repository instance.
*
@@ -1577,6 +1583,40 @@ public abstract class Repository implements AutoCloseable {
}
/**
+ * Tells whether the work tree is on a case-insensitive file system.
+ *
+ * @return {@code true} if the work tree is case-insensitive; {@code false}
+ * otherwise
+ * @throws NoWorkTreeException
+ * if the repository is bare
+ * @since 7.2
+ */
+ public boolean isWorkTreeCaseInsensitive() throws NoWorkTreeException {
+ Boolean flag = caseInsensitiveWorktree.get();
+ if (flag == null) {
+ File directory = getWorkTree();
+ // See if we can find ".git" also as ".GIT".
+ File dotGit = new File(directory, Constants.DOT_GIT);
+ if (Files.exists(dotGit.toPath(), LinkOption.NOFOLLOW_LINKS)) {
+ dotGit = new File(directory,
+ Constants.DOT_GIT.toUpperCase(Locale.ROOT));
+ flag = Boolean.valueOf(Files.exists(dotGit.toPath(),
+ LinkOption.NOFOLLOW_LINKS));
+ } else {
+ // Fall back to a mostly sane default. On Mac, HFS+ and APFS
+ // partitions are case-insensitive by default but can be
+ // configured to be case-sensitive.
+ SystemReader system = SystemReader.getInstance();
+ flag = Boolean.valueOf(system.isWindows() || system.isMacOS());
+ }
+ if (!caseInsensitiveWorktree.compareAndSet(null, flag)) {
+ flag = caseInsensitiveWorktree.get();
+ }
+ }
+ return flag.booleanValue();
+ }
+
+ /**
* Force a scan for changed refs. Fires an IndexChangedEvent(false) if
* changes are detected.
*