source: tailor/vcpx/svn.py @ 547

Revision 547, 17.3 KB checked in by lele@…, 8 years ago (diff)

Raise an exception when an error occurs at target commit

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 commit.exit_status:
342            raise ChangesetApplicationFailure("%s returned status %d" %
343                                              (str(commit), commit.exit_status))
344
345        if self.USE_PROPSET:
346            cmd = [self.repository.SVN_CMD, "propset", "%(propname)s",
347                   "--quiet", "--revprop", "-rHEAD"]
348            propset = ExternalCommand(cwd=self.basedir, command=cmd)
349
350            propset.execute(date.isoformat()+".000000Z", propname='svn:date')
351            propset.execute(author, propname='svn:author')
352
353    def _removePathnames(self, names):
354        """
355        Remove some filesystem objects.
356        """
357
358        cmd = [self.repository.SVN_CMD, "remove", "--quiet", "--force"]
359        remove = ExternalCommand(cwd=self.basedir, command=cmd)
360        remove.execute(names)
361
362    def _renamePathname(self, oldname, newname):
363        """
364        Rename a filesystem object.
365        """
366
367        cmd = [self.repository.SVN_CMD, "mv", "--quiet"]
368        move = ExternalCommand(cwd=self.basedir, command=cmd)
369        move.execute(oldname, newname)
370        if move.exit_status:
371            # Subversion does not seem to allow
372            #   $ mv a.txt b.txt
373            #   $ svn mv a.txt b.txt
374            # Here we are in this situation, since upstream VCS already
375            # moved the item. OTOH, svn really treats "mv" as "cp+rm",
376            # so we do the same here
377            self._removePathnames([oldname])
378            self._addPathnames([newname])
379
380    def __createRepository(self, target_repository, target_module):
381        """
382        Create a local repository.
383        """
384
385        assert target_repository.startswith('file:///')
386
387        cmd = [self.repository.SVNADMIN_CMD, "create", "--fs-type", "fsfs"]
388        svnadmin = ExternalCommand(command=cmd)
389        svnadmin.execute(target_repository[7:])
390
391        if svnadmin.exit_status:
392            raise TargetInitializationFailure("Was not able to create a 'fsfs' "
393                                              "svn repository at %r" %
394                                              target_repository)
395
396        if target_module and target_module <> '/':
397            cmd = [self.repository.SVN_CMD, "mkdir", "-m",
398                   "This directory will host the upstream sources"]
399            svnmkdir = ExternalCommand(command=cmd)
400            svnmkdir.execute(target_repository + target_module)
401            if svnmkdir.exit_status:
402                raise TargetInitializationFailure("Was not able to create the "
403                                                  "module %r, maybe more than "
404                                                  "one level directory?" %
405                                                  target_module)
406
407    def _prepareTargetRepository(self, source_repo):
408        """
409        Check for target repository existence, eventually create it.
410        """
411
412        if not self.repository.repository:
413            return
414
415        cmd = [self.repository.SVN_CMD, "info"]
416        svninfo = ExternalCommand(command=cmd)
417        svninfo.execute(self.repository.repository, stdout=PIPE, stderr=STDOUT)
418
419        if svninfo.exit_status:
420            if self.repository.repository.startswith('file:///'):
421                self.__createRepository(self.repository.repository,
422                                        self.repository.module)
423            else:
424                raise TargetInitializationFailure("%r does not exist and "
425                                                  "cannot be created since "
426                                                  "it's not a local (file:///) "
427                                                  "repository" %
428                                                  self.repository.repository)
429
430    def _prepareWorkingDirectory(self, source_repo):
431        """
432        Checkout a working copy of the target SVN repository.
433        """
434
435        if not self.repository.repository:
436            return
437
438        cmd = [self.repository.SVN_CMD, "co", "--quiet"]
439        svnco = ExternalCommand(command=cmd)
440        svnco.execute("%s%s" % (self.repository.repository,
441                                self.repository.module), self.basedir)
442
443    def _initializeWorkingDir(self):
444        """
445        Add the given directory to an already existing svn working tree.
446        """
447
448        from os.path import exists, join
449
450        if not exists(join(self.basedir, '.svn')):
451            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.basedir)
452
453        SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.