Skip to main content

jj_hooks/
bookmark_updates.rs

1use std::collections::HashSet;
2use std::sync::OnceLock;
3
4use regex::Regex;
5
6use crate::error::{JjHooksError, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum UpdateType {
10    MoveForward,
11    MoveBackward,
12    MoveSideways,
13    Add,
14    Delete,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct BookmarkUpdate {
19    pub remote: String,
20    pub bookmark: String,
21    pub update_type: UpdateType,
22    pub old_commit: Option<String>,
23    pub new_commit: Option<String>,
24}
25
26impl std::fmt::Display for BookmarkUpdate {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        let verb = match self.update_type {
29            UpdateType::MoveForward => "Move forward",
30            UpdateType::MoveBackward => "Move backward",
31            UpdateType::MoveSideways => "Move sideways",
32            UpdateType::Add => "Add",
33            UpdateType::Delete => "Delete",
34        };
35        write!(f, "{verb} {}", self.bookmark)?;
36        if let Some(old) = &self.old_commit {
37            write!(f, " from {}", &old[..old.len().min(8)])?;
38        }
39        if let Some(new) = &self.new_commit {
40            write!(f, " to {}", &new[..new.len().min(8)])?;
41        }
42        Ok(())
43    }
44}
45
46struct PatternSet {
47    update_type: UpdateType,
48    patterns: Vec<Regex>,
49}
50
51fn patterns() -> &'static [PatternSet] {
52    static PATTERNS: OnceLock<Vec<PatternSet>> = OnceLock::new();
53    PATTERNS.get_or_init(|| {
54        vec![
55            PatternSet {
56                update_type: UpdateType::MoveForward,
57                patterns: vec![
58                    Regex::new(
59                        r"Move forward bookmark (?P<bookmark>\S+) from (?P<old_commit>\w+) to (?P<new_commit>\w+)",
60                    )
61                    .unwrap(),
62                    Regex::new(
63                        r"^\s*bookmark:\s+(?P<bookmark>\S+)\s+\[move forward from (?P<old_commit>\w+) to (?P<new_commit>\w+)\]",
64                    )
65                    .unwrap(),
66                ],
67            },
68            PatternSet {
69                update_type: UpdateType::MoveBackward,
70                patterns: vec![
71                    Regex::new(
72                        r"Move backward bookmark (?P<bookmark>\S+) from (?P<old_commit>\w+) to (?P<new_commit>\w+)",
73                    )
74                    .unwrap(),
75                    Regex::new(
76                        r"^\s*bookmark:\s+(?P<bookmark>\S+)\s+\[move backward from (?P<old_commit>\w+) to (?P<new_commit>\w+)\]",
77                    )
78                    .unwrap(),
79                ],
80            },
81            PatternSet {
82                update_type: UpdateType::MoveSideways,
83                patterns: vec![
84                    Regex::new(
85                        r"Move sideways bookmark (?P<bookmark>\S+) from (?P<old_commit>\w+) to (?P<new_commit>\w+)",
86                    )
87                    .unwrap(),
88                    Regex::new(
89                        r"^\s*bookmark:\s+(?P<bookmark>\S+)\s+\[move sideways from (?P<old_commit>\w+) to (?P<new_commit>\w+)\]",
90                    )
91                    .unwrap(),
92                ],
93            },
94            PatternSet {
95                update_type: UpdateType::Add,
96                patterns: vec![
97                    Regex::new(r"Add bookmark (?P<bookmark>\S+) to (?P<new_commit>\w+)").unwrap(),
98                    Regex::new(
99                        r"^\s*bookmark:\s+(?P<bookmark>\S+)\s+\[add to (?P<new_commit>\w+)\]",
100                    )
101                    .unwrap(),
102                ],
103            },
104            PatternSet {
105                update_type: UpdateType::Delete,
106                patterns: vec![
107                    Regex::new(r"Delete bookmark (?P<bookmark>\S+) from (?P<old_commit>\w+)")
108                        .unwrap(),
109                    Regex::new(
110                        r"^\s*bookmark:\s+(?P<bookmark>\S+)\s+\[delete from (?P<old_commit>\w+)\]",
111                    )
112                    .unwrap(),
113                ],
114            },
115        ]
116    })
117}
118
119fn remote_pattern() -> &'static Regex {
120    static PAT: OnceLock<Regex> = OnceLock::new();
121    PAT.get_or_init(|| Regex::new(r"^Changes to push to (.+?):").unwrap())
122}
123
124pub fn parse_git_push_dry_run(output: &str) -> Result<HashSet<BookmarkUpdate>> {
125    let mut updates = HashSet::new();
126    let mut remote: Option<String> = None;
127
128    for line in output.lines() {
129        if let Some(caps) = remote_pattern().captures(line) {
130            remote = Some(caps.get(1).unwrap().as_str().to_owned());
131            continue;
132        }
133
134        for set in patterns() {
135            for pat in &set.patterns {
136                let Some(caps) = pat.captures(line) else {
137                    continue;
138                };
139                let Some(remote) = remote.as_ref() else {
140                    return Err(JjHooksError::Parse(
141                        "saw a bookmark line before any `Changes to push to <remote>:` line".into(),
142                    ));
143                };
144                let bookmark = caps.name("bookmark").unwrap().as_str().to_owned();
145                let old_commit = caps.name("old_commit").map(|m| m.as_str().to_owned());
146                let new_commit = caps.name("new_commit").map(|m| m.as_str().to_owned());
147                updates.insert(BookmarkUpdate {
148                    remote: remote.clone(),
149                    bookmark,
150                    update_type: set.update_type,
151                    old_commit,
152                    new_commit,
153                });
154            }
155        }
156    }
157
158    Ok(updates)
159}