@@ -9,7 +9,9 @@ package issues
9
9
import (
10
10
"context"
11
11
"fmt"
12
+ "regexp"
12
13
"strconv"
14
+ "strings"
13
15
"unicode/utf8"
14
16
15
17
"code.gitea.io/gitea/models/db"
@@ -21,6 +23,8 @@ import (
21
23
"code.gitea.io/gitea/modules/git"
22
24
"code.gitea.io/gitea/modules/json"
23
25
"code.gitea.io/gitea/modules/log"
26
+ "code.gitea.io/gitea/modules/markup"
27
+ "code.gitea.io/gitea/modules/markup/markdown"
24
28
"code.gitea.io/gitea/modules/references"
25
29
"code.gitea.io/gitea/modules/structs"
26
30
"code.gitea.io/gitea/modules/timeutil"
@@ -693,6 +697,31 @@ func (c *Comment) LoadReview() error {
693
697
return c .loadReview (db .DefaultContext )
694
698
}
695
699
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
+
696
725
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
697
726
func (c * Comment ) DiffSide () string {
698
727
if c .Line < 0 {
@@ -1036,28 +1065,23 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
1036
1065
// FindCommentsOptions describes the conditions to Find comments
1037
1066
type FindCommentsOptions struct {
1038
1067
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 {
1053
1079
cond := builder .NewCond ()
1054
1080
if opts .RepoID > 0 {
1055
1081
cond = cond .And (builder.Eq {"issue.repo_id" : opts .RepoID })
1056
1082
}
1057
1083
if opts .IssueID > 0 {
1058
1084
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 ))
1061
1085
}
1062
1086
if opts .ReviewID > 0 {
1063
1087
cond = cond .And (builder.Eq {"comment.review_id" : opts .ReviewID })
@@ -1077,16 +1101,13 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
1077
1101
if len (opts .TreePath ) > 0 {
1078
1102
cond = cond .And (builder.Eq {"comment.tree_path" : opts .TreePath })
1079
1103
}
1080
- if ! opts .Invalidated .IsNone () {
1081
- cond = cond .And (builder.Eq {"comment.invalidated" : opts .Invalidated .IsTrue ()})
1082
- }
1083
1104
return cond
1084
1105
}
1085
1106
1086
1107
// FindComments returns all comments according options
1087
1108
func FindComments (ctx context.Context , opts * FindCommentsOptions ) ([]* Comment , error ) {
1088
1109
comments := make ([]* Comment , 0 , 10 )
1089
- sess := db .GetEngine (ctx ).Where (opts .ToConds ())
1110
+ sess := db .GetEngine (ctx ).Where (opts .toConds ())
1090
1111
if opts .RepoID > 0 {
1091
1112
sess .Join ("INNER" , "issue" , "issue.id = comment.issue_id" )
1092
1113
}
@@ -1105,19 +1126,13 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, e
1105
1126
1106
1127
// CountComments count all comments according options by ignoring pagination
1107
1128
func CountComments (opts * FindCommentsOptions ) (int64 , error ) {
1108
- sess := db .GetEngine (db .DefaultContext ).Where (opts .ToConds ())
1129
+ sess := db .GetEngine (db .DefaultContext ).Where (opts .toConds ())
1109
1130
if opts .RepoID > 0 {
1110
1131
sess .Join ("INNER" , "issue" , "issue.id = comment.issue_id" )
1111
1132
}
1112
1133
return sess .Count (& Comment {})
1113
1134
}
1114
1135
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
-
1121
1136
// UpdateComment updates information of comment.
1122
1137
func UpdateComment (c * Comment , doer * user_model.User ) error {
1123
1138
ctx , committer , err := db .TxContext ()
@@ -1176,6 +1191,120 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
1176
1191
return DeleteReaction (ctx , & ReactionOptions {CommentID : comment .ID })
1177
1192
}
1178
1193
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
+
1179
1308
// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
1180
1309
func UpdateCommentsMigrationsByType (tp structs.GitServiceType , originalAuthorID string , posterID int64 ) error {
1181
1310
_ , err := db .GetEngine (db .DefaultContext ).Table ("comment" ).
0 commit comments