#!/usr/bin/python
# -*- coding: utf-8 -*-
#---------------------------------------------------------------------
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#---------------------------------------------------------------------
# Copyright © 2012 James Hunt.
#
# Author: James Hunt <james.hunt@ubuntu.com>
# Description: Bazaar pre-commit hook that inspects modified files
#              looking for signs that work has not been completed.
# Date: 18 December 2012
#---------------------------------------------------------------------
# To use:
#
# - copy or link this file to "~/.bazaar/plugins/bzr_find_fluff_pre_commit_hook.py"
# - review the user configurables, specifically 'regex_list' (see below).
# - try running "bzr commit" on one of your branches.
#
# Caveat Emptor:
#
# This hook only checks files that are modified by a commit so if you
# want it to check the entire tree, you'll need to tweak it.
#
# Ideas:
#
# - only commit if test build successful.
# - only commit if test suite run successful.
# - only commit if static analysis tool deems code good.
#
#---------------------------------------------------------------------
# TODO:
#
# - ability to restrict to named file patterns
# - allow settings to be modified using environment variables maybe?
# - ability to restrict branches this hook works on by using an
#   env var containing a regex to represent branches to use/ignore.
# - provide post-context too.
#---------------------------------------------------------------------

"""
Simple Bazaar pre-commit hook that checks each modified file for a set
of predefined regular expressions suggestive that the work is still not
done, or that code tidyup should occur prior to committing the changes.

Additionally, the hook looks for signs that you forgot to update the
ChangeLog or your project or debian package.
"""

from bzrlib import branch, ui, workingtree
import os
import sys
import re
import mimetypes

__version__ = '0.0.2'
version_info = 0,0,2

#---------------------------------------------------------------------
# XXX: START USER CONFIGURABLES

# list of regular expressions that this hook will search for in all
# modified files.
regex_list = [ \
    r'\bFIXME\b',    # FIXME
    r'\bTODO\b',     # TODO
    r'^#if (0|1)',   # explicitly commented out/in blocks
    r'^\/\/',        # C++ comments
]

# number of lines of pre-context to show when a regex match is found
# in a modified file.
context_lines = 5

# set to 1 to enable somewhat lame debug output ;)
debug = 0

DEBIAN_CHANGELOG = 'debian/changelog'

# XXX: END USER CONFIGURABLES
#---------------------------------------------------------------------

# unique name for this hook
tag = "pre-commit-file-checker"

def handle_abort():
    if ui.ui_factory.confirm_action("Abort commit to allow problem to be resolved",
        tag, {}):
        sys.exit("Commit aborted.")

# Display the details of the matching lines to the user and offer them
# the chance to abort the commit.
def handle_match(regex, file, line, linenum, pre_context, branch_name, branch_url, branch_path):

    msg  = ""
    msg += "\n%s: Found unexpected pattern '%s'" % ("ERROR", regex)
    msg += " at line %d in file '%s'" % (linenum, file)
    msg += "\n(branch: name '%s', path '%s', url '%s'):\n" % \
             (branch_name, branch_path, branch_url)

    print msg

    # show context
    for i, pre in enumerate(pre_context):
        if i < context_lines:
            print "%d:%s" % (linenum - context_lines + i, pre)

    # show matching line
    print "%d:%s" % (linenum, line)

    print ""

    handle_abort()

# Check the specified file for lines that match any regular expression
# in regex_list. Retains a list of context lines so user can get a feel
# for where the problem lies.
def check_file(file, branch_name, branch_url, branch_path):
    linenum=0
    pre_context = []
    post_context = []
    for line in open(file):
        linenum += 1

        line = line.rstrip()

        # calculate length prior to appending to ensure we don't record
        # the existing line
        l = len(pre_context)

        pre_context.append(line)

        if l > context_lines:
            # record an extra line in case the current line generates a
            # match (thus allowing us to still display context_lines
            # worth of pre-context).
            pre_context = pre_context[-(context_lines+1):]

        for regex in regex_list:
            if re.search(regex, line):
                handle_match(regex, file, line, linenum, pre_context, branch_name, branch_url, branch_path)


def pre_commit_hook(local, master, old_revno, old_revid, future_revno, future_revid, tree_delta, future_tree):

    changelog_exists = False
    debian_changelog_exists = False
    debian_files_changed = False
    non_debian_files_changed = False

    changelog = None
    debian_changelog = None

    # A Debian changelog file is easily to identify as it has a
    # prescribed name.
    if os.path.exists(DEBIAN_CHANGELOG):
        debian_changelog_exists = True

    # non-Debian changelog files come in a variety of names/cases,
    # so try and pick out all possibles.
    changelogs = filter(lambda file:
        re.match("changelog", file, flags=re.IGNORECASE) != None, \
        os.listdir('.'))

    if changelogs:
        changelog_exists = True

    mimetypes.init()

    print "\n"
    if debug:
        sys.stdout.write("%s - Checking modified files for signs of unfinished work... " % tag)

    working_tree = workingtree.WorkingTree.open("%s" % master.user_url)
    branch_name = master.name
    branch_url = master.user_url
    branch_path = working_tree.basedir

    # XXX: could potentially filter by branch at this point
    # XXX: (disable/enable this hook for particular branches).

    # Search through modified files for common patterns that suggest
    # work is not yet done. Any matches are displayed to the user to
    # allow them to interactively abort the commit, or continue.
    for file in tree_delta.modified + tree_delta.added + tree_delta.renamed:
        filename = file[0]

        if filename.lower() == 'changelog':
            changelog = filename
        elif filename == DEBIAN_CHANGELOG:
            debian_changelog = filename

        if re.match ('debian/', filename):
            debian_files_changed = True
        else:
            non_debian_files_changed = True

        mime = mimetypes.guess_type(filename)

        if not mime[0] and not mime[1]:
            if debug:
                print "ignoring unknown/binary file %s" % filename
            continue

        if debug:
            print "%s:%s:checking file '%s'" % (tag, "DEBUG", filename)
        check_file (filename, branch_name, branch_url, branch_path)

    if debug:
        sys.stdout.write("Done.\n")

    if changelog and debian_changelog:
        print "WARNING: both '%s' and '%s' have been modified" % (changelog, debian_changelog)
        print "WARNING: (this is generally an error)"
        handle_abort()

    if debian_files_changed and non_debian_files_changed:
        print "WARNING: both Debian and non-debian files have been modified"
        print "WARNING: (this is generally an error)"
        handle_abort()

    if debian_files_changed and debian_changelog_exists and not debian_changelog:
        print "WARNING: you have forgotten to update '%s' using dch(1)" % DEBIAN_CHANGELOG
        handle_abort()

    if non_debian_files_changed and changelog_exists and not changelog:
        print "WARNING: you have forgotten to update the changelog ('%s')" % changelogs
        handle_abort()

# install bzr hook
branch.Branch.hooks.install_named_hook('pre_commit', pre_commit_hook,
        'My pre-commit hook')
