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>