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#[derive(Args, Debug)]
8pub struct DeployOptions {
9    fork_url: String,
10}
11
12struct SshUrl(String);
13
14impl std::ops::Deref for SshUrl {
15    type Target = String;
16
17    fn deref(&self) -> &Self::Target {
18        &self.0
19    }
20}
21
22impl TryFrom<String> for SshUrl {
23    type Error = Box<dyn Error>;
24
25    fn try_from(value: String) -> Result<Self, Self::Error> {
26        // https://github.com/ReeseHatfield/oseda-lib-testing/
27        // into
28        // git@github.com:ReeseHatfield/oseda-lib-testing.git
29        let suffix = value
30            .strip_prefix("https://github.com/")
31            .ok_or("Could not get SSH URL")?;
32
33        Ok(SshUrl(format!(
34            "git@github.com:{}.git",
35            suffix.trim_end_matches('/')
36        )))
37    }
38}
39
40pub fn deploy(opts: DeployOptions) -> Result<(), Box<dyn Error>> {
41    let tmp_dir = tempfile::tempdir()?;
42    let repo_path = tmp_dir.path();
43
44    let ssh_url: SshUrl = opts.fork_url.try_into()?;
45
46    git(
47        repo_path,
48        &["clone", "--no-checkout", ssh_url.0.as_str(), "."],
49    )?;
50
51    println!("Running git with sparse checkout");
52    git(repo_path, &["sparse-checkout", "init", "--cone"])?;
53    git(repo_path, &["sparse-checkout", "set", "courses"])?;
54    git(repo_path, &["checkout"])?;
55
56    let course_name = get_current_dir_name()?;
57    let new_course_dir = repo_path.join("courses").join(&course_name);
58
59    copy_dir_all(env::current_dir()?, &new_course_dir)?;
60
61    // bails if config is bad
62    //
63    // force a no-skip-git
64    let conf = config::read_and_validate_config(false)?;
65
66    config::update_time(conf)?;
67    println!("Committing files to remote...");
68    git(repo_path, &["add", "."])?;
69    git(repo_path, &["commit", "-m", "Add new course"])?;
70    git(repo_path, &["push"])?;
71
72    println!("Project sucessfully pushed to remote.");
73
74    Ok(())
75}
76
77// this is like really stupid to have this, since
78// this logic is basically already used in `check`
79// but really most of that logic should be moved to a config.rs file
80// but until then, I am just reading the cwd with this
81fn get_current_dir_name() -> Result<String, Box<dyn Error>> {
82    let cwd = env::current_dir()?;
83    let name = cwd
84        .file_name()
85        .ok_or("couldn't get directory name")?
86        .to_string_lossy()
87        .to_string();
88    Ok(name)
89}
90
91// https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust
92fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
93    fs::create_dir_all(&dst)?;
94    for entry in fs::read_dir(src)? {
95        let entry = entry?;
96        let ty = entry.file_type()?;
97        if ty.is_dir() {
98            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
99        } else {
100            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
101        }
102    }
103    Ok(())
104}