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}