X-Git-Url: https://git.ladys.computer/Gitweb/blobdiff_plain/0dfee0388916719d0dc1c9e03751f1a24599de414c7f205e39494c006e1099f8..110e51bbc9362e2d8eaa921a9125b89224535fb2bd7e90cc9a0cff74d67e63ab:/gitweb.perl diff --git a/gitweb.perl b/gitweb.perl index 2eb76f3..c693ed9 100755 --- a/gitweb.perl +++ b/gitweb.perl @@ -31,9 +31,6 @@ our $GIT = "++GIT_BINDIR++/git"; #our $projectroot = "/pub/scm"; our $projectroot = "++GITWEB_PROJECTROOT++"; -# location for temporary files needed for diffs -our $git_temp = "/tmp/gitweb"; - # target of the home link on top of all pages our $home_link = $my_uri || "/"; @@ -51,10 +48,19 @@ our $home_text = "++GITWEB_HOMETEXT++"; our $stylesheet = "++GITWEB_CSS++"; # URI of GIT logo our $logo = "++GITWEB_LOGO++"; +# URI of GIT favicon, assumed to be image/png type +our $favicon = "++GITWEB_FAVICON++"; # source of projects list our $projects_list = "++GITWEB_LIST++"; +# show repository only if this file exists +# (only effective if this variable evaluates to true) +our $export_ok = "++GITWEB_EXPORT_OK++"; + +# only allow viewing of repositories also shown on the overview page +our $strict_export = "++GITWEB_STRICT_EXPORT++"; + # list of git base URLs used for URL to where fetch project from, # i.e. full URL is "$git_base_url/$project" our @git_base_url_list = ("++GITWEB_BASE_URL++"); @@ -70,9 +76,16 @@ our $mimetypes_file = undef; # You define site-wide feature defaults here; override them with # $GITWEB_CONFIG as necessary. our %feature = ( - # feature => {'sub' => feature-sub, 'override' => allow-override, 'default' => [ default options...] - # if feature is overridable, feature-sub will be called with default options; - # return value indicates if to enable specified feature + # feature => { + # 'sub' => feature-sub (subroutine), + # 'override' => allow-override (boolean), + # 'default' => [ default options...] (array reference)} + # + # 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 + # + # use gitweb_check_feature() to check if is enabled 'blame' => { 'sub' => \&feature_blame, @@ -84,11 +97,16 @@ our %feature = ( 'override' => 0, # => [content-encoding, suffix, program] 'default' => ['x-gzip', 'gz', 'gzip']}, + + 'pickaxe' => { + 'sub' => \&feature_pickaxe, + 'override' => 0, + 'default' => [1]}, ); sub gitweb_check_feature { my ($name) = @_; - return undef unless exists $feature{$name}; + return unless exists $feature{$name}; my ($sub, $override, @defaults) = ( $feature{$name}{'sub'}, $feature{$name}{'override'}, @@ -98,9 +116,9 @@ sub gitweb_check_feature { } # To enable system wide have in $GITWEB_CONFIG -# $feature{'blame'}{'default'} = [1]; -# To have project specific config enable override in $GITWEB_CONFIG -# $feature{'blame'}{'override'} = 1; +# $feature{'blame'}{'default'} = [1]; +# To have project specific config enable override in $GITWEB_CONFIG +# $feature{'blame'}{'override'} = 1; # and in project config gitweb.blame = 0|1; sub feature_blame { @@ -116,9 +134,9 @@ sub feature_blame { } # To disable system wide have in $GITWEB_CONFIG -# $feature{'snapshot'}{'default'} = [undef]; -# To have project specific config enable override in $GITWEB_CONFIG -# $feature{'blame'}{'override'} = 1; +# $feature{'snapshot'}{'default'} = [undef]; +# To have project specific config enable override in $GITWEB_CONFIG +# $feature{'blame'}{'override'} = 1; # and in project config gitweb.snapshot = none|gzip|bzip2 sub feature_snapshot { @@ -137,16 +155,41 @@ sub feature_snapshot { return ($ctype, $suffix, $command); } +# To enable system wide have in $GITWEB_CONFIG +# $feature{'pickaxe'}{'default'} = [1]; +# To have project specific config enable override in $GITWEB_CONFIG +# $feature{'pickaxe'}{'override'} = 1; +# and in project config gitweb.pickaxe = 0|1; + +sub feature_pickaxe { + my ($val) = git_get_project_config('pickaxe', '--bool'); + + if ($val eq 'true') { + return (1); + } elsif ($val eq 'false') { + return (0); + } + + return ($_[0]); +} + +# 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 + our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; -require $GITWEB_CONFIG if -e $GITWEB_CONFIG; +do $GITWEB_CONFIG if -e $GITWEB_CONFIG; # version of the core git binary our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown"; $projects_list ||= $projectroot; -if (! -d $git_temp) { - mkdir($git_temp, 0700) || die_error(undef, "Couldn't mkdir $git_temp"); -} # ====================================================================== # input validation and dispatch @@ -157,70 +200,66 @@ if (defined $action) { } } -our $project = ($cgi->param('p') || $ENV{'PATH_INFO'}); -if (defined $project) { - $project =~ s|^/||; - $project =~ s|/$||; - $project = undef unless $project; -} +# parameters which are pathnames +our $project = $cgi->param('p'); if (defined $project) { - if (!validate_input($project)) { - die_error(undef, "Invalid project parameter"); - } - if (!(-d "$projectroot/$project")) { - die_error(undef, "No such directory"); - } - if (!(-e "$projectroot/$project/HEAD")) { + if (!validate_pathname($project) || + !(-d "$projectroot/$project") || + !(-e "$projectroot/$project/HEAD") || + ($export_ok && !(-e "$projectroot/$project/$export_ok")) || + ($strict_export && !project_in_list($project))) { + undef $project; die_error(undef, "No such project"); } - $ENV{'GIT_DIR'} = "$projectroot/$project"; } our $file_name = $cgi->param('f'); if (defined $file_name) { - if (!validate_input($file_name)) { + if (!validate_pathname($file_name)) { die_error(undef, "Invalid file parameter"); } } our $file_parent = $cgi->param('fp'); if (defined $file_parent) { - if (!validate_input($file_parent)) { + if (!validate_pathname($file_parent)) { die_error(undef, "Invalid file parent parameter"); } } +# parameters which are refnames our $hash = $cgi->param('h'); if (defined $hash) { - if (!validate_input($hash)) { + if (!validate_refname($hash)) { die_error(undef, "Invalid hash parameter"); } } our $hash_parent = $cgi->param('hp'); if (defined $hash_parent) { - if (!validate_input($hash_parent)) { + if (!validate_refname($hash_parent)) { die_error(undef, "Invalid hash parent parameter"); } } our $hash_base = $cgi->param('hb'); if (defined $hash_base) { - if (!validate_input($hash_base)) { + if (!validate_refname($hash_base)) { die_error(undef, "Invalid hash base parameter"); } } our $hash_parent_base = $cgi->param('hpb'); if (defined $hash_parent_base) { - if (!validate_input($hash_parent_base)) { + if (!validate_refname($hash_parent_base)) { die_error(undef, "Invalid hash parent base parameter"); } } +# other parameters our $page = $cgi->param('pg'); if (defined $page) { - if ($page =~ m/[^0-9]$/) { + if ($page =~ m/[^0-9]/) { die_error(undef, "Invalid page parameter"); } } @@ -233,6 +272,55 @@ if (defined $searchtext) { $searchtext = quotemeta $searchtext; } +# now read PATH_INFO and use it as alternative to parameters +sub evaluate_path_info { + return if defined $project; + my $path_info = $ENV{"PATH_INFO"}; + return if !$path_info; + $path_info =~ s,^/+,,; + return if !$path_info; + # find which part of PATH_INFO is project + $project = $path_info; + $project =~ s,/+$,,; + while ($project && !-e "$projectroot/$project/HEAD") { + $project =~ s,/*[^/]*$,,; + } + # validate project + $project = validate_pathname($project); + if (!$project || + ($export_ok && !-e "$projectroot/$project/$export_ok") || + ($strict_export && !project_in_list($project))) { + undef $project; + return; + } + # do not change any parameters if an action is given using the query string + return if $action; + $path_info =~ s,^$project/*,,; + my ($refname, $pathname) = split(/:/, $path_info, 2); + if (defined $pathname) { + # we got "project.git/branch:filename" or "project.git/branch:dir/" + # we could use git_get_type(branch:pathname), but it needs $git_dir + $pathname =~ s,^/+,,; + if (!$pathname || substr($pathname, -1) eq "/") { + $action ||= "tree"; + $pathname =~ s,/$,,; + } else { + $action ||= "blob_plain"; + } + $hash_base ||= validate_refname($refname); + $file_name ||= validate_pathname($pathname); + } elsif (defined $refname) { + # we got "project.git/branch" + $action ||= "shortlog"; + $hash ||= validate_refname($refname); + } +} +evaluate_path_info(); + +# path to the current git repository +our $git_dir; +$git_dir = "$projectroot/$project" if $project; + # dispatch my %actions = ( "blame" => \&git_blame2, @@ -257,6 +345,7 @@ my %actions = ( # those below don't need $project "opml" => \&git_opml, "project_list" => \&git_project_list, + "project_index" => \&git_project_index, ); if (defined $project) { @@ -267,6 +356,10 @@ if (defined $project) { if (!defined($actions{$action})) { die_error(undef, "Unknown action"); } +if ($action !~ m/^(opml|project_list|project_index)$/ && + !$project) { + die_error(undef, "Project needed"); +} $actions{$action}->(); exit; @@ -286,11 +379,12 @@ sub href(%) { hash_base => "hb", hash_parent_base => "hpb", page => "pg", + order => "o", searchtext => "s", ); my %mapping = @mapping; - $params{"project"} ||= $project; + $params{'project'} = $project unless exists $params{'project'}; my @result = (); for (my $i = 0; $i < @mapping; $i += 2) { @@ -306,16 +400,34 @@ sub href(%) { ## ====================================================================== ## validation, quoting/unquoting and escaping -sub validate_input { - my $input = shift; +sub validate_pathname { + my $input = shift || return undef; - if ($input =~ m/^[0-9a-fA-F]{40}$/) { - return $input; + # no '.' or '..' as elements of path, i.e. no '.' nor '..' + # at the beginning, at the end, and between slashes. + # also this catches doubled slashes + if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { + return undef; } - if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) { + # no null characters + if ($input =~ m!\0!) { return undef; } - if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) { + return $input; +} + +sub validate_refname { + my $input = shift || return undef; + + # textual hashes are O.K. + if ($input =~ m/^[0-9a-fA-F]{40}$/) { + return $input; + } + # it must be correct pathname + $input = validate_pathname($input) + or return undef; + # restrictions on ref name according to git-check-ref-format + if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) { return undef; } return $input; @@ -324,6 +436,15 @@ sub validate_input { # 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; + $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg; + $str =~ s/\+/%2B/g; + $str =~ s/ /\+/g; + return $str; +} + +# quote unsafe chars in whole URL, so some charactrs cannot be quoted +sub esc_url { my $str = shift; $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg; $str =~ s/\+/%2B/g; @@ -364,6 +485,12 @@ sub untabify { return $line; } +sub project_in_list { + my $project = shift; + my @list = git_get_projects_list(); + return @list && scalar(grep { $_->{'path'} eq $project } @list); +} + ## ---------------------------------------------------------------------- ## HTML aware string manipulation @@ -454,7 +581,13 @@ sub mode_str { # convert file mode in octal to file type string sub file_type { - my $mode = oct shift; + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } if (S_ISDIR($mode & S_IFMT)) { return "directory"; @@ -524,7 +657,7 @@ sub format_subject_html { if (length($short) < length($long)) { return $cgi->a({-href => $href, -class => "list subject", - -title => $long}, + -title => decode("utf8", $long, Encode::FB_DEFAULT)}, esc_html($short) . $extra); } else { return $cgi->a({-href => $href, -class => "list subject"}, @@ -555,21 +688,31 @@ sub format_diff_line { ## ---------------------------------------------------------------------- ## git utility subroutines, invoking git commands +# returns path to the core git executable and the --git-dir parameter as list +sub git_cmd { + return $GIT, '--git-dir='.$git_dir; +} + +# returns path to the core git executable and the --git-dir parameter as string +sub git_cmd_str { + return join(' ', git_cmd()); +} + # get HEAD ref of given project as hash sub git_get_head_hash { my $project = shift; - my $oENV = $ENV{'GIT_DIR'}; + my $o_git_dir = $git_dir; my $retval = undef; - $ENV{'GIT_DIR'} = "$projectroot/$project"; - if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") { + $git_dir = "$projectroot/$project"; + if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") { my $head = <$fd>; close $fd; if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) { $retval = $1; } } - if (defined $oENV) { - $ENV{'GIT_DIR'} = $oENV; + if (defined $o_git_dir) { + $git_dir = $o_git_dir; } return $retval; } @@ -578,7 +721,7 @@ sub git_get_head_hash { sub git_get_type { my $hash = shift; - open my $fd, "-|", $GIT, "cat-file", '-t', $hash or return; + open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return; my $type = <$fd>; close $fd or return; chomp $type; @@ -592,7 +735,7 @@ sub git_get_project_config { $key =~ s/^gitweb\.//; return if ($key =~ m/\W/); - my @x = ($GIT, 'repo-config'); + my @x = (git_cmd(), 'repo-config'); if (defined $type) { push @x, $type; } push @x, "--get"; push @x, "gitweb.$key"; @@ -605,35 +748,27 @@ sub git_get_project_config { sub git_get_hash_by_path { my $base = shift; my $path = shift || return undef; + my $type = shift; - my $tree = $base; + $path =~ s,/+$,,; - open my $fd, "-|", $GIT, "ls-tree", $base, "--", $path + open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path or die_error(undef, "Open git-ls-tree failed"); my $line = <$fd>; close $fd or return undef; #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/; + if (defined $type && $type ne $2) { + # type doesn't match + return undef; + } return $3; } ## ...................................................................... ## git utility functions, directly accessing git repository -# assumes that PATH is not symref -sub git_get_hash_by_ref { - my $path = shift; - - open my $fd, "$projectroot/$path" or return undef; - my $head = <$fd>; - close $fd; - chomp $head; - if ($head =~ m/^[0-9a-fA-F]{40}$/) { - return $head; - } -} - sub git_get_project_description { my $path = shift; @@ -647,7 +782,7 @@ sub git_get_project_description { sub git_get_project_url_list { my $path = shift; - open my $fd, "$projectroot/$path/cloneurl" or return undef; + open my $fd, "$projectroot/$path/cloneurl" or return; my @git_project_url_list = map { chomp; $_ } <$fd>; close $fd; @@ -660,22 +795,33 @@ sub git_get_projects_list { if (-d $projects_list) { # search in directory my $dir = $projects_list; - opendir my ($dh), $dir or return undef; - while (my $dir = readdir($dh)) { - if (-e "$projectroot/$dir/HEAD") { - my $pr = { - path => $dir, - }; - push @list, $pr - } - } - closedir($dh); + my $pfxlen = length("$dir"); + + File::Find::find({ + follow_fast => 1, # follow symbolic links + 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 $_); + + my $subdir = substr($File::Find::name, $pfxlen + 1); + # we check related file in $projectroot + if (-e "$projectroot/$subdir/HEAD" && (!$export_ok || + -e "$projectroot/$subdir/$export_ok")) { + push @list, { path => $subdir }; + $File::Find::prune = 1; + } + }, + }, "$dir"); + } elsif (-f $projects_list) { # read from file(url-encoded): # 'git%2Fgit.git Linus+Torvalds' # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' - open my ($fd), $projects_list or return undef; + open my ($fd), $projects_list or return; while (my $line = <$fd>) { chomp $line; my ($path, $owner) = split ' ', $line; @@ -684,7 +830,8 @@ sub git_get_projects_list { if (!defined $path) { next; } - if (-e "$projectroot/$path/HEAD") { + if (-e "$projectroot/$path/HEAD" && (!$export_ok || + -e "$projectroot/$path/$export_ok")) { my $pr = { path => $path, owner => decode("utf8", $owner, Encode::FB_DEFAULT), @@ -732,16 +879,10 @@ sub git_get_project_owner { sub git_get_references { my $type = shift || ""; my %refs; - my $fd; # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{} - if (-f "$projectroot/$project/info/refs") { - open $fd, "$projectroot/$project/info/refs" - or return; - } else { - open $fd, "-|", $GIT, "ls-remote", "." - or return; - } + open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/" + or return; while (my $line = <$fd>) { chomp $line; @@ -757,61 +898,10 @@ sub git_get_references { return \%refs; } -sub git_get_following_references { - my $hash = shift || return undef; - my $type = shift; - my $base = shift || $hash_base || "HEAD"; - - my $refs = git_get_references($type); - open my $fd, "-|", $GIT, "rev-list", $base - or return undef; - my @commits = map { chomp; $_ } <$fd>; - close $fd - or return undef; - - my @reflist; - my $lastref; - - foreach my $commit (@commits) { - foreach my $ref (@{$refs->{$commit}}) { - $lastref = $ref; - push @reflist, $lastref; - } - if ($commit eq $hash) { - last; - } - } - - return wantarray ? @reflist : $lastref; -} - -sub git_get_preceding_references { - my $hash = shift || return undef; - my $type = shift; - - my $refs = git_get_references($type); - open my $fd, "-|", $GIT, "rev-list", $hash - or return undef; - my @commits = map { chomp; $_ } <$fd>; - close $fd - or return undef; - - my @reflist; - - foreach my $commit (@commits) { - foreach my $ref (@{$refs->{$commit}}) { - return $ref unless wantarray; - push @reflist, $ref; - } - } - - return @reflist; -} - sub git_get_rev_name_tags { my $hash = shift || return undef; - open my $fd, "-|", $GIT, "name-rev", "--tags", $hash + open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash or return; my $name_rev = <$fd>; close $fd; @@ -859,7 +949,7 @@ sub parse_tag { my %tag; my @comment; - open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return; + open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return; $tag{'id'} = $tag_id; while (my $line = <$fd>) { chomp $line; @@ -900,7 +990,7 @@ sub parse_commit { @commit_lines = @$commit_text; } else { $/ = "\0"; - open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id + open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id or return; @commit_lines = split '\n', <$fd>; close $fd or return; @@ -1048,9 +1138,30 @@ sub parse_difftree_raw_line { } } # 'c512b523472485aef4fff9e57b229d9d243c967f' - #elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { - # $res{'commit'} = $1; - #} + elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { + $res{'commit'} = $1; + } + + return wantarray ? %res : \%res; +} + +# parse line of git-ls-tree output +sub parse_ls_tree_line ($;%) { + my $line = shift; + my %opts = @_; + my %res; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + if ($opts{'-z'}) { + $res{'name'} = $4; + } else { + $res{'name'} = unquote($4); + } return wantarray ? %res : \%res; } @@ -1059,28 +1170,46 @@ sub parse_difftree_raw_line { ## parse to array of hashes functions sub git_get_refs_list { - my $ref_dir = shift; + my $type = shift || ""; + my %refs; my @reflist; my @refs; - my $pfxlen = length("$projectroot/$project/$ref_dir"); - File::Find::find(sub { - return if (/^\./); - if (-f $_) { - push @refs, substr($File::Find::name, $pfxlen + 1); + open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/" + or return; + while (my $line = <$fd>) { + chomp $line; + if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?([^\^]+))(\^\{\})?$/) { + if (defined $refs{$1}) { + push @{$refs{$1}}, $2; + } else { + $refs{$1} = [ $2 ]; + } + + if (! $4) { # unpeeled, direct reference + push @refs, { hash => $1, name => $3 }; # without type + } elsif ($3 eq $refs[-1]{'name'}) { + # most likely a tag is followed by its peeled + # (deref) one, and when that happens we know the + # previous one was of type 'tag'. + $refs[-1]{'type'} = "tag"; + } } - }, "$projectroot/$project/$ref_dir"); + } + close $fd; - foreach my $ref_file (@refs) { - my $ref_id = git_get_hash_by_ref("$project/$ref_dir/$ref_file"); - my $type = git_get_type($ref_id) || next; + foreach my $ref (@refs) { + my $ref_file = $ref->{'name'}; + my $ref_id = $ref->{'hash'}; + + my $type = $ref->{'type'} || git_get_type($ref_id) || next; my %ref_item = parse_ref($ref_file, $ref_id, $type); push @reflist, \%ref_item; } # sort refs by age @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist; - return \@reflist; + return (\@reflist, \%refs); } ## ---------------------------------------------------------------------- @@ -1121,7 +1250,7 @@ sub mimetype_guess_file { } close(MIME); - $filename =~ /\.(.*?)$/; + $filename =~ /\.([^.]*)$/; return $mimemap{$1}; } @@ -1183,7 +1312,7 @@ sub git_header_html { if (defined $action) { $title .= "/$action"; if (defined $file_name) { - $title .= " - $file_name"; + $title .= " - " . esc_html($file_name); if ($action eq "tree" && $file_name !~ m|/$|) { $title .= "/"; } @@ -1221,6 +1350,16 @@ EOF printf(''."\n", esc_param($project), href(action=>"rss")); + } else { + printf(''."\n", + $site_name, href(project=>undef, action=>"project_index")); + printf(''."\n", + $site_name, href(project=>undef, action=>"opml")); + } + if (defined $favicon) { + print qq(\n); } print "\n" . @@ -1229,7 +1368,7 @@ EOF "" . "\"git\"" . "\n"; - print $cgi->a({-href => esc_param($home_link)}, $home_link_str) . " / "; + print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; if (defined $project) { print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); if (defined $action) { @@ -1268,9 +1407,13 @@ sub git_footer_html { if (defined $descr) { print "\n"; } - print $cgi->a({-href => href(action=>"rss"), -class => "rss_logo"}, "RSS") . "\n"; + print $cgi->a({-href => href(action=>"rss"), + -class => "rss_logo"}, "RSS") . "\n"; } else { - print $cgi->a({-href => href(action=>"opml"), -class => "rss_logo"}, "OPML") . "\n"; + print $cgi->a({-href => href(project=>undef, action=>"opml"), + -class => "rss_logo"}, "OPML") . " "; + print $cgi->a({-href => href(project=>undef, action=>"project_index"), + -class => "rss_logo"}, "TXT") . "\n"; } print "\n" . "\n" . @@ -1375,6 +1518,24 @@ sub git_print_header_div { "\n\n"; } +#sub git_print_authorship (\%) { +sub git_print_authorship { + my $co = shift; + + my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'}); + print "
" . + esc_html($co->{'author_name'}) . + " [$ad{'rfc2822'}"; + if ($ad{'hour_local'} < 6) { + printf(" (%02d:%02d %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + } else { + printf(" (%02d:%02d %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + } + print "]
\n"; +} + sub git_print_page_path { my $name = shift; my $type = shift; @@ -1382,25 +1543,46 @@ sub git_print_page_path { if (!defined $name) { print "
/
\n"; - } elsif (defined $type && $type eq 'blob') { + } else { + my @dirname = split '/', $name; + my $basename = pop @dirname; + my $fullname = ''; + print "
"; - if (defined $hb) { + print $cgi->a({-href => href(action=>"tree", hash_base=>$hb), + -title => 'tree root'}, "[$project]"); + print " / "; + foreach my $dir (@dirname) { + $fullname .= ($fullname ? '/' : '') . $dir; + print $cgi->a({-href => href(action=>"tree", file_name=>$fullname, + hash_base=>$hb), + -title => $fullname}, esc_html($dir)); + print " / "; + } + if (defined $type && $type eq 'blob') { print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name, - hash_base=>$hb)}, - esc_html($name)); + hash_base=>$hb), + -title => $name}, esc_html($basename)); + } elsif (defined $type && $type eq 'tree') { + print $cgi->a({-href => href(action=>"tree", file_name=>$file_name, + hash_base=>$hb), + -title => $name}, esc_html($basename)); } else { - print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)}, - esc_html($name)); + print esc_html($basename); } print "
\n"; - } else { - print "
" . esc_html($name) . "
\n"; } } -sub git_print_log { +# sub git_print_log (\@;%) { +sub git_print_log ($;%) { my $log = shift; + my %opts = @_; + if ($opts{'-remove_title'}) { + # remove title, i.e. first line of log + shift @$log; + } # remove leading empty lines while (defined $log->[0] && $log->[0] eq "") { shift @$log; @@ -1410,6 +1592,20 @@ sub git_print_log { my $signoff = 0; my $empty = 0; foreach my $line (@$log) { + if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { + $signoff = 1; + $empty = 0; + if (! $opts{'-remove_signoff'}) { + print "" . esc_html($line) . "
\n"; + next; + } else { + # remove signoff lines + next; + } + } else { + $signoff = 0; + } + # print only one empty line # do not print empty line after signoff if ($line eq "") { @@ -1418,13 +1614,13 @@ sub git_print_log { } else { $empty = 0; } - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - $signoff = 1; - print "" . esc_html($line) . "
\n"; - } else { - $signoff = 0; - print format_log_line_html($line) . "
\n"; - } + + print format_log_line_html($line) . "
\n"; + } + + if ($opts{'-final_empty_line'}) { + # end with single empty line + print "
\n" unless $empty; } } @@ -1432,30 +1628,62 @@ sub git_print_simplified_log { my $log = shift; my $remove_title = shift; - shift @$log if $remove_title; - # remove leading empty lines - while (defined $log->[0] && $log->[0] eq "") { - shift @$log; - } + git_print_log($log, + -final_empty_line=> 1, + -remove_title => $remove_title); +} - # simplify and print log - my $empty = 0; - foreach my $line (@$log) { - # remove signoff lines - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - next; +# print tree entry (row of git_tree), but without encompassing element +sub git_print_tree_entry { + my ($t, $basedir, $hash_base, $have_blame) = @_; + + my %base_key = (); + $base_key{hash_base} = $hash_base if defined $hash_base; + + # The format of a table row is: mode list link. Where mode is + # the mode of the entry, list is the name of the entry, an href, + # and link is the action links of the entry. + + print "" . mode_str($t->{'mode'}) . "\n"; + if ($t->{'type'} eq "blob") { + print "" . + $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key), + -class => "list"}, esc_html($t->{'name'})) . "\n"; + print ""; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blame"); } - # print only one empty line - if ($line eq "") { - next if $empty; - $empty = 1; - } else { - $empty = 0; + if (defined $hash_base) { + if ($have_blame) { + print " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")}, + "history"); } - print format_log_line_html($line) . "
\n"; + print " | " . + $cgi->a({-href => href(action=>"blob_plain", + hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")}, + "raw"); + print "\n"; + + } elsif ($t->{'type'} eq "tree") { + print ""; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + esc_html($t->{'name'})); + print "\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"; } - # end with single empty line - print "
\n" unless $empty; } ## ...................................................................... @@ -1471,7 +1699,8 @@ sub git_difftree_body { print "\n"; print "\n"; - my $alternate = 0; + my $alternate = 1; + my $patchno = 0; foreach my $line (@{$difftree}) { my %diff = parse_difftree_raw_line($line); @@ -1503,35 +1732,42 @@ sub git_difftree_body { my $mode_chng = "[new $to_file_type"; $mode_chng .= " with mode: $to_mode_str" if $to_mode_str; $mode_chng .= "]"; - print "\n" . - "\n" . - "\n"; + -class => "list"}, esc_html($diff{'file'})); + print "\n"; + print "\n"; + print "\n"; } elsif ($diff{'status'} eq "D") { # deleted my $mode_chng = "[deleted $from_file_type]"; - print "\n" . - "\n" . - "\n"; + -class => "list"}, esc_html($diff{'file'})); + print "\n"; + print "\n"; + print "\n"; } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed my $mode_chnge = ""; @@ -1550,35 +1786,32 @@ sub git_difftree_body { $mode_chnge .= "]\n"; } print "\n"; + print "\n"; + print "\n" . - "\n" . - "\n"; } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied @@ -1598,18 +1831,27 @@ sub git_difftree_body { hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}), -class => "list"}, esc_html($diff{'from_file'})) . " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]\n" . - "\n"; } # we should not encounter Unmerged (U) or Unknown (X) status @@ -1624,12 +1866,12 @@ sub git_patchset_body { my $patch_idx = 0; my $in_header = 0; my $patch_found = 0; - my %diffinfo; + my $diffinfo; print "
\n"; LINE: - while (my $patch_line @$fd>) { + while (my $patch_line = <$fd>) { chomp $patch_line; if ($patch_line =~ m/^diff /) { # "git diff" header @@ -1641,56 +1883,62 @@ sub git_patchset_body { # first patch in patchset $patch_found = 1; } - print "
\n"; + print "
\n"; - %diffinfo = parse_difftree_raw_line($difftree->[$patch_idx++]); + if (ref($difftree->[$patch_idx]) eq "HASH") { + $diffinfo = $difftree->[$patch_idx]; + } else { + $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + } + $patch_idx++; # for now, no extended header, hence we skip empty patches # companion to next LINE if $in_header; - if ($diffinfo{'from_id'} eq $diffinfo{'to_id'}) { # no change + if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change $in_header = 1; next LINE; } - if ($diffinfo{'status'} eq "A") { # added - print "
" . file_type($diffinfo{'to_mode'}) . ":" . + if ($diffinfo->{'status'} eq "A") { # added + print "
" . file_type($diffinfo->{'to_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash, - hash=>$diffinfo{'to_id'}, file_name=>$diffinfo{'file'})}, - $diffinfo{'to_id'}) . "(new)" . + hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})}, + $diffinfo->{'to_id'}) . "(new)" . "
\n"; # class="diff_info" - } elsif ($diffinfo{'status'} eq "D") { # deleted - print "
" . file_type($diffinfo{'from_mode'}) . ":" . + } elsif ($diffinfo->{'status'} eq "D") { # deleted + print "
" . file_type($diffinfo->{'from_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent, - hash=>$diffinfo{'from_id'}, file_name=>$diffinfo{'file'})}, - $diffinfo{'from_id'}) . "(deleted)" . + hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})}, + $diffinfo->{'from_id'}) . "(deleted)" . "
\n"; # class="diff_info" - } elsif ($diffinfo{'status'} eq "R" || # renamed - $diffinfo{'status'} eq "C") { # copied + } elsif ($diffinfo->{'status'} eq "R" || # renamed + $diffinfo->{'status'} eq "C" || # copied + $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff) print "
" . - file_type($diffinfo{'from_mode'}) . ":" . + file_type($diffinfo->{'from_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent, - hash=>$diffinfo{'from_id'}, file_name=>$diffinfo{'from_file'})}, - $diffinfo{'from_id'}) . + hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})}, + $diffinfo->{'from_id'}) . " -> " . - file_type($diffinfo{'to_mode'}) . ":" . + file_type($diffinfo->{'to_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash, - hash=>$diffinfo{'to_id'}, file_name=>$diffinfo{'to_file'})}, - $diffinfo{'to_id'}); + hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})}, + $diffinfo->{'to_id'}); print "
\n"; # class="diff_info" } else { # modified, mode changed, ... print "
" . - file_type($diffinfo{'from_mode'}) . ":" . + file_type($diffinfo->{'from_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent, - hash=>$diffinfo{'from_id'}, file_name=>$diffinfo{'file'})}, - $diffinfo{'from_id'}) . + hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})}, + $diffinfo->{'from_id'}) . " -> " . - file_type($diffinfo{'to_mode'}) . ":" . + file_type($diffinfo->{'to_mode'}) . ":" . $cgi->a({-href => href(action=>"blob", hash_base=>$hash, - hash=>$diffinfo{'to_id'}, file_name=>$diffinfo{'file'})}, - $diffinfo{'to_id'}); + hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})}, + $diffinfo->{'to_id'}); print "
\n"; # class="diff_info" } @@ -1701,8 +1949,30 @@ sub git_patchset_body { if ($in_header && $patch_line =~ m/^---/) { - #print "
\n" + #print "
\n"; # class="diff extended_header" $in_header = 0; + + my $file = $diffinfo->{'from_file'}; + $file ||= $diffinfo->{'file'}; + $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent, + hash=>$diffinfo->{'from_id'}, file_name=>$file), + -class => "list"}, esc_html($file)); + $patch_line =~ s|a/.*$|a/$file|g; + print "
$patch_line
\n"; + + $patch_line = <$fd>; + chomp $patch_line; + + #$patch_line =~ m/^+++/; + $file = $diffinfo->{'to_file'}; + $file ||= $diffinfo->{'file'}; + $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, file_name=>$file), + -class => "list"}, esc_html($file)); + $patch_line =~ s|b/.*|b/$file|g; + print "
$patch_line
\n"; + + next LINE; } next LINE if $in_header; @@ -1719,14 +1989,11 @@ sub git_shortlog_body { # uses global variable $project my ($revlist, $from, $to, $refs, $extra) = @_; - my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); - my $have_snapshot = (defined $ctype && defined $suffix); - $from = 0 unless defined $from; $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to); print "
" . - $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + print ""; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, hash_base=>$hash, file_name=>$diff{'file'}), - -class => "list"}, esc_html($diff{'file'})) . - "$mode_chng" . - $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'})}, - "blob") . - "$mode_chng"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + } + print "" . - $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, + print ""; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, hash_base=>$parent, file_name=>$diff{'file'}), - -class => "list"}, esc_html($diff{'file'})) . - "$mode_chng" . - $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, - hash_base=>$parent, file_name=>$diff{'file'})}, - "blob") . - " | " . - $cgi->a({-href => href(action=>"history", hash_base=>$parent, - file_name=>$diff{'file'})},\ - "history") . - "$mode_chng"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + print " | "; + } + print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, + file_name=>$diff{'file'})}, + "blame") . " | "; + print $cgi->a({-href => href(action=>"history", hash_base=>$parent, + 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_html($diff{'file'})); + print "$mode_chnge"; if ($diff{'to_id'} ne $diff{'from_id'}) { # modified - print $cgi->a({-href => href(action=>"blobdiff", - hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, - hash_base=>$hash, hash_parent_base=>$parent, - file_name=>$diff{'file'}), - -class => "list"}, esc_html($diff{'file'})); - } else { # only mode changed - print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'}), - -class => "list"}, esc_html($diff{'file'})); - } - print "$mode_chnge" . - $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, - hash_base=>$hash, file_name=>$diff{'file'})}, - "blob"); - if ($diff{'to_id'} ne $diff{'from_id'}) { # modified - print " | " . - $cgi->a({-href => href(action=>"blobdiff", - hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, - hash_base=>$hash, hash_parent_base=>$parent, - file_name=>$diff{'file'})}, - "diff"); + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + } else { + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff{'file'})}, + "diff"); + } + print " | "; } - print " | " . - $cgi->a({-href => href(action=>"history", - hash_base=>$hash, file_name=>$diff{'file'})}, - "history"); + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff{'file'})}, + "blame") . " | "; + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff{'file'})}, + "history"); print "" . - $cgi->a({-href => href(action=>"blob", hash_base=>$hash, - hash=>$diff{'to_id'}, file_name=>$diff{'to_file'})}, - "blob"); + ""; if ($diff{'to_id'} ne $diff{'from_id'}) { - print " | " . - $cgi->a({-href => href(action=>"blobdiff", - 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'})}, - "diff"); + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + } else { + print $cgi->a({-href => href(action=>"blobdiff", + 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'})}, + "diff"); + } + print " | "; } + print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, + file_name=>$diff{'from_file'})}, + "blame") . " | "; + print $cgi->a({-href => href(action=>"history", hash_base=>$parent, + file_name=>$diff{'from_file'})}, + "history"); print "
\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my $commit = $revlist->[$i]; #my $ref = defined $refs ? format_ref_marker($refs, $commit) : ''; @@ -1747,10 +2014,8 @@ sub git_shortlog_body { print "\n" . "\n" . "\n"; } @@ -1764,12 +2029,15 @@ sub git_shortlog_body { sub git_history_body { # Warning: assumes constant type (blob or tree) during history - my ($fd, $refs, $hash_base, $ftype, $extra) = @_; + my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; + + $from = 0 unless defined $from; + $to = $#{$revlist} unless (defined $to && $to <= $#{$revlist}); print "
" . $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . - $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff"); - if ($have_snapshot) { - print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot"); - } + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); print "
\n"; - my $alternate = 0; - while (my $line = <$fd>) { - if ($line !~ m/^([0-9a-fA-F]{40})/) { + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) { next; } @@ -1831,7 +2099,7 @@ sub git_tags_body { $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); print "
\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my $entry = $taglist->[$i]; my %tag = %$entry; @@ -1886,14 +2154,14 @@ sub git_tags_body { sub git_heads_body { # uses global variable $project - my ($taglist, $head, $from, $to, $extra) = @_; + my ($headlist, $head, $from, $to, $extra) = @_; $from = 0 unless defined $from; - $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); + $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to); print "
\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { - my $entry = $taglist->[$i]; + my $entry = $headlist->[$i]; my %tag = %$entry; my $curr = $tag{'id'} eq $head; if ($alternate) { @@ -1909,7 +2177,8 @@ sub git_heads_body { "\n" . "\n" . ""; } @@ -1921,77 +2190,6 @@ sub git_heads_body { print "
" . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " . - $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . + $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$tag{'name'}, hash_base=>$tag{'name'})}, "tree") . "
\n"; } -## ---------------------------------------------------------------------- -## functions printing large fragments, format as one of arguments - -sub git_diff_print { - my $from = shift; - my $from_name = shift; - my $to = shift; - my $to_name = shift; - my $format = shift || "html"; - - my $from_tmp = "/dev/null"; - my $to_tmp = "/dev/null"; - my $pid = $$; - - # create tmp from-file - if (defined $from) { - $from_tmp = "$git_temp/gitweb_" . $$ . "_from"; - open my $fd2, "> $from_tmp"; - open my $fd, "-|", $GIT, "cat-file", "blob", $from; - my @file = <$fd>; - print $fd2 @file; - close $fd2; - close $fd; - } - - # create tmp to-file - if (defined $to) { - $to_tmp = "$git_temp/gitweb_" . $$ . "_to"; - open my $fd2, "> $to_tmp"; - open my $fd, "-|", $GIT, "cat-file", "blob", $to; - my @file = <$fd>; - print $fd2 @file; - close $fd2; - close $fd; - } - - open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp"; - if ($format eq "plain") { - undef $/; - print <$fd>; - $/ = "\n"; - } else { - while (my $line = <$fd>) { - chomp $line; - my $char = substr($line, 0, 1); - my $diff_class = ""; - if ($char eq '+') { - $diff_class = " add"; - } elsif ($char eq "-") { - $diff_class = " rem"; - } elsif ($char eq "@") { - $diff_class = " chunk_header"; - } elsif ($char eq "\\") { - # skip errors - next; - } - $line = untabify($line); - print "
" . esc_html($line) . "
\n"; - } - } - close $fd; - - if (defined $from) { - unlink($from_tmp); - } - if (defined $to) { - unlink($to_tmp); - } -} - - ## ====================================================================== ## ====================================================================== ## actions @@ -2012,7 +2210,7 @@ sub git_project_list { if (!defined $head) { next; } - $ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}"; + $git_dir = "$projectroot/$pr->{'path'}"; my %co = parse_commit($head); if (!%co) { next; @@ -2044,7 +2242,7 @@ sub git_project_list { print "Project\n"; } else { print "" . - $cgi->a({-href => "$my_uri?" . esc_param("o=project"), + $cgi->a({-href => href(project=>undef, order=>'project'), -class => "header"}, "Project") . "\n"; } @@ -2053,7 +2251,7 @@ sub git_project_list { print "Description\n"; } else { print "" . - $cgi->a({-href => "$my_uri?" . esc_param("o=descr"), + $cgi->a({-href => href(project=>undef, order=>'descr'), -class => "header"}, "Description") . "\n"; } @@ -2062,7 +2260,7 @@ sub git_project_list { print "Owner\n"; } else { print "" . - $cgi->a({-href => "$my_uri?" . esc_param("o=owner"), + $cgi->a({-href => href(project=>undef, order=>'owner'), -class => "header"}, "Owner") . "\n"; } @@ -2071,13 +2269,13 @@ sub git_project_list { print "Last Change\n"; } else { print "" . - $cgi->a({-href => "$my_uri?" . esc_param("o=age"), + $cgi->a({-href => href(project=>undef, order=>'age'), -class => "header"}, "Last Change") . "\n"; } print "\n" . "\n"; - my $alternate = 0; + my $alternate = 1; foreach my $pr (@projects) { if ($alternate) { print "\n"; @@ -2094,7 +2292,8 @@ sub git_project_list { "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . - $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") . "\n" . "\n"; } @@ -2102,6 +2301,30 @@ sub git_project_list { git_footer_html(); } +sub git_project_index { + my @projects = git_get_projects_list(); + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -content_disposition => 'inline; filename="index.aux"'); + + foreach my $pr (@projects) { + if (!exists $pr->{'owner'}) { + $pr->{'owner'} = get_file_owner("$projectroot/$project"); + } + + my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); + # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' ' + $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $path =~ s/ /\+/g; + $owner =~ s/ /\+/g; + + print "$path $owner\n"; + } +} + sub git_summary { my $descr = git_get_project_description($project) || "none"; my $head = git_get_head_hash($project); @@ -2110,7 +2333,19 @@ sub git_summary { my $owner = git_get_project_owner($project); - my $refs = git_get_references(); + my ($reflist, $refs) = git_get_refs_list(); + + my @taglist; + my @headlist; + foreach my $ref (@$reflist) { + if ($ref->{'name'} =~ s!^heads/!!) { + push @headlist, $ref; + } else { + $ref->{'name'} =~ s!^tags/!!; + push @taglist, $ref; + } + } + git_header_html(); git_print_page_nav('summary','', $head); @@ -2131,7 +2366,8 @@ sub git_summary { } print "\n"; - open my $fd, "-|", $GIT, "rev-list", "--max-count=17", git_get_head_hash($project) + open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17", + git_get_head_hash($project) or die_error(undef, "Open git-rev-list failed"); my @revlist = map { chomp; $_ } <$fd>; close $fd; @@ -2139,17 +2375,15 @@ sub git_summary { git_shortlog_body(\@revlist, 0, 15, $refs, $cgi->a({-href => href(action=>"shortlog")}, "...")); - my $taglist = git_get_refs_list("refs/tags"); - if (defined @$taglist) { + if (@taglist) { git_print_header_div('tags'); - git_tags_body($taglist, 0, 15, + git_tags_body(\@taglist, 0, 15, $cgi->a({-href => href(action=>"tags")}, "...")); } - my $headlist = git_get_refs_list("refs/heads"); - if (defined @$headlist) { + if (@headlist) { git_print_header_div('heads'); - git_heads_body($headlist, $head, 0, 15, + git_heads_body(\@headlist, $head, 0, 15, $cgi->a({-href => href(action=>"heads")}, "...")); } @@ -2193,7 +2427,8 @@ sub git_blame2 { my $fd; my $ftype; - if (!gitweb_check_feature('blame')) { + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { die_error('403 Permission denied', "Permission denied"); } die_error('404 Not Found', "File name not defined") if (!$file_name); @@ -2209,15 +2444,18 @@ sub git_blame2 { if ($ftype !~ "blob") { die_error("400 Bad Request", "Object is not a blob"); } - open ($fd, "-|", $GIT, "blame", '-l', $file_name, $hash_base) + open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base) 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)}, "blob") . " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, - "head"); + "HEAD"); git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); git_print_page_path($file_name, $ftype, $hash_base); @@ -2262,7 +2500,8 @@ HTML sub git_blame { my $fd; - if (!gitweb_check_feature('blame')) { + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { die_error('403 Permission denied', "Permission denied"); } die_error('404 Not Found', "File name not defined") if (!$file_name); @@ -2274,15 +2513,18 @@ sub git_blame { $hash = git_get_hash_by_path($hash_base, $file_name, "blob") or die_error(undef, "Error lookup file"); } - open ($fd, "-|", $GIT, "annotate", '-l', '-t', '-r', $file_name, $hash_base) + open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base) or die_error(undef, "Open git-annotate failed"); git_header_html(); my $formats_nav = $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blob") . " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, - "head"); + "HEAD"); git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); git_print_page_path($file_name, 'blob', $hash_base); @@ -2314,7 +2556,7 @@ HTML chomp $line; $line_class_num = ($line_class_num + 1) % $line_class_len; - if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) { + if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) { $long_rev = $1; $author = $2; $time = $3; @@ -2358,8 +2600,8 @@ sub git_tags { git_print_page_nav('','', $head,undef,$head); git_print_header_div('summary', $project); - my $taglist = git_get_refs_list("refs/tags"); - if (defined @$taglist) { + my ($taglist) = git_get_refs_list("tags"); + if (@$taglist) { git_tags_body($taglist); } git_footer_html(); @@ -2371,14 +2613,16 @@ sub git_heads { git_print_page_nav('','', $head,undef,$head); git_print_header_div('summary', $project); - my $taglist = git_get_refs_list("refs/heads"); - if (defined @$taglist) { - git_heads_body($taglist, $head); + my ($headlist) = git_get_refs_list("heads"); + if (@$headlist) { + git_heads_body($headlist, $head); } git_footer_html(); } sub git_blob_plain { + my $expires; + if (!defined $hash) { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); @@ -2387,9 +2631,13 @@ sub git_blob_plain { } else { die_error(undef, "No file name defined"); } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; } + my $type = shift; - open my $fd, "-|", $GIT, "cat-file", "blob", $hash + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash or die_error(undef, "Couldn't cat $file_name, $hash"); $type ||= blob_mimetype($fd, $file_name); @@ -2402,8 +2650,10 @@ sub git_blob_plain { $save_as .= '.txt'; } - print $cgi->header(-type => "$type", - -content_disposition => "inline; filename=\"$save_as\""); + print $cgi->header( + -type => "$type", + -expires=>$expires, + -content_disposition => 'inline; filename="' . quotemeta($save_as) . '"'); undef $/; binmode STDOUT, ':raw'; print <$fd>; @@ -2413,6 +2663,8 @@ sub git_blob_plain { } sub git_blob { + my $expires; + if (!defined $hash) { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); @@ -2421,16 +2673,20 @@ sub git_blob { } else { die_error(undef, "No file name defined"); } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; } - my $have_blame = gitweb_check_feature('blame'); - open my $fd, "-|", $GIT, "cat-file", "blob", $hash + + my ($have_blame) = gitweb_check_feature('blame'); + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash or die_error(undef, "Couldn't cat $file_name, $hash"); my $mimetype = blob_mimetype($fd, $file_name); if ($mimetype !~ m/^text\//) { close $fd; return git_blob_plain($mimetype); } - git_header_html(); + git_header_html(undef, $expires); my $formats_nav = ''; if (defined $hash_base && (my %co = parse_commit($hash_base))) { if (defined $file_name) { @@ -2442,16 +2698,20 @@ sub git_blob { " | "; } $formats_nav .= + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "history") . + " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$hash, file_name=>$file_name)}, - "plain") . + "raw") . " | " . $cgi->a({-href => href(action=>"blob", hash_base=>"HEAD", file_name=>$file_name)}, - "head"); + "HEAD"); } else { $formats_nav .= - $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain"); + $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw"); } git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); @@ -2477,6 +2737,9 @@ sub git_blob { } sub git_tree { + my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); + my $have_snapshot = (defined $ctype && defined $suffix); + if (!defined $hash) { $hash = git_get_head_hash($project); if (defined $file_name) { @@ -2488,7 +2751,7 @@ sub git_tree { } } $/ = "\0"; - open my $fd, "-|", $GIT, "ls-tree", '-z', $hash + open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash or die_error(undef, "Open git-ls-tree failed"); my @entries = map { chomp; $_ } <$fd>; close $fd or die_error(undef, "Reading tree failed"); @@ -2497,14 +2760,29 @@ sub git_tree { my $refs = git_get_references(); my $ref = format_ref_marker($refs, $hash_base); git_header_html(); - my %base_key = (); my $base = ""; - my $have_blame = gitweb_check_feature('blame'); + my ($have_blame) = gitweb_check_feature('blame'); if (defined $hash_base && (my %co = parse_commit($hash_base))) { - $base_key{hash_base} = $hash_base; - git_print_page_nav('tree','', $hash_base); + 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)}, + "history"), + $cgi->a({-href => href(action=>"tree", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"), + } + if ($have_snapshot) { + # FIXME: Should be available when we have no hash base as well. + push @views_nav, + $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, + "snapshot"); + } + git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav)); git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); } else { + undef $hash_base; print "
\n"; print "

\n"; print "
$hash
\n"; @@ -2515,56 +2793,19 @@ sub git_tree { git_print_page_path($file_name, 'tree', $hash_base); print "
\n"; print "\n"; - my $alternate = 0; + my $alternate = 1; foreach my $line (@entries) { - #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' - $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/; - my $t_mode = $1; - my $t_type = $2; - my $t_hash = $3; - my $t_name = validate_input($4); + my %t = parse_ls_tree_line($line, -z => 1); + if ($alternate) { print "\n"; } else { print "\n"; } $alternate ^= 1; - print "\n"; - if ($t_type eq "blob") { - print "\n" . - "\n"; - } elsif ($t_type eq "tree") { - print "\n" . - "\n"; - } + + git_print_tree_entry(\%t, $base, $hash_base, $have_blame); + print "\n"; } print "
" . mode_str($t_mode) . "" . - $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key), - -class => "list"}, esc_html($t_name)) . - "" . - $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key)}, - "blob"); - if ($have_blame) { - print " | " . - $cgi->a({-href => href(action=>"blame", hash=>$t_hash, file_name=>"$base$t_name", %base_key)}, - "blame"); - } - print " | " . - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, - hash=>$t_hash, file_name=>"$base$t_name")}, - "history") . - " | " . - $cgi->a({-href => href(action=>"blob_plain", - hash=>$t_hash, file_name=>"$base$t_name")}, - "raw") . - "" . - $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)}, - esc_html($t_name)) . - "" . - $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)}, - "tree") . - " | " . - $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, file_name=>"$base$t_name")}, - "history") . - "
\n" . @@ -2586,12 +2827,14 @@ sub git_snapshot { my $filename = basename($project) . "-$hash.tar.$suffix"; - print $cgi->header(-type => 'application/x-tar', - -content_encoding => $ctype, - -content_disposition => "inline; filename=\"$filename\"", - -status => '200 OK'); + print $cgi->header( + -type => 'application/x-tar', + -content_encoding => $ctype, + -content_disposition => 'inline; filename="' . quotemeta($filename) . '"', + -status => '200 OK'); - open my $fd, "-|", "$GIT tar-tree $hash \'$project\' | $command" or + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command tar-tree $hash \'$project\' | $command" or die_error(undef, "Execute git-tar-tree failed."); binmode STDOUT, ':raw'; print <$fd>; @@ -2611,7 +2854,7 @@ sub git_log { my $refs = git_get_references(); my $limit = sprintf("--max-count=%i", (100 * ($page+1))); - open my $fd, "-|", $GIT, "rev-list", $limit, $hash + open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash or die_error(undef, "Open git-rev-list failed"); my @revlist = map { chomp; $_ } <$fd>; close $fd; @@ -2642,6 +2885,8 @@ sub git_log { $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") . "
\n" . "
\n" . "" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]
\n" . @@ -2666,7 +2911,7 @@ sub git_commit { if (!defined $parent) { $parent = "--root"; } - open my $fd, "-|", $GIT, "diff-tree", '-r', '-M', $parent, $hash + open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash or die_error(undef, "Open git-diff-tree failed"); my @difftree = map { chomp; $_ } <$fd>; close $fd or die_error(undef, "Reading git-diff-tree failed"); @@ -2682,17 +2927,22 @@ sub git_commit { my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); my $have_snapshot = (defined $ctype && defined $suffix); - my $formats_nav = ''; + my @views_nav = (); if (defined $file_name && defined $co{'parent'}) { my $parent = $co{'parent'}; - $formats_nav .= + push @views_nav, $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)}, "blame"); } + if (defined $co{'parent'}) { + push @views_nav, + $cgi->a({-href => href(action=>"shortlog", hash=>$hash)}, "shortlog"), + $cgi->a({-href => href(action=>"log", hash=>$hash)}, "log"); + } git_header_html(undef, $expires); git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff', $hash, $co{'tree'}, $hash, - $formats_nav); + join (' | ', @views_nav)); if (defined $co{'parent'}) { git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash); @@ -2744,7 +2994,7 @@ sub git_commit { "" . $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") . " | " . - $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") . + $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") . "" . "\n"; } @@ -2761,43 +3011,171 @@ sub git_commit { } sub git_blobdiff { - mkdir($git_temp, 0700); - git_header_html(); - if (defined $hash_base && (my %co = parse_commit($hash_base))) { + my $format = shift || 'html'; + + my $fd; + my @difftree; + my %diffinfo; + my $expires; + + # preparing $fd and %diffinfo for git_patchset_body + # new style URI + if (defined $hash_base && defined $hash_parent_base) { + if (defined $file_name) { + # read raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } elsif (defined $hash && + $hash =~ /[0-9a-fA-F]{40}/) { + # try to find filename from $hash + + # read filtered raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base + or die_error(undef, "Open git-diff-tree failed"); + @difftree = + # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c' + # $hash == to_id + grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ } + map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters"); + } + + if (@difftree > 1) { + die_error('404 Not Found', "Ambiguous blob diff specification"); + } + + %diffinfo = parse_difftree_raw_line($difftree[0]); + $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'}; + $file_name ||= $diffinfo{'to_file'} || $diffinfo{'file'}; + + $hash_parent ||= $diffinfo{'from_id'}; + $hash ||= $diffinfo{'to_id'}; + + # non-textual hash id's can be cached + if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ && + $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "Open git-diff-tree failed"); + } + + # old/legacy style URI + if (!%diffinfo && # if new style URI failed + defined $hash && defined $hash_parent) { + # fake git-diff-tree raw output + $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob"; + $diffinfo{'from_id'} = $hash_parent; + $diffinfo{'to_id'} = $hash; + if (defined $file_name) { + if (defined $file_parent) { + $diffinfo{'status'} = '2'; + $diffinfo{'from_file'} = $file_parent; + $diffinfo{'to_file'} = $file_name; + } else { # assume not renamed + $diffinfo{'status'} = '1'; + $diffinfo{'from_file'} = $file_name; + $diffinfo{'to_file'} = $file_name; + } + } else { # no filename given + $diffinfo{'status'} = '2'; + $diffinfo{'from_file'} = $hash_parent; + $diffinfo{'to_file'} = $hash; + } + + # non-textual hash id's can be cached + if ($hash =~ m/^[0-9a-fA-F]{40}$/ && + $hash_parent =~ m/^[0-9a-fA-F]{40}$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash + or die_error(undef, "Open git-diff failed"); + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters") + unless %diffinfo; + } + + # 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)}, - "plain"); - git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + "raw"); + git_header_html(undef, $expires); + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + } else { + print "

$formats_nav
\n"; + print "
$hash vs $hash_parent
\n"; + } + if (defined $file_name) { + git_print_page_path($file_name, "blob", $hash_base); + } else { + print "
\n"; + } + + } elsif ($format eq 'plain') { + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . quotemeta($file_name) . '.patch"'); + + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + } else { - print <

-
$hash vs $hash_parent
-HTML + die_error(undef, "Unknown blobdiff format"); + } + + # patch + if ($format eq 'html') { + print "
\n"; + + git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base); + close $fd; + + print "
\n"; # class="page_body" + git_footer_html(); + + } else { + while (my $line = <$fd>) { + $line =~ s!a/($hash|$hash_parent)!'a/'.esc_html($diffinfo{'from_file'})!eg; + $line =~ s!b/($hash|$hash_parent)!'b/'.esc_html($diffinfo{'to_file'})!eg; + + print $line; + + last if $line =~ m!^\+\+\+!; + } + local $/ = undef; + print <$fd>; + close $fd; } - git_print_page_path($file_name, "blob", $hash_base); - print "
\n" . - "
blob:" . - $cgi->a({-href => href(action=>"blob", hash=>$hash_parent, - hash_base=>$hash_parent_base, file_name=>($file_parent || $file_name))}, - $hash_parent) . - " -> blob:" . - $cgi->a({-href => href(action=>"blob", hash=>$hash, - hash_base=>$hash_base, file_name=>$file_name)}, - $hash) . - "
\n"; - git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash); - print "
"; # page_body - git_footer_html(); } sub git_blobdiff_plain { - mkdir($git_temp, 0700); - print $cgi->header(-type => "text/plain", -charset => 'utf-8'); - git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain"); + git_blobdiff('plain'); } sub git_commitdiff { @@ -2814,7 +3192,7 @@ sub git_commitdiff { my $fd; my @difftree; if ($format eq 'html') { - open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C', + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, "--patch-with-raw", "--full-index", $hash_parent, $hash or die_error(undef, "Open git-diff-tree failed"); @@ -2825,7 +3203,8 @@ sub git_commitdiff { } } elsif ($format eq 'plain') { - open $fd, "-|", $GIT, "diff-tree", '-r', '-p', '-B', $hash_parent, $hash + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent, $hash or die_error(undef, "Open git-diff-tree failed"); } else { @@ -2845,11 +3224,12 @@ sub git_commitdiff { my $formats_nav = $cgi->a({-href => href(action=>"commitdiff_plain", hash=>$hash, hash_parent=>$hash_parent)}, - "plain"); + "raw"); git_header_html(undef, $expires); git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash); + git_print_authorship(\%co); print "
\n"; print "
\n"; git_print_simplified_log($co{'comment'}, 1); # skip title @@ -2864,7 +3244,7 @@ sub git_commitdiff { -type => 'text/plain', -charset => 'utf-8', -expires => $expires, - -content_disposition => qq(inline; filename="$filename")); + -content_disposition => 'inline; filename="' . quotemeta($filename) . '"'); my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); print <\n"; + git_difftree_body(\@difftree, $hash, $hash_parent); + print "
\n"; git_patchset_body($fd, \@difftree, $hash, $hash_parent); close $fd; @@ -2906,28 +3286,70 @@ sub git_history { if (!defined $hash_base) { $hash_base = git_get_head_hash($project); } + if (!defined $page) { + $page = 0; + } my $ftype; my %co = parse_commit($hash_base); if (!%co) { die_error(undef, "Unknown commit object"); } + my $refs = git_get_references(); - git_header_html(); - git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + my $limit = sprintf("--max-count=%i", (100 * ($page+1))); + if (!defined $hash && defined $file_name) { $hash = git_get_hash_by_path($hash_base, $file_name); } if (defined $hash) { $ftype = git_get_type($hash); } - git_print_page_path($file_name, $ftype, $hash_base); open my $fd, "-|", - $GIT, "rev-list", "--full-history", $hash_base, "--", $file_name; - git_history_body($fd, $refs, $hash_base, $ftype); + 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 $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + 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), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#revlist >= (100 * ($page+1)-1)) { + $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 ($#revlist >= (100 * ($page+1)-1)) { + $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"); + } + + git_header_html(); + git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav); + 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, + $refs, $hash_base, $ftype, $next_link); - close $fd; git_footer_html(); } @@ -2942,8 +3364,7 @@ sub git_search { if (!%co) { die_error(undef, "Unknown commit object"); } - # pickaxe may take all resources of your box and run for several minutes - # with every query - so decide by yourself how public you make this feature :) + my $commit_search = 1; my $author_search = 0; my $committer_search = 0; @@ -2955,16 +3376,23 @@ sub git_search { } elsif ($searchtext =~ s/^pickaxe\\://i) { $commit_search = 0; $pickaxe_search = 1; + + # pickaxe may take all resources of your box and run for several minutes + # with every query - so decide by yourself how public you make this feature + my ($have_pickaxe) = gitweb_check_feature('pickaxe'); + if (!$have_pickaxe) { + die_error('403 Permission denied', "Permission denied"); + } } git_header_html(); git_print_page_nav('','', $hash,$co{'tree'},$hash); git_print_header_div('commit', esc_html($co{'title'}), $hash); print "\n"; - my $alternate = 0; + my $alternate = 1; if ($commit_search) { $/ = "\0"; - open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", $hash or next; + open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next; while (my $commit_text = <$fd>) { if (!grep m/$searchtext/i, $commit_text) { next; @@ -3016,7 +3444,9 @@ sub git_search { if ($pickaxe_search) { $/ = "\n"; - open my $fd, "-|", "$GIT rev-list $hash | $GIT diff-tree -r --stdin -S\'$searchtext\'"; + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command rev-list $hash | " . + "$git_command diff-tree -r --stdin -S\'$searchtext\'"; undef %co; my @files; while (my $line = <$fd>) { @@ -3083,7 +3513,7 @@ sub git_shortlog { my $refs = git_get_references(); my $limit = sprintf("--max-count=%i", (100 * ($page+1))); - open my $fd, "-|", $GIT, "rev-list", $limit, $hash + open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash or die_error(undef, "Open git-rev-list failed"); my @revlist = map { chomp; $_ } <$fd>; close $fd; @@ -3111,7 +3541,7 @@ sub git_shortlog { sub git_rss { # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ - open my $fd, "-|", $GIT, "rev-list", "--max-count=150", git_get_head_hash($project) + open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project) 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"); @@ -3134,9 +3564,12 @@ XML last; } my %cd = parse_date($co{'committer_epoch'}); - open $fd, "-|", $GIT, "diff-tree", '-r', $co{'parent'}, $co{'id'} or next; + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $co{'parent'}, $co{'id'} + or next; my @difftree = map { chomp; $_ } <$fd>; - close $fd or next; + close $fd + or next; print "\n" . "" . sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) . @@ -3158,7 +3591,7 @@ XML if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) { next; } - my $file = validate_input(unquote($7)); + my $file = esc_html(unquote($7)); $file = decode("utf8", $file, Encode::FB_DEFAULT); print "$file<br/>\n"; } @@ -3189,7 +3622,7 @@ XML if (!defined $head) { next; } - $ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}"; + $git_dir = "$projectroot/$proj{'path'}"; my %co = parse_commit($head); if (!%co) { next;