package Gitolite::Rc;

# everything to do with 'rc'.  Also defines some 'constants'
# ----------------------------------------------------------------------

@EXPORT = qw(
  %rc
  glrc
  query_rc
  version
  greeting
  trigger
  _which

  $REMOTE_COMMAND_PATT
  $REF_OR_FILENAME_PATT
  $REPONAME_PATT
  $REPOPATT_PATT
  $USERNAME_PATT
  $UNSAFE_PATT
);

use Exporter 'import';
use Sys::Hostname;

use Gitolite::Common;

# ----------------------------------------------------------------------

our %rc;
our $non_core;

# ----------------------------------------------------------------------

# pre-populate some important rc keys
# ----------------------------------------------------------------------

$rc{GL_BINDIR} = $ENV{GL_BINDIR};
$rc{GL_LIBDIR} = $ENV{GL_LIBDIR};

# these keys could be overridden by the rc file later
$rc{GL_REPO_BASE}  = "$ENV{HOME}/repositories";
$rc{GL_ADMIN_BASE} = "$ENV{HOME}/.gitolite";
$rc{LOG_TEMPLATE}  = "$ENV{HOME}/.gitolite/logs/gitolite-%y-%m.log";

# variables that should probably never be changed but someone will want to, I'll bet...
# ----------------------------------------------------------------------

#<<<
$REMOTE_COMMAND_PATT  =                qr(^[-0-9a-zA-Z._\@/+ :,\%=]*$);
$REF_OR_FILENAME_PATT =     qr(^[0-9a-zA-Z][-0-9a-zA-Z._\@/+ :,]*$);
$REPONAME_PATT        =  qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@/+]*$);
$REPOPATT_PATT        = qr(^\@?[[0-9a-zA-Z][-0-9a-zA-Z._\@/+\\^$|()[\]*?{},]*$);
$USERNAME_PATT        =  qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@+]*$);

$UNSAFE_PATT          = qr([`~#\$\&()|;<>]);
#>>>

# ----------------------------------------------------------------------

# find the rc file and 'do' it
# ----------------------------------------------------------------------
my $current_data_version = "3.2";

my $rc = glrc('filename');
if ( -r $rc and -s $rc ) {
    do $rc or die $@;
}
if ( defined($GL_ADMINDIR) ) {
    say2 "";
    say2 "FATAL: '$rc' seems to be for older gitolite; please see\nhttp://gitolite.com/gitolite/migr.html";

    exit 1;
}

# let values specified in rc file override our internal ones
# ----------------------------------------------------------------------
@rc{ keys %RC } = values %RC;

# expand the non_core list into INPUT, PRE_GIT, etc using 'ENABLE' settings
non_core_expand() if $rc{ENABLE};

# add internal triggers
# ----------------------------------------------------------------------

# is the server/repo in a writable state (i.e., not down for maintenance etc)
unshift @{ $rc{ACCESS_1} }, 'Writable::access_1';

# (testing only) override the rc file silently
# ----------------------------------------------------------------------
# use an env var that is highly unlikely to appear in real life :)
do $ENV{G3T_RC} if exists $ENV{G3T_RC} and -r $ENV{G3T_RC};

# setup some perl/rc/env vars, plus umask
# ----------------------------------------------------------------------

umask ( $rc{UMASK} || 0077 );

unshift @INC, "$rc{LOCAL_CODE}/lib" if $rc{LOCAL_CODE};

$ENV{PATH} = "$ENV{GL_BINDIR}:$ENV{PATH}" unless $ENV{PATH} =~ /^$ENV{GL_BINDIR}:/;

{
    $rc{GL_TID} = $ENV{GL_TID} ||= $$;
    # TID: loosely, transaction ID.  The first PID at the entry point passes
    # it down to all its children so you can track each access, across all the
    # various commands it spawns and actions it generates.

    $rc{GL_LOGFILE} = $ENV{GL_LOGFILE} ||= gen_lfn( $rc{LOG_TEMPLATE} );
}

# these two are meant to help externally written commands (see
# src/commands/writable for an example)
$ENV{GL_REPO_BASE}  = $rc{GL_REPO_BASE};
$ENV{GL_ADMIN_BASE} = $rc{GL_ADMIN_BASE};

# ----------------------------------------------------------------------

use strict;
use warnings;

# ----------------------------------------------------------------------

my $glrc_default_text = '';
{
    local $/ = undef;
    $glrc_default_text = <DATA>;
}

# ----------------------------------------------------------------------

sub non_core_expand {
    my %enable;

    for my $e ( @{ $rc{ENABLE} } ) {
        my ( $name, $arg ) = split ' ', $e, 2;
        # store args as the hash value for the name
        $enable{$name} = $arg || '';

        # for now, we pretend everything is a command, because commands
        # are the only thing that the non_core list does not contain
        $rc{COMMANDS}{$name} = $arg || 1;
    }

    # bring in additional non-core specs from the rc file, if given
    if ( my $nc2 = $rc{NON_CORE} ) {
        for ( $non_core, $nc2 ) {
            # beat 'em into shape :)
            s/#.*//g;
            s/[ \t]+/ /g; s/^ //mg; s/ $//mg;
            s/\n+/\n/g;
        }

        for ( split "\n", $nc2 ) {
            next unless /\S/;
            my ( $name, $where, $module, $before, $name2 ) = split ' ', $_;
            if ( not $before ) {
                $non_core .= "$name $where $module\n";
                next;
            }
            die if $before ne 'before';
            $non_core =~ s(^(?=$name2 $where( |$)))($name $where $module\n)m;
        }
    }

    my @data = split "\n", $non_core || '';
    for (@data) {
        next if /^\s*(#|$)/;
        my ( $name, $where, $module ) = split ' ', $_;

        # if it appears here, it's not a command, so delete it.  At the end of
        # this loop, what's left in $rc{COMMANDS} will be those names in the
        # enable list that do not appear in the non_core list.
        delete $rc{COMMANDS}{$name};

        next unless exists $enable{$name};

        # module to call is name if specified as "."
        $module = $name if $module eq ".";

        # module to call is "name::pre_git" or such if specified as "::"
        ( $module = $name ) .= "::" . lc($where) if $module eq '::';

        # append arguments, if supplied
        $module .= " $enable{$name}" if $enable{$name};

        push @{ $rc{$where} }, $module;
    }

    # finally, add in commands that were declared in the non-core list
    map { /^(\S+)/; $rc{COMMANDS}{$1} = 1 } @{ $rc{COMMAND} };
}

# exported functions
# ----------------------------------------------------------------------

sub glrc {
    my $cmd = shift;
    if ( $cmd eq 'default-filename' ) {
        return "$ENV{HOME}/.gitolite.rc";
    } elsif ( $cmd eq 'default-text' ) {
        return $glrc_default_text if $glrc_default_text;
        _die "rc file default text not set; this should not happen!";
    } elsif ( $cmd eq 'filename' ) {
        # where is the rc file?

        # search $HOME first
        return "$ENV{HOME}/.gitolite.rc" if -f "$ENV{HOME}/.gitolite.rc";

        return '';
    } elsif ( $cmd eq 'current-data-version' ) {
        return $current_data_version;
    } else {
        _die "unknown argument to glrc: '$cmd'";
    }
}

my $all   = 0;
my $dump  = 0;
my $nonl  = 0;
my $quiet = 0;

sub query_rc {

    my @vars = args();

    no strict 'refs';

    if ($all) {
        for my $e ( sort keys %rc ) {
            print "$e=" . ( defined( $rc{$e} ) ? $rc{$e} : 'undef' ) . "\n";
        }
        exit 0;
    }

    if ($dump) {
        require Data::Dumper;
        $Data::Dumper::Sortkeys = 1;
        print Data::Dumper::Dumper \%rc;
        exit 0;
    }

    my $cv = \%rc;    # current "value"
    while (@vars) {
        my $v = shift @vars;

        # dig into the rc hash, using each var as a component
        if ( not ref($cv) ) {
            _warn "unused arguments...";
            last;
        } elsif ( ref($cv) eq 'HASH' ) {
            $cv = $cv->{$v} || '';
        } elsif ( ref($cv) eq 'ARRAY' ) {
            $cv = $cv->[$v] || '';
        } else {
            _die "dont know what to do with " . ref($cv) . " item in the rc file";
        }
    }

    # we've run out of arguments so $cv is what we have.  If we're supposed to
    # be quiet, we don't have to print anything so let's get that done first:
    exit( $cv ? 0 : 1 ) if $quiet;    # shell truth

    # print values (notice we ignore the '-n' option if it's a ref)
    if ( ref($cv) eq 'HASH' ) {
        print join( "\n", sort keys %$cv ), "\n" if %$cv;
    } elsif ( ref($cv) eq 'ARRAY' ) {
        print join( "\n", @$cv ), "\n" if @$cv;
    } else {
        print $cv . ( $nonl ? '' : "\n" ) if $cv;
    }
    exit( $cv ? 0 : 1 );              # shell truth
}

sub version {
    my $version = '';
    $version = '(unknown)';
    for ("$ENV{GL_BINDIR}/VERSION") {
        $version = slurp($_) if -r $_;
    }
    chomp($version);
    return $version;
}

sub greeting {
    my $json = shift;

    my $hn = (split /\./, hostname())[0];
    my $gv = substr( `git --version`, 12 );
    my $gl_user = $ENV{GL_USER} || '';
    $gl_user = " $gl_user" if $gl_user;

    if ($json) {
        $json->{GL_USER}          = $ENV{GL_USER};
        $json->{USER}             = ( $ENV{USER} || "httpd" ) . "\@$hn";
        $json->{gitolite_version} = version();
        chomp( $json->{git_version} = $gv );    # this thing has a newline at the end
        return;
    }

    # normal output
    return "hello$gl_user, this is " . ( $ENV{USER} || "httpd" ) . "\@$hn running gitolite3 " . version() . " on git $gv\n";
}

sub trigger {
    my $rc_section = shift;

    # if arg-2 (now arg-1, due to the 'shift' above) exists, it is a repo
    # name, so setup env from options
    require Gitolite::Conf::Load;
    Gitolite::Conf::Load->import('env_options');
    env_options( $_[0] ) if $_[0];

    if ( exists $rc{$rc_section} ) {
        if ( ref( $rc{$rc_section} ) ne 'ARRAY' ) {
            _die "'$rc_section' section in rc file is not a perl list";
        } else {
            for my $s ( @{ $rc{$rc_section} } ) {
                my ( $pgm, @args ) = split ' ', $s;

                if ( my ( $module, $sub ) = ( $pgm =~ /^(.*)::(\w+)$/ ) ) {

                    require Gitolite::Triggers;
                    trace( 2, 'trigger module', $module, $sub, @args, $rc_section, @_ );
                    Gitolite::Triggers::run( $module, $sub, @args, $rc_section, @_ );

                } else {
                    $pgm = _which( "triggers/$pgm", 'x' );

                    _warn("skipped trigger '$s' (not found or not executable)"), next if not $pgm;
                    trace( 2, 'trigger command', $s );
                    _system( $pgm, @args, $rc_section, @_ );    # they better all return with 0 exit codes!
                }
            }
        }
        return;
    }
    trace( 3, "'$rc_section' not found in rc" );
}

sub _which {
    # looks for a file in LOCAL_CODE or GL_BINDIR.  Returns whichever exists
    # (LOCAL_CODE preferred if defined) or 0 if not found.
    my $file = shift;
    my $mode = shift;    # could be 'x' or 'r'

    my @files = ("$rc{GL_BINDIR}/$file");
    unshift @files, ("$rc{LOCAL_CODE}/$file") if $rc{LOCAL_CODE};

    for my $f (@files) {
        return $f if -x $f;
        return $f if -r $f and $mode eq 'r';
    }

    return 0;
}

# ----------------------------------------------------------------------

=for args
Usage:  gitolite query-rc -a
        gitolite query-rc -d
        gitolite query-rc [-n] [-q] rc-variable

    -a          print all variables and values (first level only)
    -d          dump the entire rc structure
    -n          do not append a newline if variable is scalar
    -q          exit code only (shell truth; 0 is success)

Query the rc hash.  Second and subsequent arguments dig deeper into the hash.
The examples are for the default configuration; yours may be different.

Single values:
    gitolite query-rc GL_ADMIN_BASE     # prints "/home/git/.gitolite" or similar
    gitolite query-rc UMASK             # prints "63" (that's 0077 in decimal!)

Hashes:
    gitolite query-rc COMMANDS
        # prints "desc", "help", "info", "perms", "writable", one per line
    gitolite query-rc COMMANDS help     # prints 1
    gitolite query-rc -q COMMANDS help  # prints nothing; exit code is 0
    gitolite query-rc COMMANDS fork     # prints nothing; exit code is 1

Arrays (somewhat less useful):
    gitolite query-rc POST_GIT          # prints nothing; exit code is 0
    gitolite query-rc POST_COMPILE      # prints 4 lines
    gitolite query-rc POST_COMPILE 0    # prints the first of those 4 lines

Explore:
    gitolite query-rc -a
    # prints all first level variables and values, one per line.  Any that are
    # listed as HASH or ARRAY can be explored further in subsequent commands.
    gitolite query-rc -d                # dump the entire rc structure
=cut

sub args {
    my $help = 0;

    require Getopt::Long;
    Getopt::Long::GetOptions(
        'all|a'   => \$all,
        'dump|d'  => \$dump,
        'nonl|n'  => \$nonl,
        'quiet|q' => \$quiet,
        'help|h'  => \$help,
    ) or usage();

    _die("'-a' cannot be combined with other arguments or options; run with '-h' for usage") if $all and ( @ARGV or $dump or $nonl or $quiet );
    usage() if not $all and not $dump and not @ARGV or $help;
    return @ARGV;
}

# ----------------------------------------------------------------------

BEGIN {
    $non_core = "
    # No user-servicable parts inside.  Warranty void if seal broken.  Refer
    # servicing to authorised service center only.

    continuation-lines      SYNTACTIC_SUGAR .
    keysubdirs-as-groups    SYNTACTIC_SUGAR .
    macros                  SYNTACTIC_SUGAR .
    refex-expr              SYNTACTIC_SUGAR .

    renice                  PRE_GIT         .

    Kindergarten            INPUT           ::

    CpuTime                 INPUT           ::
    CpuTime                 POST_GIT        ::

    Shell                   INPUT           ::

    Alias                   INPUT           ::

    Motd                    INPUT           ::
    Motd                    PRE_GIT         ::
    Motd                    COMMAND         motd

    Mirroring               INPUT           ::
    Mirroring               PRE_GIT         ::
    Mirroring               POST_GIT        ::

    refex-expr              ACCESS_2        RefexExpr::access_2

    expand-deny-messages    ACCESS_1        .
    expand-deny-messages    ACCESS_2        .

    RepoUmask               PRE_GIT         ::
    RepoUmask               POST_CREATE     ::

    partial-copy            PRE_GIT         .

    upstream                PRE_GIT         .

    no-create-on-read       PRE_CREATE      AutoCreate::deny_R
    no-auto-create          PRE_CREATE      AutoCreate::deny_RW

    ssh-authkeys-split      POST_COMPILE    post-compile/ssh-authkeys-split
    ssh-authkeys            POST_COMPILE    post-compile/ssh-authkeys
    Shell                   POST_COMPILE    post-compile/ssh-authkeys-shell-users

    set-default-roles       POST_CREATE     .

    git-config              POST_COMPILE    post-compile/update-git-configs
    git-config              POST_CREATE     post-compile/update-git-configs

    create-with-reference   POST_CREATE     post-compile/create-with-reference

    gitweb                  POST_CREATE     post-compile/update-gitweb-access-list
    gitweb                  POST_COMPILE    post-compile/update-gitweb-access-list

    cgit                    POST_COMPILE    post-compile/update-description-file

    daemon                  POST_CREATE     post-compile/update-git-daemon-access-list
    daemon                  POST_COMPILE    post-compile/update-git-daemon-access-list

    repo-specific-hooks     POST_COMPILE    .
    repo-specific-hooks     POST_CREATE     .
";
}

1;

# ----------------------------------------------------------------------

__DATA__
# configuration variables for gitolite

# This file is in perl syntax.  But you do NOT need to know perl to edit it --
# just mind the commas, use single quotes unless you know what you're doing,
# and make sure the brackets and braces stay matched up!

# (Tip: perl allows a comma after the last item in a list also!)

# HELP for commands can be had by running the command with "-h".

# HELP for all the other FEATURES can be found in the documentation (look for
# "list of non-core programs shipped with gitolite" in the master index) or
# directly in the corresponding source file.

%RC = (

    # ------------------------------------------------------------------

    # default umask gives you perms of '0700'; see the rc file docs for
    # how/why you might change this
    UMASK                           =>  0077,

    # look for "git-config" in the documentation
    GIT_CONFIG_KEYS                 =>  '',

    # comment out if you don't need all the extra detail in the logfile
    LOG_EXTRA                       =>  1,
    # logging options
    # 1. leave this section as is for 'normal' gitolite logging (default)
    # 2. uncomment this line to log ONLY to syslog:
    # LOG_DEST                      => 'syslog',
    # 3. uncomment this line to log to syslog and the normal gitolite log:
    # LOG_DEST                      => 'syslog,normal',
    # 4. prefixing "repo-log," to any of the above will **also** log just the
    #    update records to "gl-log" in the bare repo directory:
    # LOG_DEST                      => 'repo-log,normal',
    # LOG_DEST                      => 'repo-log,syslog',
    # LOG_DEST                      => 'repo-log,syslog,normal',
    # syslog 'facility': defaults to 'local0', uncomment if needed.  For example:
    # LOG_FACILITY                  => 'local4',

    # roles.  add more roles (like MANAGER, TESTER, ...) here.
    #   WARNING: if you make changes to this hash, you MUST run 'gitolite
    #   compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
    ROLES => {
        READERS                     =>  1,
        WRITERS                     =>  1,
    },

    # enable caching (currently only Redis).  PLEASE RTFM BEFORE USING!!!
    # CACHE                         =>  'Redis',

    # ------------------------------------------------------------------

    # rc variables used by various features

    # the 'info' command prints this as additional info, if it is set
        # SITE_INFO                 =>  'Please see http://blahblah/gitolite for more help',

    # the CpuTime feature uses these
        # display user, system, and elapsed times to user after each git operation
        # DISPLAY_CPU_TIME          =>  1,
        # display a warning if total CPU times (u, s, cu, cs) crosses this limit
        # CPU_TIME_WARN_LIMIT       =>  0.1,

    # the Mirroring feature needs this
        # HOSTNAME                  =>  "foo",

    # TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING!
        # CACHE_TTL                 =>  600,

    # ------------------------------------------------------------------

    # suggested locations for site-local gitolite code (see cust.html)

        # this one is managed directly on the server
        # LOCAL_CODE                =>  "$ENV{HOME}/local",

        # or you can use this, which lets you put everything in a subdirectory
        # called "local" in your gitolite-admin repo.  For a SECURITY WARNING
        # on this, see http://gitolite.com/gitolite/non-core.html#using-the-gitolite-admin-repo-to-manage-non-core-code
        # LOCAL_CODE                =>  "$rc{GL_ADMIN_BASE}/local",

    # ------------------------------------------------------------------

    # List of commands and features to enable

    ENABLE => [

        # COMMANDS

            # These are the commands enabled by default
            'help',
            'desc',
            'info',
            'perms',
            'writable',

            # Uncomment or add new commands here.
            # 'create',
            # 'fork',
            # 'mirror',
            # 'readme',
            # 'sskm',
            # 'D',

        # These FEATURES are enabled by default.

            # essential (unless you're using smart-http mode)
            'ssh-authkeys',

            # creates git-config entries from gitolite.conf file entries like 'config foo.bar = baz'
            'git-config',

            # creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
            'daemon',

            # creates projects.list file; if you don't use gitweb, comment this out
            'gitweb',

        # These FEATURES are disabled by default; uncomment to enable.  If you
        # need to add new ones, ask on the mailing list :-)

        # user-visible behaviour

            # prevent wild repos auto-create on fetch/clone
            # 'no-create-on-read',
            # no auto-create at all (don't forget to enable the 'create' command!)
            # 'no-auto-create',

            # access a repo by another (possibly legacy) name
            # 'Alias',

            # give some users direct shell access.  See documentation in
            # sts.html for details on the following two choices.
            # "Shell $ENV{HOME}/.gitolite.shell-users",
            # 'Shell alice bob',

            # set default roles from lines like 'option default.roles-1 = ...', etc.
            # 'set-default-roles',

            # show more detailed messages on deny
            # 'expand-deny-messages',

            # show a message of the day
            # 'Motd',

        # system admin stuff

            # enable mirroring (don't forget to set the HOSTNAME too!)
            # 'Mirroring',

            # allow people to submit pub files with more than one key in them
            # 'ssh-authkeys-split',

            # selective read control hack
            # 'partial-copy',

            # manage local, gitolite-controlled, copies of read-only upstream repos
            # 'upstream',

            # updates 'description' file instead of 'gitweb.description' config item
            # 'cgit',

            # allow repo-specific hooks to be added
            # 'repo-specific-hooks',

        # performance, logging, monitoring...

            # be nice
            # 'renice 10',

            # log CPU times (user, system, cumulative user, cumulative system)
            # 'CpuTime',

        # syntactic_sugar for gitolite.conf and included files

            # allow backslash-escaped continuation lines in gitolite.conf
            # 'continuation-lines',

            # create implicit user groups from directory names in keydir/
            # 'keysubdirs-as-groups',

            # allow simple line-oriented macros
            # 'macros',

        # Kindergarten mode

            # disallow various things that sensible people shouldn't be doing anyway
            # 'Kindergarten',
    ],

);

# ------------------------------------------------------------------------------
# per perl rules, this should be the last line in such a file:
1;

# Local variables:
# mode: perl
# End:
# vim: set syn=perl:
