29 Commits

Author SHA1 Message Date
Hsiao-nan Cheung
362e06c79c Merge branch 'develop' into feature/install_via_git_history 2025-11-10 16:40:06 +08:00
Hsiao-nan Cheung
d9c8f0b3e7 Merge branch 'develop' into feature/install_via_git_history 2025-11-10 14:13:18 +08:00
Hsiao-nan Cheung
5d46f67e34 Merge branch 'develop' into feature/install_via_git_history 2025-09-26 23:51:08 +08:00
Bill ZHANG
1fae80b2c2 test: enhance Find-HistoricalManifestInCache with DataTable mock for improved manifest retrieval 2025-08-27 23:20:19 +10:00
Bill ZHANG
cf345e272e test: add behavior-oriented mocks for Invoke-Git to simulate HEAD version retrieval 2025-08-27 22:59:48 +10:00
Bill ZHANG
99fb8383b7 fix(manifest): enforce DataTable contract at caller; select latest matching version via h'^/h with validated fallback 2025-08-27 22:39:02 +10:00
Bill ZHANG
0a52f0d0de refactor(manifest): simplify history lookup and error handling; update tests 2025-08-27 20:45:54 +10:00
Bill ZHANG
becd429581 refactor(manifest): Streamline historical manifest retrieval and add Write-ManifestToUserCache helper function 2025-08-27 19:05:22 +10:00
Bill ZHANG
d3f681ce03 refactor(manifest): Simplify UTF-8 encoding handling and enhance test coverage 2025-08-27 18:19:56 +10:00
Hsiao-nan Cheung
9f62e55862 Merge branch 'develop' into feature/install_via_git_history 2025-08-12 18:29:52 +08:00
Bill ZHANG
887f79d2d7 refactor: remove new tests 2025-07-21 13:33:16 +10:00
Bill ZHANG
5bae77eebb refactor: Enhance Select-ScoopDBItem and Get-ScoopDBItem functions for improved data handling and output 2025-07-21 12:48:41 +10:00
Bill ZHANG
210b5e45a0 fix: Improve user guidance and error handling in generate_user_manifest function 2025-07-21 12:26:31 +10:00
Bill ZHANG
3aa64baeb1 refactor: Remove deprecated install_app tests and enhance Get-HistoricalManifestFromDB for exact version matching 2025-07-21 10:56:38 +10:00
Bill ZHANG
0470ba2d04 refactor: Enhance historical manifest retrieval with improved user guidance and fallback mechanisms 2025-07-21 10:47:07 +10:00
Bill ZHANG
398aaa2330 fix: Update installation instructions to clarify search order for app versions 2025-07-21 10:44:12 +10:00
Bill ZHANG
4585f50a54 Remove best match logic from Get-HistoricalManifestFromDB
- Keep only exact match logic (lines 177-189)
- Return null immediately when no exact match found (line 193)
- Remove trailing whitespace from manifest.ps1
- Function now only supports exact version matches, no compatibility matching
2025-07-21 10:29:54 +10:00
Bill ZHANG
9a6cdc7328 Fix test failures: improve parameter validation, file formatting, and test expectations
- Add missing psmodules.ps1 import to Install tests
- Fix architecture parameter validation (x64 -> 64bit)
- Add null/empty bucket parameter handling in Get-HistoricalManifestFromDB
- Fix file format issues (add missing newlines, remove trailing whitespace)
- Update JSON parsing tests to match actual implementation behavior
- Fix path separator normalization in Get-RelativePathCompat test
- Remove invalid error handling tests for non-existent try-catch blocks
- Reduce test failures from 11 to 4, with remaining issues being mock-related

All fixes respect existing codebase behavior and design decisions.
2025-07-20 23:26:59 +10:00
Bill ZHANG
039d2d77c2 revert: remove out-of-scope error handling changes from install.ps1
The try-catch block addition was a general error handling improvement
that belongs in a separate PR, not in this git history feature PR.
2025-07-20 23:10:14 +10:00
Bill ZHANG
ec44c6c7b2 refactor: separate git history concerns from installation layer
- Remove git history fallback logic from install_app function
- Move git history concerns to manifest layer where they belong
- Improve separation of concerns and reduce coupling
- Update tests to reflect the cleaner design
- Merge git history tests into Scoop-Install.Tests.ps1 for consistency

This follows the 'Pull complexity downward' principle by keeping
installation logic focused on installation, while manifest resolution
handles all version fallback strategies internally.
2025-07-20 22:54:33 +10:00
Bill ZHANG
c264afc6bd fix: Handle git command array output for all PowerShell versions
- Fix git show command returning arrays instead of strings
- Join array elements with newlines to create proper JSON content
- Remove misleading PowerShell 5 specific comments
- Fixes historical manifest search in both PowerShell 5 and 7
2025-07-20 21:49:44 +10:00
Bill ZHANG
4061bff270 fix: Add PowerShell 5 compatibility for UTF8 encoding
- Use Out-UTF8File instead of Set-Content with UTF8Encoding for PowerShell 5
- Add version checks to use appropriate encoding methods
- Fixes 'Cannot bind parameter Encoding' error in Windows PowerShell 5
- Ensures historical manifest search works on all PowerShell versions
2025-07-20 21:32:58 +10:00
Bill ZHANG
3e3ce096b8 docs: Fix comment mismatch for python version example
- Change '3.7.x' to '3.12.x' to match the command 'python@3.12'
- Addresses GitHub Copilot's documentation consistency feedback
2025-07-20 21:27:47 +10:00
Bill ZHANG
83696854a8 fix: Remove trailing whitespaces from manifest.ps1
- Fix code style issues to pass tests
- Maintain existing functionality without changes
2025-07-20 21:09:55 +10:00
Bill ZHANG
82a35323d3 feat: Prioritize SQLite database for historical manifest search
- Add Get-HistoricalManifestFromDB function to search SQLite cache first
- Modify Get-HistoricalManifest to use SQLite before falling back to git history
- Implement best version matching for SQLite database results
- Add conditional database import to ensure functions are available
- Improve user messages to indicate whether cache or git history is used

This addresses @niheaven's suggestion to use SQLite for faster historical version searches, significantly reducing performance bottlenecks when searching for approximate versions.
2025-07-20 21:02:16 +10:00
Bill ZHANG
dfadfcc1dc style: Remove extra blank line from CHANGELOG.md 2025-07-20 20:54:16 +10:00
Bill ZHANG
0ade4a5344 fix: Add Windows PowerShell compatibility for relative path operations
- Add Get-RelativePathCompat function that falls back to Uri-based implementation for PowerShell 5.x
- Replace direct GetRelativePath call with compatible wrapper
- Ensures historical manifest feature works across all PowerShell versions
2025-07-20 20:53:40 +10:00
Bill ZHANG
ce95819fa5 docs(changelog): update changelog 2025-05-26 22:03:23 +10:00
Bill ZHANG
c7795b44af feat(install): Add historical manifest search for app installation
- Implemented try-catch block in `install_app` function to handle installation failures and provide informative error messages.
- Added `Get-HistoricalManifest` function to search for specific app versions in git history, enhancing version management during installation.
- Updated `generate_user_manifest` to utilize historical manifests as a fallback when autoupdate fails.
- Enhanced documentation in `scoop-config.ps1` to clarify the use of git history for version installation.
2025-05-26 20:48:27 +10:00
17 changed files with 474 additions and 164 deletions

View File

@@ -2,6 +2,7 @@
### Features
- **install:** Add support for historical manifests by git history ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370))
- **install:** Add output for the setting and removal of environment variables ([#6460](https://github.com/ScoopInstaller/Scoop/issues/6460))
- **scoop-uninstall:** Allow access to `$bucket` in uninstall scripts ([#6380](https://github.com/ScoopInstaller/Scoop/issues/6380))
- **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418))
@@ -18,24 +19,15 @@
- **autoupdate:** Use origin URL to handle URLs with fragment in GitHub mode ([#6455](https://github.com/ScoopInstaller/Scoop/issues/6455))
- **buckets|scoop-info:** Switch git log date format to ISO 8601 to avoid locale issues ([#6446](https://github.com/ScoopInstaller/Scoop/issues/6446))
- **scoop-version:** Fix logic error caused by missing brackets ([#6463](https://github.com/ScoopInstaller/Scoop/issues/6463))
- **getopt:** Teach getopt to respect the `--%` token ([#6477](https://github.com/ScoopInstaller/Scoop/issues/6477))
- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
- **path:** Trim ending slash when initializing paths ([#6501](https://github.com/ScoopInstaller/Scoop/issues/6501))
- **checkver:** Allow script to run when URL fetch fails but script exists ([#6490](https://github.com/ScoopInstaller/Scoop/issues/6490), [#6556](https://github.com/ScoopInstaller/Scoop/issues/6556))
- **install:** Don't add to `$Error` when checking for service `cexecsvc` ([#6520](https://github.com/ScoopInstaller/Scoop/issues/6520))
- **checkver:** Fix incorrect version returned when script fails without output ([#6547](https://github.com/ScoopInstaller/Scoop/issues/6547))
- **uninstall:** Import `url_filename` from `download.ps1` ([#6530](https://github.com/ScoopInstaller/Scoop/issues/6530))
- **checkver:** Allow script to run when URL fetch fails but script exists ([#6490](https://github.com/ScoopInstaller/Scoop/issues/6490))
- **schema:** Add missing `hash.mode` value `github` ([#6533](https://github.com/ScoopInstaller/Scoop/issues/6533))
- **core:** Skip NO_JUNCTION logic when $app is 'scoop' in `currentdir` function ([#6541](https://github.com/ScoopInstaller/Scoop/issues/6541))
### Code Refactoring
- **output:** Replace raw prints with functions for standardized output ([#6449](https://github.com/ScoopInstaller/Scoop/issues/6449))
- **output:** Combine the separated outputs into a single output ([#6545](https://github.com/ScoopInstaller/Scoop/issues/6545))
### Builds
- **supporting:** Update System.Data.SQLite to 2.0.2 ([#6555](https://github.com/ScoopInstaller/Scoop/issues/6555), [#6560](https://github.com/ScoopInstaller/Scoop/issues/6560))
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11

View File

@@ -274,8 +274,6 @@ while ($in_progress -gt 0) {
$expected_ver = $json.version
$ver = $Version
$matchesHashtable = @{}
if (!$ver) {
if (!$regexp -and $replace) {
next "'replace' requires 're' or 'regex'"
@@ -292,9 +290,6 @@ while ($in_progress -gt 0) {
}
}
$page = $null
$source = $url
if ($url -and !$err) {
$ms = New-Object System.IO.MemoryStream
$ms.Write($result, 0, $result.Length)
@@ -304,17 +299,12 @@ while ($in_progress -gt 0) {
}
$page = (New-Object System.IO.StreamReader($ms, (Get-Encoding $wc))).ReadToEnd()
}
$source = $url
if ($script) {
$page = Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
$source = 'the output of script'
}
if ($null -eq $page) {
next "couldn't retrieve content from $source"
continue
}
if ($jsonpath) {
# Return only a single value if regex is absent
$noregex = [String]::IsNullOrEmpty($regexp)
@@ -373,6 +363,7 @@ while ($in_progress -gt 0) {
}
if ($match -and $match.Success) {
$matchesHashtable = @{}
$re.GetGroupNames() | ForEach-Object { $matchesHashtable.Add($_, $match.Groups[$_].Value) }
$ver = $matchesHashtable['1']
if ($replace) {

View File

@@ -160,7 +160,7 @@ function add_bucket($name, $repo) {
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
return 1
}
Write-Host 'OK.'
Write-Host 'OK'
if (get_config USE_SQLITE_CACHE) {
info 'Updating cache...'
Set-ScoopDB -Path (Get-ChildItem (Find-BucketDirectory $name) -Filter '*.json' -Recurse).FullName

View File

@@ -206,6 +206,9 @@ function Complete-ConfigChange {
}
if ($Name -eq 'use_sqlite_cache' -and $Value -eq $true) {
if ((Get-DefaultArchitecture) -eq 'arm64') {
abort 'SQLite cache is not supported on ARM64 platform.'
}
. "$PSScriptRoot\..\lib\database.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
info 'Initializing SQLite cache in progress... This may take a while, please wait.'
@@ -747,7 +750,7 @@ function Invoke-ExternalCommand {
[void]$Process.Start()
} catch {
if ($Activity) {
Write-Host "Error." -ForegroundColor DarkRed
Write-Host "error." -ForegroundColor DarkRed
}
error $_.Exception.Message
return $false
@@ -766,20 +769,20 @@ function Invoke-ExternalCommand {
if ($Process.ExitCode -ne 0) {
if ($ContinueExitCodes -and ($ContinueExitCodes.ContainsKey($Process.ExitCode))) {
if ($Activity) {
Write-Host "Done." -ForegroundColor DarkYellow
Write-Host "done." -ForegroundColor DarkYellow
}
warn $ContinueExitCodes[$Process.ExitCode]
return $true
} else {
if ($Activity) {
Write-Host "Error." -ForegroundColor DarkRed
Write-Host "error." -ForegroundColor DarkRed
}
error "Exit code was $($Process.ExitCode)!"
return $false
}
}
if ($Activity) {
Write-Host "Done." -ForegroundColor Green
Write-Host "done." -ForegroundColor Green
}
return $true
}

View File

@@ -4,11 +4,10 @@
.SYNOPSIS
Get SQLite .NET driver
.DESCRIPTION
Download and extract the SQLite .NET driver and SQLite precompiled binaries.
The SQLite version is automatically detected from the download page.
Download and extract the SQLite .NET driver from NuGet.
.PARAMETER Version
System.String
The version of the System.Data.SQLite NuGet package to download. (require version 2.0.0 or higher)
The version of the SQLite .NET driver to download.
.INPUTS
None
.OUTPUTS
@@ -17,44 +16,21 @@
#>
function Get-SQLite {
param (
[string]$Version = '2.0.2'
[string]$Version = '1.0.118'
)
# Install SQLite
try {
$sqliteNetPath = "$env:TEMP\sqlite.net.zip"
$sqliteDllPath = "$env:TEMP\sqlite.dll.zip"
Write-Host "Downloading SQLite $Version..." -ForegroundColor DarkYellow
$sqlitePkgPath = "$env:TEMP\sqlite.zip"
$sqliteTempPath = "$env:TEMP\sqlite"
$sqlitePath = "$PSScriptRoot\..\supporting\sqlite"
$arch = switch (Get-DefaultArchitecture) {
'32bit' { 'x86' }
'64bit' { 'x64' }
'arm64' { 'arm64' }
default { Write-Warning "Unknown architecture, using x64 as fallback"; 'x64' }
}
Write-Host "Downloading System.Data.SQLite $Version..." -ForegroundColor DarkYellow
Invoke-WebRequest -Uri "https://globalcdn.nuget.org/packages/system.data.sqlite.$Version.nupkg" -OutFile $sqliteNetPath
$downloadPage = Invoke-WebRequest -Uri 'https://sqlite.org/download.html' -UseBasicParsing
if ($downloadPage.Content -match '(?s)<!-- Download product data.*?(PRODUCT,.+?)-->') {
$productData = $Matches[1] | ConvertFrom-Csv
} else {
throw "Failed to parse SQLite download page product data"
}
$matchRow = $productData | Where-Object { $_.'RELATIVE-URL' -match "sqlite-dll-win-$arch-" }
if (-not $matchRow) {
throw "SQLite DLL for architecture $arch not found"
}
Write-Host "Downloading SQLite DLL $($matchRow.VERSION)..." -ForegroundColor DarkYellow
Invoke-WebRequest -Uri "https://sqlite.org/$($matchRow.'RELATIVE-URL')" -OutFile $sqliteDllPath
Write-Host "Extracting libraries... " -ForegroundColor DarkYellow -NoNewline
$sqliteNetPath, $sqliteDllPath | Expand-Archive -DestinationPath $sqliteTempPath -Force
$null = New-Item -Path "$sqlitePath\$arch" -ItemType Directory -Force
Move-Item -Path "$sqliteTempPath\lib\netstandard2.0\System.Data.SQLite.dll" -Destination $sqlitePath -Force
Move-Item -Path "$sqliteTempPath\sqlite3.dll" -Destination "$sqlitePath\$arch\e_sqlite3.dll" -Force
Remove-Item -Path $sqliteNetPath, $sqliteDllPath, $sqliteTempPath -Recurse -Force
Write-Host 'Done.' -ForegroundColor DarkYellow
Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/stub.system.data.sqlite.core.netframework/$version/stub.system.data.sqlite.core.netframework.$version.nupkg" -OutFile $sqlitePkgPath
Write-Host "Extracting SQLite $Version..." -ForegroundColor DarkYellow -NoNewline
Expand-Archive -Path $sqlitePkgPath -DestinationPath $sqliteTempPath -Force
New-Item -Path $sqlitePath -ItemType Directory -Force | Out-Null
Move-Item -Path "$sqliteTempPath\build\net451\*", "$sqliteTempPath\lib\net451\System.Data.SQLite.dll" -Destination $sqlitePath -Force
Remove-Item -Path $sqlitePkgPath, $sqliteTempPath -Recurse -Force
Write-Host ' Done' -ForegroundColor DarkYellow
return $true
} catch {
return $false
@@ -243,9 +219,9 @@ function Set-ScoopDB {
<#
.SYNOPSIS
Find Scoop database item(s).
Select Scoop database item(s).
.DESCRIPTION
Find item(s) from the Scoop SQLite database.
Select item(s) from the Scoop SQLite database.
The pattern is matched against the name, binaries, and shortcuts columns for apps.
.PARAMETER Pattern
System.String
@@ -257,9 +233,9 @@ function Set-ScoopDB {
System.String
.OUTPUTS
System.Data.DataTable
The found database item(s).
The selected database item(s).
#>
function Find-ScoopDBItem {
function Select-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
@@ -288,10 +264,11 @@ function Find-ScoopDBItem {
[void]$dbAdapter.Fill($result)
}
end {
$dbCommand.Dispose()
$resultCopy = $result.Copy()
$dbAdapter.Dispose()
$db.Dispose()
return $result
# Use Write-Output to ensure PowerShell properly returns the DataTable
Write-Output $resultCopy -NoEnumerate
}
}
@@ -318,13 +295,13 @@ function Find-ScoopDBItem {
function Get-ScoopDBItem {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]
$Name,
[Parameter(Mandatory, Position = 1)]
[Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
[string]
$Bucket,
[Parameter(Position = 2)]
[Parameter(Position = 2, ValueFromPipelineByPropertyName)]
[string]
$Version
)
@@ -332,31 +309,46 @@ function Get-ScoopDBItem {
begin {
$db = Open-ScoopDB
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
$dbCommand = $db.CreateCommand()
$dbCommand.CommandType = [System.Data.CommandType]::Text
$dbAdapter.SelectCommand = $dbCommand
}
process {
$result = New-Object System.Data.DataTable
# Build query based on whether version is specified
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
if ($Version) {
$dbQuery += ' AND version = @Version'
} else {
$dbQuery += ' ORDER BY version DESC LIMIT 1'
}
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
$dbAdapter.SelectCommand = $dbCommand
}
process {
$dbCommand.Parameters.Clear()
# Add parameters for this specific query
$dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null
$dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null
if ($Version) {
$dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null
}
[void]$dbAdapter.Fill($result)
# Create a copy of the DataTable to avoid disposal issues
$resultCopy = $result.Copy()
# Use Write-Output to ensure PowerShell properly returns the DataTable
Write-Output $resultCopy -NoEnumerate
}
end {
$dbCommand.Dispose()
# Clean up all resources
$dbAdapter.Dispose()
$dbCommand.Dispose()
$db.Dispose()
return $result
}
}

View File

@@ -52,9 +52,11 @@ function Invoke-Extraction {
DestinationPath = Join-Path $Path $extractTo[$extracted]
ExtractDir = $extractDir[$extracted]
}
Write-Host "Extracting $([char]0x1b)[36m$(url_remote_filename $uri[$i])$([char]0x1b)[0m... " -NoNewline
Write-Host 'Extracting ' -NoNewline
Write-Host $(url_remote_filename $uri[$i]) -ForegroundColor Cyan -NoNewline
Write-Host ' ... ' -NoNewline
& $extractFn @fnArgs -Removal
Write-Host 'Done.' -ForegroundColor Green
Write-Host 'done.' -ForegroundColor Green
$extracted++
}
}

View File

@@ -391,7 +391,9 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
}
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
Write-Host "Loading $([char]0x1b)[36m$(url_remote_filename $url)$([char]0x1b)[0m from cache."
Write-Host 'Loading ' -NoNewline
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
Write-Host ' from cache.'
} else {
$download_finished = $false
# create aria2 input file content
@@ -423,7 +425,7 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
# handle aria2 console output
Write-Host 'Starting download with aria2...'
Write-Host 'Starting download with aria2 ...'
# Set console output encoding to UTF8 for non-ASCII characters printing
$oriConsoleEncoding = [Console]::OutputEncoding
@@ -464,7 +466,7 @@ function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $
warn $urlstxt_content
warn $aria2
Write-Host 'Fallback to default downloader...'
Write-Host 'Fallback to default downloader ...'
try {
foreach ($url in $urls) {
@@ -729,7 +731,9 @@ function check_hash($file, $hash, $app_name) {
return $true, $null
}
Write-Host "Checking hash of $([char]0x1b)[36m$(url_remote_filename $url)$([char]0x1b)[0m... " -NoNewline
Write-Host 'Checking hash of ' -NoNewline
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
Write-Host ' ... ' -NoNewline
$algorithm, $expected = get_hash $hash
if ($null -eq $algorithm) {
return $false, "Hash type '$algorithm' isn't supported."
@@ -751,7 +755,7 @@ function check_hash($file, $hash, $app_name) {
}
return $false, $msg
}
Write-Host 'OK.' -f Green
Write-Host 'ok.' -f Green
return $true, $null
}

View File

@@ -9,10 +9,10 @@
# a parameter should end with '='
# returns @(opts hash, remaining_args array, error string)
# NOTES:
# The first "--" or "--%" in $argv, if any, will terminate all options; any
# following arguments are treated as non-option arguments, even if
# they begin with a hyphen. The terminator token itself ("--" or "--%")
# will not be included. (POSIX-compatible)
# The first "--" in $argv, if any, will terminate all options; any
# following arguments are treated as non-option arguments, even if
# they begin with a hyphen. The "--" itself will not be included in
# the returned $opts. (POSIX-compatible)
function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) {
$opts = @{}; $rem = @()
@@ -32,7 +32,7 @@ function getopt([String[]]$argv, [String]$shortopts, [String[]]$longopts) {
if ($arg -is [Int]) { $rem += $arg; continue }
if ($arg -is [Decimal]) { $rem += $arg; continue }
if ($arg -eq '--' -or $arg -eq '--%') {
if ($arg -eq '--') {
if ($i -lt $argv.Length - 1) {
$rem += $argv[($i + 1)..($argv.Length - 1)]
}

View File

@@ -165,9 +165,9 @@ function Invoke-HookScript {
$script = $script.script
}
if ($script) {
Write-Host "Running $HookType script... " -NoNewline
Write-Host "Running $HookType script..." -NoNewline
Invoke-Command ([scriptblock]::Create($script -join "`r`n"))
Write-Host 'Done.' -ForegroundColor Green
Write-Host 'done.' -ForegroundColor Green
}
}
@@ -557,7 +557,7 @@ function test_running_process($app, $global) {
# Required to handle docker/for-win#12240
function New-DirectoryJunction($source, $target) {
# test if this script is being executed inside a docker container
if (Get-Service -Name cexecsvc -ErrorAction Ignore) {
if (Get-Service -Name cexecsvc -ErrorAction SilentlyContinue) {
cmd.exe /d /c "mklink /j `"$source`" `"$target`""
} else {
New-Item -Path $source -ItemType Junction -Value $target

View File

@@ -18,8 +18,8 @@ function url_manifest($url) {
$wc.Headers.Add('User-Agent', (Get-UserAgent))
$data = $wc.DownloadData($url)
$str = (Get-Encoding($wc)).GetString($data)
} catch [System.Management.Automation.MethodInvocationException] {
error $_.Exception.InnerException.Message
} catch [system.management.automation.methodinvocationexception] {
warn "error: $($_.exception.innerexception.message)"
} catch {
throw
}
@@ -172,38 +172,200 @@ function Get-SupportedArchitecture($manifest, $architecture) {
}
}
function Get-RelativePathCompat($from, $to) {
<#
.SYNOPSIS
Cross-platform compatible relative path function
.DESCRIPTION
Falls back to custom implementation for Windows PowerShell compatibility
#>
if ($PSVersionTable.PSVersion.Major -ge 6) {
# PowerShell Core/7+ - use built-in method
try {
return [System.IO.Path]::GetRelativePath($from, $to)
} catch {
# Fallback if method fails
}
}
# Windows PowerShell compatible implementation
$fromUri = New-Object System.Uri($from.TrimEnd('\') + '\')
$toUri = New-Object System.Uri($to)
if ($fromUri.Scheme -ne $toUri.Scheme) {
return $to # Cannot make relative path between different schemes
}
$relativeUri = $fromUri.MakeRelativeUri($toUri)
$relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())
return $relativePath -replace '/', '\'
}
function Find-HistoricalManifestInCache($app, $bucket, $requestedVersion) {
if (!(get_config USE_SQLITE_CACHE)) {
return $null
}
# Return null if bucket is null or empty
if (!$bucket) {
return $null
}
# Import database functions if not already loaded
if (!(Get-Command 'Get-ScoopDBItem' -ErrorAction SilentlyContinue)) {
. "$PSScriptRoot\database.ps1"
}
$dbResult = Get-ScoopDBItem -Name $app -Bucket $bucket -Version $requestedVersion
# Strictly follow DB contract: must be DataTable with at least one row
if (-not ($dbResult -is [System.Data.DataTable])) { return $null }
if ($dbResult.Rows.Count -eq 0) { return $null }
$manifestText = $dbResult.Rows[0]['manifest']
if ([string]::IsNullOrWhiteSpace($manifestText)) { return $null }
$manifestObj = $null
try { $manifestObj = $manifestText | ConvertFrom-Json -ErrorAction Stop } catch {}
$manifestVersion = if ($manifestObj -and $manifestObj.version) { $manifestObj.version } else { $requestedVersion }
return @{ ManifestText = $manifestText; version = $manifestVersion; source = "sqlite_exact_match" }
return $null
}
function Find-HistoricalManifestInGit($app, $bucket, $requestedVersion) {
# Only proceed if git history is enabled
if (!(get_config USE_GIT_HISTORY $true)) {
return $null
}
if (!$bucket) {
return $null
}
$bucketDir = Find-BucketDirectory $bucket -Root
if (!(Test-Path "$bucketDir\.git")) {
warn "Bucket '$bucket' is not a git repository. Cannot search historical versions."
return $null
}
$manifestPath = "$app.json"
$innerBucketDir = Find-BucketDirectory $bucket # Non-root path
if (-not (Test-Path $innerBucketDir -PathType Container)) {
warn "Could not find inner bucket directory for '$bucket' at '$innerBucketDir'."
return $null
}
$relativeManifestPath = Get-RelativePathCompat $bucketDir (Join-Path $innerBucketDir $manifestPath)
$relativeManifestPath = $relativeManifestPath -replace '\\', '/'
try {
# Prefer precise regex match on version line, fallback to -S literal
$pattern = '"version"\s*:\s*"' + [regex]::Escape($requestedVersion) + '"'
$commits = @()
$outG = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-G',$pattern,'--',$relativeManifestPath)
if ($outG) { $commits = @($outG | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
if ($commits.Count -eq 0) {
$searchLiteral = '"version": "' + $requestedVersion + '"'
$outS = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','-n','1','--format=%H','-S',$searchLiteral,'--',$relativeManifestPath)
if ($outS) { $commits = @($outS | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
}
if ($commits.Count -eq 0) { return $null }
$h = $commits[0]
# First try parent snapshot (latest state before change), then the change itself
foreach ($spec in @("$h^","$h")) {
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$spec`:$relativeManifestPath")
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
if ($content -is [Array]) { $content = $content -join "`n" }
try {
$obj = $content | ConvertFrom-Json -ErrorAction Stop
} catch { continue }
if ($obj -and $obj.version -eq $requestedVersion) {
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$spec" }
}
}
# Fallback: iterate recent commits that touched the version string and validate
$outAll = Invoke-Git -Path $bucketDir -ArgumentList @('log','--follow','--format=%H','-G',$pattern,'--',$relativeManifestPath)
$allCommits = @()
if ($outAll) { $allCommits = @($outAll | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) }
foreach ($c in $allCommits) {
$content = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$c`:$relativeManifestPath")
if (-not $content -or ($LASTEXITCODE -ne 0)) { continue }
if ($content -is [Array]) { $content = $content -join "`n" }
try { $obj = $content | ConvertFrom-Json -ErrorAction Stop } catch { continue }
if ($obj -and $obj.version -eq $requestedVersion) {
return @{ ManifestText = $content; version = $requestedVersion; source = "git_manifest:$c" }
}
}
return $null
} catch { return $null }
}
function Find-HistoricalManifest($app, $bucket, $version) {
# Orchestrates historical manifest lookup using available providers (DB → Git)
$result = $null
if (get_config USE_SQLITE_CACHE) {
$result = Find-HistoricalManifestInCache $app $bucket $version
if ($result) {
if ($result.ManifestText) {
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
return @{ path = $path; version = $result.version; source = $result.source }
}
return $result
}
}
if (get_config USE_GIT_HISTORY $true) {
$result = Find-HistoricalManifestInGit $app $bucket $version
if ($result) {
if ($result.ManifestText) {
$path = Write-ManifestToUserCache -App $app -ManifestText $result.ManifestText
return @{ path = $path; version = $result.version; source = $result.source }
}
return $result
}
}
return $null
}
function generate_user_manifest($app, $bucket, $version) {
# 'autoupdate.ps1' 'buckets.ps1' 'manifest.ps1'
$app, $manifest, $bucket, $null = Get-Manifest "$bucket/$app"
if ("$($manifest.version)" -eq "$version") {
return manifest_path $app $bucket
}
warn "Given version ($version) does not match manifest ($($manifest.version))"
warn "Attempting to generate manifest for '$app' ($version)"
# Try historical providers via orchestrator
$historicalResult = Find-HistoricalManifest $app $bucket $version
if ($historicalResult) { return $historicalResult.path }
# No historical manifest; try autoupdate if available
if (!($manifest.autoupdate)) {
abort "Could not find manifest for '$app@$version' and no autoupdate is available"
}
ensure (usermanifestsdir) | Out-Null
$manifest_path = "$(usermanifestsdir)\$app.json"
if (get_config USE_SQLITE_CACHE) {
$cached_manifest = (Get-ScoopDBItem -Name $app -Bucket $bucket -Version $version).manifest
if ($cached_manifest) {
$cached_manifest | Out-UTF8File $manifest_path
return $manifest_path
}
}
if (!($manifest.autoupdate)) {
abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'"
}
try {
Invoke-AutoUpdate $app $manifest_path $manifest $version $(@{ })
return $manifest_path
} catch {
Write-Host -ForegroundColor DarkRed "Could not install $app@$version"
warn "Autoupdate failed for '$app@$version'"
abort "Installation of '$app@$version' is not possible"
}
return $null
}
function url($manifest, $arch) { arch_specific 'url' $manifest $arch }
@@ -212,3 +374,16 @@ function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $
function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch }
function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch }
function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch }
# Helper: write manifest text to user manifests cache directory and return path
function Write-ManifestToUserCache {
param(
[Parameter(Mandatory=$true, Position=0)][string]$App,
[Parameter(Mandatory=$true, Position=1)][string]$ManifestText
)
ensure (usermanifestsdir) | Out-Null
$tempManifestPath = "$(usermanifestsdir)\$App.json"
$ManifestText | Out-UTF8File -FilePath $tempManifestPath
return $tempManifestPath
}

View File

@@ -30,6 +30,11 @@
# use_sqlite_cache: $true|$false
# Use SQLite database for caching. This is useful for speeding up 'scoop search' and 'scoop shim' commands.
#
# use_git_history: $true|$false
# Enable searching for specific versions in git history when installing apps with version specifiers.
# When enabled, Scoop will first search the bucket's git history for the exact version before falling back to autoupdate.
# (Default is $true)
#
# no_junction: $true|$false
# The 'current' version alias will not be used. Shims and shortcuts will point to specific version instead.
#
@@ -169,7 +174,7 @@ if (!$name) {
Write-Host "'$name' has been set to '$value'"
} else {
$value = get_config $name
if($null -eq $value) {
if ($null -eq $value) {
Write-Host "'$name' is not set"
} else {
if ($value -is [System.DateTime]) {

View File

@@ -4,7 +4,7 @@
# scoop install git
#
# To install a different version of the app
# (note that this will auto-generate the manifest using current version):
# (will search sqlite cache if enabled, then git history (if use_git_history is $true), then auto-generate manifest as fallback):
# scoop install gh@2.7.0
#
# To install an app from a manifest at a URL:

View File

@@ -140,7 +140,8 @@ function search_remotes($query) {
} | Where-Object { $_.results }
if ($results.count -gt 0) {
Write-Host "Results from other known buckets...`n(add them using 'scoop bucket add <bucket name>')"
Write-Host "Results from other known buckets...
(add them using 'scoop bucket add <bucket name>')"
}
$remote_list = @()
@@ -158,7 +159,7 @@ function search_remotes($query) {
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
Find-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
Select-ScoopDBItem $query -From @('name', 'binary', 'shortcut') |
Select-Object -Property name, version, bucket, binary |
ForEach-Object {
$list.Add([PSCustomObject]@{

View File

@@ -4,7 +4,7 @@
#
# To add a custom shim, use the 'add' subcommand:
#
# scoop shim add <shim_name> <command_path> [[--|--%] <args>...]
# scoop shim add <shim_name> <command_path> [<args>...]
#
# To remove shims, use the 'rm' subcommand: (CAUTION: this could remove shims added by an app manifest)
#
@@ -25,15 +25,11 @@
# Options:
# -g, --global Manipulate global shim(s)
#
# HINT: The FIRST terminator token ('--' or PowerShell '--%'), if any, is treated as the option
# terminator and will NOT be included; everything after it is passed to the shim.
# So if you want to pass arguments like '-g' or '--global' to the shim, put them after a '--' or '--%'.
# Examples:
# POSIX-style: scoop shim add myapp 'D:\path\myapp.exe' '--' myapp_args --global
# PowerShell: scoop shim add myapp D:\path\myapp.exe --% myapp_args --global
# Notes:
# - In PowerShell, '--' should be quoted to avoid parsing.
# - '--%' disables PowerShell parsing of the remainder; pass literals as needed.
# HINT: The FIRST double-hyphen '--', if any, will be treated as the POSIX-style command option terminator
# and will NOT be included in arguments, so if you want to pass arguments like '-g' or '--global' to
# the shim, put them after a '--'. Note that in PowerShell, you must use a QUOTED '--', e.g.,
#
# scoop shim add myapp 'D:\path\myapp.exe' '--' myapp_args --global
param($SubCommand)
@@ -116,10 +112,13 @@ switch ($SubCommand) {
}
}
if ($commandPath -and (Test-Path $commandPath)) {
Write-Host "Adding $(if ($global) { 'global' } else { 'local' }) shim $([char]0x1b)[36m$shimName$([char]0x1b)[0m..."
Write-Host "Adding $(if ($global) { 'global' } else { 'local' }) shim " -NoNewline
Write-Host $shimName -ForegroundColor Cyan -NoNewline
Write-Host '...'
shim $commandPath $global $shimName $commandArgs
} else {
error "Command path does not exist: $([char]0x1b)[36m$($other[1])$([char]0x1b)[31m"
Write-Host "ERROR: Command path does not exist: " -ForegroundColor Red -NoNewline
Write-Host $($other[1]) -ForegroundColor Cyan
exit 3
}
}
@@ -134,7 +133,8 @@ switch ($SubCommand) {
}
if ($failed) {
$failed | ForEach-Object {
error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$_$([char]0x1b)[31m"
Write-Host "ERROR: $(if ($global) { 'Global' } else {'Local' }) shim not found: " -ForegroundColor Red -NoNewline
Write-Host $_ -ForegroundColor Cyan
}
exit 3
}
@@ -147,7 +147,7 @@ switch ($SubCommand) {
$pattern = $_
[void][Regex]::New($pattern)
} catch {
error "Invalid pattern: $([char]0x1b)[35m$pattern$([char]0x1b)[31m"
error "Invalid pattern: $([char]0x1b)[35m$pattern$([char]0x1b)[0m"
exit 1
}
}
@@ -171,9 +171,11 @@ switch ($SubCommand) {
if ($shimPath) {
Get-ShimInfo $shimPath
} else {
error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$shimName$([char]0x1b)[31m"
Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline
Write-Host $shimName -ForegroundColor Cyan
if (Get-ShimPath $shimName (!$global)) {
Write-Host "But a $(if ($global) { 'local' } else { 'global' }) shim exists, run 'scoop shim info $shimName$(if (!$global) { ' --global' })' to show its info."
Write-Host "But a $(if ($global) { 'local' } else {'global' }) shim exists, " -NoNewline
Write-Host "run 'scoop shim info $shimName$(if (!$global) { ' --global' })' to show its info"
exit 2
}
exit 3
@@ -185,7 +187,9 @@ switch ($SubCommand) {
if ($shimPath) {
$shimInfo = Get-ShimInfo $shimPath
if ($null -eq $shimInfo.Alternatives) {
error "No alternatives of $([char]0x1b)[36m$shimName$([char]0x1b)[31m found."
Write-Host 'ERROR: No alternatives of ' -ForegroundColor Red -NoNewline
Write-Host $shimName -ForegroundColor Cyan -NoNewline
Write-Host ' found.' -ForegroundColor Red
exit 2
}
$shimInfo.Alternatives = $shimInfo.Alternatives.Split(' ')
@@ -194,10 +198,18 @@ switch ($SubCommand) {
}
$selected = $Host.UI.PromptForChoice("Alternatives of '$shimName' command", "Please choose one that provides '$shimName' as default:", $altApps, 0)
if ($selected -eq 0) {
Write-Host "$([char]0x1b)[36m$shimName$([char]0x1b)[0m is already from $([char]0x1b)[33m$($shimInfo.Source)$([char]0x1b)[0m, nothing changed."
Write-Host 'INFO: ' -ForegroundColor Blue -NoNewline
Write-Host $shimName -ForegroundColor Cyan -NoNewline
Write-Host ' is already from ' -NoNewline
Write-Host $shimInfo.Source -ForegroundColor DarkYellow -NoNewline
Write-Host ', nothing changed.'
} else {
$newApp = $shimInfo.Alternatives[$selected]
Write-Host "Use $([char]0x1b)[36m$shimName$([char]0x1b)[0m from $([char]0x1b)[33m$newApp$([char]0x1b)[0m as default... " -NoNewline
Write-Host 'Use ' -NoNewline
Write-Host $shimName -ForegroundColor Cyan -NoNewline
Write-Host ' from ' -NoNewline
Write-Host $newApp -ForegroundColor DarkYellow -NoNewline
Write-Host ' as default...' -NoNewline
$pathNoExt = strip_ext $shimPath
'', '.shim', '.cmd', '.ps1' | ForEach-Object {
$oldShimPath = "$pathNoExt$_"
@@ -209,12 +221,14 @@ switch ($SubCommand) {
}
}
}
Write-Host 'Done.'
Write-Host 'done.'
}
} else {
error "$(if ($global) { 'Global' } else { 'Local' }) shim not found: $([char]0x1b)[36m$shimName$([char]0x1b)[31m"
Write-Host "ERROR: $(if ($global) { 'Global' } else { 'Local' }) shim not found: " -ForegroundColor Red -NoNewline
Write-Host $shimName -ForegroundColor Cyan
if (Get-ShimPath $shimName (!$global)) {
Write-Host "But a $(if ($global) { 'local' } else { 'global' }) shim exists, run 'scoop shim alter $shimName$(if (!$global) { ' --global' })' to alternate its source."
Write-Host "But a $(if ($global) { 'local' } else {'global' }) shim exists, " -NoNewline
Write-Host "run 'scoop shim alter $shimName$(if (!$global) { ' --global' })' to alternate its source"
exit 2
}
exit 3

View File

@@ -10,7 +10,6 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\system.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1" # url_filename
. "$PSScriptRoot\..\lib\shortcuts.ps1"
. "$PSScriptRoot\..\lib\psmodules.ps1"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'

View File

@@ -81,32 +81,13 @@ Describe 'getopt' -Tag 'Scoop' {
$opt, $rem, $err = getopt '--long-arg', '--' '' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.'long-arg' | Should -BeTrue
$rem | Should -BeNullOrEmpty
}
It 'handles remainder args after the option terminator' {
$rem[0] | Should -BeNullOrEmpty
$opt, $rem, $err = getopt '--long-arg', '--', '-x', '-y' 'xy' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.'long-arg' | Should -BeTrue
$opt.Keys | Should -Not -Contain 'x'
$opt.Keys | Should -Not -Contain 'y'
$opt.'x' | Should -BeNullOrEmpty
$opt.'y' | Should -BeNullOrEmpty
$rem[0] | Should -Be '-x'
$rem[1] | Should -Be '-y'
}
It 'handles PowerShell stop-parsing token' {
$opt, $rem, $err = getopt '--long-arg', '--%' '' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.'long-arg' | Should -BeTrue
$rem | Should -BeNullOrEmpty
}
It 'handles remainder args after PowerShell stop-parsing token' {
$opt, $rem, $err = getopt @('--long-arg', '--%', '--from', 'there', '--to', 'here') '' 'long-arg'
$err | Should -BeNullOrEmpty
$opt.Keys | Should -Not -Contain 'from'
$opt.Keys | Should -Not -Contain 'to'
$rem | Should -Be @('--from', 'there', '--to', 'here')
}
}

View File

@@ -1,15 +1,25 @@
BeforeAll {
. "$PSScriptRoot\..\lib\json.ps1"
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\buckets.ps1"
. "$PSScriptRoot\..\lib\database.ps1"
. "$PSScriptRoot\..\lib\autoupdate.ps1"
}
Describe 'JSON parse and beautify' -Tag 'Scoop' {
Context 'Parse JSON' {
It 'success with valid json' {
{ parse_json "$PSScriptRoot\fixtures\manifest\wget.json" } | Should -Not -Throw
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\wget.json"
$parsed | Should -Not -Be $null
}
It 'fails with invalid json' {
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Throw
It 'returns null and warns with invalid json' {
Mock warn {}
{ parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json" } | Should -Not -Throw
$parsed = parse_json "$PSScriptRoot\fixtures\manifest\broken_wget.json"
$parsed | Should -Be $null
Should -Invoke -CommandName warn -Times 1
}
}
Context 'Beautify JSON' {
@@ -84,3 +94,144 @@ Describe 'Manifest Validator' -Tag 'Validator' {
$validator.Errors | Select-Object -Last 1 | Should -Match 'Required properties are missing from object: version\.'
}
}
Describe 'Get-RelativePathCompat' -Tag 'Scoop' {
It 'returns relative path for child path' {
$from = 'C:\root\bucket'
$to = 'C:\root\bucket\foo\bar.json'
Get-RelativePathCompat $from $to | Should -Be 'foo\bar.json'
}
It 'returns original when different drive/scheme' {
$from = 'C:\root\bucket'
$to = 'D:\other\file.json'
Get-RelativePathCompat $from $to | Should -Be $to
}
}
Describe 'Find-HistoricalManifestInCache' -Tag 'Scoop' {
It 'returns $null when sqlite cache disabled' {
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.0.0'
$result | Should -Be $null
}
It 'returns manifest text and version when cache has exact match' {
$tempUM = Join-Path $env:TEMP 'ScoopTestsUM'
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Get-ScoopDBItem {
$dt = New-Object System.Data.DataTable
[void]$dt.Columns.Add('manifest')
$row = $dt.NewRow()
$row['manifest'] = '{"version":"1.2.3"}'
[void]$dt.Rows.Add($row)
Write-Output $dt -NoEnumerate
}
Mock ensure {}
$result = Find-HistoricalManifestInCache 'foo' 'main' '1.2.3'
$result | Should -Not -BeNullOrEmpty
$result.version | Should -Be '1.2.3'
$result.ManifestText | Should -Match '"version":"1.2.3"'
}
}
Describe 'Find-HistoricalManifestInGit' -Tag 'Scoop' {
It 'returns $null when git history search disabled' {
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $false }
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
$result | Should -Be $null
}
It 'returns manifest text on version match' {
$bucketRoot = 'C:\b\root'
$innerBucket = 'C:\b\root\bucket'
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock get_config -ParameterFilter { $name -eq 'use_git_history' } { $true }
Mock Find-BucketDirectory -ParameterFilter { $Root } { $bucketRoot }
Mock Find-BucketDirectory -ParameterFilter { -not $Root } { $innerBucket }
Mock Test-Path -ParameterFilter { $Path -eq (Join-Path $bucketRoot '.git') } { $true }
Mock Test-Path -ParameterFilter { $Path -eq $innerBucket -and $PathType -eq 'Container' } { $true }
# Behavior-oriented mocks: using HEAD should yield a wrong version
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' -and $ArgumentList[1] -like 'HEAD*' } { $global:LASTEXITCODE = 0; return '{"version":"2.0.0"}' }
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'show' } { $global:LASTEXITCODE = 0; return '{"version":"1.0.0"}' }
Mock Invoke-Git -ParameterFilter { $ArgumentList[0] -eq 'log' } { @('abcdef0123456789') }
$result = Find-HistoricalManifestInGit 'foo' 'main' '1.0.0'
$result | Should -Not -BeNullOrEmpty
$result.version | Should -Be '1.0.0'
$result.ManifestText | Should -Match '"version":"1.0.0"'
}
}
Describe 'generate_user_manifest (history-aware)' -Tag 'Scoop' {
It 'returns manifest_path when versions match' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='1.0.0' }, 'main', $null }
Mock manifest_path { 'C:\path\foo.json' }
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\path\foo.json'
}
It 'prefers history orchestrator hit (cache) when enabled' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Find-HistoricalManifest { @{ path = 'C:\cache\foo.json'; version = '1.0.0'; source='sqlite_exact_match' } }
Mock info {}
Mock warn {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\cache\foo.json'
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
}
It 'falls back to git history when cache misses' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
Mock Find-HistoricalManifest { @{ path = 'C:\git\foo.json'; version = '1.0.0'; source='git_manifest:hash' } }
Mock info {}
Mock warn {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be 'C:\git\foo.json'
Should -Invoke -CommandName Find-HistoricalManifest -Times 1
}
It 'uses autoupdate when no history found and autoupdate exists' {
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock ensure {}
Mock usermanifestsdir { $umdir }
Mock Invoke-AutoUpdate {}
$p = generate_user_manifest 'foo' 'main' '1.0.0'
$p | Should -Be (Join-Path $umdir 'foo.json')
}
It 'on autoupdate failure aborts with concise message' {
$umdir = Join-Path $env:TEMP 'ScoopTestsUM'
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0'; autoupdate=@{} }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock ensure {}
Mock usermanifestsdir { $umdir }
Mock Invoke-AutoUpdate { throw 'fail' }
Mock warn {}
Mock info {}
Mock Write-Host {}
Mock abort { throw 'aborted' }
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
}
It 'aborts when no history and no autoupdate' {
Mock Get-Manifest -ParameterFilter { $app -eq 'main/foo' } { 'foo', [pscustomobject]@{ version='2.0.0' }, 'main', $null }
Mock get_config -ParameterFilter { $name -eq 'use_sqlite_cache' } { $false }
Mock Find-HistoricalManifest { $null }
Mock warn {}
Mock info {}
Mock abort { throw 'aborted' }
{ generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
}
}