Changelog.pm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. #
  2. # Dpkg::Changelog
  3. #
  4. # Copyright © 2005, 2007 Frank Lichtenheld <frank@lichtenheld.de>
  5. # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  20. #
  21. =head1 NAME
  22. Dpkg::Changelog
  23. =head1 DESCRIPTION
  24. FIXME: to be written
  25. =head2 Functions
  26. =cut
  27. package Dpkg::Changelog;
  28. use strict;
  29. use warnings;
  30. use Dpkg;
  31. use Dpkg::Gettext;
  32. use Dpkg::ErrorHandling qw(:DEFAULT report);
  33. use Dpkg::Control::Changelog;
  34. use Dpkg::Control::Fields;
  35. use Dpkg::Version;
  36. use Dpkg::Vendor qw(run_vendor_hook);
  37. use base qw(Exporter);
  38. our %EXPORT_TAGS = ( 'util' => [ qw(
  39. data2rfc822
  40. data2rfc822_mult
  41. get_dpkg_changes
  42. ) ] );
  43. our @EXPORT_OK = @{$EXPORT_TAGS{util}};
  44. =pod
  45. =head3 init
  46. Creates a new object instance. Takes a reference to a hash as
  47. optional argument, which is interpreted as configuration options.
  48. There are currently no supported general configuration options, but
  49. see the other methods for more specific configuration options which
  50. can also specified to C<init>.
  51. If C<infile>, C<inhandle>, or C<instring> are specified, C<parse()>
  52. is called from C<init>. If a fatal error is encountered during parsing
  53. (e.g. the file can't be opened), C<init> will not return a
  54. valid object but C<undef>!
  55. =cut
  56. sub init {
  57. my $classname = shift;
  58. my $config = shift || {};
  59. my $self = {};
  60. bless( $self, $classname );
  61. $config->{verbose} = 1 if $config->{debug};
  62. $self->{config} = $config;
  63. $self->reset_parse_errors;
  64. if ($self->{config}{infile}
  65. || $self->{config}{inhandle}
  66. || $self->{config}{instring}) {
  67. defined($self->parse) or return undef;
  68. }
  69. return $self;
  70. }
  71. =pod
  72. =head3 reset_parse_errors
  73. Can be used to delete all information about errors ocurred during
  74. previous L<parse> runs. Note that C<parse()> also calls this method.
  75. =cut
  76. sub reset_parse_errors {
  77. my ($self) = @_;
  78. $self->{errors}{parser} = [];
  79. }
  80. sub _do_parse_error {
  81. my ($self, $file, $line_nr, $error, $line) = @_;
  82. shift;
  83. push @{$self->{errors}{parser}}, [ @_ ];
  84. unless ($self->{config}{quiet}) {
  85. if ($line) {
  86. warning("%20s(l$.): $error\nLINE: $line", $file);
  87. } else {
  88. warning("%20s(l$.): $error", $file);
  89. }
  90. }
  91. }
  92. =pod
  93. =head3 get_parse_errors
  94. Returns all error messages from the last L<parse> run.
  95. If called in scalar context returns a human readable
  96. string representation. If called in list context returns
  97. an array of arrays. Each of these arrays contains
  98. =over 4
  99. =item 1.
  100. the filename of the parsed file or C<FileHandle> or C<String>
  101. if the input came from a file handle or a string. If the
  102. reportfile configuration option was given, its value will be
  103. used instead
  104. =item 2.
  105. the line number where the error occurred
  106. =item 3.
  107. an error description
  108. =item 4.
  109. the original line
  110. =back
  111. NOTE: This format isn't stable yet and may change in later versions
  112. of this module.
  113. =cut
  114. sub get_parse_errors {
  115. my ($self) = @_;
  116. if (wantarray) {
  117. return @{$self->{errors}{parser}};
  118. } else {
  119. my $res = "";
  120. foreach my $e (@{$self->{errors}{parser}}) {
  121. if ($e->[3]) {
  122. $res .= report(_g('warning'),_g("%s(l%s): %s\nLINE: %s"), @$e );
  123. } else {
  124. $res .= report(_g('warning'),_g("%s(l%s): %s"), @$e );
  125. }
  126. }
  127. return $res;
  128. }
  129. }
  130. sub _do_fatal_error {
  131. my ($self, $msg, @msg) = @_;
  132. $self->{errors}{fatal} = report(_g('fatal error'), $msg, @msg);
  133. warning($msg, @msg) unless $self->{config}{quiet};
  134. }
  135. =pod
  136. =head3 get_error
  137. Get the last non-parser error (e.g. the file to parse couldn't be opened).
  138. =cut
  139. sub get_error {
  140. my ($self) = @_;
  141. return $self->{errors}{fatal};
  142. }
  143. =pod
  144. =head3 data
  145. C<data> returns an array (if called in list context) or a reference
  146. to an array of Dpkg::Changelog::Entry objects which each
  147. represent one entry of the changelog.
  148. This method supports the common output options described in
  149. section L<"COMMON OUTPUT OPTIONS">.
  150. =cut
  151. sub data {
  152. my ($self, $config) = @_;
  153. my $data = $self->{data};
  154. if ($config) {
  155. $self->{config}{DATA} = $config if $config;
  156. $data = $self->_data_range( $config ) or return undef;
  157. }
  158. return @$data if wantarray;
  159. return $data;
  160. }
  161. sub __sanity_check_range {
  162. my ( $data, $from, $to, $since, $until, $start, $end ) = @_;
  163. if (($$start || $$end) &&
  164. (length($$from) || length($$since) || length($$to) || length($$until)))
  165. {
  166. warning(_g( "you can't combine 'count' or 'offset' with any other range option" ));
  167. $$from = $$since = $$to = $$until = '';
  168. }
  169. if (length($$from) && length($$since)) {
  170. warning(_g( "you can only specify one of 'from' and 'since', using 'since'" ));
  171. $$from = '';
  172. }
  173. if (length($$to) && length($$until)) {
  174. warning(_g( "you can only specify one of 'to' and 'until', using 'until'" ));
  175. $$to = '';
  176. }
  177. $$start = 0 if $$start < 0;
  178. return if $$start > $#$data;
  179. $$end = $#$data if $$end > $#$data;
  180. return if $$end < 0;
  181. $$end = $$start if $$end < $$start;
  182. # Handle non-existing versions
  183. my (%versions, @versions);
  184. foreach my $entry (@{$data}) {
  185. $versions{$entry->get_version()->as_string()} = 1;
  186. push @versions, $entry->get_version()->as_string();
  187. }
  188. if ((length($$since) and not exists $versions{$$since})) {
  189. warning(_g("'%s' option specifies non-existing version"), "since");
  190. warning(_g("use newest entry that is smaller than the one specified"));
  191. foreach my $v (@versions) {
  192. if (version_compare_relation($v, REL_LT, $$since)) {
  193. $$since = $v;
  194. last;
  195. }
  196. }
  197. if (not exists $versions{$$since}) {
  198. # No version was smaller, include all
  199. warning(_g("none found, starting from the oldest entry"));
  200. $$since = '';
  201. $$from = $versions[-1];
  202. }
  203. }
  204. if ((length($$from) and not exists $versions{$$from})) {
  205. warning(_g("'%s' option specifies non-existing version"), "from");
  206. warning(_g("use oldest entry that is bigger than the one specified"));
  207. my $oldest;
  208. foreach my $v (@versions) {
  209. if (version_compare_relation($v, REL_GT, $$from)) {
  210. $oldest = $v;
  211. }
  212. }
  213. if (defined($oldest)) {
  214. $$from = $oldest;
  215. } else {
  216. warning(_g("no such entry found, ignoring '%s' parameter"), "from");
  217. $$from = ''; # No version was bigger
  218. }
  219. }
  220. if ((length($$until) and not exists $versions{$$until})) {
  221. warning(_g("'%s' option specifies non-existing version"), "until");
  222. warning(_g("use oldest entry that is bigger than the one specified"));
  223. my $oldest;
  224. foreach my $v (@versions) {
  225. if (version_compare_relation($v, REL_GT, $$until)) {
  226. $oldest = $v;
  227. }
  228. }
  229. if (defined($oldest)) {
  230. $$until = $oldest;
  231. } else {
  232. warning(_g("no such entry found, ignoring '%s' parameter"), "until");
  233. $$until = ''; # No version was bigger
  234. }
  235. }
  236. if ((length($$to) and not exists $versions{$$to})) {
  237. warning(_g("'%s' option specifies non-existing version"), "to");
  238. warning(_g("use newest entry that is smaller than the one specified"));
  239. foreach my $v (@versions) {
  240. if (version_compare_relation($v, REL_LT, $$to)) {
  241. $$to = $v;
  242. last;
  243. }
  244. }
  245. if (not exists $versions{$$to}) {
  246. # No version was smaller
  247. warning(_g("no such entry found, ignoring '%s' parameter"), "to");
  248. $$to = '';
  249. }
  250. }
  251. if (length($$since) && ($data->[0]->get_version() eq $$since)) {
  252. warning(_g( "'since' option specifies most recent version, ignoring" ));
  253. $$since = '';
  254. }
  255. if (length($$until) && ($data->[$#{$data}]->get_version() eq $$until)) {
  256. warning(_g( "'until' option specifies oldest version, ignoring" ));
  257. $$until = '';
  258. }
  259. return 1;
  260. }
  261. sub _data_range {
  262. my ($self, $config) = @_;
  263. my $data = $self->data or return undef;
  264. return [ @$data ] if $config->{all};
  265. my ($since, $until, $from, $to, $count, $offset) = ('', '', '', '', 0, 0);
  266. $since = $config->{since} if defined($config->{since});
  267. $until = $config->{until} if defined($config->{until});
  268. $from = $config->{from} if defined($config->{from});
  269. $to = $config->{to} if defined($config->{to});
  270. $count = $config->{count} if defined($config->{count});
  271. $offset = $config->{offset} if defined($config->{offset});
  272. return if $offset and not $count;
  273. if ($offset > 0) {
  274. $offset -= ($count < 0);
  275. } elsif ($offset < 0) {
  276. $offset = $#$data + ($count > 0) + $offset;
  277. } else {
  278. $offset = $#$data if $count < 0;
  279. }
  280. my $start = my $end = $offset;
  281. $start += $count+1 if $count < 0;
  282. $end += $count-1 if $count > 0;
  283. return unless __sanity_check_range( $data, \$from, \$to,
  284. \$since, \$until,
  285. \$start, \$end );
  286. unless (length($from) or length($to) or length($since) or length($until)
  287. or $start or $end)
  288. {
  289. return [ @$data ] if $config->{default_all} and not $count;
  290. return [ $data->[0] ];
  291. }
  292. return [ @{$data}[$start .. $end] ] if $start or $end;
  293. my @result;
  294. my $include = 1;
  295. $include = 0 if length($to) or length($until);
  296. foreach (@$data) {
  297. my $v = $_->get_version();
  298. $include = 1 if $to and $v eq $to;
  299. last if $since and $v eq $since;
  300. push @result, $_ if $include;
  301. $include = 1 if $until and $v eq $until;
  302. last if $from and $v eq $from;
  303. }
  304. return \@result if scalar(@result);
  305. return undef;
  306. }
  307. sub _abort_early {
  308. my ($self) = @_;
  309. my $data = $self->data or return;
  310. my $config = $self->{config} or return;
  311. # use Data::Dumper;
  312. # warn "Abort early? (\$# = $#$data)\n".Dumper($config);
  313. return if $config->{all};
  314. my ($since, $until, $from, $to, $count, $offset) = ('', '', '', '', 0, 0);
  315. $since = $config->{since} if defined($config->{since});
  316. $until = $config->{until} if defined($config->{until});
  317. $from = $config->{from} if defined($config->{from});
  318. $to = $config->{to} if defined($config->{to});
  319. $count = $config->{count} if defined($config->{count});
  320. $offset = $config->{offset} if defined($config->{offset});
  321. return if $offset and not $count;
  322. return if $offset < 0 or $count < 0;
  323. if ($offset > 0) {
  324. $offset -= ($count < 0);
  325. }
  326. my $start = my $end = $offset;
  327. $end += $count-1 if $count > 0;
  328. unless (length($from) or length($to) or length($since) or length($until)
  329. or $start or $end)
  330. {
  331. return if not $count;
  332. return 1 if @$data;
  333. }
  334. return 1 if ($start or $end)
  335. and $start < @$data and $end < @$data;
  336. return unless length($since) or length($from);
  337. foreach (@$data) {
  338. my $v = $_->get_version();
  339. return 1 if $v eq $since;
  340. return 1 if $v eq $from;
  341. }
  342. return;
  343. }
  344. =pod
  345. =head3 dpkg
  346. (and B<dpkg_str>)
  347. C<dpkg> returns a hash (in list context) or a hash reference
  348. (in scalar context) where the keys are field names and the values are
  349. field values. The following fields are given:
  350. =over 4
  351. =item Source
  352. package name (in the first entry)
  353. =item Version
  354. packages' version (from first entry)
  355. =item Distribution
  356. target distribution (from first entry)
  357. =item Urgency
  358. urgency (highest of all printed entries)
  359. =item Maintainer
  360. person that created the (first) entry
  361. =item Date
  362. date of the (first) entry
  363. =item Closes
  364. bugs closed by the entry/entries, sorted by bug number
  365. =item Changes
  366. content of the the entry/entries
  367. =back
  368. C<dpkg_str> returns a stringified version of this hash. The fields are
  369. ordered like in the list above.
  370. Both methods support the common output options described in
  371. section L<"COMMON OUTPUT OPTIONS">.
  372. =head3 dpkg_str
  373. See L<dpkg>.
  374. =cut
  375. our ( @URGENCIES, %URGENCIES );
  376. BEGIN {
  377. @URGENCIES = qw(low medium high critical emergency);
  378. my $i = 1;
  379. %URGENCIES = map { $_ => $i++ } @URGENCIES;
  380. }
  381. sub dpkg {
  382. my ($self, $config) = @_;
  383. $self->{config}{DPKG} = $config if $config;
  384. $config = $self->{config}{DPKG} || {};
  385. my $data = $self->_data_range( $config ) or return undef;
  386. my $f = Dpkg::Control::Changelog->new();
  387. $f->{Urgency} = $data->[0]->get_urgency() || "unknown";
  388. $f->{Source} = $data->[0]->get_source() || "unknown";
  389. $f->{Version} = $data->[0]->get_version() || "unknown";
  390. $f->{Distribution} = join(" ", $data->[0]->get_distributions());
  391. $f->{Maintainer} = $data->[0]->get_maintainer() || '';
  392. $f->{Date} = $data->[0]->get_timestamp() || '';
  393. $f->{Changes} = get_dpkg_changes($data->[0]);
  394. # handle optional fields
  395. my $opts = $data->[0]->get_optional_fields();
  396. my %closes;
  397. foreach (keys %$opts) {
  398. if (/^Urgency$/i) { # Already dealt
  399. } elsif (/^Closes$/i) {
  400. $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
  401. } else {
  402. field_transfer_single($opts, $f);
  403. }
  404. }
  405. my $first = 1; my $urg_comment = '';
  406. foreach my $entry (@$data) {
  407. $first = 0, next if $first;
  408. my $oldurg = $f->{Urgency} || '';
  409. my $oldurgn = $URGENCIES{$f->{Urgency}} || -1;
  410. my $newurg = $entry->get_urgency() || '';
  411. my $newurgn = $URGENCIES{$newurg} || -1;
  412. $f->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg;
  413. $f->{Changes} .= "\n ." . get_dpkg_changes($entry);
  414. # handle optional fields
  415. $opts = $entry->get_optional_fields();
  416. foreach (keys %$opts) {
  417. if (/^Closes$/i) {
  418. $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
  419. } elsif (not exists $f->{$_}) { # Don't overwrite an existing field
  420. field_transfer_single($opts, $f);
  421. }
  422. }
  423. }
  424. if (scalar keys %closes) {
  425. $f->{Closes} = join " ", sort { $a <=> $b } keys %closes;
  426. }
  427. run_vendor_hook("post-process-changelog-entry", $f);
  428. return %$f if wantarray;
  429. return $f;
  430. }
  431. sub dpkg_str {
  432. return data2rfc822(scalar dpkg(@_));
  433. }
  434. =pod
  435. =head3 rfc822
  436. (and B<rfc822_str>)
  437. C<rfc822> returns an array of hashes (in list context) or a reference
  438. to this array (in scalar context) where each hash represents one entry
  439. in the changelog. For the format of such a hash see the description
  440. of the L<"dpkg"> method (while ignoring the remarks about which
  441. values are taken from the first entry).
  442. C<rfc822_str> returns a stringified version of this array.
  443. Both methods support the common output options described in
  444. section L<"COMMON OUTPUT OPTIONS">.
  445. =head3 rfc822_str
  446. See L<rfc822>.
  447. =cut
  448. sub rfc822 {
  449. my ($self, $config) = @_;
  450. $self->{config}{RFC822} = $config if $config;
  451. $config = $self->{config}{RFC822} || {};
  452. my $data = $self->_data_range( $config ) or return undef;
  453. my @out_data;
  454. foreach my $entry (@$data) {
  455. my $f = Dpkg::Control::Changelog->new();
  456. $f->{Urgency} = $entry->get_urgency() || "unknown";
  457. $f->{Source} = $entry->get_source() || "unknown";
  458. $f->{Version} = $entry->get_version() || "unknown";
  459. $f->{Distribution} = join(" ", $entry->get_distributions());
  460. $f->{Maintainer} = $entry->get_maintainer() || "";
  461. $f->{Date} = $entry->get_timestamp() || "";
  462. $f->{Changes} = get_dpkg_changes($entry);
  463. # handle optional fields
  464. my $opts = $entry->get_optional_fields();
  465. foreach (keys %$opts) {
  466. field_transfer_single($opts, $f) unless exists $f->{$_};
  467. }
  468. run_vendor_hook("post-process-changelog-entry", $f);
  469. push @out_data, $f;
  470. }
  471. return @out_data if wantarray;
  472. return \@out_data;
  473. }
  474. sub rfc822_str {
  475. return data2rfc822(scalar rfc822(@_));
  476. }
  477. =pod
  478. =head1 COMMON OUTPUT OPTIONS
  479. The following options are supported by all output methods,
  480. all take a version number as value:
  481. =over 4
  482. =item since
  483. Causes changelog information from all versions strictly
  484. later than B<version> to be used.
  485. =item until
  486. Causes changelog information from all versions strictly
  487. earlier than B<version> to be used.
  488. =item from
  489. Similar to C<since> but also includes the information for the
  490. specified B<version> itself.
  491. =item to
  492. Similar to C<until> but also includes the information for the
  493. specified B<version> itself.
  494. =back
  495. The following options are also supported by all output methods but
  496. don't take version numbers as values:
  497. =over 4
  498. =item all
  499. If set to a true value, all entries of the changelog are returned,
  500. this overrides all other options.
  501. =item count
  502. Expects a signed integer as value. Returns C<value> entries from the
  503. top of the changelog if set to a positive integer, and C<abs(value)>
  504. entries from the tail if set to a negative integer.
  505. =item offset
  506. Expects a signed integer as value. Changes the starting point for
  507. C<count>, either counted from the top (positive integer) or from
  508. the tail (negative integer). C<offset> has no effect if C<count>
  509. wasn't given as well.
  510. =back
  511. Some examples for the above options. Imagine an example changelog with
  512. entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1.
  513. Call Included entries
  514. C<E<lt>formatE<gt>({ since =E<gt> '2.0' })> 3.1, 3.0, 2.2
  515. C<E<lt>formatE<gt>({ until =E<gt> '2.0' })> 1.3, 1.2
  516. C<E<lt>formatE<gt>({ from =E<gt> '2.0' })> 3.1, 3.0, 2.2, 2.1, 2.0
  517. C<E<lt>formatE<gt>({ to =E<gt> '2.0' })> 2.0, 1.3, 1.2
  518. C<E<lt>formatE<gt>({ count =E<gt> 2 }>> 3.1, 3.0
  519. C<E<lt>formatE<gt>({ count =E<gt> -2 }>> 1.3, 1.2
  520. C<E<lt>formatE<gt>({ count =E<gt> 3,
  521. offset=E<gt> 2 }>> 2.2, 2.1, 2.0
  522. C<E<lt>formatE<gt>({ count =E<gt> 2,
  523. offset=E<gt> -3 }>> 2.0, 1.3
  524. C<E<lt>formatE<gt>({ count =E<gt> -2,
  525. offset=E<gt> 3 }>> 3.0, 2.2
  526. C<E<lt>formatE<gt>({ count =E<gt> -2,
  527. offset=E<gt> -3 }>> 2.2, 2.1
  528. Any combination of one option of C<since> and C<from> and one of
  529. C<until> and C<to> returns the intersection of the two results
  530. with only one of the options specified.
  531. =head1 UTILITY FUNCTIONS
  532. =head3 data2rfc822
  533. Takes a single argument, either a Dpkg::Changelog::Entry object
  534. or a reference to an array of such objects.
  535. Returns the data in RFC822 format as string.
  536. =cut
  537. sub data2rfc822 {
  538. my ($data) = @_;
  539. if (ref($data) eq "ARRAY") {
  540. my @rfc822 = ();
  541. foreach my $entry (@$data) {
  542. push @rfc822, data2rfc822($entry);
  543. }
  544. return join "\n", @rfc822;
  545. } elsif (ref($data)) {
  546. my $rfc822_str = $data->output;
  547. return $rfc822_str;
  548. } else {
  549. return;
  550. }
  551. }
  552. =pod
  553. =head3 get_dpkg_changes
  554. Takes a Dpkg::Changelog::Entry object as first argument.
  555. Returns a string that is suitable for using it in a C<Changes> field
  556. in the output format of C<dpkg-parsechangelog>.
  557. =cut
  558. sub get_dpkg_changes {
  559. my $entry = shift;
  560. my $header = $entry->get_part("header") || "";
  561. $header =~ s/\s+$//;
  562. my $changes = "\n $header\n .\n";
  563. foreach my $line (@{$entry->get_part("changes")}) {
  564. $line =~ s/\s+$//;
  565. if ($line eq "") {
  566. $changes .= " .\n";
  567. } else {
  568. $changes .= " $line\n";
  569. }
  570. }
  571. chomp $changes;
  572. return $changes;
  573. }
  574. 1;
  575. __END__
  576. =head1 AUTHOR
  577. Frank Lichtenheld, E<lt>frank@lichtenheld.deE<gt>
  578. =head1 COPYRIGHT AND LICENSE
  579. Copyright E<copy> 2005, 2007 by Frank Lichtenheld
  580. Copyright E<copy> 2009 by Raphael Hertzog
  581. This program is free software; you can redistribute it and/or modify
  582. it under the terms of the GNU General Public License as published by
  583. the Free Software Foundation; either version 2 of the License, or
  584. (at your option) any later version.
  585. This program is distributed in the hope that it will be useful,
  586. but WITHOUT ANY WARRANTY; without even the implied warranty of
  587. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  588. GNU General Public License for more details.
  589. You should have received a copy of the GNU General Public License
  590. along with this program; if not, write to the Free Software
  591. Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  592. =cut