source: tailor/vcpx/svn.py @ 168

Revision 168, 11.2 KB checked in by lele@…, 9 years ago (diff)

Fix the handling of out-of-tree entries
When we are tracking a subset of the upstream repository, it may happen
that something gets renamed and moved outside our tree, or viceversa that
something from the outside moves into it. In this cases, the RENAME
operation degrades to a simpler REMOVE/ADD, respectively.

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, ChangesetApplicationFailure
16from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
17
18
19class SvnUpdate(SystemCommand):
20    COMMAND = "svn update --revision %(revision)s %(entry)s"
21
22
23class SvnInfo(SystemCommand):
24    COMMAND = "LANG= svn info %(entry)s"
25
26    def __call__(self, output=None, dry_run=False, **kwargs):
27        output = SystemCommand.__call__(self, output=True,
28                                        dry_run=dry_run,
29                                        **kwargs)
30        res = {}
31        for l in output:
32            l = l[:-1]
33            if l:
34                key, value = l.split(':', 1)
35                res[key] = value[1:]
36        return res
37
38                 
39class SvnPropGet(SystemCommand):
40    COMMAND = "svn propget %(property)s %(entry)s"
41
42   
43class SvnPropSet(SystemCommand):
44    COMMAND = "svn propset --quiet %(property)s %(value)s %(entry)s"
45
46
47class SvnLog(SystemCommand):
48    COMMAND = "TZ=UTC svn log %(quiet)s %(xml)s --revision %(startrev)s:%(endrev)s %(entry)s 2>&1"
49   
50    def __call__(self, output=None, dry_run=False, **kwargs):
51        quiet = kwargs.get('quiet', True)
52        if quiet == True:
53            kwargs['quiet'] = '--quiet'
54        elif quiet == False:
55            kwargs['quiet'] = ''
56           
57        xml = kwargs.get('xml', False)
58        if xml:
59            kwargs['xml'] = '--xml'
60            output = True
61        else:
62            kwargs['xml'] = ''
63
64        startrev = kwargs.get('startrev')
65        if not startrev:
66            kwargs['startrev'] = 'BASE'
67
68        endrev = kwargs.get('endrev')
69        if not endrev:
70            kwargs['endrev'] = 'HEAD'
71
72        output = SystemCommand.__call__(self, output=output,
73                                        dry_run=dry_run, **kwargs)
74
75        if xml:
76            # parse the output and return the result
77            pass
78
79        return output
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 %(wc)s"
115
116def changesets_from_svnlog(log, url):
117    from xml.sax import parseString
118    from xml.sax.handler import ContentHandler
119    from changes import ChangesetEntry, Changeset
120    from datetime import datetime
121
122    def get_entry_from_path(path, url=url):
123        # Given the repository url of this wc, say
124        #   "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
125        # extract the "entry" portion (a relative path) from what
126        # svn log --xml says, ie
127        #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
128        # that is to say "tests/PloneTestCase.py"
129
130        from os.path import split
131
132        prefix = split(path)[0]
133        while prefix:
134            if url.endswith(prefix):
135                return path[len(prefix)+1:]
136
137            prefix = split(prefix)[0]
138
139        # The path is outside our tracked tree...
140        return None
141
142    class SvnXMLLogHandler(ContentHandler):
143        def __init__(self):
144            self.changesets = []
145            self.current = None
146            self.current_field = []
147            self.renamed = {}
148           
149        def startElement(self, name, attributes):
150            if name == 'logentry':
151                self.current = {}
152                self.current['revision'] = attributes['revision']
153                self.current['entries'] = []
154            elif name in ['author', 'date', 'msg']:
155                self.current_field = []
156            elif name == 'path':
157                self.current_field = []
158                if attributes.has_key('copyfrom-path'):
159                    self.current_path_action = (
160                        attributes['action'],
161                        attributes['copyfrom-path'][1:], # make it relative
162                        attributes['copyfrom-rev'])
163                else:
164                    self.current_path_action = attributes['action']
165
166        def endElement(self, name):
167            if name == 'logentry':
168                # Sort the paths to make tests easier
169                self.current['entries'].sort(lambda a,b: cmp(a.name, b.name))
170
171                # Eliminate renamed entries
172                entries = [e for e in self.current['entries']
173                           if e.name not in self.renamed]
174               
175                svndate = self.current['date']
176                # 2004-04-16T17:12:48.000000Z
177                y,m,d = map(int, svndate[:10].split('-'))
178                hh,mm,ss = map(int, svndate[11:19].split(':'))
179                ms = int(svndate[20:-1])
180                timestamp = datetime(y, m, d, hh, mm, ss, ms)
181               
182                changeset = Changeset(self.current['revision'],
183                                      timestamp,
184                                      self.current['author'],
185                                      self.current['msg'],
186                                      entries)
187                self.changesets.append(changeset)
188                self.current = None
189            elif name in ['author', 'date', 'msg']:
190                self.current[name] = ''.join(self.current_field)
191            elif name == 'path':
192                path = ''.join(self.current_field)[1:]
193                entrypath = get_entry_from_path(path)
194                if entrypath:
195                    entry = ChangesetEntry(entrypath)
196
197                    if type(self.current_path_action) == type( () ):
198                        old = get_entry_from_path(self.current_path_action[1])
199                        if old:
200                            entry.action_kind = entry.RENAMED
201                            entry.old_name = old
202                            self.renamed[entry.old_name] = True
203                        else:
204                            entry.action_kind = entry.ADDED
205                    else:
206                        entry.action_kind = self.current_path_action
207
208                    self.current['entries'].append(entry)
209
210                   
211        def characters(self, data):
212            self.current_field.append(data)
213
214
215    handler = SvnXMLLogHandler()
216    parseString(log.getvalue(), handler)
217    return handler.changesets
218
219
220class SvnWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir):
221
222    ## UpdatableSourceWorkingDir
223
224    def getUpstreamChangesets(self, root, sincerev=None):
225        if sincerev:
226            sincerev = int(sincerev)
227        else:
228            sincerev = 0
229           
230        svnlog = SvnLog(working_dir=root)
231        log = svnlog(quiet='--verbose', output=True, xml=True,
232                     startrev=sincerev+1, entry='.')
233       
234        if svnlog.exit_status:
235            return []
236       
237        url = SvnInfo(working_dir=root)(entry='.')['URL']
238       
239        return self.__parseSvnLog(log, url)
240
241    def __parseSvnLog(self, log, url):
242        """Return an object representation of the ``svn log`` thru HEAD."""
243
244        return changesets_from_svnlog(log, url)
245   
246    def _applyChangeset(self, root, changeset, logger=None):
247        svnup = SvnUpdate(working_dir=root)
248        out = svnup(output=True, entry='.', revision=changeset.revision)
249
250        if svnup.exit_status:
251            raise ChangesetApplicationFailure(
252                "'svn update' returned status %s" % svnup.exit_status)
253           
254        if logger: logger.info("%s updated to %s" % (
255            ','.join([e.name for e in changeset.entries]),
256            changeset.revision))
257       
258        result = []
259        for line in out:
260            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
261                logger.warn("Conflict after 'svn update': '%s'" % line)
262                result.append(line[2:-1])
263           
264        return result
265       
266    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
267                                  subdir=None, logger=None):
268        """
269        Concretely do the checkout of the upstream revision.
270        """
271       
272        from os.path import join, exists, split
273       
274        if not subdir:
275            subdir = split(module)[1]
276           
277        wdir = join(basedir, subdir)
278
279        if not exists(wdir):
280            svnco = SvnCheckout(working_dir=basedir)
281            svnco(output=True, repository=repository,
282                  wc=module, revision=revision)
283            if svnco.exit_status:
284                raise TargetInitializationFailure(
285                    "'svn checkout' returned status %s" % svnco.exit_status)
286
287        actual = SvnInfo(working_dir=wdir)(entry='.')['Revision']
288
289        if logger: logger.info("working copy up to svn revision %s",
290                               actual)
291       
292        return actual
293   
294    ## SyncronizableTargetWorkingDir
295
296    def _addEntry(self, root, entry):
297        """
298        Add a new entry.
299        """
300
301        c = SvnAdd(working_dir=root)
302        c(entry=entry)
303
304    def _commit(self,root, date, author, remark, changelog=None, entries=None):
305        """
306        Commit the changeset.
307        """
308
309        c = SvnCommit(working_dir=root)
310       
311        logmessage = "%s\nOriginal author: %s\nDate: %s" % (remark, author,
312                                                            date)
313        if changelog:
314            logmessage = logmessage + '\n\n' + changelog
315           
316        if entries:
317            entries = ' '.join(entries)
318        else:
319            entries = '.'
320           
321        c(logmessage=logmessage, entries=entries)
322       
323    def _removeEntry(self, root, entry):
324        """
325        Remove an entry.
326        """
327
328        c = SvnRemove(working_dir=root)
329        c(entry=entry)
330
331    def _renameEntry(self, root, oldentry, newentry):
332        """
333        Rename an entry.
334        """
335
336        c = SvnMv(working_dir=root)
337        c(old=oldentry, new=newentry)
338
339    def _initializeWorkingDir(self, root, module, addentry=None):
340        """
341        Add the given directory to an already existing svn working tree.
342        """
343
344        from os.path import exists, join
345
346        if not exists(join(root, '.svn')):
347            raise TargetInitializationFailure("'%s' should already be under SVN" % root)
348
349        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root, module,
350                                                            SvnAdd)
Note: See TracBrowser for help on using the repository browser.