259 lines
7.7 KiB
Go
259 lines
7.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/openclaw/gitcrawl/internal/store"
|
|
)
|
|
|
|
func (a *App) ghThreadViewJSONRow(ctx context.Context, repoValue string, thread store.Thread, fieldsRaw string) (map[string]any, error) {
|
|
fields := parseJSONFields(fieldsRaw)
|
|
if len(fields) == 0 {
|
|
return nil, fmt.Errorf("--json requires at least one field")
|
|
}
|
|
row := make(map[string]any, len(fields))
|
|
var cache *store.PullRequestCache
|
|
for _, field := range fields {
|
|
if field == "comments" {
|
|
comments, err := a.localGHThreadComments(ctx, thread.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
row[field] = ghCommentsJSONValue(comments)
|
|
continue
|
|
}
|
|
value, err := ghSearchJSONValue(thread, field)
|
|
if err == nil {
|
|
row[field] = value
|
|
continue
|
|
}
|
|
if thread.Kind != "pull_request" {
|
|
return nil, err
|
|
}
|
|
if cache == nil {
|
|
loaded, loadErr := a.loadGHPullRequestCache(ctx, repoValue, thread.Number, ghPRFieldsNeedFresh(fields))
|
|
if loadErr != nil {
|
|
return nil, loadErr
|
|
}
|
|
cache = &loaded
|
|
}
|
|
value, err = ghPRDetailJSONValue(thread, *cache, field)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
row[field] = value
|
|
}
|
|
return row, nil
|
|
}
|
|
|
|
func (a *App) localGHPullRequestCache(ctx context.Context, repoValue string, number int) (store.PullRequestCache, error) {
|
|
owner, repoName, err := parseOwnerRepo(repoValue)
|
|
if err != nil {
|
|
return store.PullRequestCache{}, err
|
|
}
|
|
rt, err := a.openLocalRuntimeReadOnly(ctx)
|
|
if err != nil {
|
|
return store.PullRequestCache{}, localGHUnsupported(err)
|
|
}
|
|
defer rt.Store.Close()
|
|
repo, err := rt.repository(ctx, owner, repoName)
|
|
if err != nil {
|
|
return store.PullRequestCache{}, localGHUnsupported(err)
|
|
}
|
|
cache, err := rt.Store.PullRequestCache(ctx, repo.ID, number)
|
|
if err != nil {
|
|
return store.PullRequestCache{}, localGHUnsupported(err)
|
|
}
|
|
return cache, nil
|
|
}
|
|
|
|
func (a *App) localGHThreadComments(ctx context.Context, threadID int64) ([]store.Comment, error) {
|
|
rt, err := a.openLocalRuntimeReadOnly(ctx)
|
|
if err != nil {
|
|
return nil, localGHUnsupported(err)
|
|
}
|
|
defer rt.Store.Close()
|
|
comments, err := rt.Store.ListComments(ctx, threadID)
|
|
if err != nil {
|
|
return nil, localGHUnsupported(err)
|
|
}
|
|
return comments, nil
|
|
}
|
|
|
|
func ghCommentsJSONValue(comments []store.Comment) []map[string]any {
|
|
out := make([]map[string]any, 0, len(comments))
|
|
for _, comment := range comments {
|
|
out = append(out, map[string]any{
|
|
"id": comment.GitHubID,
|
|
"author": map[string]any{"login": comment.AuthorLogin, "type": comment.AuthorType},
|
|
"body": comment.Body,
|
|
"createdAt": comment.CreatedAtGitHub,
|
|
"updatedAt": comment.UpdatedAtGitHub,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ghPRDetailJSONValue(thread store.Thread, cache store.PullRequestCache, field string) (any, error) {
|
|
switch field {
|
|
case "files":
|
|
files := make([]map[string]any, 0, len(cache.Files))
|
|
for _, file := range cache.Files {
|
|
files = append(files, map[string]any{
|
|
"path": file.Path,
|
|
"additions": file.Additions,
|
|
"deletions": file.Deletions,
|
|
"status": file.Status,
|
|
})
|
|
}
|
|
return files, nil
|
|
case "commits":
|
|
commits := make([]map[string]any, 0, len(cache.Commits))
|
|
for _, commit := range cache.Commits {
|
|
headline := commit.Message
|
|
if index := strings.IndexByte(headline, '\n'); index >= 0 {
|
|
headline = headline[:index]
|
|
}
|
|
commits = append(commits, map[string]any{
|
|
"oid": commit.SHA,
|
|
"messageHeadline": headline,
|
|
"messageBody": commit.Message,
|
|
"authoredDate": commit.CommittedAt,
|
|
"url": commit.HTMLURL,
|
|
"authors": []map[string]any{{
|
|
"login": commit.AuthorLogin,
|
|
"name": commit.AuthorName,
|
|
}},
|
|
})
|
|
}
|
|
return commits, nil
|
|
case "statusCheckRollup":
|
|
return ghStatusCheckRollup(cache.Checks), nil
|
|
case "headRefName":
|
|
return cache.Detail.HeadRef, nil
|
|
case "headRefOid":
|
|
return cache.Detail.HeadSHA, nil
|
|
case "baseRefOid":
|
|
return cache.Detail.BaseSHA, nil
|
|
case "headRepositoryOwner":
|
|
owner := strings.Split(cache.Detail.HeadRepoFullName, "/")[0]
|
|
return map[string]any{"login": owner}, nil
|
|
case "headRepository":
|
|
return map[string]any{"nameWithOwner": cache.Detail.HeadRepoFullName}, nil
|
|
case "mergeStateStatus":
|
|
return strings.ToUpper(cache.Detail.MergeableState), nil
|
|
case "additions":
|
|
return cache.Detail.Additions, nil
|
|
case "deletions":
|
|
return cache.Detail.Deletions, nil
|
|
case "changedFiles":
|
|
return cache.Detail.ChangedFiles, nil
|
|
case "isDraft":
|
|
return thread.IsDraft, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported --json field %q", field)
|
|
}
|
|
}
|
|
|
|
func ghStatusCheckRollup(checks []store.PullRequestCheck) []map[string]any {
|
|
out := make([]map[string]any, 0, len(checks))
|
|
for _, check := range checks {
|
|
state := strings.ToUpper(firstNonEmpty(check.Conclusion, check.Status))
|
|
out = append(out, map[string]any{
|
|
"__typename": "CheckRun",
|
|
"name": check.Name,
|
|
"status": strings.ToUpper(check.Status),
|
|
"conclusion": strings.ToUpper(check.Conclusion),
|
|
"state": state,
|
|
"detailsUrl": check.DetailsURL,
|
|
"workflowName": check.WorkflowName,
|
|
"startedAt": check.StartedAt,
|
|
"completedAt": check.CompletedAt,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *App) runGHPRChecks(ctx context.Context, args []string) error {
|
|
if hasAnyGHFlag(args, "--watch", "--web") {
|
|
return localGHUnsupported(fmt.Errorf("interactive PR checks flags require live gh"))
|
|
}
|
|
fs := flag.NewFlagSet("pr checks", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
repoShort := fs.String("R", "", "repository")
|
|
repoLong := fs.String("repo", "", "repository")
|
|
jsonFieldsRaw := fs.String("json", "", "comma-separated JSON fields")
|
|
jqRaw := fs.String("jq", "", "jq filter")
|
|
if err := fs.Parse(normalizeCommandArgs(args, map[string]bool{"R": true, "repo": true, "json": true, "jq": true})); err != nil {
|
|
return usageErr(err)
|
|
}
|
|
if fs.NArg() != 1 {
|
|
return usageErr(fmt.Errorf("gh pr checks requires a number or GitHub URL"))
|
|
}
|
|
ref, _ := parseThreadReference(fs.Arg(0))
|
|
number, err := parseThreadNumber(fs.Arg(0))
|
|
if err != nil {
|
|
return usageErr(err)
|
|
}
|
|
repoArg := firstNonEmpty(*repoShort, *repoLong)
|
|
if repoArg == "" {
|
|
repoArg = ref.FullName()
|
|
}
|
|
repoValue, err := a.resolveGHRepo(ctx, repoArg)
|
|
if err != nil {
|
|
return localGHUnsupported(err)
|
|
}
|
|
cache, err := a.ensureFreshGHPullRequestCache(ctx, repoValue, number)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(cache.Checks) == 0 {
|
|
return localGHUnsupported(fmt.Errorf("cached PR checks are empty"))
|
|
}
|
|
if strings.TrimSpace(*jsonFieldsRaw) != "" || strings.TrimSpace(*jqRaw) != "" || a.format == FormatJSON {
|
|
fields := firstNonEmpty(strings.TrimSpace(*jsonFieldsRaw), "name,state,conclusion,detailsUrl,workflow")
|
|
rows := ghPRChecksJSONRows(cache.Checks, fields)
|
|
return a.writeJSONValue(rows, strings.TrimSpace(*jqRaw))
|
|
}
|
|
for _, check := range cache.Checks {
|
|
if _, err := fmt.Fprintf(a.Stdout, "%s\t%s\t%s\t%s\n", check.Name, check.Status, check.Conclusion, check.DetailsURL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ghPRChecksJSONRows(checks []store.PullRequestCheck, fieldsRaw string) []map[string]any {
|
|
fields := parseJSONFields(fieldsRaw)
|
|
rows := make([]map[string]any, 0, len(checks))
|
|
for _, check := range checks {
|
|
row := make(map[string]any, len(fields))
|
|
for _, field := range fields {
|
|
switch field {
|
|
case "name":
|
|
row[field] = check.Name
|
|
case "state":
|
|
row[field] = strings.ToUpper(firstNonEmpty(check.Conclusion, check.Status))
|
|
case "status":
|
|
row[field] = check.Status
|
|
case "conclusion":
|
|
row[field] = check.Conclusion
|
|
case "detailsUrl", "link":
|
|
row[field] = check.DetailsURL
|
|
case "workflow":
|
|
row[field] = check.WorkflowName
|
|
case "startedAt":
|
|
row[field] = check.StartedAt
|
|
case "completedAt":
|
|
row[field] = check.CompletedAt
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
return rows
|
|
}
|