source: tailor/vcpx/svn.py @ 314

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

Fix typo

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