install: make sure to record full paths for utilities
[girocco.git] / cgi / bundles.cgi
blobff4b79e98c51be2fd0a905584a591223c62d0069
1 #!/usr/bin/perl
3 # bundles.cgi -- support for viewing a project's downloadable bundles
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 my $lastgc = parse_any_date($projs[0]->{lastgc});
113 $bundle[0] = $lastgc if defined($lastgc) && $lastgc > $bundle[0];
114 if (defined($nextgc[0])) {
115 $expires = $nextgc[0] - $now;
116 $expires = 0 if $expires < 0;
117 } else {
118 $expires = 0 if $willgc;
120 if (defined($expires)) {
121 # Refresh is half of expires time
122 $refresh = int($expires / 2);
123 } else {
124 # we have a bundle, but for some reason we have no idea when
125 # it will expire, so refresh after 12 hours
126 $refresh = 12 * 3600;
128 my $lastch = parse_any_date($projs[0]->{lastchange});
129 if (defined($lastch)) {
130 $behind = $lastch - $bundle[0];
131 $behind = 0 if $behind < 0;
133 } else {
134 # Project could be:
135 # 1) empty -- no guess about when not "empty"
136 # 2) building one now "building"
137 # 3) $nextgc[1] if defined
138 # 4) "not available"
139 if ($projs[0]->is_empty) {
140 $isempty = 1;
141 # No idea when something will be pushed so use 8 hours
142 $refresh = 8 * 3600;
143 } elsif ($projs[0]->{gc_in_progress}) {
144 $inprogress = 1;
145 $expires = 0;
146 # Building now, use the minimum refresh
147 $refresh = $min_refresh;
148 } elsif (defined($nextgc[1])) {
149 # in this case expires indicates when we expect a bundle
150 # and we use 'Expected' instead of 'Expires'
151 $expires = $nextgc[1] - $now;
152 $expires = 0 if $expires < 0;
153 # use half the time
154 $refresh = int($expires / 2);
155 } else {
156 if ($willgc) {
157 $expires = 0 if $willgc;
158 $refresh = $min_refresh;
159 } else {
160 # else not available
161 # Make the refresh 16 hours
162 $refresh = 16 * 3600;
167 my $eh = undef;
168 $refresh = $min_refresh if defined($refresh) && $refresh < $min_refresh;
169 $eh = "<meta http-equiv=\"refresh\" content=\"$refresh\" />\n"
170 if defined($refresh) && !$got_bundle; # do not refresh away instructions
172 my $projlink = url_path($Girocco::Config::gitweburl).'/'.$projname.'.git';
173 $gcgi = Girocco::CGI->new('bundles', $projname.'.git', $eh, $projlink);
175 print "<p>Downloadable Git bundle information for project <a href=\"$projlink\">".
176 "$projname</a> as of @{[strftime('%Y-%m-%d %H:%M:%S UTC',
177 (gmtime($now))[0..5], -1, -1, -1)]}:</p>\n";
179 sub format_th {
180 my ($title, $explain) = @_;
181 return $explain ?
182 "<span class=\"hover\">$title<span><span class=\"head\">$title</span>".
183 "$explain</span></span>" :
184 $title;
187 sub rel_time {
188 my $s = shift || 0;
189 return "0" unless $s;
190 return "1 second" if $s < 2;
191 return $s . " seconds" if $s < 120;
192 $s = int(($s + 30) / 60);
193 return $s . " minutes" if $s < 120;
194 $s = int(($s + 30) / 60);
195 return $s . " hours" if $s < 48;
196 $s = int(($s + 12) / 24);
197 return $s . " days" if $s < 14;
198 $s = int(($s + 3.5) / 7);
199 return $s . " weeks" if $s < 9;
200 $s = int(($s * 7 + 15.25) / 30.5);
201 return $s . " months" if $s < 24;
202 $s = int(($s + 6) / 12);
203 return $s . " years";
206 sub expires_string {
207 my $expires = shift;
208 return "unknown" unless defined($expires);
209 return "any moment" unless $expires > 0;
210 return rel_time($expires);
213 sub behind_string {
214 my $behind = shift;
215 return "unknown" unless defined($behind);
216 return "current" unless $behind > 0;
217 return rel_time($behind);
220 sub extra_string {
221 my $extra = shift;
222 return '' if !defined($extra) || $extra !~ /^\d+$/;
223 return "empty" if !$extra;
224 return '+'.human_size($extra * 1024);
227 sub sizek_string {
228 my $sizek = shift;
229 return "unknown" if !defined($sizek) || $sizek !~ /^\d+$/;
230 return "empty" if !$sizek;
231 return human_size($sizek * 1024);
234 my ($ex_title, $ex_explain) = $got_bundle ?
235 ("Expires", "Time remaining before bundle may become unavailable"):
236 ("Expected", "Time remaining until a bundle is generated");
238 print <<EOT;
239 <table class='bundlelist'><tr><th>Project</th
240 ><th>@{[format_th("Bundle", "Downloadable git bundle")]}</th
241 ><th>Size</th
242 ><th>@{[format_th($ex_title, $ex_explain)]}</th
243 ><th>@{[format_th("Behind", "Time since bundle creation until most recently received ref change")]}</th
244 ></tr>
247 my $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[0]->{name}.'.git';
248 print "<tr class=\"odd\"><td><a href=\"$plink\">$projs[0]->{name}</a></td>";
249 my $blink;
250 if ($got_bundle) {
251 # Git yer bundle here
252 $blink = url_path($Girocco::Config::httpbundleurl).'/'.$projs[0]->{name}.'.git/'.$bundle[1];
253 print "<td><a rel='nofollow' href=\"$blink\">$bundle[1]</a></td>".
254 "<td>@{[human_size($bundle[2])]}</td>".
255 "<td>@{[expires_string($expires)]}</td>".
256 "<td>@{[behind_string($behind)]}</td></tr>\n";
257 } else {
258 print "<td>@{[$inprogress&&!$isempty?'building':'']}</td><td>".
259 sizek_string($isempty?0:$projs[0]->{reposizek}).
260 "</td><td>@{[expires_string($expires)]}</td><td></td></tr>\n";
262 my $extrasizek = 0;
263 for (my $i=1; $i <= $#projs; ++$i) {
264 print "<tr";
265 print " class=\"odd\"" unless $i % 2;
266 $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[$i]->{name}.'.git';
267 my $pname = $projs[$i]->{name};
268 $pname =~ s|^.*[^/]/||;
269 print "><td><span style=\"display:inline-block;height:1em;width:@{[2*($i-1)+1]}ex\"></span>".
270 "&#x2026;/<a href=\"$plink\">$pname</a></td><td></td><td>";
271 my $rsk = $projs[$i]->{reposizek};
272 $rsk = undef unless $rsk =~ /^\d+$/;
273 $rsk = 0 if !defined($rsk) && $projs[$i]->is_empty;
274 $extrasizek = defined($rsk) ? $extrasizek + $rsk : undef if defined($extrasizek);
275 print extra_string($extrasizek) if $i == $#projs;
276 print "</td><td></td><td>";
277 if ($got_bundle && $i == $#projs) {
278 my $lch = parse_any_date($projs[$i]->{lastchange});
279 my $bh = undef;
280 if (defined($lch)) {
281 $bh = $lch - $bundle[0];
282 $bh = 0 if $bh < 0;
284 print behind_string($bh);
286 print "</td></tr>\n";
288 print "</table>\n";
290 if (!$got_bundle) {
291 print <<EOT;
292 <p>At this time there is no Git downloadable bundle available for
293 project <a href="$projlink">$projname</a>.</p>
294 <p>You may want to check back later based on the information shown above.</p>
296 exit 0;
299 if ($#projs) {
300 print <<EOT;
301 <p>Although there is no Git downloadable bundle available for
302 project <a href="$projlink">$projname</a>, since it is a fork of
303 project <a href="$plink">$projs[0]->{name}</a> which <em>does</em>
304 have a bundle, that bundle can be used instead which will reduce the
305 amount that needs to be fetched with <tt>git fetch</tt> to only those
306 items that are unique to the project $projname fork.</p>
310 my $projbase = $projname;
311 $projbase =~ s|^.*[^/]/||;
312 my $fetchurl = $Girocco::Config::httppullurl;
313 $fetchurl = $Girocco::Config::httpbundleurl unless $fetchurl;
314 $fetchurl = $Girocco::Config::gitpullurl unless $fetchurl;
315 $fetchurl .= "/".$projname.".git";
316 my $forkchanges = '';
317 my $forksize = '';
318 if ($#projs) {
319 $forkchanges = " specific to the fork or";
320 $forksize = " and how different the fork is from its parent";
323 print <<EOT;
324 <div class="htmlcgi">
326 <h3>Instructions</h3>
328 <h4>0. Quick Overview</h4>
329 <div>
330 <ol>
331 <li>Download the bundle (possibly resuming the download if interrupted) using any available technique.
332 </li>
333 <li>Create a repository from the bundle.
334 </li>
335 <li>Reset the repository&#x2019;s origin to a fetch URL.
336 </li>
337 <li>Fetch the latest changes and (optionally) the current HEAD symbolic ref.
338 </li>
339 <li>Select a desired branch and check it out.
340 </li>
341 </ol>
342 </div>
344 <h4>1. Download the Bundle</h4>
345 <div class="indent">
346 <p>Download the <a rel='nofollow' href="$blink">$bundle[1]</a> file using your favorite method.</p>
347 <p>Web browsers typically provide one-click pause and resume. The <tt>curl</tt> command line
348 utility has a <tt>--continue-at</tt> option that can be used to resume an interrupted download.</p>
349 <p><em>Please note that it may not be possible to resume an interrupted download after the
350 &#x201c;Expires&#x201d; time shown above so plan the bundle download accordingly.</em></p>
351 <p>Subsequent instructions will assume the downloaded bundle <tt>$bundle[1]</tt> is available in
352 the current directory &#x2013; adjust them if that&#x2019;s not the case.</p>
353 </div>
355 <h4>2. Create a Repository from the Bundle</h4>
356 <div class="indent">
357 <p>It is possible to use the <tt>git clone</tt> command to create a repository
358 from a bundle file all in one step. However, that can result in unwanted local
359 tracking branches being created, so we do not use <tt>git clone</tt> in this
360 example.</p>
361 <p>This example creates a Git repository named &#x201c;<tt>$projbase</tt>&#x201d;
362 in the current directory, but that may be adjusted as desired:</p>
363 <pre class="indent">
364 git init $projbase
365 cd $projbase
366 git remote add origin ../$bundle[1]
367 git fetch
368 </pre>
369 </div>
371 <h4>3. Reset the Origin</h4>
372 <div class="indent">
373 <p>Assuming the current directory is still set to the newly created
374 &#x201c;<tt>$projbase</tt>&#x201d; repository, we set the origin to
375 a suitable fetch URL. Any valid fetch URL for the repository may be used
376 instead of the one shown here:</p>
377 <pre class="indent">
378 git remote set-url origin $fetchurl
379 </pre>
380 <p>Note that the $bundle[1] file is now no longer needed and may be kept or
381 discarded as desired.</p>
382 </div>
384 <h4>4. Fetch Updates</h4>
385 <div class="indent">
386 <p>Assuming the current directory is still set to the newly created
387 &#x201c;<tt>$projbase</tt>&#x201d; repository, this example fetches
388 the current <tt>HEAD</tt> symbolic ref (i.e. the branch that would
389 be checked out by default if the repository had been cloned directly
390 from a fetch URL instead of a bundle) and any changes$forkchanges made
391 to the repository since the bundle was created:</p>
392 <pre class="indent">
393 git fetch --prune origin
394 git remote set-head origin --auto
395 </pre>
396 <p>The amount retrieved by the <tt>fetch</tt> command depends on how many changes
397 have been pushed to the repository since the bundle was created$forksize.</p>
398 <p>The <tt>set-head</tt> command will be very fast and may be omitted if one&#x2019;s
399 not interested in the repository&#x2019;s default branch.</p>
400 </div>
402 <h4>5. Checkout</h4>
403 <div class="indent">
404 <p>Assuming the current directory is still set to the newly created
405 &#x201c;<tt>$projbase</tt>&#x201d; repository, the list of available
406 branches to checkout may be shown like so:</p>
407 <pre class="indent">
408 git branch -r
409 </pre>
410 <p>Note that if the repository has a default branch it will be shown in the
411 listing preceded by &#x201c;<tt>origin/HEAD -> </tt>&#x201d;.</p>
412 <p>In this case, however, the default branch is most likely
413 &#x201c;<tt>$projs[$#projs]->{HEAD}</tt>&#x201d; and may be checked out like so:</p>
414 <pre class="indent">
415 git checkout $projs[$#projs]->{HEAD}
416 </pre>
417 <p>Note that the leading &#x201c;<tt>origin/</tt>&#x201d; was omitted from the
418 branch name given to the <tt>git checkout</tt> command so that the automagic
419 DWIM (Do What I Mean) logic kicks in.</p>
420 <p>The repository is now ready to be used just the same as though it had been
421 cloned directly from a fetch URL.</p>
422 </div>
424 </div>