diff src/org/tmatesoft/hg/repo/HgDirstate.java @ 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 1483e57541ef
children 981f9f50bb6c
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());
+			}
+		}
 	}
 }