source: tailor/vcpx/repository/svn.py @ 1391

Revision 1391, 31.5 KB checked in by henry.ne@…, 6 years ago (diff)

svn-target-branches.patch
Subversion: Supports branches as target.

Add support for 'branches' on subversion targets. Modules, that starts
with 'branches/...' will be export under "branches".
Add 'svn-branches' to config, can change the path.

Hint: The 'trunk' is non special in tailor, simple set 'trunk' as module.

Full example from Monotone source to Subversion:

[DEFAULT]
start-revision = INITIAL
root-directory = /tmp/rootdir-Monotone
source-repository = $HOME/Monotone-database-file/monotone.mtn
target-repository =  file:///tmp/rootdir-Monotone/svn-repository-monotone
use-propset = True

# Projects
[net.venge.monotone.cvssync]
source = monotone:net.venge.monotone.cvssync
target = svn:net.venge.monotone.cvssync

[net.venge.monotone.de]
source = monotone:net.venge.monotone.de
target = svn:net.venge.monotone.de

[net.venge.monotone.svn_import]
source = monotone:net.venge.monotone.svn_import
target = svn:net.venge.monotone.svn_import

[net.venge.monotone]
source = monotone:net.venge.monotone
target = svn:net.venge.monotone

# Sources
[monotone:net.venge.monotone.cvssync]
module = net.venge.monotone.cvssync
subdir = mtnside-net.venge.monotone.cvssync

[monotone:net.venge.monotone.de]
module = net.venge.monotone.de
subdir = mtnside-net.venge.monotone.de

[monotone:net.venge.monotone.svn_import]
module = net.venge.monotone.svn_import
subdir = mtnside-net.venge.monotone.svn_import

[monotone:net.venge.monotone]
module = net.venge.monotone
subdir = mtnside-net.venge.monotone

# Targets
[svn:net.venge.monotone.cvssync]
module = branches/net.venge.monotone.cvssync
subdir = svnside-net.venge.monotone.cvssync

[svn:net.venge.monotone.de]
module = branches/net.venge.monotone.de
subdir = svnside-net.venge.monotone.de

[svn:net.venge.monotone.svn_import]
module = branches/net.venge.monotone.svn_import
subdir = svnside-net.venge.monotone.svn_import

[svn:net.venge.monotone]
module = trunk
subdir = svnside-net.venge.monotone

Line 
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"""
9This module contains supporting classes for Subversion.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from vcpx.repository import Repository
15from vcpx.shwrap import ExternalCommand, PIPE, ReopenableNamedTemporaryFile
16from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure
17from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure
18from vcpx.config import ConfigurationError
19from vcpx.tzinfo import UTC
20
21
22class 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
194def 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
378class 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)
Note: See TracBrowser for help on using the repository browser.