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

MitMaro / git-interactive-rebase-tool / 6883077488

15 Nov 2023 09:23PM CUT coverage: 93.248% (-0.4%) from 93.64%
6883077488

Pull #873

github

web-flow
Merge 0ab516642 into d7655157f
Pull Request #873: When editing in the middle of a rebase, dont clear on quit

45 of 72 new or added lines in 14 files covered. (62.5%)

1 existing line in 1 file now uncovered.

4792 of 5139 relevant lines covered (93.25%)

3.67 hits per line

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

82.68
/src/todo_file/src/lib.rs
1
// LINT-REPLACE-START
2
// This section is autogenerated, do not modify directly
3
// nightly sometimes removes/renames lints
4
#![cfg_attr(allow_unknown_lints, allow(unknown_lints))]
5
#![cfg_attr(allow_unknown_lints, allow(renamed_and_removed_lints))]
6
// enable all rustc's built-in lints
7
#![deny(
8
        future_incompatible,
9
        nonstandard_style,
10
        rust_2018_compatibility,
11
        rust_2018_idioms,
12
        rust_2021_compatibility,
13
        unused,
14
        warnings
15
)]
16
// rustc's additional allowed by default lints
17
#![deny(
18
        absolute_paths_not_starting_with_crate,
19
        deprecated_in_future,
20
        elided_lifetimes_in_paths,
21
        explicit_outlives_requirements,
22
        ffi_unwind_calls,
23
        keyword_idents,
24
        let_underscore_drop,
25
        macro_use_extern_crate,
26
        meta_variable_misuse,
27
        missing_abi,
28
        missing_copy_implementations,
29
        missing_debug_implementations,
30
        missing_docs,
31
        non_ascii_idents,
32
        noop_method_call,
33
        pointer_structural_match,
34
        rust_2021_incompatible_closure_captures,
35
        rust_2021_incompatible_or_patterns,
36
        rust_2021_prefixes_incompatible_syntax,
37
        rust_2021_prelude_collisions,
38
        single_use_lifetimes,
39
        trivial_casts,
40
        trivial_numeric_casts,
41
        unreachable_pub,
42
        unsafe_code,
43
        unsafe_op_in_unsafe_fn,
44
        unused_crate_dependencies,
45
        unused_extern_crates,
46
        unused_import_braces,
47
        unused_lifetimes,
48
        unused_macro_rules,
49
        unused_qualifications,
50
        unused_results,
51
        unused_tuple_struct_fields,
52
        variant_size_differences
53
)]
54
// enable all of Clippy's lints
55
#![deny(clippy::all, clippy::cargo, clippy::pedantic, clippy::restriction)]
56
#![allow(
57
        clippy::arithmetic_side_effects,
58
        clippy::arithmetic_side_effects,
59
        clippy::blanket_clippy_restriction_lints,
60
        clippy::bool_to_int_with_if,
61
        clippy::default_numeric_fallback,
62
        clippy::else_if_without_else,
63
        clippy::expect_used,
64
        clippy::float_arithmetic,
65
        clippy::implicit_return,
66
        clippy::indexing_slicing,
67
        clippy::map_err_ignore,
68
        clippy::min_ident_chars,
69
        clippy::missing_docs_in_private_items,
70
        clippy::missing_trait_methods,
71
        clippy::mod_module_files,
72
        clippy::module_name_repetitions,
73
        clippy::needless_raw_strings,
74
        clippy::new_without_default,
75
        clippy::non_ascii_literal,
76
        clippy::option_if_let_else,
77
        clippy::pattern_type_mismatch,
78
        clippy::pub_use,
79
        clippy::pub_with_shorthand,
80
        clippy::question_mark_used,
81
        clippy::redundant_closure_call,
82
        clippy::redundant_pub_crate,
83
        clippy::ref_patterns,
84
        clippy::single_call_fn,
85
        clippy::std_instead_of_alloc,
86
        clippy::std_instead_of_core,
87
        clippy::tabs_in_doc_comments,
88
        clippy::tests_outside_test_module,
89
        clippy::too_many_lines,
90
        clippy::unwrap_used
91
)]
92
#![deny(
93
        rustdoc::bare_urls,
94
        rustdoc::broken_intra_doc_links,
95
        rustdoc::invalid_codeblock_attributes,
96
        rustdoc::invalid_html_tags,
97
        rustdoc::missing_crate_level_docs,
98
        rustdoc::private_doc_tests,
99
        rustdoc::private_intra_doc_links
100
)]
101
// allow some things in tests
102
#![cfg_attr(
103
        test,
104
        allow(
105
                let_underscore_drop,
106
                clippy::cognitive_complexity,
107
                clippy::let_underscore_must_use,
108
                clippy::let_underscore_untyped,
109
                clippy::needless_pass_by_value,
110
                clippy::panic,
111
                clippy::shadow_reuse,
112
                clippy::shadow_unrelated,
113
                clippy::undocumented_unsafe_blocks,
114
                clippy::unimplemented,
115
                clippy::unreachable
116
        )
117
)]
118
// allowable upcoming nightly lints
119
#![cfg_attr(include_nightly_lints, allow(clippy::absolute_paths, clippy::arc_with_non_send_sync))]
120
// LINT-REPLACE-END
121

122
//! Git Interactive Rebase Tool - Todo File Module
123
//!
124
//! # Description
125
//! This module is used to handle working with the rebase todo file.
126

127
mod action;
128
mod edit_content;
129
pub mod errors;
130
mod history;
131
mod line;
132
mod line_parser;
133
mod search;
134
mod state;
135
#[cfg(not(tarpaulin_include))]
136
pub mod testutil;
137
mod todo_file_options;
138
mod utils;
139

140
use std::{
141
        fs::{read_to_string, File},
142
        io::Write,
143
        path::{Path, PathBuf},
144
        slice::Iter,
145
};
146

147
use state::detect_state;
148
pub use version_track::Version;
149

150
pub use self::{
151
        action::Action,
152
        edit_content::EditContext,
153
        line::Line,
154
        search::Search,
155
        state::State,
156
        todo_file_options::TodoFileOptions,
157
};
158
use self::{
159
        history::{History, HistoryItem},
160
        utils::{remove_range, swap_range_down, swap_range_up},
161
};
162
use crate::{
163
        errors::{FileReadErrorCause, IoError},
164
        history::Operation,
165
};
166

167
/// Represents a rebase file.
168
#[derive(Debug)]
169
pub struct TodoFile {
170
        filepath: PathBuf,
171
        history: History,
172
        is_noop: bool,
173
        lines: Vec<Line>,
174
        options: TodoFileOptions,
175
        selected_line_index: usize,
176
        version: Version,
177
        state: State,
178
}
179

180
impl TodoFile {
181
        /// Create a new instance.
182
        #[must_use]
183
        #[inline]
184
        pub fn new<Path: AsRef<std::path::Path>>(path: Path, options: TodoFileOptions) -> Self {
3✔
185
                let history = History::new(options.undo_limit);
6✔
186

187
                Self {
188
                        filepath: PathBuf::from(path.as_ref()),
6✔
189
                        history,
190
                        is_noop: false,
191
                        lines: vec![],
5✔
192
                        options,
193
                        selected_line_index: 0,
194
                        version: Version::new(),
3✔
195
                        state: State::Initial,
196
                }
197
        }
198

199
        /// Set the rebase lines.
200
        #[inline]
201
        pub fn set_lines(&mut self, lines: Vec<Line>) {
3✔
202
                self.is_noop = !lines.is_empty() && lines[0].get_action() == &Action::Noop;
5✔
203
                self.lines = if self.is_noop {
7✔
204
                        vec![]
4✔
205
                }
206
                else {
×
207
                        lines.into_iter().filter(|l| l.get_action() != &Action::Noop).collect()
10✔
208
                };
209
                if self.selected_line_index >= self.lines.len() {
5✔
210
                        self.selected_line_index = if self.lines.is_empty() { 0 } else { self.lines.len() - 1 };
2✔
211
                }
212
                self.version.reset();
2✔
213
                self.history.reset();
4✔
214
        }
215

216
        /// Set the rebase todo file state.
217
        #[inline]
218
        pub fn set_state(&mut self, state: State) {
2✔
219
                self.state = state;
2✔
220
        }
221

222
        /// Load the rebase file from disk.
223
        ///
224
        /// # Errors
225
        ///
226
        /// Returns error if the file cannot be read.
227
        #[inline]
228
        pub fn load_file(&mut self) -> Result<(), IoError> {
3✔
229
                let lines: Result<Vec<Line>, IoError> = read_to_string(self.filepath.as_path())
11✔
230
                        .map_err(|err| {
4✔
231
                                IoError::FileRead {
1✔
232
                                        file: self.filepath.clone(),
1✔
233
                                        cause: FileReadErrorCause::from(err),
1✔
234
                                }
235
                        })?
236
                        .lines()
237
                        .filter_map(|l| {
5✔
238
                                if l.starts_with(self.options.comment_prefix.as_str()) || l.is_empty() {
5✔
239
                                        None
3✔
240
                                }
241
                                else {
×
242
                                        Some(Line::parse(l).map_err(|err| {
2✔
243
                                                IoError::FileRead {
×
244
                                                        file: self.filepath.clone(),
×
245
                                                        cause: FileReadErrorCause::from(err),
×
246
                                                }
247
                                        }))
248
                                }
249
                        })
250
                        .collect();
251
                self.set_lines(lines?);
2✔
252
                self.set_state(detect_state(&self.filepath)?);
2✔
253
                Ok(())
2✔
254
        }
255

256
        /// Write the rebase file to disk.
257
        /// # Errors
258
        ///
259
        /// Returns error if the file cannot be written.
260
        #[inline]
261
        pub fn write_file(&self) -> Result<(), IoError> {
2✔
262
                let mut file = File::create(&self.filepath).map_err(|err| {
5✔
263
                        IoError::FileRead {
1✔
264
                                file: self.filepath.clone(),
1✔
265
                                cause: FileReadErrorCause::from(err),
1✔
266
                        }
267
                })?;
268
                let file_contents = if self.is_noop {
3✔
269
                        String::from("noop")
2✔
270
                }
271
                else {
×
272
                        self.lines
8✔
273
                                .iter()
274
                                .flat_map(|l| {
4✔
275
                                        let mut lines = vec![Line::to_text(l)];
2✔
276
                                        if let Some(command) = self.options.line_changed_command.as_deref() {
4✔
277
                                                if l.is_modified() {
4✔
278
                                                        let action = l.get_action();
3✔
279

280
                                                        match *action {
3✔
281
                                                                Action::Break | Action::Noop => {},
×
NEW
282
                                                                Action::Cut
×
NEW
283
                                                                | Action::Drop
×
284
                                                                | Action::Fixup
×
285
                                                                | Action::Edit
×
NEW
286
                                                                | Action::Index
×
287
                                                                | Action::Pick
×
288
                                                                | Action::Reword
×
289
                                                                | Action::Squash => {
×
290
                                                                        lines.push(format!("exec {command} \"{}\" \"{}\"", action, l.get_hash()));
5✔
291
                                                                },
292
                                                                Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => {
×
293
                                                                        let original_label =
×
294
                                                                                l.original().map_or_else(|| l.get_content(), Line::get_content);
4✔
295
                                                                        lines.push(format!(
9✔
296
                                                                                "exec {command} \"{}\" \"{}\" \"{}\"",
×
297
                                                                                action,
×
298
                                                                                original_label,
×
299
                                                                                l.get_content()
2✔
300
                                                                        ));
301
                                                                },
302
                                                        }
303
                                                }
304
                                        }
305
                                        lines
2✔
306
                                })
307
                                .collect::<Vec<String>>()
308
                                .join("\n")
309
                };
310
                writeln!(file, "{file_contents}").map_err(|err| {
4✔
311
                        IoError::FileRead {
×
312
                                file: self.filepath.clone(),
×
313
                                cause: FileReadErrorCause::from(err),
×
314
                        }
315
                })?;
316
                Ok(())
2✔
317
        }
318

319
        /// Set the selected line index returning the new index based after ensuring within range.
320
        #[inline]
321
        pub fn set_selected_line_index(&mut self, selected_line_index: usize) -> usize {
3✔
322
                self.selected_line_index = if self.lines.is_empty() {
7✔
323
                        0
1✔
324
                }
325
                else if selected_line_index >= self.lines.len() {
8✔
326
                        self.lines.len() - 1
6✔
327
                }
328
                else {
×
329
                        selected_line_index
2✔
330
                };
331
                self.selected_line_index
3✔
332
        }
333

334
        /// Swap a range of lines up.
335
        #[inline]
336
        pub fn swap_range_up(&mut self, start_index: usize, end_index: usize) -> bool {
3✔
337
                if end_index == 0 || start_index == 0 || self.lines.is_empty() {
6✔
338
                        return false;
2✔
339
                }
340

341
                let max_index = self.lines.len() - 1;
2✔
342
                let end = if end_index > max_index { max_index } else { end_index };
4✔
343
                let start = if start_index > max_index {
4✔
344
                        max_index
1✔
345
                }
346
                else {
×
347
                        start_index
2✔
348
                };
349

350
                swap_range_up(&mut self.lines, start, end);
2✔
351
                self.version.increment();
2✔
352
                self.history.record(HistoryItem::new_swap_up(start, end));
2✔
353
                true
2✔
354
        }
355

356
        /// Swap a range of lines down.
357
        #[inline]
358
        pub fn swap_range_down(&mut self, start_index: usize, end_index: usize) -> bool {
3✔
359
                let len = self.lines.len();
3✔
360
                let max_index = if len == 0 { 0 } else { len - 1 };
6✔
361

362
                if end_index == max_index || start_index == max_index {
5✔
363
                        return false;
2✔
364
                }
365

366
                swap_range_down(&mut self.lines, start_index, end_index);
2✔
367
                self.version.increment();
2✔
368
                self.history.record(HistoryItem::new_swap_down(start_index, end_index));
2✔
369
                true
2✔
370
        }
371

372
        /// Add a new line.
373
        #[inline]
374
        pub fn add_line(&mut self, index: usize, line: Line) {
3✔
375
                let i = if index > self.lines.len() {
7✔
376
                        self.lines.len()
5✔
377
                }
378
                else {
×
379
                        index
2✔
380
                };
381
                self.lines.insert(i, line);
2✔
382
                self.version.increment();
3✔
383
                self.history.record(HistoryItem::new_add(i, i));
3✔
384
        }
385

386
        /// Remove a range of lines.
387
        #[inline]
388
        pub fn remove_lines(&mut self, start_index: usize, end_index: usize) {
5✔
389
                if self.lines.is_empty() {
5✔
390
                        return;
×
391
                }
392

393
                let max_index = self.lines.len() - 1;
9✔
394
                let end = if end_index > max_index { max_index } else { end_index };
9✔
395
                let start = if start_index > max_index {
7✔
396
                        max_index
1✔
397
                }
398
                else {
×
399
                        start_index
4✔
400
                };
401

402
                let removed_lines = remove_range(&mut self.lines, start, end);
3✔
403
                self.version.increment();
7✔
404
                self.history.record(HistoryItem::new_remove(start, end, removed_lines));
7✔
405
        }
406

407
        /// Update a range of lines.
408
        #[inline]
409
        pub fn update_range(&mut self, start_index: usize, end_index: usize, edit_context: &EditContext) {
4✔
410
                if self.lines.is_empty() {
4✔
411
                        return;
×
412
                }
413

414
                let max_index = self.lines.len() - 1;
8✔
415
                let end = if end_index > max_index { max_index } else { end_index };
8✔
416
                let start = if start_index > max_index {
10✔
417
                        max_index
1✔
418
                }
419
                else {
×
420
                        start_index
5✔
421
                };
422

423
                let range = if end <= start { end..=start } else { start..=end };
5✔
424

425
                let mut lines = vec![];
5✔
426
                for index in range {
15✔
427
                        let line = &mut self.lines[index];
10✔
428
                        lines.push(line.clone());
5✔
429
                        if let Some(action) = edit_context.get_action() {
10✔
430
                                line.set_action(action);
15✔
431
                        }
432

433
                        if let Some(content) = edit_context.get_content() {
10✔
434
                                line.edit_content(content);
4✔
435
                        }
436

437
                        if let Some(option) = edit_context.get_option() {
10✔
438
                                line.toggle_option(option);
3✔
439
                        }
440
                }
441
                self.version.increment();
5✔
442
                self.history.record(HistoryItem::new_modify(start, end, lines));
5✔
443
        }
444

445
        /// Undo the last modification.
446
        #[inline]
447
        pub fn undo(&mut self) -> Option<(usize, usize)> {
5✔
448
                self.version.increment();
5✔
449
                if let Some((operation, start, end)) = self.history.undo(&mut self.lines) {
5✔
450
                        return if operation == Operation::Load {
10✔
451
                                None
1✔
452
                        }
453
                        else {
×
454
                                Some((start, end))
5✔
455
                        };
456
                }
457
                None
1✔
458
        }
459

460
        /// Redo the last undone modification.
461
        #[inline]
462
        pub fn redo(&mut self) -> Option<(usize, usize)> {
3✔
463
                self.version.increment();
3✔
464
                self.history.redo(&mut self.lines).map(|(_, start, end)| (start, end))
6✔
465
        }
466

467
        /// Get the current version
468
        #[must_use]
469
        #[inline]
470
        pub const fn version(&self) -> &Version {
2✔
471
                &self.version
4✔
472
        }
473

474
        /// Get the current state
475
        #[must_use]
476
        #[inline]
477
        pub const fn state(&self) -> &State {
2✔
478
                &self.state
2✔
479
        }
480

481
        /// Get the selected line.
482
        #[must_use]
483
        #[inline]
484
        pub fn get_selected_line(&self) -> Option<&Line> {
2✔
485
                self.lines.get(self.selected_line_index)
3✔
486
        }
487

488
        /// Get the index of the last line that can be selected.
489
        #[must_use]
490
        #[inline]
491
        pub fn get_max_selected_line_index(&self) -> usize {
2✔
492
                let len = self.lines.len();
3✔
493
                if len == 0 { 0 } else { len - 1 }
5✔
494
        }
495

496
        /// Get the selected line index
497
        #[must_use]
498
        #[inline]
499
        pub const fn get_selected_line_index(&self) -> usize {
2✔
500
                self.selected_line_index
2✔
501
        }
502

503
        /// Get the file path to the rebase file.
504
        #[must_use]
505
        #[inline]
506
        pub fn get_filepath(&self) -> &Path {
2✔
507
                self.filepath.as_path()
2✔
508
        }
509

510
        /// Get a line by index.
511
        #[must_use]
512
        #[inline]
513
        pub fn get_line(&self, index: usize) -> Option<&Line> {
3✔
514
                self.lines.get(index)
3✔
515
        }
516

517
        /// Get an owned copy of the lines.
518
        #[must_use]
519
        #[inline]
520
        pub fn get_lines_owned(&self) -> Vec<Line> {
2✔
521
                self.lines.clone()
2✔
522
        }
523

524
        /// Is the rebase file a noop.
525
        #[must_use]
526
        #[inline]
527
        pub const fn is_noop(&self) -> bool {
2✔
528
                self.is_noop
2✔
529
        }
530

531
        /// Get an iterator over the lines.
532
        #[inline]
533
        pub fn lines_iter(&self) -> Iter<'_, Line> {
2✔
534
                self.lines.iter()
4✔
535
        }
536

537
        /// Does the rebase file contain no lines.
538
        #[must_use]
539
        #[inline]
540
        pub fn is_empty(&self) -> bool {
2✔
541
                self.lines.is_empty()
2✔
542
        }
543
}
544

545
#[cfg(test)]
546
mod tests {
547
        use claims::{assert_none, assert_some_eq};
548
        use tempfile::{Builder, NamedTempFile};
549
        use testutils::{assert_empty, assert_not_empty};
550

551
        use super::*;
552

553
        fn create_line(line: &str) -> Line {
554
                Line::parse(line).unwrap()
555
        }
556

557
        fn create_and_load_todo_file_with_options(
558
                file_contents: &[&str],
559
                todo_file_options: TodoFileOptions,
560
        ) -> (TodoFile, NamedTempFile) {
561
                let todo_file_path = Builder::new()
562
                        .prefix("git-rebase-todo-scratch")
563
                        .suffix("")
564
                        .tempfile()
565
                        .unwrap();
566
                write!(todo_file_path.as_file(), "{}", file_contents.join("\n")).unwrap();
567
                let mut todo_file = TodoFile::new(todo_file_path.path().to_str().unwrap(), todo_file_options);
568
                todo_file.load_file().unwrap();
569
                (todo_file, todo_file_path)
570
        }
571

572
        fn create_and_load_todo_file(file_contents: &[&str]) -> (TodoFile, NamedTempFile) {
573
                create_and_load_todo_file_with_options(file_contents, TodoFileOptions::new(1, "#"))
574
        }
575

576
        macro_rules! assert_read_todo_file {
577
                ($todo_file_path:expr, $($arg:expr),*) => {
578
                        let expected = [$( $arg, )*];
579
                        let content = read_to_string(Path::new($todo_file_path)).unwrap();
580
                        pretty_assertions::assert_str_eq!(content, format!("{}\n", expected.join("\n")));
581
                };
582
        }
583

584
        macro_rules! assert_todo_lines {
585
                ($todo_file_path:expr, $($arg:expr),*) => {
586
                        let actual_lines = $todo_file_path.get_lines_owned();
587

588
                        let expected = vec![$( create_line($arg), )*];
589
                        pretty_assertions::assert_str_eq!(
590
                                actual_lines.iter().map(Line::to_text).collect::<Vec<String>>().join("\n"),
591
                                expected.iter().map(Line::to_text).collect::<Vec<String>>().join("\n")
592
                        );
593
                };
594
        }
595

596
        #[test]
597
        fn load_file() {
598
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa foobar"]);
599
                assert_todo_lines!(todo_file, "pick aaa foobar");
600
                assert_ne!(todo_file.version(), &Version::new());
601
        }
602

603
        #[test]
604
        fn load_noop_file() {
605
                let (todo_file, _) = create_and_load_todo_file(&["noop"]);
606
                assert_empty!(todo_file);
607
                assert!(todo_file.is_noop());
608
        }
609

610
        #[test]
611
        fn load_ignore_comments() {
612
                let (todo_file, _) = create_and_load_todo_file(&["# pick aaa comment", "pick aaa foo", "# pick aaa comment"]);
613
                assert_todo_lines!(todo_file, "pick aaa foo");
614
        }
615

616
        #[test]
617
        fn load_ignore_newlines() {
618
                let (todo_file, _) = create_and_load_todo_file(&["", "pick aaa foobar", ""]);
619
                assert_todo_lines!(todo_file, "pick aaa foobar");
620
        }
621

622
        #[test]
623
        fn set_lines() {
624
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
625
                let old_version = todo_file.version;
626
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
627
                assert_todo_lines!(todo_file, "pick bbb comment");
628
                assert_ne!(todo_file.version(), &old_version);
629
        }
630

631
        #[test]
632
        fn set_lines_reset_history() {
633
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
634
                todo_file.history.record(HistoryItem::new_add(1, 1));
635
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
636
                assert_none!(todo_file.undo());
637
        }
638

639
        #[test]
640
        fn set_lines_reset_selected_index() {
641
                let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
642
                todo_file.selected_line_index = 2;
643
                todo_file.set_lines(vec![create_line("pick a a"), create_line("pick b b")]);
644
                assert_eq!(todo_file.selected_line_index, 1);
645
        }
646

647
        #[test]
648
        fn set_lines_reset_selected_index_empty_lis() {
649
                let (mut todo_file, _) = create_and_load_todo_file(&["pick a a", "pick b b", "pick c c"]);
650
                todo_file.selected_line_index = 2;
651
                todo_file.set_lines(vec![]);
652
                assert_eq!(todo_file.selected_line_index, 0);
653
        }
654

655
        #[test]
656
        fn write_file() {
657
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
658
                todo_file.set_lines(vec![create_line("pick bbb comment")]);
659
                todo_file.write_file().unwrap();
660
                assert_todo_lines!(todo_file, "pick bbb comment");
661
        }
662

663
        #[test]
664
        fn write_file_with_exec_command_modified_line_with_reference() {
665
                fn create_modified_line(action: &str) -> Line {
666
                        let mut parsed = create_line(format!("{action} label").as_str());
667
                        parsed.edit_content("new-label");
668
                        parsed
669
                }
670
                let mut options = TodoFileOptions::new(10, "#");
671
                options.line_changed_command("command");
672
                let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options);
673
                todo_file.set_lines(vec![
674
                        create_modified_line("label"),
675
                        create_modified_line("reset"),
676
                        create_modified_line("merge"),
677
                        create_modified_line("update-ref"),
678
                ]);
679
                todo_file.write_file().unwrap();
680
                assert_read_todo_file!(
681
                        todo_file.get_filepath(),
682
                        "label new-label",
683
                        "exec command \"label\" \"label\" \"new-label\"",
684
                        "reset new-label",
685
                        "exec command \"reset\" \"label\" \"new-label\"",
686
                        "merge new-label",
687
                        "exec command \"merge\" \"label\" \"new-label\"",
688
                        "update-ref new-label",
689
                        "exec command \"update-ref\" \"label\" \"new-label\""
690
                );
691
        }
692

693
        #[test]
694
        fn write_file_with_exec_command_modified_line_with_hash() {
695
                fn create_modified_line(action: &str) -> Line {
696
                        let mut parsed = create_line(format!("{action} bbb comment").as_str());
697
                        parsed.set_action(
698
                                if parsed.get_action() == &Action::Fixup {
699
                                        Action::Pick
700
                                }
701
                                else {
702
                                        Action::Fixup
703
                                },
704
                        );
705
                        parsed
706
                }
707
                let mut options = TodoFileOptions::new(10, "#");
708
                options.line_changed_command("command");
709
                let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options);
710
                let mut line = create_line("pick bbb comment");
711
                line.set_action(Action::Fixup);
712
                todo_file.set_lines(vec![
713
                        create_modified_line("drop"),
714
                        create_modified_line("fixup"),
715
                        create_modified_line("edit"),
716
                        create_modified_line("pick"),
717
                        create_modified_line("reword"),
718
                        create_modified_line("squash"),
719
                ]);
720
                todo_file.write_file().unwrap();
721
                assert_read_todo_file!(
722
                        todo_file.get_filepath(),
723
                        "fixup bbb comment",
724
                        "exec command \"fixup\" \"bbb\"",
725
                        "pick bbb comment",
726
                        "exec command \"pick\" \"bbb\"",
727
                        "fixup bbb comment",
728
                        "exec command \"fixup\" \"bbb\"",
729
                        "fixup bbb comment",
730
                        "exec command \"fixup\" \"bbb\"",
731
                        "fixup bbb comment",
732
                        "exec command \"fixup\" \"bbb\"",
733
                        "fixup bbb comment",
734
                        "exec command \"fixup\" \"bbb\""
735
                );
736
        }
737

738
        #[test]
739
        fn write_file_with_exec_command_modified_line_with_exec() {
740
                let mut options = TodoFileOptions::new(10, "#");
741
                options.line_changed_command("command");
742
                let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options);
743
                let mut line = create_line("exec command");
744
                line.edit_content("new-command");
745
                todo_file.set_lines(vec![line]);
746
                todo_file.write_file().unwrap();
747
                assert_read_todo_file!(
748
                        todo_file.get_filepath(),
749
                        "exec new-command",
750
                        "exec command \"exec\" \"command\" \"new-command\""
751
                );
752
        }
753

754
        #[test]
755
        fn write_file_with_exec_command_modified_line_with_break() {
756
                let mut options = TodoFileOptions::new(10, "#");
757
                options.line_changed_command("command");
758
                let (mut todo_file, _) = create_and_load_todo_file_with_options(&[], options);
759
                todo_file.set_lines(vec![create_line("break")]);
760
                todo_file.write_file().unwrap();
761
                assert_read_todo_file!(todo_file.get_filepath(), "break");
762
        }
763

764
        #[test]
765
        fn write_file_noop() {
766
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
767
                todo_file.set_lines(vec![create_line("noop")]);
768
                todo_file.write_file().unwrap();
769
                assert_read_todo_file!(todo_file.get_filepath(), "noop");
770
        }
771

772
        #[test]
773
        fn add_line_index_miss() {
774
                let (mut todo_file, _) =
775
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
776
                todo_file.add_line(100, create_line("fixup ddd comment"));
777
                assert_todo_lines!(
778
                        todo_file,
779
                        "pick aaa comment",
780
                        "drop bbb comment",
781
                        "edit ccc comment",
782
                        "fixup ddd comment"
783
                );
784
        }
785

786
        #[test]
787
        fn add_line() {
788
                let (mut todo_file, _) =
789
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
790
                let old_version = *todo_file.version();
791
                todo_file.add_line(1, create_line("fixup ddd comment"));
792
                assert_todo_lines!(
793
                        todo_file,
794
                        "pick aaa comment",
795
                        "fixup ddd comment",
796
                        "drop bbb comment",
797
                        "edit ccc comment"
798
                );
799
                assert_ne!(todo_file.version(), &old_version);
800
        }
801

802
        #[test]
803
        fn add_line_record_history() {
804
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
805
                todo_file.add_line(1, create_line("fixup ddd comment"));
806
                let _undo_result = todo_file.undo();
807
                assert_todo_lines!(todo_file, "pick aaa comment");
808
        }
809

810
        #[test]
811
        fn remove_lines_index_miss_start() {
812
                let (mut todo_file, _) =
813
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
814
                todo_file.remove_lines(100, 1);
815
                assert_todo_lines!(todo_file, "pick aaa comment");
816
        }
817

818
        #[test]
819
        fn remove_lines_index_miss_end() {
820
                let (mut todo_file, _) =
821
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
822
                todo_file.remove_lines(1, 100);
823
                assert_todo_lines!(todo_file, "pick aaa comment");
824
        }
825

826
        #[test]
827
        fn remove_lines_index_miss_start_and_end() {
828
                let (mut todo_file, _) =
829
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
830
                todo_file.remove_lines(100, 100);
831
                assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment");
832
        }
833

834
        #[test]
835
        fn remove_lines() {
836
                let (mut todo_file, _) =
837
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
838
                let old_version = *todo_file.version();
839
                todo_file.remove_lines(1, 1);
840
                assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
841
                assert_ne!(todo_file.version(), &old_version);
842
        }
843

844
        #[test]
845
        fn remove_lines_empty_list() {
846
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
847
                todo_file.remove_lines(1, 1);
848
        }
849

850
        #[test]
851
        fn remove_lines_record_history() {
852
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "edit ccc comment"]);
853
                todo_file.remove_lines(1, 1);
854
                let _undo_result = todo_file.undo();
855
                assert_todo_lines!(todo_file, "pick aaa comment", "edit ccc comment");
856
        }
857

858
        #[test]
859
        fn update_range_full_set_action() {
860
                let (mut todo_file, _) =
861
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
862
                let old_version = *todo_file.version();
863
                todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
864
                assert_todo_lines!(
865
                        todo_file,
866
                        "reword aaa comment",
867
                        "reword bbb comment",
868
                        "reword ccc comment"
869
                );
870
                assert_ne!(todo_file.version(), &old_version);
871
        }
872

873
        #[test]
874
        fn update_range_full_set_content() {
875
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
876
                todo_file.update_range(0, 2, &EditContext::new().content("echo"));
877
                assert_todo_lines!(todo_file, "exec echo", "exec echo", "exec echo");
878
        }
879

880
        #[test]
881
        fn update_range_set_option() {
882
                let (mut todo_file, _) = create_and_load_todo_file(&["fixup aaa comment"]);
883
                let old_version = *todo_file.version();
884
                todo_file.update_range(0, 2, &EditContext::new().option("-c"));
885
                assert_todo_lines!(todo_file, "fixup -c aaa comment");
886
                assert_ne!(todo_file.version(), &old_version);
887
        }
888

889
        #[test]
890
        fn update_range_reverse_indexes() {
891
                let (mut todo_file, _) =
892
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
893
                todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
894
                assert_todo_lines!(
895
                        todo_file,
896
                        "reword aaa comment",
897
                        "reword bbb comment",
898
                        "reword ccc comment"
899
                );
900
        }
901

902
        #[test]
903
        fn update_range_record_history() {
904
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
905
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
906
                let _undo_result = todo_file.undo();
907
                assert_todo_lines!(todo_file, "pick aaa comment");
908
        }
909

910
        #[test]
911
        fn update_range_empty_list() {
912
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
913
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Reword));
914
        }
915

916
        #[test]
917
        fn update_range_start_index_overflow() {
918
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
919
                todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword));
920
                assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
921
        }
922

923
        #[test]
924
        fn update_range_end_index_overflow() {
925
                let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "pick bbb comment"]);
926
                todo_file.update_range(0, 2, &EditContext::new().action(Action::Reword));
927
                assert_todo_lines!(todo_file, "reword aaa comment", "reword bbb comment");
928
        }
929

930
        #[test]
931
        fn undo_load_operation() {
932
                let (mut todo_file, _) =
933
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
934
                assert_none!(todo_file.undo());
935
        }
936

937
        #[test]
938
        fn undo_empty_history() {
939
                let (mut todo_file, _) =
940
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
941
                // set short history, to remove load entry
942
                todo_file.history = History::new(1);
943
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Drop));
944
                _ = todo_file.undo(); // remove Drop operation
945
                assert_none!(todo_file.undo());
946
        }
947

948
        #[test]
949
        fn undo_operation() {
950
                let (mut todo_file, _) =
951
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
952
                todo_file.update_range(0, 1, &EditContext::new().action(Action::Drop));
953
                assert_some_eq!(todo_file.undo(), (0, 1));
954
        }
955

956
        #[test]
957
        fn history_undo_redo() {
958
                let (mut todo_file, _) =
959
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
960
                todo_file.update_range(0, 0, &EditContext::new().action(Action::Drop));
961
                let old_version = *todo_file.version();
962
                let _undo_result = todo_file.undo();
963
                assert_todo_lines!(todo_file, "pick aaa comment", "drop bbb comment", "edit ccc comment");
964
                assert_ne!(todo_file.version(), &old_version);
965
                let old_version = *todo_file.version();
966
                _ = todo_file.redo();
967
                assert_todo_lines!(todo_file, "drop aaa comment", "drop bbb comment", "edit ccc comment");
968
                assert_ne!(todo_file.version(), &old_version);
969
        }
970

971
        #[test]
972
        fn redo_empty_history() {
973
                let (mut todo_file, _) =
974
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
975
                assert_none!(todo_file.redo());
976
        }
977

978
        #[test]
979
        fn redo_operation() {
980
                let (mut todo_file, _) =
981
                        create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]);
982
                todo_file.update_range(0, 1, &EditContext::new().action(Action::Drop));
983
                _ = todo_file.undo();
984
                assert_some_eq!(todo_file.redo(), (0, 1));
985
        }
986

987
        #[test]
988
        fn swap_up() {
989
                let (mut todo_file, _) =
990
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
991
                let old_version = *todo_file.version();
992
                assert!(todo_file.swap_range_up(1, 2));
993
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
994
                assert_ne!(todo_file.version(), &old_version);
995
        }
996

997
        #[test]
998
        fn swap_up_records_history() {
999
                let (mut todo_file, _) =
1000
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1001
                _ = todo_file.swap_range_up(1, 2);
1002
                let _undo_result = todo_file.undo();
1003
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1004
        }
1005

1006
        #[test]
1007
        fn swap_up_reverse_index() {
1008
                let (mut todo_file, _) =
1009
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1010
                assert!(todo_file.swap_range_up(2, 1));
1011
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
1012
        }
1013

1014
        #[test]
1015
        fn swap_up_single_line() {
1016
                let (mut todo_file, _) =
1017
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1018
                assert!(todo_file.swap_range_up(1, 1));
1019
                assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
1020
        }
1021

1022
        #[test]
1023
        fn swap_up_at_top_start_index() {
1024
                let (mut todo_file, _) =
1025
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1026
                assert!(!todo_file.swap_range_up(0, 1));
1027
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1028
        }
1029

1030
        #[test]
1031
        fn swap_up_at_top_end_index() {
1032
                let (mut todo_file, _) =
1033
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1034
                assert!(!todo_file.swap_range_up(1, 0));
1035
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1036
        }
1037

1038
        #[test]
1039
        fn swap_up_start_index_overflow() {
1040
                let (mut todo_file, _) =
1041
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1042
                assert!(todo_file.swap_range_up(3, 1));
1043
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
1044
        }
1045

1046
        #[test]
1047
        fn swap_up_end_index_overflow() {
1048
                let (mut todo_file, _) =
1049
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1050
                assert!(todo_file.swap_range_up(3, 1));
1051
                assert_todo_lines!(todo_file, "pick bbb comment", "pick ccc comment", "pick aaa comment");
1052
        }
1053

1054
        #[test]
1055
        fn swap_up_empty_list_index_out_of_bounds() {
1056
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
1057
                assert!(!todo_file.swap_range_up(1, 1));
1058
        }
1059

1060
        #[test]
1061
        fn swap_down() {
1062
                let (mut todo_file, _) =
1063
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1064
                let old_version = *todo_file.version();
1065
                assert!(todo_file.swap_range_down(0, 1));
1066
                assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
1067
                assert_ne!(todo_file.version(), &old_version);
1068
        }
1069

1070
        #[test]
1071
        fn swap_down_records_history() {
1072
                let (mut todo_file, _) =
1073
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1074
                let _swap_result = todo_file.swap_range_down(0, 1);
1075
                let _undo_result = todo_file.undo();
1076
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1077
        }
1078

1079
        #[test]
1080
        fn swap_down_reverse_index() {
1081
                let (mut todo_file, _) =
1082
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1083
                assert!(todo_file.swap_range_down(1, 0));
1084
                assert_todo_lines!(todo_file, "pick ccc comment", "pick aaa comment", "pick bbb comment");
1085
        }
1086

1087
        #[test]
1088
        fn swap_down_single_line() {
1089
                let (mut todo_file, _) =
1090
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1091
                assert!(todo_file.swap_range_down(0, 0));
1092
                assert_todo_lines!(todo_file, "pick bbb comment", "pick aaa comment", "pick ccc comment");
1093
        }
1094

1095
        #[test]
1096
        fn swap_down_at_bottom_end_index() {
1097
                let (mut todo_file, _) =
1098
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1099
                assert!(!todo_file.swap_range_down(1, 2));
1100
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1101
        }
1102

1103
        #[test]
1104
        fn swap_down_at_bottom_start_index() {
1105
                let (mut todo_file, _) =
1106
                        create_and_load_todo_file(&["pick aaa comment", "pick bbb comment", "pick ccc comment"]);
1107
                assert!(!todo_file.swap_range_down(2, 1));
1108
                assert_todo_lines!(todo_file, "pick aaa comment", "pick bbb comment", "pick ccc comment");
1109
        }
1110

1111
        #[test]
1112
        fn selected_line_index() {
1113
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1114
                let selected_line_index = todo_file.set_selected_line_index(1);
1115
                assert_eq!(selected_line_index, 1);
1116
                assert_eq!(todo_file.get_selected_line_index(), 1);
1117
        }
1118

1119
        #[test]
1120
        fn selected_line_index_overflow() {
1121
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1122
                let selected_line_index = todo_file.set_selected_line_index(3);
1123
                assert_eq!(selected_line_index, 2);
1124
                assert_eq!(todo_file.get_selected_line_index(), 2);
1125
        }
1126

1127
        #[test]
1128
        fn selected_line() {
1129
                let (mut todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1130
                _ = todo_file.set_selected_line_index(0);
1131
                assert_some_eq!(todo_file.get_selected_line(), &create_line("exec foo"));
1132
        }
1133

1134
        #[test]
1135
        fn selected_line_empty_list() {
1136
                let (mut todo_file, _) = create_and_load_todo_file(&[]);
1137
                _ = todo_file.set_selected_line_index(0);
1138
                assert_none!(todo_file.get_selected_line());
1139
        }
1140

1141
        #[test]
1142
        fn get_max_selected_line() {
1143
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1144
                assert_eq!(todo_file.get_max_selected_line_index(), 2);
1145
        }
1146

1147
        #[test]
1148
        fn get_max_selected_line_empty_list() {
1149
                let (todo_file, _) = create_and_load_todo_file(&[]);
1150
                assert_eq!(todo_file.get_max_selected_line_index(), 0);
1151
        }
1152

1153
        #[test]
1154
        fn get_line_miss_high() {
1155
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1156
                assert_none!(todo_file.get_line(4));
1157
        }
1158

1159
        #[test]
1160
        fn get_line_hit() {
1161
                let (todo_file, _) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1162
                assert_some_eq!(todo_file.get_line(1), &create_line("exec bar"));
1163
        }
1164

1165
        #[test]
1166
        fn get_file_path() {
1167
                let (todo_file, filepath) = create_and_load_todo_file(&["exec foo", "exec bar", "exec foobar"]);
1168
                assert_eq!(todo_file.get_filepath(), filepath.path());
1169
        }
1170

1171
        #[test]
1172
        fn iter() {
1173
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
1174
                assert_some_eq!(todo_file.lines_iter().next(), &create_line("pick aaa comment"));
1175
        }
1176

1177
        #[test]
1178
        fn is_empty_true() {
1179
                let (todo_file, _) = create_and_load_todo_file(&[]);
1180
                assert_empty!(todo_file);
1181
        }
1182

1183
        #[test]
1184
        fn is_empty_false() {
1185
                let (todo_file, _) = create_and_load_todo_file(&["pick aaa comment"]);
1186
                assert_not_empty!(todo_file);
1187
        }
1188
}
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

© 2025 Coveralls, Inc