Skip to main content

verbs/
resolve_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure resolve planning: conflict marker detection and unresolved path sets.
3//!
4//! Marker detection here is **line-start** oriented (git-style conflict
5//! markers). That is intentionally stricter against false positives than
6//! [`crate::contains_conflict_marker_bytes`], which looks for full triplets
7//! anywhere in a file (refresh materialization).
8
9/// Whether content still has line-start conflict markers (`<<<<<<<`,
10/// `=======`, or `>>>>>>>`).
11///
12/// Used when marking a path resolved without `--ours`/`--theirs`/`--force`.
13pub fn contains_line_start_conflict_markers(content: &[u8]) -> bool {
14    content.split(|byte| *byte == b'\n').any(|line| {
15        line.starts_with(b"<<<<<<<") || line.starts_with(b"=======") || line.starts_with(b">>>>>>>")
16    })
17}
18
19/// Paths still unresolved: registered conflicts not yet marked resolved.
20pub fn unresolved_conflict_paths(conflicts: &[String], resolved: &[String]) -> Vec<String> {
21    conflicts
22        .iter()
23        .filter(|path| !resolved.iter().any(|r| r == *path))
24        .cloned()
25        .collect()
26}
27
28/// Whether a path is in the active conflict set.
29pub fn path_is_active_conflict(conflicts: &[String], path: &str) -> bool {
30    conflicts.iter().any(|c| c == path)
31}
32
33/// Side selection for resolve (CLI maps flags → this plan).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ResolveSideSelection {
36    /// Keep worktree content; only validate markers.
37    Worktree,
38    /// Take ours tree version.
39    Ours,
40    /// Take theirs tree version.
41    Theirs,
42}
43
44/// Plan resolve side selection from CLI flags.
45///
46/// `ours` and `theirs` together is invalid and returns [`None`] so CLI can
47/// surface its existing validation path (or treat as worktree).
48pub fn plan_resolve_side(ours: bool, theirs: bool) -> Option<ResolveSideSelection> {
49    match (ours, theirs) {
50        (true, true) => None,
51        (true, false) => Some(ResolveSideSelection::Ours),
52        (false, true) => Some(ResolveSideSelection::Theirs),
53        (false, false) => Some(ResolveSideSelection::Worktree),
54    }
55}
56
57/// Whether marker validation is required before marking resolved.
58pub fn resolve_requires_marker_check(side: ResolveSideSelection, force: bool) -> bool {
59    matches!(side, ResolveSideSelection::Worktree) && !force
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn line_start_markers_detect_git_style_lines() {
68        assert!(contains_line_start_conflict_markers(
69            b"keep\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> branch\n"
70        ));
71        assert!(contains_line_start_conflict_markers(b"=======\n"));
72        assert!(!contains_line_start_conflict_markers(b"no markers here\n"));
73        // Not at line start — line-start detector ignores mid-line noise.
74        assert!(!contains_line_start_conflict_markers(b"x<<<<<<<\n"));
75    }
76
77    #[test]
78    fn unresolved_paths_filter_resolved() {
79        let conflicts = vec!["a.rs".into(), "b.rs".into(), "c.rs".into()];
80        let resolved = vec!["b.rs".into()];
81        assert_eq!(
82            unresolved_conflict_paths(&conflicts, &resolved),
83            vec!["a.rs".to_string(), "c.rs".to_string()]
84        );
85        assert!(path_is_active_conflict(&conflicts, "a.rs"));
86        assert!(!path_is_active_conflict(&conflicts, "z.rs"));
87    }
88
89    #[test]
90    fn plan_resolve_side_and_marker_gate() {
91        assert_eq!(
92            plan_resolve_side(false, false),
93            Some(ResolveSideSelection::Worktree)
94        );
95        assert_eq!(
96            plan_resolve_side(true, false),
97            Some(ResolveSideSelection::Ours)
98        );
99        assert_eq!(
100            plan_resolve_side(false, true),
101            Some(ResolveSideSelection::Theirs)
102        );
103        assert_eq!(plan_resolve_side(true, true), None);
104        assert!(resolve_requires_marker_check(
105            ResolveSideSelection::Worktree,
106            false
107        ));
108        assert!(!resolve_requires_marker_check(
109            ResolveSideSelection::Worktree,
110            true
111        ));
112        assert!(!resolve_requires_marker_check(
113            ResolveSideSelection::Ours,
114            false
115        ));
116    }
117}