summaryrefslogtreecommitdiff
path: root/tools/cvs2fossil/lib/c2f_file.tcl
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /tools/cvs2fossil/lib/c2f_file.tcl
downloadrelic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz
initial commitHEADmaster
Diffstat (limited to 'tools/cvs2fossil/lib/c2f_file.tcl')
-rw-r--r--tools/cvs2fossil/lib/c2f_file.tcl1452
1 files changed, 1452 insertions, 0 deletions
diff --git a/tools/cvs2fossil/lib/c2f_file.tcl b/tools/cvs2fossil/lib/c2f_file.tcl
new file mode 100644
index 0000000..a113698
--- /dev/null
+++ b/tools/cvs2fossil/lib/c2f_file.tcl
@@ -0,0 +1,1452 @@
+## -*- tcl -*-
+# # ## ### ##### ######## ############# #####################
+## Copyright (c) 2007-2008 Andreas Kupries.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://fossil-scm.hwaci.com/fossil
+# # ## ### ##### ######## ############# #####################
+
+## File, part of a project, part of a CVS repository. Multiple
+## instances are possible.
+
+# # ## ### ##### ######## ############# #####################
+## Requirements
+
+package require Tcl 8.4 ; # Required runtime.
+package require snit ; # OO system.
+package require struct::set ; # Set operations.
+package require struct::list ; # Higher order operations.
+package require vc::fossil::import::cvs::blobstore ; # Blob storage.
+package require vc::fossil::import::cvs::file::rev ; # CVS per file revisions.
+package require vc::fossil::import::cvs::file::sym ; # CVS per file symbols.
+package require vc::fossil::import::cvs::state ; # State storage.
+package require vc::fossil::import::cvs::integrity ; # State integrity checks.
+package require vc::fossil::import::cvs::gtcore ; # Graph traversal core.
+package require vc::tools::trouble ; # Error reporting.
+package require vc::tools::log ; # User feedback
+package require vc::tools::misc ; # Text formatting
+
+# # ## ### ##### ######## ############# #####################
+##
+
+snit::type ::vc::fossil::import::cvs::file {
+ # # ## ### ##### ######## #############
+ ## Public API
+
+ constructor {id path usrpath executable project} {
+ set myid $id
+ set mypath $path
+ set myusrpath $usrpath
+ set myexecutable $executable
+ set myproject $project
+ set mytrunk [$myproject trunk]
+ set myblob [blobstore ${selfns}::%AUTO% $id]
+ return
+ }
+
+ method setid {id} {
+ integrity assert {$myid eq ""} {File '$mypath' already has an id, '$myid'}
+ set myid $id
+ $myblob setid $id
+ return
+ }
+
+ method id {} { return $myid }
+ method path {} { return $mypath }
+ method usrpath {} { return $myusrpath }
+ method project {} { return $myproject }
+
+ delegate method commitmessageof to myproject
+
+ # # ## ### ##### ######## #############
+ ## Methods required for the class to be a sink of the rcs parser
+
+ #method begin {} {puts begin}
+ #method sethead {h} {puts head=$h}
+ #method setprincipalbranch {b} {puts pb=$b}
+ #method deftag {s r} {puts $s=$r}
+ #method setcomment {c} {puts comment=$c}
+ #method admindone {} {puts admindone}
+ #method def {rev date author state next branches} {puts "def $rev $date $author $state $next $branches"}
+ #method defdone {} {puts def-done}
+ #method setdesc {d} {puts desc=$d}
+ #method extend {rev commitmsg deltarange} {puts "extend $commitmsg $deltarange"}
+ #method done {} {puts done}
+
+ # # ## ### ##### ######## #############
+ ## Persistence (pass II)
+
+ method persist {} {
+ # First collect the reachable revisions and symbols, then
+ # assign id's to all. They are sorted so that we will have ids
+ # which sort in order of creation. Then we can save them. This
+ # is done bottom up. Revisions, then symbols. __NOTE__ This
+ # works only because sqlite is not checking foreign key
+ # references during insert. This allows to have dangling
+ # references which are fixed later. The longest dangling
+ # references are for the project level symbols, these we do
+ # not save here, but at the end of the pass. What we need are
+ # the ids, hence the two phases.
+
+ struct::list assign [$self Active] revisions symbols
+ foreach rev $revisions { $rev defid }
+ foreach sym $symbols { $sym defid }
+
+ state transaction {
+ foreach rev $revisions { $rev persist }
+ foreach sym $symbols { $sym persist }
+ $myblob persist
+ }
+ return
+ }
+
+ method drop {} {
+ foreach {_ rev} [array get myrev] { $rev destroy }
+ foreach {_ branch} [array get mybranches] { $branch destroy }
+ foreach {_ taglist} [array get mytags] {
+ foreach tag $taglist { $tag destroy }
+ }
+ return
+ }
+
+ # # ## ### ##### ######## #############
+ ## Implement the sink
+
+ method begin {} {#ignore}
+
+ method sethead {revnr} {
+ set myheadrevnr $revnr
+ return
+ }
+
+ method setprincipalbranch {branchnr} {
+ set myprincipal $branchnr
+ return
+ }
+
+ method deftag {name revnr} {
+ # FUTURE: Perform symbol transformation here.
+
+ if {[struct::set contains $mysymbols $name]} {
+ trouble fatal "Multiple definitions of the symbol '$name' in '$mypath'"
+ return
+ }
+
+ struct::set add mysymbols $name
+
+ if {[rev isbranchrevnr $revnr -> branchnr]} {
+ $self AddBranch $name $branchnr
+ } else {
+ $self AddTag $name $revnr
+ }
+ return
+ }
+
+ method setcomment {c} {# ignore}
+
+ method admindone {} {
+ # We do nothing at the boundary of admin and revision data
+ }
+
+ method def {revnr date author state next branches} {
+ $self RecordBranchCommits $branches
+
+ if {[info exists myrev($revnr)]} {
+ trouble fatal "File $mypath contains duplicate definitions for revision $revnr."
+ return
+ }
+
+ set myaid($revnr) [$myproject defauthor $author]
+ set myrev($revnr) [rev %AUTO% $revnr $date $state $self]
+
+ $myblob add $revnr $myrev($revnr)
+
+ if {$next ne ""} {
+ # parent revision NEXT is a delta of current.
+ $myblob delta $next $revnr
+ }
+ foreach b $branches {
+ # branch child revision B is a delta of current.
+ $myblob delta $b $revnr
+ }
+
+ $self RecordBasicDependencies $revnr $next
+ return
+ }
+
+ method defdone {} {
+ # This is all done after the revision tree has been extracted
+ # from the file, before the commit mesages and delta texts are
+ # processed.
+
+ $self ProcessPrimaryDependencies
+ $self ProcessBranchDependencies
+ $self SortBranches
+ $self ProcessTagDependencies
+ $self DetermineTheRootRevision
+ return
+ }
+
+ method setdesc {d} {# ignore}
+
+ method extend {revnr commitmsg textrange} {
+ set cmid [$myproject defcmessage [string trim $commitmsg]]
+
+ set rev $myrev($revnr)
+
+ if {[$rev hasmeta]} {
+ # Apparently repositories exist in which the delta data
+ # for revision 1.1 is provided several times, at least
+ # twice. The actual cause of this duplication is not
+ # known. Speculation centers on RCS/CVS bugs, or from
+ # manual edits of the repository which borked the
+ # internals. Whatever the cause, testing showed that both
+ # cvs and rcs use the first definition when performing a
+ # checkout, and we follow their lead. Side notes: 'cvs
+ # log' fails on such a file, and 'cvs rlog' prints the log
+ # message from the first delta, ignoring the second.
+
+ log write 1 file "In file $mypath : Duplicate delta data for revision $revnr"
+ log write 1 file "Ignoring the duplicate"
+ return
+ }
+
+ # Determine the line of development for the revision (project
+ # level). This gives us the branchid too, required for the
+ # meta data group the revision is in. (Note: By putting both
+ # branch/lod and project information into the group we ensure
+ # that any cross-project and cross-branch commits are
+ # separated into multiple commits, one in each of the projects
+ # and/or branches).
+
+ set lod [$self GetLOD $revnr]
+
+ $rev setmeta [$myproject defmeta [$lod id] $myaid($revnr) $cmid]
+ # Note: We keep settext on file::rev for the hastext queries
+ # used by several checks.
+ $rev settext $textrange
+ $rev setlod $lod
+
+ $myblob extend $revnr $textrange
+
+ # If this is revision 1.1, we have to determine whether the
+ # file seems to have been created through 'cvs add' instead of
+ # 'cvs import'. This can be done by looking at the un-
+ # adulterated commit message, as CVS uses a hardwired magic
+ # message for the latter, i.e. "Initial revision\n", no
+ # period. (This fact also helps us when the time comes to
+ # determine whether this file might have had a default branch
+ # in the past.)
+
+ if {$revnr eq "1.1"} {
+ set myimported [expr {$commitmsg eq "Initial revision\n"}]
+ }
+
+ # Here we also keep track of the order in which the revisions
+ # were added to the file.
+
+ lappend myrevisions $rev
+ return
+ }
+
+ method done {} {
+ # Complete the revisions, branches, and tags. This includes
+ # looking for a non-trunk default branch, marking its members
+ # and linking them into the trunk, possibly excluding
+ # non-trunk data, and collecting aggregate symbol statistics.
+
+ $self DetermineRevisionOperations
+ $self DetermineLinesOfDevelopment
+ $self HandleNonTrunkDefaultBranch
+ $self RemoveIrrelevantDeletions
+ $self RemoveInitialBranchDeletions
+
+ if {[$myproject trunkonly]} {
+ $self ExcludeNonTrunkInformation
+ }
+
+ $self AggregateSymbolData
+ return
+ }
+
+ # # ## ### ##### ######## #############
+ ## Pass XII (Import).
+
+ method pushto {repository} {
+ log write 2 file {Importing file "$mypath"}
+
+ set ws [$repository workspace]
+ struct::list assign [$self Expand $ws] filemap revmap
+ # filemap = dict (path -> uuid)
+ # revmap = dict (path -> rid)
+
+ array set idmap [$repository importfiles $filemap]
+
+ # Wipe workspace clean of the imported files.
+ foreach x [glob -directory $ws r*] { ::file delete $x }
+
+ foreach {path rid} $revmap {
+ set uuid $idmap($path)
+ state run {
+ INSERT INTO revuuid (rid, uuid)
+ VALUES ($rid, $uuid)
+ }
+ }
+ return
+ }
+
+ method Expand {dir} {
+ set ex [struct::graph ex] ; # Expansion graph.
+ set zp [struct::graph zp] ; # Zip/Import graph.
+
+ close [open $dir/r__empty__ w];# Base for detached roots on branches.
+
+ # Phase I: Pull blobs and referenced revisions from the state
+ # and fill the graphs with them...
+
+ # Note: We use the blobs for expansion because we need them
+ # all, even those without revision, for both proper
+ # ordering and exact patch application.
+
+ set earcs {} ; # Arcs for expansion graph
+ set zarcs {} ; # Arcs for zip graph
+ set revmap {} ; # path -> rid map to later merge uuid information
+
+ state foreachrow {
+ SELECT B.rid AS xrid,
+ R.rev AS revnr,
+ R.child AS xchild,
+ B.coff AS xcoff,
+ B.clen AS xclen,
+ B.bid AS cid,
+ B.pid AS cparent
+ FROM blob B
+ LEFT OUTER JOIN revision R
+ ON B.rid = R.rid
+ WHERE B.fid = $myid
+ } {
+ # Main data are blobs, most will have revisions, but not
+ # all. The expansion graph is blob based, whereas the
+ # recompression graph is revision based.
+
+ if {$revnr ne ""} {
+ # Blob has revision, extend recompression graph.
+
+ lappend revmap r$revnr $xrid
+
+ $zp node insert $xrid
+ $zp node set $xrid revnr $revnr
+ $zp node set $xrid label <$revnr>
+
+ if {$xchild ne ""} {
+ lappend zarcs $xchild $xrid
+ }
+ } else {
+ # We fake a revnr for the blobs which have no
+ # revision, for use in the expansion graph.
+ set revnr ghost$cid
+ }
+
+ # Now the expansion graph.
+
+ $ex node insert $cid
+ $ex node set $cid text [list $xcoff $xclen]
+ $ex node set $cid revnr $revnr
+ $ex node set $cid label <$revnr>
+
+ if {$cparent ne ""} {
+ # The expansion arcs go from baseline to delta
+ # descendant, based on the blob information.
+ lappend earcs $cparent $cid
+ }
+ }
+
+ # Phase II: Insert the accumulated dependencies
+
+ foreach {from to} $earcs { $ex arc insert $from $to }
+ foreach {from to} $zarcs { $zp arc insert $from $to }
+
+ # Phase III: Traverse the graphs, expand the file, and
+ # generate import instructions.
+
+ set archive [::file join [$myproject fullpath] $mypath]
+ set ac [open $archive r]
+ fconfigure $ac -translation binary
+
+ # First traverse the expansion graph, this gives us the
+ # revisions in the order we have to expand them, which we do.
+
+ set max [llength [$ex nodes]]
+ set myimport 0
+
+ gtcore datacmd [mymethod ExpandData]
+ gtcore formatcmd [mymethod ExpandFormat]
+ gtcore sortcmd [mymethod ExpandSort]
+ gtcore savecmd [mymethod Expand1 $ac $dir $max]
+
+ gtcore traverse $ex {Expanding revisions...}
+ close $ac
+
+ # Now traverse the import graph, this builds the instruction
+ # map for the fossil deltas.
+
+ gtcore datacmd [mymethod ExpandData]
+ gtcore formatcmd [mymethod ExpandFormat]
+ gtcore sortcmd [mymethod ExpandSort]
+ gtcore savecmd [mymethod Expand2]
+
+ set myimport {}
+ gtcore traverse $zp {Generating revision order for import...}
+ set filemap $myimport
+ unset myimport
+
+ # And back to import control
+
+ $ex destroy
+ $zp destroy
+
+ return [list $filemap $revmap]
+ }
+
+ method ExpandData {graph node} { return [$graph node get $node revnr] }
+ method ExpandFormat {graph item} { return <[lindex $item 1]> } ; # revnr
+ method ExpandSort {graph candidates} {
+ # candidates = list(item), item = list(node revnr)
+ # Sort by node and revnr -> Trunk revisions come first.
+ return [lsort -index 1 -dict [lsort -index 0 -dict $candidates]]
+ }
+ method Expand1 {chan dir max graph node} {
+ log progress 3 file $myimport $max ; incr myimport
+
+ set revnr [$graph node get $node revnr]
+ set fname r$revnr
+ struct::list assign [$graph node get $node text] offset length
+
+ if {$length < 0} {
+ set data ""
+ } else {
+ seek $chan $offset start
+ set data [string map {@@ @} [read $chan $length]]
+ }
+
+ if {![$graph node keyexists $node __base__]} {
+ # Full text node. Get the data, decode it, and save.
+
+ log write 8 file {Expanding <$revnr>, full text}
+
+ fileutil::writeFile -translation binary $dir/$fname $data
+ } else {
+ # Delta node. __base__ is the name of the file containing
+ # the baseline. The patch is at the specified location of
+ # the archive file.
+
+ set fbase [$graph node get $node __base__]
+ log write 8 file {Expanding <$revnr>, is delta of <$fbase>}
+
+ set base [fileutil::cat -translation binary $dir/$fbase]
+
+ # Writing the patch to disk is just for better
+ # debugging. It is not used otherwise.
+ fileutil::writeFile $dir/rpatch $data
+ fileutil::writeFile -translation binary $dir/$fname \
+ [Apply $base $data]
+ }
+
+ # Post to all successors that the just generated file is their
+ # baseline.
+
+ foreach out [$graph nodes -out $node] {
+ $graph node set $out __base__ $fname
+ }
+ return
+ }
+
+ proc Apply {base delta} {
+ # base = base text.
+ # delta = delta in rcs format.
+ #
+ # Both strings are unencoded, i.e. things like @@, etc. have
+ # already been replaced with their proper characters.
+ #
+ # Return value is the patched text.
+
+ set base [split $base \n]
+ set blen [llength $base]
+ set ooff 0
+ set res ""
+
+ set lines [split $delta \n]
+ set nlines [llength $lines]
+
+ log write 11 file {Base lines = $blen}
+ log write 11 file {Delta lines = $nlines}
+
+ for {set i 0} {$i < $nlines} {} {
+ log write 11 file { @ $i = [lindex $lines $i]}
+ log write 11 file { ooff $ooff}
+
+ if {![regexp {^([ad])(\d+)\s(\d+)$} [lindex $lines $i] -> cmd sl cn]} {
+ trouble internal "Bad ed command '[lindex $lines $i]'"
+ }
+
+ log write 11 file { cmd $cmd}
+ log write 11 file { sl $sl}
+ log write 11 file { cn $cn}
+
+ incr i
+ set el [expr {$sl + $cn}]
+
+ log write 11 file { el $el}
+
+ switch -exact -- $cmd {
+ d {
+ incr sl -1
+ incr el -1
+ if {$sl < $ooff} { trouble internal {Deletion before last edit} }
+ if {$sl > $blen} { trouble internal {Deletion past file end} }
+ if {$el > $blen} { trouble internal {Deletion beyond file end} }
+ foreach x [lrange $base $ooff [expr {$sl - 1}]] {
+ log write 15 file {.|$x|}
+ lappend res $x
+ }
+ set ooff $el
+ }
+ a {
+ if {$sl < $ooff} { trouble internal {Insert before last edit} }
+ if {$sl > $blen} { trouble internal {Insert past file end} }
+
+ foreach x [lrange $base $ooff [expr {$sl - 1}]] {
+ log write 15 file {.|$x|}
+ lappend res $x
+ }
+ foreach x [lrange $lines $i [expr {$i + $cn - 1}]] {
+ log write 15 file {+|$x|}
+ lappend res $x
+ }
+ set ooff $sl
+ incr i $cn
+ }
+ }
+ }
+ foreach x [lrange $base $ooff end] { lappend res $x }
+ return [join $res \n]
+ }
+
+ method Expand2 {graph node} {
+ set revnr [$graph node get $node revnr]
+
+ # First import the file.
+ lappend myimport [list A r$revnr {}]
+
+ if {[$graph node keyexists $node __base__]} {
+ # Delta node. __base__ is the name of the file containing
+ # the baseline. Generate instruction to make the delta as
+ # well.
+
+ set fbase [$graph node get $node __base__]
+ lappend myimport [list D r$revnr r$fbase]
+ }
+
+ # Post to all successors that the just generated file is their
+ # baseline. Exception: Those which ave already a baseline set.
+ # Together with the sorting of trunk revisions first the trunk
+ # should one uninterupted line, with branch roots _not_ delta
+ # compressed per their branches.
+
+ foreach out [$graph nodes -out $node] {
+ if {[$graph node keyexists $out __base__]} continue
+ $graph node set $out __base__ $revnr
+ }
+ return
+ }
+
+ variable myimport
+
+ # # ## ### ##### ######## #############
+ ## State
+
+ variable myid {} ; # File id in the persistent state.
+ variable mypath {} ; # Path of the file's rcs archive.
+ variable myusrpath {} ; # Path of the file as seen by users.
+ variable myexecutable 0 ; # Boolean flag 'file executable'.
+ variable myproject {} ; # Reference to the project object
+ # the file belongs to.
+ variable myrev -array {} ; # Maps revision number to the
+ # associated revision object.
+ variable myrevisions {} ; # Same as myrev, but a list,
+ # giving us the order of
+ # revisions.
+ variable myaid -array {} ; # Map revision numbers to the id
+ # of the author who committed
+ # it. This is later aggregated
+ # with commit message, branch name
+ # and project id for a meta id.
+ variable myheadrevnr {} ; # Head revision (revision number)
+ variable myprincipal {} ; # Principal branch (branch number).
+ # Contrary to the name this is the
+ # default branch.
+ variable mydependencies {} ; # Dictionary parent -> child,
+ # records primary dependencies.
+ variable myimported 0 ; # Boolean flag. Set if and only if
+ # rev 1.1 of the file seemingly
+ # was imported instead of added
+ # normally.
+ variable myroot {} ; # Reference to the revision object
+ # holding the root revision. Its
+ # number usually is '1.1'. Can be
+ # a different number, because of
+ # gaps created via 'cvsadmin -o'.
+ variable mybranches -array {} ; # Maps branch number to the symbol
+ # object handling the branch.
+ variable mytags -array {} ; # Maps revision number to the list
+ # of symbol objects for the tags
+ # associated with the revision.
+ variable mysymbols {} ; # Set of the symbol names found in
+ # this file.
+
+ variable mybranchcnt 0 ; # Counter for branches, to record their
+ # order of definition. This also defines
+ # their order of creation, which is the
+ # reverse of definition. I.e. a smaller
+ # number means 'Defined earlier', means
+ # 'Created later'.
+
+ variable mytrunk {} ; # Direct reference to myproject -> trunk.
+ variable myroots {} ; # List of roots in the forest of
+ # lod's. Object references to revisions and
+ # branches. The latter can appear when they
+ # are severed from their parent.
+
+ variable myblob {} ; # Reference to the object managing the blob
+ # information (textrange of revisions, and
+ # delta dependencies) of this file.
+
+ # # ## ### ##### ######## #############
+ ## Internal methods
+
+ method RecordBranchCommits {branches} {
+ foreach branchrevnr $branches {
+ if {[catch {
+ set branch [$self Rev2Branch $branchrevnr]
+ }]} {
+ set branch [$self AddUnlabeledBranch [rev 2branchnr $branchrevnr]]
+ }
+
+ # Record the commit, just as revision number for
+ # now. ProcesBranchDependencies will extend that ito a
+ # proper object reference.
+
+ $branch setchildrevnr $branchrevnr
+ }
+ return
+ }
+
+ method Rev2Branch {revnr} {
+ integrity assert {![rev istrunkrevnr $revnr]} {Expected a branch revision number}
+ return $mybranches([rev 2branchnr $revnr])
+ }
+
+ method AddUnlabeledBranch {branchnr} {
+ return [$self AddBranch unlabeled-$branchnr $branchnr]
+ }
+
+ method AddBranch {name branchnr} {
+ if {[info exists mybranches($branchnr)]} {
+ log write 1 file "In '$mypath': Branch '$branchnr' named '[$mybranches($branchnr) name]'"
+ log write 1 file "Cannot have second name '$name', ignoring it"
+ return
+ }
+ set branch [sym %AUTO% branch $branchnr [$myproject getsymbol $name] $self]
+ $branch setposition [incr mybranchcnt]
+ set mybranches($branchnr) $branch
+ return $branch
+ }
+
+ method AddTag {name revnr} {
+ set tag [sym %AUTO% tag $revnr [$myproject getsymbol $name] $self]
+ lappend mytags($revnr) $tag
+ return $tag
+ }
+
+ method RecordBasicDependencies {revnr next} {
+ # Handle the revision dependencies. Record them for now, do
+ # nothing with them yet.
+
+ # On the trunk the 'next' field points to the previous
+ # revision, i.e. the _parent_ of the current one. Example:
+ # 1.6's next is 1.5 (modulo cvs admin -o).
+
+ # Contrarily on a branch the 'next' field points to the
+ # primary _child_ of the current revision. As example,
+ # 1.1.3.2's 'next' will be 1.1.3.3.
+
+ # The 'next' field actually always refers to the revision
+ # containing the delta needed to retrieve that revision.
+
+ # The dependencies needed here are the logical structure,
+ # parent/child, and not the implementation dependent delta
+ # pointers.
+
+ if {$next eq ""} return
+ # parent -> child
+ if {[rev istrunkrevnr $revnr]} {
+ lappend mydependencies $next $revnr
+ } else {
+ lappend mydependencies $revnr $next
+ }
+ return
+ }
+
+ method ProcessPrimaryDependencies {} {
+ foreach {parentrevnr childrevnr} $mydependencies {
+ set parent $myrev($parentrevnr)
+ set child $myrev($childrevnr)
+ $parent setchild $child
+ $child setparent $parent
+ }
+ return
+ }
+
+ method ProcessBranchDependencies {} {
+ foreach {branchnr branch} [array get mybranches] {
+ set revnr [$branch parentrevnr]
+
+ if {![info exists myrev($revnr)]} {
+ log write 1 file "In '$mypath': The branch '[$branch name]' references"
+ log write 1 file "the bogus revision '$revnr' and will be ignored."
+ $branch destroy
+ unset mybranches($branchnr)
+ } else {
+ set rev $myrev($revnr)
+ $rev addbranch $branch
+ $branch setparent $rev
+
+ # If revisions were committed on the branch we store a
+ # reference to the branch there, and further declare
+ # the first child's parent to be branch's parent, and
+ # list this child in the parent revision.
+
+ if {[$branch haschildrev]} {
+ set childrevnr [$branch childrevnr]
+ set child $myrev($childrevnr)
+ $branch setchild $child
+
+ $child setparentbranch $branch
+ $child setparent $rev
+ $rev addchildonbranch $child
+ }
+ }
+ }
+ return
+ }
+
+ method SortBranches {} {
+ foreach {revnr rev} [array get myrev] { $rev sortbranches }
+ return
+ }
+
+ method ProcessTagDependencies {} {
+ foreach {revnr taglist} [array get mytags] {
+ if {![info exists myrev($revnr)]} {
+ set n [llength $taglist]
+ log write 1 file "In '$mypath': The following [nsp $n tag] reference"
+ log write 1 file "the bogus revision '$revnr' and will be ignored."
+ foreach tag $taglist {
+ log write 1 file " [$tag name]"
+ $tag destroy
+ }
+ unset mytags($revnr)
+ } else {
+ set rev $myrev($revnr)
+ foreach tag $taglist {
+ $rev addtag $tag
+ $tag settagrev $rev
+ }
+ }
+ }
+ return
+ }
+
+ method DetermineTheRootRevision {} {
+ # The root is the one revision which has no parent. By
+ # checking all revisions we ensure that we can detect and
+ # report the case of multiple roots. Without that we could
+ # simply take one revision and follow the parent links to
+ # their root (sic!).
+
+ foreach {revnr rev} [array get myrev] {
+ if {[$rev hasparent]} continue
+ integrity assert {$myroot eq ""} {Multiple root revisions found}
+ set myroot $rev
+ }
+
+ # In the future we also need a list, as branches can become
+ # severed from their parent, making them their own root.
+ set myroots [list $myroot]
+ return
+ }
+
+ method DetermineRevisionOperations {} {
+ foreach rev $myrevisions { $rev determineoperation }
+ return
+ }
+
+ method DetermineLinesOfDevelopment {} {
+ # For revisions this has been done already, in 'extend'. Now
+ # we do this for the branches and tags.
+
+ foreach {_ branch} [array get mybranches] {
+ $branch setlod [$self GetLOD [$branch parentrevnr]]
+ }
+
+ foreach {_ taglist} [array get mytags] {
+ foreach tag $taglist {
+ $tag setlod [$self GetLOD [$tag tagrevnr]]
+ }
+ }
+ return
+ }
+
+ method GetLOD {revnr} {
+ if {[rev istrunkrevnr $revnr]} {
+ return $mytrunk
+ } else {
+ return [$self Rev2Branch $revnr]
+ }
+ }
+
+ method HandleNonTrunkDefaultBranch {} {
+ set revlist [$self NonTrunkDefaultRevisions]
+ if {![llength $revlist]} return
+
+ $self AdjustNonTrunkDefaultBranch $revlist
+ $self CheckLODs
+ return
+ }
+
+ method NonTrunkDefaultRevisions {} {
+ # From cvs2svn the following explanation (with modifications
+ # for our algorithm):
+
+ # Determine whether there are any non-trunk default branch
+ # revisions.
+
+ # If a non-trunk default branch is determined to have existed,
+ # return a list of objects for all revisions that were once
+ # non-trunk default revisions, in dependency order (i.e. root
+ # first).
+
+ # There are two cases to handle:
+
+ # One case is simple. The RCS file lists a default branch
+ # explicitly in its header, such as '1.1.1'. In this case, we
+ # know that every revision on the vendor branch is to be
+ # treated as head of trunk at that point in time.
+
+ # But there's also a degenerate case. The RCS file does not
+ # currently have a default branch, yet we can deduce that for
+ # some period in the past it probably *did* have one. For
+ # example, the file has vendor revisions 1.1.1.1 -> 1.1.1.96,
+ # all of which are dated before 1.2, and then it has 1.1.1.97
+ # -> 1.1.1.100 dated after 1.2. In this case, we should
+ # record 1.1.1.96 as the last vendor revision to have been the
+ # head of the default branch.
+
+ if {$myprincipal ne ""} {
+ # There is still a default branch; that means that all
+ # revisions on that branch get marked.
+
+ log write 5 file "Found explicitly marked NTDB"
+
+ set rnext [$myroot child]
+ if {$rnext ne ""} {
+ trouble fatal "File with default branch $myprincipal also has revision [$rnext revnr]"
+ return
+ }
+
+ set rev [$mybranches($myprincipal) child]
+ set res {}
+
+ while {$rev ne ""} {
+ lappend res $rev
+ set rev [$rev child]
+ }
+
+ return $res
+
+ } elseif {$myimported} {
+ # No default branch, but the file appears to have been
+ # imported. So our educated guess is that all revisions
+ # on the '1.1.1' branch with timestamps prior to the
+ # timestamp of '1.2' were non-trunk default branch
+ # revisions.
+
+ # This really only processes standard '1.1.1.*'-style
+ # vendor revisions. One could conceivably have a file
+ # whose default branch is 1.1.3 or whatever, or was that
+ # at some point in time, with vendor revisions 1.1.3.1,
+ # 1.1.3.2, etc. But with the default branch gone now,
+ # we'd have no basis for assuming that the non-standard
+ # vendor branch had ever been the default branch anyway.
+
+ # Note that we rely on comparisons between the timestamps
+ # of the revisions on the vendor branch and that of
+ # revision 1.2, even though the timestamps might be
+ # incorrect due to clock skew. We could do a slightly
+ # better job if we used the changeset timestamps, as it is
+ # possible that the dependencies that went into
+ # determining those timestamps are more accurate. But
+ # that would require an extra pass or two.
+
+ if {![info exists mybranches(1.1.1)]} { return {} }
+
+ log write 5 file "Deduced existence of NTDB"
+
+ set rev [$mybranches(1.1.1) child]
+ set res {}
+ set stop [$myroot child]
+
+ if {$stop eq ""} {
+ # Get everything on the branch
+ while {$rev ne ""} {
+ lappend res $rev
+ set rev [$rev child]
+ }
+ } else {
+ # Collect everything on the branch which seems to have
+ # been committed before the first primary child of the
+ # root revision.
+ set stopdate [$stop date]
+ while {$rev ne ""} {
+ if {[$rev date] >= $stopdate} break
+ lappend res $rev
+ set rev [$rev child]
+ }
+ }
+
+ return $res
+
+ } else {
+ return {}
+ }
+ }
+
+ # General note: In the following methods we only modify the links
+ # between revisions and symbols to restructure the revision
+ # tree. We do __not__ destroy the objects. Given the complex links
+ # GC is difficult at this level. It is much easier to drop
+ # everything when we we are done. This happens in 'drop', using
+ # the state variable 'myrev', 'mybranches', and 'mytags'. What we
+ # have to persist, performed by 'persist', we know will be
+ # reachable through the revisions listed in 'myroots' and their
+ # children and symbols.
+
+ method AdjustNonTrunkDefaultBranch {revlist} {
+ set stop [$myroot child] ;# rev '1.2'
+
+ log write 5 file "Adjusting NTDB containing [nsp [llength $revlist] revision]"
+
+ # From cvs2svn the following explanation (with modifications
+ # for our algorithm):
+
+ # Adjust the non-trunk default branch revisions found in the
+ # 'revlist'.
+
+ # 'myimported' is a boolean flag indicating whether this file
+ # appears to have been imported, which also means that
+ # revision 1.1 has a generated log message that need not be
+ # preserved. 'revlist' is a list of object references for the
+ # revisions that have been determined to be non-trunk default
+ # branch revisions.
+
+ # Note that the first revision on the default branch is
+ # handled strangely by CVS. If a file is imported (as opposed
+ # to being added), CVS creates a 1.1 revision, then creates a
+ # vendor branch 1.1.1 based on 1.1, then creates a 1.1.1.1
+ # revision that is identical to the 1.1 revision (i.e., its
+ # deltatext is empty). The log message that the user typed
+ # when importing is stored with the 1.1.1.1 revision. The 1.1
+ # revision always contains a standard, generated log message,
+ # 'Initial revision\n'.
+
+ # When we detect a straightforward import like this, we want
+ # to handle it by deleting the 1.1 revision (which doesn't
+ # contain any useful information) and making 1.1.1.1 into an
+ # independent root in the file's dependency tree. In SVN,
+ # 1.1.1.1 will be added directly to the vendor branch with its
+ # initial content. Then in a special 'post-commit', the
+ # 1.1.1.1 revision is copied back to trunk.
+
+ # If the user imports again to the same vendor branch, then CVS
+ # creates revisions 1.1.1.2, 1.1.1.3, etc. on the vendor branch,
+ # *without* counterparts in trunk (even though these revisions
+ # effectively play the role of trunk revisions). So after we add
+ # such revisions to the vendor branch, we also copy them back to
+ # trunk in post-commits.
+
+ # We mark the revisions found in 'revlist' as default branch
+ # revisions. Also, if the root revision has a primary child
+ # we set that revision to depend on the last non-trunk default
+ # branch revision and possibly adjust its type accordingly.
+
+ set first [lindex $revlist 0]
+
+ log write 6 file "<[$first revnr]> [expr {$myimported ? "imported" : "not imported"}], [$first operation], [expr {[$first hastext] ? "has text" : "no text"}]"
+
+ if {$myimported &&
+ [$first revnr] eq "1.1.1.1" &&
+ [$first operation] eq "change" &&
+ ![$first hastext]} {
+
+ set rev11 [$first parent] ; # Assert: Should be myroot.
+ log write 3 file "Removing irrelevant revision [$rev11 revnr]"
+
+ # Cut out the old myroot revision.
+
+ ldelete myroots $rev11 ; # Not a root any longer.
+
+ $first cutfromparent ; # Sever revision from parent revision.
+ if {$stop ne ""} {
+ $stop cutfromparent
+ lappend myroots $stop ; # New root, after vendor branch
+ }
+
+ # Cut out the vendor branch symbol
+
+ set vendor [$first parentbranch]
+ integrity assert {$vendor ne ""} {First NTDB revision has no branch}
+ if {[$vendor parent] eq $rev11} {
+ $rev11 removebranch $vendor
+ $rev11 removechildonbranch $first
+ $vendor cutbranchparent ;# bp = rev11, about to be gone
+ $first cutfromparentbranch ;# pb = vendor, to be a detached LOD
+ lappend myroots $first
+ }
+
+ # Change the type of first (typically from Change to Add):
+ $first retype add
+
+ # Move any tags and branches from the old to the new root.
+ $rev11 movesymbolsto $first
+ }
+
+ # Mark all the special revisions as such
+ foreach rev $revlist {
+ log write 3 file "Revision on default branch: [$rev revnr]"
+ $rev setondefaultbranch 1
+ }
+
+ if {$stop ne ""} {
+ # Revision 1.2 logically follows the imported revisions,
+ # not 1.1. Accordingly, connect it to the last NTDBR and
+ # possibly change its type.
+
+ set last [lindex $revlist end]
+ $stop setdefaultbranchparent $last ; # Retypes the revision too.
+ $last setdefaultbranchchild $stop
+ }
+ return
+ }
+
+ method CheckLODs {} {
+ foreach {_ branch} [array get mybranches] { $branch checklod }
+ foreach {_ taglist} [array get mytags] {
+ foreach tag $taglist { $tag checklod }
+ }
+ return
+ }
+
+ method RemoveIrrelevantDeletions {} {
+ # From cvs2svn: If a file is added on a branch, then a trunk
+ # revision is added at the same time in the 'Dead' state.
+ # This revision doesn't do anything useful, so delete it.
+
+ foreach root $myroots {
+ if {[$root isneeded]} continue
+ log write 2 file "Removing unnecessary dead revision [$root revnr]"
+
+ # Remove as root, make its child new root after
+ # disconnecting it from the revision just going away.
+
+ ldelete myroots $root
+ if {[$root haschild]} {
+ set child [$root child]
+ $child cutfromparent
+ lappend myroots $child
+ }
+
+ # Cut out the branches spawned by the revision to be
+ # deleted. If the branch has revisions they should already
+ # use operation 'add', no need to change that. The first
+ # revision on each branch becomes a new and disconnected
+ # root.
+
+ foreach branch [$root branches] {
+ $branch cutbranchparent
+ if {![$branch haschild]} continue
+ set first [$branch child]
+ $first cutfromparent
+ lappend myroots $first
+ }
+ $root removeallbranches
+
+ # Tagging a dead revision doesn't do anything, so remove
+ # any tags that were set on it.
+
+ $root removealltags
+
+ # This can only happen once per file, and we might have
+ # just changed myroots, so end the loop
+ break
+ }
+ return
+ }
+
+ method RemoveInitialBranchDeletions {} {
+ # From cvs2svn: If the first revision on a branch is an
+ # unnecessary delete, remove it.
+ #
+ # If a file is added on a branch (whether or not it already
+ # existed on trunk), then new versions of CVS add a first
+ # branch revision in the 'dead' state (to indicate that the
+ # file did not exist on the branch when the branch was
+ # created) followed by the second branch revision, which is an
+ # add. When we encounter this situation, we sever the branch
+ # from trunk and delete the first branch revision.
+
+ # At this point we may have already multiple roots in myroots,
+ # we have to process them all.
+
+ foreach root [$self LinesOfDevelopment] {
+ if {[$root isneededbranchdel]} continue
+ log write 2 file "Removing unnecessary initial branch delete [$root revnr]"
+
+ set branch [$root parentbranch]
+ set parent [$root parent]
+ set child [$root child]
+
+ ldelete myroots $root
+ lappend myroots $child
+
+ $branch cutbranchparent
+ $branch cutchild
+ $child cutfromparent
+
+ $branch setchild $child
+ $child setparentbranch $branch
+
+ $parent removebranch $branch
+ $parent removechildonbranch $root
+ }
+ return
+ }
+
+ method LinesOfDevelopment {} {
+ # Determine all lines of development for the file. This are
+ # the known roots, and the root of all branches found on the
+ # line of primary children.
+
+ set lodroots {}
+ foreach root $myroots {
+ $self AddBranchedLinesOfDevelopment lodroots $root
+ lappend lodroots $root
+ }
+ return $lodroots
+ }
+
+ method AddBranchedLinesOfDevelopment {lv root} {
+ upvar 1 $lv lodroots
+ while {$root ne ""} {
+ foreach branch [$root branches] {
+ if {![$branch haschild]} continue
+ set child [$branch child]
+ # Recurse into the branch for deeper branches.
+ $self AddBranchedLinesOfDevelopment lodroots $child
+ lappend lodroots $child
+ }
+ set root [$root child]
+ }
+ return
+ }
+
+ method ExcludeNonTrunkInformation {} {
+ # Remove all non-trunk branches, revisions, and tags. We do
+ # keep the tags which are on the trunk.
+
+ set ntdbroot ""
+ foreach root [$self LinesOfDevelopment] {
+ # Note: Here the order of the roots is important,
+ # i.e. that we get them in depth first order. This ensures
+ # that the removal of a branch happens only after the
+ # branches spawned from it were removed. Otherwise the
+ # system might try to access deleted objects.
+
+ # Do not exclude the trunk.
+ if {[[$root lod] istrunk]} continue
+ $self ExcludeBranch $root ntdbroot
+ }
+
+ if {$ntdbroot ne ""} {
+ $self GraftNTDB2Trunk $ntdbroot
+ }
+ return
+ }
+
+ method ExcludeBranch {root nv} {
+ # Exclude the branch/lod starting at root, a revision.
+ #
+ # If the LOD starts with non-trunk default branch revisions,
+ # we leave them in place and do not delete the branch. In that
+ # case the command sets the variable in NV so that we can
+ # later rework these revisons to be purely trunk.
+
+ if {[$root isondefaultbranch]} {
+ # Handling a NTDB. This branch may consists not only of
+ # NTDB revisions, but also some non-NTDB. The latter are
+ # truly on a branch and have to be excluded. The following
+ # loop determines if there are such revisions.
+
+ upvar 1 $nv ntdbroot
+ set ntdbroot $root
+ $root cutfromparentbranch
+
+ set rev $root
+ while {$rev ne ""} {
+ $rev removeallbranches
+ # See note [x].
+
+ if {[$rev isondefaultbranch]} {
+ set rev [$rev child]
+ } else {
+ break
+ }
+ }
+
+ # rev now contains the first non-NTDB revision after the
+ # NTDB, or is empty if there is no such. If we have some
+ # they have to removed.
+
+ if {$rev ne ""} {
+ set lastntdb [$rev parent]
+ $lastntdb cutfromchild
+ while {$rev ne ""} {
+ $rev removealltags
+ $rev removeallbranches
+ # Note [x]: We may still have had branches on the
+ # revision. Branches without revisions committed
+ # on them do not show up in the list of roots aka
+ # lines of development.
+ set rev [$rev child]
+ }
+ }
+ return
+ }
+
+ # No NTDB stuff to deal with. First delete the branch object
+ # itself, after cutting all the various connections.
+
+ set branch [$root parentbranch]
+ if {$branch ne ""} {
+ set branchparent [$branch parent]
+ $branchparent removebranch $branch
+ $branchparent removechildonbranch $root
+ }
+
+ # The root is no such any longer either.
+ ldelete myroots $root
+
+ # Now go through the line and remove all its revisions.
+
+ while {$root ne ""} {
+ $root removealltags
+ $root removeallbranches
+ # Note: See the note [x].
+
+ # From cvs2svn: If this is the last default revision on a
+ # non-trunk default branch followed by a 1.2 revision,
+ # then the 1.2 revision depends on this one. FIXME: It is
+ # questionable whether this handling is correct, since the
+ # non-trunk default branch revisions affect trunk and
+ # should therefore not just be discarded even if
+ # --trunk-only.
+
+ if {[$root hasdefaultbranchchild]} {
+ set ntdbchild [$root defaultbranchchild]
+ integrity assert {
+ [$ntdbchild defaultbranchparent] eq $ntdbchild
+ } {ntdb - trunk linkage broken}
+ $ntdbchild cutdefaultbranchparent
+ if {[$ntdbchild hasparent]} {
+ lappend myroots [$ntdbchild parent]
+ }
+ }
+
+ set root [$root child]
+ }
+
+ return
+ }
+
+ method GraftNTDB2Trunk {root} {
+ # We can now graft the non-trunk default branch revisions to
+ # trunk. They should already be alone on a CVSBranch-less
+ # branch.
+
+ integrity assert {![$root hasparentbranch]} {NTDB root still has its branch symbol}
+ integrity assert {![$root hasbranches]} {NTDB root still has spawned branches}
+
+ set last $root
+ while {[$last haschild]} {set last [$last child]}
+
+ if {[$last hasdefaultbranchchild]} {
+
+ set rev12 [$last defaultbranchchild]
+ $rev12 cutdefaultbranchparent
+ $last cutdefaultbranchchild
+
+ $rev12 changeparent $last
+ $last changechild $rev12
+
+ ldelete myroots $rev12
+
+ # Note and remember that the type of rev12 was already
+ # adjusted by AdjustNonTrunkDefaultBranch, so we don't
+ # have to change its type here.
+ }
+
+ while {$root ne ""} {
+ $root setondefaultbranch 0
+ $root setlod $mytrunk
+ foreach tag [$root tags] {
+ $tag setlod $mytrunk
+ }
+ set root [$root child]
+ }
+
+ return
+ }
+
+ method Active {} {
+ set revisions {}
+ set symbols {}
+
+ foreach root [$self LinesOfDevelopment] {
+ if {[$root hasparentbranch]} { lappend symbols [$root parentbranch] }
+ while {$root ne ""} {
+ lappend revisions $root
+ foreach tag [$root tags] { lappend symbols $tag }
+ foreach branch [$root branches] {
+ integrity assert {
+ [$branch parent] eq $root
+ } {Backreference branch to its root is missing or wrong}
+ lappend symbols $branch
+ }
+ set lod [$root lod]
+ if {![$lod istrunk]} {
+ integrity assert {
+ [$lod haschild]
+ } {Branch is LOD symbol without revisions}
+ lappend symbols $lod
+ }
+ set root [$root child]
+ }
+ }
+
+ return [list [lsort -unique -dict $revisions] [lsort -unique -dict $symbols]]
+ }
+
+
+ method AggregateSymbolData {} {
+ # Now that the exact set of revisions (and through that
+ # branches and tags) is known we can update the aggregate
+ # symbol statistics.
+
+ foreach root [$self LinesOfDevelopment] {
+ set lod [$root lod]
+
+ # Note: If the LOD is the trunk the count*, etc. methods
+ # will do nothing, as it is always present (cannot be
+ # excluded), and is always a branch too.
+
+ # Lines of development count as branches and have a commit
+ # on them (root). If they are still attached to a tree we
+ # have to compute and register possible parents.
+
+ $lod countasbranch
+ $lod countacommit
+
+ if {[$root hasparentbranch]} {
+ # Note lod == [$root parentbranch]
+ $lod possibleparents
+ } elseif {![$lod istrunk] && [$root isondefaultbranch]} {
+ # This is the root revision of a detached NTDB. We
+ # have to manually set the only possible parent for
+ # this LOD, the trunk itself.
+
+ [$lod symbol] possibleparent $mytrunk
+ }
+
+ # For the revisions in the line we register their branches
+ # and tags as blockers for the lod, and update the type
+ # counters as well. As branch symbols without commits on
+ # them are not listed as lines of development, we have to
+ # count them here as well, as plain branches. At last we
+ # have to compute and register the possible parents of the
+ # tags, in case they are later converted as branches.
+
+ while {$root ne ""} {
+ foreach branch [$root branches] {
+ $lod blockedby $branch
+ $branch possibleparents
+ if {[$branch haschild]} continue
+ $branch countasbranch
+ }
+
+ foreach tag [$root tags] {
+ $lod blockedby $tag
+ $tag possibleparents
+ $tag countastag
+ }
+
+ set root [$root child]
+ }
+ }
+
+ return
+ }
+
+ # # ## ### ##### ######## #############
+ ## Configuration
+
+ pragma -hastypeinfo no ; # no type introspection
+ pragma -hasinfo no ; # no object introspection
+ pragma -hastypemethods no ; # type is not relevant.
+
+ # # ## ### ##### ######## #############
+}
+
+namespace eval ::vc::fossil::import::cvs {
+ namespace export file
+ namespace eval file {
+ # Import not required, already a child namespace.
+ # namespace import ::vc::fossil::import::cvs::file::rev
+ # namespace import ::vc::fossil::import::cvs::file::sym
+ namespace import ::vc::tools::misc::*
+ namespace import ::vc::tools::trouble
+ namespace import ::vc::tools::log
+ namespace import ::vc::fossil::import::cvs::blobstore
+ namespace import ::vc::fossil::import::cvs::state
+ namespace import ::vc::fossil::import::cvs::integrity
+ namespace import ::vc::fossil::import::cvs::gtcore
+ }
+}
+
+# # ## ### ##### ######## ############# #####################
+## Ready
+
+package provide vc::fossil::import::cvs::file 1.0
+return