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

MitMaro / git-interactive-rebase-tool / 9285761961

29 May 2024 12:07PM UTC coverage: 97.254% (+0.03%) from 97.22%
9285761961

push

github

MitMaro
Fix `d` keybinding description

4568 of 4697 relevant lines covered (97.25%)

2.75 hits per line

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

89.22
/src/application.rs
1
use std::sync::Arc;
2

3
use anyhow::Result;
4
use parking_lot::Mutex;
5

6
use crate::{
7
        config::Config,
8
        display::Display,
9
        git::Repository,
10
        help::build_help,
11
        input::{Event, EventHandler, EventReaderFn, KeyBindings, StandardEvent},
12
        module::{self, ExitStatus, ModuleHandler},
13
        process::{self, Process},
14
        runtime::{Runtime, ThreadStatuses, Threadable},
15
        search,
16
        todo_file::{TodoFile, TodoFileOptions},
17
        view::View,
18
        Args,
19
        Exit,
20
};
21

22
pub(crate) struct Application<ModuleProvider>
23
where ModuleProvider: module::ModuleProvider + Send + 'static
24
{
25
        _config: Config,
26
        _repository: Repository,
27
        process: Process<ModuleProvider>,
28
        threads: Option<Vec<Box<dyn Threadable>>>,
29
        thread_statuses: ThreadStatuses,
30
}
31

32
impl<ModuleProvider> Application<ModuleProvider>
33
where ModuleProvider: module::ModuleProvider + Send + 'static
34
{
35
        pub(crate) fn new<EventProvider, Tui>(args: &Args, event_provider: EventProvider, tui: Tui) -> Result<Self, Exit>
11✔
36
        where
37
                EventProvider: EventReaderFn,
38
                Tui: crate::display::Tui + Send + 'static,
39
        {
40
                let filepath = Self::filepath_from_args(args)?;
24✔
41
                let repository = Self::open_repository()?;
22✔
42
                let config = Self::load_config(&repository)?;
20✔
43
                let todo_file = Arc::new(Mutex::new(Self::load_todo_file(filepath.as_str(), &config)?));
16✔
44

45
                let module_handler = ModuleHandler::new(
46
                        EventHandler::new(KeyBindings::new(&config.key_bindings)),
8✔
47
                        ModuleProvider::new(&config, repository.clone(), &todo_file),
8✔
48
                );
49

50
                let display = Display::new(tui, &config.theme);
8✔
51
                let initial_display_size = display.get_window_size();
8✔
52
                let view = View::new(
53
                        display,
4✔
54
                        config.theme.character_vertical_spacing.as_str(),
4✔
55
                        config
4✔
56
                                .key_bindings
×
57
                                .help
×
58
                                .first()
×
59
                                .map_or_else(|| String::from("?"), String::from)
×
60
                                .as_str(),
×
61
                );
62

63
                let thread_statuses = ThreadStatuses::new();
4✔
64
                let mut threads: Vec<Box<dyn Threadable>> = vec![];
4✔
65

66
                let input_threads = crate::input::Thread::new(event_provider);
8✔
67
                let input_state = input_threads.state();
8✔
68
                threads.push(Box::new(input_threads));
8✔
69

70
                let view_threads = crate::view::Thread::new(view);
4✔
71
                let view_state = view_threads.state();
8✔
72
                threads.push(Box::new(view_threads));
8✔
73

74
                let search_update_handler = Self::create_search_update_handler(input_state.clone());
4✔
75
                let search_threads = search::Thread::new(search_update_handler);
4✔
76
                let search_state = search_threads.state();
8✔
77
                threads.push(Box::new(search_threads));
8✔
78

79
                let process = Process::new(
80
                        initial_display_size,
×
81
                        todo_file,
4✔
82
                        module_handler,
4✔
83
                        input_state,
4✔
84
                        view_state,
4✔
85
                        search_state,
4✔
86
                        thread_statuses.clone(),
4✔
87
                );
88
                let process_threads = process::Thread::new(process.clone());
8✔
89
                threads.push(Box::new(process_threads));
4✔
90

91
                Ok(Self {
4✔
92
                        _config: config,
4✔
93
                        _repository: repository,
4✔
94
                        process,
4✔
95
                        threads: Some(threads),
4✔
96
                        thread_statuses,
4✔
97
                })
98
        }
99

100
        pub(crate) fn run_until_finished(&mut self) -> Result<(), Exit> {
1✔
101
                let Some(mut threads) = self.threads.take()
1✔
102
                else {
×
103
                        return Err(Exit::new(
1✔
104
                                ExitStatus::StateError,
1✔
105
                                "Attempt made to run application a second time",
×
106
                        ));
107
                };
108

109
                let runtime = Runtime::new(self.thread_statuses.clone());
2✔
110

111
                for thread in &mut threads {
3✔
112
                        runtime.register(thread.as_mut());
2✔
113
                }
114

115
                runtime.join().map_err(|err| {
6✔
116
                        Exit::new(
1✔
117
                                ExitStatus::StateError,
1✔
118
                                format!("Failed to join runtime: {err}").as_str(),
1✔
119
                        )
120
                })?;
121

122
                let exit_status = self.process.exit_status();
2✔
123
                if exit_status != ExitStatus::Good {
1✔
124
                        return Err(Exit::from(exit_status));
2✔
125
                }
126

127
                Ok(())
1✔
128
        }
129

130
        fn filepath_from_args(args: &Args) -> Result<String, Exit> {
2✔
131
                args.todo_file_path().as_ref().map(String::from).ok_or_else(|| {
3✔
132
                        Exit::new(
1✔
133
                                ExitStatus::StateError,
1✔
134
                                build_help(Some(String::from("A todo file path must be provided."))).as_str(),
1✔
135
                        )
136
                })
137
        }
138

139
        fn open_repository() -> Result<Repository, Exit> {
2✔
140
                Repository::open_from_env().map_err(|err| {
3✔
141
                        return Exit::new(
1✔
142
                                ExitStatus::StateError,
1✔
143
                                format!("Unable to load Git repository: {err}").as_str(),
1✔
144
                        );
145
                })
146
        }
147

148
        fn load_config(repo: &Repository) -> Result<Config, Exit> {
2✔
149
                Config::try_from(repo).map_err(|err| Exit::new(ExitStatus::ConfigError, format!("{err:#}").as_str()))
5✔
150
        }
151

152
        fn todo_file_options(config: &Config) -> TodoFileOptions {
2✔
153
                let mut todo_file_options = TodoFileOptions::new(config.undo_limit, config.git.comment_char.as_str());
2✔
154
                if let Some(command) = config.post_modified_line_exec_command.as_deref() {
4✔
155
                        todo_file_options.line_changed_command(command);
2✔
156
                }
157
                todo_file_options
2✔
158
        }
159

160
        fn load_todo_file(filepath: &str, config: &Config) -> Result<TodoFile, Exit> {
2✔
161
                let mut todo_file = TodoFile::new(filepath, Self::todo_file_options(config));
2✔
162
                todo_file
10✔
163
                        .load_file()
164
                        .map_err(|err| Exit::new(ExitStatus::FileReadError, err.to_string().as_str()))?;
6✔
165

166
                if todo_file.is_noop() {
4✔
167
                        return Err(Exit::new(
2✔
168
                                ExitStatus::Good,
1✔
169
                                "A noop rebase was provided, skipping editing",
×
170
                        ));
171
                }
172

173
                if todo_file.is_empty() {
4✔
174
                        return Err(Exit::new(
4✔
175
                                ExitStatus::Good,
2✔
176
                                "An empty rebase was provided, nothing to edit",
×
177
                        ));
178
                }
179

180
                Ok(todo_file)
1✔
181
        }
182

183
        fn create_search_update_handler(input_state: crate::input::State) -> impl Fn() + Send + Sync {
2✔
184
                move || input_state.push_event(Event::Standard(StandardEvent::SearchUpdate))
4✔
185
        }
186
}
187

188
#[cfg(all(unix, test))]
189
mod tests {
190
        use std::ffi::OsString;
191

192
        use claims::assert_ok;
193

194
        use super::*;
195
        use crate::{
196
                display::Size,
197
                input::{KeyCode, KeyEvent, KeyModifiers},
198
                module::Modules,
199
                runtime::{Installer, RuntimeError},
200
                test_helpers::{
201
                        create_config,
202
                        create_event_reader,
203
                        mocks,
204
                        set_git_directory,
205
                        DefaultTestModule,
206
                        TestModuleProvider,
207
                },
208
        };
209

210
        fn args(args: &[&str]) -> Args {
1✔
211
                Args::try_from(args.iter().map(OsString::from).collect::<Vec<OsString>>()).unwrap()
1✔
212
        }
213

214
        fn create_mocked_crossterm() -> mocks::CrossTerm {
1✔
215
                let mut crossterm = mocks::CrossTerm::new();
1✔
216
                crossterm.set_size(Size::new(300, 120));
2✔
217
                crossterm
1✔
218
        }
219

220
        macro_rules! application_error {
221
                ($app:expr) => {
222
                        if let Err(e) = $app {
6✔
223
                                e
6✔
224
                        }
225
                        else {
226
                                panic!("Application is not in an error state");
×
227
                        }
228
                };
229
        }
230

231
        #[test]
232
        #[serial_test::serial]
233
        fn load_filepath_from_args_failure() {
234
                let event_provider = create_event_reader(|| Ok(None));
235
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
236
                        Application::new(&args(&[]), event_provider, create_mocked_crossterm());
237
                let exit = application_error!(application);
238
                assert_eq!(exit.get_status(), &ExitStatus::StateError);
239
                assert!(
240
                        exit.get_message()
241
                                .as_ref()
242
                                .unwrap()
243
                                .contains("A todo file path must be provided")
244
                );
245
        }
246

247
        #[test]
248
        #[serial_test::serial]
249
        fn load_repository_failure() {
250
                _ = set_git_directory("fixtures/not-a-repository");
251
                let event_provider = create_event_reader(|| Ok(None));
252
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
253
                        Application::new(&args(&["todofile"]), event_provider, create_mocked_crossterm());
254
                let exit = application_error!(application);
255
                assert_eq!(exit.get_status(), &ExitStatus::StateError);
256
                assert!(
257
                        exit.get_message()
258
                                .as_ref()
259
                                .unwrap()
260
                                .contains("Unable to load Git repository: ")
261
                );
262
        }
263

264
        #[test]
265
        #[serial_test::serial]
266
        fn load_config_failure() {
267
                _ = set_git_directory("fixtures/invalid-config");
268
                let event_provider = create_event_reader(|| Ok(None));
269
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
270
                        Application::new(&args(&["rebase-todo"]), event_provider, create_mocked_crossterm());
271
                let exit = application_error!(application);
272
                assert_eq!(exit.get_status(), &ExitStatus::ConfigError);
273
        }
274

275
        #[test]
276
        fn todo_file_options_without_command() {
277
                let mut config = create_config();
278
                config.undo_limit = 10;
279
                config.git.comment_char = String::from("#");
280
                config.post_modified_line_exec_command = None;
281

282
                let expected = TodoFileOptions::new(10, "#");
283
                assert_eq!(
284
                        Application::<TestModuleProvider<DefaultTestModule>>::todo_file_options(&config),
285
                        expected
286
                );
287
        }
288

289
        #[test]
290
        fn todo_file_options_with_command() {
291
                let mut config = create_config();
292
                config.undo_limit = 10;
293
                config.git.comment_char = String::from("#");
294
                config.post_modified_line_exec_command = Some(String::from("command"));
295

296
                let mut expected = TodoFileOptions::new(10, "#");
297
                expected.line_changed_command("command");
298

299
                assert_eq!(
300
                        Application::<TestModuleProvider<DefaultTestModule>>::todo_file_options(&config),
301
                        expected
302
                );
303
        }
304

305
        #[test]
306
        #[serial_test::serial]
307
        fn load_todo_file_load_error() {
308
                _ = set_git_directory("fixtures/simple");
309
                let event_provider = create_event_reader(|| Ok(None));
310
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
311
                        Application::new(&args(&["does-not-exist"]), event_provider, create_mocked_crossterm());
312
                let exit = application_error!(application);
313
                assert_eq!(exit.get_status(), &ExitStatus::FileReadError);
314
        }
315

316
        #[test]
317
        #[serial_test::serial]
318
        fn load_todo_file_noop() {
319
                let git_dir = set_git_directory("fixtures/simple");
320
                let rebase_todo = format!("{git_dir}/rebase-todo-noop");
321
                let event_provider = create_event_reader(|| Ok(None));
322
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
323
                        &args(&[rebase_todo.as_str()]),
324
                        event_provider,
325
                        create_mocked_crossterm(),
326
                );
327
                let exit = application_error!(application);
328
                assert_eq!(exit.get_status(), &ExitStatus::Good);
329
        }
330

331
        #[test]
332
        #[serial_test::serial]
333
        fn load_todo_file_empty() {
334
                let git_dir = set_git_directory("fixtures/simple");
335
                let rebase_todo = format!("{git_dir}/rebase-todo-empty");
336
                let event_provider = create_event_reader(|| Ok(None));
337
                let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
338
                        &args(&[rebase_todo.as_str()]),
339
                        event_provider,
340
                        create_mocked_crossterm(),
341
                );
342
                let exit = application_error!(application);
343
                assert_eq!(exit.get_status(), &ExitStatus::Good);
344
                assert!(
345
                        exit.get_message()
346
                                .as_ref()
347
                                .unwrap()
348
                                .contains("An empty rebase was provided, nothing to edit")
349
                );
350
        }
351

352
        #[test]
353
        #[serial_test::serial]
354
        fn search_update_handler_handles_update() {
355
                let event_provider = create_event_reader(|| Ok(None));
356
                let input_threads = crate::input::Thread::new(event_provider);
357
                let input_state = input_threads.state();
358
                let update_handler =
359
                        Application::<TestModuleProvider<DefaultTestModule>>::create_search_update_handler(input_state.clone());
360
                update_handler();
361

362
                assert_eq!(input_state.read_event(), Event::Standard(StandardEvent::SearchUpdate));
363
        }
364

365
        #[test]
366
        #[serial_test::serial]
367
        fn run_until_finished_success() {
368
                let git_dir = set_git_directory("fixtures/simple");
369
                let rebase_todo = format!("{git_dir}/rebase-todo");
370
                let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
371
                let mut application: Application<Modules> = Application::new(
372
                        &args(&[rebase_todo.as_str()]),
373
                        event_provider,
374
                        create_mocked_crossterm(),
375
                )
376
                .unwrap();
377
                assert_ok!(application.run_until_finished());
378
        }
379

380
        #[test]
381
        #[serial_test::serial]
382
        fn run_join_error() {
383
                struct FailingThread;
384
                impl Threadable for FailingThread {
385
                        fn install(&self, installer: &Installer) {
386
                                installer.spawn("THREAD", |notifier| {
387
                                        move || {
388
                                                notifier.error(RuntimeError::ThreadSpawnError(String::from("Error")));
389
                                        }
390
                                });
391
                        }
392
                }
393

394
                let git_dir = set_git_directory("fixtures/simple");
395
                let rebase_todo = format!("{git_dir}/rebase-todo");
396
                let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
397
                let mut application: Application<Modules> = Application::new(
398
                        &args(&[rebase_todo.as_str()]),
399
                        event_provider,
400
                        create_mocked_crossterm(),
401
                )
402
                .unwrap();
403

404
                application.threads = Some(vec![Box::new(FailingThread {})]);
405

406
                let exit = application.run_until_finished().unwrap_err();
407
                assert_eq!(exit.get_status(), &ExitStatus::StateError);
408
                assert!(
409
                        exit.get_message()
410
                                .as_ref()
411
                                .unwrap()
412
                                .starts_with("Failed to join runtime:")
413
                );
414
        }
415

416
        #[test]
417
        #[serial_test::serial]
418
        fn run_until_finished_kill() {
419
                let git_dir = set_git_directory("fixtures/simple");
420
                let rebase_todo = format!("{git_dir}/rebase-todo");
421
                let event_provider = create_event_reader(|| {
422
                        Ok(Some(Event::Key(KeyEvent::new(
423
                                KeyCode::Char('c'),
424
                                KeyModifiers::CONTROL,
425
                        ))))
426
                });
427
                let mut application: Application<Modules> = Application::new(
428
                        &args(&[rebase_todo.as_str()]),
429
                        event_provider,
430
                        create_mocked_crossterm(),
431
                )
432
                .unwrap();
433
                let exit = application.run_until_finished().unwrap_err();
434
                assert_eq!(exit.get_status(), &ExitStatus::Kill);
435
        }
436

437
        #[test]
438
        #[serial_test::serial]
439
        fn run_error_on_second_attempt() {
440
                let git_dir = set_git_directory("fixtures/simple");
441
                let rebase_todo = format!("{git_dir}/rebase-todo");
442
                let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
443
                let mut application: Application<Modules> = Application::new(
444
                        &args(&[rebase_todo.as_str()]),
445
                        event_provider,
446
                        create_mocked_crossterm(),
447
                )
448
                .unwrap();
449
                assert_ok!(application.run_until_finished());
450
                let exit = application.run_until_finished().unwrap_err();
451
                assert_eq!(exit.get_status(), &ExitStatus::StateError);
452
                assert_eq!(
453
                        exit.get_message().as_ref().unwrap(),
454
                        "Attempt made to run application a second time"
455
                );
456
        }
457
}
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