# HG changeset patch # User Artem Tikhomirov # Date 1315966297 -7200 # Node ID 9774f47d904dd0b0f7da22c5fc0fe7df9b93ae2d # Parent a415fe296a50687967f768fba43a78ed8775cef8 Issue 13: Status reports filenames with case other than in dirstate incorrectly diff -r a415fe296a50 -r 9774f47d904d src/org/tmatesoft/hg/repo/HgDirstate.java --- a/src/org/tmatesoft/hg/repo/HgDirstate.java Wed Sep 14 02:16:19 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgDirstate.java Wed Sep 14 04:11:37 2011 +0200 @@ -23,6 +23,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeSet; @@ -57,7 +58,10 @@ private Map added; private Map removed; private Map merged; - private Map canonical2dirstate; // map of canonicalized file names to their originals from dirstate file + /* map of canonicalized file names to their originals from dirstate file. + * Note, only those canonical names that differ from their dirstate counterpart are recorded here + */ + private Map canonical2dirstateName; private Pair parents; private String currentBranch; @@ -83,6 +87,11 @@ added = new LinkedHashMap(); removed = new LinkedHashMap(); merged = new LinkedHashMap(); + if (canonicalPathRewrite != null) { + canonical2dirstateName = new HashMap(); + } else { + canonical2dirstateName = Collections.emptyMap(); + } try { parents = internalReadParents(da); // hg init; hg up produces an empty repository where dirstate has parents (40 bytes) only @@ -106,6 +115,22 @@ fn1 = new String(name); } Record r = new Record(fmode, size, time, pathPool.path(fn1), fn2 == null ? null : pathPool.path(fn2)); + if (canonicalPathRewrite != null) { + Path canonicalPath = pathPool.path(canonicalPathRewrite.rewrite(fn1).toString()); + if (canonicalPath != r.name()) { // == as they come from the same pool + assert !canonical2dirstateName.containsKey(canonicalPath); // otherwise there's already a file with same canonical name + // which can't happen for case-insensitive file system (or there's erroneous PathRewrite, perhaps doing smth else) + canonical2dirstateName.put(canonicalPath, r.name()); + } + if (fn2 != null) { + // not sure I need copy origin in the map, I don't seem to use it anywhere, + // but I guess I'll have to use it some day. + canonicalPath = pathPool.path(canonicalPathRewrite.rewrite(fn2).toString()); + if (canonicalPath != r.copySource()) { + canonical2dirstateName.put(canonicalPath, r.copySource()); + } + } + } if (state == 'n') { normal.put(r.name1, r); } else if (state == 'a') { @@ -217,20 +242,61 @@ } /*package-local*/ Record checkNormal(Path fname) { - return normal.get(fname); + return internalCheck(normal, fname); } /*package-local*/ Record checkAdded(Path fname) { - return added.get(fname); + return internalCheck(added, fname); } /*package-local*/ Record checkRemoved(Path fname) { - return removed.get(fname); + return internalCheck(removed, fname); } /*package-local*/ Record checkMerged(Path fname) { - return merged.get(fname); + return internalCheck(merged, fname); } + + // return non-null if fname is known, either as is, or its canonical form. in latter case, this canonical form is return value + /*package-local*/ Path known(Path fname) { + Path fnameCanonical = null; + if (canonicalPathRewrite != null) { + fnameCanonical = pathPool.path(canonicalPathRewrite.rewrite(fname).toString()); + if (fnameCanonical != fname && canonical2dirstateName.containsKey(fnameCanonical)) { + // we know right away there's name in dirstate with alternative canonical form + return canonical2dirstateName.get(fnameCanonical); + } + } + @SuppressWarnings("unchecked") + Map[] all = new Map[] { normal, added, removed, merged }; + for (int i = 0; i < all.length; i++) { + if (all[i].containsKey(fname)) { + return fname; + } + if (fnameCanonical != null && all[i].containsKey(fnameCanonical)) { + return fnameCanonical; + } + } + return null; + } + private Record internalCheck(Map map, Path fname) { + Record rv = map.get(fname); + if (rv != null || canonicalPathRewrite == null) { + return rv; + } + Path fnameCanonical = pathPool.path(canonicalPathRewrite.rewrite(fname).toString()); + if (fnameCanonical != fname) { + // case when fname = /a/B/c, and dirstate is /a/b/C + if (canonical2dirstateName.containsKey(fnameCanonical)) { + return map.get(canonical2dirstateName.get(fnameCanonical)); + } + // try canonical directly, fname = /a/B/C, dirstate has /a/b/c + if ((rv = map.get(fnameCanonical)) != null) { + return rv; + } + } + return null; + } /*package-local*/ void dump() { @@ -267,10 +333,16 @@ } public interface Inspector { + /** + * Invoked for each entry in the directory state file + * @param kind file record kind + * @param entry file record. Note, do not cache instance as it may be reused between the calls + * @return true to indicate further records are still of interest, false to stop iteration + */ boolean next(EntryKind kind, Record entry); } - public static final class Record { + public static final class Record implements Cloneable { private final int mode, size, time; // Dirstate keeps local file size (i.e. that with any filters already applied). // Thus, can't compare directly to HgDataFile.length() @@ -303,5 +375,14 @@ public int size() { return size; } + + @Override + public Record clone() { + try { + return (Record) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new InternalError(ex.toString()); + } + } } } diff -r a415fe296a50 -r 9774f47d904d src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java --- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Wed Sep 14 02:16:19 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Wed Sep 14 04:11:37 2011 +0200 @@ -161,16 +161,21 @@ ((HgStatusCollector.Record) inspector).init(rev1, rev2, sc); } final HgIgnore hgIgnore = repo.getIgnore(); - TreeSet knownEntries = getDirstate().all(); repoWalker.reset(); + TreeSet processed = new TreeSet(); // names of files we handled as they known to Dirstate (not FileIterator) + final HgDirstate ds = getDirstate(); + TreeSet knownEntries = ds.all(); // here just to get dirstate initialized while (repoWalker.hasNext()) { repoWalker.next(); final Path fname = getPathPool().path(repoWalker.name()); FileInfo f = repoWalker.file(); + Path knownInDirstate; if (!f.exists()) { // file coming from iterator doesn't exist. - if (knownEntries.remove(fname)) { - if (getDirstate().checkRemoved(fname) == null) { + if ((knownInDirstate = ds.known(fname)) != null) { + // found in dirstate + processed.add(knownInDirstate); + if (ds.checkRemoved(fname) == null) { inspector.missing(fname); } else { inspector.removed(fname); @@ -197,9 +202,10 @@ } continue; } - if (knownEntries.remove(fname)) { + if ((knownInDirstate = ds.known(fname)) != null) { // tracked file. // modified, added, removed, clean + processed.add(knownInDirstate); if (collect != null) { // need to check against base revision, not FS file checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector); } else { @@ -223,13 +229,14 @@ } } } + knownEntries.removeAll(processed); for (Path m : knownEntries) { if (!repoWalker.inScope(m)) { // do not report as missing/removed those FileIterator doesn't care about. continue; } // missing known file from a working dir - if (getDirstate().checkRemoved(m) == null) { + if (ds.checkRemoved(m) == null) { // not removed from the repository = 'deleted' inspector.missing(m); } else { diff -r a415fe296a50 -r 9774f47d904d test/org/tmatesoft/hg/test/TestDirstate.java --- a/test/org/tmatesoft/hg/test/TestDirstate.java Wed Sep 14 02:16:19 2011 +0200 +++ b/test/org/tmatesoft/hg/test/TestDirstate.java Wed Sep 14 04:11:37 2011 +0200 @@ -57,4 +57,10 @@ repo = Configuration.get().own(); Assert.assertEquals("default", repo.getWorkingCopyBranchName()); } + + public void testMixedNameCaseHandling() { + // 1. dirstate: /a/b/c, FileIterator: /a/B/C + // 2. dirstate: /a/B/C, FileIterator: /a/b/c + // 2. dirstate: /a/B/C, FileIterator: /A/b/C + } }