refactor: consolidate git remote helpers

This commit is contained in:
lucaronin
2026-05-29 05:18:52 +02:00
parent 0234314945
commit b69cf8dd5b
7 changed files with 114 additions and 161 deletions

View File

@@ -713,7 +713,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|--------|---------|
| `vault/` | Vault scanning, caching, parsing, rename, image, migration |
| `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) |
| `git/` | Git operations (`commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`, `clone.rs`, `connect.rs`) |
| `git/` | Git operations and shared system-git helpers (`command.rs`, `remote_config.rs`, `commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`, `clone.rs`, `connect.rs`) |
| `search.rs` | Keyword search — walkdir-based vault file scan with Gitignored-content visibility filtering |
| `ai_agents.rs` | CLI-agent request normalization and adapter dispatch |
| `cli_agent_runtime.rs` | Shared CLI-agent request, prompt, subprocess, version, and MCP path helpers |

View File

@@ -228,8 +228,8 @@ tolaria/
│ │ ├── frontmatter/ # Frontmatter module
│ │ │ ├── mod.rs, yaml.rs, ops.rs
│ │ ├── git/ # Git module
│ │ │ ├── mod.rs, commit.rs, status.rs, history.rs, clone.rs, connect.rs
│ │ │ ├── conflict.rs, remote.rs, pulse.rs
│ │ │ ├── mod.rs, command.rs, remote_config.rs, commit.rs, status.rs
│ │ │ ├── history.rs, clone.rs, connect.rs, conflict.rs, remote.rs, pulse.rs
│ │ ├── telemetry.rs # Sentry init + path scrubber
│ │ ├── search.rs # Keyword search (walkdir-based)
│ │ ├── ai_agents.rs # CLI-agent request normalization + adapter dispatch

View File

@@ -0,0 +1,57 @@
use std::io;
use std::path::Path;
use std::process::Output;
use super::git_command;
pub(super) fn git_output(dir: &Path, args: &[&str]) -> io::Result<Output> {
git_command().args(args).current_dir(dir).output()
}
pub(super) fn git_output_result(dir: &Path, args: &[&str]) -> Result<Output, String> {
git_output(dir, args).map_err(|e| format!("Failed to run git {}: {e}", git_command_label(args)))
}
pub(super) fn run_git(dir: &Path, args: &[&str]) -> Result<(), String> {
let output = git_output_result(dir, args)?;
if output.status.success() {
return Ok(());
}
Err(stderr_text(&output))
}
pub(super) fn stdout_text(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
pub(super) fn stdout_lines(output: &Output) -> Vec<String> {
stdout_text(output)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect()
}
pub(super) fn stderr_text(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).trim().to_string()
}
pub(super) fn stderr_or_failure(command: &str, output: &Output) -> String {
let stderr = stderr_text(output);
if stderr.is_empty() {
format!("{command} failed")
} else {
stderr
}
}
pub(super) fn git_command_label<'a>(args: &'a [&'a str]) -> &'a str {
if args.first() == Some(&"-c") {
return args.get(2).copied().unwrap_or(args[0]);
}
args[0]
}

View File

@@ -2,9 +2,12 @@ use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Output;
use super::command::{
git_output, git_output_result, run_git, stderr_text, stdout_lines, stdout_text,
};
use super::credentials::request_remote_credentials;
use super::ensure_author_config;
use super::remote_config::{configure_origin_remote, list_configured_remotes};
use super::{ensure_author_config, git_command};
const DEFAULT_REMOTE_NAME: &str = "origin";
@@ -177,7 +180,7 @@ fn connect_result(status: ConnectStatus, message: impl Into<String>) -> GitAddRe
}
fn current_branch(vault: &Path) -> Result<String, String> {
let output = git_output(vault, &["branch", "--show-current"])?;
let output = git_output_result(vault, &["branch", "--show-current"])?;
if output.status.success() {
return Ok(stdout_text(&output));
@@ -191,24 +194,7 @@ fn list_remotes(vault: &Path) -> Result<Vec<String>, String> {
}
fn unset_upstream(vault: &Path) {
let _ = git_command()
.args(["branch", "--unset-upstream"])
.current_dir(vault)
.output();
}
fn run_git(vault: &Path, args: &[&str]) -> Result<(), String> {
let output = git_command()
.args(args)
.current_dir(vault)
.output()
.map_err(|e| format!("Failed to run git {}: {e}", args[0]))?;
if output.status.success() {
return Ok(());
}
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
let _ = git_output(vault, &["branch", "--unset-upstream"]);
}
fn fetch_remote(vault: &Path) -> Result<(), String> {
@@ -216,7 +202,7 @@ fn fetch_remote(vault: &Path) -> Result<(), String> {
}
fn list_remote_branches(vault: &Path) -> Result<Vec<String>, String> {
let output = git_output(
let output = git_output_result(
vault,
&[
"for-each-ref",
@@ -236,17 +222,17 @@ fn list_remote_branches(vault: &Path) -> Result<Vec<String>, String> {
}
fn histories_share_base(vault: &Path, connection: &RemoteConnection) -> bool {
git_command()
.args(["merge-base", "HEAD", connection.remote_branch.as_str()])
.current_dir(vault)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
git_output(
vault,
&["merge-base", "HEAD", connection.remote_branch.as_str()],
)
.map(|output| output.status.success())
.unwrap_or(false)
}
fn ahead_behind_counts(vault: &Path, connection: &RemoteConnection) -> Result<(u32, u32), String> {
let revision_range = format!("HEAD...{}", connection.remote_branch);
let output = git_output(
let output = git_output_result(
vault,
&["rev-list", "--left-right", "--count", &revision_range],
)?;
@@ -313,35 +299,10 @@ fn classify_connect_error(stderr: &str) -> GitAddRemoteResult {
)
}
fn git_output(vault: &Path, args: &[&str]) -> Result<Output, String> {
git_command()
.args(args)
.current_dir(vault)
.output()
.map_err(|e| format!("Failed to run git {}: {e}", args[0]))
}
fn command_error(command: &str, output: &Output) -> String {
format!("{command} failed: {}", stderr_text(output))
}
fn stderr_text(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).trim().to_string()
}
fn stdout_text(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn stdout_lines(output: &Output) -> Vec<String> {
stdout_text(output)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn is_auth_error(lower: &str) -> bool {
[
"authentication failed",

View File

@@ -1,4 +1,5 @@
mod clone;
mod command;
mod commit;
mod conflict;
mod connect;
@@ -268,11 +269,12 @@ fn commit_initial_vault_setup(dir: &Path) -> Result<(), String> {
/// Run a git command in the given directory, returning an error on failure.
fn run_git(dir: &Path, args: &[&str]) -> Result<(), String> {
let output = git_command()
.args(args)
.current_dir(dir)
.output()
.map_err(|e| format!("Failed to run git {}: {e}", git_command_label(args)))?;
let output = command::git_output(dir, args).map_err(|e| {
format!(
"Failed to run git {}: {e}",
command::git_command_label(args)
)
})?;
if output.status.success() {
return Ok(());
@@ -280,19 +282,11 @@ fn run_git(dir: &Path, args: &[&str]) -> Result<(), String> {
Err(format!(
"git {} failed: {}",
git_command_label(args),
command::git_command_label(args),
String::from_utf8_lossy(&output.stderr)
))
}
fn git_command_label<'a>(args: &'a [&'a str]) -> &'a str {
if args.first() == Some(&"-c") {
return args.get(2).copied().unwrap_or(args[0]);
}
args[0]
}
/// Set local user.name and user.email if not already configured.
pub(crate) fn ensure_author_config(dir: &Path) -> Result<(), String> {
for (key, fallback) in [("user.name", "Tolaria"), ("user.email", "vault@tolaria.md")] {

View File

@@ -1,15 +1,12 @@
use super::git_command;
use serde::{Deserialize, Serialize};
use std::path::Path;
use super::command::{git_output, git_output_result, stderr_text, stdout_text};
use super::conflict::get_conflict_files;
use super::remote_config::has_configured_remote;
const GIT_CONFIG_SUBCOMMAND: &str = "config";
const GIT_CONFIG_GET_REGEXP: &str = "--get-regexp";
const REMOTE_URL_CONFIG_PATTERN: &str = r"^remote\..*\.url$";
const NO_REMOTE_STATUS: &str = "no_remote";
const NO_REMOTE_MESSAGE: &str = "No remote configured";
const GIT_INSPECT_REMOTES_ERROR: &str = "Failed to inspect git remotes";
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct GitPullResult {
@@ -24,33 +21,7 @@ pub struct GitPullResult {
/// Check whether the vault repo has at least one remote configured.
pub fn has_remote(vault_path: &str) -> Result<bool, String> {
let vault = Path::new(vault_path);
remote_url_config_exists(vault)
}
fn remote_url_config_exists(vault: &Path) -> Result<bool, String> {
let output = git_command()
.args([
GIT_CONFIG_SUBCOMMAND,
GIT_CONFIG_GET_REGEXP,
REMOTE_URL_CONFIG_PATTERN,
])
.current_dir(vault)
.output()
.map_err(|e| format!("Failed to inspect git remotes: {}", e))?;
if !output.status.success() {
if output.status.code() == Some(1) {
return Ok(false);
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(if stderr.is_empty() {
GIT_INSPECT_REMOTES_ERROR.to_string()
} else {
stderr
});
}
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
has_configured_remote(vault)
}
/// Pull latest changes from remote. Uses --no-rebase to merge.
@@ -67,14 +38,11 @@ pub fn git_pull(vault_path: &str) -> Result<GitPullResult, String> {
});
}
let output = git_command()
.args(["pull", "--no-rebase"])
.current_dir(vault)
.output()
let output = git_output(vault, &["pull", "--no-rebase"])
.map_err(|e| format!("Failed to run git pull: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let stdout = stdout_text(&output);
let stderr = stderr_text(&output);
if output.status.success() {
if stdout.contains("Already up to date") || stdout.contains("Already up-to-date") {
@@ -161,18 +129,14 @@ pub fn git_remote_status(vault_path: &str) -> Result<GitRemoteStatus, String> {
}
// Fetch latest remote refs (silent, best-effort)
let _ = git_command()
.args(["fetch", "--quiet"])
.current_dir(vault)
.output();
let _ = git_output(vault, &["fetch", "--quiet"]);
let branch = current_branch(vault)?;
let output = git_command()
.args(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])
.current_dir(vault)
.output()
.map_err(|e| format!("Failed to run git rev-list: {}", e))?;
let output = git_output_result(
vault,
&["rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
)?;
if !output.status.success() {
// No upstream set — report 0/0
@@ -184,8 +148,8 @@ pub fn git_remote_status(vault_path: &str) -> Result<GitRemoteStatus, String> {
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split('\t').collect();
let stdout = stdout_text(&output);
let parts: Vec<&str> = stdout.split('\t').collect();
let ahead = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let behind = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
@@ -198,12 +162,9 @@ pub fn git_remote_status(vault_path: &str) -> Result<GitRemoteStatus, String> {
}
fn current_branch(vault: &Path) -> Result<String, String> {
let output = git_command()
.args(["branch", "--show-current"])
.current_dir(vault)
.output()
let output = git_output(vault, &["branch", "--show-current"])
.map_err(|e| format!("Failed to get branch: {}", e))?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
Ok(stdout_text(&output))
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@@ -333,14 +294,11 @@ pub fn git_push(vault_path: &str) -> Result<GitPushResult, String> {
});
}
let output = git_command()
.args(["push"])
.current_dir(vault)
.output()
.map_err(|e| format!("Failed to run git push: {}", e))?;
let output =
git_output(vault, &["push"]).map_err(|e| format!("Failed to run git push: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr_text(&output);
return Ok(classify_push_error(&stderr));
}
@@ -353,6 +311,7 @@ pub fn git_push(vault_path: &str) -> Result<GitPushResult, String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::git::git_command;
use crate::git::git_commit;
use crate::git::tests::{setup_git_repo, setup_remote_pair};
use std::fs;

View File

@@ -1,25 +1,28 @@
use std::path::Path;
use std::process::Output;
use super::git_command;
use super::command::{git_output, stderr_or_failure, stdout_lines};
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 has_configured_remote(vault: &Path) -> Result<bool, String> {
Ok(!list_configured_remotes(vault)?.is_empty())
}
pub(super) fn list_configured_remotes(vault: &Path) -> Result<Vec<String>, 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}"))?;
let output = git_output(
vault,
&["config", "--get-regexp", REMOTE_URL_CONFIG_PATTERN],
)
.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));
return Err(stderr_or_failure("git config --get-regexp", &output));
}
Ok(stdout_lines(&output)
@@ -45,33 +48,12 @@ pub(super) fn configure_origin_remote(vault: &Path, remote_url: &str) -> Result<
}
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()
let output = git_output(vault, &["config", "--local", "--replace-all", key, value])
.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> {
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
}
Err(stderr_or_failure("git config", &output))
}