cut/
args.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{env, path::PathBuf, str::FromStr};
6
7use anyhow::{self, Result, bail};
8use clap::{ArgAction, Parser};
9use thiserror::Error;
10
11/// Tool for cutting duplicate versions of a subset of crates in a git
12/// repository.
13///
14/// Duplicated crate dependencies are redirected so that if a crate is
15/// duplicated with its dependency, the duplicate's dependency points to the
16/// duplicated dependency.  Package names are updated to avoid conflicts with
17/// their original. Duplicates respect membership or exclusion from a workspace.
18#[derive(Parser)]
19#[command(author, version)]
20pub(crate) struct Args {
21    /// Name of the feature the crates are being cut for -- duplicated crate
22    /// package names will be suffixed with a hyphen followed by this
23    /// feature name.
24    #[arg(short, long)]
25    pub feature: String,
26
27    /// Root of repository -- all source and destination paths must be within
28    /// this path, and it must contain the repo's `workspace` configuration.
29    /// Defaults to the parent of the working directory that contains a .git
30    /// directory.
31    pub root: Option<PathBuf>,
32
33    /// Add a directory to duplicate crates from, along with the destination to
34    /// duplicate it to, and optionally a suffix to remove from package
35    /// names within this directory, all separated by colons.
36    ///
37    /// Only crates (directories containing a `Cargo.toml` file) found under the
38    /// source (first) path whose package names were supplied as a
39    /// `--package` will be duplicated at the destination (second) path.
40    /// Copying will preserve the directory structure from the source directory
41    /// to the destination directory.
42    #[arg(short, long = "dir")]
43    pub directories: Vec<Directory>,
44
45    /// Package names to include in the cut (this must match the package name in
46    /// its source location, including any suffixes)
47    #[arg(short, long = "package")]
48    pub packages: Vec<String>,
49
50    /// Don't make changes to the workspace.
51    #[arg(long="no-workspace-update", action=ArgAction::SetFalse)]
52    pub workspace_update: bool,
53
54    /// Don't execute the cut, just display it.
55    #[arg(long)]
56    pub dry_run: bool,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub(crate) struct Directory {
61    pub src: PathBuf,
62    pub dst: PathBuf,
63    pub suffix: Option<String>,
64}
65
66#[derive(Error, Debug)]
67pub(crate) enum DirectoryParseError {
68    #[error("Can't parse an existing source directory from '{0}'")]
69    NoSrc(String),
70
71    #[error("Can't parse a destination directory from '{0}'")]
72    NoDst(String),
73}
74
75impl FromStr for Directory {
76    type Err = anyhow::Error;
77
78    fn from_str(s: &str) -> Result<Self> {
79        let mut parts = s.split(':');
80
81        let Some(src_part) = parts.next() else {
82            bail!(DirectoryParseError::NoSrc(s.to_string()))
83        };
84
85        let Some(dst_part) = parts.next() else {
86            bail!(DirectoryParseError::NoDst(s.to_string()))
87        };
88
89        let suffix = parts.next().map(|sfx| sfx.to_string());
90
91        let cwd = env::current_dir()?;
92        let src = cwd.join(src_part);
93        let dst = cwd.join(dst_part);
94
95        if !src.is_dir() {
96            bail!(DirectoryParseError::NoSrc(src_part.to_string()));
97        }
98
99        Ok(Self { src, dst, suffix })
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use expect_test::expect;
106
107    use super::*;
108
109    #[test]
110    fn test_directory_parsing_everything() {
111        // Source directory relative to CARGO_MANIFEST_DIR
112        let dir = Directory::from_str("src:dst:suffix").unwrap();
113
114        let cwd = env::current_dir().unwrap();
115        let src = cwd.join("src");
116        let dst = cwd.join("dst");
117
118        assert_eq!(
119            dir,
120            Directory {
121                src,
122                dst,
123                suffix: Some("suffix".to_string()),
124            }
125        )
126    }
127
128    #[test]
129    fn test_directory_parsing_no_suffix() {
130        // Source directory relative to CARGO_MANIFEST_DIR
131        let dir = Directory::from_str("src:dst").unwrap();
132
133        let cwd = env::current_dir().unwrap();
134        let src = cwd.join("src");
135        let dst = cwd.join("dst");
136
137        assert_eq!(
138            dir,
139            Directory {
140                src,
141                dst,
142                suffix: None,
143            }
144        )
145    }
146
147    #[test]
148    fn test_directory_parsing_no_dst() {
149        // Source directory relative to CARGO_MANIFEST_DIR
150        let err = Directory::from_str("src").unwrap_err();
151        expect!["Can't parse a destination directory from 'src'"].assert_eq(&format!("{err}"));
152    }
153
154    #[test]
155    fn test_directory_parsing_src_non_existent() {
156        // Source directory relative to CARGO_MANIFEST_DIR
157        let err = Directory::from_str("i_dont_exist:dst").unwrap_err();
158        expect!["Can't parse an existing source directory from 'i_dont_exist'"]
159            .assert_eq(&format!("{err}"));
160    }
161
162    #[test]
163    fn test_directory_parsing_empty() {
164        // Source directory relative to CARGO_MANIFEST_DIR
165        let err = Directory::from_str("").unwrap_err();
166        expect!["Can't parse a destination directory from ''"].assert_eq(&format!("{err}"));
167    }
168}