You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
calculate-utils-3-update/update/update.py

525 lines
22 KiB

#-*- 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.
import os
import re
import sys
import time
from os import path
from subprocess import Popen
from calculate.lib.utils.files import (process,getProgPath,STDOUT,removeDir,
processProgress,PercentProgress,process)
class AddonError(Exception):
"""
Исключение с добавочным сообщением
"""
def __init__(self, msg, addon=None):
self.message = msg
self.addon = addon
Exception.__init__(self,msg)
class UpdateError(AddonError):
"""Update Error"""
class GitError(AddonError):
"""Git Error"""
from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_update3',sys.modules[__name__])
__ = getLazyLocalTranslate(_)
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
def __gitDir(self,rpath):
return path.join(rpath,".git")
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():
for i in git_status:
if i.strip():
return False
else:
return True
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
class Update:
"""Основной объект для выполнения действий связанных с обновлением системы
"""
def _syncRepository(self,name,url,rpath,revision,branch,
cb_progress=None):
"""
Синхронизировать репозиторий
"""
dv = self.clVars
git = Git()
needMeta = False
if not git._checkExistsRep(rpath):
if revision == "last":
git._cloneRepository(url, rpath, branch,
cb_progress=cb_progress)
else:
git._cloneRevRepository(url, rpath, branch, revision,
cb_progress=cb_progress)
needMeta = True
else:
# если нужно обновиться до конкретной ревизии
if revision != "last":
if revision == git._getCurrentCommit(rpath):
if git._getBranch(rpath) == branch:
return True
# получить изменения из удаленного репозитория
git._fetchRepository(rpath,cb_progress=cb_progress)
# если текущая ветка не соответствует нужной
repInfo = git._getStatusInfo(rpath)
if repInfo['branch'] != branch:
# меняем ветку
needMeta = True
git._checkoutBranch(rpath,branch)
if revision == "last":
if git._resetRepository(rpath, to_origin=True):
needMeta = True
else:
git._resetRepository(rpath, to_rev=revision)
needMeta = True
if needMeta:
dv.Set('cl_update_outdate_set','on',force=True)
return True
def syncRepositories(self,repname,clean_on_error=True):
"""
Синхронизировать репозитории
"""
dv = self.clVars
url, rpath, revision, branch = (
dv.Select(["cl_update_rep_url","cl_update_rep_path",
"cl_update_rep_rev","cl_update_branch_name"],
where="cl_update_rep_name",eq=repname,limit=1))
if not url or not rpath:
raise UpdateError(_("Repositories variables is not configured"))
self.addProgress()
if clean_on_error:
try:
self._syncRepository(repname,url,rpath,revision,branch,
cb_progress=self.setProgress)
return True
except GitError as e:
if e.addon:
self.printWARNING(str(e.addon))
self.printWARNING(str(e))
self.printWARNING(_("Re-fetch {name} repository"
).format(name=repname))
removeDir(rpath)
self._syncRepository(name,url,rpath,revision,branch)
return True
def syncLaymanRepository(self,repname):
"""
Обновить репозиторий через layman
"""
layman = getProgPath('/usr/bin/layman')
if not layman:
raise UpdateError(_("Layman utility is not found"))
rpath = self.clVars.Select('cl_update_other_rep_path',
where='cl_update_other_rep_name',eq=repname,limit=1)
laymanname = path.basename(rpath)
if path.exists(path.join(rpath,'.git')):
self.addProgress()
p = PercentProgress(layman,"-s",laymanname,part=1,atty=True)
for perc in p.progress():
self.setProgress(perc)
else:
p = process(layman,"-s",repname,stderr=STDOUT)
if p.failed():
raise UpdateError(_("Failed to update repository {rname}"
).format(rname=repname),addon=p.read())
return True
def regenCache(self,repname):
"""
Обновить кэш метаданных репозитория
"""
egenCache = getProgPath('/usr/bin/egencache')
if not egenCache:
raise UpdateError(_("Portage utility is not found"))
cpu_num = self.clVars.Get('hr_cpu_num')
p = process(egenCache,"--repo=%s"%repname,"--update",
"--jobs=%s"%cpu_num,stderr=STDOUT)
if p.failed():
raise UpdateError(_("Failed to update cache of {rname} repository"
).format(rname=repname),addon=p.read())
return True
def emergeMetadata(self):
"""
Выполнить egencache и emerge --metadata
"""
emerge = getProgPath("/usr/bin/emerge")
if not emerge:
raise UpdateError(_("Emerge utility is not found"))
self.addProgress()
p = PercentProgress(emerge,"--metadata",part=1,atty=True)
for perc in p.progress():
self.setProgress(perc)
if p.failed():
raise UpdateError(_("Failed to update metadata"),addon=p.read())
return True
def eixUpdate(self):
"""
Выполенине eix-update для репозиторием
eix-update выполнятется только для тех репозиториев, которые
обновлялись, если cl_update_eixsync_force==auto, либо
все, если cl_update_eixupdate_force==force
"""
eixupdate = getProgPath("/usr/bin/eix-update")
if not eixupdate:
raise UpdateError(_("Eix utility is not found"))
self.addProgress()
excludeList = []
if self.clVars.Get('cl_update_eixupdate_force') == 'force':
countRep = len(self.clVars.Get('cl_update_rep_name'))
else:
for rep in self.clVars.Get('cl_update_rep_name'):
# подстановка имен
mapNames = {'portage':'gentoo'}
if not rep in self.clVars.Get('cl_update_sync_rep'):
excludeList.extend(["-x",mapNames.get(rep,rep)])
countRep = len(self.clVars.Get('cl_update_sync_rep'))
if (self.clVars.Get('cl_update_other_set') == 'on' or
self.clVars.Get('cl_update_eixupdate_force') == 'force'):
countRep += len(self.clVars.Get('update.cl_update_other_rep_name'))
else:
for rep in self.clVars.Get('update.cl_update_other_rep_name'):
excludeList.extend(['-x',rep])
p = PercentProgress(eixupdate,"-F",*excludeList,part=countRep or 1,atty=True)
for perc in p.progress():
self.setProgress(perc)
if p.failed():
raise UpdateError(_("Failed to update eix cache"),addon=p.read())
return True