Skip to main content

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}