source: tailor/vcpx/svn.py @ 393

Revision 393, 13.0 KB checked in by lele@…, 8 years ago (diff)

Transition to a Python 2.4 subprocess compatible way of executing external tools
The shwrap module now makes use of the new subprocess module available
with Python 2.4; under older snakes it uses an almost compatible module,
_process.py. This does not bring in any new functionality, it should just
make it easier to port the tool under other OS, and hopefully use a less
hackish way of doing the task.

BEWARE: even if I did several round trips over the various backends, there
may still be bugs in the way I translated the external commands.

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