# Copyright © 2010 Julian Blake Kongslie # Licensed under the BSD 3-clause license. package Piny::Repo; use Moose; use Moose::Util::TypeConstraints; use File::Find qw( find ); use IkiWiki::FakeSetup qw( readSetup writeSetup ); use Piny::Group; use Piny::User; # 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 => 'rw' , isa => 'Reponame' , trigger => \&_rename_repo , required => 1 ); 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 '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_config' => ( is => 'ro' , isa => 'Str' , lazy_build => 1 , init_arg => undef ); # 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 ); }; # Triggers sub _rename_repo { my ( $s, $new_name, $old_name ) = @_; return unless defined $old_name; my $olddir = "/srv/git/$old_name.git"; my $newdir = "/srv/git/$new_name.git"; rename( $olddir, $newdir ) or die "Couldn't rename $olddir to $newdir: $!"; warn "XXX Not renaming all the ikiwiki stuff!"; $s->clear_path; $s->clear_ikiwiki_setup; $s->clear_ikiwiki_destdir; $s->clear_ikiwiki_srcdir; $s->clear_ikiwiki_url; $s->clear_ikiwiki_cgiurl; $s->clear_ikiwiki_historyurl; $s->clear_ikiwiki_cgipath; $s->clear_secure_path; $s->clear_apache_config; }; sub _set_description { my ( $s, $new_description, $old_description ) = @_; return unless defined $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 ) = @_; return unless defined $old_owner; find( { wanted => sub { chown( $new_owner->uid, -1, $_ ) or die "Couldn't chown $_: $!"; }, no_chdir => 1 }, $s->path ); $s->clear_ikiwiki_setup; }; # Class methods sub all_repos { my ( $class, $dir ) = @_; $dir = "/srv/git" 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; }; # 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( @_ ); }; }; sub _build_group { my ( $s ) = @_; return Piny::Group->new( name => "git-" . $s->name ); }; sub _build_path { my ( $s ) = @_; my $dir = "/srv/git/" . $s->name . ".git"; if ( -d $dir ) { return $dir; } else { die "Expected repo $dir does not exist!"; }; }; 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 ); die "stat( " . $s->path . " ) 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_ikiwiki_setup { my ( $s ) = @_; my ( $package, $config ) = readSetup( "/usr/share/libpiny/ikiwiki.setup" ); $config->{"wikiname"} = $s->name; $config->{"adminemail"} = $s->owner->email; $config->{"srcdir"} = $s->ikiwiki_srcdir; $config->{"destdir"} = $s->ikiwiki_destdir; $config->{"url"} = $s->ikiwiki_url; $config->{"cgiurl"} = $s->ikiwiki_cgiurl; $config->{"historyurl"} = $s->ikiwiki_historyurl; $config->{"diffurl"} = $s->ikiwiki_diffurl; $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 } ]; return writeSetup( $package, $config ); }; sub _build_ikiwiki_destdir { my ( $s ) = @_; my $dir = "/srv/www/piny.be/" . $s->name; if ( -d $dir ) { return $dir; } else { die "Expected destdir $dir does not exist!"; }; }; sub _build_ikiwiki_srcdir { my ( $s ) = @_; my $dir = "/srv/ikiwiki/" . $s->name; if ( -d $dir ) { return $dir; } else { die "Expected srcdir $dir does not exist!"; }; }; sub _build_ikiwiki_url { my ( $s ) = @_; return "http://piny.be/" . $s->name; }; sub _build_ikiwiki_cgiurl { my ( $s ) = @_; return "https://secure.piny.be/repos/" . $s->name . "/ikiwiki.cgi"; }; sub _build_secure_path { my ( $s ) = @_; return "/srv/www/secure.piny.be/repos/" . $s->name; }; sub _build_ikiwiki_cgipath { my ( $s ) = @_; return $s->secure_path . "/ikiwiki.cgi"; }; sub _build_ikiwiki_historyurl { my ( $s ) = @_; return "https://secure.piny.be/cgit/" . $s->name . "/log/[[file]]"; }; sub _build_ikiwiki_diffurl { my ( $s ) = @_; return "https://secure.piny.be/cgit/" . $s->name . "/diff/?id=[[sha1_commit]]"; }; sub _build_apache_config { my ( $s ) = @_; return "secure_path . ">\n AuthPAM_Enabled on\n AuthGROUP_Enabled on\n AuthPAM_FallThrough off\n AuthBasicAuthoritative off\n AuthType Basic\n AuthName \"User access to " . $s->name . " repository needed.\"\n Require group " . $s->group->name . "\n\n"; }; # Moose boilerplate __PACKAGE__->meta->make_immutable; 1;