dmitri.shuralyov.com/service/change/...

Refactor issues.{Comment,Event} to changes.{Comment,TimelineItem}.
dmitshur committed 2 years ago commit 6c5819d156f7090ce383e102cb9bfac46dd5faca
changes.go
@@ -17,11 +17,11 @@ type Service interface {
 	Count(ctx context.Context, repo string, opt ListOptions) (uint64, error)
 
 	// Get a change.
 	Get(ctx context.Context, repo string, id uint64) (Change, error)
 
-	// ListTimeline lists timeline items (issues.Comment, issues.Event) for specified change id.
+	// ListTimeline lists timeline items (changes.Comment, changes.TimelineItem) for specified change id.
 	ListTimeline(ctx context.Context, repo string, id uint64, opt *ListTimelineOptions) ([]interface{}, error)
 	// ListCommits lists change commits.
 	ListCommits(ctx context.Context, repo string, id uint64) ([]Commit, error)
 	// Get a change diff.
 	GetDiff(ctx context.Context, repo string, id uint64, opt *GetDiffOptions) ([]byte, error)
githubapi/githubapi.go
@@ -267,11 +267,11 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
 				PublishedAt     githubql.DateTime
 				LastEditedAt    *githubql.DateTime
 				Editor          *githubqlActor
 				Body            githubql.String
 				ReactionGroups  reactionGroups
-				ViewerCanUpdate githubql.Boolean
+				ViewerCanUpdate bool
 
 				Timeline struct {
 					Nodes []struct {
 						Typename    string `graphql:"__typename"`
 						ClosedEvent struct {
@@ -315,10 +315,15 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
 								githubqlActor `graphql:"...on Actor"`
 							}
 						} `graphql:"...on ReviewRequestRemovedEvent"`
 						MergedEvent struct {
 							event
+							Commit struct {
+								OID string
+								URL string
+							}
+							MergeRefName string
 						} `graphql:"...on MergedEvent"`
 						PullRequestReview struct {
 							Author    githubqlActor
 							CreatedAt githubql.DateTime
 							State     githubql.PullRequestReviewState
@@ -332,11 +337,11 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
 						PublishedAt     githubql.DateTime
 						LastEditedAt    *githubql.DateTime
 						Editor          *githubqlActor
 						Body            githubql.String
 						ReactionGroups  reactionGroups
-						ViewerCanUpdate githubql.Boolean
+						ViewerCanUpdate bool
 					}
 				} `graphql:"comments(first:100)"` // TODO: Pagination...
 				Reviews struct {
 					Nodes []struct {
 						DatabaseID      uint64
@@ -345,11 +350,11 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
 						LastEditedAt    *githubql.DateTime
 						Editor          *githubqlActor
 						Body            string
 						ViewerCanUpdate bool
 					}
-				} `graphql:"reviews(first:100)"` // TODO: Figure out how to make pagination across 2 resource types work...
+				} `graphql:"reviews(first:100)"` // TODO: Pagination... Figure out how to make pagination across 2 resource types work...
 			} `graphql:"pullRequest(number:$prNumber)"`
 		} `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"`
 	}
 	variables := map[string]interface{}{
 		"repositoryOwner": githubql.String(repo.Owner),
@@ -365,124 +370,136 @@ func (s service) ListTimeline(ctx context.Context, rs string, id uint64, opt *ch
 		pr := q.Repository.PullRequest
 		reactions, err := s.reactions(pr.ReactionGroups)
 		if err != nil {
 			return timeline, err
 		}
-		var edited *issues.Edited
+		var edited *changes.Edited
 		if pr.LastEditedAt != nil {
-			edited = &issues.Edited{
+			edited = &changes.Edited{
 				By: ghActor(*pr.Editor),
 				At: pr.LastEditedAt.Time,
 			}
 		}
-		timeline = append(timeline, issues.Comment{
+		timeline = append(timeline, changes.Comment{
 			ID:        prDescriptionCommentID,
 			User:      ghActor(pr.Author),
 			CreatedAt: pr.PublishedAt.Time,
 			Edited:    edited,
 			Body:      string(pr.Body),
 			Reactions: reactions,
-			Editable:  bool(pr.ViewerCanUpdate),
+			Editable:  pr.ViewerCanUpdate,
 		})
 	}
 	for _, comment := range q.Repository.PullRequest.Comments.Nodes {
 		reactions, err := s.reactions(comment.ReactionGroups)
 		if err != nil {
 			return timeline, err
 		}
-		var edited *issues.Edited
+		var edited *changes.Edited
 		if comment.LastEditedAt != nil {
-			edited = &issues.Edited{
+			edited = &changes.Edited{
 				By: ghActor(*comment.Editor),
 				At: comment.LastEditedAt.Time,
 			}
 		}
-		timeline = append(timeline, issues.Comment{
+		timeline = append(timeline, changes.Comment{
 			ID:        uint64(comment.DatabaseID),
 			User:      ghActor(comment.Author),
 			CreatedAt: comment.PublishedAt.Time,
 			Edited:    edited,
 			Body:      string(comment.Body),
 			Reactions: reactions,
-			Editable:  bool(comment.ViewerCanUpdate),
+			Editable:  comment.ViewerCanUpdate,
 		})
 	}
 	for _, review := range q.Repository.PullRequest.Reviews.Nodes {
 		if review.Body == "" {
 			continue
 		}
-		var edited *issues.Edited
+		var edited *changes.Edited
 		if review.LastEditedAt != nil {
-			edited = &issues.Edited{
+			edited = &changes.Edited{
 				By: ghActor(*review.Editor),
 				At: review.LastEditedAt.Time,
 			}
 		}
-		timeline = append(timeline, issues.Comment{
+		timeline = append(timeline, changes.Comment{
 			ID:        review.DatabaseID,
 			User:      ghActor(review.Author),
 			CreatedAt: review.PublishedAt.Time,
 			Edited:    edited,
 			Body:      review.Body,
 			Editable:  review.ViewerCanUpdate,
 		})
 	}
 	for _, event := range q.Repository.PullRequest.Timeline.Nodes {
-		e := issues.Event{
-			//ID:   0, // TODO.
-			Type: ghEventType(event.Typename),
+		e := changes.TimelineItem{
+		//ID: 0, // TODO.
 		}
-		switch e.Type {
-		case issues.Closed:
+		switch event.Typename {
+		case "ClosedEvent":
 			e.Actor = ghActor(event.ClosedEvent.Actor)
 			e.CreatedAt = event.ClosedEvent.CreatedAt.Time
-		case issues.Reopened:
+			e.Payload = changes.ClosedEvent{}
+		case "ReopenedEvent":
 			e.Actor = ghActor(event.ReopenedEvent.Actor)
 			e.CreatedAt = event.ReopenedEvent.CreatedAt.Time
-		case issues.Renamed:
+			e.Payload = changes.ReopenedEvent{}
+		case "RenamedTitleEvent":
 			e.Actor = ghActor(event.RenamedTitleEvent.Actor)
 			e.CreatedAt = event.RenamedTitleEvent.CreatedAt.Time
-			e.Rename = &issues.Rename{
+			e.Payload = changes.RenamedEvent{
 				From: event.RenamedTitleEvent.PreviousTitle,
 				To:   event.RenamedTitleEvent.CurrentTitle,
 			}
-		case issues.Labeled:
+		case "LabeledEvent":
 			e.Actor = ghActor(event.LabeledEvent.Actor)
 			e.CreatedAt = event.LabeledEvent.CreatedAt.Time
-			e.Label = &issues.Label{
-				Name:  event.LabeledEvent.Label.Name,
-				Color: ghColor(event.LabeledEvent.Label.Color),
+			e.Payload = changes.LabeledEvent{
+				Label: issues.Label{
+					Name:  event.LabeledEvent.Label.Name,
+					Color: ghColor(event.LabeledEvent.Label.Color),
+				},
 			}
-		case issues.Unlabeled:
+		case "UnlabeledEvent":
 			e.Actor = ghActor(event.UnlabeledEvent.Actor)
 			e.CreatedAt = event.UnlabeledEvent.CreatedAt.Time
-			e.Label = &issues.Label{
-				Name:  event.UnlabeledEvent.Label.Name,
-				Color: ghColor(event.UnlabeledEvent.Label.Color),
+			e.Payload = changes.UnlabeledEvent{
+				Label: issues.Label{
+					Name:  event.UnlabeledEvent.Label.Name,
+					Color: ghColor(event.UnlabeledEvent.Label.Color),
+				},
 			}
 		// TODO: Wait for GitHub to add support.
-		//case issues.CommentDeleted:
+		//case "CommentDeletedEvent":
 		//	e.Actor = ghActor(event.CommentDeletedEvent.Actor)
 		//	e.CreatedAt = event.CommentDeletedEvent.CreatedAt.Time
 		case "ReviewRequestedEvent":
 			e.Actor = ghActor(event.ReviewRequestedEvent.Actor)
 			e.CreatedAt = event.ReviewRequestedEvent.CreatedAt.Time
-			// TODO: Move RequestedReviewer field to changes-only events (it doesn't apply to issues).
-			e.RequestedReviewer = ghActor(event.ReviewRequestedEvent.RequestedReviewer.githubqlActor)
+			e.Payload = changes.ReviewRequestedEvent{
+				RequestedReviewer: ghActor(event.ReviewRequestedEvent.RequestedReviewer.githubqlActor),
+			}
 		case "ReviewRequestRemovedEvent":
 			e.Actor = ghActor(event.ReviewRequestRemovedEvent.Actor)
 			e.CreatedAt = event.ReviewRequestRemovedEvent.CreatedAt.Time
-			// TODO: Move RequestedReviewer field to changes-only events (it doesn't apply to issues).
-			e.RequestedReviewer = ghActor(event.ReviewRequestRemovedEvent.RequestedReviewer.githubqlActor)
+			e.Payload = changes.ReviewRequestRemovedEvent{
+				RequestedReviewer: ghActor(event.ReviewRequestRemovedEvent.RequestedReviewer.githubqlActor),
+			}
 		case "MergedEvent":
-			e.Actor = ghActor(event.ReviewRequestRemovedEvent.Actor)
-			e.CreatedAt = event.ReviewRequestRemovedEvent.CreatedAt.Time
+			e.Actor = ghActor(event.MergedEvent.Actor)
+			e.CreatedAt = event.MergedEvent.CreatedAt.Time
+			e.Payload = changes.MergedEvent{
+				CommitID:      event.MergedEvent.Commit.OID,
+				CommitHTMLURL: event.MergedEvent.Commit.URL,
+				RefName:       event.MergedEvent.MergeRefName,
+			}
 		case "PullRequestReview":
 			switch event.PullRequestReview.State {
 			case githubql.PullRequestReviewStateApproved:
 				// TODO: Make this a thing that ListComments returns, etc. After all, it can have a non-empty body.
-				e.Type = "ApprovedEvent"
+				e.Payload = changes.ApprovedEvent{}
 			default:
 				continue
 			}
 			e.Actor = ghActor(event.PullRequestReview.Author)
 			e.CreatedAt = event.PullRequestReview.CreatedAt.Time
@@ -582,29 +599,10 @@ func ghPRState(state githubql.PullRequestState) changes.State {
 	default:
 		panic("unreachable")
 	}
 }
 
-func ghEventType(typename string) issues.EventType {
-	switch typename {
-	case "ReopenedEvent": // TODO: Use githubql.IssueTimelineItemReopenedEvent or so.
-		return issues.Reopened
-	case "ClosedEvent": // TODO: Use githubql.IssueTimelineItemClosedEvent or so.
-		return issues.Closed
-	case "RenamedTitleEvent":
-		return issues.Renamed
-	case "LabeledEvent":
-		return issues.Labeled
-	case "UnlabeledEvent":
-		return issues.Unlabeled
-	case "CommentDeletedEvent":
-		return issues.CommentDeleted
-	default:
-		return issues.EventType(typename)
-	}
-}
-
 // ghColor converts a GitHub color hex string like "ff0000"
 // into an issues.RGB value.
 func ghColor(hex string) issues.RGB {
 	var c issues.RGB
 	fmt.Sscanf(hex, "%02x%02x%02x", &c.R, &c.G, &c.B)
@@ -615,11 +613,11 @@ type reactionGroups []struct {
 	Content githubql.ReactionContent
 	Users   struct {
 		Nodes      []githubqlActor
 		TotalCount githubql.Int
 	} `graphql:"users(first:10)"`
-	ViewerHasReacted githubql.Boolean
+	ViewerHasReacted bool
 }
 
 // reactions converts []githubql.ReactionGroup to []reactions.Reaction.
 func (s service) reactions(rgs reactionGroups) ([]reactions.Reaction, error) {
 	var rs []reactions.Reaction
@@ -638,11 +636,11 @@ func (s service) reactions(rgs reactionGroups) ([]reactions.Reaction, error) {
 				if s.currentUser.ID != 0 && actor.UserSpec == s.currentUser.UserSpec {
 					addedAuthedUser = true
 				}
 			} else if i == len(rg.Users.Nodes) {
 				// Add authed user last if they've reacted, but haven't been added already.
-				if bool(rg.ViewerHasReacted) && !addedAuthedUser {
+				if rg.ViewerHasReacted && !addedAuthedUser {
 					us = append(us, s.currentUser)
 				}
 			} else {
 				us = append(us, users.User{})
 			}
timeline.go
@@ -0,0 +1,82 @@
+package changes
+
+import (
+	"time"
+
+	"github.com/shurcooL/issues"
+	"github.com/shurcooL/reactions"
+	"github.com/shurcooL/users"
+)
+
+// Comment represents a comment left on an issue.
+type Comment struct {
+	ID        uint64
+	User      users.User
+	CreatedAt time.Time
+	Edited    *Edited // Edited is nil if the comment hasn't been edited.
+	Body      string
+	Reactions []reactions.Reaction
+	Editable  bool // Editable represents whether the current user (if any) can perform edit operations on this comment.
+}
+
+// Edited provides the actor and timing information for an edited item.
+type Edited struct {
+	By users.User
+	At time.Time
+}
+
+// TimelineItem represents a timeline item.
+type TimelineItem struct {
+	ID        uint64 // TODO: See if this belongs here.
+	Actor     users.User
+	CreatedAt time.Time
+
+	// Payload specifies the event type. It's one of:
+	// ClosedEvent, ReopenedEvent, ..., MergedEvent, ApprovedEvent.
+	Payload interface{}
+}
+
+type (
+	// ClosedEvent is when a change is closed.
+	ClosedEvent struct {
+		CommitID      string // CommitID is SHA of commit that closed the change, or empty string if there's no associated commit.
+		CommitHTMLURL string // Optional.
+	}
+
+	// ReopenedEvent is when a change is reopened.
+	ReopenedEvent struct{}
+
+	// RenamedEvent is when a change is renamed.
+	RenamedEvent struct {
+		From string
+		To   string
+	}
+
+	// LabeledEvent is when a change is labeled.
+	LabeledEvent struct {
+		Label issues.Label
+	}
+	// UnlabeledEvent is when a change is unlabeled.
+	UnlabeledEvent struct {
+		Label issues.Label
+	}
+
+	// CommentDeletedEvent is when a comment is deleted.
+	CommentDeletedEvent struct{}
+
+	ReviewRequestedEvent struct {
+		RequestedReviewer users.User
+	}
+	ReviewRequestRemovedEvent struct {
+		RequestedReviewer users.User
+	}
+
+	MergedEvent struct {
+		CommitID      string
+		CommitHTMLURL string // Optional.
+		RefName       string
+	}
+
+	// TODO: Merge into Comment or so.
+	ApprovedEvent struct{}
+)