1
# arch-tag: 4ccb7253-910d-41a0-8653-d1197af1601a
2
# Copyright (C) 2004 David Allouche <david@allouche.net>
3
# 2005 Canonical Limited.
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
Internal module providing output parsing functionality.
22
This module implements some of public interface for the
23
arch_ package. But for convenience reasons the author prefers
24
to store this code in a file separate from ``__init__.py`` .
26
.. _arch: arch-module.html
28
This module is strictly internal and should never be used.
33
from pathname import *
34
from _escaping import *
42
class Chatter(object):
43
"""Chatter lines in ``tla`` output.
45
:ivar text: chatter text without the ``"* "`` prefix.
49
def __init__(self, text):
50
"""Create a chatter item.
52
:param text: tla-style chatter line, starting with ``"* "``
55
assert text[:2] == '* '
59
"""tla-style chatter line with ``"* "`` prefix."""
60
return "* " + self.text
63
def classify_chatter(iter):
64
"""Classify chatter in a sequence of strings.
66
Generator that yields Chatter objects for chatter lines, and
67
yields other lines verbatim.
69
:param iter: iterable of str
70
:rtype: iterable of `Chatter` or str
73
if line.startswith("* "):
84
class MergeOutcome(object):
85
"""Abstract base class for changeset application summary output lines.
87
:ivar name: name of the corresponding file
88
:type name: `FileName` or `DirName`
89
:ivar directory: does the change applies to a directory?
93
def _parse(self, text, pad, filepre, dirpre=None):
95
assert 2 == len(filepre)
96
assert dirpre is None or 2 == len(dirpre)
97
prefix = text[0:2+len(pad)]
98
if prefix == filepre+pad:
99
self._directory = False
100
elif dirpre is not None and prefix == dirpre+pad:
101
self._directory = True
103
raise AssertionError('bad prefix: %r' % prefix)
104
name = name_unescape(text[len(prefix):])
105
self._name = (FileName, DirName)[self._directory](name)
108
"""tla-style changeset application line."""
109
raise NotImplementedError
111
def _to_str(self, filepre, dirpre=None):
113
return ''.join((dirpre, self._pad, name_escape(self.name)))
115
return ''.join((filepre, self._pad, name_escape(self.name)))
117
def _get_directory(self):
118
return self._directory
119
directory = property(_get_directory)
123
name = property(_get_name)
126
class TreeChange(MergeOutcome):
127
"""Abstract base class for ``changes`` summary output lines."""
130
"""tla-style change line,"""
131
raise NotImplementedError
138
'FilePermissionsChange',
140
'SymlinkModification',
143
class FileAddition(TreeChange):
144
"""Changeset summary line for a new file.
146
:ivar name: name of the added file or directory.
147
:ivar directory: is ``name`` a directory name?
150
def __init__(self, text, pad):
151
self._parse(text, pad, 'A ', 'A/')
154
return self._to_str('A ', 'A/')
157
class FileDeletion(TreeChange):
158
"""Changeset summary line for a deleted file.
160
:ivar name: name of the deleted file or directory.
161
:ivar directory: is ``name`` a directory name?
164
def __init__(self, text, pad):
165
self._parse(text, pad, 'D ', 'D/')
168
return self._to_str('D ', 'D/')
171
class FileModification(TreeChange):
172
"""Changeset summary line for file whose contents were modified.
174
:ivar name: name of the modified file.
175
:ivar binary: is ``name`` a binary file?
177
:ivar directory: always False
180
def __init__(self, text, pad):
181
self._directory = False
183
prefix = text[0:2+len(pad)]
184
if prefix == 'M '+pad: self.binary = False
185
elif prefix == 'Mb'+pad: self.binary = True
186
else: raise AssertionError('bad prefix: %r' % prefix)
187
name = name_unescape(text[len(prefix):])
188
self._name = FileName(name)
192
return 'Mb%s%s' % (self._pad, name_escape(self.name))
194
return 'M %s%s' % (self._pad, name_escape(self.name))
197
class FilePermissionsChange(TreeChange):
198
"""Changeset summary line for a change in permissions.
200
:ivar name: name of the file or directory whose permissions changed.
201
:ivar directory: is ``name`` a directory name?
204
def __init__(self, text, pad):
205
if text.startswith('--/'):
207
self._parse(text, pad, '--', '-/')
210
return self._to_str('--', '-/')
213
class FileRename(TreeChange):
214
"""Changeset summary line for a renaming.
216
:ivar name: new name of the moved file or directory.
217
:ivar oldname: old name of the moved file or directory.
218
:type oldname: `FileName`, `DirName`
219
:ivar directory: are ``name`` and ``oldname`` directory names?
222
def __init__(self, text, pad):
224
prefix = text[0:2+len(pad)]
225
if prefix == '=>'+pad:
226
self._directory = False
227
elif prefix == '/>'+pad:
228
self._directory = True
230
raise AssertionError('bad prefix: %r' % prefix)
231
oldnew = text[len(prefix):].split('\t')
232
assert len(oldnew) == 2
233
ctor = (FileName, DirName)[self.directory]
234
self.oldname, self._name = map(ctor, map(name_unescape, oldnew))
238
format = '/>%s%s\t%s'
240
format = '=>%s%s\t%s'
241
names = map(name_escape, (self.oldname, self.name))
242
return format % ((self._pad,) + tuple(names))
245
class SymlinkModification(TreeChange):
246
"""Changeset summary line for a symlink modification.
248
:ivar name: name of the modified symbolic link.
249
:ivar directory: always False
252
def __init__(self, text, pad):
253
self._parse(text, pad, '->')
256
return self._to_str('->')
260
'classify_changeset_creation',
262
'classify_changeset_application',
265
def classify_changeset_creation(lines, pad=' '):
266
"""Classify the output of a changeset creation command.
268
:param lines: incremental output from a changeset creation command.
269
:type lines: iterator of str
270
:param pad: padding text between prefix and file name.
272
:rtype: Iterable of `Chatter`, `TreeChange`, str
274
:note: diff output (*e.g.* ``changes --diffs``) is not supported.
276
for line in classify_chatter(lines):
277
if isinstance(line, Chatter) or len(line) < 2:
279
elif line[:2] in ('A ', 'A/'):
280
yield FileAddition(line, pad)
281
elif line[:2] in ('D ', 'D/'):
282
yield FileDeletion(line, pad)
283
elif line[:2] in ('M ', 'Mb'):
284
yield FileModification(line, pad)
285
elif line[:2] in ('--', '-/', '--/'):
286
yield FilePermissionsChange(line, pad)
287
elif line[:2] in ('=>', '/>'):
288
yield FileRename(line, pad)
289
elif line[:2] == '->':
290
yield SymlinkModification(line, pad)
295
class PatchConflict(MergeOutcome):
296
"""Changeset application summary line for a patch conflict.
298
:ivar name: name of the file where the patch conflict occurred.
299
:ivar directory: always False
301
def __init__(self, text):
302
self._parse(text, ' ', 'C ')
305
return self._to_str('C ')
308
def classify_changeset_application(lines):
309
"""Classify the output of a change-producing command.
311
:param lines: incremental output from a changeset application command.
312
:type lines: iterable of str
313
:rtype: iterable of `Chatter`, `MergeOutcome`, str
315
:note: diff output (*e.g.* ``changes --diffs``) is not supported.
317
for line in classify_changeset_creation(lines, pad=' '):
318
if isinstance(line, (Chatter, TreeChange)) or len(line) < 2:
320
elif line[:3] == 'C ':
321
yield PatchConflict(line)