<commit_before>#!/usr/bin/perl -w
#-
# Copyright (c) 2001 <NAME>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer
#    in this position and unchanged.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#      $FreeBSD$
#

use strict;
use Data::Dumper;
use Fcntl;
use POSIX qw(isatty mktime strftime tzset);
use vars qw($TTY $NOW %MONTH %PR @EVENTS @COUNT @AGE);
use vars qw(%STATE %CATEGORY %OWNER %CLOSER);

%MONTH = (
    'Jan' => 1,
    'Feb' => 2,
    'Mar' => 3,
    'Apr' => 4,
    'May' => 5,
    'Jun' => 6,
    'Jul' => 7,
    'Aug' => 8,
    'Sep' => 9,
    'Oct' => 10,
    'Nov' => 11,
    'Dec' => 12,
);

@AGE = (
    [ 0,	7,	0 ],	# Less than one week
    [ 7,	30,	0 ],	# One week to one month
    [ 30,	90,	0 ],	# One to three months
    [ 90,	365,	0 ],	# Three months to a year
    [ 365,	1095,	0 ],	# One to three years
    [ 1095,	999999,	0 ],	# More than three years
);

sub GNATS_DIR			{ "/home/gnats" }
sub GNATS_TZ			{ "America/Los_Angeles" }
sub DATFILE			{ "/tmp/prstats.dat.$$" }
sub GNUPLOT			{ "|/usr/local/bin/gnuplot /dev/stdin" }
sub TIMEFMT			{ "%Y-%m-%d/%H:%M:%S" }

sub parse_date($) {
    my $date = shift;		# Date to parse

    my $year;
    my $month;
    my $day;
    my $hour;
    my $minute;
    my $second;

    $date =~ s/\s+/ /g;
    $date =~ s/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s*//;
    if ($date =~ m/^(\w{3}) (\d\d?) (\d\d):(\d\d):(\d\d) [A-Z ]*(\d{4})$/) {
	($month, $day, $hour, $minute, $second, $year) =
	    ($1, $2, $3, $4, $5, $6);
    } else {
	die("Unrecognized date format: $date\n");
    }
    defined($month = $MONTH{$month})
	or die("Invalid month: $month\n");
    return mktime($second, $minute, $hour, $day, $month - 1, $year - 1900);
}

sub scan_pr($) {
    my $fn = shift;		# File name

    local *FILE;		# File handle
    my $pr = {};		# PR hash
    my $age;			# PR age

    sysopen(FILE, $fn, O_RDONLY)
	or die("$fn: open(): $!\n");
    while (<FILE>) {
	if (m/^>([A-Za-z-]+):\s+(.*?)\s*$/o ||
	    m/^(Category|Responsible|State-Changed-[A-Za-z-]+):\s+(.*?)\s*$/o) {
	    $pr->{lc($1)} = $2;
	}
    }
    
    exists($PR{$pr->{'number'}})
	and die("$fn: PR $pr->{'number'} already exists\n");

    if ($TTY) {
	print(" "x40, "\r", scalar(keys(%PR)),
	      " $pr->{'category'}/$pr->{'number'} ");
    }

    foreach ('arrival-date', 'closed-date', 'last-modified',
	     'state-changed-when') {
	if (defined($pr->{$_}) && length($pr->{$_})) {
	    $pr->{$_} = parse_date($pr->{$_});
	}
    }

    $pr->{'_created'} = $pr->{'arrival-date'};
    if ($pr->{'state'} eq 'closed') {
	$pr->{'_closed'} = $pr->{'closed-date'} || $pr->{'state-changed-when'};
	$pr->{'_closed_by'} = $pr->{'state-changed-by'};
	++$CLOSER{$pr->{'_closed_by'}};
    } else {
	$age = $pr->{'arrival-date'} / 86400;
	foreach (@AGE) {
	    if ($age >= $_->[0] && $age < $_->[1]) {
		++$_->[2];
		last;
	    }
	}
	++$CATEGORY{$pr->{'category'}};
	++$OWNER{$pr->{'responsible'}};
    }
    ++$STATE{$pr->{'state'}};

    $PR{$pr->{'number'}} = {
  	'category'	=> $pr->{'category'},
  	#'number'	=> $pr->{'number'},
  	'responsible'	=> $pr->{'responsible'},
  	'created'	=> $pr->{'created'},
  	'closed'	=> $pr->{'closed'},
  	'closer'	=> $pr->{'_closed_by'},
    };
    push(@EVENTS, [ $pr->{'_created'}, +1 ]);
    push(@EVENTS, [ $pr->{'_closed'}, -1 ])
	    if defined($pr->{'_closed'});
}
<commit_msg>Long-forgotten patch: warn if a closed PR has no "closed by"<commit_after>#!/usr/bin/perl -w
#-
# Copyright (c) 2001 <NAME>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer
#    in this position and unchanged.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#      $FreeBSD$
#

use strict;
use Data::Dumper;
use Fcntl;
use POSIX qw(isatty mktime strftime tzset);
use vars qw($TTY $NOW %MONTH %PR @EVENTS @COUNT @AGE);
use vars qw(%STATE %CATEGORY %OWNER %CLOSER);

%MONTH = (
    'Jan' => 1,
    'Feb' => 2,
    'Mar' => 3,
    'Apr' => 4,
    'May' => 5,
    'Jun' => 6,
    'Jul' => 7,
    'Aug' => 8,
    'Sep' => 9,
    'Oct' => 10,
    'Nov' => 11,
    'Dec' => 12,
);

@AGE = (
    [ 0,	7,	0 ],	# Less than one week
    [ 7,	30,	0 ],	# One week to one month
    [ 30,	90,	0 ],	# One to three months
    [ 90,	365,	0 ],	# Three months to a year
    [ 365,	1095,	0 ],	# One to three years
    [ 1095,	999999,	0 ],	# More than three years
);

sub GNATS_DIR			{ "/home/gnats" }
sub GNATS_TZ			{ "America/Los_Angeles" }
sub DATFILE			{ "/tmp/prstats.dat.$$" }
sub GNUPLOT			{ "|/usr/local/bin/gnuplot /dev/stdin" }
sub TIMEFMT			{ "%Y-%m-%d/%H:%M:%S" }

sub parse_date($) {
    my $date = shift;		# Date to parse

    my $year;
    my $month;
    my $day;
    my $hour;
    my $minute;
    my $second;

    $date =~ s/\s+/ /g;
    $date =~ s/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s*//;
    if ($date =~ m/^(\w{3}) (\d\d?) (\d\d):(\d\d):(\d\d) [A-Z ]*(\d{4})$/) {
	($month, $day, $hour, $minute, $second, $year) =
	    ($1, $2, $3, $4, $5, $6);
    } else {
	die("Unrecognized date format: $date\n");
    }
    defined($month = $MONTH{$month})
	or die("Invalid month: $month\n");
    return mktime($second, $minute, $hour, $day, $month - 1, $year - 1900);
}

sub scan_pr($) {
    my $fn = shift;		# File name

    local *FILE;		# File handle
    my $pr = {};		# PR hash
    my $age;			# PR age

    sysopen(FILE, $fn, O_RDONLY)
	or die("$fn: open(): $!\n");
    while (<FILE>) {
	if (m/^>([A-Za-z-]+):\s+(.*?)\s*$/o ||
	    m/^(Category|Responsible|State-Changed-[A-Za-z-]+):\s+(.*?)\s*$/o) {
	    $pr->{lc($1)} = $2;
	}
    }
    
    exists($PR{$pr->{'number'}})
	and die("$fn: PR $pr->{'number'} already exists\n");

    if ($TTY) {
	print(" "x40, "\r", scalar(keys(%PR)),
	      " $pr->{'category'}/$pr->{'number'} ");
    }

    foreach ('arrival-date', 'closed-date', 'last-modified',
	     'state-changed-when') {
	if (defined($pr->{$_}) && length($pr->{$_})) {
	    $pr->{$_} = parse_date($pr->{$_});
	}
    }

    $pr->{'_created'} = $pr->{'arrival-date'};
    if ($pr->{'state'} eq 'closed') {
	$pr->{'_closed'} = $pr->{'closed-date'} || $pr->{'state-changed-when'};
	$pr->{'_closed_by'} = $pr->{'state-changed-by'};
	if (!defined($pr->{'_closed_by'})) {
	    warn("PR $pr->{'category'}/$pr->{'number'} is incomplete\n");
	    return;
	}
	++$CLOSER{$pr->{'_closed_by'}};
    } else {
	$age = $pr->{'arrival-date'} / 86400;
	foreach (@AGE) {
	    if ($age >= $_->[0] && $age < $_->[1]) {
		++$_->[2];
		last;
	    }
	}
	++$CATEGORY{$pr->{'category'}};
	++$OWNER{$pr->{'responsible'}};
    }
    ++$STATE{$pr->{'state'}};

    $PR{$pr->{'number'}} = {
  	'category'	=> $pr->{'category'},
  	#'number'	=> $pr->{'number'},
  	'responsible'	=> $pr->{'responsible'},
  	'created'	=> $pr->{'created'},
  	'closed'	=> $pr->{'closed'},
  	'closer'	=> $pr->{'_closed_by'},
    };
    push(@EVENTS, [ $pr->{'_created'}, +1 ]);
    push(@EVENTS, [ $pr->{'_closed'}, -1 ])
	    if defined($pr->{'_closed'});
}

{'fraction_non_alphanumeric': 0.20884468895471858, 'fraction_numerical': 0.01819720694033009, 'mean_word_length': 3.1171602787456445, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 120, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5605310', 'n_tokens_mistral': 3847, 'n_tokens_neox': 3525, 'n_words': 1019}