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

Revision 1620, 36.6 KB checked in by lele@…, 5 years ago (diff)

Revise logged information
Show the author and the timestamp of the upstream changeset, and strip
the trailing waitspace from the changelog.

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.changes import ChangesetEntry
15from vcpx.config import ConfigurationError
16from vcpx.repository import Repository
17from vcpx.shwrap import ExternalCommand, PIPE, STDOUT, ReopenableNamedTemporaryFile
18from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure
19from vcpx.target import SynchronizableTargetWorkingDir, TargetInitializationFailure, \
20                        PostCommitCheckFailure
21from vcpx.tzinfo import UTC
22
23
24class 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
202def 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,R) -> A
297
298                mv_or_cp = {}
299                for e in self.current['entries']:
300                    if e.action_kind == e.ADDED and e.old_name is not None:
301                        mv_or_cp[e.old_name] = e
302
303                def parent_was_copied(n):
304                    for p in self.copies:
305                        if n.startswith(p+'/'):
306                            return True
307                    return False
308
309                # Find renames from deleted directories:
310                # $ svn mv dir/a.txt a.txt
311                # $ svn del dir
312                def check_renames_from_dir(name):
313                    for e in mv_or_cp.values():
314                        if e.old_name.startswith(name+'/'):
315                            e.action_kind = e.RENAMED
316
317                entries = []
318                entries2 = []
319                for e in self.current['entries']:
320                    if e.action_kind==e.DELETED:
321                        if mv_or_cp.has_key(e.name):
322                            mv_or_cp[e.name].action_kind = e.RENAMED
323                        else:
324                            check_renames_from_dir(e.name)
325                            entries2.append(e)
326                    elif e.action_kind=='R':
327                        # In svn parlance, 'R' means Replaced: a typical
328                        # scenario is
329                        #   $ svn mv a.txt b.txt
330                        #   $ touch a.txt
331                        #   $ svn add a.txt
332                        if mv_or_cp.has_key(e.name):
333                            mv_or_cp[e.name].action_kind = e.RENAMED
334                        else:
335                            check_renames_from_dir(e.name)
336                        e.action_kind = e.ADDED
337                        entries2.append(e)
338                    elif parent_was_copied(e.name):
339                        if e.action_kind != e.DELETED:
340                            e.action_kind = e.ADDED
341                            entries.append(e)
342                    else:
343                        entries.append(e)
344
345                # Changes sort: first MODIFY|ADD|RENAME, than REPLACE|DELETE
346                for e in entries2:
347                    entries.append(e)
348
349                svndate = self.current['date']
350                # 2004-04-16T17:12:48.000000Z
351                y,m,d = map(int, svndate[:10].split('-'))
352                hh,mm,ss = map(int, svndate[11:19].split(':'))
353                ms = int(svndate[20:-1])
354                timestamp = datetime(y, m, d, hh, mm, ss, ms, UTC)
355
356                changeset = Changeset(self.current['revision'],
357                                      timestamp,
358                                      self.current.get('author'),
359                                      self.current['msg'],
360                                      entries)
361                self.changesets.append(changeset)
362                self.current = None
363            elif name in ['author', 'date', 'msg']:
364                self.current[name] = ''.join(self.current_field)
365            elif name == 'path':
366                path = ''.join(self.current_field)
367                entrypath = get_entry_from_path(path)
368                if entrypath:
369                    entry = ChangesetEntry(entrypath)
370                    if type(self.current_path_action) == type( () ):
371                        self.copies.append(entry.name)
372                        old = get_entry_from_path(self.current_path_action[1])
373                        if old:
374                            entry.action_kind = self.ACTIONSMAP[self.current_path_action[0]]
375                            entry.old_name = old
376                            self.renamed[entry.old_name] = True
377                        else:
378                            entry.action_kind = entry.ADDED
379                    else:
380                        entry.action_kind = self.ACTIONSMAP[self.current_path_action]
381
382                    self.current['entries'].append(entry)
383
384        def characters(self, data):
385            self.current_field.append(data)
386
387    parser = make_parser()
388    handler = SvnXMLLogHandler()
389    parser.setContentHandler(handler)
390    parser.setErrorHandler(ErrorHandler())
391
392    chunk = log.read(chunksize)
393    while chunk:
394        parser.feed(chunk)
395        for cs in handler.changesets:
396            yield cs
397        handler.changesets = []
398        chunk = log.read(chunksize)
399    parser.close()
400    for cs in handler.changesets:
401        yield cs
402
403
404class SvnWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
405
406    ## UpdatableSourceWorkingDir
407
408    def _getUpstreamChangesets(self, sincerev=None):
409        if sincerev:
410            sincerev = int(sincerev)
411        else:
412            sincerev = 0
413
414        cmd = self.repository.command("log", "--verbose", "--xml", "--non-interactive",
415                                      "--revision", "%d:HEAD" % (sincerev+1))
416        svnlog = ExternalCommand(cwd=self.repository.basedir, command=cmd)
417        log = svnlog.execute('.', stdout=PIPE, TZ='UTC0')[0]
418
419        if svnlog.exit_status:
420            return []
421
422        if self.repository.filter_badchars:
423            from string import maketrans
424            from cStringIO import StringIO
425
426            # Apparently some (SVN repo contains)/(SVN server dumps) some
427            # characters that are illegal in an XML stream. This was the case
428            # with Twisted Matrix master repository. To be safe, we replace
429            # all of them with a question mark.
430
431            if isinstance(self.repository.filter_badchars, basestring):
432                allbadchars = self.repository.filter_badchars
433            else:
434                allbadchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09" \
435                              "\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15" \
436                              "\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7f"
437
438            tt = maketrans(allbadchars, "?"*len(allbadchars))
439            log = StringIO(log.read().translate(tt))
440
441        return changesets_from_svnlog(log, self.repository)
442
443    def _applyChangeset(self, changeset):
444        from os import walk
445        from os.path import join, isdir
446        from time import sleep
447
448        # Complete changeset information, determining the is_directory
449        # flag of the removed entries, before updating to the given revision
450        for entry in changeset.entries:
451            if entry.action_kind == entry.DELETED:
452                entry.is_directory = isdir(join(self.repository.basedir, entry.name))
453
454        cmd = self.repository.command("update")
455        if self.repository.ignore_externals:
456            cmd.append("--ignore-externals")
457        cmd.extend(["--revision", changeset.revision])
458        svnup = ExternalCommand(cwd=self.repository.basedir, command=cmd)
459
460        retry = 0
461        while True:
462            out, err = svnup.execute(".", stdout=PIPE, stderr=PIPE)
463
464            if svnup.exit_status == 1:
465                retry += 1
466                if retry>3:
467                    break
468                delay = 2**retry
469                self.log.error("%s returned status %s saying\n%s",
470                               str(svnup), svnup.exit_status, err.read())
471                self.log.warning("Retrying in %d seconds...", delay)
472                sleep(delay)
473            else:
474                break
475
476        if svnup.exit_status:
477            raise ChangesetApplicationFailure(
478                "%s returned status %s saying\n%s" % (str(svnup),
479                                                     svnup.exit_status,
480                                                     err.read()))
481
482        self.log.debug("%s updated to %s",
483                       ','.join([e.name for e in changeset.entries]),
484                       changeset.revision)
485
486        # Complete changeset information, determining the is_directory
487        # flag of the added entries
488        implicitly_added_entries = []
489        known_added_entries = set()
490        for entry in changeset.entries:
491            if entry.action_kind == entry.ADDED:
492                known_added_entries.add(entry.name)
493                fullname = join(self.repository.basedir, entry.name)
494                entry.is_directory = isdir(fullname)
495                # If it is a directory, extend the entries of the
496                # changeset with all its contents, if not already there.
497                if entry.is_directory:
498                    for root, subdirs, files in walk(fullname):
499                        if '.svn' in subdirs:
500                            subdirs.remove('.svn')
501                        for f in files:
502                            name = join(root, f)[len(self.repository.basedir)+1:]
503                            newe = ChangesetEntry(name)
504                            newe.action_kind = newe.ADDED
505                            implicitly_added_entries.append(newe)
506                        for d in subdirs:
507                            name = join(root, d)[len(self.repository.basedir)+1:]
508                            newe = ChangesetEntry(name)
509                            newe.action_kind = newe.ADDED
510                            newe.is_directory = True
511                            implicitly_added_entries.append(newe)
512
513        for e in implicitly_added_entries:
514            if not e.name in known_added_entries:
515                changeset.entries.append(e)
516
517        result = []
518        for line in out:
519            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
520                self.log.warning("Conflict after svn update: %r", line)
521                result.append(line[2:-1])
522
523        return result
524
525    def _checkoutUpstreamRevision(self, revision):
526        """
527        Concretely do the checkout of the upstream revision.
528        """
529
530        from os.path import join, exists
531
532        # Verify that the we have the root of the repository: do that
533        # iterating an "svn ls" over the hierarchy until one fails
534
535        lastok = self.repository.repository
536        if not self.repository.trust_root:
537            # Use --non-interactive, so that it fails if credentials
538            # are needed.
539            cmd = self.repository.command("ls", "--non-interactive")
540            svnls = ExternalCommand(command=cmd)
541
542            # First verify that we have a valid repository
543            svnls.execute(self.repository.repository)
544            if svnls.exit_status:
545                lastok = None
546            else:
547                # Then verify it really points to the root of the
548                # repository: this is needed because later the svn log
549                # parser needs to know the "offset".
550
551                reporoot = lastok[:lastok.rfind('/')]
552
553                # Even if it would be enough asserting that the uplevel
554                # directory is not a repository, find the real root to
555                # suggest it in the exception.  But don't go too far, that
556                # is, stop when you hit schema://...
557                while '//' in reporoot:
558                    svnls.execute(reporoot)
559                    if svnls.exit_status:
560                        break
561                    lastok = reporoot
562                    reporoot = reporoot[:reporoot.rfind('/')]
563
564        if lastok is None:
565            raise ConfigurationError('%r is not the root of a svn repository. If '
566                                     'you are sure it is indeed, you may try setting '
567                                     'the option "trust-root" to "True".' %
568                                     self.repository.repository)
569        elif lastok <> self.repository.repository:
570            module = self.repository.repository[len(lastok):]
571            module += self.repository.module
572            raise ConfigurationError('Non-root svn repository %r. '
573                                     'Please specify that as "repository=%s" '
574                                     'and "module=%s".' %
575                                     (self.repository.repository,
576                                      lastok, module.rstrip('/')))
577
578        if revision == 'INITIAL':
579            initial = True
580            cmd = self.repository.command("log", "--verbose", "--xml",
581                                          "--non-interactive", "--stop-on-copy",
582                                          "--revision", "1:HEAD")
583            if self.repository.use_limit:
584                cmd.extend(["--limit", "1"])
585            svnlog = ExternalCommand(command=cmd)
586            out, err = svnlog.execute("%s%s" % (self.repository.repository,
587                                                self.repository.module),
588                                      stdout=PIPE, stderr=PIPE)
589
590            if svnlog.exit_status:
591                raise TargetInitializationFailure(
592                    "%s returned status %d saying\n%s" %
593                    (str(svnlog), svnlog.exit_status, err.read()))
594
595            csets = changesets_from_svnlog(out, self.repository)
596            last = csets.next()
597            revision = last.revision
598        else:
599            initial = False
600
601        if not exists(join(self.repository.basedir, self.repository.METADIR)):
602            self.log.debug("Checking out a working copy")
603
604            cmd = self.repository.command("co", "--quiet")
605            if self.repository.ignore_externals:
606                cmd.append("--ignore-externals")
607            cmd.extend(["--revision", revision])
608            svnco = ExternalCommand(command=cmd)
609
610            out, err = svnco.execute("%s%s@%s" % (self.repository.repository,
611                                                  self.repository.module,
612                                                  revision),
613                                     self.repository.basedir, stdout=PIPE, stderr=PIPE)
614            if svnco.exit_status:
615                raise TargetInitializationFailure(
616                    "%s returned status %s saying\n%s" % (str(svnco),
617                                                         svnco.exit_status,
618                                                         err.read()))
619        else:
620            self.log.debug("%r already exists, assuming it's "
621                           "a svn working dir", self.repository.basedir)
622
623        if not initial:
624            if revision=='HEAD':
625                revision = 'COMMITTED'
626            cmd = self.repository.command("log", "--verbose", "--xml",
627                                          "--non-interactive",
628                                          "--revision", revision)
629            svnlog = ExternalCommand(cwd=self.repository.basedir, command=cmd)
630            out, err = svnlog.execute(stdout=PIPE, stderr=PIPE)
631
632            if svnlog.exit_status:
633                raise TargetInitializationFailure(
634                    "%s returned status %d saying\n%s" %
635                    (str(svnlog), svnlog.exit_status, err.read()))
636
637            csets = changesets_from_svnlog(out, self.repository)
638            last = csets.next()
639
640        self.log.debug("Working copy up to svn revision %s", last.revision)
641
642        return last
643
644    ## SynchronizableTargetWorkingDir
645
646    def _addPathnames(self, names):
647        """
648        Add some new filesystem objects.
649        """
650
651        cmd = self.repository.command("add", "--quiet", "--no-auto-props",
652                                      "--non-recursive")
653        ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(names)
654
655    def _propsetRevision(self, out, command, date, author):
656
657        from re import search
658
659        encode = self.repository.encode
660
661        line = out.readline()
662        if not line:
663            # svn did not find anything to commit
664            self.log.warning('svn did not find anything to commit')
665            return
666
667        # Assume svn output the revision number in the last output line
668        while line:
669            lastline = line
670            line = out.readline()
671        revno = search('\d+', lastline)
672        if revno is None:
673            out.seek(0)
674            raise ChangesetApplicationFailure("%s wrote unrecognizable "
675                                              "revision number:\n%s" %
676                                              (str(command), out.read()))
677
678        revision = revno.group(0)
679
680        if self.repository.use_propset:
681
682            cmd = self.repository.command("propset", "%(propname)s",
683                                          "--quiet", "--revprop",
684                                          "--revision", revision)
685            pset = ExternalCommand(cwd=self.repository.basedir, command=cmd)
686            if self.repository.propset_date:
687                date = date.astimezone(UTC).replace(microsecond=0, tzinfo=None)
688                pset.execute(date.isoformat()+".000000Z", propname='svn:date')
689            pset.execute(encode(author), propname='svn:author')
690
691        return revision
692
693    def _tag(self, tag, date, author):
694        """
695        TAG current revision.
696        """
697        if self.repository.setupTagsDirectory():
698            src = self.repository.repository + self.repository.module
699            dest = self.repository.repository + self.repository.tags_path \
700                                              + '/' + tag.replace('/', '_')
701
702            cmd = self.repository.command("copy", src, dest, "-m", tag)
703            svntag = ExternalCommand(cwd=self.repository.basedir, command=cmd)
704            out, err = svntag.execute(stdout=PIPE, stderr=PIPE)
705
706            if svntag.exit_status:
707                raise ChangesetApplicationFailure("%s returned status %d saying\n%s"
708                                                  % (str(svntag),
709                                                     svntag.exit_status,
710                                                     err.read()))
711
712            self._propsetRevision(out, svntag, date, author)
713
714    def _commit(self, date, author, patchname, changelog=None, entries=None,
715                tags = [], isinitialcommit = False):
716        """
717        Commit the changeset.
718        """
719
720        encode = self.repository.encode
721
722        logmessage = []
723        if patchname:
724            logmessage.append(patchname)
725        if changelog:
726            logmessage.append(changelog)
727
728        # If we cannot use propset, fall back to old behaviour of
729        # appending these info to the changelog
730
731        if not self.repository.use_propset:
732            logmessage.append('')
733            logmessage.append('Original author: %s' % encode(author))
734            logmessage.append('Date: %s' % date)
735        elif not self.repository.propset_date:
736            logmessage.append('')
737            logmessage.append('Date: %s' % date)
738
739        rontf = ReopenableNamedTemporaryFile('svn', 'tailor')
740        log = open(rontf.name, "w")
741        log.write(encode('\n'.join(logmessage)))
742        log.close()
743
744        cmd = self.repository.command("commit", "--file", rontf.name)
745        commit = ExternalCommand(cwd=self.repository.basedir, command=cmd)
746
747        if not entries or self.repository.commit_all_files:
748            entries = ['.']
749
750        out, err = commit.execute(entries, stdout=PIPE, stderr=PIPE)
751
752        if commit.exit_status:
753            raise ChangesetApplicationFailure("%s returned status %d saying\n%s"
754                                              % (str(commit),
755                                                 commit.exit_status,
756                                                 err.read()))
757
758        revision = self._propsetRevision(out, commit, date, author)
759        if not revision:
760            # svn did not find anything to commit
761            return
762
763        cmd = self.repository.command("update", "--quiet")
764        if self.repository.ignore_externals:
765            cmd.append("--ignore-externals")
766        cmd.extend(["--revision", revision])
767
768        ExternalCommand(cwd=self.repository.basedir, command=cmd).execute()
769
770    def _postCommitCheck(self):
771        """
772        Assert that all the entries in the working dir are versioned.
773        """
774
775        cmd = self.repository.command("status")
776        whatsnew = ExternalCommand(cwd=self.repository.basedir, command=cmd)
777        output = whatsnew.execute(stdout=PIPE, stderr=STDOUT)[0]
778        unknown = [l for l in output.readlines() if l.startswith('?')]
779        if unknown:
780            raise PostCommitCheckFailure(
781                "Changes left in working dir after commit:\n%s" % ''.join(unknown))
782
783    def _removePathnames(self, names):
784        """
785        Remove some filesystem objects.
786        """
787
788        cmd = self.repository.command("remove", "--quiet", "--force")
789        remove = ExternalCommand(cwd=self.repository.basedir, command=cmd)
790        remove.execute(names)
791
792    def _renamePathname(self, oldname, newname):
793        """
794        Rename a filesystem object.
795        """
796
797        from os import rename
798        from os.path import join, exists, isdir
799        from time import sleep
800        from datetime import datetime
801
802        # --force in case the file has been changed and moved in one revision
803        cmd = self.repository.command("mv", "--quiet", "--force")
804        # Subversion does not seem to allow
805        #   $ mv a.txt b.txt
806        #   $ svn mv a.txt b.txt
807        # Here we are in this situation, since upstream VCS already
808        # moved the item.
809        # It may be better to let subversion do the move itself. For one thing,
810        # svn's cp+rm is different from rm+add (cp preserves history).
811        unmoved = False
812        oldpath = join(self.repository.basedir, oldname)
813        newpath = join(self.repository.basedir, newname)
814        if not exists(oldpath):
815            try:
816                rename(newpath, oldpath)
817            except OSError:
818                self.log.critical('Cannot rename %r back to %r',
819                                  newpath, oldpath)
820                raise
821            unmoved = True
822
823        # Ticket #135: Need a timediff between rsync and directory move
824        if isdir(oldpath):
825            now = datetime.now()
826            if hasattr(self, '_last_svn_move'):
827                last = self._last_svn_move
828            else:
829                last = now
830            if not (now-last).seconds:
831                sleep(1)
832            self._last_svn_move = now
833
834        move = ExternalCommand(cwd=self.repository.basedir, command=cmd)
835        out, err = move.execute(oldname, newname, stdout=PIPE, stderr=PIPE)
836        if move.exit_status:
837            if unmoved:
838                rename(oldpath, newpath)
839            raise ChangesetApplicationFailure("%s returned status %d saying\n%s"
840                                              % (str(move), move.exit_status,
841                                                 err.read()))
842
843    def _prepareTargetRepository(self):
844        """
845        Check for target repository existence, eventually create it.
846        """
847
848        if not self.repository.repository:
849            return
850
851        self.repository.create()
852
853    def _prepareWorkingDirectory(self, source_repo):
854        """
855        Checkout a working copy of the target SVN repository.
856        """
857
858        from os.path import join, exists
859        from vcpx.dualwd import IGNORED_METADIRS
860
861        if not self.repository.repository or exists(join(self.repository.basedir, self.repository.METADIR)):
862            return
863
864        cmd = self.repository.command("co", "--quiet")
865        if self.repository.ignore_externals:
866            cmd.append("--ignore-externals")
867
868        svnco = ExternalCommand(command=cmd)
869        svnco.execute("%s%s" % (self.repository.repository,
870                                self.repository.module), self.repository.basedir)
871
872        ignore = [md for md in IGNORED_METADIRS]
873
874        if self.logfile.startswith(self.repository.basedir):
875            ignore.append(self.logfile[len(self.repository.basedir)+1:])
876        if self.state_file.filename.startswith(self.repository.basedir):
877            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
878            ignore.append(sfrelname)
879            ignore.append(sfrelname+'.old')
880            ignore.append(sfrelname+'.journal')
881
882        cmd = self.repository.command("propset", "%(propname)s", "--quiet")
883        pset = ExternalCommand(cwd=self.repository.basedir, command=cmd)
884        pset.execute('\n'.join(ignore), '.', propname='svn:ignore')
885
886    def _initializeWorkingDir(self):
887        """
888        Add the given directory to an already existing svn working tree.
889        """
890
891        from os.path import exists, join
892
893        if not exists(join(self.repository.basedir, self.repository.METADIR)):
894            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.repository.basedir)
895
896        SynchronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.