summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Oram <dev@mitmaro.ca>2023-08-10 08:54:02 -0230
committerTim Oram <dev@mitmaro.ca>2023-08-12 11:58:04 -0230
commit401d96cda98b2102c9e1cbcc6dee9d3d9cbf881b (patch)
tree28a21456b366d40f77c0ac37f6c664d1d565d2bf
parent81d8b6e62f54ca9bae49ee1e9b68b7452d4a87e9 (diff)
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.
-rw-r--r--CHANGELOG.md5
-rw-r--r--README.md47
-rw-r--r--readme/customization.md24
-rw-r--r--src/config/src/lib.rs31
-rw-r--r--src/config/src/theme.rs4
-rw-r--r--src/config/src/utils/get_bool.rs6
-rw-r--r--src/config/src/utils/get_string.rs4
-rw-r--r--src/config/src/utils/get_unsigned_integer.rs8
-rw-r--r--src/config/src/utils/mod.rs2
-rw-r--r--src/core/src/application.rs45
-rw-r--r--src/todo_file/src/lib.rs148
-rw-r--r--src/todo_file/src/line.rs99
-rw-r--r--src/todo_file/src/todo_file_options.rs39
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<String>,
/// 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<F, T>(
+ #[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<F, T>(
#[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<Color, ConfigError> {
- 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<bool, ConfigError> {
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<Option<String>, ConfigError> {
+pub(crate) fn get_optional_string(config: Option<&Config>, name: &str) -> Result<Option<String>, ConfigError> {
let Some(cfg) = config
else {
return Ok(None);
@@ -24,7 +24,7 @@ pub(crate) fn _get_string(config: Option<&Config>, name: &str) -> Result<Option<
}
pub(crate) fn get_string(config: Option<&Config>, name: &str, default: &str) -> Result<String, ConfigError> {
- 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<u32, ConfigError> {
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<TodoFile, Exit> {
- 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};
@@ -265,6 +270,36 @@ mod tests {
}
#[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::<TestModuleProvider<DefaultTestModule>>::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::<TestModuleProvider<DefaultTestModule>>::todo_file_options(&config),
+ expected
+ );
+ }
+
+ #[test]
#[serial_test::serial]
fn load_todo_file_load_error() {
_ = set_git_directory("fixtures/simple");
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::<Vec<String>>().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::<Vec<String>>()
+ .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, )*];
@@ -604,6 +645,107 @@ mod tests {
}
#[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(&[]);
todo_file.set_lines(vec![create_line("noop")]);
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<String>,
- original_action: Action,
- original_content: String,
- original_option: Option<String>,
+ original_line: Option<Box<Line>>,
}
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<String>,
+ 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");
+ }
}