App-karr
view release on metacpan or search on metacpan
lib/App/karr/Cmd/Delete.pm view on Meta::CPAN
my ($id) = @_;
my $task = $self->find_task($id);
# The unguarded pre-read fires before delete_task_guarded below, so this is
# the not-found a caller normally meets; the guard raises the same line only
# on the race where the card vanishes in the window. One spelling for both,
# and for every other command on the mutation path (ticket k264).
die $self->task_not_found($id) unless $task;
# A live claim blocks the delete whoever holds it -- an empty claimant, the
# way kanban-md's cmd/delete.go calls CheckClaim. Neither implementation
# gives delete a --claim option, so releasing the claim (or letting it
# expire) is the way through, for the holder as much as for anybody else.
$self->check_claim($task, undef);
# Before the confirmation, never after it. The point of the warning is that
# it can still change the answer, and the operator who reads it and types
# "n" is the one it worked on -- so it deliberately does not wait for the
# write the way App::karr::Role::DependencyCheck/dependency_report waits for
# its own. That rule ("a warning about a move that then lost its
# compare-and-swap is a warning about something that did not happen") is
# about a card that survives to be read afterwards. Here the write is what
# removes the card, so a warning that waits for it is a warning about
# something nobody can choose to keep any more.
#
# Under --yes there is no prompt to precede and it comes anyway: --yes is
# the mode agents delete in, so a warning only on the interactive path warns
# exactly where nobody is left to read it.
my @dependents = $self->_dependent_warnings($task);
# And what this card says about the boards it cannot see (#242): the far end
# of an escalation is a tag on the card being deleted, so naming it costs
# nothing remote.
my @cross = $self->_cross_board_warnings($task);
# The channel App::karr::Role::DependencyCheck argues for one module over,
# rather than a third convention: the human copy on STDERR so STDOUT stays
# parseable, --quiet silencing that copy, and --json carrying the identical
# sentence in the result object below because a JSON consumer never reads
# STDERR.
print STDERR map { "$_\n" } @dependents, @cross
unless $self->json || $self->quiet;
my @warning_report = (
( @dependents ? ( dependent_warnings => \@dependents ) : () ),
( @cross ? ( cross_board_warnings => \@cross ) : () ),
);
unless ($self->yes) {
# STDERR, and not only under --json (#248). The question used to go to
# STDOUT, which put a bare `Delete task 1: A? [y/N] ` in front of the
# result object: the object was there, but the stream as a whole would
# not decode, and this is the command whose output a caller is most
# likely to read before doing something irreversible.
#
# The channel is unconditional rather than a branch on --json, for three
# reasons. A prompt is not a result -- `deleted` below is the result, and
# the question is dialogue, which is what STDERR is for; the rule
# App::karr::Role::DependencyCheck states one module over ("the human
# copy goes to STDERR so STDOUT stays parseable") is likewise
# unconditional, only its *suppression* depends on an option. Second, the
# non-JSON path has the same defect in a quieter form: `karr delete 1 >
# kept.txt` wrote the question into the file, so the operator at the
# terminal was asked nothing and waited at a blank cursor. And third,
# making the channel depend on a flag means the fix only reaches the
# caller who remembered the flag.
#
# Rejecting `--json` without `--yes` outright was the other candidate. It
# would have deleted a live answer: `deleted => false` with the two
# warning keys beside it is exactly the shape #236 and #242 built for a
# card the operator declined to delete, and under --yes there is no
# prompt to decline at all, so that shape would become unreachable. It
# also refuses `printf 'y\nn\ny\n' | karr delete 1,2,3 --json`, a
# per-card answer that --yes cannot express because --yes is
# all-or-nothing. --json names an output format and --yes a confirmation
# policy; they are orthogonal, not the contradicting pair #235 refuses.
printf STDERR "Delete task %d: %s? [y/N] ", $task->id, $task->title;
# The question has to be out before the read that waits for its answer,
# and this printf alone does not put it there: it ends without a newline,
# so nothing in the buffering flushes it (#241).
#
# Whether that showed depended on where stdin came from, which is why it
# went unnoticed for so long. With stdin on a terminal PerlIO flushes the
# line-buffered handles when it fills its read buffer, so the prompt got
# out by somebody else's courtesy -- a detail of the implementation, not
# a promise. With stdin anywhere else -- a pipe, a file, an agent harness
# feeding answers -- nothing does it, and the question sits in the buffer
# until the next newline or process exit pushes it out, which is after
# karr has already acted on the answer. `karr delete 1 < answers` in a
# terminal printed the question and the outcome together at the end.
#
# The flush moves with the question, and what it is worth changed under
# it: #249 turned autoflush on for STDOUT and STDERR in
# App::karr::Encoding::enable_std_utf8, so on the CLI path the question is
# already on the wire when this line runs, and t/241 passes without it.
# It stays anyway, for the same reason it was written. The promise belongs
# to the question -- it must be readable before the read below blocks --
# and not to whatever the process-wide handle setup happens to be today: a
# caller that reaches this command without bin/karr, an in-process test
# that reopened STDERR onto a capture handle and so dropped both the layer
# and the autoflush with it, or a later revert of #249, all get a buffered
# handle back. ->flush does not care how the handle is buffered, and on an
# unbuffered one it costs a call, so the question arrives before the wait
# whichever way stderr and stdin are connected.
STDERR->flush;
my $answer = <STDIN>;
# <STDIN> returns undef at EOF, and karr used to run straight on into
# `chomp $answer` -- two "Use of uninitialized value $answer" warnings on
# stderr, then "Skipped task 2: d", for every agent or CI run that forgot
# --yes (ticket #73). What the right answer to a non-answer is depends on
# where stdin came from:
#
# a terminal the user pressed Ctrl-D. That is "no": skip the task and
# exit 0, the same as typing n, and now without warnings.
#
# anything else nobody is there and nobody will be, so there is no
# point pretending the prompt happened. Refuse and say what
# to do, the way karr's other destructive commands refuse
# without --yes and the way kanban-md refuses when
# term.IsTerminal is false.
#
# An answer that *is* there is honoured either way, so piping "y" or "n"
# into `karr delete` keeps working.
( run in 1.784 second using v1.01-cache-2.11-cpan-d01c6094234 )