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
2 changes: 1 addition & 1 deletion app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ defmodule LinearCli.CLI do
end

# Ported from CLI::Caller#call's `rescue SmellsBad` clause. See
# `LinearCli.CLI.IssueHelpers`'s moduledoc for where this tagged tuple
# `LinearCli.CLI.Issue.Actions`'s moduledoc for where this tagged tuple
# comes from.
defp handle_error({:smells_bad, message}, debug, halt) do
IO.puts(:stderr, message)
Expand Down
12 changes: 6 additions & 6 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ defmodule LinearCli.CLI.Commands do
"""

alias LinearCli.Browser
alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt, WhatFor}
alias LinearCli.CLI.Issue.{Actions, Assignment, Creation, Identifiers}
alias LinearCli.CLI.{Display, Projects, Prompt, WhatFor}
alias LinearCli.CLI.Issue.{Actions, Assignment, Creation, Identifiers, PullRequest}
alias LinearCli.{Git, Linear, Profiles}

@max_concurrent_issue_updates 20
Expand Down Expand Up @@ -224,7 +224,7 @@ defmodule LinearCli.CLI.Commands do
Ported from commands/issue/pr.rb: resolves/self-assigns `issue_id`, checks
out its branch (creating it first if needed - no pull/push here, unlike
`issue_develop/2`), then opens a PR via
`LinearCli.CLI.IssueHelpers.issue_pr/2`.
`LinearCli.CLI.Issue.PullRequest.issue_pr/2`.

`opts` (this port's addition): `:cwd` (forwarded to
`LinearCli.Git.checkout_branch/2`), `:me` (forwarded to
Expand All @@ -243,7 +243,7 @@ defmodule LinearCli.CLI.Commands do
[title: options.title, description: options.description]
|> maybe_put(:runner, opts[:runner])

IssueHelpers.issue_pr(issue, pr_opts)
PullRequest.issue_pr(issue, pr_opts)
end
end

Expand Down Expand Up @@ -304,7 +304,7 @@ defmodule LinearCli.CLI.Commands do
Ported from commands/issue/update.rb: looks up every issue id in `unknown`
(see `issue_take/2`'s doc for why this is a variadic positional captured
via `unknown` rather than a declared Optimus arg) and dispatches
`LinearCli.CLI.IssueHelpers.update_issue/2` against each, per whichever
`LinearCli.CLI.Issue.Actions.update_issue/2` against each, per whichever
flags/options were given.

Ports `raise SmellsBad, 'No issue IDs provided!' if issue_ids.empty?` as
Expand Down Expand Up @@ -358,7 +358,7 @@ defmodule LinearCli.CLI.Commands do
`--body-file` is given) uses the first issue's context.

Calls `Linear.add_comment/2` directly rather than
`LinearCli.CLI.IssueHelpers.issue_comment/2` so the confirmation can be
`LinearCli.CLI.Issue.Actions.issue_comment/2` so the confirmation can be
suppressed under `--output json` - matching how `print_move_results/3`
suppresses its own confirmation for `issue move --output json`.

Expand Down
2 changes: 1 addition & 1 deletion app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ defmodule LinearCli.CLI.Display do
end

# New in this port - Ruby has no equivalent (no bare `Comment` command
# existed to display one). `LinearCli.CLI.IssueHelpers.issue_comment/2`/
# existed to display one). `LinearCli.CLI.Issue.Actions.issue_comment/2`/
# `upsert_comment/4` already print a "Comment added to.../updated on..."
# confirmation via `Prompt.ok/1` before this runs, so this only needs to
# add the one thing that isn't in that line: a link to the comment.
Expand Down
13 changes: 6 additions & 7 deletions app/lib/linear_cli/cli/issue/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule LinearCli.CLI.Issue.Actions do
Issue lifecycle mutations — comment, close/cancel, description update,
project attachment/move, and update-dispatch — for an already-loaded issue.

Extracted from `LinearCli.CLI.IssueHelpers`. Ported originally from
Extracted from the former `LinearCli.CLI.IssueHelpers`. Ported originally from
`Rubyists::Linear::CLI::Issue`
(vendor/ruby-linear-cli/lib/linear/commands/issue.rb): `issue_comment`,
`cancel_issue`, `close_issue`, `attach_project`, `update_issue`.
Expand Down Expand Up @@ -31,12 +31,11 @@ defmodule LinearCli.CLI.Issue.Actions do

## PR dispatch

`update_issue/2` dispatches to `LinearCli.CLI.IssueHelpers.issue_pr/2` for
the `:pr` option until that function is extracted in a later phase.
`update_issue/2` dispatches to `LinearCli.CLI.Issue.PullRequest.issue_pr/2`
for the `:pr` option.
"""

alias LinearCli.CLI.Issue.WorkflowStates
alias LinearCli.CLI.IssueHelpers
alias LinearCli.CLI.Issue.{PullRequest, WorkflowStates}
alias LinearCli.CLI.{Projects, Prompt, WhatFor}
alias LinearCli.Linear

Expand Down Expand Up @@ -205,7 +204,7 @@ defmodule LinearCli.CLI.Issue.Actions do
regardless of anything else
2. `:close` -> `close_issue/2`
3. `:cancel` -> `cancel_issue/2`
4. `:pr` -> `LinearCli.CLI.IssueHelpers.issue_pr/2`
4. `:pr` -> `LinearCli.CLI.Issue.PullRequest.issue_pr/2`
5. `:project` -> `attach_project/2`
6. `:description` -> `update_description/2`
7. otherwise, if only `:comment` was given, stop silently
Expand Down Expand Up @@ -236,7 +235,7 @@ defmodule LinearCli.CLI.Issue.Actions do
cond do
opts[:close] -> normalize(close_issue(issue, opts))
opts[:cancel] -> normalize(cancel_issue(issue, opts))
opts[:pr] -> IssueHelpers.issue_pr(issue, opts)
opts[:pr] -> PullRequest.issue_pr(issue, opts)
opts[:project] -> normalize(attach_project(issue, opts[:project]))
opts[:description] -> normalize(update_description(issue, opts[:description]))
opts[:comment] -> :ok
Expand Down
13 changes: 6 additions & 7 deletions app/lib/linear_cli/cli/issue/assignment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ defmodule LinearCli.CLI.Issue.Assignment do
@moduledoc """
Issue self-assignment with optional workflow-state transition.

Extracted from `LinearCli.CLI.IssueHelpers`. The single public function,
`gimme_da_issue!/2`, looks up an issue by identifier and self-assigns it
to the current user, unless already assigned. Accepts an optional
`--status` option (or an already-resolved `:state_id`) to simultaneously
transition the issue's workflow state.
Extracted from the former `LinearCli.CLI.IssueHelpers`. The single public
function, `gimme_da_issue!/2`, looks up an issue by identifier and
self-assigns it to the current user, unless already assigned. Accepts an
optional `--status` option (or an already-resolved `:state_id`) to
simultaneously transition the issue's workflow state.

Reuses `LinearCli.CLI.Issue.Identifiers.expand_issue_id/1` for bare-ID
expansion and `LinearCli.CLI.Issue.WorkflowStates.resolve_workflow_state/2`
Expand All @@ -17,8 +17,7 @@ defmodule LinearCli.CLI.Issue.Assignment do
## Return convention

Returns `{:ok, issue}` on success or `{:error, reason}` on failure (never
raises). User-visible failures use `{:error, {:smells_bad, message}}`,
matching the convention established by `LinearCli.CLI.IssueHelpers`.
raises). User-visible failures use `{:error, {:smells_bad, message}}`.
"""

alias LinearCli.CLI.Issue.{Identifiers, WorkflowStates}
Expand Down
12 changes: 6 additions & 6 deletions app/lib/linear_cli/cli/issue/creation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ defmodule LinearCli.CLI.Issue.Creation do
@moduledoc """
Interactive and non-interactive issue creation.

Extracted from `LinearCli.CLI.IssueHelpers`. The single public function,
`make_da_issue!/1`, creates a new Linear issue by resolving title,
description, team, labels, and project either interactively (when `--yes`
is not given) or strictly from provided options (when `--yes` is set).
Extracted from the former `LinearCli.CLI.IssueHelpers`. The single public
function, `make_da_issue!/1`, creates a new Linear issue by resolving
title, description, team, labels, and project either interactively (when
`--yes` is not given) or strictly from provided options (when `--yes` is
set).

Profile defaults (active team/project) are consulted before interactive
prompting when options are omitted.
Expand All @@ -15,8 +16,7 @@ defmodule LinearCli.CLI.Issue.Creation do
## Return convention

Returns `{:ok, issue}` on success or `{:error, reason}` on failure (never
raises). User-visible failures use `{:error, {:smells_bad, message}}`,
matching the convention established by `LinearCli.CLI.IssueHelpers`.
raises). User-visible failures use `{:error, {:smells_bad, message}}`.
"""

alias LinearCli.CLI.{Projects, WhatFor}
Expand Down
2 changes: 1 addition & 1 deletion app/lib/linear_cli/cli/issue/identifiers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule LinearCli.CLI.Issue.Identifiers do
Bare-issue-ID expansion: turns a plain integer string (e.g. `"1234"`) into
a team-prefixed identifier (`"CRY-1234"`) by resolving a team key.

Extracted from `LinearCli.CLI.IssueHelpers`. The single public function,
Extracted from the former `LinearCli.CLI.IssueHelpers`. The single public function,
`expand_issue_id/1`, is called by every command that accepts an issue
identifier from the user so that bare numbers work wherever full identifiers
do.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
defmodule LinearCli.CLI.IssueHelpers do
defmodule LinearCli.CLI.Issue.PullRequest do
@moduledoc """
Shared issue-command helpers - open a PR.
PR-creation workflow for an already-loaded issue: resolve a title/description
and shell out to `gh pr create`.

Ported from `Rubyists::Linear::CLI::Issue`
Extracted from the former `LinearCli.CLI.IssueHelpers`. Ported from
`Rubyists::Linear::CLI::Issue`
(vendor/ruby-linear-cli/lib/linear/commands/issue.rb): `create_pr!`,
`issue_pr`.

Lifecycle mutations (comment, close/cancel, description update, project
attachment/move, and update-dispatch) have been extracted to
`LinearCli.CLI.Issue.Actions`. Bare-ID expansion lives in
`LinearCli.CLI.Issue.Identifiers`. Workflow-state selection lives in
`LinearCli.CLI.Issue.WorkflowStates`. Issue creation lives in
`LinearCli.CLI.Issue.Creation`. Self-assignment lives in
`LinearCli.CLI.Issue.Assignment`.

## Return convention

Every function here returns `{:ok, result}` or `{:error, reason}` (never
raises).

`reason` is either whatever `LinearCli.Api`/an Ash manual action already
surfaces (a transport/GraphQL/validation error - a genuine system
failure), or a tagged tuple for "the user gave us something we can't act
on, tell them clearly" cases, mirroring Ruby's `SmellsBad` exception:

{:error, {:smells_bad, message}}

where `message` is a human-readable `String.t()`.
`create_pr!/3` returns a `String.t()` (the runner's stdout — typically the
new PR's URL). `issue_pr/2` always returns `:ok`.

## `create_pr!/3`

Expand All @@ -37,7 +22,7 @@ defmodule LinearCli.CLI.IssueHelpers do
why it never returns a Ruby-style `Tempfile` handle here), so only the
`--body` shape applies. Takes an injectable `runner` (a `(title, body) ->
String.t()` function), defaulting to a real `System.cmd/3` call, so tests
never actually shell out to a real `gh` - the same pattern this codebase
never actually shell out to a real `gh` the same pattern this codebase
already uses for `LinearCli.CLI.main/2`'s injectable `halt` and
`LinearCli.Git`'s injectable `cwd:`.
"""
Expand All @@ -47,11 +32,11 @@ defmodule LinearCli.CLI.IssueHelpers do

@doc """
Shells out to `gh pr create -a @me --title TITLE --body BODY`, returning
whatever the command printed to stdout (Ruby's backtick-captured output -
whatever the command printed to stdout (Ruby's backtick-captured output
typically the created PR's URL).

`runner`, a `(title, body) -> String.t()` function, defaults to a real
`System.cmd/3` call - pass an override in tests. Ported from
`System.cmd/3` call pass an override in tests. Ported from
`CLI::Issue#create_pr!`; see this module's moduledoc for why only the
`--body` (never `--body-file`) shape applies here.
"""
Expand All @@ -76,7 +61,7 @@ defmodule LinearCli.CLI.IssueHelpers do
already given in `opts`), then runs `create_pr!/3` and prints its output.

`opts`: `:title`, `:description` (Ruby's implicit `options[:title]`/
`options[:description]` - note Ruby's own `update_issue` never actually
`options[:description]` note Ruby's own `update_issue` never actually
passes either through, always calling `issue_pr(issue)` bare, so both are
ported for signature fidelity but are effectively always prompted for in
practice); `:runner`, this port's addition, forwarded to `create_pr!/3`.
Expand Down
15 changes: 7 additions & 8 deletions app/lib/linear_cli/cli/issue/workflow_states.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ defmodule LinearCli.CLI.Issue.WorkflowStates do
Workflow-state selection and status-name matching for issue lifecycle
commands.

Extracted from `LinearCli.CLI.IssueHelpers`. Provides two entry points for
type-filtered state selection (`cancelled_state_for/2`,
Extracted from the former `LinearCli.CLI.IssueHelpers`. Provides two entry
points for type-filtered state selection (`cancelled_state_for/2`,
`completed_state_for/2`) and one shared entry point for arbitrary
name/prefix matching (`resolve_workflow_state/2`), which is also called
directly by `LinearCli.CLI.IssueHelpers.gimme_da_issue!/2` to resolve the
`--status` option without duplicating the matching logic.
directly by `LinearCli.CLI.Issue.Assignment.gimme_da_issue!/2` to resolve
the `--status` option without duplicating the matching logic.

## Return shapes

All public functions return `{:ok, result} | {:error, term()}`.
`{:error, {:smells_bad, message}}` is returned for user-visible failures
(no matching state, ambiguous prefix, unknown status name) — the same
tagged-tuple convention as `LinearCli.CLI.IssueHelpers`.
(no matching state, ambiguous prefix, unknown status name).

## State selection / prompt behavior

Expand Down Expand Up @@ -74,8 +73,8 @@ defmodule LinearCli.CLI.Issue.WorkflowStates do
multiple prefix matches (ambiguous status).

Public so that callers outside this module (e.g.
`LinearCli.CLI.IssueHelpers.gimme_da_issue!/2` resolving `--status`) can
use the same matching logic without duplicating it.
`LinearCli.CLI.Issue.Assignment.gimme_da_issue!/2` resolving `--status`)
can use the same matching logic without duplicating it.
"""
@spec resolve_workflow_state([%Linear.WorkflowState{}], String.t()) ::
{:ok, %Linear.WorkflowState{}} | {:error, term()}
Expand Down
2 changes: 1 addition & 1 deletion app/lib/linear_cli/rollover.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule LinearCli.Rollover do

Pure logic, no `Oban.Worker` behaviour - kept directly callable/testable,
same "thin OTP boundary, pure logic underneath" split as
`LinearCli.Git`/`LinearCli.CLI.IssueHelpers`. `LinearCli.Rollover.Worker`
`LinearCli.Git`/`LinearCli.CLI.Issue.Actions`. `LinearCli.Rollover.Worker`
is the thin Oban wrapper that calls `run/2`.
"""

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule LinearCli.CLI.IssueHelpersTest do
defmodule LinearCli.CLI.Issue.PullRequestTest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO

alias LinearCli.CLI.IssueHelpers
alias LinearCli.CLI.Issue.PullRequest
alias LinearCli.Linear.{Issue, Team}

defp issue(attrs \\ %{}) do
Expand All @@ -21,14 +21,14 @@ defmodule LinearCli.CLI.IssueHelpersTest do
describe "create_pr!/3 and issue_pr/2" do
test "create_pr!/3 forwards to the injectable runner" do
runner = fn title, body -> "ran with #{title}/#{body}" end
assert IssueHelpers.create_pr!("My title", "My body", runner) == "ran with My title/My body"
assert PullRequest.create_pr!("My title", "My body", runner) == "ran with My title/My body"
end

test "issue_pr/2 resolves title/description then prints the runner's output as a warning" do
output =
capture_io(fn ->
assert :ok =
IssueHelpers.issue_pr(issue(),
PullRequest.issue_pr(issue(),
title: "fix: CRY-1 - Fix the thing",
description: "body",
runner: fn title, body -> "gh said: #{title} (#{body})" end
Expand Down
2 changes: 1 addition & 1 deletion app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule LinearCli.CLI.IssueCommandsTest do

# Dispatches to one of `pairs` ({substring, response_map}) based on which
# substring appears in the outgoing GraphQL document - see
# `LinearCli.CLI.IssueHelpersTest`'s own `stub_responses/1` for why one
# `LinearCli.CLI.Issue.ActionsTest`'s own `stub_responses/1` for why one
# stub per test is enough to drive an entire multi-call flow.
defp stub_responses(pairs) do
Req.Test.stub(LinearCli.Api, fn conn ->
Expand Down