git-shell-verify: only check read-only mode for receive-pack
[girocco.git] / cgi / bundles.cgi
bloba317e84fc388a882f1dce040f8b7604d81e1b074
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 "__BASEDIR__";
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\" _data=\"$title\"></span>".
183 "<span class=\"none\" /><br />(</span>$explain<span class=\"none\">)</span></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 _nbsp {
207 my $text = shift;
208 $text =~ s/ /&#160;/g;
209 return $text;
212 sub expires_string {
213 my $expires = shift;
214 return "unknown" unless defined($expires);
215 return "any moment" unless $expires > 0;
216 return _nbsp(rel_time($expires));
219 sub behind_string {
220 my $behind = shift;
221 return "unknown" unless defined($behind);
222 return "current" unless $behind > 0;
223 return _nbsp(rel_time($behind));
226 sub extra_string {
227 my $extra = shift;
228 return '' if !defined($extra) || $extra !~ /^\d+$/;
229 return "empty" if !$extra;
230 return _nbsp('+'.human_size($extra * 1024));
233 sub sizek_string {
234 my $sizek = shift;
235 return "unknown" if !defined($sizek) || $sizek !~ /^\d+$/;
236 return "empty" if !$sizek;
237 return _nbsp(human_size($sizek * 1024));
240 my ($ex_title, $ex_explain) = $got_bundle ?
241 ("Expires", "Time remaining before bundle may become unavailable"):
242 ("Expected", "Time remaining until a bundle is generated");
244 print <<EOT;
245 <table class='bundlelist'><tr valign="top" align="left"><th>Project</th
246 ><th>@{[format_th("Bundle", "Downloadable git bundle")]}</th
247 ><th>Size</th
248 ><th>@{[format_th($ex_title, $ex_explain)]}</th
249 ><th>@{[format_th("Behind", "Time since bundle creation until most recently received ref change")]}</th
250 ></tr>
253 my $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[0]->{name}.'.git';
254 print "<tr valign=\"top\" class=\"odd\"><td><a href=\"$plink\">$projs[0]->{name}</a></td>";
255 my $blink;
256 if ($got_bundle) {
257 # Git yer bundle here
258 $blink = url_path($Girocco::Config::httpbundleurl).'/'.$projs[0]->{name}.'.git/'.$bundle[1];
259 print "<td><a rel='nofollow' href=\"$blink\">$bundle[1]</a></td>".
260 "<td>@{[_nbsp(human_size($bundle[2]))]}</td>".
261 "<td>@{[expires_string($expires)]}</td>".
262 "<td>@{[behind_string($behind)]}</td></tr>\n";
263 } else {
264 print "<td>@{[$inprogress&&!$isempty?'building':'']}</td><td>".
265 _nbsp(sizek_string($isempty?0:$projs[0]->{reposizek})).
266 "</td><td>@{[expires_string($expires)]}</td><td></td></tr>\n";
268 my $extrasizek = 0;
269 for (my $i=1; $i <= $#projs; ++$i) {
270 print "<tr";
271 print " class=\"odd\"" unless $i % 2;
272 $plink = url_path($Girocco::Config::gitweburl).'/'.$projs[$i]->{name}.'.git';
273 my $pname = $projs[$i]->{name};
274 $pname =~ s|^.*[^/]/||;
275 print "><td><span style=\"display:inline-block;height:1em;width:@{[2*($i-1)+1]}ex\"></span>".
276 "&#x2026;/<a href=\"$plink\">$pname</a></td><td></td><td>";
277 my $rsk = $projs[$i]->{reposizek};
278 $rsk = undef unless $rsk =~ /^\d+$/;
279 $rsk = 0 if !defined($rsk) && $projs[$i]->is_empty;
280 $extrasizek = defined($rsk) ? $extrasizek + $rsk : undef if defined($extrasizek);
281 print extra_string($extrasizek) if $i == $#projs;
282 print "</td><td></td><td>";
283 if ($got_bundle && $i == $#projs) {
284 my $lch = parse_any_date($projs[$i]->{lastchange});
285 my $bh = undef;
286 if (defined($lch)) {
287 $bh = $lch - $bundle[0];
288 $bh = 0 if $bh < 0;
290 print behind_string($bh);
292 print "</td></tr>\n";
294 print "</table>\n";
296 if (!$got_bundle) {
297 print <<EOT;
298 <p>At this time there is no Git downloadable bundle available for
299 project <a href="$projlink">$projname</a>.</p>
300 <p>You may want to check back later based on the information shown above.</p>
302 exit 0;
305 if ($#projs) {
306 print <<EOT;
307 <p>Although there is no Git downloadable bundle available for
308 project <a href="$projlink">$projname</a>, since it is a fork of
309 project <a href="$plink">$projs[0]->{name}</a> which <em>does</em>
310 have a bundle, that bundle can be used instead which will reduce the
311 amount that needs to be fetched with <tt>git fetch</tt> to only those
312 items that are unique to the project $projname fork.</p>
316 my $projbase = $projname;
317 $projbase =~ s|^.*[^/]/||;
318 my $fetchurl = $Girocco::Config::httppullurl;
319 $fetchurl = $Girocco::Config::httpbundleurl unless $fetchurl;
320 $fetchurl = $Girocco::Config::gitpullurl unless $fetchurl;
321 $fetchurl .= "/".$projname.".git";
322 my $forkchanges = '';
323 my $forksize = '';
324 if ($#projs) {
325 $forkchanges = " specific to the fork or";
326 $forksize = " and how different the fork is from its parent";
329 print <<EOT;
330 <div class="htmlcgi">
332 <h3>Instructions</h3>
334 <h4>0. Quick Overview</h4>
335 <div>
336 <ol>
337 <li>Download the bundle (possibly resuming the download if interrupted) using any available technique.
338 </li>
339 <li>Create a repository from the bundle.
340 </li>
341 <li>Reset the repository&#x2019;s origin to a fetch URL.
342 </li>
343 <li>Fetch the latest changes and (optionally) the current HEAD symbolic ref.
344 </li>
345 <li>Select a desired branch and check it out.
346 </li>
347 </ol>
348 </div>
350 <h4>1. Download the Bundle</h4>
351 <div class="indent">
352 <p>Download the <a rel='nofollow' href="$blink">$bundle[1]</a> file using your favorite method.</p>
353 <p>Web browsers typically provide one-click pause and resume. The <tt>curl</tt> command line
354 utility has a <tt>--continue-at</tt> option that can be used to resume an interrupted download.</p>
355 <p><em>Please note that it may not be possible to resume an interrupted download after the
356 &#x201c;Expires&#x201d; time shown above so plan the bundle download accordingly.</em></p>
357 <p>Subsequent instructions will assume the downloaded bundle <tt>$bundle[1]</tt> is available in
358 the current directory &#x2013; adjust them if that&#x2019;s not the case.</p>
359 </div>
361 <h4>2. Create a Repository from the Bundle</h4>
362 <div class="indent">
363 <p>It is possible to use the <tt>git clone</tt> command to create a repository
364 from a bundle file all in one step. However, that can result in unwanted local
365 tracking branches being created, so we do not use <tt>git clone</tt> in this
366 example.</p>
367 <p>This example creates a Git repository named &#x201c;<tt>$projbase</tt>&#x201d;
368 in the current directory, but that may be adjusted as desired:</p>
369 <pre class="indent">
370 git init $projbase
371 cd $projbase
372 git remote add origin ../$bundle[1]
373 git fetch
374 </pre>
375 </div>
377 <h4>3. Reset the Origin</h4>
378 <div class="indent">
379 <p>Assuming the current directory is still set to the newly created
380 &#x201c;<tt>$projbase</tt>&#x201d; repository, we set the origin to
381 a suitable fetch URL. Any valid fetch URL for the repository may be used
382 instead of the one shown here:</p>
383 <pre class="indent">
384 git remote set-url origin $fetchurl
385 </pre>
386 <p>Note that the $bundle[1] file is now no longer needed and may be kept or
387 discarded as desired.</p>
388 </div>
390 <h4>4. Fetch Updates</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, this example fetches
394 the current <tt>HEAD</tt> symbolic ref (i.e. the branch that would
395 be checked out by default if the repository had been cloned directly
396 from a fetch URL instead of a bundle) and any changes$forkchanges made
397 to the repository since the bundle was created:</p>
398 <pre class="indent">
399 git fetch --prune origin
400 git remote set-head origin --auto
401 </pre>
402 <p>The amount retrieved by the <tt>fetch</tt> command depends on how many changes
403 have been pushed to the repository since the bundle was created$forksize.</p>
404 <p>The <tt>set-head</tt> command will be very fast and may be omitted if one&#x2019;s
405 not interested in the repository&#x2019;s default branch.</p>
406 </div>
408 <h4>5. Checkout</h4>
409 <div class="indent">
410 <p>Assuming the current directory is still set to the newly created
411 &#x201c;<tt>$projbase</tt>&#x201d; repository, the list of available
412 branches to checkout may be shown like so:</p>
413 <pre class="indent">
414 git branch -r
415 </pre>
416 <p>Note that if the repository has a default branch it will be shown in the
417 listing preceded by &#x201c;<tt>origin/HEAD -> </tt>&#x201d;.</p>
418 <p>In this case, however, the default branch is most likely
419 &#x201c;<tt>$projs[$#projs]->{HEAD}</tt>&#x201d; and may be checked out like so:</p>
420 <pre class="indent">
421 git checkout $projs[$#projs]->{HEAD}
422 </pre>
423 <p>Note that the leading &#x201c;<tt>origin/</tt>&#x201d; was omitted from the
424 branch name given to the <tt>git checkout</tt> command so that the automagic
425 DWIM (Do What I Mean) logic kicks in.</p>
426 <p>The repository is now ready to be used just the same as though it had been
427 cloned directly from a fetch URL.</p>
428 </div>
430 </div>