38 Commits

Author SHA1 Message Date
Hsiao-nan Cheung
5d46f67e34 Merge branch 'develop' into feature/install_via_git_history 2025-09-26 23:51:08 +08:00
abgox
4c55e7aebd feat(scoop-uninstall): Allow access to $bucket in uninstall scripts (#6380) 2025-09-26 17:14:09 +08:00
Hsiao-nan Cheung
db8d554f80 fix(scoop-update): Force sync tags w/ remote branch while scoop update (#6439) 2025-09-26 15:20:33 +08:00
z-Fng
b592b38abd feat(install): Add separator at the end of notes, highlight suggestions (#6418) 2025-09-24 16:52:03 +08:00
z-Fng
25d35007c6 fix(core|manifest): Avoid error when searching non-existent 'deprecated' directory (#6471) 2025-09-24 14:45:01 +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
z-Fng
04b7ce79c7 fix(scoop-version): Fix logic error caused by missing brackets (#6463) 2025-08-27 17:46:16 +08: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
z-Fng
ff38652e01 fix(buckets|scoop-info): Switch git log date format to ISO 8601 to avoid locale issues (#6446) 2025-08-21 14:18:33 +08:00
HUMORCE
c9f0728ff7 fix(autoupdate): Use origin url to handle urls with fragment in github mode (#6455) 2025-08-20 13:59:47 +08:00
AkariiinMKII
691c23330c fix(scoop-download): Fix function nightly_version not defined error (#6386) 2025-08-14 17:51:19 +08:00
Hsiao-nan Cheung
9f62e55862 Merge branch 'develop' into feature/install_via_git_history 2025-08-12 18:29:52 +08:00
Hsiao-nan Cheung
b588a06e41 chore(release): Bump to version 0.5.3 (resync) (#6436) 2025-08-12 18:18:31 +08:00
Hsiao-nan Cheung
9246250808 Merge branch 'master' into develop 2025-08-12 15:21:48 +08:00
Hsiao-nan Cheung
ebd8c036fa chore(release): Bump to version 0.5.3 (#6257)
* fix (decompress): `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty (#6092)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* refactor(download): Move download-related functions to 'download.ps1' (#6095)

* fix(download): Fallback to default downloader when aria2 fails (#4292)

* fix(commands): Handling broken aliases (#6141)

* fix(shim): properly check `wslpath`/`cygpath` command first (#6114)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-bucket): Add missing import for `no_junction` envs (#6181)

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>

* docs(chglog): Update to 0.5.3 (#6258)

* perf(shim): Update kiennq-shim to v3.1.2 (#6261)

* fix(decompress): Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config (#6327)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-uninstall): Fix uninstaller does not gain Global state (#6430)

* global arg

* changelog

* refactor(Get-Manifest): Select actual source for manifest (#6142)

* first step

* Revert "first step"

This reverts commit c5907c3e25.

* refactor(Get-Manifest): Select actual source for installed manifest

* rework sub-commands, `scoop-depends` is NOT working at this stage

* URI manifest

* opt

* deprecated manifest

* source of manifests

* source of manifest pt2

- Mark URI(path/URL/UNC/etc.) query as standalone manifest
- Drop `installed` and `available update` items for [query] and [installed] are different sources.

* remove variable preventing I forget it

* scoop-info: fix source of manifest on bucket

* fix `scoop-depends`

* Fix Standalone and Source detection

* fix global install

* Fix scoop-cat, scoop-home

- Query for remote manifest

* scoop-list: info +deprecated

* manifest: Fix first selected manifest

* gramma..

* Fix 61b3259

* length

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false (#6431)

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true

* CHANGELOG

* feat(autoupdate): GitHub predefined hashes support (#6416)

* feat(autoupdate): predefined hash case for GitHub

- Remove `sha256:` prefix in `format_hash()`
- Add GitHub support in `get_hash_for_app()`

Close #6381

* doc(chglog): GitHub auto hash update

* fix(autoupdate): remove prefix only

* docs(CHANGELOG): Update to 0.5.3 (#6432)

* docs(CHANGELOG): Update to 0.5.3

* 6416

---------

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Olav Rønnestad Birkeland <6450056+o-l-a-v@users.noreply.github.com>
Co-authored-by: kiennq <kien.n.quang@gmail.com>
Co-authored-by: HUMORCE <humorce@outlook.com>
Co-authored-by: Ryan <sitiom@proton.me>
Co-authored-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Bassel Rachid <101208715+basselworkforce@users.noreply.github.com>
Co-authored-by: Wordless Echo <wordless@echo.moe>
2025-08-11 00:38:38 +00: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
15 changed files with 421 additions and 47 deletions

View File

@@ -1,8 +1,25 @@
## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/v0.5.3...develop)
### Features
- **install:** Add support for historical manifests ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370))
- **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))
### Bug Fixes
- **scoop-update**: Force sync tags w/ remote branch while scoop update ([#6439](https://github.com/ScoopInstaller/Scoop/issues/6439))
- **scoop-download:** Fix function `nightly_version` not defined error ([#6386](https://github.com/ScoopInstaller/Scoop/issues/6386))
- **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))
- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11
### Features
**autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
- **autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
### Bug Fixes

View File

@@ -20,8 +20,8 @@ switch ($subCommand) {
}
({ $subCommand -in @('-v', '--version') }) {
Write-Host 'Current Scoop version:'
if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline')
if ((Test-GitAvailable) -and (Test-Path "$PSScriptRoot\..\.git") -and ((get_config SCOOP_BRANCH 'master') -ne 'master')) {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
} else {
$version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md"
Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline
@@ -31,9 +31,9 @@ switch ($subCommand) {
Get-LocalBucket | ForEach-Object {
$bucketLoc = Find-BucketDirectory $_ -Root
if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) {
if ((Test-GitAvailable) -and (Test-Path "$bucketLoc\.git")) {
Write-Host "'$_' bucket:"
Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline')
Invoke-Git -Path $bucketLoc -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
Write-Host ''
}
}

View File

@@ -3,6 +3,8 @@
function format_hash([String] $hash) {
$hash = $hash.toLower()
# Workaround for GitHub API:
# `"digest": "sha256:<SHA256_STRING>"`
if ($hash -like 'sha256:*') {
$hash = $hash.Substring(7) # Remove prefix 'sha256:'
}
@@ -209,13 +211,14 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hash = $null
$hashmode = $config.mode
$originurl = strip_fragment $url
$basename = [System.Web.HttpUtility]::UrlDecode((url_remote_filename($url)))
$substitutions = $substitutions.Clone()
$substitutions.Add('$url', (strip_fragment $url))
$substitutions.Add('$baseurl', (strip_filename (strip_fragment $url)).TrimEnd('/'))
$substitutions.Add('$url', $originurl)
$substitutions.Add('$baseurl', (strip_filename $originurl).TrimEnd('/'))
$substitutions.Add('$basename', $basename)
$substitutions.Add('$urlNoExt', (strip_ext (strip_fragment $url)))
$substitutions.Add('$urlNoExt', (strip_ext $originurl))
$substitutions.Add('$basenameNoExt', (strip_ext $basename))
debug $substitutions
@@ -297,7 +300,7 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
}
'github' {
$hashfile_url = "https://api.github.com/repos/$($matches['owner'])/$($matches['repo'])/releases"
$hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $url + "')].digest")
$hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $originurl + "')].digest")
}
}

View File

@@ -108,7 +108,7 @@ function list_buckets {
$path = Find-BucketDirectory $_ -Root
if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) {
$bucket.Source = Invoke-Git -Path $path -ArgumentList @('config', 'remote.origin.url')
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aD', '-n', '1') | Get-Date
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aI', '-n', '1') | Get-Date
} else {
$bucket.Source = friendly_path $path
$bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime

View File

@@ -555,7 +555,7 @@ function app_status($app, $global) {
$status.hold = ($install_info.hold -eq $true)
$deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
$status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse).FullName
$status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
$manifest = manifest $app $install_info.bucket $install_info.url
$status.removed = (!$manifest)

View File

@@ -264,9 +264,11 @@ function Select-ScoopDBItem {
[void]$dbAdapter.Fill($result)
}
end {
$resultCopy = $result.Copy()
$dbAdapter.Dispose()
$db.Dispose()
return $result
# Use Write-Output to ensure PowerShell properly returns the DataTable
Write-Output $resultCopy -NoEnumerate
}
}
@@ -293,13 +295,13 @@ function Select-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
)
@@ -307,28 +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
$dbCommand.Parameters.AddWithValue('@Version', $Version) | 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 {
# Clean up all resources
$dbAdapter.Dispose()
$dbCommand.Dispose()
$db.Dispose()
return $result
}
}

View File

@@ -359,6 +359,7 @@ function show_notes($manifest, $dir, $original_dir, $persist_dir) {
Write-Output 'Notes'
Write-Output '-----'
Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }))
Write-Output '-----'
}
}
@@ -419,7 +420,7 @@ function show_suggestions($suggested) {
}
if (!$fulfilled) {
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'."
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." -ForegroundColor DarkYellow
}
}
}

View File

@@ -69,7 +69,7 @@ function Get-Manifest($app) {
$manifest = manifest $app $bucket
if (!$manifest) {
$deprecated_dir = (Find-BucketDirectory -Name $bucket -Root) + '\deprecated'
$manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse).FullName
$manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
}
}
}
@@ -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.
#
@@ -166,7 +171,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

@@ -25,6 +25,7 @@
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1"
. "$PSScriptRoot\..\lib\install.ps1" # 'nightly_version'
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}

View File

@@ -122,7 +122,7 @@ if ($manifest.depends) {
if (Test-Path $manifest_file) {
if (Get-Command git -ErrorAction Ignore) {
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#'
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aI#%an', $manifest_file) 2> $null) -Split '#'
}
if ($gitinfo) {
$item.'Updated at' = $gitinfo[0] | Get-Date

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

@@ -58,6 +58,7 @@ if (!$apps) { exit 0 }
$manifest = installed_manifest $app $version $global
$install = install_info $app $version $global
$architecture = $install.architecture
$bucket = $install.bucket
Invoke-HookScript -HookType 'pre_uninstall' -Manifest $manifest -Arch $architecture

View File

@@ -136,7 +136,7 @@ function Sync-Scoop {
# reset branch HEAD
Invoke-Git -Path $currentdir -ArgumentList @('reset', '--hard', "origin/$configBranch", '-q')
} else {
Invoke-Git -Path $currentdir -ArgumentList @('pull', '-q')
Invoke-Git -Path $currentdir -ArgumentList @('pull', '--tags', '--force', '-q')
}
$res = $lastexitcode

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
}
}