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

MitMaro / git-interactive-rebase-tool / 6883077488

15 Nov 2023 09:23PM UTC 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

81.25
/src/todo_file/src/line.rs
1
use crate::{errors::ParseError, line_parser::LineParser, Action};
2

3
/// Represents a line in the rebase file.
4
#[derive(Clone, Debug, PartialEq, Eq)]
5
pub struct Line {
6
        action: Action,
7
        content: String,
8
        hash: String,
9
        mutated: bool,
10
        option: Option<String>,
11
        original_line: Option<Box<Line>>,
12
}
13

14
impl Line {
15
        fn new(action: Action, hash: &str, content: &str, option: Option<&str>) -> Self {
3✔
16
                let original_action = action;
3✔
17
                let original_content = String::from(content);
3✔
18
                let original_option = option.map(String::from);
6✔
19

20
                Self {
21
                        action,
22
                        content: String::from(content),
3✔
23
                        hash: String::from(hash),
3✔
24
                        mutated: false,
25
                        option: original_option.clone(),
3✔
26
                        original_line: Some(Box::new(Line {
6✔
27
                                action: original_action,
28
                                content: original_content,
29
                                hash: String::from(hash),
30
                                mutated: false,
31
                                option: original_option,
32
                                original_line: None,
33
                        })),
34
                }
35
        }
36

37
        /// Create a new noop line.
38
        #[must_use]
39
        fn new_noop() -> Self {
2✔
40
                Self::new(Action::Noop, "", "", None)
2✔
41
        }
42

43
        /// Create a new pick line.
44
        #[must_use]
45
        #[inline]
46
        pub fn new_pick(hash: &str) -> Self {
2✔
47
                Self::new(Action::Pick, hash, "", None)
2✔
48
        }
49

50
        /// Create a new break line.
51
        #[must_use]
52
        #[inline]
53
        pub fn new_break() -> Self {
2✔
54
                Self::new(Action::Break, "", "", None)
2✔
55
        }
56

57
        /// Create a new exec line.
58
        #[must_use]
59
        #[inline]
60
        pub fn new_exec(command: &str) -> Self {
2✔
61
                Self::new(Action::Exec, "", command, None)
2✔
62
        }
63

64
        /// Create a new merge line.
65
        #[must_use]
66
        #[inline]
67
        pub fn new_merge(label: &str) -> Self {
2✔
68
                Self::new(Action::Merge, "", label, None)
2✔
69
        }
70

71
        /// Create a new label line.
72
        #[must_use]
73
        #[inline]
74
        pub fn new_label(label: &str) -> Self {
2✔
75
                Self::new(Action::Label, "", label, None)
2✔
76
        }
77

78
        /// Create a new reset line.
79
        #[must_use]
80
        #[inline]
81
        pub fn new_reset(label: &str) -> Self {
2✔
82
                Self::new(Action::Reset, "", label, None)
2✔
83
        }
84

85
        /// Create a new update-ref line.
86
        #[must_use]
87
        #[inline]
88
        pub fn new_update_ref(ref_name: &str) -> Self {
2✔
89
                Self::new(Action::UpdateRef, "", ref_name, None)
2✔
90
        }
91

92
        /// Create a new line from a rebase file line.
93
        ///
94
        /// # Errors
95
        ///
96
        /// Returns an error if an invalid line is provided.
97
        #[inline]
98
        pub fn parse(input_line: &str) -> Result<Self, ParseError> {
5✔
99
                let mut line_parser = LineParser::new(input_line);
5✔
100

101
                let action = Action::try_from(line_parser.next()?)?;
6✔
102
                Ok(match action {
10✔
103
                        Action::Noop => Self::new_noop(),
3✔
104
                        Action::Break => Self::new_break(),
2✔
NEW
105
                        Action::Pick | Action::Reword | Action::Edit | Action::Squash | Action::Drop | Action::Cut | Action::Index => {
×
106
                                Self::new(action, line_parser.next()?, line_parser.take_remaining(), None)
10✔
107
                        },
108
                        Action::Fixup => {
×
109
                                let mut next = line_parser.next()?;
7✔
110

111
                                let option = if next.starts_with('-') {
8✔
112
                                        let opt = String::from(next);
2✔
113
                                        next = line_parser.next()?;
4✔
114
                                        Some(opt)
2✔
115
                                }
116
                                else {
×
117
                                        None
3✔
118
                                };
119

120
                                Self::new(action, next, line_parser.take_remaining(), option.as_deref())
6✔
121
                        },
122
                        Action::Exec | Action::Merge | Action::Label | Action::Reset | Action::UpdateRef => {
×
123
                                if !line_parser.has_more() {
2✔
124
                                        return Err(line_parser.parse_error());
2✔
125
                                }
126
                                Self::new(action, "", line_parser.take_remaining(), None)
2✔
127
                        },
128
                })
129
        }
130

131
        /// Set the action of the line.
132
        #[inline]
133
        pub fn set_action(&mut self, action: Action) {
5✔
134
                if !self.action.is_static() && self.action != action {
13✔
135
                        self.mutated = true;
8✔
136
                        self.action = action;
8✔
137
                        self.option = None;
16✔
138
                }
139
        }
140

141
        /// Edit the content of the line, if it is editable.
142
        #[inline]
143
        pub fn edit_content(&mut self, content: &str) {
3✔
144
                if self.is_editable() {
5✔
145
                        self.content = String::from(content);
4✔
146
                        self.mutated = true;
2✔
147
                }
148
        }
149

150
        /// Set the option on the line, toggling if the existing option matches.
151
        #[inline]
152
        pub fn toggle_option(&mut self, option: &str) {
2✔
153
                // try toggle off first
154
                if let Some(current) = self.option.as_deref() {
3✔
155
                        if current == option {
×
156
                                self.option = None;
×
157
                                return;
×
158
                        }
159
                }
160
                self.option = Some(String::from(option));
2✔
161
        }
162

163
        /// Get the original line, before any modifications
164
        #[must_use]
165
        #[inline]
166
        pub fn original(&self) -> Option<&Line> {
2✔
167
                self.original_line.as_deref()
2✔
168
        }
169

170
        /// Get the action of the line.
171
        #[must_use]
172
        #[inline]
173
        pub const fn get_action(&self) -> &Action {
5✔
174
                &self.action
5✔
175
        }
176

177
        /// Get the content of the line.
178
        #[must_use]
179
        #[inline]
180
        pub fn get_content(&self) -> &str {
4✔
181
                self.content.as_str()
4✔
182
        }
183

184
        /// Get the commit hash for the line.
185
        #[must_use]
186
        #[inline]
187
        pub fn get_hash(&self) -> &str {
4✔
188
                self.hash.as_str()
4✔
189
        }
190

191
        /// Get the commit hash for the line.
192
        #[must_use]
193
        #[inline]
194
        pub fn option(&self) -> Option<&str> {
1✔
195
                self.option.as_deref()
1✔
196
        }
197

198
        /// Does this line contain a commit reference.
199
        #[must_use]
200
        #[inline]
201
        pub fn has_reference(&self) -> bool {
4✔
202
                !self.hash.is_empty()
4✔
203
        }
204

205
        /// Can this line be edited.
206
        #[must_use]
207
        #[inline]
208
        pub const fn is_editable(&self) -> bool {
5✔
209
                match self.action {
6✔
210
                        Action::Exec | Action::Index | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => true,
2✔
211
                        Action::Break
4✔
NEW
212
                        | Action::Cut
×
213
                        | Action::Drop
×
214
                        | Action::Edit
×
215
                        | Action::Fixup
×
216
                        | Action::Noop
×
217
                        | Action::Pick
×
218
                        | Action::Reword
×
219
                        | Action::Squash => false,
×
220
                }
221
        }
222

223
        /// Has this line been modified
224
        #[must_use]
225
        #[inline]
226
        pub fn is_modified(&self) -> bool {
4✔
227
                self.mutated
4✔
228
        }
229

230
        /// Create a string containing a textual version of the line, as would be seen in the rebase file.
231
        #[must_use]
232
        #[inline]
233
        pub fn to_text(&self) -> String {
4✔
234
                match self.action {
4✔
NEW
235
                        Action::Cut | Action::Drop | Action::Edit | Action::Fixup | Action::Index | Action::Pick | Action::Reword | Action::Squash => {
×
236
                                if let Some(opt) = self.option.as_ref() {
8✔
237
                                        format!("{} {opt} {} {}", self.action, self.hash, self.content)
5✔
238
                                }
239
                                else {
×
240
                                        format!("{} {} {}", self.action, self.hash, self.content)
12✔
241
                                }
242
                        },
243
                        Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => {
×
244
                                format!("{} {}", self.action, self.content)
7✔
245
                        },
246
                        Action::Noop | Action::Break => self.action.to_string(),
2✔
247
                }
248
        }
249
}
250

251
#[cfg(test)]
252
mod tests {
253
        use claims::assert_ok_eq;
254
        use rstest::rstest;
255
        use testutils::assert_err_eq;
256

257
        use super::*;
258

259
        #[rstest]
260
        #[case::pick_action("pick aaa comment", &Line::new(Action::Pick, "aaa", "comment", None))]
261
        #[case::reword_action("reword aaa comment", &Line::new(Action::Reword, "aaa", "comment", None))]
262
        #[case::edit_action("edit aaa comment", &Line::new(Action::Edit, "aaa", "comment", None))]
263
        #[case::squash_action("squash aaa comment", &Line::new(Action::Squash, "aaa", "comment", None))]
264
        #[case::fixup_action("fixup aaa comment", & Line::new(Action::Fixup, "aaa", "comment", None))]
265
        #[case::fixup_with_option_action("fixup -c aaa comment", &Line::new(Action::Fixup, "aaa", "comment", Some("-c")))]
266
        #[case::drop_action("drop aaa comment", &Line::new(Action::Drop, "aaa", "comment", None))]
267
        #[case::action_without_comment("pick aaa", &Line::new(Action::Pick, "aaa", "", None))]
268
        #[case::exec_action("exec command", &Line::new(Action::Exec, "", "command", None))]
269
        #[case::label_action("label ref", &Line::new(Action::Label, "", "ref", None))]
270
        #[case::reset_action("reset ref", &Line::new(Action::Reset, "", "ref", None))]
271
        #[case::reset_action("merge command", &Line::new(Action::Merge, "", "command", None))]
272
        #[case::update_ref_action("update-ref reference", &Line::new(Action::UpdateRef, "", "reference", None))]
273
        #[case::break_action("break", &Line::new(Action::Break, "", "", None))]
274
        #[case::noop( "noop", &Line::new(Action::Noop, "", "", None))]
275
        fn new(#[case] line: &str, #[case] expected: &Line) {
276
                assert_ok_eq!(&Line::parse(line), expected);
277
        }
278

279
        #[test]
280
        fn line_new_pick() {
281
                assert_eq!(Line::new_pick("abc123"), Line {
282
                        action: Action::Pick,
283
                        hash: String::from("abc123"),
284
                        content: String::new(),
285
                        mutated: false,
286
                        option: None,
287
                        original_line: Some(Box::new(Line {
288
                                action: Action::Pick,
289
                                hash: String::from("abc123"),
290
                                content: String::new(),
291
                                mutated: false,
292
                                option: None,
293
                                original_line: None,
294
                        }))
295
                });
296
        }
297

298
        #[test]
299
        fn line_new_break() {
300
                assert_eq!(Line::new_break(), Line {
301
                        action: Action::Break,
302
                        hash: String::new(),
303
                        content: String::new(),
304
                        mutated: false,
305
                        option: None,
306
                        original_line: Some(Box::new(Line {
307
                                action: Action::Break,
308
                                hash: String::new(),
309
                                content: String::new(),
310
                                mutated: false,
311
                                option: None,
312
                                original_line: None,
313
                        }))
314
                });
315
        }
316

317
        #[test]
318
        fn line_new_exec() {
319
                assert_eq!(Line::new_exec("command"), Line {
320
                        action: Action::Exec,
321
                        hash: String::new(),
322
                        content: String::from("command"),
323
                        mutated: false,
324
                        option: None,
325
                        original_line: Some(Box::new(Line {
326
                                action: Action::Exec,
327
                                hash: String::new(),
328
                                content: String::from("command"),
329
                                mutated: false,
330
                                option: None,
331
                                original_line: None,
332
                        }))
333
                });
334
        }
335

336
        #[test]
337
        fn line_new_merge() {
338
                assert_eq!(Line::new_merge("command"), Line {
339
                        action: Action::Merge,
340
                        hash: String::new(),
341
                        content: String::from("command"),
342
                        mutated: false,
343
                        option: None,
344
                        original_line: Some(Box::new(Line {
345
                                action: Action::Merge,
346
                                hash: String::new(),
347
                                content: String::from("command"),
348
                                mutated: false,
349
                                option: None,
350
                                original_line: None,
351
                        }))
352
                });
353
        }
354

355
        #[test]
356
        fn line_new_label() {
357
                assert_eq!(Line::new_label("label"), Line {
358
                        action: Action::Label,
359
                        hash: String::new(),
360
                        content: String::from("label"),
361
                        mutated: false,
362
                        option: None,
363
                        original_line: Some(Box::new(Line {
364
                                action: Action::Label,
365
                                hash: String::new(),
366
                                content: String::from("label"),
367
                                mutated: false,
368
                                option: None,
369
                                original_line: None,
370
                        }))
371
                });
372
        }
373

374
        #[test]
375
        fn line_new_reset() {
376
                assert_eq!(Line::new_reset("label"), Line {
377
                        action: Action::Reset,
378
                        hash: String::new(),
379
                        content: String::from("label"),
380
                        mutated: false,
381
                        option: None,
382
                        original_line: Some(Box::new(Line {
383
                                action: Action::Reset,
384
                                hash: String::new(),
385
                                content: String::from("label"),
386
                                mutated: false,
387
                                option: None,
388
                                original_line: None,
389
                        }))
390
                });
391
        }
392

393
        #[test]
394
        fn line_new_update_ref() {
395
                assert_eq!(Line::new_update_ref("reference"), Line {
396
                        action: Action::UpdateRef,
397
                        hash: String::new(),
398
                        content: String::from("reference"),
399
                        mutated: false,
400
                        option: None,
401
                        original_line: Some(Box::new(Line {
402
                                action: Action::UpdateRef,
403
                                hash: String::new(),
404
                                content: String::from("reference"),
405
                                mutated: false,
406
                                option: None,
407
                                original_line: None,
408
                        }))
409
                });
410
        }
411

412
        #[test]
413
        fn new_err_invalid_action() {
414
                assert_err_eq!(
415
                        Line::parse("invalid aaa comment"),
416
                        ParseError::InvalidAction(String::from("invalid"))
417
                );
418
        }
419

420
        #[rstest]
421
        #[case::pick_line_only("pick")]
422
        #[case::reword_line_only("reword")]
423
        #[case::edit_line_only("edit")]
424
        #[case::squash_line_only("squash")]
425
        #[case::fixup_line_only("fixup")]
426
        #[case::exec_line_only("exec")]
427
        #[case::drop_line_only("drop")]
428
        #[case::label_line_only("label")]
429
        #[case::reset_line_only("reset")]
430
        #[case::merge_line_only("merge")]
431
        #[case::update_ref_line_only("update-ref")]
432
        fn new_err(#[case] line: &str) {
433
                assert_err_eq!(Line::parse(line), ParseError::InvalidLine(String::from(line)));
434
        }
435

436
        #[rstest]
437
        #[case::drop(Action::Drop, Action::Fixup)]
438
        #[case::edit(Action::Edit, Action::Fixup)]
439
        #[case::fixup(Action::Fixup, Action::Pick)]
440
        #[case::pick(Action::Pick, Action::Fixup)]
441
        #[case::reword(Action::Reword, Action::Fixup)]
442
        #[case::squash(Action::Squash, Action::Fixup)]
443
        fn set_action_non_static(#[case] from: Action, #[case] to: Action) {
444
                let mut line = Line::parse(format!("{from} aaa bbb").as_str()).unwrap();
445
                line.set_action(to);
446
                assert_eq!(line.action, to);
447
                assert!(line.is_modified());
448
        }
449

450
        #[rstest]
451
        #[case::break_action(Action::Break, Action::Fixup)]
452
        #[case::label_action(Action::Label, Action::Fixup)]
453
        #[case::reset_action(Action::Reset, Action::Fixup)]
454
        #[case::merge_action(Action::Merge, Action::Fixup)]
455
        #[case::exec(Action::Exec, Action::Fixup)]
456
        #[case::update_ref(Action::UpdateRef, Action::Fixup)]
457
        #[case::noop(Action::Noop, Action::Fixup)]
458
        fn set_action_static(#[case] from: Action, #[case] to: Action) {
459
                let mut line = Line::parse(format!("{from} comment").as_str()).unwrap();
460
                line.set_action(to);
461
                assert_eq!(line.action, from);
462
                assert!(!line.is_modified());
463
        }
464

465
        #[test]
466
        fn set_to_new_action_with_changed_action() {
467
                let mut line = Line::parse("pick aaa comment").unwrap();
468
                line.set_action(Action::Fixup);
469
                assert_eq!(line.action, Action::Fixup);
470
                assert!(line.is_modified());
471
        }
472

473
        #[test]
474
        fn set_to_new_action_with_unchanged_action() {
475
                let mut line = Line::parse("pick aaa comment").unwrap();
476
                line.set_action(Action::Pick);
477
                assert_eq!(line.action, Action::Pick);
478
                assert!(!line.is_modified());
479
        }
480

481
        #[rstest]
482
        #[case::break_action("break", "")]
483
        #[case::drop("drop aaa comment", "comment")]
484
        #[case::edit("edit aaa comment", "comment")]
485
        #[case::exec("exec git commit --amend 'foo'", "new")]
486
        #[case::fixup("fixup aaa comment", "comment")]
487
        #[case::pick("pick aaa comment", "comment")]
488
        #[case::reword("reword aaa comment", "comment")]
489
        #[case::squash("squash aaa comment", "comment")]
490
        #[case::label("label ref", "new")]
491
        #[case::reset("reset ref", "new")]
492
        #[case::merge("merge command", "new")]
493
        #[case::update_ref("update-ref reference", "new")]
494
        fn edit_content(#[case] line: &str, #[case] expected: &str) {
495
                let mut line = Line::parse(line).unwrap();
496
                line.edit_content("new");
497
                assert_eq!(line.get_content(), expected);
498
        }
499

500
        #[rstest]
501
        #[case::break_action("break", "")]
502
        #[case::drop("drop aaa comment", "comment")]
503
        #[case::edit("edit aaa comment", "comment")]
504
        #[case::exec("exec git commit --amend 'foo'", "git commit --amend 'foo'")]
505
        #[case::fixup("fixup aaa comment", "comment")]
506
        #[case::pick("pick aaa comment", "comment")]
507
        #[case::reword("reword aaa comment", "comment")]
508
        #[case::squash("squash aaa comment", "comment")]
509
        #[case::label("label reference", "reference")]
510
        #[case::reset("reset reference", "reference")]
511
        #[case::merge("merge command", "command")]
512
        #[case::update_ref("update-ref reference", "reference")]
513
        fn get_content(#[case] line: &str, #[case] expected: &str) {
514
                assert_eq!(Line::parse(line).unwrap().get_content(), expected);
515
        }
516

517
        #[rstest]
518
        #[case::break_action("break", Action::Break)]
519
        #[case::drop("drop aaa comment", Action::Drop)]
520
        #[case::edit("edit aaa comment", Action::Edit)]
521
        #[case::exec("exec git commit --amend 'foo'", Action::Exec)]
522
        #[case::fixup("fixup aaa comment", Action::Fixup)]
523
        #[case::pick("pick aaa comment", Action::Pick)]
524
        #[case::reword("reword aaa comment", Action::Reword)]
525
        #[case::squash("squash aaa comment", Action::Squash)]
526
        #[case::label("label reference", Action::Label)]
527
        #[case::reset("reset reference", Action::Reset)]
528
        #[case::merge("merge command", Action::Merge)]
529
        #[case::update_ref("update-ref reference", Action::UpdateRef)]
530
        fn get_action(#[case] line: &str, #[case] expected: Action) {
531
                assert_eq!(Line::parse(line).unwrap().get_action(), &expected);
532
        }
533

534
        #[rstest]
535
        #[case::break_action("break", "")]
536
        #[case::drop("drop aaa comment", "aaa")]
537
        #[case::edit("edit aaa comment", "aaa")]
538
        #[case::exec("exec git commit --amend 'foo'", "")]
539
        #[case::fixup("fixup aaa comment", "aaa")]
540
        #[case::pick("pick aaa comment", "aaa")]
541
        #[case::reword("reword aaa comment", "aaa")]
542
        #[case::squash("squash aaa comment", "aaa")]
543
        #[case::label("label reference", "")]
544
        #[case::reset("reset reference", "")]
545
        #[case::merge("merge command", "")]
546
        #[case::update_ref("update-ref reference", "")]
547
        fn get_hash(#[case] line: &str, #[case] expected: &str) {
548
                assert_eq!(Line::parse(line).unwrap().get_hash(), expected);
549
        }
550

551
        #[rstest]
552
        #[case::break_action("break", false)]
553
        #[case::drop("drop aaa comment", true)]
554
        #[case::edit("edit aaa comment", true)]
555
        #[case::exec("exec git commit --amend 'foo'", false)]
556
        #[case::fixup("fixup aaa comment", true)]
557
        #[case::pick("pick aaa comment", true)]
558
        #[case::reword("reword aaa comment", true)]
559
        #[case::squash("squash aaa comment", true)]
560
        #[case::label("label ref", false)]
561
        #[case::reset("reset ref", false)]
562
        #[case::merge("merge command", false)]
563
        #[case::update_ref("update-ref reference", false)]
564
        fn has_reference(#[case] line: &str, #[case] expected: bool) {
565
                assert_eq!(Line::parse(line).unwrap().has_reference(), expected);
566
        }
567

568
        #[rstest]
569
        #[case::drop(Action::Break, false)]
570
        #[case::drop(Action::Drop, false)]
571
        #[case::edit(Action::Edit, false)]
572
        #[case::exec(Action::Exec, true)]
573
        #[case::fixup(Action::Fixup, false)]
574
        #[case::pick(Action::Noop, false)]
575
        #[case::pick(Action::Pick, false)]
576
        #[case::reword(Action::Reword, false)]
577
        #[case::squash(Action::Squash, false)]
578
        #[case::label(Action::Label, true)]
579
        #[case::reset(Action::Reset, true)]
580
        #[case::merge(Action::Merge, true)]
581
        #[case::update_ref(Action::UpdateRef, true)]
582
        fn is_editable(#[case] from: Action, #[case] editable: bool) {
583
                let line = Line::parse(format!("{from} aaa bbb").as_str()).unwrap();
584
                assert_eq!(line.is_editable(), editable);
585
        }
586

587
        #[rstest]
588
        #[case::break_action("break")]
589
        #[case::drop("drop aaa comment")]
590
        #[case::edit("edit aaa comment")]
591
        #[case::exec("exec git commit --amend 'foo'")]
592
        #[case::fixup("fixup aaa comment")]
593
        #[case::fixup_with_options("fixup -c aaa comment")]
594
        #[case::pick("pick aaa comment")]
595
        #[case::reword("reword aaa comment")]
596
        #[case::squash("squash aaa comment")]
597
        #[case::label("label reference")]
598
        #[case::reset("reset reference")]
599
        #[case::merge("merge command")]
600
        #[case::update_ref("update-ref reference")]
601
        fn to_text(#[case] line: &str) {
602
                assert_eq!(Line::parse(line).unwrap().to_text(), line);
603
        }
604
}
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