Fossil Wrapper

Artifact [7da725b5b7]
Login

Artifact 7da725b5b706727fd2882649339fb95de34585b1:


# -*-tcl-*-

# -- Aliases:

alias  .      changes
alias  d      diff
alias  ,      ui
alias  log    timeline
alias  heads  leaves;       # for hg refugees

# -- Filters:

filter status {changes status timeline add rm addremove} {
    lassign [split [string trim $line]] status
    switch $status {
        MERGED_WITH { coloured purple $line }
        MISSING     { coloured yellow $line }
        ADDED       { coloured  green $line }
        EDITED      { coloured   cyan $line }
        DELETED     { coloured    red $line }
        default     {             set  line }
    }
}

filter log_entry {leaves timeline} {
    if {[regexp "^=== .* ===" $line]} {
        coloured blue $line
    } else {
        regsub -all {\[[A-Fa-f0-9]+\]} $line [coloured yellow &]
    }
}

# Filter on alias `d' instead of `diff' so that output can be
# redirected to create patch files.
filter diff {d} {
    switch -regexp $line {
        {^-}    { coloured    red $line }
        {^\+}   { coloured  green $line }
        {^@@}   { coloured yellow $line }
        default {             set  line }
    }
}

filter highlight_branch {branch} {
    expr {[regexp {^\* } $line] ? [coloured yellow $line] : $line}
}

# record: interactive commits
#
# We add two commands which provide interactive commits:
#
# - rec, record: interactive version of 'commit'
# - rst, rstash: interactive version of 'stash' when used with 'save'
#                are 'snapshot' subcommands
# - stash: accepts a --record option which works like 'rstash'


# askrecord question ask
# - ask == yes ... accept without question
# - ask == no  ... decline without question
# - otherwise  ... ask
#
# Return values
# - y ... do record
# - n ... do not record
# - s ... skip remaining changes of this file
# - d ... skip all remaining changes
# - f ... record remaining changes of this file
# - q ... abort, do not record anything
proc askrecord {question ask} {
    if {$ask == yes} { return f }
    if {$ask == no} { return n }
    while yes {
        puts -nonewline [coloured yellow "$question \[Ynsfdaq?\] "]
        flush stdout
        gets stdin answer
        if {$answer == {}} { set answer y }
        switch $answer {
            n { return n }
            s { return s }
            f { return f }
            d { return d }
            q { return q }
            y { return y }
            a { return a }
            ? {
                puts "y - yes, record this change"
                puts "n - no, do not record this change"
                puts "s - skip remaining changes to this file"
                puts "f - record remaining changes to this file"
                puts "d - done, skip remaining changes and files"
                puts "a - record all remaining changes to all remaining files"
                puts "q - quit, record no changes"
                puts "? - help, show this message"
            }
            default { puts "ERROR: invalid command" }
        }
    }
}

# colourizehunk hunk
# - hunk: a hunk of a diff to be colourized
#
# Returns the colourized hunk
#   - removals in red
#   - additions in green
#   - info lines in magenta
proc colourizehunk {hunk} {
    set result {}
    foreach line [split $hunk "\n"] {
        lappend result \
            [switch -regexp $line {
                {^-}       { coloured     red $line }
                {^\+}      { coloured   green $line }
                {^@@}      { coloured magenta $line }
                default    {              set  line }
            }]
    }
    return [join $result "\n"]
}

# recordhunks files params
# - files is a list of filenames to be recorded
# - params are additional parameters to the underlying command (e.g.
#   'commit')
#

# The function generates a unified diff of the specified files. The
# diff is split into files and hunks. Then the function asks
# interactive questions for each file and hunk to select those hunks
# that should be committed. The not selected hunks are use to build a
# patch. The inverse of this patch is applied to the working directory
# leaving only the selected changes. Then the underlying commit
# command is executed and afterwards the patch is used to restore the
# non-selected changes.
proc recordhunks {files params} {
    set rgxsplit {^(?:Index: .*$\n=+$\n)?^--- ([^\n]*)$\n^\+\+\+ ([^\n]*)$\n((^.+$\n)*)}
    set rgxhunk {^@@.*@@$\n((^[^@I].*$\n)*)}

    # get repo root
    set workdir [pwd]
    if {[llength $files] == 0} {
        catch {exec fossil info} inf
        regexp -line {local-root:\s*(\S.*)} $inf m workdir
    }

    # get diff and split into files and hunks
    catch {exec fossil diff -i -c 1 {*}$files} diff

    set files {}
    set start 0
    set nhunks 0
    while {[regexp -start $start -indices -line $rgxsplit $diff all inf outf hunk] == 1} {
        set htxt [string range $diff {*}$hunk]
        set hunks {}
        set hstart 0
        while {[regexp -start $hstart -indices -line $rgxhunk $htxt hall] == 1} {
            lappend hunks [string range $htxt {*}$hall]
            set hstart [lindex $hall 1]
        }
        lappend files [list \
                           [string range $diff {*}$inf] \
                           [string range $diff {*}$outf] \
                           $hunks]
        incr nhunks [llength $hunks]
        set start [lindex $all 1]
    }

    # iterate through files/hunks and ask some questions
    set recfiles {}
    set norecfiles {}
    set askfile ask
    set ifile 0
    set nfiles [llength $files]
    set ihunk 0
    foreach file $files {
        lassign $file inf outf hunks

        incr ifile
        set answer [askrecord "examine file '$inf' \[$ifile/$nfiles\]?" $askfile]
        set askhunk ask
        switch $answer {
            n { set askhunk no }
            s { set askhunk no }
            f { set askhunk yes }
            d {
                set askfile no
                set askhunk no
            }
            a {
                set askfile yes
                set askhunk yes
            }
            q { return {} }
        }

        set rechunks {}
        set norechunks {}
        foreach hunk $hunks {
            incr ihunk
            if {$askhunk == "ask"} {
                puts [colourizehunk $hunk]
            }
            set answer [askrecord "record hunk \[$ihunk/$nhunks\]?" $askhunk]
            switch $answer {
                y { set dorec yes }
                n { set dorec no }
                s {
                    set dorec no
                    set askhunk no
                }
                f {
                    set dorec yes
                    set askhunk yes
                }
                d {
                    set dorec no
                    set askfile no
                    set askhunk no
                }
                a {
                    set dorec yes
                    set askfile yes
                    set askhunk yes
                }
                q { return {} }
            }
            if $dorec {
                lappend rechunks $hunk
            } else {
                lappend norechunks $hunk
            }
        }

        if {[llength $rechunks] > 0} {
            lappend recfiles [list $inf $outf $rechunks]
        }
        if {[llength $norechunks] > 0} {
            lappend norecfiles [list $inf $outf $norechunks]
        }
    }

    set fh [open fsl-record.patch w 0600]
    puts $fh $diff
    close $fh

    # build patch of changed *not* selected
    set nopatch {}
    foreach file $norecfiles {
        lassign $file inf outf hunks
        lappend nopatch "--- $inf\n"
        lappend nopatch "+++ $outf\n"
        foreach hunk $hunks {
            lappend nopatch $hunk
        }
        lappend nopatch "\n"
    }
    set nopatch [join $nopatch {}]
    set fh [open fsl-norecord.patch w 0600]
    puts $fh $nopatch
    close $fh

    set dir [pwd]
    cd $workdir
    exec patch -p0 -R << $nopatch
    catch {fossil {*}$params}
    exec patch -p0 << $nopatch
    cd $dir
    
    file delete fsl-record.patch
    file delete fsl-norecord.patch

    return {}
}

# record: interactive version of 'commit'
interceptor rec:record {
    lset params 0 commit
    set files {}
    for {set i 1} {$i < [llength $params]} {incr i} {
        set p [lindex $params $i]
        switch -regexp $p {
            {^(-m|--message|-M|--message-file|--mimetype|--bgcolor|--branch|--branchcolor|--tag)$} { incr i }
            {^-} {}
            default { lappend files $p }
        }
    }
    return [recordhunks $files $params]
}

# rstash: interactive version of 'stash save' and 'stash snapshot'
interceptor rst:rstash {
    lset params 0 stash
    if {[llength $params] <= 1} {
        return [recordhunks {} $params]
    } elseif {[regexp {^(-.*|save|snapshot)$} [lindex $params 1]] == 1} {
        set files {}
        for {set i 2} {$i < [llength $params]} {incr i} {
            set p [lindex $params $i]
            switch -regexp $p {
                {^(-m|--comment)$} { incr i }
                {^-} {}
                default { lappend files $p }
            }
        }
        return [recordhunks $files $params]
    } else {
        return $params
    }
}

# stash --record: additional argument for stash for using interactive commits
interceptor stash {
    if {[regexp -indices -- {[[:blank:]]+(--record)} $params {} rec] == 1} {
        set params [string replace $params {*}$rec]
        lset params 0 rstash
        fossil {*}$params
        return {}
    } else {
        return $params
    }
}

# vim: ft=tcl