diff src/org/tmatesoft/hg/repo/HgDataFile.java @ 78:c25c5c348d1b

Skip metadata in the beginning of a file content. Parse metadata, recognize copies/renames
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 25 Jan 2011 02:13:53 +0100
parents c677e1593919
children 5f9635c01681
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java	Mon Jan 24 05:33:47 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDataFile.java	Tue Jan 25 02:13:53 2011 +0100
@@ -18,6 +18,10 @@
 
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.Path;
 import org.tmatesoft.hg.internal.RevlogStream;
@@ -36,6 +40,7 @@
 	// slashes, unix-style?
 	// repo location agnostic, just to give info to user, not to access real storage
 	private final Path path;
+	private Metadata metadata;
 	
 	/*package-local*/HgDataFile(HgRepository hgRepo, Path path, RevlogStream content) {
 		super(hgRepo, content);
@@ -59,6 +64,61 @@
 		return content(TIP);
 	}
 
+	// for data files need to check heading of the file content for possible metadata
+	// @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8-
+	@Override
+	public byte[] content(int revision) {
+		if (revision == TIP) {
+			revision = content.revisionCount() - 1; // FIXME maxRevision.
+		}
+		byte[] data = super.content(revision);
+		if (data.length < 4 || (data[0] != 1 && data[1] != 10)) {
+			return data;
+		}
+		int toSkip = 0;
+		if (metadata == null || !metadata.known(revision)) {
+			int lastEntryStart = 2;
+			int lastColon = -1;
+			ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>();
+			String key = null, value = null;
+			for (int i = 2; i < data.length; i++) {
+				if (data[i] == (int) ':') {
+					key = new String(data, lastEntryStart, i - lastEntryStart);
+					lastColon = i;
+				} else if (data[i] == '\n') {
+					if (key == null || lastColon == -1 || i <= lastColon) {
+						throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev
+					}
+					value = new String(data, lastColon + 1, i - lastColon - 1).trim();
+					_metadata.add(new MetadataEntry(key, value));
+					key = value = null;
+					lastColon = -1;
+					lastEntryStart = i+1;
+				} else if (data[i] == 1 && i + 1 < data.length && data[i+1] == 10) {
+					if (key != null && lastColon != -1 && i > lastColon) {
+						// just in case last entry didn't end with newline
+						value = new String(data, lastColon + 1, i - lastColon - 1);
+						_metadata.add(new MetadataEntry(key, value));
+					}
+					lastEntryStart = i+1;
+					break;
+				}
+			}
+			_metadata.trimToSize();
+			if (metadata == null) {
+				metadata = new Metadata();
+			}
+			metadata.add(revision, lastEntryStart, _metadata);
+			toSkip = lastEntryStart;
+		} else {
+			toSkip = metadata.dataOffset(revision);
+		}
+		// XXX copy of an array may be memory-hostile, a wrapper with baseOffsetShift(lastEntryStart) would be more convenient
+		byte[] rv = new byte[data.length - toSkip];
+		System.arraycopy(data, toSkip, rv, 0, rv.length);
+		return rv;
+	}
+
 	public void history(Changeset.Inspector inspector) {
 		history(0, content.revisionCount() - 1, inspector);
 	}
@@ -87,4 +147,71 @@
 		content.iterate(start, end, false, insp);
 		getRepo().getChangelog().range(inspector, commitRevisions);
 	}
+
+	public boolean isCopy() {
+		if (metadata == null) {
+			content(0); // FIXME expensive way to find out metadata, distinct RevlogStream.Iterator would be better.
+		}
+		if (metadata == null || !metadata.known(0)) {
+			return false;
+		}
+		return metadata.find(0, "copy") != null;
+	}
+
+	public Path getCopySourceName() {
+		if (isCopy()) {
+			return Path.create(metadata.find(0, "copy"));
+		}
+		throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?)
+	}
+	
+	public Nodeid getCopySourceRevision() {
+		if (isCopy()) {
+			return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid
+		}
+		throw new UnsupportedOperationException();
+	}
+
+	public static final class MetadataEntry {
+		private final String entry;
+		private final int valueStart;
+		/*package-local*/MetadataEntry(String key, String value) {
+			entry = key + value;
+			valueStart = key.length();
+		}
+		/*package-local*/boolean matchKey(String key) {
+			return key.length() == valueStart && entry.startsWith(key);
+		}
+		public String key() {
+			return entry.substring(0, valueStart);
+		}
+		public String value() {
+			return entry.substring(valueStart);
+		}
+	}
+
+	private static class Metadata {
+		// XXX sparse array needed
+		private final HashMap<Integer, Integer> offsets = new HashMap<Integer, Integer>(5);
+		private final HashMap<Integer, MetadataEntry[]> entries = new HashMap<Integer, MetadataEntry[]>(5);
+		boolean known(int revision) {
+			return offsets.containsKey(revision);
+		}
+		// since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before)
+		int dataOffset(int revision) {
+			return offsets.get(revision);
+		}
+		void add(int revision, int dataOffset, Collection<MetadataEntry> e) {
+			offsets.put(revision, dataOffset);
+			entries.put(revision, e.toArray(new MetadataEntry[e.size()]));
+		}
+		String find(int revision, String key) {
+			for (MetadataEntry me : entries.get(revision)) {
+				if (me.matchKey(key)) {
+					return me.value();
+				}
+			}
+			return null;
+		}
+	}
 }