use Log::Reproducible;
####
sample.pl -a 1 -b 2 -c 3 OTHER ARGUMENTS
#WHEN: YYYYMMDD.HHMMSS
#WORKDIR: /path/to/working/dir
#SCRIPTDIR: bin (/path/to/working/dir/bin)
####
perl sample.pl --reproduce rlog-sample.pl-YYYYMMDD.HHMMSS
####
perl sample.pl --repronote 'This is a note'
####
perl sample.pl --repronote "This is a multi-line note:
The moon had
a cat's mustache
For a second
— from Book of Haikus by Jack Kerouac"
####
Reproducing archive: /path/to/repro-archive/rlog-sample.pl-20140321.144307
Created new archive: /path/to/repro-archive/rlog-sample.pl-20140321.144335
####
export REPRO_DIR=/path/to/archive
####
use Log::Reproducible '/path/to/archive';
####
perl sample.pl --reprodir /path/to/archive
####
#GITCOMMIT: develop f483a06 Awesome commit message
#GITSTATUS: M staged-modified-file
#GITSTATUS: M unstaged-modified-file
#GITSTATUS: A newly-added-file
#GITSTATUS: ?? untracked-file
#GITDIFFSTAGED: diff --git a/staged-modified-file b/staged-modified-file
#GITDIFFSTAGED: index ce2f709..a04c0f6 100644
#GITDIFFSTAGED: --- a/staged-modified-file
#GITDIFFSTAGED: +++ b/staged-modified-file
#GITDIFFSTAGED: @@ -1,3 +1,3 @@
#GITDIFFSTAGED: An unmodified line
#GITDIFFSTAGED: -A deleted line
#GITDIFFSTAGED: +An added line
#GITDIFFSTAGED: Another unmodified line
#GITDIFF: diff --git a/unstaged-modified-file b/unstaged-modified-file
#GITDIFF: index ce2f709..a04c0f6 100644
#GITDIFF: --- a/unstaged-modified-file
#GITDIFF: +++ b/unstaged-modified-file
#GITDIFF: @@ -1,3 +1,3 @@
#GITDIFF: An unmodified line
#GITDIFF: -A deleted line
#GITDIFF: +An added line
#GITDIFF: Another unmodified line
####
package Log::Reproducible;
use strict;
use warnings;
use autodie;
use feature 'say';
use Cwd;
use File::Path 'make_path';
use File::Basename;
use POSIX qw(strftime);
# TODO: Add verbose (or silent) option
# TODO: Standalone script that can be used upstream of any command line functions
# TODO: Allow customizion of --repronote/--reprodir/--reproduce upon import (to avoid conflicts or just shorten)
sub import {
my ( $pkg, $dir ) = @_;
reproduce($dir);
}
sub _first_index (&@) { # From v0.33 of the wonderful List::MoreUtils
my $f = shift; # https://metacpan.org/pod/List::MoreUtils
foreach my $i ( 0 .. $#_ ) {
local *_ = \$_[$i];
return $i if $f->();
}
return -1;
}
sub reproduce {
my $dir = shift;
$dir = _set_dir($dir);
make_path $dir;
my ( $prog, $prog_dir, $cmd, $note ) = _parse_command();
my ( $repro_file, $now ) = _set_repro_file( $dir, $prog );
if ( $cmd =~ /\s-?-reproduce\s+(\S+)/ ) {
my $old_repro_file = $1;
$cmd = _reproduce_cmd( $prog, $old_repro_file, $repro_file );
}
_archive_cmd( $cmd, $repro_file, $note, $prog_dir, $now );
}
sub _set_dir {
my $dir = shift;
my $cli_dir = _get_repro_arg("reprodir");
if ( defined $cli_dir ) {
$dir = $cli_dir;
}
elsif ( !defined $dir ) {
if ( defined $ENV{REPRO_DIR} ) {
$dir = $ENV{REPRO_DIR};
}
else {
my $cwd = getcwd;
$dir = "$cwd/repro-archive";
}
}
return $dir;
}
sub _parse_command {
my $note = _get_repro_arg("repronote");
for (@ARGV) {
$_ = "'$_'" if /\s/;
}
my ( $prog, $prog_dir ) = fileparse $0;
my $cmd = join " ", $prog, @ARGV;
return $prog, $prog_dir, $cmd, $note;
}
sub _get_repro_arg {
my $repro_arg = shift;
my $arg;
my $arg_idx = _first_index { $_ =~ /^-?-$repro_arg$/ } @ARGV;
if ( $arg_idx > -1 ) {
$arg = $ARGV[ $arg_idx + 1 ];
splice @ARGV, $arg_idx, 2;
}
return $arg;
}
sub _set_repro_file {
my ( $dir, $prog ) = @_;
my $now = strftime "%Y%m%d.%H%M%S", localtime;
my $repro_file = "$dir/rlog-$prog-$now";
return $repro_file, $now;
}
sub _reproduce_cmd {
my ( $prog, $old_repro_file, $repro_file ) = @_;
die "Reproducible archive file ($old_repro_file) does not exists.\n"
unless -e $old_repro_file;
open my $old_repro_fh, "<", $old_repro_file;
my $cmd = <$old_repro_fh>;
close $old_repro_fh;
chomp $cmd;
my ( $old_prog, @args ) = $cmd =~ /((?:\'[^']+\')|(?:\"[^"]+\")|(?:\S+))/g;
@ARGV = @args;
say STDERR "Reproducing archive: $old_repro_file";
_validate_prog_name( $old_prog, $prog, @args );
return $cmd;
}
sub _archive_cmd {
my ( $cmd, $repro_file, $note, $prog_dir, $now ) = @_;
my ( $gitcommit, $gitstatus, $gitdiff_cached, $gitdiff )
= _git_info($prog_dir);
my $cwd = cwd;
my $full_prog_dir = $prog_dir eq "./" ? $cwd : "$cwd/$prog_dir";
$full_prog_dir = "$prog_dir ($full_prog_dir)";
open my $repro_fh, ">", $repro_file;
say $repro_fh $cmd;
_add_archive_comment( "NOTE", $note, $repro_fh );
_add_archive_comment( "WHEN", $now, $repro_fh );
_add_archive_comment( "WORKDIR", $cwd, $repro_fh );
_add_archive_comment( "SCRIPTDIR", $full_prog_dir, $repro_fh );
_add_archive_comment( "GITCOMMIT", $gitcommit, $repro_fh );
_add_archive_comment( "GITSTATUS", $gitstatus, $repro_fh );
_add_archive_comment( "GITDIFFSTAGED", $gitdiff_cached, $repro_fh );
_add_archive_comment( "GITDIFF", $gitdiff, $repro_fh );
close $repro_fh;
say STDERR "Created new archive: $repro_file";
}
sub _git_info {
my $prog_dir = shift;
return if `which git` eq '';
my $gitbranch = `cd $prog_dir; git rev-parse --abbrev-ref HEAD 2>&1;`;
return if $gitbranch =~ /fatal: Not a git repository/;
chomp $gitbranch;
my $gitlog = `cd $prog_dir; git log -n1 --oneline;`;
my $gitcommit = "$gitbranch $gitlog";
my $gitstatus = `cd $prog_dir; git status --short;`;
my $gitdiff_cached = `cd $prog_dir; git diff --cached;`;
my $gitdiff = `cd $prog_dir; git diff;`;
return $gitcommit, $gitstatus, $gitdiff_cached, $gitdiff;
}
sub _add_archive_comment {
my ( $title, $comment, $repro_fh ) = @_;
if ( defined $comment ) {
my @comment_lines = split /\n/, $comment;
say $repro_fh "#$title: $_" for @comment_lines;
}
}
sub _validate_prog_name {
my ( $old_prog, $prog, @args ) = @_;
die <