#!/usr/bin/python

import sys
from subprocess import Popen, PIPE
from reindent import Reindenter
import re
import cpp_format

svnlook = "/usr/bin/svnlook"

def _check_do_not_commit(line, filename, num, errors):
    marker = 'DO NOT' + ' COMMIT'
    if line.find(marker) >= 0:
        errors.append('%s:%d: Line contains the string "%s"' \
                      % (filename, num+1, marker));

def get_modified_files(txn, repos):
    """Get a list of all files modified or added in this transaction"""
    modfiles = []
    cmd = [svnlook, 'changed', '-t', txn, repos]
    for line in Popen(cmd, shell=False, stdout=PIPE).stdout:
        if len(line) > 4 and (line[0] == 'A' or line[0] == 'U'):
            modfiles.append(line[4:].rstrip('\r\n'))
    return modfiles

def check_c_file(filename, txn, repos, errors):
    """Check each modified C file to make sure it adheres to the standards"""
    cmd = [svnlook, 'cat', '-t', txn, repos, filename]
    pipe = Popen(cmd, shell=False, stdout=PIPE).stdout
    srch = re.compile('\s+$')
    url = re.compile('https?://')
    blank = False
    for (num, line) in enumerate(pipe):
        line = line.rstrip('\r\n')
        # No way to split URLs, so let them exceed 80 characters:
        if len(line) > 80 and not url.search(line):
            errors.append('%s:%d: Line is longer than 80 characters.' \
                          % (filename, num+1))
        if line.find('\t') >= 0:
            errors.append('%s:%d: Line contains tabs.' % (filename, num+1))
        _check_do_not_commit(line, filename, num, errors)
        if srch.search(line):
            errors.append('%s:%d: Line has trailing whitespace' \
                          % (filename, num+1))
        if not filename.endswith(".cpp") and line.startswith("#define ") \
           and not line.startswith("#define IMP") \
           and not line.startswith("#define EIGEN_YES_I_KNOW_SPARSE_"
                                   "MODULE_IS_NOT_STABLE_YET"):
            errors.append('%s:%d: Preprocessor symbols must start with IMP' \
                          % (filename, num+1))
        blank = (len(line) == 0)
        if blank and num == 0:
            errors.append('File %s has leading blank line(s)' % filename)
    if blank:
        errors.append('File %s has trailing blank line(s)' % filename)

def check_python_file(filename, txn, repos, errors):
    """Check each modified Python file to make sure it adheres to the
       standards"""
    cmd = [svnlook, 'cat', '-t', txn, repos, filename]
    temptest = re.compile('\s+def\s+temp_hide_test.*')
    test= re.compile('\s+def\s+(test_[abcdefghijklmnopqrstuvwxyz'
                     '0123456789_]*)\(')
    tests = []
    for (num, line) in enumerate(Popen(cmd, shell=False, stdout=PIPE).stdout):
        _check_do_not_commit(line, filename, num, errors)
        if temptest.match(line):
            errors.append('%s:%d: Test case has the temp_hide_ prefix' \
                          % (filename, num+1))
        m = test.match(line)
        if m:
            g = m.group(1)
            if g in tests:
                errors.append('%s:%d: Test case has multiple tests with '
                              'the same name %s' % (filename, num+1, g))
            tests.append(g)

    p1 = Popen(cmd, shell=False, stdout=PIPE).stdout
    r = Reindenter(p1)
    try:
        if 'compat_python' not in filename and r.run():
            errors.append('Python file ' + filename + ' has odd indentation; ' \
                          + 'please run through reindent.py first.')
    except Exception:
        print >> sys.stderr, "reindent.py FAILED on %s:" % filename
        raise

def get_file(filename, txn, repos):
    cmd = [svnlook, 'cat', '-t', txn, repos, filename]
    pipe = Popen(cmd, shell=False, stdout=PIPE).stdout
    return (pipe, filename)

def check_modified_file(filename, txn, repos, errors):
    """Check each modified file to make sure it adheres to the standards"""
    # skip code that isn't ours
    if filename.find("dependency") != -1:
        return
    if filename.endswith('.h') or filename.endswith('.cpp') \
       or filename.endswith('.c'):
        check_c_file(filename, txn, repos, errors)
        if filename.endswith('.h'):
            cpp_format.check_header_file(get_file(filename, txn, repos), errors)
        elif filename.endswith('.cpp'):
            cpp_format.check_cpp_file(get_file(filename, txn, repos), errors)
    elif filename.endswith('.py') or filename.endswith('SConscript') \
         or filename.endswith('SConstruct'):
        check_python_file(filename, txn, repos, errors)

def is_symlink(filename, txn, repos, errors):
    """Return True if the given filename is a symlink. Also report an error
       if a symlink also has the executable bit set, since this can trigger
       Subversion bug #2344."""
    cmd = [svnlook, 'proplist', '-v', '-t', txn, repos, filename]
    res = Popen(cmd, shell=False, stdout=PIPE).stdout.read()
    symlink = "svn:special" in res
    executable = "svn:executable" in res
    if symlink and executable:
        errors.append('You have set the svn:executable property on the ' \
                      + 'symlink ' + filename + '; please remove it, since ' \
                      + 'this can trigger SVN bug #2344.')
    return symlink

def check_logmsg(txn, repos, errors):
    """Make sure a suitable log message was supplied"""
    marker = 'DO NOT' + ' COMMIT'
    cmd = [svnlook, 'log', '-t', txn, repos]
    pipe = Popen(cmd, shell=False, stdout=PIPE).stdout
    logmsg = pipe.readline().rstrip('\r\n')
    if len(logmsg) < 8:
        errors.append("Please provide a log message to describe " \
                      + "what you changed.");
    elif logmsg.find(marker) >= 0:
        errors.append('Log message contains the string "%s"' % marker)

def main(repos, txn):
    errors = []

    modfiles = get_modified_files(txn, repos)
    for filename in modfiles:
        # Don't check symlinks, since they are not stored as true symlinks in
        # the SVN transaction, so the files aren't valid C++, Python etc.
        if not is_symlink(filename, txn, repos, errors):
            check_modified_file(filename, txn, repos, errors)
    check_logmsg(txn, repos, errors)

    if len(errors) > 0:
        sys.stderr.write("Change rejected by pre-commit for the " \
                         + "following reasons:\n\n")
        sys.stderr.write("\n".join(errors))
        sys.stderr.write("\n")
        sys.exit(1)

if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])
