Login
fossil-extend.fossi1ish at [752aad3eb7]
Login

File th1ish/fossil-extend.fossi1ish artifact 0fc92e7794 part of check-in 752aad3eb7


/**
    This file extends the core Fossil bits a bit.
*/
assert Fossil

Fossil.extendProperties = proc(src, dest){
    assert src && dest
    src.eachProperty(proc(k,v){
        dest.hasOwnProperty(k) || dest.set(k,v)
    })
    return dest
}

/**
  Generic Object/Array sorter, optionally recursive (will throw
  CWAL_RC_CYCLES_DETECTED for cyclic structures (or possibly
  CWAL_RC_ACCESS, depending on where it happens!)).
*/
Fossil.sorter = proc(v, recursive = false){
    v || return
    const sorter = argv.callee
    //assert sorter.isTheSorter
    const nono = (sorter.nono || (sorter.nono=object{
          'integer':true, 'bool':true, 'string': true, 'double': true,
          'hash': true, 'buffer': true
    }))
    nono.(typename v) && return
    if({function} === typename v.sortProperties){
      v.sortProperties()
    }
    if({function} === typename v.sort){
      v.sort()
    }
    recursive || return
    if({function} === typename v.eachProperty){
      const eachProp = (sorter.eachProp || (sorter.eachProp=proc(k,v){
            sorter(v, true)
      }.importSymbols(nameof sorter)))
      v.eachProperty(eachProp)
    }
    if({function} === typename v.eachIndex){
      const eachIndex = (sorter.eachIndex || (sorter.eachIndex=proc(v){
            sorter(v, true)
      }.importSymbols(nameof sorter)))
      v.eachIndex(eachIndex)
    }
}
//Fossil.sorter.isTheSorter = true // for checking a th1ish bugfix

/**
    createContext() extends the Context constructor
    to support more options. obj may be a falsy value
    or an Object with any of these properties:

    repoDb: path to repository DB. Default is ARGV.flags.R.

    traceSql: if true, enable SQL tracing on the context.
    Default is ARGV.flags.S.

    checkout: a path to look for a checkout in. Not used if repoDb
    is set. Default is getenv('PWD') unless ARGV.flags.C is truthy,
    in which case no checkout is opened by default.
*/
Fossil.createContext = function(obj = null){
    if(!obj){
        const rdb = ARGV.flags.R
        obj = object{
            repoDb: rdb,
            traceSql: !!ARGV.flags.S,
            checkout: rdb ? undefined
                          : (ARGV.flags.C ? undefined : getenv('PWD'))
        }
    }
    var f = this.Context(obj)
    obj || return f
    if('string'===typename obj.repoDb){
        [f.openRepo obj.repoDb]
        // run it again to test an exception:
        //[f.openRepo obj.repoDb]
    }
    else if('string'===typename obj.checkout){
        [f.openCheckout obj.checkout]
        // run it again to test an exception:
        //[f.openCheckout obj.checkout]
    }
    return f
}

/**
  Counterpart of the C-level fsl_catype_t enum.
*/
Fossil.artifactTypes = object{
    ANY: 0,
    CHECKIN: 1,
    CLUSTER: 2,
    CONTROL: 3,
    WIKI: 4,
    TICKET: 5,
    ATTACHMENT: 6,
    EVENT: 7
}

/**
  SELECT's the first column of the first row
  of the result set of the given SQL.
  If the bind argument is passed in then
  it is passed on to the Stmt.bind() method
  of the underlying statement. Use an array to
  bind multiple values. If dflt is set, it is
  returned if no row is found.
*/
Fossil.Context.selectVal = proc(sql,bind,dflt){
    const st = [this.db.prepare sql]
    var rc = dflt
    const ex = catch {
        if(1<argv.length()){
            if('array' === typename bind){
                st.bind(bind)
            }else{
                st.bind(1,bind)
            }
        }
        [st.step] && (rc = [st.get 0])
    }
    [st.finalize]
    ex ? throw ex : return rc;
}

/**
    Given a SELECT-style query and optional bind parameters
    (either a single value for a single param or an array
    of multiple params), this routine simply dumps out
    the results of the query.
*/
Fossil.Context.dumpQuery = proc(sql,bind, separator='\t'){
    const params object{
        sql:sql,
        bind:bind, // note that undefined value is treated as non-existent here
        callback:proc(){
            (1===rowNumber) && [print [columnNames.join separator]]
            [print [this.join separator]]
        }
    }
    [this.db.each params]
}

/**
    Stmt.each() loops over this.step(),
    calling func(this, N) on each iteration,
    where N is the current row number (1-based).
    If func() returns a literal false, looping
    stops without an error.
*/
Fossil.Db.Stmt.each = proc(func){
    var rowNum = 0
    var rc
    while(this.step()){
        (false === func(this, rowNum += 1)) && break
    }
}

/**
    Some framework-level utility code for apps...
*/
Fossil.client = object{
  config: object{
    projectUrl: ARGV.flags.'repo-url' || {http://pass-in-the-repo-url=URL-flag}
  },
  urlToProject: proc(path){
    return path ? (this.config.projectUrl + path) : this.config.projectUrl
  },
  linkToProject: proc(path,label){
    const url = this.urlToProject(path)
    return {<a href='}.concat(url, {'>}, label || url, {</a>})
  }
}

/**
  If passed no args, returns this instance's private cache
  storage (an Object). If passed 1 arg, returns the value
  of cache.(argv.0). Otherwise is stores the value of argv.1
  into the cache using the key argv.0 and returns the
  value it cached.
*/
Fossil.Context.cache = proc(){
    const c = (this._cache || (this._cache = object{}))
    const argc = argv.length()
    argc || return c
    (1==argc) && return c.(argv.0)
    return c.(argv.0) = argv.1
}

/**
  Returns the current repository's 'project-name' config value. The
  result is cached the first time it is succesfully fetched.
*/
Fossil.Context.getProjectName = proc(){
    var rc = this.cache('project-name')
    if(!rc){
      rc = this.selectVal({
           SELECT value FROM config WHERE name='project-name'
      })
      rc && this.cache('project-name', rc)
    }
    return rc
}

/**
    Changes to the given dir and pushes the (old) current dir
    to the directory stack. To change back to the pre-pushd()
    directory, call Fossil.file.popd(). This function relies
    on this 'this' object beeing Fossil.file.

    The array of pushed directory names is available after calling
    this function one time via the property
    Fossil.file.pushd.dirStack. The most recent directory is at the
    end of that array. If that property is undefined, pushd() has
    never been called. If it is an empty array, there are currently
    no directories in the stack.
*/
Fossil.file.pushd = proc(dir){
    assert 'string' === typename dir
    const curdir = this.currentDir()
    this.chdir(dir)
    const li = argv.callee.dirStack || (argv.callee.dirStack = array[])
    li.push( curdir )
}

/**
    Pops the directory mostly recently pushed by Fossil.file.pushd()
    off of the directory stack and changes to that directory. Throws
    if called when no directories can be popped.
*/
Fossil.file.popd = proc(){
    const list = this.pushd.dirStack
    const len = (list ? list.length() : 0)
    len || throw api.Exception(Fossil.rc.MISUSE,"No directory stack to pop.")
    const dir = list.(len-1)
    this.chdir(dir)
    return list.pop()
}