1use std::cmp::Ordering;
2
3use orfail::OrFail;
4use tuinix::{TerminalSize, TerminalStyle};
5
6use crate::{
7 canvas::{Canvas, Token},
8 diff::{ChunkDiff, ContentDiff, Diff, FileDiff, LineDiff},
9 git,
10};
11
12#[derive(Debug, Clone)]
13pub struct DiffTreeWidget {
14 unstaged_diff: PhasedDiff,
15 staged_diff: PhasedDiff,
16 root_node: DiffTreeNode,
17 cursor: Cursor,
18}
19
20impl DiffTreeWidget {
21 pub fn new(terminal_size: TerminalSize) -> orfail::Result<Self> {
22 let mut this = Self {
23 unstaged_diff: PhasedDiff {
24 phase: DiffPhase::Unstaged,
25 diff: Diff::default(),
26 },
27 staged_diff: PhasedDiff {
28 phase: DiffPhase::Staged,
29 diff: Diff::default(),
30 },
31 root_node: DiffTreeNode::new_root_node(),
32 cursor: Cursor::root(),
33 };
34 this.reload().or_fail()?;
35 this.expand_if_possible(terminal_size).or_fail()?;
36 Ok(this)
37 }
38
39 pub fn render(&self, canvas: &mut Canvas) {
40 for (node, diff) in self.children_and_diffs() {
41 if !node.render_if_need(canvas, &self.cursor, diff) {
42 break;
43 }
44 }
45 }
46
47 pub fn can_cursor_up(&self) -> bool {
48 self.root_node.cursor_up(&self.cursor).is_some()
49 }
50
51 pub fn can_cursor_down(&self) -> bool {
52 self.root_node.cursor_down(&self.cursor).is_some()
53 }
54
55 pub fn can_cursor_right(&self) -> bool {
56 self.root_node.cursor_right(&self.cursor).is_some()
57 }
58
59 pub fn can_cursor_left(&self) -> bool {
60 self.cursor.parent().is_some()
61 }
62
63 pub fn can_toggle(&self) -> bool {
64 self.root_node
65 .get_node(&self.cursor)
66 .ok()
67 .is_some_and(|n| !n.children.is_empty())
68 }
69
70 pub fn can_stage_or_discard(&self) -> bool {
71 self.root_node.children[0]
72 .can_alter(&self.cursor, &self.unstaged_diff)
73 .ok()
74 .is_some_and(|b| b)
75 }
76
77 pub fn can_unstage(&self) -> bool {
78 self.root_node.children[1]
79 .can_alter(&self.cursor, &self.staged_diff)
80 .ok()
81 .is_some_and(|b| b)
82 }
83
84 pub fn cursor_up(&mut self) -> orfail::Result<bool> {
85 if let Some(new_cursor) = self.root_node.cursor_up(&self.cursor) {
86 self.cursor = new_cursor;
87 self.expand_parent().or_fail()?;
88 Ok(true)
89 } else {
90 Ok(false)
91 }
92 }
93
94 pub fn cursor_down(&mut self) -> orfail::Result<bool> {
95 if let Some(new_cursor) = self.root_node.cursor_down(&self.cursor) {
96 self.cursor = new_cursor;
97 self.expand_parent().or_fail()?;
98 Ok(true)
99 } else {
100 Ok(false)
101 }
102 }
103
104 pub fn cursor_right(&mut self) -> orfail::Result<bool> {
105 if let Some(new_cursor) = self.root_node.cursor_right(&self.cursor) {
106 self.cursor = new_cursor;
107 self.expand_parent().or_fail()?;
108 Ok(true)
109 } else {
110 Ok(false)
111 }
112 }
113
114 pub fn cursor_left(&mut self) -> bool {
115 if let Some(parent) = self.cursor.parent() {
116 self.cursor = parent;
117 true
118 } else {
119 false
120 }
121 }
122
123 pub fn cursor_row(&self) -> usize {
124 let root_node_offset = 1;
125 self.root_node.cursor_row(&self.cursor) - root_node_offset
126 }
127
128 pub fn toggle(&mut self) -> orfail::Result<()> {
129 self.root_node.toggle(&self.cursor).or_fail()
130 }
131
132 pub fn stage(&mut self) -> orfail::Result<bool> {
133 if !self.can_stage_or_discard() {
134 return Ok(false);
135 }
136 self.root_node.children[0]
137 .stage(&self.cursor, &self.unstaged_diff.diff)
138 .or_fail()?;
139 self.reload().or_fail()?;
140 Ok(true)
141 }
142
143 pub fn discard(&mut self) -> orfail::Result<bool> {
144 if !self.can_stage_or_discard() {
145 return Ok(false);
146 }
147 self.root_node.children[0]
148 .discard(&self.cursor, &self.unstaged_diff.diff)
149 .or_fail()?;
150 self.reload().or_fail()?;
151 Ok(true)
152 }
153
154 pub fn unstage(&mut self) -> orfail::Result<bool> {
155 if !self.can_unstage() {
156 return Ok(false);
157 }
158 self.root_node.children[1]
159 .unstage(&self.cursor, &self.staged_diff.diff)
160 .or_fail()?;
161 self.reload().or_fail()?;
162 Ok(true)
163 }
164
165 fn expand_if_possible(&mut self, terminal_size: TerminalSize) -> orfail::Result<()> {
166 if !self.cursor_right().or_fail()? {
167 return Ok(());
168 }
169
170 loop {
171 self.root_node.toggle(&self.cursor).or_fail()?;
172 if self.rows() > terminal_size.rows {
173 self.root_node.toggle(&self.cursor).or_fail()?;
174 break;
175 }
176 if !self.cursor_down().or_fail()? {
177 break;
178 }
179 }
180
181 self.cursor = Cursor::root();
182 Ok(())
183 }
184
185 fn expand_parent(&mut self) -> orfail::Result<()> {
186 if let Some(parent) = self.cursor.parent() {
187 self.root_node.get_node_mut(&parent).or_fail()?.expanded = true;
188 }
189 Ok(())
190 }
191
192 fn rows(&self) -> usize {
193 let root_node_offset = 1;
194 self.root_node.rows() - root_node_offset
195 }
196
197 pub fn reload(&mut self) -> orfail::Result<()> {
198 let old = self.clone();
199 let (unstaged_diff, staged_diff) = git::unstaged_and_staged_diffs().or_fail()?;
200
201 self.unstaged_diff.diff = unstaged_diff;
202 self.staged_diff.diff = staged_diff;
203 for (node, diff) in self.children_and_diffs_mut() {
204 node.children.clear();
205 for (i, file) in diff.diff.files.iter().enumerate() {
206 let path = node.path.join(i);
207 let child = DiffTreeNode::new_file_diff_node(path, file);
208 node.children.push(child);
209 }
210
211 node.restore_expanded_state(
212 &diff.diff,
213 &old.children_and_diffs()
214 .map(|x| (x.0, &x.1.diff))
215 .collect::<Vec<_>>(),
216 );
217 }
218
219 while !self.root_node.is_valid_cursor(&self.cursor) {
220 if let Some(sibling_cursor) = self.cursor.prev_sibling() {
221 self.cursor = sibling_cursor;
222 } else if let Some(parent_cursor) = self.cursor.parent() {
223 self.cursor = parent_cursor;
224 } else {
225 self.cursor = Cursor::root();
226 break;
227 }
228 }
229
230 self.expand_parent().or_fail()?;
231
232 Ok(())
233 }
234
235 fn children_and_diffs(&self) -> impl '_ + Iterator<Item = (&DiffTreeNode, &PhasedDiff)> {
236 self.root_node
237 .children
238 .iter()
239 .zip([&self.unstaged_diff, &self.staged_diff])
240 }
241
242 fn children_and_diffs_mut(
243 &mut self,
244 ) -> impl '_ + Iterator<Item = (&mut DiffTreeNode, &mut PhasedDiff)> {
245 self.root_node
246 .children
247 .iter_mut()
248 .zip([&mut self.unstaged_diff, &mut self.staged_diff])
249 }
250}
251
252#[derive(Debug, Clone)]
253struct DiffTreeNode {
254 path: NodePath,
255 expanded: bool,
256 children: Vec<Self>,
257}
258
259impl DiffTreeNode {
260 fn new_root_node() -> Self {
261 let root_path = NodePath::root();
262 Self {
263 path: root_path.clone(),
264 expanded: true,
265 children: vec![
266 Self::new_diff_node(root_path.join(0)),
267 Self::new_diff_node(root_path.join(1)),
268 ],
269 }
270 }
271
272 fn new_diff_node(path: NodePath) -> Self {
273 Self {
274 path,
275 expanded: true,
276 children: Vec::new(),
277 }
278 }
279
280 fn new_file_diff_node(path: NodePath, diff: &FileDiff) -> Self {
281 let children = diff
282 .chunks()
283 .iter()
284 .enumerate()
285 .map(|(i, c)| DiffTreeNode::new_chunk_diff_node(path.join(i), c))
286 .collect();
287 Self {
288 path,
289 expanded: false,
290 children,
291 }
292 }
293
294 fn new_chunk_diff_node(path: NodePath, diff: &ChunkDiff) -> Self {
295 let children = (0..diff.lines.len())
296 .map(|i| DiffTreeNode::new_line_diff_node(path.join(i)))
297 .collect();
298 Self {
299 path,
300 expanded: true,
301 children,
302 }
303 }
304
305 fn new_line_diff_node(path: NodePath) -> Self {
306 Self {
307 path,
308 expanded: false,
309 children: Vec::new(),
310 }
311 }
312
313 fn restore_expanded_state(&mut self, diff: &Diff, old: &[(&Self, &Diff)]) {
314 if old.is_empty() {
315 return;
316 }
317
318 self.expanded = old.iter().any(|x| x.0.expanded);
319
320 for (c, d) in self.children.iter_mut().zip(diff.files.iter()) {
321 let expanded = old
322 .iter()
323 .flat_map(|x| x.0.children.iter().zip(x.1.files.iter()))
324 .filter(|x| x.1.path() == d.path())
325 .any(|x| x.0.expanded);
326 c.expanded = expanded;
327 }
328 }
329
330 fn render<T>(&self, canvas: &mut Canvas, cursor: &Cursor, content: &T)
331 where
332 T: DiffTreeNodeContent,
333 {
334 cursor.render(canvas, &self.path);
335 for token in content.head_line_tokens() {
336 canvas.draw(token);
337 }
338 if !self.expanded && !self.children.is_empty() {
339 canvas.draw(Token::new("…"));
340 }
341 canvas.newline();
342
343 if self.expanded {
344 for child in self.children.iter().zip(content.children().iter()) {
345 if !child.0.render_if_need(canvas, cursor, child.1) {
346 break;
347 }
348 }
349 }
350 }
351
352 fn render_if_need<T>(&self, canvas: &mut Canvas, cursor: &Cursor, content: &T) -> bool
353 where
354 T: DiffTreeNodeContent,
355 {
356 if canvas.is_frame_exceeded() {
357 return false;
358 }
359
360 let mut canvas_cursor = canvas.cursor();
361 let drawn_rows = self.rows();
362 if canvas
363 .frame_row_range()
364 .start
365 .checked_sub(canvas_cursor.row)
366 .is_some_and(|n| n >= drawn_rows)
367 {
368 canvas_cursor.row += drawn_rows;
369 canvas.set_cursor(canvas_cursor);
370 } else {
371 self.render(canvas, cursor, content);
372 }
373 true
374 }
375
376 fn rows(&self) -> usize {
377 if self.expanded {
378 1 + self.children.iter().map(|c| c.rows()).sum::<usize>()
379 } else {
380 1
381 }
382 }
383
384 fn cursor_row(&self, cursor: &Cursor) -> usize {
385 match cursor.path.0[..self.path.len()].cmp(&self.path.0) {
386 Ordering::Less => 0,
387 Ordering::Equal if cursor.path.len() == self.path.len() => 0,
388 Ordering::Equal => {
389 1 + self
390 .children
391 .iter()
392 .map(|c| c.cursor_row(cursor))
393 .sum::<usize>()
394 }
395 Ordering::Greater => self.rows(),
396 }
397 }
398
399 fn check_cursor(&self, cursor: &Cursor) -> orfail::Result<()> {
400 cursor.path.starts_with(&self.path).or_fail_with(|()| {
401 format!(
402 "invalid cursor: path={:?}, cursor={:?}",
403 self.path, cursor.path
404 )
405 })?;
406 Ok(())
407 }
408
409 fn can_alter<T>(&self, cursor: &Cursor, content: &T) -> orfail::Result<bool>
410 where
411 T: DiffTreeNodeContent,
412 {
413 self.check_cursor(cursor).or_fail()?;
414
415 if let Some(i) = cursor.path.get(self.path.len()) {
416 let child_node = self.children.get(i).or_fail()?;
417 let child_content = content.children().get(i).or_fail()?;
418 child_node.can_alter(cursor, child_content).or_fail()
419 } else {
420 Ok(content.can_alter())
421 }
422 }
423
424 fn is_valid_cursor(&self, cursor: &Cursor) -> bool {
425 self.get_node(cursor).is_ok()
426 }
427
428 fn toggle(&mut self, cursor: &Cursor) -> orfail::Result<()> {
429 let node = self.get_node_mut(cursor).or_fail()?;
430 node.expanded = !node.expanded;
431 Ok(())
432 }
433
434 fn get_node(&self, cursor: &Cursor) -> orfail::Result<&Self> {
435 if let Some((_, child)) = self.get_maybe_child(cursor).or_fail()? {
436 child.get_node(cursor).or_fail()
437 } else {
438 Ok(self)
439 }
440 }
441
442 fn get_node_mut(&mut self, cursor: &Cursor) -> orfail::Result<&mut Self> {
443 cursor.path.starts_with(&self.path).or_fail()?;
444
445 if let Some(i) = cursor.path.get(self.path.len()) {
446 let child = self.children.get_mut(i).or_fail()?;
447 child.get_node_mut(cursor).or_fail()
448 } else {
449 Ok(self)
450 }
451 }
452
453 fn get_maybe_child(&self, cursor: &Cursor) -> orfail::Result<Option<(usize, &Self)>> {
454 cursor.path.starts_with(&self.path).or_fail()?;
455
456 if let Some(i) = cursor.path.get(self.path.len()) {
457 let child = self.children.get(i).or_fail()?;
458 Ok(Some((i, child)))
459 } else {
460 Ok(None)
461 }
462 }
463
464 fn stage(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
465 let diff = self.get_diff(cursor, diff, false).or_fail()?;
466 git::stage(&diff).or_fail()?;
467 Ok(())
468 }
469
470 fn discard(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
471 let diff = self.get_diff(cursor, diff, true).or_fail()?;
472 git::discard(&diff).or_fail()?;
473 Ok(())
474 }
475
476 fn unstage(&self, cursor: &Cursor, diff: &Diff) -> orfail::Result<()> {
477 let diff = self.get_diff(cursor, diff, true).or_fail()?;
478 git::unstage(&diff).or_fail()?;
479 Ok(())
480 }
481
482 fn get_diff(&self, cursor: &Cursor, diff: &Diff, reverse: bool) -> orfail::Result<Diff> {
483 let Some((i, node)) = self.get_maybe_child(cursor).or_fail()? else {
484 return Ok(diff.clone());
485 };
486 let file = diff.files.get(i).or_fail()?;
487 let path = file.path();
488
489 let Some((i, node)) = node.get_maybe_child(cursor).or_fail()? else {
490 return Ok(file.to_diff());
491 };
492 let chunk = file.chunks().get(i).or_fail()?;
493
494 let Some((i, _node)) = node.get_maybe_child(cursor).or_fail()? else {
495 return Ok(chunk.to_diff(path));
496 };
497
498 Ok(chunk.get_line_chunk(i, reverse).or_fail()?.to_diff(path))
499 }
500
501 fn cursor_right(&self, cursor: &Cursor) -> Option<Cursor> {
502 let mut cursor = cursor.clone();
503
504 while cursor.path.len() >= self.path.len() {
505 let child_cursor = cursor.first_child();
506 if self.is_valid_cursor(&child_cursor) {
507 return Some(child_cursor);
508 }
509
510 let sibling_cursor = cursor.next_sibling();
511 if self.is_valid_cursor(&sibling_cursor) {
512 cursor = sibling_cursor;
513 } else {
514 break;
515 }
516 }
517
518 None
519 }
520
521 fn cursor_down(&self, cursor: &Cursor) -> Option<Cursor> {
522 let sibling_cursor = cursor.next_sibling();
523 if self.is_valid_cursor(&sibling_cursor) {
524 return Some(sibling_cursor);
525 }
526
527 let mut base_cursor = cursor.clone();
528 loop {
529 base_cursor = base_cursor.parent()?;
530
531 let mut next_cursor = base_cursor.next_sibling();
532 while next_cursor.path.len() < cursor.path.len() {
533 next_cursor = next_cursor.first_child();
534 }
535
536 if self.is_valid_cursor(&next_cursor) {
537 return Some(next_cursor);
538 }
539 }
540 }
541
542 fn cursor_up(&self, cursor: &Cursor) -> Option<Cursor> {
543 if let Some(sibling_cursor) = cursor.prev_sibling() {
544 return Some(sibling_cursor);
545 }
546
547 let mut base_cursor = cursor.clone();
548 loop {
549 base_cursor = base_cursor.parent()?;
550
551 let Some(mut next_cursor) = base_cursor.prev_sibling() else {
552 continue;
553 };
554 while next_cursor.path.len() < cursor.path.len() {
555 let index = self
556 .get_node(&next_cursor)
557 .ok()
558 .map(|n| n.children.len().saturating_sub(1))
559 .unwrap_or_default();
560 next_cursor = next_cursor.join(index);
561 }
562 if self.is_valid_cursor(&next_cursor) {
563 return Some(next_cursor);
564 }
565 }
566 }
567}
568
569pub trait DiffTreeNodeContent {
570 type Child: DiffTreeNodeContent;
571
572 fn head_line_tokens(&self) -> impl Iterator<Item = Token>;
573 fn can_alter(&self) -> bool;
574 fn children(&self) -> &[Self::Child];
575}
576
577impl DiffTreeNodeContent for PhasedDiff {
578 type Child = FileDiff;
579
580 fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
581 std::iter::once(Token::with_style(
582 format!("{:?} changes ({} files)", self.phase, self.diff.files.len()),
583 TerminalStyle::new().bold(),
584 ))
585 }
586
587 fn can_alter(&self) -> bool {
588 !self.diff.files.is_empty()
589 }
590
591 fn children(&self) -> &[Self::Child] {
592 &self.diff.files
593 }
594}
595
596impl DiffTreeNodeContent for FileDiff {
597 type Child = ChunkDiff;
598
599 fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
600 let path = Token::with_style(
601 self.path().display().to_string(),
602 TerminalStyle::new().underline(),
603 );
604 let tokens = match self {
605 FileDiff::Update {
606 old_mode, new_mode, ..
607 } => {
608 let mode = if let Some(old_mode) = old_mode {
609 format!(", {old_mode} -> {new_mode} mode")
610 } else {
611 "".to_string()
612 };
613 vec![
614 Token::new("modified "),
615 path,
616 Token::new(format!(
617 " ({} chunks, -{} +{} lines{})",
618 self.children().len(),
619 self.removed_lines(),
620 self.added_lines(),
621 mode
622 )),
623 ]
624 }
625 FileDiff::New { content, .. } => {
626 vec![
627 Token::new("added "),
628 path,
629 if matches!(content, ContentDiff::Binary) {
630 Token::new(" (binary)")
631 } else {
632 Token::new(format!(" (+{} lines)", self.added_lines()))
633 },
634 ]
635 }
636 FileDiff::Rename {
637 old_path, content, ..
638 } => {
639 let old_path = Token::with_style(
640 old_path.display().to_string(),
641 TerminalStyle::new().underline(),
642 );
643
644 let summary = if content.is_some() {
645 Token::new(format!(
646 " ({} chunks, -{} +{} lines)",
647 self.children().len(),
648 self.removed_lines(),
649 self.added_lines(),
650 ))
651 } else {
652 Token::new("")
653 };
654
655 vec![
656 Token::new("renamed "),
657 old_path,
658 Token::new(" -> "),
659 path,
660 summary,
661 ]
662 }
663 FileDiff::Delete { content, .. } => {
664 vec![
665 Token::new("deleted "),
666 path,
667 if matches!(content, ContentDiff::Binary) {
668 Token::new(" (binary)")
669 } else {
670 Token::new(format!(" (-{} lines)", self.removed_lines()))
671 },
672 ]
673 }
674 FileDiff::Chmod {
675 old_mode, new_mode, ..
676 } => {
677 vec![
678 Token::new("mode changed "),
679 path,
680 Token::new(format!(" {} -> {}", old_mode, new_mode)),
681 ]
682 }
683 };
684 tokens.into_iter()
685 }
686
687 fn can_alter(&self) -> bool {
688 true
689 }
690
691 fn children(&self) -> &[Self::Child] {
692 self.chunks()
693 }
694}
695
696impl DiffTreeNodeContent for ChunkDiff {
697 type Child = LineDiff;
698
699 fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
700 std::iter::once(Token::new(self.head_line()))
701 }
702
703 fn can_alter(&self) -> bool {
704 true
705 }
706
707 fn children(&self) -> &[Self::Child] {
708 &self.lines
709 }
710}
711
712impl DiffTreeNodeContent for LineDiff {
713 type Child = Self;
714
715 fn head_line_tokens(&self) -> impl Iterator<Item = Token> {
716 let style = TerminalStyle::new();
717 let style = match self {
718 LineDiff::Old(_) => style.dim(),
719 LineDiff::New(_) => style.bold(),
720 LineDiff::Both(_) => style,
721 LineDiff::NoNewlineAtEndOfFile => style,
722 };
723 std::iter::once(Token::with_style(self.to_string(), style))
724 }
725
726 fn can_alter(&self) -> bool {
727 !matches!(self, Self::Both(_))
728 }
729
730 fn children(&self) -> &[Self::Child] {
731 &[]
732 }
733}
734
735#[derive(Debug, Clone, PartialEq, Eq)]
736struct NodePath(Vec<usize>);
737
738impl NodePath {
739 fn root() -> Self {
740 Self(vec![0])
741 }
742
743 fn join(&self, index: usize) -> Self {
744 let mut child = self.clone();
745 child.0.push(index);
746 child
747 }
748
749 fn starts_with(&self, other: &Self) -> bool {
750 self.0.starts_with(&other.0)
751 }
752
753 fn len(&self) -> usize {
754 self.0.len()
755 }
756
757 fn get(&self, i: usize) -> Option<usize> {
758 self.0.get(i).copied()
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq)]
763struct Cursor {
764 path: NodePath,
765}
766
767impl Cursor {
768 fn root() -> Self {
769 Self {
770 path: NodePath::root().join(0),
771 }
772 }
773
774 fn join(&self, index: usize) -> Self {
775 Self {
776 path: self.path.join(index),
777 }
778 }
779
780 fn parent(&self) -> Option<Self> {
781 (self.path.len() > 2).then(|| {
782 let mut path = self.path.clone();
783 path.0.pop();
784 Self { path }
785 })
786 }
787
788 fn first_child(&self) -> Self {
789 let path = self.path.join(0);
790 Self { path }
791 }
792
793 fn next_sibling(&self) -> Self {
794 let mut path = self.path.clone();
795 *path.0.last_mut().expect("infallible") += 1;
796 Self { path }
797 }
798
799 fn prev_sibling(&self) -> Option<Self> {
800 let mut path = self.path.clone();
801 if path.0.last().copied() == Some(0) {
802 return None;
803 }
804 *path.0.last_mut().expect("infallible") -= 1;
805 Some(Self { path })
806 }
807
808 fn render(&self, canvas: &mut Canvas, path: &NodePath) {
809 let mut text = String::with_capacity(path.len() * 2);
810 let selected = *path == self.path;
811
812 if selected {
813 text.push('-');
814 } else {
815 text.push(' ');
816 }
817
818 for i in 2..path.len() {
819 if i == self.path.len() && path.starts_with(&self.path) {
820 text.push_str(" :")
821 } else if selected {
822 text.push_str("--")
823 } else {
824 text.push_str(" ")
825 }
826 }
827
828 if selected {
829 text.push_str(">| ");
830 } else if path.len() == self.path.len() {
831 text.push_str(" | ");
832 } else {
833 text.push_str(" ");
834 }
835
836 canvas.draw(Token::new(text));
837 }
838}
839
840#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841enum DiffPhase {
842 Unstaged,
843 Staged,
844}
845
846#[derive(Debug, Clone)]
847struct PhasedDiff {
848 phase: DiffPhase,
849 diff: Diff,
850}