"""Performs a 3-way merge for GIT files
"""

__copyright__ = """
Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>

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
from stgit.exception import *
from stgit import basedir
from stgit.config import config, file_extensions, ConfigOption
from stgit.utils import append_string
from stgit.out import *
from stgit.run import *

class GitMergeException(StgException):
    pass


#
# Options
#
merger = ConfigOption('stgit', 'merger')
keeporig = ConfigOption('stgit', 'keeporig')

#
# Utility functions
#
def __str2none(x):
    if x == '':
        return None
    else:
        return x

class MRun(Run):
    exc = GitMergeException # use a custom exception class on errors

def __checkout_files(orig_hash, file1_hash, file2_hash,
                     path,
                     orig_mode, file1_mode, file2_mode):
    """Check out the files passed as arguments
    """
    global orig, src1, src2

    extensions = file_extensions()

    if orig_hash:
        orig = path + extensions['ancestor']
        tmp = MRun('git', 'unpack-file', orig_hash).output_one_line()
        os.chmod(tmp, int(orig_mode, 8))
        os.renames(tmp, orig)
    if file1_hash:
        src1 = path + extensions['current']
        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
        os.chmod(tmp, int(file1_mode, 8))
        os.renames(tmp, src1)
    if file2_hash:
        src2 = path + extensions['patched']
        tmp = MRun('git', 'unpack-file', file2_hash).output_one_line()
        os.chmod(tmp, int(file2_mode, 8))
        os.renames(tmp, src2)

    if file1_hash and not os.path.exists(path):
        # the current file might be removed by GIT when it is a new
        # file added in both branches. Just re-generate it
        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
        os.chmod(tmp, int(file1_mode, 8))
        os.renames(tmp, path)

def __remove_files(orig_hash, file1_hash, file2_hash):
    """Remove any temporary files
    """
    if orig_hash:
        os.remove(orig)
    if file1_hash:
        os.remove(src1)
    if file2_hash:
        os.remove(src2)

def __conflict(path):
    """Write the conflict file for the 'path' variable and exit
    """
    append_string(os.path.join(basedir.get(), 'conflicts'), path)


def interactive_merge(filename):
    """Run the interactive merger on the given file. Note that the
    index should not have any conflicts.
    """
    extensions = file_extensions()

    ancestor = filename + extensions['ancestor']
    current = filename + extensions['current']
    patched = filename + extensions['patched']

    if os.path.isfile(ancestor):
        three_way = True
        files_dict = {'branch1': current,
                      'ancestor': ancestor,
                      'branch2': patched,
                      'output': filename}
        imerger = config.get('stgit.i3merge')
    else:
        three_way = False
        files_dict = {'branch1': current,
                      'branch2': patched,
                      'output': filename}
        imerger = config.get('stgit.i2merge')

    if not imerger:
        raise GitMergeException, 'No interactive merge command configured'

    # check whether we have all the files for the merge
    for fn in [filename, current, patched]:
        if not os.path.isfile(fn):
            raise GitMergeException, \
                  'Cannot run the interactive merge: "%s" missing' % fn

    mtime = os.path.getmtime(filename)

    out.info('Trying the interactive %s merge'
             % (three_way and 'three-way' or 'two-way'))

    err = os.system(imerger % files_dict)
    if err != 0:
        raise GitMergeException, 'The interactive merge failed: %d' % err
    if not os.path.isfile(filename):
        raise GitMergeException, 'The "%s" file is missing' % filename
    if mtime == os.path.getmtime(filename):
        raise GitMergeException, 'The "%s" file was not modified' % filename


#
# Main algorithm
#
def merge(orig_hash, file1_hash, file2_hash,
          path,
          orig_mode, file1_mode, file2_mode):
    """Three-way merge for one file algorithm
    """
    __checkout_files(orig_hash, file1_hash, file2_hash,
                     path,
                     orig_mode, file1_mode, file2_mode)

    # file exists in origin
    if orig_hash:
        # modified in both
        if file1_hash and file2_hash:
            # if modes are the same (git-read-tree probably dealt with it)
            if file1_hash == file2_hash:
                if os.system('git update-index --cacheinfo %s %s %s'
                             % (file1_mode, file1_hash, path)) != 0:
                    out.error('git update-index failed')
                    __conflict(path)
                    return 1
                if os.system('git checkout-index -u -f -- %s' % path):
                    out.error('git checkout-index failed')
                    __conflict(path)
                    return 1
                if file1_mode != file2_mode:
                    out.error('File added in both, permissions conflict')
                    __conflict(path)
                    return 1
            # 3-way merge
            else:
                merge_ok = os.system(str(merger) % {'branch1': src1,
                                                    'ancestor': orig,
                                                    'branch2': src2,
                                                    'output': path }) == 0

                if merge_ok:
                    os.system('git update-index -- %s' % path)
                    __remove_files(orig_hash, file1_hash, file2_hash)
                    return 0
                else:
                    out.error('Three-way merge tool failed for file "%s"'
                              % path)
                    # reset the cache to the first branch
                    os.system('git update-index --cacheinfo %s %s %s'
                              % (file1_mode, file1_hash, path))

                    if config.get('stgit.autoimerge') == 'yes':
                        try:
                            interactive_merge(path)
                        except GitMergeException, ex:
                            # interactive merge failed
                            out.error(str(ex))
                            if str(keeporig) != 'yes':
                                __remove_files(orig_hash, file1_hash,
                                               file2_hash)
                            __conflict(path)
                            return 1
                        # successful interactive merge
                        os.system('git update-index -- %s' % path)
                        __remove_files(orig_hash, file1_hash, file2_hash)
                        return 0
                    else:
                        # no interactive merge, just mark it as conflict
                        if str(keeporig) != 'yes':
                            __remove_files(orig_hash, file1_hash, file2_hash)
                        __conflict(path)
                        return 1

        # file deleted in both or deleted in one and unchanged in the other
        elif not (file1_hash or file2_hash) \
               or file1_hash == orig_hash or file2_hash == orig_hash:
            if os.path.exists(path):
                os.remove(path)
            __remove_files(orig_hash, file1_hash, file2_hash)
            return os.system('git update-index --remove -- %s' % path)
        # file deleted in one and changed in the other
        else:
            # Do something here - we must at least merge the entry in
            # the cache, instead of leaving it in U(nmerged) state. In
            # fact, stg resolved does not handle that.

            # Do the same thing cogito does - remove the file in any case.
            os.system('git update-index --remove -- %s' % path)

            #if file1_hash:
                ## file deleted upstream and changed in the patch. The
                ## patch is probably going to move the changes
                ## elsewhere.

                #os.system('git update-index --remove -- %s' % path)
            #else:
                ## file deleted in the patch and changed upstream. We
                ## could re-delete it, but for now leave it there -
                ## and let the user check if he still wants to remove
                ## the file.

                ## reset the cache to the first branch
                #os.system('git update-index --cacheinfo %s %s %s'
                #          % (file1_mode, file1_hash, path))
            __conflict(path)
            return 1

    # file does not exist in origin
    else:
        # file added in both
        if file1_hash and file2_hash:
            # files are the same
            if file1_hash == file2_hash:
                if os.system('git update-index --add --cacheinfo %s %s %s'
                             % (file1_mode, file1_hash, path)) != 0:
                    out.error('git update-index failed')
                    __conflict(path)
                    return 1
                if os.system('git checkout-index -u -f -- %s' % path):
                    out.error('git checkout-index failed')
                    __conflict(path)
                    return 1
                if file1_mode != file2_mode:
                    out.error('File "s" added in both, permissions conflict'
                              % path)
                    __conflict(path)
                    return 1
            # files added in both but different
            else:
                out.error('File "%s" added in branches but different' % path)
                # reset the cache to the first branch
                os.system('git update-index --cacheinfo %s %s %s'
                          % (file1_mode, file1_hash, path))

                if config.get('stgit.autoimerge') == 'yes':
                    try:
                        interactive_merge(path)
                    except GitMergeException, ex:
                        # interactive merge failed
                        out.error(str(ex))
                        if str(keeporig) != 'yes':
                            __remove_files(orig_hash, file1_hash,
                                           file2_hash)
                        __conflict(path)
                        return 1
                    # successful interactive merge
                    os.system('git update-index -- %s' % path)
                    __remove_files(orig_hash, file1_hash, file2_hash)
                    return 0
                else:
                    # no interactive merge, just mark it as conflict
                    if str(keeporig) != 'yes':
                        __remove_files(orig_hash, file1_hash, file2_hash)
                    __conflict(path)
                    return 1
        # file added in one
        elif file1_hash or file2_hash:
            if file1_hash:
                mode = file1_mode
                obj = file1_hash
            else:
                mode = file2_mode
                obj = file2_hash
            if os.system('git update-index --add --cacheinfo %s %s %s'
                         % (mode, obj, path)) != 0:
                out.error('git update-index failed')
                __conflict(path)
                return 1
            __remove_files(orig_hash, file1_hash, file2_hash)
            return os.system('git checkout-index -u -f -- %s' % path)

    # Unhandled case
    out.error('Unhandled merge conflict: "%s" "%s" "%s" "%s" "%s" "%s" "%s"'
              % (orig_hash, file1_hash, file2_hash,
                 path,
                 orig_mode, file1_mode, file2_mode))
    __conflict(path)
    return 1
