changeset 538:dd4f6311af52

Commit: first working version
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 05 Feb 2013 22:30:21 +0100
parents 5a455624be4f
children 9edfd5a223b8
files src/org/tmatesoft/hg/internal/ChangelogEntryBuilder.java src/org/tmatesoft/hg/internal/DataAccessProvider.java src/org/tmatesoft/hg/internal/ManifestEntryBuilder.java src/org/tmatesoft/hg/internal/ManifestRevision.java src/org/tmatesoft/hg/internal/PatchGenerator.java src/org/tmatesoft/hg/internal/RevlogStream.java src/org/tmatesoft/hg/internal/RevlogStreamWriter.java src/org/tmatesoft/hg/repo/CommitFacility.java src/org/tmatesoft/hg/repo/Revlog.java test/org/tmatesoft/hg/test/TestCommit.java test/org/tmatesoft/hg/tools/ChangelogEntryBuilder.java test/org/tmatesoft/hg/tools/ManifestEntryBuilder.java
diffstat 12 files changed, 610 insertions(+), 348 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ChangelogEntryBuilder.java	Tue Feb 05 22:30:21 2013 +0100
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 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
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.Map.Entry;
+
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.util.Path;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ChangelogEntryBuilder {
+
+	private String user;
+	private List<Path> modifiedFiles;
+	private final Map<String, String> extrasMap = new LinkedHashMap<String, String>();
+	private Integer tzOffset;
+	private Long csetTime;
+	
+	public ChangelogEntryBuilder user(String username) {
+		user = username;
+		return this;
+	}
+	
+	public String user() {
+		if (user == null) {
+			// for our testing purposes anything but null is ok. no reason to follow Hg username lookup conventions 
+			user = System.getProperty("user.name");
+		}
+		return user;
+	}
+	
+	public ChangelogEntryBuilder setModified(Collection<Path> files) {
+		modifiedFiles = new ArrayList<Path>(files == null ? Collections.<Path>emptyList() : files);
+		return this;
+	}
+
+	public ChangelogEntryBuilder addModified(Collection<Path> files) {
+		if (modifiedFiles == null) {
+			return setModified(files);
+		}
+		modifiedFiles.addAll(files);
+		return this;
+	}
+	
+	public ChangelogEntryBuilder branch(String branchName) {
+		if (branchName == null || "default".equals(branchName)) {
+			extrasMap.remove("branch");
+		} else {
+			extrasMap.put("branch", branchName);
+		}
+		return this;
+	}
+	
+	public ChangelogEntryBuilder extras(Map<String, String> extras) {
+		extrasMap.clear();
+		extrasMap.putAll(extras);
+		return this;
+	}
+	
+	public ChangelogEntryBuilder date(long seconds, int timezoneOffset) {
+		csetTime = seconds;
+		tzOffset = timezoneOffset;
+		return this;
+	}
+	
+	private long csetTime() {
+		if (csetTime != null) { 
+			return csetTime;
+		}
+		return System.currentTimeMillis() / 1000;
+	}
+	
+	private int csetTimezone(long time) {
+		if (tzOffset != null) {
+			return tzOffset;
+		}
+		return -(TimeZone.getDefault().getOffset(time) / 1000);
+	}
+
+	public byte[] build(Nodeid manifestRevision, String comment) {
+		String f = "%s\n%s\n%d %d %s\n%s\n\n%s";
+		StringBuilder extras = new StringBuilder();
+		for (Iterator<Entry<String, String>> it = extrasMap.entrySet().iterator(); it.hasNext();) {
+			final Entry<String, String> next = it.next();
+			extras.append(encodeExtrasPair(next.getKey()));
+			extras.append(':');
+			extras.append(encodeExtrasPair(next.getValue()));
+			if (it.hasNext()) {
+				extras.append('\00');
+			}
+		}
+		StringBuilder files = new StringBuilder();
+		if (modifiedFiles != null) {
+			for (Iterator<Path> it = modifiedFiles.iterator(); it.hasNext(); ) {
+				files.append(it.next());
+				if (it.hasNext()) {
+					files.append('\n');
+				}
+			}
+		}
+		final long date = csetTime();
+		final int tz = csetTimezone(date);
+		return String.format(f, manifestRevision.toString(), user(), date, tz, extras, files, comment).getBytes();
+	}
+
+	private final static CharSequence encodeExtrasPair(String s) {
+		if (s != null) {
+			return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\00", "\\0");
+		}
+		return s;
+	}
+}
--- a/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Tue Feb 05 22:30:21 2013 +0100
@@ -105,7 +105,7 @@
 			return new DataSerializer();
 		}
 		try {
-			return new StreamDataSerializer(context.getLog(), new FileOutputStream(f));
+			return new StreamDataSerializer(context.getLog(), new FileOutputStream(f, true));
 		} catch (final FileNotFoundException ex) {
 			context.getLog().dump(getClass(), Error, ex, null);
 			return new DataSerializer() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ManifestEntryBuilder.java	Tue Feb 05 22:30:21 2013 +0100
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 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
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.io.ByteArrayOutputStream;
+
+import org.tmatesoft.hg.core.Nodeid;
+
+/**
+ * Create binary manifest entry ready to write down into 00manifest.i
+ * <p>Usage:
+ * <pre>
+ *   ManifestEntryBuilder mb = new ManifestEntryBuilder();
+ *   mb.reset().add("file1", file1.getRevision(r1));
+ *   mb.add("file2", file2.getRevision(r2));
+ *   byte[] manifestRecordData = mb.build();
+ *   byte[] manifestRevlogHeader = buildRevlogHeader(..., sha1(parents, manifestRecordData), manifestRecordData.length);
+ *   manifestIndexOutputStream.write(manifestRevlogHeader);
+ *   manifestIndexOutputStream.write(manifestRecordData);
+ * </pre>
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ManifestEntryBuilder {
+	private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+	
+	public ManifestEntryBuilder reset() {
+		buffer.reset();
+		return this;
+	}
+	public ManifestEntryBuilder add(String fname, Nodeid revision) {
+		byte[] b = fname.getBytes();
+		buffer.write(b, 0, b.length);
+		buffer.write('\0');
+		b = revision.toString().getBytes();
+		buffer.write(b, 0, b.length);
+		buffer.write('\n');
+		return this;
+	}
+
+	public byte[] build() {
+		return buffer.toByteArray();
+	}
+
+}
--- a/src/org/tmatesoft/hg/internal/ManifestRevision.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/ManifestRevision.java	Tue Feb 05 22:30:21 2013 +0100
@@ -16,6 +16,8 @@
  */
 package org.tmatesoft.hg.internal;
 
+import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+
 import java.util.Collection;
 import java.util.TreeMap;
 
@@ -36,8 +38,8 @@
 	private final TreeMap<Path, HgManifest.Flags> flagsMap;
 	private final Convertor<Nodeid> idsPool;
 	private final Convertor<Path> namesPool;
-	private Nodeid manifestRev;
-	private int changelogRevIndex, manifestRevIndex;
+	private Nodeid manifestRev = Nodeid.NULL;
+	private int changelogRevIndex = NO_REVISION, manifestRevIndex = NO_REVISION;
 
 	// optional pools for effective management of nodeids and filenames (they are likely
 	// to be duplicated among different manifest revisions
--- a/src/org/tmatesoft/hg/internal/PatchGenerator.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/PatchGenerator.java	Tue Feb 05 22:30:21 2013 +0100
@@ -25,7 +25,7 @@
 import org.tmatesoft.hg.repo.HgRepository;
 
 /**
- * Mercurial cares about changes only up to the line level, e.g. a simple file version bump in manifest looks like (RevlogDump output):
+ * Mercurial cares about changes only up to the line level, e.g. a simple file version dump in manifest looks like (RevlogDump output):
  * 
  *   522:        233748      0        103      17438        433        522      521       -1     756073cf2321df44d3ed0585f2a5754bc8a1b2f6
  *   <PATCH>:
@@ -177,6 +177,12 @@
 	}
 	
 	public static void main(String[] args) throws Exception {
+		PatchGenerator pg1 = new PatchGenerator();
+		pg1.init("hello".getBytes(), "hello\nworld".getBytes());
+		pg1.findMatchingBlocks();
+		if (Boolean.TRUE.booleanValue()) {
+			return;
+		}
 		HgRepository repo = new HgLookup().detectFromWorkingDir();
 		HgDataFile df = repo.getFileNode("cmdline/org/tmatesoft/hg/console/Main.java");
 		ByteArrayChannel bac1, bac2;
@@ -223,6 +229,8 @@
 			if (lastStart < input.length) {
 				lines.add(new ByteChain(lastStart, input.length));
 			}
+			// empty chunk to keep offset of input end
+			lines.add(new ByteChain(input.length, input.length));
 		}
 		
 		public ByteChain chunk(int index) {
--- a/src/org/tmatesoft/hg/internal/RevlogStream.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogStream.java	Tue Feb 05 22:30:21 2013 +0100
@@ -223,6 +223,27 @@
 		}
 		return BAD_REVISION;
 	}
+	
+	public long newEntryOffset() {
+		if (revisionCount() == 0) {
+			return 0;
+		}
+		DataAccess daIndex = getIndexStream();
+		int lastRev = revisionCount() - 1;
+		try {
+			int recordOffset = getIndexOffsetInt(lastRev);
+			daIndex.seek(recordOffset);
+			long value = daIndex.readLong();
+			value = value >>> 16;
+			int compressedLen = daIndex.readInt();
+			return lastRev == 0 ? compressedLen : value + compressedLen;
+		} catch (IOException ex) {
+			throw new HgInvalidControlFileException("Linked revision lookup failed", ex, indexFile).setRevisionIndex(lastRev);
+		} finally {
+			daIndex.done();
+		}
+	}
+
 
 
 	// should be possible to use TIP, ALL, or -1, -2, -n notation of Hg
--- a/src/org/tmatesoft/hg/internal/RevlogStreamWriter.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogStreamWriter.java	Tue Feb 05 22:30:21 2013 +0100
@@ -36,7 +36,157 @@
  */
 public class RevlogStreamWriter {
 
+	private final DigestHelper dh = new DigestHelper();
+	private final RevlogCompressor revlogDataZip;
+	private int lastEntryBase, lastEntryIndex;
+	private byte[] lastEntryContent;
+	private Nodeid lastEntryRevision;
+	private IntMap<Nodeid> revisionCache = new IntMap<Nodeid>(32);
+	private RevlogStream revlogStream;
 	
+	public RevlogStreamWriter(SessionContext ctx, RevlogStream stream) {
+		assert ctx != null;
+		assert stream != null;
+				
+		revlogDataZip = new RevlogCompressor(ctx);
+		revlogStream = stream;
+	}
+	
+	/**
+	 * @return nodeid of added revision
+	 */
+	public Nodeid addRevision(byte[] content, int linkRevision, int p1, int p2) {
+		lastEntryRevision = Nodeid.NULL;
+		int revCount = revlogStream.revisionCount();
+		lastEntryIndex = revCount == 0 ? NO_REVISION : revCount - 1;
+		populateLastEntry();
+		//
+		PatchGenerator pg = new PatchGenerator();
+		Patch patch = pg.delta(lastEntryContent, content);
+		int patchSerializedLength = patch.serializedLength();
+		
+		final boolean writeComplete = preferCompleteOverPatch(patchSerializedLength, content.length);
+		DataSerializer.DataSource dataSource = writeComplete ? new DataSerializer.ByteArrayDataSource(content) : patch.new PatchDataSource();
+		revlogDataZip.reset(dataSource);
+		final int compressedLen;
+		final boolean useCompressedData = preferCompressedOverComplete(revlogDataZip.getCompressedLength(), dataSource.serializeLength());
+		if (useCompressedData) {
+			compressedLen= revlogDataZip.getCompressedLength();
+		} else {
+			// compression wasn't too effective,
+			compressedLen = dataSource.serializeLength() + 1 /*1 byte for 'u' - uncompressed prefix byte*/;
+		}
+		//
+		Nodeid p1Rev = revision(p1);
+		Nodeid p2Rev = revision(p2);
+		byte[] revisionNodeidBytes = dh.sha1(p1Rev, p2Rev, content).asBinary();
+		//
+
+		DataSerializer indexFile, dataFile, activeFile;
+		indexFile = dataFile = activeFile = null;
+		try {
+			//
+			activeFile = indexFile = revlogStream.getIndexStreamWriter();
+			final boolean isInlineData = revlogStream.isInlineData();
+			HeaderWriter revlogHeader = new HeaderWriter(isInlineData);
+			revlogHeader.length(content.length, compressedLen);
+			revlogHeader.nodeid(revisionNodeidBytes);
+			revlogHeader.linkRevision(linkRevision);
+			revlogHeader.parents(p1, p2);
+			revlogHeader.baseRevision(writeComplete ? lastEntryIndex+1 : lastEntryBase);
+			revlogHeader.offset(revlogStream.newEntryOffset());
+			//
+			revlogHeader.serialize(indexFile);
+			
+			if (isInlineData) {
+				dataFile = indexFile;
+			} else {
+				dataFile = revlogStream.getDataStreamWriter();
+			}
+			activeFile = dataFile;
+			if (useCompressedData) {
+				int actualCompressedLenWritten = revlogDataZip.writeCompressedData(dataFile);
+				if (actualCompressedLenWritten != compressedLen) {
+					throw new HgInvalidStateException(String.format("Expected %d bytes of compressed data, but actually wrote %d in %s", compressedLen, actualCompressedLenWritten, revlogStream.getDataFileName()));
+				}
+			} else {
+				dataFile.writeByte((byte) 'u');
+				dataSource.serialize(dataFile);
+			}
+			
+			lastEntryContent = content;
+			lastEntryBase = revlogHeader.baseRevision();
+			lastEntryIndex++;
+			lastEntryRevision = Nodeid.fromBinary(revisionNodeidBytes, 0);
+			revisionCache.put(lastEntryIndex, lastEntryRevision);
+		} catch (IOException ex) {
+			String m = String.format("Failed to write revision %d", lastEntryIndex+1, null);
+			HgInvalidControlFileException t = new HgInvalidControlFileException(m, ex, null);
+			if (activeFile == dataFile) {
+				throw revlogStream.initWithDataFile(t);
+			} else {
+				throw revlogStream.initWithIndexFile(t);
+			}
+		} finally {
+			indexFile.done();
+			if (dataFile != null && dataFile != indexFile) {
+				dataFile.done();
+			}
+		}
+		return lastEntryRevision;
+	}
+	
+	private Nodeid revision(int revisionIndex) {
+		if (revisionIndex == NO_REVISION) {
+			return Nodeid.NULL;
+		}
+		Nodeid n = revisionCache.get(revisionIndex);
+		if (n == null) {
+			n = Nodeid.fromBinary(revlogStream.nodeid(revisionIndex), 0);
+			revisionCache.put(revisionIndex, n);
+		}
+		return n;
+	}
+	
+	private void populateLastEntry() throws HgInvalidControlFileException {
+		if (lastEntryIndex != NO_REVISION && lastEntryContent == null) {
+			assert lastEntryIndex >= 0;
+			final IOException[] failure = new IOException[1];
+			revlogStream.iterate(lastEntryIndex, lastEntryIndex, true, new RevlogStream.Inspector() {
+				
+				public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) {
+					try {
+						lastEntryBase = baseRevision;
+						lastEntryRevision = Nodeid.fromBinary(nodeid, 0);
+						lastEntryContent = data.byteArray();
+					} catch (IOException ex) {
+						failure[0] = ex;
+					}
+				}
+			});
+			if (failure[0] != null) {
+				String m = String.format("Failed to get content of most recent revision %d", lastEntryIndex);
+				throw revlogStream.initWithDataFile(new HgInvalidControlFileException(m, failure[0], null));
+			}
+		}
+	}
+	
+	public static boolean preferCompleteOverPatch(int patchLength, int fullContentLength) {
+		return !decideWorthEffort(patchLength, fullContentLength);
+	}
+	
+	public static boolean preferCompressedOverComplete(int compressedLen, int fullContentLength) {
+		if (compressedLen <= 0) { // just in case, meaningless otherwise
+			return false;
+		}
+		return decideWorthEffort(compressedLen, fullContentLength);
+	}
+
+	// true if length obtained with effort is worth it 
+	private static boolean decideWorthEffort(int lengthWithExtraEffort, int lengthWithoutEffort) {
+		return lengthWithExtraEffort < (/* 3/4 of original */lengthWithoutEffort - (lengthWithoutEffort >>> 2));
+	}
+
 	/*XXX public because HgCloneCommand uses it*/
 	public static class HeaderWriter implements DataSerializer.DataSource {
 		private final ByteBuffer header;
@@ -125,147 +275,4 @@
 			return header.capacity();
 		}
 	}
-
-	private final DigestHelper dh = new DigestHelper();
-	private final RevlogCompressor revlogDataZip;
-	
-	
-	public RevlogStreamWriter(SessionContext ctx, RevlogStream stream) {
-		revlogDataZip = new RevlogCompressor(ctx);
-	}
-	
-	private int lastEntryBase, lastEntryIndex;
-	private byte[] lastEntryContent;
-	private Nodeid lastEntryRevision;
-	private IntMap<Nodeid> revisionCache = new IntMap<Nodeid>(32);
-	
-	public void addRevision(byte[] content, int linkRevision, int p1, int p2) {
-		int revCount = revlogStream.revisionCount();
-		lastEntryIndex = revCount == 0 ? NO_REVISION : revCount - 1;
-		populateLastEntry();
-		//
-		PatchGenerator pg = new PatchGenerator();
-		Patch patch = pg.delta(lastEntryContent, content);
-		int patchSerializedLength = patch.serializedLength();
-		
-		final boolean writeComplete = preferCompleteOverPatch(patchSerializedLength, content.length);
-		DataSerializer.DataSource dataSource = writeComplete ? new DataSerializer.ByteArrayDataSource(content) : patch.new PatchDataSource();
-		revlogDataZip.reset(dataSource);
-		final int compressedLen;
-		final boolean useUncompressedData = preferCompressedOverComplete(revlogDataZip.getCompressedLength(), dataSource.serializeLength());
-		if (useUncompressedData) {
-			// compression wasn't too effective,
-			compressedLen = dataSource.serializeLength() + 1 /*1 byte for 'u' - uncompressed prefix byte*/;
-		} else {
-			compressedLen= revlogDataZip.getCompressedLength();
-		}
-		//
-		Nodeid p1Rev = revision(p1);
-		Nodeid p2Rev = revision(p2);
-		byte[] revisionNodeidBytes = dh.sha1(p1Rev, p2Rev, content).asBinary();
-		//
-
-		DataSerializer indexFile, dataFile, activeFile;
-		indexFile = dataFile = activeFile = null;
-		try {
-			//
-			activeFile = indexFile = revlogStream.getIndexStreamWriter();
-			final boolean isInlineData = revlogStream.isInlineData();
-			HeaderWriter revlogHeader = new HeaderWriter(isInlineData);
-			revlogHeader.length(content.length, compressedLen);
-			revlogHeader.nodeid(revisionNodeidBytes);
-			revlogHeader.linkRevision(linkRevision);
-			revlogHeader.parents(p1, p2);
-			revlogHeader.baseRevision(writeComplete ? lastEntryIndex+1 : lastEntryBase);
-			//
-			revlogHeader.serialize(indexFile);
-			
-			if (isInlineData) {
-				dataFile = indexFile;
-			} else {
-				dataFile = revlogStream.getDataStreamWriter();
-			}
-			activeFile = dataFile;
-			if (useUncompressedData) {
-				dataFile.writeByte((byte) 'u');
-				dataSource.serialize(dataFile);
-			} else {
-				int actualCompressedLenWritten = revlogDataZip.writeCompressedData(dataFile);
-				if (actualCompressedLenWritten != compressedLen) {
-					throw new HgInvalidStateException(String.format("Expected %d bytes of compressed data, but actually wrote %d in %s", compressedLen, actualCompressedLenWritten, revlogStream.getDataFileName()));
-				}
-			}
-			
-			lastEntryContent = content;
-			lastEntryBase = revlogHeader.baseRevision();
-			lastEntryIndex++;
-			lastEntryRevision = Nodeid.fromBinary(revisionNodeidBytes, 0);
-			revisionCache.put(lastEntryIndex, lastEntryRevision);
-		} catch (IOException ex) {
-			String m = String.format("Failed to write revision %d", lastEntryIndex+1, null);
-			HgInvalidControlFileException t = new HgInvalidControlFileException(m, ex, null);
-			if (activeFile == dataFile) {
-				throw revlogStream.initWithDataFile(t);
-			} else {
-				throw revlogStream.initWithIndexFile(t);
-			}
-		} finally {
-			indexFile.done();
-			if (dataFile != null && dataFile != indexFile) {
-				dataFile.done();
-			}
-		}
-	}
-	
-	private RevlogStream revlogStream;
-	private Nodeid revision(int revisionIndex) {
-		if (revisionIndex == NO_REVISION) {
-			return Nodeid.NULL;
-		}
-		Nodeid n = revisionCache.get(revisionIndex);
-		if (n == null) {
-			n = Nodeid.fromBinary(revlogStream.nodeid(revisionIndex), 0);
-			revisionCache.put(revisionIndex, n);
-		}
-		return n;
-	}
-	
-	private void populateLastEntry() throws HgInvalidControlFileException {
-		if (lastEntryIndex != NO_REVISION && lastEntryContent == null) {
-			assert lastEntryIndex >= 0;
-			final IOException[] failure = new IOException[1];
-			revlogStream.iterate(lastEntryIndex, lastEntryIndex, true, new RevlogStream.Inspector() {
-				
-				public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) {
-					try {
-						lastEntryBase = baseRevision;
-						lastEntryRevision = Nodeid.fromBinary(nodeid, 0);
-						lastEntryContent = data.byteArray();
-					} catch (IOException ex) {
-						failure[0] = ex;
-					}
-				}
-			});
-			if (failure[0] != null) {
-				String m = String.format("Failed to get content of most recent revision %d", lastEntryIndex);
-				throw revlogStream.initWithDataFile(new HgInvalidControlFileException(m, failure[0], null));
-			}
-		}
-	}
-	
-	public static boolean preferCompleteOverPatch(int patchLength, int fullContentLength) {
-		return !decideWorthEffort(patchLength, fullContentLength);
-	}
-	
-	public static boolean preferCompressedOverComplete(int compressedLen, int fullContentLength) {
-		if (compressedLen <= 0) { // just in case, meaningless otherwise
-			return false;
-		}
-		return decideWorthEffort(compressedLen, fullContentLength);
-	}
-
-	// true if length obtained with effort is worth it 
-	private static boolean decideWorthEffort(int lengthWithExtraEffort, int lengthWithoutEffort) {
-		return lengthWithExtraEffort < (/* 3/4 of original */lengthWithoutEffort - (lengthWithoutEffort >>> 2));
-	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/repo/CommitFacility.java	Tue Feb 05 22:30:21 2013 +0100
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2013 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
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.repo;
+
+import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.tmatesoft.hg.core.HgRepositoryLockException;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.ByteArrayChannel;
+import org.tmatesoft.hg.internal.ChangelogEntryBuilder;
+import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.ManifestEntryBuilder;
+import org.tmatesoft.hg.internal.ManifestRevision;
+import org.tmatesoft.hg.internal.RevlogStreamWriter;
+import org.tmatesoft.hg.util.Pair;
+import org.tmatesoft.hg.util.Path;
+
+/**
+ * WORK IN PROGRESS
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@Experimental(reason="Work in progress")
+public class CommitFacility {
+	private final HgRepository repo;
+	private final int p1Commit, p2Commit;
+	private Map<Path, Pair<HgDataFile, ByteDataSupplier>> files = new LinkedHashMap<Path, Pair<HgDataFile, ByteDataSupplier>>();
+	
+
+	public CommitFacility(HgRepository hgRepo, int parentCommit) {
+		this(hgRepo, parentCommit, NO_REVISION);
+	}
+	
+	public CommitFacility(HgRepository hgRepo, int parent1Commit, int parent2Commit) {
+		repo = hgRepo;
+		p1Commit = parent1Commit;
+		p2Commit = parent2Commit;
+		if (parent1Commit != NO_REVISION && parent1Commit == parent2Commit) {
+			throw new IllegalArgumentException("Merging same revision is dubious");
+		}
+	}
+
+	public boolean isMerge() {
+		return p1Commit != NO_REVISION && p2Commit != NO_REVISION;
+	}
+
+	public void add(HgDataFile dataFile, ByteDataSupplier content) {
+		files.put(dataFile.getPath(), new Pair<HgDataFile, ByteDataSupplier>(dataFile, content));
+	}
+	
+	public Nodeid commit(String message) throws HgRepositoryLockException {
+		
+		final HgChangelog clog = repo.getChangelog();
+		final int clogRevisionIndex = clog.getRevisionCount();
+		ManifestRevision c1Manifest = new ManifestRevision(null, null);
+		ManifestRevision c2Manifest = new ManifestRevision(null, null);
+		if (p1Commit != NO_REVISION) {
+			repo.getManifest().walk(p1Commit, p1Commit, c1Manifest);
+		}
+		if (p2Commit != NO_REVISION) {
+			repo.getManifest().walk(p2Commit, p2Commit, c2Manifest);
+		}
+//		Pair<Integer, Integer> manifestParents = getManifestParents();
+		Pair<Integer, Integer> manifestParents = new Pair<Integer, Integer>(c1Manifest.revisionIndex(), c2Manifest.revisionIndex());
+		TreeMap<Path, Nodeid> newManifestRevision = new TreeMap<Path, Nodeid>();
+		HashMap<Path, Pair<Integer, Integer>> fileParents = new HashMap<Path, Pair<Integer,Integer>>();
+		for (Path f : c1Manifest.files()) {
+			HgDataFile df = repo.getFileNode(f);
+			Nodeid fileKnownRev = c1Manifest.nodeid(f);
+			int fileRevIndex = df.getRevisionIndex(fileKnownRev);
+			// FIXME merged files?!
+			fileParents.put(f, new Pair<Integer, Integer>(fileRevIndex, NO_REVISION));
+			newManifestRevision.put(f, fileKnownRev);
+		}
+		//
+		// Files
+		for (Pair<HgDataFile, ByteDataSupplier> e : files.values()) {
+			HgDataFile df = e.first();
+			Pair<Integer, Integer> fp = fileParents.get(df.getPath());
+			if (fp == null) {
+				// NEW FILE
+				fp = new Pair<Integer, Integer>(NO_REVISION, NO_REVISION);
+			}
+			ByteDataSupplier bds = e.second();
+			// FIXME quickfix, instead, pass ByteDataSupplier directly to RevlogStreamWriter
+			ByteBuffer bb = ByteBuffer.allocate(2048);
+			ByteArrayChannel bac = new ByteArrayChannel();
+			while (bds.read(bb) != -1) {
+				bb.flip();
+				bac.write(bb);
+				bb.clear();
+			}
+			RevlogStreamWriter fileWriter = new RevlogStreamWriter(repo.getSessionContext(), df.content);
+			Nodeid fileRev = fileWriter.addRevision(bac.toArray(), clogRevisionIndex, fp.first(), fp.second());
+			newManifestRevision.put(df.getPath(), fileRev);
+		}
+		//
+		// Manifest
+		final ManifestEntryBuilder manifestBuilder = new ManifestEntryBuilder();
+		for (Map.Entry<Path, Nodeid> me : newManifestRevision.entrySet()) {
+			manifestBuilder.add(me.getKey().toString(), me.getValue());
+		}
+		RevlogStreamWriter manifestWriter = new RevlogStreamWriter(repo.getSessionContext(), repo.getManifest().content);
+		Nodeid manifestRev = manifestWriter.addRevision(manifestBuilder.build(), clogRevisionIndex, manifestParents.first(), manifestParents.second());
+		//
+		// Changelog
+		final ChangelogEntryBuilder changelogBuilder = new ChangelogEntryBuilder();
+		changelogBuilder.setModified(files.keySet());
+		byte[] clogContent = changelogBuilder.build(manifestRev, message);
+		RevlogStreamWriter changelogWriter = new RevlogStreamWriter(repo.getSessionContext(), clog.content);
+		Nodeid changesetRev = changelogWriter.addRevision(clogContent, clogRevisionIndex, p1Commit, p2Commit);
+		return changesetRev;
+	}
+/*
+	private Pair<Integer, Integer> getManifestParents() {
+		return new Pair<Integer, Integer>(extractManifestRevisionIndex(p1Commit), extractManifestRevisionIndex(p2Commit));
+	}
+
+	private int extractManifestRevisionIndex(int clogRevIndex) {
+		if (clogRevIndex == NO_REVISION) {
+			return NO_REVISION;
+		}
+		RawChangeset commitObject = repo.getChangelog().range(clogRevIndex, clogRevIndex).get(0);
+		Nodeid manifestRev = commitObject.manifest();
+		if (manifestRev.isNull()) {
+			return NO_REVISION;
+		}
+		return repo.getManifest().getRevisionIndex(manifestRev);
+	}
+*/
+
+	// unlike DataAccess (which provides structured access), this one 
+	// deals with a sequence of bytes, when there's no need in structure of the data
+	public interface ByteDataSupplier { // TODO look if can resolve DataAccess in HgCloneCommand visibility issue
+		int read(ByteBuffer buf);
+	}
+	
+	public interface ByteDataConsumer {
+		void write(ByteBuffer buf);
+	}
+}
--- a/src/org/tmatesoft/hg/repo/Revlog.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/src/org/tmatesoft/hg/repo/Revlog.java	Tue Feb 05 22:30:21 2013 +0100
@@ -392,7 +392,7 @@
 		pw.init();
 		return pw;
 	}
-
+	
 	/*
 	 * class with cancel and few other exceptions support. TODO consider general superclass to share with e.g. HgManifestCommand.Mediator
 	 */
--- a/test/org/tmatesoft/hg/test/TestCommit.java	Tue Feb 05 20:06:22 2013 +0100
+++ b/test/org/tmatesoft/hg/test/TestCommit.java	Tue Feb 05 22:30:21 2013 +0100
@@ -16,8 +16,14 @@
  */
 package org.tmatesoft.hg.test;
 
-import org.junit.Assert;
+import java.io.File;
+import java.io.FileWriter;
+import java.nio.ByteBuffer;
+
 import org.junit.Test;
+import org.tmatesoft.hg.repo.CommitFacility;
+import org.tmatesoft.hg.repo.HgLookup;
+import org.tmatesoft.hg.repo.HgRepository;
 
 /**
  * 
@@ -27,7 +33,61 @@
 public class TestCommit {
 
 	@Test
-	public void testCommitToEmpty() throws Exception {
-		Assert.fail();
+	public void testCommitToNonEmpty() throws Exception {
+		File repoLoc = RepoUtils.initEmptyTempRepo("test-commit2non-empty");
+		FileWriter fw = new FileWriter(new File(repoLoc, "file1"));
+		fw.write("hello");
+		fw.close();
+		new ExecHelper(new OutputParser.Stub(true), repoLoc).run("hg", "commit", "--addremove", "-m", "FIRST");
+		//
+		HgRepository hgRepo = new HgLookup().detect(repoLoc);
+		CommitFacility cf = new CommitFacility(hgRepo, 0 /*NO_REVISION*/);
+		// FIXME test diff for processing changed newlines - if a whole line or just changed endings are in the patch!
+		cf.add(hgRepo.getFileNode("file1"), new ByteArraySupplier("hello\nworld".getBytes()));
+		cf.commit("commit 1");
+		// /tmp/test-commit2non-empty/.hg/ store/data/file1.i dumpData
+	}
+	
+	public static void main(String[] args) throws Exception {
+		new TestCommit().testCommitToNonEmpty();
+		String input = "abcdefghijklmnopqrstuvwxyz";
+		ByteArraySupplier bas = new ByteArraySupplier(input.getBytes());
+		ByteBuffer bb = ByteBuffer.allocate(7);
+		byte[] result = new byte[26];
+		int rpos = 0;
+		while (bas.read(bb) != -1) {
+			bb.flip();
+			bb.get(result, rpos, bb.limit());
+			rpos += bb.limit();
+			bb.clear();
+		}
+		if (input.length() != rpos) {
+			throw new AssertionError();
+		}
+		String output = new String(result);
+		if (!input.equals(output)) {
+			throw new AssertionError();
+		}
+		System.out.println(output);
+	}
+
+	static class ByteArraySupplier implements CommitFacility.ByteDataSupplier {
+
+		private final byte[] data;
+		private int pos = 0;
+
+		public ByteArraySupplier(byte[] source) {
+			data = source;
+		}
+
+		public int read(ByteBuffer buf) {
+			if (pos >= data.length) {
+				return -1;
+			}
+			int count = Math.min(buf.remaining(), data.length - pos);
+			buf.put(data, pos, count);
+			pos += count;
+			return count;
+		}
 	}
 }
--- a/test/org/tmatesoft/hg/tools/ChangelogEntryBuilder.java	Tue Feb 05 20:06:22 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/*
- * Copyright (c) 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
- * the Free Software Foundation; version 2 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * For information on how to redistribute this software under
- * the terms of a license other than GNU General Public License
- * contact TMate Software at support@hg4j.com
- */
-package org.tmatesoft.hg.tools;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.Map.Entry;
-
-import org.tmatesoft.hg.core.Nodeid;
-
-/**
- *
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class ChangelogEntryBuilder {
-
-	private String user;
-	private List<String> modifiedFiles;
-	private final Map<String, String> extrasMap = new LinkedHashMap<String, String>();
-	private Integer tzOffset;
-	private Long csetTime;
-	
-	public ChangelogEntryBuilder user(String username) {
-		user = username;
-		return this;
-	}
-	
-	public String user() {
-		if (user == null) {
-			// for our testing purposes anything but null is ok. no reason to follow Hg username lookup conventions 
-			user = System.getProperty("user.name");
-		}
-		return user;
-	}
-	
-	public ChangelogEntryBuilder setModified(List<String> files) {
-		modifiedFiles = new ArrayList<String>(files == null ? Collections.<String>emptyList() : files);
-		return this;
-	}
-
-	public ChangelogEntryBuilder addModified(List<String> files) {
-		if (modifiedFiles == null) {
-			return setModified(files);
-		}
-		modifiedFiles.addAll(files);
-		return this;
-	}
-	
-	public ChangelogEntryBuilder branch(String branchName) {
-		if (branchName == null || "default".equals(branchName)) {
-			extrasMap.remove("branch");
-		} else {
-			extrasMap.put("branch", branchName);
-		}
-		return this;
-	}
-	
-	public ChangelogEntryBuilder extras(Map<String, String> extras) {
-		extrasMap.clear();
-		extrasMap.putAll(extras);
-		return this;
-	}
-	
-	public ChangelogEntryBuilder date(long seconds, int timezoneOffset) {
-		csetTime = seconds;
-		tzOffset = timezoneOffset;
-		return this;
-	}
-	
-	private long csetTime() {
-		if (csetTime != null) { 
-			return csetTime;
-		}
-		return System.currentTimeMillis() / 1000;
-	}
-	
-	private int csetTimezone(long time) {
-		if (tzOffset != null) {
-			return tzOffset;
-		}
-		return -(TimeZone.getDefault().getOffset(time) / 1000);
-	}
-
-	public byte[] build(Nodeid manifestRevision, String comment) {
-		String f = "%s\n%s\n%d %d %s\n%s\n\n%s";
-		StringBuilder extras = new StringBuilder();
-		for (Iterator<Entry<String, String>> it = extrasMap.entrySet().iterator(); it.hasNext();) {
-			final Entry<String, String> next = it.next();
-			extras.append(encodeExtrasPair(next.getKey()));
-			extras.append(':');
-			extras.append(encodeExtrasPair(next.getValue()));
-			if (it.hasNext()) {
-				extras.append('\00');
-			}
-		}
-		StringBuilder files = new StringBuilder();
-		if (modifiedFiles != null) {
-			for (Iterator<String> it = modifiedFiles.iterator(); it.hasNext(); ) {
-				files.append(it.next());
-				if (it.hasNext()) {
-					files.append('\n');
-				}
-			}
-		}
-		final long date = csetTime();
-		final int tz = csetTimezone(date);
-		return String.format(f, manifestRevision.toString(), user(), date, tz, extras, files, comment).getBytes();
-	}
-
-	private final static CharSequence encodeExtrasPair(String s) {
-		if (s != null) {
-			return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\00", "\\0");
-		}
-		return s;
-	}
-}
--- a/test/org/tmatesoft/hg/tools/ManifestEntryBuilder.java	Tue Feb 05 20:06:22 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/*
- * Copyright (c) 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
- * the Free Software Foundation; version 2 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * For information on how to redistribute this software under
- * the terms of a license other than GNU General Public License
- * contact TMate Software at support@hg4j.com
- */
-package org.tmatesoft.hg.tools;
-
-import java.io.ByteArrayOutputStream;
-
-import org.tmatesoft.hg.core.Nodeid;
-
-/**
- * Create binary manifest entry ready to write down into 00manifest.i
- * <p>Usage:
- * <pre>
- *   ManifestEntryBuilder mb = new ManifestEntryBuilder();
- *   mb.reset().add("file1", file1.getRevision(r1));
- *   mb.add("file2", file2.getRevision(r2));
- *   byte[] manifestRecordData = mb.build();
- *   byte[] manifestRevlogHeader = buildRevlogHeader(..., sha1(parents, manifestRecordData), manifestRecordData.length);
- *   manifestIndexOutputStream.write(manifestRevlogHeader);
- *   manifestIndexOutputStream.write(manifestRecordData);
- * </pre>
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class ManifestEntryBuilder {
-	private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
-
-	
-	public ManifestEntryBuilder reset() {
-		buffer.reset();
-		return this;
-	}
-	public ManifestEntryBuilder add(String fname, Nodeid revision) {
-		byte[] b = fname.getBytes();
-		buffer.write(b, 0, b.length);
-		buffer.write('\0');
-		b = revision.toString().getBytes();
-		buffer.write(b, 0, b.length);
-		buffer.write('\n');
-		return this;
-	}
-
-	public byte[] build() {
-		return buffer.toByteArray();
-	}
-
-}