Extensions/Git.Commit.Input.UGit.Extension.ps1
<# .SYNOPSIS Git Commit Input .DESCRIPTION Makes Git Commit easier to use from PowerShell by providing parameters for the -Message, -Title, -Body, and -Trailers .EXAMPLE git commit -Title "Fixing Something" .EXAMPLE git commit -Title "Changing Stuff" -Trailers @{"Co-Authored-By"="SOMEONE ELSE <Someone@Else.com>"} #> [ValidatePattern('^git commit')] [Management.Automation.Cmdlet('Use','Git')] [CmdletBinding(PositionalBinding=$false)] param( # The title of the commit. If -Message is also provided, this will become part of the -Body [Alias('Subject')] [string] $Title, # The commit message. [string] $Message, # The type of the commit. This uses the conventional commits format. # https://www.conventionalcommits.org/en/v1.0.0/#specification [ArgumentCompleter({ param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) if ($wordToComplete) { @($ugit.ConventionalCommits.Types) -like "$WordToComplete*" -replace '^', "'" -replace '$',"'" } else { $ugit.ConventionalCommits.Types -replace '^', "'" -replace '$',"'" } })] [string] $Type, # The scope of the commit. This uses the conventional commits format. # https://www.conventionalcommits.org/en/v1.0.0/#specification [string] $Scope, # A description of the commit. This uses the conventional commits format. # https://www.conventionalcommits.org/en/v1.0.0/#specification [string] $Description, # The footer for the commit. This uses the conventional commits format. # https://www.conventionalcommits.org/en/v1.0.0/#specification [string] $Footer, # The body of the commit. [string] $Body, # Any git trailers to add to the commit. # git trailers are key-value pairs you can use to associate metadata with a commit. # As this uses --trailer, this requires git version 2.33 or greater. [Alias('Trailer','CommitMetadata','GitMetadata')] [Collections.IDictionary] $Trailers = [Ordered]@{}, # If set, will amend an existing commit. [switch] $Amend, # The commit date. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Date','Time','DateTime','Timestamp')] [datetime] $CommitDate, # If provided, will mark this commit as a fix. # This will add 'Fixes #...' to your commit message. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Fixes','Fixed')] [string[]] $Fix, # If provided, will mark this commit as a close. # This will add 'Closes #...' to your commit message. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Closed','Closes')] [string[]] $Close, # If provided, will mark this commit as a resolution. # This will add 'Resolves #...' to your commit message. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Resolves','Resolved')] [string[]] $Resolve, # If provided, will mark this commit as referencing an issue. # This will add 'Re #...' to your commit message. [Parameter(ValueFromPipelineByPropertyName)] [Alias('Re','Regard','Regards','Regarding','References')] [string[]] $Reference, # If provided, will mark this commit as co-authored by one or more people. [Parameter(ValueFromPipelineByPropertyName)] [Alias('CoAuthor','CoAuthors')] [ValidateScript({ if ($_ -notmatch "(?<Author>[^\<\>]+)\<(?<Email>[^\<\>]+)\>") { throw "Co-Authored-By must be in the format 'Name <Email>'" } return $true })] [string[]] $CoAuthoredBy, # If provided, will mark this commit as on-behalf-of one or more people. [Parameter(ValueFromPipelineByPropertyName)] [Alias('OnBehalf')] [ValidateScript({ if ($_ -notmatch "(?<Author>[^\<\>]+)\<(?<Email>[^\<\>]+)\>") { throw "On-Behalf-Of must be in the format 'Name <Email>'" } return $true })] [string[]] $OnBehalfOf, # If set, will add `[skip ci]` to the commit message. # This will usually prevent a CI/CD system from running a build. # This is supported by GitHub Workflows, Azure DevOps Pipelines, and GitLab (to name a few). [Alias('CISkip','NoCI','SkipActions','ActionSkip')] [switch] $SkipCI ) filter FixGitUserName { $onBehalf = $_ if ($onBehalf -match '\<(?<Login>\D.+?)@github.com\>$') { if (-not $script:KnownGitHubUsers) { $script:KnownGitHubUsers = @{} } $gitUserName = $matches.Login if (-not $script:KnownGitHubUsers[$gitUserName]) { $script:KnownGitHubUsers[$gitUserName] = Invoke-RestMethod "https://api.github.com/users/$gitUserName" } $gitUser = $script:KnownGitHubUsers[$gitUserName] if (-not $gitUser) { $onBehalf } else { $gitUser.name, "<$($gitUser.id)+$($gitUser.login)@users.noreply.github.com>" -join ' ' } } else { $onBehalf } } $MyParameters = [Ordered]@{} + $PSBoundParameters # git commit -m can accept multiple messages, but the first message is somewhat special. # (trailers cannot exist in the first message, and it's considered the subject by many other parts of git) # So we want several potential things to become "-m", and we have to do this in the right order. $Fixes = @( $IssuePattern = '^\#?\d+$' $IssueReplace = "^\#?" if ($Close) { $Close -match $IssuePattern -replace $IssueReplace, "Closes #" } if ($Fix) { $Fix -match $IssuePattern -replace $IssueReplace, "Fixes #" } if ($Resolve) { $Resolve -match $IssuePattern -replace $IssueReplace, "Resolves #" } if ($Reference) { $Reference -match $IssuePattern -replace $IssueReplace, "re #" } ) -join ', ' # First up is Convential Commits if ($type) # (if -Type was provided) { if (-not $Description) { if ($Title) { $Description = $title $title = '' } elseif ($Message) { $Description = $Message $Message = '' } elseif ($body) { $Description = $Body $Message = '' } } "-m" # construct a conventional commit message. "${type}$(if ($scope) { "($scope)" }): $Description$( if ($Fixes) { " ( $fixes )"} )$( if ($SkipCI) { " [skip ci]" } )" } # If title was provided, pass it as a message elseif ($Title) { if ($Fix) { if ($Title) {"-m";"$title$(if ($Fixes) { " ( $fixes )"})$( if ($SkipCI) { " [skip ci] " } )"} } else { if ($Title) {"-m";"$title$( if ($SkipCI) { " [skip ci] " } )"} } } # If -Message was provided, pass that as a message, too. if ($Message) { "-m" $message } # If Body was provided, it counts as a message. if ($Body) {"-m";$body} elseif ( # f someone passed description but not type, that should also count. $Description -and -not $type ) { "-m";$Description } if ($Footer) { "-m";$Footer } # If CoAuthoredBy was provided, add it as a trailer. if ($CoAuthoredBy) { if (-not $trailers['Co-authored-by']) { $Trailers['Co-authored-by'] = @() } $Trailers['Co-authored-by'] += foreach ($coAuthor in $CoAuthoredBy) { $coAuthor | FixGitUserName } } # If OnBehalfOf was provided, add it as a trailer. if ($OnBehalfOf) { if (-not $trailers['on-behalf-of']) { $Trailers['on-behalf-of'] = @() } $Trailers['on-behalf-of'] += foreach ($onBehalf in $OnBehalfOf) { $onBehalf | FixGitUserName } } if ($Trailers.Count) { foreach ($kv in $Trailers.GetEnumerator()) { foreach ($val in $kv.Value) { "--trailer=$($kv.Key -replace ':','_colon_' -replace '\s', '-')=$val" } } } if ($amend) { "--amend" } if ($CommitDate) { "--date" $CommitDate.ToString("o") } |