App-karr
view release on metacpan or search on metacpan
lib/App/karr/Git.pm view on Meta::CPAN
return \%oids;
}
sub read_config_ref {
my ($self) = @_;
my $content = $self->read_ref('refs/karr/config');
return {} unless $content;
return $self->maybe_repair_legacy( yaml_load($content) );
}
sub write_config_ref {
my ( $self, $data ) = @_;
return $self->write_ref( 'refs/karr/config', yaml_dump($data) );
}
sub _parse_next_id {
my ($raw) = @_;
$raw = '' unless defined $raw;
$raw =~ s/\s+\z//;
return $raw =~ /^\d+$/ ? int($raw) : 1;
}
sub read_next_id_ref {
my ($self) = @_;
return _parse_next_id( $self->read_ref(NEXT_ID_REF) );
}
sub write_next_id_ref {
my ( $self, $next_id ) = @_;
return $self->write_ref( NEXT_ID_REF, "$next_id\n" );
}
# Hand out one id and move the counter past it in a single guarded step.
#
# The old read-then-write lost tasks outright: two agents that read the same
# counter both got that id, wrote the same refs/karr/tasks/N/data, and the
# loser's task was destroyed with both processes reporting success -- 40
# parallel creates produced 32 tasks (#44). The counter has to be re-read
# inside the loop, not once outside it: retrying with the value that already
# lost would just lose again.
sub allocate_next_id_ref {
my ($self) = @_;
return $self->retry_contended( 'the next-id counter', sub {
my ( $oid, $raw ) = $self->read_ref_with_oid(NEXT_ID_REF);
my $id = _parse_next_id($raw);
return () unless $self->write_ref_cas( NEXT_ID_REF, ($id + 1) . "\n", $oid );
return $id;
} );
}
# ----- Whole-board replacement (restore) -----
# The mirror image of validate_helper_ref, which keeps helper refs out of the
# board namespace: a snapshot may only address refs inside it. Without this a
# hand-edited backup could point refs/heads/main at a parentless karr commit,
# because restore fed whatever keys the YAML carried straight to
# reference_create.
sub validate_board_ref {
my ( $self, $ref ) = @_;
defined $ref && length $ref
or die "karr: snapshot contains a ref with no name\n";
die "karr: '$ref' is outside the board namespace " . BOARD_ROOT . "\n"
unless index( $ref, BOARD_ROOT ) == 0;
die "karr: '$ref' is not a valid git ref name\n"
unless Git::Native->reference_name_is_valid($ref);
return $ref;
}
# Make the board consist of exactly the refs in %$refs (name => content).
#
# `karr restore` used to delete refs/karr/* first and write the snapshot back
# one ref at a time, so anything that failed on the way took the board with it.
# A single unusable ref name in the snapshot -- one that sorts before
# refs/karr/config is enough -- left the board empty locally and on the remote,
# because the END-block push insurance faithfully mirrored the half-executed
# destruction, prune and all (#47). The tool people reach for when they are
# already in trouble is the one that must not be able to make it worse.
#
# Nothing destructive happens here until the whole restore is known to be
# writable. Phase one validates every name and builds every commit object;
# neither touches a ref, so a failure leaves the board exactly as it was.
# Phase two then overwrites in place instead of starting from an empty
# namespace, so the board is never empty in between.
#
# Phase two is also atomic across its own writes: every ref's pre-restore
# content is snapshotted before the first write, and any die out of
# _write_ref_oid unwinds the writes that already landed before raising --
# otherwise a write failure on the second of eight refs would leave a
# board with the snapshot's config and the live board's tasks, which is
# the half-apply disaster recovery is supposed to prevent (#155).
sub replace_board_refs {
my ( $self, $refs ) = @_;
my $repo = $self->_repo
or die "karr: no usable git repository: "
. ( $self->last_error // 'unknown error' ) . "\n";
my @wanted = sort keys %$refs;
for my $ref (@wanted) {
$self->validate_board_ref($ref);
die "karr: snapshot value for '$ref' is not text\n" if ref $refs->{$ref};
}
my %commit;
for my $ref (@wanted) {
$commit{$ref} =
$self->_commit_for_content( $repo, $refs->{$ref} // '' );
}
# Snapshot the current OID+content of every ref the restore is about to
# touch, so a phase-two failure can put each one back where it was.
# Anything that existed before the restore but is not in @wanted is also
# captured, because the cleanup loop below would have deleted it on
# success and the unwind has to undo that too.
my %pre_exist = map { $_ => 1 } $self->list_refs(BOARD_ROOT);
lib/App/karr/Git.pm view on Meta::CPAN
mapped to its current OID as a hex string. Refs that can't be resolved are
silently omitted rather than included with an undef value. Returns C<undef>
-- not an empty hashref -- when the repository can't be opened; callers
throughout this class guard with C<< $git->ref_oids(...) || {} >>.
=head2 read_config_ref
my $config = $git->read_config_ref; # hashref
Returns the board config as a hashref, parsed from C<refs/karr/config>
(YAML) and repaired if the board is legacy-encoded. Returns C<{}> -- not
C<undef> -- when the ref is absent or empty.
=head2 write_config_ref
$git->write_config_ref($config);
Serializes C<$config> to YAML and writes it to C<refs/karr/config> via
L</write_ref>.
=head2 read_next_id_ref
my $next = $git->read_next_id_ref;
Returns the next task id to be handed out, as an integer. Returns C<1> when
the ref is absent or unparseable. This is a plain, unguarded read -- see
L</allocate_next_id_ref> for the version that actually reserves an id.
=head2 write_next_id_ref
$git->write_next_id_ref($next_id);
Unconditionally writes the next-id counter via L</write_ref>. Not
compare-and-swapped -- a direct caller races with L</allocate_next_id_ref>;
this is for whole-board writers (C<karr import>, C<repair>) restamping the
counter outright, not for handing out an id.
=head2 allocate_next_id_ref
my $id = $git->allocate_next_id_ref;
Hands out one task id and advances the counter past it, atomically: the read
and the compare-and-swapped write happen inside one L</retry_contended> loop,
so two callers racing for the same id can never both receive it and silently
overwrite each other's task (#44). Returns the allocated id.
That makes this the sole authority for handing out an id, but only for as long
as nothing else moves the counter: it was still possible for two creates to
receive the same id when a pull walked the counter backwards between them
(#172), which is why L</pull> merges that ref forward instead of adopting the
remote's value.
=head2 validate_board_ref
my $ref = $git->validate_board_ref($ref);
The mirror image of L</validate_helper_ref>: dies unless C<$ref> is
non-empty, inside the board namespace C<refs/karr/>, and a syntactically
valid git ref name. Returns C<$ref> unchanged on success.
L</replace_board_refs> (C<karr restore>) validates every ref in a snapshot
through this before writing anything, so a hand-edited backup can't point a
ref like C<refs/heads/main> at a board commit.
=head2 replace_board_refs
$git->replace_board_refs( \%refs ); # { $ref => $content, ... }
Makes the board consist of exactly the given refs: C<karr restore>'s
primitive. Every ref name is validated (L</validate_board_ref>) and every
commit object built I<before> any ref is touched, so a single bad name or
non-text value in C<%refs> dies without leaving the board half-overwritten.
The given refs are then written in place -- never through a
delete-everything-then-rewrite step, so the board is never briefly empty --
and any existing board ref not present in C<%refs> is deleted afterwards,
best-effort: a ref that resists deletion is left in place with a warning
rather than failing the whole restore. Always returns C<1> once the given
refs are in place, even when some stray ref could not be removed. Resets the
cached L</board_encoding_version>, since a restored snapshot may carry a
different one than the board had.
=head2 delete_refs
$git->delete_refs($prefix);
Deletes every ref currently under C<$prefix> (via L</delete_ref>, so each one
is itself retried against lock contention). Every ref is attempted even when
an earlier one refuses. Re-reads the prefix afterwards rather than trusting
the deletes to have all landed, and dies if anything is still there -- naming
each refusal and its reason, or naming the leftover refs when nothing raised
one. This is what C<karr destroy> uses, and a partial destroy reported as a
success would be worse than one that fails loudly. A ref that another process
removed in the meantime is not a failure: gone is gone.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/karr/issues>.
=head2 IRC
Join C<#langertha> on C<irc.perl.org> or message Getty directly.
=head1 CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
=head1 AUTHOR
Torsten Raudssus <getty@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
=cut
( run in 0.558 second using v1.01-cache-2.11-cpan-4ef0a570458 )