Version 2 of Wiki Revision History Feature

Updated 2004-02-20 16:51:29

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!

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>"
 }


 # This proc renders the specified revision of the specified page number

 proc renderPageRevision {PageNumber RevNumber} {

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

    puts "<body bgcolor=\"\#ffffff\">"
    puts "  <h2>"
    puts "    <small>Revision $RevNumber of</small> <a href=\"http://saskatoon/wikit/wikihist.cgi/$PageNumber\">$PageTitle </a>"
    puts "  </h2>"
    puts ""

    set HTMLContents\
       [lindex [::Wikit::Format::StreamToHTML\
                   [::Wikit::Format::TextToStream $PageContents]\
                   "http://saskatoon/wikit/wiki.cgi/"] 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"}} {

    # 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="/wikit/wikihist.css" 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=\"http://saskatoon/wikit/wikihist.cgi/$PageNumber\">$PageTitle </a>"
    } else {
       puts "    <small>Differences between version $RevNumber and version $OldRevNumber of</small> <a href=\"http://saskatoon/wikit/wikihist.cgi/$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 {
    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
 }