#!/usr/bin/env python
"""
A Mercurial extension that allows the user to quickly deal with foreign files.

This script allows you to add or delete files that are present but unknown to a
repository. It deals with the case where you have been working for a while in a
repository and forgot to tell Mercurial to add add some files before committing,
or want to cleanup and delete some of those files lying around.

Foreign runs 'status' on the given directories, to find out which files are
unaccounted for and for each of these files, it interactively asks you what to
do with it (commands are invoked with one keystroke)::

- add it in the repository (for the next commit)
- delete it
- ignore/mask it (adds the path to .hgignore)
- view the file with a pager (e.g., more)
- skip it for now (leave it where it is)
- quit/exit (stop doing this)

The script works interactively and is meant to allow you to quickly deal with
the forgotten files in a checkout checkout.

(In svn-foreign, deleted files were being backed up to a temporary location
(using some code from project xxdiff). I will eventually integrate this code in
here.)
"""
# This should work with Mercurial 1.1.

__author__ = 'Martin Blais <blais@furius.ca>'

# stdlib imports
import sys, os, termios, tty, tempfile, datetime, re
from operator import itemgetter
from subprocess import Popen, PIPE, call
from os.path import *

# mercurial imports
from mercurial import hg, cmdutil
from mercurial.commands import revert as hg_revert


def foreign(ui, repo, *pats, **opts):
    """query interactively for actions on foreign and deleted files.

    Show the status of files that are not tracked or that have been deleted but
    are still tracked in the current checkout. For each file, the user is interactively requested to take an action:

    - add file
    - ignore file (modifies .hgignore)
    - skip file
    - delete file
    - revert file

    and more.

    ! = deleted, but still tracked
    ? = not tracked
    """

    node1, node2 = cmdutil.revpair(repo, opts.get('rev'))

    matcher = cmdutil.match(repo, pats, opts)
    cwd = (pats and repo.getcwd()) or ''
    s = repo.status(node1=node1,
                    node2=node2,
                    match=matcher,
                    unknown=True)
    unknown, deleted = s[4], s[3]
    allchanges = (('?', unknown), ('!', deleted))

    # First list the entire list of changes.
    for char, changes in allchanges:
        format = "%s %%s\n" % char
        for f in changes:
            ui.write(format % repo.pathto(f, cwd))
    ui.write('\n')

    query_unregistered_svn_files(unknown, deleted, opts, ui, repo)


def query_unregistered_svn_files(unknown, deleted, opts, ui, repo):
    """
    Runs an 'svn status' command, and then loops over all the files that are not
    registered, asking the user one-by-one what action to take (see this
    module's docstring for more details).

    Return True upon completion.  Anything else signals that the user has
    interrupted the procedure.
    """

    # Get out if there is nothing to do.
    if len(unknown) + len(deleted) == 0:
        ui.write('(Nothing to do.)\n')
        return True

    # Process unknown files.
    for fn in unknown:
        # Ignore Emacs temporary files.
        if basename(fn).startswith('.#'):
            continue
        afn = join(repo.root, fn)
        try:
            size = getsize(afn)
        except OSError:
            # If the file cannot be read for size... don't crash.
            size = 0
        ftype = ''
        if islink(fn):
            ftype = '(symlink)'

        # Command loop.
        while True:
            ui.write('=> [Add|Delete|Mask|Skip|View|Quit]   %10d  %s %s ? ' %
                  (size, fn, ftype))

            # Read command
            c = read_one()
            ui.write(c)
            ui.write('\n')

            if c == 'a': # Add
                add(fn, ui, repo)
                break

            elif c in ('d', 'D'): # Delete
                rmrf(afn, ui)
                break

            elif c in ['m']: # Ignore (Mask, svn/hg ignore)
                ignore(fn, ui, repo)
                break

            elif c == 's': # Skip
                break

            elif c in ('q', 'x', chr(4)): # Quit/exit
                ui.write('(Quitting.)\n')
                return False

            elif c in 'v': # View
                view(join(repo.root, fn), ui.write)

            elif c == chr(12): # Ctrl-L: Clear
                # FIXME: is there a way to do this directly on the terminal?
                call(['clear'])

            else: # Loop again
                ui.write('(Invalid answer: chr %s)\n' % ord(c))


    # Process deleted files.
    for fn in deleted:
        afn = join(repo.root, fn)

        # Command loop.
        while True:
            ui.write('=> [Revert|Delete|Skip|Quit]            %10d  %s %s ? ' %
                  (0, fn, ''))

            # Read command
            c = read_one()
            ui.write(c)
            ui.write('\n')

            if c == 'r': # Revert
                revert(fn, ui, repo)
                break

            elif c in ('d', 'D'): # Delete
                remove(fn, ui, repo)
                break

            elif c == 's': # Skip
                break

            elif c in ('q', 'x', chr(4)): # Quit/exit
                ui.write('(Quitting.)\n')
                return False

            elif c in 'v': # View
                view(fn, ui.write)

            elif c == chr(12): # Ctrl-L: Clear
                # FIXME: is there a way to do this directly on the terminal?
                call(['clear'])

            else: # Loop again
                ui.write('(Invalid answer: chr %s)\n' % ord(c))


    ui.write('(Done.)\n')
    return True


def add(fn, ui, repo):
    """
    Add the file into Mercurial.
    """
    repo[None].add([fn])
    ui.write("Added '%s'\n" % fn)

def remove(fn, ui, repo):
    """
    Add the file into Mercurial.
    """
    repo.remove([fn])
    ui.write("Removed '%s'\n" % fn)

def revert(fn, ui, repo):
    """
    Revert the file.
    """
    opts = dict((n, None) for n in 'date rev all no_backup'.split())
    hg_revert(ui, repo, fn, **opts)
    ui.write("Reverted '%s'\n" % fn)

def ignore(fn, ui, repo):
    """
    Add the file path to the .hgignore file.
    """
    ignore_fn = join(repo.root, '.hgignore')
    f = open(ignore_fn, 'a')
    f.write(fn)
    f.write(os.linesep)
    f.close()
    ui.write("Add ignore pattern for '%s'\n" % fn)

def rmrf(fnodn, ui):
    """
    Delete the given directory and all its contents.
    """
    if not exists(fnodn):
        return

    if isdir(fnodn):
        for root, dirs, files in os.walk(fnodn, topdown=False):
            for fn in files:
                afn = join(root, fn)
                os.remove(afn)
                ui.write("Deleted '%s'\n" % afn)
            for dn in dirs:
                adn = join(root, dn)
                if islink(adn):
                    os.remove(adn)
                else:
                    os.rmdir(adn)
                ui.write("Deleted '%s'\n" % adn)
        os.rmdir(root)
        ui.write("Deleted '%s'\n" % root)
    else:
        os.remove(fnodn)
        ui.write("Deleted '%s'\n" % fnodn)


def read_one():
    """
    Reads a single character from the terminal.
    """
    # Set terminal in raw mode for faster input.
    orig_term_attribs = termios.tcgetattr(sys.stdin.fileno())
    tty.setraw(sys.stdin.fileno())
    try:
        c = sys.stdin.read(1)
        if c == chr(3): # INTR
            raise KeyboardInterrupt
    finally:
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH,
                          orig_term_attribs)
    return c

def view(fn, write):
    "Call on 'more' to view the file."
    write('-' * 80 + '\n')
    pager = os.environ.get('PAGER', '/bin/more')
    call([pager, fn])


cmdtable = {
    "foreign": (foreign, [],
                "hg foreign [options]")
}


