Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:31.414Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

stash.rs

224 lines · 7.1 KB · rust
1use crate::Oid;
2use anyhow::{Context, Result, anyhow};
3use std::{str::FromStr, sync::Arc};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
6pub struct StashEntry {
7    pub index: usize,
8    pub oid: Oid,
9    pub message: String,
10    pub branch: Option<String>,
11    pub timestamp: i64,
12}
13
14#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
15pub struct GitStash {
16    pub entries: Arc<[StashEntry]>,
17}
18
19impl GitStash {
20    pub fn apply(&mut self, other: GitStash) {
21        self.entries = other.entries;
22    }
23}
24
25impl FromStr for GitStash {
26    type Err = anyhow::Error;
27
28    fn from_str(s: &str) -> Result<Self> {
29        if s.trim().is_empty() {
30            return Ok(Self::default());
31        }
32
33        let mut entries = Vec::new();
34        let mut errors = Vec::new();
35
36        for (line_num, line) in s.lines().enumerate() {
37            if line.trim().is_empty() {
38                continue;
39            }
40
41            match parse_stash_line(line) {
42                Ok(entry) => entries.push(entry),
43                Err(e) => {
44                    errors.push(format!("Line {}: {}", line_num + 1, e));
45                }
46            }
47        }
48
49        // If we have some valid entries but also some errors, log the errors but continue
50        if !errors.is_empty() && !entries.is_empty() {
51            log::warn!("Failed to parse some stash entries: {}", errors.join(", "));
52        } else if !errors.is_empty() {
53            return Err(anyhow!(
54                "Failed to parse stash entries: {}",
55                errors.join(", ")
56            ));
57        }
58
59        Ok(Self {
60            entries: entries.into(),
61        })
62    }
63}
64
65/// Parse a single stash line in the format: "stash@{N}\0<oid>\0<timestamp>\0<message>"
66fn parse_stash_line(line: &str) -> Result<StashEntry> {
67    let parts: Vec<&str> = line.splitn(4, '\0').collect();
68
69    if parts.len() != 4 {
70        return Err(anyhow!(
71            "Expected 4 null-separated parts, got {}",
72            parts.len()
73        ));
74    }
75
76    let index = parse_stash_index(parts[0])
77        .with_context(|| format!("Failed to parse stash index from '{}'", parts[0]))?;
78
79    let oid = Oid::from_str(parts[1])
80        .with_context(|| format!("Failed to parse OID from '{}'", parts[1]))?;
81
82    let timestamp = parts[2]
83        .parse::<i64>()
84        .with_context(|| format!("Failed to parse timestamp from '{}'", parts[2]))?;
85
86    let (branch, message) = parse_stash_message(parts[3]);
87
88    Ok(StashEntry {
89        index,
90        oid,
91        message: message.to_string(),
92        branch: branch.map(Into::into),
93        timestamp,
94    })
95}
96
97/// Parse stash index from format "stash@{N}" where N is the index
98fn parse_stash_index(input: &str) -> Result<usize> {
99    let trimmed = input.trim();
100
101    if !trimmed.starts_with("stash@{") || !trimmed.ends_with('}') {
102        return Err(anyhow!(
103            "Invalid stash index format: expected 'stash@{{N}}'"
104        ));
105    }
106
107    let index_str = trimmed
108        .strip_prefix("stash@{")
109        .and_then(|s| s.strip_suffix('}'))
110        .ok_or_else(|| anyhow!("Failed to extract index from stash reference"))?;
111
112    index_str
113        .parse::<usize>()
114        .with_context(|| format!("Invalid stash index number: '{}'", index_str))
115}
116
117/// Parse stash message and extract branch information if present
118///
119/// Handles the following formats:
120/// - "WIP on <branch>: <message>" -> (Some(branch), message)
121/// - "On <branch>: <message>" -> (Some(branch), message)
122/// - "<message>" -> (None, message)
123fn parse_stash_message(input: &str) -> (Option<&str>, &str) {
124    // Handle "WIP on <branch>: <message>" pattern
125    if let Some(stripped) = input.strip_prefix("WIP on ")
126        && let Some(colon_pos) = stripped.find(": ")
127    {
128        let branch = &stripped[..colon_pos];
129        let message = &stripped[colon_pos + 2..];
130        if !branch.is_empty() && !message.is_empty() {
131            return (Some(branch), message);
132        }
133    }
134
135    // Handle "On <branch>: <message>" pattern
136    if let Some(stripped) = input.strip_prefix("On ")
137        && let Some(colon_pos) = stripped.find(": ")
138    {
139        let branch = &stripped[..colon_pos];
140        let message = &stripped[colon_pos + 2..];
141        if !branch.is_empty() && !message.is_empty() {
142            return (Some(branch), message);
143        }
144    }
145
146    // Fallback: treat entire input as message with no branch
147    (None, input)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_parse_stash_index() {
156        assert_eq!(parse_stash_index("stash@{0}").unwrap(), 0);
157        assert_eq!(parse_stash_index("stash@{42}").unwrap(), 42);
158        assert_eq!(parse_stash_index("  stash@{5}  ").unwrap(), 5);
159
160        assert!(parse_stash_index("invalid").is_err());
161        assert!(parse_stash_index("stash@{not_a_number}").is_err());
162        assert!(parse_stash_index("stash@{0").is_err());
163    }
164
165    #[test]
166    fn test_parse_stash_message() {
167        // WIP format
168        let (branch, message) = parse_stash_message("WIP on main: working on feature");
169        assert_eq!(branch, Some("main"));
170        assert_eq!(message, "working on feature");
171
172        // On format
173        let (branch, message) = parse_stash_message("On feature-branch: some changes");
174        assert_eq!(branch, Some("feature-branch"));
175        assert_eq!(message, "some changes");
176
177        // No branch format
178        let (branch, message) = parse_stash_message("just a regular message");
179        assert_eq!(branch, None);
180        assert_eq!(message, "just a regular message");
181
182        // Edge cases
183        let (branch, message) = parse_stash_message("WIP on : empty message");
184        assert_eq!(branch, None);
185        assert_eq!(message, "WIP on : empty message");
186
187        let (branch, message) = parse_stash_message("On branch-name:");
188        assert_eq!(branch, None);
189        assert_eq!(message, "On branch-name:");
190    }
191
192    #[test]
193    fn test_parse_stash_line() {
194        let line = "stash@{0}\u{0000}abc123\u{0000}1234567890\u{0000}WIP on main: test commit";
195        let entry = parse_stash_line(line).unwrap();
196
197        assert_eq!(entry.index, 0);
198        assert_eq!(entry.message, "test commit");
199        assert_eq!(entry.branch, Some("main".to_string()));
200        assert_eq!(entry.timestamp, 1234567890);
201    }
202
203    #[test]
204    fn test_git_stash_from_str() {
205        let input = "stash@{0}\u{0000}abc123\u{0000}1234567890\u{0000}WIP on main: first stash\nstash@{1}\u{0000}def456\u{0000}1234567891\u{0000}On feature: second stash";
206        let stash = GitStash::from_str(input).unwrap();
207
208        assert_eq!(stash.entries.len(), 2);
209        assert_eq!(stash.entries[0].index, 0);
210        assert_eq!(stash.entries[0].branch, Some("main".to_string()));
211        assert_eq!(stash.entries[1].index, 1);
212        assert_eq!(stash.entries[1].branch, Some("feature".to_string()));
213    }
214
215    #[test]
216    fn test_git_stash_empty_input() {
217        let stash = GitStash::from_str("").unwrap();
218        assert_eq!(stash.entries.len(), 0);
219
220        let stash = GitStash::from_str("   \n  \n  ").unwrap();
221        assert_eq!(stash.entries.len(), 0);
222    }
223}
224
Served at tenant.openagents/omega Member data and write actions are omitted.