A tiny version control

Description

Richard Suchenwirth 2006-01-30: This Sunday fun project was created because I had no fun on Saturday - coding merrily for Sepp, I suddenly reached a point when the NoteBook widget did not react, and I wasn't exactly clear what changes since the "last know good" state had caused that. At work, I use CVS for version control, so it's easy to compare the current with earlier versions - but on the cellphone? So I decided I need some version control.

At currently 61 lines of code, this is of course a far cry from the "real things" like CVS. It's also less efficient in memory - previous versions are stored fully, not as diff. (But this is not so much of a concern, when pocket source files tend to be short anyway, not much more than a K or two.) For a version-controlled file foo.bar, there are in the same directory copies named foo#0.bar, foo#1.bar etc. (somehow like on old VAX/VMS), the highest version number being the HEAD. If the "top" instance (the one without #) is equal to HEAD, it is considered up-to-date.

Usage:

ver commit filename 

Copies the top to new head, one higher than the current, if not up-to-date. (Also does cvs add-like: starts at 0 when the file wasn't versioned yet)

ver diff filename ?n?

Compares the top with the given numbered version (HEAD if no n given) line by line, displays the first three differences. (This is still crude, but already helpful on little changes). Returns "identical" if so, else {}.

ver status filename

Returns "up-to-date with #x", "locally modified - last: #x", or "nothing known" when there are no numbered versions.

ver tell filename

Returns the sorted list of available version numbers for the given top file, HEAD first.

proc ver {cmd {filename ""} {n ""}} {
    set c [info procs ver::$cmd*]
    if {[llength $c] != 1} {error "unknown $cmd - use one of: [ver::_cmds]"}
    $c $filename $n
}

namespace eval ver { }

proc ver::commit {fn} {
    set last [_head $fn]
    if {[_equal $fn "" $last] && $last ne ""} {
        return up-to-date
    }
    if {$last eq ""} {set last -1}
    file copy $fn [_fmt $fn [incr last]]
    set last
}
proc ver::diff {fn {n ""}} {
    if {$n eq ""} {set n  [_head $fn]}
    if {$n eq ""} {return "nothing to diff"}
    set f1 [open $fn]
    set f2 [open [_fmt $fn $n]]
    set diff 0
    while {[gets $f1 a]>=0 \
        && [gets $f2 b]>=0} {
        if {$a ne $b} {
            puts "< $a\n> $b"; incr diff
            if {$diff>3} break
        }
    }
    close $f1; close $f2
    if !$diff {return identical}
}
proc ver::status {fn {n ""}} {
    set last [_head $fn]
    if {$last eq ""} {return "nothing known"}
    if [_equal $fn "" $last] {
        return "up-to-date with #$last"
    } else {return "locally modified - #$last"}
}
proc ver::tell {fn {n ""}} {
    set res {}
    foreach f [glob -noc [_fmt $fn *]] {
        regexp {#(.+)\.} $f -> no
        lappend res $no
    }
    lsort -integer -dec $res
}
#---------------------------- Internal functions start with _
proc ver::_cmds {} {
    lsort [string map {::ver:: ""} \
        [info procs {::ver::[a-z]*}]]
}
proc ver::_equal {fn v1 v2} {
    expr {[file mtime [_fmt $fn $v1]]\
         ==[file mtime [_fmt $fn $v2]]}
}
proc ver::_fmt {fn n} {
    if {$n eq ""} {return $fn}
    return [file root $fn]#$n[file ext $fn]
}
proc ver::_head fn {lindex [tell $fn] 0}