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: 18 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,24 @@ $ lcomment CRY-1234 CRY-3 <5>
$ lc issue update --close --reason "These were closable" CRY-1234 CRY-2
----

==== Default team/project (profiles)

Not in Ruby's `linear-cli` - save a named team/project bundle once, then
switch to it instead of passing `--team`/`--project` on every `issue
create`/`issue list`.

[source,sh]
----
$ lc profile create manhattan --team CRY --project Manhattan
$ lc profile use manhattan
$ lc profile list
$ lc profile show
$ lc profile delete manhattan
----

An explicit `--team`/`--project` on the command line always overrides the
active profile.

==== Post a project status update

Not in Ruby's `linear-cli` - a status post on a project (Linear's own "Project
Expand Down
5 changes: 5 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ app-*.tar
# Local dev SQLite db for Oban (daemon run mode only).
/oban_dev.db*

# Local SQLite db for LinearCli.Profiles (dev and test - unlike Oban's,
# needed by every interactive invocation, not just the daemon).
/profiles_dev.db*
/profiles_test.db*

16 changes: 16 additions & 0 deletions app/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,19 @@ config :linear_cli, LinearCli.ObanRepo.Postgres,
username: System.get_env("LINEAR_CLI_PG_USER", "postgres"),
password: System.get_env("LINEAR_CLI_PG_PASSWORD", ""),
database: System.get_env("LINEAR_CLI_PG_DATABASE", default_pg_database)

# LinearCli.Profiles' own SQLite file - separate from ObanRepo's above
# (different concern, needed by every interactive invocation, not just the
# daemon). Unlike ObanRepo, there's no pooled Ecto.Repo behind this -
# LinearCli.Profiles opens/closes its own connection per call - so :test
# can't use ":memory:" the way ObanRepo's test config does: an in-memory
# database is wiped the moment that connection closes, which would be
# every single call.
default_profiles_path =
case config_env() do
:test -> Path.expand("../profiles_test.db", __DIR__)
:dev -> Path.expand("../profiles_dev.db", __DIR__)
:prod -> Path.join(System.user_home!(), ".linear_cli/profiles.db")
end

config :linear_cli, :profiles_db_path, default_profiles_path
44 changes: 43 additions & 1 deletion app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ defmodule LinearCli.CLI do
"pull-request" => "pr"
},
"team" => %{"l" => "list", "ls" => "list"},
"project" => %{"l" => "list", "ls" => "list"}
"project" => %{"l" => "list", "ls" => "list"},
"profile" => %{"l" => "list", "ls" => "list"}
}

@doc false
Expand Down Expand Up @@ -166,6 +167,16 @@ defmodule LinearCli.CLI do
defp dispatch([:project, :update], result, halt),
do: run(&Commands.project_update/1, result, halt)

defp dispatch([:profile, :create], result, halt),
do: run(&Commands.profile_create/1, result, halt)

defp dispatch([:profile, :list], result, halt), do: run(&Commands.profile_list/1, result, halt)
defp dispatch([:profile, :use], result, halt), do: run(&Commands.profile_use/1, result, halt)
defp dispatch([:profile, :show], result, halt), do: run(&Commands.profile_show/1, result, halt)

defp dispatch([:profile, :delete], result, halt),
do: run(&Commands.profile_delete/1, result, halt)

defp dispatch([:issue, :list], result, halt), do: run(&Commands.issue_list/1, result, halt)
defp dispatch([:issue, :create], result, halt), do: run(&Commands.issue_create/1, result, halt)

Expand Down Expand Up @@ -374,6 +385,37 @@ defmodule LinearCli.CLI do
]
]
],
profile: [
name: "profile",
about: "Manage saved team/project profiles",
subcommands: [
create: [
name: "create",
about: "Save a new profile",
args: [name: [value_name: "NAME", help: "Profile name", required: true]],
options: [
team: [short: "-t", long: "--team", help: "Default team for this profile"],
project: [
short: "-p",
long: "--project",
help: "Default project for this profile"
]
]
],
list: [name: "list", about: "List saved profiles"],
use: [
name: "use",
about: "Switch to a saved profile",
args: [name: [value_name: "NAME", help: "Profile name", required: true]]
],
show: [name: "show", about: "Show the active profile"],
delete: [
name: "delete",
about: "Delete a saved profile",
args: [name: [value_name: "NAME", help: "Profile name", required: true]]
]
]
],
issue: [
name: "issue",
about: "Manage issues",
Expand Down
74 changes: 69 additions & 5 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule LinearCli.CLI.Commands do
"""

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

@doc "Ported from commands/whoami.rb."
def whoami(%{flags: flags, options: options}) do
Expand Down Expand Up @@ -79,23 +79,87 @@ defmodule LinearCli.CLI.Commands do
end
end

@doc """
New in this port - Ruby has no equivalent. Saves a new named
team/project bundle (`LinearCli.Profiles.create/2`) that `profile use`
can later switch to.
"""
def profile_create(%{args: %{name: name}, options: options}) do
case Profiles.create(name, team: options.team, project: options.project) do
{:ok, profile} ->
Display.show(profile, %{output: options.output})
:ok

{:error, reason} ->
{:error, reason}
end
end

@doc "New in this port - Ruby has no equivalent. Lists every saved profile."
def profile_list(%{options: options}) do
Display.show(Profiles.list(), %{output: options.output})
:ok
end

@doc """
New in this port - Ruby has no equivalent. Switches the active profile -
its team/project become the defaults `issue create`/`issue list` fall
back to when `--team`/`--project` are omitted.
"""
def profile_use(%{args: %{name: name}}) do
case Profiles.activate(name) do
:ok ->
Prompt.ok("Switched to profile #{name}")
:ok

{:error, :not_found} ->
{:error, {:smells_bad, "No profile named #{name}"}}
end
end

@doc "New in this port - Ruby has no equivalent. Shows the active profile, if any."
def profile_show(%{options: options}) do
case Profiles.active() do
nil -> Prompt.warn("No active profile")
profile -> Display.show(profile, %{output: options.output})
end

:ok
end

@doc "New in this port - Ruby has no equivalent. Deletes a saved profile."
def profile_delete(%{args: %{name: name}}) do
case Profiles.delete(name) do
:ok ->
Prompt.ok("Deleted profile #{name}")
:ok

{:error, :not_found} ->
{:error, {:smells_bad, "No profile named #{name}"}}
end
end

@doc """
Ported from commands/issue/list.rb + operations/issue/list.rb.

`--project`/`-p` is resolved the same way Ruby's `CLI::Projects#project_for`
does - against every project in the workspace (`Project.all`, not
team-scoped), prompting interactively when the search is ambiguous or
omitted-but-requested (`-p -`). Only resolved at all when `--project` was
actually given - unlike `issue create`/`issue update`, a bare `issue list`
applies no project filter and never prompts.
actually given (or `LinearCli.Profiles.default_project/0` supplies one) -
unlike `issue create`/`issue update`, a bare `issue list` with no active
profile applies no project filter and never prompts. `--team`/`--project`
passed explicitly always win over the active profile.
"""
def issue_list(%{flags: flags, options: options, unknown: ids}) do
with {:ok, project_id} <- resolve_project_id(options.project) do
team_key = options.team || Profiles.default_team()

with {:ok, project_id} <- resolve_project_id(options.project || Profiles.default_project()) do
input = %{
ids: ids,
mine: !flags.no_mine,
unassigned: flags.unassigned,
team_key: options.team,
team_key: team_key,
project_id: project_id
}

Expand Down
9 changes: 9 additions & 0 deletions app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule LinearCli.CLI.Display do
"""

alias LinearCli.Linear.{Issue, Project, ProjectUpdate, Team, User}
alias LinearCli.Profiles.Profile

@ash_internal_fields ~w(__meta__ __metadata__ __order__ __lateral_join_source__ aggregates calculations)a

Expand Down Expand Up @@ -36,6 +37,14 @@ defmodule LinearCli.CLI.Display do
IO.puts("Posted#{health}: #{update.url}")
end

defp puts_text(%Profile{} = profile, _opts) do
marker = if profile.active, do: "* ", else: " "

IO.puts(
"#{marker}#{String.pad_trailing(profile.name, 12)} team=#{profile.team || "-"} project=#{profile.project || "-"}"
)
end

defp puts_text(%User{} = user, opts) do
IO.puts(user_line(user, opts))
end
Expand Down
11 changes: 7 additions & 4 deletions app/lib/linear_cli/cli/issue_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ defmodule LinearCli.CLI.IssueHelpers do
"""

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

@doc """
Adds a comment to `issue`, resolving `comment` (asking, or opening an
Expand Down Expand Up @@ -342,19 +342,22 @@ defmodule LinearCli.CLI.IssueHelpers do
`LinearCli.CLI.WhatFor`/`LinearCli.CLI.Projects`).

`opts` (Ruby's `**options`): `:title`, `:description`, `:team`, `:labels`,
`:project`.
`:project`. `:team`/`:project`, if omitted, fall back to
`LinearCli.Profiles.default_team/0`/`default_project/0` (the active
profile, if any) before `WhatFor.team_for/1`/`Projects.project_for/2`'s
own interactive prompting kicks in.

Ported from `CLI::Issue#make_da_issue!`.
"""
@spec make_da_issue!(keyword()) :: {:ok, %Linear.Issue{}} | {:error, term()}
def make_da_issue!(opts \\ []) do
title = WhatFor.title_for(opts[:title])
description = WhatFor.description_for(opts[:description])
team = WhatFor.team_for(opts[:team])
team = WhatFor.team_for(opts[:team] || Profiles.default_team())
labels = WhatFor.labels_for(team, opts[:labels])

with {:ok, projects} <- Linear.projects_by_team(team.id) do
project = Projects.project_for(projects, opts[:project])
project = Projects.project_for(projects, opts[:project] || Profiles.default_project())
label_ids = Enum.map(labels, & &1.id)
params = maybe_put_project_id(%{label_ids: label_ids}, project)

Expand Down
Loading