@@ -8,6 +8,9 @@ import { dangerIDToString } from "../runner/templates/githubIssueTemplate"
8
8
9
9
const d = debug ( "GitLab" )
10
10
11
+ /**
12
+ * Determines whether Danger should use threads for the "main" Danger comment.
13
+ */
11
14
const useThreads = ( ) =>
12
15
process . env . DANGER_GITLAB_USE_THREADS === "1" || process . env . DANGER_GITLAB_USE_THREADS === "true"
13
16
@@ -81,8 +84,6 @@ class GitLab implements Platform {
81
84
return {
82
85
id : `${ note . id } ` ,
83
86
body : note . body ,
84
- // XXX: we should re-use the logic in getDangerNotes, need to check what inline comment template we're using if
85
- // any
86
87
ownedByDanger : note . author . id === dangerUserID && note . body . includes ( dangerID ) ,
87
88
}
88
89
} )
@@ -99,28 +100,49 @@ class GitLab implements Platform {
99
100
updateOrCreateComment = async ( dangerID : string , newComment : string ) : Promise < string > => {
100
101
d ( "updateOrCreateComment" , { dangerID, newComment } )
101
102
102
- //Even when we don't set danger to create threads, we still need to delete them if someone answered to a single
103
- // comment created by danger, resulting in a discussion/thread. Otherwise we are left with dangling comments
104
- // that will most likely have no meaning out of context.
105
- const discussions = await this . getDangerDiscussions ( dangerID )
106
- const firstDiscussion = discussions . shift ( )
107
- const existingNote = firstDiscussion ?. notes [ 0 ]
108
- // Delete all notes from all other danger discussions (discussions cannot be deleted as a whole):
109
- await this . deleteNotes ( this . reduceNotesFromDiscussions ( discussions ) ) //delete the rest
110
-
111
- let newOrUpdatedNote : GitLabNote
103
+ if ( useThreads ( ) ) {
104
+ const discussions = await this . getDangerDiscussions ( dangerID )
105
+ const firstDiscussion = discussions . shift ( )
106
+ const existingNote = firstDiscussion ?. notes [ 0 ]
107
+ // Delete all notes from all other danger discussions (discussions cannot be deleted as a whole):
108
+ await this . deleteNotes ( this . reduceNotesFromDiscussions ( discussions ) ) //delete the rest
109
+
110
+ let newOrUpdatedNote : GitLabNote
111
+
112
+ if ( existingNote ) {
113
+ // update the existing comment
114
+ newOrUpdatedNote = await this . api . updateMergeRequestNote ( existingNote . id , newComment )
115
+ } else {
116
+ // create a new comment
117
+ newOrUpdatedNote = await this . createComment ( newComment )
118
+ }
112
119
113
- if ( existingNote ) {
114
- // update the existing comment
115
- newOrUpdatedNote = await this . api . updateMergeRequestNote ( existingNote . id , newComment )
120
+ // create URL from note
121
+ // "https://gitlab.com/group/project/merge_requests/154#note_132143425"
122
+ return ` ${ this . api . mergeRequestURL } #note_ ${ newOrUpdatedNote . id } `
116
123
} else {
117
- // create a new comment
118
- newOrUpdatedNote = await this . createComment ( newComment )
124
+ const notes : GitLabNote [ ] = await this . getMainDangerNotes ( dangerID )
125
+
126
+ let note : GitLabNote
127
+
128
+ if ( notes . length ) {
129
+ // update the first
130
+ note = await this . api . updateMergeRequestNote ( notes [ 0 ] . id , newComment )
131
+ // delete the rest
132
+ for ( let deleteme of notes ) {
133
+ if ( deleteme === notes [ 0 ] ) {
134
+ continue
135
+ }
136
+ await this . api . deleteMergeRequestNote ( deleteme . id )
137
+ }
138
+ } else {
139
+ // create a new note
140
+ note = await this . api . createMergeRequestNote ( newComment )
141
+ }
142
+ // create URL from note
143
+ // "https://gitlab.com/group/project/merge_requests/154#note_132143425"
144
+ return `${ this . api . mergeRequestURL } #note_${ note . id } `
119
145
}
120
-
121
- // create URL from note
122
- // "https://gitlab.com/group/project/merge_requests/154#note_132143425"
123
- return `${ this . api . mergeRequestURL } #note_${ newOrUpdatedNote . id } `
124
146
}
125
147
126
148
createComment = async ( comment : string ) : Promise < GitLabNote > => {
@@ -162,12 +184,24 @@ class GitLab implements Platform {
162
184
return await this . api . deleteMergeRequestNote ( nid )
163
185
}
164
186
187
+ /**
188
+ * Attempts to delete the "main" Danger comment. If the "main" Danger
189
+ * comment has any comments on it then that comment will not be deleted.
190
+ */
165
191
deleteMainComment = async ( dangerID : string ) : Promise < boolean > => {
166
- //We fetch the discussions even if we are not set to use threads because users could still have replied to a
167
- // comment by danger and thus created a discussion/thread. To not leave dangling notes, we delete the entire thread.
168
- //There is no API to delete entire discussion. They can only be deleted fully by deleting every note:
169
- const discussions = await this . getDangerDiscussions ( dangerID )
170
- return await this . deleteNotes ( this . reduceNotesFromDiscussions ( discussions ) )
192
+ if ( useThreads ( ) ) {
193
+ // There is no API to delete entire discussion. They can only be deleted fully by deleting every note:
194
+ const discussions = await this . getDangerDiscussions ( dangerID )
195
+ return await this . deleteNotes ( this . reduceNotesFromDiscussions ( discussions ) )
196
+ } else {
197
+ const notes = await this . getMainDangerNotes ( dangerID )
198
+ for ( let note of notes ) {
199
+ d ( "deleteMainComment" , { id : note . id } )
200
+ await this . api . deleteMergeRequestNote ( note . id )
201
+ }
202
+
203
+ return notes . length > 0
204
+ }
171
205
}
172
206
173
207
deleteNotes = async ( notes : GitLabNote [ ] ) : Promise < boolean > => {
@@ -183,7 +217,7 @@ class GitLab implements Platform {
183
217
* Only fetches the discussions where danger owns the top note
184
218
*/
185
219
getDangerDiscussions = async ( dangerID : string ) : Promise < GitLabDiscussion [ ] > => {
186
- const noteFilter = await this . getDangerNoteFilter ( dangerID )
220
+ const noteFilter = await this . getDangerDiscussionNoteFilter ( dangerID )
187
221
const discussions : GitLabDiscussion [ ] = await this . api . getMergeRequestDiscussions ( )
188
222
return discussions . filter ( ( { notes } ) => notes . length && noteFilter ( notes [ 0 ] ) )
189
223
}
@@ -192,22 +226,49 @@ class GitLab implements Platform {
192
226
return discussions . reduce < GitLabNote [ ] > ( ( acc , { notes } ) => [ ...acc , ...notes ] , [ ] )
193
227
}
194
228
195
- getDangerNotes = async ( dangerID : string ) : Promise < GitLabNote [ ] > => {
196
- const noteFilter = await this . getDangerNoteFilter ( dangerID )
229
+ /**
230
+ * Attempts to find the "main" Danger note and should return at most
231
+ * one item. If the "main" Danger note has any comments on it then that
232
+ * note will not be returned.
233
+ */
234
+ getMainDangerNotes = async ( dangerID : string ) : Promise < GitLabNote [ ] > => {
235
+ const noteFilter = await this . getDangerMainNoteFilter ( dangerID )
197
236
const notes : GitLabNote [ ] = await this . api . getMergeRequestNotes ( )
198
237
return notes . filter ( noteFilter )
199
238
}
200
239
201
- getDangerNoteFilter = async ( dangerID : string ) : Promise < ( note : GitLabNote ) => boolean > => {
240
+ /**
241
+ * Filters a note to determine if it was created by Danger.
242
+ */
243
+ getDangerDiscussionNoteFilter = async ( dangerID : string ) : Promise < ( note : GitLabNote ) => boolean > => {
202
244
const { id : dangerUserId } = await this . api . getUser ( )
203
245
return ( { author : { id } , body, system } : GitLabNote ) : boolean => {
204
246
return (
205
247
! system && // system notes are generated when the user interacts with the UI e.g. changing a PR title
206
248
id === dangerUserId &&
207
- //we do not check the `type` - it's `null` most of the time,
208
- // only in discussions/threads this is `DiscussionNote` for all notes. But even if danger only creates a
209
- // normal `null`-comment, any user replying to that comment will turn it into a `DiscussionNote`-typed one.
210
- // So we cannot assume anything here about danger's note type.
249
+ body . includes ( dangerIDToString ( dangerID ) )
250
+ )
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Filters a note to the "main" Danger note. If that note has any
256
+ * comments on it then it will not be found.
257
+ */
258
+ getDangerMainNoteFilter = async ( dangerID : string ) : Promise < ( note : GitLabNote ) => boolean > => {
259
+ const { id : dangerUserId } = await this . api . getUser ( )
260
+ return ( { author : { id } , body, system, type } : GitLabNote ) : boolean => {
261
+ return (
262
+ ! system && // system notes are generated when the user interacts with the UI e.g. changing a PR title
263
+ id === dangerUserId &&
264
+ // This check for the type being `null` seems to be the best option
265
+ // we have to determine whether this note is the "main" Danger note.
266
+ // This assumption does not hold if there are any comments on the
267
+ // "main" Danger note and in that case a new "main" Danger note will
268
+ // be created instead of updating the existing note. This behavior is better
269
+ // than the current alternative which is the bug described here:
270
+ // https://github.com/danger/danger-js/issues/1351
271
+ type == null &&
211
272
body . includes ( dangerIDToString ( dangerID ) )
212
273
)
213
274
}
0 commit comments