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