1#!/usr/bin/perl 2 3# gitweb - simple web interface to track changes in git repositories 4# 5# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org> 6# (C) 2005, Christian Gierke 7# 8# This program is licensed under the GPLv2 9 10use strict; 11use warnings; 12use CGI qw(:standard :escapeHTML -nosticky); 13use CGI::Util qw(unescape); 14use CGI::Carp qw(fatalsToBrowser); 15use Encode; 16use Fcntl ':mode'; 17use File::Find qw(); 18use File::Basename qw(basename); 19binmode STDOUT,':utf8'; 20 21our$t0; 22if(eval{require Time::HiRes;1; }) { 23$t0= [Time::HiRes::gettimeofday()]; 24} 25our$number_of_git_cmds=0; 26 27BEGIN{ 28 CGI->compile()if$ENV{'MOD_PERL'}; 29} 30 31our$version="++GIT_VERSION++"; 32 33our($my_url,$my_uri,$base_url,$path_info,$home_link); 34sub evaluate_uri { 35our$cgi; 36 37our$my_url=$cgi->url(); 38our$my_uri=$cgi->url(-absolute =>1); 39 40# Base URL for relative URLs in gitweb ($logo, $favicon, ...), 41# needed and used only for URLs with nonempty PATH_INFO 42our$base_url=$my_url; 43 44# When the script is used as DirectoryIndex, the URL does not contain the name 45# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we 46# have to do it ourselves. We make $path_info global because it's also used 47# later on. 48# 49# Another issue with the script being the DirectoryIndex is that the resulting 50# $my_url data is not the full script URL: this is good, because we want 51# generated links to keep implying the script name if it wasn't explicitly 52# indicated in the URL we're handling, but it means that $my_url cannot be used 53# as base URL. 54# Therefore, if we needed to strip PATH_INFO, then we know that we have 55# to build the base URL ourselves: 56our$path_info=$ENV{"PATH_INFO"}; 57if($path_info) { 58if($my_url=~ s,\Q$path_info\E$,, && 59$my_uri=~ s,\Q$path_info\E$,, && 60defined$ENV{'SCRIPT_NAME'}) { 61$base_url=$cgi->url(-base =>1) .$ENV{'SCRIPT_NAME'}; 62} 63} 64 65# target of the home link on top of all pages 66our$home_link=$my_uri||"/"; 67} 68 69# core git executable to use 70# this can just be "git" if your webserver has a sensible PATH 71our$GIT="++GIT_BINDIR++/git"; 72 73# absolute fs-path which will be prepended to the project path 74#our $projectroot = "/pub/scm"; 75our$projectroot="++GITWEB_PROJECTROOT++"; 76 77# fs traversing limit for getting project list 78# the number is relative to the projectroot 79our$project_maxdepth="++GITWEB_PROJECT_MAXDEPTH++"; 80 81# string of the home link on top of all pages 82our$home_link_str="++GITWEB_HOME_LINK_STR++"; 83 84# name of your site or organization to appear in page titles 85# replace this with something more descriptive for clearer bookmarks 86our$site_name="++GITWEB_SITENAME++" 87|| ($ENV{'SERVER_NAME'} ||"Untitled") ." Git"; 88 89# filename of html text to include at top of each page 90our$site_header="++GITWEB_SITE_HEADER++"; 91# html text to include at home page 92our$home_text="++GITWEB_HOMETEXT++"; 93# filename of html text to include at bottom of each page 94our$site_footer="++GITWEB_SITE_FOOTER++"; 95 96# URI of stylesheets 97our@stylesheets= ("++GITWEB_CSS++"); 98# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG. 99our$stylesheet=undef; 100# URI of GIT logo (72x27 size) 101our$logo="++GITWEB_LOGO++"; 102# URI of GIT favicon, assumed to be image/png type 103our$favicon="++GITWEB_FAVICON++"; 104# URI of gitweb.js (JavaScript code for gitweb) 105our$javascript="++GITWEB_JS++"; 106 107# URI and label (title) of GIT logo link 108#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; 109#our $logo_label = "git documentation"; 110our$logo_url="http://git-scm.com/"; 111our$logo_label="git homepage"; 112 113# source of projects list 114our$projects_list="++GITWEB_LIST++"; 115 116# the width (in characters) of the projects list "Description" column 117our$projects_list_description_width=25; 118 119# default order of projects list 120# valid values are none, project, descr, owner, and age 121our$default_projects_order="project"; 122 123# show repository only if this file exists 124# (only effective if this variable evaluates to true) 125our$export_ok="++GITWEB_EXPORT_OK++"; 126 127# show repository only if this subroutine returns true 128# when given the path to the project, for example: 129# sub { return -e "$_[0]/git-daemon-export-ok"; } 130our$export_auth_hook=undef; 131 132# only allow viewing of repositories also shown on the overview page 133our$strict_export="++GITWEB_STRICT_EXPORT++"; 134 135# list of git base URLs used for URL to where fetch project from, 136# i.e. full URL is "$git_base_url/$project" 137our@git_base_url_list=grep{$_ne''} ("++GITWEB_BASE_URL++"); 138 139# default blob_plain mimetype and default charset for text/plain blob 140our$default_blob_plain_mimetype='text/plain'; 141our$default_text_plain_charset=undef; 142 143# file to use for guessing MIME types before trying /etc/mime.types 144# (relative to the current git repository) 145our$mimetypes_file=undef; 146 147# assume this charset if line contains non-UTF-8 characters; 148# it should be valid encoding (see Encoding::Supported(3pm) for list), 149# for which encoding all byte sequences are valid, for example 150# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it 151# could be even 'utf-8' for the old behavior) 152our$fallback_encoding='latin1'; 153 154# rename detection options for git-diff and git-diff-tree 155# - default is '-M', with the cost proportional to 156# (number of removed files) * (number of new files). 157# - more costly is '-C' (which implies '-M'), with the cost proportional to 158# (number of changed files + number of removed files) * (number of new files) 159# - even more costly is '-C', '--find-copies-harder' with cost 160# (number of files in the original tree) * (number of new files) 161# - one might want to include '-B' option, e.g. '-B', '-M' 162our@diff_opts= ('-M');# taken from git_commit 163 164# Disables features that would allow repository owners to inject script into 165# the gitweb domain. 166our$prevent_xss=0; 167 168# information about snapshot formats that gitweb is capable of serving 169our%known_snapshot_formats= ( 170# name => { 171# 'display' => display name, 172# 'type' => mime type, 173# 'suffix' => filename suffix, 174# 'format' => --format for git-archive, 175# 'compressor' => [compressor command and arguments] 176# (array reference, optional) 177# 'disabled' => boolean (optional)} 178# 179'tgz'=> { 180'display'=>'tar.gz', 181'type'=>'application/x-gzip', 182'suffix'=>'.tar.gz', 183'format'=>'tar', 184'compressor'=> ['gzip']}, 185 186'tbz2'=> { 187'display'=>'tar.bz2', 188'type'=>'application/x-bzip2', 189'suffix'=>'.tar.bz2', 190'format'=>'tar', 191'compressor'=> ['bzip2']}, 192 193'txz'=> { 194'display'=>'tar.xz', 195'type'=>'application/x-xz', 196'suffix'=>'.tar.xz', 197'format'=>'tar', 198'compressor'=> ['xz'], 199'disabled'=>1}, 200 201'zip'=> { 202'display'=>'zip', 203'type'=>'application/x-zip', 204'suffix'=>'.zip', 205'format'=>'zip'}, 206); 207 208# Aliases so we understand old gitweb.snapshot values in repository 209# configuration. 210our%known_snapshot_format_aliases= ( 211'gzip'=>'tgz', 212'bzip2'=>'tbz2', 213'xz'=>'txz', 214 215# backward compatibility: legacy gitweb config support 216'x-gzip'=>undef,'gz'=>undef, 217'x-bzip2'=>undef,'bz2'=>undef, 218'x-zip'=>undef,''=>undef, 219); 220 221# Pixel sizes for icons and avatars. If the default font sizes or lineheights 222# are changed, it may be appropriate to change these values too via 223# $GITWEB_CONFIG. 224our%avatar_size= ( 225'default'=>16, 226'double'=>32 227); 228 229# Used to set the maximum load that we will still respond to gitweb queries. 230# If server load exceed this value then return "503 server busy" error. 231# If gitweb cannot determined server load, it is taken to be 0. 232# Leave it undefined (or set to 'undef') to turn off load checking. 233our$maxload=300; 234 235# You define site-wide feature defaults here; override them with 236# $GITWEB_CONFIG as necessary. 237our%feature= ( 238# feature => { 239# 'sub' => feature-sub (subroutine), 240# 'override' => allow-override (boolean), 241# 'default' => [ default options...] (array reference)} 242# 243# if feature is overridable (it means that allow-override has true value), 244# then feature-sub will be called with default options as parameters; 245# return value of feature-sub indicates if to enable specified feature 246# 247# if there is no 'sub' key (no feature-sub), then feature cannot be 248# overriden 249# 250# use gitweb_get_feature(<feature>) to retrieve the <feature> value 251# (an array) or gitweb_check_feature(<feature>) to check if <feature> 252# is enabled 253 254# Enable the 'blame' blob view, showing the last commit that modified 255# each line in the file. This can be very CPU-intensive. 256 257# To enable system wide have in $GITWEB_CONFIG 258# $feature{'blame'}{'default'} = [1]; 259# To have project specific config enable override in $GITWEB_CONFIG 260# $feature{'blame'}{'override'} = 1; 261# and in project config gitweb.blame = 0|1; 262'blame'=> { 263'sub'=>sub{ feature_bool('blame',@_) }, 264'override'=>0, 265'default'=> [0]}, 266 267# Enable the 'snapshot' link, providing a compressed archive of any 268# tree. This can potentially generate high traffic if you have large 269# project. 270 271# Value is a list of formats defined in %known_snapshot_formats that 272# you wish to offer. 273# To disable system wide have in $GITWEB_CONFIG 274# $feature{'snapshot'}{'default'} = []; 275# To have project specific config enable override in $GITWEB_CONFIG 276# $feature{'snapshot'}{'override'} = 1; 277# and in project config, a comma-separated list of formats or "none" 278# to disable. Example: gitweb.snapshot = tbz2,zip; 279'snapshot'=> { 280'sub'=> \&feature_snapshot, 281'override'=>0, 282'default'=> ['tgz']}, 283 284# Enable text search, which will list the commits which match author, 285# committer or commit text to a given string. Enabled by default. 286# Project specific override is not supported. 287'search'=> { 288'override'=>0, 289'default'=> [1]}, 290 291# Enable grep search, which will list the files in currently selected 292# tree containing the given string. Enabled by default. This can be 293# potentially CPU-intensive, of course. 294 295# To enable system wide have in $GITWEB_CONFIG 296# $feature{'grep'}{'default'} = [1]; 297# To have project specific config enable override in $GITWEB_CONFIG 298# $feature{'grep'}{'override'} = 1; 299# and in project config gitweb.grep = 0|1; 300'grep'=> { 301'sub'=>sub{ feature_bool('grep',@_) }, 302'override'=>0, 303'default'=> [1]}, 304 305# Enable the pickaxe search, which will list the commits that modified 306# a given string in a file. This can be practical and quite faster 307# alternative to 'blame', but still potentially CPU-intensive. 308 309# To enable system wide have in $GITWEB_CONFIG 310# $feature{'pickaxe'}{'default'} = [1]; 311# To have project specific config enable override in $GITWEB_CONFIG 312# $feature{'pickaxe'}{'override'} = 1; 313# and in project config gitweb.pickaxe = 0|1; 314'pickaxe'=> { 315'sub'=>sub{ feature_bool('pickaxe',@_) }, 316'override'=>0, 317'default'=> [1]}, 318 319# Enable showing size of blobs in a 'tree' view, in a separate 320# column, similar to what 'ls -l' does. This cost a bit of IO. 321 322# To disable system wide have in $GITWEB_CONFIG 323# $feature{'show-sizes'}{'default'} = [0]; 324# To have project specific config enable override in $GITWEB_CONFIG 325# $feature{'show-sizes'}{'override'} = 1; 326# and in project config gitweb.showsizes = 0|1; 327'show-sizes'=> { 328'sub'=>sub{ feature_bool('showsizes',@_) }, 329'override'=>0, 330'default'=> [1]}, 331 332# Make gitweb use an alternative format of the URLs which can be 333# more readable and natural-looking: project name is embedded 334# directly in the path and the query string contains other 335# auxiliary information. All gitweb installations recognize 336# URL in either format; this configures in which formats gitweb 337# generates links. 338 339# To enable system wide have in $GITWEB_CONFIG 340# $feature{'pathinfo'}{'default'} = [1]; 341# Project specific override is not supported. 342 343# Note that you will need to change the default location of CSS, 344# favicon, logo and possibly other files to an absolute URL. Also, 345# if gitweb.cgi serves as your indexfile, you will need to force 346# $my_uri to contain the script name in your $GITWEB_CONFIG. 347'pathinfo'=> { 348'override'=>0, 349'default'=> [0]}, 350 351# Make gitweb consider projects in project root subdirectories 352# to be forks of existing projects. Given project $projname.git, 353# projects matching $projname/*.git will not be shown in the main 354# projects list, instead a '+' mark will be added to $projname 355# there and a 'forks' view will be enabled for the project, listing 356# all the forks. If project list is taken from a file, forks have 357# to be listed after the main project. 358 359# To enable system wide have in $GITWEB_CONFIG 360# $feature{'forks'}{'default'} = [1]; 361# Project specific override is not supported. 362'forks'=> { 363'override'=>0, 364'default'=> [0]}, 365 366# Insert custom links to the action bar of all project pages. 367# This enables you mainly to link to third-party scripts integrating 368# into gitweb; e.g. git-browser for graphical history representation 369# or custom web-based repository administration interface. 370 371# The 'default' value consists of a list of triplets in the form 372# (label, link, position) where position is the label after which 373# to insert the link and link is a format string where %n expands 374# to the project name, %f to the project path within the filesystem, 375# %h to the current hash (h gitweb parameter) and %b to the current 376# hash base (hb gitweb parameter); %% expands to %. 377 378# To enable system wide have in $GITWEB_CONFIG e.g. 379# $feature{'actions'}{'default'} = [('graphiclog', 380# '/git-browser/by-commit.html?r=%n', 'summary')]; 381# Project specific override is not supported. 382'actions'=> { 383'override'=>0, 384'default'=> []}, 385 386# Allow gitweb scan project content tags described in ctags/ 387# of project repository, and display the popular Web 2.0-ish 388# "tag cloud" near the project list. Note that this is something 389# COMPLETELY different from the normal Git tags. 390 391# gitweb by itself can show existing tags, but it does not handle 392# tagging itself; you need an external application for that. 393# For an example script, check Girocco's cgi/tagproj.cgi. 394# You may want to install the HTML::TagCloud Perl module to get 395# a pretty tag cloud instead of just a list of tags. 396 397# To enable system wide have in $GITWEB_CONFIG 398# $feature{'ctags'}{'default'} = ['path_to_tag_script']; 399# Project specific override is not supported. 400'ctags'=> { 401'override'=>0, 402'default'=> [0]}, 403 404# The maximum number of patches in a patchset generated in patch 405# view. Set this to 0 or undef to disable patch view, or to a 406# negative number to remove any limit. 407 408# To disable system wide have in $GITWEB_CONFIG 409# $feature{'patches'}{'default'} = [0]; 410# To have project specific config enable override in $GITWEB_CONFIG 411# $feature{'patches'}{'override'} = 1; 412# and in project config gitweb.patches = 0|n; 413# where n is the maximum number of patches allowed in a patchset. 414'patches'=> { 415'sub'=> \&feature_patches, 416'override'=>0, 417'default'=> [16]}, 418 419# Avatar support. When this feature is enabled, views such as 420# shortlog or commit will display an avatar associated with 421# the email of the committer(s) and/or author(s). 422 423# Currently available providers are gravatar and picon. 424# If an unknown provider is specified, the feature is disabled. 425 426# Gravatar depends on Digest::MD5. 427# Picon currently relies on the indiana.edu database. 428 429# To enable system wide have in $GITWEB_CONFIG 430# $feature{'avatar'}{'default'} = ['<provider>']; 431# where <provider> is either gravatar or picon. 432# To have project specific config enable override in $GITWEB_CONFIG 433# $feature{'avatar'}{'override'} = 1; 434# and in project config gitweb.avatar = <provider>; 435'avatar'=> { 436'sub'=> \&feature_avatar, 437'override'=>0, 438'default'=> ['']}, 439 440# Enable displaying how much time and how many git commands 441# it took to generate and display page. Disabled by default. 442# Project specific override is not supported. 443'timed'=> { 444'override'=>0, 445'default'=> [0]}, 446 447# Enable turning some links into links to actions which require 448# JavaScript to run (like 'blame_incremental'). Not enabled by 449# default. Project specific override is currently not supported. 450'javascript-actions'=> { 451'override'=>0, 452'default'=> [0]}, 453); 454 455sub gitweb_get_feature { 456my($name) =@_; 457return unlessexists$feature{$name}; 458my($sub,$override,@defaults) = ( 459$feature{$name}{'sub'}, 460$feature{$name}{'override'}, 461@{$feature{$name}{'default'}}); 462# project specific override is possible only if we have project 463our$git_dir;# global variable, declared later 464if(!$override|| !defined$git_dir) { 465return@defaults; 466} 467if(!defined$sub) { 468warn"feature$nameis not overridable"; 469return@defaults; 470} 471return$sub->(@defaults); 472} 473 474# A wrapper to check if a given feature is enabled. 475# With this, you can say 476# 477# my $bool_feat = gitweb_check_feature('bool_feat'); 478# gitweb_check_feature('bool_feat') or somecode; 479# 480# instead of 481# 482# my ($bool_feat) = gitweb_get_feature('bool_feat'); 483# (gitweb_get_feature('bool_feat'))[0] or somecode; 484# 485sub gitweb_check_feature { 486return(gitweb_get_feature(@_))[0]; 487} 488 489 490sub feature_bool { 491my$key=shift; 492my($val) = git_get_project_config($key,'--bool'); 493 494if(!defined$val) { 495return($_[0]); 496}elsif($valeq'true') { 497return(1); 498}elsif($valeq'false') { 499return(0); 500} 501} 502 503sub feature_snapshot { 504my(@fmts) =@_; 505 506my($val) = git_get_project_config('snapshot'); 507 508if($val) { 509@fmts= ($valeq'none'? () :split/\s*[,\s]\s*/,$val); 510} 511 512return@fmts; 513} 514 515sub feature_patches { 516my@val= (git_get_project_config('patches','--int')); 517 518if(@val) { 519return@val; 520} 521 522return($_[0]); 523} 524 525sub feature_avatar { 526my@val= (git_get_project_config('avatar')); 527 528return@val?@val:@_; 529} 530 531# checking HEAD file with -e is fragile if the repository was 532# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed 533# and then pruned. 534sub check_head_link { 535my($dir) =@_; 536my$headfile="$dir/HEAD"; 537return((-e $headfile) || 538(-l $headfile&&readlink($headfile) =~/^refs\/heads\//)); 539} 540 541sub check_export_ok { 542my($dir) =@_; 543return(check_head_link($dir) && 544(!$export_ok|| -e "$dir/$export_ok") && 545(!$export_auth_hook||$export_auth_hook->($dir))); 546} 547 548# process alternate names for backward compatibility 549# filter out unsupported (unknown) snapshot formats 550sub filter_snapshot_fmts { 551my@fmts=@_; 552 553@fmts=map{ 554exists$known_snapshot_format_aliases{$_} ? 555$known_snapshot_format_aliases{$_} :$_}@fmts; 556@fmts=grep{ 557exists$known_snapshot_formats{$_} && 558!$known_snapshot_formats{$_}{'disabled'}}@fmts; 559} 560 561our($GITWEB_CONFIG,$GITWEB_CONFIG_SYSTEM); 562sub evaluate_gitweb_config { 563our$GITWEB_CONFIG=$ENV{'GITWEB_CONFIG'} ||"++GITWEB_CONFIG++"; 564our$GITWEB_CONFIG_SYSTEM=$ENV{'GITWEB_CONFIG_SYSTEM'} ||"++GITWEB_CONFIG_SYSTEM++"; 565# die if there are errors parsing config file 566if(-e $GITWEB_CONFIG) { 567do$GITWEB_CONFIG; 568die$@if$@; 569}elsif(-e $GITWEB_CONFIG_SYSTEM) { 570do$GITWEB_CONFIG_SYSTEM; 571die$@if$@; 572} 573} 574 575# Get loadavg of system, to compare against $maxload. 576# Currently it requires '/proc/loadavg' present to get loadavg; 577# if it is not present it returns 0, which means no load checking. 578sub get_loadavg { 579if( -e '/proc/loadavg'){ 580open my$fd,'<','/proc/loadavg' 581orreturn0; 582my@load=split(/\s+/,scalar<$fd>); 583close$fd; 584 585# The first three columns measure CPU and IO utilization of the last one, 586# five, and 10 minute periods. The fourth column shows the number of 587# currently running processes and the total number of processes in the m/n 588# format. The last column displays the last process ID used. 589return$load[0] ||0; 590} 591# additional checks for load average should go here for things that don't export 592# /proc/loadavg 593 594return0; 595} 596 597# version of the core git binary 598our$git_version; 599sub evaluate_git_version { 600our$git_version=qx("$GIT" --version)=~m/git version (.*)$/?$1:"unknown"; 601$number_of_git_cmds++; 602} 603 604sub check_loadavg { 605if(defined$maxload&& get_loadavg() >$maxload) { 606 die_error(503,"The load average on the server is too high"); 607} 608} 609 610# ====================================================================== 611# input validation and dispatch 612 613# input parameters can be collected from a variety of sources (presently, CGI 614# and PATH_INFO), so we define an %input_params hash that collects them all 615# together during validation: this allows subsequent uses (e.g. href()) to be 616# agnostic of the parameter origin 617 618our%input_params= (); 619 620# input parameters are stored with the long parameter name as key. This will 621# also be used in the href subroutine to convert parameters to their CGI 622# equivalent, and since the href() usage is the most frequent one, we store 623# the name -> CGI key mapping here, instead of the reverse. 624# 625# XXX: Warning: If you touch this, check the search form for updating, 626# too. 627 628our@cgi_param_mapping= ( 629 project =>"p", 630 action =>"a", 631 file_name =>"f", 632 file_parent =>"fp", 633 hash =>"h", 634 hash_parent =>"hp", 635 hash_base =>"hb", 636 hash_parent_base =>"hpb", 637 page =>"pg", 638 order =>"o", 639 searchtext =>"s", 640 searchtype =>"st", 641 snapshot_format =>"sf", 642 extra_options =>"opt", 643 search_use_regexp =>"sr", 644# this must be last entry (for manipulation from JavaScript) 645 javascript =>"js" 646); 647our%cgi_param_mapping=@cgi_param_mapping; 648 649# we will also need to know the possible actions, for validation 650our%actions= ( 651"blame"=> \&git_blame, 652"blame_incremental"=> \&git_blame_incremental, 653"blame_data"=> \&git_blame_data, 654"blobdiff"=> \&git_blobdiff, 655"blobdiff_plain"=> \&git_blobdiff_plain, 656"blob"=> \&git_blob, 657"blob_plain"=> \&git_blob_plain, 658"commitdiff"=> \&git_commitdiff, 659"commitdiff_plain"=> \&git_commitdiff_plain, 660"commit"=> \&git_commit, 661"forks"=> \&git_forks, 662"heads"=> \&git_heads, 663"history"=> \&git_history, 664"log"=> \&git_log, 665"patch"=> \&git_patch, 666"patches"=> \&git_patches, 667"rss"=> \&git_rss, 668"atom"=> \&git_atom, 669"search"=> \&git_search, 670"search_help"=> \&git_search_help, 671"shortlog"=> \&git_shortlog, 672"summary"=> \&git_summary, 673"tag"=> \&git_tag, 674"tags"=> \&git_tags, 675"tree"=> \&git_tree, 676"snapshot"=> \&git_snapshot, 677"object"=> \&git_object, 678# those below don't need $project 679"opml"=> \&git_opml, 680"project_list"=> \&git_project_list, 681"project_index"=> \&git_project_index, 682); 683 684# finally, we have the hash of allowed extra_options for the commands that 685# allow them 686our%allowed_options= ( 687"--no-merges"=> [qw(rss atom log shortlog history)], 688); 689 690# fill %input_params with the CGI parameters. All values except for 'opt' 691# should be single values, but opt can be an array. We should probably 692# build an array of parameters that can be multi-valued, but since for the time 693# being it's only this one, we just single it out 694sub evaluate_query_params { 695our$cgi; 696 697while(my($name,$symbol) =each%cgi_param_mapping) { 698if($symboleq'opt') { 699$input_params{$name} = [$cgi->param($symbol) ]; 700}else{ 701$input_params{$name} =$cgi->param($symbol); 702} 703} 704} 705 706# now read PATH_INFO and update the parameter list for missing parameters 707sub evaluate_path_info { 708return ifdefined$input_params{'project'}; 709return if!$path_info; 710$path_info=~ s,^/+,,; 711return if!$path_info; 712 713# find which part of PATH_INFO is project 714my$project=$path_info; 715$project=~ s,/+$,,; 716while($project&& !check_head_link("$projectroot/$project")) { 717$project=~ s,/*[^/]*$,,; 718} 719return unless$project; 720$input_params{'project'} =$project; 721 722# do not change any parameters if an action is given using the query string 723return if$input_params{'action'}; 724$path_info=~ s,^\Q$project\E/*,,; 725 726# next, check if we have an action 727my$action=$path_info; 728$action=~ s,/.*$,,; 729if(exists$actions{$action}) { 730$path_info=~ s,^$action/*,,; 731$input_params{'action'} =$action; 732} 733 734# list of actions that want hash_base instead of hash, but can have no 735# pathname (f) parameter 736my@wants_base= ( 737'tree', 738'history', 739); 740 741# we want to catch 742# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] 743my($parentrefname,$parentpathname,$refname,$pathname) = 744($path_info=~/^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); 745 746# first, analyze the 'current' part 747if(defined$pathname) { 748# we got "branch:filename" or "branch:dir/" 749# we could use git_get_type(branch:pathname), but: 750# - it needs $git_dir 751# - it does a git() call 752# - the convention of terminating directories with a slash 753# makes it superfluous 754# - embedding the action in the PATH_INFO would make it even 755# more superfluous 756$pathname=~ s,^/+,,; 757if(!$pathname||substr($pathname, -1)eq"/") { 758$input_params{'action'} ||="tree"; 759$pathname=~ s,/$,,; 760}else{ 761# the default action depends on whether we had parent info 762# or not 763if($parentrefname) { 764$input_params{'action'} ||="blobdiff_plain"; 765}else{ 766$input_params{'action'} ||="blob_plain"; 767} 768} 769$input_params{'hash_base'} ||=$refname; 770$input_params{'file_name'} ||=$pathname; 771}elsif(defined$refname) { 772# we got "branch". In this case we have to choose if we have to 773# set hash or hash_base. 774# 775# Most of the actions without a pathname only want hash to be 776# set, except for the ones specified in @wants_base that want 777# hash_base instead. It should also be noted that hand-crafted 778# links having 'history' as an action and no pathname or hash 779# set will fail, but that happens regardless of PATH_INFO. 780$input_params{'action'} ||="shortlog"; 781if(grep{$_eq$input_params{'action'} }@wants_base) { 782$input_params{'hash_base'} ||=$refname; 783}else{ 784$input_params{'hash'} ||=$refname; 785} 786} 787 788# next, handle the 'parent' part, if present 789if(defined$parentrefname) { 790# a missing pathspec defaults to the 'current' filename, allowing e.g. 791# someproject/blobdiff/oldrev..newrev:/filename 792if($parentpathname) { 793$parentpathname=~ s,^/+,,; 794$parentpathname=~ s,/$,,; 795$input_params{'file_parent'} ||=$parentpathname; 796}else{ 797$input_params{'file_parent'} ||=$input_params{'file_name'}; 798} 799# we assume that hash_parent_base is wanted if a path was specified, 800# or if the action wants hash_base instead of hash 801if(defined$input_params{'file_parent'} || 802grep{$_eq$input_params{'action'} }@wants_base) { 803$input_params{'hash_parent_base'} ||=$parentrefname; 804}else{ 805$input_params{'hash_parent'} ||=$parentrefname; 806} 807} 808 809# for the snapshot action, we allow URLs in the form 810# $project/snapshot/$hash.ext 811# where .ext determines the snapshot and gets removed from the 812# passed $refname to provide the $hash. 813# 814# To be able to tell that $refname includes the format extension, we 815# require the following two conditions to be satisfied: 816# - the hash input parameter MUST have been set from the $refname part 817# of the URL (i.e. they must be equal) 818# - the snapshot format MUST NOT have been defined already (e.g. from 819# CGI parameter sf) 820# It's also useless to try any matching unless $refname has a dot, 821# so we check for that too 822if(defined$input_params{'action'} && 823$input_params{'action'}eq'snapshot'&& 824defined$refname&&index($refname,'.') != -1&& 825$refnameeq$input_params{'hash'} && 826!defined$input_params{'snapshot_format'}) { 827# We loop over the known snapshot formats, checking for 828# extensions. Allowed extensions are both the defined suffix 829# (which includes the initial dot already) and the snapshot 830# format key itself, with a prepended dot 831while(my($fmt,$opt) =each%known_snapshot_formats) { 832my$hash=$refname; 833unless($hash=~s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) { 834next; 835} 836my$sfx=$1; 837# a valid suffix was found, so set the snapshot format 838# and reset the hash parameter 839$input_params{'snapshot_format'} =$fmt; 840$input_params{'hash'} =$hash; 841# we also set the format suffix to the one requested 842# in the URL: this way a request for e.g. .tgz returns 843# a .tgz instead of a .tar.gz 844$known_snapshot_formats{$fmt}{'suffix'} =$sfx; 845last; 846} 847} 848} 849 850our($action,$project,$file_name,$file_parent,$hash,$hash_parent,$hash_base, 851$hash_parent_base,@extra_options,$page,$searchtype,$search_use_regexp, 852$searchtext,$search_regexp); 853sub evaluate_and_validate_params { 854our$action=$input_params{'action'}; 855if(defined$action) { 856if(!validate_action($action)) { 857 die_error(400,"Invalid action parameter"); 858} 859} 860 861# parameters which are pathnames 862our$project=$input_params{'project'}; 863if(defined$project) { 864if(!validate_project($project)) { 865undef$project; 866 die_error(404,"No such project"); 867} 868} 869 870our$file_name=$input_params{'file_name'}; 871if(defined$file_name) { 872if(!validate_pathname($file_name)) { 873 die_error(400,"Invalid file parameter"); 874} 875} 876 877our$file_parent=$input_params{'file_parent'}; 878if(defined$file_parent) { 879if(!validate_pathname($file_parent)) { 880 die_error(400,"Invalid file parent parameter"); 881} 882} 883 884# parameters which are refnames 885our$hash=$input_params{'hash'}; 886if(defined$hash) { 887if(!validate_refname($hash)) { 888 die_error(400,"Invalid hash parameter"); 889} 890} 891 892our$hash_parent=$input_params{'hash_parent'}; 893if(defined$hash_parent) { 894if(!validate_refname($hash_parent)) { 895 die_error(400,"Invalid hash parent parameter"); 896} 897} 898 899our$hash_base=$input_params{'hash_base'}; 900if(defined$hash_base) { 901if(!validate_refname($hash_base)) { 902 die_error(400,"Invalid hash base parameter"); 903} 904} 905 906our@extra_options= @{$input_params{'extra_options'}}; 907# @extra_options is always defined, since it can only be (currently) set from 908# CGI, and $cgi->param() returns the empty array in array context if the param 909# is not set 910foreachmy$opt(@extra_options) { 911if(not exists$allowed_options{$opt}) { 912 die_error(400,"Invalid option parameter"); 913} 914if(not grep(/^$action$/, @{$allowed_options{$opt}})) { 915 die_error(400,"Invalid option parameter for this action"); 916} 917} 918 919our$hash_parent_base=$input_params{'hash_parent_base'}; 920if(defined$hash_parent_base) { 921if(!validate_refname($hash_parent_base)) { 922 die_error(400,"Invalid hash parent base parameter"); 923} 924} 925 926# other parameters 927our$page=$input_params{'page'}; 928if(defined$page) { 929if($page=~m/[^0-9]/) { 930 die_error(400,"Invalid page parameter"); 931} 932} 933 934our$searchtype=$input_params{'searchtype'}; 935if(defined$searchtype) { 936if($searchtype=~m/[^a-z]/) { 937 die_error(400,"Invalid searchtype parameter"); 938} 939} 940 941our$search_use_regexp=$input_params{'search_use_regexp'}; 942 943our$searchtext=$input_params{'searchtext'}; 944our$search_regexp; 945if(defined$searchtext) { 946if(length($searchtext) <2) { 947 die_error(403,"At least two characters are required for search parameter"); 948} 949$search_regexp=$search_use_regexp?$searchtext:quotemeta$searchtext; 950} 951} 952 953# path to the current git repository 954our$git_dir; 955sub evaluate_git_dir { 956our$git_dir="$projectroot/$project"if$project; 957} 958 959our(@snapshot_fmts,$git_avatar); 960sub configure_gitweb_features { 961# list of supported snapshot formats 962our@snapshot_fmts= gitweb_get_feature('snapshot'); 963@snapshot_fmts= filter_snapshot_fmts(@snapshot_fmts); 964 965# check that the avatar feature is set to a known provider name, 966# and for each provider check if the dependencies are satisfied. 967# if the provider name is invalid or the dependencies are not met, 968# reset $git_avatar to the empty string. 969our($git_avatar) = gitweb_get_feature('avatar'); 970if($git_avatareq'gravatar') { 971$git_avatar=''unless(eval{require Digest::MD5;1; }); 972}elsif($git_avatareq'picon') { 973# no dependencies 974}else{ 975$git_avatar=''; 976} 977} 978 979# dispatch 980sub dispatch { 981if(!defined$action) { 982if(defined$hash) { 983$action= git_get_type($hash); 984}elsif(defined$hash_base&&defined$file_name) { 985$action= git_get_type("$hash_base:$file_name"); 986}elsif(defined$project) { 987$action='summary'; 988}else{ 989$action='project_list'; 990} 991} 992if(!defined($actions{$action})) { 993 die_error(400,"Unknown action"); 994} 995if($action!~m/^(?:opml|project_list|project_index)$/&& 996!$project) { 997 die_error(400,"Project needed"); 998} 999$actions{$action}->();1000}10011002sub run_request {1003our$t0= [Time::HiRes::gettimeofday()]1004ifdefined$t0;10051006 evaluate_uri();1007 evaluate_gitweb_config();1008 evaluate_git_version();1009 check_loadavg();10101011# $projectroot and $projects_list might be set in gitweb config file1012$projects_list||=$projectroot;10131014 evaluate_query_params();1015 evaluate_path_info();1016 evaluate_and_validate_params();1017 evaluate_git_dir();10181019 configure_gitweb_features();10201021 dispatch();1022}10231024our$is_last_request=sub{1};1025our($pre_dispatch_hook,$post_dispatch_hook,$pre_listen_hook);1026our$CGI='CGI';1027our$cgi;1028sub configure_as_fcgi {1029require CGI::Fast;1030our$CGI='CGI::Fast';10311032my$request_number=0;1033# let each child service 100 requests1034our$is_last_request=sub{ ++$request_number>100};1035}1036sub evaluate_argv {1037my$script_name=$ENV{'SCRIPT_NAME'} ||$ENV{'SCRIPT_FILENAME'} || __FILE__;1038 configure_as_fcgi()1039if$script_name=~/\.fcgi$/;10401041return unless(@ARGV);10421043require Getopt::Long;1044 Getopt::Long::GetOptions(1045'fastcgi|fcgi|f'=> \&configure_as_fcgi,1046'nproc|n=i'=>sub{1047my($arg,$val) =@_;1048return unlesseval{require FCGI::ProcManager;1; };1049my$proc_manager= FCGI::ProcManager->new({1050 n_processes =>$val,1051});1052our$pre_listen_hook=sub{$proc_manager->pm_manage() };1053our$pre_dispatch_hook=sub{$proc_manager->pm_pre_dispatch() };1054our$post_dispatch_hook=sub{$proc_manager->pm_post_dispatch() };1055},1056);1057}10581059sub run {1060 evaluate_argv();10611062$pre_listen_hook->()1063if$pre_listen_hook;10641065 REQUEST:1066while($cgi=$CGI->new()) {1067$pre_dispatch_hook->()1068if$pre_dispatch_hook;10691070 run_request();10711072$pre_dispatch_hook->()1073if$post_dispatch_hook;10741075last REQUEST if($is_last_request->());1076}10771078 DONE_GITWEB:10791;1080}10811082run();10831084## ======================================================================1085## action links10861087# possible values of extra options1088# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)1089# -replay => 1 - start from a current view (replay with modifications)1090# -path_info => 0|1 - don't use/use path_info URL (if possible)1091sub href {1092my%params=@_;1093# default is to use -absolute url() i.e. $my_uri1094my$href=$params{-full} ?$my_url:$my_uri;10951096$params{'project'} =$projectunlessexists$params{'project'};10971098if($params{-replay}) {1099while(my($name,$symbol) =each%cgi_param_mapping) {1100if(!exists$params{$name}) {1101$params{$name} =$input_params{$name};1102}1103}1104}11051106my$use_pathinfo= gitweb_check_feature('pathinfo');1107if(defined$params{'project'} &&1108(exists$params{-path_info} ?$params{-path_info} :$use_pathinfo)) {1109# try to put as many parameters as possible in PATH_INFO:1110# - project name1111# - action1112# - hash_parent or hash_parent_base:/file_parent1113# - hash or hash_base:/filename1114# - the snapshot_format as an appropriate suffix11151116# When the script is the root DirectoryIndex for the domain,1117# $href here would be something like http://gitweb.example.com/1118# Thus, we strip any trailing / from $href, to spare us double1119# slashes in the final URL1120$href=~ s,/$,,;11211122# Then add the project name, if present1123$href.="/".esc_url($params{'project'});1124delete$params{'project'};11251126# since we destructively absorb parameters, we keep this1127# boolean that remembers if we're handling a snapshot1128my$is_snapshot=$params{'action'}eq'snapshot';11291130# Summary just uses the project path URL, any other action is1131# added to the URL1132if(defined$params{'action'}) {1133$href.="/".esc_url($params{'action'})unless$params{'action'}eq'summary';1134delete$params{'action'};1135}11361137# Next, we put hash_parent_base:/file_parent..hash_base:/file_name,1138# stripping nonexistent or useless pieces1139$href.="/"if($params{'hash_base'} ||$params{'hash_parent_base'}1140||$params{'hash_parent'} ||$params{'hash'});1141if(defined$params{'hash_base'}) {1142if(defined$params{'hash_parent_base'}) {1143$href.= esc_url($params{'hash_parent_base'});1144# skip the file_parent if it's the same as the file_name1145if(defined$params{'file_parent'}) {1146if(defined$params{'file_name'} &&$params{'file_parent'}eq$params{'file_name'}) {1147delete$params{'file_parent'};1148}elsif($params{'file_parent'} !~/\.\./) {1149$href.=":/".esc_url($params{'file_parent'});1150delete$params{'file_parent'};1151}1152}1153$href.="..";1154delete$params{'hash_parent'};1155delete$params{'hash_parent_base'};1156}elsif(defined$params{'hash_parent'}) {1157$href.= esc_url($params{'hash_parent'})."..";1158delete$params{'hash_parent'};1159}11601161$href.= esc_url($params{'hash_base'});1162if(defined$params{'file_name'} &&$params{'file_name'} !~/\.\./) {1163$href.=":/".esc_url($params{'file_name'});1164delete$params{'file_name'};1165}1166delete$params{'hash'};1167delete$params{'hash_base'};1168}elsif(defined$params{'hash'}) {1169$href.= esc_url($params{'hash'});1170delete$params{'hash'};1171}11721173# If the action was a snapshot, we can absorb the1174# snapshot_format parameter too1175if($is_snapshot) {1176my$fmt=$params{'snapshot_format'};1177# snapshot_format should always be defined when href()1178# is called, but just in case some code forgets, we1179# fall back to the default1180$fmt||=$snapshot_fmts[0];1181$href.=$known_snapshot_formats{$fmt}{'suffix'};1182delete$params{'snapshot_format'};1183}1184}11851186# now encode the parameters explicitly1187my@result= ();1188for(my$i=0;$i<@cgi_param_mapping;$i+=2) {1189my($name,$symbol) = ($cgi_param_mapping[$i],$cgi_param_mapping[$i+1]);1190if(defined$params{$name}) {1191if(ref($params{$name})eq"ARRAY") {1192foreachmy$par(@{$params{$name}}) {1193push@result,$symbol."=". esc_param($par);1194}1195}else{1196push@result,$symbol."=". esc_param($params{$name});1197}1198}1199}1200$href.="?".join(';',@result)ifscalar@result;12011202return$href;1203}120412051206## ======================================================================1207## validation, quoting/unquoting and escaping12081209sub validate_action {1210my$input=shift||returnundef;1211returnundefunlessexists$actions{$input};1212return$input;1213}12141215sub validate_project {1216my$input=shift||returnundef;1217if(!validate_pathname($input) ||1218!(-d "$projectroot/$input") ||1219!check_export_ok("$projectroot/$input") ||1220($strict_export&& !project_in_list($input))) {1221returnundef;1222}else{1223return$input;1224}1225}12261227sub validate_pathname {1228my$input=shift||returnundef;12291230# no '.' or '..' as elements of path, i.e. no '.' nor '..'1231# at the beginning, at the end, and between slashes.1232# also this catches doubled slashes1233if($input=~m!(^|/)(|\.|\.\.)(/|$)!) {1234returnundef;1235}1236# no null characters1237if($input=~m!\0!) {1238returnundef;1239}1240return$input;1241}12421243sub validate_refname {1244my$input=shift||returnundef;12451246# textual hashes are O.K.1247if($input=~m/^[0-9a-fA-F]{40}$/) {1248return$input;1249}1250# it must be correct pathname1251$input= validate_pathname($input)1252orreturnundef;1253# restrictions on ref name according to git-check-ref-format1254if($input=~m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {1255returnundef;1256}1257return$input;1258}12591260# decode sequences of octets in utf8 into Perl's internal form,1261# which is utf-8 with utf8 flag set if needed. gitweb writes out1262# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning1263sub to_utf8 {1264my$str=shift;1265returnundefunlessdefined$str;1266if(utf8::valid($str)) {1267 utf8::decode($str);1268return$str;1269}else{1270return decode($fallback_encoding,$str, Encode::FB_DEFAULT);1271}1272}12731274# quote unsafe chars, but keep the slash, even when it's not1275# correct, but quoted slashes look too horrible in bookmarks1276sub esc_param {1277my$str=shift;1278returnundefunlessdefined$str;1279$str=~s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;1280$str=~s/ /\+/g;1281return$str;1282}12831284# quote unsafe chars in whole URL, so some charactrs cannot be quoted1285sub esc_url {1286my$str=shift;1287returnundefunlessdefined$str;1288$str=~s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X",ord($1))/eg;1289$str=~s/\+/%2B/g;1290$str=~s/ /\+/g;1291return$str;1292}12931294# replace invalid utf8 character with SUBSTITUTION sequence1295sub esc_html {1296my$str=shift;1297my%opts=@_;12981299returnundefunlessdefined$str;13001301$str= to_utf8($str);1302$str=$cgi->escapeHTML($str);1303if($opts{'-nbsp'}) {1304$str=~s/ / /g;1305}1306$str=~ s|([[:cntrl:]])|(($1ne"\t") ? quot_cec($1) :$1)|eg;1307return$str;1308}13091310# quote control characters and escape filename to HTML1311sub esc_path {1312my$str=shift;1313my%opts=@_;13141315returnundefunlessdefined$str;13161317$str= to_utf8($str);1318$str=$cgi->escapeHTML($str);1319if($opts{'-nbsp'}) {1320$str=~s/ / /g;1321}1322$str=~ s|([[:cntrl:]])|quot_cec($1)|eg;1323return$str;1324}13251326# Make control characters "printable", using character escape codes (CEC)1327sub quot_cec {1328my$cntrl=shift;1329my%opts=@_;1330my%es= (# character escape codes, aka escape sequences1331"\t"=>'\t',# tab (HT)1332"\n"=>'\n',# line feed (LF)1333"\r"=>'\r',# carrige return (CR)1334"\f"=>'\f',# form feed (FF)1335"\b"=>'\b',# backspace (BS)1336"\a"=>'\a',# alarm (bell) (BEL)1337"\e"=>'\e',# escape (ESC)1338"\013"=>'\v',# vertical tab (VT)1339"\000"=>'\0',# nul character (NUL)1340);1341my$chr= ( (exists$es{$cntrl})1342?$es{$cntrl}1343:sprintf('\%2x',ord($cntrl)) );1344if($opts{-nohtml}) {1345return$chr;1346}else{1347return"<span class=\"cntrl\">$chr</span>";1348}1349}13501351# Alternatively use unicode control pictures codepoints,1352# Unicode "printable representation" (PR)1353sub quot_upr {1354my$cntrl=shift;1355my%opts=@_;13561357my$chr=sprintf('&#%04d;',0x2400+ord($cntrl));1358if($opts{-nohtml}) {1359return$chr;1360}else{1361return"<span class=\"cntrl\">$chr</span>";1362}1363}13641365# git may return quoted and escaped filenames1366sub unquote {1367my$str=shift;13681369sub unq {1370my$seq=shift;1371my%es= (# character escape codes, aka escape sequences1372't'=>"\t",# tab (HT, TAB)1373'n'=>"\n",# newline (NL)1374'r'=>"\r",# return (CR)1375'f'=>"\f",# form feed (FF)1376'b'=>"\b",# backspace (BS)1377'a'=>"\a",# alarm (bell) (BEL)1378'e'=>"\e",# escape (ESC)1379'v'=>"\013",# vertical tab (VT)1380);13811382if($seq=~m/^[0-7]{1,3}$/) {1383# octal char sequence1384returnchr(oct($seq));1385}elsif(exists$es{$seq}) {1386# C escape sequence, aka character escape code1387return$es{$seq};1388}1389# quoted ordinary character1390return$seq;1391}13921393if($str=~m/^"(.*)"$/) {1394# needs unquoting1395$str=$1;1396$str=~s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;1397}1398return$str;1399}14001401# escape tabs (convert tabs to spaces)1402sub untabify {1403my$line=shift;14041405while((my$pos=index($line,"\t")) != -1) {1406if(my$count= (8- ($pos%8))) {1407my$spaces=' ' x $count;1408$line=~s/\t/$spaces/;1409}1410}14111412return$line;1413}14141415sub project_in_list {1416my$project=shift;1417my@list= git_get_projects_list();1418return@list&&scalar(grep{$_->{'path'}eq$project}@list);1419}14201421## ----------------------------------------------------------------------1422## HTML aware string manipulation14231424# Try to chop given string on a word boundary between position1425# $len and $len+$add_len. If there is no word boundary there,1426# chop at $len+$add_len. Do not chop if chopped part plus ellipsis1427# (marking chopped part) would be longer than given string.1428sub chop_str {1429my$str=shift;1430my$len=shift;1431my$add_len=shift||10;1432my$where=shift||'right';# 'left' | 'center' | 'right'14331434# Make sure perl knows it is utf8 encoded so we don't1435# cut in the middle of a utf8 multibyte char.1436$str= to_utf8($str);14371438# allow only $len chars, but don't cut a word if it would fit in $add_len1439# if it doesn't fit, cut it if it's still longer than the dots we would add1440# remove chopped character entities entirely14411442# when chopping in the middle, distribute $len into left and right part1443# return early if chopping wouldn't make string shorter1444if($whereeq'center') {1445return$strif($len+5>=length($str));# filler is length 51446$len=int($len/2);1447}else{1448return$strif($len+4>=length($str));# filler is length 41449}14501451# regexps: ending and beginning with word part up to $add_len1452my$endre=qr/.{$len}\w{0,$add_len}/;1453my$begre=qr/\w{0,$add_len}.{$len}/;14541455if($whereeq'left') {1456$str=~m/^(.*?)($begre)$/;1457my($lead,$body) = ($1,$2);1458if(length($lead) >4) {1459$lead=" ...";1460}1461return"$lead$body";14621463}elsif($whereeq'center') {1464$str=~m/^($endre)(.*)$/;1465my($left,$str) = ($1,$2);1466$str=~m/^(.*?)($begre)$/;1467my($mid,$right) = ($1,$2);1468if(length($mid) >5) {1469$mid=" ... ";1470}1471return"$left$mid$right";14721473}else{1474$str=~m/^($endre)(.*)$/;1475my$body=$1;1476my$tail=$2;1477if(length($tail) >4) {1478$tail="... ";1479}1480return"$body$tail";1481}1482}14831484# takes the same arguments as chop_str, but also wraps a <span> around the1485# result with a title attribute if it does get chopped. Additionally, the1486# string is HTML-escaped.1487sub chop_and_escape_str {1488my($str) =@_;14891490my$chopped= chop_str(@_);1491if($choppedeq$str) {1492return esc_html($chopped);1493}else{1494$str=~s/[[:cntrl:]]/?/g;1495return$cgi->span({-title=>$str}, esc_html($chopped));1496}1497}14981499## ----------------------------------------------------------------------1500## functions returning short strings15011502# CSS class for given age value (in seconds)1503sub age_class {1504my$age=shift;15051506if(!defined$age) {1507return"noage";1508}elsif($age<60*60*2) {1509return"age0";1510}elsif($age<60*60*24*2) {1511return"age1";1512}else{1513return"age2";1514}1515}15161517# convert age in seconds to "nn units ago" string1518sub age_string {1519my$age=shift;1520my$age_str;15211522if($age>60*60*24*365*2) {1523$age_str= (int$age/60/60/24/365);1524$age_str.=" years ago";1525}elsif($age>60*60*24*(365/12)*2) {1526$age_str=int$age/60/60/24/(365/12);1527$age_str.=" months ago";1528}elsif($age>60*60*24*7*2) {1529$age_str=int$age/60/60/24/7;1530$age_str.=" weeks ago";1531}elsif($age>60*60*24*2) {1532$age_str=int$age/60/60/24;1533$age_str.=" days ago";1534}elsif($age>60*60*2) {1535$age_str=int$age/60/60;1536$age_str.=" hours ago";1537}elsif($age>60*2) {1538$age_str=int$age/60;1539$age_str.=" min ago";1540}elsif($age>2) {1541$age_str=int$age;1542$age_str.=" sec ago";1543}else{1544$age_str.=" right now";1545}1546return$age_str;1547}15481549useconstant{1550 S_IFINVALID =>0030000,1551 S_IFGITLINK =>0160000,1552};15531554# submodule/subproject, a commit object reference1555sub S_ISGITLINK {1556my$mode=shift;15571558return(($mode& S_IFMT) == S_IFGITLINK)1559}15601561# convert file mode in octal to symbolic file mode string1562sub mode_str {1563my$mode=oct shift;15641565if(S_ISGITLINK($mode)) {1566return'm---------';1567}elsif(S_ISDIR($mode& S_IFMT)) {1568return'drwxr-xr-x';1569}elsif(S_ISLNK($mode)) {1570return'lrwxrwxrwx';1571}elsif(S_ISREG($mode)) {1572# git cares only about the executable bit1573if($mode& S_IXUSR) {1574return'-rwxr-xr-x';1575}else{1576return'-rw-r--r--';1577};1578}else{1579return'----------';1580}1581}15821583# convert file mode in octal to file type string1584sub file_type {1585my$mode=shift;15861587if($mode!~m/^[0-7]+$/) {1588return$mode;1589}else{1590$mode=oct$mode;1591}15921593if(S_ISGITLINK($mode)) {1594return"submodule";1595}elsif(S_ISDIR($mode& S_IFMT)) {1596return"directory";1597}elsif(S_ISLNK($mode)) {1598return"symlink";1599}elsif(S_ISREG($mode)) {1600return"file";1601}else{1602return"unknown";1603}1604}16051606# convert file mode in octal to file type description string1607sub file_type_long {1608my$mode=shift;16091610if($mode!~m/^[0-7]+$/) {1611return$mode;1612}else{1613$mode=oct$mode;1614}16151616if(S_ISGITLINK($mode)) {1617return"submodule";1618}elsif(S_ISDIR($mode& S_IFMT)) {1619return"directory";1620}elsif(S_ISLNK($mode)) {1621return"symlink";1622}elsif(S_ISREG($mode)) {1623if($mode& S_IXUSR) {1624return"executable";1625}else{1626return"file";1627};1628}else{1629return"unknown";1630}1631}163216331634## ----------------------------------------------------------------------1635## functions returning short HTML fragments, or transforming HTML fragments1636## which don't belong to other sections16371638# format line of commit message.1639sub format_log_line_html {1640my$line=shift;16411642$line= esc_html($line, -nbsp=>1);1643$line=~ s{\b([0-9a-fA-F]{8,40})\b}{1644$cgi->a({-href => href(action=>"object", hash=>$1),1645-class=>"text"},$1);1646}eg;16471648return$line;1649}16501651# format marker of refs pointing to given object16521653# the destination action is chosen based on object type and current context:1654# - for annotated tags, we choose the tag view unless it's the current view1655# already, in which case we go to shortlog view1656# - for other refs, we keep the current view if we're in history, shortlog or1657# log view, and select shortlog otherwise1658sub format_ref_marker {1659my($refs,$id) =@_;1660my$markers='';16611662if(defined$refs->{$id}) {1663foreachmy$ref(@{$refs->{$id}}) {1664# this code exploits the fact that non-lightweight tags are the1665# only indirect objects, and that they are the only objects for which1666# we want to use tag instead of shortlog as action1667my($type,$name) =qw();1668my$indirect= ($ref=~s/\^\{\}$//);1669# e.g. tags/v2.6.11 or heads/next1670if($ref=~m!^(.*?)s?/(.*)$!) {1671$type=$1;1672$name=$2;1673}else{1674$type="ref";1675$name=$ref;1676}16771678my$class=$type;1679$class.=" indirect"if$indirect;16801681my$dest_action="shortlog";16821683if($indirect) {1684$dest_action="tag"unless$actioneq"tag";1685}elsif($action=~/^(history|(short)?log)$/) {1686$dest_action=$action;1687}16881689my$dest="";1690$dest.="refs/"unless$ref=~ m!^refs/!;1691$dest.=$ref;16921693my$link=$cgi->a({1694-href => href(1695 action=>$dest_action,1696 hash=>$dest1697)},$name);16981699$markers.=" <span class=\"$class\"title=\"$ref\">".1700$link."</span>";1701}1702}17031704if($markers) {1705return' <span class="refs">'.$markers.'</span>';1706}else{1707return"";1708}1709}17101711# format, perhaps shortened and with markers, title line1712sub format_subject_html {1713my($long,$short,$href,$extra) =@_;1714$extra=''unlessdefined($extra);17151716if(length($short) <length($long)) {1717$long=~s/[[:cntrl:]]/?/g;1718return$cgi->a({-href =>$href, -class=>"list subject",1719-title => to_utf8($long)},1720 esc_html($short)) .$extra;1721}else{1722return$cgi->a({-href =>$href, -class=>"list subject"},1723 esc_html($long)) .$extra;1724}1725}17261727# Rather than recomputing the url for an email multiple times, we cache it1728# after the first hit. This gives a visible benefit in views where the avatar1729# for the same email is used repeatedly (e.g. shortlog).1730# The cache is shared by all avatar engines (currently gravatar only), which1731# are free to use it as preferred. Since only one avatar engine is used for any1732# given page, there's no risk for cache conflicts.1733our%avatar_cache= ();17341735# Compute the picon url for a given email, by using the picon search service over at1736# http://www.cs.indiana.edu/picons/search.html1737sub picon_url {1738my$email=lc shift;1739if(!$avatar_cache{$email}) {1740my($user,$domain) =split('@',$email);1741$avatar_cache{$email} =1742"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/".1743"$domain/$user/".1744"users+domains+unknown/up/single";1745}1746return$avatar_cache{$email};1747}17481749# Compute the gravatar url for a given email, if it's not in the cache already.1750# Gravatar stores only the part of the URL before the size, since that's the1751# one computationally more expensive. This also allows reuse of the cache for1752# different sizes (for this particular engine).1753sub gravatar_url {1754my$email=lc shift;1755my$size=shift;1756$avatar_cache{$email} ||=1757"http://www.gravatar.com/avatar/".1758 Digest::MD5::md5_hex($email) ."?s=";1759return$avatar_cache{$email} .$size;1760}17611762# Insert an avatar for the given $email at the given $size if the feature1763# is enabled.1764sub git_get_avatar {1765my($email,%opts) =@_;1766my$pre_white= ($opts{-pad_before} ?" ":"");1767my$post_white= ($opts{-pad_after} ?" ":"");1768$opts{-size} ||='default';1769my$size=$avatar_size{$opts{-size}} ||$avatar_size{'default'};1770my$url="";1771if($git_avatareq'gravatar') {1772$url= gravatar_url($email,$size);1773}elsif($git_avatareq'picon') {1774$url= picon_url($email);1775}1776# Other providers can be added by extending the if chain, defining $url1777# as needed. If no variant puts something in $url, we assume avatars1778# are completely disabled/unavailable.1779if($url) {1780return$pre_white.1781"<img width=\"$size\"".1782"class=\"avatar\"".1783"src=\"$url\"".1784"alt=\"\"".1785"/>".$post_white;1786}else{1787return"";1788}1789}17901791sub format_search_author {1792my($author,$searchtype,$displaytext) =@_;1793my$have_search= gitweb_check_feature('search');17941795if($have_search) {1796my$performed="";1797if($searchtypeeq'author') {1798$performed="authored";1799}elsif($searchtypeeq'committer') {1800$performed="committed";1801}18021803return$cgi->a({-href => href(action=>"search", hash=>$hash,1804 searchtext=>$author,1805 searchtype=>$searchtype),class=>"list",1806 title=>"Search for commits$performedby$author"},1807$displaytext);18081809}else{1810return$displaytext;1811}1812}18131814# format the author name of the given commit with the given tag1815# the author name is chopped and escaped according to the other1816# optional parameters (see chop_str).1817sub format_author_html {1818my$tag=shift;1819my$co=shift;1820my$author= chop_and_escape_str($co->{'author_name'},@_);1821return"<$tagclass=\"author\">".1822 format_search_author($co->{'author_name'},"author",1823 git_get_avatar($co->{'author_email'}, -pad_after =>1) .1824$author) .1825"</$tag>";1826}18271828# format git diff header line, i.e. "diff --(git|combined|cc) ..."1829sub format_git_diff_header_line {1830my$line=shift;1831my$diffinfo=shift;1832my($from,$to) =@_;18331834if($diffinfo->{'nparents'}) {1835# combined diff1836$line=~s!^(diff (.*?) )"?.*$!$1!;1837if($to->{'href'}) {1838$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1839 esc_path($to->{'file'}));1840}else{# file was deleted (no href)1841$line.= esc_path($to->{'file'});1842}1843}else{1844# "ordinary" diff1845$line=~s!^(diff (.*?) )"?a/.*$!$1!;1846if($from->{'href'}) {1847$line.=$cgi->a({-href =>$from->{'href'}, -class=>"path"},1848'a/'. esc_path($from->{'file'}));1849}else{# file was added (no href)1850$line.='a/'. esc_path($from->{'file'});1851}1852$line.=' ';1853if($to->{'href'}) {1854$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1855'b/'. esc_path($to->{'file'}));1856}else{# file was deleted1857$line.='b/'. esc_path($to->{'file'});1858}1859}18601861return"<div class=\"diff header\">$line</div>\n";1862}18631864# format extended diff header line, before patch itself1865sub format_extended_diff_header_line {1866my$line=shift;1867my$diffinfo=shift;1868my($from,$to) =@_;18691870# match <path>1871if($line=~s!^((copy|rename) from ).*$!$1!&&$from->{'href'}) {1872$line.=$cgi->a({-href=>$from->{'href'}, -class=>"path"},1873 esc_path($from->{'file'}));1874}1875if($line=~s!^((copy|rename) to ).*$!$1!&&$to->{'href'}) {1876$line.=$cgi->a({-href=>$to->{'href'}, -class=>"path"},1877 esc_path($to->{'file'}));1878}1879# match single <mode>1880if($line=~m/\s(\d{6})$/) {1881$line.='<span class="info"> ('.1882 file_type_long($1) .1883')</span>';1884}1885# match <hash>1886if($line=~m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {1887# can match only for combined diff1888$line='index ';1889for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1890if($from->{'href'}[$i]) {1891$line.=$cgi->a({-href=>$from->{'href'}[$i],1892-class=>"hash"},1893substr($diffinfo->{'from_id'}[$i],0,7));1894}else{1895$line.='0' x 7;1896}1897# separator1898$line.=','if($i<$diffinfo->{'nparents'} -1);1899}1900$line.='..';1901if($to->{'href'}) {1902$line.=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1903substr($diffinfo->{'to_id'},0,7));1904}else{1905$line.='0' x 7;1906}19071908}elsif($line=~m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {1909# can match only for ordinary diff1910my($from_link,$to_link);1911if($from->{'href'}) {1912$from_link=$cgi->a({-href=>$from->{'href'}, -class=>"hash"},1913substr($diffinfo->{'from_id'},0,7));1914}else{1915$from_link='0' x 7;1916}1917if($to->{'href'}) {1918$to_link=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1919substr($diffinfo->{'to_id'},0,7));1920}else{1921$to_link='0' x 7;1922}1923my($from_id,$to_id) = ($diffinfo->{'from_id'},$diffinfo->{'to_id'});1924$line=~s!$from_id\.\.$to_id!$from_link..$to_link!;1925}19261927return$line."<br/>\n";1928}19291930# format from-file/to-file diff header1931sub format_diff_from_to_header {1932my($from_line,$to_line,$diffinfo,$from,$to,@parents) =@_;1933my$line;1934my$result='';19351936$line=$from_line;1937#assert($line =~ m/^---/) if DEBUG;1938# no extra formatting for "^--- /dev/null"1939if(!$diffinfo->{'nparents'}) {1940# ordinary (single parent) diff1941if($line=~m!^--- "?a/!) {1942if($from->{'href'}) {1943$line='--- a/'.1944$cgi->a({-href=>$from->{'href'}, -class=>"path"},1945 esc_path($from->{'file'}));1946}else{1947$line='--- a/'.1948 esc_path($from->{'file'});1949}1950}1951$result.= qq!<div class="diff from_file">$line</div>\n!;19521953}else{1954# combined diff (merge commit)1955for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1956if($from->{'href'}[$i]) {1957$line='--- '.1958$cgi->a({-href=>href(action=>"blobdiff",1959 hash_parent=>$diffinfo->{'from_id'}[$i],1960 hash_parent_base=>$parents[$i],1961 file_parent=>$from->{'file'}[$i],1962 hash=>$diffinfo->{'to_id'},1963 hash_base=>$hash,1964 file_name=>$to->{'file'}),1965-class=>"path",1966-title=>"diff". ($i+1)},1967$i+1) .1968'/'.1969$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},1970 esc_path($from->{'file'}[$i]));1971}else{1972$line='--- /dev/null';1973}1974$result.= qq!<div class="diff from_file">$line</div>\n!;1975}1976}19771978$line=$to_line;1979#assert($line =~ m/^\+\+\+/) if DEBUG;1980# no extra formatting for "^+++ /dev/null"1981if($line=~m!^\+\+\+ "?b/!) {1982if($to->{'href'}) {1983$line='+++ b/'.1984$cgi->a({-href=>$to->{'href'}, -class=>"path"},1985 esc_path($to->{'file'}));1986}else{1987$line='+++ b/'.1988 esc_path($to->{'file'});1989}1990}1991$result.= qq!<div class="diff to_file">$line</div>\n!;19921993return$result;1994}19951996# create note for patch simplified by combined diff1997sub format_diff_cc_simplified {1998my($diffinfo,@parents) =@_;1999my$result='';20002001$result.="<div class=\"diff header\">".2002"diff --cc ";2003if(!is_deleted($diffinfo)) {2004$result.=$cgi->a({-href => href(action=>"blob",2005 hash_base=>$hash,2006 hash=>$diffinfo->{'to_id'},2007 file_name=>$diffinfo->{'to_file'}),2008-class=>"path"},2009 esc_path($diffinfo->{'to_file'}));2010}else{2011$result.= esc_path($diffinfo->{'to_file'});2012}2013$result.="</div>\n".# class="diff header"2014"<div class=\"diff nodifferences\">".2015"Simple merge".2016"</div>\n";# class="diff nodifferences"20172018return$result;2019}20202021# format patch (diff) line (not to be used for diff headers)2022sub format_diff_line {2023my$line=shift;2024my($from,$to) =@_;2025my$diff_class="";20262027chomp$line;20282029if($from&&$to&&ref($from->{'href'})eq"ARRAY") {2030# combined diff2031my$prefix=substr($line,0,scalar@{$from->{'href'}});2032if($line=~m/^\@{3}/) {2033$diff_class=" chunk_header";2034}elsif($line=~m/^\\/) {2035$diff_class=" incomplete";2036}elsif($prefix=~tr/+/+/) {2037$diff_class=" add";2038}elsif($prefix=~tr/-/-/) {2039$diff_class=" rem";2040}2041}else{2042# assume ordinary diff2043my$char=substr($line,0,1);2044if($chareq'+') {2045$diff_class=" add";2046}elsif($chareq'-') {2047$diff_class=" rem";2048}elsif($chareq'@') {2049$diff_class=" chunk_header";2050}elsif($chareq"\\") {2051$diff_class=" incomplete";2052}2053}2054$line= untabify($line);2055if($from&&$to&&$line=~m/^\@{2} /) {2056my($from_text,$from_start,$from_lines,$to_text,$to_start,$to_lines,$section) =2057$line=~m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;20582059$from_lines=0unlessdefined$from_lines;2060$to_lines=0unlessdefined$to_lines;20612062if($from->{'href'}) {2063$from_text=$cgi->a({-href=>"$from->{'href'}#l$from_start",2064-class=>"list"},$from_text);2065}2066if($to->{'href'}) {2067$to_text=$cgi->a({-href=>"$to->{'href'}#l$to_start",2068-class=>"list"},$to_text);2069}2070$line="<span class=\"chunk_info\">@@$from_text$to_text@@</span>".2071"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";2072return"<div class=\"diff$diff_class\">$line</div>\n";2073}elsif($from&&$to&&$line=~m/^\@{3}/) {2074my($prefix,$ranges,$section) =$line=~m/^(\@+) (.*?) \@+(.*)$/;2075my(@from_text,@from_start,@from_nlines,$to_text,$to_start,$to_nlines);20762077@from_text=split(' ',$ranges);2078for(my$i=0;$i<@from_text; ++$i) {2079($from_start[$i],$from_nlines[$i]) =2080(split(',',substr($from_text[$i],1)),0);2081}20822083$to_text=pop@from_text;2084$to_start=pop@from_start;2085$to_nlines=pop@from_nlines;20862087$line="<span class=\"chunk_info\">$prefix";2088for(my$i=0;$i<@from_text; ++$i) {2089if($from->{'href'}[$i]) {2090$line.=$cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",2091-class=>"list"},$from_text[$i]);2092}else{2093$line.=$from_text[$i];2094}2095$line.=" ";2096}2097if($to->{'href'}) {2098$line.=$cgi->a({-href=>"$to->{'href'}#l$to_start",2099-class=>"list"},$to_text);2100}else{2101$line.=$to_text;2102}2103$line.="$prefix</span>".2104"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";2105return"<div class=\"diff$diff_class\">$line</div>\n";2106}2107return"<div class=\"diff$diff_class\">". esc_html($line, -nbsp=>1) ."</div>\n";2108}21092110# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",2111# linked. Pass the hash of the tree/commit to snapshot.2112sub format_snapshot_links {2113my($hash) =@_;2114my$num_fmts=@snapshot_fmts;2115if($num_fmts>1) {2116# A parenthesized list of links bearing format names.2117# e.g. "snapshot (_tar.gz_ _zip_)"2118return"snapshot (".join(' ',map2119$cgi->a({2120-href => href(2121 action=>"snapshot",2122 hash=>$hash,2123 snapshot_format=>$_2124)2125},$known_snapshot_formats{$_}{'display'})2126,@snapshot_fmts) .")";2127}elsif($num_fmts==1) {2128# A single "snapshot" link whose tooltip bears the format name.2129# i.e. "_snapshot_"2130my($fmt) =@snapshot_fmts;2131return2132$cgi->a({2133-href => href(2134 action=>"snapshot",2135 hash=>$hash,2136 snapshot_format=>$fmt2137),2138-title =>"in format:$known_snapshot_formats{$fmt}{'display'}"2139},"snapshot");2140}else{# $num_fmts == 02141returnundef;2142}2143}21442145## ......................................................................2146## functions returning values to be passed, perhaps after some2147## transformation, to other functions; e.g. returning arguments to href()21482149# returns hash to be passed to href to generate gitweb URL2150# in -title key it returns description of link2151sub get_feed_info {2152my$format=shift||'Atom';2153my%res= (action =>lc($format));21542155# feed links are possible only for project views2156return unless(defined$project);2157# some views should link to OPML, or to generic project feed,2158# or don't have specific feed yet (so they should use generic)2159return if($action=~/^(?:tags|heads|forks|tag|search)$/x);21602161my$branch;2162# branches refs uses 'refs/heads/' prefix (fullname) to differentiate2163# from tag links; this also makes possible to detect branch links2164if((defined$hash_base&&$hash_base=~m!^refs/heads/(.*)$!) ||2165(defined$hash&&$hash=~m!^refs/heads/(.*)$!)) {2166$branch=$1;2167}2168# find log type for feed description (title)2169my$type='log';2170if(defined$file_name) {2171$type="history of$file_name";2172$type.="/"if($actioneq'tree');2173$type.=" on '$branch'"if(defined$branch);2174}else{2175$type="log of$branch"if(defined$branch);2176}21772178$res{-title} =$type;2179$res{'hash'} = (defined$branch?"refs/heads/$branch":undef);2180$res{'file_name'} =$file_name;21812182return%res;2183}21842185## ----------------------------------------------------------------------2186## git utility subroutines, invoking git commands21872188# returns path to the core git executable and the --git-dir parameter as list2189sub git_cmd {2190$number_of_git_cmds++;2191return$GIT,'--git-dir='.$git_dir;2192}21932194# quote the given arguments for passing them to the shell2195# quote_command("command", "arg 1", "arg with ' and ! characters")2196# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"2197# Try to avoid using this function wherever possible.2198sub quote_command {2199returnjoin(' ',2200map{my$a=$_;$a=~s/(['!])/'\\$1'/g;"'$a'"}@_);2201}22022203# get HEAD ref of given project as hash2204sub git_get_head_hash {2205return git_get_full_hash(shift,'HEAD');2206}22072208sub git_get_full_hash {2209return git_get_hash(@_);2210}22112212sub git_get_short_hash {2213return git_get_hash(@_,'--short=7');2214}22152216sub git_get_hash {2217my($project,$hash,@options) =@_;2218my$o_git_dir=$git_dir;2219my$retval=undef;2220$git_dir="$projectroot/$project";2221if(open my$fd,'-|', git_cmd(),'rev-parse',2222'--verify','-q',@options,$hash) {2223$retval= <$fd>;2224chomp$retvalifdefined$retval;2225close$fd;2226}2227if(defined$o_git_dir) {2228$git_dir=$o_git_dir;2229}2230return$retval;2231}22322233# get type of given object2234sub git_get_type {2235my$hash=shift;22362237open my$fd,"-|", git_cmd(),"cat-file",'-t',$hashorreturn;2238my$type= <$fd>;2239close$fdorreturn;2240chomp$type;2241return$type;2242}22432244# repository configuration2245our$config_file='';2246our%config;22472248# store multiple values for single key as anonymous array reference2249# single values stored directly in the hash, not as [ <value> ]2250sub hash_set_multi {2251my($hash,$key,$value) =@_;22522253if(!exists$hash->{$key}) {2254$hash->{$key} =$value;2255}elsif(!ref$hash->{$key}) {2256$hash->{$key} = [$hash->{$key},$value];2257}else{2258push@{$hash->{$key}},$value;2259}2260}22612262# return hash of git project configuration2263# optionally limited to some section, e.g. 'gitweb'2264sub git_parse_project_config {2265my$section_regexp=shift;2266my%config;22672268local$/="\0";22692270open my$fh,"-|", git_cmd(),"config",'-z','-l',2271orreturn;22722273while(my$keyval= <$fh>) {2274chomp$keyval;2275my($key,$value) =split(/\n/,$keyval,2);22762277 hash_set_multi(\%config,$key,$value)2278if(!defined$section_regexp||$key=~/^(?:$section_regexp)\./o);2279}2280close$fh;22812282return%config;2283}22842285# convert config value to boolean: 'true' or 'false'2286# no value, number > 0, 'true' and 'yes' values are true2287# rest of values are treated as false (never as error)2288sub config_to_bool {2289my$val=shift;22902291return1if!defined$val;# section.key22922293# strip leading and trailing whitespace2294$val=~s/^\s+//;2295$val=~s/\s+$//;22962297return(($val=~/^\d+$/&&$val) ||# section.key = 12298($val=~/^(?:true|yes)$/i));# section.key = true2299}23002301# convert config value to simple decimal number2302# an optional value suffix of 'k', 'm', or 'g' will cause the value2303# to be multiplied by 1024, 1048576, or 10737418242304sub config_to_int {2305my$val=shift;23062307# strip leading and trailing whitespace2308$val=~s/^\s+//;2309$val=~s/\s+$//;23102311if(my($num,$unit) = ($val=~/^([0-9]*)([kmg])$/i)) {2312$unit=lc($unit);2313# unknown unit is treated as 12314return$num* ($uniteq'g'?1073741824:2315$uniteq'm'?1048576:2316$uniteq'k'?1024:1);2317}2318return$val;2319}23202321# convert config value to array reference, if needed2322sub config_to_multi {2323my$val=shift;23242325returnref($val) ?$val: (defined($val) ? [$val] : []);2326}23272328sub git_get_project_config {2329my($key,$type) =@_;23302331return unlessdefined$git_dir;23322333# key sanity check2334return unless($key);2335$key=~s/^gitweb\.//;2336return if($key=~m/\W/);23372338# type sanity check2339if(defined$type) {2340$type=~s/^--//;2341$type=undef2342unless($typeeq'bool'||$typeeq'int');2343}23442345# get config2346if(!defined$config_file||2347$config_filene"$git_dir/config") {2348%config= git_parse_project_config('gitweb');2349$config_file="$git_dir/config";2350}23512352# check if config variable (key) exists2353return unlessexists$config{"gitweb.$key"};23542355# ensure given type2356if(!defined$type) {2357return$config{"gitweb.$key"};2358}elsif($typeeq'bool') {2359# backward compatibility: 'git config --bool' returns true/false2360return config_to_bool($config{"gitweb.$key"}) ?'true':'false';2361}elsif($typeeq'int') {2362return config_to_int($config{"gitweb.$key"});2363}2364return$config{"gitweb.$key"};2365}23662367# get hash of given path at given ref2368sub git_get_hash_by_path {2369my$base=shift;2370my$path=shift||returnundef;2371my$type=shift;23722373$path=~ s,/+$,,;23742375open my$fd,"-|", git_cmd(),"ls-tree",$base,"--",$path2376or die_error(500,"Open git-ls-tree failed");2377my$line= <$fd>;2378close$fdorreturnundef;23792380if(!defined$line) {2381# there is no tree or hash given by $path at $base2382returnundef;2383}23842385#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2386$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;2387if(defined$type&&$typene$2) {2388# type doesn't match2389returnundef;2390}2391return$3;2392}23932394# get path of entry with given hash at given tree-ish (ref)2395# used to get 'from' filename for combined diff (merge commit) for renames2396sub git_get_path_by_hash {2397my$base=shift||return;2398my$hash=shift||return;23992400local$/="\0";24012402open my$fd,"-|", git_cmd(),"ls-tree",'-r','-t','-z',$base2403orreturnundef;2404while(my$line= <$fd>) {2405chomp$line;24062407#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'2408#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'2409if($line=~m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {2410close$fd;2411return$1;2412}2413}2414close$fd;2415returnundef;2416}24172418## ......................................................................2419## git utility functions, directly accessing git repository24202421sub git_get_project_description {2422my$path=shift;24232424$git_dir="$projectroot/$path";2425open my$fd,'<',"$git_dir/description"2426orreturn git_get_project_config('description');2427my$descr= <$fd>;2428close$fd;2429if(defined$descr) {2430chomp$descr;2431}2432return$descr;2433}24342435sub git_get_project_ctags {2436my$path=shift;2437my$ctags= {};24382439$git_dir="$projectroot/$path";2440opendir my$dh,"$git_dir/ctags"2441orreturn$ctags;2442foreach(grep{ -f $_}map{"$git_dir/ctags/$_"}readdir($dh)) {2443open my$ct,'<',$_ornext;2444my$val= <$ct>;2445chomp$val;2446close$ct;2447my$ctag=$_;$ctag=~ s#.*/##;2448$ctags->{$ctag} =$val;2449}2450closedir$dh;2451$ctags;2452}24532454sub git_populate_project_tagcloud {2455my$ctags=shift;24562457# First, merge different-cased tags; tags vote on casing2458my%ctags_lc;2459foreach(keys%$ctags) {2460$ctags_lc{lc$_}->{count} +=$ctags->{$_};2461if(not$ctags_lc{lc$_}->{topcount}2462or$ctags_lc{lc$_}->{topcount} <$ctags->{$_}) {2463$ctags_lc{lc$_}->{topcount} =$ctags->{$_};2464$ctags_lc{lc$_}->{topname} =$_;2465}2466}24672468my$cloud;2469if(eval{require HTML::TagCloud;1; }) {2470$cloud= HTML::TagCloud->new;2471foreach(sort keys%ctags_lc) {2472# Pad the title with spaces so that the cloud looks2473# less crammed.2474my$title=$ctags_lc{$_}->{topname};2475$title=~s/ / /g;2476$title=~s/^/ /g;2477$title=~s/$/ /g;2478$cloud->add($title,$home_link."?by_tag=".$_,$ctags_lc{$_}->{count});2479}2480}else{2481$cloud= \%ctags_lc;2482}2483$cloud;2484}24852486sub git_show_project_tagcloud {2487my($cloud,$count) =@_;2488print STDERR ref($cloud)."..\n";2489if(ref$cloudeq'HTML::TagCloud') {2490return$cloud->html_and_css($count);2491}else{2492my@tags=sort{$cloud->{$a}->{count} <=>$cloud->{$b}->{count} }keys%$cloud;2493return'<p align="center">'.join(', ',map{2494"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"2495}splice(@tags,0,$count)) .'</p>';2496}2497}24982499sub git_get_project_url_list {2500my$path=shift;25012502$git_dir="$projectroot/$path";2503open my$fd,'<',"$git_dir/cloneurl"2504orreturnwantarray?2505@{ config_to_multi(git_get_project_config('url')) } :2506 config_to_multi(git_get_project_config('url'));2507my@git_project_url_list=map{chomp;$_} <$fd>;2508close$fd;25092510returnwantarray?@git_project_url_list: \@git_project_url_list;2511}25122513sub git_get_projects_list {2514my($filter) =@_;2515my@list;25162517$filter||='';2518$filter=~s/\.git$//;25192520my$check_forks= gitweb_check_feature('forks');25212522if(-d $projects_list) {2523# search in directory2524my$dir=$projects_list. ($filter?"/$filter":'');2525# remove the trailing "/"2526$dir=~s!/+$!!;2527my$pfxlen=length("$dir");2528my$pfxdepth= ($dir=~tr!/!!);25292530 File::Find::find({2531 follow_fast =>1,# follow symbolic links2532 follow_skip =>2,# ignore duplicates2533 dangling_symlinks =>0,# ignore dangling symlinks, silently2534 wanted =>sub{2535# skip project-list toplevel, if we get it.2536return if(m!^[/.]$!);2537# only directories can be git repositories2538return unless(-d $_);2539# don't traverse too deep (Find is super slow on os x)2540if(($File::Find::name =~tr!/!!) -$pfxdepth>$project_maxdepth) {2541$File::Find::prune =1;2542return;2543}25442545my$subdir=substr($File::Find::name,$pfxlen+1);2546# we check related file in $projectroot2547my$path= ($filter?"$filter/":'') .$subdir;2548if(check_export_ok("$projectroot/$path")) {2549push@list, { path =>$path};2550$File::Find::prune =1;2551}2552},2553},"$dir");25542555}elsif(-f $projects_list) {2556# read from file(url-encoded):2557# 'git%2Fgit.git Linus+Torvalds'2558# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2559# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2560my%paths;2561open my$fd,'<',$projects_listorreturn;2562 PROJECT:2563while(my$line= <$fd>) {2564chomp$line;2565my($path,$owner) =split' ',$line;2566$path= unescape($path);2567$owner= unescape($owner);2568if(!defined$path) {2569next;2570}2571if($filterne'') {2572# looking for forks;2573my$pfx=substr($path,0,length($filter));2574if($pfxne$filter) {2575next PROJECT;2576}2577my$sfx=substr($path,length($filter));2578if($sfx!~/^\/.*\.git$/) {2579next PROJECT;2580}2581}elsif($check_forks) {2582 PATH:2583foreachmy$filter(keys%paths) {2584# looking for forks;2585my$pfx=substr($path,0,length($filter));2586if($pfxne$filter) {2587next PATH;2588}2589my$sfx=substr($path,length($filter));2590if($sfx!~/^\/.*\.git$/) {2591next PATH;2592}2593# is a fork, don't include it in2594# the list2595next PROJECT;2596}2597}2598if(check_export_ok("$projectroot/$path")) {2599my$pr= {2600 path =>$path,2601 owner => to_utf8($owner),2602};2603push@list,$pr;2604(my$forks_path=$path) =~s/\.git$//;2605$paths{$forks_path}++;2606}2607}2608close$fd;2609}2610return@list;2611}26122613our$gitweb_project_owner=undef;2614sub git_get_project_list_from_file {26152616return if(defined$gitweb_project_owner);26172618$gitweb_project_owner= {};2619# read from file (url-encoded):2620# 'git%2Fgit.git Linus+Torvalds'2621# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2622# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2623if(-f $projects_list) {2624open(my$fd,'<',$projects_list);2625while(my$line= <$fd>) {2626chomp$line;2627my($pr,$ow) =split' ',$line;2628$pr= unescape($pr);2629$ow= unescape($ow);2630$gitweb_project_owner->{$pr} = to_utf8($ow);2631}2632close$fd;2633}2634}26352636sub git_get_project_owner {2637my$project=shift;2638my$owner;26392640returnundefunless$project;2641$git_dir="$projectroot/$project";26422643if(!defined$gitweb_project_owner) {2644 git_get_project_list_from_file();2645}26462647if(exists$gitweb_project_owner->{$project}) {2648$owner=$gitweb_project_owner->{$project};2649}2650if(!defined$owner){2651$owner= git_get_project_config('owner');2652}2653if(!defined$owner) {2654$owner= get_file_owner("$git_dir");2655}26562657return$owner;2658}26592660sub git_get_last_activity {2661my($path) =@_;2662my$fd;26632664$git_dir="$projectroot/$path";2665open($fd,"-|", git_cmd(),'for-each-ref',2666'--format=%(committer)',2667'--sort=-committerdate',2668'--count=1',2669'refs/heads')orreturn;2670my$most_recent= <$fd>;2671close$fdorreturn;2672if(defined$most_recent&&2673$most_recent=~/ (\d+) [-+][01]\d\d\d$/) {2674my$timestamp=$1;2675my$age=time-$timestamp;2676return($age, age_string($age));2677}2678return(undef,undef);2679}26802681sub git_get_references {2682my$type=shift||"";2683my%refs;2684# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.112685# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}2686open my$fd,"-|", git_cmd(),"show-ref","--dereference",2687($type? ("--","refs/$type") : ())# use -- <pattern> if $type2688orreturn;26892690while(my$line= <$fd>) {2691chomp$line;2692if($line=~m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {2693if(defined$refs{$1}) {2694push@{$refs{$1}},$2;2695}else{2696$refs{$1} = [$2];2697}2698}2699}2700close$fdorreturn;2701return \%refs;2702}27032704sub git_get_rev_name_tags {2705my$hash=shift||returnundef;27062707open my$fd,"-|", git_cmd(),"name-rev","--tags",$hash2708orreturn;2709my$name_rev= <$fd>;2710close$fd;27112712if($name_rev=~ m|^$hash tags/(.*)$|) {2713return$1;2714}else{2715# catches also '$hash undefined' output2716returnundef;2717}2718}27192720## ----------------------------------------------------------------------2721## parse to hash functions27222723sub parse_date {2724my$epoch=shift;2725my$tz=shift||"-0000";27262727my%date;2728my@months= ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");2729my@days= ("Sun","Mon","Tue","Wed","Thu","Fri","Sat");2730my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($epoch);2731$date{'hour'} =$hour;2732$date{'minute'} =$min;2733$date{'mday'} =$mday;2734$date{'day'} =$days[$wday];2735$date{'month'} =$months[$mon];2736$date{'rfc2822'} =sprintf"%s,%d%s%4d%02d:%02d:%02d+0000",2737$days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec;2738$date{'mday-time'} =sprintf"%d%s%02d:%02d",2739$mday,$months[$mon],$hour,$min;2740$date{'iso-8601'} =sprintf"%04d-%02d-%02dT%02d:%02d:%02dZ",27411900+$year,1+$mon,$mday,$hour,$min,$sec;27422743$tz=~m/^([+\-][0-9][0-9])([0-9][0-9])$/;2744my$local=$epoch+ ((int$1+ ($2/60)) *3600);2745($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($local);2746$date{'hour_local'} =$hour;2747$date{'minute_local'} =$min;2748$date{'tz_local'} =$tz;2749$date{'iso-tz'} =sprintf("%04d-%02d-%02d%02d:%02d:%02d%s",27501900+$year,$mon+1,$mday,2751$hour,$min,$sec,$tz);2752return%date;2753}27542755sub parse_tag {2756my$tag_id=shift;2757my%tag;2758my@comment;27592760open my$fd,"-|", git_cmd(),"cat-file","tag",$tag_idorreturn;2761$tag{'id'} =$tag_id;2762while(my$line= <$fd>) {2763chomp$line;2764if($line=~m/^object ([0-9a-fA-F]{40})$/) {2765$tag{'object'} =$1;2766}elsif($line=~m/^type (.+)$/) {2767$tag{'type'} =$1;2768}elsif($line=~m/^tag (.+)$/) {2769$tag{'name'} =$1;2770}elsif($line=~m/^tagger (.*) ([0-9]+) (.*)$/) {2771$tag{'author'} =$1;2772$tag{'author_epoch'} =$2;2773$tag{'author_tz'} =$3;2774if($tag{'author'} =~m/^([^<]+) <([^>]*)>/) {2775$tag{'author_name'} =$1;2776$tag{'author_email'} =$2;2777}else{2778$tag{'author_name'} =$tag{'author'};2779}2780}elsif($line=~m/--BEGIN/) {2781push@comment,$line;2782last;2783}elsif($lineeq"") {2784last;2785}2786}2787push@comment, <$fd>;2788$tag{'comment'} = \@comment;2789close$fdorreturn;2790if(!defined$tag{'name'}) {2791return2792};2793return%tag2794}27952796sub parse_commit_text {2797my($commit_text,$withparents) =@_;2798my@commit_lines=split'\n',$commit_text;2799my%co;28002801pop@commit_lines;# Remove '\0'28022803if(!@commit_lines) {2804return;2805}28062807my$header=shift@commit_lines;2808if($header!~m/^[0-9a-fA-F]{40}/) {2809return;2810}2811($co{'id'},my@parents) =split' ',$header;2812while(my$line=shift@commit_lines) {2813last if$lineeq"\n";2814if($line=~m/^tree ([0-9a-fA-F]{40})$/) {2815$co{'tree'} =$1;2816}elsif((!defined$withparents) && ($line=~m/^parent ([0-9a-fA-F]{40})$/)) {2817push@parents,$1;2818}elsif($line=~m/^author (.*) ([0-9]+) (.*)$/) {2819$co{'author'} = to_utf8($1);2820$co{'author_epoch'} =$2;2821$co{'author_tz'} =$3;2822if($co{'author'} =~m/^([^<]+) <([^>]*)>/) {2823$co{'author_name'} =$1;2824$co{'author_email'} =$2;2825}else{2826$co{'author_name'} =$co{'author'};2827}2828}elsif($line=~m/^committer (.*) ([0-9]+) (.*)$/) {2829$co{'committer'} = to_utf8($1);2830$co{'committer_epoch'} =$2;2831$co{'committer_tz'} =$3;2832if($co{'committer'} =~m/^([^<]+) <([^>]*)>/) {2833$co{'committer_name'} =$1;2834$co{'committer_email'} =$2;2835}else{2836$co{'committer_name'} =$co{'committer'};2837}2838}2839}2840if(!defined$co{'tree'}) {2841return;2842};2843$co{'parents'} = \@parents;2844$co{'parent'} =$parents[0];28452846foreachmy$title(@commit_lines) {2847$title=~s/^ //;2848if($titlene"") {2849$co{'title'} = chop_str($title,80,5);2850# remove leading stuff of merges to make the interesting part visible2851if(length($title) >50) {2852$title=~s/^Automatic //;2853$title=~s/^merge (of|with) /Merge ... /i;2854if(length($title) >50) {2855$title=~s/(http|rsync):\/\///;2856}2857if(length($title) >50) {2858$title=~s/(master|www|rsync)\.//;2859}2860if(length($title) >50) {2861$title=~s/kernel.org:?//;2862}2863if(length($title) >50) {2864$title=~s/\/pub\/scm//;2865}2866}2867$co{'title_short'} = chop_str($title,50,5);2868last;2869}2870}2871if(!defined$co{'title'} ||$co{'title'}eq"") {2872$co{'title'} =$co{'title_short'} ='(no commit message)';2873}2874# remove added spaces2875foreachmy$line(@commit_lines) {2876$line=~s/^ //;2877}2878$co{'comment'} = \@commit_lines;28792880my$age=time-$co{'committer_epoch'};2881$co{'age'} =$age;2882$co{'age_string'} = age_string($age);2883my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($co{'committer_epoch'});2884if($age>60*60*24*7*2) {2885$co{'age_string_date'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2886$co{'age_string_age'} =$co{'age_string'};2887}else{2888$co{'age_string_date'} =$co{'age_string'};2889$co{'age_string_age'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2890}2891return%co;2892}28932894sub parse_commit {2895my($commit_id) =@_;2896my%co;28972898local$/="\0";28992900open my$fd,"-|", git_cmd(),"rev-list",2901"--parents",2902"--header",2903"--max-count=1",2904$commit_id,2905"--",2906or die_error(500,"Open git-rev-list failed");2907%co= parse_commit_text(<$fd>,1);2908close$fd;29092910return%co;2911}29122913sub parse_commits {2914my($commit_id,$maxcount,$skip,$filename,@args) =@_;2915my@cos;29162917$maxcount||=1;2918$skip||=0;29192920local$/="\0";29212922open my$fd,"-|", git_cmd(),"rev-list",2923"--header",2924@args,2925("--max-count=".$maxcount),2926("--skip=".$skip),2927@extra_options,2928$commit_id,2929"--",2930($filename? ($filename) : ())2931or die_error(500,"Open git-rev-list failed");2932while(my$line= <$fd>) {2933my%co= parse_commit_text($line);2934push@cos, \%co;2935}2936close$fd;29372938returnwantarray?@cos: \@cos;2939}29402941# parse line of git-diff-tree "raw" output2942sub parse_difftree_raw_line {2943my$line=shift;2944my%res;29452946# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'2947# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'2948if($line=~m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {2949$res{'from_mode'} =$1;2950$res{'to_mode'} =$2;2951$res{'from_id'} =$3;2952$res{'to_id'} =$4;2953$res{'status'} =$5;2954$res{'similarity'} =$6;2955if($res{'status'}eq'R'||$res{'status'}eq'C') {# renamed or copied2956($res{'from_file'},$res{'to_file'}) =map{ unquote($_) }split("\t",$7);2957}else{2958$res{'from_file'} =$res{'to_file'} =$res{'file'} = unquote($7);2959}2960}2961# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'2962# combined diff (for merge commit)2963elsif($line=~s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {2964$res{'nparents'} =length($1);2965$res{'from_mode'} = [split(' ',$2) ];2966$res{'to_mode'} =pop@{$res{'from_mode'}};2967$res{'from_id'} = [split(' ',$3) ];2968$res{'to_id'} =pop@{$res{'from_id'}};2969$res{'status'} = [split('',$4) ];2970$res{'to_file'} = unquote($5);2971}2972# 'c512b523472485aef4fff9e57b229d9d243c967f'2973elsif($line=~m/^([0-9a-fA-F]{40})$/) {2974$res{'commit'} =$1;2975}29762977returnwantarray?%res: \%res;2978}29792980# wrapper: return parsed line of git-diff-tree "raw" output2981# (the argument might be raw line, or parsed info)2982sub parsed_difftree_line {2983my$line_or_ref=shift;29842985if(ref($line_or_ref)eq"HASH") {2986# pre-parsed (or generated by hand)2987return$line_or_ref;2988}else{2989return parse_difftree_raw_line($line_or_ref);2990}2991}29922993# parse line of git-ls-tree output2994sub parse_ls_tree_line {2995my$line=shift;2996my%opts=@_;2997my%res;29982999if($opts{'-l'}) {3000#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'3001$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;30023003$res{'mode'} =$1;3004$res{'type'} =$2;3005$res{'hash'} =$3;3006$res{'size'} =$4;3007if($opts{'-z'}) {3008$res{'name'} =$5;3009}else{3010$res{'name'} = unquote($5);3011}3012}else{3013#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'3014$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;30153016$res{'mode'} =$1;3017$res{'type'} =$2;3018$res{'hash'} =$3;3019if($opts{'-z'}) {3020$res{'name'} =$4;3021}else{3022$res{'name'} = unquote($4);3023}3024}30253026returnwantarray?%res: \%res;3027}30283029# generates _two_ hashes, references to which are passed as 2 and 3 argument3030sub parse_from_to_diffinfo {3031my($diffinfo,$from,$to,@parents) =@_;30323033if($diffinfo->{'nparents'}) {3034# combined diff3035$from->{'file'} = [];3036$from->{'href'} = [];3037 fill_from_file_info($diffinfo,@parents)3038unlessexists$diffinfo->{'from_file'};3039for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {3040$from->{'file'}[$i] =3041defined$diffinfo->{'from_file'}[$i] ?3042$diffinfo->{'from_file'}[$i] :3043$diffinfo->{'to_file'};3044if($diffinfo->{'status'}[$i]ne"A") {# not new (added) file3045$from->{'href'}[$i] = href(action=>"blob",3046 hash_base=>$parents[$i],3047 hash=>$diffinfo->{'from_id'}[$i],3048 file_name=>$from->{'file'}[$i]);3049}else{3050$from->{'href'}[$i] =undef;3051}3052}3053}else{3054# ordinary (not combined) diff3055$from->{'file'} =$diffinfo->{'from_file'};3056if($diffinfo->{'status'}ne"A") {# not new (added) file3057$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,3058 hash=>$diffinfo->{'from_id'},3059 file_name=>$from->{'file'});3060}else{3061delete$from->{'href'};3062}3063}30643065$to->{'file'} =$diffinfo->{'to_file'};3066if(!is_deleted($diffinfo)) {# file exists in result3067$to->{'href'} = href(action=>"blob", hash_base=>$hash,3068 hash=>$diffinfo->{'to_id'},3069 file_name=>$to->{'file'});3070}else{3071delete$to->{'href'};3072}3073}30743075## ......................................................................3076## parse to array of hashes functions30773078sub git_get_heads_list {3079my$limit=shift;3080my@headslist;30813082open my$fd,'-|', git_cmd(),'for-each-ref',3083($limit?'--count='.($limit+1) : ()),'--sort=-committerdate',3084'--format=%(objectname) %(refname) %(subject)%00%(committer)',3085'refs/heads'3086orreturn;3087while(my$line= <$fd>) {3088my%ref_item;30893090chomp$line;3091my($refinfo,$committerinfo) =split(/\0/,$line);3092my($hash,$name,$title) =split(' ',$refinfo,3);3093my($committer,$epoch,$tz) =3094($committerinfo=~/^(.*) ([0-9]+) (.*)$/);3095$ref_item{'fullname'} =$name;3096$name=~s!^refs/heads/!!;30973098$ref_item{'name'} =$name;3099$ref_item{'id'} =$hash;3100$ref_item{'title'} =$title||'(no commit message)';3101$ref_item{'epoch'} =$epoch;3102if($epoch) {3103$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3104}else{3105$ref_item{'age'} ="unknown";3106}31073108push@headslist, \%ref_item;3109}3110close$fd;31113112returnwantarray?@headslist: \@headslist;3113}31143115sub git_get_tags_list {3116my$limit=shift;3117my@tagslist;31183119open my$fd,'-|', git_cmd(),'for-each-ref',3120($limit?'--count='.($limit+1) : ()),'--sort=-creatordate',3121'--format=%(objectname) %(objecttype) %(refname) '.3122'%(*objectname) %(*objecttype) %(subject)%00%(creator)',3123'refs/tags'3124orreturn;3125while(my$line= <$fd>) {3126my%ref_item;31273128chomp$line;3129my($refinfo,$creatorinfo) =split(/\0/,$line);3130my($id,$type,$name,$refid,$reftype,$title) =split(' ',$refinfo,6);3131my($creator,$epoch,$tz) =3132($creatorinfo=~/^(.*) ([0-9]+) (.*)$/);3133$ref_item{'fullname'} =$name;3134$name=~s!^refs/tags/!!;31353136$ref_item{'type'} =$type;3137$ref_item{'id'} =$id;3138$ref_item{'name'} =$name;3139if($typeeq"tag") {3140$ref_item{'subject'} =$title;3141$ref_item{'reftype'} =$reftype;3142$ref_item{'refid'} =$refid;3143}else{3144$ref_item{'reftype'} =$type;3145$ref_item{'refid'} =$id;3146}31473148if($typeeq"tag"||$typeeq"commit") {3149$ref_item{'epoch'} =$epoch;3150if($epoch) {3151$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3152}else{3153$ref_item{'age'} ="unknown";3154}3155}31563157push@tagslist, \%ref_item;3158}3159close$fd;31603161returnwantarray?@tagslist: \@tagslist;3162}31633164## ----------------------------------------------------------------------3165## filesystem-related functions31663167sub get_file_owner {3168my$path=shift;31693170my($dev,$ino,$mode,$nlink,$st_uid,$st_gid,$rdev,$size) =stat($path);3171my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) =getpwuid($st_uid);3172if(!defined$gcos) {3173returnundef;3174}3175my$owner=$gcos;3176$owner=~s/[,;].*$//;3177return to_utf8($owner);3178}31793180# assume that file exists3181sub insert_file {3182my$filename=shift;31833184open my$fd,'<',$filename;3185print map{ to_utf8($_) } <$fd>;3186close$fd;3187}31883189## ......................................................................3190## mimetype related functions31913192sub mimetype_guess_file {3193my$filename=shift;3194my$mimemap=shift;3195-r $mimemaporreturnundef;31963197my%mimemap;3198open(my$mh,'<',$mimemap)orreturnundef;3199while(<$mh>) {3200next ifm/^#/;# skip comments3201my($mimetype,$exts) =split(/\t+/);3202if(defined$exts) {3203my@exts=split(/\s+/,$exts);3204foreachmy$ext(@exts) {3205$mimemap{$ext} =$mimetype;3206}3207}3208}3209close($mh);32103211$filename=~/\.([^.]*)$/;3212return$mimemap{$1};3213}32143215sub mimetype_guess {3216my$filename=shift;3217my$mime;3218$filename=~/\./orreturnundef;32193220if($mimetypes_file) {3221my$file=$mimetypes_file;3222if($file!~m!^/!) {# if it is relative path3223# it is relative to project3224$file="$projectroot/$project/$file";3225}3226$mime= mimetype_guess_file($filename,$file);3227}3228$mime||= mimetype_guess_file($filename,'/etc/mime.types');3229return$mime;3230}32313232sub blob_mimetype {3233my$fd=shift;3234my$filename=shift;32353236if($filename) {3237my$mime= mimetype_guess($filename);3238$mimeandreturn$mime;3239}32403241# just in case3242return$default_blob_plain_mimetypeunless$fd;32433244if(-T $fd) {3245return'text/plain';3246}elsif(!$filename) {3247return'application/octet-stream';3248}elsif($filename=~m/\.png$/i) {3249return'image/png';3250}elsif($filename=~m/\.gif$/i) {3251return'image/gif';3252}elsif($filename=~m/\.jpe?g$/i) {3253return'image/jpeg';3254}else{3255return'application/octet-stream';3256}3257}32583259sub blob_contenttype {3260my($fd,$file_name,$type) =@_;32613262$type||= blob_mimetype($fd,$file_name);3263if($typeeq'text/plain'&&defined$default_text_plain_charset) {3264$type.="; charset=$default_text_plain_charset";3265}32663267return$type;3268}32693270## ======================================================================3271## functions printing HTML: header, footer, error page32723273sub git_header_html {3274my$status=shift||"200 OK";3275my$expires=shift;32763277my$title="$site_name";3278if(defined$project) {3279$title.=" - ". to_utf8($project);3280if(defined$action) {3281$title.="/$action";3282if(defined$file_name) {3283$title.=" - ". esc_path($file_name);3284if($actioneq"tree"&&$file_name!~ m|/$|) {3285$title.="/";3286}3287}3288}3289}3290my$content_type;3291# require explicit support from the UA if we are to send the page as3292# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.3293# we have to do this because MSIE sometimes globs '*/*', pretending to3294# support xhtml+xml but choking when it gets what it asked for.3295if(defined$cgi->http('HTTP_ACCEPT') &&3296$cgi->http('HTTP_ACCEPT') =~m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&3297$cgi->Accept('application/xhtml+xml') !=0) {3298$content_type='application/xhtml+xml';3299}else{3300$content_type='text/html';3301}3302print$cgi->header(-type=>$content_type, -charset =>'utf-8',3303-status=>$status, -expires =>$expires);3304my$mod_perl_version=$ENV{'MOD_PERL'} ?"$ENV{'MOD_PERL'}":'';3305print<<EOF;3306<?xml version="1.0" encoding="utf-8"?>3307<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">3308<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">3309<!-- git web interface version$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->3310<!-- git core binaries version$git_version-->3311<head>3312<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>3313<meta name="generator" content="gitweb/$versiongit/$git_version$mod_perl_version"/>3314<meta name="robots" content="index, nofollow"/>3315<title>$title</title>3316EOF3317# the stylesheet, favicon etc urls won't work correctly with path_info3318# unless we set the appropriate base URL3319if($ENV{'PATH_INFO'}) {3320print"<base href=\"".esc_url($base_url)."\"/>\n";3321}3322# print out each stylesheet that exist, providing backwards capability3323# for those people who defined $stylesheet in a config file3324if(defined$stylesheet) {3325print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3326}else{3327foreachmy$stylesheet(@stylesheets) {3328next unless$stylesheet;3329print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3330}3331}3332if(defined$project) {3333my%href_params= get_feed_info();3334if(!exists$href_params{'-title'}) {3335$href_params{'-title'} ='log';3336}33373338foreachmy$formatqw(RSS Atom){3339my$type=lc($format);3340my%link_attr= (3341'-rel'=>'alternate',3342'-title'=>"$project-$href_params{'-title'} -$formatfeed",3343'-type'=>"application/$type+xml"3344);33453346$href_params{'action'} =$type;3347$link_attr{'-href'} = href(%href_params);3348print"<link ".3349"rel=\"$link_attr{'-rel'}\"".3350"title=\"$link_attr{'-title'}\"".3351"href=\"$link_attr{'-href'}\"".3352"type=\"$link_attr{'-type'}\"".3353"/>\n";33543355$href_params{'extra_options'} ='--no-merges';3356$link_attr{'-href'} = href(%href_params);3357$link_attr{'-title'} .=' (no merges)';3358print"<link ".3359"rel=\"$link_attr{'-rel'}\"".3360"title=\"$link_attr{'-title'}\"".3361"href=\"$link_attr{'-href'}\"".3362"type=\"$link_attr{'-type'}\"".3363"/>\n";3364}33653366}else{3367printf('<link rel="alternate" title="%sprojects list" '.3368'href="%s" type="text/plain; charset=utf-8" />'."\n",3369$site_name, href(project=>undef, action=>"project_index"));3370printf('<link rel="alternate" title="%sprojects feeds" '.3371'href="%s" type="text/x-opml" />'."\n",3372$site_name, href(project=>undef, action=>"opml"));3373}3374if(defined$favicon) {3375printqq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);3376}33773378print"</head>\n".3379"<body>\n";33803381if(defined$site_header&& -f $site_header) {3382 insert_file($site_header);3383}33843385print"<div class=\"page_header\">\n".3386$cgi->a({-href => esc_url($logo_url),3387-title =>$logo_label},3388qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));3389print$cgi->a({-href => esc_url($home_link)},$home_link_str) ." / ";3390if(defined$project) {3391print$cgi->a({-href => href(action=>"summary")}, esc_html($project));3392if(defined$action) {3393print" /$action";3394}3395print"\n";3396}3397print"</div>\n";33983399my$have_search= gitweb_check_feature('search');3400if(defined$project&&$have_search) {3401if(!defined$searchtext) {3402$searchtext="";3403}3404my$search_hash;3405if(defined$hash_base) {3406$search_hash=$hash_base;3407}elsif(defined$hash) {3408$search_hash=$hash;3409}else{3410$search_hash="HEAD";3411}3412my$action=$my_uri;3413my$use_pathinfo= gitweb_check_feature('pathinfo');3414if($use_pathinfo) {3415$action.="/".esc_url($project);3416}3417print$cgi->startform(-method=>"get", -action =>$action) .3418"<div class=\"search\">\n".3419(!$use_pathinfo&&3420$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) ."\n") .3421$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) ."\n".3422$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) ."\n".3423$cgi->popup_menu(-name =>'st', -default=>'commit',3424-values=> ['commit','grep','author','committer','pickaxe']) .3425$cgi->sup($cgi->a({-href => href(action=>"search_help")},"?")) .3426" search:\n",3427$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".3428"<span title=\"Extended regular expression\">".3429$cgi->checkbox(-name =>'sr', -value =>1, -label =>'re',3430-checked =>$search_use_regexp) .3431"</span>".3432"</div>".3433$cgi->end_form() ."\n";3434}3435}34363437sub git_footer_html {3438my$feed_class='rss_logo';34393440print"<div class=\"page_footer\">\n";3441if(defined$project) {3442my$descr= git_get_project_description($project);3443if(defined$descr) {3444print"<div class=\"page_footer_text\">". esc_html($descr) ."</div>\n";3445}34463447my%href_params= get_feed_info();3448if(!%href_params) {3449$feed_class.=' generic';3450}3451$href_params{'-title'} ||='log';34523453foreachmy$formatqw(RSS Atom){3454$href_params{'action'} =lc($format);3455print$cgi->a({-href => href(%href_params),3456-title =>"$href_params{'-title'}$formatfeed",3457-class=>$feed_class},$format)."\n";3458}34593460}else{3461print$cgi->a({-href => href(project=>undef, action=>"opml"),3462-class=>$feed_class},"OPML") ." ";3463print$cgi->a({-href => href(project=>undef, action=>"project_index"),3464-class=>$feed_class},"TXT") ."\n";3465}3466print"</div>\n";# class="page_footer"34673468if(defined$t0&& gitweb_check_feature('timed')) {3469print"<div id=\"generating_info\">\n";3470print'This page took '.3471'<span id="generating_time" class="time_span">'.3472 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).3473' seconds </span>'.3474' and '.3475'<span id="generating_cmd">'.3476$number_of_git_cmds.3477'</span> git commands '.3478" to generate.\n";3479print"</div>\n";# class="page_footer"3480}34813482if(defined$site_footer&& -f $site_footer) {3483 insert_file($site_footer);3484}34853486print qq!<script type="text/javascript" src="$javascript"></script>\n!;3487if(defined$action&&3488$actioneq'blame_incremental') {3489print qq!<script type="text/javascript">\n!.3490 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.3491 qq!"!. href() .qq!");\n!.3492 qq!</script>\n!;3493}elsif(gitweb_check_feature('javascript-actions')) {3494print qq!<script type="text/javascript">\n!.3495 qq!window.onload = fixLinks;\n!.3496 qq!</script>\n!;3497}34983499print"</body>\n".3500"</html>";3501}35023503# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])3504# Example: die_error(404, 'Hash not found')3505# By convention, use the following status codes (as defined in RFC 2616):3506# 400: Invalid or missing CGI parameters, or3507# requested object exists but has wrong type.3508# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on3509# this server or project.3510# 404: Requested object/revision/project doesn't exist.3511# 500: The server isn't configured properly, or3512# an internal error occurred (e.g. failed assertions caused by bugs), or3513# an unknown error occurred (e.g. the git binary died unexpectedly).3514# 503: The server is currently unavailable (because it is overloaded,3515# or down for maintenance). Generally, this is a temporary state.3516sub die_error {3517my$status=shift||500;3518my$error= esc_html(shift) ||"Internal Server Error";3519my$extra=shift;35203521my%http_responses= (3522400=>'400 Bad Request',3523403=>'403 Forbidden',3524404=>'404 Not Found',3525500=>'500 Internal Server Error',3526503=>'503 Service Unavailable',3527);3528 git_header_html($http_responses{$status});3529print<<EOF;3530<div class="page_body">3531<br /><br />3532$status-$error3533<br />3534EOF3535if(defined$extra) {3536print"<hr />\n".3537"$extra\n";3538}3539print"</div>\n";35403541 git_footer_html();3542goto DONE_GITWEB;3543}35443545## ----------------------------------------------------------------------3546## functions printing or outputting HTML: navigation35473548sub git_print_page_nav {3549my($current,$suppress,$head,$treehead,$treebase,$extra) =@_;3550$extra=''if!defined$extra;# pager or formats35513552my@navs=qw(summary shortlog log commit commitdiff tree);3553if($suppress) {3554@navs=grep{$_ne$suppress}@navs;3555}35563557my%arg=map{$_=> {action=>$_} }@navs;3558if(defined$head) {3559for(qw(commit commitdiff)) {3560$arg{$_}{'hash'} =$head;3561}3562if($current=~m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {3563for(qw(shortlog log)) {3564$arg{$_}{'hash'} =$head;3565}3566}3567}35683569$arg{'tree'}{'hash'} =$treeheadifdefined$treehead;3570$arg{'tree'}{'hash_base'} =$treebaseifdefined$treebase;35713572my@actions= gitweb_get_feature('actions');3573my%repl= (3574'%'=>'%',3575'n'=>$project,# project name3576'f'=>$git_dir,# project path within filesystem3577'h'=>$treehead||'',# current hash ('h' parameter)3578'b'=>$treebase||'',# hash base ('hb' parameter)3579);3580while(@actions) {3581my($label,$link,$pos) =splice(@actions,0,3);3582# insert3583@navs=map{$_eq$pos? ($_,$label) :$_}@navs;3584# munch munch3585$link=~s/%([%nfhb])/$repl{$1}/g;3586$arg{$label}{'_href'} =$link;3587}35883589print"<div class=\"page_nav\">\n".3590(join" | ",3591map{$_eq$current?3592$_:$cgi->a({-href => ($arg{$_}{_href} ?$arg{$_}{_href} : href(%{$arg{$_}}))},"$_")3593}@navs);3594print"<br/>\n$extra<br/>\n".3595"</div>\n";3596}35973598sub format_paging_nav {3599my($action,$page,$has_next_link) =@_;3600my$paging_nav;360136023603if($page>0) {3604$paging_nav.=3605$cgi->a({-href => href(-replay=>1, page=>undef)},"first") .3606" ⋅ ".3607$cgi->a({-href => href(-replay=>1, page=>$page-1),3608-accesskey =>"p", -title =>"Alt-p"},"prev");3609}else{3610$paging_nav.="first ⋅ prev";3611}36123613if($has_next_link) {3614$paging_nav.=" ⋅ ".3615$cgi->a({-href => href(-replay=>1, page=>$page+1),3616-accesskey =>"n", -title =>"Alt-n"},"next");3617}else{3618$paging_nav.=" ⋅ next";3619}36203621return$paging_nav;3622}36233624## ......................................................................3625## functions printing or outputting HTML: div36263627sub git_print_header_div {3628my($action,$title,$hash,$hash_base) =@_;3629my%args= ();36303631$args{'action'} =$action;3632$args{'hash'} =$hashif$hash;3633$args{'hash_base'} =$hash_baseif$hash_base;36343635print"<div class=\"header\">\n".3636$cgi->a({-href => href(%args), -class=>"title"},3637$title?$title:$action) .3638"\n</div>\n";3639}36403641sub print_local_time {3642print format_local_time(@_);3643}36443645sub format_local_time {3646my$localtime='';3647my%date=@_;3648if($date{'hour_local'} <6) {3649$localtime.=sprintf(" (<span class=\"atnight\">%02d:%02d</span>%s)",3650$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3651}else{3652$localtime.=sprintf(" (%02d:%02d%s)",3653$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3654}36553656return$localtime;3657}36583659# Outputs the author name and date in long form3660sub git_print_authorship {3661my$co=shift;3662my%opts=@_;3663my$tag=$opts{-tag} ||'div';3664my$author=$co->{'author_name'};36653666my%ad= parse_date($co->{'author_epoch'},$co->{'author_tz'});3667print"<$tagclass=\"author_date\">".3668 format_search_author($author,"author", esc_html($author)) .3669" [$ad{'rfc2822'}";3670 print_local_time(%ad)if($opts{-localtime});3671print"]". git_get_avatar($co->{'author_email'}, -pad_before =>1)3672."</$tag>\n";3673}36743675# Outputs table rows containing the full author or committer information,3676# in the format expected for 'commit' view (& similia).3677# Parameters are a commit hash reference, followed by the list of people3678# to output information for. If the list is empty it defalts to both3679# author and committer.3680sub git_print_authorship_rows {3681my$co=shift;3682# too bad we can't use @people = @_ || ('author', 'committer')3683my@people=@_;3684@people= ('author','committer')unless@people;3685foreachmy$who(@people) {3686my%wd= parse_date($co->{"${who}_epoch"},$co->{"${who}_tz"});3687print"<tr><td>$who</td><td>".3688 format_search_author($co->{"${who}_name"},$who,3689 esc_html($co->{"${who}_name"})) ." ".3690 format_search_author($co->{"${who}_email"},$who,3691 esc_html("<".$co->{"${who}_email"} .">")) .3692"</td><td rowspan=\"2\">".3693 git_get_avatar($co->{"${who}_email"}, -size =>'double') .3694"</td></tr>\n".3695"<tr>".3696"<td></td><td>$wd{'rfc2822'}";3697 print_local_time(%wd);3698print"</td>".3699"</tr>\n";3700}3701}37023703sub git_print_page_path {3704my$name=shift;3705my$type=shift;3706my$hb=shift;370737083709print"<div class=\"page_path\">";3710print$cgi->a({-href => href(action=>"tree", hash_base=>$hb),3711-title =>'tree root'}, to_utf8("[$project]"));3712print" / ";3713if(defined$name) {3714my@dirname=split'/',$name;3715my$basename=pop@dirname;3716my$fullname='';37173718foreachmy$dir(@dirname) {3719$fullname.= ($fullname?'/':'') .$dir;3720print$cgi->a({-href => href(action=>"tree", file_name=>$fullname,3721 hash_base=>$hb),3722-title =>$fullname}, esc_path($dir));3723print" / ";3724}3725if(defined$type&&$typeeq'blob') {3726print$cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,3727 hash_base=>$hb),3728-title =>$name}, esc_path($basename));3729}elsif(defined$type&&$typeeq'tree') {3730print$cgi->a({-href => href(action=>"tree", file_name=>$file_name,3731 hash_base=>$hb),3732-title =>$name}, esc_path($basename));3733print" / ";3734}else{3735print esc_path($basename);3736}3737}3738print"<br/></div>\n";3739}37403741sub git_print_log {3742my$log=shift;3743my%opts=@_;37443745if($opts{'-remove_title'}) {3746# remove title, i.e. first line of log3747shift@$log;3748}3749# remove leading empty lines3750while(defined$log->[0] &&$log->[0]eq"") {3751shift@$log;3752}37533754# print log3755my$signoff=0;3756my$empty=0;3757foreachmy$line(@$log) {3758if($line=~m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {3759$signoff=1;3760$empty=0;3761if(!$opts{'-remove_signoff'}) {3762print"<span class=\"signoff\">". esc_html($line) ."</span><br/>\n";3763next;3764}else{3765# remove signoff lines3766next;3767}3768}else{3769$signoff=0;3770}37713772# print only one empty line3773# do not print empty line after signoff3774if($lineeq"") {3775next if($empty||$signoff);3776$empty=1;3777}else{3778$empty=0;3779}37803781print format_log_line_html($line) ."<br/>\n";3782}37833784if($opts{'-final_empty_line'}) {3785# end with single empty line3786print"<br/>\n"unless$empty;3787}3788}37893790# return link target (what link points to)3791sub git_get_link_target {3792my$hash=shift;3793my$link_target;37943795# read link3796open my$fd,"-|", git_cmd(),"cat-file","blob",$hash3797orreturn;3798{3799local$/=undef;3800$link_target= <$fd>;3801}3802close$fd3803orreturn;38043805return$link_target;3806}38073808# given link target, and the directory (basedir) the link is in,3809# return target of link relative to top directory (top tree);3810# return undef if it is not possible (including absolute links).3811sub normalize_link_target {3812my($link_target,$basedir) =@_;38133814# absolute symlinks (beginning with '/') cannot be normalized3815return if(substr($link_target,0,1)eq'/');38163817# normalize link target to path from top (root) tree (dir)3818my$path;3819if($basedir) {3820$path=$basedir.'/'.$link_target;3821}else{3822# we are in top (root) tree (dir)3823$path=$link_target;3824}38253826# remove //, /./, and /../3827my@path_parts;3828foreachmy$part(split('/',$path)) {3829# discard '.' and ''3830next if(!$part||$parteq'.');3831# handle '..'3832if($parteq'..') {3833if(@path_parts) {3834pop@path_parts;3835}else{3836# link leads outside repository (outside top dir)3837return;3838}3839}else{3840push@path_parts,$part;3841}3842}3843$path=join('/',@path_parts);38443845return$path;3846}38473848# print tree entry (row of git_tree), but without encompassing <tr> element3849sub git_print_tree_entry {3850my($t,$basedir,$hash_base,$have_blame) =@_;38513852my%base_key= ();3853$base_key{'hash_base'} =$hash_baseifdefined$hash_base;38543855# The format of a table row is: mode list link. Where mode is3856# the mode of the entry, list is the name of the entry, an href,3857# and link is the action links of the entry.38583859print"<td class=\"mode\">". mode_str($t->{'mode'}) ."</td>\n";3860if(exists$t->{'size'}) {3861print"<td class=\"size\">$t->{'size'}</td>\n";3862}3863if($t->{'type'}eq"blob") {3864print"<td class=\"list\">".3865$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3866 file_name=>"$basedir$t->{'name'}",%base_key),3867-class=>"list"}, esc_path($t->{'name'}));3868if(S_ISLNK(oct$t->{'mode'})) {3869my$link_target= git_get_link_target($t->{'hash'});3870if($link_target) {3871my$norm_target= normalize_link_target($link_target,$basedir);3872if(defined$norm_target) {3873print" -> ".3874$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,3875 file_name=>$norm_target),3876-title =>$norm_target}, esc_path($link_target));3877}else{3878print" -> ". esc_path($link_target);3879}3880}3881}3882print"</td>\n";3883print"<td class=\"link\">";3884print$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3885 file_name=>"$basedir$t->{'name'}",%base_key)},3886"blob");3887if($have_blame) {3888print" | ".3889$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},3890 file_name=>"$basedir$t->{'name'}",%base_key)},3891"blame");3892}3893if(defined$hash_base) {3894print" | ".3895$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3896 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},3897"history");3898}3899print" | ".3900$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,3901 file_name=>"$basedir$t->{'name'}")},3902"raw");3903print"</td>\n";39043905}elsif($t->{'type'}eq"tree") {3906print"<td class=\"list\">";3907print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3908 file_name=>"$basedir$t->{'name'}",3909%base_key)},3910 esc_path($t->{'name'}));3911print"</td>\n";3912print"<td class=\"link\">";3913print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3914 file_name=>"$basedir$t->{'name'}",3915%base_key)},3916"tree");3917if(defined$hash_base) {3918print" | ".3919$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3920 file_name=>"$basedir$t->{'name'}")},3921"history");3922}3923print"</td>\n";3924}else{3925# unknown object: we can only present history for it3926# (this includes 'commit' object, i.e. submodule support)3927print"<td class=\"list\">".3928 esc_path($t->{'name'}) .3929"</td>\n";3930print"<td class=\"link\">";3931if(defined$hash_base) {3932print$cgi->a({-href => href(action=>"history",3933 hash_base=>$hash_base,3934 file_name=>"$basedir$t->{'name'}")},3935"history");3936}3937print"</td>\n";3938}3939}39403941## ......................................................................3942## functions printing large fragments of HTML39433944# get pre-image filenames for merge (combined) diff3945sub fill_from_file_info {3946my($diff,@parents) =@_;39473948$diff->{'from_file'} = [ ];3949$diff->{'from_file'}[$diff->{'nparents'} -1] =undef;3950for(my$i=0;$i<$diff->{'nparents'};$i++) {3951if($diff->{'status'}[$i]eq'R'||3952$diff->{'status'}[$i]eq'C') {3953$diff->{'from_file'}[$i] =3954 git_get_path_by_hash($parents[$i],$diff->{'from_id'}[$i]);3955}3956}39573958return$diff;3959}39603961# is current raw difftree line of file deletion3962sub is_deleted {3963my$diffinfo=shift;39643965return$diffinfo->{'to_id'}eq('0' x 40);3966}39673968# does patch correspond to [previous] difftree raw line3969# $diffinfo - hashref of parsed raw diff format3970# $patchinfo - hashref of parsed patch diff format3971# (the same keys as in $diffinfo)3972sub is_patch_split {3973my($diffinfo,$patchinfo) =@_;39743975returndefined$diffinfo&&defined$patchinfo3976&&$diffinfo->{'to_file'}eq$patchinfo->{'to_file'};3977}397839793980sub git_difftree_body {3981my($difftree,$hash,@parents) =@_;3982my($parent) =$parents[0];3983my$have_blame= gitweb_check_feature('blame');3984print"<div class=\"list_head\">\n";3985if($#{$difftree} >10) {3986print(($#{$difftree} +1) ." files changed:\n");3987}3988print"</div>\n";39893990print"<table class=\"".3991(@parents>1?"combined ":"") .3992"diff_tree\">\n";39933994# header only for combined diff in 'commitdiff' view3995my$has_header=@$difftree&&@parents>1&&$actioneq'commitdiff';3996if($has_header) {3997# table header3998print"<thead><tr>\n".3999"<th></th><th></th>\n";# filename, patchN link4000for(my$i=0;$i<@parents;$i++) {4001my$par=$parents[$i];4002print"<th>".4003$cgi->a({-href => href(action=>"commitdiff",4004 hash=>$hash, hash_parent=>$par),4005-title =>'commitdiff to parent number '.4006($i+1) .': '.substr($par,0,7)},4007$i+1) .4008" </th>\n";4009}4010print"</tr></thead>\n<tbody>\n";4011}40124013my$alternate=1;4014my$patchno=0;4015foreachmy$line(@{$difftree}) {4016my$diff= parsed_difftree_line($line);40174018if($alternate) {4019print"<tr class=\"dark\">\n";4020}else{4021print"<tr class=\"light\">\n";4022}4023$alternate^=1;40244025if(exists$diff->{'nparents'}) {# combined diff40264027 fill_from_file_info($diff,@parents)4028unlessexists$diff->{'from_file'};40294030if(!is_deleted($diff)) {4031# file exists in the result (child) commit4032print"<td>".4033$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4034 file_name=>$diff->{'to_file'},4035 hash_base=>$hash),4036-class=>"list"}, esc_path($diff->{'to_file'})) .4037"</td>\n";4038}else{4039print"<td>".4040 esc_path($diff->{'to_file'}) .4041"</td>\n";4042}40434044if($actioneq'commitdiff') {4045# link to patch4046$patchno++;4047print"<td class=\"link\">".4048$cgi->a({-href =>"#patch$patchno"},"patch") .4049" | ".4050"</td>\n";4051}40524053my$has_history=0;4054my$not_deleted=0;4055for(my$i=0;$i<$diff->{'nparents'};$i++) {4056my$hash_parent=$parents[$i];4057my$from_hash=$diff->{'from_id'}[$i];4058my$from_path=$diff->{'from_file'}[$i];4059my$status=$diff->{'status'}[$i];40604061$has_history||= ($statusne'A');4062$not_deleted||= ($statusne'D');40634064if($statuseq'A') {4065print"<td class=\"link\"align=\"right\"> | </td>\n";4066}elsif($statuseq'D') {4067print"<td class=\"link\">".4068$cgi->a({-href => href(action=>"blob",4069 hash_base=>$hash,4070 hash=>$from_hash,4071 file_name=>$from_path)},4072"blob". ($i+1)) .4073" | </td>\n";4074}else{4075if($diff->{'to_id'}eq$from_hash) {4076print"<td class=\"link nochange\">";4077}else{4078print"<td class=\"link\">";4079}4080print$cgi->a({-href => href(action=>"blobdiff",4081 hash=>$diff->{'to_id'},4082 hash_parent=>$from_hash,4083 hash_base=>$hash,4084 hash_parent_base=>$hash_parent,4085 file_name=>$diff->{'to_file'},4086 file_parent=>$from_path)},4087"diff". ($i+1)) .4088" | </td>\n";4089}4090}40914092print"<td class=\"link\">";4093if($not_deleted) {4094print$cgi->a({-href => href(action=>"blob",4095 hash=>$diff->{'to_id'},4096 file_name=>$diff->{'to_file'},4097 hash_base=>$hash)},4098"blob");4099print" | "if($has_history);4100}4101if($has_history) {4102print$cgi->a({-href => href(action=>"history",4103 file_name=>$diff->{'to_file'},4104 hash_base=>$hash)},4105"history");4106}4107print"</td>\n";41084109print"</tr>\n";4110next;# instead of 'else' clause, to avoid extra indent4111}4112# else ordinary diff41134114my($to_mode_oct,$to_mode_str,$to_file_type);4115my($from_mode_oct,$from_mode_str,$from_file_type);4116if($diff->{'to_mode'}ne('0' x 6)) {4117$to_mode_oct=oct$diff->{'to_mode'};4118if(S_ISREG($to_mode_oct)) {# only for regular file4119$to_mode_str=sprintf("%04o",$to_mode_oct&0777);# permission bits4120}4121$to_file_type= file_type($diff->{'to_mode'});4122}4123if($diff->{'from_mode'}ne('0' x 6)) {4124$from_mode_oct=oct$diff->{'from_mode'};4125if(S_ISREG($to_mode_oct)) {# only for regular file4126$from_mode_str=sprintf("%04o",$from_mode_oct&0777);# permission bits4127}4128$from_file_type= file_type($diff->{'from_mode'});4129}41304131if($diff->{'status'}eq"A") {# created4132my$mode_chng="<span class=\"file_status new\">[new$to_file_type";4133$mode_chng.=" with mode:$to_mode_str"if$to_mode_str;4134$mode_chng.="]</span>";4135print"<td>";4136print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4137 hash_base=>$hash, file_name=>$diff->{'file'}),4138-class=>"list"}, esc_path($diff->{'file'}));4139print"</td>\n";4140print"<td>$mode_chng</td>\n";4141print"<td class=\"link\">";4142if($actioneq'commitdiff') {4143# link to patch4144$patchno++;4145print$cgi->a({-href =>"#patch$patchno"},"patch");4146print" | ";4147}4148print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4149 hash_base=>$hash, file_name=>$diff->{'file'})},4150"blob");4151print"</td>\n";41524153}elsif($diff->{'status'}eq"D") {# deleted4154my$mode_chng="<span class=\"file_status deleted\">[deleted$from_file_type]</span>";4155print"<td>";4156print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4157 hash_base=>$parent, file_name=>$diff->{'file'}),4158-class=>"list"}, esc_path($diff->{'file'}));4159print"</td>\n";4160print"<td>$mode_chng</td>\n";4161print"<td class=\"link\">";4162if($actioneq'commitdiff') {4163# link to patch4164$patchno++;4165print$cgi->a({-href =>"#patch$patchno"},"patch");4166print" | ";4167}4168print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4169 hash_base=>$parent, file_name=>$diff->{'file'})},4170"blob") ." | ";4171if($have_blame) {4172print$cgi->a({-href => href(action=>"blame", hash_base=>$parent,4173 file_name=>$diff->{'file'})},4174"blame") ." | ";4175}4176print$cgi->a({-href => href(action=>"history", hash_base=>$parent,4177 file_name=>$diff->{'file'})},4178"history");4179print"</td>\n";41804181}elsif($diff->{'status'}eq"M"||$diff->{'status'}eq"T") {# modified, or type changed4182my$mode_chnge="";4183if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4184$mode_chnge="<span class=\"file_status mode_chnge\">[changed";4185if($from_file_typene$to_file_type) {4186$mode_chnge.=" from$from_file_typeto$to_file_type";4187}4188if(($from_mode_oct&0777) != ($to_mode_oct&0777)) {4189if($from_mode_str&&$to_mode_str) {4190$mode_chnge.=" mode:$from_mode_str->$to_mode_str";4191}elsif($to_mode_str) {4192$mode_chnge.=" mode:$to_mode_str";4193}4194}4195$mode_chnge.="]</span>\n";4196}4197print"<td>";4198print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4199 hash_base=>$hash, file_name=>$diff->{'file'}),4200-class=>"list"}, esc_path($diff->{'file'}));4201print"</td>\n";4202print"<td>$mode_chnge</td>\n";4203print"<td class=\"link\">";4204if($actioneq'commitdiff') {4205# link to patch4206$patchno++;4207print$cgi->a({-href =>"#patch$patchno"},"patch") .4208" | ";4209}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4210# "commit" view and modified file (not onlu mode changed)4211print$cgi->a({-href => href(action=>"blobdiff",4212 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4213 hash_base=>$hash, hash_parent_base=>$parent,4214 file_name=>$diff->{'file'})},4215"diff") .4216" | ";4217}4218print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4219 hash_base=>$hash, file_name=>$diff->{'file'})},4220"blob") ." | ";4221if($have_blame) {4222print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4223 file_name=>$diff->{'file'})},4224"blame") ." | ";4225}4226print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4227 file_name=>$diff->{'file'})},4228"history");4229print"</td>\n";42304231}elsif($diff->{'status'}eq"R"||$diff->{'status'}eq"C") {# renamed or copied4232my%status_name= ('R'=>'moved','C'=>'copied');4233my$nstatus=$status_name{$diff->{'status'}};4234my$mode_chng="";4235if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4236# mode also for directories, so we cannot use $to_mode_str4237$mode_chng=sprintf(", mode:%04o",$to_mode_oct&0777);4238}4239print"<td>".4240$cgi->a({-href => href(action=>"blob", hash_base=>$hash,4241 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),4242-class=>"list"}, esc_path($diff->{'to_file'})) ."</td>\n".4243"<td><span class=\"file_status$nstatus\">[$nstatusfrom ".4244$cgi->a({-href => href(action=>"blob", hash_base=>$parent,4245 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),4246-class=>"list"}, esc_path($diff->{'from_file'})) .4247" with ". (int$diff->{'similarity'}) ."% similarity$mode_chng]</span></td>\n".4248"<td class=\"link\">";4249if($actioneq'commitdiff') {4250# link to patch4251$patchno++;4252print$cgi->a({-href =>"#patch$patchno"},"patch") .4253" | ";4254}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4255# "commit" view and modified file (not only pure rename or copy)4256print$cgi->a({-href => href(action=>"blobdiff",4257 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4258 hash_base=>$hash, hash_parent_base=>$parent,4259 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},4260"diff") .4261" | ";4262}4263print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4264 hash_base=>$parent, file_name=>$diff->{'to_file'})},4265"blob") ." | ";4266if($have_blame) {4267print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4268 file_name=>$diff->{'to_file'})},4269"blame") ." | ";4270}4271print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4272 file_name=>$diff->{'to_file'})},4273"history");4274print"</td>\n";42754276}# we should not encounter Unmerged (U) or Unknown (X) status4277print"</tr>\n";4278}4279print"</tbody>"if$has_header;4280print"</table>\n";4281}42824283sub git_patchset_body {4284my($fd,$difftree,$hash,@hash_parents) =@_;4285my($hash_parent) =$hash_parents[0];42864287my$is_combined= (@hash_parents>1);4288my$patch_idx=0;4289my$patch_number=0;4290my$patch_line;4291my$diffinfo;4292my$to_name;4293my(%from,%to);42944295print"<div class=\"patchset\">\n";42964297# skip to first patch4298while($patch_line= <$fd>) {4299chomp$patch_line;43004301last if($patch_line=~m/^diff /);4302}43034304 PATCH:4305while($patch_line) {43064307# parse "git diff" header line4308if($patch_line=~m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {4309# $1 is from_name, which we do not use4310$to_name= unquote($2);4311$to_name=~s!^b/!!;4312}elsif($patch_line=~m/^diff --(cc|combined) ("?.*"?)$/) {4313# $1 is 'cc' or 'combined', which we do not use4314$to_name= unquote($2);4315}else{4316$to_name=undef;4317}43184319# check if current patch belong to current raw line4320# and parse raw git-diff line if needed4321if(is_patch_split($diffinfo, {'to_file'=>$to_name})) {4322# this is continuation of a split patch4323print"<div class=\"patch cont\">\n";4324}else{4325# advance raw git-diff output if needed4326$patch_idx++ifdefined$diffinfo;43274328# read and prepare patch information4329$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);43304331# compact combined diff output can have some patches skipped4332# find which patch (using pathname of result) we are at now;4333if($is_combined) {4334while($to_namene$diffinfo->{'to_file'}) {4335print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4336 format_diff_cc_simplified($diffinfo,@hash_parents) .4337"</div>\n";# class="patch"43384339$patch_idx++;4340$patch_number++;43414342last if$patch_idx>$#$difftree;4343$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);4344}4345}43464347# modifies %from, %to hashes4348 parse_from_to_diffinfo($diffinfo, \%from, \%to,@hash_parents);43494350# this is first patch for raw difftree line with $patch_idx index4351# we index @$difftree array from 0, but number patches from 14352print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n";4353}43544355# git diff header4356#assert($patch_line =~ m/^diff /) if DEBUG;4357#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed4358$patch_number++;4359# print "git diff" header4360print format_git_diff_header_line($patch_line,$diffinfo,4361 \%from, \%to);43624363# print extended diff header4364print"<div class=\"diff extended_header\">\n";4365 EXTENDED_HEADER:4366while($patch_line= <$fd>) {4367chomp$patch_line;43684369last EXTENDED_HEADER if($patch_line=~m/^--- |^diff /);43704371print format_extended_diff_header_line($patch_line,$diffinfo,4372 \%from, \%to);4373}4374print"</div>\n";# class="diff extended_header"43754376# from-file/to-file diff header4377if(!$patch_line) {4378print"</div>\n";# class="patch"4379last PATCH;4380}4381next PATCH if($patch_line=~m/^diff /);4382#assert($patch_line =~ m/^---/) if DEBUG;43834384my$last_patch_line=$patch_line;4385$patch_line= <$fd>;4386chomp$patch_line;4387#assert($patch_line =~ m/^\+\+\+/) if DEBUG;43884389print format_diff_from_to_header($last_patch_line,$patch_line,4390$diffinfo, \%from, \%to,4391@hash_parents);43924393# the patch itself4394 LINE:4395while($patch_line= <$fd>) {4396chomp$patch_line;43974398next PATCH if($patch_line=~m/^diff /);43994400print format_diff_line($patch_line, \%from, \%to);4401}44024403}continue{4404print"</div>\n";# class="patch"4405}44064407# for compact combined (--cc) format, with chunk and patch simpliciaction4408# patchset might be empty, but there might be unprocessed raw lines4409for(++$patch_idxif$patch_number>0;4410$patch_idx<@$difftree;4411++$patch_idx) {4412# read and prepare patch information4413$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);44144415# generate anchor for "patch" links in difftree / whatchanged part4416print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4417 format_diff_cc_simplified($diffinfo,@hash_parents) .4418"</div>\n";# class="patch"44194420$patch_number++;4421}44224423if($patch_number==0) {4424if(@hash_parents>1) {4425print"<div class=\"diff nodifferences\">Trivial merge</div>\n";4426}else{4427print"<div class=\"diff nodifferences\">No differences found</div>\n";4428}4429}44304431print"</div>\n";# class="patchset"4432}44334434# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .44354436# fills project list info (age, description, owner, forks) for each4437# project in the list, removing invalid projects from returned list4438# NOTE: modifies $projlist, but does not remove entries from it4439sub fill_project_list_info {4440my($projlist,$check_forks) =@_;4441my@projects;44424443my$show_ctags= gitweb_check_feature('ctags');4444 PROJECT:4445foreachmy$pr(@$projlist) {4446my(@activity) = git_get_last_activity($pr->{'path'});4447unless(@activity) {4448next PROJECT;4449}4450($pr->{'age'},$pr->{'age_string'}) =@activity;4451if(!defined$pr->{'descr'}) {4452my$descr= git_get_project_description($pr->{'path'}) ||"";4453$descr= to_utf8($descr);4454$pr->{'descr_long'} =$descr;4455$pr->{'descr'} = chop_str($descr,$projects_list_description_width,5);4456}4457if(!defined$pr->{'owner'}) {4458$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") ||"";4459}4460if($check_forks) {4461my$pname=$pr->{'path'};4462if(($pname=~s/\.git$//) &&4463($pname!~/\/$/) &&4464(-d "$projectroot/$pname")) {4465$pr->{'forks'} ="-d$projectroot/$pname";4466}else{4467$pr->{'forks'} =0;4468}4469}4470$show_ctagsand$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});4471push@projects,$pr;4472}44734474return@projects;4475}44764477# print 'sort by' <th> element, generating 'sort by $name' replay link4478# if that order is not selected4479sub print_sort_th {4480print format_sort_th(@_);4481}44824483sub format_sort_th {4484my($name,$order,$header) =@_;4485my$sort_th="";4486$header||=ucfirst($name);44874488if($ordereq$name) {4489$sort_th.="<th>$header</th>\n";4490}else{4491$sort_th.="<th>".4492$cgi->a({-href => href(-replay=>1, order=>$name),4493-class=>"header"},$header) .4494"</th>\n";4495}44964497return$sort_th;4498}44994500sub git_project_list_body {4501# actually uses global variable $project4502my($projlist,$order,$from,$to,$extra,$no_header) =@_;45034504my$check_forks= gitweb_check_feature('forks');4505my@projects= fill_project_list_info($projlist,$check_forks);45064507$order||=$default_projects_order;4508$from=0unlessdefined$from;4509$to=$#projectsif(!defined$to||$#projects<$to);45104511my%order_info= (4512 project => { key =>'path', type =>'str'},4513 descr => { key =>'descr_long', type =>'str'},4514 owner => { key =>'owner', type =>'str'},4515 age => { key =>'age', type =>'num'}4516);4517my$oi=$order_info{$order};4518if($oi->{'type'}eq'str') {4519@projects=sort{$a->{$oi->{'key'}}cmp$b->{$oi->{'key'}}}@projects;4520}else{4521@projects=sort{$a->{$oi->{'key'}} <=>$b->{$oi->{'key'}}}@projects;4522}45234524my$show_ctags= gitweb_check_feature('ctags');4525if($show_ctags) {4526my%ctags;4527foreachmy$p(@projects) {4528foreachmy$ct(keys%{$p->{'ctags'}}) {4529$ctags{$ct} +=$p->{'ctags'}->{$ct};4530}4531}4532my$cloud= git_populate_project_tagcloud(\%ctags);4533print git_show_project_tagcloud($cloud,64);4534}45354536print"<table class=\"project_list\">\n";4537unless($no_header) {4538print"<tr>\n";4539if($check_forks) {4540print"<th></th>\n";4541}4542 print_sort_th('project',$order,'Project');4543 print_sort_th('descr',$order,'Description');4544 print_sort_th('owner',$order,'Owner');4545 print_sort_th('age',$order,'Last Change');4546print"<th></th>\n".# for links4547"</tr>\n";4548}4549my$alternate=1;4550my$tagfilter=$cgi->param('by_tag');4551for(my$i=$from;$i<=$to;$i++) {4552my$pr=$projects[$i];45534554next if$tagfilterand$show_ctagsand not grep{lc$_eq lc$tagfilter}keys%{$pr->{'ctags'}};4555next if$searchtextand not$pr->{'path'} =~/$searchtext/4556and not$pr->{'descr_long'} =~/$searchtext/;4557# Weed out forks or non-matching entries of search4558if($check_forks) {4559my$forkbase=$project;$forkbase||='';$forkbase=~ s#\.git$#/#;4560$forkbase="^$forkbase"if$forkbase;4561next ifnot$searchtextand not$tagfilterand$show_ctags4562and$pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe4563}45644565if($alternate) {4566print"<tr class=\"dark\">\n";4567}else{4568print"<tr class=\"light\">\n";4569}4570$alternate^=1;4571if($check_forks) {4572print"<td>";4573if($pr->{'forks'}) {4574print"<!--$pr->{'forks'} -->\n";4575print$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"+");4576}4577print"</td>\n";4578}4579print"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4580-class=>"list"}, esc_html($pr->{'path'})) ."</td>\n".4581"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4582-class=>"list", -title =>$pr->{'descr_long'}},4583 esc_html($pr->{'descr'})) ."</td>\n".4584"<td><i>". chop_and_escape_str($pr->{'owner'},15) ."</i></td>\n";4585print"<td class=\"". age_class($pr->{'age'}) ."\">".4586(defined$pr->{'age_string'} ?$pr->{'age_string'} :"No commits") ."</td>\n".4587"<td class=\"link\">".4588$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")},"summary") ." | ".4589$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")},"shortlog") ." | ".4590$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")},"log") ." | ".4591$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")},"tree") .4592($pr->{'forks'} ?" | ".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"forks") :'') .4593"</td>\n".4594"</tr>\n";4595}4596if(defined$extra) {4597print"<tr>\n";4598if($check_forks) {4599print"<td></td>\n";4600}4601print"<td colspan=\"5\">$extra</td>\n".4602"</tr>\n";4603}4604print"</table>\n";4605}46064607sub git_log_body {4608# uses global variable $project4609my($commitlist,$from,$to,$refs,$extra) =@_;46104611$from=0unlessdefined$from;4612$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);46134614for(my$i=0;$i<=$to;$i++) {4615my%co= %{$commitlist->[$i]};4616next if!%co;4617my$commit=$co{'id'};4618my$ref= format_ref_marker($refs,$commit);4619my%ad= parse_date($co{'author_epoch'});4620 git_print_header_div('commit',4621"<span class=\"age\">$co{'age_string'}</span>".4622 esc_html($co{'title'}) .$ref,4623$commit);4624print"<div class=\"title_text\">\n".4625"<div class=\"log_link\">\n".4626$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") .4627" | ".4628$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") .4629" | ".4630$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree") .4631"<br/>\n".4632"</div>\n";4633 git_print_authorship(\%co, -tag =>'span');4634print"<br/>\n</div>\n";46354636print"<div class=\"log_body\">\n";4637 git_print_log($co{'comment'}, -final_empty_line=>1);4638print"</div>\n";4639}4640if($extra) {4641print"<div class=\"page_nav\">\n";4642print"$extra\n";4643print"</div>\n";4644}4645}46464647sub git_shortlog_body {4648# uses global variable $project4649my($commitlist,$from,$to,$refs,$extra) =@_;46504651$from=0unlessdefined$from;4652$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);46534654print"<table class=\"shortlog\">\n";4655my$alternate=1;4656for(my$i=$from;$i<=$to;$i++) {4657my%co= %{$commitlist->[$i]};4658my$commit=$co{'id'};4659my$ref= format_ref_marker($refs,$commit);4660if($alternate) {4661print"<tr class=\"dark\">\n";4662}else{4663print"<tr class=\"light\">\n";4664}4665$alternate^=1;4666# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .4667print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4668 format_author_html('td', \%co,10) ."<td>";4669print format_subject_html($co{'title'},$co{'title_short'},4670 href(action=>"commit", hash=>$commit),$ref);4671print"</td>\n".4672"<td class=\"link\">".4673$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") ." | ".4674$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") ." | ".4675$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree");4676my$snapshot_links= format_snapshot_links($commit);4677if(defined$snapshot_links) {4678print" | ".$snapshot_links;4679}4680print"</td>\n".4681"</tr>\n";4682}4683if(defined$extra) {4684print"<tr>\n".4685"<td colspan=\"4\">$extra</td>\n".4686"</tr>\n";4687}4688print"</table>\n";4689}46904691sub git_history_body {4692# Warning: assumes constant type (blob or tree) during history4693my($commitlist,$from,$to,$refs,$extra,4694$file_name,$file_hash,$ftype) =@_;46954696$from=0unlessdefined$from;4697$to=$#{$commitlist}unless(defined$to&&$to<=$#{$commitlist});46984699print"<table class=\"history\">\n";4700my$alternate=1;4701for(my$i=$from;$i<=$to;$i++) {4702my%co= %{$commitlist->[$i]};4703if(!%co) {4704next;4705}4706my$commit=$co{'id'};47074708my$ref= format_ref_marker($refs,$commit);47094710if($alternate) {4711print"<tr class=\"dark\">\n";4712}else{4713print"<tr class=\"light\">\n";4714}4715$alternate^=1;4716print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4717# shortlog: format_author_html('td', \%co, 10)4718 format_author_html('td', \%co,15,3) ."<td>";4719# originally git_history used chop_str($co{'title'}, 50)4720print format_subject_html($co{'title'},$co{'title_short'},4721 href(action=>"commit", hash=>$commit),$ref);4722print"</td>\n".4723"<td class=\"link\">".4724$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)},$ftype) ." | ".4725$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff");47264727if($ftypeeq'blob') {4728my$blob_current=$file_hash;4729my$blob_parent= git_get_hash_by_path($commit,$file_name);4730if(defined$blob_current&&defined$blob_parent&&4731$blob_currentne$blob_parent) {4732print" | ".4733$cgi->a({-href => href(action=>"blobdiff",4734 hash=>$blob_current, hash_parent=>$blob_parent,4735 hash_base=>$hash_base, hash_parent_base=>$commit,4736 file_name=>$file_name)},4737"diff to current");4738}4739}4740print"</td>\n".4741"</tr>\n";4742}4743if(defined$extra) {4744print"<tr>\n".4745"<td colspan=\"4\">$extra</td>\n".4746"</tr>\n";4747}4748print"</table>\n";4749}47504751sub git_tags_body {4752# uses global variable $project4753my($taglist,$from,$to,$extra) =@_;4754$from=0unlessdefined$from;4755$to=$#{$taglist}if(!defined$to||$#{$taglist} <$to);47564757print"<table class=\"tags\">\n";4758my$alternate=1;4759for(my$i=$from;$i<=$to;$i++) {4760my$entry=$taglist->[$i];4761my%tag=%$entry;4762my$comment=$tag{'subject'};4763my$comment_short;4764if(defined$comment) {4765$comment_short= chop_str($comment,30,5);4766}4767if($alternate) {4768print"<tr class=\"dark\">\n";4769}else{4770print"<tr class=\"light\">\n";4771}4772$alternate^=1;4773if(defined$tag{'age'}) {4774print"<td><i>$tag{'age'}</i></td>\n";4775}else{4776print"<td></td>\n";4777}4778print"<td>".4779$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),4780-class=>"list name"}, esc_html($tag{'name'})) .4781"</td>\n".4782"<td>";4783if(defined$comment) {4784print format_subject_html($comment,$comment_short,4785 href(action=>"tag", hash=>$tag{'id'}));4786}4787print"</td>\n".4788"<td class=\"selflink\">";4789if($tag{'type'}eq"tag") {4790print$cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})},"tag");4791}else{4792print" ";4793}4794print"</td>\n".4795"<td class=\"link\">"." | ".4796$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})},$tag{'reftype'});4797if($tag{'reftype'}eq"commit") {4798print" | ".$cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})},"shortlog") .4799" | ".$cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})},"log");4800}elsif($tag{'reftype'}eq"blob") {4801print" | ".$cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})},"raw");4802}4803print"</td>\n".4804"</tr>";4805}4806if(defined$extra) {4807print"<tr>\n".4808"<td colspan=\"5\">$extra</td>\n".4809"</tr>\n";4810}4811print"</table>\n";4812}48134814sub git_heads_body {4815# uses global variable $project4816my($headlist,$head,$from,$to,$extra) =@_;4817$from=0unlessdefined$from;4818$to=$#{$headlist}if(!defined$to||$#{$headlist} <$to);48194820print"<table class=\"heads\">\n";4821my$alternate=1;4822for(my$i=$from;$i<=$to;$i++) {4823my$entry=$headlist->[$i];4824my%ref=%$entry;4825my$curr=$ref{'id'}eq$head;4826if($alternate) {4827print"<tr class=\"dark\">\n";4828}else{4829print"<tr class=\"light\">\n";4830}4831$alternate^=1;4832print"<td><i>$ref{'age'}</i></td>\n".4833($curr?"<td class=\"current_head\">":"<td>") .4834$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),4835-class=>"list name"},esc_html($ref{'name'})) .4836"</td>\n".4837"<td class=\"link\">".4838$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})},"shortlog") ." | ".4839$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})},"log") ." | ".4840$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})},"tree") .4841"</td>\n".4842"</tr>";4843}4844if(defined$extra) {4845print"<tr>\n".4846"<td colspan=\"3\">$extra</td>\n".4847"</tr>\n";4848}4849print"</table>\n";4850}48514852sub git_search_grep_body {4853my($commitlist,$from,$to,$extra) =@_;4854$from=0unlessdefined$from;4855$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);48564857print"<table class=\"commit_search\">\n";4858my$alternate=1;4859for(my$i=$from;$i<=$to;$i++) {4860my%co= %{$commitlist->[$i]};4861if(!%co) {4862next;4863}4864my$commit=$co{'id'};4865if($alternate) {4866print"<tr class=\"dark\">\n";4867}else{4868print"<tr class=\"light\">\n";4869}4870$alternate^=1;4871print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4872 format_author_html('td', \%co,15,5) .4873"<td>".4874$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),4875-class=>"list subject"},4876 chop_and_escape_str($co{'title'},50) ."<br/>");4877my$comment=$co{'comment'};4878foreachmy$line(@$comment) {4879if($line=~m/^(.*?)($search_regexp)(.*)$/i) {4880my($lead,$match,$trail) = ($1,$2,$3);4881$match= chop_str($match,70,5,'center');4882my$contextlen=int((80-length($match))/2);4883$contextlen=30if($contextlen>30);4884$lead= chop_str($lead,$contextlen,10,'left');4885$trail= chop_str($trail,$contextlen,10,'right');48864887$lead= esc_html($lead);4888$match= esc_html($match);4889$trail= esc_html($trail);48904891print"$lead<span class=\"match\">$match</span>$trail<br />";4892}4893}4894print"</td>\n".4895"<td class=\"link\">".4896$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .4897" | ".4898$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})},"commitdiff") .4899" | ".4900$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");4901print"</td>\n".4902"</tr>\n";4903}4904if(defined$extra) {4905print"<tr>\n".4906"<td colspan=\"3\">$extra</td>\n".4907"</tr>\n";4908}4909print"</table>\n";4910}49114912## ======================================================================4913## ======================================================================4914## actions49154916sub git_project_list {4917my$order=$input_params{'order'};4918if(defined$order&&$order!~m/none|project|descr|owner|age/) {4919 die_error(400,"Unknown order parameter");4920}49214922my@list= git_get_projects_list();4923if(!@list) {4924 die_error(404,"No projects found");4925}49264927 git_header_html();4928if(defined$home_text&& -f $home_text) {4929print"<div class=\"index_include\">\n";4930 insert_file($home_text);4931print"</div>\n";4932}4933print$cgi->startform(-method=>"get") .4934"<p class=\"projsearch\">Search:\n".4935$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".4936"</p>".4937$cgi->end_form() ."\n";4938 git_project_list_body(\@list,$order);4939 git_footer_html();4940}49414942sub git_forks {4943my$order=$input_params{'order'};4944if(defined$order&&$order!~m/none|project|descr|owner|age/) {4945 die_error(400,"Unknown order parameter");4946}49474948my@list= git_get_projects_list($project);4949if(!@list) {4950 die_error(404,"No forks found");4951}49524953 git_header_html();4954 git_print_page_nav('','');4955 git_print_header_div('summary',"$projectforks");4956 git_project_list_body(\@list,$order);4957 git_footer_html();4958}49594960sub git_project_index {4961my@projects= git_get_projects_list($project);49624963print$cgi->header(4964-type =>'text/plain',4965-charset =>'utf-8',4966-content_disposition =>'inline; filename="index.aux"');49674968foreachmy$pr(@projects) {4969if(!exists$pr->{'owner'}) {4970$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");4971}49724973my($path,$owner) = ($pr->{'path'},$pr->{'owner'});4974# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '4975$path=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4976$owner=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4977$path=~s/ /\+/g;4978$owner=~s/ /\+/g;49794980print"$path$owner\n";4981}4982}49834984sub git_summary {4985my$descr= git_get_project_description($project) ||"none";4986my%co= parse_commit("HEAD");4987my%cd=%co? parse_date($co{'committer_epoch'},$co{'committer_tz'}) : ();4988my$head=$co{'id'};49894990my$owner= git_get_project_owner($project);49914992my$refs= git_get_references();4993# These get_*_list functions return one more to allow us to see if4994# there are more ...4995my@taglist= git_get_tags_list(16);4996my@headlist= git_get_heads_list(16);4997my@forklist;4998my$check_forks= gitweb_check_feature('forks');49995000if($check_forks) {5001@forklist= git_get_projects_list($project);5002}50035004 git_header_html();5005 git_print_page_nav('summary','',$head);50065007print"<div class=\"title\"> </div>\n";5008print"<table class=\"projects_list\">\n".5009"<tr id=\"metadata_desc\"><td>description</td><td>". esc_html($descr) ."</td></tr>\n".5010"<tr id=\"metadata_owner\"><td>owner</td><td>". esc_html($owner) ."</td></tr>\n";5011if(defined$cd{'rfc2822'}) {5012print"<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";5013}50145015# use per project git URL list in $projectroot/$project/cloneurl5016# or make project git URL from git base URL and project name5017my$url_tag="URL";5018my@url_list= git_get_project_url_list($project);5019@url_list=map{"$_/$project"}@git_base_url_listunless@url_list;5020foreachmy$git_url(@url_list) {5021next unless$git_url;5022print"<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";5023$url_tag="";5024}50255026# Tag cloud5027my$show_ctags= gitweb_check_feature('ctags');5028if($show_ctags) {5029my$ctags= git_get_project_ctags($project);5030my$cloud= git_populate_project_tagcloud($ctags);5031print"<tr id=\"metadata_ctags\"><td>Content tags:<br />";5032print"</td>\n<td>"unless%$ctags;5033print"<form action=\"$show_ctags\"method=\"post\"><input type=\"hidden\"name=\"p\"value=\"$project\"/>Add: <input type=\"text\"name=\"t\"size=\"8\"/></form>";5034print"</td>\n<td>"if%$ctags;5035print git_show_project_tagcloud($cloud,48);5036print"</td></tr>";5037}50385039print"</table>\n";50405041# If XSS prevention is on, we don't include README.html.5042# TODO: Allow a readme in some safe format.5043if(!$prevent_xss&& -s "$projectroot/$project/README.html") {5044print"<div class=\"title\">readme</div>\n".5045"<div class=\"readme\">\n";5046 insert_file("$projectroot/$project/README.html");5047print"\n</div>\n";# class="readme"5048}50495050# we need to request one more than 16 (0..15) to check if5051# those 16 are all5052my@commitlist=$head? parse_commits($head,17) : ();5053if(@commitlist) {5054 git_print_header_div('shortlog');5055 git_shortlog_body(\@commitlist,0,15,$refs,5056$#commitlist<=15?undef:5057$cgi->a({-href => href(action=>"shortlog")},"..."));5058}50595060if(@taglist) {5061 git_print_header_div('tags');5062 git_tags_body(\@taglist,0,15,5063$#taglist<=15?undef:5064$cgi->a({-href => href(action=>"tags")},"..."));5065}50665067if(@headlist) {5068 git_print_header_div('heads');5069 git_heads_body(\@headlist,$head,0,15,5070$#headlist<=15?undef:5071$cgi->a({-href => href(action=>"heads")},"..."));5072}50735074if(@forklist) {5075 git_print_header_div('forks');5076 git_project_list_body(\@forklist,'age',0,15,5077$#forklist<=15?undef:5078$cgi->a({-href => href(action=>"forks")},"..."),5079'no_header');5080}50815082 git_footer_html();5083}50845085sub git_tag {5086my$head= git_get_head_hash($project);5087 git_header_html();5088 git_print_page_nav('','',$head,undef,$head);5089my%tag= parse_tag($hash);50905091if(!%tag) {5092 die_error(404,"Unknown tag object");5093}50945095 git_print_header_div('commit', esc_html($tag{'name'}),$hash);5096print"<div class=\"title_text\">\n".5097"<table class=\"object_header\">\n".5098"<tr>\n".5099"<td>object</td>\n".5100"<td>".$cgi->a({-class=>"list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5101$tag{'object'}) ."</td>\n".5102"<td class=\"link\">".$cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5103$tag{'type'}) ."</td>\n".5104"</tr>\n";5105if(defined($tag{'author'})) {5106 git_print_authorship_rows(\%tag,'author');5107}5108print"</table>\n\n".5109"</div>\n";5110print"<div class=\"page_body\">";5111my$comment=$tag{'comment'};5112foreachmy$line(@$comment) {5113chomp$line;5114print esc_html($line, -nbsp=>1) ."<br/>\n";5115}5116print"</div>\n";5117 git_footer_html();5118}51195120sub git_blame_common {5121my$format=shift||'porcelain';5122if($formateq'porcelain'&&$cgi->param('js')) {5123$format='incremental';5124$action='blame_incremental';# for page title etc5125}51265127# permissions5128 gitweb_check_feature('blame')5129or die_error(403,"Blame view not allowed");51305131# error checking5132 die_error(400,"No file name given")unless$file_name;5133$hash_base||= git_get_head_hash($project);5134 die_error(404,"Couldn't find base commit")unless$hash_base;5135my%co= parse_commit($hash_base)5136or die_error(404,"Commit not found");5137my$ftype="blob";5138if(!defined$hash) {5139$hash= git_get_hash_by_path($hash_base,$file_name,"blob")5140or die_error(404,"Error looking up file");5141}else{5142$ftype= git_get_type($hash);5143if($ftype!~"blob") {5144 die_error(400,"Object is not a blob");5145}5146}51475148my$fd;5149if($formateq'incremental') {5150# get file contents (as base)5151open$fd,"-|", git_cmd(),'cat-file','blob',$hash5152or die_error(500,"Open git-cat-file failed");5153}elsif($formateq'data') {5154# run git-blame --incremental5155open$fd,"-|", git_cmd(),"blame","--incremental",5156$hash_base,"--",$file_name5157or die_error(500,"Open git-blame --incremental failed");5158}else{5159# run git-blame --porcelain5160open$fd,"-|", git_cmd(),"blame",'-p',5161$hash_base,'--',$file_name5162or die_error(500,"Open git-blame --porcelain failed");5163}51645165# incremental blame data returns early5166if($formateq'data') {5167print$cgi->header(5168-type=>"text/plain", -charset =>"utf-8",5169-status=>"200 OK");5170local$| =1;# output autoflush5171printwhile<$fd>;5172close$fd5173or print"ERROR$!\n";51745175print'END';5176if(defined$t0&& gitweb_check_feature('timed')) {5177print' '.5178 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).5179' '.$number_of_git_cmds;5180}5181print"\n";51825183return;5184}51855186# page header5187 git_header_html();5188my$formats_nav=5189$cgi->a({-href => href(action=>"blob", -replay=>1)},5190"blob") .5191" | ";5192if($formateq'incremental') {5193$formats_nav.=5194$cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},5195"blame") ." (non-incremental)";5196}else{5197$formats_nav.=5198$cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},5199"blame") ." (incremental)";5200}5201$formats_nav.=5202" | ".5203$cgi->a({-href => href(action=>"history", -replay=>1)},5204"history") .5205" | ".5206$cgi->a({-href => href(action=>$action, file_name=>$file_name)},5207"HEAD");5208 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5209 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5210 git_print_page_path($file_name,$ftype,$hash_base);52115212# page body5213if($formateq'incremental') {5214print"<noscript>\n<div class=\"error\"><center><b>\n".5215"This page requires JavaScript to run.\nUse ".5216$cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},5217'this page').5218" instead.\n".5219"</b></center></div>\n</noscript>\n";52205221print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;5222}52235224print qq!<div class="page_body">\n!;5225print qq!<div id="progress_info">.../ ...</div>\n!5226if($formateq'incremental');5227print qq!<table id="blame_table"class="blame" width="100%">\n!.5228#qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.5229 qq!<thead>\n!.5230 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.5231 qq!</thead>\n!.5232 qq!<tbody>\n!;52335234my@rev_color=qw(light dark);5235my$num_colors=scalar(@rev_color);5236my$current_color=0;52375238if($formateq'incremental') {5239my$color_class=$rev_color[$current_color];52405241#contents of a file5242my$linenr=0;5243 LINE:5244while(my$line= <$fd>) {5245chomp$line;5246$linenr++;52475248print qq!<tr id="l$linenr"class="$color_class">!.5249 qq!<td class="sha1"><a href=""> </a></td>!.5250 qq!<td class="linenr">!.5251 qq!<a class="linenr" href="">$linenr</a></td>!;5252print qq!<td class="pre">! . esc_html($line) ."</td>\n";5253print qq!</tr>\n!;5254}52555256}else{# porcelain, i.e. ordinary blame5257my%metainfo= ();# saves information about commits52585259# blame data5260 LINE:5261while(my$line= <$fd>) {5262chomp$line;5263# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]5264# no <lines in group> for subsequent lines in group of lines5265my($full_rev,$orig_lineno,$lineno,$group_size) =5266($line=~/^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);5267if(!exists$metainfo{$full_rev}) {5268$metainfo{$full_rev} = {'nprevious'=>0};5269}5270my$meta=$metainfo{$full_rev};5271my$data;5272while($data= <$fd>) {5273chomp$data;5274last if($data=~s/^\t//);# contents of line5275if($data=~/^(\S+)(?: (.*))?$/) {5276$meta->{$1} =$2unlessexists$meta->{$1};5277}5278if($data=~/^previous /) {5279$meta->{'nprevious'}++;5280}5281}5282my$short_rev=substr($full_rev,0,8);5283my$author=$meta->{'author'};5284my%date=5285 parse_date($meta->{'author-time'},$meta->{'author-tz'});5286my$date=$date{'iso-tz'};5287if($group_size) {5288$current_color= ($current_color+1) %$num_colors;5289}5290my$tr_class=$rev_color[$current_color];5291$tr_class.=' boundary'if(exists$meta->{'boundary'});5292$tr_class.=' no-previous'if($meta->{'nprevious'} ==0);5293$tr_class.=' multiple-previous'if($meta->{'nprevious'} >1);5294print"<tr id=\"l$lineno\"class=\"$tr_class\">\n";5295if($group_size) {5296print"<td class=\"sha1\"";5297print" title=\"". esc_html($author) .",$date\"";5298print" rowspan=\"$group_size\""if($group_size>1);5299print">";5300print$cgi->a({-href => href(action=>"commit",5301 hash=>$full_rev,5302 file_name=>$file_name)},5303 esc_html($short_rev));5304if($group_size>=2) {5305my@author_initials= ($author=~/\b([[:upper:]])\B/g);5306if(@author_initials) {5307print"<br />".5308 esc_html(join('',@author_initials));5309# or join('.', ...)5310}5311}5312print"</td>\n";5313}5314# 'previous' <sha1 of parent commit> <filename at commit>5315if(exists$meta->{'previous'} &&5316$meta->{'previous'} =~/^([a-fA-F0-9]{40}) (.*)$/) {5317$meta->{'parent'} =$1;5318$meta->{'file_parent'} = unquote($2);5319}5320my$linenr_commit=5321exists($meta->{'parent'}) ?5322$meta->{'parent'} :$full_rev;5323my$linenr_filename=5324exists($meta->{'file_parent'}) ?5325$meta->{'file_parent'} : unquote($meta->{'filename'});5326my$blamed= href(action =>'blame',5327 file_name =>$linenr_filename,5328 hash_base =>$linenr_commit);5329print"<td class=\"linenr\">";5330print$cgi->a({ -href =>"$blamed#l$orig_lineno",5331-class=>"linenr"},5332 esc_html($lineno));5333print"</td>";5334print"<td class=\"pre\">". esc_html($data) ."</td>\n";5335print"</tr>\n";5336}# end while53375338}53395340# footer5341print"</tbody>\n".5342"</table>\n";# class="blame"5343print"</div>\n";# class="blame_body"5344close$fd5345or print"Reading blob failed\n";53465347 git_footer_html();5348}53495350sub git_blame {5351 git_blame_common();5352}53535354sub git_blame_incremental {5355 git_blame_common('incremental');5356}53575358sub git_blame_data {5359 git_blame_common('data');5360}53615362sub git_tags {5363my$head= git_get_head_hash($project);5364 git_header_html();5365 git_print_page_nav('','',$head,undef,$head);5366 git_print_header_div('summary',$project);53675368my@tagslist= git_get_tags_list();5369if(@tagslist) {5370 git_tags_body(\@tagslist);5371}5372 git_footer_html();5373}53745375sub git_heads {5376my$head= git_get_head_hash($project);5377 git_header_html();5378 git_print_page_nav('','',$head,undef,$head);5379 git_print_header_div('summary',$project);53805381my@headslist= git_get_heads_list();5382if(@headslist) {5383 git_heads_body(\@headslist,$head);5384}5385 git_footer_html();5386}53875388sub git_blob_plain {5389my$type=shift;5390my$expires;53915392if(!defined$hash) {5393if(defined$file_name) {5394my$base=$hash_base|| git_get_head_hash($project);5395$hash= git_get_hash_by_path($base,$file_name,"blob")5396or die_error(404,"Cannot find file");5397}else{5398 die_error(400,"No file name defined");5399}5400}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5401# blobs defined by non-textual hash id's can be cached5402$expires="+1d";5403}54045405open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5406or die_error(500,"Open git-cat-file blob '$hash' failed");54075408# content-type (can include charset)5409$type= blob_contenttype($fd,$file_name,$type);54105411# "save as" filename, even when no $file_name is given5412my$save_as="$hash";5413if(defined$file_name) {5414$save_as=$file_name;5415}elsif($type=~m/^text\//) {5416$save_as.='.txt';5417}54185419# With XSS prevention on, blobs of all types except a few known safe5420# ones are served with "Content-Disposition: attachment" to make sure5421# they don't run in our security domain. For certain image types,5422# blob view writes an <img> tag referring to blob_plain view, and we5423# want to be sure not to break that by serving the image as an5424# attachment (though Firefox 3 doesn't seem to care).5425my$sandbox=$prevent_xss&&5426$type!~m!^(?:text/plain|image/(?:gif|png|jpeg))$!;54275428print$cgi->header(5429-type =>$type,5430-expires =>$expires,5431-content_disposition =>5432($sandbox?'attachment':'inline')5433.'; filename="'.$save_as.'"');5434local$/=undef;5435binmode STDOUT,':raw';5436print<$fd>;5437binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5438close$fd;5439}54405441sub git_blob {5442my$expires;54435444if(!defined$hash) {5445if(defined$file_name) {5446my$base=$hash_base|| git_get_head_hash($project);5447$hash= git_get_hash_by_path($base,$file_name,"blob")5448or die_error(404,"Cannot find file");5449}else{5450 die_error(400,"No file name defined");5451}5452}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5453# blobs defined by non-textual hash id's can be cached5454$expires="+1d";5455}54565457my$have_blame= gitweb_check_feature('blame');5458open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5459or die_error(500,"Couldn't cat$file_name,$hash");5460my$mimetype= blob_mimetype($fd,$file_name);5461if($mimetype!~m!^(?:text/|image/(?:gif|png|jpeg)$)!&& -B $fd) {5462close$fd;5463return git_blob_plain($mimetype);5464}5465# we can have blame only for text/* mimetype5466$have_blame&&= ($mimetype=~m!^text/!);54675468 git_header_html(undef,$expires);5469my$formats_nav='';5470if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5471if(defined$file_name) {5472if($have_blame) {5473$formats_nav.=5474$cgi->a({-href => href(action=>"blame", -replay=>1)},5475"blame") .5476" | ";5477}5478$formats_nav.=5479$cgi->a({-href => href(action=>"history", -replay=>1)},5480"history") .5481" | ".5482$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5483"raw") .5484" | ".5485$cgi->a({-href => href(action=>"blob",5486 hash_base=>"HEAD", file_name=>$file_name)},5487"HEAD");5488}else{5489$formats_nav.=5490$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5491"raw");5492}5493 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5494 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5495}else{5496print"<div class=\"page_nav\">\n".5497"<br/><br/></div>\n".5498"<div class=\"title\">$hash</div>\n";5499}5500 git_print_page_path($file_name,"blob",$hash_base);5501print"<div class=\"page_body\">\n";5502if($mimetype=~m!^image/!) {5503print qq!<img type="$mimetype"!;5504if($file_name) {5505print qq! alt="$file_name" title="$file_name"!;5506}5507print qq! src="! .5508 href(action=>"blob_plain", hash=>$hash,5509 hash_base=>$hash_base, file_name=>$file_name) .5510 qq!"/>\n!;5511}else{5512my$nr;5513while(my$line= <$fd>) {5514chomp$line;5515$nr++;5516$line= untabify($line);5517printf"<div class=\"pre\"><a id=\"l%i\"href=\"". href(-replay =>1)5518."#l%i\"class=\"linenr\">%4i</a>%s</div>\n",5519$nr,$nr,$nr, esc_html($line, -nbsp=>1);5520}5521}5522close$fd5523or print"Reading blob failed.\n";5524print"</div>";5525 git_footer_html();5526}55275528sub git_tree {5529if(!defined$hash_base) {5530$hash_base="HEAD";5531}5532if(!defined$hash) {5533if(defined$file_name) {5534$hash= git_get_hash_by_path($hash_base,$file_name,"tree");5535}else{5536$hash=$hash_base;5537}5538}5539 die_error(404,"No such tree")unlessdefined($hash);55405541my$show_sizes= gitweb_check_feature('show-sizes');5542my$have_blame= gitweb_check_feature('blame');55435544my@entries= ();5545{5546local$/="\0";5547open my$fd,"-|", git_cmd(),"ls-tree",'-z',5548($show_sizes?'-l': ()),@extra_options,$hash5549or die_error(500,"Open git-ls-tree failed");5550@entries=map{chomp;$_} <$fd>;5551close$fd5552or die_error(404,"Reading tree failed");5553}55545555my$refs= git_get_references();5556my$ref= format_ref_marker($refs,$hash_base);5557 git_header_html();5558my$basedir='';5559if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5560my@views_nav= ();5561if(defined$file_name) {5562push@views_nav,5563$cgi->a({-href => href(action=>"history", -replay=>1)},5564"history"),5565$cgi->a({-href => href(action=>"tree",5566 hash_base=>"HEAD", file_name=>$file_name)},5567"HEAD"),5568}5569my$snapshot_links= format_snapshot_links($hash);5570if(defined$snapshot_links) {5571# FIXME: Should be available when we have no hash base as well.5572push@views_nav,$snapshot_links;5573}5574 git_print_page_nav('tree','',$hash_base,undef,undef,5575join(' | ',@views_nav));5576 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash_base);5577}else{5578undef$hash_base;5579print"<div class=\"page_nav\">\n";5580print"<br/><br/></div>\n";5581print"<div class=\"title\">$hash</div>\n";5582}5583if(defined$file_name) {5584$basedir=$file_name;5585if($basedirne''&&substr($basedir, -1)ne'/') {5586$basedir.='/';5587}5588 git_print_page_path($file_name,'tree',$hash_base);5589}5590print"<div class=\"page_body\">\n";5591print"<table class=\"tree\">\n";5592my$alternate=1;5593# '..' (top directory) link if possible5594if(defined$hash_base&&5595defined$file_name&&$file_name=~m![^/]+$!) {5596if($alternate) {5597print"<tr class=\"dark\">\n";5598}else{5599print"<tr class=\"light\">\n";5600}5601$alternate^=1;56025603my$up=$file_name;5604$up=~s!/?[^/]+$!!;5605undef$upunless$up;5606# based on git_print_tree_entry5607print'<td class="mode">'. mode_str('040000') ."</td>\n";5608print'<td class="size"> </td>'."\n"if$show_sizes;5609print'<td class="list">';5610print$cgi->a({-href => href(action=>"tree",5611 hash_base=>$hash_base,5612 file_name=>$up)},5613"..");5614print"</td>\n";5615print"<td class=\"link\"></td>\n";56165617print"</tr>\n";5618}5619foreachmy$line(@entries) {5620my%t= parse_ls_tree_line($line, -z =>1, -l =>$show_sizes);56215622if($alternate) {5623print"<tr class=\"dark\">\n";5624}else{5625print"<tr class=\"light\">\n";5626}5627$alternate^=1;56285629 git_print_tree_entry(\%t,$basedir,$hash_base,$have_blame);56305631print"</tr>\n";5632}5633print"</table>\n".5634"</div>";5635 git_footer_html();5636}56375638sub snapshot_name {5639my($project,$hash) =@_;56405641# path/to/project.git -> project5642# path/to/project/.git -> project5643my$name= to_utf8($project);5644$name=~ s,([^/])/*\.git$,$1,;5645$name= basename($name);5646# sanitize name5647$name=~s/[[:cntrl:]]/?/g;56485649my$ver=$hash;5650if($hash=~/^[0-9a-fA-F]+$/) {5651# shorten SHA-1 hash5652my$full_hash= git_get_full_hash($project,$hash);5653if($full_hash=~/^$hash/&&length($hash) >7) {5654$ver= git_get_short_hash($project,$hash);5655}5656}elsif($hash=~m!^refs/tags/(.*)$!) {5657# tags don't need shortened SHA-1 hash5658$ver=$1;5659}else{5660# branches and other need shortened SHA-1 hash5661if($hash=~m!^refs/(?:heads|remotes)/(.*)$!) {5662$ver=$1;5663}5664$ver.='-'. git_get_short_hash($project,$hash);5665}5666# in case of hierarchical branch names5667$ver=~s!/!.!g;56685669# name = project-version_string5670$name="$name-$ver";56715672returnwantarray? ($name,$name) :$name;5673}56745675sub git_snapshot {5676my$format=$input_params{'snapshot_format'};5677if(!@snapshot_fmts) {5678 die_error(403,"Snapshots not allowed");5679}5680# default to first supported snapshot format5681$format||=$snapshot_fmts[0];5682if($format!~m/^[a-z0-9]+$/) {5683 die_error(400,"Invalid snapshot format parameter");5684}elsif(!exists($known_snapshot_formats{$format})) {5685 die_error(400,"Unknown snapshot format");5686}elsif($known_snapshot_formats{$format}{'disabled'}) {5687 die_error(403,"Snapshot format not allowed");5688}elsif(!grep($_eq$format,@snapshot_fmts)) {5689 die_error(403,"Unsupported snapshot format");5690}56915692my$type= git_get_type("$hash^{}");5693if(!$type) {5694 die_error(404,'Object does not exist');5695}elsif($typeeq'blob') {5696 die_error(400,'Object is not a tree-ish');5697}56985699my($name,$prefix) = snapshot_name($project,$hash);5700my$filename="$name$known_snapshot_formats{$format}{'suffix'}";5701my$cmd= quote_command(5702 git_cmd(),'archive',5703"--format=$known_snapshot_formats{$format}{'format'}",5704"--prefix=$prefix/",$hash);5705if(exists$known_snapshot_formats{$format}{'compressor'}) {5706$cmd.=' | '. quote_command(@{$known_snapshot_formats{$format}{'compressor'}});5707}57085709$filename=~s/(["\\])/\\$1/g;5710print$cgi->header(5711-type =>$known_snapshot_formats{$format}{'type'},5712-content_disposition =>'inline; filename="'.$filename.'"',5713-status =>'200 OK');57145715open my$fd,"-|",$cmd5716or die_error(500,"Execute git-archive failed");5717binmode STDOUT,':raw';5718print<$fd>;5719binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5720close$fd;5721}57225723sub git_log_generic {5724my($fmt_name,$body_subr,$base,$parent,$file_name,$file_hash) =@_;57255726my$head= git_get_head_hash($project);5727if(!defined$base) {5728$base=$head;5729}5730if(!defined$page) {5731$page=0;5732}5733my$refs= git_get_references();57345735my$commit_hash=$base;5736if(defined$parent) {5737$commit_hash="$parent..$base";5738}5739my@commitlist=5740 parse_commits($commit_hash,101, (100*$page),5741defined$file_name? ($file_name,"--full-history") : ());57425743my$ftype;5744if(!defined$file_hash&&defined$file_name) {5745# some commits could have deleted file in question,5746# and not have it in tree, but one of them has to have it5747for(my$i=0;$i<@commitlist;$i++) {5748$file_hash= git_get_hash_by_path($commitlist[$i]{'id'},$file_name);5749last ifdefined$file_hash;5750}5751}5752if(defined$file_hash) {5753$ftype= git_get_type($file_hash);5754}5755if(defined$file_name&& !defined$ftype) {5756 die_error(500,"Unknown type of object");5757}5758my%co;5759if(defined$file_name) {5760%co= parse_commit($base)5761or die_error(404,"Unknown commit object");5762}576357645765my$paging_nav= format_paging_nav($fmt_name,$page,$#commitlist>=100);5766my$next_link='';5767if($#commitlist>=100) {5768$next_link=5769$cgi->a({-href => href(-replay=>1, page=>$page+1),5770-accesskey =>"n", -title =>"Alt-n"},"next");5771}5772my$patch_max= gitweb_get_feature('patches');5773if($patch_max&& !defined$file_name) {5774if($patch_max<0||@commitlist<=$patch_max) {5775$paging_nav.=" ⋅ ".5776$cgi->a({-href => href(action=>"patches", -replay=>1)},5777"patches");5778}5779}57805781 git_header_html();5782 git_print_page_nav($fmt_name,'',$hash,$hash,$hash,$paging_nav);5783if(defined$file_name) {5784 git_print_header_div('commit', esc_html($co{'title'}),$base);5785}else{5786 git_print_header_div('summary',$project)5787}5788 git_print_page_path($file_name,$ftype,$hash_base)5789if(defined$file_name);57905791$body_subr->(\@commitlist,0,99,$refs,$next_link,5792$file_name,$file_hash,$ftype);57935794 git_footer_html();5795}57965797sub git_log {5798 git_log_generic('log', \&git_log_body,5799$hash,$hash_parent);5800}58015802sub git_commit {5803$hash||=$hash_base||"HEAD";5804my%co= parse_commit($hash)5805or die_error(404,"Unknown commit object");58065807my$parent=$co{'parent'};5808my$parents=$co{'parents'};# listref58095810# we need to prepare $formats_nav before any parameter munging5811my$formats_nav;5812if(!defined$parent) {5813# --root commitdiff5814$formats_nav.='(initial)';5815}elsif(@$parents==1) {5816# single parent commit5817$formats_nav.=5818'(parent: '.5819$cgi->a({-href => href(action=>"commit",5820 hash=>$parent)},5821 esc_html(substr($parent,0,7))) .5822')';5823}else{5824# merge commit5825$formats_nav.=5826'(merge: '.5827join(' ',map{5828$cgi->a({-href => href(action=>"commit",5829 hash=>$_)},5830 esc_html(substr($_,0,7)));5831}@$parents) .5832')';5833}5834if(gitweb_check_feature('patches') &&@$parents<=1) {5835$formats_nav.=" | ".5836$cgi->a({-href => href(action=>"patch", -replay=>1)},5837"patch");5838}58395840if(!defined$parent) {5841$parent="--root";5842}5843my@difftree;5844open my$fd,"-|", git_cmd(),"diff-tree",'-r',"--no-commit-id",5845@diff_opts,5846(@$parents<=1?$parent:'-c'),5847$hash,"--"5848or die_error(500,"Open git-diff-tree failed");5849@difftree=map{chomp;$_} <$fd>;5850close$fdor die_error(404,"Reading git-diff-tree failed");58515852# non-textual hash id's can be cached5853my$expires;5854if($hash=~m/^[0-9a-fA-F]{40}$/) {5855$expires="+1d";5856}5857my$refs= git_get_references();5858my$ref= format_ref_marker($refs,$co{'id'});58595860 git_header_html(undef,$expires);5861 git_print_page_nav('commit','',5862$hash,$co{'tree'},$hash,5863$formats_nav);58645865if(defined$co{'parent'}) {5866 git_print_header_div('commitdiff', esc_html($co{'title'}) .$ref,$hash);5867}else{5868 git_print_header_div('tree', esc_html($co{'title'}) .$ref,$co{'tree'},$hash);5869}5870print"<div class=\"title_text\">\n".5871"<table class=\"object_header\">\n";5872 git_print_authorship_rows(\%co);5873print"<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";5874print"<tr>".5875"<td>tree</td>".5876"<td class=\"sha1\">".5877$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),5878class=>"list"},$co{'tree'}) .5879"</td>".5880"<td class=\"link\">".5881$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},5882"tree");5883my$snapshot_links= format_snapshot_links($hash);5884if(defined$snapshot_links) {5885print" | ".$snapshot_links;5886}5887print"</td>".5888"</tr>\n";58895890foreachmy$par(@$parents) {5891print"<tr>".5892"<td>parent</td>".5893"<td class=\"sha1\">".5894$cgi->a({-href => href(action=>"commit", hash=>$par),5895class=>"list"},$par) .5896"</td>".5897"<td class=\"link\">".5898$cgi->a({-href => href(action=>"commit", hash=>$par)},"commit") .5899" | ".5900$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)},"diff") .5901"</td>".5902"</tr>\n";5903}5904print"</table>".5905"</div>\n";59065907print"<div class=\"page_body\">\n";5908 git_print_log($co{'comment'});5909print"</div>\n";59105911 git_difftree_body(\@difftree,$hash,@$parents);59125913 git_footer_html();5914}59155916sub git_object {5917# object is defined by:5918# - hash or hash_base alone5919# - hash_base and file_name5920my$type;59215922# - hash or hash_base alone5923if($hash|| ($hash_base&& !defined$file_name)) {5924my$object_id=$hash||$hash_base;59255926open my$fd,"-|", quote_command(5927 git_cmd(),'cat-file','-t',$object_id) .' 2> /dev/null'5928or die_error(404,"Object does not exist");5929$type= <$fd>;5930chomp$type;5931close$fd5932or die_error(404,"Object does not exist");59335934# - hash_base and file_name5935}elsif($hash_base&&defined$file_name) {5936$file_name=~ s,/+$,,;59375938system(git_cmd(),"cat-file",'-e',$hash_base) ==05939or die_error(404,"Base object does not exist");59405941# here errors should not hapen5942open my$fd,"-|", git_cmd(),"ls-tree",$hash_base,"--",$file_name5943or die_error(500,"Open git-ls-tree failed");5944my$line= <$fd>;5945close$fd;59465947#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'5948unless($line&&$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {5949 die_error(404,"File or directory for given base does not exist");5950}5951$type=$2;5952$hash=$3;5953}else{5954 die_error(400,"Not enough information to find object");5955}59565957print$cgi->redirect(-uri => href(action=>$type, -full=>1,5958 hash=>$hash, hash_base=>$hash_base,5959 file_name=>$file_name),5960-status =>'302 Found');5961}59625963sub git_blobdiff {5964my$format=shift||'html';59655966my$fd;5967my@difftree;5968my%diffinfo;5969my$expires;59705971# preparing $fd and %diffinfo for git_patchset_body5972# new style URI5973if(defined$hash_base&&defined$hash_parent_base) {5974if(defined$file_name) {5975# read raw output5976open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5977$hash_parent_base,$hash_base,5978"--", (defined$file_parent?$file_parent: ()),$file_name5979or die_error(500,"Open git-diff-tree failed");5980@difftree=map{chomp;$_} <$fd>;5981close$fd5982or die_error(404,"Reading git-diff-tree failed");5983@difftree5984or die_error(404,"Blob diff not found");59855986}elsif(defined$hash&&5987$hash=~/[0-9a-fA-F]{40}/) {5988# try to find filename from $hash59895990# read filtered raw output5991open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5992$hash_parent_base,$hash_base,"--"5993or die_error(500,"Open git-diff-tree failed");5994@difftree=5995# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'5996# $hash == to_id5997grep{/^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/}5998map{chomp;$_} <$fd>;5999close$fd6000or die_error(404,"Reading git-diff-tree failed");6001@difftree6002or die_error(404,"Blob diff not found");60036004}else{6005 die_error(400,"Missing one of the blob diff parameters");6006}60076008if(@difftree>1) {6009 die_error(400,"Ambiguous blob diff specification");6010}60116012%diffinfo= parse_difftree_raw_line($difftree[0]);6013$file_parent||=$diffinfo{'from_file'} ||$file_name;6014$file_name||=$diffinfo{'to_file'};60156016$hash_parent||=$diffinfo{'from_id'};6017$hash||=$diffinfo{'to_id'};60186019# non-textual hash id's can be cached6020if($hash_base=~m/^[0-9a-fA-F]{40}$/&&6021$hash_parent_base=~m/^[0-9a-fA-F]{40}$/) {6022$expires='+1d';6023}60246025# open patch output6026open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6027'-p', ($formateq'html'?"--full-index": ()),6028$hash_parent_base,$hash_base,6029"--", (defined$file_parent?$file_parent: ()),$file_name6030or die_error(500,"Open git-diff-tree failed");6031}60326033# old/legacy style URI -- not generated anymore since 1.4.3.6034if(!%diffinfo) {6035 die_error('404 Not Found',"Missing one of the blob diff parameters")6036}60376038# header6039if($formateq'html') {6040my$formats_nav=6041$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},6042"raw");6043 git_header_html(undef,$expires);6044if(defined$hash_base&& (my%co= parse_commit($hash_base))) {6045 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);6046 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);6047}else{6048print"<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";6049print"<div class=\"title\">$hashvs$hash_parent</div>\n";6050}6051if(defined$file_name) {6052 git_print_page_path($file_name,"blob",$hash_base);6053}else{6054print"<div class=\"page_path\"></div>\n";6055}60566057}elsif($formateq'plain') {6058print$cgi->header(6059-type =>'text/plain',6060-charset =>'utf-8',6061-expires =>$expires,6062-content_disposition =>'inline; filename="'."$file_name".'.patch"');60636064print"X-Git-Url: ".$cgi->self_url() ."\n\n";60656066}else{6067 die_error(400,"Unknown blobdiff format");6068}60696070# patch6071if($formateq'html') {6072print"<div class=\"page_body\">\n";60736074 git_patchset_body($fd, [ \%diffinfo],$hash_base,$hash_parent_base);6075close$fd;60766077print"</div>\n";# class="page_body"6078 git_footer_html();60796080}else{6081while(my$line= <$fd>) {6082$line=~s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;6083$line=~s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;60846085print$line;60866087last if$line=~m!^\+\+\+!;6088}6089local$/=undef;6090print<$fd>;6091close$fd;6092}6093}60946095sub git_blobdiff_plain {6096 git_blobdiff('plain');6097}60986099sub git_commitdiff {6100my%params=@_;6101my$format=$params{-format} ||'html';61026103my($patch_max) = gitweb_get_feature('patches');6104if($formateq'patch') {6105 die_error(403,"Patch view not allowed")unless$patch_max;6106}61076108$hash||=$hash_base||"HEAD";6109my%co= parse_commit($hash)6110or die_error(404,"Unknown commit object");61116112# choose format for commitdiff for merge6113if(!defined$hash_parent&& @{$co{'parents'}} >1) {6114$hash_parent='--cc';6115}6116# we need to prepare $formats_nav before almost any parameter munging6117my$formats_nav;6118if($formateq'html') {6119$formats_nav=6120$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},6121"raw");6122if($patch_max&& @{$co{'parents'}} <=1) {6123$formats_nav.=" | ".6124$cgi->a({-href => href(action=>"patch", -replay=>1)},6125"patch");6126}61276128if(defined$hash_parent&&6129$hash_parentne'-c'&&$hash_parentne'--cc') {6130# commitdiff with two commits given6131my$hash_parent_short=$hash_parent;6132if($hash_parent=~m/^[0-9a-fA-F]{40}$/) {6133$hash_parent_short=substr($hash_parent,0,7);6134}6135$formats_nav.=6136' (from';6137for(my$i=0;$i< @{$co{'parents'}};$i++) {6138if($co{'parents'}[$i]eq$hash_parent) {6139$formats_nav.=' parent '. ($i+1);6140last;6141}6142}6143$formats_nav.=': '.6144$cgi->a({-href => href(action=>"commitdiff",6145 hash=>$hash_parent)},6146 esc_html($hash_parent_short)) .6147')';6148}elsif(!$co{'parent'}) {6149# --root commitdiff6150$formats_nav.=' (initial)';6151}elsif(scalar@{$co{'parents'}} ==1) {6152# single parent commit6153$formats_nav.=6154' (parent: '.6155$cgi->a({-href => href(action=>"commitdiff",6156 hash=>$co{'parent'})},6157 esc_html(substr($co{'parent'},0,7))) .6158')';6159}else{6160# merge commit6161if($hash_parenteq'--cc') {6162$formats_nav.=' | '.6163$cgi->a({-href => href(action=>"commitdiff",6164 hash=>$hash, hash_parent=>'-c')},6165'combined');6166}else{# $hash_parent eq '-c'6167$formats_nav.=' | '.6168$cgi->a({-href => href(action=>"commitdiff",6169 hash=>$hash, hash_parent=>'--cc')},6170'compact');6171}6172$formats_nav.=6173' (merge: '.6174join(' ',map{6175$cgi->a({-href => href(action=>"commitdiff",6176 hash=>$_)},6177 esc_html(substr($_,0,7)));6178} @{$co{'parents'}} ) .6179')';6180}6181}61826183my$hash_parent_param=$hash_parent;6184if(!defined$hash_parent_param) {6185# --cc for multiple parents, --root for parentless6186$hash_parent_param=6187@{$co{'parents'}} >1?'--cc':$co{'parent'} ||'--root';6188}61896190# read commitdiff6191my$fd;6192my@difftree;6193if($formateq'html') {6194open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6195"--no-commit-id","--patch-with-raw","--full-index",6196$hash_parent_param,$hash,"--"6197or die_error(500,"Open git-diff-tree failed");61986199while(my$line= <$fd>) {6200chomp$line;6201# empty line ends raw part of diff-tree output6202last unless$line;6203push@difftree,scalar parse_difftree_raw_line($line);6204}62056206}elsif($formateq'plain') {6207open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6208'-p',$hash_parent_param,$hash,"--"6209or die_error(500,"Open git-diff-tree failed");6210}elsif($formateq'patch') {6211# For commit ranges, we limit the output to the number of6212# patches specified in the 'patches' feature.6213# For single commits, we limit the output to a single patch,6214# diverging from the git-format-patch default.6215my@commit_spec= ();6216if($hash_parent) {6217if($patch_max>0) {6218push@commit_spec,"-$patch_max";6219}6220push@commit_spec,'-n',"$hash_parent..$hash";6221}else{6222if($params{-single}) {6223push@commit_spec,'-1';6224}else{6225if($patch_max>0) {6226push@commit_spec,"-$patch_max";6227}6228push@commit_spec,"-n";6229}6230push@commit_spec,'--root',$hash;6231}6232open$fd,"-|", git_cmd(),"format-patch",'--encoding=utf8',6233'--stdout',@commit_spec6234or die_error(500,"Open git-format-patch failed");6235}else{6236 die_error(400,"Unknown commitdiff format");6237}62386239# non-textual hash id's can be cached6240my$expires;6241if($hash=~m/^[0-9a-fA-F]{40}$/) {6242$expires="+1d";6243}62446245# write commit message6246if($formateq'html') {6247my$refs= git_get_references();6248my$ref= format_ref_marker($refs,$co{'id'});62496250 git_header_html(undef,$expires);6251 git_print_page_nav('commitdiff','',$hash,$co{'tree'},$hash,$formats_nav);6252 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash);6253print"<div class=\"title_text\">\n".6254"<table class=\"object_header\">\n";6255 git_print_authorship_rows(\%co);6256print"</table>".6257"</div>\n";6258print"<div class=\"page_body\">\n";6259if(@{$co{'comment'}} >1) {6260print"<div class=\"log\">\n";6261 git_print_log($co{'comment'}, -final_empty_line=>1, -remove_title =>1);6262print"</div>\n";# class="log"6263}62646265}elsif($formateq'plain') {6266my$refs= git_get_references("tags");6267my$tagname= git_get_rev_name_tags($hash);6268my$filename= basename($project) ."-$hash.patch";62696270print$cgi->header(6271-type =>'text/plain',6272-charset =>'utf-8',6273-expires =>$expires,6274-content_disposition =>'inline; filename="'."$filename".'"');6275my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});6276print"From: ". to_utf8($co{'author'}) ."\n";6277print"Date:$ad{'rfc2822'} ($ad{'tz_local'})\n";6278print"Subject: ". to_utf8($co{'title'}) ."\n";62796280print"X-Git-Tag:$tagname\n"if$tagname;6281print"X-Git-Url: ".$cgi->self_url() ."\n\n";62826283foreachmy$line(@{$co{'comment'}}) {6284print to_utf8($line) ."\n";6285}6286print"---\n\n";6287}elsif($formateq'patch') {6288my$filename= basename($project) ."-$hash.patch";62896290print$cgi->header(6291-type =>'text/plain',6292-charset =>'utf-8',6293-expires =>$expires,6294-content_disposition =>'inline; filename="'."$filename".'"');6295}62966297# write patch6298if($formateq'html') {6299my$use_parents= !defined$hash_parent||6300$hash_parenteq'-c'||$hash_parenteq'--cc';6301 git_difftree_body(\@difftree,$hash,6302$use_parents? @{$co{'parents'}} :$hash_parent);6303print"<br/>\n";63046305 git_patchset_body($fd, \@difftree,$hash,6306$use_parents? @{$co{'parents'}} :$hash_parent);6307close$fd;6308print"</div>\n";# class="page_body"6309 git_footer_html();63106311}elsif($formateq'plain') {6312local$/=undef;6313print<$fd>;6314close$fd6315or print"Reading git-diff-tree failed\n";6316}elsif($formateq'patch') {6317local$/=undef;6318print<$fd>;6319close$fd6320or print"Reading git-format-patch failed\n";6321}6322}63236324sub git_commitdiff_plain {6325 git_commitdiff(-format =>'plain');6326}63276328# format-patch-style patches6329sub git_patch {6330 git_commitdiff(-format =>'patch', -single =>1);6331}63326333sub git_patches {6334 git_commitdiff(-format =>'patch');6335}63366337sub git_history {6338 git_log_generic('history', \&git_history_body,6339$hash_base,$hash_parent_base,6340$file_name,$hash);6341}63426343sub git_search {6344 gitweb_check_feature('search')or die_error(403,"Search is disabled");6345if(!defined$searchtext) {6346 die_error(400,"Text field is empty");6347}6348if(!defined$hash) {6349$hash= git_get_head_hash($project);6350}6351my%co= parse_commit($hash);6352if(!%co) {6353 die_error(404,"Unknown commit object");6354}6355if(!defined$page) {6356$page=0;6357}63586359$searchtype||='commit';6360if($searchtypeeq'pickaxe') {6361# pickaxe may take all resources of your box and run for several minutes6362# with every query - so decide by yourself how public you make this feature6363 gitweb_check_feature('pickaxe')6364or die_error(403,"Pickaxe is disabled");6365}6366if($searchtypeeq'grep') {6367 gitweb_check_feature('grep')6368or die_error(403,"Grep is disabled");6369}63706371 git_header_html();63726373if($searchtypeeq'commit'or$searchtypeeq'author'or$searchtypeeq'committer') {6374my$greptype;6375if($searchtypeeq'commit') {6376$greptype="--grep=";6377}elsif($searchtypeeq'author') {6378$greptype="--author=";6379}elsif($searchtypeeq'committer') {6380$greptype="--committer=";6381}6382$greptype.=$searchtext;6383my@commitlist= parse_commits($hash,101, (100*$page),undef,6384$greptype,'--regexp-ignore-case',6385$search_use_regexp?'--extended-regexp':'--fixed-strings');63866387my$paging_nav='';6388if($page>0) {6389$paging_nav.=6390$cgi->a({-href => href(action=>"search", hash=>$hash,6391 searchtext=>$searchtext,6392 searchtype=>$searchtype)},6393"first");6394$paging_nav.=" ⋅ ".6395$cgi->a({-href => href(-replay=>1, page=>$page-1),6396-accesskey =>"p", -title =>"Alt-p"},"prev");6397}else{6398$paging_nav.="first";6399$paging_nav.=" ⋅ prev";6400}6401my$next_link='';6402if($#commitlist>=100) {6403$next_link=6404$cgi->a({-href => href(-replay=>1, page=>$page+1),6405-accesskey =>"n", -title =>"Alt-n"},"next");6406$paging_nav.=" ⋅$next_link";6407}else{6408$paging_nav.=" ⋅ next";6409}64106411if($#commitlist>=100) {6412}64136414 git_print_page_nav('','',$hash,$co{'tree'},$hash,$paging_nav);6415 git_print_header_div('commit', esc_html($co{'title'}),$hash);6416 git_search_grep_body(\@commitlist,0,99,$next_link);6417}64186419if($searchtypeeq'pickaxe') {6420 git_print_page_nav('','',$hash,$co{'tree'},$hash);6421 git_print_header_div('commit', esc_html($co{'title'}),$hash);64226423print"<table class=\"pickaxe search\">\n";6424my$alternate=1;6425local$/="\n";6426open my$fd,'-|', git_cmd(),'--no-pager','log',@diff_opts,6427'--pretty=format:%H','--no-abbrev','--raw',"-S$searchtext",6428($search_use_regexp?'--pickaxe-regex': ());6429undef%co;6430my@files;6431while(my$line= <$fd>) {6432chomp$line;6433next unless$line;64346435my%set= parse_difftree_raw_line($line);6436if(defined$set{'commit'}) {6437# finish previous commit6438if(%co) {6439print"</td>\n".6440"<td class=\"link\">".6441$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6442" | ".6443$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6444print"</td>\n".6445"</tr>\n";6446}64476448if($alternate) {6449print"<tr class=\"dark\">\n";6450}else{6451print"<tr class=\"light\">\n";6452}6453$alternate^=1;6454%co= parse_commit($set{'commit'});6455my$author= chop_and_escape_str($co{'author_name'},15,5);6456print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".6457"<td><i>$author</i></td>\n".6458"<td>".6459$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),6460-class=>"list subject"},6461 chop_and_escape_str($co{'title'},50) ."<br/>");6462}elsif(defined$set{'to_id'}) {6463next if($set{'to_id'} =~m/^0{40}$/);64646465print$cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},6466 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),6467-class=>"list"},6468"<span class=\"match\">". esc_path($set{'file'}) ."</span>") .6469"<br/>\n";6470}6471}6472close$fd;64736474# finish last commit (warning: repetition!)6475if(%co) {6476print"</td>\n".6477"<td class=\"link\">".6478$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6479" | ".6480$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6481print"</td>\n".6482"</tr>\n";6483}64846485print"</table>\n";6486}64876488if($searchtypeeq'grep') {6489 git_print_page_nav('','',$hash,$co{'tree'},$hash);6490 git_print_header_div('commit', esc_html($co{'title'}),$hash);64916492print"<table class=\"grep_search\">\n";6493my$alternate=1;6494my$matches=0;6495local$/="\n";6496open my$fd,"-|", git_cmd(),'grep','-n',6497$search_use_regexp? ('-E','-i') :'-F',6498$searchtext,$co{'tree'};6499my$lastfile='';6500while(my$line= <$fd>) {6501chomp$line;6502my($file,$lno,$ltext,$binary);6503last if($matches++>1000);6504if($line=~/^Binary file (.+) matches$/) {6505$file=$1;6506$binary=1;6507}else{6508(undef,$file,$lno,$ltext) =split(/:/,$line,4);6509}6510if($filene$lastfile) {6511$lastfileand print"</td></tr>\n";6512if($alternate++) {6513print"<tr class=\"dark\">\n";6514}else{6515print"<tr class=\"light\">\n";6516}6517print"<td class=\"list\">".6518$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6519 file_name=>"$file"),6520-class=>"list"}, esc_path($file));6521print"</td><td>\n";6522$lastfile=$file;6523}6524if($binary) {6525print"<div class=\"binary\">Binary file</div>\n";6526}else{6527$ltext= untabify($ltext);6528if($ltext=~m/^(.*)($search_regexp)(.*)$/i) {6529$ltext= esc_html($1, -nbsp=>1);6530$ltext.='<span class="match">';6531$ltext.= esc_html($2, -nbsp=>1);6532$ltext.='</span>';6533$ltext.= esc_html($3, -nbsp=>1);6534}else{6535$ltext= esc_html($ltext, -nbsp=>1);6536}6537print"<div class=\"pre\">".6538$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6539 file_name=>"$file").'#l'.$lno,6540-class=>"linenr"},sprintf('%4i',$lno))6541.' '.$ltext."</div>\n";6542}6543}6544if($lastfile) {6545print"</td></tr>\n";6546if($matches>1000) {6547print"<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";6548}6549}else{6550print"<div class=\"diff nodifferences\">No matches found</div>\n";6551}6552close$fd;65536554print"</table>\n";6555}6556 git_footer_html();6557}65586559sub git_search_help {6560 git_header_html();6561 git_print_page_nav('','',$hash,$hash,$hash);6562print<<EOT;6563<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without6564regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,6565the pattern entered is recognized as the POSIX extended6566<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case6567insensitive).</p>6568<dl>6569<dt><b>commit</b></dt>6570<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>6571EOT6572my$have_grep= gitweb_check_feature('grep');6573if($have_grep) {6574print<<EOT;6575<dt><b>grep</b></dt>6576<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing6577 a different one) are searched for the given pattern. On large trees, this search can take6578a while and put some strain on the server, so please use it with some consideration. Note that6579due to git-grep peculiarity, currently if regexp mode is turned off, the matches are6580case-sensitive.</dd>6581EOT6582}6583print<<EOT;6584<dt><b>author</b></dt>6585<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>6586<dt><b>committer</b></dt>6587<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>6588EOT6589my$have_pickaxe= gitweb_check_feature('pickaxe');6590if($have_pickaxe) {6591print<<EOT;6592<dt><b>pickaxe</b></dt>6593<dd>All commits that caused the string to appear or disappear from any file (changes that6594added, removed or "modified" the string) will be listed. This search can take a while and6595takes a lot of strain on the server, so please use it wisely. Note that since you may be6596interested even in changes just changing the case as well, this search is case sensitive.</dd>6597EOT6598}6599print"</dl>\n";6600 git_footer_html();6601}66026603sub git_shortlog {6604 git_log_generic('shortlog', \&git_shortlog_body,6605$hash,$hash_parent);6606}66076608## ......................................................................6609## feeds (RSS, Atom; OPML)66106611sub git_feed {6612my$format=shift||'atom';6613my$have_blame= gitweb_check_feature('blame');66146615# Atom: http://www.atomenabled.org/developers/syndication/6616# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ6617if($formatne'rss'&&$formatne'atom') {6618 die_error(400,"Unknown web feed format");6619}66206621# log/feed of current (HEAD) branch, log of given branch, history of file/directory6622my$head=$hash||'HEAD';6623my@commitlist= parse_commits($head,150,0,$file_name);66246625my%latest_commit;6626my%latest_date;6627my$content_type="application/$format+xml";6628if(defined$cgi->http('HTTP_ACCEPT') &&6629$cgi->Accept('text/xml') >$cgi->Accept($content_type)) {6630# browser (feed reader) prefers text/xml6631$content_type='text/xml';6632}6633if(defined($commitlist[0])) {6634%latest_commit= %{$commitlist[0]};6635my$latest_epoch=$latest_commit{'committer_epoch'};6636%latest_date= parse_date($latest_epoch);6637my$if_modified=$cgi->http('IF_MODIFIED_SINCE');6638if(defined$if_modified) {6639my$since;6640if(eval{require HTTP::Date;1; }) {6641$since= HTTP::Date::str2time($if_modified);6642}elsif(eval{require Time::ParseDate;1; }) {6643$since= Time::ParseDate::parsedate($if_modified, GMT =>1);6644}6645if(defined$since&&$latest_epoch<=$since) {6646print$cgi->header(6647-type =>$content_type,6648-charset =>'utf-8',6649-last_modified =>$latest_date{'rfc2822'},6650-status =>'304 Not Modified');6651return;6652}6653}6654print$cgi->header(6655-type =>$content_type,6656-charset =>'utf-8',6657-last_modified =>$latest_date{'rfc2822'});6658}else{6659print$cgi->header(6660-type =>$content_type,6661-charset =>'utf-8');6662}66636664# Optimization: skip generating the body if client asks only6665# for Last-Modified date.6666return if($cgi->request_method()eq'HEAD');66676668# header variables6669my$title="$site_name-$project/$action";6670my$feed_type='log';6671if(defined$hash) {6672$title.=" - '$hash'";6673$feed_type='branch log';6674if(defined$file_name) {6675$title.=" ::$file_name";6676$feed_type='history';6677}6678}elsif(defined$file_name) {6679$title.=" -$file_name";6680$feed_type='history';6681}6682$title.="$feed_type";6683my$descr= git_get_project_description($project);6684if(defined$descr) {6685$descr= esc_html($descr);6686}else{6687$descr="$project".6688($formateq'rss'?'RSS':'Atom') .6689" feed";6690}6691my$owner= git_get_project_owner($project);6692$owner= esc_html($owner);66936694#header6695my$alt_url;6696if(defined$file_name) {6697$alt_url= href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);6698}elsif(defined$hash) {6699$alt_url= href(-full=>1, action=>"log", hash=>$hash);6700}else{6701$alt_url= href(-full=>1, action=>"summary");6702}6703print qq!<?xml version="1.0" encoding="utf-8"?>\n!;6704if($formateq'rss') {6705print<<XML;6706<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">6707<channel>6708XML6709print"<title>$title</title>\n".6710"<link>$alt_url</link>\n".6711"<description>$descr</description>\n".6712"<language>en</language>\n".6713# project owner is responsible for 'editorial' content6714"<managingEditor>$owner</managingEditor>\n";6715if(defined$logo||defined$favicon) {6716# prefer the logo to the favicon, since RSS6717# doesn't allow both6718my$img= esc_url($logo||$favicon);6719print"<image>\n".6720"<url>$img</url>\n".6721"<title>$title</title>\n".6722"<link>$alt_url</link>\n".6723"</image>\n";6724}6725if(%latest_date) {6726print"<pubDate>$latest_date{'rfc2822'}</pubDate>\n";6727print"<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";6728}6729print"<generator>gitweb v.$version/$git_version</generator>\n";6730}elsif($formateq'atom') {6731print<<XML;6732<feed xmlns="http://www.w3.org/2005/Atom">6733XML6734print"<title>$title</title>\n".6735"<subtitle>$descr</subtitle>\n".6736'<link rel="alternate" type="text/html" href="'.6737$alt_url.'" />'."\n".6738'<link rel="self" type="'.$content_type.'" href="'.6739$cgi->self_url() .'" />'."\n".6740"<id>". href(-full=>1) ."</id>\n".6741# use project owner for feed author6742"<author><name>$owner</name></author>\n";6743if(defined$favicon) {6744print"<icon>". esc_url($favicon) ."</icon>\n";6745}6746if(defined$logo_url) {6747# not twice as wide as tall: 72 x 27 pixels6748print"<logo>". esc_url($logo) ."</logo>\n";6749}6750if(!%latest_date) {6751# dummy date to keep the feed valid until commits trickle in:6752print"<updated>1970-01-01T00:00:00Z</updated>\n";6753}else{6754print"<updated>$latest_date{'iso-8601'}</updated>\n";6755}6756print"<generator version='$version/$git_version'>gitweb</generator>\n";6757}67586759# contents6760for(my$i=0;$i<=$#commitlist;$i++) {6761my%co= %{$commitlist[$i]};6762my$commit=$co{'id'};6763# we read 150, we always show 30 and the ones more recent than 48 hours6764if(($i>=20) && ((time-$co{'author_epoch'}) >48*60*60)) {6765last;6766}6767my%cd= parse_date($co{'author_epoch'});67686769# get list of changed files6770open my$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6771$co{'parent'} ||"--root",6772$co{'id'},"--", (defined$file_name?$file_name: ())6773ornext;6774my@difftree=map{chomp;$_} <$fd>;6775close$fd6776ornext;67776778# print element (entry, item)6779my$co_url= href(-full=>1, action=>"commitdiff", hash=>$commit);6780if($formateq'rss') {6781print"<item>\n".6782"<title>". esc_html($co{'title'}) ."</title>\n".6783"<author>". esc_html($co{'author'}) ."</author>\n".6784"<pubDate>$cd{'rfc2822'}</pubDate>\n".6785"<guid isPermaLink=\"true\">$co_url</guid>\n".6786"<link>$co_url</link>\n".6787"<description>". esc_html($co{'title'}) ."</description>\n".6788"<content:encoded>".6789"<![CDATA[\n";6790}elsif($formateq'atom') {6791print"<entry>\n".6792"<title type=\"html\">". esc_html($co{'title'}) ."</title>\n".6793"<updated>$cd{'iso-8601'}</updated>\n".6794"<author>\n".6795" <name>". esc_html($co{'author_name'}) ."</name>\n";6796if($co{'author_email'}) {6797print" <email>". esc_html($co{'author_email'}) ."</email>\n";6798}6799print"</author>\n".6800# use committer for contributor6801"<contributor>\n".6802" <name>". esc_html($co{'committer_name'}) ."</name>\n";6803if($co{'committer_email'}) {6804print" <email>". esc_html($co{'committer_email'}) ."</email>\n";6805}6806print"</contributor>\n".6807"<published>$cd{'iso-8601'}</published>\n".6808"<link rel=\"alternate\"type=\"text/html\"href=\"$co_url\"/>\n".6809"<id>$co_url</id>\n".6810"<content type=\"xhtml\"xml:base=\"". esc_url($my_url) ."\">\n".6811"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";6812}6813my$comment=$co{'comment'};6814print"<pre>\n";6815foreachmy$line(@$comment) {6816$line= esc_html($line);6817print"$line\n";6818}6819print"</pre><ul>\n";6820foreachmy$difftree_line(@difftree) {6821my%difftree= parse_difftree_raw_line($difftree_line);6822next if!$difftree{'from_id'};68236824my$file=$difftree{'file'} ||$difftree{'to_file'};68256826print"<li>".6827"[".6828$cgi->a({-href => href(-full=>1, action=>"blobdiff",6829 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},6830 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},6831 file_name=>$file, file_parent=>$difftree{'from_file'}),6832-title =>"diff"},'D');6833if($have_blame) {6834print$cgi->a({-href => href(-full=>1, action=>"blame",6835 file_name=>$file, hash_base=>$commit),6836-title =>"blame"},'B');6837}6838# if this is not a feed of a file history6839if(!defined$file_name||$file_namene$file) {6840print$cgi->a({-href => href(-full=>1, action=>"history",6841 file_name=>$file, hash=>$commit),6842-title =>"history"},'H');6843}6844$file= esc_path($file);6845print"] ".6846"$file</li>\n";6847}6848if($formateq'rss') {6849print"</ul>]]>\n".6850"</content:encoded>\n".6851"</item>\n";6852}elsif($formateq'atom') {6853print"</ul>\n</div>\n".6854"</content>\n".6855"</entry>\n";6856}6857}68586859# end of feed6860if($formateq'rss') {6861print"</channel>\n</rss>\n";6862}elsif($formateq'atom') {6863print"</feed>\n";6864}6865}68666867sub git_rss {6868 git_feed('rss');6869}68706871sub git_atom {6872 git_feed('atom');6873}68746875sub git_opml {6876my@list= git_get_projects_list();68776878print$cgi->header(6879-type =>'text/xml',6880-charset =>'utf-8',6881-content_disposition =>'inline; filename="opml.xml"');68826883print<<XML;6884<?xml version="1.0" encoding="utf-8"?>6885<opml version="1.0">6886<head>6887 <title>$site_nameOPML Export</title>6888</head>6889<body>6890<outline text="git RSS feeds">6891XML68926893foreachmy$pr(@list) {6894my%proj=%$pr;6895my$head= git_get_head_hash($proj{'path'});6896if(!defined$head) {6897next;6898}6899$git_dir="$projectroot/$proj{'path'}";6900my%co= parse_commit($head);6901if(!%co) {6902next;6903}69046905my$path= esc_html(chop_str($proj{'path'},25,5));6906my$rss= href('project'=>$proj{'path'},'action'=>'rss', -full =>1);6907my$html= href('project'=>$proj{'path'},'action'=>'summary', -full =>1);6908print"<outline type=\"rss\"text=\"$path\"title=\"$path\"xmlUrl=\"$rss\"htmlUrl=\"$html\"/>\n";6909}6910print<<XML;6911</outline>6912</body>6913</opml>6914XML6915}