#! /usr/bin/python3
'''
Usage: %(appname)s [-hVvd] PRJ
       %(appname)s [-hVvd] OLDPRJ NEWPRJ
       -h, --help           this message
       -V, --version        print version and exit
       -v, --verbose        verbose mode (cumulative)
       -d, --diff           produce diff files
       -m, --meta           compare meta data

The program operates in two modes:

Compare all packages in PRJ with local tree, run in checked out
project tree (osc diff).

Compare all packages of NEWPRJ with packages in OLDPRJ,
given they exist in both projects (osc rdiff). If you run this
in an obs tree, be aware, that osc fetches the server from it.

Copyright:
(c)2019-2020 by %(author)s

License:
%(license)s
'''
#
# vim:set et ts=8 sw=4:
#

__version__ = '0.3'
__author__ = 'Hans-Peter Jansen <hpj@urpla.net>'
__license__ = 'GNU GPL 2 - see http://www.gnu.org/licenses/gpl2.txt for details'


import os
import sys
import getopt
import logging
import logging.handlers
import subprocess


class gpar:
    ''' global parameter class '''
    appdir, appname = os.path.split(sys.argv[0])
    if appdir == '.':
        appdir = os.getcwd()
    if appname.endswith('.py'):
        appname = appname[:-3]
    version = __version__
    author = __author__
    license = __license__
    loglevel = logging.ERROR
    oldrepo = []
    newrepo = []
    diff = False
    meta = False


log = logging.getLogger(gpar.appname)

# we need encoding failure tolerant i/o handling
seutf8 = lambda s: s.decode(encoding = 'utf-8',
                            errors = 'surrogateescape')
seopen = lambda fd: open(fd, 'w',
                         encoding = 'utf-8',
                         errors = 'surrogateescape',
                         closefd = False)

sys.stdout = seopen(1)
sys.stderr = seopen(2)

stdout = lambda *s: print(*s, file = sys.stdout, flush = True)
stderr = lambda *s: print(*s, file = sys.stderr, flush = True)


def exit(ret = 0, msg = None, usage = False):
    ''' terminate process with optional message and usage '''
    if msg:
        stderr('%s: %s' % (gpar.appname, msg))
    if usage:
        stderr(__doc__ % gpar.__dict__)
    sys.exit(ret)


def setup_logging(loglevel):
    ''' setup various aspects of logging facility '''
    logconfig = dict(
        level = loglevel,
        format = '%(asctime)s %(levelname)5s: %(message)s',
        datefmt = '%Y-%m-%d %H:%M:%S',
    )
    logging.basicConfig(**logconfig)


def osc(*args):
    ''' run osc '''
    cmd = ['osc']
    if gpar.loglevel <= logging.DEBUG:
        cmd.append('--debug')
    cmd.extend(args)
    log.debug('run: %s', ' '.join(cmd))
    # in order to handle encoding errors correctly,
    # we convert with surrogateescape manually
    try:
        res = subprocess.run(cmd,
                             check = True,
                             capture_output = True)
    except subprocess.CalledProcessError as e:
        log.debug('error: command returned %s:\n%s',
                  e.returncode, seutf8(e.stderr))
        return None
    else:
        if res.stdout:
            res.stdout = seutf8(res.stdout)
            log.debug(res.stdout)
        return res.stdout


def osc_ls_repo(repo):
    ''' run osc ls, return a list of packages '''
    res = osc('ls', repo)
    if res:
        # filter out empty lines
        return [r for r in res.split('\n') if r]


def osc_diff(meta = False):
    ''' run osc diff, return diff output '''
    args = ['diff']
    if meta:
        args.append('--meta')
    return osc(*args)


def osc_rdiff(oldprj, oldpkg, newprj, newpkg, meta = False):
    ''' run osc rdiff, return diff output '''
    args = ['rdiff']
    if meta:
        args.append('--meta')
    args.extend((oldprj, oldpkg, newprj, newpkg))
    return osc(*args)


def prjdiff(prj):
    ''' run osc diff on all packages in this project,
        that exist locally and remote
    '''
    gendiff = gpar.diff
    meta = gpar.meta

    log.info('fetch: %s', prj)
    pkglst = osc_ls_repo(prj)
    if not pkglst:
        exit(3, 'no packages in %s' % prj)

    for pkg in pkglst:
        log.info('process %s', pkg)
        cwd = os.getcwd()
        if not os.path.isdir(pkg):
            log.warning('%s doesn\'t exist in %s' % (pkg, cwd))
        else:
            os.chdir(pkg)
            diff = osc_diff(meta)
            if diff:
                if gendiff:
                    difffile = '%(prj)s-%(pkg)s.diff' % locals()
                    try:
                        open(difffile, 'w', encoding = 'utf-8').write(diff)
                    except OSError:
                        log.exception('cannot write %s:', difffile)
                else:
                    stdout('Differences in %s/%s:\n%s' % (prj, pkg, diff))
            os.chdir(cwd)

    return 0


def prjdiff2(oldprj, newprj):
    ''' run osc rdiff on all packages, that exist in both projects '''
    gendiff = gpar.diff
    meta = gpar.meta

    log.info('fetch: %s', oldprj)
    oldpkg = osc_ls_repo(oldprj)
    if not oldpkg:
        exit(3, 'no packages in %s' % oldprj)

    log.info('fetch: %s', newprj)
    newpkg = osc_ls_repo(newprj)
    if not newpkg:
        exit(3, 'no packages in %s' % newprj)

    for pkg in newpkg:
        log.info('process %s', pkg)
        if not pkg in oldpkg:
            log.warning('%s doesn\'t exist in %s' % (pkg, oldprj))
        else:
            diff = osc_rdiff(oldprj, pkg, newprj, pkg, meta)
            if diff:
                if gendiff:
                    difffile = '%(oldprj)s-%(newprj)s-%(pkg)s.diff' % locals()
                    try:
                        open(difffile, 'w', encoding = 'utf-8').write(diff)
                    except OSError:
                        log.exception('cannot write %s:', difffile)
                else:
                    stdout('Differences of %s/%s and %s/%s:\n%s'
                           % (oldprj, pkg, newprj, pkg, diff))

    return 0


def main():
    ''' Command line interface and console script entry point '''
    try:
        optlist, args = getopt.getopt(sys.argv[1:], 'hVvdm',
            ('help', 'version', 'verbose', 'diff', 'meta')
        )
    except getopt.error as msg:
        exit(1, msg, True)

    for opt, par in optlist:
        if opt in ('-h', '--help'):
            exit(usage = True)
        elif opt in ('-V', '--version'):
            exit(msg = 'version %s' % gpar.version)
        elif opt in ('-v', '--verbose'):
            if gpar.loglevel > logging.DEBUG:
                gpar.loglevel -= 10
        elif opt in ('-d', '--diff'):
            gpar.diff = True
        elif opt in ('-m', '--meta'):
            gpar.meta = True

    setup_logging(gpar.loglevel)

    try:
        if len(args) == 1:
            return prjdiff(*args)
        elif len(args) == 2:
            return prjdiff2(*args)
        else:
            exit(2, 'invalid argument count: either one or two expected')
    except KeyboardInterrupt:
        exit(5, 'Canceled')
    except:
        log.exception('internal error:')


if __name__ == '__main__':
    exit(main())

