Login
DeckDiff.fossi1ish at [752aad3eb7]
Login

File th1ish/DeckDiff.fossi1ish artifact d9e298d90f part of check-in 752aad3eb7


/**
    This module evaluates to the an object which contains
    utility functions for calculating differences between
    checkin versions.
*/
(proc(){

    /**
        Internal impl of DeckDiff.calcCheckinChangeInfo().
        See that function (below) for the main docs and the
        structure of the returned object.

        deck1 and deck2 must be Manifest objects loaded
        with Fossil.Context.loadManifest(..., true) (so that
        each Manifest contains its whole file list, even if it's
        a delta manifest).
    */
    const calcDeckDiffs = proc(deck1, deck2){
        var isManifest = proc(v){
            return v && (v.uuid && v.rid && v.type)
        }
        assert isManifest(deck1)
        assert isManifest(deck2)
        unset isManifest
        const rc = object{
            v1: deck1.uuid,
            v2: deck2.uuid,
            changes:array[]
        }
        const f1 = deck1.F ? deck1.F : array[];
        const f2 = deck2.F ? deck2.F : array[];
        var f1len = f1.length()
        var f2len = f2.length();
        const n1 = api.Hash( f1len * 2 )
        const n2 = api.Hash( f2len * 2 )

        /**
            The basic strategy:

            - put all names from f1 into hashtable n1.

            - iterate over f2 and calculate changes based on existence
            of f2.(N).name's existence in n1.
        */
        f1.eachIndex(proc(v,i){ n1.insert(v.name, v) }.importSymbols('n1'))

        $f2.eachIndex proc(fc2,i){
            n2.insert(fc2.name, fc2)
            var fc1 = n1.search(fc2.name)
            if(!fc1){
                fc2.priorName && (fc1 = n1.search(fc2.priorName))
                var row = object{
                    name: fc2.name,
                    uuid: fc2.uuid,
                    perm: fc2.perm
                }
                if(fc1){
                    n1.remove(fc2.priorName)
                    row.status = 'renamed'
                    row.priorName = fc1.name
                    row.priorContent = fc1.uuid
                }else{
                    row.status = 'added'
                }
                //row.sortProperties()
                ch.push(row)
                return
            }
            n1.remove(fc2.name)
            (fc1.uuid === fc2.uuid) && return
            const row = object{
                name: fc2.name,
                uuid: fc2.uuid,
                perm: fc2.perm,
                priorContent: fc1.uuid
            }
            ch.push(row)
            if( !fc2.uuid ){
                row.status = 'removed'
                const p1 = n1.search(fc1.name)
                assert p1
                row.priorContent = p1.uuid
                if(p1.name !== fc1.name){
                    throw {i do not think this is actually possible}
                    fc1.priorName = p1.name
                }
            }else if(fc2.priorName){
                row.status = 'renamed'
                row.priorName = fc2.priorName
                //const p1 = n1.search(fc2.priorName)
                //assert p1
            }else{
                row.status = 'modified'
            }
            //row.sortProperties()
        }.importSymbols('n1', 'n2', ((const ch = rc.changes), 'ch'))

        // Pick up any entries in the parent but not the child...
        $n1.eachEntry proc(name,fc1){
            var fc2 = n2.search(fc1.name)
            if(!fc2){
                const row = object{
                    name: fc1.name,
                    priorContent: fc1.uuid,
                    status: 'removed'
                }
                ch.push(row)
                return
            }
        }.importSymbols('n2', ((const ch = rc.changes), 'ch'))

        rc.changes.sort(proc(l,r){
            return l.name.compare(r.name)
        })
        return rc
    }

    const uuidStr = proc(u){ return u.substr(0,12) }
    const dlLink = proc(fn, uuid){
        const CGI = api.cgi
        const url = CGI.cgiRoot.concat(
            'download/uuid/', uuid,
            '?name=', CGI.urlencode(fn))
        return CGI.html.createAnchor(url, uuidStr(uuid))
    }.importSymbols('uuidStr')
    
    const DeckDiff = object{
        __typename: 'DeckDiff',
        out: api.io.output,
        /**
            Calculates file-level changes between two versions of
            a checkin.
        
            Requires f to be a Fossil.Context instance and v1/v2
            to be checkin versions to calculate file diffs for. v1 is
            the "older" version and v2 the newer.

            v1 and v2 may be symbolic names, RIDs (integers), or
            manifests loaded via Fossil.Context.loadManifest(). If
            they are Manifest objects and are delta manifests, they
            _must_ have been loaded with their baseline files
            included in their own F-card list (the 2nd parameter to
            Fossil.Context.loadManifest() must be true) or else the
            diffs will probably be somewhat unexpected! Proper
            traversal of files between a delta manifest and its
            baseline is intricate, and this routine does not do it -
            it assumes that any decks passed to it have their F-card
            list populated as if they were not delta manifests.

            If v1 is provided and v2 is not then the diff is calculated
            based on (v2,v1), where v2 is the parent checkin of v2.

            Example: to get a diff of the current and previous versions,
            pass (f,'current') to this function. Note that the special
            'current' and 'prev' symbolic names rely on an opened
            checkout.

            Returns an Object with this structure:
            
            {
                v1: "full UUID of v1",
                v2: "full UUID of v2",
                changes: array[
                    {
                        name: "filename",
                        uuid: "UUID of v2 file content.",
                        priorContent: "UUID of v1 file content", // if any
                        priorName: "oldName", // only if renamed
                        status: "added|renamed|modified|removed",
                        perm: "x|l|f" // eXecuable, symLink, or File
                    },
                    ...one for each change...
                ]
            }

            The changes array holds the list of files (sorted
            alphabetically by file name)
            changed/added/removed/renamed between the two versions.
            If a file is both renamed a modified, its status will be
            "renamed" and its priorContent will hold the UUID of its
            old content. A renamed file without a priorContent UUID
            (or where priorContent==uuid) was renamed without its
	    contents being modified in the same checkin.            
        */
        calcCheckinChangeInfo: proc(f, v1, v2){
            assert f inherits Fossil.Context
            var d1, d2
            if('object'===typename v1){
                d1 = v1
            }else if(v1){
                d1 = f.loadManifest(v1, true, false)
            }else{
                throw {Expecting v1 argument to be a version value or manifest object.}
            }
            if(!v2 && d1.P && d1.P.0){
                v2 = d1
                d1 = f.loadManifest(d1.P.0, true, false)
            }
            if('object'===typename v2){
                d2 = v2
            }else if(v2){
                d2 = f.loadManifest(v2, true, false)
            }else {
                throw {Expecting v2 argument to be a version value or manifest object.}
            }
            return (d2 && d2) ? calcDeckDiffs(d1, d2) : undefined
        }.importSymbols('calcDeckDiffs'),
        /**
            Parameters:

            f: a Fossil.Context instance

            changes: the result of calling this.calcCheckinChangeInfo()

            Requires api.cgi.

            diffOpt: If diffOpt is-a Object then it may contain
            properties to control the output of the diff. If it is a
            non-object value then defaults are used.  Any properties
            not specified in diffOpt get defaults, mostly taken from
            CGI-provided variables:

            {
                // Number of lines of context around changes:
                contextLines: +api.cgi.getVar('contextLines', 5),

                // Disables HTML output, generating a plain text diff
                text: +api.cgi.getVar('text', 0),

                // Specifies the column width for TEXT-mode side-by-side diffs.
                // Has no effect in HTML mode!
                sbsWidth: +api.cgi.getVar('sbsWidth', 80),

                // Swaps the left/right sides of the diffs (but not the labels!)
                invert: +api.cgi.getVar('invert'),

                // Disables side-by-side (sbs) mode
                inline: +api.cgi.getVar('inline', 0)
            }
        */
        showFileDiffsFromChangeInfo: proc(f,changes, diffOpt){
            assert f inherits Fossil.Context
            assert 'object' === typename changes
            const cha = changes.changes
            assert 'array' === typename cha
            if(0){
                $out "Raw change data:<br/>"
                $out {<textarea rows="30" cols="80" readonly>}
                CGI.printJSON(changes)
                $out {</textarea>}
            }
            cha.length() || return
            const out = this.out
            const CGI = api.cgi
            const defaultDiffOpt = object {
                html: 1,
                text: +CGI.getVar('text', 0),
                sbsWidth: +CGI.getVar('sbsWidth', 80), // has no effect in HTML mode!
                contextLines: +CGI.getVar('contextLines', 7),
                invert: +CGI.getVar('invert'),
                inline: +CGI.getVar('inline', 0)
            }
            diffOpt = Fossil.extendProperties(
                        defaultDiffOpt,
                        ('object'===typename diffOpt) ? diffOpt : object{}
                      )
            diffOpt.text && unset diffOpt.html
            diffOpt.inline && unset diffOpt.sbsWidth
            
            //diffOpt.sortProperties(); print(diffOpt)
            
            $cha.eachIndex proc(v,i){
                $out {<div>}
                $out {<h2>}
                if(v.priorContent){
                    $out {[<span class='uuid'>} dlLink(v.priorName || v.name, v.priorContent) {</span>]} ' → '
                }
                if(v.uuid){
                    $out ' ' {[<span class='uuid'>} dlLink(v.name, v.uuid) {</span>]}
                }
                $out ' ' {<span class='filename strong'>} v.name {</span>}
                $out {</h2>}

                if(v.priorName){
                    $out '<div>Renamed from: ' {<span class='filename'>} v.priorName {</span></div>}
                }
                //$out ' ' {<span class='filename strong'>} v.name {</span>}

                if(!v.uuid){
                    $out "Removed."
                }else{
                    if(v.priorContent){
                        $out {<div class='artifact-diff'>}
                        const diff = catch {
                            toss f.artifactDiff(v.priorContent, v.uuid, diffOpt)
                        }
                        if(diff inherits api.Exception){
                            $out "Error generating diff. "
                            if(Fossil.rc.TYPE === diff.code){
                                $out "Perhaps it's binary content."
                            }else{
                                $out CGI.toJSONString(CGI.scrubException(diff))
                            }
                        }else{
                            if(diffOpt.html){
                                $out diff
                            }else{
                                $out CGI.htmlEscape(diff)
                            }
                        }
                        $out {</div>}
                    }else{
                        $out "New file. Content: <span class='uuid'>" dlLink(v.name, v.uuid) "</span>"
                    }
                }
                $out {</div>}
            }/*showFileDiffsFromChangeInfo()*/.importSymbols('f', 'CGI', 'diffOpt', 'out', 'dlLink', 'uuidStr')
        }.importSymbols('dlLink', 'uuidStr'),
        /**
            Lists an overview of changes made between a checkin and
            its parent. Requires api.cgi.
        
            Params:

            f: a Fossil.Context instance from which...

            deck: a Manifest "deck" loaded via f.loadManifest() OR
            a symbolic name/UUID of a checkin version.

            diffOpt: if a falsy value, no file-level diffs are shown. If
            a truthy value, see showFileDiffsFromChangeInfo() for
            the details.
        */
        showDeckFileChangeList: proc(f, deck, diffOpt = false){
            assert f inherits Fossil.Context
            (deck.P && deck.P.0) || return
            const out = this.out
            const CGI = api.cgi
            if(deck.B){
                deck = deck.uuid /* have to re-load to ensure that all (incl. inherited) F-cards are loaded */
            }
            const changes = this.calcCheckinChangeInfo(F, deck)
            (changes.changes && changes.changes.length()) || return

            const statMap = object {
                added: '+',
                modified: '~',
                removed: '-'
                renamed: '→'
            }

            $out {<div>File changes: <span class='small-text'>(legend:}
            statMap.sortProperties()
            var counter = 0
            $statMap.eachProperty proc(k,v){
                counter && out(',')
                out(' ',v,' = ',k)
                counter += 1
            }.importSymbols('out')
            $out {)</span></div>}
            $out {<div class='manifest-file-change-list'>}

            Fossil.sorter(changes.changes, true)
            $changes.changes.eachIndex proc(v){
                $out {<div>}
                var pre = statMap.(v.status)
                pre && $out {<span class='monospace'>} pre {</span>} ' '
                unset pre

                if(v.uuid){
                    $out {[<span class='uuid'>}
                    const url = CGI.cgiRoot.concat(
                        'download/uuid/', v.uuid,
                        '?name=', CGI.urlencode(v.name))
                    $out CGI.html.createAnchor(url, uuidStr(v.uuid))
                    $out {</span>]}
                }

                $out ' ' {<span class='filename'}
                $out " title='" v.uuid "'>"
                if('renamed'===v.status){
                    assert v.priorName
                    $out v.priorName ' → '
                }
                $out v.name
                $out {</span>}
                $out {</div>}
            }.importSymbols('statMap', 'out', 'CGI', 'dlLink', 'uuidStr')
            $out {</div><!-- .manifest-file-change-list -->}

            diffOpt && this.showFileDiffsFromChangeInfo(f, changes, diffOpt)
            //showFileDiffs(changes)

            return
            /*$out "Raw change data:<br/>"
            $out {<textarea rows="30" cols="80" readonly>}
            CGI.printJSON(changes)
            $out {</textarea>}*/
        }.importSymbols('dlLink', 'uuidStr')/*showDeckFileChangeList()*/
    }/*DeckDiff*/


    0 && (!api.cgi) && scope { /* test code */
        api.import('fossil-extend')
        const F = Fossil.createContext()
        //print(typename dd)
        var v1, v2
        //v1 = 'd73e9a278e' v2 = '9b886baf47'
        //v1 = '684229ae618e' v2 = '22d1ad1a3b6d'
        //v1 = 'f0620d926e4b' v2 = '68fbc6aeb3fb'
        //v1 = '64c47b71d0c11ffef' v2 = '9b886baf47a29e'
        //v1 = '8d1ab6166d' v2 = '1f304f5f35'
        v1 = '5fbaf6df0a4a73e56' v2 = 'de941e7bf373cfab1'
        //v1 = 'prev' v2 = 'current'

        const x = DeckDiff.calcCheckinChangeInfo(F, v1, v2)
        //d73e9a278ee4ff2f439b303c75b55c64a42f076b
        //9b886baf47a29e0336e16d39d05731f6bcc0bb29
        //Fossil.sorter(x, true)
        print("%1$-1J".applyFormat(x))
    }

    return DeckDiff
})()