Wiki Revision History Feature

The script on this page, wikihist.cgi, provides a revision history of pages on a Wikit-based wiki. It displays a list of all the revisions of a page, and can show the differences, in WikiDiff format, between the current page and a specific revision of that page, or between a specific revision of a page and its previous revision.

The script relies on four other files. The files are:

The first three files are sourced into the main script, while the CSS file is pointed to by the generated HTML. To avoid cluttering your CGI directory, it would make sense to wrap these up into a Starkit.

The script assumes that the Wiki pages have been checked into CVS, and that a directory where the pages can be checked out to exists. This script could probably quite easily be modified to use the tclhist archive, but that is left as an exercise to the reader! OK, the writer has performed the exercise -- see Wiki Revision History Feature via tclhist.

To deploy this script at another site, you should make the following changes to the script:

  • replace the initial "exec /home/wiki..." line with the path to your Tcl interpreter
  • replace all instances of "/usr/local/bin/cvs" with the path to your version of CVS. I didn't just use "cvs" because /usr/local/bin wasn't in the PATH of our webserver.
  • replace any references to "http://saskatoon/ ..." with appropriate references for your site
  • make sure any references to wikihist.css point to your own CSS file
  • replace the line "cd /home/wiki/wikit/wikipages-co/wiki/daily" to change to a directory where you can check out the Wiki pages from CVS
  • and I think that's it.

Once you've got it installed, point your browser to http://where.ever.you.put.it/wikihist.cgi to get usage instructions. Basically, you can:

  • get a revision history of a specified page. For example, "wikihist.cgi/12" gets you the history of page 12.
  • retrieve an old revision of a specified page. For example, "wikihist.cgi/12.5" gets you revision 1.5 of page 12.
  • compare the latest revision with a specified revision. For example, "wikihist.cgi/12-5" compares the latest revision of page 12 with revision 1.5, and displays it in WikiDiff format.
  • compare two specified revisions. For example, "wikihist.cgi/12-5-2" compares revision 1.5 of page 12 with revision 1.2, and displays it in WikiDiff format.

If you get errors, you can get your browser to show what they are by enabling the debugging at the end of the script (change "if 0" to "if 1").

This can be integrated into the regular web interface to the Wiki by a short change to web.tcl in the Wikit distribution. After the line that reads:

 set updated "Updated [cgi_font size=-1 $date]"

add the following line:

 append updated " [nbspace]-[nbspace] <A HREF=\"http://saskatoon/wikit/wikihist.cgi/$N\">Revision History</A>"

 #!/bin/sh
 #\
 exec /home/wiki/wikit/public_html/wiki/tclkit-solaris-sparc $0

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

 # 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}} {
    if {[string equal $RevNumber "Latest"]} {
       catch {exec /usr/local/bin/cvs update -p $PageNumber 2> /dev/null}\
          PageContents
    } else {
       catch {exec /usr/local/bin/cvs update -r1.$RevNumber -p $PageNumber\
                 2> /dev/null} PageContents
    }

    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 the CVS log 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 last, the first being
 # the minor revision number, and the second being the time that
 # this revision was checked into CVS, given as the number of
 # seconds since the Tcl epoch.
 #
 # Only major revision # 1 is examined.

 proc getRevisionList {PageNumber} {
    set RevisionList {}

    # Note: for cvs to generate a log, it needs write permission to the
    # CVS directory.  If the Web Server doesn't have this permission,
    # this script will fail.
    catch {exec /usr/local/bin/cvs log -r1 -N $PageNumber 2> /dev/null} CvsOutput

    foreach CvsLine [split $CvsOutput \n] {

       # Look for a line indicating the revision number.
       # It will start with "revision 1.", followed by the minor
       # revision number.
       if {[regexp {^revision 1.(\d+)} $CvsLine - RevNumber]} {
          set a $RevNumber
       }

       # Also, look for a line indicating the date.
       # This should follow the revision number in the CVS output,
       # so we will associate this date with the last revision number we saw.
       # The date line starts with "date", and the date is in the format
       # Year/Month/Day Hour:Minute:Second.  Tcl is unable to interpret
       # this date format, so we need to convert it into a format Tcl
       # can understand before converting it to seconds.
       if {[regexp {^date} $CvsLine]} {
          set DateString [string range $CvsLine 6 24]
          regexp {(\d+)/(\d+)/(\d+) (\d\d:\d\d:\d\d)}\
             $DateString junk Year Mon Day Time
          set RevTime [clock scan "$Mon/$Day/$Year $Time" -gmt 1]
          lappend RevisionList $RevNumber $RevTime
       }

    }
    return $RevisionList
 }


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

 proc genRevisionHistory {PageNumber} {

    # 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="/wikit/wikihist.css" rel="stylesheet">}
    puts "</head>"
    puts ""

    puts "<body bgcolor=\"\#ffffff\">"
    puts "  <h2>"
    puts "    <a href=\"http://saskatoon/wikit/wiki.cgi/$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 -format "%b %d, %Y"]

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

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

# (missing procs go here) # LV Okay, someone needs to tell me if this is the missing code

 <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 {
    cd /home/wiki/wikit/wikipages-co/wiki/daily

    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 { [file exists $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 { [file exists $page] } {
          if { ( $sep1 == "-" ) && ( $sep2 == "-" ) } {
             renderPageDiff $page $rev1 $rev2
          } else {
             set ShowInstructions true
          }
       } else {
          set NoSuchFile true
       }
    } elseif { [regexp {^(\d+)$} $PathInfo - page] } {
       if { [file exists $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 PMC Saskatoon Wiki

     Examples:

         See the revision history of page 12:
             http://saskatoon/wikit/wiki/wikihist.cgi/12

         Retrieve version 5 of page 12:
             http://saskatoon/wikit/wiki/wikihist.cgi/12.5

         Compare version 5 of page 12 with latest version:
             http://saskatoon/wikit/wiki/wikihist.cgi/12-5

         Compare version 5 of page 12 with version 2:
             http://saskatoon/wikit/wiki/wikihist.cgi/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
 }

KPV I haven't gone and extracted the utils.tcl, et al files yet but how does this differ from Wiki History Diff?

sm If I understand Wiki History Diff correctly, it's a separate application that a user needs to run. Our users wanted something integrated right into the Wiki, so that they could see the history right within their web browser. Other than that, I don't think this script offers any additional functionality over Wiki History Diff.


LV: "Changes to a Tcler's wiki page become available after an offline process occurs once a day. So a page which is first updated today won't have any revisions available until tomorrow - regardless of how many changes were made today."

HJG I still see this message "There is no revision history for Wiki page 15176" today (2005-12-25), but the page has been created two days ago (2005-12-23). So I wonder at which points in time these revisions are created ?

Today (2005-12-26) a whole bunch of revision-entries has appeared, with dates from 2005-12-23 and 2005-12-24 - strange...


Category Wikit