Mention that pull can work locally in the synopsis
[gitweb.git] / gitweb / gitweb.perl
index 5aed66185049339125fcbecee62d9d42d5a8b5f3..ec46b8091bac2fa890c5e7356482b0842a7d829a 100755 (executable)
@@ -39,7 +39,8 @@
 
 # name of your site or organization to appear in page titles
 # replace this with something more descriptive for clearer bookmarks
-our $site_name = "++GITWEB_SITENAME++" || $ENV{'SERVER_NAME'} || "Untitled";
+our $site_name = "++GITWEB_SITENAME++"
+                 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
 
 # filename of html text to include at top of each page
 our $site_header = "++GITWEB_SITE_HEADER++";
 
 # URI of stylesheets
 our @stylesheets = ("++GITWEB_CSS++");
-our $stylesheet;
-# default is not to define style sheet, but it can be overwritten later
-undef $stylesheet;
-
-# URI of default stylesheet
-our $stylesheet = "++GITWEB_CSS++";
+# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
+our $stylesheet = undef;
 # URI of GIT logo (72x27 size)
 our $logo = "++GITWEB_LOGO++";
 # URI of GIT favicon, assumed to be image/png type
@@ -79,7 +76,7 @@
 
 # list of git base URLs used for URL to where fetch project from,
 # i.e. full URL is "$git_base_url/$project"
-our @git_base_url_list = ("++GITWEB_BASE_URL++");
+our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
 
 # default blob_plain mimetype and default charset for text/plain blob
 our $default_blob_plain_mimetype = 'text/plain';
@@ -342,6 +339,13 @@ sub check_export_ok {
        $searchtext = quotemeta $searchtext;
 }
 
+our $searchtype = $cgi->param('st');
+if (defined $searchtype) {
+       if ($searchtype =~ m/[^a-z]/) {
+               die_error(undef, "Invalid searchtype parameter");
+       }
+}
+
 # now read PATH_INFO and use it as alternative to parameters
 sub evaluate_path_info {
        return if defined $project;
@@ -406,6 +410,7 @@ sub evaluate_path_info {
        "log" => \&git_log,
        "rss" => \&git_rss,
        "search" => \&git_search,
+       "search_help" => \&git_search_help,
        "shortlog" => \&git_shortlog,
        "summary" => \&git_summary,
        "tag" => \&git_tag,
@@ -455,6 +460,7 @@ (%)
                page => "pg",
                order => "o",
                searchtext => "s",
+               searchtype => "st",
        );
        my %mapping = @mapping;
 
@@ -970,6 +976,24 @@ sub git_get_project_owner {
        return $owner;
 }
 
+sub git_get_last_activity {
+       my ($path) = @_;
+       my $fd;
+
+       $git_dir = "$projectroot/$path";
+       open($fd, "-|", git_cmd(), 'for-each-ref',
+            '--format=%(refname) %(committer)',
+            '--sort=-committerdate',
+            'refs/heads') or return;
+       my $most_recent = <$fd>;
+       close $fd or return;
+       if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
+               my $timestamp = $1;
+               my $age = time - $timestamp;
+               return ($age, age_string($age));
+       }
+}
+
 sub git_get_references {
        my $type = shift || "";
        my %refs;
@@ -1035,6 +1059,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);
        return %date;
 }
 
@@ -1073,24 +1100,6 @@ sub parse_tag {
        return %tag
 }
 
-sub git_get_last_activity {
-       my ($path) = @_;
-       my $fd;
-
-       $git_dir = "$projectroot/$path";
-       open($fd, "-|", git_cmd(), 'for-each-ref',
-            '--format=%(refname) %(committer)',
-            '--sort=-committerdate',
-            'refs/heads') or return;
-       my $most_recent = <$fd>;
-       close $fd or return;
-       if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
-               my $timestamp = $1;
-               my $age = time - $timestamp;
-               return ($age, age_string($age));
-       }
-}
-
 sub parse_commit {
        my $commit_id = shift;
        my $commit_text = shift;
@@ -1420,7 +1429,7 @@ sub git_header_html {
        my $status = shift || "200 OK";
        my $expires = shift;
 
-       my $title = "$site_name git";
+       my $title = "$site_name";
        if (defined $project) {
                $title .= " - $project";
                if (defined $action) {
@@ -1524,6 +1533,10 @@ sub git_header_html {
                      $cgi->hidden(-name => "p") . "\n" .
                      $cgi->hidden(-name => "a") . "\n" .
                      $cgi->hidden(-name => "h") . "\n" .
+                     $cgi->popup_menu(-name => 'st', -default => 'commit',
+                                      -values => ['commit', 'author', 'committer', 'pickaxe']) .
+                     $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
+                     " search:\n",
                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
                      "</div>" .
                      $cgi->end_form() . "\n";
@@ -1762,15 +1775,6 @@ ($;%)
        }
 }
 
-sub git_print_simplified_log {
-       my $log = shift;
-       my $remove_title = shift;
-
-       git_print_log($log,
-               -final_empty_line=> 1,
-               -remove_title => $remove_title);
-}
-
 # print tree entry (row of git_tree), but without encompassing <tr> element
 sub git_print_tree_entry {
        my ($t, $basedir, $hash_base, $have_blame) = @_;
@@ -2516,6 +2520,14 @@ 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";
+                       print $_ while (<$fd>);
+                       close $fd;
+               }
+       }
+
        open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
                git_get_head_hash($project)
                or die_error(undef, "Open git-rev-list failed");
@@ -2594,7 +2606,8 @@ sub git_blame2 {
        if ($ftype !~ "blob") {
                die_error("400 Bad Request", "Object is not a blob");
        }
-       open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base)
+       open ($fd, "-|", git_cmd(), "blame", '-p', '--',
+             $file_name, $hash_base)
                or die_error(undef, "Open git-blame failed");
        git_header_html();
        my $formats_nav =
@@ -2618,33 +2631,52 @@ sub git_blame2 {
 <table class="blame">
 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 HTML
-       while (<$fd>) {
-               my ($full_rev, $author, $date, $lineno, $data) =
-                       /^([0-9a-f]{40}).*?\s\((.*?)\s+([-\d]+ [:\d]+ [-+\d]+)\s+(\d+)\)\s(.*)/;
+       my %metainfo = ();
+       while (1) {
+               $_ = <$fd>;
+               last unless defined $_;
+               my ($full_rev, $orig_lineno, $lineno, $group_size) =
+                   /^([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+) (.*)$/) {
+                               $meta->{$1} = $2;
+                       }
+               }
+               my $data = $_;
                my $rev = substr($full_rev, 0, 8);
-               my $print_c8 = 0;
-
-               if (!defined $last_rev) {
-                       $last_rev = $full_rev;
-                       $print_c8 = 1;
-               } elsif ($last_rev ne $full_rev) {
-                       $last_rev = $full_rev;
+               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 % $num_colors;
-                       $print_c8 = 1;
                }
                print "<tr class=\"$rev_color[$current_color]\">\n";
-               print "<td class=\"sha1\"";
-               if ($print_c8 == 1) {
+               if ($group_size) {
+                       print "<td class=\"sha1\"";
                        print " title=\"$author, $date\"";
-               }
-               print ">";
-               if ($print_c8 == 1) {
-                       print $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
+                       print " rowspan=\"$group_size\"" if ($group_size > 1);
+                       print ">";
+                       print $cgi->a({-href => href(action=>"commit",
+                                                    hash=>$full_rev,
+                                                    file_name=>$file_name)},
                                      esc_html($rev));
+                       print "</td>\n";
                }
-               print "</td>\n";
-               print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
-                     esc_html($lineno) . "</a></td>\n";
+               my $blamed = href(action => 'blame',
+                                 file_name => $meta->{'filename'},
+                                 hash_base => $full_rev);
+               print "<td class=\"linenr\">";
+               print $cgi->a({ -href => "$blamed#l$orig_lineno",
+                               -id => "l$lineno",
+                               -class => "linenr" },
+                             esc_html($lineno));
+               print "</td>";
                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
                print "</tr>\n";
        }
@@ -3079,7 +3111,7 @@ sub git_log {
                      "</div>\n";
 
                print "<div class=\"log_body\">\n";
-               git_print_simplified_log($co{'comment'});
+               git_print_log($co{'comment'}, -final_empty_line=> 1);
                print "</div>\n";
        }
        git_footer_html();
@@ -3097,7 +3129,8 @@ sub git_commit {
        if (!defined $parent) {
                $parent = "--root";
        }
-       open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
+       open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
+               @diff_opts, $parent, $hash
                or die_error(undef, "Open git-diff-tree failed");
        my @difftree = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-diff-tree failed");
@@ -3363,6 +3396,51 @@ sub git_commitdiff {
        if (!%co) {
                die_error(undef, "Unknown commit object");
        }
+
+       # we need to prepare $formats_nav before any parameter munging
+       my $formats_nav;
+       if ($format eq 'html') {
+               $formats_nav =
+                       $cgi->a({-href => href(action=>"commitdiff_plain",
+                                              hash=>$hash, hash_parent=>$hash_parent)},
+                               "raw");
+
+               if (defined $hash_parent) {
+                       # commitdiff with two commits given
+                       my $hash_parent_short = $hash_parent;
+                       if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
+                               $hash_parent_short = substr($hash_parent, 0, 7);
+                       }
+                       $formats_nav .=
+                               ' (from: ' .
+                               $cgi->a({-href => href(action=>"commitdiff",
+                                                      hash=>$hash_parent)},
+                                       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(action=>"commitdiff",
+                                                      hash=>$co{'parent'})},
+                                       esc_html(substr($co{'parent'}, 0, 7))) .
+                               ')';
+               } else {
+                       # merge commit
+                       $formats_nav .=
+                               ' (merge: ' .
+                               join(' ', map {
+                                       $cgi->a({-href => href(action=>"commitdiff",
+                                                              hash=>$_)},
+                                               esc_html(substr($_, 0, 7)));
+                               } @{$co{'parents'}} ) .
+                               ')';
+               }
+       }
+
        if (!defined $hash_parent) {
                $hash_parent = $co{'parent'} || '--root';
        }
@@ -3372,6 +3450,7 @@ sub git_commitdiff {
        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, $hash
                        or die_error(undef, "Open git-diff-tree failed");
 
@@ -3400,19 +3479,17 @@ sub git_commitdiff {
        if ($format eq 'html') {
                my $refs = git_get_references();
                my $ref = format_ref_marker($refs, $co{'id'});
-               my $formats_nav =
-                       $cgi->a({-href => href(action=>"commitdiff_plain",
-                                              hash=>$hash, hash_parent=>$hash_parent)},
-                               "raw");
 
                git_header_html(undef, $expires);
                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
                git_print_authorship(\%co);
                print "<div class=\"page_body\">\n";
-               print "<div class=\"log\">\n";
-               git_print_simplified_log($co{'comment'}, 1); # skip title
-               print "</div>\n"; # class="log"
+               if (@{$co{'comment'}} > 1) {
+                       print "<div class=\"log\">\n";
+                       git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
+                       print "</div>\n"; # class="log"
+               }
 
        } elsif ($format eq 'plain') {
                my $refs = git_get_references("tags");
@@ -3544,18 +3621,8 @@ sub git_search {
                die_error(undef, "Unknown commit object");
        }
 
-       my $commit_search = 1;
-       my $author_search = 0;
-       my $committer_search = 0;
-       my $pickaxe_search = 0;
-       if ($searchtext =~ s/^author\\://i) {
-               $author_search = 1;
-       } elsif ($searchtext =~ s/^committer\\://i) {
-               $committer_search = 1;
-       } elsif ($searchtext =~ s/^pickaxe\\://i) {
-               $commit_search = 0;
-               $pickaxe_search = 1;
-
+       $searchtype ||= 'commit';
+       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
                my ($have_pickaxe) = gitweb_check_feature('pickaxe');
@@ -3563,23 +3630,24 @@ sub git_search {
                        die_error('403 Permission denied', "Permission denied");
                }
        }
+
        git_header_html();
        git_print_page_nav('','', $hash,$co{'tree'},$hash);
        git_print_header_div('commit', esc_html($co{'title'}), $hash);
 
        print "<table cellspacing=\"0\">\n";
        my $alternate = 1;
-       if ($commit_search) {
+       if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
                $/ = "\0";
                open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next;
                while (my $commit_text = <$fd>) {
                        if (!grep m/$searchtext/i, $commit_text) {
                                next;
                        }
-                       if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
+                       if ($searchtype eq 'author' && !grep m/\nauthor .*$searchtext/i, $commit_text) {
                                next;
                        }
-                       if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
+                       if ($searchtype eq 'committer' && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
                                next;
                        }
                        my @commit_lines = split "\n", $commit_text;
@@ -3621,7 +3689,7 @@ sub git_search {
                close $fd;
        }
 
-       if ($pickaxe_search) {
+       if ($searchtype eq 'pickaxe') {
                $/ = "\n";
                my $git_command = git_cmd_str();
                open my $fd, "-|", "$git_command rev-list $hash | " .
@@ -3681,6 +3749,31 @@ sub git_search {
        git_footer_html();
 }
 
+sub git_search_help {
+       git_header_html();
+       git_print_page_nav('','', $hash,$hash,$hash);
+       print <<EOT;
+<dl>
+<dt><b>commit</b></dt>
+<dd>The commit messages and authorship information will be scanned for the given string.</dd>
+<dt><b>author</b></dt>
+<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.</dd>
+<dt><b>committer</b></dt>
+<dd>Name and e-mail of the committer and date of commit will be scanned for the given string.</dd>
+EOT
+       my ($have_pickaxe) = gitweb_check_feature('pickaxe');
+       if ($have_pickaxe) {
+               print <<EOT;
+<dt><b>pickaxe</b></dt>
+<dd>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.</dd>
+EOT
+       }
+       print "</dl>\n";
+       git_footer_html();
+}
+
 sub git_shortlog {
        my $head = git_get_head_hash($project);
        if (!defined $hash) {
@@ -3789,7 +3882,7 @@ sub git_opml {
 <?xml version="1.0" encoding="utf-8"?>
 <opml version="1.0">
 <head>
-  <title>$site_name Git OPML Export</title>
+  <title>$site_name OPML Export</title>
 </head>
 <body>
 <outline text="git RSS feeds">