Skip to content

Conversation

@Lutra-Fs
Copy link
Member

@Lutra-Fs Lutra-Fs commented May 26, 2025

Description

Motivation and Context

Closes #3894

Relates to #XXXX

How Has This Been Tested?

Checklist:

  • I have read the Contributing Guide.
  • I have ensured that I am targeting the develop branch.
  • I have updated the documentation accordingly.
  • I have updated the tests accordingly.
  • I have added an entry in the CHANGELOG.

Summary by CodeRabbit

  • New Features

    • Install specific historical versions by searching local cache and bucket git history; manifest caching and cross-platform relative-path handling added. New setting use_git_history (default: true).
  • Improvements

    • Prefer historical manifests before autoupdate; improved reliability of database queries, output handling, parameter binding, and resource cleanup; clearer failure messages when manifests can't be obtained.
  • Documentation

    • Documented use_git_history and updated changelog.
  • Tests

    • Added tests for history-based installs, relative paths, JSON parsing, and manifest-generation flows.

- 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.
@Lutra-Fs Lutra-Fs requested a review from Copilot May 26, 2025 10:53
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a historical manifest search feature to the app installation process, enabling Scoop to search a bucket’s git history for specific app versions before falling back to automatic manifest generation.

  • Updated installation command comments to document the new historical search fallback.
  • Introduced configuration options and functions (e.g., Get-HistoricalManifest) to support historical manifest retrieval.
  • Added fallback logic in autoupdate and installation routines to use historical manifests when available.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
libexec/scoop-install.ps1 Updated comments to reflect search order: git history first, then auto-generation.
libexec/scoop-config.ps1 Added configuration option for git history usage.
lib/manifest.ps1 Implemented Get-HistoricalManifest and added detailed git history search logic.
lib/install.ps1 Wrapped installer calls in a try/catch to improve error handling and fallback behavior.

@Lutra-Fs
Copy link
Member Author

Lutra-Fs commented May 26, 2025

~
❯ scoop install [email protected]
WARN  Given version (2.30.1) does not match manifest (2.30.3)
INFO  Searching for version '2.30.1' in git history...
INFO  Found 20 historical versions for 1password-cli so far...
INFO  Found 40 historical versions for 1password-cli so far...
INFO  Found 60 historical versions for 1password-cli so far...
INFO  Found 80 historical versions for 1password-cli so far...
INFO  Found 93 distinct historical versions for '1password-cli'. Finding best match for '2.30.1'...
INFO  Could not find a suitable match for '2.30.1' for app '1password-cli'. Available (latest 10): 2.31.0, 2.30.3, 2.30.3, 2.30.0, 2.29.0, 2.28.0, 2.27.0, 2.26.1, 2.26.0, 2.25.1
WARN  No historical version found. Attempting to generate manifest for '1password-cli' (2.30.1)
'1password-cli' does not have autoupdate capability and no historical version found
couldn't find manifest for '[email protected]'

~ took 4s
❯ scoop install [email protected]
WARN  Given version (2.26.0) does not match manifest (2.30.3)
INFO  Searching for version '2.26.0' in git history...
INFO  Exact version '2.26.0' for '1password-cli' found in manifest (commit 64c47b48396a34dfec536b10cc27bc1df03be757).
INFO  Using historical manifest for '1password-cli' version '2.26.0' from git_manifest:64c47b48396a34dfec536b10cc27bc1df03be757
WARN  Scoop uses 'aria2c' for multi-connection downloads.
WARN  Should it cause issues, run 'scoop config aria2-enabled false' to disable it.
WARN  To disable this warning, run 'scoop config aria2-warning-enabled false'.
Installing '1password-cli' (2.26.0) [64bit] from 'D:\Applications\Scoop\workspace\1password-cli.json'
Starting download with aria2 ...
Download: Download Results:
Download: gid   |stat|avg speed  |path/URI
Download: ======+====+===========+=======================================================
Download: 5efff9|OK  |   4.7MiB/s|D:/Applications/Scoop/cache/1password-cli#2.26.0#9c93182.zip
Download: Status Legend:
Download: (OK):download completed.
Checking hash of op_windows_amd64_v2.26.0.zip ... ok.
Extracting op_windows_amd64_v2.26.0.zip ... done.
Linking D:\Applications\Scoop\apps\1password-cli\current => D:\Applications\Scoop\apps\1password-cli\2.26.0
Creating shim for 'op'.
'1password-cli' (2.26.0) was installed successfully!
Notes
-----
1Password CLI v2 completely changes the commands schema! Either migrate your setup, following
instructions on https://developer.1password.com/docs/cli/upgrade/#step-2-update-your-scripts
or install 1Password CLI v1 from the Versions bucket.

~ took 4s
❯ scoop install [email protected]
WARN  Given version (2.30) does not match manifest (2.30.3)
INFO  Searching for version '2.30' in git history...
INFO  Found 20 historical versions for 1password-cli so far...
INFO  Found 40 historical versions for 1password-cli so far...
INFO  Found 60 historical versions for 1password-cli so far...
INFO  Found 80 historical versions for 1password-cli so far...
INFO  Found 93 distinct historical versions for '1password-cli'. Finding best match for '2.30'...
INFO  Best match for '2.30' is '2.30.3' (commit 7e23c2015521093f8dc762f21020ff31a1e7efcc).
INFO  Using historical manifest for '1password-cli' version '2.30.3' from git_best_match:7e23c2015521093f8dc762f21020ff31a1e7efcc
WARN  Scoop uses 'aria2c' for multi-connection downloads.
WARN  Should it cause issues, run 'scoop config aria2-enabled false' to disable it.
WARN  To disable this warning, run 'scoop config aria2-warning-enabled false'.
Installing '1password-cli' (2.30.3) [64bit] from 'D:\Applications\Scoop\workspace\1password-cli.json'
Loading op_windows_amd64_v2.30.3.zip from cache.
Checking hash of op_windows_amd64_v2.30.3.zip ... ok.
Extracting op_windows_amd64_v2.30.3.zip ... done.
Linking D:\Applications\Scoop\apps\1password-cli\current => D:\Applications\Scoop\apps\1password-cli\2.30.3
Creating shim for 'op'.
'1password-cli' (2.30.3) was installed successfully!
Notes
-----
1Password CLI v2 completely changes the commands schema! Either migrate your setup, following
instructions on https://developer.1password.com/docs/cli/upgrade/#step-2-update-your-scripts
or install 1Password CLI v1 from the Versions bucket.
Due to https://www.1password.community/discussions/developers/1password-cli-not-working/155988,
It has been pinned to 2.30.3 temporarily.

@Lutra-Fs
Copy link
Member Author

One of the current implementation's issue is that when user provided a approximate version it will search all the git log entry which is a major performance bottleneck

@niheaven
Copy link
Member

niheaven commented Jun 3, 2025

The best way to search historical version is using sqlite feature enabled, and you should search the database first, if not found, use git history.

@Lutra-Fs
Copy link
Member Author

Lutra-Fs commented Jun 4, 2025

The best way to search historical version is using sqlite feature enabled, and you should search the database first, if not found, use git history.

Will implement the SQLite version when I am available!

@niheaven
Copy link
Member

niheaven commented Jun 4, 2025

It is there already, so you should combine these two methods.

Lutra-Fs added 11 commits July 20, 2025 20:53
- 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
- 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.
- Fix code style issues to pass tests
- Maintain existing functionality without changes
- Change '3.7.x' to '3.12.x' to match the command '[email protected]'
- Addresses GitHub Copilot's documentation consistency feedback
- 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
- 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
- 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.
The try-catch block addition was a general error handling improvement
that belongs in a separate PR, not in this git history feature PR.
… 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.
- 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
@Lutra-Fs Lutra-Fs force-pushed the feature/install_via_git_history branch from c51e52e to 4585f50 Compare July 21, 2025 00:29
@Lutra-Fs
Copy link
Member Author

Lutra-Fs commented Jul 22, 2025

The best way to search historical version is using sqlite feature enabled, and you should search the database first, if not found, use git history.

Hi @niheaven
I have implement the db source, now the order is

(if USE_GIT_HISTORY)
    (if USE_SQLITE_CACHE)
    SQLite Cache
    (else/failed)
    Git log --follow
(else/failed)
auto update with version variable

@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds historical manifest lookup (SQLite cache and git history), integrates it into manifest generation before autoupdate, refactors DB command lifecycle and parameter binding, documents a use_git_history config, updates tests, and adds a changelog entry.

Changes

Cohort / File(s) Summary
Changelog & installer comment
CHANGELOG.md, libexec/scoop-install.ps1
Add "install: Add support for historical manifests by git history" entry; update installer comment to describe lookup order: DB → git history → autoupdate.
Database handling
lib/database.ps1
Select-ScoopDBItem/Get-ScoopDBItem now emit a copied DataTable via Write-Output -NoEnumerate; Get-ScoopDBItem creates/reuses a dedicated DB command in Begin, clears/adds parameters in Process, and disposes adapter/command/DB in End; parameters Name, Bucket, Version, and Pattern gain ValueFromPipelineByPropertyName.
Manifest resolution (historical lookup)
lib/manifest.ps1
Add Get-RelativePathCompat, Find-HistoricalManifestInCache, Find-HistoricalManifestInGit, Find-HistoricalManifest, and Write-ManifestToUserCache; generate_user_manifest now prefers cache → git → autoupdate and writes history hits to user cache.
Config docs
libexec/scoop-config.ps1
Document use_git_history setting (defaults to true); minor whitespace formatting tweak.
Tests
test/Scoop-Manifest.Tests.ps1
Load additional libs; add/update tests for JSON parsing, Get-RelativePathCompat, cache/git historical lookups, and history-aware generate_user_manifest flows (hits, fallbacks, failures).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Installer as "scoop install"
  participant Gen as "generate_user_manifest"
  participant Cache as "SQLite Cache"
  participant Git as "Git History"
  participant AU as "Autoupdate"

  User->>Installer: install app@version
  Installer->>Gen: resolve manifest for app@version

  rect rgba(220,240,255,0.35)
    note right of Gen: Historical lookup (preferred)
    Gen->>Cache: Find-HistoricalManifestInCache(app,bucket,version)
    alt Cache hit
      Cache-->>Gen: manifest text + version
      Gen->>Installer: Write-ManifestToUserCache -> manifest path
    else Cache miss
      Gen->>Git: Find-HistoricalManifestInGit(app,bucket,version)
      alt Git hit
        Git-->>Gen: manifest text + version
        Gen->>Installer: Write-ManifestToUserCache -> manifest path
      else Git miss
        Gen-->>Installer: no historical manifest found
      end
    end
  end

  opt Fallback to autoupdate
    Gen->>AU: Invoke-AutoUpdate(app)
    alt autoupdate success
      AU-->>Gen: generated manifest path
      Gen-->>Installer: manifest path
    else autoupdate fails
      Gen-->>Installer: abort with installation failure message
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • lib/database.ps1: DB command lifecycle, parameter binding changes, and correctness of returning a copied DataTable without disposal issues.
    • lib/manifest.ps1: git-history parsing logic, path resolution (Get-RelativePathCompat), cache vs. git orchestration, and correctness/atomicity of Write-ManifestToUserCache.
    • Tests: many new mocks and branches — verify mocks reflect real behavior and config flag handling (use_git_history, cache enabled).

Poem

"I hop through commits where old manifests hide,
I nibble cached crumbs and fetch from git-side.
When autoupdate stumbles, I scout every trail,
A hop, a fetch, a cached file — to make installs prevail. 🥕"

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main feature: adding support for historical manifest searches via git history during app installation.
Linked Issues check ✅ Passed The implementation fully addresses issue #3894: it searches git history for historical manifests, supports SQLite cache as fallback, and enables installing specific older app versions without relying solely on autoupdate.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the historical manifest feature. Database refactoring, manifest retrieval logic, new helper functions, and test coverage are all necessary to implement the feature; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/install_via_git_history

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9c8f0b and 362e06c.

📒 Files selected for processing (1)
  • CHANGELOG.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (19)
CHANGELOG.md (1)

1-6: Changelog entry reads well; consider cross-linking the motivating issue.

Add a reference to issue #3894 alongside the PR/issue link to help readers connect the feature to the original problem statement.

- - **install:** Add support for historical manifests ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370))
+ - **install:** Add support for historical manifests ([#6370](https://github.com/ScoopInstaller/Scoop/issues/6370), closes [#3894](https://github.com/ScoopInstaller/Scoop/issues/3894))
libexec/scoop-install.ps1 (1)

7-7: Clarify config keys in the help text.

Name the exact settings to reduce ambiguity for users scanning help.

-# (will search sqlite cache if enabled, then git history (if use_git_history is $true), then auto-generate manifest as fallback):
+# (searches SQLite cache first if use_sqlite_cache = $true, then git history if use_git_history = $true, then autoupdate-based manifest generation as fallback):
libexec/scoop-config.ps1 (1)

33-37: Document partial-version “best match” behavior.

The feature also supports partial versions (e.g., 2.30 → 2.30.x). Reflect that to set correct expectations.

-#       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.
+#       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 requested version
+#       (exact match, or best match when a partial like '2.30' is provided) before falling back to autoupdate.
lib/database.ps1 (1)

312-315: Reusing a prepared command is correct; clear params per iteration in Process for safety.

You already clear in Process for Get-ScoopDBItem. Consider mirroring this pattern in Select-ScoopDBItem to prevent parameter accumulation across multiple Process calls.

 function Select-ScoopDBItem {
@@
-    process {
-        $dbCommand.Parameters.AddWithValue('@Pattern', $(if ($Pattern -eq '') { '%' } else { '%' + $Pattern + '%' })) | Out-Null
+    process {
+        $dbCommand.Parameters.Clear()
+        $dbCommand.Parameters.AddWithValue('@Pattern', $(if ($Pattern -eq '') { '%' } else { '%' + $Pattern + '%' })) | Out-Null
         [void]$dbAdapter.Fill($result)
     }
test/Scoop-Manifest.Tests.ps1 (3)

98-109: Paths are Windows-specific; optionally gate on platform to avoid cross-OS flakiness.

Use $IsWindows to skip on non-Windows agents, or normalize with Join-Path.

 Describe 'Get-RelativePathCompat' -Tag 'Scoop' {
-    It 'returns relative path for child path' {
+    It 'returns relative path for child path' -Skip:(-not $IsWindows) {
@@
-    It 'returns original when different drive/scheme' {
+    It 'returns original when different drive/scheme' -Skip:(-not $IsWindows) {

112-131: DB-path test shape could better reflect DataTable.

Mock Get-ScoopDBItem to return a real DataTable with Rows to align with the new function behavior and catch schema regressions.

-        Mock Get-ScoopDBItem { [pscustomobject]@{ Rows = @([pscustomobject]@{ manifest = '{"version":"1.2.3"}' }) } }
+        Mock Get-ScoopDBItem {
+            $t = New-Object System.Data.DataTable
+            [void]$t.Columns.Add('manifest',[string])
+            $row = $t.NewRow(); $row.manifest = '{"version":"1.2.3"}'; $t.Rows.Add($row) | Out-Null
+            $t
+        }

163-237: Add a test for partial-version “best match”.

Given the feature supports requests like 2.30 → best 2.30.x, add a case to assert best-match selection and source labeling (e.g., git_best_match).

I can draft this Pester example if helpful.

lib/manifest.ps1 (12)

175-204: Normalize inputs and harden Uri fallback in Get-RelativePathCompat

Guard against relative inputs and Uri construction failures to avoid edge-case crashes on PS 5.1 and non-rooted paths.

 function Get-RelativePathCompat($from, $to) {
@@
-    if ($PSVersionTable.PSVersion.Major -ge 6) {
+    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)
+    # Windows PowerShell compatible implementation
+    try {
+        $fromFull = [System.IO.Path]::GetFullPath($from)
+        $toFull   = [System.IO.Path]::GetFullPath($to)
+    } catch {
+        # If normalization fails, return original 'to' as-is
+        return $to
+    }
+    try {
+        $fromUri = New-Object System.Uri($fromFull.TrimEnd('\') + '\')
+        $toUri   = New-Object System.Uri($toFull)
+    } catch {
+        return $to
+    }
@@
-    return $relativePath -replace '/', '\'
+    return $relativePath -replace '/', '\'
 }

228-233: Wrap ConvertFrom-Json with error handling

A malformed DB entry would currently throw and skip cleanup. Add try/catch (as shown in the previous diff) to keep execution robust.


382-401: Replace Write-Host with repo-standard info/warn logging for consistency and tooling

Uniform logging helps scripting scenarios and respects verbosity settings.

-        Write-Host ""
-        Write-Host "No exact match found for version '$requestedVersion' for app '$app'."
-        Write-Host "Available versions in git history (newest to oldest):"
-        Write-Host ""
+        info ''
+        info "No exact match found for version '$requestedVersion' for app '$app'."
+        info "Available versions in git history (newest to oldest):"
+        info ''
@@
-            Write-Host "  $($allAvailableVersions[$i])"
+            info "  $($allAvailableVersions[$i])"
@@
-            Write-Host "  ... and $($allAvailableVersions.Count - $displayCount) more versions"
+            info "  ... and $($allAvailableVersions.Count - $displayCount) more versions"
@@
-        Write-Host ""
-        Write-Host "To install a specific version, use: scoop install $app@<version>"
-        Write-Host ""
+        info ''
+        info "To install a specific version, use: scoop install $app@<version>"
+        info ''

377-381: Deduplicate version list before display

Avoid repeated versions if multiple commits share the same version string.

-        $allAvailableVersions = ($foundVersions | Sort-Object {
-                try { [version]($_.version -replace '[^\d\.].*$', '') } catch { $_.version }
-            } -Descending).version
+        $allAvailableVersions = (
+            $foundVersions | Sort-Object {
+                try { [version]($_.version -replace '[^\d\.].*$', '') } catch { $_.version }
+            } -Descending
+        ).version | Select-Object -Unique

417-417: Avoid noisy warning when manifest is null

Only warn about a mismatch if a current manifest exists; otherwise clarify state.

-    warn "Given version ($version) does not match manifest ($($manifest.version))"
+    if ($manifest) {
+        warn "Given version ($version) does not match manifest ($($manifest.version))"
+    } else {
+        warn "No current manifest found for '$bucket/$app'; falling back to history/autoupdate."
+    }

501-509: Remove redundant git-history fallback after autoupdate catch

Git history has already been tried above when $historicalResult is null; re-running it here doubles the cost without benefit.

-        # If autoupdate fails and we haven't tried git history yet, try it as final fallback
-        if (!$historicalResult -and !(get_config USE_SQLITE_CACHE)) {
-            warn 'Autoupdate failed. Trying git history as final fallback...'
-            $fallbackResult = Get-HistoricalManifestFromGitHistory $app $bucket $version
-            if ($fallbackResult) {
-                warn "Using historical manifest as fallback for '$app' version '$($fallbackResult.version)'"
-                return $fallbackResult.path
-            }
-        }

486-500: Use standard warn/info logging instead of Write-Host with colors

Aligns with the rest of the codebase and improves scriptability.

-        Write-Host -ForegroundColor DarkRed "Could not install $app@$version"
-        Write-Host -ForegroundColor Yellow "Autoupdate failed for version $version"
+        warn "Could not install $app@$version"
+        warn "Autoupdate failed for version $version"
@@
-        Write-Host "Possible reasons:"
-        Write-Host "  - Version $version may not exist or be available for download"
-        Write-Host "  - Download URLs may have changed or be inaccessible"
-        Write-Host "  - The version format may be incompatible with autoupdate patterns"
-        Write-Host ""
-        Write-Host "Suggestions:"
-        Write-Host "  1. Install current version: scoop install $app"
-        Write-Host "  2. Try a different version that was shown in the available list"
-        Write-Host "  3. Check the app's official releases or download page"
-        Write-Host ""
+        info "Possible reasons:"
+        info "  - Version $version may not exist or be available for download"
+        info "  - Download URLs may have changed or be inaccessible"
+        info "  - The version format may be incompatible with autoupdate patterns"
+        info ""
+        info "Suggestions:"
+        info "  1. Install current version: scoop install $app"
+        info "  2. Try a different version that was shown in the available list"
+        info "  3. Check the app's official releases or download page"
+        info ""
@@
-        Write-Host -ForegroundColor Red "All attempts to find or generate manifest for '$app@$version' failed."
+        warn "All attempts to find or generate manifest for '$app@$version' failed."

Also applies to: 511-514


421-438: Optional: Short-circuit when DB/githistory finds a manifest to avoid continued messaging

You already return early, but consider scoping subsequent guidance inside an else branch to avoid confusion when refactoring later.


239-407: Performance: Consider DB prefix search before scanning up to 200 commits

For approximate versions, a DB LIKE-based query (e.g., '2.30.%') can be far faster than iterating git history. Keep git as the fallback when DB lacks entries.

Do we have or can we add a DB helper that supports prefix queries (VersionPrefix or -Like)? If yes, I can draft the integration.


287-294: Heuristic early-stops may skip valid candidates

The 200-commit cap and previous-minor limit can stop before encountering a valid matching version in long histories. Consider stopping only after a best-match has been selected or when crossing below the requested major/minor boundary.

Also applies to: 353-361


421-429: Ensure database.ps1 is loaded before inline DB calls (if you keep them)

If you decide not to remove the duplicate DB check, make sure to dot-source database.ps1 here too (as done in Get-HistoricalManifestFromDB) to avoid "command not found" errors when the earlier function path wasn’t taken.


239-407: Add tests for edge paths and config matrices

Please add tests for:

  • USE_SQLITE_CACHE on/off; USE_GIT_HISTORY on/off; autoupdate present/absent.
  • Approximate version resolving to best match (e.g., X.Y → highest X.Y.Z).
  • DB result shape variations (DataTable vs single object) and malformed JSON.

I can generate Pester tests covering these matrices if helpful.

Also applies to: 410-517

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b588a06 and d3f681c.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • lib/database.ps1 (2 hunks)
  • lib/manifest.ps1 (2 hunks)
  • libexec/scoop-config.ps1 (2 hunks)
  • libexec/scoop-install.ps1 (1 hunks)
  • test/Scoop-Manifest.Tests.ps1 (2 hunks)
🔇 Additional comments (13)
CHANGELOG.md (1)

11-11: Bullet formatting change looks consistent.

No functional impact; matches the surrounding style.

libexec/scoop-config.ps1 (1)

174-174: Minor style tweak LGTM.

Space after if aligns with prevailing style.

lib/database.ps1 (3)

267-272: Good fix: avoid enumerating DataTable and dispose resources.

Copying and emitting with -NoEnumerate prevents row-wise enumeration and post-disposal access issues.


298-307: Pipeline binding improvements are solid.

ValueFromPipelineByPropertyName on Name/Bucket/Version improves composability without breaking callers.


320-345: Parameterized query and per-call parameter reset are correct.

Prevents SQL injection and stale parameter values; returning a DataTable copy is appropriate for downstream consumers that expect Rows.

test/Scoop-Manifest.Tests.ps1 (8)

3-8: Expanded module loading is appropriate for new test coverage.

Brings needed functions into scope for history-aware tests.


14-23: Stronger assertions for invalid JSON behavior.

Covers both return value and warning side-effect; good.


135-160: Git-history test is representative and isolates external calls.

Mocks for Find-BucketDirectory/Test-Path/Invoke-Git are sufficient; nice.


164-169: Manifest passthrough case covered.

Ensures fast path when requested equals current.


171-182: DB-first preference test looks good.

Asserts correct call order and result selection.


184-195: Fallback-to-git test is correct.

Validates miss in DB leads to Git lookup.


197-208: Autoupdate fallback test is clear.

Covers generation path and output location.


227-236: Abort path test is appropriate.

Ensures a hard failure when neither history nor autoupdate is available.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
lib/manifest.ps1 (2)

205-233: Normalize DB result shape; current code assumes DataTable.Rows.

Get-ScoopDBItem can return a DataTable, DataRow, or PSObject. Accessing .Rows[0] will throw in the latter cases.

Apply this diff:

-    $dbResult = Get-ScoopDBItem -Name $app -Bucket $bucket -Version $requestedVersion
-    if ($dbResult.Rows.Count -gt 0) {
-        $row = $dbResult.Rows[0]
-        $manifestText = $row.manifest
-        $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" }
-    }
+    $dbResult = Get-ScoopDBItem -Name $app -Bucket $bucket -Version $requestedVersion
+    if ($null -eq $dbResult) { return $null }
+    $first = $null
+    if ($dbResult.PSObject.Properties['Rows']) {
+        if ($dbResult.Rows.Count -eq 0) { return $null }
+        $first = $dbResult.Rows[0]
+    } else {
+        $first = $dbResult
+    }
+    $manifestText = $null
+    if ($first -is [System.Data.DataRow]) {
+        $manifestText = $first['manifest']
+    } elseif ($first.PSObject.Properties['manifest']) {
+        $manifestText = $first.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" }

234-290: Fix commit selection: use the commit(s) containing the requested version, not HEAD or parent-of-first.

Using HEAD when only one match exists and commit^ when multiple is off-by-one and can fetch the wrong version. Iterate commits and return the first whose blob matches the requested version.

Apply this diff:

-        if ($commits.Count -eq 0) { return $null }
-
-        $targetSpec = if ($commits.Count -ge 2) { "$($commits[0])^" } else { 'HEAD' }
-
-        $manifestContent = Invoke-Git -Path $bucketDir -ArgumentList @('show', "$targetSpec`:$relativeManifestPath")
-        if (-not $manifestContent -or ($LASTEXITCODE -ne 0)) { return $null }
-        if ($manifestContent -is [Array]) { $manifestContent = $manifestContent -join "`n" }
-
-        $manifestObj = $null
-        try { $manifestObj = $manifestContent | ConvertFrom-Json -ErrorAction Stop } catch { return $null }
-        if (-not $manifestObj -or $manifestObj.version -ne $requestedVersion) { return $null }
-
-        return @{ ManifestText = $manifestContent; version = $requestedVersion; source = "git_manifest:$targetSpec" }
+        if ($commits.Count -eq 0) { return $null }
+
+        foreach ($c in $commits) {
+            $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

Note: ensure Invoke-Git reliably propagates $LASTEXITCODE under proxy mode; see prior comment in core.ps1 about setting $global:LASTEXITCODE from the job path.

🧹 Nitpick comments (3)
test/Scoop-Manifest.Tests.ps1 (3)

98-109: Make path assertions cross-platform.

Tests hardcode backslashes; on non-Windows/PS7, Get-RelativePathCompat returns OS-native separators. Assert with Join-Path or a regex.

Apply this diff:

 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'
+    $from = 'C:\root\bucket'
+    $to = 'C:\root\bucket\foo\bar.json'
+    $expected = Join-Path 'foo' 'bar.json'
+    Get-RelativePathCompat $from $to | Should -Be $expected
 }

111-128: Add a DB-return-shape test to prevent regressions.

Current test covers a Rows[...] shape only. Add a case where Get-ScoopDBItem returns a single PSObject with .manifest to match production variability.

Apply this diff to add coverage:

 It 'returns manifest text and version when cache has exact match' {
@@
     $result.ManifestText | Should -Match '"version":"1.2.3"'
 }
+
+It 'handles single-object DB return shape (no .Rows)' {
+    Mock get_config -ParameterFilter { $name -in @('use_sqlite_cache','use_git_history') } { $true }
+    Mock Get-ScoopDBItem { [pscustomobject]@{ manifest = '{"version":"9.9.9"}' } }
+    $result = Find-HistoricalManifestInCache 'foo' 'main' '9.9.9'
+    $result | Should -Not -BeNullOrEmpty
+    $result.version | Should -Be '9.9.9'
+    $result.ManifestText | Should -Match '"version":"9.9.9"'
+}

202-215: Assert warnings on autoupdate failure.

Strengthen the failure-path test by verifying warn was invoked once.

Apply this diff:

 Mock warn {}
@@
 { generate_user_manifest 'foo' 'main' '1.0.0' } | Should -Throw
+Should -Invoke -CommandName warn -Times 1
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3f681c and 0a52f0d.

📒 Files selected for processing (2)
  • lib/manifest.ps1 (2 hunks)
  • test/Scoop-Manifest.Tests.ps1 (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Lutra-Fs
PR: ScoopInstaller/Scoop#6370
File: lib/manifest.ps1:239-407
Timestamp: 2025-08-27T09:24:36.044Z
Learning: User Lutra-Fs prefers to keep PR scope focused and avoid adding unnecessary complexity, even when suggested functionality aligns with stated objectives. They prioritize clean, focused implementations over feature completeness in a single PR.
📚 Learning: 2025-08-27T09:24:36.044Z
Learnt from: Lutra-Fs
PR: ScoopInstaller/Scoop#6370
File: lib/manifest.ps1:239-407
Timestamp: 2025-08-27T09:24:36.044Z
Learning: User Lutra-Fs prefers to keep PR scope focused and avoid adding unnecessary complexity, even when suggested functionality aligns with stated objectives. They prioritize clean, focused implementations over feature completeness in a single PR.

Applied to files:

  • lib/manifest.ps1
🔇 Additional comments (7)
test/Scoop-Manifest.Tests.ps1 (3)

3-7: LGTM: necessary module imports loaded upfront.

Brings core, buckets, database, and autoupdate into scope for the new tests. Looks good.


14-22: LGTM: stronger assertions for parse_json.

Non-throw + null and warning checks are appropriate.


217-226: LGTM: clear abort when no history and no autoupdate.

lib/manifest.ps1 (4)

175-203: LGTM: compatible relative-path helper.

Graceful PS7 vs Windows PowerShell handling and URI fallback look good.


292-319: LGTM: clear, layered orchestration (DB → Git).

Writes to user cache only when ManifestText is present; sensible.


329-347: LGTM: history-first, then autoupdate with concise failure messaging.

Flow and messages align with PR goals.


357-367: LGTM: small, reusable writer helper.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/manifest.ps1 (2)

175-203: Get-RelativePathCompat: normalize paths and handle cross-drive early

Ensure absolute paths and bail out on different roots to avoid odd “absolute-as-relative” results on Windows PowerShell.

 function Get-RelativePathCompat($from, $to) {
@@
-    # Windows PowerShell compatible implementation
-    $fromUri = New-Object System.Uri($from.TrimEnd('\') + '\')
-    $toUri = New-Object System.Uri($to)
+    # Windows PowerShell compatible implementation
+    $fromAbs = [System.IO.Path]::GetFullPath($from)
+    $toAbs   = [System.IO.Path]::GetFullPath($to)
+    # If roots (drive/UNC) differ, return absolute target path
+    $fromRoot = ([System.IO.Path]::GetPathRoot($fromAbs)).TrimEnd('\')
+    $toRoot   = ([System.IO.Path]::GetPathRoot($toAbs)).TrimEnd('\')
+    if ($fromRoot -ne $toRoot) { return $toAbs }
+    $fromUri = [System.Uri]::new($fromAbs.TrimEnd('\') + '\')
+    $toUri   = [System.Uri]::new($toAbs)
@@
-    return $relativePath -replace '/', '\'
+    return $relativePath -replace '/', '\'
 }

378-388: Sanitize app name when writing to user cache path

Prevent odd filenames or accidental subpaths by using the existing sanitary_path helper.

-    $tempManifestPath = "$(usermanifestsdir)\$App.json"
+    $safe = sanitary_path $App
+    $tempManifestPath = "$(usermanifestsdir)\$safe.json"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fae80b and 5d46f67.

📒 Files selected for processing (2)
  • CHANGELOG.md (2 hunks)
  • lib/manifest.ps1 (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Lutra-Fs
PR: ScoopInstaller/Scoop#6370
File: lib/manifest.ps1:239-407
Timestamp: 2025-08-27T09:24:36.066Z
Learning: User Lutra-Fs prefers to keep PR scope focused and avoid adding unnecessary complexity, even when suggested functionality aligns with stated objectives. They prioritize clean, focused implementations over feature completeness in a single PR.
📚 Learning: 2025-08-27T09:24:36.066Z
Learnt from: Lutra-Fs
PR: ScoopInstaller/Scoop#6370
File: lib/manifest.ps1:239-407
Timestamp: 2025-08-27T09:24:36.066Z
Learning: User Lutra-Fs prefers to keep PR scope focused and avoid adding unnecessary complexity, even when suggested functionality aligns with stated objectives. They prioritize clean, focused implementations over feature completeness in a single PR.

Applied to files:

  • lib/manifest.ps1
🔇 Additional comments (5)
lib/manifest.ps1 (3)

238-312: Git history: $LASTEXITCODE reliance and search bounds

  • $LASTEXITCODE from Invoke-Git can be unreliable under the Start-Job proxy path; you already tracked this separately. Given that, your existing -not $content guard is the safer primary check. No action needed here within this PR.
  • Optional: cap the breadth of the fallback scan ($outAll) or make it configurable to avoid walking very deep histories on large buckets.

313-341: Orchestrator flow looks good

Clear DB→Git ordering and single place to persist the manifest. No issues spotted.


350-367: generate_user_manifest: error flow and messaging are appropriate

History-first, then autoupdate with clear aborts on failure. LGTM.

CHANGELOG.md (2)

5-5: Changelog entry reads well

Accurately reflects the added feature and links the issue.


22-22: Minor formatting tweak is fine

No semantic changes; formatting looks consistent.

Comment on lines +205 to +237
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
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

SQLite path: unreachable return and strict result-shape assumptions

  • Line 235: The return $null is unreachable. Remove it.
  • Result handling assumes a DataTable; if Get-ScoopDBItem ever returns a DataRow/PSObject, this will silently skip a valid hit. If that contract is guaranteed, fine; otherwise add a small normalization block.
     return @{ ManifestText = $manifestText; version = $manifestVersion; source = "sqlite_exact_match" }
-
-    return $null

If you want to be defensive without much code churn, consider:

if (-not ($dbResult -is [System.Data.DataTable])) { return $null }

documented as an intentional contract check (i.e., add a short comment). Otherwise, normalize the first item regardless of shape.


🏁 Script executed:

#!/bin/bash
# Locate and inspect the Get-ScoopDBItem function to confirm its return contract
rg -nP -C5 "function\s+Get-ScoopDBItem" -g "*.ps1"

Length of output: 518


🏁 Script executed:

#!/bin/bash
sed -n '290,360p' lib/database.ps1

Length of output: 2133


Remove unreachable return; DataTable check is safe

  • Drop the second return $null after the manifest return (line 235).
  • Get-ScoopDBItem is documented with .OUTPUTS System.Data.DataTable and uses Write-Output on a DataTable copy, so the strict DataTable check is safe—no normalization needed.
🤖 Prompt for AI Agents
In lib/manifest.ps1 around lines 205 to 237, remove the unreachable "return
$null" that appears after the successful "return @{ ManifestText =
$manifestText; version = $manifestVersion; source = "sqlite_exact_match" }"
(line ~235); simply delete that trailing return so control flow is correct, and
keep the existing strict DataTable checks as-is since Get-ScoopDBItem reliably
outputs a System.Data.DataTable.

@niheaven niheaven changed the title feat(install): Add historical manifest search for app installation feat(install): Add support for historical manifests by git history Nov 10, 2025
@Lutra-Fs
Copy link
Member Author

Just noticed that I cannot push to the branch anymore >﹏< since more strict permission setup has been introduced.

Comment on lines +222 to +223
# Strictly follow DB contract: must be DataTable with at least one row
if (-not ($dbResult -is [System.Data.DataTable])) { return $null }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Strictly follow DB contract: must be DataTable with at least one row
if (-not ($dbResult -is [System.Data.DataTable])) { return $null }

Comment on lines +338 to +339

return $null
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return $null

@niheaven
Copy link
Member

Still working on it, and I'll merge the reviewed code after resolved conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants