From f217ae51654a033113b7d761842d168888e7ca7d Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Wed, 17 Jul 2024 16:09:39 +0200 Subject: [PATCH 1/2] cli: decouple print_final_summary() from print_all_weeks() --- src/main.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4686fe0..4c6e317 100644 --- a/src/main.rs +++ b/src/main.rs @@ -407,6 +407,22 @@ fn print_week( } } +fn print_final_summary(all_dates: &BTreeSet, res: &Response) { + let total_time = sum_total_time_of_dates(all_dates.iter(), res); + + println!(); + // same length as the week separator + println!("{}", "-".repeat(59)); + println!(); + print!( + "{total_time_key} ({days_amount:>2} days with records): ", + total_time_key = Style::new().bold().paint("Total time"), + days_amount = all_dates.len(), + ); + print_duration(total_time, Color::Blue); + println!(); +} + fn print_all_weeks( all_dates: &BTreeSet, week_to_logs_map: &BTreeMap<(i32, u32), BTreeSet>, @@ -421,19 +437,7 @@ fn print_all_weeks( } } - let total_time = sum_total_time_of_dates(all_dates.iter(), res); - - println!(); - // same length as the week separator - println!("{}", "-".repeat(59)); - println!(); - print!( - "{total_time_key} ({days_amount:>2} days with records): ", - total_time_key = Style::new().bold().paint("Total time"), - days_amount = all_dates.len(), - ); - print_duration(total_time, Color::Blue); - println!(); + print_final_summary(all_dates, res); } const fn duration_to_hhmm(dur: Duration) -> (u64, u64) { From 8a78e82546c5d8c239444d20ceb28e77f5d6526a Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Mon, 26 Aug 2024 13:42:13 +0200 Subject: [PATCH 2/2] api: query for date range already on server This brings a significant performance boost as not all data has to be fetched always. --- .editorconfig | 2 +- CHANGELOG.md | 3 ++ src/gitlab-query.graphql | 2 +- src/main.rs | 72 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.editorconfig b/.editorconfig index 460e8fc..dae9855 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,5 @@ indent_size = 4 trim_trailing_whitespace = true max_line_length = 80 -[{*.nix,*.toml,*.yml}] +[{*.graphql,*.nix,*.toml,*.yml}] indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a00af3..865562e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased (Yet) +- time span filtering already happens on the server-side, which accelerates + requests by a notable amount. + # v0.2.2 (2024-07-04) - improve handling of default xdg config dir (unix only) diff --git a/src/gitlab-query.graphql b/src/gitlab-query.graphql index 209d566..1fb7eee 100644 --- a/src/gitlab-query.graphql +++ b/src/gitlab-query.graphql @@ -1,5 +1,5 @@ { - timelogs(username: "%USERNAME%", last: 500, before: "%BEFORE%") { + timelogs(username: "%USERNAME%", last: 500, before: "%BEFORE%", startDate: "%START_DATE%", endDate: "%END_DATE%") { nodes { spentAt timeSpent diff --git a/src/main.rs b/src/main.rs index 4c6e317..6860ec0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ SOFTWARE. use crate::cli::CfgFile; use crate::gitlab_api::types::{Response, ResponseNode}; -use chrono::{DateTime, Datelike, Local, NaiveDate, Weekday}; +use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveTime, Weekday}; use clap::Parser; use cli::CliArgs; use nu_ansi_term::{Color, Style}; @@ -62,12 +62,47 @@ mod gitlab_api; const GRAPHQL_TEMPLATE: &str = include_str!("./gitlab-query.graphql"); +/// Transforms a [`NaiveDate`] to a `DateTime`. +fn naive_date_to_local_datetime(date: NaiveDate) -> DateTime { + date.and_time(NaiveTime::MIN) + .and_local_timezone(Local) + .unwrap() +} + /// Performs a single request against the GitLab API, getting exactly one page -/// of the paged data source. -fn fetch_result(username: &str, host: &str, token: &str, before: Option<&str>) -> Response { +/// of the paged data source. The data is filtered for the date span to make the +/// request smaller/quicker. +/// +/// # Parameters +/// - `username`: The exact GitLab username of the user. +/// - `host`: Host name of the GitLab instance without `https://` +/// - `token`: GitLab token to access the GitLab instance. Must have at least +/// READ access. +/// - `before`: Identifier from previous request to get the next page of the +/// paginated result. +/// - `start_date`: Inclusive begin date. +/// - `end_date`: Inclusive end date. +fn fetch_result( + username: &str, + host: &str, + token: &str, + before: Option<&str>, + start_date: NaiveDate, + end_date: NaiveDate, +) -> Response { let graphql_query = GRAPHQL_TEMPLATE .replace("%USERNAME%", username) - .replace("%BEFORE%", before.unwrap_or_default()); + .replace("%BEFORE%", before.unwrap_or_default()) + .replace( + "%START_DATE%", + naive_date_to_local_datetime(start_date) + .to_string() + .as_str(), + ) + .replace( + "%END_DATE%", + naive_date_to_local_datetime(end_date).to_string().as_str(), + ); let payload = json!({ "query": graphql_query }); let authorization = format!("Bearer {token}", token = token); @@ -85,8 +120,22 @@ fn fetch_result(username: &str, host: &str, token: &str, before: Option<&str>) - } /// Fetches all results from the API with pagination in mind. -fn fetch_all_results(username: &str, host: &str, token: &str) -> Response { - let base = fetch_result(username, host, token, None); +/// +/// # Parameters +/// - `username`: The exact GitLab username of the user. +/// - `host`: Host name of the GitLab instance without `https://` +/// - `token`: GitLab token to access the GitLab instance. Must have at least +/// READ access. +/// - `start_date`: Inclusive begin date. +/// - `end_date`: Inclusive end date. +fn fetch_all_results( + username: &str, + host: &str, + token: &str, + start_date: NaiveDate, + end_date: NaiveDate, +) -> Response { + let base = fetch_result(username, host, token, None, start_date, end_date); let mut aggregated = base; while aggregated.data.timelogs.pageInfo.hasPreviousPage { @@ -102,6 +151,8 @@ fn fetch_all_results(username: &str, host: &str, token: &str) -> Response { .startCursor .expect("Should be valid string at this point"), ), + start_date, + end_date, ); // Ordering here is not that important, happens later anyway. @@ -185,9 +236,16 @@ fn main() -> Result<(), Box> { println!("Username : {}", cfg.username()); println!("Time Span: {} - {}", cfg.after(), cfg.before()); - let res = fetch_all_results(cfg.username(), cfg.host(), cfg.token()); + let res = fetch_all_results( + cfg.username(), + cfg.host(), + cfg.token(), + cfg.after(), + cfg.before(), + ); // All dates with timelogs. + // TODO this is now obsolete. We need to refactor the "data filter layer" let all_dates = find_dates(&res, &cfg.before(), &cfg.after()); let week_to_logs_map = aggregate_dates_by_week(&all_dates);