Skip to content

Commit 401d96c

Browse files
committed
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.
1 parent 81d8b6e commit 401d96c

File tree

13 files changed

+396
-66
lines changed

13 files changed

+396
-66
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](http://keepachangelog.com/).
55

6+
## [Unreleased]
7+
### Added
8+
- Post modified line exec command ([#888](https://github.com/MitMaro/git-interactive-rebase-tool/pull/888))
9+
10+
611
## [2.3.0] - 2023-07-19
712
### Added
813
- Support for update-ref action ([#801](https://github.com/MitMaro/git-interactive-rebase-tool/pull/801))

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,53 @@ Need to do something in your Git editor? Quickly shell out to your editor, make
7676

7777
![Shell out to editor](/docs/assets/images/girt-external-editor.gif?raw=true)
7878

79+
### Advanced Features
80+
81+
#### Modified line exec command
82+
83+
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.
84+
85+
To enable this option, set the `interactive-rebase-tool.postModifiedLineExecCommand` option, providing an executable or script.
86+
87+
```shell
88+
git config --global interactive-rebase-tool.postModifiedLineExecCommand "/path/to/global/script"
89+
```
90+
91+
Or using repository-specific configuration, for targeted scripts.
92+
93+
```shell
94+
git config --global interactive-rebase-tool.postModifiedLineExecCommand "/path/to/repo/script"
95+
```
96+
97+
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.
98+
99+
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.
100+
101+
Full example of a resulting rebase todo file, assuming that `interactive-rebase-tool.postModifiedLineExecCommand` was set to `script.sh`.
102+
103+
```
104+
# original line: label onto
105+
label new-onto
106+
exec script.sh "label" "onto" "new-onto"
107+
108+
# original line: reset onto
109+
reset new-onto
110+
exec script.sh "reset" "onto" "new-onto"
111+
112+
pick a12345 My feature
113+
# original line: pick b12345 My change
114+
squash b12345 My change
115+
exec script.sh "squash" "b12345"
116+
117+
# original line: label branch
118+
label branch
119+
exec script.sh "label" "branch" "new-branch"
120+
121+
# original line: exec command
122+
exec new-command
123+
exec script.sh "exec" "command" "new-command"
124+
```
125+
79126
## Setup
80127

81128
### Most systems

readme/customization.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,18 @@ Some values from your Git Config are directly used by this application.
3939

4040
## General
4141

42-
| Key | Default | Type | Description |
43-
|----------------------------|---------|---------|---------------------------------------------------------------------------------------------|
44-
| `autoSelectNext` | false | bool | If true, auto select the next line after action modification |
45-
| `diffIgnoreBlankLines` | none | String¹ | If to ignore blank lines during diff. |
46-
| `diffIgnoreWhitespace` | none | String¹ | If and how to ignore whitespace during diff. |
47-
| `diffShowWhitespace` | both | String² | If and how to show whitespace during diff. |
48-
| `diffSpaceSymbol` | · | String | The visible symbol for the space character. Only used when `diffShowWhitespace` is enabled. |
49-
| `diffTabSymbol` || String | The visible symbol for the tab character. Only used when `diffShowWhitespace` is enabled. |
50-
| `diffTabWidth` | 4 | Integer | The width of the tab character |
51-
| `undoLimit` | 5000 | Integer | Number of undo operations to store. |
52-
| `verticalSpacingCharacter` | ~ | String | Vertical spacing character. Can be set to an empty string. |
42+
| Key | Default | Type | Description |
43+
|-------------------------------|---------|---------|---------------------------------------------------------------------------------------------|
44+
| `autoSelectNext` | false | bool | If true, auto select the next line after action modification |
45+
| `diffIgnoreBlankLines` | none | String¹ | If to ignore blank lines during diff. |
46+
| `diffIgnoreWhitespace` | none | String¹ | If and how to ignore whitespace during diff. |
47+
| `diffShowWhitespace` | both | String² | If and how to show whitespace during diff. |
48+
| `diffSpaceSymbol` | · | String | The visible symbol for the space character. Only used when `diffShowWhitespace` is enabled. |
49+
| `diffTabSymbol` || String | The visible symbol for the tab character. Only used when `diffShowWhitespace` is enabled. |
50+
| `diffTabWidth` | 4 | Integer | The width of the tab character |
51+
| `undoLimit` | 5000 | Integer | Number of undo operations to store. |
52+
| `postModifiedLineExecCommand` | | String | Exec command to attach to modified lines. See [modified line exec command] for details. |
53+
| `verticalSpacingCharacter` | ~ | String | Vertical spacing character. Can be set to an empty string. |
5354

5455
¹ Ignore whitespace can be:
5556
- `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.
6263
- `true`, `on` or `both` to show both leading and trailing whitespace
6364
- `false`, `off`, `none` to show no whitespace
6465

66+
[modified line exec command]:../README.md#modified-line-exec-command
6567
[diffIgnoreSpaceChange]:https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---ignore-space-change
6668
[diffIgnoreAllSpace]:https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---ignore-all-space
6769

src/config/src/lib.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ pub use self::{
162162
key_bindings::KeyBindings,
163163
theme::Theme,
164164
};
165-
use crate::errors::{ConfigError, ConfigErrorCause};
165+
use crate::{
166+
errors::{ConfigError, ConfigErrorCause},
167+
utils::get_optional_string,
168+
};
166169

167170
const DEFAULT_SPACE_SYMBOL: &str = "\u{b7}"; // ·
168171
const DEFAULT_TAB_SYMBOL: &str = "\u{2192}"; // →
@@ -185,6 +188,8 @@ pub struct Config {
185188
pub diff_tab_symbol: String,
186189
/// The display width of the tab character.
187190
pub diff_tab_width: u32,
191+
/// If set, automatically add an exec line with the command after every modified line
192+
pub post_modified_line_exec_command: Option<String>,
188193
/// The maximum number of undo steps.
189194
pub undo_limit: u32,
190195
/// Configuration options loaded directly from Git.
@@ -221,6 +226,10 @@ impl Config {
221226
diff_tab_symbol: get_string(git_config, "interactive-rebase-tool.diffTabSymbol", DEFAULT_TAB_SYMBOL)?,
222227
diff_tab_width: get_unsigned_integer(git_config, "interactive-rebase-tool.diffTabWidth", 4)?,
223228
undo_limit: get_unsigned_integer(git_config, "interactive-rebase-tool.undoLimit", 5000)?,
229+
post_modified_line_exec_command: get_optional_string(
230+
git_config,
231+
"interactive-rebase-tool.postModifiedLineExecCommand",
232+
)?,
224233
git: GitConfig::new_with_config(git_config)?,
225234
key_bindings: KeyBindings::new_with_config(git_config)?,
226235
theme: Theme::new_with_config(git_config)?,
@@ -435,7 +444,6 @@ mod tests {
435444
#[case::diff_tab_width("diffTabWidth", "42", 42, |config: Config| config.diff_tab_width)]
436445
#[case::diff_tab_symbol_default("diffTabSymbol", "", String::from("→"), |config: Config| config.diff_tab_symbol)]
437446
#[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)]
438-
#[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)]
439447
#[case::diff_space_symbol_default(
440448
"diffSpaceSymbol",
441449
"",
@@ -444,8 +452,20 @@ mod tests {
444452
]
445453
#[case::diff_space_symbol("diffSpaceSymbol", "-", String::from("-"), |config: Config| config.diff_space_symbol)]
446454
#[case::undo_limit_default("undoLimit", "", 5000, |config: Config| config.undo_limit)]
447-
#[case::undo_limit_default("undoLimit", "42", 42, |config: Config| config.undo_limit)]
448-
pub(crate) fn theme_color<F, T>(
455+
#[case::undo_limit("undoLimit", "42", 42, |config: Config| config.undo_limit)]
456+
#[case::post_modified_line_exec_command(
457+
"postModifiedLineExecCommand",
458+
"command",
459+
Some(String::from("command")),
460+
|config: Config| config.post_modified_line_exec_command
461+
)]
462+
#[case::post_modified_line_exec_command_default(
463+
"postModifiedLineExecCommand",
464+
"",
465+
None,
466+
|config: Config| config.post_modified_line_exec_command
467+
)]
468+
pub(crate) fn config_test<F, T>(
449469
#[case] config_name: &str,
450470
#[case] config_value: &str,
451471
#[case] expected: T,
@@ -497,9 +517,10 @@ mod tests {
497517

498518
#[rstest]
499519
#[case::diff_tab_symbol("diffIgnoreWhitespace")]
500-
#[case::diff_tab_symbol("diffShowWhitespace")]
520+
#[case::diff_show_whitespace("diffShowWhitespace")]
501521
#[case::diff_tab_symbol("diffTabSymbol")]
502522
#[case::diff_space_symbol("diffSpaceSymbol")]
523+
#[case::post_modified_line_exec_command("postModifiedLineExecCommand")]
503524
fn value_parsing_invalid_utf(#[case] config_name: &str) {
504525
with_git_config(
505526
&[

src/config/src/theme.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use git::Config;
22

33
use crate::{
44
errors::ConfigError,
5-
utils::{_get_string, get_string},
5+
utils::{get_optional_string, get_string},
66
Color,
77
ConfigErrorCause,
88
};
99

1010
fn get_color(config: Option<&Config>, name: &str, default: Color) -> Result<Color, ConfigError> {
11-
if let Some(value) = _get_string(config, name)? {
11+
if let Some(value) = get_optional_string(config, name)? {
1212
Color::try_from(value.to_lowercase().as_str()).map_err(|invalid_color_error| {
1313
ConfigError::new(
1414
name,

src/config/src/utils/get_bool.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use git::{Config, ErrorCode};
22

3-
use crate::{utils::_get_string, ConfigError, ConfigErrorCause};
3+
use crate::{utils::get_optional_string, ConfigError, ConfigErrorCause};
44

55
pub(crate) fn get_bool(config: Option<&Config>, name: &str, default: bool) -> Result<bool, ConfigError> {
66
if let Some(cfg) = config {
@@ -10,14 +10,14 @@ pub(crate) fn get_bool(config: Option<&Config>, name: &str, default: bool) -> Re
1010
Err(e) if e.message().contains("failed to parse") => {
1111
Err(ConfigError::new_with_optional_input(
1212
name,
13-
_get_string(config, name).ok().flatten(),
13+
get_optional_string(config, name).ok().flatten(),
1414
ConfigErrorCause::InvalidBoolean,
1515
))
1616
},
1717
Err(e) => {
1818
Err(ConfigError::new_with_optional_input(
1919
name,
20-
_get_string(config, name).ok().flatten(),
20+
get_optional_string(config, name).ok().flatten(),
2121
ConfigErrorCause::UnknownError(String::from(e.message())),
2222
))
2323
},

src/config/src/utils/get_string.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use git::{Config, ErrorCode};
22

33
use crate::{ConfigError, ConfigErrorCause};
44

5-
pub(crate) fn _get_string(config: Option<&Config>, name: &str) -> Result<Option<String>, ConfigError> {
5+
pub(crate) fn get_optional_string(config: Option<&Config>, name: &str) -> Result<Option<String>, ConfigError> {
66
let Some(cfg) = config
77
else {
88
return Ok(None);
@@ -24,7 +24,7 @@ pub(crate) fn _get_string(config: Option<&Config>, name: &str) -> Result<Option<
2424
}
2525

2626
pub(crate) fn get_string(config: Option<&Config>, name: &str, default: &str) -> Result<String, ConfigError> {
27-
Ok(_get_string(config, name)?.unwrap_or_else(|| String::from(default)))
27+
Ok(get_optional_string(config, name)?.unwrap_or_else(|| String::from(default)))
2828
}
2929

3030
#[cfg(test)]

src/config/src/utils/get_unsigned_integer.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use git::{Config, ErrorCode};
22

3-
use crate::{utils::_get_string, ConfigError, ConfigErrorCause};
3+
use crate::{utils::get_optional_string, ConfigError, ConfigErrorCause};
44

55
pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default: u32) -> Result<u32, ConfigError> {
66
if let Some(cfg) = config {
@@ -9,7 +9,7 @@ pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default:
99
v.try_into().map_err(|_| {
1010
ConfigError::new_with_optional_input(
1111
name,
12-
_get_string(config, name).ok().flatten(),
12+
get_optional_string(config, name).ok().flatten(),
1313
ConfigErrorCause::InvalidUnsignedInteger,
1414
)
1515
})
@@ -18,14 +18,14 @@ pub(crate) fn get_unsigned_integer(config: Option<&Config>, name: &str, default:
1818
Err(e) if e.message().contains("failed to parse") => {
1919
Err(ConfigError::new_with_optional_input(
2020
name,
21-
_get_string(config, name).ok().flatten(),
21+
get_optional_string(config, name).ok().flatten(),
2222
ConfigErrorCause::InvalidUnsignedInteger,
2323
))
2424
},
2525
Err(e) => {
2626
Err(ConfigError::new_with_optional_input(
2727
name,
28-
_get_string(config, name).ok().flatten(),
28+
get_optional_string(config, name).ok().flatten(),
2929
ConfigErrorCause::UnknownError(String::from(e.message())),
3030
))
3131
},

src/config/src/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ pub(crate) use self::{
1212
get_diff_rename::git_diff_renames,
1313
get_diff_show_whitespace::get_diff_show_whitespace,
1414
get_input::get_input,
15-
get_string::{_get_string, get_string},
15+
get_string::{get_optional_string, get_string},
1616
get_unsigned_integer::get_unsigned_integer,
1717
};

src/core/src/application.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,16 @@ where ModuleProvider: module::ModuleProvider + Send + 'static
152152
Config::try_from(repo).map_err(|err| Exit::new(ExitStatus::ConfigError, format!("{err:#}").as_str()))
153153
}
154154

155+
fn todo_file_options(config: &Config) -> TodoFileOptions {
156+
let mut todo_file_options = TodoFileOptions::new(config.undo_limit, config.git.comment_char.as_str());
157+
if let Some(command) = config.post_modified_line_exec_command.as_deref() {
158+
todo_file_options.line_changed_command(command);
159+
}
160+
todo_file_options
161+
}
162+
155163
fn load_todo_file(filepath: &str, config: &Config) -> Result<TodoFile, Exit> {
156-
let mut todo_file = TodoFile::new(
157-
filepath,
158-
TodoFileOptions::new(config.undo_limit, config.git.comment_char.as_str()),
159-
);
164+
let mut todo_file = TodoFile::new(filepath, Self::todo_file_options(config));
160165
todo_file
161166
.load_file()
162167
.map_err(|err| Exit::new(ExitStatus::FileReadError, err.to_string().as_str()))?;
@@ -187,7 +192,7 @@ where ModuleProvider: module::ModuleProvider + Send + 'static
187192
mod tests {
188193
use std::ffi::OsString;
189194

190-
use claims::assert_ok;
195+
use claims::{assert_none, assert_ok};
191196
use display::{testutil::CrossTerm, Size};
192197
use input::{KeyCode, KeyEvent, KeyModifiers};
193198
use runtime::{Installer, RuntimeError};
@@ -264,6 +269,36 @@ mod tests {
264269
assert_eq!(exit.get_status(), &ExitStatus::ConfigError);
265270
}
266271

272+
#[test]
273+
fn todo_file_options_without_command() {
274+
let mut config = Config::new();
275+
config.undo_limit = 10;
276+
config.git.comment_char = String::from("#");
277+
config.post_modified_line_exec_command = None;
278+
279+
let expected = TodoFileOptions::new(10, "#");
280+
assert_eq!(
281+
Application::<TestModuleProvider<DefaultTestModule>>::todo_file_options(&config),
282+
expected
283+
);
284+
}
285+
286+
#[test]
287+
fn todo_file_options_with_command() {
288+
let mut config = Config::new();
289+
config.undo_limit = 10;
290+
config.git.comment_char = String::from("#");
291+
config.post_modified_line_exec_command = Some(String::from("command"));
292+
293+
let mut expected = TodoFileOptions::new(10, "#");
294+
expected.line_changed_command("command");
295+
296+
assert_eq!(
297+
Application::<TestModuleProvider<DefaultTestModule>>::todo_file_options(&config),
298+
expected
299+
);
300+
}
301+
267302
#[test]
268303
#[serial_test::serial]
269304
fn load_todo_file_load_error() {

0 commit comments

Comments
 (0)