8 my $workdir = './openwrt-changelog-data';
10 unless (defined $range) {
11 printf STDERR
"Usage: $0 range\n";
15 unless (-d
$workdir) {
16 unless (system('mkdir', '-p', $workdir) == 0) {
17 printf STDERR
"Unable to create work directory!\n";
22 my $commit_url = 'https://git.openwrt.org/?p=openwrt/openwrt.git;a=commitdiff;h=%s';
25 [ qr
'^[^:]+://(git.lede-project.org/)(.+)$' => 'https://%s?p=%s;a=commitdiff;h=%%s' ],
26 [ qr
'^[^:]+://(git.openwrt.org/)(.+)$' => 'https://%s?p=%s;a=commitdiff;h=%%s' ],
27 [ qr
'^[^:]+://(github.com/.+?)(?:\.git)?$' => 'https://%s/commit/%%s' ],
28 [ qr
'^[^:]+://git.kernel.org/pub/scm/(.+)$' => 'https://git.kernel.org/cgit/%s/commit/?id=%%s' ],
29 [ qr
'^[^:]+://w1.fi/(?:.+/)?(.+)\.git$' => 'https://w1.fi/cgit/%s/commit/?id=%%s' ],
30 [ qr
'^[^:]+://git.netfilter.org/(.+)' => 'https://git.netfilter.org/%s/commit/?id=%%s' ],
31 [ qr
'^[^:]+://git.musl-libc.org/(.+)' => 'https://git.musl-libc.org/cgit/%s/commit/?id=%%s' ],
32 [ qr
'^[^:]+://git.zx2c4.com/(.+)' => 'https://git.zx2c4.com/%s/commit/?id=%%s' ],
43 my ($fh, $default) = @_;
45 my $line = readline $fh;
57 [ qr
'^package/(kernel)/linux', 'Kernel' ],
58 [ qr
'^(target/linux/generic|include/kernel-version.mk)', 'Kernel' ],
59 [ qr
'^package/kernel/(mac80211)', 'Wireless / Common' ],
60 [ qr
'^package/kernel/(ath10k-ct)', 'Wireless / Ath10k CT' ],
61 [ qr
'^package/kernel/(mt76)', 'Wireless / MT76' ],
62 [ qr
'^package/(base-files)/', 'Packages / LEDE base files' ],
63 [ qr
'^package/(boot)/', 'Packages / Boot Loaders' ],
64 [ qr
'^package/firmware/', 'Packages / Firmware' ],
65 [ qr
'^package/.+/(uhttpd|usbmode|jsonfilter|ugps|libubox|procd|mountd|ubus|uci|usign|rpcd|fstools|ubox)/', 'Packages / LEDE system userland' ],
66 [ qr
'^package/.+/(iwinfo|umbim|uqmi|relayd|mdns|firewall|netifd|uclient|ustream-ssl|gre|ipip|qos-scripts|swconfig|vti|6in4|6rd|6to4|ds-lite|map|odhcp6c|odhcpd)/', 'Packages / LEDE network userland' ],
67 [ qr
'^package/[^/]+/([^/]+)', 'Packages / Common' ],
68 [ qr
'^target/sdk/', 'Build System / SDK' ],
69 [ qr
'^target/imagebuilder/', 'Build System / Image Builder' ],
70 [ qr
'^target/toolchain/', 'Build System / Toolchain' ],
71 [ qr
'^target/linux/([^/]+)', 'Target / $1' ],
72 [ qr
'^(tools)/[^/]+', 'Build System / Host Utilities' ],
73 [ qr
'^(toolchain)/[^/]+', 'Build System / Toolchain' ],
74 [ qr
'^(config/|include/|scripts/|target/[^/]+$|Makefile|rules\.mk)', 'Build System / Buildroot' ],
75 [ qr
'^(feeds)\b', 'Build System / Feeds' ],
78 my @subhistory_matches = (
79 qr
'(?i)^\S+: update to\b',
80 qr
'(?i)^\S+: Upstep to\b',
81 qr
'(?i)^\S+: bump to\b',
83 qr
'(?i)^\S+: backport\b',
84 qr
'(?i)\blatest HEAD\b',
93 foreach my $rs (@topic_paths)
95 if ($path =~ $rs->[0])
108 my @topics = sort keys %topics;
109 return (@topics > 0 ?
@topics : ('Miscellaneous'));
112 sub parse_history
($$)
114 my ($dir, $range) = @_;
117 my ($max_add, $total_add, $max_del, $total_del) = (0, 0, 0, 0);
119 if (open GIT
, '-|', 'git', "--git-dir=$dir/.git", 'log', '--format=@@%n%H%n%s%n%b%n@@', '--numstat', '--reverse', '--no-merges', $range)
126 my $hash = line
(GIT
, '');
127 my $subject = line
(GIT
, '');
129 last unless (length($subject) && $hash =~ m!^!);
134 my ($add, $del) = (0, 0);
136 my $is_revert = $subject =~ m!^Revert !;
138 $reverts{$hash}++ if $is_revert;
140 while ($line ne '@@')
142 $body .= length($line) ?
"$line\n" : '';
143 $line = line
(*GIT
, '@@');
145 if ($is_revert && $line =~ m!\b([0-9a-f]{40})\b!)
153 while ($line ne '@@')
155 if ($line =~ m!^(\d+|-)\s+(\d+|-)\s+(.+)$!)
157 $add += ($1 eq '-') ?
0 : int($1);
158 $del += ($2 eq '-') ?
0 : int($2);
162 $line = line
(*GIT
, '@@');
180 $max_add = ($add > $max_add) ?
$add : $max_add;
181 $max_del = ($del > $max_del) ?
$del : $max_del;
183 push @commits, $commit;
189 if (@commits > 0 && $commits[0][2] =~ /\brevert to branch defaults$/)
194 return wantarray ?
@commits : \
@commits;
197 sub fetch_subhistory
($$$)
199 my ($url, $old, $new) = @_;
201 (my $path = $url) =~ s![^a-z0-9_-]+!-!g;
203 unless (-d
"$workdir/repos/$path")
205 mkdir("$workdir/repos");
206 system('git', 'clone', '--quiet', $url, "$workdir/repos/$path");
210 system('git', "--work-tree=$workdir/repos/$path", "--git-dir=$workdir/repos/$path/.git", 'pull', '--quiet');
213 return parse_history
("$workdir/repos/$path", "$old..$new");
216 sub requires_subhistory
($$$)
218 my ($subject, $body, $hash) = @_;
220 foreach my $re (@subhistory_matches)
222 if ($subject =~ $re || $body =~ $re)
224 if (open DIFF
, '-|', 'git', 'diff', "$hash^!")
226 my ($url, $old, $new);
228 while (defined(my $line = readline DIFF
))
232 if ($line =~ m!^[ +]PKG_SOURCE_URL\s*:?=\s*(\S+)!)
235 $url =~ s!\$\(LEDE_GIT\)!https://git.lede-project.org!g;
236 $url =~ s!\$\(OPENWRT_GIT\)!https://git.openwrt.org!g;
237 $url =~ s!\$\(PROJECT_GIT\)!https://git.openwrt.org!g;
239 elsif ($line =~ m!^-\S+\s*:?=\s*([a-f0-9]{40})\b!)
243 elsif ($line =~ m!^\+\S+\s*:?=\s*([a-f0-9]{40})\b!)
248 if ($url && $old && $new)
250 return ($url, $old, $new);
262 sub find_weblink_template
($)
266 foreach my $rt (@weblinks)
268 my @m = $url =~ $rt->[0];
271 return sprintf $rt->[1], @m;
275 warn "No web link template for <$url>\n";
284 my $c = '<color #ccc>%s</color>';
285 my $g = '<color #282>%s</color>';
286 my $r = '<color #f00>%s</color>';
288 if ($commit->[7] > 1000)
290 $s .= sprintf $g, sprintf '+%.1fK', $commit->[7] / 1000;
292 elsif ($commit->[7] > 0)
294 $s .= sprintf $g, sprintf '+%d', $commit->[7];
297 if ($commit->[8] > 1000)
299 $s .= $s ?
sprintf($c, ',') : '';
300 $s .= sprintf $r, sprintf '-%.1fK', $commit->[8] / 1000;
302 elsif ($commit->[8] > 0)
304 $s .= $s ?
sprintf($c, ',') : '';
305 $s .= sprintf $r, sprintf '-%d', $commit->[8];
308 return sprintf($c, '(') . $s . sprintf($c, ')');
311 sub format_subject
($$)
313 my ($subject, $body) = @_;
315 if (length($subject) > 80)
317 $subject = substr($subject, 0, 77) . '...';
320 $subject =~ s!^([^\s:]+):\s*!</nowiki>**<nowiki>$1:</nowiki>** <nowiki>!g;
322 $subject = sprintf '<nowiki>%s</nowiki>', $subject;
323 $subject =~ s!<nowiki></nowiki>!!g;
332 printf "''[[%s|%s]]'' %s //%s//\\\\\n",
333 sprintf($commit_url, $change->[1]),
334 substr($change->[1], 0, 7),
335 format_subject
($change->[2], $change->[3]),
336 format_stat
($change);
341 foreach my $subchange (@
{$change->[6]})
345 printf " => ''[[%s|%s]]'' %s //%s//\\\\\n",
346 sprintf($change->[5], $subchange->[1]),
347 substr($subchange->[1], 0, 7),
348 format_subject
($subchange->[2], $subchange->[3]),
349 format_stat
($subchange);
353 printf " => ''%s'' %s //%s//\\\\\n",
354 substr($subchange->[1], 0, 7),
355 format_subject
($subchange->[2], $subchange->[3]),
356 format_stat
($subchange);
359 if (++$n > 15 && @
{$change->[6]} > $n)
361 printf " => + //%u more...//\\\\\n", @
{$change->[6]} - $n;
370 unless (-f
"$workdir/cveinfo.csv")
372 system('wget', '-O', "$workdir/cveinfo.csv.gz", 'https://cve.mitre.org/data/downloads/allitems.csv.gz') && return 0;
373 system('gunzip', '-f', "$workdir/cveinfo.csv.gz") && return 0;
381 my $csv = Text
::CSV
->new({ binary
=> 1 });
384 if (fetch_cve_info
() && $csv)
386 if (open CVE
, '<', "$workdir/cveinfo.csv")
388 while (defined(my $row = $csv->getline(*CVE
)))
390 foreach my $cve_id (@_)
392 if ($row->[0] eq $cve_id)
394 $cves{$cve_id} = [$row->[2], $row->[6]];
409 unless (-f
"$workdir/buginfo.csv")
411 system('wget', '-O', "$workdir/buginfo.csv", 'https://bugs.openwrt.org/index.php?string=&project=2&do=index&export_list=Export+Tasklist&advancedsearch=on&type%5B%5D=&sev%5B%5D=&pri%5B%5D=&due%5B%5D=&reported%5B%5D=&cat%5B%5D=&status%5B%5D=&percent%5B%5D=&opened=&dev=&closed=&duedatefrom=&duedateto=&changedfrom=&changedto=&openedfrom=&openedto=&closedfrom=&closedto=') && return 0;
419 my $csv = Text
::CSV
->new({ binary
=> 1, allow_loose_quotes
=> 1 });
422 if (fetch_bug_info
() && $csv)
424 if (open BUG
, '<', "$workdir/buginfo.csv")
426 while (defined(my $row = $csv->getline(*BUG
)))
428 foreach my $bug_id (@_)
430 if ($row->[0] eq $bug_id)
432 $bugs{$bug_id} = [$row->[4], $row->[5]];
448 my @commits = parse_history
('.', $range);
451 foreach my $commit (@commits)
453 my @topics = match_topics
(@
{$commit->[4]});
455 unless ($commit->[5])
457 my ($su, $so, $sn) = requires_subhistory
($commit->[2], $commit->[3], $commit->[1]);
459 $commit->[5] = find_weblink_template
($su);
460 $commit->[6] = fetch_subhistory
($su, $so, $sn);
464 foreach my $topic (@topics)
466 $topics{$topic} ||= [ ];
467 push @
{$topics{$topic}}, $commit;
470 my (%bug_ids, %cve_ids);
472 foreach my $bug ($commit->[2] =~ m!\b((?:[Pp]ull [Rr]equest |[Bb]ug |[Ii]ssue |PR |FS |GH |PR|FS|GH)#\d+)\b!g,
473 $commit->[3] =~ m!\b((?:[Pp]ull [Rr]equest |[Bb]ug |[Ii]ssue |PR |FS |GH |PR|FS|GH)#\d+)\b!g)
475 if ($bug =~ m!^(?:Bug |Issue |FS |GH |FS|GH)#(\d+)$!i)
481 foreach my $cve ($commit->[2] =~ m!\b(CVE-\d+-\d+|\d+-CVE-\d+)\b!g,
482 $commit->[3] =~ m!\b(CVE-\d+-\d+|\d+-CVE-\d+)\b!g)
484 # fix misspelled CVE IDs
485 $cve =~ s!^(\d+)-CVE-!CVE-$1-!;
489 foreach my $bug (keys %bug_ids)
492 push @
{$bugs{$bug}}, $commit;
495 foreach my $cve (keys %cve_ids)
498 push @
{$cves{$cve}}, $commit;
503 my @topics = sort { (($a eq 'Miscellaneous') <=> ($b eq 'Miscellaneous')) || $a cmp $b } keys %topics;
505 foreach my $topic (@topics)
507 my @commits = grep { !$reverts{$_->[1]} } @
{$topics{$topic}};
509 printf "==== %s (%d change%s) ====\n", $topic, 0 + @commits, @commits > 1 ?
's' : '';
511 foreach my $change (sort { $a->[0] <=> $b->[0] } @commits)
513 format_change
($change);
519 my @bugs = sort { int($a) <=> int($b) } keys %bugs;
520 my $bug_info = parse_bugs
(@bugs);
522 @bugs = grep { $bug_info->{$_} && $bug_info->{$_}[0] } @bugs;
526 printf "===== Addressed bugs =====\n";
528 foreach my $bug (@bugs)
530 printf "=== #%s ===\n", $bug;
531 printf "**Description:** <nowiki>%s</nowiki>\\\\\n", $bug_info->{$bug}[0];
532 printf "**Link:** [[https://bugs.openwrt.org/index.php?do=details&task_id=%s]]\\\\\n", $bug;
533 printf "**Commits:**\\\\\n";
535 foreach my $commit (@
{$bugs{$bug}})
537 format_change
($commit);
548 sort { ($a->[0] <=> $b->[0]) || ($a->[1] cmp $b->[1]) }
549 map { $_ =~ m!^CVE-(\d+)-(\d+)$! ?
[ $1 * 10000000 + $2, $_ ] : [ 0, $_ ] }
552 my $cve_info = parse_cves
(@cves);
556 printf "===== Security fixes ====\n";
558 foreach my $cve (@cves)
560 printf "=== %s ===\n", $cve;
562 if ($cve_info->{$cve} && $cve_info->{$cve}[0])
564 printf "**Description:** <nowiki>%s</nowiki>\n\n", $cve_info->{$cve}[0];
567 printf "**Link:** [[https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s]]\\\\\n", $cve;
568 printf "**Commits:**\\\\\n";
570 foreach my $commit (@
{$cves{$cve}})
572 format_change
($commit);