ViewVC Help
View File | Revision Log | Show Annotations | View Changeset | Root Listing
root/ozone/mgar/pkg/py311/gar/bin/cswch
Revision: 11
Committed: Sat Jun 20 21:40:57 2026 UTC (4 weeks, 2 days ago) by operz
File size: 41728 byte(s)
Log Message:
Add Python 3.11.15 recipe (OZSWpy311); update readline to link against ncurses (for Python curses module)

File Contents

# Content
1 #!/usr/bin/env python
2 """Maintain OpenCSW changelog.
3
4 cswch can maintain two styles of changelog files, namely Debian style
5 changelog files and Dam style changelog files.
6
7 Debian style changelog files are modeled after the Debian changelog
8
9 http://www.debian.org/doc/debian-policy/ch-source.html#s-dpkgchangelog
10
11 except that the header line for each entry is somewhat
12 simplyfied. Instead of using
13
14 package (version) distribution(s); urgency=urgency
15
16 it uses
17
18 package (version,REV=revision)
19
20
21 Dam style changelog files are similar to Debian style changelog files,
22 except for the header line, which looks like
23
24 package (timestamp)
25
26
27 Creating a new changelog file is done by
28
29 cswch create [--dam-style|--deb-style --version <version>] <pkg> \
30 --log-item <item1> .. <itemN> [--log-paragraph <par1> .. <parN>]
31
32 This will create a changelog.CSW file in the files/ directory and add
33 it to SVN working copy.
34
35
36 After creating the changelog file, adding a new changelog entry is a
37 matter of
38
39 cswch new [--version <version>] --log-item <item1> .. <itemN> \
40 [--log-paragraph <par1> .. <parN>]
41
42 where `--version' is only required for Debian style changelogs.
43
44 See
45
46 cswch --help
47
48 for more information.
49
50 $Id: cswch 23777 2014-06-10 07:35:18Z guengel $
51
52 """
53 import ConfigParser
54 import argparse
55 import datetime
56 import dateutil.tz
57 import email.utils
58 import errno
59 import logging
60 import os
61 import pwd
62 import pysvn
63 import re
64 import sys
65 import time
66 import unittest
67
68 ### T E S T S ###
69
70 class TestOpenCSWRev(unittest.TestCase):
71 """Test opencsw_rev()"""
72
73 def test_opencsw_rev(self):
74 """revision string from date"""
75 rev = ChangeLogHeader.opencsw_rev(datetime.date(1979, 1, 1))
76 self.assertEqual(rev, '1979.01.01')
77
78 def test_opencsw_rev_default(self):
79 """revision from current date"""
80 rev = ChangeLogHeader.opencsw_rev()
81 self.assertEqual(rev, datetime.date.today().strftime('%Y.%m.%d'))
82
83
84 class TestChangeLogHeader(unittest.TestCase):
85 """Test ChangeLogHeader"""
86
87 def test_deb_changelogheader(self):
88 """initialize debian changelog header with keywords"""
89 chlg = ChangeLogHeader(package='test', version='0.1', rev='1991.09.04')
90 self.assertEqual(ChangeLogHeader.STYLE_DEB, chlg.style)
91 self.assertEqual('test (0.1,REV=1991.09.04)', str(chlg))
92
93 def test_deb_parser(self):
94 """initialize debian changelog header from line"""
95 chlg = ChangeLogHeader(line='test (0.1,REV=1991.09.04)')
96 self.assertEqual(ChangeLogHeader.STYLE_DEB, chlg.style)
97 self.assertEqual('test (0.1,REV=1991.09.04)', str(chlg))
98
99 def test_dam_changelogheader(self):
100 """initialize dam changelog header with keywords"""
101 ts = '2014-01-01T00:01:02+0000'
102 package = 'testdam'
103 header_str = ChangeLogHeader.format_str_dam % {'package': package,
104 'timestamp': ts}
105 chlg = ChangeLogHeader(package=package, timestamp=ts)
106 self.assertEqual(ChangeLogHeader.STYLE_DAM, chlg.style)
107 self.assertEqual(header_str, str(chlg))
108
109 def test_dam_parser(self):
110 """initialize dam changelog header from line"""
111 line = 'testdam (2014-01-01T00:01:02+0300)'
112 chlg = ChangeLogHeader(line=line)
113 self.assertEqual(ChangeLogHeader.STYLE_DAM, chlg.style)
114
115 def test_garbled(self):
116 """test garbled changelog header"""
117 self.assertRaises(GarbledChangeLogHeader,
118 ChangeLogHeader, line='test (0,REV=1991.9.4)')
119
120
121 class TestChangeLogFooter(unittest.TestCase):
122 """Test ChangeLogFooter"""
123
124 def test_changelogfooter(self):
125 """initialize changelog footer from arguments"""
126 chlg = ChangeLogFooter(maintainer="Rafael Ostertag",
127 email="raos@opencsw.org",
128 timestamp=datetime.datetime(1979, 1, 1,
129 0, 0, 0))
130 self.assertEqual(' -- Rafael Ostertag <raos@opencsw.org> Mon, 01 Jan 1979 00:00:00 +0100', str(chlg))
131
132 def test_parser(self):
133 """initialize changelog footer from line"""
134 test_footer = ' -- Rafael Ostertag <raos@opencsw.org> Sun, 16 Mar 2014 10:10:33 +0100'
135 chlg = ChangeLogFooter(line=test_footer)
136 self.assertEqual(test_footer, str(chlg))
137
138 def test_garbled(self):
139 """test garbled changelog footer"""
140 self.assertRaises(GarbledChangeLogFooter,
141 ChangeLogFooter, line=' -- Rafael Ostertag <raos@opencsw.org> Sun, 16 Mar 2014 10:10:33 +010')
142
143
144 class TestChangeLogParagraph(unittest.TestCase):
145 """Test ChangeLogParagraph."""
146
147 def test_init_single_line_bullet(self):
148 """initialize ChangeLog paragraph with one line"""
149 line = ' * Lorem ipsum dolor sit amet, consectetur adipiscing'
150 chpg = ChangeLogParagraph(line)
151 self.assertEqual(line, str(chpg))
152 self.assertEqual('Lorem ipsum dolor sit amet, consectetur adipiscing',
153 repr(chpg))
154
155 def test_init_single_line_plain(self):
156 """initialize ChangeLog paragraph with one line"""
157 line = ' Lorem ipsum dolor sit amet, consectetur adipiscing'
158 chpg = ChangeLogParagraph(line)
159 self.assertEqual(line, str(chpg))
160 self.assertEqual('Lorem ipsum dolor sit amet, consectetur adipiscing',
161 repr(chpg))
162
163 def test_line_add_bullet(self):
164 """Test adding a new line to an existing paragraph"""
165 line = " * short line"
166 chpg = ChangeLogParagraph(line)
167
168 chpg.add_line("and another line appended")
169
170 self.assertEqual('short line and another line appended', repr(chpg))
171 self.assertEqual(' * short line and another line appended',
172 str(chpg))
173
174 def test_line_add_plain(self):
175 """Test adding a new line to an existing paragraph"""
176 line = " short line"
177 chpg = ChangeLogParagraph(line)
178
179 chpg.add_line("and another line appended")
180
181 self.assertEqual('short line and another line appended', repr(chpg))
182 self.assertEqual(' short line and another line appended',
183 str(chpg))
184
185
186 def test_newline_handling_bullet(self):
187 """test newline handling of ChangeLogParagraph"""
188 lines = """ * Lorem ipsum dolor sit amet,
189 consectetur adipiscing"""
190 chpg = ChangeLogParagraph(lines)
191 self.assertEqual(' * Lorem ipsum dolor sit amet, consectetur adipiscing', str(chpg))
192 self.assertEqual('Lorem ipsum dolor sit amet, consectetur adipiscing', repr(chpg))
193
194 def test_newline_handling_plain(self):
195 """test newline handling of ChangeLogParagraph"""
196 lines = """ Lorem ipsum dolor sit amet,
197 consectetur adipiscing"""
198 chpg = ChangeLogParagraph(lines)
199 self.assertEqual(' Lorem ipsum dolor sit amet, consectetur adipiscing', str(chpg))
200 self.assertEqual('Lorem ipsum dolor sit amet, consectetur adipiscing', repr(chpg))
201
202 def test_init_from_multiple_lines(self):
203 """Test initialization from multiple lines"""
204
205 lines = [' * Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent posuere est a pellentesque imperdiet.',
206 'Vivamus dapibus enim eu magna vehicula posuere. Pellentesque adipiscing purus id diam auctor gravida. Sed leo odio,',
207 'molestie quis ante sed, commodo consequat nulla. Vivamus interdum vel lorem eget faucibus. Suspendisse dignissim orci sit amet dolor venenatis convallis.',
208 'Suspendisse fringilla tortor vitae dolor blandit ullamcorper. Cras sed mauris eu lorem scelerisque blandit quis vel mauris. Nam a urna aliquet, cursus erat a, fermentum justo. Sed hendrerit dui magna, ac tempus est vestibulum at.',
209 'Duis in massa ut nisl euismod auctor nec ac odio. Phasellus suscipit neque quis metus vestibulum molestie. Phasellus eget molestie elit, et fringilla purus. Morbi augue lectus, mattis ut mauris onvallis, eleifend condimentum nunc. Phasellus sit amet facilisis massa. Cras non porttitor turpis.']
210 formatted = """ * Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent
211 posuere est a pellentesque imperdiet. Vivamus dapibus enim eu magna
212 vehicula posuere. Pellentesque adipiscing purus id diam auctor
213 gravida. Sed leo odio, molestie quis ante sed, commodo consequat
214 nulla. Vivamus interdum vel lorem eget faucibus. Suspendisse
215 dignissim orci sit amet dolor venenatis convallis. Suspendisse
216 fringilla tortor vitae dolor blandit ullamcorper. Cras sed mauris eu
217 lorem scelerisque blandit quis vel mauris. Nam a urna aliquet,
218 cursus erat a, fermentum justo. Sed hendrerit dui magna, ac tempus
219 est vestibulum at. Duis in massa ut nisl euismod auctor nec ac odio.
220 Phasellus suscipit neque quis metus vestibulum molestie. Phasellus
221 eget molestie elit, et fringilla purus. Morbi augue lectus, mattis
222 ut mauris onvallis, eleifend condimentum nunc. Phasellus sit amet
223 facilisis massa. Cras non porttitor turpis."""
224 chpg = ChangeLogParagraph(*lines)
225 self.assertEqual(formatted, str(chpg))
226
227
228 class TestChangeLogEntry(unittest.TestCase):
229 """Test ChangeLogEntry class"""
230
231 def test_entry(self):
232 """Test a simple changelog entry"""
233 clg_entry = ChangeLogEntry()
234 clg_header = ChangeLogHeader(package='cswch', version='0.1')
235 clg_paragraph1 = ChangeLogParagraph(" * One Line")
236 clg_paragraph2 = ChangeLogParagraph(" * Two Lines")
237 clg_footer = ChangeLogFooter()
238
239 clg_entry.add_header(clg_header)
240 clg_entry.add_footer(clg_footer)
241 clg_entry.add_paragraph(clg_paragraph1)
242 clg_entry.add_paragraph(clg_paragraph2)
243
244 expected_string = str(clg_header) + '\n\n' +\
245 str(clg_paragraph1) + '\n\n' +\
246 str(clg_paragraph2) + '\n\n' +\
247 str(clg_footer) + '\n\n\n'
248
249 self.assertEqual(expected_string, str(clg_entry))
250
251
252 ### C O D E ###
253
254 class ChangeLogException(Exception):
255 """Exception Base class"""
256 def __init__(self):
257 super(ChangeLogException, self).__init__()
258
259
260 class GarbledChangeLogHeader(ChangeLogException):
261 """Garbled ChangeLog header
262
263 Raised if parsing of ChangeLog header fails.
264
265 """
266 def __init__(self, header):
267 super(GarbledChangeLogHeader, self).__init__()
268 self.header = header
269
270 def __str__(self):
271 return "Garbled ChangeLog header: %s" % self.header
272
273 def __repr__(self):
274 return str(self)
275
276
277 class GarbledChangeLogFooter(ChangeLogException):
278 """Garbled ChangeLog footer
279
280 Raised if parsing of ChangeLog footer fails.
281
282 """
283 def __init__(self, footer):
284 super(GarbledChangeLogFooter, self).__init__()
285 self.footer = footer
286
287 def __str__(self):
288 return "Garbled ChangeLog footer: %s" % self.footer
289
290 def __repr__(self):
291 return str(self)
292
293
294 class EmptyChangeLogFile(ChangeLogException):
295 """Empty ChangeLog File
296
297 Raised if operation expect existing entries in the ChangeLog File
298 but none is found.
299
300 """
301 def __init__(self, filename):
302 super(EmptyChangeLogFile, self).__init__()
303 self.filename = filename
304
305 def __str__(self):
306 return "Empty ChangeLog: %s" % (self.filename,)
307
308 def __repr__(self):
309 return str(self)
310
311
312 class OperationNotApplicable(ChangeLogException):
313 """Operation is not applicable to the given changelog file style"""
314 def __init__(self, style):
315 super(OperationNotApplicable, self).__init__()
316 self.style = style
317
318 def __str__(self):
319 return "Operation not applicable to %s style change logs" % (self.style,)
320
321 def __repr__(self):
322 return str(self)
323
324
325 class Maintainer(object):
326 """
327 """
328 def __init__(self):
329 self.fullname, self.email = Maintainer._get_maintainer_fallback()
330 try:
331 cfg_defaults = { 'fullname': self.fullname,
332 'email': self.email }
333 cfgfile = ConfigParser.ConfigParser(defaults=cfg_defaults)
334 cfgfile.read(os.path.expanduser('~/.cswch.rc'))
335
336 self.fullname = cfgfile.get('general', 'fullname')
337 self.email = cfgfile.get('general', 'email')
338 except ConfigParser.Error:
339 pass
340
341
342 def __str__(self):
343 return self.fullname + '<' + self.email + '>'
344
345 @classmethod
346 def _get_maintainer_fallback(cls):
347 """Derive maintainer name and email.
348
349 Return maintainer and email as tuple.
350
351 """
352 return (pwd.getpwnam(Maintainer._logname())[4],
353 "%s@opencsw.org" % Maintainer._logname())
354
355 @classmethod
356 def _logname(cls):
357 if 'LOGNAME' in os.environ:
358 return os.environ['LOGNAME']
359 else:
360 return os.getlogin()
361
362
363 class ChangeLogHeader(object):
364 """Constitutes a changelog header.
365
366 It has the form
367
368 package (version,REV=rev)
369
370 for Debian style ChangeLog files, or
371
372 package (timestamp)
373
374 for Dam style ChangeLog files.
375
376 In any case, it marks the start of a changelog entry.
377
378 """
379 def __init__(self, **kwargs):
380 """Initialize ChangeLogHeader
381
382 :param line: parses the given line
383
384 :param package: the package name
385
386 :param version: version of the upstream packages
387
388 :param rev: revision of the opencsw package
389
390 """
391 if 'line' in kwargs:
392 self.parse_line(kwargs['line'])
393 return
394
395 assert 'package' in kwargs
396
397 if 'version' in kwargs and kwargs['version'] is not None:
398 # Must be Debian style changelog
399 self.package = kwargs['package']
400 self.version = kwargs['version']
401
402 if 'rev' not in kwargs:
403 self.rev = ChangeLogHeader.opencsw_rev()
404 else:
405 self.rev = kwargs['rev']
406
407 self.style = ChangeLogHeader.STYLE_DEB
408 else:
409 # dam style changelog
410 self.package = kwargs['package']
411 if 'timestamp' in kwargs:
412 self.timestamp = kwargs['timestamp']
413 else:
414 self.timestamp = ChangeLogHeader.get_rfc3339timestamp()
415
416 self.style = ChangeLogHeader.STYLE_DAM
417
418 def __str__(self):
419 """Get the ChangeLog header as string"""
420 if self.style == ChangeLogHeader.STYLE_DEB:
421 return ChangeLogHeader.format_str_deb % {'package': self.package,
422 'version': self.version,
423 'rev': self.rev}
424 return ChangeLogHeader.format_str_dam % {'package': self.package,
425 'timestamp': self.timestamp}
426
427 def parse_line(self, line):
428 """Parse ChangeLog header
429
430 Dissect a ChangeLog header into package, version, and
431 revision, or package, and timestamp, depending on the style of
432 the ChangeLog.
433
434 It will try to parse Debian style and Dam style header lines
435 and set self.style according to the style.
436
437 """
438 matches = ChangeLogHeader.compiled_re_deb.match(line)
439
440 if matches is None:
441 # Now, try dam style
442 matches = ChangeLogHeader.compiled_re_dam.match(line)
443 if matches is None:
444 raise GarbledChangeLogHeader(line)
445
446 self.package = matches.group('package')
447 self.timestamp = matches.group('timestamp')
448 self.style = ChangeLogHeader.STYLE_DAM
449 ## Leave method!
450 return
451
452 # Debian style log
453 self.package = matches.group('package')
454 self.version = matches.group('version')
455 self.rev = matches.group('rev')
456 self.style = ChangeLogHeader.STYLE_DEB
457
458 def refresh_timestamp(self):
459 """Set timestamp to current date/time
460
461 Just a convenience method.
462 """
463 if self.style == ChangeLogHeader.STYLE_DEB:
464 self.rev = ChangeLogHeader.opencsw_rev()
465 else:
466 self.timestamp = ChangeLogHeader.get_rfc3339timestamp()
467
468 @classmethod
469 def opencsw_rev(cls, date=None):
470 """Convert a date to as string suitable for REV=
471
472 If no date is specified, the current date is used.
473
474 """
475 if date is None:
476 date = datetime.date.today()
477
478 return date.strftime('%Y.%m.%d')
479
480 @classmethod
481 def get_rfc3339timestamp(cls, date_obj=None):
482 """Return a timestamp in the format specified in RFC3339, section 5.6
483
484 :param date_obj: datetime object to be formatted. If None, the
485 current time will be taken.
486
487 :retval: datetime formatted as RFC3339 timestamp
488
489 """
490 if date_obj is None:
491 timezone = dateutil.tz.tzlocal()
492 date_obj = datetime.datetime.now(timezone)
493
494 return date_obj.strftime('%Y-%m-%dT%H:%M:%S%z')
495
496 format_str_deb = r'%(package)s (%(version)s,REV=%(rev)s)'
497 parse_re_deb = r'(?P<package>[\w-]+) \((?P<version>[\d\.]+),REV=(?P<rev>\d{4}\.\d{2}\.\d{2})\)'
498 compiled_re_deb = re.compile(parse_re_deb)
499
500 format_str_dam = r'%(package)s (%(timestamp)s)'
501 parse_re_dam = r'(?P<package>[\w-]+) \((?P<timestamp>[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}(?:\+|-)[\d]{4})\)'
502 compiled_re_dam = re.compile(parse_re_dam)
503
504 STYLE_DEB = 'debian'
505 STYLE_DAM = 'dam'
506
507
508 class ChangeLogFooter(object):
509 """End a changelog entry.
510
511 It has the form
512
513 [one space]-- maintainer name <email address>[two spaces] date
514 """
515 def __init__(self, **kwargs):
516 if len(kwargs) == 0:
517 maintainer = Maintainer()
518 self.maintainer = maintainer.fullname
519 self.email = maintainer.email
520 self.timestamp = ChangeLogFooter.get_rfc2822timestamp()
521
522 return
523
524 if 'line' in kwargs:
525 self.parse_line(kwargs['line'])
526 return
527
528 assert 'maintainer' in kwargs and 'email' in kwargs
529
530 self.maintainer = kwargs['maintainer']
531 self.email = kwargs['email']
532
533 if 'timestamp' in kwargs:
534 self.timestamp = ChangeLogFooter.get_rfc2822timestamp(kwargs['timestamp'])
535 else:
536 self.timestamp = ChangeLogFooter.get_rfc2822timestamp()
537
538 def __str__(self):
539 return ChangeLogFooter.format_str_deb % {'maintainer': self.maintainer,
540 'email': self.email,
541 'timestamp': self.timestamp}
542
543 def parse_line(self, line):
544 """Parse ChangeLog footer
545
546 Dissect a ChangeLog footer into maintainer, email, and
547 timestamp.
548
549 """
550 matches = ChangeLogFooter.compiled_re_deb.match(line)
551
552 if matches is None:
553 raise GarbledChangeLogFooter(line)
554
555 self.maintainer = matches.group('maintainer')
556 self.email = matches.group('email')
557 self.timestamp = matches.group('timestamp')
558
559 def refresh_timestamp(self):
560 """Set timestamp to current date/time
561
562 Just a convenience method.
563 """
564 self.timestamp = ChangeLogFooter.get_rfc2822timestamp()
565
566 @classmethod
567 def get_rfc2822timestamp(cls, date_obj=None):
568 """Return a timestamp in the proper format.
569
570 The proper format is a timestamp in the format specified in RFC
571 2822.
572
573 :param date_obj: datetime object or none, in which case the current time
574 will be taken.
575
576 """
577 if date_obj is None:
578 timezone = dateutil.tz.tzlocal()
579 date_obj = datetime.datetime.now(timezone)
580
581 return email.utils.formatdate(time.mktime(date_obj.timetuple()), True)
582
583 format_str_deb = r' -- %(maintainer)s <%(email)s> %(timestamp)s'
584 parse_re_deb = r' -- (?P<maintainer>[\w\d ]+) <(?P<email>[\w\d@ \.]+)> (?P<timestamp>\w{3}, \d{1,2} \w{3} \d{4} \d{2}:\d{2}:\d{2} (\+|-)\d{4})'
585 compiled_re_deb = re.compile(parse_re_deb)
586
587
588 class ChangeLogParagraph(object):
589 """A changelog paragraph
590
591 It has the form
592
593 [two spaces]* change details
594 more change details
595
596 or
597
598 [two spaces]line1
599 line2
600
601 The first line added is analyzed to find out which type of
602 paragraph we're dealing with.
603
604 """
605 def __init__(self, *args, **kwargs):
606 """Initialize ChangeLog paragraph.
607
608 Initialize ChangeLog paragraph from one ore several
609 lines. Optionally, maxcol can be specified.
610
611 """
612 assert len(args) > 0
613
614 if 'maxcol' in kwargs:
615 self.maxcol = kwargs['maxcol']
616 else:
617 self.maxcol = 72
618
619 # will be set according to the style of the first line
620 self.bullet_style = False
621
622 self.paragraph = ""
623 [self.add_line(l) for l in args]
624
625 def add_line(self, line):
626 """Add another line to the paragraph"""
627
628 # Analyze the first line in order to find out the type of
629 # paragraph, i.e. bullet style or plain style
630 if self.paragraph == "" and ChangeLogParagraph.sanitize_re_c.match(line):
631 # it's a bullet style paragraph
632 self.bullet_style = True
633
634 # Get rid of all leading '\s+\*?\s+' and concatenate all
635 # arguments.
636 self.paragraph = " ".join([self.paragraph, ChangeLogParagraph.sanitize_re_c.sub("", line)])
637
638 # Some sanitation
639 self.paragraph = self.paragraph.strip()
640 self.paragraph = ChangeLogParagraph.whitespace_re_c.sub(' ',
641 self.paragraph)
642
643 def __repr__(self):
644 return self.paragraph
645
646 def __str__(self):
647 words = self.paragraph.split()
648
649 if self.bullet_style:
650 # line length starts at 4, because we have to take ' * ' into
651 # account for the first line.
652 line_indent = 4
653 continuation_indent = 4
654 formatted_paragraph = ' * '
655 else:
656 line_indent = 2
657 continuation_indent = 2
658 formatted_paragraph = ' '
659
660 line_length = line_indent
661
662 for word in words:
663 if len(word) + line_length < self.maxcol:
664 formatted_paragraph = formatted_paragraph + word + ' '
665 line_length = line_length + len(word) + 1
666 else:
667 # chop off the last space
668 formatted_paragraph = formatted_paragraph.rstrip()
669 # add newline and reset line length counter
670 formatted_paragraph = formatted_paragraph + '\n' +\
671 ' ' * continuation_indent + word + ' '
672 line_length = continuation_indent + len(word)
673
674 formatted_paragraph = formatted_paragraph.rstrip()
675 return formatted_paragraph
676
677 sanitize_re = r'^\s+\*\s+'
678 sanitize_re_c = re.compile(sanitize_re)
679 whitespace_re = r'\s{2,}|\n+'
680 whitespace_re_c = re.compile(whitespace_re)
681
682
683 class ChangeLogEntry(object):
684 """A ChangeLogEntry
685
686 A ChangeLogEntry is comprised of a ChangeLogHeader,
687 ChangeLogFooter, and one or more ChangeLogParagraph's.
688
689 """
690
691 def __init__(self, *args):
692 if len(args) == 3:
693 assert len(args) == 3
694 assert isinstance(args[0], ChangeLogHeader)
695 assert isinstance(args[1], list)
696 assert isinstance(args[2], ChangeLogFooter)
697 self.header = args[0]
698 self.paragraphs = args[1]
699 self.footer = args[2]
700 else:
701 self.header = None
702 self.footer = None
703 self.paragraphs = list()
704
705 def add_header(self, cl_header):
706 """Add a ChangeLog Entry header.
707
708 Add or replace the ChangeLog Entry header.
709
710 """
711 assert cl_header is not None
712 self.header = cl_header
713
714 def add_footer(self, cl_footer):
715 """Add a ChangeLog Entry footer.
716
717 Add or replace the ChangeLog Entry footer.
718
719 """
720 assert cl_footer is not None
721 self.footer = cl_footer
722
723 def add_paragraph(self, cl_paragraph, infront=False):
724 """Add a ChangeLog Entry paragraph.
725
726 Add another ChangeLog Entry paragraph.
727
728 If :param infront: is True, the paragraph will be prepended to
729 the paragraph list. Else it will be appended.
730
731 """
732 assert cl_paragraph is not None
733
734 if infront:
735 self.paragraphs.insert(0, cl_paragraph)
736 else:
737 self.paragraphs.append(cl_paragraph)
738
739 def remove_paragraph(self, index):
740 """Remove a ChangeLog paragraph
741
742 Remove a ChangeLog paragraph from the list of paragraphs. The
743 removed paragraph will be returned to the caller.
744
745 """
746 chlogp = self.paragraphs[index]
747 del self.paragraphs[index]
748 return chlogp
749
750 def __str__(self):
751 return str(self.header) + '\n\n' +\
752 "\n\n".join([str(paragraph) for paragraph in
753 self.paragraphs]) + '\n\n' +\
754 str(self.footer) + '\n\n\n'
755
756
757 class ChangeLogFile(object):
758 """A ChangeLog file"""
759
760 def __init__(self, filename):
761 self.filename = filename
762 self.changelog_entries = list()
763
764 def read(self):
765 """Read the ChangeLogFile
766
767 Raises an exception 'ChangeLogFileEmpty()' if the file is
768 empty, or no ChangeLogEnties could be extracted.
769
770 """
771 changelog_entry = None
772 current_paragraph = None
773 with open(self.filename, "r") as fobj:
774 only_whitespace_re = re.compile(r'^\s+$')
775 entry_start_re = re.compile(r'^[\w\d\.=(),]+')
776
777 for line in fobj:
778 line = line.rstrip('\n')
779
780 if only_whitespace_re.match(line) or line == "":
781 if current_paragraph is not None:
782 # This ends the current paragraph
783 changelog_entry.add_paragraph(current_paragraph)
784 current_paragraph = None
785
786 continue
787
788 # is it the start of an entry?
789 if entry_start_re.match(line):
790 assert changelog_entry is None
791
792 changelog_entry = ChangeLogEntry()
793 changelog_entry.add_header(ChangeLogHeader(line=line))
794 continue
795
796 # is it the footer, and thus marks the end of a entry?
797 if line.startswith(' -- '):
798 # is there a paragraph pending?
799 if current_paragraph is not None:
800 changelog_entry.add_paragraph(current_paragraph)
801
802 changelog_entry.add_footer(ChangeLogFooter(line=line))
803 # append entry to the list
804 self.changelog_entries.append(changelog_entry)
805
806 changelog_entry = None
807 current_paragraph = None
808 continue
809
810 if current_paragraph is None:
811 current_paragraph = ChangeLogParagraph(line)
812 else:
813 current_paragraph.add_line(line)
814
815 if len(self.changelog_entries) < 1:
816 raise EmptyChangeLogFile(self.filename)
817
818 def save(self):
819 """Save ChangeLog entries to file"""
820 with open(self.filename, 'w') as fobj:
821 [fobj.write(str(changelog_entry)) for changelog_entry in
822 self.changelog_entries]
823
824 def versions(self):
825 """Return a list of all versions in the ChangeLog file
826
827 When called on a Dam style changelog file, it throws an
828 exception, since Dam style changelog files do not have a
829 version.
830
831 """
832
833 if len(self.changelog_entries) < 1:
834 return None
835
836 if self.changelog_entries[0].header.style == ChangeLogHeader.STYLE_DAM:
837 raise OperationNotApplicable(ChangeLogHeader.STYLE_DAM)
838
839 return [changelog_entry.header.version for changelog_entry in
840 self.changelog_entries]
841
842 def count_entries(self):
843 """Return the number of ChangeLog entries"""
844 return len(self.changelog_entries)
845
846 def get_package(self):
847 """Get the package name
848
849 Package name is returned as defined by the first entry of the
850 ChangeLog file.
851
852 None is returned if changelog file is empty.
853
854 """
855 if len(self.changelog_entries) < 1:
856 return None
857
858 return self.changelog_entries[0].header.package
859
860 def add_entry(self, *args):
861 """Add an entry to the ChangeLog.
862
863 If only one argument is passed, it has to be a
864 ChangeLogEntry. Else three arguments are expected, which have
865 to be of the type ChangeLogHeader, ChangeLogParagraphs list,
866 ChangeLogFooter
867
868 """
869 if len(args) == 1:
870 assert args[0] is ChangeLogEntry
871 changelog_entry = args[0]
872 else:
873 assert len(args) == 3
874 assert isinstance(args[0], ChangeLogHeader)
875 assert isinstance(args[1], list)
876 assert isinstance(args[2], ChangeLogFooter)
877 changelog_entry = ChangeLogEntry(args[0], args[1],
878 args[2])
879
880 self.changelog_entries.insert(0, changelog_entry)
881
882 def create(self, *args):
883 """Create a new ChangeLog file
884
885 Create a new ChangeLog file. Any existing entries will be
886 overwritten.
887
888 """
889
890 self.changelog_entries = list()
891 self.add_entry(*args)
892 self.save()
893
894 def update_latest(self, paragraph):
895 """Add paragraph(s) to the latest entry.
896
897 :param paragraph: can be a list of ChangeLogParagraph or a
898 single ChangeLogParagrph.
899
900 """
901
902 assert len(self.changelog_entries) > 0
903
904 if paragraph is list:
905 [self.changelog_entries[0].add_paragraph(par, True) for par
906 in paragraph]
907 else:
908 self.changelog_entries[0].add_paragraph(paragraph, True)
909
910 self.refresh_timestamps()
911
912 def refresh_timestamps(self):
913 """Set the timestamps of the latest entry to the current date
914
915 Timestamps of ChangeLogHeader and ChangeLogFooter will updated.
916 """
917 assert len(self.changelog_entries) > 0
918
919 self.changelog_entries[0].header.refresh_timestamp()
920 self.changelog_entries[0].footer.refresh_timestamp()
921
922 def svn_add(self):
923 """Add file to svn repository
924
925 Has only effect if the directory is a subversion working
926 directory. Else it does nothing.
927
928 """
929 # First of all, make sure the file exists
930 if not os.path.exists(self.filename):
931 raise OSError((errno.ENOENT, 'No such file: %s' % self.filename))
932
933 svnclient = pysvn.Client()
934
935 # see if directory holding the file is part of a SVN
936 # repo
937 directory = os.path.dirname(self.filename)
938 if directory == "":
939 directory = './'
940
941 try:
942 svnentry = svnclient.info(directory)
943 except pysvn.ClientError:
944 # Most likely it is not a working directory, so we bail out
945 logging.warning("'%s' is not a SVN working directory. Cannot add file '%s'.",
946 directory, self.filename)
947 return
948
949 svnclient.add([self.filename])
950 logging.info("Scheduled file '%s' for addition to SVN repository", self.filename)
951
952 def in_svn(self):
953 """Checks if ChangeLogFile is added to SVN working directory.
954
955 :returns: True if the file is added to the SVN working
956 directory, else False.
957
958 """
959 # First of all, make sure the file exists
960 if not os.path.exists(self.filename):
961 raise OSError((errno.ENOENT, 'No such file: %s' % self.filename))
962
963 svnclient = pysvn.Client()
964
965 try:
966 svnclient.info(self.filename)
967 except pysvn.ClientError:
968 return False
969
970 return True
971
972 def changelog_style(self):
973 """Get the type of the changelog
974
975 Determines the style of the changelog by reading the first line.
976 """
977 with open(self.filename, "r") as changelog_file:
978 # read and parse the first line
979 changelog_header = ChangeLogHeader(line=changelog_file.readline())
980 return changelog_header.style
981
982
983 def new_changelog_entry(filename, version, log):
984 """Create a new change log entry
985
986 :param version: has to be None for Dam style log files.
987 """
988 changelog_file = ChangeLogFile(filename)
989
990 style = changelog_file.changelog_style()
991 if style == ChangeLogHeader.STYLE_DAM and\
992 version is not None:
993 logging.error("Changelog '%s' is Dam style. Version does not apply.",
994 filename)
995 sys.exit(1)
996
997 changelog_file.read()
998
999 # get the package from the first entry
1000 package = changelog_file.get_package()
1001
1002 header = ChangeLogHeader(package=package,
1003 version=version)
1004
1005 paragraphs = [ChangeLogParagraph(log_para) for log_para in log]
1006 footer = ChangeLogFooter()
1007
1008 changelog_file.add_entry(header, paragraphs, footer)
1009 changelog_file.save()
1010
1011 def update_changelog_entry(filename, log):
1012
1013 changelog_file = ChangeLogFile(filename)
1014 changelog_file.read()
1015
1016 [changelog_file.update_latest(ChangeLogParagraph(l)) for l in
1017 log]
1018 changelog_file.save()
1019
1020 def refresh_changelog(filename):
1021 changelog_file = ChangeLogFile(filename)
1022 changelog_file.read()
1023
1024 changelog_file.refresh_timestamps()
1025
1026 changelog_file.save()
1027
1028 def create_changelog_file(filename, style, overwrite, package,
1029 version, log, register_svn):
1030 # For Dam style changelogs, version is expected to be None
1031 header = ChangeLogHeader(package=package, version=version)
1032
1033 footer = ChangeLogFooter()
1034 paragraphs = [ChangeLogParagraph(log_para) for log_para in log]
1035
1036 if os.path.exists(filename) and not overwrite:
1037 logging.error("Changelog '%s' already exists. Won't overwrite", filename)
1038 sys.exit(1)
1039
1040 changelog_file = ChangeLogFile(filename)
1041 changelog_file.create(header, paragraphs, footer)
1042
1043 if (register_svn):
1044 changelog_file.svn_add()
1045
1046 def get_version(logfile, allversions):
1047 changelog_file = ChangeLogFile(logfile)
1048 changelog_file.read()
1049
1050 if allversions:
1051 print("\n".join(changelog_file.versions()))
1052 else:
1053 print(changelog_file.versions()[0])
1054
1055 def compile_log(logitem, logparagraph):
1056 """Create a list of strings having the proper format to be fed to
1057 ChangeLogParagraph
1058
1059 Returns a list of strings that is suitable for feeding to
1060 ChangeLogParagraph, i.e. item style entries are prepended with ' *
1061 ' and plain paragraphs are left alone.
1062
1063 """
1064 log = list()
1065 if logitem:
1066 log = [' * ' + item for item in logitem]
1067
1068 if logparagraph:
1069 log.extend(logparagraph)
1070
1071 return log
1072
1073 def cond_complain_svn(filename, complain):
1074 """Conditionally complain about changelog file not registered in SVN
1075
1076 :param complain: if True and file is not in SVN, complain. Else do nothing.
1077 """
1078 changelogfile = ChangeLogFile(filename)
1079 if complain and not changelogfile.in_svn():
1080 logging.warn("%s is not under version control", changelogfile.filename)
1081
1082 def cmdline_parse():
1083 """Parse the command line
1084
1085 Returns the result of argparse.parse_args()
1086
1087 """
1088 parser = argparse.ArgumentParser(description="Maintain OpenCSW changelogs")
1089 parser.add_argument('--logfile',
1090 help='filename to use (default: %(default)s)',
1091 default='files/changelog.CSW')
1092 parser.add_argument('--no-svn', help="Do not add file to SVN repository or stop complaining about file not registerd with svn.",
1093 default=False, action='store_const',
1094 const=True)
1095 subparser = parser.add_subparsers(help='sub-command help',
1096 dest='command')
1097
1098 parser_create = subparser.add_parser('create',
1099 help="create new changelog.CSW file. Existing file will not be overwritten.")
1100 group = parser_create.add_mutually_exclusive_group()
1101 group.add_argument('--deb-style',
1102 help='Create a Debian style changelog',
1103 action='store_const',
1104 dest='style', const=ChangeLogHeader.STYLE_DEB)
1105 group.add_argument('--dam-style',
1106 help='Create a Dam style changelog',
1107 action='store_const',
1108 dest='style', const=ChangeLogHeader.STYLE_DAM)
1109 parser_create.add_argument('package',
1110 help="package name")
1111 parser_create.add_argument('--version',
1112 help="package version (only required for Debian style changelog)",
1113 required=False)
1114 parser_create.add_argument('--log-item',
1115 help="the log entry to be recorded as list item",
1116 nargs='*')
1117 parser_create.add_argument('--log-paragraph',
1118 help="the log entry to be recorded as plain paragraph",
1119 nargs='*')
1120 parser_create.add_argument('--force', help="force creation of file. Overwrite existing file.",
1121 default=False, action='store_const',
1122 const=True)
1123
1124 parser_new = subparser.add_parser('new',
1125 help="new changelog entry")
1126 parser_new.add_argument('--version',
1127 help="new version (only required for Debian style changelog)",
1128 required=False)
1129 parser_new.add_argument('--log-item',
1130 help="the log entry to be recorded as list item",
1131 nargs='*')
1132 parser_new.add_argument('--log-paragraph',
1133 help="the log entry to be recorded as plain paragraph",
1134 nargs='*')
1135
1136
1137 parser_update = subparser.add_parser('update',
1138 help="update the latest changelog entry")
1139 parser_update.add_argument('--log-item',
1140 help="the log entry to be recorded as list item",
1141 nargs='*')
1142 parser_update.add_argument('--log-paragraph',
1143 help="the log entry to be recorded as plain paragraph",
1144 nargs='*')
1145
1146 parser_refresh = subparser.add_parser('refresh',
1147 help="refresh header and footer of latest changelog entry")
1148
1149 parser_version = subparser.add_parser('version',
1150 help="retrieve version from changelog.CSW")
1151 parser_version.add_argument('--all-versions',
1152 help="retrieve all versions",
1153 default=False, action='store_const',
1154 const=True)
1155
1156 parser_useage = subparser.add_parser('usage', help="Show a brief usage description")
1157
1158 parser_test = subparser.add_parser('test', help="test cswch")
1159
1160 arguments = parser.parse_args()
1161
1162 if arguments.command in ['create', 'new', 'update'] and\
1163 arguments.log_paragraph is None and\
1164 arguments.log_item is None:
1165 parser.error("'%s' requires either '--log-paragraph' or '--log-item'" % arguments.command)
1166
1167 if arguments.command == "create" and\
1168 arguments.style == ChangeLogHeader.STYLE_DAM and\
1169 arguments.version is not None:
1170 parser.error("Dam style changelogs do not take version")
1171
1172 return arguments
1173
1174
1175 if __name__ == '__main__':
1176 arguments = cmdline_parse()
1177
1178 if arguments.command == "test":
1179 # cheat unittest into thinking that there are no command line
1180 # arguments.
1181 del sys.argv[1:]
1182 unittest.main()
1183
1184 if arguments.command == "usage":
1185 print(__doc__)
1186 exit(0)
1187
1188 try:
1189 if arguments.command == "create":
1190 log = compile_log(arguments.log_item, arguments.log_paragraph)
1191 create_changelog_file(arguments.logfile,
1192 arguments.style,
1193 arguments.force,
1194 arguments.package,
1195 arguments.version,
1196 log,
1197 not arguments.no_svn)
1198
1199 if arguments.command == "new":
1200 log = compile_log(arguments.log_item, arguments.log_paragraph)
1201 new_changelog_entry(arguments.logfile,
1202 arguments.version,
1203 log)
1204 cond_complain_svn(arguments.logfile, not arguments.no_svn)
1205
1206 if arguments.command == "update":
1207 log = compile_log(arguments.log_item, arguments.log_paragraph)
1208 update_changelog_entry(arguments.logfile, log)
1209 cond_complain_svn(arguments.logfile, not arguments.no_svn)
1210
1211 if arguments.command == "refresh":
1212 refresh_changelog(arguments.logfile)
1213 cond_complain_svn(arguments.logfile, not arguments.no_svn)
1214
1215 if arguments.command == "version":
1216 get_version(arguments.logfile, arguments.all_versions)
1217 except ChangeLogException as ex:
1218 logging.error(str(ex))

Properties

Name Value
svn:executable *