Restore-Items.ps1
|
function Restore-Items { <# .SYNOPSIS Restore explicitly selected git working-tree paths. .DESCRIPTION By default, restores only the working tree from the index, discarding unstaged changes while preserving staged changes. With -IncludeIndex, restores both the index and working tree from HEAD. An explicit -Source overrides either default and must resolve to a tree (for example a commit, tag or tree ID). An invalid source is an error, never a fallback to HEAD. Uses git restore with literal path operands, not wildcard/pathspec expansion. A directory selects its tracked descendants recursively; explicitly passing '.' selects the current repository directory's tree. Selected tracked paths absent from the source can be removed, and local content at selected paths can be overwritten. Untracked-only paths absent from the source are not cleaned. Submodule working trees are not restored. Selected unmerged index entries are refused, even with -Source or -IncludeIndex; this command does not resolve conflicts. Confirmation is high impact. -WhatIf and declined confirmation perform only read-only validation. The caller's location is unchanged. Git errors are PowerShell errors; a failed multi-path restore can leave partial changes and is not automatically rolled back. .PARAMETER Files Required literal file or directory paths, relative to -Path (or the current directory), or absolute paths inside the working tree. Quote paths with spaces or PowerShell metacharacters. Each array element is a separate operand. Wildcards and Git pathspec magic are treated literally, never expanded. Empty paths and control characters are rejected. .PARAMETER Path Literal directory inside the working tree. Defaults to the current location. RepositoryPath and RepoPath are aliases; this is not a file list. Bare repositories are not supported. .PARAMETER Source Existing local tree-ish, such as HEAD~1, a tag, or a tree ID. Overrides the index default (working tree only) or HEAD default (-IncludeIndex). Option-like values and control characters are rejected. Does not fetch. .PARAMETER IncludeIndex Also overwrite staged changes. Restores both index and working tree from HEAD unless -Source is supplied. Does not bypass WhatIf or confirmation. .OUTPUTS None. Use -ErrorAction Stop to terminate on a git failure. .EXAMPLE Restore-Items -Files 'src/main.ps1', 'notes with spaces.txt' -WhatIf Preview discarding unstaged changes to exactly these paths. .EXAMPLE Restore-Items 'src/main.ps1' -IncludeIndex -Confirm:$false Discard staged and unstaged changes to this path, restoring it from HEAD. .EXAMPLE Restore-Items -Files 'src' -Source HEAD~1 -Path ../project Restore tracked paths under src from the previous commit, leaving the index unchanged and confirming before overwriting the working tree. .LINK https://git-scm.com/docs/git-restore #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', PositionalBinding = $false)] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [ValidateCount(1, 2147483647)] [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_ -notmatch '[\x00-\x1f\x7f]' }, ErrorMessage = 'Files must contain nonempty literal paths without control characters.')] [string[]]$Files, [Alias('RepositoryPath', 'RepoPath')] [ValidateNotNullOrEmpty()] [string]$Path, [ValidateNotNullOrEmpty()] [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) -and $_ -notmatch '^-|[\x00-\x1f\x7f]' }, ErrorMessage = 'Source must be a tree-ish, not an option or a value containing control characters.')] [string]$Source, [switch]$IncludeIndex ) # Ambient pathspec settings must not make an explicitly named path a pattern. $environment = @{ GIT_LITERAL_PATHSPECS = '1' GIT_GLOB_PATHSPECS = $null GIT_NOGLOB_PATHSPECS = $null GIT_ICASE_PATHSPECS = $null GIT_NO_LAZY_FETCH = '1' GIT_OPTIONAL_LOCKS = '0' } $literalFiles = @($Files | ForEach-Object { if ([IO.Path]::DirectorySeparatorChar -eq '\') { $_.Replace('\', '/') } else { $_ } }) $unmerged = Invoke-Git -Path $Path -Environment $environment -Arguments ( @('--literal-pathspecs', 'ls-files', '--unmerged', '-z', '--') + $literalFiles ) if ($null -eq $unmerged) { return } $repositoryPath = $unmerged.RepositoryPath if ($unmerged.StandardOutput) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [InvalidOperationException]::new('Selected paths have unmerged index entries; resolve conflicts explicitly before restoring.'), 'GitRestoreUnmergedPaths', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Files)) return } $arguments = @('--literal-pathspecs', 'restore', '--worktree', '--no-recurse-submodules') $sourceDescription = 'the index' if ($PSBoundParameters.ContainsKey('Source') -or $IncludeIndex) { $treeish = if ($PSBoundParameters.ContainsKey('Source')) { $Source } else { 'HEAD' } $resolvedSource = Invoke-Git -Path $repositoryPath -Environment $environment -Arguments @( 'rev-parse', '--verify', '--end-of-options', "$treeish^{tree}" ) if ($null -eq $resolvedSource) { return } $tree = $resolvedSource.StandardOutput.Trim() $arguments += "--source=$tree" $sourceDescription = "'$treeish' ($tree)" } $destination = 'working-tree paths' if ($IncludeIndex) { $arguments += '--staged' $destination = 'index and working-tree paths' } $arguments += @('--') + $literalFiles $target = "'$($Files -join "', '")' in '$repositoryPath'" if ($PSCmdlet.ShouldProcess($target, "Restore $destination from $sourceDescription")) { $null = Invoke-Git -Path $repositoryPath -Environment $environment -Arguments $arguments } } |