git_igitt/
app.rs

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        // TODO: remove once searching in diffs works
572        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        // TODO: remove once searching in diffs works
585        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        // TODO implement search in diff panel
640        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}