Skip to content

Commit 7aa534d

Browse files
committed
feat: add output CLI argument with enum for format control
1 parent d17e2ed commit 7aa534d

File tree

10 files changed

+301
-97
lines changed

10 files changed

+301
-97
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ clap = { version = "4.4.6", features = ["derive"] }
1919
log = "0.4.20"
2020
pretty_env_logger = "0.5.0"
2121

22+
# For fancy output.
23+
ansi_term = "0.12.1"
24+
2225
# For error handling.
2326
anyhow = "1.0.89"
2427

end-to-end-tests/features/steps/then.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ def assert_git_history_is_not_clean(context):
2222
result = execute_clean_git_history(context)
2323

2424
# Then
25-
assert_no_output(result)
25+
# Output now goes to stdout instead of stderr
26+
assert result.stdout != "", "Expected standard output to contain error information.\n" + \
27+
f"Standard output = {result.stdout.encode()}.\n"
28+
assert_no_errors(result)
2629
assert_command_unsuccessful(result)
2730
return result
2831

src/commits/commit/mod.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,63 @@
1-
use git2::{Oid, Repository};
1+
use crate::linting_results::CommitError;
22

3-
pub(super) struct Commit {
4-
commit_hash: git2::Oid,
3+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4+
pub struct Commit {
5+
pub hash: String,
6+
pub message: String,
57
number_of_parents: usize,
68
}
79

810
impl Commit {
9-
pub(super) fn from_git(repository: &Repository, oid: Oid) -> Result<Self, git2::Error> {
10-
let commit = repository.find_commit(oid)?;
11+
pub(super) fn from_git(commit: &git2::Commit) -> Commit {
1112
let number_of_parents = commit.parents().len();
13+
let message = match commit.message().map(|m| m.to_string()) {
14+
Some(message) => {
15+
trace!(
16+
"Found the commit message {message:?} for the commit with the hash '{}'.",
17+
commit.id()
18+
);
19+
message
20+
}
21+
None => {
22+
warn!(
23+
"Can not find commit message for the commit with the hash '{}'.",
24+
commit.id()
25+
);
26+
String::new()
27+
}
28+
};
29+
1230
debug!(
1331
"The commit with the hash '{}' has {:?} parents.",
1432
commit.id(),
1533
number_of_parents,
1634
);
1735

18-
Ok(Commit {
19-
commit_hash: commit.id(),
36+
Commit {
37+
hash: commit.id().to_string(),
38+
message,
2039
number_of_parents,
21-
})
40+
}
2241
}
2342

2443
pub(super) fn is_merge_commit(&self) -> bool {
2544
let is_merge_commit = self.number_of_parents > 1;
2645

2746
if is_merge_commit {
28-
warn!("Commit {:?} is a merge commit.", self.commit_hash);
47+
warn!("Commit {:?} is a merge commit.", self.hash);
2948
}
3049

3150
is_merge_commit
3251
}
33-
}
3452

35-
#[cfg(test)]
36-
mod tests;
53+
/// Lint this commit and return any linting errors found.
54+
pub(crate) fn lint(&self) -> Vec<CommitError> {
55+
let mut errors = Vec::new();
56+
57+
if self.is_merge_commit() {
58+
errors.push(CommitError::MergeCommit);
59+
}
60+
61+
errors
62+
}
63+
}

src/commits/commit/tests/mod.rs

Lines changed: 0 additions & 52 deletions
This file was deleted.

src/commits/mod.rs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
use std::collections::VecDeque;
1+
use std::collections::{HashMap, VecDeque};
22

33
use anyhow::{bail, Context, Result};
44
use git2::{Oid, Repository, Revwalk};
55

6-
use crate::commits::commit::Commit;
6+
use crate::linting_results::{
7+
CommitError, CommitErrors, CommitsError, CommitsErrors, LintingResults,
8+
};
79

8-
mod commit;
10+
pub mod commit;
11+
pub use commit::Commit;
912

1013
/// A representation of a range of commits within a Git repository, which can have various lints performed upon it after construction.
1114
pub struct Commits {
@@ -20,14 +23,52 @@ impl Commits {
2023
get_commits_till_head_from_oid(repository, oid)
2124
}
2225

23-
/// A lint that can be performed on the range of commits, which returns true if any of the
24-
/// commits are merge commits, i.e. has multiple parents.
25-
pub fn contains_merge_commits(&self) -> bool {
26-
self.commits.iter().any(|commit| commit.is_merge_commit())
27-
}
26+
/// Lint all commits and return the linting results if any issues are found.
27+
pub fn lint(&self, max_commits: Option<usize>) -> Option<LintingResults> {
28+
let mut commit_errors: HashMap<Commit, Vec<CommitError>> = HashMap::new();
29+
30+
// Check each commit for linting errors
31+
for commit in self.commits.iter().cloned() {
32+
let errors = commit.lint();
33+
34+
if !errors.is_empty() {
35+
warn!(
36+
"Found {} linting errors for the commit {:?}.",
37+
errors.len(),
38+
commit.hash
39+
);
40+
commit_errors.insert(commit, errors);
41+
}
42+
}
2843

29-
pub fn len(&self) -> usize {
30-
self.commits.len()
44+
// Check for aggregate errors
45+
let commits_errors = max_commits.and_then(|max| {
46+
if self.commits.len() > max {
47+
Some(vec![CommitsError::MaxCommitsExceeded {
48+
max_commits: max,
49+
actual_commits: self.commits.len(),
50+
}])
51+
} else {
52+
None
53+
}
54+
});
55+
56+
// Return None if no issues found, otherwise build LintingResults
57+
match (&commit_errors.is_empty(), &commits_errors) {
58+
(true, None) => None,
59+
(true, Some(ce)) => Some(LintingResults {
60+
commit_errors: None,
61+
commits_errors: Some(CommitsErrors::new(ce.clone())),
62+
}),
63+
(false, None) => Some(LintingResults {
64+
commit_errors: Some(CommitErrors::new(self.commits.clone(), commit_errors)),
65+
commits_errors: None,
66+
}),
67+
(false, Some(ce)) => Some(LintingResults {
68+
commit_errors: Some(CommitErrors::new(self.commits.clone(), commit_errors)),
69+
commits_errors: Some(CommitsErrors::new(ce.clone())),
70+
}),
71+
}
3172
}
3273
}
3374

@@ -49,14 +90,17 @@ fn get_commits_till_head_from_oid(
4990
let revwalker = get_revwalker(repository, from_commit_hash)?;
5091
let mut commits = VecDeque::new();
5192

52-
for commit in revwalker {
53-
let oid = commit?;
54-
55-
let commit = Commit::from_git(repository, oid)
56-
.context(format!("Can not find a commit with the hash '{oid}'."))?;
93+
for oid in revwalker {
94+
let oid = oid?;
95+
let commit = repository.find_commit(oid)?;
96+
let commit = Commit::from_git(&commit);
5797
commits.push_front(commit);
5898
}
5999

100+
if commits.is_empty() {
101+
bail!("No Git commits within the provided range.");
102+
}
103+
60104
info!("Found {} commits within the provided range.", commits.len());
61105
Ok(Commits { commits })
62106
}

src/linting_results/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::collections::{HashMap, VecDeque};
2+
3+
use crate::commits::commit::Commit;
4+
5+
mod pretty;
6+
7+
/// The representation of an error that an individual commit can have.
8+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
9+
pub enum CommitError {
10+
/// Commit is a merge commit (has multiple parents).
11+
MergeCommit,
12+
}
13+
14+
/// The representation of an error for the collection of commits as a whole.
15+
#[derive(Debug, Clone, PartialEq, Eq)]
16+
pub enum CommitsError {
17+
/// The number of commits exceeds the maximum allowed.
18+
MaxCommitsExceeded {
19+
max_commits: usize,
20+
actual_commits: usize,
21+
},
22+
}
23+
24+
/// Per-commit linting errors.
25+
pub struct CommitErrors {
26+
pub(crate) order: VecDeque<Commit>,
27+
pub(crate) errors: HashMap<Commit, Vec<CommitError>>,
28+
}
29+
30+
impl CommitErrors {
31+
pub(crate) fn new(order: VecDeque<Commit>, errors: HashMap<Commit, Vec<CommitError>>) -> Self {
32+
CommitErrors { order, errors }
33+
}
34+
}
35+
36+
/// Aggregate linting errors for the commits collection.
37+
pub struct CommitsErrors {
38+
pub(crate) errors: Vec<CommitsError>,
39+
}
40+
41+
impl CommitsErrors {
42+
pub(crate) fn new(errors: Vec<CommitsError>) -> Self {
43+
CommitsErrors { errors }
44+
}
45+
}
46+
47+
/// A representation of all linting errors found in the range of commits.
48+
pub struct LintingResults {
49+
pub commit_errors: Option<CommitErrors>,
50+
pub commits_errors: Option<CommitsErrors>,
51+
}
52+
53+
impl LintingResults {
54+
/// Get a pretty representation of the linting results as a string, suitable as output for a user.
55+
pub fn pretty(&self) -> String {
56+
pretty::print_all(self)
57+
}
58+
}

0 commit comments

Comments
 (0)