-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathissues.go
211 lines (181 loc) · 6.42 KB
/
issues.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Package issues provides an issues service definition.
package issues
import (
"context"
"fmt"
"strings"
"time"
"github.com/shurcooL/reactions"
"github.com/shurcooL/users"
)
// RepoSpec is a specification for a repository.
type RepoSpec struct {
URI string // URI is clean '/'-separated URI. E.g., "example.com/user/repo".
}
// String implements fmt.Stringer.
func (rs RepoSpec) String() string {
return rs.URI
}
// Service defines methods of an issue tracking service.
type Service interface {
// List issues.
List(ctx context.Context, repo RepoSpec, opt IssueListOptions) ([]Issue, error)
// Count issues.
Count(ctx context.Context, repo RepoSpec, opt IssueListOptions) (uint64, error)
// Get an issue.
Get(ctx context.Context, repo RepoSpec, id uint64) (Issue, error)
// TODO: After some time, if ListTimeline proves to be a good replacement for ListComments
// and ListEvents, replace them here to simplify things.
// ListComments lists comments for specified issue id.
ListComments(ctx context.Context, repo RepoSpec, id uint64, opt *ListOptions) ([]Comment, error)
// ListEvents lists events for specified issue id.
ListEvents(ctx context.Context, repo RepoSpec, id uint64, opt *ListOptions) ([]Event, error)
// Create a new issue.
Create(ctx context.Context, repo RepoSpec, issue Issue) (Issue, error)
// CreateComment creates a new comment for specified issue id.
CreateComment(ctx context.Context, repo RepoSpec, id uint64, comment Comment) (Comment, error)
// Edit the specified issue id.
Edit(ctx context.Context, repo RepoSpec, id uint64, ir IssueRequest) (Issue, []Event, error)
// EditComment edits comment of specified issue id.
EditComment(ctx context.Context, repo RepoSpec, id uint64, cr CommentRequest) (Comment, error)
}
// TimelineLister is an optional interface that combines ListComments and ListEvents methods into one
// that includes both. It's available for situations where this is more efficient to implement.
type TimelineLister interface {
// IsTimelineLister reports whether the underlying service implements TimelineLister
// fully for the specified repo.
IsTimelineLister(repo RepoSpec) bool
// ListTimeline lists timeline items (Comment, Event) for specified issue id
// in chronological order, if IsTimelineLister(repo) reported positively.
// The issue description comes first in a timeline.
ListTimeline(ctx context.Context, repo RepoSpec, id uint64, opt *ListOptions) ([]interface{}, error)
}
// CopierFrom is an optional interface that allows copying issues between services.
type CopierFrom interface {
// CopyFrom copies all issues from src for specified repo.
// ctx should provide permission to access all issues in src.
CopyFrom(ctx context.Context, src Service, repo RepoSpec) error
}
// Issue represents an issue on a repository.
type Issue struct {
ID uint64
State State
Title string
Labels []Label
Comment
Replies int // Number of replies to this issue (not counting the mandatory issue description comment).
}
// Label represents a label.
type Label struct {
Name string
Color RGB
}
// TODO: Dedup.
//
// RGB represents a 24-bit color without alpha channel.
type RGB struct {
R, G, B uint8
}
func (c RGB) RGBA() (r, g, b, a uint32) {
r = uint32(c.R)
r |= r << 8
g = uint32(c.G)
g |= g << 8
b = uint32(c.B)
b |= b << 8
a = uint32(255)
a |= a << 8
return
}
// HexString returns a hexadecimal color string. For example, "#ff0000" for red.
func (c RGB) HexString() string {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
// Milestone represents a milestone.
type Milestone struct {
Name string
}
// 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 (or the encompassing issue).
}
// Edited provides the actor and timing information for an edited item.
type Edited struct {
By users.User
At time.Time
}
// IssueRequest is a request to edit an issue.
// To edit the body, use EditComment with comment ID 0.
type IssueRequest struct {
State *State
Title *string
// TODO: Labels *[]Label
}
// CommentRequest is a request to edit a comment.
type CommentRequest struct {
ID uint64
Body *string // If not nil, set the body.
Reaction *reactions.EmojiID // If not nil, toggle this reaction.
}
// State represents the issue state.
type State string
const (
// OpenState is when an issue is open.
OpenState State = "open"
// ClosedState is when an issue is closed.
ClosedState State = "closed"
)
// Validate returns non-nil error if the issue is invalid.
func (i Issue) Validate() error {
if strings.TrimSpace(i.Title) == "" {
return fmt.Errorf("title can't be blank or all whitespace")
}
return nil
}
// Validate returns non-nil error if the issue request is invalid.
func (ir IssueRequest) Validate() error {
if ir.State != nil {
switch *ir.State {
case OpenState, ClosedState:
default:
return fmt.Errorf("bad state")
}
}
if ir.Title != nil {
if strings.TrimSpace(*ir.Title) == "" {
return fmt.Errorf("title can't be blank or all whitespace")
}
}
return nil
}
// Validate returns non-nil error if the comment is invalid.
func (c Comment) Validate() error {
// TODO: Issue descriptions can have blank bodies, support that (primarily for editing comments).
if strings.TrimSpace(c.Body) == "" {
return fmt.Errorf("comment body can't be blank or all whitespace")
}
return nil
}
// Validate validates the comment edit request, returning an non-nil error if it's invalid.
// requiresEdit reports if the edit request needs edit rights or if it can be done by anyone that can react.
func (cr CommentRequest) Validate() (requiresEdit bool, err error) {
if cr.Body != nil {
requiresEdit = true
// TODO: Issue descriptions can have blank bodies, support that (primarily for editing comments).
if strings.TrimSpace(*cr.Body) == "" {
return requiresEdit, fmt.Errorf("comment body can't be blank or all whitespace")
}
}
/*if cr.Reaction != nil {
// TODO: Maybe validate that the emojiID is one of supported ones.
// Or maybe not (unsupported ones can be handled by frontend component).
// That way custom emoji can be added/removed, etc. Figure out what the best thing to do is and do it.
}*/
return requiresEdit, nil
}