| 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)) |