opensession_git_native/
store.rs1use std::path::Path;
2
3use gix::object::tree::EntryKind;
4use gix::ObjectId;
5use tracing::{debug, info};
6
7use crate::error::Result;
8use crate::ops::{self, gix_err};
9use crate::{SESSIONS_BRANCH, SESSIONS_REF};
10
11pub struct NativeGitStorage;
16
17impl NativeGitStorage {
18 fn session_prefix(session_id: &str) -> String {
21 let prefix = if session_id.len() >= 2 {
22 &session_id[..2]
23 } else {
24 session_id
25 };
26 format!("v1/{prefix}/{session_id}")
27 }
28}
29
30impl NativeGitStorage {
31 pub fn store(
38 &self,
39 repo_path: &Path,
40 session_id: &str,
41 hail_jsonl: &[u8],
42 meta_json: &[u8],
43 ) -> Result<String> {
44 let repo = ops::open_repo(repo_path)?;
45 let hash_kind = repo.object_hash();
46
47 let hail_blob = repo.write_blob(hail_jsonl).map_err(gix_err)?.detach();
49 let meta_blob = repo.write_blob(meta_json).map_err(gix_err)?.detach();
50
51 debug!(
52 session_id,
53 hail_blob = %hail_blob,
54 meta_blob = %meta_blob,
55 "Wrote session blobs"
56 );
57
58 let prefix = Self::session_prefix(session_id);
59 let hail_path = format!("{prefix}.hail.jsonl");
60 let meta_path = format!("{prefix}.meta.json");
61
62 let tip = ops::find_ref_tip(&repo, SESSIONS_BRANCH)?;
64 let base_tree_id = match &tip {
65 Some(commit_id) => ops::commit_tree_id(&repo, commit_id.detach())?,
66 None => ObjectId::empty_tree(hash_kind),
67 };
68
69 let mut editor = repo.edit_tree(base_tree_id).map_err(gix_err)?;
71 editor
72 .upsert(&hail_path, EntryKind::Blob, hail_blob)
73 .map_err(gix_err)?;
74 editor
75 .upsert(&meta_path, EntryKind::Blob, meta_blob)
76 .map_err(gix_err)?;
77
78 let new_tree_id = editor.write().map_err(gix_err)?.detach();
79
80 debug!(tree = %new_tree_id, "Built new tree");
81
82 let parent = tip.map(|id| id.detach());
83 let message = format!("session: {session_id}");
84 let commit_id = ops::create_commit(&repo, SESSIONS_REF, new_tree_id, parent, &message)?;
85
86 info!(
87 session_id,
88 commit = %commit_id,
89 "Stored session on {SESSIONS_BRANCH}"
90 );
91
92 Ok(hail_path)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::error::GitStorageError;
100 use crate::test_utils::init_test_repo;
101 use crate::SESSIONS_BRANCH;
102
103 #[test]
104 fn test_session_prefix() {
105 assert_eq!(
106 NativeGitStorage::session_prefix("abcdef-1234"),
107 "v1/ab/abcdef-1234"
108 );
109 assert_eq!(NativeGitStorage::session_prefix("x"), "v1/x/x");
110 assert_eq!(NativeGitStorage::session_prefix("ab"), "v1/ab/ab");
111 }
112
113 #[test]
114 fn test_store() {
115 let tmp = tempfile::tempdir().unwrap();
116 init_test_repo(tmp.path());
117
118 let storage = NativeGitStorage;
119 let hail = b"{\"event\":\"test\"}\n";
120 let meta = b"{\"title\":\"Test Session\"}";
121
122 let rel_path = storage
124 .store(tmp.path(), "abc123-def456", hail, meta)
125 .expect("store failed");
126 assert_eq!(rel_path, "v1/ab/abc123-def456.hail.jsonl");
127
128 let output = std::process::Command::new("git")
130 .args(["branch", "--list", SESSIONS_BRANCH])
131 .current_dir(tmp.path())
132 .output()
133 .unwrap();
134 let branches = String::from_utf8_lossy(&output.stdout);
135 assert!(
136 branches.contains("opensession/sessions"),
137 "branch not found: {branches}"
138 );
139 }
140
141 #[test]
142 fn test_not_a_repo() {
143 let tmp = tempfile::tempdir().unwrap();
144 let storage = NativeGitStorage;
146 let err = storage
147 .store(tmp.path(), "test", b"data", b"meta")
148 .unwrap_err();
149 assert!(
150 matches!(err, GitStorageError::NotARepo(_)),
151 "expected NotARepo, got: {err}"
152 );
153 }
154}