todo_file/
line.rs

1use crate::{errors::ParseError, line_parser::LineParser, Action};
2
3/// Represents a line in the rebase file.
4#[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	/// Create a new noop line.
15	#[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	/// Create a new pick line.
27	#[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	/// Create a new break line.
40	#[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	/// Create a new exec line.
53	#[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	/// Create a new merge line.
66	#[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	/// Create a new label line.
79	#[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	/// Create a new reset line.
92	#[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	/// Create a new update-ref line.
105	#[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	/// Create a new line from a rebase file line.
118	///
119	/// # Errors
120	///
121	/// Returns an error if an invalid line is provided.
122	#[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	/// Set the action of the line.
178	#[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	/// Edit the content of the line, if it is editable.
188	#[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	/// Set the option on the line, toggling if the existing option matches.
196	#[inline]
197	pub fn toggle_option(&mut self, option: &str) {
198		// try toggle off first
199		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	/// Get the action of the line.
209	#[must_use]
210	#[inline]
211	pub const fn get_action(&self) -> &Action {
212		&self.action
213	}
214
215	/// Get the content of the line.
216	#[must_use]
217	#[inline]
218	pub fn get_content(&self) -> &str {
219		self.content.as_str()
220	}
221
222	/// Get the commit hash for the line.
223	#[must_use]
224	#[inline]
225	pub fn get_hash(&self) -> &str {
226		self.hash.as_str()
227	}
228
229	/// Get the commit hash for the line.
230	#[must_use]
231	#[inline]
232	pub fn option(&self) -> Option<&str> {
233		self.option.as_deref()
234	}
235
236	/// Does this line contain a commit reference.
237	#[must_use]
238	#[inline]
239	pub fn has_reference(&self) -> bool {
240		!self.hash.is_empty()
241	}
242
243	/// Can this line be edited.
244	#[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	/// Create a string containing a textual version of the line, as would be seen in the rebase file.
261	#[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}