From 401d96cda98b2102c9e1cbcc6dee9d3d9cbf881b Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Thu, 10 Aug 2023 08:54:02 -0230 Subject: Add Modified Line exec command feature This adds an optional feature, where a exec command is injected with the provided script after every modified line. --- CHANGELOG.md | 5 + README.md | 47 +++++++++ readme/customization.md | 24 +++-- src/config/src/lib.rs | 31 +++++- src/config/src/theme.rs | 4 +- src/config/src/utils/get_bool.rs | 6 +- src/config/src/utils/get_string.rs | 4 +- src/config/src/utils/get_unsigned_integer.rs | 8 +- src/config/src/utils/mod.rs | 2 +- src/core/src/application.rs | 45 +++++++- src/todo_file/src/lib.rs | 148 ++++++++++++++++++++++++++- src/todo_file/src/line.rs | 99 +++++++++++++----- src/todo_file/src/todo_file_options.rs | 39 ++++++- 13 files changed, 396 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb660b4..c1ee4b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). +## [Unreleased] +### Added +- Post modified line exec command ([#888](https://github.com/MitMaro/git-interactive-rebase-tool/pull/888)) + + ## [2.3.0] - 2023-07-19 ### Added - Support for update-ref action ([#801](https://github.com/MitMaro/git-interactive-rebase-tool/pull/801)) diff --git a/README.md b/README.md index f860bf9..a10cd69 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,53 @@ Need to do something in your Git editor? Quickly shell out to your editor, make ![Shell out to editor](/docs/assets/images/girt-external-editor.gif?raw=true) +### Advanced Features + +#### Modified line exec command + +This optional feature allows for the injection of an `exec` action after modified lines, where modified is determined as a changed action, command, or reference. This can be used to amend commits to update references in the commit message or run a test suite only on modified commits. + +To enable this option, set the `interactive-rebase-tool.postModifiedLineExecCommand` option, providing an executable or script. + +```shell +git config --global interactive-rebase-tool.postModifiedLineExecCommand "/path/to/global/script" +``` + +Or using repository-specific configuration, for targeted scripts. + +```shell +git config --global interactive-rebase-tool.postModifiedLineExecCommand "/path/to/repo/script" +``` + +The first argument provided to the script will always be the action performed. Then, depending on the action, the script will be provided a different set of arguments. + +For `drop`, `fixup`, `edit`, `pick`, `reword` and `squash` actions, the script will additionally receive the original commit hash, for `exec` the original and new commands are provided, and for `label`, `reset`, `merge`, and `update-ref` the original label/reference and new label/reference are provided. + +Full example of a resulting rebase todo file, assuming that `interactive-rebase-tool.postModifiedLineExecCommand` was set to `script.sh`. + +``` +# original line: label onto +label new-onto +exec script.sh "label" "onto" "new-onto" + +# original line: reset onto +reset new-onto +exec script.sh "reset" "onto" "new-onto" + +pick a12345 My feature +# original line: pick b12345 My change +squash b12345 My change +exec script.sh "squash" "b12345" + +# original line: label branch +label branch +exec script.sh "label" "branch" "new-branch" + +# original line: exec command +exec new-command +exec script.sh "exec" "command" "new-command" +``` + ## Setup ### Most systems diff --git a/readme/customization.md b/readme/customization.md index 06d667e..3f42661 100644 --- a/readme/customization.md +++ b/readme/customization.md @@ -39,17 +39,18 @@ Some values from your Git Config are directly used by this application. ## General -| Key | Default | Type | Description | -|----------------------------|---------|---------|---------------------------------------------------------------------------------------------| -| `autoSelectNext` | false | bool | If true, auto select the next line after action modification | -| `diffIgnoreBlankLines` | none | String¹ | If to ignore blank lines during diff. | -| `diffIgnoreWhitespace` | none | String¹ | If and how to ignore whitespace during diff. | -| `diffShowWhitespace` | both | String² | If and how to show whitespace during diff. | -| `diffSpaceSymbol` | · | String | The visible symbol for the space character. Only used when `diffShowWhitespace` is enabled. | -| `diffTabSymbol` | → | String | The visible symbol for the tab character. Only used when `diffShowWhitespace` is enabled. | -| `diffTabWidth` | 4 | Integer | The width of the tab character | -| `undoLimit` | 5000 | Integer | Number of undo operations to store. | -| `verticalSpacingCharacter` | ~ | String | Vertical spacing character. Can be set to an empty string. | +| Key | Default | Type | Description | +|-------------------------------|---------|---------|---------------------------------------------------------------------------------------------| +| `autoSelectNext` | false | bool | If true, auto select the next line after action modification | +| `diffIgnoreBlankLines` | none | String¹ | If to ignore blank lines during diff. | +| `diffIgnoreWhitespace` | none | String¹ | If and how to ignore whitespace during diff. | +| `diffShowWhitespace` | both | String² | If and how to show whitespace during diff. | +| `diffSpaceSymbol` | · | String | The visible symbol for the space character. Only used when `diffShowWhitespace` is enabled. | +| `diffTabSymbol` | → | String | The visible symbol for the tab character. Only used when `diffShowWhitespace` is enabled. | +| `diffTabWidth` | 4 | Integer | The width of the tab character | +| `undoLimit` | 5000 | Integer | Number of undo operations to store. | +| `postModifiedLineExecCommand` | | String | Exec command to attach to modified lines. See [modified line exec command] for details. | +| `verticalSpacingCharacter` | ~ | String | Vertical spacing character. Can be set to an empty string. | ¹ Ignore whitespace can be: - `change` to ignore changed whitespace in diffs, same as the [`--ignore-space-change`][diffIgnoreSpaceChange] flag @@ -62,6 +63,7 @@ Some values from your Git Config are directly used by this application. - `true`, `on` or `both` to show both leading and trailing whitespace - `false`, `off`, `none` to show no whitespace +[modified line exec command]:../README.md#modified-line-exec-command [diffIgnoreSpaceChange]:https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---ignore-space-change [diffIgnoreAllSpace]:https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---ignore-all-space diff --git a/src/config/src/lib.rs b/src/config/src/lib.rs index 3ee2b66..6f036a8 100644 --- a/src/config/src/lib.rs +++ b/src/config/src/lib.rs @@ -162,7 +162,10 @@ pub use self::{ key_bindings::KeyBindings, theme::Theme, }; -use crate::errors::{ConfigError, ConfigErrorCause}; +use crate::{ + errors::{ConfigError, ConfigErrorCause}, + utils::get_optional_string, +}; const DEFAULT_SPACE_SYMBOL: &str = "\u{b7}"; // · const DEFAULT_TAB_SYMBOL: &str = "\u{2192}"; // → @@ -185,6 +188,8 @@ pub struct Config { pub diff_tab_symbol: String, /// The display width of the tab character. pub diff_tab_width: u32, + /// If set, automatically add an exec line with the command after every modified line + pub post_modified_line_exec_command: Option, /// The maximum number of undo steps. pub undo_limit: u32, /// Configuration options loaded directly from Git. @@ -221,6 +226,10 @@ impl Config { diff_tab_symbol: get_string(git_config, "interactive-rebase-tool.diffTabSymbol", DEFAULT_TAB_SYMBOL)?, diff_tab_width: get_unsigned_integer(git_config, "interactive-rebase-tool.diffTabWidth", 4)?, undo_limit: get_unsigned_integer(git_config, "interactive-rebase-tool.undoLimit", 5000)?, + post_modified_line_exec_command: get_optional_string( + git_config, + "interactive-rebase-tool.postModifiedLineExecCommand", + )?, git: GitConfig::new_with_config(git_config)?, key_bindings: KeyBindings::new_with_config(git_config)?, theme: Theme::new_with_config(git_config)?, @@ -435,7 +444,6 @@ mod tests { #[case::diff_tab_width("diffTabWidth", "42", 42, |config: Config| config.diff_tab_width)] #[case::diff_tab_symbol_default("diffTabSymbol", "", String::from("→"), |config: Config| config.diff_tab_symbol)] #[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)] - #[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)] #[case::diff_space_symbol_default( "diffSpaceSymbol", "", @@ -444,8 +452,20 @@ mod tests { ] #[case::diff_space_symbol("diffSpaceSymbol", "-", String::from("-"), |config: Config| config.diff_space_symbol)] #[case::undo_limit_default("undoLimit", "", 5000, |config: Config| config.undo_limit)] - #[case::undo_limit_default("undoLimit", "42", 42, |config: Config| config.undo_limit)] - pub(crate) fn theme_color( + #[case::undo_limit("undoLimit", "42", 42, |config: Config| config.undo_limit)] + #[case::post_modified_line_exec_command( + "postModifiedLineExecCommand", + "command", + Some(String::from("command")), + |config: Config| config.post_modified_line_exec_command + )] + #[case::post_modified_line_exec_command_default( + "postModifiedLineExecCommand", + "", + None, + |config: Config| config.post_modified_line_exec_command + )] + pub(crate) fn config_test( #[case] config_name: &str, #[case] config_value: &str, #[case] expected: T, @@ -497,9 +517,10 @@ mod tests { #[rstest] #[case::diff_tab_symbol("diffIgnoreWhitespace")] - #[case::diff_tab_symbol("diffShowWhitespace")] + #[case::diff_show_whitespace("diffShowWhitespace")] #[case::diff_tab_symbol("diffTabSymbol")] #[case::diff_space_symbol("diffSpaceSymbol")] + #[case::post_modified_line_exec_command("postModifiedLineExecCommand")] fn value_parsing_invalid_utf(#[case] config_name: &str) { with_git_config( &[ diff --git a/src/config/src/theme.rs b/src/config/src/theme.rs index 78352f4..c80a4ff 100644 --- a/src/config/src/theme.rs +++ b/src/config/src/theme.rs @@ -2,13 +2,13 @@ use git::Config; use crate::{ errors::ConfigError, - utils::{_get_string, get_string}, + utils::{get_optional_string, get_string}, Color, ConfigErrorCause, }; fn get_color(config: Option<&Config>, name: &str, default: Color) -> Result { - if let Some(value) = _get_string(config, name)? { + if let Some(value) = get_optional_string(config, name)? { Color::try_from(value.to_lowercase().as_str()).map_err(|invalid_color_error| { ConfigError::new( name, diff --git a/src/config/src/utils/get_bool.rs b/src/config/src/utils/get_bool.rs index 9dd7dc8..536adea 100644 --- a/src/config/src/utils/get_bool.rs +++ b/src/config/src/utils/get_bool.rs @@ -1,6 +1,6 @@ use git::{Config, ErrorCode}; -use crate::{utils::_get_string, ConfigError, ConfigErrorCause}; +use crate::{utils::get_optional_string, ConfigError, ConfigErrorCause}; pub(crate) fn get_bool(config: Option<&Config>, name: &str, default: bool) -> Result { if let Some(cfg) = config { @@ -10,14 +10,14 @@ pub(crate) fn get_bool(config: Option<&Config>, name: &str, default: bool) -> Re Err(e) if e.message().contains("failed to parse") => { Err(ConfigError::new_with_optional_input( name, - _get_string(config, name).ok().flatten(), + get_optional_string(config, name).ok().flatten(), ConfigErrorCause::InvalidBoolean, )) }, Err(e) => { Err(ConfigError::new_with_optional_input( name, - _get_string(config, name).ok().flatten(), + get_optional_string(config, name).ok().flatten(), ConfigErrorCause::UnknownError(String::from(e.message())), )) }, diff --git a/src/config/src/utils/get_string.rs b/src/config/src/utils/get_string.rs index 0416323..dd43fed 100644 --- a/src/config/src/utils/get_string.rs +++ b/src/config/src/utils/get_string.rs @@ -2,7 +2,7 @@ use git::{Config, ErrorCode}; use crate::{ConfigError, ConfigErrorCause}; -pub(crate) fn _get_string(config: Option<&Config>, name: &str) -> Result, ConfigError> { +pub(crate) fn get_optional_string(config: Option<&Config>, name: &str) -> Result, ConfigError> { let Some(cfg) = config else { return Ok(None); @@ -24,7 +24,7 @@ pub(crate) fn _get_string(config: Option<&Config>, name: &str) -> Result, name: &str, default: &str) -> Result { - Ok(_get_string(config, name)?.unwrap_or_else(|| String::from(default))) + Ok(get_optional_string(config, name)?.unwrap_or_else(|| String::from(default))) } #[cfg(test)] diff --git a/src/config/src/utils/get_unsigned_integer.rs b/src/config/src/utils/get_unsigned_integer.rs index de1a70a..25dfa61 100644 --- a/src/config/src/utils/get_unsigned_integer.rs +++ b/src/config/src/utils/get_unsigned_integer.rs @@ -1,6 +1,6 @@ use git::{Config, ErrorCode}; -use crate::{utils::_get_string, ConfigError, ConfigErrorCause}; +use crate::{utils::get_optional_string, ConfigError, ConfigErrorCause}; pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default: u32) -> Result { if let Some(cfg) = config { @@ -9,7 +9,7 @@ pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default: v.try_into().map_err(|_| { ConfigError::new_with_optional_input( name, - _get_string(config, name).ok().flatten(), + get_optional_string(config, name).ok().flatten(), ConfigErrorCause::InvalidUnsignedInteger, ) }) @@ -18,14 +18,14 @@ pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default: Err(e) if e.message().contains("failed to parse") => { Err(ConfigError::new_with_optional_input( name, - _get_string(config, name).ok().flatten(), + get_optional_string(config, name).ok().flatten(), ConfigErrorCause::InvalidUnsignedInteger, )) }, Err(e) => { Err(ConfigError::new_with_optional_input( name, - _get_string(config, name).ok().flatten(), + get_optional_string(config, name).ok().flatten(), ConfigErrorCause::UnknownError(String::from(e.message())), )) }, diff --git a/src/config/src/utils/mod.rs b/src/config/src/utils/mod.rs index c694c94..f08f95d 100644 --- a/src/config/src/utils/mod.rs +++ b/src/config/src/utils/mod.rs @@ -12,6 +12,6 @@ pub(crate) use self::{ get_diff_rename::git_diff_renames, get_diff_show_whitespace::get_diff_show_whitespace, get_input::get_input, - get_string::{_get_string, get_string}, + get_string::{get_optional_string, get_string}, get_unsigned_integer::get_unsigned_integer, }; diff --git a/src/core/src/application.rs b/src/core/src/application.rs index 577e496..7ccc86f 100644 --- a/src/core/src/application.rs +++ b/src/core/src/application.rs @@ -152,11 +152,16 @@ where ModuleProvider: module::ModuleProvider + Send + 'static Config::try_from(repo).map_err(|err| Exit::new(ExitStatus::ConfigError, format!("{err:#}").as_str())) } + fn todo_file_options(config: &Config) -> TodoFileOptions { + let mut todo_file_options = TodoFileOptions::new(config.undo_limit, config.git.comment_char.as_str()); + if let Some(command) = config.post_modified_line_exec_command.as_deref() { + todo_file_options.line_changed_command(command); + } + todo_file_options + } + fn load_todo_file(filepath: &str, config: &Config) -> Result { - let mut todo_file = TodoFile::new( - filepath, - TodoFileOptions::new(config.undo_limit, config.git.comment_char.as_str()), - ); + let mut todo_file = TodoFile::new(filepath, Self::todo_file_options(config)); todo_file .load_file() .map_err(|err| Exit::new(ExitStatus::FileReadError, err.to_string().as_str()))?; @@ -187,7 +192,7 @@ where ModuleProvider: module::ModuleProvider + Send + 'static mod tests { use std::ffi::OsString; - use claims::assert_ok; + use claims::{assert_none, assert_ok}; use display::{testutil::CrossTerm, Size}; use input::{KeyCode, KeyEvent, KeyModifiers}; use runtime::{Installer, RuntimeError}; @@ -264,6 +269,36 @@ mod tests { assert_eq!(exit.get_status(), &ExitStatus::ConfigError); } + #[test] + fn todo_file_options_without_command() { + let mut config = Config::new(); + config.undo_limit = 10; + config.git.comment_char = String::from("#"); + config.post_modified_line_exec_command = None; + + let expected = TodoFileOptions::new(10, "#"); + assert_eq!( + Application::>::todo_file_options(&config), + expected + ); + } + + #[test] + fn todo_file_options_with_command() { + let mut config = Config::new(); + config.undo_limit = 10; + config.git.comment_char = String::from("#"); + config.post_modified_line_exec_command = Some(String::from("command")); + + let mut expected = TodoFileOptions::new(10, "#"); + expected.line_changed_command("command"); + + assert_eq!( + Application::>::todo_file_options(&config), + expected + ); + } + #[test] #[serial_test::serial] fn load_todo_file_load_error() { diff --git a/src/todo_file/src/lib.rs b/src/todo_file/src/lib.rs index cb37d0a..126cd1c 100644 --- a/src/todo_file/src/lib.rs +++ b/src/todo_file/src/lib.rs @@ -262,7 +262,41 @@ impl TodoFile { String::from("noop") } else { - self.lines.iter().map(Line::to_text).collect::>().join("\n") + self.lines + .iter() + .flat_map(|l| { + let mut lines = vec![Line::to_text(l)]; + if let Some(command) = self.options.line_changed_command.as_deref() { + if l.is_modified() { + let action = l.get_action(); + + match *action { + Action::Break | Action::Noop => {}, + Action::Drop + | Action::Fixup + | Action::Edit + | Action::Pick + | Action::Reword + | Action::Squash => { + lines.push(format!("exec {command} \"{}\" \"{}\"", action, l.get_hash())); + }, + Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => { + let original_label = + l.original().map_or_else(|| l.get_content(), Line::get_content); + lines.push(format!( + "exec {command} \"{}\" \"{}\" \"{}\"", + action, + original_label, + l.get_content() + )); + }, + } + } + } + lines + }) + .collect::>() + .join("\n") }; writeln!(file, "{file_contents}").map_err(|err| { IoError::FileRead { @@ -504,18 +538,25 @@ mod tests { Line::parse(line).unwrap() } - fn create_and_load_todo_file(file_contents: &[&str]) -> (TodoFile, NamedTempFile) { + fn create_and_load_todo_file_with_options( + file_contents: &[&str], + todo_file_options: TodoFileOptions, + ) -> (TodoFile, NamedTempFile) { let todo_file_path = Builder::new() .prefix("git-rebase-todo-scratch") .suffix("") .tempfile() .unwrap(); write!(todo_file_path.as_file(), "{}", file_contents.join("\n")).unwrap(); - let mut todo_file = TodoFile::new(todo_file_path.path().to_str().unwrap(), TodoFileOptions::new(1, "#")); + let mut todo_file = TodoFile::new(todo_file_path.path().to_str().unwrap(), todo_file_options); todo_file.load_file().unwrap(); (todo_file, todo_file_path) } + fn create_and_load_todo_file(file_contents: &[&str]) -> (TodoFile, NamedTempFile) { + create_and_load_todo_file_with_options(file_contents, TodoFileOptions::new(1, "#")) + } + macro_rules! assert_read_todo_file { ($todo_file_path:expr, $($arg:expr),*) => { let expected = [$( $arg, )*]; @@ -603,6 +644,107 @@ mod tests { assert_todo_lines!(todo_file, "pick bbb comment"); } + #[test] + fn write_file_with_exec_command_modified_line_with_reference() { + fn create_modified_line(action: &str) -> Line { + let mut parsed = create_line(format!("{action} label").as_str()); + parsed.edit_content("new-label"); + parsed + } + let mut options = TodoFileOptions::new(10, "#"); + options.line_changed_command("command"); + let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options); + todo_file.set_lines(vec![ + create_modified_line("label"), + create_modified_line("reset"), + create_modified_line("merge"), + create_modified_line("update-ref"), + ]); + todo_file.write_file().unwrap(); + assert_read_todo_file!( + todo_file.get_filepath(), + "label new-label", + "exec command \"label\" \"label\" \"new-label\"", + "reset new-label", + "exec command \"reset\" \"label\" \"new-label\"", + "merge new-label", + "exec command \"merge\" \"label\" \"new-label\"", + "update-ref new-label", + "exec command \"update-ref\" \"label\" \"new-label\"" + ); + } + + #[test] + fn write_file_with_exec_command_modified_line_with_hash() { + fn create_modified_line(action: &str) -> Line { + let mut parsed = create_line(format!("{action} bbb comment").as_str()); + parsed.set_action( + if parsed.get_action() == &Action::Fixup { + Action::Pick + } + else { + Action::Fixup + }, + ); + parsed + } + let mut options = TodoFileOptions::new(10, "#"); + options.line_changed_command("command"); + let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options); + let mut line = create_line("pick bbb comment"); + line.set_action(Action::Fixup); + todo_file.set_lines(vec![ + create_modified_line("drop"), + create_modified_line("fixup"), + create_modified_line("edit"), + create_modified_line("pick"), + create_modified_line("reword"), + create_modified_line("squash"), + ]); + todo_file.write_file().unwrap(); + assert_read_todo_file!( + todo_file.get_filepath(), + "fixup bbb comment", + "exec command \"fixup\" \"bbb\"", + "pick bbb comment", + "exec command \"pick\" \"bbb\"", + "fixup bbb comment", + "exec command \"fixup\" \"bbb\"", + "fixup bbb comment", + "exec command \"fixup\" \"bbb\"", + "fixup bbb comment", + "exec command \"fixup\" \"bbb\"", + "fixup bbb comment", + "exec command \"fixup\" \"bbb\"" + ); + } + + #[test] + fn write_file_with_exec_command_modified_line_with_exec() { + let mut options = TodoFileOptions::new(10, "#"); + options.line_changed_command("command"); + let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options); + let mut line = create_line("exec command"); + line.edit_content("new-command"); + todo_file.set_lines(vec![line]); + todo_file.write_file().unwrap(); + assert_read_todo_file!( + todo_file.get_filepath(), + "exec new-command", + "exec command \"exec\" \"command\" \"new-command\"" + ); + } + + #[test] + fn write_file_with_exec_command_modified_line_with_break() { + let mut options = TodoFileOptions::new(10, "#"); + options.line_changed_command("command"); + let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options); + todo_file.set_lines(vec![create_line("break")]); + todo_file.write_file().unwrap(); + assert_read_todo_file!(todo_file.get_filepath(), "break"); + } + #[test] fn write_file_noop() { let (mut todo_file, _) = create_and_load_todo_file(&[]); diff --git a/src/todo_file/src/line.rs b/src/todo_file/src/line.rs index 2a1dca6..030b54e 100644 --- a/src/todo_file/src/line.rs +++ b/src/todo_file/src/line.rs @@ -8,9 +8,7 @@ pub struct Line { hash: String, mutated: bool, option: Option, - original_action: Action, - original_content: String, - original_option: Option, + original_line: Option>, } impl Line { @@ -25,9 +23,14 @@ impl Line { hash: String::from(hash), mutated: false, option: original_option.clone(), - original_action, - original_content, - original_option, + original_line: Some(Box::new(Line { + action: original_action, + content: original_content, + hash: String::from(hash), + mutated: false, + option: original_option, + original_line: None, + })), } } @@ -157,6 +160,13 @@ impl Line { self.option = Some(String::from(option)); } + /// Get the original line, before any modifications + #[must_use] + #[inline] + pub fn original(&self) -> Option<&Line> { + self.original_line.as_deref() + } + /// Get the action of the line. #[must_use] #[inline] @@ -273,9 +283,14 @@ mod tests { content: String::new(), mutated: false, option: None, - original_action: Action::Pick, - original_content: String::new(), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Pick, + hash: String::from("abc123"), + content: String::new(), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -287,9 +302,14 @@ mod tests { content: String::new(), mutated: false, option: None, - original_action: Action::Break, - original_content: String::new(), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Break, + hash: String::new(), + content: String::new(), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -301,9 +321,14 @@ mod tests { content: String::from("command"), mutated: false, option: None, - original_action: Action::Exec, - original_content: String::from("command"), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Exec, + hash: String::new(), + content: String::from("command"), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -315,9 +340,14 @@ mod tests { content: String::from("command"), mutated: false, option: None, - original_action: Action::Merge, - original_content: String::from("command"), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Merge, + hash: String::new(), + content: String::from("command"), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -329,9 +359,14 @@ mod tests { content: String::from("label"), mutated: false, option: None, - original_action: Action::Label, - original_content: String::from("label"), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Label, + hash: String::new(), + content: String::from("label"), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -343,9 +378,14 @@ mod tests { content: String::from("label"), mutated: false, option: None, - original_action: Action::Reset, - original_content: String::from("label"), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::Reset, + hash: String::new(), + content: String::from("label"), + mutated: false, + option: None, + original_line: None, + })) }); } @@ -357,9 +397,14 @@ mod tests { content: String::from("reference"), mutated: false, option: None, - original_action: Action::UpdateRef, - original_content: String::from("reference"), - original_option: None, + original_line: Some(Box::new(Line { + action: Action::UpdateRef, + hash: String::new(), + content: String::from("reference"), + mutated: false, + option: None, + original_line: None, + })) }); } diff --git a/src/todo_file/src/todo_file_options.rs b/src/todo_file/src/todo_file_options.rs index 8dc9ed4..1ea7f69 100644 --- a/src/todo_file/src/todo_file_options.rs +++ b/src/todo_file/src/todo_file_options.rs @@ -1,8 +1,9 @@ /// Options for `TodoFile` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TodoFileOptions { - pub(crate) undo_limit: u32, pub(crate) comment_prefix: String, + pub(crate) line_changed_command: Option, + pub(crate) undo_limit: u32, } impl TodoFileOptions { @@ -11,8 +12,40 @@ impl TodoFileOptions { #[inline] pub fn new(undo_limit: u32, comment_prefix: &str) -> Self { Self { - undo_limit, comment_prefix: String::from(comment_prefix), + line_changed_command: None, + undo_limit, } } + + /// Set a command to be added after each changed line + #[inline] + pub fn line_changed_command(&mut self, command: &str) { + self.line_changed_command = Some(String::from(command)); + } +} + +#[cfg(test)] +mod tests { + use claims::{assert_none, assert_some_eq}; + + use super::*; + + #[test] + fn new() { + let options = TodoFileOptions::new(10, "#"); + + assert_eq!(options.undo_limit, 10); + assert_eq!(options.comment_prefix, "#"); + assert_none!(options.line_changed_command); + } + + #[test] + fn line_changed_command() { + let mut options = TodoFileOptions::new(10, "#"); + + options.line_changed_command("command"); + + assert_some_eq!(options.line_changed_command, "command"); + } } -- cgit v1.2.3