From cbb67336ff25aaa5e865055a480e55baef15cd7b Mon Sep 17 00:00:00 2001 From: Guillaume Petiot Date: Mon, 11 Mar 2024 15:43:11 +0000 Subject: [PATCH] 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 \