• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MitMaro / git-interactive-rebase-tool / 13798245227

11 Mar 2025 09:09PM UTC coverage: 97.311% (-0.01%) from 97.325%
13798245227

push

github

MitMaro
Fix ordering in build.rs

4596 of 4723 relevant lines covered (97.31%)

2.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.8
/src/modules/external_editor.rs
1
mod action;
2
mod argument_tokenizer;
3
mod external_editor_state;
4

5
#[cfg(all(unix, test))]
6
mod tests;
7

8
use std::sync::{Arc, LazyLock};
9

10
use anyhow::{Result, anyhow};
11
use parking_lot::Mutex;
12

13
use self::{action::Action, argument_tokenizer::tokenize, external_editor_state::ExternalEditorState};
14
use crate::{
15
        components::choice::{Choice, INPUT_OPTIONS as CHOICE_INPUT_OPTIONS},
16
        input::{Event, InputOptions, StandardEvent},
17
        module::{ExitStatus, Module, State},
18
        process::Results,
19
        todo_file::{Line, TodoFile},
20
        view::{RenderContext, ViewData, ViewLine},
21
};
22

23
static INPUT_OPTIONS: LazyLock<InputOptions> = LazyLock::new(|| InputOptions::RESIZE);
2✔
24

25
pub(crate) struct ExternalEditor {
26
        editor: String,
27
        empty_choice: Choice<Action>,
28
        error_choice: Choice<Action>,
29
        external_command: (String, Vec<String>),
30
        lines: Vec<Line>,
31
        state: ExternalEditorState,
32
        todo_file: Arc<Mutex<TodoFile>>,
33
        view_data: ViewData,
34
}
35

36
impl Module for ExternalEditor {
37
        fn activate(&mut self, _: State) -> Results {
1✔
38
                let todo_file = self.todo_file.lock();
1✔
39
                let mut results = Results::new();
1✔
40
                if let Err(err) = todo_file.write_file() {
2✔
41
                        results.error_with_return(err.into(), State::List);
2✔
42
                        return results;
1✔
43
                }
44

45
                if self.lines.is_empty() {
2✔
46
                        self.lines = todo_file.get_lines_owned();
1✔
47
                }
48
                drop(todo_file);
1✔
49
                match self.get_command() {
1✔
50
                        Ok(external_command) => self.external_command = external_command,
1✔
51
                        Err(err) => {
1✔
52
                                results.error_with_return(err, State::List);
1✔
53
                                return results;
1✔
54
                        },
55
                }
56

57
                self.set_state(&mut results, ExternalEditorState::Active);
1✔
58
                results
1✔
59
        }
60

61
        fn deactivate(&mut self) -> Results {
1✔
62
                self.lines.clear();
1✔
63
                self.view_data.update_view_data(|updater| updater.clear());
3✔
64
                Results::new()
1✔
65
        }
66

67
        fn build_view_data(&mut self, _: &RenderContext) -> &ViewData {
1✔
68
                match self.state {
2✔
69
                        ExternalEditorState::Active => {
70
                                self.view_data.update_view_data(|updater| {
2✔
71
                                        updater.clear();
1✔
72
                                        updater.push_leading_line(ViewLine::from("Editing..."));
1✔
73
                                });
74
                                &self.view_data
1✔
75
                        },
76
                        ExternalEditorState::Empty => self.empty_choice.get_view_data(),
1✔
77
                        ExternalEditorState::Error(ref error) => {
1✔
78
                                self.error_choice
2✔
79
                                        .set_prompt(error.chain().map(|c| ViewLine::from(format!("{c:#}"))).collect());
3✔
80
                                self.error_choice.get_view_data()
1✔
81
                        },
82
                }
83
        }
84

85
        fn input_options(&self) -> &InputOptions {
1✔
86
                match self.state {
3✔
87
                        ExternalEditorState::Active => &INPUT_OPTIONS,
1✔
88
                        ExternalEditorState::Empty | ExternalEditorState::Error(_) => &CHOICE_INPUT_OPTIONS,
1✔
89
                }
90
        }
91

92
        fn handle_event(&mut self, event: Event, view_state: &crate::view::State) -> Results {
2✔
93
                let mut results = Results::new();
2✔
94
                match self.state {
2✔
95
                        ExternalEditorState::Active => {
96
                                match event {
3✔
97
                                        Event::Standard(StandardEvent::ExternalCommandSuccess) => {
98
                                                let mut todo_file = self.todo_file.lock();
2✔
99
                                                let result = todo_file.load_file();
2✔
100
                                                let state = match result {
2✔
101
                                                        Ok(()) => {
102
                                                                if todo_file.is_empty() || todo_file.is_noop() {
2✔
103
                                                                        Some(ExternalEditorState::Empty)
1✔
104
                                                                }
105
                                                                else {
106
                                                                        results.state(State::List);
1✔
107
                                                                        None
1✔
108
                                                                }
109
                                                        },
110
                                                        Err(e) => Some(ExternalEditorState::Error(e.into())),
2✔
111
                                                };
112

113
                                                drop(todo_file);
1✔
114

115
                                                if let Some(new_state) = state {
2✔
116
                                                        self.set_state(&mut results, new_state);
2✔
117
                                                }
118
                                        },
119
                                        Event::Standard(StandardEvent::ExternalCommandError) => {
120
                                                self.set_state(
1✔
121
                                                        &mut results,
122
                                                        ExternalEditorState::Error(anyhow!("Editor returned a non-zero exit status")),
2✔
123
                                                );
124
                                        },
125
                                        _ => {},
126
                                }
127
                        },
128
                        ExternalEditorState::Empty => {
129
                                let choice = self.empty_choice.handle_event(event, view_state);
2✔
130
                                if let Some(action) = choice {
1✔
131
                                        match *action {
1✔
132
                                                Action::AbortRebase => results.exit_status(ExitStatus::Good),
2✔
133
                                                Action::EditRebase => self.set_state(&mut results, ExternalEditorState::Active),
2✔
134
                                                Action::UndoAndEdit => {
135
                                                        self.undo_and_edit(&mut results);
2✔
136
                                                },
137
                                                Action::RestoreAndAbortEdit => {},
138
                                        }
139
                                }
140
                        },
141
                        ExternalEditorState::Error(_) => {
142
                                let choice = self.error_choice.handle_event(event, view_state);
4✔
143
                                if let Some(action) = choice {
2✔
144
                                        match *action {
2✔
145
                                                Action::AbortRebase => {
146
                                                        self.todo_file.lock().set_lines(vec![]);
2✔
147
                                                        results.exit_status(ExitStatus::Good);
1✔
148
                                                },
149
                                                Action::EditRebase => self.set_state(&mut results, ExternalEditorState::Active),
2✔
150
                                                Action::RestoreAndAbortEdit => {
151
                                                        let mut todo_file = self.todo_file.lock();
2✔
152
                                                        todo_file.set_lines(self.lines.clone());
2✔
153
                                                        results.state(State::List);
1✔
154
                                                        if let Err(err) = todo_file.write_file() {
1✔
155
                                                                results.error(err.into());
×
156
                                                        }
157
                                                },
158
                                                Action::UndoAndEdit => {
159
                                                        self.undo_and_edit(&mut results);
2✔
160
                                                },
161
                                        }
162
                                }
163
                        },
164
                }
165
                results
1✔
166
        }
167
}
168

169
impl ExternalEditor {
170
        pub(crate) fn new(editor: &str, todo_file: Arc<Mutex<TodoFile>>) -> Self {
1✔
171
                let view_data = ViewData::new(|updater| {
2✔
172
                        updater.set_show_title(true);
1✔
173
                });
174

175
                let mut empty_choice = Choice::new(vec![
3✔
176
                        (Action::AbortRebase, '1', String::from("Abort rebase")),
2✔
177
                        (Action::EditRebase, '2', String::from("Edit rebase file")),
2✔
178
                        (
179
                                Action::UndoAndEdit,
1✔
180
                                '3',
181
                                String::from("Undo modifications and edit rebase file"),
1✔
182
                        ),
183
                ]);
184
                empty_choice.set_prompt(vec![ViewLine::from("The rebase file is empty.")]);
2✔
185

186
                let error_choice = Choice::new(vec![
2✔
187
                        (Action::AbortRebase, '1', String::from("Abort rebase")),
2✔
188
                        (Action::EditRebase, '2', String::from("Edit rebase file")),
2✔
189
                        (
190
                                Action::RestoreAndAbortEdit,
1✔
191
                                '3',
192
                                String::from("Restore rebase file and abort edit"),
1✔
193
                        ),
194
                        (
195
                                Action::UndoAndEdit,
1✔
196
                                '4',
197
                                String::from("Undo modifications and edit rebase file"),
1✔
198
                        ),
199
                ]);
200

201
                Self {
202
                        editor: String::from(editor),
1✔
203
                        empty_choice,
204
                        error_choice,
205
                        external_command: (String::new(), vec![]),
2✔
206
                        lines: vec![],
1✔
207
                        state: ExternalEditorState::Active,
208
                        todo_file,
209
                        view_data,
210
                }
211
        }
212

213
        fn set_state(&mut self, results: &mut Results, new_state: ExternalEditorState) {
1✔
214
                self.state = new_state;
1✔
215
                match self.state {
1✔
216
                        ExternalEditorState::Active => {
1✔
217
                                results.external_command(self.external_command.0.clone(), self.external_command.1.clone());
1✔
218
                        },
219
                        ExternalEditorState::Empty | ExternalEditorState::Error(_) => {},
220
                }
221
        }
222

223
        fn undo_and_edit(&mut self, results: &mut Results) {
1✔
224
                let mut todo_file = self.todo_file.lock();
1✔
225
                todo_file.set_lines(self.lines.clone());
2✔
226
                if let Err(err) = todo_file.write_file() {
1✔
227
                        results.error_with_return(err.into(), State::List);
×
228
                        return;
229
                }
230
                drop(todo_file);
1✔
231
                self.set_state(results, ExternalEditorState::Active);
1✔
232
        }
233

234
        fn get_command(&self) -> Result<(String, Vec<String>)> {
1✔
235
                let mut parameters = tokenize(self.editor.as_str())
4✔
236
                        .map_or(Err(anyhow!("Invalid editor: \"{}\"", self.editor)), |args| {
3✔
237
                                if args.is_empty() {
3✔
238
                                        Err(anyhow!("No editor configured"))
2✔
239
                                }
240
                                else {
241
                                        Ok(args.into_iter())
2✔
242
                                }
243
                        })
244
                        .map_err(|e| anyhow!("Please see the git \"core.editor\" configuration for details").context(e))?;
3✔
245

246
                let todo_file = self.todo_file.lock();
2✔
247
                let filepath = todo_file.get_filepath().to_str().ok_or_else(|| {
2✔
248
                        anyhow!(
×
249
                                "The file path {} is invalid",
250
                                todo_file.get_filepath().to_string_lossy()
×
251
                        )
252
                })?;
253
                let mut file_pattern_found = false;
1✔
254
                let command = parameters.next().unwrap_or_else(|| String::from("false"));
1✔
255
                let mut arguments = parameters
2✔
256
                        .map(|a| {
1✔
257
                                if a.as_str() == "%" {
3✔
258
                                        file_pattern_found = true;
1✔
259
                                        String::from(filepath)
2✔
260
                                }
261
                                else {
262
                                        a
1✔
263
                                }
264
                        })
265
                        .collect::<Vec<String>>();
266
                if !file_pattern_found {
1✔
267
                        arguments.push(String::from(filepath));
2✔
268
                }
269
                Ok((command, arguments))
1✔
270
        }
271
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc