aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2025-02-25 23:33:30 +0000
committerGerrit Code Review <support@gerrithub.io>2025-02-25 23:33:30 +0000
commitfa48cd2a77da4e14c657cd23e94f4eaba14c177f (patch)
treef8c757266e5322c2699c231b633677c51ae28322
parentadab727fdaf77d96302ef8637f14d139409221fb (diff)
parent4c4bef885c59662c22cbaa940ac7bec0dfee2d21 (diff)
downloadjgit-fa48cd2a77da4e14c657cd23e94f4eaba14c177f.tar.gz
jgit-fa48cd2a77da4e14c657cd23e94f4eaba14c177f.zip
Merge changes I83adebe5,Ibbc9ba97
* changes: DirCacheCheckout.preScanOneTree: consider mode bits Merge: improve handling of case-variants
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java92
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java26
-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.java81
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java40
5 files changed, 204 insertions, 47 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 503fef9916..1ec506798c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -21,6 +21,9 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.File;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Iterator;
import java.util.regex.Pattern;
@@ -46,6 +49,7 @@ import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.GitDateFormatter;
import org.eclipse.jgit.util.GitDateFormatter.Format;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
@@ -2101,6 +2105,94 @@ public class MergeCommandTest extends RepositoryTestCase {
}
}
+ @Test
+ public void testMergeCaseInsensitiveRename() throws Exception {
+ Assume.assumeTrue(
+ "Test makes only sense on a case-insensitive file system",
+ db.isWorkTreeCaseInsensitive());
+ try (Git git = new Git(db)) {
+ writeTrashFile("a", "aaa");
+ git.add().addFilepattern("a").call();
+ RevCommit initialCommit = git.commit().setMessage("initial").call();
+ // "Rename" "a" to "A"
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("A", "aaa");
+ git.add().addFilepattern("A").call();
+ RevCommit master = git.commit().setMessage("rename to A").call();
+
+ createBranch(initialCommit, "refs/heads/side");
+ checkoutBranch("refs/heads/side");
+
+ writeTrashFile("b", "bbb");
+ git.add().addFilepattern("b").call();
+ git.commit().setMessage("side").call();
+
+ // Merge master into side
+ MergeResult result = git.merge().include(master)
+ .setStrategy(MergeStrategy.RECURSIVE).call();
+ assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+ assertTrue(new File(db.getWorkTree(), "A").isFile());
+ // Double check
+ boolean found = true;
+ try (DirectoryStream<Path> dir = Files
+ .newDirectoryStream(db.getWorkTree().toPath())) {
+ for (Path p : dir) {
+ found = "A".equals(p.getFileName().toString());
+ if (found) {
+ break;
+ }
+ }
+ }
+ assertTrue(found);
+ }
+ }
+
+ @Test
+ public void testMergeCaseInsensitiveRenameConflict() throws Exception {
+ Assume.assumeTrue(
+ "Test makes only sense on a case-insensitive file system",
+ db.isWorkTreeCaseInsensitive());
+ try (Git git = new Git(db)) {
+ writeTrashFile("a", "aaa");
+ git.add().addFilepattern("a").call();
+ RevCommit initialCommit = git.commit().setMessage("initial").call();
+ // "Rename" "a" to "A" and change it
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("A", "yyy");
+ git.add().addFilepattern("A").call();
+ RevCommit master = git.commit().setMessage("rename to A").call();
+
+ createBranch(initialCommit, "refs/heads/side");
+ checkoutBranch("refs/heads/side");
+
+ writeTrashFile("a", "xxx");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("side").call();
+
+ // Merge master into side
+ MergeResult result = git.merge().include(master)
+ .setStrategy(MergeStrategy.RECURSIVE).call();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ File a = new File(db.getWorkTree(), "A");
+ assertTrue(a.isFile());
+ // Double check
+ boolean found = true;
+ try (DirectoryStream<Path> dir = Files
+ .newDirectoryStream(db.getWorkTree().toPath())) {
+ for (Path p : dir) {
+ found = "A".equals(p.getFileName().toString());
+ if (found) {
+ break;
+ }
+ }
+ }
+ assertTrue(found);
+ assertEquals(1, result.getConflicts().size());
+ assertTrue(result.getConflicts().containsKey("a"));
+ checkFile(a, "yyy");
+ }
+ }
+
private static void setExecutable(Git git, String path, boolean executable) {
FS.DETECTED.setExecute(
new File(git.getRepository().getWorkTree(), path), executable);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index 4265806078..99873e1be1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -42,6 +42,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
public class ResetCommandTest extends RepositoryTestCase {
@@ -555,6 +556,31 @@ public class ResetCommandTest extends RepositoryTestCase {
assertNull(db.resolve(Constants.HEAD));
}
+ @Test
+ public void testHardResetFileMode() throws Exception {
+ Assume.assumeTrue("Test must be able to set executable bit",
+ db.getFS().supportsExecute());
+ git = new Git(db);
+ File a = writeTrashFile("a.txt", "aaa");
+ File b = writeTrashFile("b.txt", "bbb");
+ db.getFS().setExecute(b, true);
+ assertFalse(db.getFS().canExecute(a));
+ assertTrue(db.getFS().canExecute(b));
+ git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+ RevCommit commit = git.commit().setMessage("files created").call();
+ db.getFS().setExecute(a, true);
+ db.getFS().setExecute(b, false);
+ assertTrue(db.getFS().canExecute(a));
+ assertFalse(db.getFS().canExecute(b));
+ git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+ git.commit().setMessage("change exe bits").call();
+ Ref ref = git.reset().setRef(commit.getName()).setMode(HARD).call();
+ assertSameAsHead(ref);
+ assertEquals(commit.getId(), ref.getObjectId());
+ assertFalse(db.getFS().canExecute(a));
+ assertTrue(db.getFS().canExecute(b));
+ }
+
private void assertReflog(ObjectId prevHead, ObjectId head)
throws IOException {
// Check the reflog for HEAD
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..18d77482e0 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;
}
/**
@@ -400,9 +410,11 @@ public class DirCacheCheckout {
// content to be checked out.
update(m);
}
- } else
+ } else {
update(m);
- } else if (f == null || !m.idEqual(i)) {
+ }
+ } else if (f == null || !m.idEqual(i)
+ || m.getEntryRawMode() != i.getEntryRawMode()) {
// The working tree file is missing or the merge content differs
// from index content
update(m);
@@ -410,11 +422,11 @@ public class DirCacheCheckout {
// The index contains a file (and not a folder)
if (f.isModified(i.getDirCacheEntry(), true,
this.walk.getObjectReader())
- || i.getDirCacheEntry().getStage() != 0)
+ || i.getDirCacheEntry().getStage() != 0) {
// The working tree file is dirty or the index contains a
// conflict
update(m);
- else {
+ } else {
// update the timestamp of the index with the one from the
// file if not set, as we are sure to be in sync here.
DirCacheEntry entry = i.getDirCacheEntry();
@@ -424,9 +436,10 @@ public class DirCacheCheckout {
}
keep(i.getEntryPathString(), entry, f);
}
- } else
+ } else {
// The index contains a folder
keep(i.getEntryPathString(), i.getDirCacheEntry(), f);
+ }
} else {
// There is no entry in the merge commit. Means: we want to delete
// what's currently in the index and working tree
@@ -521,6 +534,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 +551,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 +562,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 +580,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 +651,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 +1221,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.
*