diff options
Diffstat (limited to 'gitweb/gitweb.perl')
| -rwxr-xr-x | gitweb/gitweb.perl | 1021 | 
1 files changed, 761 insertions, 260 deletions
| diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 20ace61b6d..7a5b23acf2 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -20,6 +20,10 @@ use File::Basename qw(basename);  use Time::HiRes qw(gettimeofday tv_interval);  binmode STDOUT, ':utf8'; +if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) { +	eval 'sub CGI::multi_param { CGI::param(@_) }' +} +  our $t0 = [ gettimeofday() ];  our $number_of_git_cmds = 0; @@ -54,6 +58,11 @@ sub evaluate_uri {  	# to build the base URL ourselves:  	our $path_info = decode_utf8($ENV{"PATH_INFO"});  	if ($path_info) { +		# $path_info has already been URL-decoded by the web server, but +		# $my_url and $my_uri have not. URL-decode them so we can properly +		# strip $path_info. +		$my_url = unescape($my_url); +		$my_uri = unescape($my_uri);  		if ($my_url =~ s,\Q$path_info\E$,, &&  		    $my_uri =~ s,\Q$path_info\E$,, &&  		    defined $ENV{'SCRIPT_NAME'}) { @@ -80,6 +89,9 @@ our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";  # string of the home link on top of all pages  our $home_link_str = "++GITWEB_HOME_LINK_STR++"; +# extra breadcrumbs preceding the home link +our @extra_breadcrumbs = (); +  # name of your site or organization to appear in page titles  # replace this with something more descriptive for clearer bookmarks  our $site_name = "++GITWEB_SITENAME++" @@ -133,6 +145,12 @@ our $default_projects_order = "project";  # (only effective if this variable evaluates to true)  our $export_ok = "++GITWEB_EXPORT_OK++"; +# don't generate age column on the projects list page +our $omit_age_column = 0; + +# don't generate information about owners of repositories +our $omit_owner=0; +  # show repository only if this subroutine returns true  # when given the path to the project, for example:  #    sub { return -e "$_[0]/git-daemon-export-ok"; } @@ -259,16 +277,15 @@ our %highlight_basename = (  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 sql make), +	(map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),  	# 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 php5 phps), -	map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi' -	map { $_ => 'make'} qw(mak mk), -	map { $_ => 'xml' } qw(xhtml html htm), +	(map { $_ => 'c'   } qw(c h)), +	(map { $_ => 'sh'  } qw(sh bash zsh ksh)), +	(map { $_ => 'cpp' } qw(cpp cxx c++ cc)), +	(map { $_ => 'php' } qw(php php3 php4 php5 phps)), +	(map { $_ => 'pl'  } qw(pl perl pm)), # perhaps also 'cgi' +	(map { $_ => 'make'} qw(make mak mk)), +	(map { $_ => 'xml' } qw(xml xhtml html htm)),  );  # You define site-wide feature defaults here; override them with @@ -530,11 +547,25 @@ our %feature = (  	# $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; +	# and in project config gitweb.remoteheads = 0|1;  	'remote_heads' => {  		'sub' => sub { feature_bool('remote_heads', @_) },  		'override' => 0,  		'default' => [0]}, + +	# Enable showing branches under other refs in addition to heads + +	# To set system wide extra branch refs have in $GITWEB_CONFIG +	# $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice']; +	# To have project specific config enable override in $GITWEB_CONFIG +	# $feature{'extra-branch-refs'}{'override'} = 1; +	# and in project config gitweb.extrabranchrefs = dirs of choice +	# Every directory is separated with whitespace. + +	'extra-branch-refs' => { +		'sub' => \&feature_extra_branch_refs, +		'override' => 0, +		'default' => []},  );  sub gitweb_get_feature { @@ -613,6 +644,21 @@ sub feature_avatar {  	return @val ? @val : @_;  } +sub feature_extra_branch_refs { +	my (@branch_refs) = @_; +	my $values = git_get_project_config('extrabranchrefs'); + +	if ($values) { +		$values = config_to_multi ($values); +		@branch_refs = (); +		foreach my $value (@{$values}) { +			push @branch_refs, split /\s+/, $value; +		} +	} + +	return @branch_refs; +} +  # checking HEAD file with -e is fragile if the repository was  # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed  # and then pruned. @@ -643,6 +689,18 @@ sub filter_snapshot_fmts {  		!$known_snapshot_formats{$_}{'disabled'}} @fmts;  } +sub filter_and_validate_refs { +	my @refs = @_; +	my %unique_refs = (); + +	foreach my $ref (@refs) { +		die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref)); +		# 'heads' are added implicitly in get_branch_refs(). +		$unique_refs{$ref} = 1 if ($ref ne 'heads'); +	} +	return sort keys %unique_refs; +} +  # 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. @@ -673,7 +731,7 @@ sub evaluate_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. +	# Protect against 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); @@ -760,6 +818,7 @@ our @cgi_param_mapping = (  	search_use_regexp => "sr",  	ctag => "by_tag",  	diff_style => "ds", +	project_filter => "pf",  	# this must be last entry (for manipulation from JavaScript)  	javascript => "js"  ); @@ -816,7 +875,7 @@ sub evaluate_query_params {  	while (my ($name, $symbol) = each %cgi_param_mapping) {  		if ($symbol eq 'opt') { -			$input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ]; +			$input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];  		} else {  			$input_params{$name} = decode_utf8($cgi->param($symbol));  		} @@ -976,11 +1035,11 @@ sub evaluate_path_info {  our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,       $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp, -     $searchtext, $search_regexp); +     $searchtext, $search_regexp, $project_filter);  sub evaluate_and_validate_params {  	our $action = $input_params{'action'};  	if (defined $action) { -		if (!validate_action($action)) { +		if (!is_valid_action($action)) {  			die_error(400, "Invalid action parameter");  		}  	} @@ -988,22 +1047,29 @@ sub evaluate_and_validate_params {  	# parameters which are pathnames  	our $project = $input_params{'project'};  	if (defined $project) { -		if (!validate_project($project)) { +		if (!is_valid_project($project)) {  			undef $project;  			die_error(404, "No such project");  		}  	} +	our $project_filter = $input_params{'project_filter'}; +	if (defined $project_filter) { +		if (!is_valid_pathname($project_filter)) { +			die_error(404, "Invalid project_filter parameter"); +		} +	} +  	our $file_name = $input_params{'file_name'};  	if (defined $file_name) { -		if (!validate_pathname($file_name)) { +		if (!is_valid_pathname($file_name)) {  			die_error(400, "Invalid file parameter");  		}  	}  	our $file_parent = $input_params{'file_parent'};  	if (defined $file_parent) { -		if (!validate_pathname($file_parent)) { +		if (!is_valid_pathname($file_parent)) {  			die_error(400, "Invalid file parent parameter");  		}  	} @@ -1011,21 +1077,21 @@ sub evaluate_and_validate_params {  	# parameters which are refnames  	our $hash = $input_params{'hash'};  	if (defined $hash) { -		if (!validate_refname($hash)) { +		if (!is_valid_refname($hash)) {  			die_error(400, "Invalid hash parameter");  		}  	}  	our $hash_parent = $input_params{'hash_parent'};  	if (defined $hash_parent) { -		if (!validate_refname($hash_parent)) { +		if (!is_valid_refname($hash_parent)) {  			die_error(400, "Invalid hash parent parameter");  		}  	}  	our $hash_base = $input_params{'hash_base'};  	if (defined $hash_base) { -		if (!validate_refname($hash_base)) { +		if (!is_valid_refname($hash_base)) {  			die_error(400, "Invalid hash base parameter");  		}  	} @@ -1045,7 +1111,7 @@ sub evaluate_and_validate_params {  	our $hash_parent_base = $input_params{'hash_parent_base'};  	if (defined $hash_parent_base) { -		if (!validate_refname($hash_parent_base)) { +		if (!is_valid_refname($hash_parent_base)) {  			die_error(400, "Invalid hash parent base parameter");  		}  	} @@ -1068,7 +1134,7 @@ sub evaluate_and_validate_params {  	our $search_use_regexp = $input_params{'search_use_regexp'};  	our $searchtext = $input_params{'searchtext'}; -	our $search_regexp; +	our $search_regexp = undef;  	if (defined $searchtext) {  		if (length($searchtext) < 2) {  			die_error(403, "At least two characters are required for search parameter"); @@ -1092,7 +1158,7 @@ sub evaluate_git_dir {  	our $git_dir = "$projectroot/$project" if $project;  } -our (@snapshot_fmts, $git_avatar); +our (@snapshot_fmts, $git_avatar, @extra_branch_refs);  sub configure_gitweb_features {  	# list of supported snapshot formats  	our @snapshot_fmts = gitweb_get_feature('snapshot'); @@ -1110,6 +1176,13 @@ sub configure_gitweb_features {  	} else {  		$git_avatar = '';  	} + +	our @extra_branch_refs = gitweb_get_feature('extra-branch-refs'); +	@extra_branch_refs = filter_and_validate_refs (@extra_branch_refs); +} + +sub get_branch_refs { +	return ('heads', @extra_branch_refs);  }  # custom error handler: 'die <message>' is Internal Server Error @@ -1118,7 +1191,7 @@ sub handle_errors_html {  	# to avoid infinite loop where error occurs in die_error,  	# change handler to default handler, disabling handle_errors_html -	set_message("Error occured when inside die_error:\n$msg"); +	set_message("Error occurred when inside die_error:\n$msg");  	# you cannot jump out of die_error when called as error handler;  	# the subroutine set via CGI::Carp::set_message is called _after_ @@ -1132,8 +1205,10 @@ sub dispatch {  	if (!defined $action) {  		if (defined $hash) {  			$action = git_get_type($hash); +			$action or die_error(404, "Object does not exist");  		} elsif (defined $hash_base && defined $file_name) {  			$action = git_get_type("$hash_base:$file_name"); +			$action or die_error(404, "File or directory does not exist");  		} elsif (defined $project) {  			$action = 'summary';  		} else { @@ -1395,28 +1470,31 @@ sub href {  ## ======================================================================  ## validation, quoting/unquoting and escaping -sub validate_action { -	my $input = shift || return undef; +sub is_valid_action { +	my $input = shift;  	return undef unless exists $actions{$input}; -	return $input; +	return 1;  } -sub validate_project { -	my $input = shift || return undef; -	if (!validate_pathname($input) || +sub is_valid_project { +	my $input = shift; + +	return unless defined $input; +	if (!is_valid_pathname($input) ||  		!(-d "$projectroot/$input") ||  		!check_export_ok("$projectroot/$input") ||  		($strict_export && !project_in_list($input))) {  		return undef;  	} else { -		return $input; +		return 1;  	}  } -sub validate_pathname { -	my $input = shift || return undef; +sub is_valid_pathname { +	my $input = shift; -	# no '.' or '..' as elements of path, i.e. no '.' nor '..' +	return undef unless defined $input; +	# no '.' or '..' as elements of path, i.e. no '.' or '..'  	# at the beginning, at the end, and between slashes.  	# also this catches doubled slashes  	if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { @@ -1426,24 +1504,33 @@ sub validate_pathname {  	if ($input =~ m!\0!) {  		return undef;  	} -	return $input; +	return 1;  } -sub validate_refname { -	my $input = shift || return undef; +sub is_valid_ref_format { +	my $input = shift; -	# textual hashes are O.K. -	if ($input =~ m/^[0-9a-fA-F]{40}$/) { -		return $input; -	} -	# it must be correct pathname -	$input = validate_pathname($input) -		or return undef; +	return undef unless defined $input;  	# restrictions on ref name according to git-check-ref-format  	if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {  		return undef;  	} -	return $input; +	return 1; +} + +sub is_valid_refname { +	my $input = shift; + +	return undef unless defined $input; +	# textual hashes are O.K. +	if ($input =~ m/^[0-9a-fA-F]{40}$/) { +		return 1; +	} +	# it must be correct pathname +	is_valid_pathname($input) or return undef; +	# check git-check-ref-format restrictions +	is_valid_ref_format($input) or return undef; +	return 1;  }  # decode sequences of octets in utf8 into Perl's internal form, @@ -1536,7 +1623,7 @@ sub sanitize {  	return undef unless defined $str;  	$str = to_utf8($str); -	$str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg; +	$str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;  	return $str;  } @@ -1714,6 +1801,97 @@ sub chop_and_escape_str {  	}  } +# Highlight selected fragments of string, using given CSS class, +# and escape HTML.  It is assumed that fragments do not overlap. +# Regions are passed as list of pairs (array references). +# +# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns +# '<span class="mark">foo</span>bar' +sub esc_html_hl_regions { +	my ($str, $css_class, @sel) = @_; +	my %opts = grep { ref($_) ne 'ARRAY' } @sel; +	@sel     = grep { ref($_) eq 'ARRAY' } @sel; +	return esc_html($str, %opts) unless @sel; + +	my $out = ''; +	my $pos = 0; + +	for my $s (@sel) { +		my ($begin, $end) = @$s; + +		# Don't create empty <span> elements. +		next if $end <= $begin; + +		my $escaped = esc_html(substr($str, $begin, $end - $begin), +		                       %opts); + +		$out .= esc_html(substr($str, $pos, $begin - $pos), %opts) +			if ($begin - $pos > 0); +		$out .= $cgi->span({-class => $css_class}, $escaped); + +		$pos = $end; +	} +	$out .= esc_html(substr($str, $pos), %opts) +		if ($pos < length($str)); + +	return $out; +} + +# return positions of beginning and end of each match +sub matchpos_list { +	my ($str, $regexp) = @_; +	return unless (defined $str && defined $regexp); + +	my @matches; +	while ($str =~ /$regexp/g) { +		push @matches, [$-[0], $+[0]]; +	} +	return @matches; +} + +# highlight match (if any), and escape HTML +sub esc_html_match_hl { +	my ($str, $regexp) = @_; +	return esc_html($str) unless defined $regexp; + +	my @matches = matchpos_list($str, $regexp); +	return esc_html($str) unless @matches; + +	return esc_html_hl_regions($str, 'match', @matches); +} + + +# highlight match (if any) of shortened string, and escape HTML +sub esc_html_match_hl_chopped { +	my ($str, $chopped, $regexp) = @_; +	return esc_html_match_hl($str, $regexp) unless defined $chopped; + +	my @matches = matchpos_list($str, $regexp); +	return esc_html($chopped) unless @matches; + +	# filter matches so that we mark chopped string +	my $tail = "... "; # see chop_str +	unless ($chopped =~ s/\Q$tail\E$//) { +		$tail = ''; +	} +	my $chop_len = length($chopped); +	my $tail_len = length($tail); +	my @filtered; + +	for my $m (@matches) { +		if ($m->[0] > $chop_len) { +			push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0); +			last; +		} elsif ($m->[1] > $chop_len) { +			push @filtered, [ $m->[0], $chop_len + $tail_len ]; +			last; +		} +		push @filtered, $m; +	} + +	return esc_html_hl_regions($chopped . $tail, 'match', @filtered); +} +  ## ----------------------------------------------------------------------  ## functions returning short strings @@ -1957,7 +2135,7 @@ sub picon_url {  	if (!$avatar_cache{$email}) {  		my ($user, $domain) = split('@', $email);  		$avatar_cache{$email} = -			"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" . +			"//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .  			"$domain/$user/" .  			"users+domains+unknown/up/single";  	} @@ -1972,7 +2150,7 @@ sub gravatar_url {  	my $email = lc shift;  	my $size = shift;  	$avatar_cache{$email} ||= -		"http://www.gravatar.com/avatar/" . +		"//www.gravatar.com/avatar/" .  			Digest::MD5::md5_hex($email) . "?s=";  	return $avatar_cache{$email} . $size;  } @@ -2329,26 +2507,32 @@ sub format_cc_diff_chunk_header {  }  # process patch (diff) line (not to be used for diff headers), -# returning class and HTML-formatted (but not wrapped) line -sub process_diff_line { -	my $line = shift; -	my ($from, $to) = @_; - -	my $diff_class = diff_line_class($line, $from, $to); - -	chomp $line; -	$line = untabify($line); +# returning HTML-formatted (but not wrapped) line. +# If the line is passed as a reference, it is treated as HTML and not +# esc_html()'ed. +sub format_diff_line { +	my ($line, $diff_class, $from, $to) = @_; + +	if (ref($line)) { +		$line = $$line; +	} else { +		chomp $line; +		$line = untabify($line); -	if ($from && $to && $line =~ m/^\@{2} /) { -		$line = format_unidiff_chunk_header($line, $from, $to); -		return $diff_class, $line; +		if ($from && $to && $line =~ m/^\@{2} /) { +			$line = format_unidiff_chunk_header($line, $from, $to); +		} elsif ($from && $to && $line =~ m/^\@{3}/) { +			$line = format_cc_diff_chunk_header($line, $from, $to); +		} else { +			$line = esc_html($line, -nbsp=>1); +		} +	} -	} elsif ($from && $to && $line =~ m/^\@{3}/) { -		$line = format_cc_diff_chunk_header($line, $from, $to); -		return $diff_class, $line; +	my $diff_classes = "diff"; +	$diff_classes .= " $diff_class" if ($diff_class); +	$line = "<div class=\"$diff_classes\">$line</div>\n"; -	} -	return $diff_class, esc_html($line, -nbsp=>1); +	return $line;  }  # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)", @@ -2395,19 +2579,25 @@ sub format_snapshot_links {  sub get_feed_info {  	my $format = shift || 'Atom';  	my %res = (action => lc($format)); +	my $matched_ref = 0;  	# feed links are possible only for project views  	return unless (defined $project);  	# some views should link to OPML, or to generic project feed,  	# or don't have specific feed yet (so they should use generic) -	return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x); - -	my $branch; -	# branches refs uses 'refs/heads/' prefix (fullname) to differentiate -	# from tag links; this also makes possible to detect branch links -	if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) || -	    (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) { -		$branch = $1; +	return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x); + +	my $branch = undef; +	# branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix +	# (fullname) to differentiate from tag links; this also makes +	# possible to detect branch links +	for my $ref (get_branch_refs()) { +		if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) || +		    (defined $hash      && $hash      =~ m!^refs/\Q$ref\E/(.*)$!)) { +			$branch = $1; +			$matched_ref = $ref; +			last; +		}  	}  	# find log type for feed description (title)  	my $type = 'log'; @@ -2420,7 +2610,7 @@ sub get_feed_info {  	}  	$res{-title} = $type; -	$res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef); +	$res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);  	$res{'file_name'} = $file_name;  	return %res; @@ -2579,12 +2769,15 @@ sub git_get_project_config {  	# only subsection, if exists, is case sensitive,  	# and not lowercased by 'git config -z -l'  	if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) { +		$lo =~ s/_//g;  		$key = join(".", lc($hi), $mi, lc($lo)); +		return if ($lo =~ /\W/ || $hi =~ /\W/);  	} else {  		$key = lc($key); +		$key =~ s/_//g; +		return if ($key =~ /\W/);  	}  	$key =~ s/^gitweb\.//; -	return if ($key =~ m/\W/);  	# type sanity check  	if (defined $type) { @@ -2836,10 +3029,9 @@ sub git_get_project_url_list {  sub git_get_projects_list {  	my $filter = shift || ''; +	my $paranoid = shift;  	my @list; -	$filter =~ s/\.git$//; -  	if (-d $projects_list) {  		# search in directory  		my $dir = $projects_list; @@ -2848,7 +3040,7 @@ sub git_get_projects_list {  		my $pfxlen = length("$dir");  		my $pfxdepth = ($dir =~ tr!/!!);  		# when filtering, search only given subdirectory -		if ($filter) { +		if ($filter && !$paranoid) {  			$dir .= "/$filter";  			$dir =~ s!/+$!!;  		} @@ -2873,6 +3065,10 @@ sub git_get_projects_list {  				}  				my $path = substr($File::Find::name, $pfxlen + 1); +				# paranoidly only filter here +				if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) { +					next; +				}  				# we check related file in $projectroot  				if (check_export_ok("$projectroot/$path")) {  					push @list, { path => $path }; @@ -2902,9 +3098,11 @@ sub git_get_projects_list {  			}  			if (check_export_ok("$projectroot/$path")) {  				my $pr = { -					path => $path, -					owner => to_utf8($owner), +					path => $path  				}; +				if ($owner) { +					$pr->{'owner'} = to_utf8($owner); +				}  				push @list, $pr;  			}  		} @@ -2978,11 +3176,15 @@ sub filter_forks_from_projects_list {  sub search_projects_list {  	my ($projlist, %opts) = @_;  	my $tagfilter  = $opts{'tagfilter'}; -	my $searchtext = $opts{'searchtext'}; +	my $search_re = $opts{'search_regexp'};  	return @$projlist -		unless ($tagfilter || $searchtext); +		unless ($tagfilter || $search_re); +	# searching projects require filling to be run before it; +	fill_project_list_info($projlist, +	                       $tagfilter  ? 'ctags' : (), +	                       $search_re ? ('path', 'descr') : ());  	my @projects;   PROJECT:  	foreach my $pr (@$projlist) { @@ -2993,10 +3195,10 @@ sub search_projects_list {  				grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};  		} -		if ($searchtext) { +		if ($search_re) {  			next unless -				$pr->{'path'} =~ /$searchtext/ || -				$pr->{'descr_long'} =~ /$searchtext/; +				$pr->{'path'} =~ /$search_re/ || +				$pr->{'descr_long'} =~ /$search_re/;  		}  		push @projects, $pr; @@ -3061,7 +3263,7 @@ sub git_get_last_activity {  	     '--format=%(committer)',  	     '--sort=-committerdate',  	     '--count=1', -	     'refs/heads') or return; +	     map { "refs/$_" } get_branch_refs ()) or return;  	my $most_recent = <$fd>;  	close $fd or return;  	if (defined $most_recent && @@ -3512,7 +3714,7 @@ sub parse_from_to_diffinfo {  sub git_get_heads_list {  	my ($limit, @classes) = @_; -	@classes = ('heads') unless @classes; +	@classes = get_branch_refs() unless @classes;  	my @patterns = map { "refs/$_" } @classes;  	my @headslist; @@ -3530,9 +3732,16 @@ sub git_get_heads_list {  		my ($committer, $epoch, $tz) =  			($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);  		$ref_item{'fullname'}  = $name; -		$name =~ s!^refs/(?:head|remote)s/!!; +		my $strip_refs = join '|', map { quotemeta } get_branch_refs(); +		$name =~ s!^refs/($strip_refs|remotes)/!!; +		$ref_item{'name'} = $name; +		# for refs neither in 'heads' nor 'remotes' we want to +		# show their ref dir +		my $ref_dir = (defined $1) ? $1 : ''; +		if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { +		    $ref_item{'name'} .= ' (' . $ref_dir . ')'; +		} -		$ref_item{'name'}  = $name;  		$ref_item{'id'}    = $hash;  		$ref_item{'title'} = $title || '(no commit message)';  		$ref_item{'epoch'} = $epoch; @@ -3738,7 +3947,12 @@ sub run_highlighter {  sub get_page_title {  	my $title = to_utf8($site_name); -	return $title unless (defined $project); +	unless (defined $project) { +		if (defined $project_filter) { +			$title .= " - projects in '" . esc_path($project_filter) . "'"; +		} +		return $title; +	}  	$title .= " - " . to_utf8($project);  	return $title unless (defined $action); @@ -3782,6 +3996,7 @@ sub print_feed_meta {  				'-type' => "application/$type+xml"  			); +			$href_params{'extra_options'} = undef;  			$href_params{'action'} = $type;  			$link_attr{'-href'} = href(%href_params);  			print "<link ". @@ -3832,12 +4047,29 @@ sub print_header_links {  	}  } +sub print_nav_breadcrumbs_path { +	my $dirprefix = undef; +	while (my $part = shift) { +		$dirprefix .= "/" if defined $dirprefix; +		$dirprefix .= $part; +		print $cgi->a({-href => href(project => undef, +		                             project_filter => $dirprefix, +		                             action => "project_list")}, +			      esc_html($part)) . " / "; +	} +} +  sub print_nav_breadcrumbs {  	my %opts = @_; -	print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; +	for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) { +		print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / "; +	}  	if (defined $project) { -		print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); +		my @dirname = split '/', $project; +		my $projectbasename = pop @dirname; +		print_nav_breadcrumbs_path(@dirname); +		print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));  		if (defined $action) {  			my $action_print = $action ;  			if (defined $opts{-action_extra}) { @@ -3850,6 +4082,8 @@ sub print_nav_breadcrumbs {  			print " / $opts{-action_extra}";  		}  		print "\n"; +	} elsif (defined $project_filter) { +		print_nav_breadcrumbs_path(split '/', $project_filter);  	}  } @@ -3870,7 +4104,7 @@ sub print_search_form {  	if ($use_pathinfo) {  		$action .= "/".esc_url($project);  	} -	print $cgi->startform(-method => "get", -action => $action) . +	print $cgi->start_form(-method => "get", -action => $action) .  	      "<div class=\"search\">\n" .  	      (!$use_pathinfo &&  	      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") . @@ -3878,8 +4112,8 @@ sub print_search_form {  	      $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->a({-href => href(action=>"search_help"), +			     -title => "search help" }, "?") . " search:\n",  	      $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .  	      "<span title=\"Extended regular expression\">" .  	      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', @@ -3972,9 +4206,11 @@ sub git_footer_html {  		}  	} else { -		print $cgi->a({-href => href(project=>undef, action=>"opml"), +		print $cgi->a({-href => href(project=>undef, action=>"opml", +		                             project_filter => $project_filter),  		              -class => $feed_class}, "OPML") . " "; -		print $cgi->a({-href => href(project=>undef, action=>"project_index"), +		print $cgi->a({-href => href(project=>undef, action=>"project_index", +		                             project_filter => $project_filter),  		              -class => $feed_class}, "TXT") . "\n";  	}  	print "</div>\n"; # class="page_footer" @@ -4337,30 +4573,33 @@ sub git_print_log {  	}  	# print log -	my $signoff = 0; -	my $empty = 0; +	my $skip_blank_line = 0;  	foreach my $line (@$log) { -		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { -			$signoff = 1; -			$empty = 0; +		if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {  			if (! $opts{'-remove_signoff'}) {  				print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n"; -				next; -			} else { -				# remove signoff lines -				next; +				$skip_blank_line = 1;  			} -		} else { -			$signoff = 0; +			next; +		} + +		if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) { +			if (! $opts{'-remove_signoff'}) { +				print "<span class=\"signoff\">" . esc_html($1) . ": " . +					"<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" . +					"</span><br/>\n"; +				$skip_blank_line = 1; +			} +			next;  		}  		# print only one empty line  		# do not print empty line after signoff  		if ($line eq "") { -			next if ($empty || $signoff); -			$empty = 1; +			next if ($skip_blank_line); +			$skip_blank_line = 1;  		} else { -			$empty = 0; +			$skip_blank_line = 0;  		}  		print format_log_line_html($line) . "<br/>\n"; @@ -4368,7 +4607,7 @@ sub git_print_log {  	if ($opts{'-final_empty_line'}) {  		# end with single empty line -		print "<br/>\n" unless $empty; +		print "<br/>\n" unless $skip_blank_line;  	}  } @@ -4870,10 +5109,186 @@ sub git_difftree_body {  	print "</table>\n";  } -sub print_sidebyside_diff_chunk { -	my @chunk = @_; +# Print context lines and then rem/add lines in a side-by-side manner. +sub print_sidebyside_diff_lines { +	my ($ctx, $rem, $add) = @_; + +	# print context block before add/rem block +	if (@$ctx) { +		print join '', +			'<div class="chunk_block ctx">', +				'<div class="old">', +				@$ctx, +				'</div>', +				'<div class="new">', +				@$ctx, +				'</div>', +			'</div>'; +	} + +	if (!@$add) { +		# pure removal +		print join '', +			'<div class="chunk_block rem">', +				'<div class="old">', +				@$rem, +				'</div>', +			'</div>'; +	} elsif (!@$rem) { +		# pure addition +		print join '', +			'<div class="chunk_block add">', +				'<div class="new">', +				@$add, +				'</div>', +			'</div>'; +	} else { +		print join '', +			'<div class="chunk_block chg">', +				'<div class="old">', +				@$rem, +				'</div>', +				'<div class="new">', +				@$add, +				'</div>', +			'</div>'; +	} +} + +# Print context lines and then rem/add lines in inline manner. +sub print_inline_diff_lines { +	my ($ctx, $rem, $add) = @_; + +	print @$ctx, @$rem, @$add; +} + +# Format removed and added line, mark changed part and HTML-format them. +# Implementation is based on contrib/diff-highlight +sub format_rem_add_lines_pair { +	my ($rem, $add, $num_parents) = @_; + +	# We need to untabify lines before split()'ing them; +	# otherwise offsets would be invalid. +	chomp $rem; +	chomp $add; +	$rem = untabify($rem); +	$add = untabify($add); + +	my @rem = split(//, $rem); +	my @add = split(//, $add); +	my ($esc_rem, $esc_add); +	# Ignore leading +/- characters for each parent. +	my ($prefix_len, $suffix_len) = ($num_parents, 0); +	my ($prefix_has_nonspace, $suffix_has_nonspace); + +	my $shorter = (@rem < @add) ? @rem : @add; +	while ($prefix_len < $shorter) { +		last if ($rem[$prefix_len] ne $add[$prefix_len]); + +		$prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/); +		$prefix_len++; +	} + +	while ($prefix_len + $suffix_len < $shorter) { +		last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]); + +		$suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/); +		$suffix_len++; +	} + +	# Mark lines that are different from each other, but have some common +	# part that isn't whitespace.  If lines are completely different, don't +	# mark them because that would make output unreadable, especially if +	# diff consists of multiple lines. +	if ($prefix_has_nonspace || $suffix_has_nonspace) { +		$esc_rem = esc_html_hl_regions($rem, 'marked', +		        [$prefix_len, @rem - $suffix_len], -nbsp=>1); +		$esc_add = esc_html_hl_regions($add, 'marked', +		        [$prefix_len, @add - $suffix_len], -nbsp=>1); +	} else { +		$esc_rem = esc_html($rem, -nbsp=>1); +		$esc_add = esc_html($add, -nbsp=>1); +	} + +	return format_diff_line(\$esc_rem, 'rem'), +	       format_diff_line(\$esc_add, 'add'); +} + +# HTML-format diff context, removed and added lines. +sub format_ctx_rem_add_lines { +	my ($ctx, $rem, $add, $num_parents) = @_; +	my (@new_ctx, @new_rem, @new_add); +	my $can_highlight = 0; +	my $is_combined = ($num_parents > 1); + +	# Highlight if every removed line has a corresponding added line. +	if (@$add > 0 && @$add == @$rem) { +		$can_highlight = 1; + +		# Highlight lines in combined diff only if the chunk contains +		# diff between the same version, e.g. +		# +		#    - a +		#   -  b +		#    + c +		#   +  d +		# +		# Otherwise the highlightling would be confusing. +		if ($is_combined) { +			for (my $i = 0; $i < @$add; $i++) { +				my $prefix_rem = substr($rem->[$i], 0, $num_parents); +				my $prefix_add = substr($add->[$i], 0, $num_parents); + +				$prefix_rem =~ s/-/+/g; + +				if ($prefix_rem ne $prefix_add) { +					$can_highlight = 0; +					last; +				} +			} +		} +	} + +	if ($can_highlight) { +		for (my $i = 0; $i < @$add; $i++) { +			my ($line_rem, $line_add) = format_rem_add_lines_pair( +			        $rem->[$i], $add->[$i], $num_parents); +			push @new_rem, $line_rem; +			push @new_add, $line_add; +		} +	} else { +		@new_rem = map { format_diff_line($_, 'rem') } @$rem; +		@new_add = map { format_diff_line($_, 'add') } @$add; +	} + +	@new_ctx = map { format_diff_line($_, 'ctx') } @$ctx; + +	return (\@new_ctx, \@new_rem, \@new_add); +} + +# Print context lines and then rem/add lines. +sub print_diff_lines { +	my ($ctx, $rem, $add, $diff_style, $num_parents) = @_; +	my $is_combined = $num_parents > 1; + +	($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add, +	        $num_parents); + +	if ($diff_style eq 'sidebyside' && !$is_combined) { +		print_sidebyside_diff_lines($ctx, $rem, $add); +	} else { +		# default 'inline' style and unknown styles +		print_inline_diff_lines($ctx, $rem, $add); +	} +} + +sub print_diff_chunk { +	my ($diff_style, $num_parents, $from, $to, @chunk) = @_;  	my (@ctx, @rem, @add); +	# The class of the previous line. +	my $prev_class = ''; +  	return unless @chunk;  	# incomplete last line might be among removed or added lines, @@ -4892,55 +5307,19 @@ sub print_sidebyside_diff_chunk {  		# print chunk headers  		if ($class && $class eq 'chunk_header') { -			print $line; +			print format_diff_line($line, $class, $from, $to);  			next;  		} -		## print from accumulator when type of class of lines change -		# empty contents block on start rem/add block, or end of chunk -		if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) { -			print join '', -				'<div class="chunk_block ctx">', -					'<div class="old">', -					@ctx, -					'</div>', -					'<div class="new">', -					@ctx, -					'</div>', -				'</div>'; -			@ctx = (); -		} -		# empty add/rem block on start context block, or end of chunk -		if ((@rem || @add) && (!$class || $class eq 'ctx')) { -			if (!@add) { -				# pure removal -				print join '', -					'<div class="chunk_block rem">', -						'<div class="old">', -						@rem, -						'</div>', -					'</div>'; -			} elsif (!@rem) { -				# pure addition -				print join '', -					'<div class="chunk_block add">', -						'<div class="new">', -						@add, -						'</div>', -					'</div>'; -			} else { -				# assume that it is change -				print join '', -					'<div class="chunk_block chg">', -						'<div class="old">', -						@rem, -						'</div>', -						'<div class="new">', -						@add, -						'</div>', -					'</div>'; -			} -			@rem = @add = (); +		## print from accumulator when have some add/rem lines or end +		# of chunk (flush context lines), or when have add and rem +		# lines and new block is reached (otherwise add/rem lines could +		# be reordered) +		if (!$class || ((@rem || @add) && $class eq 'ctx') || +		    (@rem && @add && $class ne $prev_class)) { +			print_diff_lines(\@ctx, \@rem, \@add, +		                         $diff_style, $num_parents); +			@ctx = @rem = @add = ();  		}  		## adding lines to accumulator @@ -4956,6 +5335,8 @@ sub print_sidebyside_diff_chunk {  		if ($class eq 'ctx') {  			push @ctx, $line;  		} + +		$prev_class = $class;  	}  } @@ -5077,27 +5458,19 @@ sub git_patchset_body {  			next PATCH if ($patch_line =~ m/^diff /); -			my ($class, $line) = process_diff_line($patch_line, \%from, \%to); -			my $diff_classes = "diff"; -			$diff_classes .= " $class" if ($class); -			$line = "<div class=\"$diff_classes\">$line</div>\n"; +			my $class = diff_line_class($patch_line, \%from, \%to); -			if ($diff_style eq 'sidebyside' && !$is_combined) { -				if ($class eq 'chunk_header') { -					print_sidebyside_diff_chunk(@chunk); -					@chunk = ( [ $class, $line ] ); -				} else { -					push @chunk, [ $class, $line ]; -				} -			} else { -				# default 'inline' style and unknown styles -				print $line; +			if ($class eq 'chunk_header') { +				print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk); +				@chunk = ();  			} + +			push @chunk, [ $class, $patch_line ];  		}  	} continue {  		if (@chunk) { -			print_sidebyside_diff_chunk(@chunk); +			print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);  			@chunk = ();  		}  		print "</div>\n"; # class="patch" @@ -5132,35 +5505,98 @@ sub git_patchset_body {  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -# fills project list info (age, description, owner, category, forks) +sub git_project_search_form { +	my ($searchtext, $search_use_regexp) = @_; + +	my $limit = ''; +	if ($project_filter) { +		$limit = " in '$project_filter/'"; +	} + +	print "<div class=\"projsearch\">\n"; +	print $cgi->start_form(-method => 'get', -action => $my_uri) . +	      $cgi->hidden(-name => 'a', -value => 'project_list')  . "\n"; +	print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n" +		if (defined $project_filter); +	print $cgi->textfield(-name => 's', -value => $searchtext, +	                      -title => "Search project by name and description$limit", +	                      -size => 60) . "\n" . +	      "<span title=\"Extended regular expression\">" . +	      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're', +	                     -checked => $search_use_regexp) . +	      "</span>\n" . +	      $cgi->submit(-name => 'btnS', -value => 'Search') . +	      $cgi->end_form() . "\n" . +	      $cgi->a({-href => href(project => undef, searchtext => undef, +	                             project_filter => $project_filter)}, +	              esc_html("List all projects$limit")) . "<br />\n"; +	print "</div>\n"; +} + +# entry for given @keys needs filling if at least one of keys in list +# is not present in %$project_info +sub project_info_needs_filling { +	my ($project_info, @keys) = @_; + +	# return List::MoreUtils::any { !exists $project_info->{$_} } @keys; +	foreach my $key (@keys) { +		if (!exists $project_info->{$key}) { +			return 1; +		} +	} +	return; +} + +# fills project list info (age, description, owner, category, forks, etc.)  # for each project in the list, removing invalid projects from -# returned list +# returned list, or fill only specified info. +# +# Invalid projects are removed from the returned list if and only if you +# ask 'age' or 'age_string' to be filled, because they are the only fields +# that run unconditionally git command that requires repository, and +# therefore do always check if project repository is invalid. +# +# USAGE: +# * fill_project_list_info(\@project_list, 'descr_long', 'ctags') +#   ensures that 'descr_long' and 'ctags' fields are filled +# * @project_list = fill_project_list_info(\@project_list) +#   ensures that all fields are filled (and invalid projects removed) +#  # NOTE: modifies $projlist, but does not remove entries from it  sub fill_project_list_info { -	my $projlist = shift; +	my ($projlist, @wanted_keys) = @_;  	my @projects; +	my $filter_set = sub { return @_; }; +	if (@wanted_keys) { +		my %wanted_keys = map { $_ => 1 } @wanted_keys; +		$filter_set = sub { return grep { $wanted_keys{$_} } @_; }; +	}  	my $show_ctags = gitweb_check_feature('ctags');   PROJECT:  	foreach my $pr (@$projlist) { -		my (@activity) = git_get_last_activity($pr->{'path'}); -		unless (@activity) { -			next PROJECT; +		if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) { +			my (@activity) = git_get_last_activity($pr->{'path'}); +			unless (@activity) { +				next PROJECT; +			} +			($pr->{'age'}, $pr->{'age_string'}) = @activity;  		} -		($pr->{'age'}, $pr->{'age_string'}) = @activity; -		if (!defined $pr->{'descr'}) { +		if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {  			my $descr = git_get_project_description($pr->{'path'}) || "";  			$descr = to_utf8($descr);  			$pr->{'descr_long'} = $descr;  			$pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);  		} -		if (!defined $pr->{'owner'}) { +		if (project_info_needs_filling($pr, $filter_set->('owner'))) {  			$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";  		} -		if ($show_ctags) { +		if ($show_ctags && +		    project_info_needs_filling($pr, $filter_set->('ctags'))) {  			$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});  		} -		if ($projects_list_group_categories && !defined $pr->{'category'}) { +		if ($projects_list_group_categories && +		    project_info_needs_filling($pr, $filter_set->('category'))) {  			my $cat = git_get_project_category($pr->{'path'}) ||  			                                   $project_list_default_category;  			$pr->{'category'} = to_utf8($cat); @@ -5174,23 +5610,30 @@ sub fill_project_list_info {  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; +	sub order_str { +		my $key = shift; +		return sub { $a->{$key} cmp $b->{$key} };  	} -	return @projects; +	sub order_num_then_undef { +		my $key = shift; +		return sub { +			defined $a->{$key} ? +				(defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) : +				(defined $b->{$key} ? 1 : 0) +		}; +	} + +	my %orderings = ( +		project => order_str('path'), +		descr => order_str('descr_long'), +		owner => order_str('owner'), +		age => order_num_then_undef('age'), +	); + +	my $ordering = $orderings{$order}; +	return defined $ordering ? sort $ordering @$projlist : @$projlist;  }  # returns a hash of categories, containing the list of project @@ -5264,14 +5707,25 @@ sub git_project_list_rows {  			print "</td>\n";  		}  		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), -		                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" . +		                        -class => "list"}, +		                       esc_html_match_hl($pr->{'path'}, $search_regexp)) . +		      "</td>\n" .  		      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), -		                        -class => "list", -title => $pr->{'descr_long'}}, -		                        esc_html($pr->{'descr'})) . "</td>\n" . -		      "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n"; -		print "<td class=\"". age_class($pr->{'age'}) . "\">" . -		      (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" . -		      "<td class=\"link\">" . +		                        -class => "list", +		                        -title => $pr->{'descr_long'}}, +		                        $search_regexp +		                        ? esc_html_match_hl_chopped($pr->{'descr_long'}, +		                                                    $pr->{'descr'}, $search_regexp) +		                        : esc_html($pr->{'descr'})) . +		      "</td>\n"; +		unless ($omit_owner) { +		        print "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n"; +		} +		unless ($omit_age_column) { +		        print "<td class=\"". age_class($pr->{'age'}) . "\">" . +		            (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n"; +		} +		print"<td class=\"link\">" .  		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .  		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .  		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . @@ -5291,17 +5745,21 @@ sub git_project_list_body {  	my $show_ctags  = gitweb_check_feature('ctags');  	my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;  	$check_forks = undef -		if ($tagfilter || $searchtext); +		if ($tagfilter || $search_regexp);  	# 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 +	# search_projects_list pre-fills required info  	@projects = search_projects_list(\@projects, -	                                 'searchtext' => $searchtext, +	                                 'search_regexp' => $search_regexp,  	                                 'tagfilter'  => $tagfilter) -		if ($tagfilter || $searchtext); +		if ($tagfilter || $search_regexp); +	# fill the rest +	my @all_fields = ('descr', 'descr_long', 'ctags', 'category'); +	push @all_fields, ('age', 'age_string') unless($omit_age_column); +	push @all_fields, 'owner' unless($omit_owner); +	@projects = fill_project_list_info(\@projects, @all_fields);  	$order ||= $default_projects_order;  	$from = 0 unless defined $from; @@ -5332,8 +5790,8 @@ sub git_project_list_body {  		}  		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_sort_th('owner', $order, 'Owner') unless $omit_owner; +		print_sort_th('age', $order, 'Last Change') unless $omit_age_column;  		print "<th></th>\n" . # for links  		      "</tr>\n";  	} @@ -5989,7 +6447,7 @@ sub git_project_list {  		die_error(400, "Unknown order parameter");  	} -	my @list = git_get_projects_list(); +	my @list = git_get_projects_list($project_filter, $strict_export);  	if (!@list) {  		die_error(404, "No projects found");  	} @@ -6000,11 +6458,8 @@ sub git_project_list {  		insert_file($home_text);  		print "</div>\n";  	} -	print $cgi->startform(-method => "get") . -	      "<p class=\"projsearch\">Search:\n" . -	      $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" . -	      "</p>" . -	      $cgi->end_form() . "\n"; + +	git_project_search_form($searchtext, $search_use_regexp);  	git_project_list_body(\@list, $order);  	git_footer_html();  } @@ -6015,7 +6470,9 @@ sub git_forks {  		die_error(400, "Unknown order parameter");  	} -	my @list = git_get_projects_list($project); +	my $filter = $project; +	$filter =~ s/\.git$//; +	my @list = git_get_projects_list($filter);  	if (!@list) {  		die_error(404, "No forks found");  	} @@ -6028,7 +6485,7 @@ sub git_forks {  }  sub git_project_index { -	my @projects = git_get_projects_list(); +	my @projects = git_get_projects_list($project_filter, $strict_export);  	if (!@projects) {  		die_error(404, "No projects found");  	} @@ -6074,7 +6531,9 @@ sub git_summary {  	if ($check_forks) {  		# find forks of a project -		@forklist = git_get_projects_list($project); +		my $filter = $project; +		$filter =~ s/\.git$//; +		@forklist = git_get_projects_list($filter);  		# filter out forks of forks  		@forklist = filter_forks_from_projects_list(\@forklist)  			if (@forklist); @@ -6085,8 +6544,10 @@ sub git_summary {  	print "<div class=\"title\"> </div>\n";  	print "<table class=\"projects_list\">\n" . -	      "<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"; +	      "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n"; +        if ($owner and not $omit_owner) { +	        print  "<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>".format_timestamp_html(\%cd)."</td></tr>\n"; @@ -6247,6 +6708,7 @@ sub git_blame_common {  			$hash_base, '--', $file_name  			or die_error(500, "Open git-blame --porcelain failed");  	} +	binmode $fd, ':utf8';  	# incremental blame data returns early  	if ($format eq 'data') { @@ -6636,7 +7098,7 @@ sub git_blob {  	git_print_page_path($file_name, "blob", $hash_base);  	print "<div class=\"page_body\">\n";  	if ($mimetype =~ m!^image/!) { -		print qq!<img type="!.esc_attr($mimetype).qq!"!; +		print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;  		if ($file_name) {  			print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;  		} @@ -6771,6 +7233,15 @@ sub git_tree {  	git_footer_html();  } +sub sanitize_for_filename { +    my $name = shift; + +    $name =~ s!/!-!g; +    $name =~ s/[^[:alnum:]_.-]//g; + +    return $name; +} +  sub snapshot_name {  	my ($project, $hash) = @_; @@ -6778,9 +7249,7 @@ sub snapshot_name {  	# path/to/project/.git -> project  	my $name = to_utf8($project);  	$name =~ s,([^/])/*\.git$,$1,; -	$name = basename($name); -	# sanitize name -	$name =~ s/[[:cntrl:]]/?/g; +	$name = sanitize_for_filename(basename($name));  	my $ver = $hash;  	if ($hash =~ /^[0-9a-fA-F]+$/) { @@ -6794,13 +7263,25 @@ sub snapshot_name {  		$ver = $1;  	} else {  		# branches and other need shortened SHA-1 hash -		if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) { -			$ver = $1; +		my $strip_refs = join '|', map { quotemeta } get_branch_refs(); +		if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) { +			my $ref_dir = (defined $1) ? $1 : ''; +			$ver = $2; + +			$ref_dir = sanitize_for_filename($ref_dir); +			# for refs neither in heads nor remotes we want to +			# add a ref dir to archive name +			if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') { +				$ver = $ref_dir . '-' . $ver; +			}  		}  		$ver .= '-' . git_get_short_hash($project, $hash);  	} +	# special case of sanitization for filename - we change +	# slashes to dots instead of dashes  	# in case of hierarchical branch names  	$ver =~ s!/!.!g; +	$ver =~ s/[^[:alnum:]_.-]//g;  	# name = project-version_string  	$name = "$name-$ver"; @@ -6808,6 +7289,28 @@ sub snapshot_name {  	return wantarray ? ($name, $name) : $name;  } +sub exit_if_unmodified_since { +	my ($latest_epoch) = @_; +	our $cgi; + +	my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); +	if (defined $if_modified) { +		my $since; +		if (eval { require HTTP::Date; 1; }) { +			$since = HTTP::Date::str2time($if_modified); +		} elsif (eval { require Time::ParseDate; 1; }) { +			$since = Time::ParseDate::parsedate($if_modified, GMT => 1); +		} +		if (defined $since && $latest_epoch <= $since) { +			my %latest_date = parse_date($latest_epoch); +			print $cgi->header( +				-last_modified => $latest_date{'rfc2822'}, +				-status => '304 Not Modified'); +			goto DONE_GITWEB; +		} +	} +} +  sub git_snapshot {  	my $format = $input_params{'snapshot_format'};  	if (!@snapshot_fmts) { @@ -6834,6 +7337,10 @@ sub git_snapshot {  	my ($name, $prefix) = snapshot_name($project, $hash);  	my $filename = "$name$known_snapshot_formats{$format}{'suffix'}"; + +	my %co = parse_commit($hash); +	exit_if_unmodified_since($co{'committer_epoch'}) if %co; +  	my $cmd = quote_command(  		git_cmd(), 'archive',  		"--format=$known_snapshot_formats{$format}{'format'}", @@ -6843,9 +7350,15 @@ sub git_snapshot {  	}  	$filename =~ s/(["\\])/\\$1/g; +	my %latest_date; +	if (%co) { +		%latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); +	} +  	print $cgi->header(  		-type => $known_snapshot_formats{$format}{'type'},  		-content_disposition => 'inline; filename="' . $filename . '"', +		%co ? (-last_modified => $latest_date{'rfc2822'}) : (),  		-status => '200 OK');  	open my $fd, "-|", $cmd @@ -7074,7 +7587,7 @@ sub git_object {  		system(git_cmd(), "cat-file", '-e', $hash_base) == 0  			or die_error(404, "Base object does not exist"); -		# here errors should not hapen +		# here errors should not happen  		open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name  			or die_error(500, "Open git-ls-tree failed");  		my $line = <$fd>; @@ -7625,33 +8138,14 @@ 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_commit{'comitter_tz'}); -		my $if_modified = $cgi->http('IF_MODIFIED_SINCE'); -		if (defined $if_modified) { -			my $since; -			if (eval { require HTTP::Date; 1; }) { -				$since = HTTP::Date::str2time($if_modified); -			} elsif (eval { require Time::ParseDate; 1; }) { -				$since = Time::ParseDate::parsedate($if_modified, GMT => 1); -			} -			if (defined $since && $latest_epoch <= $since) { -				print $cgi->header( -					-type => $content_type, -					-charset => 'utf-8', -					-last_modified => $latest_date{'rfc2822'}, -					-status => '304 Not Modified'); -				return; -			} -		} -		print $cgi->header( -			-type => $content_type, -			-charset => 'utf-8', -			-last_modified => $latest_date{'rfc2822'}); -	} else { -		print $cgi->header( -			-type => $content_type, -			-charset => 'utf-8'); +		exit_if_unmodified_since($latest_epoch); +		%latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});  	} +	print $cgi->header( +		-type => $content_type, +		-charset => 'utf-8', +		%latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (), +		-status => '200 OK');  	# Optimization: skip generating the body if client asks only  	# for Last-Modified date. @@ -7672,6 +8166,7 @@ sub git_feed {  		$feed_type = 'history';  	}  	$title .= " $feed_type"; +	$title = esc_html($title);  	my $descr = git_get_project_description($project);  	if (defined $descr) {  		$descr = esc_html($descr); @@ -7865,7 +8360,7 @@ sub git_atom {  }  sub git_opml { -	my @list = git_get_projects_list(); +	my @list = git_get_projects_list($project_filter, $strict_export);  	if (!@list) {  		die_error(404, "No projects found");  	} @@ -7876,11 +8371,17 @@ sub git_opml {  		-content_disposition => 'inline; filename="opml.xml"');  	my $title = esc_html($site_name); +	my $filter = " within subdirectory "; +	if (defined $project_filter) { +		$filter .= esc_html($project_filter); +	} else { +		$filter = ""; +	}  	print <<XML;  <?xml version="1.0" encoding="utf-8"?>  <opml version="1.0">  <head> -  <title>$title OPML Export</title> +  <title>$title OPML Export$filter</title>  </head>  <body>  <outline text="git RSS feeds"> | 
