make-apache-conf.sh: enhance and tidy up
[girocco.git] / toolbox / projtool.pl
blobd7494f2bb7d55643fd7f800d01943e964ba072d2
1 #!/usr/bin/perl
3 # projtool.pl - command line Girocco project maintenance tool
4 # Copyright (C) 2016,2017,2020 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;
12 use vars qw($VERSION);
13 BEGIN {*VERSION = \'1.0.1'}
14 use File::Basename;
15 use Digest::MD5 qw(md5_hex);
16 use IO::Socket;
17 use Cwd qw(realpath);
18 use POSIX qw(strftime);
19 use lib "__BASEDIR__";
20 use Girocco::Config;
21 use Girocco::Util;
22 use Girocco::HashUtil;
23 use Girocco::CLIUtil;
24 use Girocco::Project;
25 use Girocco::User;
27 exit(&main(@ARGV)||0);
29 our $help;
30 BEGIN {$help = <<'HELP'}
31 Usage: %s [--quiet] <command> <options>
33 help [<command>]
34 show full help or just for <command> if given
36 list [--verbose] [--sort=lcname|name|owner|gid|no] [--owner] [<regexp>]
37 list all projects (default is --sort=lcname)
38 limit to project names matching <regex> if given
39 match <regex> against owner instead of project name with --owner
41 create [--force] [--no-alternates] [--orphan] [<option>...] <project>
42 create new project <project> (prompted)
43 <option> can be:
44 --no-alternates skip setup of objects/info/alternates
45 --orphan allow creation of subproject w/o a parent
46 -p use mkdir -p during --orphan creation
47 --no-password set password crypt to invalid value "unknown"
48 --no-owner leave the gitweb.owner config unset
49 --mirror=<url> create a mirror from <url>
50 --full-mirror mirror all refs
51 --push[=<list>] create a push project
52 --desc=<string> specify project description w/o prompt
53 --homepage=<url> specify project homepage URL w/o prompt
54 --defaults do no interactive prompting at all
55 Using --no-password skips the prompts for password, using
56 --no-owner skips the prompt for owner and using --mirror=<url>
57 or --push[=<list>] skips the prompts for mirror URL and
58 heads-only and push users. With --defaults if neither
59 --mirror=<url> nor --push[=<list>] is given then --push will
60 be implied. Using --desc=<string> will force a specific
61 description (including an empty string) and skip the prompt for
62 it. Otherwise a non-empty default description will always be
63 supplied in lieu of an empty or omitted description.
65 adopt [--force] [--type=mirror|push] [<option>...] <project> [<users>]
66 adopt project <project>
67 type of project is guessed if --type=<type> omitted
68 <users> is same as <newuserslist> for setusers command
69 <option> can be:
70 --dry-run do all the checks but don't perform adoption
71 --verbose show project info dump (useful with --dry-run)
72 --no-users no push users at all (<users> must be omitted)
73 --no-owner leave the gitweb.owner config totally unchanged
74 --owner=<val> set the gitweb.owner config to <val>
75 Both --no-owner and --owner=<val> may NOT be given, with neither
76 take owner from preexisting gitweb.owner else use admin setting.
77 For mirrors <users> is ignored otherwise if no <users> and no
78 --no-users option the push users list will consist of the single
79 user name matching the owner or empty if none or more than one.
80 With --dry-run <project> can be an absolute path to a git dir.
82 remove [--force] [--really-delete] [--keep-forks] <project>
83 remove project <project>
84 do not move to _recyclebin with --really-delete (just rm -rf)
85 remove projects with forks (by keeping forks) using --keep-forks
87 show <project>
88 show project <project>
90 listheads <project>
91 list all available heads for <project> and indicate current head
93 listtags [--verbose] <project>
94 list all ctags on project <project>
96 deltags <project> [-i] <tagstodel>
97 remove any ctags on project <project> present in <tagstodel>
98 <tagstodel> is space or comma separated list of tags to remove
99 with -i match against <tagstodel> without regard to letter case
101 addtags <project> <tagstoadd>
102 add ctags to project <project>
103 <tagstoadd> is space or comma separated list of tags to add
105 chpass [--force] <project> [random | unknown]
106 change project <project> password (prompted)
107 with "random" set to random password
108 with "unknown" set password hash to invalid value "unknown"
110 checkpw <project>
111 check project <project> password for a match (prompted)
113 gc [--force | --auto] [--redelta | --recompress] <project>
114 run the gc.sh script on project <project>
115 with --auto let the gc.sh script decide what to do
116 with --force cause a full gc to take place (force_gc=1)
117 with neither --auto nor --force do a mini or if needed a full gc
118 (in other words just touch .needsgc and run gc.sh)
119 with --redelta a full gc will use pack-objects --no-reuse-delta
120 with --recompress a full gc uses pack-objects --no-reuse-object
121 (--no-reuse-delta and --no-reuse-object are accepted as aliases)
122 unless the global --quiet option is given show_progress=1 is used
124 update [--force] [--quiet | --summary] <project>
125 run the update.sh script on project <project>
126 with --force cause a fetch to always take place (force_update=1)
127 with --quiet only show errors (show_progress is left unset)
128 with --summary show progress and ref summary (show_progress=1)
129 with neither --quiet nor --summary show it all (show_progress=2)
131 remirror [--force] <project>
132 initiate a remirror of project <project>
134 [set]owner [--force] <project> <newowner>
135 set project <project> owner to <newowner>
136 without "set" and only 1 arg, just show current project owner
138 [set]desc [--force] <project> <newdesc>
139 set project <project> description to <newdesc>
140 without "set" and only 1 arg, just show current project desc
142 [set]readme [--force] <project> <newsetting>
143 set project <project> readme to <newsetting>
144 <newsetting> is automatic|suppressed|-|[@]filename
145 without "set" and only 2 args, just show current readme setting
147 [set]head <project> <newhead>
148 set project <project> HEAD symbolic ref to <newhead>
149 without "set" and only 1 arg, just show current project HEAD
151 [set]bool [--force] <project> <flagname> <boolvalue>
152 set project <project> boolean <flagname> to <boolvalue>
153 <flagname> is cleanmirror|reverseorder|summaryonly|statusupdtaes
154 without "set" and only 2 args, just show current flag value
156 [set]hooks [--force] <project> local | global | <path>
157 set project <project> hookspath to local, global or <path>
158 without "set" and only 1 arg, just show current hookspath
160 [set]autogchack <project> <boolvalue> | unset
161 set project <project> autogchack to <boolvalue> or "unset" it
162 without "set" just show current autogchack setting if enabled
163 with "set" autogchack must be enabled in Config.pm for the
164 type of project and maintain-auto-gc-hack.sh is always run
166 [set]url [--force] <project> <urlname> <newurlvalue>
167 set project <project> url <urlname> to <newurlvalue>
168 <urlname> is baseurl|homepage|notifyjson
169 without "set" and only 2 args, just show current url value
171 [set]msgs [--force] <project> <msgsname> <eaddrlist>
172 set project <project> msgs <msgsname> to <addrlist>
173 <msgsname> is notifymail|notifytag
174 <eaddrlist> is space or comma separated list of email addresses
175 without "set" and only 2 args, just show current msgs value
177 [set]users [--force] <project> <newuserslist>
178 set push project <project> users list to <newuserslist>
179 <newuserslist> is space or comma separated list of user names
180 without "set" and only 1 arg, just show current users list
182 get <project> <fieldname>
183 show project <project> field <fieldname>
184 <fieldname> is owner|desc|readme|head|hooks|users
185 or <flagname>|autogchack|<urlname>|<msgsname>
187 set [--force] <project> <fieldname> <newfieldvalue>
188 set project <project> field <fieldname> to <newfieldvalue>
189 <fieldname> same as for get
190 <newfieldvalue> same as for corresponding set... command
191 HELP
193 our $quiet;
194 our $setopt;
195 sub die_usage {
196 my $sub = shift || diename;
197 if ($sub) {
198 die "Invalid arguments to $sub command -- try \"help\"\n";
199 } else {
200 die "Invalid arguments -- try \"help\"\n";
204 sub get_readme_desc {
205 my $rm = shift;
206 defined($rm) or $rm = '';
207 if (length($rm)) {
208 my $test = $rm;
209 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
210 $test =~ s/\s+//s;
211 return $test eq '' ? "suppressed" : "length " . length($rm);
212 } else {
213 return "automatic";
217 sub get_ctag_counts {
218 my $project = shift;
219 my $compact = shift;
220 my @ctags = ();
221 foreach ($project->get_ctag_names) {
222 my $val = 0;
223 my $ct;
224 if (open $ct, '<', $project->{path}."/ctags/$_") {
225 my $count = <$ct>;
226 close $ct;
227 defined $count or $count = '';
228 chomp $count;
229 $val = $count =~ /^[1-9]\d*$/ ? $count : 1;
231 if ($compact) {
232 if ($val == 1) {
233 push(@ctags, $_);
234 } elsif ($val > 1) {
235 push(@ctags, $_."(".$val.")");
237 } else {
238 push(@ctags, [$_, $val]) if $val;
241 @ctags;
244 sub get_clean_project {
245 my $project = get_project(@_);
246 delete $project->{loaded};
247 delete $project->{base_path};
248 delete $project->{ccrypt};
249 /^orig/i || !defined($project->{$_}) and delete $project->{$_} foreach keys %$project;
250 $project->{owner} = $project->{email}; delete $project->{email};
251 $project->{homepage} = $project->{hp}; delete $project->{hp};
252 $project->{baseurl} = $project->{url}; delete $project->{url};
253 if (defined($project->{path}) && $project->{path} ne "") {
254 my $rp = realpath($project->{path});
255 defined($rp) && $rp ne "" and $project->{realpath} = $rp;
256 if (-f "$rp/objects/info/packs") {
257 my $ipt = (stat _)[9];
258 defined($ipt) and $project->{infopackstime} =
259 strftime("%Y-%m-%d %H:%M:%S %z", localtime($ipt));
262 my $owner = $project->{owner};
263 if ($owner) {
264 $owner = lc($owner);
265 my @owner_users = map {$owner eq lc($$_[4]) ? $$_[1] : ()} get_all_users;
266 $project->{owner_users} = \@owner_users if @owner_users;
268 my $projname = $project->{name};
269 my @forks = grep {$$_[1] =~ m,^$projname/,} get_all_projects;
270 $project->{has_forks} = 1 if @forks;
271 $project->{has_alternates} = 1 if $project->has_alternates;
272 my @bundles = $project->bundles;
273 for (my $i = 0; $i < @bundles; ++$i) {
274 my $secs = $bundles[$i]->[0];
275 $bundles[$i]->[0] = strftime("%Y-%m-%d %H:%M:%S %z", localtime($secs));
276 my $sz = $bundles[$i]->[2];
277 1 while $sz =~ s/(?<=\d)(\d{3})(?:,|$)/,$1/g;
278 $bundles[$i]->[2] = $sz;
280 delete $project->{bundles};
281 $project->{bundles} = \@bundles if @bundles;
282 $project->{mirror} = 0 unless $project->{mirror};
283 $project->{is_empty} = 1 if $project->is_empty;
284 delete $project->{showpush} unless $project->{showpush};
285 delete $project->{users} if $project->{mirror};
286 delete $project->{baseurl} unless $project->{mirror};
287 delete $project->{banged} unless $project->{mirror};
288 delete $project->{lastrefresh} unless $project->{mirror};
289 delete $project->{cleanmirror} unless $project->{mirror};
290 delete $project->{statusupdates} unless $project->{mirror};
291 delete $project->{lastparentgc} unless $projname =~ m,/,;
292 unless ($project->{banged}) {
293 delete $project->{bangcount};
294 delete $project->{bangfirstfail};
295 delete $project->{bangmessagesent};
297 my $projhook = $project->_has_notifyhook;
298 if (defined($projhook) && $projhook ne "") {
299 $project->{notifyhook} = $projhook;
300 } else {
301 delete $project->{notifyhook};
303 $project->{README} = get_readme_desc($project->{README}) if exists($project->{README});
304 my @tags = get_ctag_counts($project, 1);
305 $project->{tags} = \@tags if @tags;
306 my $projconfig = read_config_file_hash($project->{path}."/config");
307 if (defined($projconfig) && defined($projconfig->{"core.hookspath"})) {
308 my $ahp = $projconfig->{"core.hookspath"};
309 my $rahp = realpath($ahp);
310 my $lhp = $project->{path}."/hooks";
311 my $rlhp = realpath($lhp);
312 my $ghp = $Girocco::Config::reporoot."/_global/hooks";
313 my $rghp = realpath($ghp);
314 $project->{has_local_hooks} = 1 if
315 defined($rahp) && defined($rlhp) && $rahp eq $rlhp;
316 $project->{has_global_hooks} = 1 if
317 defined($rahp) && defined($rghp) && $rahp eq $rghp;
318 $project->{hookspath} = $ahp unless $ahp eq $lhp || $ahp eq $ghp;
320 $project;
323 sub clean_addrlist {
324 my %seen = ();
325 my @newlist = ();
326 foreach (split(/[,\s]+/, $_[0])) {
327 next unless $_;
328 $seen{lc($_)} = 1, push(@newlist, $_) unless $seen{lc($_)};
330 return join(($_[1]||","), @newlist);
333 sub valid_addrlist {
334 my $cleaned = clean_addrlist(join(" ", @_));
335 return 1 if $cleaned eq "";
336 valid_email_multi($cleaned) && length($cleaned) <= 512;
339 sub validate_users {
340 my ($userlist, $force, $nodie, $quiet) = @_;
341 my @newusers = ();
342 my $badlist = 0;
343 my %seenuser = ();
344 my $mobok = $Girocco::Config::mob && $Girocco::Config::mob eq "mob";
345 my %users = map({($$_[1] => $_)} get_all_users);
346 foreach (split(/[\s,]+/, $userlist)) {
347 if (exists($users{$_}) || $_ eq "everyone" || ($mobok && $_ eq "mob")) {
348 $seenuser{$_}=1, push(@newusers, $_) unless $seenuser{$_};
349 next;
351 if (Girocco::User::does_exist($_, 1)) {
352 if ($force) {
353 $seenuser{$_}=1, push(@newusers, $_) unless $seenuser{$_};
354 } else {
355 $badlist = 1;
356 warn "refusing to allow questionable user \"$_\" without --force\n" unless $nodie && $quiet;
358 next;
360 $badlist = 1;
361 warn "invalid user: \"$_\"\n" unless $nodie && $quiet
363 die if $badlist && !$nodie;
364 return @newusers;
367 sub is_default_desc {
368 # "Unnamed repository; edit this file 'description' to name the repository."
369 # "Unnamed repository; edit this file to name it for gitweb."
370 local $_ = shift;
371 return 0 unless defined($_);
372 /Unnamed\s+repository;/i && /\s+edit\s+this\s+file\s+/i && /\s+to\s+name\s+/i;
375 sub valid_desc {
376 my $test = shift;
377 chomp $test;
378 return 0 if $test =~ /[\r\n]/;
379 $test =~ s/\s\s+/ /g;
380 $test =~ s/^\s+//;
381 $test =~ s/\s+$//;
382 return $test ne '';
385 sub clean_desc {
386 my $desc = shift;
387 defined($desc) or $desc = '';
388 chomp $desc;
389 $desc = to_utf8($desc, 1);
390 $desc =~ s/\s\s+/ /g;
391 $desc =~ s/^\s+//;
392 $desc =~ s/\s+$//;
393 return $desc;
396 sub parse_options {
397 Girocco::CLIUtil::_parse_options(
398 sub {
399 warn((($_[0]eq'?')?"unrecognized":"missing argument for")." option \"$_[1]\"\n")
400 unless $quiet;
401 die_usage;
402 }, @_);
405 sub cmd_list {
406 my %sortsub = (
407 lcname => sub {lc($$a[1]) cmp lc($$b[1])},
408 name => sub {$$a[1] cmp $$b[1]},
409 gid => sub {$$a[3] <=> $$b[3]},
410 owner => sub {lc($$a[4]) cmp lc($$b[4]) || lc($$a[1]) cmp lc($$b[1])},
411 no => sub {$$a[0] <=> $$b[0]},
413 my $sortopt = 'lcname';
414 my ($verbose, $owner);
415 parse_options(":sort" => \$sortopt, verbose => \$verbose, owner => \$owner);
416 my $regex;
417 if (@ARGV) {
418 my $val = shift @ARGV;
419 $regex = qr($val) or die "bad regex \"$val\"\n";
421 !@ARGV && exists($sortsub{$sortopt}) or die_usage;
422 my $sortsub = $sortsub{$sortopt};
423 my $grepsub = defined($regex) ? ($owner ? sub {$$_[4] =~ /$regex/} : sub {$$_[1] =~ /$regex/}) : sub {1};
424 my @projects = sort($sortsub grep {&$grepsub} get_all_projects);
425 if ($verbose) {
426 print map(sprintf("%s\n", join(":", (@$_)[1..5])), @projects);
427 } else {
428 print map(sprintf("%s: %s\n", $$_[1], $$_[5] =~ /^:/ ? "<mirror>" : $$_[5]), @projects);
430 return 0;
433 sub cmd_create {
434 my ($force, $noalternates, $orphanok, $optp, $nopasswd, $noowner, $defaults, $ispush, $pushusers,
435 $ismirror, $desc, $fullmirror, $homepage);
436 parse_options(
437 force => \$force, "no-alternates" => \$noalternates, orphan => \$orphanok, p => \$optp,
438 "no-password" => \$nopasswd, "no-owner" => \$noowner, defaults => \$defaults,
439 "push" => \$ispush, ":push" => \$pushusers, ":mirror" => \$ismirror, ":desc" => \$desc,
440 ":description" => \$desc, "full-mirror" => \$fullmirror, ":homepage" => \$homepage);
441 @ARGV == 1 or die_usage;
442 !defined($pushusers) || defined($ispush) or $ispush = 1;
443 defined($ismirror) && $ismirror =~ /^\s*$/ and die "--mirror url must not be empty\n";
444 die "--mirror and --push are mutually exclusive options\n" if $ismirror && $ispush;
445 die "--full-mirror requires use of --mirror=<url> option\n" if $fullmirror && !$ismirror;
446 !$defaults || defined($ispush) || defined($ismirror) or $ispush = 1;
447 !$defaults || defined($nopasswd) or $nopasswd = 1;
448 !$defaults || defined($noowner) or $noowner = 1;
449 !defined($ispush) || defined($pushusers) or $pushusers = "";
450 my $projname = $ARGV[0];
451 $projname =~ s/\.git$//i;
452 Girocco::Project::does_exist($projname, 1) and die "Project already exists: \"$projname\"\n";
453 if (!Girocco::Project::valid_name($projname, $orphanok, $optp)) {
454 warn "Refusing to create orphan project without --orphan\n"
455 if !$quiet && !$orphanok && Girocco::Project::valid_name($projname, 1, 1);
456 warn "Required orphan parent directory does not exist (use -p): ",
457 $Girocco::Config::reporoot.'/'.Girocco::Project::get_forkee_name($projname), "\n"
458 if !$quiet && $orphanok && Girocco::Project::valid_name($projname, 1, 1);
459 die "Invalid project name: \"$projname\"\n";
461 my ($forkee, $project) = ($projname =~ m#^(.*/)?([^/]+)$#);
462 my $newtype = $forkee ? 'fork' : 'project';
463 if (length($project) > 64) {
464 die "The $newtype name is longer than 64 characters. Do you really need that much?\n"
465 unless $force;
466 warn "Allowing $newtype name longer than 64 characters with --force\n" unless $quiet;
468 unless ($Girocco::Config::push || $Girocco::Config::mirror) {
469 die "Project creation disabled (no mirrors or push projects allowed)\n" unless $force;
470 warn "Continuing with --force even though both push and mirror projects are disabled\n" unless $quiet;
472 print "Enter settings for new project \"$projname\"\n" unless $defaults;
473 my %settings = ();
474 $settings{noalternates} = $noalternates;
475 if ($nopasswd) {
476 $settings{crypt} = "unknown";
477 } else {
478 my $np1 = prompt_noecho_nl_or_die("Admin password for project $projname (echo is off)");
479 $np1 ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
480 my $np2 = prompt_noecho_nl_or_die("Retype admin password for project $projname");
481 $np1 eq $np2 or die "Our high-paid security consultants have determined that\n" .
482 "the admin passwords you have entered do not match each other.\n";
483 $settings{crypt} = scrypt_sha1($np1);
485 my $owner = "";
486 unless ($noowner) {
487 $owner = prompt_or_die("Owner/email name for project $projname");
488 unless (valid_email($owner)) {
489 unless ($force) {
490 warn "Your email sure looks weird...?\n";
491 redo;
493 warn "Allowing invalid email with --force\n" unless $quiet;
495 if (length($owner) > 96) {
496 unless ($force) {
497 warn "Your email is longer than 96 characters. Do you really need that much?\n";
498 redo;
500 warn "Allowing email longer than 96 characters with --force\n" unless $quiet;
503 $settings{email} = $owner;
504 my $baseurl = "";
505 my $checkmirror = sub {
506 my $checkurl = shift;
507 unless (valid_repo_url($checkurl)) {
508 unless ($force) {
509 warn "Invalid mirror URL: \"$checkurl\"\n";
510 return undef;
512 warn "Allowing invalid mirror URL with --force\n" unless $quiet;
514 if ($Girocco::Config::restrict_mirror_hosts) {
515 my $mh = extract_url_hostname($checkurl);
516 unless (is_dns_hostname($mh)) {
517 unless ($force) {
518 warn "Invalid non-DNS mirror URL: \"$checkurl\"\n";
519 return undef;
521 warn "Allowing non-DNS mirror URL with --force\n" unless $quiet;
523 if (is_our_hostname($mh)) {
524 unless ($force) {
525 warn "Invalid same-host mirror URL: \"$checkurl\"\n";
526 return undef;
528 warn "Allowing same-host mirror URL with --force\n" unless $quiet;
531 return $checkurl;
533 if ($ispush || $ismirror) {
534 !$ispush || $force || $Girocco::Config::push or
535 die "Push projects are disabled, create a mirror (or use --force)\n";
536 !$ismirror || $force || $Girocco::Config::mirror or
537 die "Mirror projects are disabled, create a push project (or use --force)\n";
538 if ($ismirror) {
539 &$checkmirror($ismirror) or die "Invalid --mirror URL\n";
540 $baseurl = $ismirror;
541 $settings{url} = $baseurl;
542 $settings{cleanmirror} = $fullmirror ? 0 : 1;
543 } else {
544 my @newusers = ();
545 if ($pushusers !~ /^[\s,]*$/) {
546 eval {@newusers = validate_users($pushusers, $force); 1;} or
547 die "Invalid --push user list\n";
549 $settings{users} = \@newusers;
551 } elsif ($force || $Girocco::Config::mirror) {{
552 if ($force || $Girocco::Config::push) {
553 $baseurl = prompt_or_die("URL to mirror from (leave blank for push project)", "");
554 } else {{
555 $baseurl = prompt_or_die("URL to mirror from");
556 unless ($baseurl ne "") {
557 warn "Push projects are disabled, you must enter a mirror URL (or use --force)\n";
558 redo;
561 if ($baseurl ne "") {
562 &$checkmirror($baseurl) or redo;
563 $settings{url} = $baseurl;
564 $settings{cleanmirror} =
565 ynprompt_or_die("Mirror only heads, tags and notes (Y/n)", "Yes");
568 my $mirror = ($baseurl eq "") ? 0 : 1;
569 my $checkdesc = sub {
570 my $d = shift;
571 if (length($d) > 1024) {
572 unless ($force) {
573 warn "Short description length greater than 1024 characters!\n";
574 return undef;
576 warn "Allowing short description length greater than 1024 characters\n" unless $quiet;
578 return $d;
580 if (defined($desc)) {
581 $desc =~ s/^\s+//; $desc =~ s/\s+$//;
582 $desc eq "" || &$checkdesc($desc) or
583 die "Invalid --desc description\n";
584 } elsif (!$defaults) {
585 $desc = prompt_or_die("Short description", "");
586 $desc =~ s/^\s+//; $desc =~ s/\s+$//;
587 $desc eq "" || &$checkdesc($desc) or redo;
588 $desc = undef if $desc eq "";
590 defined($desc) or $desc = $mirror ? "Mirror of $baseurl" : "Push project $projname";
591 $settings{desc} = $desc;
592 my $checkhp = sub {
593 my $hpurl = shift;
594 unless (valid_web_url($hpurl)) {
595 unless ($force) {
596 warn "Invalid home page URL: \"$hpurl\"\n";
597 return undef;
599 warn "Allowing invalid home page URL with --force\n" unless $quiet;
601 return $hpurl;
603 if (defined($homepage)) {
604 $homepage =~ s/^\s+//; $homepage =~ s/\s+$//;
605 $homepage eq "" || &$checkhp($homepage) or
606 die "Invalid --homepage URL\n";
607 } elsif (!$defaults) {
608 $homepage = prompt_or_die("Home page URL", "");
609 $homepage =~ s/^\s+//; $homepage =~ s/\s+$//;
610 $homepage eq "" || &$checkhp($homepage) or redo;
611 $homepage = undef if $homepage eq "";
613 $settings{hp} = $homepage;
614 my $jsonurl = "";
615 if (!$defaults) {
616 $jsonurl = prompt_or_die("JSON notify POST URL", "");
617 if ($jsonurl ne "" && !valid_web_url($jsonurl)) {
618 unless ($force) {
619 warn "Invalid JSON notify POST URL: \$jsonurl\"\n";
620 redo;
622 warn "Allowing invalid JSON notify POST URL with --force\n" unless $quiet;
625 $settings{notifyjson} = $jsonurl;
626 my $commitaddrs = "";
627 if (!$defaults) {
628 $commitaddrs = clean_addrlist(prompt_or_die("Commit notify email addr(s)", ""));
629 if ($commitaddrs ne "" && !valid_addrlist($commitaddrs)) {
630 unless ($force) {
631 warn"invalid commit notify email address list (use --force to accept): \"$commitaddrs\"\n";
632 redo;
634 warn "using invalid commit notify email address list with --force\n" unless $quiet;
637 $settings{notifymail} = $commitaddrs;
638 $settings{reverseorder} = 1;
639 $settings{reverseorder} = ynprompt_or_die("Oldest-to-newest commit order in emails", "Yes")
640 if !$defaults && $commitaddrs ne "";
641 $settings{summaryonly} = ynprompt_or_die("Summary only (no diff) in emails", "No")
642 if !$defaults && $commitaddrs ne "";
643 my $tagaddrs = "";
644 if (!$defaults) {
645 $tagaddrs = clean_addrlist(prompt_or_die("Tag notify email addr(s)", ""));
646 if ($tagaddrs ne "" && !valid_addrlist($tagaddrs)) {
647 unless ($force) {
648 warn"invalid tag notify email address list (use --force to accept): \"$tagaddrs\"\n";
649 redo;
651 warn "using invalid tag notify email address list with --force\n" unless $quiet;
654 $settings{notifytag} = $tagaddrs;
655 if (!$mirror && !$ispush) {
656 my @newusers = ();
658 my $userlist = prompt_or_die("Push users", join(",", @newusers));
659 eval {@newusers = validate_users($userlist, $force); 1;} or redo;
661 $settings{users} = \@newusers;
663 my $newproj = Girocco::Project->ghost($projname, $mirror, $orphanok, $optp)
664 or die "Girocco::Project->ghost call failed\n";
665 my ($k, $v);
666 $newproj->{$k} = $v while ($k, $v) = each(%settings);
667 my $killowner = sub {
668 system($Girocco::Config::git_bin, '--git-dir='.$newproj->{path},
669 'config', '--unset', "gitweb.owner");
671 if ($mirror) {
672 $newproj->premirror or die "Girocco::Project->premirror failed\n";
673 !$noowner or &$killowner;
674 $newproj->clone or die "Girocco::Project->clone failed\n";
675 warn "Project $projname created and cloning successfully initiated.\n"
676 unless $quiet;
677 } else {
678 $newproj->conjure or die "Girocco::Project->conjure failed\n";
679 !$noowner or &$killowner;
680 warn "New push project fork is empty due to use of --no-alternates\n"
681 if !$quiet && $projname =~ m,/, && $noalternates;
682 warn "Project $projname successfully created.\n" unless $quiet;
684 return 0;
687 sub git_config {
688 my $gd = shift;
689 system($Girocco::Config::git_bin, "--git-dir=$gd", 'config', @_) == 0
690 or die "\"git --git-dir='$gd' config ".join(" ", @_)."\" failed.\n";
693 sub cmd_adopt {
694 my ($force, $type, $nousers, $dryrun, $noowner, $owner, $users, $verbose);
695 parse_options(force => \$force, ":type" => \$type, "no-users" => \$nousers, "dry-run" => \$dryrun,
696 "no-owner" => \$noowner,":owner" => \$owner, quiet => \$quiet, q =>\$quiet, verbose => \$verbose);
697 @ARGV or die "Please give project name on command line.\n";
698 my $projname = shift @ARGV;
699 (!$noowner || !defined($owner)) && (!$nousers || !@ARGV) or die_usage;
700 !defined($type) || $type eq "mirror" || $type eq "push" or die_usage;
701 defined($type) or $type = "";
702 my $projdir;
703 if ($dryrun && $projname =~ m,^/[^.\s/\\:], && is_git_dir(realpath($projname))) {
704 $projdir = realpath($projname);
705 $projname = $projdir;
706 $projname =~ s/\.git$//i;
707 $projname =~ s,/+$,,;
708 $projname =~ s,^.*/,,;
709 $projname ne "" or $projname = $projdir;
710 } else {
711 $projname =~ s/\.git$//i;
712 $projname ne "" or die "Invalid project name \"\".\n";
713 unless (Girocco::Project::does_exist($projname, 1)) {
714 Girocco::Project::valid_name($projname, 1, 1)
715 or die "Invalid project name \"$projname\".\n";
716 die "No such project to adopt: $projname\n";
718 defined(Girocco::Project->load($projname))
719 and die "Project already known (no need to adopt): $projname\n";
720 $projdir = $Girocco::Config::reporoot . "/" . $projname . ".git";
721 is_git_dir($projdir) or die "Not a git directory: \"$projdir\"\n";
723 my $config = read_config_file($projdir . "/config");
724 my %config = ();
725 %config = map {($$_[0], defined($$_[1])?$$_[1]:"true")} @$config if defined($config);
726 git_bool($config{"core.bare"}) or die "Not a bare git repository: \"$projdir\"\n";
727 defined(read_HEAD_symref($projdir)) or die "Project with non-symbolic HEAD ref: \"$projdir\"\n";
728 @ARGV and $users = [validate_users(join(" ", @ARGV), $force, 1, $quiet)];
729 my $desc = "";
730 if (-e "$projdir/description") {
731 open my $fd, '<', "$projdir/description" or die "Cannot open \"$projdir/description\": $!\n";
733 local $/;
734 $desc = <$fd>;
736 close $fd;
737 defined $desc or $desc = "";
738 chomp $desc;
739 $desc = to_utf8($desc, 1);
740 is_default_desc($desc) and $desc = "";
741 if ($desc ne "" && !valid_desc($desc)) {
742 die "invalid 'description' file contents (use --force to accept): \"$desc\"\n"
743 unless $force;
744 warn "using invalid 'description' file contents with --force\n" unless $quiet;
746 $desc = clean_desc($desc);
747 if (length($desc) > 1024) {
748 die "description longer than 1024 chars (use --force to accept): \"$desc\"\n"
749 unless $force;
750 warn "using longer than 1024 char description with --force\n" unless $quiet;
753 my $readme = "";
754 if (-e "$projdir/README.html") {
755 open my $fd, '<', "$projdir/README.html" or die "Cannot open \"$projdir/README.html\": $!\n";
757 local $/;
758 $readme = <$fd>;
760 close $fd;
761 defined $readme or $readme = "";
762 $readme = to_utf8($readme, 1);
763 $readme =~ s/\r\n?/\n/gs;
764 $readme =~ s/^\s+//s;
765 $readme =~ s/\s+$//s;
766 $readme eq "" or $readme .= "\n";
767 if (length($readme) > 8192) {
768 die "readme greater than 8192 chars is too long (use --force to override)\n"
769 unless $force;
770 warn "using readme greater than 8192 chars with --force\n" unless $quiet;
772 my $rd = get_readme_desc($readme);
773 if ($rd ne "automatic" && $rd ne "suppressed") {
774 my $xmllint = qx(sh -c 'command -v xmllint'); chomp $xmllint;
775 if (-f $xmllint && -x $xmllint) {
776 my $dummy = {README => $readme};
777 my ($cnt, $err) = Girocco::Project::_lint_readme($dummy, 0);
778 if ($cnt) {
779 my $msg = "xmllint: $cnt error";
780 $msg .= "s" unless $cnt == 1;
781 print STDERR "$msg\n", "-" x length($msg), "\n", $err
782 unless $force && $quiet;
783 exit(255) unless $force;
784 warn "$projname: using invalid raw HTML with --force\n" unless $quiet;
786 } else {
787 die "xmllint not available, refusing to use raw HTML without --force\n"
788 unless $force;
789 warn "xmllint not available using unchecked raw HTML with --force\n" unless $quiet;
793 # Inspect any remotes now
794 # Yes, Virginia, remote urls can be multi-valued
795 my %remotes = ();
796 foreach (@$config) {
797 my ($k,$v) = @$_;
798 next unless $k =~ /^remote\.([^\/].*?)\.([^.]+)$/; # remote name cannot start with "/"
799 my ($name, $subkey) = ($1, $2);
800 $remotes{$name}->{skip} = git_bool($v,1), next if $subkey eq "skipdefaultupdate" || $subkey eq "skipfetchall";
801 $remotes{$name}->{mirror} = git_bool($v,1), next if $subkey eq "mirror"; # we might want this
802 $remotes{$name}->{vcs} = $v, next if defined($v) && $v !~ /^\s*$/ && $subkey eq "vcs";
803 push(@{$remotes{$name}->{$subkey}}, $v), next if defined($v) && $v !~ /^\s*$/ &&
804 ($subkey eq "url" || $subkey eq "fetch" || $subkey eq "push" || $subkey eq "pushurl");
806 # remotes.default is the default remote group to fetch for "git remote update" otherwise --all
807 # the remote names in a group are separated by runs of [ \t\n] characters
808 # remote names "", ".", ".." and any name starting with "/" are invalid
809 # a remote with no url or vcs setting is not considered valid
810 my @check = ();
811 my $usingall = 0;
812 if (exists($config{"remotes.default"})) {
813 foreach (split(/[ \t\n]+/, $config{"remotes.default"})) {
814 next unless exists($remotes{$_});
815 my $rmt = $remotes{$_};
816 next if !exists($rmt->{url}) && !$rmt->{vcs};
817 push(@check, $_);
819 } else {
820 $usingall = 1;
821 my %seenrmt = ();
822 foreach (@$config) {
823 my ($k,$v) = @$_;
824 next unless $k =~ /^remote\.([^\/].*?)\.[^.]+$/;
825 next if $seenrmt{$1};
826 $seenrmt{$1} = 1;
827 next unless exists($remotes{$1});
828 my $rmt = $remotes{$1};
829 next if $rmt->{skip} || (!exists($rmt->{url}) && !$rmt->{vcs});
830 push(@check, $1);
833 my @needskip = (); # remotes that need skipDefaultUpdate set to true
834 my $foundvcs = 0;
835 my $foundfetch = 0;
836 my $foundfetchwithmirror = 0;
837 foreach (@check) {
838 my $rmt = $remotes{$_};
839 push(@needskip, $_) if $usingall && !exists($rmt->{fetch});
840 next unless exists($rmt->{fetch});
841 ++$foundfetch;
842 ++$foundfetchwithmirror if $rmt->{mirror};
843 ++$foundvcs if $rmt->{vcs} || (exists($rmt->{url}) && $rmt->{url}->[0] =~ /^[a-zA-Z0-9][a-zA-Z0-9+.-]*::/);
845 # if we have $foundvcs then we need to explicitly set fetch.prune to false
846 # if we have $foundfetch > 1 then we need to explicitly set fetch.prune to false
847 my $neednoprune = !exists($config{"fetch.prune"}) && ($foundvcs || $foundfetch > 1);
848 my $baseurl = "";
849 my $needfakeorigin = 0; # if true we need to set remote.origin.skipDefaultUpdate = true
850 # if remote "origin" exists we always pick up its first url or use ""
851 if (exists($remotes{origin})) {
852 my $rmt = $remotes{origin};
853 $baseurl = exists($rmt->{url}) ? $rmt->{url}->[0] : "";
854 $needfakeorigin = !exists($rmt->{url}) && !$rmt->{vcs} && !$rmt->{skip};
855 } else {
856 $needfakeorigin = 1;
857 # get the first url of the @check remotes
858 foreach (@check) {
859 my $rmt = $remotes{$_};
860 next unless exists($rmt->{url});
861 next unless defined($rmt->{url}->[0]) && $rmt->{url}->[0] ne "";
862 $baseurl = $rmt->{url}->[0];
863 last;
866 my $makemirror = $type eq "mirror" || ($type eq "" && $foundfetch);
868 # If we have $foundfetch we want to make a mirror but complain if
869 # we $foundfetchwithmirror as well unless we have --type=mirror.
870 # Warn if we have --type=push and $foundfetch and !$foundfetchwithmirror.
871 # Warn if we need to set fetch.prune=false when making a mirror
872 # Warn if we need to create remote.origin.skipDefaultUpdate when making a mirror
873 # Complain if @needskip AND !$usingall (warn with --force but don't set skip)
874 # Warn if $usingall and any @needskip (and set them) if making a mirror
875 # Warn if making a mirror and $baseurl eq ""
876 # Warn if we have --type=mirror and !$foundfetch
878 if ($makemirror) {
879 warn "No base URL to mirror from for adopted \"$projname\"\n" unless $quiet || $baseurl ne "";
880 warn "Adopting mirror \"$projname\" without any fetch remotes\n" unless $quiet || $foundfetch;
881 if ($foundfetchwithmirror) {
882 warn "Refusing to adopt mirror \"$projname\" with active remote.<name>.mirror=true remote(s)\n".
883 "(Use --type=mirror to override)\n"
884 unless $type eq "mirror";
885 exit(255) unless $type eq "mirror" || $dryrun;
886 warn "Adopting mirror \"$projname\" with active remote.<name>.mirror=true remotes\n"
887 unless $quiet || $type ne "mirror";
889 warn "Setting explicit fetch.prune=false for adoption of mirror \"$projname\"\n"
890 if !$quiet && $neednoprune;
891 warn "Setting remote.origin.skipDefaultUpdate=true for adoption of mirror \"$projname\"\n"
892 if !$quiet && $needfakeorigin;
893 if (!$usingall && @needskip) {
894 warn "Refusing to adopt mirror empty fetch remote(s) (override with --force)\n"
895 unless $force;
896 exit(255) unless $force || $dryrun;
897 warn "Adopting mirror with empty fetch remote(s) with --force\n"
898 unless $quiet || !$force;
900 warn "Will set skipDefaultUpdate=true on non-fetch remote(s)\n" if !$quiet && $usingall && @needskip;
901 warn "Adopting mirror with base URL \"$baseurl\"\n" unless $quiet || $baseurl eq "";
902 } else {
903 warn "Adopting push \"$projname\" but active non-mirror remotes are present\n"
904 if !$quiet && $foundfetch && !$foundfetchwithmirror;
907 if (!$noowner && !defined($owner)) {
908 # Select the owner
909 $owner = $config{"gitweb.owner"};
910 if (!defined($owner) || $owner eq "") {
911 $owner = $Girocco::Config::admin;
912 warn "Using owner \"$owner\" for adopted project\n" unless $quiet;
915 if (!$nousers && !$makemirror && !defined($users)) {
916 # select user list for push project
917 my $findowner = $owner;
918 defined($findowner) or $findowner = $config{"gitweb.owner"};
919 $findowner = lc($findowner) if defined($findowner);
920 my @owner_users = ();
921 @owner_users = map {$findowner eq lc($$_[4]) ? $$_[1] : ()} get_all_users
922 if defined($findowner) && $findowner ne "";
923 defined($findowner) or $findowner = "";
924 if (@owner_users <= 1) {
925 $users = \@owner_users;
926 warn "No users found that match owner \"$findowner\"\n" unless @owner_users || $quiet;
927 } else {
928 $users = [];
929 warn "Found ".scalar(@owner_users)." users for owner \"$findowner\" (" .
930 join(" ", @owner_users) . ") not setting any\n" unless $quiet;
933 defined($users) or $users = [];
935 # Warn if we preserve an existing receive.denyNonFastForwards or receive.denyDeleteCurrent setting
936 # Complain if core.logallrefupdates or logs subdir exists and contains any files (allow with --force
937 # and warn about preserving the setting)
939 warn "Preserving existing receive.denyNonFastForwards=true\n"
940 if !$quiet && git_bool($config{"receive.denynonfastforwards"});
941 warn "Preserving existing receive.denyDeleteCurrent=$config{'receive.denydeletecurrent'}\n"
942 if !$quiet && exists($config{"receive.denydeletecurrent"}) &&
943 $config{"receive.denydeletecurrent"} ne "warn";
945 my $reflogfiles = Girocco::Project::_contains_files("$projdir/logs");
946 my $reflogactive = git_bool($config{"core.logallrefupdates"});
947 if ($reflogactive || $reflogfiles) {
948 warn "Refusing to adopt \"$projname\" with active ref logs without --force\n" if $reflogfiles && !$force;
949 warn "Refusing to adopt \"$projname\" with core.logAllRefUpdates=true without --force\n" if $reflogactive && !$force;
950 exit(255) unless $force || $dryrun;
951 warn "Adopting \"$projname\" with active ref logs with --force\n" unless $quiet || ($reflogfiles && !$force);
952 warn "Adopting \"$projname\" with core.logAllRefUpdates=true with --force\n" unless $quiet || ($reflogactive && !$force);
955 return 0 if $dryrun && !$verbose;
957 my $newproj = eval {Girocco::Project->ghost($projname, $makemirror, 1, $dryrun)};
958 defined($newproj) or die "Girocco::Project::ghost failed: $@\n";
959 $newproj->{desc} = $desc;
960 $newproj->{README} = $readme;
961 $newproj->{url} = $baseurl if $makemirror || exists($config{"gitweb.baseurl"});
962 $newproj->{email} = $owner if defined($owner);
963 $newproj->{users} = $users;
964 $newproj->{crypt} = "unknown";
965 $newproj->{reverseorder} = 1 unless exists($config{"hooks.reverseorder"});
966 $newproj->{summaryonly} = 1 unless exists($config{"hooks.summaryonly"});
967 my $dummy = bless {}, "Girocco::Project";
968 $dummy->{path} = "$projdir";
969 $dummy->{configfilehash} = \%config;
970 $dummy->_properties_load;
971 delete $dummy->{origurl};
972 foreach my $k (keys(%$dummy)) {
973 $newproj->{$k} = $dummy->{$k}
974 if exists($dummy->{$k}) && !exists($newproj->{$k});
977 if ($verbose) {
978 use Data::Dumper;
979 my %info = %$newproj;
980 $info{README} = get_readme_desc($info{README}) if exists($info{README});
981 my $d = Data::Dumper->new([\%info], ['*'.$newproj->{name}]);
982 $d->Sortkeys(sub {[sort({lc($a) cmp lc($b)} keys %{$_[0]})]});
983 print $d->Dump([\%info], ['*'.$newproj->{name}]);
985 return 0 if $dryrun;
987 # Make any changes as needed for @needskip, $neednoprune and $needfakeorigin
988 if ($makemirror) {
989 git_config($projdir, "fetch.prune", "false") if $neednoprune;
990 git_config($projdir, "remote.origin.skipDefaultUpdate", "true") if $needfakeorigin;
991 if ($usingall && @needskip) {
992 git_config($projdir, "remote.$_.skipDefaultUpdate", "true") foreach @needskip;
996 # Perform the actual adoption
997 $newproj->adopt or die "Girocco::Project::adopt failed\n";
999 # Perhaps restore core.logAllRefUpdates, receive.denyNonFastForwards and receive.denyDeleteCurrent
1000 git_config($projdir, "receive.denyNonFastForwards", "true")
1001 if git_bool($config{"receive.denynonfastforwards"});
1002 git_config($projdir, "receive.denyDeleteCurrent", $config{"receive.denydeletecurrent"})
1003 if exists($config{"receive.denydeletecurrent"}) &&
1004 $config{"receive.denydeletecurrent"} ne "warn";
1005 git_config($projdir, "core.logAllRefUpdates", "true")
1006 if $reflogactive;
1008 # Success
1009 if ($makemirror) {
1010 warn "Mirror project \"$projname\" successfully adopted.\n" unless $quiet;
1011 } else {
1012 warn "Push project \"$projname\" successfully adopted.\n" unless $quiet;
1014 return 0;
1017 sub cmd_remove {
1018 my ($force, $reallydel, $keepforks);
1019 parse_options(force => \$force, "really-delete" => \$reallydel,
1020 "keep-forks" => \$keepforks, quiet => \$quiet, q =>\$quiet);
1021 @ARGV or die "Please give project name on command line.\n";
1022 @ARGV == 1 or die_usage;
1023 my $project = get_project($ARGV[0]);
1024 my $projname = $project->{name};
1025 my $isempty = !$project->{mirror} && $project->is_empty;
1026 if (!$project->{mirror} && !$isempty && $reallydel) {
1027 die "refusing to remove and delete non-empty push project without --force: $projname\n" unless $force;
1028 warn "allowing removal and deletion of non-empty push project with --force\n" unless $quiet;
1030 my $altwarn;
1031 my $removenogc;
1032 if ($project->has_forks) {
1033 die "refusing to remove project with forks (use --keep-forks): $projname\n" unless $keepforks;
1034 warn "allowing removal of forked project while preserving its forks with --keep-forks\n" unless $quiet;
1035 # Run pseudo GC on that repository so that objects don't get lost within forks
1036 my $basedir = $Girocco::Config::basedir;
1037 my $projdir = $project->{path};
1038 warn "We have to run pseudo GC on the repo so that the forks don't lose data. Hang on...\n" unless $quiet;
1039 my $nogcrunning = sub {
1040 die "Error: GC appears to be currently running on $projname\n"
1041 if -e "$projdir/gc.pid" || -e "$projdir/.gc_in_progress";
1043 &$nogcrunning;
1044 $removenogc = ! -e "$projdir/.nogc";
1045 recreate_file("$projdir/.nogc") if $removenogc;
1046 die "unable to create \"$projdir/.nogc\"\n" unless -e "$projdir/.nogc";
1047 delete $ENV{show_progress};
1048 $ENV{'show_progress'} = 1 unless $quiet;
1049 sleep 2; # *cough*
1050 &$nogcrunning;
1051 system("$basedir/toolbox/perform-pre-gc-linking.sh", "--include-packs", $projname) == 0
1052 or die "Running pseudo GC on project $projname failed\n";
1053 $altwarn = 1;
1055 my $archived;
1056 if (!$project->{mirror} && !$isempty && !$reallydel) {
1057 $archived = $project->archive_and_delete;
1058 unlink("$archived/.nogc") if $removenogc && defined($archived) && $archived ne "";
1059 } else {
1060 $project->delete;
1062 warn "Project '$projname' removed from $Girocco::Config::name" .
1063 ($archived ? ", backup in '$archived'" : "") .".\n" unless $quiet;
1064 warn "Retained forks may now have unwanted objects/info/alternates lines\n" if $altwarn && !$quiet;
1065 return 0;
1068 sub cmd_show {
1069 use Data::Dumper;
1070 @ARGV == 1 or die_usage;
1071 my $project = get_clean_project($ARGV[0]);
1072 my %info = %$project;
1073 my $d = Data::Dumper->new([\%info], ['*'.$project->{name}]);
1074 $d->Sortkeys(sub {[sort({lc($a) cmp lc($b)} keys %{$_[0]})]});
1075 print $d->Dump([\%info], ['*'.$project->{name}]);
1076 return 0;
1079 sub cmd_listheads {
1080 @ARGV == 1 or die_usage;
1081 my $project = get_project($ARGV[0]);
1082 my @heads = sort({lc($a) cmp lc($b)} $project->get_heads);
1083 my $cur = $project->{HEAD};
1084 defined($cur) or $cur = '';
1085 my $curmark = '*';
1086 my $headhash = get_git("--git-dir=$project->{path}", 'rev-parse', '--quiet', '--verify', 'HEAD');
1087 defined($headhash) or $headhash = '';
1088 chomp $headhash;
1089 $headhash or $curmark = '!';
1090 foreach (@heads) {
1091 my $mark = $_ eq $cur ? $curmark : ' ';
1092 print "$mark $_\n";
1094 return 0;
1097 sub cmd_listtags {
1098 my $vcnt = 0;
1099 shift(@ARGV), $vcnt=1 if @ARGV && ($ARGV[0] eq '--verbose' || $ARGV[0] eq '-v');
1100 @ARGV == 1 or die_usage;
1101 my $project = get_project($ARGV[0]);
1102 if ($vcnt) {
1103 print map("$$_[0]\t$$_[1]\n", get_ctag_counts($project));
1104 } else {
1105 print map("$_\n", $project->get_ctag_names);
1107 return 0;
1110 sub cmd_deltags {
1111 my $ic = 0;
1112 shift(@ARGV), $ic=1 if @ARGV && $ARGV[0] =~ /^(?:--?ignore-case|-i)$/i;
1113 @ARGV >= 2 or die_usage;
1114 my $project = get_project(shift @ARGV);
1115 my %curtags;
1116 if ($ic) {
1117 push(@{$curtags{lc($_)}}, $_) foreach $project->get_ctag_names;
1118 } else {
1119 push(@{$curtags{$_}}, $_) foreach $project->get_ctag_names;
1121 my @deltags = ();
1122 my %seentag = ();
1123 my $ctags = join(" ", @ARGV);
1124 $ctags = lc($ctags) if $ic;
1125 foreach (split(/[\s,]+/, $ctags)) {
1126 next unless exists($curtags{$_});
1127 $seentag{$_}=1, push(@deltags, $_) unless $seentag{$_};
1129 if (!@deltags) {
1130 warn $project->{name}, ": skipping removal of only non-existent tags\n" unless $quiet;
1131 } else {
1132 # Avoid touching anything other than the ctags
1133 foreach my $tg (@deltags) {
1134 $project->delete_ctag($_) foreach @{$curtags{$tg}};
1136 $project->_set_changed;
1137 $project->_set_forkchange;
1138 warn $project->{name}, ": specified tags have been removed\n" unless $quiet;
1140 return 0;
1143 sub cmd_addtags {
1144 @ARGV >= 2 or die_usage;
1145 my $project = get_project(shift @ARGV);
1146 my $ctags = join(" ", @ARGV);
1147 $ctags =~ /[^, a-zA-Z0-9:.+#_-]/ and
1148 die "Content tag(s) \"$ctags\" contain(s) evil character(s).\n";
1149 my $oldmask = umask();
1150 umask($oldmask & ~0060);
1151 my $changed = 0;
1152 foreach (split(/[\s,]+/, $ctags)) {
1153 ++$changed if $project->add_ctag($_, 1);
1155 if ($changed) {
1156 $project->_set_changed;
1157 $project->_set_forkchange;
1159 umask($oldmask);
1160 my $cnt = ($changed == 1) ? "1 content tag has" : $changed . " content tags have";
1161 warn $project->{name}, ": $cnt been added/updated\n" unless $quiet;
1162 return 0;
1165 sub _get_random_val {
1166 my $p = shift;
1167 my $md5;
1169 no warnings;
1170 $md5 = md5_hex(time . $$ . rand() . join(':',%$p));
1172 $md5;
1175 sub cmd_chpass {
1176 my $force = 0;
1177 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1178 my $random = undef;
1179 pop(@ARGV), $random=lc($ARGV[1]) if @ARGV==2 && $ARGV[1] =~ /^(?:random|unknown)$/i;
1180 @ARGV == 1 or die_usage;
1181 my $project = get_project($ARGV[0]);
1182 die "refusing to change locked password of project \"$ARGV[0]\" without --force\n"
1183 if $project->is_password_locked;
1184 my ($newpw, $rmsg);
1185 if ($random) {
1186 if ($random eq "random") {
1187 die "refusing to set random password without --force\n" unless $force;
1188 $rmsg = "set to random value";
1189 $newpw = _get_random_val($project);
1190 } else {
1191 die "refusing to set password hash to '$random' without --force\n" unless $force;
1192 $rmsg = "hash set to '$random'";
1193 $newpw = $random;
1195 } else {
1196 $rmsg = "updated";
1197 if (-t STDIN) {
1198 print "Changing admin password for project $ARGV[0]\n";
1199 my $np1 = prompt_noecho_nl_or_die("New password for project $ARGV[0] (echo is off)");
1200 $np1 ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
1201 my $np2 = prompt_noecho_nl_or_die("Retype new password for project $ARGV[0]");
1202 $np1 eq $np2 or die "Our high-paid security consultants have determined that\n" .
1203 "the admin passwords you have entered do not match each other.\n";
1204 $newpw = $np1;
1205 } else {
1206 $newpw = <STDIN>;
1207 defined($newpw) or die "missing new password on STDIN\n";
1208 chomp($newpw);
1211 $newpw ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
1212 my $old = $project->{crypt};
1213 $project->{crypt} = (defined($random) && $random ne "random") ? $newpw : scrypt_sha1($newpw);
1214 if (defined($old) && $old eq $project->{crypt}) {
1215 warn $project->{name}, ": skipping update of password hash to same value\n" unless $quiet;
1216 } else {
1217 # Avoid touching anything other than the password hash
1218 $project->_group_update;
1219 warn $project->{name}, ": admin password $rmsg (new hash stored)\n" unless $quiet;
1221 return 0;
1224 sub cmd_checkpw {
1225 @ARGV == 1 or die_usage;
1226 my $project = get_project($ARGV[0]);
1227 my $pwhash = $project->{crypt};
1228 defined($pwhash) or $pwhash = "";
1229 if ($pwhash eq "") {
1230 warn $project->{name}, ": no password required\n" unless $quiet;
1231 return 0;
1233 if ($project->is_password_locked) {
1234 warn $project->{name}, ": password is locked\n" unless $quiet;
1235 exit 1;
1237 my $checkpw;
1238 if (-t STDIN) {
1239 $checkpw = prompt_noecho_nl_or_die("Admin password for project $ARGV[0] (echo is off)");
1240 $checkpw ne "" or warn "checking for empty password as hash (very unlikely)\n" unless $quiet;
1241 } else {
1242 $checkpw = <STDIN>;
1243 defined($checkpw) or die "missing admin password on STDIN\n";
1244 chomp($checkpw);
1246 unless (Girocco::CLIUtil::check_passwd_match($pwhash, $checkpw)) {
1247 warn "password check failure\n" unless $quiet;
1248 exit 1;
1250 warn "admin password match\n" unless $quiet;
1251 return 0;
1254 sub cmd_gc {
1255 my ($force, $auto, $redelta, $recompress);
1256 parse_options(force => \$force, quiet => \$quiet, q => \$quiet, auto => \$auto,
1257 redelta => \$redelta, "no-reuse-delta" => \$redelta, aggressive => \$force,
1258 recompress => \$recompress, "no-reuse-object" => $recompress);
1259 $force && $auto and die "--force and --auto are mutually exclusive options\n";
1260 @ARGV or die "Please give project name on command line.\n";
1261 @ARGV == 1 or die_usage;
1262 my $project = get_project($ARGV[0]);
1263 delete $ENV{show_progress};
1264 delete $ENV{force_gc};
1265 $quiet or $ENV{"show_progress"} = 1;
1266 $force and $ENV{"force_gc"} = 1;
1267 if (!$auto && !$force && ! -e $project->{path}."/.needsgc") {
1268 open NEEDSGC, '>', $project->{path}."/.needsgc" and close NEEDSGC;
1270 my @args = ($Girocco::Config::basedir . "/jobd/gc.sh", $project->{name});
1271 $redelta && !$recompress and push(@args, "-f");
1272 $recompress and push(@args, "-F");
1273 my $lastgc = $project->{lastgc};
1274 system({$args[0]} @args) != 0 and return 1;
1275 # Do it again Sam, but only if lastgc was set, gc.sh succeeded and now it's not set
1276 if ($lastgc) {
1277 my $newlastgc = get_git("--git-dir=$project->{path}", 'config', '--get', 'gitweb.lastgc');
1278 if (!$newlastgc) {
1279 system({$args[0]} @args) != 0 and return 1;
1282 return 0;
1285 sub cmd_update {
1286 my ($force, $summary);
1287 parse_options(force => \$force, quiet => \$quiet, q => \$quiet, summary => \$summary);
1288 $quiet && $summary and die "--quiet and --summary are mutually exclusive options\n";
1289 @ARGV or die "Please give project name on command line.\n";
1290 @ARGV == 1 or die_usage;
1291 my $project = get_project($ARGV[0]);
1292 $project->{mirror} or die "Project \"$ARGV[0]\" is a push project, not a mirror project.\n";
1293 delete $ENV{show_progress};
1294 delete $ENV{force_update};
1295 if ($quiet) {
1296 $ENV{"show_progress"} = 0;
1297 } else {
1298 $ENV{"show_progress"} = ($summary ? 1 : 2);
1300 $force and $ENV{"force_update"} = 1;
1301 system($Girocco::Config::basedir . "/jobd/update.sh", $project->{name}) != 0 and return 1;
1302 return 0;
1305 sub cmd_remirror {
1306 my $force = 0;
1307 parse_options(force => \$force, quiet => \$quiet, q => \$quiet);
1308 @ARGV or die "Please give project name on command line.\n";
1309 @ARGV == 1 or die_usage;
1310 my $project = get_project($ARGV[0]);
1311 $project->{mirror} or die "Project \"$ARGV[0]\" is a push project, not a mirror project.\n";
1312 if ($project->{clone_in_progress} && !$project->{clone_failed}) {
1313 warn "Project \"$ARGV[0]\" already seems to have a clone underway at this moment.\n" unless $quiet && $force;
1314 exit(255) unless $force;
1315 yes_to_continue_or_die("Are you sure you want to force a remirror");
1317 unlink($project->_clonefail_path);
1318 unlink($project->_clonelog_path);
1319 recreate_file($project->_clonep_path);
1320 my $sock = IO::Socket::UNIX->new($Girocco::Config::chroot.'/etc/taskd.socket') or
1321 die "cannot connect to taskd.socket: $!\n";
1322 select((select($sock),$|=1)[0]);
1323 $sock->print("clone ".$project->{name}."\n");
1324 # Just ignore reply, we are going to succeed anyway and the I/O
1325 # would apparently get quite hairy.
1326 $sock->flush();
1327 sleep 2; # *cough*
1328 $sock->close();
1329 warn "Project \"$ARGV[0]\" remirror initiated.\n" unless $quiet;
1330 return 0;
1333 sub cmd_setowner {
1334 my $force = 0;
1335 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1336 @ARGV == 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage;
1337 my $project = get_project($ARGV[0]);
1338 if (@ARGV == 2 && !valid_email($ARGV[1])) {
1339 die "invalid owner/email (use --force to accept): \"$ARGV[1]\"\n"
1340 unless $force;
1341 warn "using invalid owner/email with --force\n" unless $quiet;
1343 if (@ARGV == 2 && length($ARGV[1]) > 96) {
1344 die "owner/email longer than 96 chars (use --force to accept): \"$ARGV[1]\"\n"
1345 unless $force;
1346 warn "using longer than 96 char owner/email with --force\n" unless $quiet;
1348 my $old = $project->{email};
1349 if (@ARGV == 1) {
1350 print "$old\n" if defined($old);
1351 return 0;
1353 if (defined($old) && $old eq $ARGV[1]) {
1354 warn $project->{name}, ": skipping update of owner/email to same value\n" unless $quiet;
1355 } else {
1356 # Avoid touching anything other than "gitweb.owner"
1357 $project->_property_fput("email", $ARGV[1]);
1358 $project->_update_index;
1359 $project->_set_changed;
1360 warn $project->{name}, ": owner/email updated to \"$ARGV[1]\"\n" unless $quiet;
1362 return 0;
1365 sub cmd_setdesc {
1366 my $force = 0;
1367 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1368 @ARGV >= 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage;
1369 my $project = get_project(shift @ARGV);
1370 if (@ARGV && !valid_desc(join(" ", @ARGV))) {
1371 die "invalid description (use --force to accept): \"".join(" ", @ARGV)."\"\n"
1372 unless $force;
1373 warn "using invalid description with --force\n" unless $quiet;
1375 my $desc = clean_desc(join(" ", @ARGV));
1376 if (@ARGV && length($desc) > 1024) {
1377 die "description longer than 1024 chars (use --force to accept): \"$desc\"\n"
1378 unless $force;
1379 warn "using longer than 1024 char description with --force\n" unless $quiet;
1381 my $old = $project->{desc};
1382 if (!@ARGV) {
1383 print "$old\n" if defined($old);
1384 return 0;
1386 if (defined($old) && $old eq $desc) {
1387 warn $project->{name}, ": skipping update of description to same value\n" unless $quiet;
1388 } else {
1389 # Avoid touching anything other than description file
1390 $project->_property_fput("desc", $desc);
1391 $project->_set_changed;
1392 warn $project->{name}, ": description updated to \"$desc\"\n" unless $quiet;
1394 return 0;
1397 sub cmd_setreadme {
1398 my $force = 0;
1399 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1400 @ARGV == 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage;
1401 my $project = get_project($ARGV[0]);
1402 my $old = $project->{README};
1403 if (@ARGV == 1) {
1404 chomp $old if defined($old);
1405 print "$old\n" if defined($old) && $old ne "";
1406 return 0;
1408 my ($new, $raw, $newname);
1409 $newname = '';
1410 if ($ARGV[1] eq "-") {
1411 local $/;
1412 $new = <STDIN>;
1413 $raw = 1;
1414 $newname = "contents of <STDIN>";
1415 } elsif (lc($ARGV[1]) eq "automatic" || lc($ARGV[1]) eq "auto") {
1416 $new = "";
1417 } elsif (lc($ARGV[1]) eq "suppressed" || lc($ARGV[1]) eq "suppress") {
1418 $new = "<!-- suppress -->";
1419 } else {
1420 my $fn = $ARGV[1];
1421 $fn =~ s/^\@//;
1422 die "missing filename for README\n" unless $fn ne "";
1423 die "no such file: \"$fn\"\n" unless -f $fn && -r $fn;
1424 open F, '<', $fn or die "cannot open \"$fn\" for reading: $!\n";
1425 local $/;
1426 $new = <F>;
1427 close F;
1428 $raw = 1;
1429 $newname = "contents of \"$fn\"";
1431 defined($new) or $new = '';
1432 $project->{README} = to_utf8($new, 1);
1433 $project->_cleanup_readme;
1434 if (length($project->{README}) > 8192) {
1435 die "readme greater than 8192 chars is too long (use --force to override)\n"
1436 unless $force;
1437 warn "using readme greater than 8192 chars with --force\n" unless $quiet;
1439 if ($raw) {
1440 my $rd = get_readme_desc($project->{README});
1441 if ($rd ne "automatic" && $rd ne "suppressed") {
1442 my $xmllint = qx(command -v xmllint); chomp $xmllint;
1443 if (-f $xmllint && -x $xmllint) {
1444 my ($cnt, $err) = $project->_lint_readme(0);
1445 if ($cnt) {
1446 my $msg = "xmllint: $cnt error";
1447 $msg .= "s" unless $cnt == 1;
1448 print STDERR "$msg\n", "-" x length($msg), "\n", $err
1449 unless $force && $quiet;
1450 exit(255) unless $force;
1451 warn $project->{name} . ": using invalid raw HTML with --force\n" unless $quiet;
1453 } else {
1454 die "xmllint not available, refusing to use raw HTML without --force\n"
1455 unless $force;
1456 warn "xmllint not available using unchecked raw HTML with --force\n" unless $quiet;
1460 if (defined($old) && $old eq $project->{README}) {
1461 warn $project->{name}, ": skipping update of README to same value\n" unless $quiet;
1462 } else {
1463 # Avoid touching anything other than README.html file
1464 $project->_property_fput("README", $project->{README});
1465 $project->_set_changed;
1466 my $desc = get_readme_desc($project->{README});
1467 if ($newname) {
1468 $newname .= " ($desc)";
1469 } else {
1470 $newname = $desc;
1472 warn $project->{name}, ": README updated to $newname\n" unless $quiet;
1474 return 0;
1477 sub valid_head {
1478 my ($proj, $newhead) = @_;
1479 my %okheads = map({($_ => 1)} $proj->get_heads);
1480 exists($okheads{$newhead});
1483 sub cmd_sethead {
1484 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage;
1485 my $project = get_project($ARGV[0]);
1486 if (@ARGV == 2 && !valid_head($project, $ARGV[1])) {
1487 die "invalid head (try \"@{[basename($0)]} listheads $ARGV[0]\"): \"$ARGV[1]\"\n";
1489 my $old = $project->{HEAD};
1490 if (@ARGV == 1) {
1491 print "$old\n" if defined($old);
1492 return 0;
1494 if (defined($old) && $old eq $ARGV[1]) {
1495 warn $project->{name}, ": skipping update of HEAD symref to same value\n" unless $quiet;
1496 } else {
1497 # Avoid touching anything other than the HEAD symref
1498 $project->set_HEAD($ARGV[1]);
1499 $project->_set_changed;
1500 warn $project->{name}, ": HEAD symref updated to \"refs/heads/$ARGV[1]\"\n" unless $quiet;
1502 return 0;
1505 sub cmd_sethooks {
1506 my $force = 0;
1507 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1508 @ARGV == 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage;
1509 my $project = get_project($ARGV[0]);
1510 my $projconfig = read_config_file_hash($project->{path}."/config");
1511 my $ghp = $Girocco::Config::reporoot."/_global/hooks";
1512 my $rghp = realpath($ghp);
1513 my $lhp = $project->{path}."/hooks";
1514 my $rlhp = realpath($lhp);
1515 my $ahp = "";
1516 my $rahp = undef;
1517 if (defined($projconfig) && defined($projconfig->{"core.hookspath"})) {
1518 $ahp = $projconfig->{"core.hookspath"};
1519 $rahp = realpath($ahp);
1521 if (@ARGV == 1) {
1522 if (defined($rahp) && $rahp ne "") {
1523 if ($rahp eq $rghp) {
1524 my $nc = ($ahp eq $ghp ? "" : " non-canonical");
1525 printf "%s \t(global%s)\n", $ahp, $nc;
1526 } elsif ($rahp eq $rlhp) {
1527 my $nc = ($ahp eq $lhp ? "" : " non-canonical");
1528 printf "%s \t(local%s)\n", $ahp, $nc;
1529 } elsif ($rahp ne $ahp) {
1530 print "$ahp \t($rahp)\n";
1531 } else {
1532 print "$ahp\n";
1534 } elsif ($ahp ne "") {
1535 print "$ahp \t(non-existent)\n";
1537 return 0;
1539 my $shp = $ARGV[1];
1540 if (lc($shp) eq "global") {
1541 $shp = $ghp;
1542 } elsif (lc($shp) eq "local") {
1543 $shp = $lhp;
1544 } elsif (substr($shp, 0, 2) eq "~/") {
1545 $shp = $ENV{"HOME"}.substr($shp,1);
1546 } elsif ($shp =~ m,^~([a-zA-Z_][a-zA-Z_0-9]*)((?:/.*)?)$,) {
1547 my $sfx = $2;
1548 my $hd = (getpwnam($1))[7];
1549 $shp = $hd . $sfx if defined($hd) && $hd ne "" && $hd ne "/" && -d $hd;
1551 $shp ne "" && -d $shp or die "no such directory: $ARGV[1]\n";
1552 my $rshp = realpath($shp);
1553 defined($rshp) && $rshp ne "" or die "could not realpath: $ARGV[1]\n";
1554 $rshp =~ m,^/[^/], or die "invalid hookspath: $rshp\n";
1555 die "refusing to switch from current non-global hookspath without --force\n"
1556 if !$force && defined($rahp) && $rahp ne "" && $rahp ne $rghp && $rshp ne $rahp;
1557 if (!$force && defined($rahp) && $rahp ne "") {
1558 if ($rshp eq $rahp && ($ahp eq $ghp || $ahp eq $lhp)) {
1559 warn $project->{name}, ": skipping update of hookspath to same effective value\n" unless $quiet;
1560 return 0;
1563 $rshp = $ghp if $rshp eq $rghp;
1564 $rshp = $lhp if $rshp eq $rlhp;
1565 if ($rshp eq $ahp) {
1566 warn $project->{name}, ": skipping update of hookspath to same value\n" unless $quiet;
1567 return 0;
1569 die "refusing to set neither local nor global hookspath without --force\n"
1570 if !$force && $rshp ne $ghp && $rshp ne $lhp;
1571 system($Girocco::Config::git_bin, '--git-dir='.$project->{path},
1572 'config', "core.hookspath", $rshp);
1573 my $newval = '"'.$rshp.'"';
1574 $newval = "global" if $rshp eq $ghp;
1575 $newval = "local" if $rshp eq $lhp;
1576 warn $project->{name}, ": hookspath set to $newval\n" unless $quiet;
1577 return 0;
1580 our %boolfields;
1581 BEGIN {
1582 %boolfields = (
1583 cleanmirror => 1,
1584 reverseorder => 0,
1585 summaryonly => 0,
1586 statusupdates => 1,
1590 sub cmd_setbool {
1591 my $force = 0;
1592 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1593 @ARGV == 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage;
1594 my $project = get_project($ARGV[0]);
1595 if (!exists($boolfields{$ARGV[1]})) {
1596 die "invalid boolean field name: \"$ARGV[1]\" -- try \"help\"\n";
1598 if (@ARGV == 3 && $boolfields{$ARGV[1]} && !$project->{mirror}) {
1599 die "invalid boolean field for non-mirror (use --force to accept): \"$ARGV[1]\"\n"
1600 unless $force;
1601 warn "using mirror field on non-mirror with --force\n" unless $quiet;
1603 if (@ARGV == 3 && !valid_bool($ARGV[2])) {
1604 die "invalid boolean value: \"$ARGV[2]\"\n";
1606 my $bool = clean_bool($ARGV[2]);
1607 my $old = $project->{$ARGV[1]};
1608 if (@ARGV == 2) {
1609 print "$old\n" if defined($old);
1610 return 0;
1612 if (defined($old) && $old eq $bool) {
1613 warn $project->{name}, ": skipping update of $ARGV[1] to same value\n" unless $quiet;
1614 } else {
1615 # Avoid touching anything other than $ARGV[1] field
1616 $project->_property_fput($ARGV[1], $bool);
1617 warn $project->{name}, ": $ARGV[1] updated to $bool\n" unless $quiet;
1619 return 0;
1622 sub cmd_setautogchack {
1623 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage;
1624 my $project = get_project($ARGV[0]);
1625 my $aghok = $Girocco::Config::autogchack &&
1626 ($project->{mirror} || $Girocco::Config::autogchack ne "mirror");
1627 my $old = defined($project->{autogchack}) ? clean_bool($project->{autogchack}) : "unset";
1628 if (@ARGV == 1) {
1629 print "$old\n" if $aghok;
1630 return 0;
1632 my $bool;
1633 if (lc($ARGV[1]) eq "unset") {
1634 $bool = "unset";
1635 } else {
1636 valid_bool($ARGV[1]) or die "invalid boolean value: \"$ARGV[1]\"\n";
1637 $bool = clean_bool($ARGV[1]);
1639 if (!$aghok) {
1640 die "\$Girocco::Config::autogchack is false\n" unless $Girocco::Config::autogchack;
1641 die "\$Girocco::Config::autogchack is only enabled for mirrors\n";
1643 if ($old eq $bool) {
1644 warn $project->{name}, ": autogchack value unchanged\n" unless $quiet;
1645 } else {
1646 if ($bool eq "unset") {
1647 system($Girocco::Config::git_bin, '--git-dir='.$project->{path},
1648 'config', '--unset', "girocco.autogchack");
1649 } else {
1650 system($Girocco::Config::git_bin, '--git-dir='.$project->{path},
1651 'config', '--bool', "girocco.autogchack", $bool);
1654 return system($Girocco::Config::basedir . "/jobd/maintain-auto-gc-hack.sh", $project->{name}) == 0
1655 ? 0 : 1;
1658 sub valid_url {
1659 my ($url, $type) = @_;
1660 $type ne 'baseurl' and return valid_web_url($url);
1661 valid_repo_url($url) or return 0;
1662 if ($Girocco::Config::restrict_mirror_hosts) {
1663 my $mh = extract_url_hostname($url);
1664 is_dns_hostname($mh) or return 0;
1665 !is_our_hostname($mh) or return 0;
1667 return 1;
1670 our %urlfields;
1671 BEGIN {
1672 %urlfields = (
1673 baseurl => ["url" , 1],
1674 homepage => ["hp" , 0],
1675 notifyjson => ["notifyjson", 0],
1679 sub cmd_seturl {
1680 my $force = 0;
1681 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1682 @ARGV == 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage;
1683 my $project = get_project($ARGV[0]);
1684 if (!exists($urlfields{$ARGV[1]})) {
1685 die "invalid URL field name: \"$ARGV[1]\" -- try \"help\"\n";
1687 if (@ARGV == 3 && ${$urlfields{$ARGV[1]}}[1] && !$project->{mirror}) {
1688 die "invalid URL field for non-mirror (use --force to accept): \"$ARGV[1]\"\n"
1689 unless $force;
1690 warn "using mirror field on non-mirror with --force\n" unless $quiet;
1692 if (@ARGV == 3 && !valid_url($ARGV[2], $ARGV[1])) {
1693 die "invalid URL (use --force to accept): \"$ARGV[2]\"\n"
1694 unless $force;
1695 warn "using invalid URL with --force\n" unless $quiet;
1697 my $old = $project->{${$urlfields{$ARGV[1]}}[0]};
1698 if (@ARGV == 2) {
1699 print "$old\n" if defined($old);
1700 return 0;
1702 if (defined($old) && $old eq $ARGV[2]) {
1703 warn $project->{name}, ": skipping update of $ARGV[1] to same value\n" unless $quiet;
1704 } else {
1705 # Avoid touching anything other than $ARGV[1]'s field
1706 $project->_property_fput(${$urlfields{$ARGV[1]}}[0], $ARGV[2]);
1707 if ($ARGV[1] eq "baseurl") {
1708 $project->{url} = $ARGV[2];
1709 $project->_set_bangagain;
1711 $project->_set_changed unless $ARGV[1] eq "notifyjson";
1712 warn $project->{name}, ": $ARGV[1] updated to $ARGV[2]\n" unless $quiet;
1714 return 0;
1717 our %msgsfields;
1718 BEGIN {
1719 %msgsfields = (
1720 notifymail => 1,
1721 notifytag => 1,
1725 sub cmd_setmsgs {
1726 my $force = 0;
1727 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1728 @ARGV >= 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage;
1729 my $project = get_project(shift @ARGV);
1730 my $field = shift @ARGV;
1731 if (!exists($msgsfields{$field})) {
1732 die "invalid msgs field name: \"$field\" -- try \"help\"\n";
1734 if (@ARGV && !valid_addrlist(@ARGV)) {
1735 die "invalid email address list (use --force to accept): \"".join(" ",@ARGV)."\"\n"
1736 unless $force;
1737 warn "using invalid email address list with --force\n" unless $quiet;
1739 my $old = $project->{$field};
1740 if (!@ARGV) {
1741 printf "%s\n", clean_addrlist($old, " ") if defined($old);
1742 return 0;
1744 my $newlist = clean_addrlist(join(" ",@ARGV));
1745 if (defined($old) && $old eq $newlist) {
1746 warn $project->{name}, ": skipping update of $field to same value\n" unless $quiet;
1747 } else {
1748 # Avoid touching anything other than $field's field
1749 $project->_property_fput($field, $newlist);
1750 warn $project->{name}, ": $field updated to \"$newlist\"\n" unless $quiet;
1752 return 0;
1755 sub cmd_setusers {
1756 my $force = 0;
1757 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1758 @ARGV >= 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage;
1759 my $project = get_project(shift @ARGV);
1760 my $projname = $project->{name};
1761 !@ARGV || !$project->{mirror} or die "cannot set users list for mirror project: \"$projname\"\n";
1762 my @newusers = ();
1763 if (@ARGV) {
1764 eval {@newusers = validate_users(join(" ", @ARGV), $force); 1;} or exit 255;
1765 die "refusing to set empty users list without --force\n" unless @newusers || $force;
1767 return 0 if !@ARGV && $project->{mirror};
1768 my $oldusers = $project->{users};
1769 if ($oldusers && ref($oldusers) eq "ARRAY") {
1770 $oldusers = join("\n", @$oldusers);
1771 } else {
1772 $oldusers = "";
1774 if (!@ARGV) {
1775 print "$oldusers\n" if $oldusers ne "";
1776 return 0;
1778 if ($oldusers eq join("\n", @newusers)) {
1779 warn "$projname: skipping update of users list to same value\n" unless $quiet;
1780 } else {
1781 # Avoid touching anything other than the users list
1782 $project->{users} = \@newusers;
1783 $project->_update_users;
1784 warn "$projname: users list updated to \"@{[join(',',@newusers)]}\"\n" unless $quiet;
1786 return 0;
1789 our %fieldnames;
1790 BEGIN {
1791 %fieldnames = (
1792 owner => [\&cmd_setowner, 0],
1793 desc => [\&cmd_setdesc, 0],
1794 description => [\&cmd_setdesc, 0],
1795 readme => [\&cmd_setreadme, 0],
1796 head => [\&cmd_sethead, 0],
1797 HEAD => [\&cmd_sethead, 0],
1798 hooks => [\&cmd_sethooks, 0],
1799 hookspath => [\&cmd_sethooks, 0],
1800 cleanmirror => [\&cmd_setbool, 1],
1801 reverseorder => [\&cmd_setbool, 1],
1802 summaryonly => [\&cmd_setbool, 1],
1803 statusupdates => [\&cmd_setbool, 1],
1804 autogchack => [\&cmd_setautogchack, 0],
1805 baseurl => [\&cmd_seturl, 1],
1806 homepage => [\&cmd_seturl, 1],
1807 notifyjson => [\&cmd_seturl, 1],
1808 notifymail => [\&cmd_setmsgs, 1],
1809 notifytag => [\&cmd_setmsgs, 1],
1810 users => [\&cmd_setusers, 0],
1814 sub do_getset {
1815 $setopt = shift;
1816 my @newargs = ();
1817 push(@newargs, shift) if @_ && $_[0] eq '--force';
1818 my $field = $_[1];
1819 (($setopt && @_ >= 3) || @_ == 2) && exists($fieldnames{$field}) or die_usage;
1820 push(@newargs, shift);
1821 shift unless ${$fieldnames{$field}}[1];
1822 push(@newargs, @_);
1823 diename(($setopt ? "set " : "get ") . $field);
1824 @ARGV = @newargs;
1825 &{${$fieldnames{$field}}[0]}(@ARGV);
1828 sub cmd_get {
1829 do_getset(0, @_);
1832 sub cmd_set {
1833 do_getset(1, @_);
1836 our %commands;
1837 BEGIN {
1838 %commands = (
1839 list => \&cmd_list,
1840 create => \&cmd_create,
1841 adopt => \&cmd_adopt,
1842 remove => \&cmd_remove,
1843 trash => \&cmd_remove,
1844 delete => \&cmd_remove,
1845 show => \&cmd_show,
1846 listheads => \&cmd_listheads,
1847 listtags => \&cmd_listtags,
1848 listctags => \&cmd_listtags,
1849 deltags => \&cmd_deltags,
1850 delctags => \&cmd_deltags,
1851 addtags => \&cmd_addtags,
1852 addctags => \&cmd_addtags,
1853 chpass => \&cmd_chpass,
1854 checkpw => \&cmd_checkpw,
1855 gc => \&cmd_gc,
1856 update => \&cmd_update,
1857 remirror => \&cmd_remirror,
1858 setowner => \&cmd_setowner,
1859 setdesc => \&cmd_setdesc,
1860 setdescription => \&cmd_setdesc,
1861 setreadme => \&cmd_setreadme,
1862 sethead => \&cmd_sethead,
1863 sethooks => \&cmd_sethooks,
1864 sethookspath => \&cmd_sethooks,
1865 setbool => \&cmd_setbool,
1866 setboolean => \&cmd_setbool,
1867 setflag => \&cmd_setbool,
1868 setautogchack => \&cmd_setautogchack,
1869 seturl => \&cmd_seturl,
1870 setmsgs => \&cmd_setmsgs,
1871 setusers => \&cmd_setusers,
1872 get => \&cmd_get,
1873 set => \&cmd_set,
1877 sub dohelp {
1878 my $cmd = shift;
1879 my $bn = basename($0);
1880 printf "%s version %s\n\n", $bn, $VERSION;
1881 if (defined($cmd) && $cmd ne '') {
1882 $cmd =~ s/^set(?=[a-zA-Z])//i;
1883 my $cmdhelp = '';
1884 my ($lastmt, $incmd);
1885 foreach (split('\n', sprintf($help, $bn))) {
1886 $lastmt || $incmd or $lastmt = /^\s*$/, next;
1887 $incmd = 1 if $lastmt && /^\s*(?:\[?set\]?)?$cmd\s/;
1888 last if $incmd && /^\s*$/;
1889 $incmd and $cmdhelp .= $_ . "\n";
1890 $lastmt = /^\s*$/;
1892 print $cmdhelp and exit 0 if $cmdhelp;
1894 printf $help, $bn;
1895 exit 0;
1898 sub main {
1899 local *ARGV = \@_;
1900 shift, $quiet=1 if @ARGV && $ARGV[0] =~ /^(?:-q|--quiet)$/i;
1901 dohelp($ARGV[1]) if !@ARGV || @ARGV && $ARGV[0] =~ /^(?:-h|-?-help|help)$/i;
1902 my $command = shift;
1903 diename($command);
1904 $setopt = 1;
1905 if (!exists($commands{$command}) && exists($commands{"set".$command})) {
1906 $setopt = 0;
1907 $command = "set" . $command;
1909 exists($commands{$command}) or die "Unknown command \"$command\" -- try \"help\"\n";
1910 dohelp($command) if @ARGV && ($ARGV[0] =~ /^(?:-h|-?-help)$/i ||
1911 $ARGV[0] =~ /^help$/i && !Girocco::Project::does_exist("help",1));
1912 &{$commands{$command}}(@ARGV);