changeset 415:ee8264d80747

Explicit constant for regular file flags, access to flags for a given file revision
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 22 Mar 2012 18:54:11 +0100
parents bb278ccf9866
children d30083c80d52
files cmdline/org/tmatesoft/hg/console/Main.java cmdline/org/tmatesoft/hg/console/Manifest.java src/org/tmatesoft/hg/core/HgChangeset.java src/org/tmatesoft/hg/core/HgFileInformer.java src/org/tmatesoft/hg/core/HgFileRevision.java src/org/tmatesoft/hg/core/HgLogCommand.java src/org/tmatesoft/hg/core/HgManifestCommand.java src/org/tmatesoft/hg/internal/EncodingHelper.java src/org/tmatesoft/hg/internal/IntMap.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/internal/ManifestRevision.java src/org/tmatesoft/hg/repo/HgDataFile.java src/org/tmatesoft/hg/repo/HgManifest.java src/org/tmatesoft/hg/repo/HgMergeState.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java test/org/tmatesoft/hg/test/TestIntMap.java
diffstat 16 files changed, 358 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Main.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Main.java	Thu Mar 22 18:54:11 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -93,6 +93,7 @@
 
 	public static void main(String[] args) throws Exception {
 		Main m = new Main(args);
+		m.checkFileFlags();
 //		m.buildFileLog();
 //		m.testConsoleLog();
 //		m.testTreeTraversal();
@@ -111,10 +112,21 @@
 //		m.testStatusInternals();
 //		m.catCompleteHistory();
 //		m.dumpCompleteManifestLow();
-		m.dumpCompleteManifestHigh();
+//		m.dumpCompleteManifestHigh();
 //		m.bunchOfTests();
 	}
 
+	private void checkFileFlags() throws Exception {
+		// ~/hg/test-flags repo
+		// TODO transform to a test once I keep test-flags in test-repos.jar
+		HgDataFile link = hgRepo.getFileNode("file-link");
+		HgDataFile exec = hgRepo.getFileNode("file-exec");
+		HgDataFile file = hgRepo.getFileNode("regular-file");
+		System.out.println("Link: " + link.getFlags(TIP));
+		System.out.println("Exec: " + exec.getFlags(TIP));
+		System.out.println("File: " + file.getFlags(TIP));
+	}
+	
 	private void buildFileLog() throws Exception {
 		HgLogCommand cmd = new HgLogCommand(hgRepo);
 		cmd.file("file1", false);
--- a/cmdline/org/tmatesoft/hg/console/Manifest.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Manifest.java	Thu Mar 22 18:54:11 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2011 TMate Software Ltd
+ * Copyright (c) 2010-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,11 @@
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import org.tmatesoft.hg.core.HgFileRevision;
+import org.tmatesoft.hg.core.HgInvalidControlFileException;
+import org.tmatesoft.hg.core.HgInvalidRevisionException;
 import org.tmatesoft.hg.core.HgManifestCommand;
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.repo.HgManifest;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.Path;
 
@@ -49,14 +52,30 @@
 			public void dir(Path p) {
 			}
 			public void file(HgFileRevision fileRevision) {
-				if (debug) {
-					System.out.print(fileRevision.getRevision());;
+				try {
+					if (debug) {
+						System.out.print(fileRevision.getRevision());;
+					}
+					if (debug || verbose) {
+						HgManifest.Flags flags = fileRevision.getFileFlags();
+						Object s;
+						if (flags == HgManifest.Flags.RegularFile) {
+							s = Integer.toOctalString(0644);
+						} else if (flags == HgManifest.Flags.Exec) {
+							s = Integer.toOctalString(0755);
+						} else if (flags == HgManifest.Flags.Link) {
+							s = "lnk";
+						} else {
+							s = String.valueOf(flags);
+						}
+						System.out.printf(" %s   ", s);
+					}
+					System.out.println(fileRevision.getPath());
+				} catch (HgInvalidControlFileException e) {
+					e.printStackTrace();
+				} catch (HgInvalidRevisionException e) {
+					e.printStackTrace();
 				}
-				if (debug || verbose) {
-					System.out.print(" 644"); // FIXME real flags!
-					System.out.print("   ");
-				}
-				System.out.println(fileRevision.getPath());
 			}
 			
 			public void end(Nodeid manifestRevision) {
--- a/src/org/tmatesoft/hg/core/HgChangeset.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgChangeset.java	Thu Mar 22 18:54:11 2012 +0100
@@ -272,7 +272,7 @@
 			if (nid == null) {
 				throw new HgException(String.format("For the file %s recorded as modified couldn't find revision after change", s));
 			}
-			modified.add(new HgFileRevision(repo, nid, s, null));
+			modified.add(new HgFileRevision(repo, nid, null, s, null));
 		}
 		final Map<Path, Path> copied = r.getCopied();
 		for (Path s : r.getAdded()) {
@@ -280,7 +280,7 @@
 			if (nid == null) {
 				throw new HgException(String.format("For the file %s recorded as added couldn't find revision after change", s));
 			}
-			added.add(new HgFileRevision(repo, nid, s, copied.get(s)));
+			added.add(new HgFileRevision(repo, nid, null, s, copied.get(s)));
 		}
 		for (Path s : r.getRemoved()) {
 			// with Path from getRemoved, may just copy
--- a/src/org/tmatesoft/hg/core/HgFileInformer.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileInformer.java	Thu Mar 22 18:54:11 2012 +0100
@@ -18,6 +18,7 @@
 
 import org.tmatesoft.hg.internal.ManifestRevision;
 import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgManifest;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.Status;
@@ -121,6 +122,7 @@
 			return checkResult;
 		}
 		Nodeid toExtract = null;
+		HgManifest.Flags extractRevFlags = null;
 		String phaseMsg = "Extract manifest revision failed";
 		try {
 			if (cachedManifest == null) {
@@ -130,6 +132,7 @@
 				// cachedManifest shall be meaningful - changelog.getRevisionIndex() above ensures we've got version that exists.
 			}
 			toExtract = cachedManifest.nodeid(file);
+			extractRevFlags = cachedManifest.flags(file);
 			phaseMsg = "Follow copy/rename failed";
 			if (toExtract == null && followRenames) {
 				while (toExtract == null && dataFile.isCopy()) {
@@ -137,6 +140,7 @@
 					file = dataFile.getCopySourceName();
 					dataFile = repo.getFileNode(file);
 					toExtract = cachedManifest.nodeid(file);
+					extractRevFlags = cachedManifest.flags(file);
 				}
 			}
 		} catch (HgException ex) {
@@ -144,7 +148,7 @@
 			return checkResult;
 		}
 		if (toExtract != null) {
-			fileRevision = new HgFileRevision(repo, toExtract, dataFile.getPath());
+			fileRevision = new HgFileRevision(repo, toExtract, extractRevFlags, dataFile.getPath());
 			checkResult = new Status(Status.Kind.OK, String.format("File %s, revision %s found at changeset %s", dataFile.getPath(), toExtract.shortNotation(), cset.shortNotation()));
 			return checkResult;
 		} 
--- a/src/org/tmatesoft/hg/core/HgFileRevision.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileRevision.java	Thu Mar 22 18:54:11 2012 +0100
@@ -17,6 +17,8 @@
 package org.tmatesoft.hg.core;
 
 import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgManifest;
+import org.tmatesoft.hg.repo.HgManifest.Flags;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.ByteChannel;
 import org.tmatesoft.hg.util.CancelledException;
@@ -36,20 +38,30 @@
 	private Path origin;
 	private Boolean isCopy = null; // null means not yet known
 	private Pair<Nodeid, Nodeid> parents;
+	private Flags flags; // null unless set/extracted
 
-	public HgFileRevision(HgRepository hgRepo, Nodeid rev, Path p) {
+	/**
+	 * FIXME has to be public?
+	 * 
+	 * @param hgRepo repository
+	 * @param rev file revision
+	 * @param manifestEntryFlags file flags at this revision (optional, may be null) 
+	 * @param p path of the file at the given revision
+	 */
+	public HgFileRevision(HgRepository hgRepo, Nodeid rev, HgManifest.Flags manifestEntryFlags, Path p) {
 		if (hgRepo == null || rev == null || p == null) {
 			// since it's package local, it is our code to blame for non validated arguments
 			throw new IllegalArgumentException();
 		}
 		repo = hgRepo;
 		revision = rev;
+		flags = manifestEntryFlags;
 		path = p;
 	}
 
 	// this cons shall be used when we know whether p was a copy. Perhaps, shall pass Map<Path,Path> instead to stress orig argument is not optional  
-	HgFileRevision(HgRepository hgRepo, Nodeid rev, Path p, Path orig) {
-		this(hgRepo, rev, p);
+	HgFileRevision(HgRepository hgRepo, Nodeid rev, HgManifest.Flags flags, Path p, Path orig) {
+		this(hgRepo, rev, flags, p);
 		isCopy = Boolean.valueOf(orig == null);
 		origin = orig; 
 	}
@@ -61,6 +73,20 @@
 	public Nodeid getRevision() {
 		return revision;
 	}
+	
+	/**
+	 * Executable or symbolic link, or <code>null</code> if regular file
+	 * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog  
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 */
+	public HgManifest.Flags getFileFlags() throws HgInvalidControlFileException, HgInvalidRevisionException {
+		if (flags == null) {
+			HgDataFile df = repo.getFileNode(path);
+			int revIdx = df.getRevisionIndex(revision);
+			flags = df.getFlags(revIdx);
+		}
+		return flags;
+	}
 
 	public boolean wasCopied() throws HgException {
 		if (isCopy == null) {
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Thu Mar 22 18:54:11 2012 +0100
@@ -235,8 +235,8 @@
 					// even if we do not follow history, report file rename
 					do {
 						if (handler instanceof FileHistoryHandler) {
-							HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName());
-							HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), fileNode.getPath(), src.getPath());
+							HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName());
+							HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath());
 							try {
 								((FileHistoryHandler) handler).copy(src, dst);
 							} catch (HgCallbackTargetException.Wrap ex) {
--- a/src/org/tmatesoft/hg/core/HgManifestCommand.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgManifestCommand.java	Thu Mar 22 18:54:11 2012 +0100
@@ -187,7 +187,7 @@
 			if (matcher != null && !matcher.accept(fname)) {
 				return true;
 			}
-			HgFileRevision fr = new HgFileRevision(repo, nid, fname);
+			HgFileRevision fr = new HgFileRevision(repo, nid, flags, fname);
 			if (needDirs) {
 				manifestContent.add(fr);
 			} else {
--- a/src/org/tmatesoft/hg/internal/EncodingHelper.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/EncodingHelper.java	Thu Mar 22 18:54:11 2012 +0100
@@ -17,13 +17,17 @@
 package org.tmatesoft.hg.internal;
 
 import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CharsetEncoder;
 
+import org.tmatesoft.hg.core.SessionContext;
+
 /**
  * Keep all encoding-related issues in the single place
+ * NOT thread-safe (encoder and decoder requires synchronized access)
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
@@ -34,10 +38,12 @@
 	 * http://mercurial.808500.n3.nabble.com/Unicode-support-request-td3430704.html
 	 */
 	
+	private final SessionContext sessionContext;
 	private final CharsetEncoder encoder;
 	private final CharsetDecoder decoder;
 	
-	EncodingHelper(Charset fsEncoding) {
+	EncodingHelper(Charset fsEncoding, SessionContext ctx) {
+		sessionContext = ctx;
 		decoder = fsEncoding.newDecoder();
 		encoder = fsEncoding.newEncoder();
 	}
@@ -46,16 +52,40 @@
 		try {
 			return decoder.decode(ByteBuffer.wrap(data, start, length)).toString();
 		} catch (CharacterCodingException ex) {
+			sessionContext.getLog().error(getClass(), ex, String.format("Use of charset %s failed, resort to system default", charset().name()));
 			// resort to system-default
 			return new String(data, start, length);
 		}
 	}
 
-	public String fromDirstate(byte[] data, int start, int length) throws CharacterCodingException {
+	/**
+	 * @return byte representation of the string directly comparable to bytes in manifest
+	 */
+	public byte[] toManifest(String s) {
+		if (s == null) {
+			// perhaps, can return byte[0] in this case?
+			throw new IllegalArgumentException();
+		}
+		try {
+			// synchonized(encoder) {
+			ByteBuffer bb = encoder.encode(CharBuffer.wrap(s));
+			// }
+			byte[] rv = new byte[bb.remaining()];
+			bb.get(rv, 0, rv.length);
+			return rv;
+		} catch (CharacterCodingException ex) {
+			sessionContext.getLog().error(getClass(), ex, String.format("Use of charset %s failed, resort to system default", charset().name()));
+			// resort to system-default
+			return s.getBytes();
+		}
+	}
+
+	public String fromDirstate(byte[] data, int start, int length) throws CharacterCodingException { // FIXME perhaps, log is enough, and charset() may be private?
 		return decoder.decode(ByteBuffer.wrap(data, start, length)).toString();
 	}
 
 	public Charset charset() {
 		return encoder.charset();
 	}
+
 }
--- a/src/org/tmatesoft/hg/internal/IntMap.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/IntMap.java	Thu Mar 22 18:54:11 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,13 @@
  */
 package org.tmatesoft.hg.internal;
 
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 
+import org.tmatesoft.hg.core.Nodeid;
+
 
 /**
  * Map implementation that uses plain int keys and performs with log n effectiveness.
@@ -140,8 +145,73 @@
 			size -= count;
 		} 
 	}
+
+	// document iterator is non-modifying (neither remove() nor setValue() works)
+	// perhaps, may also implement Iterable<Map.Entry> to use nice for()
+	public Iterator<Map.Entry<Integer, V>> entryIterator() {
+		class E implements Map.Entry<Integer, V> {
+			private Integer key;
+			private V value;
+
+			public Integer getKey() {
+				return key;
+			}
+
+			public V getValue() {
+				return value;
+			}
+
+			public V setValue(V value) {
+				throw new UnsupportedOperationException();
+			}
+			
+			void init(Integer k, V v) {
+				key = k;
+				value = v;
+			}
+		}
+		
+		return new Iterator<Map.Entry<Integer, V>>() {
+			private int i = 0;
+			private final E entry = new E();
+			private final int _size;
+			private final int[] _keys;
+			private final Object[] _values;
+			
+			{
+				_size = IntMap.this.size;
+				_keys = IntMap.this.keys;
+				_values = IntMap.this.values;
+			}
+
+			public boolean hasNext() {
+				return i < _size;
+			}
+
+			public Entry<Integer, V> next() {
+				if (i >= _size) {
+					throw new NoSuchElementException();
+				}
+				@SuppressWarnings("unchecked")
+				V val = (V) _values[i];
+				entry.init(_keys[i], val);
+				i++;
+				return entry;
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
 	
-	
+	public Map<Integer, ? super V> fill(Map<Integer, ? super V> map) {
+		for (Iterator<Map.Entry<Integer, V>> it = entryIterator(); it.hasNext(); ) {
+			Map.Entry<Integer, V> next = it.next();
+			map.put(next.getKey(), next.getValue());
+		}
+		return map;
+	}
 
 	// copy of Arrays.binarySearch, with upper search limit as argument
 	private static int binarySearch(int[] a, int high, int key) {
--- a/src/org/tmatesoft/hg/internal/Internals.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Thu Mar 22 18:54:11 2012 +0100
@@ -169,7 +169,7 @@
 	}
 	
 	public EncodingHelper buildFileNameEncodingHelper() {
-		return new EncodingHelper(getFileEncoding());
+		return new EncodingHelper(getFileEncoding(), sessionContext);
 	}
 	
 	private Charset getFileEncoding() {
--- a/src/org/tmatesoft/hg/internal/ManifestRevision.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/ManifestRevision.java	Thu Mar 22 18:54:11 2012 +0100
@@ -57,7 +57,8 @@
 	}
 
 	public HgManifest.Flags flags(Path fname) {
-		return flagsMap.get(fname);
+		HgManifest.Flags f = flagsMap.get(fname);
+		return f == null ? HgManifest.Flags.RegularFile : f;
 	}
 
 	/**
@@ -85,8 +86,9 @@
 			nid = idsPool.unify(nid);
 		}
 		idsMap.put(fname, nid);
-		if (flags != null) {
-			// TreeMap$Entry takes 32 bytes. No reason to keep null for such price
+		if (flags != HgManifest.Flags.RegularFile) {
+			// TreeMap$Entry takes 32 bytes. No reason to keep regular file attribute (in fact, no flags state) 
+			// for such price
 			// Alternatively, Map<Path, Pair<Nodeid, Flags>> might come as a solution
 			// however, with low rate of elements with flags this would consume more memory
 			// than two distinct maps (sizeof(Pair) == 16).  
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDataFile.java	Thu Mar 22 18:54:11 2012 +0100
@@ -528,13 +528,36 @@
 		throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?)
 	}
 	
+	/**
+	 * 
+	 * @return revision this file was copied from
+	 * @throws HgInvalidControlFileException if access to revlog or file metadata failed
+	 * @throws UnsupportedOperationException if this file doesn't represent a copy ({@link #isCopy()} was false)
+	 */
 	public Nodeid getCopySourceRevision() throws HgInvalidControlFileException {
 		if (isCopy()) {
 			return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid
 		}
 		throw new UnsupportedOperationException();
 	}
+/* FIXME	
+	public Nodeid getRevisionAtChangeset(int changesetRevision) {
+	}
 	
+	public HgManifest.Flags getFlagsAtChangeset(int changesetRevisionIndex) {
+	}
+*/
+	
+	/**
+	 * FIXME EXCEPTIONS 
+	 * @throws HgInvalidControlFileException
+	 * @throws HgInvalidRevisionException
+	 */
+	public HgManifest.Flags getFlags(int fileRevisionIndex) throws HgInvalidControlFileException, HgInvalidRevisionException {
+		int changesetRevIndex = getChangesetRevisionIndex(fileRevisionIndex);
+		return getRepo().getManifest().extractFlags(changesetRevIndex, getPath());
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder(getClass().getSimpleName());
--- a/src/org/tmatesoft/hg/repo/HgManifest.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgManifest.java	Thu Mar 22 18:54:11 2012 +0100
@@ -36,6 +36,7 @@
 import org.tmatesoft.hg.internal.DigestHelper;
 import org.tmatesoft.hg.internal.EncodingHelper;
 import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.IntMap;
 import org.tmatesoft.hg.internal.IterateControlMediator;
 import org.tmatesoft.hg.internal.Lifecycle;
 import org.tmatesoft.hg.internal.Pool2;
@@ -54,8 +55,22 @@
 	private RevisionMapper revisionMap;
 	private EncodingHelper encodingHelper;
 	
+	/**
+	 * File flags recorded in manifest
+	 */
 	public enum Flags {
-		Exec, Link; // FIXME REVISIT consider REGULAR instead of null
+		/**
+		 * Executable bit set
+		 */
+		Exec,
+		/**
+		 * Symbolic link
+		 */
+		Link,
+		/**
+		 * Regular file
+		 */
+		RegularFile; 
 		
 		static Flags parse(String flags) {
 			if ("x".equalsIgnoreCase(flags)) {
@@ -65,14 +80,14 @@
 				return Link;
 			}
 			if (flags == null) {
-				return null;
+				return RegularFile;
 			}
 			throw new IllegalStateException(flags);
 		}
 
 		static Flags parse(byte[] data, int start, int length) {
 			if (length == 0) {
-				return null;
+				return RegularFile;
 			}
 			if (length == 1) {
 				if (data[start] == 'x') {
@@ -93,6 +108,9 @@
 			if (this == Link) {
 				return "l";
 			}
+			if (this == RegularFile) {
+				return "";
+			}
 			throw new IllegalStateException(toString());
 		}
 	}
@@ -242,40 +260,31 @@
 	public Map<Integer, Nodeid> getFileRevisions(final Path file, int... changelogRevisionIndexes) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		// TODO need tests
 		int[] manifestRevisionIndexes = toManifestRevisionIndexes(changelogRevisionIndexes, null);
-		final HashMap<Integer,Nodeid> rv = new HashMap<Integer, Nodeid>(changelogRevisionIndexes.length);
-		content.iterate(manifestRevisionIndexes, true, new RevlogStream.Inspector() {
-			
-			public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgException {
-				ByteArrayOutputStream bos = new ByteArrayOutputStream();
-				try {
-					byte b;
-					while (!data.isEmpty() && (b = data.readByte()) != '\n') {
-						if (b != 0) {
-							bos.write(b);
-						} else {
-							String fname = new String(bos.toByteArray());
-							bos.reset();
-							if (file.toString().equals(fname)) {
-								byte[] nid = new byte[40];  
-								data.readBytes(nid, 0, 40);
-								rv.put(linkRevision, Nodeid.fromAscii(nid, 0, 40));
-								break;
-							} else {
-								data.skip(40);
-							}
-							// else skip to the end of line
-							while (!data.isEmpty() && (b = data.readByte()) != '\n')
-								;
-						}
-					}
-				} catch (IOException ex) {
-					throw new HgException(ex);
-				}
-			}
-		});
+		IntMap<Nodeid> resMap = new IntMap<Nodeid>(changelogRevisionIndexes.length);
+		content.iterate(manifestRevisionIndexes, true, new FileLookupInspector(encodingHelper, file, resMap, null));
+		// IntMap to HashMap, 
+		HashMap<Integer,Nodeid> rv = new HashMap<Integer, Nodeid>();
+		resMap.fill(rv);
 		return rv;
 	}
 
+	/**
+	 * {@link HgDataFile#getFlags(int)} is public API
+	 * 
+	 * @param changesetRevIndex changeset revision index
+	 * @param file path to look up
+	 * @return one of predefined enum values, or null if file was not known in the specified revision
+	 * FIXME EXCEPTIONS
+	 * @throws HgInvalidControlFileException
+	 * @throws HgInvalidRevisionException 
+	 */
+	/*package-local*/ Flags extractFlags(int changesetRevIndex, Path file) throws HgInvalidRevisionException, HgInvalidControlFileException {
+		int manifestRevIdx = fromChangelog(changesetRevIndex);
+		IntMap<Flags> resMap = new IntMap<Flags>(2);
+		content.iterate(manifestRevIdx, manifestRevIdx, true, new FileLookupInspector(encodingHelper, file, null, resMap));
+		return resMap.get(changesetRevIndex);
+	}
+
 
 	/**
 	 * @param changelogRevisionIndexes non-null
@@ -329,6 +338,12 @@
 	
 	@Experimental(reason="Explore Path alternative for filenames and enum for flags")
 	public interface Inspector2 extends Inspector {
+		/**
+		 * @param nid file revision
+		 * @param fname file name
+		 * @param flags one of {@link HgManifest.Flags} constants, not <code>null</code>
+		 * @return <code>true</code> to continue iteration, <code>false</code> to stop
+		 */
 		boolean next(Nodeid nid, Path fname, Flags flags);
 	}
 
@@ -467,11 +482,11 @@
 								// for cpython 0..10k, there are 4361062 flag checks, and there's only 1 unique flag
 								flags = Flags.parse(data, x + nodeidLen, i-x-nodeidLen);
 							} else {
-								flags = null;
+								flags = Flags.RegularFile;
 							}
 							boolean good2go;
 							if (inspector2 == null) {
-								String flagString = flags == null ? null : flags.nativeString();
+								String flagString = flags == Flags.RegularFile ? null : flags.nativeString();
 								good2go = inspector.next(nid, fname.toString(), flagString);
 							} else {
 								good2go = inspector2.next(nid, fname, flags);
@@ -606,4 +621,67 @@
 			}
 		}
 	}
+	
+	/**
+	 * Look up specified file in possibly multiple manifest revisions, collect file revision and flags.
+	 */
+	private static class FileLookupInspector implements RevlogStream.Inspector {
+		
+		private final byte[] filenameAsBytes;
+		private final IntMap<Nodeid> csetIndex2FileRev;
+		private final IntMap<Flags> csetIndex2Flags;
+
+		public FileLookupInspector(EncodingHelper eh, Path fileToLookUp, IntMap<Nodeid> csetIndex2FileRevMap, IntMap<Flags> csetIndex2FlagsMap) {
+			assert fileToLookUp != null;
+			// need at least one map for the inspector to make any sense
+			assert csetIndex2FileRevMap != null || csetIndex2FlagsMap != null;
+			csetIndex2FileRev = csetIndex2FileRevMap;
+			csetIndex2Flags = csetIndex2FlagsMap;
+			filenameAsBytes = eh.toManifest(fileToLookUp.toString());
+		}
+		
+		public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgException {
+			ByteArrayOutputStream bos = new ByteArrayOutputStream();
+			try {
+				byte b;
+				while (!data.isEmpty() && (b = data.readByte()) != '\n') {
+					if (b != 0) {
+						bos.write(b);
+					} else {
+						byte[] byteArray = bos.toByteArray();
+						bos.reset();
+						if (Arrays.equals(filenameAsBytes, byteArray)) {
+							if (csetIndex2FileRev != null) {
+								byte[] nid = new byte[40];  
+								data.readBytes(nid, 0, 40);
+								csetIndex2FileRev.put(linkRevision, Nodeid.fromAscii(nid, 0, 40));
+							} else {
+								data.skip(40);
+							}
+							if (csetIndex2Flags != null) {
+								while (!data.isEmpty() && (b = data.readByte()) != '\n') {
+									bos.write(b);
+								}
+								Flags flags;
+								if (bos.size() == 0) {
+									flags = Flags.RegularFile;
+								} else {
+									flags = Flags.parse(bos.toByteArray(), 0, bos.size());
+								}
+								csetIndex2Flags.put(linkRevision, flags);
+							}
+							break;
+						} else {
+							data.skip(40);
+						}
+						// else skip to the end of line
+						while (!data.isEmpty() && (b = data.readByte()) != '\n')
+							;
+					}
+				}
+			} catch (IOException ex) {
+				throw new HgException(ex); // FIXME EXCEPTIONS
+			}
+		}
+	}
 }
--- a/src/org/tmatesoft/hg/repo/HgMergeState.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgMergeState.java	Thu Mar 22 18:54:11 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -127,12 +127,12 @@
 				Path p1fname = pathPool.path(r[3]);
 				Nodeid nidP1 = m1.nodeid(p1fname);
 				Nodeid nidCA = nodeidPool.unify(Nodeid.fromAscii(r[5]));
-				HgFileRevision p1 = new HgFileRevision(repo, nidP1, p1fname);
+				HgFileRevision p1 = new HgFileRevision(repo, nidP1, m1.flags(p1fname), p1fname);
 				HgFileRevision ca;
 				if (nidCA == nidP1 && r[3].equals(r[4])) {
 					ca = p1;
 				} else {
-					ca = new HgFileRevision(repo, nidCA, pathPool.path(r[4]));
+					ca = new HgFileRevision(repo, nidCA, null, pathPool.path(r[4]));
 				}
 				HgFileRevision p2;
 				if (!wcp2.isNull() || !r[6].equals(r[4])) {
@@ -142,7 +142,7 @@
 						assert false : "There's not enough information (or I don't know where to look) in merge/state to find out what's the second parent";
 						nidP2 = NULL;
 					}
-					p2 = new HgFileRevision(repo, nidP2, p2fname);
+					p2 = new HgFileRevision(repo, nidP2, m2.flags(p2fname), p2fname);
 				} else {
 					// no second parent known. no idea what to do here, assume linear merge, use common ancestor as parent
 					p2 = ca;
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Thu Mar 22 18:54:11 2012 +0100
@@ -559,7 +559,7 @@
 		if ((dirstateFileMode & S_IXUSR) == S_IXUSR) {
 			return checkFlagsEqual(f, HgManifest.Flags.Exec);
 		}
-		return checkFlagsEqual(f, null); // no flags
+		return checkFlagsEqual(f, HgManifest.Flags.RegularFile); // no flags
 	}
 
 	/**
--- a/test/org/tmatesoft/hg/test/TestIntMap.java	Wed Mar 21 20:51:12 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestIntMap.java	Thu Mar 22 18:54:11 2012 +0100
@@ -18,6 +18,11 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
 import org.junit.Test;
 import org.tmatesoft.hg.internal.IntMap;
 
@@ -59,4 +64,25 @@
 		}
 		assertEquals(m.size(), actualCount);
 	}
+	
+	@Test
+	public void testIterators() {
+		IntMap<Boolean> m = new IntMap<Boolean>(20);
+		for (int i = 0; i <= 30; i+= 5) {
+			m.put(i, Boolean.TRUE);
+		}
+		HashMap<Integer, Boolean> hm = new HashMap<Integer, Boolean>();
+		for (Iterator<Map.Entry<Integer, Boolean>> it = m.entryIterator(); it.hasNext(); ) {
+			Entry<Integer, Boolean> next = it.next();
+			hm.put(next.getKey(), next.getValue());
+		}
+		assertEquals(m.size(), hm.size());
+		for (int i = 0; i <= 30; i++) {
+			assertEquals(m.get(i), hm.get(i));
+		}
+		//
+		HashMap<Integer, Boolean> hm2 = new HashMap<Integer, Boolean>();
+		m.fill(hm2);
+		assertEquals(hm, hm2);
+	}
 }