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
5 changes: 5 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ $ lc profile delete manhattan
An explicit `--team`/`--project` on the command line always overrides the
active profile.

Issue commands also accept a bare number (`lc issue develop 1234`) in
place of a full team-prefixed identifier - it's resolved via the active
profile's team, then favorited teams, then a prompt across every team you
belong to.

==== Favorite teams/projects

Not in Ruby's `linear-cli` - favorite the teams/projects you actually
Expand Down
5 changes: 3 additions & 2 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ defmodule LinearCli.CLI.Commands do

with {:ok, project_id} <- resolve_project_id(options.project || Profiles.default_project()) do
input = %{
ids: ids,
ids: Enum.map(ids, &IssueHelpers.expand_issue_id/1),
mine: !flags.no_mine,
unassigned: flags.unassigned,
team_key: team_key,
Expand Down Expand Up @@ -440,7 +440,8 @@ defmodule LinearCli.CLI.Commands do
@spec issue_update(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_update(%{unknown: issue_ids, options: options, flags: flags}) do
with :ok <- validate_issue_ids(issue_ids),
{:ok, issues} <- Linear.issues(%{ids: issue_ids}) do
{:ok, issues} <-
Linear.issues(%{ids: Enum.map(issue_ids, &IssueHelpers.expand_issue_id/1)}) do
update_opts = [
comment: options.comment,
project: options.project,
Expand Down
64 changes: 56 additions & 8 deletions app/lib/linear_cli/cli/issue_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ defmodule LinearCli.CLI.IssueHelpers do
"""

alias LinearCli.CLI.{Projects, Prompt, WhatFor}
alias LinearCli.{Linear, Profiles}
alias LinearCli.{Favorites, Linear, Profiles}

# A "bare" issue id is just digits - anything with a `-` (an already
# team-prefixed identifier, e.g. "CRY-1234") or that otherwise doesn't
# look like an id at all (a UUID) passes through `expand_issue_id/1`
# unchanged.
@bare_issue_id_regex ~r/^\d+$/

@doc """
Adds a comment to `issue`, resolving `comment` (asking, or opening an
Expand Down Expand Up @@ -368,6 +374,42 @@ defmodule LinearCli.CLI.IssueHelpers do
defp maybe_put_project_id(params, nil), do: params
defp maybe_put_project_id(params, project), do: Map.put(params, :project_id, project.id)

@doc """
Expands a bare issue number (`~r/^\\d+$/`, e.g. `"1234"`) to a full
team-prefixed identifier (`"CRY-1234"`) by resolving a team key via
`resolve_bare_team/0`. Anything else (an already-prefixed identifier, a
UUID) is returned unchanged.

Team resolution order, never a hard error short of the user having no
teams at all: the active profile's team (`LinearCli.Profiles.default_team/0`)
-> favorited teams (`LinearCli.Favorites.list/1`, single favorite used
directly, several prompted) -> a prompt across every team the user
belongs to (`LinearCli.CLI.WhatFor.ask_for_team/0`).
"""
@spec expand_issue_id(String.t()) :: String.t()
def expand_issue_id(issue_id) do
if Regex.match?(@bare_issue_id_regex, issue_id) do
"#{resolve_bare_team()}-#{issue_id}"
else
issue_id
end
end

defp resolve_bare_team do
case Profiles.default_team() do
nil -> resolve_bare_team_from_favorites()
team_key -> team_key
end
end
Comment on lines +398 to +403

defp resolve_bare_team_from_favorites do
case Favorites.list("team") do
[] -> WhatFor.ask_for_team().key
[team_key] -> team_key
team_keys -> Prompt.select("Choose a team", Enum.map(team_keys, &{&1, &1}))
end
end

@doc """
Looks up `issue_id` and self-assigns it to the caller, unless it's already
assigned to them.
Expand All @@ -381,18 +423,24 @@ defmodule LinearCli.CLI.IssueHelpers do
"""
@spec gimme_da_issue!(String.t(), keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()}
def gimme_da_issue!(issue_id, opts \\ []) do
issue_id = expand_issue_id(issue_id)

with {:ok, me} <- resolve_me(opts),
{:ok, [issue]} <- Linear.issues(%{ids: [issue_id]}) do
if issue.assignee && issue.assignee.id == me.id do
Prompt.say("You are already assigned #{issue_id}")
{:ok, issue}
else
Prompt.say("Assigning issue #{issue_id} to ya")
Linear.assign_issue(issue, me.id)
end
assign_or_confirm(issue, me, issue_id)
end
end

defp assign_or_confirm(%{assignee: %{id: id}} = issue, %{id: id}, issue_id) do
Prompt.say("You are already assigned #{issue_id}")
{:ok, issue}
end

defp assign_or_confirm(issue, me, issue_id) do
Prompt.say("Assigning issue #{issue_id} to ya")
Linear.assign_issue(issue, me.id)
end

defp resolve_me(opts) do
case Keyword.fetch(opts, :me) do
{:ok, me} -> {:ok, me}
Expand Down
92 changes: 92 additions & 0 deletions app/test/linear_cli/cli/expand_issue_id_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule LinearCli.CLI.ExpandIssueIdTest do
# Not async: shares LinearCli.Profiles/LinearCli.Favorites' one sqlite
# file (config :linear_cli, :profiles_db_path) - see
# LinearCli.CLI.ProfileDefaultsTest's own comment for why this is safe.
use ExUnit.Case, async: false
import ExUnit.CaptureIO

alias LinearCli.CLI.IssueHelpers
alias LinearCli.{Favorites, Profiles}

setup do
path = Application.fetch_env!(:linear_cli, :profiles_db_path)
File.rm(path)
:ok
end

defp teams_response(teams) do
%{
"data" => %{
"viewer" => %{
"id" => "u1",
"name" => "Ada",
"email" => "ada@example.com",
"teams" => %{"nodes" => teams}
}
}
}
end

describe "expand_issue_id/1 (Phase 11: bare issue numbers)" do
test "a bare number resolves via the active profile's team, without prompting" do
{:ok, _} = Profiles.create("manhattan", team: "CRY")
:ok = Profiles.activate("manhattan")

assert capture_io(fn ->
assert IssueHelpers.expand_issue_id("1234") == "CRY-1234"
end) == ""
end

test "with no active profile but one favorited team, uses it directly, without prompting" do
Favorites.add("team", "ENG")

assert capture_io(fn ->
assert IssueHelpers.expand_issue_id("42") == "ENG-42"
end) == ""
end

test "with no active profile and several favorited teams, prompts and uses the selected one" do
Favorites.add("team", "ENG")
Favorites.add("team", "SUP")

output =
capture_io([input: "2\n"], fn ->
assert IssueHelpers.expand_issue_id("42") == "SUP-42"
end)

assert output =~ "Choose a team"
end

test "with no profile and no favorites, falls through to ask_for_team/0's own full prompt" do
Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(
conn,
teams_response([
%{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
%{"id" => "t2", "key" => "SUP", "name" => "Support"}
])
)
end)

output =
capture_io([input: "2\n"], fn ->
assert IssueHelpers.expand_issue_id("42") == "SUP-42"
end)

assert output =~ "Choose a team"
end

test "an already-prefixed id and a UUID pass through unchanged, regardless of profile/favorites" do
{:ok, _} = Profiles.create("manhattan", team: "CRY")
:ok = Profiles.activate("manhattan")
Favorites.add("team", "ENG")

assert capture_io(fn ->
assert IssueHelpers.expand_issue_id("CRY-1234") == "CRY-1234"

assert IssueHelpers.expand_issue_id("550e8400-e29b-41d4-a716-446655440000") ==
"550e8400-e29b-41d4-a716-446655440000"
end) == ""
end
end
end
Loading