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
18 changes: 17 additions & 1 deletion Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ endif::[]
// x-release-please-end

A command line interface to https://linear.app - list, take, create, develop,
and update issues without leaving your terminal.
unassign, and update issues without leaving your terminal.

[cols="1,1",frame=none,grid=none]
|===
Expand Down Expand Up @@ -175,6 +175,7 @@ so you don't have to type the full name every time:
|`issue list` |`l`, `ls`
|`issue status` |`s`, `st`, `stat`
|`issue assign` |`a`
|`issue unassign` |
|`issue move` |`m`, `mv`
|`issue update` |`u`
|`issue view` |`v`
Expand Down Expand Up @@ -285,6 +286,21 @@ $ lc issue assign CRY-1234 <4>
<3> Short form of `--status`
<4> Prompts for the assignee interactively

==== Unassign one or more issues

This command clears the assignee from each listed issue. It accepts explicit
issue identifiers only.

[source,sh]
----
$ lc issue unassign CRY-1234
$ lc issue unassign CRY-1 CRY-2
$ lc issue unassign --output json CRY-1 CRY-2
----

Text output shows each updated issue and a confirmation. JSON output returns
one object for one issue and an array for multiple issues.

==== Create an issue

[source,sh]
Expand Down
12 changes: 10 additions & 2 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ defmodule LinearCli.CLI do
defp dispatch([:issue, :status], result, context),
do: run(&Mutations.issue_status/1, result, context)

defp dispatch([:issue, :unassign], result, context),
do: run(&Mutations.issue_unassign/1, result, context)

defp dispatch([:issue, :update], result, context),
do: run(&Mutations.issue_update/1, result, context)

Expand Down Expand Up @@ -319,7 +322,7 @@ defmodule LinearCli.CLI do
end
end

# `issue list`/`take`/`status`/`update` all set `allow_unknown_args: true` so bare
# `issue list`/`take`/`status`/`unassign`/`update` all set `allow_unknown_args: true` so bare
# tokens (e.g. `CRY-1`) can be captured as issue ids via `result.unknown`
# rather than a declared positional arg (Optimus has no `type: :array`
# equivalent - see their subcommand specs below). That same bucket also
Expand All @@ -329,7 +332,7 @@ defmodule LinearCli.CLI do
# clearly. Every other subcommand has `allow_unknown_args: false` (the
# default), where Optimus itself already rejects unknown args before we
# ever see a parse_result - so `result.unknown` is only ever non-empty here
# for those four subcommands, and only ever contains genuine bare ids
# for these subcommands, and only ever contains genuine bare ids
# once this filters out anything flag-shaped.
defp reject_unknown_flags(unknown_tokens) do
case Enum.filter(unknown_tokens, &String.starts_with?(&1, "-")) do
Expand Down Expand Up @@ -904,6 +907,11 @@ defmodule LinearCli.CLI do
]
]
],
unassign: [
name: "unassign",
about: "Clear the assignee from one or more issues (ISSUE_ID...)",
allow_unknown_args: true
],
take: [
name: "take",
about: "Assign one or more issues to yourself",
Expand Down
51 changes: 50 additions & 1 deletion app/lib/linear_cli/cli/commands/issues/mutations.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule LinearCli.CLI.Commands.Issues.Mutations do
@moduledoc """
Issue mutation commands: update, comment, status, and assign.
Issue mutation commands: update, comment, status, assign, and unassign.
Ported from vendor/ruby-linear-cli/lib/linear/commands/issue/update.rb,
comment.rb, status.rb, and assign.rb.
"""
Expand Down Expand Up @@ -119,6 +119,26 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
end
end

@doc """
Clears the assignee from one or more issues.

Optimus captures the explicit issue IDs in `unknown`, since it has no
variadic positional-argument type. Each issue is resolved before its
assignee is cleared. Updates run concurrently with the same limit and input
order as the other batch issue mutations.
"""
@spec issue_unassign(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_unassign(%{unknown: issue_ids, options: options}) do
with :ok <- validate_issue_ids(issue_ids),
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &Identifiers.expand_issue_id/1)}),
{:ok, updated_issues} <- unassign_issues(issues) do
Display.show(one_or_many(updated_issues), %{output: options.output})
print_unassign_results(updated_issues, options.output)
:ok
end
end

@doc """
Assigns an issue to a team member.

Expand Down Expand Up @@ -203,6 +223,35 @@ defmodule LinearCli.CLI.Commands.Issues.Mutations do
end)
end

defp unassign_issues([]), do: {:ok, []}

defp unassign_issues(issues) do
issues
|> Task.async_stream(
&Linear.unassign_issue/1,
max_concurrency: min(length(issues), @max_concurrent_issue_updates),
ordered: true,
timeout: 30_000
)
|> Enum.reduce_while({:ok, []}, fn
{:ok, {:ok, updated}}, {:ok, acc} -> {:cont, {:ok, [updated | acc]}}
{:ok, {:error, reason}}, _acc -> {:halt, {:error, reason}}
{:exit, reason}, _acc -> {:halt, {:error, {:task_exit, reason}}}
end)
|> then(fn
{:ok, updated_issues} -> {:ok, Enum.reverse(updated_issues)}
error -> error
end)
end

defp print_unassign_results(_updated_issues, "json"), do: :ok

defp print_unassign_results(updated_issues, _output) do
Enum.each(updated_issues, fn updated ->
Prompt.ok("#{updated.identifier} unassigned")
end)
end

defp one_or_many([one]), do: one
defp one_or_many(many), do: many

Expand Down
1 change: 1 addition & 0 deletions app/lib/linear_cli/linear.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule LinearCli.Linear do
define :issues, action: :list
define :create_issue, action: :create, args: [:title, :description, :team_id]
define :assign_issue, action: :assign, args: [:assignee_id]
define :unassign_issue, action: :unassign
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]
Expand Down
19 changes: 17 additions & 2 deletions app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ defmodule LinearCli.Linear.Issue do
manual LinearCli.Linear.Issue.Update.Assign
end

update :unassign do
manual LinearCli.Linear.Issue.Update.Unassign
end

# Ruby: Issue#attach_to_project(project)
update :attach_to_project do
argument :project_id, :string, allow_nil?: false
Expand Down Expand Up @@ -412,8 +416,8 @@ defmodule LinearCli.Linear.Issue.Update do
alias LinearCli.Linear.Issue

# Ruby: Issue#update!(input) - the shared issueUpdate mutation that
# assign!/attach_to_project!/close! all delegate to, refetching the
# updated issue via Issue.full_fragment.
# the issue update actions delegate to, refetching the updated issue via
# Issue.full_fragment.
def run(identifier, input) do
case Api.call(document(), %{"id" => identifier, "input" => input}) do
{:ok, %{"issueUpdate" => %{"issue" => issue_map}}} when is_map(issue_map) ->
Expand Down Expand Up @@ -451,6 +455,17 @@ defmodule LinearCli.Linear.Issue.Update.Assign do
end
end

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

alias LinearCli.Linear.Issue

def update(changeset, _opts, _context) do
Issue.Update.run(changeset.data.identifier, %{"assigneeId" => nil})
end
end

defmodule LinearCli.Linear.Issue.Update.AttachToProject do
@moduledoc false
use Ash.Resource.ManualUpdate
Expand Down
4 changes: 2 additions & 2 deletions app/mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
%{
"ash": {:hex, :ash, "3.33.9", "434d424f19f4d896382e51a9cc5d78bcfc197a926a6acbaa05b1a3ca89b4ad10", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ce2e1093e960917815824a71b4ffaa0d08fb6c4cecd314bc40ca49eced299a9"},
"ash": {:hex, :ash, "3.33.11", "4118ddcab0b6c3c388fa7f69cc1fe5850f550bb7c26307b34f7a642f73ae4bf2", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f36bdbab6ccc07029d09e4a8b75f9f4a1c0f14bc62db68cc4dd7eefa16c3b8b9"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"burrito": {:hex, :burrito, "1.6.0", "7af0a75f11680e8a6e9c01370c9af51cb9d0e15b3226eddf4f438dbc68570520", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "e636a00b032c45a69ff755d9fc53fa5fdc9e1d21bdbd229075fe4a15b05355fe"},
"castore": {:hex, :castore, "1.0.21", "0a0e8330dc267a40a3b7ad86d39302764bb71758172904e6a59d5ad6443ce307", [:mix], [], "hexpm", "e42e22723e25dbd46876d056a03f685513d6e98f6b5e555dc551321decd76c5c"},
Expand All @@ -19,7 +19,7 @@
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
"glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"},
"hex_core": {:hex, :hex_core, "0.19.0", "b9b5d3cde2d5227b85c62c4ab9f9db586e6b38cf65a9bf5b683757cae9ced5eb", [:rebar3], [], "hexpm", "c2cc414f3893a7edbcab28207cadca13829407abe41f689c82f379fe22f026a5"},
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
"hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"},
"igniter": {:hex, :igniter, "0.8.4", "f79f1bbdc2fb7b9ca030a22d12a585b060cbf5b94b9d3f23b1148578a9e05d11", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a9b1cbec996ccb100b4f7d8130129b2dd3f18eb4224ac9a0e907e428ca90dbd7"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
Expand Down
Loading
Loading