Skip to content

Commit

Permalink
Add a '--user' option to extract the activity of an engineer that is …
Browse files Browse the repository at this point in the history
…not the current user (#14)
  • Loading branch information
gpetiot authored Mar 12, 2024
1 parent 06b13cf commit 57cd881
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 58 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (#14, @gpetiot)

### Changed

Expand All @@ -11,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

Expand Down
29 changes: 21 additions & 8 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "@[<v>%a@]@." Contributions.pp contribs
Expand Down Expand Up @@ -68,35 +68,48 @@ let period =
in
Term.(const f $ from $ to_ $ last_week)

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 user_conv Viewer & doc)

let version =
match Build_info.V1.version () with
| None -> "dev"
| Some v -> Build_info.V1.Version.to_string v

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 ->
(* When testing formatting changes, it is quicker to fetch the data once and then load it again for each test: *)
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
21 changes: 14 additions & 7 deletions lib/contributions.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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.asprintf
{|query($from: DateTime!, $to: DateTime!) {
%a {
login
contributionsCollection(from: $from, to: $to) {
issueContributions(first: 100) {
Expand Down Expand Up @@ -54,9 +55,11 @@ let query =
}
}
}|}
User.query user

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
Expand Down Expand Up @@ -137,9 +140,13 @@ 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 =
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")
@ read_prs (contribs / "pullRequestContributions")
Expand Down
5 changes: 3 additions & 2 deletions lib/contributions.mli
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ 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:User.t -> token:Token.t -> Graphql.request

val of_json : from:string -> 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
Expand Down
7 changes: 7 additions & 0 deletions lib/user.ml
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions lib/user.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type t = Viewer | User of string

val query : Format.formatter -> t -> unit
(** Prints graphql query fragment that requests the given user. *)

val response_field : t -> string
(** The field in the graphql response that contains the user fields. *)
114 changes: 73 additions & 41 deletions test/lib/test_contributions.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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.asprintf
{|query($from: DateTime!, $to: DateTime!) {
%a {
login
contributionsCollection(from: $from, to: $to) {
issueContributions(first: 100) {
Expand Down Expand Up @@ -155,19 +136,59 @@ let test_request =
}
}
}|}
);
( "variables",
`Assoc [ ("from", `String ""); ("to", `String "") ] );
];
};
User.query user

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 = User.Viewer 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 = User.User "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 User.User u -> u | Viewer -> "gpetiot"

let activity_example ~user =
Format.sprintf
{|
{
"data": {
"viewer": {
"login": "gpetiot",
%S: {
"login": %S,
"contributionsCollection": {
"issueContributions": {
"nodes": [
Expand Down Expand Up @@ -282,13 +303,15 @@ let activity_example =
}
}
|}
(User.response_field user) (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"
Expand Down Expand Up @@ -379,17 +402,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 = User.Viewer in
make_test "no token" ~from:"" ~user
(activity_example_json ~user)
~expected:(contributions_example ~user));
(let user = User.User "gpetiot" in
make_test "no token" ~from:"" ~user
(activity_example_json ~user)
~expected:(contributions_example ~user));
]

let test_is_empty =
Expand All @@ -406,7 +435,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:Viewer)
~expected:false;
]

let test_pp =
Expand All @@ -423,7 +454,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:Viewer)
~expected:
"### gpetiot/config.ml\n\
Created repository \
Expand Down

0 comments on commit 57cd881

Please sign in to comment.