@@ -9,7 +9,9 @@ package issues
99import (
1010 "context"
1111 "fmt"
12+ "regexp"
1213 "strconv"
14+ "strings"
1315 "unicode/utf8"
1416
1517 "code.gitea.io/gitea/models/db"
@@ -21,6 +23,8 @@ import (
2123 "code.gitea.io/gitea/modules/git"
2224 "code.gitea.io/gitea/modules/json"
2325 "code.gitea.io/gitea/modules/log"
26+ "code.gitea.io/gitea/modules/markup"
27+ "code.gitea.io/gitea/modules/markup/markdown"
2428 "code.gitea.io/gitea/modules/references"
2529 "code.gitea.io/gitea/modules/structs"
2630 "code.gitea.io/gitea/modules/timeutil"
@@ -693,6 +697,31 @@ func (c *Comment) LoadReview() error {
693697 return c .loadReview (db .DefaultContext )
694698}
695699
700+ var notEnoughLines = regexp .MustCompile (`fatal: file .* has only \d+ lines?` )
701+
702+ func (c * Comment ) checkInvalidation (doer * user_model.User , repo * git.Repository , branch string ) error {
703+ // FIXME differentiate between previous and proposed line
704+ commit , err := repo .LineBlame (branch , repo .Path , c .TreePath , uint (c .UnsignedLine ()))
705+ if err != nil && (strings .Contains (err .Error (), "fatal: no such path" ) || notEnoughLines .MatchString (err .Error ())) {
706+ c .Invalidated = true
707+ return UpdateComment (c , doer )
708+ }
709+ if err != nil {
710+ return err
711+ }
712+ if c .CommitSHA != "" && c .CommitSHA != commit .ID .String () {
713+ c .Invalidated = true
714+ return UpdateComment (c , doer )
715+ }
716+ return nil
717+ }
718+
719+ // CheckInvalidation checks if the line of code comment got changed by another commit.
720+ // If the line got changed the comment is going to be invalidated.
721+ func (c * Comment ) CheckInvalidation (repo * git.Repository , doer * user_model.User , branch string ) error {
722+ return c .checkInvalidation (doer , repo , branch )
723+ }
724+
696725// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
697726func (c * Comment ) DiffSide () string {
698727 if c .Line < 0 {
@@ -1036,28 +1065,23 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
10361065// FindCommentsOptions describes the conditions to Find comments
10371066type FindCommentsOptions struct {
10381067 db.ListOptions
1039- RepoID int64
1040- IssueID int64
1041- ReviewID int64
1042- Since int64
1043- Before int64
1044- Line int64
1045- TreePath string
1046- Type CommentType
1047- IssueIDs []int64
1048- Invalidated util.OptionalBool
1049- }
1050-
1051- // ToConds implements FindOptions interface
1052- func (opts * FindCommentsOptions ) ToConds () builder.Cond {
1068+ RepoID int64
1069+ IssueID int64
1070+ ReviewID int64
1071+ Since int64
1072+ Before int64
1073+ Line int64
1074+ TreePath string
1075+ Type CommentType
1076+ }
1077+
1078+ func (opts * FindCommentsOptions ) toConds () builder.Cond {
10531079 cond := builder .NewCond ()
10541080 if opts .RepoID > 0 {
10551081 cond = cond .And (builder.Eq {"issue.repo_id" : opts .RepoID })
10561082 }
10571083 if opts .IssueID > 0 {
10581084 cond = cond .And (builder.Eq {"comment.issue_id" : opts .IssueID })
1059- } else if len (opts .IssueIDs ) > 0 {
1060- cond = cond .And (builder .In ("comment.issue_id" , opts .IssueIDs ))
10611085 }
10621086 if opts .ReviewID > 0 {
10631087 cond = cond .And (builder.Eq {"comment.review_id" : opts .ReviewID })
@@ -1077,16 +1101,13 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
10771101 if len (opts .TreePath ) > 0 {
10781102 cond = cond .And (builder.Eq {"comment.tree_path" : opts .TreePath })
10791103 }
1080- if ! opts .Invalidated .IsNone () {
1081- cond = cond .And (builder.Eq {"comment.invalidated" : opts .Invalidated .IsTrue ()})
1082- }
10831104 return cond
10841105}
10851106
10861107// FindComments returns all comments according options
10871108func FindComments (ctx context.Context , opts * FindCommentsOptions ) ([]* Comment , error ) {
10881109 comments := make ([]* Comment , 0 , 10 )
1089- sess := db .GetEngine (ctx ).Where (opts .ToConds ())
1110+ sess := db .GetEngine (ctx ).Where (opts .toConds ())
10901111 if opts .RepoID > 0 {
10911112 sess .Join ("INNER" , "issue" , "issue.id = comment.issue_id" )
10921113 }
@@ -1105,19 +1126,13 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, e
11051126
11061127// CountComments count all comments according options by ignoring pagination
11071128func CountComments (opts * FindCommentsOptions ) (int64 , error ) {
1108- sess := db .GetEngine (db .DefaultContext ).Where (opts .ToConds ())
1129+ sess := db .GetEngine (db .DefaultContext ).Where (opts .toConds ())
11091130 if opts .RepoID > 0 {
11101131 sess .Join ("INNER" , "issue" , "issue.id = comment.issue_id" )
11111132 }
11121133 return sess .Count (& Comment {})
11131134}
11141135
1115- // UpdateCommentInvalidate updates comment invalidated column
1116- func UpdateCommentInvalidate (ctx context.Context , c * Comment ) error {
1117- _ , err := db .GetEngine (ctx ).ID (c .ID ).Cols ("invalidated" ).Update (c )
1118- return err
1119- }
1120-
11211136// UpdateComment updates information of comment.
11221137func UpdateComment (c * Comment , doer * user_model.User ) error {
11231138 ctx , committer , err := db .TxContext ()
@@ -1176,6 +1191,120 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
11761191 return DeleteReaction (ctx , & ReactionOptions {CommentID : comment .ID })
11771192}
11781193
1194+ // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
1195+ type CodeComments map [string ]map [int64 ][]* Comment
1196+
1197+ // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
1198+ func FetchCodeComments (ctx context.Context , issue * Issue , currentUser * user_model.User ) (CodeComments , error ) {
1199+ return fetchCodeCommentsByReview (ctx , issue , currentUser , nil )
1200+ }
1201+
1202+ func fetchCodeCommentsByReview (ctx context.Context , issue * Issue , currentUser * user_model.User , review * Review ) (CodeComments , error ) {
1203+ pathToLineToComment := make (CodeComments )
1204+ if review == nil {
1205+ review = & Review {ID : 0 }
1206+ }
1207+ opts := FindCommentsOptions {
1208+ Type : CommentTypeCode ,
1209+ IssueID : issue .ID ,
1210+ ReviewID : review .ID ,
1211+ }
1212+
1213+ comments , err := findCodeComments (ctx , opts , issue , currentUser , review )
1214+ if err != nil {
1215+ return nil , err
1216+ }
1217+
1218+ for _ , comment := range comments {
1219+ if pathToLineToComment [comment .TreePath ] == nil {
1220+ pathToLineToComment [comment .TreePath ] = make (map [int64 ][]* Comment )
1221+ }
1222+ pathToLineToComment [comment.TreePath ][comment.Line ] = append (pathToLineToComment [comment.TreePath ][comment.Line ], comment )
1223+ }
1224+ return pathToLineToComment , nil
1225+ }
1226+
1227+ func findCodeComments (ctx context.Context , opts FindCommentsOptions , issue * Issue , currentUser * user_model.User , review * Review ) ([]* Comment , error ) {
1228+ var comments []* Comment
1229+ if review == nil {
1230+ review = & Review {ID : 0 }
1231+ }
1232+ conds := opts .toConds ()
1233+ if review .ID == 0 {
1234+ conds = conds .And (builder.Eq {"invalidated" : false })
1235+ }
1236+ e := db .GetEngine (ctx )
1237+ if err := e .Where (conds ).
1238+ Asc ("comment.created_unix" ).
1239+ Asc ("comment.id" ).
1240+ Find (& comments ); err != nil {
1241+ return nil , err
1242+ }
1243+
1244+ if err := issue .LoadRepo (ctx ); err != nil {
1245+ return nil , err
1246+ }
1247+
1248+ if err := CommentList (comments ).loadPosters (ctx ); err != nil {
1249+ return nil , err
1250+ }
1251+
1252+ // Find all reviews by ReviewID
1253+ reviews := make (map [int64 ]* Review )
1254+ ids := make ([]int64 , 0 , len (comments ))
1255+ for _ , comment := range comments {
1256+ if comment .ReviewID != 0 {
1257+ ids = append (ids , comment .ReviewID )
1258+ }
1259+ }
1260+ if err := e .In ("id" , ids ).Find (& reviews ); err != nil {
1261+ return nil , err
1262+ }
1263+
1264+ n := 0
1265+ for _ , comment := range comments {
1266+ if re , ok := reviews [comment .ReviewID ]; ok && re != nil {
1267+ // If the review is pending only the author can see the comments (except if the review is set)
1268+ if review .ID == 0 && re .Type == ReviewTypePending &&
1269+ (currentUser == nil || currentUser .ID != re .ReviewerID ) {
1270+ continue
1271+ }
1272+ comment .Review = re
1273+ }
1274+ comments [n ] = comment
1275+ n ++
1276+
1277+ if err := comment .LoadResolveDoer (); err != nil {
1278+ return nil , err
1279+ }
1280+
1281+ if err := comment .LoadReactions (issue .Repo ); err != nil {
1282+ return nil , err
1283+ }
1284+
1285+ var err error
1286+ if comment .RenderedContent , err = markdown .RenderString (& markup.RenderContext {
1287+ Ctx : ctx ,
1288+ URLPrefix : issue .Repo .Link (),
1289+ Metas : issue .Repo .ComposeMetas (),
1290+ }, comment .Content ); err != nil {
1291+ return nil , err
1292+ }
1293+ }
1294+ return comments [:n ], nil
1295+ }
1296+
1297+ // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
1298+ func FetchCodeCommentsByLine (ctx context.Context , issue * Issue , currentUser * user_model.User , treePath string , line int64 ) ([]* Comment , error ) {
1299+ opts := FindCommentsOptions {
1300+ Type : CommentTypeCode ,
1301+ IssueID : issue .ID ,
1302+ TreePath : treePath ,
1303+ Line : line ,
1304+ }
1305+ return findCodeComments (ctx , opts , issue , currentUser , nil )
1306+ }
1307+
11791308// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
11801309func UpdateCommentsMigrationsByType (tp structs.GitServiceType , originalAuthorID string , posterID int64 ) error {
11811310 _ , err := db .GetEngine (db .DefaultContext ).Table ("comment" ).
0 commit comments