Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/staged/src-tauri/src/git/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ pub struct UpstreamGitState {
pub ahead: u32,
pub behind: u32,
pub merge_base_sha: Option<String>,
/// Number of commits `origin/{base_branch}` is ahead of `origin/{branch_name}`.
///
/// Non-zero means the remote branch tip is stale relative to base. The
/// timeline UI uses this to disable "Rebase onto Origin" when rebasing
/// onto a behind-base remote tip would skip the latest base commits and
/// almost always be the wrong action.
pub behind_base: u32,
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
Expand Down Expand Up @@ -157,6 +164,7 @@ impl FastGitState {
ahead: 0,
behind: 0,
merge_base_sha: None,
behind_base: 0,
},
base: BaseGitState {
r#ref: base_ref,
Expand Down Expand Up @@ -521,6 +529,7 @@ where
fn compute_upstream_state<F>(
run_git: &F,
upstream_ref: String,
base_ref: &str,
head_sha: Option<&str>,
upstream_known_missing: bool,
) -> UpstreamGitState
Expand All @@ -543,6 +552,7 @@ where
ahead: 0,
behind: 0,
merge_base_sha: None,
behind_base: 0,
};
}

Expand All @@ -562,6 +572,15 @@ where
};
let merge_base_sha = merge_base(run_git, "HEAD", &upstream_ref);

// How many commits `origin/{base}` is ahead of `origin/{branch}`. When the
// upstream ref *is* the base ref this is always 0, which is the correct
// signal for "branch tracking its own base" workflows.
let behind_base = if upstream_ref == base_ref {
0
} else {
rev_count(run_git, &format!("{upstream_ref}..{base_ref}"))
};

UpstreamGitState {
r#ref: upstream_ref,
exists,
Expand All @@ -570,6 +589,7 @@ where
ahead,
behind,
merge_base_sha,
behind_base,
}
}

Expand Down Expand Up @@ -652,6 +672,7 @@ where
.unwrap_or(false);
let upstream_ref = origin_ref_for_branch(branch_name);
let base_ref = origin_ref_for_branch(base_branch);
let base_ref_for_upstream = base_ref.clone();

// Phase 2: Upstream and base state computations are independent of each
// other but both need fetch to have completed (for up-to-date remote refs)
Expand All @@ -661,6 +682,7 @@ where
compute_upstream_state(
&run_git,
upstream_ref,
&base_ref_for_upstream,
head_sha.as_deref(),
refresh.upstream_known_missing,
)
Expand Down Expand Up @@ -803,12 +825,14 @@ pub fn complete_local_git_state(
refresh_refs_if_needed(&cache_key, &run_git, branch_name, base_branch, fetch_mode);
let upstream_ref = origin_ref_for_branch(branch_name);
let base_ref = origin_ref_for_branch(base_branch);
let base_ref_for_upstream = base_ref.clone();

let (upstream, base) = std::thread::scope(|s| {
let u = s.spawn(|| {
compute_upstream_state(
&run_git,
upstream_ref,
&base_ref_for_upstream,
fast.head_sha.as_deref(),
refresh.upstream_known_missing,
)
Expand Down Expand Up @@ -904,6 +928,11 @@ const BATCH_GIT_STATE_SCRIPT: &str = concat!(
"if [ -n \"$up_sha\" ] && [ -n \"$head_sha\" ]; then\n",
" printf 'UP_COUNTS=%s\\n' \"$(git rev-list --left-right --count \"$head_sha\"...\"$4\" 2>/dev/null || echo '0 0')\"\n",
" printf 'UP_MB=%s\\n' \"$(git merge-base \"$head_sha\" \"$4\" 2>/dev/null || true)\"\n",
" if [ \"$4\" != \"$5\" ]; then\n",
" printf 'UP_BEHIND_BASE=%s\\n' \"$(git rev-list --count \"$4\"..\"$5\" 2>/dev/null || echo 0)\"\n",
" else\n",
" printf 'UP_BEHIND_BASE=0\\n'\n",
" fi\n",
"fi\n",
// --- Base state ---
"base_sha=$(git rev-parse --verify \"$5\" 2>/dev/null || true)\n",
Expand All @@ -929,6 +958,7 @@ struct BatchGitStateOutput {
up_ahead: u32,
up_behind: u32,
up_merge_base: Option<String>,
up_behind_base: u32,
base_sha: Option<String>,
base_behind: u32,
}
Expand All @@ -944,6 +974,7 @@ fn parse_batch_git_state_output(raw: &str) -> BatchGitStateOutput {
let mut up_ahead = 0u32;
let mut up_behind = 0u32;
let mut up_merge_base = None;
let mut up_behind_base = 0u32;
let mut base_sha = None;
let mut base_behind = 0u32;

Expand Down Expand Up @@ -994,6 +1025,8 @@ fn parse_batch_git_state_output(raw: &str) -> BatchGitStateOutput {
if !v.is_empty() {
up_merge_base = Some(v.to_string());
}
} else if let Some(val) = line.strip_prefix("UP_BEHIND_BASE=") {
up_behind_base = parse_u32(Some(val.trim()));
} else if let Some(val) = line.strip_prefix("BASE_SHA=") {
let v = val.trim();
if !v.is_empty() {
Expand All @@ -1016,6 +1049,7 @@ fn parse_batch_git_state_output(raw: &str) -> BatchGitStateOutput {
up_ahead,
up_behind,
up_merge_base,
up_behind_base,
base_sha,
base_behind,
}
Expand Down Expand Up @@ -1364,6 +1398,7 @@ where
ahead: parsed.up_ahead,
behind: parsed.up_behind,
merge_base_sha: parsed.up_merge_base,
behind_base: parsed.up_behind_base,
};

let base = BaseGitState {
Expand Down Expand Up @@ -1395,6 +1430,7 @@ where
ahead: 0,
behind: 0,
merge_base_sha: None,
behind_base: 0,
},
base: BaseGitState {
r#ref: base_ref,
Expand Down
26 changes: 26 additions & 0 deletions apps/staged/src-tauri/src/git/state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,29 @@ fn detects_base_branch_moved() {

assert_eq!(state.base.commits_since_fork, 1);
}

#[test]
fn upstream_behind_base_zero_when_origin_branch_is_caught_up() {
let (_origin, clone) = remote_backed_feature();
let state = state(&clone.path, FetchMode::Force);

// Branch and its origin tip are both forked from main, so origin/main is
// not ahead of origin/feature.
assert_eq!(state.upstream.behind_base, 0);
}

#[test]
fn upstream_behind_base_counts_origin_base_commits_missing_from_origin_branch() {
let (origin, clone) = remote_backed_feature();
// Land two commits on origin/main *after* origin/feature was pushed, so
// origin/feature is now two commits behind origin/main.
origin.run_git(&["checkout", "main"]);
origin.write_file("base1.txt", "one\n");
origin.commit("base 1");
origin.write_file("base2.txt", "two\n");
origin.commit("base 2");

let state = state(&clone.path, FetchMode::Force);

assert_eq!(state.upstream.behind_base, 2);
}
Loading