source: tailor/vcpx/darcs.py @ 425

Revision 425, 15.0 KB checked in by lele@…, 8 years ago (diff)

Was still considering output a StringIO object

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Darcs details
3# :Creato:   ven 18 giu 2004 14:45:28 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains supporting classes for the ``darcs`` versioning system.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import ExternalCommand, PIPE, STDOUT
15from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
16     GetUpstreamChangesetsFailure
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18from xml.sax import SAXException
19
20DARCS_CMD = 'darcs'
21
22MOTD = """\
23This is the Darcs equivalent of
24%s/%s
25"""
26
27def changesets_from_darcschanges(changes, unidiff=False, repodir=None):
28    """
29    Parse XML output of ``darcs changes``.
30
31    Return a list of ``Changeset`` instances.
32    """
33   
34    from xml.sax import parse
35    from xml.sax.handler import ContentHandler
36    from changes import ChangesetEntry, Changeset
37    from datetime import datetime
38
39    class DarcsXMLChangesHandler(ContentHandler):
40        def __init__(self):
41            self.changesets = []
42            self.current = None
43            self.current_field = []
44            if unidiff and repodir:
45                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
46                       "--patch", "%(patchname)s"]
47                self.darcsdiff = ExternalCommand(command=cmd)
48            else:
49                self.darcsdiff = None
50               
51        def startElement(self, name, attributes):
52            if name == 'patch':
53                self.current = {}
54                self.current['author'] = attributes['author']
55                date = attributes['date']
56                # 20040619130027
57                y = int(date[:4])
58                m = int(date[4:6])
59                d = int(date[6:8])
60                hh = int(date[8:10])
61                mm = int(date[10:12])
62                ss = int(date[12:14])
63                timestamp = datetime(y, m, d, hh, mm, ss)
64                self.current['date'] = timestamp
65                self.current['comment'] = ''
66                self.current['entries'] = []
67            elif name in ['name', 'comment',
68                          'add_file', 'add_directory',
69                          'modify_file', 'remove_file']:
70                self.current_field = []
71            elif name == 'move':
72                self.old_name = attributes['from']
73                self.new_name = attributes['to']
74
75        def endElement(self, name):
76            if name == 'patch':
77                # Sort the paths to make tests easier
78                self.current['entries'].sort(lambda x,y: cmp(x.name, y.name))
79                cset = Changeset(self.current['name'],
80                                 self.current['date'],
81                                 self.current['author'],
82                                 self.current['comment'],
83                                 self.current['entries'])
84               
85                if self.darcsdiff:
86                    cset.unidiff = self.darcsdiff.execute(
87                        stdout=PIPE, patchname=cset.revision).read()
88                   
89                self.changesets.append(cset)
90                self.current = None
91            elif name in ['name', 'comment']:
92                self.current[name] = ''.join(self.current_field)
93            elif name == 'move':
94                entry = ChangesetEntry(self.new_name)
95                entry.action_kind = entry.RENAMED
96                entry.old_name = self.old_name
97                self.current['entries'].append(entry)
98            elif name in ['add_file', 'add_directory',
99                          'modify_file', 'remove_file']:
100                entry = ChangesetEntry(''.join(self.current_field).strip())
101                entry.action_kind = { 'add_file': entry.ADDED,
102                                      'add_directory': entry.ADDED,
103                                      'modify_file': entry.UPDATED,
104                                      'remove_file': entry.DELETED,
105                                      'rename_file': entry.RENAMED
106                                    }[name]
107
108                self.current['entries'].append(entry)
109
110        def characters(self, data):
111            self.current_field.append(data)
112
113
114    handler = DarcsXMLChangesHandler()
115    parse(changes, handler)
116    changesets = handler.changesets
117   
118    # sort changeset by date
119    changesets.sort(lambda x, y: cmp(x.date, y.date))
120
121    return changesets
122
123   
124class DarcsWorkingDir(UpdatableSourceWorkingDir,SyncronizableTargetWorkingDir):
125    """
126    A working directory under ``darcs``.
127    """
128
129    ## UpdatableSourceWorkingDir
130   
131    def getUpstreamChangesets(self, root, repository, module, sincerev=None):
132        """
133        Do the actual work of fetching the upstream changeset.
134        """
135
136        from datetime import datetime
137        from time import strptime
138        from changes import Changeset
139       
140        cmd = [DARCS_CMD, "pull", "--dry-run"]
141        pull = ExternalCommand(cwd=root, command=cmd)
142        output = pull.execute(repository, stdout=PIPE, stderr=STDOUT, TZ='UTC')
143       
144        if pull.exit_status:
145            raise GetUpstreamChangesetsFailure(
146                "%s returned status %d saying \"%s\"" %
147                (str(pull), pull.exit_status, output.read()))
148
149        l = output.readline()
150        while l and not (l.startswith('Would pull the following changes:') or
151                         l == 'No remote changes to pull in!\n'):
152            l = output.readline()
153
154        changesets = []
155
156        if l <> 'No remote changes to pull in!\n':
157            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
158            ##   * Refix _getUpstreamChangesets for darcs
159
160            l = output.readline()
161            while not l.startswith('Making no changes:  this is a dry run.'):
162                # Assume it's a line like
163                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
164                # we used to split on the double space before the email,
165                # but in this case this is wrong. Waiting for xml output,
166                # is it really sane asserting date's length to 28 chars?
167                date = l[:28]
168                author = l[30:-1]
169                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
170                date = datetime(y,m,d,hh,mm,ss)
171                l = output.readline()
172                assert (l.startswith('  * ') or
173                        l.startswith('  UNDO:') or
174                        l.startswith('  tagged'))
175
176                if l.startswith('  *'):
177                    name = l[4:-1]
178                else:
179                    name = l[2:-1]
180
181                changelog = []
182                l = output.readline()
183                while l.startswith(' '):
184                    changelog.append(l.strip())
185                    l = output.readline()
186
187                changesets.append(Changeset(name, date, author, '\n'.join(changelog)))
188
189                while not l.strip():
190                    l = output.readline()
191
192        return changesets
193   
194    def _applyChangeset(self, root, changeset, logger=None):
195        """
196        Do the actual work of applying the changeset to the working copy.
197        """
198
199        if changeset.revision.startswith('tagged '):
200            selector = '--tags'
201            revtag = changeset.revision[7:]
202        else:
203            selector = '--patches'
204            revtag = changeset.revision
205           
206        cmd = [DARCS_CMD, "pull", "--all", selector, revtag]
207        pull = ExternalCommand(cwd=root, command=cmd)
208        output = pull.execute(stdout=PIPE, stderr=STDOUT)
209       
210        if pull.exit_status:
211            raise ChangesetApplicationFailure(
212                "%s returned status %d saying \"%s\"" %
213                (str(pull), pull.exit_status, output.read()))
214
215        cmd = [DARCS_CMD, "changes", selector, revtag,
216               "--xml-output", "--summ"]
217        changes = ExternalCommand(cwd=root, command=cmd)
218        last = changesets_from_darcschanges(changes.execute(stdout=PIPE))
219        if last:
220            changeset.entries.extend(last[0].entries)
221
222    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
223                                  subdir=None, logger=None, **kwargs):
224        """
225        Concretely do the checkout of the upstream revision.
226        """
227
228        from os.path import join, exists
229        from os import mkdir
230
231        if subdir == '.':
232            # This is currently *very* slow, compared to the darcs get
233            # below!
234            wdir = basedir
235            if not exists(join(wdir, '_darcs')):
236                if not exists(wdir):
237                    mkdir(wdir)
238
239                init = ExternalCommand(cwd=wdir,
240                                       command=[DARCS_CMD, "initialize"])
241                init.execute(stdout=PIPE)
242
243                if init.exit_status:
244                    raise TargetInitializationFailure(
245                        "%s returned status %s" % (str(init),
246                                                   init.exit_status))
247
248                cmd = [DARCS_CMD, "pull", "--all", "--verbose"]
249                if revision and revision<>'HEAD':
250                    cmd.extend(["--tag", revision])
251                dpull = ExternalCommand(cwd=wdir, command=cmd)
252                output = dpull.execute(repository, stdout=PIPE, stderr=STDOUT)
253                       
254                if dpull.exit_status:
255                    raise TargetInitializationFailure(
256                        "%s returned status %d saying \"%s\"" %
257                        (str(dpull), dpull.exit_status, output.read()))
258        else:
259            # Use much faster 'darcs get'
260           
261            wdir = join(basedir, subdir)
262            cmd = [DARCS_CMD, "get", "--partial", "--verbose"]
263            if revision and revision<>'HEAD':
264                cmd.extend(["--tag", revision])
265            dget = ExternalCommand(cwd=basedir, command=cmd)
266            output = dget.execute(repository, subdir,
267                                  stdout=PIPE, stderr=STDOUT)
268           
269            if dget.exit_status:
270                raise TargetInitializationFailure(
271                    "%s returned status %d saying \"%s\"" %
272                    (str(dget), dget.exit_status, output.read()))
273
274        cmd = [DARCS_CMD, "changes", "--last", "1", "--xml-output"]
275        changes = ExternalCommand(cwd=wdir, command=cmd)
276        output = changes.execute(stdout=PIPE, stderr=STDOUT)
277       
278        if changes.exit_status:
279            raise ChangesetApplicationFailure(
280                "%s returned status %d saying \"%s\"" %
281                (str(changes), changes.exit_status, output.read()))
282       
283        last = changesets_from_darcschanges(output)
284       
285        return last[0].revision
286
287   
288    ## SyncronizableTargetWorkingDir
289   
290    def _addPathnames(self, root, names):
291        """
292        Add some new filesystems objects.
293        """
294
295        cmd = [DARCS_CMD, "add", "--case-ok", "--not-recursive", "--quiet"]
296        ExternalCommand(cwd=root, command=cmd).execute(names)
297       
298    def _addSubtree(self, root, subdir):
299        """
300        Use the --recursive variant of ``darcs add`` to add a subtree.
301        """
302
303        cmd = [DARCS_CMD, "add", "--case-ok", "--recursive", "--quiet"]
304        ExternalCommand(cwd=root, command=cmd).execute(subdir)
305       
306    def _commit(self, root, date, author, remark, changelog=None, entries=None):
307        """
308        Commit the changeset.
309        """
310
311        input = "%s UTC\n%s\n%s\n%s\n" % (date.strftime('%Y/%m/%d %H:%M:%S'),
312                                          author,
313                                          remark,
314                                          changelog or '')
315       
316        cmd = [DARCS_CMD, "record", "--all", "--pipe"]
317        if not entries:
318            entries = ['.']
319           
320        record = ExternalCommand(cwd=root, command=cmd)
321        record.execute(entries, input=input, stdout=PIPE)
322       
323        if record.exit_status:
324            raise ChangesetApplicationFailure(
325                "%s returned status %d" % (str(record), record.exit_status))
326       
327    def _removePathnames(self, root, names):
328        """
329        Remove some filesystem object.
330        """
331
332        # Since the source VCS already deleted the entry, and given that
333        # darcs will do the right thing with it, do nothing here, instead
334        # of
335        #         c = ExternalCommand(cwd=root,
336        #                             command=[DARCS_CMD, "remove"])
337        #         c.execute(entries)
338        # that raises status 512 on darcs not finding the entry.
339
340        pass
341   
342    def _renamePathname(self, root, oldname, newname):
343        """
344        Rename a filesystem object.
345        """
346
347        from os.path import join, exists
348        from os import rename
349       
350        # Check to see if the oldentry is still there. If it does,
351        # that probably means one thing: it's been moved and then
352        # replaced, see svn 'R' event. In this case, rename the
353        # existing old entry to something else to trick "darcs mv"
354        # (that will assume the move was already done manually) and
355        # finally restore its name.
356
357        renamed = exists(join(root, oldname))
358        if renamed:
359            rename(oldname, oldname + '-TAILOR-HACKED-TEMP-NAME')
360
361        try:
362            cmd = [DARCS_CMD, "mv"]
363            ExternalCommand(cwd=root, command=cmd).execute(oldname, newname)
364        finally:
365            if renamed:
366                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
367
368    def _initializeWorkingDir(self, root, repository, module, subdir):
369        """
370        Execute ``darcs initialize`` and tweak the default settings of
371        the repository, then add the whole subtree.
372        """
373
374        from os.path import join
375        from re import escape
376        from dualwd import IGNORED_METADIRS
377       
378        init = ExternalCommand(cwd=root, command=[DARCS_CMD, "initialize"])
379        init.execute(stdout=PIPE)
380
381        if init.exit_status:
382            raise TargetInitializationFailure(
383                "%s returned status %s" % (str(init), init.exit_status))
384
385        motd = open(join(root, '_darcs/prefs/motd'), 'w')
386        motd.write(MOTD % (repository, module))
387        motd.close()
388
389        # Remove .cvsignore from default boring file
390        boring = open(join(root, '_darcs/prefs/boring'), 'r')
391        ignored = [line for line in boring if line <> '\.cvsignore$\n']
392        boring.close()
393       
394        # Augment the boring file, that contains a regexp per line
395        # with all known VCs metadirs to be skipped.
396        boring = open(join(root, '_darcs/prefs/boring'), 'w')
397        boring.write(''.join(ignored))
398        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
399                                for md in IGNORED_METADIRS]))
400        boring.write('\n^tailor.log$\n^tailor.info$\n')
401        boring.close()
402       
403        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root,
404                                                            repository, module,
405                                                            subdir)
Note: See TracBrowser for help on using the repository browser.