ctrlrelay 0.11.2__tar.gz → 0.11.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/CHANGELOG.md +16 -1
  2. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/PKG-INFO +1 -1
  3. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/pyproject.toml +1 -1
  4. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/worktree.py +29 -0
  5. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_worktree.py +49 -3
  6. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  7. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  8. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  9. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/dependabot.yml +0 -0
  10. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/workflows/build.yml +0 -0
  11. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/workflows/cla.yml +0 -0
  12. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/workflows/pages.yml +0 -0
  13. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/workflows/publish.yml +0 -0
  14. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.github/workflows/test.yml +0 -0
  15. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/.gitignore +0 -0
  16. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/CODE_OF_CONDUCT.md +0 -0
  17. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/CONTRIBUTING.md +0 -0
  18. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/LICENSE +0 -0
  19. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/README.md +0 -0
  20. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/SECURITY.md +0 -0
  21. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/config/orchestrator.yaml.example +0 -0
  22. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/Gemfile +0 -0
  23. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/_config.yml +0 -0
  24. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/architecture.md +0 -0
  25. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/bridge.md +0 -0
  26. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/cli.md +0 -0
  27. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/configuration.md +0 -0
  28. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/development.md +0 -0
  29. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/feedback-loop.md +0 -0
  30. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/getting-started.md +0 -0
  31. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/index.md +0 -0
  32. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/operations.md +0 -0
  33. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/docs/personalization.md +0 -0
  34. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/__init__.py +0 -0
  35. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/bridge/__init__.py +0 -0
  36. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/bridge/__main__.py +0 -0
  37. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/bridge/protocol.py +0 -0
  38. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/bridge/server.py +0 -0
  39. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  40. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/cli.py +0 -0
  41. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/__init__.py +0 -0
  42. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/archived.py +0 -0
  43. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/audit.py +0 -0
  44. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/checkpoint.py +0 -0
  45. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/code_review.py +0 -0
  46. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/config.py +0 -0
  47. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/dispatcher.py +0 -0
  48. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/github.py +0 -0
  49. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/network.py +0 -0
  50. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/obs.py +0 -0
  51. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/poller.py +0 -0
  52. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/pr_verifier.py +0 -0
  53. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/pr_watcher.py +0 -0
  54. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/question_expiry.py +0 -0
  55. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/scheduler.py +0 -0
  56. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/core/state.py +0 -0
  57. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/dashboard/__init__.py +0 -0
  58. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/dashboard/client.py +0 -0
  59. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/gh_protocol.py +0 -0
  60. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/install.py +0 -0
  61. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/personalization/__init__.py +0 -0
  62. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/personalization/manager.py +0 -0
  63. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/personalization/paths.py +0 -0
  64. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/__init__.py +0 -0
  65. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/base.py +0 -0
  66. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/dev.py +0 -0
  67. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  68. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/secops.py +0 -0
  69. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/pipelines/task.py +0 -0
  70. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/setup.py +0 -0
  71. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/__init__.py +0 -0
  72. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  73. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  74. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  75. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  76. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  77. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  78. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  79. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/transports/__init__.py +0 -0
  80. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/transports/base.py +0 -0
  81. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/transports/file_mock.py +0 -0
  82. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/src/ctrlrelay/transports/socket_client.py +0 -0
  83. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/__init__.py +0 -0
  84. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/conftest.py +0 -0
  85. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_archived.py +0 -0
  86. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_audit.py +0 -0
  87. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_bridge_protocol.py +0 -0
  88. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_bridge_server.py +0 -0
  89. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_checkpoint.py +0 -0
  90. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_ci_wait.py +0 -0
  91. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_dev.py +0 -0
  92. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_repos.py +0 -0
  93. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_secops.py +0 -0
  94. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_start.py +0 -0
  95. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_cli_version.py +0 -0
  96. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_code_review.py +0 -0
  97. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_config.py +0 -0
  98. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_dashboard_client.py +0 -0
  99. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_dev_integration.py +0 -0
  100. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_dev_pipeline.py +0 -0
  101. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_dispatcher.py +0 -0
  102. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_docs_site.py +0 -0
  103. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_gh_protocol.py +0 -0
  104. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_github.py +0 -0
  105. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_install.py +0 -0
  106. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_network.py +0 -0
  107. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_obs.py +0 -0
  108. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_personalization.py +0 -0
  109. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_pipeline_base.py +0 -0
  110. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_poller.py +0 -0
  111. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_post_merge.py +0 -0
  112. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_pr_verifier.py +0 -0
  113. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_pr_watcher.py +0 -0
  114. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_question_expiry.py +0 -0
  115. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_scheduler.py +0 -0
  116. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_secops_integration.py +0 -0
  117. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_secops_pipeline.py +0 -0
  118. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_setup.py +0 -0
  119. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_state.py +0 -0
  120. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_task_pipeline.py +0 -0
  121. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_telegram_handler.py +0 -0
  122. {ctrlrelay-0.11.2 → ctrlrelay-0.11.3}/tests/test_transport.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.3] - 2026-09-12
11
+
12
+ ### Fixed
13
+
14
+ - **A force-pushed default branch fails the fetch again.** v0.11.2
15
+ tolerated every non-fast-forward rejection so a rebased Dependabot
16
+ branch would stop aborting sweeps. That also covered the default
17
+ branch, which sessions are cut from: after a force-push to `main` on
18
+ GitHub, the bare repo would keep its old `main` and every session
19
+ would run on a tree origin no longer has, with only a log line to show
20
+ for it. A rejection of the default branch now raises
21
+ `default branch diverged from origin`; other branches are still
22
+ tolerated. Closes #120.
23
+
10
24
  ## [0.11.2] - 2026-09-10
11
25
 
12
26
  ### Fixed
@@ -1644,7 +1658,8 @@ pipeline).
1644
1658
  per-phase implementation plans (Phase 0 through Phase 4).
1645
1659
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
1646
1660
 
1647
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.2...HEAD
1661
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.3...HEAD
1662
+ [0.11.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.2...v0.11.3
1648
1663
  [0.11.2]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.1...v0.11.2
1649
1664
  [0.11.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.11.0...v0.11.1
1650
1665
  [0.11.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.10.0...v0.11.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.11.2
3
+ Version: 0.11.3
4
4
  Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
5
  Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
6
  Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.11.2"
3
+ version = "0.11.3"
4
4
  description = "Local-first orchestrator for headless coding agents across multiple GitHub repos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -122,6 +122,7 @@ class WorktreeManager:
122
122
  cwd: Path | None = None,
123
123
  timeout: int | None = None,
124
124
  tolerate_non_fast_forward: bool = False,
125
+ protected_refs: frozenset[str] = frozenset(),
125
126
  ) -> str:
126
127
  """Run git command and return stdout. `timeout` overrides self.timeout
127
128
  for one call — useful for cheap probes that shouldn't inherit the full
@@ -131,6 +132,11 @@ class WorktreeManager:
131
132
  refspec: git exits 1 when it declines to rewind a diverged local
132
133
  branch, even though every other ref updated. See
133
134
  :func:`_rejections_are_only_non_fast_forward`.
135
+
136
+ ``protected_refs`` names branches whose rejection is never
137
+ tolerated. A declined rewind of the default branch means local
138
+ and origin have diverged there, and every worktree cut from it
139
+ would run against a tree origin no longer has.
134
140
  """
135
141
  cmd = ["git", *args]
136
142
  effective_timeout = timeout if timeout is not None else self.timeout
@@ -186,6 +192,13 @@ class WorktreeManager:
186
192
  )
187
193
  if not declined:
188
194
  raise WorktreeError(f"git failed: {err}")
195
+ stuck = sorted(set(declined) & protected_refs)
196
+ if stuck:
197
+ raise WorktreeError(
198
+ f"git failed: default branch diverged from origin "
199
+ f"({', '.join(stuck)}); refusing to use a stale tree: "
200
+ f"{err}"
201
+ )
189
202
  log_event(
190
203
  _logger,
191
204
  "worktree.fetch.non_fast_forward",
@@ -864,10 +877,22 @@ class WorktreeManager:
864
877
  Git exits 1 on a declined rewind even when every other ref
865
878
  applied, and that exit code was aborting the whole session
866
879
  for a branch it had correctly protected.
880
+
881
+ The default branch is the exception: a rejection there still
882
+ raises. Sessions are cut from it, so tolerating that one would
883
+ swap a loud failure for work done quietly on a stale tree —
884
+ e.g. after a force-push to ``main`` on GitHub.
867
885
  """
868
886
  bare_path = self._get_bare_repo_path(repo)
869
887
 
870
888
  if bare_path.exists():
889
+ try:
890
+ default_branch = await self.get_default_branch(repo)
891
+ except WorktreeError:
892
+ # A bare repo with no symbolic HEAD cannot name a default
893
+ # branch to protect. create_worktree fails on the same
894
+ # lookup, so the stale-tree risk never reaches a session.
895
+ default_branch = ""
871
896
  await self._run_git(
872
897
  "fetch", "--prune", "origin",
873
898
  _BOT_BRANCH_REFSPEC,
@@ -875,6 +900,10 @@ class WorktreeManager:
875
900
  cwd=bare_path,
876
901
  timeout=_GIT_TRANSFER_TIMEOUT_SECONDS,
877
902
  tolerate_non_fast_forward=True,
903
+ protected_refs=(
904
+ frozenset({default_branch}) if default_branch
905
+ else frozenset()
906
+ ),
878
907
  )
879
908
  else:
880
909
  await self._run_git(
@@ -79,12 +79,14 @@ class TestWorktreeManager:
79
79
  mock_git.return_value = ""
80
80
  await manager.ensure_bare_repo("owner/repo")
81
81
 
82
- assert mock_git.await_count == 1
82
+ # symbolic-ref (default-branch lookup), then the fetch.
83
+ assert mock_git.await_count == 2
84
+ assert mock_git.await_args_list[0].args[0] == "symbolic-ref"
83
85
  args = mock_git.await_args.args
84
86
  # Must be a fetch, not --all, with the explicit refspec.
85
87
  assert args[0] == "fetch"
86
88
  assert "origin" in args
87
- # Non-force refspec on purpose: codex P1 — a force fetch
89
+ # Non-force refspec on purpose: a force fetch
88
90
  # would destroy unpushed local commits in the dev pipeline's
89
91
  # branch-reuse flow. Fast-forward-only fixes the stale-
90
92
  # default-branch case while leaving divergent dev branches
@@ -1597,7 +1599,10 @@ class TestGitTimeoutIsDiagnosable:
1597
1599
  wt._get_bare_repo_path("owner/repo").mkdir(parents=True, exist_ok=True)
1598
1600
  wt._run_git.reset_mock()
1599
1601
  await wt.ensure_bare_repo("owner/repo")
1600
- fetch_kwargs = wt._run_git.await_args_list[0].kwargs
1602
+ fetch_kwargs = next(
1603
+ c.kwargs for c in wt._run_git.await_args_list
1604
+ if c.args and c.args[0] == "fetch"
1605
+ )
1601
1606
  assert fetch_kwargs["timeout"] == _GIT_TRANSFER_TIMEOUT_SECONDS
1602
1607
 
1603
1608
 
@@ -1726,6 +1731,47 @@ class TestFetchSurvivesRewrittenBranches:
1726
1731
  await wt.ensure_bare_repo("owner/repo")
1727
1732
 
1728
1733
 
1734
+ @pytest.mark.asyncio
1735
+ async def test_force_pushed_default_branch_still_raises(
1736
+ self, tmp_path: Path
1737
+ ) -> None:
1738
+ """Sessions are cut from the default branch. If origin rewrote it,
1739
+ tolerating the rejection would run every session on a tree origin
1740
+ no longer has — quietly, instead of loudly."""
1741
+ import subprocess
1742
+
1743
+ from ctrlrelay.core.worktree import WorktreeError, WorktreeManager
1744
+
1745
+ origin, bare = self._build_repos(tmp_path)
1746
+
1747
+ # Rewrite main on origin, and give the bare clone its own commit
1748
+ # on main so the two have diverged.
1749
+ self._git("checkout", "-q", "main", cwd=origin)
1750
+ (origin / "f").write_text("rewritten\n")
1751
+ self._git("commit", "-qam", "x", cwd=origin)
1752
+ self._git("commit", "-q", "--amend", "-m", "force-pushed main",
1753
+ cwd=origin)
1754
+ tree = self._git("rev-parse", "refs/heads/main^{tree}",
1755
+ cwd=bare).strip()
1756
+ sha = self._git("commit-tree", tree, "-p", "refs/heads/main",
1757
+ "-m", "local main", cwd=bare).strip()
1758
+ self._git("update-ref", "refs/heads/main", sha, cwd=bare)
1759
+
1760
+ wt = WorktreeManager(
1761
+ worktrees_dir=tmp_path / "wt", bare_repos_dir=tmp_path / "repos",
1762
+ )
1763
+ target = wt._get_bare_repo_path("owner/repo")
1764
+ target.parent.mkdir(parents=True, exist_ok=True)
1765
+ bare.rename(target)
1766
+ subprocess.run(
1767
+ ["git", "remote", "set-url", "origin", str(origin)],
1768
+ cwd=target, check=True,
1769
+ )
1770
+
1771
+ with pytest.raises(WorktreeError, match="default branch diverged"):
1772
+ await wt.ensure_bare_repo("owner/repo")
1773
+
1774
+
1729
1775
  class TestNonFastForwardClassifier:
1730
1776
  """`_rejections_are_only_non_fast_forward` decides whether a
1731
1777
  non-zero `git fetch` is the refspec doing its job or a real
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes