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 app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,18 @@ defmodule LinearCli.CLI do

defp parse_statuses(value), do: {:ok, split_filter_values(value)}

defp parse_labels(value), do: {:ok, split_filter_values(value)}
defp parse_labels(value) do
values = split_filter_values(value)
bad = Enum.find(values, &String.starts_with?(&1, "-"))
parse_labels_result(values, bad)
end

defp parse_labels_result(values, nil), do: {:ok, values}

defp parse_labels_result(_values, bad),
do:
{:error,
"invalid value #{inspect(bad)} for --labels: looks like an option token, not a label name"}

defp split_filter_values(value) do
value
Expand Down Expand Up @@ -699,6 +710,11 @@ defmodule LinearCli.CLI do
all: [
long: "--all",
help: "Show all issues including completed and cancelled"
],
include_labels: [
short: "-i",
long: "--include-labels",
help: "Include labels in issue list output"
]
],
options: [
Expand Down
8 changes: 6 additions & 2 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ defmodule LinearCli.CLI.Commands do
options.project || unless no_profile, do: Profiles.default_project()

with {:ok, project_id} <- resolve_project_id(project_source, team_key) do
label_filter = Map.get(options, :labels) || []
include_labels = Map.get(flags, :include_labels, false) || label_filter != []

input = %{
ids: Enum.map(ids, &IssueHelpers.expand_issue_id/1),
mine: !flags.no_mine,
Expand All @@ -278,14 +281,15 @@ defmodule LinearCli.CLI.Commands do
all: Map.get(flags, :all, false),
state: Map.get(options, :state) || [],
status: Map.get(options, :status) || [],
labels: Map.get(options, :labels) || []
labels: label_filter,
include_labels: include_labels
}

with {:ok, issues} <- Linear.issues(input) do
Display.show(issues, %{
output: options.output,
full: flags.full,
labels: input.labels != []
labels: include_labels
})

:ok
Expand Down
3 changes: 2 additions & 1 deletion app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule LinearCli.Linear.Issue do
argument :state, {:array, :string}, default: []
argument :status, {:array, :string}, default: []
argument :labels, {:array, :string}, default: []
argument :include_labels, :boolean, default: false
manual LinearCli.Linear.Issue.Read.List
end

Expand Down Expand Up @@ -159,7 +160,7 @@ defmodule LinearCli.Linear.Issue.Read.List do
if args.ids != [] do
find_by_ids(args.ids)
else
list_all(build_filter(args), args.labels != [])
list_all(build_filter(args), args.include_labels || args.labels != [])
end
end

Expand Down
275 changes: 275 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,281 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert output =~ "CRY-1"
refute output =~ "[Bug]"
end

test "--include-labels requests label fields and shows them in compact output" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

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

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

assert_received {:query, query}
assert String.contains?(query, "labels")
assert output =~ "CRY-1"
assert output =~ "[Bug]"
end

test "-i short flag requests label fields and shows them in compact output" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

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

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

assert_received {:query, query}
assert String.contains?(query, "labels")
assert output =~ "CRY-1"
assert output =~ "[Bug]"
end

test "-N --include-labels --all sends no assignee/date filter, no label filter, requests label fields" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Feature", "description" => nil, "isGroup" => false}
]
}
})

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"]})
send(test_pid, {:query, decoded["query"]})
Req.Test.json(conn, issues_response([labeled_issue]))
end)

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

assert_received {:filter, filter}
assert_received {:query, query}
refute Map.has_key?(filter, "assignee")
refute Map.has_key?(filter, "completedAt")
refute Map.has_key?(filter, "canceledAt")
refute Map.has_key?(filter, "labels")
assert String.contains?(query, "labels")
assert output =~ "CRY-1"
assert output =~ "[Feature]"
end

test "-N -i --all is the same as -N --include-labels --all" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Feature", "description" => nil, "isGroup" => false}
]
}
})

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([labeled_issue]))
end)

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

assert_received {:filter, filter}
refute Map.has_key?(filter, "assignee")
refute Map.has_key?(filter, "labels")
assert output =~ "[Feature]"
end

test "--labels --all exits 1 (Optimus parse error, no API request)" do
Req.Test.stub(LinearCli.Api, fn _conn -> raise "no GraphQL call should happen" end)

output =
capture_io(fn ->
assert catch_throw(
LinearCli.CLI.main(
["issue", "list", "--labels", "--all"],
fn code -> throw({:halted, code}) end
)
) == {:halted, 1}
end)

assert output =~ "--labels"
end

test "-s/--status composes with --include-labels" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

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"]})
send(test_pid, {:query, decoded["query"]})
Req.Test.json(conn, issues_response([labeled_issue]))
end)

output =
capture_io(fn ->
assert :ok =
LinearCli.CLI.main([
"issue",
"list",
"--status",
"Human Review",
"--include-labels"
])
end)

assert_received {:filter, filter}
assert_received {:query, query}
assert filter["state"] == %{"name" => %{"eqIgnoreCase" => "Human Review"}}
assert String.contains?(query, "labels")
assert output =~ "CRY-1"
assert output =~ "[Bug]"
end

test "positional issue identifier with --include-labels uses full lookup and renders labels" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"query" => query} = Jason.decode!(body)
send(test_pid, {:query, query})

Req.Test.json(conn, %{
"data" => %{
"issue" =>
issue_map(%{
"labels" => %{
"nodes" => [
%{
"id" => "l1",
"name" => "Bug",
"description" => nil,
"isGroup" => false
}
]
},
"comments" => %{"nodes" => []},
"relations" => %{
"edges" => [],
"pageInfo" => %{"hasNextPage" => false, "endCursor" => nil}
},
"inverseRelations" => %{
"edges" => [],
"pageInfo" => %{"hasNextPage" => false, "endCursor" => nil}
}
})
}
})
end)

output =
capture_io(fn ->
assert :ok =
LinearCli.CLI.main(["issue", "list", "CRY-1", "--include-labels"])
end)

assert_received {:query, query}
assert String.contains?(query, "issue(id: $id)")
assert output =~ "CRY-1"
assert output =~ "Bug"
end

test "--output json --include-labels contains fetched label objects in stdout" do
test_pid = self()

labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

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

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

assert_received {:query, query}
assert String.contains?(query, "labels")
assert {:ok, [decoded]} = Jason.decode(output)
assert [label] = decoded["labels"]
assert label["name"] == "Bug"
end
end

describe "issue view" do
Expand Down
Loading