+
+$status - $error
+
+EOF
+ if (defined $extra) {
+ print " \n" .
+ "$extra\n";
+ }
+ print "\n";
+
+ git_footer_html();
+ goto DONE_GITWEB
+ unless ($opts{'-error_handler'});
+}
+
+## ----------------------------------------------------------------------
+## functions printing or outputting HTML: navigation
+
+sub git_print_page_nav {
+ my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
+ $extra = '' if !defined $extra; # pager or formats
+
+ my @navs = qw(summary shortlog log commit commitdiff tree);
+ if ($suppress) {
+ @navs = grep { $_ ne $suppress } @navs;
+ }
+
+ my %arg = map { $_ => {action=>$_} } @navs;
+ if (defined $head) {
+ for (qw(commit commitdiff)) {
+ $arg{$_}{'hash'} = $head;
+ }
+ if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
+ for (qw(shortlog log)) {
+ $arg{$_}{'hash'} = $head;
+ }
+ }
+ }
+
+ $arg{'tree'}{'hash'} = $treehead if defined $treehead;
+ $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
+
+ my @actions = gitweb_get_feature('actions');
+ my %repl = (
+ '%' => '%',
+ 'n' => $project, # project name
+ 'f' => $git_dir, # project path within filesystem
+ 'h' => $treehead || '', # current hash ('h' parameter)
+ 'b' => $treebase || '', # hash base ('hb' parameter)
+ );
+ while (@actions) {
+ my ($label, $link, $pos) = splice(@actions,0,3);
+ # insert
+ @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
+ # munch munch
+ $link =~ s/%([%nfhb])/$repl{$1}/g;
+ $arg{$label}{'_href'} = $link;
+ }
+
+ print "\n" .
+ (join " | ",
+ map { $_ eq $current ?
+ $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
+ } @navs);
+ print " \n$extra \n" .
+ "
\n";
+}
+
+# returns a submenu for the navigation of the refs views (tags, heads,
+# remotes) with the current view disabled and the remotes view only
+# available if the feature is enabled
+sub format_ref_views {
+ my ($current) = @_;
+ my @ref_views = qw{tags heads};
+ push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
+ return join " | ", map {
+ $_ eq $current ? $_ :
+ $cgi->a({-href => href(action=>$_)}, $_)
+ } @ref_views
+}
+
+sub format_paging_nav {
+ my ($action, $page, $has_next_link) = @_;
+ my $paging_nav;
+
+
+ if ($page > 0) {
+ $paging_nav .=
+ $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
+ " ⋅ " .
+ $cgi->a({-href => href(-replay=>1, page=>$page-1),
+ -accesskey => "p", -title => "Alt-p"}, "prev");
+ } else {
+ $paging_nav .= "first ⋅ prev";
+ }
+
+ if ($has_next_link) {
+ $paging_nav .= " ⋅ " .
+ $cgi->a({-href => href(-replay=>1, page=>$page+1),
+ -accesskey => "n", -title => "Alt-n"}, "next");
+ } else {
+ $paging_nav .= " ⋅ next";
+ }
+
+ return $paging_nav;
+}
+
+## ......................................................................
+## functions printing or outputting HTML: div
+
+sub git_print_header_div {
+ my ($action, $title, $hash, $hash_base) = @_;
+ my %args = ();
+
+ $args{'action'} = $action;
+ $args{'hash'} = $hash if $hash;
+ $args{'hash_base'} = $hash_base if $hash_base;
+
+ print "\n";
+}
+
+sub format_repo_url {
+ my ($name, $url) = @_;
+ return "$name $url \n";
+}
+
+# Group output by placing it in a DIV element and adding a header.
+# Options for start_div() can be provided by passing a hash reference as the
+# first parameter to the function.
+# Options to git_print_header_div() can be provided by passing an array
+# reference. This must follow the options to start_div if they are present.
+# The content can be a scalar, which is output as-is, a scalar reference, which
+# is output after html escaping, an IO handle passed either as *handle or
+# *handle{IO}, or a function reference. In the latter case all following
+# parameters will be taken as argument to the content function call.
+sub git_print_section {
+ my ($div_args, $header_args, $content);
+ my $arg = shift;
+ if (ref($arg) eq 'HASH') {
+ $div_args = $arg;
+ $arg = shift;
+ }
+ if (ref($arg) eq 'ARRAY') {
+ $header_args = $arg;
+ $arg = shift;
+ }
+ $content = $arg;
+
+ print $cgi->start_div($div_args);
+ git_print_header_div(@$header_args);
+
+ if (ref($content) eq 'CODE') {
+ $content->(@_);
+ } elsif (ref($content) eq 'SCALAR') {
+ print esc_html($$content);
+ } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
+ print <$content>;
+ } elsif (!ref($content) && defined($content)) {
+ print $content;
+ }
+
+ print $cgi->end_div;
+}
+
+sub format_timestamp_html {
+ my $date = shift;
+ my $strtime = $date->{'rfc2822'};
+
+ my (undef, undef, $datetime_class) =
+ gitweb_get_feature('javascript-timezone');
+ if ($datetime_class) {
+ $strtime = qq!$strtime !;
+ }
+
+ my $localtime_format = '(%02d:%02d %s)';
+ if ($date->{'hour_local'} < 6) {
+ $localtime_format = '(%02d:%02d %s)';
+ }
+ $strtime .= ' ' .
+ sprintf($localtime_format,
+ $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
+
+ return $strtime;
+}
+
+# Outputs the author name and date in long form
+sub git_print_authorship {
+ my $co = shift;
+ my %opts = @_;
+ my $tag = $opts{-tag} || 'div';
+ my $author = $co->{'author_name'};
+
+ my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
+ print "<$tag class=\"author_date\">" .
+ format_search_author($author, "author", esc_html($author)) .
+ " [".format_timestamp_html(\%ad)."]".
+ git_get_avatar($co->{'author_email'}, -pad_before => 1) .
+ "$tag>\n";
+}
+
+# Outputs table rows containing the full author or committer information,
+# in the format expected for 'commit' view (& similar).
+# Parameters are a commit hash reference, followed by the list of people
+# to output information for. If the list is empty it defaults to both
+# author and committer.
+sub git_print_authorship_rows {
+ my $co = shift;
+ # too bad we can't use @people = @_ || ('author', 'committer')
+ my @people = @_;
+ @people = ('author', 'committer') unless @people;
+ foreach my $who (@people) {
+ my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
+ print "$who " .
+ format_search_author($co->{"${who}_name"}, $who,
+ esc_html($co->{"${who}_name"})) . " " .
+ format_search_author($co->{"${who}_email"}, $who,
+ esc_html("<" . $co->{"${who}_email"} . ">")) .
+ " " .
+ git_get_avatar($co->{"${who}_email"}, -size => 'double') .
+ " \n" .
+ "" .
+ "" .
+ format_timestamp_html(\%wd) .
+ " " .
+ " \n";
+ }
+}
+
+sub git_print_page_path {
+ my $name = shift;
+ my $type = shift;
+ my $hb = shift;
+
+
+ print "";
+ print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
+ -title => 'tree root'}, to_utf8("[$project]"));
+ print " / ";
+ if (defined $name) {
+ my @dirname = split '/', $name;
+ my $basename = pop @dirname;
+ my $fullname = '';
+
+ foreach my $dir (@dirname) {
+ $fullname .= ($fullname ? '/' : '') . $dir;
+ print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
+ hash_base=>$hb),
+ -title => $fullname}, esc_path($dir));
+ print " / ";
+ }
+ if (defined $type && $type eq 'blob') {
+ print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
+ hash_base=>$hb),
+ -title => $name}, esc_path($basename));
+ } elsif (defined $type && $type eq 'tree') {
+ print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
+ hash_base=>$hb),
+ -title => $name}, esc_path($basename));
+ print " / ";
+ } else {
+ print esc_path($basename);
+ }
+ }
+ print "
\n";
+}
+
+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;
+ }
+
+ # print log
+ my $skip_blank_line = 0;
+ foreach my $line (@$log) {
+ if ($line =~ m/^\s*([A-Z][-A-Za-z]*-([Bb]y|[Tt]o)|C[Cc]|(Clos|Fix)es): /) {
+ if (! $opts{'-remove_signoff'}) {
+ print "" . esc_html($line) . " \n";
+ $skip_blank_line = 1;
+ }
+ next;
+ }
+
+ if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
+ if (! $opts{'-remove_signoff'}) {
+ print "" . esc_html($1) . ": " .
+ "" . esc_html($2) . " " .
+ " \n";
+ $skip_blank_line = 1;
+ }
+ next;
+ }
+
+ # print only one empty line
+ # do not print empty line after signoff
+ if ($line eq "") {
+ next if ($skip_blank_line);
+ $skip_blank_line = 1;
+ } else {
+ $skip_blank_line = 0;
+ }
+
+ print format_log_line_html($line) . " \n";
+ }
+
+ if ($opts{'-final_empty_line'}) {
+ # end with single empty line
+ print " \n" unless $skip_blank_line;
+ }
+}
+
+# return link target (what link points to)
+sub git_get_link_target {
+ my $hash = shift;
+ my $link_target;
+
+ # read link
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ or return;
+ {
+ local $/ = undef;
+ $link_target = <$fd>;
+ }
+ close $fd
+ or return;
+
+ return $link_target;
+}
+
+# given link target, and the directory (basedir) the link is in,
+# return target of link relative to top directory (top tree);
+# return undef if it is not possible (including absolute links).
+sub normalize_link_target {
+ my ($link_target, $basedir) = @_;
+
+ # absolute symlinks (beginning with '/') cannot be normalized
+ return if (substr($link_target, 0, 1) eq '/');
+
+ # normalize link target to path from top (root) tree (dir)
+ my $path;
+ if ($basedir) {
+ $path = $basedir . '/' . $link_target;
+ } else {
+ # we are in top (root) tree (dir)
+ $path = $link_target;
+ }
+
+ # remove //, /./, and /../
+ my @path_parts;
+ foreach my $part (split('/', $path)) {
+ # discard '.' and ''
+ next if (!$part || $part eq '.');
+ # handle '..'
+ if ($part eq '..') {
+ if (@path_parts) {
+ pop @path_parts;
+ } else {
+ # link leads outside repository (outside top dir)
+ return;
+ }
+ } else {
+ push @path_parts, $part;
+ }
+ }
+ $path = join('/', @path_parts);
+
+ return $path;
+}
+
+# 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 (exists $t->{'size'}) {
+ print "$t->{'size'} \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_path($t->{'name'}));
+ if (S_ISLNK(oct $t->{'mode'})) {
+ my $link_target = git_get_link_target($t->{'hash'});
+ if ($link_target) {
+ my $norm_target = normalize_link_target($link_target, $basedir);
+ if (defined $norm_target) {
+ print " -> " .
+ $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
+ file_name=>$norm_target),
+ -title => $norm_target}, esc_path($link_target));
+ } else {
+ print " -> " . esc_path($link_target);
+ }
+ }
+ }
+ print " \n";
+ print "";
+ print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
+ file_name=>"$basedir$t->{'name'}", %base_key)},
+ "blob");
+ if ($have_blame) {
+ print " | " .
+ $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
+ file_name=>"$basedir$t->{'name'}", %base_key)},
+ "blame");
+ }
+ if (defined $hash_base) {
+ print " | " .
+ $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+ hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
+ "history");
+ }
+ print " | " .
+ $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
+ 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_path($t->{'name'}));
+ print " \n";
+ print "";
+ print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
+ file_name=>"$basedir$t->{'name'}",
+ %base_key)},
+ "tree");
+ if (defined $hash_base) {
+ print " | " .
+ $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+ file_name=>"$basedir$t->{'name'}")},
+ "history");
+ }
+ print " \n";
+ } else {
+ # unknown object: we can only present history for it
+ # (this includes 'commit' object, i.e. submodule support)
+ print "" .
+ esc_path($t->{'name'}) .
+ " \n";
+ print "";
+ if (defined $hash_base) {
+ print $cgi->a({-href => href(action=>"history",
+ hash_base=>$hash_base,
+ file_name=>"$basedir$t->{'name'}")},
+ "history");
+ }
+ print " \n";
+ }
+}
+
+## ......................................................................
+## functions printing large fragments of HTML
+
+# get pre-image filenames for merge (combined) diff
+sub fill_from_file_info {
+ my ($diff, @parents) = @_;
+
+ $diff->{'from_file'} = [ ];
+ $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
+ for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
+ if ($diff->{'status'}[$i] eq 'R' ||
+ $diff->{'status'}[$i] eq 'C') {
+ $diff->{'from_file'}[$i] =
+ git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
+ }
+ }
+
+ return $diff;
+}
+
+# is current raw difftree line of file deletion
+sub is_deleted {
+ my $diffinfo = shift;
+
+ return $diffinfo->{'to_id'} eq ('0' x 40) || $diffinfo->{'to_id'} eq ('0' x 64);
+}
+
+# does patch correspond to [previous] difftree raw line
+# $diffinfo - hashref of parsed raw diff format
+# $patchinfo - hashref of parsed patch diff format
+# (the same keys as in $diffinfo)
+sub is_patch_split {
+ my ($diffinfo, $patchinfo) = @_;
+
+ return defined $diffinfo && defined $patchinfo
+ && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
+}
+
+
+sub git_difftree_body {
+ my ($difftree, $hash, @parents) = @_;
+ my ($parent) = $parents[0];
+ my $have_blame = gitweb_check_feature('blame');
+ print "\n";
+ if ($#{$difftree} > 10) {
+ print(($#{$difftree} + 1) . " files changed:\n");
+ }
+ print "
\n";
+
+ print " 1 ? "combined " : "") .
+ "diff_tree\">\n";
+
+ # header only for combined diff in 'commitdiff' view
+ my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
+ if ($has_header) {
+ # table header
+ print "\n" .
+ " \n"; # filename, patchN link
+ for (my $i = 0; $i < @parents; $i++) {
+ my $par = $parents[$i];
+ print "" .
+ $cgi->a({-href => href(action=>"commitdiff",
+ hash=>$hash, hash_parent=>$par),
+ -title => 'commitdiff to parent number ' .
+ ($i+1) . ': ' . substr($par,0,7)},
+ $i+1) .
+ " \n";
+ }
+ print " \n\n";
+ }
+
+ my $alternate = 1;
+ my $patchno = 0;
+ foreach my $line (@{$difftree}) {
+ my $diff = parsed_difftree_line($line);
+
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+
+ if (exists $diff->{'nparents'}) { # combined diff
+
+ fill_from_file_info($diff, @parents)
+ unless exists $diff->{'from_file'};
+
+ if (!is_deleted($diff)) {
+ # file exists in the result (child) commit
+ print "" .
+ $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ file_name=>$diff->{'to_file'},
+ hash_base=>$hash),
+ -class => "list"}, esc_path($diff->{'to_file'})) .
+ " \n";
+ } else {
+ print "" .
+ esc_path($diff->{'to_file'}) .
+ " \n";
+ }
+
+ if ($action eq 'commitdiff') {
+ # link to patch
+ $patchno++;
+ print "" .
+ $cgi->a({-href => href(-anchor=>"patch$patchno")},
+ "patch") .
+ " | " .
+ " \n";
+ }
+
+ my $has_history = 0;
+ my $not_deleted = 0;
+ for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
+ my $hash_parent = $parents[$i];
+ my $from_hash = $diff->{'from_id'}[$i];
+ my $from_path = $diff->{'from_file'}[$i];
+ my $status = $diff->{'status'}[$i];
+
+ $has_history ||= ($status ne 'A');
+ $not_deleted ||= ($status ne 'D');
+
+ if ($status eq 'A') {
+ print " | \n";
+ } elsif ($status eq 'D') {
+ print "" .
+ $cgi->a({-href => href(action=>"blob",
+ hash_base=>$hash,
+ hash=>$from_hash,
+ file_name=>$from_path)},
+ "blob" . ($i+1)) .
+ " | \n";
+ } else {
+ if ($diff->{'to_id'} eq $from_hash) {
+ print "";
+ } else {
+ print " ";
+ }
+ print $cgi->a({-href => href(action=>"blobdiff",
+ hash=>$diff->{'to_id'},
+ hash_parent=>$from_hash,
+ hash_base=>$hash,
+ hash_parent_base=>$hash_parent,
+ file_name=>$diff->{'to_file'},
+ file_parent=>$from_path)},
+ "diff" . ($i+1)) .
+ " | \n";
+ }
+ }
+
+ print "";
+ if ($not_deleted) {
+ print $cgi->a({-href => href(action=>"blob",
+ hash=>$diff->{'to_id'},
+ file_name=>$diff->{'to_file'},
+ hash_base=>$hash)},
+ "blob");
+ print " | " if ($has_history);
+ }
+ if ($has_history) {
+ print $cgi->a({-href => href(action=>"history",
+ file_name=>$diff->{'to_file'},
+ hash_base=>$hash)},
+ "history");
+ }
+ print " \n";
+
+ print " \n";
+ next; # instead of 'else' clause, to avoid extra indent
+ }
+ # else ordinary diff
+
+ my ($to_mode_oct, $to_mode_str, $to_file_type);
+ my ($from_mode_oct, $from_mode_str, $from_file_type);
+ if ($diff->{'to_mode'} ne ('0' x 6)) {
+ $to_mode_oct = oct $diff->{'to_mode'};
+ if (S_ISREG($to_mode_oct)) { # only for regular file
+ $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
+ }
+ $to_file_type = file_type($diff->{'to_mode'});
+ }
+ if ($diff->{'from_mode'} ne ('0' x 6)) {
+ $from_mode_oct = oct $diff->{'from_mode'};
+ if (S_ISREG($from_mode_oct)) { # only for regular file
+ $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
+ }
+ $from_file_type = file_type($diff->{'from_mode'});
+ }
+
+ if ($diff->{'status'} eq "A") { # created
+ my $mode_chng = "[new $to_file_type";
+ $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
+ $mode_chng .= "] ";
+ print "";
+ print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ hash_base=>$hash, file_name=>$diff->{'file'}),
+ -class => "list"}, esc_path($diff->{'file'}));
+ print " \n";
+ print "$mode_chng \n";
+ print "";
+ if ($action eq 'commitdiff') {
+ # link to patch
+ $patchno++;
+ print $cgi->a({-href => href(-anchor=>"patch$patchno")},
+ "patch") .
+ " | ";
+ }
+ print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ hash_base=>$hash, file_name=>$diff->{'file'})},
+ "blob");
+ print " \n";
+
+ } elsif ($diff->{'status'} eq "D") { # deleted
+ my $mode_chng = "[deleted $from_file_type] ";
+ print "";
+ print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+ hash_base=>$parent, file_name=>$diff->{'file'}),
+ -class => "list"}, esc_path($diff->{'file'}));
+ print " \n";
+ print "$mode_chng \n";
+ print "";
+ if ($action eq 'commitdiff') {
+ # link to patch
+ $patchno++;
+ print $cgi->a({-href => href(-anchor=>"patch$patchno")},
+ "patch") .
+ " | ";
+ }
+ print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
+ hash_base=>$parent, file_name=>$diff->{'file'})},
+ "blob") . " | ";
+ if ($have_blame) {
+ 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 " \n";
+
+ } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
+ my $mode_chnge = "";
+ if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
+ $mode_chnge = "[changed";
+ if ($from_file_type ne $to_file_type) {
+ $mode_chnge .= " from $from_file_type to $to_file_type";
+ }
+ if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
+ if ($from_mode_str && $to_mode_str) {
+ $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
+ } elsif ($to_mode_str) {
+ $mode_chnge .= " mode: $to_mode_str";
+ }
+ }
+ $mode_chnge .= "] \n";
+ }
+ print "";
+ print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ hash_base=>$hash, file_name=>$diff->{'file'}),
+ -class => "list"}, esc_path($diff->{'file'}));
+ print " \n";
+ print "$mode_chnge \n";
+ print "";
+ if ($action eq 'commitdiff') {
+ # link to patch
+ $patchno++;
+ print $cgi->a({-href => href(-anchor=>"patch$patchno")},
+ "patch") .
+ " | ";
+ } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
+ # "commit" view and modified file (not onlu mode changed)
+ 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 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ hash_base=>$hash, file_name=>$diff->{'file'})},
+ "blob") . " | ";
+ if ($have_blame) {
+ print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+ file_name=>$diff->{'file'})},
+ "blame") . " | ";
+ }
+ print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+ file_name=>$diff->{'file'})},
+ "history");
+ print " \n";
+
+ } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
+ my %status_name = ('R' => 'moved', 'C' => 'copied');
+ my $nstatus = $status_name{$diff->{'status'}};
+ my $mode_chng = "";
+ if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
+ # mode also for directories, so we cannot use $to_mode_str
+ $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
+ }
+ print "" .
+ $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
+ hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
+ -class => "list"}, esc_path($diff->{'to_file'})) . " \n" .
+ "[$nstatus from " .
+ $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
+ hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
+ -class => "list"}, esc_path($diff->{'from_file'})) .
+ " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng] \n" .
+ "";
+ if ($action eq 'commitdiff') {
+ # link to patch
+ $patchno++;
+ print $cgi->a({-href => href(-anchor=>"patch$patchno")},
+ "patch") .
+ " | ";
+ } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
+ # "commit" view and modified file (not only pure rename or copy)
+ 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 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
+ hash_base=>$parent, file_name=>$diff->{'to_file'})},
+ "blob") . " | ";
+ if ($have_blame) {
+ print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
+ file_name=>$diff->{'to_file'})},
+ "blame") . " | ";
+ }
+ print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+ file_name=>$diff->{'to_file'})},
+ "history");
+ print " \n";
+
+ } # we should not encounter Unmerged (U) or Unknown (X) status
+ print "\n";
+ }
+ print " " if $has_header;
+ print "
\n";
+}
+
+# Print context lines and then rem/add lines in a side-by-side manner.
+sub print_sidebyside_diff_lines {
+ my ($ctx, $rem, $add) = @_;
+
+ # print context block before add/rem block
+ if (@$ctx) {
+ print join '',
+ '',
+ '
',
+ @$ctx,
+ '
',
+ '
',
+ @$ctx,
+ '
',
+ '
';
+ }
+
+ if (!@$add) {
+ # pure removal
+ print join '',
+ '',
+ '
',
+ @$rem,
+ '
',
+ '
';
+ } elsif (!@$rem) {
+ # pure addition
+ print join '',
+ '',
+ '
',
+ @$add,
+ '
',
+ '
';
+ } else {
+ print join '',
+ '',
+ '
',
+ @$rem,
+ '
',
+ '
',
+ @$add,
+ '
',
+ '
';
+ }
+}
+
+# Print context lines and then rem/add lines in inline manner.
+sub print_inline_diff_lines {
+ my ($ctx, $rem, $add) = @_;
+
+ print @$ctx, @$rem, @$add;
+}
+
+# Format removed and added line, mark changed part and HTML-format them.
+# Implementation is based on contrib/diff-highlight
+sub format_rem_add_lines_pair {
+ my ($rem, $add, $num_parents) = @_;
+
+ # We need to untabify lines before split()'ing them;
+ # otherwise offsets would be invalid.
+ chomp $rem;
+ chomp $add;
+ $rem = untabify($rem);
+ $add = untabify($add);
+
+ my @rem = split(//, $rem);
+ my @add = split(//, $add);
+ my ($esc_rem, $esc_add);
+ # Ignore leading +/- characters for each parent.
+ my ($prefix_len, $suffix_len) = ($num_parents, 0);
+ my ($prefix_has_nonspace, $suffix_has_nonspace);
+
+ my $shorter = (@rem < @add) ? @rem : @add;
+ while ($prefix_len < $shorter) {
+ last if ($rem[$prefix_len] ne $add[$prefix_len]);
+
+ $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
+ $prefix_len++;
+ }
+
+ while ($prefix_len + $suffix_len < $shorter) {
+ last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
+
+ $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
+ $suffix_len++;
+ }
+
+ # Mark lines that are different from each other, but have some common
+ # part that isn't whitespace. If lines are completely different, don't
+ # mark them because that would make output unreadable, especially if
+ # diff consists of multiple lines.
+ if ($prefix_has_nonspace || $suffix_has_nonspace) {
+ $esc_rem = esc_html_hl_regions($rem, 'marked',
+ [$prefix_len, @rem - $suffix_len], -nbsp=>1);
+ $esc_add = esc_html_hl_regions($add, 'marked',
+ [$prefix_len, @add - $suffix_len], -nbsp=>1);
+ } else {
+ $esc_rem = esc_html($rem, -nbsp=>1);
+ $esc_add = esc_html($add, -nbsp=>1);
+ }
+
+ return format_diff_line(\$esc_rem, 'rem'),
+ format_diff_line(\$esc_add, 'add');
+}
+
+# HTML-format diff context, removed and added lines.
+sub format_ctx_rem_add_lines {
+ my ($ctx, $rem, $add, $num_parents) = @_;
+ my (@new_ctx, @new_rem, @new_add);
+ my $can_highlight = 0;
+ my $is_combined = ($num_parents > 1);
+
+ # Highlight if every removed line has a corresponding added line.
+ if (@$add > 0 && @$add == @$rem) {
+ $can_highlight = 1;
+
+ # Highlight lines in combined diff only if the chunk contains
+ # diff between the same version, e.g.
+ #
+ # - a
+ # - b
+ # + c
+ # + d
+ #
+ # Otherwise the highlighting would be confusing.
+ if ($is_combined) {
+ for (my $i = 0; $i < @$add; $i++) {
+ my $prefix_rem = substr($rem->[$i], 0, $num_parents);
+ my $prefix_add = substr($add->[$i], 0, $num_parents);
+
+ $prefix_rem =~ s/-/+/g;
+
+ if ($prefix_rem ne $prefix_add) {
+ $can_highlight = 0;
+ last;
+ }
+ }
+ }
+ }
+
+ if ($can_highlight) {
+ for (my $i = 0; $i < @$add; $i++) {
+ my ($line_rem, $line_add) = format_rem_add_lines_pair(
+ $rem->[$i], $add->[$i], $num_parents);
+ push @new_rem, $line_rem;
+ push @new_add, $line_add;
+ }
+ } else {
+ @new_rem = map { format_diff_line($_, 'rem') } @$rem;
+ @new_add = map { format_diff_line($_, 'add') } @$add;
+ }
+
+ @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
+
+ return (\@new_ctx, \@new_rem, \@new_add);
+}
+
+# Print context lines and then rem/add lines.
+sub print_diff_lines {
+ my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
+ my $is_combined = $num_parents > 1;
+
+ ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
+ $num_parents);
+
+ if ($diff_style eq 'sidebyside' && !$is_combined) {
+ print_sidebyside_diff_lines($ctx, $rem, $add);
+ } else {
+ # default 'inline' style and unknown styles
+ print_inline_diff_lines($ctx, $rem, $add);
+ }
+}
+
+sub print_diff_chunk {
+ my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
+ my (@ctx, @rem, @add);
+
+ # The class of the previous line.
+ my $prev_class = '';
+
+ return unless @chunk;
+
+ # incomplete last line might be among removed or added lines,
+ # or both, or among context lines: find which
+ for (my $i = 1; $i < @chunk; $i++) {
+ if ($chunk[$i][0] eq 'incomplete') {
+ $chunk[$i][0] = $chunk[$i-1][0];
+ }
+ }
+
+ # guardian
+ push @chunk, ["", ""];
+
+ foreach my $line_info (@chunk) {
+ my ($class, $line) = @$line_info;
+
+ # print chunk headers
+ if ($class && $class eq 'chunk_header') {
+ print format_diff_line($line, $class, $from, $to);
+ next;
+ }
+
+ ## print from accumulator when have some add/rem lines or end
+ # of chunk (flush context lines), or when have add and rem
+ # lines and new block is reached (otherwise add/rem lines could
+ # be reordered)
+ if (!$class || ((@rem || @add) && $class eq 'ctx') ||
+ (@rem && @add && $class ne $prev_class)) {
+ print_diff_lines(\@ctx, \@rem, \@add,
+ $diff_style, $num_parents);
+ @ctx = @rem = @add = ();
+ }
+
+ ## adding lines to accumulator
+ # guardian value
+ last unless $line;
+ # rem, add or change
+ if ($class eq 'rem') {
+ push @rem, $line;
+ } elsif ($class eq 'add') {
+ push @add, $line;
+ }
+ # context line
+ if ($class eq 'ctx') {
+ push @ctx, $line;
+ }
+
+ $prev_class = $class;
+ }
+}
+
+sub git_patchset_body {
+ my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
+ my ($hash_parent) = $hash_parents[0];
+
+ my $is_combined = (@hash_parents > 1);
+ my $patch_idx = 0;
+ my $patch_number = 0;
+ my $patch_line;
+ my $diffinfo;
+ my $to_name;
+ my (%from, %to);
+ my @chunk; # for side-by-side diff
+
+ print "\n";
+
+ # skip to first patch
+ while ($patch_line = <$fd>) {
+ chomp $patch_line;
+
+ last if ($patch_line =~ m/^diff /);
+ }
+
+ PATCH:
+ while ($patch_line) {
+
+ # parse "git diff" header line
+ if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
+ # $1 is from_name, which we do not use
+ $to_name = unquote($2);
+ $to_name =~ s!^b/!!;
+ } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
+ # $1 is 'cc' or 'combined', which we do not use
+ $to_name = unquote($2);
+ } else {
+ $to_name = undef;
+ }
+
+ # check if current patch belong to current raw line
+ # and parse raw git-diff line if needed
+ if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
+ # this is continuation of a split patch
+ print "
\n";
+ } else {
+ # advance raw git-diff output if needed
+ $patch_idx++ if defined $diffinfo;
+
+ # read and prepare patch information
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+ # compact combined diff output can have some patches skipped
+ # find which patch (using pathname of result) we are at now;
+ if ($is_combined) {
+ while ($to_name ne $diffinfo->{'to_file'}) {
+ print "
\n" .
+ format_diff_cc_simplified($diffinfo, @hash_parents) .
+ "
\n"; # class="patch"
+
+ $patch_idx++;
+ $patch_number++;
+
+ last if $patch_idx > $#$difftree;
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+ }
+ }
+
+ # modifies %from, %to hashes
+ parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
+
+ # this is first patch for raw difftree line with $patch_idx index
+ # we index @$difftree array from 0, but number patches from 1
+ print "
\n";
+ }
+
+ # git diff header
+ #assert($patch_line =~ m/^diff /) if DEBUG;
+ #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+ $patch_number++;
+ # print "git diff" header
+ print format_git_diff_header_line($patch_line, $diffinfo,
+ \%from, \%to);
+
+ # print extended diff header
+ print "
\n";
+ EXTENDED_HEADER:
+ while ($patch_line = <$fd>) {
+ chomp $patch_line;
+
+ last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
+
+ print format_extended_diff_header_line($patch_line, $diffinfo,
+ \%from, \%to);
+ }
+ print "
\n"; # class="diff extended_header"
+
+ # from-file/to-file diff header
+ if (! $patch_line) {
+ print "
\n"; # class="patch"
+ last PATCH;
+ }
+ next PATCH if ($patch_line =~ m/^diff /);
+ #assert($patch_line =~ m/^---/) if DEBUG;
+
+ my $last_patch_line = $patch_line;
+ $patch_line = <$fd>;
+ chomp $patch_line;
+ #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
+
+ print format_diff_from_to_header($last_patch_line, $patch_line,
+ $diffinfo, \%from, \%to,
+ @hash_parents);
+
+ # the patch itself
+ LINE:
+ while ($patch_line = <$fd>) {
+ chomp $patch_line;
+
+ next PATCH if ($patch_line =~ m/^diff /);
+
+ my $class = diff_line_class($patch_line, \%from, \%to);
+
+ if ($class eq 'chunk_header') {
+ print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
+ @chunk = ();
+ }
+
+ push @chunk, [ $class, $patch_line ];
+ }
+
+ } continue {
+ if (@chunk) {
+ print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
+ @chunk = ();
+ }
+ print "
\n"; # class="patch"
+ }
+
+ # for compact combined (--cc) format, with chunk and patch simplification
+ # the 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
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+ # generate anchor for "patch" links in difftree / whatchanged part
+ print "
\n" .
+ format_diff_cc_simplified($diffinfo, @hash_parents) .
+ "
\n"; # class="patch"
+
+ $patch_number++;
+ }
+
+ if ($patch_number == 0) {
+ if (@hash_parents > 1) {
+ print "
Trivial merge
\n";
+ } else {
+ print "
No differences found
\n";
+ }
+ }
+
+ print "
\n"; # class="patchset"
+}
+
+# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+
+sub git_project_search_form {
+ my ($searchtext, $search_use_regexp) = @_;
+
+ my $limit = '';
+ if ($project_filter) {
+ $limit = " in '$project_filter/'";
+ }
+
+ print "\n";
+ print $cgi->start_form(-method => 'get', -action => $my_uri) .
+ $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
+ print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
+ if (defined $project_filter);
+ print $cgi->textfield(-name => 's', -value => $searchtext,
+ -title => "Search project by name and description$limit",
+ -size => 60) . "\n" .
+ "" .
+ $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
+ -checked => $search_use_regexp) .
+ " \n" .
+ $cgi->submit(-name => 'btnS', -value => 'Search') .
+ $cgi->end_form() . "\n" .
+ $cgi->a({-href => href(project => undef, searchtext => undef,
+ project_filter => $project_filter)},
+ esc_html("List all projects$limit")) . " \n";
+ print "
\n";
+}
+
+# entry for given @keys needs filling if at least one of keys in list
+# is not present in %$project_info
+sub project_info_needs_filling {
+ my ($project_info, @keys) = @_;
+
+ # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
+ foreach my $key (@keys) {
+ if (!exists $project_info->{$key}) {
+ return 1;
+ }
+ }
+ return;
+}
+
+# fills project list info (age, description, owner, category, forks, etc.)
+# for each project in the list, removing invalid projects from
+# returned list, or fill only specified info.
+#
+# Invalid projects are removed from the returned list if and only if you
+# ask 'age' or 'age_string' to be filled, because they are the only fields
+# that run unconditionally git command that requires repository, and
+# therefore do always check if project repository is invalid.
+#
+# USAGE:
+# * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
+# ensures that 'descr_long' and 'ctags' fields are filled
+# * @project_list = fill_project_list_info(\@project_list)
+# ensures that all fields are filled (and invalid projects removed)
+#
+# NOTE: modifies $projlist, but does not remove entries from it
+sub fill_project_list_info {
+ my ($projlist, @wanted_keys) = @_;
+ my @projects;
+ my $filter_set = sub { return @_; };
+ if (@wanted_keys) {
+ my %wanted_keys = map { $_ => 1 } @wanted_keys;
+ $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
+ }
+
+ my $show_ctags = gitweb_check_feature('ctags');
+ PROJECT:
+ foreach my $pr (@$projlist) {
+ if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
+ my (@activity) = git_get_last_activity($pr->{'path'});
+ unless (@activity) {
+ next PROJECT;
+ }
+ ($pr->{'age'}, $pr->{'age_string'}) = @activity;
+ }
+ if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
+ my $descr = git_get_project_description($pr->{'path'}) || "";
+ $descr = to_utf8($descr);
+ $pr->{'descr_long'} = $descr;
+ $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
+ }
+ if (project_info_needs_filling($pr, $filter_set->('owner'))) {
+ $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
+ }
+ if ($show_ctags &&
+ project_info_needs_filling($pr, $filter_set->('ctags'))) {
+ $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
+ }
+ if ($projects_list_group_categories &&
+ project_info_needs_filling($pr, $filter_set->('category'))) {
+ my $cat = git_get_project_category($pr->{'path'}) ||
+ $project_list_default_category;
+ $pr->{'category'} = to_utf8($cat);
+ }
+
+ push @projects, $pr;
+ }
+
+ return @projects;
+}
+
+sub sort_projects_list {
+ my ($projlist, $order) = @_;
+
+ sub order_str {
+ my $key = shift;
+ return sub { $a->{$key} cmp $b->{$key} };
+ }
+
+ sub order_num_then_undef {
+ my $key = shift;
+ return sub {
+ defined $a->{$key} ?
+ (defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) :
+ (defined $b->{$key} ? 1 : 0)
+ };
+ }
+
+ my %orderings = (
+ project => order_str('path'),
+ descr => order_str('descr_long'),
+ owner => order_str('owner'),
+ age => order_num_then_undef('age'),
+ );
+
+ my $ordering = $orderings{$order};
+ return defined $ordering ? sort $ordering @$projlist : @$projlist;
+}
+
+# returns a hash of categories, containing the list of project
+# belonging to each category
+sub build_projlist_by_category {
+ my ($projlist, $from, $to) = @_;
+ my %categories;
+
+ $from = 0 unless defined $from;
+ $to = $#$projlist if (!defined $to || $#$projlist < $to);
+
+ for (my $i = $from; $i <= $to; $i++) {
+ my $pr = $projlist->[$i];
+ push @{$categories{ $pr->{'category'} }}, $pr;
+ }
+
+ return wantarray ? %categories : \%categories;
+}
+
+# print 'sort by' element, generating 'sort by $name' replay link
+# if that order is not selected
+sub print_sort_th {
+ print format_sort_th(@_);
+}
+
+sub format_sort_th {
+ my ($name, $order, $header) = @_;
+ my $sort_th = "";
+ $header ||= ucfirst($name);
+
+ if ($order eq $name) {
+ $sort_th .= " $header \n";
+ } else {
+ $sort_th .= "" .
+ $cgi->a({-href => href(-replay=>1, order=>$name),
+ -class => "header"}, $header) .
+ " \n";
+ }
+
+ return $sort_th;
+}
+
+sub git_project_list_rows {
+ my ($projlist, $from, $to, $check_forks) = @_;
+
+ $from = 0 unless defined $from;
+ $to = $#$projlist if (!defined $to || $#$projlist < $to);
+
+ my $alternate = 1;
+ for (my $i = $from; $i <= $to; $i++) {
+ my $pr = $projlist->[$i];
+
+ if ($alternate) {
+ print " \n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+
+ if ($check_forks) {
+ print "";
+ if ($pr->{'forks'}) {
+ my $nforks = scalar @{$pr->{'forks'}};
+ if ($nforks > 0) {
+ print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
+ -title => "$nforks forks"}, "+");
+ } else {
+ print $cgi->span({-title => "$nforks forks"}, "+");
+ }
+ }
+ print " \n";
+ }
+ print "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+ -class => "list"},
+ esc_html_match_hl($pr->{'path'}, $search_regexp)) .
+ " \n" .
+ "" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+ -class => "list",
+ -title => $pr->{'descr_long'}},
+ $search_regexp
+ ? esc_html_match_hl_chopped($pr->{'descr_long'},
+ $pr->{'descr'}, $search_regexp)
+ : esc_html($pr->{'descr'})) .
+ " \n";
+ unless ($omit_owner) {
+ print "" . chop_and_escape_str($pr->{'owner'}, 15) . " \n";
+ }
+ unless ($omit_age_column) {
+ print "{'age'}) . "\">" .
+ (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . " \n";
+ }
+ print"" .
+ $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=>"tree")}, "tree") .
+ ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
+ " \n" .
+ " \n";
+ }
+}
+
+sub git_project_list_body {
+ # actually uses global variable $project
+ my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
+ my @projects = @$projlist;
+
+ my $check_forks = gitweb_check_feature('forks');
+ my $show_ctags = gitweb_check_feature('ctags');
+ my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
+ $check_forks = undef
+ if ($tagfilter || $search_regexp);
+
+ # filtering out forks before filling info allows to do less work
+ @projects = filter_forks_from_projects_list(\@projects)
+ if ($check_forks);
+ # search_projects_list pre-fills required info
+ @projects = search_projects_list(\@projects,
+ 'search_regexp' => $search_regexp,
+ 'tagfilter' => $tagfilter)
+ if ($tagfilter || $search_regexp);
+ # fill the rest
+ my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
+ push @all_fields, ('age', 'age_string') unless($omit_age_column);
+ push @all_fields, 'owner' unless($omit_owner);
+ @projects = fill_project_list_info(\@projects, @all_fields);
+
+ $order ||= $default_projects_order;
+ $from = 0 unless defined $from;
+ $to = $#projects if (!defined $to || $#projects < $to);
+
+ # short circuit
+ if ($from > $to) {
+ print "\n".
+ "No such projects found \n".
+ "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects \n".
+ " \n \n";
+ return;
+ }
+
+ @projects = sort_projects_list(\@projects, $order);
+
+ if ($show_ctags) {
+ my $ctags = git_gather_all_ctags(\@projects);
+ my $cloud = git_populate_project_tagcloud($ctags);
+ print git_show_project_tagcloud($cloud, 64);
+ }
+
+ print "\n";
+ unless ($no_header) {
+ print "\n";
+ if ($check_forks) {
+ print " \n";
+ }
+ print_sort_th('project', $order, 'Project');
+ print_sort_th('descr', $order, 'Description');
+ print_sort_th('owner', $order, 'Owner') unless $omit_owner;
+ print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
+ print " \n" . # for links
+ " \n";
+ }
+
+ if ($projects_list_group_categories) {
+ # only display categories with projects in the $from-$to window
+ @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
+ my %categories = build_projlist_by_category(\@projects, $from, $to);
+ foreach my $cat (sort keys %categories) {
+ unless ($cat eq "") {
+ print "\n";
+ if ($check_forks) {
+ print " \n";
+ }
+ print "".esc_html($cat)." \n";
+ print " \n";
+ }
+
+ git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
+ }
+ } else {
+ git_project_list_rows(\@projects, $from, $to, $check_forks);
+ }
+
+ if (defined $extra) {
+ print "\n";
+ if ($check_forks) {
+ print " \n";
+ }
+ print "$extra \n" .
+ " \n";
+ }
+ print "
\n";
+}
+
+sub git_log_body {
+ # uses global variable $project
+ my ($commitlist, $from, $to, $refs, $extra) = @_;
+
+ $from = 0 unless defined $from;
+ $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+ for (my $i = 0; $i <= $to; $i++) {
+ my %co = %{$commitlist->[$i]};
+ next if !%co;
+ my $commit = $co{'id'};
+ my $ref = format_ref_marker($refs, $commit);
+ git_print_header_div('commit',
+ "$co{'age_string'} " .
+ esc_html($co{'title'}) . $ref,
+ $commit);
+ print "\n" .
+ "
\n" .
+ $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";
+ git_print_authorship(\%co, -tag => 'span');
+ print "
\n
\n";
+
+ print "\n";
+ git_print_log($co{'comment'}, -final_empty_line=> 1);
+ print "
\n";
+ }
+ if ($extra) {
+ print "\n";
+ print "$extra\n";
+ print "
\n";
+ }
+}
+
+sub git_shortlog_body {
+ # uses global variable $project
+ my ($commitlist, $from, $to, $refs, $extra) = @_;
+
+ $from = 0 unless defined $from;
+ $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+ print "\n";
+ my $alternate = 1;
+ for (my $i = $from; $i <= $to; $i++) {
+ my %co = %{$commitlist->[$i]};
+ my $commit = $co{'id'};
+ my $ref = format_ref_marker($refs, $commit);
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ # git_summary() used print "$co{'age_string'} \n" .
+ print "$co{'age_string_date'} \n" .
+ format_author_html('td', \%co, 10) . "";
+ print format_subject_html($co{'title'}, $co{'title_short'},
+ href(action=>"commit", hash=>$commit), $ref);
+ print " \n" .
+ "" .
+ $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");
+ my $snapshot_links = format_snapshot_links($commit);
+ if (defined $snapshot_links) {
+ print " | " . $snapshot_links;
+ }
+ print " \n" .
+ " \n";
+ }
+ if (defined $extra) {
+ print "\n" .
+ "$extra \n" .
+ " \n";
+ }
+ print "
\n";
+}
+
+sub git_history_body {
+ # Warning: assumes constant type (blob or tree) during history
+ my ($commitlist, $from, $to, $refs, $extra,
+ $file_name, $file_hash, $ftype) = @_;
+
+ $from = 0 unless defined $from;
+ $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
+
+ print "\n";
+ my $alternate = 1;
+ for (my $i = $from; $i <= $to; $i++) {
+ my %co = %{$commitlist->[$i]};
+ if (!%co) {
+ next;
+ }
+ my $commit = $co{'id'};
+
+ my $ref = format_ref_marker($refs, $commit);
+
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ print "$co{'age_string_date'} \n" .
+ # shortlog: format_author_html('td', \%co, 10)
+ format_author_html('td', \%co, 15, 3) . "";
+ # originally git_history used chop_str($co{'title'}, 50)
+ print format_subject_html($co{'title'}, $co{'title_short'},
+ href(action=>"commit", hash=>$commit), $ref);
+ print " \n" .
+ "" .
+ $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
+ $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
+
+ if ($ftype eq 'blob') {
+ print " | " .
+ $cgi->a({-href => href(action=>"blob_plain", hash_base=>$commit, file_name=>$file_name)}, "raw");
+
+ my $blob_current = $file_hash;
+ my $blob_parent = git_get_hash_by_path($commit, $file_name);
+ if (defined $blob_current && defined $blob_parent &&
+ $blob_current ne $blob_parent) {
+ print " | " .
+ $cgi->a({-href => href(action=>"blobdiff",
+ hash=>$blob_current, hash_parent=>$blob_parent,
+ hash_base=>$hash_base, hash_parent_base=>$commit,
+ file_name=>$file_name)},
+ "diff to current");
+ }
+ }
+ print " \n" .
+ " \n";
+ }
+ if (defined $extra) {
+ print "\n" .
+ "$extra \n" .
+ " \n";
+ }
+ print "
\n";
+}
+
+sub git_tags_body {
+ # uses global variable $project
+ my ($taglist, $from, $to, $extra) = @_;
+ $from = 0 unless defined $from;
+ $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
+
+ print "\n";
+}
+
+sub git_heads_body {
+ # uses global variable $project
+ my ($headlist, $head_at, $from, $to, $extra) = @_;
+ $from = 0 unless defined $from;
+ $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
+
+ print "\n";
+ my $alternate = 1;
+ for (my $i = $from; $i <= $to; $i++) {
+ my $entry = $headlist->[$i];
+ my %ref = %$entry;
+ my $curr = defined $head_at && $ref{'id'} eq $head_at;
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ print "$ref{'age'} \n" .
+ ($curr ? "" : " ") .
+ $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
+ -class => "list name"},esc_html($ref{'name'})) .
+ " \n" .
+ "" .
+ $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
+ $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
+ $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
+ " \n" .
+ " ";
+ }
+ if (defined $extra) {
+ print "\n" .
+ "$extra \n" .
+ " \n";
+ }
+ print "
\n";
+}
+
+# Display a single remote block
+sub git_remote_block {
+ my ($remote, $rdata, $limit, $head) = @_;
+
+ my $heads = $rdata->{'heads'};
+ my $fetch = $rdata->{'fetch'};
+ my $push = $rdata->{'push'};
+
+ my $urls_table = "\n" ;
+
+ if (defined $fetch) {
+ if ($fetch eq $push) {
+ $urls_table .= format_repo_url("URL", $fetch);
+ } else {
+ $urls_table .= format_repo_url("Fetch URL", $fetch);
+ $urls_table .= format_repo_url("Push URL", $push) if defined $push;
+ }
+ } elsif (defined $push) {
+ $urls_table .= format_repo_url("Push URL", $push);
+ } else {
+ $urls_table .= format_repo_url("", "No remote URL");
+ }
+
+ $urls_table .= "
\n";
+
+ my $dots;
+ if (defined $limit && $limit < @$heads) {
+ $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
+ }
+
+ print $urls_table;
+ git_heads_body($heads, $head, 0, $limit, $dots);
+}
+
+# Display a list of remote names with the respective fetch and push URLs
+sub git_remotes_list {
+ my ($remotedata, $limit) = @_;
+ print "\n";
+ my $alternate = 1;
+ my @remotes = sort keys %$remotedata;
+
+ my $limited = $limit && $limit < @remotes;
+
+ $#remotes = $limit - 1 if $limited;
+
+ while (my $remote = shift @remotes) {
+ my $rdata = $remotedata->{$remote};
+ my $fetch = $rdata->{'fetch'};
+ my $push = $rdata->{'push'};
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ print "" .
+ $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
+ -class=> "list name"},esc_html($remote)) .
+ " ";
+ print "" .
+ (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
+ " | " .
+ (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
+ " ";
+
+ print " \n";
+ }
+
+ if ($limited) {
+ print "\n" .
+ "" .
+ $cgi->a({-href => href(action=>"remotes")}, "...") .
+ " \n" . " \n";
+ }
+
+ print "
";
+}
+
+# Display remote heads grouped by remote, unless there are too many
+# remotes, in which case we only display the remote names
+sub git_remotes_body {
+ my ($remotedata, $limit, $head) = @_;
+ if ($limit and $limit < keys %$remotedata) {
+ git_remotes_list($remotedata, $limit);
+ } else {
+ fill_remote_heads($remotedata);
+ while (my ($remote, $rdata) = each %$remotedata) {
+ git_print_section({-class=>"remote", -id=>$remote},
+ ["remotes", $remote, $remote], sub {
+ git_remote_block($remote, $rdata, $limit, $head);
+ });
+ }
+ }
+}
+
+sub git_search_message {
+ my %co = @_;
+
+ my $greptype;
+ if ($searchtype eq 'commit') {
+ $greptype = "--grep=";
+ } elsif ($searchtype eq 'author') {
+ $greptype = "--author=";
+ } elsif ($searchtype eq 'committer') {
+ $greptype = "--committer=";
+ }
+ $greptype .= $searchtext;
+ my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
+ $greptype, '--regexp-ignore-case',
+ $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
+
+ my $paging_nav = '';
+ if ($page > 0) {
+ $paging_nav .=
+ $cgi->a({-href => href(-replay=>1, page=>undef)},
+ "first") .
+ " ⋅ " .
+ $cgi->a({-href => href(-replay=>1, page=>$page-1),
+ -accesskey => "p", -title => "Alt-p"}, "prev");
+ } else {
+ $paging_nav .= "first ⋅ prev";
+ }
+ my $next_link = '';
+ if ($#commitlist >= 100) {
+ $next_link =
+ $cgi->a({-href => href(-replay=>1, page=>$page+1),
+ -accesskey => "n", -title => "Alt-n"}, "next");
+ $paging_nav .= " ⋅ $next_link";
+ } else {
+ $paging_nav .= " ⋅ next";
+ }
+
+ git_header_html();
+
+ git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash);
+ if ($page == 0 && !@commitlist) {
+ print "No match.
\n";
+ } else {
+ git_search_grep_body(\@commitlist, 0, 99, $next_link);
+ }
+
+ git_footer_html();
+}
+
+sub git_search_changes {
+ my %co = @_;
+
+ local $/ = "\n";
+ open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
+ '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
+ ($search_use_regexp ? '--pickaxe-regex' : ())
+ or die_error(500, "Open git-log failed");
+
+ git_header_html();
+
+ git_print_page_nav('','', $hash,$co{'tree'},$hash);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+ print "\n";
+ my $alternate = 1;
+ undef %co;
+ my @files;
+ while (my $line = <$fd>) {
+ chomp $line;
+ next unless $line;
+
+ my %set = parse_difftree_raw_line($line);
+ if (defined $set{'commit'}) {
+ # finish previous commit
+ if (%co) {
+ print "\n" .
+ "" .
+ $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
+ "commit") .
+ " | " .
+ $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
+ hash_base=>$co{'id'})},
+ "tree") .
+ " \n" .
+ "\n";
+ }
+
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ %co = parse_commit($set{'commit'});
+ my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
+ print "$co{'age_string_date'} \n" .
+ "$author \n" .
+ "" .
+ $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
+ -class => "list subject"},
+ chop_and_escape_str($co{'title'}, 50) . " ");
+ } elsif (defined $set{'to_id'}) {
+ next if is_deleted(\%set);
+
+ print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
+ hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
+ -class => "list"},
+ "" . esc_path($set{'file'}) . " ") .
+ " \n";
+ }
+ }
+ close $fd;
+
+ # finish last commit (warning: repetition!)
+ if (%co) {
+ print " \n" .
+ "" .
+ $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
+ "commit") .
+ " | " .
+ $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
+ hash_base=>$co{'id'})},
+ "tree") .
+ " \n" .
+ " \n";
+ }
+
+ print "
\n";
+
+ git_footer_html();
+}
+
+sub git_search_files {
+ my %co = @_;
+
+ local $/ = "\n";
+ open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
+ $search_use_regexp ? ('-E', '-i') : '-F',
+ $searchtext, $co{'tree'}
+ or die_error(500, "Open git-grep failed");
+
+ git_header_html();
+
+ git_print_page_nav('','', $hash,$co{'tree'},$hash);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash);
+
+ print "\n";
+ my $alternate = 1;
+ my $matches = 0;
+ my $lastfile = '';
+ my $file_href;
+ 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 {
+ ($file, $lno, $ltext) = split(/\0/, $line, 3);
+ $file =~ s/^$co{'tree'}://;
+ }
+ if ($file ne $lastfile) {
+ $lastfile and print "\n";
+ if ($alternate++) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $file_href = href(action=>"blob", hash_base=>$co{'id'},
+ file_name=>$file);
+ print "".
+ $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
+ print " \n";
+ $lastfile = $file;
+ }
+ if ($binary) {
+ print "Binary file
\n";
+ } else {
+ $ltext = untabify($ltext);
+ if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
+ $ltext = esc_html($1, -nbsp=>1);
+ $ltext .= '';
+ $ltext .= esc_html($2, -nbsp=>1);
+ $ltext .= ' ';
+ $ltext .= esc_html($3, -nbsp=>1);
+ } else {
+ $ltext = esc_html($ltext, -nbsp=>1);
+ }
+ print "" .
+ $cgi->a({-href => $file_href.'#l'.$lno,
+ -class => "linenr"}, sprintf('%4i', $lno)) .
+ ' ' . $ltext . "
\n";
+ }
+ }
+ if ($lastfile) {
+ print " \n";
+ if ($matches > 1000) {
+ print "Too many matches, listing trimmed
\n";
+ }
+ } else {
+ print "No matches found
\n";
+ }
+ close $fd;
+
+ print "
\n";
+
+ git_footer_html();
+}
+
+sub git_search_grep_body {
+ my ($commitlist, $from, $to, $extra) = @_;
+ $from = 0 unless defined $from;
+ $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
+
+ print "\n";
+ my $alternate = 1;
+ for (my $i = $from; $i <= $to; $i++) {
+ my %co = %{$commitlist->[$i]};
+ if (!%co) {
+ next;
+ }
+ my $commit = $co{'id'};
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+ print "$co{'age_string_date'} \n" .
+ format_author_html('td', \%co, 15, 5) .
+ "" .
+ $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
+ -class => "list subject"},
+ chop_and_escape_str($co{'title'}, 50) . " ");
+ my $comment = $co{'comment'};
+ foreach my $line (@$comment) {
+ if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
+ my ($lead, $match, $trail) = ($1, $2, $3);
+ $match = chop_str($match, 70, 5, 'center');
+ my $contextlen = int((80 - length($match))/2);
+ $contextlen = 30 if ($contextlen > 30);
+ $lead = chop_str($lead, $contextlen, 10, 'left');
+ $trail = chop_str($trail, $contextlen, 10, 'right');
+
+ $lead = esc_html($lead);
+ $match = esc_html($match);
+ $trail = esc_html($trail);
+
+ print "$lead$match $trail ";
+ }
+ }
+ print " \n" .
+ "" .
+ $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
+ " | " .
+ $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
+ " | " .
+ $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
+ print " \n" .
+ " \n";
+ }
+ if (defined $extra) {
+ print "\n" .
+ "$extra \n" .
+ " \n";
+ }
+ print "
\n";
+}
+
+## ======================================================================
+## ======================================================================
+## actions
+
+sub git_project_list {
+ my $order = $input_params{'order'};
+ if (defined $order && $order !~ m/none|project|descr|owner|age/) {
+ die_error(400, "Unknown order parameter");
+ }
+
+ my @list = git_get_projects_list($project_filter, $strict_export);
+ if (!@list) {
+ die_error(404, "No projects found");
+ }
+
+ git_header_html();
+ if (defined $home_text && -f $home_text) {
+ print "\n";
+ insert_file($home_text);
+ print "
\n";
+ }
+
+ git_project_search_form($searchtext, $search_use_regexp);
+ git_project_list_body(\@list, $order);
+ git_footer_html();
+}
+
+sub git_forks {
+ my $order = $input_params{'order'};
+ if (defined $order && $order !~ m/none|project|descr|owner|age/) {
+ die_error(400, "Unknown order parameter");
+ }
+
+ my $filter = $project;
+ $filter =~ s/\.git$//;
+ my @list = git_get_projects_list($filter);
+ if (!@list) {
+ die_error(404, "No forks found");
+ }
+
+ git_header_html();
+ git_print_page_nav('','');
+ git_print_header_div('summary', "$project forks");
+ git_project_list_body(\@list, $order);
+ git_footer_html();
+}
+
+sub git_project_index {
+ my @projects = git_get_projects_list($project_filter, $strict_export);
+ if (!@projects) {
+ die_error(404, "No projects found");
+ }
+
+ 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'} = git_get_project_owner("$pr->{'path'}");
+ }
+
+ 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 %co = parse_commit("HEAD");
+ my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
+ my $head = $co{'id'};
+ my $remote_heads = gitweb_check_feature('remote_heads');
+
+ my $owner = git_get_project_owner($project);
+
+ my $refs = git_get_references();
+ # These get_*_list functions return one more to allow us to see if
+ # there are more ...
+ my @taglist = git_get_tags_list(16);
+ my @headlist = git_get_heads_list(16);
+ my %remotedata = $remote_heads ? git_get_remotes_list() : ();
+ my @forklist;
+ my $check_forks = gitweb_check_feature('forks');
+
+ if ($check_forks) {
+ # find forks of a project
+ my $filter = $project;
+ $filter =~ s/\.git$//;
+ @forklist = git_get_projects_list($filter);
+ # filter out forks of forks
+ @forklist = filter_forks_from_projects_list(\@forklist)
+ if (@forklist);
+ }
+
+ git_header_html();
+ git_print_page_nav('summary','', $head);
+
+ print "
\n";
+ print "\n" .
+ "description " . esc_html($descr) . " \n";
+ if ($owner and not $omit_owner) {
+ print "owner " . esc_html($owner) . " \n";
+ }
+ if (defined $cd{'rfc2822'}) {
+ print "last change " .
+ "".format_timestamp_html(\%cd)." \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";
+ my @url_list = git_get_project_url_list($project);
+ @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
+ foreach my $git_url (@url_list) {
+ next unless $git_url;
+ print format_repo_url($url_tag, $git_url);
+ $url_tag = "";
+ }
+
+ # Tag cloud
+ my $show_ctags = gitweb_check_feature('ctags');
+ if ($show_ctags) {
+ my $ctags = git_get_project_ctags($project);
+ if (%$ctags) {
+ # without ability to add tags, don't show if there are none
+ my $cloud = git_populate_project_tagcloud($ctags);
+ print "" .
+ "content tags " .
+ "".git_show_project_tagcloud($cloud, 48)." " .
+ " \n";
+ }
+ }
+
+ print "
\n";
+
+ # If XSS prevention is on, we don't include README.html.
+ # TODO: Allow a readme in some safe format.
+ if (!$prevent_xss && -s "$projectroot/$project/README.html") {
+ print "readme
\n" .
+ "\n";
+ insert_file("$projectroot/$project/README.html");
+ print "\n
\n"; # class="readme"
+ }
+
+ # we need to request one more than 16 (0..15) to check if
+ # those 16 are all
+ 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');
+ git_tags_body(\@taglist, 0, 15,
+ $#taglist <= 15 ? undef :
+ $cgi->a({-href => href(action=>"tags")}, "..."));
+ }
+
+ if (@headlist) {
+ git_print_header_div('heads');
+ git_heads_body(\@headlist, $head, 0, 15,
+ $#headlist <= 15 ? undef :
+ $cgi->a({-href => href(action=>"heads")}, "..."));
+ }
+
+ if (%remotedata) {
+ git_print_header_div('remotes');
+ git_remotes_body(\%remotedata, 15, $head);
+ }
+
+ if (@forklist) {
+ git_print_header_div('forks');
+ git_project_list_body(\@forklist, 'age', 0, 15,
+ $#forklist <= 15 ? undef :
+ $cgi->a({-href => href(action=>"forks")}, "..."),
+ 'no_header');
+ }
+
+ git_footer_html();
+}
+
+sub git_tag {
+ my %tag = parse_tag($hash);
+
+ if (! %tag) {
+ die_error(404, "Unknown tag object");
+ }
+
+ my $head = git_get_head_hash($project);
+ git_header_html();
+ git_print_page_nav('','', $head,undef,$head);
+ git_print_header_div('commit', esc_html($tag{'name'}), $hash);
+ print "\n" .
+ "\n\n" .
+ "
\n";
+ print "";
+ my $comment = $tag{'comment'};
+ foreach my $line (@$comment) {
+ chomp $line;
+ print esc_html($line, -nbsp=>1) . " \n";
+ }
+ print "
\n";
+ git_footer_html();
+}
+
+sub git_blame_common {
+ my $format = shift || 'porcelain';
+ if ($format eq 'porcelain' && $input_params{'javascript'}) {
+ $format = 'incremental';
+ $action = 'blame_incremental'; # for page title etc
+ }
+
+ # permissions
+ gitweb_check_feature('blame')
+ or die_error(403, "Blame view not allowed");
+
+ # error checking
+ die_error(400, "No file name given") unless $file_name;
+ $hash_base ||= git_get_head_hash($project);
+ die_error(404, "Couldn't find base commit") unless $hash_base;
+ my %co = parse_commit($hash_base)
+ or die_error(404, "Commit not found");
+ my $ftype = "blob";
+ if (!defined $hash) {
+ $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
+ or die_error(404, "Error looking up file");
+ } else {
+ $ftype = git_get_type($hash);
+ if ($ftype !~ "blob") {
+ die_error(400, "Object is not a blob");
+ }
+ }
+
+ my $fd;
+ if ($format eq 'incremental') {
+ # get file contents (as base)
+ open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
+ or die_error(500, "Open git-cat-file failed");
+ } elsif ($format eq 'data') {
+ # run git-blame --incremental
+ open $fd, "-|", git_cmd(), "blame", "--incremental",
+ $hash_base, "--", $file_name
+ or die_error(500, "Open git-blame --incremental failed");
+ } else {
+ # run git-blame --porcelain
+ open $fd, "-|", git_cmd(), "blame", '-p',
+ $hash_base, '--', $file_name
+ or die_error(500, "Open git-blame --porcelain failed");
+ }
+ binmode $fd, ':utf8';
+
+ # incremental blame data returns early
+ if ($format eq 'data') {
+ print $cgi->header(
+ -type=>"text/plain", -charset => "utf-8",
+ -status=> "200 OK");
+ local $| = 1; # output autoflush
+ while (my $line = <$fd>) {
+ print to_utf8($line);
+ }
+ close $fd
+ or print "ERROR $!\n";
+
+ print 'END';
+ if (defined $t0 && gitweb_check_feature('timed')) {
+ print ' '.
+ tv_interval($t0, [ gettimeofday() ]).
+ ' '.$number_of_git_cmds;
+ }
+ print "\n";
+
+ return;
+ }
+
+ # page header
+ git_header_html();
+ my $formats_nav =
+ $cgi->a({-href => href(action=>"blob", -replay=>1)},
+ "blob") .
+ " | ";
+ if ($format eq 'incremental') {
+ $formats_nav .=
+ $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
+ "blame") . " (non-incremental)";
+ } else {
+ $formats_nav .=
+ $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
+ "blame") . " (incremental)";
+ }
+ $formats_nav .=
+ " | " .
+ $cgi->a({-href => href(action=>"history", -replay=>1)},
+ "history") .
+ " | " .
+ $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
+ "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);
+
+ # page body
+ if ($format eq 'incremental') {
+ print "\n
\n".
+ "This page requires JavaScript to run.\n Use ".
+ $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
+ 'this page').
+ " instead.\n".
+ " \n \n";
+
+ print qq!
\n!;
+ }
+
+ print qq!\n!;
+ print qq!
... / ...
\n!
+ if ($format eq 'incremental');
+ print qq!
\n!.
+ #qq! \n!.
+ qq!\n!.
+ qq!Commit Line Data \n!.
+ qq! \n!.
+ qq!\n!;
+
+ my @rev_color = qw(light dark);
+ my $num_colors = scalar(@rev_color);
+ my $current_color = 0;
+
+ if ($format eq 'incremental') {
+ my $color_class = $rev_color[$current_color];
+
+ #contents of a file
+ my $linenr = 0;
+ LINE:
+ while (my $line = <$fd>) {
+ chomp $line;
+ $linenr++;
+
+ print qq!!.
+ qq! !.
+ qq!!.
+ qq!$linenr !;
+ print qq!! . esc_html($line) . " \n";
+ print qq! \n!;
+ }
+
+ } else { # porcelain, i.e. ordinary blame
+ my %metainfo = (); # saves information about commits
+
+ # blame data
+ LINE:
+ while (my $line = <$fd>) {
+ chomp $line;
+ # the header: []
+ # no for subsequent lines in group of lines
+ my ($full_rev, $orig_lineno, $lineno, $group_size) =
+ ($line =~ /^($oid_regex) (\d+) (\d+)(?: (\d+))?$/);
+ if (!exists $metainfo{$full_rev}) {
+ $metainfo{$full_rev} = { 'nprevious' => 0 };
+ }
+ my $meta = $metainfo{$full_rev};
+ my $data;
+ while ($data = <$fd>) {
+ chomp $data;
+ last if ($data =~ s/^\t//); # contents of line
+ if ($data =~ /^(\S+)(?: (.*))?$/) {
+ $meta->{$1} = $2 unless exists $meta->{$1};
+ }
+ if ($data =~ /^previous /) {
+ $meta->{'nprevious'}++;
+ }
+ }
+ my $short_rev = substr($full_rev, 0, 8);
+ my $author = $meta->{'author'};
+ my %date =
+ parse_date($meta->{'author-time'}, $meta->{'author-tz'});
+ my $date = $date{'iso-tz'};
+ if ($group_size) {
+ $current_color = ($current_color + 1) % $num_colors;
+ }
+ my $tr_class = $rev_color[$current_color];
+ $tr_class .= ' boundary' if (exists $meta->{'boundary'});
+ $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
+ $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
+ print "\n";
+ if ($group_size) {
+ print " 1);
+ print ">";
+ print $cgi->a({-href => href(action=>"commit",
+ hash=>$full_rev,
+ file_name=>$file_name)},
+ esc_html($short_rev));
+ if ($group_size >= 2) {
+ my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
+ if (@author_initials) {
+ print " " .
+ esc_html(join('', @author_initials));
+ # or join('.', ...)
+ }
+ }
+ print " \n";
+ }
+ # 'previous'
+ if (exists $meta->{'previous'} &&
+ $meta->{'previous'} =~ /^($oid_regex) (.*)$/) {
+ $meta->{'parent'} = $1;
+ $meta->{'file_parent'} = unquote($2);
+ }
+ my $linenr_commit =
+ exists($meta->{'parent'}) ?
+ $meta->{'parent'} : $full_rev;
+ my $linenr_filename =
+ exists($meta->{'file_parent'}) ?
+ $meta->{'file_parent'} : unquote($meta->{'filename'});
+ my $blamed = href(action => 'blame',
+ file_name => $linenr_filename,
+ hash_base => $linenr_commit);
+ print "";
+ print $cgi->a({ -href => "$blamed#l$orig_lineno",
+ -class => "linenr" },
+ esc_html($lineno));
+ print " ";
+ print "" . esc_html($data) . " \n";
+ print " \n";
+ } # end while
+
+ }
+
+ # footer
+ print " \n".
+ "
\n"; # class="blame"
+ print "
\n"; # class="blame_body"
+ close $fd
+ or print "Reading blob failed\n";
+
+ git_footer_html();
+}
+
+sub git_blame {
+ git_blame_common();
+}
+
+sub git_blame_incremental {
+ git_blame_common('incremental');
+}
+
+sub git_blame_data {
+ git_blame_common('data');
+}
+
+sub git_tags {
+ my $head = git_get_head_hash($project);
+ git_header_html();
+ git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
+ git_print_header_div('summary', $project);
+
+ my @tagslist = git_get_tags_list();
+ if (@tagslist) {
+ git_tags_body(\@tagslist);
+ }
+ git_footer_html();
+}
+
+sub git_heads {
+ my $head = git_get_head_hash($project);
+ git_header_html();
+ git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
+ git_print_header_div('summary', $project);
+
+ my @headslist = git_get_heads_list();
+ if (@headslist) {
+ git_heads_body(\@headslist, $head);
+ }
+ git_footer_html();
+}
+
+# used both for single remote view and for list of all the remotes
+sub git_remotes {
+ gitweb_check_feature('remote_heads')
+ or die_error(403, "Remote heads view is disabled");
+
+ my $head = git_get_head_hash($project);
+ my $remote = $input_params{'hash'};
+
+ my $remotedata = git_get_remotes_list($remote);
+ die_error(500, "Unable to get remote information") unless defined $remotedata;
+
+ unless (%$remotedata) {
+ die_error(404, defined $remote ?
+ "Remote $remote not found" :
+ "No remotes found");
+ }
+
+ git_header_html(undef, undef, -action_extra => $remote);
+ git_print_page_nav('', '', $head, undef, $head,
+ format_ref_views($remote ? '' : 'remotes'));
+
+ fill_remote_heads($remotedata);
+ if (defined $remote) {
+ git_print_header_div('remotes', "$remote remote for $project");
+ git_remote_block($remote, $remotedata->{$remote}, undef, $head);
+ } else {
+ git_print_header_div('summary', "$project remotes");
+ git_remotes_body($remotedata, undef, $head);
+ }
+
+ git_footer_html();
+}
+
+sub git_blob_plain {
+ my $type = shift;
+ my $expires;
+
+ if (!defined $hash) {
+ if (defined $file_name) {
+ my $base = $hash_base || git_get_head_hash($project);
+ $hash = git_get_hash_by_path($base, $file_name, "blob")
+ or die_error(404, "Cannot find file");
+ } else {
+ die_error(400, "No file name defined");
+ }
+ } elsif ($hash =~ m/^$oid_regex$/) {
+ # blobs defined by non-textual hash id's can be cached
+ $expires = "+1d";
+ }
+
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ or die_error(500, "Open git-cat-file blob '$hash' failed");
+
+ # content-type (can include charset)
+ $type = blob_contenttype($fd, $file_name, $type);
+
+ # "save as" filename, even when no $file_name is given
+ my $save_as = "$hash";
+ if (defined $file_name) {
+ $save_as = $file_name;
+ } elsif ($type =~ m/^text\//) {
+ $save_as .= '.txt';
+ }
+
+ # With XSS prevention on, blobs of all types except a few known safe
+ # ones are served with "Content-Disposition: attachment" to make sure
+ # they don't run in our security domain. For certain image types,
+ # blob view writes an tag referring to blob_plain view, and we
+ # want to be sure not to break that by serving the image as an
+ # attachment (though Firefox 3 doesn't seem to care).
+ my $sandbox = $prevent_xss &&
+ $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
+
+ # serve text/* as text/plain
+ if ($prevent_xss &&
+ ($type =~ m!^text/[a-z]+\b(.*)$! ||
+ ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
+ my $rest = $1;
+ $rest = defined $rest ? $rest : '';
+ $type = "text/plain$rest";
+ }
+
+ print $cgi->header(
+ -type => $type,
+ -expires => $expires,
+ -content_disposition =>
+ ($sandbox ? 'attachment' : 'inline')
+ . '; filename="' . $save_as . '"');
+ local $/ = undef;
+ local *FCGI::Stream::PRINT = $FCGI_Stream_PRINT_raw;
+ binmode STDOUT, ':raw';
+ print <$fd>;
+ binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
+ close $fd;
+}
+
+sub git_blob {
+ my $expires;
+
+ if (!defined $hash) {
+ if (defined $file_name) {
+ my $base = $hash_base || git_get_head_hash($project);
+ $hash = git_get_hash_by_path($base, $file_name, "blob")
+ or die_error(404, "Cannot find file");
+ } else {
+ die_error(400, "No file name defined");
+ }
+ } elsif ($hash =~ m/^$oid_regex$/) {
+ # blobs defined by non-textual hash id's can be cached
+ $expires = "+1d";
+ }
+
+ my $have_blame = gitweb_check_feature('blame');
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ or die_error(500, "Couldn't cat $file_name, $hash");
+ my $mimetype = blob_mimetype($fd, $file_name);
+ # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
+ if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
+ close $fd;
+ return git_blob_plain($mimetype);
+ }
+ # we can have blame only for text/* mimetype
+ $have_blame &&= ($mimetype =~ m!^text/!);
+
+ my $highlight = gitweb_check_feature('highlight');
+ my $syntax = guess_file_syntax($highlight, $file_name);
+ $fd = run_highlighter($fd, $highlight, $syntax);
+
+ git_header_html(undef, $expires);
+ my $formats_nav = '';
+ if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+ if (defined $file_name) {
+ if ($have_blame) {
+ $formats_nav .=
+ $cgi->a({-href => href(action=>"blame", -replay=>1)},
+ "blame") .
+ " | ";
+ }
+ $formats_nav .=
+ $cgi->a({-href => href(action=>"history", -replay=>1)},
+ "history") .
+ " | " .
+ $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
+ "raw") .
+ " | " .
+ $cgi->a({-href => href(action=>"blob",
+ hash_base=>"HEAD", file_name=>$file_name)},
+ "HEAD");
+ } else {
+ $formats_nav .=
+ $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
+ "raw");
+ }
+ git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+ } else {
+ print "\n" .
+ "
\n" .
+ "".esc_html($hash)."
\n";
+ }
+ git_print_page_path($file_name, "blob", $hash_base);
+ print "\n";
+ if ($mimetype =~ m!^image/!) {
+ print qq!
$hash,
+ hash_base=>$hash_base, file_name=>$file_name)) .
+ qq!" />\n!;
+ } else {
+ my $nr;
+ while (my $line = <$fd>) {
+ chomp $line;
+ $nr++;
+ $line = untabify($line);
+ printf qq!
\n!,
+ $nr, esc_attr(href(-replay => 1)), $nr, $nr,
+ $highlight ? sanitize($line) : esc_html($line, -nbsp=>1);
+ }
+ }
+ close $fd
+ or print "Reading blob failed.\n";
+ print "
";
+ git_footer_html();
+}
+
+sub git_tree {
+ if (!defined $hash_base) {
+ $hash_base = "HEAD";
+ }
+ if (!defined $hash) {
+ if (defined $file_name) {
+ $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
+ } else {
+ $hash = $hash_base;
+ }
+ }
+ die_error(404, "No such tree") unless defined($hash);
+
+ my $show_sizes = gitweb_check_feature('show-sizes');
+ my $have_blame = gitweb_check_feature('blame');
+
+ my @entries = ();
+ {
+ local $/ = "\0";
+ open my $fd, "-|", git_cmd(), "ls-tree", '-z',
+ ($show_sizes ? '-l' : ()), @extra_options, $hash
+ or die_error(500, "Open git-ls-tree failed");
+ @entries = map { chomp; $_ } <$fd>;
+ close $fd
+ or die_error(404, "Reading tree failed");
+ }
+
+ my $refs = git_get_references();
+ my $ref = format_ref_marker($refs, $hash_base);
+ git_header_html();
+ my $basedir = '';
+ if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+ my @views_nav = ();
+ if (defined $file_name) {
+ push @views_nav,
+ $cgi->a({-href => href(action=>"history", -replay=>1)},
+ "history"),
+ $cgi->a({-href => href(action=>"tree",
+ hash_base=>"HEAD", file_name=>$file_name)},
+ "HEAD"),
+ }
+ my $snapshot_links = format_snapshot_links($hash);
+ if (defined $snapshot_links) {
+ # FIXME: Should be available when we have no hash base as well.
+ push @views_nav, $snapshot_links;
+ }
+ git_print_page_nav('tree','', $hash_base, undef, undef,
+ join(' | ', @views_nav));
+ git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
+ } else {
+ undef $hash_base;
+ print "\n";
+ print "
\n";
+ print "".esc_html($hash)."
\n";
+ }
+ if (defined $file_name) {
+ $basedir = $file_name;
+ if ($basedir ne '' && substr($basedir, -1) ne '/') {
+ $basedir .= '/';
+ }
+ git_print_page_path($file_name, 'tree', $hash_base);
+ }
+ print "\n";
+ print "
\n";
+ my $alternate = 1;
+ # '..' (top directory) link if possible
+ if (defined $hash_base &&
+ defined $file_name && $file_name =~ m![^/]+$!) {
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+
+ my $up = $file_name;
+ $up =~ s!/?[^/]+$!!;
+ undef $up unless $up;
+ # based on git_print_tree_entry
+ print '' . mode_str('040000') . " \n";
+ print ' '."\n" if $show_sizes;
+ print '';
+ print $cgi->a({-href => href(action=>"tree",
+ hash_base=>$hash_base,
+ file_name=>$up)},
+ "..");
+ print " \n";
+ print " \n";
+
+ print " \n";
+ }
+ foreach my $line (@entries) {
+ my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
+
+ if ($alternate) {
+ print "\n";
+ } else {
+ print " \n";
+ }
+ $alternate ^= 1;
+
+ git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
+
+ print " \n";
+ }
+ print "
\n" .
+ "
";
+ git_footer_html();
+}
+
+sub sanitize_for_filename {
+ my $name = shift;
+
+ $name =~ s!/!-!g;
+ $name =~ s/[^[:alnum:]_.-]//g;
+
+ return $name;
+}
+
+sub snapshot_name {
+ my ($project, $hash) = @_;
+
+ # path/to/project.git -> project
+ # path/to/project/.git -> project
+ my $name = to_utf8($project);
+ $name =~ s,([^/])/*\.git$,$1,;
+ $name = sanitize_for_filename(basename($name));
+
+ my $ver = $hash;
+ if ($hash =~ /^[0-9a-fA-F]+$/) {
+ # shorten SHA-1 hash
+ my $full_hash = git_get_full_hash($project, $hash);
+ if ($full_hash =~ /^$hash/ && length($hash) > 7) {
+ $ver = git_get_short_hash($project, $hash);
+ }
+ } elsif ($hash =~ m!^refs/tags/(.*)$!) {
+ # tags don't need shortened SHA-1 hash
+ $ver = $1;
+ } else {
+ # branches and other need shortened SHA-1 hash
+ my $strip_refs = join '|', map { quotemeta } get_branch_refs();
+ if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
+ my $ref_dir = (defined $1) ? $1 : '';
+ $ver = $2;
+
+ $ref_dir = sanitize_for_filename($ref_dir);
+ # for refs neither in heads nor remotes we want to
+ # add a ref dir to archive name
+ if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
+ $ver = $ref_dir . '-' . $ver;
+ }
+ }
+ $ver .= '-' . git_get_short_hash($project, $hash);
+ }
+ # special case of sanitization for filename - we change
+ # slashes to dots instead of dashes
+ # in case of hierarchical branch names
+ $ver =~ s!/!.!g;
+ $ver =~ s/[^[:alnum:]_.-]//g;
+
+ # name = project-version_string
+ $name = "$name-$ver";
+
+ return wantarray ? ($name, $name) : $name;
+}
+
+sub exit_if_unmodified_since {
+ my ($latest_epoch) = @_;
+ our $cgi;
+
+ my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
+ if (defined $if_modified) {
+ my $since;
+ if (eval { require HTTP::Date; 1; }) {
+ $since = HTTP::Date::str2time($if_modified);
+ } elsif (eval { require Time::ParseDate; 1; }) {
+ $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
+ }
+ if (defined $since && $latest_epoch <= $since) {
+ my %latest_date = parse_date($latest_epoch);
+ print $cgi->header(
+ -last_modified => $latest_date{'rfc2822'},
+ -status => '304 Not Modified');
+ goto DONE_GITWEB;
+ }
+ }
+}
+
+sub git_snapshot {
+ my $format = $input_params{'snapshot_format'};
+ if (!@snapshot_fmts) {
+ die_error(403, "Snapshots not allowed");
+ }
+ # default to first supported snapshot format
+ $format ||= $snapshot_fmts[0];
+ if ($format !~ m/^[a-z0-9]+$/) {
+ die_error(400, "Invalid snapshot format parameter");
+ } elsif (!exists($known_snapshot_formats{$format})) {
+ die_error(400, "Unknown snapshot format");
+ } elsif ($known_snapshot_formats{$format}{'disabled'}) {
+ die_error(403, "Snapshot format not allowed");
+ } elsif (!grep($_ eq $format, @snapshot_fmts)) {
+ die_error(403, "Unsupported snapshot format");
+ }
+
+ my $type = git_get_type("$hash^{}");
+ if (!$type) {
+ die_error(404, 'Object does not exist');
+ } elsif ($type eq 'blob') {
+ die_error(400, 'Object is not a tree-ish');
+ }
+
+ my ($name, $prefix) = snapshot_name($project, $hash);
+ my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
+
+ my %co = parse_commit($hash);
+ exit_if_unmodified_since($co{'committer_epoch'}) if %co;
+
+ my $cmd = quote_command(
+ git_cmd(), 'archive',
+ "--format=$known_snapshot_formats{$format}{'format'}",
+ "--prefix=$prefix/", $hash);
+ if (exists $known_snapshot_formats{$format}{'compressor'}) {
+ $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
+ }
+
+ $filename =~ s/(["\\])/\\$1/g;
+ my %latest_date;
+ if (%co) {
+ %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+ }
+
+ print $cgi->header(
+ -type => $known_snapshot_formats{$format}{'type'},
+ -content_disposition => 'inline; filename="' . $filename . '"',
+ %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
+ -status => '200 OK');
+
+ open my $fd, "-|", $cmd
+ or die_error(500, "Execute git-archive failed");
+ local *FCGI::Stream::PRINT = $FCGI_Stream_PRINT_raw;
+ binmode STDOUT, ':raw';
+ print <$fd>;
+ binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
+ close $fd;
+}
+
+sub git_log_generic {
+ my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
+
+ my $head = git_get_head_hash($project);
+ if (!defined $base) {
+ $base = $head;
+ }
+ if (!defined $page) {
+ $page = 0;
+ }
+ my $refs = git_get_references();
+
+ my $commit_hash = $base;
+ if (defined $parent) {
+ $commit_hash = "$parent..$base";
+ }
+ my @commitlist =
+ parse_commits($commit_hash, 101, (100 * $page),
+ defined $file_name ? ($file_name, "--full-history") : ());
+
+ my $ftype;
+ if (!defined $file_hash && defined $file_name) {
+ # some commits could have deleted file in question,
+ # and not have it in tree, but one of them has to have it
+ for (my $i = 0; $i < @commitlist; $i++) {
+ $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
+ last if defined $file_hash;
+ }
+ }
+ if (defined $file_hash) {
+ $ftype = git_get_type($file_hash);
+ }
+ if (defined $file_name && !defined $ftype) {
+ die_error(500, "Unknown type of object");
+ }
+ my %co;
+ if (defined $file_name) {
+ %co = parse_commit($base)
+ or die_error(404, "Unknown commit object");
+ }
+
+
+ my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
+ my $next_link = '';
+ if ($#commitlist >= 100) {
+ $next_link =
+ $cgi->a({-href => href(-replay=>1, page=>$page+1),
+ -accesskey => "n", -title => "Alt-n"}, "next");
+ }
+ my $patch_max = gitweb_get_feature('patches');
+ if ($patch_max && !defined $file_name &&
+ !gitweb_check_feature('email-privacy')) {
+ if ($patch_max < 0 || @commitlist <= $patch_max) {
+ $paging_nav .= " ⋅ " .
+ $cgi->a({-href => href(action=>"patches", -replay=>1)},
+ "patches");
+ }
+ }
+
+ git_header_html();
+ git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
+ if (defined $file_name) {
+ git_print_header_div('commit', esc_html($co{'title'}), $base);
+ } else {
+ git_print_header_div('summary', $project)
+ }
+ git_print_page_path($file_name, $ftype, $hash_base)
+ if (defined $file_name);
+
+ $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
+ $file_name, $file_hash, $ftype);
+
+ git_footer_html();
+}
+
+sub git_log {
+ git_log_generic('log', \&git_log_body,
+ $hash, $hash_parent);
+}
+
+sub git_commit {
+ $hash ||= $hash_base || "HEAD";
+ my %co = parse_commit($hash)
+ or die_error(404, "Unknown commit object");
+
+ my $parent = $co{'parent'};
+ my $parents = $co{'parents'}; # listref
+
+ # we need to prepare $formats_nav before any parameter munging
+ my $formats_nav;
+ if (!defined $parent) {
+ # --root commitdiff
+ $formats_nav .= '(initial)';
+ } elsif (@$parents == 1) {
+ # single parent commit
+ $formats_nav .=
+ '(parent: ' .
+ $cgi->a({-href => href(action=>"commit",
+ hash=>$parent)},
+ esc_html(substr($parent, 0, 7))) .
+ ')';
+ } else {
+ # merge commit
+ $formats_nav .=
+ '(merge: ' .
+ join(' ', map {
+ $cgi->a({-href => href(action=>"commit",
+ hash=>$_)},
+ esc_html(substr($_, 0, 7)));
+ } @$parents ) .
+ ')';
+ }
+ if (gitweb_check_feature('patches') && @$parents <= 1 &&
+ !gitweb_check_feature('email-privacy')) {
+ $formats_nav .= " | " .
+ $cgi->a({-href => href(action=>"patch", -replay=>1)},
+ "patch");
+ }
+
+ if (!defined $parent) {
+ $parent = "--root";
+ }
+ my @difftree;
+ open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
+ @diff_opts,
+ (@$parents <= 1 ? $parent : '-c'),
+ $hash, "--"
+ or die_error(500, "Open git-diff-tree failed");
+ @difftree = map { chomp; $_ } <$fd>;
+ close $fd or die_error(404, "Reading git-diff-tree failed");
+
+ # non-textual hash id's can be cached
+ my $expires;
+ if ($hash =~ m/^$oid_regex$/) {
+ $expires = "+1d";
+ }
+ my $refs = git_get_references();
+ my $ref = format_ref_marker($refs, $co{'id'});
+
+ git_header_html(undef, $expires);
+ git_print_page_nav('commit', '',
+ $hash, $co{'tree'}, $hash,
+ $formats_nav);
+
+ if (defined $co{'parent'}) {
+ git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
+ } else {
+ git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
+ }
+ print "\n" .
+ "".
+ "
\n";
+
+ print "\n";
+ git_print_log($co{'comment'});
+ print "
\n";
+
+ git_difftree_body(\@difftree, $hash, @$parents);
+
+ git_footer_html();
+}
+
+sub git_object {
+ # object is defined by:
+ # - hash or hash_base alone
+ # - hash_base and file_name
+ my $type;
+
+ # - hash or hash_base alone
+ if ($hash || ($hash_base && !defined $file_name)) {
+ my $object_id = $hash || $hash_base;
+
+ open my $fd, "-|", quote_command(
+ git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
+ or die_error(404, "Object does not exist");
+ $type = <$fd>;
+ defined $type && chomp $type;
+ close $fd
+ or die_error(404, "Object does not exist");
+
+ # - hash_base and file_name
+ } elsif ($hash_base && defined $file_name) {
+ $file_name =~ s,/+$,,;
+
+ system(git_cmd(), "cat-file", '-e', $hash_base) == 0
+ or die_error(404, "Base object does not exist");
+
+ # here errors should not happen
+ open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
+ or die_error(500, "Open git-ls-tree failed");
+ my $line = <$fd>;
+ close $fd;
+
+ #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
+ unless ($line && $line =~ m/^([0-9]+) (.+) ($oid_regex)\t/) {
+ die_error(404, "File or directory for given base does not exist");
+ }
+ $type = $2;
+ $hash = $3;
+ } else {
+ die_error(400, "Not enough information to find object");
+ }
+
+ print $cgi->redirect(-uri => href(action=>$type, -full=>1,
+ hash=>$hash, hash_base=>$hash_base,
+ file_name=>$file_name),
+ -status => '302 Found');
+}
+
+sub git_blobdiff {
+ my $format = shift || 'html';
+ my $diff_style = $input_params{'diff_style'} || 'inline';
+
+ 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,
+ "--", (defined $file_parent ? $file_parent : ()), $file_name
+ or die_error(500, "Open git-diff-tree failed");
+ @difftree = map { chomp; $_ } <$fd>;
+ close $fd
+ or die_error(404, "Reading git-diff-tree failed");
+ @difftree
+ or die_error(404, "Blob diff not found");
+
+ } elsif (defined $hash &&
+ $hash =~ $oid_regex) {
+ # 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(500, "Open git-diff-tree failed");
+ @difftree =
+ # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
+ # $hash == to_id
+ grep { /^:[0-7]{6} [0-7]{6} $oid_regex $hash/ }
+ map { chomp; $_ } <$fd>;
+ close $fd
+ or die_error(404, "Reading git-diff-tree failed");
+ @difftree
+ or die_error(404, "Blob diff not found");
+
+ } else {
+ die_error(400, "Missing one of the blob diff parameters");
+ }
+
+ if (@difftree > 1) {
+ die_error(400, "Ambiguous blob diff specification");
+ }
+
+ %diffinfo = parse_difftree_raw_line($difftree[0]);
+ $file_parent ||= $diffinfo{'from_file'} || $file_name;
+ $file_name ||= $diffinfo{'to_file'};
+
+ $hash_parent ||= $diffinfo{'from_id'};
+ $hash ||= $diffinfo{'to_id'};
+
+ # non-textual hash id's can be cached
+ if ($hash_base =~ m/^$oid_regex$/ &&
+ $hash_parent_base =~ m/^$oid_regex$/) {
+ $expires = '+1d';
+ }
+
+ # open patch output
+ open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+ '-p', ($format eq 'html' ? "--full-index" : ()),
+ $hash_parent_base, $hash_base,
+ "--", (defined $file_parent ? $file_parent : ()), $file_name
+ or die_error(500, "Open git-diff-tree failed");
+ }
+
+ # old/legacy style URI -- not generated anymore since 1.4.3.
+ if (!%diffinfo) {
+ die_error('404 Not Found', "Missing one of the blob diff parameters")
+ }
+
+ # header
+ if ($format eq 'html') {
+ my $formats_nav =
+ $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
+ "raw");
+ $formats_nav .= diff_style_nav($diff_style);
+ git_header_html(undef, $expires);
+ if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+ git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
+ git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
+ } else {
+ print " $formats_nav
\n";
+ print "".esc_html("$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="' . "$file_name" . '.patch"');
+
+ print "X-Git-Url: " . $cgi->self_url() . "\n\n";
+
+ } else {
+ die_error(400, "Unknown blobdiff format");
+ }
+
+ # patch
+ if ($format eq 'html') {
+ print "\n";
+
+ git_patchset_body($fd, $diff_style,
+ [ \%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_path($diffinfo{'from_file'})!eg;
+ $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
+
+ print $line;
+
+ last if $line =~ m!^\+\+\+!;
+ }
+ local $/ = undef;
+ print <$fd>;
+ close $fd;
+ }
+}
+
+sub git_blobdiff_plain {
+ git_blobdiff('plain');
+}
+
+# assumes that it is added as later part of already existing navigation,
+# so it returns "| foo | bar" rather than just "foo | bar"
+sub diff_style_nav {
+ my ($diff_style, $is_combined) = @_;
+ $diff_style ||= 'inline';
+
+ return "" if ($is_combined);
+
+ my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
+ my %styles = @styles;
+ @styles =
+ @styles[ map { $_ * 2 } 0..$#styles/2 ];
+
+ return join '',
+ map { " | ".$_ }
+ map {
+ $_ eq $diff_style ? $styles{$_} :
+ $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
+ } @styles;
+}
+
+sub git_commitdiff {
+ my %params = @_;
+ my $format = $params{-format} || 'html';
+ my $diff_style = $input_params{'diff_style'} || 'inline';
+
+ my ($patch_max) = gitweb_get_feature('patches');
+ if ($format eq 'patch') {
+ die_error(403, "Patch view not allowed") unless $patch_max;
+ }
+
+ $hash ||= $hash_base || "HEAD";
+ my %co = parse_commit($hash)
+ or die_error(404, "Unknown commit object");
+
+ # 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 =
+ $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
+ "raw");
+ if ($patch_max && @{$co{'parents'}} <= 1 &&
+ !gitweb_check_feature('email-privacy')) {
+ $formats_nav .= " | " .
+ $cgi->a({-href => href(action=>"patch", -replay=>1)},
+ "patch");
+ }
+ $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
+
+ if (defined $hash_parent &&
+ $hash_parent ne '-c' && $hash_parent ne '--cc') {
+ # commitdiff with two commits given
+ my $hash_parent_short = $hash_parent;
+ if ($hash_parent =~ m/^$oid_regex$/) {
+ $hash_parent_short = substr($hash_parent, 0, 7);
+ }
+ $formats_nav .=
+ ' (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(-replay=>1,
+ hash=>$hash_parent, hash_base=>undef)},
+ esc_html($hash_parent_short)) .
+ ')';
+ } elsif (!$co{'parent'}) {
+ # --root commitdiff
+ $formats_nav .= ' (initial)';
+ } elsif (scalar @{$co{'parents'}} == 1) {
+ # single parent commit
+ $formats_nav .=
+ ' (parent: ' .
+ $cgi->a({-href => href(-replay=>1,
+ hash=>$co{'parent'}, hash_base=>undef)},
+ esc_html(substr($co{'parent'}, 0, 7))) .
+ ')';
+ } else {
+ # merge commit
+ if ($hash_parent eq '--cc') {
+ $formats_nav .= ' | ' .
+ $cgi->a({-href => href(-replay=>1,
+ hash=>$hash, hash_parent=>'-c')},
+ 'combined');
+ } else { # $hash_parent eq '-c'
+ $formats_nav .= ' | ' .
+ $cgi->a({-href => href(-replay=>1,
+ hash=>$hash, hash_parent=>'--cc')},
+ 'compact');
+ }
+ $formats_nav .=
+ ' (merge: ' .
+ join(' ', map {
+ $cgi->a({-href => href(-replay=>1,
+ hash=>$_, hash_base=>undef)},
+ esc_html(substr($_, 0, 7)));
+ } @{$co{'parents'}} ) .
+ ')';
+ }
+ }
+
+ my $hash_parent_param = $hash_parent;
+ if (!defined $hash_parent_param) {
+ # --cc for multiple parents, --root for parentless
+ $hash_parent_param =
+ @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
+ }
+
+ # read commitdiff
+ my $fd;
+ my @difftree;
+ if ($format eq 'html') {
+ open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+ "--no-commit-id", "--patch-with-raw", "--full-index",
+ $hash_parent_param, $hash, "--"
+ or die_error(500, "Open git-diff-tree failed");
+
+ while (my $line = <$fd>) {
+ chomp $line;
+ # empty line ends raw part of diff-tree output
+ last unless $line;
+ push @difftree, scalar parse_difftree_raw_line($line);
+ }
+
+ } elsif ($format eq 'plain') {
+ open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+ '-p', $hash_parent_param, $hash, "--"
+ or die_error(500, "Open git-diff-tree failed");
+ } elsif ($format eq 'patch') {
+ # For commit ranges, we limit the output to the number of
+ # patches specified in the 'patches' feature.
+ # For single commits, we limit the output to a single patch,
+ # diverging from the git-format-patch default.
+ my @commit_spec = ();
+ if ($hash_parent) {
+ if ($patch_max > 0) {
+ push @commit_spec, "-$patch_max";
+ }
+ push @commit_spec, '-n', "$hash_parent..$hash";
+ } else {
+ if ($params{-single}) {
+ push @commit_spec, '-1';
+ } else {
+ if ($patch_max > 0) {
+ push @commit_spec, "-$patch_max";
+ }
+ push @commit_spec, "-n";
+ }
+ push @commit_spec, '--root', $hash;
+ }
+ open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
+ '--encoding=utf8', '--stdout', @commit_spec
+ or die_error(500, "Open git-format-patch failed");
+ } else {
+ die_error(400, "Unknown commitdiff format");
+ }
+
+ # non-textual hash id's can be cached
+ my $expires;
+ if ($hash =~ m/^$oid_regex$/) {
+ $expires = "+1d";
+ }
+
+ # write commit message
+ if ($format eq 'html') {
+ my $refs = git_get_references();
+ my $ref = format_ref_marker($refs, $co{'id'});
+
+ 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);
+ print "\n" .
+ "".
+ "
\n";
+ print "\n";
+ if (@{$co{'comment'}} > 1) {
+ print "
\n";
+ git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
+ print "
\n"; # class="log"
+ }
+
+ } elsif ($format eq 'plain') {
+ my $refs = git_get_references("tags");
+ my $tagname = git_get_rev_name_tags($hash);
+ my $filename = basename($project) . "-$hash.patch";
+
+ print $cgi->header(
+ -type => 'text/plain',
+ -charset => 'utf-8',
+ -expires => $expires,
+ -content_disposition => 'inline; filename="' . "$filename" . '"');
+ my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
+ print "From: " . to_utf8($co{'author'}) . "\n";
+ print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
+ print "Subject: " . to_utf8($co{'title'}) . "\n";
+
+ print "X-Git-Tag: $tagname\n" if $tagname;
+ print "X-Git-Url: " . $cgi->self_url() . "\n\n";
+
+ foreach my $line (@{$co{'comment'}}) {
+ print to_utf8($line) . "\n";
+ }
+ print "---\n\n";
+ } elsif ($format eq 'patch') {
+ my $filename = basename($project) . "-$hash.patch";
+
+ print $cgi->header(
+ -type => 'text/plain',
+ -charset => 'utf-8',
+ -expires => $expires,
+ -content_disposition => 'inline; filename="' . "$filename" . '"');
+ }
+
+ # write patch
+ if ($format eq 'html') {
+ 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 "
\n";
+
+ git_patchset_body($fd, $diff_style,
+ \@difftree, $hash,
+ $use_parents ? @{$co{'parents'}} : $hash_parent);
+ close $fd;
+ print "
\n"; # class="page_body"
+ git_footer_html();
+
+ } elsif ($format eq 'plain') {
+ local $/ = undef;
+ print <$fd>;
+ close $fd
+ or print "Reading git-diff-tree failed\n";
+ } elsif ($format eq 'patch') {
+ local $/ = undef;
+ print <$fd>;
+ close $fd
+ or print "Reading git-format-patch failed\n";
+ }
+}
+
+sub git_commitdiff_plain {
+ git_commitdiff(-format => 'plain');
+}
+
+# format-patch-style patches
+sub git_patch {
+ git_commitdiff(-format => 'patch', -single => 1);
+}
+
+sub git_patches {
+ git_commitdiff(-format => 'patch');
+}
+
+sub git_history {
+ git_log_generic('history', \&git_history_body,
+ $hash_base, $hash_parent_base,
+ $file_name, $hash);
+}
+
+sub git_search {
+ $searchtype ||= 'commit';
+
+ # check if appropriate features are enabled
+ gitweb_check_feature('search')
+ or die_error(403, "Search is disabled");
+ if ($searchtype eq 'pickaxe') {
+ # pickaxe may take all resources of your box and run for several minutes
+ # with every query - so decide by yourself how public you make this feature
+ gitweb_check_feature('pickaxe')
+ or die_error(403, "Pickaxe search is disabled");
+ }
+ if ($searchtype eq 'grep') {
+ # grep search might be potentially CPU-intensive, too
+ gitweb_check_feature('grep')
+ or die_error(403, "Grep search is disabled");
+ }
+
+ if (!defined $searchtext) {
+ die_error(400, "Text field is empty");
+ }
+ if (!defined $hash) {
+ $hash = git_get_head_hash($project);
+ }
+ my %co = parse_commit($hash);
+ if (!%co) {
+ die_error(404, "Unknown commit object");
+ }
+ if (!defined $page) {
+ $page = 0;
+ }
+
+ if ($searchtype eq 'commit' ||
+ $searchtype eq 'author' ||
+ $searchtype eq 'committer') {
+ git_search_message(%co);
+ } elsif ($searchtype eq 'pickaxe') {
+ git_search_changes(%co);
+ } elsif ($searchtype eq 'grep') {
+ git_search_files(%co);
+ } else {
+ die_error(400, "Unknown search type");
+ }
+}
+
+sub git_search_help {
+ git_header_html();
+ git_print_page_nav('','', $hash,$hash,$hash);
+ print <Pattern is by default a normal string that is matched precisely (but without
+regard to case, except in the case of pickaxe). However, when you check the re checkbox,
+the pattern entered is recognized as the POSIX extended
+regular expression (also case
+insensitive).
+
+commit
+The commit messages and authorship information will be scanned for the given pattern.
+EOT
+ my $have_grep = gitweb_check_feature('grep');
+ if ($have_grep) {
+ print <grep
+All files in the currently selected tree (HEAD unless you are explicitly browsing
+ a different one) are searched for the given pattern. On large trees, this search can take
+a while and put some strain on the server, so please use it with some consideration. Note that
+due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
+case-sensitive.
+EOT
+ }
+ print <author
+Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.
+committer
+Name and e-mail of the committer and date of commit will be scanned for the given pattern.
+EOT
+ my $have_pickaxe = gitweb_check_feature('pickaxe');
+ if ($have_pickaxe) {
+ print <pickaxe
+All commits that caused the string to appear or disappear from any file (changes that
+added, removed or "modified" the string) will be listed. This search can take a while and
+takes a lot of strain on the server, so please use it wisely. Note that since you may be
+interested even in changes just changing the case as well, this search is case sensitive.
+EOT
+ }
+ print " \n";
+ git_footer_html();
+}
+
+sub git_shortlog {
+ git_log_generic('shortlog', \&git_shortlog_body,
+ $hash, $hash_parent);
+}
+
+## ......................................................................
+## feeds (RSS, Atom; OPML)
+
+sub git_feed {
+ my $format = shift || 'atom';
+ my $have_blame = gitweb_check_feature('blame');
+
+ # Atom: http://www.atomenabled.org/developers/syndication/
+ # RSS: https://web.archive.org/web/20030729001534/http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+ if ($format ne 'rss' && $format ne 'atom') {
+ die_error(400, "Unknown web feed format");
+ }
+
+ # log/feed of current (HEAD) branch, log of given branch, history of file/directory
+ my $head = $hash || 'HEAD';
+ my @commitlist = parse_commits($head, 150, 0, $file_name);
+
+ my %latest_commit;
+ my %latest_date;
+ my $content_type = "application/$format+xml";
+ if (defined $cgi->http('HTTP_ACCEPT') &&
+ $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
+ # browser (feed reader) prefers text/xml
+ $content_type = 'text/xml';
+ }
+ if (defined($commitlist[0])) {
+ %latest_commit = %{$commitlist[0]};
+ my $latest_epoch = $latest_commit{'committer_epoch'};
+ exit_if_unmodified_since($latest_epoch);
+ %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
+ }
+ print $cgi->header(
+ -type => $content_type,
+ -charset => 'utf-8',
+ %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
+ -status => '200 OK');
+
+ # Optimization: skip generating the body if client asks only
+ # for Last-Modified date.
+ return if ($cgi->request_method() eq 'HEAD');
+
+ # header variables
+ my $title = "$site_name - $project/$action";
+ my $feed_type = 'log';
+ if (defined $hash) {
+ $title .= " - '$hash'";
+ $feed_type = 'branch log';
+ if (defined $file_name) {
+ $title .= " :: $file_name";
+ $feed_type = 'history';
+ }
+ } elsif (defined $file_name) {
+ $title .= " - $file_name";
+ $feed_type = 'history';
+ }
+ $title .= " $feed_type";
+ $title = esc_html($title);
+ my $descr = git_get_project_description($project);
+ if (defined $descr) {
+ $descr = esc_html($descr);
+ } else {
+ $descr = "$project " .
+ ($format eq 'rss' ? 'RSS' : 'Atom') .
+ " feed";
+ }
+ my $owner = git_get_project_owner($project);
+ $owner = esc_html($owner);
+
+ #header
+ my $alt_url;
+ if (defined $file_name) {
+ $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
+ } elsif (defined $hash) {
+ $alt_url = href(-full=>1, action=>"log", hash=>$hash);
+ } else {
+ $alt_url = href(-full=>1, action=>"summary");
+ }
+ $alt_url = esc_attr($alt_url);
+ print qq!\n!;
+ if ($format eq 'rss') {
+ print <
+
+XML
+ print "$title \n" .
+ " $alt_url\n" .
+ "$descr \n" .
+ "en \n" .
+ # project owner is responsible for 'editorial' content
+ "$owner \n";
+ if (defined $logo || defined $favicon) {
+ # prefer the logo to the favicon, since RSS
+ # doesn't allow both
+ my $img = esc_url($logo || $favicon);
+ print "\n" .
+ "$img \n" .
+ "$title \n" .
+ " $alt_url\n" .
+ " \n";
+ }
+ if (%latest_date) {
+ print "$latest_date{'rfc2822'} \n";
+ print "$latest_date{'rfc2822'} \n";
+ }
+ print "gitweb v.$version/$git_version \n";
+ } elsif ($format eq 'atom') {
+ print <
+XML
+ print "$title \n" .
+ "$descr \n" .
+ ' ' . "\n" .
+ ' ' . "\n" .
+ "" . esc_url(href(-full=>1)) . " \n" .
+ # use project owner for feed author
+ "$owner \n";
+ if (defined $favicon) {
+ print "" . esc_url($favicon) . " \n";
+ }
+ if (defined $logo) {
+ # not twice as wide as tall: 72 x 27 pixels
+ print "" . esc_url($logo) . " \n";
+ }
+ if (! %latest_date) {
+ # dummy date to keep the feed valid until commits trickle in:
+ print "1970-01-01T00:00:00Z \n";
+ } else {
+ print "$latest_date{'iso-8601'} \n";
+ }
+ print "gitweb \n";
+ }
+
+ # contents
+ for (my $i = 0; $i <= $#commitlist; $i++) {
+ my %co = %{$commitlist[$i]};
+ my $commit = $co{'id'};
+ # we read 150, we always show 30 and the ones more recent than 48 hours
+ if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
+ last;
+ }
+ my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+
+ # get list of changed files
+ open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+ $co{'parent'} || "--root",
+ $co{'id'}, "--", (defined $file_name ? $file_name : ())
+ or next;
+ my @difftree = map { chomp; $_ } <$fd>;
+ close $fd
+ or next;
+
+ # print element (entry, item)
+ my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
+ if ($format eq 'rss') {
+ print "- \n" .
+ "
" . esc_html($co{'title'}) . " \n" .
+ "" . esc_html($co{'author'}) . " \n" .
+ "$cd{'rfc2822'} \n" .
+ "$co_url \n" .
+ " " . esc_html($co_url) . "\n" .
+ "" . esc_html($co{'title'}) . " \n" .
+ "" .
+ "\n" .
+ "" . esc_html($co{'title'}) . " \n" .
+ "$cd{'iso-8601'} \n" .
+ "\n" .
+ " " . esc_html($co{'author_name'}) . " \n";
+ if ($co{'author_email'}) {
+ print " " . esc_html($co{'author_email'}) . " \n";
+ }
+ print " \n" .
+ # use committer for contributor
+ "\n" .
+ " " . esc_html($co{'committer_name'}) . " \n";
+ if ($co{'committer_email'}) {
+ print " " . esc_html($co{'committer_email'}) . " \n";
+ }
+ print " \n" .
+ "$cd{'iso-8601'} \n" .
+ " \n" .
+ "" . esc_html($co_url) . " \n" .
+ "\n" .
+ "\n";
+ }
+ my $comment = $co{'comment'};
+ print "
\n";
+ foreach my $line (@$comment) {
+ $line = esc_html($line);
+ print "$line\n";
+ }
+ print " \n";
+ foreach my $difftree_line (@difftree) {
+ my %difftree = parse_difftree_raw_line($difftree_line);
+ next if !$difftree{'from_id'};
+
+ my $file = $difftree{'file'} || $difftree{'to_file'};
+
+ print "" .
+ "[" .
+ $cgi->a({-href => href(-full=>1, action=>"blobdiff",
+ hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
+ hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
+ file_name=>$file, file_parent=>$difftree{'from_file'}),
+ -title => "diff"}, 'D');
+ if ($have_blame) {
+ print $cgi->a({-href => href(-full=>1, action=>"blame",
+ file_name=>$file, hash_base=>$commit),
+ -title => "blame"}, 'B');
+ }
+ # if this is not a feed of a file history
+ if (!defined $file_name || $file_name ne $file) {
+ print $cgi->a({-href => href(-full=>1, action=>"history",
+ file_name=>$file, hash=>$commit),
+ -title => "history"}, 'H');
+ }
+ $file = esc_path($file);
+ print "] ".
+ "$file \n";
+ }
+ if ($format eq 'rss') {
+ print " ]]>\n" .
+ "\n" .
+ "\n";
+ } elsif ($format eq 'atom') {
+ print "\n
\n" .
+ " \n" .
+ "\n";
+ }
+ }
+
+ # end of feed
+ if ($format eq 'rss') {
+ print " \n\n";
+ } elsif ($format eq 'atom') {
+ print "\n";
+ }
+}
+
+sub git_rss {
+ git_feed('rss');
+}
+
+sub git_atom {
+ git_feed('atom');
+}
+
+sub git_opml {
+ my @list = git_get_projects_list($project_filter, $strict_export);
+ if (!@list) {
+ die_error(404, "No projects found");
+ }
+
+ print $cgi->header(
+ -type => 'text/xml',
+ -charset => 'utf-8',
+ -content_disposition => 'inline; filename="opml.xml"');
+
+ my $title = esc_html($site_name);
+ my $filter = " within subdirectory ";
+ if (defined $project_filter) {
+ $filter .= esc_html($project_filter);
+ } else {
+ $filter = "";
+ }
+ print <
+
+
+ $title OPML Export$filter
+
+
+
+XML
+
+ foreach my $pr (@list) {
+ my %proj = %$pr;
+ my $head = git_get_head_hash($proj{'path'});
+ if (!defined $head) {
+ next;
+ }
+ $git_dir = "$projectroot/$proj{'path'}";
+ my %co = parse_commit($head);
+ if (!%co) {
+ next;
+ }
+
+ my $path = esc_html(chop_str($proj{'path'}, 25, 5));
+ my $rss = esc_attr(href('project' => $proj{'path'}, 'action' => 'rss', -full => 1));
+ my $html = esc_attr(href('project' => $proj{'path'}, 'action' => 'summary', -full => 1));
+ print "\n";
+ }
+ print <
+
+
+XML
+}
diff --git a/gitweb/meson.build b/gitweb/meson.build
new file mode 100644
index 0000000..88a54b4
--- /dev/null
+++ b/gitweb/meson.build
@@ -0,0 +1,89 @@
+gitweb_config = configuration_data()
+gitweb_config.set_quoted('PERL_PATH', target_perl.full_path())
+gitweb_config.set_quoted('CSSMIN', '')
+gitweb_config.set_quoted('JSMIN', '')
+gitweb_config.set_quoted('GIT_BINDIR', get_option('prefix') / get_option('bindir'))
+gitweb_config.set_quoted('GITWEB_CONFIG', get_option('gitweb_config'))
+gitweb_config.set_quoted('GITWEB_CONFIG_SYSTEM', get_option('gitweb_config_system'))
+gitweb_config.set_quoted('GITWEB_CONFIG_COMMON', get_option('gitweb_config_common'))
+gitweb_config.set_quoted('GITWEB_HOME_LINK_STR', get_option('gitweb_home_link_str'))
+gitweb_config.set_quoted('GITWEB_SITENAME', get_option('gitweb_sitename'))
+gitweb_config.set_quoted('GITWEB_PROJECTROOT', get_option('gitweb_projectroot'))
+gitweb_config.set_quoted('GITWEB_PROJECT_MAXDEPTH', get_option('gitweb_project_maxdepth'))
+gitweb_config.set_quoted('GITWEB_EXPORT_OK', get_option('gitweb_export_ok'))
+gitweb_config.set_quoted('GITWEB_STRICT_EXPORT', get_option('gitweb_strict_export'))
+gitweb_config.set_quoted('GITWEB_BASE_URL', get_option('gitweb_base_url'))
+gitweb_config.set_quoted('GITWEB_LIST', get_option('gitweb_list'))
+gitweb_config.set_quoted('GITWEB_HOMETEXT', get_option('gitweb_hometext'))
+gitweb_config.set_quoted('GITWEB_CSS', get_option('gitweb_css'))
+gitweb_config.set_quoted('GITWEB_LOGO', get_option('gitweb_logo'))
+gitweb_config.set_quoted('GITWEB_FAVICON', get_option('gitweb_favicon'))
+gitweb_config.set_quoted('GITWEB_JS', get_option('gitweb_js'))
+gitweb_config.set_quoted('GITWEB_SITE_HTML_HEAD_STRING', get_option('gitweb_site_html_head_string'))
+gitweb_config.set_quoted('GITWEB_SITE_HEADER', get_option('gitweb_site_header'))
+gitweb_config.set_quoted('GITWEB_SITE_FOOTER', get_option('gitweb_site_footer'))
+gitweb_config.set_quoted('HIGHLIGHT_BIN', get_option('highlight_bin'))
+
+configure_file(
+ input: 'GITWEB-BUILD-OPTIONS.in',
+ output: 'GITWEB-BUILD-OPTIONS',
+ configuration: gitweb_config,
+)
+
+test_dependencies += custom_target(
+ input: 'gitweb.perl',
+ output: 'gitweb.cgi',
+ command: [
+ shell,
+ meson.current_source_dir() / 'generate-gitweb-cgi.sh',
+ meson.current_build_dir() / 'GITWEB-BUILD-OPTIONS',
+ git_version_file.full_path(),
+ '@INPUT@',
+ '@OUTPUT@',
+ ],
+ install: true,
+ install_dir: get_option('datadir') / 'gitweb',
+ depends: [git_version_file],
+)
+
+javascript_files = [
+ meson.current_source_dir() / 'static/js/adjust-timezone.js',
+ meson.current_source_dir() / 'static/js/blame_incremental.js',
+ meson.current_source_dir() / 'static/js/javascript-detection.js',
+ meson.current_source_dir() / 'static/js/lib/common-lib.js',
+ meson.current_source_dir() / 'static/js/lib/cookies.js',
+ meson.current_source_dir() / 'static/js/lib/datetime.js',
+]
+
+test_dependencies += custom_target(
+ input: javascript_files,
+ output: 'gitweb.js',
+ command: [
+ shell,
+ meson.current_source_dir() / 'generate-gitweb-js.sh',
+ '@OUTPUT@',
+ ] + javascript_files,
+ install: true,
+ install_dir: get_option('datadir') / 'gitweb/static',
+)
+
+foreach asset : [
+ 'static/git-favicon.png',
+ 'static/git-logo.png',
+ 'static/gitweb.css',
+]
+ if meson.version().version_compare('>=1.3.0')
+ fs.copyfile(asset,
+ install: true,
+ install_dir: get_option('datadir') / 'gitweb' / fs.parent(asset),
+ )
+ else
+ configure_file(
+ input: asset,
+ output: fs.stem(asset),
+ copy: true,
+ install: true,
+ install_dir: get_option('datadir') / 'gitweb' / fs.parent(asset),
+ )
+ endif
+endforeach
diff --git a/gitweb/static/git-favicon.png b/gitweb/static/git-favicon.png
new file mode 100644
index 0000000..aae35a7
Binary files /dev/null and b/gitweb/static/git-favicon.png differ
diff --git a/gitweb/static/git-logo.png b/gitweb/static/git-logo.png
new file mode 100644
index 0000000..f4ede2e
Binary files /dev/null and b/gitweb/static/git-logo.png differ
diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css
new file mode 100644
index 0000000..48d2e51
--- /dev/null
+++ b/gitweb/static/gitweb.css
@@ -0,0 +1,686 @@
+body {
+ font-family: sans-serif;
+ font-size: small;
+ border: solid #d9d8d1;
+ border-width: 1px;
+ margin: 10px;
+ background-color: #ffffff;
+ color: #000000;
+}
+
+a {
+ color: #0000cc;
+}
+
+a:hover, a:visited, a:active {
+ color: #880000;
+}
+
+span.cntrl {
+ border: dashed #aaaaaa;
+ border-width: 1px;
+ padding: 0px 2px 0px 2px;
+ margin: 0px 2px 0px 2px;
+}
+
+img.logo {
+ float: right;
+ border-width: 0px;
+}
+
+img.avatar {
+ vertical-align: middle;
+}
+
+img.blob {
+ max-height: 100%;
+ max-width: 100%;
+}
+
+a.list img.avatar {
+ border-style: none;
+}
+
+div.page_header {
+ height: 25px;
+ padding: 8px;
+ font-size: 150%;
+ font-weight: bold;
+ background-color: #d9d8d1;
+}
+
+div.page_header a:visited, a.header {
+ color: #0000cc;
+}
+
+div.page_header a:hover {
+ color: #880000;
+}
+
+div.page_nav {
+ padding: 8px;
+}
+
+div.page_nav a:visited {
+ color: #0000cc;
+}
+
+div.page_path {
+ padding: 8px;
+ font-weight: bold;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+}
+
+div.page_footer {
+ height: 22px;
+ padding: 4px 8px;
+ background-color: #d9d8d1;
+}
+
+div.page_footer_text {
+ line-height: 22px;
+ float: left;
+ color: #555555;
+ font-style: italic;
+}
+
+div#generating_info {
+ margin: 4px;
+ font-size: smaller;
+ text-align: center;
+ color: #505050;
+}
+
+div.page_body {
+ padding: 8px;
+ font-family: monospace;
+}
+
+div.title, a.title {
+ display: block;
+ padding: 6px 8px;
+ font-weight: bold;
+ background-color: #edece6;
+ text-decoration: none;
+ color: #000000;
+}
+
+div.readme {
+ padding: 8px;
+}
+
+a.title:hover {
+ background-color: #d9d8d1;
+}
+
+div.title_text {
+ padding: 6px 0px;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+ font-family: monospace;
+}
+
+div.log_body {
+ padding: 8px 8px 8px 150px;
+}
+
+span.age {
+ position: relative;
+ float: left;
+ width: 142px;
+ font-style: italic;
+}
+
+span.signoff {
+ color: #888888;
+}
+
+div.log_link {
+ padding: 0px 8px;
+ font-size: 70%;
+ font-family: sans-serif;
+ font-style: normal;
+ position: relative;
+ float: left;
+ width: 136px;
+}
+
+div.list_head {
+ padding: 6px 8px 4px;
+ border: solid #d9d8d1;
+ border-width: 1px 0px 0px;
+ font-style: italic;
+}
+
+.author_date, .author {
+ font-style: italic;
+}
+
+div.author_date {
+ padding: 8px;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px 0px;
+}
+
+a.list {
+ text-decoration: none;
+ color: #000000;
+}
+
+a.subject, a.name {
+ font-weight: bold;
+}
+
+table.tags a.subject {
+ font-weight: normal;
+}
+
+a.list:hover {
+ text-decoration: underline;
+ color: #880000;
+}
+
+a.text {
+ text-decoration: none;
+ color: #0000cc;
+}
+
+a.text:visited {
+ text-decoration: none;
+ color: #880000;
+}
+
+a.text:hover {
+ text-decoration: underline;
+ color: #880000;
+}
+
+table {
+ padding: 8px 4px;
+ border-spacing: 0;
+}
+
+table.diff_tree {
+ font-family: monospace;
+}
+
+table.combined.diff_tree th {
+ text-align: center;
+}
+
+table.combined.diff_tree td {
+ padding-right: 24px;
+}
+
+table.combined.diff_tree th.link,
+table.combined.diff_tree td.link {
+ padding: 0px 2px;
+}
+
+table.combined.diff_tree td.nochange a {
+ color: #6666ff;
+}
+
+table.combined.diff_tree td.nochange a:hover,
+table.combined.diff_tree td.nochange a:visited {
+ color: #d06666;
+}
+
+table.blame {
+ border-collapse: collapse;
+}
+
+table.blame td {
+ padding: 0px 5px;
+ font-size: 100%;
+ vertical-align: top;
+}
+
+th {
+ padding: 2px 5px;
+ font-size: 100%;
+ text-align: left;
+}
+
+/* do not change row style on hover for 'blame' view */
+tr.light,
+table.blame .light:hover {
+ background-color: #ffffff;
+}
+
+tr.dark,
+table.blame .dark:hover {
+ background-color: #f6f6f0;
+}
+
+/* currently both use the same, but it can change */
+tr.light:hover,
+tr.dark:hover {
+ background-color: #edece6;
+}
+
+/* boundary commits in 'blame' view */
+/* and commits without "previous" */
+tr.boundary td.sha1,
+tr.no-previous td.linenr {
+ font-weight: bold;
+}
+
+/* for 'blame_incremental', during processing */
+tr.color1 { background-color: #f6fff6; }
+tr.color2 { background-color: #f6f6ff; }
+tr.color3 { background-color: #fff6f6; }
+
+td {
+ padding: 2px 5px;
+ font-size: 100%;
+ vertical-align: top;
+}
+
+td.link, td.selflink {
+ padding: 2px 5px;
+ font-family: sans-serif;
+ font-size: 70%;
+}
+
+td.selflink {
+ padding-right: 0px;
+}
+
+td.sha1 {
+ font-family: monospace;
+}
+
+.error {
+ color: red;
+ background-color: yellow;
+}
+
+td.current_head {
+ text-decoration: underline;
+}
+
+td.category {
+ background-color: #d9d8d1;
+ border-top: 1px solid #000000;
+ border-left: 1px solid #000000;
+ font-weight: bold;
+}
+
+table.diff_tree span.file_status.new {
+ color: #008000;
+}
+
+table.diff_tree span.file_status.deleted {
+ color: #c00000;
+}
+
+table.diff_tree span.file_status.moved,
+table.diff_tree span.file_status.mode_chnge {
+ color: #777777;
+}
+
+table.diff_tree span.file_status.copied {
+ color: #70a070;
+}
+
+/* noage: "No commits" */
+table.project_list td.noage {
+ color: #808080;
+ font-style: italic;
+}
+
+/* age2: 60*60*24*2 <= age */
+table.project_list td.age2, table.blame td.age2 {
+ font-style: italic;
+}
+
+/* age1: 60*60*2 <= age < 60*60*24*2 */
+table.project_list td.age1 {
+ color: #009900;
+ font-style: italic;
+}
+
+table.blame td.age1 {
+ color: #009900;
+ background: transparent;
+}
+
+/* age0: age < 60*60*2 */
+table.project_list td.age0 {
+ color: #009900;
+ font-style: italic;
+ font-weight: bold;
+}
+
+table.blame td.age0 {
+ color: #009900;
+ background: transparent;
+ font-weight: bold;
+}
+
+td.pre, div.pre, div.diff {
+ font-family: monospace;
+ font-size: 12px;
+ white-space: pre;
+}
+
+td.mode {
+ font-family: monospace;
+}
+
+/* progress of blame_interactive */
+div#progress_bar {
+ height: 2px;
+ margin-bottom: -2px;
+ background-color: #d8d9d0;
+}
+div#progress_info {
+ float: right;
+ text-align: right;
+}
+
+/* format of (optional) objects size in 'tree' view */
+td.size {
+ font-family: monospace;
+ text-align: right;
+}
+
+/* styling of diffs (patchsets): commitdiff and blobdiff views */
+div.diff.header,
+div.diff.extended_header {
+ white-space: normal;
+}
+
+div.diff.header {
+ font-weight: bold;
+
+ background-color: #edece6;
+
+ margin-top: 4px;
+ padding: 4px 0px 2px 0px;
+ border: solid #d9d8d1;
+ border-width: 1px 0px 1px 0px;
+}
+
+div.diff.header a.path {
+ text-decoration: underline;
+}
+
+div.diff.extended_header,
+div.diff.extended_header a.path,
+div.diff.extended_header a.hash {
+ color: #777777;
+}
+
+div.diff.extended_header .info {
+ color: #b0b0b0;
+}
+
+div.diff.extended_header {
+ background-color: #f6f5ee;
+ padding: 2px 0px 2px 0px;
+}
+
+div.diff a.list,
+div.diff a.path,
+div.diff a.hash {
+ text-decoration: none;
+}
+
+div.diff a.list:hover,
+div.diff a.path:hover,
+div.diff a.hash:hover {
+ text-decoration: underline;
+}
+
+div.diff.to_file a.path,
+div.diff.to_file {
+ color: #007000;
+}
+
+div.diff.add {
+ color: #008800;
+}
+
+div.diff.add span.marked {
+ background-color: #aaffaa;
+}
+
+div.diff.from_file a.path,
+div.diff.from_file {
+ color: #aa0000;
+}
+
+div.diff.rem {
+ color: #cc0000;
+}
+
+div.diff.rem span.marked {
+ background-color: #ffaaaa;
+}
+
+div.diff.chunk_header a,
+div.diff.chunk_header {
+ color: #990099;
+}
+
+div.diff.chunk_header {
+ border: dotted #ffe0ff;
+ border-width: 1px 0px 0px 0px;
+ margin-top: 2px;
+}
+
+div.diff.chunk_header span.chunk_info {
+ background-color: #ffeeff;
+}
+
+div.diff.chunk_header span.section {
+ color: #aa22aa;
+}
+
+div.diff.incomplete {
+ color: #cccccc;
+}
+
+div.diff.nodifferences {
+ font-weight: bold;
+ color: #600000;
+}
+
+/* side-by-side diff */
+div.chunk_block {
+ overflow: hidden;
+}
+
+div.chunk_block div.old {
+ float: left;
+ width: 50%;
+ overflow: hidden;
+}
+
+div.chunk_block div.new {
+ margin-left: 50%;
+ width: 50%;
+}
+
+div.chunk_block.rem div.old div.diff.rem {
+ background-color: #fff5f5;
+}
+div.chunk_block.add div.new div.diff.add {
+ background-color: #f8fff8;
+}
+div.chunk_block.chg div div.diff {
+ background-color: #fffff0;
+}
+div.chunk_block.ctx div div.diff.ctx {
+ color: #404040;
+}
+
+
+div.index_include {
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+ padding: 12px 8px;
+}
+
+div.search {
+ font-size: 100%;
+ font-weight: normal;
+ margin: 4px 8px;
+ float: right;
+ top: 56px;
+ right: 12px
+}
+
+div.projsearch {
+ text-align: center;
+ margin: 20px 0px;
+}
+
+div.projsearch form {
+ margin-bottom: 2px;
+}
+
+td.linenr {
+ text-align: right;
+}
+
+a.linenr {
+ color: #999999;
+ text-decoration: none
+}
+
+a.rss_logo {
+ float: right;
+ padding: 3px 5px;
+ line-height: 10px;
+ border: 1px solid;
+ border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e;
+ color: #ffffff;
+ background-color: #ff6600;
+ font-weight: bold;
+ font-family: sans-serif;
+ font-size: 70%;
+ text-align: center;
+ text-decoration: none;
+}
+
+a.rss_logo:hover {
+ background-color: #ee5500;
+}
+
+a.rss_logo.generic {
+ background-color: #ff8800;
+}
+
+a.rss_logo.generic:hover {
+ background-color: #ee7700;
+}
+
+span.refs span {
+ padding: 0px 4px;
+ font-size: 70%;
+ font-weight: normal;
+ border: 1px solid;
+ background-color: #ffaaff;
+ border-color: #ffccff #ff00ee #ff00ee #ffccff;
+}
+
+span.refs span a {
+ text-decoration: none;
+ color: inherit;
+}
+
+span.refs span a:hover {
+ text-decoration: underline;
+}
+
+span.refs span.indirect {
+ font-style: italic;
+}
+
+span.refs span.ref {
+ background-color: #aaaaff;
+ border-color: #ccccff #0033cc #0033cc #ccccff;
+}
+
+span.refs span.tag {
+ background-color: #ffffaa;
+ border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
+}
+
+span.refs span.head {
+ background-color: #aaffaa;
+ border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
+}
+
+span.atnight {
+ color: #cc0000;
+}
+
+span.match {
+ color: #e00000;
+}
+
+div.binary {
+ font-style: italic;
+}
+
+div.remote {
+ margin: .5em;
+ border: 1px solid #d9d8d1;
+ display: inline-block;
+}
+
+/* JavaScript-based timezone manipulation */
+
+.popup { /* timezone selection UI */
+ position: absolute;
+ /* "top: 0; right: 0;" would be better, if not for bugs in browsers */
+ top: 0; left: 0;
+ border: 1px solid;
+ padding: 2px;
+ background-color: #f0f0f0;
+ font-style: normal;
+ color: #000000;
+ cursor: auto;
+}
+
+.close-button { /* close timezone selection UI without selecting */
+ /* float doesn't work within absolutely positioned container,
+ * if width of container is not set explicitly */
+ /* float: right; */
+ position: absolute;
+ top: 0px; right: 0px;
+ border: 1px solid green;
+ margin: 1px 1px 1px 1px;
+ padding-bottom: 2px;
+ width: 12px;
+ height: 10px;
+ font-size: 9px;
+ font-weight: bold;
+ text-align: center;
+ background-color: #fff0f0;
+ cursor: pointer;
+}
+
+
+/* Style definition generated by highlight 2.4.5, http://andre-simon.de/doku/highlight/en/highlight.php */
+
+/* Highlighting theme definition: */
+
+.num { color:#2928ff; }
+.esc { color:#ff00ff; }
+.str { color:#ff0000; }
+.dstr { color:#818100; }
+.slc { color:#838183; font-style:italic; }
+.com { color:#838183; font-style:italic; }
+.dir { color:#008200; }
+.sym { color:#000000; }
+.line { color:#555555; }
+.kwa { color:#000000; font-weight:bold; }
+.kwb { color:#830000; }
+.kwc { color:#000000; font-weight:bold; }
+.kwd { color:#010181; }
diff --git a/gitweb/static/js/README b/gitweb/static/js/README
new file mode 100644
index 0000000..f8460ed
--- /dev/null
+++ b/gitweb/static/js/README
@@ -0,0 +1,20 @@
+GIT web interface (gitweb) - JavaScript
+=======================================
+
+This directory holds JavaScript code used by gitweb (GIT web interface).
+Scripts from there would be concatenated together in the order specified
+by gitweb/Makefile into gitweb/static/gitweb.js, during building of
+gitweb/gitweb.cgi (during gitweb building). The resulting file (or its
+minification) would then be installed / deployed together with gitweb.
+
+Scripts in 'lib/' subdirectory compose generic JavaScript library,
+providing features required by gitweb but in no way limited to gitweb
+only. In the future those scripts could be replaced by some JavaScript
+library / framework, like e.g. jQuery, YUI, Prototype, MooTools, Dojo,
+ExtJS, Script.aculo.us or SproutCore.
+
+All scripts that manipulate gitweb output should be put outside 'lib/',
+directly in this directory ('gitweb/static/js/'). Those scripts would
+have to be rewritten if gitweb moves to using some JavaScript library.
+
+See also comments in gitweb/Makefile.
diff --git a/gitweb/static/js/adjust-timezone.js b/gitweb/static/js/adjust-timezone.js
new file mode 100644
index 0000000..0c67779
--- /dev/null
+++ b/gitweb/static/js/adjust-timezone.js
@@ -0,0 +1,330 @@
+// Copyright (C) 2011, John 'Warthog9' Hawley
+// 2011, Jakub Narebski
+
+/**
+ * @fileOverview Manipulate dates in gitweb output, adjusting timezone
+ * @license GPLv2 or later
+ */
+
+/**
+ * Get common timezone, add UI for changing timezones, and adjust
+ * dates to use requested common timezone.
+ *
+ * This function is called during onload event (added to window.onload).
+ *
+ * @param {String} tzDefault: default timezone, if there is no cookie
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzCookieInfo.name: name of cookie to store timezone
+ * @param {String} tzClassName: denotes elements with date to be adjusted
+ */
+function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) {
+ var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo);
+ var tz = tzDefault;
+
+ if (tzCookieTZ) {
+ // set timezone to value saved in a cookie
+ tz = tzCookieTZ;
+ // refresh cookie, so its expiration counts from last use of gitweb
+ setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo);
+ }
+
+ // add UI for changing timezone
+ addChangeTZ(tz, tzCookieInfo, tzClassName);
+
+ // server-side of gitweb produces datetime in UTC,
+ // so if tz is 'utc' there is no need for changes
+ var nochange = tz === 'utc';
+
+ // adjust dates to use specified common timezone
+ fixDatetimeTZ(tz, tzClassName, nochange);
+}
+
+
+/* ...................................................................... */
+/* Changing dates to use requested timezone */
+
+/**
+ * Replace RFC-2822 dates contained in SPAN elements with tzClassName
+ * CSS class with equivalent dates in given timezone.
+ *
+ * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local'
+ * @param {String} tzClassName: specifies elements to be changed
+ * @param {Boolean} nochange: markup for timezone change, but don't change it
+ */
+function fixDatetimeTZ(tz, tzClassName, nochange) {
+ // sanity check, method should be ensured by common-lib.js
+ if (!document.getElementsByClassName) {
+ return;
+ }
+
+ // translate to timezone in '(-|+)HHMM' format
+ tz = normalizeTimezoneInfo(tz);
+
+ // NOTE: result of getElementsByClassName should probably be cached
+ var classesFound = document.getElementsByClassName(tzClassName, "span");
+ for (var i = 0, len = classesFound.length; i < len; i++) {
+ var curElement = classesFound[i];
+
+ curElement.title = 'Click to change timezone';
+ if (!nochange) {
+ // we use *.firstChild.data (W3C DOM) instead of *.innerHTML
+ // as the latter doesn't always work everywhere in every browser
+ var epoch = parseRFC2822Date(curElement.firstChild.data);
+ var adjusted = formatDateRFC2882(epoch, tz);
+
+ curElement.firstChild.data = adjusted;
+ }
+ }
+}
+
+
+/* ...................................................................... */
+/* Adding triggers, generating timezone menu, displaying and hiding */
+
+/**
+ * Adds triggers for UI to change common timezone used for dates in
+ * gitweb output: it marks up and/or creates item to click to invoke
+ * timezone change UI, creates timezone UI fragment to be attached,
+ * and installs appropriate onclick trigger (via event delegation).
+ *
+ * @param {String} tzSelected: pre-selected timezone,
+ * 'utc' or 'local' or '(-|+)HHMM'
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzClassName: specifies elements to install trigger
+ */
+function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) {
+ // make link to timezone UI discoverable
+ addCssRule('.'+tzClassName + ':hover',
+ 'text-decoration: underline; cursor: help;');
+
+ // create form for selecting timezone (to be saved in a cookie)
+ var tzSelectFragment = document.createDocumentFragment();
+ tzSelectFragment = createChangeTZForm(tzSelectFragment,
+ tzSelected, tzCookieInfo, tzClassName);
+
+ // event delegation handler for timezone selection UI (clicking on entry)
+ // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/
+ // assumes that there is no existing document.onclick handler
+ document.onclick = function onclickHandler(event) {
+ //IE doesn't pass in the event object
+ event = event || window.event;
+
+ //IE uses srcElement as the target
+ var target = event.target || event.srcElement;
+
+ switch (target.className) {
+ case tzClassName:
+ // don't display timezone menu if it is already displayed
+ if (tzSelectFragment.childNodes.length > 0) {
+ displayChangeTZForm(target, tzSelectFragment);
+ }
+ break;
+ } // end switch
+ };
+}
+
+/**
+ * Create DocumentFragment with UI for changing common timezone in
+ * which dates are shown in.
+ *
+ * @param {DocumentFragment} documentFragment: where attach UI
+ * @param {String} tzSelected: default (pre-selected) timezone
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @returns {DocumentFragment}
+ */
+function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) {
+ var div = document.createElement("div");
+ div.className = 'popup';
+
+ /* 'X
' */
+ var closeButton = document.createElement('div');
+ closeButton.className = 'close-button';
+ closeButton.title = '(click on this box to close)';
+ closeButton.appendChild(document.createTextNode('X'));
+ closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName);
+ div.appendChild(closeButton);
+
+ /* 'Select timezone: ' */
+ div.appendChild(document.createTextNode('Select timezone: '));
+ var br = document.createElement('br');
+ br.clear = 'all';
+ div.appendChild(br);
+
+ /* '
+ * ...
+ * UTC-07:00
+ * UTC-06:00
+ * ...
+ * ' */
+ var select = document.createElement("select");
+ select.name = "tzoffset";
+ //select.style.clear = 'all';
+ select.appendChild(generateTZOptions(tzSelected));
+ select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName);
+ div.appendChild(select);
+
+ documentFragment.appendChild(div);
+
+ return documentFragment;
+}
+
+
+/**
+ * Hide (remove from DOM) timezone change UI, ensuring that it is not
+ * garbage collected and that it can be re-enabled later.
+ *
+ * @param {DocumentFragment} documentFragment: contains detached UI
+ * @param {HTMLSelectElement} target: select element inside of UI
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {DocumentFragment} documentFragment
+ */
+function removeChangeTZForm(documentFragment, target, tzClassName) {
+ // find containing element, where we appended timezone selection UI
+ // `target' is somewhere inside timezone menu
+ var container = target.parentNode, popup = target;
+ while (container &&
+ container.className !== tzClassName) {
+ popup = container;
+ container = container.parentNode;
+ }
+ // safety check if we found correct container,
+ // and if it isn't deleted already
+ if (!container || !popup ||
+ container.className !== tzClassName ||
+ popup.className !== 'popup') {
+ return documentFragment;
+ }
+
+ // timezone selection UI was appended as last child
+ // see also displayChangeTZForm function
+ var removed = popup.parentNode.removeChild(popup);
+ if (documentFragment.firstChild !== removed) { // the only child
+ // re-append it so it would be available for next time
+ documentFragment.appendChild(removed);
+ }
+ // all of inline style was added by this script
+ // it is not really needed to remove it, but it is a good practice
+ container.removeAttribute('style');
+
+ return documentFragment;
+}
+
+
+/**
+ * Display UI for changing common timezone for dates in gitweb output.
+ * To be used from 'onclick' event handler.
+ *
+ * @param {HTMLElement} target: where to install/display UI
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ */
+function displayChangeTZForm(target, tzSelectFragment) {
+ // for absolute positioning to be related to target element
+ target.style.position = 'relative';
+ target.style.display = 'inline-block';
+
+ // show/display UI for changing timezone
+ target.appendChild(tzSelectFragment);
+}
+
+
+/* ...................................................................... */
+/* List of timezones for timezone selection menu */
+
+/**
+ * Generate list of timezones for creating timezone select UI
+ *
+ * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' }
+ */
+function generateTZList() {
+ var timezones = [
+ { value: "utc", descr: "UTC/GMT"},
+ { value: "local", descr: "Local (per browser)"}
+ ];
+
+ // generate all full hour timezones (no fractional timezones)
+ for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) {
+ var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2);
+ timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'};
+ if (x === 0) {
+ timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC±00:00'
+ }
+ }
+
+ return timezones;
+}
+
+/**
+ * Generate elements for timezone select UI
+ *
+ * @param {String} tzSelected: default timezone
+ * @returns {DocumentFragment} list of options elements to appendChild
+ */
+function generateTZOptions(tzSelected) {
+ var elems = document.createDocumentFragment();
+ var timezones = generateTZList();
+
+ for (var i = 0, len = timezones.length; i < len; i++) {
+ var tzone = timezones[i];
+ var option = document.createElement("option");
+ if (tzone.value === tzSelected) {
+ option.defaultSelected = true;
+ }
+ option.value = tzone.value;
+ option.appendChild(document.createTextNode(tzone.descr));
+
+ elems.appendChild(option);
+ }
+
+ return elems;
+}
+
+
+/* ...................................................................... */
+/* Event handlers and/or their generators */
+
+/**
+ * Create event handler that select timezone and closes timezone select UI.
+ * To be used as $('select[name="tzselect"]').onchange handler.
+ *
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzCookieInfo.name: name of cookie to save result of selection
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {Function} event handler
+ */
+function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) {
+ //return function selectTZ(event) {
+ return function (event) {
+ event = event || window.event;
+ var target = event.target || event.srcElement;
+
+ var selected = target.options.item(target.selectedIndex);
+ removeChangeTZForm(tzSelectFragment, target, tzClassName);
+
+ if (selected) {
+ selected.defaultSelected = true;
+ setCookie(tzCookieInfo.name, selected.value, tzCookieInfo);
+ fixDatetimeTZ(selected.value, tzClassName);
+ }
+ };
+}
+
+/**
+ * Create event handler that closes timezone select UI.
+ * To be used e.g. as $('.closebutton').onclick handler.
+ *
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {Function} event handler
+ */
+function closeTZFormHandler(tzSelectFragment, tzClassName) {
+ //return function closeTZForm(event) {
+ return function (event) {
+ event = event || window.event;
+ var target = event.target || event.srcElement;
+
+ removeChangeTZForm(tzSelectFragment, target, tzClassName);
+ };
+}
+
+/* end of adjust-timezone.js */
diff --git a/gitweb/static/js/blame_incremental.js b/gitweb/static/js/blame_incremental.js
new file mode 100644
index 0000000..e100d82
--- /dev/null
+++ b/gitweb/static/js/blame_incremental.js
@@ -0,0 +1,692 @@
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb
+ * @license GPLv2 or later
+ */
+
+/* ============================================================ */
+/*
+ * This code uses DOM methods instead of (nonstandard) innerHTML
+ * to modify page.
+ *
+ * innerHTML is non-standard IE extension, though supported by most
+ * browsers; however Firefox up to version 1.5 didn't implement it in
+ * a strict mode (application/xml+xhtml mimetype).
+ *
+ * Also my simple benchmarks show that using elem.firstChild.data =
+ * 'content' is slightly faster than elem.innerHTML = 'content'. It
+ * is however more fragile (text element fragment must exists), and
+ * less feature-rich (we cannot add HTML).
+ *
+ * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the
+ * equivalent using DOM 2 Core is usually shown in comments.
+ */
+
+
+/* ............................................................ */
+/* utility/helper functions (and variables) */
+
+var projectUrl; // partial query + separator ('?' or ';')
+
+// 'commits' is an associative map. It maps SHA1s to Commit objects.
+var commits = {};
+
+/**
+ * constructor for Commit objects, used in 'blame'
+ * @class Represents a blamed commit
+ * @param {String} sha1: SHA-1 identifier of a commit
+ */
+function Commit(sha1) {
+ if (this instanceof Commit) {
+ this.sha1 = sha1;
+ this.nprevious = 0; /* number of 'previous', effective parents */
+ } else {
+ return new Commit(sha1);
+ }
+}
+
+/* ............................................................ */
+/* progress info, timing, error reporting */
+
+var blamedLines = 0;
+var totalLines = '???';
+var div_progress_bar;
+var div_progress_info;
+
+/**
+ * Detects how many lines does a blamed file have,
+ * This information is used in progress info
+ *
+ * @returns {Number|String} Number of lines in file, or string '...'
+ */
+function countLines() {
+ var table =
+ document.getElementById('blame_table') ||
+ document.getElementsByTagName('table')[0];
+
+ if (table) {
+ return table.getElementsByTagName('tr').length - 1; // for header
+ } else {
+ return '...';
+ }
+}
+
+/**
+ * update progress info and length (width) of progress bar
+ *
+ * @globals div_progress_info, div_progress_bar, blamedLines, totalLines
+ */
+function updateProgressInfo() {
+ if (!div_progress_info) {
+ div_progress_info = document.getElementById('progress_info');
+ }
+ if (!div_progress_bar) {
+ div_progress_bar = document.getElementById('progress_bar');
+ }
+ if (!div_progress_info && !div_progress_bar) {
+ return;
+ }
+
+ var percentage = Math.floor(100.0*blamedLines/totalLines);
+
+ if (div_progress_info) {
+ div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines +
+ ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)';
+ }
+
+ if (div_progress_bar) {
+ //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');
+ div_progress_bar.style.width = percentage + '%';
+ }
+}
+
+
+var t_interval_server = '';
+var cmds_server = '';
+var t0 = new Date();
+
+/**
+ * write how much it took to generate data, and to run script
+ *
+ * @globals t0, t_interval_server, cmds_server
+ */
+function writeTimeInterval() {
+ var info_time = document.getElementById('generating_time');
+ if (!info_time || !t_interval_server) {
+ return;
+ }
+ var t1 = new Date();
+ info_time.firstChild.data += ' + (' +
+ t_interval_server + ' sec server blame_data / ' +
+ (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)';
+
+ var info_cmds = document.getElementById('generating_cmd');
+ if (!info_time || !cmds_server) {
+ return;
+ }
+ info_cmds.firstChild.data += ' + ' + cmds_server;
+}
+
+/**
+ * show an error message alert to user within page (in progress info area)
+ * @param {String} str: plain text error message (no HTML)
+ *
+ * @globals div_progress_info
+ */
+function errorInfo(str) {
+ if (!div_progress_info) {
+ div_progress_info = document.getElementById('progress_info');
+ }
+ if (div_progress_info) {
+ div_progress_info.className = 'error';
+ div_progress_info.firstChild.data = str;
+ }
+}
+
+/* ............................................................ */
+/* coloring rows during blame_data (git blame --incremental) run */
+
+/**
+ * used to extract N from 'colorN', where N is a number,
+ * @constant
+ */
+var colorRe = /\bcolor([0-9]*)\b/;
+
+/**
+ * return N if , otherwise return null
+ * (some browsers require CSS class names to begin with letter)
+ *
+ * @param {HTMLElement} tr: table row element to check
+ * @param {String} tr.className: 'class' attribute of tr element
+ * @returns {Number|null} N if tr.className == 'colorN', otherwise null
+ *
+ * @globals colorRe
+ */
+function getColorNo(tr) {
+ if (!tr) {
+ return null;
+ }
+ var className = tr.className;
+ if (className) {
+ var match = colorRe.exec(className);
+ if (match) {
+ return parseInt(match[1], 10);
+ }
+ }
+ return null;
+}
+
+var colorsFreq = [0, 0, 0];
+/**
+ * return one of given possible colors (currently least used one)
+ * example: chooseColorNoFrom(2, 3) returns 2 or 3
+ *
+ * @param {Number[]} arguments: one or more numbers
+ * assumes that 1 <= arguments[i] <= colorsFreq.length
+ * @returns {Number} Least used color number from arguments
+ * @globals colorsFreq
+ */
+function chooseColorNoFrom() {
+ // choose the color which is least used
+ var colorNo = arguments[0];
+ for (var i = 1; i < arguments.length; i++) {
+ if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) {
+ colorNo = arguments[i];
+ }
+ }
+ colorsFreq[colorNo-1]++;
+ return colorNo;
+}
+
+/**
+ * given two neighbor elements, find color which would be different
+ * from color of both of neighbors; used to 3-color blame table
+ *
+ * @param {HTMLElement} tr_prev
+ * @param {HTMLElement} tr_next
+ * @returns {Number} color number N such that
+ * colorN != tr_prev.className && colorN != tr_next.className
+ */
+function findColorNo(tr_prev, tr_next) {
+ var color_prev = getColorNo(tr_prev);
+ var color_next = getColorNo(tr_next);
+
+
+ // neither of neighbors has color set
+ // THEN we can use any of 3 possible colors
+ if (!color_prev && !color_next) {
+ return chooseColorNoFrom(1,2,3);
+ }
+
+ // either both neighbors have the same color,
+ // or only one of neighbors have color set
+ // THEN we can use any color except given
+ var color;
+ if (color_prev === color_next) {
+ color = color_prev; // = color_next;
+ } else if (!color_prev) {
+ color = color_next;
+ } else if (!color_next) {
+ color = color_prev;
+ }
+ if (color) {
+ return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);
+ }
+
+ // neighbors have different colors
+ // THEN there is only one color left
+ return (3 - ((color_prev + color_next) % 3));
+}
+
+/* ............................................................ */
+/* coloring rows like 'blame' after 'blame_data' finishes */
+
+/**
+ * returns true if given row element (tr) is first in commit group
+ * to be used only after 'blame_data' finishes (after processing)
+ *
+ * @param {HTMLElement} tr: table row
+ * @returns {Boolean} true if TR is first in commit group
+ */
+function isStartOfGroup(tr) {
+ return tr.firstChild.className === 'sha1';
+}
+
+/**
+ * change colors to use zebra coloring (2 colors) instead of 3 colors
+ * concatenate neighbor commit groups belonging to the same commit
+ *
+ * @globals colorRe
+ */
+function fixColorsAndGroups() {
+ var colorClasses = ['light', 'dark'];
+ var linenum = 1;
+ var tr, prev_group;
+ var colorClass = 0;
+ var table =
+ document.getElementById('blame_table') ||
+ document.getElementsByTagName('table')[0];
+
+ while ((tr = document.getElementById('l'+linenum))) {
+ // index origin is 0, which is table header; start from 1
+ //while ((tr = table.rows[linenum])) { // <- it is slower
+ if (isStartOfGroup(tr, linenum, document)) {
+ if (prev_group &&
+ prev_group.firstChild.firstChild.href ===
+ tr.firstChild.firstChild.href) {
+ // we have to concatenate groups
+ var prev_rows = prev_group.firstChild.rowSpan || 1;
+ var curr_rows = tr.firstChild.rowSpan || 1;
+ prev_group.firstChild.rowSpan = prev_rows + curr_rows;
+ //tr.removeChild(tr.firstChild);
+ tr.deleteCell(0); // DOM2 HTML way
+ } else {
+ colorClass = (colorClass + 1) % 2;
+ prev_group = tr;
+ }
+ }
+ var tr_class = tr.className;
+ tr.className = tr_class.replace(colorRe, colorClasses[colorClass]);
+ linenum++;
+ }
+}
+
+
+/* ============================================================ */
+/* main part: parsing response */
+
+/**
+ * Function called for each blame entry, as soon as it finishes.
+ * It updates page via DOM manipulation, adding sha1 info, etc.
+ *
+ * @param {Commit} commit: blamed commit
+ * @param {Object} group: object representing group of lines,
+ * which blame the same commit (blame entry)
+ *
+ * @globals blamedLines
+ */
+function handleLine(commit, group) {
+ /*
+ This is the structure of the HTML fragment we are working
+ with:
+
+
+
+ 123
+ # times (my ext3 doesn't).
+
+ */
+
+ var resline = group.resline;
+
+ // format date and time string only once per commit
+ if (!commit.info) {
+ /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */
+ commit.info = commit.author + ', ' +
+ formatDateISOLocal(commit.authorTime, commit.authorTimezone);
+ }
+
+ // color depends on group of lines, not only on blamed commit
+ var colorNo = findColorNo(
+ document.getElementById('l'+(resline-1)),
+ document.getElementById('l'+(resline+group.numlines))
+ );
+
+ // loop over lines in commit group
+ for (var i = 0; i < group.numlines; i++, resline++) {
+ var tr = document.getElementById('l'+resline);
+ if (!tr) {
+ break;
+ }
+ /*
+
+
+ 123
+ # times (my ext3 doesn't).
+
+ */
+ var td_sha1 = tr.firstChild;
+ var a_sha1 = td_sha1.firstChild;
+ var a_linenr = td_sha1.nextSibling.firstChild;
+
+ /* */
+ var tr_class = '';
+ if (colorNo !== null) {
+ tr_class = 'color'+colorNo;
+ }
+ if (commit.boundary) {
+ tr_class += ' boundary';
+ }
+ if (commit.nprevious === 0) {
+ tr_class += ' no-previous';
+ } else if (commit.nprevious > 1) {
+ tr_class += ' multiple-previous';
+ }
+ tr.className = tr_class;
+
+ /* ? */
+ if (i === 0) {
+ td_sha1.title = commit.info;
+ td_sha1.rowSpan = group.numlines;
+
+ a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1;
+ if (a_sha1.firstChild) {
+ a_sha1.firstChild.data = commit.sha1.substr(0, 8);
+ } else {
+ a_sha1.appendChild(
+ document.createTextNode(commit.sha1.substr(0, 8)));
+ }
+ if (group.numlines >= 2) {
+ var fragment = document.createDocumentFragment();
+ var br = document.createElement("br");
+ var match = commit.author.match(/\b([A-Z])\B/g);
+ if (match) {
+ var text = document.createTextNode(
+ match.join(''));
+ }
+ if (br && text) {
+ var elem = fragment || td_sha1;
+ elem.appendChild(br);
+ elem.appendChild(text);
+ if (fragment) {
+ td_sha1.appendChild(fragment);
+ }
+ }
+ }
+ } else {
+ //tr.removeChild(td_sha1); // DOM2 Core way
+ tr.deleteCell(0); // DOM2 HTML way
+ }
+
+ /* 123 */
+ var linenr_commit =
+ ('previous' in commit ? commit.previous : commit.sha1);
+ var linenr_filename =
+ ('file_parent' in commit ? commit.file_parent : commit.filename);
+ a_linenr.href = projectUrl + 'a=blame_incremental' +
+ ';hb=' + linenr_commit +
+ ';f=' + encodeURIComponent(linenr_filename) +
+ '#l' + (group.srcline + i);
+
+ blamedLines++;
+
+ //updateProgressInfo();
+ }
+}
+
+// ----------------------------------------------------------------------
+
+/**#@+
+ * @constant
+ */
+var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/;
+var infoRe = /^([a-z-]+) ?(.*)/;
+var endRe = /^END ?([^ ]*) ?(.*)/;
+/**@-*/
+
+var curCommit = new Commit();
+var curGroup = {};
+
+/**
+ * Parse output from 'git blame --incremental [...]', received via
+ * XMLHttpRequest from server (blamedataUrl), and call handleLine
+ * (which updates page) as soon as blame entry is completed.
+ *
+ * @param {String[]} lines: new complete lines from blamedata server
+ *
+ * @globals commits, curCommit, curGroup, t_interval_server, cmds_server
+ * @globals sha1Re, infoRe, endRe
+ */
+function processBlameLines(lines) {
+ var match;
+
+ for (var i = 0, len = lines.length; i < len; i++) {
+
+ if ((match = sha1Re.exec(lines[i]))) {
+ var sha1 = match[1];
+ var srcline = parseInt(match[2], 10);
+ var resline = parseInt(match[3], 10);
+ var numlines = parseInt(match[4], 10);
+
+ var c = commits[sha1];
+ if (!c) {
+ c = new Commit(sha1);
+ commits[sha1] = c;
+ }
+ curCommit = c;
+
+ curGroup.srcline = srcline;
+ curGroup.resline = resline;
+ curGroup.numlines = numlines;
+
+ } else if ((match = infoRe.exec(lines[i]))) {
+ var info = match[1];
+ var data = match[2];
+ switch (info) {
+ case 'filename':
+ curCommit.filename = unquote(data);
+ // 'filename' information terminates the entry
+ handleLine(curCommit, curGroup);
+ updateProgressInfo();
+ break;
+ case 'author':
+ curCommit.author = data;
+ break;
+ case 'author-time':
+ curCommit.authorTime = parseInt(data, 10);
+ break;
+ case 'author-tz':
+ curCommit.authorTimezone = data;
+ break;
+ case 'previous':
+ curCommit.nprevious++;
+ // store only first 'previous' header
+ if (!('previous' in curCommit)) {
+ var parts = data.split(' ', 2);
+ curCommit.previous = parts[0];
+ curCommit.file_parent = unquote(parts[1]);
+ }
+ break;
+ case 'boundary':
+ curCommit.boundary = true;
+ break;
+ } // end switch
+
+ } else if ((match = endRe.exec(lines[i]))) {
+ t_interval_server = match[1];
+ cmds_server = match[2];
+
+ } else if (lines[i] !== '') {
+ // malformed line
+
+ } // end if (match)
+
+ } // end for (lines)
+}
+
+/**
+ * Process new data and return pointer to end of processed part
+ *
+ * @param {String} unprocessed: new data (from nextReadPos)
+ * @param {Number} nextReadPos: end of last processed data
+ * @return {Number} end of processed data (new value for nextReadPos)
+ */
+function processData(unprocessed, nextReadPos) {
+ var lastLineEnd = unprocessed.lastIndexOf('\n');
+ if (lastLineEnd !== -1) {
+ var lines = unprocessed.substring(0, lastLineEnd).split('\n');
+ nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */;
+
+ processBlameLines(lines);
+ } // end if
+
+ return nextReadPos;
+}
+
+/**
+ * Handle XMLHttpRequest errors
+ *
+ * @param {XMLHttpRequest} xhr: XMLHttpRequest object
+ * @param {Number} [xhr.pollTimer] ID of the timeout to clear
+ *
+ * @globals commits
+ */
+function handleError(xhr) {
+ errorInfo('Server error: ' +
+ xhr.status + ' - ' + (xhr.statusText || 'Error contacting server'));
+
+ if (typeof xhr.pollTimer === "number") {
+ clearTimeout(xhr.pollTimer);
+ delete xhr.pollTimer;
+ }
+ commits = {}; // free memory
+}
+
+/**
+ * Called after XMLHttpRequest finishes (loads)
+ *
+ * @param {XMLHttpRequest} xhr: XMLHttpRequest object
+ * @param {Number} [xhr.pollTimer] ID of the timeout to clear
+ *
+ * @globals commits
+ */
+function responseLoaded(xhr) {
+ if (typeof xhr.pollTimer === "number") {
+ clearTimeout(xhr.pollTimer);
+ delete xhr.pollTimer;
+ }
+
+ fixColorsAndGroups();
+ writeTimeInterval();
+ commits = {}; // free memory
+}
+
+/**
+ * handler for XMLHttpRequest onreadystatechange event
+ * @see startBlame
+ *
+ * @param {XMLHttpRequest} xhr: XMLHttpRequest object
+ * @param {Number} xhr.prevDataLength: previous value of xhr.responseText.length
+ * @param {Number} xhr.nextReadPos: start of unread part of xhr.responseText
+ * @param {Number} [xhr.pollTimer] ID of the timeout (to reset or cancel)
+ * @param {Boolean} fromTimer: if handler was called from timer
+ */
+function handleResponse(xhr, fromTimer) {
+
+ /*
+ * xhr.readyState
+ *
+ * Value Constant (W3C) Description
+ * -------------------------------------------------------------------
+ * 0 UNSENT open() has not been called yet.
+ * 1 OPENED send() has not been called yet.
+ * 2 HEADERS_RECEIVED send() has been called, and headers
+ * and status are available.
+ * 3 LOADING Downloading; responseText holds partial data.
+ * 4 DONE The operation is complete.
+ */
+
+ if (xhr.readyState !== 4 && xhr.readyState !== 3) {
+ return;
+ }
+
+ // the server returned error
+ // try ... catch block is to work around bug in IE8
+ try {
+ if (xhr.readyState === 3 && xhr.status !== 200) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+ if (xhr.readyState === 4 && xhr.status !== 200) {
+ handleError(xhr);
+ return;
+ }
+
+ // In konqueror xhr.responseText is sometimes null here...
+ if (xhr.responseText === null) {
+ return;
+ }
+
+
+ // extract new whole (complete) lines, and process them
+ if (xhr.prevDataLength !== xhr.responseText.length) {
+ xhr.prevDataLength = xhr.responseText.length;
+ var unprocessed = xhr.responseText.substring(xhr.nextReadPos);
+ xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos);
+ }
+
+ // did we finish work?
+ if (xhr.readyState === 4) {
+ responseLoaded(xhr);
+ return;
+ }
+
+ // if we get from timer, we have to restart it
+ // otherwise onreadystatechange gives us partial response, timer not needed
+ if (fromTimer) {
+ setTimeout(function () {
+ handleResponse(xhr, true);
+ }, 1000);
+
+ } else if (typeof xhr.pollTimer === "number") {
+ clearTimeout(xhr.pollTimer);
+ delete xhr.pollTimer;
+ }
+}
+
+// ============================================================
+// ------------------------------------------------------------
+
+/**
+ * Incrementally update line data in blame_incremental view in gitweb.
+ *
+ * @param {String} blamedataUrl: URL to server script generating blame data.
+ * @param {String} bUrl: partial URL to project, used to generate links.
+ *
+ * Called from 'blame_incremental' view after loading table with
+ * file contents, a base for blame view.
+ *
+ * @globals t0, projectUrl, div_progress_bar, totalLines
+*/
+function startBlame(blamedataUrl, bUrl) {
+
+ var xhr = createRequestObject();
+ if (!xhr) {
+ errorInfo('ERROR: XMLHttpRequest not supported');
+ return;
+ }
+
+ t0 = new Date();
+ projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';');
+ if ((div_progress_bar = document.getElementById('progress_bar'))) {
+ //div_progress_bar.setAttribute('style', 'width: 100%;');
+ div_progress_bar.style.cssText = 'width: 100%;';
+ }
+ totalLines = countLines();
+ updateProgressInfo();
+
+ /* add extra properties to xhr object to help processing response */
+ xhr.prevDataLength = -1; // used to detect if we have new data
+ xhr.nextReadPos = 0; // where unread part of response starts
+
+ xhr.onreadystatechange = function () {
+ handleResponse(xhr, false);
+ };
+
+ xhr.open('GET', blamedataUrl);
+ xhr.setRequestHeader('Accept', 'text/plain');
+ xhr.send(null);
+
+ // not all browsers call onreadystatechange event on each server flush
+ // poll response using timer every second to handle this issue
+ xhr.pollTimer = setTimeout(function () {
+ handleResponse(xhr, true);
+ }, 1000);
+}
+
+/* end of blame_incremental.js */
diff --git a/gitweb/static/js/javascript-detection.js b/gitweb/static/js/javascript-detection.js
new file mode 100644
index 0000000..fa2596f
--- /dev/null
+++ b/gitweb/static/js/javascript-detection.js
@@ -0,0 +1,43 @@
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Detect if JavaScript is enabled, and pass it to server-side
+ * @license GPLv2 or later
+ */
+
+
+/* ============================================================ */
+/* Manipulating links */
+
+/**
+ * used to check if link has 'js' query parameter already (at end),
+ * and other reasons to not add 'js=1' param at the end of link
+ * @constant
+ */
+var jsExceptionsRe = /[;?]js=[01](#.*)?$/;
+
+/**
+ * Add '?js=1' or ';js=1' to the end of every link in the document
+ * that doesn't have 'js' query parameter set already.
+ *
+ * Links with 'js=1' lead to JavaScript version of given action, if it
+ * exists (currently there is only 'blame_incremental' for 'blame')
+ *
+ * To be used as `window.onload` handler
+ *
+ * @globals jsExceptionsRe
+ */
+function fixLinks() {
+ var allLinks = document.getElementsByTagName("a") || document.links;
+ for (var i = 0, len = allLinks.length; i < len; i++) {
+ var link = allLinks[i];
+ if (!jsExceptionsRe.test(link)) {
+ link.href = link.href.replace(/(#|$)/,
+ (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1$1');
+ }
+ }
+}
+
+/* end of javascript-detection.js */
diff --git a/gitweb/static/js/lib/common-lib.js b/gitweb/static/js/lib/common-lib.js
new file mode 100644
index 0000000..99e3eb8
--- /dev/null
+++ b/gitweb/static/js/lib/common-lib.js
@@ -0,0 +1,224 @@
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Generic JavaScript code (helper functions)
+ * @license GPLv2 or later
+ */
+
+
+/* ============================================================ */
+/* ............................................................ */
+/* Padding */
+
+/**
+ * pad INPUT on the left with STR that is assumed to have visible
+ * width of single character (for example nonbreakable spaces),
+ * to WIDTH characters
+ *
+ * example: padLeftStr(12, 3, '\u00A0') == '\u00A012'
+ * ('\u00A0' is nonbreakable space)
+ *
+ * @param {Number|String} input: number to pad
+ * @param {Number} width: visible width of output
+ * @param {String} str: string to prefix to string, defaults to '\u00A0'
+ * @returns {String} INPUT prefixed with STR x (WIDTH - INPUT.length)
+ */
+function padLeftStr(input, width, str) {
+ var prefix = '';
+ if (typeof str === 'undefined') {
+ ch = '\u00A0'; // using ' ' doesn't work in all browsers
+ }
+
+ width -= input.toString().length;
+ while (width > 0) {
+ prefix += str;
+ width--;
+ }
+ return prefix + input;
+}
+
+/**
+ * Pad INPUT on the left to WIDTH, using given padding character CH,
+ * for example padLeft('a', 3, '_') is '__a'
+ * padLeft(4, 2) is '04' (same as padLeft(4, 2, '0'))
+ *
+ * @param {String} input: input value converted to string.
+ * @param {Number} width: desired length of output.
+ * @param {String} ch: single character to prefix to string, defaults to '0'.
+ *
+ * @returns {String} Modified string, at least SIZE length.
+ */
+function padLeft(input, width, ch) {
+ var s = input + "";
+ if (typeof ch === 'undefined') {
+ ch = '0';
+ }
+
+ while (s.length < width) {
+ s = ch + s;
+ }
+ return s;
+}
+
+
+/* ............................................................ */
+/* Handling browser incompatibilities */
+
+/**
+ * Create XMLHttpRequest object in cross-browser way
+ * @returns XMLHttpRequest object, or null
+ */
+function createRequestObject() {
+ try {
+ return new XMLHttpRequest();
+ } catch (e) {}
+ try {
+ return window.createRequest();
+ } catch (e) {}
+ try {
+ return new ActiveXObject("Msxml2.XMLHTTP");
+ } catch (e) {}
+ try {
+ return new ActiveXObject("Microsoft.XMLHTTP");
+ } catch (e) {}
+
+ return null;
+}
+
+
+/**
+ * Insert rule giving specified STYLE to given SELECTOR at the end of
+ * first CSS stylesheet.
+ *
+ * @param {String} selector: CSS selector, e.g. '.class'
+ * @param {String} style: rule contents, e.g. 'background-color: red;'
+ */
+function addCssRule(selector, style) {
+ var stylesheet = document.styleSheets[0];
+
+ var theRules = [];
+ if (stylesheet.cssRules) { // W3C way
+ theRules = stylesheet.cssRules;
+ } else if (stylesheet.rules) { // IE way
+ theRules = stylesheet.rules;
+ }
+
+ if (stylesheet.insertRule) { // W3C way
+ stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length);
+ } else if (stylesheet.addRule) { // IE way
+ stylesheet.addRule(selector, style);
+ }
+}
+
+
+/* ............................................................ */
+/* Support for legacy browsers */
+
+/**
+ * Provides getElementsByClassName method, if there is no native
+ * implementation of this method.
+ *
+ * NOTE that there are limits and differences compared to native
+ * getElementsByClassName as defined by e.g.:
+ * https://developer.mozilla.org/en/DOM/document.getElementsByClassName
+ * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-getelementsbyclassname
+ * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-getelementsbyclassname
+ *
+ * Namely, this implementation supports only single class name as
+ * argument and not set of space-separated tokens representing classes,
+ * it returns Array of nodes rather than live NodeList, and has
+ * additional optional argument where you can limit search to given tags
+ * (via getElementsByTagName).
+ *
+ * Based on
+ * https://code.google.com/p/getelementsbyclassname/
+ * http://www.dustindiaz.com/getelementsbyclass/
+ * https://stackoverflow.com/questions/1818865/do-we-have-getelementsbyclassname-in-javascript
+ *
+ * See also https://johnresig.com/blog/getelementsbyclassname-speed-comparison/
+ *
+ * @param {String} class: name of _single_ class to find
+ * @param {String} [taghint] limit search to given tags
+ * @returns {Node[]} array of matching elements
+ */
+if (!('getElementsByClassName' in document)) {
+ document.getElementsByClassName = function (classname, taghint) {
+ taghint = taghint || "*";
+ var elements = (taghint === "*" && document.all) ?
+ document.all :
+ document.getElementsByTagName(taghint);
+ var pattern = new RegExp("(^|\\s)" + classname + "(\\s|$)");
+ var matches= [];
+ for (var i = 0, j = 0, n = elements.length; i < n; i++) {
+ var el= elements[i];
+ if (el.className && pattern.test(el.className)) {
+ // matches.push(el);
+ matches[j] = el;
+ j++;
+ }
+ }
+ return matches;
+ };
+} // end if
+
+
+/* ............................................................ */
+/* unquoting/unescaping filenames */
+
+/**#@+
+ * @constant
+ */
+var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g;
+var octEscRe = /^[0-7]{1,3}$/;
+var maybeQuotedRe = /^\"(.*)\"$/;
+/**#@-*/
+
+/**
+ * unquote maybe C-quoted filename (as used by git, i.e. it is
+ * in double quotes '"' if there is any escape character used)
+ * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a'
+ *
+ * @param {String} str: git-quoted string
+ * @returns {String} Unquoted and unescaped string
+ *
+ * @globals escCodeRe, octEscRe, maybeQuotedRe
+ */
+function unquote(str) {
+ function unq(seq) {
+ var es = {
+ // character escape codes, aka escape sequences (from C)
+ // replacements are to some extent JavaScript specific
+ t: "\t", // tab (HT, TAB)
+ n: "\n", // newline (NL)
+ r: "\r", // return (CR)
+ f: "\f", // form feed (FF)
+ b: "\b", // backspace (BS)
+ a: "\x07", // alarm (bell) (BEL)
+ e: "\x1B", // escape (ESC)
+ v: "\v" // vertical tab (VT)
+ };
+
+ if (seq.search(octEscRe) !== -1) {
+ // octal char sequence
+ return String.fromCharCode(parseInt(seq, 8));
+ } else if (seq in es) {
+ // C escape sequence, aka character escape code
+ return es[seq];
+ }
+ // quoted ordinary character
+ return seq;
+ }
+
+ var match = str.match(maybeQuotedRe);
+ if (match) {
+ str = match[1];
+ // perhaps str = eval('"'+str+'"'); would be enough?
+ str = str.replace(escCodeRe,
+ function (substr, p1, offset, s) { return unq(p1); });
+ }
+ return str;
+}
+
+/* end of common-lib.js */
diff --git a/gitweb/static/js/lib/cookies.js b/gitweb/static/js/lib/cookies.js
new file mode 100644
index 0000000..66b9a07
--- /dev/null
+++ b/gitweb/static/js/lib/cookies.js
@@ -0,0 +1,114 @@
+/**
+ * @fileOverview Accessing cookies from JavaScript
+ * @license GPLv2 or later
+ */
+
+/*
+ * Based on subsection "Cookies in JavaScript" of "Professional
+ * JavaScript for Web Developers" by Nicholas C. Zakas and cookie
+ * plugin from jQuery (dual licensed under the MIT and GPL licenses)
+ */
+
+
+/**
+ * Create a cookie with the given name and value,
+ * and other optional parameters.
+ *
+ * @example
+ * setCookie('foo', 'bar'); // will be deleted when browser exits
+ * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) });
+ * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week
+ * setCookie('foo', 'bar', { expires: 14, path: '/' });
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores).
+ * @param {String} sValue: The string value stored in a cookie.
+ * @param {Object} [options] An object literal containing key/value pairs
+ * to provide optional cookie attributes.
+ * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires,
+ * or an integer specifying the expiration date from now on in days,
+ * or a Date object to be used as cookie expiration date.
+ * If a negative value is specified or a date in the past),
+ * the cookie will be deleted.
+ * If set to null or omitted, the cookie will be a session cookie
+ * and will not be retained when the browser exits.
+ * @param {String} [options.path] Restrict access of a cookie to particular directory
+ * (default: path of page that created the cookie).
+ * @param {String} [options.domain] Override what web sites are allowed to access cookie
+ * (default: domain of page that created the cookie).
+ * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set
+ * and the cookie would be accessible only from secure sites
+ * (cookie transmission will require secure protocol like HTTPS).
+ */
+function setCookie(sName, sValue, options) {
+ options = options || {};
+ if (sValue === null) {
+ sValue = '';
+ option.expires = 'delete';
+ }
+
+ var sCookie = sName + '=' + encodeURIComponent(sValue);
+
+ if (options.expires) {
+ var oExpires = options.expires, sDate;
+ if (oExpires === 'delete') {
+ sDate = 'Thu, 01 Jan 1970 00:00:00 GMT';
+ } else if (typeof oExpires === 'string') {
+ sDate = oExpires;
+ } else {
+ var oDate;
+ if (typeof oExpires === 'number') {
+ oDate = new Date();
+ oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms
+ } else {
+ oDate = oExpires;
+ }
+ sDate = oDate.toGMTString();
+ }
+ sCookie += '; expires=' + sDate;
+ }
+
+ if (options.path) {
+ sCookie += '; path=' + (options.path);
+ }
+ if (options.domain) {
+ sCookie += '; domain=' + (options.domain);
+ }
+ if (options.secure) {
+ sCookie += '; secure';
+ }
+ document.cookie = sCookie;
+}
+
+/**
+ * Get the value of a cookie with the given name.
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores)
+ * @returns {String|null} The string value stored in a cookie
+ */
+function getCookie(sName) {
+ var sRE = '(?:; )?' + sName + '=([^;]*);?';
+ var oRE = new RegExp(sRE);
+ if (oRE.test(document.cookie)) {
+ return decodeURIComponent(RegExp['$1']);
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Delete cookie with given name
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores)
+ * @param {Object} [options] An object literal containing key/value pairs
+ * to provide optional cookie attributes.
+ * @param {String} [options.path] Must be the same as when setting a cookie
+ * @param {String} [options.domain] Must be the same as when setting a cookie
+ */
+function deleteCookie(sName, options) {
+ options = options || {};
+ options.expires = 'delete';
+
+ setCookie(sName, '', options);
+}
+
+/* end of cookies.js */
diff --git a/gitweb/static/js/lib/datetime.js b/gitweb/static/js/lib/datetime.js
new file mode 100644
index 0000000..f78c60a
--- /dev/null
+++ b/gitweb/static/js/lib/datetime.js
@@ -0,0 +1,176 @@
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Datetime manipulation: parsing and formatting
+ * @license GPLv2 or later
+ */
+
+
+/* ............................................................ */
+/* parsing and retrieving datetime related information */
+
+/**
+ * used to extract hours and minutes from timezone info, e.g '-0900'
+ * @constant
+ */
+var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/;
+
+/**
+ * convert numeric timezone +/-ZZZZ to offset from UTC in seconds
+ *
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @returns {Number} offset from UTC in seconds for timezone
+ *
+ * @globals tzRe
+ */
+function timezoneOffset(timezoneInfo) {
+ var match = tzRe.exec(timezoneInfo);
+ var tz_sign = (match[1] === '-' ? -1 : +1);
+ var tz_hour = parseInt(match[2],10);
+ var tz_min = parseInt(match[3],10);
+
+ return tz_sign*(((tz_hour*60) + tz_min)*60);
+}
+
+/**
+ * return local (browser) timezone as offset from UTC in seconds
+ *
+ * @returns {Number} offset from UTC in seconds for local timezone
+ */
+function localTimezoneOffset() {
+ // getTimezoneOffset returns the time-zone offset from UTC,
+ // in _minutes_, for the current locale
+ return ((new Date()).getTimezoneOffset() * -60);
+}
+
+/**
+ * return local (browser) timezone as numeric timezone '(+|-)HHMM'
+ *
+ * @returns {String} locat timezone as -/+ZZZZ
+ */
+function localTimezoneInfo() {
+ var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1;
+
+ return formatTimezoneInfo(0, tzOffsetMinutes);
+}
+
+
+/**
+ * Parse RFC-2822 date into a Unix timestamp (into epoch)
+ *
+ * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
+ * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC'
+ */
+function parseRFC2822Date(date) {
+ // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere)
+ // date syntax, which is defined in RFC 2822 (obsoletes RFC 822)
+ // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC
+ return Date.parse(date) / 1000;
+}
+
+
+/* ............................................................ */
+/* formatting date */
+
+/**
+ * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM'
+ *
+ * @param {Number} hours: offset in hours, e.g. 2 for '+0200'
+ * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030';
+ * it is split into hours if not 0 <= minutes < 60,
+ * for example 1200 would give '+0100';
+ * defaults to 0
+ * @param {String} [sep] separator between hours and minutes part,
+ * default is '', might be ':' for W3CDTF (rfc-3339)
+ * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format
+ */
+function formatTimezoneInfo(hours, minutes, sep) {
+ minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh)
+ sep = sep || ''; // default format is +/-ZZZZ
+
+ if (minutes < 0 || minutes > 59) {
+ hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60);
+ minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours
+ // NOTE: this works correctly because there is no UTC-00:30 timezone
+ }
+
+ var tzSign = hours >= 0 ? '+' : '-';
+ if (hours < 0) {
+ hours = -hours; // sign is stored in tzSign
+ }
+
+ return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0');
+}
+
+/**
+ * translate 'utc' and 'local' to numerical timezone
+ * @param {String} timezoneInfo: might be 'utc' or 'local' (browser)
+ */
+function normalizeTimezoneInfo(timezoneInfo) {
+ switch (timezoneInfo) {
+ case 'utc':
+ return '+0000';
+ case 'local': // 'local' is browser timezone
+ return localTimezoneInfo();
+ }
+ return timezoneInfo;
+}
+
+
+/**
+ * return date in local time formatted in iso-8601 like format
+ * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'
+ *
+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @returns {String} date in local time in iso-8601 like format
+ */
+function formatDateISOLocal(epoch, timezoneInfo) {
+ // date corrected by timezone
+ var localDate = new Date(1000 * (epoch +
+ timezoneOffset(timezoneInfo)));
+ var localDateStr = // e.g. '2005-08-07'
+ localDate.getUTCFullYear() + '-' +
+ padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +
+ padLeft(localDate.getUTCDate(), 2, '0');
+ var localTimeStr = // e.g. '21:49:46'
+ padLeft(localDate.getUTCHours(), 2, '0') + ':' +
+ padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
+ padLeft(localDate.getUTCSeconds(), 2, '0');
+
+ return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
+}
+
+/**
+ * return date in local time formatted in rfc-2822 format
+ * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
+ *
+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise
+ * @returns {String} date in local time in rfc-2822 format
+ */
+function formatDateRFC2882(epoch, timezoneInfo, padDay) {
+ // A short textual representation of a month, three letters
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+ // A textual representation of a day, three letters
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ // date corrected by timezone
+ var localDate = new Date(1000 * (epoch +
+ timezoneOffset(timezoneInfo)));
+ var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005'
+ days[localDate.getUTCDay()] + ', ' +
+ (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' +
+ months[localDate.getUTCMonth()] + ' ' +
+ localDate.getUTCFullYear();
+ var localTimeStr = // e.g. '21:49:46'
+ padLeft(localDate.getUTCHours(), 2, '0') + ':' +
+ padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
+ padLeft(localDate.getUTCSeconds(), 2, '0');
+
+ return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
+}
+
+/* end of datetime.js */
--
cgit v1.2.3