diff options
author | Shawn O. Pearce <spearce@spearce.org> | 2007-01-14 02:44:18 -0500 |
---|---|---|
committer | Shawn O. Pearce <spearce@spearce.org> | 2007-01-14 02:44:18 -0500 |
commit | 1fcdd62adf81a172f45c7c6a58177212d500b9d9 (patch) | |
tree | 94acde078fd78c3d214fe09d45e85ed346a2f2d4 /gitweb | |
parent | 9938ffc53a15c755bbd3894c02492b940ea34c4c (diff) | |
parent | 696b1b507f8ff9e80a2edc4eced59ca8cdda920e (diff) | |
download | git-1fcdd62adf81a172f45c7c6a58177212d500b9d9.tar.gz git-1fcdd62adf81a172f45c7c6a58177212d500b9d9.tar.xz |
Merge branch 'master' into sp/fast-import
I'm bringing master in early so that the OBJ_OFS_DELTA implementation
is available as part of the topic. This way git-fast-import can
learn about this new slightly smaller and faster packfile format,
and can generate them directly rather than needing to have them be
repacked with git-pack-objects.
Due to the API changes in master during the period of development
of git-fast-import, a few minor tweaks to fast-import.c are needed
to produce a working merge. I've done them here as part of the
merge to ensure bisection always works.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
Diffstat (limited to 'gitweb')
-rw-r--r-- | gitweb/README | 49 | ||||
-rw-r--r-- | gitweb/git-favicon.png | bin | 0 -> 164 bytes | |||
-rw-r--r-- | gitweb/gitweb.css | 116 | ||||
-rwxr-xr-x | gitweb/gitweb.perl | 3919 |
4 files changed, 3096 insertions, 988 deletions
diff --git a/gitweb/README b/gitweb/README index 27c6dac14..e02e90f04 100644 --- a/gitweb/README +++ b/gitweb/README @@ -1,4 +1,5 @@ GIT web Interface +================= The one working on: http://www.kernel.org/git/ @@ -6,7 +7,8 @@ The one working on: From the git version 1.4.0 gitweb is bundled with git. -How to configure gitweb for your local system: +How to configure gitweb for your local system +--------------------------------------------- You can specify the following configuration variables when building GIT: * GITWEB_SITENAME @@ -24,11 +26,54 @@ You can specify the following configuration variables when building GIT: * GITWEB_LOGO Points to the location where you put git-logo.png on your web server. * GITWEB_CONFIG - This file will be loaded using 'require'. If the environment + This file will be loaded using 'require' and can be used to override any + of the options above as well as some other options - see the top of + 'gitweb.cgi' for their full list and description. If the environment $GITWEB_CONFIG is set when gitweb.cgi is executed the file in the environment variable will be loaded instead of the file specified when gitweb.cgi was created. + +Runtime gitweb configuration +---------------------------- + +You can adjust gitweb behaviour using the file specified in `GITWEB_CONFIG` +(defaults to 'gitweb_config.perl' in the same directory as the CGI). +See the top of 'gitweb.cgi' for the list of variables and some description. +The most notable thing that is not configurable at compile time are the +optional features, stored in the '%features' variable. You can find further +description on how to reconfigure the default features setting in your +`GITWEB_CONFIG` or per-project in `project.git/config` inside 'gitweb.cgi'. + + +Webserver configuration +----------------------- + +If you want to have one URL for both gitweb and your http:// +repositories, you can configure apache like this: + +<VirtualHost www:80> + ServerName git.domain.org + DocumentRoot /pub/git + RewriteEngine on + RewriteRule ^/(.*\.git/(?!/?(info|objects|refs)).*)?$ /cgi-bin/gitweb.cgi%{REQUEST_URI} [L,PT] + SetEnv GITWEB_CONFIG /etc/gitweb.conf +</VirtualHost> + +The above configuration expects your public repositories to live under +/pub/git and will serve them as http://git.domain.org/dir-under-pub-git, +both as cloneable GIT URL and as browseable gitweb interface. +If you then start your git-daemon with --base-path=/pub/git --export-all +then you can even use the git:// URL with exactly the same path. + +Setting the environment variable GITWEB_CONFIG will tell gitweb to use +the named file (i.e. in this example /etc/gitweb.conf) as a +configuration for gitweb. Perl variables defined in here will +override the defaults given at the head of the gitweb.perl (or +gitweb.cgi). Look at the comments in that file for information on +which variables and what they mean. + + Originally written by: Kay Sievers <kay.sievers@vrfy.org> diff --git a/gitweb/git-favicon.png b/gitweb/git-favicon.png Binary files differnew file mode 100644 index 000000000..de637c060 --- /dev/null +++ b/gitweb/git-favicon.png diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css index 901389585..7177c6e86 100644 --- a/gitweb/gitweb.css +++ b/gitweb/gitweb.css @@ -16,6 +16,18 @@ a:hover, a:visited, a:active { color: #880000; } +span.cntrl { + border: dashed #aaaaaa; + border-width: 1px; + padding: 0px 2px 0px 2px; + margin: 0px 2px 0px 2px; +} + +img.logo { + float: right; + border-width: 0px; +} + div.page_header { height: 25px; padding: 8px; @@ -42,6 +54,7 @@ div.page_nav a:visited { div.page_path { padding: 8px; + font-weight: bold; border: solid #d9d8d1; border-width: 0px 0px 1px; } @@ -115,13 +128,23 @@ div.list_head { font-style: italic; } +div.author_date { + padding: 8px; + border: solid #d9d8d1; + border-width: 0px 0px 1px 0px; + font-style: italic; +} + a.list { text-decoration: none; - font-weight: bold; color: #000000; } -table.tags a.list { +a.subject, a.name { + font-weight: bold; +} + +table.tags a.subject { font-weight: normal; } @@ -162,6 +185,12 @@ table.blame { border-collapse: collapse; } +table.blame td { + padding: 0px 5px; + font-size: 12px; + vertical-align: top; +} + th { padding: 2px 5px; font-size: 12px; @@ -269,25 +298,96 @@ td.mode { font-family: monospace; } +/* styling of diffs (patchsets): commitdiff and blobdiff views */ +div.diff.header, +div.diff.extended_header { + white-space: normal; +} + +div.diff.header { + font-weight: bold; + + background-color: #edece6; + + margin-top: 4px; + padding: 4px 0px 2px 0px; + border: solid #d9d8d1; + border-width: 1px 0px 1px 0px; +} + +div.diff.header a.path { + text-decoration: underline; +} + +div.diff.extended_header, +div.diff.extended_header a.path, +div.diff.extended_header a.hash { + color: #777777; +} + +div.diff.extended_header .info { + color: #b0b0b0; +} + +div.diff.extended_header { + background-color: #f6f5ee; + padding: 2px 0px 2px 0px; +} + +div.diff a.list, +div.diff a.path, +div.diff a.hash { + text-decoration: none; +} + +div.diff a.list:hover, +div.diff a.path:hover, +div.diff a.hash:hover { + text-decoration: underline; +} + +div.diff.to_file a.path, +div.diff.to_file { + color: #007000; +} + div.diff.add { color: #008800; } +div.diff.from_file a.path, +div.diff.from_file { + color: #aa0000; +} + div.diff.rem { color: #cc0000; } +div.diff.chunk_header a, div.diff.chunk_header { color: #990099; } -div.diff_info { - font-family: monospace; - color: #000099; - background-color: #edece6; - font-style: italic; +div.diff.chunk_header { + border: dotted #ffe0ff; + border-width: 1px 0px 0px 0px; + margin-top: 2px; +} + +div.diff.chunk_header span.chunk_info { + background-color: #ffeeff; } +div.diff.chunk_header span.section { + color: #aa22aa; +} + +div.diff.incomplete { + color: #cccccc; +} + + div.index_include { border: solid #d9d8d1; border-width: 0px 0px 1px; @@ -295,6 +395,8 @@ div.index_include { } div.search { + font-size: 12px; + font-weight: normal; margin: 4px 8px; position: absolute; top: 56px; diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 04282fa5e..88af2e638 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -15,8 +15,13 @@ use CGI::Carp qw(fatalsToBrowser); use Encode; use Fcntl ':mode'; use File::Find qw(); +use File::Basename qw(basename); binmode STDOUT, ':utf8'; +BEGIN { + CGI->compile() if $ENV{MOD_PERL}; +} + our $cgi = new CGI; our $version = "++GIT_VERSION++"; our $my_url = $cgi->url(); @@ -30,33 +35,52 @@ our $GIT = "++GIT_BINDIR++/git"; #our $projectroot = "/pub/scm"; our $projectroot = "++GITWEB_PROJECTROOT++"; -# location for temporary files needed for diffs -our $git_temp = "/tmp/gitweb"; - # target of the home link on top of all pages -our $home_link = $my_uri; +our $home_link = $my_uri || "/"; # string of the home link on top of all pages our $home_link_str = "++GITWEB_HOME_LINK_STR++"; # 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++"; # html text to include at home page our $home_text = "++GITWEB_HOMETEXT++"; - -# URI of default stylesheet -our $stylesheet = "++GITWEB_CSS++"; -# URI of GIT logo +# filename of html text to include at bottom of each page +our $site_footer = "++GITWEB_SITE_FOOTER++"; + +# URI of stylesheets +our @stylesheets = ("++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 +our $favicon = "++GITWEB_FAVICON++"; + +# URI and label (title) of GIT logo link +#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; +#our $logo_label = "git documentation"; +our $logo_url = "http://git.or.cz/"; +our $logo_label = "git homepage"; # source of projects list our $projects_list = "++GITWEB_LIST++"; +# show repository only if this file exists +# (only effective if this variable evaluates to true) +our $export_ok = "++GITWEB_EXPORT_OK++"; + +# only allow viewing of repositories also shown on the overview page +our $strict_export = "++GITWEB_STRICT_EXPORT++"; + # 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'; @@ -66,16 +90,198 @@ our $default_text_plain_charset = undef; # (relative to the current git repository) our $mimetypes_file = undef; +# You define site-wide feature defaults here; override them with +# $GITWEB_CONFIG as necessary. +our %feature = ( + # feature => { + # 'sub' => feature-sub (subroutine), + # 'override' => allow-override (boolean), + # 'default' => [ default options...] (array reference)} + # + # if feature is overridable (it means that allow-override has true value, + # then feature-sub will be called with default options as parameters; + # return value of feature-sub indicates if to enable specified feature + # + # use 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. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'blame'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'blame'}{'override'} = 1; + # and in project config gitweb.blame = 0|1; + 'blame' => { + 'sub' => \&feature_blame, + 'override' => 0, + 'default' => [0]}, + + # Enable the 'snapshot' link, providing a compressed tarball of any + # tree. This can potentially generate high traffic if you have large + # project. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'snapshot'}{'default'} = [undef]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'snapshot'}{'override'} = 1; + # and in project config gitweb.snapshot = none|gzip|bzip2; + 'snapshot' => { + 'sub' => \&feature_snapshot, + 'override' => 0, + # => [content-encoding, suffix, program] + 'default' => ['x-gzip', 'gz', 'gzip']}, + + # Enable text search, which will list the commits which match author, + # committer or commit text to a given string. Enabled by default. + 'search' => { + 'override' => 0, + 'default' => [1]}, + + # Enable the pickaxe search, which will list the commits that modified + # a given string in a file. This can be practical and quite faster + # alternative to 'blame', but still potentially CPU-intensive. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pickaxe'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'pickaxe'}{'override'} = 1; + # and in project config gitweb.pickaxe = 0|1; + 'pickaxe' => { + 'sub' => \&feature_pickaxe, + 'override' => 0, + 'default' => [1]}, + + # Make gitweb use an alternative format of the URLs which can be + # more readable and natural-looking: project name is embedded + # directly in the path and the query string contains other + # auxiliary information. All gitweb installations recognize + # URL in either format; this configures in which formats gitweb + # generates links. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pathinfo'}{'default'} = [1]; + # Project specific override is not supported. + + # Note that you will need to change the default location of CSS, + # favicon, logo and possibly other files to an absolute URL. Also, + # if gitweb.cgi serves as your indexfile, you will need to force + # $my_uri to contain the script name in your $GITWEB_CONFIG. + 'pathinfo' => { + 'override' => 0, + 'default' => [0]}, + + # Make gitweb consider projects in project root subdirectories + # to be forks of existing projects. Given project $projname.git, + # projects matching $projname/*.git will not be shown in the main + # projects list, instead a '+' mark will be added to $projname + # there and a 'forks' view will be enabled for the project, listing + # all the forks. This feature is supported only if project list + # is taken from a directory, not file. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'forks'}{'default'} = [1]; + # Project specific override is not supported. + 'forks' => { + 'override' => 0, + 'default' => [0]}, +); + +sub gitweb_check_feature { + my ($name) = @_; + return unless exists $feature{$name}; + my ($sub, $override, @defaults) = ( + $feature{$name}{'sub'}, + $feature{$name}{'override'}, + @{$feature{$name}{'default'}}); + if (!$override) { return @defaults; } + if (!defined $sub) { + warn "feature $name is not overrideable"; + return @defaults; + } + return $sub->(@defaults); +} + +sub feature_blame { + my ($val) = git_get_project_config('blame', '--bool'); + + if ($val eq 'true') { + return 1; + } elsif ($val eq 'false') { + return 0; + } + + return $_[0]; +} + +sub feature_snapshot { + my ($ctype, $suffix, $command) = @_; + + my ($val) = git_get_project_config('snapshot'); + + if ($val eq 'gzip') { + return ('x-gzip', 'gz', 'gzip'); + } elsif ($val eq 'bzip2') { + return ('x-bzip2', 'bz2', 'bzip2'); + } elsif ($val eq 'none') { + return (); + } + + return ($ctype, $suffix, $command); +} + +sub gitweb_have_snapshot { + my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); + my $have_snapshot = (defined $ctype && defined $suffix); + + return $have_snapshot; +} + +sub feature_pickaxe { + my ($val) = git_get_project_config('pickaxe', '--bool'); + + if ($val eq 'true') { + return (1); + } elsif ($val eq 'false') { + return (0); + } + + return ($_[0]); +} + +# checking HEAD file with -e is fragile if the repository was +# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed +# and then pruned. +sub check_head_link { + my ($dir) = @_; + my $headfile = "$dir/HEAD"; + return ((-e $headfile) || + (-l $headfile && readlink($headfile) =~ /^refs\/heads\//)); +} + +sub check_export_ok { + my ($dir) = @_; + return (check_head_link($dir) && + (!$export_ok || -e "$dir/$export_ok")); +} + +# rename detection options for git-diff and git-diff-tree +# - default is '-M', with the cost proportional to +# (number of removed files) * (number of new files). +# - more costly is '-C' (or '-C', '-M'), with the cost proportional to +# (number of changed files + number of removed files) * (number of new files) +# - even more costly is '-C', '--find-copies-harder' with cost +# (number of files in the original tree) * (number of new files) +# - one might want to include '-B' option, e.g. '-B', '-M' +our @diff_opts = ('-M'); # taken from git_commit + our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; -require $GITWEB_CONFIG if -e $GITWEB_CONFIG; +do $GITWEB_CONFIG if -e $GITWEB_CONFIG; # version of the core git binary our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown"; $projects_list ||= $projectroot; -if (! -d $git_temp) { - mkdir($git_temp, 0700) || die_error(undef, "Couldn't mkdir $git_temp"); -} # ====================================================================== # input validation and dispatch @@ -84,65 +290,68 @@ if (defined $action) { if ($action =~ m/[^0-9a-zA-Z\.\-_]/) { die_error(undef, "Invalid action parameter"); } - # action which does not check rest of parameters - if ($action eq "opml") { - git_opml(); - exit; - } } -our $project = ($cgi->param('p') || $ENV{'PATH_INFO'}); +# parameters which are pathnames +our $project = $cgi->param('p'); if (defined $project) { - $project =~ s|^/||; - $project =~ s|/$||; -} -if (defined $project && $project) { - if (!validate_input($project)) { - die_error(undef, "Invalid project parameter"); - } - if (!(-d "$projectroot/$project")) { - die_error(undef, "No such directory"); - } - if (!(-e "$projectroot/$project/HEAD")) { + if (!validate_pathname($project) || + !(-d "$projectroot/$project") || + !check_head_link("$projectroot/$project") || + ($export_ok && !(-e "$projectroot/$project/$export_ok")) || + ($strict_export && !project_in_list($project))) { + undef $project; die_error(undef, "No such project"); } - $ENV{'GIT_DIR'} = "$projectroot/$project"; -} else { - git_project_list(); - exit; } our $file_name = $cgi->param('f'); if (defined $file_name) { - if (!validate_input($file_name)) { + if (!validate_pathname($file_name)) { die_error(undef, "Invalid file parameter"); } } +our $file_parent = $cgi->param('fp'); +if (defined $file_parent) { + if (!validate_pathname($file_parent)) { + die_error(undef, "Invalid file parent parameter"); + } +} + +# parameters which are refnames our $hash = $cgi->param('h'); if (defined $hash) { - if (!validate_input($hash)) { + if (!validate_refname($hash)) { die_error(undef, "Invalid hash parameter"); } } our $hash_parent = $cgi->param('hp'); if (defined $hash_parent) { - if (!validate_input($hash_parent)) { + if (!validate_refname($hash_parent)) { die_error(undef, "Invalid hash parent parameter"); } } our $hash_base = $cgi->param('hb'); if (defined $hash_base) { - if (!validate_input($hash_base)) { + if (!validate_refname($hash_base)) { die_error(undef, "Invalid hash base parameter"); } } +our $hash_parent_base = $cgi->param('hpb'); +if (defined $hash_parent_base) { + if (!validate_refname($hash_parent_base)) { + die_error(undef, "Invalid hash parent base parameter"); + } +} + +# other parameters our $page = $cgi->param('pg'); if (defined $page) { - if ($page =~ m/[^0-9]$/) { + if ($page =~ m/[^0-9]/) { die_error(undef, "Invalid page parameter"); } } @@ -152,9 +361,68 @@ if (defined $searchtext) { if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) { die_error(undef, "Invalid search parameter"); } + if (length($searchtext) < 2) { + die_error(undef, "At least two characters are required for search parameter"); + } $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; + my $path_info = $ENV{"PATH_INFO"}; + return if !$path_info; + $path_info =~ s,^/+,,; + return if !$path_info; + # find which part of PATH_INFO is project + $project = $path_info; + $project =~ s,/+$,,; + while ($project && !check_head_link("$projectroot/$project")) { + $project =~ s,/*[^/]*$,,; + } + # validate project + $project = validate_pathname($project); + if (!$project || + ($export_ok && !-e "$projectroot/$project/$export_ok") || + ($strict_export && !project_in_list($project))) { + undef $project; + return; + } + # do not change any parameters if an action is given using the query string + return if $action; + $path_info =~ s,^$project/*,,; + my ($refname, $pathname) = split(/:/, $path_info, 2); + if (defined $pathname) { + # we got "project.git/branch:filename" or "project.git/branch:dir/" + # we could use git_get_type(branch:pathname), but it needs $git_dir + $pathname =~ s,^/+,,; + if (!$pathname || substr($pathname, -1) eq "/") { + $action ||= "tree"; + $pathname =~ s,/$,,; + } else { + $action ||= "blob_plain"; + } + $hash_base ||= validate_refname($refname); + $file_name ||= validate_pathname($pathname); + } elsif (defined $refname) { + # we got "project.git/branch" + $action ||= "shortlog"; + $hash ||= validate_refname($refname); + } +} +evaluate_path_info(); + +# path to the current git repository +our $git_dir; +$git_dir = "$projectroot/$project" if $project; + # dispatch my %actions = ( "blame" => \&git_blame2, @@ -165,22 +433,39 @@ my %actions = ( "commitdiff" => \&git_commitdiff, "commitdiff_plain" => \&git_commitdiff_plain, "commit" => \&git_commit, + "forks" => \&git_forks, "heads" => \&git_heads, "history" => \&git_history, "log" => \&git_log, "rss" => \&git_rss, + "atom" => \&git_atom, "search" => \&git_search, + "search_help" => \&git_search_help, "shortlog" => \&git_shortlog, "summary" => \&git_summary, "tag" => \&git_tag, "tags" => \&git_tags, "tree" => \&git_tree, + "snapshot" => \&git_snapshot, + "object" => \&git_object, + # those below don't need $project + "opml" => \&git_opml, + "project_list" => \&git_project_list, + "project_index" => \&git_project_index, ); -$action = 'summary' if (!defined($action)); +if (defined $project) { + $action ||= 'summary'; +} else { + $action ||= 'project_list'; +} if (!defined($actions{$action})) { die_error(undef, "Unknown action"); } +if ($action !~ m/^(opml|project_list|project_index)$/ && + !$project) { + die_error(undef, "Project needed"); +} $actions{$action}->(); exit; @@ -188,24 +473,52 @@ exit; ## action links sub href(%) { - my %mapping = ( - action => "a", + my %params = @_; + # default is to use -absolute url() i.e. $my_uri + my $href = $params{-full} ? $my_url : $my_uri; + + # XXX: Warning: If you touch this, check the search form for updating, + # too. + + my @mapping = ( project => "p", + action => "a", file_name => "f", + file_parent => "fp", hash => "h", hash_parent => "hp", hash_base => "hb", + hash_parent_base => "hpb", page => "pg", + order => "o", searchtext => "s", + searchtype => "st", ); + my %mapping = @mapping; - my %params = @_; - $params{"project"} ||= $project; + $params{'project'} = $project unless exists $params{'project'}; + + my ($use_pathinfo) = gitweb_check_feature('pathinfo'); + if ($use_pathinfo) { + # use PATH_INFO for project name + $href .= "/$params{'project'}" if defined $params{'project'}; + delete $params{'project'}; + + # Summary just uses the project path URL + if (defined $params{'action'} && $params{'action'} eq 'summary') { + delete $params{'action'}; + } + } - my $href = "$my_uri?"; - $href .= esc_param( join(";", - map { "$mapping{$_}=$params{$_}" } keys %params - ) ); + # now encode the parameters explicitly + my @result = (); + for (my $i = 0; $i < @mapping; $i += 2) { + my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]); + if (defined $params{$name}) { + push @result, $symbol . "=" . esc_param($params{$name}); + } + } + $href .= "?" . join(';', @result) if scalar @result; return $href; } @@ -214,25 +527,58 @@ sub href(%) { ## ====================================================================== ## validation, quoting/unquoting and escaping -sub validate_input { - my $input = shift; +sub validate_pathname { + my $input = shift || return undef; - if ($input =~ m/^[0-9a-fA-F]{40}$/) { - return $input; + # no '.' or '..' as elements of path, i.e. no '.' nor '..' + # at the beginning, at the end, and between slashes. + # also this catches doubled slashes + if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { + return undef; } - if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) { + # no null characters + if ($input =~ m!\0!) { return undef; } - if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) { + return $input; +} + +sub validate_refname { + my $input = shift || return undef; + + # textual hashes are O.K. + if ($input =~ m/^[0-9a-fA-F]{40}$/) { + return $input; + } + # it must be correct pathname + $input = validate_pathname($input) + or return undef; + # restrictions on ref name according to git-check-ref-format + if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) { return undef; } return $input; } +# very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT); +sub to_utf8 { + my $str = shift; + return decode("utf8", $str, Encode::FB_DEFAULT); +} + # quote unsafe chars, but keep the slash, even when it's not # correct, but quoted slashes look too horrible in bookmarks sub esc_param { my $str = shift; + $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg; + $str =~ s/\+/%2B/g; + $str =~ s/ /\+/g; + return $str; +} + +# quote unsafe chars in whole URL, so some charactrs cannot be quoted +sub esc_url { + my $str = shift; $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg; $str =~ s/\+/%2B/g; $str =~ s/ /\+/g; @@ -240,20 +586,93 @@ sub esc_param { } # replace invalid utf8 character with SUBSTITUTION sequence -sub esc_html { +sub esc_html ($;%) { my $str = shift; - $str = decode("utf8", $str, Encode::FB_DEFAULT); + my %opts = @_; + + $str = to_utf8($str); $str = escapeHTML($str); - $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file) + if ($opts{'-nbsp'}) { + $str =~ s/ / /g; + } + $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg; return $str; } +# 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 + "\t" => '\t', # tab (HT) + "\n" => '\n', # line feed (LF) + "\r" => '\r', # carrige return (CR) + "\f" => '\f', # form feed (FF) + "\b" => '\b', # backspace (BS) + "\a" => '\a', # alarm (bell) (BEL) + "\e" => '\e', # escape (ESC) + "\013" => '\v', # vertical tab (VT) + "\000" => '\0', # nul character (NUL) + ); + my $chr = ( (exists $es{$cntrl}) + ? $es{$cntrl} + : sprintf('\%03o', ord($cntrl)) ); + return "<span class=\"cntrl\">$chr</span>"; +} + +# 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>"; +} + # git may return quoted and escaped filenames sub unquote { my $str = shift; + + sub unq { + my $seq = shift; + my %es = ( # character escape codes, aka escape sequences + 't' => "\t", # tab (HT, TAB) + 'n' => "\n", # newline (NL) + 'r' => "\r", # return (CR) + 'f' => "\f", # form feed (FF) + 'b' => "\b", # backspace (BS) + 'a' => "\a", # alarm (bell) (BEL) + 'e' => "\e", # escape (ESC) + 'v' => "\013", # vertical tab (VT) + ); + + if ($seq =~ m/^[0-7]{1,3}$/) { + # octal char sequence + return chr(oct($seq)); + } elsif (exists $es{$seq}) { + # C escape sequence, aka character escape code + return $es{$seq} + } + # quoted ordinary character + return $seq; + } + if ($str =~ m/^"(.*)"$/) { + # needs unquoting $str = $1; - $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg; + $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg; } return $str; } @@ -272,6 +691,12 @@ sub untabify { return $line; } +sub project_in_list { + my $project = shift; + my @list = git_get_projects_list(); + return @list && scalar(grep { $_->{'path'} eq $project } @list); +} + ## ---------------------------------------------------------------------- ## HTML aware string manipulation @@ -362,7 +787,13 @@ sub mode_str { # convert file mode in octal to file type string sub file_type { - my $mode = oct shift; + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } if (S_ISDIR($mode & S_IFMT)) { return "directory"; @@ -375,22 +806,47 @@ sub file_type { } } +# convert file mode in octal to file type description string +sub file_type_long { + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } + + if (S_ISDIR($mode & S_IFMT)) { + return "directory"; + } elsif (S_ISLNK($mode)) { + return "symlink"; + } elsif (S_ISREG($mode)) { + if ($mode & S_IXUSR) { + return "executable"; + } else { + return "file"; + }; + } else { + return "unknown"; + } +} + + ## ---------------------------------------------------------------------- ## functions returning short HTML fragments, or transforming HTML fragments ## which don't beling to other sections -# format line of commit message or tag comment +# format line of commit message. sub format_log_line_html { my $line = shift; - $line = esc_html($line); - $line =~ s/ / /g; - if ($line =~ m/([0-9a-fA-F]{40})/) { + $line = esc_html($line, -nbsp=>1); + if ($line =~ m/([0-9a-fA-F]{8,40})/) { my $hash_text = $1; - if (git_get_type($hash_text) eq "commit") { - my $link = $cgi->a({-class => "text", -href => href(action=>"commit", hash=>$hash_text)}, $hash_text); - $line =~ s/$hash_text/$link/; - } + my $link = + $cgi->a({-href => href(action=>"object", hash=>$hash_text), + -class => "text"}, $hash_text); + $line =~ s/$hash_text/$link/; } return $line; } @@ -412,7 +868,8 @@ sub format_ref_marker { $name = $ref; } - $markers .= " <span class=\"$type\">" . esc_html($name) . "</span>"; + $markers .= " <span class=\"$type\" title=\"$ref\">" . + esc_html($name) . "</span>"; } } @@ -429,33 +886,84 @@ sub format_subject_html { $extra = '' unless defined($extra); if (length($short) < length($long)) { - return $cgi->a({-href => $href, -class => "list", - -title => $long}, + return $cgi->a({-href => $href, -class => "list subject", + -title => to_utf8($long)}, esc_html($short) . $extra); } else { - return $cgi->a({-href => $href, -class => "list"}, + return $cgi->a({-href => $href, -class => "list subject"}, esc_html($long) . $extra); } } +# format patch (diff) line (rather not to be used for diff headers) +sub format_diff_line { + my $line = shift; + my ($from, $to) = @_; + my $char = substr($line, 0, 1); + my $diff_class = ""; + + chomp $line; + + if ($char eq '+') { + $diff_class = " add"; + } elsif ($char eq "-") { + $diff_class = " rem"; + } elsif ($char eq "@") { + $diff_class = " chunk_header"; + } elsif ($char eq "\\") { + $diff_class = " incomplete"; + } + $line = untabify($line); + if ($from && $to && $line =~ m/^\@{2} /) { + my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) = + $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/; + + $from_lines = 0 unless defined $from_lines; + $to_lines = 0 unless defined $to_lines; + + if ($from->{'href'}) { + $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start", + -class=>"list"}, $from_text); + } + if ($to->{'href'}) { + $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start", + -class=>"list"}, $to_text); + } + $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" . + "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; + return "<div class=\"diff$diff_class\">$line</div>\n"; + } + return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n"; +} + ## ---------------------------------------------------------------------- ## git utility subroutines, invoking git commands +# returns path to the core git executable and the --git-dir parameter as list +sub git_cmd { + return $GIT, '--git-dir='.$git_dir; +} + +# returns path to the core git executable and the --git-dir parameter as string +sub git_cmd_str { + return join(' ', git_cmd()); +} + # get HEAD ref of given project as hash sub git_get_head_hash { my $project = shift; - my $oENV = $ENV{'GIT_DIR'}; + my $o_git_dir = $git_dir; my $retval = undef; - $ENV{'GIT_DIR'} = "$projectroot/$project"; - if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") { + $git_dir = "$projectroot/$project"; + if (open my $fd, "-|", git_cmd(), "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; + if (defined $o_git_dir) { + $git_dir = $o_git_dir; } return $retval; } @@ -464,7 +972,7 @@ sub git_get_head_hash { sub git_get_type { my $hash = shift; - open my $fd, "-|", $GIT, "cat-file", '-t', $hash or return; + open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return; my $type = <$fd>; close $fd or return; chomp $type; @@ -472,57 +980,46 @@ sub git_get_type { } sub git_get_project_config { - my $key = shift; + my ($key, $type) = @_; return unless ($key); $key =~ s/^gitweb\.//; return if ($key =~ m/\W/); - my $val = qx($GIT repo-config --get gitweb.$key); + my @x = (git_cmd(), 'repo-config'); + if (defined $type) { push @x, $type; } + push @x, "--get"; + push @x, "gitweb.$key"; + my $val = qx(@x); + chomp $val; 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 $type = shift; - my $tree = $base; + $path =~ s,/+$,,; - open my $fd, "-|", $GIT, "ls-tree", $base, "--", $path + open my $fd, "-|", git_cmd(), "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(.+)$/; + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/; + if (defined $type && $type ne $2) { + # type doesn't match + return undef; + } return $3; } ## ...................................................................... ## git utility functions, directly accessing git repository -# assumes that PATH is not symref -sub git_get_hash_by_ref { - 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_get_project_description { my $path = shift; @@ -536,7 +1033,7 @@ sub git_get_project_description { sub git_get_project_url_list { my $path = shift; - open my $fd, "$projectroot/$path/cloneurl" or return undef; + open my $fd, "$projectroot/$path/cloneurl" or return; my @git_project_url_list = map { chomp; $_ } <$fd>; close $fd; @@ -544,27 +1041,47 @@ sub git_get_project_url_list { } sub git_get_projects_list { + my ($filter) = @_; my @list; + $filter ||= ''; + $filter =~ s/\.git$//; + 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); + my $dir = $projects_list . ($filter ? "/$filter" : ''); + # remove the trailing "/" + $dir =~ s!/+$!!; + my $pfxlen = length("$dir"); + + my ($check_forks) = gitweb_check_feature('forks'); + + File::Find::find({ + follow_fast => 1, # follow symbolic links + dangling_symlinks => 0, # ignore dangling symlinks, silently + wanted => sub { + # skip project-list toplevel, if we get it. + return if (m!^[/.]$!); + # only directories can be git repositories + return unless (-d $_); + + my $subdir = substr($File::Find::name, $pfxlen + 1); + # we check related file in $projectroot + if ($check_forks and $subdir =~ m#/.#) { + $File::Find::prune = 1; + } elsif (check_export_ok("$projectroot/$filter/$subdir")) { + push @list, { path => ($filter ? "$filter/" : '') . $subdir }; + $File::Find::prune = 1; + } + }, + }, "$dir"); + } 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; + open my ($fd), $projects_list or return; while (my $line = <$fd>) { chomp $line; my ($path, $owner) = split ' ', $line; @@ -573,10 +1090,21 @@ sub git_get_projects_list { if (!defined $path) { next; } - if (-e "$projectroot/$path/HEAD") { + if ($filter ne '') { + # looking for forks; + my $pfx = substr($path, 0, length($filter)); + if ($pfx ne $filter) { + next; + } + my $sfx = substr($path, length($filter)); + if ($sfx !~ /^\/.*\.git$/) { + next; + } + } + if (check_export_ok("$projectroot/$path")) { my $pr = { path => $path, - owner => decode("utf8", $owner, Encode::FB_DEFAULT), + owner => to_utf8($owner), }; push @list, $pr } @@ -605,7 +1133,7 @@ sub git_get_project_owner { $pr = unescape($pr); $ow = unescape($ow); if ($pr eq $project) { - $owner = decode("utf8", $ow, Encode::FB_DEFAULT); + $owner = to_utf8($ow); last; } } @@ -618,23 +1146,37 @@ 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=%(committer)', + '--sort=-committerdate', + '--count=1', + '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; - my $fd; - # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11 - # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{} - if (-f "$projectroot/$project/info/refs") { - open $fd, "$projectroot/$project/info/refs" - or return; - } else { - open $fd, "-|", $GIT, "ls-remote", "." - or return; - } + # 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 { @@ -646,6 +1188,22 @@ sub git_get_references { return \%refs; } +sub git_get_rev_name_tags { + my $hash = shift || return undef; + + open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash + or return; + my $name_rev = <$fd>; + close $fd; + + if ($name_rev =~ m|^$hash tags/(.*)$|) { + return $1; + } else { + # catches also '$hash undefined' output + return undef; + } +} + ## ---------------------------------------------------------------------- ## parse to hash functions @@ -662,8 +1220,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{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min; + $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); @@ -671,6 +1233,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; } @@ -679,7 +1244,7 @@ sub parse_tag { my %tag; my @comment; - open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return; + open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return; $tag{'id'} = $tag_id; while (my $line = <$fd>) { chomp $line; @@ -709,40 +1274,31 @@ sub parse_tag { return %tag } -sub parse_commit { - my $commit_id = shift; - my $commit_text = shift; - - my @commit_lines; +sub parse_commit_text { + my ($commit_text, $withparents) = @_; + my @commit_lines = split '\n', $commit_text; 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; - } + pop @commit_lines; # Remove '\0' + 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 ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) { + push @parents, $1; } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) { $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'}; } @@ -751,12 +1307,19 @@ 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'}) { return; }; + $co{'parents'} = \@parents; + $co{'parent'} = $parents[0]; foreach my $title (@commit_lines) { $title =~ s/^ //; @@ -783,6 +1346,9 @@ sub parse_commit { last; } } + if ($co{'title'} eq "") { + $co{'title'} = $co{'title_short'} = '(no commit message)'; + } # remove added spaces foreach my $line (@commit_lines) { $line =~ s/^ //; @@ -803,6 +1369,52 @@ sub parse_commit { return %co; } +sub parse_commit { + my ($commit_id) = @_; + my %co; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--parents", + "--header", + "--max-count=1", + $commit_id, + "--", + or die_error(undef, "Open git-rev-list failed"); + %co = parse_commit_text(<$fd>, 1); + close $fd; + + return %co; +} + +sub parse_commits { + my ($commit_id, $maxcount, $skip, $arg, $filename) = @_; + my @cos; + + $maxcount ||= 1; + $skip ||= 0; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--header", + ($arg ? ($arg) : ()), + ("--max-count=" . $maxcount), + ("--skip=" . $skip), + $commit_id, + "--", + ($filename ? ($filename) : ()) + or die_error(undef, "Open git-rev-list failed"); + while (my $line = <$fd>) { + my %co = parse_commit_text($line); + push @cos, \%co; + } + close $fd; + + return wantarray ? @cos : \@cos; +} + # parse ref from ref_file, given by ref_id, with given type sub parse_ref { my $ref_file = shift; @@ -846,32 +1458,140 @@ sub parse_ref { return %ref_item; } +# parse line of git-diff-tree "raw" output +sub parse_difftree_raw_line { + my $line = shift; + my %res; + + # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c' + # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c' + if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) { + $res{'from_mode'} = $1; + $res{'to_mode'} = $2; + $res{'from_id'} = $3; + $res{'to_id'} = $4; + $res{'status'} = $5; + $res{'similarity'} = $6; + if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied + ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7); + } else { + $res{'file'} = unquote($7); + } + } + # 'c512b523472485aef4fff9e57b229d9d243c967f' + elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { + $res{'commit'} = $1; + } + + return wantarray ? %res : \%res; +} + +# parse line of git-ls-tree output +sub parse_ls_tree_line ($;%) { + my $line = shift; + my %opts = @_; + my %res; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + if ($opts{'-z'}) { + $res{'name'} = $4; + } else { + $res{'name'} = unquote($4); + } + + return wantarray ? %res : \%res; +} + ## ...................................................................... ## parse to array of hashes functions -sub git_get_refs_list { - my $ref_dir = shift; - my @reflist; +sub git_get_heads_list { + my $limit = shift; + my @headslist; - my @refs; - my $pfxlen = length("$projectroot/$project/$ref_dir"); - File::Find::find(sub { - return if (/^\./); - if (-f $_) { - push @refs, substr($File::Find::name, $pfxlen + 1); + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate', + '--format=%(objectname) %(refname) %(subject)%00%(committer)', + 'refs/heads' + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $committerinfo) = split(/\0/, $line); + my ($hash, $name, $title) = split(' ', $refinfo, 3); + my ($committer, $epoch, $tz) = + ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); + $name =~ s!^refs/heads/!!; + + $ref_item{'name'} = $name; + $ref_item{'id'} = $hash; + $ref_item{'title'} = $title || '(no commit message)'; + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; } - }, "$projectroot/$project/$ref_dir"); - foreach my $ref_file (@refs) { - my $ref_id = git_get_hash_by_ref("$project/$ref_dir/$ref_file"); - my $type = git_get_type($ref_id) || next; - my %ref_item = parse_ref($ref_file, $ref_id, $type); + push @headslist, \%ref_item; + } + close $fd; + + return wantarray ? @headslist : \@headslist; +} + +sub git_get_tags_list { + my $limit = shift; + my @tagslist; - push @reflist, \%ref_item; + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate', + '--format=%(objectname) %(objecttype) %(refname) '. + '%(*objectname) %(*objecttype) %(subject)%00%(creator)', + 'refs/tags' + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $creatorinfo) = split(/\0/, $line); + my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6); + my ($creator, $epoch, $tz) = + ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/); + $name =~ s!^refs/tags/!!; + + $ref_item{'type'} = $type; + $ref_item{'id'} = $id; + $ref_item{'name'} = $name; + if ($type eq "tag") { + $ref_item{'subject'} = $title; + $ref_item{'reftype'} = $reftype; + $ref_item{'refid'} = $refid; + } else { + $ref_item{'reftype'} = $type; + $ref_item{'refid'} = $id; + } + + if ($type eq "tag" || $type eq "commit") { + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; + } + } + + push @tagslist, \%ref_item; } - # sort refs by age - @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist; - return \@reflist; + close $fd; + + return wantarray ? @tagslist : \@tagslist; } ## ---------------------------------------------------------------------- @@ -887,7 +1607,7 @@ sub get_file_owner { } my $owner = $gcos; $owner =~ s/[,;].*$//; - return decode("utf8", $owner, Encode::FB_DEFAULT); + return to_utf8($owner); } ## ...................................................................... @@ -912,7 +1632,7 @@ sub mimetype_guess_file { } close(MIME); - $filename =~ /\.(.*?)$/; + $filename =~ /\.([^.]*)$/; return $mimemap{$1}; } @@ -968,13 +1688,13 @@ 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) { $title .= "/$action"; if (defined $file_name) { - $title .= " - $file_name"; + $title .= " - " . esc_path($file_name); if ($action eq "tree" && $file_name !~ m|/$|) { $title .= "/"; } @@ -986,12 +1706,16 @@ sub git_header_html { # 'application/xhtml+xml', otherwise send it as plain old 'text/html'. # we have to do this because MSIE sometimes globs '*/*', pretending to # support xhtml+xml but choking when it gets what it asked for. - if (defined $cgi->http('HTTP_ACCEPT') && $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && $cgi->Accept('application/xhtml+xml') != 0) { + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && + $cgi->Accept('application/xhtml+xml') != 0) { $content_type = 'application/xhtml+xml'; } else { $content_type = 'text/html'; } - print $cgi->header(-type=>$content_type, -charset => 'utf-8', -status=> $status, -expires => $expires); + print $cgi->header(-type=>$content_type, -charset => 'utf-8', + -status=> $status, -expires => $expires); + my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; print <<EOF; <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> @@ -1000,30 +1724,62 @@ sub git_header_html { <!-- git core binaries version $git_version --> <head> <meta http-equiv="content-type" content="$content_type; charset=utf-8"/> -<meta name="generator" content="gitweb/$version git/$git_version"/> +<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/> <meta name="robots" content="index, nofollow"/> <title>$title</title> -<link rel="stylesheet" type="text/css" href="$stylesheet"/> EOF +# print out each stylesheet that exist + if (defined $stylesheet) { +#provides backwards capability for those people who define style sheet in a config file + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } else { + foreach my $stylesheet (@stylesheets) { + next unless $stylesheet; + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } + } 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 feeds" '. + 'href="%s" type="text/x-opml"/>'."\n", + $site_name, href(project=>undef, action=>"opml")); + } + if (defined $favicon) { + print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n); } print "</head>\n" . - "<body>\n" . - "<div class=\"page_header\">\n" . - "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" . - "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" . - "</a>\n"; - print $cgi->a({-href => esc_param($home_link)}, $home_link_str) . " / "; + "<body>\n"; + + if (-f $site_header) { + open (my $fd, $site_header); + print <$fd>; + close $fd; + } + + print "<div class=\"page_header\">\n" . + $cgi->a({-href => esc_url($logo_url), + -title => $logo_label}, + qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>)); + print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; if (defined $project) { print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); if (defined $action) { print " / $action"; } print "\n"; + } + my ($have_search) = gitweb_check_feature('search'); + if ((defined $project) && ($have_search)) { if (!defined $searchtext) { $searchtext = ""; } @@ -1037,11 +1793,16 @@ EOF } $cgi->param("a", "search"); $cgi->param("h", $search_hash); + $cgi->param("p", $project); print $cgi->startform(-method => "get", -action => $my_uri) . "<div class=\"search\">\n" . $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"; @@ -1056,12 +1817,25 @@ sub git_footer_html { if (defined $descr) { print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n"; } - print $cgi->a({-href => href(action=>"rss"), -class => "rss_logo"}, "RSS") . "\n"; + print $cgi->a({-href => href(action=>"rss"), + -class => "rss_logo"}, "RSS") . " "; + print $cgi->a({-href => href(action=>"atom"), + -class => "rss_logo"}, "Atom") . "\n"; } else { - print $cgi->a({-href => "$my_uri?" . esc_param("a=opml"), -class => "rss_logo"}, "OPML") . "\n"; + print $cgi->a({-href => href(project=>undef, action=>"opml"), + -class => "rss_logo"}, "OPML") . " "; + print $cgi->a({-href => href(project=>undef, action=>"project_index"), + -class => "rss_logo"}, "TXT") . "\n"; } - print "</div>\n" . - "</body>\n" . + print "</div>\n" ; + + if (-f $site_footer) { + open (my $fd, $site_footer); + print <$fd>; + close $fd; + } + + print "</body>\n" . "</html>"; } @@ -1070,11 +1844,13 @@ sub die_error { my $error = shift || "Malformed query, file missing or permission denied"; git_header_html($status); - print "<div class=\"page_body\">\n" . - "<br/><br/>\n" . - "$status - $error\n" . - "<br/>\n" . - "</div>\n"; + print <<EOF; +<div class="page_body"> +<br /><br /> +$status - $error +<br /> +</div> +EOF git_footer_html(); exit; } @@ -1161,17 +1937,244 @@ sub git_print_header_div { "\n</div>\n"; } +#sub git_print_authorship (\%) { +sub git_print_authorship { + my $co = shift; + + my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'}); + print "<div class=\"author_date\">" . + esc_html($co->{'author_name'}) . + " [$ad{'rfc2822'}"; + if ($ad{'hour_local'} < 6) { + printf(" (<span class=\"atnight\">%02d:%02d</span> %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + } else { + printf(" (%02d:%02d %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + } + print "]</div>\n"; +} + sub git_print_page_path { my $name = shift; my $type = shift; + my $hb = shift; + + + print "<div class=\"page_path\">"; + print $cgi->a({-href => href(action=>"tree", hash_base=>$hb), + -title => 'tree root'}, "[$project]"); + print " / "; + if (defined $name) { + my @dirname = split '/', $name; + my $basename = pop @dirname; + my $fullname = ''; + + foreach my $dir (@dirname) { + $fullname .= ($fullname ? '/' : '') . $dir; + print $cgi->a({-href => href(action=>"tree", file_name=>$fullname, + hash_base=>$hb), + -title => esc_html($fullname)}, esc_path($dir)); + print " / "; + } + if (defined $type && $type eq 'blob') { + print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name, + hash_base=>$hb), + -title => esc_html($name)}, esc_path($basename)); + } elsif (defined $type && $type eq 'tree') { + print $cgi->a({-href => href(action=>"tree", file_name=>$file_name, + hash_base=>$hb), + -title => esc_html($name)}, esc_path($basename)); + print " / "; + } else { + print esc_path($basename); + } + } + print "<br/></div>\n"; +} + +# sub git_print_log (\@;%) { +sub git_print_log ($;%) { + my $log = shift; + my %opts = @_; + + if ($opts{'-remove_title'}) { + # remove title, i.e. first line of log + shift @$log; + } + # remove leading empty lines + while (defined $log->[0] && $log->[0] eq "") { + shift @$log; + } + + # print log + my $signoff = 0; + my $empty = 0; + foreach my $line (@$log) { + if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { + $signoff = 1; + $empty = 0; + if (! $opts{'-remove_signoff'}) { + print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n"; + next; + } else { + # remove signoff lines + next; + } + } else { + $signoff = 0; + } + + # print only one empty line + # do not print empty line after signoff + if ($line eq "") { + next if ($empty || $signoff); + $empty = 1; + } else { + $empty = 0; + } + + print format_log_line_html($line) . "<br/>\n"; + } + + if ($opts{'-final_empty_line'}) { + # end with single empty line + print "<br/>\n" unless $empty; + } +} - if (!defined $name) { - print "<div class=\"page_path\"><b>/</b></div>\n"; - } elsif (defined $type && $type eq 'blob') { - print "<div class=\"page_path\"><b>" . - $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)}, esc_html($name)) . "</b><br/></div>\n"; +# return link target (what link points to) +sub git_get_link_target { + my $hash = shift; + my $link_target; + + # read link + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or return; + { + local $/; + $link_target = <$fd>; + } + close $fd + or return; + + return $link_target; +} + +# given link target, and the directory (basedir) the link is in, +# return target of link relative to top directory (top tree); +# return undef if it is not possible (including absolute links). +sub normalize_link_target { + my ($link_target, $basedir, $hash_base) = @_; + + # we can normalize symlink target only if $hash_base is provided + return unless $hash_base; + + # absolute symlinks (beginning with '/') cannot be normalized + return if (substr($link_target, 0, 1) eq '/'); + + # normalize link target to path from top (root) tree (dir) + my $path; + if ($basedir) { + $path = $basedir . '/' . $link_target; } else { - print "<div class=\"page_path\"><b>" . esc_html($name) . "</b><br/></div>\n"; + # we are in top (root) tree (dir) + $path = $link_target; + } + + # remove //, /./, and /../ + my @path_parts; + foreach my $part (split('/', $path)) { + # discard '.' and '' + next if (!$part || $part eq '.'); + # handle '..' + if ($part eq '..') { + if (@path_parts) { + pop @path_parts; + } else { + # link leads outside repository (outside top dir) + return; + } + } else { + push @path_parts, $part; + } + } + $path = join('/', @path_parts); + + return $path; +} + +# print tree entry (row of git_tree), but without encompassing <tr> element +sub git_print_tree_entry { + my ($t, $basedir, $hash_base, $have_blame) = @_; + + my %base_key = (); + $base_key{'hash_base'} = $hash_base if defined $hash_base; + + # The format of a table row is: mode list link. Where mode is + # the mode of the entry, list is the name of the entry, an href, + # and link is the action links of the entry. + + print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n"; + if ($t->{'type'} eq "blob") { + print "<td class=\"list\">" . + $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key), + -class => "list"}, esc_path($t->{'name'})); + if (S_ISLNK(oct $t->{'mode'})) { + my $link_target = git_get_link_target($t->{'hash'}); + if ($link_target) { + my $norm_target = normalize_link_target($link_target, $basedir, $hash_base); + if (defined $norm_target) { + print " -> " . + $cgi->a({-href => href(action=>"object", hash_base=>$hash_base, + file_name=>$norm_target), + -title => $norm_target}, esc_path($link_target)); + } else { + print " -> " . esc_path($link_target); + } + } + } + print "</td>\n"; + print "<td class=\"link\">"; + print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blob"); + if ($have_blame) { + print " | " . + $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blame"); + } + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print " | " . + $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "raw"); + print "</td>\n"; + + } elsif ($t->{'type'} eq "tree") { + print "<td class=\"list\">"; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + esc_path($t->{'name'})); + print "</td>\n"; + print "<td class=\"link\">"; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "tree"); + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print "</td>\n"; } } @@ -1179,8 +2182,8 @@ sub git_print_page_path { ## functions printing large fragments of HTML sub git_difftree_body { - my ($difftree, $parent) = @_; - + my ($difftree, $hash, $parent) = @_; + my ($have_blame) = gitweb_check_feature('blame'); print "<div class=\"list_head\">\n"; if ($#{$difftree} > 10) { print(($#{$difftree} + 1) . " files changed:\n"); @@ -1188,20 +2191,10 @@ sub git_difftree_body { print "</div>\n"; print "<table class=\"diff_tree\">\n"; - my $alternate = 0; + my $alternate = 1; + my $patchno = 0; foreach my $line (@{$difftree}) { - # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c' - # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c' - 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; - } - my $from_mode = $1; - my $to_mode = $2; - my $from_id = $3; - my $to_id = $4; - my $status = $5; - my $similarity = $6; # score - my $file = validate_input(unquote($7)); + my %diff = parse_difftree_raw_line($line); if ($alternate) { print "<tr class=\"dark\">\n"; @@ -1210,126 +2203,510 @@ sub git_difftree_body { } $alternate ^= 1; - if ($status eq "A") { # created - my $mode_chng = ""; - if (S_ISREG(oct $to_mode)) { - $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777); + my ($to_mode_oct, $to_mode_str, $to_file_type); + my ($from_mode_oct, $from_mode_str, $from_file_type); + if ($diff{'to_mode'} ne ('0' x 6)) { + $to_mode_oct = oct $diff{'to_mode'}; + if (S_ISREG($to_mode_oct)) { # only for regular file + $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits } - print "<td>" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file), - -class => "list"}, esc_html($file)) . - "</td>\n" . - "<td><span class=\"file_status new\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file)}, "blob") . - "</td>\n"; - - } elsif ($status eq "D") { # deleted - print "<td>" . - $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$file), - -class => "list"}, esc_html($file)) . "</td>\n" . - "<td><span class=\"file_status deleted\">[deleted " . file_type($from_mode). "]</span></td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$file)}, "blob") . " | " . - $cgi->a({-href => href(action=>"history", hash_base=>$parent, file_name=>$file)}, "history") . - "</td>\n" - - } elsif ($status eq "M" || $status eq "T") { # modified, or type changed + $to_file_type = file_type($diff{'to_mode'}); + } + if ($diff{'from_mode'} ne ('0' x 6)) { + $from_mode_oct = oct $diff{'from_mode'}; + if (S_ISREG($to_mode_oct)) { # only for regular file + $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits + } + $from_file_type = file_type($diff{'from_mode'}); + } + + if ($diff{'status'} eq "A") { # created + my $mode_chng = "<span class=\"file_status new\">[new $to_file_type"; + $mode_chng .= " with mode: $to_mode_str" if $to_mode_str; + $mode_chng .= "]</span>"; + print "<td>"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chng</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # 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 + my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>"; + print "<td>"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, + hash_base=>$parent, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chng</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + print " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, + hash_base=>$parent, file_name=>$diff{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, + file_name=>$diff{'file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$parent, + file_name=>$diff{'file'})}, + "history"); + print "</td>\n"; + + } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed my $mode_chnge = ""; - if ($from_mode != $to_mode) { - $mode_chnge = " <span class=\"file_status mode_chnge\">[changed"; - if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) { - $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode); + if ($diff{'from_mode'} != $diff{'to_mode'}) { + $mode_chnge = "<span class=\"file_status mode_chnge\">[changed"; + if ($from_file_type ne $to_file_type) { + $mode_chnge .= " from $from_file_type to $to_file_type"; } - if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) { - if (S_ISREG($from_mode) && S_ISREG($to_mode)) { - $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777); - } elsif (S_ISREG($to_mode)) { - $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777); + if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) { + if ($from_mode_str && $to_mode_str) { + $mode_chnge .= " mode: $from_mode_str->$to_mode_str"; + } elsif ($to_mode_str) { + $mode_chnge .= " mode: $to_mode_str"; } } $mode_chnge .= "]</span>\n"; } print "<td>"; - if ($to_id ne $from_id) { # modified - print $cgi->a({-href => href(action=>"blobdiff", hash=>$to_id, hash_parent=>$from_id, hash_base=>$hash, file_name=>$file), - -class => "list"}, esc_html($file)); - } else { # mode changed - print $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file), - -class => "list"}, esc_html($file)); + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chnge</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch") . + " | "; + } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + # "commit" view and modified file (not onlu mode changed) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff{'file'})}, + "diff") . + " | "; } - print "</td>\n" . - "<td>$mode_chnge</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file)}, "blob"); - if ($to_id ne $from_id) { # modified - print $cgi->a({-href => href(action=>"blobdiff", hash=>$to_id, hash_parent=>$from_id, hash_base=>$hash, file_name=>$file)}, "diff"); + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff{'file'})}, + "blame") . " | "; } - print " | " . $cgi->a({-href => href(action=>"history", hash_base=>$hash, file_name=>$file)}, "history") . "\n"; + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff{'file'})}, + "history"); print "</td>\n"; - } elsif ($status eq "R") { # renamed - my ($from_file, $to_file) = split "\t", $file; + } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied + my %status_name = ('R' => 'moved', 'C' => 'copied'); + my $nstatus = $status_name{$diff{'status'}}; my $mode_chng = ""; - if ($from_mode != $to_mode) { - $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777); + if ($diff{'from_mode'} != $diff{'to_mode'}) { + # mode also for directories, so we cannot use $to_mode_str + $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777); } print "<td>" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file), - -class => "list"}, esc_html($to_file)) . "</td>\n" . - "<td><span class=\"file_status moved\">[moved from " . - $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$from_file), - -class => "list"}, esc_html($from_file)) . - " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file)}, "blob"); - if ($to_id ne $from_id) { - print " | " . - $cgi->a({-href => "$my_uri?" . - esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file;fp=$from_file")}, "diff"); + $cgi->a({-href => href(action=>"blob", hash_base=>$hash, + hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}), + -class => "list"}, esc_path($diff{'to_file'})) . "</td>\n" . + "<td><span class=\"file_status $nstatus\">[$nstatus from " . + $cgi->a({-href => href(action=>"blob", hash_base=>$parent, + hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}), + -class => "list"}, esc_path($diff{'from_file'})) . + " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" . + "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch") . + " | "; + } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + # "commit" view and modified file (not only pure rename or copy) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})}, + "diff") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$parent, file_name=>$diff{'to_file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff{'to_file'})}, + "blame") . " | "; } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff{'to_file'})}, + "history"); print "</td>\n"; - } elsif ($status eq "C") { # copied - my ($from_file, $to_file) = split "\t", $file; - my $mode_chng = ""; - if ($from_mode != $to_mode) { - $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777); + } # we should not encounter Unmerged (U) or Unknown (X) status + print "</tr>\n"; + } + print "</table>\n"; +} + +sub git_patchset_body { + my ($fd, $difftree, $hash, $hash_parent) = @_; + + my $patch_idx = 0; + my $patch_line; + my $diffinfo; + my (%from, %to); + + print "<div class=\"patchset\">\n"; + + # skip to first patch + while ($patch_line = <$fd>) { + chomp $patch_line; + + last if ($patch_line =~ m/^diff /); + } + + PATCH: + while ($patch_line) { + my @diff_header; + my ($from_id, $to_id); + + # git diff header + #assert($patch_line =~ m/^diff /) if DEBUG; + #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed + push @diff_header, $patch_line; + + # extended diff header + EXTENDED_HEADER: + while ($patch_line = <$fd>) { + chomp $patch_line; + + last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /); + + if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) { + $from_id = $1; + $to_id = $2; } - print "<td>" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file), - -class => "list"}, esc_html($to_file)) . "</td>\n" . - "<td><span class=\"file_status copied\">[copied from " . - $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$from_file), - -class => "list"}, esc_html($from_file)) . - " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file)}, "blob"); - if ($to_id ne $from_id) { - print " | " . - $cgi->a({-href => "$my_uri?" . - esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file;fp=$from_file")}, "diff"); + + push @diff_header, $patch_line; + } + my $last_patch_line = $patch_line; + + # check if current patch belong to current raw line + # and parse raw git-diff line if needed + if (defined $diffinfo && + $diffinfo->{'from_id'} eq $from_id && + $diffinfo->{'to_id'} eq $to_id) { + # this is split patch + print "<div class=\"patch cont\">\n"; + } else { + # advance raw git-diff output if needed + $patch_idx++ if defined $diffinfo; + + # read and prepare patch information + if (ref($difftree->[$patch_idx]) eq "HASH") { + # pre-parsed (or generated by hand) + $diffinfo = $difftree->[$patch_idx]; + } else { + $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + } + $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; + $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; + if ($diffinfo->{'status'} ne "A") { # not new (added) file + $from{'href'} = href(action=>"blob", hash_base=>$hash_parent, + hash=>$diffinfo->{'from_id'}, + file_name=>$from{'file'}); + } else { + delete $from{'href'}; + } + if ($diffinfo->{'status'} ne "D") { # not deleted file + $to{'href'} = href(action=>"blob", hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$to{'file'}); + } else { + delete $to{'href'}; + } + # this is first patch for raw difftree line with $patch_idx index + # we index @$difftree array from 0, but number patches from 1 + print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n"; + } + + # print "git diff" header + $patch_line = shift @diff_header; + $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!; + if ($from{'href'}) { + $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"}, + 'a/' . esc_path($from{'file'})); + } else { # file was added + $patch_line .= 'a/' . esc_path($from{'file'}); + } + $patch_line .= ' '; + if ($to{'href'}) { + $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, + 'b/' . esc_path($to{'file'})); + } else { # file was deleted + $patch_line .= 'b/' . esc_path($to{'file'}); + } + print "<div class=\"diff header\">$patch_line</div>\n"; + + # print extended diff header + print "<div class=\"diff extended_header\">\n" if (@diff_header > 0); + EXTENDED_HEADER: + foreach $patch_line (@diff_header) { + # match <path> + if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) { + $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"}, + esc_path($from{'file'})); + } + if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) { + $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"}, + esc_path($to{'file'})); + } + # match <mode> + if ($patch_line =~ m/\s(\d{6})$/) { + $patch_line .= '<span class="info"> (' . + file_type_long($1) . + ')</span>'; + } + # match <hash> + if ($patch_line =~ m/^index/) { + my ($from_link, $to_link); + if ($from{'href'}) { + $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"}, + substr($diffinfo->{'from_id'},0,7)); + } else { + $from_link = '0' x 7; + } + if ($to{'href'}) { + $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $to_link = '0' x 7; + } + #affirm { + # my ($from_hash, $to_hash) = + # ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/); + # my ($from_id, $to_id) = + # ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + # ($from_hash eq $from_id) && ($to_hash eq $to_id); + #} if DEBUG; + my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; + } + print $patch_line . "<br/>\n"; + } + print "</div>\n" if (@diff_header > 0); # class="diff extended_header" + + # from-file/to-file diff header + $patch_line = $last_patch_line; + if (! $patch_line) { + print "</div>\n"; # class="patch" + last PATCH; + } + next PATCH if ($patch_line =~ m/^diff /); + #assert($patch_line =~ m/^---/) if DEBUG; + if ($from{'href'} && $patch_line =~ m!^--- "?a/!) { + $patch_line = '--- a/' . + $cgi->a({-href=>$from{'href'}, -class=>"path"}, + esc_path($from{'file'})); + } + print "<div class=\"diff from_file\">$patch_line</div>\n"; + + $patch_line = <$fd>; + chomp $patch_line; + + #assert($patch_line =~ m/^+++/) if DEBUG; + if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) { + $patch_line = '+++ b/' . + $cgi->a({-href=>$to{'href'}, -class=>"path"}, + esc_path($to{'file'})); + } + print "<div class=\"diff to_file\">$patch_line</div>\n"; + + # the patch itself + LINE: + while ($patch_line = <$fd>) { + chomp $patch_line; + + next PATCH if ($patch_line =~ m/^diff /); + + print format_diff_line($patch_line, \%from, \%to); + } + + } continue { + print "</div>\n"; # class="patch" + } + + print "</div>\n"; # class="patchset" +} + +# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + +sub git_project_list_body { + my ($projlist, $order, $from, $to, $extra, $no_header) = @_; + + my ($check_forks) = gitweb_check_feature('forks'); + + my @projects; + foreach my $pr (@$projlist) { + my (@aa) = git_get_last_activity($pr->{'path'}); + unless (@aa) { + next; + } + ($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'}) { + $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || ""; + } + if ($check_forks) { + my $pname = $pr->{'path'}; + if (($pname =~ s/\.git$//) && + ($pname !~ /\/$/) && + (-d "$projectroot/$pname")) { + $pr->{'forks'} = "-d $projectroot/$pname"; + } + else { + $pr->{'forks'} = 0; + } + } + push @projects, $pr; + } + + $order ||= "project"; + $from = 0 unless defined $from; + $to = $#projects if (!defined $to || $#projects < $to); + + print "<table class=\"project_list\">\n"; + unless ($no_header) { + print "<tr>\n"; + if ($check_forks) { + print "<th></th>\n"; + } + if ($order eq "project") { + @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects; + print "<th>Project</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'project'), + -class => "header"}, "Project") . + "</th>\n"; + } + if ($order eq "descr") { + @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects; + print "<th>Description</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'descr'), + -class => "header"}, "Description") . + "</th>\n"; + } + if ($order eq "owner") { + @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects; + print "<th>Owner</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'owner'), + -class => "header"}, "Owner") . + "</th>\n"; + } + if ($order eq "age") { + @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects; + print "<th>Last Change</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'age'), + -class => "header"}, "Last Change") . + "</th>\n"; + } + print "<th></th>\n" . + "</tr>\n"; + } + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $pr = $projects[$i]; + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + if ($check_forks) { + print "<td>"; + if ($pr->{'forks'}) { + print "<!-- $pr->{'forks'} -->\n"; + print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+"); } print "</td>\n"; - } # we should not encounter Unmerged (U) or Unknown (X) status - print "</tr>\n"; + } + print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), + -class => "list"}, esc_html($pr->{'path'})) . "</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" . + "<td class=\"link\">" . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") . + ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') . + "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n"; + if ($check_forks) { + print "<td></td>\n"; + } + print "<td colspan=\"5\">$extra</td>\n" . + "</tr>\n"; } print "</table>\n"; } sub git_shortlog_body { # uses global variable $project - my ($revlist, $from, $to, $refs, $extra) = @_; + my ($commitlist, $from, $to, $refs, $extra) = @_; + + my $have_snapshot = gitweb_have_snapshot(); + $from = 0 unless defined $from; - $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to); + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); print "<table class=\"shortlog\" cellspacing=\"0\">\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { - my $commit = $revlist->[$i]; - #my $ref = defined $refs ? format_ref_marker($refs, $commit) : ''; + my %co = %{$commitlist->[$i]}; + my $commit = $co{'id'}; my $ref = format_ref_marker($refs, $commit); - my %co = parse_commit($commit); if ($alternate) { print "<tr class=\"dark\">\n"; } else { @@ -1340,12 +2717,17 @@ sub git_shortlog_body { print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" . "<td>"; - print format_subject_html($co{'title'}, $co{'title_short'}, href(action=>"commit", hash=>$commit), $ref); + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); print "</td>\n" . "<td class=\"link\">" . $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . - $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . - "</td>\n" . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); + if ($have_snapshot) { + print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot"); + } + print "</td>\n" . "</tr>\n"; } if (defined $extra) { @@ -1358,20 +2740,19 @@ sub git_shortlog_body { sub git_history_body { # Warning: assumes constant type (blob or tree) during history - my ($fd, $refs, $hash_base, $ftype, $extra) = @_; + my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; - print "<table class=\"history\" cellspacing=\"0\">\n"; - my $alternate = 0; - while (my $line = <$fd>) { - if ($line !~ m/^([0-9a-fA-F]{40})/) { - next; - } + $from = 0 unless defined $from; + $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); - my $commit = $1; - my %co = parse_commit($commit); + print "<table class=\"history\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; if (!%co) { next; } + my $commit = $co{'id'}; my $ref = format_ref_marker($refs, $commit); @@ -1386,12 +2767,12 @@ sub git_history_body { "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" . "<td>"; # originally git_history used chop_str($co{'title'}, 50) - print format_subject_html($co{'title'}, $co{'title_short'}, href(action=>"commit", hash=>$commit), $ref); + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); print "</td>\n" . "<td class=\"link\">" . - $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . - $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . - $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype); + $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff"); if ($ftype eq 'blob') { my $blob_current = git_get_hash_by_path($hash_base, $file_name); @@ -1399,7 +2780,10 @@ sub git_history_body { if (defined $blob_current && defined $blob_parent && $blob_current ne $blob_parent) { print " | " . - $cgi->a({-href => href(action=>"blobdiff", hash=>$blob_current, hash_parent=>$blob_parent, hash_base=>$commit, file_name=>$file_name)}, + $cgi->a({-href => href(action=>"blobdiff", + hash=>$blob_current, hash_parent=>$blob_parent, + hash_base=>$hash_base, hash_parent_base=>$commit, + file_name=>$file_name)}, "diff to current"); } } @@ -1421,12 +2805,11 @@ sub git_tags_body { $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); print "<table class=\"tags\" cellspacing=\"0\">\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { my $entry = $taglist->[$i]; my %tag = %$entry; - my $comment_lines = $tag{'comment'}; - my $comment = shift @$comment_lines; + my $comment = $tag{'subject'}; my $comment_short; if (defined $comment) { $comment_short = chop_str($comment, 30, 5); @@ -1437,14 +2820,19 @@ sub git_tags_body { print "<tr class=\"light\">\n"; } $alternate ^= 1; - print "<td><i>$tag{'age'}</i></td>\n" . - "<td>" . + if (defined $tag{'age'}) { + print "<td><i>$tag{'age'}</i></td>\n"; + } else { + print "<td></td>\n"; + } + print "<td>" . $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}), - -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") . + -class => "list name"}, esc_html($tag{'name'})) . "</td>\n" . "<td>"; if (defined $comment) { - print format_subject_html($comment, $comment_short, href(action=>"tag", hash=>$tag{'id'})); + print format_subject_html($comment, $comment_short, + href(action=>"tag", hash=>$tag{'id'})); } print "</td>\n" . "<td class=\"selflink\">"; @@ -1458,7 +2846,7 @@ sub git_tags_body { $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'}); if ($tag{'reftype'} eq "commit") { print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . - " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log"); + " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log"); } elsif ($tag{'reftype'} eq "blob") { print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw"); } @@ -1475,30 +2863,31 @@ sub git_tags_body { sub git_heads_body { # uses global variable $project - my ($taglist, $head, $from, $to, $extra) = @_; + my ($headlist, $head, $from, $to, $extra) = @_; $from = 0 unless defined $from; - $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); + $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to); print "<table class=\"heads\" cellspacing=\"0\">\n"; - my $alternate = 0; + my $alternate = 1; for (my $i = $from; $i <= $to; $i++) { - my $entry = $taglist->[$i]; - my %tag = %$entry; - my $curr = $tag{'id'} eq $head; + my $entry = $headlist->[$i]; + my %ref = %$entry; + my $curr = $ref{'id'} eq $head; if ($alternate) { print "<tr class=\"dark\">\n"; } else { print "<tr class=\"light\">\n"; } $alternate ^= 1; - print "<td><i>$tag{'age'}</i></td>\n" . - ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") . - $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}), - -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") . + print "<td><i>$ref{'age'}</i></td>\n" . + ($curr ? "<td class=\"current_head\">" : "<td>") . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'}), + -class => "list name"},esc_html($ref{'name'})) . "</td>\n" . "<td class=\"link\">" . - $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " . - $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'})}, "shortlog") . " | " . + $cgi->a({-href => href(action=>"log", hash=>$ref{'name'})}, "log") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$ref{'name'}, hash_base=>$ref{'name'})}, "tree") . "</td>\n" . "</tr>"; } @@ -1510,77 +2899,58 @@ sub git_heads_body { print "</table>\n"; } -## ---------------------------------------------------------------------- -## functions printing large fragments, format as one of arguments - -sub git_diff_print { - my $from = shift; - my $from_name = shift; - my $to = shift; - my $to_name = shift; - my $format = shift || "html"; - - my $from_tmp = "/dev/null"; - my $to_tmp = "/dev/null"; - my $pid = $$; - - # create tmp from-file - if (defined $from) { - $from_tmp = "$git_temp/gitweb_" . $$ . "_from"; - open my $fd2, "> $from_tmp"; - open my $fd, "-|", $GIT, "cat-file", "blob", $from; - my @file = <$fd>; - print $fd2 @file; - close $fd2; - close $fd; - } - - # create tmp to-file - if (defined $to) { - $to_tmp = "$git_temp/gitweb_" . $$ . "_to"; - open my $fd2, "> $to_tmp"; - open my $fd, "-|", $GIT, "cat-file", "blob", $to; - my @file = <$fd>; - print $fd2 @file; - close $fd2; - close $fd; - } +sub git_search_grep_body { + my ($commitlist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); - open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp"; - if ($format eq "plain") { - undef $/; - print <$fd>; - $/ = "\n"; - } else { - while (my $line = <$fd>) { - chomp $line; - my $char = substr($line, 0, 1); - my $diff_class = ""; - if ($char eq '+') { - $diff_class = " add"; - } elsif ($char eq "-") { - $diff_class = " rem"; - } elsif ($char eq "@") { - $diff_class = " chunk_header"; - } elsif ($char eq "\\") { - # skip errors - next; + print "<table class=\"grep\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + if (!%co) { + next; + } + my $commit = $co{'id'}; + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . + "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" . + "<td>" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, + esc_html(chop_str($co{'title'}, 50)) . "<br/>"); + my $comment = $co{'comment'}; + foreach my $line (@$comment) { + if ($line =~ m/^(.*)($searchtext)(.*)$/i) { + my $lead = esc_html($1) || ""; + $lead = chop_str($lead, 30, 10); + my $match = esc_html($2) || ""; + my $trail = esc_html($3) || ""; + $trail = chop_str($trail, 30, 10); + my $text = "$lead<span class=\"match\">$match</span>$trail"; + print chop_str($text, 80, 5) . "<br/>\n"; } - $line = untabify($line); - print "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n"; } + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "</td>\n" . + "</tr>\n"; } - close $fd; - - if (defined $from) { - unlink($from_tmp); - } - if (defined $to) { - unlink($to_tmp); + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"3\">$extra</td>\n" . + "</tr>\n"; } + print "</table>\n"; } - ## ====================================================================== ## ====================================================================== ## actions @@ -1592,30 +2962,9 @@ sub git_project_list { } my @list = git_get_projects_list(); - my @projects; if (!@list) { die_error(undef, "No projects found"); } - foreach my $pr (@list) { - my $head = git_get_head_hash($pr->{'path'}); - if (!defined $head) { - next; - } - $ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}"; - my %co = parse_commit($head); - if (!%co) { - next; - } - $pr->{'commit'} = \%co; - if (!defined $pr->{'descr'}) { - my $descr = git_get_project_description($pr->{'path'}) || ""; - $pr->{'descr'} = chop_str($descr, 25, 5); - } - if (!defined $pr->{'owner'}) { - $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || ""; - } - push @projects, $pr; - } git_header_html(); if (-f $home_text) { @@ -1625,81 +2974,72 @@ sub git_project_list { close $fd; print "</div>\n"; } - print "<table class=\"project_list\">\n" . - "<tr>\n"; - $order ||= "project"; - if ($order eq "project") { - @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects; - print "<th>Project</th>\n"; - } else { - print "<th>" . - $cgi->a({-href => "$my_uri?" . esc_param("o=project"), - -class => "header"}, "Project") . - "</th>\n"; - } - if ($order eq "descr") { - @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects; - print "<th>Description</th>\n"; - } else { - print "<th>" . - $cgi->a({-href => "$my_uri?" . esc_param("o=descr"), - -class => "header"}, "Description") . - "</th>\n"; - } - if ($order eq "owner") { - @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects; - print "<th>Owner</th>\n"; - } else { - print "<th>" . - $cgi->a({-href => "$my_uri?" . esc_param("o=owner"), - -class => "header"}, "Owner") . - "</th>\n"; - } - if ($order eq "age") { - @projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects; - print "<th>Last Change</th>\n"; - } else { - print "<th>" . - $cgi->a({-href => "$my_uri?" . esc_param("o=age"), - -class => "header"}, "Last Change") . - "</th>\n"; + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_forks { + my $order = $cgi->param('o'); + if (defined $order && $order !~ m/project|descr|owner|age/) { + die_error(undef, "Unknown order parameter"); } - print "<th></th>\n" . - "</tr>\n"; - my $alternate = 0; + + my @list = git_get_projects_list($project); + if (!@list) { + die_error(undef, "No forks found"); + } + + git_header_html(); + git_print_page_nav('',''); + git_print_header_div('summary', "$project forks"); + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_project_index { + my @projects = git_get_projects_list($project); + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -content_disposition => 'inline; filename="index.aux"'); + foreach my $pr (@projects) { - if ($alternate) { - print "<tr class=\"dark\">\n"; - } else { - print "<tr class=\"light\">\n"; + if (!exists $pr->{'owner'}) { + $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}"); } - $alternate ^= 1; - print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary"), - -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" . - "<td>" . esc_html($pr->{'descr'}) . "</td>\n" . - "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n"; - print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" . - $pr->{'commit'}{'age_string'} . "</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary")}, "summary") . " | " . - $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=shortlog")}, "shortlog") . " | " . - $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=log")}, "log") . - "</td>\n" . - "</tr>\n"; + + my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); + # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' ' + $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $path =~ s/ /\+/g; + $owner =~ s/ /\+/g; + + print "$path $owner\n"; } - print "</table>\n"; - git_footer_html(); } sub git_summary { my $descr = git_get_project_description($project) || "none"; - my $head = git_get_head_hash($project); - my %co = parse_commit($head); + my %co = parse_commit("HEAD"); my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + my $head = $co{'id'}; my $owner = git_get_project_owner($project); my $refs = git_get_references(); + # These get_*_list functions return one more to allow us to see if + # there are more ... + my @taglist = git_get_tags_list(16); + my @headlist = git_get_heads_list(16); + my @forklist; + my ($check_forks) = gitweb_check_feature('forks'); + + if ($check_forks) { + @forklist = git_get_projects_list($project); + } + git_header_html(); git_print_page_nav('summary','', $head); @@ -1720,28 +3060,44 @@ sub git_summary { } print "</table>\n"; - open my $fd, "-|", $GIT, "rev-list", "--max-count=17", git_get_head_hash($project) - or die_error(undef, "Open git-rev-list failed"); - my @revlist = map { chomp; $_ } <$fd>; - close $fd; + 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; + } + } + + # we need to request one more than 16 (0..15) to check if + # those 16 are all + my @commitlist = parse_commits($head, 17); git_print_header_div('shortlog'); - git_shortlog_body(\@revlist, 0, 15, $refs, + git_shortlog_body(\@commitlist, 0, 15, $refs, + $#commitlist <= 15 ? undef : $cgi->a({-href => href(action=>"shortlog")}, "...")); - my $taglist = git_get_refs_list("refs/tags"); - if (defined @$taglist) { + if (@taglist) { git_print_header_div('tags'); - git_tags_body($taglist, 0, 15, + git_tags_body(\@taglist, 0, 15, + $#taglist <= 15 ? undef : $cgi->a({-href => href(action=>"tags")}, "...")); } - my $headlist = git_get_refs_list("refs/heads"); - if (defined @$headlist) { + if (@headlist) { git_print_header_div('heads'); - git_heads_body($headlist, $head, 0, 15, + git_heads_body(\@headlist, $head, 0, 15, + $#headlist <= 15 ? undef : $cgi->a({-href => href(action=>"heads")}, "...")); } + if (@forklist) { + git_print_header_div('forks'); + git_project_list_body(\@forklist, undef, 0, 15, + $#forklist <= 15 ? undef : + $cgi->a({-href => href(action=>"forks")}, "..."), + 'noheader'); + } + git_footer_html(); } @@ -1755,20 +3111,25 @@ sub git_tag { "<table cellspacing=\"0\">\n" . "<tr>\n" . "<td>object</td>\n" . - "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, $tag{'object'}) . "</td>\n" . - "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, $tag{'type'}) . "</td>\n" . + "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'object'}) . "</td>\n" . + "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'type'}) . "</td>\n" . "</tr>\n"; if (defined($tag{'author'})) { my %ad = parse_date($tag{'epoch'}, $tag{'tz'}); print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n"; - print "<tr><td></td><td>" . $ad{'rfc2822'} . sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . "</td></tr>\n"; + print "<tr><td></td><td>" . $ad{'rfc2822'} . + sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . + "</td></tr>\n"; } print "</table>\n\n" . "</div>\n"; print "<div class=\"page_body\">"; my $comment = $tag{'comment'}; foreach my $line (@$comment) { - print esc_html($line) . "<br/>\n"; + chomp $line; + print esc_html($line, -nbsp=>1) . "<br/>\n"; } print "</div>\n"; git_footer_html(); @@ -1777,7 +3138,11 @@ sub git_tag { sub git_blame2 { my $fd; my $ftype; - die_error(undef, "Permission denied") if (!git_get_project_config_bool ('blame')); + + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { + die_error('403 Permission denied', "Permission denied"); + } die_error('404 Not Found', "File name not defined") if (!$file_name); $hash_base ||= git_get_head_hash($project); die_error(undef, "Couldn't find base commit") unless ($hash_base); @@ -1791,51 +3156,100 @@ sub git_blame2 { if ($ftype !~ "blob") { die_error("400 Bad Request", "Object is not a blob"); } - open ($fd, "-|", $GIT, "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 = - $cgi->a({-href => href(action=>"blobl", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blob") . - " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "head"); + $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "blob") . + " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + "HEAD"); git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); - git_print_page_path($file_name, $ftype); + git_print_page_path($file_name, $ftype, $hash_base); my @rev_color = (qw(light2 dark2)); my $num_colors = scalar(@rev_color); my $current_color = 0; my $last_rev; - print "<div class=\"page_body\">\n"; - print "<table class=\"blame\">\n"; - print "<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n"; - while (<$fd>) { - /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/; - my $full_rev = $1; + 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 $_; + 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 = $_; + chomp $data; my $rev = substr($full_rev, 0, 8); - my $lineno = $2; - my $data = $3; - - if (!defined $last_rev) { - $last_rev = $full_rev; - } 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 "<tr class=\"$rev_color[$current_color]\">\n"; - print "<td class=\"sha1\">" . - $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)}, esc_html($rev)) . "</td>\n"; - print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" . esc_html($lineno) . "</a></td>\n"; + if ($group_size) { + print "<td class=\"sha1\""; + print " title=\"". esc_html($author) . ", $date\""; + 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"; + } + open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") + or die_error("could not open git-rev-parse"); + my $parent_commit = <$dd>; + close $dd; + chomp($parent_commit); + 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>"; print "<td class=\"pre\">" . esc_html($data) . "</td>\n"; print "</tr>\n"; } print "</table>\n"; print "</div>"; - close $fd or print "Reading blob failed\n"; + close $fd + or print "Reading blob failed\n"; git_footer_html(); } sub git_blame { my $fd; - die_error('403 Permission denied', "Permission denied") if (!git_get_project_config_bool ('blame')); + + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { + die_error('403 Permission denied', "Permission denied"); + } die_error('404 Not Found', "File name not defined") if (!$file_name); $hash_base ||= git_get_head_hash($project); die_error(undef, "Couldn't find base commit") unless ($hash_base); @@ -1845,15 +3259,21 @@ sub git_blame { $hash = git_get_hash_by_path($hash_base, $file_name, "blob") or die_error(undef, "Error lookup file"); } - open ($fd, "-|", $GIT, "annotate", '-l', '-t', '-r', $file_name, $hash_base) + open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base) or die_error(undef, "Open git-annotate failed"); git_header_html(); my $formats_nav = - $cgi->a({-href => href(action=>"blobl", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blob") . - " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "head"); + $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "blob") . + " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + "HEAD"); git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); - git_print_page_path($file_name, 'blob'); + git_print_page_path($file_name, 'blob', $hash_base); print "<div class=\"page_body\">\n"; print <<HTML; <table class="blame"> @@ -1882,7 +3302,7 @@ HTML chomp $line; $line_class_num = ($line_class_num + 1) % $line_class_len; - if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) { + if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) { $long_rev = $1; $author = $2; $time = $3; @@ -1914,7 +3334,8 @@ HTML HTML } # while (my $line = <$fd>) print "</table>\n\n"; - close $fd or print "Reading blob failed.\n"; + close $fd + or print "Reading blob failed.\n"; print "</div>"; git_footer_html(); } @@ -1925,9 +3346,9 @@ sub git_tags { git_print_page_nav('','', $head,undef,$head); git_print_header_div('summary', $project); - my $taglist = git_get_refs_list("refs/tags"); - if (defined @$taglist) { - git_tags_body($taglist); + my @tagslist = git_get_tags_list(); + if (@tagslist) { + git_tags_body(\@tagslist); } git_footer_html(); } @@ -1938,14 +3359,16 @@ sub git_heads { git_print_page_nav('','', $head,undef,$head); git_print_header_div('summary', $project); - my $taglist = git_get_refs_list("refs/heads"); - if (defined @$taglist) { - git_heads_body($taglist, $head); + my @headslist = git_get_heads_list(); + if (@headslist) { + git_heads_body(\@headslist, $head); } git_footer_html(); } sub git_blob_plain { + my $expires; + if (!defined $hash) { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); @@ -1954,9 +3377,13 @@ sub git_blob_plain { } else { die_error(undef, "No file name defined"); } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; } + my $type = shift; - open my $fd, "-|", $GIT, "cat-file", "blob", $hash + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash or die_error(undef, "Couldn't cat $file_name, $hash"); $type ||= blob_mimetype($fd, $file_name); @@ -1969,7 +3396,10 @@ sub git_blob_plain { $save_as .= '.txt'; } - print $cgi->header(-type => "$type", '-content-disposition' => "inline; filename=\"$save_as\""); + print $cgi->header( + -type => "$type", + -expires=>$expires, + -content_disposition => 'inline; filename="' . "$save_as" . '"'); undef $/; binmode STDOUT, ':raw'; print <$fd>; @@ -1979,6 +3409,8 @@ sub git_blob_plain { } sub git_blob { + my $expires; + if (!defined $hash) { if (defined $file_name) { my $base = $hash_base || git_get_head_hash($project); @@ -1987,27 +3419,48 @@ sub git_blob { } else { die_error(undef, "No file name defined"); } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; } - my $have_blame = git_get_project_config_bool ('blame'); - open my $fd, "-|", $GIT, "cat-file", "blob", $hash + + my ($have_blame) = gitweb_check_feature('blame'); + 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); } - git_header_html(); + # 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))) { if (defined $file_name) { if ($have_blame) { - $formats_nav .= $cgi->a({-href => href(action=>"blame", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blame") . " | "; + $formats_nav .= + $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "blame") . + " | "; } $formats_nav .= - $cgi->a({-href => href(action=>"blob_plain", hash=>$hash, file_name=>$file_name)}, "plain") . - " | " . $cgi->a({-href => href(action=>"blob", hash_base=>"HEAD", file_name=>$file_name)}, "head"); + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blob_plain", + hash=>$hash, file_name=>$file_name)}, + "raw") . + " | " . + $cgi->a({-href => href(action=>"blob", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"); } else { - $formats_nav .= $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain"); + $formats_nav .= + $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw"); } git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); git_print_header_div('commit', esc_html($co{'title'}), $hash_base); @@ -2016,33 +3469,48 @@ sub git_blob { "<br/><br/></div>\n" . "<div class=\"title\">$hash</div>\n"; } - git_print_page_path($file_name, "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); + 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"; + close $fd + or print "Reading blob failed.\n"; print "</div>"; git_footer_html(); } sub git_tree { + my $have_snapshot = gitweb_have_snapshot(); + + if (!defined $hash_base) { + $hash_base = "HEAD"; + } if (!defined $hash) { - $hash = git_get_head_hash($project); if (defined $file_name) { - my $base = $hash_base || $hash; - $hash = git_get_hash_by_path($base, $file_name, "tree"); - } - if (!defined $hash_base) { - $hash_base = $hash; + $hash = git_get_hash_by_path($hash_base, $file_name, "tree"); + } else { + $hash = $hash_base; } } $/ = "\0"; - open my $fd, "-|", $GIT, "ls-tree", '-z', $hash + open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash or die_error(undef, "Open git-ls-tree failed"); my @entries = map { chomp; $_ } <$fd>; close $fd or die_error(undef, "Reading tree failed"); @@ -2051,60 +3519,79 @@ sub git_tree { my $refs = git_get_references(); my $ref = format_ref_marker($refs, $hash_base); git_header_html(); - my $base_key = ""; - my $base = ""; - my $have_blame = git_get_project_config_bool ('blame'); + my $basedir = ''; + my ($have_blame) = gitweb_check_feature('blame'); if (defined $hash_base && (my %co = parse_commit($hash_base))) { - $base_key = ";hb=$hash_base"; - git_print_page_nav('tree','', $hash_base); + my @views_nav = (); + if (defined $file_name) { + push @views_nav, + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "history"), + $cgi->a({-href => href(action=>"tree", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"), + } + if ($have_snapshot) { + # FIXME: Should be available when we have no hash base as well. + push @views_nav, + $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, + "snapshot"); + } + git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav)); git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); } else { + undef $hash_base; print "<div class=\"page_nav\">\n"; print "<br/><br/></div>\n"; print "<div class=\"title\">$hash</div>\n"; } if (defined $file_name) { - $base = esc_html("$file_name/"); + $basedir = $file_name; + if ($basedir ne '' && substr($basedir, -1) ne '/') { + $basedir .= '/'; + } } - git_print_page_path($file_name, 'tree'); + git_print_page_path($file_name, 'tree', $hash_base); print "<div class=\"page_body\">\n"; print "<table cellspacing=\"0\">\n"; - my $alternate = 0; - foreach my $line (@entries) { - #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' - $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/; - my $t_mode = $1; - my $t_type = $2; - my $t_hash = $3; - my $t_name = validate_input($4); + my $alternate = 1; + # '..' (top directory) link if possible + if (defined $hash_base && + defined $file_name && $file_name =~ m![^/]+$!) { if ($alternate) { print "<tr class=\"dark\">\n"; } else { print "<tr class=\"light\">\n"; } $alternate ^= 1; - print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n"; - if ($t_type eq "blob") { - print "<td class=\"list\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name"), -class => "list"}, esc_html($t_name)) . - "</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name")}, "blob"); - if ($have_blame) { - print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$t_hash$base_key;f=$base$t_name")}, "blame"); - } - print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$t_hash;hb=$hash_base;f=$base$t_name")}, "history") . - " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$t_hash;f=$base$t_name")}, "raw") . - "</td>\n"; - } elsif ($t_type eq "tree") { - print "<td class=\"list\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, esc_html($t_name)) . - "</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, "tree") . - " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;hb=$hash_base;f=$base$t_name")}, "history") . - "</td>\n"; + + my $up = $file_name; + $up =~ s!/?[^/]+$!!; + undef $up unless $up; + # based on git_print_tree_entry + print '<td class="mode">' . mode_str('040000') . "</td>\n"; + print '<td class="list">'; + print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base, + file_name=>$up)}, + ".."); + print "</td>\n"; + print "<td class=\"link\"></td>\n"; + + print "</tr>\n"; + } + foreach my $line (@entries) { + my %t = parse_ls_tree_line($line, -z => 1); + + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; } + $alternate ^= 1; + + git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame); + print "</tr>\n"; } print "</table>\n" . @@ -2112,6 +3599,37 @@ sub git_tree { git_footer_html(); } +sub git_snapshot { + my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); + my $have_snapshot = (defined $ctype && defined $suffix); + if (!$have_snapshot) { + die_error('403 Permission denied', "Permission denied"); + } + + if (!defined $hash) { + $hash = git_get_head_hash($project); + } + + my $filename = basename($project) . "-$hash.tar.$suffix"; + + print $cgi->header( + -type => "application/$ctype", + -content_disposition => 'inline; filename="' . "$filename" . '"', + -status => '200 OK'); + + my $git = git_cmd_str(); + my $name = $project; + $name =~ s/\047/\047\\\047\047/g; + open my $fd, "-|", + "$git archive --format=tar --prefix=\'$name\'/ $hash | $command" + or die_error(undef, "Execute git-tar-tree failed."); + binmode STDOUT, ':raw'; + print <$fd>; + binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi + close $fd; + +} + sub git_log { my $head = git_get_head_hash($project); if (!defined $hash) { @@ -2122,28 +3640,25 @@ sub git_log { } my $refs = git_get_references(); - my $limit = sprintf("--max-count=%i", (100 * ($page+1))); - open my $fd, "-|", $GIT, "rev-list", $limit, $hash - or die_error(undef, "Open git-rev-list failed"); - my @revlist = map { chomp; $_ } <$fd>; - close $fd; + my @commitlist = parse_commits($hash, 101, (100 * $page)); - my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist); + my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1))); git_header_html(); git_print_page_nav('log','', $hash,undef,undef, $paging_nav); - if (!@revlist) { + if (!@commitlist) { my %co = parse_commit($hash); git_print_header_div('summary', $project); print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n"; } - for (my $i = ($page * 100); $i <= $#revlist; $i++) { - my $commit = $revlist[$i]; - my $ref = format_ref_marker($refs, $commit); - my %co = parse_commit($commit); + my $to = ($#commitlist >= 99) ? (99) : ($#commitlist); + for (my $i = 0; $i <= $to; $i++) { + my %co = %{$commitlist[$i]}; next if !%co; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); my %ad = parse_date($co{'author_epoch'}); git_print_header_div('commit', "<span class=\"age\">$co{'age_string'}</span>" . @@ -2152,37 +3667,30 @@ sub git_log { print "<div class=\"title_text\">\n" . "<div class=\"log_link\">\n" . $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . - " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . "<br/>\n" . "</div>\n" . "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" . - "</div>\n" . - "<div class=\"log_body\">\n"; - my $comment = $co{'comment'}; - my $empty = 0; - foreach my $line (@$comment) { - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - next; - } - if ($line eq "") { - if ($empty) { - next; - } - $empty = 1; - } else { - $empty = 0; - } - print format_log_line_html($line) . "<br/>\n"; - } - if (!$empty) { - print "<br/>\n"; - } + "</div>\n"; + + print "<div class=\"log_body\">\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1); + print "</div>\n"; + } + if ($#commitlist >= 100) { + print "<div class=\"page_nav\">\n"; + print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); print "</div>\n"; } git_footer_html(); } sub git_commit { + $hash ||= $hash_base || "HEAD"; my %co = parse_commit($hash); if (!%co) { die_error(undef, "Unknown commit object"); @@ -2190,14 +3698,46 @@ sub git_commit { my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); - my $parent = $co{'parent'}; + my $parent = $co{'parent'}; + my $parents = $co{'parents'}; # listref + + # we need to prepare $formats_nav before any parameter munging + my $formats_nav; + if (!defined $parent) { + # --root commitdiff + $formats_nav .= '(initial)'; + } elsif (@$parents == 1) { + # single parent commit + $formats_nav .= + '(parent: ' . + $cgi->a({-href => href(action=>"commit", + hash=>$parent)}, + esc_html(substr($parent, 0, 7))) . + ')'; + } else { + # merge commit + $formats_nav .= + '(merge: ' . + join(' ', map { + $cgi->a({-href => href(action=>"commitdiff", + hash=>$_)}, + esc_html(substr($_, 0, 7))); + } @$parents ) . + ')'; + } + if (!defined $parent) { $parent = "--root"; } - open my $fd, "-|", $GIT, "diff-tree", '-r', '-M', $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"); + my @difftree; + if (@$parents <= 1) { + # difftree output is not printed for merges + open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id", + @diff_opts, $parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd or die_error(undef, "Reading git-diff-tree failed"); + } # non-textual hash id's can be cached my $expires; @@ -2206,15 +3746,13 @@ sub git_commit { } my $refs = git_get_references(); my $ref = format_ref_marker($refs, $co{'id'}); - my $formats_nav = ''; - if (defined $file_name && defined $co{'parent'}) { - my $parent = $co{'parent'}; - $formats_nav .= $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)}, "blame"); - } + + my $have_snapshot = gitweb_have_snapshot(); + git_header_html(undef, $expires); - git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff', - $hash, $co{'tree'}, $hash, - $formats_nav); + git_print_page_nav('commit', '', + $hash, $co{'tree'}, $hash, + $formats_nav); if (defined $co{'parent'}) { git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash); @@ -2227,281 +3765,503 @@ sub git_commit { "<tr>" . "<td></td><td> $ad{'rfc2822'}"; if ($ad{'hour_local'} < 6) { - printf(" (<span class=\"atnight\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + printf(" (<span class=\"atnight\">%02d:%02d</span> %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); } else { - printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); + printf(" (%02d:%02d %s)", + $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}); } print "</td>" . "</tr>\n"; print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n"; - print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n"; + print "<tr><td></td><td> $cd{'rfc2822'}" . + sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . + "</td></tr>\n"; print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n"; print "<tr>" . "<td>tree</td>" . "<td class=\"sha1\">" . - $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash), class => "list"}, $co{'tree'}) . - "</td>" . - "<td class=\"link\">" . $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, "tree") . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash), + class => "list"}, $co{'tree'}) . "</td>" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, + "tree"); + if ($have_snapshot) { + print " | " . + $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot"); + } + print "</td>" . "</tr>\n"; - my $parents = $co{'parents'}; + foreach my $par (@$parents) { print "<tr>" . "<td>parent</td>" . - "<td class=\"sha1\">" . $cgi->a({-href => href(action=>"commit", hash=>$par), class => "list"}, $par) . "</td>" . + "<td class=\"sha1\">" . + $cgi->a({-href => href(action=>"commit", hash=>$par), + class => "list"}, $par) . + "</td>" . "<td class=\"link\">" . $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") . - " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") . "</td>" . "</tr>\n"; } print "</table>". "</div>\n"; + print "<div class=\"page_body\">\n"; - my $comment = $co{'comment'}; - my $empty = 0; - my $signed = 0; - foreach my $line (@$comment) { - # print only one empty line - if ($line eq "") { - if ($empty || $signed) { - next; + git_print_log($co{'comment'}); + print "</div>\n"; + + if (@$parents <= 1) { + # do not output difftree/whatchanged for merges + git_difftree_body(\@difftree, $hash, $parent); + } + + git_footer_html(); +} + +sub git_object { + # object is defined by: + # - hash or hash_base alone + # - hash_base and file_name + my $type; + + # - hash or hash_base alone + if ($hash || ($hash_base && !defined $file_name)) { + my $object_id = $hash || $hash_base; + + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null" + or die_error('404 Not Found', "Object does not exist"); + $type = <$fd>; + chomp $type; + close $fd + or die_error('404 Not Found', "Object does not exist"); + + # - hash_base and file_name + } elsif ($hash_base && defined $file_name) { + $file_name =~ s,/+$,,; + + system(git_cmd(), "cat-file", '-e', $hash_base) == 0 + or die_error('404 Not Found', "Base object does not exist"); + + # here errors should not hapen + open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name + or die_error(undef, "Open git-ls-tree failed"); + my $line = <$fd>; + close $fd; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) { + die_error('404 Not Found', "File or directory for given base does not exist"); + } + $type = $2; + $hash = $3; + } else { + die_error('404 Not Found', "Not enough information to find object"); + } + + print $cgi->redirect(-uri => href(action=>$type, -full=>1, + hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name), + -status => '302 Found'); +} + +sub git_blobdiff { + my $format = shift || 'html'; + + my $fd; + my @difftree; + my %diffinfo; + my $expires; + + # preparing $fd and %diffinfo for git_patchset_body + # new style URI + if (defined $hash_base && defined $hash_parent_base) { + if (defined $file_name) { + # read raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } elsif (defined $hash && + $hash =~ /[0-9a-fA-F]{40}/) { + # try to find filename from $hash + + # read filtered raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, "--" + or die_error(undef, "Open git-diff-tree failed"); + @difftree = + # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c' + # $hash == to_id + grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ } + map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters"); + } + + if (@difftree > 1) { + die_error('404 Not Found', "Ambiguous blob diff specification"); + } + + %diffinfo = parse_difftree_raw_line($difftree[0]); + $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'}; + $file_name ||= $diffinfo{'to_file'} || $diffinfo{'file'}; + + $hash_parent ||= $diffinfo{'from_id'}; + $hash ||= $diffinfo{'to_id'}; + + # non-textual hash id's can be cached + if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ && + $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "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; } - $empty = 1; + } 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", '-p', @diff_opts, + $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff failed"); + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters") + unless %diffinfo; + } + + # header + if ($format eq 'html') { + my $formats_nav = + $cgi->a({-href => href(action=>"blobdiff_plain", + hash=>$hash, hash_parent=>$hash_parent, + hash_base=>$hash_base, hash_parent_base=>$hash_parent_base, + file_name=>$file_name, file_parent=>$file_parent)}, + "raw"); + git_header_html(undef, $expires); + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); } else { - $empty = 0; + print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n"; + print "<div class=\"title\">$hash vs $hash_parent</div>\n"; } - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - $signed = 1; - print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n"; + if (defined $file_name) { + git_print_page_path($file_name, "blob", $hash_base); } else { - $signed = 0; - print format_log_line_html($line) . "<br/>\n"; + print "<div class=\"page_path\"></div>\n"; } + + } elsif ($format eq 'plain') { + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$file_name" . '.patch"'); + + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + } else { + die_error(undef, "Unknown blobdiff format"); } - print "</div>\n"; - git_difftree_body(\@difftree, $parent); + # patch + if ($format eq 'html') { + print "<div class=\"page_body\">\n"; - git_footer_html(); -} + git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base); + close $fd; + + print "</div>\n"; # class="page_body" + git_footer_html(); -sub git_blobdiff { - mkdir($git_temp, 0700); - git_header_html(); - if (defined $hash_base && (my %co = parse_commit($hash_base))) { - my $formats_nav = - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff_plain;h=$hash;hp=$hash_parent")}, "plain"); - git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); } else { - print "<div class=\"page_nav\">\n" . - "<br/><br/></div>\n" . - "<div class=\"title\">$hash vs $hash_parent</div>\n"; - } - git_print_page_path($file_name, "blob"); - print "<div class=\"page_body\">\n" . - "<div class=\"diff_info\">blob:" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name")}, $hash_parent) . - " -> blob:" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, $hash) . - "</div>\n"; - git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash); - print "</div>"; - git_footer_html(); + while (my $line = <$fd>) { + $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg; + $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg; + + print $line; + + last if $line =~ m!^\+\+\+!; + } + local $/ = undef; + print <$fd>; + close $fd; + } } sub git_blobdiff_plain { - mkdir($git_temp, 0700); - print $cgi->header(-type => "text/plain", -charset => 'utf-8'); - git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain"); + git_blobdiff('plain'); } sub git_commitdiff { - mkdir($git_temp, 0700); + my $format = shift || 'html'; + $hash ||= $hash_base || "HEAD"; my %co = parse_commit($hash); if (!%co) { die_error(undef, "Unknown commit object"); } - if (!defined $hash_parent) { - $hash_parent = $co{'parent'} || '--root'; - } - open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_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"); - # non-textual hash id's can be cached - my $expires; - if ($hash =~ m/^[0-9a-fA-F]{40}$/) { - $expires = "+1d"; - } - my $refs = git_get_references(); - my $ref = format_ref_marker($refs, $co{'id'}); - my $formats_nav = - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff_plain;h=$hash;hp=$hash_parent")}, "plain"); - git_header_html(undef, $expires); - git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav); - git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash); - print "<div class=\"page_body\">\n"; - my $comment = $co{'comment'}; - my $empty = 0; - my $signed = 0; - my @log = @$comment; - # remove first and empty lines after that - shift @log; - while (defined $log[0] && $log[0] eq "") { - shift @log; - } - foreach my $line (@log) { - if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { - next; - } - if ($line eq "") { - if ($empty) { - next; + # 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); } - $empty = 1; + $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 { - $empty = 0; - } - print format_log_line_html($line) . "<br/>\n"; - } - print "<br/>\n"; - foreach my $line (@difftree) { - # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c' - # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c' - if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) { - next; - } - my $from_mode = $1; - my $to_mode = $2; - my $from_id = $3; - my $to_id = $4; - my $status = $5; - my $file = validate_input(unquote($6)); - if ($status eq "A") { - print "<div class=\"diff_info\">" . file_type($to_mode) . ":" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" . - "</div>\n"; - git_diff_print(undef, "/dev/null", $to_id, "b/$file"); - } elsif ($status eq "D") { - print "<div class=\"diff_info\">" . file_type($from_mode) . ":" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash_parent;f=$file")}, $from_id) . "(deleted)" . - "</div>\n"; - git_diff_print($from_id, "a/$file", undef, "/dev/null"); - } elsif ($status eq "M") { - if ($from_id ne $to_id) { - print "<div class=\"diff_info\">" . - file_type($from_mode) . ":" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash_parent;f=$file")}, $from_id) . - " -> " . - file_type($to_mode) . ":" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id); - print "</div>\n"; - git_diff_print($from_id, "a/$file", $to_id, "b/$file"); - } + # merge commit + $formats_nav .= + ' (merge: ' . + join(' ', map { + $cgi->a({-href => href(action=>"commitdiff", + hash=>$_)}, + esc_html(substr($_, 0, 7))); + } @{$co{'parents'}} ) . + ')'; } } - print "<br/>\n" . - "</div>"; - git_footer_html(); -} -sub git_commitdiff_plain { - mkdir($git_temp, 0700); - my %co = parse_commit($hash); - if (!%co) { - die_error(undef, "Unknown commit object"); - } if (!defined $hash_parent) { $hash_parent = $co{'parent'} || '--root'; } - open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash - or die_error(undef, "Open git-diff-tree failed"); - my @difftree = map { chomp; $_ } <$fd>; - close $fd or die_error(undef, "Reading diff-tree failed"); - # try to figure out the next tag after this commit - my $tagname; - my $refs = git_get_references("tags"); - open $fd, "-|", $GIT, "rev-list", "HEAD"; - my @commits = map { chomp; $_ } <$fd>; - close $fd; - foreach my $commit (@commits) { - if (defined $refs->{$commit}) { - $tagname = $refs->{$commit} - } - if ($commit eq $hash) { - last; + # read commitdiff + my $fd; + my @difftree; + if ($format eq 'html') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + "--no-commit-id", "--patch-with-raw", "--full-index", + $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + + while (my $line = <$fd>) { + chomp $line; + # empty line ends raw part of diff-tree output + last unless $line; + push @difftree, $line; } - } - print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\""); - my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); - my $comment = $co{'comment'}; - print "From: $co{'author'}\n" . - "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n". - "Subject: $co{'title'}\n"; - if (defined $tagname) { - print "X-Git-Tag: $tagname\n"; + } elsif ($format eq 'plain') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + + } else { + die_error(undef, "Unknown commitdiff format"); } - print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" . - "\n"; - foreach my $line (@$comment) {; - print "$line\n"; + # non-textual hash id's can be cached + my $expires; + if ($hash =~ m/^[0-9a-fA-F]{40}$/) { + $expires = "+1d"; } - print "---\n\n"; - foreach my $line (@difftree) { - if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) { - next; + # write commit message + if ($format eq 'html') { + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $co{'id'}); + + git_header_html(undef, $expires); + git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash); + git_print_authorship(\%co); + print "<div class=\"page_body\">\n"; + 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" } - my $from_id = $3; - my $to_id = $4; - my $status = $5; - my $file = $6; - if ($status eq "A") { - git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain"); - } elsif ($status eq "D") { - git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain"); - } elsif ($status eq "M") { - git_diff_print($from_id, "a/$file", $to_id, "b/$file", "plain"); + + } elsif ($format eq 'plain') { + my $refs = git_get_references("tags"); + my $tagname = git_get_rev_name_tags($hash); + my $filename = basename($project) . "-$hash.patch"; + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$filename" . '"'); + my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); + print <<TEXT; +From: $co{'author'} +Date: $ad{'rfc2822'} ($ad{'tz_local'}) +Subject: $co{'title'} +TEXT + print "X-Git-Tag: $tagname\n" if $tagname; + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + foreach my $line (@{$co{'comment'}}) { + print "$line\n"; } + print "---\n\n"; + } + + # write patch + if ($format eq 'html') { + git_difftree_body(\@difftree, $hash, $hash_parent); + print "<br/>\n"; + + git_patchset_body($fd, \@difftree, $hash, $hash_parent); + close $fd; + print "</div>\n"; # class="page_body" + git_footer_html(); + + } elsif ($format eq 'plain') { + local $/ = undef; + print <$fd>; + close $fd + or print "Reading git-diff-tree failed\n"; } } +sub git_commitdiff_plain { + git_commitdiff('plain'); +} + sub git_history { if (!defined $hash_base) { $hash_base = git_get_head_hash($project); } + if (!defined $page) { + $page = 0; + } my $ftype; my %co = parse_commit($hash_base); if (!%co) { die_error(undef, "Unknown commit object"); } + my $refs = git_get_references(); - git_header_html(); - git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base); - git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + my $limit = sprintf("--max-count=%i", (100 * ($page+1))); + if (!defined $hash && defined $file_name) { $hash = git_get_hash_by_path($hash_base, $file_name); } if (defined $hash) { $ftype = git_get_type($hash); } - git_print_page_path($file_name, $ftype); - open my $fd, "-|", - $GIT, "rev-list", "--full-history", $hash_base, "--", $file_name; - git_history_body($fd, $refs, $hash_base, $ftype); + my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name); + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name)}, + "first"); + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#commitlist >= 100) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_header_html(); + git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + git_print_page_path($file_name, $ftype, $hash_base); + + git_history_body(\@commitlist, 0, 99, + $refs, $hash_base, $ftype, $next_link); - close $fd; git_footer_html(); } sub git_search { + my ($have_search) = gitweb_check_feature('search'); + if (!$have_search) { + die_error('403 Permission denied', "Permission denied"); + } if (!defined $searchtext) { die_error(undef, "Text field empty"); } @@ -2512,79 +4272,82 @@ sub git_search { if (!%co) { die_error(undef, "Unknown commit object"); } - # 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 $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; + if (!defined $page) { + $page = 0; + } + + $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'); + if (!$have_pickaxe) { + 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 = 0; - if ($commit_search) { - $/ = "\0"; - open my $fd, "-|", $GIT, "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) { - next; - } - if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) { - next; - } - my @commit_lines = split "\n", $commit_text; - my %co = parse_commit(undef, \@commit_lines); - if (!%co) { - next; - } - if ($alternate) { - print "<tr class=\"dark\">\n"; - } else { - print "<tr class=\"light\">\n"; - } - $alternate ^= 1; - print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . - "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" . - "<td>" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . esc_html(chop_str($co{'title'}, 50)) . "</b><br/>"); - my $comment = $co{'comment'}; - foreach my $line (@$comment) { - if ($line =~ m/^(.*)($searchtext)(.*)$/i) { - my $lead = esc_html($1) || ""; - $lead = chop_str($lead, 30, 10); - my $match = esc_html($2) || ""; - my $trail = esc_html($3) || ""; - $trail = chop_str($trail, 30, 10); - my $text = "$lead<span class=\"match\">$match</span>$trail"; - print chop_str($text, 80, 5) . "<br/>\n"; - } - } - print "</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") . - " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree"); - print "</td>\n" . - "</tr>\n"; + if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') { + my $greptype; + if ($searchtype eq 'commit') { + $greptype = "--grep="; + } elsif ($searchtype eq 'author') { + $greptype = "--author="; + } elsif ($searchtype eq 'committer') { + $greptype = "--committer="; } - close $fd; + $greptype .= $searchtext; + my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype); + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype)}, + "first"); + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#commitlist >= 100) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + git_search_grep_body(\@commitlist, 0, 99, $next_link); } - if ($pickaxe_search) { + if ($searchtype eq 'pickaxe') { + 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; $/ = "\n"; - open my $fd, "-|", "$GIT rev-list $hash | $GIT diff-tree -r --stdin -S\'$searchtext\'"; + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command rev-list $hash | " . + "$git_command diff-tree -r --stdin -S\'$searchtext\'"; undef %co; my @files; while (my $line = <$fd>) { @@ -2612,18 +4375,22 @@ sub git_search { print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" . "<td>" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . - esc_html(chop_str($co{'title'}, 50)) . "</b><br/>"); + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + esc_html(chop_str($co{'title'}, 50)) . "<br/>"); while (my $setref = shift @files) { my %set = %$setref; - print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$set{'id'};hb=$co{'id'};f=$set{'file'}"), class => "list"}, - "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") . + print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, + hash=>$set{'id'}, file_name=>$set{'file'}), + -class => "list"}, + "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") . "<br/>\n"; } print "</td>\n" . "<td class=\"link\">" . - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") . - " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree"); + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); print "</td>\n" . "</tr>\n"; } @@ -2631,8 +4398,34 @@ sub git_search { } } close $fd; + + print "</table>\n"; } - print "</table>\n"; + 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(); } @@ -2646,102 +4439,268 @@ sub git_shortlog { } my $refs = git_get_references(); - my $limit = sprintf("--max-count=%i", (100 * ($page+1))); - open my $fd, "-|", $GIT, "rev-list", $limit, $hash - or die_error(undef, "Open git-rev-list failed"); - my @revlist = map { chomp; $_ } <$fd>; - close $fd; + my @commitlist = parse_commits($hash, 101, (100 * $page)); - my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist); + my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1))); my $next_link = ''; - if ($#revlist >= (100 * ($page+1)-1)) { + if ($#commitlist >= 100) { $next_link = - $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page+1)), - -title => "Alt-n"}, "next"); + $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); } - git_header_html(); git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav); git_print_header_div('summary', $project); - git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link); + git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link); git_footer_html(); } ## ...................................................................... -## feeds (RSS, OPML) +## feeds (RSS, Atom; OPML) + +sub git_feed { + my $format = shift || 'atom'; + my ($have_blame) = gitweb_check_feature('blame'); + + # Atom: http://www.atomenabled.org/developers/syndication/ + # RSS: 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'; + my @commitlist = parse_commits($head, 150); + + my %latest_commit; + my %latest_date; + my $content_type = "application/$format+xml"; + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->Accept('text/xml') > $cgi->Accept($content_type)) { + # browser (feed reader) prefers text/xml + $content_type = 'text/xml'; + } + if (defined($commitlist[0])) { + %latest_commit = %{$commitlist[0]}; + %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'); + } -sub git_rss { - # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ - open my $fd, "-|", $GIT, "rev-list", "--max-count=150", git_get_head_hash($project) - 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 version=\"1.0\" encoding=\"utf-8\"?>\n". - "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n"; - print "<channel>\n"; - print "<title>$project</title>\n". - "<link>" . esc_html("$my_url?p=$project;a=summary") . "</link>\n". - "<description>$project log</description>\n". - "<language>en</language>\n"; - - for (my $i = 0; $i <= $#revlist; $i++) { - my $commit = $revlist[$i]; - my %co = parse_commit($commit); + # 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> +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 <= $#commitlist; $i++) { + my %co = %{$commitlist[$i]}; + my $commit = $co{'id'}; # we read 150, we always show 30 and the ones more recent than 48 hours - if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) { + if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) { last; } - my %cd = parse_date($co{'committer_epoch'}); - open $fd, "-|", $GIT, "diff-tree", '-r', $co{'parent'}, $co{'id'} or next; + my %cd = parse_date($co{'author_epoch'}); + + # get list of changed files + open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $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"; + close $fd + or next; + + # 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 = decode("utf8", $line, Encode::FB_DEFAULT); - 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'); } - my $file = validate_input(unquote($7)); - $file = decode("utf8", $file, Encode::FB_DEFAULT); - print "$file<br/>\n"; + # if this is not a feed of a file history + if (!defined $file_name || $file_name ne $file) { + print $cgi->a({-href => href(-full=>1, action=>"history", + file_name=>$file, hash=>$commit), + -title => "history"}, 'H'); + } + $file = esc_path($file); + print "] ". + "$file</li>\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"; } - print "]]>\n" . - "</content:encoded>\n" . - "</item>\n"; } - print "</channel></rss>"; + + # end of feed + if ($format eq 'rss') { + print "</channel>\n</rss>\n"; + } elsif ($format eq 'atom') { + print "</feed>\n"; + } +} + +sub git_rss { + git_feed('rss'); +} + +sub git_atom { + git_feed('atom'); } sub git_opml { my @list = git_get_projects_list(); print $cgi->header(-type => 'text/xml', -charset => 'utf-8'); - print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n". - "<opml version=\"1.0\">\n". - "<head>". - " <title>$site_name Git OPML Export</title>\n". - "</head>\n". - "<body>\n". - "<outline text=\"git RSS feeds\">\n"; + print <<XML; +<?xml version="1.0" encoding="utf-8"?> +<opml version="1.0"> +<head> + <title>$site_name OPML Export</title> +</head> +<body> +<outline text="git RSS feeds"> +XML foreach my $pr (@list) { my %proj = %$pr; @@ -2749,7 +4708,7 @@ sub git_opml { if (!defined $head) { next; } - $ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}"; + $git_dir = "$projectroot/$proj{'path'}"; my %co = parse_commit($head); if (!%co) { next; @@ -2760,7 +4719,9 @@ sub git_opml { my $html = "$my_url?p=$proj{'path'};a=summary"; print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n"; } - print "</outline>\n". - "</body>\n". - "</opml>\n"; + print <<XML; +</outline> +</body> +</opml> +XML } |