Skip to content

Instantly share code, notes, and snippets.

@tsmarvin
Last active December 24, 2023 23:51
Show Gist options
  • Save tsmarvin/835fb35a18d3d7c9d2e09455bcd2c04e to your computer and use it in GitHub Desktop.
Save tsmarvin/835fb35a18d3d7c9d2e09455bcd2c04e to your computer and use it in GitHub Desktop.
TM-GitUtility

TM-GitUtility Module

Introduction

TM-GitUtility is a PowerShell module that provides various Git related utility functions. This module simplifies Git operations within the PowerShell environment, making it easier to manage Git repositories and their associated workflows.

This module is part of a suite of tools designed to improve and streamline the PowerShell command line and scripting experience.
Check out the rest of the modules by visiting my page on the PowerShell Gallery.

Features

  • Get-GitBranch: Retrieves the current Git branch along with a relevant symbol for master/main or development branches for easy identification.
  • Get-GitPath: Looks up the file system hierarchy for a .git directory and returns the path to the Git repository if found.
  • Get-GitStatusInfo: Instantiates a GitStatusInfo object that encapsulates detailed status information of a Git repository, such as lists of updated, unchanged, staged, untracked, or ignored files.
  • Redo-GitCommitAsSigned: Amends the last Git commit to include a signature, converting it to a signed commit which is useful for project repositories that enforce commit-signing.
  • Remove-GitTag: Deletes a specified Git tag both from the local, and the remote repository if specified, effectively untagging specific commits in the version history.
  • Set-GitFileAssumeUnchanged: Tells Git to assume that specific files are unchanged, which is useful when you want to ignore local changes to files without modifying .gitignore.
  • Set-GitSettings (Linux only): Configures default Git settings for a Linux PowerShell environment, including SSH and GPG settings. Not designed for non-Linux platforms.

Requirements

Installation

Install TM-GitUtility from the PowerShell Gallery:

Install-Module TM-GitUtility -Scope CurrentUser -Repository PSGallery

For manual installation, download the module files and place them in a "TM-GitUtility" folder in your PowerShell modules directory ($Env:PSModulePath).

MIT License
Copyright (c) 2023 Taylor Marvin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@{
# Script module or binary module file associated with this manifest.
RootModule = 'TM-GitUtility.psm1'
# Version number of this module.
ModuleVersion = '0.0.8'
# Supported PSEditions
CompatiblePSEditions = @('Desktop','Core')
# ID used to uniquely identify this module
GUID = 'a0331b25-5435-4ee7-9638-6729d75afe88'
# Author of this module
Author = 'Taylor Marvin'
# Company or vendor of this module
CompanyName = 'N/A'
# Copyright statement for this module
Copyright = 'Taylor Marvin (2023)'
# Description of the functionality provided by this module
Description = 'Provides various git related utility functions.'
# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '5.1'
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
@{ModuleName='TM-RandomUtility'; ModuleVersion='0.0.7'; GUID='c07a9da5-9562-42b6-8aba-1279fdb25a8e'},
@{ModuleName='TM-ValidationUtility'; ModuleVersion='0.0.4'; GUID='1f1eebe8-7a0b-49ae-901e-c877f090a7fc'}
)
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Get-GitBranch', 'Get-GitPath', 'Get-GitStatusInfo', 'Redo-GitCommitAsSigned', 'Remove-GitTag', 'Set-GitFileAssumeUnchanged', 'Set-GitSettings')
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('Profile', 'Utility', 'Git')
# A URL to the license for this module.
LicenseUri = 'https://gist.github.com/tsmarvin/835fb35a18d3d7c9d2e09455bcd2c04e#file-license'
# A URL to the main website for this project.
ProjectUri = 'https://gist.github.com/tsmarvin/835fb35a18d3d7c9d2e09455bcd2c04e'
# Prerelease string of this module
# Prerelease = ''
# Flag to indicate whether the module requires explicit user acceptance for install/update/save
RequireLicenseAcceptance = $false
}
}
}
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 -or $IsMacOS) {
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment