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

Revision 1649, 37.3 KB checked in by lele@…, 5 years ago (diff)

Substitute REPLACED with RENAMED when it is the case
This is not enough and there are still problems with the REPLACED kind
of entries: when it's a directory that gets replaced, tailor should go
down the slipping route of adding REMOVEs for entries that were
effectively deleted...

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) -> 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
419class 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)
Note: See TracBrowser for help on using the repository browser.