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

# Copyright 2015-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import re
import sys
import os
from os import path
from calculate.lib.configparser import ConfigParser
import shutil

from calculate.lib.utils.files import (getProgPath, STDOUT,
                                       PercentProgress, process, readFile,
                                       writeFile, listDirectory, removeDir)
from calculate.lib.utils.tools import AddonError
from calculate.lib.utils.ip import check_port

_ = lambda x: x
from calculate.lib.cl_lang import setLocalTranslate

setLocalTranslate('cl_lib3', sys.modules[__name__])


class GitError(AddonError):
    """Git Error"""


class NotGitError(GitError):
    """Данная директория не является Git"""


class Git(object):
    """
    Объект для управление git репозиторием
    """
    skip_files = ["metadata/md5-cache",
                  "metadata/cache"]

    def __init__(self, sshkey=None):
        self._git = self.__getGit()
        self.sshkey = sshkey

    def sshkey_env(self):
        options = {
            "UserKnownHostsFile": "/dev/null",
            "StrictHostKeyChecking": "no",
            "KbdInteractiveAuthentication": "no",
            "ControlPath": "none",
            "ControlMaster": "no",
            "PasswordAuthentication": "no",
            "BatchMode": "yes",
            "PreferredAuthentications": "publickey"
        }
        if self.sshkey:
            keyparam = "-i %s" % self.sshkey
        else:
            keyparam = ""
        return {
            "GIT_SSH_COMMAND": "ssh %s %s" % (
                keyparam,
                " ".join("-o %s=%s" % (k, v) for k, v in options.items()))
        }

    def checkExistsRep(self, rpath):
        """
        Проверить путь на наличие репозитория
        """
        if path.exists(rpath):
            if not path.isdir(rpath):
                raise GitError(
                    _("Repository {path} is not a directory").format(
                        path=rpath))
            if not path.isdir(self._gitDir(rpath)):
                raise NotGitError(
                    _("Repository {path} is not Git").format(
                        path=rpath))
            return True
        return False

    def __getGit(self):
        """
        Получить утилиту git
        """
        git = getProgPath("/usr/bin/git")
        if not git:
            raise GitError(_("The Git tool is not found"))
        return git

    @staticmethod
    def _gitDir(rpath):
        return path.abspath(path.join(rpath, ".git"))

    @staticmethod
    def is_git(gitpath):
        return path.isdir(Git._gitDir(gitpath))

    re_url = re.compile(r"^(ssh://(?:\w+@)?\w[\w\.-]+(?::\d+)?/[\w\./-]+?|"
        r"https?://\w[\w\.-]+(?::\d+)?/[\w\./-]+?|"
        r"((git://)?\w[\w\.-]+(?::\d+)?/[\w\./-]+?)|"
        r"\w+@\w[\w\.-]+:[\w\./-]+?)(.git)?$")

    @classmethod
    def normalize_url(cls, url):
        match = cls.re_url.match(url)
        if not match:
            return None
        if match.group(2) and not match.group(3):
            url = "git://%s" % match.group(2)
        else:
            url = match.group(1)
        return "%s.git" % url

    @staticmethod
    def get_url(rpath, remote_name):
        config = ConfigParser(strict=False)
        config.read(path.join(Git._gitDir(rpath), "config"), encoding="utf-8")
        res = config.get('remote "%s"' % remote_name, "url",
                         raw=True, fallback="")
        if res:
            return res
        fetch_info = Git.getFetchHeadInfo(Git._gitDir(rpath))
        if "url" in fetch_info:
            return fetch_info['url']
        return ""

    def process(self, *args, **kw):
        kw["envdict"] = os.environ.copy()
        kw["envdict"].update(self.sshkey_env())
        return process(*args, **kw)

    def process_progress(self, *args, **kw):
        kw["envdict"] = os.environ.copy()
        kw["envdict"].update(self.sshkey_env())
        return PercentProgress(*args, **kw)

    def cloneRepository(self, url, rpath, branch, cb_progress=None):
        """
        Сделать локальную копию репозитория

        Args:
            url: откуда качать репозиторий
            rpath: куда сохранять репозиторий
            branch: ветка на которую необходимо переключиться
        """
        if cb_progress:
            gitClone = self.process_progress(self._git, "clone", "-q",
                                             "--no-single-branch", "--progress",
                                             "--verbose",
                                             "--depth=1", "-b", branch, url,
                                             rpath, part=4, stderr=STDOUT)
            for perc in gitClone.progress():
                cb_progress(perc)
        else:
            gitClone = self.process(
                self._git, "clone", "-q", "--no-single-branch",
                "--depth=1", "-b", branch, url, rpath,
                stderr=STDOUT)
        if gitClone.failed():
            error = gitClone.read()
            if "Remote branch %s not found" % branch in error:
                raise GitError(
                    _("Branch {branch} not found in repository {url}").format(
                        branch=branch, url=url))
            if error:
                self.detect_error(error.split("\n"), url)
            raise GitError(_("Failed to clone repository {url}").format(
                url=url), error)
        return True

    def _git_command(self, git_dir, rpath, error, cb_progress=None):
        def command(*args, **kw):
            if "init" == args[0]:
                wholeCommand = [self._git, "init", rpath] + list(args[1:])
            else:
                wholeCommand = [self._git, "--git-dir", git_dir,
                                "--work-tree", rpath] + list(args)
            if cb_progress and args[0] in ("reset", "fetch", "checkout"):
                gitCmd = self.process_progress(
                    *wholeCommand,
                    stderr=STDOUT, atty=True, **kw)
                for perc in gitCmd.progress():
                    cb_progress(perc)
            else:
                gitCmd = self.process(*wholeCommand, stderr=STDOUT)
            if gitCmd.failed():
                error.append(gitCmd.read())
                return False
            return True

        return command

    @staticmethod
    def getFetchHeadInfo(git_dir):
        data = readFile(path.join(git_dir, "FETCH_HEAD")).strip()
        re_fetch = re.compile(r"^(\w+)\s+(?:(branch|tag) '([^']+)' of )?(\S+)$")
        m = re_fetch.search(data)
        if m:
            d = {'commit': m.group(1),
                 'url': m.group(4)}
            if m.group(2):
                type_map = {'tag': Git.Reference.Tag,
                            'branch': Git.Reference.Branch}
                d['type'] = type_map.get(m.group(2), Git.Reference.Unknown)
                d['reference'] = m.group(3)
            return d
        return {}

    def _copy_file(self, git_dir, file_url, fn):
        """
        Переместить index файл из одного репозитория в другой
        :param git_dir:
        :param file_url:
        :return:
        """
        old_index = path.join(file_url[7:], fn)
        new_index = path.join(git_dir, fn)
        if path.exists(old_index):
            try:
                with open(old_index, 'r') as in_f:
                    with writeFile(new_index) as out_f:
                        out_f.write(in_f.read())
                return True
            except IOError:
                pass
        return False

    def detect_error(self, error, url):
        error_map = {
            "access denied": _("Access denied to repository {url}"),
            "Permission denied": _("Access denied to repository {url}"),
            "does not appear to be a git repository":
                _("{url} does not appear to be a git "
                  "repository"),
            "Could not read from remote":
                _("Could not read from remote repository {url}")
        }
        if error:
            if self.sshkey and "ENCRYPTED" in readFile(self.sshkey):
                raise GitError("Private key is locked")
            for token, errmess in error_map.items():
                if token in error[-1]:
                    raise GitError(errmess.format(url=url))

    def cloneTagRepository(self, url, rpath, reference, cb_progress=None,
                           copy_index=False):
        """
        Сделать локальную копию репозитория для переключения по тэгам

        в отличии от обычного репозитория не содерит не remote, не ветки,
        только срез ревизии и тэги

        :param url:  откуда качать репозиторий
        :param rpath: куда сохранять репозиторий
        :param reference: метка на которую нужно перейти
        :param cb_progress: callback для прогрессбара
        :return: True в случае успешного клонирования на указанную метку
        :raise: GitError не удалось скачать, получить данные, метка не найдена
        """
        git_dir = self._gitDir(rpath)
        error = []

        command = self._git_command(git_dir, rpath, error, cb_progress)

        def command_fetch(**kw):
            """
            Получить данные репозитория (если shallow.lock - удалит и повторить)
            """
            res = command("fetch", "--depth=1", url, reference, **kw)
            if not res:
                if self._clear_lock(git_dir, "shallow.lock", error):
                    res = command("fetch", "--depth=1", url, reference, **kw)
            if not res:
                if error:
                    self.detect_error(error, url)
                raise GitError(
                    _("Reference {mark} not found in repository {url}"
                      ).format(mark=reference, url=url))
            return res

        # получить последние коммиты из удаленного репозитория
        if command("init"):
            if copy_index:
                copy_index = self._copy_file(git_dir, url, "index")
            if copy_index:
                command_fetch(part=3)
                fetch_type = self.saveFetchHead(git_dir)
                if fetch_type == self.Reference.Tag:
                    # создать master ветку с ссылкой на HEAD
                    self.saveFetchHead(
                        git_dir, force_type=self.Reference.Branch,
                        force_ref=self.Reference.Master)
                    reference = self.Reference.Master
                command("symbolic-ref", "HEAD", "refs/heads/%s" % reference)
                return True
            else:
                command_fetch(part=4, end=False)
                if command("reset", "--hard", "FETCH_HEAD", part=4,
                           startpart=3):
                    fetch_type = self.saveFetchHead(git_dir)
                    if fetch_type == self.Reference.Branch:
                        command("symbolic-ref", "HEAD",
                                "refs/heads/%s" % reference)
                    return True
        if error:
            self.detect_error(error, url)
        raise GitError(_("Failed to clone repository {url}").format(
            url=url), error[-1])
        # progressParam = {'fetch': {'part': 4, 'end': False},
        #                  'checkout': {'part': 4, 'startpart': 3}}

    @staticmethod
    def is_private_url(url):
        return Git.parse_url(url)[0] == Git.GitProtocol.SSH

    def trimRepository(self, rpath, cb_progress=None):
        """
        Удалить в репозитории лишнии объекты
        """
        git_dir = self._gitDir(rpath)

        head_info = self.getFetchHeadInfo(git_dir)
        if not head_info or not head_info.get('reference', ''):
            return False

        reference = head_info['reference']
        git_dir_old = "%s.old" % git_dir
        try:
            shutil.move(git_dir, git_dir_old)
        except OSError:
            raise GitError(_("Failed to optimize repository %s") % rpath)

        file_url = "file://%s" % git_dir_old
        self.cloneTagRepository(file_url,
                                rpath, reference, cb_progress,
                                copy_index=True)
        self._copy_file(git_dir, file_url, "config")
        self._copy_file(git_dir, file_url, "FETCH_HEAD")
        try:
            removeDir(git_dir_old)
        except Exception:
            raise GitError(_("Failed to optimize repository %s") % rpath)

    def isTagRepository(self, rpath):
        """
        :param rpath: путь до репозитория
        :return: True репозиторий переключается по тэгам
        """
        git_path = self._gitDir(rpath)
        data = self.getFetchHeadInfo(git_path)
        if "type" in data and data['type'] == self.Reference.Tag:
            return True
        return False

    class Reference(object):
        Branch = "branch"
        Tag = "tag"
        # RemoteBranch = "remote_branch"
        Unknown = "unknown"
        Master = "master"
        Develop = "develop"
        Update = "update"

    def reference_type(self, rpath, reference):
        """
        Получить тип ссылки на коммит
        :param rpath: путь до локального репозитория
        :param reference: имя ссылки
        :return: Reference (Branch - ветка, Tag - тэг,
                                Unknown - всё остальное)
        """
        refs_dir = path.join(rpath, ".git/refs")
        if reference in listDirectory(path.join(refs_dir, 'tags')):
            return self.Reference.Tag
        elif reference in listDirectory(path.join(refs_dir, 'heads')):
            return self.Reference.Branch
        return self.Reference.Unknown

    def _clear_lock(self, git_dir, lock_fn, error):
        if lock_fn in error[-1]:
            fn = path.join(git_dir, lock_fn)
            if path.exists(fn):
                try:
                    os.unlink(fn)
                    return True
                except OSError:
                    pass
        return False

    def updateTagRepository(self, url, rpath, reference, cb_progress=None,
                            clean=False):
        """
        Обновить данные до указанного тега репозитория

        :param url:  откуда качать репозиторий
        :param rpath: куда сохранять репозиторий
        :param reference: метка на которую нужно перейти
        :param cb_progress: callback для прогрессбара
        :return: True в случае успешного клонирования на указанную метку
        :raise: GitError не удалось скачать, получить данные, метка не найдена
        """
        git_dir = self._gitDir(rpath)
        error = []

        command = self._git_command(git_dir, rpath, error, cb_progress)
        # получить последние коммиты из удаленного репозитория
        try:
            if (self.reference_type(rpath, reference) == Git.Reference.Tag and
                    self.isTagRepository(rpath) and
                        self.getCurrentCommit(rpath) == self.getCommit(
                        rpath, reference)):
                if clean:
                    status = self.getStatusInfo(rpath)
                    if status and not status["files"]:
                        return False
                else:
                    return False
        except GitError:
            pass
        res = command("fetch", "--depth=1", url, reference, part=4, end=False)
        if not res:
            if self._clear_lock(git_dir, "shallow.lock", error):
                res = command("fetch", "--depth=1",
                              url, reference, part=4, end=False)
        if not res:
            if error:
                self.detect_error(error, url)
            raise GitError(
                _("Reference {mark} not found in repository {url}").format(
                    mark=reference, url=url))
        res = command("reset", "--hard", "FETCH_HEAD", part=4, startpart=3)
        if not res:
            if self._clear_lock(git_dir, "index.lock", error):
                res = command("reset", "--hard", "FETCH_HEAD",
                              part=4, startpart=3)
        if res:
            if clean:
                command("clean", "-df")
            if self.saveFetchHead(git_dir) == self.Reference.Branch:
                command("symbolic-ref", "HEAD", "refs/heads/%s" % reference)
            return True
        if error:
            self.detect_error(error, url)
        raise GitError(_("Failed to clone repository {url}").format(
            url=url), error[-1])

    # TODO: метод удаление лишних объектов, оставляя только текущий коммит

    def saveFetchHead(self, git_dir, force_type=None, force_ref=None):
        """
        Сохранить FEACH_HEAD как объект (tag или branch)
        :param git_dir: каталог .git репозитория
        :return: Reference
        """
        fetch_info = self.getFetchHeadInfo(git_dir)
        if force_type is not None:
            fetch_info['type'] = force_type
        if force_ref is not None:
            fetch_info['reference'] = force_ref
        tag_file = None
        if fetch_info.get('type', '') == self.Reference.Tag:
            tag_file = path.join(git_dir, "refs/tags/%s"
                                 % fetch_info['reference'])
        elif fetch_info.get('type', '') == self.Reference.Branch:
            tag_file = path.join(git_dir, "refs/heads/%s"
                                 % fetch_info['reference'])
        if tag_file:
            with writeFile(tag_file) as f:
                f.write(fetch_info['commit'])
            return fetch_info['type']
        return self.Reference.Unknown

    class GitProtocol(object):
        SSH = 22
        Git = 9418
        HTTP = 80
        HTTPS = 443
        FTP = 21
        FTPS = 990
        Rsync = 873
        Unknown = 0

    @staticmethod
    def parse_url(url):
        """
        Разобрать url на составляющие: тип и хост

        :param url: разбираемый URL
        :return: (тип, хост)
        """
        net_protocols = {
            'ssh': Git.GitProtocol.SSH,
            'ssh+git': Git.GitProtocol.SSH,
            'git+ssh': Git.GitProtocol.SSH,
            'http': Git.GitProtocol.HTTP,
            'https': Git.GitProtocol.HTTPS,
            'ftp': Git.GitProtocol.FTP,
            'ftps': Git.GitProtocol.FTPS,
            'rsync': Git.GitProtocol.Rsync,
            'git': Git.GitProtocol.Git}
        re_host = re.compile(
            "(%s)://(?:[^@]+@)?([^:/]+)(:\d+)?" %
            "|".join(x.replace("+", r"\+") for x in net_protocols.keys())
        )
        net_match = re_host.search(url)
        if net_match:
            if net_match.group(3):
                return int(net_match.group(3)), net_match.group(2)
            return net_protocols[net_match.group(1)], net_match.group(2)
        re_scp = re.compile("(?:[^@]+@)?([^:]+):(//)?")
        scp_match = re_scp.search(url)
        if scp_match and scp_match.group(2) is None:
            return Git.GitProtocol.SSH, scp_match.group(1)
        return Git.GitProtocol.Unknown, ""

    def checkUrl(self, url):
        """
        Проверить доступность указанного URL

        ssh://[user@]host.xz[:port]/path/to/repo.git/
        git://host.xz[:port]/path/to/repo.git/
        http[s]://host.xz[:port]/path/to/repo.git/
        ftp[s]://host.xz[:port]/path/to/repo.git/
        rsync://host.xz/path/to/repo.git/
        [user@]host.xz:path/to/repo.git/

        /path/to/repo.git/
        file:///path/to/repo.git/

        :param url: проверяемый URL
        :return:
        """
        git_port, target = Git.parse_url(url)
        if git_port != self.GitProtocol.Unknown:
            return check_port(target, git_port, timeout=20)
        return True

    def isNeedUnpack(self, rpath, fns=(".gitignore",
                                       "metadata/.gitignore")):
        """
        Проверить репозиторий на то, что он еще не распакован
        :param rpath: путь до репозитория
        :param fns: имя файла
        :return:
        """
        for fn in fns:
            if path.exists(path.join(rpath, fn)):
                return False
        else:
            git_dir = self._gitDir(rpath)
            git_log = self.process(self._git, "--git-dir", git_dir, "log",
                                   "-n1", "--format=format:%H",
                                   "--quiet", "--", *fns, stderr=STDOUT)
            if git_log.success() and git_log.read().strip():
                return True
            status = self.getStatusInfo(rpath)
            if not status or status['files']:
                return True
            return False

    def cloneRevRepository(self, url, rpath, branch, revision,
                           cb_progress=None):
        """
        Сделать локальную копию репозитория с указанной ревизией

        Args:
            url: откуда качать репозиторий
            rpath: куда сохранять репозиторий
            branch: ветка на которую необходимо переключиться
            revision: если указана - сделать ревизию текущей

        Return:
            Возвращает True если клонирование произведено с установкой на
            указанную ревизию. False если клонирование произведено с
            установкой на последнюю ревизию.
        Raises:
            GitError: Выполнение ключевых команд выполнено с ошибками (не
                удалось скачать и получить данные из удаленного репозитория)
        """
        git_dir = self._gitDir(rpath)
        error = []

        def command(cmd, startpart=0):
            """
            Выполнить одну из команд необходимой для клонирования репозитория
            """
            commands = {  # инициализация пустого репозитория
                          'init': ["init", rpath],
                          # подключить указанный удаленный как origin
                          'add_remote': ["remote", "add", "origin", url],
                          # скачать последние коммиты веток
                          'fetchshallow': ["fetch", "--depth=1"],
                          # проверить есть указанный коммит
                          'has_revision': ["log", "-n1", revision],
                          # проверить есть указанный коммит
                          'has_branch': ["log", "-n1",
                                         "remotes/origin/%s" % branch],
                          # получить ревизию из revs тэгов
                          'get_rev_tag': ["fetch", "--depth=1", "origin",
                                          "+refs/tags/%s:refs/remotes/origin/%s" %
                                          (revision, branch)],
                          # переключиться на указанную ревизию указанной веткой
                          'checkout_revision': ["checkout", "-b", branch,
                                                revision],
                          # переключить на указанную ветку
                          'checkout': ["checkout", branch],
                          # установить upstream для локальной ветки
                          'set_track': ["branch", branch, '-u',
                                        "origin/%s" % branch]
                          }
            if cmd == "init":
                wholeCommand = [self._git] + commands[cmd]
            else:
                wholeCommand = [self._git, "--git-dir", git_dir,
                                "--work-tree", rpath] + commands[cmd]
            if cb_progress and commands[cmd][0] in ("fetch", "checkout"):
                progressParam = {'fetch': {'part': 4, 'end': False},
                                 'checkout': {'part': 4, 'startpart': 3}}
                gitCmd = self.process_progress(
                    *wholeCommand + ["--progress", "--verbose"],
                    stderr=STDOUT, **progressParam)
                for perc in gitCmd.progress():
                    cb_progress(perc)
            else:
                gitCmd = self.process(*wholeCommand, stderr=STDOUT)
            if gitCmd.failed():
                error.append(gitCmd.read())
                return False
            return True

        # получить последние коммиты из удаленного репозитория
        if command("init") and command("add_remote"):
            if command("get_rev_tag") or command("fetchshallow"):
                if not command("has_branch"):
                    raise GitError(
                        _("Branch {branch} not found in repository {url}"
                          ).format(branch=branch, url=url))
                # если среди коммитов есть указанный коммит
                if command("has_revision"):
                    # переключаемся на нужный коммита, устанавливаем связь
                    if command("checkout_revision") and command("set_track"):
                        return True
                elif command("checkout"):
                    return False
        if error:
            self.detect_error(error, url)
        raise GitError(_("Failed to clone repository {url}").format(
            url=url), error[-1])

    def pullRepository(self, rpath, quiet_error=False, cb_progress=None):
        """
        Обновить репозиторий до последней версии
        """
        gitPull = self.process(self._git, "--git-dir", self._gitDir(rpath),
                               "pull", "--ff-only", stderr=STDOUT)
        if gitPull.failed():
            if not quiet_error:
                error = gitPull.read()
                raise GitError(
                    _("Failed to update the repository in {rpath}").format(
                        rpath=rpath), error)
            return False
        return True

    def fetchRepository(self, rpath, cb_progress=None):
        """
        Получить изменения из удаленно репозитория
        """
        if cb_progress:
            gitFetch = self.process_progress(self._git, "--git-dir",
                                             self._gitDir(rpath),
                                             "fetch", "--progress", "--verbose",
                                             part=3, stderr=STDOUT)
            for perc in gitFetch.progress():
                cb_progress(perc)
        else:
            gitFetch = self.process(self._git, "--git-dir", self._gitDir(rpath),
                                    "fetch", stderr=STDOUT)
        if gitFetch.failed():
            error = gitFetch.read()
            raise GitError(
                _("Failed to update the repository in {rpath}").format(
                    rpath=rpath), error)
        return True

    def checkChanges(self, rpath):
        """
        Проверить наличие изменений пользователем файлов в репозитории
        """
        git_dir = self._gitDir(rpath)
        git_status = self.process(self._git, "--git-dir", git_dir,
                                  "--work-tree",
                                  rpath,
                                  "status", "--porcelain", stderr=STDOUT)
        if git_status.success():
            return not any(x.strip() for x in git_status)
        else:
            raise GitError(
                _("Wrong repository in the {rpath} directory").format(
                    rpath=rpath))

    def parseStatusInfo(self, data):
        """
        Разобрать информацию полученную через git status -b --porcelain

        Returns:
            Словарь
            # есть ли измененные файлы пользователем
            {'files':True/False,
            # есть коммиты после текущего
             'ahead':True/False,
            # есть коммиты перед текущим (означает, что обновление
            # с основной веткой не осуществляется через fast-forward
             'behind':True/False,
            # текущая ветка
             'branch':'',
            # оригинальная ветка
             'origin':'origin/master'}
        """
        reStatus = re.compile(
            "^## (\w+)\s*(\(no branch\))?(?:\.\.\.(\S+))?(?:\s+\[(ahead \d+)?"
            "(?:, )?(behind \d+)?\])?\n?(.*|$)", re.S)
        match = reStatus.search(data)
        if not match:
            return {}
        return {'files': True if match.group(6) else False,
                'ahead': True if match.group(4) else False,
                'behind': True if match.group(5) else False,
                'origin': match.group(3) or "",
                'branch': "" if match.group(2) else match.group(1)}

    def getCurrentCommit(self, rpath):
        """
        Получить текущий коммит в репозитории
        """
        return self.getCommit(rpath, "HEAD")

    def getCommit(self, rpath, reference):
        git_dir = self._gitDir(rpath)
        git_show = self.process(self._git, "--git-dir", git_dir, "log",
                                "-n1", "--format=format:%H",
                                "--quiet", reference)
        if git_show.success():
            return git_show.read().strip()
        else:
            raise GitError(
                _("Failed to get the repository status for {rpath}").format(
                    rpath=rpath))

    def getStatusInfo(self, rpath):
        """
        Получить информацию об изменениях в репозитории

        Returns:
            Словарь выдаваемый функцией _parseStatusInfo
        """
        git_dir = self._gitDir(rpath)
        git_status = self.process(self._git, "--git-dir", git_dir,
                                  "--work-tree",
                                  rpath,
                                  "status", "-b", "--porcelain", stderr=STDOUT)
        if git_status.success():
            retDict = self.parseStatusInfo(git_status.read())
            if not retDict:
                raise GitError(
                    _("Failed to get the repository status for {rpath}").format(
                        rpath=rpath))
            return retDict
        else:
            raise GitError(
                _("Wrong repository in the {rpath} directory").format(
                    rpath=rpath))

    def resetRepository(self, rpath, to_origin=False, to_rev=None, info=None):
        """
        Удалить неиндексированные изменения в репозитории


        Args:
            to_origin: откатить все изменения до удаленного репозитория
            to_rev: откатить все изменения до определенной ревизии
            info: использовать уже полученную информация об изменения
                  в репозитории
        Return:
            True - успешное выполнение
            False - нет необходимости выполнять reset
        Raises:
            GitError: выполнение комманд reset и clean прошло с ошибкой
        """
        git_dir = self._gitDir(rpath)
        if not info:
            info = self.getStatusInfo(rpath)
        if (all(not info[x] for x in ("files", "ahead", "behind") if x in info)
            and (not info["origin"] or
                             "origin/%s" % info["branch"] == info["origin"])):
            return False
        commit = (info['origin'] if to_origin else to_rev) or "HEAD"
        git_reset = self.process(self._git, "--git-dir", git_dir, "--work-tree",
                                 rpath,
                                 "reset", "--hard", commit, stderr=STDOUT)
        git_clean = self.process(self._git, "--git-dir", git_dir, "--work-tree",
                                 rpath,
                                 "clean", "-fd", stderr=STDOUT)
        if git_reset.failed() or git_clean.failed():
            raise GitError(_("Failed to clean the {rpath} repository").format(
                rpath=rpath))
        return True

    def getBranch(self, rpath):
        """
        Получить текущую ветку
        """
        return self.getStatusInfo(rpath)['branch']

    def getTags(self, rpath):
        """
        Получить список тэгов, отсортированных по версии
        """
        git_dir = self._gitDir(rpath)
        git_tag = self.process(self._git, "--git-dir", git_dir, "--work-tree",
                               rpath,
                               "tag", "--sort=version:refname", stderr=STDOUT)
        if git_tag.success():
            return [x.strip() for x in git_tag if x.strip()]
        return []

    def checkoutBranch(self, rpath, branch):
        """
        Сменить ветку
        """
        git_dir = self._gitDir(rpath)
        git_checkout = self.process(self._git, "--git-dir", git_dir,
                                    "--work-tree", rpath,
                                    "checkout", "-f", branch, stderr=STDOUT)
        if git_checkout.failed():
            error = git_checkout.read()
            if "pathspec '%s' did not match" % branch in error:
                raise GitError(
                    _("Branch {branch} not found in repository {rpath}").format(
                        branch=branch, rpath=rpath))
            raise GitError(
                _("Failed to move to branch {branch} in the {rpath} "
                  "repository").format(branch=branch,
                                       rpath=rpath), error)
        return True


class MTimeKeeper(object):
    """
    Сохранить и восстановить mtime на файлы
    """

    def __init__(self, dn):
        self.data = {}
        self.dn = dn

    def save(self):
        for fn in listDirectory(self.dn, fullPath=True):
            try:
                stat_data = os.stat(fn)
                self.data[fn] = {'size': stat_data.st_size,
                                 'mtime': stat_data.st_mtime,
                                 'atime': stat_data.st_atime,
                                 }
            except OSError:
                pass

    def restore(self):
        for fn in self.data.keys():
            try:
                if path.exists(fn):
                    stat_data = os.stat(fn)
                    if stat_data.st_size == self.data[fn]['size']:
                        os.utime(fn, (self.data[fn]['atime'],
                                      self.data[fn]['mtime']))
            except OSError:
                pass
