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
29 changes: 27 additions & 2 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ defmodule LinearCli.CLI do
# `parse_result` is bound above, outside this try, specifically so
# it's still in scope here (bindings from inside a `do` block aren't
# visible in that same try's `rescue`, but outer-scope bindings are).
exception -> handle_error(exception, parse_result.options[:debug], halt)
exception ->
debug = is_struct(parse_result, Optimus.ParseResult) && parse_result.options[:debug]
handle_error(exception, debug, halt)
end
end

Expand Down Expand Up @@ -507,7 +509,11 @@ defmodule LinearCli.CLI do
long: "--no-mine",
help: "List the most recent issues, not just your own"
],
full: [short: "-f", long: "--full", help: "Show full issue details"]
full: [short: "-f", long: "--full", help: "Show full issue details"],
all: [
long: "--all",
help: "Show all issues including completed and cancelled"
]
],
options: [
team: [short: "-t", long: "--team", help: "Show issues for only this team"],
Expand All @@ -516,6 +522,25 @@ defmodule LinearCli.CLI do
long: "--project",
help:
"Show issues for only this project. Can be name, URL, ID, or - to select from a list"
],
status: [
short: "-s",
long: "--status",
help:
"Filter by workflow state type(s): triage, backlog, unstarted, started, completed, cancelled (comma-separated)",
parser: fn v ->
valid = ~w(triage backlog unstarted started completed cancelled canceled)
types = String.split(v, ",", trim: true)

case Enum.find(types, &(&1 not in valid)) do
nil ->
{:ok, types}

bad ->
{:error,
"unknown status #{inspect(bad)}, must be one of: #{Enum.join(valid, ", ")}"}
end
end
]
]
],
Expand Down
4 changes: 3 additions & 1 deletion app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ defmodule LinearCli.CLI.Commands do
mine: !flags.no_mine,
unassigned: flags.unassigned,
team_key: team_key,
project_id: project_id
project_id: project_id,
all: Map.get(flags, :all, false),
status: Map.get(options, :status) || []
}

with {:ok, issues} <- Linear.issues(input) do
Expand Down
45 changes: 44 additions & 1 deletion app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ defmodule LinearCli.Linear.Issue do
argument :unassigned, :boolean, default: false
argument :team_key, :string, allow_nil?: true
argument :project_id, :string, allow_nil?: true
argument :all, :boolean, default: false
argument :status, {:array, :string}, default: []
manual LinearCli.Linear.Issue.Read.List
end

Expand Down Expand Up @@ -174,11 +176,46 @@ defmodule LinearCli.Linear.Issue.Read.List do

# Ported from Rubyists::Linear::Operations::Issue::List#build_filter. `unassigned`
# is checked after `mine` here too, so it wins if both are set - same as Ruby.
# `all: true` removes the completedAt/canceledAt null-checks so closed/cancelled
# issues are included. `status` injects a state.type filter; when it includes
# "completed" or "cancelled"/"canceled", the corresponding date null-checks are
# also dropped so those issues aren't filtered out before the type filter applies.
defp build_filter(args) do
%{"completedAt" => %{"null" => true}, "canceledAt" => %{"null" => true}}
%{}
|> maybe_put_date_filters(args)
|> maybe_put_assignee_filter(args)
|> maybe_put_team_filter(args)
|> maybe_put_project_filter(args)
|> maybe_put_state_filter(args)
end

@completed_types ~w(completed)
@cancelled_types ~w(cancelled canceled)

defp maybe_put_date_filters(filter, %{all: true}), do: filter

defp maybe_put_date_filters(filter, %{status: status}) when status != [] do
filter
|> maybe_put_completed_date_filter(status)
|> maybe_put_cancelled_date_filter(status)
end

defp maybe_put_date_filters(filter, _args) do
Map.merge(filter, %{"completedAt" => %{"null" => true}, "canceledAt" => %{"null" => true}})
end

# Suppress the completedAt null-check only when the status list doesn't ask for completed.
defp maybe_put_completed_date_filter(filter, status) do
if Enum.any?(status, &(&1 in @completed_types)),
do: filter,
else: Map.put(filter, "completedAt", %{"null" => true})
end

# Suppress the canceledAt null-check only when the status list doesn't ask for cancelled.
defp maybe_put_cancelled_date_filter(filter, status) do
if Enum.any?(status, &(&1 in @cancelled_types)),
do: filter,
else: Map.put(filter, "canceledAt", %{"null" => true})
end

defp maybe_put_assignee_filter(filter, %{unassigned: true}) do
Expand All @@ -202,6 +239,12 @@ defmodule LinearCli.Linear.Issue.Read.List do
end

defp maybe_put_project_filter(filter, _args), do: filter

defp maybe_put_state_filter(filter, %{status: [_ | _] = types}) do
Map.put(filter, "state", %{"type" => %{"in" => types}})
end

defp maybe_put_state_filter(filter, _args), do: filter
end

defmodule LinearCli.Linear.Issue.Create do
Expand Down
97 changes: 97 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,103 @@ defmodule LinearCli.CLI.IssueCommandsTest do
output = capture_io(fn -> assert :ok = LinearCli.CLI.main(["issue", "list"]) end)
assert output =~ "CRY-1"
end

test "--all removes completedAt and canceledAt null-check filters" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--all"])
end)

assert_received {:filter, filter}
refute Map.has_key?(filter, "completedAt")
refute Map.has_key?(filter, "canceledAt")
end

test "--status filters by workflow state type and removes corresponding date filters" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--status", "started"])
end)

assert_received {:filter, filter}
assert filter["state"] == %{"type" => %{"in" => ["started"]}}
# "started" is not completed/cancelled so both date filters remain
assert Map.has_key?(filter, "completedAt")
assert Map.has_key?(filter, "canceledAt")
end

test "--status completed removes completedAt filter but keeps canceledAt filter" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--status", "completed"])
end)

assert_received {:filter, filter}
assert filter["state"] == %{"type" => %{"in" => ["completed"]}}
refute Map.has_key?(filter, "completedAt")
assert Map.has_key?(filter, "canceledAt")
end

test "--status accepts multiple comma-separated types" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--status", "started,completed"])
end)

assert_received {:filter, filter}
assert filter["state"] == %{"type" => %{"in" => ["started", "completed"]}}
refute Map.has_key?(filter, "completedAt")
assert Map.has_key?(filter, "canceledAt")
end

test "--status with an unknown type exits 1 (Optimus parse error)" do
test_pid = self()
halt = fn code -> send(test_pid, {:halted, code}) end

# Optimus catches the bad value and calls halt.(1); with a fake halt that
# doesn't terminate the process, execution continues and eventually crashes
# (same artifact as the --help test in cli_test.exs). Rescue it so the test
# can still verify halt was called with the right code.
try do
LinearCli.CLI.main(["issue", "list", "--status", "badtype"], halt)
rescue
_ -> :ok
end

assert_received {:halted, 1}
end
end

describe "issue create (Ruby: commands/issue/create.rb)" do
Expand Down