| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- Subversion details |
|---|
| 3 | # :Creato: ven 18 giu 2004 15:00:52 CEST |
|---|
| 4 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | This module contains supporting classes for Subversion. |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | __docformat__ = 'reStructuredText' |
|---|
| 13 | |
|---|
| 14 | from vcpx.repository import Repository |
|---|
| 15 | from vcpx.shwrap import ExternalCommand, PIPE, ReopenableNamedTemporaryFile |
|---|
| 16 | from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure |
|---|
| 17 | from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure |
|---|
| 18 | from vcpx.config import ConfigurationError |
|---|
| 19 | from vcpx.tzinfo import UTC |
|---|
| 20 | |
|---|
| 21 | |
|---|
| 22 | class SvnRepository(Repository): |
|---|
| 23 | METADIR = '.svn' |
|---|
| 24 | |
|---|
| 25 | def command(self, *args, **kwargs): |
|---|
| 26 | if kwargs.get('svnadmin', False): |
|---|
| 27 | kwargs['executable'] = self.__svnadmin |
|---|
| 28 | return Repository.command(self, *args, **kwargs) |
|---|
| 29 | |
|---|
| 30 | def _load(self, project): |
|---|
| 31 | Repository._load(self, project) |
|---|
| 32 | cget = project.config.get |
|---|
| 33 | self.EXECUTABLE = cget(self.name, 'svn-command', 'svn') |
|---|
| 34 | self.__svnadmin = cget(self.name, 'svnadmin-command', 'svnadmin') |
|---|
| 35 | self.use_propset = cget(self.name, 'use-propset', False) |
|---|
| 36 | self.propset_date = cget(self.name, 'propset-date', True) |
|---|
| 37 | self.filter_badchars = cget(self.name, 'filter-badchars', False) |
|---|
| 38 | self.use_limit = cget(self.name, 'use-limit', True) |
|---|
| 39 | self.trust_root = cget(self.name, 'trust-root', False) |
|---|
| 40 | self.ignore_externals = cget(self.name, 'ignore-externals', True) |
|---|
| 41 | self.commit_all_files = cget(self.name, 'commit-all-files', True) |
|---|
| 42 | self.tags_path = cget(self.name, 'svn-tags', '/tags') |
|---|
| 43 | self.branches_path = cget(self.name, 'svn-branches', '/branches') |
|---|
| 44 | self._setupTagsDirectory = None |
|---|
| 45 | |
|---|
| 46 | def setupTagsDirectory(self): |
|---|
| 47 | if self._setupTagsDirectory == None: |
|---|
| 48 | self._setupTagsDirectory = False |
|---|
| 49 | if self.module and self.module <> '/': |
|---|
| 50 | |
|---|
| 51 | # Check the existing tags directory |
|---|
| 52 | cmd = self.command("ls") |
|---|
| 53 | svnls = ExternalCommand(command=cmd) |
|---|
| 54 | svnls.execute(self.repository + self.tags_path) |
|---|
| 55 | if svnls.exit_status: |
|---|
| 56 | # create it, if not exist |
|---|
| 57 | cmd = self.command("mkdir", "-m", |
|---|
| 58 | "This directory will host the tags") |
|---|
| 59 | svnmkdir = ExternalCommand(command=cmd) |
|---|
| 60 | svnmkdir.execute(self.repository + self.tags_path) |
|---|
| 61 | if svnmkdir.exit_status: |
|---|
| 62 | raise TargetInitializationFailure( |
|---|
| 63 | "Was not able to create tags directory '%s'" |
|---|
| 64 | % self.tags_path) |
|---|
| 65 | else: |
|---|
| 66 | self.log.debug("Directory '%s' already exists" |
|---|
| 67 | % self.tags_path) |
|---|
| 68 | self._setupTagsDirectory = True |
|---|
| 69 | else: |
|---|
| 70 | self.log.debug("Tags needs module setup other than '/'") |
|---|
| 71 | |
|---|
| 72 | return self._setupTagsDirectory |
|---|
| 73 | |
|---|
| 74 | |
|---|
| 75 | def _validateConfiguration(self): |
|---|
| 76 | from vcpx.config import ConfigurationError |
|---|
| 77 | |
|---|
| 78 | Repository._validateConfiguration(self) |
|---|
| 79 | |
|---|
| 80 | if not self.repository: |
|---|
| 81 | self.log.critical('Missing repository information in %r', self.name) |
|---|
| 82 | raise ConfigurationError("Must specify the root of the " |
|---|
| 83 | "Subversion repository used " |
|---|
| 84 | "as %s with the option " |
|---|
| 85 | "'repository'" % self.which) |
|---|
| 86 | elif self.repository.endswith('/'): |
|---|
| 87 | self.log.debug("Removing final slash from %r in %r", |
|---|
| 88 | self.repository, self.name) |
|---|
| 89 | self.repository = self.repository.rstrip('/') |
|---|
| 90 | |
|---|
| 91 | if not self.module: |
|---|
| 92 | self.log.critical('Missing module information in %r', self.name) |
|---|
| 93 | raise ConfigurationError("Must specify the path within the " |
|---|
| 94 | "Subversion repository as 'module'") |
|---|
| 95 | |
|---|
| 96 | if self.module == '.': |
|---|
| 97 | self.log.warning("Replacing '.' with '/' in module name in %r", |
|---|
| 98 | self.name) |
|---|
| 99 | self.module = '/' |
|---|
| 100 | elif not self.module.startswith('/'): |
|---|
| 101 | self.log.debug("Prepending '/' to module %r in %r", |
|---|
| 102 | self.module, self.name) |
|---|
| 103 | self.module = '/' + self.module |
|---|
| 104 | |
|---|
| 105 | if not self.tags_path.startswith('/'): |
|---|
| 106 | self.log.debug("Prepending '/' to svn-tags %r in %r", |
|---|
| 107 | self.tags_path, self.name) |
|---|
| 108 | self.tags_path = '/' + self.tags_path |
|---|
| 109 | |
|---|
| 110 | if not self.branches_path.startswith('/'): |
|---|
| 111 | self.log.debug("Prepending '/' to svn-branches %r in %r", |
|---|
| 112 | self.branches_path, self.name) |
|---|
| 113 | self.branches_path = '/' + self.branches_path |
|---|
| 114 | |
|---|
| 115 | def create(self): |
|---|
| 116 | """ |
|---|
| 117 | Create a local SVN repository, if it does not exist, and configure it. |
|---|
| 118 | """ |
|---|
| 119 | |
|---|
| 120 | from os.path import join, exists |
|---|
| 121 | from sys import platform |
|---|
| 122 | |
|---|
| 123 | # Verify the existence of repository by listing its root |
|---|
| 124 | cmd = self.command("ls") |
|---|
| 125 | svnls = ExternalCommand(command=cmd) |
|---|
| 126 | svnls.execute(self.repository) |
|---|
| 127 | |
|---|
| 128 | # Create it if it isn't a valid repository |
|---|
| 129 | if svnls.exit_status: |
|---|
| 130 | if not self.repository.startswith('file:///'): |
|---|
| 131 | raise TargetInitializationFailure("%r does not exist and " |
|---|
| 132 | "cannot be created since " |
|---|
| 133 | "it's not a local (file:///) " |
|---|
| 134 | "repository" % |
|---|
| 135 | self.repository) |
|---|
| 136 | |
|---|
| 137 | repodir = self.repository[7:] |
|---|
| 138 | cmd = self.command("create", "--fs-type", "fsfs", svnadmin=True) |
|---|
| 139 | svnadmin = ExternalCommand(command=cmd) |
|---|
| 140 | svnadmin.execute(repodir) |
|---|
| 141 | |
|---|
| 142 | if svnadmin.exit_status: |
|---|
| 143 | raise TargetInitializationFailure("Was not able to create a 'fsfs' " |
|---|
| 144 | "svn repository at %r" % |
|---|
| 145 | self.repository) |
|---|
| 146 | if self.use_propset: |
|---|
| 147 | if not self.repository.startswith('file:///'): |
|---|
| 148 | self.log.warning("Repository is remote, cannot verify if it " |
|---|
| 149 | "has the 'pre-revprop-change' hook active, needed " |
|---|
| 150 | "by 'use-propset=True'. Assuming it does...") |
|---|
| 151 | else: |
|---|
| 152 | repodir = self.repository[7:] |
|---|
| 153 | hookname = join(repodir, 'hooks', 'pre-revprop-change') |
|---|
| 154 | if platform == 'win32': |
|---|
| 155 | hookname += '.bat' |
|---|
| 156 | if not exists(hookname): |
|---|
| 157 | prehook = open(hookname, 'w') |
|---|
| 158 | if platform <> 'win32': |
|---|
| 159 | prehook.write('#!/bin/sh\n') |
|---|
| 160 | prehook.write('exit 0\n') |
|---|
| 161 | prehook.close() |
|---|
| 162 | if platform <> 'win32': |
|---|
| 163 | from os import chmod |
|---|
| 164 | chmod(hookname, 0755) |
|---|
| 165 | |
|---|
| 166 | if self.module and self.module <> '/': |
|---|
| 167 | cmd = self.command("ls") |
|---|
| 168 | svnls = ExternalCommand(command=cmd) |
|---|
| 169 | svnls.execute(self.repository + self.module) |
|---|
| 170 | if svnls.exit_status: |
|---|
| 171 | |
|---|
| 172 | paths = [] |
|---|
| 173 | |
|---|
| 174 | # Auto detect missing "branches/" |
|---|
| 175 | if self.module.startswith(self.branches_path + '/'): |
|---|
| 176 | path = self.repository + self.branches_path |
|---|
| 177 | cmd = self.command("ls") |
|---|
| 178 | svnls = ExternalCommand(command=cmd) |
|---|
| 179 | svnls.execute(path) |
|---|
| 180 | if svnls.exit_status: |
|---|
| 181 | paths.append(path) |
|---|
| 182 | |
|---|
| 183 | paths.append(self.repository + self.module) |
|---|
| 184 | cmd = self.command("mkdir", "-m", |
|---|
| 185 | "This directory will host the upstream sources") |
|---|
| 186 | svnmkdir = ExternalCommand(command=cmd) |
|---|
| 187 | svnmkdir.execute(paths) |
|---|
| 188 | if svnmkdir.exit_status: |
|---|
| 189 | raise TargetInitializationFailure("Was not able to create the " |
|---|
| 190 | "module %r, maybe more than " |
|---|
| 191 | "one level directory?" % |
|---|
| 192 | self.module) |
|---|
| 193 | |
|---|
| 194 | def changesets_from_svnlog(log, repository, chunksize=2**15): |
|---|
| 195 | from xml.sax import make_parser |
|---|
| 196 | from xml.sax.handler import ContentHandler, ErrorHandler |
|---|
| 197 | from datetime import datetime |
|---|
| 198 | from vcpx.changes import ChangesetEntry, Changeset |
|---|
| 199 | |
|---|
| 200 | def get_entry_from_path(path, module=repository.module): |
|---|
| 201 | # Given the repository url of this wc, say |
|---|
| 202 | # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" |
|---|
| 203 | # extract the "entry" portion (a relative path) from what |
|---|
| 204 | # svn log --xml says, ie |
|---|
| 205 | # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py" |
|---|
| 206 | # that is to say "tests/PloneTestCase.py" |
|---|
| 207 | |
|---|
| 208 | if not module.endswith('/'): |
|---|
| 209 | module = module + '/' |
|---|
| 210 | if path.startswith(module): |
|---|
| 211 | relative = path[len(module):] |
|---|
| 212 | return relative |
|---|
| 213 | |
|---|
| 214 | # The path is outside our tracked tree... |
|---|
| 215 | repository.log.warning('Ignoring %r since it is not under %r', |
|---|
| 216 | path, module) |
|---|
| 217 | return None |
|---|
| 218 | |
|---|
| 219 | class SvnXMLLogHandler(ContentHandler): |
|---|
| 220 | # Map between svn action and tailor's. |
|---|
| 221 | # NB: 'R', in svn parlance, means REPLACED, something other |
|---|
| 222 | # system may view as a simpler ADD, taking the following as |
|---|
| 223 | # the most common idiom:: |
|---|
| 224 | # |
|---|
| 225 | # # Rename the old file with a better name |
|---|
| 226 | # $ svn mv somefile nicer-name-scheme.py |
|---|
| 227 | # |
|---|
| 228 | # # Be nice with lazy users |
|---|
| 229 | # $ echo "exec nicer-name-scheme.py" > somefile |
|---|
| 230 | # |
|---|
| 231 | # # Add the wrapper with the old name |
|---|
| 232 | # $ svn add somefile |
|---|
| 233 | # |
|---|
| 234 | # $ svn commit -m "Longer name for somefile" |
|---|
| 235 | |
|---|
| 236 | ACTIONSMAP = {'R': 'R', # will be ChangesetEntry.ADDED |
|---|
| 237 | 'M': ChangesetEntry.UPDATED, |
|---|
| 238 | 'A': ChangesetEntry.ADDED, |
|---|
| 239 | 'D': ChangesetEntry.DELETED} |
|---|
| 240 | |
|---|
| 241 | def __init__(self): |
|---|
| 242 | self.changesets = [] |
|---|
| 243 | self.current = None |
|---|
| 244 | self.current_field = [] |
|---|
| 245 | self.renamed = {} |
|---|
| 246 | self.copies = [] |
|---|
| 247 | |
|---|
| 248 | def startElement(self, name, attributes): |
|---|
| 249 | if name == 'logentry': |
|---|
| 250 | self.current = {} |
|---|
| 251 | self.current['revision'] = attributes['revision'] |
|---|
| 252 | self.current['entries'] = [] |
|---|
| 253 | self.copies = [] |
|---|
| 254 | elif name in ['author', 'date', 'msg']: |
|---|
| 255 | self.current_field = [] |
|---|
| 256 | elif name == 'path': |
|---|
| 257 | self.current_field = [] |
|---|
| 258 | if attributes.has_key('copyfrom-path'): |
|---|
| 259 | self.current_path_action = ( |
|---|
| 260 | attributes['action'], |
|---|
| 261 | attributes['copyfrom-path'], |
|---|
| 262 | attributes['copyfrom-rev']) |
|---|
| 263 | else: |
|---|
| 264 | self.current_path_action = attributes['action'] |
|---|
| 265 | |
|---|
| 266 | def endElement(self, name): |
|---|
| 267 | if name == 'logentry': |
|---|
| 268 | # Sort the paths to make tests easier |
|---|
| 269 | self.current['entries'].sort(lambda a,b: cmp(a.name, b.name)) |
|---|
| 270 | |
|---|
| 271 | # Eliminate "useless" entries: SVN does not have atomic |
|---|
| 272 | # renames, but rather uses a ADD+RM duo. |
|---|
| 273 | # |
|---|
| 274 | # So cycle over all entries of this patch, discarding |
|---|
| 275 | # the deletion of files that were actually renamed, and |
|---|
| 276 | # at the same time change related entry from ADDED to |
|---|
| 277 | # RENAMED. |
|---|
| 278 | |
|---|
| 279 | # When copying a directory from another location in the |
|---|
| 280 | # repository (outside the tracked tree), SVN will report files |
|---|
| 281 | # below this dir that are not being committed as being |
|---|
| 282 | # removed. |
|---|
| 283 | |
|---|
| 284 | # We thus need to change the action_kind for all entries |
|---|
| 285 | # that are below a dir that was "copyfrom" from a path |
|---|
| 286 | # outside of this module: |
|---|
| 287 | # D -> Remove entry completely (it's not going to be in here) |
|---|
| 288 | # (M,A,R) -> A |
|---|
| 289 | |
|---|
| 290 | mv_or_cp = {} |
|---|
| 291 | for e in self.current['entries']: |
|---|
| 292 | if e.action_kind == e.ADDED and e.old_name is not None: |
|---|
| 293 | mv_or_cp[e.old_name] = e |
|---|
| 294 | |
|---|
| 295 | def parent_was_copied(n): |
|---|
| 296 | for p in self.copies: |
|---|
| 297 | if n.startswith(p+'/'): |
|---|
| 298 | return True |
|---|
| 299 | return False |
|---|
| 300 | |
|---|
| 301 | entries = [] |
|---|
| 302 | for e in self.current['entries']: |
|---|
| 303 | if e.action_kind==e.DELETED and mv_or_cp.has_key(e.name): |
|---|
| 304 | mv_or_cp[e.name].action_kind = e.RENAMED |
|---|
| 305 | elif e.action_kind=='R': |
|---|
| 306 | # In svn parlance, 'R' means Replaced: a typical |
|---|
| 307 | # scenario is |
|---|
| 308 | # $ svn mv a.txt b.txt |
|---|
| 309 | # $ touch a.txt |
|---|
| 310 | # $ svn add a.txt |
|---|
| 311 | if mv_or_cp.has_key(e.name): |
|---|
| 312 | mv_or_cp[e.name].action_kind = e.RENAMED |
|---|
| 313 | e.action_kind = e.ADDED |
|---|
| 314 | entries.append(e) |
|---|
| 315 | elif parent_was_copied(e.name): |
|---|
| 316 | if e.action_kind != e.DELETED: |
|---|
| 317 | e.action_kind = e.ADDED |
|---|
| 318 | entries.append(e) |
|---|
| 319 | else: |
|---|
| 320 | entries.append(e) |
|---|
| 321 | |
|---|
| 322 | svndate = self.current['date'] |
|---|
| 323 | # 2004-04-16T17:12:48.000000Z |
|---|
| 324 | y,m,d = map(int, svndate[:10].split('-')) |
|---|
| 325 | hh,mm,ss = map(int, svndate[11:19].split(':')) |
|---|
| 326 | ms = int(svndate[20:-1]) |
|---|
| 327 | timestamp = datetime(y, m, d, hh, mm, ss, ms, UTC) |
|---|
| 328 | |
|---|
| 329 | changeset = Changeset(self.current['revision'], |
|---|
| 330 | timestamp, |
|---|
| 331 | self.current.get('author'), |
|---|
| 332 | self.current['msg'], |
|---|
| 333 | entries) |
|---|
| 334 | self.changesets.append(changeset) |
|---|
| 335 | self.current = None |
|---|
| 336 | elif name in ['author', 'date', 'msg']: |
|---|
| 337 | self.current[name] = ''.join(self.current_field) |
|---|
| 338 | elif name == 'path': |
|---|
| 339 | path = ''.join(self.current_field) |
|---|
| 340 | entrypath = get_entry_from_path(path) |
|---|
| 341 | if entrypath: |
|---|
| 342 | entry = ChangesetEntry(entrypath) |
|---|
| 343 | |
|---|
| 344 | if type(self.current_path_action) == type( () ): |
|---|
| 345 | self.copies.append(entry.name) |
|---|
| 346 | old = get_entry_from_path(self.current_path_action[1]) |
|---|
| 347 | if old: |
|---|
| 348 | entry.action_kind = self.ACTIONSMAP[self.current_path_action[0]] |
|---|
| 349 | entry.old_name = old |
|---|
| 350 | self.renamed[entry.old_name] = True |
|---|
| 351 | else: |
|---|
| 352 | entry.action_kind = entry.ADDED |
|---|
| 353 | else: |
|---|
| 354 | entry.action_kind = self.ACTIONSMAP[self.current_path_action] |
|---|
| 355 | |
|---|
| 356 | self.current['entries'].append(entry) |
|---|
| 357 | |
|---|
| 358 | def characters(self, data): |
|---|
| 359 | self.current_field.append(data) |
|---|
| 360 | |
|---|
| 361 | parser = make_parser() |
|---|
| 362 | handler = SvnXMLLogHandler() |
|---|
| 363 | parser.setContentHandler(handler) |
|---|
| 364 | parser.setErrorHandler(ErrorHandler()) |
|---|
| 365 | |
|---|
| 366 | chunk = log.read(chunksize) |
|---|
| 367 | while chunk: |
|---|
| 368 | parser.feed(chunk) |
|---|
| 369 | for cs in handler.changesets: |
|---|
| 370 | yield cs |
|---|
| 371 | handler.changesets = [] |
|---|
| 372 | chunk = log.read(chunksize) |
|---|
| 373 | parser.close() |
|---|
| 374 | for cs in handler.changesets: |
|---|
| 375 | yield cs |
|---|
| 376 | |
|---|
| 377 | |
|---|
| 378 | class SvnWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir): |
|---|
| 379 | |
|---|
| 380 | ## UpdatableSourceWorkingDir |
|---|
| 381 | |
|---|
| 382 | def _getUpstreamChangesets(self, sincerev=None): |
|---|
| 383 | if sincerev: |
|---|
| 384 | sincerev = int(sincerev) |
|---|
| 385 | else: |
|---|
| 386 | sincerev = 0 |
|---|
| 387 | |
|---|
| 388 | cmd = self.repository.command("log", "--verbose", "--xml", |
|---|
| 389 | "--revision", "%d:HEAD" % (sincerev+1)) |
|---|
| 390 | svnlog = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 391 | log = svnlog.execute('.', stdout=PIPE, TZ='UTC0')[0] |
|---|
| 392 | |
|---|
| 393 | if svnlog.exit_status: |
|---|
| 394 | return [] |
|---|
| 395 | |
|---|
| 396 | if self.repository.filter_badchars: |
|---|
| 397 | from string import maketrans |
|---|
| 398 | from cStringIO import StringIO |
|---|
| 399 | |
|---|
| 400 | # Apparently some (SVN repo contains)/(SVN server dumps) some |
|---|
| 401 | # characters that are illegal in an XML stream. This was the case |
|---|
| 402 | # with Twisted Matrix master repository. To be safe, we replace |
|---|
| 403 | # all of them with a question mark. |
|---|
| 404 | |
|---|
| 405 | if isinstance(self.repository.filter_badchars, basestring): |
|---|
| 406 | allbadchars = self.repository.filter_badchars |
|---|
| 407 | else: |
|---|
| 408 | allbadchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09" \ |
|---|
| 409 | "\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15" \ |
|---|
| 410 | "\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7f" |
|---|
| 411 | |
|---|
| 412 | tt = maketrans(allbadchars, "?"*len(allbadchars)) |
|---|
| 413 | log = StringIO(log.read().translate(tt)) |
|---|
| 414 | |
|---|
| 415 | return changesets_from_svnlog(log, self.repository) |
|---|
| 416 | |
|---|
| 417 | def _applyChangeset(self, changeset): |
|---|
| 418 | from time import sleep |
|---|
| 419 | |
|---|
| 420 | cmd = self.repository.command("update") |
|---|
| 421 | if self.repository.ignore_externals: |
|---|
| 422 | cmd.append("--ignore-externals") |
|---|
| 423 | cmd.extend(["--revision", changeset.revision]) |
|---|
| 424 | svnup = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 425 | |
|---|
| 426 | retry = 0 |
|---|
| 427 | while True: |
|---|
| 428 | out, err = svnup.execute(".", stdout=PIPE, stderr=PIPE) |
|---|
| 429 | |
|---|
| 430 | if svnup.exit_status == 1: |
|---|
| 431 | retry += 1 |
|---|
| 432 | if retry>3: |
|---|
| 433 | break |
|---|
| 434 | delay = 2**retry |
|---|
| 435 | self.log.error("%s returned status %s saying\n%s", |
|---|
| 436 | str(svnup), svnup.exit_status, err.read()) |
|---|
| 437 | self.log.warning("Retrying in %d seconds...", delay) |
|---|
| 438 | sleep(delay) |
|---|
| 439 | else: |
|---|
| 440 | break |
|---|
| 441 | |
|---|
| 442 | if svnup.exit_status: |
|---|
| 443 | raise ChangesetApplicationFailure( |
|---|
| 444 | "%s returned status %s saying\n%s" % (str(svnup), |
|---|
| 445 | svnup.exit_status, |
|---|
| 446 | err.read())) |
|---|
| 447 | |
|---|
| 448 | self.log.debug("%s updated to %s", |
|---|
| 449 | ','.join([e.name for e in changeset.entries]), |
|---|
| 450 | changeset.revision) |
|---|
| 451 | |
|---|
| 452 | result = [] |
|---|
| 453 | for line in out: |
|---|
| 454 | if len(line)>2 and line[0] == 'C' and line[1] == ' ': |
|---|
| 455 | self.log.warning("Conflict after svn update: %r", line) |
|---|
| 456 | result.append(line[2:-1]) |
|---|
| 457 | |
|---|
| 458 | return result |
|---|
| 459 | |
|---|
| 460 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 461 | """ |
|---|
| 462 | Concretely do the checkout of the upstream revision. |
|---|
| 463 | """ |
|---|
| 464 | |
|---|
| 465 | from os.path import join, exists |
|---|
| 466 | |
|---|
| 467 | # Verify that the we have the root of the repository: do that |
|---|
| 468 | # iterating an "svn ls" over the hierarchy until one fails |
|---|
| 469 | |
|---|
| 470 | lastok = self.repository.repository |
|---|
| 471 | if not self.repository.trust_root: |
|---|
| 472 | cmd = self.repository.command("ls") |
|---|
| 473 | svnls = ExternalCommand(command=cmd) |
|---|
| 474 | |
|---|
| 475 | # First verify that we have a valid repository |
|---|
| 476 | svnls.execute(self.repository.repository) |
|---|
| 477 | if svnls.exit_status: |
|---|
| 478 | lastok = None |
|---|
| 479 | else: |
|---|
| 480 | # Then verify it really points to the root of the |
|---|
| 481 | # repository: this is needed because later the svn log |
|---|
| 482 | # parser needs to know the "offset". |
|---|
| 483 | |
|---|
| 484 | reporoot = lastok[:lastok.rfind('/')] |
|---|
| 485 | |
|---|
| 486 | # Even if it would be enough asserting that the uplevel |
|---|
| 487 | # directory is not a repository, find the real root to |
|---|
| 488 | # suggest it in the exception. But don't go too far, that |
|---|
| 489 | # is, stop when you hit schema://... |
|---|
| 490 | while '//' in reporoot: |
|---|
| 491 | svnls.execute(reporoot) |
|---|
| 492 | if svnls.exit_status: |
|---|
| 493 | break |
|---|
| 494 | lastok = reporoot |
|---|
| 495 | reporoot = reporoot[:reporoot.rfind('/')] |
|---|
| 496 | |
|---|
| 497 | if lastok is None: |
|---|
| 498 | raise ConfigurationError("%r is not the root of a svn repository." % |
|---|
| 499 | self.repository.repository) |
|---|
| 500 | elif lastok <> self.repository.repository: |
|---|
| 501 | module = self.repository.repository[len(lastok):] |
|---|
| 502 | module += self.repository.module |
|---|
| 503 | raise ConfigurationError("Non-root svn repository %r. " |
|---|
| 504 | "Please specify that as 'repository=%s' " |
|---|
| 505 | "and 'module=%s'." % |
|---|
| 506 | (self.repository.repository, |
|---|
| 507 | lastok, module.rstrip('/'))) |
|---|
| 508 | |
|---|
| 509 | if revision == 'INITIAL': |
|---|
| 510 | initial = True |
|---|
| 511 | cmd = self.repository.command("log", "--verbose", "--xml", |
|---|
| 512 | "--stop-on-copy", |
|---|
| 513 | "--revision", "1:HEAD") |
|---|
| 514 | if self.repository.use_limit: |
|---|
| 515 | cmd.extend(["--limit", "1"]) |
|---|
| 516 | svnlog = ExternalCommand(command=cmd) |
|---|
| 517 | out, err = svnlog.execute("%s%s" % (self.repository.repository, |
|---|
| 518 | self.repository.module), |
|---|
| 519 | stdout=PIPE, stderr=PIPE) |
|---|
| 520 | |
|---|
| 521 | if svnlog.exit_status: |
|---|
| 522 | raise TargetInitializationFailure( |
|---|
| 523 | "%s returned status %d saying\n%s" % |
|---|
| 524 | (str(svnlog), svnlog.exit_status, err.read())) |
|---|
| 525 | |
|---|
| 526 | csets = changesets_from_svnlog(out, self.repository) |
|---|
| 527 | last = csets.next() |
|---|
| 528 | revision = last.revision |
|---|
| 529 | else: |
|---|
| 530 | initial = False |
|---|
| 531 | |
|---|
| 532 | if not exists(join(self.repository.basedir, '.svn')): |
|---|
| 533 | self.log.debug("Checking out a working copy") |
|---|
| 534 | |
|---|
| 535 | cmd = self.repository.command("co", "--quiet") |
|---|
| 536 | if self.repository.ignore_externals: |
|---|
| 537 | cmd.append("--ignore-externals") |
|---|
| 538 | cmd.extend(["--revision", revision]) |
|---|
| 539 | svnco = ExternalCommand(command=cmd) |
|---|
| 540 | |
|---|
| 541 | out, err = svnco.execute("%s%s@%s" % (self.repository.repository, |
|---|
| 542 | self.repository.module, |
|---|
| 543 | revision), |
|---|
| 544 | self.repository.basedir, stdout=PIPE, stderr=PIPE) |
|---|
| 545 | if svnco.exit_status: |
|---|
| 546 | raise TargetInitializationFailure( |
|---|
| 547 | "%s returned status %s saying\n%s" % (str(svnco), |
|---|
| 548 | svnco.exit_status, |
|---|
| 549 | err.read())) |
|---|
| 550 | else: |
|---|
| 551 | self.log.debug("%r already exists, assuming it's " |
|---|
| 552 | "a svn working dir", self.repository.basedir) |
|---|
| 553 | |
|---|
| 554 | if not initial: |
|---|
| 555 | if revision=='HEAD': |
|---|
| 556 | revision = 'COMMITTED' |
|---|
| 557 | cmd = self.repository.command("log", "--verbose", "--xml", |
|---|
| 558 | "--revision", revision) |
|---|
| 559 | svnlog = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 560 | out, err = svnlog.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 561 | |
|---|
| 562 | if svnlog.exit_status: |
|---|
| 563 | raise TargetInitializationFailure( |
|---|
| 564 | "%s returned status %d saying\n%s" % |
|---|
| 565 | (str(svnlog), svnlog.exit_status, err.read())) |
|---|
| 566 | |
|---|
| 567 | csets = changesets_from_svnlog(out, self.repository) |
|---|
| 568 | last = csets.next() |
|---|
| 569 | |
|---|
| 570 | self.log.debug("Working copy up to svn revision %s", last.revision) |
|---|
| 571 | |
|---|
| 572 | return last |
|---|
| 573 | |
|---|
| 574 | ## SynchronizableTargetWorkingDir |
|---|
| 575 | |
|---|
| 576 | def _addPathnames(self, names): |
|---|
| 577 | """ |
|---|
| 578 | Add some new filesystem objects. |
|---|
| 579 | """ |
|---|
| 580 | |
|---|
| 581 | cmd = self.repository.command("add", "--quiet", "--no-auto-props", |
|---|
| 582 | "--non-recursive") |
|---|
| 583 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(names) |
|---|
| 584 | |
|---|
| 585 | def _propsetRevision(self, out, command, date, author): |
|---|
| 586 | |
|---|
| 587 | from re import search |
|---|
| 588 | |
|---|
| 589 | encode = self.repository.encode |
|---|
| 590 | |
|---|
| 591 | line = out.readline() |
|---|
| 592 | if not line: |
|---|
| 593 | # svn did not find anything to commit |
|---|
| 594 | self.log.warning('svn did not find anything to commit') |
|---|
| 595 | return |
|---|
| 596 | |
|---|
| 597 | # Assume svn output the revision number in the last output line |
|---|
| 598 | while line: |
|---|
| 599 | lastline = line |
|---|
| 600 | line = out.readline() |
|---|
| 601 | revno = search('\d+', lastline) |
|---|
| 602 | if revno is None: |
|---|
| 603 | out.seek(0) |
|---|
| 604 | raise ChangesetApplicationFailure("%s wrote unrecognizable " |
|---|
| 605 | "revision number:\n%s" % |
|---|
| 606 | (str(command), out.read())) |
|---|
| 607 | |
|---|
| 608 | revision = revno.group(0) |
|---|
| 609 | |
|---|
| 610 | if self.repository.use_propset: |
|---|
| 611 | |
|---|
| 612 | cmd = self.repository.command("propset", "%(propname)s", |
|---|
| 613 | "--quiet", "--revprop", |
|---|
| 614 | "--revision", revision) |
|---|
| 615 | pset = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 616 | if self.repository.propset_date: |
|---|
| 617 | date = date.astimezone(UTC).replace(microsecond=0, tzinfo=None) |
|---|
| 618 | pset.execute(date.isoformat()+".000000Z", propname='svn:date') |
|---|
| 619 | pset.execute(encode(author), propname='svn:author') |
|---|
| 620 | |
|---|
| 621 | return revision |
|---|
| 622 | |
|---|
| 623 | def _tag(self, tag, date, author): |
|---|
| 624 | """ |
|---|
| 625 | TAG current revision. |
|---|
| 626 | """ |
|---|
| 627 | if self.repository.setupTagsDirectory(): |
|---|
| 628 | src = self.repository.repository + self.repository.module |
|---|
| 629 | dest = self.repository.repository + self.repository.tags_path \ |
|---|
| 630 | + '/' + tag.replace('/', '_') |
|---|
| 631 | |
|---|
| 632 | cmd = self.repository.command("copy", src, dest, "-m", tag) |
|---|
| 633 | svntag = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 634 | out, err = svntag.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 635 | |
|---|
| 636 | if svntag.exit_status: |
|---|
| 637 | raise ChangesetApplicationFailure("%s returned status %d saying\n%s" |
|---|
| 638 | % (str(svntag), |
|---|
| 639 | svntag.exit_status, |
|---|
| 640 | err.read())) |
|---|
| 641 | |
|---|
| 642 | self._propsetRevision(out, svntag, date, author) |
|---|
| 643 | |
|---|
| 644 | |
|---|
| 645 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 646 | tags = [], isinitialcommit = False): |
|---|
| 647 | """ |
|---|
| 648 | Commit the changeset. |
|---|
| 649 | """ |
|---|
| 650 | |
|---|
| 651 | encode = self.repository.encode |
|---|
| 652 | |
|---|
| 653 | logmessage = [] |
|---|
| 654 | if patchname: |
|---|
| 655 | logmessage.append(patchname) |
|---|
| 656 | if changelog: |
|---|
| 657 | logmessage.append(changelog) |
|---|
| 658 | |
|---|
| 659 | # If we cannot use propset, fall back to old behaviour of |
|---|
| 660 | # appending these info to the changelog |
|---|
| 661 | |
|---|
| 662 | if not self.repository.use_propset: |
|---|
| 663 | logmessage.append('') |
|---|
| 664 | logmessage.append('Original author: %s' % encode(author)) |
|---|
| 665 | logmessage.append('Date: %s' % date) |
|---|
| 666 | elif not self.repository.propset_date: |
|---|
| 667 | logmessage.append('') |
|---|
| 668 | logmessage.append('Date: %s' % date) |
|---|
| 669 | |
|---|
| 670 | rontf = ReopenableNamedTemporaryFile('svn', 'tailor') |
|---|
| 671 | log = open(rontf.name, "w") |
|---|
| 672 | log.write(encode('\n'.join(logmessage))) |
|---|
| 673 | log.close() |
|---|
| 674 | |
|---|
| 675 | cmd = self.repository.command("commit", "--file", rontf.name) |
|---|
| 676 | commit = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 677 | |
|---|
| 678 | if not entries or self.repository.commit_all_files: |
|---|
| 679 | entries = ['.'] |
|---|
| 680 | |
|---|
| 681 | out, err = commit.execute(entries, stdout=PIPE, stderr=PIPE) |
|---|
| 682 | |
|---|
| 683 | if commit.exit_status: |
|---|
| 684 | raise ChangesetApplicationFailure("%s returned status %d saying\n%s" |
|---|
| 685 | % (str(commit), |
|---|
| 686 | commit.exit_status, |
|---|
| 687 | err.read())) |
|---|
| 688 | |
|---|
| 689 | revision = self._propsetRevision(out, commit, date, author) |
|---|
| 690 | if not revision: |
|---|
| 691 | # svn did not find anything to commit |
|---|
| 692 | return |
|---|
| 693 | |
|---|
| 694 | cmd = self.repository.command("update", "--quiet") |
|---|
| 695 | if self.repository.ignore_externals: |
|---|
| 696 | cmd.append("--ignore-externals") |
|---|
| 697 | cmd.extend(["--revision", revision]) |
|---|
| 698 | |
|---|
| 699 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute() |
|---|
| 700 | |
|---|
| 701 | def _removePathnames(self, names): |
|---|
| 702 | """ |
|---|
| 703 | Remove some filesystem objects. |
|---|
| 704 | """ |
|---|
| 705 | |
|---|
| 706 | cmd = self.repository.command("remove", "--quiet", "--force") |
|---|
| 707 | remove = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 708 | remove.execute(names) |
|---|
| 709 | |
|---|
| 710 | def _renamePathname(self, oldname, newname): |
|---|
| 711 | """ |
|---|
| 712 | Rename a filesystem object. |
|---|
| 713 | """ |
|---|
| 714 | |
|---|
| 715 | from os import rename |
|---|
| 716 | from os.path import join, exists |
|---|
| 717 | |
|---|
| 718 | # --force in case the file has been changed and moved in one revision |
|---|
| 719 | cmd = self.repository.command("mv", "--quiet", "--force") |
|---|
| 720 | # Subversion does not seem to allow |
|---|
| 721 | # $ mv a.txt b.txt |
|---|
| 722 | # $ svn mv a.txt b.txt |
|---|
| 723 | # Here we are in this situation, since upstream VCS already |
|---|
| 724 | # moved the item. |
|---|
| 725 | # It may be better to let subversion do the move itself. For one thing, |
|---|
| 726 | # svn's cp+rm is different from rm+add (cp preserves history). |
|---|
| 727 | unmoved = False |
|---|
| 728 | oldpath = join(self.repository.basedir, oldname) |
|---|
| 729 | newpath = join(self.repository.basedir, newname) |
|---|
| 730 | if not exists(oldpath): |
|---|
| 731 | try: |
|---|
| 732 | rename(newpath, oldpath) |
|---|
| 733 | except OSError: |
|---|
| 734 | self.log.critical('Cannot rename %r back to %r', |
|---|
| 735 | newpath, oldpath) |
|---|
| 736 | raise |
|---|
| 737 | unmoved = True |
|---|
| 738 | move = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 739 | out, err = move.execute(oldname, newname, stdout=PIPE, stderr=PIPE) |
|---|
| 740 | if move.exit_status: |
|---|
| 741 | if unmoved: |
|---|
| 742 | rename(oldpath, newpath) |
|---|
| 743 | raise ChangesetApplicationFailure("%s returned status %d saying\n%s" |
|---|
| 744 | % (str(move), move.exit_status, |
|---|
| 745 | err.read())) |
|---|
| 746 | |
|---|
| 747 | def _prepareTargetRepository(self): |
|---|
| 748 | """ |
|---|
| 749 | Check for target repository existence, eventually create it. |
|---|
| 750 | """ |
|---|
| 751 | |
|---|
| 752 | if not self.repository.repository: |
|---|
| 753 | return |
|---|
| 754 | |
|---|
| 755 | self.repository.create() |
|---|
| 756 | |
|---|
| 757 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 758 | """ |
|---|
| 759 | Checkout a working copy of the target SVN repository. |
|---|
| 760 | """ |
|---|
| 761 | |
|---|
| 762 | from os.path import join, exists |
|---|
| 763 | |
|---|
| 764 | if not self.repository.repository or exists(join(self.repository.basedir, '.svn')): |
|---|
| 765 | return |
|---|
| 766 | |
|---|
| 767 | cmd = self.repository.command("co", "--quiet") |
|---|
| 768 | if self.repository.ignore_externals: |
|---|
| 769 | cmd.append("--ignore-externals") |
|---|
| 770 | |
|---|
| 771 | svnco = ExternalCommand(command=cmd) |
|---|
| 772 | svnco.execute("%s%s" % (self.repository.repository, |
|---|
| 773 | self.repository.module), self.repository.basedir) |
|---|
| 774 | |
|---|
| 775 | def _initializeWorkingDir(self): |
|---|
| 776 | """ |
|---|
| 777 | Add the given directory to an already existing svn working tree. |
|---|
| 778 | """ |
|---|
| 779 | |
|---|
| 780 | from os.path import exists, join |
|---|
| 781 | |
|---|
| 782 | if not exists(join(self.repository.basedir, '.svn')): |
|---|
| 783 | raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.repository.basedir) |
|---|
| 784 | |
|---|
| 785 | SynchronizableTargetWorkingDir._initializeWorkingDir(self) |
|---|