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);
47 title
=> 'Architecture support',
51 title
=> 'Portability',
55 title
=> 'Perl modules',
56 match
=> qr/^(?:Test|Dpkg|Dselect).*[,:] /,
60 title
=> 'Make fragments',
61 match
=> qr{^scripts/mk: },
64 title
=> 'Documentation',
65 match
=> qr/^(?:doc|man)[,:] /,
69 title
=> 'Code internals',
71 match
=> qr/^(?:lib(?:compat|dpkg)?|src|scripts|perl|utils): /,
75 title
=> 'Build system',
76 match
=> qr/^build: /,
80 match
=> qr/^debian: /,
83 title
=> 'Test suite',
84 match
=> qr/^(?:test|t): /,
87 title
=> 'Localization',
110 'Co-Author' => 'Co-authored by',
111 'Based-on-patch-by' => 'Based on a patch by',
112 'Improved-by' => 'Improved by',
113 'Prompted-by' => 'Prompted by',
114 'Reported-by' => 'Reported by',
115 'Required-by' => 'Required by',
116 'Analysis-by' => 'Analysis by',
117 'Requested-by' => 'Requested by',
118 'Suggested-by' => 'Suggested by',
119 'Spotted-by' => 'Spotted by',
120 'Naming-by' => 'Naming by',
121 'Thanks-to' => 'Thanks to',
126 'u-a' => 'update-alternatives',
127 's-s-d' => 'start-stop-daemon',
128 'dpkg-m-h' => 'dpkg-maintscript-helper',
134 'AuthorEmail: %aE%n' .
136 'CommitterEmail: %cE%n' .
138 '%(trailers:only,unfold)%N';
140 my $tag_prev = $ARGV[0];
141 my $tag_next = $ARGV[1] // "";
143 $tag_prev //= qx(git describe
--abbrev
=0);
150 qw(git log --first-parent), "--format=tformat:$log_format",
151 "$tag_prev..$tag_next"
153 to_pipe
=> \
$fh_gitlog,
156 my $log = Dpkg
::Index
->new(
157 get_key_func
=> sub { return $_[0]->{Commit
} },
160 allow_duplicate
=> 1,
163 $log->parse($fh_gitlog, 'git log');
169 # Analyze the commits and select which group and section to place them in.
170 foreach my $id (reverse $log->get_keys()) {
171 my $commit = $log->get_by_key($id);
172 my $title = $commit->{Title
};
173 my $group = $commit->{Committer
};
174 my $changelog = $commit->{'Changelog'};
175 my $sectmatch = 'main';
177 # Skip irrelevant commits.
178 if ($title =~ m/^(?:Bump version to|Release) /) {
181 if ($title =~ m/^po: Regenerate/) {
185 if (defined $changelog) {
186 # Skip silent commits.
187 next if $changelog =~ m/(?:silent|skip|ignore)/;
189 # Include the entire commit body for verbose commits.
190 if ($changelog =~ m/(?:verbose|full)/) {
191 my $body = qx(git show
-s
--pretty
=tformat
:%b $id);
192 $commit->{Title
} .= "\n$body";
195 if ($changelog =~ m{s/([^/]+)/([^/]+)/}) {
203 # Decide into what section the commit should go.
204 foreach my $sectname (keys %sections) {
205 my $section = $sections{$sectname};
207 if ((exists $section->{match
} and $title =~ m/$section->{match}/) or
208 (exists $section->{type
} and defined $changelog and
209 $changelog =~ m/$section->{type}/)) {
210 $sectmatch = $sectname;
215 # Add the group entries in order, with l10n ones at the end.
216 if (not exists $entries{$group}) {
217 push @groups, $group;
220 push @
{$entries{$group}{$sectmatch}}, $commit;
223 # Go over the groups and their sections and format them.
224 foreach my $groupname (@groups) {
226 print " [ $groupname ]\n";
228 foreach my $sectname (@sections) {
229 my $section = $sections{$sectname};
231 next unless exists $entries{$groupname}{$sectname};
232 next if @
{$entries{$groupname}{$sectname}} == 0;
234 if (exists $sections{$sectname}->{title
}) {
235 print " * $sections{$sectname}->{title}:\n";
238 foreach my $commit (@
{$entries{$groupname}{$sectname}}) {
239 my $title = $commit->{Title
} =~ s/\.$//r . '.';
241 # Remove the title prefix if needed.
242 if (exists $section->{match
} and not exists $section->{keep
}) {
243 $title =~ s/$section->{match}//;
247 if ($commit->{Author
} ne $commit->{Committer
}) {
248 $commit->{'Thanks-to'} = "$commit->{Author} <$commit->{AuthorEmail}>";
250 foreach my $metafield (@metafields) {
251 next unless exists $commit->{$metafield};
253 my $values = $commit->{$metafield};
254 $values = [ $values ] if ref $values ne 'ARRAY';
256 foreach my $value (@
{$values}) {
257 $title .= "\n$metafield{$metafield} $value.";
260 # Handle the Closes metafield last.
261 if (exists $commit->{Closes
}) {
262 $title .= " Closes: $commit->{Closes}";
265 # Handle fixups from git notes.
266 if (exists $commit->{Fixup
}) {
267 $title =~ s/\Q$commit->{Fixup}{old}\E/$commit->{Fixup}{new}/m;
271 foreach my $mapping (keys %mappings) {
272 $title =~ s/$mapping/$mappings{$mapping}/g;
275 # Select prefix formatting.
276 my ($entry_tab, $body_tab);
277 if (not exists $sections{$sectname}->{title
}) {
285 local $Text::Wrap
::columns
= 80;
286 local $Text::Wrap
::unexpand
= 0;
287 local $Text::Wrap
::huge
= 'overflow';
288 local $Text::Wrap
::break = qr/(?<!Closes:)\s/;
289 push @entries, wrap
($entry_tab, $body_tab, $title) . "\n";
292 if ($sections{$sectname}->{sort}) {
293 @entries = uniq
(sort @entries);