X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;ds=sidebyside;f=gitweb%2Fgitweb.perl;h=e788ef90c981e93f59400332da8bb7de1c2b3952;hb=dcb83ec18d5a79c438289eb55d9c01c61490d2e4;hp=dbfb0441a6a59e6fe069a515a2d293f4d860e143;hpb=e2b1accc59ab5d682d71fd801ebe959c3e871488;p=git.git diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index dbfb0441a..e788ef90c 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -35,6 +35,10 @@ our $GIT = "++GIT_BINDIR++/git"; #our $projectroot = "/pub/scm"; our $projectroot = "++GITWEB_PROJECTROOT++"; +# fs traversing limit for getting project list +# the number is relative to the projectroot +our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++"; + # target of the home link on top of all pages our $home_link = $my_uri || "/"; @@ -71,6 +75,9 @@ our $logo_label = "git homepage"; # source of projects list our $projects_list = "++GITWEB_LIST++"; +# the width (in characters) of the projects list "Description" column +our $projects_list_description_width = 25; + # default order of projects list # valid values are none, project, descr, owner, and age our $default_projects_order = "project"; @@ -101,6 +108,59 @@ our $mimetypes_file = undef; # could be even 'utf-8' for the old behavior) our $fallback_encoding = 'latin1'; +# rename detection options for git-diff and git-diff-tree +# - default is '-M', with the cost proportional to +# (number of removed files) * (number of new files). +# - more costly is '-C' (which implies '-M'), with the cost proportional to +# (number of changed files + number of removed files) * (number of new files) +# - even more costly is '-C', '--find-copies-harder' with cost +# (number of files in the original tree) * (number of new files) +# - one might want to include '-B' option, e.g. '-B', '-M' +our @diff_opts = ('-M'); # taken from git_commit + +# information about snapshot formats that gitweb is capable of serving +our %known_snapshot_formats = ( + # name => { + # 'display' => display name, + # 'type' => mime type, + # 'suffix' => filename suffix, + # 'format' => --format for git-archive, + # 'compressor' => [compressor command and arguments] + # (array reference, optional)} + # + 'tgz' => { + 'display' => 'tar.gz', + 'type' => 'application/x-gzip', + 'suffix' => '.tar.gz', + 'format' => 'tar', + 'compressor' => ['gzip']}, + + 'tbz2' => { + 'display' => 'tar.bz2', + 'type' => 'application/x-bzip2', + 'suffix' => '.tar.bz2', + 'format' => 'tar', + 'compressor' => ['bzip2']}, + + 'zip' => { + 'display' => 'zip', + 'type' => 'application/x-zip', + 'suffix' => '.zip', + 'format' => 'zip'}, +); + +# Aliases so we understand old gitweb.snapshot values in repository +# configuration. +our %known_snapshot_format_aliases = ( + 'gzip' => 'tgz', + 'bzip2' => 'tbz2', + + # backward compatibility: legacy gitweb config support + 'x-gzip' => undef, 'gz' => undef, + 'x-bzip2' => undef, 'bz2' => undef, + 'x-zip' => undef, '' => undef, +); + # You define site-wide feature defaults here; override them with # $GITWEB_CONFIG as necessary. our %feature = ( @@ -131,20 +191,22 @@ our %feature = ( 'override' => 0, 'default' => [0]}, - # Enable the 'snapshot' link, providing a compressed tarball of any + # Enable the 'snapshot' link, providing a compressed archive of any # tree. This can potentially generate high traffic if you have large # project. + # Value is a list of formats defined in %known_snapshot_formats that + # you wish to offer. # To disable system wide have in $GITWEB_CONFIG - # $feature{'snapshot'}{'default'} = [undef]; + # $feature{'snapshot'}{'default'} = []; # To have project specific config enable override in $GITWEB_CONFIG # $feature{'snapshot'}{'override'} = 1; - # and in project config gitweb.snapshot = none|gzip|bzip2|zip; + # and in project config, a comma-separated list of formats or "none" + # to disable. Example: gitweb.snapshot = tbz2,zip; 'snapshot' => { 'sub' => \&feature_snapshot, 'override' => 0, - # => [content-encoding, suffix, program] - 'default' => ['x-gzip', 'gz', 'gzip']}, + 'default' => ['tgz']}, # Enable text search, which will list the commits which match author, # committer or commit text to a given string. Enabled by default. @@ -243,28 +305,15 @@ sub feature_blame { } sub feature_snapshot { - my ($ctype, $suffix, $command) = @_; + my (@fmts) = @_; my ($val) = git_get_project_config('snapshot'); - if ($val eq 'gzip') { - return ('x-gzip', 'gz', 'gzip'); - } elsif ($val eq 'bzip2') { - return ('x-bzip2', 'bz2', 'bzip2'); - } elsif ($val eq 'zip') { - return ('x-zip', 'zip', ''); - } elsif ($val eq 'none') { - return (); + if ($val) { + @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val); } - return ($ctype, $suffix, $command); -} - -sub gitweb_have_snapshot { - my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); - my $have_snapshot = (defined $ctype && defined $suffix); - - return $have_snapshot; + return @fmts; } sub feature_grep { @@ -307,15 +356,17 @@ sub check_export_ok { (!$export_ok || -e "$dir/$export_ok")); } -# rename detection options for git-diff and git-diff-tree -# - default is '-M', with the cost proportional to -# (number of removed files) * (number of new files). -# - more costly is '-C' (or '-C', '-M'), with the cost proportional to -# (number of changed files + number of removed files) * (number of new files) -# - even more costly is '-C', '--find-copies-harder' with cost -# (number of files in the original tree) * (number of new files) -# - one might want to include '-B' option, e.g. '-B', '-M' -our @diff_opts = ('-M'); # taken from git_commit +# process alternate names for backward compatibility +# filter out unsupported (unknown) snapshot formats +sub filter_snapshot_fmts { + my @fmts = @_; + + @fmts = map { + exists $known_snapshot_format_aliases{$_} ? + $known_snapshot_format_aliases{$_} : $_} @fmts; + @fmts = grep(exists $known_snapshot_formats{$_}, @fmts); + +} our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; do $GITWEB_CONFIG if -e $GITWEB_CONFIG; @@ -383,6 +434,22 @@ if (defined $hash_base) { } } +my %allowed_options = ( + "--no-merges" => [ qw(rss atom log shortlog history) ], +); + +our @extra_options = $cgi->param('opt'); +if (defined @extra_options) { + foreach my $opt (@extra_options) { + if (not exists $allowed_options{$opt}) { + die_error(undef, "Invalid option parameter"); + } + if (not grep(/^$action$/, @{$allowed_options{$opt}})) { + die_error(undef, "Invalid option parameter for this action"); + } + } +} + our $hash_parent_base = $cgi->param('hpb'); if (defined $hash_parent_base) { if (!validate_refname($hash_parent_base)) { @@ -408,9 +475,6 @@ if (defined $searchtype) { our $searchtext = $cgi->param('s'); our $search_regexp; if (defined $searchtext) { - if ($searchtype ne 'grep' and $searchtype ne 'pickaxe' and $searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) { - die_error(undef, "Invalid search parameter"); - } if (length($searchtext) < 2) { die_error(undef, "At least two characters are required for search parameter"); } @@ -542,9 +606,20 @@ sub href(%) { order => "o", searchtext => "s", searchtype => "st", + snapshot_format => "sf", + extra_options => "opt", ); my %mapping = @mapping; + if ($params{-replay}) { + while (my ($name, $symbol) = each %mapping) { + if (!exists $params{$name}) { + # to allow for multivalued params we use arrayref form + $params{$name} = [ $cgi->param($symbol) ]; + } + } + } + $params{'project'} = $project unless exists $params{'project'}; my ($use_pathinfo) = gitweb_check_feature('pathinfo'); @@ -564,7 +639,13 @@ sub href(%) { for (my $i = 0; $i < @mapping; $i += 2) { my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]); if (defined $params{$name}) { - push @result, $symbol . "=" . esc_param($params{$name}); + if (ref($params{$name}) eq "ARRAY") { + foreach my $par (@{$params{$name}}) { + push @result, $symbol . "=" . esc_param($par); + } + } else { + push @result, $symbol . "=" . esc_param($params{$name}); + } } } $href .= "?" . join(';', @result) if scalar @result; @@ -774,6 +855,23 @@ sub chop_str { return "$body$tail"; } +# takes the same arguments as chop_str, but also wraps a around the +# result with a title attribute if it does get chopped. Additionally, the +# string is HTML-escaped. +sub chop_and_escape_str { + my $str = shift; + my $len = shift; + my $add_len = shift || 10; + + my $chopped = chop_str($str, $len, $add_len); + if ($chopped eq $str) { + return esc_html($chopped); + } else { + return qq{} . + esc_html($chopped) . qq{}; + } +} + ## ---------------------------------------------------------------------- ## functions returning short strings @@ -824,11 +922,25 @@ sub age_string { return $age_str; } +use constant { + S_IFINVALID => 0030000, + S_IFGITLINK => 0160000, +}; + +# submodule/subproject, a commit object reference +sub S_ISGITLINK($) { + my $mode = shift; + + return (($mode & S_IFMT) == S_IFGITLINK) +} + # convert file mode in octal to symbolic file mode string sub mode_str { my $mode = oct shift; - if (S_ISDIR($mode & S_IFMT)) { + if (S_ISGITLINK($mode)) { + return 'm---------'; + } elsif (S_ISDIR($mode & S_IFMT)) { return 'drwxr-xr-x'; } elsif (S_ISLNK($mode)) { return 'lrwxrwxrwx'; @@ -854,7 +966,9 @@ sub file_type { $mode = oct $mode; } - if (S_ISDIR($mode & S_IFMT)) { + if (S_ISGITLINK($mode)) { + return "submodule"; + } elsif (S_ISDIR($mode & S_IFMT)) { return "directory"; } elsif (S_ISLNK($mode)) { return "symlink"; @@ -875,7 +989,9 @@ sub file_type_long { $mode = oct $mode; } - if (S_ISDIR($mode & S_IFMT)) { + if (S_ISGITLINK($mode)) { + return "submodule"; + } elsif (S_ISDIR($mode & S_IFMT)) { return "directory"; } elsif (S_ISLNK($mode)) { return "symlink"; @@ -1236,6 +1352,43 @@ sub format_diff_line { return "
" . esc_html($line, -nbsp=>1) . "
\n"; } +# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)", +# linked. Pass the hash of the tree/commit to snapshot. +sub format_snapshot_links { + my ($hash) = @_; + my @snapshot_fmts = gitweb_check_feature('snapshot'); + @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts); + my $num_fmts = @snapshot_fmts; + if ($num_fmts > 1) { + # A parenthesized list of links bearing format names. + # e.g. "snapshot (_tar.gz_ _zip_)" + return "snapshot (" . join(' ', map + $cgi->a({ + -href => href( + action=>"snapshot", + hash=>$hash, + snapshot_format=>$_ + ) + }, $known_snapshot_formats{$_}{'display'}) + , @snapshot_fmts) . ")"; + } elsif ($num_fmts == 1) { + # A single "snapshot" link whose tooltip bears the format name. + # i.e. "_snapshot_" + my ($fmt) = @snapshot_fmts; + return + $cgi->a({ + -href => href( + action=>"snapshot", + hash=>$hash, + snapshot_format=>$fmt + ), + -title => "in format: $known_snapshot_formats{$fmt}{'display'}" + }, "snapshot"); + } else { # $num_fmts == 0 + return undef; + } +} + ## ---------------------------------------------------------------------- ## git utility subroutines, invoking git commands @@ -1279,20 +1432,121 @@ sub git_get_type { return $type; } +# repository configuration +our $config_file = ''; +our %config; + +# store multiple values for single key as anonymous array reference +# single values stored directly in the hash, not as [ ] +sub hash_set_multi { + my ($hash, $key, $value) = @_; + + if (!exists $hash->{$key}) { + $hash->{$key} = $value; + } elsif (!ref $hash->{$key}) { + $hash->{$key} = [ $hash->{$key}, $value ]; + } else { + push @{$hash->{$key}}, $value; + } +} + +# return hash of git project configuration +# optionally limited to some section, e.g. 'gitweb' +sub git_parse_project_config { + my $section_regexp = shift; + my %config; + + local $/ = "\0"; + + open my $fh, "-|", git_cmd(), "config", '-z', '-l', + or return; + + while (my $keyval = <$fh>) { + chomp $keyval; + my ($key, $value) = split(/\n/, $keyval, 2); + + hash_set_multi(\%config, $key, $value) + if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o); + } + close $fh; + + return %config; +} + +# convert config value to boolean, 'true' or 'false' +# no value, number > 0, 'true' and 'yes' values are true +# rest of values are treated as false (never as error) +sub config_to_bool { + my $val = shift; + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + return (!defined $val || # section.key + ($val =~ /^\d+$/ && $val) || # section.key = 1 + ($val =~ /^(?:true|yes)$/i)); # section.key = true +} + +# convert config value to simple decimal number +# an optional value suffix of 'k', 'm', or 'g' will cause the value +# to be multiplied by 1024, 1048576, or 1073741824 +sub config_to_int { + my $val = shift; + + # strip leading and trailing whitespace + $val =~ s/^\s+//; + $val =~ s/\s+$//; + + if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) { + $unit = lc($unit); + # unknown unit is treated as 1 + return $num * ($unit eq 'g' ? 1073741824 : + $unit eq 'm' ? 1048576 : + $unit eq 'k' ? 1024 : 1); + } + return $val; +} + +# convert config value to array reference, if needed +sub config_to_multi { + my $val = shift; + + return ref($val) ? $val : [ $val ]; +} + sub git_get_project_config { my ($key, $type) = @_; + # key sanity check return unless ($key); $key =~ s/^gitweb\.//; return if ($key =~ m/\W/); - my @x = (git_cmd(), 'config'); - if (defined $type) { push @x, $type; } - push @x, "--get"; - push @x, "gitweb.$key"; - my $val = qx(@x); - chomp $val; - return ($val); + # type sanity check + if (defined $type) { + $type =~ s/^--//; + $type = undef + unless ($type eq 'bool' || $type eq 'int'); + } + + # get config + if (!defined $config_file || + $config_file ne "$git_dir/config") { + %config = git_parse_project_config('gitweb'); + $config_file = "$git_dir/config"; + } + + # ensure given type + if (!defined $type) { + return $config{"gitweb.$key"}; + } elsif ($type eq 'bool') { + # backward compatibility: 'git config --bool' returns true/false + return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false'; + } elsif ($type eq 'int') { + return config_to_int($config{"gitweb.$key"}); + } + return $config{"gitweb.$key"}; } # get hash of given path at given ref @@ -1352,7 +1606,9 @@ sub git_get_path_by_hash { sub git_get_project_description { my $path = shift; - open my $fd, "$projectroot/$path/description" or return undef; + $git_dir = "$projectroot/$path"; + open my $fd, "$projectroot/$path/description" + or return git_get_project_config('description'); my $descr = <$fd>; close $fd; if (defined $descr) { @@ -1364,7 +1620,11 @@ sub git_get_project_description { sub git_get_project_url_list { my $path = shift; - open my $fd, "$projectroot/$path/cloneurl" or return; + $git_dir = "$projectroot/$path"; + open my $fd, "$projectroot/$path/cloneurl" + or return wantarray ? + @{ config_to_multi(git_get_project_config('url')) } : + config_to_multi(git_get_project_config('url')); my @git_project_url_list = map { chomp; $_ } <$fd>; close $fd; @@ -1386,15 +1646,22 @@ sub git_get_projects_list { # remove the trailing "/" $dir =~ s!/+$!!; my $pfxlen = length("$dir"); + my $pfxdepth = ($dir =~ tr!/!!); File::Find::find({ follow_fast => 1, # follow symbolic links + follow_skip => 2, # ignore duplicates dangling_symlinks => 0, # ignore dangling symlinks, silently wanted => sub { # skip project-list toplevel, if we get it. return if (m!^[/.]$!); # only directories can be git repositories return unless (-d $_); + # don't traverse too deep (Find is super slow on os x) + if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) { + $File::Find::prune = 1; + return; + } my $subdir = substr($File::Find::name, $pfxlen + 1); # we check related file in $projectroot @@ -1465,12 +1732,12 @@ sub git_get_projects_list { return @list; } -sub git_get_project_owner { - my $project = shift; - my $owner; +our $gitweb_project_owner = undef; +sub git_get_project_list_from_file { - return undef unless $project; + return if (defined $gitweb_project_owner); + $gitweb_project_owner = {}; # read from file (url-encoded): # 'git%2Fgit.git Linus+Torvalds' # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' @@ -1482,13 +1749,25 @@ sub git_get_project_owner { my ($pr, $ow) = split ' ', $line; $pr = unescape($pr); $ow = unescape($ow); - if ($pr eq $project) { - $owner = to_utf8($ow); - last; - } + $gitweb_project_owner->{$pr} = to_utf8($ow); } close $fd; } +} + +sub git_get_project_owner { + my $project = shift; + my $owner; + + return undef unless $project; + + if (!defined $gitweb_project_owner) { + git_get_project_list_from_file(); + } + + if (exists $gitweb_project_owner->{$project}) { + $owner = $gitweb_project_owner->{$project}; + } if (!defined $owner) { $owner = get_file_owner("$projectroot/$project"); } @@ -1514,6 +1793,7 @@ sub git_get_last_activity { my $age = time - $timestamp; return ($age, age_string($age)); } + return (undef, undef); } sub git_get_references { @@ -1576,7 +1856,7 @@ sub parse_date { $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min; $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", - 1900+$year, $mon, $mday, $hour ,$min, $sec; + 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); @@ -1757,6 +2037,7 @@ sub parse_commits { ($arg ? ($arg) : ()), ("--max-count=" . $maxcount), ("--skip=" . $skip), + @extra_options, $commit_id, "--", ($filename ? ($filename) : ()) @@ -1825,12 +2106,12 @@ sub parse_difftree_raw_line { $res{'to_mode'} = $2; $res{'from_id'} = $3; $res{'to_id'} = $4; - $res{'status'} = $5; + $res{'status'} = $res{'status_str'} = $5; $res{'similarity'} = $6; if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7); } else { - $res{'file'} = unquote($7); + $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7); } } # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh' @@ -1841,6 +2122,7 @@ sub parse_difftree_raw_line { $res{'to_mode'} = pop @{$res{'from_mode'}}; $res{'from_id'} = [ split(' ', $3) ]; $res{'to_id'} = pop @{$res{'from_id'}}; + $res{'status_str'} = $4; $res{'status'} = [ split('', $4) ]; $res{'to_file'} = unquote($5); } @@ -1852,6 +2134,19 @@ sub parse_difftree_raw_line { return wantarray ? %res : \%res; } +# wrapper: return parsed line of git-diff-tree "raw" output +# (the argument might be raw line, or parsed info) +sub parsed_difftree_line { + my $line_or_ref = shift; + + if (ref($line_or_ref) eq "HASH") { + # pre-parsed (or generated by hand) + return $line_or_ref; + } else { + return parse_difftree_raw_line($line_or_ref); + } +} + # parse line of git-ls-tree output sub parse_ls_tree_line ($;%) { my $line = shift; @@ -1884,7 +2179,10 @@ sub parse_from_to_diffinfo { fill_from_file_info($diffinfo, @parents) unless exists $diffinfo->{'from_file'}; for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { - $from->{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'}; + $from->{'file'}[$i] = + defined $diffinfo->{'from_file'}[$i] ? + $diffinfo->{'from_file'}[$i] : + $diffinfo->{'to_file'}; if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file $from->{'href'}[$i] = href(action=>"blob", hash_base=>$parents[$i], @@ -1895,7 +2193,8 @@ sub parse_from_to_diffinfo { } } } else { - $from->{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; + # ordinary (not combined) diff + $from->{'file'} = $diffinfo->{'from_file'}; if ($diffinfo->{'status'} ne "A") { # not new (added) file $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent, hash=>$diffinfo->{'from_id'}, @@ -1905,7 +2204,7 @@ sub parse_from_to_diffinfo { } } - $to->{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; + $to->{'file'} = $diffinfo->{'to_file'}; if (!is_deleted($diffinfo)) { # file exists in result $to->{'href'} = href(action=>"blob", hash_base=>$hash, hash=>$diffinfo->{'to_id'}, @@ -2150,9 +2449,17 @@ EOF printf(''."\n", esc_param($project), href(action=>"rss")); + printf(''."\n", + esc_param($project), href(action=>"rss", + extra_options=>"--no-merges")); printf(''."\n", esc_param($project), href(action=>"atom")); + printf(''."\n", + esc_param($project), href(action=>"atom", + extra_options=>"--no-merges")); } else { printf(''."\n", @@ -2201,12 +2508,18 @@ EOF } else { $search_hash = "HEAD"; } + my $action = $my_uri; + my ($use_pathinfo) = gitweb_check_feature('pathinfo'); + if ($use_pathinfo) { + $action .= "/$project"; + } else { + $cgi->param("p", $project); + } $cgi->param("a", "search"); $cgi->param("h", $search_hash); - $cgi->param("p", $project); - print $cgi->startform(-method => "get", -action => $my_uri) . + print $cgi->startform(-method => "get", -action => $action) . "
\n" . - $cgi->hidden(-name => "p") . "\n" . + (!$use_pathinfo && $cgi->hidden(-name => "p") . "\n") . $cgi->hidden(-name => "a") . "\n" . $cgi->hidden(-name => "h") . "\n" . $cgi->popup_menu(-name => 'st', -default => 'commit', @@ -2312,7 +2625,7 @@ sub format_paging_nav { if ($page > 0) { $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1), + $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= " ⋅ prev"; @@ -2320,7 +2633,7 @@ sub format_paging_nav { if ($nrevs >= (100 * ($page+1)-1)) { $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1), + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } else { $paging_nav .= " ⋅ next"; @@ -2584,12 +2897,27 @@ sub git_print_tree_entry { "history"); } print "\n"; + } else { + # unknown object: we can only present history for it + # (this includes 'commit' object, i.e. submodule support) + print "" . + esc_path($t->{'name'}) . + "\n"; + print ""; + if (defined $hash_base) { + print $cgi->a({-href => href(action=>"history", + hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print "\n"; } } ## ...................................................................... ## functions printing large fragments of HTML +# get pre-image filenames for merge (combined) diff sub fill_from_file_info { my ($diff, @parents) = @_; @@ -2606,28 +2934,25 @@ sub fill_from_file_info { return $diff; } -# parameters can be strings, or references to arrays of strings -sub from_ids_eq { - my ($a, $b) = @_; +# is current raw difftree line of file deletion +sub is_deleted { + my $diffinfo = shift; - if (ref($a) eq "ARRAY" && ref($b) eq "ARRAY" && @$a == @$b) { - for (my $i = 0; $i < @$a; ++$i) { - return 0 unless ($a->[$i] eq $b->[$i]); - } - return 1; - } elsif (!ref($a) && !ref($b)) { - return $a eq $b; - } else { - return 0; - } + return $diffinfo->{'status_str'} =~ /D/; } -sub is_deleted { - my $diffinfo = shift; +# does patch correspond to [previous] difftree raw line +# $diffinfo - hashref of parsed raw diff format +# $patchinfo - hashref of parsed patch diff format +# (the same keys as in $diffinfo) +sub is_patch_split { + my ($diffinfo, $patchinfo) = @_; - return $diffinfo->{'to_id'} eq ('0' x 40); + return defined $diffinfo && defined $patchinfo + && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'}; } + sub git_difftree_body { my ($difftree, $hash, @parents) = @_; my ($parent) = $parents[0]; @@ -2643,7 +2968,7 @@ sub git_difftree_body { "diff_tree\">\n"; # header only for combined diff in 'commitdiff' view - my $has_header = @parents > 1 && $action eq 'commitdiff'; + my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff'; if ($has_header) { # table header print "\n" . @@ -2664,13 +2989,7 @@ sub git_difftree_body { my $alternate = 1; my $patchno = 0; foreach my $line (@{$difftree}) { - my $diff; - if (ref($line) eq "HASH") { - # pre-parsed (or generated by hand) - $diff = $line; - } else { - $diff = parse_difftree_raw_line($line); - } + my $diff = parsed_difftree_line($line); if ($alternate) { print "\n"; @@ -2941,10 +3260,12 @@ sub git_patchset_body { my ($fd, $difftree, $hash, @hash_parents) = @_; my ($hash_parent) = $hash_parents[0]; + my $is_combined = (@hash_parents > 1); my $patch_idx = 0; my $patch_number = 0; my $patch_line; my $diffinfo; + my $to_name; my (%from, %to); print "
\n"; @@ -2958,140 +3279,85 @@ sub git_patchset_body { PATCH: while ($patch_line) { - my @diff_header; - my ($from_id, $to_id); - - # git diff header - #assert($patch_line =~ m/^diff /) if DEBUG; - #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed - $patch_number++; - push @diff_header, $patch_line; - - # extended diff header - EXTENDED_HEADER: - while ($patch_line = <$fd>) { - chomp $patch_line; - - last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /); - if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) { - $from_id = $1; - $to_id = $2; - } elsif ($patch_line =~ m/^index ((?:[0-9a-fA-F]{40},)+[0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) { - $from_id = [ split(',', $1) ]; - $to_id = $2; - } - - push @diff_header, $patch_line; + # parse "git diff" header line + if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) { + # $1 is from_name, which we do not use + $to_name = unquote($2); + $to_name =~ s!^b/!!; + } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) { + # $1 is 'cc' or 'combined', which we do not use + $to_name = unquote($2); + } else { + $to_name = undef; } - my $last_patch_line = $patch_line; # check if current patch belong to current raw line # and parse raw git-diff line if needed - if (defined $diffinfo && - defined $from_id && defined $to_id && - from_ids_eq($diffinfo->{'from_id'}, $from_id) && - $diffinfo->{'to_id'} eq $to_id) { + if (is_patch_split($diffinfo, { 'to_file' => $to_name })) { # this is continuation of a split patch print "
\n"; } else { # advance raw git-diff output if needed $patch_idx++ if defined $diffinfo; - # compact combined diff output can have some patches skipped - # find which patch (using pathname of result) we are at now - my $to_name; - if ($diff_header[0] =~ m!^diff --cc "?(.*)"?$!) { - $to_name = $1; - } + # read and prepare patch information + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); - do { - # read and prepare patch information - if (ref($difftree->[$patch_idx]) eq "HASH") { - # pre-parsed (or generated by hand) - $diffinfo = $difftree->[$patch_idx]; - } else { - $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); - } - - # check if current raw line has no patch (it got simplified) - if (defined $to_name && $to_name ne $diffinfo->{'to_file'}) { + # compact combined diff output can have some patches skipped + # find which patch (using pathname of result) we are at now; + if ($is_combined) { + while ($to_name ne $diffinfo->{'to_file'}) { print "
\n" . format_diff_cc_simplified($diffinfo, @hash_parents) . "
\n"; # class="patch" $patch_idx++; $patch_number++; + + last if $patch_idx > $#$difftree; + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); } - } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} || - $patch_idx > $#$difftree); + } + # modifies %from, %to hashes parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents); - if ($diffinfo->{'nparents'}) { - # combined diff - $from{'file'} = []; - $from{'href'} = []; - fill_from_file_info($diffinfo, @hash_parents) - unless exists $diffinfo->{'from_file'}; - for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { - $from{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'}; - if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file - $from{'href'}[$i] = href(action=>"blob", - hash_base=>$hash_parents[$i], - hash=>$diffinfo->{'from_id'}[$i], - file_name=>$from{'file'}[$i]); - } else { - $from{'href'}[$i] = undef; - } - } - } else { - $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; - if ($diffinfo->{'status'} ne "A") { # not new (added) file - $from{'href'} = href(action=>"blob", hash_base=>$hash_parent, - hash=>$diffinfo->{'from_id'}, - file_name=>$from{'file'}); - } else { - delete $from{'href'}; - } - } - $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; - if (!is_deleted($diffinfo)) { # file exists in result - $to{'href'} = href(action=>"blob", hash_base=>$hash, - hash=>$diffinfo->{'to_id'}, - file_name=>$to{'file'}); - } else { - delete $to{'href'}; - } # this is first patch for raw difftree line with $patch_idx index # we index @$difftree array from 0, but number patches from 1 print "
\n"; } + # git diff header + #assert($patch_line =~ m/^diff /) if DEBUG; + #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed + $patch_number++; # print "git diff" header - $patch_line = shift @diff_header; print format_git_diff_header_line($patch_line, $diffinfo, \%from, \%to); # print extended diff header - print "
\n" if (@diff_header > 0); + print "
\n"; EXTENDED_HEADER: - foreach $patch_line (@diff_header) { + while ($patch_line = <$fd>) { + chomp $patch_line; + + last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /); + print format_extended_diff_header_line($patch_line, $diffinfo, \%from, \%to); } - print "
\n" if (@diff_header > 0); # class="diff extended_header" + print "
\n"; # class="diff extended_header" # from-file/to-file diff header - $patch_line = $last_patch_line; if (! $patch_line) { print "
\n"; # class="patch" last PATCH; } next PATCH if ($patch_line =~ m/^diff /); #assert($patch_line =~ m/^---/) if DEBUG; - #assert($patch_line eq $last_patch_line) if DEBUG; + my $last_patch_line = $patch_line; $patch_line = <$fd>; chomp $patch_line; #assert($patch_line =~ m/^\+\+\+/) if DEBUG; @@ -3116,16 +3382,11 @@ sub git_patchset_body { # for compact combined (--cc) format, with chunk and patch simpliciaction # patchset might be empty, but there might be unprocessed raw lines - for ($patch_idx++ if $patch_number > 0; + for (++$patch_idx if $patch_number > 0; $patch_idx < @$difftree; - $patch_idx++) { + ++$patch_idx) { # read and prepare patch information - if (ref($difftree->[$patch_idx]) eq "HASH") { - # pre-parsed (or generated by hand) - $diffinfo = $difftree->[$patch_idx]; - } else { - $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); - } + $diffinfo = parsed_difftree_line($difftree->[$patch_idx]); # generate anchor for "patch" links in difftree / whatchanged part print "
\n" . @@ -3163,10 +3424,10 @@ sub git_project_list_body { if (!defined $pr->{'descr'}) { my $descr = git_get_project_description($pr->{'path'}) || ""; $pr->{'descr_long'} = to_utf8($descr); - $pr->{'descr'} = chop_str($descr, 25, 5); + $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5); } if (!defined $pr->{'owner'}) { - $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || ""; + $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || ""; } if ($check_forks) { my $pname = $pr->{'path'}; @@ -3253,7 +3514,7 @@ sub git_project_list_body { "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), -class => "list", -title => $pr->{'descr_long'}}, esc_html($pr->{'descr'})) . "\n" . - "" . chop_str($pr->{'owner'}, 15) . "\n"; + "" . chop_and_escape_str($pr->{'owner'}, 15) . "\n"; print "{'age'}) . "\">" . (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\n" . "" . @@ -3280,8 +3541,6 @@ sub git_shortlog_body { # uses global variable $project my ($commitlist, $from, $to, $refs, $extra) = @_; - my $have_snapshot = gitweb_have_snapshot(); - $from = 0 unless defined $from; $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); @@ -3297,9 +3556,10 @@ sub git_shortlog_body { print "\n"; } $alternate ^= 1; + my $author = chop_and_escape_str($co{'author_name'}, 10); # git_summary() used print "$co{'age_string'}\n" . print "$co{'age_string_date'}\n" . - "" . esc_html(chop_str($co{'author_name'}, 10)) . "\n" . + "" . $author . "\n" . ""; print format_subject_html($co{'title'}, $co{'title_short'}, href(action=>"commit", hash=>$commit), $ref); @@ -3308,8 +3568,9 @@ sub git_shortlog_body { $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); - if ($have_snapshot) { - print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot"); + my $snapshot_links = format_snapshot_links($commit); + if (defined $snapshot_links) { + print " | " . $snapshot_links; } print "\n" . "\n"; @@ -3346,9 +3607,10 @@ sub git_history_body { print "\n"; } $alternate ^= 1; + # shortlog uses chop_str($co{'author_name'}, 10) + my $author = chop_and_escape_str($co{'author_name'}, 15, 3); print "$co{'age_string_date'}\n" . - # shortlog uses chop_str($co{'author_name'}, 10) - "" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "\n" . + "" . $author . "\n" . ""; # originally git_history used chop_str($co{'title'}, 50) print format_subject_html($co{'title'}, $co{'title_short'}, @@ -3502,11 +3764,12 @@ sub git_search_grep_body { print "\n"; } $alternate ^= 1; + my $author = chop_and_escape_str($co{'author_name'}, 15, 5); print "$co{'age_string_date'}\n" . - "" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "\n" . + "" . $author . "\n" . "" . $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, - esc_html(chop_str($co{'title'}, 50)) . "
"); + chop_and_escape_str($co{'title'}, 50) . "
"); my $comment = $co{'comment'}; foreach my $line (@$comment) { if ($line =~ m/^(.*)($search_regexp)(.*)$/i) { @@ -3590,7 +3853,7 @@ sub git_project_index { foreach my $pr (@projects) { if (!exists $pr->{'owner'}) { - $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}"); + $pr->{'owner'} = git_get_project_owner("$pr->{'path'}"); } my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); @@ -3630,7 +3893,7 @@ sub git_summary { print "
 
\n"; print "\n" . "\n" . - "\n"; + "\n"; if (defined $cd{'rfc2822'}) { print "\n"; } @@ -3755,11 +4018,11 @@ sub git_blame2 { or die_error(undef, "Open git-blame failed"); git_header_html(); my $formats_nav = - $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blob", -replay=>1)}, "blob") . " | " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, - "history") . + $cgi->a({-href => href(action=>"history", -replay=>1)}, + "history") . " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "HEAD"); @@ -4035,18 +4298,15 @@ sub git_blob { if (defined $file_name) { if ($have_blame) { $formats_nav .= - $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blame", -replay=>1)}, "blame") . " | "; } $formats_nav .= - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"history", -replay=>1)}, "history") . " | " . - $cgi->a({-href => href(action=>"blob_plain", - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, "raw") . " | " . $cgi->a({-href => href(action=>"blob", @@ -4054,7 +4314,8 @@ sub git_blob { "HEAD"); } else { $formats_nav .= - $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw"); + $cgi->a({-href => href(action=>"blob_plain", -replay=>1)}, + "raw"); } git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); @@ -4091,8 +4352,6 @@ sub git_blob { } sub git_tree { - my $have_snapshot = gitweb_have_snapshot(); - if (!defined $hash_base) { $hash_base = "HEAD"; } @@ -4119,18 +4378,16 @@ sub git_tree { my @views_nav = (); if (defined $file_name) { push @views_nav, - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, - hash=>$hash, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"history", -replay=>1)}, "history"), $cgi->a({-href => href(action=>"tree", hash_base=>"HEAD", file_name=>$file_name)}, "HEAD"), } - if ($have_snapshot) { + my $snapshot_links = format_snapshot_links($hash); + if (defined $snapshot_links) { # FIXME: Should be available when we have no hash base as well. - push @views_nav, - $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, - "snapshot"); + push @views_nav, $snapshot_links; } git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav)); git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); @@ -4194,33 +4451,44 @@ sub git_tree { } sub git_snapshot { - my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); - my $have_snapshot = (defined $ctype && defined $suffix); - if (!$have_snapshot) { + my @supported_fmts = gitweb_check_feature('snapshot'); + @supported_fmts = filter_snapshot_fmts(@supported_fmts); + + my $format = $cgi->param('sf'); + if (!@supported_fmts) { die_error('403 Permission denied', "Permission denied"); } + # default to first supported snapshot format + $format ||= $supported_fmts[0]; + if ($format !~ m/^[a-z0-9]+$/) { + die_error(undef, "Invalid snapshot format parameter"); + } elsif (!exists($known_snapshot_formats{$format})) { + die_error(undef, "Unknown snapshot format"); + } elsif (!grep($_ eq $format, @supported_fmts)) { + die_error(undef, "Unsupported snapshot format"); + } if (!defined $hash) { $hash = git_get_head_hash($project); } - my $git = git_cmd_str(); + my $git_command = git_cmd_str(); my $name = $project; $name =~ s,([^/])/*\.git$,$1,; $name = basename($name); my $filename = to_utf8($name); $name =~ s/\047/\047\\\047\047/g; my $cmd; - if ($suffix eq 'zip') { - $filename .= "-$hash.$suffix"; - $cmd = "$git archive --format=zip --prefix=\'$name\'/ $hash"; - } else { - $filename .= "-$hash.tar.$suffix"; - $cmd = "$git archive --format=tar --prefix=\'$name\'/ $hash | $command"; + $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}"; + $cmd = "$git_command archive " . + "--format=$known_snapshot_formats{$format}{'format'} " . + "--prefix=\'$name\'/ $hash"; + if (exists $known_snapshot_formats{$format}{'compressor'}) { + $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}}; } print $cgi->header( - -type => "application/$ctype", + -type => $known_snapshot_formats{$format}{'type'}, -content_disposition => 'inline; filename="' . "$filename" . '"', -status => '200 OK'); @@ -4230,7 +4498,6 @@ sub git_snapshot { print <$fd>; binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi close $fd; - } sub git_log { @@ -4285,7 +4552,7 @@ sub git_log { } if ($#commitlist >= 100) { print "
\n"; - print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1), + print $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); print "
\n"; } @@ -4349,8 +4616,6 @@ sub git_commit { my $refs = git_get_references(); my $ref = format_ref_marker($refs, $co{'id'}); - my $have_snapshot = gitweb_have_snapshot(); - git_header_html(undef, $expires); git_print_page_nav('commit', '', $hash, $co{'tree'}, $hash, @@ -4389,9 +4654,9 @@ sub git_commit { "" . "\n"; @@ -4519,8 +4784,8 @@ sub git_blobdiff { } %diffinfo = parse_difftree_raw_line($difftree[0]); - $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'}; - $file_name ||= $diffinfo{'to_file'} || $diffinfo{'file'}; + $file_parent ||= $diffinfo{'from_file'} || $file_name; + $file_name ||= $diffinfo{'to_file'}; $hash_parent ||= $diffinfo{'from_id'}; $hash ||= $diffinfo{'to_id'}; @@ -4581,10 +4846,7 @@ sub git_blobdiff { # header if ($format eq 'html') { my $formats_nav = - $cgi->a({-href => href(action=>"blobdiff_plain", - hash=>$hash, hash_parent=>$hash_parent, - hash_base=>$hash_base, hash_parent_base=>$hash_parent_base, - file_name=>$file_name, file_parent=>$file_parent)}, + $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)}, "raw"); git_header_html(undef, $expires); if (defined $hash_base && (my %co = parse_commit($hash_base))) { @@ -4658,8 +4920,7 @@ sub git_commitdiff { my $formats_nav; if ($format eq 'html') { $formats_nav = - $cgi->a({-href => href(action=>"commitdiff_plain", - hash=>$hash, hash_parent=>$hash_parent)}, + $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)}, "raw"); if (defined $hash_parent && @@ -4854,27 +5115,20 @@ sub git_history { file_name=>$file_name)}, "first"); $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page-1), + $cgi->a({-href => href(-replay=>1, page=>$page-1), -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= "first"; $paging_nav .= " ⋅ prev"; } - if ($#commitlist >= 100) { - $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); - } else { - $paging_nav .= " ⋅ next"; - } my $next_link = ''; if ($#commitlist >= 100) { $next_link = - $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, - file_name=>$file_name, page=>$page+1), + $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(); @@ -4944,30 +5198,23 @@ sub git_search { searchtext=>$searchtext, searchtype=>$searchtype)}, "first"); $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page-1), + $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) { - $paging_nav .= " ⋅ " . - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page+1), + $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"; } - my $next_link = ''; + if ($#commitlist >= 100) { - $next_link = - $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); } git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); @@ -5011,12 +5258,13 @@ sub git_search { print "\n"; } $alternate ^= 1; + my $author = chop_and_escape_str($co{'author_name'}, 15, 5); print "\n" . - "\n" . + "\n" . "
description" . esc_html($descr) . "
owner$owner
owner" . esc_html($owner) . "
last change$cd{'rfc2822'}
" . $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, "tree"); - if ($have_snapshot) { - print " | " . - $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot"); + my $snapshot_links = format_snapshot_links($hash); + if (defined $snapshot_links) { + print " | " . $snapshot_links; } print "
$co{'age_string_date'}" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "" . $author . "" . $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, - esc_html(chop_str($co{'title'}, 50)) . "
"); + chop_and_escape_str($co{'title'}, 50) . "
"); while (my $setref = shift @files) { my %set = %$setref; print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, @@ -5165,7 +5413,7 @@ sub git_shortlog { my $next_link = ''; if ($#commitlist >= 100) { $next_link = - $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1), + $cgi->a({-href => href(-replay=>1, page=>$page+1), -accesskey => "n", -title => "Alt-n"}, "next"); } @@ -5193,7 +5441,7 @@ sub git_feed { # log/feed of current (HEAD) branch, log of given branch, history of file/directory my $head = $hash || 'HEAD'; - my @commitlist = parse_commits($head, 150); + my @commitlist = parse_commits($head, 150, 0, undef, $file_name); my %latest_commit; my %latest_date;