1use crate::print::colors::to_terminal_color;
4use crate::settings::{BranchOrder, BranchSettings, MergePatterns, Settings};
5use git2::{BranchType, Commit, Error, Oid, Reference, Repository};
6use itertools::Itertools;
7use regex::Regex;
8use std::collections::{HashMap, HashSet};
9
10const ORIGIN: &str = "origin/";
11const FORK: &str = "fork/";
12
13pub struct GitGraph {
15 pub repository: Repository,
16 pub commits: Vec<CommitInfo>,
17 pub indices: HashMap<Oid, usize>,
19 pub all_branches: Vec<BranchInfo>,
21 pub branches: Vec<usize>,
23 pub tags: Vec<usize>,
25 pub head: HeadInfo,
27}
28
29impl GitGraph {
30 pub fn new(
31 mut repository: Repository,
32 settings: &Settings,
33 max_count: Option<usize>,
34 ) -> Result<Self, String> {
35 let mut stashes = HashSet::new();
36 repository
37 .stash_foreach(|_, _, oid| {
38 stashes.insert(*oid);
39 true
40 })
41 .map_err(|err| err.message().to_string())?;
42
43 let mut walk = repository
44 .revwalk()
45 .map_err(|err| err.message().to_string())?;
46
47 walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
48 .map_err(|err| err.message().to_string())?;
49
50 walk.push_glob("*")
51 .map_err(|err| err.message().to_string())?;
52
53 if repository.is_shallow() {
54 return Err("ERROR: git-graph does not support shallow clones due to a missing feature in the underlying libgit2 library.".to_string());
55 }
56
57 let head = HeadInfo::new(&repository.head().map_err(|err| err.message().to_string())?)?;
58
59 let mut commits = Vec::new();
60 let mut indices = HashMap::new();
61 let mut idx = 0;
62 for oid in walk {
63 if let Some(max) = max_count {
64 if idx >= max {
65 break;
66 }
67 }
68 if let Ok(oid) = oid {
69 if !stashes.contains(&oid) {
70 let commit = repository.find_commit(oid).unwrap();
71
72 commits.push(CommitInfo::new(&commit));
73 indices.insert(oid, idx);
74 idx += 1;
75 }
76 }
77 }
78
79 assign_children(&mut commits, &indices);
80
81 let mut all_branches = assign_branches(&repository, &mut commits, &indices, settings)?;
82 correct_fork_merges(&commits, &indices, &mut all_branches, settings)?;
83 assign_sources_targets(&commits, &indices, &mut all_branches);
84
85 let (shortest_first, forward) = match settings.branch_order {
86 BranchOrder::ShortestFirst(fwd) => (true, fwd),
87 BranchOrder::LongestFirst(fwd) => (false, fwd),
88 };
89
90 assign_branch_columns(
91 &commits,
92 &indices,
93 &mut all_branches,
94 &settings.branches,
95 shortest_first,
96 forward,
97 );
98
99 let filtered_commits: Vec<CommitInfo> = commits
100 .into_iter()
101 .filter(|info| info.branch_trace.is_some())
102 .collect();
103
104 let filtered_indices: HashMap<Oid, usize> = filtered_commits
105 .iter()
106 .enumerate()
107 .map(|(idx, info)| (info.oid, idx))
108 .collect();
109
110 let index_map: HashMap<usize, Option<&usize>> = indices
111 .iter()
112 .map(|(oid, index)| (*index, filtered_indices.get(oid)))
113 .collect();
114
115 for branch in all_branches.iter_mut() {
116 if let Some(mut start_idx) = branch.range.0 {
117 let mut idx0 = index_map[&start_idx];
118 while idx0.is_none() {
119 start_idx += 1;
120 idx0 = index_map[&start_idx];
121 }
122 branch.range.0 = Some(*idx0.unwrap());
123 }
124 if let Some(mut end_idx) = branch.range.1 {
125 let mut idx0 = index_map[&end_idx];
126 while idx0.is_none() {
127 end_idx -= 1;
128 idx0 = index_map[&end_idx];
129 }
130 branch.range.1 = Some(*idx0.unwrap());
131 }
132 }
133
134 let branches = all_branches
135 .iter()
136 .enumerate()
137 .filter_map(|(idx, br)| {
138 if !br.is_merged && !br.is_tag {
139 Some(idx)
140 } else {
141 None
142 }
143 })
144 .collect();
145
146 let tags = all_branches
147 .iter()
148 .enumerate()
149 .filter_map(|(idx, br)| {
150 if !br.is_merged && br.is_tag {
151 Some(idx)
152 } else {
153 None
154 }
155 })
156 .collect();
157
158 Ok(GitGraph {
159 repository,
160 commits: filtered_commits,
161 indices: filtered_indices,
162 all_branches,
163 branches,
164 tags,
165 head,
166 })
167 }
168
169 pub fn take_repository(self) -> Repository {
170 self.repository
171 }
172
173 pub fn commit(&self, id: Oid) -> Result<Commit, Error> {
174 self.repository.find_commit(id)
175 }
176}
177
178pub struct HeadInfo {
180 pub oid: Oid,
181 pub name: String,
182 pub is_branch: bool,
183}
184impl HeadInfo {
185 fn new(head: &Reference) -> Result<Self, String> {
186 let name = head.name().ok_or_else(|| "No name for HEAD".to_string())?;
187 let name = if name == "HEAD" {
188 name.to_string()
189 } else {
190 name[11..].to_string()
191 };
192
193 let h = HeadInfo {
194 oid: head.target().ok_or_else(|| "No id for HEAD".to_string())?,
195 name,
196 is_branch: head.is_branch(),
197 };
198 Ok(h)
199 }
200}
201
202pub struct CommitInfo {
204 pub oid: Oid,
205 pub is_merge: bool,
206 pub parents: [Option<Oid>; 2],
207 pub children: Vec<Oid>,
208 pub branches: Vec<usize>,
209 pub tags: Vec<usize>,
210 pub branch_trace: Option<usize>,
211}
212
213impl CommitInfo {
214 fn new(commit: &Commit) -> Self {
215 CommitInfo {
216 oid: commit.id(),
217 is_merge: commit.parent_count() > 1,
218 parents: [commit.parent_id(0).ok(), commit.parent_id(1).ok()],
219 children: Vec::new(),
220 branches: Vec::new(),
221 tags: Vec::new(),
222 branch_trace: None,
223 }
224 }
225}
226
227pub struct BranchInfo {
229 pub target: Oid,
230 pub merge_target: Option<Oid>,
231 pub source_branch: Option<usize>,
232 pub target_branch: Option<usize>,
233 pub name: String,
234 pub persistence: u8,
235 pub is_remote: bool,
236 pub is_merged: bool,
237 pub is_tag: bool,
238 pub visual: BranchVis,
239 pub range: (Option<usize>, Option<usize>),
240}
241impl BranchInfo {
242 #[allow(clippy::too_many_arguments)]
243 fn new(
244 target: Oid,
245 merge_target: Option<Oid>,
246 name: String,
247 persistence: u8,
248 is_remote: bool,
249 is_merged: bool,
250 is_tag: bool,
251 visual: BranchVis,
252 end_index: Option<usize>,
253 ) -> Self {
254 BranchInfo {
255 target,
256 merge_target,
257 target_branch: None,
258 source_branch: None,
259 name,
260 persistence,
261 is_remote,
262 is_merged,
263 is_tag,
264 visual,
265 range: (end_index, None),
266 }
267 }
268}
269
270pub struct BranchVis {
272 pub order_group: usize,
274 pub target_order_group: Option<usize>,
276 pub source_order_group: Option<usize>,
278 pub term_color: u8,
280 pub svg_color: String,
282 pub column: Option<usize>,
284}
285
286impl BranchVis {
287 fn new(order_group: usize, term_color: u8, svg_color: String) -> Self {
288 BranchVis {
289 order_group,
290 target_order_group: None,
291 source_order_group: None,
292 term_color,
293 svg_color,
294 column: None,
295 }
296 }
297}
298
299fn assign_children(commits: &mut [CommitInfo], indices: &HashMap<Oid, usize>) {
301 for idx in 0..commits.len() {
302 let (oid, parents) = {
303 let info = &commits[idx];
304 (info.oid, info.parents)
305 };
306 for par_oid in &parents {
307 if let Some(par_idx) = par_oid.and_then(|oid| indices.get(&oid)) {
308 commits[*par_idx].children.push(oid);
309 }
310 }
311 }
312}
313
314fn assign_branches(
321 repository: &Repository,
322 commits: &mut [CommitInfo],
323 indices: &HashMap<Oid, usize>,
324 settings: &Settings,
325) -> Result<Vec<BranchInfo>, String> {
326 let mut branch_idx = 0;
327
328 let mut branches = extract_branches(repository, commits, indices, settings)?;
329
330 let mut index_map: Vec<_> = (0..branches.len())
331 .map(|old_idx| {
332 let (target, is_tag, is_merged) = {
333 let branch = &branches[old_idx];
334 (branch.target, branch.is_tag, branch.is_merged)
335 };
336 if let Some(&idx) = &indices.get(&target) {
337 let info = &mut commits[idx];
338 if is_tag {
339 info.tags.push(old_idx);
340 } else if !is_merged {
341 info.branches.push(old_idx);
342 }
343 let oid = info.oid;
344 let any_assigned =
345 trace_branch(repository, commits, indices, &mut branches, oid, old_idx)
346 .unwrap_or(false);
347
348 if any_assigned || !is_merged {
349 branch_idx += 1;
350 Some(branch_idx - 1)
351 } else {
352 None
353 }
354 } else {
355 None
356 }
357 })
358 .collect();
359
360 let mut commit_count = vec![0; branches.len()];
361 for info in commits.iter_mut() {
362 if let Some(trace) = info.branch_trace {
363 commit_count[trace] += 1;
364 }
365 }
366
367 let mut count_skipped = 0;
368 for (idx, branch) in branches.iter().enumerate() {
369 if let Some(mapped) = index_map[idx] {
370 if commit_count[idx] == 0 && branch.is_merged && !branch.is_tag {
371 index_map[idx] = None;
372 count_skipped += 1;
373 } else {
374 index_map[idx] = Some(mapped - count_skipped);
375 }
376 }
377 }
378
379 for info in commits.iter_mut() {
380 if let Some(trace) = info.branch_trace {
381 info.branch_trace = index_map[trace];
382 for br in info.branches.iter_mut() {
383 *br = index_map[*br].unwrap();
384 }
385 for tag in info.tags.iter_mut() {
386 *tag = index_map[*tag].unwrap();
387 }
388 }
389 }
390
391 let branches: Vec<_> = branches
392 .into_iter()
393 .enumerate()
394 .filter_map(|(arr_index, branch)| {
395 if index_map[arr_index].is_some() {
396 Some(branch)
397 } else {
398 None
399 }
400 })
401 .collect();
402
403 Ok(branches)
404}
405
406fn correct_fork_merges(
407 commits: &[CommitInfo],
408 indices: &HashMap<Oid, usize>,
409 branches: &mut [BranchInfo],
410 settings: &Settings,
411) -> Result<(), String> {
412 for idx in 0..branches.len() {
413 if let Some(merge_target) = branches[idx]
414 .merge_target
415 .and_then(|oid| indices.get(&oid))
416 .and_then(|idx| commits.get(*idx))
417 .and_then(|info| info.branch_trace)
418 .and_then(|trace| branches.get(trace))
419 {
420 if branches[idx].name == merge_target.name {
421 let name = format!("{}{}", FORK, branches[idx].name);
422 let term_col = to_terminal_color(
423 &branch_color(
424 &name,
425 &settings.branches.terminal_colors[..],
426 &settings.branches.terminal_colors_unknown,
427 idx,
428 )[..],
429 )?;
430 let pos = branch_order(&name, &settings.branches.order);
431 let svg_col = branch_color(
432 &name,
433 &settings.branches.svg_colors,
434 &settings.branches.svg_colors_unknown,
435 idx,
436 );
437
438 branches[idx].name = format!("{}{}", FORK, branches[idx].name);
439 branches[idx].visual.order_group = pos;
440 branches[idx].visual.term_color = term_col;
441 branches[idx].visual.svg_color = svg_col;
442 }
443 }
444 }
445 Ok(())
446}
447fn assign_sources_targets(
448 commits: &[CommitInfo],
449 indices: &HashMap<Oid, usize>,
450 branches: &mut [BranchInfo],
451) {
452 for idx in 0..branches.len() {
453 let target_branch_idx = branches[idx]
454 .merge_target
455 .and_then(|oid| indices.get(&oid))
456 .and_then(|idx| commits.get(*idx))
457 .and_then(|info| info.branch_trace);
458
459 branches[idx].target_branch = target_branch_idx;
460
461 let group = target_branch_idx
462 .and_then(|trace| branches.get(trace))
463 .map(|br| br.visual.order_group);
464
465 branches[idx].visual.target_order_group = group;
466 }
467 for info in commits {
468 let mut max_par_order = None;
469 let mut source_branch_id = None;
470 for par_oid in info.parents.iter() {
471 let par_info = par_oid
472 .and_then(|oid| indices.get(&oid))
473 .and_then(|idx| commits.get(*idx));
474 if let Some(par_info) = par_info {
475 if par_info.branch_trace != info.branch_trace {
476 if let Some(trace) = par_info.branch_trace {
477 source_branch_id = Some(trace);
478 }
479
480 let group = par_info
481 .branch_trace
482 .and_then(|trace| branches.get(trace))
483 .map(|br| br.visual.order_group);
484 if let Some(gr) = max_par_order {
485 if let Some(p_group) = group {
486 if p_group > gr {
487 max_par_order = group;
488 }
489 }
490 } else {
491 max_par_order = group;
492 }
493 }
494 }
495 }
496 let branch = info.branch_trace.and_then(|trace| branches.get_mut(trace));
497 if let Some(branch) = branch {
498 if let Some(order) = max_par_order {
499 branch.visual.source_order_group = Some(order);
500 }
501 if let Some(source_id) = source_branch_id {
502 branch.source_branch = Some(source_id);
503 }
504 }
505 }
506}
507
508fn extract_branches(
510 repository: &Repository,
511 commits: &[CommitInfo],
512 indices: &HashMap<Oid, usize>,
513 settings: &Settings,
514) -> Result<Vec<BranchInfo>, String> {
515 let filter = if settings.include_remote {
516 None
517 } else {
518 Some(BranchType::Local)
519 };
520 let actual_branches = repository
521 .branches(filter)
522 .map_err(|err| err.message().to_string())?
523 .collect::<Result<Vec<_>, Error>>()
524 .map_err(|err| err.message().to_string())?;
525
526 let mut counter = 0;
527
528 let mut valid_branches = actual_branches
529 .iter()
530 .filter_map(|(br, tp)| {
531 br.get().name().and_then(|n| {
532 br.get().target().map(|t| {
533 counter += 1;
534 let start_index = match tp {
535 BranchType::Local => 11,
536 BranchType::Remote => 13,
537 };
538 let name = &n[start_index..];
539 let end_index = indices.get(&t).cloned();
540
541 let term_color = match to_terminal_color(
542 &branch_color(
543 name,
544 &settings.branches.terminal_colors[..],
545 &settings.branches.terminal_colors_unknown,
546 counter,
547 )[..],
548 ) {
549 Ok(col) => col,
550 Err(err) => return Err(err),
551 };
552
553 Ok(BranchInfo::new(
554 t,
555 None,
556 name.to_string(),
557 branch_order(name, &settings.branches.persistence) as u8,
558 &BranchType::Remote == tp,
559 false,
560 false,
561 BranchVis::new(
562 branch_order(name, &settings.branches.order),
563 term_color,
564 branch_color(
565 name,
566 &settings.branches.svg_colors,
567 &settings.branches.svg_colors_unknown,
568 counter,
569 ),
570 ),
571 end_index,
572 ))
573 })
574 })
575 })
576 .collect::<Result<Vec<_>, String>>()?;
577
578 for (idx, info) in commits.iter().enumerate() {
579 let commit = repository
580 .find_commit(info.oid)
581 .map_err(|err| err.message().to_string())?;
582 if info.is_merge {
583 if let Some(summary) = commit.summary() {
584 counter += 1;
585
586 let parent_oid = commit
587 .parent_id(1)
588 .map_err(|err| err.message().to_string())?;
589
590 let branch_name = parse_merge_summary(summary, &settings.merge_patterns)
591 .unwrap_or_else(|| "unknown".to_string());
592
593 let persistence = branch_order(&branch_name, &settings.branches.persistence) as u8;
594
595 let pos = branch_order(&branch_name, &settings.branches.order);
596
597 let term_col = to_terminal_color(
598 &branch_color(
599 &branch_name,
600 &settings.branches.terminal_colors[..],
601 &settings.branches.terminal_colors_unknown,
602 counter,
603 )[..],
604 )?;
605 let svg_col = branch_color(
606 &branch_name,
607 &settings.branches.svg_colors,
608 &settings.branches.svg_colors_unknown,
609 counter,
610 );
611
612 let branch_info = BranchInfo::new(
613 parent_oid,
614 Some(info.oid),
615 branch_name,
616 persistence,
617 false,
618 true,
619 false,
620 BranchVis::new(pos, term_col, svg_col),
621 Some(idx + 1),
622 );
623 valid_branches.push(branch_info);
624 }
625 }
626 }
627
628 valid_branches.sort_by_cached_key(|branch| (branch.persistence, !branch.is_merged));
629
630 let mut tags = Vec::new();
631
632 repository
633 .tag_foreach(|oid, name| {
634 tags.push((oid, name.to_vec()));
635 true
636 })
637 .map_err(|err| err.message().to_string())?;
638
639 for (oid, name) in tags {
640 let name = std::str::from_utf8(&name[5..]).map_err(|err| err.to_string())?;
641
642 let target = repository
643 .find_tag(oid)
644 .map(|tag| tag.target_id())
645 .or_else(|_| repository.find_commit(oid).map(|_| oid));
646
647 if let Ok(target_oid) = target {
648 if let Some(target_index) = indices.get(&target_oid) {
649 counter += 1;
650 let term_col = to_terminal_color(
651 &branch_color(
652 name,
653 &settings.branches.terminal_colors[..],
654 &settings.branches.terminal_colors_unknown,
655 counter,
656 )[..],
657 )?;
658 let pos = branch_order(name, &settings.branches.order);
659 let svg_col = branch_color(
660 name,
661 &settings.branches.svg_colors,
662 &settings.branches.svg_colors_unknown,
663 counter,
664 );
665 let tag_info = BranchInfo::new(
666 target_oid,
667 None,
668 name.to_string(),
669 settings.branches.persistence.len() as u8 + 1,
670 false,
671 false,
672 true,
673 BranchVis::new(pos, term_col, svg_col),
674 Some(*target_index),
675 );
676 valid_branches.push(tag_info);
677 }
678 }
679 }
680
681 Ok(valid_branches)
682}
683
684fn trace_branch(
687 repository: &Repository,
688 commits: &mut [CommitInfo],
689 indices: &HashMap<Oid, usize>,
690 branches: &mut [BranchInfo],
691 oid: Oid,
692 branch_index: usize,
693) -> Result<bool, Error> {
694 let mut curr_oid = oid;
695 let mut prev_index: Option<usize> = None;
696 let mut start_index: Option<i32> = None;
697 let mut any_assigned = false;
698 while let Some(index) = indices.get(&curr_oid) {
699 let info = &mut commits[*index];
700 if let Some(old_trace) = info.branch_trace {
701 let (old_name, old_term, old_svg, old_range) = {
702 let old_branch = &branches[old_trace];
703 (
704 &old_branch.name.clone(),
705 old_branch.visual.term_color,
706 old_branch.visual.svg_color.clone(),
707 old_branch.range,
708 )
709 };
710 let new_name = &branches[branch_index].name;
711 let old_end = old_range.0.unwrap_or(0);
712 let new_end = branches[branch_index].range.0.unwrap_or(0);
713 if new_name == old_name && old_end >= new_end {
714 let old_branch = &mut branches[old_trace];
715 if let Some(old_end) = old_range.1 {
716 if index > &old_end {
717 old_branch.range = (None, None);
718 } else {
719 old_branch.range = (Some(*index), old_branch.range.1);
720 }
721 } else {
722 old_branch.range = (Some(*index), old_branch.range.1);
723 }
724 } else {
725 let branch = &mut branches[branch_index];
726 if branch.name.starts_with(ORIGIN) && branch.name[7..] == old_name[..] {
727 branch.visual.term_color = old_term;
728 branch.visual.svg_color = old_svg;
729 }
730 match prev_index {
731 None => start_index = Some(*index as i32 - 1),
732 Some(prev_index) => {
733 if commits[prev_index].is_merge {
736 let mut temp_index = prev_index;
737 for sibling_oid in &commits[*index].children {
738 if sibling_oid != &curr_oid {
739 let sibling_index = indices[sibling_oid];
740 if sibling_index > temp_index {
741 temp_index = sibling_index;
742 }
743 }
744 }
745 start_index = Some(temp_index as i32);
746 } else {
747 start_index = Some(*index as i32 - 1);
748 }
749 }
750 }
751 break;
752 }
753 }
754
755 info.branch_trace = Some(branch_index);
756 any_assigned = true;
757
758 let commit = repository.find_commit(curr_oid)?;
759 match commit.parent_count() {
760 0 => {
761 start_index = Some(*index as i32);
762 break;
763 }
764 _ => {
765 prev_index = Some(*index);
766 curr_oid = commit.parent_id(0)?;
767 }
768 }
769 }
770
771 let branch = &mut branches[branch_index];
772 if let Some(end) = branch.range.0 {
773 if let Some(start_index) = start_index {
774 if start_index < end as i32 {
775 branch.range = (None, None);
777 } else {
778 branch.range = (branch.range.0, Some(start_index as usize));
779 }
780 } else {
781 branch.range = (branch.range.0, None);
782 }
783 } else {
784 branch.range = (branch.range.0, start_index.map(|si| si as usize));
785 }
786 Ok(any_assigned)
787}
788
789fn assign_branch_columns(
792 commits: &[CommitInfo],
793 indices: &HashMap<Oid, usize>,
794 branches: &mut [BranchInfo],
795 settings: &BranchSettings,
796 shortest_first: bool,
797 forward: bool,
798) {
799 let mut occupied: Vec<Vec<Vec<(usize, usize)>>> = vec![vec![]; settings.order.len() + 1];
800
801 let length_sort_factor = if shortest_first { 1 } else { -1 };
802 let start_sort_factor = if forward { 1 } else { -1 };
803
804 let mut branches_sort: Vec<_> = branches
805 .iter()
806 .enumerate()
807 .filter(|(_idx, br)| br.range.0.is_some() || br.range.1.is_some())
808 .map(|(idx, br)| {
809 (
810 idx,
811 br.range.0.unwrap_or(0),
812 br.range.1.unwrap_or(branches.len() - 1),
813 br.visual
814 .source_order_group
815 .unwrap_or(settings.order.len() + 1),
816 br.visual
817 .target_order_group
818 .unwrap_or(settings.order.len() + 1),
819 )
820 })
821 .collect();
822
823 branches_sort.sort_by_cached_key(|tup| {
824 (
825 std::cmp::max(tup.3, tup.4),
826 (tup.2 as i32 - tup.1 as i32) * length_sort_factor,
827 tup.1 as i32 * start_sort_factor,
828 )
829 });
830
831 for (branch_idx, start, end, _, _) in branches_sort {
832 let branch = &branches[branch_idx];
833 let group = branch.visual.order_group;
834 let group_occ = &mut occupied[group];
835
836 let align_right = branch
837 .source_branch
838 .map(|src| branches[src].visual.order_group > branch.visual.order_group)
839 .unwrap_or(false)
840 || branch
841 .target_branch
842 .map(|trg| branches[trg].visual.order_group > branch.visual.order_group)
843 .unwrap_or(false);
844
845 let len = group_occ.len();
846 let mut found = len;
847 for i in 0..len {
848 let index = if align_right { len - i - 1 } else { i };
849 let column_occ = &group_occ[index];
850 let mut occ = false;
851 for (s, e) in column_occ {
852 if start <= *e && end >= *s {
853 occ = true;
854 break;
855 }
856 }
857 if !occ {
858 if let Some(merge_trace) = branch
859 .merge_target
860 .and_then(|t| indices.get(&t))
861 .and_then(|t_idx| commits[*t_idx].branch_trace)
862 {
863 let merge_branch = &branches[merge_trace];
864 if merge_branch.visual.order_group == branch.visual.order_group {
865 if let Some(merge_column) = merge_branch.visual.column {
866 if merge_column == index {
867 occ = true;
868 }
869 }
870 }
871 }
872 }
873 if !occ {
874 found = index;
875 break;
876 }
877 }
878
879 let branch = &mut branches[branch_idx];
880 branch.visual.column = Some(found);
881 if found == group_occ.len() {
882 group_occ.push(vec![]);
883 }
884 group_occ[found].push((start, end));
885 }
886
887 let group_offset: Vec<usize> = occupied
888 .iter()
889 .scan(0, |acc, group| {
890 *acc += group.len();
891 Some(*acc)
892 })
893 .collect();
894
895 for branch in branches {
896 if let Some(column) = branch.visual.column {
897 let offset = if branch.visual.order_group == 0 {
898 0
899 } else {
900 group_offset[branch.visual.order_group - 1]
901 };
902 branch.visual.column = Some(column + offset);
903 }
904 }
905}
906
907fn branch_order(name: &str, order: &[Regex]) -> usize {
909 order
910 .iter()
911 .position(|b| (name.starts_with(ORIGIN) && b.is_match(&name[7..])) || b.is_match(name))
912 .unwrap_or(order.len())
913}
914
915fn branch_color<T: Clone>(
917 name: &str,
918 order: &[(Regex, Vec<T>)],
919 unknown: &[T],
920 counter: usize,
921) -> T {
922 let color = order
923 .iter()
924 .find_position(|(b, _)| {
925 (name.starts_with(ORIGIN) && b.is_match(&name[7..])) || b.is_match(name)
926 })
927 .map(|(_pos, col)| &col.1[counter % col.1.len()])
928 .unwrap_or_else(|| &unknown[counter % unknown.len()]);
929 color.clone()
930}
931
932pub fn parse_merge_summary(summary: &str, patterns: &MergePatterns) -> Option<String> {
934 for regex in &patterns.patterns {
935 if let Some(captures) = regex.captures(summary) {
936 if captures.len() == 2 && captures.get(1).is_some() {
937 return captures.get(1).map(|m| m.as_str().to_string());
938 }
939 }
940 }
941 None
942}
943
944#[cfg(test)]
945mod tests {
946 use crate::settings::MergePatterns;
947
948 #[test]
949 fn parse_merge_summary() {
950 let patterns = MergePatterns::default();
951
952 let gitlab_pull = "Merge branch 'feature/my-feature' into 'master'";
953 let git_default = "Merge branch 'feature/my-feature' into dev";
954 let git_master = "Merge branch 'feature/my-feature'";
955 let github_pull = "Merge pull request #1 from user-x/feature/my-feature";
956 let github_pull_2 = "Merge branch 'feature/my-feature' of github.com:user-x/repo";
957 let bitbucket_pull = "Merged in feature/my-feature (pull request #1)";
958
959 assert_eq!(
960 super::parse_merge_summary(gitlab_pull, &patterns),
961 Some("feature/my-feature".to_string()),
962 );
963 assert_eq!(
964 super::parse_merge_summary(git_default, &patterns),
965 Some("feature/my-feature".to_string()),
966 );
967 assert_eq!(
968 super::parse_merge_summary(git_master, &patterns),
969 Some("feature/my-feature".to_string()),
970 );
971 assert_eq!(
972 super::parse_merge_summary(github_pull, &patterns),
973 Some("feature/my-feature".to_string()),
974 );
975 assert_eq!(
976 super::parse_merge_summary(github_pull_2, &patterns),
977 Some("feature/my-feature".to_string()),
978 );
979 assert_eq!(
980 super::parse_merge_summary(bitbucket_pull, &patterns),
981 Some("feature/my-feature".to_string()),
982 );
983 }
984}