mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
<!-- 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.
131 lines
6.2 KiB
PowerShell
131 lines
6.2 KiB
PowerShell
<#!
|
|
.SYNOPSIS
|
|
Remove a git worktree (and optionally its local branch and orphan fork remote).
|
|
|
|
.DESCRIPTION
|
|
Locates a worktree by branch/path pattern (supports wildcards). Ensures the primary repository
|
|
root is never removed. Optionally discards local changes with -Force. Deletes associated branch
|
|
unless -KeepBranch. If the branch tracked a non-origin remote with no remaining tracking
|
|
branches, that remote is removed unless -KeepRemote.
|
|
|
|
.PARAMETER Pattern
|
|
Branch name or path fragment (wildcards * ? allowed). If multiple matches found they are listed
|
|
and no deletion occurs.
|
|
|
|
.PARAMETER Force
|
|
Discard uncommitted changes and attempt aggressive cleanup on failure.
|
|
|
|
.PARAMETER KeepBranch
|
|
Preserve the local branch (only remove the worktree directory entry).
|
|
|
|
.PARAMETER KeepRemote
|
|
Preserve any orphan fork remote even if no branches still track it.
|
|
|
|
.EXAMPLE
|
|
./Delete-Worktree.ps1 -Pattern feature/login
|
|
|
|
.EXAMPLE
|
|
./Delete-Worktree.ps1 -Pattern fork-user-featureX -Force
|
|
|
|
.EXAMPLE
|
|
./Delete-Worktree.ps1 -Pattern hotfix -KeepBranch
|
|
|
|
.NOTES
|
|
Manual recovery:
|
|
git worktree list --porcelain
|
|
git worktree prune
|
|
Remove-Item -LiteralPath <path> -Recurse -Force
|
|
git branch -D <branch>
|
|
git remote remove <remote>
|
|
git worktree prune
|
|
#>
|
|
|
|
param(
|
|
[string] $Pattern,
|
|
[switch] $Force,
|
|
[switch] $KeepBranch,
|
|
[switch] $KeepRemote,
|
|
[switch] $Help
|
|
)
|
|
. "$PSScriptRoot/WorktreeLib.ps1"
|
|
if ($Help -or -not $Pattern) { Show-FileEmbeddedHelp -ScriptPath $MyInvocation.MyCommand.Path; return }
|
|
try {
|
|
$repoRoot = Get-RepoRoot
|
|
$entries = Get-WorktreeEntries
|
|
if (-not $entries -or $entries.Count -eq 0) { throw 'No worktrees found.' }
|
|
$hasWildcard = $Pattern -match '[\*\?]'
|
|
$matchPattern = if ($hasWildcard) { $Pattern } else { "*${Pattern}*" }
|
|
$found = $entries | Where-Object { $_.Branch -and ( $_.Branch -like $matchPattern -or $_.Path -like $matchPattern ) }
|
|
if (-not $found -or $found.Count -eq 0) { throw "No worktree matches pattern '$Pattern'" }
|
|
if ($found.Count -gt 1) {
|
|
Warn 'Pattern matches multiple worktrees:'
|
|
$found | ForEach-Object { Info (" {0} {1}" -f $_.Branch, $_.Path) }
|
|
return
|
|
}
|
|
$target = $found | Select-Object -First 1
|
|
$branch = $target.Branch
|
|
$folder = $target.Path
|
|
if (-not $branch) { throw 'Resolved worktree has no branch (detached); refusing removal.' }
|
|
try { $folder = (Resolve-Path -LiteralPath $folder -ErrorAction Stop).ProviderPath } catch {}
|
|
$primary = (Resolve-Path -LiteralPath $repoRoot).ProviderPath
|
|
if ([IO.Path]::GetFullPath($folder).TrimEnd('\\/') -ieq [IO.Path]::GetFullPath($primary).TrimEnd('\\/')) { throw 'Refusing to remove the primary worktree (repository root).' }
|
|
$status = git -C $folder status --porcelain 2>$null
|
|
if ($LASTEXITCODE -ne 0) { throw "Unable to get git status for $folder" }
|
|
if (-not $Force -and $status) { throw 'Worktree has uncommitted changes. Use -Force to discard.' }
|
|
if ($Force -and $status) {
|
|
Warn '[Force] Discarding local changes'
|
|
git -C $folder reset --hard HEAD | Out-Null
|
|
git -C $folder clean -fdx | Out-Null
|
|
}
|
|
if ($Force) { git worktree remove --force $folder } else { git worktree remove $folder }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
$exit1 = $LASTEXITCODE
|
|
$errMsg = "git worktree remove failed (exit $exit1)"
|
|
if ($Force) {
|
|
Warn 'Primary removal failed; performing aggressive fallback (Force implies brute).'
|
|
try { git -C $folder submodule deinit -f --all 2>$null | Out-Null } catch {}
|
|
try { git -C $folder clean -dfx 2>$null | Out-Null } catch {}
|
|
try { Get-ChildItem -LiteralPath $folder -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { try { $_.IsReadOnly = $false } catch {} } } catch {}
|
|
if (Test-Path $folder) { try { Remove-Item -LiteralPath $folder -Recurse -Force -ErrorAction Stop } catch { Err "Manual directory removal failed: $($_.Exception.Message)" } }
|
|
git worktree prune 2>$null | Out-Null
|
|
if (Test-Path $folder) { throw "$errMsg and aggressive cleanup did not fully remove directory: $folder" } else { Info "Aggressive cleanup removed directory $folder." }
|
|
} else {
|
|
throw "$errMsg. Rerun with -Force to attempt aggressive cleanup."
|
|
}
|
|
}
|
|
# Determine upstream before potentially deleting branch
|
|
$upRemote = Get-BranchUpstreamRemote -Branch $branch
|
|
$looksForkName = $branch -like 'fork-*'
|
|
|
|
if (-not $KeepBranch) {
|
|
git branch -D $branch 2>$null | Out-Null
|
|
if (-not $KeepRemote -and $upRemote -and $upRemote -ne 'origin') {
|
|
$otherTracking = git for-each-ref --format='%(refname:short)|%(upstream:short)' refs/heads 2>$null |
|
|
Where-Object { $_ -and ($_ -notmatch "^$branch\|") } |
|
|
ForEach-Object { $parts = $_.Split('|',2); if ($parts[1] -match '^(?<r>[^/]+)/'){ $parts[0],$Matches.r } } |
|
|
Where-Object { $_[1] -eq $upRemote }
|
|
if (-not $otherTracking) {
|
|
Warn "Removing orphan remote '$upRemote' (no more tracking branches)"
|
|
git remote remove $upRemote 2>$null | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { Warn "Failed to remove remote '$upRemote' (you may remove manually)." }
|
|
} else { Info "Remote '$upRemote' retained (other branches still track it)." }
|
|
} elseif ($looksForkName -and -not $KeepRemote -and -not $upRemote) {
|
|
Warn 'Branch looks like a fork branch (name pattern), but has no upstream remote; nothing to clean.'
|
|
}
|
|
}
|
|
|
|
Info "Removed worktree ($branch) at $folder."; if (-not $KeepBranch) { Info 'Branch deleted.' }
|
|
Show-WorktreeExecutionSummary -CurrentBranch $branch
|
|
} catch {
|
|
Err "Error: $($_.Exception.Message)"
|
|
Warn 'Manual cleanup guidelines:'
|
|
Info ' git worktree list --porcelain'
|
|
Info ' git worktree prune'
|
|
Info ' # If still present:'
|
|
Info ' Remove-Item -LiteralPath <path> -Recurse -Force'
|
|
Info ' git branch -D <branch> (if you also want to drop local branch)'
|
|
Info ' git remote remove <remote> (if orphan fork remote remains)'
|
|
Info ' git worktree prune'
|
|
exit 1
|
|
}
|