| // 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() } |