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# 'disabled' => boolean (optional)} 173# 174'tgz'=> { 175'display'=>'tar.gz', 176'type'=>'application/x-gzip', 177'suffix'=>'.tar.gz', 178'format'=>'tar', 179'compressor'=> ['gzip']}, 180 181'tbz2'=> { 182'display'=>'tar.bz2', 183'type'=>'application/x-bzip2', 184'suffix'=>'.tar.bz2', 185'format'=>'tar', 186'compressor'=> ['bzip2']}, 187 188'txz'=> { 189'display'=>'tar.xz', 190'type'=>'application/x-xz', 191'suffix'=>'.tar.xz', 192'format'=>'tar', 193'compressor'=> ['xz'], 194'disabled'=>1}, 195 196'zip'=> { 197'display'=>'zip', 198'type'=>'application/x-zip', 199'suffix'=>'.zip', 200'format'=>'zip'}, 201); 202 203# Aliases so we understand old gitweb.snapshot values in repository 204# configuration. 205our%known_snapshot_format_aliases= ( 206'gzip'=>'tgz', 207'bzip2'=>'tbz2', 208'xz'=>'txz', 209 210# backward compatibility: legacy gitweb config support 211'x-gzip'=>undef,'gz'=>undef, 212'x-bzip2'=>undef,'bz2'=>undef, 213'x-zip'=>undef,''=>undef, 214); 215 216# Pixel sizes for icons and avatars. If the default font sizes or lineheights 217# are changed, it may be appropriate to change these values too via 218# $GITWEB_CONFIG. 219our%avatar_size= ( 220'default'=>16, 221'double'=>32 222); 223 224# Used to set the maximum load that we will still respond to gitweb queries. 225# If server load exceed this value then return "503 server busy" error. 226# If gitweb cannot determined server load, it is taken to be 0. 227# Leave it undefined (or set to 'undef') to turn off load checking. 228our$maxload=300; 229 230# syntax highlighting 231our%highlight_type= ( 232# match by basename 233'SConstruct'=>'py', 234'Program'=>'py', 235'Library'=>'py', 236'Makefile'=>'make', 237# match by extension 238'\.py$'=>'py',# Python 239'\.c$'=>'c', 240'\.h$'=>'c', 241'\.cpp$'=>'cpp', 242'\.cxx$'=>'cpp', 243'\.rb$'=>'ruby', 244'\.java$'=>'java', 245'\.css$'=>'css', 246'\.php3?$'=>'php', 247'\.sh$'=>'sh',# Bash / shell script 248'\.pl$'=>'pl',# Perl 249'\.js$'=>'js',# JavaScript 250'\.tex$'=>'tex',# TeX and LaTeX 251'\.bib$'=>'bib',# BibTeX 252'\.x?html$'=>'xml', 253'\.xml$'=>'xml', 254'\.awk$'=>'awk', 255'\.bat$'=>'bat',# DOS Batch script 256'\.ini$'=>'ini', 257'\.spec$'=>'spec',# RPM Spec 258); 259 260# You define site-wide feature defaults here; override them with 261# $GITWEB_CONFIG as necessary. 262our%feature= ( 263# feature => { 264# 'sub' => feature-sub (subroutine), 265# 'override' => allow-override (boolean), 266# 'default' => [ default options...] (array reference)} 267# 268# if feature is overridable (it means that allow-override has true value), 269# then feature-sub will be called with default options as parameters; 270# return value of feature-sub indicates if to enable specified feature 271# 272# if there is no 'sub' key (no feature-sub), then feature cannot be 273# overriden 274# 275# use gitweb_get_feature(<feature>) to retrieve the <feature> value 276# (an array) or gitweb_check_feature(<feature>) to check if <feature> 277# is enabled 278 279# Enable the 'blame' blob view, showing the last commit that modified 280# each line in the file. This can be very CPU-intensive. 281 282# To enable system wide have in $GITWEB_CONFIG 283# $feature{'blame'}{'default'} = [1]; 284# To have project specific config enable override in $GITWEB_CONFIG 285# $feature{'blame'}{'override'} = 1; 286# and in project config gitweb.blame = 0|1; 287'blame'=> { 288'sub'=>sub{ feature_bool('blame',@_) }, 289'override'=>0, 290'default'=> [0]}, 291 292# Enable the 'snapshot' link, providing a compressed archive of any 293# tree. This can potentially generate high traffic if you have large 294# project. 295 296# Value is a list of formats defined in %known_snapshot_formats that 297# you wish to offer. 298# To disable system wide have in $GITWEB_CONFIG 299# $feature{'snapshot'}{'default'} = []; 300# To have project specific config enable override in $GITWEB_CONFIG 301# $feature{'snapshot'}{'override'} = 1; 302# and in project config, a comma-separated list of formats or "none" 303# to disable. Example: gitweb.snapshot = tbz2,zip; 304'snapshot'=> { 305'sub'=> \&feature_snapshot, 306'override'=>0, 307'default'=> ['tgz']}, 308 309# Enable text search, which will list the commits which match author, 310# committer or commit text to a given string. Enabled by default. 311# Project specific override is not supported. 312'search'=> { 313'override'=>0, 314'default'=> [1]}, 315 316# Enable grep search, which will list the files in currently selected 317# tree containing the given string. Enabled by default. This can be 318# potentially CPU-intensive, of course. 319 320# To enable system wide have in $GITWEB_CONFIG 321# $feature{'grep'}{'default'} = [1]; 322# To have project specific config enable override in $GITWEB_CONFIG 323# $feature{'grep'}{'override'} = 1; 324# and in project config gitweb.grep = 0|1; 325'grep'=> { 326'sub'=>sub{ feature_bool('grep',@_) }, 327'override'=>0, 328'default'=> [1]}, 329 330# Enable the pickaxe search, which will list the commits that modified 331# a given string in a file. This can be practical and quite faster 332# alternative to 'blame', but still potentially CPU-intensive. 333 334# To enable system wide have in $GITWEB_CONFIG 335# $feature{'pickaxe'}{'default'} = [1]; 336# To have project specific config enable override in $GITWEB_CONFIG 337# $feature{'pickaxe'}{'override'} = 1; 338# and in project config gitweb.pickaxe = 0|1; 339'pickaxe'=> { 340'sub'=>sub{ feature_bool('pickaxe',@_) }, 341'override'=>0, 342'default'=> [1]}, 343 344# Enable showing size of blobs in a 'tree' view, in a separate 345# column, similar to what 'ls -l' does. This cost a bit of IO. 346 347# To disable system wide have in $GITWEB_CONFIG 348# $feature{'show-sizes'}{'default'} = [0]; 349# To have project specific config enable override in $GITWEB_CONFIG 350# $feature{'show-sizes'}{'override'} = 1; 351# and in project config gitweb.showsizes = 0|1; 352'show-sizes'=> { 353'sub'=>sub{ feature_bool('showsizes',@_) }, 354'override'=>0, 355'default'=> [1]}, 356 357# Make gitweb use an alternative format of the URLs which can be 358# more readable and natural-looking: project name is embedded 359# directly in the path and the query string contains other 360# auxiliary information. All gitweb installations recognize 361# URL in either format; this configures in which formats gitweb 362# generates links. 363 364# To enable system wide have in $GITWEB_CONFIG 365# $feature{'pathinfo'}{'default'} = [1]; 366# Project specific override is not supported. 367 368# Note that you will need to change the default location of CSS, 369# favicon, logo and possibly other files to an absolute URL. Also, 370# if gitweb.cgi serves as your indexfile, you will need to force 371# $my_uri to contain the script name in your $GITWEB_CONFIG. 372'pathinfo'=> { 373'override'=>0, 374'default'=> [0]}, 375 376# Make gitweb consider projects in project root subdirectories 377# to be forks of existing projects. Given project $projname.git, 378# projects matching $projname/*.git will not be shown in the main 379# projects list, instead a '+' mark will be added to $projname 380# there and a 'forks' view will be enabled for the project, listing 381# all the forks. If project list is taken from a file, forks have 382# to be listed after the main project. 383 384# To enable system wide have in $GITWEB_CONFIG 385# $feature{'forks'}{'default'} = [1]; 386# Project specific override is not supported. 387'forks'=> { 388'override'=>0, 389'default'=> [0]}, 390 391# Insert custom links to the action bar of all project pages. 392# This enables you mainly to link to third-party scripts integrating 393# into gitweb; e.g. git-browser for graphical history representation 394# or custom web-based repository administration interface. 395 396# The 'default' value consists of a list of triplets in the form 397# (label, link, position) where position is the label after which 398# to insert the link and link is a format string where %n expands 399# to the project name, %f to the project path within the filesystem, 400# %h to the current hash (h gitweb parameter) and %b to the current 401# hash base (hb gitweb parameter); %% expands to %. 402 403# To enable system wide have in $GITWEB_CONFIG e.g. 404# $feature{'actions'}{'default'} = [('graphiclog', 405# '/git-browser/by-commit.html?r=%n', 'summary')]; 406# Project specific override is not supported. 407'actions'=> { 408'override'=>0, 409'default'=> []}, 410 411# Allow gitweb scan project content tags described in ctags/ 412# of project repository, and display the popular Web 2.0-ish 413# "tag cloud" near the project list. Note that this is something 414# COMPLETELY different from the normal Git tags. 415 416# gitweb by itself can show existing tags, but it does not handle 417# tagging itself; you need an external application for that. 418# For an example script, check Girocco's cgi/tagproj.cgi. 419# You may want to install the HTML::TagCloud Perl module to get 420# a pretty tag cloud instead of just a list of tags. 421 422# To enable system wide have in $GITWEB_CONFIG 423# $feature{'ctags'}{'default'} = ['path_to_tag_script']; 424# Project specific override is not supported. 425'ctags'=> { 426'override'=>0, 427'default'=> [0]}, 428 429# The maximum number of patches in a patchset generated in patch 430# view. Set this to 0 or undef to disable patch view, or to a 431# negative number to remove any limit. 432 433# To disable system wide have in $GITWEB_CONFIG 434# $feature{'patches'}{'default'} = [0]; 435# To have project specific config enable override in $GITWEB_CONFIG 436# $feature{'patches'}{'override'} = 1; 437# and in project config gitweb.patches = 0|n; 438# where n is the maximum number of patches allowed in a patchset. 439'patches'=> { 440'sub'=> \&feature_patches, 441'override'=>0, 442'default'=> [16]}, 443 444# Avatar support. When this feature is enabled, views such as 445# shortlog or commit will display an avatar associated with 446# the email of the committer(s) and/or author(s). 447 448# Currently available providers are gravatar and picon. 449# If an unknown provider is specified, the feature is disabled. 450 451# Gravatar depends on Digest::MD5. 452# Picon currently relies on the indiana.edu database. 453 454# To enable system wide have in $GITWEB_CONFIG 455# $feature{'avatar'}{'default'} = ['<provider>']; 456# where <provider> is either gravatar or picon. 457# To have project specific config enable override in $GITWEB_CONFIG 458# $feature{'avatar'}{'override'} = 1; 459# and in project config gitweb.avatar = <provider>; 460'avatar'=> { 461'sub'=> \&feature_avatar, 462'override'=>0, 463'default'=> ['']}, 464 465# Enable displaying how much time and how many git commands 466# it took to generate and display page. Disabled by default. 467# Project specific override is not supported. 468'timed'=> { 469'override'=>0, 470'default'=> [0]}, 471 472# Enable turning some links into links to actions which require 473# JavaScript to run (like 'blame_incremental'). Not enabled by 474# default. Project specific override is currently not supported. 475'javascript-actions'=> { 476'override'=>0, 477'default'=> [0]}, 478 479# Syntax highlighting support. This is based on Daniel Svensson's 480# and Sham Chukoury's work in gitweb-xmms2.git. 481# It requires the 'highlight' program, and therefore is disabled 482# by default. 483 484# To enable system wide have in $GITWEB_CONFIG 485# $feature{'highlight'}{'default'} = [1]; 486 487'highlight'=> { 488'sub'=>sub{ feature_bool('highlight',@_) }, 489'override'=>0, 490'default'=> [0]}, 491); 492 493sub gitweb_get_feature { 494my($name) =@_; 495return unlessexists$feature{$name}; 496my($sub,$override,@defaults) = ( 497$feature{$name}{'sub'}, 498$feature{$name}{'override'}, 499@{$feature{$name}{'default'}}); 500# project specific override is possible only if we have project 501our$git_dir;# global variable, declared later 502if(!$override|| !defined$git_dir) { 503return@defaults; 504} 505if(!defined$sub) { 506warn"feature$nameis not overridable"; 507return@defaults; 508} 509return$sub->(@defaults); 510} 511 512# A wrapper to check if a given feature is enabled. 513# With this, you can say 514# 515# my $bool_feat = gitweb_check_feature('bool_feat'); 516# gitweb_check_feature('bool_feat') or somecode; 517# 518# instead of 519# 520# my ($bool_feat) = gitweb_get_feature('bool_feat'); 521# (gitweb_get_feature('bool_feat'))[0] or somecode; 522# 523sub gitweb_check_feature { 524return(gitweb_get_feature(@_))[0]; 525} 526 527 528sub feature_bool { 529my$key=shift; 530my($val) = git_get_project_config($key,'--bool'); 531 532if(!defined$val) { 533return($_[0]); 534}elsif($valeq'true') { 535return(1); 536}elsif($valeq'false') { 537return(0); 538} 539} 540 541sub feature_snapshot { 542my(@fmts) =@_; 543 544my($val) = git_get_project_config('snapshot'); 545 546if($val) { 547@fmts= ($valeq'none'? () :split/\s*[,\s]\s*/,$val); 548} 549 550return@fmts; 551} 552 553sub feature_patches { 554my@val= (git_get_project_config('patches','--int')); 555 556if(@val) { 557return@val; 558} 559 560return($_[0]); 561} 562 563sub feature_avatar { 564my@val= (git_get_project_config('avatar')); 565 566return@val?@val:@_; 567} 568 569# checking HEAD file with -e is fragile if the repository was 570# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed 571# and then pruned. 572sub check_head_link { 573my($dir) =@_; 574my$headfile="$dir/HEAD"; 575return((-e $headfile) || 576(-l $headfile&&readlink($headfile) =~/^refs\/heads\//)); 577} 578 579sub check_export_ok { 580my($dir) =@_; 581return(check_head_link($dir) && 582(!$export_ok|| -e "$dir/$export_ok") && 583(!$export_auth_hook||$export_auth_hook->($dir))); 584} 585 586# process alternate names for backward compatibility 587# filter out unsupported (unknown) snapshot formats 588sub filter_snapshot_fmts { 589my@fmts=@_; 590 591@fmts=map{ 592exists$known_snapshot_format_aliases{$_} ? 593$known_snapshot_format_aliases{$_} :$_}@fmts; 594@fmts=grep{ 595exists$known_snapshot_formats{$_} && 596!$known_snapshot_formats{$_}{'disabled'}}@fmts; 597} 598 599our$GITWEB_CONFIG=$ENV{'GITWEB_CONFIG'} ||"++GITWEB_CONFIG++"; 600our$GITWEB_CONFIG_SYSTEM=$ENV{'GITWEB_CONFIG_SYSTEM'} ||"++GITWEB_CONFIG_SYSTEM++"; 601# die if there are errors parsing config file 602if(-e $GITWEB_CONFIG) { 603do$GITWEB_CONFIG; 604die$@if$@; 605}elsif(-e $GITWEB_CONFIG_SYSTEM) { 606do$GITWEB_CONFIG_SYSTEM; 607die$@if$@; 608} 609 610# Get loadavg of system, to compare against $maxload. 611# Currently it requires '/proc/loadavg' present to get loadavg; 612# if it is not present it returns 0, which means no load checking. 613sub get_loadavg { 614if( -e '/proc/loadavg'){ 615open my$fd,'<','/proc/loadavg' 616orreturn0; 617my@load=split(/\s+/,scalar<$fd>); 618close$fd; 619 620# The first three columns measure CPU and IO utilization of the last one, 621# five, and 10 minute periods. The fourth column shows the number of 622# currently running processes and the total number of processes in the m/n 623# format. The last column displays the last process ID used. 624return$load[0] ||0; 625} 626# additional checks for load average should go here for things that don't export 627# /proc/loadavg 628 629return0; 630} 631 632# version of the core git binary 633our$git_version=qx("$GIT" --version)=~m/git version (.*)$/?$1:"unknown"; 634$number_of_git_cmds++; 635 636$projects_list||=$projectroot; 637 638if(defined$maxload&& get_loadavg() >$maxload) { 639 die_error(503,"The load average on the server is too high"); 640} 641 642# ====================================================================== 643# input validation and dispatch 644 645# input parameters can be collected from a variety of sources (presently, CGI 646# and PATH_INFO), so we define an %input_params hash that collects them all 647# together during validation: this allows subsequent uses (e.g. href()) to be 648# agnostic of the parameter origin 649 650our%input_params= (); 651 652# input parameters are stored with the long parameter name as key. This will 653# also be used in the href subroutine to convert parameters to their CGI 654# equivalent, and since the href() usage is the most frequent one, we store 655# the name -> CGI key mapping here, instead of the reverse. 656# 657# XXX: Warning: If you touch this, check the search form for updating, 658# too. 659 660our@cgi_param_mapping= ( 661 project =>"p", 662 action =>"a", 663 file_name =>"f", 664 file_parent =>"fp", 665 hash =>"h", 666 hash_parent =>"hp", 667 hash_base =>"hb", 668 hash_parent_base =>"hpb", 669 page =>"pg", 670 order =>"o", 671 searchtext =>"s", 672 searchtype =>"st", 673 snapshot_format =>"sf", 674 extra_options =>"opt", 675 search_use_regexp =>"sr", 676# this must be last entry (for manipulation from JavaScript) 677 javascript =>"js" 678); 679our%cgi_param_mapping=@cgi_param_mapping; 680 681# we will also need to know the possible actions, for validation 682our%actions= ( 683"blame"=> \&git_blame, 684"blame_incremental"=> \&git_blame_incremental, 685"blame_data"=> \&git_blame_data, 686"blobdiff"=> \&git_blobdiff, 687"blobdiff_plain"=> \&git_blobdiff_plain, 688"blob"=> \&git_blob, 689"blob_plain"=> \&git_blob_plain, 690"commitdiff"=> \&git_commitdiff, 691"commitdiff_plain"=> \&git_commitdiff_plain, 692"commit"=> \&git_commit, 693"forks"=> \&git_forks, 694"heads"=> \&git_heads, 695"history"=> \&git_history, 696"log"=> \&git_log, 697"patch"=> \&git_patch, 698"patches"=> \&git_patches, 699"rss"=> \&git_rss, 700"atom"=> \&git_atom, 701"search"=> \&git_search, 702"search_help"=> \&git_search_help, 703"shortlog"=> \&git_shortlog, 704"summary"=> \&git_summary, 705"tag"=> \&git_tag, 706"tags"=> \&git_tags, 707"tree"=> \&git_tree, 708"snapshot"=> \&git_snapshot, 709"object"=> \&git_object, 710# those below don't need $project 711"opml"=> \&git_opml, 712"project_list"=> \&git_project_list, 713"project_index"=> \&git_project_index, 714); 715 716# finally, we have the hash of allowed extra_options for the commands that 717# allow them 718our%allowed_options= ( 719"--no-merges"=> [qw(rss atom log shortlog history)], 720); 721 722# fill %input_params with the CGI parameters. All values except for 'opt' 723# should be single values, but opt can be an array. We should probably 724# build an array of parameters that can be multi-valued, but since for the time 725# being it's only this one, we just single it out 726while(my($name,$symbol) =each%cgi_param_mapping) { 727if($symboleq'opt') { 728$input_params{$name} = [$cgi->param($symbol) ]; 729}else{ 730$input_params{$name} =$cgi->param($symbol); 731} 732} 733 734# now read PATH_INFO and update the parameter list for missing parameters 735sub evaluate_path_info { 736return ifdefined$input_params{'project'}; 737return if!$path_info; 738$path_info=~ s,^/+,,; 739return if!$path_info; 740 741# find which part of PATH_INFO is project 742my$project=$path_info; 743$project=~ s,/+$,,; 744while($project&& !check_head_link("$projectroot/$project")) { 745$project=~ s,/*[^/]*$,,; 746} 747return unless$project; 748$input_params{'project'} =$project; 749 750# do not change any parameters if an action is given using the query string 751return if$input_params{'action'}; 752$path_info=~ s,^\Q$project\E/*,,; 753 754# next, check if we have an action 755my$action=$path_info; 756$action=~ s,/.*$,,; 757if(exists$actions{$action}) { 758$path_info=~ s,^$action/*,,; 759$input_params{'action'} =$action; 760} 761 762# list of actions that want hash_base instead of hash, but can have no 763# pathname (f) parameter 764my@wants_base= ( 765'tree', 766'history', 767); 768 769# we want to catch 770# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] 771my($parentrefname,$parentpathname,$refname,$pathname) = 772($path_info=~/^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); 773 774# first, analyze the 'current' part 775if(defined$pathname) { 776# we got "branch:filename" or "branch:dir/" 777# we could use git_get_type(branch:pathname), but: 778# - it needs $git_dir 779# - it does a git() call 780# - the convention of terminating directories with a slash 781# makes it superfluous 782# - embedding the action in the PATH_INFO would make it even 783# more superfluous 784$pathname=~ s,^/+,,; 785if(!$pathname||substr($pathname, -1)eq"/") { 786$input_params{'action'} ||="tree"; 787$pathname=~ s,/$,,; 788}else{ 789# the default action depends on whether we had parent info 790# or not 791if($parentrefname) { 792$input_params{'action'} ||="blobdiff_plain"; 793}else{ 794$input_params{'action'} ||="blob_plain"; 795} 796} 797$input_params{'hash_base'} ||=$refname; 798$input_params{'file_name'} ||=$pathname; 799}elsif(defined$refname) { 800# we got "branch". In this case we have to choose if we have to 801# set hash or hash_base. 802# 803# Most of the actions without a pathname only want hash to be 804# set, except for the ones specified in @wants_base that want 805# hash_base instead. It should also be noted that hand-crafted 806# links having 'history' as an action and no pathname or hash 807# set will fail, but that happens regardless of PATH_INFO. 808$input_params{'action'} ||="shortlog"; 809if(grep{$_eq$input_params{'action'} }@wants_base) { 810$input_params{'hash_base'} ||=$refname; 811}else{ 812$input_params{'hash'} ||=$refname; 813} 814} 815 816# next, handle the 'parent' part, if present 817if(defined$parentrefname) { 818# a missing pathspec defaults to the 'current' filename, allowing e.g. 819# someproject/blobdiff/oldrev..newrev:/filename 820if($parentpathname) { 821$parentpathname=~ s,^/+,,; 822$parentpathname=~ s,/$,,; 823$input_params{'file_parent'} ||=$parentpathname; 824}else{ 825$input_params{'file_parent'} ||=$input_params{'file_name'}; 826} 827# we assume that hash_parent_base is wanted if a path was specified, 828# or if the action wants hash_base instead of hash 829if(defined$input_params{'file_parent'} || 830grep{$_eq$input_params{'action'} }@wants_base) { 831$input_params{'hash_parent_base'} ||=$parentrefname; 832}else{ 833$input_params{'hash_parent'} ||=$parentrefname; 834} 835} 836 837# for the snapshot action, we allow URLs in the form 838# $project/snapshot/$hash.ext 839# where .ext determines the snapshot and gets removed from the 840# passed $refname to provide the $hash. 841# 842# To be able to tell that $refname includes the format extension, we 843# require the following two conditions to be satisfied: 844# - the hash input parameter MUST have been set from the $refname part 845# of the URL (i.e. they must be equal) 846# - the snapshot format MUST NOT have been defined already (e.g. from 847# CGI parameter sf) 848# It's also useless to try any matching unless $refname has a dot, 849# so we check for that too 850if(defined$input_params{'action'} && 851$input_params{'action'}eq'snapshot'&& 852defined$refname&&index($refname,'.') != -1&& 853$refnameeq$input_params{'hash'} && 854!defined$input_params{'snapshot_format'}) { 855# We loop over the known snapshot formats, checking for 856# extensions. Allowed extensions are both the defined suffix 857# (which includes the initial dot already) and the snapshot 858# format key itself, with a prepended dot 859while(my($fmt,$opt) =each%known_snapshot_formats) { 860my$hash=$refname; 861unless($hash=~s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) { 862next; 863} 864my$sfx=$1; 865# a valid suffix was found, so set the snapshot format 866# and reset the hash parameter 867$input_params{'snapshot_format'} =$fmt; 868$input_params{'hash'} =$hash; 869# we also set the format suffix to the one requested 870# in the URL: this way a request for e.g. .tgz returns 871# a .tgz instead of a .tar.gz 872$known_snapshot_formats{$fmt}{'suffix'} =$sfx; 873last; 874} 875} 876} 877evaluate_path_info(); 878 879our$action=$input_params{'action'}; 880if(defined$action) { 881if(!validate_action($action)) { 882 die_error(400,"Invalid action parameter"); 883} 884} 885 886# parameters which are pathnames 887our$project=$input_params{'project'}; 888if(defined$project) { 889if(!validate_project($project)) { 890undef$project; 891 die_error(404,"No such project"); 892} 893} 894 895our$file_name=$input_params{'file_name'}; 896if(defined$file_name) { 897if(!validate_pathname($file_name)) { 898 die_error(400,"Invalid file parameter"); 899} 900} 901 902our$file_parent=$input_params{'file_parent'}; 903if(defined$file_parent) { 904if(!validate_pathname($file_parent)) { 905 die_error(400,"Invalid file parent parameter"); 906} 907} 908 909# parameters which are refnames 910our$hash=$input_params{'hash'}; 911if(defined$hash) { 912if(!validate_refname($hash)) { 913 die_error(400,"Invalid hash parameter"); 914} 915} 916 917our$hash_parent=$input_params{'hash_parent'}; 918if(defined$hash_parent) { 919if(!validate_refname($hash_parent)) { 920 die_error(400,"Invalid hash parent parameter"); 921} 922} 923 924our$hash_base=$input_params{'hash_base'}; 925if(defined$hash_base) { 926if(!validate_refname($hash_base)) { 927 die_error(400,"Invalid hash base parameter"); 928} 929} 930 931our@extra_options= @{$input_params{'extra_options'}}; 932# @extra_options is always defined, since it can only be (currently) set from 933# CGI, and $cgi->param() returns the empty array in array context if the param 934# is not set 935foreachmy$opt(@extra_options) { 936if(not exists$allowed_options{$opt}) { 937 die_error(400,"Invalid option parameter"); 938} 939if(not grep(/^$action$/, @{$allowed_options{$opt}})) { 940 die_error(400,"Invalid option parameter for this action"); 941} 942} 943 944our$hash_parent_base=$input_params{'hash_parent_base'}; 945if(defined$hash_parent_base) { 946if(!validate_refname($hash_parent_base)) { 947 die_error(400,"Invalid hash parent base parameter"); 948} 949} 950 951# other parameters 952our$page=$input_params{'page'}; 953if(defined$page) { 954if($page=~m/[^0-9]/) { 955 die_error(400,"Invalid page parameter"); 956} 957} 958 959our$searchtype=$input_params{'searchtype'}; 960if(defined$searchtype) { 961if($searchtype=~m/[^a-z]/) { 962 die_error(400,"Invalid searchtype parameter"); 963} 964} 965 966our$search_use_regexp=$input_params{'search_use_regexp'}; 967 968our$searchtext=$input_params{'searchtext'}; 969our$search_regexp; 970if(defined$searchtext) { 971if(length($searchtext) <2) { 972 die_error(403,"At least two characters are required for search parameter"); 973} 974$search_regexp=$search_use_regexp?$searchtext:quotemeta$searchtext; 975} 976 977# path to the current git repository 978our$git_dir; 979$git_dir="$projectroot/$project"if$project; 980 981# list of supported snapshot formats 982our@snapshot_fmts= gitweb_get_feature('snapshot'); 983@snapshot_fmts= filter_snapshot_fmts(@snapshot_fmts); 984 985# check that the avatar feature is set to a known provider name, 986# and for each provider check if the dependencies are satisfied. 987# if the provider name is invalid or the dependencies are not met, 988# reset $git_avatar to the empty string. 989our($git_avatar) = gitweb_get_feature('avatar'); 990if($git_avatareq'gravatar') { 991$git_avatar=''unless(eval{require Digest::MD5;1; }); 992}elsif($git_avatareq'picon') { 993# no dependencies 994}else{ 995$git_avatar=''; 996} 997 998# dispatch 999if(!defined$action) {1000if(defined$hash) {1001$action= git_get_type($hash);1002}elsif(defined$hash_base&&defined$file_name) {1003$action= git_get_type("$hash_base:$file_name");1004}elsif(defined$project) {1005$action='summary';1006}else{1007$action='project_list';1008}1009}1010if(!defined($actions{$action})) {1011 die_error(400,"Unknown action");1012}1013if($action!~m/^(?:opml|project_list|project_index)$/&&1014!$project) {1015 die_error(400,"Project needed");1016}1017$actions{$action}->();1018exit;10191020## ======================================================================1021## action links10221023sub href {1024my%params=@_;1025# default is to use -absolute url() i.e. $my_uri1026my$href=$params{-full} ?$my_url:$my_uri;10271028$params{'project'} =$projectunlessexists$params{'project'};10291030if($params{-replay}) {1031while(my($name,$symbol) =each%cgi_param_mapping) {1032if(!exists$params{$name}) {1033$params{$name} =$input_params{$name};1034}1035}1036}10371038my$use_pathinfo= gitweb_check_feature('pathinfo');1039if($use_pathinfoand defined$params{'project'}) {1040# try to put as many parameters as possible in PATH_INFO:1041# - project name1042# - action1043# - hash_parent or hash_parent_base:/file_parent1044# - hash or hash_base:/filename1045# - the snapshot_format as an appropriate suffix10461047# When the script is the root DirectoryIndex for the domain,1048# $href here would be something like http://gitweb.example.com/1049# Thus, we strip any trailing / from $href, to spare us double1050# slashes in the final URL1051$href=~ s,/$,,;10521053# Then add the project name, if present1054$href.="/".esc_url($params{'project'});1055delete$params{'project'};10561057# since we destructively absorb parameters, we keep this1058# boolean that remembers if we're handling a snapshot1059my$is_snapshot=$params{'action'}eq'snapshot';10601061# Summary just uses the project path URL, any other action is1062# added to the URL1063if(defined$params{'action'}) {1064$href.="/".esc_url($params{'action'})unless$params{'action'}eq'summary';1065delete$params{'action'};1066}10671068# Next, we put hash_parent_base:/file_parent..hash_base:/file_name,1069# stripping nonexistent or useless pieces1070$href.="/"if($params{'hash_base'} ||$params{'hash_parent_base'}1071||$params{'hash_parent'} ||$params{'hash'});1072if(defined$params{'hash_base'}) {1073if(defined$params{'hash_parent_base'}) {1074$href.= esc_url($params{'hash_parent_base'});1075# skip the file_parent if it's the same as the file_name1076if(defined$params{'file_parent'}) {1077if(defined$params{'file_name'} &&$params{'file_parent'}eq$params{'file_name'}) {1078delete$params{'file_parent'};1079}elsif($params{'file_parent'} !~/\.\./) {1080$href.=":/".esc_url($params{'file_parent'});1081delete$params{'file_parent'};1082}1083}1084$href.="..";1085delete$params{'hash_parent'};1086delete$params{'hash_parent_base'};1087}elsif(defined$params{'hash_parent'}) {1088$href.= esc_url($params{'hash_parent'})."..";1089delete$params{'hash_parent'};1090}10911092$href.= esc_url($params{'hash_base'});1093if(defined$params{'file_name'} &&$params{'file_name'} !~/\.\./) {1094$href.=":/".esc_url($params{'file_name'});1095delete$params{'file_name'};1096}1097delete$params{'hash'};1098delete$params{'hash_base'};1099}elsif(defined$params{'hash'}) {1100$href.= esc_url($params{'hash'});1101delete$params{'hash'};1102}11031104# If the action was a snapshot, we can absorb the1105# snapshot_format parameter too1106if($is_snapshot) {1107my$fmt=$params{'snapshot_format'};1108# snapshot_format should always be defined when href()1109# is called, but just in case some code forgets, we1110# fall back to the default1111$fmt||=$snapshot_fmts[0];1112$href.=$known_snapshot_formats{$fmt}{'suffix'};1113delete$params{'snapshot_format'};1114}1115}11161117# now encode the parameters explicitly1118my@result= ();1119for(my$i=0;$i<@cgi_param_mapping;$i+=2) {1120my($name,$symbol) = ($cgi_param_mapping[$i],$cgi_param_mapping[$i+1]);1121if(defined$params{$name}) {1122if(ref($params{$name})eq"ARRAY") {1123foreachmy$par(@{$params{$name}}) {1124push@result,$symbol."=". esc_param($par);1125}1126}else{1127push@result,$symbol."=". esc_param($params{$name});1128}1129}1130}1131$href.="?".join(';',@result)ifscalar@result;11321133return$href;1134}113511361137## ======================================================================1138## validation, quoting/unquoting and escaping11391140sub validate_action {1141my$input=shift||returnundef;1142returnundefunlessexists$actions{$input};1143return$input;1144}11451146sub validate_project {1147my$input=shift||returnundef;1148if(!validate_pathname($input) ||1149!(-d "$projectroot/$input") ||1150!check_export_ok("$projectroot/$input") ||1151($strict_export&& !project_in_list($input))) {1152returnundef;1153}else{1154return$input;1155}1156}11571158sub validate_pathname {1159my$input=shift||returnundef;11601161# no '.' or '..' as elements of path, i.e. no '.' nor '..'1162# at the beginning, at the end, and between slashes.1163# also this catches doubled slashes1164if($input=~m!(^|/)(|\.|\.\.)(/|$)!) {1165returnundef;1166}1167# no null characters1168if($input=~m!\0!) {1169returnundef;1170}1171return$input;1172}11731174sub validate_refname {1175my$input=shift||returnundef;11761177# textual hashes are O.K.1178if($input=~m/^[0-9a-fA-F]{40}$/) {1179return$input;1180}1181# it must be correct pathname1182$input= validate_pathname($input)1183orreturnundef;1184# restrictions on ref name according to git-check-ref-format1185if($input=~m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {1186returnundef;1187}1188return$input;1189}11901191# decode sequences of octets in utf8 into Perl's internal form,1192# which is utf-8 with utf8 flag set if needed. gitweb writes out1193# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning1194sub to_utf8 {1195my$str=shift;1196returnundefunlessdefined$str;1197if(utf8::valid($str)) {1198 utf8::decode($str);1199return$str;1200}else{1201return decode($fallback_encoding,$str, Encode::FB_DEFAULT);1202}1203}12041205# quote unsafe chars, but keep the slash, even when it's not1206# correct, but quoted slashes look too horrible in bookmarks1207sub esc_param {1208my$str=shift;1209returnundefunlessdefined$str;1210$str=~s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;1211$str=~s/ /\+/g;1212return$str;1213}12141215# quote unsafe chars in whole URL, so some charactrs cannot be quoted1216sub esc_url {1217my$str=shift;1218returnundefunlessdefined$str;1219$str=~s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X",ord($1))/eg;1220$str=~s/\+/%2B/g;1221$str=~s/ /\+/g;1222return$str;1223}12241225# replace invalid utf8 character with SUBSTITUTION sequence1226sub esc_html {1227my$str=shift;1228my%opts=@_;12291230returnundefunlessdefined$str;12311232$str= to_utf8($str);1233$str=$cgi->escapeHTML($str);1234if($opts{'-nbsp'}) {1235$str=~s/ / /g;1236}1237$str=~ s|([[:cntrl:]])|(($1ne"\t") ? quot_cec($1) :$1)|eg;1238return$str;1239}12401241# quote control characters and escape filename to HTML1242sub esc_path {1243my$str=shift;1244my%opts=@_;12451246returnundefunlessdefined$str;12471248$str= to_utf8($str);1249$str=$cgi->escapeHTML($str);1250if($opts{'-nbsp'}) {1251$str=~s/ / /g;1252}1253$str=~ s|([[:cntrl:]])|quot_cec($1)|eg;1254return$str;1255}12561257# Make control characters "printable", using character escape codes (CEC)1258sub quot_cec {1259my$cntrl=shift;1260my%opts=@_;1261my%es= (# character escape codes, aka escape sequences1262"\t"=>'\t',# tab (HT)1263"\n"=>'\n',# line feed (LF)1264"\r"=>'\r',# carrige return (CR)1265"\f"=>'\f',# form feed (FF)1266"\b"=>'\b',# backspace (BS)1267"\a"=>'\a',# alarm (bell) (BEL)1268"\e"=>'\e',# escape (ESC)1269"\013"=>'\v',# vertical tab (VT)1270"\000"=>'\0',# nul character (NUL)1271);1272my$chr= ( (exists$es{$cntrl})1273?$es{$cntrl}1274:sprintf('\%2x',ord($cntrl)) );1275if($opts{-nohtml}) {1276return$chr;1277}else{1278return"<span class=\"cntrl\">$chr</span>";1279}1280}12811282# Alternatively use unicode control pictures codepoints,1283# Unicode "printable representation" (PR)1284sub quot_upr {1285my$cntrl=shift;1286my%opts=@_;12871288my$chr=sprintf('&#%04d;',0x2400+ord($cntrl));1289if($opts{-nohtml}) {1290return$chr;1291}else{1292return"<span class=\"cntrl\">$chr</span>";1293}1294}12951296# git may return quoted and escaped filenames1297sub unquote {1298my$str=shift;12991300sub unq {1301my$seq=shift;1302my%es= (# character escape codes, aka escape sequences1303't'=>"\t",# tab (HT, TAB)1304'n'=>"\n",# newline (NL)1305'r'=>"\r",# return (CR)1306'f'=>"\f",# form feed (FF)1307'b'=>"\b",# backspace (BS)1308'a'=>"\a",# alarm (bell) (BEL)1309'e'=>"\e",# escape (ESC)1310'v'=>"\013",# vertical tab (VT)1311);13121313if($seq=~m/^[0-7]{1,3}$/) {1314# octal char sequence1315returnchr(oct($seq));1316}elsif(exists$es{$seq}) {1317# C escape sequence, aka character escape code1318return$es{$seq};1319}1320# quoted ordinary character1321return$seq;1322}13231324if($str=~m/^"(.*)"$/) {1325# needs unquoting1326$str=$1;1327$str=~s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;1328}1329return$str;1330}13311332# escape tabs (convert tabs to spaces)1333sub untabify {1334my$line=shift;13351336while((my$pos=index($line,"\t")) != -1) {1337if(my$count= (8- ($pos%8))) {1338my$spaces=' ' x $count;1339$line=~s/\t/$spaces/;1340}1341}13421343return$line;1344}13451346sub project_in_list {1347my$project=shift;1348my@list= git_get_projects_list();1349return@list&&scalar(grep{$_->{'path'}eq$project}@list);1350}13511352## ----------------------------------------------------------------------1353## HTML aware string manipulation13541355# Try to chop given string on a word boundary between position1356# $len and $len+$add_len. If there is no word boundary there,1357# chop at $len+$add_len. Do not chop if chopped part plus ellipsis1358# (marking chopped part) would be longer than given string.1359sub chop_str {1360my$str=shift;1361my$len=shift;1362my$add_len=shift||10;1363my$where=shift||'right';# 'left' | 'center' | 'right'13641365# Make sure perl knows it is utf8 encoded so we don't1366# cut in the middle of a utf8 multibyte char.1367$str= to_utf8($str);13681369# allow only $len chars, but don't cut a word if it would fit in $add_len1370# if it doesn't fit, cut it if it's still longer than the dots we would add1371# remove chopped character entities entirely13721373# when chopping in the middle, distribute $len into left and right part1374# return early if chopping wouldn't make string shorter1375if($whereeq'center') {1376return$strif($len+5>=length($str));# filler is length 51377$len=int($len/2);1378}else{1379return$strif($len+4>=length($str));# filler is length 41380}13811382# regexps: ending and beginning with word part up to $add_len1383my$endre=qr/.{$len}\w{0,$add_len}/;1384my$begre=qr/\w{0,$add_len}.{$len}/;13851386if($whereeq'left') {1387$str=~m/^(.*?)($begre)$/;1388my($lead,$body) = ($1,$2);1389if(length($lead) >4) {1390$lead=" ...";1391}1392return"$lead$body";13931394}elsif($whereeq'center') {1395$str=~m/^($endre)(.*)$/;1396my($left,$str) = ($1,$2);1397$str=~m/^(.*?)($begre)$/;1398my($mid,$right) = ($1,$2);1399if(length($mid) >5) {1400$mid=" ... ";1401}1402return"$left$mid$right";14031404}else{1405$str=~m/^($endre)(.*)$/;1406my$body=$1;1407my$tail=$2;1408if(length($tail) >4) {1409$tail="... ";1410}1411return"$body$tail";1412}1413}14141415# takes the same arguments as chop_str, but also wraps a <span> around the1416# result with a title attribute if it does get chopped. Additionally, the1417# string is HTML-escaped.1418sub chop_and_escape_str {1419my($str) =@_;14201421my$chopped= chop_str(@_);1422if($choppedeq$str) {1423return esc_html($chopped);1424}else{1425$str=~s/[[:cntrl:]]/?/g;1426return$cgi->span({-title=>$str}, esc_html($chopped));1427}1428}14291430## ----------------------------------------------------------------------1431## functions returning short strings14321433# CSS class for given age value (in seconds)1434sub age_class {1435my$age=shift;14361437if(!defined$age) {1438return"noage";1439}elsif($age<60*60*2) {1440return"age0";1441}elsif($age<60*60*24*2) {1442return"age1";1443}else{1444return"age2";1445}1446}14471448# convert age in seconds to "nn units ago" string1449sub age_string {1450my$age=shift;1451my$age_str;14521453if($age>60*60*24*365*2) {1454$age_str= (int$age/60/60/24/365);1455$age_str.=" years ago";1456}elsif($age>60*60*24*(365/12)*2) {1457$age_str=int$age/60/60/24/(365/12);1458$age_str.=" months ago";1459}elsif($age>60*60*24*7*2) {1460$age_str=int$age/60/60/24/7;1461$age_str.=" weeks ago";1462}elsif($age>60*60*24*2) {1463$age_str=int$age/60/60/24;1464$age_str.=" days ago";1465}elsif($age>60*60*2) {1466$age_str=int$age/60/60;1467$age_str.=" hours ago";1468}elsif($age>60*2) {1469$age_str=int$age/60;1470$age_str.=" min ago";1471}elsif($age>2) {1472$age_str=int$age;1473$age_str.=" sec ago";1474}else{1475$age_str.=" right now";1476}1477return$age_str;1478}14791480useconstant{1481 S_IFINVALID =>0030000,1482 S_IFGITLINK =>0160000,1483};14841485# submodule/subproject, a commit object reference1486sub S_ISGITLINK {1487my$mode=shift;14881489return(($mode& S_IFMT) == S_IFGITLINK)1490}14911492# convert file mode in octal to symbolic file mode string1493sub mode_str {1494my$mode=oct shift;14951496if(S_ISGITLINK($mode)) {1497return'm---------';1498}elsif(S_ISDIR($mode& S_IFMT)) {1499return'drwxr-xr-x';1500}elsif(S_ISLNK($mode)) {1501return'lrwxrwxrwx';1502}elsif(S_ISREG($mode)) {1503# git cares only about the executable bit1504if($mode& S_IXUSR) {1505return'-rwxr-xr-x';1506}else{1507return'-rw-r--r--';1508};1509}else{1510return'----------';1511}1512}15131514# convert file mode in octal to file type string1515sub file_type {1516my$mode=shift;15171518if($mode!~m/^[0-7]+$/) {1519return$mode;1520}else{1521$mode=oct$mode;1522}15231524if(S_ISGITLINK($mode)) {1525return"submodule";1526}elsif(S_ISDIR($mode& S_IFMT)) {1527return"directory";1528}elsif(S_ISLNK($mode)) {1529return"symlink";1530}elsif(S_ISREG($mode)) {1531return"file";1532}else{1533return"unknown";1534}1535}15361537# convert file mode in octal to file type description string1538sub file_type_long {1539my$mode=shift;15401541if($mode!~m/^[0-7]+$/) {1542return$mode;1543}else{1544$mode=oct$mode;1545}15461547if(S_ISGITLINK($mode)) {1548return"submodule";1549}elsif(S_ISDIR($mode& S_IFMT)) {1550return"directory";1551}elsif(S_ISLNK($mode)) {1552return"symlink";1553}elsif(S_ISREG($mode)) {1554if($mode& S_IXUSR) {1555return"executable";1556}else{1557return"file";1558};1559}else{1560return"unknown";1561}1562}156315641565## ----------------------------------------------------------------------1566## functions returning short HTML fragments, or transforming HTML fragments1567## which don't belong to other sections15681569# format line of commit message.1570sub format_log_line_html {1571my$line=shift;15721573$line= esc_html($line, -nbsp=>1);1574$line=~ s{\b([0-9a-fA-F]{8,40})\b}{1575$cgi->a({-href => href(action=>"object", hash=>$1),1576-class=>"text"},$1);1577}eg;15781579return$line;1580}15811582# format marker of refs pointing to given object15831584# the destination action is chosen based on object type and current context:1585# - for annotated tags, we choose the tag view unless it's the current view1586# already, in which case we go to shortlog view1587# - for other refs, we keep the current view if we're in history, shortlog or1588# log view, and select shortlog otherwise1589sub format_ref_marker {1590my($refs,$id) =@_;1591my$markers='';15921593if(defined$refs->{$id}) {1594foreachmy$ref(@{$refs->{$id}}) {1595# this code exploits the fact that non-lightweight tags are the1596# only indirect objects, and that they are the only objects for which1597# we want to use tag instead of shortlog as action1598my($type,$name) =qw();1599my$indirect= ($ref=~s/\^\{\}$//);1600# e.g. tags/v2.6.11 or heads/next1601if($ref=~m!^(.*?)s?/(.*)$!) {1602$type=$1;1603$name=$2;1604}else{1605$type="ref";1606$name=$ref;1607}16081609my$class=$type;1610$class.=" indirect"if$indirect;16111612my$dest_action="shortlog";16131614if($indirect) {1615$dest_action="tag"unless$actioneq"tag";1616}elsif($action=~/^(history|(short)?log)$/) {1617$dest_action=$action;1618}16191620my$dest="";1621$dest.="refs/"unless$ref=~ m!^refs/!;1622$dest.=$ref;16231624my$link=$cgi->a({1625-href => href(1626 action=>$dest_action,1627 hash=>$dest1628)},$name);16291630$markers.=" <span class=\"$class\"title=\"$ref\">".1631$link."</span>";1632}1633}16341635if($markers) {1636return' <span class="refs">'.$markers.'</span>';1637}else{1638return"";1639}1640}16411642# format, perhaps shortened and with markers, title line1643sub format_subject_html {1644my($long,$short,$href,$extra) =@_;1645$extra=''unlessdefined($extra);16461647if(length($short) <length($long)) {1648$long=~s/[[:cntrl:]]/?/g;1649return$cgi->a({-href =>$href, -class=>"list subject",1650-title => to_utf8($long)},1651 esc_html($short)) .$extra;1652}else{1653return$cgi->a({-href =>$href, -class=>"list subject"},1654 esc_html($long)) .$extra;1655}1656}16571658# Rather than recomputing the url for an email multiple times, we cache it1659# after the first hit. This gives a visible benefit in views where the avatar1660# for the same email is used repeatedly (e.g. shortlog).1661# The cache is shared by all avatar engines (currently gravatar only), which1662# are free to use it as preferred. Since only one avatar engine is used for any1663# given page, there's no risk for cache conflicts.1664our%avatar_cache= ();16651666# Compute the picon url for a given email, by using the picon search service over at1667# http://www.cs.indiana.edu/picons/search.html1668sub picon_url {1669my$email=lc shift;1670if(!$avatar_cache{$email}) {1671my($user,$domain) =split('@',$email);1672$avatar_cache{$email} =1673"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/".1674"$domain/$user/".1675"users+domains+unknown/up/single";1676}1677return$avatar_cache{$email};1678}16791680# Compute the gravatar url for a given email, if it's not in the cache already.1681# Gravatar stores only the part of the URL before the size, since that's the1682# one computationally more expensive. This also allows reuse of the cache for1683# different sizes (for this particular engine).1684sub gravatar_url {1685my$email=lc shift;1686my$size=shift;1687$avatar_cache{$email} ||=1688"http://www.gravatar.com/avatar/".1689 Digest::MD5::md5_hex($email) ."?s=";1690return$avatar_cache{$email} .$size;1691}16921693# Insert an avatar for the given $email at the given $size if the feature1694# is enabled.1695sub git_get_avatar {1696my($email,%opts) =@_;1697my$pre_white= ($opts{-pad_before} ?" ":"");1698my$post_white= ($opts{-pad_after} ?" ":"");1699$opts{-size} ||='default';1700my$size=$avatar_size{$opts{-size}} ||$avatar_size{'default'};1701my$url="";1702if($git_avatareq'gravatar') {1703$url= gravatar_url($email,$size);1704}elsif($git_avatareq'picon') {1705$url= picon_url($email);1706}1707# Other providers can be added by extending the if chain, defining $url1708# as needed. If no variant puts something in $url, we assume avatars1709# are completely disabled/unavailable.1710if($url) {1711return$pre_white.1712"<img width=\"$size\"".1713"class=\"avatar\"".1714"src=\"$url\"".1715"alt=\"\"".1716"/>".$post_white;1717}else{1718return"";1719}1720}17211722sub format_search_author {1723my($author,$searchtype,$displaytext) =@_;1724my$have_search= gitweb_check_feature('search');17251726if($have_search) {1727my$performed="";1728if($searchtypeeq'author') {1729$performed="authored";1730}elsif($searchtypeeq'committer') {1731$performed="committed";1732}17331734return$cgi->a({-href => href(action=>"search", hash=>$hash,1735 searchtext=>$author,1736 searchtype=>$searchtype),class=>"list",1737 title=>"Search for commits$performedby$author"},1738$displaytext);17391740}else{1741return$displaytext;1742}1743}17441745# format the author name of the given commit with the given tag1746# the author name is chopped and escaped according to the other1747# optional parameters (see chop_str).1748sub format_author_html {1749my$tag=shift;1750my$co=shift;1751my$author= chop_and_escape_str($co->{'author_name'},@_);1752return"<$tagclass=\"author\">".1753 format_search_author($co->{'author_name'},"author",1754 git_get_avatar($co->{'author_email'}, -pad_after =>1) .1755$author) .1756"</$tag>";1757}17581759# format git diff header line, i.e. "diff --(git|combined|cc) ..."1760sub format_git_diff_header_line {1761my$line=shift;1762my$diffinfo=shift;1763my($from,$to) =@_;17641765if($diffinfo->{'nparents'}) {1766# combined diff1767$line=~s!^(diff (.*?) )"?.*$!$1!;1768if($to->{'href'}) {1769$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1770 esc_path($to->{'file'}));1771}else{# file was deleted (no href)1772$line.= esc_path($to->{'file'});1773}1774}else{1775# "ordinary" diff1776$line=~s!^(diff (.*?) )"?a/.*$!$1!;1777if($from->{'href'}) {1778$line.=$cgi->a({-href =>$from->{'href'}, -class=>"path"},1779'a/'. esc_path($from->{'file'}));1780}else{# file was added (no href)1781$line.='a/'. esc_path($from->{'file'});1782}1783$line.=' ';1784if($to->{'href'}) {1785$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1786'b/'. esc_path($to->{'file'}));1787}else{# file was deleted1788$line.='b/'. esc_path($to->{'file'});1789}1790}17911792return"<div class=\"diff header\">$line</div>\n";1793}17941795# format extended diff header line, before patch itself1796sub format_extended_diff_header_line {1797my$line=shift;1798my$diffinfo=shift;1799my($from,$to) =@_;18001801# match <path>1802if($line=~s!^((copy|rename) from ).*$!$1!&&$from->{'href'}) {1803$line.=$cgi->a({-href=>$from->{'href'}, -class=>"path"},1804 esc_path($from->{'file'}));1805}1806if($line=~s!^((copy|rename) to ).*$!$1!&&$to->{'href'}) {1807$line.=$cgi->a({-href=>$to->{'href'}, -class=>"path"},1808 esc_path($to->{'file'}));1809}1810# match single <mode>1811if($line=~m/\s(\d{6})$/) {1812$line.='<span class="info"> ('.1813 file_type_long($1) .1814')</span>';1815}1816# match <hash>1817if($line=~m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {1818# can match only for combined diff1819$line='index ';1820for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1821if($from->{'href'}[$i]) {1822$line.=$cgi->a({-href=>$from->{'href'}[$i],1823-class=>"hash"},1824substr($diffinfo->{'from_id'}[$i],0,7));1825}else{1826$line.='0' x 7;1827}1828# separator1829$line.=','if($i<$diffinfo->{'nparents'} -1);1830}1831$line.='..';1832if($to->{'href'}) {1833$line.=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1834substr($diffinfo->{'to_id'},0,7));1835}else{1836$line.='0' x 7;1837}18381839}elsif($line=~m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {1840# can match only for ordinary diff1841my($from_link,$to_link);1842if($from->{'href'}) {1843$from_link=$cgi->a({-href=>$from->{'href'}, -class=>"hash"},1844substr($diffinfo->{'from_id'},0,7));1845}else{1846$from_link='0' x 7;1847}1848if($to->{'href'}) {1849$to_link=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1850substr($diffinfo->{'to_id'},0,7));1851}else{1852$to_link='0' x 7;1853}1854my($from_id,$to_id) = ($diffinfo->{'from_id'},$diffinfo->{'to_id'});1855$line=~s!$from_id\.\.$to_id!$from_link..$to_link!;1856}18571858return$line."<br/>\n";1859}18601861# format from-file/to-file diff header1862sub format_diff_from_to_header {1863my($from_line,$to_line,$diffinfo,$from,$to,@parents) =@_;1864my$line;1865my$result='';18661867$line=$from_line;1868#assert($line =~ m/^---/) if DEBUG;1869# no extra formatting for "^--- /dev/null"1870if(!$diffinfo->{'nparents'}) {1871# ordinary (single parent) diff1872if($line=~m!^--- "?a/!) {1873if($from->{'href'}) {1874$line='--- a/'.1875$cgi->a({-href=>$from->{'href'}, -class=>"path"},1876 esc_path($from->{'file'}));1877}else{1878$line='--- a/'.1879 esc_path($from->{'file'});1880}1881}1882$result.= qq!<div class="diff from_file">$line</div>\n!;18831884}else{1885# combined diff (merge commit)1886for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1887if($from->{'href'}[$i]) {1888$line='--- '.1889$cgi->a({-href=>href(action=>"blobdiff",1890 hash_parent=>$diffinfo->{'from_id'}[$i],1891 hash_parent_base=>$parents[$i],1892 file_parent=>$from->{'file'}[$i],1893 hash=>$diffinfo->{'to_id'},1894 hash_base=>$hash,1895 file_name=>$to->{'file'}),1896-class=>"path",1897-title=>"diff". ($i+1)},1898$i+1) .1899'/'.1900$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},1901 esc_path($from->{'file'}[$i]));1902}else{1903$line='--- /dev/null';1904}1905$result.= qq!<div class="diff from_file">$line</div>\n!;1906}1907}19081909$line=$to_line;1910#assert($line =~ m/^\+\+\+/) if DEBUG;1911# no extra formatting for "^+++ /dev/null"1912if($line=~m!^\+\+\+ "?b/!) {1913if($to->{'href'}) {1914$line='+++ b/'.1915$cgi->a({-href=>$to->{'href'}, -class=>"path"},1916 esc_path($to->{'file'}));1917}else{1918$line='+++ b/'.1919 esc_path($to->{'file'});1920}1921}1922$result.= qq!<div class="diff to_file">$line</div>\n!;19231924return$result;1925}19261927# create note for patch simplified by combined diff1928sub format_diff_cc_simplified {1929my($diffinfo,@parents) =@_;1930my$result='';19311932$result.="<div class=\"diff header\">".1933"diff --cc ";1934if(!is_deleted($diffinfo)) {1935$result.=$cgi->a({-href => href(action=>"blob",1936 hash_base=>$hash,1937 hash=>$diffinfo->{'to_id'},1938 file_name=>$diffinfo->{'to_file'}),1939-class=>"path"},1940 esc_path($diffinfo->{'to_file'}));1941}else{1942$result.= esc_path($diffinfo->{'to_file'});1943}1944$result.="</div>\n".# class="diff header"1945"<div class=\"diff nodifferences\">".1946"Simple merge".1947"</div>\n";# class="diff nodifferences"19481949return$result;1950}19511952# format patch (diff) line (not to be used for diff headers)1953sub format_diff_line {1954my$line=shift;1955my($from,$to) =@_;1956my$diff_class="";19571958chomp$line;19591960if($from&&$to&&ref($from->{'href'})eq"ARRAY") {1961# combined diff1962my$prefix=substr($line,0,scalar@{$from->{'href'}});1963if($line=~m/^\@{3}/) {1964$diff_class=" chunk_header";1965}elsif($line=~m/^\\/) {1966$diff_class=" incomplete";1967}elsif($prefix=~tr/+/+/) {1968$diff_class=" add";1969}elsif($prefix=~tr/-/-/) {1970$diff_class=" rem";1971}1972}else{1973# assume ordinary diff1974my$char=substr($line,0,1);1975if($chareq'+') {1976$diff_class=" add";1977}elsif($chareq'-') {1978$diff_class=" rem";1979}elsif($chareq'@') {1980$diff_class=" chunk_header";1981}elsif($chareq"\\") {1982$diff_class=" incomplete";1983}1984}1985$line= untabify($line);1986if($from&&$to&&$line=~m/^\@{2} /) {1987my($from_text,$from_start,$from_lines,$to_text,$to_start,$to_lines,$section) =1988$line=~m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;19891990$from_lines=0unlessdefined$from_lines;1991$to_lines=0unlessdefined$to_lines;19921993if($from->{'href'}) {1994$from_text=$cgi->a({-href=>"$from->{'href'}#l$from_start",1995-class=>"list"},$from_text);1996}1997if($to->{'href'}) {1998$to_text=$cgi->a({-href=>"$to->{'href'}#l$to_start",1999-class=>"list"},$to_text);2000}2001$line="<span class=\"chunk_info\">@@$from_text$to_text@@</span>".2002"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";2003return"<div class=\"diff$diff_class\">$line</div>\n";2004}elsif($from&&$to&&$line=~m/^\@{3}/) {2005my($prefix,$ranges,$section) =$line=~m/^(\@+) (.*?) \@+(.*)$/;2006my(@from_text,@from_start,@from_nlines,$to_text,$to_start,$to_nlines);20072008@from_text=split(' ',$ranges);2009for(my$i=0;$i<@from_text; ++$i) {2010($from_start[$i],$from_nlines[$i]) =2011(split(',',substr($from_text[$i],1)),0);2012}20132014$to_text=pop@from_text;2015$to_start=pop@from_start;2016$to_nlines=pop@from_nlines;20172018$line="<span class=\"chunk_info\">$prefix";2019for(my$i=0;$i<@from_text; ++$i) {2020if($from->{'href'}[$i]) {2021$line.=$cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",2022-class=>"list"},$from_text[$i]);2023}else{2024$line.=$from_text[$i];2025}2026$line.=" ";2027}2028if($to->{'href'}) {2029$line.=$cgi->a({-href=>"$to->{'href'}#l$to_start",2030-class=>"list"},$to_text);2031}else{2032$line.=$to_text;2033}2034$line.="$prefix</span>".2035"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";2036return"<div class=\"diff$diff_class\">$line</div>\n";2037}2038return"<div class=\"diff$diff_class\">". esc_html($line, -nbsp=>1) ."</div>\n";2039}20402041# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",2042# linked. Pass the hash of the tree/commit to snapshot.2043sub format_snapshot_links {2044my($hash) =@_;2045my$num_fmts=@snapshot_fmts;2046if($num_fmts>1) {2047# A parenthesized list of links bearing format names.2048# e.g. "snapshot (_tar.gz_ _zip_)"2049return"snapshot (".join(' ',map2050$cgi->a({2051-href => href(2052 action=>"snapshot",2053 hash=>$hash,2054 snapshot_format=>$_2055)2056},$known_snapshot_formats{$_}{'display'})2057,@snapshot_fmts) .")";2058}elsif($num_fmts==1) {2059# A single "snapshot" link whose tooltip bears the format name.2060# i.e. "_snapshot_"2061my($fmt) =@snapshot_fmts;2062return2063$cgi->a({2064-href => href(2065 action=>"snapshot",2066 hash=>$hash,2067 snapshot_format=>$fmt2068),2069-title =>"in format:$known_snapshot_formats{$fmt}{'display'}"2070},"snapshot");2071}else{# $num_fmts == 02072returnundef;2073}2074}20752076## ......................................................................2077## functions returning values to be passed, perhaps after some2078## transformation, to other functions; e.g. returning arguments to href()20792080# returns hash to be passed to href to generate gitweb URL2081# in -title key it returns description of link2082sub get_feed_info {2083my$format=shift||'Atom';2084my%res= (action =>lc($format));20852086# feed links are possible only for project views2087return unless(defined$project);2088# some views should link to OPML, or to generic project feed,2089# or don't have specific feed yet (so they should use generic)2090return if($action=~/^(?:tags|heads|forks|tag|search)$/x);20912092my$branch;2093# branches refs uses 'refs/heads/' prefix (fullname) to differentiate2094# from tag links; this also makes possible to detect branch links2095if((defined$hash_base&&$hash_base=~m!^refs/heads/(.*)$!) ||2096(defined$hash&&$hash=~m!^refs/heads/(.*)$!)) {2097$branch=$1;2098}2099# find log type for feed description (title)2100my$type='log';2101if(defined$file_name) {2102$type="history of$file_name";2103$type.="/"if($actioneq'tree');2104$type.=" on '$branch'"if(defined$branch);2105}else{2106$type="log of$branch"if(defined$branch);2107}21082109$res{-title} =$type;2110$res{'hash'} = (defined$branch?"refs/heads/$branch":undef);2111$res{'file_name'} =$file_name;21122113return%res;2114}21152116## ----------------------------------------------------------------------2117## git utility subroutines, invoking git commands21182119# returns path to the core git executable and the --git-dir parameter as list2120sub git_cmd {2121$number_of_git_cmds++;2122return$GIT,'--git-dir='.$git_dir;2123}21242125# quote the given arguments for passing them to the shell2126# quote_command("command", "arg 1", "arg with ' and ! characters")2127# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"2128# Try to avoid using this function wherever possible.2129sub quote_command {2130returnjoin(' ',2131map{my$a=$_;$a=~s/(['!])/'\\$1'/g;"'$a'"}@_);2132}21332134# get HEAD ref of given project as hash2135sub git_get_head_hash {2136return git_get_full_hash(shift,'HEAD');2137}21382139sub git_get_full_hash {2140return git_get_hash(@_);2141}21422143sub git_get_short_hash {2144return git_get_hash(@_,'--short=7');2145}21462147sub git_get_hash {2148my($project,$hash,@options) =@_;2149my$o_git_dir=$git_dir;2150my$retval=undef;2151$git_dir="$projectroot/$project";2152if(open my$fd,'-|', git_cmd(),'rev-parse',2153'--verify','-q',@options,$hash) {2154$retval= <$fd>;2155chomp$retvalifdefined$retval;2156close$fd;2157}2158if(defined$o_git_dir) {2159$git_dir=$o_git_dir;2160}2161return$retval;2162}21632164# get type of given object2165sub git_get_type {2166my$hash=shift;21672168open my$fd,"-|", git_cmd(),"cat-file",'-t',$hashorreturn;2169my$type= <$fd>;2170close$fdorreturn;2171chomp$type;2172return$type;2173}21742175# repository configuration2176our$config_file='';2177our%config;21782179# store multiple values for single key as anonymous array reference2180# single values stored directly in the hash, not as [ <value> ]2181sub hash_set_multi {2182my($hash,$key,$value) =@_;21832184if(!exists$hash->{$key}) {2185$hash->{$key} =$value;2186}elsif(!ref$hash->{$key}) {2187$hash->{$key} = [$hash->{$key},$value];2188}else{2189push@{$hash->{$key}},$value;2190}2191}21922193# return hash of git project configuration2194# optionally limited to some section, e.g. 'gitweb'2195sub git_parse_project_config {2196my$section_regexp=shift;2197my%config;21982199local$/="\0";22002201open my$fh,"-|", git_cmd(),"config",'-z','-l',2202orreturn;22032204while(my$keyval= <$fh>) {2205chomp$keyval;2206my($key,$value) =split(/\n/,$keyval,2);22072208 hash_set_multi(\%config,$key,$value)2209if(!defined$section_regexp||$key=~/^(?:$section_regexp)\./o);2210}2211close$fh;22122213return%config;2214}22152216# convert config value to boolean: 'true' or 'false'2217# no value, number > 0, 'true' and 'yes' values are true2218# rest of values are treated as false (never as error)2219sub config_to_bool {2220my$val=shift;22212222return1if!defined$val;# section.key22232224# strip leading and trailing whitespace2225$val=~s/^\s+//;2226$val=~s/\s+$//;22272228return(($val=~/^\d+$/&&$val) ||# section.key = 12229($val=~/^(?:true|yes)$/i));# section.key = true2230}22312232# convert config value to simple decimal number2233# an optional value suffix of 'k', 'm', or 'g' will cause the value2234# to be multiplied by 1024, 1048576, or 10737418242235sub config_to_int {2236my$val=shift;22372238# strip leading and trailing whitespace2239$val=~s/^\s+//;2240$val=~s/\s+$//;22412242if(my($num,$unit) = ($val=~/^([0-9]*)([kmg])$/i)) {2243$unit=lc($unit);2244# unknown unit is treated as 12245return$num* ($uniteq'g'?1073741824:2246$uniteq'm'?1048576:2247$uniteq'k'?1024:1);2248}2249return$val;2250}22512252# convert config value to array reference, if needed2253sub config_to_multi {2254my$val=shift;22552256returnref($val) ?$val: (defined($val) ? [$val] : []);2257}22582259sub git_get_project_config {2260my($key,$type) =@_;22612262return unlessdefined$git_dir;22632264# key sanity check2265return unless($key);2266$key=~s/^gitweb\.//;2267return if($key=~m/\W/);22682269# type sanity check2270if(defined$type) {2271$type=~s/^--//;2272$type=undef2273unless($typeeq'bool'||$typeeq'int');2274}22752276# get config2277if(!defined$config_file||2278$config_filene"$git_dir/config") {2279%config= git_parse_project_config('gitweb');2280$config_file="$git_dir/config";2281}22822283# check if config variable (key) exists2284return unlessexists$config{"gitweb.$key"};22852286# ensure given type2287if(!defined$type) {2288return$config{"gitweb.$key"};2289}elsif($typeeq'bool') {2290# backward compatibility: 'git config --bool' returns true/false2291return config_to_bool($config{"gitweb.$key"}) ?'true':'false';2292}elsif($typeeq'int') {2293return config_to_int($config{"gitweb.$key"});2294}2295return$config{"gitweb.$key"};2296}22972298# get hash of given path at given ref2299sub git_get_hash_by_path {2300my$base=shift;2301my$path=shift||returnundef;2302my$type=shift;23032304$path=~ s,/+$,,;23052306open my$fd,"-|", git_cmd(),"ls-tree",$base,"--",$path2307or die_error(500,"Open git-ls-tree failed");2308my$line= <$fd>;2309close$fdorreturnundef;23102311if(!defined$line) {2312# there is no tree or hash given by $path at $base2313returnundef;2314}23152316#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2317$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;2318if(defined$type&&$typene$2) {2319# type doesn't match2320returnundef;2321}2322return$3;2323}23242325# get path of entry with given hash at given tree-ish (ref)2326# used to get 'from' filename for combined diff (merge commit) for renames2327sub git_get_path_by_hash {2328my$base=shift||return;2329my$hash=shift||return;23302331local$/="\0";23322333open my$fd,"-|", git_cmd(),"ls-tree",'-r','-t','-z',$base2334orreturnundef;2335while(my$line= <$fd>) {2336chomp$line;23372338#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'2339#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'2340if($line=~m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {2341close$fd;2342return$1;2343}2344}2345close$fd;2346returnundef;2347}23482349## ......................................................................2350## git utility functions, directly accessing git repository23512352sub git_get_project_description {2353my$path=shift;23542355$git_dir="$projectroot/$path";2356open my$fd,'<',"$git_dir/description"2357orreturn git_get_project_config('description');2358my$descr= <$fd>;2359close$fd;2360if(defined$descr) {2361chomp$descr;2362}2363return$descr;2364}23652366sub git_get_project_ctags {2367my$path=shift;2368my$ctags= {};23692370$git_dir="$projectroot/$path";2371opendir my$dh,"$git_dir/ctags"2372orreturn$ctags;2373foreach(grep{ -f $_}map{"$git_dir/ctags/$_"}readdir($dh)) {2374open my$ct,'<',$_ornext;2375my$val= <$ct>;2376chomp$val;2377close$ct;2378my$ctag=$_;$ctag=~ s#.*/##;2379$ctags->{$ctag} =$val;2380}2381closedir$dh;2382$ctags;2383}23842385sub git_populate_project_tagcloud {2386my$ctags=shift;23872388# First, merge different-cased tags; tags vote on casing2389my%ctags_lc;2390foreach(keys%$ctags) {2391$ctags_lc{lc$_}->{count} +=$ctags->{$_};2392if(not$ctags_lc{lc$_}->{topcount}2393or$ctags_lc{lc$_}->{topcount} <$ctags->{$_}) {2394$ctags_lc{lc$_}->{topcount} =$ctags->{$_};2395$ctags_lc{lc$_}->{topname} =$_;2396}2397}23982399my$cloud;2400if(eval{require HTML::TagCloud;1; }) {2401$cloud= HTML::TagCloud->new;2402foreach(sort keys%ctags_lc) {2403# Pad the title with spaces so that the cloud looks2404# less crammed.2405my$title=$ctags_lc{$_}->{topname};2406$title=~s/ / /g;2407$title=~s/^/ /g;2408$title=~s/$/ /g;2409$cloud->add($title,$home_link."?by_tag=".$_,$ctags_lc{$_}->{count});2410}2411}else{2412$cloud= \%ctags_lc;2413}2414$cloud;2415}24162417sub git_show_project_tagcloud {2418my($cloud,$count) =@_;2419print STDERR ref($cloud)."..\n";2420if(ref$cloudeq'HTML::TagCloud') {2421return$cloud->html_and_css($count);2422}else{2423my@tags=sort{$cloud->{$a}->{count} <=>$cloud->{$b}->{count} }keys%$cloud;2424return'<p align="center">'.join(', ',map{2425"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"2426}splice(@tags,0,$count)) .'</p>';2427}2428}24292430sub git_get_project_url_list {2431my$path=shift;24322433$git_dir="$projectroot/$path";2434open my$fd,'<',"$git_dir/cloneurl"2435orreturnwantarray?2436@{ config_to_multi(git_get_project_config('url')) } :2437 config_to_multi(git_get_project_config('url'));2438my@git_project_url_list=map{chomp;$_} <$fd>;2439close$fd;24402441returnwantarray?@git_project_url_list: \@git_project_url_list;2442}24432444sub git_get_projects_list {2445my($filter) =@_;2446my@list;24472448$filter||='';2449$filter=~s/\.git$//;24502451my$check_forks= gitweb_check_feature('forks');24522453if(-d $projects_list) {2454# search in directory2455my$dir=$projects_list. ($filter?"/$filter":'');2456# remove the trailing "/"2457$dir=~s!/+$!!;2458my$pfxlen=length("$dir");2459my$pfxdepth= ($dir=~tr!/!!);24602461 File::Find::find({2462 follow_fast =>1,# follow symbolic links2463 follow_skip =>2,# ignore duplicates2464 dangling_symlinks =>0,# ignore dangling symlinks, silently2465 wanted =>sub{2466# skip project-list toplevel, if we get it.2467return if(m!^[/.]$!);2468# only directories can be git repositories2469return unless(-d $_);2470# don't traverse too deep (Find is super slow on os x)2471if(($File::Find::name =~tr!/!!) -$pfxdepth>$project_maxdepth) {2472$File::Find::prune =1;2473return;2474}24752476my$subdir=substr($File::Find::name,$pfxlen+1);2477# we check related file in $projectroot2478my$path= ($filter?"$filter/":'') .$subdir;2479if(check_export_ok("$projectroot/$path")) {2480push@list, { path =>$path};2481$File::Find::prune =1;2482}2483},2484},"$dir");24852486}elsif(-f $projects_list) {2487# read from file(url-encoded):2488# 'git%2Fgit.git Linus+Torvalds'2489# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2490# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2491my%paths;2492open my$fd,'<',$projects_listorreturn;2493 PROJECT:2494while(my$line= <$fd>) {2495chomp$line;2496my($path,$owner) =split' ',$line;2497$path= unescape($path);2498$owner= unescape($owner);2499if(!defined$path) {2500next;2501}2502if($filterne'') {2503# looking for forks;2504my$pfx=substr($path,0,length($filter));2505if($pfxne$filter) {2506next PROJECT;2507}2508my$sfx=substr($path,length($filter));2509if($sfx!~/^\/.*\.git$/) {2510next PROJECT;2511}2512}elsif($check_forks) {2513 PATH:2514foreachmy$filter(keys%paths) {2515# looking for forks;2516my$pfx=substr($path,0,length($filter));2517if($pfxne$filter) {2518next PATH;2519}2520my$sfx=substr($path,length($filter));2521if($sfx!~/^\/.*\.git$/) {2522next PATH;2523}2524# is a fork, don't include it in2525# the list2526next PROJECT;2527}2528}2529if(check_export_ok("$projectroot/$path")) {2530my$pr= {2531 path =>$path,2532 owner => to_utf8($owner),2533};2534push@list,$pr;2535(my$forks_path=$path) =~s/\.git$//;2536$paths{$forks_path}++;2537}2538}2539close$fd;2540}2541return@list;2542}25432544our$gitweb_project_owner=undef;2545sub git_get_project_list_from_file {25462547return if(defined$gitweb_project_owner);25482549$gitweb_project_owner= {};2550# read from file (url-encoded):2551# 'git%2Fgit.git Linus+Torvalds'2552# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2553# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2554if(-f $projects_list) {2555open(my$fd,'<',$projects_list);2556while(my$line= <$fd>) {2557chomp$line;2558my($pr,$ow) =split' ',$line;2559$pr= unescape($pr);2560$ow= unescape($ow);2561$gitweb_project_owner->{$pr} = to_utf8($ow);2562}2563close$fd;2564}2565}25662567sub git_get_project_owner {2568my$project=shift;2569my$owner;25702571returnundefunless$project;2572$git_dir="$projectroot/$project";25732574if(!defined$gitweb_project_owner) {2575 git_get_project_list_from_file();2576}25772578if(exists$gitweb_project_owner->{$project}) {2579$owner=$gitweb_project_owner->{$project};2580}2581if(!defined$owner){2582$owner= git_get_project_config('owner');2583}2584if(!defined$owner) {2585$owner= get_file_owner("$git_dir");2586}25872588return$owner;2589}25902591sub git_get_last_activity {2592my($path) =@_;2593my$fd;25942595$git_dir="$projectroot/$path";2596open($fd,"-|", git_cmd(),'for-each-ref',2597'--format=%(committer)',2598'--sort=-committerdate',2599'--count=1',2600'refs/heads')orreturn;2601my$most_recent= <$fd>;2602close$fdorreturn;2603if(defined$most_recent&&2604$most_recent=~/ (\d+) [-+][01]\d\d\d$/) {2605my$timestamp=$1;2606my$age=time-$timestamp;2607return($age, age_string($age));2608}2609return(undef,undef);2610}26112612sub git_get_references {2613my$type=shift||"";2614my%refs;2615# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.112616# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}2617open my$fd,"-|", git_cmd(),"show-ref","--dereference",2618($type? ("--","refs/$type") : ())# use -- <pattern> if $type2619orreturn;26202621while(my$line= <$fd>) {2622chomp$line;2623if($line=~m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {2624if(defined$refs{$1}) {2625push@{$refs{$1}},$2;2626}else{2627$refs{$1} = [$2];2628}2629}2630}2631close$fdorreturn;2632return \%refs;2633}26342635sub git_get_rev_name_tags {2636my$hash=shift||returnundef;26372638open my$fd,"-|", git_cmd(),"name-rev","--tags",$hash2639orreturn;2640my$name_rev= <$fd>;2641close$fd;26422643if($name_rev=~ m|^$hash tags/(.*)$|) {2644return$1;2645}else{2646# catches also '$hash undefined' output2647returnundef;2648}2649}26502651## ----------------------------------------------------------------------2652## parse to hash functions26532654sub parse_date {2655my$epoch=shift;2656my$tz=shift||"-0000";26572658my%date;2659my@months= ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");2660my@days= ("Sun","Mon","Tue","Wed","Thu","Fri","Sat");2661my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($epoch);2662$date{'hour'} =$hour;2663$date{'minute'} =$min;2664$date{'mday'} =$mday;2665$date{'day'} =$days[$wday];2666$date{'month'} =$months[$mon];2667$date{'rfc2822'} =sprintf"%s,%d%s%4d%02d:%02d:%02d+0000",2668$days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec;2669$date{'mday-time'} =sprintf"%d%s%02d:%02d",2670$mday,$months[$mon],$hour,$min;2671$date{'iso-8601'} =sprintf"%04d-%02d-%02dT%02d:%02d:%02dZ",26721900+$year,1+$mon,$mday,$hour,$min,$sec;26732674$tz=~m/^([+\-][0-9][0-9])([0-9][0-9])$/;2675my$local=$epoch+ ((int$1+ ($2/60)) *3600);2676($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($local);2677$date{'hour_local'} =$hour;2678$date{'minute_local'} =$min;2679$date{'tz_local'} =$tz;2680$date{'iso-tz'} =sprintf("%04d-%02d-%02d%02d:%02d:%02d%s",26811900+$year,$mon+1,$mday,2682$hour,$min,$sec,$tz);2683return%date;2684}26852686sub parse_tag {2687my$tag_id=shift;2688my%tag;2689my@comment;26902691open my$fd,"-|", git_cmd(),"cat-file","tag",$tag_idorreturn;2692$tag{'id'} =$tag_id;2693while(my$line= <$fd>) {2694chomp$line;2695if($line=~m/^object ([0-9a-fA-F]{40})$/) {2696$tag{'object'} =$1;2697}elsif($line=~m/^type (.+)$/) {2698$tag{'type'} =$1;2699}elsif($line=~m/^tag (.+)$/) {2700$tag{'name'} =$1;2701}elsif($line=~m/^tagger (.*) ([0-9]+) (.*)$/) {2702$tag{'author'} =$1;2703$tag{'author_epoch'} =$2;2704$tag{'author_tz'} =$3;2705if($tag{'author'} =~m/^([^<]+) <([^>]*)>/) {2706$tag{'author_name'} =$1;2707$tag{'author_email'} =$2;2708}else{2709$tag{'author_name'} =$tag{'author'};2710}2711}elsif($line=~m/--BEGIN/) {2712push@comment,$line;2713last;2714}elsif($lineeq"") {2715last;2716}2717}2718push@comment, <$fd>;2719$tag{'comment'} = \@comment;2720close$fdorreturn;2721if(!defined$tag{'name'}) {2722return2723};2724return%tag2725}27262727sub parse_commit_text {2728my($commit_text,$withparents) =@_;2729my@commit_lines=split'\n',$commit_text;2730my%co;27312732pop@commit_lines;# Remove '\0'27332734if(!@commit_lines) {2735return;2736}27372738my$header=shift@commit_lines;2739if($header!~m/^[0-9a-fA-F]{40}/) {2740return;2741}2742($co{'id'},my@parents) =split' ',$header;2743while(my$line=shift@commit_lines) {2744last if$lineeq"\n";2745if($line=~m/^tree ([0-9a-fA-F]{40})$/) {2746$co{'tree'} =$1;2747}elsif((!defined$withparents) && ($line=~m/^parent ([0-9a-fA-F]{40})$/)) {2748push@parents,$1;2749}elsif($line=~m/^author (.*) ([0-9]+) (.*)$/) {2750$co{'author'} = to_utf8($1);2751$co{'author_epoch'} =$2;2752$co{'author_tz'} =$3;2753if($co{'author'} =~m/^([^<]+) <([^>]*)>/) {2754$co{'author_name'} =$1;2755$co{'author_email'} =$2;2756}else{2757$co{'author_name'} =$co{'author'};2758}2759}elsif($line=~m/^committer (.*) ([0-9]+) (.*)$/) {2760$co{'committer'} = to_utf8($1);2761$co{'committer_epoch'} =$2;2762$co{'committer_tz'} =$3;2763if($co{'committer'} =~m/^([^<]+) <([^>]*)>/) {2764$co{'committer_name'} =$1;2765$co{'committer_email'} =$2;2766}else{2767$co{'committer_name'} =$co{'committer'};2768}2769}2770}2771if(!defined$co{'tree'}) {2772return;2773};2774$co{'parents'} = \@parents;2775$co{'parent'} =$parents[0];27762777foreachmy$title(@commit_lines) {2778$title=~s/^ //;2779if($titlene"") {2780$co{'title'} = chop_str($title,80,5);2781# remove leading stuff of merges to make the interesting part visible2782if(length($title) >50) {2783$title=~s/^Automatic //;2784$title=~s/^merge (of|with) /Merge ... /i;2785if(length($title) >50) {2786$title=~s/(http|rsync):\/\///;2787}2788if(length($title) >50) {2789$title=~s/(master|www|rsync)\.//;2790}2791if(length($title) >50) {2792$title=~s/kernel.org:?//;2793}2794if(length($title) >50) {2795$title=~s/\/pub\/scm//;2796}2797}2798$co{'title_short'} = chop_str($title,50,5);2799last;2800}2801}2802if(!defined$co{'title'} ||$co{'title'}eq"") {2803$co{'title'} =$co{'title_short'} ='(no commit message)';2804}2805# remove added spaces2806foreachmy$line(@commit_lines) {2807$line=~s/^ //;2808}2809$co{'comment'} = \@commit_lines;28102811my$age=time-$co{'committer_epoch'};2812$co{'age'} =$age;2813$co{'age_string'} = age_string($age);2814my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($co{'committer_epoch'});2815if($age>60*60*24*7*2) {2816$co{'age_string_date'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2817$co{'age_string_age'} =$co{'age_string'};2818}else{2819$co{'age_string_date'} =$co{'age_string'};2820$co{'age_string_age'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2821}2822return%co;2823}28242825sub parse_commit {2826my($commit_id) =@_;2827my%co;28282829local$/="\0";28302831open my$fd,"-|", git_cmd(),"rev-list",2832"--parents",2833"--header",2834"--max-count=1",2835$commit_id,2836"--",2837or die_error(500,"Open git-rev-list failed");2838%co= parse_commit_text(<$fd>,1);2839close$fd;28402841return%co;2842}28432844sub parse_commits {2845my($commit_id,$maxcount,$skip,$filename,@args) =@_;2846my@cos;28472848$maxcount||=1;2849$skip||=0;28502851local$/="\0";28522853open my$fd,"-|", git_cmd(),"rev-list",2854"--header",2855@args,2856("--max-count=".$maxcount),2857("--skip=".$skip),2858@extra_options,2859$commit_id,2860"--",2861($filename? ($filename) : ())2862or die_error(500,"Open git-rev-list failed");2863while(my$line= <$fd>) {2864my%co= parse_commit_text($line);2865push@cos, \%co;2866}2867close$fd;28682869returnwantarray?@cos: \@cos;2870}28712872# parse line of git-diff-tree "raw" output2873sub parse_difftree_raw_line {2874my$line=shift;2875my%res;28762877# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'2878# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'2879if($line=~m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {2880$res{'from_mode'} =$1;2881$res{'to_mode'} =$2;2882$res{'from_id'} =$3;2883$res{'to_id'} =$4;2884$res{'status'} =$5;2885$res{'similarity'} =$6;2886if($res{'status'}eq'R'||$res{'status'}eq'C') {# renamed or copied2887($res{'from_file'},$res{'to_file'}) =map{ unquote($_) }split("\t",$7);2888}else{2889$res{'from_file'} =$res{'to_file'} =$res{'file'} = unquote($7);2890}2891}2892# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'2893# combined diff (for merge commit)2894elsif($line=~s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {2895$res{'nparents'} =length($1);2896$res{'from_mode'} = [split(' ',$2) ];2897$res{'to_mode'} =pop@{$res{'from_mode'}};2898$res{'from_id'} = [split(' ',$3) ];2899$res{'to_id'} =pop@{$res{'from_id'}};2900$res{'status'} = [split('',$4) ];2901$res{'to_file'} = unquote($5);2902}2903# 'c512b523472485aef4fff9e57b229d9d243c967f'2904elsif($line=~m/^([0-9a-fA-F]{40})$/) {2905$res{'commit'} =$1;2906}29072908returnwantarray?%res: \%res;2909}29102911# wrapper: return parsed line of git-diff-tree "raw" output2912# (the argument might be raw line, or parsed info)2913sub parsed_difftree_line {2914my$line_or_ref=shift;29152916if(ref($line_or_ref)eq"HASH") {2917# pre-parsed (or generated by hand)2918return$line_or_ref;2919}else{2920return parse_difftree_raw_line($line_or_ref);2921}2922}29232924# parse line of git-ls-tree output2925sub parse_ls_tree_line {2926my$line=shift;2927my%opts=@_;2928my%res;29292930if($opts{'-l'}) {2931#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'2932$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;29332934$res{'mode'} =$1;2935$res{'type'} =$2;2936$res{'hash'} =$3;2937$res{'size'} =$4;2938if($opts{'-z'}) {2939$res{'name'} =$5;2940}else{2941$res{'name'} = unquote($5);2942}2943}else{2944#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2945$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;29462947$res{'mode'} =$1;2948$res{'type'} =$2;2949$res{'hash'} =$3;2950if($opts{'-z'}) {2951$res{'name'} =$4;2952}else{2953$res{'name'} = unquote($4);2954}2955}29562957returnwantarray?%res: \%res;2958}29592960# generates _two_ hashes, references to which are passed as 2 and 3 argument2961sub parse_from_to_diffinfo {2962my($diffinfo,$from,$to,@parents) =@_;29632964if($diffinfo->{'nparents'}) {2965# combined diff2966$from->{'file'} = [];2967$from->{'href'} = [];2968 fill_from_file_info($diffinfo,@parents)2969unlessexists$diffinfo->{'from_file'};2970for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {2971$from->{'file'}[$i] =2972defined$diffinfo->{'from_file'}[$i] ?2973$diffinfo->{'from_file'}[$i] :2974$diffinfo->{'to_file'};2975if($diffinfo->{'status'}[$i]ne"A") {# not new (added) file2976$from->{'href'}[$i] = href(action=>"blob",2977 hash_base=>$parents[$i],2978 hash=>$diffinfo->{'from_id'}[$i],2979 file_name=>$from->{'file'}[$i]);2980}else{2981$from->{'href'}[$i] =undef;2982}2983}2984}else{2985# ordinary (not combined) diff2986$from->{'file'} =$diffinfo->{'from_file'};2987if($diffinfo->{'status'}ne"A") {# not new (added) file2988$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,2989 hash=>$diffinfo->{'from_id'},2990 file_name=>$from->{'file'});2991}else{2992delete$from->{'href'};2993}2994}29952996$to->{'file'} =$diffinfo->{'to_file'};2997if(!is_deleted($diffinfo)) {# file exists in result2998$to->{'href'} = href(action=>"blob", hash_base=>$hash,2999 hash=>$diffinfo->{'to_id'},3000 file_name=>$to->{'file'});3001}else{3002delete$to->{'href'};3003}3004}30053006## ......................................................................3007## parse to array of hashes functions30083009sub git_get_heads_list {3010my$limit=shift;3011my@headslist;30123013open my$fd,'-|', git_cmd(),'for-each-ref',3014($limit?'--count='.($limit+1) : ()),'--sort=-committerdate',3015'--format=%(objectname) %(refname) %(subject)%00%(committer)',3016'refs/heads'3017orreturn;3018while(my$line= <$fd>) {3019my%ref_item;30203021chomp$line;3022my($refinfo,$committerinfo) =split(/\0/,$line);3023my($hash,$name,$title) =split(' ',$refinfo,3);3024my($committer,$epoch,$tz) =3025($committerinfo=~/^(.*) ([0-9]+) (.*)$/);3026$ref_item{'fullname'} =$name;3027$name=~s!^refs/heads/!!;30283029$ref_item{'name'} =$name;3030$ref_item{'id'} =$hash;3031$ref_item{'title'} =$title||'(no commit message)';3032$ref_item{'epoch'} =$epoch;3033if($epoch) {3034$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3035}else{3036$ref_item{'age'} ="unknown";3037}30383039push@headslist, \%ref_item;3040}3041close$fd;30423043returnwantarray?@headslist: \@headslist;3044}30453046sub git_get_tags_list {3047my$limit=shift;3048my@tagslist;30493050open my$fd,'-|', git_cmd(),'for-each-ref',3051($limit?'--count='.($limit+1) : ()),'--sort=-creatordate',3052'--format=%(objectname) %(objecttype) %(refname) '.3053'%(*objectname) %(*objecttype) %(subject)%00%(creator)',3054'refs/tags'3055orreturn;3056while(my$line= <$fd>) {3057my%ref_item;30583059chomp$line;3060my($refinfo,$creatorinfo) =split(/\0/,$line);3061my($id,$type,$name,$refid,$reftype,$title) =split(' ',$refinfo,6);3062my($creator,$epoch,$tz) =3063($creatorinfo=~/^(.*) ([0-9]+) (.*)$/);3064$ref_item{'fullname'} =$name;3065$name=~s!^refs/tags/!!;30663067$ref_item{'type'} =$type;3068$ref_item{'id'} =$id;3069$ref_item{'name'} =$name;3070if($typeeq"tag") {3071$ref_item{'subject'} =$title;3072$ref_item{'reftype'} =$reftype;3073$ref_item{'refid'} =$refid;3074}else{3075$ref_item{'reftype'} =$type;3076$ref_item{'refid'} =$id;3077}30783079if($typeeq"tag"||$typeeq"commit") {3080$ref_item{'epoch'} =$epoch;3081if($epoch) {3082$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3083}else{3084$ref_item{'age'} ="unknown";3085}3086}30873088push@tagslist, \%ref_item;3089}3090close$fd;30913092returnwantarray?@tagslist: \@tagslist;3093}30943095## ----------------------------------------------------------------------3096## filesystem-related functions30973098sub get_file_owner {3099my$path=shift;31003101my($dev,$ino,$mode,$nlink,$st_uid,$st_gid,$rdev,$size) =stat($path);3102my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) =getpwuid($st_uid);3103if(!defined$gcos) {3104returnundef;3105}3106my$owner=$gcos;3107$owner=~s/[,;].*$//;3108return to_utf8($owner);3109}31103111# assume that file exists3112sub insert_file {3113my$filename=shift;31143115open my$fd,'<',$filename;3116print map{ to_utf8($_) } <$fd>;3117close$fd;3118}31193120## ......................................................................3121## mimetype related functions31223123sub mimetype_guess_file {3124my$filename=shift;3125my$mimemap=shift;3126-r $mimemaporreturnundef;31273128my%mimemap;3129open(my$mh,'<',$mimemap)orreturnundef;3130while(<$mh>) {3131next ifm/^#/;# skip comments3132my($mimetype,$exts) =split(/\t+/);3133if(defined$exts) {3134my@exts=split(/\s+/,$exts);3135foreachmy$ext(@exts) {3136$mimemap{$ext} =$mimetype;3137}3138}3139}3140close($mh);31413142$filename=~/\.([^.]*)$/;3143return$mimemap{$1};3144}31453146sub mimetype_guess {3147my$filename=shift;3148my$mime;3149$filename=~/\./orreturnundef;31503151if($mimetypes_file) {3152my$file=$mimetypes_file;3153if($file!~m!^/!) {# if it is relative path3154# it is relative to project3155$file="$projectroot/$project/$file";3156}3157$mime= mimetype_guess_file($filename,$file);3158}3159$mime||= mimetype_guess_file($filename,'/etc/mime.types');3160return$mime;3161}31623163sub blob_mimetype {3164my$fd=shift;3165my$filename=shift;31663167if($filename) {3168my$mime= mimetype_guess($filename);3169$mimeandreturn$mime;3170}31713172# just in case3173return$default_blob_plain_mimetypeunless$fd;31743175if(-T $fd) {3176return'text/plain';3177}elsif(!$filename) {3178return'application/octet-stream';3179}elsif($filename=~m/\.png$/i) {3180return'image/png';3181}elsif($filename=~m/\.gif$/i) {3182return'image/gif';3183}elsif($filename=~m/\.jpe?g$/i) {3184return'image/jpeg';3185}else{3186return'application/octet-stream';3187}3188}31893190sub blob_contenttype {3191my($fd,$file_name,$type) =@_;31923193$type||= blob_mimetype($fd,$file_name);3194if($typeeq'text/plain'&&defined$default_text_plain_charset) {3195$type.="; charset=$default_text_plain_charset";3196}31973198return$type;3199}32003201## ======================================================================3202## functions printing HTML: header, footer, error page32033204sub git_header_html {3205my$status=shift||"200 OK";3206my$expires=shift;32073208my$title="$site_name";3209if(defined$project) {3210$title.=" - ". to_utf8($project);3211if(defined$action) {3212$title.="/$action";3213if(defined$file_name) {3214$title.=" - ". esc_path($file_name);3215if($actioneq"tree"&&$file_name!~ m|/$|) {3216$title.="/";3217}3218}3219}3220}3221my$content_type;3222# require explicit support from the UA if we are to send the page as3223# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.3224# we have to do this because MSIE sometimes globs '*/*', pretending to3225# support xhtml+xml but choking when it gets what it asked for.3226if(defined$cgi->http('HTTP_ACCEPT') &&3227$cgi->http('HTTP_ACCEPT') =~m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&3228$cgi->Accept('application/xhtml+xml') !=0) {3229$content_type='application/xhtml+xml';3230}else{3231$content_type='text/html';3232}3233print$cgi->header(-type=>$content_type, -charset =>'utf-8',3234-status=>$status, -expires =>$expires);3235my$mod_perl_version=$ENV{'MOD_PERL'} ?"$ENV{'MOD_PERL'}":'';3236print<<EOF;3237<?xml version="1.0" encoding="utf-8"?>3238<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">3239<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">3240<!-- git web interface version$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->3241<!-- git core binaries version$git_version-->3242<head>3243<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>3244<meta name="generator" content="gitweb/$versiongit/$git_version$mod_perl_version"/>3245<meta name="robots" content="index, nofollow"/>3246<title>$title</title>3247EOF3248# the stylesheet, favicon etc urls won't work correctly with path_info3249# unless we set the appropriate base URL3250if($ENV{'PATH_INFO'}) {3251print"<base href=\"".esc_url($base_url)."\"/>\n";3252}3253# print out each stylesheet that exist, providing backwards capability3254# for those people who defined $stylesheet in a config file3255if(defined$stylesheet) {3256print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3257}else{3258foreachmy$stylesheet(@stylesheets) {3259next unless$stylesheet;3260print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3261}3262}3263if(defined$project) {3264my%href_params= get_feed_info();3265if(!exists$href_params{'-title'}) {3266$href_params{'-title'} ='log';3267}32683269foreachmy$formatqw(RSS Atom){3270my$type=lc($format);3271my%link_attr= (3272'-rel'=>'alternate',3273'-title'=>"$project-$href_params{'-title'} -$formatfeed",3274'-type'=>"application/$type+xml"3275);32763277$href_params{'action'} =$type;3278$link_attr{'-href'} = href(%href_params);3279print"<link ".3280"rel=\"$link_attr{'-rel'}\"".3281"title=\"$link_attr{'-title'}\"".3282"href=\"$link_attr{'-href'}\"".3283"type=\"$link_attr{'-type'}\"".3284"/>\n";32853286$href_params{'extra_options'} ='--no-merges';3287$link_attr{'-href'} = href(%href_params);3288$link_attr{'-title'} .=' (no merges)';3289print"<link ".3290"rel=\"$link_attr{'-rel'}\"".3291"title=\"$link_attr{'-title'}\"".3292"href=\"$link_attr{'-href'}\"".3293"type=\"$link_attr{'-type'}\"".3294"/>\n";3295}32963297}else{3298printf('<link rel="alternate" title="%sprojects list" '.3299'href="%s" type="text/plain; charset=utf-8" />'."\n",3300$site_name, href(project=>undef, action=>"project_index"));3301printf('<link rel="alternate" title="%sprojects feeds" '.3302'href="%s" type="text/x-opml" />'."\n",3303$site_name, href(project=>undef, action=>"opml"));3304}3305if(defined$favicon) {3306printqq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);3307}33083309print"</head>\n".3310"<body>\n";33113312if(defined$site_header&& -f $site_header) {3313 insert_file($site_header);3314}33153316print"<div class=\"page_header\">\n".3317$cgi->a({-href => esc_url($logo_url),3318-title =>$logo_label},3319qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));3320print$cgi->a({-href => esc_url($home_link)},$home_link_str) ." / ";3321if(defined$project) {3322print$cgi->a({-href => href(action=>"summary")}, esc_html($project));3323if(defined$action) {3324print" /$action";3325}3326print"\n";3327}3328print"</div>\n";33293330my$have_search= gitweb_check_feature('search');3331if(defined$project&&$have_search) {3332if(!defined$searchtext) {3333$searchtext="";3334}3335my$search_hash;3336if(defined$hash_base) {3337$search_hash=$hash_base;3338}elsif(defined$hash) {3339$search_hash=$hash;3340}else{3341$search_hash="HEAD";3342}3343my$action=$my_uri;3344my$use_pathinfo= gitweb_check_feature('pathinfo');3345if($use_pathinfo) {3346$action.="/".esc_url($project);3347}3348print$cgi->startform(-method=>"get", -action =>$action) .3349"<div class=\"search\">\n".3350(!$use_pathinfo&&3351$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) ."\n") .3352$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) ."\n".3353$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) ."\n".3354$cgi->popup_menu(-name =>'st', -default=>'commit',3355-values=> ['commit','grep','author','committer','pickaxe']) .3356$cgi->sup($cgi->a({-href => href(action=>"search_help")},"?")) .3357" search:\n",3358$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".3359"<span title=\"Extended regular expression\">".3360$cgi->checkbox(-name =>'sr', -value =>1, -label =>'re',3361-checked =>$search_use_regexp) .3362"</span>".3363"</div>".3364$cgi->end_form() ."\n";3365}3366}33673368sub git_footer_html {3369my$feed_class='rss_logo';33703371print"<div class=\"page_footer\">\n";3372if(defined$project) {3373my$descr= git_get_project_description($project);3374if(defined$descr) {3375print"<div class=\"page_footer_text\">". esc_html($descr) ."</div>\n";3376}33773378my%href_params= get_feed_info();3379if(!%href_params) {3380$feed_class.=' generic';3381}3382$href_params{'-title'} ||='log';33833384foreachmy$formatqw(RSS Atom){3385$href_params{'action'} =lc($format);3386print$cgi->a({-href => href(%href_params),3387-title =>"$href_params{'-title'}$formatfeed",3388-class=>$feed_class},$format)."\n";3389}33903391}else{3392print$cgi->a({-href => href(project=>undef, action=>"opml"),3393-class=>$feed_class},"OPML") ." ";3394print$cgi->a({-href => href(project=>undef, action=>"project_index"),3395-class=>$feed_class},"TXT") ."\n";3396}3397print"</div>\n";# class="page_footer"33983399if(defined$t0&& gitweb_check_feature('timed')) {3400print"<div id=\"generating_info\">\n";3401print'This page took '.3402'<span id="generating_time" class="time_span">'.3403 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).3404' seconds </span>'.3405' and '.3406'<span id="generating_cmd">'.3407$number_of_git_cmds.3408'</span> git commands '.3409" to generate.\n";3410print"</div>\n";# class="page_footer"3411}34123413if(defined$site_footer&& -f $site_footer) {3414 insert_file($site_footer);3415}34163417print qq!<script type="text/javascript" src="$javascript"></script>\n!;3418if(defined$action&&3419$actioneq'blame_incremental') {3420print qq!<script type="text/javascript">\n!.3421 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.3422 qq!"!. href() .qq!");\n!.3423 qq!</script>\n!;3424}elsif(gitweb_check_feature('javascript-actions')) {3425print qq!<script type="text/javascript">\n!.3426 qq!window.onload = fixLinks;\n!.3427 qq!</script>\n!;3428}34293430print"</body>\n".3431"</html>";3432}34333434# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])3435# Example: die_error(404, 'Hash not found')3436# By convention, use the following status codes (as defined in RFC 2616):3437# 400: Invalid or missing CGI parameters, or3438# requested object exists but has wrong type.3439# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on3440# this server or project.3441# 404: Requested object/revision/project doesn't exist.3442# 500: The server isn't configured properly, or3443# an internal error occurred (e.g. failed assertions caused by bugs), or3444# an unknown error occurred (e.g. the git binary died unexpectedly).3445# 503: The server is currently unavailable (because it is overloaded,3446# or down for maintenance). Generally, this is a temporary state.3447sub die_error {3448my$status=shift||500;3449my$error= esc_html(shift) ||"Internal Server Error";3450my$extra=shift;34513452my%http_responses= (3453400=>'400 Bad Request',3454403=>'403 Forbidden',3455404=>'404 Not Found',3456500=>'500 Internal Server Error',3457503=>'503 Service Unavailable',3458);3459 git_header_html($http_responses{$status});3460print<<EOF;3461<div class="page_body">3462<br /><br />3463$status-$error3464<br />3465EOF3466if(defined$extra) {3467print"<hr />\n".3468"$extra\n";3469}3470print"</div>\n";34713472 git_footer_html();3473exit;3474}34753476## ----------------------------------------------------------------------3477## functions printing or outputting HTML: navigation34783479sub git_print_page_nav {3480my($current,$suppress,$head,$treehead,$treebase,$extra) =@_;3481$extra=''if!defined$extra;# pager or formats34823483my@navs=qw(summary shortlog log commit commitdiff tree);3484if($suppress) {3485@navs=grep{$_ne$suppress}@navs;3486}34873488my%arg=map{$_=> {action=>$_} }@navs;3489if(defined$head) {3490for(qw(commit commitdiff)) {3491$arg{$_}{'hash'} =$head;3492}3493if($current=~m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {3494for(qw(shortlog log)) {3495$arg{$_}{'hash'} =$head;3496}3497}3498}34993500$arg{'tree'}{'hash'} =$treeheadifdefined$treehead;3501$arg{'tree'}{'hash_base'} =$treebaseifdefined$treebase;35023503my@actions= gitweb_get_feature('actions');3504my%repl= (3505'%'=>'%',3506'n'=>$project,# project name3507'f'=>$git_dir,# project path within filesystem3508'h'=>$treehead||'',# current hash ('h' parameter)3509'b'=>$treebase||'',# hash base ('hb' parameter)3510);3511while(@actions) {3512my($label,$link,$pos) =splice(@actions,0,3);3513# insert3514@navs=map{$_eq$pos? ($_,$label) :$_}@navs;3515# munch munch3516$link=~s/%([%nfhb])/$repl{$1}/g;3517$arg{$label}{'_href'} =$link;3518}35193520print"<div class=\"page_nav\">\n".3521(join" | ",3522map{$_eq$current?3523$_:$cgi->a({-href => ($arg{$_}{_href} ?$arg{$_}{_href} : href(%{$arg{$_}}))},"$_")3524}@navs);3525print"<br/>\n$extra<br/>\n".3526"</div>\n";3527}35283529sub format_paging_nav {3530my($action,$page,$has_next_link) =@_;3531my$paging_nav;353235333534if($page>0) {3535$paging_nav.=3536$cgi->a({-href => href(-replay=>1, page=>undef)},"first") .3537" ⋅ ".3538$cgi->a({-href => href(-replay=>1, page=>$page-1),3539-accesskey =>"p", -title =>"Alt-p"},"prev");3540}else{3541$paging_nav.="first ⋅ prev";3542}35433544if($has_next_link) {3545$paging_nav.=" ⋅ ".3546$cgi->a({-href => href(-replay=>1, page=>$page+1),3547-accesskey =>"n", -title =>"Alt-n"},"next");3548}else{3549$paging_nav.=" ⋅ next";3550}35513552return$paging_nav;3553}35543555## ......................................................................3556## functions printing or outputting HTML: div35573558sub git_print_header_div {3559my($action,$title,$hash,$hash_base) =@_;3560my%args= ();35613562$args{'action'} =$action;3563$args{'hash'} =$hashif$hash;3564$args{'hash_base'} =$hash_baseif$hash_base;35653566print"<div class=\"header\">\n".3567$cgi->a({-href => href(%args), -class=>"title"},3568$title?$title:$action) .3569"\n</div>\n";3570}35713572sub print_local_time {3573print format_local_time(@_);3574}35753576sub format_local_time {3577my$localtime='';3578my%date=@_;3579if($date{'hour_local'} <6) {3580$localtime.=sprintf(" (<span class=\"atnight\">%02d:%02d</span>%s)",3581$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3582}else{3583$localtime.=sprintf(" (%02d:%02d%s)",3584$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3585}35863587return$localtime;3588}35893590# Outputs the author name and date in long form3591sub git_print_authorship {3592my$co=shift;3593my%opts=@_;3594my$tag=$opts{-tag} ||'div';3595my$author=$co->{'author_name'};35963597my%ad= parse_date($co->{'author_epoch'},$co->{'author_tz'});3598print"<$tagclass=\"author_date\">".3599 format_search_author($author,"author", esc_html($author)) .3600" [$ad{'rfc2822'}";3601 print_local_time(%ad)if($opts{-localtime});3602print"]". git_get_avatar($co->{'author_email'}, -pad_before =>1)3603."</$tag>\n";3604}36053606# Outputs table rows containing the full author or committer information,3607# in the format expected for 'commit' view (& similia).3608# Parameters are a commit hash reference, followed by the list of people3609# to output information for. If the list is empty it defalts to both3610# author and committer.3611sub git_print_authorship_rows {3612my$co=shift;3613# too bad we can't use @people = @_ || ('author', 'committer')3614my@people=@_;3615@people= ('author','committer')unless@people;3616foreachmy$who(@people) {3617my%wd= parse_date($co->{"${who}_epoch"},$co->{"${who}_tz"});3618print"<tr><td>$who</td><td>".3619 format_search_author($co->{"${who}_name"},$who,3620 esc_html($co->{"${who}_name"})) ." ".3621 format_search_author($co->{"${who}_email"},$who,3622 esc_html("<".$co->{"${who}_email"} .">")) .3623"</td><td rowspan=\"2\">".3624 git_get_avatar($co->{"${who}_email"}, -size =>'double') .3625"</td></tr>\n".3626"<tr>".3627"<td></td><td>$wd{'rfc2822'}";3628 print_local_time(%wd);3629print"</td>".3630"</tr>\n";3631}3632}36333634sub git_print_page_path {3635my$name=shift;3636my$type=shift;3637my$hb=shift;363836393640print"<div class=\"page_path\">";3641print$cgi->a({-href => href(action=>"tree", hash_base=>$hb),3642-title =>'tree root'}, to_utf8("[$project]"));3643print" / ";3644if(defined$name) {3645my@dirname=split'/',$name;3646my$basename=pop@dirname;3647my$fullname='';36483649foreachmy$dir(@dirname) {3650$fullname.= ($fullname?'/':'') .$dir;3651print$cgi->a({-href => href(action=>"tree", file_name=>$fullname,3652 hash_base=>$hb),3653-title =>$fullname}, esc_path($dir));3654print" / ";3655}3656if(defined$type&&$typeeq'blob') {3657print$cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,3658 hash_base=>$hb),3659-title =>$name}, esc_path($basename));3660}elsif(defined$type&&$typeeq'tree') {3661print$cgi->a({-href => href(action=>"tree", file_name=>$file_name,3662 hash_base=>$hb),3663-title =>$name}, esc_path($basename));3664print" / ";3665}else{3666print esc_path($basename);3667}3668}3669print"<br/></div>\n";3670}36713672sub git_print_log {3673my$log=shift;3674my%opts=@_;36753676if($opts{'-remove_title'}) {3677# remove title, i.e. first line of log3678shift@$log;3679}3680# remove leading empty lines3681while(defined$log->[0] &&$log->[0]eq"") {3682shift@$log;3683}36843685# print log3686my$signoff=0;3687my$empty=0;3688foreachmy$line(@$log) {3689if($line=~m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {3690$signoff=1;3691$empty=0;3692if(!$opts{'-remove_signoff'}) {3693print"<span class=\"signoff\">". esc_html($line) ."</span><br/>\n";3694next;3695}else{3696# remove signoff lines3697next;3698}3699}else{3700$signoff=0;3701}37023703# print only one empty line3704# do not print empty line after signoff3705if($lineeq"") {3706next if($empty||$signoff);3707$empty=1;3708}else{3709$empty=0;3710}37113712print format_log_line_html($line) ."<br/>\n";3713}37143715if($opts{'-final_empty_line'}) {3716# end with single empty line3717print"<br/>\n"unless$empty;3718}3719}37203721# return link target (what link points to)3722sub git_get_link_target {3723my$hash=shift;3724my$link_target;37253726# read link3727open my$fd,"-|", git_cmd(),"cat-file","blob",$hash3728orreturn;3729{3730local$/=undef;3731$link_target= <$fd>;3732}3733close$fd3734orreturn;37353736return$link_target;3737}37383739# given link target, and the directory (basedir) the link is in,3740# return target of link relative to top directory (top tree);3741# return undef if it is not possible (including absolute links).3742sub normalize_link_target {3743my($link_target,$basedir) =@_;37443745# absolute symlinks (beginning with '/') cannot be normalized3746return if(substr($link_target,0,1)eq'/');37473748# normalize link target to path from top (root) tree (dir)3749my$path;3750if($basedir) {3751$path=$basedir.'/'.$link_target;3752}else{3753# we are in top (root) tree (dir)3754$path=$link_target;3755}37563757# remove //, /./, and /../3758my@path_parts;3759foreachmy$part(split('/',$path)) {3760# discard '.' and ''3761next if(!$part||$parteq'.');3762# handle '..'3763if($parteq'..') {3764if(@path_parts) {3765pop@path_parts;3766}else{3767# link leads outside repository (outside top dir)3768return;3769}3770}else{3771push@path_parts,$part;3772}3773}3774$path=join('/',@path_parts);37753776return$path;3777}37783779# print tree entry (row of git_tree), but without encompassing <tr> element3780sub git_print_tree_entry {3781my($t,$basedir,$hash_base,$have_blame) =@_;37823783my%base_key= ();3784$base_key{'hash_base'} =$hash_baseifdefined$hash_base;37853786# The format of a table row is: mode list link. Where mode is3787# the mode of the entry, list is the name of the entry, an href,3788# and link is the action links of the entry.37893790print"<td class=\"mode\">". mode_str($t->{'mode'}) ."</td>\n";3791if(exists$t->{'size'}) {3792print"<td class=\"size\">$t->{'size'}</td>\n";3793}3794if($t->{'type'}eq"blob") {3795print"<td class=\"list\">".3796$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3797 file_name=>"$basedir$t->{'name'}",%base_key),3798-class=>"list"}, esc_path($t->{'name'}));3799if(S_ISLNK(oct$t->{'mode'})) {3800my$link_target= git_get_link_target($t->{'hash'});3801if($link_target) {3802my$norm_target= normalize_link_target($link_target,$basedir);3803if(defined$norm_target) {3804print" -> ".3805$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,3806 file_name=>$norm_target),3807-title =>$norm_target}, esc_path($link_target));3808}else{3809print" -> ". esc_path($link_target);3810}3811}3812}3813print"</td>\n";3814print"<td class=\"link\">";3815print$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3816 file_name=>"$basedir$t->{'name'}",%base_key)},3817"blob");3818if($have_blame) {3819print" | ".3820$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},3821 file_name=>"$basedir$t->{'name'}",%base_key)},3822"blame");3823}3824if(defined$hash_base) {3825print" | ".3826$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3827 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},3828"history");3829}3830print" | ".3831$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,3832 file_name=>"$basedir$t->{'name'}")},3833"raw");3834print"</td>\n";38353836}elsif($t->{'type'}eq"tree") {3837print"<td class=\"list\">";3838print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3839 file_name=>"$basedir$t->{'name'}",3840%base_key)},3841 esc_path($t->{'name'}));3842print"</td>\n";3843print"<td class=\"link\">";3844print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3845 file_name=>"$basedir$t->{'name'}",3846%base_key)},3847"tree");3848if(defined$hash_base) {3849print" | ".3850$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3851 file_name=>"$basedir$t->{'name'}")},3852"history");3853}3854print"</td>\n";3855}else{3856# unknown object: we can only present history for it3857# (this includes 'commit' object, i.e. submodule support)3858print"<td class=\"list\">".3859 esc_path($t->{'name'}) .3860"</td>\n";3861print"<td class=\"link\">";3862if(defined$hash_base) {3863print$cgi->a({-href => href(action=>"history",3864 hash_base=>$hash_base,3865 file_name=>"$basedir$t->{'name'}")},3866"history");3867}3868print"</td>\n";3869}3870}38713872## ......................................................................3873## functions printing large fragments of HTML38743875# get pre-image filenames for merge (combined) diff3876sub fill_from_file_info {3877my($diff,@parents) =@_;38783879$diff->{'from_file'} = [ ];3880$diff->{'from_file'}[$diff->{'nparents'} -1] =undef;3881for(my$i=0;$i<$diff->{'nparents'};$i++) {3882if($diff->{'status'}[$i]eq'R'||3883$diff->{'status'}[$i]eq'C') {3884$diff->{'from_file'}[$i] =3885 git_get_path_by_hash($parents[$i],$diff->{'from_id'}[$i]);3886}3887}38883889return$diff;3890}38913892# is current raw difftree line of file deletion3893sub is_deleted {3894my$diffinfo=shift;38953896return$diffinfo->{'to_id'}eq('0' x 40);3897}38983899# does patch correspond to [previous] difftree raw line3900# $diffinfo - hashref of parsed raw diff format3901# $patchinfo - hashref of parsed patch diff format3902# (the same keys as in $diffinfo)3903sub is_patch_split {3904my($diffinfo,$patchinfo) =@_;39053906returndefined$diffinfo&&defined$patchinfo3907&&$diffinfo->{'to_file'}eq$patchinfo->{'to_file'};3908}390939103911sub git_difftree_body {3912my($difftree,$hash,@parents) =@_;3913my($parent) =$parents[0];3914my$have_blame= gitweb_check_feature('blame');3915print"<div class=\"list_head\">\n";3916if($#{$difftree} >10) {3917print(($#{$difftree} +1) ." files changed:\n");3918}3919print"</div>\n";39203921print"<table class=\"".3922(@parents>1?"combined ":"") .3923"diff_tree\">\n";39243925# header only for combined diff in 'commitdiff' view3926my$has_header=@$difftree&&@parents>1&&$actioneq'commitdiff';3927if($has_header) {3928# table header3929print"<thead><tr>\n".3930"<th></th><th></th>\n";# filename, patchN link3931for(my$i=0;$i<@parents;$i++) {3932my$par=$parents[$i];3933print"<th>".3934$cgi->a({-href => href(action=>"commitdiff",3935 hash=>$hash, hash_parent=>$par),3936-title =>'commitdiff to parent number '.3937($i+1) .': '.substr($par,0,7)},3938$i+1) .3939" </th>\n";3940}3941print"</tr></thead>\n<tbody>\n";3942}39433944my$alternate=1;3945my$patchno=0;3946foreachmy$line(@{$difftree}) {3947my$diff= parsed_difftree_line($line);39483949if($alternate) {3950print"<tr class=\"dark\">\n";3951}else{3952print"<tr class=\"light\">\n";3953}3954$alternate^=1;39553956if(exists$diff->{'nparents'}) {# combined diff39573958 fill_from_file_info($diff,@parents)3959unlessexists$diff->{'from_file'};39603961if(!is_deleted($diff)) {3962# file exists in the result (child) commit3963print"<td>".3964$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3965 file_name=>$diff->{'to_file'},3966 hash_base=>$hash),3967-class=>"list"}, esc_path($diff->{'to_file'})) .3968"</td>\n";3969}else{3970print"<td>".3971 esc_path($diff->{'to_file'}) .3972"</td>\n";3973}39743975if($actioneq'commitdiff') {3976# link to patch3977$patchno++;3978print"<td class=\"link\">".3979$cgi->a({-href =>"#patch$patchno"},"patch") .3980" | ".3981"</td>\n";3982}39833984my$has_history=0;3985my$not_deleted=0;3986for(my$i=0;$i<$diff->{'nparents'};$i++) {3987my$hash_parent=$parents[$i];3988my$from_hash=$diff->{'from_id'}[$i];3989my$from_path=$diff->{'from_file'}[$i];3990my$status=$diff->{'status'}[$i];39913992$has_history||= ($statusne'A');3993$not_deleted||= ($statusne'D');39943995if($statuseq'A') {3996print"<td class=\"link\"align=\"right\"> | </td>\n";3997}elsif($statuseq'D') {3998print"<td class=\"link\">".3999$cgi->a({-href => href(action=>"blob",4000 hash_base=>$hash,4001 hash=>$from_hash,4002 file_name=>$from_path)},4003"blob". ($i+1)) .4004" | </td>\n";4005}else{4006if($diff->{'to_id'}eq$from_hash) {4007print"<td class=\"link nochange\">";4008}else{4009print"<td class=\"link\">";4010}4011print$cgi->a({-href => href(action=>"blobdiff",4012 hash=>$diff->{'to_id'},4013 hash_parent=>$from_hash,4014 hash_base=>$hash,4015 hash_parent_base=>$hash_parent,4016 file_name=>$diff->{'to_file'},4017 file_parent=>$from_path)},4018"diff". ($i+1)) .4019" | </td>\n";4020}4021}40224023print"<td class=\"link\">";4024if($not_deleted) {4025print$cgi->a({-href => href(action=>"blob",4026 hash=>$diff->{'to_id'},4027 file_name=>$diff->{'to_file'},4028 hash_base=>$hash)},4029"blob");4030print" | "if($has_history);4031}4032if($has_history) {4033print$cgi->a({-href => href(action=>"history",4034 file_name=>$diff->{'to_file'},4035 hash_base=>$hash)},4036"history");4037}4038print"</td>\n";40394040print"</tr>\n";4041next;# instead of 'else' clause, to avoid extra indent4042}4043# else ordinary diff40444045my($to_mode_oct,$to_mode_str,$to_file_type);4046my($from_mode_oct,$from_mode_str,$from_file_type);4047if($diff->{'to_mode'}ne('0' x 6)) {4048$to_mode_oct=oct$diff->{'to_mode'};4049if(S_ISREG($to_mode_oct)) {# only for regular file4050$to_mode_str=sprintf("%04o",$to_mode_oct&0777);# permission bits4051}4052$to_file_type= file_type($diff->{'to_mode'});4053}4054if($diff->{'from_mode'}ne('0' x 6)) {4055$from_mode_oct=oct$diff->{'from_mode'};4056if(S_ISREG($to_mode_oct)) {# only for regular file4057$from_mode_str=sprintf("%04o",$from_mode_oct&0777);# permission bits4058}4059$from_file_type= file_type($diff->{'from_mode'});4060}40614062if($diff->{'status'}eq"A") {# created4063my$mode_chng="<span class=\"file_status new\">[new$to_file_type";4064$mode_chng.=" with mode:$to_mode_str"if$to_mode_str;4065$mode_chng.="]</span>";4066print"<td>";4067print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4068 hash_base=>$hash, file_name=>$diff->{'file'}),4069-class=>"list"}, esc_path($diff->{'file'}));4070print"</td>\n";4071print"<td>$mode_chng</td>\n";4072print"<td class=\"link\">";4073if($actioneq'commitdiff') {4074# link to patch4075$patchno++;4076print$cgi->a({-href =>"#patch$patchno"},"patch");4077print" | ";4078}4079print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4080 hash_base=>$hash, file_name=>$diff->{'file'})},4081"blob");4082print"</td>\n";40834084}elsif($diff->{'status'}eq"D") {# deleted4085my$mode_chng="<span class=\"file_status deleted\">[deleted$from_file_type]</span>";4086print"<td>";4087print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4088 hash_base=>$parent, file_name=>$diff->{'file'}),4089-class=>"list"}, esc_path($diff->{'file'}));4090print"</td>\n";4091print"<td>$mode_chng</td>\n";4092print"<td class=\"link\">";4093if($actioneq'commitdiff') {4094# link to patch4095$patchno++;4096print$cgi->a({-href =>"#patch$patchno"},"patch");4097print" | ";4098}4099print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4100 hash_base=>$parent, file_name=>$diff->{'file'})},4101"blob") ." | ";4102if($have_blame) {4103print$cgi->a({-href => href(action=>"blame", hash_base=>$parent,4104 file_name=>$diff->{'file'})},4105"blame") ." | ";4106}4107print$cgi->a({-href => href(action=>"history", hash_base=>$parent,4108 file_name=>$diff->{'file'})},4109"history");4110print"</td>\n";41114112}elsif($diff->{'status'}eq"M"||$diff->{'status'}eq"T") {# modified, or type changed4113my$mode_chnge="";4114if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4115$mode_chnge="<span class=\"file_status mode_chnge\">[changed";4116if($from_file_typene$to_file_type) {4117$mode_chnge.=" from$from_file_typeto$to_file_type";4118}4119if(($from_mode_oct&0777) != ($to_mode_oct&0777)) {4120if($from_mode_str&&$to_mode_str) {4121$mode_chnge.=" mode:$from_mode_str->$to_mode_str";4122}elsif($to_mode_str) {4123$mode_chnge.=" mode:$to_mode_str";4124}4125}4126$mode_chnge.="]</span>\n";4127}4128print"<td>";4129print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4130 hash_base=>$hash, file_name=>$diff->{'file'}),4131-class=>"list"}, esc_path($diff->{'file'}));4132print"</td>\n";4133print"<td>$mode_chnge</td>\n";4134print"<td class=\"link\">";4135if($actioneq'commitdiff') {4136# link to patch4137$patchno++;4138print$cgi->a({-href =>"#patch$patchno"},"patch") .4139" | ";4140}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4141# "commit" view and modified file (not onlu mode changed)4142print$cgi->a({-href => href(action=>"blobdiff",4143 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4144 hash_base=>$hash, hash_parent_base=>$parent,4145 file_name=>$diff->{'file'})},4146"diff") .4147" | ";4148}4149print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4150 hash_base=>$hash, file_name=>$diff->{'file'})},4151"blob") ." | ";4152if($have_blame) {4153print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4154 file_name=>$diff->{'file'})},4155"blame") ." | ";4156}4157print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4158 file_name=>$diff->{'file'})},4159"history");4160print"</td>\n";41614162}elsif($diff->{'status'}eq"R"||$diff->{'status'}eq"C") {# renamed or copied4163my%status_name= ('R'=>'moved','C'=>'copied');4164my$nstatus=$status_name{$diff->{'status'}};4165my$mode_chng="";4166if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4167# mode also for directories, so we cannot use $to_mode_str4168$mode_chng=sprintf(", mode:%04o",$to_mode_oct&0777);4169}4170print"<td>".4171$cgi->a({-href => href(action=>"blob", hash_base=>$hash,4172 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),4173-class=>"list"}, esc_path($diff->{'to_file'})) ."</td>\n".4174"<td><span class=\"file_status$nstatus\">[$nstatusfrom ".4175$cgi->a({-href => href(action=>"blob", hash_base=>$parent,4176 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),4177-class=>"list"}, esc_path($diff->{'from_file'})) .4178" with ". (int$diff->{'similarity'}) ."% similarity$mode_chng]</span></td>\n".4179"<td class=\"link\">";4180if($actioneq'commitdiff') {4181# link to patch4182$patchno++;4183print$cgi->a({-href =>"#patch$patchno"},"patch") .4184" | ";4185}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4186# "commit" view and modified file (not only pure rename or copy)4187print$cgi->a({-href => href(action=>"blobdiff",4188 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4189 hash_base=>$hash, hash_parent_base=>$parent,4190 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},4191"diff") .4192" | ";4193}4194print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4195 hash_base=>$parent, file_name=>$diff->{'to_file'})},4196"blob") ." | ";4197if($have_blame) {4198print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4199 file_name=>$diff->{'to_file'})},4200"blame") ." | ";4201}4202print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4203 file_name=>$diff->{'to_file'})},4204"history");4205print"</td>\n";42064207}# we should not encounter Unmerged (U) or Unknown (X) status4208print"</tr>\n";4209}4210print"</tbody>"if$has_header;4211print"</table>\n";4212}42134214sub git_patchset_body {4215my($fd,$difftree,$hash,@hash_parents) =@_;4216my($hash_parent) =$hash_parents[0];42174218my$is_combined= (@hash_parents>1);4219my$patch_idx=0;4220my$patch_number=0;4221my$patch_line;4222my$diffinfo;4223my$to_name;4224my(%from,%to);42254226print"<div class=\"patchset\">\n";42274228# skip to first patch4229while($patch_line= <$fd>) {4230chomp$patch_line;42314232last if($patch_line=~m/^diff /);4233}42344235 PATCH:4236while($patch_line) {42374238# parse "git diff" header line4239if($patch_line=~m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {4240# $1 is from_name, which we do not use4241$to_name= unquote($2);4242$to_name=~s!^b/!!;4243}elsif($patch_line=~m/^diff --(cc|combined) ("?.*"?)$/) {4244# $1 is 'cc' or 'combined', which we do not use4245$to_name= unquote($2);4246}else{4247$to_name=undef;4248}42494250# check if current patch belong to current raw line4251# and parse raw git-diff line if needed4252if(is_patch_split($diffinfo, {'to_file'=>$to_name})) {4253# this is continuation of a split patch4254print"<div class=\"patch cont\">\n";4255}else{4256# advance raw git-diff output if needed4257$patch_idx++ifdefined$diffinfo;42584259# read and prepare patch information4260$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);42614262# compact combined diff output can have some patches skipped4263# find which patch (using pathname of result) we are at now;4264if($is_combined) {4265while($to_namene$diffinfo->{'to_file'}) {4266print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4267 format_diff_cc_simplified($diffinfo,@hash_parents) .4268"</div>\n";# class="patch"42694270$patch_idx++;4271$patch_number++;42724273last if$patch_idx>$#$difftree;4274$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);4275}4276}42774278# modifies %from, %to hashes4279 parse_from_to_diffinfo($diffinfo, \%from, \%to,@hash_parents);42804281# this is first patch for raw difftree line with $patch_idx index4282# we index @$difftree array from 0, but number patches from 14283print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n";4284}42854286# git diff header4287#assert($patch_line =~ m/^diff /) if DEBUG;4288#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed4289$patch_number++;4290# print "git diff" header4291print format_git_diff_header_line($patch_line,$diffinfo,4292 \%from, \%to);42934294# print extended diff header4295print"<div class=\"diff extended_header\">\n";4296 EXTENDED_HEADER:4297while($patch_line= <$fd>) {4298chomp$patch_line;42994300last EXTENDED_HEADER if($patch_line=~m/^--- |^diff /);43014302print format_extended_diff_header_line($patch_line,$diffinfo,4303 \%from, \%to);4304}4305print"</div>\n";# class="diff extended_header"43064307# from-file/to-file diff header4308if(!$patch_line) {4309print"</div>\n";# class="patch"4310last PATCH;4311}4312next PATCH if($patch_line=~m/^diff /);4313#assert($patch_line =~ m/^---/) if DEBUG;43144315my$last_patch_line=$patch_line;4316$patch_line= <$fd>;4317chomp$patch_line;4318#assert($patch_line =~ m/^\+\+\+/) if DEBUG;43194320print format_diff_from_to_header($last_patch_line,$patch_line,4321$diffinfo, \%from, \%to,4322@hash_parents);43234324# the patch itself4325 LINE:4326while($patch_line= <$fd>) {4327chomp$patch_line;43284329next PATCH if($patch_line=~m/^diff /);43304331print format_diff_line($patch_line, \%from, \%to);4332}43334334}continue{4335print"</div>\n";# class="patch"4336}43374338# for compact combined (--cc) format, with chunk and patch simpliciaction4339# patchset might be empty, but there might be unprocessed raw lines4340for(++$patch_idxif$patch_number>0;4341$patch_idx<@$difftree;4342++$patch_idx) {4343# read and prepare patch information4344$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);43454346# generate anchor for "patch" links in difftree / whatchanged part4347print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4348 format_diff_cc_simplified($diffinfo,@hash_parents) .4349"</div>\n";# class="patch"43504351$patch_number++;4352}43534354if($patch_number==0) {4355if(@hash_parents>1) {4356print"<div class=\"diff nodifferences\">Trivial merge</div>\n";4357}else{4358print"<div class=\"diff nodifferences\">No differences found</div>\n";4359}4360}43614362print"</div>\n";# class="patchset"4363}43644365# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43664367# fills project list info (age, description, owner, forks) for each4368# project in the list, removing invalid projects from returned list4369# NOTE: modifies $projlist, but does not remove entries from it4370sub fill_project_list_info {4371my($projlist,$check_forks) =@_;4372my@projects;43734374my$show_ctags= gitweb_check_feature('ctags');4375 PROJECT:4376foreachmy$pr(@$projlist) {4377my(@activity) = git_get_last_activity($pr->{'path'});4378unless(@activity) {4379next PROJECT;4380}4381($pr->{'age'},$pr->{'age_string'}) =@activity;4382if(!defined$pr->{'descr'}) {4383my$descr= git_get_project_description($pr->{'path'}) ||"";4384$descr= to_utf8($descr);4385$pr->{'descr_long'} =$descr;4386$pr->{'descr'} = chop_str($descr,$projects_list_description_width,5);4387}4388if(!defined$pr->{'owner'}) {4389$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") ||"";4390}4391if($check_forks) {4392my$pname=$pr->{'path'};4393if(($pname=~s/\.git$//) &&4394($pname!~/\/$/) &&4395(-d "$projectroot/$pname")) {4396$pr->{'forks'} ="-d$projectroot/$pname";4397}else{4398$pr->{'forks'} =0;4399}4400}4401$show_ctagsand$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});4402push@projects,$pr;4403}44044405return@projects;4406}44074408# print 'sort by' <th> element, generating 'sort by $name' replay link4409# if that order is not selected4410sub print_sort_th {4411print format_sort_th(@_);4412}44134414sub format_sort_th {4415my($name,$order,$header) =@_;4416my$sort_th="";4417$header||=ucfirst($name);44184419if($ordereq$name) {4420$sort_th.="<th>$header</th>\n";4421}else{4422$sort_th.="<th>".4423$cgi->a({-href => href(-replay=>1, order=>$name),4424-class=>"header"},$header) .4425"</th>\n";4426}44274428return$sort_th;4429}44304431sub git_project_list_body {4432# actually uses global variable $project4433my($projlist,$order,$from,$to,$extra,$no_header) =@_;44344435my$check_forks= gitweb_check_feature('forks');4436my@projects= fill_project_list_info($projlist,$check_forks);44374438$order||=$default_projects_order;4439$from=0unlessdefined$from;4440$to=$#projectsif(!defined$to||$#projects<$to);44414442my%order_info= (4443 project => { key =>'path', type =>'str'},4444 descr => { key =>'descr_long', type =>'str'},4445 owner => { key =>'owner', type =>'str'},4446 age => { key =>'age', type =>'num'}4447);4448my$oi=$order_info{$order};4449if($oi->{'type'}eq'str') {4450@projects=sort{$a->{$oi->{'key'}}cmp$b->{$oi->{'key'}}}@projects;4451}else{4452@projects=sort{$a->{$oi->{'key'}} <=>$b->{$oi->{'key'}}}@projects;4453}44544455my$show_ctags= gitweb_check_feature('ctags');4456if($show_ctags) {4457my%ctags;4458foreachmy$p(@projects) {4459foreachmy$ct(keys%{$p->{'ctags'}}) {4460$ctags{$ct} +=$p->{'ctags'}->{$ct};4461}4462}4463my$cloud= git_populate_project_tagcloud(\%ctags);4464print git_show_project_tagcloud($cloud,64);4465}44664467print"<table class=\"project_list\">\n";4468unless($no_header) {4469print"<tr>\n";4470if($check_forks) {4471print"<th></th>\n";4472}4473 print_sort_th('project',$order,'Project');4474 print_sort_th('descr',$order,'Description');4475 print_sort_th('owner',$order,'Owner');4476 print_sort_th('age',$order,'Last Change');4477print"<th></th>\n".# for links4478"</tr>\n";4479}4480my$alternate=1;4481my$tagfilter=$cgi->param('by_tag');4482for(my$i=$from;$i<=$to;$i++) {4483my$pr=$projects[$i];44844485next if$tagfilterand$show_ctagsand not grep{lc$_eq lc$tagfilter}keys%{$pr->{'ctags'}};4486next if$searchtextand not$pr->{'path'} =~/$searchtext/4487and not$pr->{'descr_long'} =~/$searchtext/;4488# Weed out forks or non-matching entries of search4489if($check_forks) {4490my$forkbase=$project;$forkbase||='';$forkbase=~ s#\.git$#/#;4491$forkbase="^$forkbase"if$forkbase;4492next ifnot$searchtextand not$tagfilterand$show_ctags4493and$pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe4494}44954496if($alternate) {4497print"<tr class=\"dark\">\n";4498}else{4499print"<tr class=\"light\">\n";4500}4501$alternate^=1;4502if($check_forks) {4503print"<td>";4504if($pr->{'forks'}) {4505print"<!--$pr->{'forks'} -->\n";4506print$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"+");4507}4508print"</td>\n";4509}4510print"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4511-class=>"list"}, esc_html($pr->{'path'})) ."</td>\n".4512"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4513-class=>"list", -title =>$pr->{'descr_long'}},4514 esc_html($pr->{'descr'})) ."</td>\n".4515"<td><i>". chop_and_escape_str($pr->{'owner'},15) ."</i></td>\n";4516print"<td class=\"". age_class($pr->{'age'}) ."\">".4517(defined$pr->{'age_string'} ?$pr->{'age_string'} :"No commits") ."</td>\n".4518"<td class=\"link\">".4519$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")},"summary") ." | ".4520$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")},"shortlog") ." | ".4521$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")},"log") ." | ".4522$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")},"tree") .4523($pr->{'forks'} ?" | ".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"forks") :'') .4524"</td>\n".4525"</tr>\n";4526}4527if(defined$extra) {4528print"<tr>\n";4529if($check_forks) {4530print"<td></td>\n";4531}4532print"<td colspan=\"5\">$extra</td>\n".4533"</tr>\n";4534}4535print"</table>\n";4536}45374538sub git_log_body {4539# uses global variable $project4540my($commitlist,$from,$to,$refs,$extra) =@_;45414542$from=0unlessdefined$from;4543$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);45444545for(my$i=0;$i<=$to;$i++) {4546my%co= %{$commitlist->[$i]};4547next if!%co;4548my$commit=$co{'id'};4549my$ref= format_ref_marker($refs,$commit);4550my%ad= parse_date($co{'author_epoch'});4551 git_print_header_div('commit',4552"<span class=\"age\">$co{'age_string'}</span>".4553 esc_html($co{'title'}) .$ref,4554$commit);4555print"<div class=\"title_text\">\n".4556"<div class=\"log_link\">\n".4557$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") .4558" | ".4559$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") .4560" | ".4561$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree") .4562"<br/>\n".4563"</div>\n";4564 git_print_authorship(\%co, -tag =>'span');4565print"<br/>\n</div>\n";45664567print"<div class=\"log_body\">\n";4568 git_print_log($co{'comment'}, -final_empty_line=>1);4569print"</div>\n";4570}4571if($extra) {4572print"<div class=\"page_nav\">\n";4573print"$extra\n";4574print"</div>\n";4575}4576}45774578sub git_shortlog_body {4579# uses global variable $project4580my($commitlist,$from,$to,$refs,$extra) =@_;45814582$from=0unlessdefined$from;4583$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);45844585print"<table class=\"shortlog\">\n";4586my$alternate=1;4587for(my$i=$from;$i<=$to;$i++) {4588my%co= %{$commitlist->[$i]};4589my$commit=$co{'id'};4590my$ref= format_ref_marker($refs,$commit);4591if($alternate) {4592print"<tr class=\"dark\">\n";4593}else{4594print"<tr class=\"light\">\n";4595}4596$alternate^=1;4597# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .4598print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4599 format_author_html('td', \%co,10) ."<td>";4600print format_subject_html($co{'title'},$co{'title_short'},4601 href(action=>"commit", hash=>$commit),$ref);4602print"</td>\n".4603"<td class=\"link\">".4604$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") ." | ".4605$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") ." | ".4606$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree");4607my$snapshot_links= format_snapshot_links($commit);4608if(defined$snapshot_links) {4609print" | ".$snapshot_links;4610}4611print"</td>\n".4612"</tr>\n";4613}4614if(defined$extra) {4615print"<tr>\n".4616"<td colspan=\"4\">$extra</td>\n".4617"</tr>\n";4618}4619print"</table>\n";4620}46214622sub git_history_body {4623# Warning: assumes constant type (blob or tree) during history4624my($commitlist,$from,$to,$refs,$extra,4625$file_name,$file_hash,$ftype) =@_;46264627$from=0unlessdefined$from;4628$to=$#{$commitlist}unless(defined$to&&$to<=$#{$commitlist});46294630print"<table class=\"history\">\n";4631my$alternate=1;4632for(my$i=$from;$i<=$to;$i++) {4633my%co= %{$commitlist->[$i]};4634if(!%co) {4635next;4636}4637my$commit=$co{'id'};46384639my$ref= format_ref_marker($refs,$commit);46404641if($alternate) {4642print"<tr class=\"dark\">\n";4643}else{4644print"<tr class=\"light\">\n";4645}4646$alternate^=1;4647print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4648# shortlog: format_author_html('td', \%co, 10)4649 format_author_html('td', \%co,15,3) ."<td>";4650# originally git_history used chop_str($co{'title'}, 50)4651print format_subject_html($co{'title'},$co{'title_short'},4652 href(action=>"commit", hash=>$commit),$ref);4653print"</td>\n".4654"<td class=\"link\">".4655$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)},$ftype) ." | ".4656$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff");46574658if($ftypeeq'blob') {4659my$blob_current=$file_hash;4660my$blob_parent= git_get_hash_by_path($commit,$file_name);4661if(defined$blob_current&&defined$blob_parent&&4662$blob_currentne$blob_parent) {4663print" | ".4664$cgi->a({-href => href(action=>"blobdiff",4665 hash=>$blob_current, hash_parent=>$blob_parent,4666 hash_base=>$hash_base, hash_parent_base=>$commit,4667 file_name=>$file_name)},4668"diff to current");4669}4670}4671print"</td>\n".4672"</tr>\n";4673}4674if(defined$extra) {4675print"<tr>\n".4676"<td colspan=\"4\">$extra</td>\n".4677"</tr>\n";4678}4679print"</table>\n";4680}46814682sub git_tags_body {4683# uses global variable $project4684my($taglist,$from,$to,$extra) =@_;4685$from=0unlessdefined$from;4686$to=$#{$taglist}if(!defined$to||$#{$taglist} <$to);46874688print"<table class=\"tags\">\n";4689my$alternate=1;4690for(my$i=$from;$i<=$to;$i++) {4691my$entry=$taglist->[$i];4692my%tag=%$entry;4693my$comment=$tag{'subject'};4694my$comment_short;4695if(defined$comment) {4696$comment_short= chop_str($comment,30,5);4697}4698if($alternate) {4699print"<tr class=\"dark\">\n";4700}else{4701print"<tr class=\"light\">\n";4702}4703$alternate^=1;4704if(defined$tag{'age'}) {4705print"<td><i>$tag{'age'}</i></td>\n";4706}else{4707print"<td></td>\n";4708}4709print"<td>".4710$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),4711-class=>"list name"}, esc_html($tag{'name'})) .4712"</td>\n".4713"<td>";4714if(defined$comment) {4715print format_subject_html($comment,$comment_short,4716 href(action=>"tag", hash=>$tag{'id'}));4717}4718print"</td>\n".4719"<td class=\"selflink\">";4720if($tag{'type'}eq"tag") {4721print$cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})},"tag");4722}else{4723print" ";4724}4725print"</td>\n".4726"<td class=\"link\">"." | ".4727$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})},$tag{'reftype'});4728if($tag{'reftype'}eq"commit") {4729print" | ".$cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})},"shortlog") .4730" | ".$cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})},"log");4731}elsif($tag{'reftype'}eq"blob") {4732print" | ".$cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})},"raw");4733}4734print"</td>\n".4735"</tr>";4736}4737if(defined$extra) {4738print"<tr>\n".4739"<td colspan=\"5\">$extra</td>\n".4740"</tr>\n";4741}4742print"</table>\n";4743}47444745sub git_heads_body {4746# uses global variable $project4747my($headlist,$head,$from,$to,$extra) =@_;4748$from=0unlessdefined$from;4749$to=$#{$headlist}if(!defined$to||$#{$headlist} <$to);47504751print"<table class=\"heads\">\n";4752my$alternate=1;4753for(my$i=$from;$i<=$to;$i++) {4754my$entry=$headlist->[$i];4755my%ref=%$entry;4756my$curr=$ref{'id'}eq$head;4757if($alternate) {4758print"<tr class=\"dark\">\n";4759}else{4760print"<tr class=\"light\">\n";4761}4762$alternate^=1;4763print"<td><i>$ref{'age'}</i></td>\n".4764($curr?"<td class=\"current_head\">":"<td>") .4765$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),4766-class=>"list name"},esc_html($ref{'name'})) .4767"</td>\n".4768"<td class=\"link\">".4769$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})},"shortlog") ." | ".4770$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})},"log") ." | ".4771$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})},"tree") .4772"</td>\n".4773"</tr>";4774}4775if(defined$extra) {4776print"<tr>\n".4777"<td colspan=\"3\">$extra</td>\n".4778"</tr>\n";4779}4780print"</table>\n";4781}47824783sub git_search_grep_body {4784my($commitlist,$from,$to,$extra) =@_;4785$from=0unlessdefined$from;4786$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);47874788print"<table class=\"commit_search\">\n";4789my$alternate=1;4790for(my$i=$from;$i<=$to;$i++) {4791my%co= %{$commitlist->[$i]};4792if(!%co) {4793next;4794}4795my$commit=$co{'id'};4796if($alternate) {4797print"<tr class=\"dark\">\n";4798}else{4799print"<tr class=\"light\">\n";4800}4801$alternate^=1;4802print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4803 format_author_html('td', \%co,15,5) .4804"<td>".4805$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),4806-class=>"list subject"},4807 chop_and_escape_str($co{'title'},50) ."<br/>");4808my$comment=$co{'comment'};4809foreachmy$line(@$comment) {4810if($line=~m/^(.*?)($search_regexp)(.*)$/i) {4811my($lead,$match,$trail) = ($1,$2,$3);4812$match= chop_str($match,70,5,'center');4813my$contextlen=int((80-length($match))/2);4814$contextlen=30if($contextlen>30);4815$lead= chop_str($lead,$contextlen,10,'left');4816$trail= chop_str($trail,$contextlen,10,'right');48174818$lead= esc_html($lead);4819$match= esc_html($match);4820$trail= esc_html($trail);48214822print"$lead<span class=\"match\">$match</span>$trail<br />";4823}4824}4825print"</td>\n".4826"<td class=\"link\">".4827$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .4828" | ".4829$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})},"commitdiff") .4830" | ".4831$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");4832print"</td>\n".4833"</tr>\n";4834}4835if(defined$extra) {4836print"<tr>\n".4837"<td colspan=\"3\">$extra</td>\n".4838"</tr>\n";4839}4840print"</table>\n";4841}48424843## ======================================================================4844## ======================================================================4845## actions48464847sub git_project_list {4848my$order=$input_params{'order'};4849if(defined$order&&$order!~m/none|project|descr|owner|age/) {4850 die_error(400,"Unknown order parameter");4851}48524853my@list= git_get_projects_list();4854if(!@list) {4855 die_error(404,"No projects found");4856}48574858 git_header_html();4859if(defined$home_text&& -f $home_text) {4860print"<div class=\"index_include\">\n";4861 insert_file($home_text);4862print"</div>\n";4863}4864print$cgi->startform(-method=>"get") .4865"<p class=\"projsearch\">Search:\n".4866$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".4867"</p>".4868$cgi->end_form() ."\n";4869 git_project_list_body(\@list,$order);4870 git_footer_html();4871}48724873sub git_forks {4874my$order=$input_params{'order'};4875if(defined$order&&$order!~m/none|project|descr|owner|age/) {4876 die_error(400,"Unknown order parameter");4877}48784879my@list= git_get_projects_list($project);4880if(!@list) {4881 die_error(404,"No forks found");4882}48834884 git_header_html();4885 git_print_page_nav('','');4886 git_print_header_div('summary',"$projectforks");4887 git_project_list_body(\@list,$order);4888 git_footer_html();4889}48904891sub git_project_index {4892my@projects= git_get_projects_list($project);48934894print$cgi->header(4895-type =>'text/plain',4896-charset =>'utf-8',4897-content_disposition =>'inline; filename="index.aux"');48984899foreachmy$pr(@projects) {4900if(!exists$pr->{'owner'}) {4901$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");4902}49034904my($path,$owner) = ($pr->{'path'},$pr->{'owner'});4905# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '4906$path=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4907$owner=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4908$path=~s/ /\+/g;4909$owner=~s/ /\+/g;49104911print"$path$owner\n";4912}4913}49144915sub git_summary {4916my$descr= git_get_project_description($project) ||"none";4917my%co= parse_commit("HEAD");4918my%cd=%co? parse_date($co{'committer_epoch'},$co{'committer_tz'}) : ();4919my$head=$co{'id'};49204921my$owner= git_get_project_owner($project);49224923my$refs= git_get_references();4924# These get_*_list functions return one more to allow us to see if4925# there are more ...4926my@taglist= git_get_tags_list(16);4927my@headlist= git_get_heads_list(16);4928my@forklist;4929my$check_forks= gitweb_check_feature('forks');49304931if($check_forks) {4932@forklist= git_get_projects_list($project);4933}49344935 git_header_html();4936 git_print_page_nav('summary','',$head);49374938print"<div class=\"title\"> </div>\n";4939print"<table class=\"projects_list\">\n".4940"<tr id=\"metadata_desc\"><td>description</td><td>". esc_html($descr) ."</td></tr>\n".4941"<tr id=\"metadata_owner\"><td>owner</td><td>". esc_html($owner) ."</td></tr>\n";4942if(defined$cd{'rfc2822'}) {4943print"<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";4944}49454946# use per project git URL list in $projectroot/$project/cloneurl4947# or make project git URL from git base URL and project name4948my$url_tag="URL";4949my@url_list= git_get_project_url_list($project);4950@url_list=map{"$_/$project"}@git_base_url_listunless@url_list;4951foreachmy$git_url(@url_list) {4952next unless$git_url;4953print"<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";4954$url_tag="";4955}49564957# Tag cloud4958my$show_ctags= gitweb_check_feature('ctags');4959if($show_ctags) {4960my$ctags= git_get_project_ctags($project);4961my$cloud= git_populate_project_tagcloud($ctags);4962print"<tr id=\"metadata_ctags\"><td>Content tags:<br />";4963print"</td>\n<td>"unless%$ctags;4964print"<form action=\"$show_ctags\"method=\"post\"><input type=\"hidden\"name=\"p\"value=\"$project\"/>Add: <input type=\"text\"name=\"t\"size=\"8\"/></form>";4965print"</td>\n<td>"if%$ctags;4966print git_show_project_tagcloud($cloud,48);4967print"</td></tr>";4968}49694970print"</table>\n";49714972# If XSS prevention is on, we don't include README.html.4973# TODO: Allow a readme in some safe format.4974if(!$prevent_xss&& -s "$projectroot/$project/README.html") {4975print"<div class=\"title\">readme</div>\n".4976"<div class=\"readme\">\n";4977 insert_file("$projectroot/$project/README.html");4978print"\n</div>\n";# class="readme"4979}49804981# we need to request one more than 16 (0..15) to check if4982# those 16 are all4983my@commitlist=$head? parse_commits($head,17) : ();4984if(@commitlist) {4985 git_print_header_div('shortlog');4986 git_shortlog_body(\@commitlist,0,15,$refs,4987$#commitlist<=15?undef:4988$cgi->a({-href => href(action=>"shortlog")},"..."));4989}49904991if(@taglist) {4992 git_print_header_div('tags');4993 git_tags_body(\@taglist,0,15,4994$#taglist<=15?undef:4995$cgi->a({-href => href(action=>"tags")},"..."));4996}49974998if(@headlist) {4999 git_print_header_div('heads');5000 git_heads_body(\@headlist,$head,0,15,5001$#headlist<=15?undef:5002$cgi->a({-href => href(action=>"heads")},"..."));5003}50045005if(@forklist) {5006 git_print_header_div('forks');5007 git_project_list_body(\@forklist,'age',0,15,5008$#forklist<=15?undef:5009$cgi->a({-href => href(action=>"forks")},"..."),5010'no_header');5011}50125013 git_footer_html();5014}50155016sub git_tag {5017my$head= git_get_head_hash($project);5018 git_header_html();5019 git_print_page_nav('','',$head,undef,$head);5020my%tag= parse_tag($hash);50215022if(!%tag) {5023 die_error(404,"Unknown tag object");5024}50255026 git_print_header_div('commit', esc_html($tag{'name'}),$hash);5027print"<div class=\"title_text\">\n".5028"<table class=\"object_header\">\n".5029"<tr>\n".5030"<td>object</td>\n".5031"<td>".$cgi->a({-class=>"list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5032$tag{'object'}) ."</td>\n".5033"<td class=\"link\">".$cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5034$tag{'type'}) ."</td>\n".5035"</tr>\n";5036if(defined($tag{'author'})) {5037 git_print_authorship_rows(\%tag,'author');5038}5039print"</table>\n\n".5040"</div>\n";5041print"<div class=\"page_body\">";5042my$comment=$tag{'comment'};5043foreachmy$line(@$comment) {5044chomp$line;5045print esc_html($line, -nbsp=>1) ."<br/>\n";5046}5047print"</div>\n";5048 git_footer_html();5049}50505051sub git_blame_common {5052my$format=shift||'porcelain';5053if($formateq'porcelain'&&$cgi->param('js')) {5054$format='incremental';5055$action='blame_incremental';# for page title etc5056}50575058# permissions5059 gitweb_check_feature('blame')5060or die_error(403,"Blame view not allowed");50615062# error checking5063 die_error(400,"No file name given")unless$file_name;5064$hash_base||= git_get_head_hash($project);5065 die_error(404,"Couldn't find base commit")unless$hash_base;5066my%co= parse_commit($hash_base)5067or die_error(404,"Commit not found");5068my$ftype="blob";5069if(!defined$hash) {5070$hash= git_get_hash_by_path($hash_base,$file_name,"blob")5071or die_error(404,"Error looking up file");5072}else{5073$ftype= git_get_type($hash);5074if($ftype!~"blob") {5075 die_error(400,"Object is not a blob");5076}5077}50785079my$fd;5080if($formateq'incremental') {5081# get file contents (as base)5082open$fd,"-|", git_cmd(),'cat-file','blob',$hash5083or die_error(500,"Open git-cat-file failed");5084}elsif($formateq'data') {5085# run git-blame --incremental5086open$fd,"-|", git_cmd(),"blame","--incremental",5087$hash_base,"--",$file_name5088or die_error(500,"Open git-blame --incremental failed");5089}else{5090# run git-blame --porcelain5091open$fd,"-|", git_cmd(),"blame",'-p',5092$hash_base,'--',$file_name5093or die_error(500,"Open git-blame --porcelain failed");5094}50955096# incremental blame data returns early5097if($formateq'data') {5098print$cgi->header(5099-type=>"text/plain", -charset =>"utf-8",5100-status=>"200 OK");5101local$| =1;# output autoflush5102printwhile<$fd>;5103close$fd5104or print"ERROR$!\n";51055106print'END';5107if(defined$t0&& gitweb_check_feature('timed')) {5108print' '.5109 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).5110' '.$number_of_git_cmds;5111}5112print"\n";51135114return;5115}51165117# page header5118 git_header_html();5119my$formats_nav=5120$cgi->a({-href => href(action=>"blob", -replay=>1)},5121"blob") .5122" | ";5123if($formateq'incremental') {5124$formats_nav.=5125$cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},5126"blame") ." (non-incremental)";5127}else{5128$formats_nav.=5129$cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},5130"blame") ." (incremental)";5131}5132$formats_nav.=5133" | ".5134$cgi->a({-href => href(action=>"history", -replay=>1)},5135"history") .5136" | ".5137$cgi->a({-href => href(action=>$action, file_name=>$file_name)},5138"HEAD");5139 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5140 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5141 git_print_page_path($file_name,$ftype,$hash_base);51425143# page body5144if($formateq'incremental') {5145print"<noscript>\n<div class=\"error\"><center><b>\n".5146"This page requires JavaScript to run.\nUse ".5147$cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},5148'this page').5149" instead.\n".5150"</b></center></div>\n</noscript>\n";51515152print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;5153}51545155print qq!<div class="page_body">\n!;5156print qq!<div id="progress_info">.../ ...</div>\n!5157if($formateq'incremental');5158print qq!<table id="blame_table"class="blame" width="100%">\n!.5159#qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.5160 qq!<thead>\n!.5161 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.5162 qq!</thead>\n!.5163 qq!<tbody>\n!;51645165my@rev_color=qw(light dark);5166my$num_colors=scalar(@rev_color);5167my$current_color=0;51685169if($formateq'incremental') {5170my$color_class=$rev_color[$current_color];51715172#contents of a file5173my$linenr=0;5174 LINE:5175while(my$line= <$fd>) {5176chomp$line;5177$linenr++;51785179print qq!<tr id="l$linenr"class="$color_class">!.5180 qq!<td class="sha1"><a href=""> </a></td>!.5181 qq!<td class="linenr">!.5182 qq!<a class="linenr" href="">$linenr</a></td>!;5183print qq!<td class="pre">! . esc_html($line) ."</td>\n";5184print qq!</tr>\n!;5185}51865187}else{# porcelain, i.e. ordinary blame5188my%metainfo= ();# saves information about commits51895190# blame data5191 LINE:5192while(my$line= <$fd>) {5193chomp$line;5194# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]5195# no <lines in group> for subsequent lines in group of lines5196my($full_rev,$orig_lineno,$lineno,$group_size) =5197($line=~/^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);5198if(!exists$metainfo{$full_rev}) {5199$metainfo{$full_rev} = {'nprevious'=>0};5200}5201my$meta=$metainfo{$full_rev};5202my$data;5203while($data= <$fd>) {5204chomp$data;5205last if($data=~s/^\t//);# contents of line5206if($data=~/^(\S+)(?: (.*))?$/) {5207$meta->{$1} =$2unlessexists$meta->{$1};5208}5209if($data=~/^previous /) {5210$meta->{'nprevious'}++;5211}5212}5213my$short_rev=substr($full_rev,0,8);5214my$author=$meta->{'author'};5215my%date=5216 parse_date($meta->{'author-time'},$meta->{'author-tz'});5217my$date=$date{'iso-tz'};5218if($group_size) {5219$current_color= ($current_color+1) %$num_colors;5220}5221my$tr_class=$rev_color[$current_color];5222$tr_class.=' boundary'if(exists$meta->{'boundary'});5223$tr_class.=' no-previous'if($meta->{'nprevious'} ==0);5224$tr_class.=' multiple-previous'if($meta->{'nprevious'} >1);5225print"<tr id=\"l$lineno\"class=\"$tr_class\">\n";5226if($group_size) {5227print"<td class=\"sha1\"";5228print" title=\"". esc_html($author) .",$date\"";5229print" rowspan=\"$group_size\""if($group_size>1);5230print">";5231print$cgi->a({-href => href(action=>"commit",5232 hash=>$full_rev,5233 file_name=>$file_name)},5234 esc_html($short_rev));5235if($group_size>=2) {5236my@author_initials= ($author=~/\b([[:upper:]])\B/g);5237if(@author_initials) {5238print"<br />".5239 esc_html(join('',@author_initials));5240# or join('.', ...)5241}5242}5243print"</td>\n";5244}5245# 'previous' <sha1 of parent commit> <filename at commit>5246if(exists$meta->{'previous'} &&5247$meta->{'previous'} =~/^([a-fA-F0-9]{40}) (.*)$/) {5248$meta->{'parent'} =$1;5249$meta->{'file_parent'} = unquote($2);5250}5251my$linenr_commit=5252exists($meta->{'parent'}) ?5253$meta->{'parent'} :$full_rev;5254my$linenr_filename=5255exists($meta->{'file_parent'}) ?5256$meta->{'file_parent'} : unquote($meta->{'filename'});5257my$blamed= href(action =>'blame',5258 file_name =>$linenr_filename,5259 hash_base =>$linenr_commit);5260print"<td class=\"linenr\">";5261print$cgi->a({ -href =>"$blamed#l$orig_lineno",5262-class=>"linenr"},5263 esc_html($lineno));5264print"</td>";5265print"<td class=\"pre\">". esc_html($data) ."</td>\n";5266print"</tr>\n";5267}# end while52685269}52705271# footer5272print"</tbody>\n".5273"</table>\n";# class="blame"5274print"</div>\n";# class="blame_body"5275close$fd5276or print"Reading blob failed\n";52775278 git_footer_html();5279}52805281sub git_blame {5282 git_blame_common();5283}52845285sub git_blame_incremental {5286 git_blame_common('incremental');5287}52885289sub git_blame_data {5290 git_blame_common('data');5291}52925293sub git_tags {5294my$head= git_get_head_hash($project);5295 git_header_html();5296 git_print_page_nav('','',$head,undef,$head);5297 git_print_header_div('summary',$project);52985299my@tagslist= git_get_tags_list();5300if(@tagslist) {5301 git_tags_body(\@tagslist);5302}5303 git_footer_html();5304}53055306sub git_heads {5307my$head= git_get_head_hash($project);5308 git_header_html();5309 git_print_page_nav('','',$head,undef,$head);5310 git_print_header_div('summary',$project);53115312my@headslist= git_get_heads_list();5313if(@headslist) {5314 git_heads_body(\@headslist,$head);5315}5316 git_footer_html();5317}53185319sub git_blob_plain {5320my$type=shift;5321my$expires;53225323if(!defined$hash) {5324if(defined$file_name) {5325my$base=$hash_base|| git_get_head_hash($project);5326$hash= git_get_hash_by_path($base,$file_name,"blob")5327or die_error(404,"Cannot find file");5328}else{5329 die_error(400,"No file name defined");5330}5331}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5332# blobs defined by non-textual hash id's can be cached5333$expires="+1d";5334}53355336open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5337or die_error(500,"Open git-cat-file blob '$hash' failed");53385339# content-type (can include charset)5340$type= blob_contenttype($fd,$file_name,$type);53415342# "save as" filename, even when no $file_name is given5343my$save_as="$hash";5344if(defined$file_name) {5345$save_as=$file_name;5346}elsif($type=~m/^text\//) {5347$save_as.='.txt';5348}53495350# With XSS prevention on, blobs of all types except a few known safe5351# ones are served with "Content-Disposition: attachment" to make sure5352# they don't run in our security domain. For certain image types,5353# blob view writes an <img> tag referring to blob_plain view, and we5354# want to be sure not to break that by serving the image as an5355# attachment (though Firefox 3 doesn't seem to care).5356my$sandbox=$prevent_xss&&5357$type!~m!^(?:text/plain|image/(?:gif|png|jpeg))$!;53585359print$cgi->header(5360-type =>$type,5361-expires =>$expires,5362-content_disposition =>5363($sandbox?'attachment':'inline')5364.'; filename="'.$save_as.'"');5365local$/=undef;5366binmode STDOUT,':raw';5367print<$fd>;5368binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5369close$fd;5370}53715372sub git_blob {5373my$expires;53745375if(!defined$hash) {5376if(defined$file_name) {5377my$base=$hash_base|| git_get_head_hash($project);5378$hash= git_get_hash_by_path($base,$file_name,"blob")5379or die_error(404,"Cannot find file");5380}else{5381 die_error(400,"No file name defined");5382}5383}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5384# blobs defined by non-textual hash id's can be cached5385$expires="+1d";5386}53875388my$have_blame= gitweb_check_feature('blame');5389open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5390or die_error(500,"Couldn't cat$file_name,$hash");5391my$mimetype= blob_mimetype($fd,$file_name);5392# use 'blob_plain' (aka 'raw') view for files that cannot be displayed5393if($mimetype!~m!^(?:text/|image/(?:gif|png|jpeg)$)!&& -B $fd) {5394close$fd;5395return git_blob_plain($mimetype);5396}5397# we can have blame only for text/* mimetype5398$have_blame&&= ($mimetype=~m!^text/!);53995400my$have_highlight= gitweb_check_feature('highlight');5401my$syntax;5402if($have_highlight&&defined($file_name)) {5403my$basename= basename($file_name,'.in');5404foreachmy$regexp(keys%highlight_type) {5405if($basename=~/$regexp/) {5406$syntax=$highlight_type{$regexp};5407last;5408}5409}54105411if($syntax) {5412close$fd;5413open$fd, quote_command(git_cmd(),"cat-file","blob",$hash)." | ".5414"highlight --xhtml --fragment -t 8 --syntax$syntax|"5415or die_error(500,"Couldn't open file or run syntax highlighter");5416}5417}54185419 git_header_html(undef,$expires);5420my$formats_nav='';5421if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5422if(defined$file_name) {5423if($have_blame) {5424$formats_nav.=5425$cgi->a({-href => href(action=>"blame", -replay=>1)},5426"blame") .5427" | ";5428}5429$formats_nav.=5430$cgi->a({-href => href(action=>"history", -replay=>1)},5431"history") .5432" | ".5433$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5434"raw") .5435" | ".5436$cgi->a({-href => href(action=>"blob",5437 hash_base=>"HEAD", file_name=>$file_name)},5438"HEAD");5439}else{5440$formats_nav.=5441$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5442"raw");5443}5444 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5445 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5446}else{5447print"<div class=\"page_nav\">\n".5448"<br/><br/></div>\n".5449"<div class=\"title\">$hash</div>\n";5450}5451 git_print_page_path($file_name,"blob",$hash_base);5452print"<div class=\"page_body\">\n";5453if($mimetype=~m!^image/!) {5454print qq!<img type="$mimetype"!;5455if($file_name) {5456print qq! alt="$file_name" title="$file_name"!;5457}5458print qq! src="! .5459 href(action=>"blob_plain", hash=>$hash,5460 hash_base=>$hash_base, file_name=>$file_name) .5461 qq!"/>\n!;5462}else{5463my$nr;5464while(my$line= <$fd>) {5465chomp$line;5466$nr++;5467$line= untabify($line);5468printf"<div class=\"pre\"><a id=\"l%i\"href=\"". href(-replay =>1)5469."#l%i\"class=\"linenr\">%4i</a>%s</div>\n",5470$nr,$nr,$nr,$syntax?$line: esc_html($line, -nbsp=>1);5471}5472}5473close$fd5474or print"Reading blob failed.\n";5475print"</div>";5476 git_footer_html();5477}54785479sub git_tree {5480if(!defined$hash_base) {5481$hash_base="HEAD";5482}5483if(!defined$hash) {5484if(defined$file_name) {5485$hash= git_get_hash_by_path($hash_base,$file_name,"tree");5486}else{5487$hash=$hash_base;5488}5489}5490 die_error(404,"No such tree")unlessdefined($hash);54915492my$show_sizes= gitweb_check_feature('show-sizes');5493my$have_blame= gitweb_check_feature('blame');54945495my@entries= ();5496{5497local$/="\0";5498open my$fd,"-|", git_cmd(),"ls-tree",'-z',5499($show_sizes?'-l': ()),@extra_options,$hash5500or die_error(500,"Open git-ls-tree failed");5501@entries=map{chomp;$_} <$fd>;5502close$fd5503or die_error(404,"Reading tree failed");5504}55055506my$refs= git_get_references();5507my$ref= format_ref_marker($refs,$hash_base);5508 git_header_html();5509my$basedir='';5510if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5511my@views_nav= ();5512if(defined$file_name) {5513push@views_nav,5514$cgi->a({-href => href(action=>"history", -replay=>1)},5515"history"),5516$cgi->a({-href => href(action=>"tree",5517 hash_base=>"HEAD", file_name=>$file_name)},5518"HEAD"),5519}5520my$snapshot_links= format_snapshot_links($hash);5521if(defined$snapshot_links) {5522# FIXME: Should be available when we have no hash base as well.5523push@views_nav,$snapshot_links;5524}5525 git_print_page_nav('tree','',$hash_base,undef,undef,5526join(' | ',@views_nav));5527 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash_base);5528}else{5529undef$hash_base;5530print"<div class=\"page_nav\">\n";5531print"<br/><br/></div>\n";5532print"<div class=\"title\">$hash</div>\n";5533}5534if(defined$file_name) {5535$basedir=$file_name;5536if($basedirne''&&substr($basedir, -1)ne'/') {5537$basedir.='/';5538}5539 git_print_page_path($file_name,'tree',$hash_base);5540}5541print"<div class=\"page_body\">\n";5542print"<table class=\"tree\">\n";5543my$alternate=1;5544# '..' (top directory) link if possible5545if(defined$hash_base&&5546defined$file_name&&$file_name=~m![^/]+$!) {5547if($alternate) {5548print"<tr class=\"dark\">\n";5549}else{5550print"<tr class=\"light\">\n";5551}5552$alternate^=1;55535554my$up=$file_name;5555$up=~s!/?[^/]+$!!;5556undef$upunless$up;5557# based on git_print_tree_entry5558print'<td class="mode">'. mode_str('040000') ."</td>\n";5559print'<td class="size"> </td>'."\n"if$show_sizes;5560print'<td class="list">';5561print$cgi->a({-href => href(action=>"tree",5562 hash_base=>$hash_base,5563 file_name=>$up)},5564"..");5565print"</td>\n";5566print"<td class=\"link\"></td>\n";55675568print"</tr>\n";5569}5570foreachmy$line(@entries) {5571my%t= parse_ls_tree_line($line, -z =>1, -l =>$show_sizes);55725573if($alternate) {5574print"<tr class=\"dark\">\n";5575}else{5576print"<tr class=\"light\">\n";5577}5578$alternate^=1;55795580 git_print_tree_entry(\%t,$basedir,$hash_base,$have_blame);55815582print"</tr>\n";5583}5584print"</table>\n".5585"</div>";5586 git_footer_html();5587}55885589sub snapshot_name {5590my($project,$hash) =@_;55915592# path/to/project.git -> project5593# path/to/project/.git -> project5594my$name= to_utf8($project);5595$name=~ s,([^/])/*\.git$,$1,;5596$name= basename($name);5597# sanitize name5598$name=~s/[[:cntrl:]]/?/g;55995600my$ver=$hash;5601if($hash=~/^[0-9a-fA-F]+$/) {5602# shorten SHA-1 hash5603my$full_hash= git_get_full_hash($project,$hash);5604if($full_hash=~/^$hash/&&length($hash) >7) {5605$ver= git_get_short_hash($project,$hash);5606}5607}elsif($hash=~m!^refs/tags/(.*)$!) {5608# tags don't need shortened SHA-1 hash5609$ver=$1;5610}else{5611# branches and other need shortened SHA-1 hash5612if($hash=~m!^refs/(?:heads|remotes)/(.*)$!) {5613$ver=$1;5614}5615$ver.='-'. git_get_short_hash($project,$hash);5616}5617# in case of hierarchical branch names5618$ver=~s!/!.!g;56195620# name = project-version_string5621$name="$name-$ver";56225623returnwantarray? ($name,$name) :$name;5624}56255626sub git_snapshot {5627my$format=$input_params{'snapshot_format'};5628if(!@snapshot_fmts) {5629 die_error(403,"Snapshots not allowed");5630}5631# default to first supported snapshot format5632$format||=$snapshot_fmts[0];5633if($format!~m/^[a-z0-9]+$/) {5634 die_error(400,"Invalid snapshot format parameter");5635}elsif(!exists($known_snapshot_formats{$format})) {5636 die_error(400,"Unknown snapshot format");5637}elsif($known_snapshot_formats{$format}{'disabled'}) {5638 die_error(403,"Snapshot format not allowed");5639}elsif(!grep($_eq$format,@snapshot_fmts)) {5640 die_error(403,"Unsupported snapshot format");5641}56425643my$type= git_get_type("$hash^{}");5644if(!$type) {5645 die_error(404,'Object does not exist');5646}elsif($typeeq'blob') {5647 die_error(400,'Object is not a tree-ish');5648}56495650my($name,$prefix) = snapshot_name($project,$hash);5651my$filename="$name$known_snapshot_formats{$format}{'suffix'}";5652my$cmd= quote_command(5653 git_cmd(),'archive',5654"--format=$known_snapshot_formats{$format}{'format'}",5655"--prefix=$prefix/",$hash);5656if(exists$known_snapshot_formats{$format}{'compressor'}) {5657$cmd.=' | '. quote_command(@{$known_snapshot_formats{$format}{'compressor'}});5658}56595660$filename=~s/(["\\])/\\$1/g;5661print$cgi->header(5662-type =>$known_snapshot_formats{$format}{'type'},5663-content_disposition =>'inline; filename="'.$filename.'"',5664-status =>'200 OK');56655666open my$fd,"-|",$cmd5667or die_error(500,"Execute git-archive failed");5668binmode STDOUT,':raw';5669print<$fd>;5670binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5671close$fd;5672}56735674sub git_log_generic {5675my($fmt_name,$body_subr,$base,$parent,$file_name,$file_hash) =@_;56765677my$head= git_get_head_hash($project);5678if(!defined$base) {5679$base=$head;5680}5681if(!defined$page) {5682$page=0;5683}5684my$refs= git_get_references();56855686my$commit_hash=$base;5687if(defined$parent) {5688$commit_hash="$parent..$base";5689}5690my@commitlist=5691 parse_commits($commit_hash,101, (100*$page),5692defined$file_name? ($file_name,"--full-history") : ());56935694my$ftype;5695if(!defined$file_hash&&defined$file_name) {5696# some commits could have deleted file in question,5697# and not have it in tree, but one of them has to have it5698for(my$i=0;$i<@commitlist;$i++) {5699$file_hash= git_get_hash_by_path($commitlist[$i]{'id'},$file_name);5700last ifdefined$file_hash;5701}5702}5703if(defined$file_hash) {5704$ftype= git_get_type($file_hash);5705}5706if(defined$file_name&& !defined$ftype) {5707 die_error(500,"Unknown type of object");5708}5709my%co;5710if(defined$file_name) {5711%co= parse_commit($base)5712or die_error(404,"Unknown commit object");5713}571457155716my$paging_nav= format_paging_nav($fmt_name,$page,$#commitlist>=100);5717my$next_link='';5718if($#commitlist>=100) {5719$next_link=5720$cgi->a({-href => href(-replay=>1, page=>$page+1),5721-accesskey =>"n", -title =>"Alt-n"},"next");5722}5723my$patch_max= gitweb_get_feature('patches');5724if($patch_max&& !defined$file_name) {5725if($patch_max<0||@commitlist<=$patch_max) {5726$paging_nav.=" ⋅ ".5727$cgi->a({-href => href(action=>"patches", -replay=>1)},5728"patches");5729}5730}57315732 git_header_html();5733 git_print_page_nav($fmt_name,'',$hash,$hash,$hash,$paging_nav);5734if(defined$file_name) {5735 git_print_header_div('commit', esc_html($co{'title'}),$base);5736}else{5737 git_print_header_div('summary',$project)5738}5739 git_print_page_path($file_name,$ftype,$hash_base)5740if(defined$file_name);57415742$body_subr->(\@commitlist,0,99,$refs,$next_link,5743$file_name,$file_hash,$ftype);57445745 git_footer_html();5746}57475748sub git_log {5749 git_log_generic('log', \&git_log_body,5750$hash,$hash_parent);5751}57525753sub git_commit {5754$hash||=$hash_base||"HEAD";5755my%co= parse_commit($hash)5756or die_error(404,"Unknown commit object");57575758my$parent=$co{'parent'};5759my$parents=$co{'parents'};# listref57605761# we need to prepare $formats_nav before any parameter munging5762my$formats_nav;5763if(!defined$parent) {5764# --root commitdiff5765$formats_nav.='(initial)';5766}elsif(@$parents==1) {5767# single parent commit5768$formats_nav.=5769'(parent: '.5770$cgi->a({-href => href(action=>"commit",5771 hash=>$parent)},5772 esc_html(substr($parent,0,7))) .5773')';5774}else{5775# merge commit5776$formats_nav.=5777'(merge: '.5778join(' ',map{5779$cgi->a({-href => href(action=>"commit",5780 hash=>$_)},5781 esc_html(substr($_,0,7)));5782}@$parents) .5783')';5784}5785if(gitweb_check_feature('patches') &&@$parents<=1) {5786$formats_nav.=" | ".5787$cgi->a({-href => href(action=>"patch", -replay=>1)},5788"patch");5789}57905791if(!defined$parent) {5792$parent="--root";5793}5794my@difftree;5795open my$fd,"-|", git_cmd(),"diff-tree",'-r',"--no-commit-id",5796@diff_opts,5797(@$parents<=1?$parent:'-c'),5798$hash,"--"5799or die_error(500,"Open git-diff-tree failed");5800@difftree=map{chomp;$_} <$fd>;5801close$fdor die_error(404,"Reading git-diff-tree failed");58025803# non-textual hash id's can be cached5804my$expires;5805if($hash=~m/^[0-9a-fA-F]{40}$/) {5806$expires="+1d";5807}5808my$refs= git_get_references();5809my$ref= format_ref_marker($refs,$co{'id'});58105811 git_header_html(undef,$expires);5812 git_print_page_nav('commit','',5813$hash,$co{'tree'},$hash,5814$formats_nav);58155816if(defined$co{'parent'}) {5817 git_print_header_div('commitdiff', esc_html($co{'title'}) .$ref,$hash);5818}else{5819 git_print_header_div('tree', esc_html($co{'title'}) .$ref,$co{'tree'},$hash);5820}5821print"<div class=\"title_text\">\n".5822"<table class=\"object_header\">\n";5823 git_print_authorship_rows(\%co);5824print"<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";5825print"<tr>".5826"<td>tree</td>".5827"<td class=\"sha1\">".5828$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),5829class=>"list"},$co{'tree'}) .5830"</td>".5831"<td class=\"link\">".5832$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},5833"tree");5834my$snapshot_links= format_snapshot_links($hash);5835if(defined$snapshot_links) {5836print" | ".$snapshot_links;5837}5838print"</td>".5839"</tr>\n";58405841foreachmy$par(@$parents) {5842print"<tr>".5843"<td>parent</td>".5844"<td class=\"sha1\">".5845$cgi->a({-href => href(action=>"commit", hash=>$par),5846class=>"list"},$par) .5847"</td>".5848"<td class=\"link\">".5849$cgi->a({-href => href(action=>"commit", hash=>$par)},"commit") .5850" | ".5851$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)},"diff") .5852"</td>".5853"</tr>\n";5854}5855print"</table>".5856"</div>\n";58575858print"<div class=\"page_body\">\n";5859 git_print_log($co{'comment'});5860print"</div>\n";58615862 git_difftree_body(\@difftree,$hash,@$parents);58635864 git_footer_html();5865}58665867sub git_object {5868# object is defined by:5869# - hash or hash_base alone5870# - hash_base and file_name5871my$type;58725873# - hash or hash_base alone5874if($hash|| ($hash_base&& !defined$file_name)) {5875my$object_id=$hash||$hash_base;58765877open my$fd,"-|", quote_command(5878 git_cmd(),'cat-file','-t',$object_id) .' 2> /dev/null'5879or die_error(404,"Object does not exist");5880$type= <$fd>;5881chomp$type;5882close$fd5883or die_error(404,"Object does not exist");58845885# - hash_base and file_name5886}elsif($hash_base&&defined$file_name) {5887$file_name=~ s,/+$,,;58885889system(git_cmd(),"cat-file",'-e',$hash_base) ==05890or die_error(404,"Base object does not exist");58915892# here errors should not hapen5893open my$fd,"-|", git_cmd(),"ls-tree",$hash_base,"--",$file_name5894or die_error(500,"Open git-ls-tree failed");5895my$line= <$fd>;5896close$fd;58975898#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'5899unless($line&&$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {5900 die_error(404,"File or directory for given base does not exist");5901}5902$type=$2;5903$hash=$3;5904}else{5905 die_error(400,"Not enough information to find object");5906}59075908print$cgi->redirect(-uri => href(action=>$type, -full=>1,5909 hash=>$hash, hash_base=>$hash_base,5910 file_name=>$file_name),5911-status =>'302 Found');5912}59135914sub git_blobdiff {5915my$format=shift||'html';59165917my$fd;5918my@difftree;5919my%diffinfo;5920my$expires;59215922# preparing $fd and %diffinfo for git_patchset_body5923# new style URI5924if(defined$hash_base&&defined$hash_parent_base) {5925if(defined$file_name) {5926# read raw output5927open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5928$hash_parent_base,$hash_base,5929"--", (defined$file_parent?$file_parent: ()),$file_name5930or die_error(500,"Open git-diff-tree failed");5931@difftree=map{chomp;$_} <$fd>;5932close$fd5933or die_error(404,"Reading git-diff-tree failed");5934@difftree5935or die_error(404,"Blob diff not found");59365937}elsif(defined$hash&&5938$hash=~/[0-9a-fA-F]{40}/) {5939# try to find filename from $hash59405941# read filtered raw output5942open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5943$hash_parent_base,$hash_base,"--"5944or die_error(500,"Open git-diff-tree failed");5945@difftree=5946# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'5947# $hash == to_id5948grep{/^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/}5949map{chomp;$_} <$fd>;5950close$fd5951or die_error(404,"Reading git-diff-tree failed");5952@difftree5953or die_error(404,"Blob diff not found");59545955}else{5956 die_error(400,"Missing one of the blob diff parameters");5957}59585959if(@difftree>1) {5960 die_error(400,"Ambiguous blob diff specification");5961}59625963%diffinfo= parse_difftree_raw_line($difftree[0]);5964$file_parent||=$diffinfo{'from_file'} ||$file_name;5965$file_name||=$diffinfo{'to_file'};59665967$hash_parent||=$diffinfo{'from_id'};5968$hash||=$diffinfo{'to_id'};59695970# non-textual hash id's can be cached5971if($hash_base=~m/^[0-9a-fA-F]{40}$/&&5972$hash_parent_base=~m/^[0-9a-fA-F]{40}$/) {5973$expires='+1d';5974}59755976# open patch output5977open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5978'-p', ($formateq'html'?"--full-index": ()),5979$hash_parent_base,$hash_base,5980"--", (defined$file_parent?$file_parent: ()),$file_name5981or die_error(500,"Open git-diff-tree failed");5982}59835984# old/legacy style URI -- not generated anymore since 1.4.3.5985if(!%diffinfo) {5986 die_error('404 Not Found',"Missing one of the blob diff parameters")5987}59885989# header5990if($formateq'html') {5991my$formats_nav=5992$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},5993"raw");5994 git_header_html(undef,$expires);5995if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5996 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5997 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5998}else{5999print"<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";6000print"<div class=\"title\">$hashvs$hash_parent</div>\n";6001}6002if(defined$file_name) {6003 git_print_page_path($file_name,"blob",$hash_base);6004}else{6005print"<div class=\"page_path\"></div>\n";6006}60076008}elsif($formateq'plain') {6009print$cgi->header(6010-type =>'text/plain',6011-charset =>'utf-8',6012-expires =>$expires,6013-content_disposition =>'inline; filename="'."$file_name".'.patch"');60146015print"X-Git-Url: ".$cgi->self_url() ."\n\n";60166017}else{6018 die_error(400,"Unknown blobdiff format");6019}60206021# patch6022if($formateq'html') {6023print"<div class=\"page_body\">\n";60246025 git_patchset_body($fd, [ \%diffinfo],$hash_base,$hash_parent_base);6026close$fd;60276028print"</div>\n";# class="page_body"6029 git_footer_html();60306031}else{6032while(my$line= <$fd>) {6033$line=~s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;6034$line=~s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;60356036print$line;60376038last if$line=~m!^\+\+\+!;6039}6040local$/=undef;6041print<$fd>;6042close$fd;6043}6044}60456046sub git_blobdiff_plain {6047 git_blobdiff('plain');6048}60496050sub git_commitdiff {6051my%params=@_;6052my$format=$params{-format} ||'html';60536054my($patch_max) = gitweb_get_feature('patches');6055if($formateq'patch') {6056 die_error(403,"Patch view not allowed")unless$patch_max;6057}60586059$hash||=$hash_base||"HEAD";6060my%co= parse_commit($hash)6061or die_error(404,"Unknown commit object");60626063# choose format for commitdiff for merge6064if(!defined$hash_parent&& @{$co{'parents'}} >1) {6065$hash_parent='--cc';6066}6067# we need to prepare $formats_nav before almost any parameter munging6068my$formats_nav;6069if($formateq'html') {6070$formats_nav=6071$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},6072"raw");6073if($patch_max&& @{$co{'parents'}} <=1) {6074$formats_nav.=" | ".6075$cgi->a({-href => href(action=>"patch", -replay=>1)},6076"patch");6077}60786079if(defined$hash_parent&&6080$hash_parentne'-c'&&$hash_parentne'--cc') {6081# commitdiff with two commits given6082my$hash_parent_short=$hash_parent;6083if($hash_parent=~m/^[0-9a-fA-F]{40}$/) {6084$hash_parent_short=substr($hash_parent,0,7);6085}6086$formats_nav.=6087' (from';6088for(my$i=0;$i< @{$co{'parents'}};$i++) {6089if($co{'parents'}[$i]eq$hash_parent) {6090$formats_nav.=' parent '. ($i+1);6091last;6092}6093}6094$formats_nav.=': '.6095$cgi->a({-href => href(action=>"commitdiff",6096 hash=>$hash_parent)},6097 esc_html($hash_parent_short)) .6098')';6099}elsif(!$co{'parent'}) {6100# --root commitdiff6101$formats_nav.=' (initial)';6102}elsif(scalar@{$co{'parents'}} ==1) {6103# single parent commit6104$formats_nav.=6105' (parent: '.6106$cgi->a({-href => href(action=>"commitdiff",6107 hash=>$co{'parent'})},6108 esc_html(substr($co{'parent'},0,7))) .6109')';6110}else{6111# merge commit6112if($hash_parenteq'--cc') {6113$formats_nav.=' | '.6114$cgi->a({-href => href(action=>"commitdiff",6115 hash=>$hash, hash_parent=>'-c')},6116'combined');6117}else{# $hash_parent eq '-c'6118$formats_nav.=' | '.6119$cgi->a({-href => href(action=>"commitdiff",6120 hash=>$hash, hash_parent=>'--cc')},6121'compact');6122}6123$formats_nav.=6124' (merge: '.6125join(' ',map{6126$cgi->a({-href => href(action=>"commitdiff",6127 hash=>$_)},6128 esc_html(substr($_,0,7)));6129} @{$co{'parents'}} ) .6130')';6131}6132}61336134my$hash_parent_param=$hash_parent;6135if(!defined$hash_parent_param) {6136# --cc for multiple parents, --root for parentless6137$hash_parent_param=6138@{$co{'parents'}} >1?'--cc':$co{'parent'} ||'--root';6139}61406141# read commitdiff6142my$fd;6143my@difftree;6144if($formateq'html') {6145open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6146"--no-commit-id","--patch-with-raw","--full-index",6147$hash_parent_param,$hash,"--"6148or die_error(500,"Open git-diff-tree failed");61496150while(my$line= <$fd>) {6151chomp$line;6152# empty line ends raw part of diff-tree output6153last unless$line;6154push@difftree,scalar parse_difftree_raw_line($line);6155}61566157}elsif($formateq'plain') {6158open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6159'-p',$hash_parent_param,$hash,"--"6160or die_error(500,"Open git-diff-tree failed");6161}elsif($formateq'patch') {6162# For commit ranges, we limit the output to the number of6163# patches specified in the 'patches' feature.6164# For single commits, we limit the output to a single patch,6165# diverging from the git-format-patch default.6166my@commit_spec= ();6167if($hash_parent) {6168if($patch_max>0) {6169push@commit_spec,"-$patch_max";6170}6171push@commit_spec,'-n',"$hash_parent..$hash";6172}else{6173if($params{-single}) {6174push@commit_spec,'-1';6175}else{6176if($patch_max>0) {6177push@commit_spec,"-$patch_max";6178}6179push@commit_spec,"-n";6180}6181push@commit_spec,'--root',$hash;6182}6183open$fd,"-|", git_cmd(),"format-patch",'--encoding=utf8',6184'--stdout',@commit_spec6185or die_error(500,"Open git-format-patch failed");6186}else{6187 die_error(400,"Unknown commitdiff format");6188}61896190# non-textual hash id's can be cached6191my$expires;6192if($hash=~m/^[0-9a-fA-F]{40}$/) {6193$expires="+1d";6194}61956196# write commit message6197if($formateq'html') {6198my$refs= git_get_references();6199my$ref= format_ref_marker($refs,$co{'id'});62006201 git_header_html(undef,$expires);6202 git_print_page_nav('commitdiff','',$hash,$co{'tree'},$hash,$formats_nav);6203 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash);6204print"<div class=\"title_text\">\n".6205"<table class=\"object_header\">\n";6206 git_print_authorship_rows(\%co);6207print"</table>".6208"</div>\n";6209print"<div class=\"page_body\">\n";6210if(@{$co{'comment'}} >1) {6211print"<div class=\"log\">\n";6212 git_print_log($co{'comment'}, -final_empty_line=>1, -remove_title =>1);6213print"</div>\n";# class="log"6214}62156216}elsif($formateq'plain') {6217my$refs= git_get_references("tags");6218my$tagname= git_get_rev_name_tags($hash);6219my$filename= basename($project) ."-$hash.patch";62206221print$cgi->header(6222-type =>'text/plain',6223-charset =>'utf-8',6224-expires =>$expires,6225-content_disposition =>'inline; filename="'."$filename".'"');6226my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});6227print"From: ". to_utf8($co{'author'}) ."\n";6228print"Date:$ad{'rfc2822'} ($ad{'tz_local'})\n";6229print"Subject: ". to_utf8($co{'title'}) ."\n";62306231print"X-Git-Tag:$tagname\n"if$tagname;6232print"X-Git-Url: ".$cgi->self_url() ."\n\n";62336234foreachmy$line(@{$co{'comment'}}) {6235print to_utf8($line) ."\n";6236}6237print"---\n\n";6238}elsif($formateq'patch') {6239my$filename= basename($project) ."-$hash.patch";62406241print$cgi->header(6242-type =>'text/plain',6243-charset =>'utf-8',6244-expires =>$expires,6245-content_disposition =>'inline; filename="'."$filename".'"');6246}62476248# write patch6249if($formateq'html') {6250my$use_parents= !defined$hash_parent||6251$hash_parenteq'-c'||$hash_parenteq'--cc';6252 git_difftree_body(\@difftree,$hash,6253$use_parents? @{$co{'parents'}} :$hash_parent);6254print"<br/>\n";62556256 git_patchset_body($fd, \@difftree,$hash,6257$use_parents? @{$co{'parents'}} :$hash_parent);6258close$fd;6259print"</div>\n";# class="page_body"6260 git_footer_html();62616262}elsif($formateq'plain') {6263local$/=undef;6264print<$fd>;6265close$fd6266or print"Reading git-diff-tree failed\n";6267}elsif($formateq'patch') {6268local$/=undef;6269print<$fd>;6270close$fd6271or print"Reading git-format-patch failed\n";6272}6273}62746275sub git_commitdiff_plain {6276 git_commitdiff(-format =>'plain');6277}62786279# format-patch-style patches6280sub git_patch {6281 git_commitdiff(-format =>'patch', -single =>1);6282}62836284sub git_patches {6285 git_commitdiff(-format =>'patch');6286}62876288sub git_history {6289 git_log_generic('history', \&git_history_body,6290$hash_base,$hash_parent_base,6291$file_name,$hash);6292}62936294sub git_search {6295 gitweb_check_feature('search')or die_error(403,"Search is disabled");6296if(!defined$searchtext) {6297 die_error(400,"Text field is empty");6298}6299if(!defined$hash) {6300$hash= git_get_head_hash($project);6301}6302my%co= parse_commit($hash);6303if(!%co) {6304 die_error(404,"Unknown commit object");6305}6306if(!defined$page) {6307$page=0;6308}63096310$searchtype||='commit';6311if($searchtypeeq'pickaxe') {6312# pickaxe may take all resources of your box and run for several minutes6313# with every query - so decide by yourself how public you make this feature6314 gitweb_check_feature('pickaxe')6315or die_error(403,"Pickaxe is disabled");6316}6317if($searchtypeeq'grep') {6318 gitweb_check_feature('grep')6319or die_error(403,"Grep is disabled");6320}63216322 git_header_html();63236324if($searchtypeeq'commit'or$searchtypeeq'author'or$searchtypeeq'committer') {6325my$greptype;6326if($searchtypeeq'commit') {6327$greptype="--grep=";6328}elsif($searchtypeeq'author') {6329$greptype="--author=";6330}elsif($searchtypeeq'committer') {6331$greptype="--committer=";6332}6333$greptype.=$searchtext;6334my@commitlist= parse_commits($hash,101, (100*$page),undef,6335$greptype,'--regexp-ignore-case',6336$search_use_regexp?'--extended-regexp':'--fixed-strings');63376338my$paging_nav='';6339if($page>0) {6340$paging_nav.=6341$cgi->a({-href => href(action=>"search", hash=>$hash,6342 searchtext=>$searchtext,6343 searchtype=>$searchtype)},6344"first");6345$paging_nav.=" ⋅ ".6346$cgi->a({-href => href(-replay=>1, page=>$page-1),6347-accesskey =>"p", -title =>"Alt-p"},"prev");6348}else{6349$paging_nav.="first";6350$paging_nav.=" ⋅ prev";6351}6352my$next_link='';6353if($#commitlist>=100) {6354$next_link=6355$cgi->a({-href => href(-replay=>1, page=>$page+1),6356-accesskey =>"n", -title =>"Alt-n"},"next");6357$paging_nav.=" ⋅$next_link";6358}else{6359$paging_nav.=" ⋅ next";6360}63616362if($#commitlist>=100) {6363}63646365 git_print_page_nav('','',$hash,$co{'tree'},$hash,$paging_nav);6366 git_print_header_div('commit', esc_html($co{'title'}),$hash);6367 git_search_grep_body(\@commitlist,0,99,$next_link);6368}63696370if($searchtypeeq'pickaxe') {6371 git_print_page_nav('','',$hash,$co{'tree'},$hash);6372 git_print_header_div('commit', esc_html($co{'title'}),$hash);63736374print"<table class=\"pickaxe search\">\n";6375my$alternate=1;6376local$/="\n";6377open my$fd,'-|', git_cmd(),'--no-pager','log',@diff_opts,6378'--pretty=format:%H','--no-abbrev','--raw',"-S$searchtext",6379($search_use_regexp?'--pickaxe-regex': ());6380undef%co;6381my@files;6382while(my$line= <$fd>) {6383chomp$line;6384next unless$line;63856386my%set= parse_difftree_raw_line($line);6387if(defined$set{'commit'}) {6388# finish previous commit6389if(%co) {6390print"</td>\n".6391"<td class=\"link\">".6392$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6393" | ".6394$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6395print"</td>\n".6396"</tr>\n";6397}63986399if($alternate) {6400print"<tr class=\"dark\">\n";6401}else{6402print"<tr class=\"light\">\n";6403}6404$alternate^=1;6405%co= parse_commit($set{'commit'});6406my$author= chop_and_escape_str($co{'author_name'},15,5);6407print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".6408"<td><i>$author</i></td>\n".6409"<td>".6410$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),6411-class=>"list subject"},6412 chop_and_escape_str($co{'title'},50) ."<br/>");6413}elsif(defined$set{'to_id'}) {6414next if($set{'to_id'} =~m/^0{40}$/);64156416print$cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},6417 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),6418-class=>"list"},6419"<span class=\"match\">". esc_path($set{'file'}) ."</span>") .6420"<br/>\n";6421}6422}6423close$fd;64246425# finish last commit (warning: repetition!)6426if(%co) {6427print"</td>\n".6428"<td class=\"link\">".6429$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6430" | ".6431$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6432print"</td>\n".6433"</tr>\n";6434}64356436print"</table>\n";6437}64386439if($searchtypeeq'grep') {6440 git_print_page_nav('','',$hash,$co{'tree'},$hash);6441 git_print_header_div('commit', esc_html($co{'title'}),$hash);64426443print"<table class=\"grep_search\">\n";6444my$alternate=1;6445my$matches=0;6446local$/="\n";6447open my$fd,"-|", git_cmd(),'grep','-n',6448$search_use_regexp? ('-E','-i') :'-F',6449$searchtext,$co{'tree'};6450my$lastfile='';6451while(my$line= <$fd>) {6452chomp$line;6453my($file,$lno,$ltext,$binary);6454last if($matches++>1000);6455if($line=~/^Binary file (.+) matches$/) {6456$file=$1;6457$binary=1;6458}else{6459(undef,$file,$lno,$ltext) =split(/:/,$line,4);6460}6461if($filene$lastfile) {6462$lastfileand print"</td></tr>\n";6463if($alternate++) {6464print"<tr class=\"dark\">\n";6465}else{6466print"<tr class=\"light\">\n";6467}6468print"<td class=\"list\">".6469$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6470 file_name=>"$file"),6471-class=>"list"}, esc_path($file));6472print"</td><td>\n";6473$lastfile=$file;6474}6475if($binary) {6476print"<div class=\"binary\">Binary file</div>\n";6477}else{6478$ltext= untabify($ltext);6479if($ltext=~m/^(.*)($search_regexp)(.*)$/i) {6480$ltext= esc_html($1, -nbsp=>1);6481$ltext.='<span class="match">';6482$ltext.= esc_html($2, -nbsp=>1);6483$ltext.='</span>';6484$ltext.= esc_html($3, -nbsp=>1);6485}else{6486$ltext= esc_html($ltext, -nbsp=>1);6487}6488print"<div class=\"pre\">".6489$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6490 file_name=>"$file").'#l'.$lno,6491-class=>"linenr"},sprintf('%4i',$lno))6492.' '.$ltext."</div>\n";6493}6494}6495if($lastfile) {6496print"</td></tr>\n";6497if($matches>1000) {6498print"<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";6499}6500}else{6501print"<div class=\"diff nodifferences\">No matches found</div>\n";6502}6503close$fd;65046505print"</table>\n";6506}6507 git_footer_html();6508}65096510sub git_search_help {6511 git_header_html();6512 git_print_page_nav('','',$hash,$hash,$hash);6513print<<EOT;6514<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without6515regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,6516the pattern entered is recognized as the POSIX extended6517<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case6518insensitive).</p>6519<dl>6520<dt><b>commit</b></dt>6521<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>6522EOT6523my$have_grep= gitweb_check_feature('grep');6524if($have_grep) {6525print<<EOT;6526<dt><b>grep</b></dt>6527<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing6528 a different one) are searched for the given pattern. On large trees, this search can take6529a while and put some strain on the server, so please use it with some consideration. Note that6530due to git-grep peculiarity, currently if regexp mode is turned off, the matches are6531case-sensitive.</dd>6532EOT6533}6534print<<EOT;6535<dt><b>author</b></dt>6536<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>6537<dt><b>committer</b></dt>6538<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>6539EOT6540my$have_pickaxe= gitweb_check_feature('pickaxe');6541if($have_pickaxe) {6542print<<EOT;6543<dt><b>pickaxe</b></dt>6544<dd>All commits that caused the string to appear or disappear from any file (changes that6545added, removed or "modified" the string) will be listed. This search can take a while and6546takes a lot of strain on the server, so please use it wisely. Note that since you may be6547interested even in changes just changing the case as well, this search is case sensitive.</dd>6548EOT6549}6550print"</dl>\n";6551 git_footer_html();6552}65536554sub git_shortlog {6555 git_log_generic('shortlog', \&git_shortlog_body,6556$hash,$hash_parent);6557}65586559## ......................................................................6560## feeds (RSS, Atom; OPML)65616562sub git_feed {6563my$format=shift||'atom';6564my$have_blame= gitweb_check_feature('blame');65656566# Atom: http://www.atomenabled.org/developers/syndication/6567# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ6568if($formatne'rss'&&$formatne'atom') {6569 die_error(400,"Unknown web feed format");6570}65716572# log/feed of current (HEAD) branch, log of given branch, history of file/directory6573my$head=$hash||'HEAD';6574my@commitlist= parse_commits($head,150,0,$file_name);65756576my%latest_commit;6577my%latest_date;6578my$content_type="application/$format+xml";6579if(defined$cgi->http('HTTP_ACCEPT') &&6580$cgi->Accept('text/xml') >$cgi->Accept($content_type)) {6581# browser (feed reader) prefers text/xml6582$content_type='text/xml';6583}6584if(defined($commitlist[0])) {6585%latest_commit= %{$commitlist[0]};6586my$latest_epoch=$latest_commit{'committer_epoch'};6587%latest_date= parse_date($latest_epoch);6588my$if_modified=$cgi->http('IF_MODIFIED_SINCE');6589if(defined$if_modified) {6590my$since;6591if(eval{require HTTP::Date;1; }) {6592$since= HTTP::Date::str2time($if_modified);6593}elsif(eval{require Time::ParseDate;1; }) {6594$since= Time::ParseDate::parsedate($if_modified, GMT =>1);6595}6596if(defined$since&&$latest_epoch<=$since) {6597print$cgi->header(6598-type =>$content_type,6599-charset =>'utf-8',6600-last_modified =>$latest_date{'rfc2822'},6601-status =>'304 Not Modified');6602return;6603}6604}6605print$cgi->header(6606-type =>$content_type,6607-charset =>'utf-8',6608-last_modified =>$latest_date{'rfc2822'});6609}else{6610print$cgi->header(6611-type =>$content_type,6612-charset =>'utf-8');6613}66146615# Optimization: skip generating the body if client asks only6616# for Last-Modified date.6617return if($cgi->request_method()eq'HEAD');66186619# header variables6620my$title="$site_name-$project/$action";6621my$feed_type='log';6622if(defined$hash) {6623$title.=" - '$hash'";6624$feed_type='branch log';6625if(defined$file_name) {6626$title.=" ::$file_name";6627$feed_type='history';6628}6629}elsif(defined$file_name) {6630$title.=" -$file_name";6631$feed_type='history';6632}6633$title.="$feed_type";6634my$descr= git_get_project_description($project);6635if(defined$descr) {6636$descr= esc_html($descr);6637}else{6638$descr="$project".6639($formateq'rss'?'RSS':'Atom') .6640" feed";6641}6642my$owner= git_get_project_owner($project);6643$owner= esc_html($owner);66446645#header6646my$alt_url;6647if(defined$file_name) {6648$alt_url= href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);6649}elsif(defined$hash) {6650$alt_url= href(-full=>1, action=>"log", hash=>$hash);6651}else{6652$alt_url= href(-full=>1, action=>"summary");6653}6654print qq!<?xml version="1.0" encoding="utf-8"?>\n!;6655if($formateq'rss') {6656print<<XML;6657<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">6658<channel>6659XML6660print"<title>$title</title>\n".6661"<link>$alt_url</link>\n".6662"<description>$descr</description>\n".6663"<language>en</language>\n".6664# project owner is responsible for 'editorial' content6665"<managingEditor>$owner</managingEditor>\n";6666if(defined$logo||defined$favicon) {6667# prefer the logo to the favicon, since RSS6668# doesn't allow both6669my$img= esc_url($logo||$favicon);6670print"<image>\n".6671"<url>$img</url>\n".6672"<title>$title</title>\n".6673"<link>$alt_url</link>\n".6674"</image>\n";6675}6676if(%latest_date) {6677print"<pubDate>$latest_date{'rfc2822'}</pubDate>\n";6678print"<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";6679}6680print"<generator>gitweb v.$version/$git_version</generator>\n";6681}elsif($formateq'atom') {6682print<<XML;6683<feed xmlns="http://www.w3.org/2005/Atom">6684XML6685print"<title>$title</title>\n".6686"<subtitle>$descr</subtitle>\n".6687'<link rel="alternate" type="text/html" href="'.6688$alt_url.'" />'."\n".6689'<link rel="self" type="'.$content_type.'" href="'.6690$cgi->self_url() .'" />'."\n".6691"<id>". href(-full=>1) ."</id>\n".6692# use project owner for feed author6693"<author><name>$owner</name></author>\n";6694if(defined$favicon) {6695print"<icon>". esc_url($favicon) ."</icon>\n";6696}6697if(defined$logo_url) {6698# not twice as wide as tall: 72 x 27 pixels6699print"<logo>". esc_url($logo) ."</logo>\n";6700}6701if(!%latest_date) {6702# dummy date to keep the feed valid until commits trickle in:6703print"<updated>1970-01-01T00:00:00Z</updated>\n";6704}else{6705print"<updated>$latest_date{'iso-8601'}</updated>\n";6706}6707print"<generator version='$version/$git_version'>gitweb</generator>\n";6708}67096710# contents6711for(my$i=0;$i<=$#commitlist;$i++) {6712my%co= %{$commitlist[$i]};6713my$commit=$co{'id'};6714# we read 150, we always show 30 and the ones more recent than 48 hours6715if(($i>=20) && ((time-$co{'author_epoch'}) >48*60*60)) {6716last;6717}6718my%cd= parse_date($co{'author_epoch'});67196720# get list of changed files6721open my$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6722$co{'parent'} ||"--root",6723$co{'id'},"--", (defined$file_name?$file_name: ())6724ornext;6725my@difftree=map{chomp;$_} <$fd>;6726close$fd6727ornext;67286729# print element (entry, item)6730my$co_url= href(-full=>1, action=>"commitdiff", hash=>$commit);6731if($formateq'rss') {6732print"<item>\n".6733"<title>". esc_html($co{'title'}) ."</title>\n".6734"<author>". esc_html($co{'author'}) ."</author>\n".6735"<pubDate>$cd{'rfc2822'}</pubDate>\n".6736"<guid isPermaLink=\"true\">$co_url</guid>\n".6737"<link>$co_url</link>\n".6738"<description>". esc_html($co{'title'}) ."</description>\n".6739"<content:encoded>".6740"<![CDATA[\n";6741}elsif($formateq'atom') {6742print"<entry>\n".6743"<title type=\"html\">". esc_html($co{'title'}) ."</title>\n".6744"<updated>$cd{'iso-8601'}</updated>\n".6745"<author>\n".6746" <name>". esc_html($co{'author_name'}) ."</name>\n";6747if($co{'author_email'}) {6748print" <email>". esc_html($co{'author_email'}) ."</email>\n";6749}6750print"</author>\n".6751# use committer for contributor6752"<contributor>\n".6753" <name>". esc_html($co{'committer_name'}) ."</name>\n";6754if($co{'committer_email'}) {6755print" <email>". esc_html($co{'committer_email'}) ."</email>\n";6756}6757print"</contributor>\n".6758"<published>$cd{'iso-8601'}</published>\n".6759"<link rel=\"alternate\"type=\"text/html\"href=\"$co_url\"/>\n".6760"<id>$co_url</id>\n".6761"<content type=\"xhtml\"xml:base=\"". esc_url($my_url) ."\">\n".6762"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";6763}6764my$comment=$co{'comment'};6765print"<pre>\n";6766foreachmy$line(@$comment) {6767$line= esc_html($line);6768print"$line\n";6769}6770print"</pre><ul>\n";6771foreachmy$difftree_line(@difftree) {6772my%difftree= parse_difftree_raw_line($difftree_line);6773next if!$difftree{'from_id'};67746775my$file=$difftree{'file'} ||$difftree{'to_file'};67766777print"<li>".6778"[".6779$cgi->a({-href => href(-full=>1, action=>"blobdiff",6780 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},6781 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},6782 file_name=>$file, file_parent=>$difftree{'from_file'}),6783-title =>"diff"},'D');6784if($have_blame) {6785print$cgi->a({-href => href(-full=>1, action=>"blame",6786 file_name=>$file, hash_base=>$commit),6787-title =>"blame"},'B');6788}6789# if this is not a feed of a file history6790if(!defined$file_name||$file_namene$file) {6791print$cgi->a({-href => href(-full=>1, action=>"history",6792 file_name=>$file, hash=>$commit),6793-title =>"history"},'H');6794}6795$file= esc_path($file);6796print"] ".6797"$file</li>\n";6798}6799if($formateq'rss') {6800print"</ul>]]>\n".6801"</content:encoded>\n".6802"</item>\n";6803}elsif($formateq'atom') {6804print"</ul>\n</div>\n".6805"</content>\n".6806"</entry>\n";6807}6808}68096810# end of feed6811if($formateq'rss') {6812print"</channel>\n</rss>\n";6813}elsif($formateq'atom') {6814print"</feed>\n";6815}6816}68176818sub git_rss {6819 git_feed('rss');6820}68216822sub git_atom {6823 git_feed('atom');6824}68256826sub git_opml {6827my@list= git_get_projects_list();68286829print$cgi->header(6830-type =>'text/xml',6831-charset =>'utf-8',6832-content_disposition =>'inline; filename="opml.xml"');68336834print<<XML;6835<?xml version="1.0" encoding="utf-8"?>6836<opml version="1.0">6837<head>6838 <title>$site_nameOPML Export</title>6839</head>6840<body>6841<outline text="git RSS feeds">6842XML68436844foreachmy$pr(@list) {6845my%proj=%$pr;6846my$head= git_get_head_hash($proj{'path'});6847if(!defined$head) {6848next;6849}6850$git_dir="$projectroot/$proj{'path'}";6851my%co= parse_commit($head);6852if(!%co) {6853next;6854}68556856my$path= esc_html(chop_str($proj{'path'},25,5));6857my$rss= href('project'=>$proj{'path'},'action'=>'rss', -full =>1);6858my$html= href('project'=>$proj{'path'},'action'=>'summary', -full =>1);6859print"<outline type=\"rss\"text=\"$path\"title=\"$path\"xmlUrl=\"$rss\"htmlUrl=\"$html\"/>\n";6860}6861print<<XML;6862</outline>6863</body>6864</opml>6865XML6866}