App-karr
view release on metacpan or search on metacpan
lib/App/karr/Git.pm view on Meta::CPAN
# local refs still need pushing, so it may only ever count writes that landed.
#
# A delete that was attempted and refused dies, as every other ref mutation in
# this class does. It used to answer 0 for that too -- the same 0 as "was never
# there" -- and break_lock read that 0 as "gone", so `karr unlock` reported
# "Broke lock on task N" over a lock that was still standing (#119). unlock is
# the escape hatch for a holder that never came back; a false success there
# leaves the card locked for everyone with nobody left to look.
#
# 0 therefore means "the ref is not on this board", never "we could not tell".
# The one seam is an unopenable repository, which is not a refusal to delete
# but the whole class degrading to no-ops -- during global destruction _repo is
# false by design and nothing native may run or throw (#63).
#
# Contention retries on the same terms as write_ref: losing the race for
# refs/<name>.lock is not a failed delete, it is one that has not been
# attempted yet.
sub delete_ref {
my ( $self, $ref ) = @_;
my $repo = $self->_repo or return 0;
return $self->retry_contended( "ref $ref", sub {
# Asking first keeps "nothing to delete" -- a no-op, not a write --
# apart from the failure below, which libgit2 reports the same way.
return 0 unless $repo->reference_exists($ref);
# Before the ref goes, not after: a crash in between leaves a
# tombstone for a ref that is still there, which the push skips,
# rather than a deletion no push will ever publish (#178).
$self->_note_pending_delete($ref);
my $deleted = try {
$repo->reference_delete($ref);
1;
} catch {
my $err = $_;
return 0 if _is_contended_ref_error( $err, 0 );
die _ref_error( 'delete', $ref, $err );
};
return () unless $deleted;
$WRITES++;
return 1;
} );
}
# ----- Remote / network ops: native via Git::Native::Remote -----
# The push refspec. Forced, because write_ref builds every board commit with
# `parents => []`: no board ref update is ever a fast-forward, so a non-forced
# refspec can never apply one. push has always been forced; pull was not, and
# libgit2 declines a non-ff fetch update without raising an error, so pull
# returned success while leaving the ref stale -- and the next push then
# force-wrote that stale ref over the other agent's work (#40). Both
# directions are forced now; see _fetch_refspec for the pull side.
#
# The semantics this settles on are last-writer-wins, which is what the
# parentless-commit design already implied everywhere else. Doing better
# would need compare-and-swap on the ref (git_reference_create_matching,
# unbound in Git::Libgit2 -- see ticket #81) plus per-ref rejection reporting
# from libgit2's update_tips/push_update_reference callbacks (not installed
# by Git::Native -- ticket #80). Neither is reachable from karr today.
use constant BOARD_REFSPEC => '+refs/karr/*:refs/karr/*';
use constant BOARD_ROOT => 'refs/karr/';
# The board's identity (#95): stamped once at board birth, compared on every
# pull before any reconciliation -- see _check_board_identity. Declared with
# the other namespace constants because use constant is only visible from its
# textual point on, and _check_board_identity needs it.
use constant BOARD_ID_REF => 'refs/karr/meta/board-id';
# The id counter, declared up here with the other namespace constants for the
# same reason BOARD_ID_REF is: use constant is only visible from its textual
# point on, and the reconciliation below has to name this ref to keep it from
# being walked backwards (#172). The allocator that owns it lives further down.
use constant NEXT_ID_REF => 'refs/karr/meta/next-id';
# Remote-tracking mirror: refs/karr-remote/<remote>/<X> holds the remote's
# refs/karr/<X> as of the last successful fetch or push from this clone.
#
# It exists because "the remote does not have this ref" is two different
# situations and the ref alone cannot tell them apart: the remote deleted a
# task (prune is right -- #49), or the ref is local work that has not been
# pushed yet (prune destroys it). karr promises exactly the latter after a
# failed push -- "Local refs are intact. Run 'karr sync' to retry." -- so
# pruning on that signal alone broke the promise the sync guard and the END
# flush (#37) exist to keep.
#
# The mirror makes the four cases decidable. See _reconcile_with_mirror.
use constant MIRROR_ROOT => 'refs/karr-remote';
# Where the local side of a genuine conflict is parked before the remote
# version replaces it. Outside refs/karr/, so it never reaches the remote and
# never shows up on the board.
use constant CONFLICT_ROOT => 'refs/karr-conflict';
# Deleted board refs, remembered until a push has told the remote about them.
# refs/karr-local/ is the namespace nothing pushes or fetches, so a tombstone
# is local bookkeeping the way a pick lock is -- and it points at the commit
# the deleted ref pointed at, which keeps that card reachable for a hand
# recovery until the deletion is published.
#
# This is the record a push publishes deletions from, and it exists because
# the two obvious alternatives both destroy cards under concurrency (#178):
# pruning treats every remote ref the pusher does not have as deleted, and a
# mirror-minus-local diff cannot tell a card this clone deleted from one
# another process fetched a moment ago and has not adopted into the board yet.
# A ref that was deliberately deleted here is the only thing that writes one.
use constant TOMBSTONE_ROOT => 'refs/karr-local/deleted';
# ----- The fleet-coordination namespace (#190) -----
#
# karr-foundation's shared half: the chain of planned steps, the run logs, the
# question mailbox to come, and the fleet's own design document
# (App::karr::Foundation::ChainStore, #189). It is coordination state -- every
# machine and every person has to see the same picture -- so it syncs, and it
# syncs through the same mirror the board does, for the same reason: without
# one, "the remote does not have this ref" cannot be told apart from "this
# clone has not pushed it yet", and a namespace whose ref names are minted as
# it runs meets both cases constantly.
( run in 0.854 second using v1.01-cache-2.11-cpan-364913b4093 )