TM-GitUtility.psm1
using namespace System.IO using namespace System.Collections.Generic # Write verbose messages on import if ((Get-PSCallStack)[1].Arguments -imatch 'Verbose=True') { $PSDefaultParameterValues['*:Verbose'] = $true } # Ensure we're using the primary write commands from the Microsoft.PowerShell.Utility module. Set-Alias -Name 'Write-Progress' -Value 'Microsoft.PowerShell.Utility\Write-Progress' -Scope Script Set-Alias -Name 'Write-Debug' -Value 'Microsoft.PowerShell.Utility\Write-Debug' -Scope Script Set-Alias -Name 'Write-Verbose' -Value 'Microsoft.PowerShell.Utility\Write-Verbose' -Scope Script Set-Alias -Name 'Write-Host' -Value 'Microsoft.PowerShell.Utility\Write-Host' -Scope Script Set-Alias -Name 'Write-Information' -Value 'Microsoft.PowerShell.Utility\Write-Information' -Scope Script Set-Alias -Name 'Write-Warning' -Value 'Microsoft.PowerShell.Utility\Write-Warning' -Scope Script Set-Alias -Name 'Write-Error' -Value 'Microsoft.PowerShell.Utility\Write-Error' -Scope Script if ((Test-ApplicationExistsInPath -ApplicationName 'git') -eq $false) { Write-Verbose 'git does not exist in the Path. Skipping the import of git ProfileUtility commands.' # Do not export any module commands. Export-ModuleMember return } # https://github.com/PowerShell/PowerShell/issues/17730#issuecomment-1190678484 $ExportedMembers = [List[string]]::new() class GitStatusInfo { <# .SYNOPSIS This class is used to encapsulate the current status information of a Git repository. .DESCRIPTION GitStatusInfo contains lists of files that have been updated, unchanged, stage, untracked, or ignored. It provides a static method, GetCurrentStatusInfo, which checks the status of the repository using the 'git status' command and fills these lists accordingly. This class helps in managing and visualizing the status information of a Git repository. #> $Updated = [List[FileInfo]]::new() $Deleted = [List[FileInfo]]::new() $AddedStaged = [List[FileInfo]]::new() $ModifiedStaged = [List[FileInfo]]::new() $UnTracked = [List[FileInfo]]::new() $Ignored = [List[FileInfo]]::new() $Unchanged = [List[FileInfo]]::new() [GitStatusInfo] static GetCurrentStatusInfo([string]$gitPath = [string]::Empty) { function Get-FilePaths { [OutputType([void])] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [AllowEmptyCollection()] [List[FileInfo]]$Collection, [Parameter(Mandatory)] [string]$Type ) if ([Directory]::Exists($Path)) { foreach ($File in (Get-ChildItem -Path $Path -Recurse -File)) { $Collection.Add($File) } } elseif ([File]::Exists($Path)) { $Collection.Add([FileInfo]::New($Path)) } else { Write-Warning "$Type Path '$Path' does not exist." } } $CurrentStatus = [GitStatusInfo]::New() if ([string]::IsNullOrWhiteSpace($GitPath)) { $GitPath = Get-GitPath } if ([string]::IsNullOrWhiteSpace($GitPath)) { Write-Warning 'The path heirarchy does not contain a git directory.' return $CurrentStatus } $GitPath = (Resolve-Path -Path $GitPath -ErrorAction Stop).Path Push-Location $GitPath try { foreach ($line in (git status --show-stash --ignored --porcelain)) { $StartSymbol = $Line.Substring(0, 2) $GitPorcelainPath = $Line.Substring(2).Trim().TrimStart('"').TrimEnd('"') $ItemPath = Join-Path -Path $GitPath -ChildPath $GitPorcelainPath $Item = Get-Item -Path $ItemPath -Force switch ($StartSymbol) { ' M' { $CurrentStatus.Updated.Add($Item) } ' D' { $CurrentStatus.Deleted.Add([FileInfo]::New($ItemPath)) } 'A ' { $CurrentStatus.AddedStaged.Add($Item) } 'M ' { $CurrentStatus.ModifiedStaged.Add($Item) } '??' { Get-FilePaths -Path $Item.FullName -Collection $CurrentStatus.UnTracked -Type 'UnTracked' } '!!' { Get-FilePaths -Path $Item.FullName -Collection $CurrentStatus.Ignored -Type 'Ignored' } default { Write-Warning "GitStatusInfo Unknown Element: '$StartSymbol'" } } } [string[]]$ChangeElements = @( $CurrentStatus.Updated.FullName; $CurrentStatus.Deleted.FullName; $CurrentStatus.AddedStaged.FullName; $CurrentStatus.ModifiedStaged.FullName; $CurrentStatus.UnTracked.FullName; $CurrentStatus.Ignored.FullName ) foreach ($line in (git ls-files)) { $ItemPath = Join-Path -Path $GitPath -ChildPath $line if ($ChangeElements -notcontains $ItemPath) { $CurrentStatus.Unchanged.Add((Get-Item -Path $ItemPath -Force)) } } } finally { Pop-Location } return $CurrentStatus } } function Get-GitBranch { <# .SYNOPSIS Returns the current Git branch with a relevant symbol based on the branch name. .DESCRIPTION The function recursively searches for the .git directory in the current and parent directories and retrieves the current Git branch. Symbols are also appended to the string if the branch matches the following patterns 🚀 for '/master' or '/main' 🚧 for '/dev' .PARAMETER Path An optional parameter allowing you to specify the location of the git folder to retrieve a branch information for. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $false)] [Validation.ValidatePathExists('Folder')] [string]$Path = (Get-CurrentPath) ) if ((Get-GitPath -Path $Path) -ne [string]::Empty) { # need to do this so the stderr doesn't show up in $error $ErrorActionPreferenceOld = $ErrorActionPreference $ErrorActionPreference = 'Ignore' $branch = git rev-parse --abbrev-ref --symbolic-full-name '@{u}' $ErrorActionPreference = $ErrorActionPreferenceOld # handle case where branch is local if ($lastexitcode -ne 0 -or $null -eq $branch) { $branch = git rev-parse --abbrev-ref HEAD } $branchSymbol = if (($branch -imatch '/master') -or ($branch -imatch '/main')) { '🚀' } elseif ($branch -imatch '/dev') { '🚧' } return "[$branch$branchSymbol] " } return [string]::Empty } $ExportedMembers.Add('Get-GitBranch') function Get-GitPath { <# .SYNOPSIS Retrieves the path of the current Git repository if it exists in the current path hierarchy. .DESCRIPTION This function looks for the .git directory starting from the specified path and continuing up the directory tree. .PARAMETER Path The path to begin the search for the git repository in. By default this will look in the current working directory. .OUTPUTS Returns the path of the repository where the .git directory is located as a string. If no .git directory is found, it returns an empty string. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $false)] [Validation.ValidatePathExists('Folder')] [string]$Path = (Get-CurrentPath) ) $IteratePath = "$Path" while ([string]::IsNullOrWhiteSpace($IteratePath) -eq $false) { if ([Directory]::Exists((Join-Path -Path $IteratePath -ChildPath '.git'))) { return $IteratePath } $IteratePath = [Path]::GetDirectoryName($IteratePath) } return [string]::Empty } $ExportedMembers.Add('Get-GitPath') function Get-GitStatusInfo { <# .SYNOPSIS Retrieves the git status info for the specified git directory. #> [CmdletBinding()] [OutputType([GitStatusInfo])] param( [Parameter(Mandatory = $false)] [string]$Path = [string]::Empty ) [GitStatusInfo]::GetCurrentStatusInfo($Path) } $ExportedMembers.Add('Get-GitStatusInfo') function Redo-GitCommitAsSigned { <# .SYNOPSIS Modifies the last git commit to be signed. .DESCRIPTION This function allows you to sign the last commit by amending it with the -S flag. .EXAMPLE Redo-GitCommitAsSigned Signs the last commit in the current repository. #> [Alias('Sign-LastGitCommit')] [OutputType([Void])] param() git commit --amend --no-edit -n -S } $ExportedMembers.Add('Redo-GitCommitAsSigned') function Remove-GitTag { <# .SYNOPSIS Removes a specified git tag both locally and remotely. .PARAMETER Tag The name of the git tag to remove. .PARAMETER Remote Removes the tag from the origin location in addition to the removing the tag from the local repository. .EXAMPLE # Remove the 'v1.0.0' tag from the local and remote repositories. Remove-GitTag -Tag 'v1.0.0' -Remote #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$Tag, [Parameter(Mandatory = $false)] [switch]$IncludeRemote ) git tag -d $Tag if ($IncludeRemote) { git push --delete origin $Tag } } $ExportedMembers.Add('Remove-GitTag') function Set-GitFileAssumeUnchanged { <# .SYNOPSIS Tells git to assume a file in the Git repository is unchanged. .DESCRIPTION This function makes Git "assume" the file has not been changed, meaning that Git will ignore any changes to that file. The file will not appear in 'git status' and it will not be checked for changes. This is useful for files with local changes that are needed, but should not be committed. .PARAMETER FilePath The relative or absolute path to the file to mark as "assume unchanged". .PARAMETER GitPath The relative or absolute path to the file to mark as "assume unchanged". #> [Alias('Ignore-GitFileChanges')] [OutputType([Void])] param( [Parameter(Mandatory)] [Validation.ValidatePathExists('File')] [string]$FilePath ) $Item = Get-Item -Path $FilePath -Force -ErrorAction Stop Push-Location (Get-GitPath -Path $Item.Parent) try { $StatusInfo = [GitStatusInfo]::GetCurrentStatusInfo() if ($StatusInfo.Staged.FullName -contains $Item.FullName) { # Reset the item so that it is no longer staged git reset HEAD -- $Item.FullName } git update-index --assume-unchanged $FilePath } finally { Pop-Location } } $ExportedMembers.Add('Set-GitFileAssumeUnchanged') if ($IsLinux) { function Set-GitSettings { <# .SYNOPSIS Configures default git settings for a Linux PowerShell environment. .DESCRIPTION This function sets up the default SSH and GPG settings for git within a Linux PowerShell environment. It is not designed for use on non-Linux platforms. .PARAMETER GithubSSHKeyPath The file path to the SSH key you want to use for GitHub. If no key is provided then the function will use the default id_ed25519 key if it exists or prompts the user to select an available key. .EXAMPLE # Configures git settings using the default or user-selected SSH key. Set-GitSettings .EXAMPLE # Configures git settings using the specified SSH key at '~/.ssh/my_key'. Set-GitSettings -GithubSSHKeyPath '~/.ssh/my_key' #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter(Mandatory = $false)] [IO.FileInfo]$GithubSSHKeyPath ) $SSHAgent = 'ssh-agent' $SSHAdd = 'ssh-add' if ( ( (Test-ApplicationExistsInPath -ApplicationName $SSHAgent) -and (Test-ApplicationExistsInPath -ApplicationName $SSHAdd) ) -ne $true ) { throw 'Missing required applications. Task requires both ssh-agent and ssh-add.' } try { & $SSHAgent -k *>&1 | Out-Null } catch { } $Output = & $SSHAgent -s $Env:SSH_AUTH_SOCK = $Output[0].Split('=')[1].split(';')[0] $Env:SSH_AGENT_PID = $Output[1].Split('=')[1].split(';')[0] $Env:GPG_TTY = tty # Find the right SSH key to use for github. $SSH = if (Test-Path $GithubSSHKeyPath -PathType Leaf -ErrorAction Ignore) { # If the user profides a path, and it exists, use that. $GithubSSHKeyPath } elseif (Test-Path '~/.ssh/id_ed25519' -PathType Leaf -ErrorAction Ignore) { # If no user provided key exists then try github default recommended. (Get-Item '~/.ssh/id_ed25519').FullName } else { # last resort - ask the user. $Keys = Get-ChildItem -Path '~/.ssh/' -File | Where-Object { ($_.Extension -ine '.pub') -and ($_.name -ine 'known_hosts') } if ($Keys) { $Index = 0 $KeyList = Foreach ($Key in $Keys) { [PSCustomObject]@{ Number = $Index Path = $Key.FullName } ++$Index } $PromptAnswer = Read-Host -Prompt ( "Enter the number for the ssh key you want to use.`n" + ($KeyList | Format-Table | Out-String) ) $Keys[$PromptAnswer].FullName } else { Write-Error 'No SSH Keys found.' } } & $SSHAdd $SSH } $ExportedMembers.Add('Set-GitSettings') } Export-ModuleMember -Function $ExportedMembers.ToArray() |