Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dotbot wraps AI-assisted coding in a managed, transparent workflow where every s

**Required:**
- **PowerShell 7+** - [Download](https://aka.ms/powershell)
- **Git** - [Download](https://git-scm.com/downloads)
- **Git 2.42+** - [Download](https://git-scm.com/downloads) (required for `git worktree add --orphan`, used on the first task of a fresh project)
- **AI CLI** (at least one) - [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli), [Codex CLI](https://github.com/openai/codex), or [Antigravity](https://antigravity.google/)

**Recommended MCP servers:**
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/Modules/Dotbot.Workflow/Dotbot.Workflow.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
'Test-ManifestCondition'
'Test-CanStartRun'
'Test-GitReadyForWorktree'
'ConvertTo-DotbotGitVersion'
'Get-DotbotGitVersion'
'Get-MinDotbotGitVersion'

# TaskDefinition
'Get-TaskDefinitionFields'
Expand Down
65 changes: 58 additions & 7 deletions src/runtime/Modules/Dotbot.Workflow/Dotbot.Workflow.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1452,16 +1452,56 @@ function Test-CanStartRun {
return @{ ok = $true }
}

# Minimum git version dotbot operates on. `git worktree add --orphan` (used
# by the first task in a brand-new project) requires 2.42; supporting older
# releases previously meant a fallback that failed with a cryptic
# `fatal: invalid reference`. See issue #659.
$script:MinDotbotGitVersion = [System.Version]::new(2, 42, 0)

function Get-MinDotbotGitVersion {
return $script:MinDotbotGitVersion
}

function ConvertTo-DotbotGitVersion {
<#
.SYNOPSIS
Parse the output of `git --version` into a [System.Version].
Returns $null when the input can't be parsed.
#>
param([string]$VersionOutput)
if ([string]::IsNullOrWhiteSpace($VersionOutput)) { return $null }
if ($VersionOutput -notmatch '(\d+)\.(\d+)(?:\.(\d+))?') { return $null }
$maj = [int]$Matches[1]
$min = [int]$Matches[2]
$patch = if ($Matches[3]) { [int]$Matches[3] } else { 0 }
return [System.Version]::new($maj, $min, $patch)
}

function Get-DotbotGitVersion {
<#
.SYNOPSIS
Return the installed git's [System.Version], or $null if git is not on PATH
or its version output cannot be parsed.
#>
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { return $null }
$out = & git --version 2>$null
if ($LASTEXITCODE -ne 0 -or -not $out) { return $null }
return ConvertTo-DotbotGitVersion -VersionOutput ([string]$out)
}

function Test-GitReadyForWorktree {
<#
.SYNOPSIS
Check whether a project directory satisfies the workflow worktree preconditions.

.DESCRIPTION
Starting a WorkflowRun requires that the project directory is a git repo.
Repositories with no commits are allowed; task worktrees use git's orphan
worktree mode until the first task commit establishes the base branch.
Starting a WorkflowRun requires that the project directory is a git repo
and that git itself is new enough to support the orphan-worktree path used
for the first task in a fresh project. Repositories with no commits are
allowed; task worktrees use `git worktree add --orphan` until the first
task commit establishes the base branch.
Concretely:
- git ≥ $script:MinDotbotGitVersion must be on PATH.
- <ProjectRoot>/.git must exist (directory or gitlink file — gitlink
covers the worktree case where .git is a small file pointing to the
real gitdir).
Expand All @@ -1470,10 +1510,8 @@ function Test-GitReadyForWorktree {
unborn git worktree.

On success returns @{ ok = $true }. On failure returns @{ ok = $false;
reason = 'no_git'|'git_unavailable'; message = '<text>' }
where <text> is the user-facing refusal message:

"Workflow runs require a git repo. Initialise git first, then retry."
reason = 'no_git'|'git_unavailable'|'git_too_old'|'invalid_git_repo';
message = '<text>' } where <text> is the user-facing refusal message.

This is a pure check — it neither modifies anything nor talks to a
network. Dotbot.Worktree's create call also invokes the check before
Expand Down Expand Up @@ -1507,6 +1545,16 @@ function Test-GitReadyForWorktree {
}
}

$installedGitVersion = Get-DotbotGitVersion
if (-not $installedGitVersion -or $installedGitVersion -lt $script:MinDotbotGitVersion) {
$found = if ($installedGitVersion) { "$installedGitVersion" } else { 'unknown' }
return @{
ok = $false
reason = 'git_too_old'
message = "dotbot requires git $($script:MinDotbotGitVersion) or newer (found: $found). Upgrade git and retry."
}
}

$count = $null
try {
# -C <dir> so we do not have to push/pop CWD; capture stderr to keep it
Expand Down Expand Up @@ -1635,6 +1683,9 @@ Export-ModuleMember -Function @(
'Test-ManifestCondition'
'Test-CanStartRun'
'Test-GitReadyForWorktree'
'ConvertTo-DotbotGitVersion'
'Get-DotbotGitVersion'
'Get-MinDotbotGitVersion'

# Defined in nested modules under Private/, re-exported here so the
# manifest sees them.
Expand Down
10 changes: 9 additions & 1 deletion src/runtime/Modules/Dotbot.Worktree/Dotbot.Worktree.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,20 @@ function Assert-OnBaseBranch {
Ensure the main repo is checked out on the specified branch (or the canonical
main/master if none is specified). Checks out the branch if not already on it.
Throws if the branch cannot be found or checked out.
Returns the confirmed base branch name.
Returns the confirmed base branch name, or $null when the repo is unborn.

Unborn repos have no base branch to switch to — the first task runs on an
orphan worktree, and its squash-merge creates the base branch. Callers
already handle this path; silently return $null instead of throwing a
misleading "Cannot find base branch" error.
#>
param(
[Parameter(Mandatory)][string]$ProjectRoot,
[string]$BranchName
)
if (-not (Test-RepositoryHasCommits -ProjectRoot $ProjectRoot)) {
return $null
}
if (-not $BranchName) {
$BranchName = Resolve-MainBranch -ProjectRoot $ProjectRoot
}
Expand Down
16 changes: 15 additions & 1 deletion src/runtime/Modules/Dotbot.Worktree/Private/Worktree.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,12 @@ function Resolve-WorkflowMainBranch {
return $null
}

$script:_MinGitVersion = [System.Version]::new(2, 42, 0)

function _Test-GitReadyForWorktree {
# Local copy of Test-GitReadyForWorktree's check so this module doesn't
# have to drag in Dotbot.Workflow.
# have to drag in Dotbot.Workflow. Keep these two implementations in
# lockstep (see Dotbot.Workflow.psm1).
param([Parameter(Mandatory)][string]$ProjectRoot)

$refusal = @(
Expand All @@ -168,6 +171,17 @@ function _Test-GitReadyForWorktree {
return @{ ok = $false; reason = 'git_unavailable'; message = "git CLI is not available on PATH; cannot verify the worktree precondition.`n$refusal" }
}

$installedGitVersion = $null
$verOut = & git --version 2>$null
if ($LASTEXITCODE -eq 0 -and $verOut -and ([string]$verOut) -match '(\d+)\.(\d+)(?:\.(\d+))?') {
$vp = if ($Matches[3]) { [int]$Matches[3] } else { 0 }
$installedGitVersion = [System.Version]::new([int]$Matches[1], [int]$Matches[2], $vp)
}
if (-not $installedGitVersion -or $installedGitVersion -lt $script:_MinGitVersion) {
$found = if ($installedGitVersion) { "$installedGitVersion" } else { 'unknown' }
return @{ ok = $false; reason = 'git_too_old'; message = "dotbot requires git $($script:_MinGitVersion) or newer (found: $found). Upgrade git and retry." }
}

$count = $null
try {
$stdout = & git -C $ProjectRoot rev-list --count HEAD 2>$null
Expand Down
45 changes: 45 additions & 0 deletions tests/Test-WorkflowManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,51 @@ try {

Write-Host ""

# ═══════════════════════════════════════════════════════════════════
# Git version parsing + floor (regression: #659)
# ═══════════════════════════════════════════════════════════════════

Write-Host " Git version parsing" -ForegroundColor Cyan
Write-Host " ────────────────────────────────────────────" -ForegroundColor DarkGray

# Standard Windows form.
$v = ConvertTo-DotbotGitVersion -VersionOutput 'git version 2.53.0.windows.3'
Assert-True -Name "parse windows form -> major" -Condition ($v.Major -eq 2)
Assert-True -Name "parse windows form -> minor" -Condition ($v.Minor -eq 53)
Assert-True -Name "parse windows form -> build" -Condition ($v.Build -eq 0)

# Two-component version (patch defaults to 0).
$v = ConvertTo-DotbotGitVersion -VersionOutput 'git version 2.42'
Assert-True -Name "parse two-component -> minor" -Condition ($v.Minor -eq 42)
Assert-True -Name "parse two-component -> patch defaults to 0" -Condition ($v.Build -eq 0)

# Old form the ticket reporter had.
$v = ConvertTo-DotbotGitVersion -VersionOutput 'git version 2.31.1.windows.1'
Assert-True -Name "parse legacy windows form -> minor" -Condition ($v.Minor -eq 31)

# Nonsense / empty input.
Assert-True -Name "parse empty -> null" -Condition ($null -eq (ConvertTo-DotbotGitVersion -VersionOutput ''))
Assert-True -Name "parse garbage -> null" -Condition ($null -eq (ConvertTo-DotbotGitVersion -VersionOutput 'not a version'))

# Floor is 2.42 (see Dotbot.Workflow.psm1 script scope).
$minVersion = Get-MinDotbotGitVersion
Assert-True -Name "min version is 2.42.0" `
-Condition ($minVersion -eq ([System.Version]::new(2, 42, 0)))

# Version-comparison sanity: 2.31 is below the floor; 2.42 is at the floor.
$tooOld = ConvertTo-DotbotGitVersion -VersionOutput 'git version 2.31.1.windows.1'
$atFloor = ConvertTo-DotbotGitVersion -VersionOutput 'git version 2.42.0'
Assert-True -Name "2.31 is below the min version" -Condition ($tooOld -lt $minVersion)
Assert-True -Name "2.42 meets the min version" -Condition ($atFloor -ge $minVersion)

# Get-DotbotGitVersion should return the installed git's version when git is on PATH.
if (Get-Command git -ErrorAction SilentlyContinue) {
$installed = Get-DotbotGitVersion
Assert-True -Name "Get-DotbotGitVersion returns a Version" -Condition ($installed -is [System.Version])
}

Write-Host ""

# ═══════════════════════════════════════════════════════════════════
# workflow manifest removed isolation fields + skip_worktree lint
# ═══════════════════════════════════════════════════════════════════
Expand Down
31 changes: 31 additions & 0 deletions tests/Test-Worktree.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,37 @@ try {
Remove-Item -Path $emptyRepo -Recurse -Force -ErrorAction SilentlyContinue
}

# ═══════════════════════════════════════════════════════════════════
# Assert-OnBaseBranch on unborn HEAD (regression: #659)
# ═══════════════════════════════════════════════════════════════════

Write-Host ""
Write-Host " Assert-OnBaseBranch — unborn repo" -ForegroundColor Cyan
Write-Host " ────────────────────────────────────────────" -ForegroundColor DarkGray

# Fresh `git init` with no commits used to throw "Cannot find base branch",
# surfaced as a scary "Branch guard warning" on the very first workflow run
# of a new project. The check should silently no-op instead — the first task
# creates the base branch via its orphan worktree squash-merge.
$unbornRepo = Join-Path ([System.IO.Path]::GetTempPath()) "dotbot-test-unborn-$([System.Guid]::NewGuid().ToString().Substring(0,8))"
New-Item -ItemType Directory -Path $unbornRepo -Force | Out-Null
try {
& git -C $unbornRepo init --quiet 2>$null | Out-Null
$threw = $false
$result = $null
try {
$result = Assert-OnBaseBranch -ProjectRoot $unbornRepo
} catch {
$threw = $true
}
Assert-True -Name "Assert-OnBaseBranch — does not throw on unborn HEAD" -Condition (-not $threw)
Assert-True -Name "Assert-OnBaseBranch — returns null on unborn HEAD" -Condition ($null -eq $result)
& git -C $unbornRepo rev-parse --verify HEAD 2>$null | Out-Null
Assert-True -Name "Assert-OnBaseBranch — does not create a commit" -Condition ($LASTEXITCODE -ne 0)
} finally {
Remove-Item -Path $unbornRepo -Recurse -Force -ErrorAction SilentlyContinue
}

# ═══════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════
Expand Down
Loading