feat(references): add ticket reference footers
Run checks and build archives / build (linux-x86_64) (push) Has been cancelled
Run checks and build archives / build (windows-x86_64) (push) Has been cancelled
Run checks and build archives / build (linux-aarch64) (push) Has been cancelled
Run checks and build archives / coverage-and-sonar (push) Successful in 5m29s
Run checks and build archives / build (linux-x86_64) (push) Has been cancelled
Run checks and build archives / build (windows-x86_64) (push) Has been cancelled
Run checks and build archives / build (linux-aarch64) (push) Has been cancelled
Run checks and build archives / coverage-and-sonar (push) Successful in 5m29s
Refs: #4
This commit is contained in:
+59
-1
@@ -8,7 +8,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, Scope},
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, References, Scope},
|
||||
error::Error,
|
||||
prompts::prompter::Prompter,
|
||||
};
|
||||
@@ -20,6 +20,7 @@ enum MockResponse {
|
||||
Scope(Scope),
|
||||
Description(Description),
|
||||
BreakingChange(BreakingChange),
|
||||
References(References),
|
||||
Body(Body),
|
||||
Confirm(bool),
|
||||
Error(Error),
|
||||
@@ -81,6 +82,15 @@ impl MockPrompts {
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the mock to return specific references
|
||||
pub fn with_references(self, references: References) -> Self {
|
||||
self.responses
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(MockResponse::References(references));
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the mock to return a specific body response
|
||||
pub fn with_body(self, body: Body) -> Self {
|
||||
self.responses
|
||||
@@ -140,6 +150,14 @@ impl MockPrompts {
|
||||
.contains(&"input_breaking_change".to_string())
|
||||
}
|
||||
|
||||
/// Check if input_references was called
|
||||
pub fn was_references_called(&self) -> bool {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&"input_references".to_string())
|
||||
}
|
||||
|
||||
/// Check if confirm_apply was called
|
||||
pub fn was_confirm_called(&self) -> bool {
|
||||
self.prompts_called
|
||||
@@ -207,6 +225,18 @@ impl Prompter for MockPrompts {
|
||||
}
|
||||
}
|
||||
|
||||
fn input_references(&self) -> Result<References, Error> {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("input_references".to_string());
|
||||
match self.responses.lock().unwrap().remove(0) {
|
||||
MockResponse::References(r) => Ok(r),
|
||||
MockResponse::Error(e) => Err(e),
|
||||
_ => panic!("MockPrompts: Expected References response, got different type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_body(&self) -> Result<Body, Error> {
|
||||
self.prompts_called
|
||||
.lock()
|
||||
@@ -336,6 +366,34 @@ mod tests {
|
||||
assert!(mock.emitted_messages().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references() {
|
||||
let refs = References::from("#123, #456");
|
||||
let mock = MockPrompts::new().with_references(refs.clone());
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), refs);
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references_default() {
|
||||
let mock = MockPrompts::new().with_references(References::default());
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), References::default());
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_references_error() {
|
||||
let mock = MockPrompts::new().with_error(Error::Cancelled);
|
||||
let result = mock.input_references();
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), Error::Cancelled));
|
||||
assert!(mock.was_references_called());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_input_breaking_change_no() {
|
||||
let mock = MockPrompts::new().with_breaking_change(BreakingChange::No);
|
||||
|
||||
+17
-1
@@ -9,7 +9,7 @@ use inquire::{Confirm, Text};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, Scope},
|
||||
commit::types::{Body, BreakingChange, CommitType, Description, References, Scope},
|
||||
error::Error,
|
||||
};
|
||||
|
||||
@@ -33,6 +33,9 @@ pub trait Prompter {
|
||||
/// Prompt the user to optionally add a free-form body via an external editor
|
||||
fn input_body(&self) -> Result<Body, Error>;
|
||||
|
||||
/// Prompt the user to optionally add comma-separated ticket references
|
||||
fn input_references(&self) -> Result<References, Error>;
|
||||
|
||||
/// Prompt the user to confirm applying the commit message
|
||||
fn confirm_apply(&self, message: &str) -> Result<bool, Error>;
|
||||
|
||||
@@ -91,6 +94,19 @@ impl Prompter for RealPrompts {
|
||||
}
|
||||
}
|
||||
|
||||
fn input_references(&self) -> Result<References, Error> {
|
||||
let answer = inquire::Text::new("Enter comma-separated references (optional):")
|
||||
.with_help_message("References are optional. If provided, will become footer(s) in the commit message. References must be comma-separated.")
|
||||
.with_placeholder("Leave empty if no references")
|
||||
.prompt_skippable()
|
||||
.map_err(|_| Error::Cancelled)?;
|
||||
match answer {
|
||||
None => Ok(References::default()),
|
||||
Some(s) if s.trim().is_empty() => Ok(References::default()),
|
||||
Some(s) => Ok(References::from(s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_description(&self) -> Result<Description, Error> {
|
||||
loop {
|
||||
let answer = Text::new("Enter description (required):")
|
||||
|
||||
+42
-4
@@ -6,7 +6,7 @@
|
||||
use crate::{
|
||||
commit::types::{
|
||||
Body, BreakingChange, CommitMessageError, CommitType, ConventionalCommit, Description,
|
||||
Scope,
|
||||
References, Scope,
|
||||
},
|
||||
error::Error,
|
||||
jj::JjExecutor,
|
||||
@@ -65,8 +65,16 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
let scope = self.scope_input()?;
|
||||
let description = self.description_input()?;
|
||||
let breaking_change = self.breaking_change_input()?;
|
||||
let references = self.references_input()?;
|
||||
let body = self.body_input()?;
|
||||
match self.preview_and_confirm(commit_type, scope, description, breaking_change, body) {
|
||||
match self.preview_and_confirm(
|
||||
commit_type,
|
||||
scope,
|
||||
description,
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
) {
|
||||
Ok(conventional_commit) => {
|
||||
self.executor
|
||||
.describe(revset, &conventional_commit.to_string())
|
||||
@@ -113,6 +121,11 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
self.prompts.input_breaking_change()
|
||||
}
|
||||
|
||||
/// Prompt user for references
|
||||
fn references_input(&self) -> Result<References, Error> {
|
||||
self.prompts.input_references()
|
||||
}
|
||||
|
||||
/// Prompt user to optionally add a free-form body via an external editor
|
||||
fn body_input(&self) -> Result<Body, Error> {
|
||||
self.prompts.input_body()
|
||||
@@ -129,6 +142,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
description: Description,
|
||||
breaking_change: BreakingChange,
|
||||
body: Body,
|
||||
references: References,
|
||||
) -> Result<ConventionalCommit, Error> {
|
||||
// Format the message for preview
|
||||
let message = ConventionalCommit::format_preview(
|
||||
@@ -137,6 +151,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
&description,
|
||||
&breaking_change,
|
||||
&body,
|
||||
&references,
|
||||
);
|
||||
|
||||
// Try to build the conventional commit (this validates the 72-char limit)
|
||||
@@ -146,6 +161,7 @@ impl<J: JjExecutor, P: Prompter> CommitWorkflow<J, P> {
|
||||
description.clone(),
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
) {
|
||||
Ok(cc) => cc,
|
||||
Err(CommitMessageError::FirstLineTooLong { actual, max }) => {
|
||||
@@ -276,8 +292,15 @@ mod tests {
|
||||
let description = Description::parse("test description").unwrap();
|
||||
let breaking_change = BreakingChange::No;
|
||||
let body = Body::default();
|
||||
let result =
|
||||
workflow.preview_and_confirm(commit_type, scope, description, breaking_change, body);
|
||||
let references = References::default();
|
||||
let result = workflow.preview_and_confirm(
|
||||
commit_type,
|
||||
scope,
|
||||
description,
|
||||
breaking_change,
|
||||
body,
|
||||
references,
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
@@ -322,6 +345,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add new feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -355,6 +379,7 @@ mod tests {
|
||||
.with_scope(Scope::parse("api").unwrap())
|
||||
.with_description(Description::parse("fix bug").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(false); // User cancels at confirmation
|
||||
|
||||
@@ -379,11 +404,13 @@ mod tests {
|
||||
.with_scope(Scope::parse("very-long-scope-name").unwrap())
|
||||
.with_description(Description::parse("a".repeat(45)).unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
// Second iteration: short enough to succeed
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("short description").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -478,6 +505,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -501,6 +529,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -519,6 +548,7 @@ mod tests {
|
||||
.with_scope(Scope::parse("api").unwrap())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -566,6 +596,7 @@ mod tests {
|
||||
Description::parse("remove old API").unwrap(),
|
||||
BreakingChange::Yes,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -593,6 +624,7 @@ mod tests {
|
||||
Description::parse("drop legacy API").unwrap(),
|
||||
breaking_change,
|
||||
Body::default(),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -623,6 +655,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("remove old API").unwrap())
|
||||
.with_breaking_change(BreakingChange::Yes)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -665,6 +698,7 @@ mod tests {
|
||||
Description::parse("add feature").unwrap(),
|
||||
BreakingChange::No,
|
||||
Body::from("This explains the change."),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -692,6 +726,7 @@ mod tests {
|
||||
Description::parse("drop legacy API").unwrap(),
|
||||
"removes legacy endpoint".into(),
|
||||
Body::from("The endpoint was deprecated in v2."),
|
||||
References::default(),
|
||||
);
|
||||
|
||||
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
|
||||
@@ -718,6 +753,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::from("This explains the change."))
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -750,6 +786,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("fix crash").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
@@ -803,6 +840,7 @@ mod tests {
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add feature").unwrap())
|
||||
.with_breaking_change(BreakingChange::No)
|
||||
.with_references(References::default())
|
||||
.with_body(Body::default())
|
||||
.with_confirm(true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user