1#!/usr/bin/perl 2 3# gitweb - simple web interface to track changes in git repositories 4# 5# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org> 6# (C) 2005, Christian Gierke 7# 8# This program is licensed under the GPLv2 9 10use strict; 11use warnings; 12use CGI qw(:standard :escapeHTML -nosticky); 13use CGI::Util qw(unescape); 14use CGI::Carp qw(fatalsToBrowser); 15use Encode; 16use Fcntl ':mode'; 17use File::Find qw(); 18use File::Basename qw(basename); 19binmode STDOUT,':utf8'; 20 21BEGIN{ 22 CGI->compile()if$ENV{'MOD_PERL'}; 23} 24 25our$cgi= new CGI; 26our$version="++GIT_VERSION++"; 27our$my_url=$cgi->url(); 28our$my_uri=$cgi->url(-absolute =>1); 29 30# if we're called with PATH_INFO, we have to strip that 31# from the URL to find our real URL 32# we make $path_info global because it's also used later on 33our$path_info=$ENV{"PATH_INFO"}; 34if($path_info) { 35$my_url=~ s,\Q$path_info\E$,,; 36$my_uri=~ s,\Q$path_info\E$,,; 37} 38 39# core git executable to use 40# this can just be "git" if your webserver has a sensible PATH 41our$GIT="++GIT_BINDIR++/git"; 42 43# absolute fs-path which will be prepended to the project path 44#our $projectroot = "/pub/scm"; 45our$projectroot="++GITWEB_PROJECTROOT++"; 46 47# fs traversing limit for getting project list 48# the number is relative to the projectroot 49our$project_maxdepth="++GITWEB_PROJECT_MAXDEPTH++"; 50 51# target of the home link on top of all pages 52our$home_link=$my_uri||"/"; 53 54# string of the home link on top of all pages 55our$home_link_str="++GITWEB_HOME_LINK_STR++"; 56 57# name of your site or organization to appear in page titles 58# replace this with something more descriptive for clearer bookmarks 59our$site_name="++GITWEB_SITENAME++" 60|| ($ENV{'SERVER_NAME'} ||"Untitled") ." Git"; 61 62# filename of html text to include at top of each page 63our$site_header="++GITWEB_SITE_HEADER++"; 64# html text to include at home page 65our$home_text="++GITWEB_HOMETEXT++"; 66# filename of html text to include at bottom of each page 67our$site_footer="++GITWEB_SITE_FOOTER++"; 68 69# URI of stylesheets 70our@stylesheets= ("++GITWEB_CSS++"); 71# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG. 72our$stylesheet=undef; 73# URI of GIT logo (72x27 size) 74our$logo="++GITWEB_LOGO++"; 75# URI of GIT favicon, assumed to be image/png type 76our$favicon="++GITWEB_FAVICON++"; 77 78# URI and label (title) of GIT logo link 79#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; 80#our $logo_label = "git documentation"; 81our$logo_url="http://git.or.cz/"; 82our$logo_label="git homepage"; 83 84# source of projects list 85our$projects_list="++GITWEB_LIST++"; 86 87# the width (in characters) of the projects list "Description" column 88our$projects_list_description_width=25; 89 90# default order of projects list 91# valid values are none, project, descr, owner, and age 92our$default_projects_order="project"; 93 94# show repository only if this file exists 95# (only effective if this variable evaluates to true) 96our$export_ok="++GITWEB_EXPORT_OK++"; 97 98# show repository only if this subroutine returns true 99# when given the path to the project, for example: 100# sub { return -e "$_[0]/git-daemon-export-ok"; } 101our$export_auth_hook=undef; 102 103# only allow viewing of repositories also shown on the overview page 104our$strict_export="++GITWEB_STRICT_EXPORT++"; 105 106# list of git base URLs used for URL to where fetch project from, 107# i.e. full URL is "$git_base_url/$project" 108our@git_base_url_list=grep{$_ne''} ("++GITWEB_BASE_URL++"); 109 110# default blob_plain mimetype and default charset for text/plain blob 111our$default_blob_plain_mimetype='text/plain'; 112our$default_text_plain_charset=undef; 113 114# file to use for guessing MIME types before trying /etc/mime.types 115# (relative to the current git repository) 116our$mimetypes_file=undef; 117 118# assume this charset if line contains non-UTF-8 characters; 119# it should be valid encoding (see Encoding::Supported(3pm) for list), 120# for which encoding all byte sequences are valid, for example 121# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it 122# could be even 'utf-8' for the old behavior) 123our$fallback_encoding='latin1'; 124 125# rename detection options for git-diff and git-diff-tree 126# - default is '-M', with the cost proportional to 127# (number of removed files) * (number of new files). 128# - more costly is '-C' (which implies '-M'), with the cost proportional to 129# (number of changed files + number of removed files) * (number of new files) 130# - even more costly is '-C', '--find-copies-harder' with cost 131# (number of files in the original tree) * (number of new files) 132# - one might want to include '-B' option, e.g. '-B', '-M' 133our@diff_opts= ('-M');# taken from git_commit 134 135# information about snapshot formats that gitweb is capable of serving 136our%known_snapshot_formats= ( 137# name => { 138# 'display' => display name, 139# 'type' => mime type, 140# 'suffix' => filename suffix, 141# 'format' => --format for git-archive, 142# 'compressor' => [compressor command and arguments] 143# (array reference, optional)} 144# 145'tgz'=> { 146'display'=>'tar.gz', 147'type'=>'application/x-gzip', 148'suffix'=>'.tar.gz', 149'format'=>'tar', 150'compressor'=> ['gzip']}, 151 152'tbz2'=> { 153'display'=>'tar.bz2', 154'type'=>'application/x-bzip2', 155'suffix'=>'.tar.bz2', 156'format'=>'tar', 157'compressor'=> ['bzip2']}, 158 159'zip'=> { 160'display'=>'zip', 161'type'=>'application/x-zip', 162'suffix'=>'.zip', 163'format'=>'zip'}, 164); 165 166# Aliases so we understand old gitweb.snapshot values in repository 167# configuration. 168our%known_snapshot_format_aliases= ( 169'gzip'=>'tgz', 170'bzip2'=>'tbz2', 171 172# backward compatibility: legacy gitweb config support 173'x-gzip'=>undef,'gz'=>undef, 174'x-bzip2'=>undef,'bz2'=>undef, 175'x-zip'=>undef,''=>undef, 176); 177 178# You define site-wide feature defaults here; override them with 179# $GITWEB_CONFIG as necessary. 180our%feature= ( 181# feature => { 182# 'sub' => feature-sub (subroutine), 183# 'override' => allow-override (boolean), 184# 'default' => [ default options...] (array reference)} 185# 186# if feature is overridable (it means that allow-override has true value), 187# then feature-sub will be called with default options as parameters; 188# return value of feature-sub indicates if to enable specified feature 189# 190# if there is no 'sub' key (no feature-sub), then feature cannot be 191# overriden 192# 193# use gitweb_get_feature(<feature>) to retrieve the <feature> value 194# (an array) or gitweb_check_feature(<feature>) to check if <feature> 195# is enabled 196 197# Enable the 'blame' blob view, showing the last commit that modified 198# each line in the file. This can be very CPU-intensive. 199 200# To enable system wide have in $GITWEB_CONFIG 201# $feature{'blame'}{'default'} = [1]; 202# To have project specific config enable override in $GITWEB_CONFIG 203# $feature{'blame'}{'override'} = 1; 204# and in project config gitweb.blame = 0|1; 205'blame'=> { 206'sub'=> \&feature_blame, 207'override'=>0, 208'default'=> [0]}, 209 210# Enable the 'snapshot' link, providing a compressed archive of any 211# tree. This can potentially generate high traffic if you have large 212# project. 213 214# Value is a list of formats defined in %known_snapshot_formats that 215# you wish to offer. 216# To disable system wide have in $GITWEB_CONFIG 217# $feature{'snapshot'}{'default'} = []; 218# To have project specific config enable override in $GITWEB_CONFIG 219# $feature{'snapshot'}{'override'} = 1; 220# and in project config, a comma-separated list of formats or "none" 221# to disable. Example: gitweb.snapshot = tbz2,zip; 222'snapshot'=> { 223'sub'=> \&feature_snapshot, 224'override'=>0, 225'default'=> ['tgz']}, 226 227# Enable text search, which will list the commits which match author, 228# committer or commit text to a given string. Enabled by default. 229# Project specific override is not supported. 230'search'=> { 231'override'=>0, 232'default'=> [1]}, 233 234# Enable grep search, which will list the files in currently selected 235# tree containing the given string. Enabled by default. This can be 236# potentially CPU-intensive, of course. 237 238# To enable system wide have in $GITWEB_CONFIG 239# $feature{'grep'}{'default'} = [1]; 240# To have project specific config enable override in $GITWEB_CONFIG 241# $feature{'grep'}{'override'} = 1; 242# and in project config gitweb.grep = 0|1; 243'grep'=> { 244'override'=>0, 245'default'=> [1]}, 246 247# Enable the pickaxe search, which will list the commits that modified 248# a given string in a file. This can be practical and quite faster 249# alternative to 'blame', but still potentially CPU-intensive. 250 251# To enable system wide have in $GITWEB_CONFIG 252# $feature{'pickaxe'}{'default'} = [1]; 253# To have project specific config enable override in $GITWEB_CONFIG 254# $feature{'pickaxe'}{'override'} = 1; 255# and in project config gitweb.pickaxe = 0|1; 256'pickaxe'=> { 257'sub'=> \&feature_pickaxe, 258'override'=>0, 259'default'=> [1]}, 260 261# Make gitweb use an alternative format of the URLs which can be 262# more readable and natural-looking: project name is embedded 263# directly in the path and the query string contains other 264# auxiliary information. All gitweb installations recognize 265# URL in either format; this configures in which formats gitweb 266# generates links. 267 268# To enable system wide have in $GITWEB_CONFIG 269# $feature{'pathinfo'}{'default'} = [1]; 270# Project specific override is not supported. 271 272# Note that you will need to change the default location of CSS, 273# favicon, logo and possibly other files to an absolute URL. Also, 274# if gitweb.cgi serves as your indexfile, you will need to force 275# $my_uri to contain the script name in your $GITWEB_CONFIG. 276'pathinfo'=> { 277'override'=>0, 278'default'=> [0]}, 279 280# Make gitweb consider projects in project root subdirectories 281# to be forks of existing projects. Given project $projname.git, 282# projects matching $projname/*.git will not be shown in the main 283# projects list, instead a '+' mark will be added to $projname 284# there and a 'forks' view will be enabled for the project, listing 285# all the forks. If project list is taken from a file, forks have 286# to be listed after the main project. 287 288# To enable system wide have in $GITWEB_CONFIG 289# $feature{'forks'}{'default'} = [1]; 290# Project specific override is not supported. 291'forks'=> { 292'override'=>0, 293'default'=> [0]}, 294 295# Insert custom links to the action bar of all project pages. 296# This enables you mainly to link to third-party scripts integrating 297# into gitweb; e.g. git-browser for graphical history representation 298# or custom web-based repository administration interface. 299 300# The 'default' value consists of a list of triplets in the form 301# (label, link, position) where position is the label after which 302# to insert the link and link is a format string where %n expands 303# to the project name, %f to the project path within the filesystem, 304# %h to the current hash (h gitweb parameter) and %b to the current 305# hash base (hb gitweb parameter); %% expands to %. 306 307# To enable system wide have in $GITWEB_CONFIG e.g. 308# $feature{'actions'}{'default'} = [('graphiclog', 309# '/git-browser/by-commit.html?r=%n', 'summary')]; 310# Project specific override is not supported. 311'actions'=> { 312'override'=>0, 313'default'=> []}, 314 315# Allow gitweb scan project content tags described in ctags/ 316# of project repository, and display the popular Web 2.0-ish 317# "tag cloud" near the project list. Note that this is something 318# COMPLETELY different from the normal Git tags. 319 320# gitweb by itself can show existing tags, but it does not handle 321# tagging itself; you need an external application for that. 322# For an example script, check Girocco's cgi/tagproj.cgi. 323# You may want to install the HTML::TagCloud Perl module to get 324# a pretty tag cloud instead of just a list of tags. 325 326# To enable system wide have in $GITWEB_CONFIG 327# $feature{'ctags'}{'default'} = ['path_to_tag_script']; 328# Project specific override is not supported. 329'ctags'=> { 330'override'=>0, 331'default'=> [0]}, 332 333# The maximum number of patches in a patchset generated in patch 334# view. Set this to 0 or undef to disable patch view, or to a 335# negative number to remove any limit. 336 337# To disable system wide have in $GITWEB_CONFIG 338# $feature{'patches'}{'default'} = [0]; 339# To have project specific config enable override in $GITWEB_CONFIG 340# $feature{'patches'}{'override'} = 1; 341# and in project config gitweb.patches = 0|n; 342# where n is the maximum number of patches allowed in a patchset. 343'patches'=> { 344'sub'=> \&feature_patches, 345'override'=>0, 346'default'=> [16]}, 347); 348 349sub gitweb_get_feature { 350my($name) =@_; 351return unlessexists$feature{$name}; 352my($sub,$override,@defaults) = ( 353$feature{$name}{'sub'}, 354$feature{$name}{'override'}, 355@{$feature{$name}{'default'}}); 356if(!$override) {return@defaults; } 357if(!defined$sub) { 358warn"feature$nameis not overrideable"; 359return@defaults; 360} 361return$sub->(@defaults); 362} 363 364# A wrapper to check if a given feature is enabled. 365# With this, you can say 366# 367# my $bool_feat = gitweb_check_feature('bool_feat'); 368# gitweb_check_feature('bool_feat') or somecode; 369# 370# instead of 371# 372# my ($bool_feat) = gitweb_get_feature('bool_feat'); 373# (gitweb_get_feature('bool_feat'))[0] or somecode; 374# 375sub gitweb_check_feature { 376return(gitweb_get_feature(@_))[0]; 377} 378 379 380sub feature_blame { 381my($val) = git_get_project_config('blame','--bool'); 382 383if($valeq'true') { 384return1; 385}elsif($valeq'false') { 386return0; 387} 388 389return$_[0]; 390} 391 392sub feature_snapshot { 393my(@fmts) =@_; 394 395my($val) = git_get_project_config('snapshot'); 396 397if($val) { 398@fmts= ($valeq'none'? () :split/\s*[,\s]\s*/,$val); 399} 400 401return@fmts; 402} 403 404sub feature_grep { 405my($val) = git_get_project_config('grep','--bool'); 406 407if($valeq'true') { 408return(1); 409}elsif($valeq'false') { 410return(0); 411} 412 413return($_[0]); 414} 415 416sub feature_pickaxe { 417my($val) = git_get_project_config('pickaxe','--bool'); 418 419if($valeq'true') { 420return(1); 421}elsif($valeq'false') { 422return(0); 423} 424 425return($_[0]); 426} 427 428sub feature_patches { 429my@val= (git_get_project_config('patches','--int')); 430 431if(@val) { 432return@val; 433} 434 435return($_[0]); 436} 437 438# checking HEAD file with -e is fragile if the repository was 439# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed 440# and then pruned. 441sub check_head_link { 442my($dir) =@_; 443my$headfile="$dir/HEAD"; 444return((-e $headfile) || 445(-l $headfile&&readlink($headfile) =~/^refs\/heads\//)); 446} 447 448sub check_export_ok { 449my($dir) =@_; 450return(check_head_link($dir) && 451(!$export_ok|| -e "$dir/$export_ok") && 452(!$export_auth_hook||$export_auth_hook->($dir))); 453} 454 455# process alternate names for backward compatibility 456# filter out unsupported (unknown) snapshot formats 457sub filter_snapshot_fmts { 458my@fmts=@_; 459 460@fmts=map{ 461exists$known_snapshot_format_aliases{$_} ? 462$known_snapshot_format_aliases{$_} :$_}@fmts; 463@fmts=grep(exists$known_snapshot_formats{$_},@fmts); 464 465} 466 467our$GITWEB_CONFIG=$ENV{'GITWEB_CONFIG'} ||"++GITWEB_CONFIG++"; 468if(-e $GITWEB_CONFIG) { 469do$GITWEB_CONFIG; 470}else{ 471our$GITWEB_CONFIG_SYSTEM=$ENV{'GITWEB_CONFIG_SYSTEM'} ||"++GITWEB_CONFIG_SYSTEM++"; 472do$GITWEB_CONFIG_SYSTEMif-e $GITWEB_CONFIG_SYSTEM; 473} 474 475# version of the core git binary 476our$git_version=qx("$GIT" --version)=~m/git version (.*)$/?$1:"unknown"; 477 478$projects_list||=$projectroot; 479 480# ====================================================================== 481# input validation and dispatch 482 483# input parameters can be collected from a variety of sources (presently, CGI 484# and PATH_INFO), so we define an %input_params hash that collects them all 485# together during validation: this allows subsequent uses (e.g. href()) to be 486# agnostic of the parameter origin 487 488our%input_params= (); 489 490# input parameters are stored with the long parameter name as key. This will 491# also be used in the href subroutine to convert parameters to their CGI 492# equivalent, and since the href() usage is the most frequent one, we store 493# the name -> CGI key mapping here, instead of the reverse. 494# 495# XXX: Warning: If you touch this, check the search form for updating, 496# too. 497 498our@cgi_param_mapping= ( 499 project =>"p", 500 action =>"a", 501 file_name =>"f", 502 file_parent =>"fp", 503 hash =>"h", 504 hash_parent =>"hp", 505 hash_base =>"hb", 506 hash_parent_base =>"hpb", 507 page =>"pg", 508 order =>"o", 509 searchtext =>"s", 510 searchtype =>"st", 511 snapshot_format =>"sf", 512 extra_options =>"opt", 513 search_use_regexp =>"sr", 514); 515our%cgi_param_mapping=@cgi_param_mapping; 516 517# we will also need to know the possible actions, for validation 518our%actions= ( 519"blame"=> \&git_blame, 520"blobdiff"=> \&git_blobdiff, 521"blobdiff_plain"=> \&git_blobdiff_plain, 522"blob"=> \&git_blob, 523"blob_plain"=> \&git_blob_plain, 524"commitdiff"=> \&git_commitdiff, 525"commitdiff_plain"=> \&git_commitdiff_plain, 526"commit"=> \&git_commit, 527"forks"=> \&git_forks, 528"heads"=> \&git_heads, 529"history"=> \&git_history, 530"log"=> \&git_log, 531"patch"=> \&git_patch, 532"rss"=> \&git_rss, 533"atom"=> \&git_atom, 534"search"=> \&git_search, 535"search_help"=> \&git_search_help, 536"shortlog"=> \&git_shortlog, 537"summary"=> \&git_summary, 538"tag"=> \&git_tag, 539"tags"=> \&git_tags, 540"tree"=> \&git_tree, 541"snapshot"=> \&git_snapshot, 542"object"=> \&git_object, 543# those below don't need $project 544"opml"=> \&git_opml, 545"project_list"=> \&git_project_list, 546"project_index"=> \&git_project_index, 547); 548 549# finally, we have the hash of allowed extra_options for the commands that 550# allow them 551our%allowed_options= ( 552"--no-merges"=> [qw(rss atom log shortlog history)], 553); 554 555# fill %input_params with the CGI parameters. All values except for 'opt' 556# should be single values, but opt can be an array. We should probably 557# build an array of parameters that can be multi-valued, but since for the time 558# being it's only this one, we just single it out 559while(my($name,$symbol) =each%cgi_param_mapping) { 560if($symboleq'opt') { 561$input_params{$name} = [$cgi->param($symbol) ]; 562}else{ 563$input_params{$name} =$cgi->param($symbol); 564} 565} 566 567# now read PATH_INFO and update the parameter list for missing parameters 568sub evaluate_path_info { 569return ifdefined$input_params{'project'}; 570return if!$path_info; 571$path_info=~ s,^/+,,; 572return if!$path_info; 573 574# find which part of PATH_INFO is project 575my$project=$path_info; 576$project=~ s,/+$,,; 577while($project&& !check_head_link("$projectroot/$project")) { 578$project=~ s,/*[^/]*$,,; 579} 580return unless$project; 581$input_params{'project'} =$project; 582 583# do not change any parameters if an action is given using the query string 584return if$input_params{'action'}; 585$path_info=~ s,^\Q$project\E/*,,; 586 587# next, check if we have an action 588my$action=$path_info; 589$action=~ s,/.*$,,; 590if(exists$actions{$action}) { 591$path_info=~ s,^$action/*,,; 592$input_params{'action'} =$action; 593} 594 595# list of actions that want hash_base instead of hash, but can have no 596# pathname (f) parameter 597my@wants_base= ( 598'tree', 599'history', 600); 601 602# we want to catch 603# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name] 604my($parentrefname,$parentpathname,$refname,$pathname) = 605($path_info=~/^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/); 606 607# first, analyze the 'current' part 608if(defined$pathname) { 609# we got "branch:filename" or "branch:dir/" 610# we could use git_get_type(branch:pathname), but: 611# - it needs $git_dir 612# - it does a git() call 613# - the convention of terminating directories with a slash 614# makes it superfluous 615# - embedding the action in the PATH_INFO would make it even 616# more superfluous 617$pathname=~ s,^/+,,; 618if(!$pathname||substr($pathname, -1)eq"/") { 619$input_params{'action'} ||="tree"; 620$pathname=~ s,/$,,; 621}else{ 622# the default action depends on whether we had parent info 623# or not 624if($parentrefname) { 625$input_params{'action'} ||="blobdiff_plain"; 626}else{ 627$input_params{'action'} ||="blob_plain"; 628} 629} 630$input_params{'hash_base'} ||=$refname; 631$input_params{'file_name'} ||=$pathname; 632}elsif(defined$refname) { 633# we got "branch". In this case we have to choose if we have to 634# set hash or hash_base. 635# 636# Most of the actions without a pathname only want hash to be 637# set, except for the ones specified in @wants_base that want 638# hash_base instead. It should also be noted that hand-crafted 639# links having 'history' as an action and no pathname or hash 640# set will fail, but that happens regardless of PATH_INFO. 641$input_params{'action'} ||="shortlog"; 642if(grep{$_eq$input_params{'action'} }@wants_base) { 643$input_params{'hash_base'} ||=$refname; 644}else{ 645$input_params{'hash'} ||=$refname; 646} 647} 648 649# next, handle the 'parent' part, if present 650if(defined$parentrefname) { 651# a missing pathspec defaults to the 'current' filename, allowing e.g. 652# someproject/blobdiff/oldrev..newrev:/filename 653if($parentpathname) { 654$parentpathname=~ s,^/+,,; 655$parentpathname=~ s,/$,,; 656$input_params{'file_parent'} ||=$parentpathname; 657}else{ 658$input_params{'file_parent'} ||=$input_params{'file_name'}; 659} 660# we assume that hash_parent_base is wanted if a path was specified, 661# or if the action wants hash_base instead of hash 662if(defined$input_params{'file_parent'} || 663grep{$_eq$input_params{'action'} }@wants_base) { 664$input_params{'hash_parent_base'} ||=$parentrefname; 665}else{ 666$input_params{'hash_parent'} ||=$parentrefname; 667} 668} 669 670# for the snapshot action, we allow URLs in the form 671# $project/snapshot/$hash.ext 672# where .ext determines the snapshot and gets removed from the 673# passed $refname to provide the $hash. 674# 675# To be able to tell that $refname includes the format extension, we 676# require the following two conditions to be satisfied: 677# - the hash input parameter MUST have been set from the $refname part 678# of the URL (i.e. they must be equal) 679# - the snapshot format MUST NOT have been defined already (e.g. from 680# CGI parameter sf) 681# It's also useless to try any matching unless $refname has a dot, 682# so we check for that too 683if(defined$input_params{'action'} && 684$input_params{'action'}eq'snapshot'&& 685defined$refname&&index($refname,'.') != -1&& 686$refnameeq$input_params{'hash'} && 687!defined$input_params{'snapshot_format'}) { 688# We loop over the known snapshot formats, checking for 689# extensions. Allowed extensions are both the defined suffix 690# (which includes the initial dot already) and the snapshot 691# format key itself, with a prepended dot 692while(my($fmt,%opt) =each%known_snapshot_formats) { 693my$hash=$refname; 694my$sfx; 695$hash=~s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//; 696next unless$sfx=$1; 697# a valid suffix was found, so set the snapshot format 698# and reset the hash parameter 699$input_params{'snapshot_format'} =$fmt; 700$input_params{'hash'} =$hash; 701# we also set the format suffix to the one requested 702# in the URL: this way a request for e.g. .tgz returns 703# a .tgz instead of a .tar.gz 704$known_snapshot_formats{$fmt}{'suffix'} =$sfx; 705last; 706} 707} 708} 709evaluate_path_info(); 710 711our$action=$input_params{'action'}; 712if(defined$action) { 713if(!validate_action($action)) { 714 die_error(400,"Invalid action parameter"); 715} 716} 717 718# parameters which are pathnames 719our$project=$input_params{'project'}; 720if(defined$project) { 721if(!validate_project($project)) { 722undef$project; 723 die_error(404,"No such project"); 724} 725} 726 727our$file_name=$input_params{'file_name'}; 728if(defined$file_name) { 729if(!validate_pathname($file_name)) { 730 die_error(400,"Invalid file parameter"); 731} 732} 733 734our$file_parent=$input_params{'file_parent'}; 735if(defined$file_parent) { 736if(!validate_pathname($file_parent)) { 737 die_error(400,"Invalid file parent parameter"); 738} 739} 740 741# parameters which are refnames 742our$hash=$input_params{'hash'}; 743if(defined$hash) { 744if(!validate_refname($hash)) { 745 die_error(400,"Invalid hash parameter"); 746} 747} 748 749our$hash_parent=$input_params{'hash_parent'}; 750if(defined$hash_parent) { 751if(!validate_refname($hash_parent)) { 752 die_error(400,"Invalid hash parent parameter"); 753} 754} 755 756our$hash_base=$input_params{'hash_base'}; 757if(defined$hash_base) { 758if(!validate_refname($hash_base)) { 759 die_error(400,"Invalid hash base parameter"); 760} 761} 762 763our@extra_options= @{$input_params{'extra_options'}}; 764# @extra_options is always defined, since it can only be (currently) set from 765# CGI, and $cgi->param() returns the empty array in array context if the param 766# is not set 767foreachmy$opt(@extra_options) { 768if(not exists$allowed_options{$opt}) { 769 die_error(400,"Invalid option parameter"); 770} 771if(not grep(/^$action$/, @{$allowed_options{$opt}})) { 772 die_error(400,"Invalid option parameter for this action"); 773} 774} 775 776our$hash_parent_base=$input_params{'hash_parent_base'}; 777if(defined$hash_parent_base) { 778if(!validate_refname($hash_parent_base)) { 779 die_error(400,"Invalid hash parent base parameter"); 780} 781} 782 783# other parameters 784our$page=$input_params{'page'}; 785if(defined$page) { 786if($page=~m/[^0-9]/) { 787 die_error(400,"Invalid page parameter"); 788} 789} 790 791our$searchtype=$input_params{'searchtype'}; 792if(defined$searchtype) { 793if($searchtype=~m/[^a-z]/) { 794 die_error(400,"Invalid searchtype parameter"); 795} 796} 797 798our$search_use_regexp=$input_params{'search_use_regexp'}; 799 800our$searchtext=$input_params{'searchtext'}; 801our$search_regexp; 802if(defined$searchtext) { 803if(length($searchtext) <2) { 804 die_error(403,"At least two characters are required for search parameter"); 805} 806$search_regexp=$search_use_regexp?$searchtext:quotemeta$searchtext; 807} 808 809# path to the current git repository 810our$git_dir; 811$git_dir="$projectroot/$project"if$project; 812 813# list of supported snapshot formats 814our@snapshot_fmts= gitweb_get_feature('snapshot'); 815@snapshot_fmts= filter_snapshot_fmts(@snapshot_fmts); 816 817# dispatch 818if(!defined$action) { 819if(defined$hash) { 820$action= git_get_type($hash); 821}elsif(defined$hash_base&&defined$file_name) { 822$action= git_get_type("$hash_base:$file_name"); 823}elsif(defined$project) { 824$action='summary'; 825}else{ 826$action='project_list'; 827} 828} 829if(!defined($actions{$action})) { 830 die_error(400,"Unknown action"); 831} 832if($action!~m/^(opml|project_list|project_index)$/&& 833!$project) { 834 die_error(400,"Project needed"); 835} 836$actions{$action}->(); 837exit; 838 839## ====================================================================== 840## action links 841 842sub href (%) { 843my%params=@_; 844# default is to use -absolute url() i.e. $my_uri 845my$href=$params{-full} ?$my_url:$my_uri; 846 847$params{'project'} =$projectunlessexists$params{'project'}; 848 849if($params{-replay}) { 850while(my($name,$symbol) =each%cgi_param_mapping) { 851if(!exists$params{$name}) { 852$params{$name} =$input_params{$name}; 853} 854} 855} 856 857my$use_pathinfo= gitweb_check_feature('pathinfo'); 858if($use_pathinfo) { 859# try to put as many parameters as possible in PATH_INFO: 860# - project name 861# - action 862# - hash_parent or hash_parent_base:/file_parent 863# - hash or hash_base:/filename 864# - the snapshot_format as an appropriate suffix 865 866# When the script is the root DirectoryIndex for the domain, 867# $href here would be something like http://gitweb.example.com/ 868# Thus, we strip any trailing / from $href, to spare us double 869# slashes in the final URL 870$href=~ s,/$,,; 871 872# Then add the project name, if present 873$href.="/".esc_url($params{'project'})ifdefined$params{'project'}; 874delete$params{'project'}; 875 876# since we destructively absorb parameters, we keep this 877# boolean that remembers if we're handling a snapshot 878my$is_snapshot=$params{'action'}eq'snapshot'; 879 880# Summary just uses the project path URL, any other action is 881# added to the URL 882if(defined$params{'action'}) { 883$href.="/".esc_url($params{'action'})unless$params{'action'}eq'summary'; 884delete$params{'action'}; 885} 886 887# Next, we put hash_parent_base:/file_parent..hash_base:/file_name, 888# stripping nonexistent or useless pieces 889$href.="/"if($params{'hash_base'} ||$params{'hash_parent_base'} 890||$params{'hash_parent'} ||$params{'hash'}); 891if(defined$params{'hash_base'}) { 892if(defined$params{'hash_parent_base'}) { 893$href.= esc_url($params{'hash_parent_base'}); 894# skip the file_parent if it's the same as the file_name 895delete$params{'file_parent'}if$params{'file_parent'}eq$params{'file_name'}; 896if(defined$params{'file_parent'} &&$params{'file_parent'} !~/\.\./) { 897$href.=":/".esc_url($params{'file_parent'}); 898delete$params{'file_parent'}; 899} 900$href.=".."; 901delete$params{'hash_parent'}; 902delete$params{'hash_parent_base'}; 903}elsif(defined$params{'hash_parent'}) { 904$href.= esc_url($params{'hash_parent'}).".."; 905delete$params{'hash_parent'}; 906} 907 908$href.= esc_url($params{'hash_base'}); 909if(defined$params{'file_name'} &&$params{'file_name'} !~/\.\./) { 910$href.=":/".esc_url($params{'file_name'}); 911delete$params{'file_name'}; 912} 913delete$params{'hash'}; 914delete$params{'hash_base'}; 915}elsif(defined$params{'hash'}) { 916$href.= esc_url($params{'hash'}); 917delete$params{'hash'}; 918} 919 920# If the action was a snapshot, we can absorb the 921# snapshot_format parameter too 922if($is_snapshot) { 923my$fmt=$params{'snapshot_format'}; 924# snapshot_format should always be defined when href() 925# is called, but just in case some code forgets, we 926# fall back to the default 927$fmt||=$snapshot_fmts[0]; 928$href.=$known_snapshot_formats{$fmt}{'suffix'}; 929delete$params{'snapshot_format'}; 930} 931} 932 933# now encode the parameters explicitly 934my@result= (); 935for(my$i=0;$i<@cgi_param_mapping;$i+=2) { 936my($name,$symbol) = ($cgi_param_mapping[$i],$cgi_param_mapping[$i+1]); 937if(defined$params{$name}) { 938if(ref($params{$name})eq"ARRAY") { 939foreachmy$par(@{$params{$name}}) { 940push@result,$symbol."=". esc_param($par); 941} 942}else{ 943push@result,$symbol."=". esc_param($params{$name}); 944} 945} 946} 947$href.="?".join(';',@result)ifscalar@result; 948 949return$href; 950} 951 952 953## ====================================================================== 954## validation, quoting/unquoting and escaping 955 956sub validate_action { 957my$input=shift||returnundef; 958returnundefunlessexists$actions{$input}; 959return$input; 960} 961 962sub validate_project { 963my$input=shift||returnundef; 964if(!validate_pathname($input) || 965!(-d "$projectroot/$input") || 966!check_export_ok("$projectroot/$input") || 967($strict_export&& !project_in_list($input))) { 968returnundef; 969}else{ 970return$input; 971} 972} 973 974sub validate_pathname { 975my$input=shift||returnundef; 976 977# no '.' or '..' as elements of path, i.e. no '.' nor '..' 978# at the beginning, at the end, and between slashes. 979# also this catches doubled slashes 980if($input=~m!(^|/)(|\.|\.\.)(/|$)!) { 981returnundef; 982} 983# no null characters 984if($input=~m!\0!) { 985returnundef; 986} 987return$input; 988} 989 990sub validate_refname { 991my$input=shift||returnundef; 992 993# textual hashes are O.K. 994if($input=~m/^[0-9a-fA-F]{40}$/) { 995return$input; 996} 997# it must be correct pathname 998$input= validate_pathname($input) 999orreturnundef;1000# restrictions on ref name according to git-check-ref-format1001if($input=~m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {1002returnundef;1003}1004return$input;1005}10061007# decode sequences of octets in utf8 into Perl's internal form,1008# which is utf-8 with utf8 flag set if needed. gitweb writes out1009# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning1010sub to_utf8 {1011my$str=shift;1012if(utf8::valid($str)) {1013 utf8::decode($str);1014return$str;1015}else{1016return decode($fallback_encoding,$str, Encode::FB_DEFAULT);1017}1018}10191020# quote unsafe chars, but keep the slash, even when it's not1021# correct, but quoted slashes look too horrible in bookmarks1022sub esc_param {1023my$str=shift;1024$str=~s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X",ord($1))/eg;1025$str=~s/\+/%2B/g;1026$str=~s/ /\+/g;1027return$str;1028}10291030# quote unsafe chars in whole URL, so some charactrs cannot be quoted1031sub esc_url {1032my$str=shift;1033$str=~s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X",ord($1))/eg;1034$str=~s/\+/%2B/g;1035$str=~s/ /\+/g;1036return$str;1037}10381039# replace invalid utf8 character with SUBSTITUTION sequence1040sub esc_html ($;%) {1041my$str=shift;1042my%opts=@_;10431044$str= to_utf8($str);1045$str=$cgi->escapeHTML($str);1046if($opts{'-nbsp'}) {1047$str=~s/ / /g;1048}1049$str=~ s|([[:cntrl:]])|(($1ne"\t") ? quot_cec($1) :$1)|eg;1050return$str;1051}10521053# quote control characters and escape filename to HTML1054sub esc_path {1055my$str=shift;1056my%opts=@_;10571058$str= to_utf8($str);1059$str=$cgi->escapeHTML($str);1060if($opts{'-nbsp'}) {1061$str=~s/ / /g;1062}1063$str=~ s|([[:cntrl:]])|quot_cec($1)|eg;1064return$str;1065}10661067# Make control characters "printable", using character escape codes (CEC)1068sub quot_cec {1069my$cntrl=shift;1070my%opts=@_;1071my%es= (# character escape codes, aka escape sequences1072"\t"=>'\t',# tab (HT)1073"\n"=>'\n',# line feed (LF)1074"\r"=>'\r',# carrige return (CR)1075"\f"=>'\f',# form feed (FF)1076"\b"=>'\b',# backspace (BS)1077"\a"=>'\a',# alarm (bell) (BEL)1078"\e"=>'\e',# escape (ESC)1079"\013"=>'\v',# vertical tab (VT)1080"\000"=>'\0',# nul character (NUL)1081);1082my$chr= ( (exists$es{$cntrl})1083?$es{$cntrl}1084:sprintf('\%2x',ord($cntrl)) );1085if($opts{-nohtml}) {1086return$chr;1087}else{1088return"<span class=\"cntrl\">$chr</span>";1089}1090}10911092# Alternatively use unicode control pictures codepoints,1093# Unicode "printable representation" (PR)1094sub quot_upr {1095my$cntrl=shift;1096my%opts=@_;10971098my$chr=sprintf('&#%04d;',0x2400+ord($cntrl));1099if($opts{-nohtml}) {1100return$chr;1101}else{1102return"<span class=\"cntrl\">$chr</span>";1103}1104}11051106# git may return quoted and escaped filenames1107sub unquote {1108my$str=shift;11091110sub unq {1111my$seq=shift;1112my%es= (# character escape codes, aka escape sequences1113't'=>"\t",# tab (HT, TAB)1114'n'=>"\n",# newline (NL)1115'r'=>"\r",# return (CR)1116'f'=>"\f",# form feed (FF)1117'b'=>"\b",# backspace (BS)1118'a'=>"\a",# alarm (bell) (BEL)1119'e'=>"\e",# escape (ESC)1120'v'=>"\013",# vertical tab (VT)1121);11221123if($seq=~m/^[0-7]{1,3}$/) {1124# octal char sequence1125returnchr(oct($seq));1126}elsif(exists$es{$seq}) {1127# C escape sequence, aka character escape code1128return$es{$seq};1129}1130# quoted ordinary character1131return$seq;1132}11331134if($str=~m/^"(.*)"$/) {1135# needs unquoting1136$str=$1;1137$str=~s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;1138}1139return$str;1140}11411142# escape tabs (convert tabs to spaces)1143sub untabify {1144my$line=shift;11451146while((my$pos=index($line,"\t")) != -1) {1147if(my$count= (8- ($pos%8))) {1148my$spaces=' ' x $count;1149$line=~s/\t/$spaces/;1150}1151}11521153return$line;1154}11551156sub project_in_list {1157my$project=shift;1158my@list= git_get_projects_list();1159return@list&&scalar(grep{$_->{'path'}eq$project}@list);1160}11611162## ----------------------------------------------------------------------1163## HTML aware string manipulation11641165# Try to chop given string on a word boundary between position1166# $len and $len+$add_len. If there is no word boundary there,1167# chop at $len+$add_len. Do not chop if chopped part plus ellipsis1168# (marking chopped part) would be longer than given string.1169sub chop_str {1170my$str=shift;1171my$len=shift;1172my$add_len=shift||10;1173my$where=shift||'right';# 'left' | 'center' | 'right'11741175# Make sure perl knows it is utf8 encoded so we don't1176# cut in the middle of a utf8 multibyte char.1177$str= to_utf8($str);11781179# allow only $len chars, but don't cut a word if it would fit in $add_len1180# if it doesn't fit, cut it if it's still longer than the dots we would add1181# remove chopped character entities entirely11821183# when chopping in the middle, distribute $len into left and right part1184# return early if chopping wouldn't make string shorter1185if($whereeq'center') {1186return$strif($len+5>=length($str));# filler is length 51187$len=int($len/2);1188}else{1189return$strif($len+4>=length($str));# filler is length 41190}11911192# regexps: ending and beginning with word part up to $add_len1193my$endre=qr/.{$len}\w{0,$add_len}/;1194my$begre=qr/\w{0,$add_len}.{$len}/;11951196if($whereeq'left') {1197$str=~m/^(.*?)($begre)$/;1198my($lead,$body) = ($1,$2);1199if(length($lead) >4) {1200$body=~s/^[^;]*;//if($lead=~m/&[^;]*$/);1201$lead=" ...";1202}1203return"$lead$body";12041205}elsif($whereeq'center') {1206$str=~m/^($endre)(.*)$/;1207my($left,$str) = ($1,$2);1208$str=~m/^(.*?)($begre)$/;1209my($mid,$right) = ($1,$2);1210if(length($mid) >5) {1211$left=~s/&[^;]*$//;1212$right=~s/^[^;]*;//if($mid=~m/&[^;]*$/);1213$mid=" ... ";1214}1215return"$left$mid$right";12161217}else{1218$str=~m/^($endre)(.*)$/;1219my$body=$1;1220my$tail=$2;1221if(length($tail) >4) {1222$body=~s/&[^;]*$//;1223$tail="... ";1224}1225return"$body$tail";1226}1227}12281229# takes the same arguments as chop_str, but also wraps a <span> around the1230# result with a title attribute if it does get chopped. Additionally, the1231# string is HTML-escaped.1232sub chop_and_escape_str {1233my($str) =@_;12341235my$chopped= chop_str(@_);1236if($choppedeq$str) {1237return esc_html($chopped);1238}else{1239$str=~s/([[:cntrl:]])/?/g;1240return$cgi->span({-title=>$str}, esc_html($chopped));1241}1242}12431244## ----------------------------------------------------------------------1245## functions returning short strings12461247# CSS class for given age value (in seconds)1248sub age_class {1249my$age=shift;12501251if(!defined$age) {1252return"noage";1253}elsif($age<60*60*2) {1254return"age0";1255}elsif($age<60*60*24*2) {1256return"age1";1257}else{1258return"age2";1259}1260}12611262# convert age in seconds to "nn units ago" string1263sub age_string {1264my$age=shift;1265my$age_str;12661267if($age>60*60*24*365*2) {1268$age_str= (int$age/60/60/24/365);1269$age_str.=" years ago";1270}elsif($age>60*60*24*(365/12)*2) {1271$age_str=int$age/60/60/24/(365/12);1272$age_str.=" months ago";1273}elsif($age>60*60*24*7*2) {1274$age_str=int$age/60/60/24/7;1275$age_str.=" weeks ago";1276}elsif($age>60*60*24*2) {1277$age_str=int$age/60/60/24;1278$age_str.=" days ago";1279}elsif($age>60*60*2) {1280$age_str=int$age/60/60;1281$age_str.=" hours ago";1282}elsif($age>60*2) {1283$age_str=int$age/60;1284$age_str.=" min ago";1285}elsif($age>2) {1286$age_str=int$age;1287$age_str.=" sec ago";1288}else{1289$age_str.=" right now";1290}1291return$age_str;1292}12931294useconstant{1295 S_IFINVALID =>0030000,1296 S_IFGITLINK =>0160000,1297};12981299# submodule/subproject, a commit object reference1300sub S_ISGITLINK($) {1301my$mode=shift;13021303return(($mode& S_IFMT) == S_IFGITLINK)1304}13051306# convert file mode in octal to symbolic file mode string1307sub mode_str {1308my$mode=oct shift;13091310if(S_ISGITLINK($mode)) {1311return'm---------';1312}elsif(S_ISDIR($mode& S_IFMT)) {1313return'drwxr-xr-x';1314}elsif(S_ISLNK($mode)) {1315return'lrwxrwxrwx';1316}elsif(S_ISREG($mode)) {1317# git cares only about the executable bit1318if($mode& S_IXUSR) {1319return'-rwxr-xr-x';1320}else{1321return'-rw-r--r--';1322};1323}else{1324return'----------';1325}1326}13271328# convert file mode in octal to file type string1329sub file_type {1330my$mode=shift;13311332if($mode!~m/^[0-7]+$/) {1333return$mode;1334}else{1335$mode=oct$mode;1336}13371338if(S_ISGITLINK($mode)) {1339return"submodule";1340}elsif(S_ISDIR($mode& S_IFMT)) {1341return"directory";1342}elsif(S_ISLNK($mode)) {1343return"symlink";1344}elsif(S_ISREG($mode)) {1345return"file";1346}else{1347return"unknown";1348}1349}13501351# convert file mode in octal to file type description string1352sub file_type_long {1353my$mode=shift;13541355if($mode!~m/^[0-7]+$/) {1356return$mode;1357}else{1358$mode=oct$mode;1359}13601361if(S_ISGITLINK($mode)) {1362return"submodule";1363}elsif(S_ISDIR($mode& S_IFMT)) {1364return"directory";1365}elsif(S_ISLNK($mode)) {1366return"symlink";1367}elsif(S_ISREG($mode)) {1368if($mode& S_IXUSR) {1369return"executable";1370}else{1371return"file";1372};1373}else{1374return"unknown";1375}1376}137713781379## ----------------------------------------------------------------------1380## functions returning short HTML fragments, or transforming HTML fragments1381## which don't belong to other sections13821383# format line of commit message.1384sub format_log_line_html {1385my$line=shift;13861387$line= esc_html($line, -nbsp=>1);1388if($line=~m/([0-9a-fA-F]{8,40})/) {1389my$hash_text=$1;1390my$link=1391$cgi->a({-href => href(action=>"object", hash=>$hash_text),1392-class=>"text"},$hash_text);1393$line=~s/$hash_text/$link/;1394}1395return$line;1396}13971398# format marker of refs pointing to given object13991400# the destination action is chosen based on object type and current context:1401# - for annotated tags, we choose the tag view unless it's the current view1402# already, in which case we go to shortlog view1403# - for other refs, we keep the current view if we're in history, shortlog or1404# log view, and select shortlog otherwise1405sub format_ref_marker {1406my($refs,$id) =@_;1407my$markers='';14081409if(defined$refs->{$id}) {1410foreachmy$ref(@{$refs->{$id}}) {1411# this code exploits the fact that non-lightweight tags are the1412# only indirect objects, and that they are the only objects for which1413# we want to use tag instead of shortlog as action1414my($type,$name) =qw();1415my$indirect= ($ref=~s/\^\{\}$//);1416# e.g. tags/v2.6.11 or heads/next1417if($ref=~m!^(.*?)s?/(.*)$!) {1418$type=$1;1419$name=$2;1420}else{1421$type="ref";1422$name=$ref;1423}14241425my$class=$type;1426$class.=" indirect"if$indirect;14271428my$dest_action="shortlog";14291430if($indirect) {1431$dest_action="tag"unless$actioneq"tag";1432}elsif($action=~/^(history|(short)?log)$/) {1433$dest_action=$action;1434}14351436my$dest="";1437$dest.="refs/"unless$ref=~ m!^refs/!;1438$dest.=$ref;14391440my$link=$cgi->a({1441-href => href(1442 action=>$dest_action,1443 hash=>$dest1444)},$name);14451446$markers.=" <span class=\"$class\"title=\"$ref\">".1447$link."</span>";1448}1449}14501451if($markers) {1452return' <span class="refs">'.$markers.'</span>';1453}else{1454return"";1455}1456}14571458# format, perhaps shortened and with markers, title line1459sub format_subject_html {1460my($long,$short,$href,$extra) =@_;1461$extra=''unlessdefined($extra);14621463if(length($short) <length($long)) {1464return$cgi->a({-href =>$href, -class=>"list subject",1465-title => to_utf8($long)},1466 esc_html($short) .$extra);1467}else{1468return$cgi->a({-href =>$href, -class=>"list subject"},1469 esc_html($long) .$extra);1470}1471}14721473# format git diff header line, i.e. "diff --(git|combined|cc) ..."1474sub format_git_diff_header_line {1475my$line=shift;1476my$diffinfo=shift;1477my($from,$to) =@_;14781479if($diffinfo->{'nparents'}) {1480# combined diff1481$line=~s!^(diff (.*?) )"?.*$!$1!;1482if($to->{'href'}) {1483$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1484 esc_path($to->{'file'}));1485}else{# file was deleted (no href)1486$line.= esc_path($to->{'file'});1487}1488}else{1489# "ordinary" diff1490$line=~s!^(diff (.*?) )"?a/.*$!$1!;1491if($from->{'href'}) {1492$line.=$cgi->a({-href =>$from->{'href'}, -class=>"path"},1493'a/'. esc_path($from->{'file'}));1494}else{# file was added (no href)1495$line.='a/'. esc_path($from->{'file'});1496}1497$line.=' ';1498if($to->{'href'}) {1499$line.=$cgi->a({-href =>$to->{'href'}, -class=>"path"},1500'b/'. esc_path($to->{'file'}));1501}else{# file was deleted1502$line.='b/'. esc_path($to->{'file'});1503}1504}15051506return"<div class=\"diff header\">$line</div>\n";1507}15081509# format extended diff header line, before patch itself1510sub format_extended_diff_header_line {1511my$line=shift;1512my$diffinfo=shift;1513my($from,$to) =@_;15141515# match <path>1516if($line=~s!^((copy|rename) from ).*$!$1!&&$from->{'href'}) {1517$line.=$cgi->a({-href=>$from->{'href'}, -class=>"path"},1518 esc_path($from->{'file'}));1519}1520if($line=~s!^((copy|rename) to ).*$!$1!&&$to->{'href'}) {1521$line.=$cgi->a({-href=>$to->{'href'}, -class=>"path"},1522 esc_path($to->{'file'}));1523}1524# match single <mode>1525if($line=~m/\s(\d{6})$/) {1526$line.='<span class="info"> ('.1527 file_type_long($1) .1528')</span>';1529}1530# match <hash>1531if($line=~m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {1532# can match only for combined diff1533$line='index ';1534for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1535if($from->{'href'}[$i]) {1536$line.=$cgi->a({-href=>$from->{'href'}[$i],1537-class=>"hash"},1538substr($diffinfo->{'from_id'}[$i],0,7));1539}else{1540$line.='0' x 7;1541}1542# separator1543$line.=','if($i<$diffinfo->{'nparents'} -1);1544}1545$line.='..';1546if($to->{'href'}) {1547$line.=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1548substr($diffinfo->{'to_id'},0,7));1549}else{1550$line.='0' x 7;1551}15521553}elsif($line=~m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {1554# can match only for ordinary diff1555my($from_link,$to_link);1556if($from->{'href'}) {1557$from_link=$cgi->a({-href=>$from->{'href'}, -class=>"hash"},1558substr($diffinfo->{'from_id'},0,7));1559}else{1560$from_link='0' x 7;1561}1562if($to->{'href'}) {1563$to_link=$cgi->a({-href=>$to->{'href'}, -class=>"hash"},1564substr($diffinfo->{'to_id'},0,7));1565}else{1566$to_link='0' x 7;1567}1568my($from_id,$to_id) = ($diffinfo->{'from_id'},$diffinfo->{'to_id'});1569$line=~s!$from_id\.\.$to_id!$from_link..$to_link!;1570}15711572return$line."<br/>\n";1573}15741575# format from-file/to-file diff header1576sub format_diff_from_to_header {1577my($from_line,$to_line,$diffinfo,$from,$to,@parents) =@_;1578my$line;1579my$result='';15801581$line=$from_line;1582#assert($line =~ m/^---/) if DEBUG;1583# no extra formatting for "^--- /dev/null"1584if(!$diffinfo->{'nparents'}) {1585# ordinary (single parent) diff1586if($line=~m!^--- "?a/!) {1587if($from->{'href'}) {1588$line='--- a/'.1589$cgi->a({-href=>$from->{'href'}, -class=>"path"},1590 esc_path($from->{'file'}));1591}else{1592$line='--- a/'.1593 esc_path($from->{'file'});1594}1595}1596$result.= qq!<div class="diff from_file">$line</div>\n!;15971598}else{1599# combined diff (merge commit)1600for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {1601if($from->{'href'}[$i]) {1602$line='--- '.1603$cgi->a({-href=>href(action=>"blobdiff",1604 hash_parent=>$diffinfo->{'from_id'}[$i],1605 hash_parent_base=>$parents[$i],1606 file_parent=>$from->{'file'}[$i],1607 hash=>$diffinfo->{'to_id'},1608 hash_base=>$hash,1609 file_name=>$to->{'file'}),1610-class=>"path",1611-title=>"diff". ($i+1)},1612$i+1) .1613'/'.1614$cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},1615 esc_path($from->{'file'}[$i]));1616}else{1617$line='--- /dev/null';1618}1619$result.= qq!<div class="diff from_file">$line</div>\n!;1620}1621}16221623$line=$to_line;1624#assert($line =~ m/^\+\+\+/) if DEBUG;1625# no extra formatting for "^+++ /dev/null"1626if($line=~m!^\+\+\+ "?b/!) {1627if($to->{'href'}) {1628$line='+++ b/'.1629$cgi->a({-href=>$to->{'href'}, -class=>"path"},1630 esc_path($to->{'file'}));1631}else{1632$line='+++ b/'.1633 esc_path($to->{'file'});1634}1635}1636$result.= qq!<div class="diff to_file">$line</div>\n!;16371638return$result;1639}16401641# create note for patch simplified by combined diff1642sub format_diff_cc_simplified {1643my($diffinfo,@parents) =@_;1644my$result='';16451646$result.="<div class=\"diff header\">".1647"diff --cc ";1648if(!is_deleted($diffinfo)) {1649$result.=$cgi->a({-href => href(action=>"blob",1650 hash_base=>$hash,1651 hash=>$diffinfo->{'to_id'},1652 file_name=>$diffinfo->{'to_file'}),1653-class=>"path"},1654 esc_path($diffinfo->{'to_file'}));1655}else{1656$result.= esc_path($diffinfo->{'to_file'});1657}1658$result.="</div>\n".# class="diff header"1659"<div class=\"diff nodifferences\">".1660"Simple merge".1661"</div>\n";# class="diff nodifferences"16621663return$result;1664}16651666# format patch (diff) line (not to be used for diff headers)1667sub format_diff_line {1668my$line=shift;1669my($from,$to) =@_;1670my$diff_class="";16711672chomp$line;16731674if($from&&$to&&ref($from->{'href'})eq"ARRAY") {1675# combined diff1676my$prefix=substr($line,0,scalar@{$from->{'href'}});1677if($line=~m/^\@{3}/) {1678$diff_class=" chunk_header";1679}elsif($line=~m/^\\/) {1680$diff_class=" incomplete";1681}elsif($prefix=~tr/+/+/) {1682$diff_class=" add";1683}elsif($prefix=~tr/-/-/) {1684$diff_class=" rem";1685}1686}else{1687# assume ordinary diff1688my$char=substr($line,0,1);1689if($chareq'+') {1690$diff_class=" add";1691}elsif($chareq'-') {1692$diff_class=" rem";1693}elsif($chareq'@') {1694$diff_class=" chunk_header";1695}elsif($chareq"\\") {1696$diff_class=" incomplete";1697}1698}1699$line= untabify($line);1700if($from&&$to&&$line=~m/^\@{2} /) {1701my($from_text,$from_start,$from_lines,$to_text,$to_start,$to_lines,$section) =1702$line=~m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;17031704$from_lines=0unlessdefined$from_lines;1705$to_lines=0unlessdefined$to_lines;17061707if($from->{'href'}) {1708$from_text=$cgi->a({-href=>"$from->{'href'}#l$from_start",1709-class=>"list"},$from_text);1710}1711if($to->{'href'}) {1712$to_text=$cgi->a({-href=>"$to->{'href'}#l$to_start",1713-class=>"list"},$to_text);1714}1715$line="<span class=\"chunk_info\">@@$from_text$to_text@@</span>".1716"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";1717return"<div class=\"diff$diff_class\">$line</div>\n";1718}elsif($from&&$to&&$line=~m/^\@{3}/) {1719my($prefix,$ranges,$section) =$line=~m/^(\@+) (.*?) \@+(.*)$/;1720my(@from_text,@from_start,@from_nlines,$to_text,$to_start,$to_nlines);17211722@from_text=split(' ',$ranges);1723for(my$i=0;$i<@from_text; ++$i) {1724($from_start[$i],$from_nlines[$i]) =1725(split(',',substr($from_text[$i],1)),0);1726}17271728$to_text=pop@from_text;1729$to_start=pop@from_start;1730$to_nlines=pop@from_nlines;17311732$line="<span class=\"chunk_info\">$prefix";1733for(my$i=0;$i<@from_text; ++$i) {1734if($from->{'href'}[$i]) {1735$line.=$cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",1736-class=>"list"},$from_text[$i]);1737}else{1738$line.=$from_text[$i];1739}1740$line.=" ";1741}1742if($to->{'href'}) {1743$line.=$cgi->a({-href=>"$to->{'href'}#l$to_start",1744-class=>"list"},$to_text);1745}else{1746$line.=$to_text;1747}1748$line.="$prefix</span>".1749"<span class=\"section\">". esc_html($section, -nbsp=>1) ."</span>";1750return"<div class=\"diff$diff_class\">$line</div>\n";1751}1752return"<div class=\"diff$diff_class\">". esc_html($line, -nbsp=>1) ."</div>\n";1753}17541755# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",1756# linked. Pass the hash of the tree/commit to snapshot.1757sub format_snapshot_links {1758my($hash) =@_;1759my$num_fmts=@snapshot_fmts;1760if($num_fmts>1) {1761# A parenthesized list of links bearing format names.1762# e.g. "snapshot (_tar.gz_ _zip_)"1763return"snapshot (".join(' ',map1764$cgi->a({1765-href => href(1766 action=>"snapshot",1767 hash=>$hash,1768 snapshot_format=>$_1769)1770},$known_snapshot_formats{$_}{'display'})1771,@snapshot_fmts) .")";1772}elsif($num_fmts==1) {1773# A single "snapshot" link whose tooltip bears the format name.1774# i.e. "_snapshot_"1775my($fmt) =@snapshot_fmts;1776return1777$cgi->a({1778-href => href(1779 action=>"snapshot",1780 hash=>$hash,1781 snapshot_format=>$fmt1782),1783-title =>"in format:$known_snapshot_formats{$fmt}{'display'}"1784},"snapshot");1785}else{# $num_fmts == 01786returnundef;1787}1788}17891790## ......................................................................1791## functions returning values to be passed, perhaps after some1792## transformation, to other functions; e.g. returning arguments to href()17931794# returns hash to be passed to href to generate gitweb URL1795# in -title key it returns description of link1796sub get_feed_info {1797my$format=shift||'Atom';1798my%res= (action =>lc($format));17991800# feed links are possible only for project views1801return unless(defined$project);1802# some views should link to OPML, or to generic project feed,1803# or don't have specific feed yet (so they should use generic)1804return if($action=~/^(?:tags|heads|forks|tag|search)$/x);18051806my$branch;1807# branches refs uses 'refs/heads/' prefix (fullname) to differentiate1808# from tag links; this also makes possible to detect branch links1809if((defined$hash_base&&$hash_base=~m!^refs/heads/(.*)$!) ||1810(defined$hash&&$hash=~m!^refs/heads/(.*)$!)) {1811$branch=$1;1812}1813# find log type for feed description (title)1814my$type='log';1815if(defined$file_name) {1816$type="history of$file_name";1817$type.="/"if($actioneq'tree');1818$type.=" on '$branch'"if(defined$branch);1819}else{1820$type="log of$branch"if(defined$branch);1821}18221823$res{-title} =$type;1824$res{'hash'} = (defined$branch?"refs/heads/$branch":undef);1825$res{'file_name'} =$file_name;18261827return%res;1828}18291830## ----------------------------------------------------------------------1831## git utility subroutines, invoking git commands18321833# returns path to the core git executable and the --git-dir parameter as list1834sub git_cmd {1835return$GIT,'--git-dir='.$git_dir;1836}18371838# quote the given arguments for passing them to the shell1839# quote_command("command", "arg 1", "arg with ' and ! characters")1840# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"1841# Try to avoid using this function wherever possible.1842sub quote_command {1843returnjoin(' ',1844map( {my$a=$_;$a=~s/(['!])/'\\$1'/g;"'$a'"}@_));1845}18461847# get HEAD ref of given project as hash1848sub git_get_head_hash {1849my$project=shift;1850my$o_git_dir=$git_dir;1851my$retval=undef;1852$git_dir="$projectroot/$project";1853if(open my$fd,"-|", git_cmd(),"rev-parse","--verify","HEAD") {1854my$head= <$fd>;1855close$fd;1856if(defined$head&&$head=~/^([0-9a-fA-F]{40})$/) {1857$retval=$1;1858}1859}1860if(defined$o_git_dir) {1861$git_dir=$o_git_dir;1862}1863return$retval;1864}18651866# get type of given object1867sub git_get_type {1868my$hash=shift;18691870open my$fd,"-|", git_cmd(),"cat-file",'-t',$hashorreturn;1871my$type= <$fd>;1872close$fdorreturn;1873chomp$type;1874return$type;1875}18761877# repository configuration1878our$config_file='';1879our%config;18801881# store multiple values for single key as anonymous array reference1882# single values stored directly in the hash, not as [ <value> ]1883sub hash_set_multi {1884my($hash,$key,$value) =@_;18851886if(!exists$hash->{$key}) {1887$hash->{$key} =$value;1888}elsif(!ref$hash->{$key}) {1889$hash->{$key} = [$hash->{$key},$value];1890}else{1891push@{$hash->{$key}},$value;1892}1893}18941895# return hash of git project configuration1896# optionally limited to some section, e.g. 'gitweb'1897sub git_parse_project_config {1898my$section_regexp=shift;1899my%config;19001901local$/="\0";19021903open my$fh,"-|", git_cmd(),"config",'-z','-l',1904orreturn;19051906while(my$keyval= <$fh>) {1907chomp$keyval;1908my($key,$value) =split(/\n/,$keyval,2);19091910 hash_set_multi(\%config,$key,$value)1911if(!defined$section_regexp||$key=~/^(?:$section_regexp)\./o);1912}1913close$fh;19141915return%config;1916}19171918# convert config value to boolean, 'true' or 'false'1919# no value, number > 0, 'true' and 'yes' values are true1920# rest of values are treated as false (never as error)1921sub config_to_bool {1922my$val=shift;19231924# strip leading and trailing whitespace1925$val=~s/^\s+//;1926$val=~s/\s+$//;19271928return(!defined$val||# section.key1929($val=~/^\d+$/&&$val) ||# section.key = 11930($val=~/^(?:true|yes)$/i));# section.key = true1931}19321933# convert config value to simple decimal number1934# an optional value suffix of 'k', 'm', or 'g' will cause the value1935# to be multiplied by 1024, 1048576, or 10737418241936sub config_to_int {1937my$val=shift;19381939# strip leading and trailing whitespace1940$val=~s/^\s+//;1941$val=~s/\s+$//;19421943if(my($num,$unit) = ($val=~/^([0-9]*)([kmg])$/i)) {1944$unit=lc($unit);1945# unknown unit is treated as 11946return$num* ($uniteq'g'?1073741824:1947$uniteq'm'?1048576:1948$uniteq'k'?1024:1);1949}1950return$val;1951}19521953# convert config value to array reference, if needed1954sub config_to_multi {1955my$val=shift;19561957returnref($val) ?$val: (defined($val) ? [$val] : []);1958}19591960sub git_get_project_config {1961my($key,$type) =@_;19621963# key sanity check1964return unless($key);1965$key=~s/^gitweb\.//;1966return if($key=~m/\W/);19671968# type sanity check1969if(defined$type) {1970$type=~s/^--//;1971$type=undef1972unless($typeeq'bool'||$typeeq'int');1973}19741975# get config1976if(!defined$config_file||1977$config_filene"$git_dir/config") {1978%config= git_parse_project_config('gitweb');1979$config_file="$git_dir/config";1980}19811982# ensure given type1983if(!defined$type) {1984return$config{"gitweb.$key"};1985}elsif($typeeq'bool') {1986# backward compatibility: 'git config --bool' returns true/false1987return config_to_bool($config{"gitweb.$key"}) ?'true':'false';1988}elsif($typeeq'int') {1989return config_to_int($config{"gitweb.$key"});1990}1991return$config{"gitweb.$key"};1992}19931994# get hash of given path at given ref1995sub git_get_hash_by_path {1996my$base=shift;1997my$path=shift||returnundef;1998my$type=shift;19992000$path=~ s,/+$,,;20012002open my$fd,"-|", git_cmd(),"ls-tree",$base,"--",$path2003or die_error(500,"Open git-ls-tree failed");2004my$line= <$fd>;2005close$fdorreturnundef;20062007if(!defined$line) {2008# there is no tree or hash given by $path at $base2009returnundef;2010}20112012#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2013$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;2014if(defined$type&&$typene$2) {2015# type doesn't match2016returnundef;2017}2018return$3;2019}20202021# get path of entry with given hash at given tree-ish (ref)2022# used to get 'from' filename for combined diff (merge commit) for renames2023sub git_get_path_by_hash {2024my$base=shift||return;2025my$hash=shift||return;20262027local$/="\0";20282029open my$fd,"-|", git_cmd(),"ls-tree",'-r','-t','-z',$base2030orreturnundef;2031while(my$line= <$fd>) {2032chomp$line;20332034#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'2035#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'2036if($line=~m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {2037close$fd;2038return$1;2039}2040}2041close$fd;2042returnundef;2043}20442045## ......................................................................2046## git utility functions, directly accessing git repository20472048sub git_get_project_description {2049my$path=shift;20502051$git_dir="$projectroot/$path";2052open my$fd,"$git_dir/description"2053orreturn git_get_project_config('description');2054my$descr= <$fd>;2055close$fd;2056if(defined$descr) {2057chomp$descr;2058}2059return$descr;2060}20612062sub git_get_project_ctags {2063my$path=shift;2064my$ctags= {};20652066$git_dir="$projectroot/$path";2067unless(opendir D,"$git_dir/ctags") {2068return$ctags;2069}2070foreach(grep{ -f $_}map{"$git_dir/ctags/$_"}readdir(D)) {2071open CT,$_ornext;2072my$val= <CT>;2073chomp$val;2074close CT;2075my$ctag=$_;$ctag=~ s#.*/##;2076$ctags->{$ctag} =$val;2077}2078closedir D;2079$ctags;2080}20812082sub git_populate_project_tagcloud {2083my$ctags=shift;20842085# First, merge different-cased tags; tags vote on casing2086my%ctags_lc;2087foreach(keys%$ctags) {2088$ctags_lc{lc$_}->{count} +=$ctags->{$_};2089if(not$ctags_lc{lc$_}->{topcount}2090or$ctags_lc{lc$_}->{topcount} <$ctags->{$_}) {2091$ctags_lc{lc$_}->{topcount} =$ctags->{$_};2092$ctags_lc{lc$_}->{topname} =$_;2093}2094}20952096my$cloud;2097if(eval{require HTML::TagCloud;1; }) {2098$cloud= HTML::TagCloud->new;2099foreach(sort keys%ctags_lc) {2100# Pad the title with spaces so that the cloud looks2101# less crammed.2102my$title=$ctags_lc{$_}->{topname};2103$title=~s/ / /g;2104$title=~s/^/ /g;2105$title=~s/$/ /g;2106$cloud->add($title,$home_link."?by_tag=".$_,$ctags_lc{$_}->{count});2107}2108}else{2109$cloud= \%ctags_lc;2110}2111$cloud;2112}21132114sub git_show_project_tagcloud {2115my($cloud,$count) =@_;2116print STDERR ref($cloud)."..\n";2117if(ref$cloudeq'HTML::TagCloud') {2118return$cloud->html_and_css($count);2119}else{2120my@tags=sort{$cloud->{$a}->{count} <=>$cloud->{$b}->{count} }keys%$cloud;2121return'<p align="center">'.join(', ',map{2122"<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"2123}splice(@tags,0,$count)) .'</p>';2124}2125}21262127sub git_get_project_url_list {2128my$path=shift;21292130$git_dir="$projectroot/$path";2131open my$fd,"$git_dir/cloneurl"2132orreturnwantarray?2133@{ config_to_multi(git_get_project_config('url')) } :2134 config_to_multi(git_get_project_config('url'));2135my@git_project_url_list=map{chomp;$_} <$fd>;2136close$fd;21372138returnwantarray?@git_project_url_list: \@git_project_url_list;2139}21402141sub git_get_projects_list {2142my($filter) =@_;2143my@list;21442145$filter||='';2146$filter=~s/\.git$//;21472148my$check_forks= gitweb_check_feature('forks');21492150if(-d $projects_list) {2151# search in directory2152my$dir=$projects_list. ($filter?"/$filter":'');2153# remove the trailing "/"2154$dir=~s!/+$!!;2155my$pfxlen=length("$dir");2156my$pfxdepth= ($dir=~tr!/!!);21572158 File::Find::find({2159 follow_fast =>1,# follow symbolic links2160 follow_skip =>2,# ignore duplicates2161 dangling_symlinks =>0,# ignore dangling symlinks, silently2162 wanted =>sub{2163# skip project-list toplevel, if we get it.2164return if(m!^[/.]$!);2165# only directories can be git repositories2166return unless(-d $_);2167# don't traverse too deep (Find is super slow on os x)2168if(($File::Find::name =~tr!/!!) -$pfxdepth>$project_maxdepth) {2169$File::Find::prune =1;2170return;2171}21722173my$subdir=substr($File::Find::name,$pfxlen+1);2174# we check related file in $projectroot2175if(check_export_ok("$projectroot/$filter/$subdir")) {2176push@list, { path => ($filter?"$filter/":'') .$subdir};2177$File::Find::prune =1;2178}2179},2180},"$dir");21812182}elsif(-f $projects_list) {2183# read from file(url-encoded):2184# 'git%2Fgit.git Linus+Torvalds'2185# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2186# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2187my%paths;2188open my($fd),$projects_listorreturn;2189 PROJECT:2190while(my$line= <$fd>) {2191chomp$line;2192my($path,$owner) =split' ',$line;2193$path= unescape($path);2194$owner= unescape($owner);2195if(!defined$path) {2196next;2197}2198if($filterne'') {2199# looking for forks;2200my$pfx=substr($path,0,length($filter));2201if($pfxne$filter) {2202next PROJECT;2203}2204my$sfx=substr($path,length($filter));2205if($sfx!~/^\/.*\.git$/) {2206next PROJECT;2207}2208}elsif($check_forks) {2209 PATH:2210foreachmy$filter(keys%paths) {2211# looking for forks;2212my$pfx=substr($path,0,length($filter));2213if($pfxne$filter) {2214next PATH;2215}2216my$sfx=substr($path,length($filter));2217if($sfx!~/^\/.*\.git$/) {2218next PATH;2219}2220# is a fork, don't include it in2221# the list2222next PROJECT;2223}2224}2225if(check_export_ok("$projectroot/$path")) {2226my$pr= {2227 path =>$path,2228 owner => to_utf8($owner),2229};2230push@list,$pr;2231(my$forks_path=$path) =~s/\.git$//;2232$paths{$forks_path}++;2233}2234}2235close$fd;2236}2237return@list;2238}22392240our$gitweb_project_owner=undef;2241sub git_get_project_list_from_file {22422243return if(defined$gitweb_project_owner);22442245$gitweb_project_owner= {};2246# read from file (url-encoded):2247# 'git%2Fgit.git Linus+Torvalds'2248# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'2249# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'2250if(-f $projects_list) {2251open(my$fd,$projects_list);2252while(my$line= <$fd>) {2253chomp$line;2254my($pr,$ow) =split' ',$line;2255$pr= unescape($pr);2256$ow= unescape($ow);2257$gitweb_project_owner->{$pr} = to_utf8($ow);2258}2259close$fd;2260}2261}22622263sub git_get_project_owner {2264my$project=shift;2265my$owner;22662267returnundefunless$project;2268$git_dir="$projectroot/$project";22692270if(!defined$gitweb_project_owner) {2271 git_get_project_list_from_file();2272}22732274if(exists$gitweb_project_owner->{$project}) {2275$owner=$gitweb_project_owner->{$project};2276}2277if(!defined$owner){2278$owner= git_get_project_config('owner');2279}2280if(!defined$owner) {2281$owner= get_file_owner("$git_dir");2282}22832284return$owner;2285}22862287sub git_get_last_activity {2288my($path) =@_;2289my$fd;22902291$git_dir="$projectroot/$path";2292open($fd,"-|", git_cmd(),'for-each-ref',2293'--format=%(committer)',2294'--sort=-committerdate',2295'--count=1',2296'refs/heads')orreturn;2297my$most_recent= <$fd>;2298close$fdorreturn;2299if(defined$most_recent&&2300$most_recent=~/ (\d+) [-+][01]\d\d\d$/) {2301my$timestamp=$1;2302my$age=time-$timestamp;2303return($age, age_string($age));2304}2305return(undef,undef);2306}23072308sub git_get_references {2309my$type=shift||"";2310my%refs;2311# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.112312# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}2313open my$fd,"-|", git_cmd(),"show-ref","--dereference",2314($type? ("--","refs/$type") : ())# use -- <pattern> if $type2315orreturn;23162317while(my$line= <$fd>) {2318chomp$line;2319if($line=~m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {2320if(defined$refs{$1}) {2321push@{$refs{$1}},$2;2322}else{2323$refs{$1} = [$2];2324}2325}2326}2327close$fdorreturn;2328return \%refs;2329}23302331sub git_get_rev_name_tags {2332my$hash=shift||returnundef;23332334open my$fd,"-|", git_cmd(),"name-rev","--tags",$hash2335orreturn;2336my$name_rev= <$fd>;2337close$fd;23382339if($name_rev=~ m|^$hash tags/(.*)$|) {2340return$1;2341}else{2342# catches also '$hash undefined' output2343returnundef;2344}2345}23462347## ----------------------------------------------------------------------2348## parse to hash functions23492350sub parse_date {2351my$epoch=shift;2352my$tz=shift||"-0000";23532354my%date;2355my@months= ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");2356my@days= ("Sun","Mon","Tue","Wed","Thu","Fri","Sat");2357my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($epoch);2358$date{'hour'} =$hour;2359$date{'minute'} =$min;2360$date{'mday'} =$mday;2361$date{'day'} =$days[$wday];2362$date{'month'} =$months[$mon];2363$date{'rfc2822'} =sprintf"%s,%d%s%4d%02d:%02d:%02d+0000",2364$days[$wday],$mday,$months[$mon],1900+$year,$hour,$min,$sec;2365$date{'mday-time'} =sprintf"%d%s%02d:%02d",2366$mday,$months[$mon],$hour,$min;2367$date{'iso-8601'} =sprintf"%04d-%02d-%02dT%02d:%02d:%02dZ",23681900+$year,1+$mon,$mday,$hour,$min,$sec;23692370$tz=~m/^([+\-][0-9][0-9])([0-9][0-9])$/;2371my$local=$epoch+ ((int$1+ ($2/60)) *3600);2372($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($local);2373$date{'hour_local'} =$hour;2374$date{'minute_local'} =$min;2375$date{'tz_local'} =$tz;2376$date{'iso-tz'} =sprintf("%04d-%02d-%02d%02d:%02d:%02d%s",23771900+$year,$mon+1,$mday,2378$hour,$min,$sec,$tz);2379return%date;2380}23812382sub parse_tag {2383my$tag_id=shift;2384my%tag;2385my@comment;23862387open my$fd,"-|", git_cmd(),"cat-file","tag",$tag_idorreturn;2388$tag{'id'} =$tag_id;2389while(my$line= <$fd>) {2390chomp$line;2391if($line=~m/^object ([0-9a-fA-F]{40})$/) {2392$tag{'object'} =$1;2393}elsif($line=~m/^type (.+)$/) {2394$tag{'type'} =$1;2395}elsif($line=~m/^tag (.+)$/) {2396$tag{'name'} =$1;2397}elsif($line=~m/^tagger (.*) ([0-9]+) (.*)$/) {2398$tag{'author'} =$1;2399$tag{'epoch'} =$2;2400$tag{'tz'} =$3;2401}elsif($line=~m/--BEGIN/) {2402push@comment,$line;2403last;2404}elsif($lineeq"") {2405last;2406}2407}2408push@comment, <$fd>;2409$tag{'comment'} = \@comment;2410close$fdorreturn;2411if(!defined$tag{'name'}) {2412return2413};2414return%tag2415}24162417sub parse_commit_text {2418my($commit_text,$withparents) =@_;2419my@commit_lines=split'\n',$commit_text;2420my%co;24212422pop@commit_lines;# Remove '\0'24232424if(!@commit_lines) {2425return;2426}24272428my$header=shift@commit_lines;2429if($header!~m/^[0-9a-fA-F]{40}/) {2430return;2431}2432($co{'id'},my@parents) =split' ',$header;2433while(my$line=shift@commit_lines) {2434last if$lineeq"\n";2435if($line=~m/^tree ([0-9a-fA-F]{40})$/) {2436$co{'tree'} =$1;2437}elsif((!defined$withparents) && ($line=~m/^parent ([0-9a-fA-F]{40})$/)) {2438push@parents,$1;2439}elsif($line=~m/^author (.*) ([0-9]+) (.*)$/) {2440$co{'author'} =$1;2441$co{'author_epoch'} =$2;2442$co{'author_tz'} =$3;2443if($co{'author'} =~m/^([^<]+) <([^>]*)>/) {2444$co{'author_name'} =$1;2445$co{'author_email'} =$2;2446}else{2447$co{'author_name'} =$co{'author'};2448}2449}elsif($line=~m/^committer (.*) ([0-9]+) (.*)$/) {2450$co{'committer'} =$1;2451$co{'committer_epoch'} =$2;2452$co{'committer_tz'} =$3;2453$co{'committer_name'} =$co{'committer'};2454if($co{'committer'} =~m/^([^<]+) <([^>]*)>/) {2455$co{'committer_name'} =$1;2456$co{'committer_email'} =$2;2457}else{2458$co{'committer_name'} =$co{'committer'};2459}2460}2461}2462if(!defined$co{'tree'}) {2463return;2464};2465$co{'parents'} = \@parents;2466$co{'parent'} =$parents[0];24672468foreachmy$title(@commit_lines) {2469$title=~s/^ //;2470if($titlene"") {2471$co{'title'} = chop_str($title,80,5);2472# remove leading stuff of merges to make the interesting part visible2473if(length($title) >50) {2474$title=~s/^Automatic //;2475$title=~s/^merge (of|with) /Merge ... /i;2476if(length($title) >50) {2477$title=~s/(http|rsync):\/\///;2478}2479if(length($title) >50) {2480$title=~s/(master|www|rsync)\.//;2481}2482if(length($title) >50) {2483$title=~s/kernel.org:?//;2484}2485if(length($title) >50) {2486$title=~s/\/pub\/scm//;2487}2488}2489$co{'title_short'} = chop_str($title,50,5);2490last;2491}2492}2493if(!defined$co{'title'} ||$co{'title'}eq"") {2494$co{'title'} =$co{'title_short'} ='(no commit message)';2495}2496# remove added spaces2497foreachmy$line(@commit_lines) {2498$line=~s/^ //;2499}2500$co{'comment'} = \@commit_lines;25012502my$age=time-$co{'committer_epoch'};2503$co{'age'} =$age;2504$co{'age_string'} = age_string($age);2505my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =gmtime($co{'committer_epoch'});2506if($age>60*60*24*7*2) {2507$co{'age_string_date'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2508$co{'age_string_age'} =$co{'age_string'};2509}else{2510$co{'age_string_date'} =$co{'age_string'};2511$co{'age_string_age'} =sprintf"%4i-%02u-%02i",1900+$year,$mon+1,$mday;2512}2513return%co;2514}25152516sub parse_commit {2517my($commit_id) =@_;2518my%co;25192520local$/="\0";25212522open my$fd,"-|", git_cmd(),"rev-list",2523"--parents",2524"--header",2525"--max-count=1",2526$commit_id,2527"--",2528or die_error(500,"Open git-rev-list failed");2529%co= parse_commit_text(<$fd>,1);2530close$fd;25312532return%co;2533}25342535sub parse_commits {2536my($commit_id,$maxcount,$skip,$filename,@args) =@_;2537my@cos;25382539$maxcount||=1;2540$skip||=0;25412542local$/="\0";25432544open my$fd,"-|", git_cmd(),"rev-list",2545"--header",2546@args,2547("--max-count=".$maxcount),2548("--skip=".$skip),2549@extra_options,2550$commit_id,2551"--",2552($filename? ($filename) : ())2553or die_error(500,"Open git-rev-list failed");2554while(my$line= <$fd>) {2555my%co= parse_commit_text($line);2556push@cos, \%co;2557}2558close$fd;25592560returnwantarray?@cos: \@cos;2561}25622563# parse line of git-diff-tree "raw" output2564sub parse_difftree_raw_line {2565my$line=shift;2566my%res;25672568# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'2569# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'2570if($line=~m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {2571$res{'from_mode'} =$1;2572$res{'to_mode'} =$2;2573$res{'from_id'} =$3;2574$res{'to_id'} =$4;2575$res{'status'} =$5;2576$res{'similarity'} =$6;2577if($res{'status'}eq'R'||$res{'status'}eq'C') {# renamed or copied2578($res{'from_file'},$res{'to_file'}) =map{ unquote($_) }split("\t",$7);2579}else{2580$res{'from_file'} =$res{'to_file'} =$res{'file'} = unquote($7);2581}2582}2583# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'2584# combined diff (for merge commit)2585elsif($line=~s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {2586$res{'nparents'} =length($1);2587$res{'from_mode'} = [split(' ',$2) ];2588$res{'to_mode'} =pop@{$res{'from_mode'}};2589$res{'from_id'} = [split(' ',$3) ];2590$res{'to_id'} =pop@{$res{'from_id'}};2591$res{'status'} = [split('',$4) ];2592$res{'to_file'} = unquote($5);2593}2594# 'c512b523472485aef4fff9e57b229d9d243c967f'2595elsif($line=~m/^([0-9a-fA-F]{40})$/) {2596$res{'commit'} =$1;2597}25982599returnwantarray?%res: \%res;2600}26012602# wrapper: return parsed line of git-diff-tree "raw" output2603# (the argument might be raw line, or parsed info)2604sub parsed_difftree_line {2605my$line_or_ref=shift;26062607if(ref($line_or_ref)eq"HASH") {2608# pre-parsed (or generated by hand)2609return$line_or_ref;2610}else{2611return parse_difftree_raw_line($line_or_ref);2612}2613}26142615# parse line of git-ls-tree output2616sub parse_ls_tree_line ($;%) {2617my$line=shift;2618my%opts=@_;2619my%res;26202621#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'2622$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;26232624$res{'mode'} =$1;2625$res{'type'} =$2;2626$res{'hash'} =$3;2627if($opts{'-z'}) {2628$res{'name'} =$4;2629}else{2630$res{'name'} = unquote($4);2631}26322633returnwantarray?%res: \%res;2634}26352636# generates _two_ hashes, references to which are passed as 2 and 3 argument2637sub parse_from_to_diffinfo {2638my($diffinfo,$from,$to,@parents) =@_;26392640if($diffinfo->{'nparents'}) {2641# combined diff2642$from->{'file'} = [];2643$from->{'href'} = [];2644 fill_from_file_info($diffinfo,@parents)2645unlessexists$diffinfo->{'from_file'};2646for(my$i=0;$i<$diffinfo->{'nparents'};$i++) {2647$from->{'file'}[$i] =2648defined$diffinfo->{'from_file'}[$i] ?2649$diffinfo->{'from_file'}[$i] :2650$diffinfo->{'to_file'};2651if($diffinfo->{'status'}[$i]ne"A") {# not new (added) file2652$from->{'href'}[$i] = href(action=>"blob",2653 hash_base=>$parents[$i],2654 hash=>$diffinfo->{'from_id'}[$i],2655 file_name=>$from->{'file'}[$i]);2656}else{2657$from->{'href'}[$i] =undef;2658}2659}2660}else{2661# ordinary (not combined) diff2662$from->{'file'} =$diffinfo->{'from_file'};2663if($diffinfo->{'status'}ne"A") {# not new (added) file2664$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,2665 hash=>$diffinfo->{'from_id'},2666 file_name=>$from->{'file'});2667}else{2668delete$from->{'href'};2669}2670}26712672$to->{'file'} =$diffinfo->{'to_file'};2673if(!is_deleted($diffinfo)) {# file exists in result2674$to->{'href'} = href(action=>"blob", hash_base=>$hash,2675 hash=>$diffinfo->{'to_id'},2676 file_name=>$to->{'file'});2677}else{2678delete$to->{'href'};2679}2680}26812682## ......................................................................2683## parse to array of hashes functions26842685sub git_get_heads_list {2686my$limit=shift;2687my@headslist;26882689open my$fd,'-|', git_cmd(),'for-each-ref',2690($limit?'--count='.($limit+1) : ()),'--sort=-committerdate',2691'--format=%(objectname) %(refname) %(subject)%00%(committer)',2692'refs/heads'2693orreturn;2694while(my$line= <$fd>) {2695my%ref_item;26962697chomp$line;2698my($refinfo,$committerinfo) =split(/\0/,$line);2699my($hash,$name,$title) =split(' ',$refinfo,3);2700my($committer,$epoch,$tz) =2701($committerinfo=~/^(.*) ([0-9]+) (.*)$/);2702$ref_item{'fullname'} =$name;2703$name=~s!^refs/heads/!!;27042705$ref_item{'name'} =$name;2706$ref_item{'id'} =$hash;2707$ref_item{'title'} =$title||'(no commit message)';2708$ref_item{'epoch'} =$epoch;2709if($epoch) {2710$ref_item{'age'} = age_string(time-$ref_item{'epoch'});2711}else{2712$ref_item{'age'} ="unknown";2713}27142715push@headslist, \%ref_item;2716}2717close$fd;27182719returnwantarray?@headslist: \@headslist;2720}27212722sub git_get_tags_list {2723my$limit=shift;2724my@tagslist;27252726open my$fd,'-|', git_cmd(),'for-each-ref',2727($limit?'--count='.($limit+1) : ()),'--sort=-creatordate',2728'--format=%(objectname) %(objecttype) %(refname) '.2729'%(*objectname) %(*objecttype) %(subject)%00%(creator)',2730'refs/tags'2731orreturn;2732while(my$line= <$fd>) {2733my%ref_item;27342735chomp$line;2736my($refinfo,$creatorinfo) =split(/\0/,$line);2737my($id,$type,$name,$refid,$reftype,$title) =split(' ',$refinfo,6);2738my($creator,$epoch,$tz) =2739($creatorinfo=~/^(.*) ([0-9]+) (.*)$/);2740$ref_item{'fullname'} =$name;2741$name=~s!^refs/tags/!!;27422743$ref_item{'type'} =$type;2744$ref_item{'id'} =$id;2745$ref_item{'name'} =$name;2746if($typeeq"tag") {2747$ref_item{'subject'} =$title;2748$ref_item{'reftype'} =$reftype;2749$ref_item{'refid'} =$refid;2750}else{2751$ref_item{'reftype'} =$type;2752$ref_item{'refid'} =$id;2753}27542755if($typeeq"tag"||$typeeq"commit") {2756$ref_item{'epoch'} =$epoch;2757if($epoch) {2758$ref_item{'age'} = age_string(time-$ref_item{'epoch'});2759}else{2760$ref_item{'age'} ="unknown";2761}2762}27632764push@tagslist, \%ref_item;2765}2766close$fd;27672768returnwantarray?@tagslist: \@tagslist;2769}27702771## ----------------------------------------------------------------------2772## filesystem-related functions27732774sub get_file_owner {2775my$path=shift;27762777my($dev,$ino,$mode,$nlink,$st_uid,$st_gid,$rdev,$size) =stat($path);2778my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) =getpwuid($st_uid);2779if(!defined$gcos) {2780returnundef;2781}2782my$owner=$gcos;2783$owner=~s/[,;].*$//;2784return to_utf8($owner);2785}27862787# assume that file exists2788sub insert_file {2789my$filename=shift;27902791open my$fd,'<',$filename;2792print map(to_utf8, <$fd>);2793close$fd;2794}27952796## ......................................................................2797## mimetype related functions27982799sub mimetype_guess_file {2800my$filename=shift;2801my$mimemap=shift;2802-r $mimemaporreturnundef;28032804my%mimemap;2805open(MIME,$mimemap)orreturnundef;2806while(<MIME>) {2807next ifm/^#/;# skip comments2808my($mime,$exts) =split(/\t+/);2809if(defined$exts) {2810my@exts=split(/\s+/,$exts);2811foreachmy$ext(@exts) {2812$mimemap{$ext} =$mime;2813}2814}2815}2816close(MIME);28172818$filename=~/\.([^.]*)$/;2819return$mimemap{$1};2820}28212822sub mimetype_guess {2823my$filename=shift;2824my$mime;2825$filename=~/\./orreturnundef;28262827if($mimetypes_file) {2828my$file=$mimetypes_file;2829if($file!~m!^/!) {# if it is relative path2830# it is relative to project2831$file="$projectroot/$project/$file";2832}2833$mime= mimetype_guess_file($filename,$file);2834}2835$mime||= mimetype_guess_file($filename,'/etc/mime.types');2836return$mime;2837}28382839sub blob_mimetype {2840my$fd=shift;2841my$filename=shift;28422843if($filename) {2844my$mime= mimetype_guess($filename);2845$mimeandreturn$mime;2846}28472848# just in case2849return$default_blob_plain_mimetypeunless$fd;28502851if(-T $fd) {2852return'text/plain';2853}elsif(!$filename) {2854return'application/octet-stream';2855}elsif($filename=~m/\.png$/i) {2856return'image/png';2857}elsif($filename=~m/\.gif$/i) {2858return'image/gif';2859}elsif($filename=~m/\.jpe?g$/i) {2860return'image/jpeg';2861}else{2862return'application/octet-stream';2863}2864}28652866sub blob_contenttype {2867my($fd,$file_name,$type) =@_;28682869$type||= blob_mimetype($fd,$file_name);2870if($typeeq'text/plain'&&defined$default_text_plain_charset) {2871$type.="; charset=$default_text_plain_charset";2872}28732874return$type;2875}28762877## ======================================================================2878## functions printing HTML: header, footer, error page28792880sub git_header_html {2881my$status=shift||"200 OK";2882my$expires=shift;28832884my$title="$site_name";2885if(defined$project) {2886$title.=" - ". to_utf8($project);2887if(defined$action) {2888$title.="/$action";2889if(defined$file_name) {2890$title.=" - ". esc_path($file_name);2891if($actioneq"tree"&&$file_name!~ m|/$|) {2892$title.="/";2893}2894}2895}2896}2897my$content_type;2898# require explicit support from the UA if we are to send the page as2899# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.2900# we have to do this because MSIE sometimes globs '*/*', pretending to2901# support xhtml+xml but choking when it gets what it asked for.2902if(defined$cgi->http('HTTP_ACCEPT') &&2903$cgi->http('HTTP_ACCEPT') =~m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&2904$cgi->Accept('application/xhtml+xml') !=0) {2905$content_type='application/xhtml+xml';2906}else{2907$content_type='text/html';2908}2909print$cgi->header(-type=>$content_type, -charset =>'utf-8',2910-status=>$status, -expires =>$expires);2911my$mod_perl_version=$ENV{'MOD_PERL'} ?"$ENV{'MOD_PERL'}":'';2912print<<EOF;2913<?xml version="1.0" encoding="utf-8"?>2914<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">2915<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">2916<!-- git web interface version$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->2917<!-- git core binaries version$git_version-->2918<head>2919<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>2920<meta name="generator" content="gitweb/$versiongit/$git_version$mod_perl_version"/>2921<meta name="robots" content="index, nofollow"/>2922<title>$title</title>2923EOF2924# print out each stylesheet that exist2925if(defined$stylesheet) {2926#provides backwards capability for those people who define style sheet in a config file2927print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";2928}else{2929foreachmy$stylesheet(@stylesheets) {2930next unless$stylesheet;2931print'<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";2932}2933}2934if(defined$project) {2935my%href_params= get_feed_info();2936if(!exists$href_params{'-title'}) {2937$href_params{'-title'} ='log';2938}29392940foreachmy$formatqw(RSS Atom){2941my$type=lc($format);2942my%link_attr= (2943'-rel'=>'alternate',2944'-title'=>"$project-$href_params{'-title'} -$formatfeed",2945'-type'=>"application/$type+xml"2946);29472948$href_params{'action'} =$type;2949$link_attr{'-href'} = href(%href_params);2950print"<link ".2951"rel=\"$link_attr{'-rel'}\"".2952"title=\"$link_attr{'-title'}\"".2953"href=\"$link_attr{'-href'}\"".2954"type=\"$link_attr{'-type'}\"".2955"/>\n";29562957$href_params{'extra_options'} ='--no-merges';2958$link_attr{'-href'} = href(%href_params);2959$link_attr{'-title'} .=' (no merges)';2960print"<link ".2961"rel=\"$link_attr{'-rel'}\"".2962"title=\"$link_attr{'-title'}\"".2963"href=\"$link_attr{'-href'}\"".2964"type=\"$link_attr{'-type'}\"".2965"/>\n";2966}29672968}else{2969printf('<link rel="alternate" title="%sprojects list" '.2970'href="%s" type="text/plain; charset=utf-8" />'."\n",2971$site_name, href(project=>undef, action=>"project_index"));2972printf('<link rel="alternate" title="%sprojects feeds" '.2973'href="%s" type="text/x-opml" />'."\n",2974$site_name, href(project=>undef, action=>"opml"));2975}2976if(defined$favicon) {2977printqq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);2978}29792980print"</head>\n".2981"<body>\n";29822983if(-f $site_header) {2984 insert_file($site_header);2985}29862987print"<div class=\"page_header\">\n".2988$cgi->a({-href => esc_url($logo_url),2989-title =>$logo_label},2990qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));2991print$cgi->a({-href => esc_url($home_link)},$home_link_str) ." / ";2992if(defined$project) {2993print$cgi->a({-href => href(action=>"summary")}, esc_html($project));2994if(defined$action) {2995print" /$action";2996}2997print"\n";2998}2999print"</div>\n";30003001my$have_search= gitweb_check_feature('search');3002if(defined$project&&$have_search) {3003if(!defined$searchtext) {3004$searchtext="";3005}3006my$search_hash;3007if(defined$hash_base) {3008$search_hash=$hash_base;3009}elsif(defined$hash) {3010$search_hash=$hash;3011}else{3012$search_hash="HEAD";3013}3014my$action=$my_uri;3015my$use_pathinfo= gitweb_check_feature('pathinfo');3016if($use_pathinfo) {3017$action.="/".esc_url($project);3018}3019print$cgi->startform(-method=>"get", -action =>$action) .3020"<div class=\"search\">\n".3021(!$use_pathinfo&&3022$cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) ."\n") .3023$cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) ."\n".3024$cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) ."\n".3025$cgi->popup_menu(-name =>'st', -default=>'commit',3026-values=> ['commit','grep','author','committer','pickaxe']) .3027$cgi->sup($cgi->a({-href => href(action=>"search_help")},"?")) .3028" search:\n",3029$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".3030"<span title=\"Extended regular expression\">".3031$cgi->checkbox(-name =>'sr', -value =>1, -label =>'re',3032-checked =>$search_use_regexp) .3033"</span>".3034"</div>".3035$cgi->end_form() ."\n";3036}3037}30383039sub git_footer_html {3040my$feed_class='rss_logo';30413042print"<div class=\"page_footer\">\n";3043if(defined$project) {3044my$descr= git_get_project_description($project);3045if(defined$descr) {3046print"<div class=\"page_footer_text\">". esc_html($descr) ."</div>\n";3047}30483049my%href_params= get_feed_info();3050if(!%href_params) {3051$feed_class.=' generic';3052}3053$href_params{'-title'} ||='log';30543055foreachmy$formatqw(RSS Atom){3056$href_params{'action'} =lc($format);3057print$cgi->a({-href => href(%href_params),3058-title =>"$href_params{'-title'}$formatfeed",3059-class=>$feed_class},$format)."\n";3060}30613062}else{3063print$cgi->a({-href => href(project=>undef, action=>"opml"),3064-class=>$feed_class},"OPML") ." ";3065print$cgi->a({-href => href(project=>undef, action=>"project_index"),3066-class=>$feed_class},"TXT") ."\n";3067}3068print"</div>\n";# class="page_footer"30693070if(-f $site_footer) {3071 insert_file($site_footer);3072}30733074print"</body>\n".3075"</html>";3076}30773078# die_error(<http_status_code>, <error_message>)3079# Example: die_error(404, 'Hash not found')3080# By convention, use the following status codes (as defined in RFC 2616):3081# 400: Invalid or missing CGI parameters, or3082# requested object exists but has wrong type.3083# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on3084# this server or project.3085# 404: Requested object/revision/project doesn't exist.3086# 500: The server isn't configured properly, or3087# an internal error occurred (e.g. failed assertions caused by bugs), or3088# an unknown error occurred (e.g. the git binary died unexpectedly).3089sub die_error {3090my$status=shift||500;3091my$error=shift||"Internal server error";30923093my%http_responses= (400=>'400 Bad Request',3094403=>'403 Forbidden',3095404=>'404 Not Found',3096500=>'500 Internal Server Error');3097 git_header_html($http_responses{$status});3098print<<EOF;3099<div class="page_body">3100<br /><br />3101$status-$error3102<br />3103</div>3104EOF3105 git_footer_html();3106exit;3107}31083109## ----------------------------------------------------------------------3110## functions printing or outputting HTML: navigation31113112sub git_print_page_nav {3113my($current,$suppress,$head,$treehead,$treebase,$extra) =@_;3114$extra=''if!defined$extra;# pager or formats31153116my@navs=qw(summary shortlog log commit commitdiff tree);3117if($suppress) {3118@navs=grep{$_ne$suppress}@navs;3119}31203121my%arg=map{$_=> {action=>$_} }@navs;3122if(defined$head) {3123for(qw(commit commitdiff)) {3124$arg{$_}{'hash'} =$head;3125}3126if($current=~m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {3127for(qw(shortlog log)) {3128$arg{$_}{'hash'} =$head;3129}3130}3131}31323133$arg{'tree'}{'hash'} =$treeheadifdefined$treehead;3134$arg{'tree'}{'hash_base'} =$treebaseifdefined$treebase;31353136my@actions= gitweb_get_feature('actions');3137my%repl= (3138'%'=>'%',3139'n'=>$project,# project name3140'f'=>$git_dir,# project path within filesystem3141'h'=>$treehead||'',# current hash ('h' parameter)3142'b'=>$treebase||'',# hash base ('hb' parameter)3143);3144while(@actions) {3145my($label,$link,$pos) =splice(@actions,0,3);3146# insert3147@navs=map{$_eq$pos? ($_,$label) :$_}@navs;3148# munch munch3149$link=~s/%([%nfhb])/$repl{$1}/g;3150$arg{$label}{'_href'} =$link;3151}31523153print"<div class=\"page_nav\">\n".3154(join" | ",3155map{$_eq$current?3156$_:$cgi->a({-href => ($arg{$_}{_href} ?$arg{$_}{_href} : href(%{$arg{$_}}))},"$_")3157}@navs);3158print"<br/>\n$extra<br/>\n".3159"</div>\n";3160}31613162sub format_paging_nav {3163my($action,$hash,$head,$page,$has_next_link) =@_;3164my$paging_nav;316531663167if($hashne$head||$page) {3168$paging_nav.=$cgi->a({-href => href(action=>$action)},"HEAD");3169}else{3170$paging_nav.="HEAD";3171}31723173if($page>0) {3174$paging_nav.=" ⋅ ".3175$cgi->a({-href => href(-replay=>1, page=>$page-1),3176-accesskey =>"p", -title =>"Alt-p"},"prev");3177}else{3178$paging_nav.=" ⋅ prev";3179}31803181if($has_next_link) {3182$paging_nav.=" ⋅ ".3183$cgi->a({-href => href(-replay=>1, page=>$page+1),3184-accesskey =>"n", -title =>"Alt-n"},"next");3185}else{3186$paging_nav.=" ⋅ next";3187}31883189return$paging_nav;3190}31913192## ......................................................................3193## functions printing or outputting HTML: div31943195sub git_print_header_div {3196my($action,$title,$hash,$hash_base) =@_;3197my%args= ();31983199$args{'action'} =$action;3200$args{'hash'} =$hashif$hash;3201$args{'hash_base'} =$hash_baseif$hash_base;32023203print"<div class=\"header\">\n".3204$cgi->a({-href => href(%args), -class=>"title"},3205$title?$title:$action) .3206"\n</div>\n";3207}32083209#sub git_print_authorship (\%) {3210sub git_print_authorship {3211my$co=shift;32123213my%ad= parse_date($co->{'author_epoch'},$co->{'author_tz'});3214print"<div class=\"author_date\">".3215 esc_html($co->{'author_name'}) .3216" [$ad{'rfc2822'}";3217if($ad{'hour_local'} <6) {3218printf(" (<span class=\"atnight\">%02d:%02d</span>%s)",3219$ad{'hour_local'},$ad{'minute_local'},$ad{'tz_local'});3220}else{3221printf(" (%02d:%02d%s)",3222$ad{'hour_local'},$ad{'minute_local'},$ad{'tz_local'});3223}3224print"]</div>\n";3225}32263227sub git_print_page_path {3228my$name=shift;3229my$type=shift;3230my$hb=shift;323132323233print"<div class=\"page_path\">";3234print$cgi->a({-href => href(action=>"tree", hash_base=>$hb),3235-title =>'tree root'}, to_utf8("[$project]"));3236print" / ";3237if(defined$name) {3238my@dirname=split'/',$name;3239my$basename=pop@dirname;3240my$fullname='';32413242foreachmy$dir(@dirname) {3243$fullname.= ($fullname?'/':'') .$dir;3244print$cgi->a({-href => href(action=>"tree", file_name=>$fullname,3245 hash_base=>$hb),3246-title =>$fullname}, esc_path($dir));3247print" / ";3248}3249if(defined$type&&$typeeq'blob') {3250print$cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,3251 hash_base=>$hb),3252-title =>$name}, esc_path($basename));3253}elsif(defined$type&&$typeeq'tree') {3254print$cgi->a({-href => href(action=>"tree", file_name=>$file_name,3255 hash_base=>$hb),3256-title =>$name}, esc_path($basename));3257print" / ";3258}else{3259print esc_path($basename);3260}3261}3262print"<br/></div>\n";3263}32643265# sub git_print_log (\@;%) {3266sub git_print_log ($;%) {3267my$log=shift;3268my%opts=@_;32693270if($opts{'-remove_title'}) {3271# remove title, i.e. first line of log3272shift@$log;3273}3274# remove leading empty lines3275while(defined$log->[0] &&$log->[0]eq"") {3276shift@$log;3277}32783279# print log3280my$signoff=0;3281my$empty=0;3282foreachmy$line(@$log) {3283if($line=~m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {3284$signoff=1;3285$empty=0;3286if(!$opts{'-remove_signoff'}) {3287print"<span class=\"signoff\">". esc_html($line) ."</span><br/>\n";3288next;3289}else{3290# remove signoff lines3291next;3292}3293}else{3294$signoff=0;3295}32963297# print only one empty line3298# do not print empty line after signoff3299if($lineeq"") {3300next if($empty||$signoff);3301$empty=1;3302}else{3303$empty=0;3304}33053306print format_log_line_html($line) ."<br/>\n";3307}33083309if($opts{'-final_empty_line'}) {3310# end with single empty line3311print"<br/>\n"unless$empty;3312}3313}33143315# return link target (what link points to)3316sub git_get_link_target {3317my$hash=shift;3318my$link_target;33193320# read link3321open my$fd,"-|", git_cmd(),"cat-file","blob",$hash3322orreturn;3323{3324local$/;3325$link_target= <$fd>;3326}3327close$fd3328orreturn;33293330return$link_target;3331}33323333# given link target, and the directory (basedir) the link is in,3334# return target of link relative to top directory (top tree);3335# return undef if it is not possible (including absolute links).3336sub normalize_link_target {3337my($link_target,$basedir,$hash_base) =@_;33383339# we can normalize symlink target only if $hash_base is provided3340return unless$hash_base;33413342# absolute symlinks (beginning with '/') cannot be normalized3343return if(substr($link_target,0,1)eq'/');33443345# normalize link target to path from top (root) tree (dir)3346my$path;3347if($basedir) {3348$path=$basedir.'/'.$link_target;3349}else{3350# we are in top (root) tree (dir)3351$path=$link_target;3352}33533354# remove //, /./, and /../3355my@path_parts;3356foreachmy$part(split('/',$path)) {3357# discard '.' and ''3358next if(!$part||$parteq'.');3359# handle '..'3360if($parteq'..') {3361if(@path_parts) {3362pop@path_parts;3363}else{3364# link leads outside repository (outside top dir)3365return;3366}3367}else{3368push@path_parts,$part;3369}3370}3371$path=join('/',@path_parts);33723373return$path;3374}33753376# print tree entry (row of git_tree), but without encompassing <tr> element3377sub git_print_tree_entry {3378my($t,$basedir,$hash_base,$have_blame) =@_;33793380my%base_key= ();3381$base_key{'hash_base'} =$hash_baseifdefined$hash_base;33823383# The format of a table row is: mode list link. Where mode is3384# the mode of the entry, list is the name of the entry, an href,3385# and link is the action links of the entry.33863387print"<td class=\"mode\">". mode_str($t->{'mode'}) ."</td>\n";3388if($t->{'type'}eq"blob") {3389print"<td class=\"list\">".3390$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3391 file_name=>"$basedir$t->{'name'}",%base_key),3392-class=>"list"}, esc_path($t->{'name'}));3393if(S_ISLNK(oct$t->{'mode'})) {3394my$link_target= git_get_link_target($t->{'hash'});3395if($link_target) {3396my$norm_target= normalize_link_target($link_target,$basedir,$hash_base);3397if(defined$norm_target) {3398print" -> ".3399$cgi->a({-href => href(action=>"object", hash_base=>$hash_base,3400 file_name=>$norm_target),3401-title =>$norm_target}, esc_path($link_target));3402}else{3403print" -> ". esc_path($link_target);3404}3405}3406}3407print"</td>\n";3408print"<td class=\"link\">";3409print$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},3410 file_name=>"$basedir$t->{'name'}",%base_key)},3411"blob");3412if($have_blame) {3413print" | ".3414$cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},3415 file_name=>"$basedir$t->{'name'}",%base_key)},3416"blame");3417}3418if(defined$hash_base) {3419print" | ".3420$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3421 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},3422"history");3423}3424print" | ".3425$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,3426 file_name=>"$basedir$t->{'name'}")},3427"raw");3428print"</td>\n";34293430}elsif($t->{'type'}eq"tree") {3431print"<td class=\"list\">";3432print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3433 file_name=>"$basedir$t->{'name'}",%base_key)},3434 esc_path($t->{'name'}));3435print"</td>\n";3436print"<td class=\"link\">";3437print$cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},3438 file_name=>"$basedir$t->{'name'}",%base_key)},3439"tree");3440if(defined$hash_base) {3441print" | ".3442$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,3443 file_name=>"$basedir$t->{'name'}")},3444"history");3445}3446print"</td>\n";3447}else{3448# unknown object: we can only present history for it3449# (this includes 'commit' object, i.e. submodule support)3450print"<td class=\"list\">".3451 esc_path($t->{'name'}) .3452"</td>\n";3453print"<td class=\"link\">";3454if(defined$hash_base) {3455print$cgi->a({-href => href(action=>"history",3456 hash_base=>$hash_base,3457 file_name=>"$basedir$t->{'name'}")},3458"history");3459}3460print"</td>\n";3461}3462}34633464## ......................................................................3465## functions printing large fragments of HTML34663467# get pre-image filenames for merge (combined) diff3468sub fill_from_file_info {3469my($diff,@parents) =@_;34703471$diff->{'from_file'} = [ ];3472$diff->{'from_file'}[$diff->{'nparents'} -1] =undef;3473for(my$i=0;$i<$diff->{'nparents'};$i++) {3474if($diff->{'status'}[$i]eq'R'||3475$diff->{'status'}[$i]eq'C') {3476$diff->{'from_file'}[$i] =3477 git_get_path_by_hash($parents[$i],$diff->{'from_id'}[$i]);3478}3479}34803481return$diff;3482}34833484# is current raw difftree line of file deletion3485sub is_deleted {3486my$diffinfo=shift;34873488return$diffinfo->{'to_id'}eq('0' x 40);3489}34903491# does patch correspond to [previous] difftree raw line3492# $diffinfo - hashref of parsed raw diff format3493# $patchinfo - hashref of parsed patch diff format3494# (the same keys as in $diffinfo)3495sub is_patch_split {3496my($diffinfo,$patchinfo) =@_;34973498returndefined$diffinfo&&defined$patchinfo3499&&$diffinfo->{'to_file'}eq$patchinfo->{'to_file'};3500}350135023503sub git_difftree_body {3504my($difftree,$hash,@parents) =@_;3505my($parent) =$parents[0];3506my$have_blame= gitweb_check_feature('blame');3507print"<div class=\"list_head\">\n";3508if($#{$difftree} >10) {3509print(($#{$difftree} +1) ." files changed:\n");3510}3511print"</div>\n";35123513print"<table class=\"".3514(@parents>1?"combined ":"") .3515"diff_tree\">\n";35163517# header only for combined diff in 'commitdiff' view3518my$has_header=@$difftree&&@parents>1&&$actioneq'commitdiff';3519if($has_header) {3520# table header3521print"<thead><tr>\n".3522"<th></th><th></th>\n";# filename, patchN link3523for(my$i=0;$i<@parents;$i++) {3524my$par=$parents[$i];3525print"<th>".3526$cgi->a({-href => href(action=>"commitdiff",3527 hash=>$hash, hash_parent=>$par),3528-title =>'commitdiff to parent number '.3529($i+1) .': '.substr($par,0,7)},3530$i+1) .3531" </th>\n";3532}3533print"</tr></thead>\n<tbody>\n";3534}35353536my$alternate=1;3537my$patchno=0;3538foreachmy$line(@{$difftree}) {3539my$diff= parsed_difftree_line($line);35403541if($alternate) {3542print"<tr class=\"dark\">\n";3543}else{3544print"<tr class=\"light\">\n";3545}3546$alternate^=1;35473548if(exists$diff->{'nparents'}) {# combined diff35493550 fill_from_file_info($diff,@parents)3551unlessexists$diff->{'from_file'};35523553if(!is_deleted($diff)) {3554# file exists in the result (child) commit3555print"<td>".3556$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3557 file_name=>$diff->{'to_file'},3558 hash_base=>$hash),3559-class=>"list"}, esc_path($diff->{'to_file'})) .3560"</td>\n";3561}else{3562print"<td>".3563 esc_path($diff->{'to_file'}) .3564"</td>\n";3565}35663567if($actioneq'commitdiff') {3568# link to patch3569$patchno++;3570print"<td class=\"link\">".3571$cgi->a({-href =>"#patch$patchno"},"patch") .3572" | ".3573"</td>\n";3574}35753576my$has_history=0;3577my$not_deleted=0;3578for(my$i=0;$i<$diff->{'nparents'};$i++) {3579my$hash_parent=$parents[$i];3580my$from_hash=$diff->{'from_id'}[$i];3581my$from_path=$diff->{'from_file'}[$i];3582my$status=$diff->{'status'}[$i];35833584$has_history||= ($statusne'A');3585$not_deleted||= ($statusne'D');35863587if($statuseq'A') {3588print"<td class=\"link\"align=\"right\"> | </td>\n";3589}elsif($statuseq'D') {3590print"<td class=\"link\">".3591$cgi->a({-href => href(action=>"blob",3592 hash_base=>$hash,3593 hash=>$from_hash,3594 file_name=>$from_path)},3595"blob". ($i+1)) .3596" | </td>\n";3597}else{3598if($diff->{'to_id'}eq$from_hash) {3599print"<td class=\"link nochange\">";3600}else{3601print"<td class=\"link\">";3602}3603print$cgi->a({-href => href(action=>"blobdiff",3604 hash=>$diff->{'to_id'},3605 hash_parent=>$from_hash,3606 hash_base=>$hash,3607 hash_parent_base=>$hash_parent,3608 file_name=>$diff->{'to_file'},3609 file_parent=>$from_path)},3610"diff". ($i+1)) .3611" | </td>\n";3612}3613}36143615print"<td class=\"link\">";3616if($not_deleted) {3617print$cgi->a({-href => href(action=>"blob",3618 hash=>$diff->{'to_id'},3619 file_name=>$diff->{'to_file'},3620 hash_base=>$hash)},3621"blob");3622print" | "if($has_history);3623}3624if($has_history) {3625print$cgi->a({-href => href(action=>"history",3626 file_name=>$diff->{'to_file'},3627 hash_base=>$hash)},3628"history");3629}3630print"</td>\n";36313632print"</tr>\n";3633next;# instead of 'else' clause, to avoid extra indent3634}3635# else ordinary diff36363637my($to_mode_oct,$to_mode_str,$to_file_type);3638my($from_mode_oct,$from_mode_str,$from_file_type);3639if($diff->{'to_mode'}ne('0' x 6)) {3640$to_mode_oct=oct$diff->{'to_mode'};3641if(S_ISREG($to_mode_oct)) {# only for regular file3642$to_mode_str=sprintf("%04o",$to_mode_oct&0777);# permission bits3643}3644$to_file_type= file_type($diff->{'to_mode'});3645}3646if($diff->{'from_mode'}ne('0' x 6)) {3647$from_mode_oct=oct$diff->{'from_mode'};3648if(S_ISREG($to_mode_oct)) {# only for regular file3649$from_mode_str=sprintf("%04o",$from_mode_oct&0777);# permission bits3650}3651$from_file_type= file_type($diff->{'from_mode'});3652}36533654if($diff->{'status'}eq"A") {# created3655my$mode_chng="<span class=\"file_status new\">[new$to_file_type";3656$mode_chng.=" with mode:$to_mode_str"if$to_mode_str;3657$mode_chng.="]</span>";3658print"<td>";3659print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3660 hash_base=>$hash, file_name=>$diff->{'file'}),3661-class=>"list"}, esc_path($diff->{'file'}));3662print"</td>\n";3663print"<td>$mode_chng</td>\n";3664print"<td class=\"link\">";3665if($actioneq'commitdiff') {3666# link to patch3667$patchno++;3668print$cgi->a({-href =>"#patch$patchno"},"patch");3669print" | ";3670}3671print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3672 hash_base=>$hash, file_name=>$diff->{'file'})},3673"blob");3674print"</td>\n";36753676}elsif($diff->{'status'}eq"D") {# deleted3677my$mode_chng="<span class=\"file_status deleted\">[deleted$from_file_type]</span>";3678print"<td>";3679print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},3680 hash_base=>$parent, file_name=>$diff->{'file'}),3681-class=>"list"}, esc_path($diff->{'file'}));3682print"</td>\n";3683print"<td>$mode_chng</td>\n";3684print"<td class=\"link\">";3685if($actioneq'commitdiff') {3686# link to patch3687$patchno++;3688print$cgi->a({-href =>"#patch$patchno"},"patch");3689print" | ";3690}3691print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},3692 hash_base=>$parent, file_name=>$diff->{'file'})},3693"blob") ." | ";3694if($have_blame) {3695print$cgi->a({-href => href(action=>"blame", hash_base=>$parent,3696 file_name=>$diff->{'file'})},3697"blame") ." | ";3698}3699print$cgi->a({-href => href(action=>"history", hash_base=>$parent,3700 file_name=>$diff->{'file'})},3701"history");3702print"</td>\n";37033704}elsif($diff->{'status'}eq"M"||$diff->{'status'}eq"T") {# modified, or type changed3705my$mode_chnge="";3706if($diff->{'from_mode'} !=$diff->{'to_mode'}) {3707$mode_chnge="<span class=\"file_status mode_chnge\">[changed";3708if($from_file_typene$to_file_type) {3709$mode_chnge.=" from$from_file_typeto$to_file_type";3710}3711if(($from_mode_oct&0777) != ($to_mode_oct&0777)) {3712if($from_mode_str&&$to_mode_str) {3713$mode_chnge.=" mode:$from_mode_str->$to_mode_str";3714}elsif($to_mode_str) {3715$mode_chnge.=" mode:$to_mode_str";3716}3717}3718$mode_chnge.="]</span>\n";3719}3720print"<td>";3721print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3722 hash_base=>$hash, file_name=>$diff->{'file'}),3723-class=>"list"}, esc_path($diff->{'file'}));3724print"</td>\n";3725print"<td>$mode_chnge</td>\n";3726print"<td class=\"link\">";3727if($actioneq'commitdiff') {3728# link to patch3729$patchno++;3730print$cgi->a({-href =>"#patch$patchno"},"patch") .3731" | ";3732}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {3733# "commit" view and modified file (not onlu mode changed)3734print$cgi->a({-href => href(action=>"blobdiff",3735 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},3736 hash_base=>$hash, hash_parent_base=>$parent,3737 file_name=>$diff->{'file'})},3738"diff") .3739" | ";3740}3741print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3742 hash_base=>$hash, file_name=>$diff->{'file'})},3743"blob") ." | ";3744if($have_blame) {3745print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,3746 file_name=>$diff->{'file'})},3747"blame") ." | ";3748}3749print$cgi->a({-href => href(action=>"history", hash_base=>$hash,3750 file_name=>$diff->{'file'})},3751"history");3752print"</td>\n";37533754}elsif($diff->{'status'}eq"R"||$diff->{'status'}eq"C") {# renamed or copied3755my%status_name= ('R'=>'moved','C'=>'copied');3756my$nstatus=$status_name{$diff->{'status'}};3757my$mode_chng="";3758if($diff->{'from_mode'} !=$diff->{'to_mode'}) {3759# mode also for directories, so we cannot use $to_mode_str3760$mode_chng=sprintf(", mode:%04o",$to_mode_oct&0777);3761}3762print"<td>".3763$cgi->a({-href => href(action=>"blob", hash_base=>$hash,3764 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),3765-class=>"list"}, esc_path($diff->{'to_file'})) ."</td>\n".3766"<td><span class=\"file_status$nstatus\">[$nstatusfrom ".3767$cgi->a({-href => href(action=>"blob", hash_base=>$parent,3768 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),3769-class=>"list"}, esc_path($diff->{'from_file'})) .3770" with ". (int$diff->{'similarity'}) ."% similarity$mode_chng]</span></td>\n".3771"<td class=\"link\">";3772if($actioneq'commitdiff') {3773# link to patch3774$patchno++;3775print$cgi->a({-href =>"#patch$patchno"},"patch") .3776" | ";3777}elsif($diff->{'to_id'}ne$diff->{'from_id'}) {3778# "commit" view and modified file (not only pure rename or copy)3779print$cgi->a({-href => href(action=>"blobdiff",3780 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},3781 hash_base=>$hash, hash_parent_base=>$parent,3782 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},3783"diff") .3784" | ";3785}3786print$cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},3787 hash_base=>$parent, file_name=>$diff->{'to_file'})},3788"blob") ." | ";3789if($have_blame) {3790print$cgi->a({-href => href(action=>"blame", hash_base=>$hash,3791 file_name=>$diff->{'to_file'})},3792"blame") ." | ";3793}3794print$cgi->a({-href => href(action=>"history", hash_base=>$hash,3795 file_name=>$diff->{'to_file'})},3796"history");3797print"</td>\n";37983799}# we should not encounter Unmerged (U) or Unknown (X) status3800print"</tr>\n";3801}3802print"</tbody>"if$has_header;3803print"</table>\n";3804}38053806sub git_patchset_body {3807my($fd,$difftree,$hash,@hash_parents) =@_;3808my($hash_parent) =$hash_parents[0];38093810my$is_combined= (@hash_parents>1);3811my$patch_idx=0;3812my$patch_number=0;3813my$patch_line;3814my$diffinfo;3815my$to_name;3816my(%from,%to);38173818print"<div class=\"patchset\">\n";38193820# skip to first patch3821while($patch_line= <$fd>) {3822chomp$patch_line;38233824last if($patch_line=~m/^diff /);3825}38263827 PATCH:3828while($patch_line) {38293830# parse "git diff" header line3831if($patch_line=~m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {3832# $1 is from_name, which we do not use3833$to_name= unquote($2);3834$to_name=~s!^b/!!;3835}elsif($patch_line=~m/^diff --(cc|combined) ("?.*"?)$/) {3836# $1 is 'cc' or 'combined', which we do not use3837$to_name= unquote($2);3838}else{3839$to_name=undef;3840}38413842# check if current patch belong to current raw line3843# and parse raw git-diff line if needed3844if(is_patch_split($diffinfo, {'to_file'=>$to_name})) {3845# this is continuation of a split patch3846print"<div class=\"patch cont\">\n";3847}else{3848# advance raw git-diff output if needed3849$patch_idx++ifdefined$diffinfo;38503851# read and prepare patch information3852$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);38533854# compact combined diff output can have some patches skipped3855# find which patch (using pathname of result) we are at now;3856if($is_combined) {3857while($to_namene$diffinfo->{'to_file'}) {3858print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".3859 format_diff_cc_simplified($diffinfo,@hash_parents) .3860"</div>\n";# class="patch"38613862$patch_idx++;3863$patch_number++;38643865last if$patch_idx>$#$difftree;3866$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);3867}3868}38693870# modifies %from, %to hashes3871 parse_from_to_diffinfo($diffinfo, \%from, \%to,@hash_parents);38723873# this is first patch for raw difftree line with $patch_idx index3874# we index @$difftree array from 0, but number patches from 13875print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n";3876}38773878# git diff header3879#assert($patch_line =~ m/^diff /) if DEBUG;3880#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed3881$patch_number++;3882# print "git diff" header3883print format_git_diff_header_line($patch_line,$diffinfo,3884 \%from, \%to);38853886# print extended diff header3887print"<div class=\"diff extended_header\">\n";3888 EXTENDED_HEADER:3889while($patch_line= <$fd>) {3890chomp$patch_line;38913892last EXTENDED_HEADER if($patch_line=~m/^--- |^diff /);38933894print format_extended_diff_header_line($patch_line,$diffinfo,3895 \%from, \%to);3896}3897print"</div>\n";# class="diff extended_header"38983899# from-file/to-file diff header3900if(!$patch_line) {3901print"</div>\n";# class="patch"3902last PATCH;3903}3904next PATCH if($patch_line=~m/^diff /);3905#assert($patch_line =~ m/^---/) if DEBUG;39063907my$last_patch_line=$patch_line;3908$patch_line= <$fd>;3909chomp$patch_line;3910#assert($patch_line =~ m/^\+\+\+/) if DEBUG;39113912print format_diff_from_to_header($last_patch_line,$patch_line,3913$diffinfo, \%from, \%to,3914@hash_parents);39153916# the patch itself3917 LINE:3918while($patch_line= <$fd>) {3919chomp$patch_line;39203921next PATCH if($patch_line=~m/^diff /);39223923print format_diff_line($patch_line, \%from, \%to);3924}39253926}continue{3927print"</div>\n";# class="patch"3928}39293930# for compact combined (--cc) format, with chunk and patch simpliciaction3931# patchset might be empty, but there might be unprocessed raw lines3932for(++$patch_idxif$patch_number>0;3933$patch_idx<@$difftree;3934++$patch_idx) {3935# read and prepare patch information3936$diffinfo= parsed_difftree_line($difftree->[$patch_idx]);39373938# generate anchor for "patch" links in difftree / whatchanged part3939print"<div class=\"patch\"id=\"patch". ($patch_idx+1) ."\">\n".3940 format_diff_cc_simplified($diffinfo,@hash_parents) .3941"</div>\n";# class="patch"39423943$patch_number++;3944}39453946if($patch_number==0) {3947if(@hash_parents>1) {3948print"<div class=\"diff nodifferences\">Trivial merge</div>\n";3949}else{3950print"<div class=\"diff nodifferences\">No differences found</div>\n";3951}3952}39533954print"</div>\n";# class="patchset"3955}39563957# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .39583959# fills project list info (age, description, owner, forks) for each3960# project in the list, removing invalid projects from returned list3961# NOTE: modifies $projlist, but does not remove entries from it3962sub fill_project_list_info {3963my($projlist,$check_forks) =@_;3964my@projects;39653966my$show_ctags= gitweb_check_feature('ctags');3967 PROJECT:3968foreachmy$pr(@$projlist) {3969my(@activity) = git_get_last_activity($pr->{'path'});3970unless(@activity) {3971next PROJECT;3972}3973($pr->{'age'},$pr->{'age_string'}) =@activity;3974if(!defined$pr->{'descr'}) {3975my$descr= git_get_project_description($pr->{'path'}) ||"";3976$descr= to_utf8($descr);3977$pr->{'descr_long'} =$descr;3978$pr->{'descr'} = chop_str($descr,$projects_list_description_width,5);3979}3980if(!defined$pr->{'owner'}) {3981$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") ||"";3982}3983if($check_forks) {3984my$pname=$pr->{'path'};3985if(($pname=~s/\.git$//) &&3986($pname!~/\/$/) &&3987(-d "$projectroot/$pname")) {3988$pr->{'forks'} ="-d$projectroot/$pname";3989}else{3990$pr->{'forks'} =0;3991}3992}3993$show_ctagsand$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});3994push@projects,$pr;3995}39963997return@projects;3998}39994000# print 'sort by' <th> element, generating 'sort by $name' replay link4001# if that order is not selected4002sub print_sort_th {4003my($name,$order,$header) =@_;4004$header||=ucfirst($name);40054006if($ordereq$name) {4007print"<th>$header</th>\n";4008}else{4009print"<th>".4010$cgi->a({-href => href(-replay=>1, order=>$name),4011-class=>"header"},$header) .4012"</th>\n";4013}4014}40154016sub git_project_list_body {4017# actually uses global variable $project4018my($projlist,$order,$from,$to,$extra,$no_header) =@_;40194020my$check_forks= gitweb_check_feature('forks');4021my@projects= fill_project_list_info($projlist,$check_forks);40224023$order||=$default_projects_order;4024$from=0unlessdefined$from;4025$to=$#projectsif(!defined$to||$#projects<$to);40264027my%order_info= (4028 project => { key =>'path', type =>'str'},4029 descr => { key =>'descr_long', type =>'str'},4030 owner => { key =>'owner', type =>'str'},4031 age => { key =>'age', type =>'num'}4032);4033my$oi=$order_info{$order};4034if($oi->{'type'}eq'str') {4035@projects=sort{$a->{$oi->{'key'}}cmp$b->{$oi->{'key'}}}@projects;4036}else{4037@projects=sort{$a->{$oi->{'key'}} <=>$b->{$oi->{'key'}}}@projects;4038}40394040my$show_ctags= gitweb_check_feature('ctags');4041if($show_ctags) {4042my%ctags;4043foreachmy$p(@projects) {4044foreachmy$ct(keys%{$p->{'ctags'}}) {4045$ctags{$ct} +=$p->{'ctags'}->{$ct};4046}4047}4048my$cloud= git_populate_project_tagcloud(\%ctags);4049print git_show_project_tagcloud($cloud,64);4050}40514052print"<table class=\"project_list\">\n";4053unless($no_header) {4054print"<tr>\n";4055if($check_forks) {4056print"<th></th>\n";4057}4058 print_sort_th('project',$order,'Project');4059 print_sort_th('descr',$order,'Description');4060 print_sort_th('owner',$order,'Owner');4061 print_sort_th('age',$order,'Last Change');4062print"<th></th>\n".# for links4063"</tr>\n";4064}4065my$alternate=1;4066my$tagfilter=$cgi->param('by_tag');4067for(my$i=$from;$i<=$to;$i++) {4068my$pr=$projects[$i];40694070next if$tagfilterand$show_ctagsand not grep{lc$_eq lc$tagfilter}keys%{$pr->{'ctags'}};4071next if$searchtextand not$pr->{'path'} =~/$searchtext/4072and not$pr->{'descr_long'} =~/$searchtext/;4073# Weed out forks or non-matching entries of search4074if($check_forks) {4075my$forkbase=$project;$forkbase||='';$forkbase=~ s#\.git$#/#;4076$forkbase="^$forkbase"if$forkbase;4077next ifnot$searchtextand not$tagfilterand$show_ctags4078and$pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe4079}40804081if($alternate) {4082print"<tr class=\"dark\">\n";4083}else{4084print"<tr class=\"light\">\n";4085}4086$alternate^=1;4087if($check_forks) {4088print"<td>";4089if($pr->{'forks'}) {4090print"<!--$pr->{'forks'} -->\n";4091print$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"+");4092}4093print"</td>\n";4094}4095print"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4096-class=>"list"}, esc_html($pr->{'path'})) ."</td>\n".4097"<td>".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),4098-class=>"list", -title =>$pr->{'descr_long'}},4099 esc_html($pr->{'descr'})) ."</td>\n".4100"<td><i>". chop_and_escape_str($pr->{'owner'},15) ."</i></td>\n";4101print"<td class=\"". age_class($pr->{'age'}) ."\">".4102(defined$pr->{'age_string'} ?$pr->{'age_string'} :"No commits") ."</td>\n".4103"<td class=\"link\">".4104$cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")},"summary") ." | ".4105$cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")},"shortlog") ." | ".4106$cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")},"log") ." | ".4107$cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")},"tree") .4108($pr->{'forks'} ?" | ".$cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")},"forks") :'') .4109"</td>\n".4110"</tr>\n";4111}4112if(defined$extra) {4113print"<tr>\n";4114if($check_forks) {4115print"<td></td>\n";4116}4117print"<td colspan=\"5\">$extra</td>\n".4118"</tr>\n";4119}4120print"</table>\n";4121}41224123sub git_shortlog_body {4124# uses global variable $project4125my($commitlist,$from,$to,$refs,$extra) =@_;41264127$from=0unlessdefined$from;4128$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);41294130print"<table class=\"shortlog\">\n";4131my$alternate=1;4132for(my$i=$from;$i<=$to;$i++) {4133my%co= %{$commitlist->[$i]};4134my$commit=$co{'id'};4135my$ref= format_ref_marker($refs,$commit);4136if($alternate) {4137print"<tr class=\"dark\">\n";4138}else{4139print"<tr class=\"light\">\n";4140}4141$alternate^=1;4142my$author= chop_and_escape_str($co{'author_name'},10);4143# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .4144print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4145"<td><i>".$author."</i></td>\n".4146"<td>";4147print format_subject_html($co{'title'},$co{'title_short'},4148 href(action=>"commit", hash=>$commit),$ref);4149print"</td>\n".4150"<td class=\"link\">".4151$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") ." | ".4152$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") ." | ".4153$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree");4154my$snapshot_links= format_snapshot_links($commit);4155if(defined$snapshot_links) {4156print" | ".$snapshot_links;4157}4158print"</td>\n".4159"</tr>\n";4160}4161if(defined$extra) {4162print"<tr>\n".4163"<td colspan=\"4\">$extra</td>\n".4164"</tr>\n";4165}4166print"</table>\n";4167}41684169sub git_history_body {4170# Warning: assumes constant type (blob or tree) during history4171my($commitlist,$from,$to,$refs,$hash_base,$ftype,$extra) =@_;41724173$from=0unlessdefined$from;4174$to=$#{$commitlist}unless(defined$to&&$to<=$#{$commitlist});41754176print"<table class=\"history\">\n";4177my$alternate=1;4178for(my$i=$from;$i<=$to;$i++) {4179my%co= %{$commitlist->[$i]};4180if(!%co) {4181next;4182}4183my$commit=$co{'id'};41844185my$ref= format_ref_marker($refs,$commit);41864187if($alternate) {4188print"<tr class=\"dark\">\n";4189}else{4190print"<tr class=\"light\">\n";4191}4192$alternate^=1;4193# shortlog uses chop_str($co{'author_name'}, 10)4194my$author= chop_and_escape_str($co{'author_name'},15,3);4195print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4196"<td><i>".$author."</i></td>\n".4197"<td>";4198# originally git_history used chop_str($co{'title'}, 50)4199print format_subject_html($co{'title'},$co{'title_short'},4200 href(action=>"commit", hash=>$commit),$ref);4201print"</td>\n".4202"<td class=\"link\">".4203$cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)},$ftype) ." | ".4204$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff");42054206if($ftypeeq'blob') {4207my$blob_current= git_get_hash_by_path($hash_base,$file_name);4208my$blob_parent= git_get_hash_by_path($commit,$file_name);4209if(defined$blob_current&&defined$blob_parent&&4210$blob_currentne$blob_parent) {4211print" | ".4212$cgi->a({-href => href(action=>"blobdiff",4213 hash=>$blob_current, hash_parent=>$blob_parent,4214 hash_base=>$hash_base, hash_parent_base=>$commit,4215 file_name=>$file_name)},4216"diff to current");4217}4218}4219print"</td>\n".4220"</tr>\n";4221}4222if(defined$extra) {4223print"<tr>\n".4224"<td colspan=\"4\">$extra</td>\n".4225"</tr>\n";4226}4227print"</table>\n";4228}42294230sub git_tags_body {4231# uses global variable $project4232my($taglist,$from,$to,$extra) =@_;4233$from=0unlessdefined$from;4234$to=$#{$taglist}if(!defined$to||$#{$taglist} <$to);42354236print"<table class=\"tags\">\n";4237my$alternate=1;4238for(my$i=$from;$i<=$to;$i++) {4239my$entry=$taglist->[$i];4240my%tag=%$entry;4241my$comment=$tag{'subject'};4242my$comment_short;4243if(defined$comment) {4244$comment_short= chop_str($comment,30,5);4245}4246if($alternate) {4247print"<tr class=\"dark\">\n";4248}else{4249print"<tr class=\"light\">\n";4250}4251$alternate^=1;4252if(defined$tag{'age'}) {4253print"<td><i>$tag{'age'}</i></td>\n";4254}else{4255print"<td></td>\n";4256}4257print"<td>".4258$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),4259-class=>"list name"}, esc_html($tag{'name'})) .4260"</td>\n".4261"<td>";4262if(defined$comment) {4263print format_subject_html($comment,$comment_short,4264 href(action=>"tag", hash=>$tag{'id'}));4265}4266print"</td>\n".4267"<td class=\"selflink\">";4268if($tag{'type'}eq"tag") {4269print$cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})},"tag");4270}else{4271print" ";4272}4273print"</td>\n".4274"<td class=\"link\">"." | ".4275$cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})},$tag{'reftype'});4276if($tag{'reftype'}eq"commit") {4277print" | ".$cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})},"shortlog") .4278" | ".$cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})},"log");4279}elsif($tag{'reftype'}eq"blob") {4280print" | ".$cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})},"raw");4281}4282print"</td>\n".4283"</tr>";4284}4285if(defined$extra) {4286print"<tr>\n".4287"<td colspan=\"5\">$extra</td>\n".4288"</tr>\n";4289}4290print"</table>\n";4291}42924293sub git_heads_body {4294# uses global variable $project4295my($headlist,$head,$from,$to,$extra) =@_;4296$from=0unlessdefined$from;4297$to=$#{$headlist}if(!defined$to||$#{$headlist} <$to);42984299print"<table class=\"heads\">\n";4300my$alternate=1;4301for(my$i=$from;$i<=$to;$i++) {4302my$entry=$headlist->[$i];4303my%ref=%$entry;4304my$curr=$ref{'id'}eq$head;4305if($alternate) {4306print"<tr class=\"dark\">\n";4307}else{4308print"<tr class=\"light\">\n";4309}4310$alternate^=1;4311print"<td><i>$ref{'age'}</i></td>\n".4312($curr?"<td class=\"current_head\">":"<td>") .4313$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),4314-class=>"list name"},esc_html($ref{'name'})) .4315"</td>\n".4316"<td class=\"link\">".4317$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})},"shortlog") ." | ".4318$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})},"log") ." | ".4319$cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})},"tree") .4320"</td>\n".4321"</tr>";4322}4323if(defined$extra) {4324print"<tr>\n".4325"<td colspan=\"3\">$extra</td>\n".4326"</tr>\n";4327}4328print"</table>\n";4329}43304331sub git_search_grep_body {4332my($commitlist,$from,$to,$extra) =@_;4333$from=0unlessdefined$from;4334$to=$#{$commitlist}if(!defined$to||$#{$commitlist} <$to);43354336print"<table class=\"commit_search\">\n";4337my$alternate=1;4338for(my$i=$from;$i<=$to;$i++) {4339my%co= %{$commitlist->[$i]};4340if(!%co) {4341next;4342}4343my$commit=$co{'id'};4344if($alternate) {4345print"<tr class=\"dark\">\n";4346}else{4347print"<tr class=\"light\">\n";4348}4349$alternate^=1;4350my$author= chop_and_escape_str($co{'author_name'},15,5);4351print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".4352"<td><i>".$author."</i></td>\n".4353"<td>".4354$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),4355-class=>"list subject"},4356 chop_and_escape_str($co{'title'},50) ."<br/>");4357my$comment=$co{'comment'};4358foreachmy$line(@$comment) {4359if($line=~m/^(.*?)($search_regexp)(.*)$/i) {4360my($lead,$match,$trail) = ($1,$2,$3);4361$match= chop_str($match,70,5,'center');4362my$contextlen=int((80-length($match))/2);4363$contextlen=30if($contextlen>30);4364$lead= chop_str($lead,$contextlen,10,'left');4365$trail= chop_str($trail,$contextlen,10,'right');43664367$lead= esc_html($lead);4368$match= esc_html($match);4369$trail= esc_html($trail);43704371print"$lead<span class=\"match\">$match</span>$trail<br />";4372}4373}4374print"</td>\n".4375"<td class=\"link\">".4376$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .4377" | ".4378$cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})},"commitdiff") .4379" | ".4380$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");4381print"</td>\n".4382"</tr>\n";4383}4384if(defined$extra) {4385print"<tr>\n".4386"<td colspan=\"3\">$extra</td>\n".4387"</tr>\n";4388}4389print"</table>\n";4390}43914392## ======================================================================4393## ======================================================================4394## actions43954396sub git_project_list {4397my$order=$input_params{'order'};4398if(defined$order&&$order!~m/none|project|descr|owner|age/) {4399 die_error(400,"Unknown order parameter");4400}44014402my@list= git_get_projects_list();4403if(!@list) {4404 die_error(404,"No projects found");4405}44064407 git_header_html();4408if(-f $home_text) {4409print"<div class=\"index_include\">\n";4410 insert_file($home_text);4411print"</div>\n";4412}4413print$cgi->startform(-method=>"get") .4414"<p class=\"projsearch\">Search:\n".4415$cgi->textfield(-name =>"s", -value =>$searchtext) ."\n".4416"</p>".4417$cgi->end_form() ."\n";4418 git_project_list_body(\@list,$order);4419 git_footer_html();4420}44214422sub git_forks {4423my$order=$input_params{'order'};4424if(defined$order&&$order!~m/none|project|descr|owner|age/) {4425 die_error(400,"Unknown order parameter");4426}44274428my@list= git_get_projects_list($project);4429if(!@list) {4430 die_error(404,"No forks found");4431}44324433 git_header_html();4434 git_print_page_nav('','');4435 git_print_header_div('summary',"$projectforks");4436 git_project_list_body(\@list,$order);4437 git_footer_html();4438}44394440sub git_project_index {4441my@projects= git_get_projects_list($project);44424443print$cgi->header(4444-type =>'text/plain',4445-charset =>'utf-8',4446-content_disposition =>'inline; filename="index.aux"');44474448foreachmy$pr(@projects) {4449if(!exists$pr->{'owner'}) {4450$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");4451}44524453my($path,$owner) = ($pr->{'path'},$pr->{'owner'});4454# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '4455$path=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4456$owner=~s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X",ord($1))/eg;4457$path=~s/ /\+/g;4458$owner=~s/ /\+/g;44594460print"$path$owner\n";4461}4462}44634464sub git_summary {4465my$descr= git_get_project_description($project) ||"none";4466my%co= parse_commit("HEAD");4467my%cd=%co? parse_date($co{'committer_epoch'},$co{'committer_tz'}) : ();4468my$head=$co{'id'};44694470my$owner= git_get_project_owner($project);44714472my$refs= git_get_references();4473# These get_*_list functions return one more to allow us to see if4474# there are more ...4475my@taglist= git_get_tags_list(16);4476my@headlist= git_get_heads_list(16);4477my@forklist;4478my$check_forks= gitweb_check_feature('forks');44794480if($check_forks) {4481@forklist= git_get_projects_list($project);4482}44834484 git_header_html();4485 git_print_page_nav('summary','',$head);44864487print"<div class=\"title\"> </div>\n";4488print"<table class=\"projects_list\">\n".4489"<tr id=\"metadata_desc\"><td>description</td><td>". esc_html($descr) ."</td></tr>\n".4490"<tr id=\"metadata_owner\"><td>owner</td><td>". esc_html($owner) ."</td></tr>\n";4491if(defined$cd{'rfc2822'}) {4492print"<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";4493}44944495# use per project git URL list in $projectroot/$project/cloneurl4496# or make project git URL from git base URL and project name4497my$url_tag="URL";4498my@url_list= git_get_project_url_list($project);4499@url_list=map{"$_/$project"}@git_base_url_listunless@url_list;4500foreachmy$git_url(@url_list) {4501next unless$git_url;4502print"<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";4503$url_tag="";4504}45054506# Tag cloud4507my$show_ctags= gitweb_check_feature('ctags');4508if($show_ctags) {4509my$ctags= git_get_project_ctags($project);4510my$cloud= git_populate_project_tagcloud($ctags);4511print"<tr id=\"metadata_ctags\"><td>Content tags:<br />";4512print"</td>\n<td>"unless%$ctags;4513print"<form action=\"$show_ctags\"method=\"post\"><input type=\"hidden\"name=\"p\"value=\"$project\"/>Add: <input type=\"text\"name=\"t\"size=\"8\"/></form>";4514print"</td>\n<td>"if%$ctags;4515print git_show_project_tagcloud($cloud,48);4516print"</td></tr>";4517}45184519print"</table>\n";45204521if(-s "$projectroot/$project/README.html") {4522print"<div class=\"title\">readme</div>\n".4523"<div class=\"readme\">\n";4524 insert_file("$projectroot/$project/README.html");4525print"\n</div>\n";# class="readme"4526}45274528# we need to request one more than 16 (0..15) to check if4529# those 16 are all4530my@commitlist=$head? parse_commits($head,17) : ();4531if(@commitlist) {4532 git_print_header_div('shortlog');4533 git_shortlog_body(\@commitlist,0,15,$refs,4534$#commitlist<=15?undef:4535$cgi->a({-href => href(action=>"shortlog")},"..."));4536}45374538if(@taglist) {4539 git_print_header_div('tags');4540 git_tags_body(\@taglist,0,15,4541$#taglist<=15?undef:4542$cgi->a({-href => href(action=>"tags")},"..."));4543}45444545if(@headlist) {4546 git_print_header_div('heads');4547 git_heads_body(\@headlist,$head,0,15,4548$#headlist<=15?undef:4549$cgi->a({-href => href(action=>"heads")},"..."));4550}45514552if(@forklist) {4553 git_print_header_div('forks');4554 git_project_list_body(\@forklist,'age',0,15,4555$#forklist<=15?undef:4556$cgi->a({-href => href(action=>"forks")},"..."),4557'no_header');4558}45594560 git_footer_html();4561}45624563sub git_tag {4564my$head= git_get_head_hash($project);4565 git_header_html();4566 git_print_page_nav('','',$head,undef,$head);4567my%tag= parse_tag($hash);45684569if(!%tag) {4570 die_error(404,"Unknown tag object");4571}45724573 git_print_header_div('commit', esc_html($tag{'name'}),$hash);4574print"<div class=\"title_text\">\n".4575"<table class=\"object_header\">\n".4576"<tr>\n".4577"<td>object</td>\n".4578"<td>".$cgi->a({-class=>"list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},4579$tag{'object'}) ."</td>\n".4580"<td class=\"link\">".$cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},4581$tag{'type'}) ."</td>\n".4582"</tr>\n";4583if(defined($tag{'author'})) {4584my%ad= parse_date($tag{'epoch'},$tag{'tz'});4585print"<tr><td>author</td><td>". esc_html($tag{'author'}) ."</td></tr>\n";4586print"<tr><td></td><td>".$ad{'rfc2822'} .4587sprintf(" (%02d:%02d%s)",$ad{'hour_local'},$ad{'minute_local'},$ad{'tz_local'}) .4588"</td></tr>\n";4589}4590print"</table>\n\n".4591"</div>\n";4592print"<div class=\"page_body\">";4593my$comment=$tag{'comment'};4594foreachmy$line(@$comment) {4595chomp$line;4596print esc_html($line, -nbsp=>1) ."<br/>\n";4597}4598print"</div>\n";4599 git_footer_html();4600}46014602sub git_blame {4603my$fd;4604my$ftype;46054606 gitweb_check_feature('blame')4607or die_error(403,"Blame view not allowed");46084609 die_error(400,"No file name given")unless$file_name;4610$hash_base||= git_get_head_hash($project);4611 die_error(404,"Couldn't find base commit")unless($hash_base);4612my%co= parse_commit($hash_base)4613or die_error(404,"Commit not found");4614if(!defined$hash) {4615$hash= git_get_hash_by_path($hash_base,$file_name,"blob")4616or die_error(404,"Error looking up file");4617}4618$ftype= git_get_type($hash);4619if($ftype!~"blob") {4620 die_error(400,"Object is not a blob");4621}4622open($fd,"-|", git_cmd(),"blame",'-p','--',4623$file_name,$hash_base)4624or die_error(500,"Open git-blame failed");4625 git_header_html();4626my$formats_nav=4627$cgi->a({-href => href(action=>"blob", -replay=>1)},4628"blob") .4629" | ".4630$cgi->a({-href => href(action=>"history", -replay=>1)},4631"history") .4632" | ".4633$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},4634"HEAD");4635 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);4636 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);4637 git_print_page_path($file_name,$ftype,$hash_base);4638my@rev_color= (qw(light2 dark2));4639my$num_colors=scalar(@rev_color);4640my$current_color=0;4641my$last_rev;4642print<<HTML;4643<div class="page_body">4644<table class="blame">4645<tr><th>Commit</th><th>Line</th><th>Data</th></tr>4646HTML4647my%metainfo= ();4648while(1) {4649$_= <$fd>;4650last unlessdefined$_;4651my($full_rev,$orig_lineno,$lineno,$group_size) =4652/^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;4653if(!exists$metainfo{$full_rev}) {4654$metainfo{$full_rev} = {};4655}4656my$meta=$metainfo{$full_rev};4657while(<$fd>) {4658last if(s/^\t//);4659if(/^(\S+) (.*)$/) {4660$meta->{$1} =$2;4661}4662}4663my$data=$_;4664chomp$data;4665my$rev=substr($full_rev,0,8);4666my$author=$meta->{'author'};4667my%date= parse_date($meta->{'author-time'},4668$meta->{'author-tz'});4669my$date=$date{'iso-tz'};4670if($group_size) {4671$current_color= ++$current_color%$num_colors;4672}4673print"<tr class=\"$rev_color[$current_color]\">\n";4674if($group_size) {4675print"<td class=\"sha1\"";4676print" title=\"". esc_html($author) .",$date\"";4677print" rowspan=\"$group_size\""if($group_size>1);4678print">";4679print$cgi->a({-href => href(action=>"commit",4680 hash=>$full_rev,4681 file_name=>$file_name)},4682 esc_html($rev));4683print"</td>\n";4684}4685open(my$dd,"-|", git_cmd(),"rev-parse","$full_rev^")4686or die_error(500,"Open git-rev-parse failed");4687my$parent_commit= <$dd>;4688close$dd;4689chomp($parent_commit);4690my$blamed= href(action =>'blame',4691 file_name =>$meta->{'filename'},4692 hash_base =>$parent_commit);4693print"<td class=\"linenr\">";4694print$cgi->a({ -href =>"$blamed#l$orig_lineno",4695-id =>"l$lineno",4696-class=>"linenr"},4697 esc_html($lineno));4698print"</td>";4699print"<td class=\"pre\">". esc_html($data) ."</td>\n";4700print"</tr>\n";4701}4702print"</table>\n";4703print"</div>";4704close$fd4705or print"Reading blob failed\n";4706 git_footer_html();4707}47084709sub git_tags {4710my$head= git_get_head_hash($project);4711 git_header_html();4712 git_print_page_nav('','',$head,undef,$head);4713 git_print_header_div('summary',$project);47144715my@tagslist= git_get_tags_list();4716if(@tagslist) {4717 git_tags_body(\@tagslist);4718}4719 git_footer_html();4720}47214722sub git_heads {4723my$head= git_get_head_hash($project);4724 git_header_html();4725 git_print_page_nav('','',$head,undef,$head);4726 git_print_header_div('summary',$project);47274728my@headslist= git_get_heads_list();4729if(@headslist) {4730 git_heads_body(\@headslist,$head);4731}4732 git_footer_html();4733}47344735sub git_blob_plain {4736my$type=shift;4737my$expires;47384739if(!defined$hash) {4740if(defined$file_name) {4741my$base=$hash_base|| git_get_head_hash($project);4742$hash= git_get_hash_by_path($base,$file_name,"blob")4743or die_error(404,"Cannot find file");4744}else{4745 die_error(400,"No file name defined");4746}4747}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {4748# blobs defined by non-textual hash id's can be cached4749$expires="+1d";4750}47514752open my$fd,"-|", git_cmd(),"cat-file","blob",$hash4753or die_error(500,"Open git-cat-file blob '$hash' failed");47544755# content-type (can include charset)4756$type= blob_contenttype($fd,$file_name,$type);47574758# "save as" filename, even when no $file_name is given4759my$save_as="$hash";4760if(defined$file_name) {4761$save_as=$file_name;4762}elsif($type=~m/^text\//) {4763$save_as.='.txt';4764}47654766print$cgi->header(4767-type =>$type,4768-expires =>$expires,4769-content_disposition =>'inline; filename="'.$save_as.'"');4770undef$/;4771binmode STDOUT,':raw';4772print<$fd>;4773binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi4774$/="\n";4775close$fd;4776}47774778sub git_blob {4779my$expires;47804781if(!defined$hash) {4782if(defined$file_name) {4783my$base=$hash_base|| git_get_head_hash($project);4784$hash= git_get_hash_by_path($base,$file_name,"blob")4785or die_error(404,"Cannot find file");4786}else{4787 die_error(400,"No file name defined");4788}4789}elsif($hash=~m/^[0-9a-fA-F]{40}$/) {4790# blobs defined by non-textual hash id's can be cached4791$expires="+1d";4792}47934794my$have_blame= gitweb_check_feature('blame');4795open my$fd,"-|", git_cmd(),"cat-file","blob",$hash4796or die_error(500,"Couldn't cat$file_name,$hash");4797my$mimetype= blob_mimetype($fd,$file_name);4798if($mimetype!~m!^(?:text/|image/(?:gif|png|jpeg)$)!&& -B $fd) {4799close$fd;4800return git_blob_plain($mimetype);4801}4802# we can have blame only for text/* mimetype4803$have_blame&&= ($mimetype=~m!^text/!);48044805 git_header_html(undef,$expires);4806my$formats_nav='';4807if(defined$hash_base&& (my%co= parse_commit($hash_base))) {4808if(defined$file_name) {4809if($have_blame) {4810$formats_nav.=4811$cgi->a({-href => href(action=>"blame", -replay=>1)},4812"blame") .4813" | ";4814}4815$formats_nav.=4816$cgi->a({-href => href(action=>"history", -replay=>1)},4817"history") .4818" | ".4819$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},4820"raw") .4821" | ".4822$cgi->a({-href => href(action=>"blob",4823 hash_base=>"HEAD", file_name=>$file_name)},4824"HEAD");4825}else{4826$formats_nav.=4827$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},4828"raw");4829}4830 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);4831 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);4832}else{4833print"<div class=\"page_nav\">\n".4834"<br/><br/></div>\n".4835"<div class=\"title\">$hash</div>\n";4836}4837 git_print_page_path($file_name,"blob",$hash_base);4838print"<div class=\"page_body\">\n";4839if($mimetype=~m!^image/!) {4840print qq!<img type="$mimetype"!;4841if($file_name) {4842print qq! alt="$file_name" title="$file_name"!;4843}4844print qq! src="! .4845 href(action=>"blob_plain", hash=>$hash,4846 hash_base=>$hash_base, file_name=>$file_name) .4847 qq!"/>\n!;4848}else{4849my$nr;4850while(my$line= <$fd>) {4851chomp$line;4852$nr++;4853$line= untabify($line);4854printf"<div class=\"pre\"><a id=\"l%i\"href=\"#l%i\"class=\"linenr\">%4i</a>%s</div>\n",4855$nr,$nr,$nr, esc_html($line, -nbsp=>1);4856}4857}4858close$fd4859or print"Reading blob failed.\n";4860print"</div>";4861 git_footer_html();4862}48634864sub git_tree {4865if(!defined$hash_base) {4866$hash_base="HEAD";4867}4868if(!defined$hash) {4869if(defined$file_name) {4870$hash= git_get_hash_by_path($hash_base,$file_name,"tree");4871}else{4872$hash=$hash_base;4873}4874}4875 die_error(404,"No such tree")unlessdefined($hash);4876$/="\0";4877open my$fd,"-|", git_cmd(),"ls-tree",'-z',$hash4878or die_error(500,"Open git-ls-tree failed");4879my@entries=map{chomp;$_} <$fd>;4880close$fdor die_error(404,"Reading tree failed");4881$/="\n";48824883my$refs= git_get_references();4884my$ref= format_ref_marker($refs,$hash_base);4885 git_header_html();4886my$basedir='';4887my$have_blame= gitweb_check_feature('blame');4888if(defined$hash_base&& (my%co= parse_commit($hash_base))) {4889my@views_nav= ();4890if(defined$file_name) {4891push@views_nav,4892$cgi->a({-href => href(action=>"history", -replay=>1)},4893"history"),4894$cgi->a({-href => href(action=>"tree",4895 hash_base=>"HEAD", file_name=>$file_name)},4896"HEAD"),4897}4898my$snapshot_links= format_snapshot_links($hash);4899if(defined$snapshot_links) {4900# FIXME: Should be available when we have no hash base as well.4901push@views_nav,$snapshot_links;4902}4903 git_print_page_nav('tree','',$hash_base,undef,undef,join(' | ',@views_nav));4904 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash_base);4905}else{4906undef$hash_base;4907print"<div class=\"page_nav\">\n";4908print"<br/><br/></div>\n";4909print"<div class=\"title\">$hash</div>\n";4910}4911if(defined$file_name) {4912$basedir=$file_name;4913if($basedirne''&&substr($basedir, -1)ne'/') {4914$basedir.='/';4915}4916 git_print_page_path($file_name,'tree',$hash_base);4917}4918print"<div class=\"page_body\">\n";4919print"<table class=\"tree\">\n";4920my$alternate=1;4921# '..' (top directory) link if possible4922if(defined$hash_base&&4923defined$file_name&&$file_name=~m![^/]+$!) {4924if($alternate) {4925print"<tr class=\"dark\">\n";4926}else{4927print"<tr class=\"light\">\n";4928}4929$alternate^=1;49304931my$up=$file_name;4932$up=~s!/?[^/]+$!!;4933undef$upunless$up;4934# based on git_print_tree_entry4935print'<td class="mode">'. mode_str('040000') ."</td>\n";4936print'<td class="list">';4937print$cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,4938 file_name=>$up)},4939"..");4940print"</td>\n";4941print"<td class=\"link\"></td>\n";49424943print"</tr>\n";4944}4945foreachmy$line(@entries) {4946my%t= parse_ls_tree_line($line, -z =>1);49474948if($alternate) {4949print"<tr class=\"dark\">\n";4950}else{4951print"<tr class=\"light\">\n";4952}4953$alternate^=1;49544955 git_print_tree_entry(\%t,$basedir,$hash_base,$have_blame);49564957print"</tr>\n";4958}4959print"</table>\n".4960"</div>";4961 git_footer_html();4962}49634964sub git_snapshot {4965my$format=$input_params{'snapshot_format'};4966if(!@snapshot_fmts) {4967 die_error(403,"Snapshots not allowed");4968}4969# default to first supported snapshot format4970$format||=$snapshot_fmts[0];4971if($format!~m/^[a-z0-9]+$/) {4972 die_error(400,"Invalid snapshot format parameter");4973}elsif(!exists($known_snapshot_formats{$format})) {4974 die_error(400,"Unknown snapshot format");4975}elsif(!grep($_eq$format,@snapshot_fmts)) {4976 die_error(403,"Unsupported snapshot format");4977}49784979if(!defined$hash) {4980$hash= git_get_head_hash($project);4981}49824983my$name=$project;4984$name=~ s,([^/])/*\.git$,$1,;4985$name= basename($name);4986my$filename= to_utf8($name);4987$name=~s/\047/\047\\\047\047/g;4988my$cmd;4989$filename.="-$hash$known_snapshot_formats{$format}{'suffix'}";4990$cmd= quote_command(4991 git_cmd(),'archive',4992"--format=$known_snapshot_formats{$format}{'format'}",4993"--prefix=$name/",$hash);4994if(exists$known_snapshot_formats{$format}{'compressor'}) {4995$cmd.=' | '. quote_command(@{$known_snapshot_formats{$format}{'compressor'}});4996}49974998print$cgi->header(4999-type =>$known_snapshot_formats{$format}{'type'},5000-content_disposition =>'inline; filename="'."$filename".'"',5001-status =>'200 OK');50025003open my$fd,"-|",$cmd5004or die_error(500,"Execute git-archive failed");5005binmode STDOUT,':raw';5006print<$fd>;5007binmode STDOUT,':utf8';# as set at the beginning of gitweb.cgi5008close$fd;5009}50105011sub git_log {5012my$head= git_get_head_hash($project);5013if(!defined$hash) {5014$hash=$head;5015}5016if(!defined$page) {5017$page=0;5018}5019my$refs= git_get_references();50205021my@commitlist= parse_commits($hash,101, (100*$page));50225023my$paging_nav= format_paging_nav('log',$hash,$head,$page,$#commitlist>=100);50245025 git_header_html();5026 git_print_page_nav('log','',$hash,undef,undef,$paging_nav);50275028if(!@commitlist) {5029my%co= parse_commit($hash);50305031 git_print_header_div('summary',$project);5032print"<div class=\"page_body\"> Last change$co{'age_string'}.<br/><br/></div>\n";5033}5034my$to= ($#commitlist>=99) ? (99) : ($#commitlist);5035for(my$i=0;$i<=$to;$i++) {5036my%co= %{$commitlist[$i]};5037next if!%co;5038my$commit=$co{'id'};5039my$ref= format_ref_marker($refs,$commit);5040my%ad= parse_date($co{'author_epoch'});5041 git_print_header_div('commit',5042"<span class=\"age\">$co{'age_string'}</span>".5043 esc_html($co{'title'}) .$ref,5044$commit);5045print"<div class=\"title_text\">\n".5046"<div class=\"log_link\">\n".5047$cgi->a({-href => href(action=>"commit", hash=>$commit)},"commit") .5048" | ".5049$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)},"commitdiff") .5050" | ".5051$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)},"tree") .5052"<br/>\n".5053"</div>\n".5054"<i>". esc_html($co{'author_name'}) ." [$ad{'rfc2822'}]</i><br/>\n".5055"</div>\n";50565057print"<div class=\"log_body\">\n";5058 git_print_log($co{'comment'}, -final_empty_line=>1);5059print"</div>\n";5060}5061if($#commitlist>=100) {5062print"<div class=\"page_nav\">\n";5063print$cgi->a({-href => href(-replay=>1, page=>$page+1),5064-accesskey =>"n", -title =>"Alt-n"},"next");5065print"</div>\n";5066}5067 git_footer_html();5068}50695070sub git_commit {5071$hash||=$hash_base||"HEAD";5072my%co= parse_commit($hash)5073or die_error(404,"Unknown commit object");5074my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});5075my%cd= parse_date($co{'committer_epoch'},$co{'committer_tz'});50765077my$parent=$co{'parent'};5078my$parents=$co{'parents'};# listref50795080# we need to prepare $formats_nav before any parameter munging5081my$formats_nav;5082if(!defined$parent) {5083# --root commitdiff5084$formats_nav.='(initial)';5085}elsif(@$parents==1) {5086# single parent commit5087$formats_nav.=5088'(parent: '.5089$cgi->a({-href => href(action=>"commit",5090 hash=>$parent)},5091 esc_html(substr($parent,0,7))) .5092')';5093}else{5094# merge commit5095$formats_nav.=5096'(merge: '.5097join(' ',map{5098$cgi->a({-href => href(action=>"commit",5099 hash=>$_)},5100 esc_html(substr($_,0,7)));5101}@$parents) .5102')';5103}51045105if(!defined$parent) {5106$parent="--root";5107}5108my@difftree;5109open my$fd,"-|", git_cmd(),"diff-tree",'-r',"--no-commit-id",5110@diff_opts,5111(@$parents<=1?$parent:'-c'),5112$hash,"--"5113or die_error(500,"Open git-diff-tree failed");5114@difftree=map{chomp;$_} <$fd>;5115close$fdor die_error(404,"Reading git-diff-tree failed");51165117# non-textual hash id's can be cached5118my$expires;5119if($hash=~m/^[0-9a-fA-F]{40}$/) {5120$expires="+1d";5121}5122my$refs= git_get_references();5123my$ref= format_ref_marker($refs,$co{'id'});51245125 git_header_html(undef,$expires);5126 git_print_page_nav('commit','',5127$hash,$co{'tree'},$hash,5128$formats_nav);51295130if(defined$co{'parent'}) {5131 git_print_header_div('commitdiff', esc_html($co{'title'}) .$ref,$hash);5132}else{5133 git_print_header_div('tree', esc_html($co{'title'}) .$ref,$co{'tree'},$hash);5134}5135print"<div class=\"title_text\">\n".5136"<table class=\"object_header\">\n";5137print"<tr><td>author</td><td>". esc_html($co{'author'}) ."</td></tr>\n".5138"<tr>".5139"<td></td><td>$ad{'rfc2822'}";5140if($ad{'hour_local'} <6) {5141printf(" (<span class=\"atnight\">%02d:%02d</span>%s)",5142$ad{'hour_local'},$ad{'minute_local'},$ad{'tz_local'});5143}else{5144printf(" (%02d:%02d%s)",5145$ad{'hour_local'},$ad{'minute_local'},$ad{'tz_local'});5146}5147print"</td>".5148"</tr>\n";5149print"<tr><td>committer</td><td>". esc_html($co{'committer'}) ."</td></tr>\n";5150print"<tr><td></td><td>$cd{'rfc2822'}".5151sprintf(" (%02d:%02d%s)",$cd{'hour_local'},$cd{'minute_local'},$cd{'tz_local'}) .5152"</td></tr>\n";5153print"<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";5154print"<tr>".5155"<td>tree</td>".5156"<td class=\"sha1\">".5157$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),5158class=>"list"},$co{'tree'}) .5159"</td>".5160"<td class=\"link\">".5161$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},5162"tree");5163my$snapshot_links= format_snapshot_links($hash);5164if(defined$snapshot_links) {5165print" | ".$snapshot_links;5166}5167print"</td>".5168"</tr>\n";51695170foreachmy$par(@$parents) {5171print"<tr>".5172"<td>parent</td>".5173"<td class=\"sha1\">".5174$cgi->a({-href => href(action=>"commit", hash=>$par),5175class=>"list"},$par) .5176"</td>".5177"<td class=\"link\">".5178$cgi->a({-href => href(action=>"commit", hash=>$par)},"commit") .5179" | ".5180$cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)},"diff") .5181"</td>".5182"</tr>\n";5183}5184print"</table>".5185"</div>\n";51865187print"<div class=\"page_body\">\n";5188 git_print_log($co{'comment'});5189print"</div>\n";51905191 git_difftree_body(\@difftree,$hash,@$parents);51925193 git_footer_html();5194}51955196sub git_object {5197# object is defined by:5198# - hash or hash_base alone5199# - hash_base and file_name5200my$type;52015202# - hash or hash_base alone5203if($hash|| ($hash_base&& !defined$file_name)) {5204my$object_id=$hash||$hash_base;52055206open my$fd,"-|", quote_command(5207 git_cmd(),'cat-file','-t',$object_id) .' 2> /dev/null'5208or die_error(404,"Object does not exist");5209$type= <$fd>;5210chomp$type;5211close$fd5212or die_error(404,"Object does not exist");52135214# - hash_base and file_name5215}elsif($hash_base&&defined$file_name) {5216$file_name=~ s,/+$,,;52175218system(git_cmd(),"cat-file",'-e',$hash_base) ==05219or die_error(404,"Base object does not exist");52205221# here errors should not hapen5222open my$fd,"-|", git_cmd(),"ls-tree",$hash_base,"--",$file_name5223or die_error(500,"Open git-ls-tree failed");5224my$line= <$fd>;5225close$fd;52265227#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'5228unless($line&&$line=~m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {5229 die_error(404,"File or directory for given base does not exist");5230}5231$type=$2;5232$hash=$3;5233}else{5234 die_error(400,"Not enough information to find object");5235}52365237print$cgi->redirect(-uri => href(action=>$type, -full=>1,5238 hash=>$hash, hash_base=>$hash_base,5239 file_name=>$file_name),5240-status =>'302 Found');5241}52425243sub git_blobdiff {5244my$format=shift||'html';52455246my$fd;5247my@difftree;5248my%diffinfo;5249my$expires;52505251# preparing $fd and %diffinfo for git_patchset_body5252# new style URI5253if(defined$hash_base&&defined$hash_parent_base) {5254if(defined$file_name) {5255# read raw output5256open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5257$hash_parent_base,$hash_base,5258"--", (defined$file_parent?$file_parent: ()),$file_name5259or die_error(500,"Open git-diff-tree failed");5260@difftree=map{chomp;$_} <$fd>;5261close$fd5262or die_error(404,"Reading git-diff-tree failed");5263@difftree5264or die_error(404,"Blob diff not found");52655266}elsif(defined$hash&&5267$hash=~/[0-9a-fA-F]{40}/) {5268# try to find filename from $hash52695270# read filtered raw output5271open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5272$hash_parent_base,$hash_base,"--"5273or die_error(500,"Open git-diff-tree failed");5274@difftree=5275# ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'5276# $hash == to_id5277grep{/^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/}5278map{chomp;$_} <$fd>;5279close$fd5280or die_error(404,"Reading git-diff-tree failed");5281@difftree5282or die_error(404,"Blob diff not found");52835284}else{5285 die_error(400,"Missing one of the blob diff parameters");5286}52875288if(@difftree>1) {5289 die_error(400,"Ambiguous blob diff specification");5290}52915292%diffinfo= parse_difftree_raw_line($difftree[0]);5293$file_parent||=$diffinfo{'from_file'} ||$file_name;5294$file_name||=$diffinfo{'to_file'};52955296$hash_parent||=$diffinfo{'from_id'};5297$hash||=$diffinfo{'to_id'};52985299# non-textual hash id's can be cached5300if($hash_base=~m/^[0-9a-fA-F]{40}$/&&5301$hash_parent_base=~m/^[0-9a-fA-F]{40}$/) {5302$expires='+1d';5303}53045305# open patch output5306open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5307'-p', ($formateq'html'?"--full-index": ()),5308$hash_parent_base,$hash_base,5309"--", (defined$file_parent?$file_parent: ()),$file_name5310or die_error(500,"Open git-diff-tree failed");5311}53125313# old/legacy style URI5314if(!%diffinfo&&# if new style URI failed5315defined$hash&&defined$hash_parent) {5316# fake git-diff-tree raw output5317$diffinfo{'from_mode'} =$diffinfo{'to_mode'} ="blob";5318$diffinfo{'from_id'} =$hash_parent;5319$diffinfo{'to_id'} =$hash;5320if(defined$file_name) {5321if(defined$file_parent) {5322$diffinfo{'status'} ='2';5323$diffinfo{'from_file'} =$file_parent;5324$diffinfo{'to_file'} =$file_name;5325}else{# assume not renamed5326$diffinfo{'status'} ='1';5327$diffinfo{'from_file'} =$file_name;5328$diffinfo{'to_file'} =$file_name;5329}5330}else{# no filename given5331$diffinfo{'status'} ='2';5332$diffinfo{'from_file'} =$hash_parent;5333$diffinfo{'to_file'} =$hash;5334}53355336# non-textual hash id's can be cached5337if($hash=~m/^[0-9a-fA-F]{40}$/&&5338$hash_parent=~m/^[0-9a-fA-F]{40}$/) {5339$expires='+1d';5340}53415342# open patch output5343open$fd,"-|", git_cmd(),"diff",@diff_opts,5344'-p', ($formateq'html'?"--full-index": ()),5345$hash_parent,$hash,"--"5346or die_error(500,"Open git-diff failed");5347}else{5348 die_error(400,"Missing one of the blob diff parameters")5349unless%diffinfo;5350}53515352# header5353if($formateq'html') {5354my$formats_nav=5355$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},5356"raw");5357 git_header_html(undef,$expires);5358if(defined$hash_base&& (my%co= parse_commit($hash_base))) {5359 git_print_page_nav('','',$hash_base,$co{'tree'},$hash_base,$formats_nav);5360 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5361}else{5362print"<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";5363print"<div class=\"title\">$hashvs$hash_parent</div>\n";5364}5365if(defined$file_name) {5366 git_print_page_path($file_name,"blob",$hash_base);5367}else{5368print"<div class=\"page_path\"></div>\n";5369}53705371}elsif($formateq'plain') {5372print$cgi->header(5373-type =>'text/plain',5374-charset =>'utf-8',5375-expires =>$expires,5376-content_disposition =>'inline; filename="'."$file_name".'.patch"');53775378print"X-Git-Url: ".$cgi->self_url() ."\n\n";53795380}else{5381 die_error(400,"Unknown blobdiff format");5382}53835384# patch5385if($formateq'html') {5386print"<div class=\"page_body\">\n";53875388 git_patchset_body($fd, [ \%diffinfo],$hash_base,$hash_parent_base);5389close$fd;53905391print"</div>\n";# class="page_body"5392 git_footer_html();53935394}else{5395while(my$line= <$fd>) {5396$line=~s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;5397$line=~s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;53985399print$line;54005401last if$line=~m!^\+\+\+!;5402}5403local$/=undef;5404print<$fd>;5405close$fd;5406}5407}54085409sub git_blobdiff_plain {5410 git_blobdiff('plain');5411}54125413sub git_commitdiff {5414my$format=shift||'html';54155416my$patch_max;5417if($formateq'patch') {5418($patch_max) = gitweb_get_feature('patches');5419 die_error(403,"Patch view not allowed")unless$patch_max;5420}54215422$hash||=$hash_base||"HEAD";5423my%co= parse_commit($hash)5424or die_error(404,"Unknown commit object");54255426# choose format for commitdiff for merge5427if(!defined$hash_parent&& @{$co{'parents'}} >1) {5428$hash_parent='--cc';5429}5430# we need to prepare $formats_nav before almost any parameter munging5431my$formats_nav;5432if($formateq'html') {5433$formats_nav=5434$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},5435"raw");54365437if(defined$hash_parent&&5438$hash_parentne'-c'&&$hash_parentne'--cc') {5439# commitdiff with two commits given5440my$hash_parent_short=$hash_parent;5441if($hash_parent=~m/^[0-9a-fA-F]{40}$/) {5442$hash_parent_short=substr($hash_parent,0,7);5443}5444$formats_nav.=5445' (from';5446for(my$i=0;$i< @{$co{'parents'}};$i++) {5447if($co{'parents'}[$i]eq$hash_parent) {5448$formats_nav.=' parent '. ($i+1);5449last;5450}5451}5452$formats_nav.=': '.5453$cgi->a({-href => href(action=>"commitdiff",5454 hash=>$hash_parent)},5455 esc_html($hash_parent_short)) .5456')';5457}elsif(!$co{'parent'}) {5458# --root commitdiff5459$formats_nav.=' (initial)';5460}elsif(scalar@{$co{'parents'}} ==1) {5461# single parent commit5462$formats_nav.=5463' (parent: '.5464$cgi->a({-href => href(action=>"commitdiff",5465 hash=>$co{'parent'})},5466 esc_html(substr($co{'parent'},0,7))) .5467')';5468}else{5469# merge commit5470if($hash_parenteq'--cc') {5471$formats_nav.=' | '.5472$cgi->a({-href => href(action=>"commitdiff",5473 hash=>$hash, hash_parent=>'-c')},5474'combined');5475}else{# $hash_parent eq '-c'5476$formats_nav.=' | '.5477$cgi->a({-href => href(action=>"commitdiff",5478 hash=>$hash, hash_parent=>'--cc')},5479'compact');5480}5481$formats_nav.=5482' (merge: '.5483join(' ',map{5484$cgi->a({-href => href(action=>"commitdiff",5485 hash=>$_)},5486 esc_html(substr($_,0,7)));5487} @{$co{'parents'}} ) .5488')';5489}5490}54915492my$hash_parent_param=$hash_parent;5493if(!defined$hash_parent_param) {5494# --cc for multiple parents, --root for parentless5495$hash_parent_param=5496@{$co{'parents'}} >1?'--cc':$co{'parent'} ||'--root';5497}54985499# read commitdiff5500my$fd;5501my@difftree;5502if($formateq'html') {5503open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5504"--no-commit-id","--patch-with-raw","--full-index",5505$hash_parent_param,$hash,"--"5506or die_error(500,"Open git-diff-tree failed");55075508while(my$line= <$fd>) {5509chomp$line;5510# empty line ends raw part of diff-tree output5511last unless$line;5512push@difftree,scalar parse_difftree_raw_line($line);5513}55145515}elsif($formateq'plain') {5516open$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,5517'-p',$hash_parent_param,$hash,"--"5518or die_error(500,"Open git-diff-tree failed");5519}elsif($formateq'patch') {5520# For commit ranges, we limit the output to the number of5521# patches specified in the 'patches' feature.5522# For single commits, we limit the output to a single patch,5523# diverging from the git-format-patch default.5524my@commit_spec= ();5525if($hash_parent) {5526if($patch_max>0) {5527push@commit_spec,"-$patch_max";5528}5529push@commit_spec,'-n',"$hash_parent..$hash";5530}else{5531push@commit_spec,'-1','--root',$hash;5532}5533open$fd,"-|", git_cmd(),"format-patch",'--encoding=utf8',5534'--stdout',@commit_spec5535or die_error(500,"Open git-format-patch failed");5536}else{5537 die_error(400,"Unknown commitdiff format");5538}55395540# non-textual hash id's can be cached5541my$expires;5542if($hash=~m/^[0-9a-fA-F]{40}$/) {5543$expires="+1d";5544}55455546# write commit message5547if($formateq'html') {5548my$refs= git_get_references();5549my$ref= format_ref_marker($refs,$co{'id'});55505551 git_header_html(undef,$expires);5552 git_print_page_nav('commitdiff','',$hash,$co{'tree'},$hash,$formats_nav);5553 git_print_header_div('commit', esc_html($co{'title'}) .$ref,$hash);5554 git_print_authorship(\%co);5555print"<div class=\"page_body\">\n";5556if(@{$co{'comment'}} >1) {5557print"<div class=\"log\">\n";5558 git_print_log($co{'comment'}, -final_empty_line=>1, -remove_title =>1);5559print"</div>\n";# class="log"5560}55615562}elsif($formateq'plain') {5563my$refs= git_get_references("tags");5564my$tagname= git_get_rev_name_tags($hash);5565my$filename= basename($project) ."-$hash.patch";55665567print$cgi->header(5568-type =>'text/plain',5569-charset =>'utf-8',5570-expires =>$expires,5571-content_disposition =>'inline; filename="'."$filename".'"');5572my%ad= parse_date($co{'author_epoch'},$co{'author_tz'});5573print"From: ". to_utf8($co{'author'}) ."\n";5574print"Date:$ad{'rfc2822'} ($ad{'tz_local'})\n";5575print"Subject: ". to_utf8($co{'title'}) ."\n";55765577print"X-Git-Tag:$tagname\n"if$tagname;5578print"X-Git-Url: ".$cgi->self_url() ."\n\n";55795580foreachmy$line(@{$co{'comment'}}) {5581print to_utf8($line) ."\n";5582}5583print"---\n\n";5584}elsif($formateq'patch') {5585my$filename= basename($project) ."-$hash.patch";55865587print$cgi->header(5588-type =>'text/plain',5589-charset =>'utf-8',5590-expires =>$expires,5591-content_disposition =>'inline; filename="'."$filename".'"');5592}55935594# write patch5595if($formateq'html') {5596my$use_parents= !defined$hash_parent||5597$hash_parenteq'-c'||$hash_parenteq'--cc';5598 git_difftree_body(\@difftree,$hash,5599$use_parents? @{$co{'parents'}} :$hash_parent);5600print"<br/>\n";56015602 git_patchset_body($fd, \@difftree,$hash,5603$use_parents? @{$co{'parents'}} :$hash_parent);5604close$fd;5605print"</div>\n";# class="page_body"5606 git_footer_html();56075608}elsif($formateq'plain') {5609local$/=undef;5610print<$fd>;5611close$fd5612or print"Reading git-diff-tree failed\n";5613}elsif($formateq'patch') {5614local$/=undef;5615print<$fd>;5616close$fd5617or print"Reading git-format-patch failed\n";5618}5619}56205621sub git_commitdiff_plain {5622 git_commitdiff('plain');5623}56245625# format-patch-style patches5626sub git_patch {5627 git_commitdiff('patch');5628}56295630sub git_history {5631if(!defined$hash_base) {5632$hash_base= git_get_head_hash($project);5633}5634if(!defined$page) {5635$page=0;5636}5637my$ftype;5638my%co= parse_commit($hash_base)5639or die_error(404,"Unknown commit object");56405641my$refs= git_get_references();5642my$limit=sprintf("--max-count=%i", (100* ($page+1)));56435644my@commitlist= parse_commits($hash_base,101, (100*$page),5645$file_name,"--full-history")5646or die_error(404,"No such file or directory on given branch");56475648if(!defined$hash&&defined$file_name) {5649# some commits could have deleted file in question,5650# and not have it in tree, but one of them has to have it5651for(my$i=0;$i<=@commitlist;$i++) {5652$hash= git_get_hash_by_path($commitlist[$i]{'id'},$file_name);5653last ifdefined$hash;5654}5655}5656if(defined$hash) {5657$ftype= git_get_type($hash);5658}5659if(!defined$ftype) {5660 die_error(500,"Unknown type of object");5661}56625663my$paging_nav='';5664if($page>0) {5665$paging_nav.=5666$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,5667 file_name=>$file_name)},5668"first");5669$paging_nav.=" ⋅ ".5670$cgi->a({-href => href(-replay=>1, page=>$page-1),5671-accesskey =>"p", -title =>"Alt-p"},"prev");5672}else{5673$paging_nav.="first";5674$paging_nav.=" ⋅ prev";5675}5676my$next_link='';5677if($#commitlist>=100) {5678$next_link=5679$cgi->a({-href => href(-replay=>1, page=>$page+1),5680-accesskey =>"n", -title =>"Alt-n"},"next");5681$paging_nav.=" ⋅$next_link";5682}else{5683$paging_nav.=" ⋅ next";5684}56855686 git_header_html();5687 git_print_page_nav('history','',$hash_base,$co{'tree'},$hash_base,$paging_nav);5688 git_print_header_div('commit', esc_html($co{'title'}),$hash_base);5689 git_print_page_path($file_name,$ftype,$hash_base);56905691 git_history_body(\@commitlist,0,99,5692$refs,$hash_base,$ftype,$next_link);56935694 git_footer_html();5695}56965697sub git_search {5698 gitweb_check_feature('search')or die_error(403,"Search is disabled");5699if(!defined$searchtext) {5700 die_error(400,"Text field is empty");5701}5702if(!defined$hash) {5703$hash= git_get_head_hash($project);5704}5705my%co= parse_commit($hash);5706if(!%co) {5707 die_error(404,"Unknown commit object");5708}5709if(!defined$page) {5710$page=0;5711}57125713$searchtype||='commit';5714if($searchtypeeq'pickaxe') {5715# pickaxe may take all resources of your box and run for several minutes5716# with every query - so decide by yourself how public you make this feature5717 gitweb_check_feature('pickaxe')5718or die_error(403,"Pickaxe is disabled");5719}5720if($searchtypeeq'grep') {5721 gitweb_check_feature('grep')5722or die_error(403,"Grep is disabled");5723}57245725 git_header_html();57265727if($searchtypeeq'commit'or$searchtypeeq'author'or$searchtypeeq'committer') {5728my$greptype;5729if($searchtypeeq'commit') {5730$greptype="--grep=";5731}elsif($searchtypeeq'author') {5732$greptype="--author=";5733}elsif($searchtypeeq'committer') {5734$greptype="--committer=";5735}5736$greptype.=$searchtext;5737my@commitlist= parse_commits($hash,101, (100*$page),undef,5738$greptype,'--regexp-ignore-case',5739$search_use_regexp?'--extended-regexp':'--fixed-strings');57405741my$paging_nav='';5742if($page>0) {5743$paging_nav.=5744$cgi->a({-href => href(action=>"search", hash=>$hash,5745 searchtext=>$searchtext,5746 searchtype=>$searchtype)},5747"first");5748$paging_nav.=" ⋅ ".5749$cgi->a({-href => href(-replay=>1, page=>$page-1),5750-accesskey =>"p", -title =>"Alt-p"},"prev");5751}else{5752$paging_nav.="first";5753$paging_nav.=" ⋅ prev";5754}5755my$next_link='';5756if($#commitlist>=100) {5757$next_link=5758$cgi->a({-href => href(-replay=>1, page=>$page+1),5759-accesskey =>"n", -title =>"Alt-n"},"next");5760$paging_nav.=" ⋅$next_link";5761}else{5762$paging_nav.=" ⋅ next";5763}57645765if($#commitlist>=100) {5766}57675768 git_print_page_nav('','',$hash,$co{'tree'},$hash,$paging_nav);5769 git_print_header_div('commit', esc_html($co{'title'}),$hash);5770 git_search_grep_body(\@commitlist,0,99,$next_link);5771}57725773if($searchtypeeq'pickaxe') {5774 git_print_page_nav('','',$hash,$co{'tree'},$hash);5775 git_print_header_div('commit', esc_html($co{'title'}),$hash);57765777print"<table class=\"pickaxe search\">\n";5778my$alternate=1;5779$/="\n";5780open my$fd,'-|', git_cmd(),'--no-pager','log',@diff_opts,5781'--pretty=format:%H','--no-abbrev','--raw',"-S$searchtext",5782($search_use_regexp?'--pickaxe-regex': ());5783undef%co;5784my@files;5785while(my$line= <$fd>) {5786chomp$line;5787next unless$line;57885789my%set= parse_difftree_raw_line($line);5790if(defined$set{'commit'}) {5791# finish previous commit5792if(%co) {5793print"</td>\n".5794"<td class=\"link\">".5795$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .5796" | ".5797$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");5798print"</td>\n".5799"</tr>\n";5800}58015802if($alternate) {5803print"<tr class=\"dark\">\n";5804}else{5805print"<tr class=\"light\">\n";5806}5807$alternate^=1;5808%co= parse_commit($set{'commit'});5809my$author= chop_and_escape_str($co{'author_name'},15,5);5810print"<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n".5811"<td><i>$author</i></td>\n".5812"<td>".5813$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),5814-class=>"list subject"},5815 chop_and_escape_str($co{'title'},50) ."<br/>");5816}elsif(defined$set{'to_id'}) {5817next if($set{'to_id'} =~m/^0{40}$/);58185819print$cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},5820 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),5821-class=>"list"},5822"<span class=\"match\">". esc_path($set{'file'}) ."</span>") .5823"<br/>\n";5824}5825}5826close$fd;58275828# finish last commit (warning: repetition!)5829if(%co) {5830print"</td>\n".5831"<td class=\"link\">".5832$cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},"commit") .5833" | ".5834$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})},"tree");5835print"</td>\n".5836"</tr>\n";5837}58385839print"</table>\n";5840}58415842if($searchtypeeq'grep') {5843 git_print_page_nav('','',$hash,$co{'tree'},$hash);5844 git_print_header_div('commit', esc_html($co{'title'}),$hash);58455846print"<table class=\"grep_search\">\n";5847my$alternate=1;5848my$matches=0;5849$/="\n";5850open my$fd,"-|", git_cmd(),'grep','-n',5851$search_use_regexp? ('-E','-i') :'-F',5852$searchtext,$co{'tree'};5853my$lastfile='';5854while(my$line= <$fd>) {5855chomp$line;5856my($file,$lno,$ltext,$binary);5857last if($matches++>1000);5858if($line=~/^Binary file (.+) matches$/) {5859$file=$1;5860$binary=1;5861}else{5862(undef,$file,$lno,$ltext) =split(/:/,$line,4);5863}5864if($filene$lastfile) {5865$lastfileand print"</td></tr>\n";5866if($alternate++) {5867print"<tr class=\"dark\">\n";5868}else{5869print"<tr class=\"light\">\n";5870}5871print"<td class=\"list\">".5872$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},5873 file_name=>"$file"),5874-class=>"list"}, esc_path($file));5875print"</td><td>\n";5876$lastfile=$file;5877}5878if($binary) {5879print"<div class=\"binary\">Binary file</div>\n";5880}else{5881$ltext= untabify($ltext);5882if($ltext=~m/^(.*)($search_regexp)(.*)$/i) {5883$ltext= esc_html($1, -nbsp=>1);5884$ltext.='<span class="match">';5885$ltext.= esc_html($2, -nbsp=>1);5886$ltext.='</span>';5887$ltext.= esc_html($3, -nbsp=>1);5888}else{5889$ltext= esc_html($ltext, -nbsp=>1);5890}5891print"<div class=\"pre\">".5892$cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},5893 file_name=>"$file").'#l'.$lno,5894-class=>"linenr"},sprintf('%4i',$lno))5895.' '.$ltext."</div>\n";5896}5897}5898if($lastfile) {5899print"</td></tr>\n";5900if($matches>1000) {5901print"<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";5902}5903}else{5904print"<div class=\"diff nodifferences\">No matches found</div>\n";5905}5906close$fd;59075908print"</table>\n";5909}5910 git_footer_html();5911}59125913sub git_search_help {5914 git_header_html();5915 git_print_page_nav('','',$hash,$hash,$hash);5916print<<EOT;5917<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without5918regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,5919the pattern entered is recognized as the POSIX extended5920<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case5921insensitive).</p>5922<dl>5923<dt><b>commit</b></dt>5924<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>5925EOT5926my$have_grep= gitweb_check_feature('grep');5927if($have_grep) {5928print<<EOT;5929<dt><b>grep</b></dt>5930<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing5931 a different one) are searched for the given pattern. On large trees, this search can take5932a while and put some strain on the server, so please use it with some consideration. Note that5933due to git-grep peculiarity, currently if regexp mode is turned off, the matches are5934case-sensitive.</dd>5935EOT5936}5937print<<EOT;5938<dt><b>author</b></dt>5939<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>5940<dt><b>committer</b></dt>5941<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>5942EOT5943my$have_pickaxe= gitweb_check_feature('pickaxe');5944if($have_pickaxe) {5945print<<EOT;5946<dt><b>pickaxe</b></dt>5947<dd>All commits that caused the string to appear or disappear from any file (changes that5948added, removed or "modified" the string) will be listed. This search can take a while and5949takes a lot of strain on the server, so please use it wisely. Note that since you may be5950interested even in changes just changing the case as well, this search is case sensitive.</dd>5951EOT5952}5953print"</dl>\n";5954 git_footer_html();5955}59565957sub git_shortlog {5958my$head= git_get_head_hash($project);5959if(!defined$hash) {5960$hash=$head;5961}5962if(!defined$page) {5963$page=0;5964}5965my$refs= git_get_references();59665967my$commit_hash=$hash;5968if(defined$hash_parent) {5969$commit_hash="$hash_parent..$hash";5970}5971my@commitlist= parse_commits($commit_hash,101, (100*$page));59725973my$paging_nav= format_paging_nav('shortlog',$hash,$head,$page,$#commitlist>=100);5974my$next_link='';5975if($#commitlist>=100) {5976$next_link=5977$cgi->a({-href => href(-replay=>1, page=>$page+1),5978-accesskey =>"n", -title =>"Alt-n"},"next");5979}59805981 git_header_html();5982 git_print_page_nav('shortlog','',$hash,$hash,$hash,$paging_nav);5983 git_print_header_div('summary',$project);59845985 git_shortlog_body(\@commitlist,0,99,$refs,$next_link);59865987 git_footer_html();5988}59895990## ......................................................................5991## feeds (RSS, Atom; OPML)59925993sub git_feed {5994my$format=shift||'atom';5995my$have_blame= gitweb_check_feature('blame');59965997# Atom: http://www.atomenabled.org/developers/syndication/5998# RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ5999if($formatne'rss'&&$formatne'atom') {6000 die_error(400,"Unknown web feed format");6001}60026003# log/feed of current (HEAD) branch, log of given branch, history of file/directory6004my$head=$hash||'HEAD';6005my@commitlist= parse_commits($head,150,0,$file_name);60066007my%latest_commit;6008my%latest_date;6009my$content_type="application/$format+xml";6010if(defined$cgi->http('HTTP_ACCEPT') &&6011$cgi->Accept('text/xml') >$cgi->Accept($content_type)) {6012# browser (feed reader) prefers text/xml6013$content_type='text/xml';6014}6015if(defined($commitlist[0])) {6016%latest_commit= %{$commitlist[0]};6017%latest_date= parse_date($latest_commit{'author_epoch'});6018print$cgi->header(6019-type =>$content_type,6020-charset =>'utf-8',6021-last_modified =>$latest_date{'rfc2822'});6022}else{6023print$cgi->header(6024-type =>$content_type,6025-charset =>'utf-8');6026}60276028# Optimization: skip generating the body if client asks only6029# for Last-Modified date.6030return if($cgi->request_method()eq'HEAD');60316032# header variables6033my$title="$site_name-$project/$action";6034my$feed_type='log';6035if(defined$hash) {6036$title.=" - '$hash'";6037$feed_type='branch log';6038if(defined$file_name) {6039$title.=" ::$file_name";6040$feed_type='history';6041}6042}elsif(defined$file_name) {6043$title.=" -$file_name";6044$feed_type='history';6045}6046$title.="$feed_type";6047my$descr= git_get_project_description($project);6048if(defined$descr) {6049$descr= esc_html($descr);6050}else{6051$descr="$project".6052($formateq'rss'?'RSS':'Atom') .6053" feed";6054}6055my$owner= git_get_project_owner($project);6056$owner= esc_html($owner);60576058#header6059my$alt_url;6060if(defined$file_name) {6061$alt_url= href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);6062}elsif(defined$hash) {6063$alt_url= href(-full=>1, action=>"log", hash=>$hash);6064}else{6065$alt_url= href(-full=>1, action=>"summary");6066}6067print qq!<?xml version="1.0" encoding="utf-8"?>\n!;6068if($formateq'rss') {6069print<<XML;6070<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">6071<channel>6072XML6073print"<title>$title</title>\n".6074"<link>$alt_url</link>\n".6075"<description>$descr</description>\n".6076"<language>en</language>\n";6077}elsif($formateq'atom') {6078print<<XML;6079<feed xmlns="http://www.w3.org/2005/Atom">6080XML6081print"<title>$title</title>\n".6082"<subtitle>$descr</subtitle>\n".6083'<link rel="alternate" type="text/html" href="'.6084$alt_url.'" />'."\n".6085'<link rel="self" type="'.$content_type.'" href="'.6086$cgi->self_url() .'" />'."\n".6087"<id>". href(-full=>1) ."</id>\n".6088# use project owner for feed author6089"<author><name>$owner</name></author>\n";6090if(defined$favicon) {6091print"<icon>". esc_url($favicon) ."</icon>\n";6092}6093if(defined$logo_url) {6094# not twice as wide as tall: 72 x 27 pixels6095print"<logo>". esc_url($logo) ."</logo>\n";6096}6097if(!%latest_date) {6098# dummy date to keep the feed valid until commits trickle in:6099print"<updated>1970-01-01T00:00:00Z</updated>\n";6100}else{6101print"<updated>$latest_date{'iso-8601'}</updated>\n";6102}6103}61046105# contents6106for(my$i=0;$i<=$#commitlist;$i++) {6107my%co= %{$commitlist[$i]};6108my$commit=$co{'id'};6109# we read 150, we always show 30 and the ones more recent than 48 hours6110if(($i>=20) && ((time-$co{'author_epoch'}) >48*60*60)) {6111last;6112}6113my%cd= parse_date($co{'author_epoch'});61146115# get list of changed files6116open my$fd,"-|", git_cmd(),"diff-tree",'-r',@diff_opts,6117$co{'parent'} ||"--root",6118$co{'id'},"--", (defined$file_name?$file_name: ())6119ornext;6120my@difftree=map{chomp;$_} <$fd>;6121close$fd6122ornext;61236124# print element (entry, item)6125my$co_url= href(-full=>1, action=>"commitdiff", hash=>$commit);6126if($formateq'rss') {6127print"<item>\n".6128"<title>". esc_html($co{'title'}) ."</title>\n".6129"<author>". esc_html($co{'author'}) ."</author>\n".6130"<pubDate>$cd{'rfc2822'}</pubDate>\n".6131"<guid isPermaLink=\"true\">$co_url</guid>\n".6132"<link>$co_url</link>\n".6133"<description>". esc_html($co{'title'}) ."</description>\n".6134"<content:encoded>".6135"<![CDATA[\n";6136}elsif($formateq'atom') {6137print"<entry>\n".6138"<title type=\"html\">". esc_html($co{'title'}) ."</title>\n".6139"<updated>$cd{'iso-8601'}</updated>\n".6140"<author>\n".6141" <name>". esc_html($co{'author_name'}) ."</name>\n";6142if($co{'author_email'}) {6143print" <email>". esc_html($co{'author_email'}) ."</email>\n";6144}6145print"</author>\n".6146# use committer for contributor6147"<contributor>\n".6148" <name>". esc_html($co{'committer_name'}) ."</name>\n";6149if($co{'committer_email'}) {6150print" <email>". esc_html($co{'committer_email'}) ."</email>\n";6151}6152print"</contributor>\n".6153"<published>$cd{'iso-8601'}</published>\n".6154"<link rel=\"alternate\"type=\"text/html\"href=\"$co_url\"/>\n".6155"<id>$co_url</id>\n".6156"<content type=\"xhtml\"xml:base=\"". esc_url($my_url) ."\">\n".6157"<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";6158}6159my$comment=$co{'comment'};6160print"<pre>\n";6161foreachmy$line(@$comment) {6162$line= esc_html($line);6163print"$line\n";6164}6165print"</pre><ul>\n";6166foreachmy$difftree_line(@difftree) {6167my%difftree= parse_difftree_raw_line($difftree_line);6168next if!$difftree{'from_id'};61696170my$file=$difftree{'file'} ||$difftree{'to_file'};61716172print"<li>".6173"[".6174$cgi->a({-href => href(-full=>1, action=>"blobdiff",6175 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},6176 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},6177 file_name=>$file, file_parent=>$difftree{'from_file'}),6178-title =>"diff"},'D');6179if($have_blame) {6180print$cgi->a({-href => href(-full=>1, action=>"blame",6181 file_name=>$file, hash_base=>$commit),6182-title =>"blame"},'B');6183}6184# if this is not a feed of a file history6185if(!defined$file_name||$file_namene$file) {6186print$cgi->a({-href => href(-full=>1, action=>"history",6187 file_name=>$file, hash=>$commit),6188-title =>"history"},'H');6189}6190$file= esc_path($file);6191print"] ".6192"$file</li>\n";6193}6194if($formateq'rss') {6195print"</ul>]]>\n".6196"</content:encoded>\n".6197"</item>\n";6198}elsif($formateq'atom') {6199print"</ul>\n</div>\n".6200"</content>\n".6201"</entry>\n";6202}6203}62046205# end of feed6206if($formateq'rss') {6207print"</channel>\n</rss>\n";6208}elsif($formateq'atom') {6209print"</feed>\n";6210}6211}62126213sub git_rss {6214 git_feed('rss');6215}62166217sub git_atom {6218 git_feed('atom');6219}62206221sub git_opml {6222my@list= git_get_projects_list();62236224print$cgi->header(-type =>'text/xml', -charset =>'utf-8');6225print<<XML;6226<?xml version="1.0" encoding="utf-8"?>6227<opml version="1.0">6228<head>6229 <title>$site_nameOPML Export</title>6230</head>6231<body>6232<outline text="git RSS feeds">6233XML62346235foreachmy$pr(@list) {6236my%proj=%$pr;6237my$head= git_get_head_hash($proj{'path'});6238if(!defined$head) {6239next;6240}6241$git_dir="$projectroot/$proj{'path'}";6242my%co= parse_commit($head);6243if(!%co) {6244next;6245}62466247my$path= esc_html(chop_str($proj{'path'},25,5));6248my$rss="$my_url?p=$proj{'path'};a=rss";6249my$html="$my_url?p=$proj{'path'};a=summary";6250print"<outline type=\"rss\"text=\"$path\"title=\"$path\"xmlUrl=\"$rss\"htmlUrl=\"$html\"/>\n";6251}6252print<<XML;6253</outline>6254</body>6255</opml>6256XML6257}