# $Id: CVS.pm,v 1.35 2003/12/07 00:00:37 barbee Exp $ =head1 NAME Apache::CVS - method handler provide a web interface to CVS repositories =head1 SYNOPSIS SetHandler perl-script PerlHandler Apache::CVS::HTML PerlSetVar CVSRoots cvs1=>/usr/local/CVS =head1 DESCRIPTION C is a method handler that provide a web interface to CVS repositories. Please see L<"CONFIGURATION"> to see what configuration options are available. To get started you'll at least need to set CVSRoots to your local CVS Root directory. C is does not output the contents of your CVS repository on its own. Rather, it is meant to be subclassed. A subclass that yields HTML output is provided with C. Please see L<"SUBCLASSING"> for details on creating your own subclass. =head1 CONFIGURATION Please see C for some extra configuration parameters specific to HTML display. =item CVSRoots Location of the CVS Roots. Set this like you would hash. This variable is required. PerlSetVar CVSRoots cvs1=>/path/to/cvsroot1,cvs2=>/path/to/cvsroot2 =item RCSExtension File extension of RCS files. Defaults to ',v'. PerlSetVar RCSExtension ,yourextension =item WorkingDirectory A directory to keep temporary files. Defaults to /var/tmp. Apache::CVS will try to clean up after itself and message to the error log if it couldn't. PerlSetVar WorkingDirectory /usr/tmp =item BinaryDirectory The directory of the rcs binaries. Defaults to /usr/bin. PerlSetVar BinaryDirectory /usr/local/bin =item DiffStyles The different types of diffs you want to provide to users. The values will be passed to cvs diff as arguments. If not set users will see a unified diff. PerlSetVar DiffStyles unified=>ua,side-by-side=>ya =item DefaultDiffStyle The default diff style. The value must be a valid predefined DiffStyle. If not set or set incorrectly Apache::CVS will default to the first DiffStyle. PerlSetVar DefaultDiffStyle unified =cut package Apache::CVS; use strict; use Apache::URI(); use Apache::CVS::RcsConfig(); use Apache::CVS::PlainFile(); use Apache::CVS::Directory(); use Apache::CVS::File(); use Apache::CVS::Revision(); use Apache::CVS::Diff(); eval "use Apache::CVS::Graph();"; if ($@) { $Apache::CVS::Graph = 0; } else { $Apache::CVS::Graph = 1; } $Apache::CVS::VERSION = '0.10'; =head1 SUBCLASSING Override any or all of the following to customize the display. Some of these method will take a $uri_base as an argument. It is the URI for the current item that is being displayed. For example, if a directory is being displayed, the base URI is the URI to that directory. If a revision is being displayed, the base URI is the URI to that file. =over 4 =item $self->print_http_header() Prints the HTTP headers. If you override this you should set the http_headers_sent flag with $self->http_headers_sent(1). =cut sub print_http_header { my $self = shift; return if $self->http_headers_sent(); $self->request()->content_type($self->content_type()); $self->request()->send_http_header; $self->http_headers_sent(1); } =item print_error This method takes a string that contains the error. =cut sub print_error { return; } =item print_page_header No arguments. If you override this you should set the page_headers_sent flag with $self->page_headers_sent(). =cut sub print_page_header { return; } =item print_page_footer No arguments. =cut sub print_page_footer { return; } =item print_root_list_header No arguments. =cut sub print_root_list_header { return; } =item print_root A root as a string, defined by your CVSRoots configuration. =cut sub print_root { return; } =item print_root_list_footer No arguments. =cut sub print_root_list_footer { return; } =item print_directory_list_header Takes a base uri, the sort criterion, and the sort direction (1 for ascending). Overriding method should check B to see if sorting controls should be provided. =cut sub print_directory_list_header { return; } =item print_directory Takes a base uri, an Apache::CVS::Directory object, and a row number. =cut sub print_directory { return; } =item print_file Takes a base uri, an Apache::CVS::File object, and a row number. =cut sub print_file { return; } =item sort_files Takes a reference to a list of Apache::CVS::Files, a criterion, and a sort direction (1 for ascending). This is called before printing. =cut sub sort_files { return $_[1] } =item print_plain_file Takes a base uri, an Apache::CVS::PlainFile object, and a row number. =cut sub print_plain_file { return; } =item print_directory_list_footer No arguments. =cut sub print_directory_list_footer { return; } =item print_file_list_header Takes a base uri, the sort criterion, and the sort direction (1 for ascending). Overriding method should check B to see if sorting controls should be provided. =cut sub print_file_list_header { return; } =item print_revision Takes a base uri, an Apache::CVS::Revision object, a row number and the revision number of a revision that has been selected for diffing, if such exists. =cut sub print_revision { return; } =item sort_revisions Takes a reference to a list of Apache::CVS::Revisions and a sort criterion. This is called before sorting. =cut sub sort_revisions { return $_[1]; } =item print_file_list_footer No arguments. =cut sub print_file_list_footer { return; } =item print_text_revision Takes the content of the revision as a string. =cut sub print_text_revision { return; } =item print_diff Takes an Apache::CVS::Diff object and a base uri. =cut sub print_diff { return; } =item print_graph Takes a base uri and an Apache::CVS::Graph object. Only avaiable if built with --graph passed to Makefile.PL. =cut sub print_graph { return; } =back =head1 OBJECT METHODS Here are some other methods that might be useful. =over 4 =cut sub _get_roots { my $request = shift; my %cvsroots = split /\s*(?:=>|,)\s*/, $request->dir_config('CVSRoots'); return \%cvsroots; } sub _get_rcs_config { my $request = shift; return Apache::CVS::RcsConfig->new($request->dir_config('RCSExtension'), $request->dir_config('WorkingDirectory'), $request->dir_config('BinaryDirectory')); } sub _get_diff_styles { my $request = shift; my %styles = split /\s*(?:=>|,)\s*/, $request->dir_config('DiffStyles'); # default style $styles{unified} = 'ua' unless scalar keys %styles; return \%styles; } sub _get_default_diff_style { my $request = shift; my $styles = shift; my $default = $request->dir_config('DefaultDiffStyle'); # if directive not set or style is not set up then grab a style # from the list unless ($default && exists($styles->{$default})) { if (scalar keys %{ $styles} == 1) { # if there is only one style we can stop here $default = (keys %{ $styles})[0]; } else { # otherwise parse the DiffStyles directive for the first style $request->dir_config('DiffStyles') =~ /([^\s=,]*)/; $default = $1; } } # we absolutely must have a valid default, so we should check our # work unless (exists(${$styles}{$default})) { # alright, so we screwed up somewhere, fallback to the first # style $default = (keys %{ $styles})[0]; } return $default; } sub new { my $proto = shift; my $class = ref($proto) || $proto; my $request = shift; my $self; $self->{request} = $request; $self->{rcs_config} = _get_rcs_config($self->{request}); $self->{roots} = _get_roots($self->{request}); $self->{content_type} = 'text/html'; $self->{http_headers_sent} = 0; $self->{page_headers_sent} = 0; $self->{current_root} = undef; $self->{path} = undef; $self->{diff_styles} = _get_diff_styles($self->{request}); $self->{default_diff_style} = _get_default_diff_style($self->{request}, $self->{diff_styles}); $self->{file_sorting_available} = 0; $self->{revision_sorting_available} = 0; bless ($self, $class); return $self; } =item $self->request() Returns the Apache request object. =cut sub request { my $self = shift; $self->{request} = shift if scalar @_; return $self->{request}; } =item $self->rcs_config() Returns the C object that holds the Rcs configuration. =cut sub rcs_config { my $self = shift; return $self->{rcs_config}; } =item $self->content_type() Set or get the content_type. =cut sub content_type { my $self = shift; $self->{content_type} = shift if scalar @_; return $self->{content_type}; } =item $self->http_headers_sent() Set or get this flag which indicates if the HTTP have been sent or not. =cut sub http_headers_sent { my $self = shift; $self->{http_headers_sent} = shift if scalar @_; return $self->{http_headers_sent}; } =item $self->page_headers_sent() Set or get this flag which indicates if the page headers have been sent or not. =cut sub page_headers_sent { my $self = shift; $self->{page_headers_sent} = shift if scalar @_; return $self->{page_headers_sent}; } =item $self->path() Set or get the path of to the file or directory requested. =cut sub path { my $self = shift; if (scalar @_) { $self->{path} = shift; my $real_file_path = $self->{path} . $self->rcs_config()->extension(); unless (-d $self->{path} || -r $real_file_path) { die "File or directory ($self->{path} or $real_file_path) does " . "not exist."; } } return $self->{path}; } =item $self->current_root() Set or get the CVS Root of the files being requested. =cut sub current_root { my $self = shift; $self->{current_root} = shift if scalar @_; return $self->{current_root}; } =item $self->roots() Returns the configured CVS Roots as a hash references. =cut sub roots { my $self = shift; return $self->{roots}; } =item $self->diff_styles() Returns the different styles of diff that will be available. =cut sub diff_styles { my $self = shift; $self->{diff_styles} = shift if scalar @_; return $self->{diff_styles}; } =item $self->default_diff_style() Returns the default diff styles. =cut sub default_diff_style { my $self = shift; $self->{default_diff_style} = shift if scalar @_; return $self->{default_diff_style}; } =item $self->current_root_path() Returns the path of the CVS Root of the files being requested. This is equivalent to $self->roots()->{$self->current_root()}. =cut sub current_root_path { my $self = shift; return $self->roots()->{$self->current_root()}; } =item $self->file_sorting_available() Returns true if file sorting (in a directory) is implemented. Subclasses must set this to true or false where as necessary. =cut sub file_sorting_available { my $self = shift; $self->{file_sorting_available} = shift if scalar @_; return $self->{file_sorting_available}; } =item $self->revision_sorting_available() Returns true if revision sorting (in a file) is implemented. Subclasses must set this to true or false where as necessary. =cut sub revision_sorting_available { my $self = shift; $self->{revision_sorting_available} = shift if scalar @_; return $self->{revision_sorting_available}; } =back =cut sub handle_root { my $self = shift; $self->print_root($_) foreach ( keys %{ $self->roots()} ); } sub handle_directory { my $self = shift; my $row_counter = 0; my ($uri_base, $sort_criterion, $sort_direction) = @_; $self->print_directory_list_header($uri_base, $sort_criterion, $sort_direction); my $directory = Apache::CVS::Directory->new($self->path(), $self->rcs_config()); $directory->load(); foreach ( @{ $directory->directories() } ) { $self->print_directory($uri_base, $_, $row_counter); $row_counter++; } my $sorted_files = $directory->files(); if ($self->file_sorting_available()) { $sorted_files = $self->sort_files($directory->files(), $sort_criterion, $sort_direction); } foreach ( @{ $sorted_files } ) { $self->print_file($uri_base, $_, $row_counter); $row_counter++; } foreach ( @{ $directory->plain_files() } ) { $self->print_plain_file($_); $row_counter++; } $self->print_directory_list_footer(); } sub handle_file { my $self = shift; my ($uri_base, $diff_revision, $sort_criterion, $sort_direction) = @_; my $file = Apache::CVS::File->new($self->path(), $self->rcs_config()); my $row_counter = 0; $uri_base .= $file->name(); $self->print_file_list_header($uri_base, $sort_criterion, $sort_direction); # if sorting available, go with new behavior if ($self->revision_sorting_available()) { my $sorted_revisions = $self->sort_revisions($file->revisions(), $sort_criterion, $sort_direction); foreach ( @{ $sorted_revisions }) { $self->print_revision($uri_base, $_, $row_counter, $diff_revision); $row_counter++; } # otherwise, just use old behavior where we iterate through revisions } else { while ( my $revision = $file->revision('prev') ) { $self->print_revision($uri_base, $revision, $row_counter, $diff_revision); $row_counter++; } } $self->print_file_list_footer(); } sub handle_revision { my $self = shift; my ($uri_base, $revision_num) = @_; my $file = Apache::CVS::File->new($self->path(), $self->rcs_config()); my $revision = $file->revision($revision_num); eval { if ($revision->is_binary()) { my $subrequest = $self->request()->lookup_file($revision->co_file()); $self->content_type($subrequest->content_type); $self->print_http_header(); $self->request()->send_fd($revision->filehandle()); close $revision->filehandle(); } else { $self->print_http_header(); $self->print_page_header(); $self->print_text_revision($revision->content()); } }; if ($@) { $self->request()->log_error($@); $self->print_error("Unable to get revision.\n$@"); return; } } sub handle_diff { my $self = shift; my ($source_version, $target_version, $diff_style, $uri_base) = @_; my $file = Apache::CVS::File->new($self->path(), $self->rcs_config()); my $source = $file->revision($source_version); my $target = $file->revision($target_version); $diff_style ||= $self->default_diff_style(); my $diff = Apache::CVS::Diff->new($source, $target, $self->diff_styles()->{$diff_style}); $self->print_diff($diff, $uri_base . $file->name()); } sub handle_graph { return unless $Apache::CVS::Graph; my $self = shift; my $uri_base = shift; my $file = Apache::CVS::File->new($self->path(), $self->rcs_config()); my $graph = Apache::CVS::Graph->new($file); $self->print_graph($uri_base, $file->name(), $graph); } sub handler_internal { my $self = shift; my $path_info = $self->request()->path_info; my $is_real_root = 1 unless ( $path_info and $path_info ne '/' ); if ( $is_real_root ) { $self->print_http_header(); $self->print_page_header(); $self->handle_root(); return; } # strip off the cvs root id from the front $path_info =~ s#/([^/]+)/?##; $self->current_root($1); # determine current path my $is_cvsroot; unless ( $path_info and $path_info ne '/' ) { $self->path($self->current_root_path()); $is_cvsroot = 1; } else { $self->path($self->current_root_path() . q(/) . $path_info); } my %query = $self->request()->args; my $is_revision = exists $query{'r'}; my $uri_base = $self->request()->parsed_uri->rpath() . q(/) . $self->current_root() . q(/) . $path_info; if ( -d $self->path() ) { $self->print_http_header(); $self->print_page_header(); $uri_base .= q(/) unless $uri_base =~ /\/$/; $self->handle_directory($uri_base, $query{'o'}, $query{'asc'}); } else { $uri_base =~ s/[^\/]*$//; my %query = $self->request()->args; if ( $query{'ds'} && $query{'dt'} ) { $self->print_http_header(); $self->print_page_header(); $self->handle_diff($query{'ds'}, $query{'dt'}, $query{'dy'}, $uri_base); } elsif ( $is_revision ) { $self->handle_revision($uri_base, $query{'r'}); } elsif ( $Apache::CVS::Graph and exists($query{'g'}) ) { $self->print_http_header(); $self->print_page_header(); $self->handle_graph($uri_base, $query{'r'}); } else { $self->print_http_header(); $self->print_page_header(); $self->handle_file($uri_base, $query{'ds'}, $query{'o'}, $query{'asc'}); } } } sub handler($$) { my ($self, $request) = @_; delete $ENV{'PATH'}; $self = $self->new($request) unless ref $self; eval { $self->handler_internal(); }; if ($@) { $self->request()->log_error($@); $self->print_error($@); } } =head1 SEE ALSO L, L =head1 AUTHOR John Barbee > =head1 COPYRIGHT Copyright 2001-2002 John Barbee This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1;