diff --git a/src/app/merbocop.ml b/src/app/merbocop.ml index 62a6f9e49e4f08ad0485dbe003598260fdf79ca5..2d0ed880c9d027eaadc12aa025913be25da990c4 100644 --- a/src/app/merbocop.ml +++ b/src/app/merbocop.ml @@ -119,7 +119,7 @@ let limit_to_recent_updates_term () = (** [label_mr] applies (or removes) a Gitlab, label of [doc_label] and/or [approved_label] for all merger request in [mr_list]. *) let label_mr (self_id : int option) (token : string option) - (retry_options : Web_api.Retry_options.t) + (retry_options : Web_api.Retry_options.t) (reporting_only : bool) (mr_list : (string * Merge_request.mr_result) list) (errors : string list ref) (approving : bool) (doc_label : string option) (approved_label : string option) = @@ -157,9 +157,9 @@ let label_mr (self_id : int option) (token : string option) | _ -> (add, remove @ to_list approved_label) ) ) in match ( Bot_actions.label_mr ?token ~project ~mr ~retry_options - (fst add_remove_lists) + ~reporting_only (fst add_remove_lists) , Bot_actions.unlabel_mr ?token ~project ~mr ~retry_options - (snd add_remove_lists) ) + ~reporting_only (snd add_remove_lists) ) with | Ok (Some (_ : Ezjsonm.value)), Ok (Some (_ : Ezjsonm.value)) |Ok (Some (_ : Ezjsonm.value)), Ok None @@ -198,13 +198,13 @@ let set_approved_label () = (** [approve_mr] approves (or unapproves) each merger request in [mr_list] based on its [`Doc_only | `Not_doc_only] status. *) let approve_mr (self_id : int option) (token : string option) - (retry_options : Web_api.Retry_options.t) + (retry_options : Web_api.Retry_options.t) (reporting_only : bool) (mr_list : (string * Merge_request.mr_result) list) (errors : string list ref) (approve_doc_only_mr : bool) = List.iter mr_list ~f:(function | _, Error (`Mr_not_found _) -> () | project, Ok mr -> - let reporting_only = if approve_doc_only_mr then false else true in + let reporting_only = reporting_only || not approve_doc_only_mr in List.iter (Analysis.labels_of_mr mr) ~f:(function | `Doc_only -> ( match @@ -259,12 +259,12 @@ let post_comments (self_id : int option) (token : string option) (mr_list : (string * Merge_request.mr_result) list) (retry_options : Web_api.Retry_options.t) (errors : string list ref) (comment_on_self : bool) (warnings : Warnings.t) (do_edit_existing : bool) - (require_resolve_thread : bool) = + (require_resolve_thread : bool) (reporting_only : bool) = if comment_on_self then List.iter mr_list ~f:(function | _, Error (`Mr_not_found _) -> (* Already counted *) () | project, Ok mr -> ( - let body_of_prev _previous_body = + let body = try Comment_body.simple_message mr |> String.concat ~sep:"\n" with e -> Fmt.str "ERROR: %a (%d MRs)" Exn.pp e (List.length mr_list) in @@ -280,7 +280,7 @@ let post_comments (self_id : int option) (token : string option) ~user_id: (Option.value_exn ~message:"Commenting requires the --self-id option." self_id ) - ~retry_options ~warnings body_of_prev + ~retry_options ~warnings ~reporting_only body with | Ok (`Added res) | Ok (`Edited res) -> ( say "Ok, done %s/%a %s." project Merge_request.pp_quick (Ok mr) @@ -294,6 +294,11 @@ let post_comments (self_id : int option) (token : string option) [ Fmt.str "Add-or-edit-comment: %s/%a: %s" project Merge_request.pp_quick (Ok mr) msg ] | _ -> [] ) + | Ok (`No_change json) -> + say "Nothing new to say: %s/%a %s...}" project + Merge_request.pp_quick (Ok mr) + ( Jq.value_to_string ~minify:true json + |> fun s -> String.prefix s 41 ) | Error (`Mr_not_found (proj, s, js, e)) -> say "Cannot comment, no MR there: %s/%s\n%s\n%a" proj s (Ezjsonm.value_to_string ~minify:false js) @@ -322,7 +327,7 @@ let no_comment_editing () = [mr_list] to [write_body]. *) let full_report_body (mr_list : (string * Merge_request.mr_result) list) (errors : string list ref) (write_body : string option) = - let body_of_prev _previous_body = + let body = try Comment_body.full_report () ~mr_list:(List.map ~f:snd mr_list) with e -> errors := !errors @ [Fmt.str "Making multi-mr body: %a" Exn.pp e] ; @@ -332,7 +337,7 @@ let full_report_body (mr_list : (string * Merge_request.mr_result) list) | Some p -> p | None -> Caml.Filename.temp_file "body" ".md" in let o = Caml.open_out tmp_md in - Caml.output_string o (body_of_prev None) ; + Caml.output_string o body ; Caml.close_out o ; say "Body: file://%s\n" tmp_md @@ -456,6 +461,14 @@ let wip_status () = const (fun w -> `Wip_status w) $ value (opt (enum choices) `Any (info ["wip-status-filter"] ~doc))) +let reporting_only () = + Arg.( + const (fun s -> `Reporting_only s) + $ value + (flag + (info ["dry-run"] + ~doc:"Generate then full report without executing api calls." ) )) + let () = let multi_inspect_cmd = ( const @@ -480,6 +493,7 @@ let () = (`Post_warnings post_warnings_to) (`Assign_reviewers assign_reviewers) (`Require_resolve_thread resolve_required) + (`Reporting_only report_only) -> Option.iter opt_load_path ~f:(fun path -> Web_api.load_cache ~path) ; Caml.at_exit (fun () -> @@ -513,11 +527,12 @@ let () = Some (Fmt.str "MR-not-found: %s/%s: %a" proj ci Exn.pp e) ) ; if comment_on_self then post_comments self token mr_list retry errors comment_on_self - job_warnings do_edit_existing resolve_required ; - label_mr self token retry mr_list errors approve_doc_only doc_label - approved_label ; - approve_mr self token retry mr_list errors approve_doc_only ; - Reviewers.exe token retry mr_list errors assign_reviewers ; + job_warnings do_edit_existing resolve_required report_only ; + label_mr self token retry report_only mr_list errors approve_doc_only + doc_label approved_label ; + approve_mr self token retry report_only mr_list errors + approve_doc_only ; + Reviewers.exe token retry report_only mr_list errors assign_reviewers ; full_report_body mr_list errors write_body ; post_warnings post_warnings_to token retry errors job_warnings ) ; match !errors with @@ -536,6 +551,6 @@ let () = $ no_comment_editing () $ write_body () $ load_cache () $ dump_cache () $ set_doc_label () $ set_approved_label () $ approve_doc_only_mr () $ retry_options () $ no_retry () $ post_warnings_to () - $ Reviewers.cli_term () $ Thread.cli_term () + $ Reviewers.cli_term () $ Thread.cli_term () $ reporting_only () , info "full-inspection" ~doc:"Do full inspection on a bunch of MRs" ) in exit (eval_choice (help : unit Term.t * _) [multi_inspect_cmd]) diff --git a/src/lib/Md.ml b/src/lib/Md.ml new file mode 100644 index 0000000000000000000000000000000000000000..9545eef9126da0d74cdd0b8589ca87bf6c266d01 --- /dev/null +++ b/src/lib/Md.ml @@ -0,0 +1,49 @@ +open! Base +open Fmt + +let empty = "" +let h n s = [str "%s %s" (String.make n '#') s; empty] +let par l = l @ [empty] +let verbatim l = "```" :: l @ ["```"] +let plural = function 1 -> "" | _ -> "s" +let plural_list = function [_] -> "" | _ -> "s" +let plural_be = function 1 -> "is" | _ -> "are" +let third_person = function 1 -> "s" | _ -> "" +let hl = [""; String.make 10 '-'; ""] +let colon_or_dot = function 0 -> "." | _ -> ":" + +let merge_team_doc_link = + "http://tezos.gitlab.io/developer/contributing.html#the-merge-request-bot" + +let todo_comment_doc_link = + "https://tezos.gitlab.io/developer/guidelines.html#todo-fixme-comments" + +let code_escape s = + let max_consecutive_backquotes = + String.fold s ~init:(0, 0) ~f:(fun (prev_c, prev_m) -> function + | '`' -> (prev_c + 1, prev_m) | _ -> (0, max prev_m prev_c) ) + |> snd in + let escaper = String.make (max_consecutive_backquotes + 1) '`' in + String.concat ~sep:"" + [ escaper; (match s.[0] with '`' -> " " | _ -> "" | exception _ -> " "); s + ; ( match s.[String.length s - 1] with + | '`' -> " " + | _ -> "" + | exception _ -> " " ); escaper ] + +let one_couple_few nb = + match nb with + | 0 -> "no" + | 1 -> "one" + | 2 | 3 | 4 -> "a couple of" + | _ -> "a few" + +(** [horizontal n] is a string representing a markdown horizontal line. n must + be greater than 2. Merbocop uses this for parsing its own comments. Please + don't confuse Merbocop by using the same number [n] in multiple places. See + Delimiter module below. *) +let horizontal int = + let rec aux i s = match i with 0 -> "" ^ s | i -> aux (i - 1) ("-" ^ s) in + aux int "" + +module Delimiters = struct let comment_footer = horizontal 6 end diff --git a/src/lib/bot_actions.ml b/src/lib/bot_actions.ml index 1601db5b557c763d0d039a43e650c78a1c7e93e4..9f176658a52c71c772a1b1d613a7abf9c54b3b2a 100644 --- a/src/lib/bot_actions.ml +++ b/src/lib/bot_actions.ml @@ -1,50 +1,41 @@ open! Base (** [post_or_edit_mr_comment] will post a Gitlab note (comment) [body_of_prev] - on merge request [mr_result] or give error "merge request not found." - [do_edit_existing] will attempt to edit Merbocop's preexisting note before - posting a new one. *) + on merge request [mr_result]. [do_edit_existing] will attempt to edit + previous comments by matching [user_id] preexisting note before posting a + new one. *) let post_or_edit_mr_comment ?(do_edit_existing = true) ?(token : string option) ~(mr_result : Merge_request.mr_result) ~(user_id : int) ~(retry_options : Web_api.Retry_options.t) ~(warnings : Warnings.t) - body_of_prev = + ~(reporting_only : bool) body = let open Merge_request in let my_comment = - match mr_result with - | Ok {comments= _; _} when not do_edit_existing -> None - | Ok {comments; _} -> - List.find_map - (Option.value_exn - ~message: - "Please provide the --access-token option to post comments." - comments ) ~f:(function - | {Comment.system= true; _} | {Comment.resolvable= true; _} -> None - | {Comment.json; _} -> - let open Jq in - let obj = get_dict json in - if - get_field "author" obj |> get_dict |> get_field "id" |> get_int - = user_id - then - Some - ( get_field "id" obj |> get_int - , get_field "body" obj |> get_string ) - else None ) - | Error _ -> None in + if not do_edit_existing then None + else + (* If do edit existing, look for merboco's previous comment. *) + match mr_result with + | Ok mr -> ( + match mr.comments with + | Some comments -> Comment.of_user ~comments ~user_id + | None -> None ) + | Error _ -> None in let note_id result = let open Jq in try get_dict result |> get_field "id" |> get_int with e -> - Debug.dbg "Merge_request.post_or_edit_mr_comment \n\n\n%s, %s\n\n" + Debug.dbg "Bot_actions.post_or_edit_mr_comment \n\n\n%s, %s\n\n" (Exn.to_string_mach e) (Ezjsonm.value_to_string result) ; raise e in + let report_json id = Jq.(dict [("id", int id); ("reporting", `String "OK")]) in match (my_comment, mr_result) with | None, Ok {id; project; reports; _} -> + (* If no previous comment, post new one. *) let add_result = - let body = body_of_prev None in - Comment.add_merge_request_note ?token ~project:project.id ~mr:id - ~retry_options body in + if reporting_only then report_json 000000 + else + Comment.add_merge_request_note ?token ~project:project.id ~mr:id + ~retry_options body in let new_id = note_id add_result in Reporting.add_conditional reports (Fmt.str @@ -52,72 +43,80 @@ let post_or_edit_mr_comment ?(do_edit_existing = true) ?(token : string option) note_id:%d" id new_id ) ; Ok (`Added add_result) - | Some (i, prev_body), Ok mr -> ( - let body = body_of_prev (Some prev_body) in - let edit_result = - Comment.edit_merge_request_note ?token ~project:mr.project.id ~mr:mr.id - ~retry_options ~note:i body in - match edit_result with - | `Float 500. -> - let address = - Comment.add_merge_request_note ?token ~project:mr.project.id - ~mr:mr.id ~retry_options body in - let new_id = note_id address in - Warnings.add ~warnings ~mr - ~s: - (Fmt.str - "Failed to edit comment with response code 500. A new comment \ - was posted mr:%d note_id:%d" - mr.id new_id ) - () ; - Ok (`Added address) - | _ -> - let new_id = note_id edit_result in - if Int.equal i new_id then - Reporting.add_conditional mr.reports - (Fmt.str "A previous comment was edited. mr:%d note-id:%d" mr.id i) + | Some comment, Ok mr -> ( + (* Check if comment body is a new comment *) + match Comment.is_new ~body ~comment with + | false -> + Reporting.add_conditional mr.reports + (Fmt.str + "A previous comment was found but wasn't edited because there was \ + nothing new to say mr:%d note_id:%d" + mr.id comment.id ) ; + Ok (`No_change comment.json) + | true -> ( + let report_result = report_json comment.id in + let edit_result = + if reporting_only then report_result else + Comment.edit_merge_request_note ?token ~project:mr.project.id + ~mr:mr.id ~retry_options ~note:comment.id body in + match edit_result with + | `Float 500. -> + let add_result = + if reporting_only then report_result + else + Comment.add_merge_request_note ?token ~project:mr.project.id + ~mr:mr.id ~retry_options body in + let new_id = note_id add_result in + Reporting.add_conditional mr.reports + (Fmt.str "Failed to edit comment with response code 500.") ; Warnings.add ~warnings ~mr ~s: + (Fmt.str + "Failed to edit comment with response code 500. A new \ + comment was posted mr:%d note_id:%d" + mr.id new_id ) + () ; + Ok (`Added add_result) + | _ -> + let new_id = note_id edit_result in + if Int.equal comment.id new_id then + Reporting.add_conditional mr.reports + (Fmt.str "A previous comment was edited. mr:%d note-id:%d" mr.id + comment.id ) + else + Reporting.add_conditional mr.reports (Fmt.str "Attempted to edit a comment and might have posted a new \ one. There is a new note id. mr:%d note_id:%d new_id:%d" - mr.id i new_id ) + mr.id comment.id new_id ) ; + Warnings.add ~warnings ~mr + ~s: + (Fmt.str + "Attempted to edit a comment and might have posted a new \ + one. There is a new note id. mr:%d note_id:%d new_id:%d" + mr.id comment.id new_id ) () ; - Ok (`Edited edit_result) ) + Ok (`Edited edit_result) ) ) | _, Error e -> Error e -(** [post_or_edit_mr_thread] will post a Gitlab discussion (thread) - [body_of_prev] on merge request [mr_result]. If [do_edit_existing] is true, - Merbocop will attempt to edit preexisting thread note before posting a new - one. *) +(** [post_or_edit_mr_thread] will post a Gitlab discussion (thread) [body] on + merge request [mr_result]. If [do_edit_existing] is true, Merbocop will + attempt to edit preexisting thread note before posting a new one. *) let post_or_edit_mr_thread ?(do_edit_existing = true) ?(token : string option) ~(mr_result : Merge_request.mr_result) ~(user_id : int) ~(retry_options : Web_api.Retry_options.t) ~(warnings : Warnings.t) - body_of_prev = + ~(reporting_only : bool) body = let open Merge_request in - let match_comment (c : Comment.t) = - match c with - | {system= true; _} | {resolvable= false; _} -> None - | comment -> - if comment.author.id = user_id then Some (comment.id, comment.body) - else None in - let my_thread : (string * (int * string)) option = - (* TODO need to get the thread id in there some how*) - match mr_result with - | Ok _ when not do_edit_existing -> None - | Ok mr -> - List.find_map - (Option.value_exn - ~message: - "Please provide the --access-token option to edit discussion \ - threads." - mr.threads ) ~f:(fun thread -> - let mine = List.find_map thread.comments ~f:match_comment in - match mine with - | Some comment -> Some (thread.id, comment) - | None -> None ) - | Error _ -> None in + let my_thread : (Thread.t * Comment.t) option = + if not do_edit_existing then None + else + match mr_result with + | Ok mr -> ( + match mr.threads with + | Some threads -> Thread.of_user ~threads ~user_id + | None -> None ) + | Error _ -> None in let thread_id result = let open Jq in try get_dict result |> get_field "id" |> get_string @@ -127,21 +126,15 @@ let post_or_edit_mr_thread ?(do_edit_existing = true) ?(token : string option) (Exn.to_string_mach e) (Ezjsonm.value_to_string result) ; raise e in - let note_id result = - let open Jq in - try get_dict result |> get_field "id" |> get_int - with e -> - Debug.dbg - "Merge_request.post_or_edit_mr_thread note_id error \n\n\n%s, %s\n\n" - (Exn.to_string_mach e) - (Ezjsonm.value_to_string result) ; - raise e in + let report_thread id = + Jq.(dict [("id", string id); ("reporting", `String "OK")]) in match (my_thread, mr_result) with | None, Ok mr -> let add_result = - let body = body_of_prev None in - Thread.start_merge_request_discussion ?token ~project:mr.project.id - ~mr:mr.id ~retry_options body in + if reporting_only then report_thread "dry-run-no-thread-id" + else + Thread.start_merge_request_discussion ?token ~project:mr.project.id + ~mr:mr.id ~retry_options body in let new_id = thread_id add_result in Reporting.add_conditional mr.reports (Fmt.str @@ -149,46 +142,84 @@ let post_or_edit_mr_thread ?(do_edit_existing = true) ?(token : string option) thread-id:%s" mr.id new_id ) ; Ok (`Added add_result) - | Some (id, (note, prev_body)), Ok mr -> ( - let body = body_of_prev (Some prev_body) in - let edit_result = - Thread.edit_merge_request_discussion ?token ~project:mr.project.id - ~mr:mr.id ~retry_options ~discussion:id ~note body in - match edit_result with - | `Float 500. -> - let add = - Thread.start_merge_request_discussion ?token ~project:mr.project.id - ~mr:mr.id ~retry_options body in - let new_id = thread_id add in - Warnings.add ~warnings ~mr - ~s: + | Some (thread, comment), Ok mr -> ( + match Comment.is_new ~body ~comment with + | false -> + Reporting.add_conditional mr.reports + (Fmt.str + "A previous thread was found but wasn't edited because there was \ + nothing new to say mr:%d thread_id:%s note_id:%d" + mr.id thread.id comment.id ) ; + Ok (`No_change comment.json) + | true -> ( + let report_result = report_thread "dry-run-no-thread-id" in + let edit_result = + if reporting_only then report_result + else + Thread.edit_merge_request_discussion ?token ~project:mr.project.id + ~mr:mr.id ~retry_options ~discussion:thread.id ~note:comment.id + body in + match edit_result with + | `Float 500. -> + let add_result = + if reporting_only then report_result + else + Thread.start_merge_request_discussion ?token + ~project:mr.project.id ~mr:mr.id ~retry_options body in + let new_id = thread_id add_result in + Reporting.add_conditional mr.reports (Fmt.str "Failed to edit a previous threat with response code 500. A \ new thread was posted mr:%d thread-id:%s" - mr.id new_id ) - () ; - Ok (`Added add) - | _ -> - let new_id = - note_id edit_result - (* API for Editing a descussion note returns only the note JSON info. *) - in - if Int.equal note new_id then - Reporting.add_conditional mr.reports - (Fmt.str - "A previous thread note was found and was edited. mr:%d \ - thread-id:%s note-id:%d" - mr.id id note ) - else + mr.id new_id ) ; Warnings.add ~warnings ~mr ~s: + (Fmt.str + "Failed to edit a previous threat with response code 500. A \ + new thread was posted mr:%d thread-id:%s" + mr.id new_id ) + () ; + Ok (`Added add_result) + | _ -> + let note_id result = + let open Jq in + try get_dict result |> get_field "id" |> get_int + with e -> + Debug.dbg + "Bot_actions.post_or_edit_mr_thread note_id error \n\n\n\ + %s, %s\n\n" + (Exn.to_string_mach e) + (Ezjsonm.value_to_string result) ; + raise e in + let report_note id = + Jq.(dict [("id", int id); ("reporting", `String "OK")]) in + let new_id = + note_id + (if reporting_only then report_note comment.id else edit_result) + (* API for editing a descussion note returns only the note JSON. So there is not thread id*) + in + if Int.equal comment.id new_id then + Reporting.add_conditional mr.reports + (Fmt.str + "A previous thread note was found and was edited. mr:%d \ + thread-id:%s note-id:%d" + mr.id thread.id comment.id ) + else + Reporting.add_conditional mr.reports (Fmt.str "Attempted to edit a thread and might have posted a new \ one. There is a new thread id. mr:%d previous thread-id:%s \ new note-id:%d" - mr.id id new_id ) + mr.id thread.id new_id ) ; + Warnings.add ~warnings ~mr + ~s: + (Fmt.str + "Attempted to edit a thread and might have posted a new \ + one. There is a new thread id. mr:%d previous thread-id:%s \ + new note-id:%d" + mr.id thread.id new_id ) () ; - Ok (`Edited edit_result) ) + Ok (`Edited edit_result) ) ) | _, Error e -> Error e (** [post_or_edit_mr_comment_or_thread] will post a merge request thread if @@ -197,19 +228,20 @@ let post_or_edit_mr_comment_or_thread ?(require_resolve_thread = false) ?(do_edit_existing = true) ?(token : string option) ~(mr_result : Merge_request.mr_result) ~(user_id : int) ~(retry_options : Web_api.Retry_options.t) ~(warnings : Warnings.t) - body_of_prev = + ~(reporting_only : bool) body_of_prev = if require_resolve_thread then post_or_edit_mr_thread ~do_edit_existing ?token ~mr_result ~user_id - ~retry_options ~warnings body_of_prev + ~retry_options ~warnings ~reporting_only body_of_prev else post_or_edit_mr_comment ~do_edit_existing ?token ~mr_result ~user_id - ~retry_options ~warnings body_of_prev + ~retry_options ~warnings ~reporting_only body_of_prev (** [check_json message api_call] is the [api_call] : Ezjosnm.value or an exception. check_json m a will attempt to parse the JOSN data [a] returned by a call to the Gitlab API by checking the "id" field. A failure raises and exception with the message [m]. *) -let check_json ?(message = "Gitlab API call") (api_call : Ezjsonm.value) = +let check_json ?(message = "Gitlab API call") (api_call : Ezjsonm.value) : Jq.t + = let open Jq in try get_dict api_call |> get_field "id" |> get_int @@ -222,7 +254,9 @@ let check_json ?(message = "Gitlab API call") (api_call : Ezjsonm.value) = (** [label_mr token project mr labels] adds Gitlab [labels] to a merge request. *) let label_mr ?(token : string option) ~(project : string) ~(mr : Merge_request.t) ~(retry_options : Web_api.Retry_options.t) - (labels : string list) = + ~(reporting_only : bool) (labels : string list) = + let report_json = + Ezjsonm.(dict [("id", int mr.id); ("reporting", `String "OK")]) in match List.filter labels ~f:(fun label -> List.mem mr.labels label ~equal:String.equal |> not ) @@ -232,8 +266,10 @@ let label_mr ?(token : string option) ~(project : string) try Ok (Some - ( Gitlab.add_mr_labels ?token ~project ~mr_id:mr.id ~retry_options - labels + ( ( if reporting_only then report_json + else + Gitlab.add_mr_labels ?token ~project ~mr_id:mr.id ~retry_options + labels ) |> check_json ~message: (Printf.sprintf "Gitlab.add_mr_labels project:%s mr:%d" @@ -244,7 +280,9 @@ let label_mr ?(token : string option) ~(project : string) request. *) let unlabel_mr ?(token : string option) ~(project : string) ~(mr : Merge_request.t) ~(retry_options : Web_api.Retry_options.t) - (labels : string list) = + ~(reporting_only : bool) (labels : string list) = + let report_json = + Ezjsonm.(dict [("id", int mr.id); ("reporting", `String "OK")]) in match List.filter labels ~f:(fun label -> List.mem mr.labels label ~equal:String.equal ) @@ -254,8 +292,10 @@ let unlabel_mr ?(token : string option) ~(project : string) try Ok (Some - ( Gitlab.remove_mr_labels ?token ~project ~mr_id:mr.id ~retry_options - labels + ( ( if reporting_only then report_json + else + Gitlab.remove_mr_labels ?token ~project ~mr_id:mr.id + ~retry_options labels ) |> check_json ~message: (Printf.sprintf "Gitlab.remove_mr_labels project:%s mr:%d" diff --git a/src/lib/comment.ml b/src/lib/comment.ml index 98a579cbd1e1da2fb96f6cb9aabec166d17eeb38..7128b92399bc381ee1b34a74ff239540cb19e4c4 100644 --- a/src/lib/comment.ml +++ b/src/lib/comment.ml @@ -57,7 +57,27 @@ let of_json json : t list = Debug.dbg "Comment.of_json %s\n" (Jq.value_to_string json) ; raise e -(** JSON Mockups for unitests tests. *) +(** [is_new b c] is true if new comment [b] is not equal to a + merge_request.comment.body [c.body] and false otherwise. *) +let is_new ~(body : string) ~(comment : t) : bool = + let b = + (* The comment footer include the current date so it is ignored. *) + match String.substr_index body ~pattern:Md.Delimiters.comment_footer with + | None -> body + | Some i -> String.prefix body i in + let c = comment.body in + not (String.is_prefix c ~prefix:b) + +(** [of_user c u] is Some comment where [u] matches comment.author from a list + [c] or None *) +let of_user ~(comments : t list) ~(user_id : int) : t option = + List.find_map comments ~f:(fun c -> + match c with + | {system= true; _} | {resolvable= true; _} -> + (* Ignore system notes or duscussions threads. *) None + | {author= pm; _} -> if Int.equal pm.id user_id then Some c else None ) + +(** JSON Mockups for unit tests. *) module Mockup = struct let note : int -> string -> Jq.t -> bool -> bool -> Jq.t = let open Jq in diff --git a/src/lib/comment_body.ml b/src/lib/comment_body.ml index 9212f6ac785eea2b5b841d31362d40fcf25ed40a..40262586a5738b59c9a1328b3b8eea4cd2d6b230 100644 --- a/src/lib/comment_body.ml +++ b/src/lib/comment_body.ml @@ -1,46 +1,5 @@ open! Base -module Md = struct - open Fmt - - let empty = "" - let h n s = [str "%s %s" (String.make n '#') s; empty] - let par l = l @ [empty] - let verbatim l = "```" :: l @ ["```"] - let plural = function 1 -> "" | _ -> "s" - let plural_list = function [_] -> "" | _ -> "s" - let plural_be = function 1 -> "is" | _ -> "are" - let third_person = function 1 -> "s" | _ -> "" - let hl = [""; String.make 10 '-'; ""] - let colon_or_dot = function 0 -> "." | _ -> ":" - - let merge_team_doc_link = - "http://tezos.gitlab.io/developer/contributing.html#the-merge-request-bot" - - let todo_comment_doc_link = - "https://tezos.gitlab.io/developer/guidelines.html#todo-fixme-comments" - - let code_escape s = - let max_consecutive_backquotes = - String.fold s ~init:(0, 0) ~f:(fun (prev_c, prev_m) -> function - | '`' -> (prev_c + 1, prev_m) | _ -> (0, max prev_m prev_c) ) - |> snd in - let escaper = String.make (max_consecutive_backquotes + 1) '`' in - String.concat ~sep:"" - [ escaper; (match s.[0] with '`' -> " " | _ -> "" | exception _ -> " "); s - ; ( match s.[String.length s - 1] with - | '`' -> " " - | _ -> "" - | exception _ -> " " ); escaper ] - - let one_couple_few nb = - match nb with - | 0 -> "no" - | 1 -> "one" - | 2 | 3 | 4 -> "a couple of" - | _ -> "a few" -end - let pipeline_links () = match (Caml.Sys.getenv_opt "CI_PIPELINE_ID", Caml.Sys.getenv_opt "CI_JOB_ID") @@ -60,7 +19,7 @@ let simple_message ?(with_footer = true) ?with_touched_files_tags mr = if not with_footer then [] else par - [ str "------"; "" + [ str "%s" Delimiters.comment_footer; "" ; str "I, 🤖 👮, ran this inspection on `%s`." ( System.command_to_string_list "date -R" |> fst |> String.concat ~sep:"" ) diff --git a/src/lib/reviewers.ml b/src/lib/reviewers.ml index 684bb3cb66a38e19b89f20ea5e743903d3670005..e1a6f7fd546e676ff1fc51c37b2d1f9fbb138325 100644 --- a/src/lib/reviewers.ml +++ b/src/lib/reviewers.ml @@ -146,11 +146,11 @@ let assign_or_report ?(token : string option) ~(project : string) mr.id ) ) ) with e -> Error e -let exe token retry_options mr_list errors assign_reviewers = +let exe token retry_options reporting_only mr_list errors assign_reviewers = List.iter mr_list ~f:(function | _, Error (`Mr_not_found _) -> () | project, Ok mr -> ( - let reporting_only = if assign_reviewers then false else true in + let reporting_only = reporting_only || not assign_reviewers in let reviewers = List.concat_map (new_reviewers_and_report ~mr) ~f:(fun rev -> Project_members.(of_uname ~retry_options rev |> of_json) ) diff --git a/src/lib/thread.ml b/src/lib/thread.ml index 40074b6b0063e6014b3525beca2dc33d0e664ba0..ea496635f98d75422599544eb27d00b68a151762 100644 --- a/src/lib/thread.ml +++ b/src/lib/thread.ml @@ -34,6 +34,22 @@ let edit_merge_request_discussion ?token ~project ~mr ~retry_options ~discussion (Fmt.str "projects/%d/merge_requests/%d/discussions/%s/notes/%d" project mr discussion note ) ) +(** [of_user c u] is Some (thread, comment) where user_id matches the author a + comment in a partcular thread from a thread list or None *) +let of_user ~(threads : t list) ~(user_id : int) : (t * Comment.t) option = + let match_comment comments = + let open Comment in + List.find_map comments ~f:(fun c -> + match c with + | {system= true; _} | {resolvable= false; _} -> + (* Ignore system notes or duscussions threads. *) None + | comment -> + if Int.equal comment.author.id user_id then Some c else None ) in + List.find_map threads ~f:(fun th -> + match match_comment th.comments with + | Some c -> Some (th, c) + | None -> None ) + (** A list of merge request Threads. **) let of_json json : t list = let threads =