Skip to content
Open
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
29 changes: 25 additions & 4 deletions src/stack_pr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,13 @@ def command_land(args: CommonArgs) -> None:
# already be there from the metadata that commits need to have by that
# point.
set_base_branches(st, args.target)

# If the current branch contains commits from the stack, rebase it onto the
# updated stack tip in the end so it reuses the same rewritten commits as
# the PR branches.
top_commit = st[-1].commit.commit_id()
need_to_rebase_current = is_ancestor(top_commit, current_branch, verbose=args.verbose)

print_stack(st, links=args.hyperlinks)

# Verify that the stack is correct before trying to land it.
Expand All @@ -1293,13 +1300,25 @@ def command_land(args: CommonArgs) -> None:
# All good, land the bottommost PR!
land_pr(st[0], remote=args.remote, target=args.target, verbose=args.verbose)

# Refresh the local view of the target branch after the merge.
run_shell_command(["git", "fetch", "--prune", args.remote], quiet=not args.verbose)

current_branch_base = f"{args.remote}/{args.target}"

# The rest of the stack now needs to be rebased.
if len(st) > 1:
log(h("Rebasing the rest of the stack"), level=1)
prs_to_rebase = st[1:]
print_stack(prs_to_rebase, links=args.hyperlinks, level=1)
for e in prs_to_rebase:
rebase_pr(e, remote=args.remote, target=args.target, verbose=args.verbose)

# Resolve the rewritten tip to a commit hash before the local stack
# branches are deleted below.
current_branch_base = get_command_output(
["git", "rev-parse", prs_to_rebase[-1].head]
).strip()

# Change the target of the new bottom-most PR in the stack to 'target'
run_shell_command(
["gh", "pr", "edit", prs_to_rebase[0].pr, "-B", args.target],
Expand All @@ -1317,10 +1336,12 @@ def command_land(args: CommonArgs) -> None:
["git", "rebase", f"{args.remote}/{args.target}", args.target],
quiet=not args.verbose,
)
run_shell_command(
["git", "rebase", f"{args.remote}/{args.target}", current_branch],
quiet=not args.verbose,
)

rebase_base = current_branch_base if need_to_rebase_current else f"{args.remote}/{args.target}"
cmd = ["git", "rebase", rebase_base, current_branch]
if need_to_rebase_current:
cmd.append("--committer-date-is-author-date")
run_shell_command(cmd, quiet=not args.verbose)

log(h(blue("SUCCESS!")))

Expand Down
112 changes: 112 additions & 0 deletions tests/test_land.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import sys
from pathlib import Path
from types import SimpleNamespace

sys.path.append(str(Path(__file__).parent.parent / "src"))

from stack_pr import cli


def make_args() -> cli.CommonArgs:
return cli.CommonArgs(
base="base",
head="head",
remote="origin",
target="main",
hyperlinks=False,
verbose=False,
branch_name_template="$USERNAME/stack/$ID",
show_tips=False,
land_disabled=False,
)


def make_entry(head: str, pr: str, commit_id: str) -> SimpleNamespace:
return SimpleNamespace(
head=head,
pr=pr,
commit=SimpleNamespace(commit_id=lambda: commit_id),
)


def test_land_rebases_current_branch_onto_updated_stack_tip(
monkeypatch,
) -> None:
commands: list[list[str]] = []
stack = [make_entry("stack/1", "pr-1", "commit-1"), make_entry("stack/2", "pr-2", "commit-2")]

monkeypatch.setattr(cli, "get_current_branch_name", lambda: "feature")
monkeypatch.setattr(cli, "should_update_local_base", lambda **kwargs: False)
monkeypatch.setattr(cli, "get_stack", lambda **kwargs: stack)
monkeypatch.setattr(cli, "set_base_branches", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "print_stack", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "verify", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "land_pr", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "rebase_pr", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "delete_local_branches", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "branch_exists", lambda branch: False)
monkeypatch.setattr(
cli,
"get_command_output",
lambda cmd, **kwargs: "rebased-stack-tip\n"
if cmd == ["git", "rev-parse", "stack/2"]
else "",
)
monkeypatch.setattr(
cli,
"is_ancestor",
lambda ancestor, descendant, *, verbose: ancestor == "commit-2"
and descendant == "feature",
)

def fake_run_shell_command(cmd, *, quiet, check=True, **kwargs):
commands.append(list(cmd))
return SimpleNamespace(returncode=0, stdout=b"", stderr=b"")

monkeypatch.setattr(cli, "run_shell_command", fake_run_shell_command)

cli.command_land(make_args())

assert [
"git",
"rebase",
"rebased-stack-tip",
"feature",
"--committer-date-is-author-date",
] in commands
assert ["git", "rebase", "stack/2", "feature", "--committer-date-is-author-date"] not in commands
assert ["git", "rebase", "origin/main", "feature"] not in commands


def test_land_refreshes_remote_target_before_rebasing_current_branch(
monkeypatch,
) -> None:
commands: list[list[str]] = []
stack = [make_entry("stack/1", "pr-1", "commit-1")]

monkeypatch.setattr(cli, "get_current_branch_name", lambda: "feature")
monkeypatch.setattr(cli, "should_update_local_base", lambda **kwargs: False)
monkeypatch.setattr(cli, "get_stack", lambda **kwargs: stack)
monkeypatch.setattr(cli, "set_base_branches", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "print_stack", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "verify", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "land_pr", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "delete_local_branches", lambda *args, **kwargs: None)
monkeypatch.setattr(cli, "branch_exists", lambda branch: False)
monkeypatch.setattr(
cli,
"is_ancestor",
lambda ancestor, descendant, *, verbose: ancestor == "commit-1"
and descendant == "feature",
)

def fake_run_shell_command(cmd, *, quiet, check=True, **kwargs):
commands.append(list(cmd))
return SimpleNamespace(returncode=0, stdout=b"", stderr=b"")

monkeypatch.setattr(cli, "run_shell_command", fake_run_shell_command)

cli.command_land(make_args())

assert ["git", "fetch", "--prune", "origin"] in commands
assert ["git", "rebase", "origin/main", "feature", "--committer-date-is-author-date"] in commands