use anyhow::{Context, bail};
use log::info;
use std::{
fs,
path::{Path, PathBuf},
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{hooks, util::get_tapes_default_path};
pub fn run(
source: &str,
name: Option<String>,
branch: Option<String>,
dir: Option<String>,
no_hooks: bool,
) -> Result<(), anyhow::Error> {
let source_path = PathBuf::from(source);
let is_path = source_path.exists();
let tape_name = if let Some(n) = name {
n.trim().to_string()
} else if is_path {
source_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("tape")
.to_string()
} else {
source
.trim_end_matches('/')
.trim_end_matches(".git")
.rsplit('/')
.next()
.unwrap_or("tape")
.to_string()
};
let tape_name = tape_name.to_lowercase();
let tapes_dir = get_tapes_default_path()?;
let target_dir = tapes_dir.join(&tape_name);
if target_dir.exists() {
bail!(
"Tape {} already exists at {}",
tape_name,
target_dir.display()
);
}
fs::create_dir_all(&tapes_dir)?;
let temp_clone_dir = tapes_dir.join(format!(
".tmp-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
if is_path {
println!("Copying local tape '{tape_name}' from {source_path:?}...",);
let src_to_copy = if let Some(ref d) = dir {
let sub = source_path.join(d);
if !sub.exists() {
bail!("Directory '{}' does not exist in source path", d);
}
sub
} else {
source_path
};
copy_dir(&src_to_copy, &target_dir)?;
} else {
println!("Copying tape '{tape_name}' from {source}");
let url = normalize_git_url(source);
let mut cmd = Command::new("git");
cmd.arg("clone");
if let Some(b) = branch {
cmd.arg("--branch").arg(b);
}
let clone_destination = if dir.is_some() {
&temp_clone_dir
} else {
&target_dir
};
cmd.arg(&url).arg(clone_destination);
info!("git command: {:?}", cmd);
let status = cmd.status().context("Failed to run git clone")?;
if !status.success() {
let _ = fs::remove_dir_all(&temp_clone_dir);
let _ = fs::remove_dir_all(&target_dir);
bail!("git clone failed with status: {status}");
}
if let Some(ref d) = dir {
let d = d.trim_start_matches('/');
let sub = temp_clone_dir.join(d);
info!("Requested subdir: {sub:?}");
if !sub.exists() {
let _ = fs::remove_dir_all(&temp_clone_dir);
bail!("Directory '{}' does not exist in the cloned repository", d);
}
copy_dir(&sub, &target_dir)?;
let _ = fs::remove_dir_all(&temp_clone_dir);
}
}
let manifest_path = target_dir.join("tape.toml");
if !manifest_path.exists() {
let example_toml = format!(
r#"[tape]
name = "{tape_name}"
desc = "Tape installed from {source}"
# [hooks]
# insert = [
# # "hyprctl reload",
# # "pkill waybar",
# # "qs -c {tape_name} -d" # -d in daemon mode cause hooks run in the current shell session not in the background
# ]
# eject = [
# # "hyprctl reload",
# # "qs kill",
# # "waybar &"
# ]
"#
);
fs::write(manifest_path, example_toml)?;
}
if let Some(data) = dirs::data_dir() {
let tapes_data = data.join("tapes");
let _ = fs::create_dir_all(&tapes_data);
let record = format!("name={tape_name}, source={source}\n");
let _ = fs::OpenOptions::new()
.create(true)
.append(true)
.open(tapes_data.join("install_records.txt"))
.and_then(|mut f| std::io::Write::write_all(&mut f, record.as_bytes()));
}
let manifest = crate::util::load_manifest(&target_dir)?;
hooks::run_install_hooks(manifest, no_hooks)?;
println!(
"Successfully installed tape '{tape_name}' to {}",
target_dir.display()
);
Ok(())
}
fn normalize_git_url(source: &str) -> String {
if !source.contains("://") && !source.starts_with("git@") && source.contains('/') {
format!("https://github.com/{}.git", source.trim_end_matches(".git"))
} else {
source.to_string()
}
}
fn copy_dir(source: &Path, destination: &Path) -> Result<(), anyhow::Error> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let path = entry.path();
if !path.exists() {
continue;
}
let target = destination.join(entry.file_name());
if path.is_dir() {
copy_dir(&path, &target)?;
} else {
fs::copy(&path, &target)?;
}
}
Ok(())
}