1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """Internal module providing patchlog parsing.
19
20 This module implements some of public interface for the
21 pybaz_ package. But for convenience reasons the author prefers
22 to store this code in a file separate from ``__init__.py``.
23
24 .. _pybaz: pybaz-module.html
25
26 This module is strictly internal and should never be used.
27 """
28
29 import os
30 import email
31 import email.Parser
32
33 import errors
34 from pathname import FileName
35 from _nameparser import NameParser
36 from _escaping import name_unescape
37 from deprecation import deprecated_callable
38
39 __all__ = [
40 'Patchlog',
41 ]
42
43
47
51
55
56
58
59 """Log entry associated to a revision.
60
61 May be produced by `Revision.patchlog` or `ArchSourceTree.iter_logs()`. It
62 provides an extensive summary of the associated changeset, a natural
63 language description of the changes, and any number of user-defined
64 extension headers.
65
66 Patchlogs are formatted according to RFC-822, and are parsed using the
67 standard email-handling facilities.
68
69 Note that the patchlog text is not actually parsed before it is needed.
70 That deferred evaluation feature is implemented in the `_parse` method.
71
72 The fundamental accessors are `__getitem__`, which give the text of a named
73 patchlog header, and the `description` property which gives the text of the
74 patchlog body, that is anything after the headers.
75
76 Additional properties provide appropriate standard conversion of the
77 standard headers.
78 """
79
80 - def __init__(self, revision, tree=None, fromlib=False):
81 """Patchlog associated to the given revision.
82
83 The patchlog may be retrieved from the provided ``tree``, from the
84 revision library if ``fromlib`` is set, or from the archive.
85
86 :param tree: source tree to retrieve the patchlog from.
87 :type tree: `ArchSourceTree`, None
88 :param fromlib: retrieve the patchlog from the revision library.
89 :type fromlib: bool
90 :raise ValueError: tree and fromlib are both set.
91 """
92 if tree is not None and fromlib:
93 raise ValueError("both tree and fromlib arguments are set.")
94 if _factory().isRevision(revision):
95 self.__revision = revision.fullname
96 elif _is_unsafe(revision):
97 self.__revision, = revision
98 else:
99 p = NameParser(revision)
100 if not p.has_archive() or not p.has_patchlevel:
101 raise errors.NamespaceError(revision,
102 'fully-qualified revision')
103 self.__revision = revision
104 if tree is None:
105 self.__tree = None
106 else:
107 self.__tree = str(tree)
108 self.__fromlib = bool(fromlib)
109 self.__email = None
110
111 - def tree(self): return self.__tree
112 tree = property(tree)
113
114 - def fromlib(self): return self.__fromlib
115 fromlib = property(fromlib)
116
118 fromtree = self.tree is not None
119 fromlib = self.fromlib
120 if not fromtree and not fromlib:
121 return 'Patchlog(%r)' % (self.__revision,)
122 elif fromtree and not fromlib:
123 return 'Patchlog(%r, tree=%r)' % (self.__revision, self.tree)
124 elif not fromtree and fromlib:
125 return 'Patchlog(%r, fromlib=True)' % (self.__revision,)
126 else:
127 raise AssertionError
128
146
148 """Text of a patchlog header by name.
149
150 :param header: name of a patchlog header.
151 :type header: str
152 :return: text of the header, or None if the header is not present.
153 :rtype: str, None
154 """
155 return self._parse()[header]
156
158 """List of 2-tuples containing all headers and values.
159
160 :rtype: list of 2-tuple of str
161 """
162 return self._parse().items()
163
174
176 assert self.__revision == self['Archive']+'/'+self['Revision']
177 return _factory().Revision(self.__revision)
178
179 revision = property(_get_revision, doc="""
180 Revision associated to this patchlog.
181
182 :type: `Revision`
183 """)
184
186 """Deprecated.
187
188 Patchlog summary, a one-line natural language description.
189
190 :rtype: str
191 :see: `Patchlog.summary`
192 """
193 deprecated_callable(self.get_summary, (Patchlog, 'summary'))
194 return self.summary
195
197 return self['Summary']
198
199 summary = property(_get_summary, doc="""
200 Patchlog summary, a one-line natural language description.
201
202 :type: str
203 """)
204
215
217 m = self._parse()
218 assert not m.is_multipart()
219 return m.get_payload()
220
221 description = property(_get_description, doc="""
222 Patchlog body, a long natural language description.
223
224 :type: str
225 """)
226
228 """Deprecated.
229
230 For the description of the local time tuple, see the
231 documentation of the `time` module.
232
233 :rtype: local time tuple
234 :see: `Patchlog.date`
235 """
236 deprecated_callable(self.get_date, (Patchlog, 'date'))
237 return self.date
238
240 d = self['Standard-date']
241 from time import strptime
242 return strptime(d, '%Y-%m-%d %H:%M:%S %Z')
243
244 date = property(_get_date, doc="""
245 Time of the associated revision.
246
247 For the description of the local time tuple, see the documentation
248 of the `time` module.
249
250 :type: local time tuple
251 """)
252
254 """Deprecated.
255
256 User id of the the creator of the associated revision.
257
258 :rtype: str
259 :see: `Patchlog.creator`
260 """
261 deprecated_callable(self.get_creator, (Patchlog, 'creator'))
262 return self.creator
263
265 return self['Creator']
266
267 creator = property(_get_creator, doc="""
268 User id of the the creator of the associated revision.
269
270 :type: str
271 """)
272
285
290
291 continuation = property(_get_continuation, doc="""
292 Deprecated.
293
294 Ancestor of tag revisions.
295 None for commit and import revisions.
296
297 :type: `Revision`, None.
298 :see: `Patchlog.continuation_of`
299 """)
300
302 c = self['Continuation-of']
303 if c is None:
304 return None
305 return _factory().Revision(c)
306
307 continuation_of = property(_get_continuation_of, doc="""
308 Ancestor of tag revisions.
309 None for commit and import revisions.
310
311 :type: `Revision`, None.
312 """)
313
325
327 patches = self['New-patches'].split()
328 if '!!!!!nothing-should-depend-on-this' in patches:
329 patches.remove('!!!!!nothing-should-depend-on-this')
330 patches.append("%s" % self.revision)
331 return map(_factory().Revision, patches)
332
333 new_patches = property(_get_new_patches, doc="""
334 New-patches header as an iterable of Revision.
335
336 Patchlogs added in this revision.
337
338 :type: iterable of `Revision`
339 """)
340
342 """Deprecated.
343
344 Revisions merged in this revision. That is the revisions
345 listed in the New-patches header except the revision of the
346 patchlog.
347
348 :rtype: iterable of `Revision`
349 :see: `Patchlog.merged_patches`
350 """
351 deprecated_callable(
352 Patchlog.get_merged_patches, (Patchlog, 'merged_patches'))
353 return self.merged_patches
354
358
359 merged_patches = property(_get_merged_patches, doc="""
360 Revisions merged in this revision.
361
362 That is the revisions listed in the New-patches header except the
363 revision of the patchlog.
364
365 :type: iterable of `Revision`
366 """)
367
369 """Deprecated.
370
371 Source files added in the associated revision.
372
373 :rtype: iterable of `FileName`
374 :see: `Patchlog.new_files`
375 """
376 deprecated_callable(self.get_new_files, (Patchlog, 'new_files'))
377 return self.new_files
378
380 s = self['New-files']
381 if s is None: return []
382 return [ FileName(name_unescape(f)) for f in s.split() ]
383
384 new_files = property(_get_new_files, doc="""
385 Source files added in the associated revision.
386
387 :type: iterable of `FileName`
388 """)
389
400
402 s = self['Modified-files']
403 if s is None: return []
404 return [ FileName(name_unescape(f)) for f in s.split() ]
405
406 modified_files = property(_get_modified_files, doc="""
407 Names of source files modified in the associated revision.
408
409 :type: iterable of `FileName`
410 """)
411
413 """Deprecated.
414
415 Source files renames in the associated revision.
416
417 Dictionnary whose keys are old names and whose values are the
418 corresponding new names. Explicit file ids are listed in
419 addition to their associated source file.
420
421 :rtype: dict
422 """
423 deprecated_callable(
424 self.get_renamed_files, (Patchlog, 'renamed_files'))
425 return self.renamed_files
426
436
437 renamed_files = property(_get_renamed_files, doc="""
438 Source files renames in the associated revision.
439
440 Dictionnary whose keys are old names and whose values are the
441 corresponding new names. Explicit file ids are listed in
442 addition to their associated source file.
443
444 :type: dict
445 """)
446
457
459 s = self['Removed-files']
460 if s is None: return []
461 return [ FileName(name_unescape(f)) for f in s.split() ]
462
463 removed_files = property(_get_removed_files, doc="""
464 Names of source files removed in the associated revision.
465
466 :type: iterable of `FileName`
467 """)
468