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.