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 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 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 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}