From 28efcae7180cc27edc97fc35c214ca890b37d94e Mon Sep 17 00:00:00 2001 From: "Kyle J. McKay" Date: Fri, 21 Aug 2015 06:12:13 -0700 Subject: [PATCH] girocco: support bundle listings Signed-off-by: Kyle J. McKay --- Girocco/Config.pm | 13 ++ Girocco/Util.pm | 1 + apache.conf.in | 12 +- cgi/bundles.cgi | 444 +++++++++++++++++++++++++++++++++++++++++++ gitweb/gitweb_config.perl | 4 + html/girocco.css | 38 +++- toolbox/update-all-config.sh | 2 +- 7 files changed, 506 insertions(+), 8 deletions(-) create mode 100755 cgi/bundles.cgi diff --git a/Girocco/Config.pm b/Girocco/Config.pm index 87aa668..1f44cec 100644 --- a/Girocco/Config.pm +++ b/Girocco/Config.pm @@ -414,8 +414,19 @@ our $gitweburl = "http://repo.or.cz/w"; our $gitwebfiles = "http://repo.or.cz"; # URL of the Girocco CGI web admin interface (Girocco cgi/ subdirectory) +# e.g. reguser.cgi, edituser.cgi, regproj.cgi, editproj.cgi etc. our $webadmurl = "http://repo.or.cz"; +# URL of the Girocco CGI bundles information generator (Girocco cgi/bundles.cgi) +# If mod_rewrite is enabled and the sample apache.conf configuration is used +# (with paths suitably updated), the trailing "/b" is optional for all browsers +# that send a User-Agent string WITHOUT (case insensitively) "git/". Alternatively +# a minor change to the sample apache.conf can redirect (301 or 302) URLs without +# the "/b" to a URL with it where appropriate. +# This is different from $httpbundleurl. This URL lists all available bundles +# for a project and returns that as an HTML page. +our $bundlesurl = "http://repo.or.cz/b"; + # URL of the Girocco CGI html templater (Girocco cgi/html.cgi) our $htmlurl = "http://repo.or.cz/h"; @@ -433,6 +444,8 @@ our $httppullurl = "http://repo.or.cz/r"; # repository, the final URL will be "$httpbundleurl/girocco.git/clone.bundle" # If mod_rewrite is enabled and the sample apache.conf configuration is used # (with paths suitably updated), the trailing "/r" is optional for all clients. +# This is different from $bundlesurl. This URL fetches a single Git-format +# .bundle file that is only usable with the 'git bundle' command. our $httpbundleurl = "http://repo.or.cz/r"; # HTTPS push URL of the repository collection (undef if N/A) diff --git a/Girocco/Util.pm b/Girocco/Util.pm index a816e1a..7e2485a 100644 --- a/Girocco/Util.pm +++ b/Girocco/Util.pm @@ -342,6 +342,7 @@ sub is_our_hostname { $Girocco::Config::gitweburl, $Girocco::Config::gitwebfiles, $Girocco::Config::webadmurl, + $Girocco::Config::bundlesurl, $Girocco::Config::htmlurl, $Girocco::Config::httppullurl, $Girocco::Config::httpbundleurl, diff --git a/apache.conf.in b/apache.conf.in index 88bdd24..ced9213 100644 --- a/apache.conf.in +++ b/apache.conf.in @@ -34,8 +34,9 @@ ScriptAlias /w @@cgiroot@@/gitweb.cgi + ScriptAlias /b @@cgiroot@@/bundles.cgi ScriptAlias /h @@cgiroot@@/html.cgi - AliasMatch ^/(?!(?i)gitweb\.cgi|html\.cgi(?:/|$))([^/]+\.cgi(?:/.*)?)$ @@cgiroot@@/$1 + AliasMatch ^/(?!(?i)gitweb\.cgi|bundles\.cgi|html\.cgi(?:/|$))([^/]+\.cgi(?:/.*)?)$ @@cgiroot@@/$1 RewriteEngine On @@ -60,6 +61,15 @@ RewriteRule \ ^/(?![bchrw]/)((?:[a-zA-Z0-9+._-]+(? diff --git a/cgi/bundles.cgi b/cgi/bundles.cgi new file mode 100755 index 0000000..3839216 --- /dev/null +++ b/cgi/bundles.cgi @@ -0,0 +1,444 @@ +#!/usr/bin/perl + +# projlist.cgi -- support for viewing a single owner's projects +# Copyright (c) 2015 Kyle J. McKay. All rights reserved. +# License GPLv2+: GNU GPL version 2 or later. +# www.gnu.org/licenses/gpl-2.0.html +# This is free software: you are free to change and redistribute it. +# There is NO WARRANTY, to the extent permitted by law. + +use strict; +use warnings; + +use lib "."; +use Girocco::CGI; +use Girocco::Config; +use Girocco::Project; +use Girocco::Util; +use POSIX qw(strftime); +binmode STDOUT, ':utf8'; + +# Never refresh more often than this +my $min_refresh = 120; + +# Extract the project name, we prefer PATH_INFO but will use a name= param +my $projname = ''; +if ($ENV{'PATH_INFO'}) { + $projname = $ENV{'PATH_INFO'}; + $projname =~ s|/+$||; + $projname =~ s|/bundles$||; +} +if (!$projname && $ENV{'QUERY_STRING'}) { + if ("&$ENV{'QUERY_STRING'}&" =~ /\&name=([^&]+)\&/) { + $projname = $1; + } +} +$projname =~ s|/+$||; +$projname =~ s|\.git||i; +$projname =~ s|^/+||; + +my $gcgi = undef; + +sub prefail { + $gcgi = Girocco::CGI->new('Project Bundles') + unless $gcgi; +} + +# Do we have a project name? + +if (!$projname) { + prefail; + print "

I need the project name as an argument now.

\n"; + exit; +} + +# Do we have a valid, existing project name? + +if (!Girocco::Project::does_exist($projname, 1)) { + prefail; + if (Girocco::Project::valid_name($projname)) { + print "

Sorry but the project $projname does not exist. " . + "Now, how did you get here?!

\n"; + } else { + print "

Invalid project name. Go away, sorcerer.

\n"; + } + exit; +} + +# Load the project and possibly parent projects + +my $proj = Girocco::Project->load($projname); +if (!$proj) { + prefail; + print "

not found project $projname, that's really weird!

\n"; + exit; +} +my @projs = ($proj); +my $parent = $projname; +# Walk up the parent projects loading each one that exists until we +# find a bundle or we've loaded all parents +while (!$projs[0]->has_bundle && $parent =~ m|^(.*[^/])/[^/]+$|) { + $parent = $1; + # It's okay if some parent(s) do not exist because we may simply have + # a grouping without any forking going on + next unless Girocco::Project::does_exist($parent, 1); + my $pproj = Girocco::Project->load($parent); + next unless $pproj; + unshift(@projs, $pproj); +} + +# At this point we produce different output depending on whether or not +# we actually found a bundle. + +# We also select a refresh time based on when we expect the bundle to be +# replaced (if we found one) or when we expect one to be created (if we didn't) +# If we found a bundle, it will be from $projs[0]. + +# We currently ignore all but the most recent bundle for a project. + +my $got_bundle = $projs[0]->has_bundle; +my $now = time; +my @bundle = (); +my @nextgc = $projs[0]->next_gc; +my $willgc = $projs[0]->needs_gc; +my $expires = undef; # undef = unknown, 0 = expired +my $behind = undef; # only meaningful if we've got a bundle, undef = unknown, 0 = current +my $refresh = undef; +my $isempty = undef; +my $inprogress = undef; +if ($got_bundle) { + $isempty = 0; + @bundle = @{($projs[0]->bundles)[0]}; + if (defined($nextgc[0])) { + $expires = $nextgc[0] - $now; + $expires = 0 if $expires < 0; + } else { + $expires = 0 if $willgc; + } + if (defined($expires)) { + # Refresh is half of expires time + $refresh = int($expires / 2); + } else { + # we have a bundle, but for some reason we have no idea when + # it will expire, so refresh after 12 hours + $refresh = 12 * 3600; + } + my $lastch = parse_any_date($projs[0]->{lastchange}); + if (defined($lastch)) { + $behind = $lastch - $bundle[0]; + $behind = 0 if $behind < 0; + } +} else { + # Project could be: + # 1) empty -- no guess about when not "empty" + # 2) building one now "building" + # 3) $nextgc[1] if defined + # 4) "not available" + if ($projs[0]->is_empty) { + $isempty = 1; + # No idea when something will be pushed so use 8 hours + $refresh = 8 * 3600; + } elsif ($projs[0]->{gc_in_progress}) { + $inprogress = 1; + $expires = 0; + # Building now, use the minimum refresh + $refresh = $min_refresh; + } elsif (defined($nextgc[1])) { + # in this case expires indicates when we expect a bundle + # and we use 'Expected' instead of 'Expires' + $expires = $nextgc[1] - $now; + $expires = 0 if $expires < 0; + # use half the time + $refresh = int($expires / 2); + } else { + if ($willgc) { + $expires = 0 if $willgc; + $refresh = $min_refresh; + } else { + # else not available + # Make the refresh 16 hours + $refresh = 16 * 3600; + } + } +} + +my $eh = undef; +$refresh = $min_refresh if defined($refresh) && $refresh < $min_refresh; +$eh = "\n" + if defined($refresh) && !$got_bundle; # do not refresh away instructions + +my $projlink = url_path($Girocco::Config::gitweburl).'/'.$projname.'.git'; +$gcgi = Girocco::CGI->new('bundles', $projname.'.git', $eh, $projlink); + +print "

Downloadable Git bundle information for project ". + "$projname as of @{[strftime('%Y-%m-%d %H:%M:%S GMT', + (gmtime($now))[0..5], -1, -1, -1)]}:

\n"; + +sub format_th { + my ($title, $explain) = @_; + return $explain ? + "$title$title". + "$explain" : + $title; +} + +sub tenths { + my $v = shift; + $v *= 10; + $v += 0.5; + $v = int($v); + $v /= 10; + return $v; +} + +sub rel_size { + my $v = shift || 0; + return "0" unless $v; + return "1 KiB" unless $v >= 1024; + $v /= 1024; + return tenths($v) . " KiB" if $v < 1024; + $v /= 1024; + return tenths($v) . " MiB" if $v < 1024; + $v /= 1024; + return tenths($v) . "GiB"; +} + +sub rel_time { + my $s = shift || 0; + return "0" unless $s; + return "1 second" if $s < 2; + return $s . " seconds" if $s < 120; + $s = int(($s + 30) / 60); + return $s . " minutes" if $s < 120; + $s = int(($s + 30) / 60); + return $s . " hours" if $s < 48; + $s = int(($s + 12) / 24); + return $s . " days" if $s < 14; + $s = int(($s + 3.5) / 7); + return $s . " weeks" if $s < 9; + $s = int(($s * 7 + 15.25) / 30.5); + return $s . " months" if $s < 24; + $s = int(($s + 6) / 12); + return $s . " years"; +} + +sub expires_string { + my $expires = shift; + return "unknown" unless defined($expires); + return "any moment" unless $expires > 0; + return rel_time($expires); +} + +sub behind_string { + my $behind = shift; + return "unknown" unless defined($behind); + return "current" unless $behind > 0; + return rel_time($behind); +} + +sub extra_string { + my $extra = shift; + return '' if !defined($extra) || $extra !~ /^\d+$/; + return "empty" if !$extra; + return '+'.rel_size($extra * 1024); +} + +sub sizek_string { + my $sizek = shift; + return "unknown" if !defined($sizek) || $sizek !~ /^\d+$/; + return "empty" if !$sizek; + return rel_size($sizek * 1024); +} + +my ($ex_title, $ex_explain) = $got_bundle ? + ("Expires", "Time remaining before bundle may become unavailable"): + ("Expected", "Time remaining until a bundle is generated"); + +print <Project@{[format_th("Bundle", "Downloadable git bundle")]}Size@{[format_th($ex_title, $ex_explain)]}@{[format_th("Behind", "Time since bundle creation until most recently received ref change")]} +EOT + +my $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[0]->{name}.'.git'; +print "$projs[0]->{name}"; +my $blink; +if ($got_bundle) { + # Git yer bundle here + $blink = url_path($Girocco::Config::httpbundleurl).'/'.$projs[0]->{name}.'.git/'.$bundle[1]; + print "$bundle[1]". + "@{[rel_size($bundle[2])]}". + "@{[expires_string($expires)]}". + "@{[behind_string($behind)]}\n"; +} else { + print "@{[$inprogress&&!$isempty?'building':'']}". + sizek_string($isempty?0:$projs[0]->{reposizek}). + "@{[expires_string($expires)]}\n"; +} +my $extrasizek = 0; +for (my $i=1; $i <= $#projs; ++$i) { + print "{name}.'.git'; + my $pname = $projs[$i]->{name}; + $pname =~ s|^.*[^/]/||; + print ">". + "…/$pname"; + my $rsk = $projs[$i]->{reposizek}; + $rsk = undef unless $rsk =~ /^\d+$/; + $rsk = 0 if !defined($rsk) && $projs[$i]->is_empty; + $extrasizek = defined($rsk) ? $extrasizek + $rsk : undef if defined($extrasizek); + print extra_string($extrasizek) if $i == $#projs; + print ""; + if ($got_bundle && $i == $#projs) { + my $lch = parse_any_date($projs[$i]->{lastchange}); + my $bh = undef; + if (defined($lch)) { + $bh = $lch - $bundle[0]; + $bh = 0 if $bh < 0; + } + print behind_string($bh); + } + print "\n"; +} +print "\n"; + +if (!$got_bundle) { + print <At this time there is no Git downloadable bundle available for +project $projname.

+

You may want to check back later based on the information shown above.

+EOT + exit 0; +} + +if ($#projs) { + print <Although there is no Git downloadable bundle available for +project $projname, since it is a fork of +project $projs[0]->{name} which does +have a bundle, that bundle can be used instead which will reduce the +amount that needs to be fetched with git fetch to only those +items that are unique to the project $projname fork.

+EOT +} + +my $projbase = $projname; +$projbase =~ s|^.*[^/]/||; +my $fetchurl = $Girocco::Config::httppullurl; +$fetchurl = $Girocco::Config::httpbundleurl unless $fetchurl; +$fetchurl = $Girocco::Config::gitpullurl unless $fetchurl; +$fetchurl .= "/".$projname.".git"; +my $forkchanges = ''; +my $forksize = ''; +if ($#projs) { + $forkchanges = " specific to the fork or"; + $forksize = " and how different the fork is from its parent"; +} + +print < + +

Instructions

+ +

0. Quick Overview

+
+
    +
  1. Download the bundle (possibly resuming the download if interrupted) using any available technique. +
  2. +
  3. Create a repository from the bundle. +
  4. +
  5. Reset the repository’s origin to a fetch URL. +
  6. +
  7. Fetch the latest changes and (optionally) the current HEAD symbolic ref. +
  8. +
  9. Select a desired branch and check it out. +
  10. +
+
+ +

1. Download the Bundle

+
+

Download the $bundle[1] file using your favorite method.

+

Web browsers typically provide one-click pause and resume. The curl command line +utility has a --continue-at option that can be used to resume an interrupted download.

+

Please note that it may not be possible to resume an interrupted download after the +“Expires” time shown above so plan the bundle download accordingly.

+

Subsequent instructions will assume the downloaded bundle $bundle[1] is available in +the current directory – adjust them if that’s not the case.

+
+ +

2. Create a Repository from the Bundle

+
+

It is possible to use the git clone command to create a repository +from a bundle file all in one step. However, that can result in unwanted local +tracking branches being created, so we do not use git clone in this +example.

+

This example creates a Git repository named “$projbase” +in the current directory, but that may be adjusted as desired:

+
+git init $projbase
+cd $projbase
+git remote add origin ../$bundle[1]
+git fetch
+
+
+ +

3. Reset the Origin

+
+

Assuming the current directory is still set to the newly created +“$projbase” repository, we set the origin to +a suitable fetch URL. Any valid fetch URL for the repository may be used +instead of the one shown here:

+
+git remote set-url origin $fetchurl
+
+

Note that the $bundle[1] file is now no longer needed and may be kept or +discarded as desired.

+
+ +

4. Fetch Updates

+
+

Assuming the current directory is still set to the newly created +“$projbase” repository, this example fetches +the current HEAD symbolic ref (i.e. the branch that would +be checked out by default if the repository had been cloned directly +from a fetch URL instead of a bundle) and any changes$forkchanges made +to the repository since the bundle was created:

+
+git fetch --prune origin
+git remote set-head origin --auto
+
+

The amount retrieved by the fetch command depends on how many changes +have been pushed to the repository since the bundle was created$forksize.

+

The set-head command will be very fast and may be omitted if one’s +not interested in the repository’s default branch.

+
+ +

5. Checkout

+
+

Assuming the current directory is still set to the newly created +“$projbase” repository, the list of available +branches to checkout may be shown like so:

+
+git branch -r
+
+

Note that if the repository has a default branch it will be shown in the +listing preceded by “origin/HEAD -> ”.

+

In this case, however, the default branch is most likely +“$projs[$#projs]->{HEAD}” and may be checked out like so:

+
+git checkout $projs[$#projs]->{HEAD}
+
+

Note that the leading “origin/” was omitted from the +branch name given to the git checkout command so that the automagic +DWIM logic kicks in.

+

The repository is now ready to be used just the same as though it had been +cloned directly from a fetch URL.

+
+ + +EOT diff --git a/gitweb/gitweb_config.perl b/gitweb/gitweb_config.perl index 2da2fbe..84656f5 100644 --- a/gitweb/gitweb_config.perl +++ b/gitweb/gitweb_config.perl @@ -54,6 +54,10 @@ $feature{'snapshot'}{'default'} = ['tgz', 'zip']; # Base web path our $my_uri = url_path($Girocco::Config::gitweburl); +## git base URL used for URL to fetch bundle information page +## i.e. full URL is "$git_base_bundles_url/$project/bundles" +our $git_base_bundles_url = url_path($Girocco::Config::bundlesurl); + # https hint html inserted right after any https push URL (undef for none) # e.g. "https push instructions" our $https_hint_html = undef; diff --git a/html/girocco.css b/html/girocco.css index 0e32537..3631ae2 100644 --- a/html/girocco.css +++ b/html/girocco.css @@ -4,12 +4,15 @@ padding: 0; } -.projectlist, p, pre { +.projectlist, .bundlelist, p, pre { margin-left: 1ex; margin-right: 1ex; } -div.htmlcgi .projectlist, div.htmlcgi p, div.htmlcgi pre { +div.htmlcgi .projectlist, +div.htmlcgi .bundlelist, +div.htmlcgi p, +div.htmlcgi pre { margin-left: 0; margin-right: 0; } @@ -19,6 +22,22 @@ div.htmlcgi { margin-right: 1ex; } +div.htmlcgi h1 { + font-size: 150%; +} + +div.htmlcgi h2 { + font-size: 125%; +} + +div.htmlcgi h3 { + font-size: 110%; +} + +div.htmlcgi h4 { + font-size: 100%; +} + .formlabel { margin-right: 0.5em; padding-top: 0.3em; @@ -61,15 +80,16 @@ div.htmlcgi { color: red; } -.projectlist .odd { +.projectlist .odd, .bundlelist .odd { background-color: #f4f4f4; } -.projectlist th, .projectlist td { +.projectlist th, .projectlist td, +.bundlelist th, .bundlelist td { padding: 0.5ex 0.75ex; } -.projectlist th { +.projectlist th, .bundlelist th { text-align: left; } @@ -81,7 +101,9 @@ div.htmlcgi { .projectlist td:first-child, .projectlist td.type, .projectlist td.change, -.projectlist td.idle { +.projectlist td.idle, +.bundlelist th, +.bundlelist td { white-space: nowrap; } @@ -90,6 +112,10 @@ div.htmlcgi { min-width: 0 !important; } +.indent { + margin-left: 3ex !important; +} + .hover { position: relative; border-bottom: thin dotted; diff --git a/toolbox/update-all-config.sh b/toolbox/update-all-config.sh index 19b6fc8..bb3dd23 100755 --- a/toolbox/update-all-config.sh +++ b/toolbox/update-all-config.sh @@ -209,7 +209,7 @@ do_config() { fi } -mkdirs='refs info hooks ctags htmlcache objects objects/info' +mkdirs='refs info hooks ctags htmlcache bundles objects objects/info' mkfiles='config info/lastactivity' fixdpermsdirs='. refs info ctags htmlcache objects objects/info' fixdpermsrwx='refs objects' -- 2.11.4.GIT