source: tailor/vcpx/svn.py @ 527

Revision 527, 17.0 KB checked in by lele@…, 8 years ago (diff)

Big API change, reducing arguments in favour of instance attributes
This is a big and subtle change that brings nothing in term of
functionality but make it a lot easier maintaining and extending
tailor as a whole.

Basically, 'root' and 'subdir' arguments are gone replaced by a
self.basedir, computed from the configuration; instead of 'logger',
the code uses two new methods, log_info() and log_error() on most
objects. Other arguments are derived from the configuration objects
that hang around.

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Subversion details
3# :Creato:   ven 18 giu 2004 15:00:52 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains supporting classes for Subversion.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import ExternalCommand, PIPE, STDOUT, ReopenableNamedTemporaryFile
15from source import UpdatableSourceWorkingDir, \
16     ChangesetApplicationFailure, GetUpstreamChangesetsFailure
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18
19def changesets_from_svnlog(log, repository, module):
20    from xml.sax import parseString
21    from xml.sax.handler import ContentHandler
22    from changes import ChangesetEntry, Changeset
23    from datetime import datetime
24    from string import maketrans
25
26    def get_entry_from_path(path, module=module):
27        # Given the repository url of this wc, say
28        #   "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
29        # extract the "entry" portion (a relative path) from what
30        # svn log --xml says, ie
31        #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
32        # that is to say "tests/PloneTestCase.py"
33
34        if path.startswith(module):
35            relative = path[len(module):]
36            if relative.startswith('/'):
37                return relative[1:]
38            else:
39                return relative
40
41        # The path is outside our tracked tree...
42        return None
43
44    class SvnXMLLogHandler(ContentHandler):
45        # Map between svn action and tailor's.
46        # NB: 'R', in svn parlance, means REPLACED, something other
47        # system may view as a simpler ADD, taking the following as
48        # the most common idiom::
49        #
50        #   # Rename the old file with a better name
51        #   $ svn mv somefile nicer-name-scheme.py
52        #
53        #   # Be nice with lazy users
54        #   $ echo "exec nicer-name-scheme.py" > somefile
55        #
56        #   # Add the wrapper with the old name
57        #   $ svn add somefile
58        #
59        #   $ svn commit -m "Longer name for somefile"
60
61        ACTIONSMAP = {'R': 'R', # will be ChangesetEntry.ADDED
62                      'M': ChangesetEntry.UPDATED,
63                      'A': ChangesetEntry.ADDED,
64                      'D': ChangesetEntry.DELETED}
65
66        def __init__(self):
67            self.changesets = []
68            self.current = None
69            self.current_field = []
70            self.renamed = {}
71
72        def startElement(self, name, attributes):
73            if name == 'logentry':
74                self.current = {}
75                self.current['revision'] = attributes['revision']
76                self.current['entries'] = []
77            elif name in ['author', 'date', 'msg']:
78                self.current_field = []
79            elif name == 'path':
80                self.current_field = []
81                if attributes.has_key('copyfrom-path'):
82                    self.current_path_action = (
83                        attributes['action'],
84                        attributes['copyfrom-path'],
85                        attributes['copyfrom-rev'])
86                else:
87                    self.current_path_action = attributes['action']
88
89        def endElement(self, name):
90            if name == 'logentry':
91                # Sort the paths to make tests easier
92                self.current['entries'].sort(lambda a,b: cmp(a.name, b.name))
93
94                # Eliminate "useless" entries: SVN does not have atomic
95                # renames, but rather uses a ADD+RM duo.
96                #
97                # So cycle over all entries of this patch, discarding
98                # the deletion of files that were actually renamed, and
99                # at the same time change related entry from ADDED to
100                # RENAMED.
101
102                mv_or_cp = {}
103                for e in self.current['entries']:
104                    if e.action_kind == e.ADDED and e.old_name is not None:
105                        mv_or_cp[e.old_name] = e
106
107                entries = []
108                for e in self.current['entries']:
109                    if e.action_kind==e.DELETED and mv_or_cp.has_key(e.name):
110                        mv_or_cp[e.name].action_kind = e.RENAMED
111                    elif e.action_kind=='R':
112                        if mv_or_cp.has_key(e.name):
113                            mv_or_cp[e.name].action_kind = e.RENAMED
114                        e.action_kind = e.ADDED
115                        entries.append(e)
116                    else:
117                        entries.append(e)
118
119                svndate = self.current['date']
120                # 2004-04-16T17:12:48.000000Z
121                y,m,d = map(int, svndate[:10].split('-'))
122                hh,mm,ss = map(int, svndate[11:19].split(':'))
123                ms = int(svndate[20:-1])
124                timestamp = datetime(y, m, d, hh, mm, ss, ms)
125
126                changeset = Changeset(self.current['revision'],
127                                      timestamp,
128                                      self.current.get('author'),
129                                      self.current['msg'],
130                                      entries)
131                self.changesets.append(changeset)
132                self.current = None
133            elif name in ['author', 'date', 'msg']:
134                self.current[name] = ''.join(self.current_field)
135            elif name == 'path':
136                path = ''.join(self.current_field)
137                entrypath = get_entry_from_path(path)
138                if entrypath:
139                    entry = ChangesetEntry(entrypath)
140
141                    if type(self.current_path_action) == type( () ):
142                        old = get_entry_from_path(self.current_path_action[1])
143                        if old:
144                            entry.action_kind = self.ACTIONSMAP[self.current_path_action[0]]
145                            entry.old_name = old
146                            self.renamed[entry.old_name] = True
147                        else:
148                            entry.action_kind = entry.ADDED
149                    else:
150                        entry.action_kind = self.ACTIONSMAP[self.current_path_action]
151
152                    self.current['entries'].append(entry)
153
154
155        def characters(self, data):
156            self.current_field.append(data)
157
158
159    # Apparently some (SVN repo contains)/(SVN server dumps) some characters that
160    # are illegal in an XML stream. This was the case with Twisted Matrix master
161    # repository. To be safe, we replace all of them with a question mark.
162
163    allbadchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0E\x0F\x10\x11" \
164                  "\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7f"
165    tt = maketrans(allbadchars, "?"*len(allbadchars))
166    handler = SvnXMLLogHandler()
167    parseString(log.read().translate(tt), handler)
168    return handler.changesets
169
170
171class SvnWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir):
172
173    ## UpdatableSourceWorkingDir
174
175    def _getUpstreamChangesets(self, sincerev=None):
176        if sincerev:
177            sincerev = int(sincerev)
178        else:
179            sincerev = 0
180
181        cmd = [self.repository.SVN_CMD, "log", "--verbose", "--xml",
182               "--revision", "%d:HEAD" % (sincerev+1)]
183        svnlog = ExternalCommand(cwd=self.basedir, command=cmd)
184        log = svnlog.execute('.', stdout=PIPE, TZ='UTC')
185
186        if svnlog.exit_status:
187            return []
188
189        return changesets_from_svnlog(log,
190                                      self.repository.repository,
191                                      self.repository.module)
192
193    def _applyChangeset(self, changeset):
194        cmd = [self.repository.SVN_CMD, "update",
195               "--revision", changeset.revision, "."]
196        svnup = ExternalCommand(cwd=self.basedir, command=cmd)
197        out = svnup.execute(stdout=PIPE)
198
199        if svnup.exit_status:
200            raise ChangesetApplicationFailure(
201                "%s returned status %s" % (str(svnup), svnup.exit_status))
202
203        self.log_info("%s updated to %s" % (
204            ','.join([e.name for e in changeset.entries]),
205            changeset.revision))
206
207        result = []
208        for line in out:
209            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
210                self.log_info("Conflict after 'svn update': '%s'" % line)
211                result.append(line[2:-1])
212
213        return result
214
215    def _checkoutUpstreamRevision(self, revision):
216        """
217        Concretely do the checkout of the upstream revision.
218        """
219
220        from os.path import join, exists
221
222        if revision == 'INITIAL':
223            initial = True
224            cmd = [self.repository.SVN_CMD, "log", "--verbose", "--xml",
225                   "--limit", "1", "--revision", "1:HEAD"]
226            svnlog = ExternalCommand(command=cmd)
227            output = svnlog.execute("%s%s" % (self.repository.repository,
228                                              self.repository.module),
229                                    stdout=PIPE)
230
231            if svnlog.exit_status:
232                raise ChangesetApplicationFailure(
233                    "%s returned status %d saying \"%s\"" %
234                    (str(output), changes.exit_status, output.read()))
235
236            csets = changesets_from_svnlog(output,
237                                           self.repository.repository,
238                                           self.repository.module)
239            revision = escape(csets[0].revision)
240        else:
241            initial = False
242
243        if not exists(join(self.basedir, '.svn')):
244            self.log_info("checking out a working copy")
245            cmd = [self.repository.SVN_CMD, "co", "--quiet",
246                   "--revision", revision]
247            svnco = ExternalCommand(command=cmd)
248            svnco.execute("%s%s" % (self.repository.repository,
249                                    self.repository.module), self.basedir)
250            if svnco.exit_status:
251                raise TargetInitializationFailure(
252                    "%s returned status %s" % (str(svnco), svnco.exit_status))
253        else:
254            self.log_info("%s already exists, assuming it's a svn working dir" % self.basedir)
255
256        if not initial:
257            cmd = [self.repository.SVN_CMD, "log", "--verbose", "--xml",
258                   "--revision", revision]
259            svnlog = ExternalCommand(cwd=self.basedir, command=cmd)
260            output = svnlog.execute(stdout=PIPE)
261
262            if svnlog.exit_status:
263                raise ChangesetApplicationFailure(
264                    "%s returned status %d saying \"%s\"" %
265                    (str(changes), changes.exit_status, output.read()))
266
267            csets = changesets_from_svnlog(output,
268                                           self.repository.repository,
269                                           self.repository.module)
270
271        last = csets[0]
272
273        self.log_info("working copy up to svn revision %s", last.revision)
274
275        return last
276
277    ## SyncronizableTargetWorkingDir
278
279    def _addPathnames(self, names):
280        """
281        Add some new filesystem objects.
282        """
283
284        cmd = [self.repository.SVN_CMD, "add", "--quiet", "--no-auto-props",
285               "--non-recursive"]
286        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
287
288    def _getCommitEntries(self, changeset):
289        """
290        Extract the names of the entries for the commit phase.  Since SVN
291        handles "rename" operations as "remove+add", both entries must be
292        committed.
293        """
294
295        entries = SyncronizableTargetWorkingDir._getCommitEntries(self,
296                                                                  changeset)
297        entries.extend([e.old_name for e in changeset.renamedEntries()])
298
299        return entries
300
301    def _commit(self, date, author, patchname, changelog=None, entries=None):
302        """
303        Commit the changeset.
304        """
305
306        from sys import getdefaultencoding
307
308        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
309
310        logmessage = []
311        if patchname:
312            logmessage.append(patchname.encode(encoding))
313        if changelog:
314            logmessage.append('')
315            logmessage.append(changelog.encode(encoding))
316        logmessage.append('')
317
318        # If we cannot use propset, fall back to old behaviour of
319        # appending these info to the changelog
320
321        if not self.USE_PROPSET:
322            logmessage.append('')
323            logmessage.append('Original author: %s' % author.encode(encoding))
324            logmessage.append('Date: %s' % date)
325            logmessage.append('')
326
327        rontf = ReopenableNamedTemporaryFile('svn', 'tailor')
328        log = open(rontf.name, "w")
329        log.write('\n'.join(logmessage))
330        log.close()
331
332        cmd = [self.repository.SVN_CMD, "commit", "--quiet",
333               "--file", rontf.name]
334        commit = ExternalCommand(cwd=self.basedir, command=cmd)
335
336        if not entries:
337            entries = ['.']
338
339        commit.execute(entries)
340
341        if self.USE_PROPSET:
342            cmd = [self.repository.SVN_CMD, "propset", "%(propname)s",
343                   "--quiet", "--revprop", "-rHEAD"]
344            propset = ExternalCommand(cwd=self.basedir, command=cmd)
345
346            propset.execute(date.isoformat()+".000000Z", propname='svn:date')
347            propset.execute(author, propname='svn:author')
348
349    def _removePathnames(self, names):
350        """
351        Remove some filesystem objects.
352        """
353
354        cmd = [self.repository.SVN_CMD, "remove", "--quiet", "--force"]
355        remove = ExternalCommand(cwd=self.basedir, command=cmd)
356        remove.execute(names)
357
358    def _renamePathname(self, oldname, newname):
359        """
360        Rename a filesystem object.
361        """
362
363        cmd = [self.repository.SVN_CMD, "mv", "--quiet"]
364        move = ExternalCommand(cwd=self.basedir, command=cmd)
365        move.execute(oldname, newname)
366        if move.exit_status:
367            # Subversion does not seem to allow
368            #   $ mv a.txt b.txt
369            #   $ svn mv a.txt b.txt
370            # Here we are in this situation, since upstream VCS already
371            # moved the item. OTOH, svn really treats "mv" as "cp+rm",
372            # so we do the same here
373            self._removePathnames([oldname])
374            self._addPathnames([newname])
375
376    def __createRepository(self, target_repository, target_module):
377        """
378        Create a local repository.
379        """
380
381        assert target_repository.startswith('file:///')
382
383        cmd = [self.repository.SVNADMIN_CMD, "create", "--fs-type", "fsfs"]
384        svnadmin = ExternalCommand(command=cmd)
385        svnadmin.execute(target_repository[7:])
386
387        if svnadmin.exit_status:
388            raise TargetInitializationFailure("Was not able to create a 'fsfs' "
389                                              "svn repository at %r" %
390                                              target_repository)
391
392        if target_module and target_module <> '/':
393            cmd = [self.repository.SVN_CMD, "mkdir", "-m",
394                   "This directory will host the upstream sources"]
395            svnmkdir = ExternalCommand(command=cmd)
396            svnmkdir.execute(target_repository + target_module)
397            if svnmkdir.exit_status:
398                raise TargetInitializationFailure("Was not able to create the "
399                                                  "module %r, maybe more than "
400                                                  "one level directory?" %
401                                                  target_module)
402
403    def _prepareTargetRepository(self):
404        """
405        Check for target repository existence, eventually create it.
406        """
407
408        cmd = [self.repository.SVN_CMD, "info"]
409        svninfo = ExternalCommand(command=cmd)
410        svninfo.execute(self.repository.repository, stdout=PIPE, stderr=STDOUT)
411
412        if svninfo.exit_status:
413            if self.repository.repository.startswith('file:///'):
414                self.__createRepository(self.repository.repository,
415                                        self.repository.module)
416            else:
417                raise TargetInitializationFailure("%r does not exist and "
418                                                  "cannot be created since "
419                                                  "it's not a local (file:///) "
420                                                  "repository" %
421                                                  self.repository.repository)
422
423    def _prepareWorkingDirectory(self):
424        """
425        Checkout a working copy of the target SVN repository.
426        """
427
428        cmd = [self.repository.SVN_CMD, "co", "--quiet"]
429        svnco = ExternalCommand(command=cmd)
430        svnco.execute("%s%s" % (self.repository.repository,
431                                self.repository.module), self.basedir)
432
433    def _initializeWorkingDir(self):
434        """
435        Add the given directory to an already existing svn working tree.
436        """
437
438        from os.path import exists, join
439
440        if not exists(join(self.basedir, '.svn')):
441            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.basedir)
442
443        SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.