git_meta_lib/session.rs
1use std::path::PathBuf;
2
3use time::OffsetDateTime;
4
5/// A session combining a Git repository with its git-meta metadata store.
6///
7/// This is the primary entry point for git-meta consumers. It owns the
8/// `gix::Repository`, the SQLite [`Store`](crate::db::Store), and resolved
9/// configuration values (namespace, user email).
10///
11/// # Timestamps
12///
13/// By default, workflow operations use the wall clock for timestamps.
14/// For deterministic tests, call [`with_timestamp()`](Self::with_timestamp)
15/// to pin all operations to a fixed time:
16///
17/// ```ignore
18/// let session = Session::discover()?.with_timestamp(1_700_000_000_000);
19/// session.serialize()?; // uses the fixed timestamp
20/// ```
21///
22/// # Example
23///
24/// ```no_run
25/// use git_meta_lib::Session;
26///
27/// let session = Session::discover()?;
28/// println!("email: {}", session.email());
29/// println!("namespace: {}", session.namespace());
30/// # Ok::<(), git_meta_lib::Error>(())
31/// ```
32#[derive(Debug)]
33#[must_use]
34pub struct Session {
35 pub(crate) repo: gix::Repository,
36 pub(crate) store: crate::db::Store,
37 pub(crate) namespace: String,
38 pub(crate) email: String,
39 pub(crate) name: String,
40 pub(crate) timestamp_override: Option<i64>,
41}
42
43impl Session {
44 /// Discover a git repository from the current directory and open its
45 /// metadata store.
46 ///
47 /// Walks upward from the current directory to find a `.git` directory,
48 /// reads `user.email` and `meta.namespace` from git config, and opens
49 /// (or creates) the SQLite database at `.git/git-meta.sqlite`.
50 pub fn discover() -> crate::error::Result<Self> {
51 let repo = crate::git_utils::discover_repo()?;
52 Self::from_repo(repo)
53 }
54
55 /// Open a session for a known repository.
56 ///
57 /// # Example
58 ///
59 /// ```ignore
60 /// let repo = gix::open(".")?;
61 /// let session = Session::open(repo.path())?;
62 /// ```
63 pub fn open(directory: impl Into<PathBuf>) -> crate::error::Result<Self> {
64 Self::from_repo(gix::open(directory).map_err(|err| crate::error::Error::Git(err.into()))?)
65 }
66
67 /// Pin all workflow operations to a fixed timestamp.
68 ///
69 /// The value is milliseconds since the Unix epoch. When set,
70 /// [`now()`](Self::now) returns this value instead of the wall clock.
71 /// Useful for deterministic tests and replay scenarios.
72 pub fn with_timestamp(mut self, timestamp_ms: i64) -> Self {
73 self.timestamp_override = Some(timestamp_ms);
74 self
75 }
76
77 /// The current timestamp in milliseconds since the Unix epoch.
78 ///
79 /// Returns the fixed timestamp if [`with_timestamp()`](Self::with_timestamp)
80 /// was called, otherwise the wall clock.
81 pub(crate) fn now(&self) -> i64 {
82 self.timestamp_override
83 .unwrap_or_else(|| OffsetDateTime::now_utc().unix_timestamp_nanos() as i64 / 1_000_000)
84 }
85
86 fn from_repo(repo: gix::Repository) -> crate::error::Result<Self> {
87 let db_path = crate::git_utils::db_path(&repo)?;
88 let email = crate::git_utils::get_email(&repo)?;
89 let name = crate::git_utils::get_name(&repo)?;
90 let namespace = crate::git_utils::get_namespace(&repo)?;
91 let store = crate::db::Store::open_with_repo(&db_path, repo.clone())?;
92
93 Ok(Self {
94 repo,
95 store,
96 namespace,
97 email,
98 name,
99 timestamp_override: None,
100 })
101 }
102
103 /// Access the metadata store directly.
104 ///
105 /// This is an advanced API for custom queries. Most consumers should use
106 /// [`target()`](Self::target) for read/write operations.
107 #[cfg(feature = "internal")]
108 pub fn store(&self) -> &crate::db::Store {
109 &self.store
110 }
111
112 /// Access the underlying gix repository.
113 ///
114 /// This is an advanced API. Most consumers should use Session's workflow
115 /// methods (serialize, materialize, pull, push) instead.
116 #[cfg(feature = "internal")]
117 pub fn repo(&self) -> &gix::Repository {
118 &self.repo
119 }
120
121 /// The metadata namespace (from git config `meta.namespace`, default `"meta"`).
122 ///
123 /// Used to construct ref paths like `refs/{namespace}/local/main`.
124 pub fn namespace(&self) -> &str {
125 &self.namespace
126 }
127
128 /// The user email from git config `user.email`.
129 ///
130 /// Used for authorship tracking on metadata mutations.
131 pub fn email(&self) -> &str {
132 &self.email
133 }
134
135 /// The user name from git config `user.name`.
136 ///
137 /// Used for commit signatures during serialization.
138 pub fn name(&self) -> &str {
139 &self.name
140 }
141
142 /// The local serialization ref path (e.g. `refs/meta/local/main`).
143 pub(crate) fn local_ref(&self) -> String {
144 format!("refs/{}/local/main", self.namespace)
145 }
146
147 /// A ref path for a named destination (e.g. `refs/meta/local/{destination}`).
148 pub(crate) fn destination_ref(&self, destination: &str) -> String {
149 format!("refs/{}/local/{}", self.namespace, destination)
150 }
151
152 /// Create a scoped handle for operations on a specific target.
153 ///
154 /// The handle carries the session's email and timestamp, so write
155 /// operations don't need them as parameters:
156 ///
157 /// ```ignore
158 /// let handle = session.target(&Target::parse("commit:abc123")?);
159 /// handle.set_value("key", &MetaValue::String("value".into()))?;
160 /// ```
161 pub fn target(
162 &self,
163 target: &crate::types::Target,
164 ) -> crate::session_handle::SessionTargetHandle<'_> {
165 crate::session_handle::SessionTargetHandle::new(self, target.clone())
166 }
167
168 /// Remove all local metadata keys matching a key or key namespace.
169 ///
170 /// `key_prefix` matches either an exact key or keys below it in the
171 /// colon-separated namespace. For example, `agent` clears both `agent` and
172 /// `agent:model` across every target.
173 ///
174 /// Returns the number of metadata entries removed.
175 pub fn clear_key_prefix(&self, key_prefix: &str) -> crate::error::Result<usize> {
176 self.store
177 .clear_key_prefix(key_prefix, self.email(), self.now())
178 }
179
180 /// Resolve a target's partial commit SHA using this session's repository.
181 ///
182 /// Returns a new target with the full SHA if the target was a partial commit,
183 /// or a clone of the original target otherwise.
184 pub fn resolve_target(
185 &self,
186 target: &crate::types::Target,
187 ) -> crate::error::Result<crate::types::Target> {
188 target.resolve(&self.repo)
189 }
190
191 /// Resolve which metadata remote to use.
192 ///
193 /// If `remote` is `Some`, validates that it is a configured meta remote.
194 /// If `None`, returns the first configured meta remote.
195 ///
196 /// # Parameters
197 ///
198 /// - `remote`: optional remote name to validate; if `None`, the first
199 /// configured metadata remote is returned
200 ///
201 /// # Returns
202 ///
203 /// The name of the resolved meta remote.
204 ///
205 /// # Errors
206 ///
207 /// Returns [`Error::NoRemotes`](crate::error::Error::NoRemotes) if no
208 /// meta remotes are configured, or
209 /// [`Error::RemoteNotFound`](crate::error::Error::RemoteNotFound) if the
210 /// specified name is not a meta remote.
211 pub fn resolve_remote(&self, remote: Option<&str>) -> crate::error::Result<String> {
212 crate::git_utils::resolve_meta_remote(&self.repo, remote)
213 }
214
215 /// Index metadata keys from commit history for blobless clone support.
216 ///
217 /// Walks commits from `tip_oid` backward (optionally stopping at `old_tip`)
218 /// and inserts promisor entries for all keys found in commit messages or
219 /// root-commit trees. Returns the number of new entries indexed.
220 ///
221 /// Call this after a blobless fetch to build an index of historical keys
222 /// that can be hydrated on demand.
223 pub(crate) fn index_history(
224 &self,
225 tip_oid: gix::ObjectId,
226 old_tip: Option<gix::ObjectId>,
227 ) -> crate::error::Result<usize> {
228 crate::sync::insert_promisor_entries(&self.repo, &self.store, tip_oid, old_tip)
229 }
230
231 /// Serialize local metadata to Git tree(s) and commit(s).
232 ///
233 /// Determines incremental vs full mode automatically. Applies filter
234 /// routing and pruning rules. Updates local refs and the materialization
235 /// timestamp.
236 pub fn serialize(&self) -> crate::error::Result<crate::serialize::SerializeOutput> {
237 crate::serialize::run(self, self.now(), false)
238 }
239
240 /// Serialize local metadata and report progress through a callback.
241 ///
242 /// # Parameters
243 ///
244 /// - `progress`: callback invoked at major serialization steps.
245 pub fn serialize_with_progress(
246 &self,
247 progress: impl FnMut(crate::serialize::SerializeProgress),
248 ) -> crate::error::Result<crate::serialize::SerializeOutput> {
249 crate::serialize::run_with_progress(self, self.now(), false, progress)
250 }
251
252 /// Serialize local metadata by rebuilding from the complete SQLite state.
253 ///
254 /// This bypasses incremental dirty-target detection while still avoiding a
255 /// new commit when the rebuilt tree is identical to the current serialized
256 /// ref. Applies filter routing and pruning rules. Updates local refs and
257 /// the materialization timestamp when serialization succeeds.
258 pub fn serialize_full(&self) -> crate::error::Result<crate::serialize::SerializeOutput> {
259 crate::serialize::run(self, self.now(), true)
260 }
261
262 /// Serialize all local metadata and report progress through a callback.
263 ///
264 /// # Parameters
265 ///
266 /// - `progress`: callback invoked at major serialization steps.
267 pub fn serialize_full_with_progress(
268 &self,
269 progress: impl FnMut(crate::serialize::SerializeProgress),
270 ) -> crate::error::Result<crate::serialize::SerializeOutput> {
271 crate::serialize::run_with_progress(self, self.now(), true, progress)
272 }
273
274 /// Materialize remote metadata into the local store.
275 ///
276 /// For each matching remote ref, determines the merge strategy and
277 /// applies changes. Updates tracking refs and materialization timestamp.
278 ///
279 /// # Parameters
280 ///
281 /// - `remote`: optional remote name filter. If `None`, all remotes are
282 /// materialized.
283 pub fn materialize(
284 &self,
285 remote: Option<&str>,
286 ) -> crate::error::Result<crate::materialize::MaterializeOutput> {
287 crate::materialize::run(self, remote, self.now())
288 }
289
290 /// Pull metadata from remote: fetch, materialize, and index history.
291 ///
292 /// Resolves the remote, fetches the metadata ref, hydrates tip blobs,
293 /// serializes local state for merge, materializes remote changes, and
294 /// indexes historical keys for lazy loading.
295 ///
296 /// # Parameters
297 ///
298 /// - `remote`: optional remote name to pull from. If `None`, the first
299 /// configured metadata remote is used.
300 pub fn pull(&self, remote: Option<&str>) -> crate::error::Result<crate::pull::PullOutput> {
301 crate::pull::run(self, remote, self.now())
302 }
303
304 /// Serialize and attempt a single push to the remote.
305 ///
306 /// Returns the result of the push attempt. On non-fast-forward failure,
307 /// the caller is responsible for calling [`resolve_push_conflict()`](Self::resolve_push_conflict)
308 /// and retrying.
309 ///
310 /// # Parameters
311 ///
312 /// - `remote`: optional remote name to push to. If `None`, the first
313 /// configured metadata remote is used.
314 pub fn push_once(&self, remote: Option<&str>) -> crate::error::Result<crate::push::PushOutput> {
315 crate::push::push_once(self, remote, self.now())
316 }
317
318 /// Serialize and attempt a single push to the remote, reporting progress.
319 ///
320 /// # Parameters
321 ///
322 /// - `remote`: optional remote name. If `None`, the first configured
323 /// metadata remote is used.
324 /// - `progress`: callback invoked before long-running push phases.
325 ///
326 /// # Errors
327 ///
328 /// Returns an error if serialization, ref inspection, rebasing, or pushing
329 /// fails.
330 pub fn push_once_with_progress(
331 &self,
332 remote: Option<&str>,
333 progress: impl FnMut(crate::push::PushProgress),
334 ) -> crate::error::Result<crate::push::PushOutput> {
335 crate::push::push_once_with_progress(self, remote, self.now(), progress)
336 }
337
338 /// After a failed push, fetch remote changes, materialize, re-serialize,
339 /// and rebase local ref for clean fast-forward.
340 ///
341 /// Call this between push retries.
342 ///
343 /// # Parameters
344 ///
345 /// - `remote`: optional remote name. If `None`, the first configured
346 /// metadata remote is used.
347 pub fn resolve_push_conflict(&self, remote: Option<&str>) -> crate::error::Result<()> {
348 crate::push::resolve_push_conflict(self, remote, self.now())
349 }
350
351 /// Resolve a failed push and report progress.
352 ///
353 /// # Parameters
354 ///
355 /// - `remote`: optional remote name. If `None`, the first configured
356 /// metadata remote is used.
357 /// - `progress`: callback invoked before long-running conflict resolution
358 /// phases.
359 ///
360 /// # Errors
361 ///
362 /// Returns an error if fetch, hydration, materialization, serialization, or
363 /// rebase fails.
364 pub fn resolve_push_conflict_with_progress(
365 &self,
366 remote: Option<&str>,
367 progress: impl FnMut(crate::push::PushProgress),
368 ) -> crate::error::Result<()> {
369 crate::push::resolve_push_conflict_with_progress(self, remote, self.now(), progress)
370 }
371}