git_disjoint/
pre_validation.rs1use std::fmt::Write;
2
3use git2::Commit;
4
5use crate::branch_name::BranchName;
6use crate::disjoint_branch::DisjointBranchMap;
7
8#[derive(Debug)]
9pub struct BranchConflict {
10 pub branch_name: BranchName,
11 pub commit_summary: String,
12 pub conflicting_paths: Vec<String>,
13}
14
15#[derive(Debug)]
16pub struct PreValidationReport {
17 pub conflicts: Vec<BranchConflict>,
18}
19
20impl PreValidationReport {
21 pub fn render(&self, _use_color: bool) -> String {
22 let mut output = String::new();
23 for (i, conflict) in self.conflicts.iter().enumerate() {
24 if i > 0 {
25 writeln!(output).unwrap();
26 }
27 writeln!(
28 output,
29 "error: cherry-pick would fail for branch `{}`",
30 conflict.branch_name
31 )
32 .unwrap();
33 writeln!(output, " --> commit \"{}\"", conflict.commit_summary).unwrap();
34 writeln!(output, " |").unwrap();
35 for path in &conflict.conflicting_paths {
36 writeln!(output, " = conflict in {}", path).unwrap();
37 }
38 writeln!(output, " |").unwrap();
39 writeln!(
40 output,
41 " = help: these commits have overlapping changes and cannot be split"
42 )
43 .unwrap();
44 writeln!(
45 output,
46 " into separate branches from the same base"
47 )
48 .unwrap();
49 writeln!(
50 output,
51 " = help: consider assigning them to the same issue, or use `--overlay`"
52 )
53 .unwrap();
54 writeln!(output, " to combine them into a single PR").unwrap();
55 }
56 output
57 }
58}
59
60pub fn validate<'repo>(
61 branch_map: &DisjointBranchMap<'repo>,
62 base_commit: &Commit<'repo>,
63 repo: &git2::Repository,
64) -> Result<(), PreValidationReport> {
65 let mut conflicts = Vec::new();
66
67 for (_issue_group, branch) in branch_map.iter() {
68 let mut simulated_head = base_commit.clone();
69
70 for commit in &branch.commits {
71 let mut index = repo
72 .cherrypick_commit(commit, &simulated_head, 0, None)
73 .map_err(|_| PreValidationReport {
74 conflicts: vec![BranchConflict {
75 branch_name: branch.branch_name.clone(),
76 commit_summary: commit.summary().unwrap_or("").to_string(),
77 conflicting_paths: vec!["(git2 error)".to_string()],
78 }],
79 })?;
80
81 if index.has_conflicts() {
82 let conflicting_paths: Vec<String> = index
83 .conflicts()
84 .ok()
85 .into_iter()
86 .flatten()
87 .filter_map(|conflict| {
88 let conflict = conflict.ok()?;
89 conflict
90 .our
91 .or(conflict.their)
92 .or(conflict.ancestor)
93 .map(|entry| String::from_utf8_lossy(&entry.path).to_string())
94 })
95 .collect();
96
97 conflicts.push(BranchConflict {
98 branch_name: branch.branch_name.clone(),
99 commit_summary: commit.summary().unwrap_or("").to_string(),
100 conflicting_paths,
101 });
102 break;
104 } else {
105 let tree_oid = index.write_tree_to(repo).map_err(|_| PreValidationReport {
107 conflicts: vec![BranchConflict {
108 branch_name: branch.branch_name.clone(),
109 commit_summary: commit.summary().unwrap_or("").to_string(),
110 conflicting_paths: vec!["(write_tree error)".to_string()],
111 }],
112 })?;
113 let tree = repo.find_tree(tree_oid).map_err(|_| PreValidationReport {
114 conflicts: vec![BranchConflict {
115 branch_name: branch.branch_name.clone(),
116 commit_summary: commit.summary().unwrap_or("").to_string(),
117 conflicting_paths: vec!["(find_tree error)".to_string()],
118 }],
119 })?;
120 let sig = commit.author();
121 simulated_head = repo
122 .find_commit(
123 repo.commit(None, &sig, &sig, "simulated", &tree, &[&simulated_head])
124 .map_err(|_| PreValidationReport {
125 conflicts: vec![BranchConflict {
126 branch_name: branch.branch_name.clone(),
127 commit_summary: commit.summary().unwrap_or("").to_string(),
128 conflicting_paths: vec!["(commit error)".to_string()],
129 }],
130 })?,
131 )
132 .map_err(|_| PreValidationReport {
133 conflicts: vec![BranchConflict {
134 branch_name: branch.branch_name.clone(),
135 commit_summary: commit.summary().unwrap_or("").to_string(),
136 conflicting_paths: vec!["(find_commit error)".to_string()],
137 }],
138 })?;
139 }
140 }
141 }
142
143 if conflicts.is_empty() {
144 Ok(())
145 } else {
146 Err(PreValidationReport { conflicts })
147 }
148}