oseda_cli/cmd/
deploy.rs

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