@@ -7,6 +7,8 @@ defmodule LinearCli.CLI.Commands do
77 alias LinearCli.CLI . { Display , IssueHelpers , Projects , Prompt , WhatFor }
88 alias LinearCli . { Favorites , Git , Linear , Profiles }
99
10+ @ max_concurrent_issue_updates 20
11+
1012 @ doc "Ported from commands/whoami.rb."
1113 def whoami ( % { flags: flags , options: options } ) do
1214 with { :ok , user } <- Linear . me ( ) do
@@ -518,32 +520,91 @@ defmodule LinearCli.CLI.Commands do
518520 defp validate_issue_ids ( _issue_ids ) , do: :ok
519521
520522 @ doc """
521- Changes the workflow state of an issue.
523+ Changes the workflow state of one or more issues. Optimus captures the IDs in
524+ `unknown`, since it has no variadic positional-argument type.
522525
523526 With `--status`/`-s`, matches the given name against the issue's team's
524527 workflow states (case-insensitive exact, then unique prefix). Without it,
525528 prompts interactively via `LinearCli.CLI.Prompt.select/2`.
526529
527- With `--comment`/`-m`, adds a comment to the issue before transitioning.
530+ With `--comment`/`-m`, adds a comment to each issue before transitioning it.
531+ Mutations for separate issues run concurrently with a limit of 20 in flight.
528532 """
529533 @ spec issue_status ( Optimus.ParseResult . t ( ) ) :: :ok | { :error , term ( ) }
530- def issue_status ( % { args: % { issue_id: issue_id } , options: options } ) do
531- expanded_id = IssueHelpers . expand_issue_id ( issue_id )
534+ def issue_status ( % { unknown: issue_ids , options: options } ) do
535+ with :ok <- validate_issue_ids ( issue_ids ) ,
536+ { :ok , issues } <-
537+ Linear . issues ( % { ids: Enum . map ( issue_ids , & IssueHelpers . expand_issue_id / 1 ) } ) ,
538+ { :ok , planned_updates } <- plan_status_updates ( issues , options . status ) ,
539+ { :ok , completed_updates } <- apply_status_updates ( planned_updates , options . comment ) do
540+ show_status_updates ( completed_updates , options . output )
541+ end
542+ end
532543
533- with { :ok , [ issue ] } <- Linear . issues ( % { ids: [ expanded_id ] } ) ,
534- { :ok , states } <- Linear . workflow_states_by_team ( issue . team . id ) ,
535- { :ok , target_state } <- resolve_target_state ( states , options . status ) ,
536- :ok <- maybe_add_status_comment ( issue , options . comment ) ,
544+ defp plan_status_updates ( issues , status ) do
545+ issues
546+ |> Enum . reduce_while ( { :ok , [ ] } , fn issue , { :ok , updates } ->
547+ with { :ok , states } <- Linear . workflow_states_by_team ( issue . team . id ) ,
548+ { :ok , target_state } <- resolve_target_state ( states , status ) do
549+ { :cont , { :ok , [ { issue , target_state } | updates ] } }
550+ else
551+ { :error , reason } -> { :halt , { :error , reason } }
552+ end
553+ end )
554+ |> reverse_status_updates ( )
555+ end
556+
557+ defp apply_status_updates ( [ ] , _comment ) , do: { :ok , [ ] }
558+
559+ defp apply_status_updates ( planned_updates , comment ) do
560+ planned_updates
561+ |> Task . async_stream (
562+ fn { issue , target_state } ->
563+ apply_status_update ( issue , target_state , comment )
564+ end ,
565+ max_concurrency: min ( length ( planned_updates ) , @ max_concurrent_issue_updates ) ,
566+ ordered: true ,
567+ timeout: 30_000
568+ )
569+ |> Enum . reduce_while ( { :ok , [ ] } , fn
570+ { :ok , { :ok , update } } , { :ok , updates } ->
571+ { :cont , { :ok , [ update | updates ] } }
572+
573+ { :ok , { :error , reason } } , { :ok , _updates } ->
574+ { :halt , { :error , reason } }
575+
576+ { :exit , reason } , { :ok , _updates } ->
577+ { :halt , { :error , { :task_exit , reason } } }
578+ end )
579+ |> reverse_status_updates ( )
580+ end
581+
582+ defp apply_status_update ( issue , target_state , comment ) do
583+ with :ok <- maybe_add_status_comment ( issue , comment ) ,
537584 { :ok , updated } <- Linear . set_issue_status ( issue , target_state . id ) do
538- Display . show ( updated , % { output: options . output } )
585+ { :ok , { updated , target_state } }
586+ end
587+ end
539588
540- if options . output != "json" ,
541- do: Prompt . ok ( " #{ updated . identifier } status set to #{ target_state . name } " )
589+ defp reverse_status_updates ( { :ok , updates } ) , do: { :ok , Enum . reverse ( updates ) }
590+ defp reverse_status_updates ( error ) , do: error
542591
543- :ok
592+ defp show_status_updates ( completed_updates , output ) do
593+ updated_issues = Enum . map ( completed_updates , & elem ( & 1 , 0 ) )
594+ Display . show ( one_or_many ( updated_issues ) , % { output: output } )
595+
596+ if output != "json" do
597+ Enum . each ( completed_updates , fn { updated , target_state } ->
598+ Prompt . ok ( "#{ updated . identifier } status set to #{ target_state . name } " )
599+ end )
544600 end
601+
602+ :ok
545603 end
546604
605+ defp one_or_many ( [ one ] ) , do: one
606+ defp one_or_many ( many ) , do: many
607+
547608 defp resolve_target_state ( states , nil ) do
548609 choices = Enum . sort_by ( states , & & 1 . position ) |> Enum . map ( & { & 1 . name , & 1 } )
549610 { :ok , Prompt . select ( "Choose a status" , choices ) }
0 commit comments