source: tailor/vcpx/svn.py @ 499

Revision 499, 16.6 KB checked in by lele@…, 8 years ago (diff)

Do not specify working dir when it's not needed

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