#!/usr/bin/env python
"""
A Subversion scripts that allows the user to commit modified files as multiple
changesets, in a single operation. An editor is invoked with the current list of
modified files, and the user needs to edit the file, adding a single comment
line before each set of lines, separated by some empty lines between each
desired changeset.

Something like this::

    Modified project files.
    COM  M docs/flair.txt
    COM  M docs/dmping.txt

    Added extra careful tracing and assertions.
    COM  M common/lib/pyfff/tracing.py
    COM  M common/lib/googoo/logging.c
    COM  M common/lib/googoo/strutils.c

    COM  M docs/testutils.txt
    COM  M common/lib/pyfff/__init__.py
    COM  A common/lib/pyfff/testutil
    New test utilities.

Each set will be committed independently when you exit the editor.

(For Mercurial, see the HgCommits extension.)
"""
__author__ = 'Martin Blais <blais@furius.ca>'

import sys, os, optparse, tempfile, re
from subprocess import Popen, PIPE, call
from os.path import *


fmt = 'COM  %s'
readre = re.compile('COM  .......(.*)')

def split_paragraphs(text):
    "Split the text in paragraphs, return a list of (list of lines)."
    lines = [x.strip() for x in text.splitlines()]
    pars, cur = [], []
    for line in lines:
        if not line:
            if cur:
                pars.append(cur)
                cur = []
        else:
            cur.append(line)
    if cur:
        pars.append(cur)
        cur = None
    return pars

def split_comments_and_files(lines):
    "Given a list of lines, split it between comments and filenames."
    comments, files = [], []
    for line in lines:
        mo = readre.match(line)
        if mo:
            files.append(mo.group(1).strip())
        else:
            comments.append(line)
    return comments, files

def parse_changesets(contents):
    """
    Given the text, parse it into a list of changesets, returning pairs of
    (comment, list-of-filenames).
    """
    paragraphs = split_paragraphs(contents)

    rsets = []
    invalid = 0
    for par in paragraphs:
        comments, files = split_comments_and_files(par)
        if not files or not comments:
            print >> sys.stderr, (
                "No comments or files on paragraph:\n%s\n" % '\n'.join(par))
            invalid = 1
            continue

        rsets.append( ('\n'.join(comments), files) )
    if invalid:
       return None
    return rsets

def edit(contents, fn):
    """ Launch an editor and return the contents of the editor file if exited
    with success, or None if otherwise or if the file is empty."""
    old_contents = None
    if isfile(fn):
        with open(fn) as f:
            old_contents = f.read()

    with open(fn, 'w') as f:
        f.write(contents)
        if old_contents:
            f.write('\n\n--------------------------------------------------\n')
            f.write(old_contents)

    editor = os.environ.get('EDITOR', 'vi')
    p = Popen(' '.join((editor, fn)), shell=1)
    r = p.wait()
    if r != 0:
        return None
    return open(fn).read()

def get_svn_url():
    "Return the svn info URL."
    p = Popen(('svn', 'info'), stdout=PIPE)
    out, err = p.communicate()
    if p.returncode != 0:
        return None
    else:
        mre = re.compile('^URL: (.*)$', re.M)
        mo = mre.search(out)
        if mo:
            return mo.group(1)

def svn_commit(message, files):
    "Commit the given set of files to Subversion."
    assert(message)
    cmd = ['svn', 'commit', '-m', message] + list(files)
    p = Popen(cmd, shell=0)
    return p.wait()

def main():
    parser = optparse.OptionParser(__doc__.strip())
    parser.add_option('-n', '--dry-run', action='store_true',
                      help="Don't really commit, just print actions. This is used for testing.")
    opts, args = parser.parse_args()
    if len(args) > 1:
        parser.error("Usage: [CHECKOUT].")
    dn = os.getcwd() if not args else os.getcwd()
    if not isdir(join(dn, '.svn')):
        parser.error("The CWD is not a checkout.")

    # Get the status.
    p = Popen(('svn', 'status'), stdout=PIPE)
    out, err = p.communicate()
    if p.returncode != 0:
        parser.error("Error obtaining the list of files.")
    lines = [fmt % x for x in out.splitlines()]

    commentsfn = join(os.getcwd(), 'svn-commits.tmp')

    # Edit the contents.
    contents = '\n%s\n' % os.linesep.join(lines)
    econtents = edit(contents, commentsfn)
    if econtents is None or not econtents.strip() or econtents.strip() == contents.strip():
        raise SystemExit("(Aborting.)")

    # Parse the changesets.
    sets = parse_changesets(econtents)
    if sets is None:
        raise SystemExit("(No filesets to commit.)")

    # Print actions and perform commits.
    for i, (message, files) in enumerate(sets):
        print 'Committing set %d' % (i+1)
        print message
        for fn in files:
            print fn
        print

        if not opts.dry_run:
            r = svn_commit(message, files)
            if r != 0:
                raise SystemExit("Bailing out.")

    if isfile(commentsfn):
        os.remove(commentsfn)


if __name__ == '__main__':
    main()
