App-karr
view release on metacpan or search on metacpan
lib/App/karr/Git.pm view on Meta::CPAN
=head2 normalize_ref_name
my $full = $git->normalize_ref_name('karr/foo'); # "refs/karr/foo"
my $full = $git->normalize_ref_name('refs/karr/foo'); # unchanged
Strips any leading C</> and prefixes C<refs/> unless the name already starts
with it. Dies with C<"Ref name is required\n"> when C<$ref> is C<undef>. Does
not otherwise validate the name -- see L</validate_helper_ref> and
L</validate_board_ref> for that.
=head2 validate_helper_ref
my $full_ref = $git->validate_helper_ref($ref);
Normalizes C<$ref> (L</normalize_ref_name>) and dies unless it is both a
syntactically valid git ref name and outside every namespace karr itself owns
or protects: C<refs/heads/>, C<refs/tags/>, C<refs/remotes/>, C<refs/bisect/>,
C<refs/replace/>, C<refs/stash>, C<refs/karr/> (the board) and
C<refs/karr-local/> (pick locks, deliberately kept out of reach of any
refspec -- #93). Returns the normalized ref on success. This is the gate
C<karr set-refs>/C<get-refs> go through via L</push_ref>/L</pull_ref>, so a
caller cannot point a helper ref at the board or at a branch.
=head2 retry_contended
my @result = $git->retry_contended( $what, sub {
my ($try) = @_;
...
return (); # lost the race -- read again and retry
return $answer; # committed -- stop retrying
} );
Runs C<$attempt> (called with the 1-based attempt number) up to 32 times, with
randomised backoff in between, until it returns something other than the empty
list. C<$attempt> returning C<()> means "another writer got there first, read
again and retry"; any other return value is the final answer and comes back to
the caller untouched (as a list, in list context). An exception from
C<$attempt> propagates immediately without retrying -- only contention is
retried, not a real failure. C<$what> names the thing being updated, for the
message if every attempt is exhausted: this then dies with C<"karr: gave up
updating $what after 32 attempts -- too many agents are writing the board at
once. Try again.\n">.
Every compare-and-swap operation in this class -- L</write_ref_cas>,
L</delete_ref_cas>, L</allocate_next_id_ref> -- runs its attempt through here,
which is also where contention is told apart from real failure: a lost race
can surface natively as libgit2's C<GIT_EMODIFIED> (the ref moved),
C<GIT_ENOTFOUND> (it was deleted) or C<GIT_ELOCKED> (another process
currently holds its lock file). C<GIT_ELOCKED> is the one that decides whether
this actually works under real concurrency -- it is the common outcome once
more than one process is writing, and a retry loop that only recognised
C<GIT_EMODIFIED>-style mismatches still lost most writes (16 contenders on one
counter left 4 processes dead and 4 increments missing; #85).
=head2 write_ref
$git->write_ref( $ref, $content );
Force-writes C<$ref> to a new parentless commit wrapping C<$content> (a
character string -- see L<App::karr::Encoding> for the octet boundary),
last-writer-wins. Retries transparently through L</retry_contended> when
another process holds the ref's lock, so an ordinary transient collision is
invisible to the caller; it surfaces only as the "gave up after 32 attempts"
exception when contention never clears, or as a C<karr: could not write ...>
exception for anything else. Returns a true value on success, C<undef> when
the repository can't be opened. Every non-CAS ref write in this class goes
through here -- L</save_task_ref>, L</write_config_ref>, L</write_next_id_ref>,
L</write_board_id_ref>, L</write_encoding_version> -- so it is not safe
against another writer's own write landing between two calls; use
L</write_ref_cas> when that matters.
=head2 write_ref_cas
my $ok = $git->write_ref_cas( $ref, $content, $expected_old );
The compare-and-swap sibling of L</write_ref>: the write only lands if
C<$ref> still points at C<$expected_old> (a hex OID), where C<undef> means
"the ref must not exist at all". Returns C<1> when the write landed. Returns
C<0> -- not an exception -- when someone else won the race: the ref had
already moved, had already been deleted, or another process currently holds
its lock file (libgit2's C<GIT_ELOCKED>, the common case under real
contention, distinct from and handled alongside the stale-OID
C<GIT_EMODIFIED>/C<GIT_ENOTFOUND> case -- #85). A caller getting C<0> from a
single call is expected to be inside L</retry_contended>, re-read whatever it
just decided the new expected state is, and try again. A genuine failure
C<die>s with a C<karr: could not write ...> message rather than returning
C<0>. Unlike L</write_ref>, a failed write here never increments
L</pending_writes>.
=head2 delete_ref_cas
my $ok = $git->delete_ref_cas( $ref, $expected_old );
The compare-and-swap sibling of L</delete_ref>: the ref is removed only if it
still points at C<$expected_old> (a hex OID; required -- dies with C<"karr:
could not delete ...: no expected revision given\n"> when omitted).
Internally this combines an explicit OID comparison (covering the window
between the caller's read and the lookup here) with libgit2's own
C<GIT_EMODIFIED> check on the actual removal (covering the window between
that lookup and the delete) -- together they make this a real
compare-and-swap, which the unguarded C<git_reference_remove> that
L</delete_ref> uses cannot be (#94). Returns C<1> when the delete landed,
C<0> when the ref had already moved or gone, or when another process
currently holds its lock (C<GIT_ELOCKED> -- same contention handling as
L</write_ref_cas>, #85). A caller getting C<0> is expected to be inside
L</retry_contended> and retry. A genuine failure C<die>s with a C<karr:
could not delete ...> message, as L</delete_ref> does too since #119. What
still separates the two is the guard, not the error handling: C<0> here means
"the ref moved or went first", while C<0> from L</delete_ref> means "there
was nothing to remove".
=head2 read_ref_with_oid
my ( $oid, $content ) = $git->read_ref_with_oid($ref);
Reads C<$ref> and returns both its current OID (hex string, or C<undef> when
the ref doesn't exist or the repository can't be opened) and the
character-string content of the commit it points at (chomped of one trailing
newline, matching the old C<git cat-file> transport; empty string when there
is nothing to read). Always returns both from the same read -- a
compare-and-swap caller that fetched the OID and the content separately would
be guarding against the wrong revision if the ref moved in between.
L</load_task_ref_with_oid> is the task-shaped version of this, and answers a
missing task with C<(undef, undef)> rather than C<(undef, '')>: its second
slot holds an L<App::karr::Task>, and there is no empty task the way there is
an empty string. The half worth testing is the same in both -- absence is
C<undef> in the first slot, which is where every caller in this distribution
reads it from.
=head2 read_ref
my $content = $git->read_ref($ref);
The content half of L</read_ref_with_oid>, for callers that don't need the
OID. Returns the empty string when the ref doesn't exist, never C<undef>.
=head2 ref_exists
if ( $git->ref_exists($ref) ) { ... }
Returns C<1> when C<$ref> exists, C<0> otherwise -- including when the
repository can't be opened.
=head2 delete_ref
my $removed = $git->delete_ref($ref);
Deletes C<$ref>. Retries transparently through L</retry_contended> while
another process holds the ref's lock. Returns C<1> when this exact call is
the one that removed it and C<0> when there was nothing to remove -- the ref
was not there, or the repository can't be opened at all (which includes the
global-destruction refusal every native operation in this class degrades to,
#63). A delete that was attempted and refused C<die>s with a C<karr: could
not delete ...> message, the same way L</delete_ref_cas> and L</write_ref>
report a real failure: C<0> means "not on the board", never "we could not
tell". It used to fold that failure into the same C<0>, and
L<App::karr::Lock/break_lock> read it as "already gone", so C<karr unlock>
announced a broken lock that was still held (#119). Unlike
L</delete_ref_cas>, the delete itself is unguarded -- whatever is at C<$ref>
goes, last-writer-wins.
=head2 has_remote
if ( $git->has_remote('origin') ) { ... }
Returns true when C<$remote> (default C<origin>) is configured, false
otherwise -- including when the repository can't be opened.
=head2 fetch
my $ok = $git->fetch($remote); # default 'origin'
Runs a plain C<git fetch> using the remote's configured refspecs -- unlike
L</pull>, this does not go through the C<refs/karr-remote/> mirror or touch
the board at all. Returns C<1> when C<$remote> isn't configured (a no-op) or
the fetch succeeds, C<0> on failure with L</last_error> set. Tries the native
libgit2 transport first and falls back to the system C<git> CLI on failure
(see L</DESCRIPTION>).
=head2 push_rejections
my $rejected = $git->push_rejections;
# [ { ref => 'refs/karr/tasks/12/data', reason => 'stale info' }, ... ]
Returns the per-ref rejections from the most recent C<push> or C<push_ref>,
as an array reference of C<< { ref => $name, reason => $text } >> hashes.
Empty when the last push succeeded, and empty when it failed as a whole --
no connection, a killed transport -- rather than ref by ref: a rejection is
the server's final answer, not a transport failure, and the two are kept
apart. Reset to empty at the start of every push attempt, so a rejection from
an earlier call never lingers into the read after a later one succeeds.
libgit2's C<git_remote_push> returns success even when the far side refused
every single ref -- a pre-receive hook, a protected ref, a non-fast-forward on
a non-forced refspec. The per-ref outcome only exists in the
L<Git::Native::Remote::Result> C<push> hands back, and karr used to throw
that away, so a push that landed nothing was reported as a completed sync and
the board diverged in silence (ticket #84). This is where that outcome
survives the call; the CLI fallback parses C<--porcelain> output into the
same shape, so both transports answer the same way.
L<App::karr::Role::SyncLifecycle> and L<App::karr::SyncGuard> both check this
after a failed push and stop retrying once it is non-empty: the remote was
reached and gave its answer, so further attempts would only collect the same
refusal again.
=head2 push
( run in 2.466 seconds using v1.01-cache-2.11-cpan-354807fb38d )