Check-in [df91d389d5]
Not logged in
Overview

SHA1 Hash:df91d389d58f7e9f068e7866a568fae14a6b4282
Date: 2007-09-04 05:36:56
User: aku
Comment:First semi-complete app for import from CVS. Trunk only, wholesale only.
Timelines: ancestors | descendants | both | trunk
Other Links: files | ZIP archive | manifest

Tags And Properties
Changes
[hide diffs]

Modified ci_cvs.txt from [13f4f929b2] to [78baac6911].

@@ -166,5 +166,27 @@
 
 
 This algorithm has to be refined to also take Attic/ files into
 account.
 
+-------------------------------------------------------------------------
+
+Two archive files mapping to the same user file. How are they
+interleaved ?
+
+(a)	sqlite/src/os_unix.h,v
+(b)	sqlite/src/Attic/os_unix.h,v
+
+Problem: Max version of (a) is 1.9
+	 Max version of (b) is 1.11
+	 cvs co 1.10 -> no longer in the repository.
+
+This seems to indicate that the non-Attic file is relevant.
+
+--------------------------------------------------------------------------
+
+tcllib - more problems - tklib/pie.tcl,v -
+
+invalid change text in
+/home/aku/Projects/Tcl/Fossil/Devel/Examples/cvs-tcllib/tklib/modules/tkpiechart/pie.tcl,v
+
+Possibly braces ?

Modified tools/import-cvs.tcl from [8e70daebc0] to [bb9275318e].

@@ -1,370 +1,185 @@
 #!/bin/sh
 # -*- tcl -*- \
 exec tclsh "$0" ${1+"$@"}
 
 # -----------------------------------------------------------------------------
+
+# Import the trunk of a CVS repository wholesale into a fossil repository.
+
+# Limitations implicitly mentioned:
+# - No incremental import.
+# - No import of branches.
+
+# WIBNI features (beyond eliminating the limitations):
+# - Restrict import to specific directory subtrees (SF projects use
+#   one repository for several independent modules. Examples: tcllib
+#   -> tcllib, tklib, tclapps, etc.). The restriction would allow import
+#   of only a specific module.
+# - Related to the previous, strip elements from the base path to keep
+#   it short.
+# - Export to CVS, trunk, possibly branches. I.e. extend the system to be
+#   a full bridge. Either Fossil or CVS could be the master repository.
+
+# HACKS. I.e. I do not know if the 'fixes' I use are the correct way
+#        of handling the encountered situations.
+#
+# - File F has archives F,v and Attic/F,v. Currently I will ignore the
+#   file in the Attic.
+#   Examples: sqlite/os_unix.h
+#
+# - A specific revision of a file F cannot be checked out (reported
+#   error is 'invalid change text'). This indicates a corrupt RCS
+#   file, one or more delta are bad. We report but ignore the problem
+#   in a best-effort attempt at getting as much history as possible.
+#   Examples: tcllib/tklib/modules/tkpiechart/pie.tcl
+
+# -----------------------------------------------------------------------------
 # Make private packages accessible.
 
 lappend auto_path [file join [file dirname [info script]] lib]
-package require rcsparser
-package require fileutil
+
+# -----------------------------------------------------------------------------
+# Requirements
+
+package Tcl 8.4
+package require cvs    ; # Frontend, reading from source repository
+package require fossil ; # Backend,  writing to destination repository.
 
 # -----------------------------------------------------------------------------
-# Repository management (CVS)
-
-namespace eval ::cvs {
-    variable base   ; set       base   {} ; # Repository toplevel directory.
-    variable npaths ; array set npaths {} ; # path -> actual path mapping.
-    variable rpaths ; array set rpaths {} ; # path -> rcs file mapping.
-    variable cmsg   ; array set cmsg   {} ; # Cache of commit messages.
-}
-
-proc ::cvs::hextime {hex} {
-    set t 0
-    foreach d [string map {
-	a 10 b 11 c 12 d 13 e 14 f 15
-	A 10 B 11 C 12 D 13 E 14 F 15
-    } [split $hex {}]] {
-	set t [expr {($t << 4) + $d}];#horner
+
+proc main {} {
+    global argv tot nto cvs fossil ntrunk
+
+    commandline
+
+    fossil::feedback Write ; # Setup progress feedback from the libraries
+    cvs::feedback    Write
+
+    cvs::at       $cvs  ; # Define location of CVS repository
+    cvs::scan           ; # Gather revision data from the archives
+    cvs::csets          ; # Group changes into sets
+    cvs::rtree          ; # Build revision tree (trunk only right now).
+
+    set tot 0.0
+    set nto 0
+
+    Write info {Importing ...}
+    Write info {    Setting up cvs workspace and temporary fossil repository}
+
+    cvs::workspace ; # cd's to workspace
+    fossil::new    ; # Uses cwd as workspace to connect to.
+
+    set ntrunk [cvs::ntrunk]
+    cvs::foreach_cset cset [cvs::root] {
+	import $cset
     }
-    return $t
-}
-
-proc ::cvs::at {path} {
-    variable base $path
+    cvs::wsclear
+
+    Write info "    ========= [string repeat = 61]"
+    Write info "    Imported $nto [expr {($nto == 1) ? "changeset" : "changesets"}]"
+    Write info "    Within [format %.2f $tot] seconds (avg [format %.2f [expr {$tot/$nto}]] seconds/changeset)"
+
+    Write info {    Moving to final destination}
+
+    fossil::destination $fossil
+
+    Write info Ok.
     return
 }
 
-proc ::cvs::cmsg {path rev} {
-    variable cmsg
-    set key [list $path $rev]
-
-    if {![info exists cmsg($key)]} {
-	set rcs [cvs::rcsfile $path]
-
-	#puts stderr "scan $path => $rcs"
-
-	array set p [::rcsparser::process $rcs]
-
-	foreach {r msg} $p(commit) {
-	    set cmsg([list $path $r]) $msg
+
+# -----------------------------------------------------------------------------
+
+proc commandline {} {
+    global argv cvs fossil nosign log
+
+    set nosign 0
+    while {[string match "-*" [set opt [lindex $argv 0]]]} {
+	if {$opt eq "--nosign"} {
+	    set nosign 1
+	    set argv [lrange $argv 1 end]
+	    continue
 	}
-
-	if {![info exists cmsg($key)]} {
-	    return -code error "Bogus revision $rev of file $path"
-	}
+	usage
+    }
+    if {[llength $argv] != 2} usage
+    foreach {cvs fossil} $argv break
+
+    if {
+	![file exists      $cvs] ||
+	![file readable    $cvs] ||
+	![file isdirectory $cvs]
+    } {
+	usage "CVS directory missing, not readable, or not a directory."
+    } elseif {[file exists $fossil]} {
+	usage "Fossil destination repository exists already."
     }
 
-    return $cmsg($key)
+    set log [open ${fossil}.log w]
+    return
+}
+
+proc usage {{text {}}} {
+    global argv0
+    puts stderr "Usage: $argv0 ?--nosign? cvs-repository fossil-rpeository"
+    if {$text eq ""} return
+    puts stderr "       $text"
+    exit
 }
 
-proc ::cvs::norm {path} {
-    variable npaths
-    if {![info exists npaths($path)]} {
-	set npaths($path) [NormFile $path]
-    }
-    return $npaths($path)
+proc import {cset} {
+    global tot nto nosign ntrunk
+    Write info "    Importing $cset [string repeat = [expr {60 - [string length $cset]}]]"
+    Write info "        At $nto/$ntrunk ([format %.2f [expr {double($nto)/$ntrunk}]]%)"
+
+    set usec [lindex [time {
+	foreach {uuid ad rm ch} [fossil::commit cvs2fossil $nosign \
+				     [cvs::wssetup $cset] \
+				     ::cvs::wsignore] break
+    } 1] 0]
+    cvs::uuid $cset $uuid
+
+    set sec [expr {$usec/1e6}]
+    set tot [expr {$tot + $sec}]
+    incr nto
+
+    Write info "        == $uuid +${ad}-${rm}*${ch}"
+    Write info "        in $sec seconds"
+
+    set avg [expr {$tot/$nto}]
+    set max [expr {$ntrunk * $avg}]
+    set rem [expr {$max - $tot}]
+
+    Write info "        st avg [format %.2f $avg]"
+    Write info "        st run [format %7.2f $tot] sec [format %6.2f [expr {$tot/60}]] min [format %5.2f [expr {$tot/3600}]] hr"
+    Write info "        st end [format %7.2f $max] sec [format %6.2f [expr {$max/60}]] min [format %5.2f [expr {$max/3600}]] hr"
+    Write info "        st rem [format %7.2f $rem] sec [format %6.2f [expr {$rem/60}]] min [format %5.2f [expr {$rem/3600}]] hr"
+    return
 }
 
-proc ::cvs::NormFile {path} {
-    variable base
-
-    set f $base/$path,v
-    if {[::file exists $f]} {return $path}
-
-    set hd [::file dirname $path]
-    set tl [::file tail    $path]
-
-    set f $base/$hd/Attic/$tl,v
-    if {[::file exists $f]} {return $path}
+# -----------------------------------------------------------------------------
 
-    # Bad. The dir can be truncated, i.e. it may not be an exact
-    # subdirectory of base, but deeper inside, with parents between it
-    # and base left out. Example (from the tcllib history file):
-    #
-    # M3f1d1245|afaupell|<remote>|ipentry|1.2|ChangeLog
-    # The correct path is 'tklib/modules/ipentry'.
-    # This we have to resolve too.
-
-    # normalize dance - old fileutil, modern fileutil (cvs head) doesn't do that.
-    set bx [file normalize $base]
-    foreach c [fileutil::findByPattern $bx -glob $hd] {
-	set cx [fileutil::stripPath $bx $c]
-	set c  $base/$cx
-
-	set f $c/$tl,v
-	if {[::file exists $f]} {return $cx/$tl}
-	set f $c/Attic/$tl,v
-	if {[::file exists $f]} {return $cx/$tl}
-    }
-
-    puts stderr <$f>
-    return -code error "Unable to locate actual file for $path"
+array set fl {
+    debug   {DEBUG  }
+    info    {       }
+    warning {Warning}
+    error   {ERROR  }
 }
 
-proc ::cvs::rcsfile {path} {
-    variable rpaths
-    if {![info exists rpaths($path)]} {
-	set rpaths($path) [RcsFile $path]
-    }
-    return $rpaths($path)
-}
-
-proc ::cvs::RcsFile {path} {
-    variable base
-
-    set f $base/$path,v
-    if {[::file exists $f]} {return $f}
-
-    set hd [::file dirname $path]
-    set tl [::file tail    $path]
-
-    set f $base/$hd/Attic/$tl,v
-    if {[::file exists $f]} {return $f}
-
-    # We do not have truncated directories here, assuming that only
-    # norm paths are fed into this command.
+proc Write {l t} {
+    global fl log
 
-    if 0 {
-	# Bad. The dir can be truncated, i.e. it may not be an exact
-	# subdirectory of base, but deeper inside, with parents
-	# between it and base left out. Example (from the tcllib
-	# history file):
-	#
-	# M3f1d1245|afaupell|<remote>|ipentry|1.2|ChangeLog The
-	# correct path is 'tklib/modules/ipentry'.  This we have to
-	# resolve too.
-
-	# normalize dance - old fileutil, modern fileutil (cvs head)
-	# doesn't do that.
-	set bx [file normalize $base]
-	foreach c [fileutil::findByPattern $bx -glob $hd] {
-	    set c $base/[fileutil::stripPath $bx $c]
-
-	    set f $c/$tl,v
-	    if {[::file exists $f]} {return $f}
-	    set f $c/Attic/$tl,v
-	    if {[::file exists $f]} {return $f}
-	}
+    if {[string index $t 0] eq "\r"} {
+	puts -nonewline stdout "\r$fl($l) [string range $t 0 end-1]"
+    } else {
+	puts stdout "$fl($l) $t"
+	puts $log   "$fl($l) $t"
     }
-
-    puts stderr <$f>
-    return -code error "Unable to locate rcs file for $path"
-}
-
-proc ::cvs::history {} {
-    variable base
-    return $base/CVSROOT/history
+    flush stdout
+    return
 }
 
 # -----------------------------------------------------------------------------
 
-# -----------------------------------------------------------------------------
-
-cvs::at [lindex $argv 0]
-
-#puts [::cvs::norm ipentry/ChangeLog]
-#exit
-
-#changeset state
-global cs csf
-array set cs {
-    start {} end {} cm {}
-    usr   {} dt  {}
-}
-array set csf {}
-
-proc rh {} {
-    global argv cs csf repo
-
-    set f [open [cvs::history] r]
-
-    while {[gets $f line] >= 0} {
-	# Decode line
-	foreach {op usr _ dir rev file} [split [string trim $line] |] break
-	set ts [cvs::hextime [string range $op 1 end]]
-	set op [string index $op 0]
-
-	# Filter out irrelevant parts
-	if {$op eq "O"} continue ; # checkout
-	if {$op eq "F"} continue ; # release
-	if {$op eq "T"} continue ; # rtag
-	if {$op eq "W"} continue ; # delete on update
-	if {$op eq "U"} continue ; # update
-	if {$op eq "P"} continue ; # update by patch
-	#if {$op eq "G"} continue ; # merge on update - FUTURE - identifies mergepoints.
-	if {$op eq "C"} continue ; # conflict on update - s.a.
-	if {$op eq "E"} continue ; # export
-	# left types
-	# M: commit
-	# A: addition
-	# R: removal
-
-	set df $dir/$file
-	if {[newcs $op $usr $ts $rev df cause]} {
-
-	    # NOTE 1: ChangeSets containing CVSROOT => remove such files.
-	    # NOTE 2: Empty changesets, ignore.
-
-	    #commit
-	    csstats
-
-	    if {$cause eq "cmsg"} {
-set msg
-	    } else {
-set msg ""
-	    }
-
-	    if {$cs(end) ne ""} {
-		puts =============================/$cause\ delta\ [expr {$ts - $cs(end)}]
-	    } else {
-		puts =============================/$cause
-	    }
-	    csclear
-	}
-
-	# Note: newcs normalizes df, in case the log information is
-	# bogus. So the df here may be different from before newcs
-	csadd $op $usr $ts $rev $df
-	# apply modification to workspace
-    }
-}
-
-proc newcs {op usr ts rev dfv rv} {
-    global cs csf
-    upvar 1 $rv reason $dfv df
-
-    # Logic to detect when a new change set begins. A new change sets
-    # has started with the current entry when the entry
-    #
-    # 1. is for a different user than the last.
-    # 2. tries to add a file to the changeset which is already part of it.
-    # 3.is on the trunk, and the last on a branch, or vice versa.
-    # 4. the current entry has a different commit message than the last.
-
-    set df [cvs::norm $df]
-
-    # User changed
-    if {$usr ne $cs(usr)} {
-	set reason user
-	return 1
-    }
-
-    # File is already in the changeset
-    if {[info exists csf($df)]} {
-	set reason file
-	return 1
-    }
-
-    # last/current are different regarding trunk/branch
-    set depth [llength [split $rev .]]
-    if {($cs(lastd) == 2) != ($depth == 2)} {
-	set reason branch
-	return 1
-    }
-
-    # Commit message changed
-    if {[cvs::cmsg $cs(lastf) $cs(lastr)] ne [cvs::cmsg $df $rev]} {
-	set reason cmsg
-	return 1
-    }
-
-    # Same changeset
-    return 0
-}
-
-proc csclear {} {
-    global cs csf
-    array set cs {start {} usr {} end {} dt {}}
-    array unset csf *
-    return
-}
-
-proc csadd {op usr ts rev df} {
-    global cs csf
-
-    if {$cs(usr) eq ""} {set cs(usr) $usr}
-    if {$cs(start) eq ""} {
-	set cs(start) $ts
-    } else {
-	lappend cs(dt) [expr {$ts - $cs(end)}]
-    }
-    set cs(end) $ts
-
-    set csf($df) [list $op $rev]
-    set cs(lastf) $df
-    set cs(lastr) $rev
-    set cs(lastd) [llength [split $rev .]]
-
-    puts [list $op [clock format $ts] $usr $rev $df]
-    return
-}
-
-proc csstats {} {
-    global cs csf
-
-    if {$cs(start) eq ""} return
-
-    puts "files: [array size csf]"
-    puts "delta: $cs(dt)"
-    puts "range: [expr {$cs(end) - $cs(start)}] seconds"
-    return
-}
-
-rh
-
-exit
-
-=========================================
-new fossil
-new fossil workspace
-
-open history
-
-foreach line {
-    ignore unwanted lines
-
-    accumulate changesets data
-    new change-set => commit and continue
-
-    current branch and branch of new change different ?
-    => move fossil workspace to proper revision.
-
-    apply change to workspace
-    uncommitted
-}
-
-if uncommitted => commit
-delete workspace
-copy fossil repo to destination
-=========================================
-
-Not dealt with in outline: branches, tags, merging
-
-=========================================
-
-complexities
-- apply to workspace
-  - remove simple, direct translation
-  - add => requires extraction of indicated revision from ,v
-  - modify => see above (without add following)
-
-- ,v file => Can be the either dir/file,v, or dir/Attic/file,v
-  Both ? Priority ?
-
-- How to detect changes on branches ?
-
-- Have to keep knowledge of which branches went there.
- => save change-sets information, + uuid in fossil
- => need only the leaves of each branch, and of branch points.
- => better keep all until complete.
- => uuid can be gotten from 'manifest.uuid' in workspace.
-- keep tag information ? (symbolics)
-
-=========================================
-
-CVSROOT=ORIGIN
-
-cvs -d ORIGIN checkout -r REV FILE
-Extract specific revision of a specific file.
--q, -Q quietness
+main
+exit

Added tools/lib/cvs.tcl version [abba347c2b]

@@ -1,1 +1,485 @@
+# -----------------------------------------------------------------------------
+# Repository management (CVS)
+
+# -----------------------------------------------------------------------------
+# Requirements
+
+package require Tcl 8.4
+package require fileutil  ; # Tcllib (cat)
+package require rcsparser ; # Handling the RCS archive files.
+package require struct::tree
+
+namespace eval ::cvs {}
+
+# -----------------------------------------------------------------------------
+# API
+
+# Define repository directory.
+
+proc ::cvs::at {path} {
+    variable base [file normalize $path]
+    return
+}
+
+namespace eval ::cvs {
+    # Toplevel repository directory
+    variable base {}
+}
+
+# Define logging callback command
+
+proc ::cvs::feedback {logcmd} {
+    variable lc $logcmd
+    ::rcsparser::feedback $logcmd
+    return
+}
+
+# Scan repository, collect archives, parse them, and collect revision
+# information (file, revision -> date, author, commit message)
+
+proc ::cvs::scan {} {
+    variable base
+    variable npaths
+    variable rpaths
+    variable timeline
+
+    Log info "Scanning CVS tree $base"
+
+    set n 0
+    foreach rcs [fileutil::findByPattern $base -glob *,v] {
+	set rcs [fileutil::stripPath $base $rcs]
+	# Now rcs is relative to base
+
+	Log info "    Parsing archive $rcs"
+
+	if {[string match CVSROOT* $rcs]} {
+	    Log info "                 => Ignoring admin file"
+	    continue
+	}
+
+	# Derive the regular path from the rcs path. Meaning: Chop of
+	# the ",v" suffix, and remove a possible "Attic".
+	set f [string range $rcs 0 end-2]
+	if {"Attic" eq [lindex [file split $rcs] end-1]} {
+	    set f [file join [file dirname [file dirname $f]] [file tail $f]]
+	    if {[file exists $base/$f,v]} {
+		# We have a regular archive and an Attic archive
+		# refering to the same user visible file. Ignore the
+		# file in the Attic.
+		Log info "                 => Ignoring attic for regular archive"
+
+		# TODO/CHECK. My method of co'ing exact file revisions
+		# per the info in the collected csets has the flaw
+		# that I may have to know exactly when what archive
+		# file to use, see above. It might be better to use
+		# the info only to gather when csets begin and end,
+		# and then to co complete slices per exact timestamp
+		# (-D) instead of file revisions (-r). The flaw in
+		# that is that csets can occur in the same second
+		# (trf, memchan - check for examples). For that exact
+		# checkout may be needed to recreate exact sequence of
+		# changes. Grr. Six of one ...
+
+		continue
+	    }
+	}
+
+	# Get the meta data we need (revisions, timeline, messages).
+	set meta [::rcsparser::process $base/$rcs]
+
+	Log info "         => $f"
+
+	set npaths($rcs) $f
+	set rpaths($f) $rcs
+
+	array set p $meta
+
+	foreach {rev ts} $p(date) {_ a} $p(author) {_ cm} $p(commit) {_ st} $p(state) {
+	    set op [expr {($rev eq "1.1") ? "A" : "M"}]
+	    if {$st eq "dead"} {set op "R"}
+
+	    # A dead-first revision is rev 1.1 with op R. For an
+	    # example see the file memchan/DEPENDENCIES. Such a file
+	    # seems to exist only! on its branch. The branches
+	    # information is set on the revision (extend rcsparser!),
+	    # symbols has a tag, refering to a branch, possibly magic.
+
+	    if {($rev eq "1.1") && ($op eq "R")} {
+		Log info "         => Dead first"
+	    }
+
+	    lappend timeline($ts) [list $op $ts $a $rev $f $cm]
+	}
+
+	#unset p(commit)
+	#parray p
+
+	incr n
+    }
+
+    Log info "Processed $n [expr {($n == 1) ? "file" : "files"}]"
+    return
+}
+
+namespace eval ::cvs {
+    # Path mappings. npaths: rcs file  -> user file
+    #                rpaths: user file -> rcs file, dead-status
+
+    variable npaths   ; array set npaths   {}
+    variable rpaths   ; array set rpaths   {}
+
+    # Timeline: tstamp -> (op, tstamp, author, revision, file, commit message)
+
+    variable timeline ; array set timeline {}
+}
+
+# Group single changes into changesets
+
+proc ::cvs::csets {} {
+    variable timeline
+    variable csets
+    variable ncs
+    variable cmap
+
+    array unset csets * ; array set csets {}
+    array unset cmap  * ; array set cmap  {}
+    set ncs 0
+
+    Log info "Processing timeline"
+
+    set n 0
+    CSClear
+    foreach ts [lsort -dict [array names timeline]] {
+
+	# op tstamp author revision file commit
+	# 0  1      2      3        4    5/end
+	# b         c                    a
+
+	set entries [lsort -index 2 [lsort -index 0 [lsort -index end $timeline($ts)]]]
+	#puts [join $entries \n]
+
+	foreach entry  $entries {
+	    if {![CSNone] && [CSNew $entry]} {
+		CSSave
+		CSClear
+		#puts ==\n$reason
+	    }
+	    CSAdd $entry
+	    incr n
+	}
+    }
+
+    Log info "Processed $n [expr {($n == 1) ? "entry" : "entries"}]"
+    set n [array size csets]
+    Log info "Found     $n [expr {($n == 1) ? "changeset" : "changesets"}]"
+    return
+}
+
+
+namespace eval ::cvs {
+    # Changeset data:
+    # ncs:   Counter-based id generation
+    # csets: id -> (user commit start end depth (file -> (op rev)))
+
+    variable ncs      ; set       ncs   0  ; # Counter for changesets
+    variable csets    ; array set csets {} ; # Changeset data
+}
+
+# Building the revision tree from the changesets.
+# Limitation: Currently only trunk csets is handled.
+# Limitation: Dead files are not removed, i.e. no 'R' actions right now.
+
+proc ::cvs::rtree {} {
+    variable csets
+    variable rtree {}
+    variable ntrunk 0
+
+    Log info "Extracting the trunk"
+
+    set rtree [struct::tree ::cvs::RT]
+    $rtree rename root 0 ; # Root is first changeset, always.
+    set trunk 0
+    set ntrunk 1 ; # Root is on the trunk.
+    set b      0 ; # No branch csets found yet.
+
+    # Extracting the trunk is easy, simply by looking at the involved
+    # version numbers.
+
+    foreach c [lrange [lsort -integer [array names csets]] 1 end] {
+	foreach {u cm s e rd f} $csets($c) break
+
+	# Ignore branch changes, just count them for the statistics.
+	if {$rd != 2} {
+	    incr b
+	    continue
+	}
+
+	# Trunk revision, connect to, and update the head.
+	$rtree insert $trunk end $c
+	set trunk $c
+	incr ntrunk
+    }
+
+    Log info "Processed $ntrunk trunk  [expr {($ntrunk == 1) ? "changeset" : "changesets"}]"
+    Log info "Ignored   $b branch [expr {($b == 1) ? "changeset" : "changesets"}]"
+    return
+}
+
+namespace eval ::cvs {
+    # Tree holding trunk and branch information (struct::tree).
+    # Node names are cset id's.
+
+    variable rtree {}
+    variable ntrunk 0
+}
+
+proc ::cvs::workspace {} {
+    variable cwd [pwd]
+    variable workspace [fileutil::tempfile importF_cvs_ws_]
+    file delete $workspace
+    file mkdir  $workspace
+
+    Log info "    Workspace: $workspace"
+
+    cd     $workspace ; # Checkouts go here.
+    return $workspace
+}
+
+proc ::cvs::wsignore {path} {
+    # Ignore CVS admin files.
+    if {[string match */CVS/* $path]} {return 1}
+    return 0
+}
+
+proc ::cvs::wsclear {} {
+    variable cwd
+    variable workspace
+    cd $cwd
+    file delete -force $workspace
+    return
+}
+
+proc ::cvs::wssetup {c} {
+    variable csets
+    variable cvs
+    variable base
+
+    # pwd = workspace
+
+    foreach {u cm s e rd fs} $csets($c) break
+
+    Log info "        @  $s"
+
+    foreach l [split [string trim $cm] \n] {
+	Log info "        |  $l"
+    }
+
+    foreach {f or} $fs {
+	foreach {op r} $or break
+	Log info "        -- $op $f $r"
+
+	if {$op eq "R"} {
+	    # Remove file from workspace. Prune empty directories.
+	    #
+	    # NOTE: A dead-first file (rev 1.1 dead) will never have
+	    # existed.
+	    #
+	    # NOTE: Logically empty directories still physically
+	    # contain the CVS admin directory, hence the check for ==
+	    # 1, not == 0. There might also be hidden files, we count
+	    # them as well. Always hidden are . and .. and they do not
+	    # count as user file.
+
+	    file delete $f
+	    set fd [file dirname $f]
+	    if {
+		([llength [glob -nocomplain -directory              $fd *]] == 1) &&
+		([llength [glob -nocomplain -directory -type hidden $fd *]] == 2)
+	    } {
+		file delete -force $fd
+	    }
+	} else {
+	    # Added or modified, put the requested version of the file
+	    # into the workspace.
+
+	    if {[catch {
+		exec $cvs -d $base co -r $r $f
+	    } msg]} {
+		if {[string match {*invalid change text*} $msg]} {
+		    # The archive of the file is corrupted and the
+		    # chosen version not accessible due to that. We
+		    # report the problem, but otherwise ignore it. As
+		    # a consequence the fossil repository will not
+		    # contain the full history of the named file. By
+		    # ignoring the problem we however get as much as
+		    # is possible.
+
+		    Log info "        EE Corrupted archive file. Inaccessible revision."
+		    continue
+		}
+		return -code error $msg
+	    }
+	}
+    }
+
+    # Provide metadata about the changeset the backend may wish to have
+    return [list $u $cm $s]
+}
+
+namespace eval ::cvs {
+    # CVS application
+    # Workspace where checkouts happen
+    # Current working directory to go back to after the import.
+
+    variable cvs       [auto_execok cvs]
+    variable workspace {}
+    variable cwd       {}
+}
+
+proc ::cvs::foreach_cset {cv node script} {
+    upvar 1 $cv c
+    variable rtree
+
+    set c $node
+    while {1} {
+	uplevel 1 $script
+
+	# Stop on reaching the head.
+	if {![llength [$rtree children $c]]} break
+
+	#puts <[$rtree children $c]>
+
+	# Go to next child in trunk (leftmost).
+	set c [lindex [$rtree children $c] 0]
+    }
+    return
+}
+
+proc ::cvs::root {} {
+    return 0
+}
+
+proc ::cvs::ntrunk {} {
+    variable ntrunk
+    return  $ntrunk
+}
+
+proc ::cvs::uuid {c uuid} {
+    variable rtree
+    $rtree set $c uuid $uuid
+    return
+}
+
+# -----------------------------------------------------------------------------
+# Internal helper commands: Changeset inspection and construction.
+
+proc ::cvs::CSClear {} {
+    upvar 1 start start end end cm cm user user files files lastd lastd
+
+    set start {}
+    set end   {}
+    set cm    {}
+    set user  {}
+    set lastd {}
+    array unset files *
+    array set files {}
+    return
+}
+
+proc ::cvs::CSNone {} {
+    upvar 1 start start
+    return [expr {$start eq ""}]
+}
+
+proc ::cvs::CSNew {entry} {
+    upvar 1 start start end end cm cm user user files files lastd lastd reason reason
+
+    #puts -nonewline stdout . ; flush stdout
+
+    foreach {op ts a rev f ecm} $entry break
+
+    # User change
+    if {$a ne $user} {set reason user ; return 1}
+
+    # File already in current cset
+    if {[info exists files($f)]} {set reason file ; return 1}
+
+    # Current cset trunk/branch different from entry.
+    set depth [llength [split $rev .]]
+    if {($lastd == 2) != ($depth == 2)} {set reason depth/$lastd/$depth/($rev)/$f ; return 1}
+
+    # Commit message changed
+    if {$cm ne $ecm} {set reason cmsg\ <<$ecm>> ; return 1}
+
+    # Everything is good, still the same cset
+    return 0
+}
+
+proc ::cvs::CSSave {} {
+    variable cmap
+    variable csets
+    variable ncs
+    upvar 1 start start end end cm cm user user files files lastd lastd
+
+    set csets($ncs) [list $user $cm $start $end $lastd [array get files]]
+
+    # Record which revisions of a file are in what csets
+    foreach {f or} [array get files] {
+	foreach {_ rev} $or break
+	set cmap([list $f $rev]) $ncs
+    }
+
+    #CSDump $ncs
+
+    incr ncs
+    return
+}
+
+proc ::cvs::CSAdd {entry} {
+    upvar 1 start start end end cm cm user user files files lastd lastd
+
+    foreach {op ts a rev f ecm} $entry break
+
+    if {$start eq ""} {set start $ts}
+    set end       $ts
+    set cm        $ecm
+    set user      $a
+    set files($f) [list $op $rev]
+    set lastd     [llength [split $rev .]]
+    return
+}
+
+proc ::cvs::CSDump {c} {
+    variable csets
+    foreach {u cm s e rd f} $csets($c) break
+
+    puts "$u $s"; regsub -all {.} $u { } b
+    puts "$b $e"
+    foreach {f or} $f {
+	foreach {o r} $or break
+	puts "$b $o $f $r"
+    }
+    return
+}
+
+# -----------------------------------------------------------------------------
+# Internal helper commands
+
+proc ::cvs::Log {level text} {
+    variable lc
+    uplevel #0 [linsert $lc end $level $text]
+    return
+}
+
+proc ::cvs::Nop {args} {}
+
+namespace eval ::cvs {
+    # Logging callback. No logging by default.
+    variable lc ::cvs::Nop
+}
+
+# -----------------------------------------------------------------------------
+# Ready
 
+package provide cvs 1.0
+return

Added tools/lib/fossil.tcl version [64ed240c1f]

@@ -1,1 +1,154 @@
+# -----------------------------------------------------------------------------
+# Repository management (FOSSIL)
+
+# -----------------------------------------------------------------------------
+# Requirements
+
+package require Tcl 8.4
+
+namespace eval ::fossil {}
+
+# -----------------------------------------------------------------------------
+# API
+
+# Define repository file, and connect to workspace in CWD.
+
+proc ::fossil::new {} {
+    variable fr     [file normalize [fileutil::tempfile import2_fsl_rp_]]
+    variable fossil
+
+    # pwd = workspace
+    exec $fossil new  $fr ; # create and
+    exec $fossil open $fr ; # connect
+
+    Log info "    Fossil:    $fr"
+
+    return $fr
+}
+
+# Define logging callback command
+
+proc ::fossil::feedback {logcmd} {
+    variable lc $logcmd
+    return
+}
+
+# Move generated fossil repository to final destination
+
+proc ::fossil::destination {path} {
+    variable fr
+    file rename $fr $path
+    return
+}
+
+namespace eval ::fossil {
+    # Repository file
+    variable fr {}
+
+    # Fossil application
+    variable fossil [auto_execok fossil]
+}
+
+
+proc ::fossil::commit {appname nosign meta ignore} {
+    variable fossil
+    variable lastuuid
+
+    # Commit the current state of the workspace. Scan for new and
+    # removed files and issue the appropriate fossil add/rm commands
+    # before actually comitting.
+
+    # Modified/Removed files first, that way there won't be any ADDED
+    # indicators. Nor REMOVED, only EDITED. Removed files show up as
+    # EDITED while they are not registered as removed.
+
+    set added   0
+    set removed 0
+    set changed 0
+
+    foreach line [split [exec $fossil changes] \n] {
+	regsub {^\s*EDITED\s*} $line {} path
+	if {[IGNORE $ignore $path]} continue
+
+	if {![file exists $path]} {
+	    exec $fossil rm $path
+	    incr removed
+	    Log info "        ** - $path"
+	} else {
+	    incr changed
+	    Log info "        ** * $path"
+	}
+    }
+
+    # Now look for unregistered added files.
+
+    foreach path [split [exec $fossil extra] \n] {
+	if {[IGNORE $ignore $path]} continue
+	exec $fossil add $path
+	incr added
+	Log info "        ** + $path"
+    }
+
+    # Now commit, using the provided meta data, and capture the uuid
+    # of the new baseline.
+
+    foreach {user message tstamp} $meta break
+
+    set message [join [list \
+			   "-- Originally by $user @ $tstamp" \
+			   "-- Imported by $appname" \
+			   $message] \n]
+
+    if {[catch {
+	if {$nosign} {
+	    exec $fossil commit -m $message --nosign
+	} else {
+	    exec $fossil commit -m $message
+	}
+    } line]} {
+	if {![string match "*nothing has changed*" $line]} {
+	    return -code error $line
+	}
+
+	# 'Nothing changed' can happen for changesets containing only
+	# dead-first revisions of one or more files. For fossil we
+	# re-use the last baseline. TODO: Mark them as branchpoint,
+	# and for what file.
+
+	Log info "        UNCHANGED, keeping last"
+
+	return [list $lastuuid 0 0 0]
+    }
+
+    set line [string trim $line]
+    regsub -nocase -- {^\s*New_Version:\s*} $line {} uuid
+
+    set lastuuid $uuid
+    return [list $uuid $added $removed $changed]
+}
+
+# -----------------------------------------------------------------------------
+# Internal helper commands
+
+proc ::fossil::IGNORE {ignore path} {
+    return [uplevel #0 [linsert $ignore end $path]]
+}
+
+proc ::fossil::Log {level text} {
+    variable lc
+    uplevel #0 [linsert $lc end $level $text]
+    return
+}
+
+proc ::fossil::Nop {args} {}
+
+namespace eval ::fossil {
+    # Logging callback. No logging by default.
+    variable lc ::fossil::Nop
+}
+
+# -----------------------------------------------------------------------------
+# Ready
 
+package provide fossil 1.0
+return

Modified tools/lib/pkgIndex.tcl from [71b6b857ad] to [bb6c58a2d8].

@@ -1,2 +1,4 @@
 if {![package vsatisfies [package require Tcl] 8.4]} return
 package ifneeded rcsparser 1.0 [list source [file join $dir rcsparser.tcl]]
+package ifneeded cvs       1.0 [list source [file join $dir cvs.tcl]]
+package ifneeded fossil    1.0 [list source [file join $dir fossil.tcl]]

Modified tools/lib/rcsparser.tcl from [69db20ceda] to [ec0acbb634].

@@ -12,17 +12,48 @@
 namespace eval ::rcsparser {}
 
 # -----------------------------------------------------------------------------
 # API
 
+proc ::rcsparser::feedback {logcmd} {
+    variable lc $logcmd
+    return
+}
+
 proc ::rcsparser::process {path} {
     set data [fileutil::cat -encoding binary $path]
     array set res {}
+    set res(size) [file size $path]
+    set res(done) 0
+    set res(nsize) [string length $res(size)]
+
     Admin
     Deltas
     Description
     DeltaTexts
+
+    Feedback \r
+
+    # Remove parser state
+    catch {unset res(id)}
+    catch {unset res(lastval)}
+    unset res(size)
+    unset res(nsize)
+    unset res(done)
+
+    # res: 'head'    -> head revision
+    #      'branch'  -> ?
+    #      'symbol'  -> (sym -> revision)
+    #      'lock'    -> (sym -> revision)
+    #      'comment' -> file comment
+    #      'expand'  -> ?
+    #      'date'    -> (revision -> date)
+    #      'author'  -> (revision -> author)
+    #      'state'   -> (revision -> state)
+    #      'parent'  -> (revision -> parent revision)
+    #      'commit'  -> (revision -> commit message)
+
     return [array get res]
 }
 
 # -----------------------------------------------------------------------------
 # Internal helper commands
@@ -33,11 +64,11 @@
     return
 }
 
 proc ::rcsparser::Deltas {} {
     upvar 1 data data res res
-    while {[Num 0]} { Date ; Author ; State ; Branches ; NextRev }
+    while {[Num 0]} { IsIdent ; Date ; Author ; State ; Branches ; NextRev }
     return
 }
 
 proc ::rcsparser::Description {} {
     upvar 1 data data res res
@@ -47,11 +78,11 @@
     return
 }
 
 proc ::rcsparser::DeltaTexts {} {
     upvar 1 data data res res
-    while {[Num 0]} { Log ; Text }
+    while {[Num 0]} { IsIdent ; Log ; Text }
     return
 }
 
 proc ::rcsparser::Head {} {
     upvar 1 data data res res
@@ -114,22 +145,27 @@
 }
 
 proc ::rcsparser::Date {} {
     upvar 1 data data res res
     Literal date ; Num 1 ; Literal \;
+
+    foreach {yr mo dy h m s} [split $res(lastval) .] break
+    if {$yr < 100} {incr yr 1900}
+    set res(lastval) [join [list $yr $mo $dy $h $m $s] .]
+    Map date
     return
 }
 
 proc ::rcsparser::Author {} {
     upvar 1 data data res res
-    Literal author ; Skip ; Literal \;
+    Literal author ; Skip ; Literal \; ; Map author
     return
 }
 
 proc ::rcsparser::State {} {
     upvar 1 data data res res
-    Literal state ; Skip ; Literal \;
+    Literal state ; Skip ; Literal \; ; Map state
     return
 }
 
 proc ::rcsparser::Branches {} {
     upvar 1 data data res res
@@ -137,25 +173,27 @@
     return
 }
 
 proc ::rcsparser::NextRev {} {
     upvar 1 data data res res
-    Literal next ; Skip ; Literal \;
+    Literal next ; Skip ; Literal \; ; Map parent
     return
 }
 
 proc ::rcsparser::Log {} {
     upvar 1 data data res res
-    IsIdent ; Literal log ; String 1 ; Map commit
+    Literal log ; String 1 ; Map commit
     return
 }
 
 proc ::rcsparser::Text {} {
     upvar 1 data data res res
     Literal text ; String 1
     return
 }
+
+# -----------------------------------------------------------------------------
 
 proc ::rcsparser::Ident {} {
     upvar 1 data data res res
 
     #puts I@?<[string range $data 0 10]...>
@@ -213,11 +251,12 @@
     return 1
 }
 
 proc ::rcsparser::Skip {} {
     upvar 1 data data res res
-    regexp -indices -- {^\s*[^;]*\s*} $data match
+    regexp -indices -- {^\s*([^;]*)\s*} $data match val
+    Get $val
     Next
     return
 }
 
 proc ::rcsparser::Def {key} {
@@ -230,11 +269,11 @@
 proc ::rcsparser::Map {key} {
     upvar 1 data data res res
     lappend res($key) $res(id) $res(lastval)
     #puts Map($res(id))=($res(lastval))
     unset res(lastval)
-    unset res(id)
+    #unset res(id);#Keep id for additional mappings.
     return
 }
 
 proc ::rcsparser::IsIdent {} {
     upvar 1 data data res res
@@ -250,16 +289,33 @@
     #puts G|$res(lastval)
     return
 }
 
 proc ::rcsparser::Next {} {
-    upvar 1 match match data data
+    upvar 1 match match data data res res
     foreach {s e} $match break ; incr e
     set data [string range $data $e end]
+    set res(done) [expr {$res(size) - [string length $data]}]
+
+    Feedback "\r    [format "%$res(nsize)s" $res(done)]/$res(size) "
+    return
+}
+
+# -----------------------------------------------------------------------------
+
+namespace eval ::rcsparser {
+    variable lc ::rcs::Nop
+}
+
+proc ::rcsparser::Feedback {text} {
+    variable lc
+    uplevel #0 [linsert $lc end info $text]
     return
 }
+
+proc ::rcsparser::Nop {args} {}
 
 # -----------------------------------------------------------------------------
 # Ready
 
 package provide rcsparser 1.0
 return