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