1use crate::print::colors::to_terminal_color;
21use crate::settings::{BranchOrder, BranchSettings, MergePatterns, Settings};
22use git2::{BranchType, Commit, Error, Oid, Reference, Repository};
23use regex::Regex;
24use std::collections::{HashMap, HashSet};
25
26const ORIGIN: &str = "origin/";
27const FORK: &str = "fork/";
28
29pub struct GitGraph {
31 pub repository: Repository,
32 pub commits: Vec<CommitInfo>,
33 pub indices: HashMap<Oid, usize>,
35 pub all_branches: Vec<BranchInfo>,
37 pub head: HeadInfo,
39}
40
41impl GitGraph {
42 pub fn new(
44 mut repository: Repository,
45 settings: &Settings,
46 start_point: Option<String>,
47 max_count: Option<usize>,
48 ) -> Result<Self, String> {
49 #![doc = include_str!("../docs/branch_assignment.md")]
50 let mut stashes = HashSet::new();
51 repository
52 .stash_foreach(|_, _, oid| {
53 stashes.insert(*oid);
54 true
55 })
56 .map_err(|err| err.message().to_string())?;
57
58 let mut walk = repository
59 .revwalk()
60 .map_err(|err| err.message().to_string())?;
61
62 walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
63 .map_err(|err| err.message().to_string())?;
64
65 if let Some(start) = start_point {
67 let object = repository
68 .revparse_single(&start)
69 .map_err(|err| format!("Failed to resolve start point '{}': {}", start, err))?;
70 walk.push(object.id())
71 .map_err(|err| err.message().to_string())?;
72 } else {
73 walk.push_glob("*")
74 .map_err(|err| err.message().to_string())?;
75 }
76
77 if repository.is_shallow() {
78 return Err("ERROR: git-graph does not support shallow clones due to a missing feature in the underlying libgit2 library.".to_string());
79 }
80
81 let head = HeadInfo::new(&repository.head().map_err(|err| err.message().to_string())?)?;
82
83 let mut commits = Vec::new();
86 let mut indices = HashMap::new();
87 let mut idx = 0;
88 for oid in walk {
89 if let Some(max) = max_count {
90 if idx >= max {
91 break;
92 }
93 }
94 if let Ok(oid) = oid {
95 if !stashes.contains(&oid) {
96 let commit = repository.find_commit(oid).unwrap();
97
98 commits.push(CommitInfo::new(&commit));
99 indices.insert(oid, idx);
100 idx += 1;
101 }
102 }
103 }
104
105 assign_children(&mut commits, &indices);
106
107 let mut all_branches = assign_branches(&repository, &mut commits, &indices, settings)?;
108 correct_fork_merges(&commits, &indices, &mut all_branches, settings)?;
109 assign_sources_targets(&commits, &indices, &mut all_branches);
110
111 let (shortest_first, forward) = match settings.branch_order {
112 BranchOrder::ShortestFirst(fwd) => (true, fwd),
113 BranchOrder::LongestFirst(fwd) => (false, fwd),
114 };
115
116 assign_branch_columns(
117 &commits,
118 &indices,
119 &mut all_branches,
120 &settings.branches,
121 shortest_first,
122 forward,
123 );
124
125 let filtered_commits: Vec<CommitInfo> = commits
127 .into_iter()
128 .filter(|info| info.branch_trace.is_some())
129 .collect();
130
131 let filtered_indices: HashMap<Oid, usize> = filtered_commits
133 .iter()
134 .enumerate()
135 .map(|(idx, info)| (info.oid, idx))
136 .collect();
137
138 let index_map: HashMap<usize, Option<&usize>> = indices
140 .iter()
141 .map(|(oid, index)| (*index, filtered_indices.get(oid)))
142 .collect();
143
144 for branch in all_branches.iter_mut() {
146 if let Some(mut start_idx) = branch.range.0 {
147 let mut idx0 = index_map[&start_idx];
148 while idx0.is_none() {
149 start_idx += 1;
150 idx0 = index_map[&start_idx];
151 }
152 branch.range.0 = Some(*idx0.unwrap());
153 }
154 if let Some(mut end_idx) = branch.range.1 {
155 let mut idx0 = index_map[&end_idx];
156 while idx0.is_none() {
157 end_idx -= 1;
158 idx0 = index_map[&end_idx];
159 }
160 branch.range.1 = Some(*idx0.unwrap());
161 }
162 }
163
164 Ok(GitGraph {
165 repository,
166 commits: filtered_commits,
167 indices: filtered_indices,
168 all_branches,
169 head,
170 })
171 }
172
173 pub fn take_repository(self) -> Repository {
174 self.repository
175 }
176
177 pub fn commit(&self, id: Oid) -> Result<Commit<'_>, Error> {
178 self.repository.find_commit(id)
179 }
180}
181
182pub struct HeadInfo {
184 pub oid: Oid,
185 pub name: String,
186 pub is_branch: bool,
187}
188impl HeadInfo {
189 fn new(head: &Reference) -> Result<Self, String> {
190 let name = head.name().ok_or_else(|| "No name for HEAD".to_string())?;
191 let name = if name == "HEAD" {
192 name.to_string()
193 } else {
194 name[11..].to_string()
195 };
196
197 let h = HeadInfo {
198 oid: head.target().ok_or_else(|| "No id for HEAD".to_string())?,
199 name,
200 is_branch: head.is_branch(),
201 };
202 Ok(h)
203 }
204}
205
206pub struct CommitInfo {
208 pub oid: Oid,
209 pub is_merge: bool,
210 pub parents: [Option<Oid>; 2],
211 pub children: Vec<Oid>,
212 pub branches: Vec<usize>,
213 pub tags: Vec<usize>,
214 pub branch_trace: Option<usize>,
215}
216
217impl CommitInfo {
218 fn new(commit: &Commit) -> Self {
219 CommitInfo {
220 oid: commit.id(),
221 is_merge: commit.parent_count() > 1,
222 parents: [commit.parent_id(0).ok(), commit.parent_id(1).ok()],
223 children: Vec::new(),
224 branches: Vec::new(),
225 tags: Vec::new(),
226 branch_trace: None,
227 }
228 }
229}
230
231pub struct BranchInfo {
233 pub target: Oid,
234 pub merge_target: Option<Oid>,
235 pub source_branch: Option<usize>,
236 pub target_branch: Option<usize>,
237 pub name: String,
238 pub persistence: u8,
239 pub is_remote: bool,
240 pub is_merged: bool,
241 pub is_tag: bool,
242 pub visual: BranchVis,
243 pub range: (Option<usize>, Option<usize>),
244}
245impl BranchInfo {
246 #[allow(clippy::too_many_arguments)]
247 fn new(
248 target: Oid,
249 merge_target: Option<Oid>,
250 name: String,
251 persistence: u8,
252 is_remote: bool,
253 is_merged: bool,
254 is_tag: bool,
255 visual: BranchVis,
256 end_index: Option<usize>,
257 ) -> Self {
258 BranchInfo {
259 target,
260 merge_target,
261 target_branch: None,
262 source_branch: None,
263 name,
264 persistence,
265 is_remote,
266 is_merged,
267 is_tag,
268 visual,
269 range: (end_index, None),
270 }
271 }
272}
273
274pub struct BranchVis {
276 pub order_group: usize,
278 pub target_order_group: Option<usize>,
280 pub source_order_group: Option<usize>,
282 pub term_color: u8,
284 pub svg_color: String,
286 pub column: Option<usize>,
288}
289
290impl BranchVis {
291 fn new(order_group: usize, term_color: u8, svg_color: String) -> Self {
292 BranchVis {
293 order_group,
294 target_order_group: None,
295 source_order_group: None,
296 term_color,
297 svg_color,
298 column: None,
299 }
300 }
301}
302
303fn assign_children(commits: &mut [CommitInfo], indices: &HashMap<Oid, usize>) {
305 for idx in 0..commits.len() {
306 let (oid, parents) = {
307 let info = &commits[idx];
308 (info.oid, info.parents)
309 };
310 for par_oid in &parents {
311 if let Some(par_idx) = par_oid.and_then(|oid| indices.get(&oid)) {
312 commits[*par_idx].children.push(oid);
313 }
314 }
315 }
316}
317
318fn assign_branches(
325 repository: &Repository,
326 commits: &mut [CommitInfo],
327 indices: &HashMap<Oid, usize>,
328 settings: &Settings,
329) -> Result<Vec<BranchInfo>, String> {
330 let mut branch_idx = 0;
331
332 let mut branches = extract_branches(repository, commits, indices, settings)?;
333
334 let mut index_map: Vec<_> = (0..branches.len())
335 .map(|old_idx| {
336 let (target, is_tag, is_merged) = {
337 let branch = &branches[old_idx];
338 (branch.target, branch.is_tag, branch.is_merged)
339 };
340 if let Some(&idx) = &indices.get(&target) {
341 let info = &mut commits[idx];
342 if is_tag {
343 info.tags.push(old_idx);
344 } else if !is_merged {
345 info.branches.push(old_idx);
346 }
347 let oid = info.oid;
348 let any_assigned =
349 trace_branch(repository, commits, indices, &mut branches, oid, old_idx)
350 .unwrap_or(false);
351
352 if any_assigned || !is_merged {
353 branch_idx += 1;
354 Some(branch_idx - 1)
355 } else {
356 None
357 }
358 } else {
359 None
360 }
361 })
362 .collect();
363
364 let mut commit_count = vec![0; branches.len()];
365 for info in commits.iter_mut() {
366 if let Some(trace) = info.branch_trace {
367 commit_count[trace] += 1;
368 }
369 }
370
371 let mut count_skipped = 0;
372 for (idx, branch) in branches.iter().enumerate() {
373 if let Some(mapped) = index_map[idx] {
374 if commit_count[idx] == 0 && branch.is_merged && !branch.is_tag {
375 index_map[idx] = None;
376 count_skipped += 1;
377 } else {
378 index_map[idx] = Some(mapped - count_skipped);
379 }
380 }
381 }
382
383 for info in commits.iter_mut() {
384 if let Some(trace) = info.branch_trace {
385 info.branch_trace = index_map[trace];
386 for br in info.branches.iter_mut() {
387 *br = index_map[*br].unwrap();
388 }
389 for tag in info.tags.iter_mut() {
390 *tag = index_map[*tag].unwrap();
391 }
392 }
393 }
394
395 let branches: Vec<_> = branches
396 .into_iter()
397 .enumerate()
398 .filter_map(|(arr_index, branch)| {
399 if index_map[arr_index].is_some() {
400 Some(branch)
401 } else {
402 None
403 }
404 })
405 .collect();
406
407 Ok(branches)
408}
409
410fn correct_fork_merges(
411 commits: &[CommitInfo],
412 indices: &HashMap<Oid, usize>,
413 branches: &mut [BranchInfo],
414 settings: &Settings,
415) -> Result<(), String> {
416 for idx in 0..branches.len() {
417 if let Some(merge_target) = branches[idx]
418 .merge_target
419 .and_then(|oid| indices.get(&oid))
420 .and_then(|idx| commits.get(*idx))
421 .and_then(|info| info.branch_trace)
422 .and_then(|trace| branches.get(trace))
423 {
424 if branches[idx].name == merge_target.name {
425 let name = format!("{}{}", FORK, branches[idx].name);
426 let term_col = to_terminal_color(
427 &branch_color(
428 &name,
429 &settings.branches.terminal_colors[..],
430 &settings.branches.terminal_colors_unknown,
431 idx,
432 )[..],
433 )?;
434 let pos = branch_order(&name, &settings.branches.order);
435 let svg_col = branch_color(
436 &name,
437 &settings.branches.svg_colors,
438 &settings.branches.svg_colors_unknown,
439 idx,
440 );
441
442 branches[idx].name = format!("{}{}", FORK, branches[idx].name);
443 branches[idx].visual.order_group = pos;
444 branches[idx].visual.term_color = term_col;
445 branches[idx].visual.svg_color = svg_col;
446 }
447 }
448 }
449 Ok(())
450}
451fn assign_sources_targets(
452 commits: &[CommitInfo],
453 indices: &HashMap<Oid, usize>,
454 branches: &mut [BranchInfo],
455) {
456 for idx in 0..branches.len() {
457 let target_branch_idx = branches[idx]
458 .merge_target
459 .and_then(|oid| indices.get(&oid))
460 .and_then(|idx| commits.get(*idx))
461 .and_then(|info| info.branch_trace);
462
463 branches[idx].target_branch = target_branch_idx;
464
465 let group = target_branch_idx
466 .and_then(|trace| branches.get(trace))
467 .map(|br| br.visual.order_group);
468
469 branches[idx].visual.target_order_group = group;
470 }
471 for info in commits {
472 let mut max_par_order = None;
473 let mut source_branch_id = None;
474 for par_oid in info.parents.iter() {
475 let par_info = par_oid
476 .and_then(|oid| indices.get(&oid))
477 .and_then(|idx| commits.get(*idx));
478 if let Some(par_info) = par_info {
479 if par_info.branch_trace != info.branch_trace {
480 if let Some(trace) = par_info.branch_trace {
481 source_branch_id = Some(trace);
482 }
483
484 let group = par_info
485 .branch_trace
486 .and_then(|trace| branches.get(trace))
487 .map(|br| br.visual.order_group);
488 if let Some(gr) = max_par_order {
489 if let Some(p_group) = group {
490 if p_group > gr {
491 max_par_order = group;
492 }
493 }
494 } else {
495 max_par_order = group;
496 }
497 }
498 }
499 }
500 let branch = info.branch_trace.and_then(|trace| branches.get_mut(trace));
501 if let Some(branch) = branch {
502 if let Some(order) = max_par_order {
503 branch.visual.source_order_group = Some(order);
504 }
505 if let Some(source_id) = source_branch_id {
506 branch.source_branch = Some(source_id);
507 }
508 }
509 }
510}
511
512fn extract_actual_branches(
528 repository: &Repository,
529 indices: &HashMap<Oid, usize>,
530 settings: &Settings,
531 counter: &mut usize,
532) -> Result<Vec<BranchInfo>, String> {
533 let filter = if settings.include_remote {
535 None
536 } else {
537 Some(BranchType::Local)
538 };
539
540 let actual_branches = repository
542 .branches(filter)
543 .map_err(|err| err.message().to_string())?
544 .collect::<Result<Vec<_>, Error>>()
545 .map_err(|err| err.message().to_string())?;
546
547 let valid_branches = actual_branches
549 .iter()
550 .filter_map(|(br, tp)| {
551 br.get().name().and_then(|n| {
552 br.get().target().map(|t| {
553 *counter += 1; let start_index = match tp {
557 BranchType::Local => 11, BranchType::Remote => 13, };
560 let name = &n[start_index..];
561 let end_index = indices.get(&t).cloned();
562
563 let term_color = match to_terminal_color(
565 &branch_color(
566 name,
567 &settings.branches.terminal_colors[..],
568 &settings.branches.terminal_colors_unknown,
569 *counter,
570 )[..],
571 ) {
572 Ok(col) => col,
573 Err(err) => return Err(err), };
575
576 Ok(BranchInfo::new(
578 t,
579 None, name.to_string(),
581 branch_order(name, &settings.branches.persistence) as u8,
582 &BranchType::Remote == tp, false, false, BranchVis::new(
586 branch_order(name, &settings.branches.order),
587 term_color,
588 branch_color(
589 name,
590 &settings.branches.svg_colors,
591 &settings.branches.svg_colors_unknown,
592 *counter,
593 ),
594 ),
595 end_index,
596 ))
597 })
598 })
599 })
600 .collect::<Result<Vec<_>, String>>()?; Ok(valid_branches)
603}
604
605fn extract_merge_branches(
622 repository: &Repository,
623 commits: &[CommitInfo],
624 settings: &Settings,
625 counter: &mut usize,
626) -> Result<Vec<BranchInfo>, String> {
627 let mut merge_branches = Vec::new();
628
629 for (idx, info) in commits.iter().enumerate() {
630 if info.is_merge {
632 let commit = repository
633 .find_commit(info.oid)
634 .map_err(|err| err.message().to_string())?;
635
636 if let Some(summary) = commit.summary() {
638 *counter += 1; let parent_oid = commit
641 .parent_id(1)
642 .map_err(|err| err.message().to_string())?;
643
644 let branch_name = parse_merge_summary(summary, &settings.merge_patterns)
646 .unwrap_or_else(|| "unknown".to_string());
647
648 let persistence = branch_order(&branch_name, &settings.branches.persistence) as u8;
650 let pos = branch_order(&branch_name, &settings.branches.order);
651
652 let term_col = to_terminal_color(
654 &branch_color(
655 &branch_name,
656 &settings.branches.terminal_colors[..],
657 &settings.branches.terminal_colors_unknown,
658 *counter,
659 )[..],
660 )?;
661 let svg_col = branch_color(
662 &branch_name,
663 &settings.branches.svg_colors,
664 &settings.branches.svg_colors_unknown,
665 *counter,
666 );
667
668 let branch_info = BranchInfo::new(
670 parent_oid, Some(info.oid), branch_name,
673 persistence,
674 false, true, false, BranchVis::new(pos, term_col, svg_col),
678 Some(idx + 1), );
680 merge_branches.push(branch_info);
681 }
682 }
683 }
684 Ok(merge_branches)
685}
686
687fn extract_tags_as_branches(
703 repository: &Repository,
704 indices: &HashMap<Oid, usize>,
705 settings: &Settings,
706 counter: &mut usize,
707) -> Result<Vec<BranchInfo>, String> {
708 let mut tags_info = Vec::new();
709 let mut tags_raw = Vec::new();
710
711 repository
713 .tag_foreach(|oid, name| {
714 tags_raw.push((oid, name.to_vec()));
715 true })
717 .map_err(|err| err.message().to_string())?;
718
719 for (oid, name_bytes) in tags_raw {
720 let name = std::str::from_utf8(&name_bytes[5..]).map_err(|err| err.to_string())?;
722
723 let target = repository
725 .find_tag(oid)
726 .map(|tag| tag.target_id())
727 .or_else(|_| repository.find_commit(oid).map(|_| oid)); if let Ok(target_oid) = target {
730 if let Some(target_index) = indices.get(&target_oid) {
732 *counter += 1; let term_col = to_terminal_color(
736 &branch_color(
737 name,
738 &settings.branches.terminal_colors[..],
739 &settings.branches.terminal_colors_unknown,
740 *counter,
741 )[..],
742 )?;
743 let pos = branch_order(name, &settings.branches.order);
744 let svg_col = branch_color(
745 name,
746 &settings.branches.svg_colors,
747 &settings.branches.svg_colors_unknown,
748 *counter,
749 );
750
751 let tag_info = BranchInfo::new(
753 target_oid,
754 None, name.to_string(),
756 settings.branches.persistence.len() as u8 + 1, false, false, true, BranchVis::new(pos, term_col, svg_col),
761 Some(*target_index),
762 );
763 tags_info.push(tag_info);
764 }
765 }
766 }
767 Ok(tags_info)
768}
769
770fn extract_branches(
789 repository: &Repository,
790 commits: &[CommitInfo],
791 indices: &HashMap<Oid, usize>,
792 settings: &Settings,
793) -> Result<Vec<BranchInfo>, String> {
794 let mut counter = 0; let mut all_branches: Vec<BranchInfo> = Vec::new();
796
797 let actual_branches = extract_actual_branches(repository, indices, settings, &mut counter)?;
799 all_branches.extend(actual_branches);
800
801 let merge_branches = extract_merge_branches(repository, commits, settings, &mut counter)?;
803 all_branches.extend(merge_branches);
804
805 let tags_as_branches = extract_tags_as_branches(repository, indices, settings, &mut counter)?;
807 all_branches.extend(tags_as_branches);
808
809 all_branches.sort_by_cached_key(|branch| (branch.persistence, !branch.is_merged));
812
813 Ok(all_branches)
814}
815
816fn trace_branch(
819 repository: &Repository,
820 commits: &mut [CommitInfo],
821 indices: &HashMap<Oid, usize>,
822 branches: &mut [BranchInfo],
823 oid: Oid,
824 branch_index: usize,
825) -> Result<bool, Error> {
826 let mut curr_oid = oid;
827 let mut prev_index: Option<usize> = None;
828 let mut start_index: Option<i32> = None;
829 let mut any_assigned = false;
830 while let Some(index) = indices.get(&curr_oid) {
831 let info = &mut commits[*index];
832 if let Some(old_trace) = info.branch_trace {
833 let (old_name, old_term, old_svg, old_range) = {
834 let old_branch = &branches[old_trace];
835 (
836 &old_branch.name.clone(),
837 old_branch.visual.term_color,
838 old_branch.visual.svg_color.clone(),
839 old_branch.range,
840 )
841 };
842 let new_name = &branches[branch_index].name;
843 let old_end = old_range.0.unwrap_or(0);
844 let new_end = branches[branch_index].range.0.unwrap_or(0);
845 if new_name == old_name && old_end >= new_end {
846 let old_branch = &mut branches[old_trace];
847 if let Some(old_end) = old_range.1 {
848 if index > &old_end {
849 old_branch.range = (None, None);
850 } else {
851 old_branch.range = (Some(*index), old_branch.range.1);
852 }
853 } else {
854 old_branch.range = (Some(*index), old_branch.range.1);
855 }
856 } else {
857 let branch = &mut branches[branch_index];
858 if branch.name.starts_with(ORIGIN) && branch.name[7..] == old_name[..] {
859 branch.visual.term_color = old_term;
860 branch.visual.svg_color = old_svg;
861 }
862 match prev_index {
863 None => start_index = Some(*index as i32 - 1),
864 Some(prev_index) => {
865 if commits[prev_index].is_merge {
868 let mut temp_index = prev_index;
869 for sibling_oid in &commits[*index].children {
870 if sibling_oid != &curr_oid {
871 let sibling_index = indices[sibling_oid];
872 if sibling_index > temp_index {
873 temp_index = sibling_index;
874 }
875 }
876 }
877 start_index = Some(temp_index as i32);
878 } else {
879 start_index = Some(*index as i32 - 1);
880 }
881 }
882 }
883 break;
884 }
885 }
886
887 info.branch_trace = Some(branch_index);
888 any_assigned = true;
889
890 let commit = repository.find_commit(curr_oid)?;
891 if commit.parent_count() == 0 {
892 start_index = Some(*index as i32);
894 break;
895 }
896 prev_index = Some(*index);
898 curr_oid = commit.parent_id(0)?;
899 }
900
901 let branch = &mut branches[branch_index];
902 if let Some(end) = branch.range.0 {
903 if let Some(start_index) = start_index {
904 if start_index < end as i32 {
905 branch.range = (None, None);
907 } else {
908 branch.range = (branch.range.0, Some(start_index as usize));
909 }
910 } else {
911 branch.range = (branch.range.0, None);
912 }
913 } else {
914 branch.range = (branch.range.0, start_index.map(|si| si as usize));
915 }
916 Ok(any_assigned)
917}
918
919fn assign_branch_columns(
922 commits: &[CommitInfo],
923 indices: &HashMap<Oid, usize>,
924 branches: &mut [BranchInfo],
925 settings: &BranchSettings,
926 shortest_first: bool,
927 forward: bool,
928) {
929 let mut occupied: Vec<Vec<Vec<(usize, usize)>>> = vec![vec![]; settings.order.len() + 1];
930
931 let length_sort_factor = if shortest_first { 1 } else { -1 };
932 let start_sort_factor = if forward { 1 } else { -1 };
933
934 let mut branches_sort: Vec<_> = branches
935 .iter()
936 .enumerate()
937 .filter(|(_idx, br)| br.range.0.is_some() || br.range.1.is_some())
938 .map(|(idx, br)| {
939 (
940 idx,
941 br.range.0.unwrap_or(0),
942 br.range.1.unwrap_or(branches.len() - 1),
943 br.visual
944 .source_order_group
945 .unwrap_or(settings.order.len() + 1),
946 br.visual
947 .target_order_group
948 .unwrap_or(settings.order.len() + 1),
949 )
950 })
951 .collect();
952
953 branches_sort.sort_by_cached_key(|tup| {
954 (
955 std::cmp::max(tup.3, tup.4),
956 (tup.2 as i32 - tup.1 as i32) * length_sort_factor,
957 tup.1 as i32 * start_sort_factor,
958 )
959 });
960
961 for (branch_idx, start, end, _, _) in branches_sort {
962 let branch = &branches[branch_idx];
963 let group = branch.visual.order_group;
964 let group_occ = &mut occupied[group];
965
966 let align_right = branch
967 .source_branch
968 .map(|src| branches[src].visual.order_group > branch.visual.order_group)
969 .unwrap_or(false)
970 || branch
971 .target_branch
972 .map(|trg| branches[trg].visual.order_group > branch.visual.order_group)
973 .unwrap_or(false);
974
975 let len = group_occ.len();
976 let mut found = len;
977 for i in 0..len {
978 let index = if align_right { len - i - 1 } else { i };
979 let column_occ = &group_occ[index];
980 let mut occ = false;
981 for (s, e) in column_occ {
982 if start <= *e && end >= *s {
983 occ = true;
984 break;
985 }
986 }
987 if !occ {
988 if let Some(merge_trace) = branch
989 .merge_target
990 .and_then(|t| indices.get(&t))
991 .and_then(|t_idx| commits[*t_idx].branch_trace)
992 {
993 let merge_branch = &branches[merge_trace];
994 if merge_branch.visual.order_group == branch.visual.order_group {
995 if let Some(merge_column) = merge_branch.visual.column {
996 if merge_column == index {
997 occ = true;
998 }
999 }
1000 }
1001 }
1002 }
1003 if !occ {
1004 found = index;
1005 break;
1006 }
1007 }
1008
1009 let branch = &mut branches[branch_idx];
1010 branch.visual.column = Some(found);
1011 if found == group_occ.len() {
1012 group_occ.push(vec![]);
1013 }
1014 group_occ[found].push((start, end));
1015 }
1016
1017 let mut group_offset: Vec<usize> = vec![];
1019 let mut acc = 0;
1020 for group in occupied {
1021 group_offset.push(acc);
1022 acc += group.len();
1023 }
1024
1025 for branch in branches {
1030 if let Some(column) = branch.visual.column {
1031 let offset = group_offset[branch.visual.order_group];
1032 branch.visual.column = Some(column + offset);
1033 }
1034 }
1035}
1036
1037fn branch_order(name: &str, order: &[Regex]) -> usize {
1039 order
1040 .iter()
1041 .position(|b| (name.starts_with(ORIGIN) && b.is_match(&name[7..])) || b.is_match(name))
1042 .unwrap_or(order.len())
1043}
1044
1045fn branch_color<T: Clone>(
1047 name: &str,
1048 order: &[(Regex, Vec<T>)],
1049 unknown: &[T],
1050 counter: usize,
1051) -> T {
1052 let stripped_name = name.strip_prefix(ORIGIN).unwrap_or(name);
1053
1054 for (regex, colors) in order {
1055 if regex.is_match(stripped_name) {
1056 return colors[counter % colors.len()].clone();
1057 }
1058 }
1059
1060 unknown[counter % unknown.len()].clone()
1061}
1062
1063pub fn parse_merge_summary(summary: &str, patterns: &MergePatterns) -> Option<String> {
1065 for regex in &patterns.patterns {
1066 if let Some(captures) = regex.captures(summary) {
1067 if captures.len() == 2 && captures.get(1).is_some() {
1068 return captures.get(1).map(|m| m.as_str().to_string());
1069 }
1070 }
1071 }
1072 None
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use crate::settings::MergePatterns;
1078
1079 #[test]
1080 fn parse_merge_summary() {
1081 let patterns = MergePatterns::default();
1082
1083 let gitlab_pull = "Merge branch 'feature/my-feature' into 'master'";
1084 let git_default = "Merge branch 'feature/my-feature' into dev";
1085 let git_master = "Merge branch 'feature/my-feature'";
1086 let github_pull = "Merge pull request #1 from user-x/feature/my-feature";
1087 let github_pull_2 = "Merge branch 'feature/my-feature' of github.com:user-x/repo";
1088 let bitbucket_pull = "Merged in feature/my-feature (pull request #1)";
1089
1090 assert_eq!(
1091 super::parse_merge_summary(gitlab_pull, &patterns),
1092 Some("feature/my-feature".to_string()),
1093 );
1094 assert_eq!(
1095 super::parse_merge_summary(git_default, &patterns),
1096 Some("feature/my-feature".to_string()),
1097 );
1098 assert_eq!(
1099 super::parse_merge_summary(git_master, &patterns),
1100 Some("feature/my-feature".to_string()),
1101 );
1102 assert_eq!(
1103 super::parse_merge_summary(github_pull, &patterns),
1104 Some("feature/my-feature".to_string()),
1105 );
1106 assert_eq!(
1107 super::parse_merge_summary(github_pull_2, &patterns),
1108 Some("feature/my-feature".to_string()),
1109 );
1110 assert_eq!(
1111 super::parse_merge_summary(bitbucket_pull, &patterns),
1112 Some("feature/my-feature".to_string()),
1113 );
1114 }
1115}