#!/usr/bin/perl
use Cwd qw(cwd realpath);
use Getopt::Long;
use Pod::Usage;
use Code::TidyAll;
use Code::TidyAll::Util qw(can_load dirname);
use Hash::MoreUtils qw(slice_def);
use strict;
use warnings;

sub usage {
    my $msg = shift;
    print "$msg\n" if $msg;
    require Pod::Usage;
    Pod::Usage::pod2usage( { verbose => 1 } );
}

my ( %params, $all_files, $conf_file, $git_files, $help, $inc_dirs, $svn_files );

GetOptions(
    'backup-ttl=i'    => \$params{backup_ttl},
    'check-only'      => \$params{check_only},
    'conf-file=s'     => \$conf_file,
    'data-dir=s'      => \$params{data_dir},
    'no-backups'      => \$params{no_backups},
    'no-cache'        => \$params{no_cache},
    'output-suffix=s' => \$params{output_suffix},
    'refresh-cache'   => \$params{refresh_cache},
    'root-dir=s'      => \$params{root_dir},
    'tidyall-class=s' => \$params{tidyall_class},
    'a|all'           => \$all_files,
    'g|git'           => \$git_files,
    'h|help'          => \$help,
    'm|mode=s'        => \$params{mode},
    's|svn'           => \$svn_files,
    'q|quiet'         => \$params{quiet},
    'v|verbose'       => \$params{verbose},
    'I'               => \$inc_dirs,
) or usage();

Pod::Usage::pod2usage( { verbose => 2 } ) if $help;

unshift( @INC, split( /\s*,\s*/, $inc_dirs ) ) if defined($inc_dirs);

%params = slice_def( \%params );

$conf_file = "$params{root_dir}/tidyall.ini" if $params{root_dir};

my $tidyall_class = $params{tidyall_class} || 'Code::TidyAll';

my (@results);
if ( $all_files || $svn_files || $git_files ) {
    my $conf_file = $tidyall_class->find_conf_file( cwd() );
    my $ct = $tidyall_class->new_from_conf_file( $conf_file, %params );
    my @files;
    if ($all_files) {
        @files = $ct->find_matched_files;
    }
    elsif ($svn_files) {
        require Code::TidyAll::SVN::Util;
        @files = Code::TidyAll::SVN::Util::svn_uncommitted_files( $ct->root_dir );
    }
    elsif ($git_files) {
        require Code::TidyAll::Git::Util;
        @files = Code::TidyAll::Git::Util::git_uncommitted_files( $ct->root_dir );
    }
    @results = $ct->process_files(@files);
}
elsif ( my @files = @ARGV ) {
    my $conf_file = $tidyall_class->find_conf_file( dirname( $files[0] ) );
    my $ct = $tidyall_class->new_from_conf_file( $conf_file, %params );
    @results = $ct->process_files(@files);
}
else {
    die "must pass -a/--all, -s/--svn, -g/--git, or filename(s)";
}

my $status = ( grep { $_->error } @results ) ? 1 : 0;
exit($status);

1;



=pod

=head1 NAME

tidyall - Your all-in-one code tidier and validator

=head1 VERSION

version 0.04

=head1 SYNOPSIS

    # Create a tidyall.ini at the top of your project
    #
    [PerlTidy]
    select = **/*.{pl,pm,t}
    argv = -noll -it=2

    [PerlCritic]
    select = lib/**/*.pm
    ignore = lib/UtterHack.pm
    argv = -severity 3

    # Process all files in the current project,
    # look upwards from cwd for tidyall.ini
    #
    % tidyall -a

    # Process all files in a particular project
    #
    % tidyall -a --root-dir /home/joe/project

    # Process one or more specific files,
    # look upwards from the first file for tidyall.ini
    #
    % tidyall file [file...]

=head1 DESCRIPTION

There are a lot of great code tidiers and validators out there. C<tidyall>
makes them available from a single unified interface.

You can run C<tidyall> on a single file or on an entire project hierarchy, and
configure which tidiers/validators are applied to which files. C<tidyall> will
back up files beforehand, and for efficiency will only consider files that have
changed since they were last processed.

=head2 What's a tidier? What's a validator?

A I<tidier> transforms a file so as to improve its appearance without changing
its semantics. Examples include L<perltidy>, L<podtidy> and
L<htmltidy|HTML::Tidy>.

A I<validator> analyzes a file for some definition of correctness. Examples
include L<perlcritic>, L<podchecker> and
L<xmllint|http://xmlsoft.org/xmllint.html>.

Many tidiers are also validators, e.g. C<perltidy> will throw an error on badly
formed Perl.

To use a tidier or validator with C<tidyall> it must have a corresponding
plugin class, usually under the prefix C<Code::TidyAll::Plugin::>.  This
distribution comes with plugins for
L<perltidy|Code::TidyAll::Plugin::PerlTidy>,
L<perlcritic|Code::TidyAll::Plugin::PerlCritic> and
L<podtidy|Code::TidyAll::Plugin::PodTidy>. See
L<Code::TidyAll::Plugin|Code::TidyAll::Plugin> for information about creating
your own plugin.

=head1 USING TIDYALL

C<tidyall> works on a project basis, where a project is just a directory
hierarchy of files. svn or git working directories are typical examples of
projects.

The top of the project is called the I<root directory>. In the root directory
you'll need a C<tidyall.ini> config file; it defines how various tidiers and
validators will be applied to the files in your project.

C<tidyall> will find your root directory and config file automatically
depending on how you call it:

=over

=item C<< tidyall file [file...] >>

C<tidyall> will search upwards from the first file for C<tidyall.ini>.

=item C<< tidyall -a >>

C<tidyall> will search upwards from the current working directory for
C<tidyall.ini>.

=item C<< tidyall -a --root-dir dir >>

C<tidyall> will expect to find C<tidyall.ini> in the specified root directory.

=back

=head1 CONFIGURATION

The config file (C<tidyall.ini>) is in L<Config::INI|Config::INI> format.
Here's a sample:

    [PerlTidy]
    select = **/*.{pl,pm,t}
    argv = -noll -it=2

    [PerlCritic]
    select = lib/**/*.pm
    ignore = lib/UtterHack.pm
    argv = -severity 3

    [PodTidy]
    select = lib/**/*.{pm,pod}

In order, the three sections declare:

=over

=item *

Apply C<PerlTidy> with settings "-noll -it=2" to all *.pl, *.pm, and *.t files.

=item *

Apply C<PerlCritic> with severity 3 to all Perl modules somewhere underneath
"lib/", except for "lib/UtterHack.pm".

=item *

Apply C<PodTidy> with default settings to all *.pm and *.pod files underneath
"lib/".

=back

=head2 Standard configuration elements

=over

=item [class]

The header of each section refers to a tidyall I<plugin>. The name is
automatically prefixed with C<Code::TidyAll::Plugin::> unless it begins with a
'+', e.g.

    # Uses plugin Code::TidyAll::Plugin::PerlTidy
    [PerlTidy]

    # Uses plugin My::TidyAll::Plugin
    [+My::TidyAll::Plugin]

=item select

A required L<File::Zglob|File::Zglob> pattern indicating which files to select,
e.g.

    # All .pl and .pm files somewhere under bin, lib and t
    select = {bin,lib,t}/**/*.p[lm]

    # All .txt files anywhere in the project
    select = **/*.txt

The pattern is relative to the root directory and should have no leading slash.
All standard glob characters (C<*>, C<?>, C<[]>, C<{}>) will work; in addition,
C<**> can be used to represent zero or more directories. See
L<File::Zglob|File::Zglob> documentation for more details.

=item ignore

A L<File::Zglob|File::Zglob> pattern, of the same format described above,
indicating which files to ignore.  This overrides C<select>. e.g.

    # All .pl files under bin, except under bin/tmp
    select = bin/**/*.pl
    ignore = bin/tmp/*.pl

=item only_modes

A list of modes, separated by whitespace. e.g.

    only_modes = tests cli

The plugin will I<only> run if one of these modes is passed to C<tidyall> via
C<-m> or C<--mode>.

=item except_modes

A list of modes, separated by whitespace. e.g.

    except_modes = commit editor

The plugin will I<not> run if one of these modes is passed to C<tidyall> via
C<-m> or C<--mode>.

=item argv

Many plugins (such as L<perltidy|Code::TidyAll::Plugin::PerlTidy>,
L<perlcritic|Code::TidyAll::Plugin::PerlCritic> and
L<podtidy|Code::TidyAll::Plugin::PodTidy>) take this option, which specifies
arguments to pass to the underlying command-line utility.

=back

=head1 COMMAND-LINE OPTIONS

=over

=item -a, --all        

Process all files. Does a recursive search for all files in the project
hierarchy, starting at the root, and processes any file that matches at least
one plugin in the configuration.

=item -h, --help       

Print help message

=item -m, --mode       

Optional mode that can affect which plugins run. Defaults to 'cli'. See
L</MODES>.

=item -s, --svn

Process all added or modified files in the current svn working directory.

=item -q, --quiet      

Suppress output except for errors.

=item -v, --verbose    

Show extra output.

=item -I

Add libraries to @INC, as Perl's -I. Useful if --tidyall-class or plugins are
in an alternate lib directory.

=item --backup-ttl     

Amount of time before backup files can be purged; defaults to "1h". Can be any
string recognized by L<Time::Duration::Parse|Time::Duration::Parse>.

=item --check-only     

Just check if each file is tidied (i.e. if its tidied version is equal to its
current version) and consider it an error if not; don't actually modify the
file. This is used by L<Test::Code::TidyAll|Test::Code::TidyAll>, for example.

=item --conf-file      

Specify config file explicitly. Usually this is inferred from the specified
file(s) or from the current working directory.

=item --data-dir       

Contains data like backups and cache. Defaults to root_dir/.tidyall.d

=item --no-backups     

Don't backup files before processing.

=item --no-cache       

Don't cache last processed times; process all files every time. See also
C<--refresh-cache>.

=item --output-suffix  

Suffix to add to a filename before outputting the modified version, e.g.
".tdy". Default is none, which means overwrite the file.

=item --refresh-cache  

Erase any existing cache info before processing each file, then write new cache
info. See also C<--no-cache>.

=item --root-dir       

Specify root dir explicitly. Usually this is inferred from the specified files
or the current working directory.

=item --tidyall-class  

Subclass to use instead of C<Code::TidyAll>.

=back

=head2 Specifying options in configuration

Almost any command-line option can be specified at the top of the config file,
above the plugin sections. Replace dashes with underscores. e.g.

    backup_ttl = 4h
    tidyall_class = My::Code::TidyAll

    [PerlTidy]
    select = **/*.{pl,pm,t}
    argv = -noll -it=2

    ...

If an option is passed in both places, the command-line takes precedence.

=head1 MODES

You can use tidyall in a number of different contexts, and you may not want to
run all plugins in all of them.

You can pass a mode to tidyall via C<-m> or C<--mode>, and then specify that
certain plugins should only be run in certain modes (via L</only_modes>) or
should be run in all but certain modes (via L</except_modes>).

Examples of modes:

=over

=item *

C<cli> - when invoking tidyall explicitly from the command-line with no mode
specified

=item *

C<editor> - when invoking from an editor

=item *

C<commit> - when using a commit hook like
L<Code::TidyAll::SVN::Precommit|Code::TidyAll::SVN::Precommit>

=item *

C<test> - when using L<Test::Code::TidyAll|Test::Code::TidyAll>

=back

Now since L<perlcritic|Code::TidyAll::Plugin::PerlCritic> is a bit
time-consuming, you might only want to run it during tests and explicit
command-line invocation:

    [PerlCritic]
    select = lib/**/*.pm
    only_modes = tests cli
    ...

Or you could specify that it be run in all modes I<except> the editor:

    [PerlCritic]
    select = lib/**/*.pm
    except_modes = editor
    ...

If you specify neither C<only_modes> nor C<except_modes> for a plugin, then it
will always run.

=head1 LAST-PROCESSED CACHE

C<tidyall> keeps track of each file's signature after it was last processed. On
subsequent runs, it will only process a file if its signature has changed. The
cache is kept in files under the data dir.

You can force a refresh of the cache with C<--refresh-cache>, or turn off the
behavior entirely with C<--no-cache>.

=head1 BACKUPS

C<tidyall> will backup each file before modifying it. The timestamped backups
are kept in a separate directory hierarchy under the data dir.

Old backup files will be purged automatically as part of occasional C<tidyall>
runs. The duration specified in C<--backup-ttl> indicates both the minimum
amount of time backups should be kept, and the frequency that purges should be
run. It may be specified as "30m" or "4 hours" or any string acceptable to
L<Time::Duration::Parse|Time::Duration::Parse>. It defaults to "1h" (1 hour).

You can turn off backups with C<--no-backups>.

=head1 EXIT STATUS

C<tidyall> will exit with status 1 if any errors occurred while processing
files, and 0 otherwise.

=head1 SUBVERSION PRECOMMIT

L<Code::TidyAll::SVN::Precommit|Code::TidyAll::SVN::Precommit> implements a
subversion precommit hook that checks if all files are tidied and valid
according to C<tidyall>, and rejects the commit if not.

=head1 KNOWN BUGS

=over

=item *

Does not yet work on Windows

=back

=head1 ACKNOWLEDGEMENTS

Thanks to Jeff Thalhammer for helping me refine this API. Thanks to Jeff for
perlcritic, Steve Hancock for perltidy, and all the other authors of great open
source tidiers and validators.

=head1 SEE ALSO

L<Code::TidyAll|Code::TidyAll>

=head1 AUTHOR

Jonathan Swartz <swartz@pobox.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Jonathan Swartz.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


__END__

