5 # Copyright © 2020-2022 Guillem Jover <guillem@debian.org>
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.
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.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <https://www.gnu.org/licenses/>.
26 use List
::Util
qw(uniq);
48 title
=> 'Architecture support',
52 title
=> 'Portability',
56 title
=> 'Perl modules',
57 match
=> qr/^(?:Test|Dpkg|Dselect).*[,:] /,
61 title
=> 'Make fragments',
62 match
=> qr{^scripts/mk: },
65 title
=> 'Shell library',
66 match
=> qr{^src/sh: },
69 title
=> 'Documentation',
70 match
=> qr/^(?:doc|man)[,:] /,
74 title
=> 'Code internals',
76 match
=> qr/^(?:lib(?:compat|dpkg)?|src|scripts|perl|utils): /,
80 title
=> 'Build system',
81 match
=> qr/^build: /,
85 match
=> qr/^debian: /,
88 title
=> 'Test suite',
89 match
=> qr/^(?:test|t): /,
92 title
=> 'Localization',
115 'Co-Author' => 'Co-authored by',
116 'Based-on-patch-by' => 'Based on a patch by',
117 'Improved-by' => 'Improved by',
118 'Prompted-by' => 'Prompted by',
119 'Reported-by' => 'Reported by',
120 'Required-by' => 'Required by',
121 'Analysis-by' => 'Analysis by',
122 'Requested-by' => 'Requested by',
123 'Suggested-by' => 'Suggested by',
124 'Spotted-by' => 'Spotted by',
125 'Naming-by' => 'Naming by',
126 'Thanks-to' => 'Thanks to',
131 'u-a' => 'update-alternatives',
132 's-s-d' => 'start-stop-daemon',
133 'dpkg-m-h' => 'dpkg-maintscript-helper',
139 'AuthorEmail: %aE%n' .
141 'CommitterEmail: %cE%n' .
143 '%(trailers:only,unfold)%N';
145 my $tag_prev = $ARGV[0];
146 my $tag_next = $ARGV[1] // "";
148 $tag_prev //= qx(git describe
--abbrev
=0);
155 qw(git log --first-parent), "--format=tformat:$log_format",
156 "$tag_prev..$tag_next"
158 to_pipe
=> \
$fh_gitlog,
161 my $log = Dpkg
::Index
->new(
162 get_key_func
=> sub { return $_[0]->{Commit
} },
165 allow_duplicate
=> 1,
168 $log->parse($fh_gitlog, 'git log');
174 # Analyze the commits and select which group and section to place them in.
175 foreach my $id (reverse $log->get_keys()) {
176 my $commit = $log->get_by_key($id);
177 my $title = $commit->{Title
};
178 my $group = $commit->{Committer
};
179 my $changelog = $commit->{'Changelog'};
180 my $sectmatch = 'main';
182 # Skip irrelevant commits.
183 if ($title =~ m/^(?:Bump version to|Release) /) {
186 if ($title =~ m/^po: Regenerate/) {
190 if (defined $changelog) {
191 # Skip silent commits.
192 next if $changelog =~ m/(?:silent|skip|ignore)/;
194 # Include the entire commit body for verbose commits.
195 if ($changelog =~ m/(?:verbose|full)/) {
196 my $body = qx(git show
-s
--pretty
=tformat
:%b $id);
197 $commit->{Title
} .= "\n$body";
200 if ($changelog =~ m{s/([^/]+)/([^/]+)/}) {
208 # Decide into what section the commit should go.
209 foreach my $sectname (keys %sections) {
210 my $section = $sections{$sectname};
212 if ((exists $section->{match
} and $title =~ m/$section->{match}/) or
213 (exists $section->{type
} and defined $changelog and
214 $changelog =~ m/$section->{type}/)) {
215 $sectmatch = $sectname;
220 # Add the group entries in order, with l10n ones at the end.
221 if (not exists $entries{$group}) {
222 push @groups, $group;
225 push @
{$entries{$group}{$sectmatch}}, $commit;
228 # Go over the groups and their sections and format them.
229 foreach my $groupname (@groups) {
231 print " [ $groupname ]\n";
233 foreach my $sectname (@sections) {
234 my $section = $sections{$sectname};
236 next unless exists $entries{$groupname}{$sectname};
237 next if @
{$entries{$groupname}{$sectname}} == 0;
239 if (exists $sections{$sectname}->{title
}) {
240 print " * $sections{$sectname}->{title}:\n";
243 foreach my $commit (@
{$entries{$groupname}{$sectname}}) {
244 my $title = $commit->{Title
} =~ s/\.$//r . '.';
246 # Remove the title prefix if needed.
247 if (exists $section->{match
} and not exists $section->{keep
}) {
248 $title =~ s/$section->{match}//;
252 if ($commit->{Author
} ne $commit->{Committer
}) {
253 $commit->{'Thanks-to'} = "$commit->{Author} <$commit->{AuthorEmail}>";
255 foreach my $metafield (@metafields) {
256 next unless exists $commit->{$metafield};
258 my $values = $commit->{$metafield};
259 $values = [ $values ] if ref $values ne 'ARRAY';
261 foreach my $value (@
{$values}) {
262 $title .= "\n$metafield{$metafield} $value.";
265 # Handle the Closes metafield last.
266 if (exists $commit->{Closes
}) {
267 $title .= " Closes: $commit->{Closes}";
270 # Handle fixups from git notes.
271 if (exists $commit->{Fixup
}) {
272 $title =~ s/\Q$commit->{Fixup}{old}\E/$commit->{Fixup}{new}/m;
276 foreach my $mapping (keys %mappings) {
277 $title =~ s/$mapping/$mappings{$mapping}/g;
280 # Select prefix formatting.
281 my ($entry_tab, $body_tab);
282 if (not exists $sections{$sectname}->{title
}) {
290 local $Text::Wrap
::columns
= 80;
291 local $Text::Wrap
::unexpand
= 0;
292 local $Text::Wrap
::huge
= 'overflow';
293 local $Text::Wrap
::break = qr/(?<!Closes:)\s/;
294 push @entries, wrap
($entry_tab, $body_tab, $title) . "\n";
297 if ($sections{$sectname}->{sort}) {
298 @entries = uniq
(sort @entries);