Code

Merge branch 'jc/commit-careful'
[git.git] / gitweb / gitweb.perl
index 873950126a8f2ba4125622c3b73498107cb7bce2..5ea3fda540f00e4b7ee2c11c380eac86168eca17 100755 (executable)
@@ -120,7 +120,7 @@ our %feature = (
        # To disable system wide have in $GITWEB_CONFIG
        # $feature{'snapshot'}{'default'} = [undef];
        # To have project specific config enable override in $GITWEB_CONFIG
-       # $feature{'blame'}{'override'} = 1;
+       # $feature{'snapshot'}{'override'} = 1;
        # and in project config gitweb.snapshot = none|gzip|bzip2;
        'snapshot' => {
                'sub' => \&feature_snapshot,
@@ -425,6 +425,7 @@ my %actions = (
        "history" => \&git_history,
        "log" => \&git_log,
        "rss" => \&git_rss,
+       "atom" => \&git_atom,
        "search" => \&git_search,
        "search_help" => \&git_search_help,
        "shortlog" => \&git_shortlog,
@@ -584,7 +585,21 @@ sub esc_html ($;%) {
        return $str;
 }
 
-# Make control characterss "printable".
+# quote control characters and escape filename to HTML
+sub esc_path {
+       my $str = shift;
+       my %opts = @_;
+
+       $str = to_utf8($str);
+       $str = escapeHTML($str);
+       if ($opts{'-nbsp'}) {
+               $str =~ s/ / /g;
+       }
+       $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
+       return $str;
+}
+
+# Make control characters "printable", using character escape codes (CEC)
 sub quot_cec {
        my $cntrl = shift;
        my %es = ( # character escape codes, aka escape sequences
@@ -604,22 +619,14 @@ sub quot_cec {
        return "<span class=\"cntrl\">$chr</span>";
 }
 
-# Alternatively use unicode control pictures codepoints.
+# Alternatively use unicode control pictures codepoints,
+# Unicode "printable representation" (PR)
 sub quot_upr {
        my $cntrl = shift;
        my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
        return "<span class=\"cntrl\">$chr</span>";
 }
 
-# quote control characters and escape filename to HTML
-sub esc_path {
-       my $str = shift;
-
-       $str = esc_html($str);
-       $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
-       return $str;
-}
-
 # git may return quoted and escaped filenames
 sub unquote {
        my $str = shift;
@@ -1147,14 +1154,15 @@ sub git_get_last_activity {
 sub git_get_references {
        my $type = shift || "";
        my %refs;
-       # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c      refs/tags/v2.6.11
-       # c39ae07f393806ccf406ef966e9a15afc43cc36a      refs/tags/v2.6.11^{}
-       open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
+       # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+       # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+       open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
+               ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
                or return;
 
        while (my $line = <$fd>) {
                chomp $line;
-               if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
+               if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
                        if (defined $refs{$1}) {
                                push @{$refs{$1}}, $2;
                        } else {
@@ -1198,10 +1206,12 @@ sub parse_date {
        $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{'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;
+       $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
+                            1900+$year, $mon, $mday, $hour ,$min, $sec;
 
        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
@@ -1209,9 +1219,9 @@ sub parse_date {
        $date{'hour_local'} = $hour;
        $date{'minute_local'} = $min;
        $date{'tz_local'} = $tz;
-       $date{'iso-tz'} = sprintf ("%04d-%02d-%02d %02d:%02d:%02d %s",
-                                  1900+$year, $mon+1, $mday,
-                                  $hour, $min, $sec, $tz);
+       $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
+                                 1900+$year, $mon+1, $mday,
+                                 $hour, $min, $sec, $tz);
        return %date;
 }
 
@@ -1284,8 +1294,9 @@ sub parse_commit {
                        $co{'author'} = $1;
                        $co{'author_epoch'} = $2;
                        $co{'author_tz'} = $3;
-                       if ($co{'author'} =~ m/^([^<]+) </) {
-                               $co{'author_name'} = $1;
+                       if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'author_name'}  = $1;
+                               $co{'author_email'} = $2;
                        } else {
                                $co{'author_name'} = $co{'author'};
                        }
@@ -1294,7 +1305,12 @@ sub parse_commit {
                        $co{'committer_epoch'} = $2;
                        $co{'committer_tz'} = $3;
                        $co{'committer_name'} = $co{'committer'};
-                       $co{'committer_name'} =~ s/ <.*//;
+                       if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
+                               $co{'committer_name'}  = $1;
+                               $co{'committer_email'} = $2;
+                       } else {
+                               $co{'committer_name'} = $co{'committer'};
+                       }
                }
        }
        if (!defined $co{'tree'}) {
@@ -1672,14 +1688,17 @@ EOF
                }
        }
        if (defined $project) {
-               printf('<link rel="alternate" title="%s log" '.
-                      'href="%s" type="application/rss+xml"/>'."\n",
+               printf('<link rel="alternate" title="%s log RSS feed" '.
+                      'href="%s" type="application/rss+xml" />'."\n",
                       esc_param($project), href(action=>"rss"));
+               printf('<link rel="alternate" title="%s log Atom feed" '.
+                      'href="%s" type="application/atom+xml" />'."\n",
+                      esc_param($project), href(action=>"atom"));
        } else {
                printf('<link rel="alternate" title="%s projects list" '.
                       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
                       $site_name, href(project=>undef, action=>"project_index"));
-               printf('<link rel="alternate" title="%s projects logs" '.
+               printf('<link rel="alternate" title="%s projects feeds" '.
                       'href="%s" type="text/x-opml"/>'."\n",
                       $site_name, href(project=>undef, action=>"opml"));
        }
@@ -1745,7 +1764,9 @@ sub git_footer_html {
                        print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
                }
                print $cgi->a({-href => href(action=>"rss"),
-                             -class => "rss_logo"}, "RSS") . "\n";
+                             -class => "rss_logo"}, "RSS") . " ";
+               print $cgi->a({-href => href(action=>"atom"),
+                             -class => "rss_logo"}, "Atom") . "\n";
        } else {
                print $cgi->a({-href => href(project=>undef, action=>"opml"),
                              -class => "rss_logo"}, "OPML") . " ";
@@ -2084,7 +2105,11 @@ sub git_difftree_body {
                                # link to patch
                                $patchno++;
                                print $cgi->a({-href => "#patch$patchno"}, "patch");
+                               print " | ";
                        }
+                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff{'file'})},
+                                     "blob") . " | ";
                        print "</td>\n";
 
                } elsif ($diff{'status'} eq "D") { # deleted
@@ -2104,13 +2129,11 @@ sub git_difftree_body {
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
                                                     hash_base=>$parent, file_name=>$diff{'file'})},
-                                     "blob") . " | ";
+                                     "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=>"blame", hash_base=>$parent,
+                                                            file_name=>$diff{'file'})},
+                                             "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
                                                     file_name=>$diff{'file'})},
@@ -2155,13 +2178,12 @@ sub git_difftree_body {
                                      " | ";
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                    hash_base=>$hash, file_name=>$diff{'file'})},
-                                     "blob") . " | ";
+                                                    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=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff{'file'})},
+                                             "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
                                                     file_name=>$diff{'file'})},
@@ -2200,17 +2222,16 @@ sub git_difftree_body {
                                              "diff") .
                                      " | ";
                        }
-                       print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
-                                                    hash_base=>$parent, file_name=>$diff{'from_file'})},
-                                     "blob") . " | ";
+                       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=>"blame", hash_base=>$hash,
+                                                            file_name=>$diff{'to_file'})},
+                                             "blame") . " | ";
                        }
-                       print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
-                                                   file_name=>$diff{'from_file'})},
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
+                                                   file_name=>$diff{'to_file'})},
                                      "history");
                        print "</td>\n";
 
@@ -2420,6 +2441,7 @@ sub git_project_list_body {
                ($pr->{'age'}, $pr->{'age_string'}) = @aa;
                if (!defined $pr->{'descr'}) {
                        my $descr = git_get_project_description($pr->{'path'}) || "";
+                       $pr->{'descr_long'} = to_utf8($descr);
                        $pr->{'descr'} = chop_str($descr, 25, 5);
                }
                if (!defined $pr->{'owner'}) {
@@ -2455,7 +2477,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'project'),
-                                      -class => "header"}, "Project") .
+                                      -class => "header"}, "Project") .
                              "</th>\n";
                }
                if ($order eq "descr") {
@@ -2464,7 +2486,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'descr'),
-                                      -class => "header"}, "Description") .
+                                      -class => "header"}, "Description") .
                              "</th>\n";
                }
                if ($order eq "owner") {
@@ -2473,7 +2495,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'owner'),
-                                      -class => "header"}, "Owner") .
+                                      -class => "header"}, "Owner") .
                              "</th>\n";
                }
                if ($order eq "age") {
@@ -2482,7 +2504,7 @@ sub git_project_list_body {
                } else {
                        print "<th>" .
                              $cgi->a({-href => href(project=>undef, order=>'age'),
-                                      -class => "header"}, "Last Change") .
+                                      -class => "header"}, "Last Change") .
                              "</th>\n";
                }
                print "<th></th>\n" .
@@ -2507,7 +2529,9 @@ sub git_project_list_body {
                }
                print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
-                     "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
+                     "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
+                                       -class => "list", -title => $pr->{'descr_long'}},
+                                       esc_html($pr->{'descr'})) . "</td>\n" .
                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
                      $pr->{'age_string'} . "</td>\n" .
@@ -2910,8 +2934,8 @@ sub git_tag {
        print "<div class=\"page_body\">";
        my $comment = $tag{'comment'};
        foreach my $line (@$comment) {
-               chomp($line);
-               print esc_html($line) . "<br/>\n";
+               chomp $line;
+               print esc_html($line, -nbsp=>1) . "<br/>\n";
        }
        print "</div>\n";
        git_footer_html();
@@ -2980,7 +3004,7 @@ HTML
                        }
                }
                my $data = $_;
-               chomp($data);
+               chomp $data;
                my $rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
                my %date = parse_date($meta->{'author-time'},
@@ -3205,10 +3229,13 @@ sub git_blob {
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(undef, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
-       if ($mimetype !~ m/^text\//) {
+       if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)!) {
                close $fd;
                return git_blob_plain($mimetype);
        }
+       # we can have blame only for text/* mimetype
+       $have_blame &&= ($mimetype =~ m!^text/!);
+
        git_header_html(undef, $expires);
        my $formats_nav = '';
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
@@ -3245,13 +3272,24 @@ sub git_blob {
        }
        git_print_page_path($file_name, "blob", $hash_base);
        print "<div class=\"page_body\">\n";
-       my $nr;
-       while (my $line = <$fd>) {
-               chomp $line;
-               $nr++;
-               $line = untabify($line);
-               printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
-                      $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+       if ($mimetype =~ m!^text/!) {
+               my $nr;
+               while (my $line = <$fd>) {
+                       chomp $line;
+                       $nr++;
+                       $line = untabify($line);
+                       printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
+                              $nr, $nr, $nr, esc_html($line, -nbsp=>1);
+               }
+       } elsif ($mimetype =~ m!^image/!) {
+               print qq!<img type="$mimetype"!;
+               if ($file_name) {
+                       print qq! alt="$file_name" title="$file_name"!;
+               }
+               print qq! src="! .
+                     href(action=>"blob_plain", hash=>$hash,
+                          hash_base=>$hash_base, file_name=>$file_name) .
+                     qq!" />\n!;
        }
        close $fd
                or print "Reading blob failed.\n";
@@ -4150,70 +4188,237 @@ sub git_shortlog {
 }
 
 ## ......................................................................
-## feeds (RSS, OPML)
+## feeds (RSS, Atom; OPML)
 
-sub git_rss {
-       # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+sub git_feed {
+       my $format = shift || 'atom';
+       my ($have_blame) = gitweb_check_feature('blame');
+
+       # Atom: http://www.atomenabled.org/developers/syndication/
+       # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+       if ($format ne 'rss' && $format ne 'atom') {
+               die_error(undef, "Unknown web feed format");
+       }
+
+       # log/feed of current (HEAD) branch, log of given branch, history of file/directory
+       my $head = $hash || 'HEAD';
        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
-               git_get_head_hash($project), "--"
+               $head, "--", (defined $file_name ? $file_name : ())
                or die_error(undef, "Open git-rev-list failed");
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-rev-list failed");
-       print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
-       print <<XML;
-<?xml version="1.0" encoding="utf-8"?>
+
+       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($revlist[0])) {
+               %latest_commit = parse_commit($revlist[0]);
+               %latest_date   = parse_date($latest_commit{'author_epoch'});
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8',
+                       -last_modified => $latest_date{'rfc2822'});
+       } else {
+               print $cgi->header(
+                       -type => $content_type,
+                       -charset => 'utf-8');
+       }
+
+       # 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";
+       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");
+       }
+       print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
+       if ($format eq 'rss') {
+               print <<XML;
 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
 <channel>
-<title>$project $my_uri $my_url</title>
-<link>${\esc_html("$my_url?p=$project;a=summary")}</link>
-<description>$project log</description>
-<language>en</language>
 XML
+               print "<title>$title</title>\n" .
+                     "<link>$alt_url</link>\n" .
+                     "<description>$descr</description>\n" .
+                     "<language>en</language>\n";
+       } elsif ($format eq 'atom') {
+               print <<XML;
+<feed xmlns="http://www.w3.org/2005/Atom">
+XML
+               print "<title>$title</title>\n" .
+                     "<subtitle>$descr</subtitle>\n" .
+                     '<link rel="alternate" type="text/html" href="' .
+                     $alt_url . '" />' . "\n" .
+                     '<link rel="self" type="' . $content_type . '" href="' .
+                     $cgi->self_url() . '" />' . "\n" .
+                     "<id>" . href(-full=>1) . "</id>\n" .
+                     # use project owner for feed author
+                     "<author><name>$owner</name></author>\n";
+               if (defined $favicon) {
+                       print "<icon>" . esc_url($favicon) . "</icon>\n";
+               }
+               if (defined $logo_url) {
+                       # not twice as wide as tall: 72 x 27 pixels
+                       print "<logo>" . esc_url($logo) . "</logo>\n";
+               }
+               if (! %latest_date) {
+                       # dummy date to keep the feed valid until commits trickle in:
+                       print "<updated>1970-01-01T00:00:00Z</updated>\n";
+               } else {
+                       print "<updated>$latest_date{'iso-8601'}</updated>\n";
+               }
+       }
 
+       # contents
        for (my $i = 0; $i <= $#revlist; $i++) {
                my $commit = $revlist[$i];
                my %co = parse_commit($commit);
                # 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)) {
+               if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
                        last;
                }
-               my %cd = parse_date($co{'committer_epoch'});
+               my %cd = parse_date($co{'author_epoch'});
+
+               # get list of changed files
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
-                       $co{'parent'}, $co{'id'}, "--"
+                       $co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ())
                        or next;
                my @difftree = map { chomp; $_ } <$fd>;
                close $fd
                        or next;
-               print "<item>\n" .
-                     "<title>" .
-                     sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
-                     "</title>\n" .
-                     "<author>" . esc_html($co{'author'}) . "</author>\n" .
-                     "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
-                     "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
-                     "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
-                     "<description>" . esc_html($co{'title'}) . "</description>\n" .
-                     "<content:encoded>" .
-                     "<![CDATA[\n";
+
+               # print element (entry, item)
+               my $co_url = href(-full=>1, action=>"commit", hash=>$commit);
+               if ($format eq 'rss') {
+                       print "<item>\n" .
+                             "<title>" . esc_html($co{'title'}) . "</title>\n" .
+                             "<author>" . esc_html($co{'author'}) . "</author>\n" .
+                             "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
+                             "<guid isPermaLink=\"true\">$co_url</guid>\n" .
+                             "<link>$co_url</link>\n" .
+                             "<description>" . esc_html($co{'title'}) . "</description>\n" .
+                             "<content:encoded>" .
+                             "<![CDATA[\n";
+               } elsif ($format eq 'atom') {
+                       print "<entry>\n" .
+                             "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
+                             "<updated>$cd{'iso-8601'}</updated>\n" .
+                             "<author>\n" .
+                             "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
+                       if ($co{'author_email'}) {
+                               print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
+                       }
+                       print "</author>\n" .
+                             # use committer for contributor
+                             "<contributor>\n" .
+                             "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
+                       if ($co{'committer_email'}) {
+                               print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
+                       }
+                       print "</contributor>\n" .
+                             "<published>$cd{'iso-8601'}</published>\n" .
+                             "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
+                             "<id>$co_url</id>\n" .
+                             "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
+                             "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
+               }
                my $comment = $co{'comment'};
+               print "<pre>\n";
                foreach my $line (@$comment) {
-                       $line = to_utf8($line);
-                       print "$line<br/>\n";
+                       $line = esc_html($line);
+                       print "$line\n";
                }
-               print "<br/>\n";
-               foreach my $line (@difftree) {
-                       if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
-                               next;
+               print "</pre><ul>\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 "<li>" .
+                             "[" .
+                             $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');
                        }
-                       my $file = esc_path(unquote($7));
-                       $file = to_utf8($file);
-                       print "$file<br/>\n";
+                       $file = esc_path($file);
+                       print "] ".
+                             "$file</li>\n";
                }
-               print "]]>\n" .
-                     "</content:encoded>\n" .
-                     "</item>\n";
+               if ($format eq 'rss') {
+                       print "</ul>]]>\n" .
+                             "</content:encoded>\n" .
+                             "</item>\n";
+               } elsif ($format eq 'atom') {
+                       print "</ul>\n</div>\n" .
+                             "</content>\n" .
+                             "</entry>\n";
+               }
+       }
+
+       # end of feed
+       if ($format eq 'rss') {
+               print "</channel>\n</rss>\n";
+       }       elsif ($format eq 'atom') {
+               print "</feed>\n";
        }
-       print "</channel></rss>";
+}
+
+sub git_rss {
+       git_feed('rss');
+}
+
+sub git_atom {
+       git_feed('atom');
 }
 
 sub git_opml {