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 set_message); 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# You define site-wide feature defaults here; override them with 231# $GITWEB_CONFIG as necessary. 232our%feature= ( 233# feature => { 234# 'sub' => feature-sub (subroutine), 235# 'override' => allow-override (boolean), 236# 'default' => [ default options...] (array reference)} 237# 238# if feature is overridable (it means that allow-override has true value), 239# then feature-sub will be called with default options as parameters; 240# return value of feature-sub indicates if to enable specified feature 241# 242# if there is no 'sub' key (no feature-sub), then feature cannot be 243# overriden 244# 245# use gitweb_get_feature(<feature>) to retrieve the <feature> value 246# (an array) or gitweb_check_feature(<feature>) to check if <feature> 247# is enabled 248 249# Enable the 'blame' blob view, showing the last commit that modified 250# each line in the file. This can be very CPU-intensive. 251 252# To enable system wide have in $GITWEB_CONFIG 253# $feature{'blame'}{'default'} = [1]; 254# To have project specific config enable override in $GITWEB_CONFIG 255# $feature{'blame'}{'override'} = 1; 256# and in project config gitweb.blame = 0|1; 257'blame'=> { 258'sub'=>sub{ feature_bool('blame',@_) }, 259'override'=>0, 260'default'=> [0]}, 261 262# Enable the 'snapshot' link, providing a compressed archive of any 263# tree. This can potentially generate high traffic if you have large 264# project. 265 266# Value is a list of formats defined in %known_snapshot_formats that 267# you wish to offer. 268# To disable system wide have in $GITWEB_CONFIG 269# $feature{'snapshot'}{'default'} = []; 270# To have project specific config enable override in $GITWEB_CONFIG 271# $feature{'snapshot'}{'override'} = 1; 272# and in project config, a comma-separated list of formats or "none" 273# to disable. Example: gitweb.snapshot = tbz2,zip; 274'snapshot'=> { 275'sub'=> \&feature_snapshot, 276'override'=>0, 277'default'=> ['tgz']}, 278 279# Enable text search, which will list the commits which match author, 280# committer or commit text to a given string. Enabled by default. 281# Project specific override is not supported. 282'search'=> { 283'override'=>0, 284'default'=> [1]}, 285 286# Enable grep search, which will list the files in currently selected 287# tree containing the given string. Enabled by default. This can be 288# potentially CPU-intensive, of course. 289 290# To enable system wide have in $GITWEB_CONFIG 291# $feature{'grep'}{'default'} = [1]; 292# To have project specific config enable override in $GITWEB_CONFIG 293# $feature{'grep'}{'override'} = 1; 294# and in project config gitweb.grep = 0|1; 295'grep'=> { 296'sub'=>sub{ feature_bool('grep',@_) }, 297'override'=>0, 298'default'=> [1]}, 299 300# Enable the pickaxe search, which will list the commits that modified 301# a given string in a file. This can be practical and quite faster 302# alternative to 'blame', but still potentially CPU-intensive. 303 304# To enable system wide have in $GITWEB_CONFIG 305# $feature{'pickaxe'}{'default'} = [1]; 306# To have project specific config enable override in $GITWEB_CONFIG 307# $feature{'pickaxe'}{'override'} = 1; 308# and in project config gitweb.pickaxe = 0|1; 309'pickaxe'=> { 310'sub'=>sub{ feature_bool('pickaxe',@_) }, 311'override'=>0, 312'default'=> [1]}, 313 314# Enable showing size of blobs in a 'tree' view, in a separate 315# column, similar to what 'ls -l' does. This cost a bit of IO. 316 317# To disable system wide have in $GITWEB_CONFIG 318# $feature{'show-sizes'}{'default'} = [0]; 319# To have project specific config enable override in $GITWEB_CONFIG 320# $feature{'show-sizes'}{'override'} = 1; 321# and in project config gitweb.showsizes = 0|1; 322'show-sizes'=> { 323'sub'=>sub{ feature_bool('showsizes',@_) }, 324'override'=>0, 325'default'=> [1]}, 326 327# Make gitweb use an alternative format of the URLs which can be 328# more readable and natural-looking: project name is embedded 329# directly in the path and the query string contains other 330# auxiliary information. All gitweb installations recognize 331# URL in either format; this configures in which formats gitweb 332# generates links. 333 334# To enable system wide have in $GITWEB_CONFIG 335# $feature{'pathinfo'}{'default'} = [1]; 336# Project specific override is not supported. 337 338# Note that you will need to change the default location of CSS, 339# favicon, logo and possibly other files to an absolute URL. Also, 340# if gitweb.cgi serves as your indexfile, you will need to force 341# $my_uri to contain the script name in your $GITWEB_CONFIG. 342'pathinfo'=> { 343'override'=>0, 344'default'=> [0]}, 345 346# Make gitweb consider projects in project root subdirectories 347# to be forks of existing projects. Given project $projname.git, 348# projects matching $projname/*.git will not be shown in the main 349# projects list, instead a '+' mark will be added to $projname 350# there and a 'forks' view will be enabled for the project, listing 351# all the forks. If project list is taken from a file, forks have 352# to be listed after the main project. 353 354# To enable system wide have in $GITWEB_CONFIG 355# $feature{'forks'}{'default'} = [1]; 356# Project specific override is not supported. 357'forks'=> { 358'override'=>0, 359'default'=> [0]}, 360 361# Insert custom links to the action bar of all project pages. 362# This enables you mainly to link to third-party scripts integrating 363# into gitweb; e.g. git-browser for graphical history representation 364# or custom web-based repository administration interface. 365 366# The 'default' value consists of a list of triplets in the form 367# (label, link, position) where position is the label after which 368# to insert the link and link is a format string where %n expands 369# to the project name, %f to the project path within the filesystem, 370# %h to the current hash (h gitweb parameter) and %b to the current 371# hash base (hb gitweb parameter); %% expands to %. 372 373# To enable system wide have in $GITWEB_CONFIG e.g. 374# $feature{'actions'}{'default'} = [('graphiclog', 375# '/git-browser/by-commit.html?r=%n', 'summary')]; 376# Project specific override is not supported. 377'actions'=> { 378'override'=>0, 379'default'=> []}, 380 381# Allow gitweb scan project content tags described in ctags/ 382# of project repository, and display the popular Web 2.0-ish 383# "tag cloud" near the project list. Note that this is something 384# COMPLETELY different from the normal Git tags. 385 386# gitweb by itself can show existing tags, but it does not handle 387# tagging itself; you need an external application for that. 388# For an example script, check Girocco's cgi/tagproj.cgi. 389# You may want to install the HTML::TagCloud Perl module to get 390# a pretty tag cloud instead of just a list of tags. 391 392# To enable system wide have in $GITWEB_CONFIG 393# $feature{'ctags'}{'default'} = ['path_to_tag_script']; 394# Project specific override is not supported. 395'ctags'=> { 396'override'=>0, 397'default'=> [0]}, 398 399# The maximum number of patches in a patchset generated in patch 400# view. Set this to 0 or undef to disable patch view, or to a 401# negative number to remove any limit. 402 403# To disable system wide have in $GITWEB_CONFIG 404# $feature{'patches'}{'default'} = [0]; 405# To have project specific config enable override in $GITWEB_CONFIG 406# $feature{'patches'}{'override'} = 1; 407# and in project config gitweb.patches = 0|n; 408# where n is the maximum number of patches allowed in a patchset. 409'patches'=> { 410'sub'=> \&feature_patches, 411'override'=>0, 412'default'=> [16]}, 413 414# Avatar support. When this feature is enabled, views such as 415# shortlog or commit will display an avatar associated with 416# the email of the committer(s) and/or author(s). 417 418# Currently available providers are gravatar and picon. 419# If an unknown provider is specified, the feature is disabled. 420 421# Gravatar depends on Digest::MD5. 422# Picon currently relies on the indiana.edu database. 423 424# To enable system wide have in $GITWEB_CONFIG 425# $feature{'avatar'}{'default'} = ['<provider>']; 426# where <provider> is either gravatar or picon. 427# To have project specific config enable override in $GITWEB_CONFIG 428# $feature{'avatar'}{'override'} = 1; 429# and in project config gitweb.avatar = <provider>; 430'avatar'=> { 431'sub'=> \&feature_avatar, 432'override'=>0, 433'default'=> ['']}, 434 435# Enable displaying how much time and how many git commands 436# it took to generate and display page. Disabled by default. 437# Project specific override is not supported. 438'timed'=> { 439'override'=>0, 440'default'=> [0]}, 441 442# Enable turning some links into links to actions which require 443# JavaScript to run (like 'blame_incremental'). Not enabled by 444# default. Project specific override is currently not supported. 445'javascript-actions'=> { 446'override'=>0, 447'default'=> [0]}, 448 449# Syntax highlighting support. This is based on Daniel Svensson's 450# and Sham Chukoury's work in gitweb-xmms2.git. 451# It requires the 'highlight' program present in $PATH, 452# and therefore is disabled by default. 453 454# To enable system wide have in $GITWEB_CONFIG 455# $feature{'highlight'}{'default'} = [1]; 456 457'highlight'=> { 458'sub'=>sub{ feature_bool('highlight',@_) }, 459'override'=>0, 460'default'=> [0]}, 461); 462 463sub gitweb_get_feature { 464my($name) =@_; 465return unlessexists$feature{$name}; 466my($sub,$override,@defaults) = ( 467$feature{$name}{'sub'}, 468$feature{$name}{'override'}, 469@{$feature{$name}{'default'}}); 470# project specific override is possible only if we have project 471our$git_dir;# global variable, declared later 472if(!$override|| !defined$git_dir) { 473return@defaults; 474} 475if(!defined$sub) { 476warn"feature$nameis not overridable"; 477return@defaults; 478} 479return$sub->(@defaults); 480} 481 482# A wrapper to check if a given feature is enabled. 483# With this, you can say 484# 485# my $bool_feat = gitweb_check_feature('bool_feat'); 486# gitweb_check_feature('bool_feat') or somecode; 487# 488# instead of 489# 490# my ($bool_feat) = gitweb_get_feature('bool_feat'); 491# (gitweb_get_feature('bool_feat'))[0] or somecode; 492# 493sub gitweb_check_feature { 494return(gitweb_get_feature(@_))[0]; 495} 496 497 498sub feature_bool { 499my$key=shift; 500my($val) = git_get_project_config($key,'--bool'); 501 502if(!defined$val) { 503return($_[0]); 504}elsif($valeq'true') { 505return(1); 506}elsif($valeq'false') { 507return(0); 508} 509} 510 511sub feature_snapshot { 512my(@fmts) =@_; 513 514my($val) = git_get_project_config('snapshot'); 515 516if($val) { 517@fmts= ($valeq'none'? () :split/\s*[,\s]\s*/,$val); 518} 519 520return@fmts; 521} 522 523sub feature_patches { 524my@val= (git_get_project_config('patches','--int')); 525 526if(@val) { 527return@val; 528} 529 530return($_[0]); 531} 532 533sub feature_avatar { 534my@val= (git_get_project_config('avatar')); 535 536return@val?@val:@_; 537} 538 539# checking HEAD file with -e is fragile if the repository was 540# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed 541# and then pruned. 542sub check_head_link { 543my($dir) =@_; 544my$headfile="$dir/HEAD"; 545return((-e $headfile) || 546(-l $headfile&&readlink($headfile) =~/^refs\/heads\//)); 547} 548 549sub check_export_ok { 550my($dir) =@_; 551return(check_head_link($dir) && 552(!$export_ok|| -e "$dir/$export_ok") && 553(!$export_auth_hook||$export_auth_hook->($dir))); 554} 555 556# process alternate names for backward compatibility 557# filter out unsupported (unknown) snapshot formats 558sub filter_snapshot_fmts { 559my@fmts=@_; 560 561@fmts=map{ 562exists$known_snapshot_format_aliases{$_} ? 563$known_snapshot_format_aliases{$_} :$_}@fmts; 564@fmts=grep{ 565exists$known_snapshot_formats{$_} && 566!$known_snapshot_formats{$_}{'disabled'}}@fmts; 567} 568 569our$GITWEB_CONFIG=$ENV{'GITWEB_CONFIG'} ||"++GITWEB_CONFIG++"; 570our$GITWEB_CONFIG_SYSTEM=$ENV{'GITWEB_CONFIG_SYSTEM'} ||"++GITWEB_CONFIG_SYSTEM++"; 571# die if there are errors parsing config file 572if(-e $GITWEB_CONFIG) { 573do$GITWEB_CONFIG; 574die$@if$@; 575}elsif(-e $GITWEB_CONFIG_SYSTEM) { 576do$GITWEB_CONFIG_SYSTEM; 577die$@if$@; 578} 579 580# Get loadavg of system, to compare against $maxload. 581# Currently it requires '/proc/loadavg' present to get loadavg; 582# if it is not present it returns 0, which means no load checking. 583sub get_loadavg { 584if( -e '/proc/loadavg'){ 585open my$fd,'<','/proc/loadavg' 586orreturn0; 587my@load=split(/\s+/,scalar<$fd>); 588close$fd; 589 590# The first three columns measure CPU and IO utilization of the last one, 591# five, and 10 minute periods. The fourth column shows the number of 592# currently running processes and the total number of processes in the m/n 593# format. The last column displays the last process ID used. 594return$load[0] ||0; 595} 596# additional checks for load average should go here for things that don't export 597# /proc/loadavg 598 599return0; 600} 601 602# version of the core git binary 603our$git_version=qx("$GIT" --version)=~m/git version (.*)$/?$1:"unknown"; 604$number_of_git_cmds++; 605 606$projects_list||=$projectroot; 607 608if(defined$maxload&& get_loadavg() >$maxload) { 609 die_error(503,"The load average on the server is too high"); 610} 611 612# ====================================================================== 613# input validation and dispatch 614 615# input parameters can be collected from a variety of sources (presently, CGI 616# and PATH_INFO), so we define an %input_params hash that collects them all 617# together during validation: this allows subsequent uses (e.g. href()) to be 618# agnostic of the parameter origin 619 620our%input_params= (); 621 622# input parameters are stored with the long parameter name as key. This will 623# also be used in the href subroutine to convert parameters to their CGI 624# equivalent, and since the href() usage is the most frequent one, we store 625# the name -> CGI key mapping here, instead of the reverse. 626# 627# XXX: Warning: If you touch this, check the search form for updating, 628# too. 629 630our@cgi_param_mapping= ( 631 project =>"p", 632 action =>"a", 633 file_name =>"f", 634 file_parent =>"fp", 635 hash =>"h", 636 hash_parent =>"hp", 637 hash_base =>"hb", 638 hash_parent_base =>"hpb", 639 page =>"pg", 640 order =>"o", 641 searchtext =>"s", 642 searchtype =>"st", 643 snapshot_format =>"sf", 644 extra_options =>"opt", 645 search_use_regexp =>"sr", 646# this must be last entry (for manipulation from JavaScript) 647 javascript =>"js" 648); 649our%cgi_param_mapping=@cgi_param_mapping; 650 651# we will also need to know the possible actions, for validation 652our%actions= ( 653"blame"=> \&git_blame, 654"blame_incremental"=> \&git_blame_incremental, 655"blame_data"=> \&git_blame_data, 656"blobdiff"=> \&git_blobdiff, 657"blobdiff_plain"=> \&git_blobdiff_plain, 658"blob"=> \&git_blob, 659"blob_plain"=> \&git_blob_plain, 660"commitdiff"=> \&git_commitdiff, 661"commitdiff_plain"=> \&git_commitdiff_plain, 662"commit"=> \&git_commit, 663"forks"=> \&git_forks, 664"heads"=> \&git_heads, 665"history"=> \&git_history, 666"log"=> \&git_log, 667"patch"=> \&git_patch, 668"patches"=> \&git_patches, 669"rss"=> \&git_rss, 670"atom"=> \&git_atom, 671"search"=> \&git_search, 672"search_help"=> \&git_search_help, 673"shortlog"=> \&git_shortlog, 674"summary"=> \&git_summary, 675"tag"=> \&git_tag, 676"tags"=> \&git_tags, 677"tree"=> \&git_tree, 678"snapshot"=> \&git_snapshot, 679"object"=> \&git_object, 680# those below don't need $project 681"opml"=> \&git_opml, 682"project_list"=> \&git_project_list, 683"project_index"=> \&git_project_index, 684); 685 686# finally, we have the hash of allowed extra_options for the commands that 687# allow them 688our%allowed_options= ( 689"--no-merges"=> [qw(rss atom log shortlog history)], 690); 691 692# fill %input_params with the CGI parameters. All values except for 'opt' 693# should be single values, but opt can be an array. We should probably 694# build an array of parameters that can be multi-valued, but since for the time 695# being it's only this one, we just single it out 696while(my($name,$symbol) =each%cgi_param_mapping) { 697if($symboleq'opt') { 698$input_params{$name} = [$cgi->param($symbol) ]; 699}else{ 700$input_params{$name} =$cgi->param($symbol); 701} 702} 703 704# now read PATH_INFO and update the parameter list for missing parameters 705sub evaluate_path_info { 706return ifdefined$input_params{'project'}; 707return if!$path_info; 708$path_info=~ s,^/+,,; 709return if!$path_info; 710 711# find which part of PATH_INFO is project 712my$project=$path_info; 713$project=~ s,/+$,,; 714while($project&& !check_head_link("$projectroot/$project")) { 715$project=~ s,/*[^/]*$,,; 716} 717return unless$project; 718$input_params{'project'} =$project; 719 720# do not change any parameters if an action is given using the query string 721return if$input_params{'action'}; 722$path_info=~ s,^\Q$project\E/*,,; 723 724# next, check if we have an action 725my$action=$path_info; 726$action=~ s,/.*$,,; 727if(exists$actions{$action}) { 728$path_info=~ s,^$action/*,,; 729$input_params{'action'} =$action; 730} 731 732# list of actions that want hash_base instead of hash, but can have no 733# pathname (f) parameter 734my@wants_base= ( 735'tree', 736'history', 737); 738 739# we want to catch 740# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] 741my($parentrefname,$parentpathname,$refname,$pathname) = 742($path_info=~/^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); 743 744# first, analyze the 'current' part 745if(defined$pathname) { 746# we got "branch:filename" or "branch:dir/" 747# we could use git_get_type(branch:pathname), but: 748# - it needs $git_dir 749# - it does a git() call 750# - the convention of terminating directories with a slash 751# makes it superfluous 752# - embedding the action in the PATH_INFO would make it even 753# more superfluous 754$pathname=~ s,^/+,,; 755if(!$pathname||substr($pathname, -1)eq"/") { 756$input_params{'action'} ||="tree"; 757$pathname=~ s,/$,,; 758}else{ 759# the default action depends on whether we had parent info 760# or not 761if($parentrefname) { 762$input_params{'action'} ||="blobdiff_plain"; 763}else{ 764$input_params{'action'} ||="blob_plain"; 765} 766} 767$input_params{'hash_base'} ||=$refname; 768$input_params{'file_name'} ||=$pathname; 769}elsif(defined$refname) { 770# we got "branch". In this case we have to choose if we have to 771# set hash or hash_base. 772# 773# Most of the actions without a pathname only want hash to be 774# set, except for the ones specified in @wants_base that want 775# hash_base instead. It should also be noted that hand-crafted 776# links having 'history' as an action and no pathname or hash 777# set will fail, but that happens regardless of PATH_INFO. 778$input_params{'action'} ||="shortlog"; 779if(grep{$_eq$input_params{'action'} }@wants_base) { 780$input_params{'hash_base'} ||=$refname; 781}else{ 782$input_params{'hash'} ||=$refname; 783} 784} 785 786# next, handle the 'parent' part, if present 787if(defined$parentrefname) { 788# a missing pathspec defaults to the 'current' filename, allowing e.g. 789# someproject/blobdiff/oldrev..newrev:/filename 790if($parentpathname) { 791$parentpathname=~ s,^/+,,; 792$parentpathname=~ s,/$,,; 793$input_params{'file_parent'} ||=$parentpathname; 794}else{ 795$input_params{'file_parent'} ||=$input_params{'file_name'}; 796} 797# we assume that hash_parent_base is wanted if a path was specified, 798# or if the action wants hash_base instead of hash 799if(defined$input_params{'file_parent'} || 800grep{$_eq$input_params{'action'} }@wants_base) { 801$input_params{'hash_parent_base'} ||=$parentrefname; 802}else{ 803$input_params{'hash_parent'} ||=$parentrefname; 804} 805} 806 807# for the snapshot action, we allow URLs in the form 808# $project/snapshot/$hash.ext 809# where .ext determines the snapshot and gets removed from the 810# passed $refname to provide the $hash. 811# 812# To be able to tell that $refname includes the format extension, we 813# require the following two conditions to be satisfied: 814# - the hash input parameter MUST have been set from the $refname part 815# of the URL (i.e. they must be equal) 816# - the snapshot format MUST NOT have been defined already (e.g. from 817# CGI parameter sf) 818# It's also useless to try any matching unless $refname has a dot, 819# so we check for that too 820if(defined$input_params{'action'} && 821$input_params{'action'}eq'snapshot'&& 822defined$refname&&index($refname,'.') != -1&& 823$refnameeq$input_params{'hash'} && 824!defined$input_params{'snapshot_format'}) { 825# We loop over the known snapshot formats, checking for 826# extensions. Allowed extensions are both the defined suffix 827# (which includes the initial dot already) and the snapshot 828# format key itself, with a prepended dot 829while(my($fmt,$opt) =each%known_snapshot_formats) { 830my$hash=$refname; 831unless($hash=~s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) { 832next; 833} 834my$sfx=$1; 835# a valid suffix was found, so set the snapshot format 836# and reset the hash parameter 837$input_params{'snapshot_format'} =$fmt; 838$input_params{'hash'} =$hash; 839# we also set the format suffix to the one requested 840# in the URL: this way a request for e.g. .tgz returns 841# a .tgz instead of a .tar.gz 842$known_snapshot_formats{$fmt}{'suffix'} =$sfx; 843last; 844} 845} 846} 847evaluate_path_info(); 848 849our$action=$input_params{'action'}; 850if(defined$action) { 851if(!validate_action($action)) { 852 die_error(400,"Invalid action parameter"); 853} 854} 855 856# parameters which are pathnames 857our$project=$input_params{'project'}; 858if(defined$project) { 859if(!validate_project($project)) { 860undef$project; 861 die_error(404,"No such project"); 862} 863} 864 865our$file_name=$input_params{'file_name'}; 866if(defined$file_name) { 867if(!validate_pathname($file_name)) { 868 die_error(400,"Invalid file parameter"); 869} 870} 871 872our$file_parent=$input_params{'file_parent'}; 873if(defined$file_parent) { 874if(!validate_pathname($file_parent)) { 875 die_error(400,"Invalid file parent parameter"); 876} 877} 878 879# parameters which are refnames 880our$hash=$input_params{'hash'}; 881if(defined$hash) { 882if(!validate_refname($hash)) { 883 die_error(400,"Invalid hash parameter"); 884} 885} 886 887our$hash_parent=$input_params{'hash_parent'}; 888if(defined$hash_parent) { 889if(!validate_refname($hash_parent)) { 890 die_error(400,"Invalid hash parent parameter"); 891} 892} 893 894our$hash_base=$input_params{'hash_base'}; 895if(defined$hash_base) { 896if(!validate_refname($hash_base)) { 897 die_error(400,"Invalid hash base parameter"); 898} 899} 900 901our@extra_options= @{$input_params{'extra_options'}}; 902# @extra_options is always defined, since it can only be (currently) set from 903# CGI, and $cgi->param() returns the empty array in array context if the param 904# is not set 905foreachmy$opt(@extra_options) { 906if(not exists$allowed_options{$opt}) { 907 die_error(400,"Invalid option parameter"); 908} 909if(not grep(/^$action$/, @{$allowed_options{$opt}})) { 910 die_error(400,"Invalid option parameter for this action"); 911} 912} 913 914our$hash_parent_base=$input_params{'hash_parent_base'}; 915if(defined$hash_parent_base) { 916if(!validate_refname($hash_parent_base)) { 917 die_error(400,"Invalid hash parent base parameter"); 918} 919} 920 921# other parameters 922our$page=$input_params{'page'}; 923if(defined$page) { 924if($page=~m/[^0-9]/) { 925 die_error(400,"Invalid page parameter"); 926} 927} 928 929our$searchtype=$input_params{'searchtype'}; 930if(defined$searchtype) { 931if($searchtype=~m/[^a-z]/) { 932 die_error(400,"Invalid searchtype parameter"); 933} 934} 935 936our$search_use_regexp=$input_params{'search_use_regexp'}; 937 938our$searchtext=$input_params{'searchtext'}; 939our$search_regexp; 940if(defined$searchtext) { 941if(length($searchtext) <2) { 942 die_error(403,"At least two characters are required for search parameter"); 943} 944$search_regexp=$search_use_regexp?$searchtext:quotemeta$searchtext; 945} 946 947# path to the current git repository 948our$git_dir; 949$git_dir="$projectroot/$project"if$project; 950 951# list of supported snapshot formats 952our@snapshot_fmts= gitweb_get_feature('snapshot'); 953@snapshot_fmts= filter_snapshot_fmts(@snapshot_fmts); 954 955# check that the avatar feature is set to a known provider name, 956# and for each provider check if the dependencies are satisfied. 957# if the provider name is invalid or the dependencies are not met, 958# reset $git_avatar to the empty string. 959our($git_avatar) = gitweb_get_feature('avatar'); 960if($git_avatareq'gravatar') { 961$git_avatar=''unless(eval{require Digest::MD5;1; }); 962}elsif($git_avatareq'picon') { 963# no dependencies 964}else{ 965$git_avatar=''; 966} 967 968# custom error handler: 'die <message>' is Internal Server Error 969sub handle_errors_html { 970my$msg=shift;# it is already HTML escaped 971 972# to avoid infinite loop where error occurs in die_error, 973# change handler to default handler, disabling handle_errors_html 974 set_message("Error occured when inside die_error:\n$msg"); 975 976# you cannot jump out of die_error when called as error handler; 977# the subroutine set via CGI::Carp::set_message is called _after_ 978# HTTP headers are already written, so it cannot write them itself 979 die_error(undef,undef,$msg, -error_handler =>1, -no_http_header =>1); 980} 981set_message(\&handle_errors_html); 982 983# dispatch 984if(!defined$action) { 985if(defined$hash) { 986$action= git_get_type($hash); 987}elsif(defined$hash_base&&defined$file_name) { 988$action= git_get_type("$hash_base:$file_name"); 989}elsif(defined$project) { 990$action='summary'; 991}else{ 992$action='project_list'; 993} 994} 995if(!defined($actions{$action})) { 996 die_error(400,"Unknown action"); 997} 998if($action!~m/^(?:opml|project_list|project_index)$/&& 999!$project) {1000 die_error(400,"Project needed");1001}1002$actions{$action}->();1003DONE_GITWEB:10041;10051006## ======================================================================1007## action links10081009# possible values of extra options1010# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)1011# -replay => 1 - start from a current view (replay with modifications)1012# -path_info => 0|1 - don't use/use path_info URL (if possible)1013sub href {1014my%params=@_;1015# default is to use -absolute url() i.e. $my_uri1016my$href=$params{-full} ?$my_url:$my_uri;10171018$params{'project'} =$projectunlessexists$params{'project'};10191020if($params{-replay}) {1021while(my($name,$symbol) =each%cgi_param_mapping) {1022if(!exists$params{$name}) {1023$params{$name} =$input_params{$name};1024}1025}1026}10271028my$use_pathinfo= gitweb_check_feature('pathinfo');1029if(defined$params{'project'} &&1030(exists$params{-path_info} ?$params{-path_info} :$use_pathinfo)) {1031# try to put as many parameters as possible in PATH_INFO:1032# - project name1033# - action1034# - hash_parent or hash_parent_base:/file_parent1035# - hash or hash_base:/filename1036# - the snapshot_format as an appropriate suffix10371038# When the script is the root DirectoryIndex for the domain,1039# $href here would be something like http://gitweb.example.com/1040# Thus, we strip any trailing / from $href, to spare us double1041# slashes in the final URL1042$href=~ s,/$,,;10431044# Then add the project name, if present1045$href.="/".esc_url($params{'project'});1046delete$params{'project'};10471048# since we destructively absorb parameters, we keep this1049# boolean that remembers if we're handling a snapshot1050my$is_snapshot=$params{'action'}eq'snapshot';10511052# Summary just uses the project path URL, any other action is1053# added to the URL1054if(defined$params{'action'}) {1055$href.="/".esc_url($params{'action'})unless$params{'action'}eq'summary';1056delete$params{'action'};1057}10581059# Next, we put hash_parent_base:/file_parent..hash_base:/file_name,1060# stripping nonexistent or useless pieces1061$href.="/"if($params{'hash_base'} ||$params{'hash_parent_base'}1062||$params{'hash_parent'} ||$params{'hash'});1063if(defined$params{'hash_base'}) {1064if(defined$params{'hash_parent_base'}) {1065$href.= esc_url($params{'hash_parent_base'});1066# skip the file_parent if it's the same as the file_name1067if(defined$params{'file_parent'}) {1068if(defined$params{'file_name'} &&$params{'file_parent'}eq$params{'file_name'}) {1069delete$params{'file_parent'};1070}elsif($params{'file_parent'} !~/\.\./) {1071$href.=":/".esc_url($params{'file_parent'});1072delete$params{'file_parent'};1073}1074}1075$href.="..";1076delete$params{'hash_parent'};1077delete$params{'hash_parent_base'};1078}elsif(defined$params{'hash_parent'}) {1079$href.= esc_url($params{'hash_parent'})."..";1080delete$params{'hash_parent'};1081}10821083$href.= esc_url($params{'hash_base'});1084if(defined$params{'file_name'} &&$params{'file_name'} !~/\.\./) {1085$href.=":/".esc_url($params{'file_name'});1086delete$params{'file_name'};1087}1088delete$params{'hash'};1089delete$params{'hash_base'};1090}elsif(defined$params{'hash'}) {1091$href.= esc_url($params{'hash'});1092delete$params{'hash'};1093}10941095# If the action was a snapshot, we can absorb the1096# snapshot_format parameter too1097if($is_snapshot) {1098my$fmt=$params{'snapshot_format'};1099# snapshot_format should always be defined when href()1100# is called, but just in case some code forgets, we1101# fall back to the default1102$fmt||=$snapshot_fmts[0];1103$href.=$known_snapshot_formats{$fmt}{'suffix'};1104delete$params{'snapshot_format'};1105}1106}11071108# now encode the parameters explicitly1109my@result= ();1110for(my$i=0;$i<@cgi_param_mapping;$i+=2) {1111my($name,$symbol) = ($cgi_param_mapping[$i],$cgi_param_mapping[$i+1]);1112if(defined$params{$name}) {1113if(ref($params{$name})eq"ARRAY") {1114foreachmy$par(@{$params{$name}}) {1115push@result,$symbol."=". esc_param($par);1116}1117}else{1118push@result,$symbol."=". esc_param($params{$name});1119}1120}1121}1122$href.="?".join(';',@result)ifscalar@result;11231124return$href;1125}112611271128## ======================================================================1129## validation, quoting/unquoting and escaping11301131sub validate_action {1132my$input=shift||returnundef;1133returnundefunlessexists$actions{$input};1134return$input;1135}11361137sub validate_project {1138my$input=shift||returnundef;1139if(!validate_pathname($input) ||1140!(-d "$projectroot/$input") ||1141!check_export_ok("$projectroot/$input") ||1142($strict_export&& !project_in_list($input))) {1143returnundef;1144}else{1145return$input;1146}1147}11481149sub validate_pathname {1150my$input=shift||returnundef;11511152# no '.' or '..' as elements of path, i.e. no '.' nor '..'1153# at the beginning, at the end, and between slashes.1154# also this catches doubled slashes1155if($input=~m!(^|/)(|\.|\.\.)(/|$)!) {1156returnundef;1157}1158# no null characters1159if($input=~m!\0!) {1160returnundef;1161}1162return$input;1163}11641165sub validate_refname {1166my$input=shift||returnundef;11671168# textual hashes are O.K.1169if($input=~m/^[0-9a-fA-F]{40}$/) {1170return$input;1171}1172# it must be correct pathname1173$input= validate_pathname($input)1174orreturnundef;1175# restrictions on ref name according to git-check-ref-format1176if($input=~m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {1177returnundef;1178}1179return$input;1180}11811182# decode sequences of octets in utf8 into Perl's internal form,1183# which is utf-8 with utf8 flag set if needed. gitweb writes out1184# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning1185sub to_utf8 {1186my$str=shift;1187returnundefunlessdefined$str;1188if(utf8::valid($str)) {1189 utf8::decode($str);1190return$str;1191}else{1192return decode($fallback_encoding,$str, Encode::FB_DEFAULT);1193}1194}11951196# quote unsafe chars, but keep the slash, even when it's not1197# correct, but quoted slashes look too horrible in bookmarks1198sub esc_param {1199my$str=shift;1200returnundefunlessdefined$str;1201$str=~s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;1202$str=~s/ /\+/g;1203return$str;1204}12051206# quote unsafe chars in whole URL, so some charactrs cannot be quoted1207sub esc_url {1208my$str=shift;1209returnundefunlessdefined$str;1210$str=~s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X",ord($1))/eg;1211$str=~s/\+/%2B/g;1212$str=~s/ /\+/g;1213return$str;1214}12151216# replace invalid utf8 character with SUBSTITUTION sequence1217sub esc_html {1218my$str=shift;1219my%opts=@_;12201221returnundefunlessdefined$str;12221223$str= to_utf8($str);1224$str=$cgi->escapeHTML($str);1225if($opts{'-nbsp'}) {1226$str=~s/ / /g;1227}1228$str=~ s|([[:cntrl:]])|(($1ne"\t") ? quot_cec($1) :$1)|eg;1229return$str;1230}12311232# quote control characters and escape filename to HTML1233sub esc_path {1234my$str=shift;1235my%opts=@_;12361237returnundefunlessdefined$str;12381239$str= to_utf8($str);1240$str=$cgi->escapeHTML($str);1241if($opts{'-nbsp'}) {1242$str=~s/ / /g;1243}1244$str=~ s|([[:cntrl:]])|quot_cec($1)|eg;1245return$str;1246}12471248# Make control characters "printable", using character escape codes (CEC)1249sub quot_cec {1250my$cntrl=shift;1251my%opts=@_;1252my%es= (# character escape codes, aka escape sequences1253"\t"=>'\t',# tab (HT)1254"\n"=>'\n',# line feed (LF)1255"\r"=>'\r',# carrige return (CR)1256"\f"=>'\f',# form feed (FF)1257"\b"=>'\b',# backspace (BS)1258"\a"=>'\a',# alarm (bell) (BEL)1259"\e"=>'\e',# escape (ESC)1260"\013"=>'\v',# vertical tab (VT)1261"\000"=>'\0',# nul character (NUL)1262);1263my$chr= ( (exists$es{$cntrl})1264?$es{$cntrl}1265:sprintf('\%2x',ord($cntrl)) );1266if($opts{-nohtml}) {1267return$chr;1268}else{1269return"<span class=\"cntrl\">$chr</span>";1270}1271}12721273# Alternatively use unicode control pictures codepoints,1274# Unicode "printable representation" (PR)1275sub quot_upr {1276my$cntrl=shift;1277my%opts=@_;12781279my$chr=sprintf('&#%04d;',0x2400+ord($cntrl));1280if($opts{-nohtml}) {1281return$chr;1282}else{1283return"<span class=\"cntrl\">$chr</span>";1284}1285}12861287# git may return quoted and escaped filenames1288sub unquote {1289my$str=shift;12901291sub unq {1292my$seq=shift;1293my%es= (# character escape codes, aka escape sequences1294't'=>"\t",# tab (HT, TAB)1295'n'=>"\n",# newline (NL)1296'r'=>"\r",# return (CR)1297'f'=>"\f",# form feed (FF)1298'b'=>"\b",# backspace (BS)1299'a'=>"\a",# alarm (bell) (BEL)1300'e'=>"\e",# escape (ESC)1301'v'=>"\013",# vertical tab (VT)1302);13031304if($seq=~m/^[0-7]{1,3}$/) {1305# octal char sequence1306returnchr(oct($seq));1307}elsif(exists$es{$seq}) {1308# C escape sequence, aka character escape code1309return$es{$seq};1310}1311# quoted ordinary character1312return$seq;1313}13141315if($str=~m/^"(.*)"$/) {1316# needs unquoting1317$str=$1;1318$str=~s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;1319}1320return$str;1321}13221323# escape tabs (convert tabs to spaces)1324sub untabify {1325my$line=shift;13261327while((my$pos=index($line,"\t")) != -1) {1328if(my$count= (8- ($pos%8))) {1329my$spaces=' ' x $count;1330$line=~s/\t/$spaces/;1331}1332}13331334return$line;1335}13361337sub project_in_list {1338my$project=shift;1339my@list= git_get_projects_list();1340return@list&&scalar(grep{$_->{'path'}eq$project}@list);1341}13421343## ----------------------------------------------------------------------1344## HTML aware string manipulation13451346# Try to chop given string on a word boundary between position1347# $len and $len+$add_len. If there is no word boundary there,1348# chop at $len+$add_len. Do not chop if chopped part plus ellipsis1349# (marking chopped part) would be longer than given string.1350sub chop_str {1351my$str=shift;1352my$len=shift;1353my$add_len=shift||10;1354my$where=shift||'right';# 'left' | 'center' | 'right'13551356# Make sure perl knows it is utf8 encoded so we don't1357# cut in the middle of a utf8 multibyte char.1358$str= to_utf8($str);13591360# allow only $len chars, but don't cut a word if it would fit in $add_len1361# if it doesn't fit, cut it if it's still longer than the dots we would add1362# remove chopped character entities entirely13631364# when chopping in the middle, distribute $len into left and right part1365# return early if chopping wouldn't make string shorter1366if($whereeq'center') {1367return$strif($len+5>=length($str));# filler is length 51368$len=int($len/2);1369}else{1370return$strif($len+4>=length($str));# filler is length 41371}13721373# regexps: ending and beginning with word part up to $add_len1374my$endre=qr/.{$len}\w{0,$add_len}/;1375my$begre=qr/\w{0,$add_len}.{$len}/;13761377if($whereeq'left') {1378$str=~m/^(.*?)($begre)$/;1379my($lead,$body) = ($1,$2);1380if(length($lead) >4) {1381$lead=" ...";1382}1383return"$lead$body";13841385}elsif($whereeq'center') {1386$str=~m/^($endre)(.*)$/;1387my($left,$str) = ($1,$2);1388$str=~m/^(.*?)($begre)$/;1389my($mid,$right) = ($1,$2);1390if(length($mid) >5) {1391$mid=" ... ";1392}1393return"$left$mid$right";13941395}else{1396$str=~m/^($endre)(.*)$/;1397my$body=$1;1398my$tail=$2;1399if(length($tail) >4) {1400$tail="... ";1401}1402return"$body$tail";1403}1404}14051406# takes the same arguments as chop_str, but also wraps a <span> around the1407# result with a title attribute if it does get chopped. Additionally, the1408# string is HTML-escaped.1409sub chop_and_escape_str {1410my($str) =@_;14111412my$chopped= chop_str(@_);1413if($choppedeq$str) {1414return esc_html($chopped);1415}else{1416$str=~s/[[:cntrl:]]/?/g;1417return$cgi->span({-title=>$str}, esc_html($chopped));1418}1419}14201421## ----------------------------------------------------------------------1422## functions returning short strings14231424# CSS class for given age value (in seconds)1425sub age_class {1426my$age=shift;14271428if(!defined$age) {1429return"noage";1430}elsif($age<60*60*2) {1431return"age0";1432}elsif($age<60*60*24*2) {1433return"age1";1434}else{1435return"age2";1436}1437}14381439# convert age in seconds to "nn units ago" string1440sub age_string {1441my$age=shift;1442my$age_str;14431444if($age>60*60*24*365*2) {1445$age_str= (int$age/60/60/24/365);1446$age_str.=" years ago";1447}elsif($age>60*60*24*(365/12)*2) {1448$age_str=int$age/60/60/24/(365/12);1449$age_str.=" months ago";1450}elsif($age>60*60*24*7*2) {1451$age_str=int$age/60/60/24/7;1452$age_str.=" weeks ago";1453}elsif($age>60*60*24*2) {1454$age_str=int$age/60/60/24;1455$age_str.=" days ago";1456}elsif($age>60*60*2) {1457$age_str=int$age/60/60;1458$age_str.=" hours ago";1459}elsif($age>60*2) {1460$age_str=int$age/60;1461$age_str.=" min ago";1462}elsif($age>2) {1463$age_str=int$age;1464$age_str.=" sec ago";1465}else{1466$age_str.=" right now";1467}1468return$age_str;1469}14701471useconstant{1472 S_IFINVALID =>0030000,1473 S_IFGITLINK =>0160000,1474};14751476# submodule/subproject, a commit object reference1477sub S_ISGITLINK {1478my$mode=shift;14791480return(($mode& S_IFMT) == S_IFGITLINK)1481}14821483# convert file mode in octal to symbolic file mode string1484sub mode_str {1485my$mode=oct shift;14861487if(S_ISGITLINK($mode)) {1488return'm---------';1489}elsif(S_ISDIR($mode& S_IFMT)) {1490return'drwxr-xr-x';1491}elsif(S_ISLNK($mode)) {1492return'lrwxrwxrwx';1493}elsif(S_ISREG($mode)) {1494# git cares only about the executable bit1495if($mode& S_IXUSR) {1496return'-rwxr-xr-x';1497}else{1498return'-rw-r--r--';1499};1500}else{1501return'----------';1502}1503}15041505# convert file mode in octal to file type string1506sub file_type {1507my$mode=shift;15081509if($mode!~m/^[0-7]+$/) {1510return$mode;1511}else{1512$mode=oct$mode;1513}15141515if(S_ISGITLINK($mode)) {1516return"submodule";1517}elsif(S_ISDIR($mode& S_IFMT)) {1518return"directory";1519}elsif(S_ISLNK($mode)) {1520return"symlink";1521}elsif(S_ISREG($mode)) {1522return"file";1523}else{1524return"unknown";1525}1526}15271528# convert file mode in octal to file type description string1529sub file_type_long {1530my$mode=shift;15311532if($mode!~m/^[0-7]+$/) {1533return$mode;1534}else{1535$mode=oct$mode;1536}15371538if(S_ISGITLINK($mode)) {1539return"submodule";1540}elsif(S_ISDIR($mode& S_IFMT)) {1541return"directory";1542}elsif(S_ISLNK($mode)) {1543return"symlink";1544}elsif(S_ISREG($mode)) {1545if($mode& S_IXUSR) {1546return"executable";1547}else{1548return"file";1549};1550}else{1551return"unknown";1552}1553}155415551556## ----------------------------------------------------------------------1557## functions returning short HTML fragments, or transforming HTML fragments1558## which don't belong to other sections15591560# format line of commit message.1561sub format_log_line_html {1562my$line=shift;15631564$line= esc_html($line, -nbsp=>1);1565$line=~ s{\b([0-9a-fA-F]{8,40})\b}{1566$cgi->a({-href => href(action=>"object", hash=>$1),1567-class=>"text"},$1);1568}eg;15691570return$line;1571}15721573# format marker of refs pointing to given object15741575# the destination action is chosen based on object type and current context:1576# - for annotated tags, we choose the tag view unless it's the current view1577# already, in which case we go to shortlog view1578# - for other refs, we keep the current view if we're in history, shortlog or1579# log view, and select shortlog otherwise1580sub format_ref_marker {1581my($refs,$id) =@_;1582my$markers='';15831584if(defined$refs->{$id}) {1585foreachmy$ref(@{$refs->{$id}}) {1586# this code exploits the fact that non-lightweight tags are the1587# only indirect objects, and that they are the only objects for which1588# we want to use tag instead of shortlog as action1589my($type,$name) =qw();1590my$indirect= ($ref=~s/\^\{\}$//);1591# e.g. tags/v2.6.11 or heads/next1592if($ref=~m!^(.*?)s?/(.*)$!) {1593$type=$1;1594$name=$2;1595}else{1596$type="ref";1597$name=$ref;1598}15991600my$class=$type;1601$class.=" indirect"if$indirect;16021603my$dest_action="shortlog";16041605if($indirect) {1606$dest_action="tag"unless$actioneq"tag";1607}elsif($action=~/^(history|(short)?log)$/) {1608$dest_action=$action;1609}16101611my$dest="";1612$dest.="refs/"unless$ref=~ m!^refs/!;1613$dest.=$ref;16141615my$link=$cgi->a({1616-href => href(1617 action=>$dest_action,1618 hash=>$dest1619)},$name);16201621$markers.=" <span class=\"$class\"title=\"$ref\">".1622$link."</span>";1623}1624}16251626if($markers) {1627return' <span class="refs">'.$markers.'</span>';1628}else{1629return"";1630}1631}16321633# format, perhaps shortened and with markers, title line1634sub format_subject_html {1635my($long,$short,$href,$extra) =@_;1636$extra=''unlessdefined($extra);16371638if(length($short) <length($long)) {1639$long=~s/[[:cntrl:]]/?/g;1640return$cgi->a({-href =>$href, -class=>"list subject",1641-title => to_utf8($long)},1642 esc_html($short)) .$extra;1643}else{1644return$cgi->a({-href =>$href, -class=>"list subject"},1645 esc_html($long)) .$extra;1646}1647}16481649# Rather than recomputing the url for an email multiple times, we cache it1650# after the first hit. This gives a visible benefit in views where the avatar1651# for the same email is used repeatedly (e.g. shortlog).1652# The cache is shared by all avatar engines (currently gravatar only), which1653# are free to use it as preferred. Since only one avatar engine is used for any1654# given page, there's no risk for cache conflicts.1655our%avatar_cache= ();16561657# Compute the picon url for a given email, by using the picon search service over at1658# http://www.cs.indiana.edu/picons/search.html1659sub picon_url {1660my$email=lc shift;1661if(!$avatar_cache{$email}) {1662my($user,$domain) =split('@',$email);1663$avatar_cache{$email} =1664"http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/".1665"$domain/$user/".1666"users+domains+unknown/up/single";1667}1668return$avatar_cache{$email};1669}16701671# Compute the gravatar url for a given email, if it's not in the cache already.1672# Gravatar stores only the part of the URL before the size, since that's the1673# one computationally more expensive. This also allows reuse of the cache for1674# different sizes (for this particular engine).1675sub gravatar_url {1676my$email=lc shift;1677my$size=shift;1678$avatar_cache{$email} ||=1679"http://www.gravatar.com/avatar/".1680 Digest::MD5::md5_hex($email) ."?s=";1681return$avatar_cache{$email} .$size;1682}16831684# Insert an avatar for the given $email at the given $size if the feature1685# is enabled.1686sub git_get_avatar {1687my($email,%opts) =@_;1688my$pre_white= ($opts{-pad_before} ?" ":"");1689my$post_white= ($opts{-pad_after} ?" ":"");1690$opts{-size} ||='default';1691my$size=$avatar_size{$opts{-size}} ||$avatar_size{'default'};1692my$url="";1693if($git_avatareq'gravatar') {1694$url= gravatar_url($email,$size);1695}elsif($git_avatareq'picon') {1696$url= picon_url($email);1697}1698# Other providers can be added by extending the if chain, defining $url1699# as needed. If no variant puts something in $url, we assume avatars1700# are completely disabled/unavailable.1701if($url) {1702return$pre_white.1703"<img width=\"$size\"".1704"class=\"avatar\"".1705"src=\"$url\"".1706"alt=\"\"".1707"/>".$post_white;1708}else{1709return"";1710}1711}17121713sub format_search_author {1714my($author,$searchtype,$displaytext) =@_;1715my$have_search= gitweb_check_feature('search');17161717if($have_search) {1718my$performed="";1719if($searchtypeeq'author') {1720$performed="authored";1721}elsif($searchtypeeq'committer') {1722$performed="committed";1723}17241725return$cgi->a({-href => href(action=>"search", hash=>$hash,1726 searchtext=>$author,1727 searchtype=>$searchtype),class=>"list",1728 title=>"Search for commits$performedby$author"},1729$displaytext);17301731}else{1732return$displaytext;1733}1734}17351736# format the author name of the given commit with the given tag1737# the author name is chopped and escaped according to the other1738# optional parameters (see chop_str).1739sub format_author_html {1740my$tag=shift;1741my$co=shift;1742my$author= chop_and_escape_str($co->{'author_name'},@_);1743return"<$tagclass=\"author\">".1744 format_search_author($co->{'author_name'},"author",1745 git_get_avatar($co->{'author_email'}, -pad_after =>1) .1746$author) .1747"</$tag>";1748}17491750# format git diff header line, i.e. "diff --(git|combined|cc) ..."1751sub format_git_diff_header_line {1752my$line=shift;1753my$diffinfo=shift;1754my($from,$to) =@_;17551756if($diffinfo->{'nparents'}) {1757# combined diff1758$line=~s!^(diff (.*?) )"?.*$!$1!;1759if($to->{'href'}) {1760$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1761 esc_path($to->{'file'}));1762}else{# file was deleted (no href)1763$line.= esc_path($to->{'file'});1764}1765}else{1766# "ordinary" diff1767$line=~s!^(diff (.*?) )"?a/.*$!$1!;1768if($from->{'href'}) {1769$line.=$cgi->a({-href =>$from->{'href'}, -class=>"path"},1770'a/'. esc_path($from->{'file'}));1771}else{# file was added (no href)1772$line.='a/'. esc_path($from->{'file'});1773}1774$line.=' ';1775if($to->{'href'}) {1776$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1777'b/'. esc_path($to->{'file'}));1778}else{# file was deleted1779$line.='b/'. esc_path($to->{'file'});1780}1781}17821783return"<div class=\"diff header\">$line</div>\n";1784}17851786# format extended diff header line, before patch itself1787sub format_extended_diff_header_line {1788my$line=shift;1789my$diffinfo=shift;1790my($from,$to) =@_;17911792# match <path>1793if($line=~s!^((copy|rename) from ).*$!$1!&&$from->{'href'}) {1794$line.=$cgi->a({-href=>$from->{'href'}, -class=>"path"},1795 esc_path($from->{'file'}));1796}1797if($line=~s!^((copy|rename) to ).*$!$1!&&$to->{'href'}) {1798$line.=$cgi->a({-href=>$to->{'href'}, -class=>"path"},1799 esc_path($to->{'file'}));1800}1801# match single <mode>1802if($line=~m/\s(\d{6})$/) {1803$line.='<span class="info"> ('.1804 file_type_long($1) .1805')</span>';1806}1807# match <hash>1808if($line=~m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {1809# can match only for combined diff1810$line='index ';1811for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1812if($from->{'href'}[$i]) {1813$line.=$cgi->a({-href=>$from->{'href'}[$i],1814-class=>"hash"},1815substr($diffinfo->{'from_id'}[$i],0,7));1816}else{1817$line.='0' x 7;1818}1819# separator1820$line.=','if($i<$diffinfo->{'nparents'} -1);1821}1822$line.='..';1823if($to->{'href'}) {1824$line.=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1825substr($diffinfo->{'to_id'},0,7));1826}else{1827$line.='0' x 7;1828}18291830}elsif($line=~m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {1831# can match only for ordinary diff1832my($from_link,$to_link);1833if($from->{'href'}) {1834$from_link=$cgi->a({-href=>$from->{'href'}, -class=>"hash"},1835substr($diffinfo->{'from_id'},0,7));1836}else{1837$from_link='0' x 7;1838}1839if($to->{'href'}) {1840$to_link=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1841substr($diffinfo->{'to_id'},0,7));1842}else{1843$to_link='0' x 7;1844}1845my($from_id,$to_id) = ($diffinfo->{'from_id'},$diffinfo->{'to_id'});1846$line=~s!$from_id\.\.$to_id!$from_link..$to_link!;1847}18481849return$line."<br/>\n";1850}18511852# format from-file/to-file diff header1853sub format_diff_from_to_header {1854my($from_line,$to_line,$diffinfo,$from,$to,@parents) =@_;1855my$line;1856my$result='';18571858$line=$from_line;1859#assert($line =~ m/^---/) if DEBUG;1860# no extra formatting for "^--- /dev/null"1861if(!$diffinfo->{'nparents'}) {1862# ordinary (single parent) diff1863if($line=~m!^--- "?a/!) {1864if($from->{'href'}) {1865$line='--- a/'.1866$cgi->a({-href=>$from->{'href'}, -class=>"path"},1867 esc_path($from->{'file'}));1868}else{1869$line='--- a/'.1870 esc_path($from->{'file'});1871}1872}1873$result.= qq!<div class="diff from_file">$line</div>\n!;18741875}else{1876# combined diff (merge commit)1877for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1878if($from->{'href'}[$i]) {1879$line='--- '.1880$cgi->a({-href=>href(action=>"blobdiff",1881 hash_parent=>$diffinfo->{'from_id'}[$i],1882 hash_parent_base=>$parents[$i],1883 file_parent=>$from->{'file'}[$i],1884 hash=>$diffinfo->{'to_id'},1885 hash_base=>$hash,1886 file_name=>$to->{'file'}),1887-class=>"path",1888-title=>"diff". ($i+1)},1889$i+1) .1890'/'.1891$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},1892 esc_path($from->{'file'}[$i]));1893}else{1894$line='--- /dev/null';1895}1896$result.= qq!<div class="diff from_file">$line</div>\n!;1897}1898}18991900$line=$to_line;1901#assert($line =~ m/^\+\+\+/) if DEBUG;1902# no extra formatting for "^+++ /dev/null"1903if($line=~m!^\+\+\+ "?b/!) {1904if($to->{'href'}) {1905$line='+++ b/'.1906$cgi->a({-href=>$to->{'href'}, -class=>"path"},1907 esc_path($to->{'file'}));1908}else{1909$line='+++ b/'.1910 esc_path($to->{'file'});1911}1912}1913$result.= qq!<div class="diff to_file">$line</div>\n!;19141915return$result;1916}19171918# create note for patch simplified by combined diff1919sub format_diff_cc_simplified {1920my($diffinfo,@parents) =@_;1921my$result='';19221923$result.="<div class=\"diff header\">".1924"diff --cc ";1925if(!is_deleted($diffinfo)) {1926$result.=$cgi->a({-href => href(action=>"blob",1927 hash_base=>$hash,1928 hash=>$diffinfo->{'to_id'},1929 file_name=>$diffinfo->{'to_file'}),1930-class=>"path"},1931 esc_path($diffinfo->{'to_file'}));1932}else{1933$result.= esc_path($diffinfo->{'to_file'});1934}1935$result.="</div>\n".# class="diff header"1936"<div class=\"diff nodifferences\">".1937"Simple merge".1938"</div>\n";# class="diff nodifferences"19391940return$result;1941}19421943# format patch (diff) line (not to be used for diff headers)1944sub format_diff_line {1945my$line=shift;1946my($from,$to) =@_;1947my$diff_class="";19481949chomp$line;19501951if($from&&$to&&ref($from->{'href'})eq"ARRAY") {1952# combined diff1953my$prefix=substr($line,0,scalar@{$from->{'href'}});1954if($line=~m/^\@{3}/) {1955$diff_class=" chunk_header";1956}elsif($line=~m/^\\/) {1957$diff_class=" incomplete";1958}elsif($prefix=~tr/+/+/) {1959$diff_class=" add";1960}elsif($prefix=~tr/-/-/) {1961$diff_class=" rem";1962}1963}else{1964# assume ordinary diff1965my$char=substr($line,0,1);1966if($chareq'+') {1967$diff_class=" add";1968}elsif($chareq'-') {1969$diff_class=" rem";1970}elsif($chareq'@') {1971$diff_class=" chunk_header";1972}elsif($chareq"\\") {1973$diff_class=" incomplete";1974}1975}1976$line= untabify($line);1977if($from&&$to&&$line=~m/^\@{2} /) {1978my($from_text,$from_start,$from_lines,$to_text,$to_start,$to_lines,$section) =1979$line=~m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;19801981$from_lines=0unlessdefined$from_lines;1982$to_lines=0unlessdefined$to_lines;19831984if($from->{'href'}) {1985$from_text=$cgi->a({-href=>"$from->{'href'}#l$from_start",1986-class=>"list"},$from_text);1987}1988if($to->{'href'}) {1989$to_text=$cgi->a({-href=>"$to->{'href'}#l$to_start",1990-class=>"list"},$to_text);1991}1992$line="<span class=\"chunk_info\">@@$from_text$to_text@@</span>".1993"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";1994return"<div class=\"diff$diff_class\">$line</div>\n";1995}elsif($from&&$to&&$line=~m/^\@{3}/) {1996my($prefix,$ranges,$section) =$line=~m/^(\@+) (.*?) \@+(.*)$/;1997my(@from_text,@from_start,@from_nlines,$to_text,$to_start,$to_nlines);19981999@from_text=split(' ',$ranges);2000for(my$i=0;$i<@from_text; ++$i) {2001($from_start[$i],$from_nlines[$i]) =2002(split(',',substr($from_text[$i],1)),0);2003}20042005$to_text=pop@from_text;2006$to_start=pop@from_start;2007$to_nlines=pop@from_nlines;20082009$line="<span class=\"chunk_info\">$prefix";2010for(my$i=0;$i<@from_text; ++$i) {2011if($from->{'href'}[$i]) {2012$line.=$cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",2013-class=>"list"},$from_text[$i]);2014}else{2015$line.=$from_text[$i];2016}2017$line.=" ";2018}2019if($to->{'href'}) {2020$line.=$cgi->a({-href=>"$to->{'href'}#l$to_start",2021-class=>"list"},$to_text);2022}else{2023$line.=$to_text;2024}2025$line.="$prefix</span>".2026"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";2027return"<div class=\"diff$diff_class\">$line</div>\n";2028}2029return"<div class=\"diff$diff_class\">". esc_html($line, -nbsp=>1) ."</div>\n";2030}20312032# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",2033# linked. Pass the hash of the tree/commit to snapshot.2034sub format_snapshot_links {2035my($hash) =@_;2036my$num_fmts=@snapshot_fmts;2037if($num_fmts>1) {2038# A parenthesized list of links bearing format names.2039# e.g. "snapshot (_tar.gz_ _zip_)"2040return"snapshot (".join(' ',map2041$cgi->a({2042-href => href(2043 action=>"snapshot",2044 hash=>$hash,2045 snapshot_format=>$_2046)2047},$known_snapshot_formats{$_}{'display'})2048,@snapshot_fmts) .")";2049}elsif($num_fmts==1) {2050# A single "snapshot" link whose tooltip bears the format name.2051# i.e. "_snapshot_"2052my($fmt) =@snapshot_fmts;2053return2054$cgi->a({2055-href => href(2056 action=>"snapshot",2057 hash=>$hash,2058 snapshot_format=>$fmt2059),2060-title =>"in format:$known_snapshot_formats{$fmt}{'display'}"2061},"snapshot");2062}else{# $num_fmts == 02063returnundef;2064}2065}20662067## ......................................................................2068## functions returning values to be passed, perhaps after some2069## transformation, to other functions; e.g. returning arguments to href()20702071# returns hash to be passed to href to generate gitweb URL2072# in -title key it returns description of link2073sub get_feed_info {2074my$format=shift||'Atom';2075my%res= (action =>lc($format));20762077# feed links are possible only for project views2078return unless(defined$project);2079# some views should link to OPML, or to generic project feed,2080# or don't have specific feed yet (so they should use generic)2081return if($action=~/^(?:tags|heads|forks|tag|search)$/x);20822083my$branch;2084# branches refs uses 'refs/heads/' prefix (fullname) to differentiate2085# from tag links; this also makes possible to detect branch links2086if((defined$hash_base&&$hash_base=~m!^refs/heads/(.*)$!) ||2087(defined$hash&&$hash=~m!^refs/heads/(.*)$!)) {2088$branch=$1;2089}2090# find log type for feed description (title)2091my$type='log';2092if(defined$file_name) {2093$type="history of$file_name";2094$type.="/"if($actioneq'tree');2095$type.=" on '$branch'"if(defined$branch);2096}else{2097$type="log of$branch"if(defined$branch);2098}20992100$res{-title} =$type;2101$res{'hash'} = (defined$branch?"refs/heads/$branch":undef);2102$res{'file_name'} =$file_name;21032104return%res;2105}21062107## ----------------------------------------------------------------------2108## git utility subroutines, invoking git commands21092110# returns path to the core git executable and the --git-dir parameter as list2111sub git_cmd {2112$number_of_git_cmds++;2113return$GIT,'--git-dir='.$git_dir;2114}21152116# quote the given arguments for passing them to the shell2117# quote_command("command", "arg 1", "arg with ' and ! characters")2118# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"2119# Try to avoid using this function wherever possible.2120sub quote_command {2121returnjoin(' ',2122map{my$a=$_;$a=~s/(['!])/'\\$1'/g;"'$a'"}@_);2123}21242125# get HEAD ref of given project as hash2126sub git_get_head_hash {2127return git_get_full_hash(shift,'HEAD');2128}21292130sub git_get_full_hash {2131return git_get_hash(@_);2132}21332134sub git_get_short_hash {2135return git_get_hash(@_,'--short=7');2136}21372138sub git_get_hash {2139my($project,$hash,@options) =@_;2140my$o_git_dir=$git_dir;2141my$retval=undef;2142$git_dir="$projectroot/$project";2143if(open my$fd,'-|', git_cmd(),'rev-parse',2144'--verify','-q',@options,$hash) {2145$retval= <$fd>;2146chomp$retvalifdefined$retval;2147close$fd;2148}2149if(defined$o_git_dir) {2150$git_dir=$o_git_dir;2151}2152return$retval;2153}21542155# get type of given object2156sub git_get_type {2157my$hash=shift;21582159open my$fd,"-|", git_cmd(),"cat-file",'-t',$hashorreturn;2160my$type= <$fd>;2161close$fdorreturn;2162chomp$type;2163return$type;2164}21652166# repository configuration2167our$config_file='';2168our%config;21692170# store multiple values for single key as anonymous array reference2171# single values stored directly in the hash, not as [ <value> ]2172sub hash_set_multi {2173my($hash,$key,$value) =@_;21742175if(!exists$hash->{$key}) {2176$hash->{$key} =$value;2177}elsif(!ref$hash->{$key}) {2178$hash->{$key} = [$hash->{$key},$value];2179}else{2180push@{$hash->{$key}},$value;2181}2182}21832184# return hash of git project configuration2185# optionally limited to some section, e.g. 'gitweb'2186sub git_parse_project_config {2187my$section_regexp=shift;2188my%config;21892190local$/="\0";21912192open my$fh,"-|", git_cmd(),"config",'-z','-l',2193orreturn;21942195while(my$keyval= <$fh>) {2196chomp$keyval;2197my($key,$value) =split(/\n/,$keyval,2);21982199 hash_set_multi(\%config,$key,$value)2200if(!defined$section_regexp||$key=~/^(?:$section_regexp)\./o);2201}2202close$fh;22032204return%config;2205}22062207# convert config value to boolean: 'true' or 'false'2208# no value, number > 0, 'true' and 'yes' values are true2209# rest of values are treated as false (never as error)2210sub config_to_bool {2211my$val=shift;22122213return1if!defined$val;# section.key22142215# strip leading and trailing whitespace2216$val=~s/^\s+//;2217$val=~s/\s+$//;22182219return(($val=~/^\d+$/&&$val) ||# section.key = 12220($val=~/^(?:true|yes)$/i));# section.key = true2221}22222223# convert config value to simple decimal number2224# an optional value suffix of 'k', 'm', or 'g' will cause the value2225# to be multiplied by 1024, 1048576, or 10737418242226sub config_to_int {2227my$val=shift;22282229# strip leading and trailing whitespace2230$val=~s/^\s+//;2231$val=~s/\s+$//;22322233if(my($num,$unit) = ($val=~/^([0-9]*)([kmg])$/i)) {2234$unit=lc($unit);2235# unknown unit is treated as 12236return$num* ($uniteq'g'?1073741824:2237$uniteq'm'?1048576:2238$uniteq'k'?1024:1);2239}2240return$val;2241}22422243# convert config value to array reference, if needed2244sub config_to_multi {2245my$val=shift;22462247returnref($val) ?$val: (defined($val) ? [$val] : []);2248}22492250sub git_get_project_config {2251my($key,$type) =@_;22522253return unlessdefined$git_dir;22542255# key sanity check2256return unless($key);2257$key=~s/^gitweb\.//;2258return if($key=~m/\W/);22592260# type sanity check2261if(defined$type) {2262$type=~s/^--//;2263$type=undef2264unless($typeeq'bool'||$typeeq'int');2265}22662267# get config2268if(!defined$config_file||2269$config_filene"$git_dir/config") {2270%config= git_parse_project_config('gitweb');2271$config_file="$git_dir/config";2272}22732274# check if config variable (key) exists2275return unlessexists$config{"gitweb.$key"};22762277# ensure given type2278if(!defined$type) {2279return$config{"gitweb.$key"};2280}elsif($typeeq'bool') {2281# backward compatibility: 'git config --bool' returns true/false2282return config_to_bool($config{"gitweb.$key"}) ?'true':'false';2283}elsif($typeeq'int') {2284return config_to_int($config{"gitweb.$key"});2285}2286return$config{"gitweb.$key"};2287}22882289# get hash of given path at given ref2290sub git_get_hash_by_path {2291my$base=shift;2292my$path=shift||returnundef;2293my$type=shift;22942295$path=~ s,/+$,,;22962297open my$fd,"-|", git_cmd(),"ls-tree",$base,"--",$path2298or die_error(500,"Open git-ls-tree failed");2299my$line= <$fd>;2300close$fdorreturnundef;23012302if(!defined$line) {2303# there is no tree or hash given by $path at $base2304returnundef;2305}23062307#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2308$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;2309if(defined$type&&$typene$2) {2310# type doesn't match2311returnundef;2312}2313return$3;2314}23152316# get path of entry with given hash at given tree-ish (ref)2317# used to get 'from' filename for combined diff (merge commit) for renames2318sub git_get_path_by_hash {2319my$base=shift||return;2320my$hash=shift||return;23212322local$/="\0";23232324open my$fd,"-|", git_cmd(),"ls-tree",'-r','-t','-z',$base2325orreturnundef;2326while(my$line= <$fd>) {2327chomp$line;23282329#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'2330#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'2331if($line=~m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {2332close$fd;2333return$1;2334}2335}2336close$fd;2337returnundef;2338}23392340## ......................................................................2341## git utility functions, directly accessing git repository23422343sub git_get_project_description {2344my$path=shift;23452346$git_dir="$projectroot/$path";2347open my$fd,'<',"$git_dir/description"2348orreturn git_get_project_config('description');2349my$descr= <$fd>;2350close$fd;2351if(defined$descr) {2352chomp$descr;2353}2354return$descr;2355}23562357sub git_get_project_ctags {2358my$path=shift;2359my$ctags= {};23602361$git_dir="$projectroot/$path";2362opendir my$dh,"$git_dir/ctags"2363orreturn$ctags;2364foreach(grep{ -f $_}map{"$git_dir/ctags/$_"}readdir($dh)) {2365open my$ct,'<',$_ornext;2366my$val= <$ct>;2367chomp$val;2368close$ct;2369my$ctag=$_;$ctag=~ s#.*/##;2370$ctags->{$ctag} =$val;2371}2372closedir$dh;2373$ctags;2374}23752376sub git_populate_project_tagcloud {2377my$ctags=shift;23782379# First, merge different-cased tags; tags vote on casing2380my%ctags_lc;2381foreach(keys%$ctags) {2382$ctags_lc{lc$_}->{count} +=$ctags->{$_};2383if(not$ctags_lc{lc$_}->{topcount}2384or$ctags_lc{lc$_}->{topcount} <$ctags->{$_}) {2385$ctags_lc{lc$_}->{topcount} =$ctags->{$_};2386$ctags_lc{lc$_}->{topname} =$_;2387}2388}23892390my$cloud;2391if(eval{require HTML::TagCloud;1; }) {2392$cloud= HTML::TagCloud->new;2393foreach(sort keys%ctags_lc) {2394# Pad the title with spaces so that the cloud looks2395# less crammed.2396my$title=$ctags_lc{$_}->{topname};2397$title=~s/ / /g;2398$title=~s/^/ /g;2399$title=~s/$/ /g;2400$cloud->add($title,$home_link."?by_tag=".$_,$ctags_lc{$_}->{count});2401}2402}else{2403$cloud= \%ctags_lc;2404}2405$cloud;2406}24072408sub git_show_project_tagcloud {2409my($cloud,$count) =@_;2410print STDERR ref($cloud)."..\n";2411if(ref$cloudeq'HTML::TagCloud') {2412return$cloud->html_and_css($count);2413}else{2414my@tags=sort{$cloud->{$a}->{count} <=>$cloud->{$b}->{count} }keys%$cloud;2415return'<p align="center">'.join(', ',map{2416"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"2417}splice(@tags,0,$count)) .'</p>';2418}2419}24202421sub git_get_project_url_list {2422my$path=shift;24232424$git_dir="$projectroot/$path";2425open my$fd,'<',"$git_dir/cloneurl"2426orreturnwantarray?2427@{ config_to_multi(git_get_project_config('url')) } :2428 config_to_multi(git_get_project_config('url'));2429my@git_project_url_list=map{chomp;$_} <$fd>;2430close$fd;24312432returnwantarray?@git_project_url_list: \@git_project_url_list;2433}24342435sub git_get_projects_list {2436my($filter) =@_;2437my@list;24382439$filter||='';2440$filter=~s/\.git$//;24412442my$check_forks= gitweb_check_feature('forks');24432444if(-d $projects_list) {2445# search in directory2446my$dir=$projects_list. ($filter?"/$filter":'');2447# remove the trailing "/"2448$dir=~s!/+$!!;2449my$pfxlen=length("$dir");2450my$pfxdepth= ($dir=~tr!/!!);24512452 File::Find::find({2453 follow_fast =>1,# follow symbolic links2454 follow_skip =>2,# ignore duplicates2455 dangling_symlinks =>0,# ignore dangling symlinks, silently2456 wanted =>sub{2457# global variables2458our$project_maxdepth;2459our$projectroot;2460# skip project-list toplevel, if we get it.2461return if(m!^[/.]$!);2462# only directories can be git repositories2463return unless(-d $_);2464# don't traverse too deep (Find is super slow on os x)2465if(($File::Find::name =~tr!/!!) -$pfxdepth>$project_maxdepth) {2466$File::Find::prune =1;2467return;2468}24692470my$subdir=substr($File::Find::name,$pfxlen+1);2471# we check related file in $projectroot2472my$path= ($filter?"$filter/":'') .$subdir;2473if(check_export_ok("$projectroot/$path")) {2474push@list, { path =>$path};2475$File::Find::prune =1;2476}2477},2478},"$dir");24792480}elsif(-f $projects_list) {2481# read from file(url-encoded):2482# 'git%2Fgit.git Linus+Torvalds'2483# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2484# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2485my%paths;2486open my$fd,'<',$projects_listorreturn;2487 PROJECT:2488while(my$line= <$fd>) {2489chomp$line;2490my($path,$owner) =split' ',$line;2491$path= unescape($path);2492$owner= unescape($owner);2493if(!defined$path) {2494next;2495}2496if($filterne'') {2497# looking for forks;2498my$pfx=substr($path,0,length($filter));2499if($pfxne$filter) {2500next PROJECT;2501}2502my$sfx=substr($path,length($filter));2503if($sfx!~/^\/.*\.git$/) {2504next PROJECT;2505}2506}elsif($check_forks) {2507 PATH:2508foreachmy$filter(keys%paths) {2509# looking for forks;2510my$pfx=substr($path,0,length($filter));2511if($pfxne$filter) {2512next PATH;2513}2514my$sfx=substr($path,length($filter));2515if($sfx!~/^\/.*\.git$/) {2516next PATH;2517}2518# is a fork, don't include it in2519# the list2520next PROJECT;2521}2522}2523if(check_export_ok("$projectroot/$path")) {2524my$pr= {2525 path =>$path,2526 owner => to_utf8($owner),2527};2528push@list,$pr;2529(my$forks_path=$path) =~s/\.git$//;2530$paths{$forks_path}++;2531}2532}2533close$fd;2534}2535return@list;2536}25372538our$gitweb_project_owner=undef;2539sub git_get_project_list_from_file {25402541return if(defined$gitweb_project_owner);25422543$gitweb_project_owner= {};2544# read from file (url-encoded):2545# 'git%2Fgit.git Linus+Torvalds'2546# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2547# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2548if(-f $projects_list) {2549open(my$fd,'<',$projects_list);2550while(my$line= <$fd>) {2551chomp$line;2552my($pr,$ow) =split' ',$line;2553$pr= unescape($pr);2554$ow= unescape($ow);2555$gitweb_project_owner->{$pr} = to_utf8($ow);2556}2557close$fd;2558}2559}25602561sub git_get_project_owner {2562my$project=shift;2563my$owner;25642565returnundefunless$project;2566$git_dir="$projectroot/$project";25672568if(!defined$gitweb_project_owner) {2569 git_get_project_list_from_file();2570}25712572if(exists$gitweb_project_owner->{$project}) {2573$owner=$gitweb_project_owner->{$project};2574}2575if(!defined$owner){2576$owner= git_get_project_config('owner');2577}2578if(!defined$owner) {2579$owner= get_file_owner("$git_dir");2580}25812582return$owner;2583}25842585sub git_get_last_activity {2586my($path) =@_;2587my$fd;25882589$git_dir="$projectroot/$path";2590open($fd,"-|", git_cmd(),'for-each-ref',2591'--format=%(committer)',2592'--sort=-committerdate',2593'--count=1',2594'refs/heads')orreturn;2595my$most_recent= <$fd>;2596close$fdorreturn;2597if(defined$most_recent&&2598$most_recent=~/ (\d+) [-+][01]\d\d\d$/) {2599my$timestamp=$1;2600my$age=time-$timestamp;2601return($age, age_string($age));2602}2603return(undef,undef);2604}26052606sub git_get_references {2607my$type=shift||"";2608my%refs;2609# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.112610# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}2611open my$fd,"-|", git_cmd(),"show-ref","--dereference",2612($type? ("--","refs/$type") : ())# use -- <pattern> if $type2613orreturn;26142615while(my$line= <$fd>) {2616chomp$line;2617if($line=~m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {2618if(defined$refs{$1}) {2619push@{$refs{$1}},$2;2620}else{2621$refs{$1} = [$2];2622}2623}2624}2625close$fdorreturn;2626return \%refs;2627}26282629sub git_get_rev_name_tags {2630my$hash=shift||returnundef;26312632open my$fd,"-|", git_cmd(),"name-rev","--tags",$hash2633orreturn;2634my$name_rev= <$fd>;2635close$fd;26362637if($name_rev=~ m|^$hash tags/(.*)$|) {2638return$1;2639}else{2640# catches also '$hash undefined' output2641returnundef;2642}2643}26442645## ----------------------------------------------------------------------2646## parse to hash functions26472648sub parse_date {2649my$epoch=shift;2650my$tz=shift||"-0000";26512652my%date;2653my@months= ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");2654my@days= ("Sun","Mon","Tue","Wed","Thu","Fri","Sat");2655my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($epoch);2656$date{'hour'} =$hour;2657$date{'minute'} =$min;2658$date{'mday'} =$mday;2659$date{'day'} =$days[$wday];2660$date{'month'} =$months[$mon];2661$date{'rfc2822'} =sprintf"%s,%d%s%4d%02d:%02d:%02d+0000",2662$days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec;2663$date{'mday-time'} =sprintf"%d%s%02d:%02d",2664$mday,$months[$mon],$hour,$min;2665$date{'iso-8601'} =sprintf"%04d-%02d-%02dT%02d:%02d:%02dZ",26661900+$year,1+$mon,$mday,$hour,$min,$sec;26672668$tz=~m/^([+\-][0-9][0-9])([0-9][0-9])$/;2669my$local=$epoch+ ((int$1+ ($2/60)) *3600);2670($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($local);2671$date{'hour_local'} =$hour;2672$date{'minute_local'} =$min;2673$date{'tz_local'} =$tz;2674$date{'iso-tz'} =sprintf("%04d-%02d-%02d%02d:%02d:%02d%s",26751900+$year,$mon+1,$mday,2676$hour,$min,$sec,$tz);2677return%date;2678}26792680sub parse_tag {2681my$tag_id=shift;2682my%tag;2683my@comment;26842685open my$fd,"-|", git_cmd(),"cat-file","tag",$tag_idorreturn;2686$tag{'id'} =$tag_id;2687while(my$line= <$fd>) {2688chomp$line;2689if($line=~m/^object ([0-9a-fA-F]{40})$/) {2690$tag{'object'} =$1;2691}elsif($line=~m/^type (.+)$/) {2692$tag{'type'} =$1;2693}elsif($line=~m/^tag (.+)$/) {2694$tag{'name'} =$1;2695}elsif($line=~m/^tagger (.*) ([0-9]+) (.*)$/) {2696$tag{'author'} =$1;2697$tag{'author_epoch'} =$2;2698$tag{'author_tz'} =$3;2699if($tag{'author'} =~m/^([^<]+) <([^>]*)>/) {2700$tag{'author_name'} =$1;2701$tag{'author_email'} =$2;2702}else{2703$tag{'author_name'} =$tag{'author'};2704}2705}elsif($line=~m/--BEGIN/) {2706push@comment,$line;2707last;2708}elsif($lineeq"") {2709last;2710}2711}2712push@comment, <$fd>;2713$tag{'comment'} = \@comment;2714close$fdorreturn;2715if(!defined$tag{'name'}) {2716return2717};2718return%tag2719}27202721sub parse_commit_text {2722my($commit_text,$withparents) =@_;2723my@commit_lines=split'\n',$commit_text;2724my%co;27252726pop@commit_lines;# Remove '\0'27272728if(!@commit_lines) {2729return;2730}27312732my$header=shift@commit_lines;2733if($header!~m/^[0-9a-fA-F]{40}/) {2734return;2735}2736($co{'id'},my@parents) =split' ',$header;2737while(my$line=shift@commit_lines) {2738last if$lineeq"\n";2739if($line=~m/^tree ([0-9a-fA-F]{40})$/) {2740$co{'tree'} =$1;2741}elsif((!defined$withparents) && ($line=~m/^parent ([0-9a-fA-F]{40})$/)) {2742push@parents,$1;2743}elsif($line=~m/^author (.*) ([0-9]+) (.*)$/) {2744$co{'author'} = to_utf8($1);2745$co{'author_epoch'} =$2;2746$co{'author_tz'} =$3;2747if($co{'author'} =~m/^([^<]+) <([^>]*)>/) {2748$co{'author_name'} =$1;2749$co{'author_email'} =$2;2750}else{2751$co{'author_name'} =$co{'author'};2752}2753}elsif($line=~m/^committer (.*) ([0-9]+) (.*)$/) {2754$co{'committer'} = to_utf8($1);2755$co{'committer_epoch'} =$2;2756$co{'committer_tz'} =$3;2757if($co{'committer'} =~m/^([^<]+) <([^>]*)>/) {2758$co{'committer_name'} =$1;2759$co{'committer_email'} =$2;2760}else{2761$co{'committer_name'} =$co{'committer'};2762}2763}2764}2765if(!defined$co{'tree'}) {2766return;2767};2768$co{'parents'} = \@parents;2769$co{'parent'} =$parents[0];27702771foreachmy$title(@commit_lines) {2772$title=~s/^ //;2773if($titlene"") {2774$co{'title'} = chop_str($title,80,5);2775# remove leading stuff of merges to make the interesting part visible2776if(length($title) >50) {2777$title=~s/^Automatic //;2778$title=~s/^merge (of|with) /Merge ... /i;2779if(length($title) >50) {2780$title=~s/(http|rsync):\/\///;2781}2782if(length($title) >50) {2783$title=~s/(master|www|rsync)\.//;2784}2785if(length($title) >50) {2786$title=~s/kernel.org:?//;2787}2788if(length($title) >50) {2789$title=~s/\/pub\/scm//;2790}2791}2792$co{'title_short'} = chop_str($title,50,5);2793last;2794}2795}2796if(!defined$co{'title'} ||$co{'title'}eq"") {2797$co{'title'} =$co{'title_short'} ='(no commit message)';2798}2799# remove added spaces2800foreachmy$line(@commit_lines) {2801$line=~s/^ //;2802}2803$co{'comment'} = \@commit_lines;28042805my$age=time-$co{'committer_epoch'};2806$co{'age'} =$age;2807$co{'age_string'} = age_string($age);2808my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($co{'committer_epoch'});2809if($age>60*60*24*7*2) {2810$co{'age_string_date'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2811$co{'age_string_age'} =$co{'age_string'};2812}else{2813$co{'age_string_date'} =$co{'age_string'};2814$co{'age_string_age'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2815}2816return%co;2817}28182819sub parse_commit {2820my($commit_id) =@_;2821my%co;28222823local$/="\0";28242825open my$fd,"-|", git_cmd(),"rev-list",2826"--parents",2827"--header",2828"--max-count=1",2829$commit_id,2830"--",2831or die_error(500,"Open git-rev-list failed");2832%co= parse_commit_text(<$fd>,1);2833close$fd;28342835return%co;2836}28372838sub parse_commits {2839my($commit_id,$maxcount,$skip,$filename,@args) =@_;2840my@cos;28412842$maxcount||=1;2843$skip||=0;28442845local$/="\0";28462847open my$fd,"-|", git_cmd(),"rev-list",2848"--header",2849@args,2850("--max-count=".$maxcount),2851("--skip=".$skip),2852@extra_options,2853$commit_id,2854"--",2855($filename? ($filename) : ())2856or die_error(500,"Open git-rev-list failed");2857while(my$line= <$fd>) {2858my%co= parse_commit_text($line);2859push@cos, \%co;2860}2861close$fd;28622863returnwantarray?@cos: \@cos;2864}28652866# parse line of git-diff-tree "raw" output2867sub parse_difftree_raw_line {2868my$line=shift;2869my%res;28702871# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'2872# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'2873if($line=~m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {2874$res{'from_mode'} =$1;2875$res{'to_mode'} =$2;2876$res{'from_id'} =$3;2877$res{'to_id'} =$4;2878$res{'status'} =$5;2879$res{'similarity'} =$6;2880if($res{'status'}eq'R'||$res{'status'}eq'C') {# renamed or copied2881($res{'from_file'},$res{'to_file'}) =map{ unquote($_) }split("\t",$7);2882}else{2883$res{'from_file'} =$res{'to_file'} =$res{'file'} = unquote($7);2884}2885}2886# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'2887# combined diff (for merge commit)2888elsif($line=~s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {2889$res{'nparents'} =length($1);2890$res{'from_mode'} = [split(' ',$2) ];2891$res{'to_mode'} =pop@{$res{'from_mode'}};2892$res{'from_id'} = [split(' ',$3) ];2893$res{'to_id'} =pop@{$res{'from_id'}};2894$res{'status'} = [split('',$4) ];2895$res{'to_file'} = unquote($5);2896}2897# 'c512b523472485aef4fff9e57b229d9d243c967f'2898elsif($line=~m/^([0-9a-fA-F]{40})$/) {2899$res{'commit'} =$1;2900}29012902returnwantarray?%res: \%res;2903}29042905# wrapper: return parsed line of git-diff-tree "raw" output2906# (the argument might be raw line, or parsed info)2907sub parsed_difftree_line {2908my$line_or_ref=shift;29092910if(ref($line_or_ref)eq"HASH") {2911# pre-parsed (or generated by hand)2912return$line_or_ref;2913}else{2914return parse_difftree_raw_line($line_or_ref);2915}2916}29172918# parse line of git-ls-tree output2919sub parse_ls_tree_line {2920my$line=shift;2921my%opts=@_;2922my%res;29232924if($opts{'-l'}) {2925#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'2926$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;29272928$res{'mode'} =$1;2929$res{'type'} =$2;2930$res{'hash'} =$3;2931$res{'size'} =$4;2932if($opts{'-z'}) {2933$res{'name'} =$5;2934}else{2935$res{'name'} = unquote($5);2936}2937}else{2938#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2939$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;29402941$res{'mode'} =$1;2942$res{'type'} =$2;2943$res{'hash'} =$3;2944if($opts{'-z'}) {2945$res{'name'} =$4;2946}else{2947$res{'name'} = unquote($4);2948}2949}29502951returnwantarray?%res: \%res;2952}29532954# generates _two_ hashes, references to which are passed as 2 and 3 argument2955sub parse_from_to_diffinfo {2956my($diffinfo,$from,$to,@parents) =@_;29572958if($diffinfo->{'nparents'}) {2959# combined diff2960$from->{'file'} = [];2961$from->{'href'} = [];2962 fill_from_file_info($diffinfo,@parents)2963unlessexists$diffinfo->{'from_file'};2964for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {2965$from->{'file'}[$i] =2966defined$diffinfo->{'from_file'}[$i] ?2967$diffinfo->{'from_file'}[$i] :2968$diffinfo->{'to_file'};2969if($diffinfo->{'status'}[$i]ne"A") {# not new (added) file2970$from->{'href'}[$i] = href(action=>"blob",2971 hash_base=>$parents[$i],2972 hash=>$diffinfo->{'from_id'}[$i],2973 file_name=>$from->{'file'}[$i]);2974}else{2975$from->{'href'}[$i] =undef;2976}2977}2978}else{2979# ordinary (not combined) diff2980$from->{'file'} =$diffinfo->{'from_file'};2981if($diffinfo->{'status'}ne"A") {# not new (added) file2982$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,2983 hash=>$diffinfo->{'from_id'},2984 file_name=>$from->{'file'});2985}else{2986delete$from->{'href'};2987}2988}29892990$to->{'file'} =$diffinfo->{'to_file'};2991if(!is_deleted($diffinfo)) {# file exists in result2992$to->{'href'} = href(action=>"blob", hash_base=>$hash,2993 hash=>$diffinfo->{'to_id'},2994 file_name=>$to->{'file'});2995}else{2996delete$to->{'href'};2997}2998}29993000## ......................................................................3001## parse to array of hashes functions30023003sub git_get_heads_list {3004my$limit=shift;3005my@headslist;30063007open my$fd,'-|', git_cmd(),'for-each-ref',3008($limit?'--count='.($limit+1) : ()),'--sort=-committerdate',3009'--format=%(objectname) %(refname) %(subject)%00%(committer)',3010'refs/heads'3011orreturn;3012while(my$line= <$fd>) {3013my%ref_item;30143015chomp$line;3016my($refinfo,$committerinfo) =split(/\0/,$line);3017my($hash,$name,$title) =split(' ',$refinfo,3);3018my($committer,$epoch,$tz) =3019($committerinfo=~/^(.*) ([0-9]+) (.*)$/);3020$ref_item{'fullname'} =$name;3021$name=~s!^refs/heads/!!;30223023$ref_item{'name'} =$name;3024$ref_item{'id'} =$hash;3025$ref_item{'title'} =$title||'(no commit message)';3026$ref_item{'epoch'} =$epoch;3027if($epoch) {3028$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3029}else{3030$ref_item{'age'} ="unknown";3031}30323033push@headslist, \%ref_item;3034}3035close$fd;30363037returnwantarray?@headslist: \@headslist;3038}30393040sub git_get_tags_list {3041my$limit=shift;3042my@tagslist;30433044open my$fd,'-|', git_cmd(),'for-each-ref',3045($limit?'--count='.($limit+1) : ()),'--sort=-creatordate',3046'--format=%(objectname) %(objecttype) %(refname) '.3047'%(*objectname) %(*objecttype) %(subject)%00%(creator)',3048'refs/tags'3049orreturn;3050while(my$line= <$fd>) {3051my%ref_item;30523053chomp$line;3054my($refinfo,$creatorinfo) =split(/\0/,$line);3055my($id,$type,$name,$refid,$reftype,$title) =split(' ',$refinfo,6);3056my($creator,$epoch,$tz) =3057($creatorinfo=~/^(.*) ([0-9]+) (.*)$/);3058$ref_item{'fullname'} =$name;3059$name=~s!^refs/tags/!!;30603061$ref_item{'type'} =$type;3062$ref_item{'id'} =$id;3063$ref_item{'name'} =$name;3064if($typeeq"tag") {3065$ref_item{'subject'} =$title;3066$ref_item{'reftype'} =$reftype;3067$ref_item{'refid'} =$refid;3068}else{3069$ref_item{'reftype'} =$type;3070$ref_item{'refid'} =$id;3071}30723073if($typeeq"tag"||$typeeq"commit") {3074$ref_item{'epoch'} =$epoch;3075if($epoch) {3076$ref_item{'age'} = age_string(time-$ref_item{'epoch'});3077}else{3078$ref_item{'age'} ="unknown";3079}3080}30813082push@tagslist, \%ref_item;3083}3084close$fd;30853086returnwantarray?@tagslist: \@tagslist;3087}30883089## ----------------------------------------------------------------------3090## filesystem-related functions30913092sub get_file_owner {3093my$path=shift;30943095my($dev,$ino,$mode,$nlink,$st_uid,$st_gid,$rdev,$size) =stat($path);3096my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) =getpwuid($st_uid);3097if(!defined$gcos) {3098returnundef;3099}3100my$owner=$gcos;3101$owner=~s/[,;].*$//;3102return to_utf8($owner);3103}31043105# assume that file exists3106sub insert_file {3107my$filename=shift;31083109open my$fd,'<',$filename;3110print map{ to_utf8($_) } <$fd>;3111close$fd;3112}31133114## ......................................................................3115## mimetype related functions31163117sub mimetype_guess_file {3118my$filename=shift;3119my$mimemap=shift;3120-r $mimemaporreturnundef;31213122my%mimemap;3123open(my$mh,'<',$mimemap)orreturnundef;3124while(<$mh>) {3125next ifm/^#/;# skip comments3126my($mimetype,$exts) =split(/\t+/);3127if(defined$exts) {3128my@exts=split(/\s+/,$exts);3129foreachmy$ext(@exts) {3130$mimemap{$ext} =$mimetype;3131}3132}3133}3134close($mh);31353136$filename=~/\.([^.]*)$/;3137return$mimemap{$1};3138}31393140sub mimetype_guess {3141my$filename=shift;3142my$mime;3143$filename=~/\./orreturnundef;31443145if($mimetypes_file) {3146my$file=$mimetypes_file;3147if($file!~m!^/!) {# if it is relative path3148# it is relative to project3149$file="$projectroot/$project/$file";3150}3151$mime= mimetype_guess_file($filename,$file);3152}3153$mime||= mimetype_guess_file($filename,'/etc/mime.types');3154return$mime;3155}31563157sub blob_mimetype {3158my$fd=shift;3159my$filename=shift;31603161if($filename) {3162my$mime= mimetype_guess($filename);3163$mimeandreturn$mime;3164}31653166# just in case3167return$default_blob_plain_mimetypeunless$fd;31683169if(-T $fd) {3170return'text/plain';3171}elsif(!$filename) {3172return'application/octet-stream';3173}elsif($filename=~m/\.png$/i) {3174return'image/png';3175}elsif($filename=~m/\.gif$/i) {3176return'image/gif';3177}elsif($filename=~m/\.jpe?g$/i) {3178return'image/jpeg';3179}else{3180return'application/octet-stream';3181}3182}31833184sub blob_contenttype {3185my($fd,$file_name,$type) =@_;31863187$type||= blob_mimetype($fd,$file_name);3188if($typeeq'text/plain'&&defined$default_text_plain_charset) {3189$type.="; charset=$default_text_plain_charset";3190}31913192return$type;3193}31943195# guess file syntax for syntax highlighting; return undef if no highlighting3196# the name of syntax can (in the future) depend on syntax highlighter used3197sub guess_file_syntax {3198my($highlight,$mimetype,$file_name) =@_;3199returnundefunless($highlight&&defined$file_name);32003201# configuration for 'highlight' (http://www.andre-simon.de/)3202# match by basename3203my%highlight_basename= (3204#'Program' => 'py',3205#'Library' => 'py',3206'SConstruct'=>'py',# SCons equivalent of Makefile3207'Makefile'=>'make',3208);3209# match by extension3210my%highlight_ext= (3211# main extensions, defining name of syntax;3212# see files in /usr/share/highlight/langDefs/ directory3213map{$_=>$_}3214qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl),3215# alternate extensions, see /etc/highlight/filetypes.conf3216'h'=>'c',3217map{$_=>'cpp'}qw(cxx c++ cc),3218map{$_=>'php'}qw(php3 php4),3219map{$_=>'pl'}qw(perl pm),# perhaps also 'cgi'3220'mak'=>'make',3221map{$_=>'xml'}qw(xhtml html htm),3222);32233224my$basename= basename($file_name,'.in');3225return$highlight_basename{$basename}3226ifexists$highlight_basename{$basename};32273228$basename=~/\.([^.]*)$/;3229my$ext=$1orreturnundef;3230return$highlight_ext{$ext}3231ifexists$highlight_ext{$ext};32323233returnundef;3234}32353236# run highlighter and return FD of its output,3237# or return original FD if no highlighting3238sub run_highlighter {3239my($fd,$highlight,$syntax) =@_;3240return$fdunless($highlight&&defined$syntax);32413242close$fd3243or die_error(404,"Reading blob failed");3244open$fd, quote_command(git_cmd(),"cat-file","blob",$hash)." | ".3245"highlight --xhtml --fragment --syntax$syntax|"3246or die_error(500,"Couldn't open file or run syntax highlighter");3247return$fd;3248}32493250## ======================================================================3251## functions printing HTML: header, footer, error page32523253sub get_page_title {3254my$title= to_utf8($site_name);32553256return$titleunless(defined$project);3257$title.=" - ". to_utf8($project);32583259return$titleunless(defined$action);3260$title.="/$action";# $action is US-ASCII (7bit ASCII)32613262return$titleunless(defined$file_name);3263$title.=" - ". esc_path($file_name);3264if($actioneq"tree"&&$file_name!~ m|/$|) {3265$title.="/";3266}32673268return$title;3269}32703271sub git_header_html {3272my$status=shift||"200 OK";3273my$expires=shift;3274my%opts=@_;32753276my$title= get_page_title();3277my$content_type;3278# require explicit support from the UA if we are to send the page as3279# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.3280# we have to do this because MSIE sometimes globs '*/*', pretending to3281# support xhtml+xml but choking when it gets what it asked for.3282if(defined$cgi->http('HTTP_ACCEPT') &&3283$cgi->http('HTTP_ACCEPT') =~m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&3284$cgi->Accept('application/xhtml+xml') !=0) {3285$content_type='application/xhtml+xml';3286}else{3287$content_type='text/html';3288}3289print$cgi->header(-type=>$content_type, -charset =>'utf-8',3290-status=>$status, -expires =>$expires)3291unless($opts{'-no_http_header'});3292my$mod_perl_version=$ENV{'MOD_PERL'} ?"$ENV{'MOD_PERL'}":'';3293print<<EOF;3294<?xml version="1.0" encoding="utf-8"?>3295<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">3296<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">3297<!-- git web interface version$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->3298<!-- git core binaries version$git_version-->3299<head>3300<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>3301<meta name="generator" content="gitweb/$versiongit/$git_version$mod_perl_version"/>3302<meta name="robots" content="index, nofollow"/>3303<title>$title</title>3304EOF3305# the stylesheet, favicon etc urls won't work correctly with path_info3306# unless we set the appropriate base URL3307if($ENV{'PATH_INFO'}) {3308print"<base href=\"".esc_url($base_url)."\"/>\n";3309}3310# print out each stylesheet that exist, providing backwards capability3311# for those people who defined $stylesheet in a config file3312if(defined$stylesheet) {3313print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3314}else{3315foreachmy$stylesheet(@stylesheets) {3316next unless$stylesheet;3317print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";3318}3319}3320if(defined$project) {3321my%href_params= get_feed_info();3322if(!exists$href_params{'-title'}) {3323$href_params{'-title'} ='log';3324}33253326foreachmy$formatqw(RSS Atom){3327my$type=lc($format);3328my%link_attr= (3329'-rel'=>'alternate',3330'-title'=>"$project-$href_params{'-title'} -$formatfeed",3331'-type'=>"application/$type+xml"3332);33333334$href_params{'action'} =$type;3335$link_attr{'-href'} = href(%href_params);3336print"<link ".3337"rel=\"$link_attr{'-rel'}\"".3338"title=\"$link_attr{'-title'}\"".3339"href=\"$link_attr{'-href'}\"".3340"type=\"$link_attr{'-type'}\"".3341"/>\n";33423343$href_params{'extra_options'} ='--no-merges';3344$link_attr{'-href'} = href(%href_params);3345$link_attr{'-title'} .=' (no merges)';3346print"<link ".3347"rel=\"$link_attr{'-rel'}\"".3348"title=\"$link_attr{'-title'}\"".3349"href=\"$link_attr{'-href'}\"".3350"type=\"$link_attr{'-type'}\"".3351"/>\n";3352}33533354}else{3355printf('<link rel="alternate" title="%sprojects list" '.3356'href="%s" type="text/plain; charset=utf-8" />'."\n",3357$site_name, href(project=>undef, action=>"project_index"));3358printf('<link rel="alternate" title="%sprojects feeds" '.3359'href="%s" type="text/x-opml" />'."\n",3360$site_name, href(project=>undef, action=>"opml"));3361}3362if(defined$favicon) {3363printqq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);3364}33653366print"</head>\n".3367"<body>\n";33683369if(defined$site_header&& -f $site_header) {3370 insert_file($site_header);3371}33723373print"<div class=\"page_header\">\n".3374$cgi->a({-href => esc_url($logo_url),3375-title =>$logo_label},3376qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));3377print$cgi->a({-href => esc_url($home_link)},$home_link_str) ." / ";3378if(defined$project) {3379print$cgi->a({-href => href(action=>"summary")}, esc_html($project));3380if(defined$action) {3381print" /$action";3382}3383print"\n";3384}3385print"</div>\n";33863387my$have_search= gitweb_check_feature('search');3388if(defined$project&&$have_search) {3389if(!defined$searchtext) {3390$searchtext="";3391}3392my$search_hash;3393if(defined$hash_base) {3394$search_hash=$hash_base;3395}elsif(defined$hash) {3396$search_hash=$hash;3397}else{3398$search_hash="HEAD";3399}3400my$action=$my_uri;3401my$use_pathinfo= gitweb_check_feature('pathinfo');3402if($use_pathinfo) {3403$action.="/".esc_url($project);3404}3405print$cgi->startform(-method=>"get", -action =>$action) .3406"<div class=\"search\">\n".3407(!$use_pathinfo&&3408$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) ."\n") .3409$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) ."\n".3410$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) ."\n".3411$cgi->popup_menu(-name =>'st', -default=>'commit',3412-values=> ['commit','grep','author','committer','pickaxe']) .3413$cgi->sup($cgi->a({-href => href(action=>"search_help")},"?")) .3414" search:\n",3415$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".3416"<span title=\"Extended regular expression\">".3417$cgi->checkbox(-name =>'sr', -value =>1, -label =>'re',3418-checked =>$search_use_regexp) .3419"</span>".3420"</div>".3421$cgi->end_form() ."\n";3422}3423}34243425sub git_footer_html {3426my$feed_class='rss_logo';34273428print"<div class=\"page_footer\">\n";3429if(defined$project) {3430my$descr= git_get_project_description($project);3431if(defined$descr) {3432print"<div class=\"page_footer_text\">". esc_html($descr) ."</div>\n";3433}34343435my%href_params= get_feed_info();3436if(!%href_params) {3437$feed_class.=' generic';3438}3439$href_params{'-title'} ||='log';34403441foreachmy$formatqw(RSS Atom){3442$href_params{'action'} =lc($format);3443print$cgi->a({-href => href(%href_params),3444-title =>"$href_params{'-title'}$formatfeed",3445-class=>$feed_class},$format)."\n";3446}34473448}else{3449print$cgi->a({-href => href(project=>undef, action=>"opml"),3450-class=>$feed_class},"OPML") ." ";3451print$cgi->a({-href => href(project=>undef, action=>"project_index"),3452-class=>$feed_class},"TXT") ."\n";3453}3454print"</div>\n";# class="page_footer"34553456if(defined$t0&& gitweb_check_feature('timed')) {3457print"<div id=\"generating_info\">\n";3458print'This page took '.3459'<span id="generating_time" class="time_span">'.3460 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).3461' seconds </span>'.3462' and '.3463'<span id="generating_cmd">'.3464$number_of_git_cmds.3465'</span> git commands '.3466" to generate.\n";3467print"</div>\n";# class="page_footer"3468}34693470if(defined$site_footer&& -f $site_footer) {3471 insert_file($site_footer);3472}34733474print qq!<script type="text/javascript" src="$javascript"></script>\n!;3475if(defined$action&&3476$actioneq'blame_incremental') {3477print qq!<script type="text/javascript">\n!.3478 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.3479 qq!"!. href() .qq!");\n!.3480 qq!</script>\n!;3481}elsif(gitweb_check_feature('javascript-actions')) {3482print qq!<script type="text/javascript">\n!.3483 qq!window.onload = fixLinks;\n!.3484 qq!</script>\n!;3485}34863487print"</body>\n".3488"</html>";3489}34903491# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])3492# Example: die_error(404, 'Hash not found')3493# By convention, use the following status codes (as defined in RFC 2616):3494# 400: Invalid or missing CGI parameters, or3495# requested object exists but has wrong type.3496# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on3497# this server or project.3498# 404: Requested object/revision/project doesn't exist.3499# 500: The server isn't configured properly, or3500# an internal error occurred (e.g. failed assertions caused by bugs), or3501# an unknown error occurred (e.g. the git binary died unexpectedly).3502# 503: The server is currently unavailable (because it is overloaded,3503# or down for maintenance). Generally, this is a temporary state.3504sub die_error {3505my$status=shift||500;3506my$error= esc_html(shift) ||"Internal Server Error";3507my$extra=shift;3508my%opts=@_;35093510my%http_responses= (3511400=>'400 Bad Request',3512403=>'403 Forbidden',3513404=>'404 Not Found',3514500=>'500 Internal Server Error',3515503=>'503 Service Unavailable',3516);3517 git_header_html($http_responses{$status},undef,%opts);3518print<<EOF;3519<div class="page_body">3520<br /><br />3521$status-$error3522<br />3523EOF3524if(defined$extra) {3525print"<hr />\n".3526"$extra\n";3527}3528print"</div>\n";35293530 git_footer_html();3531goto DONE_GITWEB3532unless($opts{'-error_handler'});3533}35343535## ----------------------------------------------------------------------3536## functions printing or outputting HTML: navigation35373538sub git_print_page_nav {3539my($current,$suppress,$head,$treehead,$treebase,$extra) =@_;3540$extra=''if!defined$extra;# pager or formats35413542my@navs=qw(summary shortlog log commit commitdiff tree);3543if($suppress) {3544@navs=grep{$_ne$suppress}@navs;3545}35463547my%arg=map{$_=> {action=>$_} }@navs;3548if(defined$head) {3549for(qw(commit commitdiff)) {3550$arg{$_}{'hash'} =$head;3551}3552if($current=~m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {3553for(qw(shortlog log)) {3554$arg{$_}{'hash'} =$head;3555}3556}3557}35583559$arg{'tree'}{'hash'} =$treeheadifdefined$treehead;3560$arg{'tree'}{'hash_base'} =$treebaseifdefined$treebase;35613562my@actions= gitweb_get_feature('actions');3563my%repl= (3564'%'=>'%',3565'n'=>$project,# project name3566'f'=>$git_dir,# project path within filesystem3567'h'=>$treehead||'',# current hash ('h' parameter)3568'b'=>$treebase||'',# hash base ('hb' parameter)3569);3570while(@actions) {3571my($label,$link,$pos) =splice(@actions,0,3);3572# insert3573@navs=map{$_eq$pos? ($_,$label) :$_}@navs;3574# munch munch3575$link=~s/%([%nfhb])/$repl{$1}/g;3576$arg{$label}{'_href'} =$link;3577}35783579print"<div class=\"page_nav\">\n".3580(join" | ",3581map{$_eq$current?3582$_:$cgi->a({-href => ($arg{$_}{_href} ?$arg{$_}{_href} : href(%{$arg{$_}}))},"$_")3583}@navs);3584print"<br/>\n$extra<br/>\n".3585"</div>\n";3586}35873588sub format_paging_nav {3589my($action,$page,$has_next_link) =@_;3590my$paging_nav;359135923593if($page>0) {3594$paging_nav.=3595$cgi->a({-href => href(-replay=>1, page=>undef)},"first") .3596" ⋅ ".3597$cgi->a({-href => href(-replay=>1, page=>$page-1),3598-accesskey =>"p", -title =>"Alt-p"},"prev");3599}else{3600$paging_nav.="first ⋅ prev";3601}36023603if($has_next_link) {3604$paging_nav.=" ⋅ ".3605$cgi->a({-href => href(-replay=>1, page=>$page+1),3606-accesskey =>"n", -title =>"Alt-n"},"next");3607}else{3608$paging_nav.=" ⋅ next";3609}36103611return$paging_nav;3612}36133614## ......................................................................3615## functions printing or outputting HTML: div36163617sub git_print_header_div {3618my($action,$title,$hash,$hash_base) =@_;3619my%args= ();36203621$args{'action'} =$action;3622$args{'hash'} =$hashif$hash;3623$args{'hash_base'} =$hash_baseif$hash_base;36243625print"<div class=\"header\">\n".3626$cgi->a({-href => href(%args), -class=>"title"},3627$title?$title:$action) .3628"\n</div>\n";3629}36303631sub print_local_time {3632print format_local_time(@_);3633}36343635sub format_local_time {3636my$localtime='';3637my%date=@_;3638if($date{'hour_local'} <6) {3639$localtime.=sprintf(" (<span class=\"atnight\">%02d:%02d</span>%s)",3640$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3641}else{3642$localtime.=sprintf(" (%02d:%02d%s)",3643$date{'hour_local'},$date{'minute_local'},$date{'tz_local'});3644}36453646return$localtime;3647}36483649# Outputs the author name and date in long form3650sub git_print_authorship {3651my$co=shift;3652my%opts=@_;3653my$tag=$opts{-tag} ||'div';3654my$author=$co->{'author_name'};36553656my%ad= parse_date($co->{'author_epoch'},$co->{'author_tz'});3657print"<$tagclass=\"author_date\">".3658 format_search_author($author,"author", esc_html($author)) .3659" [$ad{'rfc2822'}";3660 print_local_time(%ad)if($opts{-localtime});3661print"]". git_get_avatar($co->{'author_email'}, -pad_before =>1)3662."</$tag>\n";3663}36643665# Outputs table rows containing the full author or committer information,3666# in the format expected for 'commit' view (& similia).3667# Parameters are a commit hash reference, followed by the list of people3668# to output information for. If the list is empty it defalts to both3669# author and committer.3670sub git_print_authorship_rows {3671my$co=shift;3672# too bad we can't use @people = @_ || ('author', 'committer')3673my@people=@_;3674@people= ('author','committer')unless@people;3675foreachmy$who(@people) {3676my%wd= parse_date($co->{"${who}_epoch"},$co->{"${who}_tz"});3677print"<tr><td>$who</td><td>".3678 format_search_author($co->{"${who}_name"},$who,3679 esc_html($co->{"${who}_name"})) ." ".3680 format_search_author($co->{"${who}_email"},$who,3681 esc_html("<".$co->{"${who}_email"} .">")) .3682"</td><td rowspan=\"2\">".3683 git_get_avatar($co->{"${who}_email"}, -size =>'double') .3684"</td></tr>\n".3685"<tr>".3686"<td></td><td>$wd{'rfc2822'}";3687 print_local_time(%wd);3688print"</td>".3689"</tr>\n";3690}3691}36923693sub git_print_page_path {3694my$name=shift;3695my$type=shift;3696my$hb=shift;369736983699print"<div class=\"page_path\">";3700print$cgi->a({-href => href(action=>"tree", hash_base=>$hb),3701-title =>'tree root'}, to_utf8("[$project]"));3702print" / ";3703if(defined$name) {3704my@dirname=split'/',$name;3705my$basename=pop@dirname;3706my$fullname='';37073708foreachmy$dir(@dirname) {3709$fullname.= ($fullname?'/':'') .$dir;3710print$cgi->a({-href => href(action=>"tree", file_name=>$fullname,3711 hash_base=>$hb),3712-title =>$fullname}, esc_path($dir));3713print" / ";3714}3715if(defined$type&&$typeeq'blob') {3716print$cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,3717 hash_base=>$hb),3718-title =>$name}, esc_path($basename));3719}elsif(defined$type&&$typeeq'tree') {3720print$cgi->a({-href => href(action=>"tree", file_name=>$file_name,3721 hash_base=>$hb),3722-title =>$name}, esc_path($basename));3723print" / ";3724}else{3725print esc_path($basename);3726}3727}3728print"<br/></div>\n";3729}37303731sub git_print_log {3732my$log=shift;3733my%opts=@_;37343735if($opts{'-remove_title'}) {3736# remove title, i.e. first line of log3737shift@$log;3738}3739# remove leading empty lines3740while(defined$log->[0] &&$log->[0]eq"") {3741shift@$log;3742}37433744# print log3745my$signoff=0;3746my$empty=0;3747foreachmy$line(@$log) {3748if($line=~m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {3749$signoff=1;3750$empty=0;3751if(!$opts{'-remove_signoff'}) {3752print"<span class=\"signoff\">". esc_html($line) ."</span><br/>\n";3753next;3754}else{3755# remove signoff lines3756next;3757}3758}else{3759$signoff=0;3760}37613762# print only one empty line3763# do not print empty line after signoff3764if($lineeq"") {3765next if($empty||$signoff);3766$empty=1;3767}else{3768$empty=0;3769}37703771print format_log_line_html($line) ."<br/>\n";3772}37733774if($opts{'-final_empty_line'}) {3775# end with single empty line3776print"<br/>\n"unless$empty;3777}3778}37793780# return link target (what link points to)3781sub git_get_link_target {3782my$hash=shift;3783my$link_target;37843785# read link3786open my$fd,"-|", git_cmd(),"cat-file","blob",$hash3787orreturn;3788{3789local$/=undef;3790$link_target= <$fd>;3791}3792close$fd3793orreturn;37943795return$link_target;3796}37973798# given link target, and the directory (basedir) the link is in,3799# return target of link relative to top directory (top tree);3800# return undef if it is not possible (including absolute links).3801sub normalize_link_target {3802my($link_target,$basedir) =@_;38033804# absolute symlinks (beginning with '/') cannot be normalized3805return if(substr($link_target,0,1)eq'/');38063807# normalize link target to path from top (root) tree (dir)3808my$path;3809if($basedir) {3810$path=$basedir.'/'.$link_target;3811}else{3812# we are in top (root) tree (dir)3813$path=$link_target;3814}38153816# remove //, /./, and /../3817my@path_parts;3818foreachmy$part(split('/',$path)) {3819# discard '.' and ''3820next if(!$part||$parteq'.');3821# handle '..'3822if($parteq'..') {3823if(@path_parts) {3824pop@path_parts;3825}else{3826# link leads outside repository (outside top dir)3827return;3828}3829}else{3830push@path_parts,$part;3831}3832}3833$path=join('/',@path_parts);38343835return$path;3836}38373838# print tree entry (row of git_tree), but without encompassing <tr> element3839sub git_print_tree_entry {3840my($t,$basedir,$hash_base,$have_blame) =@_;38413842my%base_key= ();3843$base_key{'hash_base'} =$hash_baseifdefined$hash_base;38443845# The format of a table row is: mode list link. Where mode is3846# the mode of the entry, list is the name of the entry, an href,3847# and link is the action links of the entry.38483849print"<td class=\"mode\">". mode_str($t->{'mode'}) ."</td>\n";3850if(exists$t->{'size'}) {3851print"<td class=\"size\">$t->{'size'}</td>\n";3852}3853if($t->{'type'}eq"blob") {3854print"<td class=\"list\">".3855$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3856 file_name=>"$basedir$t->{'name'}",%base_key),3857-class=>"list"}, esc_path($t->{'name'}));3858if(S_ISLNK(oct$t->{'mode'})) {3859my$link_target= git_get_link_target($t->{'hash'});3860if($link_target) {3861my$norm_target= normalize_link_target($link_target,$basedir);3862if(defined$norm_target) {3863print" -> ".3864$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,3865 file_name=>$norm_target),3866-title =>$norm_target}, esc_path($link_target));3867}else{3868print" -> ". esc_path($link_target);3869}3870}3871}3872print"</td>\n";3873print"<td class=\"link\">";3874print$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3875 file_name=>"$basedir$t->{'name'}",%base_key)},3876"blob");3877if($have_blame) {3878print" | ".3879$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},3880 file_name=>"$basedir$t->{'name'}",%base_key)},3881"blame");3882}3883if(defined$hash_base) {3884print" | ".3885$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3886 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},3887"history");3888}3889print" | ".3890$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,3891 file_name=>"$basedir$t->{'name'}")},3892"raw");3893print"</td>\n";38943895}elsif($t->{'type'}eq"tree") {3896print"<td class=\"list\">";3897print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3898 file_name=>"$basedir$t->{'name'}",3899%base_key)},3900 esc_path($t->{'name'}));3901print"</td>\n";3902print"<td class=\"link\">";3903print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3904 file_name=>"$basedir$t->{'name'}",3905%base_key)},3906"tree");3907if(defined$hash_base) {3908print" | ".3909$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3910 file_name=>"$basedir$t->{'name'}")},3911"history");3912}3913print"</td>\n";3914}else{3915# unknown object: we can only present history for it3916# (this includes 'commit' object, i.e. submodule support)3917print"<td class=\"list\">".3918 esc_path($t->{'name'}) .3919"</td>\n";3920print"<td class=\"link\">";3921if(defined$hash_base) {3922print$cgi->a({-href => href(action=>"history",3923 hash_base=>$hash_base,3924 file_name=>"$basedir$t->{'name'}")},3925"history");3926}3927print"</td>\n";3928}3929}39303931## ......................................................................3932## functions printing large fragments of HTML39333934# get pre-image filenames for merge (combined) diff3935sub fill_from_file_info {3936my($diff,@parents) =@_;39373938$diff->{'from_file'} = [ ];3939$diff->{'from_file'}[$diff->{'nparents'} -1] =undef;3940for(my$i=0;$i<$diff->{'nparents'};$i++) {3941if($diff->{'status'}[$i]eq'R'||3942$diff->{'status'}[$i]eq'C') {3943$diff->{'from_file'}[$i] =3944 git_get_path_by_hash($parents[$i],$diff->{'from_id'}[$i]);3945}3946}39473948return$diff;3949}39503951# is current raw difftree line of file deletion3952sub is_deleted {3953my$diffinfo=shift;39543955return$diffinfo->{'to_id'}eq('0' x 40);3956}39573958# does patch correspond to [previous] difftree raw line3959# $diffinfo - hashref of parsed raw diff format3960# $patchinfo - hashref of parsed patch diff format3961# (the same keys as in $diffinfo)3962sub is_patch_split {3963my($diffinfo,$patchinfo) =@_;39643965returndefined$diffinfo&&defined$patchinfo3966&&$diffinfo->{'to_file'}eq$patchinfo->{'to_file'};3967}396839693970sub git_difftree_body {3971my($difftree,$hash,@parents) =@_;3972my($parent) =$parents[0];3973my$have_blame= gitweb_check_feature('blame');3974print"<div class=\"list_head\">\n";3975if($#{$difftree} >10) {3976print(($#{$difftree} +1) ." files changed:\n");3977}3978print"</div>\n";39793980print"<table class=\"".3981(@parents>1?"combined ":"") .3982"diff_tree\">\n";39833984# header only for combined diff in 'commitdiff' view3985my$has_header=@$difftree&&@parents>1&&$actioneq'commitdiff';3986if($has_header) {3987# table header3988print"<thead><tr>\n".3989"<th></th><th></th>\n";# filename, patchN link3990for(my$i=0;$i<@parents;$i++) {3991my$par=$parents[$i];3992print"<th>".3993$cgi->a({-href => href(action=>"commitdiff",3994 hash=>$hash, hash_parent=>$par),3995-title =>'commitdiff to parent number '.3996($i+1) .': '.substr($par,0,7)},3997$i+1) .3998" </th>\n";3999}4000print"</tr></thead>\n<tbody>\n";4001}40024003my$alternate=1;4004my$patchno=0;4005foreachmy$line(@{$difftree}) {4006my$diff= parsed_difftree_line($line);40074008if($alternate) {4009print"<tr class=\"dark\">\n";4010}else{4011print"<tr class=\"light\">\n";4012}4013$alternate^=1;40144015if(exists$diff->{'nparents'}) {# combined diff40164017 fill_from_file_info($diff,@parents)4018unlessexists$diff->{'from_file'};40194020if(!is_deleted($diff)) {4021# file exists in the result (child) commit4022print"<td>".4023$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4024 file_name=>$diff->{'to_file'},4025 hash_base=>$hash),4026-class=>"list"}, esc_path($diff->{'to_file'})) .4027"</td>\n";4028}else{4029print"<td>".4030 esc_path($diff->{'to_file'}) .4031"</td>\n";4032}40334034if($actioneq'commitdiff') {4035# link to patch4036$patchno++;4037print"<td class=\"link\">".4038$cgi->a({-href =>"#patch$patchno"},"patch") .4039" | ".4040"</td>\n";4041}40424043my$has_history=0;4044my$not_deleted=0;4045for(my$i=0;$i<$diff->{'nparents'};$i++) {4046my$hash_parent=$parents[$i];4047my$from_hash=$diff->{'from_id'}[$i];4048my$from_path=$diff->{'from_file'}[$i];4049my$status=$diff->{'status'}[$i];40504051$has_history||= ($statusne'A');4052$not_deleted||= ($statusne'D');40534054if($statuseq'A') {4055print"<td class=\"link\"align=\"right\"> | </td>\n";4056}elsif($statuseq'D') {4057print"<td class=\"link\">".4058$cgi->a({-href => href(action=>"blob",4059 hash_base=>$hash,4060 hash=>$from_hash,4061 file_name=>$from_path)},4062"blob". ($i+1)) .4063" | </td>\n";4064}else{4065if($diff->{'to_id'}eq$from_hash) {4066print"<td class=\"link nochange\">";4067}else{4068print"<td class=\"link\">";4069}4070print$cgi->a({-href => href(action=>"blobdiff",4071 hash=>$diff->{'to_id'},4072 hash_parent=>$from_hash,4073 hash_base=>$hash,4074 hash_parent_base=>$hash_parent,4075 file_name=>$diff->{'to_file'},4076 file_parent=>$from_path)},4077"diff". ($i+1)) .4078" | </td>\n";4079}4080}40814082print"<td class=\"link\">";4083if($not_deleted) {4084print$cgi->a({-href => href(action=>"blob",4085 hash=>$diff->{'to_id'},4086 file_name=>$diff->{'to_file'},4087 hash_base=>$hash)},4088"blob");4089print" | "if($has_history);4090}4091if($has_history) {4092print$cgi->a({-href => href(action=>"history",4093 file_name=>$diff->{'to_file'},4094 hash_base=>$hash)},4095"history");4096}4097print"</td>\n";40984099print"</tr>\n";4100next;# instead of 'else' clause, to avoid extra indent4101}4102# else ordinary diff41034104my($to_mode_oct,$to_mode_str,$to_file_type);4105my($from_mode_oct,$from_mode_str,$from_file_type);4106if($diff->{'to_mode'}ne('0' x 6)) {4107$to_mode_oct=oct$diff->{'to_mode'};4108if(S_ISREG($to_mode_oct)) {# only for regular file4109$to_mode_str=sprintf("%04o",$to_mode_oct&0777);# permission bits4110}4111$to_file_type= file_type($diff->{'to_mode'});4112}4113if($diff->{'from_mode'}ne('0' x 6)) {4114$from_mode_oct=oct$diff->{'from_mode'};4115if(S_ISREG($to_mode_oct)) {# only for regular file4116$from_mode_str=sprintf("%04o",$from_mode_oct&0777);# permission bits4117}4118$from_file_type= file_type($diff->{'from_mode'});4119}41204121if($diff->{'status'}eq"A") {# created4122my$mode_chng="<span class=\"file_status new\">[new$to_file_type";4123$mode_chng.=" with mode:$to_mode_str"if$to_mode_str;4124$mode_chng.="]</span>";4125print"<td>";4126print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4127 hash_base=>$hash, file_name=>$diff->{'file'}),4128-class=>"list"}, esc_path($diff->{'file'}));4129print"</td>\n";4130print"<td>$mode_chng</td>\n";4131print"<td class=\"link\">";4132if($actioneq'commitdiff') {4133# link to patch4134$patchno++;4135print$cgi->a({-href =>"#patch$patchno"},"patch");4136print" | ";4137}4138print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4139 hash_base=>$hash, file_name=>$diff->{'file'})},4140"blob");4141print"</td>\n";41424143}elsif($diff->{'status'}eq"D") {# deleted4144my$mode_chng="<span class=\"file_status deleted\">[deleted$from_file_type]</span>";4145print"<td>";4146print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4147 hash_base=>$parent, file_name=>$diff->{'file'}),4148-class=>"list"}, esc_path($diff->{'file'}));4149print"</td>\n";4150print"<td>$mode_chng</td>\n";4151print"<td class=\"link\">";4152if($actioneq'commitdiff') {4153# link to patch4154$patchno++;4155print$cgi->a({-href =>"#patch$patchno"},"patch");4156print" | ";4157}4158print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},4159 hash_base=>$parent, file_name=>$diff->{'file'})},4160"blob") ." | ";4161if($have_blame) {4162print$cgi->a({-href => href(action=>"blame", hash_base=>$parent,4163 file_name=>$diff->{'file'})},4164"blame") ." | ";4165}4166print$cgi->a({-href => href(action=>"history", hash_base=>$parent,4167 file_name=>$diff->{'file'})},4168"history");4169print"</td>\n";41704171}elsif($diff->{'status'}eq"M"||$diff->{'status'}eq"T") {# modified, or type changed4172my$mode_chnge="";4173if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4174$mode_chnge="<span class=\"file_status mode_chnge\">[changed";4175if($from_file_typene$to_file_type) {4176$mode_chnge.=" from$from_file_typeto$to_file_type";4177}4178if(($from_mode_oct&0777) != ($to_mode_oct&0777)) {4179if($from_mode_str&&$to_mode_str) {4180$mode_chnge.=" mode:$from_mode_str->$to_mode_str";4181}elsif($to_mode_str) {4182$mode_chnge.=" mode:$to_mode_str";4183}4184}4185$mode_chnge.="]</span>\n";4186}4187print"<td>";4188print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4189 hash_base=>$hash, file_name=>$diff->{'file'}),4190-class=>"list"}, esc_path($diff->{'file'}));4191print"</td>\n";4192print"<td>$mode_chnge</td>\n";4193print"<td class=\"link\">";4194if($actioneq'commitdiff') {4195# link to patch4196$patchno++;4197print$cgi->a({-href =>"#patch$patchno"},"patch") .4198" | ";4199}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4200# "commit" view and modified file (not onlu mode changed)4201print$cgi->a({-href => href(action=>"blobdiff",4202 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4203 hash_base=>$hash, hash_parent_base=>$parent,4204 file_name=>$diff->{'file'})},4205"diff") .4206" | ";4207}4208print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4209 hash_base=>$hash, file_name=>$diff->{'file'})},4210"blob") ." | ";4211if($have_blame) {4212print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4213 file_name=>$diff->{'file'})},4214"blame") ." | ";4215}4216print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4217 file_name=>$diff->{'file'})},4218"history");4219print"</td>\n";42204221}elsif($diff->{'status'}eq"R"||$diff->{'status'}eq"C") {# renamed or copied4222my%status_name= ('R'=>'moved','C'=>'copied');4223my$nstatus=$status_name{$diff->{'status'}};4224my$mode_chng="";4225if($diff->{'from_mode'} !=$diff->{'to_mode'}) {4226# mode also for directories, so we cannot use $to_mode_str4227$mode_chng=sprintf(", mode:%04o",$to_mode_oct&0777);4228}4229print"<td>".4230$cgi->a({-href => href(action=>"blob", hash_base=>$hash,4231 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),4232-class=>"list"}, esc_path($diff->{'to_file'})) ."</td>\n".4233"<td><span class=\"file_status$nstatus\">[$nstatusfrom ".4234$cgi->a({-href => href(action=>"blob", hash_base=>$parent,4235 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),4236-class=>"list"}, esc_path($diff->{'from_file'})) .4237" with ". (int$diff->{'similarity'}) ."% similarity$mode_chng]</span></td>\n".4238"<td class=\"link\">";4239if($actioneq'commitdiff') {4240# link to patch4241$patchno++;4242print$cgi->a({-href =>"#patch$patchno"},"patch") .4243" | ";4244}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {4245# "commit" view and modified file (not only pure rename or copy)4246print$cgi->a({-href => href(action=>"blobdiff",4247 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},4248 hash_base=>$hash, hash_parent_base=>$parent,4249 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},4250"diff") .4251" | ";4252}4253print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},4254 hash_base=>$parent, file_name=>$diff->{'to_file'})},4255"blob") ." | ";4256if($have_blame) {4257print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,4258 file_name=>$diff->{'to_file'})},4259"blame") ." | ";4260}4261print$cgi->a({-href => href(action=>"history", hash_base=>$hash,4262 file_name=>$diff->{'to_file'})},4263"history");4264print"</td>\n";42654266}# we should not encounter Unmerged (U) or Unknown (X) status4267print"</tr>\n";4268}4269print"</tbody>"if$has_header;4270print"</table>\n";4271}42724273sub git_patchset_body {4274my($fd,$difftree,$hash,@hash_parents) =@_;4275my($hash_parent) =$hash_parents[0];42764277my$is_combined= (@hash_parents>1);4278my$patch_idx=0;4279my$patch_number=0;4280my$patch_line;4281my$diffinfo;4282my$to_name;4283my(%from,%to);42844285print"<div class=\"patchset\">\n";42864287# skip to first patch4288while($patch_line= <$fd>) {4289chomp$patch_line;42904291last if($patch_line=~m/^diff /);4292}42934294 PATCH:4295while($patch_line) {42964297# parse "git diff" header line4298if($patch_line=~m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {4299# $1 is from_name, which we do not use4300$to_name= unquote($2);4301$to_name=~s!^b/!!;4302}elsif($patch_line=~m/^diff --(cc|combined) ("?.*"?)$/) {4303# $1 is 'cc' or 'combined', which we do not use4304$to_name= unquote($2);4305}else{4306$to_name=undef;4307}43084309# check if current patch belong to current raw line4310# and parse raw git-diff line if needed4311if(is_patch_split($diffinfo, {'to_file'=>$to_name})) {4312# this is continuation of a split patch4313print"<div class=\"patch cont\">\n";4314}else{4315# advance raw git-diff output if needed4316$patch_idx++ifdefined$diffinfo;43174318# read and prepare patch information4319$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);43204321# compact combined diff output can have some patches skipped4322# find which patch (using pathname of result) we are at now;4323if($is_combined) {4324while($to_namene$diffinfo->{'to_file'}) {4325print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4326 format_diff_cc_simplified($diffinfo,@hash_parents) .4327"</div>\n";# class="patch"43284329$patch_idx++;4330$patch_number++;43314332last if$patch_idx>$#$difftree;4333$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);4334}4335}43364337# modifies %from, %to hashes4338 parse_from_to_diffinfo($diffinfo, \%from, \%to,@hash_parents);43394340# this is first patch for raw difftree line with $patch_idx index4341# we index @$difftree array from 0, but number patches from 14342print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n";4343}43444345# git diff header4346#assert($patch_line =~ m/^diff /) if DEBUG;4347#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed4348$patch_number++;4349# print "git diff" header4350print format_git_diff_header_line($patch_line,$diffinfo,4351 \%from, \%to);43524353# print extended diff header4354print"<div class=\"diff extended_header\">\n";4355 EXTENDED_HEADER:4356while($patch_line= <$fd>) {4357chomp$patch_line;43584359last EXTENDED_HEADER if($patch_line=~m/^--- |^diff /);43604361print format_extended_diff_header_line($patch_line,$diffinfo,4362 \%from, \%to);4363}4364print"</div>\n";# class="diff extended_header"43654366# from-file/to-file diff header4367if(!$patch_line) {4368print"</div>\n";# class="patch"4369last PATCH;4370}4371next PATCH if($patch_line=~m/^diff /);4372#assert($patch_line =~ m/^---/) if DEBUG;43734374my$last_patch_line=$patch_line;4375$patch_line= <$fd>;4376chomp$patch_line;4377#assert($patch_line =~ m/^\+\+\+/) if DEBUG;43784379print format_diff_from_to_header($last_patch_line,$patch_line,4380$diffinfo, \%from, \%to,4381@hash_parents);43824383# the patch itself4384 LINE:4385while($patch_line= <$fd>) {4386chomp$patch_line;43874388next PATCH if($patch_line=~m/^diff /);43894390print format_diff_line($patch_line, \%from, \%to);4391}43924393}continue{4394print"</div>\n";# class="patch"4395}43964397# for compact combined (--cc) format, with chunk and patch simpliciaction4398# patchset might be empty, but there might be unprocessed raw lines4399for(++$patch_idxif$patch_number>0;4400$patch_idx<@$difftree;4401++$patch_idx) {4402# read and prepare patch information4403$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);44044405# generate anchor for "patch" links in difftree / whatchanged part4406print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".4407 format_diff_cc_simplified($diffinfo,@hash_parents) .4408"</div>\n";# class="patch"44094410$patch_number++;4411}44124413if($patch_number==0) {4414if(@hash_parents>1) {4415print"<div class=\"diff nodifferences\">Trivial merge</div>\n";4416}else{4417print"<div class=\"diff nodifferences\">No differences found</div>\n";4418}4419}44204421print"</div>\n";# class="patchset"4422}44234424# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .44254426# fills project list info (age, description, owner, forks) for each4427# project in the list, removing invalid projects from returned list4428# NOTE: modifies $projlist, but does not remove entries from it4429sub fill_project_list_info {4430my($projlist,$check_forks) =@_;4431my@projects;44324433my$show_ctags= gitweb_check_feature('ctags');4434 PROJECT:4435foreachmy$pr(@$projlist) {4436my(@activity) = git_get_last_activity($pr->{'path'});4437unless(@activity) {4438next PROJECT;4439}4440($pr->{'age'},$pr->{'age_string'}) =@activity;4441if(!defined$pr->{'descr'}) {4442my$descr= git_get_project_description($pr->{'path'}) ||"";4443$descr= to_utf8($descr);4444$pr->{'descr_long'} =$descr;4445$pr->{'descr'} = chop_str($descr,$projects_list_description_width,5);4446}4447if(!defined$pr->{'owner'}) {4448$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") ||"";4449}4450if($check_forks) {4451my$pname=$pr->{'path'};4452if(($pname=~s/\.git$//) &&4453($pname!~/\/$/) &&4454(-d "$projectroot/$pname")) {4455$pr->{'forks'} ="-d$projectroot/$pname";4456}else{4457$pr->{'forks'} =0;4458}4459}4460$show_ctagsand$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});4461push@projects,$pr;4462}44634464return@projects;4465}44664467# print 'sort by' <th> element, generating 'sort by $name' replay link4468# if that order is not selected4469sub print_sort_th {4470print format_sort_th(@_);4471}44724473sub format_sort_th {4474my($name,$order,$header) =@_;4475my$sort_th="";4476$header||=ucfirst($name);44774478if($ordereq$name) {4479$sort_th.="<th>$header</th>\n";4480}else{4481$sort_th.="<th>".4482$cgi->a({-href => href(-replay=>1, order=>$name),4483-class=>"header"},$header) .4484"</th>\n";4485}44864487return$sort_th;4488}44894490sub git_project_list_body {4491# actually uses global variable $project4492my($projlist,$order,$from,$to,$extra,$no_header) =@_;44934494my$check_forks= gitweb_check_feature('forks');4495my@projects= fill_project_list_info($projlist,$check_forks);44964497$order||=$default_projects_order;4498$from=0unlessdefined$from;4499$to=$#projectsif(!defined$to||$#projects<$to);45004501my%order_info= (4502 project => { key =>'path', type =>'str'},4503 descr => { key =>'descr_long', type =>'str'},4504 owner => { key =>'owner', type =>'str'},4505 age => { key =>'age', type =>'num'}4506);4507my$oi=$order_info{$order};4508if($oi->{'type'}eq'str') {4509@projects=sort{$a->{$oi->{'key'}}cmp$b->{$oi->{'key'}}}@projects;4510}else{4511@projects=sort{$a->{$oi->{'key'}} <=>$b->{$oi->{'key'}}}@projects;4512}45134514my$show_ctags= gitweb_check_feature('ctags');4515if($show_ctags) {4516my%ctags;4517foreachmy$p(@projects) {4518foreachmy$ct(keys%{$p->{'ctags'}}) {4519$ctags{$ct} +=$p->{'ctags'}->{$ct};4520}4521}4522my$cloud= git_populate_project_tagcloud(\%ctags);4523print git_show_project_tagcloud($cloud,64);4524}45254526print"<table class=\"project_list\">\n";4527unless($no_header) {4528print"<tr>\n";4529if($check_forks) {4530print"<th></th>\n";4531}4532 print_sort_th('project',$order,'Project');4533 print_sort_th('descr',$order,'Description');4534 print_sort_th('owner',$order,'Owner');4535 print_sort_th('age',$order,'Last Change');4536print"<th></th>\n".# for links4537"</tr>\n";4538}4539my$alternate=1;4540my$tagfilter=$cgi->param('by_tag');4541for(my$i=$from;$i<=$to;$i++) {4542my$pr=$projects[$i];45434544next if$tagfilterand$show_ctagsand not grep{lc$_eq lc$tagfilter}keys%{$pr->{'ctags'}};4545next if$searchtextand not$pr->{'path'} =~/$searchtext/4546and not$pr->{'descr_long'} =~/$searchtext/;4547# Weed out forks or non-matching entries of search4548if($check_forks) {4549my$forkbase=$project;$forkbase||='';$forkbase=~ s#\.git$#/#;4550$forkbase="^$forkbase"if$forkbase;4551next ifnot$searchtextand not$tagfilterand$show_ctags4552and$pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe4553}45544555if($alternate) {4556print"<tr class=\"dark\">\n";4557}else{4558print"<tr class=\"light\">\n";4559}4560$alternate^=1;4561if($check_forks) {4562print"<td>";4563if($pr->{'forks'}) {4564print"<!--$pr->{'forks'} -->\n";4565print$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"+");4566}4567print"</td>\n";4568}4569print"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4570-class=>"list"}, esc_html($pr->{'path'})) ."</td>\n".4571"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4572-class=>"list", -title =>$pr->{'descr_long'}},4573 esc_html($pr->{'descr'})) ."</td>\n".4574"<td><i>". chop_and_escape_str($pr->{'owner'},15) ."</i></td>\n";4575print"<td class=\"". age_class($pr->{'age'}) ."\">".4576(defined$pr->{'age_string'} ?$pr->{'age_string'} :"No commits") ."</td>\n".4577"<td class=\"link\">".4578$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")},"summary") ." | ".4579$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")},"shortlog") ." | ".4580$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")},"log") ." | ".4581$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")},"tree") .4582($pr->{'forks'} ?" | ".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"forks") :'') .4583"</td>\n".4584"</tr>\n";4585}4586if(defined$extra) {4587print"<tr>\n";4588if($check_forks) {4589print"<td></td>\n";4590}4591print"<td colspan=\"5\">$extra</td>\n".4592"</tr>\n";4593}4594print"</table>\n";4595}45964597sub git_log_body {4598# uses global variable $project4599my($commitlist,$from,$to,$refs,$extra) =@_;46004601$from=0unlessdefined$from;4602$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);46034604for(my$i=0;$i<=$to;$i++) {4605my%co= %{$commitlist->[$i]};4606next if!%co;4607my$commit=$co{'id'};4608my$ref= format_ref_marker($refs,$commit);4609my%ad= parse_date($co{'author_epoch'});4610 git_print_header_div('commit',4611"<span class=\"age\">$co{'age_string'}</span>".4612 esc_html($co{'title'}) .$ref,4613$commit);4614print"<div class=\"title_text\">\n".4615"<div class=\"log_link\">\n".4616$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") .4617" | ".4618$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") .4619" | ".4620$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree") .4621"<br/>\n".4622"</div>\n";4623 git_print_authorship(\%co, -tag =>'span');4624print"<br/>\n</div>\n";46254626print"<div class=\"log_body\">\n";4627 git_print_log($co{'comment'}, -final_empty_line=>1);4628print"</div>\n";4629}4630if($extra) {4631print"<div class=\"page_nav\">\n";4632print"$extra\n";4633print"</div>\n";4634}4635}46364637sub git_shortlog_body {4638# uses global variable $project4639my($commitlist,$from,$to,$refs,$extra) =@_;46404641$from=0unlessdefined$from;4642$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);46434644print"<table class=\"shortlog\">\n";4645my$alternate=1;4646for(my$i=$from;$i<=$to;$i++) {4647my%co= %{$commitlist->[$i]};4648my$commit=$co{'id'};4649my$ref= format_ref_marker($refs,$commit);4650if($alternate) {4651print"<tr class=\"dark\">\n";4652}else{4653print"<tr class=\"light\">\n";4654}4655$alternate^=1;4656# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .4657print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4658 format_author_html('td', \%co,10) ."<td>";4659print format_subject_html($co{'title'},$co{'title_short'},4660 href(action=>"commit", hash=>$commit),$ref);4661print"</td>\n".4662"<td class=\"link\">".4663$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") ." | ".4664$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") ." | ".4665$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree");4666my$snapshot_links= format_snapshot_links($commit);4667if(defined$snapshot_links) {4668print" | ".$snapshot_links;4669}4670print"</td>\n".4671"</tr>\n";4672}4673if(defined$extra) {4674print"<tr>\n".4675"<td colspan=\"4\">$extra</td>\n".4676"</tr>\n";4677}4678print"</table>\n";4679}46804681sub git_history_body {4682# Warning: assumes constant type (blob or tree) during history4683my($commitlist,$from,$to,$refs,$extra,4684$file_name,$file_hash,$ftype) =@_;46854686$from=0unlessdefined$from;4687$to=$#{$commitlist}unless(defined$to&&$to<=$#{$commitlist});46884689print"<table class=\"history\">\n";4690my$alternate=1;4691for(my$i=$from;$i<=$to;$i++) {4692my%co= %{$commitlist->[$i]};4693if(!%co) {4694next;4695}4696my$commit=$co{'id'};46974698my$ref= format_ref_marker($refs,$commit);46994700if($alternate) {4701print"<tr class=\"dark\">\n";4702}else{4703print"<tr class=\"light\">\n";4704}4705$alternate^=1;4706print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4707# shortlog: format_author_html('td', \%co, 10)4708 format_author_html('td', \%co,15,3) ."<td>";4709# originally git_history used chop_str($co{'title'}, 50)4710print format_subject_html($co{'title'},$co{'title_short'},4711 href(action=>"commit", hash=>$commit),$ref);4712print"</td>\n".4713"<td class=\"link\">".4714$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)},$ftype) ." | ".4715$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff");47164717if($ftypeeq'blob') {4718my$blob_current=$file_hash;4719my$blob_parent= git_get_hash_by_path($commit,$file_name);4720if(defined$blob_current&&defined$blob_parent&&4721$blob_currentne$blob_parent) {4722print" | ".4723$cgi->a({-href => href(action=>"blobdiff",4724 hash=>$blob_current, hash_parent=>$blob_parent,4725 hash_base=>$hash_base, hash_parent_base=>$commit,4726 file_name=>$file_name)},4727"diff to current");4728}4729}4730print"</td>\n".4731"</tr>\n";4732}4733if(defined$extra) {4734print"<tr>\n".4735"<td colspan=\"4\">$extra</td>\n".4736"</tr>\n";4737}4738print"</table>\n";4739}47404741sub git_tags_body {4742# uses global variable $project4743my($taglist,$from,$to,$extra) =@_;4744$from=0unlessdefined$from;4745$to=$#{$taglist}if(!defined$to||$#{$taglist} <$to);47464747print"<table class=\"tags\">\n";4748my$alternate=1;4749for(my$i=$from;$i<=$to;$i++) {4750my$entry=$taglist->[$i];4751my%tag=%$entry;4752my$comment=$tag{'subject'};4753my$comment_short;4754if(defined$comment) {4755$comment_short= chop_str($comment,30,5);4756}4757if($alternate) {4758print"<tr class=\"dark\">\n";4759}else{4760print"<tr class=\"light\">\n";4761}4762$alternate^=1;4763if(defined$tag{'age'}) {4764print"<td><i>$tag{'age'}</i></td>\n";4765}else{4766print"<td></td>\n";4767}4768print"<td>".4769$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),4770-class=>"list name"}, esc_html($tag{'name'})) .4771"</td>\n".4772"<td>";4773if(defined$comment) {4774print format_subject_html($comment,$comment_short,4775 href(action=>"tag", hash=>$tag{'id'}));4776}4777print"</td>\n".4778"<td class=\"selflink\">";4779if($tag{'type'}eq"tag") {4780print$cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})},"tag");4781}else{4782print" ";4783}4784print"</td>\n".4785"<td class=\"link\">"." | ".4786$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})},$tag{'reftype'});4787if($tag{'reftype'}eq"commit") {4788print" | ".$cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})},"shortlog") .4789" | ".$cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})},"log");4790}elsif($tag{'reftype'}eq"blob") {4791print" | ".$cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})},"raw");4792}4793print"</td>\n".4794"</tr>";4795}4796if(defined$extra) {4797print"<tr>\n".4798"<td colspan=\"5\">$extra</td>\n".4799"</tr>\n";4800}4801print"</table>\n";4802}48034804sub git_heads_body {4805# uses global variable $project4806my($headlist,$head,$from,$to,$extra) =@_;4807$from=0unlessdefined$from;4808$to=$#{$headlist}if(!defined$to||$#{$headlist} <$to);48094810print"<table class=\"heads\">\n";4811my$alternate=1;4812for(my$i=$from;$i<=$to;$i++) {4813my$entry=$headlist->[$i];4814my%ref=%$entry;4815my$curr=$ref{'id'}eq$head;4816if($alternate) {4817print"<tr class=\"dark\">\n";4818}else{4819print"<tr class=\"light\">\n";4820}4821$alternate^=1;4822print"<td><i>$ref{'age'}</i></td>\n".4823($curr?"<td class=\"current_head\">":"<td>") .4824$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),4825-class=>"list name"},esc_html($ref{'name'})) .4826"</td>\n".4827"<td class=\"link\">".4828$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})},"shortlog") ." | ".4829$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})},"log") ." | ".4830$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})},"tree") .4831"</td>\n".4832"</tr>";4833}4834if(defined$extra) {4835print"<tr>\n".4836"<td colspan=\"3\">$extra</td>\n".4837"</tr>\n";4838}4839print"</table>\n";4840}48414842sub git_search_grep_body {4843my($commitlist,$from,$to,$extra) =@_;4844$from=0unlessdefined$from;4845$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);48464847print"<table class=\"commit_search\">\n";4848my$alternate=1;4849for(my$i=$from;$i<=$to;$i++) {4850my%co= %{$commitlist->[$i]};4851if(!%co) {4852next;4853}4854my$commit=$co{'id'};4855if($alternate) {4856print"<tr class=\"dark\">\n";4857}else{4858print"<tr class=\"light\">\n";4859}4860$alternate^=1;4861print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4862 format_author_html('td', \%co,15,5) .4863"<td>".4864$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),4865-class=>"list subject"},4866 chop_and_escape_str($co{'title'},50) ."<br/>");4867my$comment=$co{'comment'};4868foreachmy$line(@$comment) {4869if($line=~m/^(.*?)($search_regexp)(.*)$/i) {4870my($lead,$match,$trail) = ($1,$2,$3);4871$match= chop_str($match,70,5,'center');4872my$contextlen=int((80-length($match))/2);4873$contextlen=30if($contextlen>30);4874$lead= chop_str($lead,$contextlen,10,'left');4875$trail= chop_str($trail,$contextlen,10,'right');48764877$lead= esc_html($lead);4878$match= esc_html($match);4879$trail= esc_html($trail);48804881print"$lead<span class=\"match\">$match</span>$trail<br />";4882}4883}4884print"</td>\n".4885"<td class=\"link\">".4886$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .4887" | ".4888$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})},"commitdiff") .4889" | ".4890$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");4891print"</td>\n".4892"</tr>\n";4893}4894if(defined$extra) {4895print"<tr>\n".4896"<td colspan=\"3\">$extra</td>\n".4897"</tr>\n";4898}4899print"</table>\n";4900}49014902## ======================================================================4903## ======================================================================4904## actions49054906sub git_project_list {4907my$order=$input_params{'order'};4908if(defined$order&&$order!~m/none|project|descr|owner|age/) {4909 die_error(400,"Unknown order parameter");4910}49114912my@list= git_get_projects_list();4913if(!@list) {4914 die_error(404,"No projects found");4915}49164917 git_header_html();4918if(defined$home_text&& -f $home_text) {4919print"<div class=\"index_include\">\n";4920 insert_file($home_text);4921print"</div>\n";4922}4923print$cgi->startform(-method=>"get") .4924"<p class=\"projsearch\">Search:\n".4925$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".4926"</p>".4927$cgi->end_form() ."\n";4928 git_project_list_body(\@list,$order);4929 git_footer_html();4930}49314932sub git_forks {4933my$order=$input_params{'order'};4934if(defined$order&&$order!~m/none|project|descr|owner|age/) {4935 die_error(400,"Unknown order parameter");4936}49374938my@list= git_get_projects_list($project);4939if(!@list) {4940 die_error(404,"No forks found");4941}49424943 git_header_html();4944 git_print_page_nav('','');4945 git_print_header_div('summary',"$projectforks");4946 git_project_list_body(\@list,$order);4947 git_footer_html();4948}49494950sub git_project_index {4951my@projects= git_get_projects_list($project);49524953print$cgi->header(4954-type =>'text/plain',4955-charset =>'utf-8',4956-content_disposition =>'inline; filename="index.aux"');49574958foreachmy$pr(@projects) {4959if(!exists$pr->{'owner'}) {4960$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");4961}49624963my($path,$owner) = ($pr->{'path'},$pr->{'owner'});4964# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '4965$path=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4966$owner=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4967$path=~s/ /\+/g;4968$owner=~s/ /\+/g;49694970print"$path$owner\n";4971}4972}49734974sub git_summary {4975my$descr= git_get_project_description($project) ||"none";4976my%co= parse_commit("HEAD");4977my%cd=%co? parse_date($co{'committer_epoch'},$co{'committer_tz'}) : ();4978my$head=$co{'id'};49794980my$owner= git_get_project_owner($project);49814982my$refs= git_get_references();4983# These get_*_list functions return one more to allow us to see if4984# there are more ...4985my@taglist= git_get_tags_list(16);4986my@headlist= git_get_heads_list(16);4987my@forklist;4988my$check_forks= gitweb_check_feature('forks');49894990if($check_forks) {4991@forklist= git_get_projects_list($project);4992}49934994 git_header_html();4995 git_print_page_nav('summary','',$head);49964997print"<div class=\"title\"> </div>\n";4998print"<table class=\"projects_list\">\n".4999"<tr id=\"metadata_desc\"><td>description</td><td>". esc_html($descr) ."</td></tr>\n".5000"<tr id=\"metadata_owner\"><td>owner</td><td>". esc_html($owner) ."</td></tr>\n";5001if(defined$cd{'rfc2822'}) {5002print"<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";5003}50045005# use per project git URL list in $projectroot/$project/cloneurl5006# or make project git URL from git base URL and project name5007my$url_tag="URL";5008my@url_list= git_get_project_url_list($project);5009@url_list=map{"$_/$project"}@git_base_url_listunless@url_list;5010foreachmy$git_url(@url_list) {5011next unless$git_url;5012print"<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";5013$url_tag="";5014}50155016# Tag cloud5017my$show_ctags= gitweb_check_feature('ctags');5018if($show_ctags) {5019my$ctags= git_get_project_ctags($project);5020my$cloud= git_populate_project_tagcloud($ctags);5021print"<tr id=\"metadata_ctags\"><td>Content tags:<br />";5022print"</td>\n<td>"unless%$ctags;5023print"<form action=\"$show_ctags\"method=\"post\"><input type=\"hidden\"name=\"p\"value=\"$project\"/>Add: <input type=\"text\"name=\"t\"size=\"8\"/></form>";5024print"</td>\n<td>"if%$ctags;5025print git_show_project_tagcloud($cloud,48);5026print"</td></tr>";5027}50285029print"</table>\n";50305031# If XSS prevention is on, we don't include README.html.5032# TODO: Allow a readme in some safe format.5033if(!$prevent_xss&& -s "$projectroot/$project/README.html") {5034print"<div class=\"title\">readme</div>\n".5035"<div class=\"readme\">\n";5036 insert_file("$projectroot/$project/README.html");5037print"\n</div>\n";# class="readme"5038}50395040# we need to request one more than 16 (0..15) to check if5041# those 16 are all5042my@commitlist=$head? parse_commits($head,17) : ();5043if(@commitlist) {5044 git_print_header_div('shortlog');5045 git_shortlog_body(\@commitlist,0,15,$refs,5046$#commitlist<=15?undef:5047$cgi->a({-href => href(action=>"shortlog")},"..."));5048}50495050if(@taglist) {5051 git_print_header_div('tags');5052 git_tags_body(\@taglist,0,15,5053$#taglist<=15?undef:5054$cgi->a({-href => href(action=>"tags")},"..."));5055}50565057if(@headlist) {5058 git_print_header_div('heads');5059 git_heads_body(\@headlist,$head,0,15,5060$#headlist<=15?undef:5061$cgi->a({-href => href(action=>"heads")},"..."));5062}50635064if(@forklist) {5065 git_print_header_div('forks');5066 git_project_list_body(\@forklist,'age',0,15,5067$#forklist<=15?undef:5068$cgi->a({-href => href(action=>"forks")},"..."),5069'no_header');5070}50715072 git_footer_html();5073}50745075sub git_tag {5076my$head= git_get_head_hash($project);5077 git_header_html();5078 git_print_page_nav('','',$head,undef,$head);5079my%tag= parse_tag($hash);50805081if(!%tag) {5082 die_error(404,"Unknown tag object");5083}50845085 git_print_header_div('commit', esc_html($tag{'name'}),$hash);5086print"<div class=\"title_text\">\n".5087"<table class=\"object_header\">\n".5088"<tr>\n".5089"<td>object</td>\n".5090"<td>".$cgi->a({-class=>"list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5091$tag{'object'}) ."</td>\n".5092"<td class=\"link\">".$cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},5093$tag{'type'}) ."</td>\n".5094"</tr>\n";5095if(defined($tag{'author'})) {5096 git_print_authorship_rows(\%tag,'author');5097}5098print"</table>\n\n".5099"</div>\n";5100print"<div class=\"page_body\">";5101my$comment=$tag{'comment'};5102foreachmy$line(@$comment) {5103chomp$line;5104print esc_html($line, -nbsp=>1) ."<br/>\n";5105}5106print"</div>\n";5107 git_footer_html();5108}51095110sub git_blame_common {5111my$format=shift||'porcelain';5112if($formateq'porcelain'&&$cgi->param('js')) {5113$format='incremental';5114$action='blame_incremental';# for page title etc5115}51165117# permissions5118 gitweb_check_feature('blame')5119or die_error(403,"Blame view not allowed");51205121# error checking5122 die_error(400,"No file name given")unless$file_name;5123$hash_base||= git_get_head_hash($project);5124 die_error(404,"Couldn't find base commit")unless$hash_base;5125my%co= parse_commit($hash_base)5126or die_error(404,"Commit not found");5127my$ftype="blob";5128if(!defined$hash) {5129$hash= git_get_hash_by_path($hash_base,$file_name,"blob")5130or die_error(404,"Error looking up file");5131}else{5132$ftype= git_get_type($hash);5133if($ftype!~"blob") {5134 die_error(400,"Object is not a blob");5135}5136}51375138my$fd;5139if($formateq'incremental') {5140# get file contents (as base)5141open$fd,"-|", git_cmd(),'cat-file','blob',$hash5142or die_error(500,"Open git-cat-file failed");5143}elsif($formateq'data') {5144# run git-blame --incremental5145open$fd,"-|", git_cmd(),"blame","--incremental",5146$hash_base,"--",$file_name5147or die_error(500,"Open git-blame --incremental failed");5148}else{5149# run git-blame --porcelain5150open$fd,"-|", git_cmd(),"blame",'-p',5151$hash_base,'--',$file_name5152or die_error(500,"Open git-blame --porcelain failed");5153}51545155# incremental blame data returns early5156if($formateq'data') {5157print$cgi->header(5158-type=>"text/plain", -charset =>"utf-8",5159-status=>"200 OK");5160local$| =1;# output autoflush5161printwhile<$fd>;5162close$fd5163or print"ERROR$!\n";51645165print'END';5166if(defined$t0&& gitweb_check_feature('timed')) {5167print' '.5168 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).5169' '.$number_of_git_cmds;5170}5171print"\n";51725173return;5174}51755176# page header5177 git_header_html();5178my$formats_nav=5179$cgi->a({-href => href(action=>"blob", -replay=>1)},5180"blob") .5181" | ";5182if($formateq'incremental') {5183$formats_nav.=5184$cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},5185"blame") ." (non-incremental)";5186}else{5187$formats_nav.=5188$cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},5189"blame") ." (incremental)";5190}5191$formats_nav.=5192" | ".5193$cgi->a({-href => href(action=>"history", -replay=>1)},5194"history") .5195" | ".5196$cgi->a({-href => href(action=>$action, file_name=>$file_name)},5197"HEAD");5198 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5199 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5200 git_print_page_path($file_name,$ftype,$hash_base);52015202# page body5203if($formateq'incremental') {5204print"<noscript>\n<div class=\"error\"><center><b>\n".5205"This page requires JavaScript to run.\nUse ".5206$cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},5207'this page').5208" instead.\n".5209"</b></center></div>\n</noscript>\n";52105211print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;5212}52135214print qq!<div class="page_body">\n!;5215print qq!<div id="progress_info">.../ ...</div>\n!5216if($formateq'incremental');5217print qq!<table id="blame_table"class="blame" width="100%">\n!.5218#qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.5219 qq!<thead>\n!.5220 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.5221 qq!</thead>\n!.5222 qq!<tbody>\n!;52235224my@rev_color=qw(light dark);5225my$num_colors=scalar(@rev_color);5226my$current_color=0;52275228if($formateq'incremental') {5229my$color_class=$rev_color[$current_color];52305231#contents of a file5232my$linenr=0;5233 LINE:5234while(my$line= <$fd>) {5235chomp$line;5236$linenr++;52375238print qq!<tr id="l$linenr"class="$color_class">!.5239 qq!<td class="sha1"><a href=""> </a></td>!.5240 qq!<td class="linenr">!.5241 qq!<a class="linenr" href="">$linenr</a></td>!;5242print qq!<td class="pre">! . esc_html($line) ."</td>\n";5243print qq!</tr>\n!;5244}52455246}else{# porcelain, i.e. ordinary blame5247my%metainfo= ();# saves information about commits52485249# blame data5250 LINE:5251while(my$line= <$fd>) {5252chomp$line;5253# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]5254# no <lines in group> for subsequent lines in group of lines5255my($full_rev,$orig_lineno,$lineno,$group_size) =5256($line=~/^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);5257if(!exists$metainfo{$full_rev}) {5258$metainfo{$full_rev} = {'nprevious'=>0};5259}5260my$meta=$metainfo{$full_rev};5261my$data;5262while($data= <$fd>) {5263chomp$data;5264last if($data=~s/^\t//);# contents of line5265if($data=~/^(\S+)(?: (.*))?$/) {5266$meta->{$1} =$2unlessexists$meta->{$1};5267}5268if($data=~/^previous /) {5269$meta->{'nprevious'}++;5270}5271}5272my$short_rev=substr($full_rev,0,8);5273my$author=$meta->{'author'};5274my%date=5275 parse_date($meta->{'author-time'},$meta->{'author-tz'});5276my$date=$date{'iso-tz'};5277if($group_size) {5278$current_color= ($current_color+1) %$num_colors;5279}5280my$tr_class=$rev_color[$current_color];5281$tr_class.=' boundary'if(exists$meta->{'boundary'});5282$tr_class.=' no-previous'if($meta->{'nprevious'} ==0);5283$tr_class.=' multiple-previous'if($meta->{'nprevious'} >1);5284print"<tr id=\"l$lineno\"class=\"$tr_class\">\n";5285if($group_size) {5286print"<td class=\"sha1\"";5287print" title=\"". esc_html($author) .",$date\"";5288print" rowspan=\"$group_size\""if($group_size>1);5289print">";5290print$cgi->a({-href => href(action=>"commit",5291 hash=>$full_rev,5292 file_name=>$file_name)},5293 esc_html($short_rev));5294if($group_size>=2) {5295my@author_initials= ($author=~/\b([[:upper:]])\B/g);5296if(@author_initials) {5297print"<br />".5298 esc_html(join('',@author_initials));5299# or join('.', ...)5300}5301}5302print"</td>\n";5303}5304# 'previous' <sha1 of parent commit> <filename at commit>5305if(exists$meta->{'previous'} &&5306$meta->{'previous'} =~/^([a-fA-F0-9]{40}) (.*)$/) {5307$meta->{'parent'} =$1;5308$meta->{'file_parent'} = unquote($2);5309}5310my$linenr_commit=5311exists($meta->{'parent'}) ?5312$meta->{'parent'} :$full_rev;5313my$linenr_filename=5314exists($meta->{'file_parent'}) ?5315$meta->{'file_parent'} : unquote($meta->{'filename'});5316my$blamed= href(action =>'blame',5317 file_name =>$linenr_filename,5318 hash_base =>$linenr_commit);5319print"<td class=\"linenr\">";5320print$cgi->a({ -href =>"$blamed#l$orig_lineno",5321-class=>"linenr"},5322 esc_html($lineno));5323print"</td>";5324print"<td class=\"pre\">". esc_html($data) ."</td>\n";5325print"</tr>\n";5326}# end while53275328}53295330# footer5331print"</tbody>\n".5332"</table>\n";# class="blame"5333print"</div>\n";# class="blame_body"5334close$fd5335or print"Reading blob failed\n";53365337 git_footer_html();5338}53395340sub git_blame {5341 git_blame_common();5342}53435344sub git_blame_incremental {5345 git_blame_common('incremental');5346}53475348sub git_blame_data {5349 git_blame_common('data');5350}53515352sub git_tags {5353my$head= git_get_head_hash($project);5354 git_header_html();5355 git_print_page_nav('','',$head,undef,$head);5356 git_print_header_div('summary',$project);53575358my@tagslist= git_get_tags_list();5359if(@tagslist) {5360 git_tags_body(\@tagslist);5361}5362 git_footer_html();5363}53645365sub git_heads {5366my$head= git_get_head_hash($project);5367 git_header_html();5368 git_print_page_nav('','',$head,undef,$head);5369 git_print_header_div('summary',$project);53705371my@headslist= git_get_heads_list();5372if(@headslist) {5373 git_heads_body(\@headslist,$head);5374}5375 git_footer_html();5376}53775378sub git_blob_plain {5379my$type=shift;5380my$expires;53815382if(!defined$hash) {5383if(defined$file_name) {5384my$base=$hash_base|| git_get_head_hash($project);5385$hash= git_get_hash_by_path($base,$file_name,"blob")5386or die_error(404,"Cannot find file");5387}else{5388 die_error(400,"No file name defined");5389}5390}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5391# blobs defined by non-textual hash id's can be cached5392$expires="+1d";5393}53945395open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5396or die_error(500,"Open git-cat-file blob '$hash' failed");53975398# content-type (can include charset)5399$type= blob_contenttype($fd,$file_name,$type);54005401# "save as" filename, even when no $file_name is given5402my$save_as="$hash";5403if(defined$file_name) {5404$save_as=$file_name;5405}elsif($type=~m/^text\//) {5406$save_as.='.txt';5407}54085409# With XSS prevention on, blobs of all types except a few known safe5410# ones are served with "Content-Disposition: attachment" to make sure5411# they don't run in our security domain. For certain image types,5412# blob view writes an <img> tag referring to blob_plain view, and we5413# want to be sure not to break that by serving the image as an5414# attachment (though Firefox 3 doesn't seem to care).5415my$sandbox=$prevent_xss&&5416$type!~m!^(?:text/plain|image/(?:gif|png|jpeg))$!;54175418print$cgi->header(5419-type =>$type,5420-expires =>$expires,5421-content_disposition =>5422($sandbox?'attachment':'inline')5423.'; filename="'.$save_as.'"');5424local$/=undef;5425binmode STDOUT,':raw';5426print<$fd>;5427binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5428close$fd;5429}54305431sub git_blob {5432my$expires;54335434if(!defined$hash) {5435if(defined$file_name) {5436my$base=$hash_base|| git_get_head_hash($project);5437$hash= git_get_hash_by_path($base,$file_name,"blob")5438or die_error(404,"Cannot find file");5439}else{5440 die_error(400,"No file name defined");5441}5442}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {5443# blobs defined by non-textual hash id's can be cached5444$expires="+1d";5445}54465447my$have_blame= gitweb_check_feature('blame');5448open my$fd,"-|", git_cmd(),"cat-file","blob",$hash5449or die_error(500,"Couldn't cat$file_name,$hash");5450my$mimetype= blob_mimetype($fd,$file_name);5451# use 'blob_plain' (aka 'raw') view for files that cannot be displayed5452if($mimetype!~m!^(?:text/|image/(?:gif|png|jpeg)$)!&& -B $fd) {5453close$fd;5454return git_blob_plain($mimetype);5455}5456# we can have blame only for text/* mimetype5457$have_blame&&= ($mimetype=~m!^text/!);54585459my$highlight= gitweb_check_feature('highlight');5460my$syntax= guess_file_syntax($highlight,$mimetype,$file_name);5461$fd= run_highlighter($fd,$highlight,$syntax)5462if$syntax;54635464 git_header_html(undef,$expires);5465my$formats_nav='';5466if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5467if(defined$file_name) {5468if($have_blame) {5469$formats_nav.=5470$cgi->a({-href => href(action=>"blame", -replay=>1)},5471"blame") .5472" | ";5473}5474$formats_nav.=5475$cgi->a({-href => href(action=>"history", -replay=>1)},5476"history") .5477" | ".5478$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5479"raw") .5480" | ".5481$cgi->a({-href => href(action=>"blob",5482 hash_base=>"HEAD", file_name=>$file_name)},5483"HEAD");5484}else{5485$formats_nav.=5486$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},5487"raw");5488}5489 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5490 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5491}else{5492print"<div class=\"page_nav\">\n".5493"<br/><br/></div>\n".5494"<div class=\"title\">$hash</div>\n";5495}5496 git_print_page_path($file_name,"blob",$hash_base);5497print"<div class=\"page_body\">\n";5498if($mimetype=~m!^image/!) {5499print qq!<img type="$mimetype"!;5500if($file_name) {5501print qq! alt="$file_name" title="$file_name"!;5502}5503print qq! src="! .5504 href(action=>"blob_plain", hash=>$hash,5505 hash_base=>$hash_base, file_name=>$file_name) .5506 qq!"/>\n!;5507}else{5508my$nr;5509while(my$line= <$fd>) {5510chomp$line;5511$nr++;5512$line= untabify($line);5513printf qq!<div class="pre"><a id="l%i" href="%s#l%i"class="linenr">%4i</a> %s</div>\n!,5514$nr, href(-replay =>1),$nr,$nr,$syntax?$line: esc_html($line, -nbsp=>1);5515}5516}5517close$fd5518or print"Reading blob failed.\n";5519print"</div>";5520 git_footer_html();5521}55225523sub git_tree {5524if(!defined$hash_base) {5525$hash_base="HEAD";5526}5527if(!defined$hash) {5528if(defined$file_name) {5529$hash= git_get_hash_by_path($hash_base,$file_name,"tree");5530}else{5531$hash=$hash_base;5532}5533}5534 die_error(404,"No such tree")unlessdefined($hash);55355536my$show_sizes= gitweb_check_feature('show-sizes');5537my$have_blame= gitweb_check_feature('blame');55385539my@entries= ();5540{5541local$/="\0";5542open my$fd,"-|", git_cmd(),"ls-tree",'-z',5543($show_sizes?'-l': ()),@extra_options,$hash5544or die_error(500,"Open git-ls-tree failed");5545@entries=map{chomp;$_} <$fd>;5546close$fd5547or die_error(404,"Reading tree failed");5548}55495550my$refs= git_get_references();5551my$ref= format_ref_marker($refs,$hash_base);5552 git_header_html();5553my$basedir='';5554if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5555my@views_nav= ();5556if(defined$file_name) {5557push@views_nav,5558$cgi->a({-href => href(action=>"history", -replay=>1)},5559"history"),5560$cgi->a({-href => href(action=>"tree",5561 hash_base=>"HEAD", file_name=>$file_name)},5562"HEAD"),5563}5564my$snapshot_links= format_snapshot_links($hash);5565if(defined$snapshot_links) {5566# FIXME: Should be available when we have no hash base as well.5567push@views_nav,$snapshot_links;5568}5569 git_print_page_nav('tree','',$hash_base,undef,undef,5570join(' | ',@views_nav));5571 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash_base);5572}else{5573undef$hash_base;5574print"<div class=\"page_nav\">\n";5575print"<br/><br/></div>\n";5576print"<div class=\"title\">$hash</div>\n";5577}5578if(defined$file_name) {5579$basedir=$file_name;5580if($basedirne''&&substr($basedir, -1)ne'/') {5581$basedir.='/';5582}5583 git_print_page_path($file_name,'tree',$hash_base);5584}5585print"<div class=\"page_body\">\n";5586print"<table class=\"tree\">\n";5587my$alternate=1;5588# '..' (top directory) link if possible5589if(defined$hash_base&&5590defined$file_name&&$file_name=~m![^/]+$!) {5591if($alternate) {5592print"<tr class=\"dark\">\n";5593}else{5594print"<tr class=\"light\">\n";5595}5596$alternate^=1;55975598my$up=$file_name;5599$up=~s!/?[^/]+$!!;5600undef$upunless$up;5601# based on git_print_tree_entry5602print'<td class="mode">'. mode_str('040000') ."</td>\n";5603print'<td class="size"> </td>'."\n"if$show_sizes;5604print'<td class="list">';5605print$cgi->a({-href => href(action=>"tree",5606 hash_base=>$hash_base,5607 file_name=>$up)},5608"..");5609print"</td>\n";5610print"<td class=\"link\"></td>\n";56115612print"</tr>\n";5613}5614foreachmy$line(@entries) {5615my%t= parse_ls_tree_line($line, -z =>1, -l =>$show_sizes);56165617if($alternate) {5618print"<tr class=\"dark\">\n";5619}else{5620print"<tr class=\"light\">\n";5621}5622$alternate^=1;56235624 git_print_tree_entry(\%t,$basedir,$hash_base,$have_blame);56255626print"</tr>\n";5627}5628print"</table>\n".5629"</div>";5630 git_footer_html();5631}56325633sub snapshot_name {5634my($project,$hash) =@_;56355636# path/to/project.git -> project5637# path/to/project/.git -> project5638my$name= to_utf8($project);5639$name=~ s,([^/])/*\.git$,$1,;5640$name= basename($name);5641# sanitize name5642$name=~s/[[:cntrl:]]/?/g;56435644my$ver=$hash;5645if($hash=~/^[0-9a-fA-F]+$/) {5646# shorten SHA-1 hash5647my$full_hash= git_get_full_hash($project,$hash);5648if($full_hash=~/^$hash/&&length($hash) >7) {5649$ver= git_get_short_hash($project,$hash);5650}5651}elsif($hash=~m!^refs/tags/(.*)$!) {5652# tags don't need shortened SHA-1 hash5653$ver=$1;5654}else{5655# branches and other need shortened SHA-1 hash5656if($hash=~m!^refs/(?:heads|remotes)/(.*)$!) {5657$ver=$1;5658}5659$ver.='-'. git_get_short_hash($project,$hash);5660}5661# in case of hierarchical branch names5662$ver=~s!/!.!g;56635664# name = project-version_string5665$name="$name-$ver";56665667returnwantarray? ($name,$name) :$name;5668}56695670sub git_snapshot {5671my$format=$input_params{'snapshot_format'};5672if(!@snapshot_fmts) {5673 die_error(403,"Snapshots not allowed");5674}5675# default to first supported snapshot format5676$format||=$snapshot_fmts[0];5677if($format!~m/^[a-z0-9]+$/) {5678 die_error(400,"Invalid snapshot format parameter");5679}elsif(!exists($known_snapshot_formats{$format})) {5680 die_error(400,"Unknown snapshot format");5681}elsif($known_snapshot_formats{$format}{'disabled'}) {5682 die_error(403,"Snapshot format not allowed");5683}elsif(!grep($_eq$format,@snapshot_fmts)) {5684 die_error(403,"Unsupported snapshot format");5685}56865687my$type= git_get_type("$hash^{}");5688if(!$type) {5689 die_error(404,'Object does not exist');5690}elsif($typeeq'blob') {5691 die_error(400,'Object is not a tree-ish');5692}56935694my($name,$prefix) = snapshot_name($project,$hash);5695my$filename="$name$known_snapshot_formats{$format}{'suffix'}";5696my$cmd= quote_command(5697 git_cmd(),'archive',5698"--format=$known_snapshot_formats{$format}{'format'}",5699"--prefix=$prefix/",$hash);5700if(exists$known_snapshot_formats{$format}{'compressor'}) {5701$cmd.=' | '. quote_command(@{$known_snapshot_formats{$format}{'compressor'}});5702}57035704$filename=~s/(["\\])/\\$1/g;5705print$cgi->header(5706-type =>$known_snapshot_formats{$format}{'type'},5707-content_disposition =>'inline; filename="'.$filename.'"',5708-status =>'200 OK');57095710open my$fd,"-|",$cmd5711or die_error(500,"Execute git-archive failed");5712binmode STDOUT,':raw';5713print<$fd>;5714binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5715close$fd;5716}57175718sub git_log_generic {5719my($fmt_name,$body_subr,$base,$parent,$file_name,$file_hash) =@_;57205721my$head= git_get_head_hash($project);5722if(!defined$base) {5723$base=$head;5724}5725if(!defined$page) {5726$page=0;5727}5728my$refs= git_get_references();57295730my$commit_hash=$base;5731if(defined$parent) {5732$commit_hash="$parent..$base";5733}5734my@commitlist=5735 parse_commits($commit_hash,101, (100*$page),5736defined$file_name? ($file_name,"--full-history") : ());57375738my$ftype;5739if(!defined$file_hash&&defined$file_name) {5740# some commits could have deleted file in question,5741# and not have it in tree, but one of them has to have it5742for(my$i=0;$i<@commitlist;$i++) {5743$file_hash= git_get_hash_by_path($commitlist[$i]{'id'},$file_name);5744last ifdefined$file_hash;5745}5746}5747if(defined$file_hash) {5748$ftype= git_get_type($file_hash);5749}5750if(defined$file_name&& !defined$ftype) {5751 die_error(500,"Unknown type of object");5752}5753my%co;5754if(defined$file_name) {5755%co= parse_commit($base)5756or die_error(404,"Unknown commit object");5757}575857595760my$paging_nav= format_paging_nav($fmt_name,$page,$#commitlist>=100);5761my$next_link='';5762if($#commitlist>=100) {5763$next_link=5764$cgi->a({-href => href(-replay=>1, page=>$page+1),5765-accesskey =>"n", -title =>"Alt-n"},"next");5766}5767my$patch_max= gitweb_get_feature('patches');5768if($patch_max&& !defined$file_name) {5769if($patch_max<0||@commitlist<=$patch_max) {5770$paging_nav.=" ⋅ ".5771$cgi->a({-href => href(action=>"patches", -replay=>1)},5772"patches");5773}5774}57755776 git_header_html();5777 git_print_page_nav($fmt_name,'',$hash,$hash,$hash,$paging_nav);5778if(defined$file_name) {5779 git_print_header_div('commit', esc_html($co{'title'}),$base);5780}else{5781 git_print_header_div('summary',$project)5782}5783 git_print_page_path($file_name,$ftype,$hash_base)5784if(defined$file_name);57855786$body_subr->(\@commitlist,0,99,$refs,$next_link,5787$file_name,$file_hash,$ftype);57885789 git_footer_html();5790}57915792sub git_log {5793 git_log_generic('log', \&git_log_body,5794$hash,$hash_parent);5795}57965797sub git_commit {5798$hash||=$hash_base||"HEAD";5799my%co= parse_commit($hash)5800or die_error(404,"Unknown commit object");58015802my$parent=$co{'parent'};5803my$parents=$co{'parents'};# listref58045805# we need to prepare $formats_nav before any parameter munging5806my$formats_nav;5807if(!defined$parent) {5808# --root commitdiff5809$formats_nav.='(initial)';5810}elsif(@$parents==1) {5811# single parent commit5812$formats_nav.=5813'(parent: '.5814$cgi->a({-href => href(action=>"commit",5815 hash=>$parent)},5816 esc_html(substr($parent,0,7))) .5817')';5818}else{5819# merge commit5820$formats_nav.=5821'(merge: '.5822join(' ',map{5823$cgi->a({-href => href(action=>"commit",5824 hash=>$_)},5825 esc_html(substr($_,0,7)));5826}@$parents) .5827')';5828}5829if(gitweb_check_feature('patches') &&@$parents<=1) {5830$formats_nav.=" | ".5831$cgi->a({-href => href(action=>"patch", -replay=>1)},5832"patch");5833}58345835if(!defined$parent) {5836$parent="--root";5837}5838my@difftree;5839open my$fd,"-|", git_cmd(),"diff-tree",'-r',"--no-commit-id",5840@diff_opts,5841(@$parents<=1?$parent:'-c'),5842$hash,"--"5843or die_error(500,"Open git-diff-tree failed");5844@difftree=map{chomp;$_} <$fd>;5845close$fdor die_error(404,"Reading git-diff-tree failed");58465847# non-textual hash id's can be cached5848my$expires;5849if($hash=~m/^[0-9a-fA-F]{40}$/) {5850$expires="+1d";5851}5852my$refs= git_get_references();5853my$ref= format_ref_marker($refs,$co{'id'});58545855 git_header_html(undef,$expires);5856 git_print_page_nav('commit','',5857$hash,$co{'tree'},$hash,5858$formats_nav);58595860if(defined$co{'parent'}) {5861 git_print_header_div('commitdiff', esc_html($co{'title'}) .$ref,$hash);5862}else{5863 git_print_header_div('tree', esc_html($co{'title'}) .$ref,$co{'tree'},$hash);5864}5865print"<div class=\"title_text\">\n".5866"<table class=\"object_header\">\n";5867 git_print_authorship_rows(\%co);5868print"<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";5869print"<tr>".5870"<td>tree</td>".5871"<td class=\"sha1\">".5872$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),5873class=>"list"},$co{'tree'}) .5874"</td>".5875"<td class=\"link\">".5876$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},5877"tree");5878my$snapshot_links= format_snapshot_links($hash);5879if(defined$snapshot_links) {5880print" | ".$snapshot_links;5881}5882print"</td>".5883"</tr>\n";58845885foreachmy$par(@$parents) {5886print"<tr>".5887"<td>parent</td>".5888"<td class=\"sha1\">".5889$cgi->a({-href => href(action=>"commit", hash=>$par),5890class=>"list"},$par) .5891"</td>".5892"<td class=\"link\">".5893$cgi->a({-href => href(action=>"commit", hash=>$par)},"commit") .5894" | ".5895$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)},"diff") .5896"</td>".5897"</tr>\n";5898}5899print"</table>".5900"</div>\n";59015902print"<div class=\"page_body\">\n";5903 git_print_log($co{'comment'});5904print"</div>\n";59055906 git_difftree_body(\@difftree,$hash,@$parents);59075908 git_footer_html();5909}59105911sub git_object {5912# object is defined by:5913# - hash or hash_base alone5914# - hash_base and file_name5915my$type;59165917# - hash or hash_base alone5918if($hash|| ($hash_base&& !defined$file_name)) {5919my$object_id=$hash||$hash_base;59205921open my$fd,"-|", quote_command(5922 git_cmd(),'cat-file','-t',$object_id) .' 2> /dev/null'5923or die_error(404,"Object does not exist");5924$type= <$fd>;5925chomp$type;5926close$fd5927or die_error(404,"Object does not exist");59285929# - hash_base and file_name5930}elsif($hash_base&&defined$file_name) {5931$file_name=~ s,/+$,,;59325933system(git_cmd(),"cat-file",'-e',$hash_base) ==05934or die_error(404,"Base object does not exist");59355936# here errors should not hapen5937open my$fd,"-|", git_cmd(),"ls-tree",$hash_base,"--",$file_name5938or die_error(500,"Open git-ls-tree failed");5939my$line= <$fd>;5940close$fd;59415942#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'5943unless($line&&$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {5944 die_error(404,"File or directory for given base does not exist");5945}5946$type=$2;5947$hash=$3;5948}else{5949 die_error(400,"Not enough information to find object");5950}59515952print$cgi->redirect(-uri => href(action=>$type, -full=>1,5953 hash=>$hash, hash_base=>$hash_base,5954 file_name=>$file_name),5955-status =>'302 Found');5956}59575958sub git_blobdiff {5959my$format=shift||'html';59605961my$fd;5962my@difftree;5963my%diffinfo;5964my$expires;59655966# preparing $fd and %diffinfo for git_patchset_body5967# new style URI5968if(defined$hash_base&&defined$hash_parent_base) {5969if(defined$file_name) {5970# read raw output5971open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5972$hash_parent_base,$hash_base,5973"--", (defined$file_parent?$file_parent: ()),$file_name5974or die_error(500,"Open git-diff-tree failed");5975@difftree=map{chomp;$_} <$fd>;5976close$fd5977or die_error(404,"Reading git-diff-tree failed");5978@difftree5979or die_error(404,"Blob diff not found");59805981}elsif(defined$hash&&5982$hash=~/[0-9a-fA-F]{40}/) {5983# try to find filename from $hash59845985# read filtered raw output5986open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5987$hash_parent_base,$hash_base,"--"5988or die_error(500,"Open git-diff-tree failed");5989@difftree=5990# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'5991# $hash == to_id5992grep{/^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/}5993map{chomp;$_} <$fd>;5994close$fd5995or die_error(404,"Reading git-diff-tree failed");5996@difftree5997or die_error(404,"Blob diff not found");59985999}else{6000 die_error(400,"Missing one of the blob diff parameters");6001}60026003if(@difftree>1) {6004 die_error(400,"Ambiguous blob diff specification");6005}60066007%diffinfo= parse_difftree_raw_line($difftree[0]);6008$file_parent||=$diffinfo{'from_file'} ||$file_name;6009$file_name||=$diffinfo{'to_file'};60106011$hash_parent||=$diffinfo{'from_id'};6012$hash||=$diffinfo{'to_id'};60136014# non-textual hash id's can be cached6015if($hash_base=~m/^[0-9a-fA-F]{40}$/&&6016$hash_parent_base=~m/^[0-9a-fA-F]{40}$/) {6017$expires='+1d';6018}60196020# open patch output6021open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6022'-p', ($formateq'html'?"--full-index": ()),6023$hash_parent_base,$hash_base,6024"--", (defined$file_parent?$file_parent: ()),$file_name6025or die_error(500,"Open git-diff-tree failed");6026}60276028# old/legacy style URI -- not generated anymore since 1.4.3.6029if(!%diffinfo) {6030 die_error('404 Not Found',"Missing one of the blob diff parameters")6031}60326033# header6034if($formateq'html') {6035my$formats_nav=6036$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},6037"raw");6038 git_header_html(undef,$expires);6039if(defined$hash_base&& (my%co= parse_commit($hash_base))) {6040 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);6041 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);6042}else{6043print"<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";6044print"<div class=\"title\">$hashvs$hash_parent</div>\n";6045}6046if(defined$file_name) {6047 git_print_page_path($file_name,"blob",$hash_base);6048}else{6049print"<div class=\"page_path\"></div>\n";6050}60516052}elsif($formateq'plain') {6053print$cgi->header(6054-type =>'text/plain',6055-charset =>'utf-8',6056-expires =>$expires,6057-content_disposition =>'inline; filename="'."$file_name".'.patch"');60586059print"X-Git-Url: ".$cgi->self_url() ."\n\n";60606061}else{6062 die_error(400,"Unknown blobdiff format");6063}60646065# patch6066if($formateq'html') {6067print"<div class=\"page_body\">\n";60686069 git_patchset_body($fd, [ \%diffinfo],$hash_base,$hash_parent_base);6070close$fd;60716072print"</div>\n";# class="page_body"6073 git_footer_html();60746075}else{6076while(my$line= <$fd>) {6077$line=~s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;6078$line=~s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;60796080print$line;60816082last if$line=~m!^\+\+\+!;6083}6084local$/=undef;6085print<$fd>;6086close$fd;6087}6088}60896090sub git_blobdiff_plain {6091 git_blobdiff('plain');6092}60936094sub git_commitdiff {6095my%params=@_;6096my$format=$params{-format} ||'html';60976098my($patch_max) = gitweb_get_feature('patches');6099if($formateq'patch') {6100 die_error(403,"Patch view not allowed")unless$patch_max;6101}61026103$hash||=$hash_base||"HEAD";6104my%co= parse_commit($hash)6105or die_error(404,"Unknown commit object");61066107# choose format for commitdiff for merge6108if(!defined$hash_parent&& @{$co{'parents'}} >1) {6109$hash_parent='--cc';6110}6111# we need to prepare $formats_nav before almost any parameter munging6112my$formats_nav;6113if($formateq'html') {6114$formats_nav=6115$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},6116"raw");6117if($patch_max&& @{$co{'parents'}} <=1) {6118$formats_nav.=" | ".6119$cgi->a({-href => href(action=>"patch", -replay=>1)},6120"patch");6121}61226123if(defined$hash_parent&&6124$hash_parentne'-c'&&$hash_parentne'--cc') {6125# commitdiff with two commits given6126my$hash_parent_short=$hash_parent;6127if($hash_parent=~m/^[0-9a-fA-F]{40}$/) {6128$hash_parent_short=substr($hash_parent,0,7);6129}6130$formats_nav.=6131' (from';6132for(my$i=0;$i< @{$co{'parents'}};$i++) {6133if($co{'parents'}[$i]eq$hash_parent) {6134$formats_nav.=' parent '. ($i+1);6135last;6136}6137}6138$formats_nav.=': '.6139$cgi->a({-href => href(action=>"commitdiff",6140 hash=>$hash_parent)},6141 esc_html($hash_parent_short)) .6142')';6143}elsif(!$co{'parent'}) {6144# --root commitdiff6145$formats_nav.=' (initial)';6146}elsif(scalar@{$co{'parents'}} ==1) {6147# single parent commit6148$formats_nav.=6149' (parent: '.6150$cgi->a({-href => href(action=>"commitdiff",6151 hash=>$co{'parent'})},6152 esc_html(substr($co{'parent'},0,7))) .6153')';6154}else{6155# merge commit6156if($hash_parenteq'--cc') {6157$formats_nav.=' | '.6158$cgi->a({-href => href(action=>"commitdiff",6159 hash=>$hash, hash_parent=>'-c')},6160'combined');6161}else{# $hash_parent eq '-c'6162$formats_nav.=' | '.6163$cgi->a({-href => href(action=>"commitdiff",6164 hash=>$hash, hash_parent=>'--cc')},6165'compact');6166}6167$formats_nav.=6168' (merge: '.6169join(' ',map{6170$cgi->a({-href => href(action=>"commitdiff",6171 hash=>$_)},6172 esc_html(substr($_,0,7)));6173} @{$co{'parents'}} ) .6174')';6175}6176}61776178my$hash_parent_param=$hash_parent;6179if(!defined$hash_parent_param) {6180# --cc for multiple parents, --root for parentless6181$hash_parent_param=6182@{$co{'parents'}} >1?'--cc':$co{'parent'} ||'--root';6183}61846185# read commitdiff6186my$fd;6187my@difftree;6188if($formateq'html') {6189open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6190"--no-commit-id","--patch-with-raw","--full-index",6191$hash_parent_param,$hash,"--"6192or die_error(500,"Open git-diff-tree failed");61936194while(my$line= <$fd>) {6195chomp$line;6196# empty line ends raw part of diff-tree output6197last unless$line;6198push@difftree,scalar parse_difftree_raw_line($line);6199}62006201}elsif($formateq'plain') {6202open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6203'-p',$hash_parent_param,$hash,"--"6204or die_error(500,"Open git-diff-tree failed");6205}elsif($formateq'patch') {6206# For commit ranges, we limit the output to the number of6207# patches specified in the 'patches' feature.6208# For single commits, we limit the output to a single patch,6209# diverging from the git-format-patch default.6210my@commit_spec= ();6211if($hash_parent) {6212if($patch_max>0) {6213push@commit_spec,"-$patch_max";6214}6215push@commit_spec,'-n',"$hash_parent..$hash";6216}else{6217if($params{-single}) {6218push@commit_spec,'-1';6219}else{6220if($patch_max>0) {6221push@commit_spec,"-$patch_max";6222}6223push@commit_spec,"-n";6224}6225push@commit_spec,'--root',$hash;6226}6227open$fd,"-|", git_cmd(),"format-patch",@diff_opts,6228'--encoding=utf8','--stdout',@commit_spec6229or die_error(500,"Open git-format-patch failed");6230}else{6231 die_error(400,"Unknown commitdiff format");6232}62336234# non-textual hash id's can be cached6235my$expires;6236if($hash=~m/^[0-9a-fA-F]{40}$/) {6237$expires="+1d";6238}62396240# write commit message6241if($formateq'html') {6242my$refs= git_get_references();6243my$ref= format_ref_marker($refs,$co{'id'});62446245 git_header_html(undef,$expires);6246 git_print_page_nav('commitdiff','',$hash,$co{'tree'},$hash,$formats_nav);6247 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash);6248print"<div class=\"title_text\">\n".6249"<table class=\"object_header\">\n";6250 git_print_authorship_rows(\%co);6251print"</table>".6252"</div>\n";6253print"<div class=\"page_body\">\n";6254if(@{$co{'comment'}} >1) {6255print"<div class=\"log\">\n";6256 git_print_log($co{'comment'}, -final_empty_line=>1, -remove_title =>1);6257print"</div>\n";# class="log"6258}62596260}elsif($formateq'plain') {6261my$refs= git_get_references("tags");6262my$tagname= git_get_rev_name_tags($hash);6263my$filename= basename($project) ."-$hash.patch";62646265print$cgi->header(6266-type =>'text/plain',6267-charset =>'utf-8',6268-expires =>$expires,6269-content_disposition =>'inline; filename="'."$filename".'"');6270my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});6271print"From: ". to_utf8($co{'author'}) ."\n";6272print"Date:$ad{'rfc2822'} ($ad{'tz_local'})\n";6273print"Subject: ". to_utf8($co{'title'}) ."\n";62746275print"X-Git-Tag:$tagname\n"if$tagname;6276print"X-Git-Url: ".$cgi->self_url() ."\n\n";62776278foreachmy$line(@{$co{'comment'}}) {6279print to_utf8($line) ."\n";6280}6281print"---\n\n";6282}elsif($formateq'patch') {6283my$filename= basename($project) ."-$hash.patch";62846285print$cgi->header(6286-type =>'text/plain',6287-charset =>'utf-8',6288-expires =>$expires,6289-content_disposition =>'inline; filename="'."$filename".'"');6290}62916292# write patch6293if($formateq'html') {6294my$use_parents= !defined$hash_parent||6295$hash_parenteq'-c'||$hash_parenteq'--cc';6296 git_difftree_body(\@difftree,$hash,6297$use_parents? @{$co{'parents'}} :$hash_parent);6298print"<br/>\n";62996300 git_patchset_body($fd, \@difftree,$hash,6301$use_parents? @{$co{'parents'}} :$hash_parent);6302close$fd;6303print"</div>\n";# class="page_body"6304 git_footer_html();63056306}elsif($formateq'plain') {6307local$/=undef;6308print<$fd>;6309close$fd6310or print"Reading git-diff-tree failed\n";6311}elsif($formateq'patch') {6312local$/=undef;6313print<$fd>;6314close$fd6315or print"Reading git-format-patch failed\n";6316}6317}63186319sub git_commitdiff_plain {6320 git_commitdiff(-format =>'plain');6321}63226323# format-patch-style patches6324sub git_patch {6325 git_commitdiff(-format =>'patch', -single =>1);6326}63276328sub git_patches {6329 git_commitdiff(-format =>'patch');6330}63316332sub git_history {6333 git_log_generic('history', \&git_history_body,6334$hash_base,$hash_parent_base,6335$file_name,$hash);6336}63376338sub git_search {6339 gitweb_check_feature('search')or die_error(403,"Search is disabled");6340if(!defined$searchtext) {6341 die_error(400,"Text field is empty");6342}6343if(!defined$hash) {6344$hash= git_get_head_hash($project);6345}6346my%co= parse_commit($hash);6347if(!%co) {6348 die_error(404,"Unknown commit object");6349}6350if(!defined$page) {6351$page=0;6352}63536354$searchtype||='commit';6355if($searchtypeeq'pickaxe') {6356# pickaxe may take all resources of your box and run for several minutes6357# with every query - so decide by yourself how public you make this feature6358 gitweb_check_feature('pickaxe')6359or die_error(403,"Pickaxe is disabled");6360}6361if($searchtypeeq'grep') {6362 gitweb_check_feature('grep')6363or die_error(403,"Grep is disabled");6364}63656366 git_header_html();63676368if($searchtypeeq'commit'or$searchtypeeq'author'or$searchtypeeq'committer') {6369my$greptype;6370if($searchtypeeq'commit') {6371$greptype="--grep=";6372}elsif($searchtypeeq'author') {6373$greptype="--author=";6374}elsif($searchtypeeq'committer') {6375$greptype="--committer=";6376}6377$greptype.=$searchtext;6378my@commitlist= parse_commits($hash,101, (100*$page),undef,6379$greptype,'--regexp-ignore-case',6380$search_use_regexp?'--extended-regexp':'--fixed-strings');63816382my$paging_nav='';6383if($page>0) {6384$paging_nav.=6385$cgi->a({-href => href(action=>"search", hash=>$hash,6386 searchtext=>$searchtext,6387 searchtype=>$searchtype)},6388"first");6389$paging_nav.=" ⋅ ".6390$cgi->a({-href => href(-replay=>1, page=>$page-1),6391-accesskey =>"p", -title =>"Alt-p"},"prev");6392}else{6393$paging_nav.="first";6394$paging_nav.=" ⋅ prev";6395}6396my$next_link='';6397if($#commitlist>=100) {6398$next_link=6399$cgi->a({-href => href(-replay=>1, page=>$page+1),6400-accesskey =>"n", -title =>"Alt-n"},"next");6401$paging_nav.=" ⋅$next_link";6402}else{6403$paging_nav.=" ⋅ next";6404}64056406if($#commitlist>=100) {6407}64086409 git_print_page_nav('','',$hash,$co{'tree'},$hash,$paging_nav);6410 git_print_header_div('commit', esc_html($co{'title'}),$hash);6411 git_search_grep_body(\@commitlist,0,99,$next_link);6412}64136414if($searchtypeeq'pickaxe') {6415 git_print_page_nav('','',$hash,$co{'tree'},$hash);6416 git_print_header_div('commit', esc_html($co{'title'}),$hash);64176418print"<table class=\"pickaxe search\">\n";6419my$alternate=1;6420local$/="\n";6421open my$fd,'-|', git_cmd(),'--no-pager','log',@diff_opts,6422'--pretty=format:%H','--no-abbrev','--raw',"-S$searchtext",6423($search_use_regexp?'--pickaxe-regex': ());6424undef%co;6425my@files;6426while(my$line= <$fd>) {6427chomp$line;6428next unless$line;64296430my%set= parse_difftree_raw_line($line);6431if(defined$set{'commit'}) {6432# finish previous commit6433if(%co) {6434print"</td>\n".6435"<td class=\"link\">".6436$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6437" | ".6438$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6439print"</td>\n".6440"</tr>\n";6441}64426443if($alternate) {6444print"<tr class=\"dark\">\n";6445}else{6446print"<tr class=\"light\">\n";6447}6448$alternate^=1;6449%co= parse_commit($set{'commit'});6450my$author= chop_and_escape_str($co{'author_name'},15,5);6451print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".6452"<td><i>$author</i></td>\n".6453"<td>".6454$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),6455-class=>"list subject"},6456 chop_and_escape_str($co{'title'},50) ."<br/>");6457}elsif(defined$set{'to_id'}) {6458next if($set{'to_id'} =~m/^0{40}$/);64596460print$cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},6461 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),6462-class=>"list"},6463"<span class=\"match\">". esc_path($set{'file'}) ."</span>") .6464"<br/>\n";6465}6466}6467close$fd;64686469# finish last commit (warning: repetition!)6470if(%co) {6471print"</td>\n".6472"<td class=\"link\">".6473$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .6474" | ".6475$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");6476print"</td>\n".6477"</tr>\n";6478}64796480print"</table>\n";6481}64826483if($searchtypeeq'grep') {6484 git_print_page_nav('','',$hash,$co{'tree'},$hash);6485 git_print_header_div('commit', esc_html($co{'title'}),$hash);64866487print"<table class=\"grep_search\">\n";6488my$alternate=1;6489my$matches=0;6490local$/="\n";6491open my$fd,"-|", git_cmd(),'grep','-n',6492$search_use_regexp? ('-E','-i') :'-F',6493$searchtext,$co{'tree'};6494my$lastfile='';6495while(my$line= <$fd>) {6496chomp$line;6497my($file,$lno,$ltext,$binary);6498last if($matches++>1000);6499if($line=~/^Binary file (.+) matches$/) {6500$file=$1;6501$binary=1;6502}else{6503(undef,$file,$lno,$ltext) =split(/:/,$line,4);6504}6505if($filene$lastfile) {6506$lastfileand print"</td></tr>\n";6507if($alternate++) {6508print"<tr class=\"dark\">\n";6509}else{6510print"<tr class=\"light\">\n";6511}6512print"<td class=\"list\">".6513$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6514 file_name=>"$file"),6515-class=>"list"}, esc_path($file));6516print"</td><td>\n";6517$lastfile=$file;6518}6519if($binary) {6520print"<div class=\"binary\">Binary file</div>\n";6521}else{6522$ltext= untabify($ltext);6523if($ltext=~m/^(.*)($search_regexp)(.*)$/i) {6524$ltext= esc_html($1, -nbsp=>1);6525$ltext.='<span class="match">';6526$ltext.= esc_html($2, -nbsp=>1);6527$ltext.='</span>';6528$ltext.= esc_html($3, -nbsp=>1);6529}else{6530$ltext= esc_html($ltext, -nbsp=>1);6531}6532print"<div class=\"pre\">".6533$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},6534 file_name=>"$file").'#l'.$lno,6535-class=>"linenr"},sprintf('%4i',$lno))6536.' '.$ltext."</div>\n";6537}6538}6539if($lastfile) {6540print"</td></tr>\n";6541if($matches>1000) {6542print"<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";6543}6544}else{6545print"<div class=\"diff nodifferences\">No matches found</div>\n";6546}6547close$fd;65486549print"</table>\n";6550}6551 git_footer_html();6552}65536554sub git_search_help {6555 git_header_html();6556 git_print_page_nav('','',$hash,$hash,$hash);6557print<<EOT;6558<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without6559regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,6560the pattern entered is recognized as the POSIX extended6561<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case6562insensitive).</p>6563<dl>6564<dt><b>commit</b></dt>6565<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>6566EOT6567my$have_grep= gitweb_check_feature('grep');6568if($have_grep) {6569print<<EOT;6570<dt><b>grep</b></dt>6571<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing6572 a different one) are searched for the given pattern. On large trees, this search can take6573a while and put some strain on the server, so please use it with some consideration. Note that6574due to git-grep peculiarity, currently if regexp mode is turned off, the matches are6575case-sensitive.</dd>6576EOT6577}6578print<<EOT;6579<dt><b>author</b></dt>6580<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>6581<dt><b>committer</b></dt>6582<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>6583EOT6584my$have_pickaxe= gitweb_check_feature('pickaxe');6585if($have_pickaxe) {6586print<<EOT;6587<dt><b>pickaxe</b></dt>6588<dd>All commits that caused the string to appear or disappear from any file (changes that6589added, removed or "modified" the string) will be listed. This search can take a while and6590takes a lot of strain on the server, so please use it wisely. Note that since you may be6591interested even in changes just changing the case as well, this search is case sensitive.</dd>6592EOT6593}6594print"</dl>\n";6595 git_footer_html();6596}65976598sub git_shortlog {6599 git_log_generic('shortlog', \&git_shortlog_body,6600$hash,$hash_parent);6601}66026603## ......................................................................6604## feeds (RSS, Atom; OPML)66056606sub git_feed {6607my$format=shift||'atom';6608my$have_blame= gitweb_check_feature('blame');66096610# Atom: http://www.atomenabled.org/developers/syndication/6611# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ6612if($formatne'rss'&&$formatne'atom') {6613 die_error(400,"Unknown web feed format");6614}66156616# log/feed of current (HEAD) branch, log of given branch, history of file/directory6617my$head=$hash||'HEAD';6618my@commitlist= parse_commits($head,150,0,$file_name);66196620my%latest_commit;6621my%latest_date;6622my$content_type="application/$format+xml";6623if(defined$cgi->http('HTTP_ACCEPT') &&6624$cgi->Accept('text/xml') >$cgi->Accept($content_type)) {6625# browser (feed reader) prefers text/xml6626$content_type='text/xml';6627}6628if(defined($commitlist[0])) {6629%latest_commit= %{$commitlist[0]};6630my$latest_epoch=$latest_commit{'committer_epoch'};6631%latest_date= parse_date($latest_epoch);6632my$if_modified=$cgi->http('IF_MODIFIED_SINCE');6633if(defined$if_modified) {6634my$since;6635if(eval{require HTTP::Date;1; }) {6636$since= HTTP::Date::str2time($if_modified);6637}elsif(eval{require Time::ParseDate;1; }) {6638$since= Time::ParseDate::parsedate($if_modified, GMT =>1);6639}6640if(defined$since&&$latest_epoch<=$since) {6641print$cgi->header(6642-type =>$content_type,6643-charset =>'utf-8',6644-last_modified =>$latest_date{'rfc2822'},6645-status =>'304 Not Modified');6646return;6647}6648}6649print$cgi->header(6650-type =>$content_type,6651-charset =>'utf-8',6652-last_modified =>$latest_date{'rfc2822'});6653}else{6654print$cgi->header(6655-type =>$content_type,6656-charset =>'utf-8');6657}66586659# Optimization: skip generating the body if client asks only6660# for Last-Modified date.6661return if($cgi->request_method()eq'HEAD');66626663# header variables6664my$title="$site_name-$project/$action";6665my$feed_type='log';6666if(defined$hash) {6667$title.=" - '$hash'";6668$feed_type='branch log';6669if(defined$file_name) {6670$title.=" ::$file_name";6671$feed_type='history';6672}6673}elsif(defined$file_name) {6674$title.=" -$file_name";6675$feed_type='history';6676}6677$title.="$feed_type";6678my$descr= git_get_project_description($project);6679if(defined$descr) {6680$descr= esc_html($descr);6681}else{6682$descr="$project".6683($formateq'rss'?'RSS':'Atom') .6684" feed";6685}6686my$owner= git_get_project_owner($project);6687$owner= esc_html($owner);66886689#header6690my$alt_url;6691if(defined$file_name) {6692$alt_url= href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);6693}elsif(defined$hash) {6694$alt_url= href(-full=>1, action=>"log", hash=>$hash);6695}else{6696$alt_url= href(-full=>1, action=>"summary");6697}6698print qq!<?xml version="1.0" encoding="utf-8"?>\n!;6699if($formateq'rss') {6700print<<XML;6701<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">6702<channel>6703XML6704print"<title>$title</title>\n".6705"<link>$alt_url</link>\n".6706"<description>$descr</description>\n".6707"<language>en</language>\n".6708# project owner is responsible for 'editorial' content6709"<managingEditor>$owner</managingEditor>\n";6710if(defined$logo||defined$favicon) {6711# prefer the logo to the favicon, since RSS6712# doesn't allow both6713my$img= esc_url($logo||$favicon);6714print"<image>\n".6715"<url>$img</url>\n".6716"<title>$title</title>\n".6717"<link>$alt_url</link>\n".6718"</image>\n";6719}6720if(%latest_date) {6721print"<pubDate>$latest_date{'rfc2822'}</pubDate>\n";6722print"<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";6723}6724print"<generator>gitweb v.$version/$git_version</generator>\n";6725}elsif($formateq'atom') {6726print<<XML;6727<feed xmlns="http://www.w3.org/2005/Atom">6728XML6729print"<title>$title</title>\n".6730"<subtitle>$descr</subtitle>\n".6731'<link rel="alternate" type="text/html" href="'.6732$alt_url.'" />'."\n".6733'<link rel="self" type="'.$content_type.'" href="'.6734$cgi->self_url() .'" />'."\n".6735"<id>". href(-full=>1) ."</id>\n".6736# use project owner for feed author6737"<author><name>$owner</name></author>\n";6738if(defined$favicon) {6739print"<icon>". esc_url($favicon) ."</icon>\n";6740}6741if(defined$logo_url) {6742# not twice as wide as tall: 72 x 27 pixels6743print"<logo>". esc_url($logo) ."</logo>\n";6744}6745if(!%latest_date) {6746# dummy date to keep the feed valid until commits trickle in:6747print"<updated>1970-01-01T00:00:00Z</updated>\n";6748}else{6749print"<updated>$latest_date{'iso-8601'}</updated>\n";6750}6751print"<generator version='$version/$git_version'>gitweb</generator>\n";6752}67536754# contents6755for(my$i=0;$i<=$#commitlist;$i++) {6756my%co= %{$commitlist[$i]};6757my$commit=$co{'id'};6758# we read 150, we always show 30 and the ones more recent than 48 hours6759if(($i>=20) && ((time-$co{'author_epoch'}) >48*60*60)) {6760last;6761}6762my%cd= parse_date($co{'author_epoch'});67636764# get list of changed files6765open my$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6766$co{'parent'} ||"--root",6767$co{'id'},"--", (defined$file_name?$file_name: ())6768ornext;6769my@difftree=map{chomp;$_} <$fd>;6770close$fd6771ornext;67726773# print element (entry, item)6774my$co_url= href(-full=>1, action=>"commitdiff", hash=>$commit);6775if($formateq'rss') {6776print"<item>\n".6777"<title>". esc_html($co{'title'}) ."</title>\n".6778"<author>". esc_html($co{'author'}) ."</author>\n".6779"<pubDate>$cd{'rfc2822'}</pubDate>\n".6780"<guid isPermaLink=\"true\">$co_url</guid>\n".6781"<link>$co_url</link>\n".6782"<description>". esc_html($co{'title'}) ."</description>\n".6783"<content:encoded>".6784"<![CDATA[\n";6785}elsif($formateq'atom') {6786print"<entry>\n".6787"<title type=\"html\">". esc_html($co{'title'}) ."</title>\n".6788"<updated>$cd{'iso-8601'}</updated>\n".6789"<author>\n".6790" <name>". esc_html($co{'author_name'}) ."</name>\n";6791if($co{'author_email'}) {6792print" <email>". esc_html($co{'author_email'}) ."</email>\n";6793}6794print"</author>\n".6795# use committer for contributor6796"<contributor>\n".6797" <name>". esc_html($co{'committer_name'}) ."</name>\n";6798if($co{'committer_email'}) {6799print" <email>". esc_html($co{'committer_email'}) ."</email>\n";6800}6801print"</contributor>\n".6802"<published>$cd{'iso-8601'}</published>\n".6803"<link rel=\"alternate\"type=\"text/html\"href=\"$co_url\"/>\n".6804"<id>$co_url</id>\n".6805"<content type=\"xhtml\"xml:base=\"". esc_url($my_url) ."\">\n".6806"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";6807}6808my$comment=$co{'comment'};6809print"<pre>\n";6810foreachmy$line(@$comment) {6811$line= esc_html($line);6812print"$line\n";6813}6814print"</pre><ul>\n";6815foreachmy$difftree_line(@difftree) {6816my%difftree= parse_difftree_raw_line($difftree_line);6817next if!$difftree{'from_id'};68186819my$file=$difftree{'file'} ||$difftree{'to_file'};68206821print"<li>".6822"[".6823$cgi->a({-href => href(-full=>1, action=>"blobdiff",6824 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},6825 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},6826 file_name=>$file, file_parent=>$difftree{'from_file'}),6827-title =>"diff"},'D');6828if($have_blame) {6829print$cgi->a({-href => href(-full=>1, action=>"blame",6830 file_name=>$file, hash_base=>$commit),6831-title =>"blame"},'B');6832}6833# if this is not a feed of a file history6834if(!defined$file_name||$file_namene$file) {6835print$cgi->a({-href => href(-full=>1, action=>"history",6836 file_name=>$file, hash=>$commit),6837-title =>"history"},'H');6838}6839$file= esc_path($file);6840print"] ".6841"$file</li>\n";6842}6843if($formateq'rss') {6844print"</ul>]]>\n".6845"</content:encoded>\n".6846"</item>\n";6847}elsif($formateq'atom') {6848print"</ul>\n</div>\n".6849"</content>\n".6850"</entry>\n";6851}6852}68536854# end of feed6855if($formateq'rss') {6856print"</channel>\n</rss>\n";6857}elsif($formateq'atom') {6858print"</feed>\n";6859}6860}68616862sub git_rss {6863 git_feed('rss');6864}68656866sub git_atom {6867 git_feed('atom');6868}68696870sub git_opml {6871my@list= git_get_projects_list();68726873print$cgi->header(6874-type =>'text/xml',6875-charset =>'utf-8',6876-content_disposition =>'inline; filename="opml.xml"');68776878print<<XML;6879<?xml version="1.0" encoding="utf-8"?>6880<opml version="1.0">6881<head>6882 <title>$site_nameOPML Export</title>6883</head>6884<body>6885<outline text="git RSS feeds">6886XML68876888foreachmy$pr(@list) {6889my%proj=%$pr;6890my$head= git_get_head_hash($proj{'path'});6891if(!defined$head) {6892next;6893}6894$git_dir="$projectroot/$proj{'path'}";6895my%co= parse_commit($head);6896if(!%co) {6897next;6898}68996900my$path= esc_html(chop_str($proj{'path'},25,5));6901my$rss= href('project'=>$proj{'path'},'action'=>'rss', -full =>1);6902my$html= href('project'=>$proj{'path'},'action'=>'summary', -full =>1);6903print"<outline type=\"rss\"text=\"$path\"title=\"$path\"xmlUrl=\"$rss\"htmlUrl=\"$html\"/>\n";6904}6905print<<XML;6906</outline>6907</body>6908</opml>6909XML6910}