package mapping import ( "slices" "strings" "git.noodles.cam/claude-skills/marketplace/cli/internal/issue" "git.noodles.cam/claude-skills/marketplace/cli/internal/wire" ) // domain -> Gitea. // RequestOptions are what the transport resolved before the call: label names // are ids by then, and a milestone title is a number. // // Both are lookups against one repository, which is why they cannot be done // here — this package never learns which repository it is translating for // beyond the name it is handed. type RequestOptions struct { // LabelIDs is name -> id for the labels this repository holds. A nil map // leaves `labels` out of the request; a non-nil one sends the list, empty // included, and a label the repository does not have is silently left off // rather than failing the write — an unknown label is a bootstrap that has // not run, not a reason to lose the issue. LabelIDs map[string]int64 // MilestoneID is the resolved milestone. nil leaves the key out, which on // an edit means "leave whatever is attached alone". MilestoneID *int64 // IncludeState sends `state`. An edit that means to open or close says so; // a create takes the tracker's default. IncludeState bool } // ToRequest is the request body for creating or editing an issue. // // The prose is sent verbatim — see the package doc on why slugs in // `## Depends on` are not rewritten to `#N`. The one addition is the id marker, // prepended (never appended) so the tracker remembers the slug after push has // deleted the local file. FromPayload takes it straight back off, so the body // still round-trips byte for byte. // // A create needs a title and a body, so those two are always filled. Every // other key is left out unless the caller has an opinion about it: on a PATCH // an absent key leaves the tracker's value alone, and a present one overwrites // it — see wire.IssueRequest for what each of them clears when it is sent // empty. func ToRequest(i *issue.Issue, opt RequestOptions) *wire.IssueRequest { r := &wire.IssueRequest{ Title: wire.Set(i.Title), Body: wire.Set(WithIDMarker(strings.TrimSpace(i.Body), i.ID)), } if opt.LabelIDs != nil { ids := []int64{} for _, name := range i.Labels { if id, ok := opt.LabelIDs[name]; ok { ids = append(ids, id) } } r.Labels = &ids } // Copied, so the request body and the issue it came from cannot alias one // slice: whatever a caller does to either afterwards is not a change to // what was sent. if len(i.Assignees) > 0 { r.Assignees = wire.Set(slices.Clone(i.Assignees)) } if opt.MilestoneID != nil { r.Milestone = opt.MilestoneID } if opt.IncludeState { r.State = wire.Set(i.State) } // An empty `branch:` is "no opinion", not "no branch": sending ref="" would // clear whatever is set on the Gitea side, so the key is left out instead. if branch := strings.TrimSpace(i.Extra[BranchKey]); branch != "" { r.Ref = wire.Set(branch) } return r } // ApplyRemote stamps the sync-owned fields onto an issue after a successful // write. Mutates and returns it; `origin` is the one domain field this touches, // and it touches it because "this work exists somewhere else now" is exactly // what has just become true. func ApplyRemote(i *issue.Issue, p *wire.Issue, repo wire.Repo, synced string) *issue.Issue { if i.Extra == nil { i.Extra = map[string]string{} } i.Origin = Origin i.Extra[GiteaKey] = wire.Key{Repo: repo, Number: p.Number}.String() i.Extra[URLKey] = p.HTMLURL i.Extra[SyncedKey] = synced if p.UpdatedAt != "" { i.Extra[RemoteUpdatedKey] = p.UpdatedAt } return i }