diff --git a/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.build.ps1 b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.build.ps1 new file mode 100644 index 0000000..a82bbd3 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.build.ps1 @@ -0,0 +1,275 @@ +using namespace System.Management.Automation +using namespace System.Management.Automation.Language +using namespace Microsoft.PowerShell.Commands + +<# + .DESCRIPTION + Assumes the following layout: + + ├── LICENSE + ├── + ├── Module.psd1 + ├── Classes + │ └── *.ps1 + ├── Private + │ └── *.ps1 + ├── Public + │ └── *.ps1 + └── Tests + └── *.Tests.ps1 +#> + +[CmdletBinding()] +param +( + [version]$NewVersion, + + [string]$PSGalleryApiKey = $env:PSGalleryApiKey, + + [string]$ModuleName = $MyInvocation.MyCommand.Name -replace '\.build\.ps1$', + + [string]$ManifestPath = "$ModuleName.psd1", + + [string[]]$Include = ('*.ps1xml', '*.psrc', 'README*', 'LICENSE*'), + + [string[]]$PSScriptFolders = ('Classes', 'Private', 'Public'), + + [string]$OutputFolder = 'Build' +) + +$RequiredModules = [ModuleSpecification[]]( + @{ModuleName = 'Pester'; ModuleVersion = '5.6.1'}, + @{ModuleName = 'PSScriptAnalyzer'; ModuleVersion = '1.22.0'}, + @{ModuleName = 'InvokeBuild'; ModuleVersion = '5.11.3'}, + @{ModuleName = 'Microsoft.PowerShell.PSResourceGet'; ModuleVersion = '1.0.5'} +) + +task InstallBuildDependencies { + $RequiredModules | + Where-Object {-not (Import-Module $_ -PassThru -ErrorAction Ignore)} | + Install-Module -Force +} + +task ParseManifest { + $Script:Psd1SourcePath = Join-Path $BuildRoot "$ModuleName.psd1" + + $ManifestAst = [Parser]::ParseFile($Psd1SourcePath, [ref]$null, [ref]$null) + $Script:ManifestContent = $ManifestAst.Extent.Text + + $Expression = $ManifestAst.EndBlock.Statements[0].PipelineElements[0].Expression + $KvpAsts = $Expression.KeyValuePairs | Group-Object {$_.Item1.Value} -AsHashTable + + $Script:RootModule = $KvpAsts['RootModule'].Item2.PipelineElements[0].Expression.Value + $Script:ManifestVersionAst = $KvpAsts['ModuleVersion'].Item2.PipelineElements[0].Expression + $Script:ManifestVersion = [version]$ManifestVersionAst.Value + + assert($RootModule) + assert($ManifestVersion) +} + +task AppveyorMetadata ParseManifest, { + $BuildVersion = $env:APPVEYOR_BUILD_VERSION + assert ($BuildVersion) + $Script:IsAppveyorTagBuild = $env:APPVEYOR_REPO_TAG -eq 'true' + if ($IsAppveyorTagBuild) + { + Write-Build -Color Green "Building tag: $env:APPVEYOR_REPO_TAG_NAME" + [version]$Version = $env:APPVEYOR_REPO_TAG_NAME -replace '^\D*' -replace '[^\.\d].*$' + assert ($Version -eq $ManifestVersion) + [int]$Build = $env:APPVEYOR_BUILD_NUMBER + $BuildVersion = $Version, ++$Build -join '-' + Update-AppveyorBuild -Version $BuildVersion + } +} + +task AppveyorAbortWhenHeadAlreadyTagged AppveyorMetadata, { + if (-not $IsAppveyorTagBuild) + { + $Refs = (git for-each-ref --points-at HEAD) -replace '.* ' + $TagRefs = @($Refs) -match '^refs/tags/' + if ($TagRefs) + { + "Commit $(git rev-parse HEAD) is already tagged in $TagRefs" | Write-Build -Color Yellow + appveyor exit + } + } +} + +task Clean { + remove $OutputFolder +} + +task AssertVersion ParseManifest, { + if ($NewVersion) + { + assert ($NewVersion -ge $ManifestVersion) + } +} + +task Version ParseManifest, AssertVersion, { + if ($NewVersion) + { + $Script:Version = $NewVersion + $Script:Tag = "v$NewVersion" + } + else + { + $Script:Version = $ManifestVersion + $Script:Tag = "v$NewVersion" + } + + if ($NewVersion -gt $ManifestVersion) + { + $ManifestContent = ( + $ManifestContent.Substring(0, $ManifestVersionAst.Extent.StartOffset), + $ManifestContent.Substring($ManifestVersionAst.Extent.EndOffset) + ) -join "'$NewVersion'" + $ManifestContent > $Psd1SourcePath + } +} + +task Tag Version, { + if (git diff -- $Psd1SourcePath) + { + git add $Psd1SourcePath + assert($?) + git commit -m $Tag + assert($?) + } + + $Output = git tag $Tag --no-sign *>&1 + if (-not $?) + { + if ($Output -match 'already exists') + { + # If tag points to head, we don't care + $Refs = (git show-ref $Tag --head) -replace ' .*' + assert($Refs.Count -ge 2) + assert($Refs[0] -eq $Refs[1]) + } + else + { + $Output = ($Output -join "`n").Trim() + throw $Output + } + } +} + +task PushTag Tag, { + git push --tags + assert($?) + git push + assert($?) +} + +task BuildDir Version, { + $Script:BuildDir = [IO.Path]::Combine($PSScriptRoot, $OutputFolder, $ModuleName, $Version) + New-Item $BuildDir -ItemType Directory -Force | Out-Null +} + +task IncludedFiles BuildDir, { + Copy-Item $Include $BuildDir +} + +task BuildPowershell Clean, Version, BuildDir, IncludedFiles, { + $Requirements = @() + $Usings = @() + + # case-insensitive matching + $Folders = Get-ChildItem -Directory | Where-Object {$_.Name -in $PSScriptFolders} + + $Content = $Folders | ForEach-Object { + $Label = ($_ | Resolve-Path -Relative) -replace '^\.[\\/]' + $Files = $_ | Get-ChildItem -File -Recurse -Filter *.ps1 + + $FileContents = $Files | ForEach-Object { + $FileAst = [Parser]::ParseFile($_, [ref]$null, [ref]$null) + $_Content = $FileAst.Extent.Text + + $Requirements += $FileAst.ScriptRequirements.Extent.Text + $Usings += $FileAst.UsingStatements.Extent.Text + + # find furthest offset from start + [int]$SnipOffset = ( + $FileAst.ScriptRequirements.Extent.EndOffset, + $FileAst.UsingStatements.Extent.EndOffset, + $FileAst.ParamBlock.Extent.EndOffset # will only exist to hold PSSA suppressions + ) | + Sort-Object | + Select-Object -Last 1 + + $_Content.Substring($SnipOffset).Trim() + } + + "#region $Label", ($FileContents -join "`n`n"), "#endregion $Label" | Write-Output + } + + $Requirements = $Requirements | Write-Output | ForEach-Object Trim | Sort-Object -Unique + $Usings = $Usings | Write-Output | ForEach-Object Trim | Sort-Object -Unique + $Psm1Content = $Requirements, $Usings, "", ($Content -join "`n`n") | Write-Output + + Copy-Item $Psd1SourcePath $BuildDir + $Psm1Content > (Join-Path $BuildDir $RootModule) +} + +task Build BuildPowershell + +task PSSA { + $Files = $Include, $PSScriptFolders | + Write-Output | + Where-Object {Test-Path $_} | + Get-ChildItem -Recurse + + $Files | + ForEach-Object { + Invoke-ScriptAnalyzer -Path $_.FullName -Recurse -Settings .\.vscode\PSScriptAnalyzerSettings.psd1 + } | + Tee-Object -Variable PSSAOutput + + if ($PSSAOutput | Where-Object Severity -ge ([int][Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Warning)) + { + throw "PSSA found code violations" + } +} + +task UnitTest { + Invoke-Pester ./tests/ +} + +Task Test PSSA, UnitTest + +task Package BuildDir, { + if (-not (Get-PSResourceRepository $ModuleName -ErrorAction Ignore)) + { + Register-PSResourceRepository $ModuleName -Uri $OutputFolder -Trusted + } + try + { + Write-Verbose "Packaging to $OutputFolder..." + Publish-PSResource -Path $BuildDir -Repository $ModuleName + } + finally + { + Unregister-PSResourceRepository $ModuleName + } +} + +task Publish BuildDir, { + if (-not $PSGalleryApiKey) + { + if (Get-Command rbw -ErrorAction Ignore) # TODO: sort out SecretManagement wrapper + { + $PSGalleryApiKey = rbw get PSGallery + } + else + { + throw 'PSGalleryApiKey is required' + } + } + + Get-ChildItem -File $OutputFolder -Filter *.nupkg | Remove-Item # PSResourceGet insists on recreating nupkg + Publish-PSResource -Path $BuildDir -DestinationPath $OutputFolder -Repository PSGallery -ApiKey $PSGalleryApiKey +} + +# Default task +task . Build diff --git a/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psd1 b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psd1 new file mode 100644 index 0000000..67e4f6b --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psd1 @@ -0,0 +1,127 @@ +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'ProfileAsync.psm1' + +# Version number of this module. +ModuleVersion = '0.4.2' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '529d1f04-8803-4335-9a5c-ca32310d5dcd' + +# Author of this module +Author = 'Freddie Sackur' + +# Company or vendor of this module +CompanyName = 'Dusty Fox' + +# Copyright statement for this module +Copyright = '(c) 2024 Freddie Sackur. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Load your powershell profile asynchronously, so you can get to the prompt faster.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.1' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @( + 'Import-ProfileAsync' +) + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'Profile' + ) + + # A URL to the license for this module. + LicenseUri = 'https://raw.githubusercontent.com/fsackur/ProfileAsync/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/fsackur/ProfileAsync' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} diff --git a/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psm1 b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psm1 new file mode 100644 index 0000000..2ee5909 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/ProfileAsync.psm1 @@ -0,0 +1,4 @@ +$Folders = "$PSScriptRoot/private", "$PSScriptRoot/public" | Resolve-Path -ea Ignore +$Folders | + Get-ChildItem -File -Recurse -Filter *.ps1 | + ForEach-Object {. $_} diff --git a/dot_config/powershell/Modules/ProfileAsync/appveyor.yml b/dot_config/powershell/Modules/ProfileAsync/appveyor.yml new file mode 100644 index 0000000..f2908ce --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/appveyor.yml @@ -0,0 +1,44 @@ +version: "{build}" + +image: Ubuntu2204 + +cache: + - /home/appveyor/.local/share/powershell/Modules/ + +branches: + only: + - main + - /^v\d+\.\d+\.\d+/ + +skip_branch_with_pr: true + +environment: + GITHUB_TOKEN: + secure: +uoFiZQbo1uuQJWgZn3DKizlzMV/Ji9V9YJxCwtUvDRkwklysZSEMJiNgbiBlBS8 + PSGALLERY_API_KEY: + secure: TgdRRYLIHZVtK6jOj3DAVgGMY+k7TaW/MF8ESPWF1PlnO2WL1jwVpqVCnQux4DP9 + +install: + - pwsh: Invoke-Build AppveyorMetadata, AppveyorAbortWhenHeadAlreadyTagged, InstallBuildDependencies + +build_script: + - pwsh: Invoke-Build Build, Package + +artifacts: + - path: ./Build/**/$(APPVEYOR_PROJECT_NAME).*.nupkg + name: nupkg + +test_script: + - pwsh: Invoke-Build Test + +deploy: + - provider: GitHub + tag: $(APPVEYOR_REPO_TAG_NAME) + description: $(APPVEYOR_PROJECT_NAME) $(APPVEYOR_REPO_TAG_NAME) + auth_token: $(GITHUB_TOKEN) + artifact: nupkg + on: + APPVEYOR_REPO_TAG: true + +after_deploy: + - pwsh: Invoke-Build Publish -PSGalleryApiKey $env:PSGALLERY_API_KEY diff --git a/dot_config/powershell/Modules/ProfileAsync/docs/executable_README.md b/dot_config/powershell/Modules/ProfileAsync/docs/executable_README.md new file mode 100644 index 0000000..a2ef19e --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/docs/executable_README.md @@ -0,0 +1,135 @@ +# ProfileAsync + +Load your powershell profile asynchronously, so you can get to the prompt faster. + +This module is not suitable for general-purpose asynchrony. The `ThreadJob` module is likely to be a better solution. See the warnings below. + +## Usage + +This module exports one command: `Import-ProfileAsync`. + +Say you have a profile that looks like this (with example timings): + +```powershell +# Detect VS Code terminal +# example code; will not work in every case! +$IsVSCode = $env:TERM_PROGRAM -eq 'vscode' # 1ms +if (-not $IsVSCode) {Set-Location ~/git/} # 2ms +Import-Module -Global SomeMonsterAzureModule # 1000ms +``` + +If we consistently run commands from `SomeMonsterAzureModule` immediately when we get to the prompt, then there is no shortcut: we must wait while it loads. But, if that is not the most common case, then we can get to the prompt without waiting for the module to load: + +```powershell +# Detect VS Code terminal +# example code; will not work in every case! +$IsVSCode = $env:TERM_PROGRAM -eq 'vscode' # 1ms +if (-not $IsVSCode) {Set-Location ~/git/} # 2ms + +$AsyncScriptblock = {Import-Module -Global SomeMonsterAzureModule} # 1ms + +Import-ProfileAsync $AsyncScriptblock # 10ms +# End of profile code; control is turned to the user +# Async scriptblock starts running +``` + +This change shaves a second off the time to the prompt. The downside is that it increases the time until the profile is fully loaded, due to asynchrony overhead. This is usually a good trade-off. + +Here's a more realistic example: + +```powershell +$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = [Text.Encoding]::UTF8 + +if ($PSVersionTable.PSEdition -ne 'Core') { <# omitted for brevity #> } + +if ($IsLinux -or $IsMacOS) { <# omitted for brevity #> } + +function Test-VSCode { <# omitted for brevity #> } + +$env:GITROOT = if (Test-Path /git) {"/git"} elseif (Test-Path ~/git) {"~/git"} +if ($env:GITROOT -and -not (Test-VSCode)) {Set-Location $env:GITROOT} + +# Starship is a cross-platform prompt customisation tool - https://starship.rs +if (Get-Command starship -ErrorAction Ignore) +{ + $env:STARSHIP_CONFIG = $PSScriptRoot | Split-Path | Join-Path -ChildPath starship.toml + starship init powershell --print-full-init | Out-String | Invoke-Expression +} + +# a thousand lines of code moved out of main profile into dot-sourced scripts: +. /home/freddie/.local/share/chezmoi/PSHelpers/Console.ps1 +. /home/freddie/.local/share/chezmoi/PSHelpers/git_helpers.ps1 +. /home/freddie/.local/share/chezmoi/PSHelpers/pipe_operators.ps1 +. /home/freddie/.local/share/chezmoi/PSHelpers/Utils.ps1 +. /home/freddie/.local/share/chezmoi/PSHelpers/LinuxNetworking.ps1 +. /home/freddie/.local/share/chezmoi/PSHelpers/ModuleLoad.ps1 +``` + +This profile, including the dot-source script files, was taking nearly two seconds to load: + +```powershell +Measure-Command {pwsh -c ""} | % TotalMilliseconds +1912.2259 +``` + +200ms is from the powershell engine itself: + +```powershell +Measure-Command {pwsh -NoProfile -c ""} | % TotalMilliseconds +199.9063 +``` + +So, the profile adds 1700ms to the start-up time. + +The slow part is the dot-sourced scripts, which import modules and register argument completers. By moving the slow code into `Import-ProfileAsync`, we can get that time right down: + +```powershell +# ...other code... + +$AsyncScriptblock = { + . /home/freddie/.local/share/chezmoi/PSHelpers/Console.ps1 + . /home/freddie/.local/share/chezmoi/PSHelpers/git_helpers.ps1 + . /home/freddie/.local/share/chezmoi/PSHelpers/pipe_operators.ps1 + . /home/freddie/.local/share/chezmoi/PSHelpers/Utils.ps1 + . /home/freddie/.local/share/chezmoi/PSHelpers/LinuxNetworking.ps1 + . /home/freddie/.local/share/chezmoi/PSHelpers/ModuleLoad.ps1 +} +Import-ProfileAsync $AsyncScriptblock +``` + +```powershell +Measure-Command {pwsh -c ""} | % TotalMilliseconds +525.1334 +``` + +Since 200ms is powershell itself, we have got our profile load time down from ~1700ms to ~325ms - that's five times faster! + +The downside is that, due to overhead, the profile takes longer to fully load. + +You need to decide what profile code you want immediately, and what you are willing to wait an extra ~500ms for. + +Put all the fast code at the top of your profile, and the slow code at the bottom. Wrap the slow code in a scriptblock and pass it to `Import-ProfileAsync`, in a single call, at the bottom of your profile. It doesn't have to be the last line, but avoid importing modules or defining functions or registering argument completers after the call to `Import-ProfileAsync`. + +## Risk + +This code works by forcing a second thread to run the initialisation code. Parts of the PowerShell engine are not thread-safe by design. + +> We are hacking. Your session may crash. Errors may be misleading. Do not use in server scripts. + +The risk is minimised in the designed use case: + +- use only in your powershell profile +- only call this command once +- call this command at the bottom + +## Troubleshooting + +If you get unexpected errors, particularly intermittent errors, then increase the value of the Delay parameter. (The default value can be checked with `Get-Help Import-ProfileAsync -Parameter Delay`). This gives the engine more time to complete initialisation, which may be needed on slow machines or when security software is scanning code execution. + +If your profile won't load at all, you can temporarily disable the async feature by setting the env var `PWSH_PROFILE_ASYNC_DISABLE` to `1`, or by passing the `-PWSH_PROFILE_ASYNC_DISABLE` switch. This should get you to the prompt, but your profile may not work as expected. + +## Contributing + +Bug reports are welcome! Submit a new issue [here](https://github.com/fsackur/ProfileAsync/issues/new). + +Code contributions are welcome - please fork the repo and PR to the `main` branch from your fork. diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/HEAD b/dot_config/powershell/Modules/ProfileAsync/dot_git/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/config b/dot_config/powershell/Modules/ProfileAsync/dot_git/config new file mode 100644 index 0000000..bfabe93 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/config @@ -0,0 +1,11 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +[remote "origin"] + url = https://github.com/fsackur/ProfileAsync.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/description b/dot_config/powershell/Modules/ProfileAsync/dot_git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_applypatch-msg.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_applypatch-msg.sample new file mode 100644 index 0000000..a5d7b84 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_commit-msg.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_commit-msg.sample new file mode 100644 index 0000000..b58d118 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_fsmonitor-watchman.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_fsmonitor-watchman.sample new file mode 100644 index 0000000..23e856f --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_post-update.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_post-update.sample new file mode 100644 index 0000000..ec17ec1 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-applypatch.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-applypatch.sample new file mode 100644 index 0000000..4142082 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-commit.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-commit.sample new file mode 100644 index 0000000..29ed5ee --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff-index --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-merge-commit.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-merge-commit.sample new file mode 100644 index 0000000..399eab1 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-push.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-push.sample new file mode 100644 index 0000000..4ce688d --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-rebase.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-rebase.sample new file mode 100644 index 0000000..6cbef5c --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-receive.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-receive.sample new file mode 100644 index 0000000..a1fd29e --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_prepare-commit-msg.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_prepare-commit-msg.sample new file mode 100644 index 0000000..10fa14c --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_push-to-checkout.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_push-to-checkout.sample new file mode 100644 index 0000000..af5a0c0 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_update.sample b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_update.sample new file mode 100644 index 0000000..c4d426b --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/hooks/executable_update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/index b/dot_config/powershell/Modules/ProfileAsync/dot_git/index new file mode 100644 index 0000000000000000000000000000000000000000..5a404a0ea24255779ae7247b9fe4c305e60c7fc9 GIT binary patch literal 1643 zcmZ?q402{*U|?ckVBpW#*lQ(v!siN%W?*1YXOK2L$H36|mVtr!1q8GI$*zlgTYX}E z^p?$~zS7)hduIIa=4VjTOV2FHC{5D$3=R%;jSq444e)Ucan*M!O^**sEyyn_(W^|% z&0%0*U;vwcva=CJL+v?_Y7cX9N4~gd>CxnQyC=^yG?G#gQZ|>K#-NSE9=FuQlG38o z_@LCn($r#vWw`CTfNCFu>>9mp_q$hrFAcfnXc?$8t1dB8wSqwrhkeQUd1;yHdX>33 zxcqaGfuV5;B+Niz$H3{*r>?Sf5t}i8;j(WUn?CK(Eb+2q-~ySSnVy$llnM$P5Qc>d zjE08KWmNN+6y7fWIdjv4oinai>K1SQAb9G7u|fias$N-fa(+sxen4<=a#3bMiDO=3 zPGwbUQE+NWNoHPpv0g!OiXq4vT<*St8ZIm@E`K#|>`wi6gWtV%MtA`K2TqpH3mDja zJe^(rf?aWk?^RUy%B!r<-fQoy6{?qY(mr9wbJxy36?tI>v4Eodw9K4T$KuMoWWA)) z%$yXxf?}`(Vg85F&~UhhYOX*=`jc-vRw-y5tlaZSeNV*1Wp1H?FBk+-%mw=yW)6&o zns*)5JciYJPbbV&idv(o<`o$}yRvmJmpaP^4D)ggafROvRP$JvK27L(_+nai&|1&g zTXp2xrkO1-6JX#;EGQ^Tt;{b1Cn0?CaSJs*#8xbNFZujX#mgeL@s+XGrG7#oZ7!CFB9aQ&moO24lzWdFT+q1N0?XCMORNpJHce@0G zd_hrWSz<}5zF%s&u2X(#UP?fId1_H`MruwDG=gB}!f2@Z_fX9j{T0ZNeKqrF!;IfT zy0JD_rXO4HDGT zZkzrzok6A~wYa2MAI01dkPy`IuylLw$wjdH&po*ab^k+D^BF7RHRTo7I|N2uKlsRc zSM)8zb&oIXWe|dxUjRwHh6qy`7(#+vLD}LrgQ2N{8P}0ti@3jfKCR*07atsOPjH^V zi8r%Aam8S$V8EqvIKA~N$K}Z`mMNP)Is2Tf*_&YsHkiRk!GNpv`KR=640HSZ+e;h1 zF>>9@J2G7Y5^P}2O;ts_UmX+`ex2L;{w;g2!@9l$x}ee|qcn-ZSiz7>qq=h)@2r%Z zX(A;Drf-?jcjL;|-KeEAgRz1ES4?e)+_Q6QHU3x3cC>luu~%t%ZY;DA0b4V5^7
    X OQTX>V@# 1756466490 +0200 clone: from https://github.com/fsackur/ProfileAsync.git diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/heads/main b/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/heads/main new file mode 100644 index 0000000..b645329 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 058c520a9de4559598215c0cfe3047b4cb9bc6b8 Meisterlala <6453306+Meisterlala@users.noreply.github.com> 1756466490 +0200 clone: from https://github.com/fsackur/ProfileAsync.git diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/remotes/origin/HEAD b/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..b645329 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_git/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 058c520a9de4559598215c0cfe3047b4cb9bc6b8 Meisterlala <6453306+Meisterlala@users.noreply.github.com> 1756466490 +0200 clone: from https://github.com/fsackur/ProfileAsync.git diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/info/.keep b/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/info/.keep new file mode 100644 index 0000000..e69de29 diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.idx b/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.idx new file mode 100644 index 0000000000000000000000000000000000000000..57b445836b1f1e84422c935f02f5cf392d5d4c3b GIT binary patch literal 4880 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 literal 0 HcmV?d00001 diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.pack b/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.pack new file mode 100644 index 0000000000000000000000000000000000000000..2c44dce2bae0d9fb0aaae0dc7975214bc3e08948 GIT binary patch literal 34509 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 S`K#HqU!KKlI6-O0i8KI#|Byfc literal 0 HcmV?d00001 diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.rev b/dot_config/powershell/Modules/ProfileAsync/dot_git/objects/pack/readonly_pack-ed658821038fe24f263c87d39a0d319022b8c866.rev new file mode 100644 index 0000000000000000000000000000000000000000..adb9d5d22e6e761ddaf37dff756b38313fbc3863 GIT binary patch literal 596 zcmWIYbctYKU|@t|HUKL!SdS_TG&a0Uhj za|Q;6HUylTLuP(PzDBu1_lNO6$S85kJa85kHc7#J9s z85kJ&7#J8@85kI>7#J8p;o`->z|hRVz>v?tz~IEdz~I8bz~ITiz);1&z);4(z`(-5 zz);D+z+lS2z@W^)z!1d1z#z@Qz`)JGz+lh7z!1p5z#z)Nz#zrIz!1y8z+lb5z|hFR zz+lI~z)-}%z!1&Az)--zz)-@#z>v(qz@W&$z#z`Rz~IQhz`(@7z`)PIz>vkjz|g|L zz>vtmz+lF}z@Wsyz#zuJz>vzoz#zcDz);A*z);P=z@Wvzz@W|m0qG133=#|s3{4CS z45|za41x>{3>pj!3?d8+4D}2Q3?2*&3^5E046+Oi47m&p4Dk#M47v;q3^@!83;_%b z3{eaW3?>W=3^oi53>*v$48aTx3`TELI~1AwANi} + List of steps, sample code, failing test or link to a project that reproduces the behavior. + Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues. + placeholder: > + I am experiencing a problem with X. + I think Y should be happening but Z is actually happening. + validations: + required: true +- type: textarea + attributes: + label: Expected behavior + render: console + placeholder: | + PS> 2 + 2 + 4 + validations: + required: true +- type: textarea + attributes: + label: Actual behavior + render: console + placeholder: | + PS> 2 + 2 + 5 + validations: + required: true +- type: textarea + attributes: + label: Error details + description: Paste verbatim output from `Get-Error` if ProfileAsync returned an error. + render: console + placeholder: PS> Get-Error +- type: textarea + attributes: + label: Environment data + description: | + Paste output from `[System.Environment]::OSVersion` and `Get-Module` below. The aim + is to have version information about any code that may be relevant to the bug; + include any script that reproduces the bug. + render: powershell + placeholder: | + PS> [System.Environment]::OSVersion + PS> Get-Module + validations: + required: true +- type: textarea + attributes: + label: Visuals + description: > + If it helps to reproduce the issue, upload images or animations in the area below. diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_Feature_Request.yaml b/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_Feature_Request.yaml new file mode 100644 index 0000000..886f171 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_Feature_Request.yaml @@ -0,0 +1,20 @@ +name: Feature Request / Idea 🚀 +description: Suggest a new feature or improvement (this does not mean you have to implement it) +labels: [Issue-Enhancement, Needs-Triage] +body: +- type: textarea + attributes: + label: Summary of the new feature / enhancement + description: > + A clear and concise description of what the problem is that the + new feature would solve. Try formulating it in user story style + (if applicable). + placeholder: "'As a user I want X so that Y...' with X being the being the action and Y being the value of the action." + validations: + required: true +- type: textarea + attributes: + label: Proposed technical implementation details (optional) + placeholder: > + A clear and concise description of what you want to happen. + Consider providing an example ProfileAsync experience with expected result. diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_config.yml b/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_config.yml new file mode 100644 index 0000000..ac2e8b4 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_github/ISSUE_TEMPLATE/executable_config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_gitignore b/dot_config/powershell/Modules/ProfileAsync/dot_gitignore new file mode 100644 index 0000000..448e272 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_gitignore @@ -0,0 +1 @@ +Build/** diff --git a/dot_config/powershell/Modules/ProfileAsync/dot_vscode/PSScriptAnalyzerSettings.psd1 b/dot_config/powershell/Modules/ProfileAsync/dot_vscode/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..eda3f99 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/dot_vscode/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,19 @@ +@{ + IncludeDefaultRules = $true + IncludeRules = '*' + ExcludeRules = @() + Rules = @{ + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $false + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NoEmptyLineBefore = $false + IgnoreOneLineBlock = $true + NewLineAfter = $true + } + } +} diff --git a/dot_config/powershell/Modules/ProfileAsync/executable_LICENSE b/dot_config/powershell/Modules/ProfileAsync/executable_LICENSE new file mode 100644 index 0000000..44fd29d --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/executable_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Freddie Sackur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dot_config/powershell/Modules/ProfileAsync/private/New-BoundPowerShell.ps1 b/dot_config/powershell/Modules/ProfileAsync/private/New-BoundPowerShell.ps1 new file mode 100644 index 0000000..4257d7b --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/private/New-BoundPowerShell.ps1 @@ -0,0 +1,53 @@ +function New-BoundPowerShell +{ + <# + .DESCRIPTION + Reflection magic! + + Returns an instance of PowerShell with some internal runspace objects set to the objects + from the current execution context. These objects are not supposed to be shared; race conditions + may occur. + #> + + [CmdletBinding()] + param () + + # A runspace to run our code asynchronously; pass in $Host to support Write-Host + $Runspace = [runspacefactory]::CreateRunspace($Host) + $Powershell = [powershell]::Create($Runspace) + $Runspace.Open() + + # ArgumentCompleters are set on the ExecutionContext, not the SessionState + # Note that $ExecutionContext is not an ExecutionContext, it's an EngineIntrinsics + $Private = [System.Reflection.BindingFlags]'Instance, NonPublic' + $ContextField = [System.Management.Automation.EngineIntrinsics].GetField('_context', $Private) + $GlobalContext = $ContextField.GetValue($ExecutionContext) + + # Get the ArgumentCompleters. If null, initialise them. + $ContextCACProperty = $GlobalContext.GetType().GetProperty('CustomArgumentCompleters', $Private) + $ContextNACProperty = $GlobalContext.GetType().GetProperty('NativeArgumentCompleters', $Private) + $CAC = $ContextCACProperty.GetValue($GlobalContext) + $NAC = $ContextNACProperty.GetValue($GlobalContext) + if ($null -eq $CAC) + { + $CAC = [System.Collections.Generic.Dictionary[string, scriptblock]]::new() + $ContextCACProperty.SetValue($GlobalContext, $CAC) + } + if ($null -eq $NAC) + { + $NAC = [System.Collections.Generic.Dictionary[string, scriptblock]]::new() + $ContextNACProperty.SetValue($GlobalContext, $NAC) + } + + # Get the AutomationEngine and ExecutionContext of the runspace + $RSEngineField = $Runspace.GetType().GetField('_engine', $Private) + $RSEngine = $RSEngineField.GetValue($Runspace) + $EngineContextField = $RSEngine.GetType().GetFields($Private) | Where-Object {$_.FieldType.Name -eq 'ExecutionContext'} + $RSContext = $EngineContextField.GetValue($RSEngine) + + # Set the runspace to use the global ArgumentCompleters + $ContextCACProperty.SetValue($RSContext, $CAC) + $ContextNACProperty.SetValue($RSContext, $NAC) + + return $Powershell +} diff --git a/dot_config/powershell/Modules/ProfileAsync/public/Import-ProfileAsync.ps1 b/dot_config/powershell/Modules/ProfileAsync/public/Import-ProfileAsync.ps1 new file mode 100644 index 0000000..51006bd --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/public/Import-ProfileAsync.ps1 @@ -0,0 +1,163 @@ +function Import-ProfileAsync +{ + <# + .SYNOPSIS + Load your powershell profile asynchronously, so you can get to the prompt faster. + + .LINK + https://github.com/fsackur/ProfileAsync + + .DESCRIPTION + This command executes a scriptblock asynchronously using the current session's + execution context. In simple terms, this runs code asynchronously in the caller's + scope. + + This command is not the best tool if you do not need that specific behaviour. + + When used in a powershell profile, it effectively runs in the global scope. Things in the + scriptblock will be available in the session when the scriptblock completes. + + This includes modules, functions, aliases, variables and argument completers. + + Warning: + + This command uses reflection hacks. PowerShell is designed to avoid async bugs in areas + that we jam async code into. Your session may crash. Errors may be misleading. Do not use + in server scripts. + + The risk is minimised in the designed use case: + + - use only in your powershell profile + - only call this command once + - call this command at the bottom + - increase delay if you get errors + - don't do other async stuff before the async code completes + + .PARAMETER ScriptBlock + The code to be executed asynchronously. + + .PARAMETER Delay + Interval, in milliseconds, to wait within the asynchronous runspace before executing the + scriptblock. + + This is necessary because this command subverts normal runspace initialisation. Without + this delay, command availability may be unreliable when the command is run at startup. + + This delay can be set to 0 when the command is run in a fully-initialised powershell + session. + + 10ms may be sufficient on a fast machine. 100-200ms should cover most recent machines. + + .PARAMETER PWSH_PROFILE_ASYNC_DISABLE + Disables the async and scope features. Also accepted as an env var; parameter takes + precedence. + + Use this to recover a crashing profile. Code will not be run in the global scope. + #> + + [CmdletBinding()] + param + ( + [Parameter(Mandatory, Position = 0)] + [scriptblock]$ScriptBlock, + + [ValidateRange(0, 5000)] + [PSDefaultValue(Help = "500ms")] + [int]$Delay = 500, + + [switch]$PWSH_PROFILE_ASYNC_DISABLE + ) + + if ($PWSH_PROFILE_ASYNC_DISABLE -or $env:PWSH_PROFILE_ASYNC_DISABLE -imatch '^(1|true|yes)$') + { + . $ScriptBlock + return + } + + $Silent = (Get-Command tty -CommandType Application -ErrorAction Ignore) -and (tty) -match "not a tty" + if ($Silent) + { + $VerbosePreference = 'SilentlyContinue' + } + + + $Powershell = New-BoundPowerShell + + # https://seeminglyscience.github.io/powershell/2017/09/30/invocation-operators-states-and-scopes + $GlobalState = [psmoduleinfo]::new($false) + $GlobalState.SessionState = $ExecutionContext.SessionState + + $Powershell.Runspace.SessionStateProxy.PSVariable.Set('GlobalState', $GlobalState) + $Powershell.Runspace.SessionStateProxy.PSVariable.Set('ScriptBlock', $ScriptBlock) + $Powershell.Runspace.SessionStateProxy.PSVariable.Set('Delay', $Delay) + + + "Starting asynchronous execution" | Write-Verbose + + $Wrapper = { + [System.Diagnostics.DebuggerHidden()] + param() + + # Runspace init is unsafe. Stack traces point to PSReadLine; not sure + Start-Sleep -Milliseconds $Delay + + . $GlobalState {. $args[0]} $ScriptBlock + } + $AsyncResult = $Powershell.AddScript($Wrapper).BeginInvoke() + + + $SourceIdentifier = "__ProfileAsyncCleanup__" + [guid]::NewGuid() + $HandlerParams = @{ + MessageData = $AsyncResult, $Silent, $VerbosePreference + InputObject = $Powershell + EventName = "InvocationStateChanged" + SourceIdentifier = $SourceIdentifier + } + $null = Register-ObjectEvent @HandlerParams -Action { + $AsyncResult, $Silent, $VerbosePreference = $Event.MessageData + $Powershell = $Event.Sender + $SourceIdentifier = $EventSubscriber.SourceIdentifier + + if ($Powershell.Streams.Error) + { + $Powershell.Streams.Error | Out-String | Write-Host -ForegroundColor Red + $Powershell.Streams.Error.Clear() + } + + if ($Powershell.InvocationStateInfo.State -ge 2) + { + try + { + $Powershell.EndInvoke($AsyncResult) + } + catch + { + $_ | Out-String | Write-Host -ForegroundColor Red + } + + $Formats = $Powershell.Runspace.InitialSessionState.Formats + $FormatFiles = $Formats.FileName | Where-Object {$_ -and (Test-Path $_)} + if ($FormatFiles) + { + Update-FormatData -PrependPath $FormatFiles + } + + $Types = $Powershell.Runspace.InitialSessionState.Types + $TypeFiles = $Types.FileName | Where-Object {$_ -and (Test-Path $_)} + if ($TypeFiles) + { + Update-TypeData -PrependPath $TypeFiles + } + + Unregister-Event $SourceIdentifier + Get-Job $SourceIdentifier | Remove-Job + + if ($VerbosePreference -eq 'Continue' -and -not $Silent) + { + $State = [string]$Powershell.InvocationStateInfo.State + $Msg = "VERBOSE: Asynchronous execution $($State.ToLower())" + $Msg | Write-Host -ForegroundColor Yellow + } + } + } +} diff --git a/dot_config/powershell/Modules/ProfileAsync/tests/ProfileAsync.Tests.ps1 b/dot_config/powershell/Modules/ProfileAsync/tests/ProfileAsync.Tests.ps1 new file mode 100644 index 0000000..a17a098 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/tests/ProfileAsync.Tests.ps1 @@ -0,0 +1,100 @@ +BeforeAll { + $TestRoot = $PSScriptRoot + $ModuleBase = $PSScriptRoot | Split-Path + $PesterBase = (Get-Module Pester).ModuleBase + $ContainerRunner = 'docker', 'podman' | ForEach-Object { Get-Command $_ -ErrorAction SilentlyContinue } | Select-Object -First 1 -Expand Name + $Containers = @{} + + function Test + { + param + ( + [Parameter(Mandatory, ValueFromPipeline, Position = 0)] + [string]$TestProfile + ) + + begin + { + $Image = "mcr.microsoft.com/powershell" + $Listing = & "$ContainerRunner" image ls --format json $Image | ConvertFrom-Json + if (-not $Listing) {& "$ContainerRunner" pull $Image} + $FactScript = { + "waiting async marker" | Write-Verbose + while (-not (Test-Path $Marker)) {Start-Sleep -Milliseconds 100} + "marker found" | Write-Verbose + Get-Module | % Name + } + } + + process + { + $RunArgs = @( + "--detach", + "--label", "test", + "--name", $TestProfile, + "-v", "$TestRoot/$TestProfile`:/opt/microsoft/powershell/7/profile.ps1", + "-v", "$ModuleBase/:/opt/microsoft/powershell/7/Modules/ProfileAsync/", + "-v", "$PesterBase/:/opt/microsoft/powershell/7/Modules/Pester/", + $Image, + "pwsh", "-c", "$FactScript" + ) + + $Id = & "$ContainerRunner" run @RunArgs + if (-not $?) {throw} + $Containers[$TestProfile] = $Id + } + } + + function Wait-Container + { + param + ( + [Parameter(Mandatory, ValueFromPipeline, Position = 0)] + [string]$Id + ) + + process + { + while ($true) + { + $Container = & "$ContainerRunner" inspect $Id | ConvertFrom-Json + if (-not $Container.State.Running) {break} + Start-Sleep -Milliseconds 500 + } + $Id + } + } + + function Receive-Container + { + param + ( + [Parameter(Mandatory, ValueFromPipeline, Position = 0)] + [string]$Id + ) + + process + { + & "$ContainerRunner" logs $Id + & "$ContainerRunner" rm -f $Id | Out-Null + } + } +} + +Describe "Import-ProfileAsync" { + + BeforeAll { + $TestProfile = "profile1.ps1" + "Testing $TestProfile" | Write-Verbose + Test $TestProfile + } + + BeforeEach { + $Output = $Containers[$TestProfile] | Wait-Container | Receive-Container + $StdOut = $Output -notmatch "^\e\[33;1mVERBOSE:" + } + + It "Imports Pester" { + "Pester" | Should -BeIn $StdOut + } +} diff --git a/dot_config/powershell/Modules/ProfileAsync/tests/profile1.ps1 b/dot_config/powershell/Modules/ProfileAsync/tests/profile1.ps1 new file mode 100644 index 0000000..5f291f2 --- /dev/null +++ b/dot_config/powershell/Modules/ProfileAsync/tests/profile1.ps1 @@ -0,0 +1,12 @@ +# $VerbosePreference = 'Continue' + +"starting sync" | Write-Verbose +Import-Module ProfileAsync -ea Stop -Verbose:$false +$Marker = Join-Path ([IO.Path]::GetTempPath()) "test.marker" +Import-ProfileAsync -Delay 1000 { + "starting async" | Write-Verbose + Import-Module Pester -Verbose:$false + "" > $Marker + "async return" | Write-Verbose +} +"sync return" | Write-Verbose