girocco: support bundle listings
[girocco.git] / cgi / bundles.cgi
blob3839216a39b720766b5ae263e809bb69babded9f
1 #!/usr/bin/perl
3 # projlist.cgi -- support for viewing a single owner's projects
4 # Copyright (c) 2015 Kyle J. McKay. All rights reserved.
5 # License GPLv2+: GNU GPL version 2 or later.
6 # www.gnu.org/licenses/gpl-2.0.html
7 # This is free software: you are free to change and redistribute it.
8 # There is NO WARRANTY, to the extent permitted by law.
10 use strict;
11 use warnings;
13 use lib ".";
14 use Girocco::CGI;
15 use Girocco::Config;
16 use Girocco::Project;
17 use Girocco::Util;
18 use POSIX qw(strftime);
19 binmode STDOUT, ':utf8';
21 # Never refresh more often than this
22 my $min_refresh = 120;
24 # Extract the project name, we prefer PATH_INFO but will use a name= param
25 my $projname = '';
26 if ($ENV{'PATH_INFO'}) {
27 $projname = $ENV{'PATH_INFO'};
28 $projname =~ s|/+$||;
29 $projname =~ s|/bundles$||;
31 if (!$projname && $ENV{'QUERY_STRING'}) {
32 if ("&$ENV{'QUERY_STRING'}&" =~ /\&name=([^&]+)\&/) {
33 $projname = $1;
36 $projname =~ s|/+$||;
37 $projname =~ s|\.git||i;
38 $projname =~ s|^/+||;
40 my $gcgi = undef;
42 sub prefail {
43 $gcgi = Girocco::CGI->new('Project Bundles')
44 unless $gcgi;
47 # Do we have a project name?
49 if (!$projname) {
50 prefail;
51 print "<p>I need the project name as an argument now.</p>\n";
52 exit;
55 # Do we have a valid, existing project name?
57 if (!Girocco::Project::does_exist($projname, 1)) {
58 prefail;
59 if (Girocco::Project::valid_name($projname)) {
60 print "<p>Sorry but the project $projname does not exist. " .
61 "Now, how did you <em>get</em> here?!</p>\n";
62 } else {
63 print "<p>Invalid project name. Go away, sorcerer.</p>\n";
65 exit;
68 # Load the project and possibly parent projects
70 my $proj = Girocco::Project->load($projname);
71 if (!$proj) {
72 prefail;
73 print "<p>not found project $projname, that's really weird!</p>\n";
74 exit;
76 my @projs = ($proj);
77 my $parent = $projname;
78 # Walk up the parent projects loading each one that exists until we
79 # find a bundle or we've loaded all parents
80 while (!$projs[0]->has_bundle && $parent =~ m|^(.*[^/])/[^/]+$|) {
81 $parent = $1;
82 # It's okay if some parent(s) do not exist because we may simply have
83 # a grouping without any forking going on
84 next unless Girocco::Project::does_exist($parent, 1);
85 my $pproj = Girocco::Project->load($parent);
86 next unless $pproj;
87 unshift(@projs, $pproj);
90 # At this point we produce different output depending on whether or not
91 # we actually found a bundle.
93 # We also select a refresh time based on when we expect the bundle to be
94 # replaced (if we found one) or when we expect one to be created (if we didn't)
95 # If we found a bundle, it will be from $projs[0].
97 # We currently ignore all but the most recent bundle for a project.
99 my $got_bundle = $projs[0]->has_bundle;
100 my $now = time;
101 my @bundle = ();
102 my @nextgc = $projs[0]->next_gc;
103 my $willgc = $projs[0]->needs_gc;
104 my $expires = undef; # undef = unknown, 0 = expired
105 my $behind = undef; # only meaningful if we've got a bundle, undef = unknown, 0 = current
106 my $refresh = undef;
107 my $isempty = undef;
108 my $inprogress = undef;
109 if ($got_bundle) {
110 $isempty = 0;
111 @bundle = @{($projs[0]->bundles)[0]};
112 if (defined($nextgc[0])) {
113 $expires = $nextgc[0] - $now;
114 $expires = 0 if $expires < 0;
115 } else {
116 $expires = 0 if $willgc;
118 if (defined($expires)) {
119 # Refresh is half of expires time
120 $refresh = int($expires / 2);
121 } else {
122 # we have a bundle, but for some reason we have no idea when
123 # it will expire, so refresh after 12 hours
124 $refresh = 12 * 3600;
126 my $lastch = parse_any_date($projs[0]->{lastchange});
127 if (defined($lastch)) {
128 $behind = $lastch - $bundle[0];
129 $behind = 0 if $behind < 0;
131 } else {
132 # Project could be:
133 # 1) empty -- no guess about when not "empty"
134 # 2) building one now "building"
135 # 3) $nextgc[1] if defined
136 # 4) "not available"
137 if ($projs[0]->is_empty) {
138 $isempty = 1;
139 # No idea when something will be pushed so use 8 hours
140 $refresh = 8 * 3600;
141 } elsif ($projs[0]->{gc_in_progress}) {
142 $inprogress = 1;
143 $expires = 0;
144 # Building now, use the minimum refresh
145 $refresh = $min_refresh;
146 } elsif (defined($nextgc[1])) {
147 # in this case expires indicates when we expect a bundle
148 # and we use 'Expected' instead of 'Expires'
149 $expires = $nextgc[1] - $now;
150 $expires = 0 if $expires < 0;
151 # use half the time
152 $refresh = int($expires / 2);
153 } else {
154 if ($willgc) {
155 $expires = 0 if $willgc;
156 $refresh = $min_refresh;
157 } else {
158 # else not available
159 # Make the refresh 16 hours
160 $refresh = 16 * 3600;
165 my $eh = undef;
166 $refresh = $min_refresh if defined($refresh) && $refresh < $min_refresh;
167 $eh = "<meta http-equiv=\"refresh\" content=\"$refresh\" />\n"
168 if defined($refresh) && !$got_bundle; # do not refresh away instructions
170 my $projlink = url_path($Girocco::Config::gitweburl).'/'.$projname.'.git';
171 $gcgi = Girocco::CGI->new('bundles', $projname.'.git', $eh, $projlink);
173 print "<p>Downloadable Git bundle information for project <a href=\"$projlink\">".
174 "$projname</a> as of @{[strftime('%Y-%m-%d %H:%M:%S GMT',
175 (gmtime($now))[0..5], -1, -1, -1)]}:</p>\n";
177 sub format_th {
178 my ($title, $explain) = @_;
179 return $explain ?
180 "<span class=\"hover\">$title<span><span class=\"head\">$title</span>".
181 "$explain</span></span>" :
182 $title;
185 sub tenths {
186 my $v = shift;
187 $v *= 10;
188 $v += 0.5;
189 $v = int($v);
190 $v /= 10;
191 return $v;
194 sub rel_size {
195 my $v = shift || 0;
196 return "0" unless $v;
197 return "1 KiB" unless $v >= 1024;
198 $v /= 1024;
199 return tenths($v) . " KiB" if $v < 1024;
200 $v /= 1024;
201 return tenths($v) . " MiB" if $v < 1024;
202 $v /= 1024;
203 return tenths($v) . "GiB";
206 sub rel_time {
207 my $s = shift || 0;
208 return "0" unless $s;
209 return "1 second" if $s < 2;
210 return $s . " seconds" if $s < 120;
211 $s = int(($s + 30) / 60);
212 return $s . " minutes" if $s < 120;
213 $s = int(($s + 30) / 60);
214 return $s . " hours" if $s < 48;
215 $s = int(($s + 12) / 24);
216 return $s . " days" if $s < 14;
217 $s = int(($s + 3.5) / 7);
218 return $s . " weeks" if $s < 9;
219 $s = int(($s * 7 + 15.25) / 30.5);
220 return $s . " months" if $s < 24;
221 $s = int(($s + 6) / 12);
222 return $s . " years";
225 sub expires_string {
226 my $expires = shift;
227 return "unknown" unless defined($expires);
228 return "any moment" unless $expires > 0;
229 return rel_time($expires);
232 sub behind_string {
233 my $behind = shift;
234 return "unknown" unless defined($behind);
235 return "current" unless $behind > 0;
236 return rel_time($behind);
239 sub extra_string {
240 my $extra = shift;
241 return '' if !defined($extra) || $extra !~ /^\d+$/;
242 return "empty" if !$extra;
243 return '+'.rel_size($extra * 1024);
246 sub sizek_string {
247 my $sizek = shift;
248 return "unknown" if !defined($sizek) || $sizek !~ /^\d+$/;
249 return "empty" if !$sizek;
250 return rel_size($sizek * 1024);
253 my ($ex_title, $ex_explain) = $got_bundle ?
254 ("Expires", "Time remaining before bundle may become unavailable"):
255 ("Expected", "Time remaining until a bundle is generated");
257 print <<EOT;
258 <table class='bundlelist'><tr><th>Project</th
259 ><th>@{[format_th("Bundle", "Downloadable git bundle")]}</th
260 ><th>Size</th
261 ><th>@{[format_th($ex_title, $ex_explain)]}</th
262 ><th>@{[format_th("Behind", "Time since bundle creation until most recently received ref change")]}</th
263 ></tr>
266 my $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[0]->{name}.'.git';
267 print "<tr class=\"odd\"><td><a href=\"$plink\">$projs[0]->{name}</a></td>";
268 my $blink;
269 if ($got_bundle) {
270 # Git yer bundle here
271 $blink = url_path($Girocco::Config::httpbundleurl).'/'.$projs[0]->{name}.'.git/'.$bundle[1];
272 print "<td><a href=\"$blink\">$bundle[1]</a></td>".
273 "<td>@{[rel_size($bundle[2])]}</td>".
274 "<td>@{[expires_string($expires)]}</td>".
275 "<td>@{[behind_string($behind)]}</td></tr>\n";
276 } else {
277 print "<td>@{[$inprogress&&!$isempty?'building':'']}</td><td>".
278 sizek_string($isempty?0:$projs[0]->{reposizek}).
279 "</td><td>@{[expires_string($expires)]}</td><td></td></tr>\n";
281 my $extrasizek = 0;
282 for (my $i=1; $i <= $#projs; ++$i) {
283 print "<tr";
284 print " class=\"odd\"" unless $i % 2;
285 $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[$i]->{name}.'.git';
286 my $pname = $projs[$i]->{name};
287 $pname =~ s|^.*[^/]/||;
288 print "><td><span style=\"display:inline-block;height:1em;width:@{[2*($i-1)+1]}ex\"></span>".
289 "&#x2026;/<a href=\"$plink\">$pname</a></td><td></td><td>";
290 my $rsk = $projs[$i]->{reposizek};
291 $rsk = undef unless $rsk =~ /^\d+$/;
292 $rsk = 0 if !defined($rsk) && $projs[$i]->is_empty;
293 $extrasizek = defined($rsk) ? $extrasizek + $rsk : undef if defined($extrasizek);
294 print extra_string($extrasizek) if $i == $#projs;
295 print "</td><td></td><td>";
296 if ($got_bundle && $i == $#projs) {
297 my $lch = parse_any_date($projs[$i]->{lastchange});
298 my $bh = undef;
299 if (defined($lch)) {
300 $bh = $lch - $bundle[0];
301 $bh = 0 if $bh < 0;
303 print behind_string($bh);
305 print "</td></tr>\n";
307 print "</table>\n";
309 if (!$got_bundle) {
310 print <<EOT;
311 <p>At this time there is no Git downloadable bundle available for
312 project <a href="$projlink">$projname</a>.</p>
313 <p>You may want to check back later based on the information shown above.</p>
315 exit 0;
318 if ($#projs) {
319 print <<EOT;
320 <p>Although there is no Git downloadable bundle available for
321 project <a href="$projlink">$projname</a>, since it is a fork of
322 project <a href="$plink">$projs[0]->{name}</a> which <em>does</em>
323 have a bundle, that bundle can be used instead which will reduce the
324 amount that needs to be fetched with <tt>git fetch</tt> to only those
325 items that are unique to the project $projname fork.</p>
329 my $projbase = $projname;
330 $projbase =~ s|^.*[^/]/||;
331 my $fetchurl = $Girocco::Config::httppullurl;
332 $fetchurl = $Girocco::Config::httpbundleurl unless $fetchurl;
333 $fetchurl = $Girocco::Config::gitpullurl unless $fetchurl;
334 $fetchurl .= "/".$projname.".git";
335 my $forkchanges = '';
336 my $forksize = '';
337 if ($#projs) {
338 $forkchanges = " specific to the fork or";
339 $forksize = " and how different the fork is from its parent";
342 print <<EOT;
343 <div class="htmlcgi">
345 <h3>Instructions</h3>
347 <h4>0. Quick Overview</h4>
348 <div>
349 <ol>
350 <li>Download the bundle (possibly resuming the download if interrupted) using any available technique.
351 </li>
352 <li>Create a repository from the bundle.
353 </li>
354 <li>Reset the repository&#x2019;s origin to a fetch URL.
355 </li>
356 <li>Fetch the latest changes and (optionally) the current HEAD symbolic ref.
357 </li>
358 <li>Select a desired branch and check it out.
359 </li>
360 </ol>
361 </div>
363 <h4>1. Download the Bundle</h4>
364 <div class="indent">
365 <p>Download the <a href="$blink">$bundle[1]</a> file using your favorite method.</p>
366 <p>Web browsers typically provide one-click pause and resume. The <tt>curl</tt> command line
367 utility has a <tt>--continue-at</tt> option that can be used to resume an interrupted download.</p>
368 <p>Please note that it may not be possible to resume an interrupted download after the
369 &#x201c;Expires&#x201d; time shown above so plan the bundle download accordingly.</p>
370 <p>Subsequent instructions will assume the downloaded bundle <tt>$bundle[1]</tt> is available in
371 the current directory &#x2013; adjust them if that&#x2019;s not the case.</p>
372 </div>
374 <h4>2. Create a Repository from the Bundle</h4>
375 <div class="indent">
376 <p>It is possible to use the <tt>git clone</tt> command to create a repository
377 from a bundle file all in one step. However, that can result in unwanted local
378 tracking branches being created, so we do not use <tt>git clone</tt> in this
379 example.</p>
380 <p>This example creates a Git repository named &#x201c;<tt>$projbase</tt>&#x201d;
381 in the current directory, but that may be adjusted as desired:</p>
382 <pre class="indent">
383 git init $projbase
384 cd $projbase
385 git remote add origin ../$bundle[1]
386 git fetch
387 </pre>
388 </div>
390 <h4>3. Reset the Origin</h4>
391 <div class="indent">
392 <p>Assuming the current directory is still set to the newly created
393 &#x201c;<tt>$projbase</tt>&#x201d; repository, we set the origin to
394 a suitable fetch URL. Any valid fetch URL for the repository may be used
395 instead of the one shown here:</p>
396 <pre class="indent">
397 git remote set-url origin $fetchurl
398 </pre>
399 <p>Note that the $bundle[1] file is now no longer needed and may be kept or
400 discarded as desired.</p>
401 </div>
403 <h4>4. Fetch Updates</h4>
404 <div class="indent">
405 <p>Assuming the current directory is still set to the newly created
406 &#x201c;<tt>$projbase</tt>&#x201d; repository, this example fetches
407 the current <tt>HEAD</tt> symbolic ref (i.e. the branch that would
408 be checked out by default if the repository had been cloned directly
409 from a fetch URL instead of a bundle) and any changes$forkchanges made
410 to the repository since the bundle was created:</p>
411 <pre class="indent">
412 git fetch --prune origin
413 git remote set-head origin --auto
414 </pre>
415 <p>The amount retrieved by the <tt>fetch</tt> command depends on how many changes
416 have been pushed to the repository since the bundle was created$forksize.</p>
417 <p>The <tt>set-head</tt> command will be very fast and may be omitted if one&#x2019;s
418 not interested in the repository&#x2019;s default branch.</p>
419 </div>
421 <h4>5. Checkout</h4>
422 <div class="indent">
423 <p>Assuming the current directory is still set to the newly created
424 &#x201c;<tt>$projbase</tt>&#x201d; repository, the list of available
425 branches to checkout may be shown like so:</p>
426 <pre class="indent">
427 git branch -r
428 </pre>
429 <p>Note that if the repository has a default branch it will be shown in the
430 listing preceded by &#x201c;<tt>origin/HEAD -> </tt>&#x201d;.</p>
431 <p>In this case, however, the default branch is most likely
432 &#x201c;<tt>$projs[$#projs]->{HEAD}</tt>&#x201d; and may be checked out like so:</p>
433 <pre class="indent">
434 git checkout $projs[$#projs]->{HEAD}
435 </pre>
436 <p>Note that the leading &#x201c;<tt>origin/</tt>&#x201d; was omitted from the
437 branch name given to the <tt>git checkout</tt> command so that the automagic
438 DWIM logic kicks in.</p>
439 <p>The repository is now ready to be used just the same as though it had been
440 cloned directly from a fetch URL.</p>
441 </div>
443 </div>