oseda_cli/cmd/
deploy.rs

1use std::{env, error::Error, fs, path::Path};
2
3use clap::Args;
4
5use crate::{config, github::git};
6
7/// Options for the `oseda deploy` command
8#[derive(Args, Debug)]
9pub struct DeployOptions {
10    fork_url: String,
11}
12
13struct SshUrl(String);
14
15/// string deref
16impl std::ops::Deref for SshUrl {
17    type Target = String;
18
19    fn deref(&self) -> &Self::Target {
20        &self.0
21    }
22}
23
24/// Convert a standard HTTPS GitHub URL to SSH format
25///
26/// # Arguments
27/// * `value` - a String starting with `https://github.com/...`
28///
29/// # Returns
30/// * `Ok(SshUrl)` if parsing succeeds
31/// * `Err` if the format is not recognized
32impl TryFrom<String> for SshUrl {
33    type Error = Box<dyn Error>;
34
35    fn try_from(value: String) -> Result<Self, Self::Error> {
36        // https://github.com/ReeseHatfield/oseda-lib-testing/
37        // into
38        // git@github.com:ReeseHatfield/oseda-lib-testing.git
39        let suffix = value
40            .strip_prefix("https://github.com/")
41            .ok_or("Could not get SSH URL")?;
42
43        Ok(SshUrl(format!(
44            "git@github.com:{}.git",
45            suffix.trim_end_matches('/')
46        )))
47    }
48}
49
50/// Deploys an Oseda project to the provided fork URL
51///
52/// # Arguments
53/// * `opts` - options with the `fork_url` for the deployment target
54///
55/// # Returns
56/// * `Ok(())` on success
57/// * `Err` if any git, file, or config step fails, including a check failsure
58pub fn deploy(opts: DeployOptions) -> Result<(), Box<dyn Error>> {
59    let tmp_dir = tempfile::tempdir()?;
60    let repo_path = tmp_dir.path();
61
62    let ssh_url: SshUrl = opts.fork_url.try_into()?;
63
64    git(
65        repo_path,
66        &["clone", "--no-checkout", ssh_url.0.as_str(), "."],
67    )?;
68
69    println!("Running git with sparse checkout");
70    git(repo_path, &["sparse-checkout", "init", "--cone"])?;
71    git(repo_path, &["sparse-checkout", "set", "courses"])?;
72    git(repo_path, &["checkout"])?;
73
74    let course_name = get_current_dir_name()?;
75    let new_course_dir = repo_path.join("courses").join(&course_name);
76
77    copy_dir_all(env::current_dir()?, &new_course_dir)?;
78
79    // bails if config is bad
80    //
81    // force a no-skip-git
82    let conf = config::read_and_validate_config(false)?;
83
84    config::update_time(conf)?;
85    println!("Committing files to remote...");
86    git(repo_path, &["add", "."])?;
87    git(repo_path, &["commit", "-m", "Add new course"])?;
88    git(repo_path, &["push"])?;
89
90    println!("Project sucessfully pushed to remote.");
91
92    Ok(())
93}
94
95/// Util fn to get teh current working directory name
96///
97/// # Returns
98/// * `Ok(String)` with the directory name
99/// * `Err` if the name failed to be extracted
100fn get_current_dir_name() -> Result<String, Box<dyn Error>> {
101    // this is like really stupid to have this, since
102    // this logic is basically already used in `check`
103    // but really most of that logic should be moved to a config.rs file
104    // but until then, I am just reading the cwd with this
105    let cwd = env::current_dir()?;
106    let name = cwd
107        .file_name()
108        .ok_or("couldn't get directory name")?
109        .to_string_lossy()
110        .to_string();
111    Ok(name)
112}
113
114/// Recursivly copy a directory
115/// https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust
116fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
117    fs::create_dir_all(&dst)?;
118    for entry in fs::read_dir(src)? {
119        let entry = entry?;
120        let ty = entry.file_type()?;
121        if ty.is_dir() {
122            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
123        } else {
124            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
125        }
126    }
127    Ok(())
128}