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>