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;