source: tailor/vcpx/svn.py @ 215

Revision 215, 13.1 KB checked in by lele@…, 8 years ago (diff)

Do 'adds' and 'removes' in batches
Possibly use a single command to add/remove multiple paths, instead of
executing the target VCS command once for each path.

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