# 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";
# (relative to the current git repository)
our $mimetypes_file = undef;
+# assume this charset if line contains non-UTF-8 characters;
+# it should be valid encoding (see Encoding::Supported(3pm) for list),
+# for which encoding all byte sequences are valid, for example
+# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
+# could be even 'utf-8' for the old behavior)
+our $fallback_encoding = 'latin1';
+
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
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(<feature>) to check if <feature> is enabled
# Enable the 'blame' blob view, showing the last commit that modified
# $feature{'snapshot'}{'default'} = [undef];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'snapshot'}{'override'} = 1;
- # and in project config gitweb.snapshot = none|gzip|bzip2;
+ # and in project config gitweb.snapshot = none|gzip|bzip2|zip;
'snapshot' => {
'sub' => \&feature_snapshot,
'override' => 0,
# 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.
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 ();
}
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');
}
}
+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;
-}
-
-our $searchtype = $cgi->param('st');
-if (defined $searchtype) {
- if ($searchtype =~ m/[^a-z]/) {
- die_error(undef, "Invalid searchtype parameter");
- }
+ $search_regexp = quotemeta $searchtext;
}
# now read PATH_INFO and use it as alternative to parameters
return $input;
}
+# decode sequences of octets in utf8 into Perl's internal form,
+# which is utf-8 with utf8 flag set if needed. gitweb writes out
+# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
+sub to_utf8 {
+ my $str = shift;
+ my $res;
+ eval { $res = decode_utf8($str, Encode::FB_CROAK); };
+ if (defined $res) {
+ return $res;
+ } else {
+ return decode($fallback_encoding, $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 {
my $str = shift;
my %opts = @_;
- $str = decode_utf8($str);
+ $str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ / /g;
my $str = shift;
my %opts = @_;
- $str = decode_utf8($str);
+ $str = to_utf8($str);
$str = $cgi->escapeHTML($str);
if ($opts{'-nbsp'}) {
$str =~ s/ / /g;
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";
if (length($short) < length($long)) {
return $cgi->a({-href => $href, -class => "list subject",
- -title => decode_utf8($long)},
+ -title => to_utf8($long)},
esc_html($short) . $extra);
} else {
return $cgi->a({-href => $href, -class => "list subject"},
}
}
-# format patch (diff) line (rather not to be used for diff headers)
+# format git diff header line, i.e. "diff --(git|combined|cc) ..."
+sub format_git_diff_header_line {
+ my $line = shift;
+ my $diffinfo = shift;
+ my ($from, $to) = @_;
+
+ if ($diffinfo->{'nparents'}) {
+ # combined diff
+ $line =~ s!^(diff (.*?) )"?.*$!$1!;
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+ esc_path($to->{'file'}));
+ } else { # file was deleted (no href)
+ $line .= esc_path($to->{'file'});
+ }
+ } else {
+ # "ordinary" diff
+ $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
+ if ($from->{'href'}) {
+ $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
+ 'a/' . esc_path($from->{'file'}));
+ } else { # file was added (no href)
+ $line .= 'a/' . esc_path($from->{'file'});
+ }
+ $line .= ' ';
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+ 'b/' . esc_path($to->{'file'}));
+ } else { # file was deleted
+ $line .= 'b/' . esc_path($to->{'file'});
+ }
+ }
+
+ return "<div class=\"diff header\">$line</div>\n";
+}
+
+# format extended diff header line, before patch itself
+sub format_extended_diff_header_line {
+ my $line = shift;
+ my $diffinfo = shift;
+ my ($from, $to) = @_;
+
+ # match <path>
+ if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+ $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ }
+ if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+ esc_path($to->{'file'}));
+ }
+ # match single <mode>
+ if ($line =~ m/\s(\d{6})$/) {
+ $line .= '<span class="info"> (' .
+ file_type_long($1) .
+ ')</span>';
+ }
+ # match <hash>
+ if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
+ # can match only for combined diff
+ $line = 'index ';
+ for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+ if ($from->{'href'}[$i]) {
+ $line .= $cgi->a({-href=>$from->{'href'}[$i],
+ -class=>"hash"},
+ substr($diffinfo->{'from_id'}[$i],0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+ # separator
+ $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+ }
+ $line .= '..';
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+
+ } elsif ($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"},
+ substr($diffinfo->{'from_id'},0,7));
+ } else {
+ $from_link = '0' x 7;
+ }
+ if ($to->{'href'}) {
+ $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $to_link = '0' x 7;
+ }
+ my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+ $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+ }
+
+ return $line . "<br/>\n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+ my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+ my $line;
+ my $result = '';
+
+ $line = $from_line;
+ #assert($line =~ m/^---/) if DEBUG;
+ # no extra formatting for "^--- /dev/null"
+ if (! $diffinfo->{'nparents'}) {
+ # ordinary (single parent) diff
+ if ($line =~ m!^--- "?a/!) {
+ if ($from->{'href'}) {
+ $line = '--- a/' .
+ $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ } else {
+ $line = '--- a/' .
+ esc_path($from->{'file'});
+ }
+ }
+ $result .= qq!<div class="diff from_file">$line</div>\n!;
+
+ } else {
+ # combined diff (merge commit)
+ for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+ if ($from->{'href'}[$i]) {
+ $line = '--- ' .
+ $cgi->a({-href=>href(action=>"blobdiff",
+ hash_parent=>$diffinfo->{'from_id'}[$i],
+ hash_parent_base=>$parents[$i],
+ file_parent=>$from->{'file'}[$i],
+ hash=>$diffinfo->{'to_id'},
+ hash_base=>$hash,
+ file_name=>$to->{'file'}),
+ -class=>"path",
+ -title=>"diff" . ($i+1)},
+ $i+1) .
+ '/' .
+ $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
+ esc_path($from->{'file'}[$i]));
+ } else {
+ $line = '--- /dev/null';
+ }
+ $result .= qq!<div class="diff from_file">$line</div>\n!;
+ }
+ }
+
+ $line = $to_line;
+ #assert($line =~ m/^\+\+\+/) if DEBUG;
+ # no extra formatting for "^+++ /dev/null"
+ if ($line =~ m!^\+\+\+ "?b/!) {
+ if ($to->{'href'}) {
+ $line = '+++ b/' .
+ $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+ esc_path($to->{'file'}));
+ } else {
+ $line = '+++ b/' .
+ esc_path($to->{'file'});
+ }
+ }
+ $result .= qq!<div class="diff to_file">$line</div>\n!;
+
+ return $result;
+}
+
+# create note for patch simplified by combined diff
+sub format_diff_cc_simplified {
+ my ($diffinfo, @parents) = @_;
+ my $result = '';
+
+ $result .= "<div class=\"diff header\">" .
+ "diff --cc ";
+ if (!is_deleted($diffinfo)) {
+ $result .= $cgi->a({-href => href(action=>"blob",
+ hash_base=>$hash,
+ hash=>$diffinfo->{'to_id'},
+ file_name=>$diffinfo->{'to_file'}),
+ -class => "path"},
+ esc_path($diffinfo->{'to_file'}));
+ } else {
+ $result .= esc_path($diffinfo->{'to_file'});
+ }
+ $result .= "</div>\n" . # class="diff header"
+ "<div class=\"diff nodifferences\">" .
+ "Simple merge" .
+ "</div>\n"; # class="diff nodifferences"
+
+ return $result;
+}
+
+# format patch (diff) line (not to be used for diff headers)
sub format_diff_line {
my $line = shift;
my ($from, $to) = @_;
open my $fd, "$projectroot/$path/description" or return undef;
my $descr = <$fd>;
close $fd;
- chomp $descr;
+ if (defined $descr) {
+ chomp $descr;
+ }
return $descr;
}
if (check_export_ok("$projectroot/$path")) {
my $pr = {
path => $path,
- owner => decode_utf8($owner),
+ owner => to_utf8($owner),
};
push @list, $pr;
(my $forks_path = $path) =~ s/\.git$//;
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'
my ($pr, $ow) = split ' ', $line;
$pr = unescape($pr);
$ow = unescape($ow);
- if ($pr eq $project) {
- $owner = decode_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");
}
'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));
}
+ return (undef, undef);
}
sub git_get_references {
return wantarray ? %res : \%res;
}
+# generates _two_ hashes, references to which are passed as 2 and 3 argument
+sub parse_from_to_diffinfo {
+ my ($diffinfo, $from, $to, @parents) = @_;
+
+ if ($diffinfo->{'nparents'}) {
+ # combined diff
+ $from->{'file'} = [];
+ $from->{'href'} = [];
+ 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'};
+ if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
+ $from->{'href'}[$i] = href(action=>"blob",
+ hash_base=>$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'};
+ }
+}
+
## ......................................................................
## parse to array of hashes functions
}
my $owner = $gcos;
$owner =~ s/[,;].*$//;
- return decode_utf8($owner);
+ return to_utf8($owner);
}
## ......................................................................
my $title = "$site_name";
if (defined $project) {
- $title .= " - " . decode_utf8($project);
+ $title .= " - " . to_utf8($project);
if (defined $action) {
$title .= "/$action";
if (defined $file_name) {
}
print "\n";
}
+ print "</div>\n";
+
my ($have_search) = gitweb_check_feature('search');
if ((defined $project) && ($have_search)) {
if (!defined $searchtext) {
} 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) .
"<div class=\"search\">\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',
- -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" .
"</div>" .
$cgi->end_form() . "\n";
}
- print "</div>\n";
}
sub git_footer_html {
print "<div class=\"page_path\">";
print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
- -title => 'tree root'}, decode_utf8("[$project]"));
+ -title => 'tree root'}, to_utf8("[$project]"));
print " / ";
if (defined $name) {
my @dirname = split '/', $name;
}
}
+sub is_deleted {
+ my $diffinfo = shift;
+
+ return $diffinfo->{'to_id'} eq ('0' x 40);
+}
sub git_difftree_body {
my ($difftree, $hash, @parents) = @_;
print "<table class=\"" .
(@parents > 1 ? "combined " : "") .
"diff_tree\">\n";
+
+ # header only for combined diff in 'commitdiff' view
+ my $has_header = @parents > 1 && $action eq 'commitdiff';
+ if ($has_header) {
+ # table header
+ print "<thead><tr>\n" .
+ "<th></th><th></th>\n"; # filename, patchN link
+ for (my $i = 0; $i < @parents; $i++) {
+ my $par = $parents[$i];
+ print "<th>" .
+ $cgi->a({-href => href(action=>"commitdiff",
+ hash=>$hash, hash_parent=>$par),
+ -title => 'commitdiff to parent number ' .
+ ($i+1) . ': ' . substr($par,0,7)},
+ $i+1) .
+ " </th>\n";
+ }
+ print "</tr></thead>\n<tbody>\n";
+ }
+
my $alternate = 1;
my $patchno = 0;
foreach my $line (@{$difftree}) {
fill_from_file_info($diff, @parents)
unless exists $diff->{'from_file'};
- if ($diff->{'to_id'} ne ('0' x 40)) {
+ if (!is_deleted($diff)) {
# file exists in the result (child) commit
print "<td>" .
$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
} # we should not encounter Unmerged (U) or Unknown (X) status
print "</tr>\n";
}
+ print "</tbody>" if $has_header;
print "</table>\n";
}
# advance raw git-diff output if needed
$patch_idx++ if defined $diffinfo;
- # 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]);
+ # 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;
}
+
+ 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'}) {
+ print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+ format_diff_cc_simplified($diffinfo, @hash_parents) .
+ "</div>\n"; # class="patch"
+
+ $patch_idx++;
+ $patch_number++;
+ }
+ } 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'} = [];
delete $from{'href'};
}
}
+
$to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
- if ($diffinfo->{'status'} ne "D") { # not deleted 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'});
# print "git diff" header
$patch_line = shift @diff_header;
- 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 "<div class=\"diff header\">$patch_line</div>\n";
+ print format_git_diff_header_line($patch_line, $diffinfo,
+ \%from, \%to);
# print extended diff header
print "<div class=\"diff extended_header\">\n" if (@diff_header > 0);
EXTENDED_HEADER:
foreach $patch_line (@diff_header) {
- # match <path>
- if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) {
- $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"},
- 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'}));
- }
- # match single <mode>
- if ($patch_line =~ m/\s(\d{6})$/) {
- $patch_line .= '<span class="info"> (' .
- file_type_long($1) .
- ')</span>';
- }
- # match <hash>
- 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"},
- substr($diffinfo->{'from_id'},0,7));
- } else {
- $from_link = '0' x 7;
- }
- if ($to{'href'}) {
- $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"},
- substr($diffinfo->{'to_id'},0,7));
- } else {
- $to_link = '0' x 7;
- }
- #affirm {
- # my ($from_hash, $to_hash) =
- # ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/);
- # my ($from_id, $to_id) =
- # ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
- # ($from_hash eq $from_id) && ($to_hash eq $to_id);
- #} if DEBUG;
- my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
- $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
- }
- print $patch_line . "<br/>\n";
+ print format_extended_diff_header_line($patch_line, $diffinfo,
+ \%from, \%to);
}
print "</div>\n" if (@diff_header > 0); # class="diff extended_header"
}
next PATCH if ($patch_line =~ m/^diff /);
#assert($patch_line =~ m/^---/) if DEBUG;
- 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'}));
- }
- print "<div class=\"diff from_file\">$patch_line</div>\n";
+ #assert($patch_line eq $last_patch_line) if DEBUG;
$patch_line = <$fd>;
chomp $patch_line;
+ #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
- #assert($patch_line =~ m/^+++/) if DEBUG;
- if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) {
- $patch_line = '+++ b/' .
- $cgi->a({-href=>$to{'href'}, -class=>"path"},
- esc_path($to{'file'}));
- }
- print "<div class=\"diff to_file\">$patch_line</div>\n";
+ print format_diff_from_to_header($last_patch_line, $patch_line,
+ $diffinfo, \%from, \%to,
+ @hash_parents);
# the patch itself
LINE:
} continue {
print "</div>\n"; # class="patch"
}
- print "<div class=\"diff nodifferences\">No differences found</div>\n" if (!$patch_number);
+
+ # 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;
+ $patch_idx < @$difftree;
+ $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]);
+ }
+
+ # generate anchor for "patch" links in difftree / whatchanged part
+ print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
+ format_diff_cc_simplified($diffinfo, @hash_parents) .
+ "</div>\n"; # class="patch"
+
+ $patch_number++;
+ }
+
+ if ($patch_number == 0) {
+ if (@hash_parents > 1) {
+ print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
+ } else {
+ print "<div class=\"diff nodifferences\">No differences found</div>\n";
+ }
+ }
print "</div>\n"; # class="patchset"
}
($pr->{'age'}, $pr->{'age_string'}) = @aa;
if (!defined $pr->{'descr'}) {
my $descr = git_get_project_description($pr->{'path'}) || "";
- $pr->{'descr_long'} = decode_utf8($descr);
- $pr->{'descr'} = chop_str($descr, 25, 5);
+ $pr->{'descr_long'} = to_utf8($descr);
+ $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'};
esc_html($pr->{'descr'})) . "</td>\n" .
"<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
print "<td class=\"". age_class($pr->{'age'}) . "\">" .
- $pr->{'age_string'} . "</td>\n" .
+ (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
"<td class=\"link\">" .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
esc_html(chop_str($co{'title'}, 50)) . "<br/>");
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) || "";
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'});
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);
print "<div class=\"title\"> </div>\n";
print "<table cellspacing=\"0\">\n" .
"<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
- "<tr><td>owner</td><td>$owner</td></tr>\n" .
- "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
+ "<tr><td>owner</td><td>$owner</td></tr>\n";
+ if (defined $cd{'rfc2822'}) {
+ print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\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";
# 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');
$hash = git_get_head_hash($project);
}
- my $filename = decode_utf8(basename($project)) . "-$hash.tar.$suffix";
+ my $git = 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";
+ }
print $cgi->header(
-type => "application/$ctype",
-content_disposition => 'inline; filename="' . "$filename" . '"',
-status => '200 OK');
- my $git = git_cmd_str();
- 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");
+ open my $fd, "-|", $cmd
+ or die_error(undef, "Execute git-archive failed");
binmode STDOUT, ':raw';
print <$fd>;
binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
die_error(undef, "Unknown commit object");
}
- # we need to prepare $formats_nav before any parameter munging
+ # choose format for commitdiff for merge
+ if (! defined $hash_parent && @{$co{'parents'}} > 1) {
+ $hash_parent = '--cc';
+ }
+ # we need to prepare $formats_nav before almost any parameter munging
my $formats_nav;
if ($format eq 'html') {
$formats_nav =
hash=>$hash, hash_parent=>$hash_parent)},
"raw");
- if (defined $hash_parent) {
+ if (defined $hash_parent &&
+ $hash_parent ne '-c' && $hash_parent ne '--cc') {
# commitdiff with two commits given
my $hash_parent_short = $hash_parent;
if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
$hash_parent_short = substr($hash_parent, 0, 7);
}
$formats_nav .=
- ' (from: ' .
+ ' (from';
+ for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
+ if ($co{'parents'}[$i] eq $hash_parent) {
+ $formats_nav .= ' parent ' . ($i+1);
+ last;
+ }
+ }
+ $formats_nav .= ': ' .
$cgi->a({-href => href(action=>"commitdiff",
hash=>$hash_parent)},
esc_html($hash_parent_short)) .
')';
} else {
# merge commit
+ if ($hash_parent eq '--cc') {
+ $formats_nav .= ' | ' .
+ $cgi->a({-href => href(action=>"commitdiff",
+ hash=>$hash, hash_parent=>'-c')},
+ 'combined');
+ } else { # $hash_parent eq '-c'
+ $formats_nav .= ' | ' .
+ $cgi->a({-href => href(action=>"commitdiff",
+ hash=>$hash, hash_parent=>'--cc')},
+ 'compact');
+ }
$formats_nav .=
' (merge: ' .
join(' ', map {
}
my $hash_parent_param = $hash_parent;
- if (!defined $hash_parent) {
+ if (!defined $hash_parent_param) {
+ # --cc for multiple parents, --root for parentless
$hash_parent_param =
- @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root';
+ @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
}
# read commitdiff
# write patch
if ($format eq 'html') {
- git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}});
+ my $use_parents = !defined $hash_parent ||
+ $hash_parent eq '-c' || $hash_parent eq '--cc';
+ git_difftree_body(\@difftree, $hash,
+ $use_parents ? @{$co{'parents'}} : $hash_parent);
print "<br/>\n";
- git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}});
+ git_patchset_body($fd, \@difftree, $hash,
+ $use_parents ? @{$co{'parents'}} : $hash_parent);
close $fd;
print "</div>\n"; # class="page_body"
git_footer_html();
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();
} elsif ($searchtype eq 'committer') {
$greptype = "--committer=";
}
- $greptype .= $searchtext;
+ $greptype .= $search_regexp;
my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
my $paging_nav = '';
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>) {
print "</table>\n";
}
+
+ if ($searchtype eq 'grep') {
+ git_print_page_nav('','', $hash,$co{'tree'},$hash);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+ print "<table cellspacing=\"0\">\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 "</td></tr>\n";
+ if ($alternate++) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ print "<td class=\"list\">".
+ $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+ file_name=>"$file"),
+ -class => "list"}, esc_path($file));
+ print "</td><td>\n";
+ $lastfile = $file;
+ }
+ if ($binary) {
+ print "<div class=\"binary\">Binary file</div>\n";
+ } else {
+ $ltext = untabify($ltext);
+ if ($ltext =~ m/^(.*)($searchtext)(.*)$/i) {
+ $ltext = esc_html($1, -nbsp=>1);
+ $ltext .= '<span class="match">';
+ $ltext .= esc_html($2, -nbsp=>1);
+ $ltext .= '</span>';
+ $ltext .= esc_html($3, -nbsp=>1);
+ } else {
+ $ltext = esc_html($ltext, -nbsp=>1);
+ }
+ print "<div class=\"pre\">" .
+ $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
+ file_name=>"$file").'#l'.$lno,
+ -class => "linenr"}, sprintf('%4i', $lno))
+ . ' ' . $ltext . "</div>\n";
+ }
+ }
+ if ($lastfile) {
+ print "</td></tr>\n";
+ if ($matches > 1000) {
+ print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
+ }
+ } else {
+ print "<div class=\"diff nodifferences\">No matches found</div>\n";
+ }
+ close $fd;
+
+ print "</table>\n";
+ }
git_footer_html();
}
<dl>
<dt><b>commit</b></dt>
<dd>The commit messages and authorship information will be scanned for the given string.</dd>
+EOT
+ my ($have_grep) = gitweb_check_feature('grep');
+ if ($have_grep) {
+ print <<EOT;
+<dt><b>grep</b></dt>
+<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
+ a different one) are searched for the given
+<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a>
+(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.</dd>
+EOT
+ }
+ print <<EOT;
<dt><b>author</b></dt>
<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.</dd>
<dt><b>committer</b></dt>
# get list of changed files
open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
- $co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ())
+ $co{'parent'} || "--root",
+ $co{'id'}, "--", (defined $file_name ? $file_name : ())
or next;
my @difftree = map { chomp; $_ } <$fd>;
close $fd