Updater.py 8.64 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
##############################################################################
#
# 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.
#
##############################################################################
27 28 29
import errno
import os
import re
30
from . import logger
31
from .ProcessManager import SubprocessError
32
from .Utils import rmtree
33
from slapos.util import bytes2str, str2bytes
34

35 36 37
SVN_UP_REV = re.compile(r'^(?:At|Updated to) revision (\d+).$')
SVN_CHANGED_REV = re.compile(r'^Last Changed Rev.*:\s*(\d+)', re.MULTILINE)

38 39 40 41

GIT_TYPE = 'git'
SVN_TYPE = 'svn'

42
class Updater(object):
43 44 45

  _git_cache = {}

46
  def __init__(self, repository_path, revision=None, git_binary='git',
47 48
      branch=None, realtime_output=True, process_manager=None, url=None,
      working_directory=None):
49 50
    self.revision = revision
    self._path_list = []
51
    self.branch = branch
52 53
    self.repository_path = repository_path
    self.git_binary = git_binary
54
    self.realtime_output = realtime_output
55
    self.process_manager = process_manager
56 57
    self.url = url
    self.working_directory = working_directory
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

  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"""
80
    for path, _, file_list in os.walk(path):
81 82 83 84 85
      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))
86
          except OSError as e:
87 88 89 90
            if e.errno != errno.ENOENT:
              raise

  def spawn(self, *args, **kw):
91 92 93
    cwd = kw.pop("cwd", None)
    if cwd is None:
      cwd = self.getRepositoryPath()
94 95
    return self.process_manager.spawn(*args, 
                                      log_prefix='git',
96
                                      cwd=cwd,
97
                                      **kw)
98 99

  def _git(self, *args, **kw):
100
    return bytes2str(self.spawn(self.git_binary, *args, **kw)['stdout'].strip())
101

102 103 104 105 106 107 108 109 110 111 112 113
  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)
114
    logger.debug("checking link %s -> %s..",
115
             git_repository_link_path, git_repository_path)
116 117 118 119
    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)
120
        logger.debug("link: %s -> %s created",
121 122
          git_repository_link_path, git_repository_path)
      except OSError:
123
        logger.error("Cannot create link from %s -> %s",
124
          git_repository_link_path, git_repository_path)
125
  
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
  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

150
  def deleteRepository(self):
151
    logger.info("Wrong repository or wrong url, deleting repos %s",
152
             self.repository_path)
153
    rmtree(self.repository_path)
154 155 156 157 158 159 160 161 162 163

  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
164
        except SubprocessError:
165
          logger.exception("")
166 167 168 169 170 171 172 173 174
        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)

175
  def checkout(self, *path_list):
176
    self.checkRepository()
177 178 179 180 181 182 183
    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:
184
          h = revision
185 186 187
        else:
          h = revision[1]
        if h != self._git('rev-parse', 'HEAD'):
188 189 190 191
          self._git('clean', '-fdx')
          # For performance, it is ok to use 'git reset --hard',
          # theses days it is not slow like it was long time ago.
          self._git('reset', '--hard', h)
192
      else:
193
        self._git('clean', '-fdx')
194 195 196
        if os.path.exists('.git/svn'):
          self._git('svn', 'rebase')
        else:
197 198 199
          self._git('fetch', '--all', '--prune')
          if self.branch and \
            not ("* %s" % self.branch in self._git('branch').split("\n")):
200
              # Delete branch if already exists
201
              if self.branch in [x.strip() for x in self._git('branch').split("\n")]:
202
                self._git('branch', '-D', self.branch)
203 204
              self._git('checkout',  'origin/%s' % self.branch, '-b',
                        self.branch)
205
          self._git('reset', '--hard', '@{u}')
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
        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)
223
        except SubprocessError as e:
224 225 226 227 228 229 230 231 232 233
          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
234
    self.git_update_server_info()