Skip to main content

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