stitch.psm1
using namespace Markdig using namespace Markdig.Syntax using namespace System.Management.Automation.Language using namespace System.Diagnostics.CodeAnalysis using namespace System.Collections.Specialized #Region '.\enum\ModuleFlag.ps1' -1 [Flags()] enum ModuleFlag { None = 0x00 HasManifest = 0x01 HasModule = 0x02 } #EndRegion '.\enum\ModuleFlag.ps1' 7 #Region '.\private\Changelog\Format-ChangelogEntry.ps1' -1 function Format-ChangelogEntry { <# .SYNOPSIS Format the entry text by replacing tokens from the config file with their values .DESCRIPTION Format-ChangelogEntry uses the Format.Entry line from the config file to format the Entry line in the Changelog. The following fields are available in an Entry: | Field | Pattern | |-------------|---------------| | Description | `{desc}` | | Type | `{type}` | | Scope | `{scope}` | | Title | `{title}` | | Sha | `{sha}` | | Author | `{author}` | | Email | `{email}` | | Footer | `{ft.<name>}` | .EXAMPLE $Entry | Format-ChangelogEntry #> [CmdletBinding()] param( # Information about the Entry (commit) object [Parameter( ValueFromPipeline )] [object]$EntryInfo ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $DEFAULT_FORMAT = '- {sha} {desc} ({author})' $DEFAULT_BREAKING_FORMAT = '- {sha} **breaking change** {desc} ({author})' $config = Get-ChangelogConfig $descriptionPattern = '\{desc\}' $typePattern = '\{type\}' $scopePattern = '\{scope\}' $titlePattern = '\{title\}' $shaPattern = '\{sha\}' $authorPattern = '\{author\}' $emailPattern = '\{email\}' $footerPattern = '\{ft\.(\w+)\}' } process { if ($EntryInfo.IsBreakingChange) { $formatOptions = $config.Format.BreakingChange ?? $DEFAULT_BREAKING_FORMAT } else { $formatOptions = $config.Format.Entry ?? $DEFAULT_FORMAT } $format = $formatOptions -replace $descriptionPattern , $EntryInfo.Description $format = $format -replace $typePattern , $EntryInfo.Type $format = $format -replace $scopePattern , $EntryInfo.Scope $format = $format -replace $titlePattern , $EntryInfo.Title $format = $format -replace $shaPattern , $EntryInfo.ShortSha $format = $format -replace $authorPattern , $EntryInfo.Author.Name $format = $format -replace $emailPattern , $EntryInfo.Author.Email if ($format -match $footerPattern) { if ($matches.Count -gt 0) { if ($EntryInfo.Footers[$Matches.1]) { $format = $format -replace "\{ft\.$($Matches.1)\}", ($EntryInfo.Footers[$Matches.1] -join ', ') } } } } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-ChangelogEntry.ps1' 77 #Region '.\private\Changelog\Format-ChangelogFooter.ps1' -1 function Format-ChangelogFooter { <# .SYNOPSIS Format the footer in the Changelog .EXAMPLE Format-ChangelogFooter #> [OutputType([string])] [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $DEFAULT_FORMAT = '' $config = Get-ChangelogConfig if (-not([string]::IsNullorEmpty($config.Footer))) { $formatOptions = $config.Footer } else { $formatOptions = $DEFAULT_FORMAT } } process { #! There are no replacements in the footer yet $format = $formatOptions } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-ChangelogFooter.ps1' 32 #Region '.\private\Changelog\Format-ChangelogGroup.ps1' -1 function Format-ChangelogGroup { <# .SYNOPSIS Format the heading of a group of changelog entries .EXAMPLE $group | Format-ChangelogGroup #> [OutputType([string])] [CmdletBinding()] param( # A table of information about a changelog group [Parameter( ValueFromPipeline )] [object]$GroupInfo ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $DEFAULT_FORMAT = '### {name}' $config = Get-ChangelogConfig $formatOptions = ($config.Format.Group ?? $DEFAULT_FORMAT) $namePattern = '\{name\}' } process { Write-Debug "Format was '$formatOptions'" Write-Debug "GroupInfo is $($GroupInfo | ConvertTo-Psd)" if (-not ([string]::IsNullorEmpty($GroupInfo.DisplayName))) { Write-Debug " - DisplayName is $($GroupInfo.DisplayName)" $format = $formatOptions -replace $namePattern, $GroupInfo.DisplayName } elseif (-not ([string]::IsNullorEmpty($GroupInfo.Name))) { $format = $formatOptions -replace $namePattern, $GroupInfo.Name Write-Debug " - Name is $($GroupInfo.Name)" } } end { Write-Debug "Format is '$format'" $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-ChangelogGroup.ps1' 45 #Region '.\private\Changelog\Format-ChangelogHeader.ps1' -1 function Format-ChangelogHeader { <# .SYNOPSIS Format the header in the Changelog .EXAMPLE Format-ChangelogHeader #> [OutputType([string])] [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $DEFAULT_FORMAT = '' $config = Get-ChangelogConfig if (-not([string]::IsNullorEmpty($config.Header))) { $formatOptions = $config.Header } else { $formatOptions = $DEFAULT_FORMAT } } process { #! There are no replacements in the header yet $format = $formatOptions } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-ChangelogHeader.ps1' 34 #Region '.\private\Changelog\Format-ChangelogRelease.ps1' -1 function Format-ChangelogRelease { <# .SYNOPSIS Format the heading for a release in the changelog by replacing tokens form the config file with thier values .DESCRIPTION Format-ChangelogRelease uses the Format.Release line from the config file to format the Release heading in the Changelog. The following fields are available in a Release: | Field | Pattern | |-------------------|--------------------------| | Name | `{name}` | | Date | `{date}` | | Date with Format | `{date yyyy-MM-dd}` | >EXAMPLE $release | Format-ChangelogRelease #> [CmdletBinding()] param( # A table of information about a release [Parameter( ValueFromPipeline )] [object]$ReleaseInfo ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $DEFAULT_FORMAT = '## [{name}] - {date yyyy-MM-dd}' $config = Get-ChangelogConfig $formatOptions = $config.Format.Release ?? $DEFAULT_FORMAT $namePattern = '\{name\}' $datePattern = '\{date\}' $dateFormatPattern = '\{date (?<df>.*?)\}' } process { Write-Debug " Items: $($ReleaseInfo.Keys)" $format = $formatOptions -replace $namePattern, $ReleaseInfo.Name # date if (-not([string]::IsNullorEmpty($ReleaseInfo.Timestamp))) { if ($format -match $dateFormatPattern) { if ($ReleaseInfo.Timestamp -is [System.DateTimeOffset]) { $dateText = (Get-Date $ReleaseInfo.Timestamp.UtcDateTime -Format $dateFormat) } else { $dateText = (Get-Date $ReleaseInfo.Timestamp -Format $dateFormat) } $dateField = $Matches.0 # we want to replace the whole field so store that $dateFormat = $Matches.df # the format of the datetime object $format = $format -replace $dateField , $dateText } else { $format = $format -replace $datePattern, $ReleaseInfo.Timestamp } } } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-ChangelogRelease.ps1' 68 #Region '.\private\Changelog\Format-HeadingText.ps1' -1 function Format-HeadingText { <# .SYNOPSIS If the given Heading Block is a LinkInline, recreate the markdown link text, if not return the headings content .EXAMPLE $heading | Format-HeadingText .EXAMPLE $heading | Format-HeadingText -NoLink #> [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [Markdig.Syntax.HeadingBlock]$Heading, # Return the text only without link markup [Parameter( )] [switch]$NoLink ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $headingText = '' } process { $child = $Heading.Inline.FirstChild while ($null -ne $child) { if ($child -is [Markdig.Syntax.Inlines.LinkInline]) { if ($NoLink) { $headingText = $child.FirstChild.Content.ToString() }else { Write-Debug ' - creating link text' $headingText += ( -join ('[', $child.FirstChild.Content.ToString(), ']')) Write-Debug " - $headingText" $headingText += ( -join ('(', $child.Url, ')' )) Write-Debug " - $headingText" } } else { $headingText += $child.Content.ToString() } $child = $child.NextSibling } } end { $headingText Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Format-HeadingText.ps1' 53 #Region '.\private\Changelog\Resolve-ChangelogGroup.ps1' -1 function Resolve-ChangelogGroup { <# .SYNOPSIS Given a git commit and a configuration identify what group the commit should be in .EXAMPLE Get-GitCommit | ConvertFrom-ConventionalCommit | Resolve-ChangelogGroup #> [CmdletBinding()] param( # A conventional commit object [Parameter( ValueFromPipeline )] [PSTypeName('Git.ConventionalCommitInfo')][Object[]]$Commit ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $config = Get-ChangelogConfig } process { Write-Debug "Processing Commit : $($Commit.Title)" foreach ($key in $config.Groups.Keys) { $group = $config.Groups[$key] $display = $group.DisplayName ?? $key $group['Name'] = $key Write-Debug "Checking group $key" switch ($group.Keys) { 'Type' { if (($null -ne $Commit.Type) -and ($group.Type.Count -gt 0)) { Write-Debug " - Has Type entries" foreach ($type in $group.Type) { Write-Debug " - Checking for a match with $type" if ($Commit.Type -match $type) { return $group } } } continue } 'Title' { if (($null -ne $Commit.Title) -and ($group.Title.Count -gt 0)) { Write-Debug " - Has Title entries" foreach ($title in $group.Title) { Write-Debug " - Checking for a match with $title" if ($Commit.Title -match $title) { return $group } } } continue } 'Scope' { if (($null -ne $Commit.Scope) -and ($group.Scope.Count -gt 0)) { Write-Debug " - Has Scope entries" foreach ($scope in $group.Scope) { Write-Debug " - Checking for a match with $scope" if ($Commit.Scope -match $scope) { return $group } } } continue } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Changelog\Resolve-ChangelogGroup.ps1' 75 #Region '.\private\FeatureFlags\Disable-FeatureFlag.ps1' -1 function Disable-FeatureFlag { [CmdletBinding()] param( # The name of the feature flag to disable [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]]$Name, # The description of the feature flag [Parameter( )] [string]$Description ) begin { #TODO: I'm relying on BuildInfo, because I don't see a scenario right now where we would use this without it } process { if ($null -ne $BuildInfo) { if ($BuildInfo.Keys -contains 'Flags') { if ($BuildInfo.Flags.ContainsKey($Name)) { $BuildInfo.Flags[$Name].Enabled = $true } else { $BuildInfo.Flags[$Name] = @{ Enabled = $true Description = $Description ?? "Missing description" } } } } } end { } } #EndRegion '.\private\FeatureFlags\Disable-FeatureFlag.ps1' 38 #Region '.\private\FeatureFlags\Enable-FeatureFlag.ps1' -1 function Enable-FeatureFlag { [CmdletBinding()] param( # The name of the feature flag to enable [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]]$Name, # The description of the feature flag [Parameter( )] [string]$Description ) begin { #TODO: I'm relying on BuildInfo, because I don't see a scenario right now where we would use this without it } process { if ($null -ne $BuildInfo) { if ($BuildInfo.Keys -contains 'Flags') { if ($BuildInfo.Flags.ContainsKey($Name)) { $BuildInfo.Flags[$Name].Enabled = $true } else { $BuildInfo.Flags[$Name] = @{ Enabled = $true Description = $Description ?? "Missing description" } } } } } end { } } #EndRegion '.\private\FeatureFlags\Enable-FeatureFlag.ps1' 38 #Region '.\private\InvokeBuild\Invoke-TaskNameCompletion.ps1' -1 function Invoke-TaskNameCompletion { <# .SYNOPSIS Complete the given task name .NOTES The Parameter that uses this function must be named 'Name' #> param( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) Write-Debug "Command $commandName parameter $parameterName with '$wordToComplete'" $possibleValues = (Invoke-Build ? | Select-Object -ExpandProperty Name) if ($fakeBoundParameters.ContainsKey('Name')) { $possibleValues | Where-Object { $_ -like "$wordToComplete*" } } else { $possibleValues | ForEach-Object { $_ } } } #EndRegion '.\private\InvokeBuild\Invoke-TaskNameCompletion.ps1' 27 #Region '.\private\Markdown\Add-MarkdownElement.ps1' -1 function Add-MarkdownElement { [CmdletBinding()] param( # Markdown element(s) to add to the document [Parameter( Position = 0 )] [Object]$Element, # The document to add the element to [Parameter( Position = 1, ValueFromPipeline )] [ref]$Document, # Index to insert the Elements to, append to end if not specified [Parameter( Position = 2 )] [int]$Index, # Return the updated document to the pipeline [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { } end { if ($PSBoundParameters.ContainsKey('Index')) { $Document.Value.Insert($Index, $Element) } else { $Document.Value.Add($Element) } # if ($PassThru) { $Document.Value | Write-Output -NoEnumerate } Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Add-MarkdownElement.ps1' 43 #Region '.\private\Markdown\Get-MarkdownDescendant.ps1' -1 #using namespace Markdig #using namespace Markdig.Syntax function Get-MarkdownDescendant { [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [MarkdownObject]$InputObject, # The type of element to return [Parameter( Position = 0 )] [string]$TypeName ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('TypeName')) { #Check Type $type = $TypeName -as [Type] if (-not $type) { throw "Type: '$TypeName' not found" } $methodDescendants = [MarkdownObjectExtensions].GetMethod('Descendants', 1, [MarkdownObject]) $mdExtensionsType = [MarkdownObjectExtensions] $method = $methodDescendants.MakeGenericMethod($Type) $method.Invoke($mdExtensionsType, @(, $InputObject)) | ForEach-Object { $PSCmdlet.WriteObject($_, $false) } } else { [MarkdownObjectExtensions]::Descendants($InputObject) } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Get-MarkdownDescendant.ps1' 42 #Region '.\private\Markdown\Get-MarkdownElement.ps1' -1 function Get-MarkdownElement { [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [Markdig.Syntax.MarkdownObject]$InputObject, # The type of element to return [Parameter( Position = 0 )] [string]$TypeName ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { } end { #Check Type if ($TypeName -notmatch '^Markdig\.Syntax') { $TypeName = 'Markdig.Syntax.' + $TypeName } $type = $TypeName -as [Type] if (-not $type) { throw "Type: '$TypeName' not found" } Write-Verbose "Looking for a $type" foreach ($token in $InputObject) { if ($token -is $type) { $token | Write-Output } } Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Get-MarkdownElement.ps1' 39 #Region '.\private\Markdown\Get-MarkdownFrontMatter.ps1' -1 function Get-MarkdownFrontMatter { [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [Markdig.Syntax.MarkdownDocument]$InputObject ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Get-MarkdownElement -InputObject $InputObject -TypeName 'Markdig.Extensions.Yaml.YamlFrontMatterBlock' } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Get-MarkdownFrontMatter.ps1' 20 #Region '.\private\Markdown\Get-MarkdownHeading.ps1' -1 function Get-MarkdownHeading { [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [Markdig.Syntax.MarkdownObject[]]$InputObject ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSItem -is [Markdig.Syntax.HeadingBlock]) { $PSItem | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Get-MarkdownHeading.ps1' 22 #Region '.\private\Markdown\Import-Markdown.ps1' -1 function Import-Markdown { [CmdletBinding()] [OutputType([Markdig.Syntax.MarkdownDocument])] param( # A markdown file to be converted [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # Additional extensions to add # Note advanced and yaml already added [Parameter( )] [string[]]$Extension, # Enable track trivia [Parameter( )] [switch]$TrackTrivia ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { try { $content = Get-Content $Path -Raw $builder = New-Object Markdig.MarkdownPipelineBuilder $builder = [Markdig.MarkdownExtensions]::Configure($builder, 'advanced+yaml') $builder.PreciseSourceLocation = $true if ($TrackTrivia) { $builder = [Markdig.MarkdownExtensions]::EnableTrackTrivia($builder) } [Markdig.Syntax.MarkdownDocument]$document = [Markdig.Parsers.MarkdownParser]::Parse( $content , $builder.Build() ) $PSCmdlet.WriteObject($document, $false) } catch { $PSCmdlet.ThrowTerminatingError($_) } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Import-Markdown.ps1' 53 #Region '.\private\Markdown\New-MarkdownElement.ps1' -1 function New-MarkdownElement { [CmdletBinding( ConfirmImpact = 'Low' )] param( # Text to parse into Markdown Element(s) [Parameter( ValueFromPipeline )] [string[]]$InputObject ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $collect = @() } process { $collect += $InputObject } end { [Markdig.Markdown]::Parse( ($collect -join [System.Environment]::NewLine) , $true ) | Write-Output -NoEnumerate Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\New-MarkdownElement.ps1' 25 #Region '.\private\Markdown\Write-MarkdownDocument.ps1' -1 function Write-MarkdownDocument { [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [Markdig.Syntax.MarkdownObject]$InputObject ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $sw = [System.IO.StringWriter]::new() $rr = [Markdig.Renderers.Roundtrip.RoundtripRenderer]::new($sw) $rr.Write($InputObject) $sw.ToString() | Write-Output } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Markdown\Write-MarkdownDocument.ps1' 23 #Region '.\private\SourceInfo\Get-SourceItemInfo.ps1' -1 #using namespace System.Management.Automation.Language function Get-SourceItemInfo { [CmdletBinding()] param( # The directory to look in for source files [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, # The root directory of the source item, using the convention of a # source folder with one or more module folders in it. # Should be the Module's Source folder of your project [Parameter( Position = 0 )] [string]$Root, # Path to the source type map [Parameter( )] [string]$TypeMap ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" @( @{ Option = 'Constant' Name = 'POWERSHELL_FILETYPES' Value = @( '.ps1', '.psm1' ) Description = 'Files that are Parsable into an AST' } @{ Option = 'Constant' Name = 'DATA_FILETYPES' Value = @( '.psd1' ) Description = 'PowerShell Data files' } ) | ForEach-Object { New-Variable @_ } try { if ($PSBoundParameters.ContainsKey('TypeMap')) { $map = Get-SourceTypeMap -Path $TypeMap } else { # try to load defaults $map = Get-SourceTypeMap } } catch { #TODO: It would be better to have a minimal source map to fall back to throw "Could not find map for source types`n$_" } } process { :path foreach ($p in $Path) { Write-Debug "Processing $p" #------------------------------------------------------------------------------- #region Load file try { $fileItem = Get-Item $p -ErrorAction Stop $itemProperties = $fileItem.psobject.Properties | Select-Object -ExpandProperty Name } catch { Write-Error "Could not read $p`n$_" continue path } #endregion Load file #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Create sourceItem object $sourceObject = @{ PSTypeName = 'Stitch.SourceItemInfo' Path = $fileItem.FullName BaseName = $fileItem.BaseName FileName = $fileItem.Name Name = $fileItem.BaseName FileType = '' Ast = '' Tokens = @() ParseErrors = @() Directory = '' Module = '' Type = '' Component = '' Visibility = '' Verb = '' Noun = '' } #endregion Create sourceItem object #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region File types # Any pattern listed in the FileTypes key will be considered a SourceItem $fileTypes = $map.FileTypes :filemap foreach ($fileMap in $fileTypes.GetEnumerator()) { $pattern = $fileMap.Key $properties = $fileMap.Value if ($fileItem.Name -match $pattern) { foreach ($property in $properties.GetEnumerator()) { $sourceObject[$property.Key] = $property.Value } } } $sourceObject.FileType = $fileType ?? 'Source File' #endregion File types #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Parse file # The filetypes listed in POWERSHELL_FILETYPES are the extensions that are able to # be parsed into an AST #TODO: Move these into a key in sourcetype config if ($POWERSHELL_FILETYPES -contains $fileItem.Extension) { try { Write-Debug ' - Parsing powershell' $tokens = @() $parseErrors = @() $ast = [Parser]::ParseFile($fileItem.FullName, [ref]$tokens, [ref]$parseErrors) if ($null -ne $ast) { $sourceObject.Ast = $ast $sourceObject.Tokens = $tokens $sourceObject.ParseErrors = $parseErrors } } catch { Write-Warning "Could not parse source item $($fileItem.FullName)`n$_" } # The filetypes listed in DATA_FILETYPES are able to be imported into the 'Data' field # currently, only psd1 files are listed } elseif ($DATA_FILETYPES -contains $fileItem.Extension) { switch -Regex ($fileItem.Extension) { '^\.psd1$' { try { $sourceObject['Data'] = Import-Psd $fileItem.FullName -Unsafe } catch { Write-Warning "Could not import data from $($fileItem.FullName)`n$_" } } } } #endregion Parse file #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Root directory if ([string]::IsNullorEmpty($Root)) { Write-Debug ' - No Root path given. Attempting to resolve from project root' $projectRoot = Resolve-ProjectRoot if ($null -eq $projectRoot) { Write-Verbose ' - Could not resolve the Project Root' $possibleBuildRoot = $PSCmdlet.GetVariableValue('BuildRoot') if ($null -ne $possibleBuildRoot) { Write-Debug ' - Using BuildRoot as root' $projectRoot = $possibleBuildRoot } else { Write-Debug ' - Using current location as root' $projectRoot = Get-Location } Write-Verbose " - Project root is : $projectRoot" } $relativeToProject = [System.IO.Path]::GetRelativePath($projectRoot, $fileItem.FullName) $projectPathParts = $relativeToProject -split [regex]::Escape([System.IO.Path]::DirectorySeparatorChar) $rootName = $projectPathParts[0] Write-Debug " - Guessing $rootName is the Source directory" $Root = (Join-Path $projectRoot $rootName) Write-Verbose " - Setting Root to $Root" } if ([string]::IsNullorEmpty($Root)) { throw 'Could not determine the Root directory for SourceItems' } #endregion Root directory #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Match path Write-Debug "Getting relative path from root '$Root'" $adjustedPath = [System.IO.Path]::GetRelativePath($Root, $fileItem.FullName) Write-Debug " - '$($fileItem.FullName)' adjusted path is '$adjustedPath'" $sourceObject['ProjectPath'] = $adjustedPath $pathItems = [System.Collections.ArrayList]@( $adjustedPath -split [regex]::Escape([System.IO.Path]::DirectorySeparatorChar) ) #! The first item should be the module, second is the directory $sourceObject['Directory'] = $pathItems[1] Write-Debug "Path Items for ${adjustedPath}: $($pathItems -join ', ')" Write-Debug "Matching 'Path' settings in Source Types Configuration" #! levels is an Array of hashes $levels = $map.Path :level foreach ($level in $levels) { #------------------------------------------------------------------------------- #region depth check $pathItemIndex = $levels.IndexOf($level) #! There are more levels configured than there are level in this sourceItem. #! break out of the level loop if ($pathItemsIndex -ge $pathItems.Count) { Write-Debug " - Index is $pathItemsIndex. No more path items" break level } #endregion depth check #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Path level # pathField is the current component of the path when it was split, and level is the hashtable # from the sourcetype map config $pathField = $pathItems[$pathItemIndex] # The user has the option of setting the pathField to a property of the sourceObject by # adding an entry in the $levels Array as a string # or using a regex to set values by adding a hashtable with the regex as the key and a hashtable of # sourceObject property => value if ($level -is [String]) { Write-Debug " - level $pathItemIndex is $level" $sourceObject[$level] = $pathField Write-Debug " - $level => $pathField" continue level } elseif ($level -is [hashtable]) { Write-Debug " - level $pathItemIndex is a hashtable" foreach ($levelMap in $level.GetEnumerator()) { $pattern = $levelMap.Key $properties = $levelMap.Value Write-Debug " - testing if $pathField matches $pattern" if ($pathField -match $pattern) { # Save the matches so we can use them when we match on the values below $pathFieldMatches = $Matches foreach ($property in $properties.GetEnumerator()) { #! if the value has `{<num>}` then use that match group as the value if ($property.Value -match '\{(\d+)\}') { $matchNumber = [int]$Matches.1 Write-Debug " - Match number: $($property.Key) => $($pathFieldMatches[$matchNumber])" if (-not ([string]::IsNullorEmpty($pathFieldMatches[$matchNumber]))) { $sourceObject[$property.Key] = $pathFieldMatches[$matchNumber] } #! if the value has `{<word>}` then use that match group as the value } elseif ($property.Value -match '\{(\w+)\}') { $matchWord = $Matches.1 if (-not ([string]::IsNullorEmpty($pathFieldMatches[$matchWord]))) { Write-Debug " - Match word: $($property.Key) => $($pathFieldMatches[$matchWord])" $sourceObject[$property.Key] = $pathFieldMatches[$matchWord] } } else { $sourceObject[$property.Key] = $property.Value } } } } continue level } } #endregion Path level #------------------------------------------------------------------------------- } #endregion Match path #------------------------------------------------------------------------------- $mapProperties = $map.Keys foreach ($mapProperty in $mapProperties) { # if the key maps to a property of the fileItem if ($itemProperties -contains $mapProperty) { # The fileItem field we are going to compare against $field = $mapProperty Write-Debug "Matching $field settings in sourcetypes" foreach ($fieldMap in $map[$field].GetEnumerator()) { $pattern = $fieldMap.Key $properties = $fieldMap.Value if ($fileItem.($field) -match $pattern) { Write-Debug " - $($fileItem.($field)) matches $pattern" #! Store these matches in $fieldMatches so that we don't lose them when we do #! additional matches below $fieldMatches = $Matches foreach ($matchMap in $properties.GetEnumerator()) { Write-Debug " - $field map $($matchMap.Key) => $($matchMap.Value)" #! if the value has `{<num>}` then use that match group as the value if ($matchMap.Value -match '\{(\d+)\}') { $matchNumber = [int]$Matches.1 Write-Debug " - Match number: $($matchMap.Key) => $($fieldMatches[$matchNumber])" if (-not ([string]::IsNullorEmpty($fieldMatches[$matchNumber]))) { $sourceObject[$matchMap.Key] = $fieldMatches[$matchNumber] } #! if the value has `{<word>}` then use that match group as the value } elseif ($matchMap.Value -match '\{(\w+)\}') { $matchWord = $Matches.1 if (-not ([string]::IsNullorEmpty($fieldMatches[$matchWord]))) { Write-Debug " - Match word: $($matchMap.Key) => $($fieldMatches[$matchWord])" $sourceObject[$matchMap.Key] = $fieldMatches[$matchWord] } } else { Write-Debug " - $($matchMap.Key) => $($matchMap.Value)" $sourceObject[$matchMap.Key] = $matchMap.Value } } } } } } #! special case: Manifest file if ($fileItem.Extension -like '.psd1') { #! this is why this one is special. A GUID field means that it is probably a manifest #TODO: Add the ability to "lookup" a field from a definition in the map config if ($sourceObject.Data.ContainsKey('GUID')) { $sourceObject['FileType'] = 'PowerShell Module Manifest' $sourceObject['Type'] = 'manifest' $sourceObject['Visibility'] = 'public' } } $sourceInfo = [PSCustomObject]$sourceObject $sourceInfo = $sourceInfo | Add-Member -MemberType ScriptMethod -Name ToString -Value { Get-Content $this.Path } -Force -PassThru $sourceInfo | Write-Output } # end foreach } # end process block end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\SourceInfo\Get-SourceItemInfo.ps1' 347 #Region '.\private\SourceInfo\Get-TestItemInfo.ps1' -1 function Get-TestItemInfo { [CmdletBinding()] param( # The directory to look in for source files [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, # The root directory to use for test properties [Parameter( )] [string]$Root ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { foreach ($p in $Path) { Write-Debug "Processing $p" #------------------------------------------------------------------------------- #region File selection $fileItem = Get-Item $p -ErrorAction Stop if ($fileItem.Extension -notlike '.ps1') { Write-Verbose "Not adding $($fileItem.Name) because it is not a .ps1 file" continue } else { Write-Debug "$($fileItem.Name) is a test item" } #endregion File selection #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Object creation $pesterConfig = New-PesterConfiguration $pesterConfig.Run.Path = $p.FullName $pesterConfig.Run.SkipRun = $true $pesterConfig.Run.PassThru = $true $pesterConfig.Output.Verbosity = 'None' # Quiet try { $testResult = Invoke-Pester -Configuration $pesterConfig Write-Debug "Root is $Root" } catch { throw "Could not load test item $Path`n$_ " } $testInfo = @{ PSTypeName = 'Stitch.TestItemInfo' Tests = $testResult.Tests Path = $p.FullName } [PSCustomObject]$testInfo | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\SourceInfo\Get-TestItemInfo.ps1' 67 #Region '.\private\Template\Get-StitchTemplateMetadata.ps1' -1 function Get-StitchTemplateMetadata { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $content = ($template | Get-Content -Raw) $null = $content -match '(?sm)---(.*?)---' if ($Matches.Count -gt 0) { Write-Debug " - YAML header info found $($Matches.1)" $Matches.1 | ConvertFrom-Yaml | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Template\Get-StitchTemplateMetadata.ps1' 28 #Region '.\private\Template\Invoke-StitchTemplate.ps1' -1 function Invoke-StitchTemplate { [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Medium' )] param( # Specifies a path to the template source [Parameter( Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Source, # The directory to place the new file in [Parameter()] [string]$Destination, # The name of target file [Parameter( ValueFromPipelineByPropertyName )] [string]$Name, # The target path to write the template output to [Parameter( Mandatory, Position = 0, ValueFromPipelineByPropertyName )] [string]$Target, # Binding data to be given to the template [Parameter( ValueFromPipelineByPropertyName )] [hashtable]$Data, # Overwrite the Target with the output [Parameter( )] [switch]$Force, # Return the path to the generated file [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ([string]::IsNullorEmpty($Source))) { if (-not (Test-Path $Source)) { throw "Template file $Source not found" } try { if ([string]::IsNullorEmpty($Data)) { $Data = @{} } $Data['Name'] = $Name # Templates can use this to import/include other templates $Data['TemplatePath'] = $Source | Split-Path -Parent $templateOptions = @{ Path = $Source } $templateOptions['Binding'] = $Data $templateOptions['Safe'] = $true Write-Debug "Converting template $Name with options" Write-Debug "Output of template to $Target" foreach ($option in $templateOptions.Keys) { Write-Debug " - $option => $($templateOptions[$option])" } if (-not ([string]::IsNullorEmpty($templateOptions.Binding))) { Write-Debug " - Bindings:" foreach ($key in $templateOptions.Binding.Keys) { Write-Debug " - $key => $($templateOptions.Binding[$key])" } } $verboseFile = [System.IO.Path]::GetTempFileName() <# EPS builds the templates using StringBuilder, and then "executes" them in a separate powershell instance. Because of that, some errors and exceptions dont show up, you just get no output. To get the actual error, you need to see what the error of the scriptblock is. It looks like there is an update on the [github repo](https://github.com/straightdave/eps) but it is not the released version ! So to confirm that the template functions correctly, check for content first #> $content = Invoke-EpsTemplate @templateOptions -Verbose 4>$verboseFile #! Check this here and use it after we are out of the try block $contentExists = (-not([string]::IsNullorEmpty($content))) } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($contentExists) { $overwrite = $false if (Test-Path $Target) { if (-not ($Force)) { $writeErrorSplat = @{ Message = "$Target already exists. Use -Force to overwrite" Category = 'ResourceExists' CategoryTargetName = $Target } Write-Error @writeErrorSplat } else { $overwrite = $true } } if ($overwrite) { Write-Debug "It does exist" $operation = 'Overwrite file' } else { Write-Debug 'It does not exist yet' $operation = 'Write file' } if ($PSCmdlet.ShouldProcess($Target, $operation)) { try { $targetDir = $Target | Split-Path -Parent if (-not (Test-Path $targetDir)) { mkdir $targetDir -Force } $content | Set-Content $Target if ($PassThru) { $Target | Write-Output } } catch { throw "Could not write template content to $Target`n$_" } } } else { #------------------------------------------------------------------------------- #region Get template error Write-Debug "No content. Getting inner error" $verboseOutput = [System.Collections.ArrayList]@(Get-Content $verboseFile) #! Replace the first and last lines with braces to make it a scriptblock so we can execute the inner content $null = $verboseOutput.RemoveAt(0) $null = $verboseOutput.RemoveAt($verboseOutput.Count - 1) $null = $verboseOutput.Insert( 0 , 'try {') $verboseOutput += @( '} catch {', 'throw $_', '}' ) $stringBuilderScript = [scriptblock]::Create(($verboseOutput | Out-String)) try { Invoke-Command -ScriptBlock $stringBuilderScript } catch { throw $_ } #endregion Get template error #------------------------------------------------------------------------------- } } else { throw "No Source given to process" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\private\Template\Invoke-StitchTemplate.ps1' 179 #Region '.\public\Changelog\Add-ChangelogEntry.ps1' -1 function Add-ChangelogEntry { <# .SYNOPSIS Add an entry to the changelog #> [CmdletBinding()] param( # The commit to add [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [PSTypeName('Git.ConventionalCommitInfo')][Object]$Commit, # Specifies a path to the changelog file [Parameter( Position = 0 )] [Alias('PSPath')] [string]$Path, # The release to add the entry to [Parameter( Position = 2 )] [string]$Release = 'unreleased' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" enum DocumentState { NONE = 0 RELEASE = 1 GROUP = 2 } } process { $group = $Commit | Resolve-ChangelogGroup Write-Debug "Commit $($Commit.MessageShort) resolves to group $($group.DisplayName)" if (Test-Path $Path) { Write-Debug "Now parsing $Path" [Markdig.Syntax.MarkdownDocument]$doc = $Path | Import-Markdown -TrackTrivia $state = [DocumentState]::NONE $tokenCount = 0 foreach ($token in $doc) { Write-Debug "--- $state : Line $($token.Line) $($token.GetType()) Index $($doc.IndexOf($token))" switch ($token.GetType()) { 'Markdig.Syntax.HeadingBlock' { switch ($token.Level) { 2 { Write-Debug " - Is a level 2 heading" $text = $token | Format-HeadingText -NoLink if ($text -match [regex]::Escape($Release)) { Write-Debug " - *** Heading '$text' matches $Release ***" $state = [DocumentState]::RELEASE } else { Write-Debug " - $text did not match" } continue } 3 { Write-Debug " - Is a level 3 heading" if ($state -eq [DocumentState]::RELEASE) { $text = $token | Format-HeadingText -NoLink if ($text -like $group.DisplayName) { Write-Debug " - *** Heading '$text' matches group ***" $state = [DocumentState]::GROUP } } else { Write-Debug " - Not in release" } continue } Default {} } continue } 'Markdig.Syntax.ListBlock' { if ($state -eq [DocumentState]::GROUP) { Write-Debug "Listblock while GROUP is set" $text = $Commit | Format-ChangelogEntry Write-Debug "Wanting to add '$text' to the list" Write-Debug "$($token.Count) items in the list" # $conversion = $text | ConvertFrom-Markdown | Select-Object -ExpandProperty Tokens | # Select-Object -First 1 $text = "$([System.Environment]::NewLine)$text" $entry = [Markdig.Markdown]::Parse($text, $true) Write-Debug "The entry we want to add is a $($entry.GetType()) at $tokenCount" try { $doc.Insert($doc.IndexOf($token), $entry) } catch { $PSCmdlet.ThrowTerminatingError($_) } finally { $state = [DocumentState]::NONE } } continue } 'Markdig.Syntax.LinkReferenceDefinitionGroup' { $doc.RemoveAt($doc.IndexOf($token)) } } $tokenCount++ } } $doc | Write-MarkdownDocument | Out-File $Path } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Changelog\Add-ChangelogEntry.ps1' 118 #Region '.\public\Changelog\ConvertFrom-Changelog.ps1' -1 function ConvertFrom-Changelog { <# .SYNOPSIS Convert a Changelog file into a PSObject #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, # Optionally return a hashtable instead of an object [Parameter( )] [switch]$AsHashTable ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $changelogObject = @{ Releases = [System.Collections.ArrayList]@() } } process { foreach ($file in $Path) { if (Test-Path $file) { try { Write-Debug "Importing markdown document $file" $doc = Get-Item $file | Import-Markdown } catch { throw "Error parsing markdown`n$_" } } else { throw "$file is not a valid path" } Write-Debug "Parsing tokens in $file" foreach ($token in $doc) { switch ($token) { { $_ -is [Markdig.Syntax.HeadingBlock] } { $text = $token | Format-HeadingText switch ($token.Level) { <# if this is a level 2 heading then it is a new release every token after this one should be added to the release and the group should be added to the changelog after it has been completely filled out #> 2 { Write-Debug "at Line $($token.Line) Found new release heading '$text'" if ($null -ne $thisRelease) { Write-Debug ' - Adding previous group to changelog' if ($AsHashTable) { $null = $changelogObject.Releases.Add($thisRelease) } else { $null = $changelogObject.Releases.Add([PSCustomObject]$thisRelease) } Remove-Variable release, group -ErrorAction SilentlyContinue } $thisRelease = @{ Groups = [System.Collections.ArrayList]@() } if (-not($AsHashTable)) { $thisRelease['PSTypeName'] = 'Changelog.Release' } # unreleased if ($text -match '^\[?unreleased\]? - (.*)?') { Write-Debug '- matches unreleased' $thisRelease['Version'] = 'unreleased' $thisRelease['Name'] = 'unreleased' $thisRelease['Type'] = 'Unreleased' if ($null -ne $Matches.1) { $thisRelease['Timestamp'] = (Get-Date $Matches.1) } else { $thisRelease['Timestamp'] = (Get-Date -Format 'yyyy-MM-dd') } # version, link and date # [1.0.1](https://github.com/user/repo/compare/vprev..vcur) 1986-02-25 } elseif ($text -match '^\[(?<ver>[0-9\.]+)\]\((?<link>[^\)]+)\)\s*-?\s*(?<dt>\d\d\d\d-\d\d-\d\d)?') { Write-Debug '- matches version,link and date' if ($null -ne $Matches.ver) { $thisRelease['Type'] = 'Release' $thisRelease['Version'] = $Matches.ver $thisRelease['Name'] = $Matches.ver if ($null -ne $Matches.dt) { $thisRelease['Link'] = $Matches.link } if ($null -ne $Matches.dt) { $thisRelease['Timestamp'] = $Matches.dt } } # version and date # [1.0.1] 1986-02-25 } elseif ($text -match '^\[(?<ver>[0-9\.]+)\]\s*-?\s*(?<dt>\d\d\d\d-\d\d-\d\d)?') { Write-Debug '- matches version and date' if ($null -ne $Matches.ver) { $thisRelease['Type'] = 'Release' $thisRelease['Version'] = $Matches.ver $thisRelease['Name'] = $Matches.ver if ($null -ne $Matches.dt) { $thisRelease['Timestamp'] = $Matches.dt } } } } 3 { if ($null -ne $group) { if ($AsHashTable) { $null = $thisRelease.Groups.Add($group) } else { $null = $thisRelease.Groups.Add([PSCustomObject]$group) } $group.Clear() } $group = @{ Entries = [System.Collections.ArrayList]@() } $group['DisplayName'] = $text $group['Name'] = $text if (-not($AsHashTable)) { $group['PSTypeName'] = 'Changelog.Group' } } } } { $_ -is [Markdig.Syntax.ListItemBlock] } { Write-Debug " - list item block at line $($token.Line) column $($token.Column)" # token is a collection of ListItems foreach ($listItem in $token) { Write-Debug " - list item at line $($listItem.Line) column $($listItem.Column)" $text = $listItem.Inline.Content.ToString() $null = $group.Entries.Add( @{ Title = $text Description = $text } ) } continue } } } } Write-Debug ' - adding last release to changelog' if ($AsHashTable) { $null = $changelogObject.Releases.Add($thisRelease) } else { $null = $changelogObject.Releases.Add([PSCustomObject]$thisRelease) } } end { if ($AsHashTable) { $changelogObject | Write-Output } else { $changelogObject['PSTypeName'] = 'ChangelogInfo' [PSCustomObject]$changelogObject | Write-Output } Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Changelog\ConvertFrom-Changelog.ps1' 169 #Region '.\public\Changelog\ConvertTo-Changelog.ps1' -1 function ConvertTo-Changelog { <# .SYNOPSIS Convert Git-History to a Changelog #> [CmdletBinding()] param( # A git history table to be converted [Parameter( ValueFromPipeline )] [hashtable]$History ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $config = Get-ChangelogConfig } process { Format-ChangelogHeader [System.Environment]::NewLine foreach ($releaseName in (($History.GetEnumerator() | Sort-Object { $_.Value.Timestamp } -Descending | Select-Object -ExpandProperty Name))) { $release = $History[$releaseName] $release | Format-ChangelogRelease [System.Environment]::NewLine foreach ($groupName in ($release.Groups.GetEnumerator() | Sort-Object { $_.Value.Sort } | Select-Object -ExpandProperty Name)) { if ($groupName -like 'omit') { continue } $group = $release.Groups[$groupName] $group | Format-ChangelogGroup [System.Environment]::NewLine foreach ($entry in $group.Entries) { $entry | Format-ChangelogEntry } [System.Environment]::NewLine } } [System.Environment]::NewLine Format-ChangelogFooter } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Changelog\ConvertTo-Changelog.ps1' 50 #Region '.\public\Changelog\Export-ReleaseNotes.ps1' -1 #using namespace System.Diagnostics.CodeAnalysis function Export-ReleaseNotes { [SuppressMessage('PSUseSingularNouns', '', Justification = 'ReleaseNotes is a single document' )] [CmdletBinding()] param( # Specifies a path to the Changelog.md file [Parameter( Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path = 'CHANGELOG.md', # The path to the destination file. Outputs to pipeline if not specified [Parameter( Position = 0 )] [string]$Destination, # The release version to create a release from [Parameter( )] [string]$Release ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $changelogData = $null function outputItem { param( [Parameter( Position = 1, ValueFromPipeline )] [string]$Item, [Parameter( Position = 0 )] [bool]$toFile ) if ($toFile) { $Destination | Add-Content $Item } else { $Item | Write-Output } } } process { $writeToFile = $PSBoundParameters.ContainsKey('Destination') if (-not ([string]::IsNullorEmpty($Path))) { if (Test-Path $Path) { Write-Debug "Converting Changelog : $Path" $dpref = $DebugPreference $DebugPreference = 'SilentlyContinue' $changelogData = ($Path | ConvertFrom-Changelog) $DebugPreference = $dpref if ($null -ne $changelogData) { Write-Debug "There are $($changelogData.Releases.Count) release sections" :section foreach ($section in $changelogData.Releases ) { Write-Debug "$($section.Type) Section: Version = $($section.Version) Timestamp = $($section.Timestamp)" if ($section.Type -like 'Unreleased') { continue section } if (-not ([string]::IsNullorEmpty($Release))) { if ( [semver]::new($section.Version) -gt [semver]::new($Release)) { continue section } } #! we can use our Format to assemble the Timestamp, version, etc #! the other items should already be in the format we want $section | Format-ChangelogRelease | outputItem $writeToFile foreach ($group in $section.Groups) { #! no need to reformat it $group | Format-ChangelogGroup | outputItem $writeToFile foreach ($entry in $group.Entries) { $entry | Format-ChangelogEntry | outputItem $writeToFile } } } } } } else { throw "$Path is not a valid Path" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Changelog\Export-ReleaseNotes.ps1' 95 #Region '.\public\Changelog\Get-ChangelogConfig.ps1' -1 function Get-ChangelogConfig { <# .SYNOPSIS Look for a psd1 configuration file in the local folder, the path specified, or the module folder #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path ) begin { $defaultConfigFile = '.changelog.config.psd1' } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { #! if not specified, look in the local directory for the config file $Path = Get-Location } Write-Debug "Path is set as $Path" if (Test-Path $Path) { $pathItem = Get-Item $Path if ($pathItem.PSIsContainer) { Write-Debug "Path is a directory. Looking for $defaultConfigFile" $possiblePath = (Join-Path $pathItem $defaultConfigFile) # look for the file in the directory if (Test-Path $possiblePath) { Write-Debug " - Found" $configFile = Get-Item $possiblePath } } else { $configFile = $pathItem } } else { $configFile = Get-Item (Join-Path $ExecutionContext.SessionState.Module.ModuleBase $defaultConfigFile) } Write-Verbose "Loading configuration from $($configFile.FullName)" $config = Import-PowerShellDataFile $configFile.FullName } end { $config } } #EndRegion '.\public\Changelog\Get-ChangelogConfig.ps1' 53 #Region '.\public\Changelog\Set-ChangelogRelease.ps1' -1 function Set-ChangelogRelease { <# .SYNOPSIS Create a new release section in the Changelog based on the changes in 'Unreleased' and creates a new blank 'Unreleased' section #> [Alias('Update-Changelog')] [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'medium' )] param( # Specifies a path to the changelog file [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The unreleased section will be moved to this version [Parameter( )] [string]$Release, # The date of the release [Parameter( )] [datetime]$releaseDate, # Skip checking the current git tag information [Parameter( )] [switch]$SkipGitTag ) begin { Write-Verbose "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $config = Get-ChangelogConfig if (-not($SkipGitTag)) { if ($PSBoundParameters.ContainsKey('Release')) { $tag = Get-GitTag -Name $Release -ErrorAction SilentlyContinue } else { $tag = Get-GitTag | Where-Object { $_.Name -match $config.TagPattern } | Select-Object -First 1 -ErrorAction SilentlyContinue } if ($null -ne $tag) { $releaseDate = $tag.Target.Author.When.UtcDateTime if ($null -ne $Release) { $null = $tag.FriendlyName -match $config.TagPattern if ($Matches.Count -gt 0) { $Release = $Matches.1 } } } else { $PSCmdlet.WriteError("Could not find tag $Release") } } if ([string]::IsNullorEmpty($Release)) { throw "No Release version could be found" } if ([string]::IsNullorEmpty($ReleaseDate)) { $PSCmdlet.WriteError("No release date was found for release $Release") } } process { Write-Verbose "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (Test-Path $Path) { Write-Verbose "Setting up temp document" $tempFile = [System.IO.Path]::GetTempFileName() Get-Content $Path -Raw | Set-Content $tempFile Write-Verbose "Now parsing document" [Markdig.Syntax.MarkdownDocument]$doc = $tempFile | Import-Markdown -TrackTrivia $currentVersionHeading = $doc | Get-MarkdownHeading | Where-Object { ($_ | Format-HeadingText -NoLink) -match $config.CurrentVersion } Write-Verbose "Found $($currentVersionHeading.Count) current version headings" if ($null -ne $currentVersionHeading) { $afterCurrentHeading = ($doc.IndexOf($currentVersionHeading) + 1) $releaseData = @{ Name = $Release TimeStamp = $releaseDate } $newHeading = [Markdig.Markdown]::Parse( ($releaseData | Format-ChangelogRelease), $true ) if ($null -ne $newHeading) { $newText =$newHeading | Format-HeadingText -NoLink Write-Verbose "New Heading is $newText" [ref]$doc | Add-MarkdownElement $newHeading -Index $afterCurrentHeading [ref]$doc | Add-MarkdownElement ([Markdig.Syntax.BlankLineBlock]::new()) -Index $afterCurrentHeading } } $linkRefs = $doc | Get-MarkdownElement LinkReferenceDefinitionGroup | Select-Object -First 1 if ($null -ne $linkRefs) { $doc.RemoveAt($doc.IndexOf($linkRefs)) } try { $doc | Write-MarkdownDocument | Out-File $tempFile #! -Force required to overwrite our file $tempFile | Move-Item -Destination $Path -Force } catch { $PSCmdlet.ThrowTerminatingError($_) } } Write-Verbose "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Verbose "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Changelog\Set-ChangelogRelease.ps1' 120 #Region '.\public\Configuration\Convert-ConfigurationFile.ps1' -1 function Convert-ConfigurationFile { <# .SYNOPSIS Convert a configuration file into a powershell hashtable. Can be psd1, yaml, or json #> [CmdletBinding()] param( # Specifies a path to one or more configuration files. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "Getting ready to convert configuration file $Path" if (Test-Path $Path) { Write-Debug ' - File exists' $pathItem = Get-Item $Path if ($pathItem.PSISContainer) { Get-ChildItem -Path $Path -Recurse | Convert-ConfigurationFile } else { switch -Regex ($pathItem.Extension) { '\.psd1' { #! Note we use the 'Unsafe' parameter so we can have scriptblocks and #! variables in our psd Write-Debug ' - Importing PSD' $configOptions = (Import-Psd -Path $pathItem -Unsafe) | Write-Output } '\.y(a)?ml' { Write-Debug ' - Importing YAML' $configOptions = (Get-Content $pathItem | ConvertFrom-Yaml -Ordered) | Write-Output } '\.json(c)?' { Write-Debug ' - Importing JSON' $configOptions = (Get-Content $pathItem | ConvertFrom-Json -Depth 16) | Write-Output } default { Write-Warning "Could not determine the type for $($pathItem.FullName)" } } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Convert-ConfigurationFile.ps1' 55 #Region '.\public\Configuration\Get-BuildConfiguration.ps1' -1 #using namespace System.Collections.Specialized function Get-BuildConfiguration { <# .SYNOPSIS Gather information about the project for use in tasks .DESCRIPTION `Get-BuildConfiguration` collects information about paths, source items, versions and modules that it finds in -Path. Configuration information can be added/updated using configuration files. .EXAMPLE Get-BuildConfiguration . -ConfigurationFiles ./.build/config gci .build\config | Get-BuildConfiguration . #> [OutputType([System.Collections.Specialized.OrderedDictionary])] [CmdletBinding()] param( # Specifies a path to the folder to build the configuration for [Parameter( Position = 0, ValueFromPipelineByPropertyName )] [string]$Path = (Get-Location), # Path to the build configuration file [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$ConfigurationFiles, # Default Source directory [Parameter( )] [string]$Source, # Default Tests directory [Parameter( )] [string]$Tests, # Default Staging directory [Parameter( )] [string]$Staging, # Default Artifact directory [Parameter( )] [string]$Artifact, # Default Docs directory [Parameter( )] [string]$Docs ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" # The info table holds all of the gathered project information, which will ultimately be returned to the # caller $info = [ordered]@{ Project = @{} } #------------------------------------------------------------------------------- #region Set defaults <# !used throughout to set "project locations" which is why we don't just add it directly to $info #> $defaultLocations = @{ Source = "source" Tests = 'tests' Staging = 'stage' Artifact = 'out' Docs = 'docs' } # Add them as top level keys $defaultLocations.Keys | ForEach-Object { $info[$_] = '' } #endregion Set defaults #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Normalize paths Write-Debug ( @( "Paths used to build configuration:", "Path : $Path", "Source : $Source", "Staging : $Staging", "Artifact : $Artifact", "Tests : $Tests", "Docs : $Docs") -join "`n") $possibleRoot = $PSCmdlet.GetVariableValue('BuildRoot') if ($null -eq $possibleRoot) { Write-Debug "`$BuildRoot not found, using current location" $possibleRoot = (Get-Location) } foreach ($location in $defaultLocations.Keys) { Write-Debug "Setting the $location path" <# The paths to the individual locations are vital to the correct operation of the build. Each variable is checked to see if it exists as a parameter, and then in the caller scope (set via the script that called this function). Finally, we test to see if the "default" is true, and add it #> if ($PSBoundParameters.ContainsKey($location)) { $possibleLocation = $PSBoundParameters[$location] } elseif ($PSCmdlet.GetVariableValue($location)) { $possibleLocation = $PSCmdlet.GetVariableValue($location) } else { $possibleLocation = $defaultLocations[$location] } if ($null -ne $possibleLocation) { if (-not([System.IO.Path]::IsPathFullyQualified($possibleLocation))) { $possibleLocation = (Join-Path $possibleRoot $possibleLocation) } if (-not(Test-Path $possibleLocation)) { Write-Warning "$possibleLocation set as `$$location, but path does not exist" } #? Not sure what the right action is here. I could fail the function #? because I can't find the path... for now, I will just leave the #? unresolved string there because it must have been for a reason? Write-Debug " - $possibleLocation" $info[$location] = $possibleLocation } } #endregion Normalize paths #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Feature flags $flags = Get-FeatureFlag if ($null -ne $flags) { $info['Flags'] = $flags } else { Write-Debug "No feature flags were found" } #endregion Feature flags #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Version info $versionInfo = Get-ProjectVersionInfo if ($null -ne $versionInfo) { Write-Debug "Setting 'Version' key with version info" $info.Project['Version'] = $versionInfo } else { Write-Debug 'No version information found in project' } #endregion Version info #------------------------------------------------------------------------------- } process { #------------------------------------------------------------------------------- #region Configuration files foreach ($f in $ConfigurationFiles) { Write-Debug "Merging $f into BuildInfo" if (Test-Path $f) { $f | Merge-BuildConfiguration -Object ([ref]$info) } } #endregion Configuration files #------------------------------------------------------------------------------- } end { try { Write-Debug 'Resolving project root' $resolveRootOptions = @{ Path = $Path } $root = (Get-Item (Resolve-ProjectRoot @resolveRootOptions -ErrorAction SilentlyContinue)) } catch { Write-Warning "Could not find Project Root`n$_" } if ($null -ne $root) { Write-Debug ' - root found:' Write-Debug " - Path is : $($root.FullName)" Write-Debug " - Name is : $($root.BaseName)" $info['Project'] = @{ Path = $root.FullName Name = $root.BaseName } } else { Write-Debug " - Project root was not found. 'Path' and 'Name' will be empty" $info['Project'] = @{ Path = '' Name = '' } } $info['Modules'] = @{} Write-Debug " Loading modules from $($info.Source)" foreach ($item in (Get-ModuleItem $info.Source)) { Write-Debug " Adding $($item.Name) to the collection" #! Get the names of the paths to process from failsafe_defaults, but the #! values come from the info table Write-Debug " - Adding field 'Paths' to module $($item.Name)" $item | Add-Member -NotePropertyName Paths -NotePropertyValue ($defaultLocations.Keys) foreach ($location in $defaultLocations.Keys) { $moduleLocation = (Join-Path $info[$location] $item.Name) Write-Debug " - Adding $location Path : $moduleLocation" $item | Add-Member -NotePropertyName $location -NotePropertyValue $moduleLocation } $info.Modules[$item.Name] = $item } <#------------------------------------------------------------------ Now, configure the directories for each module. If a module is a Nested Module of another, then the staging folder should be: $Staging/RootModuleName/NestedModuleName ------------------------------------------------------------------#> Write-Debug "$('-' * 80)`n --- Getting NestedModules" foreach ($key in $info.Modules.Keys) { $currentModule = $info.Modules[$key] if ($null -ne $currentModule.NestedModules) { foreach ($nest in $currentModule.NestedModules) { if ($nest -is [string]) { $nestedModule = $nest } elseif ($nest -is [hashtable]) { $nestedModule = $nest.ModuleName } Write-Debug " Nested module: $nestedModule" $found = '' switch -Regex ($nestedModule) { # path\to\ModuleName.psm1 # path/to/ModuleName.psm1 '[\\/]?(?<fname>)\.psm1$' { Write-Debug " - Found path to module file $($Matches.fname)" $found = $Matches.fname continue } # path\to\ModuleName # path/to/ModuleName '(\w+[\\/])*(?<lword>\w+)$' { Write-Debug " - Found path to directory $($Matches.lword)" $found = $Matches.lword continue } Default { Write-Debug ' - Does not match a pattern' $found = $nestedModule } } if ($info.Modules.Keys -contains $found) { Write-Debug " Adding $($currentModule.Name) as parent of $found" $info.Modules[$found] | Add-Member -NotePropertyName 'Parent' -NotePropertyValue $currentModule.Name } else { Write-Debug " $found not found in project's modules`n$($info.Modules.Keys -join "`n - ") " } } } } Write-Debug "Completed building configuration settings" $info Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Get-BuildConfiguration.ps1' 279 #Region '.\public\Configuration\Get-BuildRunBook.ps1' -1 function Get-BuildRunBook { <# .SYNOPSIS Return the runbooks in the given directory #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Optionally recurse into children [Parameter( )] [switch]$Recurse, # Optional runbook filter [Parameter( )] [string]$Filter = '*runbook.ps1' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "Looking for runbooks in $($Path.FullName)'" $options = @{ Path = $Path Recurse = $Recurse Filter = $Filter } Get-ChildItem @options } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Get-BuildRunBook.ps1' 45 #Region '.\public\Configuration\Get-TaskConfiguration.ps1' -1 function Get-TaskConfiguration { <# .SYNOPSIS Get the configuration file for the given task if it exists. First looks in the local user's stitch directory, and then the local build configuration directory .DESCRIPTION Look for the given task's configuration in `<buildconfig>/config/tasks` #> [CmdletBinding()] param( # The task object [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string]$Name, [Parameter( Position = 0 )] [string]$TaskConfigPath ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $taskConfigOptions = @{ Filter = '*.config.psd1' Recurse = $true } $taskConfigPathOptions = @{ ChildPath = 'config' AdditionalChildPath = 'tasks' } #! Because we are looking in both the user's stitch directory and the current #! project's stitch directory, create an empty array to hold all the files $taskConfigFiles = [System.Collections.ArrayList]::new() } process { #------------------------------------------------------------------------------- #region User stitch directory $userStitchDirectory = Find-LocalUserStitchDirectory if ($null -ne $userStitchDirectory) { $possibleUserTaskConfigDirectory = (Join-Path -Path $userStitchDirectory @taskConfigPathOptions) if (Test-Path $possibleUserTaskConfigDirectory) { Write-Debug "User task configuration directory found at $possibleUserTaskConfigDirectory" $userTaskConfigDirectory = $possibleUserTaskConfigDirectory Get-ChildItem -Path $userTaskConfigDirectory @taskConfigOptions | Merge-FileCollection ([ref]$taskConfigFiles) } Remove-Variable possibleUserTaskConfigDirectory -ErrorAction SilentlyContinue } else { Write-Verbose "No stitch directory found for in user's home" } #endregion User stitch directory #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Find path if (-not($PSBoundParameters.ContainsKey('TaskConfigPath'))) { Write-Debug 'No TaskConfigPath given. Looking for BuildConfigPath' $possibleBuildConfigPath = Find-BuildConfigurationDirectory if (-not ([string]::IsNullorEmpty($possibleBuildConfigPath))) { Write-Debug "found BuildConfigPath at $possibleBuildConfigPath" $BuildConfigPath = $possibleBuildConfigPath $TaskConfigPath = (Join-Path -Path $BuildConfigPath @taskConfigPathOptions) Remove-Variable possibleBuildConfigPath -ErrorAction SilentlyContinue } } #endregion Find path #------------------------------------------------------------------------------- if (Test-Path $TaskConfigPath) { Write-Debug "Looking for task config files in $TaskConfigPath" Get-ChildItem -Path $TaskConfigPath @taskConfigOptions | Merge-FileCollection ([ref]$taskConfigFiles) Write-Debug " - Found $($taskConfigFiles.Count) config files" } if ($taskConfigFiles.Count -gt 0) { foreach ($taskConfigFile in $taskConfigFiles) { if ((-not ($PSBoundParameters.ContainsKey('Name'))) -or ($TaskConfigFile.BaseName -like "$Name.config")) { try { #TODO: Use the Convert-ConfigurationFile to support any kind of config file, not just psd $config = Import-Psd -Path $taskConfigFile -Unsafe if ($null -eq $config) { $config = @{} } $config['TaskName'] = ($TaskConfigFile.BaseName -replace '\.config$', '') $config['ConfigPath'] = $TaskConfigFile.FullName } catch { $PSCmdlet.ThrowTerminatingError($_) } #TODO: I'm not sure we should return the config object here, unless we change the name to Import $config | Write-Output } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Get-TaskConfiguration.ps1' 106 #Region '.\public\Configuration\Merge-BuildConfiguration.ps1' -1 function Merge-BuildConfiguration { [CmdletBinding()] param( # The object to merge the configuration into (by reference) [Parameter( Mandatory, Position = 0 )] [ref]$Object, # The top level key in which to add the given table [Parameter( Position = 1 )] [string]$Key, # Specifies a path to one or more configuration files [Parameter( Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { foreach ($file in $Path) { $options = Convert-ConfigurationFile $Path if ($null -ne $options) { if ($PSBoundParameters.ContainsKey('Key')) { $Object.Value.$Key | Update-Object -UpdateObject $options } else { $Object.Value | Update-Object -UpdateObject $options } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Merge-BuildConfiguration.ps1' 46 #Region '.\public\Configuration\Select-BuildRunBook.ps1' -1 function Select-BuildRunBook { <# .SYNOPSIS Locate the runbook for the given BuildProfile .DESCRIPTION Select-BuildRunBook locates the runbook associated with the BuildProfile. If no BuildProfile is given, Select-BuildRunBook will use default names to search for .EXAMPLE $ProfilePath | Select-BuildRunBook 'default' $ProfilePath | Select-BuildRunBook 'site' #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The build profile to select the runbook for [Parameter( Position = 0 )] [string]$BuildProfile ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $defaultProfileNames = @( 'default', 'build' ) $defaultRunbookSuffix = "runbook.ps1" } process { if (-not ($PSBoundParameters.ContainsKey('Path'))) { if (-not ([string]::IsNullorEmpty($PSCmdlet.GetVariableValue('ProfileRoot')))) { $Path = $PSCmdlet.GetVariableValue('ProfileRoot') } else { $Path = (Get-Location).Path } } if (-not ($PSBoundParameters.ContainsKey('BuildProfile'))) { $searches = $defaultProfileNames } else { $searches = $BuildProfile } foreach ($p in $Path) { if (Test-Path $p) { foreach ($searchFor in $searches) { Write-Debug "Looking in $p for $searchFor runbook" <# First, look for the buildprofile.runbook.ps1 in the given directory #> $options = @{ Path = $p Filter = "$searchFor.$defaultRunbookSuffix" } $possibleRunbook = Get-ChildItem @options | Select-Object -First 1 if ($null -eq $possibleRunbook) { Write-Debug " - No runbook found in $p matching $($options.Filter)" $null = $options.Clear() $options = @{ Path = (Join-Path $p $searchFor) Filter = "*$defaultRunbookSuffix" } Write-Debug "Looking in $($options.Path) using $($options.Filter)" if (Test-Path $options.Path) { $possibleRunbook = Get-ChildItem @options | Select-Object -First 1 } } if ($null -ne $possibleRunbook) { $possibleRunbook | Write-Output } } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Configuration\Select-BuildRunBook.ps1' 91 #Region '.\public\Content\Checkpoint-Directory.ps1' -1 function Checkpoint-Directory { <# .SYNOPSIS Output the relative path and the MD5 hash value of each file in the given Path .DESCRIPTION #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms', '', Justification = 'We are only using MD5 to verify the file has not changed')] [OutputType('File.Checksum')] [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-Path $Path) { foreach ($file in (Get-ChildItem -Path $Path -File -Recurse)) { $relative = [System.IO.Path]::GetRelativePath((Resolve-Path $Path), $file.FullName) $checksum = $file | Checkpoint-File [PSCustomObject]@{ PSTypeName = 'File.Checksum' Path = $relative Hash = $checksum } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Checkpoint-Directory.ps1' 45 #Region '.\public\Content\Checkpoint-File.ps1' -1 function Checkpoint-File { <# .SYNOPSIS Create a hash of the given file #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms', '', Justification = 'We are only using MD5 to verify the file has not changed')] [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $hashingAlgorithm = 'MD5' } process { foreach ($file in $Path) { $checksum = $file | Get-FileHash -Algorithm $hashingAlgorithm | Select-Object -ExpandProperty Hash [PSCustomObject]@{ PSTypeName = 'File.Checksum' TimeStamp = Get-Date Hash = $checksum } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Checkpoint-File.ps1' 43 #Region '.\public\Content\Checkpoint-String.ps1' -1 function Checkpoint-String { <# .SYNOPSIS Hash the given string using the MD5 algorithm #> [OutputType('System.String')] [CmdletBinding()] param( # The string to hash [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [string]$InputObject ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider $utf8 = new-object -TypeName System.Text.UTF8Encoding } process { $hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($InputObject))) $hash -replace '-', '' } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Checkpoint-String.ps1' 30 #Region '.\public\Content\Convert-LineEnding.ps1' -1 function Convert-LineEnding { <# .SYNOPSIS Convert the line endings in the given file to "Windows" (CRLF) or "Unix" (LF) .DESCRIPTION `Convert-LineEnding` will convert all of the line endings in the given file to the type specified. If 'Windows' or 'CRLF' is given, all line endings will be '\r\n' and if 'Unix' or 'LF' is given all line endings will be '\n' 'Unix' (LF) is the default .EXAMPLE Get-ChildItem . -Filter "*.txt" | Convert-LineEnding -LF Convert all txt files in the current directory to '\n' .NOTES WARNING! this can corrupt a binary file. #> [CmdletBinding( DefaultParameterSetName = 'Unix' )] param( # The file to be converted [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Convert line endings to 'Unix' (LF) [Parameter( ParameterSetName = 'Unix', Position = 1 )] [switch]$LF, # Convert line endings to 'Windows' (CRLF) [Parameter( ParameterSetName = 'Windows', Position = 1 )] [switch]$CRLF ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { foreach ($file in $Path) { if ($CRLF) { Write-Verbose " Converting line endings in $($file.Name) to 'CRLF'" ((Get-Content $file) -join "`r`n") | Set-Content -NoNewline -Path $file } elseif ($LF) { Write-Verbose " Converting line endings in $($file.Name) to 'LF'" ((Get-Content $file) -join "`n") | Set-Content -NoNewline -Path $file } else { Write-Error "No EOL format specified. Please use '-LF' or '-CRLF'" } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Convert-LineEnding.ps1' 67 #Region '.\public\Content\Find-ParseToken.ps1' -1 function Find-ParseToken { <# .SYNOPSIS Return an array of tokens that match the given pattern #> [OutputType([System.Array])] [CmdletBinding()] param( # The token to find, as a regex [Parameter( Position = 0 )] [string]$Pattern, # The type of token to look in [Parameter( Position = 1 )] [System.Management.Automation.PSTokenType]$Type, # Specifies a path to one or more locations. [Parameter( Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $options = $PSBoundParameters $null = $options.Remove('Pattern') try { $tokens = Get-ParseToken @options } catch { throw "Could not parse $Path`n$_" } if ($null -ne $tokens) { Write-Debug " - Looking for $Pattern in $($tokens.Count) tokens" foreach ($token in $tokens) { Write-Debug " - Checking $($token.Content)" if ($token.Content -Match $Pattern) { $token | Write-Output } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Find-ParseToken.ps1' 59 #Region '.\public\Content\Format-File.ps1' -1 function Format-File { <# .SYNOPSIS Run PSSA formatter on the given files #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Path to the code format settings [Parameter( Position = 0 )] [object]$Settings = 'CodeFormatting.psd1' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { if ($null -ne $psEditor) { $currentFile = $psEditor.GetEditorContext().CurrentFile.Path if (Test-Path $currentFile) { Write-Debug "Formatting current VSCode file '$currentFile'" $Path += $currentFile } } } foreach ($file in $Path) { if (Test-Path $file) { $content = Get-Content $file -Raw $options = @{ ScriptDefinition = $content Settings = $Settings } try { Invoke-Formatter @options | Set-Content $file } catch { $PSCmdlet.ThrowTerminatingError($_) } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Format-File.ps1' 57 #Region '.\public\Content\Get-ParseToken.ps1' -1 function Get-ParseToken { <# .SYNOPSIS Return an array of Tokens from parsing a file #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The type of token to return [Parameter( )] [System.Management.Automation.PSTokenType]$Type ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-Path $Path) { $errors = @() $content = Get-Item $Path | Get-Content -Raw $parsedText = [System.Management.Automation.PSParser]::Tokenize($content, [ref]$errors) if ($errors.Count) { throw "There were errors parsing $($Path.FullName). $($errors -join "`n")" } foreach ($token in $parsedText) { if ((-not($PSBoundParameters.ContainsKey('Type'))) -or ($token.Type -like $Type)) { $token | Write-Output } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Get-ParseToken.ps1' 49 #Region '.\public\Content\Invoke-ReplaceToken.ps1' -1 function Invoke-ReplaceToken { <# .SYNOPSIS Replace a given string 'Token' with another string in a given file. #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'medium' )] param( # File(s) to replace tokens in [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath', 'Path')] [string]$In, # The token to replace, written as a regular-expression [Parameter( Position = 0, Mandatory )] [string]$Token, # The value to replace the token with [Parameter( Position = 1, Mandatory )] [Alias('Value')] [string]$With, # The destination file to write the new content to # If destination is a directory, `Invoke-ReplaceToken` will put the content in a file named the same as # the input, but in the given directory [Parameter( Position = 2 )] [Alias('Out')] [string]$Destination ) begin { } process { try { $content = Get-Content $In -Raw if ($content | Select-String -Pattern $Token) { Write-Debug "Token $Token found, replacing with $With" $newContent = ($content -replace [regex]::Escape($Token), $With) if ($PSBoundParameters.ContainsKey('Destination')) { $destObject = Get-Item $Destination if ($destObject -is [System.IO.FileInfo]) { $destFile = $Destination } elseif ($destObject -is [System.IO.DirectoryInfo]) { $destFile = (Join-Path $Destination ((Get-Item $file).Name)) } else { throw "$Destination should be a file or directory" } } else { $newContent | Write-Output } if ($PSCmdlet.ShouldProcess($destFile, "Replace $Token with $With")) { Write-Verbose "Writing output to $destFile" $newContent | Set-Content $destFile -Encoding utf8NoBOM } } else { #! This is a little rude, but I have to find a way to let the user know that nothing changed, #! and I don't want to send anything out to the console in case it is being directed somewhere #TODO: Consider using Write-Warning $save_verbose = $VerbosePreference $VerbosePreference = 'Continue' Write-Verbose "$Token not found in $In" $VerbosePreference = $save_verbose } } catch { $PSCmdlet.ThrowTerminatingError($_) } } end { } } #EndRegion '.\public\Content\Invoke-ReplaceToken.ps1' 88 #Region '.\public\Content\Measure-File.ps1' -1 function Measure-File { <# .SYNOPSIS Run PSSA analyzer on the given files #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Path to the code format settings [Parameter( )] [object]$Settings = 'PSScriptAnalyzerSettings.psd1', # Optionally apply fixes [Parameter( )] [switch]$Fix ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { if ($null -ne $psEditor) { $currentFile = $psEditor.GetEditorContext().CurrentFile.Path if (Test-Path $currentFile) { Write-Debug "Formatting current VSCode file" $Path += $currentFile } } } foreach ($file in $Path) { if (Test-Path $file) { $options = @{ Path = $file Settings = $Settings Fix = $Fix } try { Invoke-ScriptAnalyzer @options } catch { $PSCmdlet.ThrowTerminatingError($_) } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Measure-File.ps1' 60 #Region '.\public\Content\Merge-SourceItem.ps1' -1 #using namespace System.Management.Automation.Language function Merge-SourceItem { [CmdletBinding()] param( # The SourceItems to be merged [Parameter( ValueFromPipeline )] [PSTypeName('Stitch.SourceItemInfo')][object[]]$SourceItem, # File to merge the SourceItem into [Parameter( Position = 0 )] [string]$Path, # Optionally wrap the given source items in `#section/endsection` tags [Parameter( )] [string]$AsSection ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $pre = '#region {0} {1}' $post = '#endregion {0} {1}' $root = Resolve-ProjectRoot $sb = New-Object System.Text.StringBuilder if ($PSBoundParameters.ContainsKey('AsSection')) { $null = $sb.AppendJoin('', @('#', ('=' * 79))).AppendLine() $null = $sb.AppendFormat( '#region {0}', $AsSection).AppendLine() } #------------------------------------------------------------------------------- #region Setup $sourceInfoUsingStatements = [System.Collections.ArrayList]@() $sourceInfoRequires = [System.Collections.ArrayList]@() #endregion Setup #------------------------------------------------------------------------------- } process { Write-Debug "Processing SourceItem $($PSItem.Name)" #------------------------------------------------------------------------------- #region Parse SourceItem #------------------------------------------------------------------------------- #region Content Write-Debug "Parsing SourceItem $($PSItem.Name)" #! The first NamedBlock in the AST *should* be the enum, class or function $predicate = { param($a) $a -is [NamedBlockAst] } $ast = $PSItem.Ast if ($null -eq $ast) { throw "Could not parse $($PSItem.Name)" } $nb = $ast.Find($predicate, $false) $start = $nb.Extent.StartLineNumber $end = $nb.Extent.EndLineNumber Write-Debug " - First NamedBlock found starting on line $start ending on line $end" $relativePath = $PSItem.Path -replace [regex]::Escape($root) , '' #! remove the leading '\' if it's there if ($relativePath.SubString(0,1) -like '\') { $relativePath = $relativePath.Substring(1,($relativePath.Length - 1)) } Write-Debug " - Setting relative path to $relativePath" #endregion Content #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Using statements if ($ast.UsingStatements.Count -gt 0) { Write-Debug ' - Storing using statements' $sourceInfoUsingStatements += $ast.UsingStatements } #endregion Using statements #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Requires statements if ($ast.ScriptRequirements.Count -gt 0) { Write-Debug ' - Storing Requires statements' $sourceInfoRequires += $ast.ScriptRequirements } #endregion Requires statements #------------------------------------------------------------------------------- #endregion Parse SourceItem #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region SourceItem content Write-Debug " - Merging $($PSItem.Name) contents" $null = $sb.AppendFormat( $pre, $relativePath, $start ).AppendLine() $null = $sb.Append( $nb.Extent.Text).AppendLine() $null = $sb.AppendFormat( $post, $relativePath, $end).AppendLine() #endregion SourceItem content #------------------------------------------------------------------------------- } end { #------------------------------------------------------------------------------- #region Update module content #------------------------------------------------------------------------------- #region Add sourceItem if ($PSBoundParameters.ContainsKey('AsSection')) { $null = $sb.AppendFormat( '#endregion {0}', $AsSection).AppendLine() $null = $sb.AppendJoin('', @('#', ('=' * 79))).AppendLine() } Write-Debug "Writing new content to $Path" $sb.ToString() | Add-Content $Path $null = $sb.Clear() #endregion Add sourceItem #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Parse module Write-Debug "$Path exists. Parsing contents" $moduleText = Get-Content $Path $module = [Parser]::ParseInput($moduleText, [ref]$null, [ref]$null) $content = $moduleText #endregion Parse module #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Requires statements Write-Debug ' - Parsing Requires statements' $combinedRequires = $module.ScriptRequirements + $sourceInfoRequires if (-not([string]::IsNullorEmpty($combinedRequires.ScriptRequirements.RequiredApplicationId))) { $s = "#Requires -ShellId $($combinedRequires.ScriptRequirements.RequiredApplicationId)" $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } if (-not([string]::IsNullorEmpty($combinedRequires.ScriptRequirements.RequiredPSVersion))) { $s = "#Requires -Version $($combinedRequires.ScriptRequirements.RequiredPSVersion)" $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } foreach ($rm in $combinedRequires.ScriptRequirements.RequiredModules) { $s = "#Requires -Modules $($rm.ToString())" $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } foreach ($ra in $combinedRequires.ScriptRequirements.RequiredAssemblies) { $s = "#Requires -Assembly $ra" $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } foreach ($re in $combinedRequires.ScriptRequirements.RequiredPSEditions) { $s = "#Requires -PSEdition $re" $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } foreach ($rp in $combinedRequires.ScriptRequirements.RequiresPSSnapIns) { $s = "#Requires -PSnapIn $($rp.Name)" if (-not([string]::IsNullorEmpty($rp.Version))) { $s += " -Version $(rp.Version)" } $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } if ($combinedRequires.ScriptRequirements.IsElevationRequired) { $s = '#Requires -RunAsAdministrator' $content = ($content) -replace [regex]::Escape($s), '' $null = $sb.AppendLine($s) Remove-Variable s } #endregion Requires statements #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Using statements $combinedUsingStatements = $module.UsingStatements + $sourceInfoUsingStatements if ($combinedUsingStatements.Count -gt 0) { Write-Debug " - Parsing using statements in $Path" Write-Debug "There are $($combinedUsingStatements.Count) using statements" Write-Debug "$($combinedUsingStatements | Select-Object Name, UsingStatementKind | Out-String)" foreach ($kind in [UsingStatementKind].GetEnumValues()) { Write-Debug " - Checking for using $kind statements" $statements = $combinedUsingStatements | Where-Object UsingStatementKind -Like $kind if ($statements.Count -gt 0) { Write-Debug " - $($statements.Count) statements found" $added = @() foreach ($statement in $statements) { $s = $statement.Extent.Text Write-Debug " - Statement Text: '$s'" if ($added -contains $s) { Write-Debug " - already processed" } else { Write-Debug " - Looking for '$s' in content" # first, remove the line from the original content if ($content -match "^$([regex]::Escape($s))`$") { Write-Debug " - found '$s' in content" $content = ($content) -replace "^$([regex]::Escape($s))`$", '' } $null = $sb.AppendLine($s) $added += $s } } } } } else { Write-Debug 'No using statements in module or sourceInfo' } #endregion Using statements #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Content Write-Debug "Writing content back to $Path" $null = $sb.AppendJoin("`n", $content) $sb.ToString() | Set-Content $Path #endregion Content #------------------------------------------------------------------------------- #endregion Update module content #------------------------------------------------------------------------------- } } #EndRegion '.\public\Content\Merge-SourceItem.ps1' 238 #Region '.\public\Content\Save-Checkpoint.ps1' -1 function Save-Checkpoint { <# .SYNOPSIS Store the relative path and MD5 sum of file(s) in path to the file in CSV format #> [CmdletBinding()] param( # Specifies a path to one or more locations to compute the checksum (MD5 hashes) for [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The file that will contain the checksums (CSV format) [Parameter( Position = 0 )] [string]$ChecksumFile = ".checksum.csv", # Force the overwrite of an existing Checksum file [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-Path $ChecksumFile) { if ($Force) { Clear-Content $ChecksumFile } else { throw "$ChecksumFile already exists. Use -Force to overwrite" } } $path | Checkpoint-Directory | EXport-Csv $ChecksumFile } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Save-Checkpoint.ps1' 47 #Region '.\public\Content\Test-Checkpoint.ps1' -1 function Test-Checkpoint { <# .SYNOPSIS Compares the checkpoint of a file to its current hash .DESCRIPTION Compare the MD5 hash to the HASH given. Returns true if they are equal, false if not .EXAMPLE $file | Test-Checkpoint "C72CD5EBFDC6D41E2A9F539AA94F2E8A" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms', '', Justification = 'We are only using MD5 to verify the file has not changed')] [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The md5 hash to compare to [Parameter( Position = 0, Mandatory )] [string]$Hash ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $item = Get-Item $Path if ($item.PSIsContainer) { throw "Can only compare files not directories" } $currentHash = $Path | Get-FileHash -Algorithm MD5 | Select-Object -ExpandProperty Hash #! return true or false based on if the hash has changed ($Hash -eq $currentHash) | Write-Output } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Test-Checkpoint.ps1' 52 #Region '.\public\Content\Test-ContentChanged.ps1' -1 function Test-ContentChanged { <# .SYNOPSIS Compare the given path to a list of MD5 hashes to determine if files have changed .DESCRIPTION For each file in the given path, compare the current MD5 hash with the one stored in Checksum. If they are the same, return $false, otherwise return $true (one or more files have changed) #> [OutputType('bool')] [CmdletBinding( DefaultParameterSetName = 'File' )] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, [Parameter( ParameterSetName = 'Array', Position = 0 )] [PSTypeName('File.Checksum')][Object[]]$Checksums, # The file that contains the checksums (CSV format) [Parameter( ParameterSetName = 'File', Position = 0 )] [string]$ChecksumFile ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $changedFiles = [System.Collections.ArrayList]::new() } process { if (-not ($PSBoundParameters.ContainsKey('ChecksumFile'))) { if (-not ($Checksums.Count -gt 0)) { throw "Checksums required. Either provide an array of 'File.Checksum' items, or a path to the checksum file" } } else { if (Test-Path $ChecksumFile) { $Checksums = (Import-Csv $ChecksumFile) } else { throw "$ChecksumFile is not a valid path" } } $ChecksumList = [System.Collections.ArrayList]::new($Checksums) $currentFiles = Get-ChildItem $Path -File -Recurse foreach ($file in $currentFiles) { $relative = [System.Io.Path]::GetRelativePath((Resolve-Path $Path), $file) $listItem = $ChecksumList | Where-Object Path -Like $relative if ($null -ne $listItem) { if ($file | Test-Checkpoint $listItem.Hash) { [void]$ChecksumList.Remove($listItem) } else { Write-Verbose "$relative has changed" [void]$changedFiles.Add($file.FullName) } } else { Write-Verbose "$relative was added" [void]$changedFiles.Add($file.FullName) } } } end { #! if there are any files left in the list, then it was deleted. Output $true because content changed if ($ChecksumList.Count -gt 0) { $true | Write-Output } else { #! If any files were changed, output $true, otherwise $false ($changedFiles.Count -gt 0) | Write-Output } Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Test-ContentChanged.ps1' 87 #Region '.\public\Content\Test-WindowsLineEnding.ps1' -1 function Test-WindowsLineEnding { <# .SYNOPSIS Test for "Windows Line Endings" (CRLF) in the given file .DESCRIPTION `Test-WindowsLineEnding` returns true if the file contains CRLF endings, and false if not #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [ValidateNotNullOrEmpty()] [string]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-Path $Path) { (Get-Content $Path -Raw) -match '\r\n$' } else { Write-Error "$Path could not be found" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Content\Test-WindowsLineEnding.ps1' 35 #Region '.\public\FeatureFlags\Get-FeatureFlag.ps1' -1 function Get-FeatureFlag { <# .SYNOPSIS Retrieve feature flags for the stitch module #> [CmdletBinding()] param( # The name of the feature flag to test [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [string]$Name ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $featureFlagFile = (Join-Path (Get-ModulePath) 'feature.flags.config.psd1') } process { if ($null -ne $BuildInfo.Flags) { Write-Debug "Found the buildinfo table and it has Flags set" $featureFlags = $BuildInfo.Flags } elseif ($null -ne $featureFlagFile) { if (Test-Path $featureFlagFile) { $featureFlags = Import-Psd $featureFlagFile -Unsafe } } if ($null -ne $featureFlags) { switch ($featureFlags) { ($_ -is [System.Collections.Hashtable]) { foreach ($key in $featureFlags.Keys) { $flag = $featureFlags[$key] $flag['PSTypeName'] = 'Stitch.FeatureFlag' $flag['Name'] = $key if ((-not ($PSBoundParameters.ContainsKey('Name'))) -or ($flag.Name -like $Name)) { [PSCustomObject]$flag | Write-Output } continue } } default { foreach ($flag in $featureFlags.PSobject.properties) { Write-Debug "Name is $($flag.Name)" if ((-not ($PSBoundParameters.ContainsKey('Name'))) -or ($flag.Name -like $Name)) { $flag | Write-Output } } } } } else { Write-Information "No feature flag data was found" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\FeatureFlags\Get-FeatureFlag.ps1' 63 #Region '.\public\FeatureFlags\Test-FeatureFlag.ps1' -1 function Test-FeatureFlag { <# .SYNOPSIS Test if a feature flag is enabled #> [OutputType([bool])] [CmdletBinding()] param( # The name of the feature flag to test [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string]$Name ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $flag = Get-FeatureFlag -Name $Name if ([string]::IsNullorEmpty($flag)) { $false } else { $flag.Enabled } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\FeatureFlags\Test-FeatureFlag.ps1' 32 #Region '.\public\Git\Add-GitFile.ps1' -1 function Add-GitFile { <# .EXAMPLE Get-ChildItem *.md | function Add-GitFile .EXAMPLE Get-GitStatus | function Add-GitFile #> [CmdletBinding( DefaultParameterSetName = 'asPath' )] param( # Accept a statusentry [Parameter( ParameterSetName = 'asEntry', ValueFromPipeline )] [LibGit2Sharp.RepositoryStatus[]]$Entry, # Paths to files to add [Parameter( Position = 0, ParameterSetName = 'asPath', ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Add All items [Parameter( )] [switch]$All, # The repository root [Parameter( )] [string]$RepoRoot, # Return objects to the pipeline [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('Entry')) { $PSBoundParameters['Path'] = @() Write-Debug ' processing entry' foreach ($e in $Entry) { Write-Debug " - adding $($e.FilePath)" $PSBoundParameters['Path'] += $e.FilePath } } foreach ($file in $Path) { Add-GitItem (Resolve-Path $file -Relative) } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Add-GitFile.ps1' 66 #Region '.\public\Git\Checkpoint-GitWorkingDirectory.ps1' -1 function Checkpoint-GitWorkingDirectory { <# .SYNOPSIS Save all changes (including untracked) and push to upstream #> [CmdletBinding()] param( # Message to use for the checkpoint commit. # Defaults to: # `[checkpoint] Creating checkpoint before continuing <date>` [Parameter( )] [string]$Message ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ($PSBoundParameters.ContainsKey('Message'))) { $Message = "[checkpoint] Creating checkpoint before continuing $(Get-Date -Format FileDateTimeUniversal)" } Write-Verbose 'Staging all changes' Add-GitItem -All Write-Verbose 'Commiting changes' Save-GitCommit -Message $Message Write-Verbose 'Pushing changes upstream' if (-not(Get-GitBranch -Current | Select-Object -ExpandProperty IsTracking)) { Get-GitBranch -Current | Send-GitBranch -SetUpstream } else { Get-GitBranch -Current | Send-GitBranch } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Checkpoint-GitWorkingDirectory.ps1' 39 #Region '.\public\Git\Clear-MergedGitBranch.ps1' -1 function Clear-MergedGitBranch { <# .SYNOPSIS Prune remote branches and local branches with no tracking branch #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'High' )] param( # Only clear remote branches [Parameter( ParameterSetName = 'Remote' )] [switch]$RemoteOnly, # Only clear remote branches [Parameter( ParameterSetName = 'Local' )] [switch]$LocalOnly ) Write-Verbose "Pruning remote first" if (-not ($LocalOnly)) { if ($PSCmdlet.ShouldProcess("Remote origin", "Prune")) { #TODO: Find a "PowerGit way" to do this part git remote prune origin } } if (-not ($RemoteOnly)) { $branches = Get-GitBranch | Where-Object { $_.IsTracking -and $_.TrackedBranch.IsGone } if ($null -ne $branches) { Write-Verbose "Removing $($branches.Count) local branches" foreach ($branch in $branches) { if ($PSCmdlet.ShouldProcess($branch.FriendlyName, "Remove branch")) { Remove-GitBranch } } } } } #EndRegion '.\public\Git\Clear-MergedGitBranch.ps1' 42 #Region '.\public\Git\ConvertFrom-ConventionalCommit.ps1' -1 function ConvertFrom-ConventionalCommit { <# .SYNOPSIS Convert a git commit message (such as from PowerGit\Get-GitCommit) into an object on the pipeline .DESCRIPTION A git commit message is technically unstructured text. However, a long standing convention is to structure the message should be a single line title, followed by a blank line and then any amount of text in the body. Conventional Commits provide additional structure by adding "metadata" to the title: - | |<------ title ----------------------| <- 50 char or less | <type>[optional scope]: <description> message | [optional body] <- 72 char or less | | [optional footer(s)] <- 72 char or less - Recommended types are: - build - chore - ci - docs - feat - fix - perf - refactor - revert - style - test #> [CmdletBinding()] param( # The commit message to parse [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [string]$Message, [Parameter( ValueFromPipelineByPropertyName )] [object]$Sha, [Parameter( ValueFromPipelineByPropertyName )] [object]$Author, [Parameter( ValueFromPipelineByPropertyName )] [object]$Committer ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" enum Section { NONE = 0 HEAD = 1 BODY = 2 FOOT = 3 } } process { # This will restart for each message on the pipeline # Messages (at least the ones from PowerGit objects) are multiline strings $section = [Section]::NONE $title = $type = $scope = '' $body = [System.Collections.ArrayList]@() $footers = @{} $breakingChange = $false $conforming = $false $lineNum = 1 foreach ($line in ($Message -split '\n')) { try { Write-Debug "Parsing line #$lineNum : '$line'" switch -Regex ($line) { '^#+' { Write-Debug ' - Comment line' continue } #! This may match the head, but also may match a specific kind of footer #! too. So we check the line number and go from there @' (?x) # Matches either a conventional title <type>(<scope>)!: <description> # or a footer of like <type>: <description> ^(?<t>\w+) # Header must start with a type word (\((?<s>\w+)\))? # Optionally a scope in '()' (?<b>!)? # Optionally a ! to denote a breaking change :\s+ # Mandatory colon and a space (?<d>.+)$ # Everything else is the description '@ { Write-Debug ' - Head line' # Parse as a heading only if we are on line one! if ($lineNum -eq 1) { $title = $line $type = $Matches.t $scope = $Matches.s ?? '' $desc = $Matches.d $section = [Section]::HEAD $breakingChange = ($Matches.b -eq '!') $conforming = $true } else { Write-Debug ' - Footer' # There could be multiple entries of the same type of footer # such as: # closes #9 # closes #7 if ($footers.ContainsKey($Matches.t)) { $footers[$Matches.t] += $Matches.d } else { $footers[$Matches.t] = @($Matches.d) } $section = [Section]::FOOT } continue } @' (?x) # Matches a git-trailer style footer <type>: <description> or <type> #<description> ^\s* (?<t>[a-zA-Z0-9-]+) (:\s|\s\#) (?<v>.*)$ '@ { Write-Debug ' - Footer' # There could be multiple entries of the same type of footer # such as: # closes #9 # closes #7 if ($footers.ContainsKey($Matches.t)) { $footers[$Matches.t] += $Matches.d } else { $footers[$Matches.t] = @($Matches.d) } $section = [Section]::FOOT continue } @' (?x) # Matches either BREAKING CHANGE: <description> or BREAKING-CHANGE: <description> ^\s* (?<t>BREAKING[- ]CHANGE) :\s (?<v>.*)$ '@ { Write-Debug ' - Breaking change footer' $footers[$Matches.t] = $Matches.v $breakingChange = $true } '^\s*$' { # might be the end of a section, or it might be in the middle of the body if ($section -eq [Section]::HEAD) { # this is our "one blank line convention" # so the next line should be the start of the body $section = [Section]::BODY } continue } Default { #! if the first line is not in the proper format, it will #! end up here: We can add it as the title, but none of #! the conventional commit specs will be filled if ($lineNum -eq 1) { Write-Verbose " '$line' does not seem to be a conventional commit" $title = $line $desc = $line $conforming = $false } else { # if it matched nothing else, it should be in the body Write-Debug ' - Default match, adding to the body text' $body += $line } continue } } } catch { throw "At $lineNum : '$line'`n$_" } $lineNum++ } [PSCustomObject]@{ PSTypeName = 'Git.ConventionalCommitInfo' Message = $Message IsConventional = $conforming IsBreakingChange = $breakingChange Title = $title Type = $type Scope = $scope Description = $desc Body = $body Footers = $footers Sha = $Sha ShortSha = $Sha.Substring(0, 7) Author = $Author Committer = $Committer } | Write-Output } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\ConvertFrom-ConventionalCommit.ps1' 206 #Region '.\public\Git\Get-GitFile.ps1' -1 function Get-GitFile { <# .SYNOPSIS Return a list of the files listed in git status #> [OutputType([System.IO.FileInfo])] [CmdletBinding()] param( # The type of files to return [Parameter( )] [ValidateSet( 'Added', 'Ignored', 'Missing', 'Modified', 'Removed', 'Staged', 'Unaltered', 'Untracked', 'RenamedInIndex', 'RenamedInWorkDir', 'ChangedInIndex', 'ChangedInWorkDir')] [AllowNull()] [string]$Type ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('Type')) { $status = Get-GitStatus | Select-Object -ExpandProperty $Type } else { $status = Get-GitStatus } $status | Select-Object -ExpandProperty FilePath | ForEach-Object { Get-Item (Resolve-Path $_) | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitFile.ps1' 39 #Region '.\public\Git\Get-GitHistory.ps1' -1 function Get-GitHistory { [CmdletBinding()] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $config = Get-ChangelogConfig $currentVersion = $config.CurrentVersion ?? 'unreleased' $releases = @{ $currentVersion = @{ Name = $currentVersion Timestamp = (Get-Date) Groups = @{} } } } process { foreach ($commit in Get-GitCommit) { #------------------------------------------------------------------------------- #region Convert commit message Write-Debug "Converting $($commit.MessageShort)" try { $commitObject = $commit | ConvertFrom-ConventionalCommit } catch { $exception = [Exception]::new("Could not convert commit $($commit.MessageShort)`n$($_.PSMessageDetails)") $errorRecord = [System.Management.Automation.ErrorRecord]::new( $exception, $_.FullyQualifiedErrorId, $_.CategoryInfo, $commit ) $PSCmdlet.ThrowTerminatingError($errorRecord) } #endregion Convert commit message #------------------------------------------------------------------------------- if ($null -ne $commit.Refs) { foreach ($ref in $commit.Refs) { $name = $ref.CanonicalName -replace '^refs\/', '' if ($name -match '^tags\/(?<tag>.*)$') { Write-Debug ' - is a tag' $commitObject | Add-Member -NotePropertyName Tag -NotePropertyValue $Matches.tag if ($commitObject.Tag -match $config.TagPattern) { # Add a version to the releases $currentVersion = $Matches.1 $releases[$currentVersion] = @{ Name = $currentVersion Timestamp = (Get-Date '1970-01-01') # set it as the epoch, but update below Groups = @{} } if ($null -ne $commitObject.Author.When.UtcDateTime) { $releases[$currentVersion].Timestamp = $commitObject.Author.When.UtcDateTime } } } } } #------------------------------------------------------------------------------- #region Add to group $group = $commitObject | Resolve-ChangelogGroup if ($null -eq $group) { Write-Debug "no group information found for $($commitObject.MessageShort)" $group = @{ Name = 'Other' DisplayName = 'Other' Sort = 99999 } } if (-not($releases[$currentVersion].Groups.ContainsKey($group.Name))) { $releases[$currentVersion].Groups[$group.Name] = @{ DisplayName = $group.DisplayName Sort = $group.Sort Entries = @() } } $releases[$currentVersion].Groups[$group.Name].Entries += $commitObject #endregion Add to group #------------------------------------------------------------------------------- } } end { $releases | Write-Output Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitHistory.ps1' 99 #Region '.\public\Git\Get-GitHubDefaultBranch.ps1' -1 function Get-GitHubDefaultBranch { <# .SYNOPSIS Returns the default branch of the given github repository #> [CmdletBinding()] param( # The repository to find the default brach in [Parameter( )] [string]$RepositoryName ) if ($PSBoundParameters.Key -notcontains 'RepositoryName') { $RepositoryName = Get-GitRepository | Select-Object -ExpandProperty RepositoryName } Get-GitHubRepository -RepositoryName $RepositoryName | Select-Object -ExpandProperty DefaultBranch } #EndRegion '.\public\Git\Get-GitHubDefaultBranch.ps1' 20 #Region '.\public\Git\Get-GitMergedBranch.ps1' -1 function Get-GitMergedBranch { <# .SYNOPSIS Return a list of branches that have been merged into the given branch (or default branch if none specified) #> [CmdletBinding()] param( # The branch to use for the "base" (the branch the returned branches are merged into) [Parameter( ValueFromPipelineByPropertyName )] [string]$FriendlyName = (Get-GitHubDefaultBranch) ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $defaultTip = Get-GitBranch -Name $FriendlyName | Foreach-Object {$_.Tip.Sha } Get-GitBranch | Where-Object { ($_.FriendlyName -ne $FriendlyName) -and ($_.Commits | Select-Object -ExpandProperty Sha) -contains $defaultTip } | Write-Output } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitMergedBranch.ps1' 31 #Region '.\public\Git\Get-GitModifiedFile.ps1' -1 function Get-GitModifiedFile { <# .SYNOPSIS Return a list of the files modified in the current repository #> [CmdletBinding()] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Get-GitFile -Type Modified } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitModifiedFile.ps1' 18 #Region '.\public\Git\Get-GitRemoteTrackingBranch.ps1' -1 function Get-GitRemoteTrackingBranch { Get-GitBranch | Select-Object -ExpandProperty TrackedBranch } #EndRegion '.\public\Git\Get-GitRemoteTrackingBranch.ps1' 4 #Region '.\public\Git\Get-GitStagedFile.ps1' -1 function Get-GitStagedFile { <# .SYNOPSIS Return a list of the files modified in the current repository #> [CmdletBinding()] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Get-GitFile -Type Staged } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitStagedFile.ps1' 19 #Region '.\public\Git\Get-GitUntrackedFile.ps1' -1 function Get-GitUntrackedFile { <# .SYNOPSIS Return a list of the files untracked in the current repository #> [CmdletBinding()] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Get-GitFile -Type Untracked } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Get-GitUntrackedFile.ps1' 19 #Region '.\public\Git\Join-PullRequest.ps1' -1 function Join-PullRequest { <# .SYNOPSIS Merge the current branch's pull request, then pull them into '$DefaultBranch' (usually 'main' or 'master') .DESCRIPTION Ensuring the current branch is up-to-date on the remote, and that it has a pull-request, this function will then: 1. Merge the current pull request 1. Switch to the `$DefaultBranch` branch 1. Pull the latest changes #> param( # The name of the repository. Uses the current repository if not specified [Parameter( ValueFromPipelineByPropertyName )] [string]$RepositoryName, # By default the remote and local branches are deleted if successfully merged. Add -DontDelete to # keep the branches [Parameter()] [switch]$DontDelete, # The default branch. usually 'main' or 'master' [Parameter( )] [string]$DefaultBranch ) if (-not($PSBoundParameters.ContainsKey('RepositoryName'))) { $PSBoundParameters['RepositoryName'] = (Get-GitRepository | Select-ExpandProperty RepositoryName) } $status = Get-GitStatus if ($status.IsDirty) { throw "Changes exist in working directory.`nCommit or stash them first" } else { if (-not ($PSBoundParameters.ContainsKey('DefaultBranch'))) { $DefaultBranch = Get-GitHubDefaultBranch } if ([string]::IsNullorEmpty($DefaultBranch)) { throw "Could not determine default branch. Use -DefaultBranch parameter to specify" } $branch = Get-GitBranch -Current if ($null -ne $branch) { #------------------------------------------------------------------------------- #region Merge PullRequest Write-Debug "Getting Pull Request for branch $($branch.FriendlyName)" $pr = $branch | Get-GitHubPullRequest if ($null -ne $pr) { Write-Verbose "Merging Pull Request # $($pr.number)" try { if ($DontDelete) { $pr | Merge-GitHubPullRequest Write-Verbose ' - (remote branch not deleted)' } else { $pr | Merge-GitHubPullRequest -DeleteBranch Write-Verbose ' - (remote branch deleted)' } } catch { throw "Could not merge Pull Request`n$_" } #endregion Merge PullRequest #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Pull changes try { Write-Verbose "Switching to branch '$DefaultBranch'" Set-GitHead $DefaultBranch } catch { throw "Could not switch to branch $DefaultBranch`n$_" } try { Write-Verbose 'Pulling changes from remote' Receive-GitBranch Write-Verbose "Successfully merged pr #$($pr.number) and updated project" } catch { throw "Could not update $DefaultBranch`n$_" } #endregion Pull changes #------------------------------------------------------------------------------- try { Remove-GitBranch $branch } catch { throw "Could not delete local branch $($branch.FriendlyName)" } } else { throw "Couldn't find a Pull Request for $($branch.FriendlyName)" } } else { throw "Couldn't get the current branch" } } } #EndRegion '.\public\Git\Join-PullRequest.ps1' 105 #Region '.\public\Git\Start-GitBranch.ps1' -1 function Start-GitBranch { param( [string]$Name ) New-GitBranch $Name | Set-GitHead } #EndRegion '.\public\Git\Start-GitBranch.ps1' 8 #Region '.\public\Git\Sync-GitRepository.ps1' -1 function Sync-GitRepository { <# .SYNOPSIS Update the working directory of the current branch .DESCRIPTION This is equivelant to `git pull --rebase #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Medium' )] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $br = Get-GitBranch -Current if ($br.IsTracking) { $remote = $br.TrackedBranch if ($PSCmdlet.ShouldProcess($br.FriendlyName, "Update")) { $br | Send-GitBranch origin Start-GitRebase -Upstream $remote.FriendlyName -Branch $br.FriendlyName } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Sync-GitRepository.ps1' 32 #Region '.\public\Git\Undo-GitCommit.ps1' -1 function Undo-GitCommit { <# .SYNOPSIS Reset the branch to before the previous commit .DESCRIPTION There are three types of reset: but keep all the changes in the working directory Without This is equivelant to `git reset HEAD~1 --mixed #> [CmdletBinding()] param( # Hard reset [Parameter( ParameterSetName = 'Hard' )] [switch]$Hard, # Soft reset [Parameter( ParameterSetName = 'Soft' )] [switch]$Soft ) #! The default mode is mixed, it does not have a parameter Reset-GitHead -Revision 'HEAD~1' @PSBoundParameters } #EndRegion '.\public\Git\Undo-GitCommit.ps1' 31 #Region '.\public\Git\Update-GitRepository.ps1' -1 function Update-GitRepository { <# .SYNOPSIS Update the working directory of the current branch .DESCRIPTION This is equivelant to `git pull --rebase #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Medium' )] param() begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $br = Get-GitBranch -Current if ($br.IsTracking) { $remote = $br.TrackedBranch if ($PSCmdlet.ShouldProcess($br.FriendlyName, "Update")) { Start-GitRebase -Upstream $remote.FriendlyName -Branch $br.FriendlyName } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Git\Update-GitRepository.ps1' 31 #Region '.\public\InvokeBuild\Get-BuildProperty.ps1' -1 function Get-BuildProperty { <# .SYNOPSIS Return the variable specified using defined variables, environment variables and parameters #> [CmdletBinding()] param( # The name of the property [Parameter( Mandatory, Position = 0 )] [string]$Name, # The default value if one is not found [Parameter( Position = 1 )] $Value ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($null -ne $PSCmdlet.GetVariableValue($Name)) { return $PSCmdlet.GetVariableValue($Name) } elseif ($null -ne [Environment]::GetEnvironmentVariable($Name)) { return [Environment]::GetEnvironmentVariable($Name) } elseif ($null -ne $Value) { return $Value } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\InvokeBuild\Get-BuildProperty.ps1' 38 #Region '.\public\InvokeBuild\Get-BuildTask.ps1' -1 function Get-BuildTask { [CmdletBinding()] param( # The name of the task to get [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ArgumentCompleter({ Invoke-TaskNameCompletion @args })] [string]$Name ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-InInvokeBuild) { Write-Debug 'Running under Invoke-Build' $allData = $PSCmdlet.GetVariableValue('*') if ($null -ne $allData) { Write-Debug 'Found the star variable' Write-Debug "$($allData.All | Show-ObjectProperties | Out-String)" $taskData = $allData.All } else { throw 'Could not retrieve task from Invoke-Build' } } else { $taskData = Invoke-Build ?? } if ($null -ne $taskData) { $descriptions = Invoke-Build ? foreach ($key in $taskData.Keys) { $task = $taskData[$key] if ($null -ne $task) { if ($null -eq $task.Synopsis) { $synopsis = ( $descriptions | Where-Object -Property Name -Like $key | Select-Object -ExpandProperty Synopsis ) ?? 'No Synopsis' $task | Add-Member -NotePropertyName Synopsis -NotePropertyValue $synopsis } if ($null -eq $task.IsPhase) { #! if the task was written as 'phase <name>' then the InvocationName #! can be used to find it. Add a property 'IsPhase' for easier sorting $task | Add-Member -NotePropertyName IsPhase -NotePropertyValue ( ( $task.InvocationInfo.InvocationName -like 'phase' ) ? $true : $false ) } if ($null -eq $task.Path) { $task | Add-Member -NotePropertyName Path -NotePropertyValue ( Get-Item $task.InvocationInfo.ScriptName ) } if ($null -eq $task.Line) { $task | Add-Member -NotePropertyName Line -NotePropertyValue $task.InvocationInfo.ScriptLineNumber $task.PSObject.TypeNames.Insert(0, 'InvokeBuild.TaskInfo') } if ((-not ($PSBoundParameters.ContainsKey('Name'))) -or ($key -like $Name)) { $task | Write-Output } } else { throw "No task with name $key" } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\InvokeBuild\Get-BuildTask.ps1' 77 #Region '.\public\InvokeBuild\Get-MetadataComment.ps1' -1 function Get-MetadataComment { <# .SYNOPSIS Return the metadata stored in a special comment within the task file #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $dataPattern = '(?sm)---(?<data>.*?)---' } process { foreach ($file in $Path) { if ($file | Test-Path) { try { $fileItem = Get-Item $file $helpInfo = Get-Help -Name $fileItem.FullName -Full if ($null -ne $helpInfo) { if ($null -ne $helpInfo.alertSet) { if (-not ([string]::IsNullorEmpty($helpInfo.alertSet.alert.Text))) { $noteInfo = $helpInfo.alertSet.alert.Text if ($noteInfo -match $dataPattern) { if ($Matches.data) { $dataText = $Matches.data $sb = [scriptblock]::Create($dataText) & $sb } } } } } } catch { throw "Could not get path $file`n$_" } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\InvokeBuild\Get-MetadataComment.ps1' 53 #Region '.\public\InvokeBuild\Get-TaskHelp.ps1' -1 function Get-TaskHelp { <# .SYNOPSIS Retrieve the comment based help for the given task .NOTES If the given task's file does not have help info, it won't be very helpful... #> [CmdletBinding()] param( # The name of the task to get the help documentation for [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ArgumentCompleter({ Invoke-TaskNameCompletion @args})] [string[]]$Name, # The InvocationInfo of a task [Parameter( ValueFromPipelineByPropertyName )] [System.Management.Automation.InvocationInfo]$InvocationInfo ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('InvocationInfo')) { Get-Help $InvocationInfo.ScriptName -Full } elseif ($PSBoundParameters.ContainsKey('Name')) { foreach ($taskName in $Name) { $task = Get-BuildTask -Name $taskName if ($null -ne $task) { Get-Help $task.InvocationInfo.ScriptName -Full } } } else { throw "No task given" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\InvokeBuild\Get-TaskHelp.ps1' 47 #Region '.\public\InvokeBuild\Test-InInvokeBuild.ps1' -1 function Test-InInvokeBuild { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $invokeBuildPattern = 'Invoke-Build.ps1' } process { $callStack = Get-PSCallStack $inInvokeBuild = $false for ($i = 1; $i -lt $callStack.Length; $i++) { $caller = $callStack[$i] Write-Debug "This caller is $($caller.Command)" if ($caller.Command -match $invokeBuildPattern) { $inInvokeBuild = $true break } } $inInvokeBuild } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\InvokeBuild\Test-InInvokeBuild.ps1' 26 #Region '.\public\Manifest\ConvertFrom-CommentedProperty.ps1' -1 function ConvertFrom-CommentedProperty { <# .SYNOPSIS Uncomment the given Manifest Item .DESCRIPTION In a typical manifest, unused properties are listed, but commented out with a '#' like `# ReleaseNotes = ''` Update-Metadata, Import-Psd and similar functions need to have these fields available. `ConvertFrom-CommentedProperty` will remove the '#' from the line so that those functions can use the given property .EXAMPLE $manifest | ConvertFrom-CommentedProperty 'ReleaseNotes' #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The item to uncomment [Parameter( Position = 0 )] [Alias('PropertyName')] [string]$Property ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('Path')) { if (Test-Path $Path) { $commentToken = $Path | Find-ParseToken -Type Comment -Pattern "^\s*#\s*$Property\s+=.*$" | Select-Object -First 1 if ($null -ne $commentToken) { $replacementIndent = (' ' * ($commentToken.StartColumn - 1)) $newContent = $commentToken.Content -replace '#\s*', $replacementIndent $fileContent = @(Get-Content $Path) $fileContent[$commentToken.StartLine - 1] = $newContent $fileContent | Set-Content $Path } else { # if we did not find the comment, signal that it was not successful Write-Warning "$Property comment not found" } } else { throw "$Path is not a valid path" } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Manifest\ConvertFrom-CommentedProperty.ps1' 59 #Region '.\public\Manifest\Get-ModuleExtension.ps1' -1 function Get-ModuleExtension { <# .SYNOPSIS Find modules with the `Extension` key in the manifest .NOTES This function was pulled from the Plaster Source at commit #d048667 #> [CmdletBinding()] param( [string] $ModuleName, [Version] $ModuleVersion, [Switch] $ListAvailable ) #Only get the latest version of each module $modules = Get-Module -ListAvailable if (!$ListAvailable) { $modules = $modules | Group-Object Name | Foreach-Object { $_.group | Sort-Object Version | Select-Object -Last 1 } } Write-Verbose "`nFound $($modules.Length) installed modules to scan for extensions." function ParseVersion($versionString) { $parsedVersion = $null if ($versionString) { # We're targeting Semantic Versioning 2.0 so make sure the version has # at least 3 components (X.X.X). This logic ensures that the "patch" # (third) component has been specified. $versionParts = $versionString.Split('.'); if ($versionParts.Length -lt 3) { $versionString = "$versionString.0" } if ($PSVersionTable.PSEdition -eq "Core") { $parsedVersion = New-Object -TypeName "System.Management.Automation.SemanticVersion" -ArgumentList $versionString } else { $parsedVersion = New-Object -TypeName "System.Version" -ArgumentList $versionString } } return $parsedVersion } foreach ($module in $modules) { if ($module.PrivateData -and $module.PrivateData.PSData -and $module.PrivateData.PSData.Extensions) { Write-Verbose "Found module with extensions: $($module.Name)" foreach ($extension in $module.PrivateData.PSData.Extensions) { Write-Verbose "Comparing against module extension: $($extension.Module)" $minimumVersion = ParseVersion $extension.MinimumVersion $maximumVersion = ParseVersion $extension.MaximumVersion if (($extension.Module -eq $ModuleName) -and (!$minimumVersion -or $ModuleVersion -ge $minimumVersion) -and (!$maximumVersion -or $ModuleVersion -le $maximumVersion)) { # Return a new object with the extension information [PSCustomObject]@{ Module = $module MinimumVersion = $minimumVersion MaximumVersion = $maximumVersion Details = $extension.Details } } } } } } #EndRegion '.\public\Manifest\Get-ModuleExtension.ps1' 86 #Region '.\public\Manifest\Test-CommentedProperty.ps1' -1 function Test-CommentedProperty { <# .SYNOPSIS Test if the given property is commented in the given manifest .EXAMPLE $manifest | Test-CommentedProperty 'ReleaseNotes' #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The item to uncomment [Parameter( Position = 0 )] [Alias('PropertyName')] [string]$Property ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('Path')) { if (Test-Path $Path) { $commentToken = $Path | Find-ParseToken -Type Comment -Pattern "^\s*#\s*$Property\s+=.*$" | Select-Object -First 1 $null -ne $commentToken | Write-Output } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Manifest\Test-CommentedProperty.ps1' 41 #Region '.\public\Manifest\Update-ManifestField.ps1' -1 function Update-ManifestField { <# .SYNOPSIS Set the Value of the given PropertyName, even if it is commented out in the manifest given in Path. .EXAMPLE Get-ChildItem foo.psd1 -Recurse | Update-ManifestField 'ModuleVersion' '0.9.0' Sets ModuleVersion to '0.9.0' in foo.psd1 #> [CmdletBinding( SupportsShouldProcess )] param( # Specifies a path to a manifest file [Parameter( Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # Field in the Manifest to update [Parameter( Mandatory, Position = 0 )] [string]$PropertyName, # List of strings to add to the field [Parameter( Mandatory, Position = 1 )] [string[]]$Value ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { try { Write-Debug "Loading manifest $Path" $manifestItem = Get-Item $Path $manifestObject = Import-Psd $manifestItem.FullName } catch { throw "Cannot load $($Path)`n$_" } $options = $PSBoundParameters $null = $options.Remove('Name') #! if we don't do this, then every field gets written as an array. That doesn't work well for ! many of the #! fields like ModuleVersion, etc. if ($options.Value.Count -eq 1) { $options.Value = [string]$options.Value[0] } if ($manifestObject.ContainsKey($PropertyName)) { #------------------------------------------------------------------------------- #region Field exists Write-Debug " - Manifest has a $PropertyName field. Updating" try { if ($PSCmdlet.ShouldProcess($Path, "Update $PropertyName to $Value")) { Update-Metadata @options } } catch { throw "Cannot update $PropertyName in $Path`n$_" } #endregion Field exists #------------------------------------------------------------------------------- } else { #------------------------------------------------------------------------------- #region Commented Write-Debug "Manifest does not have $PropertyName field. Looking for it in comments" $fieldToken = $manifestItem | Find-ParseToken $PropertyName Comment if ($null -ne $fieldToken) { Write-Debug " - Found comment" try { if ($PSCmdlet.ShouldProcess($Path, "Update $PropertyName to $Value")) { $manifestItem | ConvertFrom-CommentedProperty -Property $PropertyName Update-Metadata @options } } catch { throw "Cannot update $PropertyName in $Path`n$_" } #endregion Commented #------------------------------------------------------------------------------- } else { #------------------------------------------------------------------------------- #region Field missing #! Update-ModuleManifest is not really the best option for editing the psd1, because #! it does a poor job of formatting "proper" arrays, and it doesn't deal with "non-standard" #! fields very well. However, if the field is missing from the file, it is better to use #! Update-ModuleManifest than to clobber the comments and formatting ... Write-Debug "Could not find $PropertyName in Manifest. Calling Update-ModuleManifest" $null = $options.Clear() $options = @{ Path = $Path $PropertyName = $Value } try { if ($PSCmdlet.ShouldProcess($Path, "Update $PropertyName to $Value")) { Update-ModuleManifest @options } } catch { throw "Cannot update $PropertyName in $Path`n$_" } #endregion Field missing #------------------------------------------------------------------------------- } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Manifest\Update-ManifestField.ps1' 125 #Region '.\public\Notification\Invoke-BuildNotification.ps1' -1 function Invoke-BuildNotification { <# .SYNOPSIS Display a Toast notification for a completed build .EXAMPLE Invoke-BuildNotification -LogFile .\out\logs\build-20230525T2051223032Z.log -Status Passed #> [CmdletBinding()] param( # The text to add to the notification [Parameter( )] [string]$Text, # Build status [Parameter( )] [ValidateSet('Passed', 'Failed', 'Unknown')] [string]$Status, # Path to the log file [Parameter( )] [string]$LogFile ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $appImage = (Join-Path (Get-ModulePath) "spool-of-thread_1f9f5.png") } process { if (-not ($PSBoundParameters.ContainsKey('Text'))) { $Text = "Build Complete" } if ($PSBoundParameters.ContainsKey('Status')) { if ($Status -like 'Passed') { $Text = "`u{2705} $Text" } elseif ($Status -like 'Failed') { $Text = "`u{1f6a8} $Text" } } else { $Text = "`u{2754} $Text" } $toastOptions = @{ Text = $Text AppLogo = $appImage } if ($PSBoundParameters.ContainsKey('LogFile')) { if (Test-Path $LogFile) { $logItem = Get-Item $LogFile $btnOptions = @{ Content = "Build Log" ActivationType = 'Protocol' Arguments = $logItem.FullName } $logButton = New-BTButton @btnOptions $toastOptions.Text = @($Text, "View the log file") $toastOptions.Button = $logButton } } New-BurntToastNotification @toastOptions } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Notification\Invoke-BuildNotification.ps1' 74 #Region '.\public\Path\Confirm-Path.ps1' -1 function Confirm-Path { <# .SYNOPSIS Tests if the directory exists and if it does not, creates it. #> [OutputType([bool])] [CmdletBinding()] param( # The path to confirm [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The type of item to confirm [Parameter( )] [ValidateSet('Directory', 'File', 'SymbolicLink', 'Junction', 'HardLink')] [string]$ItemType = 'Directory' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (Test-Path $Path) { Write-Debug "Path exists" $true } else { try { Write-Debug "Checking if the directory exists" $directory = $Path | Split-Path -Parent if (Test-Path $directory) { Write-Debug " - The directory $directory exists" } else { $null = New-Item $directory -Force -ItemType Directory } Write-Debug "Creating $ItemType $Path" $null = New-Item -Path $Path -ItemType $ItemType -Force Write-Debug "Now confirming $Path exists" if (Test-Path $Path) { $true } else { $false } } catch { throw "There was an error confirming $Path`n$_" } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Confirm-Path.ps1' 59 #Region '.\public\Path\Find-BuildConfigurationDirectory.ps1' -1 function Find-BuildConfigurationDirectory { <# .SYNOPSIS Find the directory that contains the build configuration for the given profile #> [Alias('Resolve-BuildConfigurationDirectory')] [CmdletBinding()] param( # The BuildProfile to use [Parameter( )] [string]$BuildProfile ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $Options = @{ Option = 'Constant' Name = 'DEFAULT_BUILD_PROFILE' Value = 'default' Description = 'The default build profile' } New-Variable @Options } process { if (-not ($PSBoundParameters.ContainsKey('BuildProfile'))) { $possibleBuildProfile = $PSCmdlet.GetVariableValue('BuildProfile') if ($null -ne $possibleBuildProfile) { $BuildProfile = $possibleBuildProfile } } if ([string]::IsNullorEmpty($BuildProfile)) { $BuildProfile = $DEFAULT_BUILD_PROFILE } $possibleProfileRoot = Find-BuildProfileRootDirectory if ($null -ne $possibleProfileRoot) { $possibleBuildProfilePath = (Join-Path -Path $possibleProfileRoot -ChildPath $BuildProfile) if (Test-Path $possibleBuildProfilePath) { Get-Item $possibleBuildProfilePath | Write-Output } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-BuildConfigurationDirectory.ps1' 50 #Region '.\public\Path\Find-BuildConfigurationRootDirectory.ps1' -1 function Find-BuildConfigurationRootDirectory { <# .SYNOPSIS Find the build configuration root directory for this project .EXAMPLE Find-BuildConfigurationRootDirectory -Path $BuildRoot .EXAMPLE $BuildRoot | Find-BuildConfigurationRootDirectory .NOTES `Find-BuildConfigurationRootDirectory` looks in the current directory of the caller if no Path is given #> [Alias('Resolve-BuildConfigurationRootDirectory')] [OutputType([System.IO.DirectoryInfo])] [CmdletBinding()] param( # Specifies a path to a location to look for the build configuration root [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #TODO: A good example of what would be in the module's (PoshCode) Configuration if we used it $possibleRoots = @( '.build', '.stitch' ) $configurationRootDirectory = $null } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { $Path = Get-Location } #! if this function is called within a build script, then BuildConfigRoot should be set already $possibleBuildConfigRoot = $PSCmdlet.GetVariableValue('BuildConfigRoot') if (-not ([string]::IsNullOrEmpty($possibleBuildConfigRoot))) { Write-Debug "Found `$BuildConfigRoot => $possibleBuildConfigRoot" $configurationRootDirectory = $possibleBuildConfigRoot } else { :path foreach ($possibleRootPath in $Path) { Write-Debug "Looking for a build configuration directory in $possibleRootPath" :root foreach ($possibleRoot in $possibleRoots) { Write-Debug " - Looking for $possibleRoot directory" $possiblePath = (Join-Path $possibleRootPath $possibleRoot) if (Test-Path $possiblePath) { $possiblePathItem = (Get-Item $possiblePath) if ($possiblePathItem.PSIsContainer) { $configurationRootDirectory = $possiblePathItem } else { $configurationRootDirectory = (Get-Item ($possiblePathItem | Split-Path -Parent)) } Write-Debug " - Found build configuration root directory '$configurationRootDirectory'" break path } } } } } end { $configurationRootDirectory | Write-Output Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-BuildConfigurationRootDirectory.ps1' 68 #Region '.\public\Path\Find-BuildProfileRootDirectory.ps1' -1 function Find-BuildProfileRootDirectory { <# .SYNOPSIS Find the directory that has the profiles defined #> [Alias('Resolve-BuildProfileRootDirectory')] [CmdletBinding()] param( # Specifies a path to a location that contains Build Profiles (This should be BuildConfigPath) [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [AllowEmptyString()] [AllowNull()] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleProfileDirectories = @( 'profiles', 'profile', 'runbooks' ) $profileDirectory = $null } process { if ([string]::IsNullorEmpty($Path)) { $possibleBuildConfigRoot = Find-BuildConfigurationRootDirectory if ([string]::IsNullorEmpty($possibleBuildConfigRoot)) { $Path += Get-Location } else { $Path += $possibleBuildConfigRoot } } #! First, loop through each configuration root directory :root foreach ($possibleRootPath in $Path) { #! Then, loop through each of the default names for a profile directory :profile foreach ($possibleProfileDirectory in $possibleProfileDirectories) { $possibleProfilePath = (Join-Path $possibleRootPath $possibleProfileDirectory) if (Test-Path $possibleProfilePath) { $possiblePathItem = (Get-Item $possibleProfilePath) if ($possiblePathItem.PSIsContainer) { $profileDirectory = $possibleProfilePath } else { $profileDirectory = $possibleProfilePath | Split-Path -Parent } Write-Debug "Found profile root directory '$profileDirectory'" break root } } } } end { $profileDirectory Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-BuildProfileRootDirectory.ps1' 62 #Region '.\public\Path\Find-BuildRunBook.ps1' -1 function Find-BuildRunBook { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleRunbookFilters = @( "*runbook.ps1" ) } process { :path foreach ($location in $Path) { :filter foreach ($possibleRunbookFilter in $possibleRunbookFilters) { $options = @{ Path = $location Recurse = $true Filter = $possibleRunbookFilter File = $true } Get-Childitem @options } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-BuildRunBook.ps1' 37 #Region '.\public\Path\Find-InvokeBuildScript.ps1' -1 function Find-InvokeBuildScript { <# .SYNOPSIS Find all "build script" files. These are files that contain tasks to be executed by Invoke-Build .LINK Find-InvokeBuildTaskFile #> [CmdletBinding()] param( # Specifies a path to one or more locations to look for build scripts. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $buildScriptPattern = "*.build.ps1" } process { foreach ($location in $Path) { if (Test-Path $location) { $options = @{ Path = $location Recurse = $true Filter = $buildScriptPattern } Get-ChildItem @options } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-InvokeBuildScript.ps1' 39 #Region '.\public\Path\Find-InvokeBuildTaskFile.ps1' -1 function Find-InvokeBuildTaskFile { <# .SYNOPSIS Find all "task type" files. These are files that contain "extensions" to the task types. They define a function that creates tasks. .LINK Find-InvokeBuildScript #> [CmdletBinding()] param( # Specifies a path to one or more locations to look for task files. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $taskFilePattern = "*.task.ps1" } process { foreach ($location in $Path) { if (Test-Path $location) { $options = @{ Path = $location Recurse = $true Filter = $taskFilePattern } Get-ChildItem @options } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-InvokeBuildTaskFile.ps1' 41 #Region '.\public\Path\Find-LocalUserStitchDirectory.ps1' -1 function Find-LocalUserStitchDirectory { <# .SYNOPSIS Find the directory in the users home directory that contains stitch configuration items #> [Alias('Resolve-LocalUserStitchDirectory')] [CmdletBinding()] param( # Specifies a path to one or more locations to look for the users local stitch directory [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleRootDirectories = @( $env:USERPROFILE, $env:HOME, $env:LOCALAPPDATA, $env:APPDATA ) $possibleStitchDirectories = @( '.stitch' ) $userStitchDirectory = $null } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { $Path = $possibleRootDirectories } #! We only need to search the 'possibleRootDirectories' if a Path was not given :root foreach ($possibleRootDirectory in $Path) { :stitch foreach ($possibleStitchDirectory in $possibleStitchDirectories) { if ((-not ([string]::IsNullorEmpty($possibleRootDirectory))) -and (-not ([string]::IsNullorEmpty($possibleStitchDirectory)))) { $possiblePath = (Join-Path $possibleRootDirectory $possibleStitchDirectory) if (Test-Path $possiblePath) { $possiblePathItem = (Get-Item $possiblePath) if ($possiblePathItem.PSIsContainer) { $userStitchDirectory = $possiblePath } else { $userStitchDirectory = $possiblePath | Split-Path -Parent } Write-Debug "Local user stitch directory found at $userStitchDirectory" break root } } } } } end { $userStitchDirectory Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-LocalUserStitchDirectory.ps1' 64 #Region '.\public\Path\Find-ModuleManifest.ps1' -1 function Find-ModuleManifest { <# .SYNOPSIS Find all module manifests in the given directory. #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $possibleManifests = Get-ChildItem @PSBoundParameters -Recurse -Filter "*.psd1" foreach ($possibleManifest in $possibleManifests) { try { $module = $possibleManifest | Import-Psd -Unsafe } catch { Write-Debug "$possibleManifest could not be imported" continue } if ($null -ne $module) { Write-Debug "Checking if $($possibleManifest.Name) is a manifest" if ( ($module.Keys -contains 'ModuleVersion') -and ($module.Keys -contains 'GUID') -and ($module.Keys -contains 'PrivateData') ) { $possibleManifest | Write-Output } else { Write-Debug "- Not a module manifest file" } } else { continue } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-ModuleManifest.ps1' 51 #Region '.\public\Path\Find-StitchConfigurationFile.ps1' -1 function Find-StitchConfigurationFile { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleConfigFileFilters = @( 'stitch.config.ps1', '.config.ps1' ) } process { :path foreach ($location in $Path) { Write-Debug "Looking in $location" :filter foreach ($possibleConfigFileFilter in $possibleConfigFileFilters) { $options = @{ Path = $location Recurse = $true Filter = $possibleConfigFileFilter File = $true } $result = Get-Childitem @options | Select-Object -First 1 if ($null -ne $result) { $result | Write-Output continue path } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-StitchConfigurationFile.ps1' 43 #Region '.\public\Path\Find-TestDirectory.ps1' -1 function Find-TestDirectory { <# .SYNOPSIS Find the directory where tests are stored #> [CmdletBinding()] param( # Test file pattern [Parameter( )] [string]$TestsPattern = '*.Tests.ps1' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $root = Resolve-ProjectRoot Write-Debug "Looking for test directory in $root" $testFiles = Find-TestFile $root -TestsPattern $TestsPattern $foundDirectories = [System.Collections.ArrayList]::new() if ($testFiles.Count -gt 0) { :testfile foreach ($testFile in $testFiles) { $relativePath = [System.IO.Path]::GetRelativePath($root, $testFile.FullName) $parts = $relativePath -split [regex]::Escape([System.IO.Path]::DirectorySeparatorChar) Write-Debug "$($testFile.FullName) is $($parts.Count) levels below root" :parts switch ($parts.Count) { 0 { throw "The path to $($testFile.FullName) is invalid" } default { $possibleTestPath = (Join-Path $root $parts[0]) if ($possibleTestPath -notin $foundDirectories) { [void]$foundDirectories.Add($possibleTestPath) continue testfile } } } } } } end { $foundDirectories | Foreach-Object { Get-Item $_ | Write-Output } Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-TestDirectory.ps1' 49 #Region '.\public\Path\Find-TestFile.ps1' -1 function Find-TestFile { <# .SYNOPSIS Find files that contain tests #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Test file pattern [Parameter( )] [string]$TestsPattern = '*.Tests.ps1' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $possibleTestFiles = Get-ChildItem -Path $Path -Recurse -File -Filter $TestsPattern foreach ($possibleTestFile in $possibleTestFiles) { Write-Debug "Checking $possibleTestFile for Pester tests" if ($possibleTestFile | Select-String '\s*Describe') { Write-Debug "- Has the 'Describe' keyword" $possibleTestFile | Write-Output } else { continue } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Find-TestFile.ps1' 43 #Region '.\public\Path\Get-ModulePath.ps1' -1 function Get-ModulePath { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $callStack = Get-PSCallStack $caller = $callStack[1] $caller.InvocationInfo.MyCommand.Module.ModuleBase } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Get-ModulePath.ps1' 19 #Region '.\public\Path\Merge-FileCollection.ps1' -1 function Merge-FileCollection { <# .SYNOPSIS Merge an array of files into an existing collection, overwritting any that have the same basename .NOTES The collection is passed in by reference. This is so that the collection is updated without having to reapply the result. .EXAMPLE $updates | Merge-FileCollection [ref]$allFiles .EXAMPLE Get-ChildItem -Path . -Filter *.ps1 | Merge-FileCollection [ref]$allScripts #> [CmdletBinding()] param( # The collection of files to merge the updates into [Parameter( Mandatory, Position = 0 )] [AllowEmptyCollection()] [ref]$Collection, # The additional files to update the collection with [Parameter( Mandatory, Position = 1, ValueFromPipeline )] [Array]$UpdateFiles ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { foreach ($currentUpdateFile in $UpdateFiles) { <# if this file name exists in the Collection array, we remove it from the collection and add this file, otherwise just add the file #> $baseNames = $Collection.Value | Select-Object -ExpandProperty BaseName if ($baseNames -contains $currentUpdateFile.BaseName ) { $previousTaskFile = $Collection.Value | Where-Object { $_.BaseName -like $currentUpdateFile.BaseName } if ($null -ne $previousTaskFile) { Write-Verbose "Overriding $($currentUpdateFile.BaseName)" $index = $Collection.Value.IndexOf( $previousTaskFile ) $Collection.Value[$index] = $currentUpdateFile } } else { [void]$Collection.Value.Add($currentUpdateFile) } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Merge-FileCollection.ps1' 61 #Region '.\public\Path\Resolve-ProjectRoot.ps1' -1 function Resolve-ProjectRoot { <# .SYNOPSIS Find the root of the current project .DESCRIPTION Resolve-ProjectRoot will recurse directories toward the root folder looking for a directory that passes `Test-ProjectRoot`, unless `$BuildRoot` is already set .LINK Test-ProjectRoot #> [CmdletBinding()] param( # Optionally set the starting path to search from [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path = (Get-Location).ToString(), # Optionally limit the number of levels to seach [Parameter()] [int]$Depth = 8 ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $level = 1 $originalLocation = $Path | Get-Item $currentLocation = $originalLocation $driveRoot = $currentLocation.Root Write-Debug "Current location: $($currentLocation.FullName)" Write-Debug "Current root: $($driveRoot.FullName)" } process { $rootReached = $false if ($null -ne $BuildRoot) { Write-Debug 'BuildRoot is set, using that' $BuildRoot | Write-Output break } :location do { if ($null -ne $currentLocation) { Write-Debug "Level $level : Testing directory $($currentLocation.FullName)" if ($currentLocation.FullName | Test-ProjectRoot) { $rootReached = $true Write-Debug "- Project Root found : $($currentLocation.FullName)" $currentLocation.FullName | Write-Output break location } elseif ($level -eq $Depth) { $rootReached = $true throw "- Could not find project root in $Depth levels" break location } elseif ($currentLocation -like $driveRoot) { $rootReached = $true throw "- $driveRoot reached looking for project root" break location } else { Write-Debug "- $($currentLocation.Name) is not the project root" } } else { Write-Debug "- Reached the root of the drive" $rootReached = $true } Write-Debug 'Setting current location to Parent' $currentLocation = $currentLocation.Parent $level++ } until ($rootReached) } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Resolve-ProjectRoot.ps1' 78 #Region '.\public\Path\Resolve-SourceDirectory.ps1' -1 function Resolve-SourceDirectory { <# .SYNOPSIS Resolve the directory that contains the project's source files #> [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $ignoredDirectories = @('.build', '.stitch') $maximumNestedLevel = 4 } process { $root = Resolve-ProjectRoot $sourceDirectory = $null Write-Debug "Looking for source directory in $root" $manifests = Find-ModuleManifest $root if ($manifests.Count -gt 0) { :manifest foreach ($manifest in $manifests) { $relativePath = [System.IO.Path]::GetRelativePath($root, $manifest.FullName) $parts = $relativePath -split [regex]::Escape([System.IO.Path]::DirectorySeparatorChar) Write-Debug "$($manifest.FullName) is $($parts.Count) levels below root" :parts switch ($parts.Count) { 0 { throw "The path to $($manifest.FullName) is invalid" } default { if ($parts[0] -notin $ignoredDirectories) { if ($parts.Count -lt $maximumNestedLevel ) { $possibleSourceDirectory = Get-Item (Join-Path $root $parts[0]) if ($null -eq $sourceDirectory) { $sourceDirectory = $possibleSourceDirectory } else { if ($possibleSourceDirectory.FullName -eq $sourceDirectory.FullName) { Write-Debug "$($possibleSourceDirectory.Name) already set" } } } else { Write-Debug "$($manifest.Name) is nested below maximum levels: $($parts.Count)" } } else { Write-Debug "$($parts[0]) is ignored" } continue manifest } } } } else { throw "No manifests found in project '$root'" } $sourceDirectory } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Resolve-SourceDirectory.ps1' 61 #Region '.\public\Path\Test-PathIsIn.ps1' -1 function Test-PathIsIn { <# .SYNOPSIS Confirm if the given path is within the other .DESCRIPTION `Test-PathIsIn` checks if the given path (-Path) is a subdirectory of the other (-Parent) .EXAMPLE Test-PathIsIn "C:\Windows" -Path "C:\Windows\System32\" .EXAMPLE "C:\Windows\System32" | Test-PathIsIn "C:\Windows" #> [OutputType([System.Boolean])] [CmdletBinding()] param( # The path to test (the subdirectory) [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The path to test (the subdirectory) [Parameter( Position = 0 )] [string]$Parent, # Compare paths using case sensitivity [Parameter( )] [switch]$CaseSensitive ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { try { Write-Debug "Resolving given Path $Path" $childItem = Get-Item (Resolve-Path $Path) Write-Debug "Resolving given Parent $Parent" $parentItem = Get-Item (Resolve-Path $Parent) if ($CaseSensitive) { Write-Debug "Matching case-sensitive" $parentPath = $parentItem.FullName $childPath = $childItem.FullName } else { Write-Debug "Matching" $parentPath = $parentItem.FullName.ToLowerInvariant() $childPath = $childItem.FullName.ToLowerInvariant() } Write-Verbose "Testing if '$childPath' is in '$parentPath'" # early test using string comparison #! note: will return a false positive for directories with partial match like #! c:\windows\system , c:\windows\system32 Write-Debug "Does '$childPath' start with '$parentPath'" if (-not($childPath.StartsWith($parentPath))) { Write-Debug " - Yes. Return False" return $false } else { $childRoot = $childItem.Root $parentRoot = $parentItem.Root Write-Debug " - Yes. Checking path roots '$childRoot' and '$parentRoot'" # they /should/ be equal if we made it here if ($parentRoot -notlike $childRoot) { return $false } $childPathParts = $childPath -split [regex]::Escape([IO.Path]::DirectorySeparatorChar) $depth = $childPathParts.Count $currentPath = $childItem $parentFound = $false :depth foreach ($level in 1..($depth - 1)) { $currentPath = $currentPath.Parent Write-Debug "Testing if $currentPath equals $($parentItem.FullName)" if ($currentPath -like $parentItem.FullName) { Write-Debug " - Parent found" $parentFound = $true break depth } } if ($parentFound) { Write-Debug " - Parent found. Return True" return $true } Write-Debug " - Parent not found. Return False" return $false } } catch { $PSCmdlet.ThrowTerminatingError($_) } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Test-PathIsIn.ps1' 108 #Region '.\public\Path\Test-ProjectRoot.ps1' -1 function Test-ProjectRoot { <# .SYNOPSIS Test if the given directory is the root directory of a project .DESCRIPTION `Test-ProjectRoot` looks for the build configuration file and directory `.build.ps1` and either `.stitch\` or `.build` .EXAMPLE Test-ProjectRoot Without a -Path, tests the current directory for default project directories .EXAMPLE $projectPath | Test-ProjectRoot .NOTES Defaults are: - Source : .\source - Staging : .\stage - Tests : .\tests - Artifact : .\out - Docs : .\docs #> [CmdletBinding()] param( # Optionally give a path to start in [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateScript( { if (-not($_ | Test-Path)) { throw "$_ does not exist" } return $true } )] [Alias('PSPath')] [string]$Path = (Get-Location).ToString() ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleRoots = @('.build', '.stitch') } process { $possibleBuildConfigRoot = $Path | Find-BuildConfigurationRootDirectory if ($null -ne $possibleBuildConfigRoot) { Write-Debug "Found build config root directory '$possibleBuildConfigRoot'" if ($possibleBuildConfigRoot.Name -in $possibleRoots) { Write-Debug "$($possibleBuildConfigRoot.Name) is a valid root" if (Get-ChildItem -Path $Path -Filter '.build.ps1') { Write-Debug "Found build script" $true | Write-Output } else { Write-Debug "Did not find build script" $false | Write-Output } } else { $false | Write-Output } } else { $false | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Path\Test-ProjectRoot.ps1' 70 #Region '.\public\Project\Get-GitDescribeInfo.ps1' -1 function Get-GitDescribeInfo { <# .SYNOPSIS Return the version information found in `git describe` .DESCRIPTION `git describe` will print out the version information in the form of: <tag>-<commits since>-<short sha> #> [CmdletBinding()] param( # Only use annotated tags (--tags is used by default) [Parameter( )] [switch]$AnnotatedOnly ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $describePattern = (@( '^v?(?<majr>\d+)\.', '(?<minr>\d+)\.', '(?<ptch>\d+)', '(?<rmdr>.*)$' ) -join '') $versionInfo = @{ PSTypeName = 'Stitch.VersionInfo' Full = '' Major = 0 Minor = 0 Patch = 0 CommitsSinceVersionSource = 0 ShortSha = '' PreReleaseTag = '' MajorMinorPatch = '' SemVer = '' } } process { $gitCommand = (Get-Command 'git.exe' -ErrorAction SilentlyContinue) if ($null -ne $gitCommand) { $arguments = [System.Collections.ArrayList]@('describe') if (-not($AnnotatedOnly)) { [void]$arguments.Add('--tags') } [void]$arguments.Add('--long') Write-Debug "calling git with arguments $($arguments -join ' ')" $result = & $gitCommand $arguments if ($result.Length -gt 0) { $versionInfo.Full = $result if ($result -match $describePattern) { $versionInfo.Major = ($Matches.majr ?? 0) $versionInfo.Minor = ($Matches.minr ?? 0) $versionInfo.Patch = ($Matches.ptch ?? 0) $versionInfo.MajorMinorPatch = ('{0}.{1}.{2}' -f $Matches.majr, $Matches.minr, $Matches.ptch) $parts = [System.Collections.ArrayList]@($Matches.rmdr.Split('-')) switch ($parts.Count) { 0 { Write-Debug "Did not find any parts in $($Matches.rmdr)" } 1 { $versionInfo.ShortSha = $parts[0] continue } 2 { $versionInfo.CommitsSinceVersionSource = $parts[0] $versionInfo.ShortSha = $parts[1] continue } default { $versionInfo.ShortSha = $parts[-1] [void]$parts.Remove($parts[-1]) $versionInfo.CommitsSinceVersionSource = $parts[-1] [void]$parts.Remove($parts[-1]) $versionInfo.PreReleaseTag = ($parts -join '-') } } $versionInfo.SemVer = ( @( $versionInfo.MajorMinorPatch, $versionInfo.PreReleaseTag) -join '') [PSCustomObject]$versionInfo | Write-Output } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Get-GitDescribeInfo.ps1' 94 #Region '.\public\Project\Get-GitVersionInfo.ps1' -1 function Get-GitVersionInfo { <# .SYNOPSIS Return the output of gitversion dotnet tool as an object #> [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug ' - Checking for gitversion utility' $gitverCmd = Get-Command dotnet-gitversion.exe -ErrorAction SilentlyContinue if ($null -ne $gitverCmd) { Write-Verbose 'Using gitversion for version info' $gitVersionCommandInfo = & $gitverCmd @('-?') Write-Debug ' - gitversion found. Getting version info' $gitVersionCommandInfo | Write-Debug $gitVersionOutput = & $gitverCmd @( '-output', 'json') if ([string]::IsNullorEmpty($gitVersionOutput)) { Write-Warning 'No output from gitversion' } else { Write-Debug "Version info: $gitVersionOutput" try { $gitVersionOutput | ConvertFrom-Json | Write-Output } catch { throw "Could not parse json:`n$gitVersionOutput`n$_" } } } Write-Debug ' - gitversion not found' Write-Information "GitVersion is not installed.`nsee <https://gitversion.net/docs/usage/cli/installation> for details" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Get-GitVersionInfo.ps1' 42 #Region '.\public\Project\Get-ProjectPath.ps1' -1 function Get-ProjectPath { <# .SYNOPSIS Retrieve the paths to the major project components. (Source, Tests, Docs, Artifacts, Staging) #> [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)'" $stitchPathFiles = @( '.stitch.config.psd1', '.stitch.psd1', 'stitch.config.psd1' ) } process { $root = Resolve-ProjectRoot if ($null -ne $root) { $possibleBuildRoot = $PSCmdlet.GetVariableValue('BuildRoot') if (-not ([string]::IsNullorEmpty($possibleBuildRoot))) { $root = $possibleBuildRoot } else { $root = Get-Location } } Write-Verbose "Looking for path config file in $root" $pathConfigFiles = (Get-ChildItem -Path "$root/*.psd1" -Include $stitchPathFiles) if ($pathConfigFiles.Count -gt 0) { Write-Debug ('Found ' + ($pathConfigFiles.Name -join "`n")) $pathConfigFile = $pathConfigFiles[0] } if ($null -ne $pathConfigFile) { Write-Verbose " - found $pathConfigFile" try { $config = Import-Psd $pathConfigFile $resolved = @{} foreach ($key in $config.Keys) { $resolved[$key] = (Resolve-Path $config[$key]) } } catch { $PSCmdlet.ThrowTerminatingError($_) } [PSCustomObject]$resolved | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Get-ProjectPath.ps1' 52 #Region '.\public\Project\Get-ProjectVersionInfo.ps1' -1 function Get-ProjectVersionInfo { <# .SYNOPSIS Return a collection of Version Information about the project .DESCRIPTION gitversion dotnet tool git describe version.(psd1|json|yaml) #> [CmdletBinding( DefaultParameterSetName = 'gitdescribe' )] param( # Use git describe instead of gitversion [Parameter( ParameterSetName = 'gitdescribe' )] [switch]$UseGitDescribe, # Use the information in version.(psd1|json|yml) [Parameter( ParameterSetName = 'versionfile' )] [switch]$UseVersionFile ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug 'Checking for version information' try { if ($UseVersionFile) { Get-VersionFileInfo } else { $cmd = Get-Command 'gitversion' -ErrorAction SilentlyContinue if (($null -ne $cmd) -and (-not ($UseGitDescribe))) { Get-GitVersionInfo } else { Get-GitDescribeInfo } } } catch { $message = "Could not get version information for the project" $exceptionText = ( @($message, $_.ToString()) -join "`n") $thisException = [Exception]::new($exceptionText) $eRecord = New-Object System.Management.Automation.ErrorRecord -ArgumentList ( $thisException, $null, # errorId $_.CategoryInfo.Category, # errorCategory $null # targetObject ) $PSCmdlet.ThrowTerminatingError( $eRecord ) } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Get-ProjectVersionInfo.ps1' 61 #Region '.\public\Project\Get-VersionFileInfo.ps1' -1 function Get-VersionFileInfo { <# .SYNOPSIS Return version info stored in a file in the project #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ($PSBoundParameters.ContainsKey('Path'))) { $Path = Resolve-ProjectRoot } Write-Debug ' - Looking for version.* file' $found = Get-ChildItem -Path $Path -Filter 'version.*' -Recurse | Sort-Object LastWriteTime | Select-Object -Last 1 if ($null -ne $found) { Write-Verbose "Using $found for version info" Write-Debug " - Found $($found.FullName)" switch -Regex ($found.extension) { 'psd1' { $versionInfo = Import-Psd $found } 'json' { $versionInfo = (Get-Content $found | ConvertFrom-Json) } 'y(a)?ml' { $versionInfo = (Get-Content $found | ConvertFrom-Yaml) } Default { Write-Information "$($found.Name) found but no converter for $($found.extension) is set" } } $versionInfo['PSTypeName'] = 'Stitch.VersionInfo' [PSCustomObject]$versionInfo | Write-Output } else { throw "Could not find version file in $Path" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Get-VersionFileInfo.ps1' 49 #Region '.\public\Project\Initialize-StitchProject.ps1' -1 #using namespace System.Diagnostics.CodeAnalysis function Initialize-StitchProject { [Alias('Institchilize')] [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'high' )] [SuppressMessage('PSAvoidUsingWriteHost', '', Justification='Output of write operation should not be redirected')] param( # The directory to initialize the build tool in. # Defaults to the current directory. [Parameter( )] [string]$Destination, # Overwrite existing files [Parameter( )] [switch]$Force, # Do not output any status to the console [Parameter( )] [switch]$Quiet ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ([string]::IsNullorEmpty($Destination)) { Write-Debug "Setting Destination to current directory" $Destination = (Get-Location).Path } $possibleBuildConfigRoot = $Destination | Find-BuildConfigurationRootDirectory if (-not ([string]::IsNullorEmpty($possibleBuildConfigRoot))) { $buildConfigDir = $possibleBuildConfigRoot } else { $buildConfigDefaultDir = '.build' } #------------------------------------------------------------------------------- #region Gather info if (-not($Quiet)) { Write-StitchLogo -Size 'large' } New-StitchPathConfigurationFile -Force:$Force if (-not ([string]::IsNullorEmpty($buildConfigDir))) { "Found your build configuration directory '$(Resolve-Path $buildConfigDir -Relative)'" } else { $prompt = ( -join @( 'What is the name of your build configuration directory? ', $PSStyle.Foreground.BrightBlack, " ( $buildConfigDefaultDir )", $PSStyle.Reset ) ) $ans = Read-Host $prompt if ([string]::IsNullorEmpty($ans)) { $ans = $buildConfigDefaultDir } $buildConfigDir = (Join-Path $Destination $ans) } #endregion Gather info #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Create directories Write-Debug "Create directories if they do not exist" Write-Debug " - Looking for $buildConfigDir" if (-not(Test-Path $buildConfigDir)) { try { '{0} does not exist. {1}Creating{2}' -f $buildConfigDir, $PSStyle.Foreground.Green, $PSStyle.Reset $null = mkdir $buildConfigDir -Force } catch { throw "Could not create Build config directory $BuildConfigDir`n$_" } } $profileRoot = $buildConfigDir | Find-BuildProfileRootDirectory if ($null -eq $profileRoot) { $profileRoot = (Join-Path $buildConfigDir 'profiles') try { '{0} does not exist. {1}Creating{2}' -f $profileRoot, $PSStyle.Foreground.Green, $PSStyle.Reset $null = mkdir $profileRoot -Force } catch { throw "Could not create build profile directory $profileRoot`n$_" } } if (-not (Test-Path (Join-Path $profileRoot 'default'))) { '{0} does not exist. {1}Creating{2}' -f 'default profile', $PSStyle.Foreground.Green, $PSStyle.Reset } $profileRoot | New-StitchBuildProfile -Name 'default' -Force:$Force Get-ChildItem (Join-Path $profileRoot 'default') -Filter "*.ps1" | Foreach-Object { $_ | Format-File 'CodeFormattingOTBS' } if (-not (Test-Path (Join-Path $Destination '.build.ps1'))) { '{0} does not exist. {1}Creating{2}' -f 'build runner', $PSStyle.Foreground.Green, $PSStyle.Reset } $Destination | New-StitchBuildRunner -Force:$Force Get-ChildItem $Destination -Filter ".build.ps1" | Format-File 'CodeFormattingOTBS' #endregion Create directories #------------------------------------------------------------------------------- } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Initialize-StitchProject.ps1' 117 #Region '.\public\Project\New-StitchBuildProfile.ps1' -1 #using namespace System.Diagnostics.CodeAnalysis function New-StitchBuildProfile { [SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'File creation methods have their own ShouldProcess')] [CmdletBinding()] param( # The name of the profile to create [Parameter( Mandatory, Position = 0 )] [string]$Name, # Profile path in the build config path [Parameter( Position = 1, ValueFromPipeline )] [string]$ProfileRoot, # Overwrite the profile if it exists [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ($PSBoundParameters.ContainsKey('ProfileRoot'))) { $possibleProfileRoot = Find-BuildProfileRootDirectory if ($null -ne $possibleProfileRoot) { $ProfileRoot = $possibleProfileRoot Remove-Variable $possibleProfileRoot -ErrorAction SilentlyContinue } else { throw 'Could not find the build profile root directory. Use -ProfileRoot' } } $newProfileDirectory = (Join-Path $ProfileRoot $Name) if ((Test-Path $newProfileDirectory) -and (-not ($Force))) { throw "Profile '$Name' already exists at $newProfileDirectory. Use -Force to Overwrite" } else { try { Write-Debug 'Creating directory' $null = mkdir $newProfileDirectory -Force Write-Debug 'Creating runbook' $newProfileDirectory | New-StitchRunBook -Force:$Force Write-Debug 'Creating configuration file' $newProfileDirectory | New-StitchConfigurationFile -Force:$Force } catch { throw "Could not create new build profile '$Name' in '$newProfileDirectory'`n$_" } #TODO: if we fail to create a file, should we remove the folder in a finally block? } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchBuildProfile.ps1' 64 #Region '.\public\Project\New-StitchBuildRunner.ps1' -1 function New-StitchBuildRunner { <# .SYNOPSIS Create the main stitch build script .EXAMPLE New-StitchBuildRunner $BuildRoot Creates the file $BuildRoot\.build.ps1 #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Low' )] param( # Specifies a path to the folder where the runbook should be created [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The name of the main build script. [Parameter( )] [string]$Name, # Overwrite the file if it exists [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $template = Get-StitchTemplate -Type 'install' -Name '.build.ps1' if ($null -ne $template) { $template.Destination = $Path if ($PSBoundParameters.ContainsKey('Name')) { $template.Name = $Name } if (Test-Path $template.Target) { if ($Force) { if ($PSCmdlet.ShouldProcess($template.Target, 'Overwrite file')) { $template | Invoke-StitchTemplate -Force } } else { throw "$($template.Target) already exists. Use -Force to overwrite" } } else { $template | Invoke-StitchTemplate } } else { throw 'Could not find the stitch build script file template' } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchBuildRunner.ps1' 66 #Region '.\public\Project\New-StitchConfigurationFile.ps1' -1 function New-StitchConfigurationFile { <# .SYNOPSIS Create a configuration in the folder specified in Path. .EXAMPLE New-StitchConfigurationFile $BuildRoot\.stitch\profiles\site Creates the file $BuildRoot\.stitch\profiles\site\stitch.config.ps1 #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Low' )] param( # Specifies a path to the folder where the runbook should be created [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The name of the configuration file. [Parameter( )] [string]$Name, # Overwrite the file if it exists [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $template = Get-StitchTemplate -Type 'install' -Name '.config.ps1' if ($null -ne $template) { $template.Destination = $Path if ($PSBoundParameters.ContainsKey('Name')) { $template.Name = $Name } else { $template.Name = 'stitch.config.ps1' } if (Test-Path $template.Target) { if ($Force) { if ($PSCmdlet.ShouldProcess($template.Target, 'Overwrite file')) { $template | Invoke-StitchTemplate -Force } } else { throw "$($template.Target) already exists. Use -Force to overwrite" } } else { $template | Invoke-StitchTemplate } } else { throw 'Could not find the stitch configuration file template' } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchConfigurationFile.ps1' 67 #Region '.\public\Project\New-StitchConfigurationPath.ps1' -1 function New-StitchConfigurationPath { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The name of the directory. Supports '.build' or '.stitch' [Parameter( )] [ValidateSet('.build', '.stitch')] [string]$Name = '.build' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ($PSBoundParameters.ContainsKey('Path'))) { $Path = Get-Location } $buildConfigDir = (Join-Path $Path $Name) Write-Debug 'Create directories if they do not exist' Write-Debug " - Looking for $buildConfigDir" if (-not(Test-Path $buildConfigDir)) { try { '{0} does not exist. {1}Creating{2}' -f $buildConfigDir, $PSStyle.Foreground.Green, $PSStyle.Reset $null = mkdir $buildConfigDir -Force } catch { throw "Could not create Build config directory $BuildConfigDir`n$_" } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchConfigurationPath.ps1' 43 #Region '.\public\Project\New-StitchPathConfigurationFile.ps1' -1 function New-StitchPathConfigurationFile { [CmdletBinding( SupportsShouldProcess )] param( # Default Source directory [Parameter( )] [string]$Source, # Default Tests directory [Parameter( )] [string]$Tests, # Default Staging directory [Parameter( )] [string]$Staging, # Default Artifact directory [Parameter( )] [string]$Artifact, # Default Docs directory [Parameter( )] [string]$Docs, # Do not validate paths [Parameter( )] [switch]$DontValidate, # Overwrite an existing file [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $defaultPathConfigFile = (Join-Path (Get-Location) '.stitch.config.psd1') $locations = @{ Source = @{} Tests = @{} Staging = @{} Artifacts = @{} Docs = @{} } } process { foreach ($location in $locations.Keys) { if (-not ($PSBoundParameters.ContainsKey($location))) { $pathIsSet = $false do { $ans = Read-Host "The directory where this project's $location is stored " if (-not ($DontValidate)) { $possiblePath = (Join-Path (Get-Location) $ans) if (-not (Test-Path $possiblePath)) { $confirmAnswer = Read-Host "$possiblePath does not exist. Use anyway?" if (([string]::IsNullorEmpty($confirmAnswer)) -or ($confirmAnswer -match '^[yY]')) { $PSBoundParameters[$location] = $ans $pathIsSet = $true # break out of loop for this location } } else { $pathIsSet = $true } } else { $pathIsSet = $true } } while (-not ($pathIsSet)) } } $pathSettings = $PSBoundParameters foreach ($unusedParameter in @('DontValidate', 'Force')) { if ($pathSettings.ContainsKey($unusedParameter)) { $null = $pathSettings.Remove($unusedParameter) } } if (Test-Path $defaultPathConfigFile) { if ($Force) { if ($PSCmdlet.ShouldProcess("$defaultPathConfigFile", "Overwrite existing file")) { $pathSettings | Export-Psd -Path $defaultPathConfigFile } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchPathConfigurationFile.ps1' 98 #Region '.\public\Project\New-StitchRunBook.ps1' -1 function New-StitchRunBook { <# .SYNOPSIS Create a runbook in the folder specified in Path. .EXAMPLE New-StitchRunBook $BuildRoot\.stitch\profiles\site Creates the file $BuildRoot\.stitch\profiles\site\runbook.ps1 #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Low' )] param( # Specifies a path to the folder where the runbook should be created [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The name of the runbook. Not needed if using profiles [Parameter( )] [string]$Name, # Overwrite the file if it exists [Parameter( )] [switch]$Force ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $template = Get-StitchTemplate -Type 'install' -Name 'runbook.ps1' $template.Destination = $Path if ($null -ne $template) { if ($PSBoundParameters.ContainsKey('Name')) { $template.Name = $Name } if (Test-Path $template.Target) { if ($Force) { if ($PSCmdlet.ShouldProcess($template.Target, "Overwrite file")) { $template | Invoke-StitchTemplate -Force } } else { throw "$($template.Target) already exists. Use -Force to overwrite" } } else { $template | Invoke-StitchTemplate } } else { throw "Could not find the runbook template" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\New-StitchRunBook.ps1' 65 #Region '.\public\Project\Resolve-ProjectName.ps1' -1 function Resolve-ProjectName { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $config = Get-BuildConfiguration if ([string]::IsNullorEmpty($config.Project.Name)) { Write-Debug "Project name not set in configuration`n trying to resolve project root" $root = (Resolve-ProjectRoot).BaseName } else { Write-Debug "Project Name found in configuration" $root = $config.Project.Name } } end { $root Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Resolve-ProjectName.ps1' 24 #Region '.\public\Project\Test-ProjectPath.ps1' -1 function Test-ProjectPath { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Test-ProjectPath.ps1' 17 #Region '.\public\Project\Write-StitchLogo.ps1' -1 function Write-StitchLogo { [CmdletBinding()] param( # Small or large logo [Parameter( )] [ValidateSet('small', 'large')] [string]$Size = 'large', # Do not print the logo in color [Parameter( )] [switch]$NoColor ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $stitchEmoji = "`u{1f9f5}" $stitchLogoSmall = @' :2: ___ _ _ _ _ :2: / __|| |_ (_)| |_ __ | |_ :2: \__ \| _|| || _|/ _|| \ :2: |___/ \__||_| \__|\__||_||_| '@ $stitchLogoLarge = @' :1:-=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=:0: :1:=- ________________ =-:0: :1:-= (________________) xxxxxx xx xxxx xx xx -=:0: :1:=-:0: :2:(______ ) :1: x x x x x x x x x x =-:0: :1:-=:0: :2:( _____ ) :1: x xxxx x xxxxx xxxx x xxxxx xxxx x xxx -=:0: :1:=-:0: :2:( ____ ) :1: x x x x xxxx x x x x x x =-:0: :1:-=:0: :2:( ____) :1: xxxx x x xxx x x x xxx x xxx x x -=:0: :1:=-:0: _:2:(____________):1:_ x x x x x x x x x x x x x x =-:0: :1:-= (________________) xxxxxxx xxxxxx xxxx xxxxxx xxxxxx xxxx xxxx -=:0: :1:=- =-:0: :1:-=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=--=-=:0: '@ } process { if ($Size -like 'small') { $logoSource = $stitchLogoSmall } else { $logoSource = $stitchLogoLarge } if (-not($NoColor)) { $colors = @( $PSStyle.Reset, $PSStyle.Foreground.FromRgb('#b1a986'), $PSStyle.Foreground.FromRgb('#0679d0') ) } else { $colors = @( '', '', '' ) } $logoOutput = $logoSource for ($c = 0; $c -lt $colors.Length; $c++) { $logoOutput = $logoOutput -replace ":$($c.ToString()):", $colors[$c] } } end { $logoOutput Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Project\Write-StitchLogo.ps1' 72 #Region '.\public\SourceInfo\Find-TodoItem.ps1' -1 function Find-TodoItem { <# .SYNOPSIS Find all comments in the code base that have the 'TODO' keyword .DESCRIPTION Show a list of all "TODO comments" in the code base starting at the directory specified in Path .EXAMPLE Find-TodoItem $BuildRoot #> [OutputType('Stitch.SourceItem.Todo')] [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $todoPattern = '^(\s*)(#)?\s*TODO(:)?\s+(.*)$' } process { #TODO: To refine this we could parse the file and use the comment tokens to give to Select-String $results = Get-ChildItem $Path -Recurse | Select-String -Pattern $todoPattern -CaseSensitive -AllMatches foreach ($result in $results) { [PSCustomObject]@{ PSTypeName = 'Stitch.SourceItem.Todo' Text = $result.Matches[0].Groups[4].Value Position = (-join ($result.Path, ':', $result.LineNumber)) File = (Get-Item $result.Path) Line = $result.LineNumber } | Write-Output } #> } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Find-TodoItem.ps1' 46 #Region '.\public\SourceInfo\Get-ModuleItem.ps1' -1 function Get-ModuleItem { <# .SYNOPSIS Retrieve the modules in the given path .DESCRIPTION Get-ModuleItem returns an object representing the information about the modules in the directory given in Path. It returns information from the manifest such as version number, etc. as well as SourceItemInfo objects for all of the source items found in it's subdirectories .EXAMPLE Get-ModuleItem .\source .LINK Get-SourceItem #> [OutputType('Stitch.ModuleItemInfo')] [CmdletBinding()] param( # Specifies a path to one or more locations containing Module Source [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [ValidateNotNullOrEmpty()] [string[]]$Path, # Optionally return a hashtable instead of an object [Parameter( )] [switch]$AsHashTable ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not ($PSBoundParameters.ContainsKey('Path'))) { $Path = Resolve-SourceDirectory } foreach ($p in $Path) { Write-Debug " Looking for module source in '$p'" try { $pathItem = Get-Item $p -ErrorAction Stop if (-not($pathItem.PSIsContainer)) { Write-Verbose "$p is not a Directory, skipping" continue } foreach ($modulePath in ($pathItem | Get-ChildItem -Directory)) { $info = @{} [ModuleFlag]$flags = [ModuleFlag]::None $name = $modulePath.Name Write-Debug " Module name is $name" $info['Name'] = $name $info['ModuleName'] = $name $manifestFile = (Join-Path $modulePath "$name.psd1") if (Test-Path $manifestFile) { $manifestObject = Import-Psd $manifestFile if (($manifestObject.Keys -contains 'PrivateData') -and ($manifestObject.Keys -contains 'GUID')) { [ModuleFlag]$flags = [ModuleFlag]::HasManifest Write-Debug " Found $name.psd1 testing Manifest" $info['ManifestFile'] = "$name.psd1" $info['Path'] = $manifestFile } } $sourceInfo = Get-SourceItem $modulePath.Parent | Where-Object Module -like $name if ($null -ne $sourceInfo) { [ModuleFlag]$flags += [ModuleFlag]::HasModule } if ($flags.hasFlag([ModuleFlag]::HasManifest)) { Write-Verbose "Manifest found in $($modulePath.BaseName)" foreach ($key in $manifestObject.Keys) { if ($key -notlike 'PrivateData') { $info[$key] = $manifestObject[$key] } } foreach ($key in $manifestObject.PrivateData.PSData.Keys) { $info[$key] = $manifestObject.PrivateData.PSData[$key] } } if ($flags.hasFlag([ModuleFlag]::HasModule)) { $info['SourceDirectories'] = $sourceInfo | Where-Object { @('function', 'class', 'enum') -contains $_.Type } | Select-Object -ExpandProperty Directory | Sort-Object -Unique $info['SourceInfo'] = $sourceInfo if ($info.Keys -notcontains 'RootModule') { $info['ModuleFile'] = "$name.psm1" } else { $info['ModuleFile'] = $info.RootModule } Write-Verbose "Module source found in $($modulePath.BaseName)" } if ($AsHashTable) { $info | Write-Output } else { $info['PSTypeName'] = 'Stitch.ModuleItemInfo' [PSCustomObject]$info | Write-Output } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Get-ModuleItem.ps1' 114 #Region '.\public\SourceInfo\Get-SourceItem.ps1' -1 function Get-SourceItem { <# #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # Path to the source type map [Parameter( )] [string]$TypeMap ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { Write-Debug "No path specified. Using default source folder" #TODO: Yikes! hard-coded source path $Path = (Join-Path (Resolve-ProjectRoot) 'source') Write-Debug "Source path root: $Path" } foreach ($p in $Path) { $sourceRoot = $p try { $item = Get-Item $p -ErrorAction Stop if ($item.PSIsContainer) { Get-ChildItem $item.FullName -Recurse -File | Get-SourceItemInfo -Root $sourceRoot | Write-Output continue } else { $item | Get-SourceItemInfo -Root $sourceRoot | Write-Output continue } } catch { Write-Warning "$p is not a valid path`n$_" } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Get-SourceItem.ps1' 57 #Region '.\public\SourceInfo\Get-SourceTypeMap.ps1' -1 function Get-SourceTypeMap { <# .SYNOPSIS Retrieve the table that maps source items to the appropriate Visibility and Type .LINK Get-SourceItemInfo #> [CmdletBinding()] param( # Specifies a path to the source type map file. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #TODO: Another item that would be good to add to the PoshCode Configuration New-Variable -Name DEFAULT_FILE_NAME -Value 'sourcetypes.config.psd1' -Option Constant } process { if (-not ($PSBoundParameters.ContainsKey('Path'))) { Write-Debug "No Path given. Looking for Map file in Build Configuration Directory" $possibleBuildConfigPath = Find-BuildConfigurationDirectory if ($null -ne $possibleBuildConfigPath) { $possibleMapFile = (Join-Path $possibleBuildConfigPath $DEFAULT_FILE_NAME) } if ($null -eq $possibleMapFile) { Write-Debug "Not found. Looking for Map file in Build Configuration Root Directory" $possibleBuildConfigPath = Find-BuildConfigurationRootDirectory if ($null -ne $possibleBuildConfigPath) { $possibleMapFile = (Join-Path $possibleBuildConfigPath $DEFAULT_FILE_NAME) } } } else { if (Test-Path $Path) { $pathItem = Get-Item $Path if ($pathItem.PSIsContainer) { $possibleMapFile = (Join-Path $Path $DEFAULT_FILE_NAME) } else { $possibleMapFile = $Path } } } if (Test-Path $possibleMapFile) { Write-Verbose "Source Type Map file was found at $possibleMapFile" try { $config = Import-Psd $possibleMapFile -Unsafe if ($null -ne $config) { $config['TypeMapFilePath'] = $possibleMapFile $config | Write-Output } } catch { $PSCmdlet.ThrowTerminatingError($_) } } else { Write-Error "No $DEFAULT_FILE_NAME could be found" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Get-SourceTypeMap.ps1' 73 #Region '.\public\SourceInfo\Get-TestItem.ps1' -1 function Get-TestItem { [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if (-not($PSBoundParameters.ContainsKey('Path'))) { Write-Debug "No path specified. Looking for `$Tests" $testsVariable = $PSCmdlet.GetVariableValue('Tests') if ($null -ne $testsVariable) { Write-Debug " - found `$Tests: $testsVariable" } else { Write-Debug 'Checking for default tests folder' $possiblePath = (Join-Path (Resolve-ProjectRoot) 'tests') if ($null -ne $possiblePath) { if (Test-Path $possiblePath) { $Path = $possiblePath } } } if ($null -eq $Path) { throw 'Could not resolve a Path to tests' } else { Write-Debug "Path is $Path" } } foreach ($p in $Path) { try { $item = Get-Item $p -ErrorAction Stop } catch { Write-Warning "$p is not a valid path`n$_" continue } if ($item.PSIsContainer) { try { Get-ChildItem $item.FullName -Recurse:$Recurse -File | Get-TestItemInfo -Root $item.FullName | Write-Output } catch { Write-Warning "$_" } continue } else { if ($item.Extension -eq '.ps1') { try { $item | Get-TestItemtInfo | Write-Output } catch { Write-Warning "$_" } continue } continue } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Get-TestItem.ps1' 74 #Region '.\public\SourceInfo\New-FunctionItem.ps1' -1 function New-FunctionItem { <# .SYNOPSIS Create a new function source item in the given module's source folder with the give visibility .EXAMPLE $module | New-FunctionItem Get-FooItem public .EXAMPLE New-FunctionItem Get-FooItem Foo public #> [CmdletBinding()] param( # The name of the Function to create [Parameter( Mandatory, Position = 0 )] [string]$Name, # The name of the module to create the function for [Parameter( Mandatory, Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('ModuleName')] [string]$Module, # Visibility of the function ('public' for exported commands, 'private' for internal commands) # defaults to 'public' [Parameter( Position = 2 )] [ValidateSet('public', 'private')] [string]$Visibility = 'public', # Code to be added to the begin block of the function [Parameter( )] [string]$Begin, # Code to be added to the process block of the function [Parameter( )] [string]$Process, # Code to be added to the End block of the function [Parameter( )] [string]$End, # Optionally provide a component folder [Parameter( )] [string]$Component, # Overwrite an existing file [Parameter( )] [switch]$Force, # Return the path to the generated file [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $projectPaths = Get-ProjectPath if ($null -ne $projectPaths) { if (-not ([string]::IsNullorEmpty($projectPaths.Source))) { if ($PSBoundParameters.ContainsKey('Module')) { $modulePath = (Join-Path $projectPaths.Source $Module) } $filePath = (Join-Path -Path $modulePath -ChildPath $Visibility) if ($PSBoundParameters.ContainsKey('Component')) { $filePath = (Join-Path $filePath $Component) if (-not(Confirm-Path $filePath -ItemType Directory)) { throw "Could not create source directory $filePath" } } Write-Debug " - filePath is $filePath" $options = @{ Type = 'function' Name = $Name Destination = $filePath Data = @{ 'Name' = $Name } Force = $Force PassThru = $PassThru } if ($PSBoundParameters.ContainsKey('Begin')) { $options.Data['Begin'] = $Begin } if ($PSBoundParameters.ContainsKey('Process')) { $options.Data['Process'] = $Process } if ($PSBoundParameters.ContainsKey('End')) { $options.Data['End'] = $End } try { New-SourceItem @options } catch { $PSCmdlet.ThrowTerminatingError($_) } } else { throw 'Could not resolve Source directory' } } else { throw 'Could not get project path information' } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\New-FunctionItem.ps1' 123 #Region '.\public\SourceInfo\New-SourceComponent.ps1' -1 function New-SourceComponent { <# .SYNOPSIS Add a new Component folder to the module's source #> [CmdletBinding()] param( # The name of the component to add [Parameter( Position = 0 )] [string]$Name, # The name of the module to add the component to [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [string]$Module, # Only add the component to the public functions [Parameter( ParameterSetName = 'public' )] [switch]$PublicOnly, # Only add the component to the private functions [Parameter( ParameterSetName = 'private' )] [switch]$PrivateOnly ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $possibleSourceFolder = $PSCmdlet.GetVariableValue('Source') if ($null -eq $possibleSourceFolder) { $projectSourcePath = Get-ProjectPath | Select-Object -ExpandProperty Source Write-Debug "Project path value for Source: $projectSourcePath" } else { Write-Debug "Source path set from `$Source variable: $Source" $projectSourcePath = $possibleSourceFolder } $moduleDirectory = (Join-Path $projectSourcePath $Module) Write-Debug "Module directory is $moduleDirectory" if ($null -ne $moduleDirectory) { if (-not ($PublicOnly)) { $privateDirectory = (Join-Path $moduleDirectory 'private') if (Test-Path $privateDirectory) { $null = (Join-Path $privateDirectory $Name) | Confirm-Path -ItemType Directory } else { throw "Could not find $privateDirectory" } } if (-not ($PrivateOnly)) { $publicDirectory = (Join-Path $moduleDirectory 'public') if (Test-Path $publicDirectory) { $null = (Join-Path $publicDirectory $Name) | Confirm-Path -ItemType Directory } else { throw "Could not find $publicDirectory" } } } else { throw "Module source not found : $moduleDirectory" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\New-SourceComponent.ps1' 74 #Region '.\public\SourceInfo\New-SourceItem.ps1' -1 function New-SourceItem { <# .SYNOPSIS Create a new source item using templates .DESCRIPTION `New-SourceItem #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact = 'Low' )] param( # The type of file to create [Parameter( Position = 0 )] [string]$Type, # The file name [Parameter( Position = 1 )] [string]$Name, # The data to pass into the template binding [Parameter( Position = 2 )] [hashtable]$Data, # The directory to place the new file in [Parameter()] [string]$Destination, # Overwrite an existing file [Parameter( )] [switch]$Force, # Return the path to the generated file [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $template = Get-StitchTemplate -Type 'new' -Name $Type if ($null -ne $template) { if ($PSBoundParameters.ContainsKey('Name')) { $template.Name = $Name } if (-not ([string]::IsNullorEmpty($template.Extension))) { $template.Name = ( -join ($template.Name, $template.Extension)) } if ($PSBoundParameters.ContainsKey('Destination')) { $template.Destination = $Destination } if ($PSBoundParameters.ContainsKey('Data')) { Write-Debug "Processing template Data" if (-not ([string]::IsNullorEmpty($template.Data))) { Write-Debug " - Updating Data" $template.Data = ($template.Data | Update-Object $Data) } else { Write-Debug " - Setting Data" $template.Data = $Data } } Write-Debug "Invoking template" $template | Invoke-StitchTemplate -Force:$Force -PassThru:$PassThru } else { throw "Could not find a 'new' template for type: $Type" } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #close New-Sourceitem #EndRegion '.\public\SourceInfo\New-SourceItem.ps1' 89 #Region '.\public\SourceInfo\New-TestItem.ps1' -1 function New-TestItem { <# .SYNOPSIS Create a test item from a source item using the test template #> [CmdletBinding()] param( # The SourceItemInfo object to create the test from [Parameter( ValueFromPipeline )] [PSTypeName('Stitch.SourceItemInfo')] [Object[]]$SourceItem, # Overwrite an existing file [Parameter( )] [switch]$Force, # Return the path to the generated file [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $projectPaths = Get-ProjectPath if ($null -ne $projectPaths) { if (-not ([string]::IsNullorEmpty($projectPaths.Source))) { $relativePath = [System.IO.Path]::GetRelativePath(($projectPaths.Source), $SourceItem.Path) Write-Debug "Relative Source path is $relativePath" $filePath = $relativePath -replace [regex]::Escape($SourceItem.FileName) , '' Write-Debug " - filePath is $filePath" $testName = "$filePath$([System.IO.Path]::DirectorySeparatorChar)$($SourceItem.BaseName).Tests.ps1" Write-Debug "Setting template Name to $testName" $options = @{ Type = 'test' Name = $testName Data = @{ s = $SourceItem } Force = $Force PassThru = $PassThru } try { New-SourceItem @options } catch { $PSCmdlet.ThrowTerminatingError($_) } } else { throw 'Could not resolve Source directory' } } else { throw 'Could not get project path information' } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\New-TestItem.ps1' 62 #Region '.\public\SourceInfo\Rename-SourceItem.ps1' -1 function Rename-SourceItem { <# .SYNOPSIS Rename the file and the function, enum or class in the file #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # The New name of the function [Parameter( )] [string]$NewName, # Return the new file object [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $predicates = @{ function = { param($ast) $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] } class = { param($ast) (($ast -is [System.Management.Automation.Language.TypeDefinitionAst]) -and ($ast.Type -like 'Class')) } enum = { param($ast) (($ast -is [System.Management.Automation.Language.TypeDefinitionAst]) -and ($ast.Type -like 'Enum')) } } } process { :file foreach ($file in $Path) { if (Test-Path $file) { $fileItem = Get-Item $file try { $ast = [Parser]::ParseFile($fileItem.FullName, [ref]$null, [ref]$null) } catch { throw "Could not parse source item $($fileItem.FullName)`n$_" } # try to find the type of SourceInfo this is $typeWasFound = $false :type foreach ($type in $predicates.GetEnumerator()) { $innerAst = $ast.Find($type.Value, $false) if ($null -ne $innerAst) { $typeWasFound = $true $oldName = $innerAst.Name Write-Verbose "Found $($type.Name)" break type } } #! replace all occurances of the old name in the file $newExtent = $ast.Extent.Text -replace [regex]::Escape($oldName), $NewName try { $newExtent | Set-Content -Path $fileItem.FullName Write-Debug "Updating content in $($fileItem.Name)" } catch { throw "Could not write content to $($fileItem.FullName)`n$_" } $baseDirectory = $fileItem | Split-Path -Parent $originalExtension = $fileItem.Extension # pretty sure this will always be .ps1, but ... $NewPath = (Join-Path $baseDirectory "$NewName$originalExtension") try { Move-Item $fileItem.FullName -Destination $NewPath Write-Debug "Renaming file to $NewPath" } catch { throw "Could not rename $($fileItem.Name)" } if ($PassThru) { Get-Item $NewPath } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\SourceInfo\Rename-SourceItem.ps1' 99 #Region '.\public\Template\Get-StitchTemplate.ps1' -1 function Get-StitchTemplate { [CmdletBinding()] param( # The type of template to retrieve [Parameter( )] [string]$Type, # The name of the template to retrieve [Parameter( )] [string]$Name ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $templatePath = (Join-Path (Get-ModulePath) 'templates') } process { $templateTypes = @{} Get-ChildItem $templatePath -Directory | ForEach-Object { Write-Debug "Found template file '$($_.Name)' Adding as $" $templateTypes.Add($_.BaseName, $_.FullName) } foreach ($templateType in $templateTypes.GetEnumerator()) { $templates = Get-ChildItem $templateType.Value -Filter '*.eps1' -File foreach ($template in $templates) { $templateObject = [PSCustomObject]@{ PSTypeName = 'Stitch.TemplateInfo' Type = $templateType.Name Source = $template.FullName Destination = '' Name = $template.BaseName -replace '_', '.' Description = '' Data = @{} } $metaData = Get-StitchTemplateMetadata if ($null -ne $metaData) { $null = $templateObject | Update-Object -UpdateObject $metaData } #------------------------------------------------------------------------------- #region Set Target #! Making this a ScriptProperty means that when Destination or Name are updated #! this value will be updated to reflect if ([string]::IsNullorEmpty($templateObject.Target)) { $templateObject | Add-Member ScriptProperty -Name Target -Value { if ([string]::IsNullorEmpty($this.Destination)) { $this.Destination = (Get-Location) } (Join-Path ($ExecutionContext.InvokeCommand.ExpandString($this.Destination)) $this.Name) } } #endregion Set Name #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Set destination if ([string]::IsNullorEmpty($templateObject.Target)) { #TODO: I don't think this is right. We should not be using 'path' for anything if ($null -ne $templateObject.path) { $templateObject.Destination = "$($templateObject.path)/$($templateObject.Target)" } } #endregion Set destination #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Binding data if (-not ([string]::IsNullorEmpty($templateObject.bind))) { $pathOptions = @{ Path = (Split-Path $template.FullName -Parent) ChildPath = ($ExecutionContext.InvokeCommand.ExpandString($templateObject.bind)) } $possibleDataFile = (Join-Path @pathOptions) Write-Debug "Template has a bind parameter $possibleDataFile" if (Test-Path $possibleDataFile) { try { $templateData = Import-Psd $possibleDataFile -Unsafe $templateObject.Data = $templateObject.Data | Update-Object $templateData } catch { throw "An error occurred updating $($templateObject.Name) template data`n$_" } } } #endregion Binding data #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- #region Set display properties $defaultDisplaySet = 'Type', 'Name', 'Destination' $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$defaultDisplaySet) $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet) $templateObject | Add-Member MemberSet PSStandardMembers $PSStandardMembers #endregion Set display properties #------------------------------------------------------------------------------- #TODO: There is probably a better way to do this # if no parameters are set if ((-not ($PSBoundParameters.ContainsKey('Type'))) -and (-not ($PSBoundParameters.ContainsKey('Name')))) { $templateObject | Write-Output # if both are set and they match the object } elseif (($PSBoundParameters.ContainsKey('Type')) -and ($PSBoundParameters.ContainsKey('Name'))) { if (($templateObject.Type -like $Type) -and ($templateObject.Name -like $Name)) { $templateObject | Write-Output } # if Type is set and it matches the object } elseif ($PSBoundParameters.ContainsKey('Type')) { if ($templateObject.Type -like $Type) { $templateObject | Write-Output } # if Name is set and it matches the object } elseif ($PSBoundParameters.ContainsKey('Name')) { if ($templateObject.Name -like $Name) { $templateObject | Write-Output } } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Template\Get-StitchTemplate.ps1' 136 #Region '.\public\Tests\ConvertFrom-NUnit.ps1' -1 function ConvertFrom-NUnit { <# .SYNOPSIS Convert data in NUnit XML format into a test result object #> [CmdletBinding( DefaultParameterSetName = 'asXml' )] param( # Specifies a path to one or more locations. [Parameter( ParameterSetName = 'asFile', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # The xml content in NUnit format [Parameter( ParameterSetName = 'asXml', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [xml]$Xml ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSCmdlet.ParameterSetName -like 'asFile') { [xml]$Xml = Get-Content $Path } # -------------------------------------------------------------------------------- # #region Confirm format if ($null -eq $Xml.'test-results') { throw 'Content does not contain Test Results' } $testResults = $Xml.'test-results' $environment = $testResults.environment if ($null -eq $environment) { throw 'No environment information found in result' } #! Pester puts the entire result output into the 'test-results' node, #! then all of the tests are under the 'test-suite'.results #! finally, each file is added as a 'test-suite' nodes $resultsNode = $Xml.'test-results'.'test-suite'.results if ($null -eq $resultsNode) { throw 'No results node found in content' } $fileNodes = $resultsNode.SelectNodes('test-suite') if ($null -eq $fileNodes) { throw 'No test suites found within result' } # #endregion Confirm format # -------------------------------------------------------------------------------- # -------------------------------------------------------------------------------- # #region Environment info $runId = New-Guid $testRunDirectory = $environment.cwd $user = (@($environment.'user-domain' , $environment.user) -join '\') $machine = $environment.'machine-name' $TimeStamp = (Get-Date (@( $testResults.date, $testResults.time ) -join ' ')) # #endregion Environment info # -------------------------------------------------------------------------------- foreach ($fileNode in $fileNodes) { $fullPath = $fileNode.name $relativePath = [System.IO.Path]::GetRelativePath( $testRunDirectory, $fullPath) $fileResult = $fileNode.result $testCases = $fileNode.SelectNodes('//test-case') foreach ($testCase in $testCases) { $testInfo = @{ RunId = $runId Timestamp = $TimeStamp File = $relativePath Name = $testCase.description TestPath = $testCase.name Executed = $testCase.executed Result = $testCase.result Time = $testCase.time } [PSCustomObject]$testInfo } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Tests\ConvertFrom-NUnit.ps1' 110 #Region '.\public\Tests\ConvertFrom-PesterTestResult.ps1' -1 function ConvertFrom-PesterTestResult { <# .SYNOPSIS Convert a Pester Test Result object to a Stitch.TestResultInfo #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( ParameterSetName = 'asPath', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # CliXml content [Parameter( ParameterSetName = 'asXml', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [xml]$Xml, # The output of Invoke-Pester -PassThru [Parameter( ParameterSetName = 'asObject', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Pester.Run]$Results ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($PSBoundParameters.ContainsKey('Path')) { if ($Path | Test-Path) { $testResult = Import-Clixml $Path } else { throw "$Path is not a valid path" } } elseif ($PSBoundParameters.ContainsKey('Xml')) { try { $testResult = [System.Management.Automation.PSSerializer]::Deserialize( $Xml ) } catch { throw "Could not import XML content`n$_" } } elseif ($PSBoundParameters.ContainsKey('Results')) { $testResult = $Results } else { throw "No content was given to convert" } if ($null -eq $testResult) { throw 'No containers found in test result' } if ($null -eq $testResult.Containers) { throw 'No containers found in test result' } if ($null -eq $testResult.Tests) { throw 'No tests found in test result' } <#------------------------------------------------------------------ All checks completed, we should have a usable object now ------------------------------------------------------------------#> $files = $testResult.Containers foreach ($test in $testResult.Tests) { $currentFile = files | Where-Object { $_.Block -contains "[+] $($test.Path[0])" } if ($null -ne $currentFile) { if ($currentFile | Test-Path) { $checkpoint = Checkpoint-File $currentFile } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Tests\ConvertFrom-PesterTestResult.ps1' 94 #Region '.\public\Tests\Initialize-TestDatabase.ps1' -1 function Initialize-TestDatabase { <# .SYNOPSIS Create a new database for storing Pester test data #> [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Tests\Initialize-TestDatabase.ps1' 20 #Region '.\public\Tests\New-TestDataDirectory.ps1' -1 function New-TestDataDirectory { <# .SYNOPSIS Create a standard directory for test data #> [CmdletBinding()] param( # Specifies a path to one or more locations. [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # Return the new data directory [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { $directory = $Path | Split-Path $newName = $Path | Split-Path -LeafBase $newName = $newName -replace 'Tests$', 'Data' $dataDirectory = (Join-Path $directory $newName) if (-not($dataDirectory | Test-Path)) { $dir = mkdir $dataDirectory -Force if ($PassThru) { $dir } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Tests\New-TestDataDirectory.ps1' 43 #Region '.\public\Tests\Save-TestResult.ps1' -1 function Save-TestResult { <# .SYNOPSIS Save the Pester test results to the database #> [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { #TODO(Tests): Write the test info to the database } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\Tests\Save-TestResult.ps1' 21 #Region '.\public\VSCode\Get-CurrentEditorFile.ps1' -1 function Get-CurrentEditorFile { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { if ($null -ne $psEditor) { $psEditor.GetEditorContext().CurrentFile } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\VSCode\Get-CurrentEditorFile.ps1' 17 #Region '.\public\VSCode\Get-VSCodeSetting.ps1' -1 function Get-VSCodeSetting { [CmdletBinding()] param( # The name of the setting to return [Parameter( Position = 0 )] [string]$Name, # Treat the Name as a regular expression [Parameter( )] [switch]$Regex ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $settingsFile = "$env:APPDATA\Code\User\settings.json" } process { if (Test-Path $settingsFile) { Write-Debug "Loading the settings file" $settings = Get-Content $settingsFile | ConvertFrom-Json -Depth 16 -AsHashtable } if ($PSBoundParameters.ContainsKey('Name')) { if ($Regex) { Write-Debug "Looking for settings that match $Name" $matchedKeys = $settings.Keys | Where-Object { $_ -match $Name } } else { Write-Debug "Looking for settings that are like $Name" $matchedKeys = $settings.Keys | Where-Object { $_ -like $Name } } if ($matchedKeys.Count -gt 0) { Write-Debug "Found $($matchedKeys.Count) settings" $settingsSubSet = @{} foreach ($matchedKey in $matchedKeys) { $settingsSubSet[$matchedKey] = $settings[$matchedKey] } Write-Debug "Creating settings subset" $settings = $settingsSubSet } } $settings['PSTypeName'] = 'VSCode.SettingsInfo' [PSCustomObject]$settings | Write-Output } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #EndRegion '.\public\VSCode\Get-VSCodeSetting.ps1' 51 #Region '.\suffix.ps1' -1 Set-Alias -Name Import-BuildScript -Value "$PSScriptRoot\Import-BuildScript.ps1" Set-Alias -Name Import-TaskFile -Value "$PSScriptRoot\Import-TaskFile.ps1" #EndRegion '.\suffix.ps1' 3 |