Modify setup_path() to only add git_exec_path() to PATH
[gitweb.git] / gitweb / gitweb.perl
index 06da30c50186eaaaba59dde91b8b8a34f6b89840..931db4f7ebb7e789278ac3e20a8570d40873163e 100755 (executable)
@@ -190,7 +190,9 @@ BEGIN
        # if there is no 'sub' key (no feature-sub), then feature cannot be
        # overriden
        #
-       # use gitweb_check_feature(<feature>) to check if <feature> is enabled
+       # use gitweb_get_feature(<feature>) to retrieve the <feature> value
+       # (an array) or gitweb_check_feature(<feature>) to check if <feature>
+       # is enabled
 
        # Enable the 'blame' blob view, showing the last commit that modified
        # each line in the file. This can be very CPU-intensive.
@@ -201,7 +203,7 @@ BEGIN
        # $feature{'blame'}{'override'} = 1;
        # and in project config gitweb.blame = 0|1;
        'blame' => {
-               'sub' => \&feature_blame,
+               'sub' => sub { feature_bool('blame', @_) },
                'override' => 0,
                'default' => [0]},
 
@@ -239,6 +241,7 @@ BEGIN
        # $feature{'grep'}{'override'} = 1;
        # and in project config gitweb.grep = 0|1;
        'grep' => {
+               'sub' => sub { feature_bool('grep', @_) },
                'override' => 0,
                'default' => [1]},
 
@@ -252,7 +255,7 @@ BEGIN
        # $feature{'pickaxe'}{'override'} = 1;
        # and in project config gitweb.pickaxe = 0|1;
        'pickaxe' => {
-               'sub' => \&feature_pickaxe,
+               'sub' => sub { feature_bool('pickaxe', @_) },
                'override' => 0,
                'default' => [1]},
 
@@ -297,10 +300,10 @@ BEGIN
 
        # The 'default' value consists of a list of triplets in the form
        # (label, link, position) where position is the label after which
-       # to inster the link and link is a format string where %n expands
+       # to insert the link and link is a format string where %n expands
        # to the project name, %f to the project path within the filesystem,
        # %h to the current hash (h gitweb parameter) and %b to the current
-       # hash base (hb gitweb parameter).
+       # hash base (hb gitweb parameter); %% expands to %.
 
        # To enable system wide have in $GITWEB_CONFIG e.g.
        # $feature{'actions'}{'default'} = [('graphiclog',
@@ -327,9 +330,24 @@ BEGIN
        'ctags' => {
                'override' => 0,
                'default' => [0]},
+
+       # The maximum number of patches in a patchset generated in patch
+       # view. Set this to 0 or undef to disable patch view, or to a
+       # negative number to remove any limit.
+
+       # To disable system wide have in $GITWEB_CONFIG
+       # $feature{'patches'}{'default'} = [0];
+       # To have project specific config enable override in $GITWEB_CONFIG
+       # $feature{'patches'}{'override'} = 1;
+       # and in project config gitweb.patches = 0|n;
+       # where n is the maximum number of patches allowed in a patchset.
+       'patches' => {
+               'sub' => \&feature_patches,
+               'override' => 0,
+               'default' => [16]},
 );
 
-sub gitweb_check_feature {
+sub gitweb_get_feature {
        my ($name) = @_;
        return unless exists $feature{$name};
        my ($sub, $override, @defaults) = (
@@ -344,16 +362,33 @@ sub gitweb_check_feature {
        return $sub->(@defaults);
 }
 
-sub feature_blame {
-       my ($val) = git_get_project_config('blame', '--bool');
+# A wrapper to check if a given feature is enabled.
+# With this, you can say
+#
+#   my $bool_feat = gitweb_check_feature('bool_feat');
+#   gitweb_check_feature('bool_feat') or somecode;
+#
+# instead of
+#
+#   my ($bool_feat) = gitweb_get_feature('bool_feat');
+#   (gitweb_get_feature('bool_feat'))[0] or somecode;
+#
+sub gitweb_check_feature {
+       return (gitweb_get_feature(@_))[0];
+}
+
+
+sub feature_bool {
+       my $key = shift;
+       my ($val) = git_get_project_config($key, '--bool');
 
        if ($val eq 'true') {
-               return 1;
+               return (1);
        } elsif ($val eq 'false') {
-               return 0;
+               return (0);
        }
 
-       return $_[0];
+       return ($_[0]);
 }
 
 sub feature_snapshot {
@@ -368,25 +403,11 @@ sub feature_snapshot {
        return @fmts;
 }
 
-sub feature_grep {
-       my ($val) = git_get_project_config('grep', '--bool');
-
-       if ($val eq 'true') {
-               return (1);
-       } elsif ($val eq 'false') {
-               return (0);
-       }
+sub feature_patches {
+       my @val = (git_get_project_config('patches', '--int'));
 
-       return ($_[0]);
-}
-
-sub feature_pickaxe {
-       my ($val) = git_get_project_config('pickaxe', '--bool');
-
-       if ($val eq 'true') {
-               return (1);
-       } elsif ($val eq 'false') {
-               return (0);
+       if (@val) {
+               return @val;
        }
 
        return ($_[0]);
@@ -485,6 +506,8 @@ sub filter_snapshot_fmts {
        "heads" => \&git_heads,
        "history" => \&git_history,
        "log" => \&git_log,
+       "patch" => \&git_patch,
+       "patches" => \&git_patches,
        "rss" => \&git_rss,
        "atom" => \&git_atom,
        "search" => \&git_search,
@@ -622,6 +645,45 @@ sub evaluate_path_info {
                        $input_params{'hash_parent'} ||= $parentrefname;
                }
        }
+
+       # for the snapshot action, we allow URLs in the form
+       # $project/snapshot/$hash.ext
+       # where .ext determines the snapshot and gets removed from the
+       # passed $refname to provide the $hash.
+       #
+       # To be able to tell that $refname includes the format extension, we
+       # require the following two conditions to be satisfied:
+       # - the hash input parameter MUST have been set from the $refname part
+       #   of the URL (i.e. they must be equal)
+       # - the snapshot format MUST NOT have been defined already (e.g. from
+       #   CGI parameter sf)
+       # It's also useless to try any matching unless $refname has a dot,
+       # so we check for that too
+       if (defined $input_params{'action'} &&
+               $input_params{'action'} eq 'snapshot' &&
+               defined $refname && index($refname, '.') != -1 &&
+               $refname eq $input_params{'hash'} &&
+               !defined $input_params{'snapshot_format'}) {
+               # We loop over the known snapshot formats, checking for
+               # extensions. Allowed extensions are both the defined suffix
+               # (which includes the initial dot already) and the snapshot
+               # format key itself, with a prepended dot
+               while (my ($fmt, %opt) = each %known_snapshot_formats) {
+                       my $hash = $refname;
+                       my $sfx;
+                       $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
+                       next unless $sfx = $1;
+                       # a valid suffix was found, so set the snapshot format
+                       # and reset the hash parameter
+                       $input_params{'snapshot_format'} = $fmt;
+                       $input_params{'hash'} = $hash;
+                       # we also set the format suffix to the one requested
+                       # in the URL: this way a request for e.g. .tgz returns
+                       # a .tgz instead of a .tar.gz
+                       $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
+                       last;
+               }
+       }
 }
 evaluate_path_info();
 
@@ -727,6 +789,10 @@ sub evaluate_path_info {
 our $git_dir;
 $git_dir = "$projectroot/$project" if $project;
 
+# list of supported snapshot formats
+our @snapshot_fmts = gitweb_get_feature('snapshot');
+@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
+
 # dispatch
 if (!defined $action) {
        if (defined $hash) {
@@ -767,13 +833,14 @@ (%)
                }
        }
 
-       my ($use_pathinfo) = gitweb_check_feature('pathinfo');
-       if ($use_pathinfo) {
+       my $use_pathinfo = gitweb_check_feature('pathinfo');
+       if ($use_pathinfo and defined $params{'project'}) {
                # try to put as many parameters as possible in PATH_INFO:
                #   - project name
                #   - action
                #   - hash_parent or hash_parent_base:/file_parent
                #   - hash or hash_base:/filename
+               #   - the snapshot_format as an appropriate suffix
 
                # When the script is the root DirectoryIndex for the domain,
                # $href here would be something like http://gitweb.example.com/
@@ -782,9 +849,13 @@ (%)
                $href =~ s,/$,,;
 
                # Then add the project name, if present
-               $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
+               $href .= "/".esc_url($params{'project'});
                delete $params{'project'};
 
+               # since we destructively absorb parameters, we keep this
+               # boolean that remembers if we're handling a snapshot
+               my $is_snapshot = $params{'action'} eq 'snapshot';
+
                # Summary just uses the project path URL, any other action is
                # added to the URL
                if (defined $params{'action'}) {
@@ -824,6 +895,18 @@ (%)
                        $href .= esc_url($params{'hash'});
                        delete $params{'hash'};
                }
+
+               # If the action was a snapshot, we can absorb the
+               # snapshot_format parameter too
+               if ($is_snapshot) {
+                       my $fmt = $params{'snapshot_format'};
+                       # snapshot_format should always be defined when href()
+                       # is called, but just in case some code forgets, we
+                       # fall back to the default
+                       $fmt ||= $snapshot_fmts[0];
+                       $href .= $known_snapshot_formats{$fmt}{'suffix'};
+                       delete $params{'snapshot_format'};
+               }
        }
 
        # now encode the parameters explicitly
@@ -1652,8 +1735,6 @@ sub format_diff_line {
 # linked.  Pass the hash of the tree/commit to snapshot.
 sub format_snapshot_links {
        my ($hash) = @_;
-       my @snapshot_fmts = gitweb_check_feature('snapshot');
-       @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
        my $num_fmts = @snapshot_fmts;
        if ($num_fmts > 1) {
                # A parenthesized list of links bearing format names.
@@ -2043,7 +2124,7 @@ sub git_get_projects_list {
        $filter ||= '';
        $filter =~ s/\.git$//;
 
-       my ($check_forks) = gitweb_check_feature('forks');
+       my $check_forks = gitweb_check_feature('forks');
 
        if (-d $projects_list) {
                # search in directory
@@ -2070,8 +2151,9 @@ sub git_get_projects_list {
 
                                my $subdir = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
-                               if (check_export_ok("$projectroot/$filter/$subdir")) {
-                                       push @list, { path => ($filter ? "$filter/" : '') . $subdir };
+                               my $path = ($filter ? "$filter/" : '') . $subdir;
+                               if (check_export_ok("$projectroot/$path")) {
+                                       push @list, { path => $path };
                                        $File::Find::prune = 1;
                                }
                        },
@@ -2682,6 +2764,15 @@ sub get_file_owner {
        return to_utf8($owner);
 }
 
+# assume that file exists
+sub insert_file {
+       my $filename = shift;
+
+       open my $fd, '<', $filename;
+       print map { to_utf8($_) } <$fd>;
+       close $fd;
+}
+
 ## ......................................................................
 ## mimetype related functions
 
@@ -2870,9 +2961,7 @@ sub git_header_html {
              "<body>\n";
 
        if (-f $site_header) {
-               open (my $fd, $site_header);
-               print <$fd>;
-               close $fd;
+               insert_file($site_header);
        }
 
        print "<div class=\"page_header\">\n" .
@@ -2889,7 +2978,7 @@ sub git_header_html {
        }
        print "</div>\n";
 
-       my ($have_search) = gitweb_check_feature('search');
+       my $have_search = gitweb_check_feature('search');
        if (defined $project && $have_search) {
                if (!defined $searchtext) {
                        $searchtext = "";
@@ -2903,7 +2992,7 @@ sub git_header_html {
                        $search_hash = "HEAD";
                }
                my $action = $my_uri;
-               my ($use_pathinfo) = gitweb_check_feature('pathinfo');
+               my $use_pathinfo = gitweb_check_feature('pathinfo');
                if ($use_pathinfo) {
                        $action .= "/".esc_url($project);
                }
@@ -2959,9 +3048,7 @@ sub git_footer_html {
        print "</div>\n"; # class="page_footer"
 
        if (-f $site_footer) {
-               open (my $fd, $site_footer);
-               print <$fd>;
-               close $fd;
+               insert_file($site_footer);
        }
 
        print "</body>\n" .
@@ -3026,15 +3113,20 @@ sub git_print_page_nav {
        $arg{'tree'}{'hash'} = $treehead if defined $treehead;
        $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
 
-       my @actions = gitweb_check_feature('actions');
+       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) = (shift(@actions), shift(@actions), shift(@actions));
+               my ($label, $link, $pos) = splice(@actions,0,3);
+               # insert
                @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
                # munch munch
-               $link =~ s#%n#$project#g;
-               $link =~ s#%f#$git_dir#g;
-               $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
-               $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
+               $link =~ s/%([%nfhb])/$repl{$1}/g;
                $arg{$label}{'_href'} = $link;
        }
 
@@ -3391,7 +3483,7 @@ sub is_patch_split {
 sub git_difftree_body {
        my ($difftree, $hash, @parents) = @_;
        my ($parent) = $parents[0];
-       my ($have_blame) = gitweb_check_feature('blame');
+       my $have_blame = gitweb_check_feature('blame');
        print "<div class=\"list_head\">\n";
        if ($#{$difftree} > 10) {
                print(($#{$difftree} + 1) . " files changed:\n");
@@ -3905,7 +3997,7 @@ sub git_project_list_body {
        # actually uses global variable $project
        my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
 
-       my ($check_forks) = gitweb_check_feature('forks');
+       my $check_forks = gitweb_check_feature('forks');
        my @projects = fill_project_list_info($projlist, $check_forks);
 
        $order ||= $default_projects_order;
@@ -4295,9 +4387,7 @@ sub git_project_list {
        git_header_html();
        if (-f $home_text) {
                print "<div class=\"index_include\">\n";
-               open (my $fd, $home_text);
-               print <$fd>;
-               close $fd;
+               insert_file($home_text);
                print "</div>\n";
        }
        print $cgi->startform(-method => "get") .
@@ -4365,7 +4455,7 @@ sub git_summary {
        my @taglist  = git_get_tags_list(16);
        my @headlist = git_get_heads_list(16);
        my @forklist;
-       my ($check_forks) = gitweb_check_feature('forks');
+       my $check_forks = gitweb_check_feature('forks');
 
        if ($check_forks) {
                @forklist = git_get_projects_list($project);
@@ -4394,7 +4484,7 @@ sub git_summary {
        }
 
        # Tag cloud
-       my $show_ctags = (gitweb_check_feature('ctags'))[0];
+       my $show_ctags = gitweb_check_feature('ctags');
        if ($show_ctags) {
                my $ctags = git_get_project_ctags($project);
                my $cloud = git_populate_project_tagcloud($ctags);
@@ -4409,13 +4499,10 @@ sub git_summary {
        print "</table>\n";
 
        if (-s "$projectroot/$project/README.html") {
-               if (open my $fd, "$projectroot/$project/README.html") {
-                       print "<div class=\"title\">readme</div>\n" .
-                             "<div class=\"readme\">\n";
-                       print $_ while (<$fd>);
-                       print "\n</div>\n"; # class="readme"
-                       close $fd;
-               }
+               print "<div class=\"title\">readme</div>\n" .
+                     "<div class=\"readme\">\n";
+               insert_file("$projectroot/$project/README.html");
+               print "\n</div>\n"; # class="readme"
        }
 
        # we need to request one more than 16 (0..15) to check if
@@ -4493,28 +4580,33 @@ sub git_tag {
 }
 
 sub git_blame {
-       my $fd;
-       my $ftype;
-
+       # permissions
        gitweb_check_feature('blame')
-           or die_error(403, "Blame view not allowed");
+               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);
+       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");
+               }
        }
-       $ftype = git_get_type($hash);
-       if ($ftype !~ "blob") {
-               die_error(400, "Object is not a blob");
-       }
-       open ($fd, "-|", git_cmd(), "blame", '-p', '--',
-             $file_name, $hash_base)
+
+       # run git-blame --porcelain
+       open my $fd, "-|", git_cmd(), "blame", '-p',
+               $hash_base, '--', $file_name
                or die_error(500, "Open git-blame failed");
+
+       # page header
        git_header_html();
        my $formats_nav =
                $cgi->a({-href => href(action=>"blob", -replay=>1)},
@@ -4528,42 +4620,46 @@ sub git_blame {
        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);
-       my @rev_color = (qw(light2 dark2));
+
+       # page body
+       my @rev_color = qw(light2 dark2);
        my $num_colors = scalar(@rev_color);
        my $current_color = 0;
-       my $last_rev;
+       my %metainfo = ();
+
        print <<HTML;
 <div class="page_body">
 <table class="blame">
 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 HTML
-       my %metainfo = ();
-       while (1) {
-               $_ = <$fd>;
-               last unless defined $_;
+ LINE:
+       while (my $line = <$fd>) {
+               chomp $line;
+               # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
+               # no <lines in group> for subsequent lines in group of lines
                my ($full_rev, $orig_lineno, $lineno, $group_size) =
-                   /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
+                  ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
                if (!exists $metainfo{$full_rev}) {
                        $metainfo{$full_rev} = {};
                }
                my $meta = $metainfo{$full_rev};
-               while (<$fd>) {
-                       last if (s/^\t//);
-                       if (/^(\S+) (.*)$/) {
+               my $data;
+               while ($data = <$fd>) {
+                       chomp $data;
+                       last if ($data =~ s/^\t//); # contents of line
+                       if ($data =~ /^(\S+) (.*)$/) {
                                $meta->{$1} = $2;
                        }
                }
-               my $data = $_;
-               chomp $data;
-               my $rev = substr($full_rev, 0, 8);
+               my $short_rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
-               my %date = parse_date($meta->{'author-time'},
-                                     $meta->{'author-tz'});
+               my %date =
+                       parse_date($meta->{'author-time'}, $meta->{'author-tz'});
                my $date = $date{'iso-tz'};
                if ($group_size) {
-                       $current_color = ++$current_color % $num_colors;
+                       $current_color = ($current_color + 1) % $num_colors;
                }
-               print "<tr class=\"$rev_color[$current_color]\">\n";
+               print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
                if ($group_size) {
                        print "<td class=\"sha1\"";
                        print " title=\"". esc_html($author) . ", $date\"";
@@ -4572,20 +4668,25 @@ sub git_blame {
                        print $cgi->a({-href => href(action=>"commit",
                                                     hash=>$full_rev,
                                                     file_name=>$file_name)},
-                                     esc_html($rev));
+                                     esc_html($short_rev));
                        print "</td>\n";
                }
-               open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
-                       or die_error(500, "Open git-rev-parse failed");
-               my $parent_commit = <$dd>;
-               close $dd;
-               chomp($parent_commit);
+               my $parent_commit;
+               if (!exists $meta->{'parent'}) {
+                       open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
+                               or die_error(500, "Open git-rev-parse failed");
+                       $parent_commit = <$dd>;
+                       close $dd;
+                       chomp($parent_commit);
+                       $meta->{'parent'} = $parent_commit;
+               } else {
+                       $parent_commit = $meta->{'parent'};
+               }
                my $blamed = href(action => 'blame',
                                  file_name => $meta->{'filename'},
                                  hash_base => $parent_commit);
                print "<td class=\"linenr\">";
                print $cgi->a({ -href => "$blamed#l$orig_lineno",
-                               -id => "l$lineno",
                                -class => "linenr" },
                              esc_html($lineno));
                print "</td>";
@@ -4596,6 +4697,8 @@ sub git_blame {
        print "</div>";
        close $fd
                or print "Reading blob failed\n";
+
+       # page footer
        git_footer_html();
 }
 
@@ -4684,7 +4787,7 @@ sub git_blob {
                $expires = "+1d";
        }
 
-       my ($have_blame) = gitweb_check_feature('blame');
+       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);
@@ -4777,7 +4880,7 @@ sub git_tree {
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
        my $basedir = '';
-       my ($have_blame) = gitweb_check_feature('blame');
+       my $have_blame = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                my @views_nav = ();
                if (defined $file_name) {
@@ -4855,20 +4958,17 @@ sub git_tree {
 }
 
 sub git_snapshot {
-       my @supported_fmts = gitweb_check_feature('snapshot');
-       @supported_fmts = filter_snapshot_fmts(@supported_fmts);
-
        my $format = $input_params{'snapshot_format'};
-       if (!@supported_fmts) {
+       if (!@snapshot_fmts) {
                die_error(403, "Snapshots not allowed");
        }
        # default to first supported snapshot format
-       $format ||= $supported_fmts[0];
+       $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 (!grep($_ eq $format, @supported_fmts)) {
+       } elsif (!grep($_ eq $format, @snapshot_fmts)) {
                die_error(403, "Unsupported snapshot format");
        }
 
@@ -4918,6 +5018,15 @@ sub git_log {
 
        my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
 
+       my ($patch_max) = gitweb_get_feature('patches');
+       if ($patch_max) {
+               if ($patch_max < 0 || @commitlist <= $patch_max) {
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(action=>"patches", -replay=>1)},
+                                       "patches");
+               }
+       }
+
        git_header_html();
        git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 
@@ -4997,6 +5106,11 @@ sub git_commit {
                        } @$parents ) .
                        ')';
        }
+       if (gitweb_check_feature('patches')) {
+               $formats_nav .= " | " .
+                       $cgi->a({-href => href(action=>"patch", -replay=>1)},
+                               "patch");
+       }
 
        if (!defined $parent) {
                $parent = "--root";
@@ -5206,43 +5320,9 @@ sub git_blobdiff {
                        or die_error(500, "Open git-diff-tree failed");
        }
 
-       # old/legacy style URI
-       if (!%diffinfo && # if new style URI failed
-           defined $hash && defined $hash_parent) {
-               # fake git-diff-tree raw output
-               $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
-               $diffinfo{'from_id'} = $hash_parent;
-               $diffinfo{'to_id'}   = $hash;
-               if (defined $file_name) {
-                       if (defined $file_parent) {
-                               $diffinfo{'status'} = '2';
-                               $diffinfo{'from_file'} = $file_parent;
-                               $diffinfo{'to_file'}   = $file_name;
-                       } else { # assume not renamed
-                               $diffinfo{'status'} = '1';
-                               $diffinfo{'from_file'} = $file_name;
-                               $diffinfo{'to_file'}   = $file_name;
-                       }
-               } else { # no filename given
-                       $diffinfo{'status'} = '2';
-                       $diffinfo{'from_file'} = $hash_parent;
-                       $diffinfo{'to_file'}   = $hash;
-               }
-
-               # non-textual hash id's can be cached
-               if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
-                   $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
-                       $expires = '+1d';
-               }
-
-               # open patch output
-               open $fd, "-|", git_cmd(), "diff", @diff_opts,
-                       '-p', ($format eq 'html' ? "--full-index" : ()),
-                       $hash_parent, $hash, "--"
-                       or die_error(500, "Open git-diff failed");
-       } else  {
-               die_error(400, "Missing one of the blob diff parameters")
-                       unless %diffinfo;
+       # 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
@@ -5307,7 +5387,14 @@ sub git_blobdiff_plain {
 }
 
 sub git_commitdiff {
-       my $format = shift || 'html';
+       my %params = @_;
+       my $format = $params{-format} || 'html';
+
+       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");
@@ -5322,6 +5409,11 @@ sub git_commitdiff {
                $formats_nav =
                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
                                "raw");
+               if ($patch_max) {
+                       $formats_nav .= " | " .
+                               $cgi->a({-href => href(action=>"patch", -replay=>1)},
+                                       "patch");
+               }
 
                if (defined $hash_parent &&
                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
@@ -5405,7 +5497,31 @@ sub git_commitdiff {
                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", '--encoding=utf8',
+                       '--stdout', @commit_spec
+                       or die_error(500, "Open git-format-patch failed");
        } else {
                die_error(400, "Unknown commitdiff format");
        }
@@ -5454,6 +5570,14 @@ sub git_commitdiff {
                        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
@@ -5475,11 +5599,25 @@ sub git_commitdiff {
                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('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 {
@@ -5778,7 +5916,7 @@ sub git_search_help {
 <dt><b>commit</b></dt>
 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
 EOT
-       my ($have_grep) = gitweb_check_feature('grep');
+       my $have_grep = gitweb_check_feature('grep');
        if ($have_grep) {
                print <<EOT;
 <dt><b>grep</b></dt>
@@ -5795,7 +5933,7 @@ sub git_search_help {
 <dt><b>committer</b></dt>
 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
 EOT
-       my ($have_pickaxe) = gitweb_check_feature('pickaxe');
+       my $have_pickaxe = gitweb_check_feature('pickaxe');
        if ($have_pickaxe) {
                print <<EOT;
 <dt><b>pickaxe</b></dt>
@@ -5832,6 +5970,14 @@ sub git_shortlog {
                        $cgi->a({-href => href(-replay=>1, page=>$page+1),
                                 -accesskey => "n", -title => "Alt-n"}, "next");
        }
+       my $patch_max = gitweb_check_feature('patches');
+       if ($patch_max) {
+               if ($patch_max < 0 || @commitlist <= $patch_max) {
+                       $paging_nav .= " &sdot; " .
+                               $cgi->a({-href => href(action=>"patches", -replay=>1)},
+                                       "patches");
+               }
+       }
 
        git_header_html();
        git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
@@ -5847,7 +5993,7 @@ sub git_shortlog {
 
 sub git_feed {
        my $format = shift || 'atom';
-       my ($have_blame) = gitweb_check_feature('blame');
+       my $have_blame = gitweb_check_feature('blame');
 
        # Atom: http://www.atomenabled.org/developers/syndication/
        # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
@@ -6076,7 +6222,11 @@ sub git_atom {
 sub git_opml {
        my @list = git_get_projects_list();
 
-       print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
+       print $cgi->header(
+               -type => 'text/xml',
+               -charset => 'utf-8',
+               -content_disposition => 'inline; filename="opml.xml"');
+
        print <<XML;
 <?xml version="1.0" encoding="utf-8"?>
 <opml version="1.0">
@@ -6100,8 +6250,8 @@ sub git_opml {
                }
 
                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
-               my $rss  = "$my_url?p=$proj{'path'};a=rss";
-               my $html = "$my_url?p=$proj{'path'};a=summary";
+               my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
+               my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
        }
        print <<XML;