#!/usr/bin/perl -w
# $Id: release2git,v 1.90 2024/05/18 00:02:14 tom Exp $
# -----------------------------------------------------------------------------
# Copyright 2016-2023,2024 by Thomas E. Dickey
#
#                         All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Except as contained in this notice, the name(s) of the above copyright
# holders shall not be used in advertising or otherwise to promote the
# sale, use or other dealings in this Software without prior written
# authorization.
# -----------------------------------------------------------------------------
# Rather than attempt to export RCS archive to Git using rcs-fast-export.rb
# (and have to do this repeatedly), analyze an RCS archive and apply release
# snapshots to a Git-ball incrementally.  That loses most metadata (such as the
# individual file timestamps, comments, etc), but should work even with my
# larger archives.

use strict;

use Cwd;
use File::Temp qw/ tempdir /;
use Getopt::Std;
use Time::Local;
use Time::Piece;

$| = 1;

our (
    $opt_a, $opt_b, $opt_C, $opt_c, $opt_D, $opt_d, $opt_G,
    $opt_g, $opt_L, $opt_n, $opt_p, $opt_Q, $opt_R, $opt_r,
    $opt_s, $opt_t, $opt_v, $opt_x, $opt_y
);
our $debug = 0;
our $rcs_tree;
our $cutoff;
our %executable;    # hash to denote executable files
our %signed_tag;

our @CHANGES;
our $made_CHANGES = 0;
our $need_CHANGES;
our $date_CHANGES = 100000;
our $RCS_DIR      = "RCS";

our $quiet = "-q";
our $noerr = "2>/dev/null";

sub failed($) {
    my $text = shift;
    printf STDERR "? %s\n", $text;
    exit 1;
}

sub find_rcs_files($$$) {
    my %data = %{ $_[0] };
    my $base = $_[1];
    my $path = $_[2];
    if ( -d $path ) {
        if ( opendir( my $dh, $path ) ) {
            my @list = sort readdir($dh);
            closedir $dh;
            for my $n ( 0 .. $#list ) {
                chomp $list[$n];
                next if ( $list[$n] =~ /^\.(\.)?$/ );
                %data = &find_rcs_files( \%data, $base, sprintf "%s/%s",
                    $path, $list[$n] );
            }
        }
        else {
            printf STDERR "can't opendir $path: $!\n";
        }
    }
    elsif ( -f $path and &valid_rcs_file($path) ) {
        my $file = substr $path, ( length $base );
        $file =~ s/\/$RCS_DIR\//\//;
        $file =~ s/^\///;
        $file =~ s/,v$//;
        $data{$path} = $file;
        my @keys = ( keys %data );
    }
    return %data;
}

sub count_hash($) {
    my %hash   = %{ $_[0] };
    my @keys   = ( keys %hash );
    my $result = $#keys;
    return $result;
}

# Read rlog output for each file, returning the result as a hash of arrays
sub read_rcs_logs($) {
    my %files = %{ $_[0] };
    my %result;
    for my $path ( sort keys %files ) {
        my $fh;
        my @log;
        if ( $path =~ /.,v$/ and &valid_rcs_file($path) and open $fh,
            "rlog $path|" )
        {
            @log = <$fh>;
            close $fh;
        }
        for my $n ( 0 .. $#log ) {
            chomp $log[$n];
        }
        $result{$path} = \@log;
    }
    return %result;
}

# Read rlog output for a single file, assigning dates to labels.  If there's
# already a date assigned, update the hash to use the most recent date.
sub read_rcs_file($$) {
    my %result = %{ $_[0] };
    my @log    = @{ $_[1] };

    my $date;
    my $label;
    my $version;
    my %labels;
    my %versions;
    my $state = 0;

    for my $n ( 0 .. $#log ) {
        chomp $log[$n];
        if ( $log[$n] eq "symbolic names:" ) {
            $state = 1;
        }
        elsif ( $state == 1 ) {
            if ( $log[$n] =~ /^\t/ ) {
                $label = $log[$n];
                $label =~ s/^\s+//;
                $label =~ s/:.*//;
                $version = $log[$n];
                $version =~ s/^.*:\s+//;
                $labels{$label} = $version;
                my %obj;
                if ( $versions{$version} ) {
                    %obj = %{ $versions{$version} };
                }
                $obj{$label} = "";
                my @keys = keys %obj;
                $versions{$version} = \%obj;
            }
            else {
                $state = 2;
            }
        }
        elsif ( $state == 2 ) {
            if ( $log[$n] =~ /^-------/ ) {
                $state   = 3;
                $version = "";
            }
        }
        elsif ( $state > 2 ) {
            if ( $log[$n] =~ /^-------/ ) {
                $version = "";
            }
            elsif ( $log[$n] =~ /^revision \d/ ) {
                $version = $log[$n];
                $version =~ s/^revision //;
            }
            elsif ( $log[$n] =~ /^date: \d/ ) {
                $date = $log[$n];
                $date =~ s/^date: //;
                $date =~ s/;.*//;

                # if this was a labeled version, save the date
                if ( $versions{$version} ) {
                    my %obj = %{ $versions{$version} };
                    for my $label ( sort keys %obj ) {
                        if ( $result{$label} ) {
                            if ( $result{$label} lt $date ) {
                                $result{$label} = $date;
                            }
                        }
                        else {
                            $result{$label} = $date;
                        }
                    }
                }
            }
        }
    }
    return %result;
}

# rlog doesn't give the initial number of lines added.  Get that using co.
sub initial_size($$) {
    my $archive  = shift;
    my $revision = shift;
    my $result   = 0;
    if ( open my $fh, "co $quiet -p$revision $archive|" ) {
        my @lines = <$fh>;
        close $fh;
        $result = 1 + $#lines;
    }
    return $result;
}

# The real date from my point of view is the local time.
sub actual_date($) {
    my $timestamp = shift;
    my @fields    = split /[:\/\s]+/, $timestamp;
    my $global    = timegm(
        $fields[5], $fields[4],     $fields[3],
        $fields[2], $fields[1] - 1, $fields[0]
    );
    @fields = localtime($global);
    my $local = sprintf "%04d/%02d/%02d",
      $fields[5] + 1900,
      $fields[4] + 1,
      $fields[3];
    return $local;
}

sub read_rcs_activity($$$) {
    my %buckets = %{ $_[0] };
    my $archive = $_[1];
    my @rcs_log = @{ $_[2] };
    my $state   = 0;
    my $version = "";
    my @fields;
    my $timestamp;
    my $additions;
    my $deletions;
    my %archive;

    for my $l ( 0 .. $#rcs_log ) {
        my $line = $rcs_log[$l];
        if ( $line =~ /^--------/ ) {
            $state = 1;
        }
        elsif ( $state == 0 ) {
            next;
        }
        else {
            next if ( $state++ > 2 );
        }
        if ( $line =~ /^revision\s+/ ) {
            @fields = split /\s+/, $line;
            next if ( $#fields <= 0 );
            next if ( $fields[1] =~ /^\d+\.\d+\./ );
            $version = $fields[1];
            next;
        }
        elsif ( $line !~ /^date:/ ) {
            next;
        }
        @fields    = split /;\s+/, $line;
        $timestamp = "";
        $additions = "";
        $deletions = "";
        for my $f ( 0 .. $#fields ) {
            if ( $fields[$f] =~ /^date:\s+/ ) {
                $timestamp = $fields[$f];
                $timestamp =~ s/^date:\s+//;
            }
            elsif ( $fields[$f] =~ /^lines:\s+/ ) {
                @fields    = split /\s+/, $fields[$f];
                $additions = $fields[1];
                $deletions = $fields[2];
            }
        }
        next unless ( $timestamp ne "" );

        $additions = &initial_size( $archive, $version )
          if ( $additions eq "" and $deletions eq "" and $version ne "" );
        $deletions = 0 unless ( $deletions ne "" );
        my $date = &actual_date($timestamp);

        my %obj;
        %obj = %{ $archive{$date} } if ( $archive{$date} );
        $obj{FILES} += 1;
        $obj{ADD}   += $additions;
        $obj{DEL}   -= $deletions;
        $archive{$date} = \%obj;
    }

    # rlog was in reverse-order of date.  Now work forward to obtain the size
    # of the file at different dates.
    my $size = 0;
    my %dates;
    for my $date ( sort keys %archive ) {
        $dates{$date} = 1;
        my %item = %{ $archive{$date} };
        $size += ( $item{ADD} - $item{DEL} );
        $item{SIZE} = $size;
        $archive{$date} = \%item;

        printf "this %s { %d ( %d - %d ) }\n", $date, $item{SIZE},
          $item{ADD}, $item{DEL}
          if ($debug);
    }

    for my $date ( sort keys %buckets ) {
        $dates{$date} = 1;
    }

    # Finally, merge this archive's data against the buckets, filling in the
    # size for this one in buckets where this one has no corresponding date.
    my @dates = sort keys %dates;
    my $last  = "";
    for my $d ( 0 .. $#dates ) {
        my $date = $dates[$d];
        my %bucket;

        my %item;
        if ( $archive{$date} ) {
            %item = %{ $archive{$date} };
        }
        elsif ( $last ne "" ) {
            %item = %{ $archive{$last} };
        }

        if ( $buckets{$date} ) {
            %bucket = %{ $buckets{$date} };
            if ( $last ne "" or $archive{$date} ) {
                $bucket{FILES} += 1;
                $bucket{SIZE}  += $item{SIZE};
            }
            printf "case 1 - " if ($debug);
        }
        elsif ( $d > 0 ) {
            my %temp = %{ $buckets{ $dates[ $d - 1 ] } };

            $bucket{FILES} = $temp{FILES};
            $bucket{SIZE}  = $temp{SIZE};

            if ( $last eq "" and $archive{$date} ) {
                $bucket{FILES} += 1;
                $bucket{SIZE}  += $item{SIZE};
            }
            printf "case 2 - " if ($debug);
        }
        else {
            $bucket{FILES} = 1;
            $bucket{SIZE}  = $item{SIZE};
            printf "case 3 - " if ($debug);
        }
        printf "all %s %d files, %d size %s\n", $date, $bucket{FILES},
          $bucket{SIZE}, $last
          if ($debug);
        $buckets{$date} = \%bucket;
        $last = $date if ( $archive{$date} );
    }
    return %buckets;
}

sub show_rcs_activity($) {
    my %buckets = %{ $_[0] };
    for my $date ( sort keys %buckets ) {
        my %bucket = %{ $buckets{$date} };
        printf "%s %d files, %d size\n", $date, $bucket{FILES}, $bucket{SIZE};
    }
}

# Given a list of files in the archive, read the labels along with the most
# recent date for each label.
sub find_rcs_labels($) {
    my %logs = %{ $_[0] };
    my %labels;
    for my $p ( sort keys %logs ) {
        my @log = @{ $logs{$p} };
        %labels = &read_rcs_file( \%labels, \@log );
    }
    return %labels;
}

# Invert the list of dates-by-label to obtain labels-by-date, possible aliases.
sub labels_by_date($) {
    my %labels = %{ $_[0] };
    my %releases;
    for my $label ( keys %labels ) {
        my $date = $labels{$label};
        my %data;
        %data            = %{ $releases{$date} } if ( $releases{$date} );
        $data{$label}    = $label;
        $releases{$date} = \%data;
    }
    return %releases;
}

sub first_label($) {
    my %labels = %{ $_[0] };
    my @labels = sort keys %labels;
    my $result = "";
    $result = $labels[0] if ( $#labels >= 0 );
    return $result;
}

sub show_rcs_labels($) {
    my %labels   = %{ $_[0] };
    my %releases = &labels_by_date( \%labels );
    for my $date ( sort keys %releases ) {
        my %data = %{ $releases{$date} };
        my $text = "";
        for my $label ( sort keys %data ) {
            if ( $text eq "" ) {
                $text = $label;
            }
            else {
                $text = "$text, $label";
            }
        }
        printf "%s %s\n", $date, $text;
    }
}

sub compute_cutoff($) {
    my $result = shift;
    my $given  = $result;

    # pad the cutoff to the right format
    $result =~ s/\s+/ /g;
    $result .= " 23:59" unless ( $result =~ /\d:\d/ );
    $result .= ":59"    unless ( $result =~ /\d:\d+:\d/ );
    $result =~ s/[\/-](\d)/\/0$1/g;
    $result =~ s/[:](\d)/:0$1/g;
    $result =~ s/([:\/])0(\d\d)/${1}${2}/g;

    # now... that's localtime.  But for cutoff, we actually want gmtime.
    my $time = Time::Piece->strptime( $result, "%Y/%m/%d %H:%M:%S" );
    my @time = localtime($time);
    $time -= ( timegm(@time) - timelocal(@time) );
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
      gmtime($time);
    $result = sprintf "%04d/%02d/%02d %02d:%02d:%02d", $year + 1900, $mon + 1,
      $mday, $hour, $min, $sec;

    printf "CUTOFF %s ->%s\n", $given, $result if ($opt_v);
    $cutoff = $result;
}

sub cutoff_revision($) {
    my @log       = @{ $_[0] };
    my $result    = "";
    my $state     = 0;
    my $test_rev  = "";
    my $best_rev  = "";
    my $test_date = "";
    my $best_date = "";
    for my $n ( 0 .. $#log ) {

        if ( $log[$n] =~ /^---------/ ) {
            $state = 1;
        }
        elsif ( $state == 1 and $log[$n] =~ /^revision / ) {
            $test_rev = $log[$n];
            $test_rev =~ s/\s+$//;
            $test_rev =~ s/^.*\s//;
            $state = 2;
        }
        elsif ( $state == 2 and $log[$n] =~ /^date: / ) {
            $test_date = $log[$n];
            $test_date =~ s/^date:\s+//;
            $test_date =~ s/;.*$//;
            if ( $test_date le $cutoff and $test_date gt $best_date ) {
                $best_rev  = $test_rev;
                $best_date = $test_date;
                $state     = 3;
            }
        }
    }
    return $best_rev;
}

sub show_rcs_bydate($) {
    my %logs = %{ $_[0] };
    printf "DATE '%s' -> '%s'\n", $opt_C, $cutoff if ($opt_v);
    for my $file ( sort keys %logs ) {
        my @log      = @{ $logs{$file} };
        my $revision = &cutoff_revision( \@log );
        printf "%s %s\n", $revision, $file if ( $revision ne "" );
    }
}

# For each date where there's activity, show the number of files in the
# archive, along with the number of files changed and lines changed.
sub show_maybe_date($) {
    my %logs = %{ $_[0] };
    my %buckets;
    for my $file ( sort keys %logs ) {
        my @log = @{ $logs{$file} };
        printf "FILE %s\n", $file if ($debug);
        %buckets = &read_rcs_activity( \%buckets, $file, \@log );
    }
    &show_rcs_activity( \%buckets );
}

# My RCS archives are either compressed, or have permit-files "RCS,v"
sub valid_rcs_archive($) {
    my $dir     = shift;
    my $project = $dir;
    my $result  = 0;
    $project =~ s/^.*\///;
    $result = 1 if ( -f "$dir/$project.tbz" );
    $result = 1 if ( -f "$dir/$RCS_DIR/$RCS_DIR,v" );
    $result = 1 if ( -f "$dir/$RCS_DIR/README,v" );
    $project =~ s/\.vcs$//;
    $result = 1 if ( -f "$dir/$project/$RCS_DIR/README,v" );
    return $result;
}

sub open_rcs_archive($) {
    my $dir     = shift;
    my $project = $dir;
    $project =~ s/^.*\///;
    if ( -f "$dir/$project.tbz" ) {
        my $expanded = tempdir( CLEANUP => 1 );
        system("tar xf $dir/$project.tbz -C $expanded ");
        $dir = $expanded;
    }
    return $dir;
}

sub valid_rcs_file($) {
    my $path   = shift;
    my $result = 1;
    $result = 0 if ( -l $path );
    $result = 0 unless ( -f $path );
    $result = 0 unless ( $path =~ /[^\/],v$/ );
    $result = 0 if ( $path =~ /\/$RCS_DIR,v$/ );
    return $result;
}

# For a given project name, find the RCS tree.
sub find_rcs_tree($) {
    my $project = shift;
    my $result  = "";
    printf "** find tree %s\n", $project if ($opt_s);
    my $check;
    if ( &valid_rcs_archive( $check = "/users/source/archives/$project.vcs" ) )
    {
        $result = &open_rcs_archive($check);
    }
    elsif ( &valid_rcs_archive( $check = "/usr/build/VCS/$project" ) ) {
        $result = &open_rcs_archive($check);
    }
    die "no RCS archive found for $project\n" if ( $result eq "" );
    printf "** rcs(%s) -> %s\n", $project, $result if ($opt_s);
    return $result;
}

# Determine the project name from the current working directory.
sub current_project() {
    my $result = "";
    my $dir    = getcwd;
    if ( $dir =~ /^\/usr\/build\/.*/ ) {
        $result = $dir;
        $result =~ s,^/usr/build/,,;
        $result =~ s,/.*,,;
    }
    die "no project name given" if ( $result eq "" );
    return $result;
}

sub parent_dir($) {
    my $path = shift;
    $path =~ s,/[^/]*$,,;
    return $path;
}

sub make_dir($) {
    my $path = shift;
    if ( $path ne "" and !-d $path ) {
        &make_dir( &parent_dir($path) ) if ( $path =~ /\// );
        mkdir $path;
    }
}

sub remove_all($$) {
    my $path = shift;
    my $all  = shift;
    if ( opendir( my $dh, $path ) ) {
        my @list = sort readdir($dh);
        closedir $dh;
        for my $n ( 0 .. $#list ) {
            next if ( $list[$n] =~ /^\.(\.)?$/ );
            next if ( $list[$n] =~ /^\.git$/ );
            my $full = sprintf "%s/%s", $path, $list[$n];
            if ( -l $full ) {

                # ignore
            }
            elsif ( -d $full ) {
                &remove_all( $full, $all );
                rmdir $full;
            }
            elsif ($all) {
                unlink $full;
            }
        }
    }
}

# Ensure that the directory exists and is empty, use it as the current
# directory.
sub clean_tree($) {
    my $newdir = shift;
    &make_dir($newdir);
    chdir $newdir;
    &remove_all( ".", 1 );
}

# Make a working directory containing checked-out files from the RCS archive.
sub make_workdir($$) {
    my $label  = shift;
    my %files  = %{ $_[0] };
    my $olddir = getcwd;
    &clean_tree($opt_d);
    for my $archive ( sort keys %files ) {
        my $working = $files{$archive};
        &make_dir( &parent_dir($working) ) if ( $working =~ /\// );
        system( sprintf "co $quiet -M -r%s \"%s\" \"%s\" $noerr",
            $label, $archive, $working );
        my $destdir = $working;
        $destdir =~ s/\/[^\/]*$//;
        $destdir = "." if ( $destdir eq "" );
        if (    $RCS_DIR ne "RCS"
            and !-f $working
            and $working !~ /\b${RCS_DIR}$/
            and $working !~ /\bRCS$/ )
        {

            # co won't honor pathnames on both parameters.
            # This will work for "rcs-blame", which has no ",v" files
            # in the top-level directory.
            my $actual = $working;
            $actual =~ s,^.*/,,;
            rename $actual, $working if ( -f $actual );
        }
        $executable{$working} = 1 if ( -x $working );
    }
    &remove_all( ".", 0 );
    chdir $olddir;
}

# Make an RCS tree of symbolic links to the original archives for generating
# the CHANGES or MANIFEST files.  Do it this way to allow merging archives as
# is done for ncurses.
sub make_rcs_tree($$$) {
    my $project   = $_[0];
    my $release   = $_[1];
    my %rcs_files = %{ $_[2] };

    my $olddir = getcwd;
    if ($rcs_tree) {
        chdir $rcs_tree;
        &clean_tree(".");
    }
    else {
        printf "...creating RCS tree for $project\n" if ($opt_v);
        $rcs_tree = $opt_t ? $opt_t : tempdir( CLEANUP => 1 );
        $rcs_tree .= "/$project";
        mkdir $rcs_tree;
        chdir $rcs_tree;
        for my $archive ( sort keys %rcs_files ) {
            my $working = $rcs_files{$archive};
            my $linkage = $working . ",v";
            if ( $linkage =~ /\// ) {
                $linkage =~ s/(\/[^\/]+)$/\/$RCS_DIR$1/;
            }
            else {
                $linkage = "$RCS_DIR/$linkage";
            }
            &make_dir( &parent_dir($linkage) ) if ( $linkage =~ /\// );
            symlink $archive, $linkage;
        }
    }

    chdir $olddir;
}

sub head_label($) {
    my %labels   = %{ $_[0] };
    my $result   = "";
    my %releases = &labels_by_date( \%labels );
    my @releases = sort keys(%releases);
    if ( $#releases >= 0 ) {
        my %head = %{ $releases{ $releases[$#releases] } };
        my @keys = sort keys(%head);
        if ( $#keys >= 0 ) {
            $result = $keys[0];
        }
    }
    return $result;
}

sub ChangeLog_name($$) {
    my $project   = $_[0];
    my %rcs_files = %{ $_[1] };
    my $result    = "";

    if ( defined $need_CHANGES ) {
        $result = $need_CHANGES;
    }
    elsif ( $project eq "dbmalloc" ) {
        $result = "ChangeLog";

        # special case
    }
    else {
        for my $file ( sort keys %rcs_files ) {
            $result = "CHANGES";
            if (   $file =~ /\/CHANGES,v$/
                or $file =~ /\/CHANGES.txt,v$/
                or $file =~ /\/ChangeLog,v$/
                or $file =~ /\/NEWS,v$/
                or $file =~ /\/$project.log.html,v$/ )
            {
                $result = "";
                last;
            }
        }
    }
    return $result;
}

sub need_CHANGES($$) {
    my $project = $_[0];
    my $check   = &ChangeLog_name( $_[0], $_[1] );
    my $result  = ( $check ne "" ) ? 1 : 0;

    return $result;
}

sub mtime_of($) {
    my $filename = shift;
    my (
        $dev,  $ino,   $mode,  $nlink, $uid,     $gid, $rdev,
        $size, $atime, $mtime, $ctime, $blksize, $blocks
    ) = stat($filename);
    return $mtime;
}

sub read_CHANGES($$) {
    my $project   = shift;
    my $rcs_files = shift;
    my $result    = "";
    $result = "";
    if (@CHANGES) {
        $result = &ChangeLog_name( $project, $rcs_files );
    }
    else {
        my $olddir = getcwd;
        if ( chdir $rcs_tree ) {
            $result = &ChangeLog_name( $project, $rcs_files );
            my $command = "touch $result; run-rcs2log ";
            $command .= "-v " if ($opt_v);
            $command .= $result;
            $command .= ">/dev/null" unless ($opt_v);
            system($command);
            if ( open my $fh, $result ) {
                my $mark = 0;
                @CHANGES = <$fh>;
                close $fh;
                $date_CHANGES = &mtime_of($result);
            }
        }
        chdir $olddir;
    }
    return $result;
}

sub make_CHANGES($$$$) {
    my $project   = $_[0];
    my $release   = $_[1];
    my %rcs_files = %{ $_[2] };
    my $date      = $_[3];
    my $when      = $date;

    # We get GMT dates from RCS history, but the dates from rcs2log are in
    # local-time.  Convert...
    if ( $when =~ /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/ ) {
        my $t = Time::Piece->strptime( $when, "%Y/%m/%d %H:%M:%S" );
        my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
          localtime($t);
        my $g = timegm( $sec, $min, $hour, $mday, $mon, $year );
        ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
          localtime($g);
        $when = sprintf "%04d-%02d-%02d", $year + 1900, $mon + 1, $mday;
    }
    else {
        # just look for yyyy-mm-dd
        $when =~ s/\//-/g;
        $when =~ s/\s.*//;
    }

    if ( &need_CHANGES( $project, \%rcs_files ) ) {
        &make_rcs_tree( $project, $release, \%rcs_files );
        my $result = &read_CHANGES( $project, \%rcs_files );
        if ( $result ne "" ) {
            printf "...making $result file ($date ->$when)\n" if ($opt_v);
            my $mark = -1;
            for my $n ( 0 .. $#CHANGES ) {
                my $test = $CHANGES[$n];
                next unless ( $test =~ /^\d{4}-\d{2}-\d{2}\s+/ );
                if ( $test le $when or $test =~ /^$when\s.*/ ) {
                    $mark = $n;
                    last;
                }
            }
            if ( $mark < 0 ) {
                &failed("FULL CHANGES ($when)!");
            }
            printf "...lines %d..%d\n", $mark, $#CHANGES if ($opt_v);
            my $output = $result;
            my $input  = $result;
            $input  = "$rcs_tree/$result" unless ( $result =~ /\// );
            $output = "$opt_d/$result"    unless ( $result =~ /^\// );
            unlink $output;
            if ( open my $fh, ">$output" ) {

                for my $n ( $mark .. $#CHANGES ) {
                    printf $fh "%s", $CHANGES[$n];
                }
                close $fh;
                chmod 0444, $output;
                utime $date_CHANGES, $date_CHANGES, $output;
                $made_CHANGES = 1;
            }
            else {
                &failed("cannot create $output");
            }
        }
    }
}

sub need_MANIFEST($) {
    my $project = shift;
    my $result  = 0;

    $result = 1
      if (
        $project =~ /^(add
			|atac
			|bcpp
			|byacc
			|c_count
			|cm_tools
			|conflict
			|copyrite
			|cproto
			|dbmalloc
			|ded
			|mawk
			|my-autoconf
			|sccs_tools
			|td_lib
			|vile
			|vttest
			|xterm)$/x
      );
    return $result;
}

sub make_MANIFEST($$$) {
    my $project   = $_[0];
    my $release   = $_[1];
    my %rcs_files = %{ $_[2] };

    if ( &need_MANIFEST($project) ) {
        &make_rcs_tree( $project, $release, \%rcs_files );
        my $olddir = getcwd;
        if ( chdir $rcs_tree ) {
            system("touch CHANGES") if $made_CHANGES;
            my $result = "MANIFEST";
            system("rcsget -d $quiet -r$release $noerr");
            system("manifest $opt_p -d -r$release");
            if ( -f "$result" ) {
                system(
                    "cp -f " . ( $opt_v ? "-v" : "" ) . " $result $opt_d/" );
                chmod 0444, "$opt_d/$result";
            }
            chdir $olddir;
        }
    }
}

sub do_it($) {
    my $command = shift;
    printf "%% %s\n", $command if ( $opt_n or $opt_v );
    system($command) unless ($opt_n);
}

sub fix_permissions() {
    if ( open my $fh, "git ls-tree -r HEAD |" ) {
        my @data = <$fh>;
        chomp @data;
        close $fh;
        for my $n ( 0 .. $#data ) {
            my @fields = split /\s+/, $data[$n];
            if ( $#fields == 3 ) {

                # fields[0] == permission
                # fields[1] == blob
                # fields[2] == hash
                # fields[3] == filename
                my $working = $fields[3];
                if ( $fields[0] eq "100755" ) {
                    &do_it("TZ=0 git update-index --chmod=-x $working")
                      unless $executable{$working};
                }
                elsif ( $fields[0] eq "100644" ) {
                    &do_it("TZ=0 git update-index --chmod=+x $working")
                      if $executable{$working};
                }
            }
        }
    }
}

sub add_to_git($$$) {
    my $what   = shift;
    my $date   = shift;
    my %labels = %{ $_[0] };
    my $label  = &first_label( \%labels );
    my $olddir = getcwd;
    chdir $opt_d;
    if ( defined $signed_tag{$label} ) {
        printf STDERR "?? $label (ignore duplicate)\n";
        return;
    }
    printf ".. adding %s %s\n", $date, $label if ($opt_v);
    &do_it("git init") unless ( -d ".git" );
    &do_it("TZ=0 git add .");
    &fix_permissions;
    &do_it( "TZ=0 git commit -a -S "
          . "--date=\"$date\" "
          . "-m \"snapshot of project \\\"$what\\\", label $label\"" );

    for my $n ( sort keys %labels ) {
        next if ( defined $signed_tag{$n} );
        &do_it( "TZ=0 git tag -s "
              . "-m \"snapshot of project \\\"$what\\\", label $label\" "
              . "$n" );
    }
    chdir $olddir;
}

sub find_git_labels() {
    my $olddir = getcwd;
    my %result;
    if ( chdir $opt_d ) {
        my $fh;
        if (
            # signed tags do not have an authordate,
            # and commits do not have a taggerdate
            open $fh,
              "git for-each-ref "
            . "--format=\"%(authordate:iso)\t%(taggerdate:iso)\t%(refname:short)\" "
            . "refs/tags "
            . "$noerr |"
          )
        {
            my @data = <$fh>;
            close $fh;
            for my $n ( 0 .. $#data ) {
                chomp $data[$n];
                $data[$n] =~ s/^\s+//;
                my @fields = split /\s+/, $data[$n];
                if ( $#fields < 3 ) {
                    if ($debug) {
                        printf STDERR "unexpected fields:\n";
                        for my $n2 ( $n - 3 .. $n ) {
                            printf STDERR "%s\t%s\n",
                              ( $n2 == $n ) ? "-->" : "", $data[$n2]
                              if ( $n2 >= 0 );
                        }
                    }
                    $signed_tag{ $fields[1] } = 1 if ( $#fields == 1 );
                    next;
                }
                $fields[0] =~ s/-/\//g;
                $result{ $fields[3] } = $fields[0] . " " . $fields[1];
            }
        }
        chdir $olddir;
    }
    return %result;
}

sub show_git_labels($) {
    my %labels   = %{ $_[0] };
    my %releases = &labels_by_date( \%labels );
    for my $date ( sort keys %releases ) {
        my %data = %{ $releases{$date} };
        my $text = "";
        for my $label ( sort keys %data ) {
            if ( $text eq "" ) {
                $text = $label;
            }
            else {
                $text = "$text, $label";
            }
        }
        printf "%s %s\n", $date, $text;
    }
}

sub bundle_name($) {
    my $project = shift;
    return "$opt_b/$project-bundle.git";
}

sub bundle_start($) {
    my $project = shift;
    my $bundle  = &bundle_name($project);
    my $olddir  = getcwd;
    if ( -f $bundle and chdir $opt_d ) {
        printf ".. cloning $bundle\n" if ($opt_v);
        system("git clone $bundle $opt_d");
        chdir $olddir;
    }
}

sub bundle_finish($) {
    my $project = shift;
    my $bundle  = &bundle_name($project);
    my $olddir  = getcwd;
    if ( chdir $opt_d ) {
        &make_dir($opt_b);
        printf ".. bundling $bundle\n" if ($opt_v);
        system("git bundle create $bundle --all");
        chdir $olddir;
    }
}

# Given a project name, perform the selected operations.  If no options are
# given, just summarize the source/target.
sub release2git($) {
    my $project = shift;
    $project = &current_project if ( $project eq "." );
    printf "** release2git(%s)\n", $project if ($opt_s);

    my %rcs_trees;
    $rcs_trees{$project} = &find_rcs_tree($project);
    if ( $project eq "ncurses" ) {
        $rcs_trees{"Ada95"}    = &find_rcs_tree("Ada95");
        $rcs_trees{"terminfo"} = &find_rcs_tree("terminfo");
    }

    my %rcs_files;
    %rcs_files =
      &find_rcs_files( \%rcs_files, $rcs_trees{$project},
        $rcs_trees{$project} );
    if ( $project eq "ncurses" ) {
        %rcs_files =
          &find_rcs_files( \%rcs_files, $rcs_trees{"Ada95"},
            $rcs_trees{"Ada95"} );
        my %terminfo;
        %terminfo =
          &find_rcs_files( \%terminfo, $rcs_trees{"terminfo"},
            $rcs_trees{"terminfo"} );
        my $fixup = "";
        for my $key ( sort keys %terminfo ) {
            $fixup = $key if ( $terminfo{$key} eq "terminfo.src" );
        }
        $rcs_files{$fixup} = "misc/terminfo.src";
    }
    if ($opt_L) {
        for my $key ( sort keys %rcs_files ) {
            printf "%s ->%s\n", $rcs_files{$key}, $key;
        }
    }

    my %logs;
    my %rcs_labels;
    if ( $opt_C or $opt_D or $opt_R or $opt_a or $opt_r or $opt_x or $opt_y ) {
        %logs       = &read_rcs_logs( \%rcs_files );
        %rcs_labels = &find_rcs_labels( \%logs );
    }

    &bundle_start($project) if ($opt_b);

    # find the existing Git-labels to allow adding new labels from RCS
    my %git_labels;
    %git_labels = &find_git_labels if ( $opt_G or $opt_g );

    &show_rcs_bydate( \%logs )       if ($opt_C);
    &show_maybe_date( \%logs )       if ($opt_D);
    &show_rcs_labels( \%rcs_labels ) if ($opt_R);
    &show_git_labels( \%git_labels ) if ($opt_G);
    if ($opt_a) {
        my %releases = &labels_by_date( \%rcs_labels );
        for my $date ( sort keys %releases ) {
            my $release = &first_label( $releases{$date} );
            last if ( $opt_c      and ( $date gt $cutoff ) );
            next if ( %git_labels and $git_labels{$release} );
            &make_workdir( $release, \%rcs_files );
            &make_CHANGES( $project, $release, \%rcs_files, $date );
            &make_MANIFEST( $project, $release, \%rcs_files );
            &add_to_git( $project, $date, $releases{$date} ) if ($opt_g);
        }
    }
    elsif ($opt_r) {
        my %hash;
        $hash{$opt_r} = $opt_r;
        &make_workdir( $opt_r, \%rcs_files );
        &make_CHANGES( $project, $opt_r, \%rcs_files, $rcs_labels{$opt_r} );
        &make_MANIFEST( $project, $opt_r, \%rcs_files );
        &add_to_git( $project, $rcs_labels{$opt_r}, \%hash ) if ($opt_g);
    }
    elsif ($opt_x) {
        my $head = &head_label( \%rcs_labels );
        if ( $head ne "" ) {
            printf ".. HEAD %s ->%s\n", $head, $rcs_labels{$head}
              if ($opt_v);
            &make_workdir( $head, \%rcs_files );
            &make_CHANGES( $project, $head, \%rcs_files, $rcs_labels{$head} );
            &make_MANIFEST( $project, $head, \%rcs_files );
        }
    }
    if ($opt_y) {
        my $head = &head_label( \%rcs_labels );
        $need_CHANGES = $opt_y;
        &make_CHANGES( $project, $head, \%rcs_files, $rcs_labels{$head} );
        undef $need_CHANGES;
    }
    &bundle_finish($project) if ($opt_b);
}

sub main::HELP_MESSAGE() {
    printf STDERR <<EOF
Usage: $0 [options]

Options:

  -a         export all labeled revisions from trunk to Git
  -b dir     get/put Git-balls from this directory
  -C date    list revisions from RCS given cutoff-date
  -c date    limit exports in -a to this cutoff-date
  -D         list potential dates for labels
  -d dir     use this staging directory (otherwise temporary)
  -L         list archive-files in RCS, vs working-files
  -G         list revisions from Git
  -g         add working directory to Git
  -n         no-op (do not update Git)
  -p         portable (omit revision information from MANIFEST)
  -Q         unquiet/debug
  -R         list revisions from RCS
  -r label   retrieve this label from RCS, add to Git
  -s         show summary of archive location
  -t dir     use this directory for RCS-tree (otherwise temporary)
  -v         verbose (shows files created)
  -x         retrieve head revision from RCS (use with -d)
  -y file    extract rcs2log information to this file
EOF
      ;
    exit;
}

# special case needed for "rcs-blame"
$RCS_DIR = $ENV{RCS_DIR} if ( $ENV{RCS_DIR} );

&getopts('ab:C:Dc:d:GgLnpQRr:st:vxy:') || main::HELP_MESSAGE;
&main::HELP_MESSAGE if ( $opt_c and not $opt_a );
&main::HELP_MESSAGE if ( $opt_b and not( $opt_g or $opt_G ) );
&main::HELP_MESSAGE if ( $opt_x and not($opt_d) );
&main::HELP_MESSAGE if ( $opt_x and ( $opt_a or $opt_r ) );
$opt_p = "-p" if ($opt_p);
$opt_p = "" unless ($opt_p);
$opt_s = 1 if ($opt_v);

if ($opt_Q) {
    $quiet = "";
    $noerr = "";
}

&compute_cutoff($opt_C) if ($opt_C);
&compute_cutoff($opt_c) if ($opt_c);

$opt_d = tempdir( CLEANUP => 1 ) unless ($opt_d);

if ( $#ARGV >= 0 ) {
    while ( $#ARGV >= 0 ) {
        &release2git( shift @ARGV );
    }
}
else {
    &release2git(&current_project);
}

1;
