3 # projtool.pl - command line Girocco project maintenance tool
4 # Copyright (C) 2016,2017,2020,2021 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.
12 use vars
qw($VERSION);
13 BEGIN {*VERSION = \'1.0.7'}
15 use Digest::MD5 qw(md5_hex);
18 use POSIX
qw(strftime);
21 BEGIN {$origHOME = $ENV{HOME
}}
23 use lib
"__BASEDIR__";
26 use Girocco
::HashUtil
;
31 exit(&main
(@ARGV)||0);
34 BEGIN {my @a; /^(.*)$/s && push(@a, $1) foreach @ARGV; @ARGV=@a;}
35 BEGIN {$help = <<'HELP'}
36 Usage: %s [<global option>...] <command> <options>
39 -q | --quiet suppress warning messages
40 -p | --pager force output to be paginated
41 --no-pager never paginate output
43 Note that as a convenience, where an existing <project> is required as
44 an argument, a path to the project may be given instead of the name in
45 most places. The "remove" and "prune" commands only accept names.
46 Since a matching project name takes precedence over a path, to force
47 interpretation as a path, start the path with "/" or "./" or "../".
48 Giving "." will find the project matching the current working directory.
51 show full help or just for <command> if given
53 list [--verbose] [--sort=lcname|name|owner|gid|no] [--owner] [<regexp>]
54 list all projects (default is --sort=lcname)
55 limit to project names matching <regex> if given
56 match <regex> against owner instead of project name with --owner
58 create [--force] [--no-alternates] [--orphan] [<option>...] <project>
59 create new project <project> (prompted)
61 --no-alternates skip setup of objects/info/alternates
62 --orphan allow creation of subproject w/o a parent
63 -p use mkdir -p during --orphan creation
64 --no-password set password crypt to invalid value "unknown"
65 --no-owner leave the gitweb.owner config unset
66 --mirror=<url> create a mirror from <url>
67 --full-mirror mirror all refs
68 --push[=<list>] create a push project
69 --desc=<string> specify project description w/o prompt
70 --homepage=<url> specify project homepage URL w/o prompt
71 --defaults do no interactive prompting at all
72 Using --no-password skips the prompts for password, using
73 --no-owner skips the prompt for owner and using --mirror=<url>
74 or --push[=<list>] skips the prompts for mirror URL and
75 heads-only and push users. With --defaults if neither
76 --mirror=<url> nor --push[=<list>] is given then --push will
77 be implied. Using --desc=<string> will force a specific
78 description (including an empty string) and skip the prompt for
79 it. Otherwise a non-empty default description will always be
80 supplied in lieu of an empty or omitted description.
82 adopt [--force] [--type=mirror|push] [<option>...] <project> [<users>]
83 adopt project <project>
84 type of project is guessed if --type=<type> omitted
85 <users> is same as <newuserslist> for setusers command
87 --dry-run do all the checks but don't perform adoption
88 --verbose show project info dump (useful with --dry-run)
89 --no-users no push users at all (<users> must be omitted)
90 --no-owner leave the gitweb.owner config totally unchanged
91 --owner=<val> set the gitweb.owner config to <val>
92 Both --no-owner and --owner=<val> may NOT be given, with neither
93 take owner from preexisting gitweb.owner else use admin setting.
94 For mirrors <users> is ignored otherwise if no <users> and no
95 --no-users option the push users list will consist of the single
96 user name matching the owner or empty if none or more than one.
97 With --dry-run <project> can be an absolute path to a git dir.
99 remove [--force] [--really-delete] [--keep-forks] <project>
100 remove project <project>
101 do not move to _recyclebin with --really-delete (just rm -rf)
102 remove projects with forks (by keeping forks) using --keep-forks
104 prune [--quiet] (--force | --dry-run) [<project>...]
105 check to see if any projects (default is all projects) are
106 missing the associated project directory on disk.
107 Requires either --force or --dry-run option to operate.
108 With --dry-run only show what would be done.
109 With --prune actually remove any extraneous project(s).
110 With --dry-run exit code is non-zero if any action needed.
111 With --quiet, suppress output message, if any.
114 show project <project>
116 verify [--quiet] [--dir] <project>
117 show the canonical project name for <project> (which might
118 be a path) if and only if it exists. With --dir show the
119 canonical full path to the project directory instead.
120 If the project name is invalid or does not exist, display
121 an error (unless --quiet is used). Exit status will be 0
122 if project found, non-zero otherwise.
124 worktree [--force] <project> <git worktree arguments>
125 run 'git --git-dir=<project-git-dir> worktree <worktree args>'
126 except that if <git worktree arguments> consists of the
127 subcommand "add" and a single non-option argument and the
128 <project>'s git-dir is bare (the usual case) and the <project>'s
129 HEAD branch is not already checked out in any other worktree,
130 then suitable arguments will be passed to the `worktree`
131 command to make the newly created worktree checkout the HEAD
132 branch of the project (special logic makes that work even
135 urls [--push] <project>
136 show available fetch/push URLs for <project>
137 Note that this does NOT include non-Git protocol URLs such
138 as any home page or any upstream URL for a mirror project --
139 those are all accessible via the "show" command.
140 The URLs shown are those that would be shown by gitweb.cgi.
141 With --push only show push urls (mirrors have no push urls)
144 list all available heads for <project> and indicate current head
146 listtags [--verbose] <project>
147 list all ctags on project <project>
148 with --verbose include tag counts
150 deltags <project> [-i] <tagstodel>
151 remove any ctags on project <project> present in <tagstodel>
152 <tagstodel> is space or comma separated list of tags to remove
153 with -i match against <tagstodel> without regard to letter case
155 addtags <project> <tagstoadd>
156 add ctags to project <project>
157 <tagstoadd> is space or comma separated list of tags to add
159 chpass [--force] <project> [random | unknown]
160 change project <project> password (prompted)
161 with "random" set to random password
162 with "unknown" set password hash to invalid value "unknown"
165 check project <project> password for a match (prompted)
167 gc [--force | --auto] [--redelta | --recompress] <project>
168 run the gc.sh script on project <project>
169 with --auto let the gc.sh script decide what to do
170 with --force cause a full gc to take place (force_gc=1)
171 with neither --auto nor --force do a mini or if needed a full gc
172 (in other words just touch .needsgc and run gc.sh)
173 with --redelta a full gc will use pack-objects --no-reuse-delta
174 with --recompress a full gc uses pack-objects --no-reuse-object
175 (--no-reuse-delta and --no-reuse-object are accepted as aliases)
176 with --aggressive activate the --force and --redelta options
177 unless the global --quiet option is given show_progress=1 is used
179 update [--force] [--quiet | --summary] <project>
180 run the update.sh script on project <project>
181 with --force cause a fetch to always take place (force_update=1)
182 with --quiet only show errors (show_progress is left unset)
183 with --summary show progress and ref summary (show_progress=1)
184 with neither --quiet nor --summary show it all (show_progress=2)
186 remirror [--force] <project>
187 initiate a remirror of project <project>
189 [set]owner [--force] <project> <newowner>
190 set project <project> owner to <newowner>
191 without "set" and only 1 arg, just show current project owner
193 [set]desc [--force] <project> <newdesc>
194 set project <project> description to <newdesc>
195 without "set" and only 1 arg, just show current project desc
197 [set]readme [--force] [--format=<readmetype>] <project> [<newsetting>]
198 set project <project> readme to <newsetting>
199 <readmetype> is markdown|plain|html (default is no change)
200 <newsetting> is automatic|suppressed|-|[@]filename
201 with "set" <readmetype> and/or <newsetting> is required
202 without "set" and only 2 args, just show current readme setting
204 [set]head <project> <newhead>
205 set project <project> HEAD symbolic ref to <newhead>
206 without "set" and only 1 arg, just show current project HEAD
208 [set]bool [--force] <project> <flagname> <boolvalue>
209 set project <project> boolean <flagname> to <boolvalue>
210 <flagname> is cleanmirror|reverseorder|summaryonly|statusupdtaes
211 without "set" and only 2 args, just show current flag value
213 [set]hooks [--force] <project> local | global | <path>
214 set project <project> hookspath to local, global or <path>
215 without "set" and only 1 arg, just show current hookspath
217 [set]autogchack <project> <boolvalue> | unset
218 set project <project> autogchack to <boolvalue> or "unset" it
219 without "set" just show current autogchack setting if enabled
220 with "set" autogchack must be enabled in Config.pm for the
221 type of project and maintain-auto-gc-hack.sh is always run
223 [set]url [--force] <project> <urlname> <newurlvalue>
224 set project <project> url <urlname> to <newurlvalue>
225 <urlname> is baseurl|homepage|notifyjson
226 without "set" and only 2 args, just show current url value
228 [set]msgs [--force] <project> <msgsname> <eaddrlist>
229 set project <project> msgs <msgsname> to <addrlist>
230 <msgsname> is notifymail|notifytag
231 <eaddrlist> is space or comma separated list of email addresses
232 without "set" and only 2 args, just show current msgs value
234 [set]users [--force] <project> <newuserslist>
235 set push project <project> users list to <newuserslist>
236 <newuserslist> is space or comma separated list of user names
237 without "set" and only 1 arg, just show current users list
239 [set]jsontype <project> <newjsontype>
240 set project <project> JSON Content-Type to <newjsontype>
241 <newjsontype> is x-www-form-urlencoded or json
242 without "set" and only 1 arg, just show current jsontype
244 [set]jsonsecret <project> <newjsonsecret>
245 set project <project> JSON secret to <newjsonsecret>
246 <newjsonsecret> is a string (empty string disables signatures)
247 without "set" and only 1 arg, just show current jsonsecret
249 get <project> <fieldname>
250 show project <project> field <fieldname>
251 <fieldname> is owner|desc|readme|head|hooks|users|jsontype
252 or jsonsecret|<flagname>|autogchack|<urlname>|<msgsname>
254 set [--force] <project> <fieldname> <newfieldvalue>
255 set project <project> field <fieldname> to <newfieldvalue>
256 <fieldname> same as for get
257 <newfieldvalue> same as for corresponding set... command
264 my $sub = shift || diename
;
266 die "Invalid arguments to $sub command -- try \"help\"\n";
268 die "Invalid arguments -- try \"help\"\n";
274 defined($rm) or $rm = '';
275 return "length " . length($rm);
278 sub get_readme_desc
{
280 defined($rm) or $rm = '';
283 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
285 return $test eq '' ?
"suppressed" : "length " . length($rm);
291 sub get_ctag_counts
{
295 foreach ($project->get_ctag_names) {
298 if (open $ct, '<', $project->{path
}."/ctags/$_") {
301 defined $count or $count = '';
303 $val = $count =~ /^[1-9]\d*$/ ?
$count : 1;
309 push(@ctags, $_."(".$val.")");
312 push(@ctags, [$_, $val]) if $val;
318 sub get_clean_project
{
319 my $project = get_project_harder
(@_);
320 delete $project->{loaded
};
321 delete $project->{base_path
};
322 delete $project->{ccrypt
};
323 /^orig/i || !defined($project->{$_}) and delete $project->{$_} foreach keys %$project;
324 $project->{owner
} = $project->{email
}; delete $project->{email
};
325 $project->{homepage
} = $project->{hp
}; delete $project->{hp
};
326 $project->{baseurl
} = $project->{url
}; delete $project->{url
};
327 if (defined($project->{path
}) && $project->{path
} ne "") {
328 my $rp = realpath
($project->{path
});
329 defined($rp) && $rp ne "" and $project->{realpath
} = $rp;
330 if (-f
"$rp/objects/info/packs") {
331 my $ipt = (stat _
)[9];
332 defined($ipt) and $project->{infopackstime
} =
333 strftime
("%Y-%m-%d %H:%M:%S %z", localtime($ipt));
336 my $owner = $project->{owner
};
339 my @owner_users = map {$owner eq lc($$_[4]) ?
$$_[1] : ()} get_all_users
;
340 $project->{owner_users
} = \
@owner_users if @owner_users;
342 my $projname = $project->{name
};
343 my @forks = grep {$$_[1] =~ m
,^$projname/,} get_all_projects
;
344 $project->{has_forks
} = 1 if @forks;
345 $project->{has_alternates
} = 1 if $project->has_alternates;
346 my @bundles = $project->bundles;
347 for (my $i = 0; $i < @bundles; ++$i) {
348 my $secs = $bundles[$i]->[0];
349 $bundles[$i]->[0] = strftime
("%Y-%m-%d %H:%M:%S %z", localtime($secs));
350 my $sz = $bundles[$i]->[2];
351 1 while $sz =~ s/(?<=\d)(\d{3})(?:,|$)/,$1/g;
352 $bundles[$i]->[2] = $sz;
354 delete $project->{bundles
};
355 $project->{bundles
} = \
@bundles if @bundles;
356 $project->{mirror
} = 0 unless $project->{mirror
};
357 $project->{is_empty
} = 1 if $project->is_empty;
358 delete $project->{showpush
} unless $project->{showpush
};
359 delete $project->{users
} if $project->{mirror
};
360 delete $project->{baseurl
} unless $project->{mirror
};
361 delete $project->{banged
} unless $project->{mirror
};
362 delete $project->{lastrefresh
} unless $project->{mirror
};
363 delete $project->{cleanmirror
} unless $project->{mirror
};
364 delete $project->{statusupdates
} unless $project->{mirror
};
365 delete $project->{lastparentgc
} unless $projname =~ m
,/,;
366 unless ($project->{banged
}) {
367 delete $project->{bangcount
};
368 delete $project->{bangfirstfail
};
369 delete $project->{bangmessagesent
};
371 my $projhook = $project->_has_notifyhook;
372 if (defined($projhook) && $projhook ne "") {
373 $project->{notifyhook
} = $projhook;
375 delete $project->{notifyhook
};
377 $project->{README
} = get_readme_desc
($project->{README
}) if exists($project->{README
});
378 $project->{READMEDATA
} = get_readme_len
($project->{READMEDATA
}) if exists($project->{READMEDATA
});
379 my @tags = get_ctag_counts
($project, 1);
380 $project->{tags
} = \
@tags if @tags;
381 my $projconfig = read_config_file_hash
($project->{path
}."/config");
382 if (defined($projconfig) && defined($projconfig->{"core.hookspath"})) {
383 my $ahp = $projconfig->{"core.hookspath"};
384 my $rahp = realpath
($ahp);
385 my $lhp = $project->{path
}."/hooks";
386 my $rlhp = realpath
($lhp);
387 my $ghp = $Girocco::Config
::reporoot
."/_global/hooks";
388 my $rghp = realpath
($ghp);
389 $project->{has_local_hooks
} = 1 if
390 defined($rahp) && defined($rlhp) && $rahp eq $rlhp;
391 $project->{has_global_hooks
} = 1 if
392 defined($rahp) && defined($rghp) && $rahp eq $rghp;
393 $project->{hookspath
} = $ahp unless $ahp eq $lhp || $ahp eq $ghp;
401 foreach (split(/[,\s]+/, $_[0])) {
403 $seen{lc($_)} = 1, push(@newlist, $_) unless $seen{lc($_)};
405 return join(($_[1]||","), @newlist);
409 my $cleaned = clean_addrlist
(join(" ", @_));
410 return 1 if $cleaned eq "";
411 valid_email_multi
($cleaned) && length($cleaned) <= 512;
415 my ($userlist, $force, $nodie) = @_;
419 my $mobok = $Girocco::Config
::mob
&& $Girocco::Config
::mob
eq "mob";
420 my %users = map({($$_[1] => $_)} get_all_users
);
421 foreach (split(/[\s,]+/, $userlist)) {
422 if (exists($users{$_}) || $_ eq "everyone" || ($mobok && $_ eq "mob")) {
423 $seenuser{$_}=1, push(@newusers, $_) unless $seenuser{$_};
426 if (Girocco
::User
::does_exist
($_, 1)) {
428 $seenuser{$_}=1, push(@newusers, $_) unless $seenuser{$_};
431 warn "refusing to allow questionable user \"$_\" without --force\n" unless $nodie && $quiet;
436 warn "invalid user: \"$_\"\n" unless $nodie && $quiet
438 die if $badlist && !$nodie;
442 sub is_default_desc
{
443 # "Unnamed repository; edit this file 'description' to name the repository."
444 # "Unnamed repository; edit this file to name it for gitweb."
446 return 0 unless defined($_);
447 /Unnamed\s+repository;/i && /\s+edit\s+this\s+file\s+/i && /\s+to\s+name\s+/i;
453 return 0 if $test =~ /[\r\n]/;
454 $test =~ s/\s\s+/ /g;
462 defined($desc) or $desc = '';
464 $desc = to_utf8
($desc, 1);
465 $desc =~ s/\s\s+/ /g;
472 Girocco
::CLIUtil
::_parse_options
(
474 warn((($_[0]eq'?')?
"unrecognized":"missing argument for")." option \"$_[1]\"\n")
482 lcname
=> sub {lc($$a[1]) cmp lc($$b[1])},
483 name
=> sub {$$a[1] cmp $$b[1]},
484 gid
=> sub {$$a[3] <=> $$b[3]},
485 owner
=> sub {lc($$a[4]) cmp lc($$b[4]) || lc($$a[1]) cmp lc($$b[1])},
486 no => sub {$$a[0] <=> $$b[0]},
488 my $sortopt = 'lcname';
489 my ($verbose, $owner);
490 parse_options
(":sort" => \
$sortopt, verbose
=> \
$verbose, owner
=> \
$owner);
493 my $val = shift @ARGV;
494 $regex = qr
($val) or die "bad regex \"$val\"\n";
496 !@ARGV && exists($sortsub{$sortopt}) or die_usage
;
497 my $sortsub = $sortsub{$sortopt};
498 my $grepsub = defined($regex) ?
($owner ?
sub {$$_[4] =~ /$regex/} : sub {$$_[1] =~ /$regex/}) : sub {1};
499 my @projects = sort($sortsub grep {&$grepsub} get_all_projects
);
501 print map(sprintf("%s\n", join(":", (@
$_)[1..5])), @projects);
503 print map(sprintf("%s: %s\n", $$_[1], $$_[5] =~ /^:/ ?
"<mirror>" : $$_[5]), @projects);
509 my ($force, $noalternates, $orphanok, $optp, $nopasswd, $noowner, $defaults, $ispush, $pushusers,
510 $ismirror, $desc, $fullmirror, $homepage);
512 force
=> \
$force, "no-alternates" => \
$noalternates, orphan
=> \
$orphanok, p
=> \
$optp,
513 "no-password" => \
$nopasswd, "no-owner" => \
$noowner, defaults
=> \
$defaults,
514 "push" => \
$ispush, ":push" => \
$pushusers, ":mirror" => \
$ismirror, ":desc" => \
$desc,
515 ":description" => \
$desc, "full-mirror" => \
$fullmirror, ":homepage" => \
$homepage);
516 @ARGV == 1 or die_usage
;
517 !defined($pushusers) || defined($ispush) or $ispush = 1;
518 defined($ismirror) && $ismirror =~ /^\s*$/ and die "--mirror url must not be empty\n";
519 die "--mirror and --push are mutually exclusive options\n" if $ismirror && $ispush;
520 die "--full-mirror requires use of --mirror=<url> option\n" if $fullmirror && !$ismirror;
521 !$defaults || defined($ispush) || defined($ismirror) or $ispush = 1;
522 !$defaults || defined($nopasswd) or $nopasswd = 1;
523 !$defaults || defined($noowner) or $noowner = 1;
524 !defined($ispush) || defined($pushusers) or $pushusers = "";
525 my $projname = $ARGV[0];
526 $projname =~ s/\.git$//i;
527 Girocco
::Project
::does_exist
($projname, 1) and die "Project already exists: \"$projname\"\n";
528 if (!Girocco
::Project
::valid_name
($projname, $orphanok, $optp)) {
529 warn "Refusing to create orphan project without --orphan\n"
530 if !$quiet && !$orphanok && Girocco
::Project
::valid_name
($projname, 1, 1);
531 warn "Required orphan parent directory does not exist (use -p): ",
532 $Girocco::Config
::reporoot
.'/'.Girocco
::Project
::get_forkee_name
($projname), "\n"
533 if !$quiet && $orphanok && Girocco
::Project
::valid_name
($projname, 1, 1);
534 die "Invalid project name: \"$projname\"\n";
536 my ($forkee, $project) = ($projname =~ m
#^(.*/)?([^/]+)$#);
537 my $newtype = $forkee ?
'fork' : 'project';
538 if (length($project) > 64) {
539 die "The $newtype name is longer than 64 characters. Do you really need that much?\n"
541 warn "Allowing $newtype name longer than 64 characters with --force\n" unless $quiet;
543 unless ($Girocco::Config
::push || $Girocco::Config
::mirror
) {
544 die "Project creation disabled (no mirrors or push projects allowed)\n" unless $force;
545 warn "Continuing with --force even though both push and mirror projects are disabled\n" unless $quiet;
547 print "Enter settings for new project \"$projname\"\n" unless $defaults;
549 $settings{noalternates
} = $noalternates;
551 $settings{crypt} = "unknown";
553 my $np1 = prompt_noecho_nl_or_die
("Admin password for project $projname (echo is off)");
554 $np1 ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
555 my $np2 = prompt_noecho_nl_or_die
("Retype admin password for project $projname");
556 $np1 eq $np2 or die "Our high-paid security consultants have determined that\n" .
557 "the admin passwords you have entered do not match each other.\n";
558 $settings{crypt} = scrypt_sha1
($np1);
562 $owner = prompt_or_die
("Owner/email name for project $projname");
563 unless (valid_email
($owner)) {
565 warn "Your email sure looks weird...?\n";
568 warn "Allowing invalid email with --force\n" unless $quiet;
570 if (length($owner) > 96) {
572 warn "Your email is longer than 96 characters. Do you really need that much?\n";
575 warn "Allowing email longer than 96 characters with --force\n" unless $quiet;
578 $settings{email
} = $owner;
580 my $checkmirror = sub {
581 my $checkurl = shift;
582 unless (valid_repo_url
($checkurl)) {
584 warn "Invalid mirror URL: \"$checkurl\"\n";
587 warn "Allowing invalid mirror URL with --force\n" unless $quiet;
589 if ($Girocco::Config
::restrict_mirror_hosts
) {
590 my $mh = extract_url_hostname
($checkurl);
591 unless (is_dns_hostname
($mh)) {
593 warn "Invalid non-DNS mirror URL: \"$checkurl\"\n";
596 warn "Allowing non-DNS mirror URL with --force\n" unless $quiet;
598 if (is_our_hostname
($mh)) {
600 warn "Invalid same-host mirror URL: \"$checkurl\"\n";
603 warn "Allowing same-host mirror URL with --force\n" unless $quiet;
608 if ($ispush || $ismirror) {
609 !$ispush || $force || $Girocco::Config
::push or
610 die "Push projects are disabled, create a mirror (or use --force)\n";
611 !$ismirror || $force || $Girocco::Config
::mirror
or
612 die "Mirror projects are disabled, create a push project (or use --force)\n";
614 &$checkmirror($ismirror) or die "Invalid --mirror URL\n";
615 $baseurl = $ismirror;
616 $settings{url
} = $baseurl;
617 $settings{cleanmirror
} = $fullmirror ?
0 : 1;
620 if ($pushusers !~ /^[\s,]*$/) {
621 eval {@newusers = validate_users
($pushusers, $force); 1;} or
622 die "Invalid --push user list\n";
624 $settings{users
} = \
@newusers;
626 } elsif ($force || $Girocco::Config
::mirror
) {{
627 if ($force || $Girocco::Config
::push) {
628 $baseurl = prompt_or_die
("URL to mirror from (leave blank for push project)", "");
630 $baseurl = prompt_or_die
("URL to mirror from");
631 unless ($baseurl ne "") {
632 warn "Push projects are disabled, you must enter a mirror URL (or use --force)\n";
636 if ($baseurl ne "") {
637 &$checkmirror($baseurl) or redo;
638 $settings{url
} = $baseurl;
639 $settings{cleanmirror
} =
640 ynprompt_or_die
("Mirror only heads, tags and notes (Y/n)", "Yes");
643 my $mirror = ($baseurl eq "") ?
0 : 1;
644 my $checkdesc = sub {
646 if (length($d) > 1024) {
648 warn "Short description length greater than 1024 characters!\n";
651 warn "Allowing short description length greater than 1024 characters\n" unless $quiet;
655 if (defined($desc)) {
656 $desc =~ s/^\s+//; $desc =~ s/\s+$//;
657 $desc eq "" || &$checkdesc($desc) or
658 die "Invalid --desc description\n";
659 } elsif (!$defaults) {{
660 $desc = prompt_or_die
("Short description", "");
661 $desc =~ s/^\s+//; $desc =~ s/\s+$//;
662 $desc eq "" || &$checkdesc($desc) or redo;
663 $desc = undef if $desc eq "";
665 defined($desc) or $desc = $mirror ?
"Mirror of $baseurl" : "Push project $projname";
666 $settings{desc
} = $desc;
669 unless (valid_web_url
($hpurl)) {
671 warn "Invalid home page URL: \"$hpurl\"\n";
674 warn "Allowing invalid home page URL with --force\n" unless $quiet;
678 if (defined($homepage)) {
679 $homepage =~ s/^\s+//; $homepage =~ s/\s+$//;
680 $homepage eq "" || &$checkhp($homepage) or
681 die "Invalid --homepage URL\n";
682 } elsif (!$defaults) {{
683 $homepage = prompt_or_die
("Home page URL", "");
684 $homepage =~ s/^\s+//; $homepage =~ s/\s+$//;
685 $homepage eq "" || &$checkhp($homepage) or redo;
686 $homepage = undef if $homepage eq "";
688 $settings{hp
} = $homepage;
691 $jsonurl = prompt_or_die
("JSON notify POST URL", "");
692 if ($jsonurl ne "" && !valid_web_url
($jsonurl)) {
694 warn "Invalid JSON notify POST URL: \$jsonurl\"\n";
697 warn "Allowing invalid JSON notify POST URL with --force\n" unless $quiet;
700 $settings{notifyjson
} = $jsonurl;
701 my $commitaddrs = "";
703 $commitaddrs = clean_addrlist
(prompt_or_die
("Commit notify email addr(s)", ""));
704 if ($commitaddrs ne "" && !valid_addrlist
($commitaddrs)) {
706 warn"invalid commit notify email address list (use --force to accept): \"$commitaddrs\"\n";
709 warn "using invalid commit notify email address list with --force\n" unless $quiet;
712 $settings{notifymail
} = $commitaddrs;
713 $settings{reverseorder
} = 1;
714 $settings{reverseorder
} = ynprompt_or_die
("Oldest-to-newest commit order in emails", "Yes")
715 if !$defaults && $commitaddrs ne "";
716 $settings{summaryonly
} = ynprompt_or_die
("Summary only (no diff) in emails", "No")
717 if !$defaults && $commitaddrs ne "";
720 $tagaddrs = clean_addrlist
(prompt_or_die
("Tag notify email addr(s)", ""));
721 if ($tagaddrs ne "" && !valid_addrlist
($tagaddrs)) {
723 warn"invalid tag notify email address list (use --force to accept): \"$tagaddrs\"\n";
726 warn "using invalid tag notify email address list with --force\n" unless $quiet;
729 $settings{notifytag
} = $tagaddrs;
730 if (!$mirror && !$ispush) {{
733 my $userlist = prompt_or_die
("Push users", join(",", @newusers));
734 eval {@newusers = validate_users
($userlist, $force); 1;} or redo;
736 $settings{users
} = \
@newusers;
738 my $newproj = Girocco
::Project
->ghost($projname, $mirror, $orphanok, $optp)
739 or die "Girocco::Project->ghost call failed\n";
741 $newproj->{$k} = $v while ($k, $v) = each(%settings);
742 my $killowner = sub {
743 system($Girocco::Config
::git_bin
, '--git-dir='.$newproj->{path
},
744 'config', '--unset', "gitweb.owner");
747 $newproj->premirror or die "Girocco::Project->premirror failed\n";
748 !$noowner or &$killowner;
749 $newproj->clone or die "Girocco::Project->clone failed\n";
750 warn "Project $projname created and cloning successfully initiated.\n"
753 $newproj->conjure or die "Girocco::Project->conjure failed\n";
754 !$noowner or &$killowner;
755 warn "New push project fork is empty due to use of --no-alternates\n"
756 if !$quiet && $projname =~ m
,/, && $noalternates;
757 warn "Project $projname successfully created.\n" unless $quiet;
764 system($Girocco::Config
::git_bin
, "--git-dir=$gd", 'config', @_) == 0
765 or die "\"git --git-dir='$gd' config ".join(" ", @_)."\" failed.\n";
769 my ($force, $type, $nousers, $dryrun, $noowner, $owner, $users, $verbose);
770 parse_options
(force
=> \
$force, ":type" => \
$type, "no-users" => \
$nousers, "dry-run" => \
$dryrun,
771 "no-owner" => \
$noowner,":owner" => \
$owner, quiet
=> \
$quiet, q
=>\
$quiet, verbose
=> \
$verbose);
772 @ARGV or die "Please give project name on command line.\n";
773 my $projname = shift @ARGV;
774 (!$noowner || !defined($owner)) && (!$nousers || !@ARGV) or die_usage
;
775 !defined($type) || $type eq "mirror" || $type eq "push" or die_usage
;
776 defined($type) or $type = "";
778 if ($dryrun && $projname =~ m
,^/[^.\s/\\:], && is_git_dir
(realpath
($projname))) {
779 $projdir = realpath
($projname);
780 $projname = $projdir;
781 $projname =~ s/\.git$//i;
782 $projname =~ s
,/+$,,;
783 $projname =~ s
,^.*/,,;
784 $projname ne "" or $projname = $projdir;
786 $projname =~ s/\.git$//i;
787 $projname ne "" or die "Invalid project name \"\".\n";
788 unless (Girocco
::Project
::does_exist
($projname, 1)) {
789 Girocco
::Project
::valid_name
($projname, 1, 1)
790 or die "Invalid project name \"$projname\".\n";
791 die "No such project to adopt: $projname\n";
793 defined(Girocco
::Project
->load($projname))
794 and die "Project already known (no need to adopt): $projname\n";
795 $projdir = $Girocco::Config
::reporoot
. "/" . $projname . ".git";
796 is_git_dir
($projdir) or die "Not a git directory: \"$projdir\"\n";
798 my $config = read_config_file
($projdir . "/config");
800 %config = map {($$_[0], defined($$_[1])?
$$_[1]:"true")} @
$config if defined($config);
801 git_bool
($config{"core.bare"}) or die "Not a bare git repository: \"$projdir\"\n";
802 defined(read_HEAD_symref
($projdir)) or die "Project with non-symbolic HEAD ref: \"$projdir\"\n";
803 @ARGV and $users = [validate_users
(join(" ", @ARGV), $force, 1)];
805 if (-e
"$projdir/description") {
806 open my $fd, '<', "$projdir/description" or die "Cannot open \"$projdir/description\": $!\n";
812 defined $desc or $desc = "";
814 $desc = to_utf8
($desc, 1);
815 is_default_desc
($desc) and $desc = "";
816 if ($desc ne "" && !valid_desc
($desc)) {
817 die "invalid 'description' file contents (use --force to accept): \"$desc\"\n"
819 warn "using invalid 'description' file contents with --force\n" unless $quiet;
821 $desc = clean_desc
($desc);
822 if (length($desc) > 1024) {
823 die "description longer than 1024 chars (use --force to accept): \"$desc\"\n"
825 warn "using longer than 1024 char description with --force\n" unless $quiet;
831 my $origreadmedata = "";
832 my $readmetype = Girocco
::Project
::_normalize_rmtype
($config{"girocco.readmetype"},1);
833 if (-e
"$projdir/README.html") {
834 open my $fd, '<', "$projdir/README.html" or die "Cannot open \"$projdir/README.html\": $!\n";
840 defined $readme or $readme = "";
841 $readme = to_utf8
($readme, 1);
842 $readme =~ s/\r\n?/\n/gs;
843 $readme =~ s/^\s+//s;
844 $readme =~ s/\s+$//s;
845 $readme eq "" or $readme .= "\n";
846 $origreadme = $readme;
847 if (-e
"$projdir/README.dat") {
848 open my $fd2, '<', "$projdir/README.dat" or die "Cannot open \"$projdir/README.dat\": $!\n";
851 $readmedata = <$fd2>;
854 defined $readmedata or $readmedata = "";
855 $readmedata = to_utf8
($readmedata, 1);
856 $readmedata =~ s/\r\n?/\n/gs;
857 $readmedata =~ s/^\s+//s;
858 $readmedata =~ s/\s+$//s;
859 $readmedata eq "" or $readmedata .= "\n";
860 $origreadmedata = $readmedata;
862 !$readmetype && length($readme) && !length($readmedata) and do {
863 # the old HTML format
864 $readmetype = 'HTML';
865 $readmedata = $readme;
867 if (length($readmedata) > 8192) {
868 die "readme greater than 8192 chars is too long (use --force to override)\n"
870 warn "using readme greater than 8192 chars with --force\n" unless $quiet;
873 my $dummy = {READMEDATA
=> $readmedata, rmtype
=> $readmetype, name
=> $projname};
874 my ($cnt, $err) = Girocco
::Project
::_lint_readme
($dummy, 0);
876 my $msg = "README: $cnt error";
877 $msg .= "s" unless $cnt == 1;
878 print STDERR
"$msg\n", "-" x
length($msg), "\n", $err
879 unless $force && $quiet;
880 exit(255) unless $force && $readmetype eq 'HTML';
881 warn "$projname: using invalid raw HTML with --force\n" unless $quiet;
883 $readme = $dummy->{README
};
887 $readmetype or $readmetype = Girocco
::Project
::_normalize_rmtype
(""); # use default type
888 # Inspect any remotes now
889 # Yes, Virginia, remote urls can be multi-valued
893 next unless $k =~ /^remote\.([^\/].*?
)\
.([^.]+)$/; # remote name cannot start with "/"
894 my ($name, $subkey) = ($1, $2);
895 $remotes{$name}->{skip} = git_bool($v,1), next if $subkey eq "skipdefaultupdate
" || $subkey eq "skipfetchall
";
896 $remotes{$name}->{mirror} = git_bool($v,1), next if $subkey eq "mirror
"; # we might want this
897 $remotes{$name}->{vcs} = $v, next if defined($v) && $v !~ /^\s*$/ && $subkey eq "vcs
";
898 push(@{$remotes{$name}->{$subkey}}, $v), next if defined($v) && $v !~ /^\s*$/ &&
899 ($subkey eq "url
" || $subkey eq "fetch
" || $subkey eq "push" || $subkey eq "pushurl
");
901 # remotes.default is the default remote group to fetch for "git remote update
" otherwise --all
902 # the remote names in a group are separated by runs of [ \t\n] characters
903 # remote names "", ".", ".." and any name starting with "/" are invalid
904 # a remote with no url or vcs setting is not considered valid
907 if (exists($config{"remotes
.default"})) {
908 foreach (split(/[ \t\n]+/, $config{"remotes
.default"})) {
909 next unless exists($remotes{$_});
910 my $rmt = $remotes{$_};
911 next if !exists($rmt->{url}) && !$rmt->{vcs};
919 next unless $k =~ /^remote\.([^\/].*?)\.[^.]+$/;
920 next if $seenrmt{$1};
922 next unless exists($remotes{$1});
923 my $rmt = $remotes{$1};
924 next if $rmt->{skip} || (!exists($rmt->{url}) && !$rmt->{vcs});
928 my @needskip = (); # remotes that need skipDefaultUpdate set to true
931 my $foundfetchwithmirror = 0;
933 my $rmt = $remotes{$_};
934 push(@needskip, $_) if $usingall && !exists($rmt->{fetch});
935 next unless exists($rmt->{fetch});
937 ++$foundfetchwithmirror if $rmt->{mirror};
938 ++$foundvcs if $rmt->{vcs} || (exists($rmt->{url}) && $rmt->{url}->[0] =~ /^[a-zA-Z0-9][a-zA-Z0-9+.-]*::/);
940 # if we have $foundvcs then we need to explicitly set fetch.prune to false
941 # if we have $foundfetch > 1 then we need to explicitly set fetch.prune to false
942 my $neednoprune = !exists($config{"fetch
.prune
"}) && ($foundvcs || $foundfetch > 1);
944 my $needfakeorigin = 0; # if true we need to set remote.origin.skipDefaultUpdate = true
945 # if remote "origin
" exists we always pick up its first url or use ""
946 if (exists($remotes{origin})) {
947 my $rmt = $remotes{origin};
948 $baseurl = exists($rmt->{url}) ? $rmt->{url}->[0] : "";
949 $needfakeorigin = !exists($rmt->{url}) && !$rmt->{vcs} && !$rmt->{skip};
952 # get the first url of the @check remotes
954 my $rmt = $remotes{$_};
955 next unless exists($rmt->{url});
956 next unless defined($rmt->{url}->[0]) && $rmt->{url}->[0] ne "";
957 $baseurl = $rmt->{url}->[0];
961 my $makemirror = $type eq "mirror
" || ($type eq "" && $foundfetch);
963 # If we have $foundfetch we want to make a mirror but complain if
964 # we $foundfetchwithmirror as well unless we have --type=mirror.
965 # Warn if we have --type=push and $foundfetch and !$foundfetchwithmirror.
966 # Warn if we need to set fetch.prune=false when making a mirror
967 # Warn if we need to create remote.origin.skipDefaultUpdate when making a mirror
968 # Complain if @needskip AND !$usingall (warn with --force but don't set skip)
969 # Warn if $usingall and any @needskip (and set them) if making a mirror
970 # Warn if making a mirror and $baseurl eq ""
971 # Warn if we have --type=mirror and !$foundfetch
974 warn "No base URL to mirror from
for adopted
\"$projname\"\n" unless $quiet || $baseurl ne "";
975 warn "Adopting mirror
\"$projname\" without any fetch remotes
\n" unless $quiet || $foundfetch;
976 if ($foundfetchwithmirror) {
977 warn "Refusing to adopt mirror
\"$projname\" with active remote
.<name
>.mirror
=true remote
(s
)\n".
978 "(Use
--type
=mirror to override
)\n"
979 unless $type eq "mirror
";
980 exit(255) unless $type eq "mirror
" || $dryrun;
981 warn "Adopting mirror
\"$projname\" with active remote
.<name
>.mirror
=true remotes
\n"
982 unless $quiet || $type ne "mirror
";
984 warn "Setting explicit fetch
.prune
=false
for adoption of mirror
\"$projname\"\n"
985 if !$quiet && $neednoprune;
986 warn "Setting remote
.origin
.skipDefaultUpdate
=true
for adoption of mirror
\"$projname\"\n"
987 if !$quiet && $needfakeorigin;
988 if (!$usingall && @needskip) {
989 warn "Refusing to adopt mirror empty fetch remote
(s
) (override with
--force
)\n"
991 exit(255) unless $force || $dryrun;
992 warn "Adopting mirror with empty fetch remote
(s
) with
--force
\n"
993 unless $quiet || !$force;
995 warn "Will set skipDefaultUpdate
=true on non
-fetch remote
(s
)\n" if !$quiet && $usingall && @needskip;
996 warn "Adopting mirror with base URL
\"$baseurl\"\n" unless $quiet || $baseurl eq "";
998 warn "Adopting
push \"$projname\" but active non
-mirror remotes are present
\n"
999 if !$quiet && $foundfetch && !$foundfetchwithmirror;
1002 if (!$noowner && !defined($owner)) {
1004 $owner = $config{"gitweb
.owner
"};
1005 if (!defined($owner) || $owner eq "") {
1006 $owner = $Girocco::Config::admin;
1007 warn "Using owner
\"$owner\" for adopted project
\n" unless $quiet;
1010 if (!$nousers && !$makemirror && !defined($users)) {
1011 # select user list for push project
1012 my $findowner = $owner;
1013 defined($findowner) or $findowner = $config{"gitweb
.owner
"};
1014 $findowner = lc($findowner) if defined($findowner);
1015 my @owner_users = ();
1016 @owner_users = map {$findowner eq lc($$_[4]) ? $$_[1] : ()} get_all_users
1017 if defined($findowner) && $findowner ne "";
1018 defined($findowner) or $findowner = "";
1019 if (@owner_users <= 1) {
1020 $users = \@owner_users;
1021 warn "No users found that match owner
\"$findowner\"\n" unless @owner_users || $quiet;
1024 warn "Found
".scalar(@owner_users)." users
for owner
\"$findowner\" (" .
1025 join(" ", @owner_users) . ") not setting any
\n" unless $quiet;
1028 defined($users) or $users = [];
1030 # Warn if we preserve an existing receive.denyNonFastForwards or receive.denyDeleteCurrent setting
1031 # Complain if core.logallrefupdates or logs subdir exists and contains any files (allow with --force
1032 # and warn about preserving the setting)
1034 warn "Preserving existing receive
.denyNonFastForwards
=true
\n"
1035 if !$quiet && git_bool($config{"receive
.denynonfastforwards
"});
1036 warn "Preserving existing receive
.denyDeleteCurrent
=$config{'receive.denydeletecurrent'}\n"
1037 if !$quiet && exists($config{"receive
.denydeletecurrent
"}) &&
1038 $config{"receive
.denydeletecurrent
"} ne "warn";
1040 my $reflogfiles = Girocco::Project::_contains_files("$projdir/logs
");
1041 my $reflogactive = git_bool($config{"core
.logallrefupdates
"});
1042 if ($reflogactive || $reflogfiles) {
1043 warn "Refusing to adopt
\"$projname\" with active
ref logs without
--force
\n" if $reflogfiles && !$force;
1044 warn "Refusing to adopt
\"$projname\" with core
.logAllRefUpdates
=true without
--force
\n" if $reflogactive && !$force;
1045 exit(255) unless $force || $dryrun;
1046 warn "Adopting
\"$projname\" with active
ref logs with
--force
\n" unless $quiet || ($reflogfiles && !$force);
1047 warn "Adopting
\"$projname\" with core
.logAllRefUpdates
=true with
--force
\n" unless $quiet || ($reflogactive && !$force);
1050 return 0 if $dryrun && !$verbose;
1052 my $newproj = eval {Girocco::Project->ghost($projname, $makemirror, 1, $dryrun)};
1053 defined($newproj) or die "Girocco
::Project
::ghost failed
: $@
\n";
1054 $newproj->{desc} = $desc;
1055 $newproj->{README} = $readme;
1056 $newproj->{READMEDATA} = $readmedata;
1057 $newproj->{rmtype} = $readmetype;
1058 $newproj->{url} = $baseurl if $makemirror || exists($config{"gitweb
.baseurl
"});
1059 $newproj->{email} = $owner if defined($owner);
1060 $newproj->{users} = $users;
1061 $newproj->{crypt} = "unknown
";
1062 $newproj->{reverseorder} = 1 unless exists($config{"hooks
.reverseorder
"});
1063 $newproj->{summaryonly} = 1 unless exists($config{"hooks
.summaryonly
"});
1064 my $dummy = bless {}, "Girocco
::Project
";
1065 $dummy->{path} = "$projdir";
1066 $dummy->{configfilehash} = \%config;
1067 $dummy->_properties_load;
1068 delete $dummy->{origurl};
1069 foreach my $k (keys(%$dummy)) {
1070 $newproj->{$k} = $dummy->{$k}
1071 if exists($dummy->{$k}) && !exists($newproj->{$k});
1076 my %info = %$newproj;
1077 $info{README} = get_readme_desc($info{README}) if exists($info{README});
1078 my $d = Data::Dumper->new([\%info], ['*'.$newproj->{name}]);
1079 $d->Sortkeys(sub {[sort({lc($a) cmp lc($b)} keys %{$_[0]})]});
1080 print $d->Dump([\%info], ['*'.$newproj->{name}]);
1082 return 0 if $dryrun;
1084 # Make any changes as needed for @needskip, $neednoprune and $needfakeorigin
1086 git_config($projdir, "fetch
.prune
", "false
") if $neednoprune;
1087 git_config($projdir, "remote
.origin
.skipDefaultUpdate
", "true
") if $needfakeorigin;
1088 if ($usingall && @needskip) {
1089 git_config($projdir, "remote
.$_.skipDefaultUpdate
", "true
") foreach @needskip;
1093 # Write out any README.dat/README.html changes before the actual Adoption
1094 # Otherwise they will get stepped on. The Girocco::Project::adopt function
1095 # does not know how to validate README.html during adoption like the above code does.
1096 if ($readmedata ne $origreadmedata) {
1097 open my $fd, '>', "$projdir/README.dat" or die "Cannot write \"$projdir/README
.dat
\": $!\n";
1098 print $fd $readmedata or die "Error writing
\"$projdir/README
.dat
\": $!\n";
1099 close $fd or die "Error closing
\"$projdir/README
.dat
\": $!\n";
1101 if ($readme ne $origreadme || ! -e "$projdir/README
.html
") {
1102 open my $fd, '>', "$projdir/README.html" or die "Cannot write \"$projdir/README
.html
\": $!\n";
1103 print $fd $readme or die "Error writing
\"$projdir/README
.html
\": $!\n";
1104 close $fd or die "Error closing
\"$projdir/README
.html
\": $!\n";
1106 git_config($projdir, "girocco
.rmtype
", $readmetype);
1108 # Perform the actual adoption
1109 $newproj->adopt or die "Girocco
::Project
::adopt failed
\n";
1111 # Perhaps restore core.logAllRefUpdates, receive.denyNonFastForwards and receive.denyDeleteCurrent
1112 git_config($projdir, "receive
.denyNonFastForwards
", "true
")
1113 if git_bool($config{"receive
.denynonfastforwards
"});
1114 git_config($projdir, "receive
.denyDeleteCurrent
", $config{"receive
.denydeletecurrent
"})
1115 if exists($config{"receive
.denydeletecurrent
"}) &&
1116 $config{"receive
.denydeletecurrent
"} ne "warn";
1117 git_config($projdir, "core
.logAllRefUpdates
", "true
")
1122 warn "Mirror project
\"$projname\" successfully adopted
.\n" unless $quiet;
1124 warn "Push project
\"$projname\" successfully adopted
.\n" unless $quiet;
1130 my ($force, $reallydel, $keepforks);
1131 parse_options(force => \$force, "really
-delete" => \$reallydel,
1132 "keep
-forks
" => \$keepforks, quiet => \$quiet, q =>\$quiet);
1133 @ARGV or die "Please give project name on command line
.\n";
1134 @ARGV == 1 or die_usage;
1135 my $project = get_project($ARGV[0]); # for safety only names accepted here
1136 my $projname = $project->{name};
1137 my $isempty = !$project->{mirror} && $project->is_empty;
1138 if (!$project->{mirror} && !$isempty && $reallydel) {
1139 die "refusing to remove
and delete non
-empty
push project without
--force
: $projname\n" unless $force;
1140 warn "allowing removal
and deletion of non
-empty
push project with
--force
\n" unless $quiet;
1144 if ($project->has_forks) {
1145 die "refusing to remove project with forks
(use --keep
-forks
): $projname\n" unless $keepforks;
1146 warn "allowing removal of forked project
while preserving its forks with
--keep
-forks
\n" unless $quiet;
1147 # Run pseudo GC on that repository so that objects don't get lost within forks
1148 my $basedir = $Girocco::Config::basedir;
1149 my $projdir = $project->{path};
1150 warn "We have to run pseudo GC on the repo so that the forks don
't lose data. Hang on...\n" unless $quiet;
1151 my $nogcrunning = sub {
1152 die "Error: GC appears to be currently running on $projname\n"
1153 if -e "$projdir/gc.pid" || -e "$projdir/.gc_in_progress";
1156 $removenogc = ! -e "$projdir/.nogc";
1157 recreate_file("$projdir/.nogc") if $removenogc;
1158 die "unable to create \"$projdir/.nogc\"\n" unless -e "$projdir/.nogc";
1159 delete $ENV{show_progress};
1160 $ENV{'show_progress
'} = 1 unless $quiet;
1163 system("$basedir/toolbox/perform-pre-gc-linking.sh", "--include-packs", $projname) == 0
1164 or die "Running pseudo GC on project $projname failed\n";
1168 if (!$project->{mirror} && !$isempty && !$reallydel) {
1169 $archived = $project->archive_and_delete;
1170 unlink("$archived/.nogc") if $removenogc && defined($archived) && $archived ne "";
1174 warn "Project '$projname' removed from $Girocco::Config::name" .
1175 ($archived ? ", backup in '$archived'" : "") .".\n" unless $quiet;
1176 warn "Retained forks may now have unwanted objects/info/alternates lines\n" if $altwarn && !$quiet;
1181 my ($force, $dryrun);
1182 parse_options(force => \$force, "dry-run" => \$dryrun, "quiet" => \$quiet);
1183 ($force && !$dryrun) || (!$force && $dryrun) or die_usage;
1185 my %allprojs = map({($$_[0] => $_)} Girocco::Project::get_full_list_extended());
1186 my @allprojs = sort({lc($a) cmp lc($b) || $a cmp $b} keys(%allprojs));
1188 @projs or @projs = @allprojs;
1189 my $bd = $Girocco::Config::reporoot.'/';
1192 !$seen{$_} && $allprojs{$_} && ${$allprojs{$_}}[2] >= 65536 or next;
1194 /^[a-zA-Z0-9]/ or next;
1195 my $pd = $bd . $_ . '.git
';
1197 warn "$_: no such directory: $pd\n" unless $quiet;
1200 warn "$_: exists but not directory: $pd\n" unless $quiet;
1204 warn "\n" if @remove && !$quiet;
1206 return 0 unless @remove;
1207 my $msg = "Would remove ".scalar(@remove). " project entr";
1208 $msg .= (@remove == 1 ? "y" : "ies");
1210 $msg .= join("", map("\t$_\n", @remove));
1211 print $msg unless $quiet;
1214 my $msg = "Removed ".scalar(@remove). " project entr";
1215 $msg .= (@remove == 1 ? "y" : "ies");
1217 $msg .= join("", map("\t$_\n", @remove));
1218 my %remove = map({$_ => 1} @remove);
1219 filedb_atomic_edit(jailed_file('/etc/group
'), sub {
1220 my ($name,undef,$gid) = split /:/;
1221 $gid =~ /^\d+$/ && $gid >= 65536 or return $_;
1222 $name =~ /^[a-zA-Z0-9]/ or return $_;
1223 !exists($remove{$name}) and return $_;
1225 print $msg unless $quiet;
1231 @ARGV == 1 or die_usage;
1232 my $project = get_clean_project($ARGV[0]);
1233 my %info = %$project;
1234 my $d = Data::Dumper->new([\%info], ['*'.$project->{name}]);
1235 $d->Sortkeys(sub {[sort({lc($a) cmp lc($b)} keys %{$_[0]})]});
1236 print $d->Dump([\%info], ['*'.$project->{name}]);
1241 use Scalar::Util ();
1243 my $rt = sub { return ref($_[0]) ? Scalar::Util::reftype($_[0]) : '' };
1244 parse_options("quiet" => \$quiet, "dir" => \$dirfp, "directory" => \$dirfp, "git-dir" =>\$dirfp);
1245 @ARGV == 1 or die_usage;
1246 my $project = undef;
1249 $project = get_project_harder($ARGV[0]);
1251 $pname = $project->{name} if &$rt($project) eq 'HASH
';
1252 defined($pname) && $pname ne "" or $project = undef;
1253 !$@ && &$rt($project) ne 'HASH
' and $@ = "No such project: \"$ARGV[0]\"\n";
1254 warn $@ if $@ && !$quiet;
1256 $dirfp && defined($project->{path}) && $project->{path} ne "" and
1257 $pname = $project->{path};
1258 printf "%s\n", $pname;
1264 return () unless -d "$gd/worktrees";
1265 opendir my $dh, "$gd/worktrees" or return ();
1266 my @dirs = grep { !/^\.\.?$/ && -d "$gd/worktrees/$_"} readdir($dh);
1270 open my $hd, '<', "$gd/worktrees/$_/HEAD" or next;
1273 defined($hh) or next;
1275 if ($hh =~ /^[0-9a-f]{40,}$/) {
1276 push(@wt, [$_, $hh]);
1277 } elsif ($hh =~ m{^ref:\s?(refs/heads/.+)$}) {
1278 push(@wt, [$_, $1]);
1285 eval '$Girocco::Config
::var_have_git_250
' or die "worktree requires Git 2.5.0 or later\n";
1287 parse_options("force" => \$force, quiet => \$quiet, q =>\$quiet);
1288 @ARGV >= 2 or die_usage;
1289 my $project = get_project_harder($ARGV[0]);
1290 my $gd = $project->{path};
1291 defined($gd) && -d $gd or die "Project \"$$project{name}\" does not actually exist\n";
1292 if ($project->{mirror}) {
1293 die "Cannot use worktree command on mirror project\n" unless $force;
1294 warn "Continuing with --force even though project is a mirror\n" unless $quiet;
1299 my $wantdwimadd = 0;
1300 my $hb = $project->{HEAD};
1301 defined($hb) or $hb = "";
1302 $hb !~ /^\[/ or $hb = "";
1303 if ($ARGV[0] eq "add" && (@ARGV == 2 && $ARGV[1] !~ /^-/ ||
1304 @ARGV == 3 && $ARGV[1] !~ /^-/ && $hb && $ARGV[2] eq $hb)) {{
1306 # Only "add" subcommand has special handling
1307 my $isbare = get_git_chomp("--git-dir=".$gd, "rev-parse", "--is-bare-repository");
1308 defined($isbare) && $isbare eq "true" or last; # only for bare repos
1309 $symref = get_git_chomp("--git-dir=".$gd, "symbolic-ref", "-q", "HEAD");
1310 defined($symref) && $symref =~ m{^refs/heads/.} or last; # only if symref HEAD
1311 !grep({$$_[1] eq $symref} get_worktrees($gd)) or last; # and not checked out
1312 if (get_git_chomp("--git-dir=".$gd, "rev-parse", "--verify", "-q", $symref)) {
1313 # easy case, branch already exists, just add its name to arg list
1314 push(@ARGV, substr($symref, 11)) unless @ARGV == 3;
1318 # nasty workaround for broken worktree command
1319 my $mttree = get_git_chomp("--git-dir=".$gd, "mktree");
1320 defined($mttree) && $mttree =~ /^[0-9a-f]{40,}$/ or last;
1322 my $cmt = "tree $mttree\nauthor - <-> $now +0000\ncommitter - <-> $now +0000\n\n";
1323 my ($st, $rslt) = capture_command(1, $cmt, $Girocco::Config::git_bin,
1324 "--git-dir=".$gd, "hash-object", "-t", "commit", "-w", "--stdin");
1325 defined($st) && $st == 0 && defined($rslt) or last;
1327 $rslt =~ /^[0-9a-f]{40,}$/ or last;
1329 pop(@ARGV) if @ARGV == 3;
1330 push(@ARGV, $mthash); # requires ugly fixup afterwards
1334 warn $project->{name}, ": cannot DWIM worktree add (check `worktree list` output)\n"
1335 if $wantdwimadd == -1 && !$quiet;
1336 my $saveHOME = $ENV{HOME};
1337 if (defined($origHOME) && $origHOME ne "" && $origHOME =~ m{^(/.+)$} && -d $1) {
1340 my $ec = system($Girocco::Config::git_bin, "--git-dir=".$gd, "worktree", @ARGV);
1341 $ENV{HOME} = $saveHOME;
1342 if (defined($ec) && $ec == 0 && defined($symref) && defined($mthash)) {
1344 foreach (map($$_[0], grep({$$_[1] eq $mthash} get_worktrees($gd)))) {
1345 open my $hf, '>', "$gd/worktrees/$_/HEAD" or next;
1346 print $hf "ref: $symref\n";
1350 defined($ec) && $ec != -1 or return 1;
1356 parse_options("push" => \$pushonly);
1358 @ARGV == 1 or die_usage;
1359 my $project = get_project_harder($ARGV[0]);
1360 my $suffix = "/".$project->{name}.".git";
1361 @Gitweb::Config::git_base_url_list = ();
1362 @Gitweb::Config::git_base_push_urls = ();
1363 @Gitweb::Config::git_base_mirror_urls = ();
1365 package Gitweb::Config;
1366 do $Girocco::Config::basedir."/gitweb/gitweb_config.perl";
1367 !$! or die "could not read gitweb_config.perl: $!\n";
1368 !$@ or die "could not parse gitweb_config.perl: $@\n";
1370 my @fetch_urls = ();
1376 ref($_) eq 'ARRAY
' or die "expected ARRAY ref";
1378 defined($u) && $u ne "" and
1379 push(@$array, $u.$suffix);
1380 } elsif (defined($_) && $_ ne "") {
1381 push(@$array, $_.$suffix);
1387 $items{$_} = 1 foreach @_;
1390 &$add_url(\@fetch_urls, @Gitweb::Config::git_base_url_list);
1391 if ($project->{mirror}) {
1392 &$add_url(\@fetch_urls, @Gitweb::Config::git_base_mirror_urls);
1394 &$add_url(\@push_urls, @Gitweb::Config::git_base_push_urls);
1398 push(@urls, &$uniq(@push_urls));
1400 push(@urls, &$uniq(@fetch_urls, @push_urls));
1402 print map "$_\n", @urls;
1407 @ARGV == 1 or die_usage;
1408 my $project = get_project_harder($ARGV[0]);
1409 my @heads = sort({lc($a) cmp lc($b)} $project->get_heads);
1410 my $cur = $project->{HEAD};
1411 defined($cur) or $cur = '';
1413 my $headhash = get_git("--git-dir=$project->{path}", 'rev
-parse
', '--quiet
', '--verify
', 'HEAD
');
1414 defined($headhash) or $headhash = '';
1416 $headhash or $curmark = '!';
1418 my $mark = $_ eq $cur ? $curmark : ' ';
1426 shift(@ARGV), $vcnt=1 if @ARGV && ($ARGV[0] eq '--verbose
' || $ARGV[0] eq '-v
');
1427 @ARGV == 1 or die_usage;
1428 my $project = get_project_harder($ARGV[0]);
1430 print map("$$_[0]\t$$_[1]\n", get_ctag_counts($project));
1432 print map("$_\n", $project->get_ctag_names);
1439 shift(@ARGV), $ic=1 if @ARGV && $ARGV[0] =~ /^(?:--?ignore-case|-i)$/i;
1440 @ARGV >= 2 or die_usage;
1441 my $project = get_project_harder(shift @ARGV);
1444 push(@{$curtags{lc($_)}}, $_) foreach $project->get_ctag_names;
1446 push(@{$curtags{$_}}, $_) foreach $project->get_ctag_names;
1450 my $ctags = join(" ", @ARGV);
1451 $ctags = lc($ctags) if $ic;
1452 foreach (split(/[\s,]+/, $ctags)) {
1453 next unless exists($curtags{$_});
1454 $seentag{$_}=1, push(@deltags, $_) unless $seentag{$_};
1457 warn $project->{name}, ": skipping removal of only non-existent tags\n" unless $quiet;
1459 # Avoid touching anything other than the ctags
1460 foreach my $tg (@deltags) {
1461 $project->delete_ctag($_) foreach @{$curtags{$tg}};
1463 $project->_set_changed;
1464 $project->_set_forkchange;
1465 warn $project->{name}, ": specified tags have been removed\n" unless $quiet;
1471 @ARGV >= 2 or die_usage;
1472 my $project = get_project_harder(shift @ARGV);
1473 my $ctags = join(" ", @ARGV);
1474 $ctags =~ /[^, a-zA-Z0-9:.+#_-]/ and
1475 die "Content tag(s) \"$ctags\" contain(s) evil character(s).\n";
1476 my $oldmask = umask();
1477 umask($oldmask & ~0060);
1479 foreach (split(/[\s,]+/, $ctags)) {
1480 ++$changed if $project->add_ctag($_, 1);
1483 $project->_set_changed;
1484 $project->_set_forkchange;
1487 my $cnt = ($changed == 1) ? "1 content tag has" : $changed . " content tags have";
1488 warn $project->{name}, ": $cnt been added/updated\n" unless $quiet;
1492 sub _get_random_val {
1497 $md5 = md5_hex(time . $$ . rand() . join(':',%$p));
1504 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force
';
1506 pop(@ARGV), $random=lc($ARGV[1]) if @ARGV==2 && $ARGV[1] =~ /^(?:random|unknown)$/i;
1507 @ARGV == 1 or die_usage;
1508 my $project = get_project_harder($ARGV[0]);
1509 die "refusing to change locked password of project \"$ARGV[0]\" without --force\n"
1510 if $project->is_password_locked;
1513 if ($random eq "random") {
1514 die "refusing to set random password without --force\n" unless $force;
1515 $rmsg = "set to random value";
1516 $newpw = _get_random_val($project);
1518 die "refusing to set password hash to '$random' without --force\n" unless $force;
1519 $rmsg = "hash set to '$random'";
1525 print "Changing admin password for project $ARGV[0]\n";
1526 my $np1 = prompt_noecho_nl_or_die("New password for project $ARGV[0] (echo is off)");
1527 $np1 ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
1528 my $np2 = prompt_noecho_nl_or_die("Retype new password for project $ARGV[0]");
1529 $np1 eq $np2 or die "Our high-paid security consultants have determined that\n" .
1530 "the admin passwords you have entered do not match each other.\n";
1534 defined($newpw) or die "missing new password on STDIN\n";
1538 $newpw ne "" or die "empty passwords are not permitted (brokenness will ensue)\n";
1539 my $old = $project->{crypt};
1540 $project->{crypt} = (defined($random) && $random ne "random") ? $newpw : scrypt_sha1($newpw);
1541 if (defined($old) && $old eq $project->{crypt}) {
1542 warn $project->{name}, ": skipping update of password hash to same value\n" unless $quiet;
1544 # Avoid touching anything other than the password hash
1545 $project->_group_update;
1546 warn $project->{name}, ": admin password $rmsg (new hash stored)\n" unless $quiet;
1552 @ARGV == 1 or die_usage;
1553 my $project = get_project_harder($ARGV[0]);
1554 my $pwhash = $project->{crypt};
1555 defined($pwhash) or $pwhash = "";
1556 if ($pwhash eq "") {
1557 warn $project->{name}, ": no password required\n" unless $quiet;
1560 if ($project->is_password_locked) {
1561 warn $project->{name}, ": password is locked\n" unless $quiet;
1566 $checkpw = prompt_noecho_nl_or_die("Admin password for project $ARGV[0] (echo is off)");
1567 $checkpw ne "" or warn "checking for empty password as hash (very unlikely)\n" unless $quiet;
1570 defined($checkpw) or die "missing admin password on STDIN\n";
1573 unless (Girocco::CLIUtil::check_passwd_match($pwhash, $checkpw)) {
1574 warn "password check failure\n" unless $quiet;
1577 warn "admin password match\n" unless $quiet;
1582 my ($force, $auto, $redelta, $recompress, $aggressive);
1583 parse_options(force => \$force, quiet => \$quiet, q => \$quiet, auto => \$auto,
1584 redelta => \$redelta, "no-reuse-delta" => \$redelta, aggressive => \$force,
1585 recompress => \$recompress, "no-reuse-object" => $recompress,
1586 aggressive => \$aggressive);
1587 $aggressive and $force = $redelta = 1;
1588 $force && $auto and die "--force and --auto are mutually exclusive options\n";
1589 @ARGV or die "Please give project name on command line.\n";
1590 @ARGV == 1 or die_usage;
1591 my $project = get_project_harder($ARGV[0]);
1592 delete $ENV{show_progress};
1593 delete $ENV{force_gc};
1594 $quiet or $ENV{"show_progress"} = 1;
1595 $force and $ENV{"force_gc"} = 1;
1596 if (!$auto && !$force && ! -e $project->{path}."/.needsgc") {
1597 open NEEDSGC, '>', $project->{path}."/.needsgc" and close NEEDSGC;
1599 my @args = ($Girocco::Config::basedir . "/jobd/gc.sh", $project->{name});
1600 $redelta && !$recompress and push(@args, "-f");
1601 $recompress and push(@args, "-F");
1602 my $lastgc = $project->{lastgc};
1603 system({$args[0]} @args) != 0 and return 1;
1604 # Do it again Sam, but only if lastgc was set, gc.sh succeeded and now it's
not set
1606 my $newlastgc = get_git
("--git-dir=$project->{path}", 'config', '--get', 'gitweb.lastgc');
1608 system({$args[0]} @args) != 0 and return 1;
1615 my ($force, $summary);
1616 parse_options
(force
=> \
$force, quiet
=> \
$quiet, q
=> \
$quiet, summary
=> \
$summary);
1617 $quiet && $summary and die "--quiet and --summary are mutually exclusive options\n";
1618 @ARGV or die "Please give project name on command line.\n";
1619 @ARGV == 1 or die_usage
;
1620 my $project = get_project_harder
($ARGV[0]);
1621 $project->{mirror
} or die "Project \"$ARGV[0]\" is a push project, not a mirror project.\n";
1622 delete $ENV{show_progress
};
1623 delete $ENV{force_update
};
1625 $ENV{"show_progress"} = 0;
1627 $ENV{"show_progress"} = ($summary ?
1 : 2);
1629 $force and $ENV{"force_update"} = 1;
1630 system($Girocco::Config
::basedir
. "/jobd/update.sh", $project->{name
}) != 0 and return 1;
1636 parse_options
(force
=> \
$force, quiet
=> \
$quiet, q
=> \
$quiet);
1637 @ARGV or die "Please give project name on command line.\n";
1638 @ARGV == 1 or die_usage
;
1639 my $project = get_project_harder
($ARGV[0]);
1640 $project->{mirror
} or die "Project \"$ARGV[0]\" is a push project, not a mirror project.\n";
1641 if ($project->{clone_in_progress
} && !$project->{clone_failed
}) {
1642 warn "Project \"$ARGV[0]\" already seems to have a clone underway at this moment.\n" unless $quiet && $force;
1643 exit(255) unless $force;
1644 yes_to_continue_or_die
("Are you sure you want to force a remirror");
1646 unlink($project->_clonefail_path);
1647 unlink($project->_clonelog_path);
1648 recreate_file
($project->_clonep_path);
1649 my $sock = IO
::Socket
::UNIX
->new($Girocco::Config
::chroot.'/etc/taskd.socket') or
1650 die "cannot connect to taskd.socket: $!\n";
1651 select((select($sock),$|=1)[0]);
1652 $sock->print("clone ".$project->{name
}."\n");
1653 # Just ignore reply, we are going to succeed anyway and the I/O
1654 # would apparently get quite hairy.
1658 warn "Project \"$ARGV[0]\" remirror initiated.\n" unless $quiet;
1664 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1665 @ARGV == 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage
;
1666 my $project = get_project_harder
($ARGV[0]);
1667 if (@ARGV == 2 && !valid_email
($ARGV[1])) {
1668 die "invalid owner/email (use --force to accept): \"$ARGV[1]\"\n"
1670 warn "using invalid owner/email with --force\n" unless $quiet;
1672 if (@ARGV == 2 && length($ARGV[1]) > 96) {
1673 die "owner/email longer than 96 chars (use --force to accept): \"$ARGV[1]\"\n"
1675 warn "using longer than 96 char owner/email with --force\n" unless $quiet;
1677 my $old = $project->{email
};
1679 print "$old\n" if defined($old);
1682 if (defined($old) && $old eq $ARGV[1]) {
1683 warn $project->{name
}, ": skipping update of owner/email to same value\n" unless $quiet;
1685 # Avoid touching anything other than "gitweb.owner"
1686 $project->_property_fput("email", $ARGV[1]);
1687 $project->_update_index;
1688 $project->_set_changed;
1689 warn $project->{name
}, ": owner/email updated to \"$ARGV[1]\"\n" unless $quiet;
1696 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1697 @ARGV >= 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage
;
1698 my $project = get_project_harder
(shift @ARGV);
1699 if (@ARGV && !valid_desc
(join(" ", @ARGV))) {
1700 die "invalid description (use --force to accept): \"".join(" ", @ARGV)."\"\n"
1702 warn "using invalid description with --force\n" unless $quiet;
1704 my $desc = clean_desc
(join(" ", @ARGV));
1705 if (@ARGV && length($desc) > 1024) {
1706 die "description longer than 1024 chars (use --force to accept): \"$desc\"\n"
1708 warn "using longer than 1024 char description with --force\n" unless $quiet;
1710 my $old = $project->{desc
};
1712 print "$old\n" if defined($old);
1715 if (defined($old) && $old eq $desc) {
1716 warn $project->{name
}, ": skipping update of description to same value\n" unless $quiet;
1718 # Avoid touching anything other than description file
1719 $project->_property_fput("desc", $desc);
1720 $project->_set_changed;
1721 warn $project->{name
}, ": description updated to \"$desc\"\n" unless $quiet;
1727 my ($force, $readmetype) = (0, undef);
1728 parse_options
(force
=> \
$force, ":type" => \
$readmetype, ":format" => \
$readmetype);
1729 @ARGV == 1 && defined($readmetype) and push(@ARGV, undef);
1730 @ARGV == 2 || (@ARGV == 1 && !$force && !defined($readmetype) && !$setopt) or die_usage
;
1731 defined($readmetype) and $readmetype = Girocco
::Project
::_normalize_rmtype
($readmetype,1);
1732 defined($readmetype) && !$readmetype and die_usage
;
1733 my $project = get_project_harder
($ARGV[0]);
1734 my $old = $project->{READMEDATA
};
1736 chomp $old if defined($old);
1737 print "$old\n" if defined($old) && $old ne "";
1740 $readmetype or $readmetype = $project->{rmtype
};
1741 my ($new, $raw, $newname);
1743 if (!defined($ARGV[1])) {
1745 $newname = "original README data";
1746 $readmetype ne $project->{rmtype
} && $new ne "" and $raw = 1;
1747 } elsif ($ARGV[1] eq "-") {
1751 $newname = "contents of <STDIN>";
1752 } elsif (lc($ARGV[1]) eq "automatic" || lc($ARGV[1]) eq "auto") {
1754 } elsif (lc($ARGV[1]) eq "suppressed" || lc($ARGV[1]) eq "suppress") {
1755 $new = "<!-- suppress -->";
1759 die "missing filename for README\n" unless $fn ne "";
1760 die "no such file: \"$fn\"\n" unless -f
$fn && -r
$fn;
1761 open F
, '<', $fn or die "cannot open \"$fn\" for reading: $!\n";
1766 $newname = "contents of \"$fn\"";
1768 defined($new) or $new = '';
1769 my $origrmtype = $project->{rmtype
};
1770 $project->{rmtype
} = $readmetype;
1771 $project->{READMEDATA
} = to_utf8
($new, 1);
1772 $project->_cleanup_readme;
1773 if (length($project->{READMEDATA
}) > 8192) {
1774 die "readme greater than 8192 chars is too long (use --force to override)\n"
1776 warn "using readme greater than 8192 chars with --force\n" unless $quiet;
1779 my ($cnt, $err) = $project->_lint_readme(0);
1781 my $msg = "README: $cnt error";
1782 $msg .= "s" unless $cnt == 1;
1783 print STDERR
"$msg\n", "-" x
length($msg), "\n", $err
1784 unless $force && $quiet;
1785 exit(255) unless $force && $project->{rmtype
} eq 'HTML';
1786 warn $project->{name
} . ": using invalid raw HTML with --force\n" unless $quiet;
1787 $project->{README
} = $project->{READMEDATA
};
1790 if (defined($old) && $old eq $project->{READMEDATA
} && $readmetype eq $origrmtype && !$force) {
1791 warn $project->{name
}, ": skipping update of README to same value\n" unless $quiet;
1793 # Avoid touching anything other than README.html file
1794 $project->_property_fput("READMEDATA", $project->{READMEDATA
}, 1);
1795 $project->_property_fput("README", $project->{README
});
1796 $project->_property_fput("rmtype", $readmetype) if $readmetype ne $origrmtype;
1797 $project->_set_changed;
1798 my $desc = get_readme_desc
($project->{README
});
1800 $newname .= " ($desc)";
1804 warn $project->{name
}, ": README $readmetype format updated to $newname\n" unless $quiet;
1810 my ($proj, $newhead) = @_;
1811 my %okheads = map({($_ => 1)} $proj->get_heads);
1812 exists($okheads{$newhead});
1816 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage
;
1817 my $project = get_project_harder
($ARGV[0]);
1818 if (@ARGV == 2 && !valid_head
($project, $ARGV[1])) {
1819 die "invalid head (try \"@{[basename($0)]} listheads $ARGV[0]\"): \"$ARGV[1]\"\n";
1821 my $old = $project->{HEAD
};
1823 print "$old\n" if defined($old);
1826 if (defined($old) && $old eq $ARGV[1]) {
1827 warn $project->{name
}, ": skipping update of HEAD symref to same value\n" unless $quiet;
1829 # Avoid touching anything other than the HEAD symref
1830 $project->set_HEAD($ARGV[1]);
1831 $project->_set_changed;
1832 warn $project->{name
}, ": HEAD symref updated to \"refs/heads/$ARGV[1]\"\n" unless $quiet;
1839 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1840 @ARGV == 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage
;
1841 my $project = get_project_harder
($ARGV[0]);
1842 my $projconfig = read_config_file_hash
($project->{path
}."/config");
1843 my $ghp = $Girocco::Config
::reporoot
."/_global/hooks";
1844 my $rghp = realpath
($ghp);
1845 my $lhp = $project->{path
}."/hooks";
1846 my $rlhp = realpath
($lhp);
1849 if (defined($projconfig) && defined($projconfig->{"core.hookspath"})) {
1850 $ahp = $projconfig->{"core.hookspath"};
1851 $rahp = realpath
($ahp);
1854 if (defined($rahp) && $rahp ne "") {
1855 if ($rahp eq $rghp) {
1856 my $nc = ($ahp eq $ghp ?
"" : " non-canonical");
1857 printf "%s \t(global%s)\n", $ahp, $nc;
1858 } elsif ($rahp eq $rlhp) {
1859 my $nc = ($ahp eq $lhp ?
"" : " non-canonical");
1860 printf "%s \t(local%s)\n", $ahp, $nc;
1861 } elsif ($rahp ne $ahp) {
1862 print "$ahp \t($rahp)\n";
1866 } elsif ($ahp ne "") {
1867 print "$ahp \t(non-existent)\n";
1872 if (lc($shp) eq "global") {
1874 } elsif (lc($shp) eq "local") {
1876 } elsif (substr($shp, 0, 2) eq "~/") {
1877 $shp = $ENV{"HOME"}.substr($shp,1);
1878 } elsif ($shp =~ m
,^~([a
-zA
-Z_
][a
-zA
-Z_0
-9]*)((?
:/.*)?
)$,) {
1880 my $hd = (getpwnam($1))[7];
1881 $shp = $hd . $sfx if defined($hd) && $hd ne "" && $hd ne "/" && -d
$hd;
1883 $shp ne "" && -d
$shp or die "no such directory: $ARGV[1]\n";
1884 my $rshp = realpath
($shp);
1885 defined($rshp) && $rshp ne "" or die "could not realpath: $ARGV[1]\n";
1886 $rshp =~ m
,^/[^/], or die "invalid hookspath: $rshp\n";
1887 die "refusing to switch from current non-global hookspath without --force\n"
1888 if !$force && defined($rahp) && $rahp ne "" && $rahp ne $rghp && $rshp ne $rahp;
1889 if (!$force && defined($rahp) && $rahp ne "") {
1890 if ($rshp eq $rahp && ($ahp eq $ghp || $ahp eq $lhp)) {
1891 warn $project->{name
}, ": skipping update of hookspath to same effective value\n" unless $quiet;
1895 $rshp = $ghp if $rshp eq $rghp;
1896 $rshp = $lhp if $rshp eq $rlhp;
1897 if ($rshp eq $ahp) {
1898 warn $project->{name
}, ": skipping update of hookspath to same value\n" unless $quiet;
1901 die "refusing to set neither local nor global hookspath without --force\n"
1902 if !$force && $rshp ne $ghp && $rshp ne $lhp;
1903 system($Girocco::Config
::git_bin
, '--git-dir='.$project->{path
},
1904 'config', "core.hookspath", $rshp);
1905 my $newval = '"'.$rshp.'"';
1906 $newval = "global" if $rshp eq $ghp;
1907 $newval = "local" if $rshp eq $lhp;
1908 warn $project->{name
}, ": hookspath set to $newval\n" unless $quiet;
1924 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
1925 @ARGV == 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage
;
1926 my $project = get_project_harder
($ARGV[0]);
1927 if (!exists($boolfields{$ARGV[1]})) {
1928 die "invalid boolean field name: \"$ARGV[1]\" -- try \"help\"\n";
1930 if (@ARGV == 3 && $boolfields{$ARGV[1]} && !$project->{mirror
}) {
1931 die "invalid boolean field for non-mirror (use --force to accept): \"$ARGV[1]\"\n"
1933 warn "using mirror field on non-mirror with --force\n" unless $quiet;
1935 if (@ARGV == 3 && !valid_bool
($ARGV[2])) {
1936 die "invalid boolean value: \"$ARGV[2]\"\n";
1938 my $bool = clean_bool
($ARGV[2]);
1939 my $old = $project->{$ARGV[1]};
1941 print "$old\n" if defined($old);
1944 if (defined($old) && $old eq $bool) {
1945 warn $project->{name
}, ": skipping update of $ARGV[1] to same value\n" unless $quiet;
1947 # Avoid touching anything other than $ARGV[1] field
1948 $project->_property_fput($ARGV[1], $bool);
1949 warn $project->{name
}, ": $ARGV[1] updated to $bool\n" unless $quiet;
1954 sub cmd_setjsontype
{
1955 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage
;
1956 my $project = get_project_harder
($ARGV[0]);
1959 my $jt = lc($ARGV[1]);
1960 index($jt, "/") >= 0 or $jt = "application/".$jt;
1961 $jt eq 'application/x-www-form-urlencoded' ||
1962 $jt eq 'application/json' or
1963 die "invalid jsontype value: \"$ARGV[1]\"\n";
1966 my $old = $project->{jsontype
};
1968 print "$old\n" if defined($old);
1971 if (defined($old) && $old eq $jsontype) {
1972 warn $project->{name
}, ": skipping update of jsontype to same value\n" unless $quiet;
1974 # Avoid touching anything other than jsontype field
1975 $project->_property_fput('jsontype', $jsontype);
1976 warn $project->{name
}, ": jsontype updated to $jsontype\n" unless $quiet;
1981 sub cmd_setjsonsecret
{
1982 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage
;
1983 my $project = get_project_harder
($ARGV[0]);
1987 $js =~ s/^\s+//; $js =~ s/\s+$//;
1990 my $old = $project->{jsonsecret
};
1992 print "$old\n" if defined($old);
1995 if (defined($old) && $old eq $jsonsecret) {
1996 warn $project->{name
}, ": skipping update of jsonsecret to same value\n" unless $quiet;
1998 # Avoid touching anything other than jsonsecret field
1999 $project->_property_fput('jsonsecret', $jsonsecret);
2000 warn $project->{name
}, ": jsonsecret updated to \"$jsonsecret\"\n" unless $quiet;
2005 sub cmd_setautogchack
{
2006 @ARGV == 2 || (@ARGV == 1 && !$setopt) or die_usage
;
2007 my $project = get_project_harder
($ARGV[0]);
2008 my $aghok = $Girocco::Config
::autogchack
&&
2009 ($project->{mirror
} || $Girocco::Config
::autogchack
ne "mirror");
2010 my $old = defined($project->{autogchack
}) ? clean_bool
($project->{autogchack
}) : "unset";
2012 print "$old\n" if $aghok;
2016 if (lc($ARGV[1]) eq "unset") {
2019 valid_bool
($ARGV[1]) or die "invalid boolean value: \"$ARGV[1]\"\n";
2020 $bool = clean_bool
($ARGV[1]);
2023 die "\$Girocco::Config::autogchack is false\n" unless $Girocco::Config
::autogchack
;
2024 die "\$Girocco::Config::autogchack is only enabled for mirrors\n";
2026 if ($old eq $bool) {
2027 warn $project->{name
}, ": autogchack value unchanged\n" unless $quiet;
2029 if ($bool eq "unset") {
2030 system($Girocco::Config
::git_bin
, '--git-dir='.$project->{path
},
2031 'config', '--unset', "girocco.autogchack");
2033 system($Girocco::Config
::git_bin
, '--git-dir='.$project->{path
},
2034 'config', '--bool', "girocco.autogchack", $bool);
2037 return system($Girocco::Config
::basedir
. "/jobd/maintain-auto-gc-hack.sh", $project->{name
}) == 0
2042 my ($url, $type) = @_;
2043 $type ne 'baseurl' and return valid_web_url
($url);
2044 valid_repo_url
($url) or return 0;
2045 if ($Girocco::Config
::restrict_mirror_hosts
) {
2046 my $mh = extract_url_hostname
($url);
2047 is_dns_hostname
($mh) or return 0;
2048 !is_our_hostname
($mh) or return 0;
2056 baseurl
=> ["url" , 1],
2057 homepage
=> ["hp" , 0],
2058 notifyjson
=> ["notifyjson", 0],
2064 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
2065 @ARGV == 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage
;
2066 my $project = get_project_harder
($ARGV[0]);
2067 if (!exists($urlfields{$ARGV[1]})) {
2068 die "invalid URL field name: \"$ARGV[1]\" -- try \"help\"\n";
2070 if (@ARGV == 3 && ${$urlfields{$ARGV[1]}}[1] && !$project->{mirror
}) {
2071 die "invalid URL field for non-mirror (use --force to accept): \"$ARGV[1]\"\n"
2073 warn "using mirror field on non-mirror with --force\n" unless $quiet;
2075 if (@ARGV == 3 && !valid_url
($ARGV[2], $ARGV[1])) {
2076 die "invalid URL (use --force to accept): \"$ARGV[2]\"\n"
2078 warn "using invalid URL with --force\n" unless $quiet;
2080 my $old = $project->{${$urlfields{$ARGV[1]}}[0]};
2082 print "$old\n" if defined($old);
2085 if (defined($old) && $old eq $ARGV[2]) {
2086 warn $project->{name
}, ": skipping update of $ARGV[1] to same value\n" unless $quiet;
2088 # Avoid touching anything other than $ARGV[1]'s field
2089 $project->_property_fput(${$urlfields{$ARGV[1]}}[0], $ARGV[2]);
2090 if ($ARGV[1] eq "baseurl") {
2091 $project->{url
} = $ARGV[2];
2092 $project->_set_bangagain;
2094 $project->_set_changed unless $ARGV[1] eq "notifyjson";
2095 warn $project->{name
}, ": $ARGV[1] updated to $ARGV[2]\n" unless $quiet;
2110 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
2111 @ARGV >= 3 || (@ARGV == 2 && !$force && !$setopt) or die_usage
;
2112 my $project = get_project_harder
(shift @ARGV);
2113 my $field = shift @ARGV;
2114 if (!exists($msgsfields{$field})) {
2115 die "invalid msgs field name: \"$field\" -- try \"help\"\n";
2117 if (@ARGV && !valid_addrlist
(@ARGV)) {
2118 die "invalid email address list (use --force to accept): \"".join(" ",@ARGV)."\"\n"
2120 warn "using invalid email address list with --force\n" unless $quiet;
2122 my $old = $project->{$field};
2124 printf "%s\n", clean_addrlist
($old, " ") if defined($old);
2127 my $newlist = clean_addrlist
(join(" ",@ARGV));
2128 if (defined($old) && $old eq $newlist) {
2129 warn $project->{name
}, ": skipping update of $field to same value\n" unless $quiet;
2131 # Avoid touching anything other than $field's field
2132 $project->_property_fput($field, $newlist);
2133 warn $project->{name
}, ": $field updated to \"$newlist\"\n" unless $quiet;
2140 shift(@ARGV), $force=1 if @ARGV && $ARGV[0] eq '--force';
2141 @ARGV >= 2 || (@ARGV == 1 && !$force && !$setopt) or die_usage
;
2142 my $project = get_project_harder
(shift @ARGV);
2143 my $projname = $project->{name
};
2144 !@ARGV || !$project->{mirror
} or die "cannot set users list for mirror project: \"$projname\"\n";
2147 eval {@newusers = validate_users
(join(" ", @ARGV), $force); 1;} or exit 255;
2148 die "refusing to set empty users list without --force\n" unless @newusers || $force;
2150 return 0 if !@ARGV && $project->{mirror
};
2151 my $oldusers = $project->{users
};
2152 if ($oldusers && ref($oldusers) eq "ARRAY") {
2153 $oldusers = join("\n", @
$oldusers);
2158 print "$oldusers\n" if $oldusers ne "";
2161 if ($oldusers eq join("\n", @newusers)) {
2162 warn "$projname: skipping update of users list to same value\n" unless $quiet;
2164 # Avoid touching anything other than the users list
2165 $project->{users
} = \
@newusers;
2166 $project->_update_users;
2167 warn "$projname: users list updated to \"@{[join(',',@newusers)]}\"\n" unless $quiet;
2175 owner
=> [\
&cmd_setowner
, 0],
2176 desc
=> [\
&cmd_setdesc
, 0],
2177 description
=> [\
&cmd_setdesc
, 0],
2178 readme
=> [\
&cmd_setreadme
, 0],
2179 head
=> [\
&cmd_sethead
, 0],
2180 HEAD
=> [\
&cmd_sethead
, 0],
2181 hooks
=> [\
&cmd_sethooks
, 0],
2182 hookspath
=> [\
&cmd_sethooks
, 0],
2183 cleanmirror
=> [\
&cmd_setbool
, 1],
2184 reverseorder
=> [\
&cmd_setbool
, 1],
2185 summaryonly
=> [\
&cmd_setbool
, 1],
2186 statusupdates
=> [\
&cmd_setbool
, 1],
2187 autogchack
=> [\
&cmd_setautogchack
, 0],
2188 baseurl
=> [\
&cmd_seturl
, 1],
2189 homepage
=> [\
&cmd_seturl
, 1],
2190 notifyjson
=> [\
&cmd_seturl
, 1],
2191 jsontype
=> [\
&cmd_setjsontype
, 0],
2192 jsonsecret
=> [\
&cmd_setjsonsecret
, 0],
2193 notifymail
=> [\
&cmd_setmsgs
, 1],
2194 notifytag
=> [\
&cmd_setmsgs
, 1],
2195 users
=> [\
&cmd_setusers
, 0],
2202 push(@newargs, shift) if @_ && $_[0] eq '--force';
2204 (($setopt && @_ >= 3) || @_ == 2) && exists($fieldnames{$field}) or die_usage
;
2205 push(@newargs, shift);
2206 shift unless ${$fieldnames{$field}}[1];
2208 diename
(($setopt ?
"set " : "get ") . $field);
2210 &{${$fieldnames{$field}}[0]}(@ARGV);
2225 create
=> \
&cmd_create
,
2226 adopt
=> \
&cmd_adopt
,
2227 remove
=> \
&cmd_remove
,
2228 trash
=> \
&cmd_remove
,
2229 delete => \
&cmd_remove
,
2230 prune
=> \
&cmd_prune
,
2232 verify
=> \
&cmd_verify
,
2233 worktree
=> \
&cmd_worktree
,
2235 listheads
=> \
&cmd_listheads
,
2236 listtags
=> \
&cmd_listtags
,
2237 listctags
=> \
&cmd_listtags
,
2238 deltags
=> \
&cmd_deltags
,
2239 delctags
=> \
&cmd_deltags
,
2240 addtags
=> \
&cmd_addtags
,
2241 addctags
=> \
&cmd_addtags
,
2242 chpass
=> \
&cmd_chpass
,
2243 checkpw
=> \
&cmd_checkpw
,
2245 update
=> \
&cmd_update
,
2246 remirror
=> \
&cmd_remirror
,
2247 setowner
=> \
&cmd_setowner
,
2248 setdesc
=> \
&cmd_setdesc
,
2249 setdescription
=> \
&cmd_setdesc
,
2250 setreadme
=> \
&cmd_setreadme
,
2251 sethead
=> \
&cmd_sethead
,
2252 sethooks
=> \
&cmd_sethooks
,
2253 sethookspath
=> \
&cmd_sethooks
,
2254 setbool
=> \
&cmd_setbool
,
2255 setboolean
=> \
&cmd_setbool
,
2256 setflag
=> \
&cmd_setbool
,
2257 setautogchack
=> \
&cmd_setautogchack
,
2258 seturl
=> \
&cmd_seturl
,
2259 setjsontype
=> \
&cmd_setjsontype
,
2260 setjsonsecret
=> \
&cmd_setjsonsecret
,
2261 setmsgs
=> \
&cmd_setmsgs
,
2262 setusers
=> \
&cmd_setusers
,
2268 BEGIN { %nopager = (
2269 # 1 => pager never allowed
2270 # -1 => pager defaults to off instead of on
2288 setdescription
=> -1,
2296 setautogchack
=> -1,
2299 setjsonsecret
=> -1,
2310 my $bn = basename
($0);
2311 setup_pager_stdout
($usepager);
2312 printf "%s version %s\n\n", $bn, $VERSION;
2313 if (defined($cmd) && $cmd ne '') {
2314 $cmd =~ s/^set(?=[a-zA-Z])//i;
2316 my ($lastmt, $incmd);
2317 foreach (split('\n', sprintf($help, $bn))) {
2318 $lastmt || $incmd or $lastmt = /^\s*$/, next;
2319 $incmd = 1 if $lastmt && /^\s*(?:\[?set\]?)?$cmd\s/;
2320 last if $incmd && /^\s*$/;
2321 $incmd and $cmdhelp .= $_ . "\n";
2324 print $cmdhelp and exit 0 if $cmdhelp;
2333 shift, $quiet=1, redo if @ARGV && $ARGV[0] =~ /^(?:-q|--quiet)$/i;
2334 shift, $usepager=1, redo if @ARGV && $ARGV[0] =~ /^(?:-p|--pager|--paginate)$/i;
2335 shift, $usepager=0, redo if @ARGV && $ARGV[0] =~ /^(?:--no-pager|--no-paginate)$/i;
2337 dohelp
($ARGV[1]) if !@ARGV || @ARGV && $ARGV[0] =~ /^(?:-h|-?-help|help)$/i;
2338 my $command = shift;
2341 if (!exists($commands{$command}) && exists($commands{"set".$command})) {
2343 $command = "set" . $command;
2345 exists($commands{$command}) or die "Unknown command \"$command\" -- try \"help\"\n";
2346 dohelp
($command) if @ARGV && ($ARGV[0] =~ /^(?:-h|-?-help)$/i ||
2347 $ARGV[0] =~ /^help$/i && !Girocco
::Project
::does_exist
("help",1));
2348 $nopager{$command} && $nopager{$command} > 0 and $usepager = 0;
2349 my $pgdfltoff = $nopager{$command} && $nopager{$command} < 0 ?
1 : 0;
2350 setup_pager_stdout
($usepager, $pgdfltoff);
2351 &{$commands{$command}}(@ARGV);