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
22 changes: 22 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,28 @@ pub(crate) fn run_push(preflight: &env::PreflightContext) -> ExitCode {

for index in state.completed_pushes..state.push_branches.len() {
let branch = &state.push_branches[index];

let remote_ref = format!("refs/remotes/origin/{branch}");
let local_ref = format!("refs/heads/{branch}");
match gitops::is_ancestor(&remote_ref, &local_ref) {
Ok(true) => {}
Ok(false) => {
if let Err(save_error) = sync_state::save_push(&state) {
eprintln!("error: {save_error}");
return ExitCode::from(1);
}
eprintln!(
"error: remote branch `origin/{branch}` has commits not in local `{branch}`; \
pull or rebase to integrate remote changes before pushing"
);
return ExitCode::from(1);
}
Err(message) => {
eprintln!("error: {message}");
return ExitCode::from(1);
}
}

println!(
"Pushing branch {}/{}: {}",
index + 1,
Expand Down
14 changes: 14 additions & 0 deletions tests/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ if [[ "${1:-}" == "merge-base" && "${2:-}" == "--is-ancestor" ]]; then
descendant_branch="${descendant#refs/heads/}"
descendant_branch="${descendant_branch#refs/remotes/origin/}"

if [[ -n "${STCK_TEST_NOT_ANCESTOR_PAIRS:-}" ]]; then
IFS=',' read -ra pairs <<< "${STCK_TEST_NOT_ANCESTOR_PAIRS}"
for pair in "${pairs[@]}"; do
IFS=':' read -r pa pd <<< "${pair}"
if [[ "${ancestor_branch}" == "${pa}" && "${descendant_branch}" == "${pd}" ]]; then
exit 1
fi
done
fi

if [[ -n "${STCK_TEST_ANCESTOR_PAIRS:-}" ]]; then
IFS=',' read -ra pairs <<< "${STCK_TEST_ANCESTOR_PAIRS}"
for pair in "${pairs[@]}"; do
Expand All @@ -238,6 +248,10 @@ if [[ "${1:-}" == "merge-base" && "${2:-}" == "--is-ancestor" ]]; then
done
fi

if [[ "${ancestor_branch}" == "${descendant_branch}" ]]; then
exit 0
fi

case "${ancestor_branch}:${descendant_branch}" in
main:feature-base|main:feature-branch|main:feature-child) exit 0 ;;
feature-base:feature-branch|feature-base:feature-child) exit 0 ;;
Expand Down
28 changes: 28 additions & 0 deletions tests/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,34 @@ fn sync_then_push_after_squash_merge_produces_correct_retargets() {
));
}

#[test]
fn push_aborts_when_remote_has_commits_not_in_local_branch() {
let (temp, mut cmd) = stck_cmd_with_stubbed_tools();
let log_path = log_path(&temp, "stck-push-diverged.log");
cmd.env("STCK_TEST_LOG", log_path.as_os_str());
cmd.env("STCK_TEST_NEEDS_PUSH_BRANCHES", "feature-branch");
// Simulate remote/feature-branch NOT being an ancestor of local feature-branch,
// meaning the remote has commits that would be lost on force push.
cmd.env(
"STCK_TEST_NOT_ANCESTOR_PAIRS",
"feature-branch:feature-branch",
);
cmd.arg("push");

cmd.assert()
.code(1)
.stderr(predicate::str::contains("feature-branch"));

// The force push should NOT have been executed.
if log_path.exists() {
let log = fs::read_to_string(&log_path).expect("push log should be readable");
assert!(
!log.contains("push --force-with-lease origin feature-branch"),
"push should not force-push a branch whose remote has diverged"
);
}
}

#[test]
fn push_blocked_while_sync_state_exists() {
let (temp, mut sync) = stck_cmd_with_stubbed_tools();
Expand Down
Loading