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())
231 }
232
233 /// Materialize remote metadata into the local store.
234 ///
235 /// For each matching remote ref, determines the merge strategy and
236 /// applies changes. Updates tracking refs and materialization timestamp.
237 ///
238 /// # Parameters
239 ///
240 /// - `remote`: optional remote name filter. If `None`, all remotes are
241 /// materialized.
242 pub fn materialize(
243 &self,
244 remote: Option<&str>,
245 ) -> crate::error::Result<crate::materialize::MaterializeOutput> {
246 crate::materialize::run(self, remote, self.now())
247 }
248
249 /// Pull metadata from remote: fetch, materialize, and index history.
250 ///
251 /// Resolves the remote, fetches the metadata ref, hydrates tip blobs,
252 /// serializes local state for merge, materializes remote changes, and
253 /// indexes historical keys for lazy loading.
254 ///
255 /// # Parameters
256 ///
257 /// - `remote`: optional remote name to pull from. If `None`, the first
258 /// configured metadata remote is used.
259 pub fn pull(&self, remote: Option<&str>) -> crate::error::Result<crate::pull::PullOutput> {
260 crate::pull::run(self, remote, self.now())
261 }
262
263 /// Serialize and attempt a single push to the remote.
264 ///
265 /// Returns the result of the push attempt. On non-fast-forward failure,
266 /// the caller is responsible for calling [`resolve_push_conflict()`](Self::resolve_push_conflict)
267 /// and retrying.
268 ///
269 /// # Parameters
270 ///
271 /// - `remote`: optional remote name to push to. If `None`, the first
272 /// configured metadata remote is used.
273 pub fn push_once(&self, remote: Option<&str>) -> crate::error::Result<crate::push::PushOutput> {
274 crate::push::push_once(self, remote, self.now())
275 }
276
277 /// After a failed push, fetch remote changes, materialize, re-serialize,
278 /// and rebase local ref for clean fast-forward.
279 ///
280 /// Call this between push retries.
281 ///
282 /// # Parameters
283 ///
284 /// - `remote`: optional remote name. If `None`, the first configured
285 /// metadata remote is used.
286 pub fn resolve_push_conflict(&self, remote: Option<&str>) -> crate::error::Result<()> {
287 crate::push::resolve_push_conflict(self, remote, self.now())
288 }
289}