commit 3b4d891b5c407796aa9e413132db07323440551b
parent 6df7de115843504ddfb52829dce36f79ab11edea
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 6 Jul 2024 12:19:06 -0700
Fix CLI output for relative SSH destinations
Previously host:~/.coliru/foo would be printed as host:foo, and transfer
messages weren't printed at all for run commands.
Diffstat:
| M | src/core.rs | | | 90 | ++++++++++++++++++++++++++++++++++++++++++++----------------------------------- |
| M | src/ssh.rs | | | 62 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
| M | tests/ssh.rs | | | 7 | +++++++ |
3 files changed, 111 insertions(+), 48 deletions(-)
diff --git a/src/core.rs b/src/core.rs
@@ -5,10 +5,27 @@ use std::path::Path;
use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file};
use super::tags::tags_match;
use super::local::{copy_file, link_file, run_command};
-use super::ssh::{send_command, send_staged_files, stage_file};
+use super::ssh::{resolve_path, send_command, send_staged_files, stage_file};
use tempfile::tempdir;
-/// Execute the steps in a coliru manifest file according to a set of tag rules
+/// The base directory for SSH installs, relative to the home directory
+const SSH_INSTALL_DIR: &str = ".coliru";
+
+/// Performs a dry-run check inside of a loop
+///
+/// Will print `(DRY RUN)` and then continue to next loop iteration if `dry_run`
+/// evaluates to `true`.
+macro_rules! check_dry_run {
+ ($dry_run:expr) => {
+ if $dry_run {
+ println!(" (DRY RUN)");
+ continue;
+ }
+ println!("");
+ }
+}
+
+/// Executes the steps in a coliru manifest file according to a set of tag rules
pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str,
dry_run: bool, copy: bool) {
@@ -50,29 +67,32 @@ pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str,
}
}
-/// Execute a set of copy commands
+/// Executes a set of copy commands
fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path,
dry_run: bool, step_str: &str) {
for copy in copies {
- if host == "" {
- print!("{} Copy {} to {}", step_str, copy.src, copy.dst);
+ // Resolve relative dst paths if installing over SSH
+ let _dst = if host != "" {
+ resolve_path(©.dst, &format!("~/{}", SSH_INSTALL_DIR))
} else {
- print!("{} Copy {} to {}:{}", step_str, copy.src, host, copy.dst);
- }
+ copy.dst.clone()
+ };
- if dry_run {
- println!(" (DRY RUN)");
- continue;
+ print!("{} Copy {} to ", step_str, copy.src);
+ if host != "" {
+ print!("{}:", host);
}
- println!("");
+ print!("{}", _dst);
+
+ check_dry_run!(dry_run);
if host == "" {
- if let Err(why) = copy_file(©.src, ©.dst) {
+ if let Err(why) = copy_file(©.src, &_dst) {
eprintln!(" Error: {}", why);
}
} else {
- if let Err(why) = stage_file(©.src, ©.dst, staging_dir) {
+ if let Err(why) = stage_file(©.src, &_dst, staging_dir) {
eprintln!(" Error: {}", why);
}
}
@@ -85,16 +105,12 @@ fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path,
}
}
-/// Execute a set of link commands
+/// Executes a set of link commands
fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
for link in links {
print!("{} Link {} to {}", step_str, link.src, link.dst);
- if dry_run {
- println!(" (DRY RUN)");
- continue;
- }
- println!("");
+ check_dry_run!(dry_run);
if let Err(why) = link_file(&link.src, &link.dst) {
eprintln!(" Error: {}", why);
@@ -102,43 +118,37 @@ fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
}
}
-/// Execute a set of run commands
+/// Executes a set of run commands
fn execute_runs(runs: &[RunOptions], tag_rules: &[String], host: &str,
staging_dir: &Path, dry_run: bool, step_str: &str) {
- if !dry_run && host != "" {
- for run in runs {
- if let Err(why) = stage_file(&run.src, &run.src, staging_dir) {
- eprintln!("Error: {}", why);
- }
- }
+ if host != "" {
+ // Copy scripts to remote machine
+ let run_copies: Vec<CopyLinkOptions> = runs.iter().map(|x| {
+ CopyLinkOptions { src: x.src.clone(), dst: x.src.clone() }
+ }).collect();
- if let Err(why) = send_staged_files(staging_dir, host) {
- eprintln!("Error: {}", why);
- }
+ execute_copies(&run_copies, host, staging_dir, dry_run, step_str);
}
for run in runs {
- let postfix = run.postfix.replace("$COLIRU_RULES", &tag_rules.join(" "));
+ let postfix = run.postfix.replace("$COLIRU_RULES",
+ &tag_rules.join(" "));
let cmd = format!("{} {} {}", run.prefix, run.src, postfix);
- if host == "" {
- print!("{} Run {}", step_str, cmd);
- } else {
- print!("{} Run {} on {}", step_str, cmd, host);
- }
- if dry_run {
- println!(" (DRY RUN)");
- continue;
+ print!("{} Run {}", step_str, cmd);
+ if host != "" {
+ print!(" on {}", host);
}
- println!("");
+
+ check_dry_run!(dry_run);
if host == "" {
if let Err(why) = run_command(&cmd) {
eprintln!(" Error: {}", why);
}
} else {
- let ssh_cmd = format!("cd .coliru && {}", &cmd);
+ let ssh_cmd = format!("cd {} && {}", SSH_INSTALL_DIR, &cmd);
if let Err(why) = send_command(&ssh_cmd, host) {
eprintln!(" Error: {}", why);
}
diff --git a/src/ssh.rs b/src/ssh.rs
@@ -17,28 +17,44 @@ use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf};
use std::process::Command;
use super::local::copy_file;
+/// Makes a relative path absolute according to a certain base directory
+///
+/// Paths begining with tildes are interpreted as absolute paths.
+///
+/// ```
+/// assert_eq!(resolve_path("dir1/foo", "~/dir2"), "~/dir2/dir1/foo");
+/// assert_eq!(resolve_path("/dir1/foo", "~/dir2"), "/dir1/foo");
+/// assert_eq!(resolve_path("~/dir1/foo", "~/dir2"), "~/dir1/foo");
+/// ```
+pub fn resolve_path(src: &str, dir: &str) -> String {
+ if !src.starts_with("~") && Path::new(src).is_relative() {
+ return format!("{dir}/{src}")
+ }
+ src.to_owned()
+}
+
/// Copies a file to an SCP staging directory
///
-/// Tildes will be expanded to the remote user's home directory. Relative paths
-/// are interpreted relative to `~/.coliru`.
+/// Tildes are expanded and relative paths are interpreted relative to the
+/// remote user's home directory.
///
/// ```
-/// // Prepare to transfer foo to ~/foo, bar to /bar, and baz to ~/.coliru/baz
+/// // Prepare to transfer foo to ~/foo, bar to /bar, and baz to ~/baz
/// let staging_dir = Path::new("/tmp/staging");
/// stage_file("foo", "~/foo", staging_dir);
/// stage_file("bar", "/bar", staging_dir);
/// stage_file("baz", "baz", staging_dir);
/// ```
+pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(),
+ String> {
-pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String> {
// Staging directories are used to copy multiple files at once while
// automatically creating missing directories on the remote machine. The
// example code above produces the following staging directory layout:
//
// /tmp/staging/
// ├── home/
- // │ ├── .coliru
- // │ │ └── baz
+ // │ ├── baz
// │ └── foo
// └── root/
// └── bar
@@ -54,7 +70,7 @@ pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String
.into();
// Resolve relative paths to home staging directory:
- _dst = home_dir.join(".coliru").join(_dst);
+ _dst = home_dir.join(_dst);
// Resolve other absolute paths to root staging directory:
if !_dst.starts_with(home_dir) {
@@ -158,6 +174,36 @@ mod tests {
use std::fs;
#[test]
+ fn test_resolve_path_relative() {
+ let result = resolve_path("dir1/foo", "~/dir2");
+
+ assert_eq!(result, "~/dir2/dir1/foo");
+ }
+
+ #[test]
+ fn test_resolve_path_tilde() {
+ let result = resolve_path("~/dir1/foo", "~/dir2");
+
+ assert_eq!(result, "~/dir1/foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_resolve_path_absolute() {
+ let result = resolve_path("/dir1/foo", "~/dir2");
+
+ assert_eq!(result, "/dir1/foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "windows")]
+ fn test_resolve_path_absolute() {
+ let result = resolve_path("C:\\dir1\\foo", "~/dir2");
+
+ assert_eq!(result, "C:\\dir1\\foo");
+ }
+
+ #[test]
fn test_stage_file_tilde() {
let tmp = setup_integration("test_stage_file_tilde");
@@ -180,7 +226,7 @@ mod tests {
let src = tmp.local.join("foo");
let dst = "dir/bar";
- let dst_real = tmp.local.join("home").join(".coliru").join("dir")
+ let dst_real = tmp.local.join("home").join("dir")
.join("bar");
let staging = &tmp.local;
write_file(&src, "contents of foo");
diff --git a/tests/ssh.rs b/tests/ssh.rs
@@ -15,6 +15,7 @@ fn test_ssh_standard() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_standard/.gitconfig
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_standard/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_standard/.vimrc
+[2/3] Copy test_ssh_standard/script.sh to {SSH_HOST}:~/.coliru/test_ssh_standard/script.sh
[2/3] Run sh test_ssh_standard/script.sh arg1 linux on {SSH_HOST}
script.sh called with arg1 linux
");
@@ -43,6 +44,7 @@ fn test_ssh_run_alternate_tag_rules_1() {
let expected = format!("\
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.vimrc
+[2/3] Copy test_ssh_run_alternate_tag_rules_1/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_alternate_tag_rules_1/script.sh
[2/3] Run sh test_ssh_run_alternate_tag_rules_1/script.sh arg1 linux ^windows on {SSH_HOST}
script.sh called with arg1 linux ^windows
");
@@ -72,6 +74,7 @@ fn test_ssh_run_alternate_tag_rules_2() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.gitconfig
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.vimrc
+[2/3] Copy test_ssh_run_alternate_tag_rules_2/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_alternate_tag_rules_2/script.sh
[2/3] Run sh test_ssh_run_alternate_tag_rules_2/script.sh arg1 macos on {SSH_HOST}
script.sh called with arg1 macos
");
@@ -100,6 +103,7 @@ fn test_ssh_dry_run() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_dry_run/.gitconfig (DRY RUN)
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_dry_run/.bashrc (DRY RUN)
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_dry_run/.vimrc (DRY RUN)
+[2/3] Copy test_ssh_dry_run/script.sh to {SSH_HOST}:~/.coliru/test_ssh_dry_run/script.sh (DRY RUN)
[2/3] Run sh test_ssh_dry_run/script.sh arg1 linux on {SSH_HOST} (DRY RUN)
");
assert_eq!(&stderr_to_string(&mut cmd), "");
@@ -128,6 +132,7 @@ fn test_ssh_copy() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_copy/.gitconfig
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_copy/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_copy/.vimrc
+[2/3] Copy test_ssh_copy/script.sh to {SSH_HOST}:~/.coliru/test_ssh_copy/script.sh
[2/3] Run sh test_ssh_copy/script.sh arg1 linux on {SSH_HOST}
script.sh called with arg1 linux
");
@@ -158,6 +163,7 @@ fn test_ssh_run_failure() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_failure/.gitconfig
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_failure/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_failure/.vimrc
+[2/3] Copy test_ssh_run_failure/script.sh to {SSH_HOST}:~/.coliru/test_ssh_run_failure/script.sh
[2/3] Run sh test_ssh_run_failure/script.sh arg1 linux on {SSH_HOST}
");
let expected_stderr = " Error: SSH exited with exit status: 1\n";
@@ -186,6 +192,7 @@ fn test_ssh_missing_file() {
[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_missing_file/.gitconfig
[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_missing_file/.bashrc
[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_missing_file/.vimrc
+[2/3] Copy test_ssh_missing_file/script.sh to {SSH_HOST}:~/.coliru/test_ssh_missing_file/script.sh
[2/3] Run sh test_ssh_missing_file/script.sh arg1 linux on {SSH_HOST}
script.sh called with arg1 linux
");