Compare commits

..

3 Commits

Author SHA1 Message Date
e28f1fdbe1 feat(errors): preserve jj-emitted errors when loading config
Some checks failed
Publish Docker Images / coverage-and-sonar (push) Failing after 16m33s
2026-04-05 15:56:24 +02:00
10d52d3e72 refactor(BreakingChange): rename method ignore to is_absent
Method `ignore` did not carry its meaning well by the way it is named.
This commit renames it to `is_absent` to clearly state this method
returns whether we have a breaking change.
2026-04-05 15:56:24 +02:00
7149337353 refactor(workflow): remove unnecessary async declarations 2026-04-05 15:56:24 +02:00
2 changed files with 56 additions and 16 deletions

View File

@@ -61,11 +61,7 @@ impl Scope {
/// Returns the visible length of the header segment /// Returns the visible length of the header segment
pub fn header_segment_len(&self) -> usize { pub fn header_segment_len(&self) -> usize {
if self.is_empty() { self.header_segment().chars().count()
0
} else {
self.0.chars().count() + 2
}
} }
} }

View File

@@ -17,7 +17,7 @@ use crate::{
/// ///
/// Implement this trait to supply a custom front-end (interactive TUI, mock, /// Implement this trait to supply a custom front-end (interactive TUI, mock,
/// headless, etc.) to [`CommitWorkflow`](super::CommitWorkflow). /// headless, etc.) to [`CommitWorkflow`](super::CommitWorkflow).
pub trait Prompter { pub trait Prompter: Send + Sync {
/// Prompt the user to select a commit type /// Prompt the user to select a commit type
fn select_commit_type(&self) -> Result<CommitType, Error>; fn select_commit_type(&self) -> Result<CommitType, Error>;
@@ -66,32 +66,67 @@ pub struct RealPrompts;
impl Prompter for RealPrompts { impl Prompter for RealPrompts {
fn select_commit_type(&self) -> Result<CommitType, Error> { fn select_commit_type(&self) -> Result<CommitType, Error> {
inquire::Select::new("Select commit type:", CommitType::all().to_vec()) use inquire::Select;
let options: Vec<_> = CommitType::all()
.iter()
.map(|ct| format!("{}: {}", ct, ct.description()))
.collect();
let answer = Select::new("Select commit type:", options)
.with_page_size(11) .with_page_size(11)
.with_help_message( .with_help_message(
"Use arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.", "Use arrow keys to navigate, Enter to select. See https://www.conventionalcommits.org/ for details.",
) )
.with_formatter(&|option| format!("{}: {}", option.value.as_str(), option.value.description()))
.prompt() .prompt()
.map_err(|_| Error::Cancelled) .map_err(|_| Error::Cancelled)?;
// Extract the commit type from the selected option
let selected_type = answer
.split(':')
.next()
.ok_or_else(|| Error::JjOperation {
context: "Failed to parse selected commit type".to_string(),
})?
.trim();
CommitType::all()
.iter()
.find(|ct| ct.as_str() == selected_type)
.copied()
.ok_or_else(|| Error::JjOperation {
context: format!("Unknown commit type: {}", selected_type),
})
} }
fn input_scope(&self) -> Result<Scope, Error> { fn input_scope(&self) -> Result<Scope, Error> {
let answer = inquire::Text::new("Enter scope (optional):") use inquire::Text;
let answer = Text::new("Enter scope (optional):")
.with_help_message( .with_help_message(
"Scope is optional. If provided, it should be a noun representing the section of code affected (e.g., 'api', 'ui', 'config'). Max 30 characters.", "Scope is optional. If provided, it should be a noun representing the section of code affected (e.g., 'api', 'ui', 'config'). Max 30 characters.",
) )
.with_placeholder("Leave empty if no scope") .with_placeholder("Leave empty if no scope")
.prompt_skippable() .prompt_skippable()
.map_err(|_| Error::Cancelled)?; .map_err(|_| Error::Cancelled)?;
match answer {
Some(s) if s.trim().is_empty() => Ok(Scope::empty()), // Empty input is valid (no scope)
Some(s) => Scope::parse(s.trim()).map_err(|e| Error::InvalidScope(e.to_string())), let answer_str = match answer {
None => Ok(Scope::empty()), Some(s) => s,
None => return Ok(Scope::empty()),
};
if answer_str.trim().is_empty() {
return Ok(Scope::empty());
} }
// Parse and validate the scope
Scope::parse(answer_str.trim()).map_err(|e| Error::InvalidScope(e.to_string()))
} }
fn input_description(&self) -> Result<Description, Error> { fn input_description(&self) -> Result<Description, Error> {
use inquire::Text;
loop { loop {
let answer = Text::new("Enter description (required):") let answer = Text::new("Enter description (required):")
.with_help_message( .with_help_message(
@@ -145,10 +180,13 @@ impl Prompter for RealPrompts {
} }
fn input_body(&self) -> Result<Body, Error> { fn input_body(&self) -> Result<Body, Error> {
use inquire::Editor;
let wants_body = Confirm::new("Add a body?") let wants_body = Confirm::new("Add a body?")
.with_default(false) .with_default(false)
.prompt() .prompt()
.map_err(|_| Error::Cancelled)?; .map_err(|_| Error::Cancelled)?;
if !wants_body { if !wants_body {
return Ok(Body::default()); return Ok(Body::default());
} }
@@ -158,11 +196,12 @@ JJ: Body (optional). Markdown is supported.\n\
JJ: Wrap prose lines at 72 characters where possible.\n\ JJ: Wrap prose lines at 72 characters where possible.\n\
JJ: Lines starting with \"JJ:\" will be removed.\n"; JJ: Lines starting with \"JJ:\" will be removed.\n";
let raw = inquire::Editor::new("Body:") let raw = Editor::new("Body:")
.with_predefined_text(template) .with_predefined_text(template)
.with_file_extension(".md") .with_file_extension(".md")
.prompt() .prompt()
.map_err(|_| Error::Cancelled)?; .map_err(|_| Error::Cancelled)?;
let stripped: String = raw let stripped: String = raw
.lines() .lines()
.filter(|line| !line.starts_with("JJ:")) .filter(|line| !line.starts_with("JJ:"))
@@ -173,11 +212,16 @@ JJ: Lines starting with \"JJ:\" will be removed.\n";
} }
fn confirm_apply(&self, message: &str) -> Result<bool, Error> { fn confirm_apply(&self, message: &str) -> Result<bool, Error> {
use inquire::Confirm;
// Show preview
println!( println!(
"\n📝 Commit Message Preview:\n{}\n", "\n📝 Commit Message Preview:\n{}\n",
format_message_box(message) format_message_box(message)
); );
inquire::Confirm::new("Apply this commit message?")
// Get confirmation
Confirm::new("Apply this commit message?")
.with_default(true) .with_default(true)
.with_help_message("Select 'No' to cancel and start over") .with_help_message("Select 'No' to cancel and start over")
.prompt() .prompt()