Wiki Revision History Feature via tclhist

The page Wiki Revision History Feature provides a Tcl CGI script that adds a Wiki Revision History feature to Wikit. However, that script assumes the existence of a CVS repository containing the history of the pages of the Wiki. The script provided on this page, revhist.cgi, makes use of The Tcler's Wiki tclhist feature.

The script relies on four other files. See Wiki Revision History Feature for more details.

To deploy the script, make the following changes:

  • replace the initial "exec /usr/local/bin/tclkit $0" line with the path to your Tcl interpreter
  • replace the "set ScriptURL..." line to point to the URL of this script.
  • replace the "set StyleURL..." line to point to the CSS file.

Then let 'er rip!


 #!/bin/sh
 #\
 exec /usr/local/bin/tclkit $0

 source utils.tcl
 source format.tcl
 source worddiff.tcl

 package require http

 # ScriptURL defines the URL of this script
 set ScriptURL http://mini.net/revhist.cgi

 # TclhistURL defines the URL of the tclhist page
 set TclhistURL http://mini.net/tclhist

 # WikiURL defines the URL of the Wiki whose history is being displayed
 set WikiURL https://wiki.tcl-lang.org

 # StyleURL defines the URL of the stylesheet used to display this page
 set StyleURL /wikihist.css

 # WikiName is the name of the Wiki to be displayed
 set WikiName "Tcler's Wiki"

 # This proc determines whether the specified page exists.
 # It does this by looking for the revision history from Tclhist.
 # If the returned page doesn't match the expected format, there is
 # no revision history for this page.
 # Note that this proc uses the revision history rather than the
 # actual page -- this is done because I'm guessing that the
 # revision history is a less taxing operation on the server.

 proc pageExists {PageNumber} {
    global TclhistURL

    set URL $TclhistURL/$PageNumber*
    set Token [::http::geturl $URL]
    set PageContents [::http::data $Token]
    ::http::cleanup $Token

    # The page contents returned should start with a revision number and date.
    # The call to regexp returns an indication of whether it does.
    regexp {^\d+ \d+} $PageContents -
 }

 # This proc returns the contents of the specified revision number
 # of the specified Wiki page.  If no revision number is specified,
 # the latest revision is obtained.
 # As coded, this obtains it from the CVS repository.  If desired,
 # this could be recoded to get it from a tclhist archive.

 proc getPageContents {PageNumber {RevNumber Latest}} {
    global TclhistURL

    if {[string equal $RevNumber "Latest"]} {
       set URL $TclhistURL/$PageNumber
    } else {
       set URL $TclhistURL/$PageNumber.$RevNumber
    }

    set Token [::http::geturl $URL]
    set PageContents [::http::data $Token]
    ::http::cleanup $Token

    return $PageContents
 }


 # This proc determines the title of the specified Wiki page.

 proc getPageTitle {PageNumber} {

    # Grab a copy of the latest version of the page from CVS.
    # The title is found on the first line of this page.
    set PageContents [getPageContents $PageNumber]
    set TitleLine [lindex [split $PageContents \n] 0]

    # Now, extract the title from the title line
    regexp {^Title:\s*(.*)$} $TitleLine junk Title

    return $Title
 }


 # This procedure splits long lines into lines no longer than 80 characters

 proc splitlongline { line } {
    set maxlen 80

    set thisline ""
    set lines [list]

    set opentag 0
    foreach word [split $line " "] {
       if { $opentag || [string first "<" $word] > -1 } {
          set opentag 1
          append thisline " $word"
          if { [string last ">" $word] > [string last "<" $word] } {
             set opentag 0
          }
       } else {

          if { [string equal $thisline ""] } {
             append thisline $word
          } else {
             if { [string length "$thisline $word"] > $maxlen } {
                if { [llength $lines] > 0 } {
                   set thisline "<span class=\"continue\">... </span>$thisline"
                }
                lappend lines $thisline
                set thisline $word
             } else {
                append thisline " $word"
             }
          }
       }
    }

    if { [llength $lines] > 0 } {
       set thisline "<span class=\"continue\">... </span>$thisline"
    }
    lappend lines $thisline

    return [join $lines \n]
 }


 # This proc substitutes any special characters with their HTML equivalents.

 proc quoteHtml {s} {
    string map { & &amp; < &lt; > &gt; } $s
 }


 # For the given Wiki page number, this proc examines Tclhist to determine
 # the revision number and check-in time of all revisions.  The list
 # returned is ordered from latest revision to earliest revision.
 # Every revision results in two items in the list, the first being
 # the minor revision number, and the second being the time that
 # this revision was checked in, given as the number of
 # seconds since the Tcl epoch.

 proc getRevisionList {PageNumber} {
    global TclhistURL

    set RevisionList {}

    set URL $TclhistURL/$PageNumber*
    set Token [::http::geturl $URL]
    set AllRevs [::http::data $Token]
    ::http::cleanup $Token

    foreach RevisionEntry [split $AllRevs \n] {

       # Look for all lines with revision numbers. They start with the
       # revision number and the date in seconds since the Tcl epoch.

       if {[regexp {^(\d+) (\d+)} $RevisionEntry - RevNumber RevTime]} {
          lappend RevisionList $RevNumber $RevTime
       }

    }
    return $RevisionList
 }


 # This procedure generates the revision history HTML page
 # for the specified Wiki page number

 proc genRevisionHistory {PageNumber} {
    global WikiURL
    global ScriptURL
    global StyleURL

    # Generate the content type for the page (HTML)
    puts "Content-type: text/html"
    puts "Pragma: no-cache"
    puts ""

    # Determine the title of this page
    set PageTitle [quoteHtml [getPageTitle $PageNumber]]

    # Now, generate the page header.

    puts "<html>"
    puts ""
    puts "<head>"
    puts "  <title>Revision History of $PageTitle</title>"
    puts "  <meta content=\"no-cache\" http-equiv=\"Pragma\">"
    puts "  <meta content=\"Mon, 04 Dec 1999 21:29:02 GMT\" http-equiv=\"Expire\">"
    puts "  <link type=\"text/css\" href=\"$StyleURL\" rel=\"stylesheet\">"
    puts "</head>"
    puts ""

    puts "<body bgcolor=\"\#ffffff\">"
    puts "  <h2>"
    puts "    <a href=\"$WikiURL/$PageNumber\">$PageTitle</a>"
    puts "  </h2>"
    puts "  <b>Revision History</b>"
    puts ""

    puts "  <p>"
    puts "    Legend: (current) = difference with current version,"
    puts "    (last) = difference with preceding version,"
    puts "    <em>date</em> = that day's version"
    puts "  </p>"
    puts ""

    # Now, obtain a list of the revisions and the time that they were
    # checked in.
    set RevisionList [getRevisionList $PageNumber]

    # Now that we've got the list of revisions, we need to generate the
    # revision information to display on the page.
    puts "  <ul>"
    set FirstEntry true
    foreach {RevNumber RevDate} $RevisionList {
       set DateString [clock format $RevDate -gmt 1]

       puts "    <li>"
       puts "      Version $RevNumber:"
       if { !$FirstEntry } {
          puts "        (<a href=\"$ScriptURL/$PageNumber-$RevNumber\">current</a>)"
       } else {
          set FirstEntry false
       }
       if { $RevNumber != 1 } {
          puts "        (<a href=\"$ScriptURL/$PageNumber-$RevNumber-[expr $RevNumber - 1]\">last</a>)"
       }
       puts "        ..."
       puts "        <a href=\"$ScriptURL/$PageNumber.$RevNumber\">$DateString</a>"
    }
    puts "  </ul>"
    puts ""

    puts "</body>"
    puts ""
    puts "</html>"
 }


 # This proc renders the specified revision of the specified page number
  proc renderPageRevision {PageNumber RevNumber} {
    global StyleURL
    global ScriptURL
    global WikiURL

    set PageContents [getPageContents $PageNumber $RevNumber]

    # Extract the page title and check-in date,
    # then strip off the title, date, and site
    # (first 4 lines of the contents).
    set TitleLine [lindex [split $PageContents \n] 0]
    regexp {^Title:\s*(.*)$} $TitleLine junk PageTitle
    set PageTitle [quoteHtml $PageTitle]

    set PageContents [join [lrange [split $PageContents \n] 3 end] \n]

    # Generate the content type for the page (HTML)
    puts "Content-type: text/html"
    puts "Pragma: no-cache"
    puts ""

    # Now, generate the page header.

    puts "<html>"
    puts ""
    puts "<head>"
    puts "  <title>Revision $RevNumber of $PageTitle</title>"
    puts "  <meta content=\"no-cache\" http-equiv=\"Pragma\">"
    puts "  <meta content=\"Mon, 04 Dec 1999 21:29:02 GMT\" http-equiv=\"Expire\">"
    puts "  <link type=\"text/css\" href=\"$StyleURL\" rel=\"stylesheet\">"
    puts "</head>"
    puts ""

    puts "<body bgcolor=\"\#ffffff\">"
    puts "  <h2>"
    puts "    <small>Revision $RevNumber of</small> <a href=\"$ScriptURL/$PageNumber\">$PageTitle </a>"
    puts "  </h2>"
    puts ""

    set HTMLContents\
       [lindex [::Wikit::Format::StreamToHTML\
                   [::Wikit::Format::TextToStream $PageContents]\
                   "$WikiURL/"] 0]
    puts $HTMLContents
    puts ""

    puts "</body>"
    puts ""
    puts "</html>"
 }


 # This proc renders the page showing the difference between two revisions
 # of a page.  If only one revision number is given, the difference between
 # the current page and the specified page is shown.

 proc renderPageDiff {PageNumber RevNumber {OldRevNumber "Unspecified"}} {
    global StyleURL
    global ScriptURL

    # If the old revision number isn't specified, we do a diff between
    # the current contents of the page and the specified revision.
    if { [string equal $OldRevNumber "Unspecified"] } {
       set PageContents [getPageContents $PageNumber]
       set OldContents [getPageContents $PageNumber $RevNumber]
    } else {
       set PageContents [getPageContents $PageNumber $RevNumber]
       set OldContents [getPageContents $PageNumber $OldRevNumber]
    }

    # Determine the title of the page
    set TitleLine [lindex [split $PageContents \n] 0]
    regexp {^Title:\s*(.*)$} $TitleLine junk PageTitle
    set PageTitle [quoteHtml $PageTitle]

    # Strip the page header information from the pages
    set PageContents [join [lrange [split $PageContents \n] 3 end] \n]
    set OldContents [join [lrange [split $OldContents \n] 3 end] \n]

    # Generate the HTML description of the differences
    set LongHtmlChanges [doHtmlDiff context $OldContents $PageContents]

    # Split long lines
    set HtmlChanges ""
    foreach line [split $LongHtmlChanges \n] {
       append HtmlChanges "[splitlongline $line]\n"
    }

    # Generate the content type for the page (HTML)
    puts "Content-type: text/html"
    puts "Pragma: no-cache"
    puts ""

    # Now, generate the page header.

    puts "<html>"
    puts ""
    puts "<head>"
    if { [string equal $OldRevNumber "Unspecified"] } {
       puts "  <title>Differences between latest version and version $RevNumber of $PageTitle</title>"
    } else {
       puts "  <title>Differences between version $RevNumber and version $OldRevNumber of $PageTitle</title>"
    }
    puts "  <meta content=\"no-cache\" http-equiv=\"Pragma\">"
    puts "  <meta content=\"Mon, 04 Dec 1999 21:29:02 GMT\" http-equiv=\"Expire\">"
    puts "  <link type=\"text/css\" href=\"$StyleURL\" rel=\"stylesheet\">"
    puts "</head>"
    puts ""

    puts "<body bgcolor=\"\#ffffff\">"
    puts "  <h2>"
    if { [string equal $OldRevNumber "Unspecified"] } {
       puts "    <small>Differences between latest version and version $RevNumber of</small> <a href=\"$ScriptURL/$PageNumber\">$PageTitle </a>"
    } else {
       puts "    <small>Differences between version $RevNumber and version $OldRevNumber of</small> <a href=\"$ScriptURL/$PageNumber\">$PageTitle </a>"
    }
    puts "  </h2>"

    puts {
   <p>
     Legend:
     <br>

   </p>

   <blockquote>
 <pre><span class="context">gray text: context matter</span>
 <span class="old">red text: old text, or that which has been removed.</span>
 <span class="new">green text: new text, interesting new knowledge.</span>
 <span class="newpage">yellow text: new text, brand new page, interestly fresh knowledge.</span></pre>
   </blockquote>
    }

    puts "  <p>"
    puts "    Differences:"
    puts "  </p>"
    puts ""

    # Display the differences
    if { [string length $HtmlChanges] == 0 } {
       puts "  <p>"
       puts "    There were no differences between the two versions of the page."
       puts "  </p>"
       puts ""
    } else {
       puts "<blockquote><pre class=\"diff\">$HtmlChanges</pre></blockquote>"
       puts ""
    }

    puts "</body>"
    puts ""
    puts "</html>"
 }

 catch {
    if { [info exists env(PATH_INFO)] } {
       set PathInfo [file tail $env(PATH_INFO)]
    } else {
       set PathInfo {}
    }

    set ShowInstructions false
    set NoSuchFile false

    if { [regexp {^(\d+)(\D)(\d*)$} $PathInfo - page sep rev] } {
       if { [pageExists $page] } {
          switch -- $sep {
             . {
                renderPageRevision $page $rev
             }
             - {
                renderPageDiff $page $rev
             }
             default {
                set ShowInstructions true
             }
          }
       } else {
          set NoSuchFile true
       }
    } elseif { [regexp {^(\d+)(\D)(\d*)(\D)(\d*)$} $PathInfo\
                   - page sep1 rev1 sep2 rev2] } {
       if { [pageExists $page] } {
          if { ( $sep1 == "-" ) && ( $sep2 == "-" ) } {
             renderPageDiff $page $rev1 $rev2
          } else {
             set ShowInstructions true
          }
       } else {
          set NoSuchFile true
       }
    } elseif { [regexp {^(\d+)$} $PathInfo - page] } {
       if { [pageExists $page] } {
          genRevisionHistory $page
       } else {
          set NoSuchFile true
       }
    } else {
       set ShowInstructions true
    }

    if { $ShowInstructions } {
       puts "Content-type: text/plain"
       puts "Pragma: no-cache"

       puts "
     This is the historical archive of the $WikiName

     Examples:

         See the revision history of page 12:
             $ScriptURL/12

         Retrieve version 5 of page 12:
             $ScriptURL/12.5

         Compare version 5 of page 12 with latest version:
             $ScriptURL/12-5

         Compare version 5 of page 12 with version 2:
             $ScriptURL/12-5-2
       "
    } elseif { $NoSuchFile } {
       puts "Content-type: text/plain"
       puts "Pragma: no-cache"
       puts ""

       puts "There is no revision history for Wiki page $page."
    }
 } err

 if 0 {
   puts ####################
   puts $err
   puts $errorInfo
 }

12may04 jcw - Shane, as always, your contributions are a delight to try out. I've installed it on this site as an experiment for now. Need to look into server load (and making sure bots don't clog it), but I can see no issues with it. Thanks also to Pascal for sharing the diff code.

Try http://mini.net/tclrevs/11426 for example. Note that the latest version is there but revisions before it may lag by up to 24 hours, since CVS updates are merged only once a day. The http://mini.net/tclhist/ mechanism also continues to be available.


LV If some day someone else, like me, wanders by using Lynx or some other non-graphical web browser, perhaps we could brain storm on how we could modify this code so that it was not color-dependant for reporting differences. Right now, non-graphical web browsers, as well as color blind users, are unable to see the differences.


LV 2007 August 16 The above code and discussions are relative to version 1 of the wikit. I suspect that it is not relevant to the current incarnation of the wikit, which I think was reimplemented.