#
# Stuff for dealing with the git log output.
#
# Someday this will be the only version of grabpatch, honest.
#
import re, datetime
from email.utils import parsedate
from patterns import patterns
import database


#
# Input file handling.  Someday it would be good to make this smarter
# so that it handles running git with the right options and such.
#
# Someday.
#
SavedLine = ''

def getline(input):
    global SavedLine
    if SavedLine:
        ret = SavedLine
        SavedLine = ''
        return ret
    l = input.readline()
    if not l:
        return None
    #
    # In theory everything coming out of git is utf8.  In practice,
    # there's some funky stuff in the kernel repo.  So...fall back
    # to latin1 if a utf8 decode fails; that doesn't work for everything
    # but it's about as good as we can do.
    #
    try:
        l = l.decode('utf8')
    except UnicodeDecodeError:
        l = l.decode('latin1')
    return l.rstrip()
    
def SaveLine(line):
    global SavedLine
    SavedLine = line

#
# A simple state machine based on where we are in the patch.  The
# first stuff we get is the header.
#
S_HEADER = 0
#
# Then comes the single-line description.
#
S_DESC = 1
#
# ...the full changelog...
#
S_CHANGELOG = 2
#
# ...the tag section....
#
S_TAGS = 3
#
# ...the numstat section.
#
S_NUMSTAT = 4

S_DONE = 5

#
# The functions to handle each of these states.
#
def get_header(patch, line, input):
    if line == '':
        if patch.author == '':
            print('Funky auth line in', patch.commit)
            patch.author = database.LookupStoreHacker('Unknown',
                                                      'unknown@hacker.net')
        return S_DESC
    m = patterns['author'].match(line)
    if m:
        patch.email = database.RemapEmail(m.group(2))
        patch.author = database.LookupStoreHacker(m.group(1), patch.email)
    else:
        m = patterns['date'].match(line)
        if m:
            dt = parsedate(m.group(2))
            patch.date = datetime.date(dt[0], dt[1], dt[2])
    return S_HEADER

def get_desc(patch, line, input):
    if not line:
        print('Missing desc in', patch.commit)
        return S_CHANGELOG
    patch.desc = line
    line = getline(input)
    while line:
        patch.desc += line
        line = getline(input)
    return S_CHANGELOG

tagline = re.compile(r'^\s+(([-a-z]+-by)|cc|fixes):.*$', re.I)
def get_changelog(patch, line, input):
    if not line:
        if patch.templog:
            patch.changelog += patch.templog
            patch.templog = ''
    if patterns['commit'].match(line):
        # No changelog at all - usually a Linus tag
        SaveLine(line)
        return S_DONE
    elif tagline.match(line):
        if patch.templog:
            patch.changelog += patch.templog
        return get_tag(patch, line, input)
    else:
        patch.templog += line + '\n'
    return S_CHANGELOG

def get_tag(patch, line, input):
    #
    # Some people put blank lines in the middle of tags.
    #
    if not line:
        return S_TAGS
    #
    # A new commit line says we've gone too far.
    #
    if patterns['commit'].match(line):
        SaveLine(line)
        return S_DONE
    #
    # Check for a numstat line
    #
    if patterns['numstat'].match(line):
        return get_numstat(patch, line, input)
    #
    # Look for interesting tags
    #
    m = patterns['signed-off-by'].match(line)
    if m:
        patch.signoffs.append(m.group(2))
    else:
        #
        # Look for other tags indicating that somebody at least
        # looked at the patch.
        #
        for tag in ('acked-by', 'reviewed-by', 'tested-by'):
            if patterns[tag].match(line):
                patch.othertags += 1
                break
    patch.taglines.append(line)
    return S_TAGS

def get_numstat(patch, line, input):
    m = patterns['numstat'].match(line)
    if not m:
        return S_DONE
    try:
        patch.addfile(int(m.group(1)), int(m.group(2)), m.group(3))
    #
    # Binary files just have "-" in the line fields.  In this case, set
    # the counts to zero so that we at least track that the file was
    # touched.
    #
    except ValueError:
        patch.addfile(0, 0, m.group(3))
    return S_NUMSTAT

grabbers = [ get_header, get_desc, get_changelog, get_tag, get_numstat ]


#
# A variant on the gitdm patch class.
#
class patch:
    def __init__(self, commit):
        self.commit = commit
        self.desc = ''
        self.changelog = ''
        self.templog = ''
        self.author = ''
        self.signoffs = [ ]
        self.othertags = 0
        self.added = self.removed = 0
        self.files = [ ]
        self.taglines = [ ]

    def addfile(self, added, removed, file):
        self.added += added
        self.removed += removed
        self.files.append(file)

def grabpatch(input):
    #
    # If it's not a patch something is screwy.
    #
    line = getline(input)
    if line is None:
        return None
    m = patterns['commit'].match(line)
    if not m:
        print('noncommit', line)
        return None
    p = patch(m.group(1))
    state = S_HEADER
    #
    # Crank through the patch.
    #
    while state != S_DONE:
        line = getline(input)
        if line is None:
            if state != S_NUMSTAT:
                print('Ran out of patch', state)
                return None
            return p
        state = grabbers[state](p, line, input)
    return p
