X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/d2e2a3cc0ef917caf014ec31ccac98fcb72e09aaeab896ecef072532bdd9e6f2..37a1c2b878d97ac4b417233a76dc12dc8c3371f915008fe43f6bc8723e7060fe:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index e334450..c0905d1 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -18,6 +18,10 @@ use File::Find qw(); use File::Basename qw(basename); binmode STDOUT, ':utf8'; +BEGIN { + CGI->compile() if $ENV{'MOD_PERL'}; +} + our $cgi = new CGI; our $version = "++GIT_VERSION++"; our $my_url = $cgi->url(); @@ -68,6 +72,10 @@ our $logo_label = "git homepage"; # source of projects list our $projects_list = "++GITWEB_LIST++"; +# default order of projects list +# valid values are none, project, descr, owner, and age +our $default_projects_order = "project"; + # show repository only if this file exists # (only effective if this variable evaluates to true) our $export_ok = "++GITWEB_EXPORT_OK++"; @@ -95,10 +103,13 @@ our %feature = ( # 'override' => allow-override (boolean), # 'default' => [ default options...] (array reference)} # - # if feature is overridable (it means that allow-override has true value, + # if feature is overridable (it means that allow-override has true value), # then feature-sub will be called with default options as parameters; # return value of feature-sub indicates if to enable specified feature # + # if there is no 'sub' key (no feature-sub), then feature cannot be + # overriden + # # use gitweb_check_feature() to check if is enabled # Enable the 'blame' blob view, showing the last commit that modified @@ -131,10 +142,24 @@ 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. 'search' => { 'override' => 0, 'default' => [1]}, + # 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. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'grep'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'grep'}{'override'} = 1; + # and in project config gitweb.grep = 0|1; + 'grep' => { + 'override' => 0, + 'default' => [1]}, + # 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. @@ -173,8 +198,8 @@ our %feature = ( # projects matching $projname/*.git will not be shown in the main # projects list, instead a '+' mark will be added to $projname # there and a 'forks' view will be enabled for the project, listing - # all the forks. This feature is supported only if project list - # is taken from a directory, not file. + # all the forks. If project list is taken from a file, forks have + # to be listed after the main project. # To enable system wide have in $GITWEB_CONFIG # $feature{'forks'}{'default'} = [1]; @@ -234,6 +259,18 @@ sub gitweb_have_snapshot { return $have_snapshot; } +sub feature_grep { + my ($val) = git_get_project_config('grep', '--bool'); + + if ($val eq 'true') { + return (1); + } elsif ($val eq 'false') { + return (0); + } + + return ($_[0]); +} + sub feature_pickaxe { my ($val) = git_get_project_config('pickaxe', '--bool'); @@ -353,15 +390,23 @@ if (defined $page) { } } +our $searchtype = $cgi->param('st'); +if (defined $searchtype) { + if ($searchtype =~ m/[^a-z]/) { + die_error(undef, "Invalid searchtype parameter"); + } +} + our $searchtext = $cgi->param('s'); +our $search_regexp; if (defined $searchtext) { - if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) { + 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"); } - $searchtext = quotemeta $searchtext; + $search_regexp = quotemeta $searchtext; } our $searchtype = $cgi->param('st'); @@ -451,10 +496,16 @@ my %actions = ( "project_index" => \&git_project_index, ); -if (defined $project) { - $action ||= 'summary'; -} else { - $action ||= 'project_list'; +if (!defined $action) { + if (defined $hash) { + $action = git_get_type($hash); + } elsif (defined $hash_base && defined $file_name) { + $action = git_get_type("$hash_base:$file_name"); + } elsif (defined $project) { + $action = 'summary'; + } else { + $action = 'project_list'; + } } if (!defined($actions{$action})) { die_error(undef, "Unknown action"); @@ -557,12 +608,6 @@ sub validate_refname { return $input; } -# very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT); -sub to_utf8 { - my $str = shift; - return decode("utf8", $str, Encode::FB_DEFAULT); -} - # quote unsafe chars, but keep the slash, even when it's not # correct, but quoted slashes look too horrible in bookmarks sub esc_param { @@ -587,8 +632,8 @@ sub esc_html ($;%) { my $str = shift; my %opts = @_; - $str = to_utf8($str); - $str = escapeHTML($str); + $str = decode_utf8($str); + $str = $cgi->escapeHTML($str); if ($opts{'-nbsp'}) { $str =~ s/ / /g; } @@ -601,8 +646,8 @@ sub esc_path { my $str = shift; my %opts = @_; - $str = to_utf8($str); - $str = escapeHTML($str); + $str = decode_utf8($str); + $str = $cgi->escapeHTML($str); if ($opts{'-nbsp'}) { $str =~ s/ / /g; } @@ -721,7 +766,9 @@ sub chop_str { sub age_class { my $age = shift; - if ($age < 60*60*2) { + if (!defined $age) { + return "noage"; + } elsif ($age < 60*60*2) { return "age0"; } elsif ($age < 60*60*24*2) { return "age1"; @@ -831,7 +878,7 @@ sub file_type_long { ## ---------------------------------------------------------------------- ## functions returning short HTML fragments, or transforming HTML fragments -## which don't beling to other sections +## which don't belong to other sections # format line of commit message. sub format_log_line_html { @@ -884,7 +931,7 @@ sub format_subject_html { if (length($short) < length($long)) { return $cgi->a({-href => $href, -class => "list subject", - -title => to_utf8($long)}, + -title => decode_utf8($long)}, esc_html($short) . $extra); } else { return $cgi->a({-href => $href, -class => "list subject"}, @@ -896,19 +943,34 @@ sub format_subject_html { sub format_diff_line { my $line = shift; my ($from, $to) = @_; - my $char = substr($line, 0, 1); my $diff_class = ""; chomp $line; - 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"; + 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"; + } } $line = untabify($line); if ($from && $to && $line =~ m/^\@{2} /) { @@ -929,6 +991,39 @@ sub format_diff_line { $line = "@@ $from_text $to_text @@" . "" . esc_html($section, -nbsp=>1) . ""; return "
$line
\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); + + @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 = "$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); + } else { + $line .= $to_text; + } + $line .= " $prefix" . + "" . esc_html($section, -nbsp=>1) . ""; + return "
$line
\n"; } return "
" . esc_html($line, -nbsp=>1) . "
\n"; } @@ -983,7 +1078,7 @@ sub git_get_project_config { $key =~ s/^gitweb\.//; return if ($key =~ m/\W/); - my @x = (git_cmd(), 'repo-config'); + my @x = (git_cmd(), 'config'); if (defined $type) { push @x, $type; } push @x, "--get"; push @x, "gitweb.$key"; @@ -1005,6 +1100,11 @@ sub git_get_hash_by_path { my $line = <$fd>; close $fd or return undef; + if (!defined $line) { + # there is no tree or hash given by $path at $base + return undef; + } + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/; if (defined $type && $type ne $2) { @@ -1014,6 +1114,30 @@ sub git_get_hash_by_path { return $3; } +# get path of entry with given hash at given tree-ish (ref) +# used to get 'from' filename for combined diff (merge commit) for renames +sub git_get_path_by_hash { + my $base = shift || return; + my $hash = shift || return; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base + or return undef; + while (my $line = <$fd>) { + chomp $line; + + #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb' + #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README' + if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) { + close $fd; + return $1; + } + } + close $fd; + return undef; +} + ## ...................................................................... ## git utility functions, directly accessing git repository @@ -1023,7 +1147,9 @@ sub git_get_project_description { open my $fd, "$projectroot/$path/description" or return undef; my $descr = <$fd>; close $fd; - chomp $descr; + if (defined $descr) { + chomp $descr; + } return $descr; } @@ -1044,6 +1170,8 @@ sub git_get_projects_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" : ''); @@ -1051,8 +1179,6 @@ sub git_get_projects_list { $dir =~ s!/+$!!; my $pfxlen = length("$dir"); - my ($check_forks) = gitweb_check_feature('forks'); - File::Find::find({ follow_fast => 1, # follow symbolic links dangling_symlinks => 0, # ignore dangling symlinks, silently @@ -1078,7 +1204,9 @@ 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>) { chomp $line; my ($path, $owner) = split ' ', $line; @@ -1091,24 +1219,41 @@ sub git_get_projects_list { # looking for forks; my $pfx = substr($path, 0, length($filter)); if ($pfx ne $filter) { - next; + next PROJECT; } my $sfx = substr($path, length($filter)); if ($sfx !~ /^\/.*\.git$/) { - next; + 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 (check_export_ok("$projectroot/$path")) { my $pr = { path => $path, - owner => to_utf8($owner), + owner => decode_utf8($owner), }; - push @list, $pr + push @list, $pr; + (my $forks_path = $path) =~ s/\.git$//; + $paths{$forks_path}++; } } close $fd; } - @list = sort {$a->{'path'} cmp $b->{'path'}} @list; return @list; } @@ -1130,7 +1275,7 @@ sub git_get_project_owner { $pr = unescape($pr); $ow = unescape($ow); if ($pr eq $project) { - $owner = to_utf8($ow); + $owner = decode_utf8($ow); last; } } @@ -1155,7 +1300,8 @@ sub git_get_last_activity { 'refs/heads') or return; my $most_recent = <$fd>; close $fd or return; - if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) { + if (defined $most_recent && + $most_recent =~ / (\d+) [-+][01]\d\d\d$/) { my $timestamp = $1; my $age = time - $timestamp; return ($age, age_string($age)); @@ -1272,23 +1418,26 @@ sub parse_tag { } sub parse_commit_text { - my ($commit_text) = @_; + my ($commit_text, $withparents) = @_; my @commit_lines = split '\n', $commit_text; my %co; pop @commit_lines; # Remove '\0' + if (! @commit_lines) { + return; + } + my $header = shift @commit_lines; - if (!($header =~ m/^[0-9a-fA-F]{40}/)) { + if ($header !~ m/^[0-9a-fA-F]{40}/) { return; } - $co{'id'} = $header; - my @parents; + ($co{'id'}, my @parents) = split ' ', $header; while (my $line = shift @commit_lines) { last if $line eq "\n"; if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) { $co{'tree'} = $1; - } elsif ($line =~ m/^parent ([0-9a-fA-F]{40})$/) { + } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) { push @parents, $1; } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { $co{'author'} = $1; @@ -1374,12 +1523,13 @@ sub parse_commit { local $/ = "\0"; open my $fd, "-|", git_cmd(), "rev-list", + "--parents", "--header", "--max-count=1", $commit_id, "--", or die_error(undef, "Open git-rev-list failed"); - %co = parse_commit_text(<$fd>); + %co = parse_commit_text(<$fd>, 1); close $fd; return %co; @@ -1392,35 +1542,13 @@ sub parse_commits { $maxcount ||= 1; $skip ||= 0; - # Delete once rev-list supports the --skip option - if ($skip > 0) { - open my $fd, "-|", git_cmd(), "rev-list", - ($arg ? ($arg) : ()), - ("--max-count=" . ($maxcount + $skip)), - $commit_id, - "--", - ($filename ? ($filename) : ()) - or die_error(undef, "Open git-rev-list failed"); - while (my $line = <$fd>) { - if ($skip-- <= 0) { - chomp $line; - my %co = parse_commit($line); - push @cos, \%co; - } - } - close $fd; - - return wantarray ? @cos : \@cos; - } - local $/ = "\0"; open my $fd, "-|", git_cmd(), "rev-list", "--header", ($arg ? ($arg) : ()), ("--max-count=" . $maxcount), - # Add once rev-list supports the --skip option - # ("--skip=" . $skip), + ("--skip=" . $skip), $commit_id, "--", ($filename ? ($filename) : ()) @@ -1497,6 +1625,17 @@ sub parse_difftree_raw_line { $res{'file'} = unquote($7); } } + # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh' + # combined diff (for merge commit) + elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) { + $res{'nparents'} = length($1); + $res{'from_mode'} = [ split(' ', $2) ]; + $res{'to_mode'} = pop @{$res{'from_mode'}}; + $res{'from_id'} = [ split(' ', $3) ]; + $res{'to_id'} = pop @{$res{'from_id'}}; + $res{'status'} = [ split('', $4) ]; + $res{'to_file'} = unquote($5); + } # 'c512b523472485aef4fff9e57b229d9d243c967f' elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { $res{'commit'} = $1; @@ -1626,7 +1765,7 @@ sub get_file_owner { } my $owner = $gcos; $owner =~ s/[,;].*$//; - return to_utf8($owner); + return decode_utf8($owner); } ## ...................................................................... @@ -1709,7 +1848,7 @@ sub git_header_html { my $title = "$site_name"; if (defined $project) { - $title .= " - $project"; + $title .= " - " . decode_utf8($project); if (defined $action) { $title .= "/$action"; if (defined $file_name) { @@ -1734,6 +1873,7 @@ sub git_header_html { } print $cgi->header(-type=>$content_type, -charset => 'utf-8', -status=> $status, -expires => $expires); + my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; print < @@ -1742,7 +1882,7 @@ sub git_header_html { - + $title EOF @@ -1796,6 +1936,8 @@ EOF } print "\n"; } + print "\n"; + my ($have_search) = gitweb_check_feature('search'); if ((defined $project) && ($have_search)) { if (!defined $searchtext) { @@ -1818,14 +1960,13 @@ EOF $cgi->hidden(-name => "a") . "\n" . $cgi->hidden(-name => "h") . "\n" . $cgi->popup_menu(-name => 'st', -default => 'commit', - -values => ['commit', 'author', 'committer', 'pickaxe']) . + -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) . $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) . " search:\n", $cgi->textfield(-name => "s", -value => $searchtext) . "\n" . "" . $cgi->end_form() . "\n"; } - print "\n"; } sub git_footer_html { @@ -1888,16 +2029,16 @@ sub git_print_page_nav { my %arg = map { $_ => {action=>$_} } @navs; if (defined $head) { for (qw(commit commitdiff)) { - $arg{$_}{hash} = $head; + $arg{$_}{'hash'} = $head; } if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) { for (qw(shortlog log)) { - $arg{$_}{hash} = $head; + $arg{$_}{'hash'} = $head; } } } - $arg{tree}{hash} = $treehead if defined $treehead; - $arg{tree}{hash_base} = $treebase if defined $treebase; + $arg{'tree'}{'hash'} = $treehead if defined $treehead; + $arg{'tree'}{'hash_base'} = $treebase if defined $treebase; print "
\n" . (join " | ", @@ -1945,9 +2086,9 @@ sub git_print_header_div { my ($action, $title, $hash, $hash_base) = @_; my %args = (); - $args{action} = $action; - $args{hash} = $hash if $hash; - $args{hash_base} = $hash_base if $hash_base; + $args{'action'} = $action; + $args{'hash'} = $hash if $hash; + $args{'hash_base'} = $hash_base if $hash_base; print "
\n" . $cgi->a({-href => href(%args), -class => "title"}, @@ -1981,7 +2122,7 @@ sub git_print_page_path { print "
"; print $cgi->a({-href => href(action=>"tree", hash_base=>$hb), - -title => 'tree root'}, "[$project]"); + -title => 'tree root'}, decode_utf8("[$project]")); print " / "; if (defined $name) { my @dirname = split '/', $name; @@ -1992,17 +2133,17 @@ sub git_print_page_path { $fullname .= ($fullname ? '/' : '') . $dir; print $cgi->a({-href => href(action=>"tree", file_name=>$fullname, hash_base=>$hb), - -title => esc_html($fullname)}, esc_path($dir)); + -title => $fullname}, esc_path($dir)); print " / "; } if (defined $type && $type eq 'blob') { print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name, hash_base=>$hb), - -title => esc_html($name)}, esc_path($basename)); + -title => $name}, esc_path($basename)); } elsif (defined $type && $type eq 'tree') { print $cgi->a({-href => href(action=>"tree", file_name=>$file_name, hash_base=>$hb), - -title => esc_html($name)}, esc_path($basename)); + -title => $name}, esc_path($basename)); print " / "; } else { print esc_path($basename); @@ -2199,8 +2340,42 @@ sub git_print_tree_entry { ## ...................................................................... ## functions printing large fragments of HTML +sub fill_from_file_info { + my ($diff, @parents) = @_; + + $diff->{'from_file'} = [ ]; + $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef; + for (my $i = 0; $i < $diff->{'nparents'}; $i++) { + if ($diff->{'status'}[$i] eq 'R' || + $diff->{'status'}[$i] eq 'C') { + $diff->{'from_file'}[$i] = + git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]); + } + } + + return $diff; +} + +# parameters can be strings, or references to arrays of strings +sub from_ids_eq { + my ($a, $b) = @_; + + 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; + } +} + + sub git_difftree_body { - my ($difftree, $hash, $parent) = @_; + my ($difftree, $hash, @parents) = @_; + my ($parent) = $parents[0]; my ($have_blame) = gitweb_check_feature('blame'); print "
\n"; if ($#{$difftree} > 10) { @@ -2208,11 +2383,19 @@ sub git_difftree_body { } print "
\n"; - print "\n"; + print "
1 ? "combined " : "") . + "diff_tree\">\n"; my $alternate = 1; my $patchno = 0; foreach my $line (@{$difftree}) { - my %diff = parse_difftree_raw_line($line); + my $diff; + if (ref($line) eq "HASH") { + # pre-parsed (or generated by hand) + $diff = $line; + } else { + $diff = parse_difftree_raw_line($line); + } if ($alternate) { print "\n"; @@ -2221,31 +2404,120 @@ sub git_difftree_body { } $alternate ^= 1; + if (exists $diff->{'nparents'}) { # combined diff + + fill_from_file_info($diff, @parents) + unless exists $diff->{'from_file'}; + + if ($diff->{'to_id'} ne ('0' x 40)) { + # file exists in the result (child) commit + print "\n"; + } else { + print "\n"; + } + + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print "\n"; + } + + my $has_history = 0; + my $not_deleted = 0; + for (my $i = 0; $i < $diff->{'nparents'}; $i++) { + my $hash_parent = $parents[$i]; + my $from_hash = $diff->{'from_id'}[$i]; + my $from_path = $diff->{'from_file'}[$i]; + my $status = $diff->{'status'}[$i]; + + $has_history ||= ($status ne 'A'); + $not_deleted ||= ($status ne 'D'); + + if ($status eq 'A') { + print "\n"; + } elsif ($status eq 'D') { + print "\n"; + } else { + if ($diff->{'to_id'} eq $from_hash) { + print "\n"; + } + } + + print "\n"; + + print "\n"; + next; # instead of 'else' clause, to avoid extra indent + } + # else ordinary diff + my ($to_mode_oct, $to_mode_str, $to_file_type); my ($from_mode_oct, $from_mode_str, $from_file_type); - if ($diff{'to_mode'} ne ('0' x 6)) { - $to_mode_oct = oct $diff{'to_mode'}; + if ($diff->{'to_mode'} ne ('0' x 6)) { + $to_mode_oct = oct $diff->{'to_mode'}; if (S_ISREG($to_mode_oct)) { # only for regular file $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits } - $to_file_type = file_type($diff{'to_mode'}); + $to_file_type = file_type($diff->{'to_mode'}); } - if ($diff{'from_mode'} ne ('0' x 6)) { - $from_mode_oct = oct $diff{'from_mode'}; + if ($diff->{'from_mode'} ne ('0' x 6)) { + $from_mode_oct = oct $diff->{'from_mode'}; if (S_ISREG($to_mode_oct)) { # only for regular file $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits } - $from_file_type = file_type($diff{'from_mode'}); + $from_file_type = file_type($diff->{'from_mode'}); } - if ($diff{'status'} eq "A") { # created + if ($diff->{'status'} eq "A") { # created my $mode_chng = "[new $to_file_type"; $mode_chng .= " with mode: $to_mode_str" if $to_mode_str; $mode_chng .= "]"; print "\n"; print "\n"; print "\n"; - } elsif ($diff{'status'} eq "D") { # deleted + } elsif ($diff->{'status'} eq "D") { # deleted my $mode_chng = "[deleted $from_file_type]"; print "\n"; print "\n"; print "\n"; - } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed + } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed my $mode_chnge = ""; - if ($diff{'from_mode'} != $diff{'to_mode'}) { + if ($diff->{'from_mode'} != $diff->{'to_mode'}) { $mode_chnge = "[changed"; - if ($from_file_type != $to_file_type) { + if ($from_file_type ne $to_file_type) { $mode_chnge .= " from $from_file_type to $to_file_type"; } if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) { @@ -2305,9 +2577,9 @@ sub git_difftree_body { $mode_chnge .= "]\n"; } print "\n"; print "\n"; print "\n"; - } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied + } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied my %status_name = ('R' => 'moved', 'C' => 'copied'); - my $nstatus = $status_name{$diff{'status'}}; + my $nstatus = $status_name{$diff->{'status'}}; my $mode_chng = ""; - if ($diff{'from_mode'} != $diff{'to_mode'}) { + if ($diff->{'from_mode'} != $diff->{'to_mode'}) { # mode also for directories, so we cannot use $to_mode_str $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777); } print "\n" . + hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}), + -class => "list"}, esc_path($diff->{'to_file'})) . "\n" . "\n" . + hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}), + -class => "list"}, esc_path($diff->{'from_file'})) . + " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]\n" . "\n"; @@ -2390,13 +2662,14 @@ sub git_difftree_body { } sub git_patchset_body { - my ($fd, $difftree, $hash, $hash_parent) = @_; + my ($fd, $difftree, $hash, @hash_parents) = @_; + my ($hash_parent) = $hash_parents[0]; my $patch_idx = 0; + my $patch_number = 0; my $patch_line; my $diffinfo; my (%from, %to); - my ($from_id, $to_id); print "
\n"; @@ -2410,10 +2683,12 @@ 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 @@ -2421,24 +2696,27 @@ sub git_patchset_body { while ($patch_line = <$fd>) { chomp $patch_line; - last EXTENDED_HEADER if ($patch_line =~ m/^--- /); + 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; } - #last PATCH unless $patch_line; 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 && - $diffinfo->{'from_id'} eq $from_id && - $diffinfo->{'to_id'} eq $to_id) { - # this is split patch + defined $from_id && defined $to_id && + from_ids_eq($diffinfo->{'from_id'}, $from_id) && + $diffinfo->{'to_id'} eq $to_id) { + # this is continuation of a split patch print "
\n"; } else { # advance raw git-diff output if needed @@ -2451,17 +2729,41 @@ sub git_patchset_body { } else { $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); } - $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; - $to{'file'} = $diffinfo->{'to_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'}); + 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'}; + } } - if ($diffinfo->{'status'} ne "D") { # not deleted file + + $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; + if ($diffinfo->{'to_id'} ne ('0' x 40)) { # 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 @@ -2470,19 +2772,34 @@ sub git_patchset_body { # print "git diff" header $patch_line = shift @diff_header; - $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!; - if ($from{'href'}) { - $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"}, - 'a/' . esc_path($from{'file'})); - } else { # file was added - $patch_line .= 'a/' . esc_path($from{'file'}); - } - $patch_line .= ' '; - if ($to{'href'}) { - $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, - 'b/' . esc_path($to{'file'})); - } else { # file was deleted - $patch_line .= 'b/' . esc_path($to{'file'}); + if ($diffinfo->{'nparents'}) { + + # combined diff + $patch_line =~ s!^(diff (.*?) )"?.*$!$1!; + if ($to{'href'}) { + $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, + esc_path($to{'file'})); + } else { # file was deleted + $patch_line .= esc_path($to{'file'}); + } + + } else { + + $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!; + if ($from{'href'}) { + $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"}, + 'a/' . esc_path($from{'file'})); + } else { # file was added + $patch_line .= 'a/' . esc_path($from{'file'}); + } + $patch_line .= ' '; + if ($to{'href'}) { + $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, + 'b/' . esc_path($to{'file'})); + } else { # file was deleted + $patch_line .= 'b/' . esc_path($to{'file'}); + } + } print "
$patch_line
\n"; @@ -2493,20 +2810,43 @@ sub git_patchset_body { # match if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) { $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"}, - esc_path($from{'file'})); + esc_path($from{'file'})); } if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) { - $patch_line = $cgi->a({-href=>$to{'href'}, -class=>"path"}, - esc_path($to{'file'})); + $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"}, + esc_path($to{'file'})); } - # match + # match single if ($patch_line =~ m/\s(\d{6})$/) { $patch_line .= ' (' . file_type_long($1) . ')'; } # match - if ($patch_line =~ m/^index/) { + if ($patch_line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) { + # can match only for combined diff + $patch_line = 'index '; + for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) { + if ($from{'href'}[$i]) { + $patch_line .= $cgi->a({-href=>$from{'href'}[$i], + -class=>"hash"}, + substr($diffinfo->{'from_id'}[$i],0,7)); + } else { + $patch_line .= '0' x 7; + } + # separator + $patch_line .= ',' if ($i < $diffinfo->{'nparents'} - 1); + } + $patch_line .= '..'; + if ($to{'href'}) { + $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $patch_line .= '0' x 7; + } + + } elsif ($patch_line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) { + # can match only for ordinary diff my ($from_link, $to_link); if ($from{'href'}) { $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"}, @@ -2536,8 +2876,14 @@ sub git_patchset_body { # 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; - if ($from{'href'}) { + if (!$diffinfo->{'nparents'} && # not from-file line for combined diff + $from{'href'} && $patch_line =~ m!^--- "?a/!) { $patch_line = '--- a/' . $cgi->a({-href=>$from{'href'}, -class=>"path"}, esc_path($from{'file'})); @@ -2545,11 +2891,10 @@ sub git_patchset_body { print "
$patch_line
\n"; $patch_line = <$fd>; - #last PATCH unless $patch_line; chomp $patch_line; #assert($patch_line =~ m/^+++/) if DEBUG; - if ($to{'href'}) { + if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) { $patch_line = '+++ b/' . $cgi->a({-href=>$to{'href'}, -class=>"path"}, esc_path($to{'file'})); @@ -2570,6 +2915,14 @@ sub git_patchset_body { print "
\n"; # class="patch" } + if ($patch_number == 0) { + if (@hash_parents > 1) { + print "
Trivial merge
\n"; + } else { + print "
No differences found
\n"; + } + } + print "\n"; # class="patchset" } @@ -2589,7 +2942,7 @@ sub git_project_list_body { ($pr->{'age'}, $pr->{'age_string'}) = @aa; if (!defined $pr->{'descr'}) { my $descr = git_get_project_description($pr->{'path'}) || ""; - $pr->{'descr_long'} = to_utf8($descr); + $pr->{'descr_long'} = decode_utf8($descr); $pr->{'descr'} = chop_str($descr, 25, 5); } if (!defined $pr->{'owner'}) { @@ -2609,7 +2962,7 @@ sub git_project_list_body { push @projects, $pr; } - $order ||= "project"; + $order ||= $default_projects_order; $from = 0 unless defined $from; $to = $#projects if (!defined $to || $#projects < $to); @@ -2682,7 +3035,7 @@ sub git_project_list_body { esc_html($pr->{'descr'})) . "\n" . "\n"; print "\n" . + (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "\n" . "
" . + $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + file_name=>$diff->{'to_file'}, + hash_base=>$hash), + -class => "list"}, esc_path($diff->{'to_file'})) . + "" . + esc_path($diff->{'to_file'}) . + "" . + $cgi->a({-href => "#patch$patchno"}, "patch") . + " | " . + " | " . + $cgi->a({-href => href(action=>"blob", + hash_base=>$hash, + hash=>$from_hash, + file_name=>$from_path)}, + "blob" . ($i+1)) . + " | "; + } + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff->{'to_id'}, + hash_parent=>$from_hash, + hash_base=>$hash, + hash_parent_base=>$hash_parent, + file_name=>$diff->{'to_file'}, + file_parent=>$from_path)}, + "diff" . ($i+1)) . + " | "; + if ($not_deleted) { + print $cgi->a({-href => href(action=>"blob", + hash=>$diff->{'to_id'}, + file_name=>$diff->{'to_file'}, + hash_base=>$hash)}, + "blob"); + print " | " if ($has_history); + } + if ($has_history) { + print $cgi->a({-href => href(action=>"history", + file_name=>$diff->{'to_file'}, + hash_base=>$hash)}, + "history"); + } + print "
"; - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'}), - -class => "list"}, esc_path($diff{'file'})); + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); print "$mode_chng"; @@ -2255,17 +2527,17 @@ sub git_difftree_body { print $cgi->a({-href => "#patch$patchno"}, "patch"); print " | "; } - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'})}, - "blob") . " | "; + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'})}, + "blob"); print ""; - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, - hash_base=>$parent, file_name=>$diff{'file'}), - -class => "list"}, esc_path($diff{'file'})); + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'}, + hash_base=>$parent, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); print "$mode_chng"; @@ -2275,24 +2547,24 @@ sub git_difftree_body { print $cgi->a({-href => "#patch$patchno"}, "patch"); print " | "; } - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, - hash_base=>$parent, file_name=>$diff{'file'})}, + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'}, + hash_base=>$parent, file_name=>$diff->{'file'})}, "blob") . " | "; if ($have_blame) { print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, - file_name=>$diff{'file'})}, + file_name=>$diff->{'file'})}, "blame") . " | "; } print $cgi->a({-href => href(action=>"history", hash_base=>$parent, - file_name=>$diff{'file'})}, + file_name=>$diff->{'file'})}, "history"); print ""; - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'}), - -class => "list"}, esc_path($diff{'file'})); + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'}), + -class => "list"}, esc_path($diff->{'file'})); print "$mode_chnge"; @@ -2316,70 +2588,70 @@ sub git_difftree_body { $patchno++; print $cgi->a({-href => "#patch$patchno"}, "patch") . " | "; - } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) { # "commit" view and modified file (not onlu mode changed) print $cgi->a({-href => href(action=>"blobdiff", - hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'}, hash_base=>$hash, hash_parent_base=>$parent, - file_name=>$diff{'file'})}, + file_name=>$diff->{'file'})}, "diff") . " | "; } - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'})}, + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$hash, file_name=>$diff->{'file'})}, "blob") . " | "; if ($have_blame) { print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, - file_name=>$diff{'file'})}, + file_name=>$diff->{'file'})}, "blame") . " | "; } print $cgi->a({-href => href(action=>"history", hash_base=>$hash, - file_name=>$diff{'file'})}, + file_name=>$diff->{'file'})}, "history"); print "" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash, - hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}), - -class => "list"}, esc_path($diff{'to_file'})) . "[$nstatus from " . $cgi->a({-href => href(action=>"blob", hash_base=>$parent, - hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}), - -class => "list"}, esc_path($diff{'from_file'})) . - " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]"; if ($action eq 'commitdiff') { # link to patch $patchno++; print $cgi->a({-href => "#patch$patchno"}, "patch") . " | "; - } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) { # "commit" view and modified file (not only pure rename or copy) print $cgi->a({-href => href(action=>"blobdiff", - hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'}, hash_base=>$hash, hash_parent_base=>$parent, - file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})}, + file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})}, "diff") . " | "; } - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$parent, file_name=>$diff{'to_file'})}, + print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'}, + hash_base=>$parent, file_name=>$diff->{'to_file'})}, "blob") . " | "; if ($have_blame) { print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, - file_name=>$diff{'to_file'})}, + file_name=>$diff->{'to_file'})}, "blame") . " | "; } print $cgi->a({-href => href(action=>"history", hash_base=>$hash, - file_name=>$diff{'to_file'})}, + file_name=>$diff->{'to_file'})}, "history"); print "" . chop_str($pr->{'owner'}, 15) . "{'age'}) . "\">" . - $pr->{'age_string'} . "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . @@ -2751,23 +3104,19 @@ sub git_shortlog_body { sub git_history_body { # Warning: assumes constant type (blob or tree) during history - my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; + my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; $from = 0 unless defined $from; - $to = $#{$revlist} unless (defined $to && $to <= $#{$revlist}); + $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); print "\n"; my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { - if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) { - next; - } - - my $commit = $1; - my %co = parse_commit($commit); + my %co = %{$commitlist->[$i]}; if (!%co) { next; } + my $commit = $co{'id'}; my $ref = format_ref_marker($refs, $commit); @@ -2835,8 +3184,12 @@ sub git_tags_body { print "\n"; } $alternate ^= 1; - print "\n" . - "\n"; + } else { + print "\n"; + } + print "\n" . @@ -2936,7 +3289,7 @@ sub git_search_grep_body { esc_html(chop_str($co{'title'}, 50)) . "
"); my $comment = $co{'comment'}; foreach my $line (@$comment) { - if ($line =~ m/^(.*)($searchtext)(.*)$/i) { + if ($line =~ m/^(.*)($search_regexp)(.*)$/i) { my $lead = esc_html($1) || ""; $lead = chop_str($lead, 30, 10); my $match = esc_html($2) || ""; @@ -2968,7 +3321,7 @@ sub git_search_grep_body { sub git_project_list { my $order = $cgi->param('o'); - if (defined $order && $order !~ m/project|descr|owner|age/) { + if (defined $order && $order !~ m/none|project|descr|owner|age/) { die_error(undef, "Unknown order parameter"); } @@ -2991,7 +3344,7 @@ sub git_project_list { sub git_forks { my $order = $cgi->param('o'); - if (defined $order && $order !~ m/project|descr|owner|age/) { + if (defined $order && $order !~ m/none|project|descr|owner|age/) { die_error(undef, "Unknown order parameter"); } @@ -3017,7 +3370,7 @@ sub git_project_index { foreach my $pr (@projects) { if (!exists $pr->{'owner'}) { - $pr->{'owner'} = get_file_owner("$projectroot/$project"); + $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}"); } my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); @@ -3034,7 +3387,7 @@ sub git_project_index { sub git_summary { my $descr = git_get_project_description($project) || "none"; my %co = parse_commit("HEAD"); - my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : (); my $head = $co{'id'}; my $owner = git_get_project_owner($project); @@ -3057,8 +3410,11 @@ sub git_summary { print "
 
\n"; print "
$tag{'age'}" . + if (defined $tag{'age'}) { + print "$tag{'age'}" . $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}), -class => "list name"}, esc_html($tag{'name'})) . "
\n" . "\n" . - "\n" . - "\n"; + "\n"; + if (defined $cd{'rfc2822'}) { + print "\n"; + } + # use per project git URL list in $projectroot/$project/cloneurl # or make project git URL from git base URL and project name my $url_tag = "URL"; @@ -3081,11 +3437,13 @@ sub git_summary { # we need to request one more than 16 (0..15) to check if # those 16 are all - my @commitlist = parse_commits($head, 17); - git_print_header_div('shortlog'); - git_shortlog_body(\@commitlist, 0, 15, $refs, - $#commitlist <= 15 ? undef : - $cgi->a({-href => href(action=>"shortlog")}, "...")); + my @commitlist = $head ? parse_commits($head, 17) : (); + if (@commitlist) { + git_print_header_div('shortlog'); + git_shortlog_body(\@commitlist, 0, 15, $refs, + $#commitlist <= 15 ? undef : + $cgi->a({-href => href(action=>"shortlog")}, "...")); + } if (@taglist) { git_print_header_div('tags'); @@ -3106,7 +3464,7 @@ sub git_summary { git_project_list_body(\@forklist, undef, 0, 15, $#forklist <= 15 ? undef : $cgi->a({-href => href(action=>"forks")}, "..."), - 'noheader'); + 'noheader'); } git_footer_html(); @@ -3117,6 +3475,11 @@ sub git_tag { git_header_html(); git_print_page_nav('','', $head,undef,$head); my %tag = parse_tag($hash); + + if (! %tag) { + die_error(undef, "Unknown tag object"); + } + git_print_header_div('commit', esc_html($tag{'name'}), $hash); print "
\n" . "
description" . esc_html($descr) . "
owner$owner
last change$cd{'rfc2822'}
owner$owner
last change$cd{'rfc2822'}
\n" . @@ -3165,7 +3528,7 @@ sub git_blame2 { } $ftype = git_get_type($hash); if ($ftype !~ "blob") { - die_error("400 Bad Request", "Object is not a blob"); + die_error('400 Bad Request', "Object is not a blob"); } open ($fd, "-|", git_cmd(), "blame", '-p', '--', $file_name, $hash_base) @@ -3213,7 +3576,7 @@ HTML my $rev = substr($full_rev, 0, 8); my $author = $meta->{'author'}; my %date = parse_date($meta->{'author-time'}, - $meta->{'author-tz'}); + $meta->{'author-tz'}); my $date = $date{'iso-tz'}; if ($group_size) { $current_color = ++$current_color % $num_colors; @@ -3225,19 +3588,24 @@ HTML print " rowspan=\"$group_size\"" if ($group_size > 1); print ">"; print $cgi->a({-href => href(action=>"commit", - hash=>$full_rev, - file_name=>$file_name)}, - esc_html($rev)); + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($rev)); print "\n"; } + open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") + or die_error(undef, "Open git-rev-parse failed"); + my $parent_commit = <$dd>; + close $dd; + chomp($parent_commit); my $blamed = href(action => 'blame', - file_name => $meta->{'filename'}, - hash_base => $full_rev); + file_name => $meta->{'filename'}, + hash_base => $parent_commit); print ""; print "\n"; print "\n"; @@ -3616,7 +3984,7 @@ sub git_snapshot { $hash = git_get_head_hash($project); } - my $filename = basename($project) . "-$hash.tar.$suffix"; + my $filename = decode_utf8(basename($project)) . "-$hash.tar.$suffix"; print $cgi->header( -type => "application/$ctype", @@ -3627,8 +3995,8 @@ sub git_snapshot { my $name = $project; $name =~ s/\047/\047\\\047\047/g; open my $fd, "-|", - "$git archive --format=tar --prefix=\'$name\'/ $hash | $command" - or die_error(undef, "Execute git-tar-tree failed."); + "$git archive --format=tar --prefix=\'$name\'/ $hash | $command" + or die_error(undef, "Execute git-tar-tree failed"); binmode STDOUT, ':raw'; print <$fd>; binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi @@ -3725,7 +4093,7 @@ sub git_commit { $formats_nav .= '(merge: ' . join(' ', map { - $cgi->a({-href => href(action=>"commitdiff", + $cgi->a({-href => href(action=>"commit", hash=>$_)}, esc_html(substr($_, 0, 7))); } @$parents ) . @@ -3736,14 +4104,13 @@ sub git_commit { $parent = "--root"; } my @difftree; - if (@$parents <= 1) { - # difftree output is not printed for merges - open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id", - @diff_opts, $parent, $hash, "--" - or die_error(undef, "Open git-diff-tree failed"); - @difftree = map { chomp; $_ } <$fd>; - close $fd or die_error(undef, "Reading git-diff-tree failed"); - } + open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id", + @diff_opts, + (@$parents <= 1 ? $parent : '-c'), + $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd or die_error(undef, "Reading git-diff-tree failed"); # non-textual hash id's can be cached my $expires; @@ -3821,10 +4188,7 @@ sub git_commit { git_print_log($co{'comment'}); print "\n"; - if (@$parents <= 1) { - # do not output difftree/whatchanged for merges - git_difftree_body(\@difftree, $hash, $parent); - } + git_difftree_body(\@difftree, $hash, @$parents); git_footer_html(); } @@ -3891,7 +4255,7 @@ sub git_blobdiff { # read raw output open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base, - "--", $file_name + "--", (defined $file_parent ? $file_parent : ()), $file_name or die_error(undef, "Open git-diff-tree failed"); @difftree = map { chomp; $_ } <$fd>; close $fd @@ -3940,8 +4304,9 @@ sub git_blobdiff { # open patch output open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, - '-p', $hash_parent_base, $hash_base, - "--", $file_name + '-p', ($format eq 'html' ? "--full-index" : ()), + $hash_parent_base, $hash_base, + "--", (defined $file_parent ? $file_parent : ()), $file_name or die_error(undef, "Open git-diff-tree failed"); } @@ -3975,7 +4340,8 @@ sub git_blobdiff { } # open patch output - open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, + open $fd, "-|", git_cmd(), "diff", @diff_opts, + '-p', ($format eq 'html' ? "--full-index" : ()), $hash_parent, $hash, "--" or die_error(undef, "Open git-diff failed"); } else { @@ -4099,8 +4465,10 @@ sub git_commitdiff { } } + my $hash_parent_param = $hash_parent; if (!defined $hash_parent) { - $hash_parent = $co{'parent'} || '--root'; + $hash_parent_param = + @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root'; } # read commitdiff @@ -4109,19 +4477,19 @@ sub git_commitdiff { if ($format eq 'html') { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, "--no-commit-id", "--patch-with-raw", "--full-index", - $hash_parent, $hash, "--" + $hash_parent_param, $hash, "--" or die_error(undef, "Open git-diff-tree failed"); while (my $line = <$fd>) { chomp $line; # empty line ends raw part of diff-tree output last unless $line; - push @difftree, $line; + push @difftree, scalar parse_difftree_raw_line($line); } } elsif ($format eq 'plain') { open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, - '-p', $hash_parent, $hash, "--" + '-p', $hash_parent_param, $hash, "--" or die_error(undef, "Open git-diff-tree failed"); } else { @@ -4177,10 +4545,10 @@ TEXT # write patch if ($format eq 'html') { - git_difftree_body(\@difftree, $hash, $hash_parent); + git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}}); print "
\n"; - git_patchset_body($fd, \@difftree, $hash, $hash_parent); + git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}}); close $fd; print "\n"; # class="page_body" git_footer_html(); @@ -4220,12 +4588,7 @@ sub git_history { $ftype = git_get_type($hash); } - open my $fd, "-|", - git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name - or die_error(undef, "Open git-rev-list-failed"); - my @revlist = map { chomp; $_ } <$fd>; - close $fd - or die_error(undef, "Reading git-rev-list failed"); + my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name); my $paging_nav = ''; if ($page > 0) { @@ -4241,7 +4604,7 @@ sub git_history { $paging_nav .= "first"; $paging_nav .= " ⋅ prev"; } - if ($#revlist >= (100 * ($page+1)-1)) { + if ($#commitlist >= 100) { $paging_nav .= " ⋅ " . $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name, page=>$page+1), @@ -4250,11 +4613,11 @@ sub git_history { $paging_nav .= " ⋅ next"; } my $next_link = ''; - if ($#revlist >= (100 * ($page+1)-1)) { + if ($#commitlist >= 100) { $next_link = $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name, page=>$page+1), - -title => "Alt-n"}, "next"); + -accesskey => "n", -title => "Alt-n"}, "next"); } git_header_html(); @@ -4262,7 +4625,7 @@ sub git_history { git_print_header_div('commit', esc_html($co{'title'}), $hash_base); git_print_page_path($file_name, $ftype, $hash_base); - git_history_body(\@revlist, ($page * 100), $#revlist, + git_history_body(\@commitlist, 0, 99, $refs, $hash_base, $ftype, $next_link); git_footer_html(); @@ -4296,6 +4659,12 @@ sub git_search { die_error('403 Permission denied', "Permission denied"); } } + if ($searchtype eq 'grep') { + my ($have_grep) = gitweb_check_feature('grep'); + if (!$have_grep) { + die_error('403 Permission denied', "Permission denied"); + } + } git_header_html(); @@ -4308,20 +4677,20 @@ sub git_search { } elsif ($searchtype eq 'committer') { $greptype = "--committer="; } - $greptype .= $searchtext; + $greptype .= $search_regexp; my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype); my $paging_nav = ''; if ($page > 0) { $paging_nav .= $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype)}, - "first"); + searchtext=>$searchtext, searchtype=>$searchtype)}, + "first"); $paging_nav .= " ⋅ " . $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page-1), - -accesskey => "p", -title => "Alt-p"}, "prev"); + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); } else { $paging_nav .= "first"; $paging_nav .= " ⋅ prev"; @@ -4329,9 +4698,9 @@ sub git_search { if ($#commitlist >= 100) { $paging_nav .= " ⋅ " . $cgi->a({-href => href(action=>"search", hash=>$hash, - searchtext=>$searchtext, searchtype=>$searchtype, - page=>$page+1), - -accesskey => "n", -title => "Alt-n"}, "next"); + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); } else { $paging_nav .= " ⋅ next"; } @@ -4339,9 +4708,9 @@ sub git_search { 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"); + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); } git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); @@ -4357,8 +4726,10 @@ sub git_search { my $alternate = 1; $/ = "\n"; my $git_command = git_cmd_str(); + my $searchqtext = $searchtext; + $searchqtext =~ s/'/'\\''/; open my $fd, "-|", "$git_command rev-list $hash | " . - "$git_command diff-tree -r --stdin -S\'$searchtext\'"; + "$git_command diff-tree -r --stdin -S\'$searchqtext\'"; undef %co; my @files; while (my $line = <$fd>) { @@ -4412,6 +4783,73 @@ sub git_search { print "
"; print $cgi->a({ -href => "$blamed#l$orig_lineno", - -id => "l$lineno", - -class => "linenr" }, - esc_html($lineno)); + -id => "l$lineno", + -class => "linenr" }, + esc_html($lineno)); print "" . esc_html($data) . "
\n"; } + + if ($searchtype eq 'grep') { + git_print_page_nav('','', $hash,$co{'tree'},$hash); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + + print "\n"; + my $alternate = 1; + my $matches = 0; + $/ = "\n"; + open my $fd, "-|", git_cmd(), 'grep', '-n', '-i', '-E', $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 "\n"; + if ($alternate++) { + print "\n"; + } else { + print "\n"; + } + print "\n"; + if ($matches > 1000) { + print "
Too many matches, listing trimmed
\n"; + } + } else { + print "
No matches found
\n"; + } + close $fd; + + print "
". + $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, + file_name=>"$file"), + -class => "list"}, esc_path($file)); + print "\n"; + $lastfile = $file; + } + if ($binary) { + print "
Binary file
\n"; + } else { + $ltext = untabify($ltext); + if ($ltext =~ m/^(.*)($searchtext)(.*)$/i) { + $ltext = esc_html($1, -nbsp=>1); + $ltext .= ''; + $ltext .= esc_html($2, -nbsp=>1); + $ltext .= ''; + $ltext .= esc_html($3, -nbsp=>1); + } else { + $ltext = esc_html($ltext, -nbsp=>1); + } + print "
" . + $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'}, + file_name=>"$file").'#l'.$lno, + -class => "linenr"}, sprintf('%4i', $lno)) + . ' ' . $ltext . "
\n"; + } + } + if ($lastfile) { + print "
\n"; + } git_footer_html(); } @@ -4422,6 +4860,20 @@ sub git_search_help {
commit
The commit messages and authorship information will be scanned for the given string.
+EOT + my ($have_grep) = gitweb_check_feature('grep'); + if ($have_grep) { + print <grep +
All files in the currently selected tree (HEAD unless you are explicitly browsing + a different one) are searched for the given +regular expression +(POSIX extended) and the matches are listed. On large +trees, this search can take a while and put some strain on the server, so please use it with +some consideration.
+EOT + } + print <author
Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.
committer
@@ -4450,7 +4902,7 @@ sub git_shortlog { } my $refs = git_get_references(); - my @commitlist = parse_commits($head, 101, (100 * $page)); + my @commitlist = parse_commits($hash, 101, (100 * $page)); my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1))); my $next_link = '';