source: tailor/vcpx/svn.py @ 424

Revision 424, 13.3 KB checked in by lele@…, 8 years ago (diff)

Factored out the call to svn info

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