1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Branch {
12 pub name: String,
13 pub is_head: bool,
15 pub tip: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct CommitRow {
22 pub id: String,
24 pub summary: String,
26 pub author: String,
28 pub time: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct TreeEntry {
35 pub name: String,
36 pub is_dir: bool,
37 pub oid: String,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
43pub struct RepoSnapshot {
44 pub workdir: String,
46 pub branches: Vec<Branch>,
47 pub commits: Vec<CommitRow>,
48 pub tree: Vec<TreeEntry>,
50}
51
52impl RepoSnapshot {
53 #[must_use]
55 pub fn head_branch(&self) -> Option<&str> {
56 self.branches.iter().find(|b| b.is_head).map(|b| b.name.as_str())
57 }
58
59 #[must_use]
61 pub fn demo() -> Self {
62 RepoSnapshot {
63 workdir: "/repo".into(),
64 branches: vec![
65 Branch { name: "main".into(), is_head: true, tip: "a1b2c3d".into() },
66 Branch { name: "feature/x".into(), is_head: false, tip: "0f0f0f0".into() },
67 ],
68 commits: vec![
69 CommitRow { id: "a1b2c3d".into(), summary: "add the thing".into(), author: "rickard".into(), time: "2026-06-30T10:00:00Z".into() },
70 CommitRow { id: "9988776".into(), summary: "fix the other thing".into(), author: "ada".into(), time: "2026-06-29T09:00:00Z".into() },
71 ],
72 tree: vec![
73 TreeEntry { name: "src".into(), is_dir: true, oid: "tttt001".into() },
74 TreeEntry { name: "Cargo.toml".into(), is_dir: false, oid: "ffff002".into() },
75 TreeEntry { name: "README.md".into(), is_dir: false, oid: "ffff003".into() },
76 ],
77 }
78 }
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
83pub struct Blob {
84 pub path: String,
85 pub text: String,
86 pub binary: bool,
88}
89
90#[cfg(feature = "gix")]
92mod live {
93 use super::*;
94
95 fn short(id: &gix::ObjectId) -> String {
96 id.to_hex_with_len(7).to_string()
97 }
98
99 impl RepoSnapshot {
100 pub fn open(path: &str, max_commits: usize) -> Result<RepoSnapshot, String> {
105 let repo = gix::open(path).map_err(|e| format!("open {path}: {e}"))?;
106 let workdir = repo
107 .workdir()
108 .map(|p| p.display().to_string())
109 .unwrap_or_else(|| path.to_string());
110
111 let head_name = repo.head_name().ok().flatten().map(|n| n.shorten().to_string());
113 let mut branches = Vec::new();
114 if let Ok(refs) = repo.references() {
115 if let Ok(locals) = refs.local_branches() {
116 for r in locals.flatten() {
117 let name = r.name().shorten().to_string();
118 let tip = r.try_id().map(|id| short(&id.detach())).unwrap_or_default();
119 let is_head = head_name.as_deref() == Some(name.as_str());
120 branches.push(Branch { name, is_head, tip });
121 }
122 }
123 }
124 branches.sort_by(|a, b| b.is_head.cmp(&a.is_head).then(a.name.cmp(&b.name)));
125
126 let mut commits = Vec::new();
128 if let Ok(head_id) = repo.head_id() {
129 if let Ok(walk) = head_id.ancestors().all() {
130 for info in walk.take(max_commits).flatten() {
131 if let Ok(commit) = info.object() {
132 let summary = commit
133 .message()
134 .ok()
135 .map(|m| m.summary().to_string())
136 .unwrap_or_default();
137 let author = commit
138 .author()
139 .map(|a| a.name.to_string())
140 .unwrap_or_default();
141 let time = commit.time().map(|t| t.seconds.to_string()).unwrap_or_default();
142 commits.push(CommitRow { id: short(&info.id), summary, author, time });
143 }
144 }
145 }
146 }
147
148 let mut tree = Vec::new();
150 if let Ok(commit) = repo.head_commit() {
151 if let Ok(t) = commit.tree() {
152 for e in t.iter().flatten() {
153 let is_dir = e.mode().is_tree();
154 tree.push(TreeEntry {
155 name: e.filename().to_string(),
156 is_dir,
157 oid: short(&e.oid().to_owned()),
158 });
159 }
160 }
161 }
162 tree.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
163
164 Ok(RepoSnapshot { workdir, branches, commits, tree })
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
176 fn demo_snapshot_is_wellformed() {
177 let s = RepoSnapshot::demo();
178 assert_eq!(s.head_branch(), Some("main"));
179 assert_eq!(s.branches.len(), 2);
180 assert_eq!(s.commits.len(), 2);
181 assert!(s.tree.iter().any(|e| e.is_dir && e.name == "src"));
182 let j = serde_json::to_string(&s).unwrap();
184 let back: RepoSnapshot = serde_json::from_str(&j).unwrap();
185 assert_eq!(s, back);
186 }
187}