diff options
Diffstat (limited to 'gitweb/gitweb.perl')
| -rwxr-xr-x | gitweb/gitweb.perl | 1776 | 
1 files changed, 1231 insertions, 545 deletions
| diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index a85e2f6319..70a576a626 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -7,6 +7,7 @@  #  # This program is licensed under the GPLv2 +use 5.008;  use strict;  use warnings;  use CGI qw(:standard :escapeHTML -nosticky); @@ -16,12 +17,10 @@ use Encode;  use Fcntl ':mode';  use File::Find qw();  use File::Basename qw(basename); +use Time::HiRes qw(gettimeofday tv_interval);  binmode STDOUT, ':utf8'; -our $t0; -if (eval { require Time::HiRes; 1; }) { -	$t0 = [Time::HiRes::gettimeofday()]; -} +our $t0 = [ gettimeofday() ];  our $number_of_git_cmds = 0;  BEGIN { @@ -116,6 +115,14 @@ our $projects_list = "++GITWEB_LIST++";  # the width (in characters) of the projects list "Description" column  our $projects_list_description_width = 25; +# group projects by category on the projects list +# (enabled if this variable evaluates to true) +our $projects_list_group_categories = 0; + +# default category if none specified +# (leave the empty string for no category) +our $project_list_default_category = ""; +  # default order of projects list  # valid values are none, project, descr, owner, and age  our $default_projects_order = "project"; @@ -165,6 +172,12 @@ our @diff_opts = ('-M'); # taken from git_commit  # the gitweb domain.  our $prevent_xss = 0; +# Path to the highlight executable to use (must be the one from +# http://www.andre-simon.de due to assumptions about parameters and output). +# Useful if highlight is not installed on your webserver's PATH. +# [Default: highlight] +our $highlight_bin = "++HIGHLIGHT_BIN++"; +  # information about snapshot formats that gitweb is capable of serving  our %known_snapshot_formats = (  	# name => { @@ -181,7 +194,7 @@ our %known_snapshot_formats = (  		'type' => 'application/x-gzip',  		'suffix' => '.tar.gz',  		'format' => 'tar', -		'compressor' => ['gzip']}, +		'compressor' => ['gzip', '-n']},  	'tbz2' => {  		'display' => 'tar.bz2', @@ -245,13 +258,14 @@ our %highlight_ext = (  	# main extensions, defining name of syntax;  	# see files in /usr/share/highlight/langDefs/ directory  	map { $_ => $_ } -		qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl), +		qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),  	# alternate extensions, see /etc/highlight/filetypes.conf  	'h' => 'c', +	map { $_ => 'sh'  } qw(bash zsh ksh),  	map { $_ => 'cpp' } qw(cxx c++ cc), -	map { $_ => 'php' } qw(php3 php4), +	map { $_ => 'php' } qw(php3 php4 php5 phps),  	map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi' -	'mak' => 'make', +	map { $_ => 'make'} qw(mak mk),  	map { $_ => 'xml' } qw(xhtml html htm),  ); @@ -307,6 +321,10 @@ our %feature = (  	# Enable text search, which will list the commits which match author,  	# committer or commit text to a given string.  Enabled by default.  	# Project specific override is not supported. +	# +	# Note that this controls all search features, which means that if +	# it is disabled, then 'grep' and 'pickaxe' search would also be +	# disabled.  	'search' => {  		'override' => 0,  		'default' => [1]}, @@ -314,6 +332,7 @@ our %feature = (  	# Enable grep search, which will list the files in currently selected  	# tree containing the given string. Enabled by default. This can be  	# potentially CPU-intensive, of course. +	# Note that you need to have 'search' feature enabled too.  	# To enable system wide have in $GITWEB_CONFIG  	# $feature{'grep'}{'default'} = [1]; @@ -328,6 +347,7 @@ our %feature = (  	# Enable the pickaxe search, which will list the commits that modified  	# a given string in a file. This can be practical and quite faster  	# alternative to 'blame', but still potentially CPU-intensive. +	# Note that you need to have 'search' feature enabled too.  	# To enable system wide have in $GITWEB_CONFIG  	# $feature{'pickaxe'}{'default'} = [1]; @@ -406,20 +426,23 @@ our %feature = (  		'override' => 0,  		'default' => []}, -	# Allow gitweb scan project content tags described in ctags/ -	# of project repository, and display the popular Web 2.0-ish -	# "tag cloud" near the project list. Note that this is something -	# COMPLETELY different from the normal Git tags. +	# Allow gitweb scan project content tags of project repository, +	# and display the popular Web 2.0-ish "tag cloud" near the projects +	# list.  Note that this is something COMPLETELY different from the +	# normal Git tags.  	# gitweb by itself can show existing tags, but it does not handle -	# tagging itself; you need an external application for that. -	# For an example script, check Girocco's cgi/tagproj.cgi. +	# tagging itself; you need to do it externally, outside gitweb. +	# The format is described in git_get_project_ctags() subroutine.  	# You may want to install the HTML::TagCloud Perl module to get  	# a pretty tag cloud instead of just a list of tags.  	# To enable system wide have in $GITWEB_CONFIG -	# $feature{'ctags'}{'default'} = ['path_to_tag_script']; +	# $feature{'ctags'}{'default'} = [1];  	# Project specific override is not supported. + +	# In the future whether ctags editing is enabled might depend +	# on the value, but using 1 should always mean no editing of ctags.  	'ctags' => {  		'override' => 0,  		'default' => [0]}, @@ -474,6 +497,18 @@ our %feature = (  		'override' => 0,  		'default' => [0]}, +	# Enable and configure ability to change common timezone for dates +	# in gitweb output via JavaScript.  Enabled by default. +	# Project specific override is not supported. +	'javascript-timezone' => { +		'override' => 0, +		'default' => [ +			'local',     # default timezone: 'utc', 'local', or '(-|+)HHMM' format, +			             # or undef to turn off this feature +			'gitweb_tz', # name of cookie where to store selected timezone +			'datetime',  # CSS class used to mark up dates for manipulation +		]}, +  	# Syntax highlighting support. This is based on Daniel Svensson's  	# and Sham Chukoury's work in gitweb-xmms2.git.  	# It requires the 'highlight' program present in $PATH, @@ -486,6 +521,18 @@ our %feature = (  		'sub' => sub { feature_bool('highlight', @_) },  		'override' => 0,  		'default' => [0]}, + +	# Enable displaying of remote heads in the heads list + +	# To enable system wide have in $GITWEB_CONFIG +	# $feature{'remote_heads'}{'default'} = [1]; +	# To have project specific config enable override in $GITWEB_CONFIG +	# $feature{'remote_heads'}{'override'} = 1; +	# and in project config gitweb.remote_heads = 0|1; +	'remote_heads' => { +		'sub' => sub { feature_bool('remote_heads', @_) }, +		'override' => 0, +		'default' => [0]},  );  sub gitweb_get_feature { @@ -594,18 +641,50 @@ sub filter_snapshot_fmts {  		!$known_snapshot_formats{$_}{'disabled'}} @fmts;  } -our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM); -sub evaluate_gitweb_config { -	our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; -	our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++"; +# If it is set to code reference, it is code that it is to be run once per +# request, allowing updating configurations that change with each request, +# while running other code in config file only once. +# +# Otherwise, if it is false then gitweb would process config file only once; +# if it is true then gitweb config would be run for each request. +our $per_request_config = 1; + +# read and parse gitweb config file given by its parameter. +# returns true on success, false on recoverable error, allowing +# to chain this subroutine, using first file that exists. +# dies on errors during parsing config file, as it is unrecoverable. +sub read_config_file { +	my $filename = shift; +	return unless defined $filename;  	# die if there are errors parsing config file -	if (-e $GITWEB_CONFIG) { -		do $GITWEB_CONFIG; -		die $@ if $@; -	} elsif (-e $GITWEB_CONFIG_SYSTEM) { -		do $GITWEB_CONFIG_SYSTEM; +	if (-e $filename) { +		do $filename;  		die $@ if $@; +		return 1;  	} +	return; +} + +our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON); +sub evaluate_gitweb_config { +	our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; +	our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++"; +	our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++"; + +	# Protect agains duplications of file names, to not read config twice. +	# Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so +	# there possibility of duplication of filename there doesn't matter. +	$GITWEB_CONFIG = ""        if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON); +	$GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON); + +	# Common system-wide settings for convenience. +	# Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM. +	read_config_file($GITWEB_CONFIG_COMMON); + +	# Use first config file that exists.  This means use the per-instance +	# GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG. +	read_config_file($GITWEB_CONFIG) and return; +	read_config_file($GITWEB_CONFIG_SYSTEM);  }  # Get loadavg of system, to compare against $maxload. @@ -677,6 +756,7 @@ our @cgi_param_mapping = (  	snapshot_format => "sf",  	extra_options => "opt",  	search_use_regexp => "sr", +	ctag => "by_tag",  	# this must be last entry (for manipulation from JavaScript)  	javascript => "js"  ); @@ -700,6 +780,7 @@ our %actions = (  	"log" => \&git_log,  	"patch" => \&git_patch,  	"patches" => \&git_patches, +	"remotes" => \&git_remotes,  	"rss" => \&git_rss,  	"atom" => \&git_atom,  	"search" => \&git_search, @@ -774,10 +855,10 @@ sub evaluate_path_info {  		'history',  	); -	# we want to catch +	# we want to catch, among others  	# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]  	my ($parentrefname, $parentpathname, $refname, $pathname) = -		($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); +		($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);  	# first, analyze the 'current' part  	if (defined $pathname) { @@ -813,8 +894,15 @@ sub evaluate_path_info {  		# hash_base instead. It should also be noted that hand-crafted  		# links having 'history' as an action and no pathname or hash  		# set will fail, but that happens regardless of PATH_INFO. -		$input_params{'action'} ||= "shortlog"; -		if (grep { $_ eq $input_params{'action'} } @wants_base) { +		if (defined $parentrefname) { +			# if there is parent let the default be 'shortlog' action +			# (for http://git.example.com/repo.git/A..B links); if there +			# is no parent, dispatch will detect type of object and set +			# action appropriately if required (if action is not set) +			$input_params{'action'} ||= "shortlog"; +		} +		if ($input_params{'action'} && +		    grep { $_ eq $input_params{'action'} } @wants_base) {  			$input_params{'hash_base'} ||= $refname;  		} else {  			$input_params{'hash'} ||= $refname; @@ -1051,16 +1139,27 @@ sub dispatch {  }  sub reset_timer { -	our $t0 = [Time::HiRes::gettimeofday()] +	our $t0 = [ gettimeofday() ]  		if defined $t0;  	our $number_of_git_cmds = 0;  } +our $first_request = 1;  sub run_request {  	reset_timer();  	evaluate_uri(); -	evaluate_gitweb_config(); +	if ($first_request) { +		evaluate_gitweb_config(); +		evaluate_git_version(); +	} +	if ($per_request_config) { +		if (ref($per_request_config) eq 'CODE') { +			$per_request_config->(); +		} elsif (!$first_request) { +			evaluate_gitweb_config(); +		} +	}  	check_loadavg();  	# $projectroot and $projects_list might be set in gitweb config file @@ -1113,8 +1212,8 @@ sub evaluate_argv {  sub run {  	evaluate_argv(); -	evaluate_git_version(); +	$first_request = 1;  	$pre_listen_hook->()  		if $pre_listen_hook; @@ -1127,6 +1226,7 @@ sub run {  		$post_dispatch_hook->()  			if $post_dispatch_hook; +		$first_request = 0;  		last REQUEST if ($is_last_request->());  	} @@ -1153,11 +1253,15 @@ if (defined caller) {  # -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)  # -replay => 1      - start from a current view (replay with modifications)  # -path_info => 0|1 - don't use/use path_info URL (if possible) +# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone  sub href {  	my %params = @_;  	# default is to use -absolute url() i.e. $my_uri  	my $href = $params{-full} ? $my_url : $my_uri; +	# implicit -replay, must be first of implicit params +	$params{-replay} = 1 if (keys %params == 1 && $params{-anchor}); +  	$params{'project'} = $project unless exists $params{'project'};  	if ($params{-replay}) { @@ -1185,7 +1289,7 @@ sub href {  		$href =~ s,/$,,;  		# Then add the project name, if present -		$href .= "/".esc_url($params{'project'}); +		$href .= "/".esc_path_info($params{'project'});  		delete $params{'project'};  		# since we destructively absorb parameters, we keep this @@ -1195,7 +1299,8 @@ sub href {  		# Summary just uses the project path URL, any other action is  		# added to the URL  		if (defined $params{'action'}) { -			$href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary'; +			$href .= "/".esc_path_info($params{'action'}) +				unless $params{'action'} eq 'summary';  			delete $params{'action'};  		} @@ -1205,13 +1310,13 @@ sub href {  			|| $params{'hash_parent'} || $params{'hash'});  		if (defined $params{'hash_base'}) {  			if (defined $params{'hash_parent_base'}) { -				$href .= esc_url($params{'hash_parent_base'}); +				$href .= esc_path_info($params{'hash_parent_base'});  				# skip the file_parent if it's the same as the file_name  				if (defined $params{'file_parent'}) {  					if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {  						delete $params{'file_parent'};  					} elsif ($params{'file_parent'} !~ /\.\./) { -						$href .= ":/".esc_url($params{'file_parent'}); +						$href .= ":/".esc_path_info($params{'file_parent'});  						delete $params{'file_parent'};  					}  				} @@ -1219,19 +1324,19 @@ sub href {  				delete $params{'hash_parent'};  				delete $params{'hash_parent_base'};  			} elsif (defined $params{'hash_parent'}) { -				$href .= esc_url($params{'hash_parent'}). ".."; +				$href .= esc_path_info($params{'hash_parent'}). "..";  				delete $params{'hash_parent'};  			} -			$href .= esc_url($params{'hash_base'}); +			$href .= esc_path_info($params{'hash_base'});  			if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) { -				$href .= ":/".esc_url($params{'file_name'}); +				$href .= ":/".esc_path_info($params{'file_name'});  				delete $params{'file_name'};  			}  			delete $params{'hash'};  			delete $params{'hash_base'};  		} elsif (defined $params{'hash'}) { -			$href .= esc_url($params{'hash'}); +			$href .= esc_path_info($params{'hash'});  			delete $params{'hash'};  		} @@ -1264,6 +1369,13 @@ sub href {  	}  	$href .= "?" . join(';', @result) if scalar @result; +	# final transformation: trailing spaces must be escaped (URI-encoded) +	$href =~ s/(\s+)$/CGI::escape($1)/e; + +	if ($params{-anchor}) { +		$href .= "#".esc_param($params{-anchor}); +	} +  	return $href;  } @@ -1346,6 +1458,17 @@ sub esc_param {  	return $str;  } +# the quoting rules for path_info fragment are slightly different +sub esc_path_info { +	my $str = shift; +	return undef unless defined $str; + +	# path_info doesn't treat '+' as space (specially), but '?' must be escaped +	$str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg; + +	return $str; +} +  # quote unsafe chars in whole URL, so some characters cannot be quoted  sub esc_url {  	my $str = shift; @@ -1355,6 +1478,13 @@ sub esc_url {  	return $str;  } +# quote unsafe characters in HTML attributes +sub esc_attr { + +	# for XHTML conformance escaping '"' to '"' is not enough +	return esc_html(@_); +} +  # replace invalid utf8 character with SUBSTITUTION sequence  sub esc_html {  	my $str = shift; @@ -1760,7 +1890,7 @@ sub format_ref_marker {  					hash=>$dest  				)}, $name); -			$markers .= " <span class=\"$class\" title=\"$ref\">" . +			$markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .  				$link . "</span>";  		}  	} @@ -1844,7 +1974,7 @@ sub git_get_avatar {  		return $pre_white .  		       "<img width=\"$size\" " .  		            "class=\"avatar\" " . -		            "src=\"$url\" " . +		            "src=\"".esc_url($url)."\" " .  			    "alt=\"\" " .  		       "/>" . $post_white;  	} else { @@ -2396,6 +2526,13 @@ sub git_get_project_config {  	# key sanity check  	return unless ($key); +	# only subsection, if exists, is case sensitive, +	# and not lowercased by 'git config -z -l' +	if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) { +		$key = join(".", lc($hi), $mi, lc($lo)); +	} else { +		$key = lc($key); +	}  	$key =~ s/^gitweb\.//;  	return if ($key =~ m/\W/); @@ -2482,37 +2619,94 @@ sub git_get_path_by_hash {  ## ......................................................................  ## git utility functions, directly accessing git repository -sub git_get_project_description { -	my $path = shift; +# get the value of config variable either from file named as the variable +# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name +# configuration variable in the repository config file. +sub git_get_file_or_project_config { +	my ($path, $name) = @_;  	$git_dir = "$projectroot/$path"; -	open my $fd, '<', "$git_dir/description" -		or return git_get_project_config('description'); -	my $descr = <$fd>; +	open my $fd, '<', "$git_dir/$name" +		or return git_get_project_config($name); +	my $conf = <$fd>;  	close $fd; -	if (defined $descr) { -		chomp $descr; +	if (defined $conf) { +		chomp $conf;  	} -	return $descr; +	return $conf;  } -sub git_get_project_ctags { +sub git_get_project_description { +	my $path = shift; +	return git_get_file_or_project_config($path, 'description'); +} + +sub git_get_project_category {  	my $path = shift; +	return git_get_file_or_project_config($path, 'category'); +} + + +# supported formats: +# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory) +#   - if its contents is a number, use it as tag weight, +#   - otherwise add a tag with weight 1 +# * $GIT_DIR/ctags file, each line is a tag (with weight 1) +#   the same value multiple times increases tag weight +# * `gitweb.ctag' multi-valued repo config variable +sub git_get_project_ctags { +	my $project = shift;  	my $ctags = {}; -	$git_dir = "$projectroot/$path"; -	opendir my $dh, "$git_dir/ctags" -		or return $ctags; -	foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) { -		open my $ct, '<', $_ or next; -		my $val = <$ct>; -		chomp $val; -		close $ct; -		my $ctag = $_; $ctag =~ s#.*/##; -		$ctags->{$ctag} = $val; +	$git_dir = "$projectroot/$project"; +	if (opendir my $dh, "$git_dir/ctags") { +		my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh); +		foreach my $tagfile (@files) { +			open my $ct, '<', $tagfile +				or next; +			my $val = <$ct>; +			chomp $val if $val; +			close $ct; + +			(my $ctag = $tagfile) =~ s#.*/##; +			if ($val =~ /^\d+$/) { +				$ctags->{$ctag} = $val; +			} else { +				$ctags->{$ctag} = 1; +			} +		} +		closedir $dh; + +	} elsif (open my $fh, '<', "$git_dir/ctags") { +		while (my $line = <$fh>) { +			chomp $line; +			$ctags->{$line}++ if $line; +		} +		close $fh; + +	} else { +		my $taglist = config_to_multi(git_get_project_config('ctag')); +		foreach my $tag (@$taglist) { +			$ctags->{$tag}++; +		}  	} -	closedir $dh; -	$ctags; + +	return $ctags; +} + +# return hash, where keys are content tags ('ctags'), +# and values are sum of weights of given tag in every project +sub git_gather_all_ctags { +	my $projects = shift; +	my $ctags = {}; + +	foreach my $p (@$projects) { +		foreach my $ct (keys %{$p->{'ctags'}}) { +			$ctags->{$ct} += $p->{'ctags'}->{$ct}; +		} +	} + +	return $ctags;  }  sub git_populate_project_tagcloud { @@ -2530,33 +2724,49 @@ sub git_populate_project_tagcloud {  	}  	my $cloud; +	my $matched = $cgi->param('by_tag');  	if (eval { require HTML::TagCloud; 1; }) {  		$cloud = HTML::TagCloud->new; -		foreach (sort keys %ctags_lc) { +		foreach my $ctag (sort keys %ctags_lc) {  			# Pad the title with spaces so that the cloud looks  			# less crammed. -			my $title = $ctags_lc{$_}->{topname}; +			my $title = esc_html($ctags_lc{$ctag}->{topname});  			$title =~ s/ / /g;  			$title =~ s/^/ /g;  			$title =~ s/$/ /g; -			$cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count}); +			if (defined $matched && $matched eq $ctag) { +				$title = qq(<span class="match">$title</span>); +			} +			$cloud->add($title, href(project=>undef, ctag=>$ctag), +			            $ctags_lc{$ctag}->{count});  		}  	} else { -		$cloud = \%ctags_lc; +		$cloud = {}; +		foreach my $ctag (keys %ctags_lc) { +			my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1); +			if (defined $matched && $matched eq $ctag) { +				$title = qq(<span class="match">$title</span>); +			} +			$cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count}; +			$cloud->{$ctag}{ctag} = +				$cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title); +		}  	} -	$cloud; +	return $cloud;  }  sub git_show_project_tagcloud {  	my ($cloud, $count) = @_; -	print STDERR ref($cloud)."..\n";  	if (ref $cloud eq 'HTML::TagCloud') {  		return $cloud->html_and_css($count);  	} else { -		my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud; -		return '<p align="center">' . join (', ', map { -			"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>" -		} splice(@tags, 0, $count)) . '</p>'; +		my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud; +		return +			'<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' . +			join (', ', map { +				$cloud->{$_}->{'ctag'} +			} splice(@tags, 0, $count)) . +			'</div>';  	}  } @@ -2575,21 +2785,23 @@ sub git_get_project_url_list {  }  sub git_get_projects_list { -	my ($filter) = @_; +	my $filter = shift || '';  	my @list; -	$filter ||= '';  	$filter =~ s/\.git$//; -	my $check_forks = gitweb_check_feature('forks'); -  	if (-d $projects_list) {  		# search in directory -		my $dir = $projects_list . ($filter ? "/$filter" : ''); +		my $dir = $projects_list;  		# remove the trailing "/"  		$dir =~ s!/+$!!; -		my $pfxlen = length("$dir"); -		my $pfxdepth = ($dir =~ tr!/!!); +		my $pfxlen = length("$projects_list"); +		my $pfxdepth = ($projects_list =~ tr!/!!); +		# when filtering, search only given subdirectory +		if ($filter) { +			$dir .= "/$filter"; +			$dir =~ s!/+$!!; +		}  		File::Find::find({  			follow_fast => 1, # follow symbolic links @@ -2604,14 +2816,14 @@ sub git_get_projects_list {  				# only directories can be git repositories  				return unless (-d $_);  				# don't traverse too deep (Find is super slow on os x) +				# $project_maxdepth excludes depth of $projectroot  				if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {  					$File::Find::prune = 1;  					return;  				} -				my $subdir = substr($File::Find::name, $pfxlen + 1); +				my $path = substr($File::Find::name, $pfxlen + 1);  				# we check related file in $projectroot -				my $path = ($filter ? "$filter/" : '') . $subdir;  				if (check_export_ok("$projectroot/$path")) {  					push @list, { path => $path };  					$File::Find::prune = 1; @@ -2624,7 +2836,6 @@ sub git_get_projects_list {  		# 'git%2Fgit.git Linus+Torvalds'  		# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'  		# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' -		my %paths;  		open my $fd, '<', $projects_list or return;  	PROJECT:  		while (my $line = <$fd>) { @@ -2635,32 +2846,9 @@ sub git_get_projects_list {  			if (!defined $path) {  				next;  			} -			if ($filter ne '') { -				# looking for forks; -				my $pfx = substr($path, 0, length($filter)); -				if ($pfx ne $filter) { -					next PROJECT; -				} -				my $sfx = substr($path, length($filter)); -				if ($sfx !~ /^\/.*\.git$/) { -					next PROJECT; -				} -			} elsif ($check_forks) { -			PATH: -				foreach my $filter (keys %paths) { -					# looking for forks; -					my $pfx = substr($path, 0, length($filter)); -					if ($pfx ne $filter) { -						next PATH; -					} -					my $sfx = substr($path, length($filter)); -					if ($sfx !~ /^\/.*\.git$/) { -						next PATH; -					} -					# is a fork, don't include it in -					# the list -					next PROJECT; -				} +			# if $filter is rpovided, check if $path begins with $filter +			if ($filter && $path !~ m!^\Q$filter\E/!) { +				next;  			}  			if (check_export_ok("$projectroot/$path")) {  				my $pr = { @@ -2668,8 +2856,6 @@ sub git_get_projects_list {  					owner => to_utf8($owner),  				};  				push @list, $pr; -				(my $forks_path = $path) =~ s/\.git$//; -				$paths{$forks_path}++;  			}  		}  		close $fd; @@ -2677,6 +2863,98 @@ sub git_get_projects_list {  	return @list;  } +# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile) +# as side effects it sets 'forks' field to list of forks for forked projects +sub filter_forks_from_projects_list { +	my $projects = shift; + +	my %trie; # prefix tree of directories (path components) +	# generate trie out of those directories that might contain forks +	foreach my $pr (@$projects) { +		my $path = $pr->{'path'}; +		$path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory +		next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git' +		next unless ($path);      # skip '.git' repository: tests, git-instaweb +		next unless (-d $path);   # containing directory exists +		$pr->{'forks'} = [];      # there can be 0 or more forks of project + +		# add to trie +		my @dirs = split('/', $path); +		# walk the trie, until either runs out of components or out of trie +		my $ref = \%trie; +		while (scalar @dirs && +		       exists($ref->{$dirs[0]})) { +			$ref = $ref->{shift @dirs}; +		} +		# create rest of trie structure from rest of components +		foreach my $dir (@dirs) { +			$ref = $ref->{$dir} = {}; +		} +		# create end marker, store $pr as a data +		$ref->{''} = $pr if (!exists $ref->{''}); +	} + +	# filter out forks, by finding shortest prefix match for paths +	my @filtered; + PROJECT: +	foreach my $pr (@$projects) { +		# trie lookup +		my $ref = \%trie; +	DIR: +		foreach my $dir (split('/', $pr->{'path'})) { +			if (exists $ref->{''}) { +				# found [shortest] prefix, is a fork - skip it +				push @{$ref->{''}{'forks'}}, $pr; +				next PROJECT; +			} +			if (!exists $ref->{$dir}) { +				# not in trie, cannot have prefix, not a fork +				push @filtered, $pr; +				next PROJECT; +			} +			# If the dir is there, we just walk one step down the trie. +			$ref = $ref->{$dir}; +		} +		# we ran out of trie +		# (shouldn't happen: it's either no match, or end marker) +		push @filtered, $pr; +	} + +	return @filtered; +} + +# note: fill_project_list_info must be run first, +# for 'descr_long' and 'ctags' to be filled +sub search_projects_list { +	my ($projlist, %opts) = @_; +	my $tagfilter  = $opts{'tagfilter'}; +	my $searchtext = $opts{'searchtext'}; + +	return @$projlist +		unless ($tagfilter || $searchtext); + +	my @projects; + PROJECT: +	foreach my $pr (@$projlist) { + +		if ($tagfilter) { +			next unless ref($pr->{'ctags'}) eq 'HASH'; +			next unless +				grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}}; +		} + +		if ($searchtext) { +			next unless +				$pr->{'path'} =~ /$searchtext/ || +				$pr->{'descr_long'} =~ /$searchtext/; +		} + +		push @projects, $pr; +	} + +	return @projects; +} +  our $gitweb_project_owner = undef;  sub git_get_project_list_from_file { @@ -2745,6 +3023,44 @@ sub git_get_last_activity {  	return (undef, undef);  } +# Implementation note: when a single remote is wanted, we cannot use 'git +# remote show -n' because that command always work (assuming it's a remote URL +# if it's not defined), and we cannot use 'git remote show' because that would +# try to make a network roundtrip. So the only way to find if that particular +# remote is defined is to walk the list provided by 'git remote -v' and stop if +# and when we find what we want. +sub git_get_remotes_list { +	my $wanted = shift; +	my %remotes = (); + +	open my $fd, '-|' , git_cmd(), 'remote', '-v'; +	return unless $fd; +	while (my $remote = <$fd>) { +		chomp $remote; +		$remote =~ s!\t(.*?)\s+\((\w+)\)$!!; +		next if $wanted and not $remote eq $wanted; +		my ($url, $key) = ($1, $2); + +		$remotes{$remote} ||= { 'heads' => () }; +		$remotes{$remote}{$key} = $url; +	} +	close $fd or return; +	return wantarray ? %remotes : \%remotes; +} + +# Takes a hash of remotes as first parameter and fills it by adding the +# available remote heads for each of the indicated remotes. +sub fill_remote_heads { +	my $remotes = shift; +	my @heads = map { "remotes/$_" } keys %$remotes; +	my @remoteheads = git_get_heads_list(undef, @heads); +	foreach my $remote (keys %$remotes) { +		$remotes->{$remote}{'heads'} = [ grep { +			$_->{'name'} =~ s!^$remote/!! +			} @remoteheads ]; +	} +} +  sub git_get_references {  	my $type = shift || "";  	my %refs; @@ -2807,8 +3123,10 @@ sub parse_date {  	$date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",  	                     1900+$year, 1+$mon, $mday, $hour ,$min, $sec; -	$tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/; -	my $local = $epoch + ((int $1 + ($2/60)) * 3600); +	my ($tz_sign, $tz_hour, $tz_min) = +		($tz =~ m/^([-+])(\d\d)(\d\d)$/); +	$tz_sign = ($tz_sign eq '-' ? -1 : +1); +	my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);  	($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);  	$date{'hour_local'} = $hour;  	$date{'minute_local'} = $min; @@ -3143,13 +3461,15 @@ sub parse_from_to_diffinfo {  ## parse to array of hashes functions  sub git_get_heads_list { -	my $limit = shift; +	my ($limit, @classes) = @_; +	@classes = ('heads') unless @classes; +	my @patterns = map { "refs/$_" } @classes;  	my @headslist;  	open my $fd, '-|', git_cmd(), 'for-each-ref',  		($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',  		'--format=%(objectname) %(refname) %(subject)%00%(committer)', -		'refs/heads' +		@patterns  		or return;  	while (my $line = <$fd>) {  		my %ref_item; @@ -3160,7 +3480,7 @@ sub git_get_heads_list {  		my ($committer, $epoch, $tz) =  			($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);  		$ref_item{'fullname'}  = $name; -		$name =~ s!^refs/heads/!!; +		$name =~ s!^refs/(?:head|remote)s/!!;  		$ref_item{'name'}  = $name;  		$ref_item{'id'}    = $hash; @@ -3265,12 +3585,9 @@ sub mimetype_guess_file {  	open(my $mh, '<', $mimemap) or return undef;  	while (<$mh>) {  		next if m/^#/; # skip comments -		my ($mimetype, $exts) = split(/\t+/); -		if (defined $exts) { -			my @exts = split(/\s+/, $exts); -			foreach my $ext (@exts) { -				$mimemap{$ext} = $mimetype; -			} +		my ($mimetype, @exts) = split(/\s+/); +		foreach my $ext (@exts) { +			$mimemap{$ext} = $mimetype;  		}  	}  	close($mh); @@ -3357,10 +3674,10 @@ sub run_highlighter {  	my ($fd, $highlight, $syntax) = @_;  	return $fd unless ($highlight && defined $syntax); -	close $fd -		or die_error(404, "Reading blob failed"); +	close $fd;  	open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ". -	          "highlight --xhtml --fragment --syntax $syntax |" +	          quote_command($highlight_bin). +	          " --replace-tabs=8 --fragment --syntax $syntax |"  		or die_error(500, "Couldn't open file or run syntax highlighter");  	return $fd;  } @@ -3386,13 +3703,7 @@ sub get_page_title {  	return $title;  } -sub git_header_html { -	my $status = shift || "200 OK"; -	my $expires = shift; -	my %opts = @_; - -	my $title = get_page_title(); -	my $content_type; +sub get_content_type_html {  	# require explicit support from the UA if we are to send the page as  	# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.  	# we have to do this because MSIE sometimes globs '*/*', pretending to @@ -3400,52 +3711,24 @@ sub git_header_html {  	if (defined $cgi->http('HTTP_ACCEPT') &&  	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&  	    $cgi->Accept('application/xhtml+xml') != 0) { -		$content_type = 'application/xhtml+xml'; -	} else { -		$content_type = 'text/html'; -	} -	print $cgi->header(-type=>$content_type, -charset => 'utf-8', -	                   -status=> $status, -expires => $expires) -		unless ($opts{'-no_http_header'}); -	my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; -	print <<EOF; -<?xml version="1.0" encoding="utf-8"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US"> -<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke --> -<!-- git core binaries version $git_version --> -<head> -<meta http-equiv="content-type" content="$content_type; charset=utf-8"/> -<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/> -<meta name="robots" content="index, nofollow"/> -<title>$title</title> -EOF -	# the stylesheet, favicon etc urls won't work correctly with path_info -	# unless we set the appropriate base URL -	if ($ENV{'PATH_INFO'}) { -		print "<base href=\"".esc_url($base_url)."\" />\n"; -	} -	# print out each stylesheet that exist, providing backwards capability -	# for those people who defined $stylesheet in a config file -	if (defined $stylesheet) { -		print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; +		return 'application/xhtml+xml';  	} else { -		foreach my $stylesheet (@stylesheets) { -			next unless $stylesheet; -			print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; -		} +		return 'text/html';  	} +} + +sub print_feed_meta {  	if (defined $project) {  		my %href_params = get_feed_info();  		if (!exists $href_params{'-title'}) {  			$href_params{'-title'} = 'log';  		} -		foreach my $format qw(RSS Atom) { +		foreach my $format (qw(RSS Atom)) {  			my $type = lc($format);  			my %link_attr = (  				'-rel' => 'alternate', -				'-title' => "$project - $href_params{'-title'} - $format feed", +				'-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),  				'-type' => "application/$type+xml"  			); @@ -3472,71 +3755,141 @@ EOF  	} else {  		printf('<link rel="alternate" title="%s projects list" '.  		       'href="%s" type="text/plain; charset=utf-8" />'."\n", -		       $site_name, href(project=>undef, action=>"project_index")); +		       esc_attr($site_name), href(project=>undef, action=>"project_index"));  		printf('<link rel="alternate" title="%s projects feeds" '.  		       'href="%s" type="text/x-opml" />'."\n", -		       $site_name, href(project=>undef, action=>"opml")); -	} -	if (defined $favicon) { -		print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n); +		       esc_attr($site_name), href(project=>undef, action=>"opml"));  	} +} -	print "</head>\n" . -	      "<body>\n"; +sub print_header_links { +	my $status = shift; -	if (defined $site_header && -f $site_header) { -		insert_file($site_header); +	# print out each stylesheet that exist, providing backwards capability +	# for those people who defined $stylesheet in a config file +	if (defined $stylesheet) { +		print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n"; +	} else { +		foreach my $stylesheet (@stylesheets) { +			next unless $stylesheet; +			print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n"; +		} +	} +	print_feed_meta() +		if ($status eq '200 OK'); +	if (defined $favicon) { +		print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);  	} +} + +sub print_nav_breadcrumbs { +	my %opts = @_; -	print "<div class=\"page_header\">\n" . -	      $cgi->a({-href => esc_url($logo_url), -	               -title => $logo_label}, -	              qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));  	print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";  	if (defined $project) {  		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));  		if (defined $action) { -			print " / $action"; +			my $action_print = $action ; +			if (defined $opts{-action_extra}) { +				$action_print = $cgi->a({-href => href(action=>$action)}, +					$action); +			} +			print " / $action_print"; +		} +		if (defined $opts{-action_extra}) { +			print " / $opts{-action_extra}";  		}  		print "\n";  	} +} + +sub print_search_form { +	if (!defined $searchtext) { +		$searchtext = ""; +	} +	my $search_hash; +	if (defined $hash_base) { +		$search_hash = $hash_base; +	} elsif (defined $hash) { +		$search_hash = $hash; +	} else { +		$search_hash = "HEAD"; +	} +	my $action = $my_uri; +	my $use_pathinfo = gitweb_check_feature('pathinfo'); +	if ($use_pathinfo) { +		$action .= "/".esc_url($project); +	} +	print $cgi->startform(-method => "get", -action => $action) . +	      "<div class=\"search\">\n" . +	      (!$use_pathinfo && +	      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . +	      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" . +	      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" . +	      $cgi->popup_menu(-name => 'st', -default => 'commit', +	                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) . +	      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) . +	      " search:\n", +	      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . +	      "<span title=\"Extended regular expression\">" . +	      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', +	                     -checked => $search_use_regexp) . +	      "</span>" . +	      "</div>" . +	      $cgi->end_form() . "\n"; +} + +sub git_header_html { +	my $status = shift || "200 OK"; +	my $expires = shift; +	my %opts = @_; + +	my $title = get_page_title(); +	my $content_type = get_content_type_html(); +	print $cgi->header(-type=>$content_type, -charset => 'utf-8', +	                   -status=> $status, -expires => $expires) +		unless ($opts{'-no_http_header'}); +	my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; +	print <<EOF; +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US"> +<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke --> +<!-- git core binaries version $git_version --> +<head> +<meta http-equiv="content-type" content="$content_type; charset=utf-8"/> +<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/> +<meta name="robots" content="index, nofollow"/> +<title>$title</title> +EOF +	# the stylesheet, favicon etc urls won't work correctly with path_info +	# unless we set the appropriate base URL +	if ($ENV{'PATH_INFO'}) { +		print "<base href=\"".esc_url($base_url)."\" />\n"; +	} +	print_header_links($status); +	print "</head>\n" . +	      "<body>\n"; + +	if (defined $site_header && -f $site_header) { +		insert_file($site_header); +	} + +	print "<div class=\"page_header\">\n"; +	if (defined $logo) { +		print $cgi->a({-href => esc_url($logo_url), +		               -title => $logo_label}, +		              $cgi->img({-src => esc_url($logo), +		                         -width => 72, -height => 27, +		                         -alt => "git", +		                         -class => "logo"})); +	} +	print_nav_breadcrumbs(%opts);  	print "</div>\n";  	my $have_search = gitweb_check_feature('search');  	if (defined $project && $have_search) { -		if (!defined $searchtext) { -			$searchtext = ""; -		} -		my $search_hash; -		if (defined $hash_base) { -			$search_hash = $hash_base; -		} elsif (defined $hash) { -			$search_hash = $hash; -		} else { -			$search_hash = "HEAD"; -		} -		my $action = $my_uri; -		my $use_pathinfo = gitweb_check_feature('pathinfo'); -		if ($use_pathinfo) { -			$action .= "/".esc_url($project); -		} -		print $cgi->startform(-method => "get", -action => $action) . -		      "<div class=\"search\">\n" . -		      (!$use_pathinfo && -		      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . -		      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" . -		      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" . -		      $cgi->popup_menu(-name => 'st', -default => 'commit', -		                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) . -		      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) . -		      " search:\n", -		      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . -		      "<span title=\"Extended regular expression\">" . -		      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', -		                     -checked => $search_use_regexp) . -		      "</span>" . -		      "</div>" . -		      $cgi->end_form() . "\n"; +		print_search_form();  	}  } @@ -3556,7 +3909,7 @@ sub git_footer_html {  		}  		$href_params{'-title'} ||= 'log'; -		foreach my $format qw(RSS Atom) { +		foreach my $format (qw(RSS Atom)) {  			$href_params{'action'} = lc($format);  			print $cgi->a({-href => href(%href_params),  			              -title => "$href_params{'-title'} $format feed", @@ -3575,7 +3928,7 @@ sub git_footer_html {  		print "<div id=\"generating_info\">\n";  		print 'This page took '.  		      '<span id="generating_time" class="time_span">'. -		      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). +		      tv_interval($t0, [ gettimeofday() ]).  		      ' seconds </span>'.  		      ' and '.  		      '<span id="generating_cmd">'. @@ -3589,16 +3942,27 @@ sub git_footer_html {  		insert_file($site_footer);  	} -	print qq!<script type="text/javascript" src="$javascript"></script>\n!; +	print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;  	if (defined $action &&  	    $action eq 'blame_incremental') {  		print qq!<script type="text/javascript">\n!.  		      qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.  		      qq!           "!. href() .qq!");\n!.  		      qq!</script>\n!; -	} elsif (gitweb_check_feature('javascript-actions')) { +	} else { +		my ($jstimezone, $tz_cookie, $datetime_class) = +			gitweb_get_feature('javascript-timezone'); +  		print qq!<script type="text/javascript">\n!. -		      qq!window.onload = fixLinks;\n!. +		      qq!window.onload = function () {\n!; +		if (gitweb_check_feature('javascript-actions')) { +			print qq!	fixLinks();\n!; +		} +		if ($jstimezone && $tz_cookie && $datetime_class) { +			print qq!	var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days +			      qq!	onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!; +		} +		print qq!};\n!.  		      qq!</script>\n!;  	} @@ -3703,6 +4067,19 @@ sub git_print_page_nav {  	      "</div>\n";  } +# returns a submenu for the nagivation of the refs views (tags, heads, +# remotes) with the current view disabled and the remotes view only +# available if the feature is enabled +sub format_ref_views { +	my ($current) = @_; +	my @ref_views = qw{tags heads}; +	push @ref_views, 'remotes' if gitweb_check_feature('remote_heads'); +	return join " | ", map { +		$_ eq $current ? $_ : +		$cgi->a({-href => href(action=>$_)}, $_) +	} @ref_views +} +  sub format_paging_nav {  	my ($action, $page, $has_next_link) = @_;  	my $paging_nav; @@ -3746,22 +4123,68 @@ sub git_print_header_div {  	      "\n</div>\n";  } -sub print_local_time { -	print format_local_time(@_); +sub format_repo_url { +	my ($name, $url) = @_; +	return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";  } -sub format_local_time { -	my $localtime = ''; -	my %date = @_; -	if ($date{'hour_local'} < 6) { -		$localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)", -			$date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'}); -	} else { -		$localtime .= sprintf(" (%02d:%02d %s)", -			$date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'}); +# Group output by placing it in a DIV element and adding a header. +# Options for start_div() can be provided by passing a hash reference as the +# first parameter to the function. +# Options to git_print_header_div() can be provided by passing an array +# reference. This must follow the options to start_div if they are present. +# The content can be a scalar, which is output as-is, a scalar reference, which +# is output after html escaping, an IO handle passed either as *handle or +# *handle{IO}, or a function reference. In the latter case all following +# parameters will be taken as argument to the content function call. +sub git_print_section { +	my ($div_args, $header_args, $content); +	my $arg = shift; +	if (ref($arg) eq 'HASH') { +		$div_args = $arg; +		$arg = shift; +	} +	if (ref($arg) eq 'ARRAY') { +		$header_args = $arg; +		$arg = shift;  	} +	$content = $arg; -	return $localtime; +	print $cgi->start_div($div_args); +	git_print_header_div(@$header_args); + +	if (ref($content) eq 'CODE') { +		$content->(@_); +	} elsif (ref($content) eq 'SCALAR') { +		print esc_html($$content); +	} elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') { +		print <$content>; +	} elsif (!ref($content) && defined($content)) { +		print $content; +	} + +	print $cgi->end_div; +} + +sub format_timestamp_html { +	my $date = shift; +	my $strtime = $date->{'rfc2822'}; + +	my (undef, undef, $datetime_class) = +		gitweb_get_feature('javascript-timezone'); +	if ($datetime_class) { +		$strtime = qq!<span class="$datetime_class">$strtime</span>!; +	} + +	my $localtime_format = '(%02d:%02d %s)'; +	if ($date->{'hour_local'} < 6) { +		$localtime_format = '(<span class="atnight">%02d:%02d</span> %s)'; +	} +	$strtime .= ' ' . +	            sprintf($localtime_format, +	                    $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'}); + +	return $strtime;  }  # Outputs the author name and date in long form @@ -3774,10 +4197,9 @@ sub git_print_authorship {  	my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});  	print "<$tag class=\"author_date\">" .  	      format_search_author($author, "author", esc_html($author)) . -	      " [$ad{'rfc2822'}"; -	print_local_time(%ad) if ($opts{-localtime}); -	print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1) -		  . "</$tag>\n"; +	      " [".format_timestamp_html(\%ad)."]". +	      git_get_avatar($co->{'author_email'}, -pad_before => 1) . +	      "</$tag>\n";  }  # Outputs table rows containing the full author or committer information, @@ -3794,16 +4216,16 @@ sub git_print_authorship_rows {  		my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});  		print "<tr><td>$who</td><td>" .  		      format_search_author($co->{"${who}_name"}, $who, -			       esc_html($co->{"${who}_name"})) . " " . +		                           esc_html($co->{"${who}_name"})) . " " .  		      format_search_author($co->{"${who}_email"}, $who, -			       esc_html("<" . $co->{"${who}_email"} . ">")) . +		                           esc_html("<" . $co->{"${who}_email"} . ">")) .  		      "</td><td rowspan=\"2\">" .  		      git_get_avatar($co->{"${who}_email"}, -size => 'double') .  		      "</td></tr>\n" .  		      "<tr>" . -		      "<td></td><td> $wd{'rfc2822'}"; -		print_local_time(%wd); -		print "</td>" . +		      "<td></td><td>" . +		      format_timestamp_html(\%wd) . +		      "</td>" .  		      "</tr>\n";  	}  } @@ -4153,7 +4575,8 @@ sub git_difftree_body {  				# link to patch  				$patchno++;  				print "<td class=\"link\">" . -				      $cgi->a({-href => "#patch$patchno"}, "patch") . +				      $cgi->a({-href => href(-anchor=>"patch$patchno")}, +				              "patch") .  				      " | " .  				      "</td>\n";  			} @@ -4230,7 +4653,7 @@ sub git_difftree_body {  		}  		if ($diff->{'from_mode'} ne ('0' x 6)) {  			$from_mode_oct = oct $diff->{'from_mode'}; -			if (S_ISREG($to_mode_oct)) { # only for regular file +			if (S_ISREG($from_mode_oct)) { # only for regular file  				$from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits  			}  			$from_file_type = file_type($diff->{'from_mode'}); @@ -4250,8 +4673,9 @@ sub git_difftree_body {  			if ($action eq 'commitdiff') {  				# link to patch  				$patchno++; -				print $cgi->a({-href => "#patch$patchno"}, "patch"); -				print " | "; +				print $cgi->a({-href => href(-anchor=>"patch$patchno")}, +				              "patch") . +				      " | ";  			}  			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},  			                             hash_base=>$hash, file_name=>$diff->{'file'})}, @@ -4270,8 +4694,9 @@ sub git_difftree_body {  			if ($action eq 'commitdiff') {  				# link to patch  				$patchno++; -				print $cgi->a({-href => "#patch$patchno"}, "patch"); -				print " | "; +				print $cgi->a({-href => href(-anchor=>"patch$patchno")}, +				              "patch") . +				      " | ";  			}  			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},  			                             hash_base=>$parent, file_name=>$diff->{'file'})}, @@ -4312,7 +4737,8 @@ sub git_difftree_body {  			if ($action eq 'commitdiff') {  				# link to patch  				$patchno++; -				print $cgi->a({-href => "#patch$patchno"}, "patch") . +				print $cgi->a({-href => href(-anchor=>"patch$patchno")}, +				              "patch") .  				      " | ";  			} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {  				# "commit" view and modified file (not onlu mode changed) @@ -4357,7 +4783,8 @@ sub git_difftree_body {  			if ($action eq 'commitdiff') {  				# link to patch  				$patchno++; -				print $cgi->a({-href => "#patch$patchno"}, "patch") . +				print $cgi->a({-href => href(-anchor=>"patch$patchno")}, +				              "patch") .  				      " | ";  			} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {  				# "commit" view and modified file (not only pure rename or copy) @@ -4541,11 +4968,12 @@ sub git_patchset_body {  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -# fills project list info (age, description, owner, forks) for each -# project in the list, removing invalid projects from returned list +# fills project list info (age, description, owner, category, forks) +# for each project in the list, removing invalid projects from +# returned list  # NOTE: modifies $projlist, but does not remove entries from it  sub fill_project_list_info { -	my ($projlist, $check_forks) = @_; +	my $projlist = shift;  	my @projects;  	my $show_ctags = gitweb_check_feature('ctags'); @@ -4565,23 +4993,59 @@ sub fill_project_list_info {  		if (!defined $pr->{'owner'}) {  			$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";  		} -		if ($check_forks) { -			my $pname = $pr->{'path'}; -			if (($pname =~ s/\.git$//) && -			    ($pname !~ /\/$/) && -			    (-d "$projectroot/$pname")) { -				$pr->{'forks'} = "-d $projectroot/$pname"; -			} else { -				$pr->{'forks'} = 0; -			} +		if ($show_ctags) { +			$pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); +		} +		if ($projects_list_group_categories && !defined $pr->{'category'}) { +			my $cat = git_get_project_category($pr->{'path'}) || +			                                   $project_list_default_category; +			$pr->{'category'} = to_utf8($cat);  		} -		$show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'}); +  		push @projects, $pr;  	}  	return @projects;  } +sub sort_projects_list { +	my ($projlist, $order) = @_; +	my @projects; + +	my %order_info = ( +		project => { key => 'path', type => 'str' }, +		descr => { key => 'descr_long', type => 'str' }, +		owner => { key => 'owner', type => 'str' }, +		age => { key => 'age', type => 'num' } +	); +	my $oi = $order_info{$order}; +	return @$projlist unless defined $oi; +	if ($oi->{'type'} eq 'str') { +		@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist; +	} else { +		@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist; +	} + +	return @projects; +} + +# returns a hash of categories, containing the list of project +# belonging to each category +sub build_projlist_by_category { +	my ($projlist, $from, $to) = @_; +	my %categories; + +	$from = 0 unless defined $from; +	$to = $#$projlist if (!defined $to || $#$projlist < $to); + +	for (my $i = $from; $i <= $to; $i++) { +		my $pr = $projlist->[$i]; +		push @{$categories{ $pr->{'category'} }}, $pr; +	} + +	return wantarray ? %categories : \%categories; +} +  # print 'sort by' <th> element, generating 'sort by $name' replay link  # if that order is not selected  sub print_sort_th { @@ -4605,70 +5069,15 @@ sub format_sort_th {  	return $sort_th;  } -sub git_project_list_body { -	# actually uses global variable $project -	my ($projlist, $order, $from, $to, $extra, $no_header) = @_; - -	my $check_forks = gitweb_check_feature('forks'); -	my @projects = fill_project_list_info($projlist, $check_forks); +sub git_project_list_rows { +	my ($projlist, $from, $to, $check_forks) = @_; -	$order ||= $default_projects_order;  	$from = 0 unless defined $from; -	$to = $#projects if (!defined $to || $#projects < $to); - -	my %order_info = ( -		project => { key => 'path', type => 'str' }, -		descr => { key => 'descr_long', type => 'str' }, -		owner => { key => 'owner', type => 'str' }, -		age => { key => 'age', type => 'num' } -	); -	my $oi = $order_info{$order}; -	if ($oi->{'type'} eq 'str') { -		@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects; -	} else { -		@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects; -	} - -	my $show_ctags = gitweb_check_feature('ctags'); -	if ($show_ctags) { -		my %ctags; -		foreach my $p (@projects) { -			foreach my $ct (keys %{$p->{'ctags'}}) { -				$ctags{$ct} += $p->{'ctags'}->{$ct}; -			} -		} -		my $cloud = git_populate_project_tagcloud(\%ctags); -		print git_show_project_tagcloud($cloud, 64); -	} +	$to = $#$projlist if (!defined $to || $#$projlist < $to); -	print "<table class=\"project_list\">\n"; -	unless ($no_header) { -		print "<tr>\n"; -		if ($check_forks) { -			print "<th></th>\n"; -		} -		print_sort_th('project', $order, 'Project'); -		print_sort_th('descr', $order, 'Description'); -		print_sort_th('owner', $order, 'Owner'); -		print_sort_th('age', $order, 'Last Change'); -		print "<th></th>\n" . # for links -		      "</tr>\n"; -	}  	my $alternate = 1; -	my $tagfilter = $cgi->param('by_tag');  	for (my $i = $from; $i <= $to; $i++) { -		my $pr = $projects[$i]; - -		next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}}; -		next if $searchtext and not $pr->{'path'} =~ /$searchtext/ -			and not $pr->{'descr_long'} =~ /$searchtext/; -		# Weed out forks or non-matching entries of search -		if ($check_forks) { -			my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#; -			$forkbase="^$forkbase" if $forkbase; -			next if not $searchtext and not $tagfilter and $show_ctags -				and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe -		} +		my $pr = $projlist->[$i];  		if ($alternate) {  			print "<tr class=\"dark\">\n"; @@ -4676,11 +5085,17 @@ sub git_project_list_body {  			print "<tr class=\"light\">\n";  		}  		$alternate ^= 1; +  		if ($check_forks) {  			print "<td>";  			if ($pr->{'forks'}) { -				print "<!-- $pr->{'forks'} -->\n"; -				print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+"); +				my $nforks = scalar @{$pr->{'forks'}}; +				if ($nforks > 0) { +					print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"), +					               -title => "$nforks forks"}, "+"); +				} else { +					print $cgi->span({-title => "$nforks forks"}, "+"); +				}  			}  			print "</td>\n";  		} @@ -4701,6 +5116,84 @@ sub git_project_list_body {  		      "</td>\n" .  		      "</tr>\n";  	} +} + +sub git_project_list_body { +	# actually uses global variable $project +	my ($projlist, $order, $from, $to, $extra, $no_header) = @_; +	my @projects = @$projlist; + +	my $check_forks = gitweb_check_feature('forks'); +	my $show_ctags  = gitweb_check_feature('ctags'); +	my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef; +	$check_forks = undef +		if ($tagfilter || $searchtext); + +	# filtering out forks before filling info allows to do less work +	@projects = filter_forks_from_projects_list(\@projects) +		if ($check_forks); +	@projects = fill_project_list_info(\@projects); +	# searching projects require filling to be run before it +	@projects = search_projects_list(\@projects, +	                                 'searchtext' => $searchtext, +	                                 'tagfilter'  => $tagfilter) +		if ($tagfilter || $searchtext); + +	$order ||= $default_projects_order; +	$from = 0 unless defined $from; +	$to = $#projects if (!defined $to || $#projects < $to); + +	# short circuit +	if ($from > $to) { +		print "<center>\n". +		      "<b>No such projects found</b><br />\n". +		      "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n". +		      "</center>\n<br />\n"; +		return; +	} + +	@projects = sort_projects_list(\@projects, $order); + +	if ($show_ctags) { +		my $ctags = git_gather_all_ctags(\@projects); +		my $cloud = git_populate_project_tagcloud($ctags); +		print git_show_project_tagcloud($cloud, 64); +	} + +	print "<table class=\"project_list\">\n"; +	unless ($no_header) { +		print "<tr>\n"; +		if ($check_forks) { +			print "<th></th>\n"; +		} +		print_sort_th('project', $order, 'Project'); +		print_sort_th('descr', $order, 'Description'); +		print_sort_th('owner', $order, 'Owner'); +		print_sort_th('age', $order, 'Last Change'); +		print "<th></th>\n" . # for links +		      "</tr>\n"; +	} + +	if ($projects_list_group_categories) { +		# only display categories with projects in the $from-$to window +		@projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to]; +		my %categories = build_projlist_by_category(\@projects, $from, $to); +		foreach my $cat (sort keys %categories) { +			unless ($cat eq "") { +				print "<tr>\n"; +				if ($check_forks) { +					print "<td></td>\n"; +				} +				print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n"; +				print "</tr>\n"; +			} + +			git_project_list_rows($categories{$cat}, undef, undef, $check_forks); +		} +	} else { +		git_project_list_rows(\@projects, $from, $to, $check_forks); +	} +  	if (defined $extra) {  		print "<tr>\n";  		if ($check_forks) { @@ -4724,7 +5217,6 @@ sub git_log_body {  		next if !%co;  		my $commit = $co{'id'};  		my $ref = format_ref_marker($refs, $commit); -		my %ad = parse_date($co{'author_epoch'});  		git_print_header_div('commit',  		               "<span class=\"age\">$co{'age_string'}</span>" .  		               esc_html($co{'title'}) . $ref, @@ -4945,7 +5437,7 @@ sub git_heads_body {  		      "<td class=\"link\">" .  		      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .  		      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " . -		      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") . +		      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .  		      "</td>\n" .  		      "</tr>";  	} @@ -4957,6 +5449,311 @@ sub git_heads_body {  	print "</table>\n";  } +# Display a single remote block +sub git_remote_block { +	my ($remote, $rdata, $limit, $head) = @_; + +	my $heads = $rdata->{'heads'}; +	my $fetch = $rdata->{'fetch'}; +	my $push = $rdata->{'push'}; + +	my $urls_table = "<table class=\"projects_list\">\n" ; + +	if (defined $fetch) { +		if ($fetch eq $push) { +			$urls_table .= format_repo_url("URL", $fetch); +		} else { +			$urls_table .= format_repo_url("Fetch URL", $fetch); +			$urls_table .= format_repo_url("Push URL", $push) if defined $push; +		} +	} elsif (defined $push) { +		$urls_table .= format_repo_url("Push URL", $push); +	} else { +		$urls_table .= format_repo_url("", "No remote URL"); +	} + +	$urls_table .= "</table>\n"; + +	my $dots; +	if (defined $limit && $limit < @$heads) { +		$dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "..."); +	} + +	print $urls_table; +	git_heads_body($heads, $head, 0, $limit, $dots); +} + +# Display a list of remote names with the respective fetch and push URLs +sub git_remotes_list { +	my ($remotedata, $limit) = @_; +	print "<table class=\"heads\">\n"; +	my $alternate = 1; +	my @remotes = sort keys %$remotedata; + +	my $limited = $limit && $limit < @remotes; + +	$#remotes = $limit - 1 if $limited; + +	while (my $remote = shift @remotes) { +		my $rdata = $remotedata->{$remote}; +		my $fetch = $rdata->{'fetch'}; +		my $push = $rdata->{'push'}; +		if ($alternate) { +			print "<tr class=\"dark\">\n"; +		} else { +			print "<tr class=\"light\">\n"; +		} +		$alternate ^= 1; +		print "<td>" . +		      $cgi->a({-href=> href(action=>'remotes', hash=>$remote), +			       -class=> "list name"},esc_html($remote)) . +		      "</td>"; +		print "<td class=\"link\">" . +		      (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") . +		      " | " . +		      (defined $push ? $cgi->a({-href=> $push}, "push") : "push") . +		      "</td>"; + +		print "</tr>\n"; +	} + +	if ($limited) { +		print "<tr>\n" . +		      "<td colspan=\"3\">" . +		      $cgi->a({-href => href(action=>"remotes")}, "...") . +		      "</td>\n" . "</tr>\n"; +	} + +	print "</table>"; +} + +# Display remote heads grouped by remote, unless there are too many +# remotes, in which case we only display the remote names +sub git_remotes_body { +	my ($remotedata, $limit, $head) = @_; +	if ($limit and $limit < keys %$remotedata) { +		git_remotes_list($remotedata, $limit); +	} else { +		fill_remote_heads($remotedata); +		while (my ($remote, $rdata) = each %$remotedata) { +			git_print_section({-class=>"remote", -id=>$remote}, +				["remotes", $remote, $remote], sub { +					git_remote_block($remote, $rdata, $limit, $head); +				}); +		} +	} +} + +sub git_search_message { +	my %co = @_; + +	my $greptype; +	if ($searchtype eq 'commit') { +		$greptype = "--grep="; +	} elsif ($searchtype eq 'author') { +		$greptype = "--author="; +	} elsif ($searchtype eq 'committer') { +		$greptype = "--committer="; +	} +	$greptype .= $searchtext; +	my @commitlist = parse_commits($hash, 101, (100 * $page), undef, +	                               $greptype, '--regexp-ignore-case', +	                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings'); + +	my $paging_nav = ''; +	if ($page > 0) { +		$paging_nav .= +			$cgi->a({-href => href(-replay=>1, page=>undef)}, +			        "first") . +			" ⋅ " . +			$cgi->a({-href => href(-replay=>1, page=>$page-1), +			         -accesskey => "p", -title => "Alt-p"}, "prev"); +	} else { +		$paging_nav .= "first ⋅ prev"; +	} +	my $next_link = ''; +	if ($#commitlist >= 100) { +		$next_link = +			$cgi->a({-href => href(-replay=>1, page=>$page+1), +			         -accesskey => "n", -title => "Alt-n"}, "next"); +		$paging_nav .= " ⋅ $next_link"; +	} else { +		$paging_nav .= " ⋅ next"; +	} + +	git_header_html(); + +	git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); +	git_print_header_div('commit', esc_html($co{'title'}), $hash); +	if ($page == 0 && !@commitlist) { +		print "<p>No match.</p>\n"; +	} else { +		git_search_grep_body(\@commitlist, 0, 99, $next_link); +	} + +	git_footer_html(); +} + +sub git_search_changes { +	my %co = @_; + +	local $/ = "\n"; +	open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts, +		'--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext", +		($search_use_regexp ? '--pickaxe-regex' : ()) +			or die_error(500, "Open git-log failed"); + +	git_header_html(); + +	git_print_page_nav('','', $hash,$co{'tree'},$hash); +	git_print_header_div('commit', esc_html($co{'title'}), $hash); + +	print "<table class=\"pickaxe search\">\n"; +	my $alternate = 1; +	undef %co; +	my @files; +	while (my $line = <$fd>) { +		chomp $line; +		next unless $line; + +		my %set = parse_difftree_raw_line($line); +		if (defined $set{'commit'}) { +			# finish previous commit +			if (%co) { +				print "</td>\n" . +				      "<td class=\"link\">" . +				      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, +				              "commit") . +				      " | " . +				      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, +				                             hash_base=>$co{'id'})}, +				              "tree") . +				      "</td>\n" . +				      "</tr>\n"; +			} + +			if ($alternate) { +				print "<tr class=\"dark\">\n"; +			} else { +				print "<tr class=\"light\">\n"; +			} +			$alternate ^= 1; +			%co = parse_commit($set{'commit'}); +			my $author = chop_and_escape_str($co{'author_name'}, 15, 5); +			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . +			      "<td><i>$author</i></td>\n" . +			      "<td>" . +			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), +			              -class => "list subject"}, +			              chop_and_escape_str($co{'title'}, 50) . "<br/>"); +		} elsif (defined $set{'to_id'}) { +			next if ($set{'to_id'} =~ m/^0{40}$/); + +			print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, +			                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}), +			              -class => "list"}, +			              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") . +			      "<br/>\n"; +		} +	} +	close $fd; + +	# finish last commit (warning: repetition!) +	if (%co) { +		print "</td>\n" . +		      "<td class=\"link\">" . +		      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, +		              "commit") . +		      " | " . +		      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, +		                             hash_base=>$co{'id'})}, +		              "tree") . +		      "</td>\n" . +		      "</tr>\n"; +	} + +	print "</table>\n"; + +	git_footer_html(); +} + +sub git_search_files { +	my %co = @_; + +	local $/ = "\n"; +	open my $fd, "-|", git_cmd(), 'grep', '-n', +		$search_use_regexp ? ('-E', '-i') : '-F', +		$searchtext, $co{'tree'} +			or die_error(500, "Open git-grep failed"); + +	git_header_html(); + +	git_print_page_nav('','', $hash,$co{'tree'},$hash); +	git_print_header_div('commit', esc_html($co{'title'}), $hash); + +	print "<table class=\"grep_search\">\n"; +	my $alternate = 1; +	my $matches = 0; +	my $lastfile = ''; +	while (my $line = <$fd>) { +		chomp $line; +		my ($file, $lno, $ltext, $binary); +		last if ($matches++ > 1000); +		if ($line =~ /^Binary file (.+) matches$/) { +			$file = $1; +			$binary = 1; +		} else { +			(undef, $file, $lno, $ltext) = split(/:/, $line, 4); +		} +		if ($file ne $lastfile) { +			$lastfile and print "</td></tr>\n"; +			if ($alternate++) { +				print "<tr class=\"dark\">\n"; +			} else { +				print "<tr class=\"light\">\n"; +			} +			print "<td class=\"list\">". +				$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, +						       file_name=>"$file"), +					-class => "list"}, esc_path($file)); +			print "</td><td>\n"; +			$lastfile = $file; +		} +		if ($binary) { +			print "<div class=\"binary\">Binary file</div>\n"; +		} else { +			$ltext = untabify($ltext); +			if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) { +				$ltext = esc_html($1, -nbsp=>1); +				$ltext .= '<span class="match">'; +				$ltext .= esc_html($2, -nbsp=>1); +				$ltext .= '</span>'; +				$ltext .= esc_html($3, -nbsp=>1); +			} else { +				$ltext = esc_html($ltext, -nbsp=>1); +			} +			print "<div class=\"pre\">" . +				$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, +						       file_name=>"$file").'#l'.$lno, +					-class => "linenr"}, sprintf('%4i', $lno)) +				. ' ' .  $ltext . "</div>\n"; +		} +	} +	if ($lastfile) { +		print "</td></tr>\n"; +		if ($matches > 1000) { +			print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n"; +		} +	} else { +		print "<div class=\"diff nodifferences\">No matches found</div>\n"; +	} +	close $fd; + +	print "</table>\n"; + +	git_footer_html(); +} +  sub git_search_grep_body {  	my ($commitlist, $from, $to, $extra) = @_;  	$from = 0 unless defined $from; @@ -5066,7 +5863,10 @@ sub git_forks {  }  sub git_project_index { -	my @projects = git_get_projects_list($project); +	my @projects = git_get_projects_list(); +	if (!@projects) { +		die_error(404, "No projects found"); +	}  	print $cgi->header(  		-type => 'text/plain', @@ -5094,6 +5894,7 @@ sub git_summary {  	my %co = parse_commit("HEAD");  	my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();  	my $head = $co{'id'}; +	my $remote_heads = gitweb_check_feature('remote_heads');  	my $owner = git_get_project_owner($project); @@ -5102,11 +5903,16 @@ sub git_summary {  	# there are more ...  	my @taglist  = git_get_tags_list(16);  	my @headlist = git_get_heads_list(16); +	my %remotedata = $remote_heads ? git_get_remotes_list() : ();  	my @forklist;  	my $check_forks = gitweb_check_feature('forks');  	if ($check_forks) { +		# find forks of a project  		@forklist = git_get_projects_list($project); +		# filter out forks of forks +		@forklist = filter_forks_from_projects_list(\@forklist) +			if (@forklist);  	}  	git_header_html(); @@ -5117,7 +5923,8 @@ sub git_summary {  	      "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .  	      "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";  	if (defined $cd{'rfc2822'}) { -		print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n"; +		print "<tr id=\"metadata_lchange\"><td>last change</td>" . +		      "<td>".format_timestamp_html(\%cd)."</td></tr>\n";  	}  	# use per project git URL list in $projectroot/$project/cloneurl @@ -5127,7 +5934,7 @@ sub git_summary {  	@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;  	foreach my $git_url (@url_list) {  		next unless $git_url; -		print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n"; +		print format_repo_url($url_tag, $git_url);  		$url_tag = "";  	} @@ -5135,13 +5942,14 @@ sub git_summary {  	my $show_ctags = gitweb_check_feature('ctags');  	if ($show_ctags) {  		my $ctags = git_get_project_ctags($project); -		my $cloud = git_populate_project_tagcloud($ctags); -		print "<tr id=\"metadata_ctags\"><td>Content tags:<br />"; -		print "</td>\n<td>" unless %$ctags; -		print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"; -		print "</td>\n<td>" if %$ctags; -		print git_show_project_tagcloud($cloud, 48); -		print "</td></tr>"; +		if (%$ctags) { +			# without ability to add tags, don't show if there are none +			my $cloud = git_populate_project_tagcloud($ctags); +			print "<tr id=\"metadata_ctags\">" . +			      "<td>content tags</td>" . +			      "<td>".git_show_project_tagcloud($cloud, 48)."</td>" . +			      "</tr>\n"; +		}  	}  	print "</table>\n"; @@ -5179,6 +5987,11 @@ sub git_summary {  		               $cgi->a({-href => href(action=>"heads")}, "..."));  	} +	if (%remotedata) { +		git_print_header_div('remotes'); +		git_remotes_body(\%remotedata, 15, $head); +	} +  	if (@forklist) {  		git_print_header_div('forks');  		git_project_list_body(\@forklist, 'age', 0, 15, @@ -5283,7 +6096,7 @@ sub git_blame_common {  		print 'END';  		if (defined $t0 && gitweb_check_feature('timed')) {  			print ' '. -			      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]). +			      tv_interval($t0, [ gettimeofday() ]).  			      ' '.$number_of_git_cmds;  		}  		print "\n"; @@ -5470,7 +6283,7 @@ sub git_blame_data {  sub git_tags {  	my $head = git_get_head_hash($project);  	git_header_html(); -	git_print_page_nav('','', $head,undef,$head); +	git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));  	git_print_header_div('summary', $project);  	my @tagslist = git_get_tags_list(); @@ -5483,7 +6296,7 @@ sub git_tags {  sub git_heads {  	my $head = git_get_head_hash($project);  	git_header_html(); -	git_print_page_nav('','', $head,undef,$head); +	git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));  	git_print_header_div('summary', $project);  	my @headslist = git_get_heads_list(); @@ -5493,6 +6306,39 @@ sub git_heads {  	git_footer_html();  } +# used both for single remote view and for list of all the remotes +sub git_remotes { +	gitweb_check_feature('remote_heads') +		or die_error(403, "Remote heads view is disabled"); + +	my $head = git_get_head_hash($project); +	my $remote = $input_params{'hash'}; + +	my $remotedata = git_get_remotes_list($remote); +	die_error(500, "Unable to get remote information") unless defined $remotedata; + +	unless (%$remotedata) { +		die_error(404, defined $remote ? +			"Remote $remote not found" : +			"No remotes found"); +	} + +	git_header_html(undef, undef, -action_extra => $remote); +	git_print_page_nav('', '',  $head, undef, $head, +		format_ref_views($remote ? '' : 'remotes')); + +	fill_remote_heads($remotedata); +	if (defined $remote) { +		git_print_header_div('remotes', "$remote remote for $project"); +		git_remote_block($remote, $remotedata->{$remote}, undef, $head); +	} else { +		git_print_header_div('summary', "$project remotes"); +		git_remotes_body($remotedata, undef, $head); +	} + +	git_footer_html(); +} +  sub git_blob_plain {  	my $type = shift;  	my $expires; @@ -5531,7 +6377,16 @@ sub git_blob_plain {  	# want to be sure not to break that by serving the image as an  	# attachment (though Firefox 3 doesn't seem to care).  	my $sandbox = $prevent_xss && -		$type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!; +		$type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!; + +	# serve text/* as text/plain +	if ($prevent_xss && +	    ($type =~ m!^text/[a-z]+\b(.*)$! || +	     ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) { +		my $rest = $1; +		$rest = defined $rest ? $rest : ''; +		$type = "text/plain$rest"; +	}  	print $cgi->header(  		-type => $type, @@ -5609,14 +6464,14 @@ sub git_blob {  	} else {  		print "<div class=\"page_nav\">\n" .  		      "<br/><br/></div>\n" . -		      "<div class=\"title\">$hash</div>\n"; +		      "<div class=\"title\">".esc_html($hash)."</div>\n";  	}  	git_print_page_path($file_name, "blob", $hash_base);  	print "<div class=\"page_body\">\n";  	if ($mimetype =~ m!^image/!) { -		print qq!<img type="$mimetype"!; +		print qq!<img type="!.esc_attr($mimetype).qq!"!;  		if ($file_name) { -			print qq! alt="$file_name" title="$file_name"!; +			print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;  		}  		print qq! src="! .  		      href(action=>"blob_plain", hash=>$hash, @@ -5629,7 +6484,7 @@ sub git_blob {  			$nr++;  			$line = untabify($line);  			printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!, -			       $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1); +			       $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? to_utf8($line) : esc_html($line, -nbsp=>1);  		}  	}  	close $fd @@ -5691,7 +6546,7 @@ sub git_tree {  		undef $hash_base;  		print "<div class=\"page_nav\">\n";  		print "<br/><br/></div>\n"; -		print "<div class=\"title\">$hash</div>\n"; +		print "<div class=\"title\">".esc_html($hash)."</div>\n";  	}  	if (defined $file_name) {  		$basedir = $file_name; @@ -6159,7 +7014,7 @@ sub git_blobdiff {  			git_print_header_div('commit', esc_html($co{'title'}), $hash_base);  		} else {  			print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n"; -			print "<div class=\"title\">$hash vs $hash_parent</div>\n"; +			print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";  		}  		if (defined $file_name) {  			git_print_page_path($file_name, "blob", $hash_base); @@ -6454,7 +7309,23 @@ sub git_history {  }  sub git_search { -	gitweb_check_feature('search') or die_error(403, "Search is disabled"); +	$searchtype ||= 'commit'; + +	# check if appropriate features are enabled +	gitweb_check_feature('search') +		or die_error(403, "Search is disabled"); +	if ($searchtype eq 'pickaxe') { +		# pickaxe may take all resources of your box and run for several minutes +		# with every query - so decide by yourself how public you make this feature +		gitweb_check_feature('pickaxe') +			or die_error(403, "Pickaxe search is disabled"); +	} +	if ($searchtype eq 'grep') { +		# grep search might be potentially CPU-intensive, too +		gitweb_check_feature('grep') +			or die_error(403, "Grep search is disabled"); +	} +  	if (!defined $searchtext) {  		die_error(400, "Text field is empty");  	} @@ -6469,205 +7340,17 @@ sub git_search {  		$page = 0;  	} -	$searchtype ||= 'commit'; -	if ($searchtype eq 'pickaxe') { -		# pickaxe may take all resources of your box and run for several minutes -		# with every query - so decide by yourself how public you make this feature -		gitweb_check_feature('pickaxe') -		    or die_error(403, "Pickaxe is disabled"); -	} -	if ($searchtype eq 'grep') { -		gitweb_check_feature('grep') -		    or die_error(403, "Grep is disabled"); -	} - -	git_header_html(); - -	if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') { -		my $greptype; -		if ($searchtype eq 'commit') { -			$greptype = "--grep="; -		} elsif ($searchtype eq 'author') { -			$greptype = "--author="; -		} elsif ($searchtype eq 'committer') { -			$greptype = "--committer="; -		} -		$greptype .= $searchtext; -		my @commitlist = parse_commits($hash, 101, (100 * $page), undef, -		                               $greptype, '--regexp-ignore-case', -		                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings'); - -		my $paging_nav = ''; -		if ($page > 0) { -			$paging_nav .= -				$cgi->a({-href => href(action=>"search", hash=>$hash, -				                       searchtext=>$searchtext, -				                       searchtype=>$searchtype)}, -				        "first"); -			$paging_nav .= " ⋅ " . -				$cgi->a({-href => href(-replay=>1, page=>$page-1), -				         -accesskey => "p", -title => "Alt-p"}, "prev"); -		} else { -			$paging_nav .= "first"; -			$paging_nav .= " ⋅ prev"; -		} -		my $next_link = ''; -		if ($#commitlist >= 100) { -			$next_link = -				$cgi->a({-href => href(-replay=>1, page=>$page+1), -				         -accesskey => "n", -title => "Alt-n"}, "next"); -			$paging_nav .= " ⋅ $next_link"; -		} else { -			$paging_nav .= " ⋅ next"; -		} - -		git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); -		git_print_header_div('commit', esc_html($co{'title'}), $hash); -		if ($page == 0 && !@commitlist) { -			print "<p>No match.</p>\n"; -		} else { -			git_search_grep_body(\@commitlist, 0, 99, $next_link); -		} -	} - -	if ($searchtype eq 'pickaxe') { -		git_print_page_nav('','', $hash,$co{'tree'},$hash); -		git_print_header_div('commit', esc_html($co{'title'}), $hash); - -		print "<table class=\"pickaxe search\">\n"; -		my $alternate = 1; -		local $/ = "\n"; -		open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts, -			'--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext", -			($search_use_regexp ? '--pickaxe-regex' : ()); -		undef %co; -		my @files; -		while (my $line = <$fd>) { -			chomp $line; -			next unless $line; - -			my %set = parse_difftree_raw_line($line); -			if (defined $set{'commit'}) { -				# finish previous commit -				if (%co) { -					print "</td>\n" . -					      "<td class=\"link\">" . -					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . -					      " | " . -					      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); -					print "</td>\n" . -					      "</tr>\n"; -				} - -				if ($alternate) { -					print "<tr class=\"dark\">\n"; -				} else { -					print "<tr class=\"light\">\n"; -				} -				$alternate ^= 1; -				%co = parse_commit($set{'commit'}); -				my $author = chop_and_escape_str($co{'author_name'}, 15, 5); -				print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . -				      "<td><i>$author</i></td>\n" . -				      "<td>" . -				      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -				              -class => "list subject"}, -				              chop_and_escape_str($co{'title'}, 50) . "<br/>"); -			} elsif (defined $set{'to_id'}) { -				next if ($set{'to_id'} =~ m/^0{40}$/); - -				print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, -				                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}), -				              -class => "list"}, -				              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") . -				      "<br/>\n"; -			} -		} -		close $fd; - -		# finish last commit (warning: repetition!) -		if (%co) { -			print "</td>\n" . -			      "<td class=\"link\">" . -			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . -			      " | " . -			      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); -			print "</td>\n" . -			      "</tr>\n"; -		} - -		print "</table>\n"; -	} - -	if ($searchtype eq 'grep') { -		git_print_page_nav('','', $hash,$co{'tree'},$hash); -		git_print_header_div('commit', esc_html($co{'title'}), $hash); - -		print "<table class=\"grep_search\">\n"; -		my $alternate = 1; -		my $matches = 0; -		local $/ = "\n"; -		open my $fd, "-|", git_cmd(), 'grep', '-n', -			$search_use_regexp ? ('-E', '-i') : '-F', -			$searchtext, $co{'tree'}; -		my $lastfile = ''; -		while (my $line = <$fd>) { -			chomp $line; -			my ($file, $lno, $ltext, $binary); -			last if ($matches++ > 1000); -			if ($line =~ /^Binary file (.+) matches$/) { -				$file = $1; -				$binary = 1; -			} else { -				(undef, $file, $lno, $ltext) = split(/:/, $line, 4); -			} -			if ($file ne $lastfile) { -				$lastfile and print "</td></tr>\n"; -				if ($alternate++) { -					print "<tr class=\"dark\">\n"; -				} else { -					print "<tr class=\"light\">\n"; -				} -				print "<td class=\"list\">". -					$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, -							       file_name=>"$file"), -						-class => "list"}, esc_path($file)); -				print "</td><td>\n"; -				$lastfile = $file; -			} -			if ($binary) { -				print "<div class=\"binary\">Binary file</div>\n"; -			} else { -				$ltext = untabify($ltext); -				if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) { -					$ltext = esc_html($1, -nbsp=>1); -					$ltext .= '<span class="match">'; -					$ltext .= esc_html($2, -nbsp=>1); -					$ltext .= '</span>'; -					$ltext .= esc_html($3, -nbsp=>1); -				} else { -					$ltext = esc_html($ltext, -nbsp=>1); -				} -				print "<div class=\"pre\">" . -					$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, -							       file_name=>"$file").'#l'.$lno, -						-class => "linenr"}, sprintf('%4i', $lno)) -					. ' ' .  $ltext . "</div>\n"; -			} -		} -		if ($lastfile) { -			print "</td></tr>\n"; -			if ($matches > 1000) { -				print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n"; -			} -		} else { -			print "<div class=\"diff nodifferences\">No matches found</div>\n"; -		} -		close $fd; - -		print "</table>\n"; +	if ($searchtype eq 'commit' || +	    $searchtype eq 'author' || +	    $searchtype eq 'committer') { +		git_search_message(%co); +	} elsif ($searchtype eq 'pickaxe') { +		git_search_changes(%co); +	} elsif ($searchtype eq 'grep') { +		git_search_files(%co); +	} else { +		die_error(400, "Unknown search type");  	} -	git_footer_html();  }  sub git_search_help { @@ -6747,7 +7430,7 @@ sub git_feed {  	if (defined($commitlist[0])) {  		%latest_commit = %{$commitlist[0]};  		my $latest_epoch = $latest_commit{'committer_epoch'}; -		%latest_date   = parse_date($latest_epoch); +		%latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});  		my $if_modified = $cgi->http('IF_MODIFIED_SINCE');  		if (defined $if_modified) {  			my $since; @@ -6857,7 +7540,7 @@ XML  		if (defined $favicon) {  			print "<icon>" . esc_url($favicon) . "</icon>\n";  		} -		if (defined $logo_url) { +		if (defined $logo) {  			# not twice as wide as tall: 72 x 27 pixels  			print "<logo>" . esc_url($logo) . "</logo>\n";  		} @@ -6878,7 +7561,7 @@ XML  		if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {  			last;  		} -		my %cd = parse_date($co{'author_epoch'}); +		my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});  		# get list of changed files  		open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, @@ -6988,6 +7671,9 @@ sub git_atom {  sub git_opml {  	my @list = git_get_projects_list(); +	if (!@list) { +		die_error(404, "No projects found"); +	}  	print $cgi->header(  		-type => 'text/xml', | 
