source: tailor/vcpx/svn.py @ 252

Revision 252, 14.3 KB checked in by lele@…, 8 years ago (diff)

Use mktemp() to get a safer temporary filename

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