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$cgi= new CGI; 32our$version="++GIT_VERSION++"; 33our$my_url=$cgi->url(); 34our$my_uri=$cgi->url(-absolute =>1); 35 36# Base URL for relative URLs in gitweb ($logo, $favicon, ...), 37# needed and used only for URLs with nonempty PATH_INFO 38our$base_url=$my_url; 39 40# When the script is used as DirectoryIndex, the URL does not contain the name 41# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we 42# have to do it ourselves. We make $path_info global because it's also used 43# later on. 44# 45# Another issue with the script being the DirectoryIndex is that the resulting 46# $my_url data is not the full script URL: this is good, because we want 47# generated links to keep implying the script name if it wasn't explicitly 48# indicated in the URL we're handling, but it means that $my_url cannot be used 49# as base URL. 50# Therefore, if we needed to strip PATH_INFO, then we know that we have 51# to build the base URL ourselves: 52our$path_info=$ENV{"PATH_INFO"}; 53if($path_info) { 54if($my_url=~ s,\Q$path_info\E$,, && 55$my_uri=~ s,\Q$path_info\E$,, && 56defined$ENV{'SCRIPT_NAME'}) { 57$base_url=$cgi->url(-base =>1) .$ENV{'SCRIPT_NAME'}; 58} 59} 60 61# core git executable to use 62# this can just be "git" if your webserver has a sensible PATH 63our$GIT="++GIT_BINDIR++/git"; 64 65# absolute fs-path which will be prepended to the project path 66#our $projectroot = "/pub/scm"; 67our$projectroot="++GITWEB_PROJECTROOT++"; 68 69# fs traversing limit for getting project list 70# the number is relative to the projectroot 71our$project_maxdepth="++GITWEB_PROJECT_MAXDEPTH++"; 72 73# target of the home link on top of all pages 74our$home_link=$my_uri||"/"; 75 76# string of the home link on top of all pages 77our$home_link_str="++GITWEB_HOME_LINK_STR++"; 78 79# name of your site or organization to appear in page titles 80# replace this with something more descriptive for clearer bookmarks 81our$site_name="++GITWEB_SITENAME++" 82|| ($ENV{'SERVER_NAME'} ||"Untitled") ." Git"; 83 84# filename of html text to include at top of each page 85our$site_header="++GITWEB_SITE_HEADER++"; 86# html text to include at home page 87our$home_text="++GITWEB_HOMETEXT++"; 88# filename of html text to include at bottom of each page 89our$site_footer="++GITWEB_SITE_FOOTER++"; 90 91# URI of stylesheets 92our@stylesheets= ("++GITWEB_CSS++"); 93# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG. 94our$stylesheet=undef; 95# URI of GIT logo (72x27 size) 96our$logo="++GITWEB_LOGO++"; 97# URI of GIT favicon, assumed to be image/png type 98our$favicon="++GITWEB_FAVICON++"; 99# URI of gitweb.js (JavaScript code for gitweb) 100our$javascript="++GITWEB_JS++"; 101 102# URI and label (title) of GIT logo link 103#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; 104#our $logo_label = "git documentation"; 105our$logo_url="http://git-scm.com/"; 106our$logo_label="git homepage"; 107 108# source of projects list 109our$projects_list="++GITWEB_LIST++"; 110 111# the width (in characters) of the projects list "Description" column 112our$projects_list_description_width=25; 113 114# default order of projects list 115# valid values are none, project, descr, owner, and age 116our$default_projects_order="project"; 117 118# show repository only if this file exists 119# (only effective if this variable evaluates to true) 120our$export_ok="++GITWEB_EXPORT_OK++"; 121 122# show repository only if this subroutine returns true 123# when given the path to the project, for example: 124# sub { return -e "$_[0]/git-daemon-export-ok"; } 125our$export_auth_hook=undef; 126 127# only allow viewing of repositories also shown on the overview page 128our$strict_export="++GITWEB_STRICT_EXPORT++"; 129 130# list of git base URLs used for URL to where fetch project from, 131# i.e. full URL is "$git_base_url/$project" 132our@git_base_url_list=grep{$_ne''} ("++GITWEB_BASE_URL++"); 133 134# default blob_plain mimetype and default charset for text/plain blob 135our$default_blob_plain_mimetype='text/plain'; 136our$default_text_plain_charset=undef; 137 138# file to use for guessing MIME types before trying /etc/mime.types 139# (relative to the current git repository) 140our$mimetypes_file=undef; 141 142# assume this charset if line contains non-UTF-8 characters; 143# it should be valid encoding (see Encoding::Supported(3pm) for list), 144# for which encoding all byte sequences are valid, for example 145# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it 146# could be even 'utf-8' for the old behavior) 147our$fallback_encoding='latin1'; 148 149# rename detection options for git-diff and git-diff-tree 150# - default is '-M', with the cost proportional to 151# (number of removed files) * (number of new files). 152# - more costly is '-C' (which implies '-M'), with the cost proportional to 153# (number of changed files + number of removed files) * (number of new files) 154# - even more costly is '-C', '--find-copies-harder' with cost 155# (number of files in the original tree) * (number of new files) 156# - one might want to include '-B' option, e.g. '-B', '-M' 157our@diff_opts= ('-M');# taken from git_commit 158 159# Disables features that would allow repository owners to inject script into 160# the gitweb domain. 161our$prevent_xss=0; 162 163# information about snapshot formats that gitweb is capable of serving 164our%known_snapshot_formats= ( 165# name => { 166# 'display' => display name, 167# 'type' => mime type, 168# 'suffix' => filename suffix, 169# 'format' => --format for git-archive, 170# 'compressor' => [compressor command and arguments] 171# (array reference, optional)} 172# 173'tgz'=> { 174'display'=>'tar.gz', 175'type'=>'application/x-gzip', 176'suffix'=>'.tar.gz', 177'format'=>'tar', 178'compressor'=> ['gzip']}, 179 180'tbz2'=> { 181'display'=>'tar.bz2', 182'type'=>'application/x-bzip2', 183'suffix'=>'.tar.bz2', 184'format'=>'tar', 185'compressor'=> ['bzip2']}, 186 187'zip'=> { 188'display'=>'zip', 189'type'=>'application/x-zip', 190'suffix'=>'.zip', 191'format'=>'zip'}, 192); 193 194# Aliases so we understand old gitweb.snapshot values in repository 195# configuration. 196our%known_snapshot_format_aliases= ( 197'gzip'=>'tgz', 198'bzip2'=>'tbz2', 199 200# backward compatibility: legacy gitweb config support 201'x-gzip'=>undef,'gz'=>undef, 202'x-bzip2'=>undef,'bz2'=>undef, 203'x-zip'=>undef,''=>undef, 204); 205 206# Pixel sizes for icons and avatars. If the default font sizes or lineheights 207# are changed, it may be appropriate to change these values too via 208# $GITWEB_CONFIG. 209our%avatar_size= ( 210'default'=>16, 211'double'=>32 212); 213 214# You define site-wide feature defaults here; override them with 215# $GITWEB_CONFIG as necessary. 216our%feature= ( 217# feature => { 218# 'sub' => feature-sub (subroutine), 219# 'override' => allow-override (boolean), 220# 'default' => [ default options...] (array reference)} 221# 222# if feature is overridable (it means that allow-override has true value), 223# then feature-sub will be called with default options as parameters; 224# return value of feature-sub indicates if to enable specified feature 225# 226# if there is no 'sub' key (no feature-sub), then feature cannot be 227# overriden 228# 229# use gitweb_get_feature(<feature>) to retrieve the <feature> value 230# (an array) or gitweb_check_feature(<feature>) to check if <feature> 231# is enabled 232 233# Enable the 'blame' blob view, showing the last commit that modified 234# each line in the file. This can be very CPU-intensive. 235 236# To enable system wide have in $GITWEB_CONFIG 237# $feature{'blame'}{'default'} = [1]; 238# To have project specific config enable override in $GITWEB_CONFIG 239# $feature{'blame'}{'override'} = 1; 240# and in project config gitweb.blame = 0|1; 241'blame'=> { 242'sub'=>sub{ feature_bool('blame',@_) }, 243'override'=>0, 244'default'=> [0]}, 245 246# Enable the 'snapshot' link, providing a compressed archive of any 247# tree. This can potentially generate high traffic if you have large 248# project. 249 250# Value is a list of formats defined in %known_snapshot_formats that 251# you wish to offer. 252# To disable system wide have in $GITWEB_CONFIG 253# $feature{'snapshot'}{'default'} = []; 254# To have project specific config enable override in $GITWEB_CONFIG 255# $feature{'snapshot'}{'override'} = 1; 256# and in project config, a comma-separated list of formats or "none" 257# to disable. Example: gitweb.snapshot = tbz2,zip; 258'snapshot'=> { 259'sub'=> \&feature_snapshot, 260'override'=>0, 261'default'=> ['tgz']}, 262 263# Enable text search, which will list the commits which match author, 264# committer or commit text to a given string. Enabled by default. 265# Project specific override is not supported. 266'search'=> { 267'override'=>0, 268'default'=> [1]}, 269 270# Enable grep search, which will list the files in currently selected 271# tree containing the given string. Enabled by default. This can be 272# potentially CPU-intensive, of course. 273 274# To enable system wide have in $GITWEB_CONFIG 275# $feature{'grep'}{'default'} = [1]; 276# To have project specific config enable override in $GITWEB_CONFIG 277# $feature{'grep'}{'override'} = 1; 278# and in project config gitweb.grep = 0|1; 279'grep'=> { 280'sub'=>sub{ feature_bool('grep',@_) }, 281'override'=>0, 282'default'=> [1]}, 283 284# Enable the pickaxe search, which will list the commits that modified 285# a given string in a file. This can be practical and quite faster 286# alternative to 'blame', but still potentially CPU-intensive. 287 288# To enable system wide have in $GITWEB_CONFIG 289# $feature{'pickaxe'}{'default'} = [1]; 290# To have project specific config enable override in $GITWEB_CONFIG 291# $feature{'pickaxe'}{'override'} = 1; 292# and in project config gitweb.pickaxe = 0|1; 293'pickaxe'=> { 294'sub'=>sub{ feature_bool('pickaxe',@_) }, 295'override'=>0, 296'default'=> [1]}, 297 298# Make gitweb use an alternative format of the URLs which can be 299# more readable and natural-looking: project name is embedded 300# directly in the path and the query string contains other 301# auxiliary information. All gitweb installations recognize 302# URL in either format; this configures in which formats gitweb 303# generates links. 304 305# To enable system wide have in $GITWEB_CONFIG 306# $feature{'pathinfo'}{'default'} = [1]; 307# Project specific override is not supported. 308 309# Note that you will need to change the default location of CSS, 310# favicon, logo and possibly other files to an absolute URL. Also, 311# if gitweb.cgi serves as your indexfile, you will need to force 312# $my_uri to contain the script name in your $GITWEB_CONFIG. 313'pathinfo'=> { 314'override'=>0, 315'default'=> [0]}, 316 317# Make gitweb consider projects in project root subdirectories 318# to be forks of existing projects. Given project $projname.git, 319# projects matching $projname/*.git will not be shown in the main 320# projects list, instead a '+' mark will be added to $projname 321# there and a 'forks' view will be enabled for the project, listing 322# all the forks. If project list is taken from a file, forks have 323# to be listed after the main project. 324 325# To enable system wide have in $GITWEB_CONFIG 326# $feature{'forks'}{'default'} = [1]; 327# Project specific override is not supported. 328'forks'=> { 329'override'=>0, 330'default'=> [0]}, 331 332# Insert custom links to the action bar of all project pages. 333# This enables you mainly to link to third-party scripts integrating 334# into gitweb; e.g. git-browser for graphical history representation 335# or custom web-based repository administration interface. 336 337# The 'default' value consists of a list of triplets in the form 338# (label, link, position) where position is the label after which 339# to insert the link and link is a format string where %n expands 340# to the project name, %f to the project path within the filesystem, 341# %h to the current hash (h gitweb parameter) and %b to the current 342# hash base (hb gitweb parameter); %% expands to %. 343 344# To enable system wide have in $GITWEB_CONFIG e.g. 345# $feature{'actions'}{'default'} = [('graphiclog', 346# '/git-browser/by-commit.html?r=%n', 'summary')]; 347# Project specific override is not supported. 348'actions'=> { 349'override'=>0, 350'default'=> []}, 351 352# Allow gitweb scan project content tags described in ctags/ 353# of project repository, and display the popular Web 2.0-ish 354# "tag cloud" near the project list. Note that this is something 355# COMPLETELY different from the normal Git tags. 356 357# gitweb by itself can show existing tags, but it does not handle 358# tagging itself; you need an external application for that. 359# For an example script, check Girocco's cgi/tagproj.cgi. 360# You may want to install the HTML::TagCloud Perl module to get 361# a pretty tag cloud instead of just a list of tags. 362 363# To enable system wide have in $GITWEB_CONFIG 364# $feature{'ctags'}{'default'} = ['path_to_tag_script']; 365# Project specific override is not supported. 366'ctags'=> { 367'override'=>0, 368'default'=> [0]}, 369 370# The maximum number of patches in a patchset generated in patch 371# view. Set this to 0 or undef to disable patch view, or to a 372# negative number to remove any limit. 373 374# To disable system wide have in $GITWEB_CONFIG 375# $feature{'patches'}{'default'} = [0]; 376# To have project specific config enable override in $GITWEB_CONFIG 377# $feature{'patches'}{'override'} = 1; 378# and in project config gitweb.patches = 0|n; 379# where n is the maximum number of patches allowed in a patchset. 380'patches'=> { 381'sub'=> \&feature_patches, 382'override'=>0, 383'default'=> [16]}, 384 385# Avatar support. When this feature is enabled, views such as 386# shortlog or commit will display an avatar associated with 387# the email of the committer(s) and/or author(s). 388 389# Currently available providers are gravatar and picon. 390# If an unknown provider is specified, the feature is disabled. 391 392# Gravatar depends on Digest::MD5. 393# Picon currently relies on the indiana.edu database. 394 395# To enable system wide have in $GITWEB_CONFIG 396# $feature{'avatar'}{'default'} = ['<provider>']; 397# where <provider> is either gravatar or picon. 398# To have project specific config enable override in $GITWEB_CONFIG 399# $feature{'avatar'}{'override'} = 1; 400# and in project config gitweb.avatar = <provider>; 401'avatar'=> { 402'sub'=> \&feature_avatar, 403'override'=>0, 404'default'=> ['']}, 405 406# Enable displaying how much time and how many git commands 407# it took to generate and display page. Disabled by default. 408# Project specific override is not supported. 409'timed'=> { 410'override'=>0, 411'default'=> [0]}, 412); 413 414sub gitweb_get_feature { 415my($name) =@_; 416return unlessexists$feature{$name}; 417my($sub,$override,@defaults) = ( 418$feature{$name}{'sub'}, 419$feature{$name}{'override'}, 420@{$feature{$name}{'default'}}); 421if(!$override) {return@defaults; } 422if(!defined$sub) { 423warn"feature$nameis not overrideable"; 424return@defaults; 425} 426return$sub->(@defaults); 427} 428 429# A wrapper to check if a given feature is enabled. 430# With this, you can say 431# 432# my $bool_feat = gitweb_check_feature('bool_feat'); 433# gitweb_check_feature('bool_feat') or somecode; 434# 435# instead of 436# 437# my ($bool_feat) = gitweb_get_feature('bool_feat'); 438# (gitweb_get_feature('bool_feat'))[0] or somecode; 439# 440sub gitweb_check_feature { 441return(gitweb_get_feature(@_))[0]; 442} 443 444 445sub feature_bool { 446my$key=shift; 447my($val) = git_get_project_config($key,'--bool'); 448 449if(!defined$val) { 450return($_[0]); 451}elsif($valeq'true') { 452return(1); 453}elsif($valeq'false') { 454return(0); 455} 456} 457 458sub feature_snapshot { 459my(@fmts) =@_; 460 461my($val) = git_get_project_config('snapshot'); 462 463if($val) { 464@fmts= ($valeq'none'? () :split/\s*[,\s]\s*/,$val); 465} 466 467return@fmts; 468} 469 470sub feature_patches { 471my@val= (git_get_project_config('patches','--int')); 472 473if(@val) { 474return@val; 475} 476 477return($_[0]); 478} 479 480sub feature_avatar { 481my@val= (git_get_project_config('avatar')); 482 483return@val?@val:@_; 484} 485 486# checking HEAD file with -e is fragile if the repository was 487# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed 488# and then pruned. 489sub check_head_link { 490my($dir) =@_; 491my$headfile="$dir/HEAD"; 492return((-e $headfile) || 493(-l $headfile&&readlink($headfile) =~/^refs\/heads\//)); 494} 495 496sub check_export_ok { 497my($dir) =@_; 498return(check_head_link($dir) && 499(!$export_ok|| -e "$dir/$export_ok") && 500(!$export_auth_hook||$export_auth_hook->($dir))); 501} 502 503# process alternate names for backward compatibility 504# filter out unsupported (unknown) snapshot formats 505sub filter_snapshot_fmts { 506my@fmts=@_; 507 508@fmts=map{ 509exists$known_snapshot_format_aliases{$_} ? 510$known_snapshot_format_aliases{$_} :$_}@fmts; 511@fmts=grep{ 512exists$known_snapshot_formats{$_} }@fmts; 513} 514 515our$GITWEB_CONFIG=$ENV{'GITWEB_CONFIG'} ||"++GITWEB_CONFIG++"; 516if(-e $GITWEB_CONFIG) { 517do$GITWEB_CONFIG; 518}else{ 519our$GITWEB_CONFIG_SYSTEM=$ENV{'GITWEB_CONFIG_SYSTEM'} ||"++GITWEB_CONFIG_SYSTEM++"; 520do$GITWEB_CONFIG_SYSTEMif-e $GITWEB_CONFIG_SYSTEM; 521} 522 523# version of the core git binary 524our$git_version=qx("$GIT" --version)=~m/git version (.*)$/?$1:"unknown"; 525$number_of_git_cmds++; 526 527$projects_list||=$projectroot; 528 529# ====================================================================== 530# input validation and dispatch 531 532# input parameters can be collected from a variety of sources (presently, CGI 533# and PATH_INFO), so we define an %input_params hash that collects them all 534# together during validation: this allows subsequent uses (e.g. href()) to be 535# agnostic of the parameter origin 536 537our%input_params= (); 538 539# input parameters are stored with the long parameter name as key. This will 540# also be used in the href subroutine to convert parameters to their CGI 541# equivalent, and since the href() usage is the most frequent one, we store 542# the name -> CGI key mapping here, instead of the reverse. 543# 544# XXX: Warning: If you touch this, check the search form for updating, 545# too. 546 547our@cgi_param_mapping= ( 548 project =>"p", 549 action =>"a", 550 file_name =>"f", 551 file_parent =>"fp", 552 hash =>"h", 553 hash_parent =>"hp", 554 hash_base =>"hb", 555 hash_parent_base =>"hpb", 556 page =>"pg", 557 order =>"o", 558 searchtext =>"s", 559 searchtype =>"st", 560 snapshot_format =>"sf", 561 extra_options =>"opt", 562 search_use_regexp =>"sr", 563# this must be last entry (for manipulation from JavaScript) 564 javascript =>"js" 565); 566our%cgi_param_mapping=@cgi_param_mapping; 567 568# we will also need to know the possible actions, for validation 569our%actions= ( 570"blame"=> \&git_blame, 571"blame_incremental"=> \&git_blame_incremental, 572"blame_data"=> \&git_blame_data, 573"blobdiff"=> \&git_blobdiff, 574"blobdiff_plain"=> \&git_blobdiff_plain, 575"blob"=> \&git_blob, 576"blob_plain"=> \&git_blob_plain, 577"commitdiff"=> \&git_commitdiff, 578"commitdiff_plain"=> \&git_commitdiff_plain, 579"commit"=> \&git_commit, 580"forks"=> \&git_forks, 581"heads"=> \&git_heads, 582"history"=> \&git_history, 583"log"=> \&git_log, 584"patch"=> \&git_patch, 585"patches"=> \&git_patches, 586"rss"=> \&git_rss, 587"atom"=> \&git_atom, 588"search"=> \&git_search, 589"search_help"=> \&git_search_help, 590"shortlog"=> \&git_shortlog, 591"summary"=> \&git_summary, 592"tag"=> \&git_tag, 593"tags"=> \&git_tags, 594"tree"=> \&git_tree, 595"snapshot"=> \&git_snapshot, 596"object"=> \&git_object, 597# those below don't need $project 598"opml"=> \&git_opml, 599"project_list"=> \&git_project_list, 600"project_index"=> \&git_project_index, 601); 602 603# finally, we have the hash of allowed extra_options for the commands that 604# allow them 605our%allowed_options= ( 606"--no-merges"=> [qw(rss atom log shortlog history)], 607); 608 609# fill %input_params with the CGI parameters. All values except for 'opt' 610# should be single values, but opt can be an array. We should probably 611# build an array of parameters that can be multi-valued, but since for the time 612# being it's only this one, we just single it out 613while(my($name,$symbol) =each%cgi_param_mapping) { 614if($symboleq'opt') { 615$input_params{$name} = [$cgi->param($symbol) ]; 616}else{ 617$input_params{$name} =$cgi->param($symbol); 618} 619} 620 621# now read PATH_INFO and update the parameter list for missing parameters 622sub evaluate_path_info { 623return ifdefined$input_params{'project'}; 624return if!$path_info; 625$path_info=~ s,^/+,,; 626return if!$path_info; 627 628# find which part of PATH_INFO is project 629my$project=$path_info; 630$project=~ s,/+$,,; 631while($project&& !check_head_link("$projectroot/$project")) { 632$project=~ s,/*[^/]*$,,; 633} 634return unless$project; 635$input_params{'project'} =$project; 636 637# do not change any parameters if an action is given using the query string 638return if$input_params{'action'}; 639$path_info=~ s,^\Q$project\E/*,,; 640 641# next, check if we have an action 642my$action=$path_info; 643$action=~ s,/.*$,,; 644if(exists$actions{$action}) { 645$path_info=~ s,^$action/*,,; 646$input_params{'action'} =$action; 647} 648 649# list of actions that want hash_base instead of hash, but can have no 650# pathname (f) parameter 651my@wants_base= ( 652'tree', 653'history', 654); 655 656# we want to catch 657# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] 658my($parentrefname,$parentpathname,$refname,$pathname) = 659($path_info=~/^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); 660 661# first, analyze the 'current' part 662if(defined$pathname) { 663# we got "branch:filename" or "branch:dir/" 664# we could use git_get_type(branch:pathname), but: 665# - it needs $git_dir 666# - it does a git() call 667# - the convention of terminating directories with a slash 668# makes it superfluous 669# - embedding the action in the PATH_INFO would make it even 670# more superfluous 671$pathname=~ s,^/+,,; 672if(!$pathname||substr($pathname, -1)eq"/") { 673$input_params{'action'} ||="tree"; 674$pathname=~ s,/$,,; 675}else{ 676# the default action depends on whether we had parent info 677# or not 678if($parentrefname) { 679$input_params{'action'} ||="blobdiff_plain"; 680}else{ 681$input_params{'action'} ||="blob_plain"; 682} 683} 684$input_params{'hash_base'} ||=$refname; 685$input_params{'file_name'} ||=$pathname; 686}elsif(defined$refname) { 687# we got "branch". In this case we have to choose if we have to 688# set hash or hash_base. 689# 690# Most of the actions without a pathname only want hash to be 691# set, except for the ones specified in @wants_base that want 692# hash_base instead. It should also be noted that hand-crafted 693# links having 'history' as an action and no pathname or hash 694# set will fail, but that happens regardless of PATH_INFO. 695$input_params{'action'} ||="shortlog"; 696if(grep{$_eq$input_params{'action'} }@wants_base) { 697$input_params{'hash_base'} ||=$refname; 698}else{ 699$input_params{'hash'} ||=$refname; 700} 701} 702 703# next, handle the 'parent' part, if present 704if(defined$parentrefname) { 705# a missing pathspec defaults to the 'current' filename, allowing e.g. 706# someproject/blobdiff/oldrev..newrev:/filename 707if($parentpathname) { 708$parentpathname=~ s,^/+,,; 709$parentpathname=~ s,/$,,; 710$input_params{'file_parent'} ||=$parentpathname; 711}else{ 712$input_params{'file_parent'} ||=$input_params{'file_name'}; 713} 714# we assume that hash_parent_base is wanted if a path was specified, 715# or if the action wants hash_base instead of hash 716if(defined$input_params{'file_parent'} || 717grep{$_eq$input_params{'action'} }@wants_base) { 718$input_params{'hash_parent_base'} ||=$parentrefname; 719}else{ 720$input_params{'hash_parent'} ||=$parentrefname; 721} 722} 723 724# for the snapshot action, we allow URLs in the form 725# $project/snapshot/$hash.ext 726# where .ext determines the snapshot and gets removed from the 727# passed $refname to provide the $hash. 728# 729# To be able to tell that $refname includes the format extension, we 730# require the following two conditions to be satisfied: 731# - the hash input parameter MUST have been set from the $refname part 732# of the URL (i.e. they must be equal) 733# - the snapshot format MUST NOT have been defined already (e.g. from 734# CGI parameter sf) 735# It's also useless to try any matching unless $refname has a dot, 736# so we check for that too 737if(defined$input_params{'action'} && 738$input_params{'action'}eq'snapshot'&& 739defined$refname&&index($refname,'.') != -1&& 740$refnameeq$input_params{'hash'} && 741!defined$input_params{'snapshot_format'}) { 742# We loop over the known snapshot formats, checking for 743# extensions. Allowed extensions are both the defined suffix 744# (which includes the initial dot already) and the snapshot 745# format key itself, with a prepended dot 746while(my($fmt,$opt) =each%known_snapshot_formats) { 747my$hash=$refname; 748unless($hash=~s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) { 749next; 750} 751my$sfx=$1; 752# a valid suffix was found, so set the snapshot format 753# and reset the hash parameter 754$input_params{'snapshot_format'} =$fmt; 755$input_params{'hash'} =$hash; 756# we also set the format suffix to the one requested 757# in the URL: this way a request for e.g. .tgz returns 758# a .tgz instead of a .tar.gz 759$known_snapshot_formats{$fmt}{'suffix'} =$sfx; 760last; 761} 762} 763} 764evaluate_path_info(); 765 766our$action=$input_params{'action'}; 767if(defined$action) { 768if(!validate_action($action)) { 769 die_error(400,"Invalid action parameter"); 770} 771} 772 773# parameters which are pathnames 774our$project=$input_params{'project'}; 775if(defined$project) { 776if(!validate_project($project)) { 777undef$project; 778 die_error(404,"No such project"); 779} 780} 781 782our$file_name=$input_params{'file_name'}; 783if(defined$file_name) { 784if(!validate_pathname($file_name)) { 785 die_error(400,"Invalid file parameter"); 786} 787} 788 789our$file_parent=$input_params{'file_parent'}; 790if(defined$file_parent) { 791if(!validate_pathname($file_parent)) { 792 die_error(400,"Invalid file parent parameter"); 793} 794} 795 796# parameters which are refnames 797our$hash=$input_params{'hash'}; 798if(defined$hash) { 799if(!validate_refname($hash)) { 800 die_error(400,"Invalid hash parameter"); 801} 802} 803 804our$hash_parent=$input_params{'hash_parent'}; 805if(defined$hash_parent) { 806if(!validate_refname($hash_parent)) { 807 die_error(400,"Invalid hash parent parameter"); 808} 809} 810 811our$hash_base=$input_params{'hash_base'}; 812if(defined$hash_base) { 813if(!validate_refname($hash_base)) { 814 die_error(400,"Invalid hash base parameter"); 815} 816} 817 818our@extra_options= @{$input_params{'extra_options'}}; 819# @extra_options is always defined, since it can only be (currently) set from 820# CGI, and $cgi->param() returns the empty array in array context if the param 821# is not set 822foreachmy$opt(@extra_options) { 823if(not exists$allowed_options{$opt}) { 824 die_error(400,"Invalid option parameter"); 825} 826if(not grep(/^$action$/, @{$allowed_options{$opt}})) { 827 die_error(400,"Invalid option parameter for this action"); 828} 829} 830 831our$hash_parent_base=$input_params{'hash_parent_base'}; 832if(defined$hash_parent_base) { 833if(!validate_refname($hash_parent_base)) { 834 die_error(400,"Invalid hash parent base parameter"); 835} 836} 837 838# other parameters 839our$page=$input_params{'page'}; 840if(defined$page) { 841if($page=~m/[^0-9]/) { 842 die_error(400,"Invalid page parameter"); 843} 844} 845 846our$searchtype=$input_params{'searchtype'}; 847if(defined$searchtype) { 848if($searchtype=~m/[^a-z]/) { 849 die_error(400,"Invalid searchtype parameter"); 850} 851} 852 853our$search_use_regexp=$input_params{'search_use_regexp'}; 854 855our$searchtext=$input_params{'searchtext'}; 856our$search_regexp; 857if(defined$searchtext) { 858if(length($searchtext) <2) { 859 die_error(403,"At least two characters are required for search parameter"); 860} 861$search_regexp=$search_use_regexp?$searchtext:quotemeta$searchtext; 862} 863 864# path to the current git repository 865our$git_dir; 866$git_dir="$projectroot/$project"if$project; 867 868# list of supported snapshot formats 869our@snapshot_fmts= gitweb_get_feature('snapshot'); 870@snapshot_fmts= filter_snapshot_fmts(@snapshot_fmts); 871 872# check that the avatar feature is set to a known provider name, 873# and for each provider check if the dependencies are satisfied. 874# if the provider name is invalid or the dependencies are not met, 875# reset $git_avatar to the empty string. 876our($git_avatar) = gitweb_get_feature('avatar'); 877if($git_avatareq'gravatar') { 878$git_avatar=''unless(eval{require Digest::MD5;1; }); 879}elsif($git_avatareq'picon') { 880# no dependencies 881}else{ 882$git_avatar=''; 883} 884 885# dispatch 886if(!defined$action) { 887if(defined$hash) { 888$action= git_get_type($hash); 889}elsif(defined$hash_base&&defined$file_name) { 890$action= git_get_type("$hash_base:$file_name"); 891}elsif(defined$project) { 892$action='summary'; 893}else{ 894$action='project_list'; 895} 896} 897if(!defined($actions{$action})) { 898 die_error(400,"Unknown action"); 899} 900if($action!~m/^(?:opml|project_list|project_index)$/&& 901!$project) { 902 die_error(400,"Project needed"); 903} 904$actions{$action}->(); 905exit; 906 907## ====================================================================== 908## action links 909 910sub href { 911my%params=@_; 912# default is to use -absolute url() i.e. $my_uri 913my$href=$params{-full} ?$my_url:$my_uri; 914 915$params{'project'} =$projectunlessexists$params{'project'}; 916 917if($params{-replay}) { 918while(my($name,$symbol) =each%cgi_param_mapping) { 919if(!exists$params{$name}) { 920$params{$name} =$input_params{$name}; 921} 922} 923} 924 925my$use_pathinfo= gitweb_check_feature('pathinfo'); 926if($use_pathinfoand defined$params{'project'}) { 927# try to put as many parameters as possible in PATH_INFO: 928# - project name 929# - action 930# - hash_parent or hash_parent_base:/file_parent 931# - hash or hash_base:/filename 932# - the snapshot_format as an appropriate suffix 933 934# When the script is the root DirectoryIndex for the domain, 935# $href here would be something like http://gitweb.example.com/ 936# Thus, we strip any trailing / from $href, to spare us double 937# slashes in the final URL 938$href=~ s,/$,,; 939 940# Then add the project name, if present 941$href.="/".esc_url($params{'project'}); 942delete$params{'project'}; 943 944# since we destructively absorb parameters, we keep this 945# boolean that remembers if we're handling a snapshot 946my$is_snapshot=$params{'action'}eq'snapshot'; 947 948# Summary just uses the project path URL, any other action is 949# added to the URL 950if(defined$params{'action'}) { 951$href.="/".esc_url($params{'action'})unless$params{'action'}eq'summary'; 952delete$params{'action'}; 953} 954 955# Next, we put hash_parent_base:/file_parent..hash_base:/file_name, 956# stripping nonexistent or useless pieces 957$href.="/"if($params{'hash_base'} ||$params{'hash_parent_base'} 958||$params{'hash_parent'} ||$params{'hash'}); 959if(defined$params{'hash_base'}) { 960if(defined$params{'hash_parent_base'}) { 961$href.= esc_url($params{'hash_parent_base'}); 962# skip the file_parent if it's the same as the file_name 963delete$params{'file_parent'}if$params{'file_parent'}eq$params{'file_name'}; 964if(defined$params{'file_parent'} &&$params{'file_parent'} !~/\.\./) { 965$href.=":/".esc_url($params{'file_parent'}); 966delete$params{'file_parent'}; 967} 968$href.=".."; 969delete$params{'hash_parent'}; 970delete$params{'hash_parent_base'}; 971}elsif(defined$params{'hash_parent'}) { 972$href.= esc_url($params{'hash_parent'}).".."; 973delete$params{'hash_parent'}; 974} 975 976$href.= esc_url($params{'hash_base'}); 977if(defined$params{'file_name'} &&$params{'file_name'} !~/\.\./) { 978$href.=":/".esc_url($params{'file_name'}); 979delete$params{'file_name'}; 980} 981delete$params{'hash'}; 982delete$params{'hash_base'}; 983}elsif(defined$params{'hash'}) { 984$href.= esc_url($params{'hash'}); 985delete$params{'hash'}; 986} 987 988# If the action was a snapshot, we can absorb the 989# snapshot_format parameter too 990if($is_snapshot) { 991my$fmt=$params{'snapshot_format'}; 992# snapshot_format should always be defined when href() 993# is called, but just in case some code forgets, we 994# fall back to the default 995$fmt||=$snapshot_fmts[0]; 996$href.=$known_snapshot_formats{$fmt}{'suffix'}; 997delete$params{'snapshot_format'}; 998} 999}10001001# now encode the parameters explicitly1002my@result= ();1003for(my$i=0;$i<@cgi_param_mapping;$i+=2) {1004my($name,$symbol) = ($cgi_param_mapping[$i],$cgi_param_mapping[$i+1]);1005if(defined$params{$name}) {1006if(ref($params{$name})eq"ARRAY") {1007foreachmy$par(@{$params{$name}}) {1008push@result,$symbol."=". esc_param($par);1009}1010}else{1011push@result,$symbol."=". esc_param($params{$name});1012}1013}1014}1015$href.="?".join(';',@result)ifscalar@result;10161017return$href;1018}101910201021## ======================================================================1022## validation, quoting/unquoting and escaping10231024sub validate_action {1025my$input=shift||returnundef;1026returnundefunlessexists$actions{$input};1027return$input;1028}10291030sub validate_project {1031my$input=shift||returnundef;1032if(!validate_pathname($input) ||1033!(-d "$projectroot/$input") ||1034!check_export_ok("$projectroot/$input") ||1035($strict_export&& !project_in_list($input))) {1036returnundef;1037}else{1038return$input;1039}1040}10411042sub validate_pathname {1043my$input=shift||returnundef;10441045# no '.' or '..' as elements of path, i.e. no '.' nor '..'1046# at the beginning, at the end, and between slashes.1047# also this catches doubled slashes1048if($input=~m!(^|/)(|\.|\.\.)(/|$)!) {1049returnundef;1050}1051# no null characters1052if($input=~m!\0!) {1053returnundef;1054}1055return$input;1056}10571058sub validate_refname {1059my$input=shift||returnundef;10601061# textual hashes are O.K.1062if($input=~m/^[0-9a-fA-F]{40}$/) {1063return$input;1064}1065# it must be correct pathname1066$input= validate_pathname($input)1067orreturnundef;1068# restrictions on ref name according to git-check-ref-format1069if($input=~m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {1070returnundef;1071}1072return$input;1073}10741075# decode sequences of octets in utf8 into Perl's internal form,1076# which is utf-8 with utf8 flag set if needed. gitweb writes out1077# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning1078sub to_utf8 {1079my$str=shift;1080if(utf8::valid($str)) {1081 utf8::decode($str);1082return$str;1083}else{1084return decode($fallback_encoding,$str, Encode::FB_DEFAULT);1085}1086}10871088# quote unsafe chars, but keep the slash, even when it's not1089# correct, but quoted slashes look too horrible in bookmarks1090sub esc_param {1091my$str=shift;1092$str=~s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X",ord($1))/eg;1093$str=~s/\+/%2B/g;1094$str=~s/ /\+/g;1095return$str;1096}10971098# quote unsafe chars in whole URL, so some charactrs cannot be quoted1099sub esc_url {1100my$str=shift;1101$str=~s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X",ord($1))/eg;1102$str=~s/\+/%2B/g;1103$str=~s/ /\+/g;1104return$str;1105}11061107# replace invalid utf8 character with SUBSTITUTION sequence1108sub esc_html {1109my$str=shift;1110my%opts=@_;11111112$str= to_utf8($str);1113$str=$cgi->escapeHTML($str);1114if($opts{'-nbsp'}) {1115$str=~s/ / /g;1116}1117$str=~ s|([[:cntrl:]])|(($1ne"\t") ? quot_cec($1) :$1)|eg;1118return$str;1119}11201121# quote control characters and escape filename to HTML1122sub esc_path {1123my$str=shift;1124my%opts=@_;11251126$str= to_utf8($str);1127$str=$cgi->escapeHTML($str);1128if($opts{'-nbsp'}) {1129$str=~s/ / /g;1130}1131$str=~ s|([[:cntrl:]])|quot_cec($1)|eg;1132return$str;1133}11341135# Make control characters "printable", using character escape codes (CEC)1136sub quot_cec {1137my$cntrl=shift;1138my%opts=@_;1139my%es= (# character escape codes, aka escape sequences1140"\t"=>'\t',# tab (HT)1141"\n"=>'\n',# line feed (LF)1142"\r"=>'\r',# carrige return (CR)1143"\f"=>'\f',# form feed (FF)1144"\b"=>'\b',# backspace (BS)1145"\a"=>'\a',# alarm (bell) (BEL)1146"\e"=>'\e',# escape (ESC)1147"\013"=>'\v',# vertical tab (VT)1148"\000"=>'\0',# nul character (NUL)1149);1150my$chr= ( (exists$es{$cntrl})1151?$es{$cntrl}1152:sprintf('\%2x',ord($cntrl)) );1153if($opts{-nohtml}) {1154return$chr;1155}else{1156return"<span class=\"cntrl\">$chr</span>";1157}1158}11591160# Alternatively use unicode control pictures codepoints,1161# Unicode "printable representation" (PR)1162sub quot_upr {1163my$cntrl=shift;1164my%opts=@_;11651166my$chr=sprintf('&#%04d;',0x2400+ord($cntrl));1167if($opts{-nohtml}) {1168return$chr;1169}else{1170return"<span class=\"cntrl\">$chr</span>";1171}1172}11731174# git may return quoted and escaped filenames1175sub unquote {1176my$str=shift;11771178sub unq {1179my$seq=shift;1180my%es= (# character escape codes, aka escape sequences1181't'=>"\t",# tab (HT, TAB)1182'n'=>"\n",# newline (NL)1183'r'=>"\r",# return (CR)1184'f'=>"\f",# form feed (FF)1185'b'=>"\b",# backspace (BS)1186'a'=>"\a",# alarm (bell) (BEL)1187'e'=>"\e",# escape (ESC)1188'v'=>"\013",# vertical tab (VT)1189);11901191if($seq=~m/^[0-7]{1,3}$/) {1192# octal char sequence1193returnchr(oct($seq));1194}elsif(exists$es{$seq}) {1195# C escape sequence, aka character escape code1196return$es{$seq};1197}1198# quoted ordinary character1199return$seq;1200}12011202if($str=~m/^"(.*)"$/) {1203# needs unquoting1204$str=$1;1205$str=~s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;1206}1207return$str;1208}12091210# escape tabs (convert tabs to spaces)1211sub untabify {1212my$line=shift;12131214while((my$pos=index($line,"\t")) != -1) {1215if(my$count= (8- ($pos%8))) {1216my$spaces=' ' x $count;1217$line=~s/\t/$spaces/;1218}1219}12201221return$line;1222}12231224sub project_in_list {1225my$project=shift;1226my@list= git_get_projects_list();1227return@list&&scalar(grep{$_->{'path'}eq$project}@list);1228}12291230## ----------------------------------------------------------------------1231## HTML aware string manipulation12321233# Try to chop given string on a word boundary between position1234# $len and $len+$add_len. If there is no word boundary there,1235# chop at $len+$add_len. Do not chop if chopped part plus ellipsis1236# (marking chopped part) would be longer than given string.1237sub chop_str {1238my$str=shift;1239my$len=shift;1240my$add_len=shift||10;1241my$where=shift||'right';# 'left' | 'center' | 'right'12421243# Make sure perl knows it is utf8 encoded so we don't1244# cut in the middle of a utf8 multibyte char.1245$str= to_utf8($str);12461247# allow only $len chars, but don't cut a word if it would fit in $add_len1248# if it doesn't fit, cut it if it's still longer than the dots we would add1249# remove chopped character entities entirely12501251# when chopping in the middle, distribute $len into left and right part1252# return early if chopping wouldn't make string shorter1253if($whereeq'center') {1254return$strif($len+5>=length($str));# filler is length 51255$len=int($len/2);1256}else{1257return$strif($len+4>=length($str));# filler is length 41258}12591260# regexps: ending and beginning with word part up to $add_len1261my$endre=qr/.{$len}\w{0,$add_len}/;1262my$begre=qr/\w{0,$add_len}.{$len}/;12631264if($whereeq'left') {1265$str=~m/^(.*?)($begre)$/;1266my($lead,$body) = ($1,$2);1267if(length($lead) >4) {1268$body=~s/^[^;]*;//if($lead=~m/&[^;]*$/);1269$lead=" ...";1270}1271return"$lead$body";12721273}elsif($whereeq'center') {1274$str=~m/^($endre)(.*)$/;1275my($left,$str) = ($1,$2);1276$str=~m/^(.*?)($begre)$/;1277my($mid,$right) = ($1,$2);1278if(length($mid) >5) {1279$left=~s/&[^;]*$//;1280$right=~s/^[^;]*;//if($mid=~m/&[^;]*$/);1281$mid=" ... ";1282}1283return"$left$mid$right";12841285}else{1286$str=~m/^($endre)(.*)$/;1287my$body=$1;1288my$tail=$2;1289if(length($tail) >4) {1290$body=~s/&[^;]*$//;1291$tail="... ";1292}1293return"$body$tail";1294}1295}12961297# takes the same arguments as chop_str, but also wraps a <span> around the1298# result with a title attribute if it does get chopped. Additionally, the1299# string is HTML-escaped.1300sub chop_and_escape_str {1301my($str) =@_;13021303my$chopped= chop_str(@_);1304if($choppedeq$str) {1305return esc_html($chopped);1306}else{1307$str=~s/[[:cntrl:]]/?/g;1308return$cgi->span({-title=>$str}, esc_html($chopped));1309}1310}13111312## ----------------------------------------------------------------------1313## functions returning short strings13141315# CSS class for given age value (in seconds)1316sub age_class {1317my$age=shift;13181319if(!defined$age) {1320return"noage";1321}elsif($age<60*60*2) {1322return"age0";1323}elsif($age<60*60*24*2) {1324return"age1";1325}else{1326return"age2";1327}1328}13291330# convert age in seconds to "nn units ago" string1331sub age_string {1332my$age=shift;1333my$age_str;13341335if($age>60*60*24*365*2) {1336$age_str= (int$age/60/60/24/365);1337$age_str.=" years ago";1338}elsif($age>60*60*24*(365/12)*2) {1339$age_str=int$age/60/60/24/(365/12);1340$age_str.=" months ago";1341}elsif($age>60*60*24*7*2) {1342$age_str=int$age/60/60/24/7;1343$age_str.=" weeks ago";1344}elsif($age>60*60*24*2) {1345$age_str=int$age/60/60/24;1346$age_str.=" days ago";1347}elsif($age>60*60*2) {1348$age_str=int$age/60/60;1349$age_str.=" hours ago";1350}elsif($age>60*2) {1351$age_str=int$age/60;1352$age_str.=" min ago";1353}elsif($age>2) {1354$age_str=int$age;1355$age_str.=" sec ago";1356}else{1357$age_str.=" right now";1358}1359return$age_str;1360}13611362useconstant{1363 S_IFINVALID =>0030000,1364 S_IFGITLINK =>0160000,1365};13661367# submodule/subproject, a commit object reference1368sub S_ISGITLINK {1369my$mode=shift;13701371return(($mode& S_IFMT) == S_IFGITLINK)1372}13731374# convert file mode in octal to symbolic file mode string1375sub mode_str {1376my$mode=oct shift;13771378if(S_ISGITLINK($mode)) {1379return'm---------';1380}elsif(S_ISDIR($mode& S_IFMT)) {1381return'drwxr-xr-x';1382}elsif(S_ISLNK($mode)) {1383return'lrwxrwxrwx';1384}elsif(S_ISREG($mode)) {1385# git cares only about the executable bit1386if($mode& S_IXUSR) {1387return'-rwxr-xr-x';1388}else{1389return'-rw-r--r--';1390};1391}else{1392return'----------';1393}1394}13951396# convert file mode in octal to file type string1397sub file_type {1398my$mode=shift;13991400if($mode!~m/^[0-7]+$/) {1401return$mode;1402}else{1403$mode=oct$mode;1404}14051406if(S_ISGITLINK($mode)) {1407return"submodule";1408}elsif(S_ISDIR($mode& S_IFMT)) {1409return"directory";1410}elsif(S_ISLNK($mode)) {1411return"symlink";1412}elsif(S_ISREG($mode)) {1413return"file";1414}else{1415return"unknown";1416}1417}14181419# convert file mode in octal to file type description string1420sub file_type_long {1421my$mode=shift;14221423if($mode!~m/^[0-7]+$/) {1424return$mode;1425}else{1426$mode=oct$mode;1427}14281429if(S_ISGITLINK($mode)) {1430return"submodule";1431}elsif(S_ISDIR($mode& S_IFMT)) {1432return"directory";1433}elsif(S_ISLNK($mode)) {1434return"symlink";1435}elsif(S_ISREG($mode)) {1436if($mode& S_IXUSR) {1437return"executable";1438}else{1439return"file";1440};1441}else{1442return"unknown";1443}1444}144514461447## ----------------------------------------------------------------------1448## functions returning short HTML fragments, or transforming HTML fragments1449## which don't belong to other sections14501451# format line of commit message.1452sub format_log_line_html {1453my$line=shift;14541455$line= esc_html($line, -nbsp=>1);1456$line=~ s{\b([0-9a-fA-F]{8,40})\b}{1457$cgi->a({-href => href(action=>"object", hash=>$1),1458-class=>"text"},$1);1459}eg;14601461return$line;1462}14631464# format marker of refs pointing to given object14651466# the destination action is chosen based on object type and current context:1467# - for annotated tags, we choose the tag view unless it's the current view1468# already, in which case we go to shortlog view1469# - for other refs, we keep the current view if we're in history, shortlog or1470# log view, and select shortlog otherwise1471sub format_ref_marker {1472my($refs,$id) =@_;1473my$markers='';14741475if(defined$refs->{$id}) {1476foreachmy$ref(@{$refs->{$id}}) {1477# this code exploits the fact that non-lightweight tags are the1478# only indirect objects, and that they are the only objects for which1479# we want to use tag instead of shortlog as action1480my($type,$name) =qw();1481my$indirect= ($ref=~s/\^\{\}$//);1482# e.g. tags/v2.6.11 or heads/next1483if($ref=~m!^(.*?)s?/(.*)$!) {1484$type=$1;1485$name=$2;1486}else{1487$type="ref";1488$name=$ref;1489}14901491my$class=$type;1492$class.=" indirect"if$indirect;14931494my$dest_action="shortlog";14951496if($indirect) {1497$dest_action="tag"unless$actioneq"tag";1498}elsif($action=~/^(history|(short)?log)$/) {1499$dest_action=$action;1500}15011502my$dest="";1503$dest.="refs/"unless$ref=~ m!^refs/!;1504$dest.=$ref;15051506my$link=$cgi->a({1507-href => href(1508 action=>$dest_action,1509 hash=>$dest1510)},$name);15111512$markers.=" <span class=\"$class\"title=\"$ref\">".1513$link."</span>";1514}1515}15161517if($markers) {1518return' <span class="refs">'.$markers.'</span>';1519}else{1520return"";1521}1522}15231524# format, perhaps shortened and with markers, title line1525sub format_subject_html {1526my($long,$short,$href,$extra) =@_;1527$extra=''unlessdefined($extra);15281529if(length($short) <length($long)) {1530$long=~s/[[:cntrl:]]/?/g;1531return$cgi->a({-href =>$href, -class=>"list subject",1532-title => to_utf8($long)},1533 esc_html($short) .$extra);1534}else{1535return$cgi->a({-href =>$href, -class=>"list subject"},1536 esc_html($long) .$extra);1537}1538}15391540# Rather than recomputing the url for an email multiple times, we cache it1541# after the first hit. This gives a visible benefit in views where the avatar1542# for the same email is used repeatedly (e.g. shortlog).1543# The cache is shared by all avatar engines (currently gravatar only), which1544# are free to use it as preferred. Since only one avatar engine is used for any1545# given page, there's no risk for cache conflicts.1546our%avatar_cache= ();15471548# Compute the picon url for a given email, by using the picon search service over at1549# http://www.cs.indiana.edu/picons/search.html1550sub picon_url {1551my$email=lc shift;1552if(!$avatar_cache{$email}) {1553my($user,$domain) =split('@',$email);1554$avatar_cache{$email} =1555"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/".1556"$domain/$user/".1557"users+domains+unknown/up/single";1558}1559return$avatar_cache{$email};1560}15611562# Compute the gravatar url for a given email, if it's not in the cache already.1563# Gravatar stores only the part of the URL before the size, since that's the1564# one computationally more expensive. This also allows reuse of the cache for1565# different sizes (for this particular engine).1566sub gravatar_url {1567my$email=lc shift;1568my$size=shift;1569$avatar_cache{$email} ||=1570"http://www.gravatar.com/avatar/".1571 Digest::MD5::md5_hex($email) ."?s=";1572return$avatar_cache{$email} .$size;1573}15741575# Insert an avatar for the given $email at the given $size if the feature1576# is enabled.1577sub git_get_avatar {1578my($email,%opts) =@_;1579my$pre_white= ($opts{-pad_before} ?" ":"");1580my$post_white= ($opts{-pad_after} ?" ":"");1581$opts{-size} ||='default';1582my$size=$avatar_size{$opts{-size}} ||$avatar_size{'default'};1583my$url="";1584if($git_avatareq'gravatar') {1585$url= gravatar_url($email,$size);1586}elsif($git_avatareq'picon') {1587$url= picon_url($email);1588}1589# Other providers can be added by extending the if chain, defining $url1590# as needed. If no variant puts something in $url, we assume avatars1591# are completely disabled/unavailable.1592if($url) {1593return$pre_white.1594"<img width=\"$size\"".1595"class=\"avatar\"".1596"src=\"$url\"".1597"alt=\"\"".1598"/>".$post_white;1599}else{1600return"";1601}1602}16031604# format the author name of the given commit with the given tag1605# the author name is chopped and escaped according to the other1606# optional parameters (see chop_str).1607sub format_author_html {1608my$tag=shift;1609my$co=shift;1610my$author= chop_and_escape_str($co->{'author_name'},@_);1611return"<$tagclass=\"author\">".1612 git_get_avatar($co->{'author_email'}, -pad_after =>1) .1613$author."</$tag>";1614}16151616# format git diff header line, i.e. "diff --(git|combined|cc) ..."1617sub format_git_diff_header_line {1618my$line=shift;1619my$diffinfo=shift;1620my($from,$to) =@_;16211622if($diffinfo->{'nparents'}) {1623# combined diff1624$line=~s!^(diff (.*?) )"?.*$!$1!;1625if($to->{'href'}) {1626$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1627 esc_path($to->{'file'}));1628}else{# file was deleted (no href)1629$line.= esc_path($to->{'file'});1630}1631}else{1632# "ordinary" diff1633$line=~s!^(diff (.*?) )"?a/.*$!$1!;1634if($from->{'href'}) {1635$line.=$cgi->a({-href =>$from->{'href'}, -class=>"path"},1636'a/'. esc_path($from->{'file'}));1637}else{# file was added (no href)1638$line.='a/'. esc_path($from->{'file'});1639}1640$line.=' ';1641if($to->{'href'}) {1642$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1643'b/'. esc_path($to->{'file'}));1644}else{# file was deleted1645$line.='b/'. esc_path($to->{'file'});1646}1647}16481649return"<div class=\"diff header\">$line</div>\n";1650}16511652# format extended diff header line, before patch itself1653sub format_extended_diff_header_line {1654my$line=shift;1655my$diffinfo=shift;1656my($from,$to) =@_;16571658# match <path>1659if($line=~s!^((copy|rename) from ).*$!$1!&&$from->{'href'}) {1660$line.=$cgi->a({-href=>$from->{'href'}, -class=>"path"},1661 esc_path($from->{'file'}));1662}1663if($line=~s!^((copy|rename) to ).*$!$1!&&$to->{'href'}) {1664$line.=$cgi->a({-href=>$to->{'href'}, -class=>"path"},1665 esc_path($to->{'file'}));1666}1667# match single <mode>1668if($line=~m/\s(\d{6})$/) {1669$line.='<span class="info"> ('.1670 file_type_long($1) .1671')</span>';1672}1673# match <hash>1674if($line=~m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {1675# can match only for combined diff1676$line='index ';1677for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1678if($from->{'href'}[$i]) {1679$line.=$cgi->a({-href=>$from->{'href'}[$i],1680-class=>"hash"},1681substr($diffinfo->{'from_id'}[$i],0,7));1682}else{1683$line.='0' x 7;1684}1685# separator1686$line.=','if($i<$diffinfo->{'nparents'} -1);1687}1688$line.='..';1689if($to->{'href'}) {1690$line.=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1691substr($diffinfo->{'to_id'},0,7));1692}else{1693$line.='0' x 7;1694}16951696}elsif($line=~m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {1697# can match only for ordinary diff1698my($from_link,$to_link);1699if($from->{'href'}) {1700$from_link=$cgi->a({-href=>$from->{'href'}, -class=>"hash"},1701substr($diffinfo->{'from_id'},0,7));1702}else{1703$from_link='0' x 7;1704}1705if($to->{'href'}) {1706$to_link=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1707substr($diffinfo->{'to_id'},0,7));1708}else{1709$to_link='0' x 7;1710}1711my($from_id,$to_id) = ($diffinfo->{'from_id'},$diffinfo->{'to_id'});1712$line=~s!$from_id\.\.$to_id!$from_link..$to_link!;1713}17141715return$line."<br/>\n";1716}17171718# format from-file/to-file diff header1719sub format_diff_from_to_header {1720my($from_line,$to_line,$diffinfo,$from,$to,@parents) =@_;1721my$line;1722my$result='';17231724$line=$from_line;1725#assert($line =~ m/^---/) if DEBUG;1726# no extra formatting for "^--- /dev/null"1727if(!$diffinfo->{'nparents'}) {1728# ordinary (single parent) diff1729if($line=~m!^--- "?a/!) {1730if($from->{'href'}) {1731$line='--- a/'.1732$cgi->a({-href=>$from->{'href'}, -class=>"path"},1733 esc_path($from->{'file'}));1734}else{1735$line='--- a/'.1736 esc_path($from->{'file'});1737}1738}1739$result.= qq!<div class="diff from_file">$line</div>\n!;17401741}else{1742# combined diff (merge commit)1743for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1744if($from->{'href'}[$i]) {1745$line='--- '.1746$cgi->a({-href=>href(action=>"blobdiff",1747 hash_parent=>$diffinfo->{'from_id'}[$i],1748 hash_parent_base=>$parents[$i],1749 file_parent=>$from->{'file'}[$i],1750 hash=>$diffinfo->{'to_id'},1751 hash_base=>$hash,1752 file_name=>$to->{'file'}),1753-class=>"path",1754-title=>"diff". ($i+1)},1755$i+1) .1756'/'.1757$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},1758 esc_path($from->{'file'}[$i]));1759}else{1760$line='--- /dev/null';1761}1762$result.= qq!<div class="diff from_file">$line</div>\n!;1763}1764}17651766$line=$to_line;1767#assert($line =~ m/^\+\+\+/) if DEBUG;1768# no extra formatting for "^+++ /dev/null"1769if($line=~m!^\+\+\+ "?b/!) {1770if($to->{'href'}) {1771$line='+++ b/'.1772$cgi->a({-href=>$to->{'href'}, -class=>"path"},1773 esc_path($to->{'file'}));1774}else{1775$line='+++ b/'.1776 esc_path($to->{'file'});1777}1778}1779$result.= qq!<div class="diff to_file">$line</div>\n!;17801781return$result;1782}17831784# create note for patch simplified by combined diff1785sub format_diff_cc_simplified {1786my($diffinfo,@parents) =@_;1787my$result='';17881789$result.="<div class=\"diff header\">".1790"diff --cc ";1791if(!is_deleted($diffinfo)) {1792$result.=$cgi->a({-href => href(action=>"blob",1793 hash_base=>$hash,1794 hash=>$diffinfo->{'to_id'},1795 file_name=>$diffinfo->{'to_file'}),1796-class=>"path"},1797 esc_path($diffinfo->{'to_file'}));1798}else{1799$result.= esc_path($diffinfo->{'to_file'});1800}1801$result.="</div>\n".# class="diff header"1802"<div class=\"diff nodifferences\">".1803"Simple merge".1804"</div>\n";# class="diff nodifferences"18051806return$result;1807}18081809# format patch (diff) line (not to be used for diff headers)1810sub format_diff_line {1811my$line=shift;1812my($from,$to) =@_;1813my$diff_class="";18141815chomp$line;18161817if($from&&$to&&ref($from->{'href'})eq"ARRAY") {1818# combined diff1819my$prefix=substr($line,0,scalar@{$from->{'href'}});1820if($line=~m/^\@{3}/) {1821$diff_class=" chunk_header";1822}elsif($line=~m/^\\/) {1823$diff_class=" incomplete";1824}elsif($prefix=~tr/+/+/) {1825$diff_class=" add";1826}elsif($prefix=~tr/-/-/) {1827$diff_class=" rem";1828}1829}else{1830# assume ordinary diff1831my$char=substr($line,0,1);1832if($chareq'+') {1833$diff_class=" add";1834}elsif($chareq'-') {1835$diff_class=" rem";1836}elsif($chareq'@') {1837$diff_class=" chunk_header";1838}elsif($chareq"\\") {1839$diff_class=" incomplete";1840}1841}1842$line= untabify($line);1843if($from&&$to&&$line=~m/^\@{2} /) {1844my($from_text,$from_start,$from_lines,$to_text,$to_start,$to_lines,$section) =1845$line=~m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;18461847$from_lines=0unlessdefined$from_lines;1848$to_lines=0unlessdefined$to_lines;18491850if($from->{'href'}) {1851$from_text=$cgi->a({-href=>"$from->{'href'}#l$from_start",1852-class=>"list"},$from_text);1853}1854if($to->{'href'}) {1855$to_text=$cgi->a({-href=>"$to->{'href'}#l$to_start",1856-class=>"list"},$to_text);1857}1858$line="<span class=\"chunk_info\">@@$from_text$to_text@@</span>".1859"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";1860return"<div class=\"diff$diff_class\">$line</div>\n";1861}elsif($from&&$to&&$line=~m/^\@{3}/) {1862my($prefix,$ranges,$section) =$line=~m/^(\@+) (.*?) \@+(.*)$/;1863my(@from_text,@from_start,@from_nlines,$to_text,$to_start,$to_nlines);18641865@from_text=split(' ',$ranges);1866for(my$i=0;$i<@from_text; ++$i) {1867($from_start[$i],$from_nlines[$i]) =1868(split(',',substr($from_text[$i],1)),0);1869}18701871$to_text=pop@from_text;1872$to_start=pop@from_start;1873$to_nlines=pop@from_nlines;18741875$line="<span class=\"chunk_info\">$prefix";1876for(my$i=0;$i<@from_text; ++$i) {1877if($from->{'href'}[$i]) {1878$line.=$cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",1879-class=>"list"},$from_text[$i]);1880}else{1881$line.=$from_text[$i];1882}1883$line.=" ";1884}1885if($to->{'href'}) {1886$line.=$cgi->a({-href=>"$to->{'href'}#l$to_start",1887-class=>"list"},$to_text);1888}else{1889$line.=$to_text;1890}1891$line.="$prefix</span>".1892"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";1893return"<div class=\"diff$diff_class\">$line</div>\n";1894}1895return"<div class=\"diff$diff_class\">". esc_html($line, -nbsp=>1) ."</div>\n";1896}18971898# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",1899# linked. Pass the hash of the tree/commit to snapshot.1900sub format_snapshot_links {1901my($hash) =@_;1902my$num_fmts=@snapshot_fmts;1903if($num_fmts>1) {1904# A parenthesized list of links bearing format names.1905# e.g. "snapshot (_tar.gz_ _zip_)"1906return"snapshot (".join(' ',map1907$cgi->a({1908-href => href(1909 action=>"snapshot",1910 hash=>$hash,1911 snapshot_format=>$_1912)1913},$known_snapshot_formats{$_}{'display'})1914,@snapshot_fmts) .")";1915}elsif($num_fmts==1) {1916# A single "snapshot" link whose tooltip bears the format name.1917# i.e. "_snapshot_"1918my($fmt) =@snapshot_fmts;1919return1920$cgi->a({1921-href => href(1922 action=>"snapshot",1923 hash=>$hash,1924 snapshot_format=>$fmt1925),1926-title =>"in format:$known_snapshot_formats{$fmt}{'display'}"1927},"snapshot");1928}else{# $num_fmts == 01929returnundef;1930}1931}19321933## ......................................................................1934## functions returning values to be passed, perhaps after some1935## transformation, to other functions; e.g. returning arguments to href()19361937# returns hash to be passed to href to generate gitweb URL1938# in -title key it returns description of link1939sub get_feed_info {1940my$format=shift||'Atom';1941my%res= (action =>lc($format));19421943# feed links are possible only for project views1944return unless(defined$project);1945# some views should link to OPML, or to generic project feed,1946# or don't have specific feed yet (so they should use generic)1947return if($action=~/^(?:tags|heads|forks|tag|search)$/x);19481949my$branch;1950# branches refs uses 'refs/heads/' prefix (fullname) to differentiate1951# from tag links; this also makes possible to detect branch links1952if((defined$hash_base&&$hash_base=~m!^refs/heads/(.*)$!) ||1953(defined$hash&&$hash=~m!^refs/heads/(.*)$!)) {1954$branch=$1;1955}1956# find log type for feed description (title)1957my$type='log';1958if(defined$file_name) {1959$type="history of$file_name";1960$type.="/"if($actioneq'tree');1961$type.=" on '$branch'"if(defined$branch);1962}else{1963$type="log of$branch"if(defined$branch);1964}19651966$res{-title} =$type;1967$res{'hash'} = (defined$branch?"refs/heads/$branch":undef);1968$res{'file_name'} =$file_name;19691970return%res;1971}19721973## ----------------------------------------------------------------------1974## git utility subroutines, invoking git commands19751976# returns path to the core git executable and the --git-dir parameter as list1977sub git_cmd {1978$number_of_git_cmds++;1979return$GIT,'--git-dir='.$git_dir;1980}19811982# quote the given arguments for passing them to the shell1983# quote_command("command", "arg 1", "arg with ' and ! characters")1984# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"1985# Try to avoid using this function wherever possible.1986sub quote_command {1987returnjoin(' ',1988map{my$a=$_;$a=~s/(['!])/'\\$1'/g;"'$a'"}@_);1989}19901991# get HEAD ref of given project as hash1992sub git_get_head_hash {1993my$project=shift;1994my$o_git_dir=$git_dir;1995my$retval=undef;1996$git_dir="$projectroot/$project";1997if(open my$fd,"-|", git_cmd(),"rev-parse","--verify","HEAD") {1998my$head= <$fd>;1999close$fd;2000if(defined$head&&$head=~/^([0-9a-fA-F]{40})$/) {2001$retval=$1;2002}2003}2004if(defined$o_git_dir) {2005$git_dir=$o_git_dir;2006}2007return$retval;2008}20092010# get type of given object2011sub git_get_type {2012my$hash=shift;20132014open my$fd,"-|", git_cmd(),"cat-file",'-t',$hashorreturn;2015my$type= <$fd>;2016close$fdorreturn;2017chomp$type;2018return$type;2019}20202021# repository configuration2022our$config_file='';2023our%config;20242025# store multiple values for single key as anonymous array reference2026# single values stored directly in the hash, not as [ <value> ]2027sub hash_set_multi {2028my($hash,$key,$value) =@_;20292030if(!exists$hash->{$key}) {2031$hash->{$key} =$value;2032}elsif(!ref$hash->{$key}) {2033$hash->{$key} = [$hash->{$key},$value];2034}else{2035push@{$hash->{$key}},$value;2036}2037}20382039# return hash of git project configuration2040# optionally limited to some section, e.g. 'gitweb'2041sub git_parse_project_config {2042my$section_regexp=shift;2043my%config;20442045local$/="\0";20462047open my$fh,"-|", git_cmd(),"config",'-z','-l',2048orreturn;20492050while(my$keyval= <$fh>) {2051chomp$keyval;2052my($key,$value) =split(/\n/,$keyval,2);20532054 hash_set_multi(\%config,$key,$value)2055if(!defined$section_regexp||$key=~/^(?:$section_regexp)\./o);2056}2057close$fh;20582059return%config;2060}20612062# convert config value to boolean: 'true' or 'false'2063# no value, number > 0, 'true' and 'yes' values are true2064# rest of values are treated as false (never as error)2065sub config_to_bool {2066my$val=shift;20672068return1if!defined$val;# section.key20692070# strip leading and trailing whitespace2071$val=~s/^\s+//;2072$val=~s/\s+$//;20732074return(($val=~/^\d+$/&&$val) ||# section.key = 12075($val=~/^(?:true|yes)$/i));# section.key = true2076}20772078# convert config value to simple decimal number2079# an optional value suffix of 'k', 'm', or 'g' will cause the value2080# to be multiplied by 1024, 1048576, or 10737418242081sub config_to_int {2082my$val=shift;20832084# strip leading and trailing whitespace2085$val=~s/^\s+//;2086$val=~s/\s+$//;20872088if(my($num,$unit) = ($val=~/^([0-9]*)([kmg])$/i)) {2089$unit=lc($unit);2090# unknown unit is treated as 12091return$num* ($uniteq'g'?1073741824:2092$uniteq'm'?1048576:2093$uniteq'k'?1024:1);2094}2095return$val;2096}20972098# convert config value to array reference, if needed2099sub config_to_multi {2100my$val=shift;21012102returnref($val) ?$val: (defined($val) ? [$val] : []);2103}21042105sub git_get_project_config {2106my($key,$type) =@_;21072108# key sanity check2109return unless($key);2110$key=~s/^gitweb\.//;2111return if($key=~m/\W/);21122113# type sanity check2114if(defined$type) {2115$type=~s/^--//;2116$type=undef2117unless($typeeq'bool'||$typeeq'int');2118}21192120# get config2121if(!defined$config_file||2122$config_filene"$git_dir/config") {2123%config= git_parse_project_config('gitweb');2124$config_file="$git_dir/config";2125}21262127# check if config variable (key) exists2128return unlessexists$config{"gitweb.$key"};21292130# ensure given type2131if(!defined$type) {2132return$config{"gitweb.$key"};2133}elsif($typeeq'bool') {2134# backward compatibility: 'git config --bool' returns true/false2135return config_to_bool($config{"gitweb.$key"}) ?'true':'false';2136}elsif($typeeq'int') {2137return config_to_int($config{"gitweb.$key"});2138}2139return$config{"gitweb.$key"};2140}21412142# get hash of given path at given ref2143sub git_get_hash_by_path {2144my$base=shift;2145my$path=shift||returnundef;2146my$type=shift;21472148$path=~ s,/+$,,;21492150open my$fd,"-|", git_cmd(),"ls-tree",$base,"--",$path2151or die_error(500,"Open git-ls-tree failed");2152my$line= <$fd>;2153close$fdorreturnundef;21542155if(!defined$line) {2156# there is no tree or hash given by $path at $base2157returnundef;2158}21592160#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2161$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;2162if(defined$type&&$typene$2) {2163# type doesn't match2164returnundef;2165}2166return$3;2167}21682169# get path of entry with given hash at given tree-ish (ref)2170# used to get 'from' filename for combined diff (merge commit) for renames2171sub git_get_path_by_hash {2172my$base=shift||return;2173my$hash=shift||return;21742175local$/="\0";21762177open my$fd,"-|", git_cmd(),"ls-tree",'-r','-t','-z',$base2178orreturnundef;2179while(my$line= <$fd>) {2180chomp$line;21812182#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'2183#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'2184if($line=~m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {2185close$fd;2186return$1;2187}2188}2189close$fd;2190returnundef;2191}21922193## ......................................................................2194## git utility functions, directly accessing git repository21952196sub git_get_project_description {2197my$path=shift;21982199$git_dir="$projectroot/$path";2200open my$fd,'<',"$git_dir/description"2201orreturn git_get_project_config('description');2202my$descr= <$fd>;2203close$fd;2204if(defined$descr) {2205chomp$descr;2206}2207return$descr;2208}22092210sub git_get_project_ctags {2211my$path=shift;2212my$ctags= {};22132214$git_dir="$projectroot/$path";2215opendir my$dh,"$git_dir/ctags"2216orreturn$ctags;2217foreach(grep{ -f $_}map{"$git_dir/ctags/$_"}readdir($dh)) {2218open my$ct,'<',$_ornext;2219my$val= <$ct>;2220chomp$val;2221close$ct;2222my$ctag=$_;$ctag=~ s#.*/##;2223$ctags->{$ctag} =$val;2224}2225closedir$dh;2226$ctags;2227}22282229sub git_populate_project_tagcloud {2230my$ctags=shift;22312232# First, merge different-cased tags; tags vote on casing2233my%ctags_lc;2234foreach(keys%$ctags) {2235$ctags_lc{lc$_}->{count} +=$ctags->{$_};2236if(not$ctags_lc{lc$_}->{topcount}2237or$ctags_lc{lc$_}->{topcount} <$ctags->{$_}) {2238$ctags_lc{lc$_}->{topcount} =$ctags->{$_};2239$ctags_lc{lc$_}->{topname} =$_;2240}2241}22422243my$cloud;2244if(eval{require HTML::TagCloud;1; }) {2245$cloud= HTML::TagCloud->new;2246foreach(sort keys%ctags_lc) {2247# Pad the title with spaces so that the cloud looks2248# less crammed.2249my$title=$ctags_lc{$_}->{topname};2250$title=~s/ / /g;2251$title=~s/^/ /g;2252$title=~s/$/ /g;2253$cloud->add($title,$home_link."?by_tag=".$_,$ctags_lc{$_}->{count});2254}2255}else{2256$cloud= \%ctags_lc;2257}2258$cloud;2259}22602261sub git_show_project_tagcloud {2262my($cloud,$count) =@_;2263print STDERR ref($cloud)."..\n";2264if(ref$cloudeq'HTML::TagCloud') {2265return$cloud->html_and_css($count);2266}else{2267my@tags=sort{$cloud->{$a}->{count} <=>$cloud->{$b}->{count} }keys%$cloud;2268return'<p align="center">'.join(', ',map{2269"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"2270}splice(@tags,0,$count)) .'</p>';2271}2272}22732274sub git_get_project_url_list {2275my$path=shift;22762277$git_dir="$projectroot/$path";2278open my$fd,'<',"$git_dir/cloneurl"2279orreturnwantarray?2280@{ config_to_multi(git_get_project_config('url')) } :2281 config_to_multi(git_get_project_config('url'));2282my@git_project_url_list=map{chomp;$_} <$fd>;2283close$fd;22842285returnwantarray?@git_project_url_list: \@git_project_url_list;2286}22872288sub git_get_projects_list {2289my($filter) =@_;2290my@list;22912292$filter||='';2293$filter=~s/\.git$//;22942295my$check_forks= gitweb_check_feature('forks');22962297if(-d $projects_list) {2298# search in directory2299my$dir=$projects_list. ($filter?"/$filter":'');2300# remove the trailing "/"2301$dir=~s!/+$!!;2302my$pfxlen=length("$dir");2303my$pfxdepth= ($dir=~tr!/!!);23042305 File::Find::find({2306 follow_fast =>1,# follow symbolic links2307 follow_skip =>2,# ignore duplicates2308 dangling_symlinks =>0,# ignore dangling symlinks, silently2309 wanted =>sub{2310# skip project-list toplevel, if we get it.2311return if(m!^[/.]$!);2312# only directories can be git repositories2313return unless(-d $_);2314# don't traverse too deep (Find is super slow on os x)2315if(($File::Find::name =~tr!/!!) -$pfxdepth>$project_maxdepth) {2316$File::Find::prune =1;2317return;2318}23192320my$subdir=substr($File::Find::name,$pfxlen+1);2321# we check related file in $projectroot2322my$path= ($filter?"$filter/":'') .$subdir;2323if(check_export_ok("$projectroot/$path")) {2324push@list, { path =>$path};2325$File::Find::prune =1;2326}2327},2328},"$dir");23292330}elsif(-f $projects_list) {2331# read from file(url-encoded):2332# 'git%2Fgit.git Linus+Torvalds'2333# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2334# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2335my%paths;2336open my$fd,'<',$projects_listorreturn;2337 PROJECT:2338while(my$line= <$fd>) {2339chomp$line;2340my($path,$owner) =split' ',$line;2341$path= unescape($path);2342$owner= unescape($owner);2343if(!defined$path) {2344next;2345}2346if($filterne'') {2347# looking for forks;2348my$pfx=substr($path,0,length($filter));2349if($pfxne$filter) {2350next PROJECT;2351}2352my$sfx=substr($path,length($filter));2353if($sfx!~/^\/.*\.git$/) {2354next PROJECT;2355}2356}elsif($check_forks) {2357 PATH:2358foreachmy$filter(keys%paths) {2359# looking for forks;2360my$pfx=substr($path,0,length($filter));2361if($pfxne$filter) {2362next PATH;2363}2364my$sfx=substr($path,length($filter));2365if($sfx!~/^\/.*\.git$/) {2366next PATH;2367}2368# is a fork, don't include it in2369# the list2370next PROJECT;2371}2372}2373if(check_export_ok("$projectroot/$path")) {2374my$pr= {2375 path =>$path,2376 owner => to_utf8($owner),2377};2378push@list,$pr;2379(my$forks_path=$path) =~s/\.git$//;2380$paths{$forks_path}++;2381}2382}2383close$fd;2384}2385return@list;2386}23872388our$gitweb_project_owner=undef;2389sub git_get_project_list_from_file {23902391return if(defined$gitweb_project_owner);23922393$gitweb_project_owner= {};2394# read from file (url-encoded):2395# 'git%2Fgit.git Linus+Torvalds'2396# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2397# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2398if(-f $projects_list) {2399open(my$fd,'<',$projects_list);2400while(my$line= <$fd>) {2401chomp$line;2402my($pr,$ow) =split' ',$line;2403$pr= unescape($pr);2404$ow= unescape($ow);2405$gitweb_project_owner->{$pr} = to_utf8($ow);2406}2407close$fd;2408}2409}24102411sub git_get_project_owner {2412my$project=shift;2413my$owner;24142415returnundefunless$project;2416$git_dir="$projectroot/$project";24172418if(!defined$gitweb_project_owner) {2419 git_get_project_list_from_file();2420}24212422if(exists$gitweb_project_owner->{$project}) {2423$owner=$gitweb_project_owner->{$project};2424}2425if(!defined$owner){2426$owner= git_get_project_config('owner');2427}2428if(!defined$owner) {2429$owner= get_file_owner("$git_dir");2430}24312432return$owner;2433}24342435sub git_get_last_activity {2436my($path) =@_;2437my$fd;24382439$git_dir="$projectroot/$path";2440open($fd,"-|", git_cmd(),'for-each-ref',2441'--format=%(committer)',2442'--sort=-committerdate',2443'--count=1',2444'refs/heads')orreturn;2445my$most_recent= <$fd>;2446close$fdorreturn;2447if(defined$most_recent&&2448$most_recent=~/ (\d+) [-+][01]\d\d\d$/) {2449my$timestamp=$1;2450my$age=time-$timestamp;2451return($age, age_string($age));2452}2453return(undef,undef);2454}24552456sub git_get_references {2457my$type=shift||"";2458my%refs;2459# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.112460# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}2461open my$fd,"-|", git_cmd(),"show-ref","--dereference",2462($type? ("--","refs/$type") : ())# use -- <pattern> if $type2463orreturn;24642465while(my$line= <$fd>) {2466chomp$line;2467if($line=~m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {2468if(defined$refs{$1}) {2469push@{$refs{$1}},$2;2470}else{2471$refs{$1} = [$2];2472}2473}2474}2475close$fdorreturn;2476return \%refs;2477}24782479sub git_get_rev_name_tags {2480my$hash=shift||returnundef;24812482open my$fd,"-|", git_cmd(),"name-rev","--tags",$hash2483orreturn;2484my$name_rev= <$fd>;2485close$fd;24862487if($name_rev=~ m|^$hash tags/(.*)$|) {2488return$1;2489}else{2490# catches also '$hash undefined' output2491returnundef;2492}2493}24942495## ----------------------------------------------------------------------2496## parse to hash functions24972498sub parse_date {2499my$epoch=shift;2500my$tz=shift||"-0000";25012502my%date;2503my@months= ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");2504my@days= ("Sun","Mon","Tue","Wed","Thu","Fri","Sat");2505my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($epoch);2506$date{'hour'} =$hour;2507$date{'minute'} =$min;2508$date{'mday'} =$mday;2509$date{'day'} =$days[$wday];2510$date{'month'} =$months[$mon];2511$date{'rfc2822'} =sprintf"%s,%d%s%4d%02d:%02d:%02d+0000",2512$days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec;2513$date{'mday-time'} =sprintf"%d%s%02d:%02d",2514$mday,$months[$mon],$hour,$min;2515$date{'iso-8601'} =sprintf"%04d-%02d-%02dT%02d:%02d:%02dZ",25161900+$year,1+$mon,$mday,$hour,$min,$sec;25172518$tz=~m/^([+\-][0-9][0-9])([0-9][0-9])$/;2519my$local=$epoch+ ((int$1+ ($2/60)) *3600);2520($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($local);2521$date{'hour_local'} =$hour;2522$date{'minute_local'} =$min;2523$date{'tz_local'} =$tz;2524$date{'iso-tz'} =sprintf("%04d-%02d-%02d%02d:%02d:%02d%s",25251900+$year,$mon+1,$mday,2526$hour,$min,$sec,$tz);2527return%date;2528}25292530sub parse_tag {2531my$tag_id=shift;2532my%tag;2533my@comment;25342535open my$fd,"-|", git_cmd(),"cat-file","tag",$tag_idorreturn;2536$tag{'id'} =$tag_id;2537while(my$line= <$fd>) {2538chomp$line;2539if($line=~m/^object ([0-9a-fA-F]{40})$/) {2540$tag{'object'} =$1;2541}elsif($line=~m/^type (.+)$/) {2542$tag{'type'} =$1;2543}elsif($line=~m/^tag (.+)$/) {2544$tag{'name'} =$1;2545}elsif($line=~m/^tagger (.*) ([0-9]+) (.*)$/) {2546$tag{'author'} =$1;2547$tag{'author_epoch'} =$2;2548$tag{'author_tz'} =$3;2549if($tag{'author'} =~m/^([^<]+) <([^>]*)>/) {2550$tag{'author_name'} =$1;2551$tag{'author_email'} =$2;2552}else{2553$tag{'author_name'} =$tag{'author'};2554}2555}elsif($line=~m/--BEGIN/) {2556push@comment,$line;2557last;2558}elsif($lineeq"") {2559last;2560}2561}2562push@comment, <$fd>;2563$tag{'comment'} = \@comment;2564close$fdorreturn;2565if(!defined$tag{'name'}) {2566return2567};2568return%tag2569}25702571sub parse_commit_text {2572my($commit_text,$withparents) =@_;2573my@commit_lines=split'\n',$commit_text;2574my%co;25752576pop@commit_lines;# Remove '\0'25772578if(!@commit_lines) {2579return;2580}25812582my$header=shift@commit_lines;2583if($header!~m/^[0-9a-fA-F]{40}/) {2584return;2585}2586($co{'id'},my@parents) =split' ',$header;2587while(my$line=shift@commit_lines) {2588last if$lineeq"\n";2589if($line=~m/^tree ([0-9a-fA-F]{40})$/) {2590$co{'tree'} =$1;2591}elsif((!defined$withparents) && ($line=~m/^parent ([0-9a-fA-F]{40})$/)) {2592push@parents,$1;2593}elsif($line=~m/^author (.*) ([0-9]+) (.*)$/) {2594$co{'author'} =$1;2595$co{'author_epoch'} =$2;2596$co{'author_tz'} =$3;2597if($co{'author'} =~m/^([^<]+) <([^>]*)>/) {2598$co{'author_name'} =$1;2599$co{'author_email'} =$2;2600}else{2601$co{'author_name'} =$co{'author'};2602}2603}elsif($line=~m/^committer (.*) ([0-9]+) (.*)$/) {2604$co{'committer'} =$1;2605$co{'committer_epoch'} =$2;2606$co{'committer_tz'} =$3;2607$co{'committer_name'} =$co{'committer'};2608if($co{'committer'} =~m/^([^<]+) <([^>]*)>/) {2609$co{'committer_name'} =$1;2610$co{'committer_email'} =$2;2611}else{2612$co{'committer_name'} =$co{'committer'};2613}2614}2615}2616if(!defined$co{'tree'}) {2617return;2618};2619$co{'parents'} = \@parents;2620$co{'parent'} =$parents[0];26212622foreachmy$title(@commit_lines) {2623$title=~s/^ //;2624if($titlene"") {2625$co{'title'} = chop_str($title,80,5);2626# remove leading stuff of merges to make the interesting part visible2627if(length($title) >50) {2628$title=~s/^Automatic //;2629$title=~s/^merge (of|with) /Merge ... /i;2630if(length($title) >50) {2631$title=~s/(http|rsync):\/\///;2632}2633if(length($title) >50) {2634$title=~s/(master|www|rsync)\.//;2635}2636if(length($title) >50) {2637$title=~s/kernel.org:?//;2638}2639if(length($title) >50) {2640$title=~s/\/pub\/scm//;2641}2642}2643$co{'title_short'} = chop_str($title,50,5);2644last;2645}2646}2647if(!defined$co{'title'} ||$co{'title'}eq"") {2648$co{'title'} =$co{'title_short'} ='(no commit message)';2649}2650# remove added spaces2651foreachmy$line(@commit_lines) {2652$line=~s/^ //;2653}2654$co{'comment'} = \@commit_lines;26552656my$age=time-$co{'committer_epoch'};2657$co{'age'} =$age;2658$co{'age_string'} = age_string($age);2659my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($co{'committer_epoch'});2660if($age>60*60*24*7*2) {2661$co{'age_string_date'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2662$co{'age_string_age'} =$co{'age_string'};2663}else{2664$co{'age_string_date'} =$co{'age_string'};2665$co{'age_string_age'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2666}2667return%co;2668}26692670sub parse_commit {2671my($commit_id) =@_;2672my%co;26732674local$/="\0";26752676open my$fd,"-|", git_cmd(),"rev-list",2677"--parents",2678"--header",2679"--max-count=1",2680$commit_id,2681"--",2682or die_error(500,"Open git-rev-list failed");2683%co= parse_commit_text(<$fd>,1);2684close$fd;26852686return%co;2687}26882689sub parse_commits {2690my($commit_id,$maxcount,$skip,$filename,@args) =@_;2691my@cos;26922693$maxcount||=1;2694$skip||=0;26952696local$/="\0";26972698open my$fd,"-|", git_cmd(),"rev-list",2699"--header",2700@args,2701("--max-count=".$maxcount),2702("--skip=".$skip),2703@extra_options,2704$commit_id,2705"--",2706($filename? ($filename) : ())2707or die_error(500,"Open git-rev-list failed");2708while(my$line= <$fd>) {2709my%co= parse_commit_text($line);2710push@cos, \%co;2711}2712close$fd;27132714returnwantarray?@cos: \@cos;2715}27162717# parse line of git-diff-tree "raw" output2718sub parse_difftree_raw_line {2719my$line=shift;2720my%res;27212722# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'2723# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'2724if($line=~m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {2725$res{'from_mode'} =$1;2726$res{'to_mode'} =$2;2727$res{'from_id'} =$3;2728$res{'to_id'} =$4;2729$res{'status'} =$5;2730$res{'similarity'} =$6;2731if($res{'status'}eq'R'||$res{'status'}eq'C') {# renamed or copied2732($res{'from_file'},$res{'to_file'}) =map{ unquote($_) }split("\t",$7);2733}else{2734$res{'from_file'} =$res{'to_file'} =$res{'file'} = unquote($7);2735}2736}2737# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'2738# combined diff (for merge commit)2739elsif($line=~s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {2740$res{'nparents'} =length($1);2741$res{'from_mode'} = [split(' ',$2) ];2742$res{'to_mode'} =pop@{$res{'from_mode'}};2743$res{'from_id'} = [split(' ',$3) ];2744$res{'to_id'} =pop@{$res{'from_id'}};2745$res{'status'} = [split('',$4) ];2746$res{'to_file'} = unquote($5);2747}2748# 'c512b523472485aef4fff9e57b229d9d243c967f'2749elsif($line=~m/^([0-9a-fA-F]{40})$/) {2750$res{'commit'} =$1;2751}27522753returnwantarray?%res: \%res;2754}27552756# wrapper: return parsed line of git-diff-tree "raw" output2757# (the argument might be raw line, or parsed info)2758sub parsed_difftree_line {2759my$line_or_ref=shift;27602761if(ref($line_or_ref)eq"HASH") {2762# pre-parsed (or generated by hand)2763return$line_or_ref;2764}else{2765return parse_difftree_raw_line($line_or_ref);2766}2767}27682769# parse line of git-ls-tree output2770sub parse_ls_tree_line {2771my$line=shift;2772my%opts=@_;2773my%res;27742775#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2776$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;27772778$res{'mode'} =$1;2779$res{'type'} =$2;2780$res{'hash'} =$3;2781if($opts{'-z'}) {2782$res{'name'} =$4;2783}else{2784$res{'name'} = unquote($4);2785}27862787returnwantarray?%res: \%res;2788}27892790# generates _two_ hashes, references to which are passed as 2 and 3 argument2791sub parse_from_to_diffinfo {2792my($diffinfo,$from,$to,@parents) =@_;27932794if($diffinfo->{'nparents'}) {2795# combined diff2796$from->{'file'} = [];2797$from->{'href'} = [];2798 fill_from_file_info($diffinfo,@parents)2799unlessexists$diffinfo->{'from_file'};2800for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {2801$from->{'file'}[$i] =2802defined$diffinfo->{'from_file'}[$i] ?2803$diffinfo->{'from_file'}[$i] :2804$diffinfo->{'to_file'};2805if($diffinfo->{'status'}[$i]ne"A") {# not new (added) file2806$from->{'href'}[$i] = href(action=>"blob",2807 hash_base=>$parents[$i],2808 hash=>$diffinfo->{'from_id'}[$i],2809 file_name=>$from->{'file'}[$i]);2810}else{2811$from->{'href'}[$i] =undef;2812}2813}2814}else{2815# ordinary (not combined) diff2816$from->{'file'} =$diffinfo->{'from_file'};2817if($diffinfo->{'status'}ne"A") {# not new (added) file2818$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,2819 hash=>$diffinfo->{'from_id'},2820 file_name=>$from->{'file'});2821}else{2822delete$from->{'href'};2823}2824}28252826$to->{'file'} =$diffinfo->{'to_file'};2827if(!is_deleted($diffinfo)) {# file exists in result2828$to->{'href'} = href(action=>"blob", hash_base=>$hash,2829 hash=>$diffinfo->{'to_id'},2830 file_name=>$to->{'file'});2831}else{2832delete$to->{'href'};2833}2834}28352836## ......................................................................2837## parse to array of hashes functions28382839sub git_get_heads_list {2840my$limit=shift;2841my@headslist;28422843open my$fd,'-|', git_cmd(),'for-each-ref',2844($limit?'--count='.($limit+1) : ()),'--sort=-committerdate',2845'--format=%(objectname) %(refname) %(subject)%00%(committer)',2846'refs/heads'2847orreturn;2848while(my$line= <$fd>) {2849my%ref_item;28502851chomp$line;2852my($refinfo,$committerinfo) =split(/\0/,$line);2853my($hash,$name,$title) =split(' ',$refinfo,3);2854my($committer,$epoch,$tz) =2855($committerinfo=~/^(.*) ([0-9]+) (.*)$/);2856$ref_item{'fullname'} =$name;2857$name=~s!^refs/heads/!!;28582859$ref_item{'name'} =$name;2860$ref_item{'id'} =$hash;2861$ref_item{'title'} =$title||'(no commit message)';2862$ref_item{'epoch'} =$epoch;2863if($epoch) {2864$ref_item{'age'} = age_string(time-$ref_item{'epoch'});2865}else{2866$ref_item{'age'} ="unknown";2867}28682869push@headslist, \%ref_item;2870}2871close$fd;28722873returnwantarray?@headslist: \@headslist;2874}28752876sub git_get_tags_list {2877my$limit=shift;2878my@tagslist;28792880open my$fd,'-|', git_cmd(),'for-each-ref',2881($limit?'--count='.($limit+1) : ()),'--sort=-creatordate',2882'--format=%(objectname) %(objecttype) %(refname) '.2883'%(*objectname) %(*objecttype) %(subject)%00%(creator)',2884'refs/tags'2885orreturn;2886while(my$line= <$fd>) {2887my%ref_item;28882889chomp$line;2890my($refinfo,$creatorinfo) =split(/\0/,$line);2891my($id,$type,$name,$refid,$reftype,$title) =split(' ',$refinfo,6);2892my($creator,$epoch,$tz) =2893($creatorinfo=~/^(.*) ([0-9]+) (.*)$/);2894$ref_item{'fullname'} =$name;2895$name=~s!^refs/tags/!!;28962897$ref_item{'type'} =$type;2898$ref_item{'id'} =$id;2899$ref_item{'name'} =$name;2900if($typeeq"tag") {2901$ref_item{'subject'} =$title;2902$ref_item{'reftype'} =$reftype;2903$ref_item{'refid'} =$refid;2904}else{2905$ref_item{'reftype'} =$type;2906$ref_item{'refid'} =$id;2907}29082909if($typeeq"tag"||$typeeq"commit") {2910$ref_item{'epoch'} =$epoch;2911if($epoch) {2912$ref_item{'age'} = age_string(time-$ref_item{'epoch'});2913}else{2914$ref_item{'age'} ="unknown";2915}2916}29172918push@tagslist, \%ref_item;2919}2920close$fd;29212922returnwantarray?@tagslist: \@tagslist;2923}29242925## ----------------------------------------------------------------------2926## filesystem-related functions29272928sub get_file_owner {2929my$path=shift;29302931my($dev,$ino,$mode,$nlink,$st_uid,$st_gid,$rdev,$size) =stat($path);2932my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) =getpwuid($st_uid);2933if(!defined$gcos) {2934returnundef;2935}2936my$owner=$gcos;2937$owner=~s/[,;].*$//;2938return to_utf8($owner);2939}29402941# assume that file exists2942sub insert_file {2943my$filename=shift;29442945open my$fd,'<',$filename;2946print map{ to_utf8($_) } <$fd>;2947close$fd;2948}29492950## ......................................................................2951## mimetype related functions29522953sub mimetype_guess_file {2954my$filename=shift;2955my$mimemap=shift;2956-r $mimemaporreturnundef;29572958my%mimemap;2959open(my$mh,'<',$mimemap)orreturnundef;2960while(<$mh>) {2961next ifm/^#/;# skip comments2962my($mimetype,$exts) =split(/\t+/);2963if(defined$exts) {2964my@exts=split(/\s+/,$exts);2965foreachmy$ext(@exts) {2966$mimemap{$ext} =$mimetype;2967}2968}2969}2970close($mh);29712972$filename=~/\.([^.]*)$/;2973return$mimemap{$1};2974}29752976sub mimetype_guess {2977my$filename=shift;2978my$mime;2979$filename=~/\./orreturnundef;29802981if($mimetypes_file) {2982my$file=$mimetypes_file;2983if($file!~m!^/!) {# if it is relative path2984# it is relative to project2985$file="$projectroot/$project/$file";2986}2987$mime= mimetype_guess_file($filename,$file);2988}2989$mime||= mimetype_guess_file($filename,'/etc/mime.types');2990return$mime;2991}29922993sub blob_mimetype {2994my$fd=shift;2995my$filename=shift;29962997if($filename) {2998my$mime= mimetype_guess($filename);2999$mimeandreturn$mime;3000}30013002# just in case3003return$default_blob_plain_mimetypeunless$fd;30043005if(-T $fd) {3006return'text/plain';3007}elsif(!$filename) {3008return'application/octet-stream';3009}elsif($filename=~m/\.png$/i) {3010return'image/png';3011}elsif($filename=~m/\.gif$/i) {3012return'image/gif';3013}elsif($filename=~m/\.jpe?g$/i) {3014return'image/jpeg';3015}else{3016return'application/octet-stream';3017}3018}30193020sub blob_contenttype {3021my($fd,$file_name,$type) =@_;30223023$type||= blob_mimetype($fd,$file_name);3024if($typeeq'text/plain'&&defined$default_text_plain_charset) {3025$type.="; charset=$default_text_plain_charset";3026}30273028return$type;3029}30303031## ======================================================================3032## functions printing HTML: header, footer, error page30333034sub git_header_html {3035my$status=shift||"200 OK";3036my$expires=shift;30373038my$title="$site_name";3039if(defined$project) {3040$title.=" - ". to_utf8($project);3041if(defined$action) {3042$title.="/$action";3043if(defined$file_name) {3044$title.=" - ". esc_path($file_name);3045if($actioneq"tree"&&$file_name!~ m|/$|) {3046$title.="/";3047}3048}3049}3050}3051my$content_type;3052# require explicit support from the UA if we are to send the page as3053# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.3054# we have to do this because MSIE sometimes globs '*/*', pretending to3055# support xhtml+xml but choking when it gets what it asked for.3056if(defined$cgi->http('HTTP_ACCEPT') &&3057$cgi->http('HTTP_ACCEPT') =~m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&3058$cgi->Accept('application/xhtml+xml') !=0) {3059$content_type='application/xhtml+xml';3060}else{3061$content_type='text/html';3062}3063print$cgi->header(-type=>$content_type, -charset =>'utf-8',3064-status=>$status, -expires =>$expires);3065my$mod_perl_version=$ENV{'MOD_PERL'} ?"$ENV{'MOD_PERL'}":'';3066print<<EOF;3067<?xml version="1.0" encoding="utf-8"?>3068<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">3069<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">3070<!-- git web interface version$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->3071<!-- git core binaries version$git_version-->3072<head>3073<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>3074<meta name="generator" content="gitweb/$versiongit/$git_version$mod_perl_version"/>3075<meta name="robots" content="index, nofollow"/>3076<title>$title</title>3077EOF3078# the stylesheet, favicon etc urls won't work correctly with path_info3079# unless we set the appropriate base URL3080if($ENV{'PATH_INFO'}) {3081print"<base href=\"".esc_url($base_url)."\"/>\n";3082}3083# print out each stylesheet that exist, providing backwards capability3084# for those people who defined $stylesheet in a config file3085if(defined$stylesheet) {3086print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3087}else{3088foreachmy$stylesheet(@stylesheets) {3089next unless$stylesheet;3090print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3091}3092}3093if(defined$project) {3094my%href_params= get_feed_info();3095if(!exists$href_params{'-title'}) {3096$href_params{'-title'} ='log';3097}30983099foreachmy$formatqw(RSS Atom){3100my$type=lc($format);3101my%link_attr= (3102'-rel'=>'alternate',3103'-title'=>"$project-$href_params{'-title'} -$formatfeed",3104'-type'=>"application/$type+xml"3105);31063107$href_params{'action'} =$type;3108$link_attr{'-href'} = href(%href_params);3109print"<link ".3110"rel=\"$link_attr{'-rel'}\"".3111"title=\"$link_attr{'-title'}\"".3112"href=\"$link_attr{'-href'}\"".3113"type=\"$link_attr{'-type'}\"".3114"/>\n";31153116$href_params{'extra_options'} ='--no-merges';3117$link_attr{'-href'} = href(%href_params);3118$link_attr{'-title'} .=' (no merges)';3119print"<link ".3120"rel=\"$link_attr{'-rel'}\"".3121"title=\"$link_attr{'-title'}\"".3122"href=\"$link_attr{'-href'}\"".3123"type=\"$link_attr{'-type'}\"".3124"/>\n";3125}31263127}else{3128printf('<link rel="alternate" title="%sprojects list" '.3129'href="%s" type="text/plain; charset=utf-8" />'."\n",3130$site_name, href(project=>undef, action=>"project_index"));3131printf('<link rel="alternate" title="%sprojects feeds" '.3132'href="%s" type="text/x-opml" />'."\n",3133$site_name, href(project=>undef, action=>"opml"));3134}3135if(defined$favicon) {3136printqq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);3137}31383139print"</head>\n".3140"<body>\n";31413142if(-f $site_header) {3143 insert_file($site_header);3144}31453146print"<div class=\"page_header\">\n".3147$cgi->a({-href => esc_url($logo_url),3148-title =>$logo_label},3149qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));3150print$cgi->a({-href => esc_url($home_link)},$home_link_str) ." / ";3151if(defined$project) {3152print$cgi->a({-href => href(action=>"summary")}, esc_html($project));3153if(defined$action) {3154print" /$action";3155}3156print"\n";3157}3158print"</div>\n";31593160my$have_search= gitweb_check_feature('search');3161if(defined$project&&$have_search) {3162if(!defined$searchtext) {3163$searchtext="";3164}3165my$search_hash;3166if(defined$hash_base) {3167$search_hash=$hash_base;3168}elsif(defined$hash) {3169$search_hash=$hash;3170}else{3171$search_hash="HEAD";3172}3173my$action=$my_uri;3174my$use_pathinfo= gitweb_check_feature('pathinfo');3175if($use_pathinfo) {3176$action.="/".esc_url($project);3177}3178print$cgi->startform(-method=>"get", -action =>$action) .3179"<div class=\"search\">\n".3180(!$use_pathinfo&&3181$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) ."\n") .3182$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) ."\n".3183$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) ."\n".3184$cgi->popup_menu(-name =>'st', -default=>'commit',3185-values=> ['commit','grep','author','committer','pickaxe']) .3186$cgi->sup($cgi->a({-href => href(action=>"search_help")},"?")) .3187" search:\n",3188$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".3189"<span title=\"Extended regular expression\">".3190$cgi->checkbox(-name =>'sr', -value =>1, -label =>'re',3191-checked =>$search_use_regexp) .3192"</span>".3193"</div>".3194$cgi->end_form() ."\n";3195}3196}31973198sub git_footer_html {3199my$feed_class='rss_logo';32003201print"<div class=\"page_footer\">\n";3202if(defined$project) {3203my$descr= git_get_project_description($project);3204if(defined$descr) {3205print"<div class=\"page_footer_text\">". esc_html($descr) ."</div>\n";3206}32073208my%href_params= get_feed_info();3209if(!%href_params) {3210$feed_class.=' generic';3211}3212$href_params{'-title'} ||='log';32133214foreachmy$formatqw(RSS Atom){3215$href_params{'action'} =lc($format);3216print$cgi->a({-href => href(%href_params),3217-title =>"$href_params{'-title'}$formatfeed",3218-class=>$feed_class},$format)."\n";3219}32203221}else{3222print$cgi->a({-href => href(project=>undef, action=>"opml"),3223-class=>$feed_class},"OPML") ." ";3224print$cgi->a({-href => href(project=>undef, action=>"project_index"),3225-class=>$feed_class},"TXT") ."\n";3226}3227print"</div>\n";# class="page_footer"32283229if(defined$t0&& gitweb_check_feature('timed')) {3230print"<div id=\"generating_info\">\n";3231print'This page took '.3232'<span id="generating_time" class="time_span">'.3233 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).3234' seconds </span>'.3235' and '.3236'<span id="generating_cmd">'.3237$number_of_git_cmds.3238'</span> git commands '.3239" to generate.\n";3240print"</div>\n";# class="page_footer"3241}32423243if(-f $site_footer) {3244 insert_file($site_footer);3245}32463247print qq!<script type="text/javascript" src="$javascript"></script>\n!;3248if($actioneq'blame_incremental') {3249print qq!<script type="text/javascript">\n!.3250 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.3251 qq!"!. href() .qq!");\n!.3252 qq!</script>\n!;3253}else{3254print qq!<script type="text/javascript">\n!.3255 qq!window.onload = fixLinks;\n!.3256 qq!</script>\n!;3257}32583259print"</body>\n".3260"</html>";3261}32623263# die_error(<http_status_code>, <error_message>)3264# Example: die_error(404, 'Hash not found')3265# By convention, use the following status codes (as defined in RFC 2616):3266# 400: Invalid or missing CGI parameters, or3267# requested object exists but has wrong type.3268# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on3269# this server or project.3270# 404: Requested object/revision/project doesn't exist.3271# 500: The server isn't configured properly, or3272# an internal error occurred (e.g. failed assertions caused by bugs), or3273# an unknown error occurred (e.g. the git binary died unexpectedly).3274sub die_error {3275my$status=shift||500;3276my$error=shift||"Internal server error";32773278my%http_responses= (400=>'400 Bad Request',3279403=>'403 Forbidden',3280404=>'404 Not Found',3281500=>'500 Internal Server Error');3282 git_header_html($http_responses{$status});3283print<<EOF;3284<div class="page_body">3285<br /><br />3286$status-$error3287<br />3288</div>3289EOF3290 git_footer_html();3291exit;3292}32933294## ----------------------------------------------------------------------3295## functions printing or outputting HTML: navigation32963297sub git_print_page_nav {3298my($current,$suppress,$head,$treehead,$treebase,$extra) =@_;3299$extra=''if!defined$extra;# pager or formats33003301my@navs=qw(summary shortlog log commit commitdiff tree);3302if($suppress) {3303@navs=grep{$_ne$suppress}@navs;3304}33053306my%arg=map{$_=> {action=>$_} }@navs;3307if(defined$head) {3308for(qw(commit commitdiff)) {3309$arg{$_}{'hash'} =$head;3310}3311if($current=~m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {3312for(qw(shortlog log)) {3313$arg{$_}{'hash'} =$head;3314}3315}3316}33173318$arg{'tree'}{'hash'} =$treeheadifdefined$treehead;3319$arg{'tree'}{'hash_base'} =$treebaseifdefined$treebase;33203321my@actions= gitweb_get_feature('actions');3322my%repl= (3323'%'=>'%',3324'n'=>$project,# project name3325'f'=>$git_dir,# project path within filesystem3326'h'=>$treehead||'',# current hash ('h' parameter)3327'b'=>$treebase||'',# hash base ('hb' parameter)3328);3329while(@actions) {3330my($label,$link,$pos) =splice(@actions,0,3);3331# insert3332@navs=map{$_eq$pos? ($_,$label) :$_}@navs;3333# munch munch3334$link=~s/%([%nfhb])/$repl{$1}/g;3335$arg{$label}{'_href'} =$link;3336}33373338print"<div class=\"page_nav\">\n".3339(join" | ",3340map{$_eq$current?3341$_:$cgi->a({-href => ($arg{$_}{_href} ?$arg{$_}{_href} : href(%{$arg{$_}}))},"$_")3342}@navs);3343print"<br/>\n$extra<br/>\n".3344"</div>\n";3345}33463347sub format_paging_nav {3348my($action,$hash,$head,$page,$has_next_link) =@_;3349my$paging_nav;335033513352if($hashne$head||$page) {3353$paging_nav.=$cgi->a({-href => href(action=>$action)},"HEAD");3354}else{3355$paging_nav.="HEAD";3356}33573358if($page>0) {3359$paging_nav.=" ⋅ ".3360$cgi->a({-href => href(-replay=>1, page=>$page-1),3361-accesskey =>"p", -title =>"Alt-p"},"prev");3362}else{3363$paging_nav.=" ⋅ prev";3364}33653366if($has_next_link) {3367$paging_nav.=" ⋅ ".3368$cgi->a({-href => href(-replay=>1, page=>$page+1),3369-accesskey =>"n", -title =>"Alt-n"},"next");3370}else{3371$paging_nav.=" ⋅ next";3372}33733374return$paging_nav;3375}33763377## ......................................................................3378## functions printing or outputting HTML: div33793380sub git_print_header_div {3381my($action,$title,$hash,$hash_base) =@_;3382my%args= ();33833384$args{'action'} =$action;3385$args{'hash'} =$hashif$hash;3386$args{'hash_base'} =$hash_baseif$hash_base;33873388print"<div class=\"header\">\n".3389$cgi->a({-href => href(%args), -class=>"title"},3390$title?$title:$action) .3391"\n</div>\n";3392}33933394sub print_local_time {3395my%date=@_;3396if($date{'hour_local'} <6) {3397printf(" (<span class=\"atnight\">%02d:%02d</span>%s)",3398$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3399}else{3400printf(" (%02d:%02d%s)",3401$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3402}3403}34043405# Outputs the author name and date in long form3406sub git_print_authorship {3407my$co=shift;3408my%opts=@_;3409my$tag=$opts{-tag} ||'div';34103411my%ad= parse_date($co->{'author_epoch'},$co->{'author_tz'});3412print"<$tagclass=\"author_date\">".3413 esc_html($co->{'author_name'}) .3414" [$ad{'rfc2822'}";3415 print_local_time(%ad)if($opts{-localtime});3416print"]". git_get_avatar($co->{'author_email'}, -pad_before =>1)3417."</$tag>\n";3418}34193420# Outputs table rows containing the full author or committer information,3421# in the format expected for 'commit' view (& similia).3422# Parameters are a commit hash reference, followed by the list of people3423# to output information for. If the list is empty it defalts to both3424# author and committer.3425sub git_print_authorship_rows {3426my$co=shift;3427# too bad we can't use @people = @_ || ('author', 'committer')3428my@people=@_;3429@people= ('author','committer')unless@people;3430foreachmy$who(@people) {3431my%wd= parse_date($co->{"${who}_epoch"},$co->{"${who}_tz"});3432print"<tr><td>$who</td><td>". esc_html($co->{$who}) ."</td>".3433"<td rowspan=\"2\">".3434 git_get_avatar($co->{"${who}_email"}, -size =>'double') .3435"</td></tr>\n".3436"<tr>".3437"<td></td><td>$wd{'rfc2822'}";3438 print_local_time(%wd);3439print"</td>".3440"</tr>\n";3441}3442}34433444sub git_print_page_path {3445my$name=shift;3446my$type=shift;3447my$hb=shift;344834493450print"<div class=\"page_path\">";3451print$cgi->a({-href => href(action=>"tree", hash_base=>$hb),3452-title =>'tree root'}, to_utf8("[$project]"));3453print" / ";3454if(defined$name) {3455my@dirname=split'/',$name;3456my$basename=pop@dirname;3457my$fullname='';34583459foreachmy$dir(@dirname) {3460$fullname.= ($fullname?'/':'') .$dir;3461print$cgi->a({-href => href(action=>"tree", file_name=>$fullname,3462 hash_base=>$hb),3463-title =>$fullname}, esc_path($dir));3464print" / ";3465}3466if(defined$type&&$typeeq'blob') {3467print$cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,3468 hash_base=>$hb),3469-title =>$name}, esc_path($basename));3470}elsif(defined$type&&$typeeq'tree') {3471print$cgi->a({-href => href(action=>"tree", file_name=>$file_name,3472 hash_base=>$hb),3473-title =>$name}, esc_path($basename));3474print" / ";3475}else{3476print esc_path($basename);3477}3478}3479print"<br/></div>\n";3480}34813482sub git_print_log {3483my$log=shift;3484my%opts=@_;34853486if($opts{'-remove_title'}) {3487# remove title, i.e. first line of log3488shift@$log;3489}3490# remove leading empty lines3491while(defined$log->[0] &&$log->[0]eq"") {3492shift@$log;3493}34943495# print log3496my$signoff=0;3497my$empty=0;3498foreachmy$line(@$log) {3499if($line=~m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {3500$signoff=1;3501$empty=0;3502if(!$opts{'-remove_signoff'}) {3503print"<span class=\"signoff\">". esc_html($line) ."</span><br/>\n";3504next;3505}else{3506# remove signoff lines3507next;3508}3509}else{3510$signoff=0;3511}35123513# print only one empty line3514# do not print empty line after signoff3515if($lineeq"") {3516next if($empty||$signoff);3517$empty=1;3518}else{3519$empty=0;3520}35213522print format_log_line_html($line) ."<br/>\n";3523}35243525if($opts{'-final_empty_line'}) {3526# end with single empty line3527print"<br/>\n"unless$empty;3528}3529}35303531# return link target (what link points to)3532sub git_get_link_target {3533my$hash=shift;3534my$link_target;35353536# read link3537open my$fd,"-|", git_cmd(),"cat-file","blob",$hash3538orreturn;3539{3540local$/=undef;3541$link_target= <$fd>;3542}3543close$fd3544orreturn;35453546return$link_target;3547}35483549# given link target, and the directory (basedir) the link is in,3550# return target of link relative to top directory (top tree);3551# return undef if it is not possible (including absolute links).3552sub normalize_link_target {3553my($link_target,$basedir) =@_;35543555# absolute symlinks (beginning with '/') cannot be normalized3556return if(substr($link_target,0,1)eq'/');35573558# normalize link target to path from top (root) tree (dir)3559my$path;3560if($basedir) {3561$path=$basedir.'/'.$link_target;3562}else{3563# we are in top (root) tree (dir)3564$path=$link_target;3565}35663567# remove //, /./, and /../3568my@path_parts;3569foreachmy$part(split('/',$path)) {3570# discard '.' and ''3571next if(!$part||$parteq'.');3572# handle '..'3573if($parteq'..') {3574if(@path_parts) {3575pop@path_parts;3576}else{3577# link leads outside repository (outside top dir)3578return;3579}3580}else{3581push@path_parts,$part;3582}3583}3584$path=join('/',@path_parts);35853586return$path;3587}35883589# print tree entry (row of git_tree), but without encompassing <tr> element3590sub git_print_tree_entry {3591my($t,$basedir,$hash_base,$have_blame) =@_;35923593my%base_key= ();3594$base_key{'hash_base'} =$hash_baseifdefined$hash_base;35953596# The format of a table row is: mode list link. Where mode is3597# the mode of the entry, list is the name of the entry, an href,3598# and link is the action links of the entry.35993600print"<td class=\"mode\">". mode_str($t->{'mode'}) ."</td>\n";3601if($t->{'type'}eq"blob") {3602print"<td class=\"list\">".3603$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3604 file_name=>"$basedir$t->{'name'}",%base_key),3605-class=>"list"}, esc_path($t->{'name'}));3606if(S_ISLNK(oct$t->{'mode'})) {3607my$link_target= git_get_link_target($t->{'hash'});3608if($link_target) {3609my$norm_target= normalize_link_target($link_target,$basedir);3610if(defined$norm_target) {3611print" -> ".3612$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,3613 file_name=>$norm_target),3614-title =>$norm_target}, esc_path($link_target));3615}else{3616print" -> ". esc_path($link_target);3617}3618}3619}3620print"</td>\n";3621print"<td class=\"link\">";3622print$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3623 file_name=>"$basedir$t->{'name'}",%base_key)},3624"blob");3625if($have_blame) {3626print" | ".3627$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},3628 file_name=>"$basedir$t->{'name'}",%base_key)},3629"blame");3630}3631if(defined$hash_base) {3632print" | ".3633$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3634 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},3635"history");3636}3637print" | ".3638$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,3639 file_name=>"$basedir$t->{'name'}")},3640"raw");3641print"</td>\n";36423643}elsif($t->{'type'}eq"tree") {3644print"<td class=\"list\">";3645print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3646 file_name=>"$basedir$t->{'name'}",%base_key)},3647 esc_path($t->{'name'}));3648print"</td>\n";3649print"<td class=\"link\">";3650print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3651 file_name=>"$basedir$t->{'name'}",%base_key)},3652"tree");3653if(defined$hash_base) {3654print" | ".3655$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3656 file_name=>"$basedir$t->{'name'}")},3657"history");3658}3659print"</td>\n";3660}else{3661# unknown object: we can only present history for it3662# (this includes 'commit' object, i.e. submodule support)3663print"<td class=\"list\">".3664 esc_path($t->{'name'}) .3665"</td>\n";3666print"<td class=\"link\">";3667if(defined$hash_base) {3668print$cgi->a({-href => href(action=>"history",3669 hash_base=>$hash_base,3670 file_name=>"$basedir$t->{'name'}")},3671"history");3672}3673print"</td>\n";3674}3675}36763677## ......................................................................3678## functions printing large fragments of HTML36793680# get pre-image filenames for merge (combined) diff3681sub fill_from_file_info {3682my($diff,@parents) =@_;36833684$diff->{'from_file'} = [ ];3685$diff->{'from_file'}[$diff->{'nparents'} -1] =undef;3686for(my$i=0;$i<$diff->{'nparents'};$i++) {3687if($diff->{'status'}[$i]eq'R'||3688$diff->{'status'}[$i]eq'C') {3689$diff->{'from_file'}[$i] =3690 git_get_path_by_hash($parents[$i],$diff->{'from_id'}[$i]);3691}3692}36933694return$diff;3695}36963697# is current raw difftree line of file deletion3698sub is_deleted {3699my$diffinfo=shift;37003701return$diffinfo->{'to_id'}eq('0' x 40);3702}37033704# does patch correspond to [previous] difftree raw line3705# $diffinfo - hashref of parsed raw diff format3706# $patchinfo - hashref of parsed patch diff format3707# (the same keys as in $diffinfo)3708sub is_patch_split {3709my($diffinfo,$patchinfo) =@_;37103711returndefined$diffinfo&&defined$patchinfo3712&&$diffinfo->{'to_file'}eq$patchinfo->{'to_file'};3713}371437153716sub git_difftree_body {3717my($difftree,$hash,@parents) =@_;3718my($parent) =$parents[0];3719my$have_blame= gitweb_check_feature('blame');3720print"<div class=\"list_head\">\n";3721if($#{$difftree} >10) {3722print(($#{$difftree} +1) ." files changed:\n");3723}3724print"</div>\n";37253726print"<table class=\"".3727(@parents>1?"combined ":"") .3728"diff_tree\">\n";37293730# header only for combined diff in 'commitdiff' view3731my$has_header=@$difftree&&@parents>1&&$actioneq'commitdiff';3732if($has_header) {3733# table header3734print"<thead><tr>\n".3735"<th></th><th></th>\n";# filename, patchN link3736for(my$i=0;$i<@parents;$i++) {3737my$par=$parents[$i];3738print"<th>".3739$cgi->a({-href => href(action=>"commitdiff",3740 hash=>$hash, hash_parent=>$par),3741-title =>'commitdiff to parent number '.3742($i+1) .': '.substr($par,0,7)},3743$i+1) .3744" </th>\n";3745}3746print"</tr></thead>\n<tbody>\n";3747}37483749my$alternate=1;3750my$patchno=0;3751foreachmy$line(@{$difftree}) {3752my$diff= parsed_difftree_line($line);37533754if($alternate) {3755print"<tr class=\"dark\">\n";3756}else{3757print"<tr class=\"light\">\n";3758}3759$alternate^=1;37603761if(exists$diff->{'nparents'}) {# combined diff37623763 fill_from_file_info($diff,@parents)3764unlessexists$diff->{'from_file'};37653766if(!is_deleted($diff)) {3767# file exists in the result (child) commit3768print"<td>".3769$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3770 file_name=>$diff->{'to_file'},3771 hash_base=>$hash),3772-class=>"list"}, esc_path($diff->{'to_file'})) .3773"</td>\n";3774}else{3775print"<td>".3776 esc_path($diff->{'to_file'}) .3777"</td>\n";3778}37793780if($actioneq'commitdiff') {3781# link to patch3782$patchno++;3783print"<td class=\"link\">".3784$cgi->a({-href =>"#patch$patchno"},"patch") .3785" | ".3786"</td>\n";3787}37883789my$has_history=0;3790my$not_deleted=0;3791for(my$i=0;$i<$diff->{'nparents'};$i++) {3792my$hash_parent=$parents[$i];3793my$from_hash=$diff->{'from_id'}[$i];3794my$from_path=$diff->{'from_file'}[$i];3795my$status=$diff->{'status'}[$i];37963797$has_history||= ($statusne'A');3798$not_deleted||= ($statusne'D');37993800if($statuseq'A') {3801print"<td class=\"link\"align=\"right\"> | </td>\n";3802}elsif($statuseq'D') {3803print"<td class=\"link\">".3804$cgi->a({-href => href(action=>"blob",3805 hash_base=>$hash,3806 hash=>$from_hash,3807 file_name=>$from_path)},3808"blob". ($i+1)) .3809" | </td>\n";3810}else{3811if($diff->{'to_id'}eq$from_hash) {3812print"<td class=\"link nochange\">";3813}else{3814print"<td class=\"link\">";3815}3816print$cgi->a({-href => href(action=>"blobdiff",3817 hash=>$diff->{'to_id'},3818 hash_parent=>$from_hash,3819 hash_base=>$hash,3820 hash_parent_base=>$hash_parent,3821 file_name=>$diff->{'to_file'},3822 file_parent=>$from_path)},3823"diff". ($i+1)) .3824" | </td>\n";3825}3826}38273828print"<td class=\"link\">";3829if($not_deleted) {3830print$cgi->a({-href => href(action=>"blob",3831 hash=>$diff->{'to_id'},3832 file_name=>$diff->{'to_file'},3833 hash_base=>$hash)},3834"blob");3835print" | "if($has_history);3836}3837if($has_history) {3838print$cgi->a({-href => href(action=>"history",3839 file_name=>$diff->{'to_file'},3840 hash_base=>$hash)},3841"history");3842}3843print"</td>\n";38443845print"</tr>\n";3846next;# instead of 'else' clause, to avoid extra indent3847}3848# else ordinary diff38493850my($to_mode_oct,$to_mode_str,$to_file_type);3851my($from_mode_oct,$from_mode_str,$from_file_type);3852if($diff->{'to_mode'}ne('0' x 6)) {3853$to_mode_oct=oct$diff->{'to_mode'};3854if(S_ISREG($to_mode_oct)) {# only for regular file3855$to_mode_str=sprintf("%04o",$to_mode_oct&0777);# permission bits3856}3857$to_file_type= file_type($diff->{'to_mode'});3858}3859if($diff->{'from_mode'}ne('0' x 6)) {3860$from_mode_oct=oct$diff->{'from_mode'};3861if(S_ISREG($to_mode_oct)) {# only for regular file3862$from_mode_str=sprintf("%04o",$from_mode_oct&0777);# permission bits3863}3864$from_file_type= file_type($diff->{'from_mode'});3865}38663867if($diff->{'status'}eq"A") {# created3868my$mode_chng="<span class=\"file_status new\">[new$to_file_type";3869$mode_chng.=" with mode:$to_mode_str"if$to_mode_str;3870$mode_chng.="]</span>";3871print"<td>";3872print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3873 hash_base=>$hash, file_name=>$diff->{'file'}),3874-class=>"list"}, esc_path($diff->{'file'}));3875print"</td>\n";3876print"<td>$mode_chng</td>\n";3877print"<td class=\"link\">";3878if($actioneq'commitdiff') {3879# link to patch3880$patchno++;3881print$cgi->a({-href =>"#patch$patchno"},"patch");3882print" | ";3883}3884print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3885 hash_base=>$hash, file_name=>$diff->{'file'})},3886"blob");3887print"</td>\n";38883889}elsif($diff->{'status'}eq"D") {# deleted3890my$mode_chng="<span class=\"file_status deleted\">[deleted$from_file_type]</span>";3891print"<td>";3892print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},3893 hash_base=>$parent, file_name=>$diff->{'file'}),3894-class=>"list"}, esc_path($diff->{'file'}));3895print"</td>\n";3896print"<td>$mode_chng</td>\n";3897print"<td class=\"link\">";3898if($actioneq'commitdiff') {3899# link to patch3900$patchno++;3901print$cgi->a({-href =>"#patch$patchno"},"patch");3902print" | ";3903}3904print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},3905 hash_base=>$parent, file_name=>$diff->{'file'})},3906"blob") ." | ";3907if($have_blame) {3908print$cgi->a({-href => href(action=>"blame", hash_base=>$parent,3909 file_name=>$diff->{'file'})},3910"blame") ." | ";3911}3912print$cgi->a({-href => href(action=>"history", hash_base=>$parent,3913 file_name=>$diff->{'file'})},3914"history");3915print"</td>\n";39163917}elsif($diff->{'status'}eq"M"||$diff->{'status'}eq"T") {# modified, or type changed3918my$mode_chnge="";3919if($diff->{'from_mode'} !=$diff->{'to_mode'}) {3920$mode_chnge="<span class=\"file_status mode_chnge\">[changed";3921if($from_file_typene$to_file_type) {3922$mode_chnge.=" from$from_file_typeto$to_file_type";3923}3924if(($from_mode_oct&0777) != ($to_mode_oct&0777)) {3925if($from_mode_str&&$to_mode_str) {3926$mode_chnge.=" mode:$from_mode_str->$to_mode_str";3927}elsif($to_mode_str) {3928$mode_chnge.=" mode:$to_mode_str";3929}3930}3931$mode_chnge.="]</span>\n";3932}3933print"<td>";3934print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3935 hash_base=>$hash, file_name=>$diff->{'file'}),3936-class=>"list"}, esc_path($diff->{'file'}));3937print"</td>\n";3938print"<td>$mode_chnge</td>\n";3939print"<td class=\"link\">";3940if($actioneq'commitdiff') {3941# link to patch3942$patchno++;3943print$cgi->a({-href =>"#patch$patchno"},"patch") .3944" | ";3945}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {3946# "commit" view and modified file (not onlu mode changed)3947print$cgi->a({-href => href(action=>"blobdiff",3948 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},3949 hash_base=>$hash, hash_parent_base=>$parent,3950 file_name=>$diff->{'file'})},3951"diff") .3952" | ";3953}3954print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3955 hash_base=>$hash, file_name=>$diff->{'file'})},3956"blob") ." | ";3957if($have_blame) {3958print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,3959 file_name=>$diff->{'file'})},3960"blame") ." | ";3961}3962print$cgi->a({-href => href(action=>"history", hash_base=>$hash,3963 file_name=>$diff->{'file'})},3964"history");3965print"</td>\n";39663967}elsif($diff->{'status'}eq"R"||$diff->{'status'}eq"C") {# renamed or copied3968my%status_name= ('R'=>'moved','C'=>'copied');3969my$nstatus=$status_name{$diff->{'status'}};3970my$mode_chng="";3971if($diff->{'from_mode'} !=$diff->{'to_mode'}) {3972# mode also for directories, so we cannot use $to_mode_str3973$mode_chng=sprintf(", mode:%04o",$to_mode_oct&0777);3974}3975print"<td>".3976$cgi->a({-href => href(action=>"blob", hash_base=>$hash,3977 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),3978-class=>"list"}, esc_path($diff->{'to_file'})) ."</td>\n".3979"<td><span class=\"file_status$nstatus\">[$nstatusfrom ".3980$cgi->a({-href => href(action=>"blob", hash_base=>$parent,3981 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),3982-class=>"list"}, esc_path($diff->{'from_file'})) .3983" with ". (int$diff->{'similarity'}) ."% similarity$mode_chng]</span></td>\n".3984"<td class=\"link\">";3985if($actioneq'commitdiff') {3986# link to patch3987$patchno++;3988print$cgi->a({-href =>"#patch$patchno"},"patch") .3989" | ";3990}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {3991# "commit" view and modified file (not only pure rename or copy)3992print$cgi->a({-href => href(action=>"blobdiff",3993 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},3994 hash_base=>$hash, hash_parent_base=>$parent,3995 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},3996"diff") .3997" | ";3998}3999print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4000 hash_base=>$parent, file_name=>$diff->{'to_file'})},4001"blob") ." | ";4002if($have_blame) {4003print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4004 file_name=>$diff->{'to_file'})},4005"blame") ." | ";4006}4007print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4008 file_name=>$diff->{'to_file'})},4009"history");4010print"</td>\n";40114012}# we should not encounter Unmerged (U) or Unknown (X) status4013print"</tr>\n";4014}4015print"</tbody>"if$has_header;4016print"</table>\n";4017}40184019sub git_patchset_body {4020my($fd,$difftree,$hash,@hash_parents) =@_;4021my($hash_parent) =$hash_parents[0];40224023my$is_combined= (@hash_parents>1);4024my$patch_idx=0;4025my$patch_number=0;4026my$patch_line;4027my$diffinfo;4028my$to_name;4029my(%from,%to);40304031print"<div class=\"patchset\">\n";40324033# skip to first patch4034while($patch_line= <$fd>) {4035chomp$patch_line;40364037last if($patch_line=~m/^diff /);4038}40394040 PATCH:4041while($patch_line) {40424043# parse "git diff" header line4044if($patch_line=~m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {4045# $1 is from_name, which we do not use4046$to_name= unquote($2);4047$to_name=~s!^b/!!;4048}elsif($patch_line=~m/^diff --(cc|combined) ("?.*"?)$/) {4049# $1 is 'cc' or 'combined', which we do not use4050$to_name= unquote($2);4051}else{4052$to_name=undef;4053}40544055# check if current patch belong to current raw line4056# and parse raw git-diff line if needed4057if(is_patch_split($diffinfo, {'to_file'=>$to_name})) {4058# this is continuation of a split patch4059print"<div class=\"patch cont\">\n";4060}else{4061# advance raw git-diff output if needed4062$patch_idx++ifdefined$diffinfo;40634064# read and prepare patch information4065$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);40664067# compact combined diff output can have some patches skipped4068# find which patch (using pathname of result) we are at now;4069if($is_combined) {4070while($to_namene$diffinfo->{'to_file'}) {4071print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4072 format_diff_cc_simplified($diffinfo,@hash_parents) .4073"</div>\n";# class="patch"40744075$patch_idx++;4076$patch_number++;40774078last if$patch_idx>$#$difftree;4079$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);4080}4081}40824083# modifies %from, %to hashes4084 parse_from_to_diffinfo($diffinfo, \%from, \%to,@hash_parents);40854086# this is first patch for raw difftree line with $patch_idx index4087# we index @$difftree array from 0, but number patches from 14088print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n";4089}40904091# git diff header4092#assert($patch_line =~ m/^diff /) if DEBUG;4093#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed4094$patch_number++;4095# print "git diff" header4096print format_git_diff_header_line($patch_line,$diffinfo,4097 \%from, \%to);40984099# print extended diff header4100print"<div class=\"diff extended_header\">\n";4101 EXTENDED_HEADER:4102while($patch_line= <$fd>) {4103chomp$patch_line;41044105last EXTENDED_HEADER if($patch_line=~m/^--- |^diff /);41064107print format_extended_diff_header_line($patch_line,$diffinfo,4108 \%from, \%to);4109}4110print"</div>\n";# class="diff extended_header"41114112# from-file/to-file diff header4113if(!$patch_line) {4114print"</div>\n";# class="patch"4115last PATCH;4116}4117next PATCH if($patch_line=~m/^diff /);4118#assert($patch_line =~ m/^---/) if DEBUG;41194120my$last_patch_line=$patch_line;4121$patch_line= <$fd>;4122chomp$patch_line;4123#assert($patch_line =~ m/^\+\+\+/) if DEBUG;41244125print format_diff_from_to_header($last_patch_line,$patch_line,4126$diffinfo, \%from, \%to,4127@hash_parents);41284129# the patch itself4130 LINE:4131while($patch_line= <$fd>) {4132chomp$patch_line;41334134next PATCH if($patch_line=~m/^diff /);41354136print format_diff_line($patch_line, \%from, \%to);4137}41384139}continue{4140print"</div>\n";# class="patch"4141}41424143# for compact combined (--cc) format, with chunk and patch simpliciaction4144# patchset might be empty, but there might be unprocessed raw lines4145for(++$patch_idxif$patch_number>0;4146$patch_idx<@$difftree;4147++$patch_idx) {4148# read and prepare patch information4149$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);41504151# generate anchor for "patch" links in difftree / whatchanged part4152print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4153 format_diff_cc_simplified($diffinfo,@hash_parents) .4154"</div>\n";# class="patch"41554156$patch_number++;4157}41584159if($patch_number==0) {4160if(@hash_parents>1) {4161print"<div class=\"diff nodifferences\">Trivial merge</div>\n";4162}else{4163print"<div class=\"diff nodifferences\">No differences found</div>\n";4164}4165}41664167print"</div>\n";# class="patchset"4168}41694170# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .41714172# fills project list info (age, description, owner, forks) for each4173# project in the list, removing invalid projects from returned list4174# NOTE: modifies $projlist, but does not remove entries from it4175sub fill_project_list_info {4176my($projlist,$check_forks) =@_;4177my@projects;41784179my$show_ctags= gitweb_check_feature('ctags');4180 PROJECT:4181foreachmy$pr(@$projlist) {4182my(@activity) = git_get_last_activity($pr->{'path'});4183unless(@activity) {4184next PROJECT;4185}4186($pr->{'age'},$pr->{'age_string'}) =@activity;4187if(!defined$pr->{'descr'}) {4188my$descr= git_get_project_description($pr->{'path'}) ||"";4189$descr= to_utf8($descr);4190$pr->{'descr_long'} =$descr;4191$pr->{'descr'} = chop_str($descr,$projects_list_description_width,5);4192}4193if(!defined$pr->{'owner'}) {4194$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") ||"";4195}4196if($check_forks) {4197my$pname=$pr->{'path'};4198if(($pname=~s/\.git$//) &&4199($pname!~/\/$/) &&4200(-d "$projectroot/$pname")) {4201$pr->{'forks'} ="-d$projectroot/$pname";4202}else{4203$pr->{'forks'} =0;4204}4205}4206$show_ctagsand$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});4207push@projects,$pr;4208}42094210return@projects;4211}42124213# print 'sort by' <th> element, generating 'sort by $name' replay link4214# if that order is not selected4215sub print_sort_th {4216my($name,$order,$header) =@_;4217$header||=ucfirst($name);42184219if($ordereq$name) {4220print"<th>$header</th>\n";4221}else{4222print"<th>".4223$cgi->a({-href => href(-replay=>1, order=>$name),4224-class=>"header"},$header) .4225"</th>\n";4226}4227}42284229sub git_project_list_body {4230# actually uses global variable $project4231my($projlist,$order,$from,$to,$extra,$no_header) =@_;42324233my$check_forks= gitweb_check_feature('forks');4234my@projects= fill_project_list_info($projlist,$check_forks);42354236$order||=$default_projects_order;4237$from=0unlessdefined$from;4238$to=$#projectsif(!defined$to||$#projects<$to);42394240my%order_info= (4241 project => { key =>'path', type =>'str'},4242 descr => { key =>'descr_long', type =>'str'},4243 owner => { key =>'owner', type =>'str'},4244 age => { key =>'age', type =>'num'}4245);4246my$oi=$order_info{$order};4247if($oi->{'type'}eq'str') {4248@projects=sort{$a->{$oi->{'key'}}cmp$b->{$oi->{'key'}}}@projects;4249}else{4250@projects=sort{$a->{$oi->{'key'}} <=>$b->{$oi->{'key'}}}@projects;4251}42524253my$show_ctags= gitweb_check_feature('ctags');4254if($show_ctags) {4255my%ctags;4256foreachmy$p(@projects) {4257foreachmy$ct(keys%{$p->{'ctags'}}) {4258$ctags{$ct} +=$p->{'ctags'}->{$ct};4259}4260}4261my$cloud= git_populate_project_tagcloud(\%ctags);4262print git_show_project_tagcloud($cloud,64);4263}42644265print"<table class=\"project_list\">\n";4266unless($no_header) {4267print"<tr>\n";4268if($check_forks) {4269print"<th></th>\n";4270}4271 print_sort_th('project',$order,'Project');4272 print_sort_th('descr',$order,'Description');4273 print_sort_th('owner',$order,'Owner');4274 print_sort_th('age',$order,'Last Change');4275print"<th></th>\n".# for links4276"</tr>\n";4277}4278my$alternate=1;4279my$tagfilter=$cgi->param('by_tag');4280for(my$i=$from;$i<=$to;$i++) {4281my$pr=$projects[$i];42824283next if$tagfilterand$show_ctagsand not grep{lc$_eq lc$tagfilter}keys%{$pr->{'ctags'}};4284next if$searchtextand not$pr->{'path'} =~/$searchtext/4285and not$pr->{'descr_long'} =~/$searchtext/;4286# Weed out forks or non-matching entries of search4287if($check_forks) {4288my$forkbase=$project;$forkbase||='';$forkbase=~ s#\.git$#/#;4289$forkbase="^$forkbase"if$forkbase;4290next ifnot$searchtextand not$tagfilterand$show_ctags4291and$pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe4292}42934294if($alternate) {4295print"<tr class=\"dark\">\n";4296}else{4297print"<tr class=\"light\">\n";4298}4299$alternate^=1;4300if($check_forks) {4301print"<td>";4302if($pr->{'forks'}) {4303print"<!--$pr->{'forks'} -->\n";4304print$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"+");4305}4306print"</td>\n";4307}4308print"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4309-class=>"list"}, esc_html($pr->{'path'})) ."</td>\n".4310"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4311-class=>"list", -title =>$pr->{'descr_long'}},4312 esc_html($pr->{'descr'})) ."</td>\n".4313"<td><i>". chop_and_escape_str($pr->{'owner'},15) ."</i></td>\n";4314print"<td class=\"". age_class($pr->{'age'}) ."\">".4315(defined$pr->{'age_string'} ?$pr->{'age_string'} :"No commits") ."</td>\n".4316"<td class=\"link\">".4317$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")},"summary") ." | ".4318$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")},"shortlog") ." | ".4319$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")},"log") ." | ".4320$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")},"tree") .4321($pr->{'forks'} ?" | ".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"forks") :'') .4322"</td>\n".4323"</tr>\n";4324}4325if(defined$extra) {4326print"<tr>\n";4327if($check_forks) {4328print"<td></td>\n";4329}4330print"<td colspan=\"5\">$extra</td>\n".4331"</tr>\n";4332}4333print"</table>\n";4334}43354336sub git_shortlog_body {4337# uses global variable $project4338my($commitlist,$from,$to,$refs,$extra) =@_;43394340$from=0unlessdefined$from;4341$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);43424343print"<table class=\"shortlog\">\n";4344my$alternate=1;4345for(my$i=$from;$i<=$to;$i++) {4346my%co= %{$commitlist->[$i]};4347my$commit=$co{'id'};4348my$ref= format_ref_marker($refs,$commit);4349if($alternate) {4350print"<tr class=\"dark\">\n";4351}else{4352print"<tr class=\"light\">\n";4353}4354$alternate^=1;4355# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .4356print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4357 format_author_html('td', \%co,10) ."<td>";4358print format_subject_html($co{'title'},$co{'title_short'},4359 href(action=>"commit", hash=>$commit),$ref);4360print"</td>\n".4361"<td class=\"link\">".4362$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") ." | ".4363$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") ." | ".4364$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree");4365my$snapshot_links= format_snapshot_links($commit);4366if(defined$snapshot_links) {4367print" | ".$snapshot_links;4368}4369print"</td>\n".4370"</tr>\n";4371}4372if(defined$extra) {4373print"<tr>\n".4374"<td colspan=\"4\">$extra</td>\n".4375"</tr>\n";4376}4377print"</table>\n";4378}43794380sub git_history_body {4381# Warning: assumes constant type (blob or tree) during history4382my($commitlist,$from,$to,$refs,$hash_base,$ftype,$extra) =@_;43834384$from=0unlessdefined$from;4385$to=$#{$commitlist}unless(defined$to&&$to<=$#{$commitlist});43864387print"<table class=\"history\">\n";4388my$alternate=1;4389for(my$i=$from;$i<=$to;$i++) {4390my%co= %{$commitlist->[$i]};4391if(!%co) {4392next;4393}4394my$commit=$co{'id'};43954396my$ref= format_ref_marker($refs,$commit);43974398if($alternate) {4399print"<tr class=\"dark\">\n";4400}else{4401print"<tr class=\"light\">\n";4402}4403$alternate^=1;4404print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4405# shortlog: format_author_html('td', \%co, 10)4406 format_author_html('td', \%co,15,3) ."<td>";4407# originally git_history used chop_str($co{'title'}, 50)4408print format_subject_html($co{'title'},$co{'title_short'},4409 href(action=>"commit", hash=>$commit),$ref);4410print"</td>\n".4411"<td class=\"link\">".4412$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)},$ftype) ." | ".4413$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff");44144415if($ftypeeq'blob') {4416my$blob_current= git_get_hash_by_path($hash_base,$file_name);4417my$blob_parent= git_get_hash_by_path($commit,$file_name);4418if(defined$blob_current&&defined$blob_parent&&4419$blob_currentne$blob_parent) {4420print" | ".4421$cgi->a({-href => href(action=>"blobdiff",4422 hash=>$blob_current, hash_parent=>$blob_parent,4423 hash_base=>$hash_base, hash_parent_base=>$commit,4424 file_name=>$file_name)},4425"diff to current");4426}4427}4428print"</td>\n".4429"</tr>\n";4430}4431if(defined$extra) {4432print"<tr>\n".4433"<td colspan=\"4\">$extra</td>\n".4434"</tr>\n";4435}4436print"</table>\n";4437}44384439sub git_tags_body {4440# uses global variable $project4441my($taglist,$from,$to,$extra) =@_;4442$from=0unlessdefined$from;4443$to=$#{$taglist}if(!defined$to||$#{$taglist} <$to);44444445print"<table class=\"tags\">\n";4446my$alternate=1;4447for(my$i=$from;$i<=$to;$i++) {4448my$entry=$taglist->[$i];4449my%tag=%$entry;4450my$comment=$tag{'subject'};4451my$comment_short;4452if(defined$comment) {4453$comment_short= chop_str($comment,30,5);4454}4455if($alternate) {4456print"<tr class=\"dark\">\n";4457}else{4458print"<tr class=\"light\">\n";4459}4460$alternate^=1;4461if(defined$tag{'age'}) {4462print"<td><i>$tag{'age'}</i></td>\n";4463}else{4464print"<td></td>\n";4465}4466print"<td>".4467$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),4468-class=>"list name"}, esc_html($tag{'name'})) .4469"</td>\n".4470"<td>";4471if(defined$comment) {4472print format_subject_html($comment,$comment_short,4473 href(action=>"tag", hash=>$tag{'id'}));4474}4475print"</td>\n".4476"<td class=\"selflink\">";4477if($tag{'type'}eq"tag") {4478print$cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})},"tag");4479}else{4480print" ";4481}4482print"</td>\n".4483"<td class=\"link\">"." | ".4484$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})},$tag{'reftype'});4485if($tag{'reftype'}eq"commit") {4486print" | ".$cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})},"shortlog") .4487" | ".$cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})},"log");4488}elsif($tag{'reftype'}eq"blob") {4489print" | ".$cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})},"raw");4490}4491print"</td>\n".4492"</tr>";4493}4494if(defined$extra) {4495print"<tr>\n".4496"<td colspan=\"5\">$extra</td>\n".4497"</tr>\n";4498}4499print"</table>\n";4500}45014502sub git_heads_body {4503# uses global variable $project4504my($headlist,$head,$from,$to,$extra) =@_;4505$from=0unlessdefined$from;4506$to=$#{$headlist}if(!defined$to||$#{$headlist} <$to);45074508print"<table class=\"heads\">\n";4509my$alternate=1;4510for(my$i=$from;$i<=$to;$i++) {4511my$entry=$headlist->[$i];4512my%ref=%$entry;4513my$curr=$ref{'id'}eq$head;4514if($alternate) {4515print"<tr class=\"dark\">\n";4516}else{4517print"<tr class=\"light\">\n";4518}4519$alternate^=1;4520print"<td><i>$ref{'age'}</i></td>\n".4521($curr?"<td class=\"current_head\">":"<td>") .4522$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),4523-class=>"list name"},esc_html($ref{'name'})) .4524"</td>\n".4525"<td class=\"link\">".4526$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})},"shortlog") ." | ".4527$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})},"log") ." | ".4528$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})},"tree") .4529"</td>\n".4530"</tr>";4531}4532if(defined$extra) {4533print"<tr>\n".4534"<td colspan=\"3\">$extra</td>\n".4535"</tr>\n";4536}4537print"</table>\n";4538}45394540sub git_search_grep_body {4541my($commitlist,$from,$to,$extra) =@_;4542$from=0unlessdefined$from;4543$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);45444545print"<table class=\"commit_search\">\n";4546my$alternate=1;4547for(my$i=$from;$i<=$to;$i++) {4548my%co= %{$commitlist->[$i]};4549if(!%co) {4550next;4551}4552my$commit=$co{'id'};4553if($alternate) {4554print"<tr class=\"dark\">\n";4555}else{4556print"<tr class=\"light\">\n";4557}4558$alternate^=1;4559print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4560 format_author_html('td', \%co,15,5) .4561"<td>".4562$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),4563-class=>"list subject"},4564 chop_and_escape_str($co{'title'},50) ."<br/>");4565my$comment=$co{'comment'};4566foreachmy$line(@$comment) {4567if($line=~m/^(.*?)($search_regexp)(.*)$/i) {4568my($lead,$match,$trail) = ($1,$2,$3);4569$match= chop_str($match,70,5,'center');4570my$contextlen=int((80-length($match))/2);4571$contextlen=30if($contextlen>30);4572$lead= chop_str($lead,$contextlen,10,'left');4573$trail= chop_str($trail,$contextlen,10,'right');45744575$lead= esc_html($lead);4576$match= esc_html($match);4577$trail= esc_html($trail);45784579print"$lead<span class=\"match\">$match</span>$trail<br />";4580}4581}4582print"</td>\n".4583"<td class=\"link\">".4584$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .4585" | ".4586$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})},"commitdiff") .4587" | ".4588$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");4589print"</td>\n".4590"</tr>\n";4591}4592if(defined$extra) {4593print"<tr>\n".4594"<td colspan=\"3\">$extra</td>\n".4595"</tr>\n";4596}4597print"</table>\n";4598}45994600## ======================================================================4601## ======================================================================4602## actions46034604sub git_project_list {4605my$order=$input_params{'order'};4606if(defined$order&&$order!~m/none|project|descr|owner|age/) {4607 die_error(400,"Unknown order parameter");4608}46094610my@list= git_get_projects_list();4611if(!@list) {4612 die_error(404,"No projects found");4613}46144615 git_header_html();4616if(-f $home_text) {4617print"<div class=\"index_include\">\n";4618 insert_file($home_text);4619print"</div>\n";4620}4621print$cgi->startform(-method=>"get") .4622"<p class=\"projsearch\">Search:\n".4623$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".4624"</p>".4625$cgi->end_form() ."\n";4626 git_project_list_body(\@list,$order);4627 git_footer_html();4628}46294630sub git_forks {4631my$order=$input_params{'order'};4632if(defined$order&&$order!~m/none|project|descr|owner|age/) {4633 die_error(400,"Unknown order parameter");4634}46354636my@list= git_get_projects_list($project);4637if(!@list) {4638 die_error(404,"No forks found");4639}46404641 git_header_html();4642 git_print_page_nav('','');4643 git_print_header_div('summary',"$projectforks");4644 git_project_list_body(\@list,$order);4645 git_footer_html();4646}46474648sub git_project_index {4649my@projects= git_get_projects_list($project);46504651print$cgi->header(4652-type =>'text/plain',4653-charset =>'utf-8',4654-content_disposition =>'inline; filename="index.aux"');46554656foreachmy$pr(@projects) {4657if(!exists$pr->{'owner'}) {4658$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");4659}46604661my($path,$owner) = ($pr->{'path'},$pr->{'owner'});4662# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '4663$path=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4664$owner=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4665$path=~s/ /\+/g;4666$owner=~s/ /\+/g;46674668print"$path$owner\n";4669}4670}46714672sub git_summary {4673my$descr= git_get_project_description($project) ||"none";4674my%co= parse_commit("HEAD");4675my%cd=%co? parse_date($co{'committer_epoch'},$co{'committer_tz'}) : ();4676my$head=$co{'id'};46774678my$owner= git_get_project_owner($project);46794680my$refs= git_get_references();4681# These get_*_list functions return one more to allow us to see if4682# there are more ...4683my@taglist= git_get_tags_list(16);4684my@headlist= git_get_heads_list(16);4685my@forklist;4686my$check_forks= gitweb_check_feature('forks');46874688if($check_forks) {4689@forklist= git_get_projects_list($project);4690}46914692 git_header_html();4693 git_print_page_nav('summary','',$head);46944695print"<div class=\"title\"> </div>\n";4696print"<table class=\"projects_list\">\n".4697"<tr id=\"metadata_desc\"><td>description</td><td>". esc_html($descr) ."</td></tr>\n".4698"<tr id=\"metadata_owner\"><td>owner</td><td>". esc_html($owner) ."</td></tr>\n";4699if(defined$cd{'rfc2822'}) {4700print"<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";4701}47024703# use per project git URL list in $projectroot/$project/cloneurl4704# or make project git URL from git base URL and project name4705my$url_tag="URL";4706my@url_list= git_get_project_url_list($project);4707@url_list=map{"$_/$project"}@git_base_url_listunless@url_list;4708foreachmy$git_url(@url_list) {4709next unless$git_url;4710print"<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";4711$url_tag="";4712}47134714# Tag cloud4715my$show_ctags= gitweb_check_feature('ctags');4716if($show_ctags) {4717my$ctags= git_get_project_ctags($project);4718my$cloud= git_populate_project_tagcloud($ctags);4719print"<tr id=\"metadata_ctags\"><td>Content tags:<br />";4720print"</td>\n<td>"unless%$ctags;4721print"<form action=\"$show_ctags\"method=\"post\"><input type=\"hidden\"name=\"p\"value=\"$project\"/>Add: <input type=\"text\"name=\"t\"size=\"8\"/></form>";4722print"</td>\n<td>"if%$ctags;4723print git_show_project_tagcloud($cloud,48);4724print"</td></tr>";4725}47264727print"</table>\n";47284729# If XSS prevention is on, we don't include README.html.4730# TODO: Allow a readme in some safe format.4731if(!$prevent_xss&& -s "$projectroot/$project/README.html") {4732print"<div class=\"title\">readme</div>\n".4733"<div class=\"readme\">\n";4734 insert_file("$projectroot/$project/README.html");4735print"\n</div>\n";# class="readme"4736}47374738# we need to request one more than 16 (0..15) to check if4739# those 16 are all4740my@commitlist=$head? parse_commits($head,17) : ();4741if(@commitlist) {4742 git_print_header_div('shortlog');4743 git_shortlog_body(\@commitlist,0,15,$refs,4744$#commitlist<=15?undef:4745$cgi->a({-href => href(action=>"shortlog")},"..."));4746}47474748if(@taglist) {4749 git_print_header_div('tags');4750 git_tags_body(\@taglist,0,15,4751$#taglist<=15?undef:4752$cgi->a({-href => href(action=>"tags")},"..."));4753}47544755if(@headlist) {4756 git_print_header_div('heads');4757 git_heads_body(\@headlist,$head,0,15,4758$#headlist<=15?undef:4759$cgi->a({-href => href(action=>"heads")},"..."));4760}47614762if(@forklist) {4763 git_print_header_div('forks');4764 git_project_list_body(\@forklist,'age',0,15,4765$#forklist<=15?undef:4766$cgi->a({-href => href(action=>"forks")},"..."),4767'no_header');4768}47694770 git_footer_html();4771}47724773sub git_tag {4774my$head= git_get_head_hash($project);4775 git_header_html();4776 git_print_page_nav('','',$head,undef,$head);4777my%tag= parse_tag($hash);47784779if(!%tag) {4780 die_error(404,"Unknown tag object");4781}47824783 git_print_header_div('commit', esc_html($tag{'name'}),$hash);4784print"<div class=\"title_text\">\n".4785"<table class=\"object_header\">\n".4786"<tr>\n".4787"<td>object</td>\n".4788"<td>".$cgi->a({-class=>"list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},4789$tag{'object'}) ."</td>\n".4790"<td class=\"link\">".$cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},4791$tag{'type'}) ."</td>\n".4792"</tr>\n";4793if(defined($tag{'author'})) {4794 git_print_authorship_rows(\%tag,'author');4795}4796print"</table>\n\n".4797"</div>\n";4798print"<div class=\"page_body\">";4799my$comment=$tag{'comment'};4800foreachmy$line(@$comment) {4801chomp$line;4802print esc_html($line, -nbsp=>1) ."<br/>\n";4803}4804print"</div>\n";4805 git_footer_html();4806}48074808sub git_blame_common {4809my$format=shift||'porcelain';4810if($formateq'porcelain'&&$cgi->param('js')) {4811$format='incremental';4812$action='blame_incremental';# for page title etc4813}48144815# permissions4816 gitweb_check_feature('blame')4817or die_error(403,"Blame view not allowed");48184819# error checking4820 die_error(400,"No file name given")unless$file_name;4821$hash_base||= git_get_head_hash($project);4822 die_error(404,"Couldn't find base commit")unless$hash_base;4823my%co= parse_commit($hash_base)4824or die_error(404,"Commit not found");4825my$ftype="blob";4826if(!defined$hash) {4827$hash= git_get_hash_by_path($hash_base,$file_name,"blob")4828or die_error(404,"Error looking up file");4829}else{4830$ftype= git_get_type($hash);4831if($ftype!~"blob") {4832 die_error(400,"Object is not a blob");4833}4834}48354836my$fd;4837if($formateq'incremental') {4838# get file contents (as base)4839open$fd,"-|", git_cmd(),'cat-file','blob',$hash4840or die_error(500,"Open git-cat-file failed");4841}elsif($formateq'data') {4842# run git-blame --incremental4843open$fd,"-|", git_cmd(),"blame","--incremental",4844$hash_base,"--",$file_name4845or die_error(500,"Open git-blame --incremental failed");4846}else{4847# run git-blame --porcelain4848open$fd,"-|", git_cmd(),"blame",'-p',4849$hash_base,'--',$file_name4850or die_error(500,"Open git-blame --porcelain failed");4851}48524853# incremental blame data returns early4854if($formateq'data') {4855print$cgi->header(4856-type=>"text/plain", -charset =>"utf-8",4857-status=>"200 OK");4858local$| =1;# output autoflush4859printwhile<$fd>;4860close$fd4861or print"ERROR$!\n";48624863print'END';4864if(defined$t0&& gitweb_check_feature('timed')) {4865print' '.4866 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).4867' '.$number_of_git_cmds;4868}4869print"\n";48704871return;4872}48734874# page header4875 git_header_html();4876my$formats_nav=4877$cgi->a({-href => href(action=>"blob", -replay=>1)},4878"blob") .4879" | ".4880$cgi->a({-href => href(action=>"history", -replay=>1)},4881"history") .4882" | ".4883$cgi->a({-href => href(action=>$action, file_name=>$file_name)},4884"HEAD");4885 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);4886 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);4887 git_print_page_path($file_name,$ftype,$hash_base);48884889# page body4890if($formateq'incremental') {4891print"<noscript>\n<div class=\"error\"><center><b>\n".4892"This page requires JavaScript to run.\nUse ".4893$cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},4894'this page').4895" instead.\n".4896"</b></center></div>\n</noscript>\n";48974898print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;4899}49004901print qq!<div class="page_body">\n!;4902print qq!<div id="progress_info">.../ ...</div>\n!4903if($formateq'incremental');4904print qq!<table id="blame_table"class="blame" width="100%">\n!.4905#qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.4906 qq!<thead>\n!.4907 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.4908 qq!</thead>\n!.4909 qq!<tbody>\n!;49104911my@rev_color=qw(light dark);4912my$num_colors=scalar(@rev_color);4913my$current_color=0;49144915if($formateq'incremental') {4916my$color_class=$rev_color[$current_color];49174918#contents of a file4919my$linenr=0;4920 LINE:4921while(my$line= <$fd>) {4922chomp$line;4923$linenr++;49244925print qq!<tr id="l$linenr"class="$color_class">!.4926 qq!<td class="sha1"><a href=""> </a></td>!.4927 qq!<td class="linenr">!.4928 qq!<a class="linenr" href="">$linenr</a></td>!;4929print qq!<td class="pre">! . esc_html($line) ."</td>\n";4930print qq!</tr>\n!;4931}49324933}else{# porcelain, i.e. ordinary blame4934my%metainfo= ();# saves information about commits49354936# blame data4937 LINE:4938while(my$line= <$fd>) {4939chomp$line;4940# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]4941# no <lines in group> for subsequent lines in group of lines4942my($full_rev,$orig_lineno,$lineno,$group_size) =4943($line=~/^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);4944if(!exists$metainfo{$full_rev}) {4945$metainfo{$full_rev} = {'nprevious'=>0};4946}4947my$meta=$metainfo{$full_rev};4948my$data;4949while($data= <$fd>) {4950chomp$data;4951last if($data=~s/^\t//);# contents of line4952if($data=~/^(\S+)(?: (.*))?$/) {4953$meta->{$1} =$2unlessexists$meta->{$1};4954}4955if($data=~/^previous /) {4956$meta->{'nprevious'}++;4957}4958}4959my$short_rev=substr($full_rev,0,8);4960my$author=$meta->{'author'};4961my%date=4962 parse_date($meta->{'author-time'},$meta->{'author-tz'});4963my$date=$date{'iso-tz'};4964if($group_size) {4965$current_color= ($current_color+1) %$num_colors;4966}4967my$tr_class=$rev_color[$current_color];4968$tr_class.=' boundary'if(exists$meta->{'boundary'});4969$tr_class.=' no-previous'if($meta->{'nprevious'} ==0);4970$tr_class.=' multiple-previous'if($meta->{'nprevious'} >1);4971print"<tr id=\"l$lineno\"class=\"$tr_class\">\n";4972if($group_size) {4973print"<td class=\"sha1\"";4974print" title=\"". esc_html($author) .",$date\"";4975print" rowspan=\"$group_size\""if($group_size>1);4976print">";4977print$cgi->a({-href => href(action=>"commit",4978 hash=>$full_rev,4979 file_name=>$file_name)},4980 esc_html($short_rev));4981if($group_size>=2) {4982my@author_initials= ($author=~/\b([[:upper:]])\B/g);4983if(@author_initials) {4984print"<br />".4985 esc_html(join('',@author_initials));4986# or join('.', ...)4987}4988}4989print"</td>\n";4990}4991# 'previous' <sha1 of parent commit> <filename at commit>4992if(exists$meta->{'previous'} &&4993$meta->{'previous'} =~/^([a-fA-F0-9]{40}) (.*)$/) {4994$meta->{'parent'} =$1;4995$meta->{'file_parent'} = unquote($2);4996}4997my$linenr_commit=4998exists($meta->{'parent'}) ?4999$meta->{'parent'} :$full_rev;5000my$linenr_filename=5001exists($meta->{'file_parent'}) ?5002$meta->{'file_parent'} : unquote($meta->{'filename'});5003my$blamed= href(action =>'blame',5004 file_name =>$linenr_filename,5005 hash_base =>$linenr_commit);5006print"<td class=\"linenr\">";5007print$cgi->a({ -href =>"$blamed#l$orig_lineno",5008-class=>"linenr"},5009 esc_html($lineno));5010print"</td>";5011print"<td class=\"pre\">". esc_html($data) ."</td>\n";5012print"</tr>\n";5013}# end while50145015}50165017# footer5018print"</tbody>\n".5019"</table>\n";# class="blame"5020print"</div>\n";# class="blame_body"5021close$fd5022or print"Reading blob failed\n";50235024 git_footer_html();5025}50265027sub git_blame {5028 git_blame_common();5029}50305031sub git_blame_incremental {5032 git_blame_common('incremental');5033}50345035sub git_blame_data {5036 git_blame_common('data');5037}50385039sub git_tags {5040my$head= git_get_head_hash($project);5041 git_header_html();5042 git_print_page_nav('','',$head,undef,$head);5043 git_print_header_div('summary',$project);50445045my@tagslist= git_get_tags_list();5046if(@tagslist) {5047 git_tags_body(\@tagslist);5048}5049 git_footer_html();5050}50515052sub git_heads {5053my$head= git_get_head_hash($project);5054 git_header_html();5055 git_print_page_nav('','',$head,undef,$head);5056 git_print_header_div('summary',$project);50575058my@headslist= git_get_heads_list();5059if(@headslist) {5060 git_heads_body(\@headslist,$head);5061}5062 git_footer_html();5063}50645065sub git_blob_plain {5066my$type=shift;5067my$expires;50685069if(!defined$hash) {5070if(defined$file_name) {5071my$base=$hash_base|| git_get_head_hash($project);5072$hash= git_get_hash_by_path($base,$file_name,"blob")5073or die_error(404,"Cannot find file");5074}else{5075 die_error(400,"No file name defined");5076}5077}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5078# blobs defined by non-textual hash id's can be cached5079$expires="+1d";5080}50815082open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5083or die_error(500,"Open git-cat-file blob '$hash' failed");50845085# content-type (can include charset)5086$type= blob_contenttype($fd,$file_name,$type);50875088# "save as" filename, even when no $file_name is given5089my$save_as="$hash";5090if(defined$file_name) {5091$save_as=$file_name;5092}elsif($type=~m/^text\//) {5093$save_as.='.txt';5094}50955096# With XSS prevention on, blobs of all types except a few known safe5097# ones are served with "Content-Disposition: attachment" to make sure5098# they don't run in our security domain. For certain image types,5099# blob view writes an <img> tag referring to blob_plain view, and we5100# want to be sure not to break that by serving the image as an5101# attachment (though Firefox 3 doesn't seem to care).5102my$sandbox=$prevent_xss&&5103$type!~m!^(?:text/plain|image/(?:gif|png|jpeg))$!;51045105print$cgi->header(5106-type =>$type,5107-expires =>$expires,5108-content_disposition =>5109($sandbox?'attachment':'inline')5110.'; filename="'.$save_as.'"');5111local$/=undef;5112binmode STDOUT,':raw';5113print<$fd>;5114binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5115close$fd;5116}51175118sub git_blob {5119my$expires;51205121if(!defined$hash) {5122if(defined$file_name) {5123my$base=$hash_base|| git_get_head_hash($project);5124$hash= git_get_hash_by_path($base,$file_name,"blob")5125or die_error(404,"Cannot find file");5126}else{5127 die_error(400,"No file name defined");5128}5129}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5130# blobs defined by non-textual hash id's can be cached5131$expires="+1d";5132}51335134my$have_blame= gitweb_check_feature('blame');5135open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5136or die_error(500,"Couldn't cat$file_name,$hash");5137my$mimetype= blob_mimetype($fd,$file_name);5138if($mimetype!~m!^(?:text/|image/(?:gif|png|jpeg)$)!&& -B $fd) {5139close$fd;5140return git_blob_plain($mimetype);5141}5142# we can have blame only for text/* mimetype5143$have_blame&&= ($mimetype=~m!^text/!);51445145 git_header_html(undef,$expires);5146my$formats_nav='';5147if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5148if(defined$file_name) {5149if($have_blame) {5150$formats_nav.=5151$cgi->a({-href => href(action=>"blame", -replay=>1)},5152"blame") .5153" | ";5154}5155$formats_nav.=5156$cgi->a({-href => href(action=>"history", -replay=>1)},5157"history") .5158" | ".5159$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5160"raw") .5161" | ".5162$cgi->a({-href => href(action=>"blob",5163 hash_base=>"HEAD", file_name=>$file_name)},5164"HEAD");5165}else{5166$formats_nav.=5167$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5168"raw");5169}5170 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5171 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5172}else{5173print"<div class=\"page_nav\">\n".5174"<br/><br/></div>\n".5175"<div class=\"title\">$hash</div>\n";5176}5177 git_print_page_path($file_name,"blob",$hash_base);5178print"<div class=\"page_body\">\n";5179if($mimetype=~m!^image/!) {5180print qq!<img type="$mimetype"!;5181if($file_name) {5182print qq! alt="$file_name" title="$file_name"!;5183}5184print qq! src="! .5185 href(action=>"blob_plain", hash=>$hash,5186 hash_base=>$hash_base, file_name=>$file_name) .5187 qq!"/>\n!;5188}else{5189my$nr;5190while(my$line= <$fd>) {5191chomp$line;5192$nr++;5193$line= untabify($line);5194printf"<div class=\"pre\"><a id=\"l%i\"href=\"#l%i\"class=\"linenr\">%4i</a>%s</div>\n",5195$nr,$nr,$nr, esc_html($line, -nbsp=>1);5196}5197}5198close$fd5199or print"Reading blob failed.\n";5200print"</div>";5201 git_footer_html();5202}52035204sub git_tree {5205if(!defined$hash_base) {5206$hash_base="HEAD";5207}5208if(!defined$hash) {5209if(defined$file_name) {5210$hash= git_get_hash_by_path($hash_base,$file_name,"tree");5211}else{5212$hash=$hash_base;5213}5214}5215 die_error(404,"No such tree")unlessdefined($hash);52165217my@entries= ();5218{5219local$/="\0";5220open my$fd,"-|", git_cmd(),"ls-tree",'-z',$hash5221or die_error(500,"Open git-ls-tree failed");5222@entries=map{chomp;$_} <$fd>;5223close$fd5224or die_error(404,"Reading tree failed");5225}52265227my$refs= git_get_references();5228my$ref= format_ref_marker($refs,$hash_base);5229 git_header_html();5230my$basedir='';5231my$have_blame= gitweb_check_feature('blame');5232if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5233my@views_nav= ();5234if(defined$file_name) {5235push@views_nav,5236$cgi->a({-href => href(action=>"history", -replay=>1)},5237"history"),5238$cgi->a({-href => href(action=>"tree",5239 hash_base=>"HEAD", file_name=>$file_name)},5240"HEAD"),5241}5242my$snapshot_links= format_snapshot_links($hash);5243if(defined$snapshot_links) {5244# FIXME: Should be available when we have no hash base as well.5245push@views_nav,$snapshot_links;5246}5247 git_print_page_nav('tree','',$hash_base,undef,undef,join(' | ',@views_nav));5248 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash_base);5249}else{5250undef$hash_base;5251print"<div class=\"page_nav\">\n";5252print"<br/><br/></div>\n";5253print"<div class=\"title\">$hash</div>\n";5254}5255if(defined$file_name) {5256$basedir=$file_name;5257if($basedirne''&&substr($basedir, -1)ne'/') {5258$basedir.='/';5259}5260 git_print_page_path($file_name,'tree',$hash_base);5261}5262print"<div class=\"page_body\">\n";5263print"<table class=\"tree\">\n";5264my$alternate=1;5265# '..' (top directory) link if possible5266if(defined$hash_base&&5267defined$file_name&&$file_name=~m![^/]+$!) {5268if($alternate) {5269print"<tr class=\"dark\">\n";5270}else{5271print"<tr class=\"light\">\n";5272}5273$alternate^=1;52745275my$up=$file_name;5276$up=~s!/?[^/]+$!!;5277undef$upunless$up;5278# based on git_print_tree_entry5279print'<td class="mode">'. mode_str('040000') ."</td>\n";5280print'<td class="list">';5281print$cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,5282 file_name=>$up)},5283"..");5284print"</td>\n";5285print"<td class=\"link\"></td>\n";52865287print"</tr>\n";5288}5289foreachmy$line(@entries) {5290my%t= parse_ls_tree_line($line, -z =>1);52915292if($alternate) {5293print"<tr class=\"dark\">\n";5294}else{5295print"<tr class=\"light\">\n";5296}5297$alternate^=1;52985299 git_print_tree_entry(\%t,$basedir,$hash_base,$have_blame);53005301print"</tr>\n";5302}5303print"</table>\n".5304"</div>";5305 git_footer_html();5306}53075308sub git_snapshot {5309my$format=$input_params{'snapshot_format'};5310if(!@snapshot_fmts) {5311 die_error(403,"Snapshots not allowed");5312}5313# default to first supported snapshot format5314$format||=$snapshot_fmts[0];5315if($format!~m/^[a-z0-9]+$/) {5316 die_error(400,"Invalid snapshot format parameter");5317}elsif(!exists($known_snapshot_formats{$format})) {5318 die_error(400,"Unknown snapshot format");5319}elsif(!grep($_eq$format,@snapshot_fmts)) {5320 die_error(403,"Unsupported snapshot format");5321}53225323if(!defined$hash) {5324$hash= git_get_head_hash($project);5325}53265327my$name=$project;5328$name=~ s,([^/])/*\.git$,$1,;5329$name= basename($name);5330my$filename= to_utf8($name);5331$name=~s/\047/\047\\\047\047/g;5332my$cmd;5333$filename.="-$hash$known_snapshot_formats{$format}{'suffix'}";5334$cmd= quote_command(5335 git_cmd(),'archive',5336"--format=$known_snapshot_formats{$format}{'format'}",5337"--prefix=$name/",$hash);5338if(exists$known_snapshot_formats{$format}{'compressor'}) {5339$cmd.=' | '. quote_command(@{$known_snapshot_formats{$format}{'compressor'}});5340}53415342print$cgi->header(5343-type =>$known_snapshot_formats{$format}{'type'},5344-content_disposition =>'inline; filename="'."$filename".'"',5345-status =>'200 OK');53465347open my$fd,"-|",$cmd5348or die_error(500,"Execute git-archive failed");5349binmode STDOUT,':raw';5350print<$fd>;5351binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5352close$fd;5353}53545355sub git_log {5356my$head= git_get_head_hash($project);5357if(!defined$hash) {5358$hash=$head;5359}5360if(!defined$page) {5361$page=0;5362}5363my$refs= git_get_references();53645365my@commitlist= parse_commits($hash,101, (100*$page));53665367my$paging_nav= format_paging_nav('log',$hash,$head,$page,$#commitlist>=100);53685369my($patch_max) = gitweb_get_feature('patches');5370if($patch_max) {5371if($patch_max<0||@commitlist<=$patch_max) {5372$paging_nav.=" ⋅ ".5373$cgi->a({-href => href(action=>"patches", -replay=>1)},5374"patches");5375}5376}53775378 git_header_html();5379 git_print_page_nav('log','',$hash,undef,undef,$paging_nav);53805381if(!@commitlist) {5382my%co= parse_commit($hash);53835384 git_print_header_div('summary',$project);5385print"<div class=\"page_body\"> Last change$co{'age_string'}.<br/><br/></div>\n";5386}5387my$to= ($#commitlist>=99) ? (99) : ($#commitlist);5388for(my$i=0;$i<=$to;$i++) {5389my%co= %{$commitlist[$i]};5390next if!%co;5391my$commit=$co{'id'};5392my$ref= format_ref_marker($refs,$commit);5393my%ad= parse_date($co{'author_epoch'});5394 git_print_header_div('commit',5395"<span class=\"age\">$co{'age_string'}</span>".5396 esc_html($co{'title'}) .$ref,5397$commit);5398print"<div class=\"title_text\">\n".5399"<div class=\"log_link\">\n".5400$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") .5401" | ".5402$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") .5403" | ".5404$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree") .5405"<br/>\n".5406"</div>\n";5407 git_print_authorship(\%co, -tag =>'span');5408print"<br/>\n</div>\n";54095410print"<div class=\"log_body\">\n";5411 git_print_log($co{'comment'}, -final_empty_line=>1);5412print"</div>\n";5413}5414if($#commitlist>=100) {5415print"<div class=\"page_nav\">\n";5416print$cgi->a({-href => href(-replay=>1, page=>$page+1),5417-accesskey =>"n", -title =>"Alt-n"},"next");5418print"</div>\n";5419}5420 git_footer_html();5421}54225423sub git_commit {5424$hash||=$hash_base||"HEAD";5425my%co= parse_commit($hash)5426or die_error(404,"Unknown commit object");54275428my$parent=$co{'parent'};5429my$parents=$co{'parents'};# listref54305431# we need to prepare $formats_nav before any parameter munging5432my$formats_nav;5433if(!defined$parent) {5434# --root commitdiff5435$formats_nav.='(initial)';5436}elsif(@$parents==1) {5437# single parent commit5438$formats_nav.=5439'(parent: '.5440$cgi->a({-href => href(action=>"commit",5441 hash=>$parent)},5442 esc_html(substr($parent,0,7))) .5443')';5444}else{5445# merge commit5446$formats_nav.=5447'(merge: '.5448join(' ',map{5449$cgi->a({-href => href(action=>"commit",5450 hash=>$_)},5451 esc_html(substr($_,0,7)));5452}@$parents) .5453')';5454}5455if(gitweb_check_feature('patches')) {5456$formats_nav.=" | ".5457$cgi->a({-href => href(action=>"patch", -replay=>1)},5458"patch");5459}54605461if(!defined$parent) {5462$parent="--root";5463}5464my@difftree;5465open my$fd,"-|", git_cmd(),"diff-tree",'-r',"--no-commit-id",5466@diff_opts,5467(@$parents<=1?$parent:'-c'),5468$hash,"--"5469or die_error(500,"Open git-diff-tree failed");5470@difftree=map{chomp;$_} <$fd>;5471close$fdor die_error(404,"Reading git-diff-tree failed");54725473# non-textual hash id's can be cached5474my$expires;5475if($hash=~m/^[0-9a-fA-F]{40}$/) {5476$expires="+1d";5477}5478my$refs= git_get_references();5479my$ref= format_ref_marker($refs,$co{'id'});54805481 git_header_html(undef,$expires);5482 git_print_page_nav('commit','',5483$hash,$co{'tree'},$hash,5484$formats_nav);54855486if(defined$co{'parent'}) {5487 git_print_header_div('commitdiff', esc_html($co{'title'}) .$ref,$hash);5488}else{5489 git_print_header_div('tree', esc_html($co{'title'}) .$ref,$co{'tree'},$hash);5490}5491print"<div class=\"title_text\">\n".5492"<table class=\"object_header\">\n";5493 git_print_authorship_rows(\%co);5494print"<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";5495print"<tr>".5496"<td>tree</td>".5497"<td class=\"sha1\">".5498$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),5499class=>"list"},$co{'tree'}) .5500"</td>".5501"<td class=\"link\">".5502$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},5503"tree");5504my$snapshot_links= format_snapshot_links($hash);5505if(defined$snapshot_links) {5506print" | ".$snapshot_links;5507}5508print"</td>".5509"</tr>\n";55105511foreachmy$par(@$parents) {5512print"<tr>".5513"<td>parent</td>".5514"<td class=\"sha1\">".5515$cgi->a({-href => href(action=>"commit", hash=>$par),5516class=>"list"},$par) .5517"</td>".5518"<td class=\"link\">".5519$cgi->a({-href => href(action=>"commit", hash=>$par)},"commit") .5520" | ".5521$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)},"diff") .5522"</td>".5523"</tr>\n";5524}5525print"</table>".5526"</div>\n";55275528print"<div class=\"page_body\">\n";5529 git_print_log($co{'comment'});5530print"</div>\n";55315532 git_difftree_body(\@difftree,$hash,@$parents);55335534 git_footer_html();5535}55365537sub git_object {5538# object is defined by:5539# - hash or hash_base alone5540# - hash_base and file_name5541my$type;55425543# - hash or hash_base alone5544if($hash|| ($hash_base&& !defined$file_name)) {5545my$object_id=$hash||$hash_base;55465547open my$fd,"-|", quote_command(5548 git_cmd(),'cat-file','-t',$object_id) .' 2> /dev/null'5549or die_error(404,"Object does not exist");5550$type= <$fd>;5551chomp$type;5552close$fd5553or die_error(404,"Object does not exist");55545555# - hash_base and file_name5556}elsif($hash_base&&defined$file_name) {5557$file_name=~ s,/+$,,;55585559system(git_cmd(),"cat-file",'-e',$hash_base) ==05560or die_error(404,"Base object does not exist");55615562# here errors should not hapen5563open my$fd,"-|", git_cmd(),"ls-tree",$hash_base,"--",$file_name5564or die_error(500,"Open git-ls-tree failed");5565my$line= <$fd>;5566close$fd;55675568#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'5569unless($line&&$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {5570 die_error(404,"File or directory for given base does not exist");5571}5572$type=$2;5573$hash=$3;5574}else{5575 die_error(400,"Not enough information to find object");5576}55775578print$cgi->redirect(-uri => href(action=>$type, -full=>1,5579 hash=>$hash, hash_base=>$hash_base,5580 file_name=>$file_name),5581-status =>'302 Found');5582}55835584sub git_blobdiff {5585my$format=shift||'html';55865587my$fd;5588my@difftree;5589my%diffinfo;5590my$expires;55915592# preparing $fd and %diffinfo for git_patchset_body5593# new style URI5594if(defined$hash_base&&defined$hash_parent_base) {5595if(defined$file_name) {5596# read raw output5597open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5598$hash_parent_base,$hash_base,5599"--", (defined$file_parent?$file_parent: ()),$file_name5600or die_error(500,"Open git-diff-tree failed");5601@difftree=map{chomp;$_} <$fd>;5602close$fd5603or die_error(404,"Reading git-diff-tree failed");5604@difftree5605or die_error(404,"Blob diff not found");56065607}elsif(defined$hash&&5608$hash=~/[0-9a-fA-F]{40}/) {5609# try to find filename from $hash56105611# read filtered raw output5612open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5613$hash_parent_base,$hash_base,"--"5614or die_error(500,"Open git-diff-tree failed");5615@difftree=5616# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'5617# $hash == to_id5618grep{/^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/}5619map{chomp;$_} <$fd>;5620close$fd5621or die_error(404,"Reading git-diff-tree failed");5622@difftree5623or die_error(404,"Blob diff not found");56245625}else{5626 die_error(400,"Missing one of the blob diff parameters");5627}56285629if(@difftree>1) {5630 die_error(400,"Ambiguous blob diff specification");5631}56325633%diffinfo= parse_difftree_raw_line($difftree[0]);5634$file_parent||=$diffinfo{'from_file'} ||$file_name;5635$file_name||=$diffinfo{'to_file'};56365637$hash_parent||=$diffinfo{'from_id'};5638$hash||=$diffinfo{'to_id'};56395640# non-textual hash id's can be cached5641if($hash_base=~m/^[0-9a-fA-F]{40}$/&&5642$hash_parent_base=~m/^[0-9a-fA-F]{40}$/) {5643$expires='+1d';5644}56455646# open patch output5647open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5648'-p', ($formateq'html'?"--full-index": ()),5649$hash_parent_base,$hash_base,5650"--", (defined$file_parent?$file_parent: ()),$file_name5651or die_error(500,"Open git-diff-tree failed");5652}56535654# old/legacy style URI -- not generated anymore since 1.4.3.5655if(!%diffinfo) {5656 die_error('404 Not Found',"Missing one of the blob diff parameters")5657}56585659# header5660if($formateq'html') {5661my$formats_nav=5662$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},5663"raw");5664 git_header_html(undef,$expires);5665if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5666 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5667 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5668}else{5669print"<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";5670print"<div class=\"title\">$hashvs$hash_parent</div>\n";5671}5672if(defined$file_name) {5673 git_print_page_path($file_name,"blob",$hash_base);5674}else{5675print"<div class=\"page_path\"></div>\n";5676}56775678}elsif($formateq'plain') {5679print$cgi->header(5680-type =>'text/plain',5681-charset =>'utf-8',5682-expires =>$expires,5683-content_disposition =>'inline; filename="'."$file_name".'.patch"');56845685print"X-Git-Url: ".$cgi->self_url() ."\n\n";56865687}else{5688 die_error(400,"Unknown blobdiff format");5689}56905691# patch5692if($formateq'html') {5693print"<div class=\"page_body\">\n";56945695 git_patchset_body($fd, [ \%diffinfo],$hash_base,$hash_parent_base);5696close$fd;56975698print"</div>\n";# class="page_body"5699 git_footer_html();57005701}else{5702while(my$line= <$fd>) {5703$line=~s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;5704$line=~s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;57055706print$line;57075708last if$line=~m!^\+\+\+!;5709}5710local$/=undef;5711print<$fd>;5712close$fd;5713}5714}57155716sub git_blobdiff_plain {5717 git_blobdiff('plain');5718}57195720sub git_commitdiff {5721my%params=@_;5722my$format=$params{-format} ||'html';57235724my($patch_max) = gitweb_get_feature('patches');5725if($formateq'patch') {5726 die_error(403,"Patch view not allowed")unless$patch_max;5727}57285729$hash||=$hash_base||"HEAD";5730my%co= parse_commit($hash)5731or die_error(404,"Unknown commit object");57325733# choose format for commitdiff for merge5734if(!defined$hash_parent&& @{$co{'parents'}} >1) {5735$hash_parent='--cc';5736}5737# we need to prepare $formats_nav before almost any parameter munging5738my$formats_nav;5739if($formateq'html') {5740$formats_nav=5741$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},5742"raw");5743if($patch_max) {5744$formats_nav.=" | ".5745$cgi->a({-href => href(action=>"patch", -replay=>1)},5746"patch");5747}57485749if(defined$hash_parent&&5750$hash_parentne'-c'&&$hash_parentne'--cc') {5751# commitdiff with two commits given5752my$hash_parent_short=$hash_parent;5753if($hash_parent=~m/^[0-9a-fA-F]{40}$/) {5754$hash_parent_short=substr($hash_parent,0,7);5755}5756$formats_nav.=5757' (from';5758for(my$i=0;$i< @{$co{'parents'}};$i++) {5759if($co{'parents'}[$i]eq$hash_parent) {5760$formats_nav.=' parent '. ($i+1);5761last;5762}5763}5764$formats_nav.=': '.5765$cgi->a({-href => href(action=>"commitdiff",5766 hash=>$hash_parent)},5767 esc_html($hash_parent_short)) .5768')';5769}elsif(!$co{'parent'}) {5770# --root commitdiff5771$formats_nav.=' (initial)';5772}elsif(scalar@{$co{'parents'}} ==1) {5773# single parent commit5774$formats_nav.=5775' (parent: '.5776$cgi->a({-href => href(action=>"commitdiff",5777 hash=>$co{'parent'})},5778 esc_html(substr($co{'parent'},0,7))) .5779')';5780}else{5781# merge commit5782if($hash_parenteq'--cc') {5783$formats_nav.=' | '.5784$cgi->a({-href => href(action=>"commitdiff",5785 hash=>$hash, hash_parent=>'-c')},5786'combined');5787}else{# $hash_parent eq '-c'5788$formats_nav.=' | '.5789$cgi->a({-href => href(action=>"commitdiff",5790 hash=>$hash, hash_parent=>'--cc')},5791'compact');5792}5793$formats_nav.=5794' (merge: '.5795join(' ',map{5796$cgi->a({-href => href(action=>"commitdiff",5797 hash=>$_)},5798 esc_html(substr($_,0,7)));5799} @{$co{'parents'}} ) .5800')';5801}5802}58035804my$hash_parent_param=$hash_parent;5805if(!defined$hash_parent_param) {5806# --cc for multiple parents, --root for parentless5807$hash_parent_param=5808@{$co{'parents'}} >1?'--cc':$co{'parent'} ||'--root';5809}58105811# read commitdiff5812my$fd;5813my@difftree;5814if($formateq'html') {5815open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5816"--no-commit-id","--patch-with-raw","--full-index",5817$hash_parent_param,$hash,"--"5818or die_error(500,"Open git-diff-tree failed");58195820while(my$line= <$fd>) {5821chomp$line;5822# empty line ends raw part of diff-tree output5823last unless$line;5824push@difftree,scalar parse_difftree_raw_line($line);5825}58265827}elsif($formateq'plain') {5828open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5829'-p',$hash_parent_param,$hash,"--"5830or die_error(500,"Open git-diff-tree failed");5831}elsif($formateq'patch') {5832# For commit ranges, we limit the output to the number of5833# patches specified in the 'patches' feature.5834# For single commits, we limit the output to a single patch,5835# diverging from the git-format-patch default.5836my@commit_spec= ();5837if($hash_parent) {5838if($patch_max>0) {5839push@commit_spec,"-$patch_max";5840}5841push@commit_spec,'-n',"$hash_parent..$hash";5842}else{5843if($params{-single}) {5844push@commit_spec,'-1';5845}else{5846if($patch_max>0) {5847push@commit_spec,"-$patch_max";5848}5849push@commit_spec,"-n";5850}5851push@commit_spec,'--root',$hash;5852}5853open$fd,"-|", git_cmd(),"format-patch",'--encoding=utf8',5854'--stdout',@commit_spec5855or die_error(500,"Open git-format-patch failed");5856}else{5857 die_error(400,"Unknown commitdiff format");5858}58595860# non-textual hash id's can be cached5861my$expires;5862if($hash=~m/^[0-9a-fA-F]{40}$/) {5863$expires="+1d";5864}58655866# write commit message5867if($formateq'html') {5868my$refs= git_get_references();5869my$ref= format_ref_marker($refs,$co{'id'});58705871 git_header_html(undef,$expires);5872 git_print_page_nav('commitdiff','',$hash,$co{'tree'},$hash,$formats_nav);5873 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash);5874print"<div class=\"title_text\">\n".5875"<table class=\"object_header\">\n";5876 git_print_authorship_rows(\%co);5877print"</table>".5878"</div>\n";5879print"<div class=\"page_body\">\n";5880if(@{$co{'comment'}} >1) {5881print"<div class=\"log\">\n";5882 git_print_log($co{'comment'}, -final_empty_line=>1, -remove_title =>1);5883print"</div>\n";# class="log"5884}58855886}elsif($formateq'plain') {5887my$refs= git_get_references("tags");5888my$tagname= git_get_rev_name_tags($hash);5889my$filename= basename($project) ."-$hash.patch";58905891print$cgi->header(5892-type =>'text/plain',5893-charset =>'utf-8',5894-expires =>$expires,5895-content_disposition =>'inline; filename="'."$filename".'"');5896my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});5897print"From: ". to_utf8($co{'author'}) ."\n";5898print"Date:$ad{'rfc2822'} ($ad{'tz_local'})\n";5899print"Subject: ". to_utf8($co{'title'}) ."\n";59005901print"X-Git-Tag:$tagname\n"if$tagname;5902print"X-Git-Url: ".$cgi->self_url() ."\n\n";59035904foreachmy$line(@{$co{'comment'}}) {5905print to_utf8($line) ."\n";5906}5907print"---\n\n";5908}elsif($formateq'patch') {5909my$filename= basename($project) ."-$hash.patch";59105911print$cgi->header(5912-type =>'text/plain',5913-charset =>'utf-8',5914-expires =>$expires,5915-content_disposition =>'inline; filename="'."$filename".'"');5916}59175918# write patch5919if($formateq'html') {5920my$use_parents= !defined$hash_parent||5921$hash_parenteq'-c'||$hash_parenteq'--cc';5922 git_difftree_body(\@difftree,$hash,5923$use_parents? @{$co{'parents'}} :$hash_parent);5924print"<br/>\n";59255926 git_patchset_body($fd, \@difftree,$hash,5927$use_parents? @{$co{'parents'}} :$hash_parent);5928close$fd;5929print"</div>\n";# class="page_body"5930 git_footer_html();59315932}elsif($formateq'plain') {5933local$/=undef;5934print<$fd>;5935close$fd5936or print"Reading git-diff-tree failed\n";5937}elsif($formateq'patch') {5938local$/=undef;5939print<$fd>;5940close$fd5941or print"Reading git-format-patch failed\n";5942}5943}59445945sub git_commitdiff_plain {5946 git_commitdiff(-format =>'plain');5947}59485949# format-patch-style patches5950sub git_patch {5951 git_commitdiff(-format =>'patch', -single=>1);5952}59535954sub git_patches {5955 git_commitdiff(-format =>'patch');5956}59575958sub git_history {5959if(!defined$hash_base) {5960$hash_base= git_get_head_hash($project);5961}5962if(!defined$page) {5963$page=0;5964}5965my$ftype;5966my%co= parse_commit($hash_base)5967or die_error(404,"Unknown commit object");59685969my$refs= git_get_references();5970my$limit=sprintf("--max-count=%i", (100* ($page+1)));59715972my@commitlist= parse_commits($hash_base,101, (100*$page),5973$file_name,"--full-history")5974or die_error(404,"No such file or directory on given branch");59755976if(!defined$hash&&defined$file_name) {5977# some commits could have deleted file in question,5978# and not have it in tree, but one of them has to have it5979for(my$i=0;$i<=@commitlist;$i++) {5980$hash= git_get_hash_by_path($commitlist[$i]{'id'},$file_name);5981last ifdefined$hash;5982}5983}5984if(defined$hash) {5985$ftype= git_get_type($hash);5986}5987if(!defined$ftype) {5988 die_error(500,"Unknown type of object");5989}59905991my$paging_nav='';5992if($page>0) {5993$paging_nav.=5994$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,5995 file_name=>$file_name)},5996"first");5997$paging_nav.=" ⋅ ".5998$cgi->a({-href => href(-replay=>1, page=>$page-1),5999-accesskey =>"p", -title =>"Alt-p"},"prev");6000}else{6001$paging_nav.="first";6002$paging_nav.=" ⋅ prev";6003}6004my$next_link='';6005if($#commitlist>=100) {6006$next_link=6007$cgi->a({-href => href(-replay=>1, page=>$page+1),6008-accesskey =>"n", -title =>"Alt-n"},"next");6009$paging_nav.=" ⋅$next_link";6010}else{6011$paging_nav.=" ⋅ next";6012}60136014 git_header_html();6015 git_print_page_nav('history','',$hash_base,$co{'tree'},$hash_base,$paging_nav);6016 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);6017 git_print_page_path($file_name,$ftype,$hash_base);60186019 git_history_body(\@commitlist,0,99,6020$refs,$hash_base,$ftype,$next_link);60216022 git_footer_html();6023}60246025sub git_search {6026 gitweb_check_feature('search')or die_error(403,"Search is disabled");6027if(!defined$searchtext) {6028 die_error(400,"Text field is empty");6029}6030if(!defined$hash) {6031$hash= git_get_head_hash($project);6032}6033my%co= parse_commit($hash);6034if(!%co) {6035 die_error(404,"Unknown commit object");6036}6037if(!defined$page) {6038$page=0;6039}60406041$searchtype||='commit';6042if($searchtypeeq'pickaxe') {6043# pickaxe may take all resources of your box and run for several minutes6044# with every query - so decide by yourself how public you make this feature6045 gitweb_check_feature('pickaxe')6046or die_error(403,"Pickaxe is disabled");6047}6048if($searchtypeeq'grep') {6049 gitweb_check_feature('grep')6050or die_error(403,"Grep is disabled");6051}60526053 git_header_html();60546055if($searchtypeeq'commit'or$searchtypeeq'author'or$searchtypeeq'committer') {6056my$greptype;6057if($searchtypeeq'commit') {6058$greptype="--grep=";6059}elsif($searchtypeeq'author') {6060$greptype="--author=";6061}elsif($searchtypeeq'committer') {6062$greptype="--committer=";6063}6064$greptype.=$searchtext;6065my@commitlist= parse_commits($hash,101, (100*$page),undef,6066$greptype,'--regexp-ignore-case',6067$search_use_regexp?'--extended-regexp':'--fixed-strings');60686069my$paging_nav='';6070if($page>0) {6071$paging_nav.=6072$cgi->a({-href => href(action=>"search", hash=>$hash,6073 searchtext=>$searchtext,6074 searchtype=>$searchtype)},6075"first");6076$paging_nav.=" ⋅ ".6077$cgi->a({-href => href(-replay=>1, page=>$page-1),6078-accesskey =>"p", -title =>"Alt-p"},"prev");6079}else{6080$paging_nav.="first";6081$paging_nav.=" ⋅ prev";6082}6083my$next_link='';6084if($#commitlist>=100) {6085$next_link=6086$cgi->a({-href => href(-replay=>1, page=>$page+1),6087-accesskey =>"n", -title =>"Alt-n"},"next");6088$paging_nav.=" ⋅$next_link";6089}else{6090$paging_nav.=" ⋅ next";6091}60926093if($#commitlist>=100) {6094}60956096 git_print_page_nav('','',$hash,$co{'tree'},$hash,$paging_nav);6097 git_print_header_div('commit', esc_html($co{'title'}),$hash);6098 git_search_grep_body(\@commitlist,0,99,$next_link);6099}61006101if($searchtypeeq'pickaxe') {6102 git_print_page_nav('','',$hash,$co{'tree'},$hash);6103 git_print_header_div('commit', esc_html($co{'title'}),$hash);61046105print"<table class=\"pickaxe search\">\n";6106my$alternate=1;6107local$/="\n";6108open my$fd,'-|', git_cmd(),'--no-pager','log',@diff_opts,6109'--pretty=format:%H','--no-abbrev','--raw',"-S$searchtext",6110($search_use_regexp?'--pickaxe-regex': ());6111undef%co;6112my@files;6113while(my$line= <$fd>) {6114chomp$line;6115next unless$line;61166117my%set= parse_difftree_raw_line($line);6118if(defined$set{'commit'}) {6119# finish previous commit6120if(%co) {6121print"</td>\n".6122"<td class=\"link\">".6123$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6124" | ".6125$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6126print"</td>\n".6127"</tr>\n";6128}61296130if($alternate) {6131print"<tr class=\"dark\">\n";6132}else{6133print"<tr class=\"light\">\n";6134}6135$alternate^=1;6136%co= parse_commit($set{'commit'});6137my$author= chop_and_escape_str($co{'author_name'},15,5);6138print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".6139"<td><i>$author</i></td>\n".6140"<td>".6141$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),6142-class=>"list subject"},6143 chop_and_escape_str($co{'title'},50) ."<br/>");6144}elsif(defined$set{'to_id'}) {6145next if($set{'to_id'} =~m/^0{40}$/);61466147print$cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},6148 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),6149-class=>"list"},6150"<span class=\"match\">". esc_path($set{'file'}) ."</span>") .6151"<br/>\n";6152}6153}6154close$fd;61556156# finish last commit (warning: repetition!)6157if(%co) {6158print"</td>\n".6159"<td class=\"link\">".6160$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6161" | ".6162$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6163print"</td>\n".6164"</tr>\n";6165}61666167print"</table>\n";6168}61696170if($searchtypeeq'grep') {6171 git_print_page_nav('','',$hash,$co{'tree'},$hash);6172 git_print_header_div('commit', esc_html($co{'title'}),$hash);61736174print"<table class=\"grep_search\">\n";6175my$alternate=1;6176my$matches=0;6177local$/="\n";6178open my$fd,"-|", git_cmd(),'grep','-n',6179$search_use_regexp? ('-E','-i') :'-F',6180$searchtext,$co{'tree'};6181my$lastfile='';6182while(my$line= <$fd>) {6183chomp$line;6184my($file,$lno,$ltext,$binary);6185last if($matches++>1000);6186if($line=~/^Binary file (.+) matches$/) {6187$file=$1;6188$binary=1;6189}else{6190(undef,$file,$lno,$ltext) =split(/:/,$line,4);6191}6192if($filene$lastfile) {6193$lastfileand print"</td></tr>\n";6194if($alternate++) {6195print"<tr class=\"dark\">\n";6196}else{6197print"<tr class=\"light\">\n";6198}6199print"<td class=\"list\">".6200$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6201 file_name=>"$file"),6202-class=>"list"}, esc_path($file));6203print"</td><td>\n";6204$lastfile=$file;6205}6206if($binary) {6207print"<div class=\"binary\">Binary file</div>\n";6208}else{6209$ltext= untabify($ltext);6210if($ltext=~m/^(.*)($search_regexp)(.*)$/i) {6211$ltext= esc_html($1, -nbsp=>1);6212$ltext.='<span class="match">';6213$ltext.= esc_html($2, -nbsp=>1);6214$ltext.='</span>';6215$ltext.= esc_html($3, -nbsp=>1);6216}else{6217$ltext= esc_html($ltext, -nbsp=>1);6218}6219print"<div class=\"pre\">".6220$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6221 file_name=>"$file").'#l'.$lno,6222-class=>"linenr"},sprintf('%4i',$lno))6223.' '.$ltext."</div>\n";6224}6225}6226if($lastfile) {6227print"</td></tr>\n";6228if($matches>1000) {6229print"<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";6230}6231}else{6232print"<div class=\"diff nodifferences\">No matches found</div>\n";6233}6234close$fd;62356236print"</table>\n";6237}6238 git_footer_html();6239}62406241sub git_search_help {6242 git_header_html();6243 git_print_page_nav('','',$hash,$hash,$hash);6244print<<EOT;6245<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without6246regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,6247the pattern entered is recognized as the POSIX extended6248<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case6249insensitive).</p>6250<dl>6251<dt><b>commit</b></dt>6252<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>6253EOT6254my$have_grep= gitweb_check_feature('grep');6255if($have_grep) {6256print<<EOT;6257<dt><b>grep</b></dt>6258<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing6259 a different one) are searched for the given pattern. On large trees, this search can take6260a while and put some strain on the server, so please use it with some consideration. Note that6261due to git-grep peculiarity, currently if regexp mode is turned off, the matches are6262case-sensitive.</dd>6263EOT6264}6265print<<EOT;6266<dt><b>author</b></dt>6267<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>6268<dt><b>committer</b></dt>6269<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>6270EOT6271my$have_pickaxe= gitweb_check_feature('pickaxe');6272if($have_pickaxe) {6273print<<EOT;6274<dt><b>pickaxe</b></dt>6275<dd>All commits that caused the string to appear or disappear from any file (changes that6276added, removed or "modified" the string) will be listed. This search can take a while and6277takes a lot of strain on the server, so please use it wisely. Note that since you may be6278interested even in changes just changing the case as well, this search is case sensitive.</dd>6279EOT6280}6281print"</dl>\n";6282 git_footer_html();6283}62846285sub git_shortlog {6286my$head= git_get_head_hash($project);6287if(!defined$hash) {6288$hash=$head;6289}6290if(!defined$page) {6291$page=0;6292}6293my$refs= git_get_references();62946295my$commit_hash=$hash;6296if(defined$hash_parent) {6297$commit_hash="$hash_parent..$hash";6298}6299my@commitlist= parse_commits($commit_hash,101, (100*$page));63006301my$paging_nav= format_paging_nav('shortlog',$hash,$head,$page,$#commitlist>=100);6302my$next_link='';6303if($#commitlist>=100) {6304$next_link=6305$cgi->a({-href => href(-replay=>1, page=>$page+1),6306-accesskey =>"n", -title =>"Alt-n"},"next");6307}6308my$patch_max= gitweb_check_feature('patches');6309if($patch_max) {6310if($patch_max<0||@commitlist<=$patch_max) {6311$paging_nav.=" ⋅ ".6312$cgi->a({-href => href(action=>"patches", -replay=>1)},6313"patches");6314}6315}63166317 git_header_html();6318 git_print_page_nav('shortlog','',$hash,$hash,$hash,$paging_nav);6319 git_print_header_div('summary',$project);63206321 git_shortlog_body(\@commitlist,0,99,$refs,$next_link);63226323 git_footer_html();6324}63256326## ......................................................................6327## feeds (RSS, Atom; OPML)63286329sub git_feed {6330my$format=shift||'atom';6331my$have_blame= gitweb_check_feature('blame');63326333# Atom: http://www.atomenabled.org/developers/syndication/6334# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ6335if($formatne'rss'&&$formatne'atom') {6336 die_error(400,"Unknown web feed format");6337}63386339# log/feed of current (HEAD) branch, log of given branch, history of file/directory6340my$head=$hash||'HEAD';6341my@commitlist= parse_commits($head,150,0,$file_name);63426343my%latest_commit;6344my%latest_date;6345my$content_type="application/$format+xml";6346if(defined$cgi->http('HTTP_ACCEPT') &&6347$cgi->Accept('text/xml') >$cgi->Accept($content_type)) {6348# browser (feed reader) prefers text/xml6349$content_type='text/xml';6350}6351if(defined($commitlist[0])) {6352%latest_commit= %{$commitlist[0]};6353my$latest_epoch=$latest_commit{'committer_epoch'};6354%latest_date= parse_date($latest_epoch);6355my$if_modified=$cgi->http('IF_MODIFIED_SINCE');6356if(defined$if_modified) {6357my$since;6358if(eval{require HTTP::Date;1; }) {6359$since= HTTP::Date::str2time($if_modified);6360}elsif(eval{require Time::ParseDate;1; }) {6361$since= Time::ParseDate::parsedate($if_modified, GMT =>1);6362}6363if(defined$since&&$latest_epoch<=$since) {6364print$cgi->header(6365-type =>$content_type,6366-charset =>'utf-8',6367-last_modified =>$latest_date{'rfc2822'},6368-status =>'304 Not Modified');6369return;6370}6371}6372print$cgi->header(6373-type =>$content_type,6374-charset =>'utf-8',6375-last_modified =>$latest_date{'rfc2822'});6376}else{6377print$cgi->header(6378-type =>$content_type,6379-charset =>'utf-8');6380}63816382# Optimization: skip generating the body if client asks only6383# for Last-Modified date.6384return if($cgi->request_method()eq'HEAD');63856386# header variables6387my$title="$site_name-$project/$action";6388my$feed_type='log';6389if(defined$hash) {6390$title.=" - '$hash'";6391$feed_type='branch log';6392if(defined$file_name) {6393$title.=" ::$file_name";6394$feed_type='history';6395}6396}elsif(defined$file_name) {6397$title.=" -$file_name";6398$feed_type='history';6399}6400$title.="$feed_type";6401my$descr= git_get_project_description($project);6402if(defined$descr) {6403$descr= esc_html($descr);6404}else{6405$descr="$project".6406($formateq'rss'?'RSS':'Atom') .6407" feed";6408}6409my$owner= git_get_project_owner($project);6410$owner= esc_html($owner);64116412#header6413my$alt_url;6414if(defined$file_name) {6415$alt_url= href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);6416}elsif(defined$hash) {6417$alt_url= href(-full=>1, action=>"log", hash=>$hash);6418}else{6419$alt_url= href(-full=>1, action=>"summary");6420}6421print qq!<?xml version="1.0" encoding="utf-8"?>\n!;6422if($formateq'rss') {6423print<<XML;6424<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">6425<channel>6426XML6427print"<title>$title</title>\n".6428"<link>$alt_url</link>\n".6429"<description>$descr</description>\n".6430"<language>en</language>\n".6431# project owner is responsible for 'editorial' content6432"<managingEditor>$owner</managingEditor>\n";6433if(defined$logo||defined$favicon) {6434# prefer the logo to the favicon, since RSS6435# doesn't allow both6436my$img= esc_url($logo||$favicon);6437print"<image>\n".6438"<url>$img</url>\n".6439"<title>$title</title>\n".6440"<link>$alt_url</link>\n".6441"</image>\n";6442}6443if(%latest_date) {6444print"<pubDate>$latest_date{'rfc2822'}</pubDate>\n";6445print"<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";6446}6447print"<generator>gitweb v.$version/$git_version</generator>\n";6448}elsif($formateq'atom') {6449print<<XML;6450<feed xmlns="http://www.w3.org/2005/Atom">6451XML6452print"<title>$title</title>\n".6453"<subtitle>$descr</subtitle>\n".6454'<link rel="alternate" type="text/html" href="'.6455$alt_url.'" />'."\n".6456'<link rel="self" type="'.$content_type.'" href="'.6457$cgi->self_url() .'" />'."\n".6458"<id>". href(-full=>1) ."</id>\n".6459# use project owner for feed author6460"<author><name>$owner</name></author>\n";6461if(defined$favicon) {6462print"<icon>". esc_url($favicon) ."</icon>\n";6463}6464if(defined$logo_url) {6465# not twice as wide as tall: 72 x 27 pixels6466print"<logo>". esc_url($logo) ."</logo>\n";6467}6468if(!%latest_date) {6469# dummy date to keep the feed valid until commits trickle in:6470print"<updated>1970-01-01T00:00:00Z</updated>\n";6471}else{6472print"<updated>$latest_date{'iso-8601'}</updated>\n";6473}6474print"<generator version='$version/$git_version'>gitweb</generator>\n";6475}64766477# contents6478for(my$i=0;$i<=$#commitlist;$i++) {6479my%co= %{$commitlist[$i]};6480my$commit=$co{'id'};6481# we read 150, we always show 30 and the ones more recent than 48 hours6482if(($i>=20) && ((time-$co{'author_epoch'}) >48*60*60)) {6483last;6484}6485my%cd= parse_date($co{'author_epoch'});64866487# get list of changed files6488open my$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6489$co{'parent'} ||"--root",6490$co{'id'},"--", (defined$file_name?$file_name: ())6491ornext;6492my@difftree=map{chomp;$_} <$fd>;6493close$fd6494ornext;64956496# print element (entry, item)6497my$co_url= href(-full=>1, action=>"commitdiff", hash=>$commit);6498if($formateq'rss') {6499print"<item>\n".6500"<title>". esc_html($co{'title'}) ."</title>\n".6501"<author>". esc_html($co{'author'}) ."</author>\n".6502"<pubDate>$cd{'rfc2822'}</pubDate>\n".6503"<guid isPermaLink=\"true\">$co_url</guid>\n".6504"<link>$co_url</link>\n".6505"<description>". esc_html($co{'title'}) ."</description>\n".6506"<content:encoded>".6507"<![CDATA[\n";6508}elsif($formateq'atom') {6509print"<entry>\n".6510"<title type=\"html\">". esc_html($co{'title'}) ."</title>\n".6511"<updated>$cd{'iso-8601'}</updated>\n".6512"<author>\n".6513" <name>". esc_html($co{'author_name'}) ."</name>\n";6514if($co{'author_email'}) {6515print" <email>". esc_html($co{'author_email'}) ."</email>\n";6516}6517print"</author>\n".6518# use committer for contributor6519"<contributor>\n".6520" <name>". esc_html($co{'committer_name'}) ."</name>\n";6521if($co{'committer_email'}) {6522print" <email>". esc_html($co{'committer_email'}) ."</email>\n";6523}6524print"</contributor>\n".6525"<published>$cd{'iso-8601'}</published>\n".6526"<link rel=\"alternate\"type=\"text/html\"href=\"$co_url\"/>\n".6527"<id>$co_url</id>\n".6528"<content type=\"xhtml\"xml:base=\"". esc_url($my_url) ."\">\n".6529"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";6530}6531my$comment=$co{'comment'};6532print"<pre>\n";6533foreachmy$line(@$comment) {6534$line= esc_html($line);6535print"$line\n";6536}6537print"</pre><ul>\n";6538foreachmy$difftree_line(@difftree) {6539my%difftree= parse_difftree_raw_line($difftree_line);6540next if!$difftree{'from_id'};65416542my$file=$difftree{'file'} ||$difftree{'to_file'};65436544print"<li>".6545"[".6546$cgi->a({-href => href(-full=>1, action=>"blobdiff",6547 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},6548 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},6549 file_name=>$file, file_parent=>$difftree{'from_file'}),6550-title =>"diff"},'D');6551if($have_blame) {6552print$cgi->a({-href => href(-full=>1, action=>"blame",6553 file_name=>$file, hash_base=>$commit),6554-title =>"blame"},'B');6555}6556# if this is not a feed of a file history6557if(!defined$file_name||$file_namene$file) {6558print$cgi->a({-href => href(-full=>1, action=>"history",6559 file_name=>$file, hash=>$commit),6560-title =>"history"},'H');6561}6562$file= esc_path($file);6563print"] ".6564"$file</li>\n";6565}6566if($formateq'rss') {6567print"</ul>]]>\n".6568"</content:encoded>\n".6569"</item>\n";6570}elsif($formateq'atom') {6571print"</ul>\n</div>\n".6572"</content>\n".6573"</entry>\n";6574}6575}65766577# end of feed6578if($formateq'rss') {6579print"</channel>\n</rss>\n";6580}elsif($formateq'atom') {6581print"</feed>\n";6582}6583}65846585sub git_rss {6586 git_feed('rss');6587}65886589sub git_atom {6590 git_feed('atom');6591}65926593sub git_opml {6594my@list= git_get_projects_list();65956596print$cgi->header(6597-type =>'text/xml',6598-charset =>'utf-8',6599-content_disposition =>'inline; filename="opml.xml"');66006601print<<XML;6602<?xml version="1.0" encoding="utf-8"?>6603<opml version="1.0">6604<head>6605 <title>$site_nameOPML Export</title>6606</head>6607<body>6608<outline text="git RSS feeds">6609XML66106611foreachmy$pr(@list) {6612my%proj=%$pr;6613my$head= git_get_head_hash($proj{'path'});6614if(!defined$head) {6615next;6616}6617$git_dir="$projectroot/$proj{'path'}";6618my%co= parse_commit($head);6619if(!%co) {6620next;6621}66226623my$path= esc_html(chop_str($proj{'path'},25,5));6624my$rss= href('project'=>$proj{'path'},'action'=>'rss', -full =>1);6625my$html= href('project'=>$proj{'path'},'action'=>'summary', -full =>1);6626print"<outline type=\"rss\"text=\"$path\"title=\"$path\"xmlUrl=\"$rss\"htmlUrl=\"$html\"/>\n";6627}6628print<<XML;6629</outline>6630</body>6631</opml>6632XML6633}