From cbb67336ff25aaa5e865055a480e55baef15cd7b Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 15:43:11 +0000 Subject: [PATCH 1/6] Add a '--user' option to extract the activity of an engineer that is not the current user --- CHANGES.md | 1 + bin/main.ml | 20 +++--- lib/contributions.ml | 29 ++++++-- lib/contributions.mli | 8 ++- test/lib/test_contributions.ml | 117 +++++++++++++++++++++------------ 5 files changed, 117 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index df9d662..a27edab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ### Added - Add the `--version` command-line option (#13, @gpetiot) +- Add a `--user` option to extract the activity of an engineer that is not the current user (#, @gpetiot) ### Changed diff --git a/bin/main.ml b/bin/main.ml index a3fa828..417c003 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -33,8 +33,8 @@ let mtime path = let get_token () = Token.load (home / ".github" / "github-activity-token") -let show ~from json = - let contribs = Contributions.of_json ~from json in +let show ~from ~user json = + let contribs = Contributions.of_json ~from ~user json in if Contributions.is_empty contribs then Fmt.epr "(no activity found since %s)@." from else Fmt.pr "@[%a@]@." Contributions.pp contribs @@ -68,6 +68,10 @@ let period = in Term.(const f $ from $ to_ $ last_week) +let user = + let doc = Arg.info ~doc:"User name" [ "user" ] in + Arg.(value & opt (some string) None & doc) + let version = match Build_info.V1.version () with | None -> "dev" @@ -75,19 +79,19 @@ let version = let info = Cmd.info "get-activity" ~version -let run period : unit = +let run period user : unit = match mode with | `Normal -> Period.with_period period ~last_fetch_file ~f:(fun period -> (* Fmt.pr "period: %a@." Fmt.(pair string string) period; *) let* token = get_token () in - let request = Contributions.request ~period ~token in + let request = Contributions.request ~period ~user ~token in let* contributions = Graphql.exec request in - show ~from:(fst period) contributions) + show ~from:(fst period) ~user contributions) | `Save -> Period.with_period period ~last_fetch_file ~f:(fun period -> let* token = get_token () in - let request = Contributions.request ~period ~token in + let request = Contributions.request ~period ~user ~token in let* contributions = Graphql.exec request in Yojson.Safe.to_file "activity.json" contributions) | `Load -> @@ -95,8 +99,8 @@ let run period : unit = let from = mtime last_fetch_file |> Option.value ~default:0.0 |> Period.to_8601 in - show ~from @@ Yojson.Safe.from_file "activity.json" + show ~from ~user @@ Yojson.Safe.from_file "activity.json" -let term = Term.(const run $ period) +let term = Term.(const run $ period $ user) let cmd = Cmd.v info term let () = Stdlib.exit @@ Cmd.eval cmd diff --git a/lib/contributions.ml b/lib/contributions.ml index ac61024..6d2c0ae 100644 --- a/lib/contributions.ml +++ b/lib/contributions.ml @@ -2,9 +2,10 @@ module Json = Yojson.Safe let ( / ) a b = Json.Util.member b a -let query = - {|query($from: DateTime!, $to: DateTime!) { - viewer { +let query user = + Format.sprintf + {|query($from: DateTime!, $to: DateTime!) { + %s { login contributionsCollection(from: $from, to: $to) { issueContributions(first: 100) { @@ -54,9 +55,13 @@ let query = } } }|} + (match user with + | Some u -> Format.sprintf "user(login: %S)" u + | None -> "viewer") -let request ~period:(start, finish) ~token = +let request ~period:(start, finish) ~user ~token = let variables = [ ("from", `String start); ("to", `String finish) ] in + let query = query user in Graphql.request ~token ~variables ~query () module Datetime = struct @@ -137,9 +142,19 @@ let read_repos json = repo; } -let of_json ~from json = - let username = json / "data" / "viewer" / "login" |> Json.Util.to_string in - let contribs = json / "data" / "viewer" / "contributionsCollection" in +let of_json ~from ~user json = + let username, contribs = + match user with + | Some username -> + let contribs = json / "data" / "user" / "contributionsCollection" in + (username, contribs) + | None -> + let username = + json / "data" / "viewer" / "login" |> Json.Util.to_string + in + let contribs = json / "data" / "viewer" / "contributionsCollection" in + (username, contribs) + in let items = read_issues (contribs / "issueContributions") @ read_prs (contribs / "pullRequestContributions") diff --git a/lib/contributions.mli b/lib/contributions.mli index cf59050..6981322 100644 --- a/lib/contributions.mli +++ b/lib/contributions.mli @@ -15,9 +15,13 @@ module Repo_map : Map.S with type key = string type t = { username : string; activity : item list Repo_map.t } -val request : period:string * string -> token:Token.t -> Graphql.request +val request : + period:string * string -> + user:string option -> + token:Token.t -> + Graphql.request -val of_json : from:string -> Yojson.Safe.t -> t +val of_json : from:string -> user:string option -> Yojson.Safe.t -> t (** We pass [from] again here so we can filter out anything that GitHub included by accident. *) val is_empty : t -> bool diff --git a/test/lib/test_contributions.ml b/test/lib/test_contributions.ml index fde6c91..51f3f1a 100644 --- a/test/lib/test_contributions.ml +++ b/test/lib/test_contributions.ml @@ -83,29 +83,10 @@ module Testable = struct let contributions = Contributions.testable end -let test_request = - let make_test name ~period ~token ~expected = - let name = Printf.sprintf "request: %s" name in - let test_fun () = - let actual = Contributions.request ~period ~token in - Alcotest.(check Alcotest_ext.request) name expected actual - in - (name, `Quick, test_fun) - in - [ - make_test "no token" ~token:"" ~period:("", "") - ~expected: - { - meth = `POST; - url = "https://api.github.com/graphql"; - headers = [ ("Authorization", "bearer ") ]; - body = - `Assoc - [ - ( "query", - `String - {|query($from: DateTime!, $to: DateTime!) { - viewer { +let request ~user = + Format.sprintf + {|query($from: DateTime!, $to: DateTime!) { + %s { login contributionsCollection(from: $from, to: $to) { issueContributions(first: 100) { @@ -155,19 +136,61 @@ let test_request = } } }|} - ); - ( "variables", - `Assoc [ ("from", `String ""); ("to", `String "") ] ); - ]; - }; + (match user with + | Some u -> Format.sprintf "user(login: %S)" u + | None -> "viewer") + +let test_request = + let make_test name ~period ~user ~token ~expected = + let name = Printf.sprintf "request: %s" name in + let test_fun () = + let actual = Contributions.request ~period ~user ~token in + Alcotest.(check Alcotest_ext.request) name expected actual + in + (name, `Quick, test_fun) + in + [ + (let user = None in + make_test "no token" ~user ~token:"" ~period:("", "") + ~expected: + { + meth = `POST; + url = "https://api.github.com/graphql"; + headers = [ ("Authorization", "bearer ") ]; + body = + `Assoc + [ + ("query", `String (request ~user)); + ( "variables", + `Assoc [ ("from", `String ""); ("to", `String "") ] ); + ]; + }); + (let user = Some "me" in + make_test "no token" ~user ~token:"" ~period:("", "") + ~expected: + { + meth = `POST; + url = "https://api.github.com/graphql"; + headers = [ ("Authorization", "bearer ") ]; + body = + `Assoc + [ + ("query", `String (request ~user)); + ( "variables", + `Assoc [ ("from", `String ""); ("to", `String "") ] ); + ]; + }); ] -let activity_example = - {| +let or_viewer = function Some u -> u | None -> "gpetiot" + +let activity_example ~user = + Format.sprintf + {| { "data": { - "viewer": { - "login": "gpetiot", + %S: { + "login": %S, "contributionsCollection": { "issueContributions": { "nodes": [ @@ -282,13 +305,16 @@ let activity_example = } } |} + (match user with Some _ -> "user" | None -> "viewer") + (user |> or_viewer) -let activity_example_json = Yojson.Safe.from_string activity_example +let activity_example_json ~user = + Yojson.Safe.from_string (activity_example ~user) -let contributions_example = +let contributions_example ~user = let open Contributions in { - username = "gpetiot"; + username = user |> or_viewer; activity = Repo_map.empty |> Repo_map.add "gpetiot/config.ml" @@ -379,17 +405,23 @@ let contributions_example = } let test_of_json = - let make_test name ~from json ~expected = + let make_test name ~from ~user json ~expected = let name = Printf.sprintf "of_json: %s" name in let test_fun () = - let actual = Contributions.of_json ~from json in + let actual = Contributions.of_json ~from ~user json in Alcotest.(check Testable.contributions) name expected actual in (name, `Quick, test_fun) in [ - make_test "no token" ~from:"" activity_example_json - ~expected:contributions_example; + (let user = None in + make_test "no token" ~from:"" ~user + (activity_example_json ~user) + ~expected:(contributions_example ~user)); + (let user = Some "gpetiot" in + make_test "no token" ~from:"" ~user + (activity_example_json ~user) + ~expected:(contributions_example ~user)); ] let test_is_empty = @@ -406,7 +438,9 @@ let test_is_empty = ~input: { Contributions.username = ""; activity = Contributions.Repo_map.empty } ~expected:true; - make_test "not empty" ~input:contributions_example ~expected:false; + make_test "not empty" + ~input:(contributions_example ~user:None) + ~expected:false; ] let test_pp = @@ -423,7 +457,8 @@ let test_pp = ~input: { Contributions.username = ""; activity = Contributions.Repo_map.empty } ~expected:"(no activity)"; - make_test "not empty" ~input:contributions_example + make_test "not empty" + ~input:(contributions_example ~user:None) ~expected: "### gpetiot/config.ml\n\ Created repository \ From e7733dbe54d58d45df438025baa8a8dad6ff3dc8 Mon Sep 17 00:00:00 2001 From: "Guillaume \"Liam\" Petiot" Date: Mon, 11 Mar 2024 16:04:50 +0000 Subject: [PATCH 2/6] Update CHANGES.md Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a27edab..55ed706 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ ### Added - Add the `--version` command-line option (#13, @gpetiot) -- Add a `--user` option to extract the activity of an engineer that is not the current user (#, @gpetiot) +- Add a `--user` option to extract the activity of an engineer that is not the current user (#14, @gpetiot) ### Changed From c43af04839f6bbf61c3c8f633ea0665575db9cc6 Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 16:19:49 +0000 Subject: [PATCH 3/6] Define a User type --- bin/main.ml | 13 +++++++++++-- lib/contributions.ml | 24 ++++++++---------------- lib/contributions.mli | 7 ++----- test/lib/test_contributions.ml | 25 +++++++++++-------------- 4 files changed, 32 insertions(+), 37 deletions(-) diff --git a/bin/main.ml b/bin/main.ml index 417c003..6ee6275 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -68,9 +68,18 @@ let period = in Term.(const f $ from $ to_ $ last_week) -let user = +let user : User.t Term.t = + let str_parser, str_printer = Arg.string in + let parser x = + match str_parser x with `Ok x -> `Ok (User.User x) | `Error e -> `Error e + in + let printer fs = function + | User.Viewer -> str_printer fs "viewer" + | User x -> str_printer fs x + in + let user_conv = (parser, printer) in let doc = Arg.info ~doc:"User name" [ "user" ] in - Arg.(value & opt (some string) None & doc) + Arg.(value & opt user_conv Viewer & doc) let version = match Build_info.V1.version () with diff --git a/lib/contributions.ml b/lib/contributions.ml index 6d2c0ae..8694b70 100644 --- a/lib/contributions.ml +++ b/lib/contributions.ml @@ -3,9 +3,9 @@ module Json = Yojson.Safe let ( / ) a b = Json.Util.member b a let query user = - Format.sprintf + Format.asprintf {|query($from: DateTime!, $to: DateTime!) { - %s { + %a { login contributionsCollection(from: $from, to: $to) { issueContributions(first: 100) { @@ -55,9 +55,7 @@ let query user = } } }|} - (match user with - | Some u -> Format.sprintf "user(login: %S)" u - | None -> "viewer") + User.query user let request ~period:(start, finish) ~user ~token = let variables = [ ("from", `String start); ("to", `String finish) ] in @@ -143,17 +141,11 @@ let read_repos json = } let of_json ~from ~user json = - let username, contribs = - match user with - | Some username -> - let contribs = json / "data" / "user" / "contributionsCollection" in - (username, contribs) - | None -> - let username = - json / "data" / "viewer" / "login" |> Json.Util.to_string - in - let contribs = json / "data" / "viewer" / "contributionsCollection" in - (username, contribs) + let username = + json / "data" / User.response_field user / "login" |> Json.Util.to_string + in + let contribs = + json / "data" / User.response_field user / "contributionsCollection" in let items = read_issues (contribs / "issueContributions") diff --git a/lib/contributions.mli b/lib/contributions.mli index 6981322..3c2bfc4 100644 --- a/lib/contributions.mli +++ b/lib/contributions.mli @@ -16,12 +16,9 @@ module Repo_map : Map.S with type key = string type t = { username : string; activity : item list Repo_map.t } val request : - period:string * string -> - user:string option -> - token:Token.t -> - Graphql.request + period:string * string -> user:User.t -> token:Token.t -> Graphql.request -val of_json : from:string -> user:string option -> Yojson.Safe.t -> t +val of_json : from:string -> user:User.t -> Yojson.Safe.t -> t (** We pass [from] again here so we can filter out anything that GitHub included by accident. *) val is_empty : t -> bool diff --git a/test/lib/test_contributions.ml b/test/lib/test_contributions.ml index 51f3f1a..565e9c0 100644 --- a/test/lib/test_contributions.ml +++ b/test/lib/test_contributions.ml @@ -84,9 +84,9 @@ module Testable = struct end let request ~user = - Format.sprintf + Format.asprintf {|query($from: DateTime!, $to: DateTime!) { - %s { + %a { login contributionsCollection(from: $from, to: $to) { issueContributions(first: 100) { @@ -136,9 +136,7 @@ let request ~user = } } }|} - (match user with - | Some u -> Format.sprintf "user(login: %S)" u - | None -> "viewer") + User.query user let test_request = let make_test name ~period ~user ~token ~expected = @@ -150,7 +148,7 @@ let test_request = (name, `Quick, test_fun) in [ - (let user = None in + (let user = User.Viewer in make_test "no token" ~user ~token:"" ~period:("", "") ~expected: { @@ -165,7 +163,7 @@ let test_request = `Assoc [ ("from", `String ""); ("to", `String "") ] ); ]; }); - (let user = Some "me" in + (let user = User.User "me" in make_test "no token" ~user ~token:"" ~period:("", "") ~expected: { @@ -182,7 +180,7 @@ let test_request = }); ] -let or_viewer = function Some u -> u | None -> "gpetiot" +let or_viewer = function User.User u -> u | Viewer -> "gpetiot" let activity_example ~user = Format.sprintf @@ -305,8 +303,7 @@ let activity_example ~user = } } |} - (match user with Some _ -> "user" | None -> "viewer") - (user |> or_viewer) + (User.response_field user) (user |> or_viewer) let activity_example_json ~user = Yojson.Safe.from_string (activity_example ~user) @@ -414,11 +411,11 @@ let test_of_json = (name, `Quick, test_fun) in [ - (let user = None in + (let user = User.Viewer in make_test "no token" ~from:"" ~user (activity_example_json ~user) ~expected:(contributions_example ~user)); - (let user = Some "gpetiot" in + (let user = User.User "gpetiot" in make_test "no token" ~from:"" ~user (activity_example_json ~user) ~expected:(contributions_example ~user)); @@ -439,7 +436,7 @@ let test_is_empty = { Contributions.username = ""; activity = Contributions.Repo_map.empty } ~expected:true; make_test "not empty" - ~input:(contributions_example ~user:None) + ~input:(contributions_example ~user:Viewer) ~expected:false; ] @@ -458,7 +455,7 @@ let test_pp = { Contributions.username = ""; activity = Contributions.Repo_map.empty } ~expected:"(no activity)"; make_test "not empty" - ~input:(contributions_example ~user:None) + ~input:(contributions_example ~user:Viewer) ~expected: "### gpetiot/config.ml\n\ Created repository \ From 170853f1fe0e0c839820d3ac96b67bf37e8a9878 Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 16:25:56 +0000 Subject: [PATCH 4/6] Forgot files --- lib/user.ml | 7 +++++++ lib/user.mli | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 lib/user.ml create mode 100644 lib/user.mli diff --git a/lib/user.ml b/lib/user.ml new file mode 100644 index 0000000..e39c9b6 --- /dev/null +++ b/lib/user.ml @@ -0,0 +1,7 @@ +type t = Viewer | User of string + +let query fs = function + | User u -> Format.fprintf fs "user(login: %S)" u + | Viewer -> Format.fprintf fs "viewer" + +let response_field = function User _ -> "user" | Viewer -> "viewer" diff --git a/lib/user.mli b/lib/user.mli new file mode 100644 index 0000000..69cdccd --- /dev/null +++ b/lib/user.mli @@ -0,0 +1,7 @@ +type t = Viewer | User of string + +val query : Format.formatter -> t -> unit +(** Prints graphql query fragment that request the given user. *) + +val response_field : t -> string +(** The field in the grphql response that contains the user fields. *) From 4be8116629e59984443a172c6093d8d3685888d8 Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 16:27:42 +0000 Subject: [PATCH 5/6] changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 55ed706..3b502e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Redesign the graphql requests (#12, @gpetiot) + `Graphql.exec` now takes a `request` + `Contributions.fetch` has been replaced by `Contributions.request` that builds a `request` +- Add a `~user:User.t` parameter to `Contributions.request` and `Contributions.of_json` (#14, @gpetiot) ## 0.2.0 From 8de2d374933ff3ac05874ee62f5e956843f30d69 Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 16:48:18 +0000 Subject: [PATCH 6/6] typo --- lib/user.mli | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/user.mli b/lib/user.mli index 69cdccd..852b443 100644 --- a/lib/user.mli +++ b/lib/user.mli @@ -1,7 +1,7 @@ type t = Viewer | User of string val query : Format.formatter -> t -> unit -(** Prints graphql query fragment that request the given user. *) +(** Prints graphql query fragment that requests the given user. *) val response_field : t -> string -(** The field in the grphql response that contains the user fields. *) +(** The field in the graphql response that contains the user fields. *)