changeset 293:9774f47d904d

Issue 13: Status reports filenames with case other than in dirstate incorrectly
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 14 Sep 2011 04:11:37 +0200
parents a415fe296a50
children 32890bab7209
files src/org/tmatesoft/hg/repo/HgDirstate.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java test/org/tmatesoft/hg/test/TestDirstate.java
diffstat 3 files changed, 105 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- 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<Path, Record> added;
 	private Map<Path, Record> removed;
 	private Map<Path, Record> merged;
-	private Map<Path, Path> 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<Path, Path> canonical2dirstateName; 
 	private Pair<Nodeid, Nodeid> parents;
 	private String currentBranch;
 	
@@ -83,6 +87,11 @@
 		added = new LinkedHashMap<Path, Record>();
 		removed = new LinkedHashMap<Path, Record>();
 		merged = new LinkedHashMap<Path, Record>();
+		if (canonicalPathRewrite != null) {
+			canonical2dirstateName = new HashMap<Path,Path>();
+		} 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<Path, Record>[] 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<Path, Record> 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 <code>true</code> to indicate further records are still of interest, <code>false</code> 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());
+			}
+		}
 	}
 }
--- 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<Path> knownEntries = getDirstate().all();
 		repoWalker.reset();
+		TreeSet<Path> processed = new TreeSet<Path>(); // names of files we handled as they known to Dirstate (not FileIterator)
+		final HgDirstate ds = getDirstate();
+		TreeSet<Path> 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 {
--- 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
+	}
 }