1#[macro_use]
2extern crate failure;
3extern crate cargo_metadata;
4extern crate copy_dir;
5extern crate semver;
6extern crate tempdir;
7
8use std::{
9 collections::HashMap,
10 ffi::OsString,
11 fmt, fs,
12 path::{Path, PathBuf},
13 process::{Command, Stdio},
14 str::FromStr,
15};
16
17use copy_dir::copy_dir;
18use semver::Version;
19use tempdir::TempDir;
20
21pub use failure::Error;
22pub type Result<T> = ::std::result::Result<T, Error>;
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct PackageId {
30 name: String,
31 version: Version,
32}
33
34impl fmt::Display for PackageId {
35 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
36 self.name.fmt(fmt)?;
37 fmt.write_str(":")?;
38 self.version.fmt(fmt)
39 }
40}
41
42impl FromStr for PackageId {
43 type Err = Error;
44 fn from_str(s: &str) -> Result<PackageId> {
45 let colon_idx = s.find(':').ok_or_else(|| {
46 format_err!(
47 "invalid package specification: {:?}; expected \"name:x.y.z\"",
48 s
49 )
50 })?;
51 let name = s[..colon_idx].to_string();
52 let version: Version = s[colon_idx + 1..].parse()?;
53 Ok(PackageId { name, version })
54 }
55}
56
57#[derive(Debug)]
58pub struct Diff {
59 pub first: PackageId,
60 pub second: PackageId,
61 pub dest: Option<PathBuf>,
62}
63
64impl Diff {
65 pub fn run(self) -> Result<()> {
66 let first_src = fetch(&self.first)?;
67 let second_src = fetch(&self.second)?;
68 if let Some(dir) = self.dest {
69 fs::create_dir_all(&dir)?;
70 copy_dir(&first_src, &dir.join(self.first.to_string()))?;
71 copy_dir(&second_src, &dir.join(self.second.to_string()))?;
72 } else {
73 run_diff_cmd(&first_src, &second_src)?;
74 }
75 Ok(())
76 }
77}
78
79pub fn run_diff_cmd(a: &Path, b: &Path) -> Result<()> {
80 let mut diff_cmd = Command::new("diff");
81 let diff_status = diff_cmd
82 .args(&["--color=auto", "-r"])
83 .arg(a)
84 .arg(b)
85 .stdout(Stdio::inherit())
86 .stderr(Stdio::inherit())
87 .status();
88 if diff_status.is_err() {
89 if !has_diff_cmd() {
90 bail!("looks like you don't have a suitable diff command installed.\n\
91 Try using --destination flag to run a custom diff tool or to compare sources manually.")
92 }
93 }
94 diff_status?;
95 Ok(())
96}
97
98#[derive(Debug)]
99pub struct Current {
100 pub dest: PathBuf,
101}
102
103impl Current {
104 pub fn run(self) -> Result<()> {
105 let metadata = Metadata {
106 manifest_path: None,
107 }
108 .run()?;
109
110 fs::create_dir_all(&self.dest)?;
111 for pkg in crates_io_packages(&metadata) {
112 let src = pkg_dir(&pkg)?;
113 let dst = self.dest.join(format!("{}:{}", pkg.name, pkg.version));
114 copy_dir(&src, &dst)?;
115 }
116 Ok(())
117 }
118}
119
120#[derive(Debug)]
121pub struct UpdateDiff {
122 pub dest: Option<PathBuf>,
123 pub args: Vec<OsString>,
124}
125
126impl UpdateDiff {
127 pub fn run(self) -> Result<()> {
128 let before_metadata = Metadata {
129 manifest_path: None,
130 }
131 .run()?;
132 let workspace_root = Path::new(&before_metadata.workspace_root);
133 let lockfile = workspace_root.join("Cargo.lock");
134 let mut lockfile_guard = LockfileGuard::new(lockfile)?;
135
136 let status = Command::new("cargo")
137 .arg("update")
138 .args(&self.args)
139 .stdout(Stdio::inherit())
140 .stderr(Stdio::inherit())
141 .status()?;
142
143 if !status.success() {
144 bail!("running cargo update failed");
145 }
146 let after_metadata = Metadata {
147 manifest_path: None,
148 }
149 .run()?;
150 let tmpdir;
151 let dest = match self.dest.as_ref() {
152 Some(it) => it.as_path(),
153 None => {
154 tmpdir = TempDir::new("cargo-diff-fetches")?;
155 tmpdir.path()
156 }
157 };
158 let before_dir = dest.join("before");
159 let after_dir = dest.join("after");
160 fs::create_dir_all(&before_dir)?;
161 fs::create_dir_all(&after_dir)?;
162 for pdiff in metadata_diff(&before_metadata, &after_metadata)? {
163 pdiff.dump_to(&dest)?;
164 }
165
166 if self.dest.is_none() {
167 run_diff_cmd(&before_dir, &after_dir)?
168 }
169 lockfile_guard.restore_lockfile()?;
170 Ok(())
171 }
172}
173
174#[derive(Debug)]
175struct PackageDiff {
176 name: String,
177 before: Option<PathBuf>,
178 after: Option<PathBuf>,
179}
180
181impl PackageDiff {
182 fn dump_to(&self, dest: &Path) -> Result<()> {
183 if let Some(src) = self.before.as_ref() {
184 let dst = dest.join("before").join(&self.name);
185 copy_dir(&src, &dst)?;
186 }
187 if let Some(src) = self.after.as_ref() {
188 let dst = dest.join("after").join(&self.name);
189 copy_dir(&src, &dst)?;
190 }
191 Ok(())
192 }
193}
194
195fn metadata_diff(
196 before: &cargo_metadata::Metadata,
197 after: &cargo_metadata::Metadata,
198) -> Result<Vec<PackageDiff>> {
199 let before = extract_packages(before)?;
200 let after = extract_packages(after)?;
201 let mut res = Vec::new();
202 for (name, before_path) in before.iter() {
203 if !after.contains_key(name) {
204 res.push(PackageDiff {
205 name: name.clone(),
206 before: Some(before_path.clone()),
207 after: None,
208 })
209 }
210 }
211 for (name, after_path) in after.iter() {
212 if !before.contains_key(name) {
213 res.push(PackageDiff {
214 name: name.clone(),
215 before: None,
216 after: Some(after_path.clone()),
217 })
218 }
219 }
220 for (name, before_path) in before.iter() {
221 if let Some(after_path) = after.get(name) {
222 if before_path == after_path {
223 continue;
224 }
225 res.push(PackageDiff {
226 name: name.clone(),
227 before: Some(before_path.clone()),
228 after: Some(after_path.clone()),
229 });
230 }
231 }
232
233 Ok(res)
234}
235
236fn extract_packages(meta: &cargo_metadata::Metadata) -> Result<HashMap<String, PathBuf>> {
237 let mut res = HashMap::new();
238 for pkg in crates_io_packages(meta) {
239 let version = Version::parse(&pkg.version)?;
240 let semver_compatible_version = if version.major == 0 {
241 format!("0.{}", version.minor)
242 } else {
243 format!("{}", version.major)
244 };
245 let name = format!("{}:{}", pkg.name, semver_compatible_version);
246 res.insert(name, pkg_dir(pkg)?);
247 }
248 Ok(res)
249}
250
251fn crates_io_packages<'a>(
252 meta: &'a cargo_metadata::Metadata,
253) -> impl Iterator<Item = &'a cargo_metadata::Package> {
254 meta.packages.iter().filter(|pkg| {
255 let is_cratesio_dep = pkg.id.contains("crates.io-index");
257 if !is_cratesio_dep {
258 eprintln!(
259 "Skipping package `{}`: not a crates.io dependency",
260 pkg.name
261 );
262 }
263 is_cratesio_dep
264 })
265}
266
267#[derive(Debug)]
270struct LockfileGuard {
271 lockfile_path: PathBuf,
272 lockfile_copy_path: PathBuf,
273 lockfile_contents: String,
274 restored: bool,
275}
276
277impl LockfileGuard {
278 fn new(path: impl Into<PathBuf>) -> Result<LockfileGuard> {
279 let lockfile_path = path.into();
280 let lockfile_copy_path = lockfile_path.with_extension(".lock.back");
281 let lockfile_contents = fs::read_to_string(&lockfile_path)?;
282 fs::write(&lockfile_copy_path, &lockfile_contents)?;
283 let res = LockfileGuard {
284 lockfile_path,
285 lockfile_copy_path,
286 lockfile_contents,
287 restored: false,
288 };
289 Ok(res)
290 }
291
292 fn restore_lockfile(&mut self) -> Result<()> {
293 self.restored = true;
294 fs::write(&self.lockfile_path, &self.lockfile_contents)?;
295 fs::remove_file(self.lockfile_copy_path.as_path())?;
296 Ok(())
297 }
298}
299
300impl Drop for LockfileGuard {
301 fn drop(&mut self) {
302 if !self.restored {
303 let _ = self.restore_lockfile();
304 }
305 }
306}
307
308struct Metadata<'a> {
309 manifest_path: Option<&'a Path>,
310}
311
312impl<'a> Metadata<'a> {
313 fn run(self) -> Result<cargo_metadata::Metadata> {
314 let metadata = cargo_metadata::metadata_deps(
315 self.manifest_path,
316 true, )
318 .map_err(|err| format_err!("cargo metadata failed: {}", err))?; Ok(metadata)
320 }
321}
322
323fn has_diff_cmd() -> bool {
324 match Command::new("diff").arg("--version").status() {
325 Err(_) => false,
326 Ok(status) => status.success(),
327 }
328}
329
330fn fetch(pkg_id: &PackageId) -> Result<PathBuf> {
333 let dir = TempDir::new("cargo-diff-fetches")?;
334 let temp_manifest = dir.path().join("Cargo.toml");
335 fs::write(&temp_manifest, format_cargo_toml(pkg_id))?;
336 let metadata = Metadata {
337 manifest_path: Some(temp_manifest.as_path()),
338 }
339 .run()?;
340
341 let package = metadata
342 .packages
343 .iter()
344 .find(|it| it.name == pkg_id.name && it.version == pkg_id.version.to_string())
345 .ok_or_else(|| format_err!("unexpected error: can't find package {:?}", pkg_id))?;
346 pkg_dir(&package)
347}
348
349fn pkg_dir(pkg: &cargo_metadata::Package) -> Result<PathBuf> {
350 let res = PathBuf::from(&pkg.manifest_path)
351 .parent()
352 .ok_or_else(|| {
353 format_err!(
354 "unexpected error: bad manifest path {:?}",
355 pkg.manifest_path
356 )
357 })?
358 .to_path_buf();
359 Ok(res)
360}
361
362fn format_cargo_toml(pkg_id: &PackageId) -> String {
364 format!(
365 r#"
366[package]
367name = "cargo-diff-temp-pkg"
368version = "0.0.0"
369
370[lib]
371path = "./Cargo.toml"
372
373[dependencies]
374{} = "={}"
375"#,
376 pkg_id.name, pkg_id.version
377 )
378}