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
21 changes: 21 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ defmodule LinearCli.CLI do
"dev" => "develop",
"l" => "list",
"ls" => "list",
"s" => "status",
"u" => "update",
"pull-request" => "pr"
},
Expand Down Expand Up @@ -203,6 +204,7 @@ defmodule LinearCli.CLI do

defp dispatch([:issue, :pr], result, halt), do: run(&Commands.issue_pr/1, result, halt)
defp dispatch([:issue, :take], result, halt), do: run(&Commands.issue_take/1, result, halt)
defp dispatch([:issue, :status], result, halt), do: run(&Commands.issue_status/1, result, halt)
defp dispatch([:issue, :update], result, halt), do: run(&Commands.issue_update/1, result, halt)

# A valid subcommand path that stops short of a leaf (e.g. `lc project`
Expand Down Expand Up @@ -594,6 +596,25 @@ defmodule LinearCli.CLI do
description: [long: "--description", help: "The description of the PR"]
]
],
status: [
name: "status",
about: "Change the workflow state of an issue",
args: [
issue_id: [value_name: "ISSUE_ID", help: "The Issue (i.e. CRY-1)", required: true]
],
options: [
status: [
short: "-s",
long: "--status",
help: "Workflow state name to set (prompts if omitted)"
],
comment: [
short: "-m",
long: "--comment",
help: "Comment to add alongside the status change"
]
]
],
take: [
name: "take",
about: "Assign one or more issues to yourself",
Expand Down
68 changes: 68 additions & 0 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,72 @@ defmodule LinearCli.CLI.Commands do

defp validate_issue_ids([]), do: {:error, {:smells_bad, "No issue IDs provided!"}}
defp validate_issue_ids(_issue_ids), do: :ok

@doc """
Changes the workflow state of an issue.

With `--status`/`-s`, matches the given name against the issue's team's
workflow states (case-insensitive exact, then unique prefix). Without it,
prompts interactively via `LinearCli.CLI.Prompt.select/2`.

With `--comment`/`-m`, adds a comment to the issue before transitioning.
"""
@spec issue_status(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_status(%{args: %{issue_id: issue_id}, options: options}) do
expanded_id = IssueHelpers.expand_issue_id(issue_id)

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
{:ok, states} <- Linear.workflow_states_by_team(issue.team.id),
{:ok, target_state} <- resolve_target_state(states, options.status),
:ok <- maybe_add_status_comment(issue, options.comment),
{:ok, updated} <- Linear.set_issue_status(issue, target_state.id) do
Display.show(updated, %{output: options.output})

if options.output != "json",
do: Prompt.ok("#{updated.identifier} status set to #{target_state.name}")

:ok
end
end

defp resolve_target_state(states, nil) do
choices = Enum.sort_by(states, & &1.position) |> Enum.map(&{&1.name, &1})
{:ok, Prompt.select("Choose a status", choices)}
end

defp resolve_target_state(states, name) do
Comment thread
bougyman marked this conversation as resolved.
normalized_name = String.downcase(name)

states
|> Enum.filter(&(String.downcase(&1.name) == normalized_name))
|> use_prefix_matches_if_empty(states, normalized_name)
|> resolve_state_matches(states, name)
end

defp use_prefix_matches_if_empty([], states, name) do
Enum.filter(states, &String.starts_with?(String.downcase(&1.name), name))
end

defp use_prefix_matches_if_empty(matches, _states, _name), do: matches

defp resolve_state_matches([state], _states, _name), do: {:ok, state}

defp resolve_state_matches([], states, name) do
available = Enum.map_join(states, ", ", & &1.name)
{:error, {:smells_bad, "Unknown status #{inspect(name)}. Available: #{available}"}}
end

defp resolve_state_matches(matches, _states, name) do
ambiguous = Enum.map_join(matches, ", ", & &1.name)
{:error, {:smells_bad, "Ambiguous status #{inspect(name)}: matches #{ambiguous}"}}
end

defp maybe_add_status_comment(_issue, nil), do: :ok

defp maybe_add_status_comment(issue, comment) do
case IssueHelpers.issue_comment(issue, comment) do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
end
1 change: 1 addition & 0 deletions app/lib/linear_cli/linear.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule LinearCli.Linear do
define :assign_issue, action: :assign, args: [:assignee_id]
define :attach_issue_to_project, action: :attach_to_project, args: [:project_id]
define :close_issue, action: :close, args: [:state_id]
define :set_issue_status, action: :set_status, args: [:state_id]
end

resource LinearCli.Linear.Label do
Expand Down
18 changes: 18 additions & 0 deletions app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ defmodule LinearCli.Linear.Issue do
argument :trash, :boolean, default: false
manual LinearCli.Linear.Issue.Update.Close
end

update :set_status do
argument :state_id, :string, allow_nil?: false
manual LinearCli.Linear.Issue.Update.SetStatus
end
end

attributes do
Expand Down Expand Up @@ -360,3 +365,16 @@ defmodule LinearCli.Linear.Issue.Update.Close do
Issue.Update.run(changeset.data.identifier, input)
end
end

defmodule LinearCli.Linear.Issue.Update.SetStatus do
@moduledoc false
use Ash.Resource.ManualUpdate

alias LinearCli.Linear.Issue

def update(changeset, _opts, _context) do
Issue.Update.run(changeset.data.identifier, %{
"stateId" => changeset.arguments.state_id
})
end
end
Loading