site

(djk)
2011-05-29: First pass at rebasing.

First pass at rebasing.

diff --git a/alien/src/wormarc/ExternalRefs.java b/alien/src/wormarc/ExternalRefs.java
--- a/alien/src/wormarc/ExternalRefs.java
+++ b/alien/src/wormarc/ExternalRefs.java
@@ -125,15 +125,17 @@ public final class ExternalRefs {
         return new ExternalRefs(refs);
     }
 
-    public String pretty() {
+    public String pretty(String labelWithTrailingSpace) {
         StringBuilder buffer = new StringBuilder();
-        buffer.append("--- ExternalRefs ---\n");
+        buffer.append(String.format("--- %sExternalRefs ---\n", labelWithTrailingSpace));
         for (Reference ref: mRefs) {
             buffer.append(String.format("   [%d]:%s\n", ref.mKind, ref.mExternalKey));
         }
         buffer.append("---");
         return buffer.toString();
     }
+    public String pretty() { return pretty(""); }
+
 
     public static ExternalRefs create(List<String> keys, int kind) {
         List<Reference> refs = new ArrayList<Reference>();
diff --git a/alien/src/wormarc/FileManifest.java b/alien/src/wormarc/FileManifest.java
--- a/alien/src/wormarc/FileManifest.java
+++ b/alien/src/wormarc/FileManifest.java
@@ -48,6 +48,8 @@ import java.util.List;
 import java.util.Set;
 
 public class FileManifest implements LinkContainer {
+    public final static Changes NO_CHANGES = new FileManifest.Changes();
+
     // Map the digest of a file to the head of the file chain it is stored in.
     Map<LinkDigest, LinkDigest> mFileDigestToChainHeadDigest = new HashMap<LinkDigest, LinkDigest>();
     // Map a human readable name to the digest of a file
@@ -349,7 +351,7 @@ public class FileManifest implements Lin
         public final Set<String> mDeleted;
         public final Set<String> mAdded;
         public final Set<String> mModified;
-        public final Set<String> mUnmodified;
+        public final Set<String> mUnmodified; // DCI: GET RID OF THIS?
 
         // PUNT for now. takes a little work.
         // The LinkDigest -> name inverse map isn't guaranteed to be one to one.
@@ -362,6 +364,14 @@ public class FileManifest implements Lin
             mUnmodified = unmodified;
         }
 
+        protected Changes() { // For NO_CHANGES
+            this(new HashSet<String>(),
+                 new HashSet<String>(),
+                 new HashSet<String>(),
+                 new HashSet<String>() // Not correct. LATER: get rid of unmodified?
+                 );
+        }
+
         public boolean isUnmodified() {
             return mDeleted.isEmpty() && mAdded.isEmpty() && mModified.isEmpty();
         }
diff --git a/alien/src/wormarc/LinkDigest.java b/alien/src/wormarc/LinkDigest.java
--- a/alien/src/wormarc/LinkDigest.java
+++ b/alien/src/wormarc/LinkDigest.java
@@ -26,6 +26,8 @@ package wormarc;
 
 import java.util.Arrays;
 
+// DCI: Should this just be called digest, since I use it for other stuff
+//      e.g. file digests in FileManifest?
 // !^*&^$#&^*&^%  no immutable byte[] values in Java!
 public final class LinkDigest implements Comparable<LinkDigest> {
     private final byte[] mBytes = new byte[20];
diff --git a/alien/src/wormarc/RootObjectKind.java b/alien/src/wormarc/RootObjectKind.java
--- a/alien/src/wormarc/RootObjectKind.java
+++ b/alien/src/wormarc/RootObjectKind.java
@@ -36,6 +36,7 @@ public class RootObjectKind {
     // Otherwise the root objects don't fit in a topkey.
     public final static int SINGLE_FILE = 3;
     public final static int PARENT_REFERENCES = 4;
+    public final static int REBASE_REFERENCES = 5; // hmmm application specfic code creeping into wormarc.
 
     public static LinkContainer getContainer(Archive archive,
                                              Archive.RootObject obj) throws IOException {
diff --git a/alien/src/wormarc/cli/CLI.java b/alien/src/wormarc/cli/CLI.java
--- a/alien/src/wormarc/cli/CLI.java
+++ b/alien/src/wormarc/cli/CLI.java
@@ -471,10 +471,17 @@ public class CLI {
                 LinkDigest digest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
                 if (!digest.isNullDigest()) {
                     ExternalRefs parents = ExternalRefs.fromBytes(archive.getFile(digest));
-                    sOut.println(parents.pretty());
+                    sOut.println(parents.pretty("parent "));
                 } else {
                     sOut.println("No PARENT_REFERENCES in the root objects!");
                 }
+                digest = archive.getRootObject(RootObjectKind.REBASE_REFERENCES);
+                if (!digest.isNullDigest()) {
+                    ExternalRefs parents = ExternalRefs.fromBytes(archive.getFile(digest));
+                    sOut.println(parents.pretty("rebase "));
+                } else {
+                    sOut.println("No REBASE_REFERENCES in the root objects!");
+                }
             }
         },
         new Command("push", true, false, " <insert_uri>", "insert the current head into Freenet") {
diff --git a/doc/latest_release.txt b/doc/latest_release.txt
--- a/doc/latest_release.txt
+++ b/doc/latest_release.txt
@@ -1,9 +1,25 @@
-I added explicit support for "Discussion" links in page headers.
-The special page, "TalkPageDoesNotExist" is displayed if no talk page exists yet.
+I added preliminary support for rebasing changes from other wiki versions.
 
-I had to modify the DumpWiki CLI client to handle the new page header format.
-You need to handle a new %s for the talk page href.
-See ./templates/wiki_dump_template.html for an example.
+You can load a "secondary" archive to rebase from either by using
+the "[rebase]" links in the "Discover" page or by explictly entering
+an archive uri at the bottom of a page and checking the "rebase"
+radio button.
 
+The new macro:
 
+<<<RebasedChanges>>> shows a directory level diff between the
+parent version and the rebased version.
 
+There have been a lot of changes and this release may be buggier
+than previous releases.
+
+If all else fails go back to the previous version:
+USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/jfniki_releases/0/
+
+KNOWN LIMITATIONS:
+o There is no file level diffing yet (probably for a while yet)
+o You can't rebase into an empty archive.
+o There is no support in the changelog or discover UI for rebased
+  versions yet.
+o Submitted versions are INCOMPATIBLE with all earlier versions and
+  will cause spurious scary validation error. Sorry.
diff --git a/readme.txt b/readme.txt
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
-20110515
+20110529
 djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 
 WARNING:
@@ -84,7 +84,6 @@ sethcg@a-tin0kMl1I~8xn5lkQDqYZRExKLzJITr
 ---
 Dev notes
 ---
-BUG: Header links to discussion pages. [requested by a real user]
 BUG: Default FCP port wrong for CLI client. [requested by a real user]
 BUG: fix the discover UI to correctly handle posts from a different nym than the insert
 BUG: wikitext should use unix line terminators not DOS (+1 byte per line)
@@ -123,6 +122,7 @@ IDEA: Freetalk vs Freenet interop
       Hmmm... not sure if people would use this feature because of the correlation of ids.
 ---
 Fixed bugs:
+710d700bc7a1: BUG: Header links to discussion pages. [requested by a real user]
 2ce3a4499a2c: BUG: No way to create an empty wiki from the UI. [requested by a real user]
 cab9533f4cb8: BUG: Can the <<<TOC>>> macro be made to play nice with the ContentFilter? [suggestion from sethcg]
 
diff --git a/release/cut_release.py b/release/cut_release.py
--- a/release/cut_release.py
+++ b/release/cut_release.py
@@ -65,7 +65,7 @@ PUBLIC_SITE = "USK@kRM~jJVREwnN2qnA8R0Vt
 ############################################################
 # Indexes of refereneced USK sites
 
-FREENET_DOC_WIKI_IDX = 31
+FREENET_DOC_WIKI_IDX = 34
 FNIKI_IDX = 84
 REPO_IDX = 15
 
diff --git a/src/fniki/wiki/ArchiveManager.java b/src/fniki/wiki/ArchiveManager.java
--- a/src/fniki/wiki/ArchiveManager.java
+++ b/src/fniki/wiki/ArchiveManager.java
@@ -27,8 +27,10 @@ package fniki.wiki;
 import java.io.IOException;
 import java.io.PrintStream;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
@@ -72,11 +74,18 @@ public class ArchiveManager {
     // Block hex digest to CHK key map.
     Map<String, String> mSha1ToChk = new HashMap<String, String>();
 
+    // Main version
     String mParentUri;
     Archive mArchive;
     FileManifest mFileManifest;
     LocalWikiChanges mOverlay;
 
+    // Read only secondary version to rebase into main version.
+    String mSecondaryUri;
+    Archive mSecondaryArchive;
+    FileManifest mSecondaryFileManifest;
+    WikiTextChanges mSecondaryChanges;
+
     public void setDebugOutput(PrintStream out) {
         FreenetIO.setDebugOutput(out);
     }
@@ -129,18 +138,42 @@ public class ArchiveManager {
     public void setBissName(String value) { mBissName= value; }
     public String getBissName() { return mBissName; }
 
-    // DCI: Fix this to roll back state on exceptions.
-    public void load(String uri) throws IOException {
+    public String getSecondaryUri() { return mSecondaryUri; }
+
+    public void load(String uri, boolean isSecondary) throws IOException {
+        if (isSecondary && mFileManifest == null) {
+            throw new IOException("Can't load secondary archive because no primary archive is loaded yet!");
+        }
         FreenetIO io = makeIO();
         io.setRequestUri(uri);
         Archive archive = Archive.load(io);
         validateUriHashes(archive, uri, true);
+        FileManifest manifest = FileManifest.fromArchiveRootObject(archive);
 
+        if (isSecondary) {
+            WikiTextChanges remoteChanges = new RemoteWikiTextChanges(mFileManifest, archive, manifest);
+            // Survived possible exceptions.
+            mSecondaryArchive = archive;
+            mSecondaryFileManifest = manifest;
+            mSecondaryChanges = remoteChanges;
+            mSecondaryUri = uri;
+            return;
+        }
+
+        LocalWikiChanges localChanges = new LocalWikiChanges(archive, manifest);
+        // Survived possible exceptions.
         mArchive = archive;
-        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
-        mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
+        mFileManifest = manifest;
+        mOverlay = localChanges;
         mParentUri = uri;
+
+        // Loading primary resets secondary.
+        mSecondaryArchive = null;
+        mSecondaryFileManifest = null;
+        mSecondaryChanges = null;
+        mSecondaryUri = null;
     }
+    public void load(String uri) throws IOException { load(uri, false); }
 
     public void createEmptyArchive() throws IOException {
         mArchive = new Archive();
@@ -159,33 +192,40 @@ public class ArchiveManager {
         return new FreenetIO(mFcpHost, mFcpPort, null, mSha1ToChk);
     }
 
+
+    private static String getExternalRefDigest(Archive archive, int kind) throws IOException {
+        String value = "";
+        LinkDigest refsDigest = archive.getRootObject(kind);
+        if (!refsDigest.isNullDigest()) {
+            ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
+            for (ExternalRefs.Reference ref : refs.mRefs) {
+                if (ref.mKind != ExternalRefs.KIND_FREENET) {
+                    continue;
+                }
+                value += "_";
+                value += IOUtil.getFileDigest(IOUtil.toStreamAsUtf8(ref.mExternalKey))
+                    .hexDigest(8);
+            }
+        }
+        return value;
+    }
+
     // The name of a jfniki archive includes the hash of the
     // full archive manifest file, and hashes of the SSK of
-    // it's parent(s).
+    // it's parent(s) including an optional rebase parent.
     //
     // use '_' instead of '|' because '|' gets percent escaped.
-    // <sha1_of_am_file>[<underbar>sha1_of_parent_ssk]
+    // <sha1_of_am_file>[[<underbar>sha1_of_parent_ssk] [<underbar>sha1_of_rebase_ssk]]
     private static String makeUriNamePart(Archive archive) throws IOException {
         // Generate a unique SSK.
         LinkDigest digest = archive.getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
         // The hash of the actual file, not just the chain head SHA.
         LinkDigest fileHash = IOUtil.getFileDigest(archive.getFile(digest));
 
-        String parentKeyHashes = "";
-        LinkDigest refsDigest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
-        if (!refsDigest.isNullDigest()) {
-            ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
-            for (ExternalRefs.Reference ref : refs.mRefs) {
-                if (ref.mKind != ExternalRefs.KIND_FREENET) {
-                    continue;
-                }
-                parentKeyHashes += "_";
-                parentKeyHashes += IOUtil.getFileDigest(IOUtil.toStreamAsUtf8(ref.mExternalKey))
-                    .hexDigest(8);
-            }
-        }
-
-        return fileHash.hexDigest(8) + parentKeyHashes;
+        return
+            fileHash.hexDigest(8) +
+            getExternalRefDigest(archive, RootObjectKind.PARENT_REFERENCES) +
+            getExternalRefDigest(archive, RootObjectKind.REBASE_REFERENCES);
     }
 
     private static void validateUriHashes(Archive archive,
@@ -244,6 +284,7 @@ public class ArchiveManager {
 
         // Commit local changes to the Archive.
         copy.unsetRootObject(RootObjectKind.PARENT_REFERENCES);
+        copy.unsetRootObject(RootObjectKind.REBASE_REFERENCES);
 
         // Update the archive
         copy.startUpdate();
@@ -260,6 +301,15 @@ public class ArchiveManager {
                                       RootObjectKind.PARENT_REFERENCES);
         }
 
+        if (mSecondaryUri != null) {
+            out.println("Set REBASE_REFERENCES: " + mSecondaryUri);
+            List<String> keys = Arrays.asList(mSecondaryUri);
+            LinkDigest refs =
+                copy.updateRootObject(ExternalRefs.create(keys, ExternalRefs.KIND_FREENET)
+                                      .toBytes(),
+                                      RootObjectKind.REBASE_REFERENCES);
+        }
+
         copy.commitUpdate();
         copy.compressAndUpdateArchiveManifest();
 
@@ -298,6 +348,17 @@ public class ArchiveManager {
         return mFileManifest.diffTo(mArchive, mOverlay);
     }
 
+    public List<RebaseStatus.Record> getRebaseStatus() throws IOException {
+        if (mSecondaryFileManifest == null) {
+            System.err.println("No rebased changes!");
+            return new ArrayList<RebaseStatus.Record>();
+        }
+
+        return RebaseStatus.getStatus(mOverlay.getFiles(),
+                                      mFileManifest.getMap(),
+                                      mSecondaryFileManifest.getMap());
+    }
+
     public void readChangeLog(PrintStream out,
                               AuditArchive.ChangeLogCallback callback) throws IOException {
 
@@ -343,6 +404,13 @@ public class ArchiveManager {
         return mOverlay;
     }
 
+    public WikiTextChanges getRemoteChanges() throws IOException {
+        if (mSecondaryChanges == null) {
+            return RemoteWikiTextChanges.NO_REMOTE_CHANGES;
+        }
+        return mSecondaryChanges;
+    }
+
     public String getNym(String sskRequestUri, boolean showPublicKey) {
         int start = sskRequestUri.indexOf("@");
         int end = sskRequestUri.indexOf(",");
diff --git a/src/fniki/wiki/HtmlUtils.java b/src/fniki/wiki/HtmlUtils.java
--- a/src/fniki/wiki/HtmlUtils.java
+++ b/src/fniki/wiki/HtmlUtils.java
@@ -42,7 +42,8 @@ public class HtmlUtils {
 
     public static String makeHref(String fullPath,
                                   String actionValue, String titleValue,
-                                  String uriValue, String gotoValue) {
+                                  String uriValue, String gotoValue,
+                                  String secondaryValue) {
 
         String query = "";
         if (actionValue != null) {
@@ -64,6 +65,11 @@ public class HtmlUtils {
             query += "goto=" + gotoValue;
         }
 
+        if (secondaryValue != null) {
+            if (query.length() > 0) { query += "&"; }
+            query += "secondary=" + secondaryValue;
+        }
+
         if (query.length() == 0) {
             query = null;
         }
@@ -82,7 +88,7 @@ public class HtmlUtils {
     }
 
     public static String makeHref(String fullPath) {
-        return makeHref(fullPath, null, null, null, null);
+        return makeHref(fullPath, null, null, null, null, null);
     }
 
     public static String makeFproxyHref(String fproxyPrefix, String freenetUri) {
@@ -100,11 +106,12 @@ public class HtmlUtils {
         }
 
         String href = makeHref(prefix + '/' + name,
-                               action, null, null, name);
+                               action, null, null, name, null);
 
         sb.append("<a href=\"" + href + "\">" + escapeHTML(title) + "</a>");
     }
 
+
     public static void appendChangesSet(String prefix, StringBuilder sb, String label, Set<String> values) {
         if (values.size() == 0) {
             return;
@@ -129,6 +136,41 @@ public class HtmlUtils {
         appendChangesSet(prefix, sb, "Modified", changes.mModified);
     }
 
+    private static final String opName(int op) {
+        switch (op) {
+        case RebaseStatus.OP_ADDED: return "Added";
+        case RebaseStatus.OP_DELETED: return "Missing";
+        case RebaseStatus.OP_MODIFIED: return "Modified";
+        default: throw new IllegalArgumentException();
+        }
+    }
+
+    private static final String statusName(int status) {
+        switch (status) {
+        case RebaseStatus.LOCALLY_MODIFIED: return "Locally Modified";
+        case RebaseStatus.PARENT: return "Parent";
+        case RebaseStatus.REBASE: return "Rebase";
+        default: throw new IllegalArgumentException();
+        }
+    }
+
+    public static void appendRebaseStatusHtml(List<RebaseStatus.Record> records, String prefix, StringBuilder sb) {
+        sb.append("<table border=\"1\">\n");
+        sb.append("<tr> <th>Page</th> <th>Rebase Change</th> <th>Local Copy</th> </tr> \n");
+        for (RebaseStatus.Record record : records) {
+            sb.append("<tr> <td><a href=\"");
+            sb.append(makeHref(prefix + "/" + record.mName, "view", null, null, null, null));
+            sb.append("\">");
+            sb.append(escapeHTML(record.mName)); // Paranoid.
+            sb.append("</a></td> <td>");
+            sb.append(escapeHTML(opName(record.mDiffOp))); // Paranoid.
+            sb.append("</td> <td>");
+            sb.append(escapeHTML(statusName(record.mStatus))); // Paranoid.
+            sb.append("</td> </tr>\n");
+        }
+        sb.append("</table>\n");
+    }
+
     // Path is the ony variable that has potentially dangerous data.
     public static String buttonHtml(String fullPath, String label, String action) {
         final String fmt =
@@ -150,13 +192,28 @@ public class HtmlUtils {
         }
     }
 
+    // DCI: Change name: getLoadVersionLink
     public static String getVersionLink(String prefix, String name, String uri, String action,
                                         boolean hexLabel) {
         String label = uri;
         if (hexLabel) {
             label = getVersionHex(uri);
         }
-        String href = makeHref(prefix + name, action, null, uri, null);
+        String href = makeHref(prefix + name, action, null, uri, null, null);
+
+        return String.format("<a href=\"%s\">%s</a>", href, escapeHTML(label));
+    }
+
+    // DCI: Think through arguments
+    public static String getRebaseLink(String prefix, String name, String uri, String action, String label,
+                                       boolean hexLabel) {
+
+        if (label == null) { label = uri; }
+
+        if (hexLabel) {
+            label = getVersionHex(uri);
+        }
+        String href = makeHref(prefix + name, action, null, uri, null, "true");
 
         return String.format("<a href=\"%s\">%s</a>", href, escapeHTML(label));
     }
diff --git a/src/fniki/wiki/LocalWikiChanges.java b/src/fniki/wiki/LocalWikiChanges.java
--- a/src/fniki/wiki/LocalWikiChanges.java
+++ b/src/fniki/wiki/LocalWikiChanges.java
@@ -110,6 +110,11 @@ public class LocalWikiChanges implements
         return mMap.containsKey(pageName);
     }
 
+    public boolean wasLocallyDeleted(String pageName) {
+        LocalChange change = mMap.get(pageName);
+        return (change != null) && change.mDeleted;
+    }
+
     public void revertLocalChange(String pageName) {
         if (!mMap.containsKey(pageName)) {
             return;
@@ -117,14 +122,31 @@ public class LocalWikiChanges implements
         mMap.remove(pageName);
     }
 
+    public boolean hasUnmodifiedPage(String pageName) throws IOException {
+        return mBaseVersion.getMap().containsKey(pageName);
+    }
+
+    public String getUnmodifiedPage(String name) throws IOException {
+        return IOUtil.readUtf8StringAndClose(mBaseVersion.getFile(mArchive, name));
+    }
+
     ////////////////////////////////////////////////////////////
     // FileManifest.IO implementation
+
+    // Implement optional digest so we can use the return value from this
+    // function in FileManifest.diff().
     public Map<String, LinkDigest> getFiles() throws IOException {
-        Map<String, LinkDigest> files = new HashMap<String, LinkDigest>();
-        for (String name : getNames()) {
-            files.put(name, LinkDigest.NULL_DIGEST);
+        // DCI: Test carefully. Might hit untested code paths in FileManifest.
+        Map<String, LinkDigest> map = new HashMap<String, LinkDigest>(mBaseVersion.getMap()); // Must copy!
+
+        for (String name : mMap.keySet()) {
+            if (mMap.get(name).mDeleted) {
+                map.remove(name);
+            } else { // Either added or modified.
+                map.put(name, IOUtil.getFileDigest(getFile(name)));
+            }
         }
-        return files;
+        return map;
     }
 
     public InputStream getFile(String name) throws IOException {
diff --git a/src/fniki/wiki/QueryBase.java b/src/fniki/wiki/QueryBase.java
--- a/src/fniki/wiki/QueryBase.java
+++ b/src/fniki/wiki/QueryBase.java
@@ -39,6 +39,7 @@ public abstract class QueryBase implemen
     protected final static String PARAMS[] = new String[] {
         "action", "title",
         "uri", "goto",
+        "secondary",
         "savepage", "savetext",
         "formPassword",
         // Configuration stuff.
diff --git a/src/fniki/wiki/RebaseStatus.java b/src/fniki/wiki/RebaseStatus.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/RebaseStatus.java
@@ -0,0 +1,202 @@
+/* Class to generate the underlying rep used to display rebase status.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library 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; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library 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.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import wormarc.FileManifest;
+import wormarc.LinkDigest;
+
+public class RebaseStatus {
+    public final static int LOCALLY_MODIFIED = 1;
+    public final static int PARENT = 2;
+    public final static int REBASE = 3;
+
+    public final static int OP_ADDED = 4;
+    public final static int OP_DELETED = 5;
+    public final static int OP_MODIFIED = 6;
+
+    public final static class Record {
+        public final String mName;
+        public final int mDiffOp;
+        public final int mStatus;
+        Record(String name, int op, int status) {
+            if (name == null || name.trim().equals("")) {
+                throw new IllegalArgumentException("Bad name.");
+            }
+            if (op < OP_ADDED || op > OP_MODIFIED) {
+                throw new IllegalArgumentException("Bad op.");
+            }
+            if (status < LOCALLY_MODIFIED || status > REBASE) {
+                throw new IllegalArgumentException("Bad status.");
+            }
+            mName = name;
+            mDiffOp = op;
+            mStatus = status;
+        }
+    }
+
+    private static LinkDigest get(Map<String, LinkDigest> map, String name) {
+        LinkDigest digest = map.get(name);
+        if (digest == null) {
+            return LinkDigest.NULL_DIGEST;
+        }
+        return digest;
+    }
+
+    // Get status of all rebased files at once.
+    public static List<Record> getStatus(Map<String, LinkDigest> working,
+                                         Map<String, LinkDigest> parent,
+                                         Map<String, LinkDigest> rebase) {
+
+        // LATER: Is this worth cleaning up?
+        // RemoteWikiTextChanges has a copy of changes. Pass it in? Not worth it? diff is cheap
+        FileManifest.Changes changes = FileManifest.diff(parent, rebase);
+
+        HashMap<String, Integer> opLut = new HashMap<String, Integer>();
+        for (String name : changes.mAdded) { opLut.put(name, OP_ADDED); }
+        for (String name : changes.mDeleted) { opLut.put(name, OP_DELETED); }
+        for (String name : changes.mModified) { opLut.put(name, OP_MODIFIED); }
+
+        List<String> ordered = new ArrayList<String>(opLut.keySet());
+        Collections.sort(ordered);
+
+        List<Record> records = new ArrayList<Record>();
+        for (String name : ordered) {
+            int state = 0;
+            LinkDigest workingDigest = get(working, name);
+            if (workingDigest.equals(get(parent, name))) {
+                state = PARENT;
+            } else if (workingDigest.equals(get(rebase, name))) {
+                state = REBASE;
+            } else {
+                state = LOCALLY_MODIFIED;
+            }
+            //System.err.println(String.format("%s, %d, %d", name, opLut.get(name), state));
+            records.add(new Record(name, opLut.get(name), state));
+        }
+        return records;
+    }
+
+    private static void assert_(boolean condition) {
+        if (!condition) {throw new RuntimeException("Assertion failure."); }
+    }
+
+    // Determine whether the current version of a single page  is the
+    // PARENT, REBASE or LOCALLY_MODIFIED version.
+    //
+    // INTENT: Encapsulate logic and keep it separate from presentation.
+    // LATER: Possible to simplify this? Seems way too complicated. This is really crappy code.
+    public static int pageChangeKind(WikiTextStorage localStorage, WikiTextChanges remoteChanges,
+                                      String name) throws IOException {
+        if ((!localStorage.hasLocalChange(name)) &&
+            (!remoteChanges.hasChange(name))) {
+            //System.err.println("BC0");
+            if (localStorage.hasPage(name)) {
+                return PARENT;
+            } else {
+                return LOCALLY_MODIFIED; // Handle new pages.
+            }
+        }
+
+        String remote = null;
+        if (remoteChanges.hasChange(name) &&
+            !remoteChanges.wasDeleted(name)) {
+            remote = remoteChanges.getPage(name);
+        }
+
+        String local = null;
+        if (localStorage.hasLocalChange(name) &&
+            !localStorage.wasLocallyDeleted(name)) {
+            local = localStorage.getPage(name);
+        }
+
+        if (local == null && remote == null) {
+            // Hmmmm... could get rid of this case (remove assertion and handle below).
+            // Leave it for now. This is already hard enough to understand.
+            if (localStorage.hasLocalChange(name) &&
+                remoteChanges.hasChange(name)) {
+                assert_(localStorage.wasLocallyDeleted(name));
+                assert_(remoteChanges.wasDeleted(name));
+                //System.err.println("BC1");
+                return REBASE; // Deleted in both. Is remote.
+            }
+
+            // Was deleted in one but didn't exist in the other.
+            if (localStorage.hasLocalChange(name)) {
+                assert_(localStorage.wasLocallyDeleted(name));
+                assert_(!remoteChanges.hasChange(name));
+                //System.err.println("BC2");
+                return LOCALLY_MODIFIED; // Locally deleted
+            }
+            if (remoteChanges.hasChange(name)) {
+                assert_(remoteChanges.wasDeleted(name));
+                assert_(!localStorage.hasLocalChange(name));
+                assert_(localStorage.hasUnmodifiedPage(name));
+                //System.err.println("BC3");
+                return PARENT; // Deleted in remote
+            }
+            assert_(false);
+        }
+
+        if (local != null && remote != null) {
+            // Has changes in both, and deleted in neither.
+            if (local.equals(remote)) {
+                //System.err.println("BC4");
+                return REBASE;
+            } else {
+                //System.err.println("BC5");
+                return LOCALLY_MODIFIED;
+            }
+        }
+
+        if (local != null) {
+            // Has local, non-delete changes.
+            // Either it was deleted in the remote, or didn't exist in the remote.
+            //System.err.println("BC6");
+            return LOCALLY_MODIFIED;
+        }
+
+        if (remote != null) {
+            // Has remote, non-delete changes
+            // Either it was deleted in the local, or didn't exist in the local.
+            if (localStorage.hasLocalChange(name)) {
+                //System.err.println("BC7");
+                return LOCALLY_MODIFIED;
+            } else {
+                //System.err.println("BC8");
+                return PARENT;
+            }
+        }
+        assert_(false);
+        return -1; // unreachable.
+    }
+}
diff --git a/src/fniki/wiki/RemoteWikiTextChanges.java b/src/fniki/wiki/RemoteWikiTextChanges.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/RemoteWikiTextChanges.java
@@ -0,0 +1,65 @@
+/* A WikiTextChanges implementation to diff wikitext from wormarc archives.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library 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; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library 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.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+import wormarc.Archive;
+import wormarc.FileManifest;
+import wormarc.IOUtil;
+
+public class RemoteWikiTextChanges implements WikiTextChanges {
+    private final Archive mArchive;
+    private final FileManifest mManifest;
+    private final FileManifest.Changes mChanges;
+
+    public final static WikiTextChanges NO_REMOTE_CHANGES = new WikiTextChanges() {
+            public boolean hasChange(String name) throws IOException { return false; }
+            public boolean wasDeleted(String name) throws IOException { return false; }
+            public String getPage(String name) throws IOException { throw new IOException("No such change."); }
+        };
+
+    public RemoteWikiTextChanges(FileManifest parentManifest,
+                                 Archive remoteArchive,
+                                 FileManifest remoteManifest) {
+        mChanges = FileManifest.diff(parentManifest.getMap(), remoteManifest.getMap());
+        mArchive = remoteArchive;
+        mManifest = remoteManifest;
+    }
+
+    public boolean hasChange(String name) throws IOException {
+        return mChanges.mDeleted.contains(name) ||
+            mChanges.mAdded.contains(name) ||
+            mChanges.mModified.contains(name);
+    }
+
+    public boolean wasDeleted(String name) throws IOException {
+        return mChanges.mDeleted.contains(name);
+    }
+
+    public String getPage(String name) throws IOException {
+        return IOUtil.readUtf8StringAndClose(mManifest.getFile(mArchive, name));
+    }
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/SecondaryWikiText.java b/src/fniki/wiki/SecondaryWikiText.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/SecondaryWikiText.java
@@ -0,0 +1,34 @@
+/* Interface for read only changes to the primary wikitext.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library 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; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library 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.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface SecondaryWikiText {
+    boolean hasModifiedVersion(String name) throws IOException;
+    boolean wasDeleted() throws IOException;
+    String getPage(String name) throws IOException;
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/WikiApp.java b/src/fniki/wiki/WikiApp.java
--- a/src/fniki/wiki/WikiApp.java
+++ b/src/fniki/wiki/WikiApp.java
@@ -312,6 +312,7 @@ public class WikiApp implements ChildCon
     ////////////////////////////////////////////////////////////
     // Wiki context implementations.
     public WikiTextStorage getStorage() throws IOException { return mArchiveManager.getStorage(); }
+    public WikiTextChanges getRemoteChanges() throws IOException { return mArchiveManager.getRemoteChanges(); }
 
     public FreenetWikiTextParser.ParserDelegate getParserDelegate() { return mParserDelegate; }
 
@@ -329,6 +330,12 @@ public class WikiApp implements ChildCon
                 return defaultValue;
             }
             return mArchiveManager.getParentUri();
+        } else if (keyName.equals("secondary_uri")) {
+            if (mArchiveManager.getSecondaryUri() == null) {
+                // Can be null
+                return defaultValue;
+            }
+            return mArchiveManager.getSecondaryUri();
         } else if (keyName.equals("container_prefix")) {
             return containerPrefix();
         } else if (keyName.equals("form_password") && mFormPassword != null) {
diff --git a/src/fniki/wiki/WikiContext.java b/src/fniki/wiki/WikiContext.java
--- a/src/fniki/wiki/WikiContext.java
+++ b/src/fniki/wiki/WikiContext.java
@@ -33,6 +33,7 @@ public interface WikiContext extends Req
     String getTitle(); // hmmm
 
     WikiTextStorage getStorage() throws IOException;
+    WikiTextChanges getRemoteChanges() throws IOException;
     FreenetWikiTextParser.ParserDelegate getParserDelegate();
 
     // Client must deal with url escaping.
diff --git a/src/fniki/wiki/WikiParserDelegate.java b/src/fniki/wiki/WikiParserDelegate.java
--- a/src/fniki/wiki/WikiParserDelegate.java
+++ b/src/fniki/wiki/WikiParserDelegate.java
@@ -29,6 +29,7 @@ package fniki.wiki;
 import static ys.wikiparser.Utils.*; // DCI: clean up
 
 import java.io.IOException;
+import java.util.List;
 
 import wormarc.FileManifest;
 
@@ -49,11 +50,13 @@ public abstract class WikiParserDelegate
     protected abstract String makeFreenetLink(String uri);
 
     protected boolean processedLocalChangesMacro(StringBuilder sb, String text) {
-        try { // Should never happen while dumping and existing archive to html.
+        try {
             FileManifest.Changes changes =  mArchiveManager.getLocalChanges();
             if (changes.isUnmodified()) {
                 sb.append("<br>No local changes.<br>");
+                return true;
             }
+             // Should never be reached while dumping an existing archive to html.
             appendChangesHtml(changes, getContainerPrefix(), sb);
         } catch (IOException ioe) {
             sb.append("{ERROR PROCESSING LOCALCHANGES MACRO}");
@@ -61,6 +64,26 @@ public abstract class WikiParserDelegate
         return true;
     }
 
+    protected boolean processedRebasedChangesMacro(StringBuilder sb, String text) {
+        try {
+            List<RebaseStatus.Record> records = mArchiveManager.getRebaseStatus();
+            if (records.isEmpty()) {
+                sb.append("<br>No rebased changes.<br>");
+                return true;
+            }
+            // Should never be reached while dumping an existing archive to html.
+            sb.append(String.format("Displaying changes from version: %s to version: %s<br>",
+                                    getVersionHex(mArchiveManager.getParentUri()),
+                                    getVersionHex(mArchiveManager.getSecondaryUri())
+                                    ));
+
+            appendRebaseStatusHtml(records, getContainerPrefix(), sb);
+        } catch (IOException ioe) {
+            sb.append("{ERROR PROCESSING REBASEDCHANGES MACRO}");
+        }
+        return true;
+    }
+
     protected boolean processedTitleIndexMacro(StringBuilder sb, String text) {
         try {
             for (String name : mArchiveManager.getStorage().getNames()) {
@@ -77,6 +100,8 @@ public abstract class WikiParserDelegate
     public boolean processedMacro(StringBuilder sb, String text) {
         if (text.equals("LocalChanges")) {
             return processedLocalChangesMacro(sb, text);
+        } else if (text.equals("RebasedChanges")) {
+            return processedRebasedChangesMacro(sb, text);
         } else if (text.equals("TitleIndex")) {
             return processedTitleIndexMacro(sb, text);
         }
diff --git a/src/fniki/wiki/WikiTextChanges.java b/src/fniki/wiki/WikiTextChanges.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/WikiTextChanges.java
@@ -0,0 +1,33 @@
+/* Interface for read only changes to the primary wikitext.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library 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; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library 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.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.wiki;
+
+import java.io.IOException;
+
+public interface WikiTextChanges {
+    boolean hasChange(String name) throws IOException;
+    boolean wasDeleted(String name) throws IOException;
+    String getPage(String name) throws IOException;
+}
\ No newline at end of file
diff --git a/src/fniki/wiki/WikiTextStorage.java b/src/fniki/wiki/WikiTextStorage.java
--- a/src/fniki/wiki/WikiTextStorage.java
+++ b/src/fniki/wiki/WikiTextStorage.java
@@ -34,7 +34,12 @@ public interface WikiTextStorage {
     List<String> getNames() throws IOException;
     void deletePage(String name) throws IOException;
 
+    // DCI: local changes stuff corrupting original design
     boolean hasLocalChange(String name);
+    boolean wasLocallyDeleted(String name);
     // Reverting changes on a page that has no changes is allowed.
     void revertLocalChange(String name);
+
+    boolean hasUnmodifiedPage(String name) throws IOException;
+    String  getUnmodifiedPage(String name) throws IOException;
 }
\ No newline at end of file
diff --git a/src/fniki/wiki/child/GotoRedirect.java b/src/fniki/wiki/child/GotoRedirect.java
--- a/src/fniki/wiki/child/GotoRedirect.java
+++ b/src/fniki/wiki/child/GotoRedirect.java
@@ -42,8 +42,10 @@ public class GotoRedirect implements Chi
         }
         if (context.getQuery().get("uri") != null) {
             target = context.makeLink("/fniki/loadarchive?uri=" + context.getQuery().get("uri"));
+            if (context.getQuery().get("secondary") != null) {
+                target += "&secondary=" + context.getQuery().get("secondary");
+            }
         }
-
         context.raiseRedirect(target, "Redirecting...");
 
         return "unreachable code";
diff --git a/src/fniki/wiki/child/LoadingArchive.java b/src/fniki/wiki/child/LoadingArchive.java
--- a/src/fniki/wiki/child/LoadingArchive.java
+++ b/src/fniki/wiki/child/LoadingArchive.java
@@ -40,6 +40,7 @@ import fniki.wiki.WikiContext;
 
 public class LoadingArchive extends AsyncTaskContainer {
     private String mUri;
+    private boolean mSecondary = false;
     public LoadingArchive(ArchiveManager archiveManager) {
         super(archiveManager);
     }
@@ -51,6 +52,11 @@ public class LoadingArchive extends Asyn
                 System.err.println("handle -- set uri: " + mUri);
             }
 
+            if (context.getQuery().get("secondary") != null) {
+                mSecondary = context.getQuery().get("secondary").toLowerCase().equals("true");
+                System.err.println("handle -- set secondary true");
+            }
+
             if (context.getAction().equals("confirm")) {
                 if (mUri != null) {
                     startTask();
@@ -103,7 +109,8 @@ public class LoadingArchive extends Asyn
             }
             body.println("<h3>" + escapeHTML(title) + "</h3>");
             if (showUri) {
-                body.println(escapeHTML("Load Version: " + getVersionHex(mUri)));
+                String secondary = mSecondary ? "Secondary Archive " : "";
+                body.println(escapeHTML(String.format("Load %sVersion: %s", secondary, getVersionHex(mUri))));
                 body.println("<br>");
                 body.println(escapeHTML("from: " + mUri));
                 body.println("<p>Clicking Load will discard any unsubmitted local changes.</p>");
@@ -137,8 +144,10 @@ public class LoadingArchive extends Asyn
         }
 
         try {
-            out.println("Loading. Please be patient...");
-            mArchiveManager.load(mUri);
+            String msg = "";
+            if (mSecondary) { msg = " secondary archive."; }
+            out.println(String.format("Loading%s. Please be patient...", msg));
+            mArchiveManager.load(mUri, mSecondary);
             out.println("Loaded " + mUri);
             return true;
         } catch (IOException ioe) {
@@ -148,8 +157,8 @@ public class LoadingArchive extends Asyn
     }
 
     public void entered(WikiContext context) {
-        System.err.println("DCI: entered called, reset mUri");
         mUri = null;
+        mSecondary = false;
     }
 
 }
\ No newline at end of file
diff --git a/src/fniki/wiki/child/LoadingVersionList.java b/src/fniki/wiki/child/LoadingVersionList.java
--- a/src/fniki/wiki/child/LoadingVersionList.java
+++ b/src/fniki/wiki/child/LoadingVersionList.java
@@ -304,7 +304,12 @@ public class LoadingVersionList extends 
                 String versionLink = getShortVersionLink(mContainerPrefix, "/jfniki/loadarchive",
                                                          references.get(0).mKey); // All the same.
 
-                lines.add(versionLink);
+                String rebaseLink = getRebaseLink(mContainerPrefix, "/jfniki/loadarchive",
+                                                  references.get(0).mKey, "finished",
+                                                  "[rebase]", false);
+
+                lines.add(versionLink + " " + rebaseLink);
+
                 for (FMSUtil.BISSRecord reference : references) {
                     // DCI: Sort by date
                     lines.add(String.format("user: %s (%s, %s, %s, %s)",
diff --git a/src/fniki/wiki/child/SettingConfig.java b/src/fniki/wiki/child/SettingConfig.java
--- a/src/fniki/wiki/child/SettingConfig.java
+++ b/src/fniki/wiki/child/SettingConfig.java
@@ -168,7 +168,7 @@ public class SettingConfig implements Mo
             // Causes the routing logic in WikiApp.routeRequest to transition
             // out of this modal ui state.
             String redirectHref = makeHref(context.makeLink("/fniki/config"),
-                                           "finished", null, null, null);
+                                           "finished", null, null, null, null);
             context.raiseRedirect(redirectHref, "Redirecting...");
         }
     }
@@ -177,7 +177,7 @@ public class SettingConfig implements Mo
         handlePost(context);
 
         String href = makeHref(context.makeLink("/fniki/config"),
-                               null, null, null, null);
+                               null, null, null, null, null);
 
         // Html escape CDATA
         // http://www.w3.org/TR/html401/types.html#type-cdata
diff --git a/src/fniki/wiki/child/WikiContainer.java b/src/fniki/wiki/child/WikiContainer.java
--- a/src/fniki/wiki/child/WikiContainer.java
+++ b/src/fniki/wiki/child/WikiContainer.java
@@ -47,8 +47,10 @@ import wormarc.IOUtil;
 
 import fniki.wiki.FreenetWikiTextParser;
 import static fniki.wiki.HtmlUtils.*;
+import fniki.wiki.RebaseStatus;
 import static fniki.wiki.Validations.*;
 import fniki.wiki.WikiApp;
+import fniki.wiki.WikiTextChanges;
 import fniki.wiki.WikiContext;
 import fniki.wiki.WikiTextStorage;
 
@@ -81,14 +83,18 @@ public class WikiContainer implements Ch
 
             Query query = context.getQuery();
 
-            if (action.equals("view")) {
-                return handleView(context, title);
+            if (action.equals("view") ||       // editable
+                action.equals("viewparent") || // view parent version read only
+                action.equals("viewrebase")) { // view rebase version read only
+                return handleView(context, title, action);
             } else if (action.equals("edit")) {
                 return handleEdit(context, title);
             } else if (action.equals("delete")) {
                 return handleDelete(context, title);
             } else if (action.equals("revert")) {
                 return handleRevert(context, title);
+            } else if (action.equals("rebased")) {
+                return handleRebase(context, title);
             } else if (action.equals("save")) {
                 return handleSave(context, query);
             } else  {
@@ -101,8 +107,8 @@ public class WikiContainer implements Ch
         return "unreachable code";
     }
 
-    private String handleView(WikiContext context, String name) throws IOException {
-        return getPageHtml(context, name);
+    private String handleView(WikiContext context, String name, String action) throws IOException {
+        return getPageHtml(context, name, action);
     }
 
     private String handleEdit(WikiContext context, String name) throws IOException {
@@ -115,7 +121,7 @@ public class WikiContainer implements Ch
         }
 
         // LATER: do better.
-        return getPageHtml(context, name);
+        return getPageHtml(context, name, "view");
     }
 
     private String handleRevert(WikiContext context, String name) throws ChildContainerException, IOException {
@@ -124,6 +130,20 @@ public class WikiContainer implements Ch
         return "unreachable code";
     }
 
+    private String handleRebase(WikiContext context, String name) throws ChildContainerException, IOException {
+        if (context.getRemoteChanges().hasChange(name)) {
+            if (context.getRemoteChanges().wasDeleted(name)) {
+                // Delete from the working version.
+                context.getStorage().deletePage(name);
+            } else {
+                // Overwrite the working version with the rebase version.
+                context.getStorage().putPage(name, context.getRemoteChanges().getPage(name));
+            }
+        }
+        context.raiseRedirect(context.makeLink("/" + name), "Redirecting...");
+        return "unreachable code";
+    }
+
     private String handleSave(WikiContext context, Query form) throws ChildContainerException, IOException {
         // Name is included in the query data.
         System.err.println("handleSave -- ENTERED");
@@ -159,47 +179,82 @@ public class WikiContainer implements Ch
         return "Talk_" + name;
     }
 
-    private String getPageHtml(WikiContext context, String name) throws IOException {
-        StringBuilder buffer = new StringBuilder();
-        addHeader(context, name, getTalkPage(context, name), buffer);
-        if (context.getStorage().hasPage(name)) {
-            buffer.append(renderXHTML(context, context.getStorage().getPage(name)));
+    private void addHtmlForNonExistantPage(WikiContext context, String name,
+                                           StringBuilder buffer) throws IOException {
+
+        // Hmmmm... too branchy
+        if (name.equals(context.getString("default_page", "Front_Page"))) {
+            buffer.append(renderXHTML(context,
+                                      context.getString("default_wikitext",
+                                                        "Page doesn't exist in the wiki yet.")));
         } else {
-            // Hmmmm... too branchy
-            if (name.equals(context.getString("default_page", "Front_Page"))) {
-                buffer.append(renderXHTML(context,
-                                          context.getString("default_wikitext",
-                                                            "Page doesn't exist in the wiki yet.")));
+            if (name.startsWith("Talk_")) {
+                if (context.getStorage().hasPage("TalkPageDoesNotExist")) {
+                    // LATER: Revisit. Also, ExternalLink. Evil submissions can change this to something confusing.
+                    buffer.append(renderXHTML(context, context.getStorage().getPage("TalkPageDoesNotExist")));
+                } else {
+                    buffer.append("Discussion page doesn't exist in the wiki yet.");
+                }
             } else {
-                if (name.startsWith("Talk_")) {
-                    if (context.getStorage().hasPage("TalkPageDoesNotExist")) {
-                        // LATER: Revisit. Also, ExternalLink. Evil submissions can change this to something confusing.
-                        buffer.append(renderXHTML(context, context.getStorage().getPage("TalkPageDoesNotExist")));
-                    } else {
-                        buffer.append("Discussion page doesn't exist in the wiki yet.");
-                    }
+                if (context.getStorage().hasPage("PageDoesNotExist")) {
+                    // LATER: as above.
+                    buffer.append(renderXHTML(context, context.getStorage().getPage("PageDoesNotExist")));
                 } else {
-                    if (context.getStorage().hasPage("PageDoesNotExist")) {
-                        // LATER: as above.
-                        buffer.append(renderXHTML(context, context.getStorage().getPage("PageDoesNotExist")));
-                    } else {
-                        buffer.append("Page doesn't exist in the wiki yet.");
-                    }
+                    buffer.append("Page doesn't exist in the wiki yet.");
                 }
             }
         }
-        addFooter(context, name, buffer);
+    }
+
+    private static String getPageWikiText(WikiContext context, String name, String action) throws IOException {
+        if (action.equals("view")) {
+            return context.getStorage().getPage(name);
+        } else if (action.equals("viewparent")) {
+            return context.getStorage().getUnmodifiedPage(name);
+        } else if (action.equals("viewrebase")) {
+            if (context.getRemoteChanges().wasDeleted(name)) {
+                return "Page doesn't exist in the rebase version.";
+            } else {
+                return context.getRemoteChanges().getPage(name);
+            }
+        } else {
+            throw new RuntimeException("Unhandled action: " + action);
+        }
+    }
+
+    private static String titlePrefix(String action) {
+        if (action.equals("viewrebase")) {
+            return "{Rebase Version}:";
+        } else if (action.equals("viewParent")) {
+            return "{Parent Version}:";
+        }
+        return "";
+    }
+
+    private String getPageHtml(WikiContext context, String name, String action) throws IOException {
+        StringBuilder buffer = new StringBuilder();
+        String escapedName = escapeHTML(titlePrefix(action) + unescapedTitleFromName(name));
+        addHeader(context, escapedName, getTalkPage(context, name), buffer);
+
+        if ((action.equals("view") && context.getStorage().hasPage(name)) ||
+            (action.equals("viewparent") && context.getStorage().hasUnmodifiedPage(name)) ||
+            (action.equals("viewrebase") && context.getRemoteChanges().hasChange(name))) {
+            buffer.append(renderXHTML(context, getPageWikiText(context, name, action)));
+        } else {
+            addHtmlForNonExistantPage(context, name, buffer);
+        }
+        addFooter(context, name, !action.equals("view"), buffer);
         return buffer.toString();
     }
 
-    private void addHeader(WikiContext context, String name, String talkName,
+    private void addHeader(WikiContext context, String escapedName, String talkName,
                            StringBuilder buffer) throws IOException {
         buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
         buffer.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
                       "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
         buffer.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n");
         buffer.append("<head><title>\n");
-        buffer.append(escapeHTML(unescapedTitleFromName(name)));
+        buffer.append(escapedName);
         buffer.append("</title>\n");
         buffer.append("<style type=\"text/css\">\n");
         // CAREFUL: MUST audit .css files built into .jar to make sure they are safe.
@@ -209,12 +264,12 @@ public class WikiContainer implements Ch
         buffer.append("</head>\n");
         buffer.append("<body>\n");
         buffer.append("<h1 class=\"pagetitle\">\n");
-        buffer.append(escapeHTML(unescapedTitleFromName(name)));
+        buffer.append(escapedName);
         buffer.append("</h1>\n");
         if (talkName != null) {
             String talkClass = context.getStorage().hasPage(talkName) ? "talktitle" : "notalktitle";
             buffer.append(String.format("<h4 class=\"%s\">\n", talkClass));
-            String href = makeHref(context.makeLink("/" + talkName), null, talkName, null, null);
+            String href = makeHref(context.makeLink("/" + talkName), null, talkName, null, null, null);
             buffer.append(String.format("<a class=\"%s\" href=\"%s\">%s</a>",
                                         talkClass, href, escapeHTML("Discussion")));
             buffer.append("</h4>\n");
@@ -223,29 +278,104 @@ public class WikiContainer implements Ch
     }
 
     private String makeLocalLink(WikiContext context, String name, String action, String label) {
-        String href = makeHref(context.makeLink("/" + name), action, name, null, null);
+        String href = makeHref(context.makeLink("/" + name), action, name, null, null, null);
         return String.format("<a href=\"%s\">%s</a>", href, escapeHTML(label));
     }
 
-    private void addFooter(WikiContext context, String name, StringBuilder buffer) throws IOException {
-        buffer.append("<hr>\n");
-        buffer.append("Parent Version: ");
+    private final static String kindToString(int kind) {
+        switch(kind) {
+        case RebaseStatus.PARENT: return "parent";
+        case RebaseStatus.REBASE: return "rebase";
+        // LATER: also "locally added"
+        case RebaseStatus.LOCALLY_MODIFIED: return "locally modified";
+        }
+        return "???";
+    }
+
+    private void addStatusInfo(WikiContext context, String name, int kind,
+                                StringBuilder buffer) throws IOException {
+        buffer.append("<table border=\"1\">\n");
+        buffer.append(String.format("<tr><th align=\"left\">Page:</th><td>%s</td>" +
+                                    "<th align=\"left\">Origin:</th><td>%s</td></tr>\n",
+                                    name, kindToString(kind)));
+        buffer.append("<tr><th align=\"left\">Parent:</th><td>");
         // DCI: css class to make this smaller.
         String version = getVersionHex(context.getString("parent_uri", null));
         buffer.append(escapeHTML(version));
-        buffer.append("<hr>\n");
+        buffer.append("</td</tr>\n");
+        String secondaryUri = context.getString("secondary_uri", null);
+        if (secondaryUri != null) {
+            buffer.append("<tr><th align=\"left\">Rebase:</th><td>");
+            buffer.append(escapeHTML(getVersionHex(secondaryUri)));
+            buffer.append("</td></tr>\n");
+        }
+        // Same row
+        buffer.append(String.format("<tr><th align=\"left\">Wiki Name:</th><td>%s</td>",
+                                    context.getString("wikiname", "???")));
+        buffer.append(String.format("<th align=\"left\">Board:</th><td>%s</td></tr>",
+                                    context.getString("fms_group", "???"))); // LATER: fix fms_group
 
-        buffer.append(makeLocalLink(context, name, "edit", "Edit"));
-        buffer.append(" this page.<br>");
+        buffer.append("</table>\n");
+        buffer.append("<p/>\n");
+    }
 
-        buffer.append(makeLocalLink(context, name, "delete", "Delete"));
-        buffer.append(" this page without confirmation!<br>");
+    private void addDynamicLinks(WikiContext context, String name,
+                                 int kind, boolean readOnly,
+                                 StringBuilder buffer) throws IOException {
+        boolean hasLocalChanges = context.getStorage().hasLocalChange(name);
+        boolean hasRemoteChanges = context.getRemoteChanges().hasChange(name);
+        boolean showRevert = hasLocalChanges;
+        boolean showRevertToRemote = hasRemoteChanges;
 
-        if (context.getStorage().hasLocalChange(name)) {
-            buffer.append(makeLocalLink(context, name, "revert", "Revert"));
-            buffer.append(" local changes to this page without confirmation!<br>");
+        boolean existsInParent = context.getStorage().hasUnmodifiedPage(name);
+        boolean existsInRemote = context.getRemoteChanges().hasChange(name) && !context.getRemoteChanges().wasDeleted(name);
+
+        if (kind == RebaseStatus.PARENT) { showRevert = false; }
+        if (kind == RebaseStatus.REBASE) { showRevertToRemote = false; }
+
+        if (!readOnly) {
+            buffer.append(makeLocalLink(context, name, "edit", "Edit"));
+            buffer.append(" this page.<br>");
+            buffer.append(makeLocalLink(context, name, "delete", "Delete"));
+            buffer.append(" this page without confirmation!<br>");
+            if (showRevert) {
+                buffer.append(makeLocalLink(context, name, "revert", "Revert"));
+                buffer.append(" to the parent version of this page, without confirmation!");
+                if (!existsInParent) {
+                    buffer.append("<em>Will delete page</em>");
+                }
+                buffer.append("<br>\n");
+            }
+
+            if (showRevertToRemote) {
+                buffer.append(makeLocalLink(context, name, "rebased", "Replace"));
+                buffer.append(" this page with the rebase version,  without confirmation!");
+                if (!existsInRemote) {
+                    buffer.append("<em>Will delete page.</em>");
+                }
+                buffer.append("<br>\n");
+            }
         }
 
+        if (hasLocalChanges && existsInParent && !readOnly) {
+            buffer.append(makeLocalLink(context, name, "viewparent", "View Parent Version"));
+            buffer.append("<br/>\n");
+        }
+
+        if (hasRemoteChanges && existsInRemote && !readOnly) {
+            buffer.append(makeLocalLink(context, name, "viewrebase", "View Rebase Version"));
+            buffer.append("<br/>\n");
+        }
+        if (readOnly) {
+            buffer.append(makeLocalLink(context, name, "view", "Goto Editable Version"));
+            buffer.append("<br/>\n");
+        }
+
+    }
+
+    private void addStaticLinks(WikiContext context, String name,
+                                StringBuilder buffer) throws IOException {
+
         buffer.append(makeLocalLink(context, "fniki/submit", null, "Submit"));
         buffer.append(" local changes. <br>");
 
@@ -253,9 +383,7 @@ public class WikiContainer implements Ch
         buffer.append(" change history for this version. <br>");
 
         buffer.append(makeLocalLink(context, "fniki/getversions", "confirm", "Discover"));
-        buffer.append(String.format(" other recent version of this wiki (wikiname: [%s], FMS group: [%s])<br>",
-                                    escapeHTML(context.getString("wikiname", "NOT_SET")),
-                                    escapeHTML(context.getString("fms_group", "NOT_SET"))));
+        buffer.append(" other recent version of this wiki.<br/>");
 
         buffer.append(makeLocalLink(context, "fniki/config", "view", "View"));
         buffer.append(" configuration.<p/>\n");
@@ -263,18 +391,38 @@ public class WikiContainer implements Ch
                                        context.getString("default_page", "Front_Page")));
 
         buffer.append("<hr>\n");
-        buffer.append(makeLocalLink(context, "fniki/resettoempty", "view", "Create Wiki!"));
-        buffer.append(" (<em>careful:</em> This deletes all content and history without confirmation.)<p/>\n");
-
-        // LATER: Quick hack. Clean this up.
+        // LATER: Quick hack. Clean this up. Use CSS instead of table
         buffer.append(String.format("<p><form method=\"get\" action=\"%s\" accept-charset=\"UTF-8\">\n",
                                     context.makeLink("/fniki/loadarchive"), null, null, null, null));
         buffer.append("   <table><tr>\n");
         buffer.append("   <td><input type=submit value=\"Load Archive\"/></td>\n");
-        buffer.append("   <td><input style=\"font-size:60%;\" type=text name=\"uri\" size=\"140\" value=\"\"/></td>\n");
+        buffer.append("   <td><input style=\"font-size:60%;\" type=text name=\"uri\" size=\"140\" value=\"\"/></td></tr>\n");
+        // TRICKY: You only see the default check state change on initial load.
+        buffer.append("   <tr><td><input type=\"radio\" name=\"secondary\" value=\"false\" checked />primary</td>\n");
+        buffer.append("   <td><input type=\"radio\" name=\"secondary\" value=\"true\" />rebase</td>\n");
         buffer.append("   </tr></table>\n");
+        buffer.append("<hr>\n");
+        buffer.append(makeLocalLink(context, "fniki/resettoempty", "view", "Create Wiki!"));
+        buffer.append(" (<em>careful:</em> This deletes all content and history without confirmation.)<p/>\n");
+    }
+
+    private void addFooter(WikiContext context, String name, boolean readOnly, StringBuilder buffer) throws IOException {
+        buffer.append("<hr>\n");
+
+        int kind = RebaseStatus.pageChangeKind(context.getStorage(), context.getRemoteChanges(), name);
+        System.err.println("kind: " + kind);
+
+        if (!readOnly) {
+            addStatusInfo(context, name, kind, buffer);
+        }
+
+        addDynamicLinks(context, name, kind, readOnly, buffer);
+
+        if (!readOnly) {
+            addStaticLinks(context, name, buffer);
+        }
+
         buffer.append("</form>\n");
-
         buffer.append("</body></html>\n");
     }
 
@@ -292,12 +440,27 @@ public class WikiContainer implements Ch
 
         String escapedName = escapeHTML(unescapedTitleFromName(name));
         String href = makeHref(context.makeLink("/" +name),
-                               "save", null, null, null);
+                               "save", null, null, null, null);
         String wikiText = "Page doesn't exist in the wiki yet.";
         if (context.getStorage().hasPage(name)) {
             wikiText = context.getStorage().getPage(name);
         }
 
+        String parentWikiText = "none";
+        if (context.getStorage().hasUnmodifiedPage(name)) {
+            parentWikiText = context.getStorage().getUnmodifiedPage(name);
+        }
+        String rebaseWikiText = "none";
+        // System.err.println(String.format("hasChange: %s wasDeleted: %s len:%d",
+        //                                  "" + context.getRemoteChanges().hasChange(name),
+        //                                  "" + context.getRemoteChanges().wasDeleted(name),
+        //                                  context.getRemoteChanges().getPage(name).length()));
+
+        if (context.getRemoteChanges().hasChange(name) &&
+            !context.getRemoteChanges().wasDeleted(name)) {
+            rebaseWikiText = context.getRemoteChanges().getPage(name);
+        }
+
         return String.format(template,
                              escapedName,
                              escapedName,
@@ -306,7 +469,9 @@ public class WikiContainer implements Ch
                              escapeHTML(wikiText),
                              // IMPORTANT: Required by Freenet Plugin.
                              // Doesn't need escaping.
-                             context.getString("form_password", "FORM_PASSWORD_NOT_SET"));
+                             context.getString("form_password", "FORM_PASSWORD_NOT_SET"),
+                             escapeHTML(parentWikiText),
+                             escapeHTML(rebaseWikiText));
     }
 
     public String renderXHTML(WikiContext context, String wikiText) {
diff --git a/templates/edit_form.html b/templates/edit_form.html
--- a/templates/edit_form.html
+++ b/templates/edit_form.html
@@ -22,6 +22,11 @@
   <br/>
 </form>
 <hr/>
+<h1>Parent Wikitext</h1>
+<textarea wrap="virtual" name="pwt" rows="17" cols="120" readonly>%s</textarea>
+<hr/>
+<h1>Rebase WikiText</h1>
+<textarea wrap="virtual" name="rwt" rows="17" cols="120" readonly>%s</textarea>
 </body>
 </html>