+ return $line;
+}
+
+# format marker of refs pointing to given object
+sub git_get_referencing {
+ my ($refs, $id) = @_;
+
+ if (defined $refs->{$id}) {
+ return ' <span class="tag">' . esc_html($refs->{$id}) . '</span>';
+ } else {
+ return "";
+ }
+}
+
+## ----------------------------------------------------------------------
+## git utility subroutines, invoking git commands
+
+# get HEAD ref of given project as hash
+sub git_read_head {
+ my $project = shift;
+ my $oENV = $ENV{'GIT_DIR'};
+ my $retval = undef;
+ $ENV{'GIT_DIR'} = "$projectroot/$project";
+ if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") {
+ my $head = <$fd>;
+ close $fd;
+ if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
+ $retval = $1;
+ }
+ }
+ if (defined $oENV) {
+ $ENV{'GIT_DIR'} = $oENV;
+ }
+ return $retval;
+}
+
+# get type of given object
+sub git_get_type {
+ my $hash = shift;
+
+ open my $fd, "-|", $GIT, "cat-file", '-t', $hash or return;
+ my $type = <$fd>;
+ close $fd or return;
+ chomp $type;
+ return $type;
+}
+
+sub git_get_project_config {
+ my $key = shift;
+
+ return unless ($key);
+ $key =~ s/^gitweb\.//;
+ return if ($key =~ m/\W/);
+
+ my $val = qx($GIT repo-config --get gitweb.$key);
+ return ($val);
+}
+
+sub git_get_project_config_bool {
+ my $val = git_get_project_config (@_);
+ if ($val and $val =~ m/true|yes|on/) {
+ return (1);
+ }
+ return; # implicit false
+}
+
+# get hash of given path at given ref
+sub git_get_hash_by_path {
+ my $base = shift;
+ my $path = shift || return undef;
+
+ my $tree = $base;
+
+ open my $fd, "-|", $GIT, "ls-tree", $base, "--", $path
+ or die_error(undef, "Open git-ls-tree failed.");
+ my $line = <$fd>;
+ close $fd or return undef;
+
+ #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
+ $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
+ return $3;
+}
+
+## ......................................................................
+## git utility functions, directly accessing git repository
+
+# assumes that PATH is not symref
+sub git_read_hash {
+ my $path = shift;
+
+ open my $fd, "$projectroot/$path" or return undef;
+ my $head = <$fd>;
+ close $fd;
+ chomp $head;
+ if ($head =~ m/^[0-9a-fA-F]{40}$/) {
+ return $head;
+ }
+}
+
+sub git_read_description {
+ my $path = shift;
+
+ open my $fd, "$projectroot/$path/description" or return undef;
+ my $descr = <$fd>;
+ close $fd;
+ chomp $descr;
+ return $descr;
+}
+
+sub git_read_projects {
+ my @list;
+
+ if (-d $projects_list) {
+ # search in directory
+ my $dir = $projects_list;
+ opendir my ($dh), $dir or return undef;
+ while (my $dir = readdir($dh)) {
+ if (-e "$projectroot/$dir/HEAD") {
+ my $pr = {
+ path => $dir,
+ };
+ push @list, $pr
+ }
+ }
+ closedir($dh);
+ } elsif (-f $projects_list) {
+ # read from file(url-encoded):
+ # 'git%2Fgit.git Linus+Torvalds'
+ # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
+ # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
+ open my ($fd), $projects_list or return undef;
+ while (my $line = <$fd>) {
+ chomp $line;
+ my ($path, $owner) = split ' ', $line;
+ $path = unescape($path);
+ $owner = unescape($owner);
+ if (!defined $path) {
+ next;
+ }
+ if (-e "$projectroot/$path/HEAD") {
+ my $pr = {
+ path => $path,
+ owner => decode("utf8", $owner, Encode::FB_DEFAULT),
+ };
+ push @list, $pr
+ }
+ }
+ close $fd;
+ }
+ @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
+ return @list;
+}
+
+sub read_info_ref {
+ my $type = shift || "";
+ my %refs;
+ # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+ # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+ open my $fd, "$projectroot/$project/info/refs" or return;
+ while (my $line = <$fd>) {
+ chomp $line;
+ # attention: for $type == "" it saves only last path part of ref name
+ # e.g. from 'refs/heads/jn/gitweb' it would leave only 'gitweb'
+ if ($line =~ m/^([0-9a-fA-F]{40})\t.*$type\/([^\^]+)/) {
+ if (defined $refs{$1}) {
+ $refs{$1} .= " / $2";
+ } else {
+ $refs{$1} = $2;
+ }
+ }
+ }
+ close $fd or return;
+ return \%refs;
+}
+
+## ----------------------------------------------------------------------
+## parse to hash functions
+
+sub date_str {
+ my $epoch = shift;
+ my $tz = shift || "-0000";
+
+ my %date;
+ my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
+ my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
+ my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
+ $date{'hour'} = $hour;
+ $date{'minute'} = $min;
+ $date{'mday'} = $mday;
+ $date{'day'} = $days[$wday];
+ $date{'month'} = $months[$mon];
+ $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
+ $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
+
+ $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
+ my $local = $epoch + ((int $1 + ($2/60)) * 3600);
+ ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
+ $date{'hour_local'} = $hour;
+ $date{'minute_local'} = $min;
+ $date{'tz_local'} = $tz;
+ return %date;
+}
+
+sub git_read_tag {
+ my $tag_id = shift;
+ my %tag;
+ my @comment;
+
+ open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return;
+ $tag{'id'} = $tag_id;
+ while (my $line = <$fd>) {
+ chomp $line;
+ if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
+ $tag{'object'} = $1;
+ } elsif ($line =~ m/^type (.+)$/) {
+ $tag{'type'} = $1;
+ } elsif ($line =~ m/^tag (.+)$/) {
+ $tag{'name'} = $1;
+ } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
+ $tag{'author'} = $1;
+ $tag{'epoch'} = $2;
+ $tag{'tz'} = $3;
+ } elsif ($line =~ m/--BEGIN/) {
+ push @comment, $line;
+ last;
+ } elsif ($line eq "") {
+ last;
+ }
+ }
+ push @comment, <$fd>;
+ $tag{'comment'} = \@comment;
+ close $fd or return;
+ if (!defined $tag{'name'}) {
+ return
+ };
+ return %tag
+}
+
+sub git_read_commit {
+ my $commit_id = shift;
+ my $commit_text = shift;
+
+ my @commit_lines;
+ my %co;
+
+ if (defined $commit_text) {
+ @commit_lines = @$commit_text;
+ } else {
+ $/ = "\0";
+ open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id or return;
+ @commit_lines = split '\n', <$fd>;
+ close $fd or return;
+ $/ = "\n";
+ pop @commit_lines;
+ }
+ my $header = shift @commit_lines;
+ if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
+ return;
+ }
+ ($co{'id'}, my @parents) = split ' ', $header;
+ $co{'parents'} = \@parents;
+ $co{'parent'} = $parents[0];
+ while (my $line = shift @commit_lines) {
+ last if $line eq "\n";
+ if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
+ $co{'tree'} = $1;
+ } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
+ $co{'author'} = $1;
+ $co{'author_epoch'} = $2;
+ $co{'author_tz'} = $3;