agpm_cli/version/constraints/mod.rs
1//! Version constraint parsing and resolution for AGPM dependencies.
2//!
3//! This module provides comprehensive version constraint handling for AGPM dependencies,
4//! supporting semantic versioning, Git references, and various constraint types. It enables
5//! dependency resolution with conflict detection and version matching.
6//!
7//! # Version Constraint Types
8//!
9//! AGPM supports several types of version constraints:
10//!
11//! - **Exact versions**: `"1.0.0"` - Matches exactly the specified version
12//! - **Semantic version ranges**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"` - Uses semver ranges
13//! - **Git references**: `"main"`, `"feature/branch"`, `"abc123"`, `"latest"` - Git branches, tags, or commits
14//!
15//! # Constraint Resolution
16//!
17//! The constraint system provides:
18//! - **Conflict detection**: Prevents incompatible constraints for the same dependency
19//! - **Version resolution**: Finds best matching versions from available options
20//! - **Prerelease handling**: Manages alpha, beta, RC versions appropriately
21//! - **Precedence rules**: Resolves multiple constraints consistently
22//!
23//! # Examples
24//!
25//! ## Basic Constraint Parsing
26//!
27//! ```rust,no_run
28//! use agpm_cli::version::constraints::VersionConstraint;
29//!
30//! // Parse different constraint types
31//! let exact = VersionConstraint::parse("1.0.0")?;
32//! let caret = VersionConstraint::parse("^1.0.0")?;
33//! let latest = VersionConstraint::parse("latest")?;
34//! let branch = VersionConstraint::parse("main")?;
35//! # Ok::<(), anyhow::Error>(())
36//! ```
37//!
38//! ## Constraint Set Management
39//!
40//! ```rust,no_run
41//! use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
42//! use semver::Version;
43//!
44//! let mut set = ConstraintSet::new();
45//! set.add(VersionConstraint::parse(">=1.0.0")?)?;
46//! set.add(VersionConstraint::parse("<2.0.0")?)?;
47//!
48//! let versions = vec![
49//! Version::parse("0.9.0")?,
50//! Version::parse("1.5.0")?,
51//! Version::parse("2.0.0")?,
52//! ];
53//!
54//! let best = set.find_best_match(&versions).unwrap();
55//! assert_eq!(best, &Version::parse("1.5.0")?);
56//! # Ok::<(), anyhow::Error>(())
57//! ```
58//!
59//! ## Dependency Resolution
60//!
61//! ```rust,no_run
62//! use agpm_cli::version::constraints::ConstraintResolver;
63//! use semver::Version;
64//! use std::collections::HashMap;
65//!
66//! let mut resolver = ConstraintResolver::new();
67//! resolver.add_constraint("dep1", "^1.0.0")?;
68//! resolver.add_constraint("dep2", "~2.1.0")?;
69//!
70//! let mut available = HashMap::new();
71//! available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
72//! available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
73//!
74//! let resolved = resolver.resolve(&available)?;
75//! # Ok::<(), anyhow::Error>(())
76//! ```
77//!
78//! # Constraint Syntax Reference
79//!
80//! | Syntax | Description | Example |
81//! |--------|-------------|----------|
82//! | `1.0.0` | Exact version | `"1.0.0"` |
83//! | `^1.0.0` | Compatible within major version | `"^1.0.0"` matches `1.x.x` |
84//! | `~1.2.0` | Compatible within minor version | `"~1.2.0"` matches `1.2.x` |
85//! | `>=1.0.0` | Greater than or equal | `">=1.0.0"` |
86//! | `<2.0.0` | Less than | `"<2.0.0"` |
87//! | `>=1.0.0, <2.0.0` | Range constraint | Multiple constraints |
88//! | `main` | Git branch reference | Branch name |
89//! | `latest` | Git tag or branch name | Just a regular ref |
90//! | `v1.0.0` | Git tag reference | Tag name |
91//! | `abc123` | Git commit reference | Commit hash (full or abbreviated) |
92//!
93//! # Version Resolution Precedence
94//!
95//! When resolving versions, AGPM follows this precedence:
96//!
97//! 1. **Exact matches** take highest priority
98//! 2. **Semantic version requirements** are resolved to highest compatible version
99//! 3. **Stable versions** are preferred over prereleases (unless explicitly allowed)
100//! 4. **Newer versions** are preferred when multiple versions satisfy constraints
101//! 5. **Git references** bypass semantic versioning and use exact ref matching
102//!
103//! # Prerelease Version Handling
104//!
105//! - **Default behavior**: Prereleases (alpha, beta, RC) are excluded from resolution
106//! - **Explicit inclusion**: Use Git references to include prereleases
107//! - **Version ranges**: Prereleases only match if explicitly specified in range
108//! - **Constraint sets**: If any constraint allows prereleases, the entire set does
109//!
110//! # Error Conditions
111//!
112//! The constraint system handles these error conditions:
113//! - **Conflicting constraints**: Same dependency with incompatible requirements
114//! - **Invalid version strings**: Malformed semantic version specifications
115//! - **Resolution failures**: No available version satisfies all constraints
116//! - **Missing dependencies**: Required dependency not found in available versions
117
118use anyhow::Result;
119use semver::{Version, VersionReq};
120use std::fmt;
121
122pub mod constraint_set;
123pub mod resolver;
124
125pub use constraint_set::ConstraintSet;
126pub use resolver::ConstraintResolver;
127
128/// A version constraint that defines acceptable versions for a dependency.
129///
130/// Version constraints in AGPM support multiple formats to handle different
131/// versioning strategies and Git-based dependencies. Each constraint type
132/// provides specific matching behavior for version resolution.
133///
134/// # Constraint Types
135///
136/// - [`Exact`](Self::Exact): Matches exactly one specific semantic version
137/// - [`Requirement`](Self::Requirement): Matches versions using semver ranges
138/// - [`GitRef`](Self::GitRef): Matches specific Git branches, tags, or commit hashes (including "latest")
139///
140/// # Examples
141///
142/// ```rust,no_run
143/// use agpm_cli::version::constraints::VersionConstraint;
144/// use semver::Version;
145///
146/// // Parse various constraint formats
147/// let exact = VersionConstraint::parse("1.0.0")?;
148/// let caret = VersionConstraint::parse("^1.0.0")?; // Compatible versions
149/// let tilde = VersionConstraint::parse("~1.2.0")?; // Patch-level compatible
150/// let range = VersionConstraint::parse(">=1.0.0, <2.0.0")?; // Version range
151/// let branch = VersionConstraint::parse("main")?;
152/// let latest_tag = VersionConstraint::parse("latest")?; // Just a tag name
153/// let commit = VersionConstraint::parse("abc123def")?;
154///
155/// // Test version matching
156/// let version = Version::parse("1.2.3")?;
157/// assert!(caret.matches(&version));
158/// # Ok::<(), anyhow::Error>(())
159/// ```
160///
161/// # Prerelease Handling
162///
163/// By default, most constraints exclude prerelease versions to ensure stability:
164/// - `GitRef` constraints (including "latest" tag names) may reference any commit
165///
166/// # Git Reference Matching
167///
168/// Git references are matched by name rather than semantic version:
169/// - Branch names: `"main"`, `"develop"`, `"feature/auth"`
170/// - Tag names: `"v1.0.0"`, `"release-2023-01"`
171/// - Commit hashes: `"abc123def456"` (full or abbreviated)
172///
173/// # Prefix Support (Monorepo Versioning)
174///
175/// Constraints can include optional prefixes for monorepo-style versioning:
176/// - `"agents-v1.0.0"`: Exact version with prefix
177/// - `"agents-^v1.0.0"`: Compatible version range with prefix
178/// - Prefixed constraints only match tags with the same prefix
179#[derive(Debug, Clone)]
180pub enum VersionConstraint {
181 /// Exact version match with optional prefix (e.g., "1.0.0", "agents-v1.0.0")
182 Exact {
183 prefix: Option<String>,
184 version: Version,
185 },
186
187 /// Semantic version requirement with optional prefix (e.g., "^1.0.0", "agents-^v1.0.0")
188 Requirement {
189 prefix: Option<String>,
190 req: VersionReq,
191 },
192
193 /// Git tag or branch name (including "latest" - it's just a tag name)
194 GitRef(String),
195}
196
197impl VersionConstraint {
198 /// Parse a constraint string into a [`VersionConstraint`].
199 ///
200 /// This method intelligently determines the constraint type based on the input format.
201 /// It handles various syntaxes including semantic versions, version ranges, special
202 /// keywords, and Git references.
203 ///
204 /// # Parsing Logic
205 ///
206 /// 1. **Special keywords**: `"*"` (wildcard for any version)
207 /// 2. **Exact versions**: `"1.0.0"`, `"v1.0.0"` (without range operators)
208 /// 3. **Version requirements**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"`, `"<2.0.0"`
209 /// 4. **Git references**: Any string that doesn't match the above patterns (including "latest")
210 ///
211 /// # Arguments
212 ///
213 /// * `constraint` - The constraint string to parse (whitespace is trimmed)
214 ///
215 /// # Returns
216 ///
217 /// Returns `Ok(VersionConstraint)` on successful parsing, or `Err` if the
218 /// semantic version parsing fails (Git references always succeed).
219 ///
220 /// # Examples
221 ///
222 /// ```rust,no_run
223 /// use agpm_cli::version::constraints::VersionConstraint;
224 ///
225 /// // Exact version matching
226 /// let exact = VersionConstraint::parse("1.0.0")?;
227 /// let exact_with_v = VersionConstraint::parse("v1.0.0")?;
228 ///
229 /// // Semantic version ranges
230 /// let caret = VersionConstraint::parse("^1.0.0")?; // 1.x.x compatible
231 /// let tilde = VersionConstraint::parse("~1.2.0")?; // 1.2.x compatible
232 /// let gte = VersionConstraint::parse(">=1.0.0")?; // Greater or equal
233 /// let range = VersionConstraint::parse(">1.0.0, <2.0.0")?; // Range
234 ///
235 /// // Special keywords
236 /// let any = VersionConstraint::parse("*")?; // Any version
237 ///
238 /// // Git references
239 /// let branch = VersionConstraint::parse("main")?; // Branch name
240 /// let tag = VersionConstraint::parse("release-v1")?; // Tag name
241 /// let latest = VersionConstraint::parse("latest")?; // Just a tag/branch name
242 /// let commit = VersionConstraint::parse("abc123def")?; // Commit hash
243 /// # Ok::<(), anyhow::Error>(())
244 /// ```
245 ///
246 /// # Error Handling
247 ///
248 /// This method only returns errors for malformed semantic version strings.
249 /// Git references and special keywords always parse successfully.
250 pub fn parse(constraint: &str) -> Result<Self> {
251 let trimmed = constraint.trim();
252
253 // Extract prefix from constraint first (e.g., "agents-^v1.0.0" → (Some("agents"), "^v1.0.0"))
254 let (prefix, version_str) = crate::version::split_prefix_and_version(trimmed);
255
256 // Check for wildcard in the version portion (supports both "*" and "agents-*")
257 if version_str == "*" {
258 // Wildcard means any version - treat as a GitRef that matches everything
259 return Ok(Self::GitRef(trimmed.to_string()));
260 }
261
262 // Try to parse as exact version (with or without 'v' prefix)
263 let cleaned_version_str = version_str.strip_prefix('v').unwrap_or(version_str);
264 if let Ok(version) = Version::parse(cleaned_version_str) {
265 // Check if it's a range operator
266 if !version_str.starts_with('^')
267 && !version_str.starts_with('~')
268 && !version_str.starts_with('>')
269 && !version_str.starts_with('<')
270 && !version_str.starts_with('=')
271 {
272 return Ok(Self::Exact {
273 prefix,
274 version,
275 });
276 }
277 }
278
279 // Try to parse as version requirement (with v-prefix normalization)
280 match crate::version::parse_version_req(version_str) {
281 Ok(req) => {
282 return Ok(Self::Requirement {
283 prefix,
284 req,
285 });
286 }
287 Err(e) => {
288 // If it looks like a semver constraint but failed to parse, return error
289 if version_str.starts_with('^')
290 || version_str.starts_with('~')
291 || version_str.starts_with('=')
292 || version_str.starts_with('>')
293 || version_str.starts_with('<')
294 {
295 return Err(anyhow::anyhow!("Invalid semver constraint '{trimmed}': {e}"));
296 }
297 // Otherwise it might be a git ref, continue
298 }
299 }
300
301 // Otherwise treat as git ref
302 Ok(Self::GitRef(trimmed.to_string()))
303 }
304
305 /// Check if a semantic version satisfies this constraint.
306 ///
307 /// This method tests whether a given semantic version matches the requirements
308 /// of this constraint. Different constraint types use different matching logic:
309 ///
310 /// - **Exact**: Version must match exactly
311 /// - **Requirement**: Version must satisfy the semver range
312 /// - **`GitRef`**: Never matches semantic versions (Git refs are matched separately)
313 ///
314 /// # Arguments
315 ///
316 /// * `version` - The semantic version to test against this constraint
317 ///
318 /// # Returns
319 ///
320 /// Returns `true` if the version satisfies the constraint, `false` otherwise.
321 ///
322 /// # Examples
323 ///
324 /// ```rust,no_run
325 /// use agpm_cli::version::constraints::VersionConstraint;
326 /// use semver::Version;
327 ///
328 /// let constraint = VersionConstraint::parse("^1.0.0")?;
329 /// let version = Version::parse("1.2.3")?;
330 ///
331 /// assert!(constraint.matches(&version)); // 1.2.3 is compatible with ^1.0.0
332 /// # Ok::<(), anyhow::Error>(())
333 /// ```
334 ///
335 /// # Note
336 ///
337 /// Git reference constraints always return `false` for this method since they
338 /// operate on Git refs rather than semantic versions. Use [`matches_ref`](Self::matches_ref)
339 /// to test Git reference matching.
340 #[must_use]
341 pub fn matches(&self, version: &Version) -> bool {
342 match self {
343 Self::Exact {
344 version: v,
345 ..
346 } => v == version,
347 Self::Requirement {
348 req,
349 ..
350 } => req.matches(version),
351 Self::GitRef(_) => false, // Git refs don't match semver versions
352 }
353 }
354
355 /// Check if a Git reference satisfies this constraint.
356 ///
357 /// This method tests whether a Git reference (branch, tag, or commit hash)
358 /// matches a Git reference constraint. Only [`GitRef`](Self::GitRef) constraints
359 /// can match Git references - all other constraint types return `false`.
360 ///
361 /// # Arguments
362 ///
363 /// * `git_ref` - The Git reference string to test (branch, tag, or commit)
364 ///
365 /// # Returns
366 ///
367 /// Returns `true` if this is a `GitRef` constraint with matching reference name,
368 /// `false` otherwise.
369 ///
370 /// # Examples
371 ///
372 /// ```rust,no_run
373 /// use agpm_cli::version::constraints::VersionConstraint;
374 ///
375 /// let branch_constraint = VersionConstraint::parse("main")?;
376 /// assert!(branch_constraint.matches_ref("main"));
377 /// assert!(!branch_constraint.matches_ref("develop"));
378 ///
379 /// let version_constraint = VersionConstraint::parse("^1.0.0")?;
380 /// assert!(!version_constraint.matches_ref("main")); // Version constraints don't match refs
381 /// # Ok::<(), anyhow::Error>(())
382 /// ```
383 ///
384 /// # Use Cases
385 ///
386 /// This method is primarily used during dependency resolution to match
387 /// dependencies that specify Git branches, tags, or commit hashes rather
388 /// than semantic versions.
389 #[must_use]
390 pub fn matches_ref(&self, git_ref: &str) -> bool {
391 match self {
392 Self::GitRef(ref_name) => ref_name == git_ref,
393 _ => false,
394 }
395 }
396
397 /// Check if a VersionInfo satisfies this constraint, including prefix matching.
398 ///
399 /// This method performs comprehensive matching that considers both the prefix
400 /// (for monorepo-style versioning) and the semantic version. It's the preferred
401 /// method for version resolution when working with potentially prefixed versions.
402 ///
403 /// # Matching Rules
404 ///
405 /// - **Prefix matching**: Constraint and version must have the same prefix (both None, or same String)
406 /// - **Version matching**: After prefix check, applies standard semver matching rules
407 /// - **Prerelease handling**: Follows same rules as [`matches`](Self::matches)
408 ///
409 /// # Arguments
410 ///
411 /// * `version_info` - The version information to test, including prefix and semver
412 ///
413 /// # Returns
414 ///
415 /// Returns `true` if both the prefix matches AND the version satisfies the constraint.
416 ///
417 /// # Examples
418 ///
419 /// ```rust,no_run
420 /// use agpm_cli::version::constraints::VersionConstraint;
421 /// use agpm_cli::version::VersionInfo;
422 /// use semver::Version;
423 ///
424 /// // Prefixed version matching
425 /// let constraint = VersionConstraint::parse("agents-^v1.0.0")?;
426 /// let version = VersionInfo {
427 /// prefix: Some("agents".to_string()),
428 /// version: Version::parse("1.2.0")?,
429 /// tag: "agents-v1.2.0".to_string(),
430 /// prerelease: false,
431 /// };
432 /// assert!(constraint.matches_version_info(&version));
433 ///
434 /// // Prefix mismatch
435 /// let wrong_prefix = VersionInfo {
436 /// prefix: Some("snippets".to_string()),
437 /// version: Version::parse("1.2.0")?,
438 /// tag: "snippets-v1.2.0".to_string(),
439 /// prerelease: false,
440 /// };
441 /// assert!(!constraint.matches_version_info(&wrong_prefix));
442 ///
443 /// // Unprefixed constraint only matches unprefixed versions
444 /// let no_prefix_constraint = VersionConstraint::parse("^1.0.0")?;
445 /// let no_prefix_version = VersionInfo {
446 /// prefix: None,
447 /// version: Version::parse("1.2.0")?,
448 /// tag: "v1.2.0".to_string(),
449 /// prerelease: false,
450 /// };
451 /// assert!(no_prefix_constraint.matches_version_info(&no_prefix_version));
452 /// assert!(!no_prefix_constraint.matches_version_info(&version)); // Has prefix
453 /// # Ok::<(), anyhow::Error>(())
454 /// ```
455 #[inline]
456 #[must_use]
457 pub fn matches_version_info(&self, version_info: &crate::version::VersionInfo) -> bool {
458 // Check prefix first
459 let constraint_prefix = match self {
460 Self::Exact {
461 prefix,
462 ..
463 }
464 | Self::Requirement {
465 prefix,
466 ..
467 } => prefix.as_ref(),
468 _ => None,
469 };
470
471 // Prefix must match (both None or both Some with same value)
472 if constraint_prefix != version_info.prefix.as_ref() {
473 return false;
474 }
475
476 // Then check version using existing logic
477 self.matches(&version_info.version)
478 }
479
480 /// Convert this constraint to a semantic version requirement if applicable.
481 ///
482 /// This method converts version-based constraints into [`VersionReq`] objects
483 /// that can be used with the semver crate for version matching. Git reference
484 /// constraints cannot be converted since they don't represent version ranges.
485 ///
486 /// # Returns
487 ///
488 /// Returns `Some(VersionReq)` for constraints that can be expressed as semantic
489 /// version requirements, or `None` for Git reference constraints.
490 ///
491 /// # Conversion Rules
492 ///
493 /// - **Exact**: Converted to `=1.0.0` requirement
494 /// - **Requirement**: Returns the inner `VersionReq` directly
495 /// - **`GitRef`**: Returns `None` (cannot be converted)
496 ///
497 /// # Examples
498 ///
499 /// ```rust,no_run
500 /// use agpm_cli::version::constraints::VersionConstraint;
501 /// use semver::Version;
502 ///
503 /// let exact = VersionConstraint::parse("1.0.0")?;
504 /// let req = exact.to_version_req().unwrap();
505 /// assert!(req.matches(&Version::parse("1.0.0")?));
506 ///
507 /// let caret = VersionConstraint::parse("^1.0.0")?;
508 /// let req = caret.to_version_req().unwrap();
509 /// assert!(req.matches(&Version::parse("1.2.0")?));
510 ///
511 /// let git_ref = VersionConstraint::parse("main")?;
512 /// assert!(git_ref.to_version_req().is_none()); // Git refs can't be converted
513 /// # Ok::<(), anyhow::Error>(())
514 /// ```
515 ///
516 /// # Use Cases
517 ///
518 /// This method is useful for integrating with existing semver-based tooling
519 /// or for performing version calculations that require `VersionReq` objects.
520 #[must_use]
521 pub fn to_version_req(&self) -> Option<VersionReq> {
522 match self {
523 Self::Exact {
524 version,
525 ..
526 } => {
527 // Create an exact version requirement
528 VersionReq::parse(&format!("={version}")).ok()
529 }
530 Self::Requirement {
531 req,
532 ..
533 } => Some(req.clone()),
534 Self::GitRef(_) => None, // Git refs cannot be converted to version requirements
535 }
536 }
537
538 /// Check if this constraint allows prerelease versions.
539 ///
540 /// Prerelease versions contain identifiers like `-alpha`, `-beta`, `-rc` that
541 /// indicate pre-release status. This method determines whether the constraint
542 /// should consider such versions during resolution.
543 ///
544 /// # Prerelease Policy
545 ///
546 /// - **`GitRef`**: Allows prereleases (Git refs may point to any commit)
547 /// - **Exact/Requirement**: Excludes prereleases unless explicitly specified
548 ///
549 /// # Returns
550 ///
551 /// Returns `true` if prerelease versions should be considered, `false` if only
552 /// stable versions should be considered.
553 ///
554 /// # Examples
555 ///
556 /// ```rust,no_run
557 /// use agpm_cli::version::constraints::VersionConstraint;
558 ///
559 /// let branch = VersionConstraint::parse("main")?;
560 /// assert!(branch.allows_prerelease()); // Git refs may be any version
561 ///
562 /// let latest = VersionConstraint::parse("latest")?;
563 /// assert!(latest.allows_prerelease()); // Git ref - just a tag name
564 ///
565 /// let exact = VersionConstraint::parse("1.0.0")?;
566 /// assert!(!exact.allows_prerelease()); // Exact stable version
567 /// # Ok::<(), anyhow::Error>(())
568 /// ```
569 ///
570 /// # Impact on Resolution
571 ///
572 /// During version resolution, if any constraint in a set allows prereleases,
573 /// the entire constraint set will consider prerelease versions as candidates.
574 #[must_use]
575 pub const fn allows_prerelease(&self) -> bool {
576 matches!(self, Self::GitRef(_))
577 }
578
579 /// Check if this constraint represents a semantic version constraint.
580 ///
581 /// Returns `true` for [`Exact`](Self::Exact) and [`Requirement`](Self::Requirement)
582 /// variants, `false` for [`GitRef`](Self::GitRef). This distinguishes between
583 /// stable version tags (e.g., `v1.0.0`, `^1.0.0`) and floating refs (e.g.,
584 /// branch names like `main`, commit SHAs).
585 ///
586 /// # Returns
587 ///
588 /// - `true` for semver constraints (`Exact` or `Requirement`)
589 /// - `false` for git references (`GitRef`)
590 ///
591 /// # Examples
592 ///
593 /// ```rust,no_run
594 /// use agpm_cli::version::constraints::VersionConstraint;
595 ///
596 /// let exact = VersionConstraint::parse("v1.0.0")?;
597 /// assert!(exact.is_semver()); // Exact version
598 ///
599 /// let caret = VersionConstraint::parse("^1.0.0")?;
600 /// assert!(caret.is_semver()); // Requirement
601 ///
602 /// let branch = VersionConstraint::parse("main")?;
603 /// assert!(!branch.is_semver()); // Git branch ref
604 ///
605 /// let commit = VersionConstraint::parse("abc123")?;
606 /// assert!(!commit.is_semver()); // Git commit ref
607 /// # Ok::<(), anyhow::Error>(())
608 /// ```
609 ///
610 /// # Use Cases
611 ///
612 /// This method is useful during version conflict resolution to prefer
613 /// semver constraints over floating git refs. Semver constraints provide
614 /// more stable, reproducible builds.
615 #[must_use]
616 pub const fn is_semver(&self) -> bool {
617 matches!(self, Self::Exact { .. } | Self::Requirement { .. })
618 }
619}
620
621impl fmt::Display for VersionConstraint {
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 match self {
624 Self::Exact {
625 prefix,
626 version,
627 } => {
628 if let Some(p) = prefix {
629 write!(f, "{p}-{version}")
630 } else {
631 write!(f, "{version}")
632 }
633 }
634 Self::Requirement {
635 prefix,
636 req,
637 } => {
638 if let Some(p) = prefix {
639 write!(f, "{p}-{req}")
640 } else {
641 write!(f, "{req}")
642 }
643 }
644 Self::GitRef(ref_name) => write!(f, "{ref_name}"),
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests;