Files
PowerToys/tools/build/New-WorktreeFromFork.ps1

128 lines
5.0 KiB
PowerShell
Raw Normal View History

Introduce worktree helper scripts for faster multi-branch development in PowerToys (#42076) <!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This pull request introduces a new suite of helper scripts for managing Git worktrees in the `tools/build` directory, along with comprehensive documentation. The scripts streamline common workflows such as creating, reusing, and deleting worktrees for feature branches, forks, and issue-based development, making it easier for developers to work on multiple changes in parallel without duplicating the repository. Each script is provided as both a PowerShell (`.ps1`) and Windows batch (`.cmd`) wrapper for convenience. A detailed markdown guide explains usage patterns, scenarios, and best practices. **New worktree management scripts:** * Added `New-WorktreeFromFork.ps1`/`.cmd` to create a worktree from a branch in a personal fork, handling remote creation and branch tracking automatically. [[1]](diffhunk://#diff-ea4d43777029cdde7fb9fda8ee6a0ed3dcfd75b22ed6ae566c6a77797c8bef54R1-R111) [[2]](diffhunk://#diff-1314b08f84ac8c2e7d020e5584d9f2f19dbf116bbc13c14de0de432006912cfeR1-R4) * Added `New-WorktreeFromBranch.ps1`/`.cmd` to create or reuse a worktree for an existing local or remote branch, with logic to fetch and track branches as needed. [[1]](diffhunk://#diff-07c08acfb570e1b54647370cae17e663e76ee8cb09614cac7a23a9367f625a3eR1-R69) [[2]](diffhunk://#diff-6297be534792c3e6d1bc377b84bcd20b2eb5b3de84d4376a2592b25fc9a88a88R1-R4) * Added `New-WorktreeFromIssue.ps1`/`.cmd` to create a new issue branch from a base ref (default `origin/main`), slugifying the issue title for branch naming. [[1]](diffhunk://#diff-36cb35f3b814759c60f770fc9cc1cc9fa10ceee53811d95a85881d8e69c1ab07R1-R67) [[2]](diffhunk://#diff-890880241ffc24b5d29ddb69ce4c19697a2fce6be6861d0a24d02ebf65b35694R1-R4) * Added `Delete-Worktree.ps1`/`.cmd` to safely remove a worktree, with options to force removal, keep the local branch, or retain orphan fork remotes. Includes robust error handling and manual recovery guidance. [[1]](diffhunk://#diff-8a335544864c1630d7f9bec6f4113c10d84b8e26054996735da41516ad93e173R1-R120) [[2]](diffhunk://#diff-19a810e57f8b82e1dc2476f35d051eb43f2d31e4f68ca7c011c89fd297718020R1-R4) **Documentation:** * Introduced `Wokrtree-Guidelines.md`, a comprehensive guide covering the purpose, usage, flows, naming conventions, troubleshooting, and best practices for the new worktree scripts.
2025-10-09 21:11:28 -07:00
<#!
.SYNOPSIS
Create (or reuse) a worktree from a branch in a personal fork: <ForkUser>:<ForkBranch>.
.DESCRIPTION
Adds a transient uniquely named fork remote (fork-xxxxx) unless -RemoteName specified.
Fetches only the target branch (fallback full fetch once if needed), creates a local tracking
branch (fork-<user>-<sanitized-branch> or custom alias), and delegates worktree creation/reuse
to shared helpers in WorktreeLib.
.PARAMETER Spec
Fork spec in the form <ForkUser>:<ForkBranch>.
.PARAMETER ForkRepo
Repository name in the fork (default: PowerToys).
.PARAMETER RemoteName
Desired remote name; if left as 'fork' a unique suffix will be generated.
.PARAMETER BranchAlias
Optional local branch name override; defaults to fork-<user>-<sanitized-branch>.
.PARAMETER VSCodeProfile
VS Code profile to pass through to worktree opening (Default profile by default).
.EXAMPLE
./New-WorktreeFromFork.ps1 -Spec alice:feature/new-ui
.EXAMPLE
./New-WorktreeFromFork.ps1 -Spec bob:bugfix/crash -BranchAlias fork-bob-crash
.NOTES
Manual equivalent if this script fails:
git remote add fork-temp https://github.com/<user>/<repo>.git
git fetch fork-temp
git branch --track fork-<user>-<branch> fork-temp/<branch>
git worktree add ../Repo-XX fork-<user>-<branch>
code ../Repo-XX
#>
param(
[string] $Spec,
[string] $ForkRepo = 'PowerToys',
[string] $RemoteName = 'fork',
[string] $BranchAlias,
[Alias('Profile')][string] $VSCodeProfile = 'Default',
[switch] $Help
)
. "$PSScriptRoot/WorktreeLib.ps1"
if ($Help -or -not $Spec) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) { throw 'Not inside a git repository.' }
# Parse spec
if ($Spec -notmatch '^[^:]+:.+$') { throw "Spec must be <ForkUser>:<ForkBranch>, got '$Spec'" }
$ForkUser,$ForkBranch = $Spec.Split(':',2)
$forkUrl = "https://github.com/$ForkUser/$ForkRepo.git"
# Auto-suffix remote name if user left default 'fork'
$allRemotes = @(git remote 2>$null)
if ($RemoteName -eq 'fork') {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
do {
$suffix = -join ((1..5) | ForEach-Object { $chars[(Get-Random -Max $chars.Length)] })
$candidate = "fork-$suffix"
} while ($allRemotes -contains $candidate)
$RemoteName = $candidate
Info "Assigned unique remote name: $RemoteName"
}
$existing = $allRemotes | Where-Object { $_ -eq $RemoteName }
if (-not $existing) {
Info "Adding remote $RemoteName -> $forkUrl"
git remote add $RemoteName $forkUrl | Out-Null
} else {
$currentUrl = git remote get-url $RemoteName 2>$null
if ($currentUrl -ne $forkUrl) { Warn "Remote $RemoteName points to $currentUrl (expected $forkUrl). Using existing." }
}
## Note: Verbose fetch & stale lock auto-clean removed for simplicity.
try {
Info "Fetching branch '$ForkBranch' from $RemoteName..."
& git fetch $RemoteName $ForkBranch 1>$null 2>$null
$fetchExit = $LASTEXITCODE
if ($fetchExit -ne 0) {
# Retry full fetch silently once (covers servers not supporting branch-only fetch syntax)
& git fetch $RemoteName 1>$null 2>$null
$fetchExit = $LASTEXITCODE
}
if ($fetchExit -ne 0) { throw "Fetch failed for remote $RemoteName (branch $ForkBranch)." }
$remoteRef = "refs/remotes/$RemoteName/$ForkBranch"
git show-ref --verify --quiet $remoteRef
if ($LASTEXITCODE -ne 0) { throw "Remote branch not found: $RemoteName/$ForkBranch" }
$sanitizedBranch = ($ForkBranch -replace '[\\/:*?"<>|]','-')
if ($BranchAlias) { $localBranch = $BranchAlias } else { $localBranch = "fork-$ForkUser-$sanitizedBranch" }
git show-ref --verify --quiet "refs/heads/$localBranch"
if ($LASTEXITCODE -ne 0) {
Info "Creating local tracking branch $localBranch from $RemoteName/$ForkBranch"
git branch --track $localBranch "$RemoteName/$ForkBranch" 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Failed to create local tracking branch $localBranch" }
} else { Info "Local branch $localBranch already exists." }
New-WorktreeForExistingBranch -Branch $localBranch -VSCodeProfile $VSCodeProfile
# Ensure upstream so future 'git push' works
Set-BranchUpstream -LocalBranch $localBranch -RemoteName $RemoteName -RemoteBranchPath $ForkBranch
$after = Get-WorktreeEntries | Where-Object { $_.Branch -eq $localBranch }
$path = ($after | Select-Object -First 1).Path
Show-WorktreeExecutionSummary -CurrentBranch $localBranch -WorktreePath $path
Warn "Remote $RemoteName ready (URL: $forkUrl)"
$hasUp = git rev-parse --abbrev-ref --symbolic-full-name "$localBranch@{upstream}" 2>$null
if ($hasUp) { Info "Push with: git push (upstream: $hasUp)" } else { Warn 'Upstream not set; run: git push -u <remote> <local>:<remoteBranch>' }
} catch {
Err "Error: $($_.Exception.Message)"
Warn 'Manual steps:'
Info " git remote add temp-fork $forkUrl"
Info " git fetch temp-fork"
Info " git branch --track fork-<user>-<branch> temp-fork/$ForkBranch"
Info ' git worktree add ../<Repo>-XX fork-<user>-<branch>'
Info ' code ../<Repo>-XX'
exit 1
}