source: tailor/vcpx/svn.py @ 713

Revision 713, 17.9 KB checked in by lele@…, 8 years ago (diff)

Fix svn bootstrap from HEAD

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')[0]
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        from time import sleep
195
196        cmd = [self.repository.SVN_CMD, "update",
197               "--revision", changeset.revision, "."]
198        svnup = ExternalCommand(cwd=self.basedir, command=cmd)
199
200        retry = 0
201        while True:
202            out = svnup.execute(stdout=PIPE)[0]
203
204            if svnup.exit_status == 1:
205                retry += 1
206                if retry>3:
207                    break
208                delay = 2**retry
209                self.log_info("%s returned status %s, "
210                              "retrying in %d seconds..." %
211                              (str(svnup), svnup.exit_status, delay))
212                sleep(delay)
213            else:
214                break
215
216        if svnup.exit_status:
217            raise ChangesetApplicationFailure(
218                "%s returned status %s" % (str(svnup), svnup.exit_status))
219
220        self.log_info("%s updated to %s" % (
221            ','.join([e.name for e in changeset.entries]),
222            changeset.revision))
223
224        result = []
225        for line in out:
226            if len(line)>2 and line[0] == 'C' and line[1] == ' ':
227                self.log_info("Conflict after 'svn update': '%s'" % line)
228                result.append(line[2:-1])
229
230        return result
231
232    def _checkoutUpstreamRevision(self, revision):
233        """
234        Concretely do the checkout of the upstream revision.
235        """
236
237        from os.path import join, exists
238
239        if revision == 'INITIAL':
240            initial = True
241            cmd = [self.repository.SVN_CMD, "log", "--verbose", "--xml",
242                   "--limit", "1", "--revision", "1:HEAD"]
243            svnlog = ExternalCommand(command=cmd)
244            output = svnlog.execute("%s%s" % (self.repository.repository,
245                                              self.repository.module),
246                                    stdout=PIPE)[0]
247
248            if svnlog.exit_status:
249                raise ChangesetApplicationFailure(
250                    "%s returned status %d saying \"%s\"" %
251                    (str(output), changes.exit_status, output.read()))
252
253            csets = changesets_from_svnlog(output,
254                                           self.repository.repository,
255                                           self.repository.module)
256            revision = csets[0].revision
257        else:
258            initial = False
259
260        if not exists(join(self.basedir, '.svn')):
261            self.log_info("checking out a working copy")
262            cmd = [self.repository.SVN_CMD, "co", "--quiet",
263                   "--revision", revision]
264            svnco = ExternalCommand(command=cmd)
265            svnco.execute("%s%s" % (self.repository.repository,
266                                    self.repository.module), self.basedir)
267            if svnco.exit_status:
268                raise TargetInitializationFailure(
269                    "%s returned status %s" % (str(svnco), svnco.exit_status))
270        else:
271            self.log_info("%s already exists, assuming it's a svn working dir" % self.basedir)
272
273        if not initial:
274            cmd = [self.repository.SVN_CMD, "log", "--verbose", "--xml",
275                   "--revision", revision=='HEAD' and 'COMMITTED' or revision]
276            svnlog = ExternalCommand(cwd=self.basedir, command=cmd)
277            output = svnlog.execute(stdout=PIPE)[0]
278
279            if svnlog.exit_status:
280                raise ChangesetApplicationFailure(
281                    "%s returned status %d saying \"%s\"" %
282                    (str(changes), changes.exit_status, output.read()))
283
284            csets = changesets_from_svnlog(output,
285                                           self.repository.repository,
286                                           self.repository.module)
287
288        last = csets[0]
289
290        self.log_info("working copy up to svn revision %s" % last.revision)
291
292        return last
293
294    ## SyncronizableTargetWorkingDir
295
296    def _addPathnames(self, names):
297        """
298        Add some new filesystem objects.
299        """
300
301        cmd = [self.repository.SVN_CMD, "add", "--quiet", "--no-auto-props",
302               "--non-recursive"]
303        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
304
305    def _getCommitEntries(self, changeset):
306        """
307        Extract the names of the entries for the commit phase.  Since SVN
308        handles "rename" operations as "remove+add", both entries must be
309        committed.
310        """
311
312        entries = SyncronizableTargetWorkingDir._getCommitEntries(self,
313                                                                  changeset)
314        entries.extend([e.old_name for e in changeset.renamedEntries()])
315
316        return entries
317
318    def _commit(self, date, author, patchname, changelog=None, entries=None):
319        """
320        Commit the changeset.
321        """
322
323        from sys import getdefaultencoding
324
325        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
326
327        logmessage = []
328        if patchname:
329            logmessage.append(patchname.encode(encoding))
330        if changelog:
331            logmessage.append(changelog.encode(encoding))
332
333        # If we cannot use propset, fall back to old behaviour of
334        # appending these info to the changelog
335
336        if not self.USE_PROPSET:
337            logmessage.append('')
338            logmessage.append('Original author: %s' % author.encode(encoding))
339            logmessage.append('Date: %s' % date)
340
341        rontf = ReopenableNamedTemporaryFile('svn', 'tailor')
342        log = open(rontf.name, "w")
343        log.write('\n'.join(logmessage))
344        log.close()
345
346        cmd = [self.repository.SVN_CMD, "commit", "--quiet",
347               "--file", rontf.name]
348        commit = ExternalCommand(cwd=self.basedir, command=cmd)
349
350        if not entries:
351            entries = ['.']
352
353        commit.execute(entries)
354
355        if commit.exit_status:
356            raise ChangesetApplicationFailure("%s returned status %d" %
357                                              (str(commit), commit.exit_status))
358
359        if self.USE_PROPSET:
360            cmd = [self.repository.SVN_CMD, "propset", "%(propname)s",
361                   "--quiet", "--revprop", "-rHEAD"]
362            propset = ExternalCommand(cwd=self.basedir, command=cmd)
363
364            propset.execute(date.isoformat()+".000000Z", propname='svn:date')
365            propset.execute(author, propname='svn:author')
366
367        cmd = [self.repository.SVN_CMD, "update", "--quiet"]
368        ExternalCommand(cwd=self.basedir, command=cmd).execute()
369
370    def _removePathnames(self, names):
371        """
372        Remove some filesystem objects.
373        """
374
375        cmd = [self.repository.SVN_CMD, "remove", "--quiet", "--force"]
376        remove = ExternalCommand(cwd=self.basedir, command=cmd)
377        remove.execute(names)
378
379    def _renamePathname(self, oldname, newname):
380        """
381        Rename a filesystem object.
382        """
383
384        cmd = [self.repository.SVN_CMD, "mv", "--quiet"]
385        move = ExternalCommand(cwd=self.basedir, command=cmd)
386        move.execute(oldname, newname)
387        if move.exit_status:
388            # Subversion does not seem to allow
389            #   $ mv a.txt b.txt
390            #   $ svn mv a.txt b.txt
391            # Here we are in this situation, since upstream VCS already
392            # moved the item. OTOH, svn really treats "mv" as "cp+rm",
393            # so we do the same here
394            self._removePathnames([oldname])
395            self._addPathnames([newname])
396
397    def __createRepository(self, target_repository, target_module):
398        """
399        Create a local repository.
400        """
401
402        assert target_repository.startswith('file:///')
403
404        cmd = [self.repository.SVNADMIN_CMD, "create", "--fs-type", "fsfs"]
405        svnadmin = ExternalCommand(command=cmd)
406        svnadmin.execute(target_repository[7:])
407
408        if svnadmin.exit_status:
409            raise TargetInitializationFailure("Was not able to create a 'fsfs' "
410                                              "svn repository at %r" %
411                                              target_repository)
412
413        if target_module and target_module <> '/':
414            cmd = [self.repository.SVN_CMD, "mkdir", "-m",
415                   "This directory will host the upstream sources"]
416            svnmkdir = ExternalCommand(command=cmd)
417            svnmkdir.execute(target_repository + target_module)
418            if svnmkdir.exit_status:
419                raise TargetInitializationFailure("Was not able to create the "
420                                                  "module %r, maybe more than "
421                                                  "one level directory?" %
422                                                  target_module)
423
424    def _prepareTargetRepository(self):
425        """
426        Check for target repository existence, eventually create it.
427        """
428
429        if not self.repository.repository:
430            return
431
432        # Verify the existence of repository by listing its root
433        cmd = [self.repository.SVN_CMD, "ls"]
434        svnls = ExternalCommand(command=cmd)
435        svnls.execute(self.repository.repository)
436
437        if svnls.exit_status:
438            if self.repository.repository.startswith('file:///'):
439                self.__createRepository(self.repository.repository,
440                                        self.repository.module)
441            else:
442                raise TargetInitializationFailure("%r does not exist and "
443                                                  "cannot be created since "
444                                                  "it's not a local (file:///) "
445                                                  "repository" %
446                                                  self.repository.repository)
447
448    def _prepareWorkingDirectory(self, source_repo):
449        """
450        Checkout a working copy of the target SVN repository.
451        """
452
453        from os.path import join, exists
454
455        if not self.repository.repository or exists(join(self.basedir, '.svn')):
456            return
457
458        cmd = [self.repository.SVN_CMD, "co", "--quiet"]
459        svnco = ExternalCommand(command=cmd)
460        svnco.execute("%s%s" % (self.repository.repository,
461                                self.repository.module), self.basedir)
462
463    def _initializeWorkingDir(self):
464        """
465        Add the given directory to an already existing svn working tree.
466        """
467
468        from os.path import exists, join
469
470        if not exists(join(self.basedir, '.svn')):
471            raise TargetInitializationFailure("'%s' needs to be an SVN working copy already under SVN" % self.basedir)
472
473        SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.