fix: retry commits when signing helper is missing
This commit is contained in:
@@ -394,7 +394,7 @@ interface PulseCommit {
|
||||
| `history.rs` | File history | `git log` — last 20 commits per file |
|
||||
| `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` |
|
||||
| `status.rs` | File diff | `git diff`, fallback to `--cached`, then synthetic for untracked |
|
||||
| `commit.rs` | Commit | `git add -A && git commit -m "..."` |
|
||||
| `commit.rs` | Commit | `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit |
|
||||
| `remote.rs` | Pull / Push | `git pull --rebase` / `git push` |
|
||||
| `connect.rs` | Add remote | Adds `origin`, fetches it, validates history compatibility, and only starts tracking when the remote is safe |
|
||||
| `conflict.rs` | Conflict resolution | Detect conflicts, resolve with ours/theirs/manual |
|
||||
|
||||
@@ -443,7 +443,7 @@ On first launch, `useOnboarding` checks if the default vault exists. If not, it
|
||||
- **Open an existing folder** → system file picker
|
||||
- **Get started with a template** → pick a parent folder, then call `create_getting_started_vault()` with the derived `.../Getting Started` child path so the cloned vault opens into the populated repo root immediately
|
||||
|
||||
When an opened folder is not yet a git repo, `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.
|
||||
When an opened folder is not yet a git repo, `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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
struct CommitFailure {
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
}
|
||||
|
||||
/// Commit all changes with a message.
|
||||
pub fn git_commit(vault_path: &str, message: &str) -> Result<String, String> {
|
||||
let vault = Path::new(vault_path);
|
||||
@@ -17,26 +22,65 @@ pub fn git_commit(vault_path: &str, message: &str) -> Result<String, String> {
|
||||
return Err(format!("git add failed: {}", stderr));
|
||||
}
|
||||
|
||||
// Commit
|
||||
let commit = Command::new("git")
|
||||
match run_commit(vault, message, false) {
|
||||
Ok(stdout) => Ok(stdout),
|
||||
Err(failure) if is_commit_signing_failure(&failure.detail()) => {
|
||||
run_commit(vault, message, true).map_err(|retry_failure| {
|
||||
format!(
|
||||
"git commit signing failed; retried without signing but git commit still failed: {}",
|
||||
retry_failure.detail()
|
||||
)
|
||||
})
|
||||
}
|
||||
Err(failure) => Err(format!("git commit failed: {}", failure.detail())),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_commit(vault: &Path, message: &str, disable_signing: bool) -> Result<String, CommitFailure> {
|
||||
let mut command = Command::new("git");
|
||||
if disable_signing {
|
||||
command.args(["-c", "commit.gpgsign=false"]);
|
||||
}
|
||||
|
||||
let commit = command
|
||||
.args(["commit", "-m", message])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run git commit: {}", e))?;
|
||||
.map_err(|e| CommitFailure {
|
||||
stdout: String::new(),
|
||||
stderr: format!("Failed to run git commit: {}", e),
|
||||
})?;
|
||||
|
||||
if !commit.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&commit.stderr);
|
||||
let stdout = String::from_utf8_lossy(&commit.stdout);
|
||||
// git writes "nothing to commit" to stdout, not stderr
|
||||
let detail = if stderr.trim().is_empty() {
|
||||
stdout
|
||||
} else {
|
||||
stderr
|
||||
};
|
||||
return Err(format!("git commit failed: {}", detail.trim()));
|
||||
if commit.status.success() {
|
||||
return Ok(String::from_utf8_lossy(&commit.stdout).to_string());
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&commit.stdout).to_string())
|
||||
Err(CommitFailure {
|
||||
stdout: String::from_utf8_lossy(&commit.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&commit.stderr).to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
impl CommitFailure {
|
||||
fn detail(&self) -> String {
|
||||
// git writes "nothing to commit" to stdout, not stderr.
|
||||
let detail = if self.stderr.trim().is_empty() {
|
||||
&self.stdout
|
||||
} else {
|
||||
&self.stderr
|
||||
};
|
||||
detail.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_commit_signing_failure(detail: &str) -> bool {
|
||||
let lower = detail.to_ascii_lowercase();
|
||||
lower.contains("cannot run gpg")
|
||||
|| lower.contains("gpg failed to sign")
|
||||
|| lower.contains("failed to sign the data")
|
||||
|| lower.contains("gpg.ssh")
|
||||
|| (lower.contains("failed to write commit object")
|
||||
&& (lower.contains("sign") || lower.contains("gpg")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -84,4 +128,53 @@ mod tests {
|
||||
"Error should mention 'nothing to commit'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_commit_retries_without_signing_when_gpg_is_missing() {
|
||||
let dir = setup_git_repo();
|
||||
let vault = dir.path();
|
||||
let vp = vault.to_str().unwrap();
|
||||
|
||||
Command::new("git")
|
||||
.args(["config", "commit.gpgsign", "true"])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.unwrap();
|
||||
Command::new("git")
|
||||
.args(["config", "gpg.program", "/missing/tolaria-test-gpg"])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::write(vault.join("signed-config.md"), "# Signed config\n").unwrap();
|
||||
|
||||
let result = git_commit(vp, "Commit with broken signing config");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"commit should retry unsigned when signing helper is missing: {result:?}"
|
||||
);
|
||||
|
||||
let log = Command::new("git")
|
||||
.args(["log", "--oneline", "-1"])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(String::from_utf8_lossy(&log.stdout).contains("Commit with broken signing config"));
|
||||
|
||||
let config = Command::new("git")
|
||||
.args(["config", "commit.gpgsign"])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(String::from_utf8_lossy(&config.stdout).trim(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_signing_failure_detection_is_specific() {
|
||||
assert!(is_commit_signing_failure(
|
||||
"error: cannot run gpg: No such file or directory\nfatal: failed to write commit object"
|
||||
));
|
||||
assert!(!is_commit_signing_failure(
|
||||
"On branch main\nnothing to commit, working tree clean"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user