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
4 changes: 4 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,13 @@ Show full details for a single issue (title, description, and all comments):
$ lc issue view CRY-1234
$ lc i v CRY-1234 <1>
$ lc issue view CRY-1234 --output json <2>
$ lc issue view CRY-1234 --web <3>
$ lc issue view CRY-1234 -w <4>
----
<1> Short alias: `i` for `issue`, `v` for `view`
<2> Output as JSON
<3> Open the issue in your browser instead of printing it
<4> Short form of `--web`

==== Assign one or more issues to yourself (take em!)

Expand Down
28 changes: 28 additions & 0 deletions app/lib/linear_cli/browser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule LinearCli.Browser do
@moduledoc false

@doc """
Opens `url` in the system's default browser.

Accepts an injectable `opener` in `opts` (a `url -> :ok | {:error, term()}`
function) for test doubles — real callers omit it.
"""
def open_url(url, opts \\ []) do
opener = Keyword.get(opts, :opener, &default_opener/1)
opener.(url)
end

defp default_opener(url) do
{cmd, args} =
case :os.type() do
{:unix, :darwin} -> {"open", [url]}
{:win32, _} -> {"cmd", ["/c", "start", url]}
_ -> {"xdg-open", [url]}
end

case System.cmd(cmd, args, stderr_to_stdout: true) do
{_, 0} -> :ok
{output, code} -> {:error, {:browser_open_failed, code, output}}
end
end
end
3 changes: 3 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ defmodule LinearCli.CLI do
about: "Show full details for a single issue",
args: [
issue_id: [value_name: "ISSUE_ID", help: "The Issue (i.e. CRY-1)", required: true]
],
flags: [
web: [short: "-w", long: "--web", help: "Open the issue in your browser"]
]
],
assign: [
Expand Down
17 changes: 14 additions & 3 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule LinearCli.CLI.Commands do
result. Ported from vendor/ruby-linear-cli/lib/linear/commands/**.
"""

alias LinearCli.Browser
alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt, WhatFor}
alias LinearCli.{Favorites, Git, Linear, Profiles}

Expand Down Expand Up @@ -291,13 +292,23 @@ defmodule LinearCli.CLI.Commands do

Mirrors `gh issue view`: a dedicated verb for the single-issue display case,
making it discoverable without knowing about `list`'s `--full` flag.

With `-w`/`--web`, opens the issue URL in the default browser instead of
printing it. The `opts` keyword arg accepts an injectable `opener` for tests.
"""
def issue_view(%{args: %{issue_id: issue_id}, options: options}) do
@spec issue_view(Optimus.ParseResult.t(), keyword()) :: :ok | {:error, term()}
def issue_view(result, opts \\ [])

def issue_view(%{args: %{issue_id: issue_id}, flags: flags, options: options}, opts) do
expanded_id = IssueHelpers.expand_issue_id(issue_id)

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}) do
Display.show(issue, %{output: options.output, full: true})
:ok
if flags.web do
Browser.open_url(issue.url, opts)
else
Display.show(issue, %{output: options.output, full: true})
:ok
end
end
end

Expand Down
4 changes: 3 additions & 1 deletion app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ defmodule LinearCli.Linear.Issue do
attribute :title, :string, public?: true
attribute :branch_name, :string, public?: true
attribute :description, :string, public?: true
attribute :url, :string, public?: true
attribute :assignee, :term, public?: true
attribute :state, :term, public?: true
attribute :team, :term, public?: true
attribute :comments, {:array, :term}, public?: true, default: []
end

@issue_fields "id identifier title branchName description createdAt updatedAt"
@issue_fields "id identifier title branchName description url createdAt updatedAt"
@state_fields "id name type"

@doc "GraphQL field selection for an issue plus its assignee/team (Ruby: Issue.base_fragment)."
Expand Down Expand Up @@ -102,6 +103,7 @@ defmodule LinearCli.Linear.Issue do
title: map["title"],
branch_name: map["branchName"],
description: map["description"],
url: map["url"],
assignee: map["assignee"] && LinearCli.Linear.User.from_map(map["assignee"]),
state: map["state"] && LinearCli.Linear.WorkflowState.from_map(map["state"]),
team: map["team"] && LinearCli.Linear.Team.from_map(map["team"]),
Expand Down
28 changes: 28 additions & 0 deletions app/test/linear_cli/browser_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule LinearCli.BrowserTest do
use ExUnit.Case, async: true

alias LinearCli.Browser

describe "open_url/2" do
test "calls the injected opener with the URL" do
test_pid = self()

assert :ok =
Browser.open_url("https://linear.app/the-rubyists/issue/EXT-20",
opener: fn url ->
send(test_pid, {:opened, url})
:ok
end
)

assert_received {:opened, "https://linear.app/the-rubyists/issue/EXT-20"}
end

test "returns the opener's error result unchanged" do
assert {:error, :fake_error} =
Browser.open_url("https://example.com",
opener: fn _url -> {:error, :fake_error} end
)
end
end
end
100 changes: 100 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,106 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert output =~ "CRY-1"
assert output =~ "Fix the thing"
end

test "--web opens the issue URL in the browser and prints nothing" do
test_pid = self()

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

Req.Test.json(conn, %{
"data" => %{
"issue" => issue_map(%{"url" => "https://linear.app/the-rubyists/issue/CRY-1"})
}
})
end)

output =
capture_io(fn ->
assert :ok =
Commands.issue_view(
%{
args: %{issue_id: "CRY-1"},
flags: %{web: true},
options: %{output: "text"}
},
opener: fn url ->
send(test_pid, {:opened, url})
:ok
end
)
end)

assert_received {:opened, "https://linear.app/the-rubyists/issue/CRY-1"}
assert output == ""
end

test "-w short flag opens the browser via the full CLI dispatch path" do
test_pid = self()

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

Req.Test.json(conn, %{
"data" => %{
"issue" => issue_map(%{"url" => "https://linear.app/the-rubyists/issue/CRY-1"})
}
})
end)

capture_io(fn ->
assert :ok =
Commands.issue_view(
%{
args: %{issue_id: "CRY-1"},
flags: %{web: true},
options: %{output: "text"}
},
opener: fn url ->
send(test_pid, {:opened, url})
:ok
end
)
end)

assert_received {:opened, "https://linear.app/the-rubyists/issue/CRY-1"}
end

test "--web with --output json opens browser and prints nothing" do
test_pid = self()

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

Req.Test.json(conn, %{
"data" => %{
"issue" => issue_map(%{"url" => "https://linear.app/the-rubyists/issue/CRY-1"})
}
})
end)

output =
capture_io(fn ->
assert :ok =
Commands.issue_view(
%{
args: %{issue_id: "CRY-1"},
flags: %{web: true},
options: %{output: "json"}
},
opener: fn url ->
send(test_pid, {:opened, url})
:ok
end
)
end)

assert_received {:opened, "https://linear.app/the-rubyists/issue/CRY-1"}
assert output == ""
end
end

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