Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,11 @@ $ lc issue move --from Retired --to Active --dry-run <4>
<4> Preview without mutating

NOTE: In JSON mode, ID and bulk moves require `--yes` or `--dry-run`. Project
names must match exactly. A name-based bulk move must provide `--team` or an
active profile; UUID-only moves do not need either. Partial matches and other
names must match exactly. A bare-number ID must resolve its team from an
active profile, one favorite team, or the only available team. If team
selection is ambiguous, the command writes a diagnostic to stderr and exits
before mutation. A name-based bulk move must provide `--team` or an active
profile; UUID-only moves do not need either. Partial matches and other
prompt-required inputs return a diagnostic on stderr. A successful dry-run
writes the selected issues as one JSON value.

Expand Down
60 changes: 40 additions & 20 deletions app/lib/linear_cli/cli/commands/issues/development.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule LinearCli.CLI.Commands.Issues.Development do
"""

alias LinearCli.CLI.{Display, Prompt}
alias LinearCli.CLI.Issue.{Assignment, PullRequest}
alias LinearCli.CLI.Issue.{Assignment, Identifiers, PullRequest}
alias LinearCli.Git

@doc """
Expand All @@ -25,7 +25,9 @@ defmodule LinearCli.CLI.Commands.Issues.Development do
"""
@spec issue_develop(Optimus.ParseResult.t(), keyword()) :: :ok | {:error, term()}
def issue_develop(result, opts \\ [])
def issue_develop(%{args: %{issue_id: issue_id}}, opts), do: run_develop(issue_id, opts)

def issue_develop(%{args: %{issue_id: issue_id}} = result, opts),
do: run_develop(issue_id, maybe_put(opts, :output, result_output(result)))

@doc """
Ported from commands/issue/pr.rb: resolves/self-assigns `issue_id`, checks
Expand All @@ -42,6 +44,8 @@ defmodule LinearCli.CLI.Commands.Issues.Development do
def issue_pr(result, opts \\ [])

def issue_pr(%{args: %{issue_id: issue_id}, options: options}, opts) do
opts = maybe_put(opts, :output, Map.get(options, :output, "text"))

with {:ok, issue} <- Assignment.gimme_da_issue!(issue_id, opts),
{:ok, _branch} <- Git.checkout_branch(issue.branch_name, opts) do
Prompt.ok("Checked out branch #{issue.branch_name}")
Expand Down Expand Up @@ -72,10 +76,13 @@ defmodule LinearCli.CLI.Commands.Issues.Development do
def issue_take(result, opts \\ [])

def issue_take(%{unknown: issue_ids, options: options}, opts) do
opts = maybe_put_status(opts, Map.get(options, :status))
opts =
opts
|> maybe_put_status(Map.get(options, :status))
|> maybe_put(:output, Map.get(options, :output, "text"))

with {:ok, updates} <- take_issues(issue_ids, opts) do
Display.show(updates, %{output: options.output})
Display.show(updates, %{output: Map.get(options, :output, "text")})
:ok
end
end
Expand Down Expand Up @@ -112,27 +119,40 @@ defmodule LinearCli.CLI.Commands.Issues.Development do
defp maybe_put(list, _key, nil), do: list
defp maybe_put(list, key, value), do: Keyword.put(list, key, value)

defp result_output(%{options: options}), do: Map.get(options, :output, "text")
defp result_output(_result), do: "text"

defp maybe_put_status(opts, nil), do: opts
defp maybe_put_status(opts, status), do: Keyword.put(opts, :status, status)

defp take_issues(issue_ids, opts) do
issue_ids
|> Enum.reduce_while({:ok, []}, fn issue_id, {:ok, acc} ->
case Assignment.gimme_da_issue!(issue_id, opts) do
{:ok, issue} ->
{:cont, {:ok, [issue | acc]}}

{:error, %Ash.Error.Unknown{errors: [%{value: [{:not_found, id}]} | _]}} ->
Prompt.warn("No issue found with id #{id}")
{:cont, {:ok, acc}}

{:error, reason} ->
{:halt, {:error, reason}}
with {:ok, resolved_ids} <- preflight_take_ids(issue_ids, opts) do
resolved_ids
|> Enum.reduce_while({:ok, []}, fn issue_id, {:ok, acc} ->
case Assignment.gimme_da_issue!(issue_id, opts) do
{:ok, issue} ->
{:cont, {:ok, [issue | acc]}}

{:error, %Ash.Error.Unknown{errors: [%{value: [{:not_found, id}]} | _]}} ->
Prompt.warn("No issue found with id #{id}")
{:cont, {:ok, acc}}

{:error, reason} ->
{:halt, {:error, reason}}
end
end)
|> case do
{:ok, acc} -> {:ok, Enum.reverse(acc)}
error -> error
end
end)
|> case do
{:ok, acc} -> {:ok, Enum.reverse(acc)}
error -> error
end
end

defp preflight_take_ids(issue_ids, opts) do
if Keyword.get(opts, :output, "text") == "json" do
Identifiers.expand_issue_ids(issue_ids, output: "json")
else
{:ok, issue_ids}
end
end
end
3 changes: 2 additions & 1 deletion app/lib/linear_cli/cli/commands/issues/move.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ defmodule LinearCli.CLI.Commands.Issues.Move do

defp move_issues_by_id(issue_ids, options, flags) do
with :ok <- validate_issue_ids(issue_ids),
{:ok, expanded_ids} <- Identifiers.expand_issue_ids(issue_ids, output: options.output),
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}),
Linear.issues(%{ids: expanded_ids}),
{:ok, project} <- resolve_move_project(issues, options) do
print_move_plan(issues, project, options.output)
execute_moves_if_confirmed(issues, project, flags, options.output)
Expand Down
36 changes: 24 additions & 12 deletions app/lib/linear_cli/cli/commands/issues/mutations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
"""
@spec issue_update(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_update(%{unknown: issue_ids, options: options, flags: flags}) do
output = Map.get(options, :output, "text")

with :ok <- validate_issue_ids(issue_ids),
:ok <- validate_body_file_exclusion(options, :description, "--description"),
{:ok, description} <- resolve_body_from_file(options, :description),
{:ok, priority} <- parse_priority(Map.get(options, :priority)),
{:ok, expanded_ids} <- Identifiers.expand_issue_ids(issue_ids, output: output),
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}) do
Linear.issues(%{ids: expanded_ids}) do
update_opts = [
comment: options.comment,
description: description,
Expand Down Expand Up @@ -82,20 +85,23 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
"""
@spec issue_comment(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_comment(%{unknown: issue_ids, options: options}) do
output = Map.get(options, :output, "text")

with :ok <- validate_issue_ids(issue_ids),
:ok <- validate_body_file_exclusion(options, :comment, "--comment"),
{:ok, comment_text} <- resolve_body_from_file(options, :comment),
{:ok, expanded_ids} <- Identifiers.expand_issue_ids(issue_ids, output: output),
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}),
Linear.issues(%{ids: expanded_ids}),
body = WhatFor.comment_for(hd(issues), comment_text),
{:ok, pairs} <- add_comments_to_issues(issues, body) do
unless options.output == "json" do
unless output == "json" do
Enum.each(pairs, fn {issue, _comment} ->
Prompt.ok("Comment added to #{issue.identifier}")
end)
end

Display.show(one_or_many(Enum.map(pairs, &elem(&1, 1))), %{output: options.output})
Display.show(one_or_many(Enum.map(pairs, &elem(&1, 1))), %{output: output})
:ok
end
end
Expand All @@ -113,12 +119,15 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
"""
@spec issue_status(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_status(%{unknown: issue_ids, options: options}) do
output = Map.get(options, :output, "text")

with :ok <- validate_issue_ids(issue_ids),
{:ok, expanded_ids} <- Identifiers.expand_issue_ids(issue_ids, output: output),
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}),
Linear.issues(%{ids: expanded_ids}),
{:ok, planned_updates} <- plan_status_updates(issues, options.status),
{:ok, completed_updates} <- apply_status_updates(planned_updates, options.comment) do
show_status_updates(completed_updates, options.output)
show_status_updates(completed_updates, output)
end
end

Expand Down Expand Up @@ -150,17 +159,18 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
"""
@spec issue_assign(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_assign(%{args: %{issue_id: issue_id}, options: options}) do
expanded_id = Identifiers.expand_issue_id(issue_id)
output = Map.get(options, :output, "text")

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
with {:ok, expanded_id} <- Identifiers.expand_issue_id(issue_id, output: output),
{:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
{:ok, members} <- Linear.team_members(issue.team.id),
:ok <- guard_has_members(members, issue),
{:ok, target_member} <- resolve_target_member(members, options.assignee),
{:ok, state_id} <- resolve_optional_status(issue, Map.get(options, :status)),
{:ok, updated} <- Linear.assign_issue(issue, target_member.id, %{state_id: state_id}) do
Display.show(updated, %{output: options.output})
Display.show(updated, %{output: output})

if options.output != "json" do
if output != "json" do
msg = "#{updated.identifier} assigned to #{target_member.name}"

msg =
Expand All @@ -176,8 +186,10 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
end

defp issue_unassign_by_ids(issue_ids, options) do
with {:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}),
output = Map.get(options, :output, "text")

with {:ok, expanded_ids} <- Identifiers.expand_issue_ids(issue_ids, output: output),
{:ok, issues} <- Linear.issues(%{ids: expanded_ids}),
{:ok, updated_issues} <- unassign_issues(issues) do
show_unassign_results(updated_issues, options)
end
Expand Down
14 changes: 6 additions & 8 deletions app/lib/linear_cli/cli/commands/issues/read.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ defmodule LinearCli.CLI.Commands.Issues.Read do
`--team`/`--project` passed explicitly always win over the active profile.
"""
def issue_list(%{flags: flags, options: options, unknown: ids}) do
with {:ok, input} <-
Filter.build_input(flags, options, Enum.map(ids, &Identifiers.expand_issue_id/1)) do
with {:ok, expanded_ids} <- Identifiers.expand_issue_ids(ids, output: options.output),
{:ok, input} <- Filter.build_input(flags, options, expanded_ids) do
with {:ok, issues} <- Linear.issues(input) do
Display.show(issues, %{
output: options.output,
Expand Down Expand Up @@ -61,18 +61,16 @@ defmodule LinearCli.CLI.Commands.Issues.Read do
{:error, {:smells_bad, "--graph and --web cannot be used together"}}

graph? ->
expanded_id = Identifiers.expand_issue_id(issue_id)

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
with {:ok, expanded_id} <- Identifiers.expand_issue_id(issue_id, output: options.output),
{:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
{:ok, graph} <- Graph.build(issue.identifier, issue) do
Display.show_graph(graph, %{output: options.output})
:ok
end

true ->
expanded_id = Identifiers.expand_issue_id(issue_id)

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}) do
with {:ok, expanded_id} <- Identifiers.expand_issue_id(issue_id, output: options.output),
{:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}) do
if web? do
Browser.open_url(issue.url, opts)
else
Expand Down
88 changes: 61 additions & 27 deletions app/lib/linear_cli/cli/commands/issues/relations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ defmodule LinearCli.CLI.Commands.Issues.Relations do
"""
@spec issue_relation_list(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_relation_list(%{args: %{issue_id: issue_id}, options: options}) do
expanded_id = Identifiers.expand_issue_id(issue_id)

with {:ok, relations} <- Linear.issue_relations(expanded_id) do
with {:ok, expanded_id} <- Identifiers.expand_issue_id(issue_id, output: options.output),
{:ok, relations} <- Linear.issue_relations(expanded_id) do
Display.show(relations, %{output: options.output, relations: true})
:ok
end
Expand Down Expand Up @@ -50,16 +49,33 @@ defmodule LinearCli.CLI.Commands.Issues.Relations do
do: {:error, {:smells_bad, "At least one RELATED_ISSUE is required"}}

def issue_relation_add(%{unknown: [subject_id | related_ids], options: options}, opts) do
expanded_subject = Identifiers.expand_issue_id(subject_id)
user_type = options.type

results =
Enum.map(related_ids, fn related_id ->
expanded_related = Identifiers.expand_issue_id(related_id)
add_single_relation(expanded_subject, expanded_related, user_type)
end)
if options.output == "json" do
with {:ok, [expanded_subject | expanded_related_ids]} <-
Identifiers.expand_issue_ids([subject_id | related_ids], output: options.output) do
results =
Enum.map(expanded_related_ids, fn expanded_related ->
add_single_relation(expanded_subject, expanded_related, user_type)
end)

finish_relation_add(results, options.output, Keyword.get(opts, :stderr, :stderr))
end
else
expanded_subject = Identifiers.expand_issue_id(subject_id)

results =
Enum.map(related_ids, fn related_id ->
expanded_related = Identifiers.expand_issue_id(related_id)
add_single_relation(expanded_subject, expanded_related, user_type)
end)

finish_relation_add(results, options.output, Keyword.get(opts, :stderr, :stderr))
end
end

print_relation_add_results(results, options.output, Keyword.get(opts, :stderr, :stderr))
defp finish_relation_add(results, output, stderr) do
print_relation_add_results(results, output, stderr)

failed_count =
Enum.count(results, fn r -> match?({:failed, _, _}, r) or match?({:self_link, _}, r) end)
Expand Down Expand Up @@ -205,32 +221,50 @@ defmodule LinearCli.CLI.Commands.Issues.Relations do
do: {:error, {:smells_bad, "At least one RELATED_ISSUE is required"}}

def issue_relation_remove(%{unknown: [subject_id | related_ids], options: options}, opts) do
expanded_subject = Identifiers.expand_issue_id(subject_id)
user_type = options.type

with {:ok, all_relations} <- Linear.issue_relations(expanded_subject) do
results =
Enum.map(related_ids, fn related_id ->
expanded_related = Identifiers.expand_issue_id(related_id)
remove_single_relation(expanded_subject, expanded_related, user_type, all_relations)
end)
if options.output == "json" do
with {:ok, [expanded_subject | expanded_related_ids]} <-
Identifiers.expand_issue_ids([subject_id | related_ids], output: options.output),
{:ok, all_relations} <- Linear.issue_relations(expanded_subject) do
results =
Enum.map(expanded_related_ids, fn expanded_related ->
remove_single_relation(expanded_subject, expanded_related, user_type, all_relations)
end)

print_relation_remove_results(results, options.output, Keyword.get(opts, :stderr, :stderr))
finish_relation_remove(results, options.output, Keyword.get(opts, :stderr, :stderr))
end
else
expanded_subject = Identifiers.expand_issue_id(subject_id)

failed_count =
Enum.count(results, fn r ->
match?({:failed, _, _}, r) or match?({:ambiguous, _, _}, r) or
match?({:self_link, _}, r)
end)
with {:ok, all_relations} <- Linear.issue_relations(expanded_subject) do
results =
Enum.map(related_ids, fn related_id ->
expanded_related = Identifiers.expand_issue_id(related_id)
remove_single_relation(expanded_subject, expanded_related, user_type, all_relations)
end)

if failed_count > 0 do
{:error, {:smells_bad, "#{failed_count} relation(s) failed to be removed"}}
else
:ok
finish_relation_remove(results, options.output, Keyword.get(opts, :stderr, :stderr))
end
end
end

defp finish_relation_remove(results, output, stderr) do
print_relation_remove_results(results, output, stderr)

failed_count =
Enum.count(results, fn r ->
match?({:failed, _, _}, r) or match?({:ambiguous, _, _}, r) or
match?({:self_link, _}, r)
end)

if failed_count > 0 do
{:error, {:smells_bad, "#{failed_count} relation(s) failed to be removed"}}
else
:ok
end
end

defp remove_single_relation(subject_id, related_id, _user_type, _relations)
when subject_id == related_id do
{:self_link, subject_id}
Expand Down
5 changes: 3 additions & 2 deletions app/lib/linear_cli/cli/issue/assignment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ defmodule LinearCli.CLI.Issue.Assignment do
"""
@spec gimme_da_issue!(String.t(), keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()}
def gimme_da_issue!(issue_id, opts \\ []) do
issue_id = Identifiers.expand_issue_id(issue_id)
status_opt = parse_status_opt(opts)

with {:ok, me} <- resolve_me(opts),
with {:ok, issue_id} <-
Identifiers.expand_issue_id(issue_id, output: Keyword.get(opts, :output, "text")),
{:ok, me} <- resolve_me(opts),
{:ok, [issue]} <- Linear.issues(%{ids: [issue_id]}),
{:ok, state_id} <- resolve_status_for_issue(issue, status_opt) do
assign_or_confirm(issue, me, issue_id, state_id)
Expand Down
Loading
Loading