diff --git a/.vscode/.gitignore b/.vscode/.gitignore new file mode 100644 index 000000000..33662f554 --- /dev/null +++ b/.vscode/.gitignore @@ -0,0 +1 @@ +/* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..2f53f0aa3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "editor.insertSpaces": false, + "editor.tabSize": 4, + "files.insertFinalNewline": true, + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + }, + "[markdown]": { + "editor.insertSpaces": true, + } +} diff --git a/README.md b/README.md index b0d3a27a3..e8df75a23 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ Other platforms are not officially supported. Some platforms have community supp ### Install Rust -To start developing the project, you will need to [install Rust][install-rust], which can generally be done using [rustup]. +To start developing the project, you will need to [install Rust][install-rust], which should be done using [rustup]. ### Setup diff --git a/readme/customization.md b/readme/customization.md index c57573daa..19b795605 100644 --- a/readme/customization.md +++ b/readme/customization.md @@ -115,6 +115,8 @@ Most keys can be changed to any printable character or supported special charact | `inputActionPick` | p | String | Key for setting action to pick | | `inputActionReword` | r | String | Key for setting action to reword | | `inputActionSquash` | s | String | Key for setting action to squash | +| `inputActionCut` | x | String | Key for setting action to cut (git-revise) | +| `inputActionIndex` | i | String | Key for setting action to index (git-revise) | | `inputConfirmNo` | n | String | Key for rejecting a confirmation | | `inputConfirmYes` | y | String | Key for confirming a confirmation | | `inputEdit` | E | String | Key for entering edit mode | diff --git a/src/config/key_bindings.rs b/src/config/key_bindings.rs index b81b235cd..764921ffe 100644 --- a/src/config/key_bindings.rs +++ b/src/config/key_bindings.rs @@ -20,12 +20,16 @@ pub(crate) struct KeyBindings { pub(crate) abort: Vec, /// Key bindings for the break action. pub(crate) action_break: Vec, + /// Key bindings for the cut action. + pub(crate) action_cut: Vec, /// Key bindings for the drop action. pub(crate) action_drop: Vec, /// Key bindings for the edit action. pub(crate) action_edit: Vec, /// Key bindings for the fixup action. pub(crate) action_fixup: Vec, + /// Key bindings for the index action. + pub(crate) action_index: Vec, /// Key bindings for the pick action. pub(crate) action_pick: Vec, /// Key bindings for the reword action. @@ -126,9 +130,11 @@ impl KeyBindings { Ok(Self { abort: get_input(git_config, "interactive-rebase-tool.inputAbort", "q")?, action_break: get_input(git_config, "interactive-rebase-tool.inputActionBreak", "b")?, + action_cut: get_input(git_config, "interactive-rebase-tool.inputActionCut", "x")?, action_drop: get_input(git_config, "interactive-rebase-tool.inputActionDrop", "d")?, action_edit: get_input(git_config, "interactive-rebase-tool.inputActionEdit", "e")?, action_fixup: get_input(git_config, "interactive-rebase-tool.inputActionFixup", "f")?, + action_index: get_input(git_config, "interactive-rebase-tool.inputActionIndex", "i")?, action_pick: get_input(git_config, "interactive-rebase-tool.inputActionPick", "p")?, action_reword: get_input(git_config, "interactive-rebase-tool.inputActionReword", "r")?, action_squash: get_input(git_config, "interactive-rebase-tool.inputActionSquash", "s")?, @@ -243,6 +249,7 @@ mod tests { config_test!(action_drop, "inputActionDrop", "d"); config_test!(action_edit, "inputActionEdit", "e"); config_test!(action_fixup, "inputActionFixup", "f"); + config_test!(action_index, "inputActionIndex", "i"); config_test!(action_pick, "inputActionPick", "p"); config_test!(action_reword, "inputActionReword", "r"); config_test!(action_squash, "inputActionSquash", "s"); diff --git a/src/config/theme.rs b/src/config/theme.rs index 3310aa443..9896bc015 100644 --- a/src/config/theme.rs +++ b/src/config/theme.rs @@ -31,6 +31,8 @@ pub(crate) struct Theme { pub(crate) character_vertical_spacing: String, /// The color for the break action. pub(crate) color_action_break: Color, + /// The color for the cut action. + pub(crate) color_action_cut: Color, /// The color for the drop action. pub(crate) color_action_drop: Color, /// The color for the edit action. @@ -39,6 +41,8 @@ pub(crate) struct Theme { pub(crate) color_action_exec: Color, /// The color for the fixup action. pub(crate) color_action_fixup: Color, + /// The color for the fixup action. + pub(crate) color_action_index: Color, /// The color for the pick action. pub(crate) color_action_pick: Color, /// The color for the reword action. @@ -83,10 +87,12 @@ impl Theme { "~", )?, color_action_break: get_color(git_config, "interactive-rebase-tool.breakColor", Color::LightWhite)?, + color_action_cut: get_color(git_config, "interactive-rebase-tool.cutColor", Color::DarkRed)?, color_action_drop: get_color(git_config, "interactive-rebase-tool.dropColor", Color::LightRed)?, color_action_edit: get_color(git_config, "interactive-rebase-tool.editColor", Color::LightBlue)?, color_action_exec: get_color(git_config, "interactive-rebase-tool.execColor", Color::LightWhite)?, color_action_fixup: get_color(git_config, "interactive-rebase-tool.fixupColor", Color::LightMagenta)?, + color_action_index: get_color(git_config, "interactive-rebase-tool.indexColor", Color::DarkGreen)?, color_action_pick: get_color(git_config, "interactive-rebase-tool.pickColor", Color::LightGreen)?, color_action_reword: get_color(git_config, "interactive-rebase-tool.rewordColor", Color::LightYellow)?, color_action_squash: get_color(git_config, "interactive-rebase-tool.squashColor", Color::LightCyan)?, @@ -204,6 +210,7 @@ mod tests { config_test!(color_action_edit, "editColor", Color::LightBlue); config_test!(color_action_exec, "execColor", Color::LightWhite); config_test!(color_action_fixup, "fixupColor", Color::LightMagenta); + config_test!(color_action_index, "indexColor", Color::DarkGreen); config_test!(color_action_pick, "pickColor", Color::LightGreen); config_test!(color_action_reword, "rewordColor", Color::LightYellow); config_test!(color_action_squash, "squashColor", Color::LightCyan); diff --git a/src/display.rs b/src/display.rs index 1a6bf4950..b590a9e68 100644 --- a/src/display.rs +++ b/src/display.rs @@ -34,10 +34,12 @@ use crate::config::Theme; #[derive(Debug)] pub(crate) struct Display { action_break: (Colors, Colors), + action_cut: (Colors, Colors), action_drop: (Colors, Colors), action_edit: (Colors, Colors), action_exec: (Colors, Colors), action_fixup: (Colors, Colors), + action_index: (Colors, Colors), action_label: (Colors, Colors), action_merge: (Colors, Colors), action_pick: (Colors, Colors), @@ -77,6 +79,12 @@ impl Display { theme.color_background, theme.color_selected_background, ); + let action_cut = register_selectable_color_pairs( + color_mode, + theme.color_action_cut, + theme.color_background, + theme.color_selected_background, + ); let action_drop = register_selectable_color_pairs( color_mode, theme.color_action_drop, @@ -101,6 +109,12 @@ impl Display { theme.color_background, theme.color_selected_background, ); + let action_index = register_selectable_color_pairs( + color_mode, + theme.color_action_index, + theme.color_background, + theme.color_selected_background, + ); let action_pick = register_selectable_color_pairs( color_mode, theme.color_action_pick, @@ -176,10 +190,12 @@ impl Display { Self { action_break, + action_cut, action_drop, action_edit, action_exec, action_fixup, + action_index, action_label, action_merge, action_pick, @@ -236,10 +252,12 @@ impl Display { if selected { match color { DisplayColor::ActionBreak => self.action_break.1, + DisplayColor::ActionCut => self.action_cut.1, DisplayColor::ActionDrop => self.action_drop.1, DisplayColor::ActionEdit => self.action_edit.1, DisplayColor::ActionExec => self.action_exec.1, DisplayColor::ActionFixup => self.action_fixup.1, + DisplayColor::ActionIndex => self.action_index.1, DisplayColor::ActionPick => self.action_pick.1, DisplayColor::ActionReword => self.action_reword.1, DisplayColor::ActionSquash => self.action_squash.1, @@ -259,10 +277,12 @@ impl Display { else { match color { DisplayColor::ActionBreak => self.action_break.0, + DisplayColor::ActionCut => self.action_cut.0, DisplayColor::ActionDrop => self.action_drop.0, DisplayColor::ActionEdit => self.action_edit.0, DisplayColor::ActionExec => self.action_exec.0, DisplayColor::ActionFixup => self.action_fixup.0, + DisplayColor::ActionIndex => self.action_index.0, DisplayColor::ActionPick => self.action_pick.0, DisplayColor::ActionReword => self.action_reword.0, DisplayColor::ActionSquash => self.action_squash.0, @@ -427,6 +447,13 @@ mod tests { CrosstermColor::Magenta, CrosstermColor::AnsiValue(237) )] + #[case::action_index(DisplayColor::ActionIndex, false, CrosstermColor::DarkGreen, CrosstermColor::Reset)] + #[case::action_index_selected( + DisplayColor::ActionIndex, + true, + CrosstermColor::DarkGreen, + CrosstermColor::AnsiValue(237) + )] #[case::action_pick(DisplayColor::ActionPick, false, CrosstermColor::Green, CrosstermColor::Reset)] #[case::action_pick_selected( DisplayColor::ActionPick, diff --git a/src/display/display_color.rs b/src/display/display_color.rs index ae1fd4631..23bcd91b8 100644 --- a/src/display/display_color.rs +++ b/src/display/display_color.rs @@ -4,6 +4,8 @@ pub(crate) enum DisplayColor { /// The color for the break action. ActionBreak, + /// The color for the cut action. + ActionCut, /// The color for the drop action. ActionDrop, /// The color for the edit action. @@ -12,6 +14,8 @@ pub(crate) enum DisplayColor { ActionExec, /// The color for the fixup action. ActionFixup, + /// The color for the index action. + ActionIndex, /// The color for the pick action. ActionPick, /// The color for the reword action. diff --git a/src/input/key_bindings.rs b/src/input/key_bindings.rs index 550c82d5d..5a396a75a 100644 --- a/src/input/key_bindings.rs +++ b/src/input/key_bindings.rs @@ -40,12 +40,16 @@ pub(crate) struct KeyBindings { pub(crate) abort: Vec, /// Key bindings for the break action. pub(crate) action_break: Vec, + /// Key bindings for the cut action. + pub(crate) action_cut: Vec, /// Key bindings for the drop action. pub(crate) action_drop: Vec, /// Key bindings for the edit action. pub(crate) action_edit: Vec, /// Key bindings for the fixup action. pub(crate) action_fixup: Vec, + /// Key bindings for the index action. + pub(crate) action_index: Vec, /// Key bindings for the pick action. pub(crate) action_pick: Vec, /// Key bindings for the reword action. @@ -121,9 +125,11 @@ impl KeyBindings { search_previous: map_keybindings(&key_bindings.search_previous), abort: map_keybindings(&key_bindings.abort), action_break: map_keybindings(&key_bindings.action_break), + action_cut: map_keybindings(&key_bindings.action_cut), action_drop: map_keybindings(&key_bindings.action_drop), action_edit: map_keybindings(&key_bindings.action_edit), action_fixup: map_keybindings(&key_bindings.action_fixup), + action_index: map_keybindings(&key_bindings.action_index), action_pick: map_keybindings(&key_bindings.action_pick), action_reword: map_keybindings(&key_bindings.action_reword), action_squash: map_keybindings(&key_bindings.action_squash), diff --git a/src/input/standard_event.rs b/src/input/standard_event.rs index 59e659d29..e33e2756f 100644 --- a/src/input/standard_event.rs +++ b/src/input/standard_event.rs @@ -49,12 +49,16 @@ pub(crate) enum StandardEvent { ForceRebase, /// The break action meta event. ActionBreak, + /// The cut (git-revise) action meta event. + ActionCut, /// The drop action meta event. ActionDrop, /// The edit action meta event. ActionEdit, /// The fixup action meta event. ActionFixup, + /// The index (git-revise) action meta event. + ActionIndex, /// The pick action meta event. ActionPick, /// The reword action meta event. diff --git a/src/modules/confirm_abort.rs b/src/modules/confirm_abort.rs index c158d1d60..611e516bd 100644 --- a/src/modules/confirm_abort.rs +++ b/src/modules/confirm_abort.rs @@ -7,7 +7,7 @@ use crate::{ input::{Event, InputOptions, KeyBindings}, module::{ExitStatus, Module, State}, process::Results, - todo_file::TodoFile, + todo_file::{TodoFile, State as TodoFileState}, view::{RenderContext, ViewData}, }; @@ -34,7 +34,10 @@ impl Module for ConfirmAbort { let mut results = Results::new(); match confirmed { Confirmed::Yes => { - self.todo_file.lock().set_lines(vec![]); + let todo_state = self.todo_file.lock().state().clone(); + if todo_state != TodoFileState::Edit { + self.todo_file.lock().set_lines(vec![]); + } results.exit_status(ExitStatus::Good); }, Confirmed::No => { @@ -135,4 +138,24 @@ mod tests { }, ); } + + #[test] + fn handle_event_yes_in_edit() { + module_test( + &["pick aaa comment"], + &[Event::from(MetaEvent::Yes)], + |mut test_context| { + let mut todo_file = test_context.take_todo_file(); + todo_file.set_state(TodoFileState::Edit); + + let mut module = create_confirm_abort(todo_file); + assert_results!( + test_context.handle_event(&mut module), + Artifact::Event(Event::from(MetaEvent::Yes)), + Artifact::ExitStatus(ExitStatus::Good) + ); + assert!(!module.todo_file.lock().is_empty()); + }, + ); + } } diff --git a/src/modules/list.rs b/src/modules/list.rs index 88196b08a..943c9775b 100644 --- a/src/modules/list.rs +++ b/src/modules/list.rs @@ -554,9 +554,11 @@ impl List { match event { e if key_bindings.abort.contains(&e) => Event::from(StandardEvent::Abort), e if key_bindings.action_break.contains(&e) => Event::from(StandardEvent::ActionBreak), + e if key_bindings.action_cut.contains(&e) => Event::from(StandardEvent::ActionCut), e if key_bindings.action_drop.contains(&e) => Event::from(StandardEvent::ActionDrop), e if key_bindings.action_edit.contains(&e) => Event::from(StandardEvent::ActionEdit), e if key_bindings.action_fixup.contains(&e) => Event::from(StandardEvent::ActionFixup), + e if key_bindings.action_index.contains(&e) => Event::from(StandardEvent::ActionIndex), e if key_bindings.action_pick.contains(&e) => Event::from(StandardEvent::ActionPick), e if key_bindings.action_reword.contains(&e) => Event::from(StandardEvent::ActionReword), e if key_bindings.action_squash.contains(&e) => Event::from(StandardEvent::ActionSquash), @@ -642,9 +644,11 @@ impl List { Event::Standard(standard_event) => { match standard_event { StandardEvent::Abort => self.abort(&mut results), + StandardEvent::ActionCut => self.set_selected_line_action(Action::Cut), StandardEvent::ActionDrop => self.set_selected_line_action(Action::Drop), StandardEvent::ActionEdit => self.set_selected_line_action(Action::Edit), StandardEvent::ActionFixup => self.set_selected_line_action(Action::Fixup), + StandardEvent::ActionIndex => self.set_selected_line_action(Action::Index), StandardEvent::ActionPick => self.set_selected_line_action(Action::Pick), StandardEvent::ActionReword => self.set_selected_line_action(Action::Reword), StandardEvent::ActionSquash => self.set_selected_line_action(Action::Squash), diff --git a/src/modules/list/search.rs b/src/modules/list/search.rs index 3a362ac61..af593323a 100644 --- a/src/modules/list/search.rs +++ b/src/modules/list/search.rs @@ -59,9 +59,11 @@ impl Searchable for Search { let has_hash_match = match action { Action::Break | Action::Noop | Action::Label | Action::Reset | Action::Merge | Action::Exec => false, - Action::Drop + Action::Cut + | Action::Drop | Action::Edit | Action::Fixup + | Action::Index | Action::Pick | Action::Reword | Action::Squash @@ -69,9 +71,11 @@ impl Searchable for Search { }; let has_content_match = match action { Action::Break | Action::Noop => false, - Action::Drop + Action::Cut + | Action::Drop | Action::Edit | Action::Fixup + | Action::Index | Action::Pick | Action::Reword | Action::Squash diff --git a/src/modules/list/tests/help.rs b/src/modules/list/tests/help.rs index 78ec6ac11..41fb302f8 100644 --- a/src/modules/list/tests/help.rs +++ b/src/modules/list/tests/help.rs @@ -40,6 +40,8 @@ fn normal_mode_help() { " s |Set selected commits to be squashed", " f |Set selected commits to be fixed-up", " d |Set selected commits to be dropped", + " x |Set selected commits to be cut / split (git-revise)", + " i |Set selected commits to be staged in the index (git-revise)", " E |Edit an exec, label, reset or merge action's content", " I |Insert a new line", " Delete |Completely remove the selected lines", @@ -105,6 +107,8 @@ fn visual_mode_help() { " s |Set selected commits to be squashed", " f |Set selected commits to be fixed-up", " d |Set selected commits to be dropped", + " x |Set selected commits to be cut / split (git-revise)", + " i |Set selected commits to be staged in the index (git-revise)", " Delete |Completely remove the selected lines", " Controlz|Undo the last change", " Controly|Redo the previous undone change", diff --git a/src/modules/list/utils.rs b/src/modules/list/utils.rs index b938eac6e..1dc8fcf12 100644 --- a/src/modules/list/utils.rs +++ b/src/modules/list/utils.rs @@ -127,6 +127,16 @@ fn build_help_lines(key_bindings: &KeyBindings, selector: HelpLinesSelector) -> "Set selected commits to be dropped", HelpLinesSelector::Common, ), + ( + &key_bindings.action_cut, + "Set selected commits to be cut / split (git-revise)", + HelpLinesSelector::Common, + ), + ( + &key_bindings.action_index, + "Set selected commits to be staged in the index (git-revise)", + HelpLinesSelector::Common, + ), ( &key_bindings.edit, "Edit an exec, label, reset or merge action's content", @@ -185,10 +195,12 @@ pub(super) fn get_list_visual_mode_help_lines(key_bindings: &KeyBindings) -> Vec const fn get_action_color(action: Action) -> DisplayColor { match action { Action::Break => DisplayColor::ActionBreak, + Action::Cut => DisplayColor::ActionCut, Action::Drop => DisplayColor::ActionDrop, Action::Edit => DisplayColor::ActionEdit, Action::Exec => DisplayColor::ActionExec, Action::Fixup => DisplayColor::ActionFixup, + Action::Index => DisplayColor::ActionIndex, Action::Pick => DisplayColor::ActionPick, Action::Reword => DisplayColor::ActionReword, Action::Squash => DisplayColor::ActionSquash, @@ -208,8 +220,8 @@ pub(super) fn get_line_action_maximum_width(todo_file: &TodoFile) -> usize { let action_length = match line.get_action() { // allow these to overflow their bounds &Action::Exec | &Action::UpdateRef => 0, - &Action::Drop | &Action::Edit | &Action::Noop | &Action::Pick => 4, - &Action::Break | &Action::Label | &Action::Reset | &Action::Merge => 5, + &Action::Cut | &Action::Drop | &Action::Edit | &Action::Noop | &Action::Pick => 4, + &Action::Break | &Action::Label | &Action::Reset | &Action::Merge | &Action::Index => 5, &Action::Fixup => { if line.option().is_some() { 8 // "fixup -C" = 8 @@ -297,7 +309,7 @@ pub(super) fn get_todo_line_segments( // render hash match *action { - Action::Drop | Action::Edit | Action::Fixup | Action::Pick | Action::Reword | Action::Squash => { + Action::Cut | Action::Drop | Action::Edit | Action::Fixup | Action::Index | Action::Pick | Action::Reword | Action::Squash => { let action_width = if is_full_width { 8 } else { 3 }; let max_index = cmp::min(line.get_hash().len(), action_width); let search_hash_match = search_match.map_or(false, |m| m.hash()); diff --git a/src/test_helpers/with_todo_file.rs b/src/test_helpers/with_todo_file.rs index 10cf213de..2761753a9 100644 --- a/src/test_helpers/with_todo_file.rs +++ b/src/test_helpers/with_todo_file.rs @@ -1,14 +1,23 @@ //! Utilities for writing tests that interact with todo file use std::{ cell::RefCell, + io::Write, fmt::{Debug, Formatter}, - path::Path, + fs::File, + path::{Path, PathBuf}, }; -use tempfile::{Builder, NamedTempFile}; +use tempfile::{Builder, NamedTempFile, TempDir}; use crate::todo_file::{Line, TodoFile, TodoFileOptions}; +fn get_repo_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("fixtures") + .join("simple") +} + /// Context for `with_todo_file` pub(crate) struct TodoFileTestContext { todo_file: TodoFile, @@ -76,10 +85,7 @@ impl TodoFileTestContext { /// Will panic if a temporary file cannot be created pub(crate) fn with_todo_file(lines: &[&str], callback: C) where C: FnOnce(TodoFileTestContext) { - let git_repo_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("test") - .join("fixtures") - .join("simple"); + let git_repo_dir = get_repo_dir(); let git_todo_file = Builder::new() .prefix("git-rebase-todo-scratch") .suffix("") @@ -93,3 +99,136 @@ where C: FnOnce(TodoFileTestContext) { todo_file, }); } + + +/// Context for `with_todo_file` +pub struct TodoFileTestDirContext { + todo_file: TodoFile, + git_todo_dir: RefCell, + git_todo_file: RefCell, +} + +impl Debug for TodoFileTestDirContext { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TodoFileTestDirContext") + .field("todo_file", &self.todo_file) + .field("filepath", &self.todo_file.filepath) + .finish() + } +} + +impl TodoFileTestDirContext { + /// Return the path of the todo file + #[inline] + pub fn path(&self) -> String { + String::from(self.todo_file.filepath.to_str().unwrap_or_default()) + } + + /// Return the path of the todo dir + #[inline] + pub fn dir_path(&self) -> String { + String::from(self.git_todo_dir.borrow().path().to_str().unwrap_or_default()) + } + + /// Get the todo file instance + #[inline] + pub const fn todo_file(&self) -> &TodoFile { + &self.todo_file + } + + /// Get the todo file instance as mutable + #[inline] + pub fn todo_file_mut(&mut self) -> &mut TodoFile { + &mut self.todo_file + } + + /// Get the todo file instance + #[inline] + pub fn to_owned(self) -> (TempDir, TodoFile) { + (self.git_todo_dir.into_inner(), self.todo_file) + } + + /// Delete the path behind the todo dir + /// + /// # Panics + /// Will panic if the dir cannot be deleted for any reason + #[inline] + pub fn delete_dir(&self) { + self.git_todo_dir + .replace(Builder::new().tempdir().unwrap()) + .close() + .unwrap(); + } + + /// Set the path behind ot todo file as readonly + /// + /// # Panics + /// Will panic if the file permissions cannot be changed for any reason + #[inline] + pub fn set_file_readonly(&self) { + let git_todo_file = self.git_todo_file.borrow_mut(); + let mut permissions = git_todo_file.metadata().unwrap().permissions(); + permissions.set_readonly(true); + git_todo_file.set_permissions(permissions).unwrap(); + } +} + +/// Provide a `TodoFileTestDirContext` instance containing a `Todo` for use in tests. +/// +/// # Panics +/// Will panic if a temporary file cannot be created +#[inline] +fn with_todo_file_dir(prefix: &str, filename: &str, lines: &[&str], callback: C) +where C: FnOnce(TodoFileTestDirContext) { + let git_repo_dir = get_repo_dir(); + let git_todo_dir = Builder::new() + .prefix(prefix) + .suffix("") + .tempdir_in(git_repo_dir.as_path()) + .unwrap(); + let git_todo_file_path = git_todo_dir.path().join(filename); + let git_todo_file = File::create(git_todo_file_path.clone()).unwrap(); + + let mut todo_file = TodoFile::new(git_todo_file_path, TodoFileOptions::new(1, "#")); + todo_file.set_lines(lines.iter().map(|l| Line::parse(l).unwrap()).collect()); + callback(TodoFileTestDirContext { + git_todo_dir: RefCell::new(git_todo_dir), + todo_file, + git_todo_file: RefCell::new(git_todo_file), + }); +} + +/// Provide a `TodoFileTestDirContext` instance containing a `Todo` for use in tests. +/// +/// # Panics +/// Will panic if a temporary file cannot be created +#[inline] +pub fn with_todo_revise_file(lines: &[&str], callback: C) +where C: FnOnce(TodoFileTestDirContext) { + with_todo_file_dir("revise.", "git-revise-todo", lines, callback) +} + +/// Provide a `TodoFileTestDirContext` instance containing a `Todo` for use in tests. +/// +/// # Panics +/// Will panic if a temporary file cannot be created +#[inline] +pub fn with_todo_rebase_edit_file(lines: &[&str], callback: C) +where C: FnOnce(TodoFileTestDirContext) { + with_todo_file_dir("rebase-merge-scratch", "git-rebase-todo", lines, callback) +} + +/// Provide a `TodoFileTestDirContext` instance containing a `Todo`, with a stopped-sha +/// +/// # Panics +/// Will panic if a temporary file cannot be created +#[inline] +pub fn with_todo_rebase_edit_file_stopped(lines: &[&str], callback: C) +where C: FnOnce(TodoFileTestDirContext) { + with_todo_rebase_edit_file(lines, |context| { + let mut stopped_sha = File::create(Path::new(context.dir_path().as_str()).join("stopped-sha")).unwrap(); + writeln!(&mut stopped_sha, "this is a test file").unwrap(); + callback(context); + }); +} diff --git a/src/todo_file.rs b/src/todo_file.rs index a76001c50..3631d88eb 100644 --- a/src/todo_file.rs +++ b/src/todo_file.rs @@ -9,6 +9,7 @@ mod errors; mod history; mod line; mod line_parser; +mod state; mod todo_file_options; mod utils; @@ -19,6 +20,7 @@ use std::{ slice::Iter, }; +use state::detect_state; use version_track::Version; pub(crate) use self::{ @@ -27,6 +29,7 @@ pub(crate) use self::{ errors::ParseError, line::Line, line_parser::LineParser, + state::State, todo_file_options::TodoFileOptions, }; use self::{ @@ -48,6 +51,7 @@ pub(crate) struct TodoFile { options: TodoFileOptions, selected_line_index: usize, version: Version, + state: State, } impl TodoFile { @@ -64,6 +68,7 @@ impl TodoFile { options, selected_line_index: 0, version: Version::new(), + state: State::Initial, } } @@ -83,6 +88,12 @@ impl TodoFile { self.history.reset(); } + /// Set the rebase todo file state. + #[inline] + pub fn set_state(&mut self, state: State) { + self.state = state; + } + /// Load the rebase file from disk. /// /// # Errors @@ -112,6 +123,7 @@ impl TodoFile { }) .collect(); self.set_lines(lines?); + self.set_state(detect_state(&self.filepath)?); Ok(()) } @@ -140,9 +152,11 @@ impl TodoFile { match *action { Action::Break | Action::Noop => {}, - Action::Drop + Action::Cut + | Action::Drop | Action::Fixup | Action::Edit + | Action::Index | Action::Pick | Action::Reword | Action::Squash => { @@ -321,6 +335,13 @@ impl TodoFile { &self.version } + /// Get the current state + #[must_use] + #[inline] + pub const fn state(&self) -> &State { + &self.state + } + /// Get the selected line. #[must_use] pub(crate) fn get_selected_line(&self) -> Option<&Line> { diff --git a/src/todo_file/action.rs b/src/todo_file/action.rs index f57f47adc..3bd4a395b 100644 --- a/src/todo_file/action.rs +++ b/src/todo_file/action.rs @@ -8,6 +8,8 @@ use crate::todo_file::ParseError; pub(crate) enum Action { /// A break action. Break, + /// A cut action for git-revise. + Cut, /// A drop action. Drop, /// An edit action. @@ -16,6 +18,8 @@ pub(crate) enum Action { Exec, /// A fixup action. Fixup, + /// A index action for git-revise. + Index, /// A noop action. Noop, /// A pick action. @@ -41,9 +45,11 @@ impl Action { String::from(match self { Self::Break => "b", Self::Drop => "d", + Self::Cut => "c", Self::Edit => "e", Self::Exec => "x", Self::Fixup => "f", + Self::Index => "i", Self::Label => "l", Self::Merge => "m", Self::Noop => "n", @@ -60,7 +66,7 @@ impl Action { pub(crate) const fn is_static(self) -> bool { match self { Self::Break | Self::Exec | Self::Noop | Self::Reset | Self::Label | Self::Merge | Self::UpdateRef => true, - Self::Drop | Self::Edit | Self::Fixup | Self::Pick | Self::Reword | Self::Squash => false, + Self::Cut | Self::Drop | Self::Edit | Self::Index | Self::Fixup | Self::Pick | Self::Reword | Self::Squash => false, } } } @@ -69,10 +75,12 @@ impl Display for Action { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match *self { Self::Break => "break", + Self::Cut => "cut", Self::Drop => "drop", Self::Edit => "edit", Self::Exec => "exec", Self::Fixup => "fixup", + Self::Index => "index", Self::Label => "label", Self::Merge => "merge", Self::Noop => "noop", @@ -91,10 +99,12 @@ impl TryFrom<&str> for Action { fn try_from(s: &str) -> Result { match s { "break" | "b" => Ok(Self::Break), + // "cut" => Ok(Self::Cut), "drop" | "d" => Ok(Self::Drop), "edit" | "e" => Ok(Self::Edit), "exec" | "x" => Ok(Self::Exec), "fixup" | "f" => Ok(Self::Fixup), + "index" => Ok(Self::Index), "noop" | "n" => Ok(Self::Noop), "pick" | "p" => Ok(Self::Pick), "reword" | "r" => Ok(Self::Reword), diff --git a/src/todo_file/errors/io.rs b/src/todo_file/errors/io.rs index 799eddeb0..c1c8c2be5 100644 --- a/src/todo_file/errors/io.rs +++ b/src/todo_file/errors/io.rs @@ -15,6 +15,9 @@ pub(crate) enum FileReadErrorCause { /// Caused by a parse error #[error(transparent)] ParseError(#[from] ParseError), + /// Caused by the file path returning None for parent() + #[error("NoParentDir")] + NoParentDir(), } impl PartialEq for FileReadErrorCause { @@ -23,6 +26,7 @@ impl PartialEq for FileReadErrorCause { match (self, other) { (Self::IoError(self_err), Self::IoError(other_err)) => self_err.kind() == other_err.kind(), (Self::ParseError(self_err), Self::ParseError(other_err)) => self_err == other_err, + (Self::NoParentDir(), Self::NoParentDir()) => true, _ => false, } } diff --git a/src/todo_file/line.rs b/src/todo_file/line.rs index 56389f4e3..eeb234dac 100644 --- a/src/todo_file/line.rs +++ b/src/todo_file/line.rs @@ -95,7 +95,7 @@ impl Line { Ok(match action { Action::Noop => Self::new_noop(), Action::Break => Self::new_break(), - Action::Pick | Action::Reword | Action::Edit | Action::Squash | Action::Drop => { + Action::Pick | Action::Reword | Action::Edit | Action::Squash | Action::Drop | Action::Cut | Action::Index => { Self::new(action, line_parser.next()?, line_parser.take_remaining(), None) }, Action::Fixup => { @@ -190,8 +190,9 @@ impl Line { #[must_use] pub(crate) const fn is_editable(&self) -> bool { match self.action { - Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => true, + Action::Exec | Action::Index | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => true, Action::Break + | Action::Cut | Action::Drop | Action::Edit | Action::Fixup @@ -212,7 +213,7 @@ impl Line { #[must_use] pub(crate) fn to_text(&self) -> String { match self.action { - Action::Drop | Action::Edit | Action::Fixup | Action::Pick | Action::Reword | Action::Squash => { + Action::Cut | Action::Drop | Action::Edit | Action::Fixup | Action::Index | Action::Pick | Action::Reword | Action::Squash => { if let Some(opt) = self.option.as_ref() { format!("{} {opt} {} {}", self.action, self.hash, self.content) } diff --git a/src/todo_file/state.rs b/src/todo_file/state.rs new file mode 100644 index 000000000..006ff66b5 --- /dev/null +++ b/src/todo_file/state.rs @@ -0,0 +1,82 @@ +use std::{ + fmt::{Display, Formatter}, + path::{Path, PathBuf}, +}; + +use crate::todo_file::{FileReadErrorCause, IoError}; + +/// Describes the state of rebase when editing the rebase todo file. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[allow(clippy::exhaustive_enums)] +pub enum State { + /// Editing todo at start of a rebase. + Initial, + /// Editing todo in the middle of a rebase with --edit. + Edit, + /// Editing the todo file for git-revise + Revise, +} + +pub(crate) fn detect_state(filepath: &Path) -> Result { + if filepath.ends_with("git-revise-todo") { + return Ok(State::Revise); + } + if let Some(parent) = filepath.parent() { + if parent.join("stopped-sha").try_exists().map_err(|err| { + IoError::FileRead { + file: PathBuf::from(parent), + cause: FileReadErrorCause::from(err), + } + })? { + return Ok(State::Edit); + } + } + return Ok(State::Initial); +} + +impl State {} + +impl Display for State { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match *self { + Self::Initial => "initial", + Self::Edit => "edit", + Self::Revise => "revise", + }) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + use crate::testutil::{with_todo_revise_file, TodoFileTestDirContext, with_todo_rebase_edit_file, with_todo_rebase_edit_file_stopped}; + + #[rstest] + #[case::edit(State::Initial, "initial")] + #[case::edit(State::Edit, "edit")] + #[case::edit(State::Revise, "revise")] + fn to_string(#[case] action: State, #[case] expected: &str) { + assert_eq!(format!("{action}"), expected); + } + + #[rstest] + #[case::edit(State::Initial)] + #[case::edit(State::Edit)] + #[case::edit(State::Revise)] + fn detect(#[case] expected: State) { + let check = |context: TodoFileTestDirContext| { + assert_eq!( + detect_state(context.todo_file().filepath.as_path()).unwrap(), + expected + ) + }; + match expected { + State::Initial => with_todo_rebase_edit_file(&[], check), + State::Edit => with_todo_rebase_edit_file_stopped(&[], check), + State::Revise => with_todo_revise_file(&[], check), + } + } +}