blob: cfc043e8f423ccfdc14d71debb01db0eda0e9c1c [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"context"
"errors"
"fmt"
"iter"
"strings"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/model"
"golang.org/x/oscar/internal/storage/timed"
)
// IssueSource returns a [model.Source] providing access to GitHub issues and issue comments.
func (a *Adapter) IssueSource() model.Source[model.Post] {
return &issueSource{a}
}
type issueSource struct {
a *Adapter
}
func (s *issueSource) Name() string {
return "GitHubIssues"
}
// Read implements [model.Source.Read].
func (s *issueSource) Read(ctx context.Context, id string) (model.Post, error) {
switch {
case strings.Contains(id, "/issues/comments/"):
return s.a.ic.DownloadIssueComment(ctx, id)
case strings.Contains(id, "/issues/"):
return s.a.ic.DownloadIssue(ctx, id)
default:
return nil, fmt.Errorf("github.IssueSource: unknown id %q", id)
}
}
// Delete implements [model.Source.Delete].
func (*issueSource) Delete(_ context.Context, id string) error {
return errors.ErrUnsupported
}
// Create implements [model.Source.Create] by
// creating a new issue comment on GitHub.
// Creating new issues is unsupported.
// The Post p must have the new Body and a ParentID referring to the containing issue.
// Other fields of the Post are ignored.
func (s *issueSource) Create(ctx context.Context, p model.Post) (string, error) {
// PostIssueComment requires an Issue, although only the URL is really needed; Number is for
// diverted edits.
iurl := p.ParentID()
_, num, err := github.ParseIssueURL(iurl)
if err != nil {
return "", err
}
issue := &github.Issue{
URL: iurl,
Number: num,
}
aurl, _, err := s.a.ic.PostIssueComment(ctx, issue, &github.IssueCommentChanges{Body: p.Body_()})
return aurl, err
}
// Update implements [model.Source.Update] by changing
// an issue or issue comment on GitHub.
// If p is a [*github.Issue], the title, body, state and labels can be changed.
// (It is not possible to set the title, body or state to the empty string.)
// Labels are replaced, not added to; include all the previous labels.
//
// If p is [*github.IssueComment], only the body can be changed.
func (s *issueSource) Update(ctx context.Context, p model.Post, u model.Updates) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("github IssueSource update: %w", err)
}
}()
switch x := p.(type) {
default:
return fmt.Errorf("bad type %T", p)
case *github.Issue:
c := u.(*github.IssueChanges)
return s.a.ic.EditIssue(ctx, x, c)
case *github.IssueComment:
c := u.(*github.IssueCommentChanges)
return s.a.ic.EditIssueComment(ctx, x, c)
}
}
// IssueWatcher returns a new [model.Watcher][model.DBContent] with the given name.
// The Watcher delivers only issues and issue comments, not events or pull requests.
// It picks up where any previous Watcher of the same name left off.
func (a *Adapter) IssueWatcher(name string) model.Watcher[model.DBContent] {
return &issueWatcher{a.ic.EventWatcher(name)}
}
type issueWatcher struct {
w *timed.Watcher[*github.Event]
}
func (w *issueWatcher) Recent() iter.Seq[model.DBContent] {
return func(yield func(model.DBContent) bool) {
for e := range w.w.Recent() {
switch x := e.Typed.(type) {
case *github.Issue:
if x.PullRequest != nil {
continue
}
if !yield(model.DBContent{DBTime: e.DBTime, Content: x}) {
return
}
case *github.IssueComment:
if !yield(model.DBContent{DBTime: e.DBTime, Content: x}) {
return
}
}
}
}
}
func (w *issueWatcher) Restart() { w.w.Restart() }
func (w *issueWatcher) MarkOld(t timed.DBTime) { w.w.MarkOld(t) }
func (w *issueWatcher) Flush() { w.w.Flush() }
func (w *issueWatcher) Latest() timed.DBTime { return w.w.Latest() }