fix: set git identity before remote connect
This commit is contained in:
@@ -461,7 +461,7 @@ On first launch, `useOnboarding` checks if the default vault exists. If not, it
|
||||
|
||||
When an opened folder is not yet a git repo, Tolaria shows a dismissible Git setup dialog and a persistent `Git disabled` status-bar warning. Markdown scanning, note browsing, note editing, and search continue normally. Git-dependent surfaces (history, changes, commit, sync, conflict resolution, remotes, AutoGit, and auto-sync) stay unavailable until the user explicitly initializes Git from the dialog, the status-bar warning, or the `Initialize Git for Current Vault` command-palette action.
|
||||
|
||||
When the user enables Git later, `init_git_repo` runs `git init`, ensures Tolaria's default `.gitignore`, stages the vault, and writes the initial `Initial vault setup` commit. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures.
|
||||
When the user enables Git later, `init_git_repo` runs `git init`, ensures Tolaria's default `.gitignore`, stages the vault, and writes the initial `Initial vault setup` commit. Before app-managed setup and remote-connection commits, Tolaria ensures the vault has local `user.name` / `user.email` values, falling back to `Tolaria <vault@tolaria.app>` when the vault has no local Git identity yet. That app-managed setup commit explicitly disables commit signing for the single command so inherited global or local `commit.gpgsign` preferences cannot strand onboarding when GPG is missing or misconfigured. Later `git_commit` calls honor the user's signing configuration first, then retry the same app-managed commit once with `commit.gpgsign=false` only when Git reports a signing-helper failure, so working GPG/SSH signing setups continue to sign while broken GPG setups do not create repeated opaque commit failures.
|
||||
|
||||
Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnboardingPrompt`. That prompt reads `useAiAgentsStatus` so first launch surfaces whether Claude Code and Codex are installed, offers per-agent install links when they are missing, and stores local dismissal so the prompt does not repeat on every launch.
|
||||
|
||||
@@ -587,7 +587,7 @@ flowchart TD
|
||||
|
||||
If the current vault is not a Git repository, Tolaria treats Git as disabled instead of degraded. The status bar replaces changes, commit, sync, remote, conflict, and history controls with a `Git disabled` warning that reopens Git setup. Command registration follows the same state: only `Initialize Git for Current Vault` is available in the Git group, while pull, commit, changes, conflict, and remote commands are hidden. `useAutoSync` is disabled for non-git vaults so the app does not run background Git commands against plain folders.
|
||||
|
||||
The same local-only state enables the explicit Add Remote flow. `AddRemoteModal` is reachable from the `No remote` chip and the command palette. The backend `git_add_remote` command adds `origin`, fetches it, refuses incompatible histories, and only enables tracking after a safe push or fast-forward-compatible check succeeds.
|
||||
The same local-only state enables the explicit Add Remote flow. `AddRemoteModal` is reachable from the `No remote` chip and the command palette. The backend `git_add_remote` command ensures the local author identity, adds `origin`, fetches it, refuses incompatible histories, and only enables tracking after a safe push or fast-forward-compatible check succeeds.
|
||||
|
||||
`useCommitFlow` also exposes `runAutomaticCheckpoint()`, a dialog-free commit path shared by AutoGit and the bottom-bar Commit button. `useAutoGit` watches the last editor activity plus app focus/visibility state, and when the vault is git-backed, all saves are flushed, and no unsaved edits remain, it triggers the same deterministic `Updated N note(s)` / `Updated N file(s)` commit message path after the configured idle or inactive thresholds. The bottom-bar quick action reuses that checkpoint flow after forcing a save first, so manual quick commits and scheduled AutoGit commits stay aligned on message generation and push behavior.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::process::Output;
|
||||
|
||||
use super::git_command;
|
||||
use super::{ensure_author_config, git_command};
|
||||
|
||||
const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||
|
||||
@@ -92,6 +92,8 @@ pub fn git_add_remote(vault_path: &str, remote_url: &str) -> Result<GitAddRemote
|
||||
));
|
||||
}
|
||||
|
||||
ensure_author_config(vault)?;
|
||||
|
||||
let branch = current_branch(vault)?;
|
||||
if branch.is_empty() {
|
||||
return Ok(connect_result(
|
||||
@@ -444,6 +446,28 @@ mod tests {
|
||||
git_commit(path.to_str().unwrap(), message).unwrap();
|
||||
}
|
||||
|
||||
fn clear_local_author(path: &Path) {
|
||||
for key in ["user.name", "user.email"] {
|
||||
StdCommand::new("git")
|
||||
.args(["config", "--local", "--unset-all", key])
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn local_author_is_configured(path: &Path) -> bool {
|
||||
["user.name", "user.email"].into_iter().all(|key| {
|
||||
let output = StdCommand::new("git")
|
||||
.args(["config", "--local", key])
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
output.status.success() && !String::from_utf8_lossy(&output.stdout).trim().is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnect_all_remotes_removes_every_remote() {
|
||||
let dir = setup_git_repo();
|
||||
@@ -489,6 +513,26 @@ mod tests {
|
||||
assert_eq!((status.ahead, status.behind), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_add_remote_sets_local_identity_when_existing_repo_has_none() {
|
||||
let local = setup_git_repo();
|
||||
create_local_commit(local.path(), "note.md", "Local", "Initial local commit");
|
||||
clear_local_author(local.path());
|
||||
assert!(!local_author_is_configured(local.path()));
|
||||
|
||||
let bare = TempDir::new().unwrap();
|
||||
init_bare_remote(bare.path());
|
||||
|
||||
let result = git_add_remote(
|
||||
local.path().to_str().unwrap(),
|
||||
bare.path().to_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.status, "connected");
|
||||
assert!(local_author_is_configured(local.path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_add_remote_pushes_when_remote_is_the_local_branch_ancestor() {
|
||||
let local = setup_git_repo();
|
||||
|
||||
@@ -129,21 +129,23 @@ fn git_command_label<'a>(args: &'a [&'a str]) -> &'a str {
|
||||
}
|
||||
|
||||
/// Set local user.name and user.email if not already configured.
|
||||
fn ensure_author_config(dir: &Path) -> Result<(), String> {
|
||||
pub(crate) fn ensure_author_config(dir: &Path) -> Result<(), String> {
|
||||
for (key, fallback) in [
|
||||
("user.name", "Tolaria"),
|
||||
("user.email", "vault@tolaria.app"),
|
||||
] {
|
||||
let check = git_command()
|
||||
.args(["config", key])
|
||||
let local = git_command()
|
||||
.args(["config", "--local", key])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check git config: {}", e))?;
|
||||
.map_err(|e| format!("Failed to check git config {key}: {e}"))?;
|
||||
|
||||
let value = String::from_utf8_lossy(&check.stdout);
|
||||
if !check.status.success() || value.trim().is_empty() {
|
||||
run_git(dir, &["config", key, fallback])?;
|
||||
let value = String::from_utf8_lossy(&local.stdout);
|
||||
if local.status.success() && !value.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
run_git(dir, &["config", "--local", key, fallback])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ fn refresh_cloned_vault_config_files(vault_path: &Path) -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_commit_identity(vault_path)?;
|
||||
crate::git::ensure_author_config(vault_path)?;
|
||||
crate::git::git_commit(
|
||||
path_to_utf8(vault_path, "Vault path")?,
|
||||
"Initialize Tolaria config files",
|
||||
@@ -593,38 +593,6 @@ fn vault_has_pending_changes(vault_path: &Path) -> Result<bool, String> {
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_commit_identity(vault_path: &Path) -> Result<(), String> {
|
||||
for (key, fallback) in [
|
||||
("user.name", "Tolaria"),
|
||||
("user.email", "vault@tolaria.app"),
|
||||
] {
|
||||
let output = crate::hidden_command("git")
|
||||
.args(["config", key])
|
||||
.current_dir(vault_path)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to inspect git config {key}: {e}"))?;
|
||||
|
||||
if output.status.success() && !String::from_utf8_lossy(&output.stdout).trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let set_output = crate::hidden_command("git")
|
||||
.args(["config", key, fallback])
|
||||
.current_dir(vault_path)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to set git config {key}: {e}"))?;
|
||||
|
||||
if !set_output.status.success() {
|
||||
return Err(format!(
|
||||
"git config {key} failed: {}",
|
||||
String::from_utf8_lossy(&set_output.stderr).trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user