jj_hooks/
bookmark_updates.rs1use 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}