App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Git.pm  view on Meta::CPAN

    }

    # Read before the push, never after -- see L</push>: a ref written in
    # between is published without being in the snapshot, which leaves the
    # mirror lagging the remote, the direction that converges harmlessly.
    my $local      = $self->ref_oids(FOUNDATION_ROOT) || {};
    my $tombstones = $self->_pending_deletes(FOUNDATION_ROOT);
    # A tombstone whose ref is back in the namespace is stale, not a deletion.
    my @deletes = grep { !exists $local->{$_} } sort keys %$tombstones;

    # Nothing here and nothing deleted is nothing to say, and saying it would
    # be an error: a wildcard refspec matching no local ref, sent on its own,
    # makes git exit non-zero ("No refs in common and none specified"), so
    # every repository that is not the hub would end `karr sync` complaining
    # about a namespace it does not have. This is also what keeps the second
    # round trip off the boards that never carry fleet state.
    return 1 unless %$local || @deletes;

    my @refspecs = ( FOUNDATION_REFSPEC, map { ":$_" } @deletes );
    return 0 unless $self->_push_refspecs( $remote, \@refspecs );

    $self->_mirror_local_state( $remote, $local, \@deletes,
        FOUNDATION_ROOT, FOUNDATION_MIRROR_ROOT );
    $self->_clear_pending_deletes( $tombstones, \@deletes );
    return 1;
}


sub _reconcile_foundation {
    my ( $self, $remote, $prefix, $tracked ) = @_;
    return unless $self->_repo;

    my ($plan) = $self->_mirror_plan( FOUNDATION_ROOT, $prefix, $tracked );

    my ( @conflicts, @unapplied, @stale );
    for my $step (@$plan) {
        my ( $ref, $oid, undef, $conflict ) = @$step;
        my $applied = defined $oid
            ? $self->_write_ref_untracked( $ref, $oid )
            : $self->_delete_ref_untracked($ref);
        if ($applied) {
            CORE::push @conflicts, $ref if $conflict;
            next;
        }
        # Same rule as the board's: the mirror goes back to what this ref had
        # before the fetch, and the pull stops (#154).
        CORE::push @unapplied, $ref;
        CORE::push @stale, $self->_rollback_mirror_ref(
            $prefix, $tracked, $ref, FOUNDATION_ROOT );
    }

    $self->_warn_foundation_conflicts( $remote, \@conflicts ) if @conflicts;
    die $self->_unapplied_refs_error( $remote, \@unapplied, \@stale )
        if @unapplied;
    return;
}

# Said, but not parked. The board keeps the losing side under
# refs/karr-conflict/ because a displaced card is something a person reads back
# and re-applies; a displaced chain step is a plan, and the answer to a plan
# that lost a race is another planning round, not an archaeology dig. What is
# worth saying is that it happened at all: two planners writing one chain is
# the condition, and the operator is the only one who can end it.
sub _warn_foundation_conflicts {
    my ( $self, $remote, $refs ) = @_;
    my $names = join ', ', map { substr $_, length FOUNDATION_ROOT } @$refs;
    warn "karr: this clone and the remote '$remote' both changed $names "
       . "under " . FOUNDATION_ROOT . " since the last sync.\n"
       . "The remote version is now in place and the local one is not kept. "
       . "Re-plan rather than re-edit.\n";
    return;
}

# Fallback transport via the system `git` CLI so that ssh-config directives
# libgit2 ignores (ProxyCommand, Host aliases, IdentityFile, insteadOf) are
# honoured. Returns 1 on success, 0 on failure (setting _last_error to the real
# git-CLI stderr). $verb is 'push' or 'fetch'. @$refspecs may be empty
# (fetch => configured refspecs). %opt: prune => bool. Disabled by
# KARR_NO_CLI_FALLBACK=1.
#
# `push` runs with --porcelain so the per-ref outcomes come back parseable.
# The CLI already exits non-zero on a rejection, so the failure was never
# invisible here the way it was natively (#84) -- but the error contract has
# to be the same on both transports, or "which ref did the server refuse, and
# why" would depend on which one happened to run.
sub _cli_transport {
    my ( $self, $verb, $remote, $refspecs, %opt ) = @_;
    return 0 if $ENV{KARR_NO_CLI_FALLBACK};

    my @args = ($verb);
    CORE::push @args, '--porcelain' if $verb eq 'push';
    CORE::push @args, '--prune' if $opt{prune};
    CORE::push @args, $remote, @$refspecs;

    my $native = $self->{_last_error};
    my $run    = $self->_run_git(@args);

    if ( $run->{failure} eq 'start' ) {
        $self->{_last_error} =
            "git CLI fallback unavailable: $run->{err}"
          . ( defined $native ? " (native: $native)" : '' );
        return 0;
    }

    my $detail = $run->{err};
    $detail =~ s/\s+\z//;
    my $suffix = length $detail ? ": $detail" : '';

    if ( $run->{failure} eq 'timeout' ) {
        $self->{_last_error} = "git $verb (CLI fallback) timed out after "
          . "$run->{timeout}s and was killed$suffix";
        return 0;
    }

    # `$? >> 8` is 0 both for "exited cleanly" and for "died from a signal",
    # so a git the OOM killer, a Ctrl-C on the process group or a SIGPIPE took
    # down used to be reported as a successful transport -- the task was
    # announced as created and the remote never saw it (#42). The signal bits
    # have to be read first.
    if ( my $sig = $run->{status} & 127 ) {
        $self->{_last_error} =



( run in 0.588 second using v1.01-cache-2.11-cpan-aadc1410aed )