code.gitea.io/gitea@v1.21.7/models/issues/comment.go (about) 1 // Copyright 2018 The Gitea Authors. 2 // Copyright 2016 The Gogs Authors. 3 // All rights reserved. 4 // SPDX-License-Identifier: MIT 5 6 package issues 7 8 import ( 9 "context" 10 "fmt" 11 "strconv" 12 "unicode/utf8" 13 14 "code.gitea.io/gitea/models/db" 15 git_model "code.gitea.io/gitea/models/git" 16 "code.gitea.io/gitea/models/organization" 17 project_model "code.gitea.io/gitea/models/project" 18 repo_model "code.gitea.io/gitea/models/repo" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/container" 21 "code.gitea.io/gitea/modules/git" 22 "code.gitea.io/gitea/modules/json" 23 "code.gitea.io/gitea/modules/log" 24 "code.gitea.io/gitea/modules/references" 25 "code.gitea.io/gitea/modules/structs" 26 "code.gitea.io/gitea/modules/timeutil" 27 "code.gitea.io/gitea/modules/translation" 28 "code.gitea.io/gitea/modules/util" 29 30 "xorm.io/builder" 31 "xorm.io/xorm" 32 ) 33 34 // ErrCommentNotExist represents a "CommentNotExist" kind of error. 35 type ErrCommentNotExist struct { 36 ID int64 37 IssueID int64 38 } 39 40 // IsErrCommentNotExist checks if an error is a ErrCommentNotExist. 41 func IsErrCommentNotExist(err error) bool { 42 _, ok := err.(ErrCommentNotExist) 43 return ok 44 } 45 46 func (err ErrCommentNotExist) Error() string { 47 return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID) 48 } 49 50 func (err ErrCommentNotExist) Unwrap() error { 51 return util.ErrNotExist 52 } 53 54 // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. 55 type CommentType int 56 57 // CommentTypeUndefined is used to search for comments of any type 58 const CommentTypeUndefined CommentType = -1 59 60 const ( 61 CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) 62 63 CommentTypeReopen // 1 64 CommentTypeClose // 2 65 66 CommentTypeIssueRef // 3 References. 67 CommentTypeCommitRef // 4 Reference from a commit (not part of a pull request) 68 CommentTypeCommentRef // 5 Reference from a comment 69 CommentTypePullRef // 6 Reference from a pull request 70 71 CommentTypeLabel // 7 Labels changed 72 CommentTypeMilestone // 8 Milestone changed 73 CommentTypeAssignees // 9 Assignees changed 74 CommentTypeChangeTitle // 10 Change Title 75 CommentTypeDeleteBranch // 11 Delete Branch 76 77 CommentTypeStartTracking // 12 Start a stopwatch for time tracking 78 CommentTypeStopTracking // 13 Stop a stopwatch for time tracking 79 CommentTypeAddTimeManual // 14 Add time manual for time tracking 80 CommentTypeCancelTracking // 15 Cancel a stopwatch for time tracking 81 CommentTypeAddedDeadline // 16 Added a due date 82 CommentTypeModifiedDeadline // 17 Modified the due date 83 CommentTypeRemovedDeadline // 18 Removed a due date 84 85 CommentTypeAddDependency // 19 Dependency added 86 CommentTypeRemoveDependency // 20 Dependency removed 87 88 CommentTypeCode // 21 Comment a line of code 89 CommentTypeReview // 22 Reviews a pull request by giving general feedback 90 91 CommentTypeLock // 23 Lock an issue, giving only collaborators access 92 CommentTypeUnlock // 24 Unlocks a previously locked issue 93 94 CommentTypeChangeTargetBranch // 25 Change pull request's target branch 95 96 CommentTypeDeleteTimeManual // 26 Delete time manual for time tracking 97 98 CommentTypeReviewRequest // 27 add or remove Request from one 99 CommentTypeMergePull // 28 merge pull request 100 CommentTypePullRequestPush // 29 push to PR head branch 101 102 CommentTypeProject // 30 Project changed 103 CommentTypeProjectBoard // 31 Project board changed 104 105 CommentTypeDismissReview // 32 Dismiss Review 106 107 CommentTypeChangeIssueRef // 33 Change issue ref 108 109 CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed 110 CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed 111 112 CommentTypePin // 36 pin Issue 113 CommentTypeUnpin // 37 unpin Issue 114 ) 115 116 var commentStrings = []string{ 117 "comment", 118 "reopen", 119 "close", 120 "issue_ref", 121 "commit_ref", 122 "comment_ref", 123 "pull_ref", 124 "label", 125 "milestone", 126 "assignees", 127 "change_title", 128 "delete_branch", 129 "start_tracking", 130 "stop_tracking", 131 "add_time_manual", 132 "cancel_tracking", 133 "added_deadline", 134 "modified_deadline", 135 "removed_deadline", 136 "add_dependency", 137 "remove_dependency", 138 "code", 139 "review", 140 "lock", 141 "unlock", 142 "change_target_branch", 143 "delete_time_manual", 144 "review_request", 145 "merge_pull", 146 "pull_push", 147 "project", 148 "project_board", 149 "dismiss_review", 150 "change_issue_ref", 151 "pull_scheduled_merge", 152 "pull_cancel_scheduled_merge", 153 "pin", 154 "unpin", 155 } 156 157 func (t CommentType) String() string { 158 return commentStrings[t] 159 } 160 161 func AsCommentType(typeName string) CommentType { 162 for index, name := range commentStrings { 163 if typeName == name { 164 return CommentType(index) 165 } 166 } 167 return CommentTypeUndefined 168 } 169 170 func (t CommentType) HasContentSupport() bool { 171 switch t { 172 case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview: 173 return true 174 } 175 return false 176 } 177 178 func (t CommentType) HasAttachmentSupport() bool { 179 switch t { 180 case CommentTypeComment, CommentTypeCode, CommentTypeReview: 181 return true 182 } 183 return false 184 } 185 186 // RoleInRepo presents the user's participation in the repo 187 type RoleInRepo string 188 189 // RoleDescriptor defines comment "role" tags 190 type RoleDescriptor struct { 191 IsPoster bool 192 RoleInRepo RoleInRepo 193 } 194 195 // Enumerate all the role tags. 196 const ( 197 RoleRepoOwner RoleInRepo = "owner" 198 RoleRepoMember RoleInRepo = "member" 199 RoleRepoCollaborator RoleInRepo = "collaborator" 200 RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor" 201 RoleRepoContributor RoleInRepo = "contributor" 202 ) 203 204 // LocaleString returns the locale string name of the role 205 func (r RoleInRepo) LocaleString(lang translation.Locale) string { 206 return lang.Tr("repo.issues.role." + string(r)) 207 } 208 209 // LocaleHelper returns the locale tooltip of the role 210 func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { 211 return lang.Tr("repo.issues.role." + string(r) + "_helper") 212 } 213 214 // Comment represents a comment in commit and issue page. 215 type Comment struct { 216 ID int64 `xorm:"pk autoincr"` 217 Type CommentType `xorm:"INDEX"` 218 PosterID int64 `xorm:"INDEX"` 219 Poster *user_model.User `xorm:"-"` 220 OriginalAuthor string 221 OriginalAuthorID int64 222 IssueID int64 `xorm:"INDEX"` 223 Issue *Issue `xorm:"-"` 224 LabelID int64 225 Label *Label `xorm:"-"` 226 AddedLabels []*Label `xorm:"-"` 227 RemovedLabels []*Label `xorm:"-"` 228 OldProjectID int64 229 ProjectID int64 230 OldProject *project_model.Project `xorm:"-"` 231 Project *project_model.Project `xorm:"-"` 232 OldMilestoneID int64 233 MilestoneID int64 234 OldMilestone *Milestone `xorm:"-"` 235 Milestone *Milestone `xorm:"-"` 236 TimeID int64 237 Time *TrackedTime `xorm:"-"` 238 AssigneeID int64 239 RemovedAssignee bool 240 Assignee *user_model.User `xorm:"-"` 241 AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"` 242 AssigneeTeam *organization.Team `xorm:"-"` 243 ResolveDoerID int64 244 ResolveDoer *user_model.User `xorm:"-"` 245 OldTitle string 246 NewTitle string 247 OldRef string 248 NewRef string 249 DependentIssueID int64 `xorm:"index"` // This is used by issue_service.deleteIssue 250 DependentIssue *Issue `xorm:"-"` 251 252 CommitID int64 253 Line int64 // - previous line / + proposed line 254 TreePath string 255 Content string `xorm:"LONGTEXT"` 256 RenderedContent string `xorm:"-"` 257 258 // Path represents the 4 lines of code cemented by this comment 259 Patch string `xorm:"-"` 260 PatchQuoted string `xorm:"LONGTEXT patch"` 261 262 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 263 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 264 265 // Reference issue in commit message 266 CommitSHA string `xorm:"VARCHAR(40)"` 267 268 Attachments []*repo_model.Attachment `xorm:"-"` 269 Reactions ReactionList `xorm:"-"` 270 271 // For view issue page. 272 ShowRole RoleDescriptor `xorm:"-"` 273 274 Review *Review `xorm:"-"` 275 ReviewID int64 `xorm:"index"` 276 Invalidated bool 277 278 // Reference an issue or pull from another comment, issue or PR 279 // All information is about the origin of the reference 280 RefRepoID int64 `xorm:"index"` // Repo where the referencing 281 RefIssueID int64 `xorm:"index"` 282 RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) 283 RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves 284 RefIsPull bool 285 286 RefRepo *repo_model.Repository `xorm:"-"` 287 RefIssue *Issue `xorm:"-"` 288 RefComment *Comment `xorm:"-"` 289 290 Commits []*git_model.SignCommitWithStatuses `xorm:"-"` 291 OldCommit string `xorm:"-"` 292 NewCommit string `xorm:"-"` 293 CommitsNum int64 `xorm:"-"` 294 IsForcePush bool `xorm:"-"` 295 } 296 297 func init() { 298 db.RegisterModel(new(Comment)) 299 } 300 301 // PushActionContent is content of push pull comment 302 type PushActionContent struct { 303 IsForcePush bool `json:"is_force_push"` 304 CommitIDs []string `json:"commit_ids"` 305 } 306 307 // LoadIssue loads the issue reference for the comment 308 func (c *Comment) LoadIssue(ctx context.Context) (err error) { 309 if c.Issue != nil { 310 return nil 311 } 312 c.Issue, err = GetIssueByID(ctx, c.IssueID) 313 return err 314 } 315 316 // BeforeInsert will be invoked by XORM before inserting a record 317 func (c *Comment) BeforeInsert() { 318 c.PatchQuoted = c.Patch 319 if !utf8.ValidString(c.Patch) { 320 c.PatchQuoted = strconv.Quote(c.Patch) 321 } 322 } 323 324 // BeforeUpdate will be invoked by XORM before updating a record 325 func (c *Comment) BeforeUpdate() { 326 c.PatchQuoted = c.Patch 327 if !utf8.ValidString(c.Patch) { 328 c.PatchQuoted = strconv.Quote(c.Patch) 329 } 330 } 331 332 // AfterLoad is invoked from XORM after setting the values of all fields of this object. 333 func (c *Comment) AfterLoad(session *xorm.Session) { 334 c.Patch = c.PatchQuoted 335 if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' { 336 unquoted, err := strconv.Unquote(c.PatchQuoted) 337 if err == nil { 338 c.Patch = unquoted 339 } 340 } 341 } 342 343 // LoadPoster loads comment poster 344 func (c *Comment) LoadPoster(ctx context.Context) (err error) { 345 if c.Poster != nil { 346 return nil 347 } 348 349 c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) 350 if err != nil { 351 if user_model.IsErrUserNotExist(err) { 352 c.PosterID = -1 353 c.Poster = user_model.NewGhostUser() 354 } else { 355 log.Error("getUserByID[%d]: %v", c.ID, err) 356 } 357 } 358 return err 359 } 360 361 // AfterDelete is invoked from XORM after the object is deleted. 362 func (c *Comment) AfterDelete(ctx context.Context) { 363 if c.ID <= 0 { 364 return 365 } 366 367 _, err := repo_model.DeleteAttachmentsByComment(ctx, c.ID, true) 368 if err != nil { 369 log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) 370 } 371 } 372 373 // HTMLURL formats a URL-string to the issue-comment 374 func (c *Comment) HTMLURL(ctx context.Context) string { 375 err := c.LoadIssue(ctx) 376 if err != nil { // Silently dropping errors :unamused: 377 log.Error("LoadIssue(%d): %v", c.IssueID, err) 378 return "" 379 } 380 err = c.Issue.LoadRepo(ctx) 381 if err != nil { // Silently dropping errors :unamused: 382 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 383 return "" 384 } 385 return c.Issue.HTMLURL() + c.hashLink(ctx) 386 } 387 388 // Link formats a relative URL-string to the issue-comment 389 func (c *Comment) Link(ctx context.Context) string { 390 err := c.LoadIssue(ctx) 391 if err != nil { // Silently dropping errors :unamused: 392 log.Error("LoadIssue(%d): %v", c.IssueID, err) 393 return "" 394 } 395 err = c.Issue.LoadRepo(ctx) 396 if err != nil { // Silently dropping errors :unamused: 397 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 398 return "" 399 } 400 return c.Issue.Link() + c.hashLink(ctx) 401 } 402 403 func (c *Comment) hashLink(ctx context.Context) string { 404 if c.Type == CommentTypeCode { 405 if c.ReviewID == 0 { 406 return "/files#" + c.HashTag() 407 } 408 if c.Review == nil { 409 if err := c.LoadReview(ctx); err != nil { 410 log.Warn("LoadReview(%d): %v", c.ReviewID, err) 411 return "/files#" + c.HashTag() 412 } 413 } 414 if c.Review.Type <= ReviewTypePending { 415 return "/files#" + c.HashTag() 416 } 417 } 418 return "#" + c.HashTag() 419 } 420 421 // APIURL formats a API-string to the issue-comment 422 func (c *Comment) APIURL(ctx context.Context) string { 423 err := c.LoadIssue(ctx) 424 if err != nil { // Silently dropping errors :unamused: 425 log.Error("LoadIssue(%d): %v", c.IssueID, err) 426 return "" 427 } 428 err = c.Issue.LoadRepo(ctx) 429 if err != nil { // Silently dropping errors :unamused: 430 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 431 return "" 432 } 433 434 return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID) 435 } 436 437 // IssueURL formats a URL-string to the issue 438 func (c *Comment) IssueURL(ctx context.Context) string { 439 err := c.LoadIssue(ctx) 440 if err != nil { // Silently dropping errors :unamused: 441 log.Error("LoadIssue(%d): %v", c.IssueID, err) 442 return "" 443 } 444 445 if c.Issue.IsPull { 446 return "" 447 } 448 449 err = c.Issue.LoadRepo(ctx) 450 if err != nil { // Silently dropping errors :unamused: 451 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 452 return "" 453 } 454 return c.Issue.HTMLURL() 455 } 456 457 // PRURL formats a URL-string to the pull-request 458 func (c *Comment) PRURL(ctx context.Context) string { 459 err := c.LoadIssue(ctx) 460 if err != nil { // Silently dropping errors :unamused: 461 log.Error("LoadIssue(%d): %v", c.IssueID, err) 462 return "" 463 } 464 465 err = c.Issue.LoadRepo(ctx) 466 if err != nil { // Silently dropping errors :unamused: 467 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 468 return "" 469 } 470 471 if !c.Issue.IsPull { 472 return "" 473 } 474 return c.Issue.HTMLURL() 475 } 476 477 // CommentHashTag returns unique hash tag for comment id. 478 func CommentHashTag(id int64) string { 479 return fmt.Sprintf("issuecomment-%d", id) 480 } 481 482 // HashTag returns unique hash tag for comment. 483 func (c *Comment) HashTag() string { 484 return CommentHashTag(c.ID) 485 } 486 487 // EventTag returns unique event hash tag for comment. 488 func (c *Comment) EventTag() string { 489 return fmt.Sprintf("event-%d", c.ID) 490 } 491 492 // LoadLabel if comment.Type is CommentTypeLabel, then load Label 493 func (c *Comment) LoadLabel(ctx context.Context) error { 494 var label Label 495 has, err := db.GetEngine(ctx).ID(c.LabelID).Get(&label) 496 if err != nil { 497 return err 498 } else if has { 499 c.Label = &label 500 } else { 501 // Ignore Label is deleted, but not clear this table 502 log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID) 503 } 504 505 return nil 506 } 507 508 // LoadProject if comment.Type is CommentTypeProject, then load project. 509 func (c *Comment) LoadProject(ctx context.Context) error { 510 if c.OldProjectID > 0 { 511 var oldProject project_model.Project 512 has, err := db.GetEngine(ctx).ID(c.OldProjectID).Get(&oldProject) 513 if err != nil { 514 return err 515 } else if has { 516 c.OldProject = &oldProject 517 } 518 } 519 520 if c.ProjectID > 0 { 521 var project project_model.Project 522 has, err := db.GetEngine(ctx).ID(c.ProjectID).Get(&project) 523 if err != nil { 524 return err 525 } else if has { 526 c.Project = &project 527 } 528 } 529 530 return nil 531 } 532 533 // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone 534 func (c *Comment) LoadMilestone(ctx context.Context) error { 535 if c.OldMilestoneID > 0 { 536 var oldMilestone Milestone 537 has, err := db.GetEngine(ctx).ID(c.OldMilestoneID).Get(&oldMilestone) 538 if err != nil { 539 return err 540 } else if has { 541 c.OldMilestone = &oldMilestone 542 } 543 } 544 545 if c.MilestoneID > 0 { 546 var milestone Milestone 547 has, err := db.GetEngine(ctx).ID(c.MilestoneID).Get(&milestone) 548 if err != nil { 549 return err 550 } else if has { 551 c.Milestone = &milestone 552 } 553 } 554 return nil 555 } 556 557 // LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored) 558 func (c *Comment) LoadAttachments(ctx context.Context) error { 559 if len(c.Attachments) > 0 { 560 return nil 561 } 562 563 var err error 564 c.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, c.ID) 565 if err != nil { 566 log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err) 567 } 568 return nil 569 } 570 571 // UpdateAttachments update attachments by UUIDs for the comment 572 func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { 573 ctx, committer, err := db.TxContext(ctx) 574 if err != nil { 575 return err 576 } 577 defer committer.Close() 578 579 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) 580 if err != nil { 581 return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) 582 } 583 for i := 0; i < len(attachments); i++ { 584 attachments[i].IssueID = c.IssueID 585 attachments[i].CommentID = c.ID 586 if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { 587 return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) 588 } 589 } 590 return committer.Commit() 591 } 592 593 // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees 594 func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { 595 var err error 596 597 if c.AssigneeID > 0 && c.Assignee == nil { 598 c.Assignee, err = user_model.GetUserByID(ctx, c.AssigneeID) 599 if err != nil { 600 if !user_model.IsErrUserNotExist(err) { 601 return err 602 } 603 c.Assignee = user_model.NewGhostUser() 604 } 605 } else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { 606 if err = c.LoadIssue(ctx); err != nil { 607 return err 608 } 609 610 if err = c.Issue.LoadRepo(ctx); err != nil { 611 return err 612 } 613 614 if err = c.Issue.Repo.LoadOwner(ctx); err != nil { 615 return err 616 } 617 618 if c.Issue.Repo.Owner.IsOrganization() { 619 c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID) 620 if err != nil && !organization.IsErrTeamNotExist(err) { 621 return err 622 } 623 } 624 } 625 return nil 626 } 627 628 // LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer 629 func (c *Comment) LoadResolveDoer(ctx context.Context) (err error) { 630 if c.ResolveDoerID == 0 || c.Type != CommentTypeCode { 631 return nil 632 } 633 c.ResolveDoer, err = user_model.GetUserByID(ctx, c.ResolveDoerID) 634 if err != nil { 635 if user_model.IsErrUserNotExist(err) { 636 c.ResolveDoer = user_model.NewGhostUser() 637 err = nil 638 } 639 } 640 return err 641 } 642 643 // IsResolved check if an code comment is resolved 644 func (c *Comment) IsResolved() bool { 645 return c.ResolveDoerID != 0 && c.Type == CommentTypeCode 646 } 647 648 // LoadDepIssueDetails loads Dependent Issue Details 649 func (c *Comment) LoadDepIssueDetails(ctx context.Context) (err error) { 650 if c.DependentIssueID <= 0 || c.DependentIssue != nil { 651 return nil 652 } 653 c.DependentIssue, err = GetIssueByID(ctx, c.DependentIssueID) 654 return err 655 } 656 657 // LoadTime loads the associated time for a CommentTypeAddTimeManual 658 func (c *Comment) LoadTime() error { 659 if c.Time != nil || c.TimeID == 0 { 660 return nil 661 } 662 var err error 663 c.Time, err = GetTrackedTimeByID(c.TimeID) 664 return err 665 } 666 667 func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { 668 if c.Reactions != nil { 669 return nil 670 } 671 c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{ 672 IssueID: c.IssueID, 673 CommentID: c.ID, 674 }) 675 if err != nil { 676 return err 677 } 678 // Load reaction user data 679 if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil { 680 return err 681 } 682 return nil 683 } 684 685 // LoadReactions loads comment reactions 686 func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) error { 687 return c.loadReactions(ctx, repo) 688 } 689 690 func (c *Comment) loadReview(ctx context.Context) (err error) { 691 if c.ReviewID == 0 { 692 return nil 693 } 694 if c.Review == nil { 695 if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil { 696 // review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them. 697 if c.Type == CommentTypeReviewRequest { 698 return nil 699 } 700 return err 701 } 702 } 703 c.Review.Issue = c.Issue 704 return nil 705 } 706 707 // LoadReview loads the associated review 708 func (c *Comment) LoadReview(ctx context.Context) error { 709 return c.loadReview(ctx) 710 } 711 712 // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes. 713 func (c *Comment) DiffSide() string { 714 if c.Line < 0 { 715 return "previous" 716 } 717 return "proposed" 718 } 719 720 // UnsignedLine returns the LOC of the code comment without + or - 721 func (c *Comment) UnsignedLine() uint64 { 722 if c.Line < 0 { 723 return uint64(c.Line * -1) 724 } 725 return uint64(c.Line) 726 } 727 728 // CodeCommentLink returns the url to a comment in code 729 func (c *Comment) CodeCommentLink(ctx context.Context) string { 730 err := c.LoadIssue(ctx) 731 if err != nil { // Silently dropping errors :unamused: 732 log.Error("LoadIssue(%d): %v", c.IssueID, err) 733 return "" 734 } 735 err = c.Issue.LoadRepo(ctx) 736 if err != nil { // Silently dropping errors :unamused: 737 log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) 738 return "" 739 } 740 return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag()) 741 } 742 743 // LoadPushCommits Load push commits 744 func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { 745 if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush { 746 return nil 747 } 748 749 var data PushActionContent 750 751 err = json.Unmarshal([]byte(c.Content), &data) 752 if err != nil { 753 return err 754 } 755 756 c.IsForcePush = data.IsForcePush 757 758 if c.IsForcePush { 759 if len(data.CommitIDs) != 2 { 760 return nil 761 } 762 c.OldCommit = data.CommitIDs[0] 763 c.NewCommit = data.CommitIDs[1] 764 } else { 765 repoPath := c.Issue.Repo.RepoPath() 766 gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath) 767 if err != nil { 768 return err 769 } 770 defer closer.Close() 771 772 c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) 773 c.CommitsNum = int64(len(c.Commits)) 774 } 775 776 return err 777 } 778 779 // CreateComment creates comment with context 780 func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { 781 ctx, committer, err := db.TxContext(ctx) 782 if err != nil { 783 return nil, err 784 } 785 defer committer.Close() 786 787 e := db.GetEngine(ctx) 788 var LabelID int64 789 if opts.Label != nil { 790 LabelID = opts.Label.ID 791 } 792 793 comment := &Comment{ 794 Type: opts.Type, 795 PosterID: opts.Doer.ID, 796 Poster: opts.Doer, 797 IssueID: opts.Issue.ID, 798 LabelID: LabelID, 799 OldMilestoneID: opts.OldMilestoneID, 800 MilestoneID: opts.MilestoneID, 801 OldProjectID: opts.OldProjectID, 802 ProjectID: opts.ProjectID, 803 TimeID: opts.TimeID, 804 RemovedAssignee: opts.RemovedAssignee, 805 AssigneeID: opts.AssigneeID, 806 AssigneeTeamID: opts.AssigneeTeamID, 807 CommitID: opts.CommitID, 808 CommitSHA: opts.CommitSHA, 809 Line: opts.LineNum, 810 Content: opts.Content, 811 OldTitle: opts.OldTitle, 812 NewTitle: opts.NewTitle, 813 OldRef: opts.OldRef, 814 NewRef: opts.NewRef, 815 DependentIssueID: opts.DependentIssueID, 816 TreePath: opts.TreePath, 817 ReviewID: opts.ReviewID, 818 Patch: opts.Patch, 819 RefRepoID: opts.RefRepoID, 820 RefIssueID: opts.RefIssueID, 821 RefCommentID: opts.RefCommentID, 822 RefAction: opts.RefAction, 823 RefIsPull: opts.RefIsPull, 824 IsForcePush: opts.IsForcePush, 825 Invalidated: opts.Invalidated, 826 } 827 if _, err = e.Insert(comment); err != nil { 828 return nil, err 829 } 830 831 if err = opts.Repo.LoadOwner(ctx); err != nil { 832 return nil, err 833 } 834 835 if err = updateCommentInfos(ctx, opts, comment); err != nil { 836 return nil, err 837 } 838 839 if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { 840 return nil, err 841 } 842 if err = committer.Commit(); err != nil { 843 return nil, err 844 } 845 return comment, nil 846 } 847 848 func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) { 849 // Check comment type. 850 switch opts.Type { 851 case CommentTypeCode: 852 if comment.ReviewID != 0 { 853 if comment.Review == nil { 854 if err := comment.loadReview(ctx); err != nil { 855 return err 856 } 857 } 858 if comment.Review.Type <= ReviewTypePending { 859 return nil 860 } 861 } 862 fallthrough 863 case CommentTypeComment: 864 if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { 865 return err 866 } 867 fallthrough 868 case CommentTypeReview: 869 // Check attachments 870 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) 871 if err != nil { 872 return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) 873 } 874 875 for i := range attachments { 876 attachments[i].IssueID = opts.Issue.ID 877 attachments[i].CommentID = comment.ID 878 // No assign value could be 0, so ignore AllCols(). 879 if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { 880 return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) 881 } 882 } 883 884 comment.Attachments = attachments 885 case CommentTypeReopen, CommentTypeClose: 886 if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil { 887 return err 888 } 889 } 890 // update the issue's updated_unix column 891 return UpdateIssueCols(ctx, opts.Issue, "updated_unix") 892 } 893 894 func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { 895 var content string 896 var commentType CommentType 897 898 // newDeadline = 0 means deleting 899 if newDeadlineUnix == 0 { 900 commentType = CommentTypeRemovedDeadline 901 content = issue.DeadlineUnix.Format("2006-01-02") 902 } else if issue.DeadlineUnix == 0 { 903 // Check if the new date was added or modified 904 // If the actual deadline is 0 => deadline added 905 commentType = CommentTypeAddedDeadline 906 content = newDeadlineUnix.Format("2006-01-02") 907 } else { // Otherwise modified 908 commentType = CommentTypeModifiedDeadline 909 content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02") 910 } 911 912 if err := issue.LoadRepo(ctx); err != nil { 913 return nil, err 914 } 915 916 opts := &CreateCommentOptions{ 917 Type: commentType, 918 Doer: doer, 919 Repo: issue.Repo, 920 Issue: issue, 921 Content: content, 922 } 923 comment, err := CreateComment(ctx, opts) 924 if err != nil { 925 return nil, err 926 } 927 return comment, nil 928 } 929 930 // Creates issue dependency comment 931 func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) { 932 cType := CommentTypeAddDependency 933 if !add { 934 cType = CommentTypeRemoveDependency 935 } 936 if err = issue.LoadRepo(ctx); err != nil { 937 return err 938 } 939 940 // Make two comments, one in each issue 941 opts := &CreateCommentOptions{ 942 Type: cType, 943 Doer: doer, 944 Repo: issue.Repo, 945 Issue: issue, 946 DependentIssueID: dependentIssue.ID, 947 } 948 if _, err = CreateComment(ctx, opts); err != nil { 949 return err 950 } 951 952 opts = &CreateCommentOptions{ 953 Type: cType, 954 Doer: doer, 955 Repo: issue.Repo, 956 Issue: dependentIssue, 957 DependentIssueID: issue.ID, 958 } 959 _, err = CreateComment(ctx, opts) 960 return err 961 } 962 963 // CreateCommentOptions defines options for creating comment 964 type CreateCommentOptions struct { 965 Type CommentType 966 Doer *user_model.User 967 Repo *repo_model.Repository 968 Issue *Issue 969 Label *Label 970 971 DependentIssueID int64 972 OldMilestoneID int64 973 MilestoneID int64 974 OldProjectID int64 975 ProjectID int64 976 TimeID int64 977 AssigneeID int64 978 AssigneeTeamID int64 979 RemovedAssignee bool 980 OldTitle string 981 NewTitle string 982 OldRef string 983 NewRef string 984 CommitID int64 985 CommitSHA string 986 Patch string 987 LineNum int64 988 TreePath string 989 ReviewID int64 990 Content string 991 Attachments []string // UUIDs of attachments 992 RefRepoID int64 993 RefIssueID int64 994 RefCommentID int64 995 RefAction references.XRefAction 996 RefIsPull bool 997 IsForcePush bool 998 Invalidated bool 999 } 1000 1001 // GetCommentByID returns the comment by given ID. 1002 func GetCommentByID(ctx context.Context, id int64) (*Comment, error) { 1003 c := new(Comment) 1004 has, err := db.GetEngine(ctx).ID(id).Get(c) 1005 if err != nil { 1006 return nil, err 1007 } else if !has { 1008 return nil, ErrCommentNotExist{id, 0} 1009 } 1010 return c, nil 1011 } 1012 1013 // FindCommentsOptions describes the conditions to Find comments 1014 type FindCommentsOptions struct { 1015 db.ListOptions 1016 RepoID int64 1017 IssueID int64 1018 ReviewID int64 1019 Since int64 1020 Before int64 1021 Line int64 1022 TreePath string 1023 Type CommentType 1024 IssueIDs []int64 1025 Invalidated util.OptionalBool 1026 IsPull util.OptionalBool 1027 } 1028 1029 // ToConds implements FindOptions interface 1030 func (opts *FindCommentsOptions) ToConds() builder.Cond { 1031 cond := builder.NewCond() 1032 if opts.RepoID > 0 { 1033 cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) 1034 } 1035 if opts.IssueID > 0 { 1036 cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID}) 1037 } else if len(opts.IssueIDs) > 0 { 1038 cond = cond.And(builder.In("comment.issue_id", opts.IssueIDs)) 1039 } 1040 if opts.ReviewID > 0 { 1041 cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID}) 1042 } 1043 if opts.Since > 0 { 1044 cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since}) 1045 } 1046 if opts.Before > 0 { 1047 cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before}) 1048 } 1049 if opts.Type != CommentTypeUndefined { 1050 cond = cond.And(builder.Eq{"comment.type": opts.Type}) 1051 } 1052 if opts.Line != 0 { 1053 cond = cond.And(builder.Eq{"comment.line": opts.Line}) 1054 } 1055 if len(opts.TreePath) > 0 { 1056 cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) 1057 } 1058 if !opts.Invalidated.IsNone() { 1059 cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) 1060 } 1061 if opts.IsPull != util.OptionalBoolNone { 1062 cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) 1063 } 1064 return cond 1065 } 1066 1067 // FindComments returns all comments according options 1068 func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { 1069 comments := make([]*Comment, 0, 10) 1070 sess := db.GetEngine(ctx).Where(opts.ToConds()) 1071 if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone { 1072 sess.Join("INNER", "issue", "issue.id = comment.issue_id") 1073 } 1074 1075 if opts.Page != 0 { 1076 sess = db.SetSessionPagination(sess, opts) 1077 } 1078 1079 // WARNING: If you change this order you will need to fix createCodeComment 1080 1081 return comments, sess. 1082 Asc("comment.created_unix"). 1083 Asc("comment.id"). 1084 Find(&comments) 1085 } 1086 1087 // CountComments count all comments according options by ignoring pagination 1088 func CountComments(ctx context.Context, opts *FindCommentsOptions) (int64, error) { 1089 sess := db.GetEngine(ctx).Where(opts.ToConds()) 1090 if opts.RepoID > 0 { 1091 sess.Join("INNER", "issue", "issue.id = comment.issue_id") 1092 } 1093 return sess.Count(&Comment{}) 1094 } 1095 1096 // UpdateCommentInvalidate updates comment invalidated column 1097 func UpdateCommentInvalidate(ctx context.Context, c *Comment) error { 1098 _, err := db.GetEngine(ctx).ID(c.ID).Cols("invalidated").Update(c) 1099 return err 1100 } 1101 1102 // UpdateComment updates information of comment. 1103 func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error { 1104 ctx, committer, err := db.TxContext(ctx) 1105 if err != nil { 1106 return err 1107 } 1108 defer committer.Close() 1109 sess := db.GetEngine(ctx) 1110 1111 if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil { 1112 return err 1113 } 1114 if err := c.LoadIssue(ctx); err != nil { 1115 return err 1116 } 1117 if err := c.AddCrossReferences(ctx, doer, true); err != nil { 1118 return err 1119 } 1120 if err := committer.Commit(); err != nil { 1121 return fmt.Errorf("Commit: %w", err) 1122 } 1123 1124 return nil 1125 } 1126 1127 // DeleteComment deletes the comment 1128 func DeleteComment(ctx context.Context, comment *Comment) error { 1129 e := db.GetEngine(ctx) 1130 if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil { 1131 return err 1132 } 1133 1134 if _, err := db.DeleteByBean(ctx, &ContentHistory{ 1135 CommentID: comment.ID, 1136 }); err != nil { 1137 return err 1138 } 1139 1140 if comment.Type == CommentTypeComment { 1141 if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { 1142 return err 1143 } 1144 } 1145 if _, err := e.Table("action"). 1146 Where("comment_id = ?", comment.ID). 1147 Update(map[string]any{ 1148 "is_deleted": true, 1149 }); err != nil { 1150 return err 1151 } 1152 1153 if err := comment.neuterCrossReferences(ctx); err != nil { 1154 return err 1155 } 1156 1157 return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID}) 1158 } 1159 1160 // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id 1161 func UpdateCommentsMigrationsByType(ctx context.Context, tp structs.GitServiceType, originalAuthorID string, posterID int64) error { 1162 _, err := db.GetEngine(ctx).Table("comment"). 1163 Join("INNER", "issue", "issue.id = comment.issue_id"). 1164 Join("INNER", "repository", "issue.repo_id = repository.id"). 1165 Where("repository.original_service_type = ?", tp). 1166 And("comment.original_author_id = ?", originalAuthorID). 1167 Update(map[string]any{ 1168 "poster_id": posterID, 1169 "original_author": "", 1170 "original_author_id": 0, 1171 }) 1172 return err 1173 } 1174 1175 // CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes 1176 func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) { 1177 if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge { 1178 return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ) 1179 } 1180 if err = pr.LoadIssue(ctx); err != nil { 1181 return nil, err 1182 } 1183 1184 if err = pr.LoadBaseRepo(ctx); err != nil { 1185 return nil, err 1186 } 1187 1188 comment, err = CreateComment(ctx, &CreateCommentOptions{ 1189 Type: typ, 1190 Doer: doer, 1191 Repo: pr.BaseRepo, 1192 Issue: pr.Issue, 1193 }) 1194 return comment, err 1195 } 1196 1197 // RemapExternalUser ExternalUserRemappable interface 1198 func (c *Comment) RemapExternalUser(externalName string, externalID, userID int64) error { 1199 c.OriginalAuthor = externalName 1200 c.OriginalAuthorID = externalID 1201 c.PosterID = userID 1202 return nil 1203 } 1204 1205 // GetUserID ExternalUserRemappable interface 1206 func (c *Comment) GetUserID() int64 { return c.PosterID } 1207 1208 // GetExternalName ExternalUserRemappable interface 1209 func (c *Comment) GetExternalName() string { return c.OriginalAuthor } 1210 1211 // GetExternalID ExternalUserRemappable interface 1212 func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID } 1213 1214 // CountCommentTypeLabelWithEmptyLabel count label comments with empty label 1215 func CountCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) { 1216 return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment)) 1217 } 1218 1219 // FixCommentTypeLabelWithEmptyLabel count label comments with empty label 1220 func FixCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) { 1221 return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment)) 1222 } 1223 1224 // CountCommentTypeLabelWithOutsideLabels count label comments with outside label 1225 func CountCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) { 1226 return db.GetEngine(ctx).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel). 1227 Table("comment"). 1228 Join("inner", "label", "label.id = comment.label_id"). 1229 Join("inner", "issue", "issue.id = comment.issue_id "). 1230 Join("inner", "repository", "issue.repo_id = repository.id"). 1231 Count() 1232 } 1233 1234 // FixCommentTypeLabelWithOutsideLabels count label comments with outside label 1235 func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) { 1236 res, err := db.GetEngine(ctx).Exec(`DELETE FROM comment WHERE comment.id IN ( 1237 SELECT il_too.id FROM ( 1238 SELECT com.id 1239 FROM comment AS com 1240 INNER JOIN label ON com.label_id = label.id 1241 INNER JOIN issue on issue.id = com.issue_id 1242 INNER JOIN repository ON issue.repo_id = repository.id 1243 WHERE 1244 com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)) 1245 ) AS il_too)`, CommentTypeLabel) 1246 if err != nil { 1247 return 0, err 1248 } 1249 1250 return res.RowsAffected() 1251 } 1252 1253 // HasOriginalAuthor returns if a comment was migrated and has an original author. 1254 func (c *Comment) HasOriginalAuthor() bool { 1255 return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 1256 } 1257 1258 // InsertIssueComments inserts many comments of issues. 1259 func InsertIssueComments(ctx context.Context, comments []*Comment) error { 1260 if len(comments) == 0 { 1261 return nil 1262 } 1263 1264 issueIDs := make(container.Set[int64]) 1265 for _, comment := range comments { 1266 issueIDs.Add(comment.IssueID) 1267 } 1268 1269 ctx, committer, err := db.TxContext(ctx) 1270 if err != nil { 1271 return err 1272 } 1273 defer committer.Close() 1274 for _, comment := range comments { 1275 if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { 1276 return err 1277 } 1278 1279 for _, reaction := range comment.Reactions { 1280 reaction.IssueID = comment.IssueID 1281 reaction.CommentID = comment.ID 1282 } 1283 if len(comment.Reactions) > 0 { 1284 if err := db.Insert(ctx, comment.Reactions); err != nil { 1285 return err 1286 } 1287 } 1288 } 1289 1290 for issueID := range issueIDs { 1291 if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", 1292 issueID, CommentTypeComment, issueID); err != nil { 1293 return err 1294 } 1295 } 1296 return committer.Commit() 1297 }