changeset 730:ae1358e79ee1

Merge
author eugene.petrenko@jetbrains.com
date Tue, 14 Jan 2014 11:36:24 +0100
parents 785ab04c78da (diff) d1469a7cc038 (current diff)
children 5becb1dfecbd
files mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SubRepo.java
diffstat 23 files changed, 810 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/.idea/artifacts/mercurial_common_jar.xml	Mon Jan 13 18:15:41 2014 +0100
+++ b/.idea/artifacts/mercurial_common_jar.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -3,6 +3,9 @@
     <output-path>$PROJECT_DIR$/out/artifacts/mercurial_common_jar</output-path>
     <root id="archive" name="mercurial-common.jar">
       <element id="module-output" name="mercurial-common" />
+      <element id="directory" name="python">
+        <element id="dir-copy" path="$PROJECT_DIR$/mercurial-common/src/python" />
+      </element>
     </root>
   </artifact>
 </component>
\ No newline at end of file
--- a/.idea/artifacts/mercurial_vcs_worker.xml	Mon Jan 13 18:15:41 2014 +0100
+++ b/.idea/artifacts/mercurial_vcs_worker.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -13,6 +13,7 @@
         <element id="artifact" artifact-name="mercurial-server.jar" />
         <element id="library" level="project" name="quartz-1.6.0" />
         <element id="library" level="project" name="commons-compress-1.5" />
+        <element id="library" level="project" name="commons-codec-1.4" />
       </element>
       <element id="file-copy" path="$PROJECT_DIR$/teamcity-plugin.xml" />
     </root>
--- a/.idea/artifacts/plugin.xml	Mon Jan 13 18:15:41 2014 +0100
+++ b/.idea/artifacts/plugin.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -14,6 +14,7 @@
         <element id="artifact" artifact-name="mercurial-server-tc.jar" />
         <element id="library" level="project" name="quartz-1.6.0" />
         <element id="library" level="project" name="commons-compress-1.5" />
+        <element id="library" level="project" name="commons-codec-1.4" />
       </element>
       <element id="directory" name="agent">
         <element id="archive" name="mercurial.zip">
@@ -21,6 +22,8 @@
             <element id="directory" name="lib">
               <element id="artifact" artifact-name="mercurial-common.jar" />
               <element id="artifact" artifact-name="mercurial-agent.jar" />
+              <element id="library" level="project" name="commons-codec-1.4" />
+              <element id="library" level="project" name="commons-compress-1.5" />
             </element>
           </element>
         </element>
--- a/.idea/compiler.xml	Mon Jan 13 18:15:41 2014 +0100
+++ b/.idea/compiler.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -20,6 +20,7 @@
       <entry name="?*.tag" />
       <entry name="?*.template" />
       <entry name="do-not-load-in-vcs-mode" />
+      <entry name="?*.py" />
     </wildcardResourcePatterns>
     <annotationProcessing>
       <profile default="true" name="Default" enabled="false">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.idea/libraries/commons_codec_1_4.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,9 @@
+<component name="libraryTable">
+  <library name="commons-codec-1.4">
+    <CLASSES>
+      <root url="jar://$PROJECT_DIR$/lib/commons-codec-1.4.jar!/" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>
\ No newline at end of file
Binary file lib/commons-codec-1.4.jar has changed
--- a/mercurial-common/mercurial-common.iml	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-common/mercurial-common.iml	Tue Jan 14 11:36:24 2014 +0100
@@ -11,6 +11,7 @@
     <orderEntry type="library" exported="" name="TeamCityAPI-common" level="project" />
     <orderEntry type="library" exported="" name="IDEA-openapi" level="project" />
     <orderEntry type="library" name="jdom" level="project" />
+    <orderEntry type="library" name="commons-codec-1.4" level="project" />
   </component>
 </module>
 
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgRepo.java	Tue Jan 14 11:36:24 2014 +0100
@@ -61,6 +61,11 @@
     return new LogCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
   }
 
+  @NotNull
+  public CommitsAndMountPointsCommand logSubstates() throws VcsException {
+    return new CommitsAndMountPointsCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
+  }
+
   public UpdateCommand update() {
     return new UpdateCommand(myCommandSettingsFactory.create(), myHgPath, myWorkingDir, myAuthSettings);
   }
@@ -298,9 +303,7 @@
     File catDir = null;
     try {
       catDir = cc.execute(asList(".hgsub", ".hgsubstate"), false);
-      File hgsub = new File(catDir, ".hgsub");
-      File hgsubstate = new File(catDir, ".hgsubstate");
-      subrepos = readSubrepositories(hgsub, hgsubstate);
+      subrepos = HgSubs.readSubrepositories(new File(catDir, ".hgsub"), new File(catDir, ".hgsubstate"));
       mySubreposCache.put(revId, subrepos);
       return new HashMap<String, SubRepo>(subrepos);
     } catch (VcsException e) {
@@ -315,48 +318,10 @@
     return myWorkingDir.getAbsolutePath();
   }
 
-  private Map<String, SubRepo> readSubrepositories(@NotNull final File hgsub, @NotNull final File hgsubstate) {
-    if (hgsub.exists() && hgsubstate.exists()) {
-      try {
-        Map<String, String> path2repo = readHgsub(hgsub);
-        Map<String, String> path2revision = readHgsubstate(hgsubstate);
-        Map<String, SubRepo> result = new HashMap<String, SubRepo>();
-        for (Map.Entry<String, String> entry : path2repo.entrySet()) {
-          String path = entry.getKey();
-          String url = entry.getValue();
-          String revision = path2revision.get(path);
-          if (revision != null)
-            result.put(path, new SubRepo(path, url, revision));
-        }
-        return result;
-      } catch (IOException e) {
-        return emptyMap();
-      }
-    } else {
-      return emptyMap();
-    }
-  }
-
-  /*returns map: relative path -> repository url */
-  private Map<String, String> readHgsub(@NotNull final File hgsub) throws IOException {
-    Map<String, String> result = new HashMap<String, String>();
-    for (String line : FileUtil.readFile(hgsub)) {
-      String[] parts = line.split(" = ");
-      if (parts.length == 2)
-        result.put(parts[0], parts[1]);
-    }
-    return result;
-  }
-
-
-  /*returns map: relative path -> revision */
-  private Map<String, String> readHgsubstate(@NotNull final File hgsubstate) throws IOException {
-    Map<String, String> result = new HashMap<String, String>();
-    for (String line : FileUtil.readFile(hgsubstate)) {
-      String[] parts = line.split(" ");
-      if (parts.length == 2)
-        result.put(parts[1], parts[0]);
-    }
-    return result;
+  @NotNull
+  public static String shortId(@NotNull final String s) {
+    if (s.length() > 12)
+      return s.substring(0, 12);
+    return s;
   }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/HgSubs.java	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,102 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.util.StringUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static java.util.Collections.emptyMap;
+
+/**
+ * Created 13.01.14 19:51
+ *
+ * @author Eugene Petrenko (eugene.petrenko@jetbrains.com)
+ */
+public class HgSubs {
+
+  @NotNull
+  public static Map<String, SubRepo> readSubrepositories(@NotNull final File hgsub,
+                                                         @NotNull final File hgsubstate) {
+    if (!hgsub.exists() || !hgsubstate.exists()) {
+      return emptyMap();
+    }
+
+    final Map<String, String> path2repo;
+    final Map<String, String> path2revision;
+    try {
+      path2repo = readHgsub(hgsub);
+      path2revision = readHgsubstate(hgsubstate);
+    } catch (IOException e) {
+      return emptyMap();
+    }
+
+    return readSubrepositories(path2repo, path2revision);
+  }
+
+  @NotNull
+  public static Map<String, SubRepo> readSubrepositories(@Nullable final String hgsubText,
+                                                         @Nullable final String hgsubstateText) {
+
+    if (hgsubstateText == null || hgsubText == null) return emptyMap();
+
+    return readSubrepositories(
+            readHgsub(Arrays.asList(StringUtil.splitByLines(hgsubText))),
+            readHgsub(Arrays.asList(StringUtil.splitByLines(hgsubstateText)))
+    );
+  }
+
+  @NotNull
+  private static Map<String, SubRepo> readSubrepositories(@NotNull final Map<String, String> path2repo,
+                                                          @NotNull final Map<String, String> path2revision) {
+    final Map<String, SubRepo> result = new HashMap<String, SubRepo>();
+    for (Map.Entry<String, String> entry : path2repo.entrySet()) {
+      final String path = entry.getKey();
+      final String url = entry.getValue();
+      final String revision = path2revision.get(path);
+      if (revision != null)
+        result.put(path, new SubRepo(path, url, revision));
+    }
+    return result;
+  }
+
+  @NotNull
+  /*returns map: relative path -> repository url */
+  private static Map<String, String> readHgsub(@NotNull final File hgsub) throws IOException {
+    return readHgsub(FileUtil.readFile(hgsub));
+  }
+
+  @NotNull
+  /*returns map: relative path -> repository url */
+  private static Map<String, String> readHgsub(@NotNull final Collection<String> lines) {
+    Map<String, String> result = new HashMap<String, String>();
+    for (String line : lines) {
+      String[] parts = line.split(" = ");
+      if (parts.length == 2)
+        result.put(parts[0], parts[1]);
+    }
+    return result;
+  }
+
+
+  @NotNull
+  /*returns map: relative path -> revision */
+  private static Map<String, String> readHgsubstate(@NotNull final File hgsubstate) throws IOException {
+    return readHgsubstate(FileUtil.readFile(hgsubstate));
+  }
+
+  @NotNull
+  /*returns map: relative path -> revision */
+  private static Map<String, String> readHgsubstate(@NotNull final Collection<String> lines) {
+    final Map<String, String> result = new HashMap<String, String>();
+    for (String line : lines) {
+      String[] parts = line.split(" ");
+      if (parts.length == 2)
+        result.put(parts[1], parts[0]);
+    }
+    return result;
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SubRepo.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/SubRepo.java	Tue Jan 14 11:36:24 2014 +0100
@@ -17,9 +17,18 @@
 
   public SubRepo(@NotNull String path, @NotNull String url, @NotNull String revision) {
     myPath = path;
-    myUrl = url.trim();
     myRevision = revision;
-    myVcsType = parseVcsType();
+
+    if (url.startsWith("[svn]")) {
+      myVcsType = VcsType.svn;
+      myUrl = url.substring(5);
+    } else if (url.startsWith("[git]")) {
+      myVcsType = VcsType.git;
+      myUrl = url.substring(5);
+    } else {
+      myVcsType = VcsType.hg;
+      myUrl = url;
+    }
   }
 
   @NotNull
@@ -34,9 +43,7 @@
 
   @NotNull
   public String revision() {
-    if (myRevision.length() > 12)
-      return myRevision.substring(0, 12);
-    return myRevision;
+    return HgRepo.shortId(myRevision);
   }
 
   @NotNull
@@ -48,7 +55,9 @@
     return !myUrl.equals(other.url());
   }
 
+  @NotNull
   public String resolveUrl(@NotNull String parentRepoUrl) throws WrongSubrepoUrlException {
+    //TODO: Handle paths on windows. Those paths are not papable for URL
     if (!parentRepoUrl.endsWith("/"))
       parentRepoUrl = parentRepoUrl + "/";
     try {
@@ -86,16 +95,21 @@
     return myPath + " = " + myUrl + "#" + myRevision;
   }
 
-  private VcsType parseVcsType() {
-    if (myUrl.startsWith("[svn]"))
-      return VcsType.svn;
-    if (myUrl.startsWith("[git]"))
-      return VcsType.git;
-    return VcsType.hg;
-  }
+  public static enum VcsType {
+    hg(Constants.VCS_NAME), git("jetbrains.git"), svn("svn")
+    ;
 
-  public static enum VcsType {
-    hg, git, svn
+    private final String myVcsPluginName;
+
+    VcsType(@NotNull String vcsPluginName) {
+      myVcsPluginName = vcsPluginName;
+    }
+
+    @NotNull
+    public String getVcsPluginName() {
+      return myVcsPluginName;
+    }
+
   }
 
   @Override
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommitsAndMountPointsCommand.java	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,131 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import com.intellij.openapi.diagnostic.Logger;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.HgFileUtil;
+import jetbrains.buildServer.util.FileUtil;
+import jetbrains.buildServer.vcs.VcsException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommitsAndMountPointsParser.parseCommits;
+import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommitsAndMountPointsParser.parseFileLog;
+
+/**
+ * Created 03.01.14 14:53
+ *
+ * @author Eugene Petrenko (eugene.petrenko@jetbrains.com)
+ */
+public class CommitsAndMountPointsCommand extends VcsRootCommand {
+  private static final Logger LOG = Logger.getInstance(CommitsAndMountPointsCommand.class.getName());
+
+  public CommitsAndMountPointsCommand(@NotNull final CommandSettings commandSettings,
+                                      @NotNull final String hgPath,
+                                      @NotNull final File workingDir,
+                                      @NotNull final AuthSettings authSettings) {
+    super(commandSettings.addHgEnv("HGENCODING", "UTF-8"), hgPath, workingDir, authSettings);
+  }
+
+  @NotNull
+  private File createTmpDir() throws VcsException {
+    try {
+      return HgFileUtil.createTempDir();
+    } catch (IOException e) {
+      throw new VcsException("Unable to create temporary directory", e);
+    }
+  }
+
+  @NotNull
+  private File extractCommandPy(@NotNull final File root) throws VcsException {
+    try {
+      final File py = new File(root, "load-substates-command.py");
+
+      FileUtil.copyResource(getClass(), "/python/load-substates-command.py", py);
+
+      if (py.length() < 100) throw new IOException("Failed to unpack command resource");
+      return py;
+    } catch (IOException e) {
+      throw new VcsException("Failed to extract .py file: " + e.getMessage(), e);
+    }
+  }
+
+  public void call(@NotNull final Callback consumer) throws VcsException {
+    final File root = createTmpDir();
+
+    try {
+      final File py = extractCommandPy(root);
+
+      callImpl(root, py, consumer);
+    } finally {
+      FileUtil.delete(root);
+    }
+  }
+  
+  public interface Callback {
+    void processHGSubFile(@NotNull final String fileId, @NotNull final String file);
+    void processHGSubStateFile(@NotNull final String fileId, @NotNull final String file);
+    void onCommit(
+            @NotNull String commitNum,
+            @NotNull String commitId,
+            @NotNull String[] parents,
+            @NotNull String branch,
+            @NotNull String[] tags,
+            @NotNull String author,
+            @NotNull String message,
+            @NotNull final Date timestamp,
+            @Nullable String hgsubNodeId,
+            @Nullable String hgsubstateNodeId);
+  }
+
+  private void callImpl(@NotNull final File root,
+                        @NotNull final File commandPy,
+                        @NotNull final Callback consumer) throws VcsException {
+    final MercurialCommandLine cli = createCommandLine();
+    cli.addParameter("--debug");
+    cli.addParameter("--config");
+    cli.addParameter("extensions.logextcj=" + commandPy);
+    cli.addParameter("load-substates");
+    cli.addParameter(new File(root, "result").getPath());
+
+    final CommandResult res = runCommand(cli);
+    final String output = res.getStdout();
+
+    if (!output.contains("##Completed##")) throw new VcsException("Command failed: " + output);
+
+    try {
+      parseFileLog(new File(root, "result.hgsub"), new CommitsAndMountPointsParser.ContentsConsumer() {
+        public void onCommit(@NotNull final String fileNodeId, @NotNull final String content) {
+          consumer.processHGSubFile(fileNodeId, content);
+        }
+      });
+      parseFileLog(new File(root, "result.hgsubstate"), new CommitsAndMountPointsParser.ContentsConsumer() {
+        public void onCommit(@NotNull final String fileNodeId, @NotNull final String content) {
+          consumer.processHGSubStateFile(fileNodeId, content);
+        }
+      });
+
+      parseCommits(new File(root, "result.commits"), new CommitsAndMountPointsParser.CommitsConsumer() {
+        public void onCommit(@NotNull String commitNum,
+                             @NotNull String commitId,
+                             @NotNull String[] parents,
+                             @NotNull String branch,
+                             @NotNull String[] tags,
+                             @NotNull String author,
+                             @NotNull String message,
+                             @NotNull Date timestamp,
+                             @Nullable String hgsubNodeId,
+                             @Nullable String hgsubstateNodeId) {
+          consumer.onCommit(commitNum, commitId, parents, branch, tags, author, message, timestamp, hgsubNodeId, hgsubstateNodeId);
+        }
+      });
+    } catch (IOException e) {
+      throw new VcsException("Failed to parse response files for 'load-substates' command. " + e.getMessage(), e);
+    }
+  }
+
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommitsAndMountPointsParser.java	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,152 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.apache.commons.codec.binary.Base64;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Created 09.01.14 17:10
+ *
+ * @author Eugene Petrenko (eugene.petrenko@jetbrains.com)
+ */
+public class CommitsAndMountPointsParser {
+
+  public static void parseFileLog(@NotNull final File dump,
+                                  @NotNull final ContentsConsumer consumer) throws IOException {
+    final InputStream is = new BufferedInputStream(new FileInputStream(dump));
+
+    final Decoder fileDecoder = new Decoder(5);
+    final BufferedReader st = new BufferedReader(new InputStreamReader(is, "utf-8"));
+    String line;
+    while((line = st.readLine()) != null) {
+      if (!line.startsWith("$$@@@@ ")) continue;
+      final String[] items = line.split(" ");
+      if (items.length != 1 + 2) continue;
+
+      final String commitId = items[1];
+      final String content = fileDecoder.decode(items[2]);
+
+      consumer.onCommit(commitId, content == null ? "" : content);
+    }
+  }
+
+  public interface ContentsConsumer {
+    void onCommit(
+            @NotNull String fileNodeId,
+            @NotNull String content);
+  }
+
+  public interface CommitsConsumer {
+    void onCommit(
+            @NotNull String commitNum,
+            @NotNull String commitId,
+            @NotNull String[] parents,
+            @NotNull String branch,
+            @NotNull String[] tags,
+            @NotNull String author,
+            @NotNull String message,
+            @NotNull Date timestamp,
+            @Nullable String hgsubNodeId,
+            @Nullable String hgsubstateNodeId);
+  }
+
+
+  public static void parseCommits(@NotNull final File dump, @NotNull final CommitsConsumer consumer) throws IOException {
+    final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'Z'HH:mm:ss'T'Z", Locale.ENGLISH);
+
+    final InputStream is = new BufferedInputStream(new FileInputStream(dump));
+
+    final Decoder branchDecoder = new Decoder(250);
+    final Decoder tagsDecoder = new Decoder(250);
+    final Decoder authorDecoder = new Decoder(200);
+    final Decoder messageDecoder = new Decoder(210);
+
+    final BufferedReader st = new BufferedReader(new InputStreamReader(is, "utf-8"));
+    String line;
+    while((line = st.readLine()) != null) {
+      if (!line.startsWith("$$@@@@ ")) continue;
+      final Iterator<String> items = Arrays.asList(line.split(" ")).iterator();
+      items.next(); //$$@@@@
+
+      try {
+      final String commitNum = items.next();
+      final String commitId = items.next();
+      final String[] parents = new String[Integer.parseInt(items.next())];
+      for (int i = 0; i < parents.length; i++) {
+        parents[i] = items.next();
+      }
+      final String branch = branchDecoder.decode(items.next());
+      final String[] tags = new String[Integer.parseInt(items.next())];
+      for (int i = 0; i < tags.length; i++) {
+        tags[i] = tagsDecoder.decode(items.next());
+      }
+
+      final String author = authorDecoder.decode(items.next());
+      final String message = messageDecoder.decode(items.next());
+      final Date time = parseTime(dateFormat, items.next());
+      final String hgsub = textOrNull(items.next());
+      final String hgsubstate = textOrNull(items.next());
+
+      consumer.onCommit(
+              commitNum,
+              commitId,
+              parents,
+              branch,
+              tags,
+              author == null ? "" : author,
+              message == null ? "" : message,
+              time,
+              hgsub,
+              hgsubstate);
+      } catch (NoSuchElementException e) {
+        //NOP
+      }
+    }
+  }
+
+  @NotNull
+  private static Date parseTime(@NotNull final SimpleDateFormat dateFormat,
+                                @NotNull final String time) {
+    try {
+      return dateFormat.parse(time);
+    } catch (ParseException e) {
+      throw new RuntimeException("Failed to parse datetime: " + time + ". " + e, e);
+    }
+  }
+
+  @Nullable
+  private static String textOrNull(@NotNull final String text) {
+    if (text.equals("=====")) return null;
+    return text;
+  }
+
+  private static class Decoder {
+    private final Map<String, String> myCache;
+
+    public Decoder(final int SZ) {
+      myCache = new LinkedHashMap<String, String>(SZ, 0.9f) {
+        @Override
+        protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
+          return size() > SZ;
+        }
+      };
+    }
+
+    public String decode(@NotNull final String base64) throws UnsupportedEncodingException {
+      if (textOrNull(base64) == null) return null;
+
+      final String result = myCache.get(base64);
+      if (result != null) return result;
+
+      final String value = new String(Base64.decodeBase64(base64), "utf-8");
+      //noinspection RedundantStringConstructorCall
+      myCache.put(new String(base64), value);
+      return value;
+    }
+  }
+}
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/LogCommand.java	Tue Jan 14 11:36:24 2014 +0100
@@ -28,10 +28,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 public class LogCommand extends VcsRootCommand {
 
@@ -47,6 +44,7 @@
   private boolean myCalculateParents = true;
   private String myRevsets;
   private File myTemplate;
+  private List<String> myFiles = new ArrayList<String>();
 
   public LogCommand(@NotNull CommandSettings commandSettings,
                     @NotNull String hgPath,
@@ -66,12 +64,12 @@
   }
 
   public LogCommand fromRevision(@Nullable String fromRevision) {
-    myFromId = fromRevision == null ? fromRevision : new ChangeSet(fromRevision).getId();
+    myFromId = fromRevision == null ? null : new ChangeSet(fromRevision).getId();
     return this;
   }
 
   public LogCommand toRevision(@Nullable String toRevision) {
-    myToId = toRevision == null ? toRevision: new ChangeSet(toRevision).getId();
+    myToId = toRevision == null ? null : new ChangeSet(toRevision).getId();
     return this;
   }
 
@@ -100,6 +98,11 @@
     return this;
   }
 
+  public LogCommand forFile(@NotNull final String file) {
+    myFiles.add(file);
+    return this;
+  }
+
   public List<ChangeSet> call() throws VcsException {
     MercurialCommandLine cli = createCommandLine();
     cli.setCharset(Charset.forName("UTF-8"));
@@ -125,6 +128,8 @@
       cli.addParameter(myLimit.toString());
     }
 
+    cli.addParameters(myFiles);
+
     CommandResult res = runCommand(cli);
     String output = res.getStdout();
     try {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-common/src/python/load-substates-command.py	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+##
+##    Copyright 2000-2014 JetBrains
+##
+##     Licensed under the Apache License, Version 2.0 (the "License");
+##       you may not use this file except in compliance with the License.
+##      You may obtain a copy of the License at
+##
+##          http://www.apache.org/licenses/LICENSE-2.0
+##
+##       Unless required by applicable law or agreed to in writing, software
+##       distributed under the License is distributed on an "AS IS" BASIS,
+##       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+##       See the License for the specific language governing permissions and
+##       limitations under the License.
+##
+##
+##  http://www.gnu.org/licenses/gpl-faq.html#GPLModuleLicense
+##  http://www.gnu.org/licenses/license-list.html#apache2
+##  http://en.wikipedia.org/wiki/Apache_License#GPL_compatibility
+##
+##
+"""
+load-substates-command
+"""
+
+import base64
+from mercurial import util, node
+from threading import Thread
+
+
+def load_substates_command(ui, repo, outputFile, **opts):
+    """Tons of docs"""
+
+    ui.write("Fetching commits...")
+
+    NONE = "====="
+
+    def b64(x):
+      if x is None or x == "":
+        return NONE
+      return base64.b64encode( x )
+
+    def fetch_commits():
+      ui.write("Iterating over commits...\n")
+      with open(outputFile + ".commits", "w", 5 * 1024 * 1024) as result:
+        result.write("format: prefix commitID commitHash num_parents parent branch num_tags tag user message date [.hgsub] [.hgsubstate]\n")
+        result.flush()
+
+        commit_to_substates = {}
+
+        def update_sub_states(ctx):
+          def filenode(ctx, filename, i):
+            if filename in ctx.files():
+              try:
+                return node.hex(ctx.filenode(filename))
+              except:
+                # file could have been deleted => so there would be no filenode for it
+                # this also means we should avoid parents as file source
+                return NONE
+            else:
+              for p in ctx.parents():
+                if commit_to_substates.has_key(p.hex()):
+                  v = commit_to_substates[p.hex()][i]
+                  if v != NONE:
+                    return v
+            return NONE
+
+          best_sub = filenode(ctx, ".hgsub", 0)
+          best_state = filenode(ctx, ".hgsubstate", 1)
+          commit_to_substates[ctx.hex()] = (best_sub, best_state)
+          return best_sub, best_state
+
+        for r in list(repo.changelog):
+          ctx = repo[r]
+
+          result.write("$$@@@@ ")                           # magic
+          result.write( str( ctx.rev() ) )                 # commit Num
+          result.write(" ")
+          result.write( ctx.hex() )                        # commit ID
+          result.write(" ")
+          result.write( str( len( ctx.parents()) ) )       # num parents
+
+          for p in ctx.parents():                          # parents
+             result.write(" ")
+             result.write(p.hex())
+
+          result.write(" ")
+          result.write( b64( ctx.branch() ) )              # commit branch
+
+          result.write(" ")
+          result.write( str( len( ctx.tags() ) ) )         # num tags
+
+          for tag in ctx.tags():                           # tags
+             result.write(" ")
+             result.write( b64 ( tag ) )
+
+          result.write(" ")                                 # user
+          result.write( b64( ctx.user()  ) )
+
+          result.write(" ")                                 # message
+          result.write( b64( ctx.description() ) )
+
+          result.write(" ")                                 # date
+          result.write( util.datestr( ctx.date(), "%Y-%m-%dZ%H:%M:%ST%1%2") )
+
+          #resolve sub-repo mounts
+          (sub_node, state_node) = update_sub_states(ctx)
+          result.write(" " + sub_node + " " + state_node)
+          result.write("\n")
+
+        ui.write("Commits iteration completed")
+
+    def fetch_file_revisions(filename):
+      ui.write("Fetching revisions of " + filename + " file\n")
+      with open(outputFile + filename, "w", 5 * 1024 * 1024) as result:
+        result.write("format: prefix commitID base64(" + filename + ")\n")
+        result.flush()
+
+        log = repo.file(filename)
+        for r in log:
+          result.write("$$@@@@ " + node.hex(log.node(r)) + " " + b64(log.read(r)) + "\n")
+
+      ui.write("All revisions of file " + filename + " are fetched\n")
+
+    tasks = [
+        Thread(target=fetch_commits, args=[], name="Fetch commits graph"),
+        Thread(target=fetch_file_revisions, args=[".hgsub"], name="Fetch .hgsub"),
+        Thread(target=fetch_file_revisions, args=[".hgsubstate"], name="Fetch .hgsubstate"),
+    ]
+
+    for task in tasks:
+        task.start()
+
+    for task in tasks:
+        task.join()
+
+    ui.write("\n##Completed##\n")
+
+
+#so here goes command registration and options
+cmdtable = {
+    "load-substates": (load_substates_command, [ ], " [options] OUTPUT_FILE")
+}
+
+testedwith = '2.2.2'
+buglink = "@jonnyzzz"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialCommitsInfoBuilderStates.java	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,38 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created 13.01.14 19:45
+ *
+ * @author Eugene Petrenko (eugene.petrenko@jetbrains.com)
+ */
+public class MercurialCommitsInfoBuilderStates {
+  private final Map<String, String> myHgStateNodes = new HashMap<String, String>();
+  private final Map<String, String> myHgSubNodes = new HashMap<String, String>();
+
+  public void addSubNode(@NotNull final String hash, @NotNull final String text) {
+    myHgSubNodes.put(hash, text);
+  }
+
+  public void addStateNode(@NotNull final String hash, @NotNull final String text) {
+    myHgStateNodes.put(hash, text);
+  }
+
+
+  @NotNull
+  public Map<String, SubRepo> mounts(@Nullable final String subHash,
+                                     @Nullable final String stateHash) {
+    if (subHash == null || stateHash == null) return Collections.emptyMap();
+
+    //TODO: may cache parsed contents to make it work faster
+    return HgSubs.readSubrepositories(
+            myHgSubNodes.get(subHash),
+            myHgStateNodes.get(stateHash));
+  }
+}
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialCommitsInfoBuilderSupport.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialCommitsInfoBuilderSupport.java	Tue Jan 14 11:36:24 2014 +0100
@@ -1,16 +1,14 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.ChangeSet;
-import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.ChangeSetRevision;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommitsAndMountPointsCommand;
 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.HgVcsRoot;
-import jetbrains.buildServer.util.MultiMap;
+import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand;
 import jetbrains.buildServer.vcs.*;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
-import java.util.Map;
-
-import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand.ZERO_PARENT_ID;
-import static jetbrains.buildServer.buildTriggers.vcs.mercurial.command.LogCommand.ZERO_PARENT_SHORT_ID;
+import java.net.URISyntaxException;
+import java.util.Date;
 
 /**
  * Created 30.09.13 13:05
@@ -36,31 +34,67 @@
     final ServerHgRepo repo = mySupport.createRepo(hgRoot);
     mySupport.syncRepository(hgRoot);
 
-    MultiMap<String, String> commitToTag = new MultiMap<String, String>();
-    for (Map.Entry<String, String> e : repo.tags().call().entrySet()) {
-      commitToTag.putValue(e.getValue(), e.getKey());
-    }
 
-    //TODO: make stream parsing of commits instead of parsing of collected string output
-    for (ChangeSet set : repo.logNoFiles().showCommitsFromAllBranches().call()) {
-      final CommitDataBean change = new CommitDataBean(set.getId(), set.getFullVersion(), set.getTimestamp());
-      for (ChangeSetRevision p : set.getParents()) {
-        final String commitId = p.getId();
+    repo.logSubstates().call(new CommitsAndMountPointsCommand.Callback() {
+      private final MercurialCommitsInfoBuilderStates subs = new MercurialCommitsInfoBuilderStates();
 
-        if (ZERO_PARENT_ID.equals(commitId)) continue;
-        if (ZERO_PARENT_SHORT_ID.equals(commitId)) continue;
-        change.addParentRevision(commitId);
+      public void processHGSubFile(@NotNull String fileId, @NotNull String text) {
+        subs.addSubNode(fileId, text);
       }
 
-      change.setCommitMessage(set.getDescription());
-      change.setCommitAuthor(set.getUser());
-      change.addBranch(set.getBranch());
-
-      for (String tag : commitToTag.get(set.getId())) {
-        change.addTag(tag);
+      public void processHGSubStateFile(@NotNull String fileId, @NotNull String text) {
+        subs.addStateNode(fileId, text);
       }
 
-      consumer.consumeCommit(change);
-    }
+      public void onCommit(@NotNull String commitNum,
+                           @NotNull String commitId,
+                           @NotNull String[] parents,
+                           @NotNull String branch,
+                           @NotNull String[] tags,
+                           @NotNull String author,
+                           @NotNull String message,
+                           @NotNull Date timestamp,
+                           @Nullable String hgsubNodeId,
+                           @Nullable String hgsubstateNodeId) {
+
+        final CommitDataBean bean = new CommitDataBean(
+                HgRepo.shortId(commitId),
+                HgRepo.shortId(commitId),
+                timestamp
+                );
+
+        for (String parent : parents) {
+          if (LogCommand.ZERO_PARENT_ID.equals(parent)) continue;
+          if (LogCommand.ZERO_PARENT_SHORT_ID.equals(parent)) continue;
+          bean.addParentRevision(HgRepo.shortId(parent));
+        }
+
+        for (String tag : tags) {
+          if ("tip".equals(tag)) continue;
+
+          bean.addTag(tag);
+        }
+
+        bean.addBranch(branch);
+        bean.setCommitAuthor(author);
+        bean.setCommitMessage(message);
+
+        for (SubRepo subRepo : subs.mounts(hgsubNodeId, hgsubstateNodeId).values()) {
+          try {
+            bean.addMountPoint(new CommitMountPointDataBean(
+                    subRepo.vcsType().getVcsPluginName(),
+                    subRepo.resolveUrl(hgRoot.getRepository()),
+                    subRepo.path(),
+                    subRepo.revision()
+                    ));
+          } catch (URISyntaxException e) {
+            //NOP
+          }
+        }
+
+        consumer.consumeCommit(bean);
+      }
+    });
   }
+
 }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/BaseMercurialTestCase.java	Tue Jan 14 11:36:24 2014 +0100
@@ -1,6 +1,8 @@
 package jetbrains.buildServer.buildTriggers.vcs.mercurial;
 
 import jetbrains.buildServer.TempFiles;
+import jetbrains.buildServer.serverSide.BasePropertiesModel;
+import jetbrains.buildServer.serverSide.TeamCityProperties;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 
@@ -9,6 +11,7 @@
 
   @BeforeMethod
   public void setUp() throws Exception {
+    new TeamCityProperties() {{ setModel(new BasePropertiesModel() {});}};
     myTempFiles = new TempFiles();
   }
 
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommitsInfoBuilderSupportTest.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/CommitsInfoBuilderSupportTest.java	Tue Jan 14 11:36:24 2014 +0100
@@ -103,4 +103,20 @@
     }
   }
 
+  @Test(enabled = false)
+  public void should_return_graphcommits() throws Exception {
+    VcsRoot root = vcsRoot().withLocalRepository(new File("F:\\Work\\ReSharper")).build();
+
+    final long start = System.currentTimeMillis();
+    mySupport.collectCommits(root, CheckoutRules.DEFAULT, new CommitsInfoBuilder.CommitsConsumer() {
+      public void consumeCommit(@NotNull CommitInfo commit) {
+
+      }
+    });
+
+    final long actual = System.currentTimeMillis() - start;
+    System.out.println("computed in " + actual + " ms");
+  }
+
+
 }
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/VcsRootBuilder.java	Tue Jan 14 11:36:24 2014 +0100
@@ -82,6 +82,11 @@
   }
 
 
+  public VcsRootBuilder withLocalRepository(@NotNull final File repo) {
+    return withUrl(repo.getPath()).withCloneRepositoryTo(repo.getParentFile());
+  }
+
+
   public VcsRootBuilder withUrl(@NotNull File repository) {
     myRepository = repository.getAbsolutePath();
     return this;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/CommitsAndMountPointsCommandParserTest.java	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,72 @@
+package jetbrains.buildServer.buildTriggers.vcs.mercurial.command;
+
+import org.jetbrains.annotations.NotNull;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Created 08.01.14 22:39
+ *
+ * @author Eugene Petrenko (eugene.petrenko@jetbrains.com)
+ */
+public class CommitsAndMountPointsCommandParserTest {
+
+  @Test
+  public void test_parse_commits_a() throws IOException {
+    final List<String> log = new ArrayList<String>();
+    CommitsAndMountPointsParser.parseCommits(new File("./mercurial-tests/testData/subst/a.commits"), proxy(CommitsAndMountPointsParser.CommitsConsumer.class, log));
+
+    for (String s : log) {
+      System.out.println(s);
+    }
+    Assert.assertEquals(log, Arrays.asList(
+            "onCommit [0, 4cc56f5e7eee469e4b33db02a22baaac46c1ecee, [0000000000000000000000000000000000000000], default, [], dsha, API changes, 1184063759000, null, null]",
+            "onCommit [5, 90ca28808a205909463386921939f59eb4259fcf, [e7298836dbcbbf3a79b255db718eca04f5b4824f], default, [], dsl, Switch to Orcas, 1184687116000, null, null]",
+            "onCommit [2490, 6a4484ecc639cc0e3783a86f40a3ad87405970fc, [1d30763d0f9f94ba7368c36a186119c11c453f4c, 549dca11e6a1080a81ce711849e20c23ec1b4e05], default, [], Sergey.Shkredov, Merge with serjic, 1334149579000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, e64b208e4ae1f3b3302731e358fc4c494dbe881d]",
+            "onCommit [2513, 6cef50ceefad10da64d256a01d2b2ac3153b465a, [afb1278c857e2dbfe1a6cd4e7e646e5fe7c48b51, 3cc6eef56b1ed4a49b39c0fb785334ca618c5e9c], SDKReferences, [], Kirill.Skrygan, Merge two SDKReferences heads, 1334240172000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, 35b0fe2dd3e1e2553a6407cf732e0fc7cd4c2e08]",
+            "onCommit [2592, 6f096ed0264b2378f98f125c545f01b227cdf23d, [9816adffa38d039da651b902a08d3ad45bc35a12, 9de1e1e88af7b440c6f4c55cb0410afdad2ce89f], default, [], Alexander.Shvedov, Merge with XamlWorkshop, 1334582015000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, 0b2f91b87bb6b3fc848111b9ddc6c250d6b06453]",
+            "onCommit [2593, b4047e0d64ecb070d2826166d1b4b9d25672e023, [c3205f71291d525b6d9282b8491abe20ec8373d0], serjic, [], Sergey.Shkredov, fix some problems., 1334574266000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, d8c99c4baa952506946dadd627ca918ffce24bc7]",
+            "onCommit [2600, ddd190f94b157f1ac215df16f530e51a91b4c7c2, [66f3b250e585ec69f9396bf311ce8fe986e4c1e3], xvost, [], xvost, RSRP-294634, 1334569744000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, c6a6c6fb5b932c1c89e0c411c150859ec4906078]",
+            "onCommit [812, 68f94180a341a6c174d015cd0fa0b10a66e2615b, [cf898c0dac48a20985a5b72dbc801a03300d4a3c], Subplatforms, [MsiReady], baltic, ++, 1328895848000, 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53, 8418205f67a695ae82b4b4d4946cec9082202553]"
+    ));
+  }
+
+  @Test
+  public void test_parse_files_a() throws IOException {
+    final List<String> log = new ArrayList<String>();
+    CommitsAndMountPointsParser.parseFileLog(new File("./mercurial-tests/testData/subst/a.files"), proxy(CommitsAndMountPointsParser.ContentsConsumer.class, log));
+
+    for (String s : log) {
+      System.out.println(s);
+    }
+    Assert.assertEquals(log, Arrays.asList(
+            "onCommit [b80de5d138758541c5f05265ad144ab9fa86d1db, ]",
+            "onCommit [05a11677a1346201e93dee1968178c019b42bbb0, 0000000000000000000000000000000000000000 Platform\n]",
+            "onCommit [d0ae0cceebc043697902c66e9fec0ecf621d954c, 0c4b35f8765c032a4101fa2024267be31f3c8343 Platform\n]"
+    ));
+  }
+
+  @NotNull
+  private <T> T proxy(@NotNull final Class<T> clazz, @NotNull final List<String> log) {
+    return clazz.cast(Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{clazz}, new InvocationHandler() {
+      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+        for (int i = 0; i < args.length; i++) {
+          Object arg = args[i];
+          if (arg instanceof Date) args[i] = ((Date) arg).getTime();
+        }
+        log.add(method.getName() + " " + Arrays.deepToString(args));
+        return null;
+      }
+    }));
+  }
+}
--- a/mercurial-tests/src/testng.xml	Mon Jan 13 18:15:41 2014 +0100
+++ b/mercurial-tests/src/testng.xml	Tue Jan 14 11:36:24 2014 +0100
@@ -37,6 +37,7 @@
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.CommitsInfoBuilderSupportTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialModificationInfoBuilderTest"/>
       <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.MercurialUrlSupportTest"/>
+      <class name="jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommitsAndMountPointsCommandParserTest"/>
     </classes>
   </test>
 </suite>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subst/a.commits	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,9 @@
+format: prefix commitID commitHash num_parents parent branch num_tags tag user message date [.hgsub] [.hgsubstate]
+$$@@@@ 0 4cc56f5e7eee469e4b33db02a22baaac46c1ecee 1 0000000000000000000000000000000000000000 ZGVmYXVsdA== 0 ZHNoYQ== QVBJIGNoYW5nZXM= 2007-07-10Z10:35:59T+0000 ===== =====
+$$@@@@ 5 90ca28808a205909463386921939f59eb4259fcf 1 e7298836dbcbbf3a79b255db718eca04f5b4824f ZGVmYXVsdA== 0 ZHNs U3dpdGNoIHRvIE9yY2Fz 2007-07-17Z15:45:16T+0000 ===== =====
+$$@@@@ 2490 6a4484ecc639cc0e3783a86f40a3ad87405970fc 2 1d30763d0f9f94ba7368c36a186119c11c453f4c 549dca11e6a1080a81ce711849e20c23ec1b4e05 ZGVmYXVsdA== 0 U2VyZ2V5LlNoa3JlZG92 TWVyZ2Ugd2l0aCBzZXJqaWM= 2012-04-11Z17:06:19T+0400 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 e64b208e4ae1f3b3302731e358fc4c494dbe881d
+$$@@@@ 2513 6cef50ceefad10da64d256a01d2b2ac3153b465a 2 afb1278c857e2dbfe1a6cd4e7e646e5fe7c48b51 3cc6eef56b1ed4a49b39c0fb785334ca618c5e9c U0RLUmVmZXJlbmNlcw== 0 S2lyaWxsLlNrcnlnYW4= TWVyZ2UgdHdvIFNES1JlZmVyZW5jZXMgaGVhZHM= 2012-04-12Z18:16:12T+0400 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 35b0fe2dd3e1e2553a6407cf732e0fc7cd4c2e08
+$$@@@@ 2592 6f096ed0264b2378f98f125c545f01b227cdf23d 2 9816adffa38d039da651b902a08d3ad45bc35a12 9de1e1e88af7b440c6f4c55cb0410afdad2ce89f ZGVmYXVsdA== 0 QWxleGFuZGVyLlNodmVkb3Y= TWVyZ2Ugd2l0aCBYYW1sV29ya3Nob3A= 2012-04-16Z17:13:35T+0400 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 0b2f91b87bb6b3fc848111b9ddc6c250d6b06453
+$$@@@@ 2593 b4047e0d64ecb070d2826166d1b4b9d25672e023 1 c3205f71291d525b6d9282b8491abe20ec8373d0 c2Vyamlj 0 U2VyZ2V5LlNoa3JlZG92 Zml4IHNvbWUgcHJvYmxlbXMu 2012-04-16Z15:04:26T+0400 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 d8c99c4baa952506946dadd627ca918ffce24bc7
+$$@@@@ 2600 ddd190f94b157f1ac215df16f530e51a91b4c7c2 1 66f3b250e585ec69f9396bf311ce8fe986e4c1e3 eHZvc3Q= 0 eHZvc3Q= UlNSUC0yOTQ2MzQ= 2012-04-16Z11:49:04T+0200 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 c6a6c6fb5b932c1c89e0c411c150859ec4906078
+$$@@@@ 812 68f94180a341a6c174d015cd0fa0b10a66e2615b 1 cf898c0dac48a20985a5b72dbc801a03300d4a3c U3VicGxhdGZvcm1z 1 TXNpUmVhZHk= YmFsdGlj Kys= 2012-02-10Z21:44:08T+0400 9a12f93d5158a5cd75962d4ebd24fbe7e45ace53 8418205f67a695ae82b4b4d4946cec9082202553
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial-tests/testData/subst/a.files	Tue Jan 14 11:36:24 2014 +0100
@@ -0,0 +1,4 @@
+format: prefix commitID base64(.hgsubstate)
+$$@@@@ b80de5d138758541c5f05265ad144ab9fa86d1db =====
+$$@@@@ 05a11677a1346201e93dee1968178c019b42bbb0 MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCBQbGF0Zm9ybQo=
+$$@@@@ d0ae0cceebc043697902c66e9fec0ecf621d954c MGM0YjM1Zjg3NjVjMDMyYTQxMDFmYTIwMjQyNjdiZTMxZjNjODM0MyBQbGF0Zm9ybQo=