# Copyright © 2010 Julian Blake Kongslie <jblake@omgwallhack.org>
# Licensed under the BSD 3-clause license.

use strict;
use warnings;

package Piny::Repo;

use Moose;
use Moose::Util::TypeConstraints;
use MooseX::StrictConstructor;

use Digest::SHA qw( sha256_hex );
use File::Copy;
use File::Find qw( find );
use File::Temp qw( );
use IO::Dir qw( );
use Math::GMP qw( );
use POSIX qw( );

use IkiWiki::FakeSetup qw( readSetup writeSetup );

use Piny::Config;
use Piny::Environment;
use Piny::Group;
use Piny::User;
use Piny::User::IkiWiki;

# Types

subtype 'Reponame'
  => as 'Str'
  => where { $_ =~ /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/ }
  => message { 'That name is not in the correct format for a piny repo.' }
  ;

subtype 'SimpleText'
  => as 'Str'
  => where { $_ =~ /^[\x{0020}-\x{FDCF}\x{FDF0}-\x{FFFD}]{1,80}$/ }
  => message { 'That description is not in the correct format for a piny repo.' }
  ;

# Attributes

has 'name' =>
  ( is        => 'ro'
  , isa       => 'Reponame'
  , required  => 1
  );

has 'shortname' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'group' =>
  ( is          => 'ro'
  , isa         => 'Piny::Group'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'path' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'description' =>
  ( is          => 'rw'
  , isa         => 'SimpleText'
  , trigger     => \&_set_description
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'repostat' =>
  ( is          => 'ro'
  , isa         => 'ArrayRef'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'owner' =>
  ( is          => 'rw'
  , isa         => 'Piny::User'
  , trigger     => \&_change_owner
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'globally_readable' =>
  ( is          => 'ro'
  , isa         => 'Bool'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'globally_writable' =>
  ( is          => 'ro'
  , isa         => 'Bool'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'cgit_url' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_setup' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_destdir' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_srcdir' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_url' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_cgiurl' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_historyurl' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_diffurl' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'ikiwiki_cgipath' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'secure_path' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'apache_global_config' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'apache_secure_config' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'apache_www_config' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'config' =>
  ( is          => 'ro'
  , isa         => 'Piny::Config'
  , lazy_build  => 1
  , init_arg    => undef
  , clearer     => 'clear_config'
  );

# Public methods

sub add_access {
  my ( $s, @users ) = @_;

  $s->group->add_member( @users );
};

sub remove_access {
  my ( $s, @users ) = @_;

  $s->group->remove_member( @users );
};

sub has_access {
  my ( $s, $user ) = @_;

  return $s->owner->uid == $user->uid || $user->has_group( $s->group );
};

sub rebuild {
  my ( $s ) = @_;

  if( POSIX::access( $s->path, &POSIX::W_OK ) ){
    $s->rebuild_git;
  } else {
    warn "Unwritable git repository. Skipping git rebuild.";
  };
  $s->rebuild_apache;

  $s->destroy_ikiwiki;
  if ( $s->config->piny_ikiwiki =~ /^(1|true)$/ ) {
    $s->rebuild_ikiwiki;
  };

};

sub rebuild_git {
  my ( $s ) = @_;

  my $dirperm;
  my $immutablefileperm;
  my $fileperm;

  unless( getgrnam("git-" . $s->shortname ) ) {
    system( "/usr/sbin/addgroup", "--quiet", "--system", "git-" . $s->shortname ) and die "Could not create repo group!";
    system( "/usr/sbin/adduser", "--quiet", $s->owner->name, "git-" . $s->shortname ) and die "Could not add you to the repo group!";
    getpwnam("iki-" . $s->shortname) and system( "/usr/sbin/adduser", "--quiet", "iki-" . $s->shortname, "git-" . $s->shortname ) and die "Could not add ikiwiki user to git group!";
  };

  foreach( "git-daemon-export-ok", "packed-refs" ) {
    unless( -e $s->path . "/" . $_ ) {
      open( TOUCH, ">", $s->path . "/" . $_ ) or die "Could not touch $_ for repo: $!";
      close( TOUCH );
    };
  };

  unless( -d $s->path . "/incoming" ) {
    mkdir( $s->path . "/incoming" ) or die "Could not mkdir incoming: $!";
  };

  unless( -d $s->path . "/snowflakes" ) {
    mkdir( $s->path . "/snowflakes" ) or die "Could not mkdir snowflakes: $!";
  };

  unless( -l $s->path . "/packed-refs" ) {
    rename( $s->path . "/packed-refs", $s->path . "/snowflakes/packed-refs" ) or die "Could not rename packed-refs: $!";
    symlink( "snowflakes/packed-refs", $s->path . "/packed-refs" ) or die "Could not symlink packed-refs: $!";
  };

  unless( -l $s->path . "/HEAD" ) {
    rename( $s->path . "/HEAD", $s->path . "/refs/HEAD" ) or die "Could not rename HEAD: $!";
    symlink( "refs/HEAD", $s->path . "/HEAD" ) or die "Could not symlink HEAD: $!";
  };

  foreach( "info", "logs", "branches" ) {
    (-e $s->path . "/" . $_) or mkdir( $s->path . "/" . $_ ) or die "Could not mkdir $_ for repo: $!";
  };

  if      ( $s->config->core_sharedrepository eq   "0666" ) {
    $dirperm  = "2777";
    $immutablefileperm = "0644";
    $fileperm = "0666";
  } elsif ( $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    $dirperm  = "2775";
    $immutablefileperm = "0644";
    $fileperm = "0664";
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ ) {
    $dirperm  = "2770";
    $immutablefileperm = "0640";
    $fileperm = "0660";
  } elsif ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    $dirperm  = "2750";
    $immutablefileperm = "0640";
    $fileperm = "0640";
  } else {
    die $s->config->core_sharedrepository . "is an unhandled value!"
  };

  # FIXME: we should verify we are not breaking someone else's object hardlinks with these chmod or chown operations
  system( "/usr/bin/find " . $s->path . "/refs " . $s->path . "/info " . $s->path . "/branches " . $s->path . "/objects " . $s->path . "/logs " . $s->path . "/HEAD " . $s->path . "/incoming " . $s->path . "/snowflakes -type d -print0 | /usr/bin/xargs -0 /bin/chmod $dirperm" ) and die "Could not chmod shared git resources!";
  system( "/usr/bin/find " .                                                                       $s->path . "/objects " .                                                                    "-type f -print0 | /usr/bin/xargs -0 --no-run-if-empty /bin/chmod $immutablefileperm" ) and die "Could not chmod shared git resources!"; # most files are either immutable or replaced at link level
  system( "/usr/bin/find " . $s->path . "/refs " . $s->path . "/info " . $s->path . "/branches " .                          $s->path . "/logs " . $s->path . "/HEAD " . $s->path . "/incoming " . $s->path . "/snowflakes -type f -print0 | /usr/bin/xargs -0 /bin/chmod $fileperm" ) and die "Could not chmod shared git resources!"; # most files are either immutable or replaced at link level
  system( "/usr/bin/find " . $s->path . "/refs " . $s->path . "/info " . $s->path . "/branches " . $s->path . "/objects " . $s->path . "/logs " . $s->path . "/HEAD " . $s->path . "/incoming " . $s->path . "/snowflakes -print0 | /usr/bin/xargs -0 /bin/chgrp " . $s->group->name ) and die "Could not chgrp shared git resources!";

  $ENV{"GIT_DIR"} = $s->path;
    system( "/usr/bin/git", "config", "gitweb.owner", $s->owner->email->address ) and die "Could not git config gitweb.owner!";
    # packed-refs files aren't modifiable under our permission system and are poorly supported on old version of git anyway.
    system( "/usr/bin/git", "config", "gc.packrefs", "0" ) and die "Could not git config gc.packrefs!";
    # git-upload-pack will default to core.sharedrepository 'false', so we make sure the pinyconfig value is set here.
    system( "/usr/bin/git", "config", "core.sharedrepository", $s->config->core_sharedrepository ) and die "Could not git config core.sharedrepository!";
  delete $ENV{"GIT_DIR"};

  chown( 0, 0, $s->path ) or die "Could not change ownership of " . $s->path;
  chmod( 00755, $s->path ) or die "Could not change mode of " . $s->path;

  chown( 0, 0, $s->path . "/hooks" ) or die "Could not change ownership of " . $s->path . "/hooks";
  chmod( 00755, $s->path . "/hooks" ) or die "Could not change mode of " . $s->path . "/hooks";

  foreach( "config", "description", "git-daemon-export-ok" ) {
    chown( 0, 0, $s->path . "/" . $_ ) or die "Could not change ownership of $_!";
    chmod( 00644, $s->path . "/" . $_ ) or die "Could not change mode of $_!";
  };

  # FIXME: most of these are probably unnecessary to check due to limitations of bare repositories.
  foreach( "applypatch-msg", "pre-applypatch", "post-applypatch", "pre-commit", "prepare-commit-msg", "commit-msg", "post-commit", "pre-rebase", "post-checkout", "post-merge", "pre-push", "pre-receive", "update", "post-receive", "post-update", "push-to-checkout", "pre-auto-gc", "post-rewrite", "sendemail-validate", "fsmonitor-watchman" ) {
    my $link = $s->path . "/hooks/" . $_;
    while ( -l $link ) {
      $link = readlink( $link );
    };
    my ( $dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks ) = stat( $link ) or next;
    my $name  = getpwuid( $uid ) or die "Unable to find name  for uid" . $uid . "for hook " . $s->path . "/hooks/" . $_ . "\n";
    my $group = getgrgid( $gid ) or die "Unable to find group for gid" . $gid . "for hook " . $s->path . "/hooks/" . $_ . "\n";
    unless ( $name =~ /^(root|iki-$s->{shortname})/ ) {
      warn( "Security warning: $link is not owned by user root or iki-$s->{shortname}!\n" );
    };
    unless ( $group =~ /^(root|iki-$s->{shortname}|git-$s->{shortname})$/ ) {
      warn( "Security warning: $link is not owned by group root, iki-$s->{shortname}, or git-$s->{shortname}!\n" );
    };
  };

  $s->clear_config;
  $s->config->save;

};

sub rebuild_apache {

  my ( $s ) = @_;

  my $globaltemp = File::Temp->new( DIR => "/etc/apache2/piny/global/" ) or die "Could not create temporary file: $!";
  $globaltemp->unlink_on_destroy( 0 );
  print $globaltemp $s->apache_global_config;
  $globaltemp->close or die "Could not close new wikilist: $!";
  rename( $globaltemp->filename, "/etc/apache2/piny/global/" . $s->name . ".conf" ) or die "Could not rename apache config: $!";

  my $securetemp = File::Temp->new( DIR => "/etc/apache2/piny/secure/" ) or die "Could not create temporary file: $!";
  $securetemp->unlink_on_destroy( 0 );
  print $securetemp $s->apache_secure_config;
  $securetemp->close or die "Could not close new wikilist: $!";
  rename( $securetemp->filename, "/etc/apache2/piny/secure/" . $s->name . ".conf" ) or die "Could not rename apache config: $!";

  my $wwwtemp = File::Temp->new( DIR => "/etc/apache2/piny/www/" ) or die "Could not create temporary file: $!";
  $wwwtemp->unlink_on_destroy( 0 );
  print $wwwtemp $s->apache_www_config;
  $wwwtemp->close or die "Could not close new wikilist: $!";
  rename( $wwwtemp->filename, "/etc/apache2/piny/www/" . $s->name . ".conf" ) or die "Could not rename apache config: $!";

  if ( $s->config->piny_apachereload =~ /^(1|true)$/ ) {
    system( "/etc/init.d/apache2", "reload" ) and die "Could not reload apache config!";
  };
};

sub rebuild_ikiwiki {
  my ( $s ) = @_;

  chdir( $s->config->piny_gitpath ); # because git likes to chdir back to its original cwd for some dumb reason, which isn't guaranteed to work.

  unless( getpwnam("iki-" . $s->shortname ) ) {
    system( "/usr/sbin/adduser", "--quiet", "--system", "--group", "--gecos", $s->shortname, "iki-" . $s->shortname ) and die "Could not create ikiwiki user!";
    system( "/usr/sbin/adduser", "--quiet", "iki-" . $s->shortname, "git-" . $s->shortname ) and warn "Could not add ikiwiki user to the repo group!";
  };

  my $ikiuser = Piny::User::IkiWiki->new( "name" => "iki-" . $s->shortname );

  system( "/bin/chown", $ikiuser->name . "." . $ikiuser->name, $s->path . "/hooks" ) and warn "Could not change ownership of git hooks!";

  open( SETUP, ">", "/etc/ikiwiki/piny/" . $s->name . ".setup" ) or die "Could not open new ikiwiki setup file: $!";
  print SETUP $s->ikiwiki_setup;
  close( SETUP ) or die "Could not close new ikiwiki setup file: $!";
  chmod( 00644, "/etc/ikiwiki/piny/" . $s->name . ".setup" ) or die "Could not chmod ikiwiki setup file: $!";

  foreach( $s->ikiwiki_srcdir, $s->config->piny_ikiwikidestdir . $s->name, $s->ikiwiki_destdir, $s->secure_path ) {
    unless( -d $_ ) { mkdir( $_ ) };
    system( "/bin/chown", "-R", $ikiuser->name . ".", $_ ) and die "Could not change ownership of ikiwiki directories!";
  };

  chmod( 0700, $s->ikiwiki_srcdir ) or die "Could not chmod $s->ikiwiki_srcdir: $!";

  system( "/usr/bin/find " . $s->ikiwiki_srcdir . " -type d -name .ikiwiki -print0 | xargs -0 --no-run-if-empty rm -r") and die "Could not remove old Ikiwiki state dir!";

  unless( -d $s->ikiwiki_srcdir . ".git" ) {
    # do this after the chown -R because there are a lot of object hardlinks flying around that don't want to chown
    system( "/usr/bin/sudo", "-H", "-u", $ikiuser->name, "/usr/bin/git", "clone", "--quiet", $s->path, $s->ikiwiki_srcdir ) and die "Could not clone repo to ikiwiki srcdir!"; 
  };

  open( WIKILIST, ">", "/etc/ikiwiki/wikilist.d/" . $s->name ) or die "Could not create wikilist.d file: $!";
  print WIKILIST $ikiuser->name . " /etc/ikiwiki/piny/" . $s->name . ".setup\n";
  close( WIKILIST ) or die "Could not close wikilist.d file: $!";

  $s->rebuild_wikilist;

  system( "/usr/bin/sudo", "-H", "-u", $ikiuser->name, "/usr/bin/ikiwiki", "--setup", "/etc/ikiwiki/piny/" . $s->name . ".setup" ) and die "Could not do initial compile of ikiwiki!";
};

sub rebuild_wikilist {
  my $temp = File::Temp->new( DIR => "/etc/ikiwiki/wikilist.d/" ) or die "Could not create temporary file: $!";
  $temp->unlink_on_destroy( 0 );

  my $dh = IO::Dir->new( "/etc/ikiwiki/wikilist.d" ) or die "Could not open wikilist.d directory: $!";
  while ( defined ( my $entry = $dh->read ) ) {
    next if ( $entry !~ /.conf$/ );
    open( FILE, "<", "/etc/ikiwiki/wikilist.d/" . $entry ) or die "Could not open wikilist.d entry $entry: $!";
    print $temp <FILE>;
    close( FILE ) or die "Could not close wikilist.d entry $entry: $!";
  };

  $temp->close or die "Could not close new wikilist: $!";

  chmod( 00644, $temp->filename ) or die "Could not fix mode of new wikilist: $!";

  rename( $temp->filename, "/etc/ikiwiki/wikilist" ) or die "Could not rename over old wikilist: $!";
};

sub destroy {
  my ( $s ) = @_;

  $s->destroy_ikiwiki;
  $s->destroy_apache;
  $s->destroy_git;
};

sub destroy_git {
  my ( $s ) = @_;
  system( "rm", "-rf", $s->path );
  system( "delgroup", $s->group->name );
};

sub destroy_apache {
  my ( $s ) = @_;

  foreach(
    "/etc/apache2/piny/global/" . $s->name,
    "/etc/apache2/piny/global/" . $s->name . ".conf",
    "/etc/apache2/piny/secure/" . $s->name,
    "/etc/apache2/piny/secure/" . $s->name . ".conf",
    "/etc/apache2/piny/www/" . $s->name,
    "/etc/apache2/piny/www/" . $s->name . ".conf",
  ) {
    if ( -e $_ ) {
       unlink( $_ );
    };
  };

  if ( $s->config->piny_apachereload =~ /^(1|true)$/ ) {
    system( "/etc/init.d/apache2", "reload" ) and die "Could not reload apache config!";
  };
};

sub destroy_ikiwiki {
  my ( $s ) = @_;

  my $user = Piny::Environment->instance->user;

  foreach(
    "/etc/ikiwiki/wikilist.d/" . $s->name,
    "/etc/ikiwiki/wikilist.d/" . $s->name . ".conf"
  ) {
    if ( -e $_ ) {
       unlink( $_ );
       $s->rebuild_wikilist;
    };
  };

  foreach( $s->secure_path, $s->config->piny_ikiwikidestdir . $s->name, $s->config->piny_ikiwikisecurepath . "read/" . $s->name, $s->ikiwiki_destdir, $s->ikiwiki_srcdir, "/etc/ikiwiki/piny/" . $s->name . ".setup" ) {
    if ( -e $_ ) {
      system( "rm", "-rf", $_ );
    };
  };

  my $ikiuser = Piny::User::IkiWiki->new( "name" => "iki-" . $s->name );

  getpwnam( "iki-" . $s->shortname ) and system( "deluser", "--quiet", "--remove-home", "iki-" . $s->shortname );
  getgrnam( "iki-" . $s->shortname ) and system( "delgroup", "--quiet", "iki-" . $s->shortname );

};

# Triggers

sub _set_description {
  my ( $s, $new_description, $old_description ) = @_;

  open( my $fd, ">", $s->path . "/description" ) or die "Unable to open " . $s->path . "/description for writing: $!";
  print $fd $new_description;
  close( $fd ) or die "Error when closing " . $s->path . "/description: $!";
};

sub _change_owner {
  my ( $s, $new_owner, $old_owner ) = @_;

  find( { wanted => sub { chown( $new_owner->uid, -1, $_ ) or die "Couldn't chown $_: $!"; }, no_chdir => 1 }, $s->path . "/objects" );

  $s->clear_ikiwiki_setup;
};

# Class methods

sub all_repos {
  my ( $class, $dir ) = @_;

  my $config = Piny::Config->new( );

  $dir = $config->piny_gitpath unless defined $dir;

  my @ret;

  find( { wanted => sub { if ( /^[^.].*\.git$/ ) { $File::Find::prune = 1; push( @ret, $File::Find::name ); }; } }, $dir );

  @ret = map { s/^\Q$dir\E\/?//; s/\.git$//; $class->new( name => $_ ); } @ret;

  return @ret;
};

# Only sets up needed data; ->rebuild can take care of the rest.
sub create {
  my ( $class, $name, $description, $remote ) = @_;

  if ( not defined $remote) {
    $remote = Piny::Config->new->piny_template;
  };

  my $user = Piny::Environment->instance->user;

  find_type_constraint( "Reponame" )->assert_valid( $name );
  find_type_constraint( "SimpleText" )->assert_valid( $description );

  my $repo = $class->new( "name" => $name );

  mkdir( $repo->path ) or die "The repo $name appears to already exist! ($!)";

  chdir( $repo->config->piny_gitpath ); # so git-clone can do the work of resolving local repo names for us
  if ( defined $remote ) {
    system( "/bin/chown", $user->name, $repo->path ) and die "Could not change ownership of repo path!"; # permissions are overridden later
    if ( system( "/usr/bin/sudo", "-H", "-u", $user->name, "/usr/bin/git", "clone", "--bare", "--no-hardlinks", "--quiet", $remote, $repo->path ) ) { # sudo -u $user needed in order to test user readability of whatever they want to clone. the mode o+rx should probably be masked off if we want private repos to stay private.
      system( "/bin/rm", "-r", $repo->path ) and die "Failed to clean up after failed clone operation!";
      die( "Could not clone repository!\n" );
    } else {
      my $valid = 0;
      eval {
        Piny::Repo->new( $remote )->owner;
        $valid = 1;
      };
      if( $valid ) {
        copy( $remote . ".git/config", $repo->path . "/config" ) or die "Config copy failed: $!";
      } else {
        print( "$remote does not appear to be a piny reponame, continuing without copying config file...\n");
      }
    };
  } else {
    system( "/usr/bin/git", "init", "--bare", "--quiet", $repo->path ) and die "Could not initialize git repo!";
  };

  system( "/bin/chown", "-R", $user->name, $repo->path . "/objects" ) and die "Could not change ownership of $_ for repo: $!";

  $repo->description( $description );

  $repo = $class->new( $name );

  return $repo;
};

# Builder methods

# If constructed with just one argument, then treat it as a repo name.
around BUILDARGS => sub {
  my ( $orig, $class ) = ( shift, shift );

  if ( @_ == 1 && ! ref $_[0] ) {
    return $class->$orig( name => $_[0] );
  } else {
    return $class->$orig( @_ );
  };
};

my @letter1 =             ( 'a' .. 'z' );           # First digit is base 26.
my @lettern = ( '0' .. '9', 'a' .. 'z', '-', '_' ); # Remaining digits are base 38.

sub _build_shortname {
  my ( $s ) = @_;

  my $str = $s->name;

  # Short inputs don't need to be hashed.
  return $str if length $str <= 28;

  # We have to use Math::GMP here instead of Math::BigInt or pragma bignum
  # support because we actually require infinite precision integer arithmetic,
  # and bignum tends to only give us 40 decimal digits of precision during
  # division because the Perl maintainers are jackasses.
  my $n = Math::GMP->new( "0x" . sha256_hex( $str ) );

  my ( $m );

  # First digit.
  ( $n, $m ) = $n->bdiv( scalar @letter1 );
  my $out = $letter1[$m];

  # Remaining digits.
  while ( length $out < 28 and $n > 0 ) {
    ( $n, $m ) = $n->bdiv( scalar @lettern );
    $out .= $lettern[$m];
  };

  return $out;
};

sub _build_group {
  my ( $s ) = @_;

  return Piny::Group->new( name => "git-" . $s->shortname );
};

sub _build_path {
  my ( $s ) = @_;

  my $config = Piny::Config->new( ); # New, otherwise we'd wind up recursing. It's considered global anyway.

  return $config->piny_gitpath . "/" . $s->name . ".git";
};

sub _build_description {
  my ( $s ) = @_;

  open( my $d, "<", $s->path . "/description" ) or die "Unable to open " . $s->path . "/description: $!";
  my $desc;
  {
    local $/ = undef;
    $desc = <$d>;
  };
  close( $d );

  return $desc;
};

sub _build_repostat {
  my ( $s ) = @_;

  my @res = stat( $s->path . "/objects" );
  die "stat( " . $s->path . "/objects ) failed: $!" unless @res;
  return \@res;
};

sub _build_owner {
  my ( $s ) = @_;

  my ( $uid ) = $s->repostat->[4];

  return Piny::User->new( uid => $uid );
};

sub _build_globally_readable {
  my ( $s ) = @_;

  return ( $s->repostat->[2] & 0444 ) == 0444;
};

sub _build_globally_writable {
  my ( $s ) = @_;

  return ( $s->repostat->[2] & 0111 ) == 0111;
};

sub _build_cgit_url {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666" ) {
    return $s->config->piny_ikiwikisecureurl . "cgit/" . $s->name;
  } elsif ( $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    return $s->config->piny_ikiwikisecureurl . "cgit/" . $s->name;
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ ) {
    return $s->config->piny_ikiwikisecureurl . "auth/cgit/" . $s->name;
  } elsif ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    return $s->config->piny_ikiwikisecureurl . "auth/cgit/" . $s->name;
  } else {
    die ( $s->config->core_sharedrepository . "is an unhandled value!" );
  };
}

sub _build_ikiwiki_setup {
  my ( $s ) = @_;

  my ( $package, $config ) = readSetup( "/usr/share/libpiny/ikiwiki.setup" );

  $config->{"wikiname"}   = $s->name;
  $config->{"adminemail"} = $s->owner->email->address;
  $config->{"srcdir"}     = $s->ikiwiki_srcdir;
  $config->{"destdir"}    = $s->ikiwiki_destdir;
  $config->{"url"}        = $s->ikiwiki_url;
  $config->{"historyurl"} = $s->ikiwiki_historyurl;
  $config->{"diffurl"}    = $s->ikiwiki_diffurl;

  if ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    warn( "warning: ikiwiki web-based editing interface relies on group writability.\n" );
    $config->{"wrappers"} =
      [ { "wrapper"       => $s->path . "/hooks/post-update"
        , "wrappergroup"  => $s->group->name
        , "wrappermode"   => "06755"
        , "notify"        => 0
        }
      ];
  } else {
    $config->{"cgiurl"}     = $s->ikiwiki_cgiurl;
    $config->{"wrappers"} =
      [ { "wrapper"       => $s->ikiwiki_cgipath
        , "wrappergroup"  => $s->group->name
        , "wrappermode"   => "06755"
        , "cgi"           => 1
        }
      , { "wrapper"       => $s->path . "/hooks/post-update"
        , "wrappergroup"  => $s->group->name
        , "wrappermode"   => "06755"
        , "notify"        => 0
        }
      ];
    # Search relies on cgiurl
    push(@{$config->{add_plugins}}, qw{
      search
    });
  };

  if ( -e "/etc/ikiwiki/piny.setup.pl" ) {
    system( "perl", "-C", "/etc/ikiwiki/piny.setup.pl" ) && die ( 'Failed to compile global ikiwiki overrides file!' );
    undef $@;
    eval {
      package TEMP;
      use Piny;
      $TEMP::repo = $s;
      $TEMP::conf = $config;
      no strict 'vars';
      do "/etc/ikiwiki/piny.setup.pl";
    };
  };

  if ( -e "/etc/ikiwiki/piny/" . $s->name . ".setup.pl" ) {
    system( "perl", "-C", "/etc/ikiwiki/piny/" . $s->name . ".setup.pl" ) && die ( 'Failed to compile ikiwiki overrides file!' );
    undef $@;
    eval {
      package TEMP;
      use Piny;
      $TEMP::repo = $s;
      $TEMP::conf = $config;
      no strict 'vars';
      do "/etc/ikiwiki/piny/" . $s->name . ".setup.pl";
    };
  };

  return writeSetup( $package, $config );
};

sub _build_ikiwiki_destdir {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666" ) {
    return ( $s->config->piny_ikiwikidestdir . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    return ( $s->config->piny_ikiwikidestdir . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ ) {
    return ( $s->config->piny_ikiwikisecurepath . "read/" . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    return ( $s->config->piny_ikiwikisecurepath . "read/" . $s->name );
  } else {
    die ( $s->config->core_sharedrepository . "is an unhandled value!" );
  };
};

sub _build_ikiwiki_srcdir {
  my ( $s ) = @_;

  return $s->config->piny_ikiwikisrcdir . "/" . $s->name;
};

sub _build_ikiwiki_url {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666" ) {
    return ( $s->config->piny_ikiwikiurl . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    return ( $s->config->piny_ikiwikiurl . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ ) {
    return ( $s->config->piny_ikiwikisecureurl . "read/" . $s->name );
  } elsif ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    return ( $s->config->piny_ikiwikisecureurl . "read/" . $s->name );
  } else {
    die ( $s->config->core_sharedrepository . "is an unhandled value!" );
  };
};

sub _build_ikiwiki_cgiurl {
  my ( $s ) = @_;

  return $s->config->piny_ikiwikisecureurl . "write/" . $s->name . "/ikiwiki.cgi";
};

sub _build_secure_path {
  my ( $s ) = @_;

  return $s->config->piny_ikiwikisecurepath . "write/" . $s->name;
};

sub _build_ikiwiki_cgipath {
  my ( $s ) = @_;

  return $s->secure_path . "/ikiwiki.cgi";
};

sub _build_ikiwiki_historyurl {
  my ( $s ) = @_;

  return $s->cgit_url . "/log/[[file]]";
};

sub _build_ikiwiki_diffurl {
  my ( $s ) = @_;

  return $s->cgit_url . "/commit/?id=[[sha1_commit]]";
};

sub _build_apache_global_config {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666" ) {
    return (
      "<Directory " . $s->secure_path .     ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"Valid Piny user needed.\"\n" .                            "  Require valid-user\n" .             "  </Directory>\n"
    );
  } elsif ( $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    return (
      "<Directory " . $s->secure_path .     ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"User with access to " . $s->name . " repository needed.\"\n  Require unix-group " . $s->group->name . "\n</Directory>\n"
    );
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ ) {
    return (
      "<Directory " . $s->secure_path .     ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"User with access to " . $s->name . " repository needed.\"\n  Require unix-group " . $s->group->name . "\n</Directory>\n" .
      "<Directory " . $s->ikiwiki_destdir . ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"User with access to " . $s->name . " repository needed.\"\n  Require unix-group " . $s->group->name . "\n</Directory>\n"
    );
  } elsif ( $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    return (
      "<Directory " . $s->secure_path .     ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"Owner of " .            $s->name . " repository needed.\"\n  Require user " .  $s->owner->name . "\n</Directory>\n" .
      "<Directory " . $s->ikiwiki_destdir . ">\n  AuthBasicProvider PAM\n  AuthPAMService other\n  AuthBasicAuthoritative off\n  AuthType Basic\n  AuthName \"User with access to " . $s->name . " repository needed.\"\n  Require unix-group " . $s->group->name . "\n</Directory>\n"
    );
  } else {
    die ( $s->config->core_sharedrepository . " is an unhandled value!" );
  };
};

sub _build_apache_secure_config {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666"                or $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
      return (                                                              "Redirect /read/" . $s->name . " " . $s->ikiwiki_url . "\n" );
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ or $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) { 
    if ( $s->config->piny_ikiwiki =~ /^(1|true)$/ ) {
      return ( "Redirect /cgit/" . $s->name . " /auth/cgit/" . $s->name . "\n" );
    } elsif ( $s->config->piny_ikiwiki =~ /^(0|false)$/ ) {
      return ( "Redirect /cgit/" . $s->name . " /auth/cgit/" . $s->name . "\nRedirect /read/" . $s->name . " /cgit/" . $s->name . "/tree\n" );         
    } else {
      die ( $s->config->piny_ikiwiki . " is an unhandled value!" );
    };
  } else {
    die ( $s->config->core_sharedrepository . " is an unhandled value!" );
  };
};

sub _build_apache_www_config {
  my ( $s ) = @_;

  if      ( $s->config->core_sharedrepository eq   "0666"                or $s->config->core_sharedrepository =~ /^(0664|all|everybody|world)$/ ) {
    if ( $s->config->piny_ikiwiki =~ /^(1|true)$/ ) {
      return ( "\n" );
    } elsif ( $s->config->piny_ikiwiki =~ /^(0|false)$/ ) {
      return ( "Redirect /" . $s->name . " " . $s->cgit_url . "/tree\n" );         
    } else {
      die ( $s->config->piny_ikiwiki . " is an unhandled value!" );
    };
  } elsif ( $s->config->core_sharedrepository =~ /^(0660|true|1|group)$/ or $s->config->core_sharedrepository =~ /^(0640|false|0)$/ ) {
    return (   "Redirect /" . $s->name . " " . $s->ikiwiki_url . "\n" );
  } else {
    die ( $s->config->core_sharedrepository . " is an unhandled value!" );
  };
};

sub _build_config {
  my ( $s ) = @_;

  return Piny::Config->new( confpath => $s->path . "/config" );
};

# Moose boilerplate

__PACKAGE__->meta->make_immutable;

1;
