############################################################################## # # Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved. # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsibility of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # guarantees and support are strongly advised to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 3 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## import errno import os import re import shutil import subprocess import sys import threading from ProcessManager import SubprocessError SVN_UP_REV = re.compile(r'^(?:At|Updated to) revision (\d+).$') SVN_CHANGED_REV = re.compile(r'^Last Changed Rev.*:\s*(\d+)', re.MULTILINE) GIT_TYPE = 'git' SVN_TYPE = 'svn' class Updater(object): _git_cache = {} stdin = file(os.devnull) def __init__(self, repository_path, log, revision=None, git_binary=None, branch=None, realtime_output=True, process_manager=None, url=None, working_directory=None): self.log = log self.revision = revision self._path_list = [] self.branch = branch self.repository_path = repository_path self.git_binary = git_binary self.realtime_output = realtime_output self.process_manager = process_manager self.url = url self.working_directory = working_directory def getRepositoryPath(self): return self.repository_path def getRepositoryType(self): try: return self.repository_type except AttributeError: # guess the type of repository we have if os.path.isdir(os.path.join( self.getRepositoryPath(), '.git')): repository_type = GIT_TYPE elif os.path.isdir(os.path.join( self.getRepositoryPath(), '.svn')): repository_type = SVN_TYPE else: raise NotImplementedError self.repository_type = repository_type return repository_type def deletePycFiles(self, path): """Delete *.pyc files so that deleted/moved files can not be imported""" for path, dir_list, file_list in os.walk(path): for file in file_list: if file[-4:] in ('.pyc', '.pyo'): # allow several processes clean the same folder at the same time try: os.remove(os.path.join(path, file)) except OSError, e: if e.errno != errno.ENOENT: raise def spawn(self, *args, **kw): cwd = kw.pop("cwd", None) if cwd is None: cwd = self.getRepositoryPath() return self.process_manager.spawn(*args, log_prefix='git', cwd=cwd, **kw) def _git(self, *args, **kw): return self.spawn(self.git_binary, *args, **kw)['stdout'].strip() def git_update_server_info(self): return self._git('update-server-info', '-f') def git_create_repository_link(self): """ Create a link in depository to the ".git" directory. ex: for "../erp5/.git" "../erp5/erp5.git"->"../erp5/.git" will be created. """ git_repository_path = os.path.join(self.getRepositoryPath(), '.git') name = os.path.basename(os.path.normpath(self.getRepositoryPath())) git_repository_link_path = os.path.join(self.getRepositoryPath(), '%s.git' %name) self.log("checking link %s -> %s.." %(git_repository_link_path,git_repository_path)) if ( not os.path.lexists(git_repository_link_path) and \ not os.path.exists(git_repository_link_path) ): try: os.symlink(git_repository_path, git_repository_link_path) self.log("link: %s -> %s created" %(git_repository_link_path,git_repository_path)) except: self.log("Cannot create link from %s -> %s" %(git_repository_link_path,git_repository_path)) def _git_find_rev(self, ref): try: return self._git_cache[ref] except KeyError: if os.path.exists('.git/svn'): r = self._git('svn', 'find-rev', ref) assert r self._git_cache[ref[0] != 'r' and 'r%u' % int(r) or r] = ref else: r = self._git('rev-list', '--topo-order', '--count', ref), ref self._git_cache[ref] = r return r def getRevision(self, *path_list): if not path_list: path_list = self._path_list if self.getRepositoryType() == GIT_TYPE: h = self._git('log', '-1', '--format=%H', '--', *path_list) return self._git_find_rev(h) elif self.getRepositoryType() == SVN_TYPE: stdout = self.spawn('svn', 'info', *path_list)['stdout'] return str(max(map(int, SVN_CHANGED_REV.findall(stdout)))) raise NotImplementedError def deleteRepository(self): self.log("Wrong repository or wrong url, deleting repos %s" % \ self.repository_path) shutil.rmtree(self.repository_path) def checkRepository(self): # make sure that the repository is like we expect if self.url: if os.path.exists(self.repository_path): correct_url = False try: remote_url = self._git("config", "--get", "remote.origin.url") if remote_url == self.url: correct_url = True except (SubprocessError,) as e: self.log("SubprocessError", exc_info=sys.exc_info()) if not(correct_url): self.deleteRepository() if not os.path.exists(self.repository_path): parameter_list = ['clone', self.url] if self.branch is not None: parameter_list.extend(['-b', self.branch]) parameter_list.append(self.repository_path) self._git(*parameter_list, cwd=self.working_directory) def checkout(self, *path_list): self.checkRepository() if not path_list: path_list = '.', revision = self.revision if self.getRepositoryType() == GIT_TYPE: # edit .git/info/sparse-checkout if you want sparse checkout if revision: if type(revision) is str: h = revision else: h = revision[1] if h != self._git('rev-parse', 'HEAD'): self.deletePycFiles(self.repository_path) # For performance reasons, 'reset --merge' only looks at mtime & ctime # to check is the index is correct and conflicts immediately if # contents or metadata changed. Even hardlinking a file changes its # ctime, so at least for buildout (local download), we need to # refresh index first. self._git('update-index', '--refresh') self._git('reset', '--merge', h) else: self.deletePycFiles(self.repository_path) if os.path.exists('.git/svn'): self._git('svn', 'rebase') else: self._git('fetch', '--all', '--prune') if self.branch and \ not ("* %s" % self.branch in self._git('branch').split("\n")): # Delete branch if already exists if self.branch in self._git('branch'): self._git('branch', '-D', self.branch) self._git('checkout', 'origin/%s' % self.branch, '-b', self.branch) self._git('reset', '--hard', '@{u}') self._git('clean', '-fdx') self.revision = self._git_find_rev(self._git('rev-parse', 'HEAD')) elif self.getRepositoryType() == SVN_TYPE: # following code allows sparse checkout def svn_mkdirs(path): path = os.path.dirname(path) if path and not os.path.isdir(path): svn_mkdirs(path) self.spawn(*(args + ['--depth=empty', path])) for path in path_list: args = ['svn', 'up', '--force', '--non-interactive'] if revision: args.append('-r%s' % revision) svn_mkdirs(path) args += '--set-depth=infinity', path self.deletePycFiles(path) try: status_dict = self.spawn(*args) except SubprocessError, e: if 'cleanup' not in e.stderr: raise self.spawn('svn', 'cleanup', path) status_dict = self.spawn(*args) if not revision: self.revision = revision = SVN_UP_REV.findall( status_dict['stdout'].splitlines()[-1])[0] else: raise NotImplementedError self._path_list += path_list self.git_update_server_info()