1use crate::{errors::ParseError, line_parser::LineParser, Action};
2
3#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct Line {
6 action: Action,
7 content: String,
8 hash: String,
9 mutated: bool,
10 option: Option<String>,
11}
12
13impl Line {
14 #[must_use]
16 const fn new_noop() -> Self {
17 Self {
18 action: Action::Noop,
19 content: String::new(),
20 hash: String::new(),
21 mutated: false,
22 option: None,
23 }
24 }
25
26 #[must_use]
28 #[inline]
29 pub fn new_pick(hash: &str) -> Self {
30 Self {
31 action: Action::Pick,
32 content: String::new(),
33 hash: String::from(hash),
34 mutated: false,
35 option: None,
36 }
37 }
38
39 #[must_use]
41 #[inline]
42 pub const fn new_break() -> Self {
43 Self {
44 action: Action::Break,
45 content: String::new(),
46 hash: String::new(),
47 mutated: false,
48 option: None,
49 }
50 }
51
52 #[must_use]
54 #[inline]
55 pub fn new_exec(command: &str) -> Self {
56 Self {
57 action: Action::Exec,
58 content: String::from(command),
59 hash: String::new(),
60 mutated: false,
61 option: None,
62 }
63 }
64
65 #[must_use]
67 #[inline]
68 pub fn new_merge(command: &str) -> Self {
69 Self {
70 action: Action::Merge,
71 content: String::from(command),
72 hash: String::new(),
73 mutated: false,
74 option: None,
75 }
76 }
77
78 #[must_use]
80 #[inline]
81 pub fn new_label(label: &str) -> Self {
82 Self {
83 action: Action::Label,
84 content: String::from(label),
85 hash: String::new(),
86 mutated: false,
87 option: None,
88 }
89 }
90
91 #[must_use]
93 #[inline]
94 pub fn new_reset(label: &str) -> Self {
95 Self {
96 action: Action::Reset,
97 content: String::from(label),
98 hash: String::new(),
99 mutated: false,
100 option: None,
101 }
102 }
103
104 #[must_use]
106 #[inline]
107 pub fn new_update_ref(ref_name: &str) -> Self {
108 Self {
109 action: Action::UpdateRef,
110 content: String::from(ref_name),
111 hash: String::new(),
112 mutated: false,
113 option: None,
114 }
115 }
116
117 #[inline]
123 pub fn new(input_line: &str) -> Result<Self, ParseError> {
124 let mut line_parser = LineParser::new(input_line);
125
126 let action = Action::try_from(line_parser.next()?)?;
127 Ok(match action {
128 Action::Noop => Self::new_noop(),
129 Action::Break => Self::new_break(),
130 Action::Pick | Action::Reword | Action::Edit | Action::Squash | Action::Drop => {
131 let hash = String::from(line_parser.next()?);
132 Self {
133 action,
134 hash,
135 content: String::from(line_parser.take_remaining()),
136 mutated: false,
137 option: None,
138 }
139 },
140 Action::Fixup => {
141 let mut next = line_parser.next()?;
142
143 let option = if next.starts_with('-') {
144 let opt = String::from(next);
145 next = line_parser.next()?;
146 Some(opt)
147 }
148 else {
149 None
150 };
151
152 let hash = String::from(next);
153
154 Self {
155 action,
156 hash,
157 content: String::from(line_parser.take_remaining()),
158 mutated: false,
159 option,
160 }
161 },
162 Action::Exec | Action::Merge | Action::Label | Action::Reset | Action::UpdateRef => {
163 if !line_parser.has_more() {
164 return Err(line_parser.parse_error());
165 }
166 Self {
167 action,
168 hash: String::new(),
169 content: String::from(line_parser.take_remaining()),
170 mutated: false,
171 option: None,
172 }
173 },
174 })
175 }
176
177 #[inline]
179 pub fn set_action(&mut self, action: Action) {
180 if !self.action.is_static() && self.action != action {
181 self.mutated = true;
182 self.action = action;
183 self.option = None;
184 }
185 }
186
187 #[inline]
189 pub fn edit_content(&mut self, content: &str) {
190 if self.is_editable() {
191 self.content = String::from(content);
192 }
193 }
194
195 #[inline]
197 pub fn toggle_option(&mut self, option: &str) {
198 if let Some(current) = self.option.as_deref() {
200 if current == option {
201 self.option = None;
202 return;
203 }
204 }
205 self.option = Some(String::from(option));
206 }
207
208 #[must_use]
210 #[inline]
211 pub const fn get_action(&self) -> &Action {
212 &self.action
213 }
214
215 #[must_use]
217 #[inline]
218 pub fn get_content(&self) -> &str {
219 self.content.as_str()
220 }
221
222 #[must_use]
224 #[inline]
225 pub fn get_hash(&self) -> &str {
226 self.hash.as_str()
227 }
228
229 #[must_use]
231 #[inline]
232 pub fn option(&self) -> Option<&str> {
233 self.option.as_deref()
234 }
235
236 #[must_use]
238 #[inline]
239 pub fn has_reference(&self) -> bool {
240 !self.hash.is_empty()
241 }
242
243 #[must_use]
245 #[inline]
246 pub const fn is_editable(&self) -> bool {
247 match self.action {
248 Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => true,
249 Action::Break
250 | Action::Drop
251 | Action::Edit
252 | Action::Fixup
253 | Action::Noop
254 | Action::Pick
255 | Action::Reword
256 | Action::Squash => false,
257 }
258 }
259
260 #[must_use]
262 #[inline]
263 pub fn to_text(&self) -> String {
264 match self.action {
265 Action::Drop | Action::Edit | Action::Fixup | Action::Pick | Action::Reword | Action::Squash => {
266 if let Some(opt) = self.option.as_ref() {
267 format!("{} {opt} {} {}", self.action, self.hash, self.content)
268 }
269 else {
270 format!("{} {} {}", self.action, self.hash, self.content)
271 }
272 },
273 Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => {
274 format!("{} {}", self.action, self.content)
275 },
276 Action::Noop | Action::Break => self.action.to_string(),
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use claim::assert_ok_eq;
284 use rstest::rstest;
285 use testutils::assert_err_eq;
286
287 use super::*;
288
289 #[rstest]
290 #[case::pick_action("pick aaa comment", &Line {
291 action: Action::Pick,
292 hash: String::from("aaa"),
293 content: String::from("comment"),
294 mutated: false,
295 option: None,
296 })]
297 #[case::reword_action("reword aaa comment", &Line {
298 action: Action::Reword,
299 hash: String::from("aaa"),
300 content: String::from("comment"),
301 mutated: false,
302 option: None,
303 })]
304 #[case::edit_action("edit aaa comment", &Line {
305 action: Action::Edit,
306 hash: String::from("aaa"),
307 content: String::from("comment"),
308 mutated: false,
309 option: None,
310 })]
311 #[case::squash_action("squash aaa comment", &Line {
312 action: Action::Squash,
313 hash: String::from("aaa"),
314 content: String::from("comment"),
315 mutated: false,
316 option: None,
317 })]
318 #[case::fixup_action("fixup aaa comment", &Line {
319 action: Action::Fixup,
320 hash: String::from("aaa"),
321 content: String::from("comment"),
322 mutated: false,
323 option: None,
324 })]
325 #[case::fixup_with_option_action("fixup -c aaa comment", &Line {
326 action: Action::Fixup,
327 hash: String::from("aaa"),
328 content: String::from("comment"),
329 mutated: false,
330 option: Some(String::from("-c")),
331 })]
332 #[case::drop_action("drop aaa comment", &Line {
333 action: Action::Drop,
334 hash: String::from("aaa"),
335 content: String::from("comment"),
336 mutated: false,
337 option: None,
338 })]
339 #[case::action_without_comment("pick aaa", &Line {
340 action: Action::Pick,
341 hash: String::from("aaa"),
342 content: String::new(),
343 mutated: false,
344 option: None,
345 })]
346 #[case::exec_action("exec command", &Line {
347 action: Action::Exec,
348 hash: String::new(),
349 content: String::from("command"),
350 mutated: false,
351 option: None,
352 })]
353 #[case::label_action("label ref", &Line {
354 action: Action::Label,
355 hash: String::new(),
356 content: String::from("ref"),
357 mutated: false,
358 option: None,
359 })]
360 #[case::reset_action("reset ref", &Line {
361 action: Action::Reset,
362 hash: String::new(),
363 content: String::from("ref"),
364 mutated: false,
365 option: None,
366 })]
367 #[case::reset_action("merge command", &Line {
368 action: Action::Merge,
369 hash: String::new(),
370 content: String::from("command"),
371 mutated: false,
372 option: None,
373 })]
374 #[case::update_ref_action("update-ref reference", &Line {
375 action: Action::UpdateRef,
376 hash: String::new(),
377 content: String::from("reference"),
378 mutated: false,
379 option: None,
380 })]
381 #[case::break_action("break", &Line {
382 action: Action::Break,
383 hash: String::new(),
384 content: String::new(),
385 mutated: false,
386 option: None,
387 })]
388 #[case::nnop( "noop", &Line {
389 action: Action::Noop,
390 hash: String::new(),
391 content: String::new(),
392 mutated: false,
393 option: None,
394 })]
395 fn new(#[case] line: &str, #[case] expected: &Line) {
396 assert_ok_eq!(&Line::new(line), expected);
397 }
398
399 #[test]
400 fn line_new_pick() {
401 assert_eq!(Line::new_pick("abc123"), Line {
402 action: Action::Pick,
403 hash: String::from("abc123"),
404 content: String::new(),
405 mutated: false,
406 option: None,
407 });
408 }
409
410 #[test]
411 fn line_new_break() {
412 assert_eq!(Line::new_break(), Line {
413 action: Action::Break,
414 hash: String::new(),
415 content: String::new(),
416 mutated: false,
417 option: None,
418 });
419 }
420
421 #[test]
422 fn line_new_exec() {
423 assert_eq!(Line::new_exec("command"), Line {
424 action: Action::Exec,
425 hash: String::new(),
426 content: String::from("command"),
427 mutated: false,
428 option: None,
429 });
430 }
431
432 #[test]
433 fn line_new_merge() {
434 assert_eq!(Line::new_merge("command"), Line {
435 action: Action::Merge,
436 hash: String::new(),
437 content: String::from("command"),
438 mutated: false,
439 option: None,
440 });
441 }
442
443 #[test]
444 fn line_new_label() {
445 assert_eq!(Line::new_label("label"), Line {
446 action: Action::Label,
447 hash: String::new(),
448 content: String::from("label"),
449 mutated: false,
450 option: None,
451 });
452 }
453
454 #[test]
455 fn line_new_reset() {
456 assert_eq!(Line::new_reset("label"), Line {
457 action: Action::Reset,
458 hash: String::new(),
459 content: String::from("label"),
460 mutated: false,
461 option: None,
462 });
463 }
464
465 #[test]
466 fn line_new_update_ref() {
467 assert_eq!(Line::new_update_ref("reference"), Line {
468 action: Action::UpdateRef,
469 hash: String::new(),
470 content: String::from("reference"),
471 mutated: false,
472 option: None,
473 });
474 }
475
476 #[test]
477 fn new_err_invalid_action() {
478 assert_err_eq!(
479 Line::new("invalid aaa comment"),
480 ParseError::InvalidAction(String::from("invalid"))
481 );
482 }
483
484 #[rstest]
485 #[case::pick_line_only("pick")]
486 #[case::reword_line_only("reword")]
487 #[case::edit_line_only("edit")]
488 #[case::squash_line_only("squash")]
489 #[case::fixup_line_only("fixup")]
490 #[case::exec_line_only("exec")]
491 #[case::drop_line_only("drop")]
492 #[case::label_line_only("label")]
493 #[case::reset_line_only("reset")]
494 #[case::merge_line_only("merge")]
495 #[case::update_ref_line_only("update-ref")]
496 fn new_err(#[case] line: &str) {
497 assert_err_eq!(Line::new(line), ParseError::InvalidLine(String::from(line)));
498 }
499
500 #[rstest]
501 #[case::drop(Action::Drop, Action::Fixup)]
502 #[case::edit(Action::Edit, Action::Fixup)]
503 #[case::fixup(Action::Fixup, Action::Pick)]
504 #[case::pick(Action::Pick, Action::Fixup)]
505 #[case::reword(Action::Reword, Action::Fixup)]
506 #[case::squash(Action::Squash, Action::Fixup)]
507 fn set_action_non_static(#[case] from: Action, #[case] to: Action) {
508 let mut line = Line::new(format!("{from} aaa bbb").as_str()).unwrap();
509 line.set_action(to);
510 assert_eq!(line.action, to);
511 assert!(line.mutated);
512 }
513
514 #[rstest]
515 #[case::break_action(Action::Break, Action::Fixup)]
516 #[case::label_action(Action::Label, Action::Fixup)]
517 #[case::reset_action(Action::Reset, Action::Fixup)]
518 #[case::merge_action(Action::Merge, Action::Fixup)]
519 #[case::exec(Action::Exec, Action::Fixup)]
520 #[case::update_ref(Action::UpdateRef, Action::Fixup)]
521 #[case::noop(Action::Noop, Action::Fixup)]
522 fn set_action_static(#[case] from: Action, #[case] to: Action) {
523 let mut line = Line::new(format!("{from} comment").as_str()).unwrap();
524 line.set_action(to);
525 assert_eq!(line.action, from);
526 assert!(!line.mutated);
527 }
528
529 #[test]
530 fn set_to_new_action_with_changed_action() {
531 let mut line = Line::new("pick aaa comment").unwrap();
532 line.set_action(Action::Fixup);
533 assert_eq!(line.action, Action::Fixup);
534 assert!(line.mutated);
535 }
536
537 #[test]
538 fn set_to_new_action_with_unchanged_action() {
539 let mut line = Line::new("pick aaa comment").unwrap();
540 line.set_action(Action::Pick);
541 assert_eq!(line.action, Action::Pick);
542 assert!(!line.mutated);
543 }
544
545 #[rstest]
546 #[case::break_action("break", "")]
547 #[case::drop("drop aaa comment", "comment")]
548 #[case::edit("edit aaa comment", "comment")]
549 #[case::exec("exec git commit --amend 'foo'", "new")]
550 #[case::fixup("fixup aaa comment", "comment")]
551 #[case::pick("pick aaa comment", "comment")]
552 #[case::reword("reword aaa comment", "comment")]
553 #[case::squash("squash aaa comment", "comment")]
554 #[case::label("label ref", "new")]
555 #[case::reset("reset ref", "new")]
556 #[case::merge("merge command", "new")]
557 #[case::update_ref("update-ref reference", "new")]
558 fn edit_content(#[case] line: &str, #[case] expected: &str) {
559 let mut line = Line::new(line).unwrap();
560 line.edit_content("new");
561 assert_eq!(line.get_content(), expected);
562 }
563
564 #[rstest]
565 #[case::break_action("break", "")]
566 #[case::drop("drop aaa comment", "comment")]
567 #[case::edit("edit aaa comment", "comment")]
568 #[case::exec("exec git commit --amend 'foo'", "git commit --amend 'foo'")]
569 #[case::fixup("fixup aaa comment", "comment")]
570 #[case::pick("pick aaa comment", "comment")]
571 #[case::reword("reword aaa comment", "comment")]
572 #[case::squash("squash aaa comment", "comment")]
573 #[case::label("label reference", "reference")]
574 #[case::reset("reset reference", "reference")]
575 #[case::merge("merge command", "command")]
576 #[case::update_ref("update-ref reference", "reference")]
577 fn get_content(#[case] line: &str, #[case] expected: &str) {
578 assert_eq!(Line::new(line).unwrap().get_content(), expected);
579 }
580
581 #[rstest]
582 #[case::break_action("break", Action::Break)]
583 #[case::drop("drop aaa comment", Action::Drop)]
584 #[case::edit("edit aaa comment", Action::Edit)]
585 #[case::exec("exec git commit --amend 'foo'", Action::Exec)]
586 #[case::fixup("fixup aaa comment", Action::Fixup)]
587 #[case::pick("pick aaa comment", Action::Pick)]
588 #[case::reword("reword aaa comment", Action::Reword)]
589 #[case::squash("squash aaa comment", Action::Squash)]
590 #[case::label("label reference", Action::Label)]
591 #[case::reset("reset reference", Action::Reset)]
592 #[case::merge("merge command", Action::Merge)]
593 #[case::update_ref("update-ref reference", Action::UpdateRef)]
594 fn get_action(#[case] line: &str, #[case] expected: Action) {
595 assert_eq!(Line::new(line).unwrap().get_action(), &expected);
596 }
597
598 #[rstest]
599 #[case::break_action("break", "")]
600 #[case::drop("drop aaa comment", "aaa")]
601 #[case::edit("edit aaa comment", "aaa")]
602 #[case::exec("exec git commit --amend 'foo'", "")]
603 #[case::fixup("fixup aaa comment", "aaa")]
604 #[case::pick("pick aaa comment", "aaa")]
605 #[case::reword("reword aaa comment", "aaa")]
606 #[case::squash("squash aaa comment", "aaa")]
607 #[case::label("label reference", "")]
608 #[case::reset("reset reference", "")]
609 #[case::merge("merge command", "")]
610 #[case::update_ref("update-ref reference", "")]
611 fn get_hash(#[case] line: &str, #[case] expected: &str) {
612 assert_eq!(Line::new(line).unwrap().get_hash(), expected);
613 }
614
615 #[rstest]
616 #[case::break_action("break", false)]
617 #[case::drop("drop aaa comment", true)]
618 #[case::edit("edit aaa comment", true)]
619 #[case::exec("exec git commit --amend 'foo'", false)]
620 #[case::fixup("fixup aaa comment", true)]
621 #[case::pick("pick aaa comment", true)]
622 #[case::reword("reword aaa comment", true)]
623 #[case::squash("squash aaa comment", true)]
624 #[case::label("label ref", false)]
625 #[case::reset("reset ref", false)]
626 #[case::merge("merge command", false)]
627 #[case::update_ref("update-ref reference", false)]
628 fn has_reference(#[case] line: &str, #[case] expected: bool) {
629 assert_eq!(Line::new(line).unwrap().has_reference(), expected);
630 }
631
632 #[rstest]
633 #[case::drop(Action::Break, false)]
634 #[case::drop(Action::Drop, false)]
635 #[case::edit(Action::Edit, false)]
636 #[case::exec(Action::Exec, true)]
637 #[case::fixup(Action::Fixup, false)]
638 #[case::pick(Action::Noop, false)]
639 #[case::pick(Action::Pick, false)]
640 #[case::reword(Action::Reword, false)]
641 #[case::squash(Action::Squash, false)]
642 #[case::label(Action::Label, true)]
643 #[case::reset(Action::Reset, true)]
644 #[case::merge(Action::Merge, true)]
645 #[case::update_ref(Action::UpdateRef, true)]
646 fn is_editable(#[case] from: Action, #[case] editable: bool) {
647 let line = Line::new(format!("{from} aaa bbb").as_str()).unwrap();
648 assert_eq!(line.is_editable(), editable);
649 }
650
651 #[rstest]
652 #[case::break_action("break")]
653 #[case::drop("drop aaa comment")]
654 #[case::edit("edit aaa comment")]
655 #[case::exec("exec git commit --amend 'foo'")]
656 #[case::fixup("fixup aaa comment")]
657 #[case::fixup_with_options("fixup -c aaa comment")]
658 #[case::pick("pick aaa comment")]
659 #[case::reword("reword aaa comment")]
660 #[case::squash("squash aaa comment")]
661 #[case::label("label reference")]
662 #[case::reset("reset reference")]
663 #[case::merge("merge command")]
664 #[case::update_ref("update-ref reference")]
665 fn to_text(#[case] line: &str) {
666 assert_eq!(Line::new(line).unwrap().to_text(), line);
667 }
668}