1
1
import GitLabAPI from "./gitlab/GitLabAPI"
2
- import { Platform , Comment } from "./platform"
2
+ import { Comment , Platform } from "./platform"
3
3
import { GitDSL , GitJSONDSL } from "../dsl/GitDSL"
4
4
import { GitCommit } from "../dsl/Commit"
5
- import { GitLabDSL , GitLabJSONDSL , GitLabNote } from "../dsl/GitLabDSL"
6
-
5
+ import { GitLabDiscussion , GitLabDSL , GitLabJSONDSL , GitLabNote } from "../dsl/GitLabDSL"
7
6
import { debug } from "../debug"
8
7
import { dangerIDToString } from "../runner/templates/githubIssueTemplate"
8
+
9
9
const d = debug ( "GitLab" )
10
10
11
+ const useThreads = ( ) => process . env . DANGER_GITLAB_USE_THREADS === "1" ||
12
+ process . env . DANGER_GITLAB_USE_THREADS === "true"
13
+
11
14
class GitLab implements Platform {
12
15
public readonly name : string
13
16
@@ -78,7 +81,8 @@ class GitLab implements Platform {
78
81
return {
79
82
id : `${ note . id } ` ,
80
83
body : note . body ,
81
- // XXX: we should re-use the logic in getDangerNotes, need to check what inline comment template we're using if any
84
+ // XXX: we should re-use the logic in getDangerNotes, need to check what inline comment template we're using if
85
+ // any
82
86
ownedByDanger : note . author . id === dangerUserID && note . body . includes ( dangerID ) ,
83
87
}
84
88
} )
@@ -95,51 +99,54 @@ class GitLab implements Platform {
95
99
updateOrCreateComment = async ( dangerID : string , newComment : string ) : Promise < string > => {
96
100
d ( "updateOrCreateComment" , { dangerID, newComment } )
97
101
98
- const notes : GitLabNote [ ] = await this . getDangerNotes ( dangerID )
99
-
100
- let note : GitLabNote
101
-
102
- if ( notes . length ) {
103
- // update the first
104
- note = await this . api . updateMergeRequestNote ( notes [ 0 ] . id , newComment )
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
105
110
106
- // delete the rest
107
- for ( let deleteme of notes ) {
108
- if ( deleteme === notes [ 0 ] ) {
109
- continue
110
- }
111
+ let newOrUpdatedNote : GitLabNote
111
112
112
- await this . api . deleteMergeRequestNote ( deleteme . id )
113
- }
113
+ if ( existingNote ) {
114
+ // update the existing comment
115
+ newOrUpdatedNote = await this . api . updateMergeRequestNote ( existingNote . id , newComment )
114
116
} else {
115
- // create a new note
116
- note = await this . api . createMergeRequestNote ( newComment )
117
+ // create a new comment
118
+ newOrUpdatedNote = await this . createComment ( newComment )
117
119
}
118
120
119
121
// create URL from note
120
122
// "https://gitlab.com/group/project/merge_requests/154#note_132143425"
121
- return `${ this . api . mergeRequestURL } #note_${ note . id } `
123
+ return `${ this . api . mergeRequestURL } #note_${ newOrUpdatedNote . id } `
122
124
}
123
125
124
- createComment = async ( comment : string ) : Promise < any > => {
126
+ createComment = async ( comment : string ) : Promise < GitLabNote > => {
125
127
d ( "createComment" , { comment } )
128
+ if ( useThreads ( ) ) {
129
+ return ( await this . api . createMergeRequestDiscussion ( comment ) ) . notes [ 0 ]
130
+ }
126
131
return this . api . createMergeRequestNote ( comment )
127
132
}
128
133
129
- createInlineComment = async ( git : GitDSL , comment : string , path : string , line : number ) : Promise < string > => {
134
+ createInlineComment = async ( git : GitDSL , comment : string , path : string , line : number ) : Promise < GitLabDiscussion > => {
130
135
d ( "createInlineComment" , { git, comment, path, line } )
131
136
132
137
const mr = await this . api . getMergeRequestInfo ( )
133
138
134
139
return this . api . createMergeRequestDiscussion ( comment , {
135
- position_type : "text" ,
136
- base_sha : mr . diff_refs . base_sha ,
137
- start_sha : mr . diff_refs . start_sha ,
138
- head_sha : mr . diff_refs . head_sha ,
139
- old_path : path ,
140
- old_line : null ,
141
- new_path : path ,
142
- new_line : line ,
140
+ position : {
141
+ position_type : "text" ,
142
+ base_sha : mr . diff_refs . base_sha ,
143
+ start_sha : mr . diff_refs . start_sha ,
144
+ head_sha : mr . diff_refs . head_sha ,
145
+ old_path : path ,
146
+ old_line : null ,
147
+ new_path : path ,
148
+ new_line : line ,
149
+ } ,
143
150
} )
144
151
}
145
152
@@ -156,26 +163,54 @@ class GitLab implements Platform {
156
163
}
157
164
158
165
deleteMainComment = async ( dangerID : string ) : Promise < boolean > => {
159
- const notes = await this . getDangerNotes ( dangerID )
160
- for ( let note of notes ) {
161
- d ( "deleteMainComment" , { id : note . id } )
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 ) )
171
+ }
172
+
173
+ deleteNotes = async ( notes : GitLabNote [ ] ) : Promise < boolean > => {
174
+ for ( const note of notes ) {
175
+ d ( "deleteNotes" , { id : note . id } )
162
176
await this . api . deleteMergeRequestNote ( note . id )
163
177
}
164
178
165
179
return notes . length > 0
166
180
}
167
181
182
+ /**
183
+ * Only fetches the discussions where danger owns the top note
184
+ */
185
+ getDangerDiscussions = async ( dangerID : string ) : Promise < GitLabDiscussion [ ] > => {
186
+ const noteFilter = await this . getDangerNoteFilter ( dangerID )
187
+ const discussions : GitLabDiscussion [ ] = await this . api . getMergeRequestDiscussions ( )
188
+ return discussions . filter ( ( { notes } ) => notes . length && noteFilter ( notes [ 0 ] ) )
189
+ }
190
+
191
+ reduceNotesFromDiscussions = ( discussions : GitLabDiscussion [ ] ) : GitLabNote [ ] => {
192
+ return discussions . reduce < GitLabNote [ ] > ( ( acc , { notes } ) => [ ...acc , ...notes ] , [ ] )
193
+ }
194
+
168
195
getDangerNotes = async ( dangerID : string ) : Promise < GitLabNote [ ] > => {
169
- const { id : dangerUserId } = await this . api . getUser ( )
196
+ const noteFilter = await this . getDangerNoteFilter ( dangerID )
170
197
const notes : GitLabNote [ ] = await this . api . getMergeRequestNotes ( )
198
+ return notes . filter ( noteFilter )
199
+ }
171
200
172
- return notes . filter (
173
- ( { author : { id } , body, system, type } : GitLabNote ) =>
174
- ! system && // system notes are generated when the user interacts with the UI e.g. changing a PR title
175
- type == null && // we only want "normal" comments on the main body of the MR;
201
+ getDangerNoteFilter = async (
202
+ dangerID : string ,
203
+ ) : Promise < ( note : GitLabNote ) => boolean > => {
204
+ const { id : dangerUserId } = await this . api . getUser ( )
205
+ return ( { author : { id } , body, system } : GitLabNote ) : boolean => {
206
+ return ! system && // system notes are generated when the user interacts with the UI e.g. changing a PR title
176
207
id === dangerUserId &&
177
- body ! . includes ( dangerIDToString ( dangerID ) ) // danger-id-(dangerID) is included in a hidden comment in the githubIssueTemplate
178
- )
208
+ //we do not check the `type` - it's `null` most of the time,
209
+ // only in discussions/threads this is `DiscussionNote` for all notes. But even if danger only creates a
210
+ // normal `null`-comment, any user replying to that comment will turn it into a `DiscussionNote`-typed one.
211
+ // So we cannot assume anything here about danger's note type.
212
+ body . includes ( dangerIDToString ( dangerID ) )
213
+ }
179
214
}
180
215
181
216
updateStatus = async ( ) : Promise < boolean > => {
0 commit comments