diff --git a/src-tauri/src/git/connect.rs b/src-tauri/src/git/connect.rs index 0f9f3a8b..728ebc37 100644 --- a/src-tauri/src/git/connect.rs +++ b/src-tauri/src/git/connect.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::Output; use super::credentials::request_remote_credentials; +use super::remote_config::{configure_origin_remote, list_configured_remotes}; use super::{ensure_author_config, git_command}; const DEFAULT_REMOTE_NAME: &str = "origin"; @@ -105,7 +106,7 @@ pub fn git_add_remote(vault_path: &str, remote_url: &str) -> Result Result { } fn list_remotes(vault: &Path) -> Result, String> { - let output = git_output(vault, &["remote"])?; - - if !output.status.success() { - return Err(command_error("git remote", &output)); - } - - Ok(stdout_lines(&output)) + list_configured_remotes(vault) } fn unset_upstream(vault: &Path) { diff --git a/src-tauri/src/git/mod.rs b/src-tauri/src/git/mod.rs index 4c440b95..3a0562a3 100644 --- a/src-tauri/src/git/mod.rs +++ b/src-tauri/src/git/mod.rs @@ -7,6 +7,7 @@ mod dates; mod history; mod pulse; mod remote; +mod remote_config; mod status; use std::ffi::{OsStr, OsString}; diff --git a/src-tauri/src/git/remote_config.rs b/src-tauri/src/git/remote_config.rs new file mode 100644 index 00000000..91321014 --- /dev/null +++ b/src-tauri/src/git/remote_config.rs @@ -0,0 +1,77 @@ +use std::path::Path; +use std::process::Output; + +use super::git_command; + +const DEFAULT_FETCH_REFSPEC: &str = "+refs/heads/*:refs/remotes/origin/*"; +const REMOTE_URL_CONFIG_PATTERN: &str = r"^remote\..*\.url$"; +const ORIGIN_URL_CONFIG_KEY: &str = "remote.origin.url"; +const ORIGIN_FETCH_CONFIG_KEY: &str = "remote.origin.fetch"; + +pub(super) fn list_configured_remotes(vault: &Path) -> Result, String> { + let output = git_command() + .args(["config", "--get-regexp", REMOTE_URL_CONFIG_PATTERN]) + .current_dir(vault) + .output() + .map_err(|e| format!("Failed to inspect git remotes: {e}"))?; + + if output.status.code() == Some(1) { + return Ok(Vec::new()); + } + if !output.status.success() { + return Err(command_error("git config --get-regexp", &output)); + } + + Ok(stdout_lines(&output) + .into_iter() + .filter_map(|line| remote_name_from_url_config(&line)) + .collect()) +} + +fn remote_name_from_url_config(line: &str) -> Option { + let (key, value) = line.split_once(' ')?; + if value.trim().is_empty() { + return None; + } + key.strip_prefix("remote.") + .and_then(|name| name.strip_suffix(".url")) + .filter(|name| !name.is_empty()) + .map(ToString::to_string) +} + +pub(super) fn configure_origin_remote(vault: &Path, remote_url: &str) -> Result<(), String> { + run_git_config(vault, ORIGIN_URL_CONFIG_KEY, remote_url)?; + run_git_config(vault, ORIGIN_FETCH_CONFIG_KEY, DEFAULT_FETCH_REFSPEC) +} + +fn run_git_config(vault: &Path, key: &str, value: &str) -> Result<(), String> { + let output = git_command() + .args(["config", "--local", "--replace-all", key, value]) + .current_dir(vault) + .output() + .map_err(|e| format!("Failed to run git config: {e}"))?; + + if output.status.success() { + return Ok(()); + } + + Err(command_error("git config", &output)) +} + +fn stdout_lines(output: &Output) -> Vec { + String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(ToString::to_string) + .collect() +} + +fn command_error(command: &str, output: &Output) -> String { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + if stderr.is_empty() { + format!("{command} failed") + } else { + stderr + } +} diff --git a/src-tauri/tests/git_connect_no_remote.rs b/src-tauri/tests/git_connect_no_remote.rs new file mode 100644 index 00000000..3e81bb69 --- /dev/null +++ b/src-tauri/tests/git_connect_no_remote.rs @@ -0,0 +1,59 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + +use tempfile::TempDir; +use tolaria_lib::git::{git_add_remote, git_commit, git_remote_status}; + +fn run_git(path: &Path, args: &[&str]) { + let output = Command::new("git") + .args(args) + .current_dir(path) + .output() + .unwrap(); + assert!( + output.status.success(), + "git {:?} failed: {}", + args, + String::from_utf8_lossy(&output.stderr) + ); +} + +fn setup_repo() -> TempDir { + let dir = TempDir::new().unwrap(); + run_git(dir.path(), &["init", "-b", "main"]); + dir +} + +fn setup_bare_repo() -> TempDir { + let dir = TempDir::new().unwrap(); + run_git(dir.path(), &["init", "--bare"]); + dir +} + +#[test] +fn git_add_remote_ignores_name_only_origin_config() { + let local = setup_repo(); + fs::write(local.path().join("note.md"), "# Note\n").unwrap(); + git_commit(local.path().to_str().unwrap(), "initial").unwrap(); + + run_git(local.path(), &["config", "remote.origin.prune", "true"]); + let remote_names = Command::new("git") + .args(["remote"]) + .current_dir(local.path()) + .output() + .unwrap(); + assert!(String::from_utf8_lossy(&remote_names.stdout).contains("origin")); + + let bare = setup_bare_repo(); + let result = git_add_remote( + local.path().to_str().unwrap(), + bare.path().to_str().unwrap(), + ) + .unwrap(); + + assert_eq!(result.status, "connected"); + assert!(git_remote_status(local.path().to_str().unwrap()) + .unwrap() + .has_remote); +}