stitch.psm1
#=============================================================================== #region prefix # Generated by stitch build system 2023-06-28 11:38:25 #endregion prefix #=============================================================================== using namespace Markdig using namespace Markdig.Syntax using namespace System.Management.Automation.Language using namespace System.Diagnostics.CodeAnalysis using namespace System.Collections.Specialized .Syntax #=============================================================================== #region enum #region source\stitch\enum\ModuleFlag.ps1 1 [Flags()] enum ModuleFlag { None = 0x00 HasManifest = 0x01 HasModule = 0x02 } #endregion source\stitch\enum\ModuleFlag.ps1 6 #endregion enum #=============================================================================== #=============================================================================== #region Private functions #region source\stitch\private\Changelog\Format-ChangelogEntry.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 ', ') } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Format-ChangelogEntry.ps1 79 #region source\stitch\private\Changelog\Format-ChangelogFooter.ps1 3 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #! There are no replacements in the footer yet $format = $formatOptions Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Format-ChangelogFooter.ps1 33 #region source\stitch\private\Changelog\Format-ChangelogGroup.ps1 2 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 "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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)" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "Format is '$format'" $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Format-ChangelogGroup.ps1 46 #region source\stitch\private\Changelog\Format-ChangelogHeader.ps1 3 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #! There are no replacements in the header yet $format = $formatOptions Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Format-ChangelogHeader.ps1 35 #region source\stitch\private\Changelog\Format-ChangelogRelease.ps1 2 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 "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $format Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Format-ChangelogRelease.ps1 70 #region source\stitch\private\Changelog\Format-HeadingText.ps1 2 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 source\stitch\private\Changelog\Format-HeadingText.ps1 52 #region source\stitch\private\Changelog\Resolve-ChangelogGroup.ps1 2 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 "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Changelog\Resolve-ChangelogGroup.ps1 77 #region source\stitch\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 source\stitch\private\FeatureFlags\Disable-FeatureFlag.ps1 37 #region source\stitch\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 source\stitch\private\FeatureFlags\Enable-FeatureFlag.ps1 37 #region source\stitch\private\InvokeBuild\Invoke-TaskNameCompletion.ps1 1 function Invoke-TaskNameCompletion { <# .SYNOPSIS A tab completion provider for task names #> param( [Parameter( Mandatory )] [ArgumentCompleter( { param( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) $possibleValues = Invoke-Build ? | Select-Object -ExpandProperty Name if ($fakeBoundParameters.ContainsKey('Name')) { $possibleValues | Where-Object { $_ -like "$wordToComplete*" } } else { $possibleValues | ForEach-Object { $_ } } })]$Name ) } #endregion source\stitch\private\InvokeBuild\Invoke-TaskNameCompletion.ps1 30 #region source\stitch\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 source\stitch\private\Markdown\Add-MarkdownElement.ps1 42 #region source\stitch\private\Markdown\Get-MarkdownDescendant.ps1 4 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 source\stitch\private\Markdown\Get-MarkdownDescendant.ps1 41 #region source\stitch\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 source\stitch\private\Markdown\Get-MarkdownElement.ps1 38 #region source\stitch\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 source\stitch\private\Markdown\Get-MarkdownFrontMatter.ps1 19 #region source\stitch\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 source\stitch\private\Markdown\Get-MarkdownHeading.ps1 21 #region source\stitch\private\Markdown\Import-Markdown.ps1 2 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 source\stitch\private\Markdown\Import-Markdown.ps1 52 #region source\stitch\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 source\stitch\private\Markdown\New-MarkdownElement.ps1 24 #region source\stitch\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 source\stitch\private\Markdown\Write-MarkdownDocument.ps1 22 #region source\stitch\private\SourceInfo\Get-SourceItemInfo.ps1 4 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)" $POWERSHELL_FILETYPES = @( '.ps1', '.psm1' ) $DATA_FILETYPES = @( '.psd1' ) try { if ($PSBoundParameters.ContainsKey('TypeMap')) { $map = Get-SourceTypeMap -Path $TypeMap } else { # try to load defaults $map = Get-SourceTypeMap } } catch { throw "Could not find map for source types`n$_" } } process { foreach ($p in $Path) { Write-Debug "Processing $p" $fileTypes = $map.FileTypes try { $fileItem = Get-Item $p -ErrorAction Stop } catch { throw "Could not read $p`n$_" } if ($fileTypes.Keys -contains $fileItem.Extension) { $fileType = $fileTypes[$fileItem.Extension] Write-Debug " - $($file.Name) is a $fileType item" } else { Write-Verbose "Not adding $($fileItem.Name) because it is a $($fileItem.Extension) file" continue } $sourceObject = @{ PSTypeName = 'Stitch.SourceItemInfo' Path = $fileItem.FullName BaseName = $fileItem.BaseName FileName = $fileItem.Name Name = $fileItem.BaseName FileType = $fileType Ast = '' Directory = '' Module = '' Type = '' Component = '' Visibility = '' Verb = '' Noun = '' } if ($POWERSHELL_FILETYPES -contains $fileItem.Extension) { try { Write-Debug " - Parsing powershell" $ast = [Parser]::ParseFile($fileItem.FullName, [ref]$null, [ref]$null) if ($null -ne $ast) { $sourceObject.Ast = $ast } } catch { Write-Warning "Could not parse source item $($fileItem.FullName)`n$_" } } 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$_" } } } } if ([string]::IsNullorEmpty($Root)) { Write-Debug " - No Root path given. Attempting to resolve from project root" $projectRoot = Resolve-ProjectRoot Write-Debug " - 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-Debug " - Setting Root to $Root" } if ([string]::IsNullorEmpty($Root)) { throw "Could not determine the Root directory for SourceItems" } 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) ) Write-Debug "Matching Path settings in sourcetypes" $levels = $map.Path :level foreach ($level in $levels) { $pathItemIndex = $levels.IndexOf($level) if ($pathItemsIndex -ge $pathItems.Count) { Write-Debug " - Index is $pathItemsIndex. No more path items" break level } $pathField = $pathItems[$pathItemIndex] 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 ($pattern in $level.Keys) { Write-Debug " - testing if $pathField matches $pattern" if ($pathField -match $pattern) { foreach ($field in ($level[$pattern]).Keys) { $sourceObject[$field] = $level[$pattern][$field] } } } continue level } } Write-Debug "Matching Name settings in sourcetypes" foreach ($namePattern in $map.Name.Keys) { if ($fileItem.Name -match $namePattern) { Write-Debug " - $($fileItem.Name) matches $namePattern" $nameMaps = $map.Name[$namePattern] $nameMatches = $Matches foreach ($nameMap in $nameMaps.GetEnumerator()) { Write-Debug " - Name map $($nameMap.Name) => $($nameMap.Value)" if ($nameMap.Value -match '^Matches\.(\d+)') { $matchNumber = [int]$Matches.1 Write-Debug " - Match number: $($nameMap.Name) => $($nameMatches[$matchNumber])" $sourceObject[$nameMap.Name] = $nameMatches[$matchNumber] } elseif ($nameMap.Value -match '^Matches\.(\w+)') { $matchWord = $Matches.1 Write-Debug " - Match word: $($nameMap.Name) => $($nameMatches[$matchWord])" $sourceObject[$nameMap.Name] = $nameMatches[$matchWord] } else { Write-Debug " - $($nameMap.Name) => $($nameMap.Value)" $sourceObject[$nameMap.Name] = $nameMap.Value } } } } #! special case: Manifest file if ($fileItem.Extension -like '.psd1') { if ($sourceObject.Data.ContainsKey('GUID')) { $sourceObject['FileType'] = 'PowerShell Module Manifest' $sourceObject['Type'] = 'manifest' $sourceObject['Visibility'] = 'public' } } [PSCustomObject]$sourceObject | Write-Output } # end foreach } # end process block end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\SourceInfo\Get-SourceItemInfo.ps1 205 #region source\stitch\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, # Optionally run the testes [Parameter( )] [switch]$RunTest ) 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 #TODO: Are there other extensions we should look for ? 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 Write-Debug " Creating item $($fileItem.BaseName) from $($fileItem.FullName)" Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $pesterConfig = New-PesterConfiguration $pesterConfig.Run.PassThru = $true $pesterConfig.Run.SkipRun = (-not ($RunTest)) try { $pesterContainer = New-PesterContainer -Path:$p $pesterConfig.Run.Container = $pesterContainer $testResult = Invoke-Pester -Configuration $pesterConfig Write-Debug "Root is $Root" } catch { throw "Could not load test item $Path`n$_ " } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $testResult Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\SourceInfo\Get-TestItemInfo.ps1 68 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Template\Get-StitchTemplateMetadata.ps1 30 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\private\Template\Invoke-StitchTemplate.ps1 181 #endregion Private functions #=============================================================================== #=============================================================================== #region Public functions #region source\stitch\public\Changelog\Add-ChangelogEntry.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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++ } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $doc | Write-MarkdownDocument | Out-File $Path } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Changelog\Add-ChangelogEntry.ps1 119 #region source\stitch\public\Changelog\ConvertFrom-Changelog.ps1 2 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 source\stitch\public\Changelog\ConvertFrom-Changelog.ps1 168 #region source\stitch\public\Changelog\ConvertTo-Changelog.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Changelog\ConvertTo-Changelog.ps1 52 #region source\stitch\public\Changelog\Export-ReleaseNotes.ps1 5 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Changelog\Export-ReleaseNotes.ps1 97 #region source\stitch\public\Changelog\Get-ChangelogConfig.ps1 2 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 source\stitch\public\Changelog\Get-ChangelogConfig.ps1 52 #region source\stitch\public\Changelog\Set-ChangelogRelease.ps1 2 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 source\stitch\public\Changelog\Set-ChangelogRelease.ps1 119 #region source\stitch\public\Configuration\Convert-ConfigurationFile.ps1 2 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 "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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)" } } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Configuration\Convert-ConfigurationFile.ps1 56 #region source\stitch\public\Configuration\Get-BuildConfiguration.ps1 3 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 Source = $Source Tests = $Tests Staging = $Staging Artifact = $Artifact Docs = $Docs } $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 source\stitch\public\Configuration\Get-BuildConfiguration.ps1 284 #region source\stitch\public\Configuration\Get-TaskConfiguration.ps1 3 function Get-TaskConfiguration { [CmdletBinding()] param( # The task object [Parameter( Position = 1, ValueFromPipeline )] [psobject]$Task, [Parameter( Position = 0 )] [string]$TaskConfigPath ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (-not($PSBoundParameters.ContainsKey('TaskConfigPath'))) { Write-Debug "No TaskConfigPath given. Looking for BuildConfigPath" $possibleBuildConfigPath = $PSCmdlet.GetVariableValue('BuildConfigPath') if (-not ([string]::IsNullorEmpty($possibleBuildConfigPath))) { Write-Debug "found BuildConfigPath at $possibleBuildConfigPath" $BuildConfigPath = $possibleBuildConfigPath $TaskConfigPath = (Join-Path -Path $BuildConfigPath -ChildPath 'config' -AdditionalChildPath 'tasks') Remove-Variable possibleBuildConfigPath -ErrorAction SilentlyContinue } } } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if ($null -eq $TaskConfigPath) { throw "Could not find $($Task.Name) configuration because TaskConfigPath was not set" } if (Test-Path $TaskConfigPath) { Write-Debug "Looking for task config files in $TaskConfigPath" $taskConfigFiles = Get-ChildItem -Path $TaskConfigPath -Filter "*.config.psd1" Write-Debug " - Found $($taskConfigFiles.Count) config files" foreach ($taskConfigFile in $taskConfigFiles) { if ((-not ($PSBoundParameters.ContainsKey('Task'))) -or ($TaskConfigFile.BaseName -like "$($Task.Name).config")) { try { $config = Import-Psd -Path $taskConfigFile -Unsafe $config['TaskName'] = ($TaskConfigFile.BaseName -replace '\.config$', '') } catch { throw "THere was an error loading $taskConfigFile `n$_" } $config | Write-Output } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Configuration\Get-TaskConfiguration.ps1 60 #region source\stitch\public\Configuration\Merge-BuildConfiguration.ps1 1 function Merge-BuildConfiguration { [CmdletBinding()] param( # Specifies a path to one or more configuration files [Parameter( Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path, # 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 ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" foreach ($file in $Path) { $options = Convert-ConfigurationFile $Path if ($null -ne $options) { $Object.Value | Update-Object -UpdateObject $options } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Configuration\Merge-BuildConfiguration.ps1 42 #region source\stitch\public\Configuration\Select-BuildRunBook.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (-not ($PSBoundParameters.ContainsKey('Path'))) { if (-not ([string]::IsNullorEmpty($PSCmdlet.GetVariableValue('ProfileRoot')))) { $Path = $PSCmdlet.GetVariableValue('ProfileRoot') } else { $Path = (Get-Location) } } 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 } } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Configuration\Select-BuildRunBook.ps1 92 #region source\stitch\public\Content\Convert-LineEnding.ps1 2 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 source\stitch\public\Content\Convert-LineEnding.ps1 65 #region source\stitch\public\Content\Find-ParseToken.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Content\Find-ParseToken.ps1 60 #region source\stitch\public\Content\Format-File.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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($_) } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Content\Format-File.ps1 58 #region source\stitch\public\Content\Get-ParseToken.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Content\Get-ParseToken.ps1 50 #region source\stitch\public\Content\Invoke-ReplaceToken.ps1 2 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 source\stitch\public\Content\Invoke-ReplaceToken.ps1 87 #region source\stitch\public\Content\Measure-File.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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($_) } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Content\Measure-File.ps1 61 #region source\stitch\public\Content\Merge-SourceItem.ps1 2 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) found" $added = @() foreach ($statement in $statements) { $s = $statement.Extent.Text if ($added -contains $s) { Write-Debug " - '$s' 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 source\stitch\public\Content\Merge-SourceItem.ps1 235 #region source\stitch\public\Content\Test-WindowsLineEnding.ps1 2 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 source\stitch\public\Content\Test-WindowsLineEnding.ps1 34 #region source\stitch\public\FeatureFlags\Get-FeatureFlag.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\FeatureFlags\Get-FeatureFlag.ps1 64 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $flag = Get-FeatureFlag -Name $Name if ([string]::IsNullorEmpty($flag)) { $false } else { $flag.Enabled } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\FeatureFlags\Test-FeatureFlag.ps1 34 #region source\stitch\public\Git\Add-GitFile.ps1 2 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 source\stitch\public\Git\Add-GitFile.ps1 65 #region source\stitch\public\Git\Checkpoint-GitWorkingDirectory.ps1 2 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-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Git\Checkpoint-GitWorkingDirectory.ps1 41 #region source\stitch\public\Git\Clear-MergedGitBranch.ps1 2 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 source\stitch\public\Git\Clear-MergedGitBranch.ps1 41 #region source\stitch\public\Git\ConvertFrom-ConventionalCommit.ps1 2 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 source\stitch\public\Git\ConvertFrom-ConventionalCommit.ps1 205 #region source\stitch\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 source\stitch\public\Git\Get-GitFile.ps1 37 #region source\stitch\public\Git\Get-GitHistory.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 #------------------------------------------------------------------------------- } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $releases | Write-Output Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Git\Get-GitHistory.ps1 101 #region source\stitch\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 source\stitch\public\Git\Get-GitHubDefaultBranch.ps1 19 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Git\Get-GitMergedBranch.ps1 33 #region source\stitch\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 source\stitch\public\Git\Get-GitModifiedFile.ps1 17 #region source\stitch\public\Git\Get-GitRemoteTrackingBranch.ps1 1 function Get-GitRemoteTrackingBranch { Get-GitBranch | Select-Object -ExpandProperty TrackedBranch } #endregion source\stitch\public\Git\Get-GitRemoteTrackingBranch.ps1 3 #region source\stitch\public\Git\Get-GitStagedFile.ps1 2 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 source\stitch\public\Git\Get-GitStagedFile.ps1 18 #region source\stitch\public\Git\Get-GitUntrackedFile.ps1 2 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 source\stitch\public\Git\Get-GitUntrackedFile.ps1 18 #region source\stitch\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 source\stitch\public\Git\Join-PullRequest.ps1 104 #region source\stitch\public\Git\Start-GitBranch.ps1 2 function Start-GitBranch { param( [string]$Name ) New-GitBranch $Name | Set-GitHead } #endregion source\stitch\public\Git\Start-GitBranch.ps1 7 #region source\stitch\public\Git\Sync-GitRepository.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Git\Sync-GitRepository.ps1 33 #region source\stitch\public\Git\Undo-GitCommit.ps1 2 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 source\stitch\public\Git\Undo-GitCommit.ps1 30 #region source\stitch\public\Git\Update-GitRepository.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $br = Get-GitBranch -Current if ($br.IsTracking) { $remote = $br.TrackedBranch if ($PSCmdlet.ShouldProcess($br.FriendlyName, "Update")) { Start-GitRebase -Upstream $remote.FriendlyName -Branch $br.FriendlyName } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Git\Update-GitRepository.ps1 32 #region source\stitch\public\InvokeBuild\Get-BuildTask.ps1 2 function Get-BuildTask { [CmdletBinding()] param( # The name of the task to get [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [ArgumentCompleter({ Invoke-TaskNameCompletion @args })] [string]$Name ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" function Add-TaskProperty { param( [ref]$TaskRef ) #! if the task was written as 'phase <name>' then the InvocationName #! can be used to find it. Add a property 'IsPhase' for easier sorting $TaskRef.Value | Add-Member -NotePropertyName Synopsis -NotePropertyValue ( (Get-BuildSynopsis ${*}.All[$TaskRef.Value.Name] -ErrorAction SilentlyContinue) ?? '' ) $TaskRef.Value | Add-Member -NotePropertyName IsPhase -NotePropertyValue ( ( $TaskRef.Value.InvocationInfo.InvocationName -like 'phase' ) ? $true : $false ) $TaskRef.Value | Add-Member -NotePropertyName Path -NotePropertyValue ( Get-Item $TaskRef.Value.InvocationInfo.ScriptName ) $TaskRef.Value | Add-Member -NotePropertyName Line -NotePropertyValue $TaskRef.Value.InvocationInfo.ScriptLineNumber $TaskRef.Value.PSObject.TypeNames.Insert(0, 'InvokeBuild.TaskInfo') } } process { if (Test-InInvokeBuild) { $taskData = ${*}.AllTasks } else { $taskData = Invoke-Build ?? } if ($null -ne $taskData) { if ($PSBoundParameters.ContainsKey('Name')) { $task = $taskData[$Name] if (-not ([string]::IsNullorEmpty($task))) { Add-TaskProperty ([ref]$task) $task | Write-Output } else { throw "There is no task named $Name in this project" } } else { foreach ($key in $taskData.Keys) { $task = $taskData[$key] Add-TaskProperty ([ref]$task) $task | Write-Output } } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\InvokeBuild\Get-BuildTask.ps1 64 #region source\stitch\public\InvokeBuild\Get-TaskHelp.ps1 1 function Get-TaskHelp { <# .SYNOPSIS Retrieve the comment based help for the given task #> [CmdletBinding()] param( # The name of the task to get the help documentation for [Parameter( 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if ($PSBoundParameters.ContainsKey('InvocationInfo')) { Get-Help $InvocationInfo.ScriptName -Full } else { $task = Get-BuildTask -Name:$Name if ($null -ne $task) { Get-Help $task.InvocationInfo.ScriptName -Full } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\InvokeBuild\Get-TaskHelp.ps1 40 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\InvokeBuild\Test-InInvokeBuild.ps1 27 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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" } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Manifest\ConvertFrom-CommentedProperty.ps1 61 #region source\stitch\public\Manifest\Get-ModuleExtension.ps1 2 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 source\stitch\public\Manifest\Get-ModuleExtension.ps1 85 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Manifest\Test-CommentedProperty.ps1 42 #region source\stitch\public\Manifest\Update-ManifestField.ps1 2 function Update-ManifestField { [CmdletBinding()] 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 ($manifestObject.ContainsKey($PropertyName)) { #------------------------------------------------------------------------------- #region Field exists Write-Debug " - Manifest has a $PropertyName field. Updating" try { 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 { $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 { Update-ModuleManifest @options } catch { throw "Cannot update $PropertyName in $Path`n$_" } #endregion Field missing #------------------------------------------------------------------------------- } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Manifest\Update-ManifestField.ps1 105 #region source\stitch\public\Notification\Invoke-BuildNotification.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Notification\Invoke-BuildNotification.ps1 77 #region source\stitch\public\Path\Confirm-Path.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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$_" } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Confirm-Path.ps1 60 #region source\stitch\public\Path\Find-BuildConfigurationRootDirectory.ps1 2 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 #> [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' ) $buildConfigRoot = $null } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (-not($PSBoundParameters.ContainsKey('Path'))) { $Path = Get-Location } :path foreach ($possibleRootPath in $Path) { :root foreach ($possibleRoot in $possibleRoots) { $possiblePath = (Join-Path $possibleRootPath $possibleRoot) if (Test-Path $possiblePath) { $possiblePathItem = (Get-Item $possiblePath) if ($possiblePathItem.PSIsContainer) { $buildConfigRoot = $possiblePathItem } else { $buildConfigRoot = (Get-Item ($possiblePathItem | Split-Path -Parent)) } Write-Debug "Found build configuration root directory 'buildConfigRoot'" break path } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $buildConfigRoot Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-BuildConfigurationRootDirectory.ps1 61 #region source\stitch\public\Path\Find-BuildProfileRootDirectory.ps1 2 function Find-BuildProfileRootDirectory { <# .SYNOPSIS Find the directory that has the profiles defined #> [CmdletBinding()] param( # Specifies a path to a location that contains Build Profiles (This should be BuildConfigPath) [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string[]]$Path ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $possibleProfileDirectories = @( 'profiles', 'profile', 'runbooks' ) $profileDirectory = $null } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (-not($PSBoundParameters.ContainsKey('Path'))) { $possibleBuildConfigRoot += $PSCmdlet.GetVariableValue('BuildConfigRoot') if ([string]::IsNullorEmpty($possibleBuildConfigRoot)) { $Path += Get-Location } else { $Path += $possibleBuildConfigRoot } } :root foreach ($possibleRootPath in $Path) { :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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $profileDirectory Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-BuildProfileRootDirectory.ps1 59 #region source\stitch\public\Path\Find-BuildRunBook.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" :path foreach ($location in $Path) { :filter foreach ($possibleRunbookFilter in $possibleRunbookFilters) { $options = @{ Path = $location Recurse = $true Filter = $possibleRunbookFilter File = $true } Get-Childitem @options } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-BuildRunBook.ps1 38 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" foreach ($location in $Path) { if (Test-Path $location) { $options = @{ Path = $location Recurse = $true Filter = $buildScriptPattern } Get-ChildItem @options } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-InvokeBuildScript.ps1 40 #region source\stitch\public\Path\Find-InvokeBuildTaskFile.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" foreach ($location in $Path) { if (Test-Path $location) { $options = @{ Path = $location Recurse = $true Filter = $taskFilePattern } Get-ChildItem @options } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-InvokeBuildTaskFile.ps1 42 #region source\stitch\public\Path\Find-LocalUserStitchDirectory.ps1 2 function Find-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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $userStitchDirectory Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-LocalUserStitchDirectory.ps1 60 #region source\stitch\public\Path\Find-StitchConfigurationFile.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" :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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Find-StitchConfigurationFile.ps1 44 #region source\stitch\public\Path\Get-ModulePath.ps1 2 function Get-ModulePath { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $callStack = Get-PSCallStack $caller = $callStack[1] $caller.InvocationInfo.MyCommand.Module.ModuleBase Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Get-ModulePath.ps1 21 #region source\stitch\public\Path\Resolve-ProjectRoot.ps1 2 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, # Powershell Data File with defaults [Parameter( )] [string]$Defaults, # Default Source directory [Parameter( )] [string]$Source = '.\source', # Default Tests directory [Parameter( )] [string]$Tests = '.\tests', # Default Staging directory [Parameter( )] [string]$Staging = '.\stage', # Default Artifact directory [Parameter( )] [string]$Artifact = '.\out', # Default Docs directory [Parameter( )] [string]$Docs = '.\docs' ) 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 } #TODO: Here we could look for .build.ps1 as the root, or perhaps tie it to a repository by looking for .git/ :location do { if ($null -ne $currentLocation) { $null = $PSBoundParameters.Remove('Path') if (Test-ProjectRoot @PSBoundParameters -Path $currentLocation.FullName) { $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 " Level: $level - $($currentLocation.Name) is not the project root" } } else { $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 source\stitch\public\Path\Resolve-ProjectRoot.ps1 106 #region source\stitch\public\Path\Test-PathIsIn.ps1 2 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 source\stitch\public\Path\Test-PathIsIn.ps1 107 #region source\stitch\public\Path\Test-ProjectRoot.ps1 2 function Test-ProjectRoot { <# .SYNOPSIS Test if the given directory is the root directory of a project .DESCRIPTION `Test-ProjectRoot` looks for "typical" project directories in the given -Path and returns true if at least two of them exist. Typical project directories are: - A source directory (this may be controlled by the variable $Source) - A staging directory (the variable $Staging) - A tests directory (the variable $Tests) - A artifact/output directory (the variable $Artifact) - A documentation directory (the variable $Docs) .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(), # Powershell Data File with defaults [Parameter( )] [string]$Defaults, # Default Source directory [Parameter( )] [string]$Source = '.\source', # Default Tests directory [Parameter( )] [string]$Tests = '.\tests', # Default Staging directory [Parameter( )] [string]$Staging = '.\stage', # Default Artifact directory [Parameter( )] [string]$Artifact = '.\out', # Default Docs directory [Parameter( )] [string]$Docs = '.\docs' ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #! How many default directories must be present to be considered true $DEFAULTS_REQUIRED = 2 $FAILSAFE_DEFAULTS = @{ Source = $Source Tests = $Tests Staging = $Staging Artifact = $Artifact Docs = $Docs } if ($PSBoundParameters.ContainsKey('Defaults')) { if (Test-Path $Defaults) { Write-Debug "Importing defaults from $Defaults" $defaultFolders = Import-PowerShellDataFile $Defaults } } else { Write-Debug 'No defaults file found using internal defaults' $defaultFolders = $FAILSAFE_DEFAULTS } Write-Debug "Default Folders are:" foreach ($key in $defaultFolders.Keys) { Write-Debug (" - {0,-16} => {1}" -f $key, $defaultFolders[$key]) } } process { Write-Debug "Testing against default project directories in $Path" $defaultsInDirectory = 0 foreach ($key in $defaultFolders.Keys) { Write-Debug "Checking for $key variable. Defaults found so far $defaultsInDirectory" $pathVariable = Get-Variable $key -ValueOnly -ErrorAction SilentlyContinue Write-Debug " - The path we are looking for is $pathVariable" if ($null -ne $pathVariable) { if ([system.io.path]::IsPathFullyQualified($pathVariable)) { Write-Debug " - found $pathVariable fully qualified" $pathToTest = $pathVariable } else { $pathToTest = (Join-Path $Path $pathVariable) } } else { throw "No value given for `$$key" } Write-Debug "Testing if $pathToTest is present" if (Test-Path $pathToTest) { Write-Debug ' - It was found' $defaultsInDirectory += 1 } else { Write-Debug ' - It was NOT found' } } } end { Write-Debug "$defaultsInDirectory found $DEFAULTS_REQUIRED needed to pass" $defaultsInDirectory -ge $DEFAULTS_REQUIRED | Write-Output Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Path\Test-ProjectRoot.ps1 139 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\Get-ProjectPath.ps1 53 #region source\stitch\public\Project\Get-ProjectVersionInfo.ps1 1 function Get-ProjectVersionInfo { [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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if (-not($PSBoundParameters.ContainsKey('Path'))) { $Path = Get-Location } #TODO: We could also parse the version field from the root module's manifest Write-Debug 'Checking for version information' Write-Debug ' - Checking for gitversion utility' $gitverCmd = Get-Command dotnet-gitversion.exe -ErrorAction SilentlyContinue if ($null -eq $gitverCmd) { Write-Information "GitVersion is not installed.`nsee <https://gitversion.net/docs/usage/cli/installation> for details" Write-Debug ' - gitversion not found' 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" } } } else { Write-Debug " - No version files found in $Path" $buildInfo = $PSCmdlet.GetVariableValue('BuildInfo') if ($null -ne $buildInfo) { switch ($buildInfo.Modules.Keys.Count) { 0 { throw "Could not find any modules in project to get version info" } 1 { $buildInfo.Modules[0].ModuleVersion | Write-Output } default { Write-Verbose "Multiple module versions found using highest version" $buildInfo.Modules.ModuleVersion | Sort-Object -Descending | Select-Object -First 1 | Write-Output } } } } } else { 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 { $versionInfo = $gitVersionOutput | ConvertFrom-Json } catch { throw "Could not parse json:`n$gitVersionOutput`n$_" } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $versionInfo Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\Get-ProjectVersionInfo.ps1 88 #region source\stitch\public\Project\Initialize-StitchProject.ps1 4 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 source\stitch\public\Project\Initialize-StitchProject.ps1 116 #region source\stitch\public\Project\New-StitchBuildProfile.ps1 4 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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? } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchBuildProfile.ps1 65 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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' } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchBuildRunner.ps1 67 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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' } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchConfigurationFile.ps1 68 #region source\stitch\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 "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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$_" } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchConfigurationPath.ps1 44 #region source\stitch\public\Project\New-StitchPathConfigurationFile.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchPathConfigurationFile.ps1 101 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\New-StitchRunBook.ps1 66 #region source\stitch\public\Project\Resolve-ProjectName.ps1 2 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 source\stitch\public\Project\Resolve-ProjectName.ps1 23 #region source\stitch\public\Project\Test-ProjectPath.ps1 2 function Test-ProjectPath { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\Test-ProjectPath.ps1 18 #region source\stitch\public\Project\Write-StitchLogo.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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] } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { $logoOutput Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Project\Write-StitchLogo.ps1 74 #region source\stitch\public\SourceInfo\Find-TodoItem.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" #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 } #> Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\Find-TodoItem.ps1 47 #region source\stitch\public\SourceInfo\Get-ModuleItem.ps1 2 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( 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)" } process { 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" } } $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 source\stitch\public\SourceInfo\Get-ModuleItem.ps1 110 #region source\stitch\public\SourceInfo\Get-SourceItem.ps1 2 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" $Path = (Join-Path (Resolve-ProjectRoot) 'source') Write-Debug " $Path" } foreach ($p in $Path) { try { $item = Get-Item $p -ErrorAction Stop if ($item.PSIsContainer) { Get-ChildItem $item.FullName -Recurse:$Recurse | Get-sourceItemInfo -Root $item.FullName | Write-Output continue } else { if ($item.Extension -eq '.ps1') { $item | Get-SourceItemInfo | 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 source\stitch\public\SourceInfo\Get-SourceItem.ps1 54 #region source\stitch\public\SourceInfo\Get-SourceTypeMap.ps1 2 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)" $moduleMapName = 'script:_stitchSourceTypeMap' } process { <# ! If we already loaded the map, then return that one TODO: I need a better way to manage the items that depend on the variables set during Invoke-Build when not in Invoke-Build #> $map = (Get-Variable -Name $moduleMapName -ValueOnly -ErrorAction SilentlyContinue) if ($null -ne $map) { Write-Debug "found map in $moduleMapName" $map | Write-Output } else { Write-Debug "Source type map not set. Creating now." if ($PSBoundParameters.ContainsKey('Path')) { New-SourceTypeMap -PassThru -Path $Path | Write-Output } else { New-SourceTypeMap -PassThru | Write-Output } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\Get-SourceTypeMap.ps1 46 #region source\stitch\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-TesItemtInfo | Write-Output } catch { Write-Warning "$_" } continue } continue } } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\Get-TestItem.ps1 73 #region source\stitch\public\SourceInfo\New-FunctionItem.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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' } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\New-FunctionItem.ps1 124 #region source\stitch\public\SourceInfo\New-SourceComponent.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\New-SourceComponent.ps1 75 #region source\stitch\public\SourceInfo\New-SourceItem.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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" } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\New-SourceItem.ps1 90 #region source\stitch\public\SourceInfo\New-SourceTypeMap.ps1 2 function New-SourceTypeMap { <# .SYNOPSIS Create a new mapping table of source items to their Visibility and Type .DESCRIPTION The source type map is used by `Get-SourceItemInfo` to set the Visibility and Type attribute on items found in the module source directories. `Visibility` and `Type` is used to determine if the item should be included in the module manifest `Export` keys (for example, if `Type` is `function` and `Visibility` is `public`, the item is listed in `FunctionsToExport`, etc.) `New-SourceTypeMap` looks in the following locations for data to populate the map: - The file sourcetypes.config.psd1 in the profile path, the build config path, or this module - The `SourceTypeMap` variable - The -TypeMap parameter Each, if found will update the existing type map #> [CmdletBinding()] param( # Specifies a path to a SourceTypeMap file [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('PSPath')] [string]$Path, # Hash table of mappings to create the new map from [Parameter( )] [hashtable]$TypeMap, # Send the object out to the pipeline in addition to setting the script scope variable [Parameter( )] [switch]$PassThru ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" # The name of the Module variable to store the map in once created $moduleMapName = 'script:_stitchSourceTypeMap' # The variable to look for in the current environment for a map $defaultMapVar = 'SourceTypeMap' # Used internally to the function to create the map $internalMap = @{} #------------------------------------------------------------------------------- #region file path if ($PSBoundParameters.ContainsKey('Path')) { if (Test-Path $Path) { if ((Get-Item $Path).PSIsContainer) { $defaultMapFile = (Join-Path $Path 'sourcetypes.config.psd1') } else { $defaultMapFile = $Path } } } elseif ($null -ne $BuildConfigPath) { #! always prefer the Profile's configuration $defaultMapFile = (Join-Path $BuildConfigPath 'sourcetypes.config.psd1') } elseif ($null -ne $BuildConfigRoot) { #! Then the build config directory (.build ?) $defaultMapFile = (Join-Path $BuildConfigRoot 'sourcetypes.config.psd1') } else { #! If those aren't found fall back to the modules internal config $defaultMapFile = (join-Path (Get-Item $MyInvocation.MyCommand.Module.Path).Directory 'sourcetypes.config.psd1') } #endregion file path #------------------------------------------------------------------------------- } process { # Load the file Write-Debug "Looking for source type map file '$defaultMapFile'" if (Test-Path $defaultMapFile) { Write-Debug "Updating source type map using '$defaultMapFile'" $null = $internalMap = $internalMap | Update-Object (Import-Psd $defaultMapFile) } # Load the variable Write-Debug "Looking for source type map variable '$defaultMapVar'" $map = (Get-Variable -Name $defaultMapVar -ValueOnly -ErrorAction SilentlyContinue) if ($null -ne $map) { Write-Debug "Updating source type map using `$$defaultMapVar" $null = $internalMap = $internalMap | Update-Object $map } # Load the Parameter Write-Debug "Looking for source type map Parameter 'TypeMap'" if ($PSBoundParameters.ContainsKey('TypeMap')) { Write-Debug "Updating source type map using -TypeMap parameter" $null = $internalMap = $internalMap | Update-Object $TypeMap } Write-Debug "Type map created. Updating `$SourceTypeMap table" Set-Variable -Name $moduleMapName -Value $internalMap -Scope 'Script' if ($PassThru) { $internalMap | Write-Output } } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\New-SourceTypeMap.ps1 104 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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' } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\New-TestItem.ps1 63 #region source\stitch\public\SourceInfo\Rename-SourceItem.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" :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 } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\SourceInfo\Rename-SourceItem.ps1 100 #region source\stitch\public\Template\Get-StitchTemplate.ps1 2 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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" $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 } } } } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\Template\Get-StitchTemplate.ps1 137 #region source\stitch\public\VSCode\Get-CurrentEditorFile.ps1 1 function Get-CurrentEditorFile { [CmdletBinding()] param( ) begin { Write-Debug "`n$('-' * 80)`n-- Begin $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } process { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" if ($null -ne $psEditor) { $psEditor.GetEditorContext().CurrentFile } Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\VSCode\Get-CurrentEditorFile.ps1 18 #region source\stitch\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 { Write-Debug "`n$('-' * 80)`n-- Process start $($MyInvocation.MyCommand.Name)`n$('-' * 80)" 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 Write-Debug "`n$('-' * 80)`n-- Process end $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } end { Write-Debug "`n$('-' * 80)`n-- End $($MyInvocation.MyCommand.Name)`n$('-' * 80)" } } #endregion source\stitch\public\VSCode\Get-VSCodeSetting.ps1 53 #endregion Public functions #=============================================================================== #=============================================================================== #region suffix Set-Alias -Name Import-BuildScript -Value "$PSScriptRoot\Import-BuildScript.ps1" Set-Alias -Name Import-TaskFile -Value "$PSScriptRoot\Import-TaskFile.ps1" #endregion suffix #=============================================================================== |