+sub patch_matches_spec ($$) {
+ my ($parsedname, $spec) = @_;
+ foreach my $k (qw(Email Domain Nick)) {
+ debug("patch_matches_spec mismatch $k"), return 0
+ if defined $spec->{$k} &&
+ $parsedname->{$k} ne $spec->{$k};
+ }
+ debug("patch_matches_spec mismatch DatePrefix"), return 0
+ if defined $spec->{DatePrefix} &&
+ substr($parsedname->{Date}, 0, length $spec->{DatePrefix})
+ ne $spec->{DatePrefix};
+ debug("patch_matches_spec match"), return 1;
+}
+
+#----- reading topbloke metadata -----
+
+sub foreach_patch ($$$$) {
+ my ($spec, $deleted_ok, $want, $body) = @_;
+ # runs $body->($patch, $parsedname, \%props, \%deps, \%pprops, \%included)
+ # $want->[0] 1 2 3
+ # where $deps->{$fullname} etc. are 1 for true or nonexistent for false
+ # and if $want->[$item] is not true, the corresponding item may be undef
+ # and $parsedname is only valid if $spec is not undef
+ # (say $spec { } if you want the name parsed but no restrictions)
+ my @want = @$want;
+ $want[0] ||= !$deleted_ok;
+ run_git(sub {
+ debug("foreach_patch considering $_");
+ m/ / or die "$_ ?";
+ my $objname = $`;
+ my @out;
+ my $patch = substr($',19); #');
+ my $wantix = 0;
+ foreach my $file (qw(props deps pprops included)) {
+
+ if ($file eq 'deps') {
+ # do this check after checking for deleted patches,
+ # so we don't parse deleted patches' names
+ # right, check the spec next
+ if ($spec) {
+ my $have = parse_patch_name($patch);
+ debug("foreach_patch mismatch"), return
+ unless patch_matches_spec($have, $spec);
+ unshift @out, $have;
+ } else {
+ unshift @out, undef;
+ }
+ }
+
+ if (!$want[$wantix++]) {
+ push @out, undef;
+ next;
+ }
+
+ my ($got, $data) = git_get_object("$objname:.topbloke/$file");
+ die "$patch $file ?" unless defined $data;
+ my %data;
+ if ($file !~ m/props/) {
+ $data{$_}=1 foreach split /\n/, $data;
+ } elseif {
+ foreach (split /\n/, $data) {
+ m/ / or m/$/;
+ $data{$`} = $'; #';
+ }
+ }
+
+ if ($file eq 'props') {
+ debug("foreach_patch Deleted"), return
+ if !$deleted_ok && $data{Deleted};
+ }
+
+ push @out, \%data;
+ }
+ debug("foreach_patch YES ".(join '', map { 0+defined } @out)), return
+ $body->($patch, @out);
+ },
+ qw(for-each-ref --format), '%(objectname) %(refname)',
+ qw(refs/topbloke-tips));
+}
+
+#----- updating topbloke metadata -----
+
+sub propsfile_set_prop ($$$) {
+ # set $value to undef to delete; returns old value
+ my ($propsfile, $prop, $value) = @_;
+ my $wf = wf_start(".topbloke/$propsfile");
+ my $oldvalue;
+ open FI, '<', ".topbloke/$propsfile" or die $!;
+ while (<FI>) {
+ chomp or die;
+ m/ / or m/$/;
+ if ($` eq $prop) {
+ die "prop $prop repeated in $propsfile ?!" if defined $oldvalue;
+ $oldvalue = $'; #';
+ } else {
+ wf($wf, "$_\n");
+ }
+ }
+ FI->error and die $!;
+ close FI or die $!;
+ wf($wf, "$prop $value\n") if defined $value;
+ wf_done($wf);
+ return $oldvalue;
+}
+
+sub depssfile_add_dep ($$) {
+ my ($depsfile, $depspec) = @_;
+ my $wf = wf_start(".topbloke/$depsfile");
+ open FI, '<', ".topbloke/$depsfile" or die $!;
+ while (<FI>) {
+ chomp or die;
+ die "dep $depspec already set in $depsfile ?!" if $_ eq $depspec;
+ wf($wf, "$_\n");
+ }
+ FI->error and die $!;
+ close FI or die $!;
+ wf($wf, "$depspec\n");
+ wf_done($wf);
+}
+
+#----- general utilities -----
+
+sub wf_start ($) {
+ my ($path) = @_;
+ my $fh = new IO::File "$path.tmp", '>' or die "create $path.tmp: $!\n";
+ return [ $fh, $path ];
+}
+
+sub wf ($$) {
+ my ($wf, $data) = @_;
+ my ($fh, $path) = @$wf;
+ print $fh $data or die "write $path.tmp: $!\n";
+}
+
+sub wf_abort ($) {
+ my ($wf) = @_;
+ my ($fh, $path) = @$wf;
+ close $fh;
+ unlink "$path.tmp" or die "remove $path.tmp: $!\n";
+}
+
+sub wf_done ($) {
+ my ($wf) = @_;
+ my ($fh, $path) = @$wf;
+ close $fh or die "finish writing $path.tmp: $!\n";
+ rename "$path.tmp", $path or die "install new $path: $!\n";
+}
+
+sub wf_contents ($$) {
+ my ($path,$contents) = @_;
+ my $wf = wf_start($path);
+ wf($wf, $contents);
+ wf_done($wf);
+}
+
+sub closeout () {
+ STDOUT->error and die $!;
+ close STDOUT or die $!;
+}
+