feat: logins list/default, actions run delete, date filters, label update alias

- fgj logins {list,default}: complementary UI to 'fgj auth'. 'list'
  shows all configured hosts (hostname, user, protocol, default flag,
  match_dirs) with --json. 'default [hostname]' gets or sets which
  host wins in resolution when no other signal is present.
  Adds 'Default bool' field to HostConfig; GetHost consults it between
  match_dirs and the codeberg.org fallback. Multiple defaults tolerated
  with a stderr warning; alphabetical-first wins.

- fgj actions run delete: delete a completed workflow run via raw
  DELETE (SDK v0.23.2 has no DeleteRepoActionRun). Fetches run first
  and refuses to delete non-terminal states unless --force; suggests
  'actions run cancel' for those. Confirmation prompt unless --yes.

- pr list / issue list gain --since and --before date filter flags.
  Accepts YYYY-MM-DD, RFC 3339, YYYY-MM-DD HH:MM:SS, and relative
  deltas (7d, 24h, 2w, 1m — months=30 days). Issues use server-side
  filter via ListIssueOption.Since/Before; PRs fall back to client-side
  (SDK lacks Since/Before on ListPullRequestsOptions).

- fgj label update added as alias for 'fgj label edit' (tea-compat).

All changes:
  cmd/logins.go (new, 140 LOC)
  cmd/actions_run_delete.go (new, ~85 LOC)
  cmd/pr.go, cmd/issue.go (+parseDateArg helper, filter wiring)
  cmd/label.go (1-line alias)
  internal/config/config.go (Default field + DefaultHost method)
  CHANGELOG.md
Built in parallel by three sub-agents; plus the label alias done
serially. go build / go vet / go test -race all clean.
This commit is contained in:
sid 2026-04-19 23:04:33 -06:00
parent d15deaf064
commit 2d69873f3e
7 changed files with 462 additions and 7 deletions

108
cmd/pr.go
View file

@ -4,7 +4,9 @@ import (
"fmt"
"net/http"
"os/exec"
"strconv"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"forgejo.zerova.net/public/fgj-sid/internal/api"
@ -14,6 +16,54 @@ import (
"github.com/spf13/cobra"
)
// parseDateArg parses a --since / --before argument.
// Accepted forms: "2006-01-02", RFC 3339, "2006-01-02 15:04:05" (local),
// or a relative delta like "7d", "24h", "2w", "1m" (months treated as 30 days).
func parseDateArg(s string) (time.Time, error) {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}, fmt.Errorf("empty date")
}
// Relative delta: <N><unit>
if last := s[len(s)-1]; last == 'h' || last == 'd' || last == 'w' || last == 'm' {
numPart := s[:len(s)-1]
if n, err := strconv.Atoi(numPart); err == nil && n >= 0 {
var d time.Duration
switch last {
case 'h':
d = time.Duration(n) * time.Hour
case 'd':
d = time.Duration(n) * 24 * time.Hour
case 'w':
d = time.Duration(n) * 7 * 24 * time.Hour
case 'm':
// Months treated as 30 days (crude but documented).
d = time.Duration(n) * 30 * 24 * time.Hour
}
return time.Now().Add(-d), nil
}
}
layouts := []string{
"2006-01-02",
time.RFC3339,
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
if layout == "2006-01-02" || layout == "2006-01-02 15:04:05" {
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
return t, nil
}
} else {
if t, err := time.Parse(layout, s); err == nil {
return t, nil
}
}
}
return time.Time{}, fmt.Errorf("unrecognized date format: %q (expected YYYY-MM-DD, RFC 3339, 'YYYY-MM-DD HH:MM:SS', or a relative delta like 7d/24h/2w/1m)", s)
}
var prCmd = &cobra.Command{
Use: "pr",
Aliases: []string{"pull-request"},
@ -32,7 +82,13 @@ var prListCmd = &cobra.Command{
fgj pr list -s all -R owner/repo
# Output as JSON
fgj pr list --json`,
fgj pr list --json
# PRs updated in the last 7 days
fgj pr list --since 7d
# Issues touched between two dates
fgj issue list --since 2026-04-01 --before 2026-04-15`,
RunE: runPRList,
}
@ -167,6 +223,8 @@ func init() {
prListCmd.Flags().Bool("draft", false, "Filter by draft status")
prListCmd.Flags().String("head", "", "Filter by head branch")
prListCmd.Flags().String("base", "", "Filter by base branch")
prListCmd.Flags().String("since", "", "Only items updated at or after this date (YYYY-MM-DD, RFC 3339, or relative like 7d)")
prListCmd.Flags().String("before", "", "Only items updated strictly before this date (YYYY-MM-DD, RFC 3339, or relative like 1d)")
prListCmd.Flags().BoolP("web", "w", false, "Open list in web browser")
addJSONFlags(prListCmd, "Output pull requests as JSON")
@ -217,6 +275,27 @@ func runPRList(cmd *cobra.Command, args []string) error {
draft, _ := cmd.Flags().GetBool("draft")
head, _ := cmd.Flags().GetString("head")
base, _ := cmd.Flags().GetString("base")
sinceStr, _ := cmd.Flags().GetString("since")
beforeStr, _ := cmd.Flags().GetString("before")
var sinceTime, beforeTime time.Time
var hasSince, hasBefore bool
if sinceStr != "" {
t, err := parseDateArg(sinceStr)
if err != nil {
return fmt.Errorf("invalid --since: %w", err)
}
sinceTime = t
hasSince = true
}
if beforeStr != "" {
t, err := parseDateArg(beforeStr)
if err != nil {
return fmt.Errorf("invalid --before: %w", err)
}
beforeTime = t
hasBefore = true
}
owner, name, err := parseRepo(repo)
if err != nil {
@ -249,7 +328,8 @@ func runPRList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid state: %s", state)
}
needsClientFilter := assignee != "" || author != "" || len(labels) > 0 || search != "" || draft || head != "" || base != ""
// server-side since/before unsupported for pulls; filtering client-side
needsClientFilter := assignee != "" || author != "" || len(labels) > 0 || search != "" || draft || head != "" || base != "" || hasSince || hasBefore
ios.StartSpinner("Fetching pull requests...")
var prs []*gitea.PullRequest
@ -271,6 +351,9 @@ func runPRList(cmd *cobra.Command, args []string) error {
page++
}
prs = filterPRs(prs, author, assignee, labels, search, draft, head, base)
if hasSince || hasBefore {
prs = filterPRsByDate(prs, sinceTime, hasSince, beforeTime, hasBefore)
}
if len(prs) > limit {
prs = prs[:limit]
}
@ -354,6 +437,27 @@ func filterPRs(prs []*gitea.PullRequest, author, assignee string, labels []strin
return result
}
// filterPRsByDate applies the --since / --before range against pr.Updated.
func filterPRsByDate(prs []*gitea.PullRequest, since time.Time, hasSince bool, before time.Time, hasBefore bool) []*gitea.PullRequest {
if !hasSince && !hasBefore {
return prs
}
result := make([]*gitea.PullRequest, 0, len(prs))
for _, pr := range prs {
if pr.Updated == nil {
continue
}
if hasSince && pr.Updated.Before(since) {
continue
}
if hasBefore && !pr.Updated.Before(before) {
continue
}
result = append(result, pr)
}
return result
}
func runPRView(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")