Skip to content

Added File Churn Metric #1071

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

Merged
merged 31 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5019e81
add churn metric
o2sh Jun 3, 2023
205dd36
add diff_count
o2sh Jun 3, 2023
6dadbf8
revert
o2sh Jun 3, 2023
5f941dd
rename
o2sh Jun 3, 2023
667ca9a
add churn cli flags
o2sh Jun 3, 2023
6850da5
fix integration test
o2sh Jun 3, 2023
3d58e26
add unit tests
o2sh Jun 3, 2023
d4a5339
Merge branch 'main' into feat/churn
o2sh Jun 4, 2023
6ffd49c
try fix codeowners
o2sh Jun 4, 2023
5f91e6e
fix codeowners
o2sh Jun 4, 2023
2f7a2b4
Merge branch 'main' into feat/churn
o2sh Jun 4, 2023
5b85a3f
Merge branch 'main' into feat/churn
o2sh Jun 4, 2023
afa5788
Optimize diff implementation
Byron Jun 4, 2023
0339581
Don't fail on missing parent as we want to work in shallow repos, too
Byron Jun 4, 2023
c36fe41
Increase performance by decoding the commit only once
Byron Jun 4, 2023
fee2db3
track changes on executable files
o2sh Jun 4, 2023
ce54746
remove for_each method
o2sh Jun 4, 2023
f9e0777
use horizontal ellipsis
o2sh Jun 4, 2023
27246fe
review
o2sh Jun 5, 2023
7cae61b
use MAIN_SEPERATOR when building path
o2sh Jun 5, 2023
195fe73
revert
o2sh Jun 5, 2023
627b9f2
run expensive diffs in parallel and abort them once we run out of time.
Byron Jun 6, 2023
1433c17
Always calculate at least one diff for 'churn'
Byron Jun 6, 2023
27af8a5
improved readability + churn_pool_size CLI flag
o2sh Jun 7, 2023
cc6b3ef
fix test
o2sh Jun 7, 2023
ddeaea3
halt if the churn pool size is bigger than the total number of commits
o2sh Jun 7, 2023
2876860
improve readability
o2sh Jun 8, 2023
0b0abae
add unit test
o2sh Jun 8, 2023
16d2274
refactor
Byron Jun 9, 2023
c8a8cb7
update to latest `gix` version
Byron Jun 9, 2023
b767374
Avoid exhaustive memory consumption by sending the commit-id instead …
Byron Jun 9, 2023
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
627 changes: 513 additions & 114 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ clap_complete = "4.2.3"
gix-features-for-configuration-only = { package = "gix-features", version = "0.29.0", features = [
"zlib-ng",
] }
gix = { version = "0.44.1", default-features = false, features = [
gix = { version = "0.45.1", default-features = false, features = [
"max-performance-safe",
] }
git2 = { version = "0.17.1", default-features = false }
Expand Down Expand Up @@ -55,6 +55,7 @@ criterion = "0.4.0"
gix-testtools = "0.12.0"
insta = { version = "1.29.0", features = ["json", "redactions"] }
pretty_assertions = "1.3.0"
rstest = "0.17.0"

[[bench]]
name = "repo"
Expand Down
11 changes: 11 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ pub struct InfoCliOptions {
/// Maximum NUM of languages to be shown
#[arg(long, default_value_t = 6usize, value_name = "NUM")]
pub number_of_languages: usize,
/// Maximum NUM of file churns to be shown
#[arg(long, default_value_t = 3usize, value_name = "NUM")]
pub number_of_file_churns: usize,
/// Maximum NUM of commits from HEAD used to compute the churn summary
///
/// By default, the actual value is non-deterministic due to time-based computation
/// and will be displayed under the info title "Churn (NUM)"
#[arg(long, value_name = "NUM")]
pub churn_pool_size: Option<usize>,
/// Ignore all files & directories matching EXCLUDE
#[arg(long, short, num_args = 1.., value_hint = ValueHint::AnyPath)]
pub exclude: Vec<PathBuf>,
Expand Down Expand Up @@ -228,6 +237,8 @@ impl Default for InfoCliOptions {
InfoCliOptions {
number_of_authors: 3,
number_of_languages: 6,
number_of_file_churns: 3,
churn_pool_size: Option::default(),
exclude: Vec::default(),
no_bots: Option::default(),
no_merges: Default::default(),
Expand Down
58 changes: 20 additions & 38 deletions src/info/author.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
cli::NumberSeparator,
info::{format_number, utils::git::Commits, utils::info_field::InfoField},
info::{format_number, utils::git::CommitMetrics, utils::info_field::InfoField},
};
use owo_colors::{DynColors, OwoColorize};
use serde::Serialize;
Expand All @@ -10,46 +10,42 @@ use std::fmt::Write;
#[serde(rename_all = "camelCase")]
pub struct Author {
pub name: String,
email: String,
email: Option<String>,
nbr_of_commits: usize,
contribution: usize,
#[serde(skip_serializing)]
show_email: bool,
#[serde(skip_serializing)]
number_separator: NumberSeparator,
}

impl Author {
pub fn new(
name: gix::bstr::BString,
email: gix::bstr::BString,
name: String,
email: Option<String>,
nbr_of_commits: usize,
total_nbr_of_commits: usize,
show_email: bool,
number_separator: NumberSeparator,
) -> Self {
let contribution =
(nbr_of_commits as f32 * 100. / total_nbr_of_commits as f32).round() as usize;
Self {
name: name.to_string(),
email: email.to_string(),
name,
email,
nbr_of_commits,
contribution,
show_email,
number_separator,
}
}
}

impl std::fmt::Display for Author {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.show_email {
if let Some(email) = &self.email {
write!(
f,
"{}% {} <{}> {}",
self.contribution,
self.name,
self.email,
email,
format_number(&self.nbr_of_commits, self.number_separator)
)
} else {
Expand All @@ -72,8 +68,8 @@ pub struct AuthorsInfo {
}

impl AuthorsInfo {
pub fn new(info_color: DynColors, commits: &Commits) -> Self {
let authors = commits.authors_to_display.clone();
pub fn new(info_color: DynColors, commit_metrics: &CommitMetrics) -> Self {
let authors = commit_metrics.authors_to_display.clone();
Self {
authors,
info_color,
Expand All @@ -91,9 +87,9 @@ impl std::fmt::Display for AuthorsInfo {
let author_str = author.color(self.info_color);

if i == 0 {
let _ = write!(authors_info, "{author_str}");
write!(authors_info, "{author_str}")?;
} else {
let _ = write!(authors_info, "\n{:<width$}{}", "", author_str, width = pad);
write!(authors_info, "\n{:<width$}{}", "", author_str, width = pad)?;
}
}

Expand Down Expand Up @@ -128,10 +124,9 @@ mod test {
fn test_display_author() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
Some("john.doe@email.com".into()),
1500,
2000,
true,
NumberSeparator::Plain,
);

Expand All @@ -140,14 +135,7 @@ mod test {

#[test]
fn test_display_author_with_no_email() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
1500,
2000,
false,
NumberSeparator::Plain,
);
let author = Author::new("John Doe".into(), None, 1500, 2000, NumberSeparator::Plain);

assert_eq!(author.to_string(), "75% John Doe 1500");
}
Expand All @@ -156,10 +144,9 @@ mod test {
fn test_authors_info_title_with_one_author() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
Some("john.doe@email.com".into()),
1500,
2000,
true,
NumberSeparator::Plain,
);

Expand All @@ -175,19 +162,17 @@ mod test {
fn test_authors_info_title_with_two_authors() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
Some("john.doe@email.com".into()),
1500,
2000,
true,
NumberSeparator::Plain,
);

let author_2 = Author::new(
"Roberto Berto".into(),
"bertolone2000@email.com".into(),
None,
240,
300,
false,
NumberSeparator::Plain,
);

Expand All @@ -203,10 +188,9 @@ mod test {
fn test_author_info_value_with_one_author() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
Some("john.doe@email.com".into()),
1500,
2000,
true,
NumberSeparator::Plain,
);

Expand All @@ -227,19 +211,17 @@ mod test {
fn test_author_info_value_with_two_authors() {
let author = Author::new(
"John Doe".into(),
"john.doe@email.com".into(),
Some("john.doe@email.com".into()),
1500,
2000,
true,
NumberSeparator::Plain,
);

let author_2 = Author::new(
"Roberto Berto".into(),
"bertolone2000@email.com".into(),
None,
240,
300,
false,
NumberSeparator::Plain,
);

Expand Down
157 changes: 157 additions & 0 deletions src/info/churn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use super::utils::{git::CommitMetrics, info_field::InfoField};
use crate::{cli::NumberSeparator, info::format_number};
use owo_colors::{DynColors, OwoColorize};
use serde::Serialize;
use std::fmt::Write;

#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FileChurn {
pub file_path: String,
pub nbr_of_commits: usize,
#[serde(skip_serializing)]
number_separator: NumberSeparator,
}

impl FileChurn {
pub fn new(
file_path: String,
nbr_of_commits: usize,
number_separator: NumberSeparator,
) -> Self {
Self {
file_path,
nbr_of_commits,
number_separator,
}
}
}

impl std::fmt::Display for FileChurn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} {}",
shorten_file_path(&self.file_path, 2),
format_number(&self.nbr_of_commits, self.number_separator)
)
}
}

#[derive(Serialize)]
pub struct ChurnInfo {
pub file_churns: Vec<FileChurn>,
pub churn_pool_size: usize,
#[serde(skip_serializing)]
pub info_color: DynColors,
}
impl ChurnInfo {
pub fn new(info_color: DynColors, commit_metrics: &CommitMetrics) -> Self {
let file_churns = commit_metrics.file_churns_to_display.clone();
Self {
file_churns,
churn_pool_size: commit_metrics.churn_pool_size,
info_color,
}
}
}
impl std::fmt::Display for ChurnInfo {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut churn_info = String::new();

let pad = self.title().len() + 2;

for (i, churn) in self.file_churns.iter().enumerate() {
let churn_str = churn.color(self.info_color);

if i == 0 {
write!(churn_info, "{churn_str}")?;
} else {
write!(churn_info, "\n{:<width$}{}", "", churn_str, width = pad)?;
}
}

write!(f, "{churn_info}")
}
}

#[typetag::serialize]
impl InfoField for ChurnInfo {
fn value(&self) -> String {
self.to_string()
}

fn title(&self) -> String {
format!("Churn ({})", self.churn_pool_size)
}

fn should_color(&self) -> bool {
false
}
}

fn shorten_file_path(file_path: &str, depth: usize) -> String {
let components: Vec<&str> = file_path.split('/').collect();

if depth == 0 || components.len() <= depth {
return file_path.to_string();
}

let truncated_components: Vec<&str> = components
.iter()
.skip(components.len() - depth)
.copied()
.collect();

format!("\u{2026}/{}", truncated_components.join("/"))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_display_file_churn() {
let file_churn = FileChurn::new("path/to/file.txt".into(), 50, NumberSeparator::Plain);

assert_eq!(file_churn.to_string(), "\u{2026}/to/file.txt 50");
}

#[test]
fn test_churn_info_value_with_two_file_churns() {
let file_churn_1 = FileChurn::new("path/to/file.txt".into(), 50, NumberSeparator::Plain);
let file_churn_2 = FileChurn::new("file_2.txt".into(), 30, NumberSeparator::Plain);

let churn_info = ChurnInfo {
file_churns: vec![file_churn_1, file_churn_2],
churn_pool_size: 5,
info_color: DynColors::Rgb(255, 0, 0),
};

assert!(churn_info.value().contains(
&"\u{2026}/to/file.txt 50"
.color(DynColors::Rgb(255, 0, 0))
.to_string()
));

assert!(churn_info
.value()
.contains(&"file_2.txt 30".color(DynColors::Rgb(255, 0, 0)).to_string()));
}

#[test]
fn test_truncate_file_path() {
assert_eq!(shorten_file_path("path/to/file.txt", 3), "path/to/file.txt");
assert_eq!(shorten_file_path("another/file.txt", 2), "another/file.txt");
assert_eq!(shorten_file_path("file.txt", 1), "file.txt");
assert_eq!(
shorten_file_path("path/to/file.txt", 2),
"\u{2026}/to/file.txt"
);
assert_eq!(
shorten_file_path("another/file.txt", 1),
"\u{2026}/file.txt"
);
assert_eq!(shorten_file_path("file.txt", 0), "file.txt");
}
}
Loading