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
+
+
+- Download the bundle (possibly resuming the download if interrupted) using any available technique.
+
+- Create a repository from the bundle.
+
+- Reset the repository’s origin to a fetch URL.
+
+- Fetch the latest changes and (optionally) the current HEAD symbolic ref.
+
+- Select a desired branch and check it out.
+
+
+
+
+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