po: Update Dutch man pages translations
[dpkg.git] / build-aux / gen-changelog
blobad1444c94215c25a3d94abd805df8924dc7157e5
1 #!/usr/bin/perl
3 # gen-changelog
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/>.
21 use strict;
22 use warnings;
24 use lib qw(scripts);
26 use List::Util qw(uniq);
27 use Text::Wrap;
28 use Dpkg::IPC;
29 use Dpkg::Index;
31 my @sections = qw(
32 main
33 arch
34 port
35 perl-mod
36 make-mod
37 shell-mod
38 doc
39 code-int
40 build-sys
41 pkg
42 test
43 l10n
46 my %sections = (
47 arch => {
48 title => 'Architecture support',
49 match => qr/^arch: /,
51 port => {
52 title => 'Portability',
53 type => 'porting',
55 'perl-mod' => {
56 title => 'Perl modules',
57 match => qr/^(?:Test|Dpkg|Dselect).*[,:] /,
58 keep => 1,
60 'make-mod' => {
61 title => 'Make fragments',
62 match => qr{^scripts/mk: },
64 'shell-mod' => {
65 title => 'Shell library',
66 match => qr{^src/sh: },
68 doc => {
69 title => 'Documentation',
70 match => qr/^(?:doc|man)[,:] /,
71 keep => 1,
73 'code-int' => {
74 title => 'Code internals',
75 type => 'internal',
76 match => qr/^(?:lib(?:compat|dpkg)?|src|scripts|perl|utils): /,
77 keep => 1,
79 'build-sys' => {
80 title => 'Build system',
81 match => qr/^build: /,
83 pkg => {
84 title => 'Packaging',
85 match => qr/^debian: /,
87 test => {
88 title => 'Test suite',
89 match => qr/^(?:test|t): /,
91 l10n => {
92 title => 'Localization',
93 match => qr/^po: /,
94 sort => 1,
98 my @metafields = qw(
99 Thanks-to
100 Co-Author
101 Based-on-patch-by
102 Improved-by
103 Prompted-by
104 Reported-by
105 Required-by
106 Analysis-by
107 Requested-by
108 Suggested-by
109 Spotted-by
110 Naming-by
114 my %metafield = (
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',
127 'Ref' => 'See',
130 my %mappings = (
131 'u-a' => 'update-alternatives',
132 's-s-d' => 'start-stop-daemon',
133 'dpkg-m-h' => 'dpkg-maintscript-helper',
136 my $log_format =
137 'Commit: %H%n' .
138 'Author: %aN%n' .
139 'AuthorEmail: %aE%n' .
140 'Committer: %cN%n' .
141 'CommitterEmail: %cE%n' .
142 'Title: %s%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);
149 chomp $tag_prev;
151 my $fh_gitlog;
153 spawn(
154 exec => [
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} },
163 item_opts => {
164 keep_duplicate => 1,
165 allow_duplicate => 1,
168 $log->parse($fh_gitlog, 'git log');
170 my %entries;
171 my %groups;
172 my @groups;
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) /) {
184 next;
186 if ($title =~ m/^po: Regenerate/) {
187 next;
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/([^/]+)/([^/]+)/}) {
201 $commit->{Fixup} = {
202 old => $1,
203 new => $2,
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;
216 last;
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) {
230 print "\n";
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";
242 my @entries;
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}//;
251 # Metafields.
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;
275 # Handle mappings.
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}) {
283 $entry_tab = ' * ';
284 $body_tab = ' ';
285 } else {
286 $entry_tab = ' - ';
287 $body_tab = ' ';
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);
301 print @entries;