#!/usr/bin/env perl # # InspIRCd -- Internet Relay Chat Daemon # # Copyright (C) 2020-2024 Sadie Powell # # This file is part of InspIRCd. InspIRCd is free software: you can # redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # use v5.26.0; use strict; use warnings FATAL => qw(all); use File::Basename qw(dirname); use File::Util (); use FindBin qw($RealDir); use List::Util qw(uniq); use POSIX qw(strftime); use lib dirname $RealDir; use make::common; use make::console; my @ignored_revisions = ( '46a39046196f55b52336e19662bb7bac85b731ac', # adding copyright headers '4a6fedd9324d87349a806c9c1d0ae6e7d3c1fd38', # mass-updating descriptions 'bab14f0dd2345c9d7dcbc47c918563709e1ac094', # peavey breaking line endings 'de6d4dbd1e8845e08c2d87cd89a919e5b21ba619', # jsoref fixing a ton of typos 'f2acdbc3820f0f4f5ef76a0a64e73d2a320df91f', # peavey fixing line endings ); sub make_range($@) { my ($separator, @nums) = @_; my ($last_num, $start_num, @num_ranges); for my $num (uniq sort { $a <=> $b } @nums) { if (!defined $last_num) { $start_num = $last_num = $num } elsif ($num == $last_num + 1) { $last_num = $num; } else { if ($last_num == $start_num) { push @num_ranges, $last_num; } else { push @num_ranges, "$start_num$separator$last_num"; } $start_num = $last_num = $num; } } if (defined $last_num) { if ($last_num == $start_num) { push @num_ranges, $last_num; } else { push @num_ranges, "$start_num$separator$last_num"; } } return @num_ranges; } my @paths = File::Util->new->list_dir(dirname($RealDir) => { recurse => 1 }); my @updated; for my $path (@paths) { next unless -f $path; next if $path =~ /\/\./; next if $path =~ /\/build\//; next if $path =~ /\/vendor\//; if (system "git ls-files --error-unmatch -- $path 1>/dev/null 2>/dev/null") { say STDERR console_format "Skipping <|YELLOW $path|> as it has not been committed." if defined $ENV{MKHEADERS_VERBOSE}; next; } open(my $fh, $path) or print_error "unable to read from $path: $!"; my ($copyright, $indent, $linenum, @linenums, @lines); for my $line (<$fh>) { $linenum += 1; chomp $line; if ($line =~ /^([^0-9A-Za-z]+\s)Copyright\s+\(C\)\s+[^<]+(\s+<[^>]+>)?[\r\s]*$/) { $copyright = scalar @lines; $indent = $1; } else { push @lines, $line; push @linenums, $linenum; } } close $fh; if (defined $copyright) { say console_format "Updating copyright headers in <|GREEN $path|>." if defined $ENV{MKHEADERS_VERBOSE}; my (%authors, $commit, %commits); my $ignored_args = join ' ', map { "--ignore-rev $_" } @ignored_revisions; my $line_args = join ' ', map { "-L $_" } make_range ',', @linenums; for my $line (split /\n+/, `git blame $ignored_args $line_args --incremental -M -w HEAD -- $path`) { if ($line =~ /^([0-9a-f]{40})(?:\s\d+){3}$/) { $commit = $1; $commits{$commit} //= {}; } elsif ($line =~ /^author (.+)/) { $commits{$commit}->{NAME} = $1; } elsif ($line =~ /^author-mail <(.+)>/) { next if $1 eq 'unknown@email.invalid'; next if $1 =~ /\@users.noreply.github.com$/; $commits{$commit}->{EMAIL} = $1; } elsif ($line =~ /^author-time (.+)/) { $commits{$commit}->{YEAR} = strftime '%Y', gmtime $1; } elsif ($line =~ /^filename /) { my $display = $commits{$commit}->{NAME}; if (exists $commits{$commit}->{EMAIL}) { $display .= sprintf " <%s>", $commits{$commit}->{EMAIL}; } $authors{$display} //= []; push @{$authors{$display}}, $commits{$commit}->{YEAR}; my $details = `git rev-list --format=%B --max-count=1 $commit`; while ($details =~ /co-authored-by: ([^<]+<[^>]+>)/gi) { open(my $fh, '-|', 'git', 'check-mailmap', $1); chomp(my $coauthor = <$fh>); close $fh; my $year = $details =~ /co-authored-year: (\d+)/i ? $1 : $commits{$commit}->{YEAR}; $authors{$coauthor} //= []; push @{$authors{$coauthor}}, $year; } undef $commit; } } my @copyrights; while (my ($display, $years) = each %authors) { next if $display eq 'InspIRCd Robot '; my @year_ranges = make_range '-', @$years; my $joined_years = join ', ', @year_ranges; push @copyrights, "${\$indent}Copyright (C) $joined_years $display"; } splice @lines, $copyright, 0, reverse sort @copyrights; open(my $fh, '>', $path) or print_error "unable to write to $path: $!"; for my $line (@lines) { say $fh $line; } close $fh; push @updated, $path; } else { say STDERR console_format "Skipping <|YELLOW $path|> as it contains no copyright headers." if defined $ENV{MKHEADERS_VERBOSE}; } } if ($ENV{MKHEADERS_COMMIT} // 1) { execute 'git', 'commit', '--author', 'InspIRCd Robot ', '--message', 'Update copyright headers.', '--', @updated; }