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