#!/usr/bin/perl

# This is the recursive version of sccs2git
# Combining all SCCS repos to one single git repo

use strict;
use warnings;

use VCS::SCCS;
use File::Find;

our $VERSION = "0.26 - 20200423";
our $CMD = $0; $CMD =~ s{.*/}{};

sub usage {
    my $err = shift and select STDERR;
    print "usage: $CMD [--no-recurse]\n\n";
    print "    --no-recurse  Only convert the current folder to git\n\n";
    print "This script will convert - recursively by default - a tree\n";
    print "of SCCS repositories to git starting in the current folder\n";
    print "The script expects a working git command in your \$PATH\n";
    exit $err;
    } # usage

use CSV;
use Getopt::Long qw(:config bundling);
GetOptions (
    "help|?"		=> sub { usage (0); },
    "V|version"		=> sub { print "$CMD [$VERSION]\n"; exit 0; },

    "r|recursive!"	=> \(my $recurse = 1),

    "v|verbose:1"	=> \(my $opt_v   = 0),
    ) or usage (1);

my %sccs_ext;
my @sccs;
if ($recurse) {
    find (sub { $File::Find::dir =~ m{(?:^|/)SCCS$} and m/^s\./ or return;
		push @sccs, $File::Find::name;
		m/\.(\w+)$/ and $sccs_ext{$1}++;
	      }, glob "*");
    }
else {
    -d "SCCS" and push @sccs => "SCCS";
    }

@sccs or die "No SCCS source files to convert\n";

-d ".git" and die ".git already exists\n";
system "git init";
# http://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
# could be explored to write checkout hooks that translate SCCS
# keywords to actual content. Would be hard to translate back

sub pr_date {
    my @dt = localtime shift;
    sprintf "%s %02d-%02d-%4d %02d:%02d:%02d",
	(qw( Sun Mon Tue Wed Thu Fri Sat ))[$dt[6]],
	$dt[3], $dt[4] + 1, $dt[5] + 1900,
	$dt[2], $dt[1], $dt[0];
    } # pr_date

# fire up GFI
chomp (my $branchname = qx(git symbolic-ref HEAD));
open my $gfi, "|-", qw(git fast-import --quiet);
my $mark;

# use a best guess at user, hostname, etc.  FIXME: add authors map :)
use Time::Local;
use Net::Domain;
my $domain = Net::Domain::hostdomain () || "procura.nl";
my %tzoffset;
my $tzoffset = sub {
    use integer;
    my $offset_s = timegm (localtime ($_[0])) - $_[0];
    my $off = abs $offset_s / 60;
    my ($off_h, $off_m) = ($off / 60, $off % 60);
    $tzoffset{$offset_s} ||= ( $offset_s >= 0 ? "+" : "-" )
	. sprintf "%02d%02d", $off_h, $off_m;
    };

# Submit in the same sequence as the original
my %sccs;
my %file;
my @fail;
foreach my $f (sort @sccs) {
    my $sccs;
    eval { $sccs = VCS::SCCS->new ($f) };
    unless ($sccs) {
	warn "Cannot convert $f\n";
	push @fail, $f;
	next;
	}
    # GIT supports get-hooks, to translate on retrieval
    # But it will be useless as you cannot translate back
    $sccs->set_translate ("SCCS");
    my $fn = $sccs->file ();
    $file{$fn}++;
    foreach my $rm (@{$sccs->revision_map ()}) {
	my ($rev, $vsn) = @{$rm};
	my $delta = $sccs->delta ($rev);
	$sccs{pack "NA*", $delta->{stamp}, $fn} = [ $sccs, $rev, ++$mark ];
	my $data = scalar $sccs->body ($rev);
	print { $gfi } "blob\nmark :", $mark,
			   "\ndata ", length ($data),
			   "\n", $data, "\n";
	printf STDERR "%-20s %3d %8s  %s\r", $fn, $rev, $vsn,
	    pr_date ($delta->{stamp});
	}
    print STDERR "\n";
    }

foreach my $c (sort keys %sccs) {
    my ($sccs, $rev, $mark) = @{$sccs{$c}};

    my $fn    = $sccs->file ();
    my %delta = %{$sccs->delta ($rev)};
    my $stamp = pr_date ($delta{stamp});
    my $vsn   = $delta{version};

    printf STDERR "%-20s %3d %6s  %s %s %s\n", $fn, $rev, $vsn,
	$stamp, $delta{date}, $delta{"time"};

    # Advance earlier epoch timestamp to git epoch timestamp.
    # git considers anything with timestamp earlier
    # than 100000000 since epoch as invalid timestamp.
    $delta{stamp} < 100_000_000 and $delta{stamp}++;

    print { $gfi } "commit ", $branchname, "\n";
    print { $gfi } "committer ", $delta{committer},
	" <", $delta{committer}, "@", $domain, "> ",
	$delta{stamp}, " ", $tzoffset->($delta{stamp}), "\n";

    # tradition is to save all potentially useful but
    # uncategorized metadata as RFC822-style headers in the commit
    # message
    my $mr  = $delta{mr} || ""; $mr =~ s/^-$//;
    $mr  and $mr  = "SCCS-mr: $mr";
    $vsn and $vsn = "SCCS-vsn: $vsn";
    my $cmnt = $delta{comment} || "";
    $cmnt ||= "(no message)";
    $cmnt  .= "\n";
    my $msg  = join "\n", $cmnt, grep m/\S/, $mr, $vsn;

    print { $gfi } "data ", length ($msg), "\n$msg\n";

    my $mode = $delta{flags}{x} ? "755" : "644";
    print { $gfi } "M $mode :$mark $fn\n";
    print { $gfi } "\n";
    }

print { $gfi } "checkpoint\n";

close $gfi;

system "git", "checkout";
system "git", "reset", "--hard";

open my $gi, ">", ".gitignore";
print $gi <<EOG;
SCCS
core
old
save
*.log
*.out
*.sv
*.t[gb]z
*.zip
EOG

exists $sccs_ext{as} and print $gi <<EOG;
*.[af]o
*.[af]q
*.al
*.bk
*.fa
*.hz
EOG

exists $sccs_ext{c}  and print $gi <<EOG;
*.[oa]
a.out
EOG

exists $sccs_ext{ic} and print $gi <<EOG;
*.ec
*.u
udir
EOG

close $gi;

system "git", "add", ".gitignore";
system "git", "commit", "-m", "Add default ignore list";

@fail and print STDERR join "\n    ",
    "The following files could not be converted and were skipped:", @fail, "\r";
