source: tailor/vcpx/svn.py @ 806

Revision 806, 19.5 KB checked in by lele@…, 8 years ago (diff)

Create the pre-revprop-change hook in the new subversion repository
If use-propset is True when the repository is created at bootstrap time
automatically create a minimal (and maybe too permissive) pre-revprop-change
script, that's needed to activate the functionality.

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 shwrap import ExternalCommand, PIPE, STDOUT, ReopenableNamedTemporaryFile
15from source import UpdatableSourceWorkingDir, \
16     ChangesetApplicationFailure, GetUpstreamChangesetsFailure
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18
19def changesets_from_svnlog(log, repository, module):
20    from xml.sax import parseString
21    from xml.sax.handler import ContentHandler
22    from changes import ChangesetEntry, Changeset
23    from datetime import datetime
24    from string import maketrans
25
26    def get_entry_from_path(path, module=module):
27        # Given the repository url of this wc, say
28        #   "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
29        # extract the "entry" portion (a relative path) from what
30        # svn log --xml says, ie
31        #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
32        # that is to say "tests/PloneTestCase.py"
33
34        if path.startswith(module):
35            relative = path[len(module):]
36            if relative.startswith('/'):
37                return relative[1:]
38            else:
39                return relative
40
41        # The path is outside our tracked tree...
42        return None
43
44    class SvnXMLLogHandler(ContentHandler):
45        # Map between svn action and tailor's.
46        # NB: 'R', in svn parlance, means REPLACED, something other
47        # system may view as a simpler ADD, taking the following as
48        # the most common idiom::
49        #
50        #   # Rename the old file with a better name
51        #   $ svn mv somefile nicer-name-scheme.py
52        #
53        #   # Be nice with lazy users
54        #   $ echo "exec nicer-name-scheme.py" > somefile
55        #
56        #   # Add the wrapper with the old name
57        #   $ svn add somefile
58        #
59        #   $ svn commit -m "Longer name for somefile"
60
61        ACTIONSMAP = {'R': 'R', # will be ChangesetEntry.ADDED
62                      'M': ChangesetEntry.UPDATED,
63                      'A': ChangesetEntry.ADDED,
64                      'D': ChangesetEntry.DELETED}
65
66        def __init__(self):
67            self.changesets = []
68            self.current = None
69            self.current_field = []
70            self.renamed = {}
71
72        def startElement(self, name, attributes):
73            if name == 'logentry':
74                self.current = {}
75                self.current['revision'] = attributes['revision']
76                self.current['entries'] = []
77            elif name in ['author', 'date', 'msg']:
78                self.current_field = []
79            elif name == 'path':
80                self.current_field = []
81                if attributes.has_key('copyfrom-path'):
82                    self.current_path_action = (
83                        attributes['action'],
84                        attributes['copyfrom-path'],
85                        attributes['copyfrom-rev'])
86                else:
87                    self.current_path_action = attributes['action']
88
89        def endElement(self, name):
90            if name == 'logentry':
91                # Sort the paths to make tests easier
92                self.current['entries'].sort(lambda a,b: cmp(a.name, b.name))
93
94                # Eliminate "useless" entries: SVN does not have atomic
95                # renames, but rather uses a ADD+RM duo.
96                #
97                # So cycle over all entries of this patch, discarding
98                # the deletion of files that were actually renamed, and
99                # at the same time change related entry from ADDED to
100                # RENAMED.
101
102                mv_or_cp = {}
103                for e in self.current['entries']:
104                    if e.action_kind == e.ADDED and e.old_name is not None:
105                        mv_or_cp[e.old_name] = e
106
107                entries = []
108                for e in self.current['entries']:
109                    if e.action_kind==e.DELETED and mv_or_cp.has_key(e.name):
110                        mv_or_cp[e.name].action_kind = e.RENAMED
111                    elif e.action_kind=='R':
112                        if mv_or_cp.has_key(e.name):
113                            mv_or_cp[e.name].action_kind = e.RENAMED
114                        e.action_kind = e.ADDED
115                        entries.append(e)
116                    else:
117                        entries.append(e)
118
119                svndate = self.current['date']
120                # 2004-04-16T17:12:48.000000Z
121                y,m,d = map(int, svndate[:10].split('-'))
122                hh,mm,ss = map(int, svndate[11:19].split(':'))
123                ms = int(svndate[20:-1])
124                timestamp = datetime(y, m, d, hh, mm, ss, ms)
125
126                changeset = Changeset(self.current['revision'],
127                                      timestamp,
128                                      self.current.get('author'),
129                                      self.current['msg'],
130                                      entries)
131                self.changesets.append(changeset)
132                self.current = None
133            elif name in ['author', 'date', 'msg']:
134                self.current[name] = ''.join(self.current_field)
135            elif name == 'path':
136                path = ''.join(self.current_field)
137                entrypath = get_entry_from_path(path)
138                if entrypath:
139                    entry = ChangesetEntry(entrypath)
140
141                    if type(self.current_path_action) == type( () ):
142                        old = get_entry_from_path(self.current_path_action[1])
143                        if old:
144                            entry.action_kind = self.ACTIONSMAP[self.current_path_action[0]]
145                            entry.old_name = old
146                            self.renamed[entry.old_name] = True
147                        else:
148                            entry.action_kind = entry.ADDED
149                    else:
150                        entry.action_kind = self.ACTIONSMAP[self.current_path_action]
151
152                    self.current['entries'].append(entry)
153
154
155        def characters(self, data):
156            self.current_field.append(data)
157
158
159    # Apparently some (SVN repo contains)/(SVN server dumps) some characters that
160    # are illegal in an XML stream. This was the case with Twisted Matrix master
161    # repository. To be safe, we replace all of them with a question mark.
162
163    allbadchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0E\x0F\x10\x11" \
164                  "\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7f"
165    tt = maketrans(allbadchars, "?"*len(allbadchars))
166    handler = SvnXMLLogHandler()
167    parseString(log.read().translate(tt), handler)
168    return handler.changesets
169
170
171class SvnWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir):
172
173    ## UpdatableSourceWorkingDir
174
175    def _getUpstreamChangesets(self, sincerev=None):
176        if sincerev:
177            sincerev = int(sincerev)
178        else:
179            sincerev = 0
180
181        cmd = self.repository.command("log", "--verbose", "--xml",
182                                      "--revision", "%d:HEAD" % (sincerev+1))
183        svnlog = ExternalCommand(cwd=self.basedir, command=cmd)
184        log = svnlog.execute('.', stdout=PIPE, TZ='UTC')[0]
185
186        if svnlog.exit_status:
187            return []
188
189        return changesets_from_svnlog(log,
190                                      self.repository.repository,
191                                      self.repository.module)
192
193    def _applyChangeset(self, changeset):
194        from time import sleep
195
196        cmd = self.repository.command("update",
197                                      "--revision", changeset.revision, ".")
198        svnup = ExternalCommand(cwd=self.basedir, command=cmd)
199
200        retry = 0
201        while True:
202            out = svnup.execute(stdout=PIPE)[0]
203
204            if svnup.exit_status == 1:
205                retry += 1
206                if retry>3:
207                    break
208                delay = 2**retry
209                self.log_info("%s returned status %s, "
210                              "retrying in %d seconds..." %
211                              (str(svnup), svnup.exit_status, delay))
212                sleep(delay)
213            else:
214                break
215
216        if svnup.exit_status:
217            raise ChangesetApplicationFailure(
218                "%s returned status %s" % (str(svnup), svnup.exit_status))
219
220        self.log_info("%s updated to %s" % (
221            ','.join([e.name for e in changeset.entries]),
222            changeset.revision))
223
224        result = []
225        for line in out:
226            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
227                self.log_info("Conflict after 'svn update': '%s'" % line)
228                result.append(line[2:-1])
229
230        return result
231
232    def _checkoutUpstreamRevision(self, revision):
233        """
234        Concretely do the checkout of the upstream revision.
235        """
236
237        from os.path import join, exists
238
239        if revision == 'INITIAL':
240            initial = True
241            cmd = self.repository.command("log", "--verbose", "--xml",
242                                          "--limit", "1",
243                                          "--revision", "1:HEAD")
244            svnlog = ExternalCommand(command=cmd)
245            output = svnlog.execute("%s%s" % (self.repository.repository,
246                                              self.repository.module),
247                                    stdout=PIPE)[0]
248
249            if svnlog.exit_status:
250                raise TargetInitializationFailure(
251                    "%s returned status %d saying \"%s\"" %
252                    (str(output), changes.exit_status, output.read()))
253
254            csets = changesets_from_svnlog(output,
255                                           self.repository.repository,
256                                           self.repository.module)
257            revision = csets[0].revision
258        else:
259            initial = False
260
261        if not exists(join(self.basedir, '.svn')):
262            self.log_info("checking out a working copy")
263            cmd = self.repository.command("co", "--quiet",
264                                          "--revision", revision)
265            svnco = ExternalCommand(command=cmd)
266            svnco.execute("%s%s" % (self.repository.repository,
267                                    self.repository.module), self.basedir)
268            if svnco.exit_status:
269                raise TargetInitializationFailure(
270                    "%s returned status %s" % (str(svnco), svnco.exit_status))
271        else:
272            self.log_info("%s already exists, assuming it's a svn working dir" % self.basedir)
273
274        if not initial:
275            cmd = self.repository.command("log", "--verbose", "--xml",
276                                          "--revision", revision=='HEAD' and 'COMMITTED' or revision)
277            svnlog = ExternalCommand(cwd=self.basedir, command=cmd)
278            output = svnlog.execute(stdout=PIPE)[0]
279
280            if svnlog.exit_status:
281                raise TargetInitializationFailure(
282                    "%s returned status %d saying \"%s\"" %
283                    (str(changes), changes.exit_status, output.read()))
284
285            csets = changesets_from_svnlog(output,
286                                           self.repository.repository,
287                                           self.repository.module)
288
289        last = csets[0]
290
291        self.log_info("working copy up to svn revision %s" % last.revision)
292
293        return last
294
295    ## SyncronizableTargetWorkingDir
296
297    def _addPathnames(self, names):
298        """
299        Add some new filesystem objects.
300        """
301
302        cmd = self.repository.command("add", "--quiet", "--no-auto-props",
303                                      "--non-recursive")
304        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
305
306    def _getCommitEntries(self, changeset):
307        """
308        Extract the names of the entries for the commit phase.  Since SVN
309        handles "rename" operations as "remove+add", both entries must be
310        committed.
311        """
312
313        entries = SyncronizableTargetWorkingDir._getCommitEntries(self,
314                                                                  changeset)
315        entries.extend([e.old_name for e in changeset.renamedEntries()])
316
317        return entries
318
319    def _commit(self, date, author, patchname, changelog=None, entries=None):
320        """
321        Commit the changeset.
322        """
323
324        from sys import getdefaultencoding
325
326        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
327
328        logmessage = []
329        if patchname:
330            logmessage.append(patchname.encode(encoding))
331        if changelog:
332            logmessage.append(changelog.encode(encoding))
333
334        # If we cannot use propset, fall back to old behaviour of
335        # appending these info to the changelog
336
337        if not self.USE_PROPSET:
338            logmessage.append('')
339            logmessage.append('Original author: %s' % author.encode(encoding))
340            logmessage.append('Date: %s' % date)
341
342        rontf = ReopenableNamedTemporaryFile('svn', 'tailor')
343        log = open(rontf.name, "w")
344        log.write('\n'.join(logmessage))
345        log.close()
346
347        cmd = self.repository.command("commit", "--file", rontf.name)
348        commit = ExternalCommand(cwd=self.basedir, command=cmd)
349
350        if not entries:
351            entries = ['.']
352
353        out = commit.execute(entries, stdout=PIPE, LANG='C')[0]
354
355        if commit.exit_status:
356            raise ChangesetApplicationFailure("%s returned status %d" %
357                                              (str(commit), commit.exit_status))
358        line = out.readline()
359        while line and not line.startswith('Committed revision '):
360            if not line.startswith('Sending ') and \
361               not line.startswith('Transmitting file data ') and \
362               not line.startswith('Adding ') and \
363               not line.startswith('Deleting '):
364                break
365            line = out.readline()
366        if not line.startswith('Committed revision '):
367            raise ChangesetApplicationFailure("%s wrote unexpected line %r" %
368                                              (str(commit), line))
369        revision = line[19:-2]
370
371        if self.USE_PROPSET:
372            cmd = self.repository.command("propset", "%(propname)s",
373                                          "--quiet", "--revprop",
374                                          "--revision", revision)
375            propset = ExternalCommand(cwd=self.basedir, command=cmd)
376
377            propset.execute(date.isoformat()+".000000Z", propname='svn:date')
378            propset.execute(author, propname='svn:author')
379
380        cmd = self.repository.command("update", "--quiet",
381                                      "--revision", revision)
382        ExternalCommand(cwd=self.basedir, command=cmd).execute()
383
384    def _removePathnames(self, names):
385        """
386        Remove some filesystem objects.
387        """
388
389        cmd = self.repository.command("remove", "--quiet", "--force")
390        remove = ExternalCommand(cwd=self.basedir, command=cmd)
391        remove.execute(names)
392
393    def _renamePathname(self, oldname, newname):
394        """
395        Rename a filesystem object.
396        """
397
398        cmd = self.repository.command("mv", "--quiet")
399        move = ExternalCommand(cwd=self.basedir, command=cmd)
400        move.execute(oldname, newname)
401        if move.exit_status:
402            # Subversion does not seem to allow
403            #   $ mv a.txt b.txt
404            #   $ svn mv a.txt b.txt
405            # Here we are in this situation, since upstream VCS already
406            # moved the item. OTOH, svn really treats "mv" as "cp+rm",
407            # so we do the same here
408            self._removePathnames([oldname])
409            self._addPathnames([newname])
410
411    def __createRepository(self, target_repository, target_module):
412        """
413        Create a local repository.
414        """
415
416        from os.path import join
417        from sys import platform
418
419        assert target_repository.startswith('file:///')
420        repodir = target_repository[7:]
421        cmd = self.repository.command("create", "--fs-type", "fsfs",
422                                      svnadmin=True)
423        svnadmin = ExternalCommand(command=cmd)
424        svnadmin.execute(repodir)
425
426        if svnadmin.exit_status:
427            raise TargetInitializationFailure("Was not able to create a 'fsfs' "
428                                              "svn repository at %r" %
429                                              target_repository)
430        if self.USE_PROPSET:
431            hookname = join(repodir, 'hooks', 'pre-revprop-change')
432            if platform == 'win32':
433                hookname += '.bat'
434            prehook = open(hookname, 'wU')
435            if platform <> 'win32':
436                prehook.write('#!/bin/sh\n')
437            prehook.write('exit 0\n')
438            prehook.close()
439            if platform <> 'win32':
440                from os import chmod
441                chmod(hookname, 0755)
442
443        if target_module and target_module <> '/':
444            cmd = self.repository.command("mkdir", "-m",
445                                          "This directory will host the "
446                                          "upstream sources")
447            svnmkdir = ExternalCommand(command=cmd)
448            svnmkdir.execute(target_repository + target_module)
449            if svnmkdir.exit_status:
450                raise TargetInitializationFailure("Was not able to create the "
451                                                  "module %r, maybe more than "
452                                                  "one level directory?" %
453                                                  target_module)
454
455    def _prepareTargetRepository(self):
456        """
457        Check for target repository existence, eventually create it.
458        """
459
460        if not self.repository.repository:
461            return
462
463        # Verify the existence of repository by listing its root
464        cmd = self.repository.command("ls")
465        svnls = ExternalCommand(command=cmd)
466        svnls.execute(self.repository.repository)
467
468        if svnls.exit_status:
469            if self.repository.repository.startswith('file:///'):
470                self.__createRepository(self.repository.repository,
471                                        self.repository.module)
472            else:
473                raise TargetInitializationFailure("%r does not exist and "
474                                                  "cannot be created since "
475                                                  "it's not a local (file:///) "
476                                                  "repository" %
477                                                  self.repository.repository)
478
479    def _prepareWorkingDirectory(self, source_repo):
480        """
481        Checkout a working copy of the target SVN repository.
482        """
483
484        from os.path import join, exists
485
486        if not self.repository.repository or exists(join(self.basedir, '.svn')):
487            return
488
489        cmd = self.repository.command("co", "--quiet")
490        svnco = ExternalCommand(command=cmd)
491        svnco.execute("%s%s" % (self.repository.repository,
492                                self.repository.module), self.basedir)
493
494    def _initializeWorkingDir(self):
495        """
496        Add the given directory to an already existing svn working tree.
497        """
498
499        from os.path import exists, join
500
501        if not exists(join(self.basedir, '.svn')):
502            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.basedir)
503
504        SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.