1use objects::{
5 error::{HeddleError, Result as HeddleResult},
6 object::StateId,
7};
8
9use super::Repository;
10
11pub type EmptyHeadBootstrap<'a> = dyn Fn(&Repository) -> HeddleResult<()> + 'a;
14
15#[derive(Clone, Copy, Default)]
24pub struct ResolvePolicy<'a> {
25 pub git_import_guidance: bool,
26 pub bootstrap_on_empty_head: Option<&'a EmptyHeadBootstrap<'a>>,
27}
28
29impl<'a> ResolvePolicy<'a> {
30 pub fn minimal() -> Self {
32 Self {
33 git_import_guidance: false,
34 bootstrap_on_empty_head: None,
35 }
36 }
37
38 #[cfg(feature = "git-overlay")]
40 pub fn with_git_overlay_hints() -> Self {
41 Self {
42 git_import_guidance: true,
43 bootstrap_on_empty_head: None,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct ResolvedState {
51 pub state_id: StateId,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum StateResolveFailure {
57 NotFound { spec: String },
58 GitBranchHistoryNotImported { branch: String },
59 GitTagHistoryNotImported { tag: String },
60}
61
62impl std::fmt::Display for StateResolveFailure {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::NotFound { spec } => write!(f, "state not found: {spec}"),
66 Self::GitBranchHistoryNotImported { branch } => {
67 write!(f, "git branch history not imported: {branch}")
68 }
69 Self::GitTagHistoryNotImported { tag } => {
70 write!(f, "git tag history not imported: {tag}")
71 }
72 }
73 }
74}
75
76impl std::error::Error for StateResolveFailure {}
77
78#[derive(Debug)]
80pub enum StateResolveError {
81 Repository(HeddleError),
82 Failure(StateResolveFailure),
83}
84
85impl std::fmt::Display for StateResolveError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::Repository(err) => write!(f, "{err}"),
89 Self::Failure(failure) => write!(f, "{failure}"),
90 }
91 }
92}
93
94impl std::error::Error for StateResolveError {
95 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
96 match self {
97 Self::Repository(err) => Some(err),
98 Self::Failure(failure) => Some(failure),
99 }
100 }
101}
102
103impl From<HeddleError> for StateResolveError {
104 fn from(value: HeddleError) -> Self {
105 Self::Repository(value)
106 }
107}
108
109pub fn resolve_state_for_command(
114 repo: &Repository,
115 spec: &str,
116 policy: ResolvePolicy<'_>,
117) -> Result<ResolvedState, StateResolveError> {
118 if let Some(bootstrap) = policy.bootstrap_on_empty_head
119 && matches!(spec, "HEAD" | "@")
120 && repo.current_state()?.is_none()
121 {
122 bootstrap(repo)?;
123 }
124
125 match repo.resolve_state(spec)? {
126 Some(state_id) => Ok(ResolvedState { state_id }),
127 None => resolve_missing_state(repo, spec, policy),
128 }
129}
130
131fn resolve_missing_state(
132 repo: &Repository,
133 spec: &str,
134 policy: ResolvePolicy<'_>,
135) -> Result<ResolvedState, StateResolveError> {
136 #[cfg(feature = "git-overlay")]
137 if policy.git_import_guidance {
138 if let Some(tip) = repo.git_overlay_branch_tip(spec)?
139 && !tip.history_imported
140 {
141 return Err(StateResolveError::Failure(
142 StateResolveFailure::GitBranchHistoryNotImported { branch: tip.branch },
143 ));
144 }
145 if let Some(tip) = repo.git_overlay_tag_tip(spec)?
146 && !tip.history_imported
147 {
148 return Err(StateResolveError::Failure(
149 StateResolveFailure::GitTagHistoryNotImported { tag: tip.tag },
150 ));
151 }
152 }
153 #[cfg(not(feature = "git-overlay"))]
154 let _ = (repo, policy);
155
156 Err(StateResolveError::Failure(StateResolveFailure::NotFound {
157 spec: spec.to_string(),
158 }))
159}
160
161#[cfg(test)]
162mod tests {
163 use std::sync::atomic::{AtomicBool, Ordering};
164
165 use tempfile::TempDir;
166
167 use super::*;
168 use crate::Repository;
169
170 fn repo_with_snapshot() -> (TempDir, Repository, StateId) {
171 let temp = TempDir::new().unwrap();
172 let repo = Repository::init_default(temp.path()).unwrap();
173 std::fs::write(temp.path().join("a.txt"), "a").unwrap();
174 let state = repo.snapshot(Some("first".into()), None).unwrap();
175 (temp, repo, state.id())
176 }
177
178 #[test]
179 fn minimal_policy_resolves_known_state() {
180 let (_temp, repo, id) = repo_with_snapshot();
181 let resolved =
182 resolve_state_for_command(&repo, &id.to_string_full(), ResolvePolicy::minimal())
183 .unwrap();
184 assert_eq!(resolved.state_id, id);
185 }
186
187 #[test]
188 fn minimal_policy_returns_not_found_without_hints() {
189 let (_temp, repo, _) = repo_with_snapshot();
190 let err = resolve_state_for_command(&repo, "hs-zzzzzzzzzzzz", ResolvePolicy::minimal())
191 .unwrap_err();
192 assert!(matches!(
193 err,
194 StateResolveError::Failure(StateResolveFailure::NotFound { .. })
195 ));
196 }
197
198 #[test]
199 fn bootstrap_runs_only_for_empty_head_spec() {
200 let (_temp, repo, id) = repo_with_snapshot();
201 let called = AtomicBool::new(false);
202 let bootstrap = |_: &Repository| {
203 called.store(true, Ordering::SeqCst);
204 Ok(())
205 };
206 let policy = ResolvePolicy {
207 git_import_guidance: false,
208 bootstrap_on_empty_head: Some(&bootstrap),
209 };
210
211 let resolved = resolve_state_for_command(&repo, "HEAD", policy).unwrap();
212 assert_eq!(resolved.state_id, id);
213 assert!(!called.load(Ordering::SeqCst));
214
215 let resolved = resolve_state_for_command(&repo, &id.to_string_full(), policy).unwrap();
216 assert_eq!(resolved.state_id, id);
217 assert!(!called.load(Ordering::SeqCst));
218 }
219
220 #[test]
221 fn bootstrap_runs_for_empty_head_before_resolving_head() {
222 let temp = TempDir::new().unwrap();
223 let repo = Repository::init(temp.path()).unwrap();
224 assert!(repo.current_state().unwrap().is_none());
225 std::fs::write(temp.path().join("a.txt"), "a").unwrap();
226
227 let bootstrapped = AtomicBool::new(false);
228 let bootstrap = |repo: &Repository| {
229 bootstrapped.store(true, Ordering::SeqCst);
230 repo.snapshot(Some("bootstrap".into()), None).map(|_| ())
231 };
232 let policy = ResolvePolicy {
233 git_import_guidance: false,
234 bootstrap_on_empty_head: Some(&bootstrap),
235 };
236
237 let resolved = resolve_state_for_command(&repo, "HEAD", policy).unwrap();
238 assert!(bootstrapped.load(Ordering::SeqCst));
239 assert_eq!(repo.head().unwrap(), Some(resolved.state_id));
240 }
241
242 #[cfg(feature = "git-overlay")]
243 #[test]
244 fn git_overlay_hint_policy_distinguishes_not_found() {
245 let (_temp, repo, _) = repo_with_snapshot();
246 let err = resolve_state_for_command(
247 &repo,
248 "missing-branch",
249 ResolvePolicy::with_git_overlay_hints(),
250 )
251 .unwrap_err();
252 assert!(matches!(
253 err,
254 StateResolveError::Failure(StateResolveFailure::NotFound { .. })
255 ));
256 }
257}