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
20 changes: 17 additions & 3 deletions factory/worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@ def _bootstrap_unborn_repo(project_path: Path) -> None:
def detect_default_branch(project_path: Path) -> str:
"""Detect the default branch for a git repository.

Cascade: remote HEAD → probe main/master → current HEAD → fallback 'main'.
Cascade: remote HEAD, then probe main/master, then current HEAD, then the
symbolic HEAD of an unborn repo. Raises RuntimeError if none of these
resolve, rather than guessing a branch that does not exist.
"""
project_path = project_path.resolve()

Expand Down Expand Up @@ -524,8 +526,20 @@ def detect_default_branch(project_path: Path) -> str:
log.debug("detect_default_branch", source="symbolic_ref", branch=branch)
return branch

log.debug("detect_default_branch", source="fallback", branch="main")
return "main"
# Every detection method failed: no origin/HEAD, no local main or master,
# HEAD is detached, and the symbolic HEAD did not resolve either. Probing
# for main or master already happened above, so returning a hardcoded
# "main" here would only hand create_worktree a branch that does not
# exist, which then surfaces as an opaque "git rev-parse main returned
# non-zero exit status 128" error. Fail with something the caller can act
# on instead.
log.debug("detect_default_branch", source="fallback", branch=None)
raise RuntimeError(
f"Could not determine a default branch for {project_path}. "
"There is no origin/HEAD, no local 'main' or 'master' branch, and "
"HEAD is detached. Set `target_branch` in .factory/config.json or "
"check out a branch before running."
)


def _list_active_worktrees(project_path: Path) -> set[str]:
Expand Down
43 changes: 43 additions & 0 deletions tests/test_worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,49 @@ def test_fallback_to_current_branch(self, tmp_path: Path) -> None:

assert detect_default_branch(project) == "develop"

def test_raises_when_no_branch_detectable(self, tmp_path: Path) -> None:
"""A detached HEAD with no main/master and no origin raises instead of
returning a nonexistent 'main' branch."""
project = tmp_path / "project"
project.mkdir()

env = {
"GIT_AUTHOR_NAME": "test",
"GIT_AUTHOR_EMAIL": "test@test.com",
"GIT_COMMITTER_NAME": "test",
"GIT_COMMITTER_EMAIL": "test@test.com",
"HOME": str(tmp_path),
"PATH": "/usr/bin:/bin:/usr/local/bin",
}

subprocess.run(
["git", "init", "-b", "develop"],
cwd=project,
capture_output=True,
check=True,
)
(project / "README.md").write_text("hello")
subprocess.run(["git", "add", "."], cwd=project, capture_output=True, check=True)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=project,
capture_output=True,
check=True,
env=env,
)
# Detach HEAD so abbrev-ref returns "HEAD" and symbolic-ref fails,
# leaving no detectable default branch.
subprocess.run(
["git", "checkout", "--detach", "HEAD"],
cwd=project,
capture_output=True,
check=True,
env=env,
)

with pytest.raises(RuntimeError, match="Could not determine a default branch"):
detect_default_branch(project)


class TestCreateWorktreeWithMaster:
def test_create_worktree_on_master_repo(self, git_project_master: Path) -> None:
Expand Down