Bring Pinning to PowerShell Cmdlets#6190
Conversation
|
Were you able to figure out the PlatyPS MAML stuff for the PowerShell help? |
|
There was a problem hiding this comment.
Pull request overview
This PR expands WinGet’s pinning feature end-to-end (repository schema + CLI workflow + COM/WinRT API + PowerShell cmdlets), adding pin metadata (DateAdded, Note), a new winget pin show command, and PowerShell cmdlets to manage pins.
Changes:
- Add
DateAddedandNotesupport to the pinning index (schema v1.1) with migration from v1.0. - Extend the COM API surface for pin management (get/pin/unpin/reset) and expose
PackagePin,PinPackageOptions,PinPackageResult. - Add PowerShell cmdlets (
Get/Add/Remove/Reset-WinGetPin) and related PS output objects/help.
Reviewed changes
Copilot reviewed 59 out of 59 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 | Exports new pin cmdlets from the module. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs | Adds PS wrapper for COM pin operation results. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs | Adds PS wrapper for COM PackagePin objects. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs | Adds IsPinned property on installed package output objects. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs | Adds wrapper methods for new COM pin APIs. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs | Adds conversion helper for PackagePinType. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs | Adds COM factory creation for PinPackageOptions. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs | Adds engine command to reset pins via COM. |
| src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs | Adds engine command to get/add/remove pins via COM. |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs | Adds parameter sets and noun constant for pin cmdlets. |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs | Adds Reset-WinGetPin cmdlet. |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs | Adds Remove-WinGetPin cmdlet (supports pipeline pin input). |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs | Adds PowerShell-facing enum for pin types. |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs | Adds Get-WinGetPin cmdlet. |
| src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs | Adds Add-WinGetPin cmdlet (type/gatedVersion/note/force). |
| src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md | Adds help documentation for reset pin cmdlet. |
| src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md | Adds help documentation for remove pin cmdlet. |
| src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md | Adds module index entries for new cmdlets. |
| src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md | Adds help documentation for get pin cmdlet. |
| src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md | Adds help documentation for add pin cmdlet. |
| src/Microsoft.Management.Deployment/Public/ComClsids.h | Adds CLSIDs for PinPackageOptions activation. |
| src/Microsoft.Management.Deployment/PinPackageResult.h | Introduces WinRT result type for pin operations. |
| src/Microsoft.Management.Deployment/PinPackageResult.cpp | Implements WinRT result type for pin operations. |
| src/Microsoft.Management.Deployment/PinPackageOptions.h | Introduces WinRT options type for pin operations. |
| src/Microsoft.Management.Deployment/PinPackageOptions.cpp | Implements WinRT options type for pin operations. |
| src/Microsoft.Management.Deployment/PackagePin.h | Introduces WinRT PackagePin runtimeclass. |
| src/Microsoft.Management.Deployment/PackagePin.cpp | Implements WinRT PackagePin object creation from internal pins. |
| src/Microsoft.Management.Deployment/PackageManager.idl | Adds pinning APIs + types to the public WinRT contract. |
| src/Microsoft.Management.Deployment/PackageManager.h | Declares new PackageManager pinning methods. |
| src/Microsoft.Management.Deployment/PackageManager.cpp | Implements GetPins/GetAllPins/Pin/Unpin/Reset via pinning data store. |
| src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj | Adds new pin-related source/header files to project build. |
| src/Microsoft.Management.Deployment/Converters.h | Declares GetPinOperationStatus mapping. |
| src/Microsoft.Management.Deployment/Converters.cpp | Implements HRESULT→PinResultStatus mapping. |
| src/Microsoft.Management.Deployment/ComClsids.cpp | Enables in-proc→out-of-proc redirection for PinPackageOptions. |
| src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs | Adds projection factory method for PinPackageOptions. |
| src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs | Registers PinPackageOptions class for projection activation. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp | Adds v1.1 pinning schema interface + migration path. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h | Declares v1.1 schema interface. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h | Declares v1.1 pin table with new columns. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp | Implements v1.1 pin table CRUD/migration including note/date. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp | Implements MigrateFrom for v1.0 (not supported). |
| src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h | Adds MigrateFrom to v1.0 interface declaration. |
| src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h | Adds MigrateFrom to schema interface abstraction. |
| src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h | Adds helper to create schema interface for a specific version. |
| src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp | Implements migration-on-open for pinning index and version dispatch. |
| src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | Adds v1.1 pinning schema files to repository core build. |
| src/AppInstallerCommonCore/Public/winget/Pin.h | Adds DateAdded + Note fields to internal Pin model. |
| src/AppInstallerCLITests/PinningIndex.cpp | Adds unit tests for v1.1 schema and migration behavior. |
| src/AppInstallerCLITests/PinFlow.cpp | Adds workflow tests for note/date and new pin show behavior. |
| src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | Adds localized strings for --note and pin show output. |
| src/AppInstallerCLIPackage/Package.appxmanifest | Registers COM server class for PinPackageOptions (dev out-of-proc). |
| src/AppInstallerCLICore/Workflows/PinFlow.h | Declares new workflow step ShowPinDetails. |
| src/AppInstallerCLICore/Workflows/PinFlow.cpp | Sets date/note on add; implements winget pin show output filtering. |
| src/AppInstallerCLICore/Resources.h | Adds resource IDs for new pin strings. |
| src/AppInstallerCLICore/ExecutionArgs.h | Adds PinNote execution arg. |
| src/AppInstallerCLICore/Commands/PinCommand.h | Adds PinShowCommand declaration. |
| src/AppInstallerCLICore/Commands/PinCommand.cpp | Wires up winget pin show and adds --note to pin add. |
| src/AppInstallerCLICore/Argument.cpp | Adds CLI argument mapping for --note. |
| .github/actions/spelling/expect.txt | Adds expected spelling words introduced by new tests/log strings. |
| std::make_unique<PinRemoveCommand>(FullName()), | ||
| std::make_unique<PinListCommand>(FullName()), | ||
| std::make_unique<PinResetCommand>(FullName()), | ||
| std::make_unique<PinShowCommand>(FullName()), |
There was a problem hiding this comment.
I would prefer that we follow the model of package list and have --details on the existing list command. That also removes the need to create a whole new search mechanism and just makes the change in ReportPins.
Followup: Well it looks like it would need a whole new search mechanism to actually implement the arguments that it claims to support...
There was a problem hiding this comment.
Yes, we would need a whole new search mechanism anyways. I do think that there is some overlap with winget list <query> --details, but to me it felt more akin to winget show <query>. The semantics of list vs show as a base command don't really apply to pins. Where show in the base command is for a remote package / manifest information, list is for installed applications.
I will think on this and probably end up refactoring to extend the behavior of list instead of adding show
| auto newPin = CreatePinFromOptions(pinKey, options); | ||
|
|
||
| auto existingPin = pinningData.GetPin(pinKey); | ||
| if (existingPin && !(*existingPin == newPin)) |
There was a problem hiding this comment.
Shouldn't this only be comparing the pin type rather than the entire pin?
There was a problem hiding this comment.
I don't think so. The CLI compares the version as well for gating pins so that updating a gating pin by overwriting it requires --force . e.g winget pin add Microsoft.WingetCreate --version 1.9 and then running winget pin add Microsoft.WingetCreate --version 1.10 would give an error that there is already a pin and it needs to be overriden.
This matches the behavior of the CLI
| /// <summary> | ||
| /// Must match the user-settable values of Microsoft.Management.Deployment.PackagePinType. | ||
| /// </summary> | ||
| public enum PSPackagePinType |
There was a problem hiding this comment.
Do we need the "pinned by the manifest" type?
There was a problem hiding this comment.
I don't believe we do, since it isn't user-settable and the CLI doesn't expose it when listing pins as far as I can tell
There was a problem hiding this comment.
It feels like a thing that we probably should expose everywhere, but that can be a separate change.
It is no longer needed since the interface surface is so small
This comment has been minimized.
This comment has been minimized.
* Updated .github/copilot-instructions.md
| bool hasName = context.Args.Contains(Execution::Args::Type::Name); | ||
| bool hasQuery = context.Args.Contains(Execution::Args::Type::Query); | ||
| bool exactMatch = context.Args.Contains(Execution::Args::Type::Exact); | ||
|
|
There was a problem hiding this comment.
winget pin show currently allows running with no query/id/name arguments, in which case it will match every pin and dump verbose details for all of them. This seems inconsistent with the intent of a "show" command (and the comment above suggests at least one filter is expected). Consider enforcing that at least one of --id/--name/--query is provided and terminating with an appropriate error message when none are specified.
| if (!hasId && !hasName && !hasQuery) | |
| { | |
| context.Reporter.Error() << "The 'pin show' command requires at least one of --id, --name, or --query." << std::endl; | |
| AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); | |
| } |
JohnMcPMS
left a comment
There was a problem hiding this comment.
I would like to see the PinTable stuff be thought through more to re-use code in a way that allows the 1.0 code to stay largely untouched and 1.0 to leverage it without anything over.
| - Parsing in `AppInstallerCommonCore/Manifest/` | ||
| - Multi-file manifests: installer, locale, version, defaultLocale | ||
|
|
||
| ## SQLite Statement Builder |
There was a problem hiding this comment.
This should go into a path specific instructions file (with the appropriate target paths). It is a lot of lines that are only needed in a few cases.
| | `Equals(optional<T>)` | `IS NULL` | **WHERE** / filter clauses | | ||
| | `AssignValue(optional<T>)` | `= ?` (binds NULL) | **UPDATE SET** assignments | | ||
|
|
||
| Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col = IS NULL`. |
There was a problem hiding this comment.
| Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col = IS NULL`. | |
| Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col IS NULL`. |
| #include <Microsoft/Schema/IPinningIndex.h> | ||
| #include <Microsoft/Schema/Pinning_1_0/PinTable.h> | ||
| #include <Microsoft/Schema/Pinning_1_1/PinTable.h> | ||
| #include <Microsoft/Schema/Pinning_1_1/PinTable_1_1.h> |
There was a problem hiding this comment.
We can definitely leave the header as PinTable.h.
| { | ||
| Builder::StatementBuilder update; | ||
| update.Update(s_tableName).Set().Column(s_firstColumn).Equals(firstVal).Column(s_secondColumn).Equals(secondVal); | ||
| update.Update(s_tableName).Set().Column(s_firstColumn).AssignValue(firstVal).Column(s_secondColumn).AssignValue(secondVal); |
There was a problem hiding this comment.
New test that provides an empty optional?
| bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; | ||
|
|
||
| protected: | ||
| virtual SQLite::rowid_t IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin); |
There was a problem hiding this comment.
The proper mechanism would be:
V1_1::AddPin(pin)
{
savepoint_1_1;
pin_row = V1_0::AddPin(pin);
modify_new_pin_with_new_fields(pin_row, pin);
savepoint_1_1.commit();
}
This is certainly inefficient, but it ensures the proper separation of schema concerns. So a bug fix in 1_0 doesn't have to be duplicated across the range.
|
|
||
| protected: | ||
| // Creates the table with named indices. | ||
| static void Create(SQLite::Connection& connection); |
| } | ||
|
|
||
| using namespace std::string_view_literals; | ||
| static constexpr std::string_view s_PinTable_Table_Name = "pin"sv; |
There was a problem hiding this comment.
These here and 1.0 should be moved to under the PinTable type so that they are not defined multiple times.
| m_interface = CreateIPinningIndexForVersion(m_version); | ||
| } | ||
|
|
||
| m_interface = std::move(latestInterface); |
There was a problem hiding this comment.
Should be inside the above if block (is a functional no-op when they are already the same except for a tiny bit of move overhead).
| /// </summary> | ||
| protected override void ProcessRecord() | ||
| { | ||
| if (this.Blocking && !string.IsNullOrEmpty(this.GatedVersion)) |
There was a problem hiding this comment.
Can we do this through parameter sets? Not sure if it is more complicated or by how much, but it would better integrate with PS (at least I assume that once it has decided which set you are using on the shell it won't offer tab-suggestions from the other sets).
| return; | ||
| } | ||
|
|
||
| // Fetch all pins in a single COM call and build a lookup set to avoid |
There was a problem hiding this comment.
This feels like it is shifting the cost up front; a cost that doesn't need to be paid when the caller doesn't examine the IsPinned property. This isn't a single COM call either, ever PackageId property retrieval is also one. I think it would be better to just cache the IsPinned state from the previous change lazily rather than forcibly populating it.
Summary
This PR extends the WinGet pinning system with new metadata, a new CLI subcommand, expanded COM API surface, and PowerShell cmdlets for managing pins.
CLI Changes
winget pin add: Added--noteargument to attach an optional freeform note to a pin. The timestamp when a pin is created is now recorded automatically.winget pin show: New subcommand that displays detailed information about pins for a specific package (query by ID, name, or keyword).Pinning Index Schema (v1.1)
DateAddedandNotecolumns to the pin table to persist the new metadata.COM API (
Microsoft.Management.Deployment, contract v29)PackageManager.GetAllPins()— retrieve all pins across all sources.PackageManager.GetPins(CatalogPackage)— retrieve pins for a specific package.PackageManager.PinPackage(CatalogPackage, PinPackageOptions)— add or update a pin.PackageManager.UnpinPackage(CatalogPackage)— remove all pins for a package.PackageManager.ResetAllPins(String sourceName)— reset all pins, optionally scoped to a source.PackagePinruntime class exposingPackageId,SourceId,Type,GatedVersion,DateAdded,Note, andIsForInstalledPackage.PinPackageOptionsandPinPackageResultruntime classes.PackagePinTypeenum (PinnedByManifest,Pinning,Gating,Blocking).PowerShell (
Microsoft.WinGet.Client)Add-WinGetPin— pin a package with a specified pin type, optional gated version, note, and force flag.Get-WinGetPin— list pins, filterable by package/source.Remove-WinGetPin— remove pins for a package.Reset-WinGetPin— reset all pins, optionally scoped to a source.Get-WinGetPackage— newIsPinnedproperty on returned objects.cc @denelon for Naming
Microsoft Reviewers: Open in CodeFlow