{"thread":{"id":"27224","subject":"[PATCH 0/6] gitweb: Improve ctags, introduce categories","startedAt":"2011-04-29T17:51:55Z","lastAt":"2011-05-07T18:39:22Z","messageCount":13,"participants":["Jakub Narebski","Junio C Hamano","Øyvind A. Holm"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"166759","messageId":"1304099521-27617-1-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":null,"subject":"[PATCH 0/6] gitweb: Improve ctags, introduce categories","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:51:55Z","receivedAt":"2011-04-29T17:51:55Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This series is composed of two independent parts: improvements to\ncontent tags (ctags) handling in patches 2-3, and introducing optional\ngrouping of projects by categories in patches 4-6 (where 4-5 are\npreparatory).\n\nBoth of those series are based on 1st patch in series which\nrestructures how projects list is being generated, to make those\nchanges easy to introduce.  Because both of those depend on the same\nintroductory patch they are put in single series... that and the fact\nthat there was trivial conflict to be resolved in rebasing 'project\ncategory' part on top of 'ctags' part.\n\nTable of contents:\n~~~~~~~~~~~~~~~~~~\n* [PATCH 1/6 (v2)] gitweb: Restructure projects list generation\n\n  This patch extracts filtering out \"forks\" and searching for projects\n  (i.e. limiting listed projects) into separate subroutines.  Also\n  filtering is done upfront and not while printing.  This makes for\n  more clear code and makes subsequent changes much easier.\n\n  Filtering forks is now faster thanks to using trie; see comments in\n  code and in the patch itself.\n\n* [PATCH 2/6] gitweb: Change the way \"content tags\" ('ctags') are handled\n\n  Making ctags info parsing more robust sort of fixes bug noticed in\n\n    gitweb: cloud tags feature produces malformed XML for errors\n    http://thread.gmane.org/gmane.linux.debian.devel.bugs.general/802865/focus=168266\n    http://bugs.debian.org/616005\n\n  caused by myshandling underdocumented then 'ctags' feature.  This\n  patch DOES NOT however resolve deeper fundamental problem with error\n  / exception handling in gitweb after some data was already sent to a\n  browser.\n\n  It also removes gitweb half of interface for adding content tags\n  (labels) to a project; it was quarter of feature at best (there\n  is/was no example implementation of server side half of adding\n  content tags, no way to delete or fix content tags).\n\n* [PATCH 3/6] gitweb: Mark matched 'ctag' / contents tag (?by_tag=foo)\n\n  This is simple improvement, done while working on 'ctags' feature.\n\n* [PATCH 4/6] gitweb: Split git_project_list_body in two functions\n  [PATCH 5/6] gitweb: Modularized git_get_project_description to be more generic\n\n  Those two patches are refactoring preparing way for project\n  categories (for the next patch).\n\n* [PATCH 6/6] gitweb: Optional grouping of projects by category\n\n  Port of Sebastien Cevey port of Sham Chukoury's patch for the XMMS2\n  gitweb to modern gitweb.\n\nShortlog:\n~~~~~~~~~\nJakub Narebski (3):\n  gitweb: Restructure projects list generation\n  gitweb: Change the way \"content tags\" ('ctags') are handled\n  gitweb: Mark matched 'ctag' / contents tag (?by_tag=foo)\n\nSebastien Cevey (3):\n  gitweb: Split git_project_list_body in two functions\n  gitweb: Modularized git_get_project_description to be more generic\n  gitweb: Optional grouping of projects by category\n\nDiffstat:\n~~~~~~~~~\n gitweb/README                             |   16 +\n gitweb/gitweb.perl                        |  546 ++++++++++++++++++++---------\n gitweb/static/gitweb.css                  |    7 +\n t/t9500-gitweb-standalone-no-errors.sh    |   57 +++\n t/t9502-gitweb-standalone-parse-output.sh |   74 ++++\n 5 files changed, 542 insertions(+), 158 deletions(-)\n\n-- \n1.7.3\n"},{"id":"166764","messageId":"1304099521-27617-2-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 1/6 (v2)] gitweb: Restructure projects list generation","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:51:56Z","receivedAt":"2011-04-29T17:51:56Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Extract filtering out forks (which is done if 'forks' feature is\nenabled) into filter_forks_from_projects_list subroutine, and\nsearching projects (via projects search form, or via content tags)\ninto search_projects_list subroutine.\n\nBoth are now run _before_ displaying projects, and not while printing;\nthis allow to know upfront if there were any found projects.  Gitweb\nnow can and do print 'No such projects found' if user searches for\nphrase which does not correspond to any project (any repository).\nThis also would allow splitting projects list into pages, if we so\ndesire.\n\nFiltering out forks and marking repository (project) as having forks\nis now consolidated into one subroutine (special case of handling\nforks in git_get_projects_list only for $projects_list being file is\nnow removed).  Forks handling is also cleaned up and simplified.\n$pr->{'forks'} now contains un-filled list of forks; we can now also\ndetect situation where the way for having forks is prepared, but there\nare no forks yet.\n\nSorting projects got also refactored in a very straight way (just\nmoving code) into sort_projects_list subroutine.\n\nThe interaction between forks, content tags and searching is now made\nmore explicit: searching whether by tag, or via search form turns off\nfork filtering (gitweb searches also forks, and will show all\nresults).  If 'ctags' feature is disabled, then searching by tag is\ntoo.\n\nThe t9500 test now includes some basic test for 'forks' and 'ctags'\nfeatures; the t9502 includes test checking if gitweb correctly filters\nout forks.\n\n\nGenerating list of projects by scanning given directory is now also a\nbit simplified wrt. handling filtering; it is byproduct of extracting\nfiltering forks to separate subroutine.\n\nWhile at it we now detect that there are no projects and respond with\n\"404 No projects found\" also for 'project_index' and 'opml' actions.\n\nHelped-by: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was sent to git mailing list as\n  \n  [PATCHv2/RFC] gitweb: Restructure projects list generation\n  http://thread.gmane.org/gmane.comp.version-control.git/167996/focus=168321\n\nThe main difference from previous version\n\n  [PATCH/RFC] gitweb: Restructure projects list generation\n  http://thread.gmane.org/gmane.comp.version-control.git/167996\n\nis that newly introduced filter_forks_from_projects_list() no longer\nrelies on the assumption that forked project immediately precedes all\nof its forks... which is not true when $porjects_list is a directory\nand we have e.g. such situation:\n\n  repo.git\n  repo.related.git\n  repo_other.git\n  repo/fork.git\n\nThis issue was noticed by Jonathan Nieder.\n\nfilter_forks_from_projects_list() uses trie[1] (prefix tree) to find\nwhich repositories are forks of other projects; directories are used\nas \"letters\" in trie.  Algorithm to create trie and find prefix match\nwas created with some help of code of Tree::Tree[2] Perl module from\nCPAN.\n\n[1]: http://en.wikipedia.org/wiki/Trie\n[2]: http://p3rl.org/Tree::Trie\n\nThis revision includes additionally tests in t9502 that correct\nprojects are filtered.\n\n\nNote that before this commit filtering out forks worked only for\n$projects_list being a file, it assumed that forks are always after\nforked project (not very unreasonable), and used algorithm which is\nO(<projects>^2).\n\nCurrent code filters out projects only where it should, regardless\nwhether $projects_list is directory or a file, and trie-based\nalgorithm is O(<projects>) + O(<projects forked>*<max depth>).\nCurrent code also consistently finds shortest prefix (important in\ncounting forks in case of forks of forks).\n\n\nThere remains a question whether $pr->{'forks'} should be a list of\nforks, or would having $pr->{'forks'} being number of forks be enough.\n\nRefactoring sort_projects_list and fixing handling of $project_maxdepth\ncould be done in a separate patches.\n\n gitweb/gitweb.perl                        |  250 ++++++++++++++++++++---------\n t/t9500-gitweb-standalone-no-errors.sh    |   49 ++++++\n t/t9502-gitweb-standalone-parse-output.sh |   74 +++++++++\n 3 files changed, 296 insertions(+), 77 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex ee69ea6..014b33b 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -2651,21 +2651,23 @@ sub git_get_project_url_list {\n }\n \n sub git_get_projects_list {\n-\tmy ($filter) = @_;\n+\tmy $filter = shift || '';\n \tmy @list;\n \n-\t$filter ||= '';\n \t$filter =~ s/\\.git$//;\n \n-\tmy $check_forks = gitweb_check_feature('forks');\n-\n \tif (-d $projects_list) {\n \t\t# search in directory\n-\t\tmy $dir = $projects_list . ($filter ? \"/$filter\" : '');\n+\t\tmy $dir = $projects_list;\n \t\t# remove the trailing \"/\"\n \t\t$dir =~ s!/+$!!;\n-\t\tmy $pfxlen = length(\"$dir\");\n-\t\tmy $pfxdepth = ($dir =~ tr!/!!);\n+\t\tmy $pfxlen = length(\"$projects_list\");\n+\t\tmy $pfxdepth = ($projects_list =~ tr!/!!);\n+\t\t# when filtering, search only given subdirectory\n+\t\tif ($filter) {\n+\t\t\t$dir .= \"/$filter\";\n+\t\t\t$dir =~ s!/+$!!;\n+\t\t}\n \n \t\tFile::Find::find({\n \t\t\tfollow_fast => 1, # follow symbolic links\n@@ -2680,14 +2682,14 @@ sub git_get_projects_list {\n \t\t\t\t# only directories can be git repositories\n \t\t\t\treturn unless (-d $_);\n \t\t\t\t# don't traverse too deep (Find is super slow on os x)\n+\t\t\t\t# $project_maxdepth excludes depth of $projectroot\n \t\t\t\tif (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {\n \t\t\t\t\t$File::Find::prune = 1;\n \t\t\t\t\treturn;\n \t\t\t\t}\n \n-\t\t\t\tmy $subdir = substr($File::Find::name, $pfxlen + 1);\n+\t\t\t\tmy $path = substr($File::Find::name, $pfxlen + 1);\n \t\t\t\t# we check related file in $projectroot\n-\t\t\t\tmy $path = ($filter ? \"$filter/\" : '') . $subdir;\n \t\t\t\tif (check_export_ok(\"$projectroot/$path\")) {\n \t\t\t\t\tpush @list, { path => $path };\n \t\t\t\t\t$File::Find::prune = 1;\n@@ -2700,7 +2702,6 @@ sub git_get_projects_list {\n \t\t# 'git%2Fgit.git Linus+Torvalds'\n \t\t# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'\n \t\t# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'\n-\t\tmy %paths;\n \t\topen my $fd, '<', $projects_list or return;\n \tPROJECT:\n \t\twhile (my $line = <$fd>) {\n@@ -2711,32 +2712,9 @@ sub git_get_projects_list {\n \t\t\tif (!defined $path) {\n \t\t\t\tnext;\n \t\t\t}\n-\t\t\tif ($filter ne '') {\n-\t\t\t\t# looking for forks;\n-\t\t\t\tmy $pfx = substr($path, 0, length($filter));\n-\t\t\t\tif ($pfx ne $filter) {\n-\t\t\t\t\tnext PROJECT;\n-\t\t\t\t}\n-\t\t\t\tmy $sfx = substr($path, length($filter));\n-\t\t\t\tif ($sfx !~ /^\\/.*\\.git$/) {\n-\t\t\t\t\tnext PROJECT;\n-\t\t\t\t}\n-\t\t\t} elsif ($check_forks) {\n-\t\t\tPATH:\n-\t\t\t\tforeach my $filter (keys %paths) {\n-\t\t\t\t\t# looking for forks;\n-\t\t\t\t\tmy $pfx = substr($path, 0, length($filter));\n-\t\t\t\t\tif ($pfx ne $filter) {\n-\t\t\t\t\t\tnext PATH;\n-\t\t\t\t\t}\n-\t\t\t\t\tmy $sfx = substr($path, length($filter));\n-\t\t\t\t\tif ($sfx !~ /^\\/.*\\.git$/) {\n-\t\t\t\t\t\tnext PATH;\n-\t\t\t\t\t}\n-\t\t\t\t\t# is a fork, don't include it in\n-\t\t\t\t\t# the list\n-\t\t\t\t\tnext PROJECT;\n-\t\t\t\t}\n+\t\t\t# if $filter is rpovided, check if $path begins with $filter\n+\t\t\tif ($filter && $path !~ m!^\\Q$filter\\E/!) {\n+\t\t\t\tnext;\n \t\t\t}\n \t\t\tif (check_export_ok(\"$projectroot/$path\")) {\n \t\t\t\tmy $pr = {\n@@ -2744,8 +2722,6 @@ sub git_get_projects_list {\n \t\t\t\t\towner => to_utf8($owner),\n \t\t\t\t};\n \t\t\t\tpush @list, $pr;\n-\t\t\t\t(my $forks_path = $path) =~ s/\\.git$//;\n-\t\t\t\t$paths{$forks_path}++;\n \t\t\t}\n \t\t}\n \t\tclose $fd;\n@@ -2753,6 +2729,98 @@ sub git_get_projects_list {\n \treturn @list;\n }\n \n+# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)\n+# as side effects it sets 'forks' field to list of forks for forked projects\n+sub filter_forks_from_projects_list {\n+\tmy $projects = shift;\n+\n+\tmy %trie; # prefix tree of directories (path components)\n+\t# generate trie out of those directories that might contain forks\n+\tforeach my $pr (@$projects) {\n+\t\tmy $path = $pr->{'path'};\n+\t\t$path =~ s/\\.git$//;      # forks of 'repo.git' are in 'repo/' directory\n+\t\tnext if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'\n+\t\tnext unless ($path);      # skip '.git' repository: tests, git-instaweb\n+\t\tnext unless (-d $path);   # containing directory exists\n+\t\t$pr->{'forks'} = [];      # there can be 0 or more forks of project\n+\n+\t\t# add to trie\n+\t\tmy @dirs = split('/', $path);\n+\t\t# walk the trie, until either runs out of components or out of trie\n+\t\tmy $ref = \\%trie;\n+\t\twhile (scalar @dirs &&\n+\t\t       exists($ref->{$dirs[0]})) {\n+\t\t\t$ref = $ref->{shift @dirs};\n+\t\t}\n+\t\t# create rest of trie structure from rest of components\n+\t\tforeach my $dir (@dirs) {\n+\t\t\t$ref = $ref->{$dir} = {};\n+\t\t}\n+\t\t# create end marker, store $pr as a data\n+\t\t$ref->{''} = $pr if (!exists $ref->{''});\n+\t}\n+\n+\t# filter out forks, by finding shortest prefix match for paths\n+\tmy @filtered;\n+ PROJECT:\n+\tforeach my $pr (@$projects) {\n+\t\t# trie lookup\n+\t\tmy $ref = \\%trie;\n+\tDIR:\n+\t\tforeach my $dir (split('/', $pr->{'path'})) {\n+\t\t\tif (exists $ref->{''}) {\n+\t\t\t\t# found [shortest] prefix, is a fork - skip it\n+\t\t\t\tpush @{$ref->{''}{'forks'}}, $pr;\n+\t\t\t\tnext PROJECT;\n+\t\t\t}\n+\t\t\tif (!exists $ref->{$dir}) {\n+\t\t\t\t# not in trie, cannot have prefix, not a fork\n+\t\t\t\tpush @filtered, $pr;\n+\t\t\t\tnext PROJECT;\n+\t\t\t}\n+\t\t\t# If the dir is there, we just walk one step down the trie.\n+\t\t\t$ref = $ref->{$dir};\n+\t\t}\n+\t\t# we ran out of trie\n+\t\t# (shouldn't happen: it's either no match, or end marker)\n+\t\tpush @filtered, $pr;\n+\t}\n+\n+\treturn @filtered;\n+}\n+\n+# note: fill_project_list_info must be run first,\n+# for 'descr_long' and 'ctags' to be filled\n+sub search_projects_list {\n+\tmy ($projlist, %opts) = @_;\n+\tmy $tagfilter  = $opts{'tagfilter'};\n+\tmy $searchtext = $opts{'searchtext'};\n+\n+\treturn @$projlist\n+\t\tunless ($tagfilter || $searchtext);\n+\n+\tmy @projects;\n+ PROJECT:\n+\tforeach my $pr (@$projlist) {\n+\n+\t\tif ($tagfilter) {\n+\t\t\tnext unless ref($pr->{'ctags'}) eq 'HASH';\n+\t\t\tnext unless\n+\t\t\t\tgrep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};\n+\t\t}\n+\n+\t\tif ($searchtext) {\n+\t\t\tnext unless\n+\t\t\t\t$pr->{'path'} =~ /$searchtext/ ||\n+\t\t\t\t$pr->{'descr_long'} =~ /$searchtext/;\n+\t\t}\n+\n+\t\tpush @projects, $pr;\n+\t}\n+\n+\treturn @projects;\n+}\n+\n our $gitweb_project_owner = undef;\n sub git_get_project_list_from_file {\n \n@@ -4742,7 +4810,7 @@ sub git_patchset_body {\n # project in the list, removing invalid projects from returned list\n # NOTE: modifies $projlist, but does not remove entries from it\n sub fill_project_list_info {\n-\tmy ($projlist, $check_forks) = @_;\n+\tmy $projlist = shift;\n \tmy @projects;\n \n \tmy $show_ctags = gitweb_check_feature('ctags');\n@@ -4762,23 +4830,36 @@ sub fill_project_list_info {\n \t\tif (!defined $pr->{'owner'}) {\n \t\t\t$pr->{'owner'} = git_get_project_owner(\"$pr->{'path'}\") || \"\";\n \t\t}\n-\t\tif ($check_forks) {\n-\t\t\tmy $pname = $pr->{'path'};\n-\t\t\tif (($pname =~ s/\\.git$//) &&\n-\t\t\t    ($pname !~ /\\/$/) &&\n-\t\t\t    (-d \"$projectroot/$pname\")) {\n-\t\t\t\t$pr->{'forks'} = \"-d $projectroot/$pname\";\n-\t\t\t} else {\n-\t\t\t\t$pr->{'forks'} = 0;\n-\t\t\t}\n+\t\tif ($show_ctags) {\n+\t\t\t$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});\n \t\t}\n-\t\t$show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});\n \t\tpush @projects, $pr;\n \t}\n \n \treturn @projects;\n }\n \n+sub sort_projects_list {\n+\tmy ($projlist, $order) = @_;\n+\tmy @projects;\n+\n+\tmy %order_info = (\n+\t\tproject => { key => 'path', type => 'str' },\n+\t\tdescr => { key => 'descr_long', type => 'str' },\n+\t\towner => { key => 'owner', type => 'str' },\n+\t\tage => { key => 'age', type => 'num' }\n+\t);\n+\tmy $oi = $order_info{$order};\n+\treturn @$projlist unless defined $oi;\n+\tif ($oi->{'type'} eq 'str') {\n+\t\t@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;\n+\t} else {\n+\t\t@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;\n+\t}\n+\n+\treturn @projects;\n+}\n+\n # print 'sort by' <th> element, generating 'sort by $name' replay link\n # if that order is not selected\n sub print_sort_th {\n@@ -4805,28 +4886,39 @@ sub format_sort_th {\n sub git_project_list_body {\n \t# actually uses global variable $project\n \tmy ($projlist, $order, $from, $to, $extra, $no_header) = @_;\n+\tmy @projects = @$projlist;\n \n \tmy $check_forks = gitweb_check_feature('forks');\n-\tmy @projects = fill_project_list_info($projlist, $check_forks);\n+\tmy $show_ctags  = gitweb_check_feature('ctags');\n+\tmy $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;\n+\t$check_forks = undef\n+\t\tif ($tagfilter || $searchtext);\n+\n+\t# filtering out forks before filling info allows to do less work\n+\t@projects = filter_forks_from_projects_list(\\@projects)\n+\t\tif ($check_forks);\n+\t@projects = fill_project_list_info(\\@projects);\n+\t# searching projects require filling to be run before it\n+\t@projects = search_projects_list(\\@projects,\n+\t                                 'searchtext' => $searchtext,\n+\t                                 'tagfilter'  => $tagfilter)\n+\t\tif ($tagfilter || $searchtext);\n \n \t$order ||= $default_projects_order;\n \t$from = 0 unless defined $from;\n \t$to = $#projects if (!defined $to || $#projects < $to);\n \n-\tmy %order_info = (\n-\t\tproject => { key => 'path', type => 'str' },\n-\t\tdescr => { key => 'descr_long', type => 'str' },\n-\t\towner => { key => 'owner', type => 'str' },\n-\t\tage => { key => 'age', type => 'num' }\n-\t);\n-\tmy $oi = $order_info{$order};\n-\tif ($oi->{'type'} eq 'str') {\n-\t\t@projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;\n-\t} else {\n-\t\t@projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;\n+\t# short circuit\n+\tif ($from > $to) {\n+\t\tprint \"<center>\\n\".\n+\t\t      \"<b>No such projects found</b><br />\\n\".\n+\t\t      \"Click \".$cgi->a({-href=>href(project=>undef)},\"here\").\" to view all projects<br />\\n\".\n+\t\t      \"</center>\\n<br />\\n\";\n+\t\treturn;\n \t}\n \n-\tmy $show_ctags = gitweb_check_feature('ctags');\n+\t@projects = sort_projects_list(\\@projects, $order);\n+\n \tif ($show_ctags) {\n \t\tmy %ctags;\n \t\tforeach my $p (@projects) {\n@@ -4852,32 +4944,26 @@ sub git_project_list_body {\n \t\t      \"</tr>\\n\";\n \t}\n \tmy $alternate = 1;\n-\tmy $tagfilter = $cgi->param('by_tag');\n \tfor (my $i = $from; $i <= $to; $i++) {\n \t\tmy $pr = $projects[$i];\n \n-\t\tnext if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};\n-\t\tnext if $searchtext and not $pr->{'path'} =~ /$searchtext/\n-\t\t\tand not $pr->{'descr_long'} =~ /$searchtext/;\n-\t\t# Weed out forks or non-matching entries of search\n-\t\tif ($check_forks) {\n-\t\t\tmy $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\\.git$#/#;\n-\t\t\t$forkbase=\"^$forkbase\" if $forkbase;\n-\t\t\tnext if not $searchtext and not $tagfilter and $show_ctags\n-\t\t\t\tand $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe\n-\t\t}\n-\n \t\tif ($alternate) {\n \t\t\tprint \"<tr class=\\\"dark\\\">\\n\";\n \t\t} else {\n \t\t\tprint \"<tr class=\\\"light\\\">\\n\";\n \t\t}\n \t\t$alternate ^= 1;\n+\n \t\tif ($check_forks) {\n \t\t\tprint \"<td>\";\n \t\t\tif ($pr->{'forks'}) {\n-\t\t\t\tprint \"<!-- $pr->{'forks'} -->\\n\";\n-\t\t\t\tprint $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\")}, \"+\");\n+\t\t\t\tmy $nforks = scalar @{$pr->{'forks'}};\n+\t\t\t\tif ($nforks > 0) {\n+\t\t\t\t\tprint $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\"),\n+\t\t\t\t\t               -title => \"$nforks forks\"}, \"+\");\n+\t\t\t\t} else {\n+\t\t\t\t\tprint $cgi->span({-title => \"$nforks forks\"}, \"+\");\n+\t\t\t\t}\n \t\t\t}\n \t\t\tprint \"</td>\\n\";\n \t\t}\n@@ -5357,7 +5443,10 @@ sub git_forks {\n }\n \n sub git_project_index {\n-\tmy @projects = git_get_projects_list($project);\n+\tmy @projects = git_get_projects_list();\n+\tif (!@projects) {\n+\t\tdie_error(404, \"No projects found\");\n+\t}\n \n \tprint $cgi->header(\n \t\t-type => 'text/plain',\n@@ -5399,7 +5488,11 @@ sub git_summary {\n \tmy $check_forks = gitweb_check_feature('forks');\n \n \tif ($check_forks) {\n+\t\t# find forks of a project\n \t\t@forklist = git_get_projects_list($project);\n+\t\t# filter out forks of forks\n+\t\t@forklist = filter_forks_from_projects_list(\\@forklist)\n+\t\t\tif (@forklist);\n \t}\n \n \tgit_header_html();\n@@ -7319,6 +7412,9 @@ sub git_atom {\n \n sub git_opml {\n \tmy @list = git_get_projects_list();\n+\tif (!@list) {\n+\t\tdie_error(404, \"No projects found\");\n+\t}\n \n \tprint $cgi->header(\n \t\t-type => 'text/xml',\ndiff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh\nindex afac5b5..71ef0ac 100755\n--- a/t/t9500-gitweb-standalone-no-errors.sh\n+++ b/t/t9500-gitweb-standalone-no-errors.sh\n@@ -595,4 +595,53 @@ test_expect_success HIGHLIGHT \\\n \t git commit -m \"Add test.sh\" &&\n \t gitweb_run \"p=.git;a=blob;f=test.sh\"'\n \n+# ----------------------------------------------------------------------\n+# forks of projects\n+\n+cat >>gitweb_config.perl <<\\EOF &&\n+$feature{'forks'}{'default'} = [1];\n+EOF\n+\n+test_expect_success \\\n+\t'forks: prepare' \\\n+\t'git init --bare foo.git &&\n+\t git --git-dir=foo.git --work-tree=. add file &&\n+\t git --git-dir=foo.git --work-tree=. commit -m \"Initial commit\" &&\n+\t echo \"foo\" > foo.git/description &&\n+\t mkdir -p foo &&\n+\t (cd foo &&\n+\t  git clone --shared --bare ../foo.git foo-forked.git &&\n+\t  echo \"fork of foo\" > foo-forked.git/description)'\n+\n+test_expect_success \\\n+\t'forks: projects list' \\\n+\t'gitweb_run'\n+\n+test_expect_success \\\n+\t'forks: forks action' \\\n+\t'gitweb_run \"p=foo.git;a=forks\"'\n+\n+# ----------------------------------------------------------------------\n+# content tags (tag cloud)\n+\n+cat >>gitweb_config.perl <<-\\EOF &&\n+# we don't test _setting_ content tags, so any true value is good\n+$feature{'ctags'}{'default'} = ['ctags_script.cgi'];\n+EOF\n+\n+test_expect_success \\\n+\t'ctags: tag cloud in projects list' \\\n+\t'mkdir .git/ctags &&\n+\t echo \"2\" > .git/ctags/foo &&\n+\t echo \"1\" > .git/ctags/bar &&\n+\tgitweb_run'\n+\n+test_expect_success \\\n+\t'ctags: search projects by existing tag' \\\n+\t'gitweb_run \"by_tag=foo\"'\n+\n+test_expect_success \\\n+\t'ctags: search projects by non existent tag' \\\n+\t'gitweb_run \"by_tag=non-existent\"'\n+\n test_done\ndiff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh\nindex dd83890..731e64c 100755\n--- a/t/t9502-gitweb-standalone-parse-output.sh\n+++ b/t/t9502-gitweb-standalone-parse-output.sh\n@@ -112,4 +112,78 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '\n '\n test_debug 'cat gitweb.headers'\n \n+# ----------------------------------------------------------------------\n+# forks of projects\n+\n+test_expect_success 'forks: setup' '\n+\tgit init --bare foo.git &&\n+\techo file > file &&\n+\tgit --git-dir=foo.git --work-tree=. add file &&\n+\tgit --git-dir=foo.git --work-tree=. commit -m \"Initial commit\" &&\n+\techo \"foo\" > foo.git/description &&\n+\tgit clone --bare foo.git foo.bar.git &&\n+\techo \"foo.bar\" > foo.bar.git/description &&\n+\tgit clone --bare foo.git foo_baz.git &&\n+\techo \"foo_baz\" > foo_baz.git/description &&\n+\trm -fr   foo &&\n+\tmkdir -p foo &&\n+\t(\n+\t\tcd foo &&\n+\t\tgit clone --shared --bare ../foo.git foo-forked.git &&\n+\t\techo \"fork of foo\" > foo-forked.git/description\n+\t)\n+'\n+\n+test_expect_success 'forks: not skipped unless \"forks\" feature enabled' '\n+\tgitweb_run \"a=project_list\" &&\n+\tgrep -q \">\\\\.git<\"               gitweb.body &&\n+\tgrep -q \">foo\\\\.git<\"            gitweb.body &&\n+\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n+\tgrep -q \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n+\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n+\tgrep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n+\tgrep -q \">fork of .*<\"           gitweb.body\n+'\n+\n+cat >>gitweb_config.perl <<\\EOF &&\n+$feature{'forks'}{'default'} = [1];\n+EOF\n+\n+test_expect_success 'forks: forks skipped if \"forks\" feature enabled' '\n+\tgitweb_run \"a=project_list\" &&\n+\tgrep -q \">\\\\.git<\"               gitweb.body &&\n+\tgrep -q \">foo\\\\.git<\"            gitweb.body &&\n+\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n+\tgrep -q \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n+\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n+\tgrep -v \">foo/foo-forked\\\\.git<\" gitweb.body &&\n+\tgrep -v \">fork of .*<\"           gitweb.body\n+'\n+\n+test_expect_success 'forks: \"forks\" action for forked repository' '\n+\tgitweb_run \"p=foo.git;a=forks\" &&\n+\tgrep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n+\tgrep -q \">fork of foo<\"          gitweb.body\n+'\n+\n+test_expect_success 'forks: can access forked repository' '\n+\tgitweb_run \"p=foo/foo-forked.git;a=summary\" &&\n+\tgrep -q \"200 OK\"        gitweb.headers &&\n+\tgrep -q \">fork of foo<\" gitweb.body\n+'\n+\n+test_expect_success 'forks: project_index lists all projects (incl. forks)' '\n+\tcat >expected <<-\\EOF\n+\t.git\n+\tfoo.bar.git\n+\tfoo.git\n+\tfoo/foo-forked.git\n+\tfoo_baz.git\n+\tEOF\n+\tgitweb_run \"a=project_index\" &&\n+\tsed -e \"s/ .*//\" <gitweb.body | sort >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+\n test_done\n-- \n1.7.3\n"},{"id":"166761","messageId":"1304099521-27617-3-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 2/6] gitweb: Change the way \"content tags\" ('ctags') are handled","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:51:57Z","receivedAt":"2011-04-29T17:51:57Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"The major change is removing the ability to edit content tags (ctags)\nin a web browser.\n\nThe interface was created by gitweb, while actual editing of tags was\nto be done by external script; the API was not defined, and neither\nwas provided example implementation.  Such split is also a bit fragile\n- interface and implementation have to be kept in sync.  Gitweb\nprovided only ability to add tags; you could not edit tags nor delete\nthem.\n\n\nFormat of ctags is now described in the comment above git_get_project_ctags\nsubroutine.  Gitweb now is more robust with respect to original ctags\nformat; it also accepts two new formats: $GIT_DIR/ctags file, with one\ncontent tag per line, and multi-value `gitweb.ctag' config variable.\n\nGathering all ctags of all project is now put in git_gather_all_ctags\nsubroutine, making git_project_list_body more clear.\n\ngit_populate_project_tagcloud subroutine now generates data used for\ntag cloud, including generation of ctag link, also in the case\nHTML::TagCloud module is unavailable.  Links are now generated using\nhref() subroutine - this is more robust, as ctags might contain '?',\n';' and '=' special characters that need to be escaped in query param.\nShown tags are HTML-escaped.\n\nThe generation of tag cloud in git_show_project_tagcloud in the case\nwhen HTML::TagCloud is not available is now changed slightly.\n\nThe 'content tags' field on project summary page is made more in line\nwith other fields in \"projects_list\" table.  Because one cannot now\nadd new tags from web interface, this field is no longer displayed\nwhen there are no content tags for given project.\n\nCtags-issue-Reported-by: Uwe Kleine-König <u.kleine-koenig@pengutronix.de>\nCtags-issue-Reported-by: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was originally sent to git mailing list in\n\n  [RFC/PATCH] gitweb: Change the way \"content tags\" ('ctags') are handled\n  http://thread.gmane.org/gmane.linux.debian.devel.bugs.general/802865/focus=168599\n\nas part of\n\n  gitweb: cloud tags feature produces malformed XML for errors\n  http://thread.gmane.org/gmane.linux.debian.devel.bugs.general/802865/focus=168266\n\nthread, crossposted to git mailing list.\n\n\nHardening parsing of ctags files, so that gitweb does not crash on\nmalformed entries, but just ignores them, sort of fixes original issue\nreported in\n\n  Bug#616005: libhtml-tagcloud-perl breaks gitweb\n  http://thread.gmane.org/gmane.linux.debian.devel.bugs.general/802865/\n  http://bugs.debian.org/616005\n\nby Uwe Kleine-König and crossposted to git mailing list by Jonathan\nNieder.\n\nI say \"sort of fixes\" because this patch doesn't attempt to solve\ndeeper fundamental problem with error / exception handling in gitweb\nafter some data was already sent to a browser.\n\nNote also that the bug was caused by mishandling (incorrectly\ngenerating) underdocumented then 'ctags' feature.\n\n gitweb/gitweb.perl |  141 +++++++++++++++++++++++++++++++++++----------------\n 1 files changed, 97 insertions(+), 44 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 014b33b..60cb772 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -412,20 +412,23 @@ our %feature = (\n \t\t'override' => 0,\n \t\t'default' => []},\n \n-\t# Allow gitweb scan project content tags described in ctags/\n-\t# of project repository, and display the popular Web 2.0-ish\n-\t# \"tag cloud\" near the project list. Note that this is something\n-\t# COMPLETELY different from the normal Git tags.\n+\t# Allow gitweb scan project content tags of project repository,\n+\t# and display the popular Web 2.0-ish \"tag cloud\" near the projects\n+\t# list.  Note that this is something COMPLETELY different from the\n+\t# normal Git tags.\n \n \t# gitweb by itself can show existing tags, but it does not handle\n-\t# tagging itself; you need an external application for that.\n-\t# For an example script, check Girocco's cgi/tagproj.cgi.\n+\t# tagging itself; you need to do it externally, outside gitweb.\n+\t# The format is described in git_get_project_ctags() subroutine.\n \t# You may want to install the HTML::TagCloud Perl module to get\n \t# a pretty tag cloud instead of just a list of tags.\n \n \t# To enable system wide have in $GITWEB_CONFIG\n-\t# $feature{'ctags'}{'default'} = ['path_to_tag_script'];\n+\t# $feature{'ctags'}{'default'} = [1];\n \t# Project specific override is not supported.\n+\n+\t# In the future whether ctags editing is enabled might depend\n+\t# on the value, but using 1 should always mean no editing of ctags.\n \t'ctags' => {\n \t\t'override' => 0,\n \t\t'default' => [0]},\n@@ -703,6 +706,7 @@ our @cgi_param_mapping = (\n \tsnapshot_format => \"sf\",\n \textra_options => \"opt\",\n \tsearch_use_regexp => \"sr\",\n+\tctag => \"by_tag\",\n \t# this must be last entry (for manipulation from JavaScript)\n \tjavascript => \"js\"\n );\n@@ -2572,23 +2576,66 @@ sub git_get_project_description {\n \treturn $descr;\n }\n \n+# supported formats:\n+# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)\n+#   - if its contents is a number, use it as tag weight,\n+#   - otherwise add a tag with weight 1\n+# * $GIT_DIR/ctags file, each line is a tag (with weight 1)\n+#   the same value multiple times increases tag weight\n+# * `gitweb.ctag' multi-valued repo config variable\n sub git_get_project_ctags {\n-\tmy $path = shift;\n+\tmy $project = shift;\n \tmy $ctags = {};\n \n-\t$git_dir = \"$projectroot/$path\";\n-\topendir my $dh, \"$git_dir/ctags\"\n-\t\tor return $ctags;\n-\tforeach (grep { -f $_ } map { \"$git_dir/ctags/$_\" } readdir($dh)) {\n-\t\topen my $ct, '<', $_ or next;\n-\t\tmy $val = <$ct>;\n-\t\tchomp $val;\n-\t\tclose $ct;\n-\t\tmy $ctag = $_; $ctag =~ s#.*/##;\n-\t\t$ctags->{$ctag} = $val;\n+\t$git_dir = \"$projectroot/$project\";\n+\tif (opendir my $dh, \"$git_dir/ctags\") {\n+\t\tmy @files = grep { -f $_ } map { \"$git_dir/ctags/$_\" } readdir($dh);\n+\t\tforeach my $tagfile (@files) {\n+\t\t\topen my $ct, '<', $tagfile\n+\t\t\t\tor next;\n+\t\t\tmy $val = <$ct>;\n+\t\t\tchomp $val if $val;\n+\t\t\tclose $ct;\n+\n+\t\t\t(my $ctag = $tagfile) =~ s#.*/##;\n+\t\t\tif ($val =~ /\\d+/) {\n+\t\t\t\t$ctags->{$ctag} = $val;\n+\t\t\t} else {\n+\t\t\t\t$ctags->{$ctag} = 1;\n+\t\t\t}\n+\t\t}\n+\t\tclosedir $dh;\n+\n+\t} elsif (open my $fh, '<', \"$git_dir/ctags\") {\n+\t\twhile (my $line = <$fh>) {\n+\t\t\tchomp $line;\n+\t\t\t$ctags->{$line}++ if $line;\n+\t\t}\n+\t\tclose $fh;\n+\n+\t} else {\n+\t\tmy $taglist = config_to_multi(git_get_project_config('ctag'));\n+\t\tforeach my $tag (@$taglist) {\n+\t\t\t$ctags->{$tag}++;\n+\t\t}\n \t}\n-\tclosedir $dh;\n-\t$ctags;\n+\n+\treturn $ctags;\n+}\n+\n+# return hash, where keys are content tags ('ctags'),\n+# and values are sum of weights of given tag in every project\n+sub git_gather_all_ctags {\n+\tmy $projects = shift;\n+\tmy $ctags = {};\n+\n+\tforeach my $p (@$projects) {\n+\t\tforeach my $ct (keys %{$p->{'ctags'}}) {\n+\t\t\t$ctags->{$ct} += $p->{'ctags'}->{$ct};\n+\t\t}\n+\t}\n+\n+\treturn $ctags;\n }\n \n sub git_populate_project_tagcloud {\n@@ -2608,31 +2655,41 @@ sub git_populate_project_tagcloud {\n \tmy $cloud;\n \tif (eval { require HTML::TagCloud; 1; }) {\n \t\t$cloud = HTML::TagCloud->new;\n-\t\tforeach (sort keys %ctags_lc) {\n+\t\tforeach my $ctag (sort keys %ctags_lc) {\n \t\t\t# Pad the title with spaces so that the cloud looks\n \t\t\t# less crammed.\n-\t\t\tmy $title = $ctags_lc{$_}->{topname};\n+\t\t\tmy $title = esc_html($ctags_lc{$ctag}->{topname});\n \t\t\t$title =~ s/ /&nbsp;/g;\n \t\t\t$title =~ s/^/&nbsp;/g;\n \t\t\t$title =~ s/$/&nbsp;/g;\n-\t\t\t$cloud->add($title, $home_link.\"?by_tag=\".$_, $ctags_lc{$_}->{count});\n+\t\t\t$cloud->add($title, href(project=>undef, ctag=>$ctag),\n+\t\t\t            $ctags_lc{$ctag}->{count});\n \t\t}\n \t} else {\n-\t\t$cloud = \\%ctags_lc;\n+\t\t$cloud = {};\n+\t\tforeach my $ctag (keys %ctags_lc) {\n+\t\t\tmy $title = $ctags_lc{$ctag}->{topname};\n+\t\t\t$cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};\n+\t\t\t$cloud->{$ctag}{ctag} =\n+\t\t\t\t$cgi->a({-href=>href(project=>undef, ctag=>$ctag)},\n+\t\t\t          esc_html($title, -nbsp=>1));\n+\t\t}\n \t}\n-\t$cloud;\n+\treturn $cloud;\n }\n \n sub git_show_project_tagcloud {\n \tmy ($cloud, $count) = @_;\n-\tprint STDERR ref($cloud).\"..\\n\";\n \tif (ref $cloud eq 'HTML::TagCloud') {\n \t\treturn $cloud->html_and_css($count);\n \t} else {\n-\t\tmy @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;\n-\t\treturn '<p align=\"center\">' . join (', ', map {\n-\t\t\t$cgi->a({-href=>\"$home_link?by_tag=$_\"}, $cloud->{$_}->{topname})\n-\t\t} splice(@tags, 0, $count)) . '</p>';\n+\t\tmy @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;\n+\t\treturn\n+\t\t\t'<div id=\"htmltagcloud\"'.($project ? '' : ' align=\"center\"').'>' .\n+\t\t\tjoin (', ', map {\n+\t\t\t\t$cloud->{$_}->{'ctag'}\n+\t\t\t} splice(@tags, 0, $count)) .\n+\t\t\t'</div>';\n \t}\n }\n \n@@ -4920,13 +4977,8 @@ sub git_project_list_body {\n \t@projects = sort_projects_list(\\@projects, $order);\n \n \tif ($show_ctags) {\n-\t\tmy %ctags;\n-\t\tforeach my $p (@projects) {\n-\t\t\tforeach my $ct (keys %{$p->{'ctags'}}) {\n-\t\t\t\t$ctags{$ct} += $p->{'ctags'}->{$ct};\n-\t\t\t}\n-\t\t}\n-\t\tmy $cloud = git_populate_project_tagcloud(\\%ctags);\n+\t\tmy $ctags = git_gather_all_ctags(\\@projects);\n+\t\tmy $cloud = git_populate_project_tagcloud($ctags);\n \t\tprint git_show_project_tagcloud($cloud, 64);\n \t}\n \n@@ -5521,13 +5573,14 @@ sub git_summary {\n \tmy $show_ctags = gitweb_check_feature('ctags');\n \tif ($show_ctags) {\n \t\tmy $ctags = git_get_project_ctags($project);\n-\t\tmy $cloud = git_populate_project_tagcloud($ctags);\n-\t\tprint \"<tr id=\\\"metadata_ctags\\\"><td>Content tags:<br />\";\n-\t\tprint \"</td>\\n<td>\" unless %$ctags;\n-\t\tprint \"<form action=\\\"$show_ctags\\\" method=\\\"post\\\"><input type=\\\"hidden\\\" name=\\\"p\\\" value=\\\"$project\\\" />Add: <input type=\\\"text\\\" name=\\\"t\\\" size=\\\"8\\\" /></form>\";\n-\t\tprint \"</td>\\n<td>\" if %$ctags;\n-\t\tprint git_show_project_tagcloud($cloud, 48);\n-\t\tprint \"</td></tr>\";\n+\t\tif (%$ctags) {\n+\t\t\t# without ability to add tags, don't show if there are none\n+\t\t\tmy $cloud = git_populate_project_tagcloud($ctags);\n+\t\t\tprint \"<tr id=\\\"metadata_ctags\\\">\" .\n+\t\t\t      \"<td>content tags</td>\" .\n+\t\t\t      \"<td>\".git_show_project_tagcloud($cloud, 48).\"</td>\" .\n+\t\t\t      \"</tr>\\n\";\n+\t\t}\n \t}\n \n \tprint \"</table>\\n\";\n-- \n1.7.3\n"},{"id":"166765","messageId":"1304099521-27617-4-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 3/6] gitweb: Mark matched 'ctag' / contents tag (?by_tag=foo)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:51:58Z","receivedAt":"2011-04-29T17:51:58Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"It might have been hard to discover that current view is limited to\nprojects with given content tag (ctag), as it was distinquished only\nin gitweb URL.  Mark matched contents tag in the tag cloud using\n\"match\" class, for easier discovery.\n\nThis commit introduces a bit of further code duplication in\ngit_populate_project_tagcloud().\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\nAcked-by: Petr Baudis <pasky@suse.cz>\n---\nThis patch was originally sent to git mailing list as\n\n  [PATCH 2/1] gitweb: Mark matched 'ctag' / contents tag (?by_tag=foo)\n  http://thread.gmane.org/gmane.linux.debian.devel.bugs.general/802865/focus=168731\n\n(yes, it was crossposted).\n\n gitweb/gitweb.perl |   12 +++++++++---\n 1 files changed, 9 insertions(+), 3 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 60cb772..e81c27d 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -2653,6 +2653,7 @@ sub git_populate_project_tagcloud {\n \t}\n \n \tmy $cloud;\n+\tmy $matched = $cgi->param('by_tag');\n \tif (eval { require HTML::TagCloud; 1; }) {\n \t\t$cloud = HTML::TagCloud->new;\n \t\tforeach my $ctag (sort keys %ctags_lc) {\n@@ -2662,17 +2663,22 @@ sub git_populate_project_tagcloud {\n \t\t\t$title =~ s/ /&nbsp;/g;\n \t\t\t$title =~ s/^/&nbsp;/g;\n \t\t\t$title =~ s/$/&nbsp;/g;\n+\t\t\tif (defined $matched && $matched eq $ctag) {\n+\t\t\t\t$title = qq(<span class=\"match\">$title</span>);\n+\t\t\t}\n \t\t\t$cloud->add($title, href(project=>undef, ctag=>$ctag),\n \t\t\t            $ctags_lc{$ctag}->{count});\n \t\t}\n \t} else {\n \t\t$cloud = {};\n \t\tforeach my $ctag (keys %ctags_lc) {\n-\t\t\tmy $title = $ctags_lc{$ctag}->{topname};\n+\t\t\tmy $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);\n+\t\t\tif (defined $matched && $matched eq $ctag) {\n+\t\t\t\t$title = qq(<span class=\"match\">$title</span>);\n+\t\t\t}\n \t\t\t$cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};\n \t\t\t$cloud->{$ctag}{ctag} =\n-\t\t\t\t$cgi->a({-href=>href(project=>undef, ctag=>$ctag)},\n-\t\t\t          esc_html($title, -nbsp=>1));\n+\t\t\t\t$cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);\n \t\t}\n \t}\n \treturn $cloud;\n-- \n1.7.3\n"},{"id":"166763","messageId":"1304099521-27617-5-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 4/6] gitweb: Split git_project_list_body in two functions","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:51:59Z","receivedAt":"2011-04-29T17:51:59Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"From: Sebastien Cevey <seb@cine7.net>\n\nExtract the printing of project rows (body/contents of projects list\ntable) on the 'project_list' page into a separate git_project_list_rows\nfunction. This makes it easier to reuse the code to print different\nsubsets of the whole project list.\n\n[jn: Updated to post restructuring projects list generation]\n\nSigned-off-by: Sebastien Cevey <seb@cine7.net>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch and subsequent patches were sent to git mailing list\n(before rebasing on top of 'ctags' improvements) as\n\n  [RFC/PATCH 0/3] gitweb: Categories support\n  http://thread.gmane.org/gmane.comp.version-control.git/168616\n\n\nThis subseries is long in wait port of Sebastien Cevey series from\nDecember 2008\n\n  http://thread.gmane.org/gmane.comp.version-control.git/102844\n\nto modern gitweb.  It was waiting for restructuring of projects list\ngeneration that is part of this series.\n\nUwe, it might be an alternative to use of ctags (content tags, aka\nproject labels).\n\n gitweb/gitweb.perl |   89 +++++++++++++++++++++++++++++-----------------------\n 1 files changed, 50 insertions(+), 39 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e81c27d..f8d5722 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -4946,6 +4946,55 @@ sub format_sort_th {\n \treturn $sort_th;\n }\n \n+sub git_project_list_rows {\n+\tmy ($projlist, $from, $to, $check_forks) = @_;\n+\n+\t$from = 0 unless defined $from;\n+\t$to = $#$projlist if (!defined $to || $#$projlist < $to);\n+\n+\tmy $alternate = 1;\n+\tfor (my $i = $from; $i <= $to; $i++) {\n+\t\tmy $pr = $projlist->[$i];\n+\n+\t\tif ($alternate) {\n+\t\t\tprint \"<tr class=\\\"dark\\\">\\n\";\n+\t\t} else {\n+\t\t\tprint \"<tr class=\\\"light\\\">\\n\";\n+\t\t}\n+\t\t$alternate ^= 1;\n+\n+\t\tif ($check_forks) {\n+\t\t\tprint \"<td>\";\n+\t\t\tif ($pr->{'forks'}) {\n+\t\t\t\tmy $nforks = scalar @{$pr->{'forks'}};\n+\t\t\t\tif ($nforks > 0) {\n+\t\t\t\t\tprint $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\"),\n+\t\t\t\t\t               -title => \"$nforks forks\"}, \"+\");\n+\t\t\t\t} else {\n+\t\t\t\t\tprint $cgi->span({-title => \"$nforks forks\"}, \"+\");\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tprint \"</td>\\n\";\n+\t\t}\n+\t\tprint \"<td>\" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\"),\n+\t\t                        -class => \"list\"}, esc_html($pr->{'path'})) . \"</td>\\n\" .\n+\t\t      \"<td>\" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\"),\n+\t\t                        -class => \"list\", -title => $pr->{'descr_long'}},\n+\t\t                        esc_html($pr->{'descr'})) . \"</td>\\n\" .\n+\t\t      \"<td><i>\" . chop_and_escape_str($pr->{'owner'}, 15) . \"</i></td>\\n\";\n+\t\tprint \"<td class=\\\"\". age_class($pr->{'age'}) . \"\\\">\" .\n+\t\t      (defined $pr->{'age_string'} ? $pr->{'age_string'} : \"No commits\") . \"</td>\\n\" .\n+\t\t      \"<td class=\\\"link\\\">\" .\n+\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\")}, \"summary\")   . \" | \" .\n+\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"shortlog\")}, \"shortlog\") . \" | \" .\n+\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"log\")}, \"log\") . \" | \" .\n+\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"tree\")}, \"tree\") .\n+\t\t      ($pr->{'forks'} ? \" | \" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\")}, \"forks\") : '') .\n+\t\t      \"</td>\\n\" .\n+\t\t      \"</tr>\\n\";\n+\t}\n+}\n+\n sub git_project_list_body {\n \t# actually uses global variable $project\n \tmy ($projlist, $order, $from, $to, $extra, $no_header) = @_;\n@@ -5001,47 +5050,9 @@ sub git_project_list_body {\n \t\tprint \"<th></th>\\n\" . # for links\n \t\t      \"</tr>\\n\";\n \t}\n-\tmy $alternate = 1;\n-\tfor (my $i = $from; $i <= $to; $i++) {\n-\t\tmy $pr = $projects[$i];\n \n-\t\tif ($alternate) {\n-\t\t\tprint \"<tr class=\\\"dark\\\">\\n\";\n-\t\t} else {\n-\t\t\tprint \"<tr class=\\\"light\\\">\\n\";\n-\t\t}\n-\t\t$alternate ^= 1;\n+\tgit_project_list_rows(\\@projects, $from, $to, $check_forks);\n \n-\t\tif ($check_forks) {\n-\t\t\tprint \"<td>\";\n-\t\t\tif ($pr->{'forks'}) {\n-\t\t\t\tmy $nforks = scalar @{$pr->{'forks'}};\n-\t\t\t\tif ($nforks > 0) {\n-\t\t\t\t\tprint $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\"),\n-\t\t\t\t\t               -title => \"$nforks forks\"}, \"+\");\n-\t\t\t\t} else {\n-\t\t\t\t\tprint $cgi->span({-title => \"$nforks forks\"}, \"+\");\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tprint \"</td>\\n\";\n-\t\t}\n-\t\tprint \"<td>\" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\"),\n-\t\t                        -class => \"list\"}, esc_html($pr->{'path'})) . \"</td>\\n\" .\n-\t\t      \"<td>\" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\"),\n-\t\t                        -class => \"list\", -title => $pr->{'descr_long'}},\n-\t\t                        esc_html($pr->{'descr'})) . \"</td>\\n\" .\n-\t\t      \"<td><i>\" . chop_and_escape_str($pr->{'owner'}, 15) . \"</i></td>\\n\";\n-\t\tprint \"<td class=\\\"\". age_class($pr->{'age'}) . \"\\\">\" .\n-\t\t      (defined $pr->{'age_string'} ? $pr->{'age_string'} : \"No commits\") . \"</td>\\n\" .\n-\t\t      \"<td class=\\\"link\\\">\" .\n-\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"summary\")}, \"summary\")   . \" | \" .\n-\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"shortlog\")}, \"shortlog\") . \" | \" .\n-\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"log\")}, \"log\") . \" | \" .\n-\t\t      $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"tree\")}, \"tree\") .\n-\t\t      ($pr->{'forks'} ? \" | \" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>\"forks\")}, \"forks\") : '') .\n-\t\t      \"</td>\\n\" .\n-\t\t      \"</tr>\\n\";\n-\t}\n \tif (defined $extra) {\n \t\tprint \"<tr>\\n\";\n \t\tif ($check_forks) {\n-- \n1.7.3\n"},{"id":"166760","messageId":"1304099521-27617-6-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 5/6] gitweb: Modularized git_get_project_description to be more generic","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:52:00Z","receivedAt":"2011-04-29T17:52:00Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"From: Sebastien Cevey <seb@cine7.net>\n\nIntroduce a git_get_file_or_project_config utility function to\nretrieve a repository variable either from a plain text file in the\n$GIT_DIR or else from 'gitweb.$variable' in the repository config\n(e.g. 'description').\n\nThis would be used in next commit to retrieve category for a project,\nwhich is to be stored in the same way as project description.\n\nSigned-off-by: Sebastien Cevey <seb@cine7.net>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n gitweb/gitweb.perl |   24 ++++++++++++++++--------\n 1 files changed, 16 insertions(+), 8 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex f8d5722..e8685ac 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -2562,18 +2562,26 @@ sub git_get_path_by_hash {\n ## ......................................................................\n ## git utility functions, directly accessing git repository\n \n-sub git_get_project_description {\n-\tmy $path = shift;\n+# get the value of config variable either from file named as the variable\n+# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name\n+# configuration variable in the repository config file.\n+sub git_get_file_or_project_config {\n+\tmy ($path, $name) = @_;\n \n \t$git_dir = \"$projectroot/$path\";\n-\topen my $fd, '<', \"$git_dir/description\"\n-\t\tor return git_get_project_config('description');\n-\tmy $descr = <$fd>;\n+\topen my $fd, '<', \"$git_dir/$name\"\n+\t\tor return git_get_project_config($name);\n+\tmy $conf = <$fd>;\n \tclose $fd;\n-\tif (defined $descr) {\n-\t\tchomp $descr;\n+\tif (defined $conf) {\n+\t\tchomp $conf;\n \t}\n-\treturn $descr;\n+\treturn $conf;\n+}\n+\n+sub git_get_project_description {\n+\tmy $path = shift;\n+\treturn git_get_file_or_project_config($path, 'description');\n }\n \n # supported formats:\n-- \n1.7.3\n"},{"id":"166762","messageId":"1304099521-27617-7-git-send-email-jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 6/6] gitweb: Optional grouping of projects by category","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T17:52:01Z","receivedAt":"2011-04-29T17:52:01Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"From: Sebastien Cevey <seb@cine7.net>\n\nThis adds the $projects_list_group_categories option which, if enabled,\nwill result in grouping projects by category on the project list page.\nThe category is specified for each project by the $GIT_DIR/category file\nor the 'gitweb.category' variable in its configuration file. By default,\nprojects are put in the $project_list_default_category category.\n\nNote:\n- Categories are always sorted alphabetically, with projects in\n  each category sorted according to the globally selected $order.\n- When displaying a subset of all the projects (page limiting), the\n  category headers are only displayed for projects present on the page.\n\nThe feature is inspired from Sham Chukoury's patch for the XMMS2\ngitweb, but has been rewritten for the current gitweb code. The CSS\nfor categories is inspired from Gustavo Sverzut Barbieri's patch to\ngroup projects by path.\n\nThanks to Florian Ragwitz for Perl tips.\n\n[jn: Updated to post restructuring projects list generation, fixed bugs,\n added very basic test in t9500 that there are no warnings from Perl.]\n\nSigned-off-by: Sebastien Cevey <seb@cine7.net>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n gitweb/README                          |   16 ++++++++\n gitweb/gitweb.perl                     |   62 ++++++++++++++++++++++++++++++--\n gitweb/static/gitweb.css               |    7 ++++\n t/t9500-gitweb-standalone-no-errors.sh |    8 ++++\n 4 files changed, 90 insertions(+), 3 deletions(-)\n\ndiff --git a/gitweb/README b/gitweb/README\nindex a92bde7..a3a697b 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -207,6 +207,15 @@ not include variables usually directly set during build):\n    full description is available as 'title' attribute (usually shown on\n    mouseover).  By default set to 25, which might be too small if you\n    use long project descriptions.\n+ * $projects_list_group_categories\n+   Enables the grouping of projects by category on the project list page.\n+   The category of a project is determined by the $GIT_DIR/category\n+   file or the 'gitweb.category' variable in its repository configuration.\n+   Disabled by default.\n+ * $project_list_default_category\n+   Default category for projects for which none is specified.  If set\n+   to the empty string, such projects will remain uncategorized and\n+   listed at the top, above categorized projects.\n  * @git_base_url_list\n    List of git base URLs used for URL to where fetch project from, shown\n    in project summary page.  Full URL is \"$git_base_url/$project\".\n@@ -314,6 +323,13 @@ You can use the following files in repository:\n    from the template during repository creation. You can use the\n    gitweb.description repo configuration variable, but the file takes\n    precedence.\n+ * category (or gitweb.category)\n+   Singe line category of a project, used to group projects if\n+   $projects_list_group_categories is enabled. By default (file and\n+   configuration variable absent), uncategorized projects are put in\n+   the $project_list_default_category category. You can use the\n+   gitweb.category repo configuration variable, but the file takes\n+   precedence.\n  * cloneurl (or multiple-valued gitweb.url)\n    File with repository URL (used for clone and fetch), one per line.\n    Displayed in the project summary page. You can use multiple-valued\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e8685ac..f78fdd7 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -115,6 +115,14 @@ our $projects_list = \"++GITWEB_LIST++\";\n # the width (in characters) of the projects list \"Description\" column\n our $projects_list_description_width = 25;\n \n+# group projects by category on the projects list\n+# (enabled if this variable evaluates to true)\n+our $projects_list_group_categories = 0;\n+\n+# default category if none specified\n+# (leave the empty string for no category)\n+our $project_list_default_category = \"\";\n+\n # default order of projects list\n # valid values are none, project, descr, owner, and age\n our $default_projects_order = \"project\";\n@@ -2584,6 +2592,12 @@ sub git_get_project_description {\n \treturn git_get_file_or_project_config($path, 'description');\n }\n \n+sub git_get_project_category {\n+\tmy $path = shift;\n+\treturn git_get_file_or_project_config($path, 'category');\n+}\n+\n+\n # supported formats:\n # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)\n #   - if its contents is a number, use it as tag weight,\n@@ -4877,8 +4891,9 @@ sub git_patchset_body {\n \n # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\n \n-# fills project list info (age, description, owner, forks) for each\n-# project in the list, removing invalid projects from returned list\n+# fills project list info (age, description, owner, category, forks)\n+# for each project in the list, removing invalid projects from\n+# returned list\n # NOTE: modifies $projlist, but does not remove entries from it\n sub fill_project_list_info {\n \tmy $projlist = shift;\n@@ -4904,6 +4919,12 @@ sub fill_project_list_info {\n \t\tif ($show_ctags) {\n \t\t\t$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});\n \t\t}\n+\t\tif ($projects_list_group_categories && !defined $pr->{'category'}) {\n+\t\t\tmy $cat = git_get_project_category($pr->{'path'}) ||\n+\t\t\t                                   $project_list_default_category;\n+\t\t\t$pr->{'category'} = to_utf8($cat);\n+\t\t}\n+\n \t\tpush @projects, $pr;\n \t}\n \n@@ -4931,6 +4952,23 @@ sub sort_projects_list {\n \treturn @projects;\n }\n \n+# returns a hash of categories, containing the list of project\n+# belonging to each category\n+sub build_projlist_by_category {\n+\tmy ($projlist, $from, $to) = @_;\n+\tmy %categories;\n+\n+\t$from = 0 unless defined $from;\n+\t$to = $#$projlist if (!defined $to || $#$projlist < $to);\n+\n+\tfor (my $i = $from; $i <= $to; $i++) {\n+\t\tmy $pr = $projlist->[$i];\n+\t\tpush @{$categories{ $pr->{'category'} }}, $pr;\n+\t}\n+\n+\treturn wantarray ? %categories : \\%categories;\n+}\n+\n # print 'sort by' <th> element, generating 'sort by $name' replay link\n # if that order is not selected\n sub print_sort_th {\n@@ -5059,7 +5097,25 @@ sub git_project_list_body {\n \t\t      \"</tr>\\n\";\n \t}\n \n-\tgit_project_list_rows(\\@projects, $from, $to, $check_forks);\n+\tif ($projects_list_group_categories) {\n+\t\t# only display categories with projects in the $from-$to window\n+\t\t@projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];\n+\t\tmy %categories = build_projlist_by_category(\\@projects, $from, $to);\n+\t\tforeach my $cat (sort keys %categories) {\n+\t\t\tunless ($cat eq \"\") {\n+\t\t\t\tprint \"<tr>\\n\";\n+\t\t\t\tif ($check_forks) {\n+\t\t\t\t\tprint \"<td></td>\\n\";\n+\t\t\t\t}\n+\t\t\t\tprint \"<td class=\\\"category\\\" colspan=\\\"5\\\">\".esc_html($cat).\"</td>\\n\";\n+\t\t\t\tprint \"</tr>\\n\";\n+\t\t\t}\n+\n+\t\t\tgit_project_list_rows($categories{$cat}, undef, undef, $check_forks);\n+\t\t}\n+\t} else {\n+\t\tgit_project_list_rows(\\@projects, $from, $to, $check_forks);\n+\t}\n \n \tif (defined $extra) {\n \t\tprint \"<tr>\\n\";\ndiff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css\nindex 79d7eeb..4df2d16 100644\n--- a/gitweb/static/gitweb.css\n+++ b/gitweb/static/gitweb.css\n@@ -295,6 +295,13 @@ td.current_head {\n \ttext-decoration: underline;\n }\n \n+td.category {\n+\tbackground-color: #d9d8d1;\n+\tborder-top: 1px solid #000000;\n+\tborder-left: 1px solid #000000;\n+\tfont-weight: bold;\n+}\n+\n table.diff_tree span.file_status.new {\n \tcolor: #008000;\n }\ndiff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh\nindex 71ef0ac..f5648a6 100755\n--- a/t/t9500-gitweb-standalone-no-errors.sh\n+++ b/t/t9500-gitweb-standalone-no-errors.sh\n@@ -644,4 +644,12 @@ test_expect_success \\\n \t'ctags: search projects by non existent tag' \\\n \t'gitweb_run \"by_tag=non-existent\"'\n \n+# ----------------------------------------------------------------------\n+# categories\n+\n+test_expect_success \\\n+\t'categories: projects list, only default category' \\\n+\t'echo \"\\$projects_list_group_categories = 1;\" >>gitweb_config.perl &&\n+\t gitweb_run'\n+\n test_done\n-- \n1.7.3\n"},{"id":"166775","messageId":"7v8vuswxqs.fsf@alter.siamese.dyndns.org","threadId":"27224","inReplyTo":"1304099521-27617-1-git-send-email-jnareb@gmail.com","subject":"Re: [PATCH 0/6] gitweb: Improve ctags, introduce categories","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2011-04-29T21:31:23Z","receivedAt":"2011-04-29T21:31:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A tangent. It is curious why [PATCH 2/6] alone ended up with an encoded\n\"Subject\" header, like this:\n\n  Subject: =?UTF-8?q?=5BPATCH=202/6=5D=20gitweb=3A=20Change=20the=20\n   way=20=22content=20tags=22=20=28=27ctags=27=29=20are=20handled?=\n\nThe message actually has the above as a long single line, as can be seen\nat http://article.gmane.org/gmane.comp.version-control.git/172479/raw\n\nJust being curious.\n\nThe headers suggest that sending MUA was git-send-email speaking to gmail\nSMTP.  Did we introduce bugs to send-email recently?\n"},{"id":"166793","messageId":"201104300153.09304.jnareb@gmail.com","threadId":"27224","inReplyTo":"7v8vuswxqs.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH 0/6] gitweb: Improve ctags, introduce categories","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-04-29T23:53:06Z","receivedAt":"2011-04-29T23:53:06Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Junio C Hamano wrote:\n\n> A tangent. It is curious why [PATCH 2/6] alone ended up with an encoded\n> \"Subject\" header, like this:\n> \n>   Subject: =?UTF-8?q?=5BPATCH=202/6=5D=20gitweb=3A=20Change=20the=20\n>    way=20=22content=20tags=22=20=28=27ctags=27=29=20are=20handled?=\n> \n> The message actually has the above as a long single line, as can be seen\n> at http://article.gmane.org/gmane.comp.version-control.git/172479/raw\n> \n> Just being curious.\n> \n> The headers suggest that sending MUA was git-send-email speaking to gmail\n> SMTP.  Did we introduce bugs to send-email recently?\n\nThis is git-send-email from git version 1.7.3 (not most recent).\n\nWhat might be important, and what you can't get from mail itself, is that\npatch was generated using git-format-patch, but UTF-8 characters in body\nof email were introduced during editing it, and were not present in commit\nmessage (sorry, bad practice).  So git-send-email asked about encoding,\nand I chosen default UTF-8\n\n  The following files are 8bit, but do not declare a Content-Transfer-Encoding.\n      mdir.7/0002-gitweb-Change-the-way-content-tags-ctags-are-handled.txt\n  Which 8bit encoding should I declare [UTF-8]? <ENTER>\n\nThough subject itself does contain only 7bit US-ASCII, so there is no need\nfor encoding it (with quoted-printable, as Subject appears before \nContent-Encoding and MIME-Version headers).\n\n-- \nJakub Narebski\nPoland\n"},{"id":"166803","messageId":"BANLkTi=cckKr3baDZAktAjz8F=JXjQJWiA@mail.gmail.com","threadId":"27224","inReplyTo":"7v8vuswxqs.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH 0/6] gitweb: Improve ctags, introduce categories","fromName":"Øyvind A. Holm","fromEmail":"sunny@sunbase.org","sentAt":"2011-04-30T20:36:29Z","receivedAt":"2011-04-30T20:36:29Z","isPatch":true,"sender":{"key":"sunny@sunbase.org","avatar":"https://avatars.githubusercontent.com/u/113445?v=4"},"body":"On 29 April 2011 23:31, Junio C Hamano <gitster@pobox.com> wrote:\n> A tangent. It is curious why [PATCH 2/6] alone ended up with an encoded\n> \"Subject\" header, like this:\n>\n>  Subject: =?UTF-8?q?=5BPATCH=202/6=5D=20gitweb=3A=20Change=20the=20\n>   way=20=22content=20tags=22=20=28=27ctags=27=29=20are=20handled?=\n>\n> The message actually has the above as a long single line, as can be seen at\n> http://article.gmane.org/gmane.comp.version-control.git/172479/raw\n>\n> Just being curious.\n\nThis seems as the same thing that I reported on 2010-04-25 23:35:49Z,\n<http://thread.gmane.org/gmane.comp.version-control.git/145774>. If there's a\ncharacter above U+007F in the log message below line #2, the Subject: line is\ngarbled. In this case it is, it's the \"ö\" in Uwe's name that leads to this\nerror.\n\nA test to reproduce this is at <https://gist.github.com/378785>, but it seems\nas this was fixed between v1.7.4.1-292-ge2a57aa and v1.7.4.1-343-ga91df69 ,\nprobably happened in dc7f96f (Merge branch 'jk/format-patch-multiline-header').\nThe patch at <http://article.gmane.org/gmane.comp.version-control.git/172479/raw>\nwas generated with git-1.7.3, so it would trigger the error in this case.\n\nRegards,\nØyvind\n"},{"id":"166934","messageId":"201105031602.30874.jnareb@gmail.com","threadId":"27224","inReplyTo":"BANLkTi=cckKr3baDZAktAjz8F=JXjQJWiA@mail.gmail.com","subject":"Re: git-send-email and non 7bit clean message (was: [PATCH 0/6] gitweb: Improve ctags, introduce categories)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-05-03T14:02:28Z","receivedAt":"2011-05-03T14:02:28Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Sat, 30 Apr 2011, Øyvind A. Holm wrote:\n> On 29 April 2011 23:31, Junio C Hamano <gitster@pobox.com> wrote:\n> > A tangent. It is curious why [PATCH 2/6] alone ended up with an encoded\n> > \"Subject\" header, like this:\n> >\n> >  Subject: =?UTF-8?q?=5BPATCH=202/6=5D=20gitweb=3A=20Change=20the=20\n> >   way=20=22content=20tags=22=20=28=27ctags=27=29=20are=20handled?=\n> >\n> > The message actually has the above as a long single line, as can be seen at\n> > http://article.gmane.org/gmane.comp.version-control.git/172479/raw\n> >\n> > Just being curious.\n> \n> This seems as the same thing that I reported on 2010-04-25 23:35:49Z,\n> <http://thread.gmane.org/gmane.comp.version-control.git/145774>. If there's a\n> character above U+007F in the log message below line #2, the Subject: line is\n> garbled. In this case it is, it's the \"ö\" in Uwe's name that leads to this\n> error.\n> \n> A test to reproduce this is at <https://gist.github.com/378785>, but it seems\n> as this was fixed between v1.7.4.1-292-ge2a57aa and v1.7.4.1-343-ga91df69 ,\n> probably happened in dc7f96f (Merge branch 'jk/format-patch-multiline-header').\n> The patch at <http://article.gmane.org/gmane.comp.version-control.git/172479/raw>\n> was generated with git-1.7.3, so it would trigger the error in this case.\n\nI have just upgraded git to 1.7.5, and unfortunately it still has the\nsame bug (note that UTF-8 character was introduced while editing patch,\nso git-format-patch doesn't see it):\n\n  5369:[gitweb/web@git]# git send-email [...] --dry-run mdir.1/*.txt\n  The following files are 8bit, but do not declare a Content-Transfer-Encoding.\n      mdir.1/0001-gitweb-Prepare-for-splitting-gitweb.txt\n  Which 8bit encoding should I declare [UTF-8]? <ENTER>\n  Dry-Sent [PATCHv2 0/2] gitweb: Beginnings of splitting gitweb into modules\n  Dry-Sent =?UTF-8?q?=5BPATCHv2=201/2=20=28RFC=3F=29=5D=20gitweb=3A=20Prepare=20for=20splitting=20gitweb?=\n  Dry-Sent [PATCHv2 2/2 (PoC)] gitweb: Create Gitweb::Util module\n\nNote that having\n\n  MIME-Version: 1.0\n  Content-Type: text/plain; charset=utf-8\n\nis not enough[1].  However the patch send is understood correctly by email\nprograms: see [PATCH 2/6] in this thread.\n\nI have added \n\n  Content-Transfer-Encoding: 8bit\n\nto mdir.1/0001-gitweb-Prepare-for-splitting-gitweb.txt, and now it works\nall right.\n\n  5370:[gitweb/web@git]# git send-email [...] mdir.1/*.txt\n  Dry-Sent [PATCHv2 0/2] gitweb: Beginnings of splitting gitweb into modules\n  Dry-Sent [PATCHv2 1/2 (RFC?)] gitweb: Prepare for splitting gitweb\n  Dry-Sent [PATCHv2 2/2 (PoC)] gitweb: Create Gitweb::Util module\n\n\nFootnotes:\n^^^^^^^^^^\n[1]: Note that git-send-email does something strange: first, the problem\nis with Content-Transfer-Encoding, and git-send-email asks for 8bit\nencoding, suggesting UTF-8, instead of asking for transfer encoding,\nsugesting e.g. 8bit.\n\nSecond, from email headers I have added git-send-email should _know_ that\nmessage uses UTF-8 encoding (though this is side issue, and probably result\nof above).\n\n-- \nJakub Narebski\nPoland\n"},{"id":"167018","messageId":"201105041550.16355.jnareb@gmail.com","threadId":"27224","inReplyTo":"201105031602.30874.jnareb@gmail.com","subject":"[PATCH/RFC] git-send-email: Do not encode Subject if not required (was: git-send-email and non 7bit clean message)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-05-04T13:50:12Z","receivedAt":"2011-05-04T13:50:12Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Tue, 3 May 2011, Jakub Narebski wrote:\n> On Sat, 30 Apr 2011, Øyvind A. Holm wrote:\n> > On 29 April 2011 23:31, Junio C Hamano <gitster@pobox.com> wrote:\n\n> > > A tangent. It is curious why [PATCH 2/6] alone ended up with an encoded\n> > > \"Subject\" header, like this:\n> > >\n> > >  Subject: =?UTF-8?q?=5BPATCH=202/6=5D=20gitweb=3A=20Change=20the=20\n> > >   way=20=22content=20tags=22=20=28=27ctags=27=29=20are=20handled?=\n> > >\n> > > The message actually has the above as a long single line, as can be seen at\n> > > http://article.gmane.org/gmane.comp.version-control.git/172479/raw\n> > >\n> > > Just being curious.\n> > \n> > This seems as the same thing that I reported on 2010-04-25 23:35:49Z,\n> > <http://thread.gmane.org/gmane.comp.version-control.git/145774>. If there's a\n> > character above U+007F in the log message below line #2, the Subject: line is\n> > garbled. In this case it is, it's the \"ö\" in Uwe's name that leads to this\n> > error.\n\nNote: this line was added \"by hand\" when editing patch, so commit message\nthat git-format-patch seen did not contain any non-ASCII characters.\nOtherwise fir-format-patch would add correct MIME headers itself.\n\n> > A test to reproduce this is at <https://gist.github.com/378785>, but it seems\n> > as this was fixed between v1.7.4.1-292-ge2a57aa and v1.7.4.1-343-ga91df69 ,\n> > probably happened in dc7f96f (Merge branch 'jk/format-patch-multiline-header').\n> > The patch at <http://article.gmane.org/gmane.comp.version-control.git/172479/raw>\n> > was generated with git-1.7.3, so it would trigger the error in this case.\n> \n> I have just upgraded git to 1.7.5, and unfortunately it still has the\n> same bug (note that UTF-8 character was introduced while editing patch,\n> so git-format-patch doesn't see it):\n> \n>   5369:[gitweb/web@git]# git send-email [...] --dry-run mdir.1/*.txt\n>   The following files are 8bit, but do not declare a Content-Transfer-Encoding.\n>       mdir.1/0001-gitweb-Prepare-for-splitting-gitweb.txt\n>   Which 8bit encoding should I declare [UTF-8]? <ENTER>\n>   Dry-Sent [PATCHv2 0/2] gitweb: Beginnings of splitting gitweb into modules\n>   Dry-Sent =?UTF-8?q?=5BPATCHv2=201/2=20=28RFC=3F=29=5D=20gitweb=3A=20Prepare=20for=20splitting=20gitweb?=\n>   Dry-Sent [PATCHv2 2/2 (PoC)] gitweb: Create Gitweb::Util module\n> \n> Note that having\n> \n>   MIME-Version: 1.0\n>   Content-Type: text/plain; charset=utf-8\n> \n> is not enough[1].\n \nPerhaps something like attached patch could be beginning of fixing\nthis issue.\n \n> Footnotes:\n> ^^^^^^^^^^\n> [1]: Note that git-send-email does something strange: first, the problem\n> is with Content-Transfer-Encoding, and git-send-email asks for 8bit\n> encoding, suggesting UTF-8, instead of asking for transfer encoding,\n> sugesting e.g. 8bit.\n\nActually git-send-email assumes 8bit if not stated, but it does not\nparse 'Content-Type' header for encoding to be used, perhaps correctly\nassuming that if required 'Content-Transfer-Encoding' is missing, then\n'Content-Type' is most probably missing as well.\n\n-- >8 ---------- >8 --\nSubject: [PATCH] git-send-email: Do not encode Subject if not required\n\nIf \"Subject:\" header of an email does not contain non-ASCII characters,\nthen we don't need to RFC-2047 encode it, even if file had broken\nencoding.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n git-send-email.perl |    4 +++-\n 1 files changed, 3 insertions(+), 1 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 98ab33a..1c6b1a8 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -1281,7 +1281,9 @@ foreach my $t (@files) {\n \t\t$body_encoding = $auto_8bit_encoding;\n \t}\n \n-\tif ($broken_encoding{$t} && !is_rfc2047_quoted($subject)) {\n+\tif ($broken_encoding{$t} &&\n+\t    !is_rfc2047_quoted($subject) &&\n+\t    $subject =~ /[^[:ascii:]]/) {\n \t\t$subject = quote_rfc2047($subject, $auto_8bit_encoding);\n \t}\n \n-- \n1.7.5\n"},{"id":"167332","messageId":"201105072039.24548.jnareb@gmail.com","threadId":"27224","inReplyTo":"1304099521-27617-2-git-send-email-jnareb@gmail.com","subject":"Re: [PATCH 1/6 (v2)] gitweb: Restructure projects list generation","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-05-07T18:39:22Z","receivedAt":"2011-05-07T18:39:22Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 29 Apr 2011, Jakub Narebski wrote:\n\n> Extract filtering out forks (which is done if 'forks' feature is\n> enabled) into filter_forks_from_projects_list subroutine, and\n> searching projects (via projects search form, or via content tags)\n> into search_projects_list subroutine.\n\nIf it was all this patch did, it would be probably easier to review.\nShould I then split this patch into smaller, though not entirely\nstandalone patches: pure refactoring, improving finding forks, and\nadding tests?\n\n> Sorting projects got also refactored in a very straight way (just\n> moving code) into sort_projects_list subroutine.\n\nThis also could be refactored in a separate patch... though I rather\nhave all refactorings together, so one can see the result: clean code.\n \n> The interaction between forks, content tags and searching is now made\n> more explicit: searching whether by tag, or via search form turns off\n> fork filtering (gitweb searches also forks, and will show all\n> results).  If 'ctags' feature is disabled, then searching by tag is\n> too.\n\nThis is functional change that makes code more clear, and I think it\nmakes better behavior... but it changes how gitweb behaves, so it is\nnot pure refactoring.  Should I put it into a bit artificial separate\npatch?\n \n> While at it we now detect that there are no projects and respond with\n> \"404 No projects found\" also for 'project_index' and 'opml' actions.\n\nThis \"while at it\" is here because we can now say \"No projects found\"\nfor 'projects_list' action, because filtering / searching is done\nupfront, so we know if we found anything.\n\nBut again, this perhaps would be better put in a separate patch.\n \n[...]\n> filter_forks_from_projects_list() uses trie[1] (prefix tree) to find\n> which repositories are forks of other projects; directories are used\n> as \"letters\" in trie.  Algorithm to create trie and find prefix match\n> was created with some help of code of Tree::Tree[2] Perl module from\n> CPAN.\n> \n> [1]: http://en.wikipedia.org/wiki/Trie\n> [2]: http://p3rl.org/Tree::Trie\n\nThis change in algorithm should perhaps be put in a separate patch...\n\n> Note that before this commit filtering out forks worked only for\n> $projects_list being a file, it assumed that forks are always after\n> forked project (not very unreasonable), and used algorithm which is\n> O(<projects>^2).\n> \n> Current code filters out projects only where it should, regardless\n> whether $projects_list is directory or a file, and trie-based\n> algorithm is O(<projects>) + O(<projects forked>*<max depth>).\n> Current code also consistently finds shortest prefix (important in\n> counting forks in case of forks of forks).\n\nJakub Narebski wrote in\n\"Re: What's cooking in git.git (May 2011, #02; Wed, 4)\"\nhttp://thread.gmane.org/gmane.comp.version-control.git/172789/focus=172820\n\n> Junio C Hamano <gitster@pobox.com> writes:\n[...]\n>> * jn/ctags (2011-04-29) 6 commits\n>>  - gitweb: Optional grouping of projects by category\n>>  - gitweb: Modularized git_get_project_description to be more generic\n>>  - gitweb: Split git_project_list_body in two functions\n>>  - gitweb: Mark matched 'ctag' / contents tag (?by_tag=foo)\n>>  - gitweb: Change the way \"content tags\" ('ctags') are handled\n>>  - gitweb: Restructure projects list generation\n>> \n>> Waiting for comments.\n> \n> Should I do and post benchmarks for\n> \n>    - gitweb: Restructure projects list generation\n> \n> change (when 'forks' feature is used)?\n\nIt is not easy to compare old and new code, because those algorithm\nhave different behavior.\n\nBest I can come up with is comparing pure fork detection (not the time\nit takes to generate page) - I can easy mock that - for sizes comparable\nwith http://repo.or.cz which uses 'forks' feature.  repo.or.cz has\naround 4,000 projects, around 300 of those have forks (note: forks are\nnot included in number of projects).  From non-representative sample\nmost forked project have but single fork; note however that there are\nrepositories such as git.git, which has 55 forks, and some of those have\nforks on its own (forks of forks).\n\n plan: nprojects=4000; nprojects_forked=300; nforks_per_forked_project=1\n number of projects: 4000\n\n Benchmark: running old, new for at least 600 CPU seconds...\n        old: 1231.38 wallclock secs (620.23 usr +  1.30 sys = 621.53 CPU) @  0.03/s (n=21)\n        new:  847.6  wallclock secs (632.34 usr +  0.80 sys = 633.14 CPU) @  5.18/s (n=3277)\n     s/iter    old    new\n old   29.6     --   -99%\n new  0.193 15219%     --\n\n Dumbbench: running old, new for at least 50 iterations...\n old: Ran 106 iterations (25 outliers).\n old: Rounded run time per iteration: 3.316e+01  +/- 1.5e-01 (0.5%)\n new: Ran 66 iterations (15 outliers).\n new: Rounded run time per iteration: 2.0213e-01 +/- 2.1e-04 (0.1%)\n\nBelow there is table of time versus number of projects, used to generate\nattached and linked images.  Data was generated using Dumbbench module\nfrom CPAN.\n\n ########################################################\n # nforks  = 5 (forks per project forked)\n # nforked = 5.00%\n # 1:projects 2:nforked  3:old 4:old_err  5:new 5:new_err\n #  0   0  8.959e-06  2.2e-08  1.0137e-05 2.0e-08\n   50   3  3.211e-03  1.6e-05  2.0524e-03 5.0e-06\n  103   5  1.4003e-02 6.7e-05  4.477e-03  2.2e-05\n  204  10  5.647e-02  2.3e-04  9.464e-03  4.6e-05\n  402  20  2.1939e-01 3.2e-04  1.9943e-02 9.6e-05\n  500  25  3.666e-01  3.6e-03  2.706e-02  2.5e-04\n  800  40  9.124e-01  1.7e-03  4.175e-02  2.1e-04\n 1000  50  1.575e+00  1.3e-02  5.896e-02  5.3e-04\n 1600  80  3.738e+00  1.2e-02  9.347e-02  4.3e-04\n 2004 100  6.820e+00  6.7e-02  1.1216e-01 3.6e-04\n 3203 160  1.8189e+01 8.5e-02  1.8144e-01 4.5e-04\n 4003 200  2.927e+01  2.9e-01  2.2939e-01 5.7e-04\n\nScript used to generate this data is attached.\n\nFrom those data we can generate a plot to show how time depends on size\nof problem (on total number of projects).  Assuming that both \"old\" ane\n\"new\" are P i.e. are O(n^k), where 'n' is number of projects, this\ndependency is best examined on loglog scale.\n\n      y = x^k\n  log y = log (x^k) = k * log x\n      v = k * u,  where u = log x, v = log y\n\nThis plot, together with fit of cubic polynomial and linear function\nto the data, is shown in\n\n  https://git.wiki.kernel.org/index.php/File:Benchmark_find_forks-old_vs_new.png\n\n(I'm sorry for abusing git wiki for sharing images...).\n-- \nJakub Narebski\nPoland\n\n\n#!/usr/bin/perl\n\nuse strict;\nuse warnings;\n\nuse List::Util qw(first);\nuse POSIX qw(ceil);\n\nuse Acme::MetaSyntactic qw(any);\nuse Benchmark qw(:all :hireswallclock);\nuse Dumbbench;\n\nuse Data::Dumper::Concise; # for debugging\n\n\n# ----------------------------------------------------------------------\n\n# old: version used to be in git_get_projects_list for $projects_list file\nsub filter__git_get_projects_list {\n\tmy ($projects, $need_sorting) = @_;\n\n\t@$projects = sort { $a->{'path'} cmp $b->{'path'} } @$projects\n\t\tif ($need_sorting);\n\n\tmy (@filtered, %paths);\n PROJECT:\n\tforeach my $pr (@$projects) {\n\t\tmy $path = $pr->{'path'};\n\n\t\t# below is v1.7.5.1:gitweb/gitweb.perl lines 2725 and later\n\tPATH:\n\t\tforeach my $filter (keys %paths) {\n\t\t\t# looking for forks\n\t\t\tmy $pfx = substr($path, 0, length($filter));\n\t\t\tif ($pfx ne $filter) {\n\t\t\t\tnext PATH;\n\t\t\t}\n\t\t\tmy $sfx = substr($path, length($filter));\n\t\t\tif ($sfx !~ /^\\/.*\\.git$/) {\n\t\t\t\tnext PATH;\n\t\t\t}\n\t\t\t# is a fork, don't include it in\n\t\t\t# the list\n\t\t\tnext PROJECT;\n\t\t}\n\n\t\t# [...]\n\t\t# below is v1.7.5.1:gitweb/gitweb.perl line 2746 and later\n\t\tpush @filtered, $pr;\n\t\t(my $forks_path = $path) =~ s/\\.git$//;\n\t\t$paths{$forks_path}++;\n\t}\n\n\treturn @filtered;\n}\n\n\n# new: using 'path' only, using trie; code borrowed from Tree::Trie (Perl Artistic License)\nsub filter_forks_from_projects_list {\n\tmy ($projects, $need_sorting) = @_;\n\n\t@$projects = sort { $a->{'path'} cmp $b->{'path'} } @$projects\n\t\tif ($need_sorting);\n\n\tmy %trie; # prefix tree of directories (path components)\n\t# generate trie out of those directories that might contain forks\n\tforeach my $pr (@$projects) {\n\t\tmy $path = $pr->{'path'};\n\t\t$path =~ s/\\.git$//;      # forks of 'repo.git' are in 'repo/' directory\n\t\tnext if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'\n\t\tnext unless ($path);      # skip '.git' repository: tests, git-instaweb\n\t\t#next unless (-d $path);  # containing directory exists (NOT IN TESTS!!!)\n\t\t$pr->{'forks'} = [];      # there can be 0 or more forks of project\n\n\t\t# add to trie\n\t\tmy @dirs = split('/', $path);\n\t\t# walk the trie, until either runs out of components or out of trie\n\t\tmy $ref = \\%trie;\n\t\twhile (scalar @dirs &&\n\t\t       exists($ref->{$dirs[0]})) {\n\t\t\t$ref = $ref->{shift @dirs};\n\t\t}\n\t\t# create rest of trie structure from rest of components\n\t\tforeach my $dir (@dirs) {\n\t\t\t$ref = $ref->{$dir} = {};\n\t\t}\n\t\t# create end marker, store $pr as a data\n\t\t$ref->{''} = $pr if (!exists $ref->{''});\n\t}\n\n\t# filter out forks, by finding shortest prefix match for paths\n\tmy @filtered;\n PROJECT:\n\tforeach my $pr (@$projects) {\n\t\t# trie lookup\n\t\tmy $ref = \\%trie;\n\tDIR:\n\t\tforeach my $dir (split('/', $pr->{'path'})) {\n\t\t\tif (exists $ref->{''}) {\n\t\t\t\t# found [shortest] prefix, is a fork - skip it\n\t\t\t\tpush @{$ref->{''}{'forks'}}, $pr;\n\t\t\t\tnext PROJECT;\n\t\t\t}\n\t\t\tif (!exists $ref->{$dir}) {\n\t\t\t\t# not in trie, cannot have prefix, not a fork\n\t\t\t\tpush @filtered, $pr;\n\t\t\t\tnext PROJECT;\n\t\t\t}\n\t\t\t# If the dir is there, we just walk one step down the trie.\n\t\t\t$ref = $ref->{$dir};\n\t\t}\n\t\t# we ran out of trie\n\t\t# (shouldn't happen: it's either no match, or end marker)\n\t\tpush @filtered, $pr;\n\t}\n\n\treturn @filtered;\n}\n\n# ----------------------------------------------------------------------\n\nsub prepare_projects {\n\tmy ($nprojects, $nforked, $nforks_per_project, $exclude_forks) = @_;\n\tmy @projects;\n\n\twhile ($nprojects > 0) {\n\t\tpush @projects, metaname().\".git\";\n\t\t$nprojects--;\n\t\tlast unless $nprojects;\n\n\t\tif (rand() < (1.0*$nforked/$nprojects)) {\n\t\t\t(my $base = $projects[-1]) =~ s/\\.git$//;\n\t\t\tpush @projects, map { \"$base/$_.git\" } metaname($nforks_per_project);\n\t\t\t$nprojects -= $nforks_per_project unless $exclude_forks;\n\t\t\t$nforked--;\n\t\t}\n\t}\n\n\t#print Dumper(\\@projects);\n\t@projects = map { { 'path' => $_ } } @projects;\n\n\treturn @projects;\n}\n\n# ######################################################################\n\nmy (@projects, @filtered);\n\n# repo.or.cz\n# * 4038 projects (without forks?), based on http://repo.or.cz/w?a=project_index\n# * 4024 projects, based on \"grep -c -e '<tr class=\"'\" on http://repo.or.cz/w?a=project_list\n# * 3428 active projects, based on \"grep -c -e '\"age'\" on http://repo.or.cz/w?a=project_list\n# *  308 projects have forks (7.6-10%), based on grep -c -e '/forks\">+</a>' project_list.html\n# * nonrepresentative sample of nforks: 6, 2, 1, 1, 0, 0, 1, 0, 1, 2, 3, 3, 3, 55+, 1, 1, 1\n\nmy $nprojects = 100;\nmy $nforked_frac = 0.05;\nmy $nforked   = ceil($nforked_frac*$nprojects); # 5%\nmy $nforks    = 5;\n\n$nprojects = 4000;\n$nforked   = 300;\n$nforks    = 1;\n\nprint \"plan: nprojects=$nprojects; nforked=$nforked; nforks=$nforks\\n\";\n@projects = prepare_projects($nprojects, $nforked, $nforks);\nprint \"number of projects: \".(scalar @projects).\"\\n\";\n\nprint \"\\n\";\nmy $results = timethese(-600, {\n\t'old' => sub { filter__git_get_projects_list(\\@projects) },\n\t'new' => sub { filter_forks_from_projects_list(\\@projects) },\n});\n#print Dumper($results);\ncmpthese($results);\nprint \"\\n\";\n\nprint \"Dumbbench: old, new\\n\";\nmy $bench = Dumbbench->new(\n\ttarget_rel_precision => 0.005, # seek ~0.5%\n\tinitial_runs         => 50,    # the higher the more reliable\n);\n$bench->add_instances(\n\tDumbbench::Instance::PerlSub->new(\n\t\tname => 'old',\n\t\tcode => sub { filter__git_get_projects_list(\\@projects) },\n\t),\n\tDumbbench::Instance::PerlSub->new(\n\t\tname => 'new',\n\t\tcode => sub { filter_forks_from_projects_list(\\@projects) },\n\t),\n);\n$bench->run();\n$bench->report();\n\nprint \"\\n\";\n#print Dumper($bench->instances());\n__END__\n\nprint \"#####################################################################\\n\".\n      \"# nforks  = $nforks (forks per project forked)\\n\".\n      \"# nforked = \".(sprintf \"%.2f%%\", $nforked_frac*100.0).\"\\n\".\n      \"# 1:projects 2:nforked  3:old 4:old_err  5:new 5:new_err\\n\";\nfor (my $i = 50; $i <= 4000; $i *= 2) {\n\t$nforked  = ceil($nforked_frac*$i);\n\t@projects = prepare_projects($i, $nforked, $nforks);\n\n\t$bench = Dumbbench->new(\n\t\ttarget_rel_precision => 0.005, # seek ~0.5%\n\t\tinitial_runs         => 50,    # the higher the more reliable\n\t);\n\t$bench->add_instances(\n\t\tDumbbench::Instance::PerlSub->new(\n\t\t\tname => 'old',\n\t\t\tcode => sub { filter__git_get_projects_list(\\@projects) },\n\t\t),\n\t\tDumbbench::Instance::PerlSub->new(\n\t\t\tname => 'new',\n\t\t\tcode => sub { filter_forks_from_projects_list(\\@projects) },\n\t\t),\n\t);\n\t$bench->run();\n\n\tmy %instances = map { $_->name() => $_ } $bench->instances();\n\n\tprintf \"%4d %3d \", (scalar @projects), $nforked;\n\tforeach my $name (qw(old new)) {\n\t\tmy $result = $instances{$name}->result();\n\t\tprint \" \".$result->number();\n\t\tprint \" \".$result->error()->[0];\n\t}\n\tprint \"\\n\";\n}\n#print Dumper($bench->instances());\n#print Dumper($_->result()) foreach ($bench->instances());\n\n\nprint \"\\n\";\n\n\n1;\n__END__\n"}]}