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

Refactor issues.{Comment,Event} to changes.{Comment,TimelineItem}.
dmitshur committed 6 years ago commit 6c5819d156f7090ce383e102cb9bfac46dd5faca
Collapse all
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{}
)