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

add -x/--extended-summary CLI option #15

Merged
merged 4 commits into from
Sep 3, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased (Yet)

- Added `-x/--extended-summary` flag to show an extended summary at the end
of the output, where the summarized time per epic and per issue is listed.
- internal code improvements

# v0.3.0 (2024-08-26)

- time span filtering already happens on the server-side, which accelerates
Expand Down
8 changes: 8 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ pub struct CliArgs {
/// Must be no more than `--before`.
#[arg(long = "after", default_value_t = get_default_after_date())]
gitlab_after: NaiveDate,
/// Show an extended summary at the end with the time per issue and per
/// epic.
#[arg(short = 'x', long = "extended-summary")]
print_extended_summary: bool,
}

impl CliArgs {
Expand All @@ -111,6 +115,10 @@ impl CliArgs {
pub const fn after(&self) -> NaiveDate {
self.gitlab_after
}

pub const fn print_extended_summary(&self) -> bool {
self.print_extended_summary
}
}

fn current_date() -> NaiveDate {
Expand Down
151 changes: 0 additions & 151 deletions src/filtering.rs

This file was deleted.

32 changes: 9 additions & 23 deletions src/gitlab_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,30 @@ pub mod types {
use serde::Deserialize;
use std::time::Duration;

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Epic {
pub title: String,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Issue {
pub title: String,
/// Full http link to issue.
pub webUrl: String,
pub epic: Option<Epic>,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Group {
pub fullName: String,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Project {
pub group: Option<Group>,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResponseNode {
pub spentAt: String,
/// For some totally weird reason, GitLab allows negative times.
Expand All @@ -73,24 +73,10 @@ pub mod types {
(self.timeSpent.is_positive(), dur)
}

pub fn group_name(&self) -> Option<&str> {
self.project.group.as_ref().map(|g| g.fullName.as_str())
}

pub fn epic_name(&self) -> Option<&str> {
self.issue.epic.as_ref().map(|e| e.title.as_str())
}

pub fn has_group(&self, name: &str) -> bool {
self.group_name()
.map(|group| group == name)
.unwrap_or(false)
}

pub fn has_epic(&self, name: &str) -> bool {
self.epic_name().map(|epic| epic == name).unwrap_or(false)
}

/// Parses the UTC timestring coming from GitLab in the local timezone of
/// the user. This is necessary so that entries accounted to a Monday on
/// `00:00` in CEST are not displayed as Sunday. The value is returned
Expand All @@ -102,26 +88,26 @@ pub mod types {
}
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResponsePageInfo {
pub hasPreviousPage: bool,
pub startCursor: Option<String>,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResponseTimelogs {
pub nodes: Vec<ResponseNode>,
pub pageInfo: ResponsePageInfo,
}

#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ResponseData {
pub timelogs: ResponseTimelogs,
}

/// The response from the GitLab API with all timelogs for the given
/// time frame.
#[derive(Clone, Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Response {
pub data: ResponseData,
}
Expand Down
64 changes: 49 additions & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ SOFTWARE.
#![deny(rustdoc::all)]

use crate::cfg::get_cfg;
use crate::cli::CliArgs;
use crate::fetch::fetch_results;
use crate::filtering::filter_timelogs;
use crate::gitlab_api::types::ResponseNode;
use chrono::{Datelike, NaiveDate, Weekday};
use nu_ansi_term::{Color, Style};
Expand All @@ -53,7 +53,6 @@ use std::time::Duration;
mod cfg;
mod cli;
mod fetch;
mod filtering;
mod gitlab_api;
mod views;

Expand All @@ -64,28 +63,26 @@ fn main() -> Result<(), Box<dyn Error>> {
println!("Username : {}", cfg.username());
println!("Time Span: {} - {}", cfg.after(), cfg.before());

let data_all = fetch_results(
let response = fetch_results(
cfg.username(),
cfg.host(),
cfg.token(),
cfg.after(),
cfg.before(),
);

// All dates with timelogs.
let data_filtered = filter_timelogs(
&data_all, None, /* time already filtered on server */
None, None,
)
.collect::<Vec<_>>();
// All nodes but as vector to references.
// Simplifies the handling with other parts of the code, especially the
// `views` module.
let nodes = response.data.timelogs.nodes.iter().collect::<Vec<_>>();

if data_filtered.is_empty() {
if nodes.is_empty() {
print_warning(
"Empty response. Is the username correct? Does the token has read permission?",
0,
);
} else {
print_all_weeks(data_filtered.as_slice());
print_all_weeks(nodes.as_slice(), &cfg);
}

Ok(())
Expand Down Expand Up @@ -208,7 +205,41 @@ fn print_week(week: (i32 /* year */, u32 /* iso week */), nodes_of_week: &[&Resp
}
}

fn print_final_summary(nodes: &[&ResponseNode]) {
fn print_extended_summary(nodes: &[&ResponseNode]) {
{
let nodes_by_epic = views::to_nodes_by_epic(nodes);
for (epic, nodes_of_epic) in nodes_by_epic {
let duration = views::to_time_spent_sum(&nodes_of_epic);
print!(" ");
print_duration(duration, Color::Magenta);
print!(
" - {epic_key} {epic_name}",
epic_key = Style::new().dimmed().paint("Epic:"),
epic_name = Style::new().bold().paint(
epic.as_ref()
.map(|e| e.title.as_str())
.unwrap_or("<No Epic>")
)
);
println!();
}
}
{
let nodes_by_issue = views::to_nodes_by_issue(nodes);
for (issue, nodes_of_issue) in nodes_by_issue {
let duration = views::to_time_spent_sum(&nodes_of_issue);
print!(" ");
print_duration(duration, Color::Magenta);
print!(
" - Issue: {issue_name}",
issue_name = Style::new().bold().fg(Color::Green).paint(issue.title)
);
println!();
}
}
}

fn print_final_summary(nodes: &[&ResponseNode], cfg: &CliArgs) {
// Print separator.
{
println!();
Expand All @@ -228,10 +259,13 @@ fn print_final_summary(nodes: &[&ResponseNode]) {
print_duration(total_time, Color::Blue);
println!();

// TODO print by epic, by issue, and by group
if cfg.print_extended_summary() {
println!();
print_extended_summary(nodes);
}
}

fn print_all_weeks(nodes: &[&ResponseNode]) {
fn print_all_weeks(nodes: &[&ResponseNode], cfg: &CliArgs) {
let view = views::to_nodes_by_week(nodes);
for (i, (week, nodes_of_week)) in view.iter().enumerate() {
print_week((week.year(), week.week()), nodes_of_week);
Expand All @@ -242,7 +276,7 @@ fn print_all_weeks(nodes: &[&ResponseNode]) {
}
}

print_final_summary(nodes);
print_final_summary(nodes, cfg);
}

const fn duration_to_hhmm(dur: Duration) -> (u64, u64) {
Expand Down
Loading
Loading