1use crate::settings::AppSettings;
2use crate::util::syntax_highlight::highlight;
3use crate::widgets::branches_view::{BranchItem, BranchItemType};
4use crate::widgets::commit_view::{CommitViewInfo, CommitViewState, DiffItem};
5use crate::widgets::diff_view::{DiffViewInfo, DiffViewState};
6use crate::widgets::graph_view::GraphViewState;
7use crate::widgets::list::StatefulList;
8use crate::widgets::models_view::ModelListState;
9use git2::{Commit, DiffDelta, DiffFormat, DiffHunk, DiffLine, DiffOptions as GDiffOptions, Oid};
10use git_graph::config::get_available_models;
11use git_graph::graph::GitGraph;
12use git_graph::print::unicode::{format_branches, print_unicode};
13use git_graph::settings::Settings;
14use std::fmt::Write;
15use std::path::PathBuf;
16use std::str::FromStr;
17use tui::style::Color;
18
19const HASH_COLOR: u8 = 11;
20
21#[derive(PartialEq, Eq)]
22pub enum ActiveView {
23 Branches,
24 Graph,
25 Commit,
26 Files,
27 Diff,
28 Models,
29 Search,
30 Help(u16),
31}
32
33pub enum DiffType {
34 Added,
35 Deleted,
36 Modified,
37 Renamed,
38}
39
40#[derive(PartialEq, Eq)]
41pub enum DiffMode {
42 Diff,
43 Old,
44 New,
45}
46
47pub struct DiffOptions {
48 pub context_lines: u32,
49 pub diff_mode: DiffMode,
50 pub line_numbers: bool,
51 pub syntax_highlight: bool,
52 pub wrap_lines: bool,
53}
54
55impl Default for DiffOptions {
56 fn default() -> Self {
57 Self {
58 context_lines: 3,
59 diff_mode: DiffMode::Diff,
60 line_numbers: true,
61 syntax_highlight: true,
62 wrap_lines: false,
63 }
64 }
65}
66
67impl FromStr for DiffType {
68 type Err = String;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let tp = match s {
72 "A" => DiffType::Added,
73 "D" => DiffType::Deleted,
74 "M" => DiffType::Modified,
75 "R" => DiffType::Renamed,
76 other => return Err(format!("Unknown diff type {}", other)),
77 };
78 Ok(tp)
79 }
80}
81
82impl ToString for DiffType {
83 fn to_string(&self) -> String {
84 match self {
85 DiffType::Added => "+",
86 DiffType::Deleted => "-",
87 DiffType::Modified => "m",
88 DiffType::Renamed => "r",
89 }
90 .to_string()
91 }
92}
93
94impl DiffType {
95 pub fn to_color(&self) -> Color {
96 match self {
97 DiffType::Added => Color::LightGreen,
98 DiffType::Deleted => Color::LightRed,
99 DiffType::Modified => Color::LightYellow,
100 DiffType::Renamed => Color::LightBlue,
101 }
102 }
103}
104
105pub type CurrentBranches = Vec<(Option<String>, Option<Oid>)>;
106pub type DiffLines = Vec<(String, Option<u32>, Option<u32>)>;
107
108pub struct App {
109 pub settings: AppSettings,
110 pub graph_state: GraphViewState,
111 pub commit_state: CommitViewState,
112 pub diff_state: DiffViewState,
113 pub models_state: Option<ModelListState>,
114 pub title: String,
115 pub repo_name: String,
116 pub active_view: ActiveView,
117 pub prev_active_view: Option<ActiveView>,
118 pub curr_branches: Vec<(Option<String>, Option<Oid>)>,
119 pub is_fullscreen: bool,
120 pub horizontal_split: bool,
121 pub show_branches: bool,
122 pub color: bool,
123 pub models_path: PathBuf,
124 pub error_message: Option<String>,
125 pub diff_options: DiffOptions,
126 pub search_term: Option<String>,
127}
128
129impl App {
130 pub fn new(
131 settings: AppSettings,
132 title: String,
133 repo_name: String,
134 models_path: PathBuf,
135 ) -> App {
136 App {
137 settings,
138 graph_state: GraphViewState::default(),
139 commit_state: CommitViewState::default(),
140 diff_state: DiffViewState::default(),
141 models_state: None,
142 title,
143 repo_name,
144 active_view: ActiveView::Graph,
145 prev_active_view: None,
146 curr_branches: vec![],
147 is_fullscreen: false,
148 horizontal_split: true,
149 show_branches: false,
150 color: true,
151 models_path,
152 error_message: None,
153 diff_options: DiffOptions::default(),
154 search_term: None,
155 }
156 }
157
158 pub fn with_graph(
159 mut self,
160 graph: GitGraph,
161 graph_lines: Vec<String>,
162 text_lines: Vec<String>,
163 indices: Vec<usize>,
164 select_head: bool,
165 ) -> Result<App, String> {
166 let branches = get_branches(&graph);
167
168 self.graph_state.graph = Some(graph);
169 self.graph_state.graph_lines = graph_lines;
170 self.graph_state.text_lines = text_lines;
171 self.graph_state.indices = indices;
172 self.graph_state.branches = Some(StatefulList::with_items(branches));
173
174 if select_head {
175 if let Some(graph) = &self.graph_state.graph {
176 if let Some(index) = graph.indices.get(&graph.head.oid) {
177 self.graph_state.selected = Some(*index);
178 self.selection_changed()?;
179 }
180 }
181 }
182
183 Ok(self)
184 }
185
186 pub fn with_branches(mut self, branches: Vec<(Option<String>, Option<Oid>)>) -> App {
187 self.curr_branches = branches;
188 self
189 }
190
191 pub fn with_color(mut self, color: bool) -> App {
192 self.color = color;
193 self
194 }
195
196 pub fn clear_graph(mut self) -> App {
197 self.graph_state.graph = None;
198 self.graph_state.graph_lines = vec![];
199 self.graph_state.text_lines = vec![];
200 self.graph_state.indices = vec![];
201 self
202 }
203
204 pub fn reload(
205 mut self,
206 settings: &Settings,
207 max_commits: Option<usize>,
208 ) -> Result<App, String> {
209 let selected = self.graph_state.selected;
210 let mut temp = None;
211 std::mem::swap(&mut temp, &mut self.graph_state.graph);
212 if let Some(graph) = temp {
213 let sel_oid = selected
214 .and_then(|idx| graph.commits.get(idx))
215 .map(|info| info.oid);
216 let repo = graph.take_repository();
217 let graph = GitGraph::new(repo, settings, max_commits)?;
218 let (graph_lines, text_lines, indices) = print_unicode(&graph, settings)?;
219
220 let sel_idx = sel_oid.and_then(|oid| graph.indices.get(&oid)).cloned();
221 let old_idx = self.graph_state.selected;
222 self.graph_state.selected = sel_idx;
223 if sel_idx.is_some() != old_idx.is_some() {
224 self.selection_changed()?;
225 }
226
227 self.with_graph(graph, graph_lines, text_lines, indices, false)
228 } else {
229 Ok(self)
230 }
231 }
232
233 pub fn on_up(&mut self, is_shift: bool, is_ctrl: bool) -> Result<(bool, bool), String> {
234 let step = if is_shift { 10 } else { 1 };
235 match self.active_view {
236 ActiveView::Graph => {
237 if is_ctrl {
238 if self.graph_state.move_secondary_selection(step, false) {
239 if self.graph_state.secondary_selected == self.graph_state.selected {
240 self.graph_state.secondary_selected = None;
241 }
242 return Ok((true, false));
243 }
244 } else if self.graph_state.move_selection(step, false) {
245 return Ok((true, false));
246 }
247 }
248 ActiveView::Branches => {
249 if let Some(list) = &mut self.graph_state.branches {
250 list.bwd(step);
251 }
252 }
253 ActiveView::Help(scroll) => {
254 self.active_view = ActiveView::Help(scroll.saturating_sub(step as u16))
255 }
256 ActiveView::Commit => {
257 if let Some(content) = &mut self.commit_state.content {
258 content.scroll = content.scroll.saturating_sub(step as u16);
259 }
260 }
261 ActiveView::Files => {
262 if let Some(content) = &mut self.commit_state.content {
263 return Ok((false, content.diffs.bwd(step)));
264 }
265 }
266 ActiveView::Diff => {
267 if let Some(content) = &mut self.diff_state.content {
268 content.scroll = (
269 content.scroll.0.saturating_sub(step as u16),
270 content.scroll.1,
271 );
272 }
273 }
274 ActiveView::Models => {
275 if let Some(state) = &mut self.models_state {
276 state.bwd(step)
277 }
278 }
279 _ => {}
280 }
281 Ok((false, false))
282 }
283
284 pub fn on_down(&mut self, is_shift: bool, is_ctrl: bool) -> Result<(bool, bool), String> {
285 let step = if is_shift { 10 } else { 1 };
286 match self.active_view {
287 ActiveView::Graph => {
288 if is_ctrl {
289 if self.graph_state.move_secondary_selection(step, true) {
290 if self.graph_state.secondary_selected == self.graph_state.selected {
291 self.graph_state.secondary_selected = None;
292 }
293 return Ok((true, false));
294 }
295 } else if self.graph_state.move_selection(step, true) {
296 return Ok((true, false));
297 }
298 }
299 ActiveView::Branches => {
300 if let Some(list) = &mut self.graph_state.branches {
301 list.fwd(step);
302 }
303 }
304 ActiveView::Help(scroll) => {
305 self.active_view = ActiveView::Help(scroll.saturating_add(step as u16))
306 }
307 ActiveView::Commit => {
308 if let Some(content) = &mut self.commit_state.content {
309 content.scroll = content.scroll.saturating_add(step as u16);
310 }
311 }
312 ActiveView::Files => {
313 if let Some(content) = &mut self.commit_state.content {
314 return Ok((false, content.diffs.fwd(step)));
315 }
316 }
317 ActiveView::Diff => {
318 if let Some(content) = &mut self.diff_state.content {
319 content.scroll = (
320 content.scroll.0.saturating_add(step as u16),
321 content.scroll.1,
322 );
323 }
324 }
325 ActiveView::Models => {
326 if let Some(state) = &mut self.models_state {
327 state.fwd(step)
328 }
329 }
330 _ => {}
331 }
332 Ok((false, false))
333 }
334
335 pub fn on_home(&mut self) -> Result<bool, String> {
336 if let ActiveView::Graph = self.active_view {
337 if let Some(graph) = &self.graph_state.graph {
338 if let Some(index) = graph.indices.get(&graph.head.oid) {
339 self.graph_state.selected = Some(*index);
340 return Ok(true);
341 } else if !self.graph_state.graph_lines.is_empty() {
342 self.graph_state.selected = Some(0);
343 return Ok(true);
344 }
345 }
346 }
347 Ok(false)
348 }
349
350 pub fn on_end(&mut self) -> Result<bool, String> {
351 if let ActiveView::Graph = self.active_view {
352 if !self.graph_state.indices.is_empty() {
353 self.graph_state.selected = Some(self.graph_state.indices.len() - 1);
354 return Ok(true);
355 }
356 }
357 Ok(false)
358 }
359
360 pub fn on_right(&mut self, is_shift: bool, is_ctrl: bool) -> Result<bool, String> {
361 let mut reload_file_diff = false;
362 if is_ctrl {
363 let step = if is_shift { 15 } else { 3 };
364 match self.active_view {
365 ActiveView::Diff => {
366 if let Some(content) = &mut self.diff_state.content {
367 content.scroll = (
368 content.scroll.0,
369 content.scroll.1.saturating_add(step as u16),
370 );
371 }
372 }
373 ActiveView::Files => {
374 if let Some(content) = &mut self.commit_state.content {
375 content.diffs.state.scroll_x =
376 content.diffs.state.scroll_x.saturating_add(step as u16);
377 }
378 }
379 ActiveView::Branches => {
380 if let Some(branches) = &mut self.graph_state.branches {
381 branches.state.scroll_x =
382 branches.state.scroll_x.saturating_add(step as u16);
383 }
384 }
385 _ => {}
386 }
387 } else {
388 self.active_view = match &self.active_view {
389 ActiveView::Branches => ActiveView::Graph,
390 ActiveView::Graph => ActiveView::Commit,
391 ActiveView::Commit => {
392 if let Some(commit) = &mut self.commit_state.content {
393 if commit.diffs.state.selected.is_none() && !commit.diffs.items.is_empty() {
394 commit.diffs.state.selected = Some(0);
395 reload_file_diff = true;
396 }
397 }
398 ActiveView::Files
399 }
400 ActiveView::Files => ActiveView::Diff,
401 ActiveView::Diff => ActiveView::Diff,
402 ActiveView::Help(_) => self.prev_active_view.take().unwrap_or(ActiveView::Graph),
403 ActiveView::Models => ActiveView::Models,
404 ActiveView::Search => ActiveView::Search,
405 }
406 }
407 Ok(reload_file_diff)
408 }
409 pub fn on_left(&mut self, is_shift: bool, is_ctrl: bool) {
410 if is_ctrl {
411 let step = if is_shift { 15 } else { 3 };
412 match self.active_view {
413 ActiveView::Diff => {
414 if let Some(content) = &mut self.diff_state.content {
415 content.scroll = (
416 content.scroll.0,
417 content.scroll.1.saturating_sub(step as u16),
418 );
419 }
420 }
421 ActiveView::Files => {
422 if let Some(content) = &mut self.commit_state.content {
423 content.diffs.state.scroll_x =
424 content.diffs.state.scroll_x.saturating_sub(step as u16);
425 }
426 }
427 ActiveView::Branches => {
428 if let Some(branches) = &mut self.graph_state.branches {
429 branches.state.scroll_x =
430 branches.state.scroll_x.saturating_sub(step as u16);
431 }
432 }
433 _ => {}
434 }
435 } else {
436 self.active_view = match &self.active_view {
437 ActiveView::Branches => ActiveView::Branches,
438 ActiveView::Graph => ActiveView::Branches,
439 ActiveView::Commit => ActiveView::Graph,
440 ActiveView::Files => ActiveView::Commit,
441 ActiveView::Diff => ActiveView::Files,
442 ActiveView::Help(_) => self.prev_active_view.take().unwrap_or(ActiveView::Graph),
443 ActiveView::Models => ActiveView::Models,
444 ActiveView::Search => ActiveView::Search,
445 }
446 }
447 }
448
449 pub fn on_enter(&mut self, is_control: bool) -> Result<bool, String> {
450 match &self.active_view {
451 ActiveView::Help(_) => {
452 self.active_view = self.prev_active_view.take().unwrap_or(ActiveView::Graph)
453 }
454 ActiveView::Search => {
455 self.active_view = self.prev_active_view.take().unwrap_or(ActiveView::Graph);
456 self.search()?;
457 }
458 ActiveView::Branches => {
459 if let Some(graph) = &self.graph_state.graph {
460 if let Some(state) = &self.graph_state.branches {
461 if let Some(sel) = state.state.selected() {
462 let br = &state.items[sel];
463 if let Some(index) = br.index {
464 let branch_info = &graph.all_branches[index];
465 let commit_idx = graph.indices[&branch_info.target];
466 if is_control {
467 if self.graph_state.selected.is_some() {
468 self.graph_state.secondary_selected = Some(commit_idx);
469 self.graph_state.secondary_changed = true;
470 if self.is_fullscreen {
471 self.active_view = ActiveView::Graph;
472 }
473 return Ok(true);
474 }
475 } else {
476 self.graph_state.selected = Some(commit_idx);
477 self.graph_state.secondary_changed = false;
478 if self.is_fullscreen {
479 self.active_view = ActiveView::Graph;
480 }
481 return Ok(true);
482 }
483 }
484 }
485 }
486 }
487 }
488 _ => {}
489 }
490 Ok(false)
491 }
492
493 pub fn on_backspace(&mut self) -> Result<bool, String> {
494 match &self.active_view {
495 ActiveView::Help(_) | ActiveView::Models => {}
496 ActiveView::Search => {
497 if let Some(term) = &self.search_term {
498 let term = &term[0..(term.len() - 1)];
499 self.search_term = if term.is_empty() {
500 None
501 } else {
502 Some(term.to_string())
503 };
504 }
505 }
506 _ => {
507 if self.graph_state.secondary_selected.is_some() {
508 self.graph_state.secondary_selected = None;
509 self.graph_state.secondary_changed = false;
510 return Ok(true);
511 }
512 }
513 }
514 Ok(false)
515 }
516
517 pub fn on_plus(&mut self) -> Result<bool, String> {
518 if self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files {
519 self.diff_options.context_lines = self.diff_options.context_lines.saturating_add(1);
520 return Ok(true);
521 }
522 Ok(false)
523 }
524
525 pub fn on_minus(&mut self) -> Result<bool, String> {
526 if self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files {
527 self.diff_options.context_lines = self.diff_options.context_lines.saturating_sub(1);
528 return Ok(true);
529 }
530 Ok(false)
531 }
532
533 pub fn on_tab(&mut self) {
534 self.is_fullscreen = !self.is_fullscreen;
535 }
536
537 pub fn on_esc(&mut self) -> Result<bool, String> {
538 match self.active_view {
539 ActiveView::Models | ActiveView::Help(_) => {
540 self.active_view = self.prev_active_view.take().unwrap_or(ActiveView::Graph);
541 }
542 ActiveView::Search => {
543 self.active_view = self.prev_active_view.take().unwrap_or(ActiveView::Graph);
544 self.exit_search(true);
545 }
546 _ => {
547 self.active_view = ActiveView::Graph;
548 self.is_fullscreen = false;
549 if let Some(content) = &mut self.commit_state.content {
550 content.diffs.state.scroll_x = 0;
551 }
552 self.diff_options.diff_mode = DiffMode::Diff;
553 return Ok(true);
554 }
555 }
556
557 Ok(false)
558 }
559
560 pub fn character_entered(&mut self, c: char) {
561 if let ActiveView::Search = self.active_view {
562 if let Some(term) = &self.search_term {
563 self.search_term = Some(format!("{}{}", term, c))
564 } else {
565 self.search_term = Some(format!("{}", c))
566 }
567 }
568 }
569
570 pub fn open_search(&mut self) {
571 self.active_view = ActiveView::Graph;
573
574 if let ActiveView::Search = self.active_view {
575 } else {
576 let mut temp = ActiveView::Search;
577 std::mem::swap(&mut temp, &mut self.active_view);
578 self.prev_active_view = Some(temp);
579 }
580 }
581 pub fn exit_search(&mut self, _abort: bool) {}
582
583 pub fn search(&mut self) -> Result<bool, String> {
584 self.active_view = ActiveView::Graph;
586
587 let update = match &self.active_view {
588 ActiveView::Branches | ActiveView::Graph | ActiveView::Commit => self.search_graph()?,
589 ActiveView::Files | ActiveView::Diff => self.search_diff(),
590 _ => false,
591 };
592 Ok(update)
593 }
594 fn search_graph(&mut self) -> Result<bool, String> {
595 if let Some(search) = &self.search_term {
596 let term = search.to_lowercase();
597
598 let search_start = if let Some(sel_idx) = &self.graph_state.selected {
599 sel_idx + 1
600 } else {
601 0
602 };
603 for idx in search_start..self.graph_state.indices.len() {
604 if self.commit_contains(idx, &term) {
605 self.graph_state.selected = Some(idx);
606 return Ok(true);
607 }
608 }
609 for idx in 0..search_start {
610 if self.commit_contains(idx, &term) {
611 self.graph_state.selected = Some(idx);
612 return Ok(true);
613 }
614 }
615 }
616 Ok(false)
617 }
618
619 fn commit_contains(&self, commit_idx: usize, term: &str) -> bool {
620 let num_lines = self.graph_state.text_lines.len();
621 let line_start = self.graph_state.indices[commit_idx];
622 let line_end = self
623 .graph_state
624 .indices
625 .get(commit_idx + 1)
626 .unwrap_or(&num_lines);
627 for line_idx in line_start..*line_end {
628 if self.graph_state.text_lines[line_idx]
629 .to_lowercase()
630 .contains(term)
631 {
632 return true;
633 }
634 }
635 false
636 }
637
638 fn search_diff(&mut self) -> bool {
639 false
641 }
642
643 pub fn set_diff_mode(&mut self, mode: DiffMode) -> Result<bool, String> {
644 if mode != self.diff_options.diff_mode
645 && (self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files)
646 {
647 self.diff_options.diff_mode = mode;
648 return Ok(true);
649 }
650 Ok(false)
651 }
652
653 pub fn toggle_line_numbers(&mut self) -> Result<bool, String> {
654 if self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files {
655 self.diff_options.line_numbers = !self.diff_options.line_numbers;
656 return Ok(true);
657 }
658 Ok(false)
659 }
660
661 pub fn toggle_line_wrap(&mut self) -> Result<bool, String> {
662 if self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files {
663 self.diff_options.wrap_lines = !self.diff_options.wrap_lines;
664 return Ok(true);
665 }
666 Ok(false)
667 }
668
669 pub fn toggle_syntax_highlight(&mut self) -> Result<bool, String> {
670 if self.active_view == ActiveView::Diff || self.active_view == ActiveView::Files {
671 self.diff_options.syntax_highlight = !self.diff_options.syntax_highlight;
672 return Ok(true);
673 }
674 Ok(false)
675 }
676
677 pub fn toggle_layout(&mut self) {
678 self.horizontal_split = !self.horizontal_split;
679 }
680
681 pub fn toggle_branches(&mut self) {
682 self.show_branches = !self.show_branches;
683 }
684
685 pub fn show_help(&mut self) {
686 if let ActiveView::Help(_) = self.active_view {
687 } else {
688 let mut temp = ActiveView::Help(0);
689 std::mem::swap(&mut temp, &mut self.active_view);
690 self.prev_active_view = Some(temp);
691 }
692 }
693
694 pub fn select_model(&mut self) -> Result<(), String> {
695 if let ActiveView::Models = self.active_view {
696 } else {
697 let mut temp = ActiveView::Models;
698 std::mem::swap(&mut temp, &mut self.active_view);
699 self.prev_active_view = Some(temp);
700
701 let models = get_available_models(&self.models_path).map_err(|err| {
702 format!(
703 "Unable to load model files from %APP_DATA%/git-graph/models\n{}",
704 err
705 )
706 })?;
707 self.models_state = Some(ModelListState::new(models, self.color));
708 }
709 Ok(())
710 }
711
712 pub fn selection_changed(&mut self) -> Result<(), String> {
713 self.reload_diff_message()?;
714 let _reload_file = self.reload_diff_files()?;
715 Ok(())
716 }
717
718 pub fn reload_diff_message(&mut self) -> Result<(), String> {
719 if let Some(graph) = &self.graph_state.graph {
720 self.commit_state.content =
721 if let Some((info, idx)) = self.graph_state.selected.and_then(move |sel_idx| {
722 graph.commits.get(sel_idx).map(|commit| (commit, sel_idx))
723 }) {
724 let commit = graph
725 .repository
726 .find_commit(info.oid)
727 .map_err(|err| err.message().to_string())?;
728
729 let head_idx = graph.indices.get(&graph.head.oid);
730 let head = if head_idx.map_or(false, |h| h == &idx) {
731 Some(&graph.head)
732 } else {
733 None
734 };
735
736 let hash_color = if self.color { Some(HASH_COLOR) } else { None };
737 let branches = format_branches(graph, info, head, self.color);
738 let message_fmt = crate::util::format::format(&commit, branches, hash_color);
739
740 let compare_to = if let Some(sel) = self.graph_state.secondary_selected {
741 let sec_selected_info = graph.commits.get(sel);
742 if let Some(info) = sec_selected_info {
743 Some(
744 graph
745 .repository
746 .find_commit(info.oid)
747 .map_err(|err| err.message().to_string())?,
748 )
749 } else {
750 commit.parent(0).ok()
751 }
752 } else {
753 commit.parent(0).ok()
754 };
755 let comp_oid = compare_to.as_ref().map(|c| c.id());
756
757 Some(CommitViewInfo::new(
758 message_fmt,
759 StatefulList::default(),
760 info.oid,
761 comp_oid.unwrap_or_else(Oid::zero),
762 ))
763 } else {
764 None
765 }
766 }
767 Ok(())
768 }
769
770 pub fn reload_diff_files(&mut self) -> Result<bool, String> {
771 if let Some(graph) = &self.graph_state.graph {
772 if let Some(content) = &mut self.commit_state.content {
773 let commit = graph
774 .repository
775 .find_commit(content.oid)
776 .map_err(|err| err.message().to_string())?;
777
778 let compare_to = if content.compare_oid.is_zero() {
779 None
780 } else {
781 Some(
782 graph
783 .repository
784 .find_commit(content.compare_oid)
785 .map_err(|err| err.message().to_string())?,
786 )
787 };
788
789 let diffs = get_diff_files(graph, compare_to.as_ref(), &commit)?;
790
791 content.diffs = StatefulList::with_items(diffs)
792 }
793 }
794 Ok(true)
795 }
796
797 pub fn clear_file_diff(&mut self) {
798 if let Some(content) = &mut self.diff_state.content {
799 content.diffs.clear();
800 content.highlighted = None;
801 }
802 }
803
804 pub fn file_changed(&mut self, reset_scroll: bool) -> Result<(), String> {
805 if let (Some(graph), Some(state)) = (&self.graph_state.graph, &self.commit_state.content) {
806 self.diff_state.content = if let Some((info, sel_index)) = self
807 .graph_state
808 .selected
809 .and_then(move |sel_idx| graph.commits.get(sel_idx))
810 .and_then(|info| {
811 state
812 .diffs
813 .state
814 .selected()
815 .map(|sel_index| (info, sel_index))
816 }) {
817 let commit = graph
818 .repository
819 .find_commit(info.oid)
820 .map_err(|err| err.message().to_string())?;
821
822 let compare_to = if let Some(sel) = self.graph_state.secondary_selected {
823 let sec_selected_info = graph.commits.get(sel);
824 if let Some(info) = sec_selected_info {
825 Some(
826 graph
827 .repository
828 .find_commit(info.oid)
829 .map_err(|err| err.message().to_string())?,
830 )
831 } else {
832 commit.parent(0).ok()
833 }
834 } else {
835 commit.parent(0).ok()
836 };
837 let comp_oid = compare_to.as_ref().map(|c| c.id());
838
839 let selection = &state.diffs.items[sel_index];
840
841 let diffs = get_file_diffs(
842 graph,
843 compare_to.as_ref(),
844 &commit,
845 &selection.file,
846 &self.diff_options,
847 &self.settings.tab_spaces,
848 )?;
849
850 let highlighted = if self.color
851 && self.diff_options.syntax_highlight
852 && self.diff_options.diff_mode != DiffMode::Diff
853 && diffs.len() == 2
854 {
855 PathBuf::from(&selection.file)
856 .extension()
857 .and_then(|ext| ext.to_str().and_then(|ext| highlight(&diffs[1].0, ext)))
858 } else {
859 None
860 };
861
862 let mut info = DiffViewInfo::new(
863 diffs,
864 highlighted,
865 info.oid,
866 comp_oid.unwrap_or_else(Oid::zero),
867 );
868
869 if !reset_scroll {
870 if let Some(diff_state) = &self.diff_state.content {
871 info.scroll = diff_state.scroll;
872 }
873 }
874
875 Some(info)
876 } else {
877 None
878 }
879 }
880 Ok(())
881 }
882
883 pub fn set_error(&mut self, msg: String) {
884 self.error_message = Some(msg);
885 }
886 pub fn clear_error(&mut self) {
887 self.error_message = None;
888 }
889}
890
891fn get_diff_files(
892 graph: &GitGraph,
893 old: Option<&Commit>,
894 new: &Commit,
895) -> Result<Vec<DiffItem>, String> {
896 let mut diffs = vec![];
897 let diff = graph
898 .repository
899 .diff_tree_to_tree(
900 old.map(|c| c.tree())
901 .map_or(Ok(None), |v| v.map(Some))
902 .map_err(|err| err.message().to_string())?
903 .as_ref(),
904 Some(&new.tree().map_err(|err| err.message().to_string())?),
905 None,
906 )
907 .map_err(|err| err.message().to_string())?;
908
909 let mut diff_err = Ok(());
910 diff.print(DiffFormat::NameStatus, |d, _h, l| {
911 let content =
912 std::str::from_utf8(l.content()).unwrap_or("Invalid UTF8 character in file name.");
913 let tp = match DiffType::from_str(&content[..1]) {
914 Ok(tp) => tp,
915 Err(err) => {
916 diff_err = Err(err);
917 return false;
918 }
919 };
920 let f = match tp {
921 DiffType::Deleted | DiffType::Modified => d.old_file(),
922 DiffType::Added | DiffType::Renamed => d.new_file(),
923 };
924 diffs.push(DiffItem {
925 file: f.path().and_then(|p| p.to_str()).unwrap_or("").to_string(),
926 diff_type: tp,
927 });
928 true
929 })
930 .map_err(|err| err.message().to_string())?;
931
932 diff_err?;
933
934 Ok(diffs)
935}
936
937fn get_file_diffs(
938 graph: &GitGraph,
939 old: Option<&Commit>,
940 new: &Commit,
941 path: &str,
942 options: &DiffOptions,
943 tab_spaces: &str,
944) -> Result<DiffLines, String> {
945 let mut diffs = vec![];
946 let mut opts = GDiffOptions::new();
947 opts.context_lines(options.context_lines);
948 opts.indent_heuristic(true);
949 opts.pathspec(path);
950 opts.disable_pathspec_match(true);
951 let diff = graph
952 .repository
953 .diff_tree_to_tree(
954 old.map(|c| c.tree())
955 .map_or(Ok(None), |v| v.map(Some))
956 .map_err(|err| err.message().to_string())?
957 .as_ref(),
958 Some(&new.tree().map_err(|err| err.message().to_string())?),
959 Some(&mut opts),
960 )
961 .map_err(|err| err.message().to_string())?;
962
963 let mut diff_error = Ok(());
964
965 if options.diff_mode == DiffMode::Diff {
966 diff.print(DiffFormat::Patch, |d, h, l| {
967 diffs.push((
968 print_diff_line(&d, &h, &l).replace('\t', tab_spaces),
969 l.old_lineno(),
970 l.new_lineno(),
971 ));
972 true
973 })
974 .map_err(|err| err.message().to_string())?;
975 } else {
976 match diff.print(DiffFormat::PatchHeader, |d, _h, l| {
977 let (blob_oid, oid) = if options.diff_mode == DiffMode::New {
978 (d.new_file().id(), new.id())
979 } else {
980 (
981 d.old_file().id(),
982 old.map(|c| c.id()).unwrap_or_else(Oid::zero),
983 )
984 };
985
986 let line = std::str::from_utf8(l.content())
987 .unwrap_or("Invalid UTF8 character.")
988 .replace('\t', tab_spaces);
989 diffs.push((line, None, None));
990
991 if blob_oid.is_zero() {
992 diffs.push((
993 format!("File does not exist in {}", &oid.to_string()[..7]),
994 None,
995 None,
996 ))
997 } else {
998 let blob = match graph.repository.find_blob(blob_oid) {
999 Ok(blob) => blob,
1000 Err(err) => {
1001 diff_error = Err(err.to_string());
1002 return false;
1003 }
1004 };
1005
1006 let text = std::str::from_utf8(blob.content())
1007 .map_err(|err| err.to_string())
1008 .unwrap_or("Invalid UTF8 character.");
1009 diffs.push((text.replace('\t', tab_spaces), None, None));
1010 }
1011 true
1012 }) {
1013 Ok(_) => {}
1014 Err(_) => {
1015 let oid = if options.diff_mode == DiffMode::New {
1016 new.id()
1017 } else {
1018 old.map(|c| c.id()).unwrap_or_else(Oid::zero)
1019 };
1020 diffs.push((
1021 format!("File does not exist in {}", &oid.to_string()[..7]),
1022 None,
1023 None,
1024 ))
1025 }
1026 };
1027 }
1028 diff_error?;
1029 Ok(diffs)
1030}
1031
1032fn print_diff_line(_delta: &DiffDelta, _hunk: &Option<DiffHunk>, line: &DiffLine) -> String {
1033 let mut out = String::new();
1034 match line.origin() {
1035 '+' | '-' | ' ' => write!(out, "{}", line.origin()).unwrap(),
1036 _ => {}
1037 }
1038 write!(
1039 out,
1040 "{}",
1041 std::str::from_utf8(line.content()).unwrap_or("Invalid UTF8 character.")
1042 )
1043 .unwrap();
1044
1045 out
1046}
1047
1048fn get_branches(graph: &GitGraph) -> Vec<BranchItem> {
1049 let mut branches = Vec::new();
1050
1051 branches.push(BranchItem::new(
1052 "Branches".to_string(),
1053 None,
1054 7,
1055 BranchItemType::Heading,
1056 ));
1057 for idx in &graph.branches {
1058 let branch = &graph.all_branches[*idx];
1059 if !branch.is_remote {
1060 branches.push(BranchItem::new(
1061 branch.name.clone(),
1062 Some(*idx),
1063 branch.visual.term_color,
1064 BranchItemType::LocalBranch,
1065 ));
1066 }
1067 }
1068
1069 branches.push(BranchItem::new(
1070 "Remotes".to_string(),
1071 None,
1072 7,
1073 BranchItemType::Heading,
1074 ));
1075 for idx in &graph.branches {
1076 let branch = &graph.all_branches[*idx];
1077 if branch.is_remote {
1078 branches.push(BranchItem::new(
1079 branch.name.clone(),
1080 Some(*idx),
1081 branch.visual.term_color,
1082 BranchItemType::RemoteBranch,
1083 ));
1084 }
1085 }
1086
1087 branches.push(BranchItem::new(
1088 "Tags".to_string(),
1089 None,
1090 7,
1091 BranchItemType::Heading,
1092 ));
1093
1094 let mut tags: Vec<_> = graph
1095 .tags
1096 .iter()
1097 .filter_map(|idx| {
1098 let branch = &graph.all_branches[*idx];
1099 if let Ok(commit) = graph.repository.find_commit(branch.target) {
1100 let time = commit.time();
1101 Some((
1102 BranchItem::new(
1103 branch.name.clone(),
1104 Some(*idx),
1105 branch.visual.term_color,
1106 BranchItemType::Tag,
1107 ),
1108 time.seconds() + time.offset_minutes() as i64 * 60,
1109 ))
1110 } else {
1111 None
1112 }
1113 })
1114 .collect();
1115
1116 tags.sort_by_key(|bt| -bt.1);
1117
1118 branches.extend(tags.into_iter().map(|bt| bt.0));
1119
1120 branches
1121}