233 lines
6.4 KiB
Go
233 lines
6.4 KiB
Go
package view
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/internal/ghinstance"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
|
|
"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
|
|
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const defaultLimit = 40
|
|
|
|
type ViewOptions struct {
|
|
IO *iostreams.IOStreams
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
CapiClient func() (capi.CapiClient, error)
|
|
HttpClient func() (*http.Client, error)
|
|
Finder prShared.PRFinder
|
|
Prompter prompter.Prompter
|
|
|
|
SelectorArg string
|
|
PRNumber int
|
|
SessionID string
|
|
}
|
|
|
|
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
|
opts := &ViewOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
CapiClient: shared.CapiClientFunc(f),
|
|
Prompter: f.Prompter,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "view [<session-id> | <pr-number> | <pr-url> | <pr-branch>]",
|
|
Short: "View an agent task session",
|
|
Long: heredoc.Doc(`
|
|
View an agent task session.
|
|
`),
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// Support -R/--repo override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
if len(args) > 0 {
|
|
opts.SelectorArg = args[0]
|
|
if shared.IsSessionID(opts.SelectorArg) {
|
|
opts.SessionID = opts.SelectorArg
|
|
}
|
|
}
|
|
|
|
if opts.SessionID == "" && !opts.IO.CanPrompt() {
|
|
return fmt.Errorf("session ID is required when not running interactively")
|
|
}
|
|
|
|
if opts.Finder == nil {
|
|
opts.Finder = prShared.NewFinder(f)
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return viewRun(opts)
|
|
},
|
|
}
|
|
|
|
cmdutil.EnableRepoOverride(cmd, f)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func viewRun(opts *ViewOptions) error {
|
|
capiClient, err := opts.CapiClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cs := opts.IO.ColorScheme()
|
|
|
|
opts.IO.StartProgressIndicatorWithLabel("Fetching agent session...")
|
|
defer opts.IO.StopProgressIndicator()
|
|
|
|
var session *capi.Session
|
|
|
|
if opts.SessionID != "" {
|
|
if sess, err := capiClient.GetSession(ctx, opts.SessionID); err != nil {
|
|
if errors.Is(err, capi.ErrSessionNotFound) {
|
|
fmt.Fprintln(opts.IO.ErrOut, "session not found")
|
|
return cmdutil.SilentError
|
|
}
|
|
return err
|
|
} else {
|
|
session = sess
|
|
}
|
|
} else {
|
|
var resourceID int64
|
|
var prURL string
|
|
|
|
if opts.SelectorArg != "" {
|
|
// Finder does not support the PR/issue reference format (e.g. owner/repo#123)
|
|
// so we need to check if the selector arg is a reference and fetch the PR
|
|
// directly.
|
|
if repo, num, err := prShared.ParseFullReference(opts.SelectorArg); err == nil {
|
|
// Since the selector was a reference (i.e. without hostname data), we need to
|
|
// check the base repo to get the hostname.
|
|
baseRepo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostname := baseRepo.RepoHost()
|
|
if hostname != ghinstance.Default() {
|
|
return fmt.Errorf("agent tasks are not supported on this host: %s", hostname)
|
|
}
|
|
|
|
resourceID, prURL, err = capiClient.GetPullRequestDatabaseID(ctx, hostname, repo.RepoOwner(), repo.RepoName(), num)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch pull request: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if resourceID == 0 {
|
|
findOptions := prShared.FindOptions{
|
|
Selector: opts.SelectorArg,
|
|
Fields: []string{"id", "url", "fullDatabaseId"},
|
|
}
|
|
|
|
pr, repo, err := opts.Finder.Find(findOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if repo.RepoHost() != ghinstance.Default() {
|
|
return fmt.Errorf("agent tasks are not supported on this host: %s", repo.RepoHost())
|
|
}
|
|
|
|
databaseID, err := strconv.ParseInt(pr.FullDatabaseID, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse pull request: %w", err)
|
|
}
|
|
|
|
resourceID = databaseID
|
|
prURL = pr.URL
|
|
}
|
|
|
|
// TODO(babakks): currently we just fetch a pre-defined number of
|
|
// matching sessions to avoid hitting the API too many times, but it's
|
|
// technically possible for a PR to be associated with lots of sessions
|
|
// (i.e. above our selected limit).
|
|
sessions, err := capiClient.ListSessionsByResourceID(ctx, "pull", resourceID, defaultLimit)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list sessions for pull request: %w", err)
|
|
}
|
|
|
|
if len(sessions) == 0 {
|
|
fmt.Fprintln(opts.IO.ErrOut, "no session found for pull request")
|
|
return cmdutil.SilentError
|
|
}
|
|
|
|
session = sessions[0]
|
|
if len(sessions) > 1 {
|
|
now := time.Now()
|
|
options := make([]string, 0, len(sessions))
|
|
for _, session := range sessions {
|
|
options = append(options, fmt.Sprintf(
|
|
"%s %s • %s",
|
|
shared.SessionSymbol(cs, session.State),
|
|
session.Name,
|
|
text.FuzzyAgo(now, session.CreatedAt),
|
|
))
|
|
}
|
|
|
|
opts.IO.StopProgressIndicator()
|
|
selected, err := opts.Prompter.Select("Select a session", "", options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
session = sessions[selected]
|
|
}
|
|
}
|
|
|
|
opts.IO.StopProgressIndicator()
|
|
|
|
out := opts.IO.Out
|
|
|
|
if session.PullRequest != nil {
|
|
fmt.Fprintf(out, "%s • %s • %s%s\n",
|
|
shared.ColorFuncForSessionState(*session, cs)(shared.SessionStateString(session.State)),
|
|
cs.Bold(session.PullRequest.Title),
|
|
session.PullRequest.Repository.NameWithOwner,
|
|
cs.ColorFromString(prShared.ColorForPRState(*session.PullRequest))(fmt.Sprintf("#%d", session.PullRequest.Number)),
|
|
)
|
|
} else {
|
|
// This can happen when the session is just created and a PR is not yet available for it
|
|
fmt.Fprintf(out, "%s\n", shared.ColorFuncForSessionState(*session, cs)(shared.SessionStateString(session.State)))
|
|
}
|
|
|
|
if session.User != nil {
|
|
fmt.Fprintf(out, "Started on behalf of %s %s\n", session.User.Login, text.FuzzyAgo(time.Now(), session.CreatedAt))
|
|
} else {
|
|
// Should never happen, but we need to cover the path
|
|
fmt.Fprintf(out, "Started %s\n", text.FuzzyAgo(time.Now(), session.CreatedAt))
|
|
}
|
|
|
|
// TODO(babakks): uncomment when we have the --logs option ready
|
|
// fmt.Fprintln(out, "")
|
|
// fmt.Fprintf(out, "For the detailed session logs, try: gh agent-task view '%s' --logs\n", opts.SelectorArg)
|
|
|
|
if session.PullRequest != nil {
|
|
fmt.Fprintln(out, "")
|
|
fmt.Fprintln(out, cs.Muted("View this session on GitHub:"))
|
|
fmt.Fprintln(out, cs.Muted(fmt.Sprintf("%s/agent-sessions/%s", session.PullRequest.URL, url.PathEscape(session.ID))))
|
|
}
|
|
|
|
return nil
|
|
}
|