Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable auto-formatting #5

Merged
merged 1 commit into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .ocamlformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = 0.26.1
profile = conventional
43 changes: 20 additions & 23 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ let ( / ) = Filename.concat
let or_die = function
| Ok x -> x
| Error (`Msg m) ->
Fmt.epr "%s@." m;
exit 1
Fmt.epr "%s@." m;
exit 1

let home =
match Sys.getenv_opt "HOME" with
Expand All @@ -15,9 +15,8 @@ let home =

let ensure_dir_exists ~mode path =
match Unix.stat path with
| exception Unix.Unix_error(Unix.ENOENT, _, _) ->
Unix.mkdir path mode
| Unix.{ st_kind = S_DIR; _} -> ()
| exception Unix.Unix_error (Unix.ENOENT, _, _) -> Unix.mkdir path mode
| Unix.{ st_kind = S_DIR; _ } -> ()
| _ -> Fmt.failwith "%S is not a directory!" path

let last_fetch_file =
Expand All @@ -28,17 +27,15 @@ let last_fetch_file =
let mtime path =
match Unix.stat path with
| info -> Some info.Unix.st_mtime
| exception Unix.Unix_error(Unix.ENOENT, _, _) -> None
| exception Unix.Unix_error (Unix.ENOENT, _, _) -> None

let get_token () =
Token.load (home / ".github" / "github-activity-token")
let get_token () = Token.load (home / ".github" / "github-activity-token")

let show ~from json =
let contribs = Contributions.of_json ~from json in
if Contributions.is_empty contribs then
Fmt.epr "(no activity found since %s)@." from
else
Fmt.pr "@[<v>%a@]@." Contributions.pp contribs
else Fmt.pr "@[<v>%a@]@." Contributions.pp contribs

let mode = `Normal

Expand Down Expand Up @@ -74,21 +71,21 @@ let info = Cmd.info "get-activity"
let run period : 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 () |> or_die in
show ~from:(fst period) @@ Contributions.fetch ~period ~token
)
Period.with_period period ~last_fetch_file ~f:(fun period ->
(* Fmt.pr "period: %a@." Fmt.(pair string string) period; *)
let token = get_token () |> or_die in
show ~from:(fst period) @@ Contributions.fetch ~period ~token)
| `Save ->
Period.with_period period ~last_fetch_file ~f:(fun period ->
let token = get_token () |> or_die in
Contributions.fetch ~period ~token
|> Yojson.Safe.to_file "activity.json"
)
Period.with_period period ~last_fetch_file ~f:(fun period ->
let token = get_token () |> or_die in
Contributions.fetch ~period ~token
|> Yojson.Safe.to_file "activity.json")
| `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"
(* 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"

let term = Term.(const run $ period)
let cmd = Cmd.v info term
Expand Down
1 change: 0 additions & 1 deletion dune-project
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
(lang dune 2.8)
(name get-activity)
(formatting disabled)
(generate_opam_files true)
(source (github tarides/get-activity))
(authors "[email protected]")
Expand Down
153 changes: 83 additions & 70 deletions lib/contributions.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,9 @@ let query =
}|}

let fetch ~period:(start, finish) ~token =
Lwt_main.run begin
let variables = [
"from", `String start;
"to", `String finish;
] in
Graphql.exec ~token ~variables ~query ()
end
Lwt_main.run
(let variables = [ ("from", `String start); ("to", `String finish) ] in
Graphql.exec ~token ~variables ~query ())

module Datetime = struct
type t = string
Expand All @@ -72,12 +68,12 @@ module Datetime = struct
| x -> Fmt.failwith "Invalid Datatime %a" Json.pp x
end

module Repo_map = Map.Make(String)
module Repo_map = Map.Make (String)

type item = {
repo : string;
kind : [`Issue | `PR | `Review of string | `New_repo ];
date: Datetime.t;
kind : [ `Issue | `PR | `Review of string | `New_repo ];
date : Datetime.t;
url : string;
title : string;
body : string;
Expand All @@ -86,103 +82,120 @@ type item = {
type t = { username : string; activity : item list Repo_map.t }

let read_issues json =
Json.Util.to_list (json / "nodes") |> List.filter ((<>) `Null) |> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let x = node / "issue" in
let url = x / "url" |> Json.Util.to_string in
let title = x / "title" |> Json.Util.to_string in
let body = x / "body" |> Json.Util.to_string in
let repo = x / "repository" / "nameWithOwner" |> Json.Util.to_string in
{ kind = `Issue; date; url; title; body; repo }
Json.Util.to_list (json / "nodes")
|> List.filter (( <> ) `Null)
|> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let x = node / "issue" in
let url = x / "url" |> Json.Util.to_string in
let title = x / "title" |> Json.Util.to_string in
let body = x / "body" |> Json.Util.to_string in
let repo = x / "repository" / "nameWithOwner" |> Json.Util.to_string in
{ kind = `Issue; date; url; title; body; repo }

let read_prs json =
Json.Util.to_list (json / "nodes") |> List.filter ((<>) `Null) |> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let pr = node / "pullRequest" in
let url = pr / "url" |> Json.Util.to_string in
let title = pr / "title" |> Json.Util.to_string in
let body = pr / "body" |> Json.Util.to_string in
let repo = pr / "repository" / "nameWithOwner" |> Json.Util.to_string in
{ kind = `PR; date; url; title; body; repo }
Json.Util.to_list (json / "nodes")
|> List.filter (( <> ) `Null)
|> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let pr = node / "pullRequest" in
let url = pr / "url" |> Json.Util.to_string in
let title = pr / "title" |> Json.Util.to_string in
let body = pr / "body" |> Json.Util.to_string in
let repo = pr / "repository" / "nameWithOwner" |> Json.Util.to_string in
{ kind = `PR; date; url; title; body; repo }

let read_reviews json =
Json.Util.to_list (json / "nodes") |> List.filter ((<>) `Null) |> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let review = node / "pullRequestReview" in
let state = review / "state" |> Json.Util.to_string in
let url = review / "url" |> Json.Util.to_string in
let pr = review / "pullRequest" in
let title = pr / "title" |> Json.Util.to_string in
let body = review / "body" |> Json.Util.to_string in
let repo = review / "repository" / "nameWithOwner" |> Json.Util.to_string in
{ kind = `Review state; date; url; title; body; repo }
Json.Util.to_list (json / "nodes")
|> List.filter (( <> ) `Null)
|> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let review = node / "pullRequestReview" in
let state = review / "state" |> Json.Util.to_string in
let url = review / "url" |> Json.Util.to_string in
let pr = review / "pullRequest" in
let title = pr / "title" |> Json.Util.to_string in
let body = review / "body" |> Json.Util.to_string in
let repo =
review / "repository" / "nameWithOwner" |> Json.Util.to_string
in
{ kind = `Review state; date; url; title; body; repo }

let read_repos json =
Json.Util.to_list (json / "nodes") |> List.filter ((<>) `Null) |> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let repo = node / "repository" in
let url = repo / "url" |> Json.Util.to_string in
let repo = repo / "nameWithOwner" |> Json.Util.to_string in
{ kind = `New_repo; date; url; title = "Created new repository"; body = ""; repo }
Json.Util.to_list (json / "nodes")
|> List.filter (( <> ) `Null)
|> List.map @@ fun node ->
let date = Datetime.parse (node / "occurredAt") in
let repo = node / "repository" in
let url = repo / "url" |> Json.Util.to_string in
let repo = repo / "nameWithOwner" |> Json.Util.to_string in
{
kind = `New_repo;
date;
url;
title = "Created new repository";
body = "";
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 items =
read_issues (contribs / "issueContributions") @
read_prs (contribs / "pullRequestContributions") @
read_reviews (contribs / "pullRequestReviewContributions") @
read_repos (contribs / "repositoryContributions")
read_issues (contribs / "issueContributions")
@ read_prs (contribs / "pullRequestContributions")
@ read_reviews (contribs / "pullRequestReviewContributions")
@ read_repos (contribs / "repositoryContributions")
in
let activity =
(* GitHub seems to ignore the time part, so do the filtering here. *)
items
|> List.filter (fun item -> item.date >= from)
|> List.fold_left (fun acc item ->
let items = Repo_map.find_opt item.repo acc |> Option.value ~default:[] in
Repo_map.add item.repo (item :: items) acc
) Repo_map.empty
|> List.fold_left
(fun acc item ->
let items =
Repo_map.find_opt item.repo acc |> Option.value ~default:[]
in
Repo_map.add item.repo (item :: items) acc)
Repo_map.empty
in
{ username; activity }

let id url =
match Astring.String.cut ~sep:"/" ~rev:true url with
| None -> Fmt.failwith "Invalid URL %S" url
| Some (_, id) ->
match Astring.String.cut ~sep:"#" id with
| Some (id, _) -> id
| None -> id
| Some (_, id) -> (
match Astring.String.cut ~sep:"#" id with
| Some (id, _) -> id
| None -> id)

let pp_title f t =
match t.kind with
| `Issue -> Fmt.pf f "%s [#%s](%s)" t.title (id t.url) t.url
| `PR -> Fmt.pf f "%s [#%s](%s)" t.title (id t.url) t.url
| `Review s -> Fmt.pf f "%s %s [#%s](%s)" s t.title (id t.url) t.url
| `New_repo ->
begin match Astring.String.cuts ~sep:"/" t.url |> List.rev with
| repo :: org :: _ -> Fmt.pf f "Created repository [%s/%s](%s)" org repo t.url
| _ -> Fmt.failwith "Malformed URL %S" t.url
end
| `New_repo -> (
match Astring.String.cuts ~sep:"/" t.url |> List.rev with
| repo :: org :: _ ->
Fmt.pf f "Created repository [%s/%s](%s)" org repo t.url
| _ -> Fmt.failwith "Malformed URL %S" t.url)

let pp_body f = function
| "" -> ()
| body ->
let body = body |> String.split_on_char (Char.chr 13) |> String.concat "" in
Fmt.pf f " @,@[<hov>%a@]" Fmt.text body

let pp_item f t =
Fmt.pf f "@[<v>%a.%a@]" pp_title t pp_body t.body
let body =
body |> String.split_on_char (Char.chr 13) |> String.concat ""
in
Fmt.pf f " @,@[<hov>%a@]" Fmt.text body

let pp_item f t = Fmt.pf f "@[<v>%a.%a@]" pp_title t pp_body t.body
let pp_items = Fmt.(list ~sep:(cut ++ cut) pp_item)

let pp_repo f (name, items) =
Fmt.pf f "### %s@,@,%a" name pp_items items

let is_empty { activity; _} = Repo_map.is_empty activity
let pp_repo f (name, items) = Fmt.pf f "### %s@,@,%a" name pp_items items
let is_empty { activity; _ } = Repo_map.is_empty activity

let pp f { activity; _ } =
let by_repo = Repo_map.bindings activity in
match by_repo with
| [] -> Fmt.string f "(no activity)"
| [(_, items)] -> pp_items f items
| [ (_, items) ] -> pp_items f items
| repos -> Fmt.(list ~sep:(cut ++ cut)) pp_repo f repos
6 changes: 3 additions & 3 deletions lib/contributions.mli
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ end

type item = {
repo : string;
kind : [`Issue | `PR | `Review of string | `New_repo ];
date: Datetime.t;
kind : [ `Issue | `PR | `Review of string | `New_repo ];
date : Datetime.t;
url : string;
title : string;
body : string;
Expand All @@ -15,7 +15,7 @@ module Repo_map : Map.S with type key = string

type t = { username : string; activity : item list Repo_map.t }

val fetch : period:(string * string) -> token:Token.t -> Yojson.Safe.t
val fetch : period:string * string -> token:Token.t -> Yojson.Safe.t

val of_json : from:string -> Yojson.Safe.t -> t
(** We pass [from] again here so we can filter out anything that GitHub included by accident. *)
Expand Down
36 changes: 18 additions & 18 deletions lib/graphql.ml
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
open Lwt.Infix

let graphql_endpoint = Uri.of_string "https://api.github.com/graphql"

let ( / ) a b = Yojson.Safe.Util.member b a

let exec ?variables ~token ~query () =
let body =
`Assoc (
("query", `String query) ::
`Assoc
(("query", `String query)
::
(match variables with
| None -> []
| Some v -> ["variables", `Assoc v])
)
|> Yojson.Safe.to_string
|> Cohttp_lwt.Body.of_string
| None -> []
| Some v -> [ ("variables", `Assoc v) ]))
|> Yojson.Safe.to_string |> Cohttp_lwt.Body.of_string
in
let headers = Cohttp.Header.init_with "Authorization" ("bearer " ^ token) in
Cohttp_lwt_unix.Client.post ~headers ~body graphql_endpoint >>=
fun (resp, body) ->
Cohttp_lwt_unix.Client.post ~headers ~body graphql_endpoint
>>= fun (resp, body) ->
Cohttp_lwt.Body.to_string body >|= fun body ->
match Cohttp.Response.status resp with
| `OK ->
let json = Yojson.Safe.from_string body in
begin match json / "errors" with
| `OK -> (
let json = Yojson.Safe.from_string body in
match json / "errors" with
| `Null -> json
| _errors ->
Fmt.failwith "@[<v2>GitHub returned errors: %a@]" (Yojson.Safe.pretty_print ~std:true) json;
end
| err -> Fmt.failwith "@[<v2>Error performing GraphQL query on GitHub: %s@,%s@]"
(Cohttp.Code.string_of_status err)
body
Fmt.failwith "@[<v2>GitHub returned errors: %a@]"
(Yojson.Safe.pretty_print ~std:true)
json)
| err ->
Fmt.failwith "@[<v2>Error performing GraphQL query on GitHub: %s@,%s@]"
(Cohttp.Code.string_of_status err)
body
7 changes: 6 additions & 1 deletion lib/graphql.mli
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
val exec : ?variables:(string * Yojson.Safe.t) list -> token:string -> query:string -> unit -> Yojson.Safe.t Lwt.t
val exec :
?variables:(string * Yojson.Safe.t) list ->
token:string ->
query:string ->
unit ->
Yojson.Safe.t Lwt.t
Loading
Loading