# Copyright (c) 2005 Fredrik Kuivinen <freku045@student.liu.se>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
# 
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import sys, os, re, itertools
from sets import Set
from ctcore import *

def initialize():
    if not os.environ.has_key('GIT_DIR'):
        os.environ['GIT_DIR'] = '.git'

    if not os.environ.has_key('GIT_OBJECT_DIRECTORY'):
        os.environ['GIT_OBJECT_DIRECTORY'] = os.environ['GIT_DIR'] + '/objects'

    if not (os.path.exists(os.environ['GIT_DIR']) and
            os.path.exists(os.environ['GIT_DIR'] + '/refs') and
            os.path.exists(os.environ['GIT_OBJECT_DIRECTORY'])):
        print "Git archive not found."
        print "Make sure that the current working directory contains a '.git' directory, or\nthat GIT_DIR is set appropriately."
        sys.exit(1)

    files = runProgram(['git-diff-files', '--name-only', '-z']).split('\0')
    files.pop()
    updateIndex(['--remove'], files)

class GitFile(File):
    def __init__(self, unknown=False):
        File.__init__(self)
        self.unknown = unknown

    def code(self):
        '''Only defined for non-unknown files'''
        assert(self.text != None)
        return (self.text, self.dstSHA, self.dstMode)

    def getPatchImpl(self):
        if self.unknown:
            updateIndex(['--add'], [self.srcName])
            patch = runProgram(['git-diff-index', '-p', '--cached', 'HEAD', '--', self.srcName])
            updateIndex(['--force-remove'], [self.srcName])
            return patch
        elif self.change == 'C' or self.change == 'R':
            return getPatch(self.srcName, self.dstName)
        else:
            return getPatch(self.srcName)

class GitFileSet(FileSet):
    def __init__(self, addCallback, removeCallback):
        FileSet.__init__(self, addCallback, removeCallback)
        self.codeDict = {}

    def add(self, file):
        if not file.unknown:
            self.codeDict[file.code()] = file
        FileSet.add(self, file)

    def remove(self, file):
        if not file.unknown:
            del self.codeDict[file.code()]
        FileSet.remove(self, file)

    def getByCode(self, file):
        if file.unknown:
            return None
        else:
            return self.codeDict.get(file.code())

def fileSetFactory(addCallback, removeCallback):
    return GitFileSet(addCallback, removeCallback)

parseDiffRE = re.compile(':([0-9]+) ([0-9]+) ([0-9a-f]{40}) ([0-9a-f]{40}) ([MCRNADUT])([0-9]*)')
def parseDiff(prog):
    inp = runProgram(prog)
    ret = []
    try:
        recs = inp.split("\0")
        recs.pop() # remove last entry (which is '')
        it = recs.__iter__()
        while True:
            rec = it.next()
            m = parseDiffRE.match(rec)
            
            if not m:
                print "Unknown output from " + str(prog) + "!: " + rec + "\n"
                continue

            f = GitFile()
            f.srcMode = m.group(1)
            f.dstMode = m.group(2)
            f.srcSHA = m.group(3)
            f.dstSHA = m.group(4)
            if m.group(5) == 'N':
                f.change = 'A'
            else:
                f.change = m.group(5)
            f.score = m.group(6)
            f.srcName = f.dstName = it.next()

            if f.change == 'C' or f.change == 'R':
                f.dstName = it.next()

            ret.append(f)
    except StopIteration:
        pass
    return ret

# origProg is a sequence of strings the first element is the program
# name and subsequent elements are arguments. args is a sequence of
# sequences. The function will repeatedly feed 
#
#    origProg.extend(flatten(args[i:j]))
#
# for some indices i and j to runProgram in such a way that every
# sequence in args is fed to runProgram exactly once.
def runXargsStyle(origProg, args):
    for a in args:
        assert(type(a) is list)
    steps = range(10, len(args), 10)
    prog = origProg[:]
    prev = 0
    for i in steps:
        for a in args[prev:i]:
            prog.extend(a)
        runProgram(prog)
        prog = origProg[:]
        prev = i

    for a in args[prev:]:
        prog.extend(a)
    runProgram(prog)

def updateIndex(args, fileNames):
    # Make sure we don't get one single string as fileNames. As
    # strings are sequences strange things happen in the call to
    # join.
    assert(type(fileNames) is list)

    runProgram(['git-update-index'] + args + ['-z', '--stdin'], input='\0'.join(fileNames)+'\0')

def getUnknownFiles():
     args = []

     if settings().gitExcludeFile():
         if os.path.exists(settings().gitExcludeFile()):
             args.append('--exclude-from=' + settings().gitExcludeFile())
     if settings().gitExcludeDir():
         args.append('--exclude-per-directory=' + settings().gitExcludeDir())

     inp = runProgram(['git-ls-files', '-z', '--others'] + args)
     files = inp.split("\0")
     files.pop() # remove last entry (which is '')
     
     fileObjects = []

     for fileName in files:
         f = GitFile(unknown=True)
         f.srcName = f.dstName = fileName
         f.change = '?'

         fileObjects.append(f)
         f.text = 'New file: ' + fileName

     return fileObjects

def getChangedFiles():
    files = parseDiff('git-diff-index -z -M --cached HEAD')
    for f in files:
        c = f.change
        if   c == 'C':
            f.text = 'Copy from ' + f.srcName + ' to ' + f.dstName
        elif c == 'R':
            f.text = 'Rename from ' + f.srcName + ' to ' + f.dstName
        elif c == 'A':
            f.text = 'New file: ' + f.srcName
        elif c == 'D':
            f.text = 'Deleted file: ' + f.srcName
        elif c == 'T':
            f.text = 'Type change: ' + f.srcName
        else:
            f.text = f.srcName
    return files

# HEAD is src in the returned File objects. That is, srcName is the
# name in HEAD and dstName is the name in the cache.
def updateFiles(fileSet):
    files = parseDiff('git-diff-files -z')
    updateIndex(['--remove', '--add', '--replace'], [f.srcName for f in files])

    markForDeletion = Set()
    for f in fileSet:
        markForDeletion.add(f)

    if settings().showUnknown:
        unknowns = getUnknownFiles()
    else:
        unknowns = []

    files = getChangedFiles()
    
    for f in itertools.chain(files, unknowns):
        fs = fileSet.getByCode(f)
        if fs:
            markForDeletion.discard(fs)
        else:
            fileSet.add(f)

    for f in markForDeletion:
        fileSet.remove(f)

def getPatch(file, otherFile = None):
    if otherFile:
        f = [file, otherFile]
    else:
        f = [file]
    return runProgram(['git-diff-index', '-p', '-M', '--cached', 'HEAD', '--'] + f)

def doCommit(filesToKeep, filesToCommit, msg):
    # If we have a new file in the cache which we do not want to
    # commit we have to remove it from the cache. We will add this
    # cache entry back in to the cache at the end of this
    # function.
    updateIndex(['--force-remove'],
                [f.srcName for f in filesToKeep if f.change == 'A'])

    updateIndex(['--force-remove'],
                [f.dstName for f in filesToKeep if f.change == 'R'])
    runXargsStyle(['git-update-index', '--add', '--replace'],
                  [['--cacheinfo', f.srcMode, f.srcSHA, f.srcName] \
                   for f in filesToKeep if f.change == 'R'])

    runXargsStyle(['git-update-index', '--add', '--replace'],
                  [['--cacheinfo', f.srcMode, f.srcSHA, f.srcName] \
                   for f in filesToKeep if f.change != 'A' and \
                                           f.change != 'R' and \
                                           f.change != '?'])

    updateIndex(['--add'], [f.dstName for f in filesToCommit if f.change == '?'])

    tree = runProgram(['git-write-tree'])
    tree = tree.rstrip()

    if commitIsMerge():
        merge = ['-p', 'MERGE_HEAD']
    else:
        merge = []
    commit = runProgram(['git-commit-tree', tree, '-p', 'HEAD'] + merge, msg).rstrip()
    
    runProgram(['git-update-ref', 'HEAD', commit])

    try:
        os.unlink(os.environ['GIT_DIR'] + '/MERGE_HEAD')
    except OSError:
        pass

    # Don't add files that are going to be deleted back to the cache
    runXargsStyle(['git-update-index', '--add', '--replace'],
                  [['--cacheinfo', f.dstMode, f.dstSHA, f.dstName] \
                   for f in filesToKeep if f.change != 'D' and \
                                           f.change != '?'])
    updateIndex(['--remove'], [f.srcName for f in filesToKeep if f.change == 'R'])

def discardFile(file):
    runProgram(['git-read-tree', 'HEAD'])
    c = file.change
    if c == 'M' or c == 'T':
        runProgram(['git-checkout-index', '-f', '-q', '--', file.dstName])
    elif c == 'A' or c == 'C':
        # The file won't be tracked by git now. We could unlink it
        # from the working directory, but that seems a little bit
        # too dangerous.
        pass 
    elif c == 'D':
        runProgram(['git-checkout-index', '-f', '-q', '--', file.dstName])
    elif c == 'R':
        # Same comment applies here as to the 'A' or 'C' case.
        runProgram(['git-checkout-index', '-f', '-q', '--', file.srcName])

def ignoreFile(file):
    ignoreExpr = re.sub(r'([][*?!\\])', r'\\\1', file.dstName)
    
    excludefile = settings().gitExcludeFile()
    excludefiledir = os.path.dirname(excludefile)
    if not os.path.exists(excludefiledir):
        os.mkdir(excludefiledir)
    if not os.path.isdir(excludefiledir):
        return
    exclude = open(excludefile, 'a')
    print >> exclude, ignoreExpr
    exclude.close()

    pass

def commitIsMerge():
    try:
        os.stat(os.environ['GIT_DIR'] + '/MERGE_HEAD')
        return True
    except OSError:
        return False

def mergeMessage():
    return '''This is a merge commit if you do not want to commit a ''' + \
           '''merge remove the file $GIT_DIR/MERGE_HEAD.'''

# This caching is here to avoid forking and execing git-symbolic-ref all the
# time.
cachedBranch = None
prevStat = None
def getCurrentBranch():
    global prevStat, cachedBranch
    newStat = list(os.lstat(os.environ['GIT_DIR'] + '/HEAD'))
    newStat[7] = 0 # Number 7 is atime and we don't care about atime
    if newStat == prevStat:
        return cachedBranch

    prevStat = newStat

    b = runProgram(['git-symbolic-ref', 'HEAD'])
    cachedBranch = b.rstrip().replace('refs/heads/', '', 1)
    return cachedBranch
