|
|
|
|
#-*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
# Copyright 2014 Calculate Ltd. 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.
|
|
|
|
|
from collections import Mapping, defaultdict
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from calculate.lib.utils.colortext.palette import TextState
|
|
|
|
|
from calculate.lib.utils.tools import AddonError, SavableIterator
|
|
|
|
|
import time
|
|
|
|
|
import datetime
|
|
|
|
|
|
|
|
|
|
Colors = TextState.Colors
|
|
|
|
|
import pexpect
|
|
|
|
|
from os import path
|
|
|
|
|
from calculate.lib.utils.files import (getProgPath, STDOUT,
|
|
|
|
|
PercentProgress, process, readFile,
|
|
|
|
|
readLinesFile)
|
|
|
|
|
from calculate.lib.utils.common import cmpVersion
|
|
|
|
|
from contextlib import closing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
|
|
|
|
from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate
|
|
|
|
|
from functools import total_ordering
|
|
|
|
|
from itertools import ifilter, imap, chain
|
|
|
|
|
|
|
|
|
|
setLocalTranslate('cl_update3', sys.modules[__name__])
|
|
|
|
|
__ = getLazyLocalTranslate(_)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GitError(AddonError):
|
|
|
|
|
"""Git Error"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Layman:
|
|
|
|
|
"""
|
|
|
|
|
Объект для управления репозиториями Layman
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
installed: путь до installed.xml
|
|
|
|
|
makeconf: путь до makeconf
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, installed, makeconf):
|
|
|
|
|
self.installed = installed
|
|
|
|
|
self.makeconf = makeconf
|
|
|
|
|
|
|
|
|
|
def _add_to_installed(self, rname, rurl):
|
|
|
|
|
"""
|
|
|
|
|
Добавить репозиторий в installed.xml
|
|
|
|
|
"""
|
|
|
|
|
if path.exists(self.installed) and readFile(self.installed).strip():
|
|
|
|
|
tree = ET.parse(self.installed)
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
# если репозиторий уже присутсвует в installed.xml
|
|
|
|
|
if root.find("repo[name='%s']" % rname) is not None:
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
root = ET.Element("repositories", version="1.0")
|
|
|
|
|
tree = ET.ElementTree(root)
|
|
|
|
|
|
|
|
|
|
newrepo = ET.SubElement(root, "repo", priority="50",
|
|
|
|
|
quality="experimental",
|
|
|
|
|
status="unofficial")
|
|
|
|
|
name = ET.SubElement(newrepo, "name")
|
|
|
|
|
name.text = rname
|
|
|
|
|
source = ET.SubElement(newrepo, "source", type="git")
|
|
|
|
|
source.text = rurl
|
|
|
|
|
try:
|
|
|
|
|
from layman.utils import indent
|
|
|
|
|
|
|
|
|
|
indent(root)
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
pass
|
|
|
|
|
with open(self.installed, 'w') as f:
|
|
|
|
|
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
|
|
|
tree.write(f, encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
def _add_to_makeconf(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Добавить репозиторий в layman/make.conf
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def fixContent(match):
|
|
|
|
|
repos = match.group(1).strip().split('\n')
|
|
|
|
|
if not rpath in repos:
|
|
|
|
|
repos.insert(0, rpath)
|
|
|
|
|
return 'PORTDIR_OVERLAY="\n%s"' % "\n".join(repos);
|
|
|
|
|
|
|
|
|
|
if path.exists(self.makeconf):
|
|
|
|
|
content = readFile(self.makeconf)
|
|
|
|
|
if "PORTDIR_OVERLAY" in content:
|
|
|
|
|
new_content = re.sub("\APORTDIR_OVERLAY=\"([^\"]+)\"",
|
|
|
|
|
fixContent, content, re.DOTALL)
|
|
|
|
|
if new_content == content:
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
content = new_content
|
|
|
|
|
else:
|
|
|
|
|
content = 'PORTDIR_OVERLAY="\n%s"\n' % rpath + content
|
|
|
|
|
else:
|
|
|
|
|
content = 'PORTDIR_OVERLAY="\n%s"\n' % rpath
|
|
|
|
|
with open(self.makeconf, 'w') as f:
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
def add(self, rname, rurl, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Добавить репозиторий в installed.xml и layman/make.conf
|
|
|
|
|
"""
|
|
|
|
|
self._add_to_installed(rname, rurl)
|
|
|
|
|
self._add_to_makeconf(rpath)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
class Git:
|
|
|
|
|
"""
|
|
|
|
|
Объект для управление git репозиторием
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self._git = self.__getGit()
|
|
|
|
|
|
|
|
|
|
def checkExistsRep(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Проверить путь на наличие репозитория
|
|
|
|
|
"""
|
|
|
|
|
if path.exists(rpath):
|
|
|
|
|
if not path.isdir(rpath):
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("Repository {path} is not directory").format(
|
|
|
|
|
path=rpath))
|
|
|
|
|
if not path.isdir(self._gitDir(rpath)):
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("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(_("Git utility is not found"))
|
|
|
|
|
return git
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _gitDir(rpath):
|
|
|
|
|
return path.join(rpath, ".git")
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def is_git(gitpath):
|
|
|
|
|
return path.isdir(Git._gitDir(gitpath))
|
|
|
|
|
|
|
|
|
|
def cloneRepository(self, url, rpath, branch, cb_progress=None):
|
|
|
|
|
"""
|
|
|
|
|
Сделать локальную копию репозитория
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
url: откуда качать репозиторий
|
|
|
|
|
rpath: куда сохранять репозиторий
|
|
|
|
|
branch: ветка на которую необходимо переключиться
|
|
|
|
|
"""
|
|
|
|
|
if cb_progress:
|
|
|
|
|
gitClone = PercentProgress(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 = 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 {url} repository").format(
|
|
|
|
|
branch=branch, url=url))
|
|
|
|
|
raise GitError(_("Failed to clone {url} repository").format(
|
|
|
|
|
url=url), error)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
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/revs/%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}}
|
|
|
|
|
gitClone = PercentProgress(
|
|
|
|
|
*wholeCommand + ["--progress", "--verbose"],
|
|
|
|
|
stderr=STDOUT, **progressParam)
|
|
|
|
|
for perc in gitClone.progress():
|
|
|
|
|
cb_progress(perc)
|
|
|
|
|
else:
|
|
|
|
|
gitCmd = 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 {url} repository"
|
|
|
|
|
).format(branch=branch, url=url))
|
|
|
|
|
# если среди коммитов есть указанный коммит
|
|
|
|
|
if command("has_revision"):
|
|
|
|
|
# переключаемся на нужный коммита, устанавливаем связь
|
|
|
|
|
if command("checkout_revision") and command("set_track"):
|
|
|
|
|
return True
|
|
|
|
|
elif command("checkout"):
|
|
|
|
|
return False
|
|
|
|
|
raise GitError(_("Failed to clone {url} repository").format(
|
|
|
|
|
url=url), error[-1])
|
|
|
|
|
|
|
|
|
|
def pullRepository(self, rpath, quiet_error=False, cb_progress=None):
|
|
|
|
|
"""
|
|
|
|
|
Обновить репозиторий до последней версии
|
|
|
|
|
"""
|
|
|
|
|
gitPull = 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 repository in {rpath}").format(
|
|
|
|
|
rpath=rpath), error)
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def fetchRepository(self, rpath, cb_progress=None):
|
|
|
|
|
"""
|
|
|
|
|
Получить изменения из удаленно репозитория
|
|
|
|
|
"""
|
|
|
|
|
if cb_progress:
|
|
|
|
|
gitFetch = PercentProgress(self._git, "--git-dir",
|
|
|
|
|
self._gitDir(rpath),
|
|
|
|
|
"fetch", "--progress", "--verbose",
|
|
|
|
|
part=3, stderr=STDOUT)
|
|
|
|
|
for perc in gitFetch.progress():
|
|
|
|
|
cb_progress(perc)
|
|
|
|
|
else:
|
|
|
|
|
gitFetch = process(self._git, "--git-dir", self._gitDir(rpath),
|
|
|
|
|
"fetch", stderr=STDOUT)
|
|
|
|
|
if gitFetch.failed():
|
|
|
|
|
error = gitFetch.read()
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("Failed to update repository in {rpath}").format(
|
|
|
|
|
rpath=rpath), error)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def checkChanges(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Проверить наличие изменений пользователем файлов в репозитории
|
|
|
|
|
"""
|
|
|
|
|
git_dir = self._gitDir(rpath)
|
|
|
|
|
git_status = 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 {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+))?(?:\s+\[(ahead \d+)?"
|
|
|
|
|
"(?:, )?(behind \d+)?\])?\n?(.*|$)", re.S)
|
|
|
|
|
match = reStatus.search(data)
|
|
|
|
|
if not match:
|
|
|
|
|
return {}
|
|
|
|
|
return {'files': True if match.group(5) else False,
|
|
|
|
|
'ahead': True if match.group(3) else False,
|
|
|
|
|
'behind': True if match.group(4) else False,
|
|
|
|
|
'origin': match.group(2) or "",
|
|
|
|
|
'branch': match.group(1)}
|
|
|
|
|
|
|
|
|
|
def getCurrentCommit(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Получить текущий коммит в репозитории
|
|
|
|
|
"""
|
|
|
|
|
git_dir = self._gitDir(rpath)
|
|
|
|
|
git_show = process(self._git, "--git-dir", git_dir, "show",
|
|
|
|
|
"--format=format:%H",
|
|
|
|
|
"--quiet", stderr=STDOUT)
|
|
|
|
|
if git_show.success():
|
|
|
|
|
return git_show.read().strip()
|
|
|
|
|
else:
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("Failed to get status of repository in "
|
|
|
|
|
"{rpath} directory").format(
|
|
|
|
|
rpath=rpath))
|
|
|
|
|
|
|
|
|
|
def getStatusInfo(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Получить информацию об изменениях в репозитории
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Словарь выдаваемый функцией _parseStatusInfo
|
|
|
|
|
"""
|
|
|
|
|
git_dir = self._gitDir(rpath)
|
|
|
|
|
git_status = 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 status of repository in "
|
|
|
|
|
"{rpath} directory").format(
|
|
|
|
|
rpath=rpath))
|
|
|
|
|
return retDict
|
|
|
|
|
else:
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("Wrong repository in {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 = process(self._git, "--git-dir", git_dir, "--work-tree",
|
|
|
|
|
rpath,
|
|
|
|
|
"reset", "--hard", commit, stderr=STDOUT)
|
|
|
|
|
git_clean = 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 {rpath} repository").format(
|
|
|
|
|
rpath=rpath))
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def getBranch(self, rpath):
|
|
|
|
|
"""
|
|
|
|
|
Получить текущую ветку
|
|
|
|
|
"""
|
|
|
|
|
return self.getStatusInfo(rpath)['branch']
|
|
|
|
|
|
|
|
|
|
def checkoutBranch(self, rpath, branch):
|
|
|
|
|
"""
|
|
|
|
|
Сменить ветку
|
|
|
|
|
"""
|
|
|
|
|
git_dir = self._gitDir(rpath)
|
|
|
|
|
git_checkout = 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 {rpath} repository").format(
|
|
|
|
|
branch=branch, rpath=rpath))
|
|
|
|
|
raise GitError(
|
|
|
|
|
_("Failed to change branch to {branch} in "
|
|
|
|
|
"{rpath} repository").format(branch=branch,
|
|
|
|
|
rpath=rpath), error)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@total_ordering
|
|
|
|
|
class EmergePackage(Mapping):
|
|
|
|
|
"""
|
|
|
|
|
Данные о пакете
|
|
|
|
|
|
|
|
|
|
Item keys: CATEGORY, P, PN, PV, P, PF, PR, PVR
|
|
|
|
|
|
|
|
|
|
Поддерживает сравнение объекта с другим таким же объектом по версии, либо
|
|
|
|
|
со строкой, содержащей версию. Сравнение выполняется по категория/имя, затем
|
|
|
|
|
по версии
|
|
|
|
|
"""
|
|
|
|
|
default_repo = 'gentoo'
|
|
|
|
|
prefix = r"(?:.*/var/db/pkg/|=)?"
|
|
|
|
|
category = r"(?:(\w+(?:-\w+)?)/)?"
|
|
|
|
|
pn = "([^/]*?)"
|
|
|
|
|
pv = r"(?:-(\d[^-]*?))?"
|
|
|
|
|
pr = r"(?:-(r\d+))?"
|
|
|
|
|
tbz = r"(?:.(tbz2))?"
|
|
|
|
|
slot = r'(?::(\w+(?:\.\w+)*(?:/\w+(?:\.\w+)*)?))?'
|
|
|
|
|
repo = r'(?:::(\w+))?'
|
|
|
|
|
|
|
|
|
|
reParse = re.compile(
|
|
|
|
|
r'^{prefix}{category}(({pn}{pv}){pr}){slot}{repo}{tbz}$'.format(
|
|
|
|
|
prefix=prefix, category=category, pn=pn, pv=pv, pr=pr, tbz=tbz,
|
|
|
|
|
slot=slot, repo=repo))
|
|
|
|
|
|
|
|
|
|
attrs = ('CATEGORY', 'PN', 'PF', 'SLOT', 'REPO', 'P', 'PV', 'PR', 'PVR',
|
|
|
|
|
'CATEGORY/PN')
|
|
|
|
|
|
|
|
|
|
def _parsePackageString(self, s):
|
|
|
|
|
"""
|
|
|
|
|
Преобразовать строка в части названия пакета
|
|
|
|
|
"""
|
|
|
|
|
x = self.reParse.search(s)
|
|
|
|
|
if x:
|
|
|
|
|
CATEGORY, PF, P, PN, PV, PR, SLOT, REPO, TBZ = range(0, 9)
|
|
|
|
|
x = x.groups()
|
|
|
|
|
d = {'CATEGORY': x[CATEGORY] or "",
|
|
|
|
|
'PN': x[PN],
|
|
|
|
|
'PV': x[PV] or '0',
|
|
|
|
|
'PF': x[PF],
|
|
|
|
|
'P': x[P],
|
|
|
|
|
'SLOT': x[SLOT] or '0',
|
|
|
|
|
'REPO': x[REPO] or self.default_repo,
|
|
|
|
|
'CATEGORY/PN': "%s/%s" % (x[CATEGORY], x[PN]),
|
|
|
|
|
'PR': x[PR] or 'r0'}
|
|
|
|
|
if x[PR]:
|
|
|
|
|
d['PVR'] = "%s-%s" % (d['PV'], d['PR'])
|
|
|
|
|
else:
|
|
|
|
|
d['PVR'] = d['PV']
|
|
|
|
|
if d['PF'].endswith('-r0'):
|
|
|
|
|
d['PF'] = d['PF'][:-3]
|
|
|
|
|
return d.copy()
|
|
|
|
|
else:
|
|
|
|
|
return {k: '' for k in self.attrs}
|
|
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
|
return iter(self.attrs)
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
if not self['PN']:
|
|
|
|
|
return 0
|
|
|
|
|
else:
|
|
|
|
|
return len(self.attrs)
|
|
|
|
|
|
|
|
|
|
def __lt__(self, version):
|
|
|
|
|
"""
|
|
|
|
|
В объектах сравнивается совпадение категории и PF
|
|
|
|
|
"""
|
|
|
|
|
if "CATEGORY/PN" in version and "PVR" in version:
|
|
|
|
|
if self['CATEGORY/PN'] < version['CATEGORY/PN']:
|
|
|
|
|
return True
|
|
|
|
|
elif self['CATEGORY/PN'] > version['CATEGORY/PN']:
|
|
|
|
|
return False
|
|
|
|
|
version = "%s-%s" % (version['PV'], version['PR'])
|
|
|
|
|
currentVersion = "%s-%s" % (self['PV'], self['PR'])
|
|
|
|
|
return cmpVersion(currentVersion, version) == -1
|
|
|
|
|
|
|
|
|
|
def __eq__(self, version):
|
|
|
|
|
if "CATEGORY" in version and "PF" in version:
|
|
|
|
|
return ("%s/%s" % (self['CATEGORY'], self['PF']) ==
|
|
|
|
|
"%s/%s" % (version['CATEGORY'], version['PF']))
|
|
|
|
|
else:
|
|
|
|
|
currentVersion = "%s-%s" % (self['PV'], self['PR'])
|
|
|
|
|
return cmpVersion(currentVersion, version) == 0
|
|
|
|
|
|
|
|
|
|
def __init__(self, package):
|
|
|
|
|
if isinstance(package, EmergePackage):
|
|
|
|
|
self.__result = package.__result
|
|
|
|
|
self._package = package._package
|
|
|
|
|
else:
|
|
|
|
|
self._package = package
|
|
|
|
|
self.__result = None
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
|
if not self.__result:
|
|
|
|
|
self.__result = self._parsePackageString(self._package)
|
|
|
|
|
return self.__result[item]
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return "EmergePackage(%s/%s)" % (self['CATEGORY'], self['PF'])
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return "%s/%s" % (self['CATEGORY'], self['PF'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PackageInformation:
|
|
|
|
|
"""
|
|
|
|
|
Объект позволяет получать информацию о пакете из eix
|
|
|
|
|
"""
|
|
|
|
|
eix_cmd = getProgPath("/usr/bin/eix")
|
|
|
|
|
|
|
|
|
|
query_packages = []
|
|
|
|
|
information_cache = defaultdict(dict)
|
|
|
|
|
|
|
|
|
|
fields = ["DESCRIPTION"]
|
|
|
|
|
|
|
|
|
|
def __init__(self, pkg):
|
|
|
|
|
self._pkg = pkg
|
|
|
|
|
if not pkg in self.query_packages:
|
|
|
|
|
self.query_packages.append(pkg)
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
|
if not self._pkg['CATEGORY/PN'] in self.information_cache and \
|
|
|
|
|
self.query_packages:
|
|
|
|
|
self.query_information()
|
|
|
|
|
try:
|
|
|
|
|
return self.information_cache[self._pkg['CATEGORY/PN']][item]
|
|
|
|
|
except KeyError:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def query_information(self):
|
|
|
|
|
pkg_list = "|".join(
|
|
|
|
|
[x['CATEGORY/PN'].replace("+", r"\+") for x in self.query_packages])
|
|
|
|
|
output = pexpect.spawn(self.eix_cmd, ["--xml", pkg_list]).read()
|
|
|
|
|
xml = ET.fromstring(output)
|
|
|
|
|
for pkg in self.query_packages:
|
|
|
|
|
cat_pn = pkg['CATEGORY/PN']
|
|
|
|
|
if not cat_pn in self.information_cache:
|
|
|
|
|
descr_node = xml.find(
|
|
|
|
|
'category[@name="%s"]/package[@name="%s"]/description'
|
|
|
|
|
% (pkg['CATEGORY'], pkg['PN']))
|
|
|
|
|
if descr_node is not None:
|
|
|
|
|
self.information_cache[cat_pn]['DESCRIPTION'] = \
|
|
|
|
|
descr_node.text
|
|
|
|
|
while self.query_packages:
|
|
|
|
|
self.query_packages.pop()
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def add_info(cls, pkg):
|
|
|
|
|
pkg.info = cls(pkg)
|
|
|
|
|
return pkg
|
|
|
|
|
|
|
|
|
|
class UnmergePackage(EmergePackage):
|
|
|
|
|
"""
|
|
|
|
|
Информация об обновлении одного пакета
|
|
|
|
|
"""
|
|
|
|
|
re_pkg_info = re.compile("^\s(\S+)\n\s+selected:\s(\S+)",re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
def __init__(self, package):
|
|
|
|
|
super(UnmergePackage, self).__init__(package)
|
|
|
|
|
if not isinstance(package, EmergePackage):
|
|
|
|
|
self._package = self.convert_package_info(package)
|
|
|
|
|
|
|
|
|
|
def convert_package_info(self, package):
|
|
|
|
|
match = self.re_pkg_info.search(package)
|
|
|
|
|
if match:
|
|
|
|
|
return "%s-%s" % match.groups()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def recalculate_update_info(cls):
|
|
|
|
|
"""
|
|
|
|
|
Добавить
|
|
|
|
|
"""
|
|
|
|
|
cls.update_info = re.compile(
|
|
|
|
|
r"^({install_info})\s+({atom_info})\s*(?:{prev_version})?"
|
|
|
|
|
r"\s*({use_info})?.*?({pkg_size})?$".format(
|
|
|
|
|
install_info=cls.install_info,
|
|
|
|
|
atom_info=cls.atom_info,
|
|
|
|
|
prev_version=cls.prev_version,
|
|
|
|
|
use_info=cls.use_info,
|
|
|
|
|
pkg_size=cls.pkg_size), re.MULTILINE)
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
@recalculate_update_info
|
|
|
|
|
@total_ordering
|
|
|
|
|
class EmergeUpdateInfo(Mapping):
|
|
|
|
|
"""
|
|
|
|
|
Информация об обновлении одного пакета
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
install_info = "\[(binary|ebuild)[^\]]+\]"
|
|
|
|
|
atom_info = r"\S+"
|
|
|
|
|
use_info = 'USE="[^"]+"'
|
|
|
|
|
prev_version = "\[([^\]]+)\]"
|
|
|
|
|
pkg_size = r"[\d,]+ \w+"
|
|
|
|
|
|
|
|
|
|
attrs = ['binary', 'REPLACING_VERSIONS', 'SIZE']
|
|
|
|
|
|
|
|
|
|
def __init__(self, data):
|
|
|
|
|
self._data = data
|
|
|
|
|
self._package = None
|
|
|
|
|
self._info = {}
|
|
|
|
|
|
|
|
|
|
def _parseData(self):
|
|
|
|
|
r = self.update_info.search(self._data)
|
|
|
|
|
if r:
|
|
|
|
|
self._info['binary'] = r.group(2) == 'binary'
|
|
|
|
|
self._package = EmergePackage(r.group(3))
|
|
|
|
|
self._info['REPLACING_VERSIONS'] = r.group(4) or ""
|
|
|
|
|
self._info['SIZE'] = r.group(6) or ""
|
|
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
|
return chain(EmergePackage.attrs, self.attrs)
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
if not self['PN']:
|
|
|
|
|
return 0
|
|
|
|
|
else:
|
|
|
|
|
return len(EmergePackage.attrs) + len(self.attrs)
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
|
if not self._info:
|
|
|
|
|
self._parseData()
|
|
|
|
|
if item in self._info:
|
|
|
|
|
return self._info[item]
|
|
|
|
|
if self._package:
|
|
|
|
|
return self._package[item]
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def __lt__(self, version):
|
|
|
|
|
if not self._info:
|
|
|
|
|
self._parseData()
|
|
|
|
|
return self._package < version
|
|
|
|
|
|
|
|
|
|
def __eq__(self, version):
|
|
|
|
|
if not self._info:
|
|
|
|
|
self._parseData()
|
|
|
|
|
return self._package == version
|
|
|
|
|
|
|
|
|
|
def __contains__(self, item):
|
|
|
|
|
if not self._info:
|
|
|
|
|
self._parseData()
|
|
|
|
|
return item in self.attrs or item in self._package
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return "EmergeUpdateInfo(%s/%s,%s)" % (
|
|
|
|
|
self['CATEGORY'], self['PF'],
|
|
|
|
|
"binary" if self.binary else "ebuild")
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return "%s/%s" % (self['CATEGORY'], self['PF'])
|
|
|
|
|
|
|
|
|
|
@recalculate_update_info
|
|
|
|
|
class EmergeRemoveInfo(EmergeUpdateInfo):
|
|
|
|
|
"""
|
|
|
|
|
Информация об удалении одного пакета (в списке обновляемых пакетов)
|
|
|
|
|
"""
|
|
|
|
|
install_info = "\[(uninstall)[^\]]+\]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Eix:
|
|
|
|
|
"""
|
|
|
|
|
Вызов eix
|
|
|
|
|
|
|
|
|
|
package : пакет или список пакетов
|
|
|
|
|
*options : параметры eix
|
|
|
|
|
all_versions : отобразить все версии пакета или наилучшую
|
|
|
|
|
"""
|
|
|
|
|
cmd = getProgPath("/usr/bin/eix")
|
|
|
|
|
|
|
|
|
|
class Option:
|
|
|
|
|
Installed = '--installed'
|
|
|
|
|
Xml = '--xml'
|
|
|
|
|
Upgrade = '--upgrade'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
default_options = [Option.Xml]
|
|
|
|
|
|
|
|
|
|
def __init__(self, package, *options, **kwargs):
|
|
|
|
|
if type(package) in (tuple, list):
|
|
|
|
|
self.package = list(package)
|
|
|
|
|
else:
|
|
|
|
|
self.package = [package]
|
|
|
|
|
self.options = list(options) + self.package + self.default_options
|
|
|
|
|
if not kwargs.get('all_versions', False):
|
|
|
|
|
self.__get_versions = self._get_versions
|
|
|
|
|
self._get_versions = self._get_best_version
|
|
|
|
|
|
|
|
|
|
def _get_best_version(self, et):
|
|
|
|
|
ret = None
|
|
|
|
|
for ver in ifilter(lambda x: x.find('mask') is None,
|
|
|
|
|
et.iterfind('version')):
|
|
|
|
|
ret = ver.attrib['id']
|
|
|
|
|
yield ret
|
|
|
|
|
|
|
|
|
|
def get_output(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить вывод eix
|
|
|
|
|
"""
|
|
|
|
|
with closing(process(self.cmd, *self.options)) as p:
|
|
|
|
|
return p.read()
|
|
|
|
|
|
|
|
|
|
def get_packages(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить список пакетов
|
|
|
|
|
"""
|
|
|
|
|
return list(self._parseXml(self.get_output()))
|
|
|
|
|
|
|
|
|
|
def _get_versions(self, et):
|
|
|
|
|
for ver in et.iterfind('version'):
|
|
|
|
|
yield ver.attrib['id']
|
|
|
|
|
|
|
|
|
|
def _get_packages(self, et):
|
|
|
|
|
for pkg in et:
|
|
|
|
|
for version in self._get_versions(pkg):
|
|
|
|
|
yield "%s-%s" % (pkg.attrib['name'], version)
|
|
|
|
|
|
|
|
|
|
def _get_categories(self, et):
|
|
|
|
|
for category in et:
|
|
|
|
|
for pkg in self._get_packages(category):
|
|
|
|
|
yield "%s/%s" % (category.attrib['name'], pkg)
|
|
|
|
|
|
|
|
|
|
def _parseXml(self, buffer):
|
|
|
|
|
try:
|
|
|
|
|
eix_xml = ET.fromstring(buffer)
|
|
|
|
|
return self._get_categories(eix_xml)
|
|
|
|
|
except ET.ParseError:
|
|
|
|
|
return iter(())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EmergeLogTask(object):
|
|
|
|
|
def has_marker(self, line):
|
|
|
|
|
"""
|
|
|
|
|
Определить есть ли в строке маркер задачи
|
|
|
|
|
"""
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def get_begin_marker(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить маркер начала задачи
|
|
|
|
|
"""
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def get_end_marker(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить маркер завершения задачи
|
|
|
|
|
"""
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EmergeLogNamedTask(EmergeLogTask):
|
|
|
|
|
date_format = "%b %d, %Y %T"
|
|
|
|
|
|
|
|
|
|
def __init__(self, taskname):
|
|
|
|
|
self.taskname = taskname
|
|
|
|
|
|
|
|
|
|
def has_marker(self, line):
|
|
|
|
|
"""
|
|
|
|
|
Определить есть ли в строке маркер задачи
|
|
|
|
|
"""
|
|
|
|
|
return self.get_end_marker() in line
|
|
|
|
|
|
|
|
|
|
def get_begin_marker(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить маркер начала задачи
|
|
|
|
|
"""
|
|
|
|
|
return "Calculate: Started {taskname} on: {date}".format(
|
|
|
|
|
taskname=self.taskname,
|
|
|
|
|
date=datetime.datetime.now().strftime(self.date_format))
|
|
|
|
|
|
|
|
|
|
def get_end_marker(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить маркер завершения задачи
|
|
|
|
|
"""
|
|
|
|
|
return " *** Calculate: Finished %s" % self.taskname
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EmergeLog:
|
|
|
|
|
"""
|
|
|
|
|
EmergeLog(after).get_update(package_pattern)
|
|
|
|
|
"""
|
|
|
|
|
emerge_log = "/var/log/emerge.log"
|
|
|
|
|
re_complete_emerge = re.compile(r":::\scompleted (emerge) \(.*?\) (\S+)",
|
|
|
|
|
re.M)
|
|
|
|
|
re_complete_unmerge = re.compile(r">>>\s(unmerge) success: (\S+)", re.M)
|
|
|
|
|
|
|
|
|
|
def __init__(self, emerge_task=EmergeLogTask()):
|
|
|
|
|
"""
|
|
|
|
|
@type emerge_task: EmergeLogTask
|
|
|
|
|
"""
|
|
|
|
|
self.emerge_task = emerge_task
|
|
|
|
|
self._list = None
|
|
|
|
|
self._remove_list = None
|
|
|
|
|
|
|
|
|
|
def _get_last_changes(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить список измений по логу, от последней записи маркера
|
|
|
|
|
"""
|
|
|
|
|
log_data = SavableIterator(readLinesFile(self.emerge_log))
|
|
|
|
|
for line in log_data.save():
|
|
|
|
|
if self.emerge_task.has_marker(line):
|
|
|
|
|
log_data.save()
|
|
|
|
|
return log_data.restore()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def list(self):
|
|
|
|
|
if self._list is None:
|
|
|
|
|
self.get_packages()
|
|
|
|
|
return self._list
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def remove_list(self):
|
|
|
|
|
if self._remove_list is None:
|
|
|
|
|
self.get_packages()
|
|
|
|
|
return self._remove_list
|
|
|
|
|
|
|
|
|
|
def get_packages(self):
|
|
|
|
|
"""
|
|
|
|
|
Получить список пакетов
|
|
|
|
|
"""
|
|
|
|
|
self._list, self._remove_list = \
|
|
|
|
|
zip(*self._parse_log(self._get_last_changes()))
|
|
|
|
|
self._list = filter(None,self._list)
|
|
|
|
|
self._remove_list = filter(None, self._remove_list)
|
|
|
|
|
|
|
|
|
|
def _parse_log(self, data):
|
|
|
|
|
searcher = lambda x:(self.re_complete_emerge.search(x) or
|
|
|
|
|
self.re_complete_unmerge.search(x))
|
|
|
|
|
for re_match in ifilter(None, imap(searcher, data)):
|
|
|
|
|
if re_match.group(1) == "emerge":
|
|
|
|
|
yield re_match.group(2), None
|
|
|
|
|
else:
|
|
|
|
|
yield None, re_match.group(2)
|
|
|
|
|
yield None, None
|
|
|
|
|
|
|
|
|
|
def _set_marker(self, text_marker):
|
|
|
|
|
with open(self.emerge_log, 'a') as f:
|
|
|
|
|
f.write("{0:.0f}: {1}\n".format(time.time(), text_marker))
|
|
|
|
|
|
|
|
|
|
def mark_begin_task(self):
|
|
|
|
|
"""
|
|
|
|
|
Отметить в emerge.log начало выполнения задачи
|
|
|
|
|
"""
|
|
|
|
|
marker = self.emerge_task.get_begin_marker()
|
|
|
|
|
if marker:
|
|
|
|
|
self._set_marker(marker)
|
|
|
|
|
|
|
|
|
|
def mark_end_task(self):
|
|
|
|
|
"""
|
|
|
|
|
Отметить в emerge.log завершение выполнения задачи
|
|
|
|
|
"""
|
|
|
|
|
marker = self.emerge_task.get_end_marker()
|
|
|
|
|
if marker:
|
|
|
|
|
self._set_marker(marker)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PackageList(object):
|
|
|
|
|
"""
|
|
|
|
|
Список пакетов с возможностью среза и сравнением с версией
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, packages):
|
|
|
|
|
self._raw_list = packages
|
|
|
|
|
self.result = None
|
|
|
|
|
|
|
|
|
|
def _packages(self):
|
|
|
|
|
if self.result is None:
|
|
|
|
|
self.result = filter(lambda x: x['PN'],
|
|
|
|
|
imap(lambda x: (x if isinstance(x, Mapping)
|
|
|
|
|
else EmergePackage(x)),
|
|
|
|
|
self._raw_list))
|
|
|
|
|
return self.result
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
|
|
|
|
re_item = re.compile(item)
|
|
|
|
|
return PackageList([pkg for pkg in self._packages() if
|
|
|
|
|
re_item.search(pkg['CATEGORY/PN'])])
|
|
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
|
return iter(self._packages())
|
|
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
|
return len(list(self._packages()))
|
|
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
|
return any(pkg < other for pkg in self._packages())
|
|
|
|
|
|
|
|
|
|
def __le__(self, other):
|
|
|
|
|
return any(pkg <= other for pkg in self._packages())
|
|
|
|
|
|
|
|
|
|
def __gt__(self, other):
|
|
|
|
|
return any(pkg > other for pkg in self._packages())
|
|
|
|
|
|
|
|
|
|
def __ge__(self, other):
|
|
|
|
|
return any(pkg >= other for pkg in self._packages())
|
|
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
for pkg in self._packages():
|
|
|
|
|
if pkg == other:
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return False
|
|
|
|
|
return any(pkg == other for pkg in self._packages())
|
|
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
|
return any(pkg != other for pkg in self._packages())
|