gitea

Зеркало из https://github.com/go-gitea/gitea
Форк
0
/
issue.go 
936 строк · 26.6 Кб
1
// Copyright 2014 The Gogs Authors. All rights reserved.
2
// Copyright 2020 The Gitea Authors. All rights reserved.
3
// SPDX-License-Identifier: MIT
4

5
package issues
6

7
import (
8
	"context"
9
	"fmt"
10
	"html/template"
11
	"regexp"
12
	"slices"
13

14
	"code.gitea.io/gitea/models/db"
15
	project_model "code.gitea.io/gitea/models/project"
16
	repo_model "code.gitea.io/gitea/models/repo"
17
	user_model "code.gitea.io/gitea/models/user"
18
	"code.gitea.io/gitea/modules/container"
19
	"code.gitea.io/gitea/modules/log"
20
	"code.gitea.io/gitea/modules/setting"
21
	api "code.gitea.io/gitea/modules/structs"
22
	"code.gitea.io/gitea/modules/timeutil"
23
	"code.gitea.io/gitea/modules/util"
24

25
	"xorm.io/builder"
26
)
27

28
// ErrIssueNotExist represents a "IssueNotExist" kind of error.
29
type ErrIssueNotExist struct {
30
	ID     int64
31
	RepoID int64
32
	Index  int64
33
}
34

35
// IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
36
func IsErrIssueNotExist(err error) bool {
37
	_, ok := err.(ErrIssueNotExist)
38
	return ok
39
}
40

41
func (err ErrIssueNotExist) Error() string {
42
	return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
43
}
44

45
func (err ErrIssueNotExist) Unwrap() error {
46
	return util.ErrNotExist
47
}
48

49
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
50
type ErrIssueIsClosed struct {
51
	ID     int64
52
	RepoID int64
53
	Index  int64
54
}
55

56
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
57
func IsErrIssueIsClosed(err error) bool {
58
	_, ok := err.(ErrIssueIsClosed)
59
	return ok
60
}
61

62
func (err ErrIssueIsClosed) Error() string {
63
	return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
64
}
65

66
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
67
type ErrNewIssueInsert struct {
68
	OriginalError error
69
}
70

71
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
72
func IsErrNewIssueInsert(err error) bool {
73
	_, ok := err.(ErrNewIssueInsert)
74
	return ok
75
}
76

77
func (err ErrNewIssueInsert) Error() string {
78
	return err.OriginalError.Error()
79
}
80

81
// ErrIssueWasClosed is used when close a closed issue
82
type ErrIssueWasClosed struct {
83
	ID    int64
84
	Index int64
85
}
86

87
// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
88
func IsErrIssueWasClosed(err error) bool {
89
	_, ok := err.(ErrIssueWasClosed)
90
	return ok
91
}
92

93
func (err ErrIssueWasClosed) Error() string {
94
	return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
95
}
96

97
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
98

99
// Issue represents an issue or pull request of repository.
100
type Issue struct {
101
	ID                int64                  `xorm:"pk autoincr"`
102
	RepoID            int64                  `xorm:"INDEX UNIQUE(repo_index)"`
103
	Repo              *repo_model.Repository `xorm:"-"`
104
	Index             int64                  `xorm:"UNIQUE(repo_index)"` // Index in one repository.
105
	PosterID          int64                  `xorm:"INDEX"`
106
	Poster            *user_model.User       `xorm:"-"`
107
	OriginalAuthor    string
108
	OriginalAuthorID  int64                  `xorm:"index"`
109
	Title             string                 `xorm:"name"`
110
	Content           string                 `xorm:"LONGTEXT"`
111
	RenderedContent   template.HTML          `xorm:"-"`
112
	ContentVersion    int                    `xorm:"NOT NULL DEFAULT 0"`
113
	Labels            []*Label               `xorm:"-"`
114
	isLabelsLoaded    bool                   `xorm:"-"`
115
	MilestoneID       int64                  `xorm:"INDEX"`
116
	Milestone         *Milestone             `xorm:"-"`
117
	isMilestoneLoaded bool                   `xorm:"-"`
118
	Project           *project_model.Project `xorm:"-"`
119
	Priority          int
120
	AssigneeID        int64            `xorm:"-"`
121
	Assignee          *user_model.User `xorm:"-"`
122
	isAssigneeLoaded  bool             `xorm:"-"`
123
	IsClosed          bool             `xorm:"INDEX"`
124
	IsRead            bool             `xorm:"-"`
125
	IsPull            bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.
126
	PullRequest       *PullRequest     `xorm:"-"`
127
	NumComments       int
128
	Ref               string
129
	PinOrder          int `xorm:"DEFAULT 0"`
130

131
	DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
132

133
	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
134
	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
135
	ClosedUnix  timeutil.TimeStamp `xorm:"INDEX"`
136

137
	Attachments         []*repo_model.Attachment `xorm:"-"`
138
	isAttachmentsLoaded bool                     `xorm:"-"`
139
	Comments            CommentList              `xorm:"-"`
140
	Reactions           ReactionList             `xorm:"-"`
141
	TotalTrackedTime    int64                    `xorm:"-"`
142
	Assignees           []*user_model.User       `xorm:"-"`
143

144
	// IsLocked limits commenting abilities to users on an issue
145
	// with write access
146
	IsLocked bool `xorm:"NOT NULL DEFAULT false"`
147

148
	// For view issue page.
149
	ShowRole RoleDescriptor `xorm:"-"`
150
}
151

152
var (
153
	issueTasksPat     = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
154
	issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
155
)
156

157
// IssueIndex represents the issue index table
158
type IssueIndex db.ResourceIndex
159

160
func init() {
161
	db.RegisterModel(new(Issue))
162
	db.RegisterModel(new(IssueIndex))
163
}
164

165
// LoadTotalTimes load total tracked time
166
func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
167
	opts := FindTrackedTimesOptions{IssueID: issue.ID}
168
	issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
169
	if err != nil {
170
		return err
171
	}
172
	return nil
173
}
174

175
// IsOverdue checks if the issue is overdue
176
func (issue *Issue) IsOverdue() bool {
177
	if issue.IsClosed {
178
		return issue.ClosedUnix >= issue.DeadlineUnix
179
	}
180
	return timeutil.TimeStampNow() >= issue.DeadlineUnix
181
}
182

183
// LoadRepo loads issue's repository
184
func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
185
	if issue.Repo == nil && issue.RepoID != 0 {
186
		issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
187
		if err != nil {
188
			return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
189
		}
190
	}
191
	return nil
192
}
193

194
func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
195
	if issue.isAttachmentsLoaded || issue.Attachments != nil {
196
		return nil
197
	}
198

199
	issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
200
	if err != nil {
201
		return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
202
	}
203
	issue.isAttachmentsLoaded = true
204
	return nil
205
}
206

207
// IsTimetrackerEnabled returns true if the repo enables timetracking
208
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
209
	if err := issue.LoadRepo(ctx); err != nil {
210
		log.Error(fmt.Sprintf("loadRepo: %v", err))
211
		return false
212
	}
213
	return issue.Repo.IsTimetrackerEnabled(ctx)
214
}
215

216
// LoadPoster loads poster
217
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
218
	if issue.Poster == nil && issue.PosterID != 0 {
219
		issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
220
		if err != nil {
221
			issue.PosterID = user_model.GhostUserID
222
			issue.Poster = user_model.NewGhostUser()
223
			if !user_model.IsErrUserNotExist(err) {
224
				return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
225
			}
226
			return nil
227
		}
228
	}
229
	return err
230
}
231

232
// LoadPullRequest loads pull request info
233
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
234
	if issue.IsPull {
235
		if issue.PullRequest == nil && issue.ID != 0 {
236
			issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
237
			if err != nil {
238
				if IsErrPullRequestNotExist(err) {
239
					return err
240
				}
241
				return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
242
			}
243
		}
244
		if issue.PullRequest != nil {
245
			issue.PullRequest.Issue = issue
246
		}
247
	}
248
	return nil
249
}
250

251
func (issue *Issue) loadComments(ctx context.Context) (err error) {
252
	return issue.loadCommentsByType(ctx, CommentTypeUndefined)
253
}
254

255
// LoadDiscussComments loads discuss comments
256
func (issue *Issue) LoadDiscussComments(ctx context.Context) error {
257
	return issue.loadCommentsByType(ctx, CommentTypeComment)
258
}
259

260
func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) {
261
	if issue.Comments != nil {
262
		return nil
263
	}
264
	issue.Comments, err = FindComments(ctx, &FindCommentsOptions{
265
		IssueID: issue.ID,
266
		Type:    tp,
267
	})
268
	return err
269
}
270

271
func (issue *Issue) loadReactions(ctx context.Context) (err error) {
272
	if issue.Reactions != nil {
273
		return nil
274
	}
275
	reactions, _, err := FindReactions(ctx, FindReactionsOptions{
276
		IssueID: issue.ID,
277
	})
278
	if err != nil {
279
		return err
280
	}
281
	if err = issue.LoadRepo(ctx); err != nil {
282
		return err
283
	}
284
	// Load reaction user data
285
	if _, err := reactions.LoadUsers(ctx, issue.Repo); err != nil {
286
		return err
287
	}
288

289
	// Cache comments to map
290
	comments := make(map[int64]*Comment)
291
	for _, comment := range issue.Comments {
292
		comments[comment.ID] = comment
293
	}
294
	// Add reactions either to issue or comment
295
	for _, react := range reactions {
296
		if react.CommentID == 0 {
297
			issue.Reactions = append(issue.Reactions, react)
298
		} else if comment, ok := comments[react.CommentID]; ok {
299
			comment.Reactions = append(comment.Reactions, react)
300
		}
301
	}
302
	return nil
303
}
304

305
// LoadMilestone load milestone of this issue.
306
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
307
	if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
308
		issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
309
		if err != nil && !IsErrMilestoneNotExist(err) {
310
			return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
311
		}
312
		issue.isMilestoneLoaded = true
313
	}
314
	return nil
315
}
316

317
// LoadAttributes loads the attribute of this issue.
318
func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
319
	if err = issue.LoadRepo(ctx); err != nil {
320
		return err
321
	}
322

323
	if err = issue.LoadPoster(ctx); err != nil {
324
		return err
325
	}
326

327
	if err = issue.LoadLabels(ctx); err != nil {
328
		return err
329
	}
330

331
	if err = issue.LoadMilestone(ctx); err != nil {
332
		return err
333
	}
334

335
	if err = issue.LoadProject(ctx); err != nil {
336
		return err
337
	}
338

339
	if err = issue.LoadAssignees(ctx); err != nil {
340
		return err
341
	}
342

343
	if err = issue.LoadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) {
344
		// It is possible pull request is not yet created.
345
		return err
346
	}
347

348
	if err = issue.LoadAttachments(ctx); err != nil {
349
		return err
350
	}
351

352
	if err = issue.loadComments(ctx); err != nil {
353
		return err
354
	}
355

356
	if err = issue.Comments.LoadAttributes(ctx); err != nil {
357
		return err
358
	}
359
	if issue.IsTimetrackerEnabled(ctx) {
360
		if err = issue.LoadTotalTimes(ctx); err != nil {
361
			return err
362
		}
363
	}
364

365
	return issue.loadReactions(ctx)
366
}
367

368
func (issue *Issue) ResetAttributesLoaded() {
369
	issue.isLabelsLoaded = false
370
	issue.isMilestoneLoaded = false
371
	issue.isAttachmentsLoaded = false
372
	issue.isAssigneeLoaded = false
373
}
374

375
// GetIsRead load the `IsRead` field of the issue
376
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
377
	issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
378
	if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
379
		return err
380
	} else if !has {
381
		issue.IsRead = false
382
		return nil
383
	}
384
	issue.IsRead = issueUser.IsRead
385
	return nil
386
}
387

388
// APIURL returns the absolute APIURL to this issue.
389
func (issue *Issue) APIURL(ctx context.Context) string {
390
	if issue.Repo == nil {
391
		err := issue.LoadRepo(ctx)
392
		if err != nil {
393
			log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
394
			return ""
395
		}
396
	}
397
	return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
398
}
399

400
// HTMLURL returns the absolute URL to this issue.
401
func (issue *Issue) HTMLURL() string {
402
	var path string
403
	if issue.IsPull {
404
		path = "pulls"
405
	} else {
406
		path = "issues"
407
	}
408
	return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
409
}
410

411
// Link returns the issue's relative URL.
412
func (issue *Issue) Link() string {
413
	var path string
414
	if issue.IsPull {
415
		path = "pulls"
416
	} else {
417
		path = "issues"
418
	}
419
	return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index)
420
}
421

422
// DiffURL returns the absolute URL to this diff
423
func (issue *Issue) DiffURL() string {
424
	if issue.IsPull {
425
		return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
426
	}
427
	return ""
428
}
429

430
// PatchURL returns the absolute URL to this patch
431
func (issue *Issue) PatchURL() string {
432
	if issue.IsPull {
433
		return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
434
	}
435
	return ""
436
}
437

438
// State returns string representation of issue status.
439
func (issue *Issue) State() api.StateType {
440
	if issue.IsClosed {
441
		return api.StateClosed
442
	}
443
	return api.StateOpen
444
}
445

446
// HashTag returns unique hash tag for issue.
447
func (issue *Issue) HashTag() string {
448
	return fmt.Sprintf("issue-%d", issue.ID)
449
}
450

451
// IsPoster returns true if given user by ID is the poster.
452
func (issue *Issue) IsPoster(uid int64) bool {
453
	return issue.OriginalAuthorID == 0 && issue.PosterID == uid
454
}
455

456
// GetTasks returns the amount of tasks in the issues content
457
func (issue *Issue) GetTasks() int {
458
	return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
459
}
460

461
// GetTasksDone returns the amount of completed tasks in the issues content
462
func (issue *Issue) GetTasksDone() int {
463
	return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
464
}
465

466
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
467
func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
468
	if issue.IsClosed {
469
		return issue.ClosedUnix
470
	}
471
	return issue.CreatedUnix
472
}
473

474
// GetLastEventLabel returns the localization label for the current issue.
475
func (issue *Issue) GetLastEventLabel() string {
476
	if issue.IsClosed {
477
		if issue.IsPull && issue.PullRequest.HasMerged {
478
			return "repo.pulls.merged_by"
479
		}
480
		return "repo.issues.closed_by"
481
	}
482
	return "repo.issues.opened_by"
483
}
484

485
// GetLastComment return last comment for the current issue.
486
func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
487
	var c Comment
488
	exist, err := db.GetEngine(ctx).Where("type = ?", CommentTypeComment).
489
		And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c)
490
	if err != nil {
491
		return nil, err
492
	}
493
	if !exist {
494
		return nil, nil
495
	}
496
	return &c, nil
497
}
498

499
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
500
func (issue *Issue) GetLastEventLabelFake() string {
501
	if issue.IsClosed {
502
		if issue.IsPull && issue.PullRequest.HasMerged {
503
			return "repo.pulls.merged_by_fake"
504
		}
505
		return "repo.issues.closed_by_fake"
506
	}
507
	return "repo.issues.opened_by_fake"
508
}
509

510
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
511
func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
512
	if index < 1 {
513
		return nil, ErrIssueNotExist{}
514
	}
515
	issue := &Issue{
516
		RepoID: repoID,
517
		Index:  index,
518
	}
519
	has, err := db.GetEngine(ctx).Get(issue)
520
	if err != nil {
521
		return nil, err
522
	} else if !has {
523
		return nil, ErrIssueNotExist{0, repoID, index}
524
	}
525
	return issue, nil
526
}
527

528
// GetIssueWithAttrsByIndex returns issue by index in a repository.
529
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
530
	issue, err := GetIssueByIndex(ctx, repoID, index)
531
	if err != nil {
532
		return nil, err
533
	}
534
	return issue, issue.LoadAttributes(ctx)
535
}
536

537
// GetIssueByID returns an issue by given ID.
538
func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
539
	issue := new(Issue)
540
	has, err := db.GetEngine(ctx).ID(id).Get(issue)
541
	if err != nil {
542
		return nil, err
543
	} else if !has {
544
		return nil, ErrIssueNotExist{id, 0, 0}
545
	}
546
	return issue, nil
547
}
548

549
// GetIssuesByIDs return issues with the given IDs.
550
// If keepOrder is true, the order of the returned issues will be the same as the given IDs.
551
func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) {
552
	issues := make([]*Issue, 0, len(issueIDs))
553

554
	if err := db.GetEngine(ctx).In("id", issueIDs).Find(&issues); err != nil {
555
		return nil, err
556
	}
557

558
	if len(keepOrder) > 0 && keepOrder[0] {
559
		m := make(map[int64]*Issue, len(issues))
560
		appended := container.Set[int64]{}
561
		for _, issue := range issues {
562
			m[issue.ID] = issue
563
		}
564
		issues = issues[:0]
565
		for _, id := range issueIDs {
566
			if issue, ok := m[id]; ok && !appended.Contains(id) { // make sure the id is existed and not appended
567
				appended.Add(id)
568
				issues = append(issues, issue)
569
			}
570
		}
571
	}
572

573
	return issues, nil
574
}
575

576
// GetIssueIDsByRepoID returns all issue ids by repo id
577
func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) {
578
	ids := make([]int64, 0, 10)
579
	err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids)
580
	return ids, err
581
}
582

583
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
584
// but skips joining with `user` for performance reasons.
585
// User permissions must be verified elsewhere if required.
586
func GetParticipantsIDsByIssueID(ctx context.Context, issueID int64) ([]int64, error) {
587
	userIDs := make([]int64, 0, 5)
588
	return userIDs, db.GetEngine(ctx).
589
		Table("comment").
590
		Cols("poster_id").
591
		Where("issue_id = ?", issueID).
592
		And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
593
		Distinct("poster_id").
594
		Find(&userIDs)
595
}
596

597
// IsUserParticipantsOfIssue return true if user is participants of an issue
598
func IsUserParticipantsOfIssue(ctx context.Context, user *user_model.User, issue *Issue) bool {
599
	userIDs, err := issue.GetParticipantIDsByIssue(ctx)
600
	if err != nil {
601
		log.Error(err.Error())
602
		return false
603
	}
604
	return slices.Contains(userIDs, user.ID)
605
}
606

607
// DependencyInfo represents high level information about an issue which is a dependency of another issue.
608
type DependencyInfo struct {
609
	Issue                 `xorm:"extends"`
610
	repo_model.Repository `xorm:"extends"`
611
}
612

613
// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
614
func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
615
	if issue == nil {
616
		return nil, nil
617
	}
618
	userIDs := make([]int64, 0, 5)
619
	if err := db.GetEngine(ctx).Table("comment").Cols("poster_id").
620
		Where("`comment`.issue_id = ?", issue.ID).
621
		And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
622
		And("`user`.is_active = ?", true).
623
		And("`user`.prohibit_login = ?", false).
624
		Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
625
		Distinct("poster_id").
626
		Find(&userIDs); err != nil {
627
		return nil, fmt.Errorf("get poster IDs: %w", err)
628
	}
629
	if !slices.Contains(userIDs, issue.PosterID) {
630
		return append(userIDs, issue.PosterID), nil
631
	}
632
	return userIDs, nil
633
}
634

635
// BlockedByDependencies finds all Dependencies an issue is blocked by
636
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
637
	sess := db.GetEngine(ctx).
638
		Table("issue").
639
		Join("INNER", "repository", "repository.id = issue.repo_id").
640
		Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
641
		Where("issue_id = ?", issue.ID).
642
		// sort by repo id then created date, with the issues of the same repo at the beginning of the list
643
		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
644
	if opts.Page != 0 {
645
		sess = db.SetSessionPagination(sess, &opts)
646
	}
647
	err = sess.Find(&issueDeps)
648

649
	for _, depInfo := range issueDeps {
650
		depInfo.Issue.Repo = &depInfo.Repository
651
	}
652

653
	return issueDeps, err
654
}
655

656
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
657
func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
658
	err = db.GetEngine(ctx).
659
		Table("issue").
660
		Join("INNER", "repository", "repository.id = issue.repo_id").
661
		Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
662
		Where("dependency_id = ?", issue.ID).
663
		// sort by repo id then created date, with the issues of the same repo at the beginning of the list
664
		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
665
		Find(&issueDeps)
666

667
	for _, depInfo := range issueDeps {
668
		depInfo.Issue.Repo = &depInfo.Repository
669
	}
670

671
	return issueDeps, err
672
}
673

674
func migratedIssueCond(tp api.GitServiceType) builder.Cond {
675
	return builder.In("issue_id",
676
		builder.Select("issue.id").
677
			From("issue").
678
			InnerJoin("repository", "issue.repo_id = repository.id").
679
			Where(builder.Eq{
680
				"repository.original_service_type": tp,
681
			}),
682
	)
683
}
684

685
// RemapExternalUser ExternalUserRemappable interface
686
func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error {
687
	issue.OriginalAuthor = externalName
688
	issue.OriginalAuthorID = externalID
689
	issue.PosterID = userID
690
	return nil
691
}
692

693
// GetUserID ExternalUserRemappable interface
694
func (issue *Issue) GetUserID() int64 { return issue.PosterID }
695

696
// GetExternalName ExternalUserRemappable interface
697
func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
698

699
// GetExternalID ExternalUserRemappable interface
700
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
701

702
// HasOriginalAuthor returns if an issue was migrated and has an original author.
703
func (issue *Issue) HasOriginalAuthor() bool {
704
	return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
705
}
706

707
var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched")
708

709
// IsPinned returns if a Issue is pinned
710
func (issue *Issue) IsPinned() bool {
711
	return issue.PinOrder != 0
712
}
713

714
// Pin pins a Issue
715
func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error {
716
	// If the Issue is already pinned, we don't need to pin it twice
717
	if issue.IsPinned() {
718
		return nil
719
	}
720

721
	var maxPin int
722
	_, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
723
	if err != nil {
724
		return err
725
	}
726

727
	// Check if the maximum allowed Pins reached
728
	if maxPin >= setting.Repository.Issue.MaxPinned {
729
		return ErrIssueMaxPinReached
730
	}
731

732
	_, err = db.GetEngine(ctx).Table("issue").
733
		Where("id = ?", issue.ID).
734
		Update(map[string]any{
735
			"pin_order": maxPin + 1,
736
		})
737
	if err != nil {
738
		return err
739
	}
740

741
	// Add the pin event to the history
742
	opts := &CreateCommentOptions{
743
		Type:  CommentTypePin,
744
		Doer:  user,
745
		Repo:  issue.Repo,
746
		Issue: issue,
747
	}
748
	if _, err = CreateComment(ctx, opts); err != nil {
749
		return err
750
	}
751

752
	return nil
753
}
754

755
// UnpinIssue unpins a Issue
756
func (issue *Issue) Unpin(ctx context.Context, user *user_model.User) error {
757
	// If the Issue is not pinned, we don't need to unpin it
758
	if !issue.IsPinned() {
759
		return nil
760
	}
761

762
	// This sets the Pin for all Issues that come after the unpined Issue to the correct value
763
	_, err := db.GetEngine(ctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
764
	if err != nil {
765
		return err
766
	}
767

768
	_, err = db.GetEngine(ctx).Table("issue").
769
		Where("id = ?", issue.ID).
770
		Update(map[string]any{
771
			"pin_order": 0,
772
		})
773
	if err != nil {
774
		return err
775
	}
776

777
	// Add the unpin event to the history
778
	opts := &CreateCommentOptions{
779
		Type:  CommentTypeUnpin,
780
		Doer:  user,
781
		Repo:  issue.Repo,
782
		Issue: issue,
783
	}
784
	if _, err = CreateComment(ctx, opts); err != nil {
785
		return err
786
	}
787

788
	return nil
789
}
790

791
// PinOrUnpin pins or unpins a Issue
792
func (issue *Issue) PinOrUnpin(ctx context.Context, user *user_model.User) error {
793
	if !issue.IsPinned() {
794
		return issue.Pin(ctx, user)
795
	}
796

797
	return issue.Unpin(ctx, user)
798
}
799

800
// MovePin moves a Pinned Issue to a new Position
801
func (issue *Issue) MovePin(ctx context.Context, newPosition int) error {
802
	// If the Issue is not pinned, we can't move them
803
	if !issue.IsPinned() {
804
		return nil
805
	}
806

807
	if newPosition < 1 {
808
		return fmt.Errorf("The Position can't be lower than 1")
809
	}
810

811
	dbctx, committer, err := db.TxContext(ctx)
812
	if err != nil {
813
		return err
814
	}
815
	defer committer.Close()
816

817
	var maxPin int
818
	_, err = db.GetEngine(dbctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
819
	if err != nil {
820
		return err
821
	}
822

823
	// If the new Position bigger than the current Maximum, set it to the Maximum
824
	if newPosition > maxPin+1 {
825
		newPosition = maxPin + 1
826
	}
827

828
	// Lower the Position of all Pinned Issue that came after the current Position
829
	_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
830
	if err != nil {
831
		return err
832
	}
833

834
	// Higher the Position of all Pinned Issues that comes after the new Position
835
	_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ?", issue.RepoID, issue.IsPull, newPosition)
836
	if err != nil {
837
		return err
838
	}
839

840
	_, err = db.GetEngine(dbctx).Table("issue").
841
		Where("id = ?", issue.ID).
842
		Update(map[string]any{
843
			"pin_order": newPosition,
844
		})
845
	if err != nil {
846
		return err
847
	}
848

849
	return committer.Commit()
850
}
851

852
// GetPinnedIssues returns the pinned Issues for the given Repo and type
853
func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) {
854
	issues := make(IssueList, 0)
855

856
	err := db.GetEngine(ctx).
857
		Table("issue").
858
		Where("repo_id = ?", repoID).
859
		And("is_pull = ?", isPull).
860
		And("pin_order > 0").
861
		OrderBy("pin_order").
862
		Find(&issues)
863
	if err != nil {
864
		return nil, err
865
	}
866

867
	err = issues.LoadAttributes(ctx)
868
	if err != nil {
869
		return nil, err
870
	}
871

872
	return issues, nil
873
}
874

875
// IsNewPinnedAllowed returns if a new Issue or Pull request can be pinned
876
func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) {
877
	var maxPin int
878
	_, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ? AND pin_order > 0", repoID, isPull).Get(&maxPin)
879
	if err != nil {
880
		return false, err
881
	}
882

883
	return maxPin < setting.Repository.Issue.MaxPinned, nil
884
}
885

886
// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
887
func IsErrIssueMaxPinReached(err error) bool {
888
	return err == ErrIssueMaxPinReached
889
}
890

891
// InsertIssues insert issues to database
892
func InsertIssues(ctx context.Context, issues ...*Issue) error {
893
	ctx, committer, err := db.TxContext(ctx)
894
	if err != nil {
895
		return err
896
	}
897
	defer committer.Close()
898

899
	for _, issue := range issues {
900
		if err := insertIssue(ctx, issue); err != nil {
901
			return err
902
		}
903
	}
904
	return committer.Commit()
905
}
906

907
func insertIssue(ctx context.Context, issue *Issue) error {
908
	sess := db.GetEngine(ctx)
909
	if _, err := sess.NoAutoTime().Insert(issue); err != nil {
910
		return err
911
	}
912
	issueLabels := make([]IssueLabel, 0, len(issue.Labels))
913
	for _, label := range issue.Labels {
914
		issueLabels = append(issueLabels, IssueLabel{
915
			IssueID: issue.ID,
916
			LabelID: label.ID,
917
		})
918
	}
919
	if len(issueLabels) > 0 {
920
		if _, err := sess.Insert(issueLabels); err != nil {
921
			return err
922
		}
923
	}
924

925
	for _, reaction := range issue.Reactions {
926
		reaction.IssueID = issue.ID
927
	}
928

929
	if len(issue.Reactions) > 0 {
930
		if _, err := sess.Insert(issue.Reactions); err != nil {
931
			return err
932
		}
933
	}
934

935
	return nil
936
}
937

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.