Skip to content

Commit

Permalink
feat: breaking change footers (#101)
Browse files Browse the repository at this point in the history
Adds a prompt, and the functionality, for adding a breaking change
footer describing the breaking changes in detail, as described in the
Conventional Commits specification. I decided against using a dash (-)
between "BREAKING" and "CHANGE" as I fear that not every tool supports
it, but it may be configurable in the future.
Also fixes the hint of the commit body prompt having a double comma.

Closes #86
  • Loading branch information
cococonscious authored Oct 16, 2024
1 parent 5114108 commit 7ac730d
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 18 deletions.
Binary file modified meta/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 110 additions & 17 deletions src/lib/answers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,33 @@ fn get_summary(
}
}

/// If there is a referenced issue, we want to return a new string
/// appending it to the body. If not, just give back the body
fn get_amended_body(body: &Option<String>, issue_reference: &Option<String>) -> Option<String> {
let body = match (body, issue_reference) {
(Some(body), Some(issue_reference)) => Some(format!("{body}\n\n{issue_reference}")),
(Some(body), None) => Some(body.into()),
(None, Some(issue_reference)) => Some(issue_reference.to_owned()),
(None, None) => None,
/// Appends the breaking change footer key to the breaking change text if provided.
/// Functions using this are expected to check for the text validity before calling this function.
fn into_breaking_footer(breaking_text: &Option<String>) -> Option<String> {
breaking_text
.as_ref()
.map(|b| format!("BREAKING CHANGE: {b}"))
}

/// Get the body, amending it with issue references and breaking changes if provided
fn get_amended_body(
body: &Option<String>,
issue_reference: &Option<String>,
breaking_text: &Option<String>,
) -> Option<String> {
let body = match (body, issue_reference, breaking_text) {
(Some(body), Some(issue_reference), Some(breaking_text)) => {
Some(format!("{body}\n\n{issue_reference}\n{breaking_text}"))
}
(Some(body), Some(issue_reference), None) => Some(format!("{body}\n\n{issue_reference}")),
(Some(body), None, Some(breaking_text)) => Some(format!("{body}\n\n{breaking_text}")),
(Some(body), None, None) => Some(body.into()),
(None, Some(issue_reference), Some(breaking_text)) => {
Some(format!("{issue_reference}\n{breaking_text}"))
}
(None, Some(issue_reference), None) => Some(issue_reference.to_owned()),
(None, None, Some(breaking_text)) => Some(breaking_text.to_owned()),
(None, None, None) => None,
};
body.map(|b| b.replace_emoji_shortcodes())
}
Expand All @@ -49,6 +68,14 @@ pub struct ExtractedAnswers {
/// Extract the prompt answers into an `ExtractedAnswers`,
/// making it usable for creating a commit
pub fn get_extracted_answers(answers: Answers, config: &Config) -> Result<ExtractedAnswers> {
// The breaking change footer text should never be present if `is_breaking_change` is false, but
// we're checking for it anyway
let breaking_change_footer: Option<String> = if answers.is_breaking_change {
into_breaking_footer(&answers.breaking_change_footer)
} else {
None
};

Ok(ExtractedAnswers {
commit_type: answers.commit_type.clone(),
scope: answers.scope.map(|s| s.replace_emoji_shortcodes()),
Expand All @@ -58,7 +85,11 @@ pub fn get_extracted_answers(answers: Answers, config: &Config) -> Result<Extrac
&answers.commit_type,
&config.commit_types,
)?,
body: get_amended_body(&answers.body, &answers.issue_footer),
body: get_amended_body(
&answers.body,
&answers.issue_footer,
&breaking_change_footer,
),
is_breaking_change: answers.is_breaking_change,
})
}
Expand Down Expand Up @@ -129,27 +160,63 @@ mod tests {
);
}

#[test]
fn test_into_breaking_footer() {
let breaking_text = Some("this is a breaking change".to_string());
assert_eq!(
into_breaking_footer(&breaking_text),
Some("BREAKING CHANGE: this is a breaking change".into())
);

let breaking_text = None;
assert_eq!(into_breaking_footer(&breaking_text), None);
}

#[test]
fn test_get_amended_body() {
let body = Some("i _really_ like badges".to_string());
let issue_reference = Some("closes #1".to_string());
let breaking_text = Some("BREAKING CHANGE: this is a breaking change".to_string());

assert_eq!(
get_amended_body(&body, &issue_reference),
get_amended_body(&body, &issue_reference, &breaking_text),
Some(
"i _really_ like badges\n\ncloses #1\nBREAKING CHANGE: this is a breaking change"
.into()
)
);

assert_eq!(
get_amended_body(&body, &issue_reference, &None),
Some("i _really_ like badges\n\ncloses #1".into())
);

assert_eq!(
get_amended_body(&body, &None),
get_amended_body(&body, &None, &breaking_text),
Some("i _really_ like badges\n\nBREAKING CHANGE: this is a breaking change".into())
);

assert_eq!(
get_amended_body(&body, &None, &None),
Some("i _really_ like badges".into())
);

assert_eq!(
get_amended_body(&None, &issue_reference),
get_amended_body(&None, &issue_reference, &breaking_text),
Some("closes #1\nBREAKING CHANGE: this is a breaking change".into())
);

assert_eq!(
get_amended_body(&None, &issue_reference, &None),
Some("closes #1".into())
);

assert_eq!(get_amended_body(&None, &None), None);
assert_eq!(
get_amended_body(&None, &None, &breaking_text),
Some("BREAKING CHANGE: this is a breaking change".into())
);

assert_eq!(get_amended_body(&None, &None, &None), None);
}

#[test]
Expand All @@ -159,8 +226,9 @@ mod tests {
scope: Some("space".into()),
summary: "add more space".into(),
body: Some("just never enough space!".into()),
is_breaking_change: false,
issue_footer: Some("closes #554".into()),
is_breaking_change: true,
breaking_change_footer: Some("this is a breaking change".into()),
};

let config = Config::new(None).unwrap();
Expand All @@ -172,8 +240,8 @@ mod tests {
commit_type: "feat".into(),
scope: Some("space".into()),
summary: "add more space".into(),
body: Some("just never enough space!\n\ncloses #554".into()),
is_breaking_change: false,
body: Some("just never enough space!\n\ncloses #554\nBREAKING CHANGE: this is a breaking change".into()),
is_breaking_change: true,
}
);

Expand All @@ -189,7 +257,32 @@ mod tests {

assert_eq!(
message,
"feat(space): add more space\n\njust never enough space!\n\ncloses #554"
"feat(space)!: add more space\n\njust never enough space!\n\ncloses #554\nBREAKING CHANGE: this is a breaking change"
);

// Test with no breaking change

let answers = Answers {
commit_type: "feat".into(),
scope: Some("space".into()),
summary: "add more space".into(),
body: Some("just never enough space!".into()),
issue_footer: Some("closes #554".into()),
is_breaking_change: false,
breaking_change_footer: Some("this is a breaking change".into()),
};

let extracted_answers = get_extracted_answers(answers, &config).unwrap();

assert_eq!(
extracted_answers,
ExtractedAnswers {
commit_type: "feat".into(),
scope: Some("space".into()),
summary: "add more space".into(),
body: Some("just never enough space!\n\ncloses #554".into()),
is_breaking_change: false,
}
);
}
}
27 changes: 26 additions & 1 deletion src/lib/questions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ pub fn prompt_summary(msg: String) -> Result<String> {

#[cfg(not(tarpaulin_include))]
pub fn prompt_body() -> Result<Option<String>> {
let help_message = format!("{}, {}", "Use '\\n' for newlines, ", get_skip_hint());
let help_message = format!("{}, {}", "Use '\\n' for newlines", get_skip_hint());

let summary = Text::new("Provide a longer description of the change:")
.with_render_config(get_render_config())
Expand All @@ -205,6 +205,25 @@ pub fn prompt_breaking() -> Result<bool> {
Ok(answer)
}

#[cfg(not(tarpaulin_include))]
pub fn prompt_breaking_text() -> Result<Option<String>> {
let help_message = format!("{}, {}", "Use '\\n' for newlines", get_skip_hint());

let breaking_text = Text::new("Describe the breaking changes in detail:")
.with_render_config(get_render_config())
.with_help_message(help_message.as_str())
.prompt_skippable()?;

if let Some(breaking_text) = breaking_text {
if breaking_text.is_empty() {
return Ok(None);
}
Ok(Some(breaking_text.replace("\\n", "\n")))
} else {
Ok(None)
}
}

#[cfg(not(tarpaulin_include))]
pub fn prompt_issues() -> Result<bool> {
let answer = Confirm::new("Does this change affect any open issues?")
Expand Down Expand Up @@ -234,6 +253,7 @@ pub struct Answers {
pub body: Option<String>,
pub issue_footer: Option<String>,
pub is_breaking_change: bool,
pub breaking_change_footer: Option<String>,
}

/// Create the interactive prompt
Expand All @@ -245,8 +265,12 @@ pub fn create_prompt(last_message: String, config: &Config) -> Result<Answers> {
let body = prompt_body()?;

let mut breaking = false;
let mut breaking_footer: Option<String> = None;
if config.breaking_changes {
breaking = prompt_breaking()?;
if breaking {
breaking_footer = prompt_breaking_text()?;
}
}

let mut issue_footer = None;
Expand All @@ -261,6 +285,7 @@ pub fn create_prompt(last_message: String, config: &Config) -> Result<Answers> {
body,
issue_footer,
is_breaking_change: breaking,
breaking_change_footer: breaking_footer,
})
}

Expand Down

0 comments on commit 7ac730d

Please sign in to comment.