changeset 660:4fd317a2fecf

Pull: phase1 get remote changes and add local revisions
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 09 Jul 2013 21:46:45 +0200
parents d10399f80f4e
children 5d8798772cca
files src/org/tmatesoft/hg/core/HgPullCommand.java src/org/tmatesoft/hg/core/HgRepoFacade.java src/org/tmatesoft/hg/internal/AddRevInspector.java src/org/tmatesoft/hg/internal/CommitFacility.java src/org/tmatesoft/hg/internal/RevlogStreamWriter.java test/org/tmatesoft/hg/test/RepoUtils.java test/org/tmatesoft/hg/test/TestCommit.java test/org/tmatesoft/hg/test/TestPull.java
diffstat 8 files changed, 488 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgPullCommand.java	Tue Jul 09 21:46:45 2013 +0200
@@ -0,0 +1,111 @@
+/*
+ * 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.core;
+
+import java.util.List;
+
+import org.tmatesoft.hg.internal.AddRevInspector;
+import org.tmatesoft.hg.internal.COWTransaction;
+import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.internal.PhasesHelper;
+import org.tmatesoft.hg.internal.RepositoryComparator;
+import org.tmatesoft.hg.internal.RevisionSet;
+import org.tmatesoft.hg.internal.Transaction;
+import org.tmatesoft.hg.repo.HgBundle;
+import org.tmatesoft.hg.repo.HgChangelog;
+import org.tmatesoft.hg.repo.HgInternals;
+import org.tmatesoft.hg.repo.HgParentChildMap;
+import org.tmatesoft.hg.repo.HgRemoteRepository;
+import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.ProgressSupport;
+
+/**
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class HgPullCommand extends HgAbstractCommand<HgPullCommand> {
+
+	private final HgRepository repo;
+	private HgRemoteRepository remote;
+
+	public HgPullCommand(HgRepository hgRepo) {
+		repo = hgRepo;
+	}
+
+	public HgPullCommand source(HgRemoteRepository hgRemote) {
+		remote = hgRemote;
+		return this;
+	}
+
+	public void execute() throws HgRemoteConnectionException, HgIOException, HgLibraryFailureException, CancelledException {
+		final ProgressSupport progress = getProgressSupport(null);
+		try {
+			progress.start(100);
+			// TODO refactor same code in HgIncomingCommand #getComparator and #getParentHelper
+			final HgChangelog clog = repo.getChangelog();
+			final HgParentChildMap<HgChangelog> parentHelper = new HgParentChildMap<HgChangelog>(clog);
+			parentHelper.init();
+			final RepositoryComparator comparator = new RepositoryComparator(parentHelper, remote);
+			// get incoming revisions
+			comparator.compare(new ProgressSupport.Sub(progress, 50), getCancelSupport(null, true));
+			final List<Nodeid> common = comparator.getCommon();
+			// get bundle with changes from remote
+			HgBundle incoming = remote.getChanges(common);
+			//
+			// add revisions to changelog, manifest, files
+			final Internals implRepo = HgInternals.getImplementationRepo(repo);
+			final AddRevInspector insp;
+			Transaction.Factory trFactory = new COWTransaction.Factory();
+			Transaction tr = trFactory.create(repo);
+			try {
+				incoming.inspectAll(insp = new AddRevInspector(implRepo, tr));
+				tr.commit();
+			} catch (HgRuntimeException ex) {
+				tr.rollback();
+				throw ex;
+			} catch (RuntimeException ex) {
+				tr.rollback();
+				throw ex;
+			}
+			progress.worked(45);
+			RevisionSet added = insp.addedChangesets();
+			
+			// get remote phases, update local phases to match that of remote
+			final PhasesHelper phaseHelper = new PhasesHelper(implRepo, parentHelper);
+			if (phaseHelper.isCapableOfPhases()) {
+				RevisionSet rsCommon = new RevisionSet(common);
+				HgRemoteRepository.Phases remotePhases = remote.getPhases();
+				if (remotePhases.isPublishingServer()) {
+					final RevisionSet knownPublic = rsCommon.union(added);
+					RevisionSet newDraft = phaseHelper.allDraft().subtract(knownPublic);
+					RevisionSet newSecret = phaseHelper.allSecret().subtract(knownPublic);
+					phaseHelper.updateRoots(newDraft.asList(), newSecret.asList());
+				} else {
+					// FIXME refactor reuse from HgPushCommand
+				}
+			}
+			progress.worked(5);
+		} catch (HgRuntimeException ex) {
+			throw new HgLibraryFailureException(ex);
+		} finally {
+			progress.done();
+		}
+	}
+}
--- a/src/org/tmatesoft/hg/core/HgRepoFacade.java	Thu Jul 04 21:09:33 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgRepoFacade.java	Tue Jul 09 21:46:45 2013 +0200
@@ -169,4 +169,8 @@
 	public HgPushCommand createPushCommand() {
 		return new HgPushCommand(repo);
 	}
+	
+	public HgPullCommand createPullCommand() {
+		return new HgPullCommand(repo);
+	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/AddRevInspector.java	Tue Jul 09 21:46:45 2013 +0200
@@ -0,0 +1,128 @@
+/*
+ * 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.internal;
+
+import java.util.HashMap;
+import java.util.Set;
+
+import org.tmatesoft.hg.core.HgIOException;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.repo.HgBundle;
+import org.tmatesoft.hg.repo.HgBundle.GroupElement;
+import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgInvalidControlFileException;
+import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.util.Pair;
+
+/**
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public final class AddRevInspector implements HgBundle.Inspector {
+	private final Internals repo;
+	private final Transaction tr;
+	private Set<Nodeid> added;
+	private RevlogStreamWriter revlog;
+	private RevMap clogRevs;
+	private RevMap revlogRevs;
+
+	public AddRevInspector(Internals implRepo, Transaction transaction) {
+		repo = implRepo;
+		tr = transaction;
+	}
+
+	public void changelogStart() throws HgRuntimeException {
+		// TODO Auto-generated method stub
+		RevlogStream rs = repo.getImplAccess().getChangelogStream();
+		revlog = new RevlogStreamWriter(repo, rs, tr);
+		revlogRevs = clogRevs = new RevMap(rs);
+	}
+
+	public void changelogEnd() throws HgRuntimeException {
+		revlog = null;
+		revlogRevs = null;
+		added = clogRevs.added();
+	}
+
+	public void manifestStart() throws HgRuntimeException {
+		RevlogStream rs = repo.getImplAccess().getManifestStream();
+		revlog = new RevlogStreamWriter(repo, rs, tr);
+		revlogRevs = new RevMap(rs);
+	}
+
+	public void manifestEnd() throws HgRuntimeException {
+		revlog = null;
+		revlogRevs = null;
+	}
+
+	public void fileStart(String name) throws HgRuntimeException {
+		HgDataFile df = repo.getRepo().getFileNode(name);
+		RevlogStream rs = repo.getImplAccess().getStream(df);
+		revlog = new RevlogStreamWriter(repo, rs, tr);
+		revlogRevs = new RevMap(rs);
+		// FIXME collect new files and update fncache
+	}
+
+	public void fileEnd(String name) throws HgRuntimeException {
+		revlog = null;
+		revlogRevs = null;
+	}
+
+	public boolean element(GroupElement ge) throws HgRuntimeException {
+		assert clogRevs != null;
+		assert revlogRevs != null;
+		try {
+			Pair<Integer, Nodeid> newRev = revlog.addPatchRevision(ge, clogRevs, revlogRevs);
+			revlogRevs.update(newRev.first(), newRev.second());
+			return true;
+		} catch (HgIOException ex) {
+			throw new HgInvalidControlFileException(ex, true);
+		}
+	}
+
+	public RevisionSet addedChangesets() {
+		return new RevisionSet(added);
+	}
+
+	private static class RevMap implements RevlogStreamWriter.RevisionToIndexMap {
+		
+		private final RevlogStream revlog;
+		private HashMap<Nodeid, Integer> added = new HashMap<Nodeid, Integer>();
+
+		public RevMap(RevlogStream revlogStream) {
+			revlog = revlogStream;
+		}
+
+		public int revisionIndex(Nodeid revision) {
+			Integer a = added.get(revision);
+			if (a != null) {
+				return a;
+			}
+			int f = revlog.findRevisionIndex(revision);
+			return f == HgRepository.BAD_REVISION ? HgRepository.NO_REVISION : f;
+		}
+		
+		public void update(Integer revIndex, Nodeid rev) {
+			added.put(rev, revIndex);
+		}
+		
+		Set<Nodeid> added() {
+			return added.keySet();
+		}
+	}
+}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/CommitFacility.java	Thu Jul 04 21:09:33 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/CommitFacility.java	Tue Jul 09 21:46:45 2013 +0200
@@ -157,7 +157,7 @@
 				newlyAddedFiles.put(df.getPath(), contentStream);
 			}
 			RevlogStreamWriter fileWriter = new RevlogStreamWriter(repo, contentStream, transaction);
-			Nodeid fileRev = fileWriter.addRevision(bds, clogRevisionIndex, fp.first(), fp.second());
+			Nodeid fileRev = fileWriter.addRevision(bds, clogRevisionIndex, fp.first(), fp.second()).second();
 			newManifestRevision.put(df.getPath(), fileRev);
 			touchInDirstate.add(df.getPath());
 		}
@@ -168,7 +168,7 @@
 			manifestBuilder.add(me.getKey().toString(), me.getValue());
 		}
 		RevlogStreamWriter manifestWriter = new RevlogStreamWriter(repo, repo.getImplAccess().getManifestStream(), transaction);
-		Nodeid manifestRev = manifestWriter.addRevision(manifestBuilder, clogRevisionIndex, manifestParents.first(), manifestParents.second());
+		Nodeid manifestRev = manifestWriter.addRevision(manifestBuilder, clogRevisionIndex, manifestParents.first(), manifestParents.second()).second();
 		//
 		// Changelog
 		final ChangelogEntryBuilder changelogBuilder = new ChangelogEntryBuilder();
@@ -177,7 +177,7 @@
 		changelogBuilder.user(String.valueOf(user));
 		changelogBuilder.manifest(manifestRev).comment(message);
 		RevlogStreamWriter changelogWriter = new RevlogStreamWriter(repo, repo.getImplAccess().getChangelogStream(), transaction);
-		Nodeid changesetRev = changelogWriter.addRevision(changelogBuilder, clogRevisionIndex, p1Commit, p2Commit);
+		Nodeid changesetRev = changelogWriter.addRevision(changelogBuilder, clogRevisionIndex, p1Commit, p2Commit).second();
 		// TODO move fncache update to an external facility, along with dirstate and bookmark update
 		if (!newlyAddedFiles.isEmpty() && repo.fncacheInUse()) {
 			FNCacheFile fncache = new FNCacheFile(repo);
--- a/src/org/tmatesoft/hg/internal/RevlogStreamWriter.java	Thu Jul 04 21:09:33 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/RevlogStreamWriter.java	Tue Jul 09 21:46:45 2013 +0200
@@ -25,13 +25,16 @@
 import org.tmatesoft.hg.core.HgIOException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.internal.DataSerializer.ByteArrayDataSource;
 import org.tmatesoft.hg.internal.DataSerializer.ByteArraySerializer;
-import org.tmatesoft.hg.internal.DataSerializer.ByteArrayDataSource;
 import org.tmatesoft.hg.internal.DataSerializer.DataSource;
+import org.tmatesoft.hg.repo.HgBundle.GroupElement;
 import org.tmatesoft.hg.repo.HgInvalidControlFileException;
 import org.tmatesoft.hg.repo.HgInvalidRevisionException;
 import org.tmatesoft.hg.repo.HgInvalidStateException;
+import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.util.Pair;
 
 /**
  * 
@@ -45,8 +48,10 @@
 	private final DigestHelper dh = new DigestHelper();
 	private final RevlogCompressor revlogDataZip;
 	private final Transaction transaction;
-	private int lastEntryBase, lastEntryIndex;
-	private byte[] lastEntryContent;
+	private int lastEntryBase, lastEntryIndex, lastEntryActualLen;
+	// record revision and its full content
+	// the name might be misleading, it does not necessarily match lastEntryIndex
+	private Pair<Integer, byte[]> lastFullContent;
 	private Nodeid lastEntryRevision;
 	private IntMap<Nodeid> revisionCache = new IntMap<Nodeid>(32);
 	private RevlogStream revlogStream;
@@ -61,22 +66,98 @@
 		transaction = tr;
 	}
 	
+	public Pair<Integer,Nodeid> addPatchRevision(GroupElement ge, RevisionToIndexMap clogRevs, RevisionToIndexMap revlogRevs) throws HgIOException, HgRuntimeException {
+		populateLastEntryIndex();
+		//
+		final Nodeid nodeRev = ge.node();
+		final Nodeid csetRev = ge.cset();
+		int linkRev;
+		if (nodeRev.equals(csetRev)) {
+			linkRev = lastEntryIndex+1;
+		} else {
+			linkRev = clogRevs.revisionIndex(csetRev);
+		}
+		assert linkRev >= 0;
+		final Nodeid p1Rev = ge.firstParent();
+		int p1 = p1Rev.isNull() ? NO_REVISION : revlogRevs.revisionIndex(p1Rev);
+		final Nodeid p2Rev = ge.secondParent();
+		int p2 = p2Rev.isNull() ? NO_REVISION : revlogRevs.revisionIndex(p2Rev);
+		Patch p = new Patch();
+		final byte[] patchBytes;
+		try {
+			// XXX there's ge.rawData(), to avoid extra array wrap
+			patchBytes = ge.rawDataByteArray();
+			p.read(new ByteArrayDataAccess(patchBytes));
+		} catch (IOException ex) {
+			throw new HgIOException("Failed to read patch information", ex, null);
+		}
+		//
+		final Nodeid patchBase = ge.patchBase();
+		int patchBaseRev = patchBase.isNull() ? NO_REVISION : revlogRevs.revisionIndex(patchBase);
+		int baseRev = lastEntryIndex == NO_REVISION ? 0 : revlogStream.baseRevision(patchBaseRev);
+		int revLen;
+		DataSource ds;
+		byte[] complete = null;
+		if (patchBaseRev == lastEntryIndex && lastEntryIndex != NO_REVISION) {
+			// we may write patch from GroupElement as is
+			int patchBaseLen = dataLength(patchBaseRev);
+			revLen = patchBaseLen + p.patchSizeDelta();
+			ds = new ByteArrayDataSource(patchBytes);
+		} else {
+			// read baseRev, unless it's the pull to empty repository
+			try {
+				if (lastEntryIndex == NO_REVISION) {
+					complete = p.apply(new ByteArrayDataAccess(new byte[0]), -1);
+					baseRev = 0; // it's done above, but doesn't hurt
+				} else {
+					ReadContentInspector insp = new ReadContentInspector().read(revlogStream, baseRev);
+					complete = p.apply(new ByteArrayDataAccess(insp.content), -1);
+					baseRev = lastEntryIndex + 1;
+				}
+				ds = new ByteArrayDataSource(complete);
+				revLen = complete.length;
+			} catch (IOException ex) {
+				// unlikely to happen, as ByteArrayDataSource doesn't throw IOException
+				throw new HgIOException("Failed to reconstruct revision", ex, null);
+			}
+		}
+		doAdd(nodeRev, p1, p2, linkRev, baseRev, revLen, ds);
+		if (complete != null) {
+			lastFullContent = new Pair<Integer, byte[]>(lastEntryIndex, complete);
+		}
+		return new Pair<Integer, Nodeid>(lastEntryIndex, lastEntryRevision);
+	}
+	
 	/**
 	 * @return nodeid of added revision
 	 * @throws HgRuntimeException 
 	 */
-	public Nodeid addRevision(DataSource content, int linkRevision, int p1, int p2) throws HgIOException, HgRuntimeException {
-		lastEntryRevision = Nodeid.NULL;
-		int revCount = revlogStream.revisionCount();
-		lastEntryIndex = revCount == 0 ? NO_REVISION : revCount - 1;
-		populateLastEntry();
+	public Pair<Integer,Nodeid> addRevision(DataSource content, int linkRevision, int p1, int p2) throws HgIOException, HgRuntimeException {
+		populateLastEntryIndex();
+		populateLastEntryContent();
 		//
 		byte[] contentByteArray = toByteArray(content);
-		Patch patch = GeneratePatchInspector.delta(lastEntryContent, contentByteArray);
+		Patch patch = GeneratePatchInspector.delta(lastFullContent.second(), contentByteArray);
 		int patchSerializedLength = patch.serializedLength();
 		
 		final boolean writeComplete = preferCompleteOverPatch(patchSerializedLength, contentByteArray.length);
 		DataSerializer.DataSource dataSource = writeComplete ? new ByteArrayDataSource(contentByteArray) : patch.new PatchDataSource();
+		//
+		Nodeid p1Rev = revision(p1);
+		Nodeid p2Rev = revision(p2);
+		Nodeid newRev = Nodeid.fromBinary(dh.sha1(p1Rev, p2Rev, contentByteArray).asBinary(), 0);
+		doAdd(newRev, p1, p2, linkRevision, writeComplete ? lastEntryIndex+1 : lastEntryBase, contentByteArray.length, dataSource);
+		lastFullContent = new Pair<Integer, byte[]>(lastEntryIndex, contentByteArray);
+		return new Pair<Integer, Nodeid>(lastEntryIndex, lastEntryRevision);
+	}
+
+	private Nodeid doAdd(Nodeid rev, int p1, int p2, int linkRevision, int baseRevision, int revLen, DataSerializer.DataSource dataSource) throws HgIOException, HgRuntimeException  {
+		assert linkRevision >= 0;
+		assert baseRevision >= 0;
+		assert p1 == NO_REVISION || p1 >= 0;
+		assert p2 == NO_REVISION || p2 >= 0;
+		assert !rev.isNull();
+		assert revLen >= 0;
 		revlogDataZip.reset(dataSource);
 		final int compressedLen;
 		final boolean useCompressedData = preferCompressedOverComplete(revlogDataZip.getCompressedLength(), dataSource.serializeLength());
@@ -87,11 +168,6 @@
 			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, contentByteArray).asBinary();
-		//
-
 		DataSerializer indexFile, dataFile;
 		indexFile = dataFile = null;
 		try {
@@ -99,11 +175,11 @@
 			indexFile = revlogStream.getIndexStreamWriter(transaction);
 			final boolean isInlineData = revlogStream.isInlineData();
 			HeaderWriter revlogHeader = new HeaderWriter(isInlineData);
-			revlogHeader.length(contentByteArray.length, compressedLen);
-			revlogHeader.nodeid(revisionNodeidBytes);
+			revlogHeader.length(revLen, compressedLen);
+			revlogHeader.nodeid(rev.toByteArray());
 			revlogHeader.linkRevision(linkRevision);
 			revlogHeader.parents(p1, p2);
-			revlogHeader.baseRevision(writeComplete ? lastEntryIndex+1 : lastEntryBase);
+			revlogHeader.baseRevision(baseRevision);
 			long lastEntryOffset = revlogStream.newEntryOffset();
 			revlogHeader.offset(lastEntryOffset);
 			//
@@ -124,11 +200,10 @@
 				dataSource.serialize(dataFile);
 			}
 			
-			
-			lastEntryContent = contentByteArray;
 			lastEntryBase = revlogHeader.baseRevision();
 			lastEntryIndex++;
-			lastEntryRevision = Nodeid.fromBinary(revisionNodeidBytes, 0);
+			lastEntryActualLen = revLen;
+			lastEntryRevision = rev;
 			revisionCache.put(lastEntryIndex, lastEntryRevision);
 
 			revlogStream.revisionAdded(lastEntryIndex, lastEntryRevision, lastEntryBase, lastEntryOffset);
@@ -159,32 +234,38 @@
 		return n;
 	}
 	
-	private void populateLastEntry() throws HgRuntimeException {
-		if (lastEntryContent != null) {
+	private int dataLength(int revisionIndex) throws HgInvalidControlFileException, HgInvalidRevisionException {
+		assert revisionIndex >= 0;
+		if (revisionIndex == lastEntryIndex) {
+			return lastEntryActualLen;
+		}
+		if (lastFullContent != null && lastFullContent.first() == revisionIndex) {
+			return lastFullContent.second().length;
+		}
+		return revlogStream.dataLength(revisionIndex);
+	}
+	
+	private void populateLastEntryIndex() throws HgRuntimeException {
+		int revCount = revlogStream.revisionCount();
+		lastEntryIndex = revCount == 0 ? NO_REVISION : revCount - 1;
+	}
+	
+	private void populateLastEntryContent() throws HgRuntimeException {
+		if (lastFullContent != null && lastFullContent.first() == lastEntryIndex) {
+			// we have last entry cached
 			return;
 		}
+		lastEntryRevision = Nodeid.NULL;
 		if (lastEntryIndex != NO_REVISION) {
-			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));
-			}
+			ReadContentInspector insp = new ReadContentInspector().read(revlogStream, lastEntryIndex);
+			lastEntryBase = insp.baseRev;
+			lastEntryRevision = insp.rev;
+			lastFullContent = new Pair<Integer, byte[]>(lastEntryIndex, insp.content);
 		} else {
-			lastEntryContent = new byte[0];
+			lastFullContent = new Pair<Integer, byte[]>(lastEntryIndex, new byte[0]);
 		}
+		assert lastFullContent.first() == lastEntryIndex;
+		assert lastFullContent.second() != null;
 	}
 	
 	public static boolean preferCompleteOverPatch(int patchLength, int fullContentLength) {
@@ -290,4 +371,40 @@
 			return header.capacity();
 		}
 	}
-}
+	
+	// XXX part of HgRevisionMap contract, need public counterparts (along with IndexToRevisionMap)
+	public interface RevisionToIndexMap {
+		
+		/**
+		 * @return {@link HgRepository#NO_REVISION} if unknown revision
+		 */
+		int revisionIndex(Nodeid revision);
+	}
+
+	private static class ReadContentInspector implements RevlogStream.Inspector {
+		public int baseRev;
+		public Nodeid rev;
+		public byte[] content;
+		private IOException failure;
+		
+		public ReadContentInspector read(RevlogStream rs, int revIndex) throws HgInvalidControlFileException {
+			assert revIndex >= 0;
+			rs.iterate(revIndex, revIndex, true, this);
+			if (failure != null) {
+				String m = String.format("Failed to get content of revision %d", revIndex);
+				throw rs.initWithDataFile(new HgInvalidControlFileException(m, failure, null));
+			}
+			return this;
+		}
+		
+		public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) {
+			try {
+				baseRev = baseRevision;
+				rev = Nodeid.fromBinary(nodeid, 0);
+				content = data.byteArray();
+			} catch (IOException ex) {
+				failure = ex;
+			}
+		}
+	}
+}
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/RepoUtils.java	Thu Jul 04 21:09:33 2013 +0200
+++ b/test/org/tmatesoft/hg/test/RepoUtils.java	Tue Jul 09 21:46:45 2013 +0200
@@ -199,4 +199,10 @@
 		}
 		return allRevs;
 	}
+
+	static void assertHgVerifyOk(ErrorCollectorExt errorCollector, File repoLoc) throws InterruptedException, IOException {
+		ExecHelper verifyRun = new ExecHelper(new OutputParser.Stub(), repoLoc);
+		verifyRun.run("hg", "verify");
+		errorCollector.assertEquals("hg verify", 0, verifyRun.getExitValue());
+	}
 }
--- a/test/org/tmatesoft/hg/test/TestCommit.java	Thu Jul 04 21:09:33 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestCommit.java	Tue Jul 09 21:46:45 2013 +0200
@@ -20,7 +20,6 @@
 import static org.tmatesoft.hg.repo.HgRepository.*;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.List;
 
 import org.junit.Rule;
@@ -160,7 +159,7 @@
 		// check if cached value in hgRepo got updated
 		errorCollector.assertEquals("branch1", hgRepo.getWorkingCopyBranchName());
 		//
-		assertHgVerifyOk(repoLoc);
+		RepoUtils.assertHgVerifyOk(errorCollector, repoLoc);
 	}
 
 	/**
@@ -192,7 +191,7 @@
 		new HgCatCommand(hgRepo).file(Path.create("xx")).changeset(commitRev).execute(sink);
 		assertArrayEquals("xyz".getBytes(), sink.toArray());
 		//
-		assertHgVerifyOk(repoLoc);
+		RepoUtils.assertHgVerifyOk(errorCollector, repoLoc);
 	}
 	/**
 	 * perform few commits one by one, into different branches
@@ -240,7 +239,7 @@
 		errorCollector.assertEquals("FIRST", c1.getComment());
 		errorCollector.assertEquals("SECOND", c2.getComment());
 		errorCollector.assertEquals("THIRD", c3.getComment());
-		assertHgVerifyOk(repoLoc);
+		RepoUtils.assertHgVerifyOk(errorCollector, repoLoc);
 	}
 	
 	@Test
@@ -289,7 +288,7 @@
 		errorCollector.assertEquals(csets.get(1).getNodeid(), c2);
 		errorCollector.assertEquals(csets.get(0).getComment(), "FIRST");
 		errorCollector.assertEquals(csets.get(1).getComment(), "SECOND");
-		assertHgVerifyOk(repoLoc);
+		RepoUtils.assertHgVerifyOk(errorCollector, repoLoc);
 		// new commits are drafts by default, check our commit respects this
 		// TODO more tests with children of changesets with draft, secret or public phases (latter - 
 		// new commit is child of public, but there are other commits with draft/secret phases - ensure they are intact)
@@ -488,13 +487,7 @@
 		errorCollector.assertTrue(status.get(Kind.Modified).contains(dfB.getPath()));
 		errorCollector.assertTrue(status.get(Kind.Removed).contains(dfD.getPath()));
 		
-		assertHgVerifyOk(repoLoc);
-	}
-	
-	private void assertHgVerifyOk(File repoLoc) throws InterruptedException, IOException {
-		ExecHelper verifyRun = new ExecHelper(new OutputParser.Stub(), repoLoc);
-		verifyRun.run("hg", "verify");
-		errorCollector.assertEquals("hg verify", 0, verifyRun.getExitValue());
+		RepoUtils.assertHgVerifyOk(errorCollector, repoLoc);
 	}
 	
 	private Transaction newTransaction(SessionContext.Source ctxSource) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/org/tmatesoft/hg/test/TestPull.java	Tue Jul 09 21:46:45 2013 +0200
@@ -0,0 +1,72 @@
+/*
+ * 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.test;
+
+import static org.tmatesoft.hg.repo.HgRepository.TIP;
+
+import java.io.File;
+import java.util.List;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.tmatesoft.hg.core.HgIncomingCommand;
+import org.tmatesoft.hg.core.HgPullCommand;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.repo.HgLookup;
+import org.tmatesoft.hg.repo.HgRemoteRepository;
+import org.tmatesoft.hg.repo.HgRepository;
+
+/**
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class TestPull {
+
+	@Rule
+	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
+	
+	@Test
+	public void testPullToEmpty() throws Exception {
+		File srcRepoLoc = RepoUtils.cloneRepoToTempLocation("test-annotate", "test-pull2empty-src", false);
+		File dstRepoLoc = RepoUtils.initEmptyTempRepo("test-pull2empty-dst");
+		HgServer server = new HgServer().start(srcRepoLoc);
+		try {
+			final HgLookup hgLookup = new HgLookup();
+			final HgRemoteRepository srcRemote = hgLookup.detect(server.getURL());
+			HgRepository dstRepo = hgLookup.detect(dstRepoLoc);
+			HgPullCommand cmd = new HgPullCommand(dstRepo).source(srcRemote);
+			cmd.execute();
+			final HgRepository srcRepo = hgLookup.detect(srcRepoLoc);
+			checkRepositoriesAreSame(srcRepo, dstRepo);
+			final List<Nodeid> incoming = new HgIncomingCommand(dstRepo).against(srcRemote).executeLite();
+			errorCollector.assertTrue(incoming.toString(), incoming.isEmpty());
+			RepoUtils.assertHgVerifyOk(errorCollector, dstRepoLoc);
+		} finally {
+			server.stop();
+		}
+	}
+	
+	// test when pull comes with new file (if AddRevInspector/RevlogStreamWriter is ok with file that doesn't exist 
+
+	private void checkRepositoriesAreSame(HgRepository srcRepo, HgRepository dstRepo) {
+		// XXX copy of TestPush#checkRepositoriesAreSame
+		errorCollector.assertEquals(srcRepo.getChangelog().getRevisionCount(), dstRepo.getChangelog().getRevisionCount());
+		errorCollector.assertEquals(srcRepo.getChangelog().getRevision(0), dstRepo.getChangelog().getRevision(0));
+		errorCollector.assertEquals(srcRepo.getChangelog().getRevision(TIP), dstRepo.getChangelog().getRevision(TIP));
+	}
+}