From d6ccf12f51c00a2b9f013d0bd43c13502ee4b9c1 Mon Sep 17 00:00:00 2001 From: landon Date: Wed, 9 Sep 2026 19:41:21 -0500 Subject: [PATCH] Don't report a first sync as failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A repository being pushed for the first time has no upstream, so the pull leg failed with "There is no tracking information for the current branch" and dragged the whole sync's status down — the app said "Sync finished with errors" even though the push had gone through and set the upstream itself. The upstream is now checked up front. Without one there is nothing to rebase onto, so the pull is skipped with a note in the log and the push sets the upstream directly, rather than being run once to fail and then retried. The argument choice moves to push_args so it can be asserted without a repository on disk. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ --- src/gitsync.rs | 65 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/src/gitsync.rs b/src/gitsync.rs index 01b6072..f5bdaca 100644 --- a/src/gitsync.rs +++ b/src/gitsync.rs @@ -64,6 +64,26 @@ fn has_origin(workspace: &Path) -> bool { ok && out.lines().any(|l| l.trim() == "origin") } +/// True if the current branch has an upstream to pull from. +/// +/// A repository being pushed for the first time has none, and `rev-parse` +/// exits non-zero rather than printing anything. +fn has_upstream(workspace: &Path) -> bool { + run_git(workspace, &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).0 +} + +/// The push for a branch, which has to set the upstream the first time. +/// +/// Kept separate from running it so it can be asserted in tests, like +/// [`origin_args`]. +pub fn push_args(branch: &str, has_upstream: bool) -> Vec<&str> { + if has_upstream { + vec!["push"] + } else { + vec!["push", "--set-upstream", "origin", branch] + } +} + /// The URL `origin` points at, if the remote exists. /// /// Read from the repository rather than from this app's settings: a remote @@ -140,23 +160,22 @@ pub fn sync(workspace: &Path, message: &str) -> GitOutcome { } if has_origin(workspace) { - let (p_ok, p_out) = run_git(workspace, &["pull", "--rebase", "--autostash"]); - log.push_str(&p_out); - ok &= p_ok; - - let (push_ok, push_out) = run_git(workspace, &["push"]); - log.push_str(&push_out); - // A failed push often just means no upstream is set yet; surface it but - // do not treat a missing upstream as a hard failure of the whole sync. - if !push_ok && push_out.contains("no upstream") { - let branch = current_branch(workspace); - let (u_ok, u_out) = - run_git(workspace, &["push", "--set-upstream", "origin", &branch]); - log.push_str(&u_out); - ok &= u_ok; + let upstream = has_upstream(workspace); + if upstream { + let (p_ok, p_out) = run_git(workspace, &["pull", "--rebase", "--autostash"]); + log.push_str(&p_out); + ok &= p_ok; } else { - ok &= push_ok; + // Nothing to rebase onto yet, and pulling anyway fails with "no + // tracking information" — which used to make a first sync report + // errors even though its push had gone through fine. + log.push_str("(no upstream branch yet — nothing to pull)\n"); } + + let branch = current_branch(workspace); + let (push_ok, push_out) = run_git(workspace, &push_args(&branch, upstream)); + log.push_str(&push_out); + ok &= push_ok; } else { log.push_str("(no 'origin' remote configured — committed locally only)\n"); } @@ -208,6 +227,22 @@ fn strip_command_echo(text: &str) -> String { mod tests { use super::*; + #[test] + fn a_first_push_sets_the_upstream_and_later_ones_do_not() { + // Without this the first push fails with "has no upstream branch". + assert_eq!( + push_args("main", false), + vec!["push", "--set-upstream", "origin", "main"] + ); + // Once it is set, a plain push is what respects the tracking config. + assert_eq!(push_args("main", true), vec!["push"]); + // Whatever the branch is actually called. + assert_eq!( + push_args("draft-2", false), + vec!["push", "--set-upstream", "origin", "draft-2"] + ); + } + #[test] fn setting_a_remote_adds_it_only_when_it_is_missing() { // Fresh repository: the remote has to be created.