crates_index_diff/types.rs
1use std::collections::HashMap;
2
3use bstr::BString;
4use smartstring::alias::String as SmolString;
5use std::hash::Hash;
6use std::{fmt, slice};
7
8/// A wrapper for a repository of the crates.io index.
9pub struct Index {
10 /// The name and path of the reference used to keep track of the last seen state of the
11 /// crates.io repository. The default value is `refs/heads/crates-index-diff_last-seen`.
12 pub seen_ref_name: &'static str,
13 /// The name of the branch to fetch. This value also affects the tracking branch.
14 pub branch_name: &'static str,
15 /// The name of the symbolic name of the remote to fetch from.
16 /// If `None`, obtain the remote name from the configuration of the currently checked-out branch.
17 pub remote_name: Option<BString>,
18 /// The git repository to use for diffing
19 pub(crate) repo: gix::Repository,
20}
21
22/// Identify a kind of change that occurred to a crate
23#[derive(Clone, Eq, PartialEq, Debug)]
24pub enum Change {
25 /// A crate version was added.
26 Added(CrateVersion),
27 /// A crate version was unyanked.
28 Unyanked(CrateVersion),
29 /// A crate version was added in a yanked state.
30 ///
31 /// This can happen if we don't see the commit that added them, so it appears to pop into existence yanked.
32 /// Knowing this should help to trigger the correct action, as simply `Yanked` crates would be treated quite differently.
33 AddedAndYanked(CrateVersion),
34 /// A crate version was yanked.
35 Yanked(CrateVersion),
36 /// The name of the crate whose file was deleted, which implies all versions were deleted as well.
37 CrateDeleted {
38 /// The name of the deleted crate.
39 name: String,
40 /// All of its versions that were deleted along with the file.
41 versions: Vec<CrateVersion>,
42 },
43 /// A crate version was deleted.
44 ///
45 /// Note that this is equivalent to deleting a line from a crates version file.
46 /// Should more than one lines be removed per commit, the order of these changes is nondeterministic.
47 VersionDeleted(CrateVersion),
48}
49
50impl Change {
51 /// Return the added crate, if this is this kind of change.
52 pub fn added(&self) -> Option<&CrateVersion> {
53 match self {
54 Change::Added(v) | Change::AddedAndYanked(v) => Some(v),
55 _ => None,
56 }
57 }
58
59 /// Return the yanked crate, if this is this kind of change.
60 pub fn yanked(&self) -> Option<&CrateVersion> {
61 match self {
62 Change::Yanked(v) | Change::AddedAndYanked(v) => Some(v),
63 _ => None,
64 }
65 }
66
67 /// Return the unyanked crate, if this is this kind of change.
68 pub fn unyanked(&self) -> Option<&CrateVersion> {
69 match self {
70 Change::Unyanked(v) => Some(v),
71 _ => None,
72 }
73 }
74
75 /// Return the deleted crate, if this is this kind of change.
76 pub fn crate_deleted(&self) -> Option<(&str, &[CrateVersion])> {
77 match self {
78 Change::CrateDeleted { name, versions } => Some((name.as_str(), versions)),
79 _ => None,
80 }
81 }
82
83 /// Return the deleted version crate, if this is this kind of change.
84 pub fn version_deleted(&self) -> Option<&CrateVersion> {
85 match self {
86 Change::VersionDeleted(v) => Some(v),
87 _ => None,
88 }
89 }
90
91 /// Returns all versions affected by this change.
92 ///
93 /// The returned slice usually has length 1.
94 /// However, if a crate was purged from the index by an admin,
95 /// all versions of the purged crate are returned.
96 pub fn versions(&self) -> &[CrateVersion] {
97 match self {
98 Change::Added(v)
99 | Change::Unyanked(v)
100 | Change::AddedAndYanked(v)
101 | Change::Yanked(v)
102 | Change::VersionDeleted(v) => slice::from_ref(v),
103 Change::CrateDeleted { versions, .. } => versions,
104 }
105 }
106}
107
108impl fmt::Display for Change {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(
111 f,
112 "{}",
113 match *self {
114 Change::Added(_) => "added",
115 Change::Yanked(_) => "yanked",
116 Change::CrateDeleted { .. } => "crate deleted",
117 Change::VersionDeleted(_) => "version deleted",
118 Change::Unyanked(_) => "unyanked",
119 Change::AddedAndYanked(_) => "added and yanked",
120 }
121 )
122 }
123}
124
125/// Section in which a dependency was defined in.
126#[derive(
127 Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash, Ord, PartialOrd,
128)]
129#[serde(rename_all = "lowercase")]
130pub enum DependencyKind {
131 /// Used for production builds.
132 Normal,
133 /// Used only for tests and examples.
134 Dev,
135 /// Used in build scripts.
136 Build,
137}
138
139/// Pack all information we know about a change made to a version of a crate.
140#[derive(Default, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
141pub struct CrateVersion {
142 /// The crate name, i.e. `clap`.
143 pub name: SmolString,
144 /// is the release yanked?
145 pub yanked: bool,
146 /// The semantic version of the crate.
147 #[serde(rename = "vers")]
148 pub version: SmolString,
149 /// The checksum over the crate archive
150 #[serde(rename = "cksum", with = "hex")]
151 pub checksum: [u8; 32],
152 /// All cargo features
153 pub features: HashMap<String, Vec<String>>,
154 /// All crate dependencies
155 #[serde(rename = "deps")]
156 pub dependencies: Vec<Dependency>,
157}
158
159impl CrateVersion {
160 /// Parse and return this crate's version as a `semver::Version`.
161 ///
162 /// The crate index guarantees versions follow Semantic Versioning, so
163 /// parsing is expected to succeed. Both crates.io and cargo make sure
164 /// of that, and also rely on it.
165 ///
166 /// If parsing fails, this indicates a violation of that guarantee and
167 /// the method will panic.
168 #[cfg(feature = "semver")]
169 pub fn version(&self) -> semver::Version {
170 semver::Version::parse(&self.version)
171 .expect("crate index guarantees a valid semantic version")
172 }
173}
174
175/// A single dependency of a specific crate version
176#[derive(
177 Clone, serde::Serialize, serde::Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug, Hash,
178)]
179pub struct Dependency {
180 /// The crate name
181 pub name: SmolString,
182 /// The version the parent crate requires of this dependency
183 #[serde(rename = "req")]
184 pub required_version: SmolString,
185 /// All cargo features configured by the parent crate
186 pub features: Vec<String>,
187 /// True if this is an optional dependency
188 pub optional: bool,
189 /// True if default features are enabled
190 pub default_features: bool,
191 /// The name of the build target
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub target: Option<SmolString>,
194 /// The kind of dependency, usually 'normal' or 'dev'
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub kind: Option<DependencyKind>,
197 /// The package this crate is contained in
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub package: Option<SmolString>,
200}
201impl Dependency {
202 /// Parse and return this dependency's semantic version requirement as `semver::VersionReq`.
203 ///
204 /// # Panics
205 ///
206 /// Panics if `self.required_version` is not a valid SemVer requirement.
207 ///
208 /// Cargo and crates.io enforce SemVer compliance for dependencies when a crate is
209 /// published or updated. Nevertheless, our index contains a small number of releases
210 /// whose dependency requirements are not valid SemVer (currently 14 out of ~1.8M).
211 #[cfg(feature = "semver")]
212 pub fn required_version(&self) -> semver::VersionReq {
213 semver::VersionReq::parse(&self.required_version)
214 .expect("version requirement should always be a valid SemVer")
215 }
216}