1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use std::env::current_dir;
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};

use anyhow::Context;
use clap::{value_parser, Parser, ValueEnum};
use derive_setters::Setters;

#[derive(ValueEnum, Default, Eq, PartialEq, Hash, Clone, Copy, Debug)]
pub enum RepoVisibility {
    Public,
    #[default]
    Private,
    Internal,
}

impl RepoVisibility {
    pub fn to_gh_create_repo_flag(&self) -> &'static str {
        match self {
            RepoVisibility::Public => "--public",
            RepoVisibility::Private => "--private",
            RepoVisibility::Internal => "--internal",
        }
    }
}

#[derive(Parser, Setters, Debug)]
#[setters(into)]
pub struct CreateRustGithubRepo {
    #[arg(long, short = 'n', help = "Repository name")]
    name: String,

    #[arg(long, short, help = "Target directory for cloning the repository (must include the repo name) (defaults to \"{current_dir}/{repo_name}\") (see also: --workspace)", value_parser = value_parser!(PathBuf))]
    dir: Option<PathBuf>,

    #[arg(long, short, help = "Parent of the target directory for cloning the repository (must NOT include the repo name). If this option is specified, then the repo is cloned to \"{workspace}/{repo_name}\". The --dir option overrides this option", value_parser = value_parser!(PathBuf))]
    workspace: Option<PathBuf>,

    #[arg(long, short = 'v', help = "Repository visibility", value_enum, default_value_t)]
    visibility: RepoVisibility,

    #[arg(long, short, help = "Source directory for configuration files", value_parser = value_parser!(PathBuf))]
    copy_configs_from: Option<PathBuf>,

    #[arg(long, help = "Message for git commit", default_value = "Add configs")]
    git_commit_message: String,

    #[arg(long, help = "Extra config file paths (relative to `source` directory)", value_delimiter = ',')]
    extra_configs: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `gh repo create`", value_delimiter = ' ')]
    gh_repo_create_args: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `gh repo clone`", value_delimiter = ' ')]
    gh_repo_clone_args: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `cargo init`", value_delimiter = ' ')]
    cargo_init_args: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `cargo build`", value_delimiter = ' ')]
    cargo_build_args: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `git commit`", value_delimiter = ' ')]
    git_commit_args: Vec<String>,

    #[arg(long, help = "Forwarded arguments for `git push`", value_delimiter = ' ')]
    git_push_args: Vec<String>,
}

impl CreateRustGithubRepo {
    pub fn run(self) -> anyhow::Result<()> {
        let current_dir = current_dir()?;
        let dir = self
            .dir
            .or_else(|| self.workspace.map(|workspace| workspace.join(&self.name)))
            .unwrap_or(current_dir.join(&self.name));

        // Create a GitHub repo
        exec(
            "gh",
            [
                "repo",
                "create",
                &self.name,
                self.visibility.to_gh_create_repo_flag(),
            ],
            self.gh_repo_create_args.into_iter(),
            &current_dir,
        )
        .context("Failed to create GitHub repository")?;

        // Clone the repo
        exec("gh", ["repo", "clone", &self.name, dir.to_str().unwrap()], self.gh_repo_clone_args.into_iter(), &current_dir).context("Failed to clone repository")?;

        // Run cargo init
        exec("cargo", ["init"], self.cargo_init_args.into_iter(), &dir).context("Failed to initialize Cargo project")?;

        if let Some(copy_configs_from) = self.copy_configs_from {
            let mut configs: Vec<String> = vec![];
            configs.extend(CONFIGS.iter().copied().map(ToOwned::to_owned));
            configs.extend(self.extra_configs);
            // Copy config files
            copy_configs(&copy_configs_from, &dir, configs).context("Failed to copy configuration files")?;
        }

        // Run cargo build
        exec("cargo", ["build"], self.cargo_build_args.into_iter(), &dir).context("Failed to build Cargo project")?;

        // Git commit
        exec("git", ["add", "."], Vec::<String>::new().into_iter(), &dir).context("Failed to stage files for commit")?;

        exec("git", ["commit", "-m", &self.git_commit_message], self.git_commit_args.into_iter(), &dir).context("Failed to commit changes")?;

        // Git push
        exec("git", ["push"], self.git_push_args.into_iter(), &dir).context("Failed to push changes")?;

        Ok(())
    }
}

pub fn exec(cmd: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>, extra_args: impl IntoIterator<Item = impl AsRef<OsStr>>, current_dir: impl AsRef<Path>) -> io::Result<ExitStatus> {
    Command::new(cmd)
        .args(args)
        .args(extra_args)
        .current_dir(current_dir)
        .spawn()?
        .wait()
        .and_then(|status| if status.success() { Ok(status) } else { Err(io::Error::new(io::ErrorKind::Other, format!("Process exited with with status {}", status))) })
}

pub fn copy_configs<P: Clone + AsRef<Path>>(source: &Path, target: &Path, configs: impl IntoIterator<Item = P>) -> io::Result<()> {
    for config in configs {
        let source_path = source.join(config.clone());
        let target_path = target.join(config);
        if source_path.exists() && !target_path.exists() {
            fs_err::copy(&source_path, &target_path)?;
        }
    }
    Ok(())
}

pub const CONFIGS: &[&str] = &[
    "clippy.toml",
    "rustfmt.toml",
    "Justfile",
    "lefthook.yml",
    ".lefthook.yml",
    "lefthook.yaml",
    ".lefthook.yaml",
    "lefthook.toml",
    ".lefthook.toml",
    "lefthook.json",
    ".lefthook.json",
];