You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by fr...@apache.org on 2016/05/27 09:23:02 UTC

svn commit: r1745725 - in /jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak: plugins/segment/FileStoreDiff.java run/FileStoreDiffCommand.java run/PrintingDiff.java run/SegmentTarUtils.java run/SegmentUtils.java

Author: frm
Date: Fri May 27 09:23:02 2016
New Revision: 1745725

URL: http://svn.apache.org/viewvc?rev=1745725&view=rev
Log:
OAK-4340 - Add a flag to choose between segment store implementations in the "tarmkdiff" command

Added:
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java   (with props)
Removed:
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreDiff.java
Modified:
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/FileStoreDiffCommand.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentTarUtils.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentUtils.java

Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/FileStoreDiffCommand.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/FileStoreDiffCommand.java?rev=1745725&r1=1745724&r2=1745725&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/FileStoreDiffCommand.java (original)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/FileStoreDiffCommand.java Fri May 27 09:23:02 2016
@@ -17,13 +17,59 @@
 
 package org.apache.jackrabbit.oak.run;
 
-import org.apache.jackrabbit.oak.plugins.segment.FileStoreDiff;
+import static java.util.Arrays.asList;
+
+import java.io.File;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
 
 class FileStoreDiffCommand implements Command {
 
     @Override
     public void execute(String... args) throws Exception {
-        FileStoreDiff.main(args);
+        OptionParser parser = new OptionParser();
+        OptionSpec<?> help = parser.acceptsAll(asList("h", "?", "help"), "show help").forHelp();
+        OptionSpec<File> storeO = parser.nonOptions("Path to segment store (required)").ofType(File.class);
+        OptionSpec<File> outO = parser.accepts("output", "Output file").withRequiredArg().ofType(File.class).defaultsTo(defaultOutFile());
+        OptionSpec<?> listOnlyO = parser.accepts("list", "Lists available revisions");
+        OptionSpec<String> intervalO = parser.accepts("diff", "Revision diff interval. Ex '--diff=R0..R1'. 'HEAD' can be used to reference the latest head revision, ie. '--diff=R0..HEAD'").withRequiredArg().ofType(String.class);
+        OptionSpec<?> incrementalO = parser.accepts("incremental", "Runs diffs between each subsequent revisions in the provided interval");
+        OptionSpec<String> pathO = parser.accepts("path", "Filter diff by given path").withRequiredArg().ofType(String.class).defaultsTo("/");
+        OptionSpec<?> ignoreSNFEsO = parser.accepts("ignore-snfes", "Ignores SegmentNotFoundExceptions and continues running the diff (experimental)");
+        OptionSpec segmentTar = parser.accepts("segment-tar", "Use oak-segment-tar instead of oak-segment");
+        OptionSet options = parser.parse(args);
+
+        if (options.has(help)) {
+            parser.printHelpOn(System.out);
+            System.exit(0);
+        }
+
+        File store = storeO.value(options);
+
+        if (store == null) {
+            parser.printHelpOn(System.out);
+            System.exit(1);
+        }
+
+        File out = outO.value(options);
+
+        boolean listOnly = options.has(listOnlyO);
+        String interval = intervalO.value(options);
+        boolean incremental = options.has(incrementalO);
+        String path = pathO.value(options);
+        boolean ignoreSNFEs = options.has(ignoreSNFEsO);
+
+        if (options.has(segmentTar)) {
+            SegmentTarUtils.diff(store, out, listOnly, interval, incremental, path, ignoreSNFEs);
+        } else {
+            SegmentUtils.diff(store, out, listOnly, interval, incremental, path, ignoreSNFEs);
+        }
+    }
+
+    private File defaultOutFile() {
+        return new File("diff_" + System.currentTimeMillis() + ".log");
     }
 
 }

Added: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java?rev=1745725&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java (added)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java Fri May 27 09:23:02 2016
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.jackrabbit.oak.run;
+
+import static com.google.common.collect.Iterables.transform;
+import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
+import static org.apache.jackrabbit.oak.api.Type.BINARIES;
+import static org.apache.jackrabbit.oak.api.Type.BINARY;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
+
+import java.io.PrintWriter;
+
+import com.google.common.base.Function;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+
+final class PrintingDiff implements NodeStateDiff {
+
+    private static final Function<Blob, String> BLOB_LENGTH = new Function<Blob, String>() {
+
+        @Override
+        public String apply(Blob b) {
+            return safeGetLength(b);
+        }
+
+        private String safeGetLength(Blob b) {
+            try {
+                return byteCountToDisplaySize(b.length());
+            } catch (IllegalStateException e) {
+                // missing BlobStore probably
+            }
+            return "[N/A]";
+        }
+
+    };
+
+    private final PrintWriter pw;
+
+    private final String path;
+
+    private final boolean skipProps;
+
+    PrintingDiff(PrintWriter pw, String path) {
+        this(pw, path, false);
+    }
+
+    private PrintingDiff(PrintWriter pw, String path, boolean skipProps) {
+        this.pw = pw;
+        this.path = path;
+        this.skipProps = skipProps;
+    }
+
+    @Override
+    public boolean propertyAdded(PropertyState after) {
+        if (!skipProps) {
+            pw.println("    + " + toString(after));
+        }
+        return true;
+    }
+
+    @Override
+    public boolean propertyChanged(PropertyState before, PropertyState after) {
+        if (!skipProps) {
+            pw.println("    ^ " + before.getName());
+            pw.println("      - " + toString(before));
+            pw.println("      + " + toString(after));
+        }
+        return true;
+    }
+
+    @Override
+    public boolean propertyDeleted(PropertyState before) {
+        if (!skipProps) {
+            pw.println("    - " + toString(before));
+        }
+        return true;
+    }
+
+    @Override
+    public boolean childNodeAdded(String name, NodeState after) {
+        String p = concat(path, name);
+        pw.println("+ " + p);
+        return after.compareAgainstBaseState(EMPTY_NODE, new PrintingDiff(
+                pw, p));
+    }
+
+    @Override
+    public boolean childNodeChanged(String name, NodeState before, NodeState after) {
+        String p = concat(path, name);
+        pw.println("^ " + p);
+        return after.compareAgainstBaseState(before,
+                new PrintingDiff(pw, p));
+    }
+
+    @Override
+    public boolean childNodeDeleted(String name, NodeState before) {
+        String p = concat(path, name);
+        pw.println("- " + p);
+        return MISSING_NODE.compareAgainstBaseState(before, new PrintingDiff(pw, p, true));
+    }
+
+    private static String toString(PropertyState ps) {
+        StringBuilder val = new StringBuilder();
+        val.append(ps.getName()).append("<").append(ps.getType()).append(">");
+        if (ps.getType() == BINARY) {
+            String v = BLOB_LENGTH.apply(ps.getValue(BINARY));
+            val.append(" = {").append(v).append("}");
+        } else if (ps.getType() == BINARIES) {
+            String v = transform(ps.getValue(BINARIES), BLOB_LENGTH).toString();
+            val.append("[").append(ps.count()).append("] = ").append(v);
+        } else if (ps.isArray()) {
+            val.append("[").append(ps.count()).append("] = ").append(ps.getValue(STRINGS));
+        } else {
+            val.append(" = ").append(ps.getValue(STRING));
+        }
+        return ps.getName() + "<" + ps.getType() + ">" + val.toString();
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PrintingDiff.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentTarUtils.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentTarUtils.java?rev=1745725&r1=1745724&r2=1745725&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentTarUtils.java (original)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentTarUtils.java Fri May 27 09:23:02 2016
@@ -17,13 +17,17 @@
 
 package org.apache.jackrabbit.oak.run;
 
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Lists.reverse;
 import static com.google.common.collect.Sets.newHashSet;
 import static com.google.common.collect.Sets.newTreeSet;
 import static com.google.common.escape.Escapers.builder;
 import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
+import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.checkFileStoreVersionOrFail;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.isValidFileStoreOrFail;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.newBasicReadOnlyBlobStore;
+import static org.apache.jackrabbit.oak.segment.RecordId.fromString;
 import static org.apache.jackrabbit.oak.segment.RecordType.NODE;
 import static org.apache.jackrabbit.oak.segment.SegmentGraph.writeGCGraph;
 import static org.apache.jackrabbit.oak.segment.SegmentGraph.writeSegmentGraph;
@@ -35,12 +39,14 @@ import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.io.RandomAccessFile;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
@@ -73,6 +79,7 @@ import org.apache.jackrabbit.oak.segment
 import org.apache.jackrabbit.oak.segment.SegmentId;
 import org.apache.jackrabbit.oak.segment.SegmentNodeState;
 import org.apache.jackrabbit.oak.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.segment.SegmentNotFoundException;
 import org.apache.jackrabbit.oak.segment.SegmentPropertyState;
 import org.apache.jackrabbit.oak.segment.SegmentTracker;
 import org.apache.jackrabbit.oak.segment.file.FileStore;
@@ -209,6 +216,150 @@ final class SegmentTarUtils {
         }
     }
 
+    static void diff(File store, File out, boolean listOnly, String interval, boolean incremental, String path, boolean ignoreSNFEs) throws IOException {
+        if (listOnly) {
+            listRevs(store, out);
+        } else {
+            diff(store, interval, incremental, out, path, ignoreSNFEs);
+        }
+    }
+
+    private static void listRevs(File store, File out) throws IOException {
+        System.out.println("Store " + store);
+        System.out.println("Writing revisions to " + out);
+        List<String> revs = readRevisions(store);
+        if (revs.isEmpty()) {
+            System.out.println("No revisions found.");
+            return;
+        }
+        PrintWriter pw = new PrintWriter(out);
+        try {
+            for (String r : revs) {
+                pw.println(r);
+            }
+        } finally {
+            pw.close();
+        }
+    }
+
+    private static List<String> readRevisions(File store) {
+        File journal = new File(store, "journal.log");
+        if (!journal.exists()) {
+            return newArrayList();
+        }
+
+        List<String> revs = newArrayList();
+        JournalReader journalReader = null;
+        try {
+            journalReader = new JournalReader(journal);
+            try {
+                revs = newArrayList(journalReader.iterator());
+            } finally {
+                journalReader.close();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (journalReader != null) {
+                    journalReader.close();
+                }
+            } catch (IOException e) {
+            }
+        }
+        return revs;
+    }
+
+    private static void diff(File dir, String interval, boolean incremental, File out, String filter, boolean ignoreSNFEs) throws IOException {
+        System.out.println("Store " + dir);
+        System.out.println("Writing diff to " + out);
+        String[] tokens = interval.trim().split("\\.\\.");
+        if (tokens.length != 2) {
+            System.out.println("Error parsing revision interval '" + interval
+                    + "'.");
+            return;
+        }
+        ReadOnlyStore store = FileStore.builder(dir).withBlobStore(newBasicReadOnlyBlobStore()).buildReadOnly();
+        RecordId idL = null;
+        RecordId idR = null;
+        try {
+            if (tokens[0].equalsIgnoreCase("head")) {
+                idL = store.getHead().getRecordId();
+            } else {
+                idL = fromString(store.getTracker(), tokens[0]);
+            }
+            if (tokens[1].equalsIgnoreCase("head")) {
+                idR = store.getHead().getRecordId();
+            } else {
+                idR = fromString(store.getTracker(), tokens[1]);
+            }
+        } catch (IllegalArgumentException ex) {
+            System.out.println("Error parsing revision interval '" + interval + "': " + ex.getMessage());
+            ex.printStackTrace();
+            return;
+        }
+
+        long start = System.currentTimeMillis();
+        PrintWriter pw = new PrintWriter(out);
+        try {
+            if (incremental) {
+                List<String> revs = readRevisions(dir);
+                System.out.println("Generating diff between " + idL + " and " + idR + " incrementally. Found " + revs.size() + " revisions.");
+
+                int s = revs.indexOf(idL.toString10());
+                int e = revs.indexOf(idR.toString10());
+                if (s == -1 || e == -1) {
+                    System.out.println("Unable to match input revisions with FileStore.");
+                    return;
+                }
+                List<String> revDiffs = revs.subList(Math.min(s, e), Math.max(s, e) + 1);
+                if (s > e) {
+                    // reverse list
+                    revDiffs = reverse(revDiffs);
+                }
+                if (revDiffs.size() < 2) {
+                    System.out.println("Nothing to diff: " + revDiffs);
+                    return;
+                }
+                Iterator<String> revDiffsIt = revDiffs.iterator();
+                RecordId idLt = fromString(store.getTracker(), revDiffsIt.next());
+                while (revDiffsIt.hasNext()) {
+                    RecordId idRt = fromString(store.getTracker(), revDiffsIt.next());
+                    boolean good = diff(store, idLt, idRt, filter, pw);
+                    idLt = idRt;
+                    if (!good && !ignoreSNFEs) {
+                        break;
+                    }
+                }
+            } else {
+                System.out.println("Generating diff between " + idL + " and " + idR);
+                diff(store, idL, idR, filter, pw);
+            }
+        } finally {
+            pw.close();
+        }
+        long dur = System.currentTimeMillis() - start;
+        System.out.println("Finished in " + dur + " ms.");
+    }
+
+    private static boolean diff(ReadOnlyStore store, RecordId idL, RecordId idR, String filter, PrintWriter pw) throws IOException {
+        pw.println("rev " + idL + ".." + idR);
+        try {
+            NodeState before = new SegmentNodeState(store, idL).getChildNode("root");
+            NodeState after = new SegmentNodeState(store, idR).getChildNode("root");
+            for (String name : elements(filter)) {
+                before = before.getChildNode(name);
+                after = after.getChildNode(name);
+            }
+            after.compareAgainstBaseState(before, new PrintingDiff(pw, filter));
+            return true;
+        } catch (SegmentNotFoundException ex) {
+            System.out.println(ex.getMessage());
+            pw.println("#SNFE " + ex.getSegmentId());
+            return false;
+        }
+    }
+
     private static void debugFileStore(FileStore store) {
         Map<SegmentId, List<SegmentId>> idmap = Maps.newHashMap();
         int dataCount = 0;
@@ -420,10 +571,10 @@ final class SegmentTarUtils {
                 RecordId id1 = store.getHead().getRecordId();
                 RecordId id2 = null;
                 if (matcher.group(2) != null) {
-                    id1 = RecordId.fromString(store.getTracker(),
+                    id1 = fromString(store.getTracker(),
                             matcher.group(3));
                     if (matcher.group(4) != null) {
-                        id2 = RecordId.fromString(store.getTracker(),
+                        id2 = fromString(store.getTracker(),
                                 matcher.group(5));
                     }
                 }

Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentUtils.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentUtils.java?rev=1745725&r1=1745724&r2=1745725&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentUtils.java (original)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/SegmentUtils.java Fri May 27 09:23:02 2016
@@ -17,15 +17,19 @@
 
 package org.apache.jackrabbit.oak.run;
 
+import static com.google.common.collect.Lists.reverse;
 import static com.google.common.collect.Sets.newHashSet;
 import static com.google.common.collect.Sets.newTreeSet;
 import static com.google.common.escape.Escapers.builder;
 import static javax.jcr.PropertyType.BINARY;
 import static javax.jcr.PropertyType.STRING;
 import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
+import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.newBasicReadOnlyBlobStore;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.openFileStore;
 import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.openReadOnlyFileStore;
+import static org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper.readRevisions;
+import static org.apache.jackrabbit.oak.plugins.segment.RecordId.fromString;
 import static org.apache.jackrabbit.oak.plugins.segment.RecordType.NODE;
 import static org.apache.jackrabbit.oak.plugins.segment.SegmentGraph.writeGCGraph;
 import static org.apache.jackrabbit.oak.plugins.segment.SegmentGraph.writeSegmentGraph;
@@ -37,12 +41,14 @@ import static org.slf4j.LoggerFactory.ge
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.io.RandomAccessFile;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
@@ -75,10 +81,12 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.segment.SegmentId;
 import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeState;
 import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentNotFoundException;
 import org.apache.jackrabbit.oak.plugins.segment.SegmentPropertyState;
 import org.apache.jackrabbit.oak.plugins.segment.SegmentTracker;
 import org.apache.jackrabbit.oak.plugins.segment.compaction.CompactionStrategy;
 import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore.ReadOnlyStore;
 import org.apache.jackrabbit.oak.plugins.segment.file.JournalReader;
 import org.apache.jackrabbit.oak.plugins.segment.file.tooling.RevisionHistory;
 import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
@@ -225,6 +233,122 @@ class SegmentUtils {
         }
     }
 
+    static void diff(File store, File out, boolean listOnly, String interval, boolean incremental, String path, boolean ignoreSNFEs) throws IOException {
+        if (listOnly) {
+            listRevs(store, out);
+        } else {
+            diff(store, interval, incremental, out, path, ignoreSNFEs);
+        }
+    }
+
+    private static void listRevs(File store, File out) throws IOException {
+        System.out.println("Store " + store);
+        System.out.println("Writing revisions to " + out);
+        List<String> revs = readRevisions(store);
+        if (revs.isEmpty()) {
+            System.out.println("No revisions found.");
+            return;
+        }
+        PrintWriter pw = new PrintWriter(out);
+        try {
+            for (String r : revs) {
+                pw.println(r);
+            }
+        } finally {
+            pw.close();
+        }
+    }
+
+    private static void diff(File dir, String interval, boolean incremental, File out, String filter, boolean ignoreSNFEs) throws IOException {
+        System.out.println("Store " + dir);
+        System.out.println("Writing diff to " + out);
+        String[] tokens = interval.trim().split("\\.\\.");
+        if (tokens.length != 2) {
+            System.out.println("Error parsing revision interval '" + interval
+                    + "'.");
+            return;
+        }
+        ReadOnlyStore store = FileStore.builder(dir).withBlobStore(newBasicReadOnlyBlobStore()).buildReadOnly();
+        RecordId idL = null;
+        RecordId idR = null;
+        try {
+            if (tokens[0].equalsIgnoreCase("head")) {
+                idL = store.getHead().getRecordId();
+            } else {
+                idL = fromString(store.getTracker(), tokens[0]);
+            }
+            if (tokens[1].equalsIgnoreCase("head")) {
+                idR = store.getHead().getRecordId();
+            } else {
+                idR = fromString(store.getTracker(), tokens[1]);
+            }
+        } catch (IllegalArgumentException ex) {
+            System.out.println("Error parsing revision interval '" + interval + "': " + ex.getMessage());
+            ex.printStackTrace();
+            return;
+        }
+
+        long start = System.currentTimeMillis();
+        PrintWriter pw = new PrintWriter(out);
+        try {
+            if (incremental) {
+                List<String> revs = readRevisions(dir);
+                System.out.println("Generating diff between " + idL + " and " + idR + " incrementally. Found " + revs.size() + " revisions.");
+
+                int s = revs.indexOf(idL.toString10());
+                int e = revs.indexOf(idR.toString10());
+                if (s == -1 || e == -1) {
+                    System.out.println("Unable to match input revisions with FileStore.");
+                    return;
+                }
+                List<String> revDiffs = revs.subList(Math.min(s, e), Math.max(s, e) + 1);
+                if (s > e) {
+                    // reverse list
+                    revDiffs = reverse(revDiffs);
+                }
+                if (revDiffs.size() < 2) {
+                    System.out.println("Nothing to diff: " + revDiffs);
+                    return;
+                }
+                Iterator<String> revDiffsIt = revDiffs.iterator();
+                RecordId idLt = fromString(store.getTracker(), revDiffsIt.next());
+                while (revDiffsIt.hasNext()) {
+                    RecordId idRt = fromString(store.getTracker(), revDiffsIt.next());
+                    boolean good = diff(store, idLt, idRt, filter, pw);
+                    idLt = idRt;
+                    if (!good && !ignoreSNFEs) {
+                        break;
+                    }
+                }
+            } else {
+                System.out.println("Generating diff between " + idL + " and " + idR);
+                diff(store, idL, idR, filter, pw);
+            }
+        } finally {
+            pw.close();
+        }
+        long dur = System.currentTimeMillis() - start;
+        System.out.println("Finished in " + dur + " ms.");
+    }
+
+    private static boolean diff(ReadOnlyStore store, RecordId idL, RecordId idR, String filter, PrintWriter pw) throws IOException {
+        pw.println("rev " + idL + ".." + idR);
+        try {
+            NodeState before = new SegmentNodeState(idL).getChildNode("root");
+            NodeState after = new SegmentNodeState(idR).getChildNode("root");
+            for (String name : elements(filter)) {
+                before = before.getChildNode(name);
+                after = after.getChildNode(name);
+            }
+            after.compareAgainstBaseState(before, new PrintingDiff(pw, filter));
+            return true;
+        } catch (SegmentNotFoundException ex) {
+            System.out.println(ex.getMessage());
+            pw.println("#SNFE " + ex.getSegmentId());
+            return false;
+        }
+    }
+
     private static void debugFileStore(FileStore store) {
         Map<SegmentId, List<SegmentId>> idmap = Maps.newHashMap();
         int dataCount = 0;