#
# This file is part of Dist-Zilla-Plugin-Git
#
# This software is copyright (c) 2009 by Jerome Quelin.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#
package Dist::Zilla::Plugin::Git::GatherDir;
# ABSTRACT: Gather all tracked files in a Git working directory

our $VERSION = '2.048';

use Moose;
extends 'Dist::Zilla::Plugin::GatherDir' => { -version => 4.200016 }; # exclude_match

#pod =head1 SYNOPSIS
#pod
#pod In your F<dist.ini>:
#pod
#pod     [Git::GatherDir]
#pod     root = .                     ; this is the default
#pod     prefix =                     ; this is the default
#pod     include_dotfiles = 0         ; this is the default
#pod     include_untracked = 0        ; this is the default
#pod     exclude_filename = dir/skip  ; there is no default
#pod     exclude_match = ^local_      ; there is no default
#pod
#pod =head1 DESCRIPTION
#pod
#pod This is a trivial variant of the L<GatherDir|Dist::Zilla::Plugin::GatherDir>
#pod plugin.  It looks in the directory named in the L</root> attribute and adds all
#pod the Git tracked files it finds there (as determined by C<git ls-files>).  If the
#pod root begins with a tilde, the tilde portion is passed through C<glob()> first.
#pod
#pod Most users just need:
#pod
#pod   [Git::GatherDir]
#pod
#pod ...and this will pick up all tracked files from the current directory into the
#pod dist.  You can use it multiple times, as you can any other plugin, by providing
#pod a plugin name.  For example, if you want to include external specification
#pod files into a subdir of your dist, you might write:
#pod
#pod   [Git::GatherDir]
#pod   ; this plugin needs no config and gathers most of your files
#pod
#pod   [Git::GatherDir / SpecFiles]
#pod   ; this plugin gets all tracked files in the root dir and adds them under ./spec
#pod   root   = ~/projects/my-project/spec
#pod   prefix = spec
#pod
#pod =cut

use List::Util 1.45 qw(uniq);
use Types::Standard 'Bool';

use namespace::autoclean;

#pod =attr root
#pod
#pod This is the directory in which to look for files.  If not given, it defaults to
#pod the dist root -- generally, the place where your F<dist.ini> or other
#pod configuration file is located.  It may begin with C<~> (or C<~user>)
#pod to mean your (or some other user's) home directory.  If a relative path,
#pod it's relative to the dist root.  It does not need to be the root of a
#pod Git repository, but it must be inside a repository.
#pod
#pod =attr prefix
#pod
#pod This parameter can be set to gather all the files found under a common
#pod directory.  See the L<description|DESCRIPTION> above for an example.
#pod
#pod =attr include_dotfiles
#pod
#pod By default, files will not be included if they begin with a dot.  This goes
#pod both for files and for directories relative to the C<root>.
#pod
#pod In almost all cases, the default value (false) is correct.
#pod
#pod =attr include_untracked
#pod
#pod By default, files not tracked by Git will not be gathered.  If this is
#pod set to a true value, then untracked files not covered by a Git ignore
#pod pattern (i.e. those reported by C<git ls-files -o --exclude-standard>)
#pod are also gathered (and you'll probably want to use
#pod L<Git::Check|Dist::Zilla::Plugin::Git::Check> to ensure all files are
#pod checked in before a release).
#pod
#pod C<include_untracked> requires at least Git 1.5.4, but you should
#pod probably not use it if your Git is older than 1.6.5.2.  Versions
#pod before that would not list files matched by your F<.gitignore>, even
#pod if they were already being tracked by Git (which means they will not
#pod be gathered, even though they should be).  Whether that is a problem
#pod depends on the contents of your exclude files (including the global
#pod one, if any).
#pod
#pod =attr follow_symlinks
#pod
#pod Git::GatherDir does not honor GatherDir's
#pod L<follow_symlinks|Dist::Zilla::Plugin::GatherDir/follow_symlinks>
#pod option.  While the attribute exists (because Git::GatherDir is a
#pod subclass), setting it has no effect.
#pod
#pod Directories that are symlinks will not be gathered.  Instead, you'll
#pod get a message saying C<WARNING: %s is symlink to directory, skipping it>.
#pod To suppress the warning, add that directory to C<exclude_filename> or
#pod C<exclude_match>.  To gather the files in the symlinked directory, use
#pod a second instance of GatherDir or Git::GatherDir with appropriate
#pod C<root> and C<prefix> options.
#pod
#pod Files which are symlinks are always gathered.
#pod
#pod =attr exclude_filename
#pod
#pod To exclude certain files from being gathered, use the C<exclude_filename>
#pod option. This may be used multiple times to specify multiple files to exclude.
#pod
#pod =attr exclude_match
#pod
#pod This is just like C<exclude_filename> but provides a regular expression
#pod pattern.  Files matching the pattern are not gathered.  This may be used
#pod multiple times to specify multiple patterns to exclude.
#pod
#pod =cut

has include_untracked => (
  is  => 'ro',
  isa => Bool,
  default => 0,
);

around dump_config => sub
{
    my $orig = shift;
    my $self = shift;

    my $config = $self->$orig;

    $config->{+__PACKAGE__} = {
        include_untracked => $self->include_untracked ? 1 : 0,
        blessed($self) ne __PACKAGE__ ? ( version => $VERSION ) : (),
    };

    foreach my $opt (qw(prune_directory follow_symlinks)) {
      $self->log('WARNING: unused config variable "'.$opt.'"') if exists $config->{+__PACKAGE__}{$opt};
      delete $config->{+__PACKAGE__}{$opt};
    }

    return $config;
};

override gather_files => sub {
  my ($self) = @_;

  require Git::Wrapper;
  require Path::Tiny;

  my $root = '' . $self->root;

  # Convert ~ portion to real directory:
  $root =~ s{^(~[^\\/]*)([\\/])}{$self->_homedir($1) . $2}e;

  $root = Path::Tiny::path($root)->absolute($self->zilla->root);

  # Prepare to gather files
  my $git = Git::Wrapper->new($root->stringify);

  my @opts;
  @opts = qw(--cached --others --exclude-standard) if $self->include_untracked;

  my $exclude_regex = qr/\000/;
  $exclude_regex = qr/$exclude_regex|$_/
    for (@{ $self->exclude_match });

  my %is_excluded = map +($_ => 1), @{ $self->exclude_filename };

  my $prefix = $self->prefix;

  # Loop over files reported by git ls-files
  for my $filename (uniq $git->ls_files(@opts)) {
    # $file is a Path::Tiny relative to $root
    my $file = Path::Tiny::path($filename);

    $self->log_debug("considering $file");

    # Exclusion tests
    unless ($self->include_dotfiles) {
      next if grep /^\./, split q{/}, $file->stringify;
    }

    next if $file =~ $exclude_regex;
    next if $is_excluded{ $file };

    # DZil can't gather directory symlinks
    my $path = $root->child($file);

    if (-d $path) {
      $self->log("WARNING: $file is symlink to directory, skipping it");
      next;
    }

    # Gather the file
    my $fileobj = $self->_file_from_filename($path->stringify);

    $file = Path::Tiny::path($prefix, $file) if length $prefix;

    $fileobj->name($file->stringify);
    $self->add_file($fileobj);
    $self->log_debug("gathered $file");
  }

  return;
};

sub _homedir {
  my ($self, $tilde_dir) = @_;

  # other architectures have no issue with globbing any kind of ~user path
  return (glob($tilde_dir))[0] if $^O ne 'Win32';

  # on Win32, we can use the environment variable(s) for the current user
  return "$]" < '5.016' ? $ENV{HOME} || $ENV{USERPROFILE} : (glob('~'))[0]
    if $tilde_dir eq '~';

  # but otherwise, we can't support this at all, on any version.
  $self->log_fatal('expanding "' . $tilde_dir . '" on Win32 not supported');
}

__PACKAGE__->meta->make_immutable;
no Moose;
1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Dist::Zilla::Plugin::Git::GatherDir - Gather all tracked files in a Git working directory

=head1 VERSION

version 2.048

=head1 SYNOPSIS

In your F<dist.ini>:

    [Git::GatherDir]
    root = .                     ; this is the default
    prefix =                     ; this is the default
    include_dotfiles = 0         ; this is the default
    include_untracked = 0        ; this is the default
    exclude_filename = dir/skip  ; there is no default
    exclude_match = ^local_      ; there is no default

=head1 DESCRIPTION

This is a trivial variant of the L<GatherDir|Dist::Zilla::Plugin::GatherDir>
plugin.  It looks in the directory named in the L</root> attribute and adds all
the Git tracked files it finds there (as determined by C<git ls-files>).  If the
root begins with a tilde, the tilde portion is passed through C<glob()> first.

Most users just need:

  [Git::GatherDir]

...and this will pick up all tracked files from the current directory into the
dist.  You can use it multiple times, as you can any other plugin, by providing
a plugin name.  For example, if you want to include external specification
files into a subdir of your dist, you might write:

  [Git::GatherDir]
  ; this plugin needs no config and gathers most of your files

  [Git::GatherDir / SpecFiles]
  ; this plugin gets all tracked files in the root dir and adds them under ./spec
  root   = ~/projects/my-project/spec
  prefix = spec

=head1 ATTRIBUTES

=head2 root

This is the directory in which to look for files.  If not given, it defaults to
the dist root -- generally, the place where your F<dist.ini> or other
configuration file is located.  It may begin with C<~> (or C<~user>)
to mean your (or some other user's) home directory.  If a relative path,
it's relative to the dist root.  It does not need to be the root of a
Git repository, but it must be inside a repository.

=head2 prefix

This parameter can be set to gather all the files found under a common
directory.  See the L<description|DESCRIPTION> above for an example.

=head2 include_dotfiles

By default, files will not be included if they begin with a dot.  This goes
both for files and for directories relative to the C<root>.

In almost all cases, the default value (false) is correct.

=head2 include_untracked

By default, files not tracked by Git will not be gathered.  If this is
set to a true value, then untracked files not covered by a Git ignore
pattern (i.e. those reported by C<git ls-files -o --exclude-standard>)
are also gathered (and you'll probably want to use
L<Git::Check|Dist::Zilla::Plugin::Git::Check> to ensure all files are
checked in before a release).

C<include_untracked> requires at least Git 1.5.4, but you should
probably not use it if your Git is older than 1.6.5.2.  Versions
before that would not list files matched by your F<.gitignore>, even
if they were already being tracked by Git (which means they will not
be gathered, even though they should be).  Whether that is a problem
depends on the contents of your exclude files (including the global
one, if any).

=head2 follow_symlinks

Git::GatherDir does not honor GatherDir's
L<follow_symlinks|Dist::Zilla::Plugin::GatherDir/follow_symlinks>
option.  While the attribute exists (because Git::GatherDir is a
subclass), setting it has no effect.

Directories that are symlinks will not be gathered.  Instead, you'll
get a message saying C<WARNING: %s is symlink to directory, skipping it>.
To suppress the warning, add that directory to C<exclude_filename> or
C<exclude_match>.  To gather the files in the symlinked directory, use
a second instance of GatherDir or Git::GatherDir with appropriate
C<root> and C<prefix> options.

Files which are symlinks are always gathered.

=head2 exclude_filename

To exclude certain files from being gathered, use the C<exclude_filename>
option. This may be used multiple times to specify multiple files to exclude.

=head2 exclude_match

This is just like C<exclude_filename> but provides a regular expression
pattern.  Files matching the pattern are not gathered.  This may be used
multiple times to specify multiple patterns to exclude.

=for Pod::Coverage gather_dir
    gather_files

=head1 SUPPORT

Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-Git>
(or L<bug-Dist-Zilla-Plugin-Git@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-Git@rt.cpan.org>).

There is also a mailing list available for users of this distribution, at
L<http://dzil.org/#mailing-list>.

There is also an irc channel available for users of this distribution, at
L<C<#distzilla> on C<irc.perl.org>|irc://irc.perl.org/#distzilla>.

=head1 AUTHOR

Jerome Quelin

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2009 by Jerome Quelin.

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