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.
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
26 if ($ENV{'PATH_INFO'}) {
27 $projname = $ENV{'PATH_INFO'};
29 $projname =~ s
|/bundles
$||;
31 if (!$projname && $ENV{'QUERY_STRING'}) {
32 if ("&$ENV{'QUERY_STRING'}&" =~ /\&name=([^&]+)\&/) {
37 $projname =~ s
|\
.git
||i
;
43 $gcgi = Girocco
::CGI
->new('Project Bundles')
47 # Do we have a project name?
51 print "<p>I need the project name as an argument now.</p>\n";
55 # Do we have a valid, existing project name?
57 if (!Girocco
::Project
::does_exist
($projname, 1)) {
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";
63 print "<p>Invalid project name. Go away, sorcerer.</p>\n";
68 # Load the project and possibly parent projects
70 my $proj = Girocco
::Project
->load($projname);
73 print "<p>not found project $projname, that's really weird!</p>\n";
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
|^(.*[^/])/[^/]+$|) {
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);
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;
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
108 my $inprogress = undef;
111 @bundle = @
{($projs[0]->bundles)[0]};
112 if (defined($nextgc[0])) {
113 $expires = $nextgc[0] - $now;
114 $expires = 0 if $expires < 0;
116 $expires = 0 if $willgc;
118 if (defined($expires)) {
119 # Refresh is half of expires time
120 $refresh = int($expires / 2);
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;
133 # 1) empty -- no guess about when not "empty"
134 # 2) building one now "building"
135 # 3) $nextgc[1] if defined
137 if ($projs[0]->is_empty) {
139 # No idea when something will be pushed so use 8 hours
141 } elsif ($projs[0]->{gc_in_progress
}) {
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;
152 $refresh = int($expires / 2);
155 $expires = 0 if $willgc;
156 $refresh = $min_refresh;
159 # Make the refresh 16 hours
160 $refresh = 16 * 3600;
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";
178 my ($title, $explain) = @_;
180 "<span class=\"hover\">$title<span><span class=\"head\">$title</span>".
181 "$explain</span></span>" :
196 return "0" unless $v;
197 return "1 KiB" unless $v >= 1024;
199 return tenths
($v) . " KiB" if $v < 1024;
201 return tenths
($v) . " MiB" if $v < 1024;
203 return tenths
($v) . "GiB";
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";
227 return "unknown" unless defined($expires);
228 return "any moment" unless $expires > 0;
229 return rel_time
($expires);
234 return "unknown" unless defined($behind);
235 return "current" unless $behind > 0;
236 return rel_time
($behind);
241 return '' if !defined($extra) || $extra !~ /^\d+$/;
242 return "empty" if !$extra;
243 return '+'.rel_size
($extra * 1024);
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");
258 <table class='bundlelist'><tr><th>Project</th
259 ><th>@{[format_th("Bundle", "Downloadable git bundle")]}</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
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>";
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";
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";
282 for (my $i=1; $i <= $#projs; ++$i) {
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 "…/<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
});
300 $bh = $lch - $bundle[0];
303 print behind_string
($bh);
305 print "</td></tr>\n";
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>
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 = '';
338 $forkchanges = " specific to the fork or";
339 $forksize = " and how different the fork is from its parent";
343 <div class="htmlcgi">
345 <h3>Instructions</h3>
347 <h4>0. Quick Overview</h4>
350 <li>Download the bundle (possibly resuming the download if interrupted) using any available technique.
352 <li>Create a repository from the bundle.
354 <li>Reset the repository’s origin to a fetch URL.
356 <li>Fetch the latest changes and (optionally) the current HEAD symbolic ref.
358 <li>Select a desired branch and check it out.
363 <h4>1. Download the Bundle</h4>
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 “Expires” 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 – adjust them if that’s not the case.</p>
374 <h4>2. Create a Repository from the Bundle</h4>
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
380 <p>This example creates a Git repository named “<tt>$projbase</tt>”
381 in the current directory, but that may be adjusted as desired:</p>
385 git remote add origin ../$bundle[1]
390 <h4>3. Reset the Origin</h4>
392 <p>Assuming the current directory is still set to the newly created
393 “<tt>$projbase</tt>” 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>
397 git remote set-url origin $fetchurl
399 <p>Note that the $bundle[1] file is now no longer needed and may be kept or
400 discarded as desired.</p>
403 <h4>4. Fetch Updates</h4>
405 <p>Assuming the current directory is still set to the newly created
406 “<tt>$projbase</tt>” 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>
412 git fetch --prune origin
413 git remote set-head origin --auto
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’s
418 not interested in the repository’s default branch.</p>
423 <p>Assuming the current directory is still set to the newly created
424 “<tt>$projbase</tt>” repository, the list of available
425 branches to checkout may be shown like so:</p>
429 <p>Note that if the repository has a default branch it will be shown in the
430 listing preceded by “<tt>origin/HEAD -> </tt>”.</p>
431 <p>In this case, however, the default branch is most likely
432 “<tt>$projs[$#projs]->{HEAD}</tt>” and may be checked out like so:</p>
434 git checkout $projs[$#projs]->{HEAD}
436 <p>Note that the leading “<tt>origin/</tt>” 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>