#!/usr/bin/env python

# gob.py - ooo-build to GIT conversion:  dump gob branch description files
#          and create branched git
#
# Usage: python bin/gob --help

import optparse
import operator
import os
import re
import sys
import shutil

class SystemFailed (Exception):
    pass

def log (s, threshold=1):
    if options.verbose > threshold:
        print >> sys.stderr, s

def info (s):
    log (s, threshold=0)

def exception_string (exception=Exception ('no message')):
    import traceback
    return traceback.format_exc (None)

def filter_out (predicate, lst):
    return filter (lambda x: not predicate (x), lst)

def makedirs (dir):
    log ('mkdir %(dir)s' % locals ())
    os.makedirs (dir)

def symlink (src, dest):
    log ('ln -s %(src)s %(dest)s' % locals ())
    os.symlink (src, dest)

def rename (src, dest):
    log ('mv %(src)s %(dest)s' % locals ())
    os.rename (src, dest)

def rmdir (dir):
    log ('rmdir %(dir)s' % locals ())
    os.rmdir (dir)

def system (command, raise_on_error=True):
    if options.verbose > 1:
        log ('executing: %(command)s' % locals ())
    if options.verbose < 3:
        command = '(%(command)s) > gob.log 2>&1' % locals ()
    status = os.system (command)
    if status and raise_on_error:
        info (command)
        info (file ('gob.log').read ())
        raise SystemFailed ('Command failed: %(command)s' % locals ())
    return status

def read_pipe (command, raise_on_error=True):
    log ('executing: %(command)s' % locals ())
    if options.verbose < 3:
        command = '(%(command)s) 2> gob.log' % locals ()
    pipe = os.popen (command)
    output = pipe.read ()
    log ('pipe-output:\n%(output)s)s' % locals (), threshold=2)
    if pipe.close () and raise_on_error:
        info (command)
        info (file ('gob.log').read ())
        raise SystemFailed ('Pipe failed: %(command)s' % locals ())
    return output

def list_dirs (dir, allow_link=True):
    return filter (lambda x: (os.path.isdir (os.path.join (dir, x))
                              and (allow_link
                                   or not os.path.islink (os.path.join (dir, x)))),
                   os.listdir (dir))

def find_file (root, path, name):
    for dir in path:
        file_name = os.path.abspath (os.path.join (root, dir, name))
        if os.path.isfile (file_name):
            return file_name
    return None

def _apply_patch (dir, patch):
    system ('patch -l -p0 -d %(dir)s < %(patch)s' % locals ())

def apply_patch (dir, patch):
    if not options.split:
        return _apply_patch (dir, patch)
    apply_dir = os.path.join (dir, options.flat_apply_dir)
    _apply_patch (apply_dir, patch)
    for module in list_dirs (apply_dir, allow_link=False):
        rename (os.path.join (apply_dir, module), os.path.join (dir, module))

def get_srcpack2_dict ():
    def modules_line (x):
        e = x.split ("=")
        return e[0], e[1].split (',')
    return dict (map (modules_line, file (src_dir + 'bin/modules2.txt').readlines ()))

def for_each (func, lst):
    for i in lst:
        func (i)

def move (src):
    src2 = get_srcpack2_dict ()
    def move_pack (pack):
        pack_dir = os.path.join (src, pack)
        def move_module (module):
            module_dir = src + '/' + module
            pack_module_dir = pack_dir + '-/' + module
            if os.path.exists (module_dir):
                rename (module_dir, pack_module_dir)
        makedirs (pack_dir + '-')
        for_each (move_module, src2[pack])
        rename (pack_dir + '-', pack_dir)
    for_each (move_pack, src2.keys ())

def move_back (src):
    src2 = get_srcpack2_dict ()
    def move_pack (pack):
        pack_dir = os.path.join (src, pack)
        def move_module (module):
            print 'renaming:', module
            module_dir = src + '/' + module
            pack_module_dir = pack_dir + '-/' + module
            if os.path.exists (pack_module_dir):
                rename (pack_module_dir, module_dir)
            else:
                print 'no such dir:', pack_module_dir
        rename (pack_dir, pack_dir + '-')
        for_each (move_module, src2[pack])
        rmdir (pack_dir + '-')
    for_each (move_pack, src2.keys ())

def setup_flat_apply_dir (src):
    src2 = get_srcpack2_dict ()
    apply_dir = os.path.join (src, options.flat_apply_dir)
    shutil.rmtree (apply_dir, ignore_errors=True)
    makedirs (apply_dir)
    missing = ['mdbtools', 'libwpg', 'libwps', 'xalan']
    for pack in src2.keys ():
        for module in src2[pack]:
            symlink (os.path.join ('..', '..', pack, module), os.path.join (apply_dir, module))
    for pack in missing:
        symlink (os.path.join ('..', '..', pack), os.path.join (apply_dir, pack))

def patch_get_branch (patch):
    patch_file = patch.file_name
    if not patch_file:
        return None

    if 'vba' in options.dir_branch and int (options.milestone) > 19:
        m = re.search ('(cws-scsheetprotection02|sc-autofilter-empty-nonempty|sc-copy-source-border|sc-datapilot|sc-move-from-origin|sc-paste-on-enter|sc-save-password-minlength|scroll-accel|xl-import-formradiobutton)', patch_file)
        if m:
            return 'vba'

    # Prevent from going into ooxml
    m = re.search ('(writerfilter-qnametostr-NOOPTFILES)', patch_file)
    if m:
        return m.group (1)

    dir = os.path.basename (os.path.dirname  (patch_file))
    base = os.path.splitext (os.path.basename (patch_file))[0]

    module_re = None
    if not module_re:
        modules = list_dirs (options.build_dir)
        if options.split:
            modules = list_dirs (os.path.join (options.build_dir, options.flat_apply_dir))
        module_re = '|'.join (modules)

    # Patches with a simple digit suffix are aggregated into one branch
    if (not re.search ('%(module_re)s$|\d\d+$' % locals (), base)
        and re.search ('\d$', base)):
        base = re.sub ('-*\d$', '', base)

    # Patches in a separated may be aggregated into one branch
    if dir in options.dir_branch:
        return dir

    # Pathes with a branch_prefix are aggregated into one branch
    branch_prefix = [
        'cjk-character-units',
        'cws-layoutdialogs',
        'cws-scsheetprotection02',
        'emf+',
        'fpicker-kde',
        'jvmfwk-gij',
        'lockfile',
        'mono',
        'sal-strintern-speed',
        'sc-dataform',
        'sc-datapilot',
        'speed-configmgr',
        'svg-import',
        'system-lpsolve',
        'tools-urlobj-smb-scheme',
        'transogl',
        'unittesting',
        'unxsplash',
        'vba',
        'wpgimporter',
        'writerfiltery'
        ]

    branch_prefix_re = '^(' + '|'.join (branch_prefix).replace ('+', '\+') + ')'
    m = re.search (branch_prefix_re, base)
    if m:
        def assert_dir_group (m, s):
            return s in options.dir_branch or m.group (1) != s
        if (assert_dir_group (m, 'vba')
            and assert_dir_group (m, 'emf+')
            and assert_dir_group (m, 'unittesting')):
            return m.group (1)

    # Some patches are declared in the middle of a branch [another
    # series of patches that form a branch] which depends on that
    # patch, but have derogatory naming.  These patches must be
    # categorised explicitly.  The ASSIMILATE warning helps to detect
    # these.
    if 'emf+' in options.dir_branch and re.search ('^(cairocanvas-alpha-pixmap-rewrite|vcl-grey-alpha-unix-sal-bitmap)', base):
        return 'emf+'

    if 'ooxml' in options.dir_branch and re.search ('^(win32-installer-register-moox-types)', base):
        return 'ooxml'

    if 'vba' in options.dir_branch and re.search ('^(default-autotext-and-form-name|sc-toggle-merge-center)', base):
        return 'vba'

    if re.search ('^(fix-linkoo|linkoo-)', base):
        return 'linkoo'

    if re.search ('^(fpicker-common-scp2)', base):
        return 'fpicker-kde'

    # Remove workspace and milestone suffixes
    workspace = options.workspace
    milestone = options.milestone
    base = re.sub ('-%(workspace)s' % locals (), '', base)
    base = re.sub ('-m%(milestone)s' % locals (), '', base)

    # Patches with a -localize suffix are aggregated into one branch
    base = re.sub ('-localize$' % locals (), '', base)

    # Patches with a module suffix are aggregated into one branch
    base = re.sub ('-(%(module_re)s)$' % locals (), '', base)

    # git does not like dots in branch names
    base = base.replace('.','-')

    return base

# Hard handy work for m19 gets quickly bit rotten
# Use patch dependency calculation instead
manual_m19_branch_dependencies = {
    'buildfix-layoutdialogs': ['cws-layoutdialogs'],
    'cairocanvas-fix-image-cache': ['cairo'],
    'cws-scsheetprotection02': ['sc-datapilot', 'sc-paste-on-enter'],
    'emf+': ['link-as-needed'],
    'forms-radio-button-group-names': ['form-control-visibility'],
    'layout-plugin': ['cws-layoutdialogs'],
    'layout-tab': ['layout-plugin'],
    'linkwarn-svtools-miscopts-bits': ['novell-win32-odma'],
    'ooo59127.vcl.honourcairofont': ['ooo64508.vcl.honourfontconfighinting'],
    'oosplash-etc-openoffice-sofficerc': ['unxsplash'],
    'ooxml': ['lwp-filter-component'],
    'sc-copy-on-merged-cells': ['sc-dataform'],
    'sc-dataform': ['sc-hrc-ooo-build-resources'],
    'sc-datapilot': ['sc-dataform'],
    'sc-dp-gridlayout': ['sc-datapilot'],
    'sc-export-shape-hlink-bindings': ['sc-export-shape-macro-bindings'],
    'sc-simple-sort-include-format-header': ['sc-natural-sort'],
    'sc-toggle-merge-center': ['vba'],
    'sfx2-pre-and-postprocess-crash-fix': ['sfx2-pre-and-postprocess-during-save-load'],
    'sfx2-pre-and-postprocess-during-save-load': ['sfx2-remove-check-update-on-fileload'],
    'speed-bdirect': ['speed-symbolic-functions'],
    'speed-store-lck': ['store-core'],
    'static-libs-use-_pic': ['system-lpsolve'],
    'ui-desktop-integration': ['linkwarn-svtools-miscopts-bits'],
    'unittesting': ['tools-qa-urlobj-unittest', 'gnome-vfs-late-init'],
    'vba': ['cws-npower10', 'cws-pflin10', 'cws-npower11'],
    'vcl-linking-randr': ['autocorrect-accidental-caps-lock', 'internal-mesa-headers'],
    'wpgimporter': ['wpsimport'],
}

def range_union (a, b, fuzz=0):
    u = (max (a[0], b[0]) - fuzz, min (a[1], b[1]) + fuzz, a[2], b[2])
    if u[0] > u[1]:
        return None
    return u

class File:
    def __init__ (self, s):
        self.string = s
        self.ranges = None
        self.name = None
        if self.string.find ('\n+++ ') >= 0:
            self.name = re.search ('\n[+]{3}\s+([.]/)?([^\s]+)', self.string).group (2)
    def __repr__ (self):
        return '<File: %(name)s>' % self.__dict__
    def get_ranges (self):
        if not self.ranges:
            self.numbers = re.findall ('\n(@@ -(\d+),(\d+) [+](\d+),(\d+) @@(.|\n[^@])*)', self.string)
            self.ranges = map (lambda x: (min (int (x[1]), int (x[3])), max (int (x[1]) + int (x[2]), int (x[3]) + int (x[4])), x[0][:160]), self.numbers)
        return self.ranges

def patch_depend (p, q):
    files = []
    for file_name in p.files.keys ():
        if file_name in q.files.keys ():
            for a in p.files[file_name].get_ranges ():
                for b in q.files[file_name].get_ranges ():
                    union = range_union (a, b, int (options.fuzz))
                    if union:
                        return union
    return False

def patch_get_dependencies (patches, patch):
    dependencies = ['pristine']
    for p in patches:
        if p == patch:
            break
        if patch_depend (patch, p):
            dependencies += [p.name]
    return dependencies

def branch_get_dependencies (branches, patches, branch):
    patch_dependencies = {}
    patch_overlaps = []
    first_patch = None
    for patch in patches:
        if patch in branches[branch]:
            first_patch = patch
            break
    last_patch = None
    for patch in reversed (patches):
        if patch in branches[branch]:
            last_patch = patch
            break
    for patch in branches[branch]:
        assimilate = False
        for p in patches:
            if p == last_patch:
                name = p.name
                break
            if p == first_patch:
                name = p.name
                # We cannot have a branch pre-depend on a patch/branch
                # that does not exist yet.  FIXME: if it is really
                # needed, it should be auto-assimilated by the branch.
                # This is now done manually in patch_get_branch ().
                assimilate = True
            o = patch_depend (patch, p)
            if assimilate and o and p not in branches[branch]:
                name = p.name
                ab = patch_get_branch (p)
                print 'ASSIMILATE[%(branch)s]: %(name)s [%(ab)s]' % locals ()
                continue
            if o:
                patch_dependencies[p] = p
                patch_overlaps += [o]
    branch_dependencies = {}
    for patch in patch_dependencies.values ():
        b = patch_get_branch (patch)
        if b != branch:
            branch_dependencies[b] = b
    if not branch_dependencies:
        return ['pristine']
    return branch_dependencies.values ()

# No overrides are necessary when using fuzz >= 40
branch_override_dependencies = {
    }

def branch_get_dependencies_with_override (branches, patches, branch):
    return branch_override_dependencies.get (branch, branch_get_dependencies (branches, patches, branch))

GitFailed = SystemFailed

gitignores = '''
*-
*-HEAD
*-git
*-patched
*-pristine
*.bak
*.cxx-*
*.deps
*.git
*.hxx-*
*.log
*.orig
*.patched
*.pristine
*.pyc
*.rej
*~
.\#*
/Linux*Env.Set*
/bootstrap
/makefile.mk
/solver
CVS
TAGS
\#*
xxx-have-in-patches-now:localize.sdf
unxlng*.pro
autom4te.cache/
config.log
config.parms
config.status
configure
set_soenv
tmon.out
visibility.cxx
visibility.s
warn
default_images/introabout/intro-save.bmp
dmake/Makefile
dmake/config.h
dmake/config.log
dmake/config.status
dmake/dmake
dmake/*.o
dmake/dmakeroot.h
dmake/stamp-h1
dmake/startup/Makefile
dmake/startup/config.mk
dmake/startup/unix/Makefile
dmake/startup/unix/cygwin/Makefile
dmake/startup/unix/linux/Makefile
dmake/startup/unix/macosx/Makefile
dmake/startup/unix/solaris/Makefile
dmake/startup/unix/sysvr4/Makefile
dmake/startup/winnt/Makefile
dmake/startup/winnt/mingw/Makefile
dmake/startup/winnt/msvc6/Makefile
dmake/tests/Makefile
dmake/unix/.dirstamp
instsetoo_native/res/banner_nld.bmp
instsetoo_native/res/banner_ooop.bmp
instsetoo_native/res/nologoinstall_nld.bmp
instsetoo_native/res/nologoinstall_ooop.bmp
instsetoo_native/util/OpenOffice
sd/xml/transitions-ogl.xml
setup_native/source/win32/nsis/ooobanner_nld.bmp
setup_native/source/win32/nsis/ooobanner_ooop.bmp
setup_native/source/win32/nsis/ooobitmap_nld.bmp
setup_native/source/win32/nsis/ooobitmap_ooop.bmp
setup_native/source/win32/nsis/ooosetup_nld.ico
solenv/unxlng*/
svx/res
'''
keep = '''
default_images/introabout/intro-save.bmp
default_images/introabout/intro.bmp
default_images/svx/res/openabout_ark.png
default_images/svx/res/openabout_translateorgza.png
default_images/sw/res/go-oo-team.png
'''

def create_gitignores (dir):
    for i in filter_out (operator.not_, gitignores.split ('\n')):
        if options.split:
            i = i.replace ('/bootstrap', '/bootstrap/bootstrap')
        if i[0] == '/':
            file (dir + '/.gitignore', 'a').write (i + '\n')
        else:
            slash = ''
            if i[-1] == '/':
                i = i[:-1]
                slash = '/'
            file (dir + '/' + os.path.dirname (i) + '/.gitignore', 'a').write (os.path.basename (i) + slash + '\n')

class Setup:
    string = None
    vars = {}
    def __init__ (self, file_name='config.log'):
        if not self.string and os.path.exists (file_name):
            self.string = file (file_name).read ()
    def get (self, key, default=None):
        return self.vars.get (key, self.read_ (key, default))
    def read_ (self, key, default):
        m = re.search ('''%(key)s=['"*]([^*"']*)''' % locals (), self.string)
        if m:
            self.vars[key] = m.group (1)
        else:
            self.vars[key] = default
        return self.vars[key]

def get_svn_revision ():
    return re.search ('\nRevision: ([0-9]+)', read_pipe ('svn info')).group (1)

def get_git_committish ():
    return re.search ('([^ ]+)', read_pipe ('git log --pretty=oneline -1')).group (1)

class Git:
    def __init__ (self, dir, patched, clean=False):
        self.dir = dir
        self.patched = patched
        self.scratch = 'work/scratch'
        self.workspace = options.workspace
        self.milestone = options.milestone
        self.pristine = 'upstream/%(workspace)s-m%(milestone)s' % self.__dict__
        self.commits = {}
        self.log = {}
        if not os.path.exists (self.dir):
            drink = Setup ().get ('DRINK', 'tea')
            info ('Unpacking source tree - [ go and have some %(drink)s ] ...' % locals ())
            system ('cd bin && ./unpack')
            create_gitignores (dir)
            if options.split:
                move (self.dir)
                setup_flat_apply_dir (self.dir)
            self.system ('touch unpack')
        if not os.path.isdir (dir + '/.git/refs'):
            drink = Setup ().get ('DRINK')
            info ('Creating GIT archive - [ go and have some %(drink)s ] ...' % locals ())
            self.system ('git init')
            #svn_revision = get_svn_revision ()
            #self.commit ('Initial svn:r%(svn_revision)s unpatched.' % locals ())
            git_committish = get_git_committish ()
            self.commit ('Initial ooo-build: %(git_committish)s unpatched.' % locals ())
            self.system ('git branch %(pristine)s' % self.__dict__)
            self.system ('git tag gob-%(workspace)s-%(milestone)s %(pristine)s' % self.__dict__)
            self.system ('git branch pristine')
            self.system ('git gc')
        if clean:
            if self.is_modified ():
                self.system ('git reset --hard HEAD')
                self.system ('git clean -df')
        if self.has_branch (self.patched):
            self.checkout (self.patched)
        else:
            self.system ('git checkout -b %(patched)s pristine' % self.__dict__)
        if self.has_branch (self.scratch):
            self.system ('git branch -D %(scratch)s' % self.__dict__)
    def pipe (self, command, raise_on_error=True):
        dir = self.dir
        return read_pipe ('cd %(dir)s && %(command)s' % locals (), raise_on_error)
    def system (self, command, raise_on_error=True):
        dir = self.dir
        return system ('cd %(dir)s && %(command)s' % locals (), raise_on_error)
    def get_branches (self):
        return filter_out (operator.not_,
                           self.pipe ('git branch')
                           .replace ('*', '')
                           .replace (' ', '').split ('\n'))
    def get_log (self, branch=''):
        commit = self.get_current_commit (branch)
        self.log[commit] = self.log.get (commit, self.pipe ('git log --pretty=oneline %(branch)s --' % locals ()))
        return self.log[commit]
    def get_current_commit (self, branch=''):
        if not branch:
            branch = 'HEAD'
        return self.pipe ('git rev-parse %(branch)s' % locals ())[:-1]
    def get_commit (self, patch):
        if not self.commits:
            log = self.get_log (self.patched)
            def grok_log_line (s):
                m = re.match ('([^ ]+) Apply.*/([^/]+[.](diff|patch))', s)
                if not m:
                    info ('Skipping line:%(s)s:' % locals ())
                    return None, None
                return m.group (2), m.group (1)
            self.commits = dict (map (grok_log_line, log.split ('\n')[:-2]))
        return self.commits.get (patch, None)
    def commit (self, message):
        self.system ('git add .')
        self.system ('git add -u .')
        self.system ('''git commit -m '%(message)s' ''' % locals ())
    def is_modified (self):
        return re.sub ('# On branch.*\nnothing to commit \(working directory clean\)\n', '',
                       self.pipe ('git status', raise_on_error=False))
    def assert_clean (self):
        dir = self.dir
        pending = self.is_modified ()
        if pending:
            raise GitFailed ('working directory unclean: %(dir)s\n%(pending)s' % locals ())
    def checkout (self, branch):
        if not self.is_on_branch (branch):
            self.system ('git checkout %(branch)s' % locals ())
    def get_current_branch (self):
        return self.pipe ('git symbolic-ref HEAD', raise_on_error=False)[len ('refs/heads/'):-1]
    def is_on_branch (self, branch):
        return branch == self.get_current_branch ()
    def has_branch (self, branch):
        return branch in self.get_branches ()
    def apply_patch (self, branches, patches, patch):
        branch = patch_get_branch (patch)
        info ('Applying patch[%(branch)s]: ' % locals () + patch.name)
        patched = self.get_current_branch ()
        apply_patch (self.dir, patch.file_name)
        base = os.path.basename (patch.file_name)
        self.commit ('Apply %(base)s.' % locals ())
        if options.milestone == '19':
            dependencies = manual_m19_branch_dependencies.get (branch, ['pristine'])
        else:
            dependencies = branch_get_dependencies_with_override (branches, patches, branch)
        if not self.has_branch (branch):
            if not options.topgit:
                base_depend = dependencies[0]
                self.system ('git checkout -b %(branch)s %(base_depend)s' % locals ())
                for dependency in dependencies[1:]:
                    self.system ('git rebase %(dependency)s' % locals ())
            else:
                depend_str = ' '.join (dependencies)
                self.system ('tg create %(branch)s %(depend_str)s' % locals ())
                self.system ('git commit -am "topgit branch info %(branch)s"' % locals ())
        else:
            self.checkout (branch)
        log = self.get_log (patched)
        commit = log[:log.index (' ')]
        self.system ('git cherry-pick -x %(commit)s' % locals ())
    def before_ (self):
        self.assert_clean ()
        self.system ('git checkout -b %(scratch)s %(patched)s' % self.__dict__)
    def after_ (self):
        self.system ('git push . %(scratch)s:%(patched)s' % self.__dict__)
        self.checkout (self.patched)
        self.system ('git branch -D %(scratch)s' % self.__dict__)
    def pick_patch (self, patch, commit):
        branch = patch_get_branch (patch)
        info ('Picking patch[%(branch)s]: ' % locals () + patch.name)
        self.system ('git cherry-pick -x %(commit)s' % locals ())
    def add_patch (self, branches, patches, patch):
        if patch.name in self.get_log ():
            info ('patch already applied, skipping: ' + patch.name)
            return
        if file (patch.file_name).read ().find ('\n+++ ') == -1:
            info ('patch is empty, skipping: ' + patch.name)
            return
        commit = None
        branch = patch_get_branch (patch)
        if self.has_branch (branch):
            name = patch.name.replace ('+', '\+')
            m = re.search ('(^|\n)([^\s]+)\s+.*(\s|/)%(name)s' % locals (), self.get_log (branch))
            if m:
                commit = m.group (2)
        self.before_ ()
        if commit:
            self.pick_patch (patch, commit)
        else:
            self.apply_patch (branches, patches, patch)
        self.after_ ()
    def dump_gob (self, branches, patches, branch):
        gob_dir = self.dir + '/.git/refs/gob'
        if not os.path.exists (gob_dir):
            makedirs (gob_dir)
        branch_patches = branches.get (branch, [])
        if not branch_patches:
            return
        owner = ''
        for patch in branch_patches:
            owner = patch.owner
            if owner:
                break
        issues = []
        for patch in branch_patches:
            issues += patch.issues
        issue_string = ', '.join (issues)
        dependencies = filter (lambda x: x != 'pristine', branch_get_dependencies (branches, patches, branch))
        dependencies_string = ', '.join (dependencies)
        commit = self.get_current_commit ()
        gob_file_name = os.path.join (gob_dir, branch)
        info ('Writing: ' + gob_file_name)
        file (gob_file_name, 'w').write ('''%(commit)s
state: stable
issue: %(issue_string)s
owner: %(owner)s
depend: %(dependencies_string)s
''' % locals ())

class Patch:
    def __init__ (self, file_name):
        self.file_name = file_name
        if self.file_name:
            self.set_files ()
        self.file_name = None
    def set_files (self):
        self.files = dict (map (lambda x: (x.name, x), map (File, ('\n' + file (self.file_name).read ()).split ('\n---')[1:])))
    def __repr__ (self):
        return '<Patch: ' + str (map (str, self.files.values ())) + ' >'

class Apply_patch (Patch):
    def __init__ (self, s, section, owner, issue):
        Patch.__init__ (self, None)
        self.string = s
        self.section = section
        self.owner = owner
        m = re.search (', (\w\w+)', self.string)
        if m:
            self.owner = m.group (1)
        m = re.match ('^\s*(([^#\s].*).(diff|patch))(.*)', self.string)
        self.name = m.group (1)
        self.base = m.group (2)
        self.issues = map (lambda x: x[0], re.findall ('((i|n)#[0-9]+)', self.string))
        if issue:
            self.issues.append (issue)
    def __repr__ (self):
        return '<Patch: %(file_name)s [%(section)s] %(owner)s %(issues)s>' % self.__dict__
    def set_file_name (self, dir, path):
        self.file_name = find_file (dir, path, self.name)
        self.set_files ()
        return self

class Section:
    def __init__ (self, s):
        self.string = s
        m = re.match ('[[\s]*(.*[^\s])\s*\]', self.string)
        self.tag = m.group (1).replace (' ', '')
        self.name = re.sub ('[^\w].*', '', self.tag)
        m = re.search ('\n(SectionOwner)\s*=>\s*(.*[^\s])', self.string)
        self.owner = ''
        if m:
            self.owner = m.group (2)
        m = re.search ('\n(SectionIssue)\s*=>\s*(.*[^\s])', self.string)
        self.issue = ''
        if m:
            self.issue = m.group (2)
    def __repr__ (self):
        return '<Section: %(tag)s %(owner)s %(issue)s>' % self.__dict__
    def get_patches (self):
        def create_patch (s):
            return Apply_patch (s[0], self.tag, self.owner, self.issue)
        return map (create_patch, re.findall ('\n\s*([^#\s].*.(diff|patch).*)', self.string))

class Apply:
    def __init__ (self, apply_file, workspace, milestone):
        self.workspace = workspace
        self.milestone = milestone
        self.string = file (apply_file).read ()
        first_section = self.string.index ('\n[')
        self.distros_string = self.string[:first_section]
        self.sections_string = self.string[first_section:]
        self.path = re.search ('\nPATCHPATH=(.*)', self.string).group (1).split (':')
        self.distro = {}
        self.master = {}
        self.distros_string = re.sub ('\s*\\\s*\n\s*', '', self.distros_string)
        for distro in re.findall ('\n\s*([^#=\s:]+)\s*:\s*(.*)', self.distros_string):
            lst = distro[1].replace (' ', '').split (',')
            expanded = []
            for i in lst:
                if i in self.master.keys ():
                    del self.master[i]
                expanded += self.distro.get (i, [i])
            self.master[distro[0]] = self.distro[distro[0]] = expanded
        # convenience: add sections as distro
        #self.distro.update (dict (map (lambda x: (x.name, x.name), self.get_sections ())))
    def get_section_strings (self):
        return map (lambda x: '[' + x, self.sections_string.split ('\n[')[1:])
    def get_sections (self):
        return map (Section, self.get_section_strings ())
    def get_distro (self, distro_name):
        def section_in_distro (section):
            if distro_name not in self.distro.keys ():
                return distro_name == section.name
            m = re.search ('(\w+).*(<=|<|==|!=|>=|>)%(workspace)s-m([0-9]+)'
            % self.__dict__, section.tag)
            if m:
                name = m.group (1)
                if not name in self.distro[distro_name]:
                    return False
                left_milestone = self.milestone
                operator = m.group (2)
                right_milestone = int (m.group (3))
                return eval ('%(left_milestone)s %(operator)s %(right_milestone)s' % locals ())
            else:
                return section.tag in self.distro[distro_name]
        return filter (section_in_distro, self.get_sections ())

class Command:
    def __init__ (self, apply, options):
        self.options = options
        self.apply = apply
        self.patches_ = []
        self.branches_ = {}
    def patches (self):
        '''list patches'''
        print '\n'.join (map (str, self.get_patches ()))
    def sections (self):
        '''list sections'''
        print '\n'.join (map (str, self.get_sections ()))
    def distros (self):
        '''list distros'''
        print '\n'.join (map (str, self.apply.distro.keys ()))
    def masters (self):
        '''list masters'''
        print '\n'.join (map (str, self.apply.master.keys ()))
    def branches (self):
        '''list branches'''
        print '\n'.join (map (str, self.get_branches ().keys ()))
    def get_branches (self):
        if not self.branches_:
            for patch in self.get_patches ():
                branch = patch_get_branch (patch)
                self.branches_[branch] = self.branches_.get (branch, []) + [patch]
        return self.branches_
    def dump_gobs (self):
        '''dump gob files'''
        branches = self.get_branches ()
        patches = self.get_patches ()
        git = Git (self.options.build_dir, self.options.patched)
        for branch in git.get_branches ():
            git.dump_gob (branches, patches, branch)
    def checkout (self):
        '''checkout patched ('master') branch'''
        git = Git (self.options.build_dir, self.options.patched)
    def git_export (self):
        '''export to GIT with branches'''
        git = Git (self.options.build_dir, self.options.patched, clean=True)
        patches = self.get_patches ()
        branches = self.get_branches ()
        for patch in self.get_patches ():
            git.add_patch (branches, patches, patch)
        git.system ('git gc')
    def get_patches_for_distro (self, distro):
        patches = reduce (operator.add, map (lambda section: section.get_patches (), self.get_sections_for_distro (distro)))
        for patch in patches:
            patch.set_file_name (self.options.apply_dir, self.apply.path)
        return patches
    def get_patches (self):
        if not self.patches_:
            self.patches_ = reduce (operator.add, map (self.get_patches_for_distro, self.options.distros))
        return self.patches_
    def get_sections_for_distro (self, distro):
        return self.apply.get_distro (distro)
    def get_sections (self):
        return reduce (operator.add, map (self.get_sections_for_distro, self.options.distros))
    def prepare (self):
        '''prepare ooo-build dir for use with GIT'''
        git = Git (self.options.build_dir, self.options.patched)
    def postpare (self):
        '''update ooo-build dir GIT after patching'''
        git = Git (self.options.build_dir, self.options.patched)
        #svn_revision = get_svn_revision ()
        #git.commit ('Update to svn:r%(svn_revision)s patched.' % locals ())
        git_committish = get_git_committish ()
        git.commit ('Update to ooo-build: %(git_committish)s patched.' % locals ())
        git.system ('git rebase patched')
    def dependencies (self):
        '''list branch dependencies'''
        patches = self.get_patches ()
        branches = self.get_branches ()
        independent = 0
        for branch in branches:
            dependencies = sorted (branch_get_dependencies (branches, patches, branch))
            if dependencies == ['pristine']:
                independent += 1
            print 'BRANCH:', branch, '->', dependencies
        print 'INDEPENDENT:', independent
    def patch_dependencies (self):
        '''list patch dependencies'''
        branches = self.get_branches ()
        patches = self.get_patches ()
        for patch in patches:
            branch = patch_get_branch (patch)
            name = patch.name
            print 'PATCH[%(branch)s]: %(name)s -> ' % locals (), patch_get_dependencies (patches, patch)
    def bump (self):
        '''bump BRANCH TAG-NAME - bump current branch (BRANCH=new upstream)'''
        if len (self.options.arguments) != 2:
            print 'Usage: gob bump BRANCH TAG-NAME'
            print 'BRANCH: new upstream'
            print 'Example:'
            print '    gob bump upstream/dev300-m21 my-21-update'
            sys.exit (2)
        branch = self.options.arguments[0]
        tag = self.options.arguments[1]
        git_dir = self.options.build_dir
        system ('cd %(git_dir)s && gob-bump %(branch)s %(tag)s' % locals ())
    def update (self):
        '''update BRANCH - update current branch (BRANCH='master')'''
        if len (self.options.arguments) != 1:
            print 'Usage: gob update BRANCH'
            print '''BRANCH: the 'master' branch'''
            print 'Example: '
            print '    gob update distro/SUSE'
            sys.exit (2)
        branch = self.options.arguments[0]
        git_dir = self.options.build_dir
        system ('cdi %(git_dir)s && gob-update %(branch)s' % locals ())
    def reset (self):
        '''reset - set GIT tree to pristine and remove all branches'''
        git = Git (self.options.build_dir, self.options.patched)
        git.system ('git checkout -f %(patched)s' % git.__dict__)
        git.system ('git reset --hard pristine')
        git.system ('git clean -df')
        git.system ('rm -rf .git/refs/top-bases')
        cmd = 'xargs git branch -D'
        if options.split:
            git.system ('''git branch | grep -Ev '/|master|patched|pristine|upstream|%(patched)s' | xargs tg delete -f ''' % git.__dict__)
        git.system ('''git branch | grep -Ev '/|master|patched|pristine|upstream|%(patched)s' | xargs git branch -D''' % git.__dict__)
    def patch_depend (self):
        '''patch-depend PATCH-1 PATCH-2 - show overlap between patches'''
        if len (self.options.arguments) != 2:
            print 'Usage: patch-depend PATCH-1 PATCH-2'
            sys.exit (2)
        union = patch_depend (Patch (self.options.arguments[0]), Patch (self.options.arguments[1]))
        if union:
            print union[0], '--', union[1]
            print union[2]
            print '<<<<<<<<<<<<<<<<<<<<<<<<<=========================>>>>>>>>>>>>>>>>>>>>>>>>>'
            print union[3]
            sys.exit (1)
    def statistics (self):
        ### not advertised
        ### show some statistics on dependensies
        patches = self.get_patches ()
        branches = self.get_branches ()
        full_match = 0
        match = 0
        fail = 0
        extra = 0
        independent = 0
        for branch in branches:
            manual_dependencies = sorted (manual_m19_branch_dependencies.get (branch, ['pristine']))
            auto_dependencies = sorted (branch_get_dependencies (branches, patches, branch))
            missing = False
            for m in manual_dependencies:
                if m != 'pristine' and not m in auto_dependencies:
                    missing = True
                    break
            if missing:
                fail += 1
                print
                print 'BRANCH:', branch
                print 'MANUAL:', manual_dependencies
                print 'AUTO:', auto_dependencies
                #dependencies 'OVERLAPS:', overlaps
            if auto_dependencies == ['pristine']:
                independent += 1
            elif auto_dependencies == manual_dependencies:
                full_match += 1
            elif manual_dependencies == ['pristine']:
                extra += 1
                #print 'BRANCH:', branch
                #print 'EXTRA:', auto_dependencies
            elif not missing:
                match += 1
        print
        print 'FAIL:', fail
        print 'FULL_MATCH:', full_match
        print 'MATCH:', match
        print 'EXTRA:', extra
        print 'INDEPENDENT:', independent
    def move (self):
        move (options.build_dir)
        setup_flat_apply_dir (options.build_dir)
    def move_back (self):
        move_back (options.build_dir)

def get_cli_parser ():
    p = optparse.OptionParser ()

    p.usage = '%prog [OPTION]... COMMAND\n\nCommands:\n'
    d = Command.__dict__
    commands = [(k, d[k].__doc__) for k in d.keys ()
                if d[k].__doc__ and type (d[k]) == type (lambda x: x)]
    commands.sort ()

    global src_dir
    src_dir = ''
    if not os.path.exists ('patches'):
        src_dir = '../'
    os.environ['PATH'] = src_dir + '/bin:' + os.environ['PATH']

    for (command, doc) in commands:
        p.usage += '    %s - %s\n' % (re.sub ('_', '-', command), doc)

    def get (option):
        return p.get_option ('--' + option.replace ('-', '_')).default

    setup_workspace, setup_milestone = Setup ().get ('CVSTAG', 'dev300-m19').split ('-m')

    p.add_option ('--workspace',
                  dest='workspace',
                  default=setup_workspace,
                  metavar='STRING',
                  help='set master workspace')
    p.add_option ('--milestone',
                  dest='milestone',
                  default=setup_milestone,
                  metavar='STRING',
                  help='set milestone')
    p.add_option ('--distro',
                  action='append',
                  dest='distros',
                  default=[],
                  metavar='DISTRO',
                  help='add distribution')
    p.add_option ('--build-dir',
                  default=src_dir + 'build/' + get ('workspace') + '-m' + get ('milestone'),
                  dest='build_dir',
                  metavar='DIR',
                  help='build (/git) directory')
    p.add_option ('--apply-dir',
                  default=src_dir + 'patches/' + get ('workspace'),
                  dest='apply_dir',
                  metavar='DIR',
                  help='directory with APPLY file')
    p.add_option ('--patched',
                  default='patched',
                  dest='patched',
                  metavar='NAME',
                  help='''use NAME as patched 'master' branch name''')
    p.add_option ('--dir-branch',
                  default=[],
                  dest='dir_branch',
                  metavar='DIR',
                  help='aggregate patches in directory DIR into one branch')
    p.add_option ('--fuzz',
                  default='40', # FIXME: 40 = magic sweet spot for dev300-m21
                  dest='fuzz',
                  metavar='INT',
                  help='use FUZZ as fuzz factor for patch overlap')
    p.add_option ('--force', action='store_true', dest='force', default=False)
    p.add_option ('--split', action='store_true', dest='split', default=False)
    p.add_option ('--topgit', action='store_true', dest='topgit', default=False)
    p.add_option ('-v', '--verbose', action='count', dest='verbose', default=1)
    p.add_option ('-q', '--quiet', action='count', dest='quiet', default=0)
    return p

def parse_options ():
    p = get_cli_parser ()
    (options, arguments) = p.parse_args ()

    options.command = ''
    options.arguments = []
    if arguments:
        options.command = re.sub ('-', '_', arguments.pop (0))
    options.arguments = arguments

    if options.command in Command.__dict__:
        return options
    if options.command:
        sys.stderr.write ('no such command: ' + options.command)
        sys.stderr.write ('\n\n')
    p.print_help ()
    sys.exit (2)

def set_option_defaults (options):
    options.flat_apply_dir = '.git/apply-dir'
    options.verbose -= options.quiet
    options.apply_dir = (options.apply_dir
                         .replace ('ooo300', 'dev300')
                         .replace ('ooo310', 'dev300')
                         .replace ('ooo320', 'dev300')
                         )
    if not options.distros:
        options.distros = ['SUSE']
    if not options.dir_branch:
        options.dir_branch = filter (lambda x: x not in ['.', '..', '.svn', '64bit', 'dev300', 'hotfixes'], list_dirs (options.apply_dir + '/..'))
        too_dispersed = ['cairo', 'vba']
        options.dir_branch = filter (lambda x: x not in too_dispersed, options.dir_branch)
    return options

options = None
def main ():
    global options
    options = set_option_defaults (parse_options ())
    apply_file = options.apply_dir + '/apply'
    apply = Apply (apply_file, options.workspace, options.milestone)
    Command.__dict__[options.command] (Command (apply, options))

if __name__ == '__main__':
    main ()
