#-*- 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