refactor: consolidate git remote helpers
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
57
src-tauri/src/git/command.rs
Normal file
57
src-tauri/src/git/command.rs
Normal 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]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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")] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user