Open repositories on another machine over ssh; keep the window responsive while git runs #5
@@ -12,11 +12,11 @@ pub enum Location {
|
|||||||
|
|
||||||
/// Reads `[user@]host:path` as a remote location. Everything else is local: text where the
|
/// Reads `[user@]host:path` as a remote location. Everything else is local: text where the
|
||||||
/// part before the first `:` is a single character (a Windows drive like `C:`) or contains
|
/// part before the first `:` is a single character (a Windows drive like `C:`) or contains
|
||||||
/// `/` or `\`, and text that is the whole name of a folder that exists inside `cwd`.
|
/// `/` or `\`, text starting with `-` (ssh would read it as an option), and text that is the whole name of a folder that exists inside `cwd`.
|
||||||
pub fn parse(text: &str, cwd: &Path) -> Location {
|
pub fn parse(text: &str, cwd: &Path) -> Location {
|
||||||
let local = || Location::Local(PathBuf::from(text));
|
let local = || Location::Local(PathBuf::from(text));
|
||||||
let Some((target, path)) = text.split_once(':') else { return local() };
|
let Some((target, path)) = text.split_once(':') else { return local() };
|
||||||
if target.chars().count() <= 1 || target.contains('/') || target.contains('\\') || cwd.join(text).exists() {
|
if target.chars().count() <= 1 || target.starts_with('-') || target.contains('/') || target.contains('\\') || cwd.join(text).exists() {
|
||||||
return local();
|
return local();
|
||||||
}
|
}
|
||||||
Location::Ssh { target: target.to_string(), path: path.to_string() }
|
Location::Ssh { target: target.to_string(), path: path.to_string() }
|
||||||
@@ -45,10 +45,10 @@ pub fn quote(word: &str) -> String {
|
|||||||
format!("'{}'", word.replace('\'', r"'\''"))
|
format!("'{}'", word.replace('\'', r"'\''"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The command line to run on the remote machine: `git -C <path>` followed by `args`.
|
/// The command for a shell that reads quotes the standard way: `git -C <path>` followed by
|
||||||
/// A path starting with `~/` keeps the `~` outside the quotes so the shell still
|
/// `args`. A path starting with `~/` keeps the `~` outside the quotes so the shell still
|
||||||
/// replaces it with the home folder.
|
/// replaces it with the home folder.
|
||||||
pub fn remote_command(path: &str, args: &[&str]) -> String {
|
pub fn sh_command(path: &str, args: &[&str]) -> String {
|
||||||
let path = match path.strip_prefix("~/") {
|
let path = match path.strip_prefix("~/") {
|
||||||
Some(rest) => format!("~/{}", quote(rest)),
|
Some(rest) => format!("~/{}", quote(rest)),
|
||||||
None => quote(path),
|
None => quote(path),
|
||||||
@@ -61,6 +61,24 @@ pub fn remote_command(path: &str, args: &[&str]) -> String {
|
|||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps a command so every login shell on the server (sh, bash, zsh, fish) runs it the same
|
||||||
|
/// way. Fish reads quotes differently from the others, so the command is not sent as written:
|
||||||
|
/// every byte becomes a backslash and three octal digits, which no shell changes inside single
|
||||||
|
/// quotes, and `printf` turns them back into the command for `sh` to run.
|
||||||
|
pub fn shell_neutral(sh_command: &str) -> String {
|
||||||
|
let mut wrapped = String::from("printf '");
|
||||||
|
for byte in sh_command.bytes() {
|
||||||
|
wrapped.push_str(&format!("\\{byte:03o}"));
|
||||||
|
}
|
||||||
|
wrapped.push_str("' | sh");
|
||||||
|
wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The command line to give to ssh to run `git -C <path> <args>` on the remote machine.
|
||||||
|
pub fn remote_command(path: &str, args: &[&str]) -> String {
|
||||||
|
shell_neutral(&sh_command(path, args))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -89,6 +107,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_a_target_that_ssh_would_read_as_an_option_local() {
|
||||||
|
let text = "-oProxyCommand=x:y";
|
||||||
|
assert_eq!(parse(text, &cwd()), Location::Local(PathBuf::from(text)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_an_existing_local_folder_with_a_colon_local() {
|
fn keeps_an_existing_local_folder_with_a_colon_local() {
|
||||||
let temp = std::env::temp_dir().join(format!("gites-location-{}", std::process::id()));
|
let temp = std::env::temp_dir().join(format!("gites-location-{}", std::process::id()));
|
||||||
@@ -122,12 +146,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapped_words_survive_every_login_shell() {
|
||||||
|
let words = [
|
||||||
|
"plain", "with space", "it's", "\"double\"", "$HOME", "a;b", "*", "%s", "line\nbreak", "a\\b", "\\",
|
||||||
|
"a\\'b", "x\\",
|
||||||
|
];
|
||||||
|
let mut shells_run = Vec::new();
|
||||||
|
for shell in ["sh", "bash", "zsh", "fish"] {
|
||||||
|
if Command::new(shell).arg("-c").arg("true").output().is_err() {
|
||||||
|
assert_ne!(shell, "sh", "sh must be installed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
shells_run.push(shell);
|
||||||
|
for word in words {
|
||||||
|
let wrapper = shell_neutral(&format!("printf %s {}", quote(word)));
|
||||||
|
let out = Command::new(shell).arg("-c").arg(&wrapper).output().unwrap();
|
||||||
|
assert_eq!(String::from_utf8(out.stdout).unwrap(), word, "{shell} with {word:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(shells_run.contains(&"sh"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_remote_command_uses_only_plain_characters() {
|
||||||
|
let command = remote_command("~/x y", &["status", "it's"]);
|
||||||
|
assert!(command.starts_with("printf '") && command.ends_with("' | sh"));
|
||||||
|
assert!(command.chars().all(|c| "printf'\\0123456789 |sh".contains(c)), "{command}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_the_remote_command() {
|
fn builds_the_remote_command() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
remote_command("/srv/my app", &["log", "--format=%H"]),
|
sh_command("/srv/my app", &["log", "--format=%H"]),
|
||||||
"git -C '/srv/my app' 'log' '--format=%H'"
|
"git -C '/srv/my app' 'log' '--format=%H'"
|
||||||
);
|
);
|
||||||
assert_eq!(remote_command("~/x y", &["status"]), "git -C ~/'x y' 'status'");
|
assert_eq!(sh_command("~/x y", &["status"]), "git -C ~/'x y' 'status'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user