agent_code_lib/services/
git_ops.rs1use regex::Regex;
8use std::sync::LazyLock;
9
10#[derive(Debug, Clone)]
12pub struct GitOperation {
13 pub kind: GitOpKind,
14 pub detail: String,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum GitOpKind {
20 Commit,
21 Push,
22 PrCreate,
23 PrMerge,
24 BranchCreate,
25 BranchSwitch,
26 Merge,
27 Rebase,
28 Stash,
29 Tag,
30}
31
32static COMMIT_RE: LazyLock<Regex> =
33 LazyLock::new(|| Regex::new(r"\[(\S+)\s+([a-f0-9]{7,40})\]").unwrap());
34
35static PUSH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:To\s+\S+|->)\s+(\S+)").unwrap());
36
37static PR_CREATE_RE: LazyLock<Regex> =
38 LazyLock::new(|| Regex::new(r"https://github\.com/[^\s]+/pull/(\d+)").unwrap());
39
40static GH_PR_CREATE_CMD: LazyLock<Regex> =
41 LazyLock::new(|| Regex::new(r"gh\s+pr\s+create").unwrap());
42
43static GH_PR_MERGE_CMD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"gh\s+pr\s+merge").unwrap());
44
45static BRANCH_CREATE_RE: LazyLock<Regex> =
46 LazyLock::new(|| Regex::new(r"git\s+(?:checkout\s+-b|switch\s+-c)\s+(\S+)").unwrap());
47
48static BRANCH_SWITCH_RE: LazyLock<Regex> =
49 LazyLock::new(|| Regex::new(r"Switched to (?:a new )?branch '(\S+)'").unwrap());
50
51pub fn detect_git_ops(command: &str, output: &str) -> Vec<GitOperation> {
53 let mut ops = Vec::new();
54
55 if let Some(cap) = COMMIT_RE.captures(output) {
57 ops.push(GitOperation {
58 kind: GitOpKind::Commit,
59 detail: cap
60 .get(2)
61 .map(|m| m.as_str().to_string())
62 .unwrap_or_default(),
63 });
64 }
65
66 if command.contains("git push")
68 && let Some(cap) = PUSH_RE.captures(output)
69 {
70 ops.push(GitOperation {
71 kind: GitOpKind::Push,
72 detail: cap
73 .get(1)
74 .map(|m| m.as_str().to_string())
75 .unwrap_or_default(),
76 });
77 }
78
79 if GH_PR_CREATE_CMD.is_match(command) {
81 let url = PR_CREATE_RE
82 .find(output)
83 .map(|m| m.as_str().to_string())
84 .unwrap_or_default();
85 ops.push(GitOperation {
86 kind: GitOpKind::PrCreate,
87 detail: url,
88 });
89 }
90
91 if GH_PR_MERGE_CMD.is_match(command) {
93 ops.push(GitOperation {
94 kind: GitOpKind::PrMerge,
95 detail: String::new(),
96 });
97 }
98
99 if let Some(cap) = BRANCH_CREATE_RE.captures(command) {
101 ops.push(GitOperation {
102 kind: GitOpKind::BranchCreate,
103 detail: cap
104 .get(1)
105 .map(|m| m.as_str().to_string())
106 .unwrap_or_default(),
107 });
108 }
109
110 if let Some(cap) = BRANCH_SWITCH_RE.captures(output) {
112 ops.push(GitOperation {
113 kind: GitOpKind::BranchSwitch,
114 detail: cap
115 .get(1)
116 .map(|m| m.as_str().to_string())
117 .unwrap_or_default(),
118 });
119 }
120
121 if command.contains("git merge") {
123 ops.push(GitOperation {
124 kind: GitOpKind::Merge,
125 detail: String::new(),
126 });
127 }
128
129 if command.contains("git rebase") {
131 ops.push(GitOperation {
132 kind: GitOpKind::Rebase,
133 detail: String::new(),
134 });
135 }
136
137 if command.contains("git stash") {
139 ops.push(GitOperation {
140 kind: GitOpKind::Stash,
141 detail: String::new(),
142 });
143 }
144
145 if command.contains("git tag") {
147 ops.push(GitOperation {
148 kind: GitOpKind::Tag,
149 detail: String::new(),
150 });
151 }
152
153 ops
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_detect_commit() {
162 let ops = detect_git_ops(
163 "git commit -m 'test'",
164 "[main abc1234] test commit\n 1 file changed",
165 );
166 assert_eq!(ops.len(), 1);
167 assert_eq!(ops[0].kind, GitOpKind::Commit);
168 assert_eq!(ops[0].detail, "abc1234");
169 }
170
171 #[test]
172 fn test_detect_pr_create() {
173 let ops = detect_git_ops(
174 "gh pr create --title 'fix'",
175 "https://github.com/owner/repo/pull/42",
176 );
177 assert_eq!(ops.len(), 1);
178 assert_eq!(ops[0].kind, GitOpKind::PrCreate);
179 assert!(ops[0].detail.contains("pull/42"));
180 }
181
182 #[test]
183 fn test_detect_branch_switch() {
184 let ops = detect_git_ops(
185 "git checkout -b feature",
186 "Switched to a new branch 'feature'",
187 );
188 assert!(ops.iter().any(|o| o.kind == GitOpKind::BranchCreate));
189 assert!(ops.iter().any(|o| o.kind == GitOpKind::BranchSwitch));
190 }
191
192 #[test]
193 fn test_detect_push() {
194 let ops = detect_git_ops(
195 "git push origin main",
196 "To github.com:owner/repo.git\n abc123..def456 main -> main",
197 );
198 assert!(ops.iter().any(|o| o.kind == GitOpKind::Push));
199 }
200
201 #[test]
202 fn test_no_false_positives() {
203 let ops = detect_git_ops("ls -la", "total 42\ndrwxr-xr-x");
204 assert!(ops.is_empty());
205 }
206}