patch for rcs in tcl

A quickie to apply patches generated by diff -n (so-called rcs format) - CMcC 20041006

# patch a file given diffs in rcs (diff -n) format
package require fileutil

namespace eval rcs {}

# convert \n delimited file to an array indexed by line name
proc rcs::file2array {filename arr} {
    upvar 1 $arr lines
    set lnum 0
    fileutil::foreachLine line $filename {
    set lines([incr lnum]) $line
    }
}

# convert \n delimited text to an array indexed by line name
proc rcs::text2array {text arr} {
    upvar 1 $arr lines
    set lnum 0
    foreach line [split $text \n] {
    set lines([incr lnum]) $line
    }
}

# Apply some rcs diff -n format patches to the text in array
proc rcs::patch {patch arr} {
    upvar 1 $arr lines
    set patch [split $patch \n]
    while {$patch != {}} {
    set pc [string trim [lindex $patch 0]]
    puts stderr "doing $pc"
    set patch [lrange $patch 1 end]
    switch -glob -- $pc {
        "" {}
        a* {
        foreach {start len} [split [string range $pc 1 end]] break
        set adding [join [lrange $patch 0 [expr {$len - 1}]] \n]
        if {[info exists lines($start)]} {
            append lines($start) \n
        }
        append lines($start) "$adding"
        puts stderr "ADD: '$adding'"
        set patch [lrange $patch $len end]
        puts stderr "$pc: $lines($start)"
        }
        d* {
        foreach {start len} [split [string range $pc 1 end]] break
        while {$len > 0} {
            puts stderr "DEL $start: $lines($start)"
            unset lines($start)
            incr start
            incr len -1
        }
        }
        default {
        error "Unknown patch: '$pc'"
        }
    }
    }

    set result ""
    foreach lnum [lsort -integer [array names lines]] {
    append result \n $lines($lnum)
    }
    
    return [string range $result 1 end]
}

Below comes a version to apply a patch to a directory tree for the most widely used unified context diff format, generated by diff -ruN old/ new/ This performs the analogous task of patch -p<striplevel> < patch

##        Apply a patch in unified diff format
# @synopsis{Patch directory striplevel patch}
#
#
# @param[in] dir        root directory of the original source tree
# @param[in] striplevel number of path elements to be removed from the diff header
# @param[in] patch      output of diff -ru

proc ApplyPatch {dir striplevel patch} {
        set patchlines [split $patch \n]
        set inhunk false
        set oldcode {}
        set newcode {}
        
        for {set lineidx 0} {$lineidx<[llength $patchlines]} {incr lineidx} {
                set line [lindex $patchlines $lineidx]
                if {[string match diff* $line]} {
                        # a diff block starts. Next two lines should be
                        # --- oldfile date time TZ
                        # +++ newfile date time TZ
                        incr lineidx
                        set in [lindex $patchlines $lineidx]
                        incr lineidx
                        set out [lindex $patchlines $lineidx]

                        if {![string match ---* $in] || ![string match +++* $out]} {
                                puts $in
                                puts $out
                                return -code error "Patch not in unified diff format, line $lineidx $in $out"
                        }

                        # the quoting is compatible with list
                        lassign $in -> oldfile
                        lassign $out -> newfile

                        set fntopatch [file join $dir {*}[lrange [file split $newfile] $striplevel end]]
                        set inhunk false
                        #puts "Found diffline for $fntopatch"
                        continue
                }

                # state machine for parsing the hunks
                set typechar [string index $line 0]
                set codeline [string range $line 1 end]
                switch $typechar {
                        @ {
                                if {![regexp {@@\s+\-(\d+),(\d+)\s+\+(\d+),(\d+)\s+@@} $line \
                                        -> oldstart oldlen newstart newlen]} {
                                        return code -error "Erroneous hunk in line $lindeidx, $line"
                                }
                                # adjust line numbers for 0-based indexing
                                incr oldstart -1
                                incr newstart -1
                                #puts "New hunk"
                                set newcode {}
                                set oldcode {}
                                set inhunk true
                        }
                        - { # line only in old code
                                if {$inhunk} {
                                        lappend oldcode $codeline
                                }
                        }
                        + { # line only in new code
                                if {$inhunk} {
                                        lappend newcode $codeline
                                }
                        }
                        " " { # common line
                                if {$inhunk} {
                                        lappend oldcode $codeline
                                        lappend newcode $codeline
                                }
                        }
                        default {
                                # puts "Junk: $codeline";
                                continue
                        }
                }
                # test if the hunk is complete
                if {[llength $oldcode]==$oldlen && [llength $newcode]==$newlen} {
                        set hunk [dict create \
                                oldcode $oldcode \
                                newcode $newcode \
                                oldstart $oldstart \
                                newstart $newstart]
                        #puts "hunk complete: $hunk"
                        set inhunk false
                        dict lappend patchdict $fntopatch $hunk
                }
        }

        # now we have parsed the patch. Apply
        dict for {fn hunks} $patchdict {
                puts "Patching file $fn"
                if {[catch {open $fn} fd]} {
                        set orig {}
                } else {
                        set orig [split [read $fd] \n]
                }
                close $fd

                set patched $orig

                set fail false
                set already_applied false
                set hunknr 1
                foreach hunk $hunks {
                        dict with hunk {
                                set oldend [expr {$oldstart+[llength $oldcode]-1}]
                                set newend [expr {$newstart+[llength $newcode]-1}]
                                # check if the hunk matches
                                set origcode [lrange $orig $oldstart $oldend]
                                if {$origcode ne $oldcode} {
                                        set fail true
                                        puts "Hunk #$hunknr failed"
                                        # check if the patch is already applied
                                        set origcode_applied [lrange $orig $newstart $newend]
                                        if {$origcode_applied eq $newcode} {
                                                set already_applied true
                                                puts "Patch already applied"
                                        } else {
                                                puts "Expected:\n[join $oldcode \n]"
                                                puts "Seen:\n[join $origcode \n]"
                                        }
                                        break
                                }
                                # apply patch
                                set patched [list {*}[lrange $patched 0 $newstart-1] {*}$newcode {*}[lrange $patched $oldend+1 end]]
                        }
                        incr hunknr
                }

                if {!$fail} {
                        # success - write the result back
                        set fd [open $fn w]
                        puts -nonewline $fd [join $patched \n]
                        close $fd
                }
        }
}