source: tailor/vcpx/svn.py @ 251

Revision 251, 14.2 KB checked in by lele@…, 8 years ago (diff)

Do not use StringIO specific getvalue(), use read() instead

Line 
1#! /usr/bin/python
2# -*- mode: python; coding: utf-8 -*-
3# :Progetto: vcpx -- Subversion details
4# :Creato:   ven 18 giu 2004 15:00:52 CEST
5# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
6#
7
8"""
9This module contains supporting classes for Subversion.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import SystemCommand, shrepr
15from source import UpdatableSourceWorkingDir, \
16     ChangesetApplicationFailure, GetUpstreamChangesetsFailure
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18
19
20class SvnUpdate(SystemCommand):
21    COMMAND = "svn update --revision %(revision)s %(entry)s"
22
23
24class SvnInfo(SystemCommand):
25    COMMAND = "LANG= svn info %(entry)s"
26
27    def __call__(self, output=None, dry_run=False, **kwargs):
28        output = SystemCommand.__call__(self, output=True,
29                                        dry_run=dry_run,
30                                        **kwargs)
31        res = {}
32        for l in output:
33            l = l[:-1]
34            if l:
35                key, value = l.split(':', 1)
36                res[key] = value[1:]
37        return res
38
39                 
40class SvnPropGet(SystemCommand):
41    COMMAND = "svn propget %(property)s %(entry)s"
42
43   
44class SvnPropSet(SystemCommand):
45    COMMAND = "svn propset --quiet %(property)s %(value)s %(entry)s"
46
47
48class SvnLog(SystemCommand):
49    COMMAND = "TZ=UTC svn log %(quiet)s %(xml)s --revision %(startrev)s:%(endrev)s %(entry)s > /tmp/tailor.last.svnlog 2>&1"
50   
51    def __call__(self, output=None, dry_run=False, **kwargs):
52        quiet = kwargs.get('quiet', True)
53        if quiet == True:
54            kwargs['quiet'] = '--quiet'
55        elif quiet == False:
56            kwargs['quiet'] = ''
57           
58        xml = kwargs.get('xml', False)
59        if xml:
60            kwargs['xml'] = '--xml'
61            output = True
62        else:
63            kwargs['xml'] = ''
64
65        startrev = kwargs.get('startrev')
66        if not startrev:
67            kwargs['startrev'] = 'BASE'
68
69        endrev = kwargs.get('endrev')
70        if not endrev:
71            kwargs['endrev'] = 'HEAD'
72
73        SystemCommand.__call__(self, output=False, dry_run=dry_run, **kwargs)
74
75        return open('/tmp/tailor.last.svnlog')
76
77
78class SvnCommit(SystemCommand):
79    COMMAND = "svn commit --quiet --file %(logfile)s %(entries)s"
80
81    def __call__(self, output=None, dry_run=False, **kwargs):
82        logfile = kwargs.get('logfile')
83        if not logfile:
84            from tempfile import NamedTemporaryFile
85
86            log = NamedTemporaryFile(bufsize=0)
87            logmessage = kwargs.get('logmessage')
88            if logmessage:
89                log.write(logmessage)
90           
91            kwargs['logfile'] = log.name
92       
93        return SystemCommand.__call__(self, output=output,
94                                      dry_run=dry_run, **kwargs)
95
96
97class SvnAdd(SystemCommand):
98    COMMAND = "svn add --quiet --no-auto-props --non-recursive %(entry)s"
99
100       
101class SvnRemove(SystemCommand):
102    COMMAND = "svn remove --quiet --force %(entry)s"
103
104
105class SvnMv(SystemCommand):
106    COMMAND = "svn mv --quiet %(old)s %(new)s"
107
108   
109class SvnCheckout(SystemCommand):
110    COMMAND = "svn co --revision %(revision)s %(repository)s%(module)s %(wc)s"
111
112    def __call__(self, output=None, dry_run=False, **kwargs):
113        module = kwargs.get('module')
114        if module:
115            module = '/%s' % module
116        else:
117            module = ''
118        kwargs['module'] = module
119        return SystemCommand.__call__(self, output=output,
120                                      dry_run=dry_run, **kwargs)
121
122
123def changesets_from_svnlog(log, url, repository, module):
124    from xml.sax import parseString
125    from xml.sax.handler import ContentHandler
126    from changes import ChangesetEntry, Changeset
127    from datetime import datetime
128
129    def get_entry_from_path(path, module=module):
130        # Given the repository url of this wc, say
131        #   "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
132        # extract the "entry" portion (a relative path) from what
133        # svn log --xml says, ie
134        #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
135        # that is to say "tests/PloneTestCase.py"
136
137        if path.startswith(module):
138            relative = path[len(module):]
139            if relative.startswith('/'):
140                return relative[1:]
141            else:
142                return relative
143       
144        # The path is outside our tracked tree...
145        return None
146       
147    class SvnXMLLogHandler(ContentHandler):
148        # Map between svn action and tailor's.
149        # NB: 'R', in svn parlance, means REPLACED, something other
150        # system may view as a simpler ADD, taking the following as
151        # the most common idiom::
152        #
153        #   # Rename the old file with a better name
154        #   $ svn mv somefile nicer-name-scheme.py
155        #
156        #   # Be nice with lazy users
157        #   $ echo "exec nicer-name-scheme.py" > somefile
158        #
159        #   # Add the wrapper with the old name
160        #   $ svn add somefile
161        #
162        #   $ svn commit -m "Longer name for somefile"
163
164        ACTIONSMAP = {'R': 'R', # will be ChangesetEntry.ADDED
165                      'M': ChangesetEntry.UPDATED,
166                      'A': ChangesetEntry.ADDED,
167                      'D': ChangesetEntry.DELETED}
168       
169        def __init__(self):
170            self.changesets = []
171            self.current = None
172            self.current_field = []
173            self.renamed = {}
174           
175        def startElement(self, name, attributes):
176            if name == 'logentry':
177                self.current = {}
178                self.current['revision'] = attributes['revision']
179                self.current['entries'] = []
180            elif name in ['author', 'date', 'msg']:
181                self.current_field = []
182            elif name == 'path':
183                self.current_field = []
184                if attributes.has_key('copyfrom-path'):
185                    self.current_path_action = (
186                        attributes['action'],
187                        attributes['copyfrom-path'],
188                        attributes['copyfrom-rev'])
189                else:
190                    self.current_path_action = attributes['action']
191
192        def endElement(self, name):
193            if name == 'logentry':
194                # Sort the paths to make tests easier
195                self.current['entries'].sort(lambda a,b: cmp(a.name, b.name))
196
197                # Eliminate "useless" entries: SVN does not have atomic
198                # renames, but rather uses a ADD+RM duo.
199                #
200                # So cycle over all entries of this patch, discarding
201                # the deletion of files that were actually renamed, and
202                # at the same time change related entry from ADDED to
203                # RENAMED.
204
205                mv_or_cp = {}
206                for e in self.current['entries']:
207                    if e.action_kind == e.ADDED and e.old_name is not None:
208                        mv_or_cp[e.old_name] = e
209               
210                entries = []
211                for e in self.current['entries']:
212                    if e.action_kind==e.DELETED and mv_or_cp.has_key(e.name):
213                        mv_or_cp[e.name].action_kind = e.RENAMED
214                    elif e.action_kind=='R':
215                        if mv_or_cp.has_key(e.name):
216                            mv_or_cp[e.name].action_kind = e.RENAMED
217                        e.action_kind = e.ADDED
218                        entries.append(e)
219                    else:
220                        entries.append(e)                       
221               
222                svndate = self.current['date']
223                # 2004-04-16T17:12:48.000000Z
224                y,m,d = map(int, svndate[:10].split('-'))
225                hh,mm,ss = map(int, svndate[11:19].split(':'))
226                ms = int(svndate[20:-1])
227                timestamp = datetime(y, m, d, hh, mm, ss, ms)
228               
229                changeset = Changeset(self.current['revision'],
230                                      timestamp,
231                                      self.current['author'],
232                                      self.current['msg'],
233                                      entries)
234                self.changesets.append(changeset)
235                self.current = None
236            elif name in ['author', 'date', 'msg']:
237                self.current[name] = ''.join(self.current_field)
238            elif name == 'path':
239                path = ''.join(self.current_field)
240                entrypath = get_entry_from_path(path)
241                if entrypath:
242                    entry = ChangesetEntry(entrypath)
243
244                    if type(self.current_path_action) == type( () ):
245                        old = get_entry_from_path(self.current_path_action[1])
246                        if old:
247                            entry.action_kind = self.ACTIONSMAP[self.current_path_action[0]]
248                            entry.old_name = old
249                            self.renamed[entry.old_name] = True
250                        else:
251                            entry.action_kind = entry.ADDED
252                    else:
253                        entry.action_kind = self.ACTIONSMAP[self.current_path_action]
254
255                    self.current['entries'].append(entry)
256
257                   
258        def characters(self, data):
259            self.current_field.append(data)
260
261
262    handler = SvnXMLLogHandler()
263    parseString(log.read(), handler)
264    return handler.changesets
265
266
267class SvnWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir):
268
269    ## UpdatableSourceWorkingDir
270
271    def getUpstreamChangesets(self, root, repository, module, sincerev=None):
272        if sincerev:
273            sincerev = int(sincerev)
274        else:
275            sincerev = 0
276           
277        svnlog = SvnLog(working_dir=root)
278        log = svnlog(quiet='--verbose', output=True, xml=True,
279                     startrev=sincerev+1, entry='.')
280       
281        if svnlog.exit_status:
282            return []
283
284        svninfo = SvnInfo(working_dir=root)
285        info = svninfo(entry='.')
286
287        if svninfo.exit_status:
288            raise GetUpstreamChangesetsFailure('svn info on %r exited with status %d' % (root, svninfo.exit_status))
289       
290        return self.__parseSvnLog(log, info['URL'], repository, module)
291
292    def __parseSvnLog(self, log, url, repository, module):
293        """Return an object representation of the ``svn log`` thru HEAD."""
294
295        return changesets_from_svnlog(log, url, repository, module)
296   
297    def _applyChangeset(self, root, changeset, logger=None):
298        svnup = SvnUpdate(working_dir=root)
299        out = svnup(output=True, entry='.', revision=changeset.revision)
300
301        if svnup.exit_status:
302            raise ChangesetApplicationFailure(
303                "'svn update' returned status %s" % svnup.exit_status)
304           
305        if logger: logger.info("%s updated to %s" % (
306            ','.join([e.name for e in changeset.entries]),
307            changeset.revision))
308       
309        result = []
310        for line in out:
311            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
312                logger.warn("Conflict after 'svn update': '%s'" % line)
313                result.append(line[2:-1])
314           
315        return result
316       
317    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
318                                  subdir=None, logger=None):
319        """
320        Concretely do the checkout of the upstream revision.
321        """
322       
323        from os.path import join, exists
324       
325        wdir = join(basedir, subdir)
326
327        if not exists(join(wdir, '.svn')):
328            if logger: logger.info("checking out a working copy")
329            svnco = SvnCheckout(working_dir=basedir)
330            svnco(output=True, repository=repository, module=module,
331                  wc=shrepr(subdir), revision=revision)
332            if svnco.exit_status:
333                raise TargetInitializationFailure(
334                    "'svn checkout' returned status %s" % svnco.exit_status)
335        else:
336            if logger: logger.info("%s already exists, assuming it's a svn working dir" % wdir)
337
338        svninfo = SvnInfo(working_dir=wdir)
339        info = svninfo(entry='.')
340        if svninfo.exit_status:
341            raise GetUpstreamChangesetsFailure(
342                'svn info on %r exited with status %d' %
343                (wdir, svninfo.exit_status))       
344
345        actual = info['Revision']
346       
347        if logger: logger.info("working copy up to svn revision %s",
348                               actual)
349       
350        return actual
351   
352    ## SyncronizableTargetWorkingDir
353
354    def _addEntries(self, root, entries):
355        """
356        Add a sequence of entries.
357        """
358
359        c = SvnAdd(working_dir=root)
360        c(entry=' '.join([shrepr(e.name) for e in entries]))
361
362    def _commit(self,root, date, author, remark, changelog=None, entries=None):
363        """
364        Commit the changeset.
365        """
366
367        c = SvnCommit(working_dir=root)
368       
369        logmessage = "%s\nOriginal author: %s\nDate: %s" % (remark, author,
370                                                            date)
371        if changelog:
372            logmessage = logmessage + '\n\n' + changelog
373           
374        if entries:
375            entries = ' '.join([shrepr(e) for e in entries])
376        else:
377            entries = '.'
378           
379        c(logmessage=logmessage, entries=entries)
380       
381    def _removeEntries(self, root, entries):
382        """
383        Remove a sequence of entries.
384        """
385
386        c = SvnRemove(working_dir=root)
387        c(entry=' '.join([shrepr(e.name) for e in entries]))
388
389    def _renameEntry(self, root, oldentry, newentry):
390        """
391        Rename an entry.
392        """
393
394        c = SvnMv(working_dir=root)
395        c(old=shrepr(oldentry), new=repr(newentry))
396
397    def _initializeWorkingDir(self, root, repository, module, subdir, addentry=None):
398        """
399        Add the given directory to an already existing svn working tree.
400        """
401
402        from os.path import exists, join
403
404        if not exists(join(root, '.svn')):
405            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already be under SVN" % root)
406
407        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root,
408                                                            repository, module,
409                                                            subdir, SvnAdd)
Note: See TracBrowser for help on using the repository browser.