diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index 4145e2a..be7d12f 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -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) diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index a191714..67d1531 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -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 @@ -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 @@ -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 @@ -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 @@ -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`. diff --git a/app/lib/linear_cli/cli/display.ex b/app/lib/linear_cli/cli/display.ex index 7c2842e..a1b7a0f 100644 --- a/app/lib/linear_cli/cli/display.ex +++ b/app/lib/linear_cli/cli/display.ex @@ -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. diff --git a/app/lib/linear_cli/cli/issue/actions.ex b/app/lib/linear_cli/cli/issue/actions.ex index c8ecb33..b4b325e 100644 --- a/app/lib/linear_cli/cli/issue/actions.ex +++ b/app/lib/linear_cli/cli/issue/actions.ex @@ -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`. @@ -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 @@ -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 @@ -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 diff --git a/app/lib/linear_cli/cli/issue/assignment.ex b/app/lib/linear_cli/cli/issue/assignment.ex index 4ad4afe..07b3be7 100644 --- a/app/lib/linear_cli/cli/issue/assignment.ex +++ b/app/lib/linear_cli/cli/issue/assignment.ex @@ -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` @@ -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} diff --git a/app/lib/linear_cli/cli/issue/creation.ex b/app/lib/linear_cli/cli/issue/creation.ex index 44fdda7..dab6f6b 100644 --- a/app/lib/linear_cli/cli/issue/creation.ex +++ b/app/lib/linear_cli/cli/issue/creation.ex @@ -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. @@ -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} diff --git a/app/lib/linear_cli/cli/issue/identifiers.ex b/app/lib/linear_cli/cli/issue/identifiers.ex index b5a025b..7669929 100644 --- a/app/lib/linear_cli/cli/issue/identifiers.ex +++ b/app/lib/linear_cli/cli/issue/identifiers.ex @@ -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. diff --git a/app/lib/linear_cli/cli/issue_helpers.ex b/app/lib/linear_cli/cli/issue/pull_request.ex similarity index 67% rename from app/lib/linear_cli/cli/issue_helpers.ex rename to app/lib/linear_cli/cli/issue/pull_request.ex index 5968eb1..1ce0cf5 100644 --- a/app/lib/linear_cli/cli/issue_helpers.ex +++ b/app/lib/linear_cli/cli/issue/pull_request.ex @@ -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` @@ -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:`. """ @@ -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. """ @@ -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`. diff --git a/app/lib/linear_cli/cli/issue/workflow_states.ex b/app/lib/linear_cli/cli/issue/workflow_states.ex index 739fb4a..6dd4830 100644 --- a/app/lib/linear_cli/cli/issue/workflow_states.ex +++ b/app/lib/linear_cli/cli/issue/workflow_states.ex @@ -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 @@ -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()} diff --git a/app/lib/linear_cli/rollover.ex b/app/lib/linear_cli/rollover.ex index 73cefca..9f85db6 100644 --- a/app/lib/linear_cli/rollover.ex +++ b/app/lib/linear_cli/rollover.ex @@ -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`. """ diff --git a/app/test/linear_cli/cli/issue_helpers_test.exs b/app/test/linear_cli/cli/issue/pull_request_test.exs similarity index 81% rename from app/test/linear_cli/cli/issue_helpers_test.exs rename to app/test/linear_cli/cli/issue/pull_request_test.exs index 6e3d872..5d1ffa1 100644 --- a/app/test/linear_cli/cli/issue_helpers_test.exs +++ b/app/test/linear_cli/cli/issue/pull_request_test.exs @@ -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 @@ -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 diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index 62a28eb..90cbcf7 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -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 ->