source: tailor/vcpx/svn.py @ 301

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

Don't use native svn add recursion to import the initial bootstrap
I gave up trying to set svn:ignore so that it ignores other VCS
metadata, so, following the KISS principle, use the good'n'old way
of adding the subtree.

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