Skip to main content

git_disjoint/
pre_validation.rs

1use 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                // Stop simulating this branch after first conflict
103                break;
104            } else {
105                // Advance simulated head
106                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}