cut/
args.rs

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