App-karr
view release on metacpan or search on metacpan
lib/App/karr/Task.pm view on Meta::CPAN
# would be exactly the silent frontmatter deletion of ticket #69. Only an
# explicit unblock throws the reason away.
sub _set_blocked_flag {
my ( $self, $value ) = @_;
return $value ? $self->blocked(!!1) : $self->clear_blocked;
}
sub block {
my ( $self, $reason ) = @_;
$self->blocked(!!1);
if ( defined $reason && length $reason ) {
$self->block_reason($reason);
} else {
$self->clear_block_reason;
}
return $self;
}
sub unblock {
my ($self) = @_;
$self->clear_blocked;
$self->clear_block_reason;
return $self;
}
# kanban-md's AppendBody (internal/board/mutate.go:571): the trailing newlines
# come off the existing body and the two halves are joined with a blank line.
# karr joined with a single "\n" until ticket #238, and Markdown folds a single
# newline into a space -- so every note appended by `karr edit -a` or
# `karr handoff --note` was rendered into the paragraph above it, and a card
# that collected notes over a session ended up as one block.
#
# Emptiness is measured *after* the trim, which is where this parts company with
# the reference: Go tests `existing != ""` first, so a body that is nothing but
# newlines still gets the separator and comes back with a leading blank line.
# That body cannot arrive from the board -- _parse_content strips trailing
# newlines, the normal form ticket #78 settled -- but it can arrive from a
# caller in process, and "an empty body gains no separator" is the whole point
# of the branch. Only newlines are trimmed, never other whitespace: an indented
# last line is content.
#
# length, not truth, on the trimmed body: appending to a body of "0" must
# preserve it (ticket #78).
sub append_body {
my ( $self, $text, $timestamp ) = @_;
# The handoff form, deliberately not kanban-md's: karr writes every stamp in
# UTC (App::karr::Role::ClaimTimeout says why), and `[[2006-01-02]]` is
# wiki-link syntax that means nothing here. Opt-in at both call sites -- an
# inline prefix would turn `-a "## Findings"` into a line that is no longer a
# heading, so it cannot be forced on every append.
$text = gmtime->strftime('%Y-%m-%d %H:%M') . ' ' . $text if $timestamp;
my $body = defined $self->body ? $self->body : '';
$body =~ s/\n+\z//;
$self->body( length $body ? $body . "\n\n" . $text : $text );
return $self;
}
sub update_timestamps {
my ( $self, $old_status, $new_status, $first_status, $config ) = @_;
my $now = gmtime->datetime . 'Z';
# Called on the class, is_terminal_status answers for the default board --
# the literal `done` and `archived`. That is all this method could ever ask
# before the last #67 leftover fell, so a board whose final column is named
# anything else recorded no completion at all: `karr move 1 shipped` stamped
# `started` and left `completed` unset for ever, and every reader built on
# it (metrics, context's recently-completed) saw an empty set. Hand the
# board's own App::karr::Config in and its statuses decide instead.
$config //= 'App::karr::Config';
# First move out of the board's first status starts the clock, and never
# restarts it.
if ( !$self->has_started
&& defined $first_status
&& defined $old_status
&& $old_status eq $first_status
&& $new_status ne $first_status )
{
$self->started($now);
}
if ( $config->is_terminal_status($new_status) ) {
$self->completed($now) unless $self->has_completed;
# A task dragged straight to done never passed through in-progress, so it
# has no start; without this its cycle time would be unmeasurable.
$self->started($now) unless $self->has_started;
} elsif ( defined $old_status
&& $config->is_terminal_status($old_status) )
{
# Reopening. `started` is deliberately kept: the work did begin then.
$self->clear_completed;
}
return $self;
}
sub slug {
my ($self) = @_;
my $slug = lc($self->title);
$slug =~ s/[^a-z0-9]+/-/g;
$slug =~ s/^-|-$//g;
return $slug if length($slug) <= MAX_SLUG_LENGTH;
# Truncate on a word boundary the way kanban-md's GenerateSlug does
# (internal/task/slug.go): cutting mid-word backs up to the last dash, and a
# cut that already landed on one keeps the whole final word. A hard cut at 50
# gave the same task two different filenames in a shared tasks/ directory.
my $truncated = substr( $slug, 0, MAX_SLUG_LENGTH );
if ( substr( $slug, MAX_SLUG_LENGTH, 1 ) ne '-' ) {
my $idx = rindex( $truncated, '-' );
$truncated = substr( $truncated, 0, $idx ) if $idx > 0;
}
$truncated =~ s/-+\z//;
return $truncated;
}
( run in 2.412 seconds using v1.01-cache-2.11-cpan-364913b4093 )