Skip to main content

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}