diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-05 21:25:50 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-05 22:12:07 +0000 |
| commit | f6d1e03dc99b3ea48cb6e4bd1d3371dd924a567a (patch) | |
| tree | 19fb789e83bd8d4ccc25f735a80cf41c0eccad17 /test_utils/src | |
| parent | 83b0886b97e2e90e328f91fcfaeb59726c93308f (diff) | |
fix(pr-checkout): require --force on diverged proposal branch
checkout_patch() previously re-applied the patch chain whenever the local
branch tip didn't match the published tip, silently overwriting local
amendments and rebased revisions without warning.
Now detects the relationship between local and published tips:
- up to date: check out as-is
- behind (local is ancestor of published): fast-forward, no flag needed
- local commits on top (published is ancestor of local): check out as-is
- diverged (neither ancestor): bail with guidance, --force to overwrite
- published tip not found locally and branch exists: same as diverged
Also adds --force flag to `ngit pr checkout` to explicitly opt in to
overwriting a diverged branch, covering both local amendments and
author force-pushes.
Bug discovered during test implementation in tests/ngit_pr_checkout.rs.
Diffstat (limited to 'test_utils/src')
| -rw-r--r-- | test_utils/src/lib.rs | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index ae0fa66..ccd9d80 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs | |||
| @@ -1532,6 +1532,93 @@ pub fn use_ngit_list_to_download_and_checkout_proposal_branch( | |||
| 1532 | Ok(()) | 1532 | Ok(()) |
| 1533 | } | 1533 | } |
| 1534 | 1534 | ||
| 1535 | /// Fetch proposals into the local cache and checkout the one matching | ||
| 1536 | /// `branch_name_in_event` using `ngit pr checkout <id>`. | ||
| 1537 | /// Requires relays to already be running. | ||
| 1538 | pub fn use_ngit_pr_checkout( | ||
| 1539 | test_repo: &GitTestRepo, | ||
| 1540 | branch_name_in_event: &str, | ||
| 1541 | ) -> Result<()> { | ||
| 1542 | // populate the local cache | ||
| 1543 | let mut p = CliTester::new_from_dir( | ||
| 1544 | &test_repo.dir, | ||
| 1545 | [ | ||
| 1546 | "--nsec", | ||
| 1547 | TEST_KEY_1_NSEC, | ||
| 1548 | "--password", | ||
| 1549 | TEST_PASSWORD, | ||
| 1550 | "--disable-cli-spinners", | ||
| 1551 | "pr", | ||
| 1552 | "list", | ||
| 1553 | ], | ||
| 1554 | ); | ||
| 1555 | p.expect_end_eventually()?; | ||
| 1556 | |||
| 1557 | // resolve the event id offline from the now-populated cache | ||
| 1558 | let output = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit")) | ||
| 1559 | .env("NGITTEST", "TRUE") | ||
| 1560 | .current_dir(&test_repo.dir) | ||
| 1561 | .args([ | ||
| 1562 | "--nsec", | ||
| 1563 | TEST_KEY_1_NSEC, | ||
| 1564 | "--password", | ||
| 1565 | TEST_PASSWORD, | ||
| 1566 | "--disable-cli-spinners", | ||
| 1567 | "pr", | ||
| 1568 | "list", | ||
| 1569 | "--json", | ||
| 1570 | "--offline", | ||
| 1571 | ]) | ||
| 1572 | .output()?; | ||
| 1573 | let stdout = String::from_utf8(output.stdout)?; | ||
| 1574 | let proposals: Vec<serde_json::Value> = serde_json::from_str(&stdout) | ||
| 1575 | .map_err(|e| anyhow::anyhow!("failed to parse pr list json: {e}\nstdout: {stdout}"))?; | ||
| 1576 | let entry = proposals | ||
| 1577 | .iter() | ||
| 1578 | .find(|p| { | ||
| 1579 | p["branch"] | ||
| 1580 | .as_str() | ||
| 1581 | .map(|b| b.starts_with(&format!("pr/{branch_name_in_event}("))) | ||
| 1582 | .unwrap_or(false) | ||
| 1583 | }) | ||
| 1584 | .ok_or_else(|| { | ||
| 1585 | anyhow::anyhow!( | ||
| 1586 | "no proposal found for branch {branch_name_in_event} in: {stdout}" | ||
| 1587 | ) | ||
| 1588 | })?; | ||
| 1589 | let proposal_id = entry["id"].as_str().unwrap_or_default().to_string(); | ||
| 1590 | |||
| 1591 | let status = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit")) | ||
| 1592 | .env("NGITTEST", "TRUE") | ||
| 1593 | .current_dir(&test_repo.dir) | ||
| 1594 | .args([ | ||
| 1595 | "--nsec", | ||
| 1596 | TEST_KEY_1_NSEC, | ||
| 1597 | "--password", | ||
| 1598 | TEST_PASSWORD, | ||
| 1599 | "--disable-cli-spinners", | ||
| 1600 | "pr", | ||
| 1601 | "checkout", | ||
| 1602 | "--offline", | ||
| 1603 | &proposal_id, | ||
| 1604 | ]) | ||
| 1605 | .status()?; | ||
| 1606 | anyhow::ensure!(status.success(), "ngit pr checkout exited with {status}"); | ||
| 1607 | Ok(()) | ||
| 1608 | } | ||
| 1609 | |||
| 1610 | /// Returns (originating_repo, test_repo) with proposal branch checked out. | ||
| 1611 | /// Uses `ngit pr checkout` instead of the old interactive `ngit list`. | ||
| 1612 | pub fn create_proposals_and_repo_with_proposal_branch_checked_out( | ||
| 1613 | branch_name_in_event: &str, | ||
| 1614 | ) -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 1615 | let originating_repo = cli_tester_create_proposals()?; | ||
| 1616 | let test_repo = GitTestRepo::default(); | ||
| 1617 | test_repo.populate()?; | ||
| 1618 | use_ngit_pr_checkout(&test_repo, branch_name_in_event)?; | ||
| 1619 | Ok((originating_repo, test_repo)) | ||
| 1620 | } | ||
| 1621 | |||
| 1535 | pub fn remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main( | 1622 | pub fn remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main( |
| 1536 | test_repo: &GitTestRepo, | 1623 | test_repo: &GitTestRepo, |
| 1537 | ) -> Result<String> { | 1624 | ) -> Result<String> { |