#!/usr/bin/env python
"""
git-hq: globally manage git remotes with a single config file

Manages git remotes through a simple git-like syntax with a
traditional git-like config file.

sample usage:
git hq remote add github git@github.com:username/{repo}.git
git hq init
git hq attach github
git hq push master
git hq pull
"""

import sys,os
# try:
#     import configparser as cp
# except:
#     import ConfigParser as cp   # python 2.x
#sys.path.insert(0,os.getcwd())
import git
import config

__author__ = "Jarrell Waggoner"
__copyright__ = "Copyright 2011"
__credits__ = ["Jarrell Waggoner", "Andrew Temlyakov"]
__license__ = "BSD"
__version__ = "1.0"
__maintainer__ = "Jarrell Waggoner"
__email__ = "jarrell.waggoner@gmail.com"

program_name = "git-hq"
config_path = os.getenv("HOME")+"/."+program_name

class CmdFailed(Exception):
    def __init__(self,cmd,reason=None):
        self.cmd = cmd
        if not reason is None: self.reason = reason
    def __str__(self):
        return repr(self.cmd)
    def message(self):
        sys.stderr.write(program_name+": "+self.cmd+" command failed\n")
        if hasattr(self,'reason'): sys.stderr.write(self.reason+"\n")

class NoRepo(Exception):
    def message(self,exists):
        if exists:
            sys.stderr.write(program_name+
                             ": git repository not in config file\n")
            # suggest()
        else:
            sys.stderr.write(program_name+
                             ": no repository found or not in config file\n")

class NoDesiredRepo(Exception):
    def message(self,exists):
        if exists:
            sys.stderr.write(program_name+
                             ": git repository currently exists here\n")
        else:
            sys.stderr.write(program_name+
                             ": repository not in config file\n")

def identify_current_repo(repos):
    """compare all the hashes in the current repo to the repos dict"""
    for h in git.hashes():
        if h in repos:
            return repos[h]
    return None

def identify_desired_repo(repos,name):
    """find a specified repo by (lowercase) name"""
    try:
        return next(r for k,r in repos.items() if r.name.lower()==name.lower())
    except StopIteration:
        raise CmdFailed('clone',"repo not in config file")
        return None

def suggest():
    """suggest that the user add the repo to their config file"""
    try:
#         print("""Add the following to ~/.%s to include this repo
# [%s]
# id=%s
# repos=...""" % (program_name, git.name(), git.last_hash()) )
        print("""repo not found in config file: run
\tgit hq init
to add repo to git-hq""")
    except:
        print("""Add repo information to ~/.%s or run in existing repository""" %
              program_name)

def cmd_help(args=None):
    """print help information"""
    print("""usage: %s <command>
commands available include:
\tclone
\tinit
\tpurge
\tpull
\tpush
\tremote
\tpath
\trun
\thelp""" % program_name)

def valid_argument(arg,err):
    """shorthand to save on ugly code"""
    if arg is None:
        raise err

def remote(args):
    """subcommand to manage remotes"""
    def sub_add(args):
        if len(args['argv']) < 2: raise CmdFailed('remote add',"too few arguments")
        config.ammend_property_config(config_path,'remotes',(args['argv'][0],args['argv'][1]))

    def sub_delete(args):
        if len(args['argv']) < 1: raise CmdFailed('remote delete',"too few arguments")
        config.remove_property_config(config_path,'remotes',args['argv'][0])

    def sub_attach(args):
        if len(args['argv']) < 1: raise CmdFailed('remote attach',"too few arguments")
        valid_argument(args['repo'],NoRepo)
        if not args['argv'][0] in args['remotes']: raise CmdFailed('remote attach',"remote not in config file")
        if args['argv'][0] in [r[0] for r in args['repo'].repos]: raise CmdFailed('remote attach',"remote already attached")
        config.ammend_property_config(config_path,
                      args['repo'].name,
                      ('repos',
                       ','.join([r[0] for r in args['repo'].repos]+[args['argv'][0]])))

    def sub_detach(args):
        if len(args['argv']) < 1: raise CmdFailed('remote detach',"too few arguments")
        valid_argument(args['repo'],NoRepo)
        if not args['argv'][0] in [r[0] for r in args['repo'].repos]: raise CmdFailed('remote detach',"remote not attached")
        config.ammend_property_config(config_path,
                      args['repo'].name,
                      ('repos',
                       ','.join([r[0] for r in filter(lambda r: r[0]!=args['argv'][0],args['repo'].repos)])))

    def sub_commit(args):
        valid_argument(args['repo'],NoRepo)
        for name,url in args['repo'].repos:
            print(program_name+": Adding "+name+" remote")
            git.cmd(['remote','add',name,url],path=args['cwd'])

    subcmd = {'add':sub_add, 'delete':sub_delete, 'attach':sub_attach, 'detach':sub_detach, 'commit':sub_commit}
    param = args['argv']
    if not param:
        remotes = args['remotes']
        for r in remotes:
            print(r+" "+remotes[r])
    elif not param[0] in subcmd:
        raise CmdFailed('remote',"subcommand not recognized")
    else:
        args['argv'] = param[1:]
        subcmd[param[0]](args)

def clone(args):
    if not args['argv']:
        raise CmdFailed('clone',"no repository specified")
    desired_repo = identify_desired_repo(args['repos'],args['argv'][0])
    valid_argument(desired_repo,NoDesiredRepo)
    git.cmd(['clone',desired_repo.repos[0][1]]+list(args['argv'][1:]))

def push_pull(args):
    argv = list(args['argv'])
    cmd = args['cmd']
    repo = args['repo']
    valid_argument(repo,NoRepo)
    # intercept recognized repo name
    requested_repos = [x for x in argv if x in [r[0] for r in repo.repos]]
    # remove from arguments handed off to git
    for r in requested_repos:
        argv.remove(r)
    repo_list = [r for r in repo.repos if r[0] in requested_repos] \
        if requested_repos else repo.repos
    if '-o' in argv:            # -o = only first repo
        argv.remove('-o')
        repo_list = [repo_list[0]]
    if not repo.repos: raise CmdFailed(cmd,"no repos to "+cmd+" from")
    for name,url in repo_list:
        try:
            git.cmd([cmd,url]+argv,path=args['cwd'])
        except:
            CmdFailed(cmd,"command failed")

def init(args):
    repo_name = args['argv'][0] if len(args['argv']) > 0 and not args['argv'][0] in args['remotes'] else git.name()
    h = git.last_hash()
    if h and not h is None:
        config.ammend_section_config(config_path,repo_name,[('id',git.last_hash()),
                                                            ('repos',
                                                             ','.join([x for x in args['argv'] if x in args['remotes']])),
                                                            ('path',git.path())])
    else:
        raise CmdFailed('init',"no initial commit found")

def purge(args):
    if args['repo'] is None and not args['argv']:
        raise CmdFailed('purge',"repo not specified")
    section_to_remove = args['argv'][0] if args['repo'] else args['repo'].name
    config.remove_section_config(config_path,section_to_remove)

def run_cmd(args):
    # failure cases
    if not args['repos']:
        raise CmdFailed('run',"no repos in config file")
    if not args['argv']:
        raise CmdFailed('run',"no command specified")
    # loop over all repos
    for r in args['repos'].values():
        # failure cases for individual repo
        if r.path is None:
            sys.stderr.write("#"+r.name+": no path defined for repository\n")
            sys.stdout.flush()
        elif not os.path.isdir(r.path):
            sys.stderr.write("#"+r.name+": not checkout out\n")
            sys.stdout.flush()
        # run git-hq'ed command
        elif args['argv'][0] in verbs:
            sys.stdout.write("#"+r.name+"\n")
            sys.stdout.flush()
            new_args         = args.copy()
            new_args['repo'] = r
            new_args['cmd']  = args['argv'][0]
            new_args['argv'] = args['argv'][1:]
            new_args['cwd']  = r.path
            verbs[new_args['cmd']](new_args)
        # run pass-through command
        else:
            try:
                sys.stdout.write("#"+r.name+"\n")
                git.cmd(list(args['argv']),r.path)
            except IOError:
                sys.stderr.write("#"+r.name+": not checkout out\n")

def path_cmd(args):
    if args['repo'] is None:
        if not args['argv']:
            raise CmdFailed('path',"repo not specified")
        else:
            desired_repo = identify_desired_repo(args['repos'],args['argv'][0])
            valid_argument(desired_repo,NoDesiredRepo)
            print(desired_repo.path)
            return
        # raise CmdFailed('purge',"repo not specified")
    if not args['argv'] and not args['repo'].path is None:
        print(args['repo'].path)
    elif args['argv']:
        config.ammend_property_config(config_path,
                               args['repo'].name,
                               ('path',' '.join(args['argv'])))
    else:
        raise CmdFailed('path',"no path specified")

def pwd_cmd(args):
    if args['repo'] is None and not args['argv']:
        raise CmdFailed('path',"repo not specified")
    elif args['argv']:
        desired_repo = identify_desired_repo(args['repos'],args['argv'][0])
        valid_argument(desired_repo,NoDesiredRepo)
        print(desired_repo.path)
    elif not args['repo'] is None:
        print(args['repo'].path)
    else:
        raise CmdFailed('path',"repo not specified")

verbs = {'clone'  : clone,
         'init'   : init,
         'purge'  : purge,
         'pull'   : push_pull,
         'push'   : push_pull,
         'fetch'   : push_pull,
         'remote' : remote,
         'run'    : run_cmd,
         'pwd'    : pwd_cmd,
         'path'   : path_cmd,
         'help'   : cmd_help}

def main(*argv):

    try:
        repos,remotes = config.load_config(config_path)
    except IOError:
        config.create_config(config_path)
        print(program_name + " : no ~/."+program_name+" config file found")
        print(program_name + ": creating ~/."+program_name)
        return 1

    if (len(argv) < 2):
        cmd_help()
        return 0

    # check if we can even get repo info...
    exists = git.exists()
    # obtain repo info if so
    repo = identify_current_repo(repos) if exists else None

    arguments = {'exists' : exists,
                 'repo'   : repo,
                 'repos'  : repos,
                 'remotes': remotes,
                 'cmd'    : argv[1],
                 'cwd'    : os.getcwd(),
                 'argv'   : argv[2:]}

    try:
        verbs[argv[1]](arguments)
    except KeyError:
        sys.stderr.write(program_name+
                         ": "+argv[1]+
                         " is not a "+
                         program_name+
                         " command. See 'git hq help'.")
        return 1
    except (NoRepo,NoDesiredRepo) as e:
        e.message(exists)
        return 1
    except CmdFailed as e:
        e.message()
    # except:
    #     print(program_name+": git command failed")
    #     return 1

    return 0

if __name__ == '__main__':
    sys.exit(main(*sys.argv))
