1239fdee70
The transport was hand-rolled net/http against the REST API. The payload shapes were ours, in internal/wire, which meant every field Gitea learned was a field somebody here had to notice; and "does this instance have issue dependencies?" had to be guessed from a status code, because a 404 from a missing route and a 404 from a missing issue look alike. The SDK settles both. The shapes are maintained by the people who maintain the server, and the client negotiates the server's version when it is built, so the dependency endpoint is now gated on `>= 1.20.0` — verified against the release where the route appears, not assumed. Below the gate nothing is requested at all. internal/wire keeps what the SDK has no answer for: addressing. The SDK takes an owner, a name and an int64 and never parses, while `42`, `#42`, `owner/repo#42` and an issue URL are four spellings of one address, all four are what somebody has in hand, and Key is what the ledger is keyed by. The payload structs go. Four things that had to survive the move, and did: - request bodies still land in .kettle/payload/, now via an http.RoundTripper on the client the SDK is given — which is better than before, because it files every request rather than the ones a call site remembered to name; - errors still carry the HTTP status AND the response body, and a decode failure on a 2xx is deliberately not an APIError, so the dependency probe cannot read a bad decode as "feature missing"; - the number -> slug ledger is untouched, entries still outlive the files they name; - Client.For(repo) still re-points at another repository for one call. What it cost, written down in AGENTS.md where it happened. internal/mapping's layering test was a fact about the import graph — nothing in its closure could open a socket — and the SDK ships its types and its client in one package, so the test now asserts what is still true: the bridge performs no I/O, checked on direct imports plus a grep for time.Now. A run makes one extra request before it does anything. Gitea's issue edit endpoint carries no labels, so a push whose labels changed needs a second call; push makes it and says so. go.mod requires go 1.26, which the SDK sets and which is now the floor for building this binary. internal/issue and internal/project are byte-identical. The domain did not notice, which is the whole argument for the layering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
4.0 KiB
Go
115 lines
4.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.noodles.cam/claude-skills/marketplace/cli/internal/gitea"
|
|
"git.noodles.cam/claude-skills/marketplace/cli/internal/mapping"
|
|
"git.noodles.cam/claude-skills/marketplace/cli/internal/wire"
|
|
)
|
|
|
|
// labelColumn is how much of the label list a row shows before it is cut.
|
|
const labelColumn = 38
|
|
|
|
func init() {
|
|
register(&Command{
|
|
Name: "remote",
|
|
Group: GroupSync,
|
|
Short: "list what exists in the tracker, one line each",
|
|
Long: `Discovery only: this prints and WRITES NOTHING. The local store is a store, not a
|
|
search-results folder, and a listing that landed in it would leave files nobody
|
|
asked for beside the issues somebody did. Pick the numbers here, then pull them.
|
|
|
|
#42 open type/task, tech/sql Wire sqlc into the repo layer
|
|
└─ local: wire-sqlc-appclick
|
|
|
|
The second line appears when the number is already in the local ledger, so it is
|
|
obvious what a pull would refresh and what it would add.
|
|
|
|
--limit here caps the LISTING: N lines out, closed ones among them. That is not
|
|
what the same flag means to ` + "`kettle pull`" + `, and the difference is not an oversight —
|
|
pull bounds what it WRITES, this command writes nothing, and enumeration is the
|
|
whole job.
|
|
|
|
Projects are not filterable: the projects API is not exposed by Gitea. Use
|
|
milestones or labels, or the web UI.`,
|
|
Examples: []Example{
|
|
{"kettle remote", "the open issues, 30 of them"},
|
|
{"kettle remote --state all --label type/bug --limit 50", "every bug, open and closed"},
|
|
{"kettle remote --milestone v0.2", "what is in a milestone"},
|
|
{"kettle remote -q sqlc", "keyword search over title and body"},
|
|
},
|
|
Setup: func(fs *flag.FlagSet) func([]string) error {
|
|
state := fs.String("state", "open", "open, closed or all")
|
|
var labels stringList
|
|
fs.Var(&labels, "label", "filter by label; repeat for AND")
|
|
// Both spellings, the way the Python this replaces took them.
|
|
var query string
|
|
fs.StringVar(&query, "q", "", "search text in title and body")
|
|
fs.StringVar(&query, "query", "", "the long spelling of -q")
|
|
milestone := fs.String("milestone", "", "milestone id or title")
|
|
limit := fs.Int("limit", 30, "how many lines to print")
|
|
out := storeFlag(fs)
|
|
|
|
return func(args []string) error {
|
|
if len(args) > 0 {
|
|
return Fail("remote takes no arguments — filter with --label, --milestone or -q")
|
|
}
|
|
if !contains([]string{"open", "closed", "all"}, *state) {
|
|
return Fail("--state %q must be open, closed or all", *state)
|
|
}
|
|
if *limit < 1 {
|
|
return Fail("--limit must be 1 or more, got %d", *limit)
|
|
}
|
|
|
|
root, client, err := syncStart(*out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
listing, err := client.ListIssues(gitea.IssueFilter{
|
|
State: *state, Labels: labels, Query: query,
|
|
Milestone: *milestone, Limit: *limit,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The ledger, not the files: a pushed issue has no file left and
|
|
// is still something a pull would land on a known slug.
|
|
ledger := gitea.LoadRemoteMap(root)
|
|
repo := client.Repo()
|
|
|
|
for _, p := range listing.Issues {
|
|
labels := "-"
|
|
if names := mapping.LabelNames(p); len(names) > 0 {
|
|
labels = strings.Join(names, ", ")
|
|
}
|
|
// One line per issue is the whole point; a repository that
|
|
// namespaces heavily would wrap the column otherwise.
|
|
if len(labels) > labelColumn {
|
|
labels = labels[:labelColumn]
|
|
}
|
|
fmt.Printf("#%-5d %-7s %-38s %s\n", p.Index, p.State, labels, p.Title)
|
|
if local := ledger.Slug(wire.Key{Repo: repo, Number: int(p.Index)}); local != "" {
|
|
fmt.Printf("%13s└─ local: %s\n", "", local)
|
|
}
|
|
}
|
|
|
|
scope := ""
|
|
if listing.Milestone != "" {
|
|
scope = " in milestone " + listing.Milestone
|
|
}
|
|
hint := "<n>"
|
|
if *milestone != "" {
|
|
hint = "--milestone " + *milestone
|
|
}
|
|
fmt.Printf("%d issue(s)%s — pull them with: kettle pull %s\n",
|
|
len(listing.Issues), scope, hint)
|
|
return nil
|
|
}
|
|
},
|
|
})
|
|
}
|