# -*- coding: utf-8 -*-

# Copyright (c) 2005 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the version control systems interface to Mercurial.
"""

import os
import shutil
import types
import urllib

from qt import *

import mercurial.commands as commands
import mercurial.util as util
from mercurial.hg import repository, RepoError
import mercurial.hg as hg

from KdeQt import KQMessageBox, KQInputDialog

from VCS.VersionControl import VersionControl
from ProjectBrowserHelper import HgProjectBrowserHelper
from ProjectHelper import HgProjectHelper
from HgDialog import HgDialog, HgDummyDialog
from OptionsDialog import HgOptionsDialog
from NewProjectDialog import HgNewProjectOptionsDialog
from CommitDialog import HgCommitDialog
from StatusDialog import HgStatusDialog
from TagDialog import HgTagDialog
from TagListDialog import HgTagListDialog
from DiffDialog import HgDiffDialog
from LogDialog import HgLogDialog
from RevisionSelectionDialog import HgRevisionSelectionDialog
from SwitchDialog import HgSwitchDialog
from CommandDialog import HgCommandDialog
from CopyDialog import HgCopyDialog
from PullDialog import HgPullDialog
from PushDialog import HgPushDialog
from IncomingDialog import HgIncomingDialog
from OutgoingDialog import HgOutgoingDialog
from ManifestDialog import HgManifestDialog

import Utilities

class Mercurial(VersionControl):
    """
    Class implementing the version control systems interface to Mercurial.
    """
    def __init__(self, parent=None, name=None):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        @param name name of this object (string or QString)
        """
        VersionControl.__init__(self, parent, name)
        
        self.options = self.defaultOptions
        self.tagList = QStringList()
        self.commandHistory = QStringList()

    def vcsExists(self):
        """
        Public method used to test for the presence of Mercurial.
        
        @return flag indicating the existance (boolean)
        """
        self.versionStr = QString.null
        
        try:
            import mercurial.version
            vers = mercurial.version.get_version()
            if vers != mercurial.version.unknown_version:
                self.versionStr = QString(vers)
                return 1
            else:
                return 0
        except ImportError:
            return 0
        
    def vcsConvertProject(self, vcsDataDict, project):
        """
        Public method to convert an uncontrolled project to a version controlled project.
        
        @param vcsDataDict dictionary of data required for the conversion
        @param project reference to the project object
        """
        success = self.vcsImport(vcsDataDict, project.ppath)[0]
        if not success:
            KQMessageBox.critical(None,
                self.trUtf8("Create project repository"),
                self.trUtf8("""The project repository could not be created."""),
                self.trUtf8("&OK"),
                QString.null,
                QString.null,
                0, -1)
        else:
            pfn = project.pfile
            if not os.path.isfile(pfn):
                pfn += "z"
            project.closeProject()
            project.openProject(pfn)
        
    def vcsImport(self, vcsDataDict, projectDir, noDialog=0):
        """
        Public method used to import the project into the Mercurial repository.
        
        @param vcsDataDict dictionary of data required for the import
        @param projectDir project directory (string)
        @param noDialog flag indicating quiet operations
        @return flag indicating an execution without errors (boolean)
        """
        projectDir = str(projectDir)
        msg = vcsDataDict["message"]
        if not msg:
            msg = '***'
        
        ui = HgDialog(self.trUtf8("Creating Mercurial repository"), 
                      "init %s" % projectDir)
        try:
            # init the repository
            commands.init(ui, projectDir)
            # prepare for the initial add
            repo = repository(ui, projectDir)
            # perform addremove
            cwd = os.getcwd()
            os.chdir(projectDir)
            # perform the initial commit
            cmdoptions = self._makeOptions("commit", 
                                            {"message" : vcsDataDict["message"],
                                             "addremove" : 1})
            commands.commit(ui, repo, **cmdoptions)
            os.chdir(cwd)
            status = 1
        except:
            status = 0
        ui.finish()
        ui.exec_loop()
        
        return status, 1
        
    def vcsCheckout(self, vcsDataDict, projectDir, noDialog=0):
        """
        Public method used to check the project out of the Mercurial repository.
        
        @param vcsDataDict dictionary of data required for the checkout
        @param projectDir project directory to create (string)
        @param noDialog flag indicating quiet operations
        @return flag indicating an execution without errors (boolean)
        """
        projectDir = str(projectDir)
        if os.path.isdir(projectDir):
            os.rmdir(projectDir)
        
        try:
            tag = vcsDataDict["tag"]
        except KeyError:
            tag = None
        vcsDir = self.hgNormalizeURL(vcsDataDict["url"])
        if vcsDir.startswith('file://'):
            vcsDir = vcsDir[7:]
            
        ui = HgDialog(self.trUtf8("Cloning Mercurial repository"), 
                      "clone %s %s" % (vcsDir, projectDir))
        cmdDict = {}
        if tag:
            cmdDict[noupdate] = 1
        try:
            cmdoptions = self._makeOptions("clone", cmdDict)
            commands.clone(ui, vcsDir, projectDir, **cmdoptions)
            if tag:
                repo = repository(ui, projectDir)
                cmdoptions = self._makeOptions("update", {"branch" : tag})
                commands.update(ui, repo, **cmdoptions)
            status = 1
        except:
            status = 0
        ui.finish()
        ui.exec_loop()
        
        return status
        
    def vcsExport(self, vcsDataDict, projectDir):
        """
        Public method used to export a directory from the Mercurial repository.
        
        @param vcsDataDict dictionary of data required for the checkout
        @param projectDir project directory to create (string)
        @return flag indicating an execution without errors (boolean)
        """
        status = self.vcsCheckout(vcsDataDict, projectDir)
        shutil.rmtree(os.path.join(projectDir, '.hg'), 1)
        return status
        
    def vcsCommit(self, name, message, noDialog=0, parent=None):
        """
        Public method used to make the change of a file/directory permanent in the Mercurial repository.
        
        @param name file/directory name to be committed (string or list of strings)
        @param message message for this operation (string)
        @param noDialog flag indicating quiet operations
        @param parent reference to the parent object of the commit dialog (QWidget)
        @return flag indicating an execution without errors (boolean)
        """
        msg = QString(message)
        
        if not noDialog and msg.isEmpty():
            # call CommitDialog and get message from there
            dlg = HgCommitDialog(parent)
            if dlg.exec_loop() == QDialog.Accepted:
                msg = dlg.logMessage()
            else:
                return 0
        
        if msg.isEmpty():
            msg = '***'
        else:
            msg = unicode(msg)
        
        if type(name) is types.ListType:
            dname, fnames = self.splitPathList(name)
        else:
            dname, fname = self.splitPath(name)
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        names = ''
        pats = []
        if type(name) is types.ListType:
            for n in name:
                if n != repodir:
                    names = "%s %s" % (names, n)
                    pats.append(str(n))
        elif name != repodir:
            names = " %s" % name
            pats = [str(name)]
        
        ui = HgDialog(self.trUtf8("Commiting changes to the Mercurial repository"), 
                      "commit -m %s%s" % (msg, names))
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            cmdoptions = self._makeOptions("commit", {"message" : str(msg)})
            commands.commit(ui, repo, *pats, **cmdoptions)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
        
    def vcsUpdate(self, name, merge=0, node=None):
        """
        Public method used to update a file/directory with the Mercurial repository.
        
        @param name repository path to be updated (string or list of strings)
        @param merge flag indicating a merge operation (boolean)
        @param node tag name or revision to switch to (string)
        """
        if type(name) is types.ListType:
            dname, fnames = self.splitPathList(name)
        else:
            dname, fname = self.splitPath(name)
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return
        
        ui = HgDialog(self.trUtf8("Synchronizing with the Mercurial repository"), 
                      "update%s%s" % ((merge and " -m" or ""), (node and " "+node or "")))
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            commands.update(ui, repo, merge=merge, node=node)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
        
    def vcsAdd(self, name, isDir=0):
        """
        Public method used to add a file/directory to the Mercurial repository.
        
        @param name file/directory name to be added (string)
        @param isDir flag indicating name is a directory (boolean)
        """
        names = ''
        pats = []
        if type(name) is types.ListType:
            if isDir:
                dname, fname = os.path.split(unicode(name[0]))
            else:
                dname, fnames = self.splitPathList(name)
            for n in name:
                names = "%s %s" % (names, n)
                pats.append(str(n))
        else:
            if isDir:
                dname, fname = os.path.split(unicode(name))
            else:
                dname, fname = self.splitPath(name)
            names = " %s" % name
            pats = [str(name)]
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        ui = HgDialog(self.trUtf8("Adding files/directories to the Mercurial repository"), 
                      "add%s" % names)
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            cmdoptions = self._makeOptions("add", {})
            commands.add(ui, repo, *pats, **cmdoptions)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
        
    def vcsAddBinary(self, name, isDir=0):
        """
        Public method used to add a file/directory in binary mode to the Mercurial repository.
        
        @param name file/directory name to be added (string)
        @param isDir flag indicating name is a directory (boolean)
        """
        self.vcsAdd(name, isDir)
        
    def vcsAddTree(self, path):
        """
        Public method to add a directory tree rooted at path to the Mercurial repository.
        
        @param path root directory of the tree to be added (string or list of strings))
        """
        self.vcsAdd(path, 0)
        
    def vcsRemove(self, name, project=0):
        """
        Public method used to remove a file/directory from the Mercurial repository.
        
        The default operation is to remove the local copy as well.
        
        @param name file/directory name to be removed (string or list of strings))
        @param project flag indicating deletion of a project tree (boolean)
        @return flag indicating successfull operation (boolean)
        """
        if project:
            if type(name) is types.ListType:
                name = name[0]
            name = unicode(name)
            cwd = os.getcwd()
            if cwd == name:
                os.chdir(os.path.dirname(name))
            shutil.rmtree(name, 1)
            return 1
            
        if type(name) is types.ListType:
            dname, fnames = self.splitPathList(name)
        else:
            dname, fname = self.splitPath(name)
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        if not type(name) is types.ListType:
            name = [name]
            
        cwd = os.getcwd()
        os.chdir(repodir)
        # remove the files/directories from disk
        for n in name:
            if os.path.isdir(n):
                shutil.rmtree(n, 1)
            elif os.path.exists(n):
                try:
                    os.remove(n)
                except:
                    pass
                    
        # remove them from repository
        ui = HgDummyDialog()
        repo = repository(ui, repodir)
        cmdoptions = self._makeOptions("status", {"removed" : 1})
        _cwd = repo.getcwd()
        _files, _matchfn = commands.matchpats(repo, _cwd, [], cmdoptions)
        (c, a, d, u) = [[util.pathto(_cwd, x) for x in n]
                        for n in repo.changes(files=_files, match=_matchfn)]
        if d:
            names = ""
            for n in d:
                names = "%s %s" % (names, n)
            ui = HgDialog(self.trUtf8("Removing files/directories from the Mercurial repository"), 
                          "remove%s" % names)
            try:
                f1 = d[0]
                del d[0]
                commands.add(ui, repo, f1, *d)
            except:
                pass
            ui.finish()
            ui.exec_loop()
        
        os.chdir(cwd)
        return 1    # always report success
        
    def vcsLog(self, name):
        """
        Public method used to view the log of a file/directory from the Mercurial repository.
        
        @param name file/directory name to show the log of (string)
        """
        self.log = HgLogDialog(self)
        self.log.show()
        self.log.start(name)
        
    def vcsDiff(self, name):
        """
        Public method used to view the difference of a file/directory to the Mercurial repository.
        
        If name is a directory and is the project directory, all project files
        are saved first. If name is a file (or list of files), which is/are being edited 
        and has unsaved modification, they can be saved or the operation may be aborted.
        
        @param name file/directory name to be diffed (string)
        """
        if type(name) is types.ListType:
            names = name[:]
        else:
            names = [name]
        for nam in names:
            if os.path.isfile(nam):
                editor = qApp.mainWidget().getViewManager().getOpenEditor(nam)
                if editor and not editor.checkDirty() :
                    return
            else:
                project = qApp.mainWidget().getProject()
                if nam == project.ppath and not project.saveAllScripts():
                    return
        self.diff = HgDiffDialog(self)
        self.diff.show()
        self.diff.start(name)
        
    def vcsStatus(self, name):
        """
        Public method used to view the status of a file in the Mercurial repository.
        
        @param name file/directory name to show the status of (string or list of strings)
        """
        self.status = HgStatusDialog(self)
        self.status.show()
        self.status.start(name)
        
    def vcsTag(self, name):
        """
        Public method used to set the tag of a file/directory in the Mercurial repository.
        
        @param name file/directory name to be tagged (string)
        """
        dname, fname = self.splitPath(name)
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return
        
        dlg = HgTagDialog(self.tagList)
        if dlg.exec_loop() == QDialog.Accepted:
            tag, tagOp = dlg.getParameters()
            self.tagList.remove(tag)
            self.tagList.prepend(tag)
        else:
            return
            
        cwd = os.getcwd()
        os.chdir(repodir)
        
        ui = HgDialog(self.trUtf8("Tagging in the Mercurial repository"), 
                      "tag %s%s" % (tagOp == 2 and "local " or "", tag))
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            cmdDict = {"message" : "Created tag %s" % tag}
            if tagOp == 2:
                cmdDict["local"] = 1
            cmdoptions = self._makeOptions("tag", cmdDict)
            commands.tag(ui, repo, tag, **cmdoptions)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
        
    def vcsRevert(self, name):
        """
        Public method used to revert changes made to a file/directory.
        
        @param name file/directory name to be reverted (string)
        """
        if type(name) is types.ListType:
            dname, fnames = self.splitPathList(name)
        else:
            dname, fname = self.splitPath(name)
            name = [name]
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return
        
        pats = []
        for f in name:
            f = str(f).replace(repodir, '') # make relative to repodir
            if f.startswith(os.sep):
                f = f[1:]
            if f:
                pats.append(f)
        
        ui = HgDialog(self.trUtf8("Reverting local changes"), 
                      "revert %s" % ' '.join(pats))
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            cmdoptions = self._makeOptions("revert", {})
            commands.revert(ui, repo, *pats, **cmdoptions)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
    
    def vcsSwitch(self, name):
        """
        Public method used to switch a working directory to a different tag.
        
        @param name directory name to be switched (string)
        """
        dlg = HgSwitchDialog(self.tagList)
        if dlg.exec_loop() == QDialog.Accepted:
            tag = dlg.getParameters()
            if tag:
                self.tagList.remove(tag)
                self.tagList.prepend(tag)
        else:
            return
        
        self.vcsUpdate(name, node=tag)
        
    def vcsRegisteredState(self, name):
        """
        Public method used to get the registered state of a file in the vcs.
        
        @param name filename to check (string)
        @return a combination of canBeCommited and canBeAdded or
            0 in order to signal an error
        """
        dname, fname = self.splitPath(name)
        dname = unicode(dname)
        fname = unicode(fname)
        
        if fname == '.':
            if os.path.isdir(os.path.join(dname, '.hg')):
                return self.canBeCommitted
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        ui = HgDummyDialog()
        repo = repository(ui, repodir)
        n = repo.manifest.tip()
        m = repo.manifest.read(n)
        files = m.keys()

        cmdoptions = self._makeOptions("status", {"added" : 1})
        cwd = repo.getcwd()
        cwd = os.getcwd()
        os.chdir(repodir)
        _cwd = repo.getcwd()
        _files, _matchfn = commands.matchpats(repo, _cwd, [], cmdoptions)
        (c, a, d, u) = [[util.pathto(_cwd, x) for x in n]
                        for n in repo.changes(files=_files, match=_matchfn)]
        files.extend(a)
        os.chdir(cwd)

        files.sort()

        for f in files:
            fn = os.path.join(repodir, f)
            if fname == '.':
                if fn.startswith(dname):
                    return self.canBeCommitted
            else:
                if fn == name:
                    return self.canBeCommitted
            
        return self.canBeAdded
        
    def vcsAllRegisteredStates(self, names, dname):
        """
        Public method used to get the registered states of a number of files in the vcs.
        
        @param names dictionary with all filenames to be checked as keys
        @param dname directory to check in (string)
        @return the received dictionary completed with a combination of 
            canBeCommited and canBeAdded or None in order to signal an error
        """
        if not names.keys():
            return names
        
        dname = unicode(dname)
        if dname.endswith(os.sep):
            dname = dname[:-1]
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        ui = HgDummyDialog()
        repo = repository(ui, repodir)
        n = repo.manifest.tip()
        m = repo.manifest.read(n)
        files = m.keys()

        cmdoptions = self._makeOptions("status", {"added" : 1})
        cwd = os.getcwd()
        os.chdir(repodir)
        _cwd = repo.getcwd()
        _files, _matchfn = commands.matchpats(repo, _cwd, [], cmdoptions)
        (c, a, d, u) = [[util.pathto(_cwd, x) for x in n]
                        for n in repo.changes(files=_files, match=_matchfn)]
        files.extend(a)
        os.chdir(cwd)

        files.sort()

        dirs = [x for x in names.keys() if os.path.isdir(x)]
        for f in files:
            fn = os.path.join(repodir, f)
            if os.path.dirname(fn) == dname:
                name = os.path.normcase(fn)
                if names.has_key(name):
                    names[name] = self.canBeCommitted
            elif dirs:
                for d in dirs:
                    if fn.startswith(d):
                        names[d] = self.canBeCommitted
                        dirs.remove(d)
                        break
        
        return names
        
    def vcsName(self):
        """
        Public method returning the name of the vcs.
        
        @return always 'Mercurial' (string)
        """
        return "Mercurial"

    def vcsCleanup(self, name):
        """
        Public method used to cleanup the working directory.
        
        @param name directory name to be cleaned up (string)
        """
        entries = []
        entries.extend(Utilities.direntries(name, 1, '*.orig'))
        for entry in entries:
            try:
                os.remove(entry)
            except OSError:
                pass
        
    def vcsCommandLine(self, name):
        """
        Public method used to execute arbitrary mercurial commands.
        
        @param name directory name of the working directory (string)
        """
        dlg = HgCommandDialog(self.commandHistory, name)
        if dlg.exec_loop() == QDialog.Accepted:
            command = dlg.getData()
            args = Utilities.parseOptionString(command)
            
            # This moves any previous occurence of these arguments to the head
            # of the list.
            self.commandHistory.remove(command)
            self.commandHistory.prepend(command)
            
            try:
                cmd, func, args, options, cmdoptions = commands.parse(args)
            except commands.ParseError, inst:
                KQMessageBox.critical(None,
                    self.trUtf8("Mercurial Command Error"),
                    self.trUtf8("""<p>The Mercurial command could not be executed.</p>"""
                                """<p>Reason: %1</p>""").arg(str(inst)),
                    self.trUtf8("&OK"),
                    QString.null,
                    QString.null,
                    0, -1)
                return
            except UnknownCommand, inst:
                KQMessageBox.critical(None,
                    self.trUtf8("Mercurial Command Error"),
                    self.trUtf8("""<p>Unknown Mercurial command %1</p>""").arg(inst.args[0]),
                    self.trUtf8("&OK"),
                    QString.null,
                    QString.null,
                    0, -1)
                return
                
            ui = HgDialog(self.trUtf8("Mercurial Command"), str(command),
                          debug=options["debug"])
            cwd = os.getcwd()
            os.chdir(name)
            try:
                if cmd not in commands.norepo.split():
                    repo = repository(ui, name)
                    func(ui, repo, *args, **cmdoptions)
                else:
                    func(ui, *args, **cmdoptions)
            except RepoError, inst:
                ui.warn("abort: ", inst, "\n")
            except:
                pass
            os.chdir(cwd)
            ui.finish()
            ui.exec_loop()
    
        
    def vcsOptionsDialog(self, project, archive, editable=0, parent=None):
        """
        Public method to get a dialog to enter repository info.
        
        @param project reference to the project object
        @param archive name of the project in the repository (string)
        @param editable flag indicating that the project name is editable (boolean)
        @param parent parent widget (QWidget)
        """
        return HgOptionsDialog(project, parent)
        
    def vcsNewProjectOptionsDialog(self, parent = None):
        """
        Public method to get a dialog to enter repository info for getting a new project.
        
        @param parent parent widget (QWidget)
        """
        return HgNewProjectOptionsDialog(parent)
        
    def vcsRepositoryInfos(self, ppath):
        """
        Public method to retrieve information about the repository.
        
        @param ppath local path to get the repository infos (string)
        @return string with ready formated info for display (QString)
        """
        info = {\
            'id' : '',
            'defaultPath' : '',
        }
        
        if not os.path.isdir(os.path.join(ppath, '.hg')):
            return self.trUtf8("The repository directory '.hg' does not exist.")
        
        ui = HgDummyDialog()
        repo = repository(ui, ppath)
        cwd = os.getcwd()
        os.chdir(ppath)
        
        # determine repository identification
        parents = [p for p in repo.dirstate.parents() if p != hg.nullid]
        if not parents:
            info['id'] = self.trUtf8("unknown")
        else:
            (c, a, d, u) = repo.changes()
            output = QStringList(QString("%1%2")\
                .arg('+'.join([hg.hex(parent) for parent in parents]))\
                .arg((c or a or d) and self.trUtf8(' (modified)') or "")
            )
            # multiple tags for a single parent separated by '/'
            parenttags = ['/'.join(tags)
                          for tags in map(repo.nodetags, parents) if tags]
            # tags for multiple parents separated by ' + '
            if parenttags:
                output.append(' + '.join(parenttags))
            info['id'] = output.join('<br />')
        
        # determine default path
        defaultPath = ui.expandpath("default")
        if defaultPath and defaultPath != "default":
            info['defaultPath'] = defaultPath
        
        os.chdir(cwd)
        
        return self.trUtf8(\
            """<h3>Repository information</h3>"""
            """<table>"""
            """<tr><td><b>Mercurial V.</b></td><td>%1</td></tr>"""
            """<tr><td><b>Repo Id</b></td><td>%2</td></tr>"""
            """<tr><td><b>Default Path</b></td><td>%3</td></tr>"""
            """</table>"""
            )\
            .arg(self.versionStr)\
            .arg(info['id'])\
            .arg(info['defaultPath'])
    
    ############################################################################
    ## Public Mercurial specific methods are below.
    ############################################################################
    
    def hgExtendedDiff(self, name):
        """
        Public method used to view the difference of a file/directory to the Mercurial repository.
        
        If name is a directory and is the project directory, all project files
        are saved first. If name is a file (or list of files), which is/are being edited 
        and has unsaved modification, they can be saved or the operation may be aborted.
        
        This method gives the chance to enter the revisions to be compared. An entry of 
        0 for any specific revision is identical to the current revision (i.e the
        revision as found in the working copy)
        
        @param name file/directory name to be diffed (string)
        """
        if type(name) is types.ListType:
            names = name[:]
        else:
            names = [name]
        for nam in names:
            if os.path.isfile(nam):
                editor = qApp.mainWidget().getViewManager().getOpenEditor(nam)
                if editor and not editor.checkDirty() :
                    return
            else:
                project = qApp.mainWidget().getProject()
                if nam == project.ppath and not project.saveAllScripts():
                    return
        dlg = HgRevisionSelectionDialog()
        if dlg.exec_loop() == QDialog.Accepted:
            revisions = dlg.getRevisions()
            self.diff = HgDiffDialog(self)
            self.diff.show()
            self.diff.start(name, revisions)

    def hgNormalizeURL(self, url):
        """
        Private method to normalize a url for Mercurial.
        
        @param url url string (string)
        @return properly normalized url for Mercurial
        """
        url = url.replace('\\', '/')
        if url.endswith('/'):
            url = url[:-1]
        urll = url.split('//')
        return "%s//%s" % (urll[0], '/'.join(urll[1:]))

    def hgListTags(self, path):
        """
        Public method used to list the available tags.
        
        @param path directory name of the project (string)
        """
        self.tagListDlg = HgTagListDialog(self)
        self.tagListDlg.show()
        self.tagListDlg.start(path)
    
    def hgMerge(self, path):
        """
        @param path repository path to be updated (string or list of strings)
        """
        self.vcsUpdate(path, 1)

    def hgCopy(self, name, project):
        """
        Public method used to copy a file/directory.
        
        @param name file/directory name to be copied (string)
        @param project reference to the project object
        @return flag indicating successfull operation (boolean)
        """
        dlg = HgCopyDialog(name)
        res = 0
        if dlg.exec_loop() == QDialog.Accepted:
            target = dlg.getData()
            
            # copy the file/directory and remember the filename(s)
            if os.path.isdir(name):
                sFilenames = Utilities.direntries(name, 1)
                dFilenames = [f.replace(name, target) for f in sFilenames]
                try:
                    shutil.copytree(name, target)
                    res = 1
                except:
                    res = 0
            else:
                sFilenames = [name]
                dFilenames = [target]
                try:
                    shutil.copy2(name, target)
                    res = 1
                except:
                    res = 0
            
            if res:
                # now do the Mercurial stuff
                # find the root of the repo
                repodir = str(name)
                while not os.path.isdir(os.path.join(repodir, '.hg')):
                    repodir = os.path.dirname(repodir)
                    if repodir == os.sep:
                        repodir = ''
                        break
                # do the hg copy command
                if repodir:
                    ui = HgDialog(self.trUtf8("Mercurial Copy"), '')
                    cwd = os.getcwd()
                    os.chdir(repodir)
                    try:
                        repo = repository(ui, repodir)
                        sFilenames = [str(f.replace(repodir+os.sep, '')) for f in sFilenames]
                        dFilenames = [str(f.replace(repodir+os.sep, '')) for f in dFilenames]
                        for source, dest in zip(sFilenames, dFilenames):
                            commands.copy(ui, repo, source, dest)
                        del repo
                    except:
                        pass
                    os.chdir(cwd)
                    ui.finish()
                    ui.exec_loop()
            
                # now change the project
                if os.path.isdir(name):
                    project.copyDirectory(name, target)
                else:
                    project.appendFile(target)
        return res
    
    def hgMove(self, name, project):
        """
        Public method used to move a file/directory.
        
        @param name file/directory name to be moved (string)
        @param project reference to the project object
        @return flag indicating successfull operation (boolean)
        """
        dlg = HgCopyDialog(name, None, 1)
        res = 0
        if dlg.exec_loop() == QDialog.Accepted:
            target = dlg.getData()
            
            # move the file/directory and remember the filename(s)
            if os.path.isdir(name):
                sFilenames = Utilities.direntries(name, 1)
                dFilenames = [f.replace(name, target) for f in sFilenames]
                try:
                    shutil.copytree(name, target)
                    shutil.rmtree(name, 1)
                    res = 1
                except:
                    res = 0
            else:
                sFilenames = [name]
                dFilenames = [target]
                try:
                    shutil.copy2(name, target)
                    os.remove(name)
                    res = 1
                except:
                    res = 0
            
            if res:
                # now do the Mercurial stuff
                # find the root of the repo
                repodir = str(name)
                while not os.path.isdir(os.path.join(repodir, '.hg')):
                    repodir = os.path.dirname(repodir)
                    if repodir == os.sep:
                        repodir = ''
                        break
                # do the hg copy command
                if repodir:
                    ui = HgDialog(self.trUtf8("Mercurial Copy"), '')
                    cwd = os.getcwd()
                    os.chdir(repodir)
                    try:
                        repo = repository(ui, repodir)
                        sFilenames = [str(f.replace(repodir+os.sep, '')) for f in sFilenames]
                        dFilenames = [str(f.replace(repodir+os.sep, '')) for f in dFilenames]
                        for source, dest in zip(sFilenames, dFilenames):
                            commands.copy(ui, repo, source, dest)
                        del repo
                    except:
                        pass
                    os.chdir(cwd)
                    ui.finish()
                    ui.exec_loop()
            
                # now change the project
                if os.path.isdir(name):
                    project.moveDirectory(name, target)
                else:
                    if project.hasEntry(name):
                        project.removeFile(name)
                        project.appendFile(target)
                        project.renameMainScript(name, target)
        return res
        
    def hgPull(self, ppath):
        """
        Public method used to pull changesets into the repository.
        
        @param ppath directory name of the project (string)
        """
        dlg = HgPullDialog()
        if dlg.exec_loop() == QDialog.Accepted:
            source, _update = dlg.getData()
            
            ui = HgDialog(self.trUtf8("Mercurial pull"),
                          'pull%s %s' % ((_update and ' -u' or ''), source))
            cwd = os.getcwd()
            os.chdir(ppath)
            try:
                repo = repository(ui, ppath)
                if not source:
                    source = ui.expandpath("default")
                    if source == "default":
                        source = ""
                cmdoptions = self._makeOptions("pull", _update and {"update" : 1} or {})
                commands.pull(ui, repo, source, **cmdoptions)
            except RepoError, inst:
                ui.warn("abort: ", inst, "\n")
            except:
                pass
            os.chdir(cwd)
            ui.finish()
            ui.exec_loop()
        
    def hgPush(self, ppath):
        """
        Public method used to to push changesets from the repository.
        
        @param ppath directory name of the project (string)
        """
        dlg = HgPushDialog()
        if dlg.exec_loop() == QDialog.Accepted:
            dest, force = dlg.getData()
            
            ui = HgDialog(self.trUtf8("Mercurial pull"),
                          'pull%s %s' % ((force and ' -f' or ''), dest))
            cwd = os.getcwd()
            os.chdir(ppath)
            try:
                repo = repository(ui, ppath)
                if not dest:
                    dest = ui.expandpath("default-push")
                    if dest == "default-push":
                        dest = ui.expandpath("default")
                        if dest == "default":
                            dest = ""
                commands.push(ui, repo, dest, force=force)
            except RepoError, inst:
                ui.warn("abort: ", inst, "\n")
            except:
                pass
            os.chdir(cwd)
            ui.finish()
            ui.exec_loop()
        
    def hgIncoming(self, ppath):
        """
        Public method used to check changesets incoming to the repository.
        
        @param ppath directory name of the project (string)
        """
        dlg = HgPullDialog(forIncoming=1)
        if dlg.exec_loop() == QDialog.Accepted:
            source = dlg.getData()[0]
            
            self.hgIncomingDialog = HgIncomingDialog(self)
            self.hgIncomingDialog.show()
            self.hgIncomingDialog.start(ppath, source)
        
    def hgOutgoing(self, ppath):
        """
        Public method used to check changesets outgoing from the repository.
        
        @param ppath directory name of the project (string)
        """
        dlg = HgPushDialog(forOutgoing=1)
        if dlg.exec_loop() == QDialog.Accepted:
            dest = dlg.getData()[0]
            
            self.hgOutgoingDialog = HgOutgoingDialog(self)
            self.hgOutgoingDialog.show()
            self.hgOutgoingDialog.start(ppath, dest)
        
    def hgManifest(self, ppath):
        """
        Public method used to show the manifest of the repository.
        
        @param ppath directory name of the project (string)
        """
        self.manifest = HgManifestDialog(self)
        self.manifest.show()
        self.manifest.start(ppath)
        
    def hgUndo(self, ppath):
        """
        Public method used to undo the last commit or pull transaction.
        
        @param ppath directory name of the project (string)
        """
        ui = HgDialog(self.trUtf8("Mercurial undo"), 'undo')
        cwd = os.getcwd()
        os.chdir(ppath)
        try:
            repo = repository(ui, ppath)
            repo.undo()
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()
        
    def hgForget(self, name):
        """
        Public method used to add a file/directory to the Mercurial repository.
        
        @param name file/directory name to be added (string)
        @param isDir flag indicating name is a directory (boolean)
        """
        names = ''
        pats = []
        if type(name) is types.ListType:
            dname, fnames = self.splitPathList(name)
            for n in name:
                names = "%s %s" % (names, n)
                pats.append(str(n))
        else:
            dname, fname = self.splitPath(name)
            names = " %s" % name
            pats = [str(name)]
        
        # find the root of the repo
        repodir = str(dname)
        while not os.path.isdir(os.path.join(repodir, '.hg')):
            repodir = os.path.dirname(repodir)
            if repodir == os.sep:
                return 0
        
        ui = HgDialog(self.trUtf8("Forgetting add transactions"), 
                      "forget%s" % names)
        cwd = os.getcwd()
        os.chdir(repodir)
        try:
            repo = repository(ui, repodir)
            cmdoptions = self._makeOptions("forget", {})
            commands.forget(ui, repo, *pats, **cmdoptions)
        except:
            pass
        os.chdir(cwd)
        ui.finish()
        ui.exec_loop()

    ############################################################################
    ## Methods to get the helper objects are below.
    ############################################################################
    
    def vcsGetProjectBrowserHelper(self, browser, project, isTranslationsBrowser=0):
        """
        Public method to instanciate a helper object for the different project browsers.
        
        @param browser reference to the project browser object
        @param project reference to the project object
        @param isTranslationsBrowser flag indicating, the helper is requested for the
            translations browser (this needs some special treatment)
        @return the project browser helper object
        """
        return HgProjectBrowserHelper(self, browser, project, isTranslationsBrowser)

    def vcsGetProjectHelper(self, project):
        """
        Public method to instanciate a helper object for the project.
        
        @param project reference to the project object
        @return the project helper object
        """
        return HgProjectHelper(self, project)

    ############################################################################
    # Private helper methods below
    ############################################################################

    def _makeOptions(self, cmd, options):
        """
        Private method to setup the command options for the Mercurial commands
        
        @param cmd Mercurial command to prepare for (string)
        @param options commandoptions to set explicitly (dictionary)
        @return dictionary of all command options for the given command
        """
        opts = commands.find(cmd)[1][1]
        state = {}
        for s, l, d, c in opts:
            state[l] = d
        for k, v in options.items():
            state[k] = v
        return state
