diff options
Diffstat (limited to 'gitweb/gitweb.perl')
| -rwxr-xr-x | gitweb/gitweb.perl | 1703 | 
1 files changed, 1127 insertions, 576 deletions
| diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 1b9369d1a7..f80f2594cb 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -85,6 +85,8 @@ our $home_link_str = "++GITWEB_HOME_LINK_STR++";  our $site_name = "++GITWEB_SITENAME++"                   || ($ENV{'SERVER_NAME'} || "Untitled") . " Git"; +# html snippet to include in the <head> section of each page +our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";  # filename of html text to include at top of each page  our $site_header = "++GITWEB_SITE_HEADER++";  # html text to include at home page @@ -115,6 +117,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"; @@ -186,7 +196,7 @@ our %known_snapshot_formats = (  		'type' => 'application/x-gzip',  		'suffix' => '.tar.gz',  		'format' => 'tar', -		'compressor' => ['gzip']}, +		'compressor' => ['gzip', '-n']},  	'tbz2' => {  		'display' => 'tar.bz2', @@ -313,6 +323,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]}, @@ -320,6 +334,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]; @@ -334,6 +349,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]; @@ -412,20 +428,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]}, @@ -480,6 +499,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, @@ -620,18 +651,42 @@ sub filter_snapshot_fmts {  # if it is true then gitweb config would be run for each request.  our $per_request_config = 1; -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++"; +# 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. @@ -703,6 +758,8 @@ our @cgi_param_mapping = (  	snapshot_format => "sf",  	extra_options => "opt",  	search_use_regexp => "sr", +	ctag => "by_tag", +	diff_style => "ds",  	# this must be last entry (for manipulation from JavaScript)  	javascript => "js"  ); @@ -1199,11 +1256,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}) { @@ -1314,6 +1375,10 @@ sub href {  	# 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;  } @@ -1455,6 +1520,17 @@ sub esc_path {  	return $str;  } +# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0) +sub sanitize { +	my $str = shift; + +	return undef unless defined $str; + +	$str = to_utf8($str); +	$str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg; +	return $str; +} +  # Make control characters "printable", using character escape codes (CEC)  sub quot_cec {  	my $cntrl = shift; @@ -2150,93 +2226,119 @@ sub format_diff_cc_simplified {  	return $result;  } -# format patch (diff) line (not to be used for diff headers) -sub format_diff_line { -	my $line = shift; -	my ($from, $to) = @_; -	my $diff_class = ""; - -	chomp $line; +sub diff_line_class { +	my ($line, $from, $to) = @_; +	# ordinary diff +	my $num_sign = 1; +	# combined diff  	if ($from && $to && ref($from->{'href'}) eq "ARRAY") { -		# combined diff -		my $prefix = substr($line, 0, scalar @{$from->{'href'}}); -		if ($line =~ m/^\@{3}/) { -			$diff_class = " chunk_header"; -		} elsif ($line =~ m/^\\/) { -			$diff_class = " incomplete"; -		} elsif ($prefix =~ tr/+/+/) { -			$diff_class = " add"; -		} elsif ($prefix =~ tr/-/-/) { -			$diff_class = " rem"; -		} -	} else { -		# assume ordinary diff -		my $char = substr($line, 0, 1); -		if ($char eq '+') { -			$diff_class = " add"; -		} elsif ($char eq '-') { -			$diff_class = " rem"; -		} elsif ($char eq '@') { -			$diff_class = " chunk_header"; -		} elsif ($char eq "\\") { -			$diff_class = " incomplete"; -		} +		$num_sign = scalar @{$from->{'href'}}; +	} + +	my @diff_line_classifier = ( +		{ regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"}, +		{ regexp => qr/^\\/,               class => "incomplete"  }, +		{ regexp => qr/^ {$num_sign}/,     class => "ctx" }, +		# classifier for context must come before classifier add/rem, +		# or we would have to use more complicated regexp, for example +		# qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1; +		{ regexp => qr/^[+ ]{$num_sign}/,   class => "add" }, +		{ regexp => qr/^[- ]{$num_sign}/,   class => "rem" }, +	); +	for my $clsfy (@diff_line_classifier) { +		return $clsfy->{'class'} +			if ($line =~ $clsfy->{'regexp'});  	} -	$line = untabify($line); -	if ($from && $to && $line =~ m/^\@{2} /) { -		my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) = -			$line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/; -		$from_lines = 0 unless defined $from_lines; -		$to_lines   = 0 unless defined $to_lines; +	# fallback +	return ""; +} -		if ($from->{'href'}) { -			$from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start", -			                     -class=>"list"}, $from_text); -		} -		if ($to->{'href'}) { -			$to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start", -			                     -class=>"list"}, $to_text); -		} -		$line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" . -		        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; -		return "<div class=\"diff$diff_class\">$line</div>\n"; -	} elsif ($from && $to && $line =~ m/^\@{3}/) { -		my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/; -		my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines); +# assumes that $from and $to are defined and correctly filled, +# and that $line holds a line of chunk header for unified diff +sub format_unidiff_chunk_header { +	my ($line, $from, $to) = @_; -		@from_text = split(' ', $ranges); -		for (my $i = 0; $i < @from_text; ++$i) { -			($from_start[$i], $from_nlines[$i]) = -				(split(',', substr($from_text[$i], 1)), 0); -		} +	my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) = +		$line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/; -		$to_text   = pop @from_text; -		$to_start  = pop @from_start; -		$to_nlines = pop @from_nlines; +	$from_lines = 0 unless defined $from_lines; +	$to_lines   = 0 unless defined $to_lines; -		$line = "<span class=\"chunk_info\">$prefix "; -		for (my $i = 0; $i < @from_text; ++$i) { -			if ($from->{'href'}[$i]) { -				$line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]", -				                  -class=>"list"}, $from_text[$i]); -			} else { -				$line .= $from_text[$i]; -			} -			$line .= " "; -		} -		if ($to->{'href'}) { -			$line .= $cgi->a({-href=>"$to->{'href'}#l$to_start", -			                  -class=>"list"}, $to_text); +	if ($from->{'href'}) { +		$from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start", +		                     -class=>"list"}, $from_text); +	} +	if ($to->{'href'}) { +		$to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start", +		                     -class=>"list"}, $to_text); +	} +	$line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" . +	        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; +	return $line; +} + +# assumes that $from and $to are defined and correctly filled, +# and that $line holds a line of chunk header for combined diff +sub format_cc_diff_chunk_header { +	my ($line, $from, $to) = @_; + +	my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/; +	my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines); + +	@from_text = split(' ', $ranges); +	for (my $i = 0; $i < @from_text; ++$i) { +		($from_start[$i], $from_nlines[$i]) = +			(split(',', substr($from_text[$i], 1)), 0); +	} + +	$to_text   = pop @from_text; +	$to_start  = pop @from_start; +	$to_nlines = pop @from_nlines; + +	$line = "<span class=\"chunk_info\">$prefix "; +	for (my $i = 0; $i < @from_text; ++$i) { +		if ($from->{'href'}[$i]) { +			$line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]", +			                  -class=>"list"}, $from_text[$i]);  		} else { -			$line .= $to_text; +			$line .= $from_text[$i];  		} -		$line .= " $prefix</span>" . -		         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; -		return "<div class=\"diff$diff_class\">$line</div>\n"; +		$line .= " "; +	} +	if ($to->{'href'}) { +		$line .= $cgi->a({-href=>"$to->{'href'}#l$to_start", +		                  -class=>"list"}, $to_text); +	} else { +		$line .= $to_text; +	} +	$line .= " $prefix</span>" . +	         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; +	return $line; +} + +# 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); + +	if ($from && $to && $line =~ m/^\@{2} /) { +		$line = format_unidiff_chunk_header($line, $from, $to); +		return $diff_class, $line; + +	} elsif ($from && $to && $line =~ m/^\@{3}/) { +		$line = format_cc_diff_chunk_header($line, $from, $to); +		return $diff_class, $line; +  	} -	return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n"; +	return $diff_class, esc_html($line, -nbsp=>1);  }  # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)", @@ -2464,6 +2566,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/); @@ -2550,37 +2659,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 { @@ -2598,33 +2764,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 { -			$cgi->a({-href=>"$home_link?by_tag=$_"}, $cloud->{$_}->{topname}) -		} 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>';  	}  } @@ -2643,21 +2825,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 @@ -2672,14 +2856,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; @@ -2692,7 +2876,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>) { @@ -2703,32 +2886,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 = { @@ -2736,8 +2896,6 @@ sub git_get_projects_list {  					owner => to_utf8($owner),  				};  				push @list, $pr; -				(my $forks_path = $path) =~ s/\.git$//; -				$paths{$forks_path}++;  			}  		}  		close $fd; @@ -2745,6 +2903,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 "$projectroot/$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 { @@ -2913,8 +3163,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; @@ -3373,12 +3625,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); @@ -3468,7 +3717,7 @@ sub run_highlighter {  	close $fd;  	open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".  	          quote_command($highlight_bin). -	          " --fragment --syntax $syntax |" +	          " --replace-tabs=8 --fragment --syntax $syntax |"  		or die_error(500, "Couldn't open file or run syntax highlighter");  	return $fd;  } @@ -3494,6 +3743,20 @@ sub get_page_title {  	return $title;  } +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 +	# support xhtml+xml but choking when it gets what it asked for. +	if (defined $cgi->http('HTTP_ACCEPT') && +	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && +	    $cgi->Accept('application/xhtml+xml') != 0) { +		return 'application/xhtml+xml'; +	} else { +		return 'text/html'; +	} +} +  sub print_feed_meta {  	if (defined $project) {  		my %href_params = get_feed_info(); @@ -3539,24 +3802,90 @@ sub print_feed_meta {  	}  } +sub print_header_links { +	my $status = shift; + +	# 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 $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) { +			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; -	# 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 -	# support xhtml+xml but choking when it gets what it asked for. -	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'; -	} +	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'}); @@ -3578,20 +3907,10 @@ EOF  	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="'.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); +	print_header_links($status); + +	if (defined $site_html_head_string) { +		print to_utf8($site_html_head_string);  	}  	print "</head>\n" . @@ -3610,59 +3929,12 @@ EOF  		                         -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) { -			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"; -	} +	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();  	}  } @@ -3722,9 +3994,20 @@ sub git_footer_html {  		      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!;  	} @@ -3928,22 +4211,25 @@ sub git_print_section {  	print $cgi->end_div;  } -sub print_local_time { -	print format_local_time(@_); -} +sub format_timestamp_html { +	my $date = shift; +	my $strtime = $date->{'rfc2822'}; -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'}); +	my (undef, undef, $datetime_class) = +		gitweb_get_feature('javascript-timezone'); +	if ($datetime_class) { +		$strtime = qq!<span class="$datetime_class">$strtime</span>!;  	} -	return $localtime; +	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 @@ -3956,10 +4242,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, @@ -3976,16 +4261,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";  	}  } @@ -4335,7 +4620,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";  			} @@ -4432,8 +4718,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'})}, @@ -4452,8 +4739,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'})}, @@ -4494,7 +4782,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) @@ -4539,7 +4828,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) @@ -4570,8 +4860,97 @@ sub git_difftree_body {  	print "</table>\n";  } +sub print_sidebyside_diff_chunk { +	my @chunk = @_; +	my (@ctx, @rem, @add); + +	return unless @chunk; + +	# incomplete last line might be among removed or added lines, +	# or both, or among context lines: find which +	for (my $i = 1; $i < @chunk; $i++) { +		if ($chunk[$i][0] eq 'incomplete') { +			$chunk[$i][0] = $chunk[$i-1][0]; +		} +	} + +	# guardian +	push @chunk, ["", ""]; + +	foreach my $line_info (@chunk) { +		my ($class, $line) = @$line_info; + +		# print chunk headers +		if ($class && $class eq 'chunk_header') { +			print $line; +			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 = (); +		} + +		## adding lines to accumulator +		# guardian value +		last unless $line; +		# rem, add or change +		if ($class eq 'rem') { +			push @rem, $line; +		} elsif ($class eq 'add') { +			push @add, $line; +		} +		# context line +		if ($class eq 'ctx') { +			push @ctx, $line; +		} +	} +} +  sub git_patchset_body { -	my ($fd, $difftree, $hash, @hash_parents) = @_; +	my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;  	my ($hash_parent) = $hash_parents[0];  	my $is_combined = (@hash_parents > 1); @@ -4581,6 +4960,7 @@ sub git_patchset_body {  	my $diffinfo;  	my $to_name;  	my (%from, %to); +	my @chunk; # for side-by-side diff  	print "<div class=\"patchset\">\n"; @@ -4687,10 +5067,29 @@ sub git_patchset_body {  			next PATCH if ($patch_line =~ m/^diff /); -			print format_diff_line($patch_line, \%from, \%to); +			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"; + +			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; +			}  		}  	} continue { +		if (@chunk) { +			print_sidebyside_diff_chunk(@chunk); +			@chunk = (); +		}  		print "</div>\n"; # class="patch"  	} @@ -4723,11 +5122,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'); @@ -4747,23 +5147,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 { @@ -4787,70 +5223,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; -	} +	$to = $#$projlist if (!defined $to || $#$projlist < $to); -	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); -	} - -	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"; @@ -4858,11 +5239,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";  		} @@ -4883,6 +5270,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) { @@ -4906,7 +5371,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, @@ -5234,6 +5698,216 @@ sub git_remotes_body {  	}  } +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; @@ -5343,7 +6017,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', @@ -5385,7 +6062,11 @@ sub git_summary {  	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(); @@ -5396,7 +6077,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 @@ -5414,13 +6096,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"; @@ -5848,7 +6531,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, @@ -5946,7 +6638,8 @@ 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, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1); +			       $nr, esc_attr(href(-replay => 1)), $nr, $nr, +			       $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);  		}  	}  	close $fd @@ -6392,6 +7085,7 @@ sub git_object {  sub git_blobdiff {  	my $format = shift || 'html'; +	my $diff_style = $input_params{'diff_style'} || 'inline';  	my $fd;  	my @difftree; @@ -6470,6 +7164,7 @@ sub git_blobdiff {  		my $formats_nav =  			$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},  			        "raw"); +		$formats_nav .= diff_style_nav($diff_style);  		git_header_html(undef, $expires);  		if (defined $hash_base && (my %co = parse_commit($hash_base))) {  			git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); @@ -6501,7 +7196,8 @@ sub git_blobdiff {  	if ($format eq 'html') {  		print "<div class=\"page_body\">\n"; -		git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base); +		git_patchset_body($fd, $diff_style, +		                  [ \%diffinfo ], $hash_base, $hash_parent_base);  		close $fd;  		print "</div>\n"; # class="page_body" @@ -6526,9 +7222,31 @@ sub git_blobdiff_plain {  	git_blobdiff('plain');  } +# assumes that it is added as later part of already existing navigation, +# so it returns "| foo | bar" rather than just "foo | bar" +sub diff_style_nav { +	my ($diff_style, $is_combined) = @_; +	$diff_style ||= 'inline'; + +	return "" if ($is_combined); + +	my @styles = (inline => 'inline', 'sidebyside' => 'side by side'); +	my %styles = @styles; +	@styles = +		@styles[ map { $_ * 2 } 0..$#styles/2 ]; + +	return join '', +		map { " | ".$_ } +		map { +			$_ eq $diff_style ? $styles{$_} : +			$cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_}) +		} @styles; +} +  sub git_commitdiff {  	my %params = @_;  	my $format = $params{-format} || 'html'; +	my $diff_style = $input_params{'diff_style'} || 'inline';  	my ($patch_max) = gitweb_get_feature('patches');  	if ($format eq 'patch') { @@ -6554,6 +7272,7 @@ sub git_commitdiff {  				$cgi->a({-href => href(action=>"patch", -replay=>1)},  					"patch");  		} +		$formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);  		if (defined $hash_parent &&  		    $hash_parent ne '-c' && $hash_parent ne '--cc') { @@ -6571,8 +7290,8 @@ sub git_commitdiff {  				}  			}  			$formats_nav .= ': ' . -				$cgi->a({-href => href(action=>"commitdiff", -				                       hash=>$hash_parent)}, +				$cgi->a({-href => href(-replay=>1, +				                       hash=>$hash_parent, hash_base=>undef)},  				        esc_html($hash_parent_short)) .  				')';  		} elsif (!$co{'parent'}) { @@ -6582,28 +7301,28 @@ sub git_commitdiff {  			# single parent commit  			$formats_nav .=  				' (parent: ' . -				$cgi->a({-href => href(action=>"commitdiff", -				                       hash=>$co{'parent'})}, +				$cgi->a({-href => href(-replay=>1, +				                       hash=>$co{'parent'}, hash_base=>undef)},  				        esc_html(substr($co{'parent'}, 0, 7))) .  				')';  		} else {  			# merge commit  			if ($hash_parent eq '--cc') {  				$formats_nav .= ' | ' . -					$cgi->a({-href => href(action=>"commitdiff", +					$cgi->a({-href => href(-replay=>1,  					                       hash=>$hash, hash_parent=>'-c')},  					        'combined');  			} else { # $hash_parent eq '-c'  				$formats_nav .= ' | ' . -					$cgi->a({-href => href(action=>"commitdiff", +					$cgi->a({-href => href(-replay=>1,  					                       hash=>$hash, hash_parent=>'--cc')},  					        'compact');  			}  			$formats_nav .=  				' (merge: ' .  				join(' ', map { -					$cgi->a({-href => href(action=>"commitdiff", -					                       hash=>$_)}, +					$cgi->a({-href => href(-replay=>1, +					                       hash=>$_, hash_base=>undef)},  					        esc_html(substr($_, 0, 7)));  				} @{$co{'parents'}} ) .  				')'; @@ -6732,7 +7451,8 @@ sub git_commitdiff {  		                  $use_parents ? @{$co{'parents'}} : $hash_parent);  		print "<br/>\n"; -		git_patchset_body($fd, \@difftree, $hash, +		git_patchset_body($fd, $diff_style, +		                  \@difftree, $hash,  		                  $use_parents ? @{$co{'parents'}} : $hash_parent);  		close $fd;  		print "</div>\n"; # class="page_body" @@ -6771,7 +7491,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");  	} @@ -6786,205 +7522,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 { @@ -7064,7 +7612,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; @@ -7195,7 +7743,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, @@ -7305,6 +7853,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', | 
