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

455 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#-*- 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 calculate.lib.utils.files import (process,getProgPath,STDOUT,removeDir,
processProgress)
class UpdateError(Exception):
"""Update Error"""
class GitError(Exception):
"""Git Error"""
def __init__(self, msg, addon=None):
self.message = msg
self.addon = addon
Exception.__init__(self,msg)
from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_update3',sys.modules[__name__])
__ = getLazyLocalTranslate(_)
class PercentProgress(processProgress):
"""
Объект выдает прогресс, ища в выводе \d+%
Args:
part: количество прогрессов в программе
delimeter: разделители строк по умолчанию \n и \r
cachefilter: фильтр вывода программы (регулярная строка)
"""
def init(self,*args,**kwargs):
self.rePerc = re.compile("(\d+)%",re.S)
self.part = kwargs.get("part",1)
self.add_offset = 100 / self.part
self.offset = 0
self.stderr = STDOUT
self.delimeter = re.compile("[%s]"%kwargs.get("delimeter","\n\r"))
self.cachedata = re.compile(kwargs.get("cachefilter",
"((?:error|warning|fatal):.*)"))
def processInit(self):
self.percent = 0
self.showval = 0
return 0
def processEnd(self):
self.percent = 100
return 100
def processString(self,strdata):
match = self.rePerc.search(strdata)
resSearch = self.cachedata.search(strdata)
if resSearch:
self.cacheresult.append(resSearch.group(1))
if match:
percent = int(match.group(1))
if percent < self.percent:
self.offset = self.offset + self.add_offset
percent = percent / self.part
if percent != self.percent:
self.percent = percent
showval = min(99,self.percent + self.offset)
if showval != self.showval:
self.showval = showval
return self.showval
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=3,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 если клонирование произведено с
установкой на последнюю ревизию. В остальных случаях 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] == "fetch":
gitClone = PercentProgress(*wholeCommand+["--progress","--verbose"],
part=3,stderr=STDOUT)
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: использовать уже полученную информация об изменения в репозитории
"""
git_dir = self.__gitDir(rpath)
if to_origin and not info:
info = self._getStatusInfo(rpath)
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))
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):
"""
Синхронизировать репозиторий
"""
git = Git()
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)
else:
# если нужно обновиться до конкретной ревизии
if revision != "last":
if revision == git._getCurrentCommit(rpath):
if git._getBranch(rpath) == branch:
return
# получить изменения из удаленного репозитория
git._fetchRepository(rpath,cb_progress=cb_progress)
# если текущая ветка не соответствует нужной
repInfo = git._getStatusInfo(rpath)
if repInfo['branch'] != branch:
# меняем ветку
git._checkoutBranch(rpath,branch)
if revision == "last":
git._resetRepository(rpath, to_origin=True)
else:
git._resetRepository(rpath, to_rev=revision)
return True
def syncRepositories(self,repname,clean_on_error=True):
"""
Синхронизировать репозитории
"""
# TODO: оновление shallow репозитория возможно придется
# скачивать конкретную ревизию через refs/revs
# TODO: прогресс скачивания (обновление shallow)
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))
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=name))
removeDir(rpath)
self._syncRepository(name,url,rpath,revision,branch)
return True