From 32b17d4d8f9473b793cb941a8f85fbba1dffcc43 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 08:58:33 -0500 Subject: [PATCH 01/46] Add note, date created, and show subcommand --- src/AppInstallerCLICore/Argument.cpp | 2 + .../Commands/PinCommand.cpp | 34 +++ src/AppInstallerCLICore/Commands/PinCommand.h | 15 ++ src/AppInstallerCLICore/ExecutionArgs.h | 1 + src/AppInstallerCLICore/Resources.h | 12 + src/AppInstallerCLICore/Workflows/PinFlow.cpp | 115 ++++++++- src/AppInstallerCLICore/Workflows/PinFlow.h | 8 + .../Shared/Strings/en-us/winget.resw | 46 ++++ .../Public/winget/Pin.h | 9 + .../AppInstallerRepositoryCore.vcxproj | 6 + .../Microsoft/PinningIndex.cpp | 54 +++- .../Microsoft/PinningIndex.h | 3 + .../Microsoft/Schema/IPinningIndex.h | 4 + .../Pinning_1_0/PinningIndexInterface.h | 1 + .../Pinning_1_0/PinningIndexInterface_1_0.cpp | 6 + .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 232 ++++++++++++++++++ .../Microsoft/Schema/Pinning_1_1/PinTable.h | 46 ++++ .../Pinning_1_1/PinningIndexInterface.h | 23 ++ .../Pinning_1_1/PinningIndexInterface_1_1.cpp | 118 +++++++++ 19 files changed, 730 insertions(+), 5 deletions(-) create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 99699bdb0a..f22524c8b2 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -203,6 +203,8 @@ namespace AppInstaller::CLI return { type, "blocking"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PinType }; case Execution::Args::Type::PinInstalled: return { type, "installed"_liv, ArgTypeCategory::None }; + case Execution::Args::Type::PinNote: + return { type, "note"_liv, ArgTypeCategory::None }; // Error command case Execution::Args::Type::ErrorInput: diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp index 9d9219d68f..f47e81055d 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.cpp +++ b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -23,6 +23,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -65,6 +66,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Force), Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag }, Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, + Argument{ Args::Type::PinNote, Resource::String::PinNoteArgumentDescription, ArgumentType::Standard }, }; } @@ -343,4 +345,36 @@ namespace AppInstaller::CLI Workflow::ReportPins; } } + + std::vector PinShowCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Exact), + }; + } + + Resource::LocString PinShowCommand::ShortDescription() const + { + return { Resource::String::PinShowCommandShortDescription }; + } + + Resource::LocString PinShowCommand::LongDescription() const + { + return { Resource::String::PinShowCommandLongDescription }; + } + + Utility::LocIndView PinShowCommand::HelpLink() const + { + return s_PinCommand_HelpLink; + } + + void PinShowCommand::ExecuteInternal(Execution::Context& context) const + { + context << + Workflow::OpenPinningIndex(/* readOnly */ true) << + Workflow::ShowPinDetails; + } } diff --git a/src/AppInstallerCLICore/Commands/PinCommand.h b/src/AppInstallerCLICore/Commands/PinCommand.h index 285e66eaa7..2852f26bc0 100644 --- a/src/AppInstallerCLICore/Commands/PinCommand.h +++ b/src/AppInstallerCLICore/Commands/PinCommand.h @@ -87,4 +87,19 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + + struct PinShowCommand final : public Command + { + PinShowCommand(std::string_view parent) : Command("show", parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 675576a9d7..947aa7c6fd 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -122,6 +122,7 @@ namespace AppInstaller::CLI::Execution GatedVersion, // Differs from Version in that this supports wildcards BlockingPin, PinInstalled, + PinNote, // User-provided note to attach to a pin // Error command ErrorInput, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index fa369b8ae5..a7395f6200 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -530,6 +530,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinCannotOpenIndex); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinDateAdded); WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting); WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg); @@ -537,6 +538,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledSource); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinNote); + WINGET_DEFINE_RESOURCE_STRINGID(PinNoteArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription); @@ -546,6 +549,15 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll); WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelDateAdded); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelId); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelNote); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelSource); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelType); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelVersion); + WINGET_DEFINE_RESOURCE_STRINGID(PinShowNoMatchFound); WINGET_DEFINE_RESOURCE_STRINGID(PinType); WINGET_DEFINE_RESOURCE_STRINGID(PinVersion); WINGET_DEFINE_RESOURCE_STRINGID(PlatformArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp index ace205fc44..91d863d5e9 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -3,7 +3,9 @@ #include "pch.h" #include "Resources.h" #include "PinFlow.h" +#include "ShowFlow.h" #include "TableOutput.h" +#include #include #include #include @@ -197,9 +199,21 @@ namespace AppInstaller::CLI::Workflow if (!pinsToAddOrUpdate.empty()) { - for (const auto& pin : pinsToAddOrUpdate) + std::string dateAdded = Utility::TimePointToString( + std::chrono::system_clock::now(), + Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day | + Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second); + std::optional note; + if (context.Args.Contains(Execution::Args::Type::PinNote)) { + note = std::string{ context.Args.GetArg(Execution::Args::Type::PinNote) }; + } + + for (auto& pin : pinsToAddOrUpdate) + { + pin.SetDateAdded(dateAdded); + pin.SetNote(note); pinningData.AddOrUpdatePin(pin); } @@ -335,4 +349,103 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; } } + + void ShowPinDetails(Execution::Context& context) + { + auto& pinningData = context.Get(); + auto allPins = pinningData.GetAllPins(); + + // Apply filtering based on provided arguments + bool hasId = context.Args.Contains(Execution::Args::Type::Id); + 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); + + std::vector matchingPins; + for (const auto& pin : allPins) + { + const auto& packageId = pin.GetKey().PackageId; + + if (hasId) + { + std::string_view idArg = context.Args.GetArg(Execution::Args::Type::Id); + bool match = exactMatch + ? Utility::CaseInsensitiveEquals(packageId, idArg) + : Utility::CaseInsensitiveContainsSubstring(packageId, idArg); + if (!match) + { + continue; + } + } + else if (hasName || hasQuery) + { + // Without an open source, we can only match against PackageId + std::string_view queryArg = hasName + ? context.Args.GetArg(Execution::Args::Type::Name) + : context.Args.GetArg(Execution::Args::Type::Query); + bool match = exactMatch + ? Utility::CaseInsensitiveEquals(packageId, queryArg) + : Utility::CaseInsensitiveContainsSubstring(packageId, queryArg); + if (!match) + { + continue; + } + } + + matchingPins.push_back(pin); + } + + if (matchingPins.empty()) + { + context.Reporter.Info() << Resource::String::PinShowNoMatchFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + } + + auto info = context.Reporter.Info(); + bool firstPin = true; + for (const auto& pin : matchingPins) + { + if (!firstPin) + { + info << std::endl; + } + firstPin = false; + + const auto& pinKey = pin.GetKey(); + + // ID + ShowSingleLineField(info, Resource::String::PinShowLabelId, Utility::LocIndView{ pinKey.PackageId }); + + // Source + if (!pinKey.SourceId.empty() && !pinKey.IsForInstalled()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelSource, Utility::LocIndView{ pinKey.SourceId }); + } + + // Type + std::string pinTypeStr{ ToString(pin.GetType()) }; + ShowSingleLineField(info, Resource::String::PinShowLabelType, Utility::LocIndView{ pinTypeStr }); + + // Version (gated version string; empty for pinning/blocking pins) + std::string gatedVersionStr = pin.GetGatedVersion().ToString(); + if (!gatedVersionStr.empty()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelVersion, Utility::LocIndView{ gatedVersionStr }); + } + + // Date Added + const auto& dateAdded = pin.GetDateAdded(); + if (!dateAdded.empty()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelDateAdded, Utility::LocIndView{ dateAdded }); + } + + // Note (only shown if present) + const auto& note = pin.GetNote(); + if (note.has_value() && !note->empty()) + { + ShowSingleLineField(info, Resource::String::PinShowLabelNote, Utility::LocIndView{ *note }); + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h index 2e0e55a5db..e9ff60faa4 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.h +++ b/src/AppInstallerCLICore/Workflows/PinFlow.h @@ -54,6 +54,14 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ReportPins(Execution::Context& context); + // Shows details for a single matching pin (for `winget pin show`). + // Filters the pinning index by query/name/id/exact args and outputs + // detailed field-by-field info (Name, ID, Version, Source, Type, Date Added, Note). + // Required Args: None (at least one of --query/--name/--id expected) + // Inputs: PinningIndex + // Outputs: None + void ShowPinDetails(Execution::Context& context); + // Resets all the existing pins. // Required Args: None // Inputs: None diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 177d2828c0..bb3ea1e83d 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1648,6 +1648,10 @@ Please specify one of them using the --source option to proceed. Pin a specific installed version + + Optional note to store with the pin + Description for the --note argument used with `winget pin add` + Installed Value used in a table to indicate that a package comes from the list of packages installed in the machine @@ -1913,6 +1917,48 @@ Please specify one of them using the --source option to proceed. Pinned version Table header for the version to which a package is pinned; meaning it should not update from that version. + + Date added + Label shown in the pin show output for when the pin was added or last updated. + + + Note + Label shown in the pin show output for the user-provided note stored with the pin. + + + Show details about a pin + + + Show detailed information about a specific pin, including the package ID, version, type, date added, and any note stored with the pin. + + + Date added: + Label shown in the `winget pin show` output for when the pin was added or last updated. + + + Id: + Label shown in the `winget pin show` output for the package identifier. + + + Note: + Label shown in the `winget pin show` output for the user-provided note stored with the pin. + + + Source: + Label shown in the `winget pin show` output for the package source. + + + Pin type: + Label shown in the `winget pin show` output for the type of pin (e.g., Pinning, Gating, Blocking). + + + Pinned version: + Label shown in the `winget pin show` output for the version string of a gating pin. + + + No pin found matching the specified criteria. + Shown when `winget pin show` finds no matching pin. + <See the log file for additional details> The brackets are intended to make the value stand out from other text which it will follow. Any locale appropriate mechanism that achieves this is acceptable. diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h index 2039b81a5e..752eb02d44 100644 --- a/src/AppInstallerCommonCore/Public/winget/Pin.h +++ b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -3,6 +3,8 @@ #pragma once #include "winget/Manifest.h" #include "AppInstallerVersions.h" +#include +#include namespace AppInstaller::Pinning { @@ -97,6 +99,11 @@ namespace AppInstaller::Pinning PinType GetType() const { return m_type; } const PinKey& GetKey() const { return m_key; } const Utility::GatedVersion& GetGatedVersion() const { return m_gatedVersion; } + const std::string& GetDateAdded() const { return m_dateAdded; } + const std::optional& GetNote() const { return m_note; } + + void SetDateAdded(std::string dateAdded) { m_dateAdded = std::move(dateAdded); } + void SetNote(std::optional note) { m_note = std::move(note); } bool operator==(const Pin& other) const; bool operator<(const Pin& other) const @@ -114,5 +121,7 @@ namespace AppInstaller::Pinning PinType m_type = PinType::Unknown; PinKey m_key; Utility::GatedVersion m_gatedVersion; + std::string m_dateAdded; + std::optional m_note; }; } diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 53a8260f9c..9affa80d82 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -338,6 +338,8 @@ + + @@ -449,6 +451,10 @@ + + + $(IntDir)Schema\Pinning_1_1\ + diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index 645cfb127d..42b96ebf5b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -4,6 +4,7 @@ #include "PinningIndex.h" #include #include "Schema/Pinning_1_0/PinningIndexInterface.h" +#include "Schema/Pinning_1_1/PinningIndexInterface.h" namespace AppInstaller::Repository::Microsoft { @@ -186,13 +187,24 @@ namespace AppInstaller::Repository::Microsoft std::unique_ptr PinningIndex::CreateIPinningIndex() const { - if (m_version == SQLite::Version{ 1, 0 } || - m_version.MajorVersion == 1 || - m_version.IsLatest()) + // Always return the latest interface; migration will handle version mismatches. + return std::make_unique(); + } + + std::unique_ptr PinningIndex::CreateIPinningIndexForVersion(const SQLite::Version& version) + { + if (version == SQLite::Version{ 1, 0 }) { return std::make_unique(); } + if (version == SQLite::Version{ 1, 1 } || + version.MajorVersion == 1 || + version.IsLatest()) + { + return std::make_unique(); + } + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } @@ -201,7 +213,41 @@ namespace AppInstaller::Repository::Microsoft { AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); m_interface = CreateIPinningIndex(); - THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); + + if (m_version != m_interface->GetVersion()) + { + if (disposition == SQLiteStorageBase::OpenDisposition::ReadWrite) + { + // Attempt to migrate from the stored version to the current version. + AICLI_LOG(Repo, Info, << "Attempting to migrate Pinning Index from [" << m_version << "] to [" << m_interface->GetVersion() << "]"); + + // Create an interface representing the existing (older) schema so MigrateFrom can inspect it. + std::unique_ptr oldInterface = CreateIPinningIndexForVersion(m_version); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_migrate"); + bool migrated = m_interface->MigrateFrom(m_dbconn, oldInterface.get()); + + if (migrated) + { + m_interface->GetVersion().SetSchemaVersion(m_dbconn); + SetLastWriteTime(); + savepoint.Commit(); + m_version = m_interface->GetVersion(); + AICLI_LOG(Repo, Info, << "Migration successful"); + } + else + { + AICLI_LOG(Repo, Error, << "Migration failed"); + THROW_HR(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX); + } + } + else + { + // Read-only open: use an interface matching the stored schema version to avoid querying missing columns. + AICLI_LOG(Repo, Info, << "Read-only open with older schema [" << m_version << "]; using compatible interface"); + m_interface = CreateIPinningIndexForVersion(m_version); + } + } } PinningIndex::PinningIndex(const std::string& target, SQLite::Version version) : SQLiteStorageBase(target, version) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index 672f8dba8f..346766fd87 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -70,6 +70,9 @@ namespace AppInstaller::Repository::Microsoft // Creates the IPinningIndex interface object for this version. std::unique_ptr CreateIPinningIndex() const; + // Creates an IPinningIndex interface object for a specific version. + static std::unique_ptr CreateIPinningIndexForVersion(const SQLite::Version& version); + std::unique_ptr m_interface; }; } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h index 8eba0be3d6..9fde5e84a1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h @@ -17,6 +17,10 @@ namespace AppInstaller::Repository::Microsoft::Schema // Creates all of the version dependent tables within the database. virtual void CreateTables(SQLite::Connection& connection) = 0; + // Migrates the schema from an older version. + // Returns true if migration succeeded, false if migration is not applicable. + virtual bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) = 0; + // Version 1.0 // Adds a pin to the index. virtual SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h index 7d7a38f0dc..42f744c68d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h @@ -10,6 +10,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Version 1.0 SQLite::Version GetVersion() const override; void CreateTables(SQLite::Connection& connection) override; + bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; private: SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp index 2d81684846..9d81c5a087 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp @@ -35,6 +35,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 savepoint.Commit(); } + bool PinningIndexInterface::MigrateFrom(SQLite::Connection&, const IPinningIndex*) + { + // Version 1.0 cannot migrate from any prior version. + return false; + } + SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { auto existingPin = GetExistingPinId(connection, pin.GetKey()); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp new file mode 100644 index 0000000000..aa795aef8e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinTable.h" +#include +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + namespace + { + std::optional GetPinFromRow( + std::string_view packageId, + std::string_view sourceId, + Pinning::PinType type, + std::string_view version, + std::string_view dateAdded, + std::optional note) + { + std::optional result; + + switch (type) + { + case Pinning::PinType::Blocking: + result = Pinning::Pin::CreateBlockingPin({ packageId, sourceId }); + break; + case Pinning::PinType::Pinning: + result = Pinning::Pin::CreatePinningPin({ packageId, sourceId }); + break; + case Pinning::PinType::Gating: + result = Pinning::Pin::CreateGatingPin({ packageId, sourceId }, Utility::GatedVersion{ version }); + break; + default: + return {}; + } + + result->SetDateAdded(std::string{ dateAdded }); + result->SetNote(std::move(note)); + + return result; + } + } + + using namespace std::string_view_literals; + static constexpr std::string_view s_PinTable_Table_Name = "pin"sv; + static constexpr std::string_view s_PinTable_PackageId_Column = "package_id"sv; + static constexpr std::string_view s_PinTable_SourceId_Column = "source_id"sv; + static constexpr std::string_view s_PinTable_Type_Column = "type"sv; + static constexpr std::string_view s_PinTable_Version_Column = "version"sv; + static constexpr std::string_view s_PinTable_DateAdded_Column = "date_added"sv; + static constexpr std::string_view s_PinTable_Note_Column = "note"sv; + static constexpr std::string_view s_PinTable_Index = "pin_index"sv; + + std::string_view PinTable::TableName() + { + return s_PinTable_Table_Name; + } + + void PinTable::Create(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); + + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_PinTable_Table_Name).BeginColumns(); + + createTableBuilder.Column(ColumnBuilder(s_PinTable_PackageId_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_SourceId_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Type_Column, Type::Int64).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Version_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_DateAdded_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_PinTable_Note_Column, Type::Text)); + + createTableBuilder.EndColumns(); + createTableBuilder.Execute(connection); + + // Create an index over the pairs package,source + StatementBuilder createIndexBuilder; + createIndexBuilder.CreateUniqueIndex(s_PinTable_Index).On(s_PinTable_Table_Name) + .Columns({ s_PinTable_PackageId_Column, s_PinTable_SourceId_Column }); + createIndexBuilder.Execute(connection); + + savepoint.Commit(); + } + + void PinTable::MigrateFrom1_0(SQLite::Connection& connection) + { + SQLite::Statement addDateAdded = SQLite::Statement::Create(connection, + "ALTER TABLE pin ADD COLUMN date_added TEXT NOT NULL DEFAULT ''"); + addDateAdded.Execute(); + + SQLite::Statement addNote = SQLite::Statement::Create(connection, + "ALTER TABLE pin ADD COLUMN note TEXT"); + addNote.Execute(); + } + + std::optional PinTable::GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::RowIDName).From(s_PinTable_Table_Name) + .Where(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) + .And(s_PinTable_SourceId_Column).Equals((std::string_view)pinKey.SourceId); + + SQLite::Statement select = builder.Prepare(connection); + + if (select.Step()) + { + return select.GetColumn(0); + } + else + { + return {}; + } + } + + SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); + builder.InsertInto(s_PinTable_Table_Name) + .Columns({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .Values( + (std::string_view)pinKey.PackageId, + pinKey.SourceId, + pin.GetType(), + pin.GetGatedVersion().ToString(), + (std::string_view)pin.GetDateAdded(), + pin.GetNote()); + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } + + bool PinTable::UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + SQLite::Builder::StatementBuilder builder; + const auto& pinKey = pin.GetKey(); + builder.Update(s_PinTable_Table_Name).Set() + .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) + .Column(s_PinTable_Type_Column).Equals(pin.GetType()) + .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_DateAdded_Column).Equals((std::string_view)pin.GetDateAdded()) + .Column(s_PinTable_Note_Column).Equals(pin.GetNote()) + .Where(SQLite::RowIDName).Equals(pinId); + + builder.Execute(connection); + return connection.GetChanges() != 0; + } + + void PinTable::RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId) + { + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); + builder.Execute(connection); + } + + std::optional PinTable::GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .From(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); + + SQLite::Statement select = builder.Prepare(connection); + + if (!select.Step()) + { + return {}; + } + + auto [packageId, sourceId, pinType, gatedVersion, dateAdded, note] = + select.GetRow>(); + return GetPinFromRow(packageId, sourceId, pinType, gatedVersion, dateAdded, std::move(note)); + } + + std::vector PinTable::GetAllPins(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ + s_PinTable_PackageId_Column, + s_PinTable_SourceId_Column, + s_PinTable_Type_Column, + s_PinTable_Version_Column, + s_PinTable_DateAdded_Column, + s_PinTable_Note_Column }) + .From(s_PinTable_Table_Name); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector pins; + while (select.Step()) + { + auto [packageId, sourceId, pinType, gatedVersion, dateAdded, note] = + select.GetRow>(); + auto pin = GetPinFromRow(packageId, sourceId, pinType, gatedVersion, dateAdded, std::move(note)); + if (pin) + { + pins.push_back(std::move(pin.value())); + } + } + + return pins; + } + + bool PinTable::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) + { + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_PinTable_Table_Name); + + if (!sourceId.empty()) + { + builder.Where(s_PinTable_SourceId_Column).Equals(sourceId); + } + + builder.Execute(connection); + + return connection.GetChanges() != 0; + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h new file mode 100644 index 0000000000..b36c954bfd --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include "Microsoft/Schema/IPinningIndex.h" +#include + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + struct PinTable + { + // Get the table name. + static std::string_view TableName(); + + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + // Migrates an existing v1.0 pin table by adding the date_added and note columns. + static void MigrateFrom1_0(SQLite::Connection& connection); + + // Gets the row ID for the pin, if it exists. + static std::optional GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey); + + // Adds a new pin. Returns the row ID of the added pin. + static SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin); + + // Updates an existing pin. + // Returns a value indicating whether there were any changes. + static bool UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); + + // Removes a pin given its row ID. + static void RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId); + + // Gets a pin by its row ID if it exists. + // Used for testing + static std::optional GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId); + + // Gets all the currently existing pins. + static std::vector GetAllPins(SQLite::Connection& connection); + + // Resets all pins from a given source, or from all sources if none is specified. + // Returns a value indicating whether there were any changes. + static bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId = {}); + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h new file mode 100644 index 0000000000..5e6e58c441 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/IPinningIndex.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + struct PinningIndexInterface : public IPinningIndex + { + // Version 1.1 + SQLite::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; + + private: + SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; + std::vector GetAllPins(SQLite::Connection& connection) override; + bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp new file mode 100644 index 0000000000..0d65a8d3c0 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h" +#include "Microsoft/Schema/Pinning_1_0/PinTable.h" +#include "Microsoft/Schema/Pinning_1_1/PinTable.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 +{ + namespace + { + std::optional GetExistingPinId(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto result = PinTable::GetIdByPinKey(connection, pinKey); + + if (!result) + { + AICLI_LOG(Repo, Verbose, << "Did not find pin " << pinKey.ToString()); + } + + return result; + } + } + + // Version 1.1 + SQLite::Version PinningIndexInterface::GetVersion() const + { + return { 1, 1 }; + } + + void PinningIndexInterface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); + Pinning_V1_1::PinTable::Create(connection); + savepoint.Commit(); + } + + bool PinningIndexInterface::MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) + { + if (!current || current->GetVersion() != SQLite::Version{ 1, 0 }) + { + return false; + } + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migratepintable_v1_0_to_v1_1"); + Pinning_V1_1::PinTable::MigrateFrom1_0(connection); + savepoint.Commit(); + + return true; + } + + SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + auto existingPin = GetExistingPinId(connection, pin.GetKey()); + + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), existingPin.has_value()); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_1"); + SQLite::rowid_t pinId = PinTable::AddPin(connection, pin); + + savepoint.Commit(); + return pinId; + } + + std::pair PinningIndexInterface::UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + auto existingPinId = GetExistingPinId(connection, pin.GetKey()); + + // If the pin doesn't exist, fail the update + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_1"); + bool status = PinTable::UpdatePinById(connection, existingPinId.value(), pin); + + savepoint.Commit(); + return { status, existingPinId.value() }; + } + + SQLite::rowid_t PinningIndexInterface::RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = GetExistingPinId(connection, pinKey); + + // If the pin doesn't exist, fail the remove + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "removepin_v1_1"); + PinTable::RemovePinById(connection, existingPinId.value()); + + savepoint.Commit(); + return existingPinId.value(); + } + + std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + { + auto existingPinId = GetExistingPinId(connection, pinKey); + + if (!existingPinId) + { + return {}; + } + + return PinTable::GetPinById(connection, existingPinId.value()); + } + + std::vector PinningIndexInterface::GetAllPins(SQLite::Connection& connection) + { + return PinTable::GetAllPins(connection); + } + + bool PinningIndexInterface::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "resetpins_v1_1"); + bool result = PinTable::ResetAllPins(connection, sourceId); + savepoint.Commit(); + + return result; + } +} From 697ca1dc6553cdfc0e62cf66ea257b693cacf16b Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 10:40:46 -0500 Subject: [PATCH 02/46] Add tests --- src/AppInstallerCLITests/PinFlow.cpp | 219 +++++++++++++++++- src/AppInstallerCLITests/PinningIndex.cpp | 171 ++++++++++++++ .../Microsoft/PinningIndex.cpp | 2 +- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 16 +- 4 files changed, 403 insertions(+), 5 deletions(-) diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp index ade8c5b252..bf7687191c 100644 --- a/src/AppInstallerCLITests/PinFlow.cpp +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -198,4 +198,221 @@ TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]") INFO(pinResetOutput.str()); REQUIRE(pinResetOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos); -} \ No newline at end of file +} + +TEST_CASE("PinFlow_Add_SetsDateAdded", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); + addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + addContext.Args.AddArg(Execution::Args::Type::BlockingPin); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE_FALSE(pins[0].GetDateAdded().empty()); +} + +TEST_CASE("PinFlow_Add_WithNote", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); + addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + addContext.Args.AddArg(Execution::Args::Type::PinNote, "my test note"sv); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetNote().has_value()); + REQUIRE(pins[0].GetNote().value() == "my test note"); +} + +TEST_CASE("PinFlow_Add_WithoutNote", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + std::ostringstream pinAddOutput; + TestContext addContext{ pinAddOutput, std::cin }; + OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); + addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); + + PinAddCommand pinAdd({}); + pinAdd.Execute(addContext); + INFO(pinAddOutput.str()); + + auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); + auto pins = index.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE_FALSE(pins[0].GetNote().has_value()); +} + +// Helper: Creates a v1.1 pinning index at the given path and adds the provided pins directly. +// Each pin should already have date_added and note set as desired. +namespace +{ + void PopulatePinIndexForShow(const std::filesystem::path& indexPath, const std::vector& pins) + { + PinningIndex index = PinningIndex::CreateNew(indexPath.u8string(), AppInstaller::SQLite::Version::Latest()); + for (const auto& pin : pins) + { + index.AddPin(pin); + } + } +} + +TEST_CASE("PinFlow_Show_NoMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin existingPin = Pin::CreateBlockingPin({ "SomePackage.Id", "sourceId" }); + existingPin.SetDateAdded("2026-01-15 10:00:00"); + PopulatePinIndexForShow(indexFile.GetPath(), { existingPin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "ThisQueryMatchesNothing"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_TERMINATED_WITH(showContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); + REQUIRE(showOutput.str().find(Resource::LocString(Resource::String::PinShowNoMatchFound)) != std::string::npos); +} + +TEST_CASE("PinFlow_Show_MatchById", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "MyApp.Package", "sourceId" }); + pin.SetDateAdded("2026-06-01 09:00:00"); + pin.SetNote(std::string{ "keep this one" }); + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Id, "MyApp.Package"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("MyApp.Package") != std::string::npos); + REQUIRE(showOutput.str().find("Blocking") != std::string::npos); + REQUIRE(showOutput.str().find("2026-06-01 09:00:00") != std::string::npos); + REQUIRE(showOutput.str().find("keep this one") != std::string::npos); +} + +TEST_CASE("PinFlow_Show_MatchByQuery", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "Contoso.AppOne", "sourceId" }); + pin.SetDateAdded("2026-03-10 12:00:00"); + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + // Partial, case-insensitive match on the package ID + showContext.Args.AddArg(Execution::Args::Type::Query, "appone"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("Contoso.AppOne") != std::string::npos); +} + +TEST_CASE("PinFlow_Show_ExactMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + // Two pins sharing a prefix + Pin pinA = Pin::CreateBlockingPin({ "Vendor.App", "src" }); + pinA.SetDateAdded("2026-01-01 00:00:00"); + + Pin pinB = Pin::CreateBlockingPin({ "Vendor.AppExtra", "src" }); + pinB.SetDateAdded("2026-01-01 00:00:00"); + + PopulatePinIndexForShow(indexFile.GetPath(), { pinA, pinB }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Id, "Vendor.App"sv); + showContext.Args.AddArg(Execution::Args::Type::Exact); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + // Only the exact-match pin should appear + REQUIRE(showOutput.str().find("Vendor.App") != std::string::npos); + // The inexact match should NOT appear + REQUIRE(showOutput.str().find("Vendor.AppExtra") == std::string::npos); +} + +TEST_CASE("PinFlow_Show_NoNote_DoesNotShowNoteLabel", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "NoNote.Package", "src" }); + pin.SetDateAdded("2026-05-01 08:00:00"); + // note intentionally not set + PopulatePinIndexForShow(indexFile.GetPath(), { pin }); + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "NoNote.Package"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_FALSE(showContext.IsTerminated()); + REQUIRE(showOutput.str().find("NoNote.Package") != std::string::npos); + REQUIRE(showOutput.str().find(Resource::LocString(Resource::String::PinShowLabelNote)) == std::string::npos); +} + +TEST_CASE("PinFlow_Show_EmptyIndex_NoMatch", "[PinFlow][workflow]") +{ + TempFile indexFile("pinningIndex", ".db"); + TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); + + // Create an empty index (no pins) + { PinningIndex::CreateNew(indexFile.GetPath().u8string(), AppInstaller::SQLite::Version::Latest()); } + + std::ostringstream showOutput; + TestContext showContext{ showOutput, std::cin }; + showContext.Args.AddArg(Execution::Args::Type::Query, "AnyQuery"sv); + + PinShowCommand pinShow({}); + pinShow.Execute(showContext); + INFO(showOutput.str()); + + REQUIRE_TERMINATED_WITH(showContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST); +} diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index 0682dc31f6..00ea3945d2 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include using namespace std::string_literals; @@ -163,4 +164,174 @@ TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]") index.AddPin(pin); REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS); +} + +TEST_CASE("PinningIndexCreateLatest_Is_V1_1", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + REQUIRE(index.GetVersion() == Version{ 1, 1 }); +} + +TEST_CASE("PinningIndex_V1_1_AddPin_WithDateAndNote", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "pkgId", "sourceId" }); + pin.SetDateAdded("2026-01-15 10:30:00"); + pin.SetNote(std::string{ "test note" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + + auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex->GetType() == PinType::Blocking); + REQUIRE(pinFromIndex->GetKey().PackageId == "pkgId"); + REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 10:30:00"); + REQUIRE(pinFromIndex->GetNote().has_value()); + REQUIRE(pinFromIndex->GetNote().value() == "test note"); + } +} + +TEST_CASE("PinningIndex_V1_1_AddPin_WithoutNote", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "pkgId", "sourceId" }); + pin.SetDateAdded("2026-01-15 10:30:00"); + // note intentionally left unset + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + + auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 10:30:00"); + REQUIRE_FALSE(pinFromIndex->GetNote().has_value()); + } +} + +TEST_CASE("PinningIndex_V1_1_AddUpdateRemove", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreateBlockingPin({ "pkgId", "srcId" }); + pin.SetDateAdded("2026-01-15 10:00:00"); + pin.SetNote(std::string{ "original note" }); + + Pin updatedPin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*"sv }); + updatedPin.SetDateAdded("2026-01-15 11:00:00"); + updatedPin.SetNote(std::string{ "updated note" }); + + { + PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); + index.AddPin(pin); + REQUIRE(index.UpdatePin(updatedPin)); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); + + auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); + REQUIRE(pinFromIndex.has_value()); + REQUIRE(pinFromIndex->GetType() == PinType::Gating); + REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 11:00:00"); + REQUIRE(pinFromIndex->GetNote().has_value()); + REQUIRE(pinFromIndex->GetNote().value() == "updated note"); + } + + { + PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + index.RemovePin(updatedPin.GetKey()); + } + + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + REQUIRE(Pinning_V1_1::PinTable::GetAllPins(connection).empty()); + REQUIRE(!Pinning_V1_1::PinTable::GetPinById(connection, 1)); + } +} + +TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin1 = Pin::CreateBlockingPin({ "pkg1", "src1" }); + Pin pin2 = Pin::CreateGatingPin({ "pkg2", "src2" }, { "2.*"sv }); + + // Create a v1.0 index and add two pins (no date_added or note columns yet) + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin1); + index.AddPin(pin2); + REQUIRE(index.GetVersion() == Version{ 1, 0 }); + } + + // Re-open with ReadWrite: should trigger automatic migration to v1.1 + { + PinningIndex migratedIndex = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + REQUIRE(migratedIndex.GetVersion() == Version{ 1, 1 }); + + auto pins = migratedIndex.GetAllPins(); + REQUIRE(pins.size() == 2); + + for (const auto& pin : pins) + { + // Migration adds columns with DEFAULT '' and NULL respectively + REQUIRE(pin.GetDateAdded() == ""); + REQUIRE_FALSE(pin.GetNote().has_value()); + } + + // Verify that the original pin data is still intact + auto foundPin1 = migratedIndex.GetPin(pin1.GetKey()); + REQUIRE(foundPin1.has_value()); + REQUIRE(foundPin1->GetType() == PinType::Blocking); + + auto foundPin2 = migratedIndex.GetPin(pin2.GetKey()); + REQUIRE(foundPin2.has_value()); + REQUIRE(foundPin2->GetType() == PinType::Gating); + REQUIRE(foundPin2->GetGatedVersion().ToString() == pin2.GetGatedVersion().ToString()); + } +} + +TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1_ReadOnly_Uses_OldInterface", "[pinningIndex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Pin pin = Pin::CreatePinningPin({ "pkgId", "srcId" }); + + // Create a v1.0 index and add a pin + { + PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 }); + index.AddPin(pin); + } + + // Re-open with Read (read-only): should NOT migrate, should use v1.0 interface + { + PinningIndex readOnlyIndex = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read); + REQUIRE(readOnlyIndex.GetVersion() == Version{ 1, 0 }); + + auto pins = readOnlyIndex.GetAllPins(); + REQUIRE(pins.size() == 1); + REQUIRE(pins[0].GetType() == PinType::Pinning); + REQUIRE(pins[0].GetKey() == pin.GetKey()); + } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index 42b96ebf5b..c0b8a8150b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -252,7 +252,7 @@ namespace AppInstaller::Repository::Microsoft PinningIndex::PinningIndex(const std::string& target, SQLite::Version version) : SQLiteStorageBase(target, version) { - m_interface = CreateIPinningIndex(); + m_interface = CreateIPinningIndexForVersion(version); m_version = m_interface->GetVersion(); } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index aa795aef8e..88f819d9a7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -147,10 +147,20 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) .Column(s_PinTable_Type_Column).Equals(pin.GetType()) .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) - .Column(s_PinTable_DateAdded_Column).Equals((std::string_view)pin.GetDateAdded()) - .Column(s_PinTable_Note_Column).Equals(pin.GetNote()) - .Where(SQLite::RowIDName).Equals(pinId); + .Column(s_PinTable_DateAdded_Column).Equals((std::string_view)pin.GetDateAdded()); + // Use Unbound (= ?) for null note so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. + const auto& note = pin.GetNote(); + if (note.has_value()) + { + builder.Column(s_PinTable_Note_Column).Equals(note.value()); + } + else + { + builder.Column(s_PinTable_Note_Column).Equals(SQLite::Builder::Unbound); + } + + builder.Where(SQLite::RowIDName).Equals(pinId); builder.Execute(connection); return connection.GetChanges() != 0; } From b6502ef3b96512ed82e8d7859c8db06ad324d730 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 11:10:02 -0500 Subject: [PATCH 03/46] Add pinning to COM APIs --- .../Package.appxmanifest | 2 + .../ComClsids.cpp | 133 ++++----- .../Converters.cpp | 23 ++ .../Converters.h | 1 + .../Microsoft.Management.Deployment.vcxproj | 6 + .../PackageManager.cpp | 270 ++++++++++++++++++ .../PackageManager.h | 6 + .../PackageManager.idl | 126 +++++++- .../PackagePin.cpp | 75 +++++ .../PackagePin.h | 36 +++ .../PinPackageOptions.cpp | 67 +++++ .../PinPackageOptions.h | 50 ++++ .../PinPackageResult.cpp | 27 ++ .../PinPackageResult.h | 27 ++ .../Public/ComClsids.h | 3 + 15 files changed, 787 insertions(+), 65 deletions(-) create mode 100644 src/Microsoft.Management.Deployment/PackagePin.cpp create mode 100644 src/Microsoft.Management.Deployment/PackagePin.h create mode 100644 src/Microsoft.Management.Deployment/PinPackageOptions.cpp create mode 100644 src/Microsoft.Management.Deployment/PinPackageOptions.h create mode 100644 src/Microsoft.Management.Deployment/PinPackageResult.cpp create mode 100644 src/Microsoft.Management.Deployment/PinPackageResult.h diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest index 8559da43ae..0fac2aad64 100644 --- a/src/AppInstallerCLIPackage/Package.appxmanifest +++ b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -86,6 +86,8 @@ + + diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp index 24c64ec942..daf023da93 100644 --- a/src/Microsoft.Management.Deployment/ComClsids.cpp +++ b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -1,64 +1,65 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Public/ComClsids.h" -#pragma warning( push ) -#pragma warning ( disable : 4467 ) -// 4467 Allow use of uuid attribute for com object creation. -#include "PackageManager.h" -#include "FindPackagesOptions.h" -#include "CreateCompositePackageCatalogOptions.h" -#include "InstallOptions.h" -#include "UninstallOptions.h" -#include "PackageMatchFilter.h" -#include "PackageManagerSettings.h" -#include "DownloadOptions.h" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/ComClsids.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 ) +// 4467 Allow use of uuid attribute for com object creation. +#include "PackageManager.h" +#include "FindPackagesOptions.h" +#include "CreateCompositePackageCatalogOptions.h" +#include "InstallOptions.h" +#include "UninstallOptions.h" +#include "PackageMatchFilter.h" +#include "PackageManagerSettings.h" +#include "DownloadOptions.h" #include "AuthenticationArguments.h" #include "RepairOptions.h" #include "AddPackageCatalogOptions.h" -#include "RemovePackageCatalogOptions.h" -#include "EditPackageCatalogOptions.h" -#pragma warning( pop ) - -namespace winrt::Microsoft::Management::Deployment -{ - CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid) - { - if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManager)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManager); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_FindPackagesOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::FindPackagesOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_CreateCompositePackageCatalogOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::CreateCompositePackageCatalogOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_InstallOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::InstallOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_UninstallOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::UninstallOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_DownloadOptions)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::DownloadOptions); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageMatchFilter)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_AuthenticationArguments)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::AuthenticationArguments); - } - else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) - { - return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); +#include "RemovePackageCatalogOptions.h" +#include "EditPackageCatalogOptions.h" +#include "PinPackageOptions.h" +#pragma warning( pop ) + +namespace winrt::Microsoft::Management::Deployment +{ + CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid) + { + if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManager)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManager); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_FindPackagesOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::FindPackagesOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_CreateCompositePackageCatalogOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::CreateCompositePackageCatalogOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_InstallOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::InstallOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_UninstallOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::UninstallOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_DownloadOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::DownloadOptions); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageMatchFilter)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_AuthenticationArguments)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::AuthenticationArguments); + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); } else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_RepairOptions)) { @@ -75,10 +76,14 @@ namespace winrt::Microsoft::Management::Deployment else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::EditPackageCatalogOptions); - } - else - { - return CLSID_NULL; - } - } + } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PinPackageOptions)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PinPackageOptions); + } + else + { + return CLSID_NULL; + } + } } diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index 3c3b4f238b..bbd82c6e2e 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -543,6 +543,29 @@ namespace winrt::Microsoft::Management::Deployment::implementation } } + PinResultStatus GetPinOperationStatus(winrt::hresult hresult) + { + switch (hresult) + { + case S_OK: + return PinResultStatus::Ok; + case APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY: + case APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED: + return PinResultStatus::BlockedByPolicy; + case APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS: + return PinResultStatus::PackagePinAlreadyExists; + case APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST: + return PinResultStatus::PackagePinNotFound; + case E_INVALIDARG: + case APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS: + return PinResultStatus::InvalidOptions; + default: + return PinResultStatus::InternalError; + } + } + ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(WindowsPlatform value) { switch (value) diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index 643658705c..888bc72ff6 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -34,6 +34,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::AddPackageCatalogStatus GetAddPackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); + winrt::Microsoft::Management::Deployment::PinResultStatus GetPinOperationStatus(winrt::hresult hresult); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj index 86dbab6088..89f91e9222 100644 --- a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj +++ b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -171,6 +171,9 @@ + + + @@ -223,6 +226,9 @@ + + + diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 39d53771b5..5516ebefd6 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -20,6 +20,9 @@ #include // 4467 Allow use of uuid attribute for com object creation. #include "PackageManager.h" +#include "PackagePin.h" +#include "PinPackageOptions.h" +#include "PinPackageResult.h" #pragma warning( pop ) #include "PackageManager.g.cpp" #include "CatalogPackage.h" @@ -40,6 +43,10 @@ #include "AppInstallerRuntime.h" #include #include +#include +#include +#include +#include using namespace std::literals::chrono_literals; using namespace ::AppInstaller::CLI; @@ -1488,5 +1495,268 @@ namespace winrt::Microsoft::Management::Deployment::implementation return GetEditPackageCatalogResult(terminationHR); } + namespace + { + // Builds PackagePin WinRT objects from a vector of internal Pin objects. + winrt::Windows::Foundation::Collections::IVectorView + MakePackagePinVectorView(const std::vector<::AppInstaller::Pinning::Pin>& pins) + { + auto result = winrt::single_threaded_vector(); + for (const auto& pin : pins) + { + auto packagePin = winrt::make_self>(); + packagePin->Initialize(pin); + result.Append(*packagePin); + } + return result.GetView(); + } + + // Collects PinKeys for all available sources of a package, and optionally for its installed identity. + std::vector<::AppInstaller::Pinning::PinKey> GetPinKeysForCatalogPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage const& package, + bool includeInstalled) + { + std::vector<::AppInstaller::Pinning::PinKey> pinKeys; + + // Keys for each available source version + for (auto const& versionId : package.AvailableVersions()) + { + auto versionInfo = package.GetPackageVersionInfo(versionId); + if (versionInfo) + { + std::string packageId = winrt::to_string(package.Id()); + std::string sourceId = winrt::to_string(versionInfo.PackageCatalog().Info().Id()); + if (!packageId.empty() && !sourceId.empty()) + { + ::AppInstaller::Pinning::PinKey key{ packageId, sourceId }; + // Avoid duplicates (same packageId+sourceId may appear for multiple versions) + if (std::find(pinKeys.begin(), pinKeys.end(), key) == pinKeys.end()) + { + pinKeys.push_back(std::move(key)); + } + } + } + } + + // Key for the installed package identity (ProductCode / PackageFamilyName) + if (includeInstalled) + { + auto installedVersion = package.InstalledVersion(); + if (installedVersion) + { + // Prefer PackageFamilyName (MSIX), fall back to ProductCode (MSI/EXE) + auto pfns = installedVersion.PackageFamilyNames(); + if (pfns && pfns.Size() > 0) + { + for (auto const& pfn : pfns) + { + pinKeys.push_back(::AppInstaller::Pinning::PinKey::GetPinKeyForInstalled(winrt::to_string(pfn))); + } + } + else + { + auto productCodes = installedVersion.ProductCodes(); + if (productCodes) + { + for (auto const& productCode : productCodes) + { + pinKeys.push_back(::AppInstaller::Pinning::PinKey::GetPinKeyForInstalled(winrt::to_string(productCode))); + } + } + } + } + } + + return pinKeys; + } + + winrt::Microsoft::Management::Deployment::PinPackageResult MakePinPackageResult(HRESULT hr) + { + auto result = winrt::make_self>(); + result->Initialize(GetPinOperationStatus(hr), hr); + return *result; + } + + // Converts PinPackageOptions.PinType to the internal pin representation. + ::AppInstaller::Pinning::Pin CreatePinFromOptions( + const ::AppInstaller::Pinning::PinKey& pinKey, + winrt::Microsoft::Management::Deployment::PinPackageOptions const& options) + { + switch (options.PinType()) + { + case winrt::Microsoft::Management::Deployment::PackagePinType::Blocking: + return ::AppInstaller::Pinning::Pin::CreateBlockingPin(pinKey); + case winrt::Microsoft::Management::Deployment::PackagePinType::Gating: + return ::AppInstaller::Pinning::Pin::CreateGatingPin( + pinKey, + ::AppInstaller::Utility::GatedVersion{ winrt::to_string(options.GatedVersion()) }); + default: + return ::AppInstaller::Pinning::Pin::CreatePinningPin(pinKey); + } + } + } + + winrt::Windows::Foundation::Collections::IVectorView + PackageManager::GetAllPins() + { + LogStartupIfApplicable(); + + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadOnly }; + return MakePackagePinVectorView(pinningData.GetAllPins()); + } + + winrt::Windows::Foundation::Collections::IVectorView + PackageManager::GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package) + { + LogStartupIfApplicable(); + + THROW_HR_IF_NULL(E_POINTER, package); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageQuery)); + + auto pinKeys = GetPinKeysForCatalogPackage(package, /* includeInstalled */ true); + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadOnly }; + + std::vector<::AppInstaller::Pinning::Pin> pins; + for (const auto& pinKey : pinKeys) + { + auto pin = pinningData.GetPin(pinKey); + if (pin) + { + pins.push_back(std::move(*pin)); + } + } + + return MakePackagePinVectorView(pins); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::PinPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage package, + winrt::Microsoft::Management::Deployment::PinPackageOptions options) + { + LogStartupIfApplicable(); + + HRESULT terminationHR = S_OK; + try + { + THROW_HR_IF_NULL(E_POINTER, package); + THROW_HR_IF_NULL(E_POINTER, options); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + // Gating pins require a non-empty version range. + if (options.PinType() == winrt::Microsoft::Management::Deployment::PackagePinType::Gating && + options.GatedVersion().empty()) + { + THROW_HR(E_INVALIDARG); + } + + auto pinKeys = GetPinKeysForCatalogPackage(package, options.PinInstalledPackage()); + THROW_HR_IF(E_INVALIDARG, pinKeys.empty()); + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + + std::string dateAdded = ::AppInstaller::Utility::TimePointToString( + std::chrono::system_clock::now(), + ::AppInstaller::Utility::TimeFacet::Year | ::AppInstaller::Utility::TimeFacet::Month | + ::AppInstaller::Utility::TimeFacet::Day | ::AppInstaller::Utility::TimeFacet::Hour | + ::AppInstaller::Utility::TimeFacet::Minute | ::AppInstaller::Utility::TimeFacet::Second); + + std::optional note; + if (!options.Note().empty()) + { + note = winrt::to_string(options.Note()); + } + + for (const auto& pinKey : pinKeys) + { + auto newPin = CreatePinFromOptions(pinKey, options); + + auto existingPin = pinningData.GetPin(pinKey); + if (existingPin && !(*existingPin == newPin)) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS, !options.Force()); + } + + newPin.SetDateAdded(dateAdded); + newPin.SetNote(note); + pinningData.AddOrUpdatePin(newPin); + } + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::UnpinPackage( + winrt::Microsoft::Management::Deployment::CatalogPackage package) + { + LogStartupIfApplicable(); + + HRESULT terminationHR = S_OK; + try + { + THROW_HR_IF_NULL(E_POINTER, package); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + auto pinKeys = GetPinKeysForCatalogPackage(package, /* includeInstalled */ true); + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + + bool anyRemoved = false; + for (const auto& pinKey : pinKeys) + { + auto existingPin = pinningData.GetPin(pinKey); + if (existingPin) + { + pinningData.RemovePin(pinKey); + anyRemoved = true; + } + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST, !anyRemoved); + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::ResetAllPins(winrt::hstring const& sourceName) + { + LogStartupIfApplicable(); + + HRESULT terminationHR = S_OK; + try + { + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + std::string sourceId; + if (!sourceName.empty()) + { + auto matchingSource = GetMatchingSource(winrt::to_string(sourceName)); + if (matchingSource.has_value()) + { + sourceId = matchingSource->Identifier; + } + } + + auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; + pinningData.ResetAllPins(sourceId); + } + catch (...) + { + terminationHR = AppInstaller::CLI::Workflow::HandleException(nullptr, std::current_exception()); + } + + return MakePinPackageResult(terminationHR); + } + CoCreatableMicrosoftManagementDeploymentClass(PackageManager); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index c973df2dbe..d4c2ed5397 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -54,6 +54,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::hstring Version() const; // Contract 28.0 winrt::Microsoft::Management::Deployment::EditPackageCatalogResult EditPackageCatalog(winrt::Microsoft::Management::Deployment::EditPackageCatalogOptions options); + // Contract 30.0 + winrt::Windows::Foundation::Collections::IVectorView GetAllPins(); + winrt::Windows::Foundation::Collections::IVectorView GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package); + winrt::Microsoft::Management::Deployment::PinPackageResult PinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PinPackageOptions options); + winrt::Microsoft::Management::Deployment::PinPackageResult UnpinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package); + winrt::Microsoft::Management::Deployment::PinPackageResult ResetAllPins(winrt::hstring const& sourceName); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 25567032f2..6a5d6096ae 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(29)] // For version 1.29 + [contractversion(30)] // For version 1.30 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -1679,8 +1679,130 @@ namespace Microsoft.Management.Deployment /// Edit an existing Windows Package Catalog. EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + { + /// Get all pins across all sources. + Windows.Foundation.Collections.IVectorView GetAllPins(); + + /// Get the pins associated with the specified package. + /// Returns pins for all sources the package is available from, and for the installed + /// package identity if InstalledVersion is present. + Windows.Foundation.Collections.IVectorView GetPins(CatalogPackage package); + + /// Add or update a pin for the specified package. + /// Requires Force = true to overwrite an existing pin of a different type. + PinPackageResult PinPackage(CatalogPackage package, PinPackageOptions options); + + /// Remove all pins associated with the specified package. + /// Returns PackagePinNotFound if no pin exists for the package. + PinPackageResult UnpinPackage(CatalogPackage package); + + /// Reset (remove) all pins. If sourceName is non-empty, only pins from that + /// catalog source are removed; otherwise all pins are removed. + PinPackageResult ResetAllPins(String sourceName); + } } + /// IMPLEMENTATION NOTE: Pinning::PinType from AppInstaller::Pinning + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + enum PackagePinType + { + /// Unknown pin type or not pinned. + Unknown, + /// Pinned by the manifest using the RequiresExplicitUpgrade field. + /// Behaves the same as Pinning. + PinnedByManifest, + /// The package is excluded from upgrade --all, unless --include-pinned is added. + /// upgrade is not blocked. + Pinning, + /// The package is pinned to a specific version range. + Gating, + /// The package is blocked from upgrade --all and upgrade . + /// User must unpin to allow update. + Blocking, + }; + + /// IMPLEMENTATION NOTE: Pinning::Pin from AppInstaller::Pinning + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + runtimeclass PackagePin + { + /// The package ID that the pin applies to (for available-package pins) or the + /// ProductCode/PackageFamilyName (for installed-package pins). + String PackageId { get; }; + + /// The source identifier the pin applies to. Empty for installed-package pins. + String SourceId { get; }; + + /// The type of the pin. + PackagePinType Type { get; }; + + /// The gated version range. Only meaningful when Type is Gating; empty otherwise. + String GatedVersion { get; }; + + /// The UTC date/time when the pin was added (ISO 8601 format). + String DateAdded { get; }; + + /// Optional note associated with the pin. May be empty. + String Note { get; }; + + /// True if this pin is keyed on the installed package identity + /// (ProductCode or PackageFamilyName) rather than available package ID + source. + Boolean IsForInstalledPackage { get; }; + }; + + /// Options for adding or updating a package pin. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + runtimeclass PinPackageOptions + { + PinPackageOptions(); + + /// The type of pin to create. Required. + PackagePinType PinType; + + /// The gated version range. Required when PinType is Gating; ignored otherwise. + /// SAMPLE VALUE: "1.0.*" or "<2.0" + String GatedVersion; + + /// When true, the pin is keyed on the installed package identity (ProductCode or + /// PackageFamilyName) instead of the available package ID and source. + /// Mirrors the winget pin add --installed flag. Default is false. + Boolean PinInstalledPackage; + + /// When true, an existing pin with a different type will be overwritten. + /// When false and a pin already exists with a different type, the operation returns + /// PinResultStatus::PackagePinAlreadyExists. Default is false. + Boolean Force; + + /// Optional note to store with the pin. + String Note; + }; + + /// Status of a pin operation. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + enum PinResultStatus + { + Ok, + BlockedByPolicy, + InternalError, + InvalidOptions, + /// Returned by UnpinPackage when no pin exists for the package. + PackagePinNotFound, + /// Returned by PinPackage when a pin with a different type already exists + /// and Force is false. + PackagePinAlreadyExists, + }; + + /// Result of a pin operation. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + runtimeclass PinPackageResult + { + PinResultStatus Status { get; }; + + /// The error code of the overall operation. + HRESULT ExtendedErrorCode { get; }; + }; + /// Global settings for PackageManager operations. /// This settings should be invoked prior to invocation of PackageManager class. /// This settings is only exposed in in-proc Com invocation. @@ -1789,5 +1911,7 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView; interface Windows.Foundation.Collections.IVector; interface Windows.Foundation.Collections.IVectorView; + interface Windows.Foundation.Collections.IVector; + interface Windows.Foundation.Collections.IVectorView; } } diff --git a/src/Microsoft.Management.Deployment/PackagePin.cpp b/src/Microsoft.Management.Deployment/PackagePin.cpp new file mode 100644 index 0000000000..dd06b89ad1 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PackagePin.cpp @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PackagePin.h" +#include "PackagePin.g.cpp" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + namespace + { + winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type) + { + switch (type) + { + case ::AppInstaller::Pinning::PinType::PinnedByManifest: + return winrt::Microsoft::Management::Deployment::PackagePinType::PinnedByManifest; + case ::AppInstaller::Pinning::PinType::Pinning: + return winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; + case ::AppInstaller::Pinning::PinType::Gating: + return winrt::Microsoft::Management::Deployment::PackagePinType::Gating; + case ::AppInstaller::Pinning::PinType::Blocking: + return winrt::Microsoft::Management::Deployment::PackagePinType::Blocking; + default: + return winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; + } + } + } + + void PackagePin::Initialize(const ::AppInstaller::Pinning::Pin& pin) + { + m_packageId = winrt::to_hstring(pin.GetKey().PackageId); + m_sourceId = winrt::to_hstring(pin.GetKey().SourceId); + m_type = ConvertPinType(pin.GetType()); + m_gatedVersion = winrt::to_hstring(pin.GetGatedVersion().ToString()); + m_dateAdded = winrt::to_hstring(pin.GetDateAdded()); + m_note = pin.GetNote() ? winrt::to_hstring(*pin.GetNote()) : hstring{}; + m_isForInstalledPackage = pin.GetKey().IsForInstalled(); + } + + hstring PackagePin::PackageId() + { + return m_packageId; + } + + hstring PackagePin::SourceId() + { + return m_sourceId; + } + + winrt::Microsoft::Management::Deployment::PackagePinType PackagePin::Type() + { + return m_type; + } + + hstring PackagePin::GatedVersion() + { + return m_gatedVersion; + } + + hstring PackagePin::DateAdded() + { + return m_dateAdded; + } + + hstring PackagePin::Note() + { + return m_note; + } + + bool PackagePin::IsForInstalledPackage() + { + return m_isForInstalledPackage; + } +} diff --git a/src/Microsoft.Management.Deployment/PackagePin.h b/src/Microsoft.Management.Deployment/PackagePin.h new file mode 100644 index 0000000000..2d3c0f37ff --- /dev/null +++ b/src/Microsoft.Management.Deployment/PackagePin.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PackagePin.g.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct PackagePin : PackagePinT + { + PackagePin() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize(const ::AppInstaller::Pinning::Pin& pin); +#endif + + hstring PackageId(); + hstring SourceId(); + winrt::Microsoft::Management::Deployment::PackagePinType Type(); + hstring GatedVersion(); + hstring DateAdded(); + hstring Note(); + bool IsForInstalledPackage(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + hstring m_packageId; + hstring m_sourceId; + winrt::Microsoft::Management::Deployment::PackagePinType m_type = winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; + hstring m_gatedVersion; + hstring m_dateAdded; + hstring m_note; + bool m_isForInstalledPackage = false; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/PinPackageOptions.cpp b/src/Microsoft.Management.Deployment/PinPackageOptions.cpp new file mode 100644 index 0000000000..f7e4359c8e --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageOptions.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include +// 4467 Allow use of uuid attribute for com object creation. +#include "PinPackageOptions.h" +#pragma warning( pop ) +#include "PinPackageOptions.g.cpp" +#include "Helpers.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + winrt::Microsoft::Management::Deployment::PackagePinType PinPackageOptions::PinType() + { + return m_pinType; + } + + void PinPackageOptions::PinType(winrt::Microsoft::Management::Deployment::PackagePinType value) + { + m_pinType = value; + } + + hstring PinPackageOptions::GatedVersion() + { + return m_gatedVersion; + } + + void PinPackageOptions::GatedVersion(hstring const& value) + { + m_gatedVersion = value; + } + + bool PinPackageOptions::PinInstalledPackage() + { + return m_pinInstalledPackage; + } + + void PinPackageOptions::PinInstalledPackage(bool value) + { + m_pinInstalledPackage = value; + } + + bool PinPackageOptions::Force() + { + return m_force; + } + + void PinPackageOptions::Force(bool value) + { + m_force = value; + } + + hstring PinPackageOptions::Note() + { + return m_note; + } + + void PinPackageOptions::Note(hstring const& value) + { + m_note = value; + } + + CoCreatableMicrosoftManagementDeploymentClass(PinPackageOptions); +} diff --git a/src/Microsoft.Management.Deployment/PinPackageOptions.h b/src/Microsoft.Management.Deployment/PinPackageOptions.h new file mode 100644 index 0000000000..8200d32682 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageOptions.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PinPackageOptions.g.h" +#include "public/ComClsids.h" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions)] + struct PinPackageOptions : PinPackageOptionsT + { + PinPackageOptions() = default; + + winrt::Microsoft::Management::Deployment::PackagePinType PinType(); + void PinType(winrt::Microsoft::Management::Deployment::PackagePinType value); + + hstring GatedVersion(); + void GatedVersion(hstring const& value); + + bool PinInstalledPackage(); + void PinInstalledPackage(bool value); + + bool Force(); + void Force(bool value); + + hstring Note(); + void Note(hstring const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PackagePinType m_pinType = winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; + hstring m_gatedVersion; + bool m_pinInstalledPackage = false; + bool m_force = false; + hstring m_note; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct PinPackageOptions : + PinPackageOptionsT, + AppInstaller::WinRT::ModuleCountBase + { + }; +} +#endif diff --git a/src/Microsoft.Management.Deployment/PinPackageResult.cpp b/src/Microsoft.Management.Deployment/PinPackageResult.cpp new file mode 100644 index 0000000000..76e52f5336 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageResult.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PinPackageResult.h" +#include "PinPackageResult.g.cpp" +#include + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void PinPackageResult::Initialize( + winrt::Microsoft::Management::Deployment::PinResultStatus status, + winrt::hresult extendedErrorCode) + { + m_status = status; + m_extendedErrorCode = extendedErrorCode; + } + + winrt::Microsoft::Management::Deployment::PinResultStatus PinPackageResult::Status() + { + return m_status; + } + + winrt::hresult PinPackageResult::ExtendedErrorCode() + { + return m_extendedErrorCode; + } +} diff --git a/src/Microsoft.Management.Deployment/PinPackageResult.h b/src/Microsoft.Management.Deployment/PinPackageResult.h new file mode 100644 index 0000000000..f96d1ef343 --- /dev/null +++ b/src/Microsoft.Management.Deployment/PinPackageResult.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "PinPackageResult.g.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct PinPackageResult : PinPackageResultT + { + PinPackageResult() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize( + winrt::Microsoft::Management::Deployment::PinResultStatus status, + winrt::hresult extendedErrorCode); +#endif + + winrt::Microsoft::Management::Deployment::PinResultStatus Status(); + winrt::hresult ExtendedErrorCode(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::PinResultStatus m_status = winrt::Microsoft::Management::Deployment::PinResultStatus::Ok; + winrt::hresult m_extendedErrorCode = S_OK; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h index e9137d788f..0eede1998a 100644 --- a/src/Microsoft.Management.Deployment/Public/ComClsids.h +++ b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -18,6 +18,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "DB9D012D-00D7-47EE-8FB1-606E10AC4F51" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "032B1C58-B975-469B-A013-E632B6ECE8D8" #define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "A9F5E736-68CE-463C-BA6D-DE968F0CCE04" +#define WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions "93409EF2-29D0-46D3-8085-13EDE73939C4" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -32,6 +33,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_AddPackageCatalogOptions "D58C7E4C-70E6-476C-A5D4-80341ED80252" #define WINGET_OUTOFPROC_COM_CLSID_RemovePackageCatalogOptions "87A96609-1A39-4955-BE72-7174E147B7DC" #define WINGET_OUTOFPROC_COM_CLSID_EditPackageCatalogOptions "29B19238-81AD-4A8E-A2FC-ADF17C38CAEB" +#define WINGET_OUTOFPROC_COM_CLSID_PinPackageOptions "B3A61CCB-A3D0-497D-B300-A904904EEA56" #endif // Clsids only used in in-proc invocation @@ -53,6 +55,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_AddPackageCatalogOptions = { 0x24e6f1fa, 0xe4c3, 0x4acd, 0x96, 0x5d, 0xdf, 0x21, 0x3f, 0xd5, 0x8f, 0x15 }; // {24E6F1FA-E4C3-4ACD-965D-DF213FD58F15} const CLSID WINGET_INPROC_COM_CLSID_RemovePackageCatalogOptions = { 0x1125d3a6, 0xe2ce, 0x479a, 0x91, 0xd5, 0x71, 0xa3, 0xf6, 0xf8, 0xb0, 0xb }; // {1125D3A6-E2CE-479A-91D5-71A3F6F8B00B} const CLSID WINGET_INPROC_COM_CLSID_EditPackageCatalogOptions = { 0xe8e12fe1, 0xab77, 0x40c4, 0xa5, 0x62, 0xe9, 0x1f, 0xb5, 0x1b, 0x4e, 0x82 }; // {E8E12FE1-AB77-40C4-A562-E91FB51B4E82} + const CLSID WINGET_INPROC_COM_CLSID_PinPackageOptions = { 0xba9bb1af, 0x4453, 0x4274, 0xbb, 0xf9, 0x4c, 0x17, 0x79, 0x4e, 0xfa, 0x8e }; // {BA9BB1AF-4453-4274-BBF9-4C17794EFA8E} CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); } From ae483b07d10064aeae3008a065011173e0792af2 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 11:18:08 -0500 Subject: [PATCH 04/46] Add IsPinned propery for Get-WingetPackage --- .../Helpers/PackageManagerWrapper.cs | 12 ++++++++++++ .../PSObjects/PSInstalledCatalogPackage.cs | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs index f1ef5204e0..6e34ab089d 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -136,6 +136,18 @@ public PackageCatalogReference CreateCompositePackageCatalog(CreateCompositePack false); } + /// + /// Wrapper for GetPins. + /// + /// The package to get pins for. + /// A read-only list of PackagePin objects. + public IReadOnlyList GetPins(CatalogPackage package) + { + return this.Execute( + () => this.packageManager.GetPins(package), + false); + } + /// /// Gets the version of the package manager that is running. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs index 3c3dcb7519..d053cc204d 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Helpers; /// /// InstalledCatalogPackage wrapper object for displaying to PowerShell. @@ -31,6 +32,14 @@ public string InstalledVersion get { return this.CatalogPackageCOM.InstalledVersion.Version; } } + /// + /// Gets a value indicating whether the package is pinned. + /// + public bool IsPinned + { + get { return PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; } + } + /// /// Compares versions. /// From fac4ec49188f44eb4343fe632f3c7897b1c0c92e Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 12:25:22 -0500 Subject: [PATCH 05/46] Add pinning to PowerShell Cmdlets --- .../ClassesDefinition.cs | 32 +- .../WinGetProjectionFactory.cs | 12 +- .../Microsoft.WinGet.Client/Add-WinGetPin.md | 350 ++++++++++++++++++ .../Microsoft.WinGet.Client/Get-WinGetPin.md | 221 +++++++++++ .../Microsoft.WinGet.Client.md | 12 + .../Remove-WinGetPin.md | 276 ++++++++++++++ .../Reset-WinGetPin.md | 132 +++++++ .../Cmdlets/AddPinCmdlet.cs | 92 +++++ .../Cmdlets/GetPinCmdlet.cs | 74 ++++ .../Cmdlets/PSObjects/PSPackagePinType.cs | 29 ++ .../Cmdlets/RemovePinCmdlet.cs | 77 ++++ .../Cmdlets/ResetPinCmdlet.cs | 62 ++++ .../Common/Constants.cs | 15 + .../Commands/PinPackageCommand.cs | 179 +++++++++ .../Commands/ResetPinCommand.cs | 41 ++ .../Helpers/ManagementDeploymentFactory.cs | 14 + .../Helpers/PSEnumHelpers.cs | 16 + .../Helpers/PackageManagerWrapper.cs | 48 +++ .../PSObjects/PSPackagePin.cs | 83 +++++ .../PSObjects/PSPinResult.cs | 62 ++++ .../ModuleFiles/Microsoft.WinGet.Client.psd1 | 4 + 21 files changed, 1816 insertions(+), 15 deletions(-) create mode 100644 src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md create mode 100644 src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md create mode 100644 src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md create mode 100644 src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs index ecae3141cf..02c7d6109f 100644 --- a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs +++ b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -152,16 +152,28 @@ internal static class ClassesDefinition } }, - [typeof(EditPackageCatalogOptions)] = new() - { - ProjectedClassType = typeof(EditPackageCatalogOptions), - InterfaceType = typeof(IEditPackageCatalogOptions), - Clsids = new Dictionary() - { - [ClsidContext.InProc] = new Guid("E8E12FE1-AB77-40C4-A562-E91FB51B4E82"), - [ClsidContext.OutOfProc] = new Guid("A9F5E736-68CE-463C-BA6D-DE968F0CCE04"), - [ClsidContext.OutOfProcDev] = new Guid("29B19238-81AD-4A8E-A2FC-ADF17C38CAEB"), - } + [typeof(EditPackageCatalogOptions)] = new() + { + ProjectedClassType = typeof(EditPackageCatalogOptions), + InterfaceType = typeof(IEditPackageCatalogOptions), + Clsids = new Dictionary() + { + [ClsidContext.InProc] = new Guid("E8E12FE1-AB77-40C4-A562-E91FB51B4E82"), + [ClsidContext.OutOfProc] = new Guid("A9F5E736-68CE-463C-BA6D-DE968F0CCE04"), + [ClsidContext.OutOfProcDev] = new Guid("29B19238-81AD-4A8E-A2FC-ADF17C38CAEB"), + } + }, + + [typeof(PinPackageOptions)] = new() + { + ProjectedClassType = typeof(PinPackageOptions), + InterfaceType = typeof(IPinPackageOptions), + Clsids = new Dictionary() + { + [ClsidContext.InProc] = new Guid("BA9BB1AF-4453-4274-BBF9-4C17794EFA8E"), + [ClsidContext.OutOfProc] = new Guid("93409EF2-29D0-46D3-8085-13EDE73939C4"), + [ClsidContext.OutOfProcDev] = new Guid("B3A61CCB-A3D0-497D-B300-A904904EEA56"), + } } }; diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs index 356d1ff8b3..84f305b210 100644 --- a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs +++ b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -36,11 +36,13 @@ public WinGetProjectionFactory(IInstanceInitializer instanceInitializer) public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance(); public RepairOptions CreateRepairOptions() => InstanceInitializer.CreateInstance(); - - public AddPackageCatalogOptions CreateAddPackageCatalogOptions() => InstanceInitializer.CreateInstance(); - - public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); - + + public AddPackageCatalogOptions CreateAddPackageCatalogOptions() => InstanceInitializer.CreateInstance(); + + public RemovePackageCatalogOptions CreateRemovePackageCatalogOptions() => InstanceInitializer.CreateInstance(); + public EditPackageCatalogOptions CreateEditPackageCatalogOptions() => InstanceInitializer.CreateInstance(); + + public PinPackageOptions CreatePinPackageOptions() => InstanceInitializer.CreateInstance(); } } diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md new file mode 100644 index 0000000000..2992b76ed8 --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md @@ -0,0 +1,350 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Add-WinGetPin + +## SYNOPSIS +Adds a WinGet package pin. + +## SYNTAX + +### FoundSet (Default) +``` +Add-WinGetPin [-PinType ] [-GatedVersion ] [-PinInstalledPackage] + [-Force] [-Note ] [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### GivenSet +``` +Add-WinGetPin [-PinType ] [-GatedVersion ] [-PinInstalledPackage] + [-Force] [-Note ] [[-PSCatalogPackage] ] + [-ProgressAction ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +This command adds a pin for a WinGet package, preventing automatic updates. Mirrors the behavior +of `winget pin add`. By default, all string-based searches are case-insensitive exact matches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +Pin types: +- **Pinning** (default): Prevents automatic updates but allows manual upgrades. +- **Blocking**: Prevents all upgrades, including manual ones. +- **Gating**: Allows upgrades only to versions below the specified **GatedVersion**. + +## EXAMPLES + +### Example 1: Pin a package by Id +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" +``` +This example adds a Pinning pin for `Microsoft.PowerShell`, preventing automatic updates. + +### Example 2: Add a blocking pin +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" -PinType Blocking +``` +This example adds a Blocking pin for `Microsoft.PowerShell`, preventing all upgrades. + +### Example 3: Add a gating pin +```powershell +Add-WinGetPin -Id "Microsoft.PowerShell" -PinType Gating -GatedVersion "<7.5" +``` +This example adds a Gating pin that allows upgrades only to versions below 7.5. + +### Example 4: Pin a package using the pipeline +```powershell +Find-WinGetPackage -Id "Microsoft.PowerShell" | Add-WinGetPin +``` +This example pins a package passed from `Find-WinGetPackage`. + +### Example 5: Pin an installed package +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Add-WinGetPin -PinInstalledPackage +``` +This example adds a pin for the currently installed version of `Microsoft.PowerShell`. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Forces the pin operation even if an existing pin is already present. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -GatedVersion + +Specify the version range for a Gating pin. This parameter is required when **PinType** is +`Gating`. The value uses WinGet version range syntax (e.g., `<7.5`, `>=7.0,<8.0`). + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +exact match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: EqualsCaseInsensitive +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Note + +Specify an optional note to attach to the pin. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PinInstalledPackage + +When specified, pins the currently installed version of the package rather than all versions. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PinType + +Specify the type of pin to add. Accepted values: + +- **Pinning** (default): Prevents automatic upgrades. +- **Blocking**: Prevents all upgrades. +- **Gating**: Limits upgrades to versions below **GatedVersion**. + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackagePinType +Parameter Sets: (All) +Aliases: +Accepted values: Pinning, Blocking, Gating + +Required: False +Position: Named +Default value: Pinning +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +### Microsoft.WinGet.Client.PSObjects.PSPackagePinType + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md new file mode 100644 index 0000000000..09cb3000cf --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Get-WinGetPin.md @@ -0,0 +1,221 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Get-WinGetPin + +## SYNOPSIS +Gets WinGet package pins. + +## SYNTAX + +### AllSet (Default) +``` +Get-WinGetPin [] +``` + +### GivenSet +``` +Get-WinGetPin [[-PSCatalogPackage] ] [] +``` + +### FoundSet +``` +Get-WinGetPin [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [] +``` + +## DESCRIPTION +This command retrieves WinGet package pins. When called without parameters, it returns all pins. +When called with package search criteria or a catalog package, it returns pins for the matching +package. By default, all string-based searches are case-insensitive substring searches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +## EXAMPLES + +### Example 1: Get all pins +```powershell +Get-WinGetPin +``` +This example gets all package pins configured in WinGet. + +### Example 2: Get the pin for a specific package by Id +```powershell +Get-WinGetPin -Id "Microsoft.PowerShell" +``` +This example gets the pin for the package with the identifier `Microsoft.PowerShell`. + +### Example 3: Get the pin for a package using a query +```powershell +Get-WinGetPin -Query "PowerShell" +``` +This example gets pins for packages matching the query `PowerShell`. + +### Example 4: Get the pin for an installed package using the pipeline +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Get-WinGetPin +``` +This example gets the pin for an installed package passed from `Get-WinGetPackage`. + +## PARAMETERS + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +substring match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin + +## NOTES + +## RELATED LINKS + +[Add-WinGetPin](Add-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md index 9765cc16fb..c06de08783 100644 --- a/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Microsoft.WinGet.Client.md @@ -13,6 +13,9 @@ title: Microsoft.WinGet.Client Module Microsoft WinGet Client Module for the Windows Package Manager ## Microsoft.WinGet.Client Cmdlets +### [Add-WinGetPin](Add-WinGetPin.md) +Adds a WinGet package pin. + ### [Add-WinGetSource](Add-WinGetSource.md) Adds a new WinGet source. @@ -34,6 +37,9 @@ Searches configured sources for packages. ### [Get-WinGetPackage](Get-WinGetPackage.md) Gets installed packages. +### [Get-WinGetPin](Get-WinGetPin.md) +Gets WinGet package pins. + ### [Get-WinGetSetting](Get-WinGetSetting.md) Gets WinGet settings. @@ -49,6 +55,9 @@ Gets the installed version of WinGet. ### [Install-WinGetPackage](Install-WinGetPackage.md) Install a WinGet Package. +### [Remove-WinGetPin](Remove-WinGetPin.md) +Removes a WinGet package pin. + ### [Remove-WinGetSource](Remove-WinGetSource.md) Removes a configured source. @@ -58,6 +67,9 @@ Repairs a WinGet Package. ### [Repair-WinGetPackageManager](Repair-WinGetPackageManager.md) Repairs the WinGet client. +### [Reset-WinGetPin](Reset-WinGetPin.md) +Resets all WinGet package pins. + ### [Reset-WinGetSource](Reset-WinGetSource.md) Resets default WinGet sources. diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md new file mode 100644 index 0000000000..b55d58c831 --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Remove-WinGetPin.md @@ -0,0 +1,276 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Remove-WinGetPin + +## SYNOPSIS +Removes a WinGet package pin. + +## SYNTAX + +### FoundSet (Default) +``` +Remove-WinGetPin [-Id ] [-Name ] [-Moniker ] [-Source ] + [[-Query] ] [-MatchOption ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### GivenSet +``` +Remove-WinGetPin [[-PSCatalogPackage] ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +### PinSet +``` +Remove-WinGetPin [[-PSPackagePin] ] [-ProgressAction ] + [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION +This command removes a pin for a WinGet package. Mirrors the behavior of `winget pin remove`. +The command accepts a **PSPackagePin** object from the pipeline (e.g., from `Get-WinGetPin`), +a **PSCatalogPackage** object from `Get-WinGetPackage` or `Find-WinGetPackage`, or package +search criteria. By default, all string-based searches are case-insensitive exact matches. +Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. + +## EXAMPLES + +### Example 1: Remove a pin by package Id +```powershell +Remove-WinGetPin -Id "Microsoft.PowerShell" +``` +This example removes the pin for the package with the identifier `Microsoft.PowerShell`. + +### Example 2: Remove a pin using the pipeline from Get-WinGetPin +```powershell +Get-WinGetPin -Id "Microsoft.PowerShell" | Remove-WinGetPin +``` +This example removes a pin passed from `Get-WinGetPin`. + +### Example 3: Remove all pins +```powershell +Get-WinGetPin | Remove-WinGetPin +``` +This example removes all pins by piping the output of `Get-WinGetPin` to `Remove-WinGetPin`. + +### Example 4: Remove a pin using the pipeline from Get-WinGetPackage +```powershell +Get-WinGetPackage -Id "Microsoft.PowerShell" | Remove-WinGetPin +``` +This example removes the pin for an installed package passed from `Get-WinGetPackage`. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Id + +Specify the package identifier to search for. By default, the command does a case-insensitive +exact match. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -MatchOption + +Specify the match option for a WinGet package query. This parameter accepts the following values: + +```yaml +Type: Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption +Parameter Sets: FoundSet +Aliases: +Accepted values: Equals, EqualsCaseInsensitive, StartsWithCaseInsensitive, ContainsCaseInsensitive + +Required: False +Position: Named +Default value: EqualsCaseInsensitive +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Moniker + +Specify the moniker of the WinGet package to search for. For example, the moniker for the +Microsoft.PowerShell package is `pwsh`. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Name + +Specify the name of the WinGet package to search for. If the name contains spaces, enclose the +name in quotes. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PSCatalogPackage + +Provide a **PSCatalogPackage** object. You can get a **PSCatalogPackage** object by using the +`Find-WinGetPackage` or `Get-WinGetPackage` commands. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage +Parameter Sets: GivenSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -PSPackagePin + +Provide a **PSPackagePin** object. You can get a **PSPackagePin** object by using the +`Get-WinGetPin` command. + +```yaml +Type: Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin +Parameter Sets: PinSet +Aliases: InputObject + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Query + +Specify one or more strings to search for. By default, the command searches all configured sources. +Wildcards are not supported. The command compares the value provided to the following package +manifest properties: + + - `PackageIdentifier` + - `PackageName` + - `Moniker` + - `Tags` + +The command does a case-insensitive substring comparison of these properties. + +```yaml +Type: System.String[] +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source. + +```yaml +Type: System.String +Parameter Sets: FoundSet +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPackagePin + +### Microsoft.WinGet.Client.Engine.PSObjects.PSCatalogPackage + +### System.String + +### System.String[] + +### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Add-WinGetPin](Add-WinGetPin.md) + +[Reset-WinGetPin](Reset-WinGetPin.md) + +[Find-WinGetPackage](Find-WinGetPackage.md) + +[Get-WinGetPackage](Get-WinGetPackage.md) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md new file mode 100644 index 0000000000..1142a29f5c --- /dev/null +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Reset-WinGetPin.md @@ -0,0 +1,132 @@ +--- +external help file: Microsoft.WinGet.Client.Cmdlets.dll-Help.xml +Module Name: Microsoft.WinGet.Client +ms.date: 08/01/2024 +online version: +schema: 2.0.0 +--- + +# Reset-WinGetPin + +## SYNOPSIS +Resets all WinGet package pins. + +## SYNTAX + +``` +Reset-WinGetPin [-Source ] [-Force] [-ProgressAction ] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION +This command resets all WinGet package pins, removing all pin records. Mirrors the behavior of +`winget pin reset`. You can scope the reset to a specific source by providing the **Source** +parameter. Use the **Force** parameter to skip the confirmation prompt. + +## EXAMPLES + +### Example 1: Reset all pins +```powershell +Reset-WinGetPin +``` +This example resets all package pins across all configured sources. + +### Example 2: Reset pins for a specific source +```powershell +Reset-WinGetPin -Source "winget" +``` +This example resets all package pins for the `winget` source. + +### Example 3: Reset all pins without confirmation +```powershell +Reset-WinGetPin -Force +``` +This example resets all pins without prompting for confirmation. + +## PARAMETERS + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +When specified, skips the confirmation prompt and resets pins immediately. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Source + +Specify the name of a configured WinGet source to scope the reset. If not specified, pins +across all sources are reset. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +## OUTPUTS + +### Microsoft.WinGet.Client.Engine.PSObjects.PSPinResult + +## NOTES + +## RELATED LINKS + +[Get-WinGetPin](Get-WinGetPin.md) + +[Add-WinGetPin](Add-WinGetPin.md) + +[Remove-WinGetPin](Remove-WinGetPin.md) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs new file mode 100644 index 0000000000..8542dab160 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs @@ -0,0 +1,92 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; + + /// + /// Adds a pin for a package. Mirrors the behavior of winget pin add. + /// + [Cmdlet( + VerbsCommon.Add, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class AddPinCmdlet : PackageCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets the pin type. Defaults to . + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public PSPackagePinType PinType { get; set; } = PSPackagePinType.Pinning; + + /// + /// Gets or sets the gated version range. Required when is Gating. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public string GatedVersion { get; set; } + + /// + /// Gets or sets a value indicating whether to pin the installed version of the package. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter PinInstalledPackage { get; set; } + + /// + /// Gets or sets a value indicating whether to force the pin even if an existing pin is present. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// + /// Gets or sets an optional note to attach to the pin. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Note { get; set; } + + /// + /// Adds a pin for the specified package. + /// + protected override void ProcessRecord() + { + this.command = new PinPackageCommand( + this, + this.PSCatalogPackage, + this.Id, + this.Name, + this.Moniker, + this.Source, + this.Query); + this.command.Add( + this.MatchOption.ToString(), + this.PinType.ToString(), + this.GatedVersion, + this.PinInstalledPackage.ToBool(), + this.Force.ToBool(), + this.Note); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs new file mode 100644 index 0000000000..e53ecf8c37 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetPinCmdlet.cs @@ -0,0 +1,74 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + + /// + /// Gets package pins. If no filter is provided, returns all pins. + /// + [Cmdlet( + VerbsCommon.Get, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.AllSet)] + [OutputType(typeof(PSPackagePin))] + public sealed class GetPinCmdlet : FinderCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets the package to retrieve pins for. + /// + [Alias("InputObject")] + [ValidateNotNull] + [Parameter( + ParameterSetName = Constants.GivenSet, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSCatalogPackage PSCatalogPackage { get; set; } = null; + + /// + /// Retrieves pins based on the specified parameters. + /// + protected override void ProcessRecord() + { + this.command = new PinPackageCommand( + this, + this.PSCatalogPackage, + this.Id, + this.Name, + this.Moniker, + this.Source, + this.Query); + + if (this.ParameterSetName == Constants.AllSet) + { + this.command.GetAll(); + } + else + { + this.command.Get(this.MatchOption.ToString()); + } + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs new file mode 100644 index 0000000000..705bde2a49 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/PSObjects/PSPackagePinType.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.PSObjects +{ + /// + /// Must match the user-settable values of Microsoft.Management.Deployment.PackagePinType. + /// + public enum PSPackagePinType + { + /// + /// Pinning - prevents automatic updates to the current version. + /// + Pinning, + + /// + /// Blocking - prevents installation or upgrade of the package. + /// + Blocking, + + /// + /// Gating - limits updates to versions below the specified GatedVersion. + /// + Gating, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs new file mode 100644 index 0000000000..791de2b270 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs @@ -0,0 +1,77 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Client.PSObjects; + + /// + /// Removes a package pin. Mirrors the behavior of winget pin remove. + /// Accepts a from the pipeline (e.g., from Get-WinGetPin), + /// a from the pipeline, or package search criteria. + /// + [Cmdlet( + VerbsCommon.Remove, + Constants.WinGetNouns.Pin, + DefaultParameterSetName = Constants.FoundSet, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class RemovePinCmdlet : PackageCmdlet + { + private PinPackageCommand command = null; + + /// + /// Gets or sets the pin object to remove. Accepts pipeline input from Get-WinGetPin. + /// + [ValidateNotNull] + [Parameter( + ParameterSetName = Constants.PinSet, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSPackagePin PSPackagePin { get; set; } = null; + + /// + /// Removes the pin for the specified package. + /// + protected override void ProcessRecord() + { + PSCatalogPackage catalogPackage = this.PSCatalogPackage; + string id = this.Id; + + if (this.ParameterSetName == Constants.PinSet) + { + id = this.PSPackagePin.PackageId; + } + + this.command = new PinPackageCommand( + this, + catalogPackage, + id, + this.Name, + this.Moniker, + this.Source, + this.Query); + this.command.Remove(this.MatchOption.ToString()); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs new file mode 100644 index 0000000000..b1e1360d2e --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs @@ -0,0 +1,62 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + using Microsoft.WinGet.Client.Engine.PSObjects; + + /// + /// Resets all package pins, optionally scoped to a source. Mirrors the behavior of winget pin reset. + /// + [Cmdlet( + VerbsCommon.Reset, + Constants.WinGetNouns.Pin, + SupportsShouldProcess = true)] + [OutputType(typeof(PSPinResult))] + public sealed class ResetPinCmdlet : PSCmdlet + { + private ResetPinCommand command = null; + + /// + /// Gets or sets the source name to scope the reset. If not specified, all sources are reset. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Source { get; set; } + + /// + /// Gets or sets a value indicating whether to skip confirmation. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// + /// Resets pins. + /// + protected override void ProcessRecord() + { + string target = string.IsNullOrEmpty(this.Source) ? "All sources" : this.Source; + if (this.Force || this.ShouldProcess(target)) + { + this.command = new ResetPinCommand(this); + this.command.Reset(this.Source); + } + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.command != null) + { + this.command.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs index 62847c6567..dca94fbc28 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs @@ -52,6 +52,16 @@ internal static class Constants /// public const string IntegrityLatestSet = "IntegrityLatestSet"; + /// + /// This parameter set indicates that no filter is applied and all items should be returned. + /// + public const string AllSet = "AllSet"; + + /// + /// This parameter set indicates that a pin object was provided via a parameter or the pipeline. + /// + public const string PinSet = "PinSet"; + /// /// Nouns used for different cmdlets. Changing this will alter the names of the related commands. /// @@ -86,6 +96,11 @@ public static class WinGetNouns /// The noun for enable/disable winget admin settings. /// public const string Setting = "WinGetSetting"; + + /// + /// WinGetPin. + /// + public const string Pin = "WinGetPin"; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs new file mode 100644 index 0000000000..314a818acd --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/PinPackageCommand.cs @@ -0,0 +1,179 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System.Collections.Generic; + using System.Management.Automation; + using System.Threading.Tasks; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; + using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + + /// + /// Engine command for pin operations (get, add, remove). + /// + public sealed class PinPackageCommand : PackageCommand + { + /// + /// Initializes a new instance of the class. + /// + /// Caller cmdlet. + /// PSCatalogPackage (optional). + /// Package identifier. + /// Name of package. + /// Moniker of package. + /// Source to search. If null, all are searched. + /// Match against any field of a package. + public PinPackageCommand( + PSCmdlet psCmdlet, + PSCatalogPackage psCatalogPackage, + string id, + string name, + string moniker, + string source, + string[] query) + : base(psCmdlet) + { + if (psCatalogPackage != null) + { + this.CatalogPackage = psCatalogPackage; + } + + this.Id = id; + this.Name = name; + this.Moniker = moniker; + this.Source = source; + this.Query = query; + } + + /// + /// Gets all pins. + /// + public void GetAll() + { + IReadOnlyList pins = this.Execute( + () => PackageManagerWrapper.Instance.GetAllPins()); + + for (int i = 0; i < pins.Count; i++) + { + this.Write(StreamType.Object, new PSPackagePin(pins[i])); + } + } + + /// + /// Gets pins for a specific package. + /// + /// PSPackageFieldMatchOption string. + public void Get(string psPackageFieldMatchOption) + { + IReadOnlyList pins = this.Execute(() => + { + CatalogPackage package = this.FindCatalogPackage( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption)); + return PackageManagerWrapper.Instance.GetPins(package); + }); + + for (int i = 0; i < pins.Count; i++) + { + this.Write(StreamType.Object, new PSPackagePin(pins[i])); + } + } + + /// + /// Adds a pin for a package. + /// + /// PSPackageFieldMatchOption string. + /// Pin type string. + /// Gated version range (for Gating pins). + /// Whether to pin the installed package. + /// Whether to force the pin. + /// Optional user note. + public void Add( + string psPackageFieldMatchOption, + string pinType, + string gatedVersion, + bool pinInstalledPackage, + bool force, + string note) + { + var result = this.Execute( + async () => await this.GetPackageAndExecuteAsync( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + async (package, version) => + { + var options = ManagementDeploymentFactory.Instance.CreatePinPackageOptions(); + options.PinType = PSEnumHelpers.ToPackagePinType(pinType); + if (!string.IsNullOrEmpty(gatedVersion)) + { + options.GatedVersion = gatedVersion; + } + + options.PinInstalledPackage = pinInstalledPackage; + options.Force = force; + if (!string.IsNullOrEmpty(note)) + { + options.Note = note; + } + + return await Task.FromResult(PackageManagerWrapper.Instance.PinPackage(package, options)); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSPinResult(result.Item1)); + } + } + + /// + /// Removes the pin for a package. + /// + /// PSPackageFieldMatchOption string. + public void Remove(string psPackageFieldMatchOption) + { + var result = this.Execute( + async () => await this.GetPackageAndExecuteAsync( + CompositeSearchBehavior.AllCatalogs, + PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption), + async (package, version) => + { + return await Task.FromResult(PackageManagerWrapper.Instance.UnpinPackage(package)); + })); + + if (result != null) + { + this.Write(StreamType.Object, new PSPinResult(result.Item1)); + } + } + + private CatalogPackage FindCatalogPackage(CompositeSearchBehavior behavior, PackageFieldMatchOption match) + { + if (this.CatalogPackage != null) + { + return this.CatalogPackage.CatalogPackageCOM; + } + + IReadOnlyList results = this.FindPackages(behavior, 0, match); + if (results.Count == 0) + { + throw new NoPackageFoundException(); + } + else if (results.Count == 1) + { + return results[0].CatalogPackage; + } + else + { + throw new VagueCriteriaException(results); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs new file mode 100644 index 0000000000..b25b9871e4 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Helpers; + using Microsoft.WinGet.Client.Engine.PSObjects; + using Microsoft.WinGet.Common.Command; + + /// + /// Engine command for resetting all pins. + /// + public sealed class ResetPinCommand : ManagementDeploymentCommand + { + /// + /// Initializes a new instance of the class. + /// + /// Caller cmdlet. + public ResetPinCommand(PSCmdlet psCmdlet) + : base(psCmdlet) + { + } + + /// + /// Resets all pins, optionally scoped to a source. + /// + /// The source name to scope the reset. Pass null or empty to reset all sources. + public void Reset(string sourceName) + { + var result = this.Execute( + () => PackageManagerWrapper.Instance.ResetAllPins(sourceName ?? string.Empty)); + + this.Write(StreamType.Object, new PSPinResult(result)); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs index 76d9121e4a..5528da6149 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/ManagementDeploymentFactory.cs @@ -31,6 +31,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("4CBABE76-7322-4BE4-9CEA-2589A80682DC"); private static readonly Guid RepairOptionsClsid = Guid.Parse("0498F441-3097-455F-9CAF-148F28293865"); + private static readonly Guid PinPackageOptionsClsid = Guid.Parse("93409EF2-29D0-46D3-8085-13EDE73939C4"); #else private static readonly Guid PackageManagerClsid = Guid.Parse("74CB3139-B7C5-4B9E-9388-E6616DEA288C"); private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"); @@ -40,6 +41,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); private static readonly Guid DownloadOptionsClsid = Guid.Parse("8EF324ED-367C-4880-83E5-BB2ABD0B72F6"); private static readonly Guid RepairOptionsClsid = Guid.Parse("E62BB1E7-C7B2-4AEC-9E28-FB649B30FF03"); + private static readonly Guid PinPackageOptionsClsid = Guid.Parse("B3A61CCB-A3D0-497D-B300-A904904EEA56"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); @@ -57,6 +59,8 @@ internal sealed class ManagementDeploymentFactory private static readonly Type? DownloadOptionsType = Type.GetTypeFromCLSID(DownloadOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type? RepairOptionsType = Type.GetTypeFromCLSID(RepairOptionsClsid); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] + private static readonly Type? PinPackageOptionsType = Type.GetTypeFromCLSID(PinPackageOptionsClsid); // These GUIDs correspond to the CSWinRT interface IIDs generated for Microsoft.Management.Deployment.Projection // and are auto-generated by the WinRT tool. @@ -68,6 +72,7 @@ internal sealed class ManagementDeploymentFactory private static readonly Guid PackageMatchFilterIid = Guid.Parse("D981ECA3-4DE5-5AD7-967A-698C7D60FC3B"); private static readonly Guid DownloadOptionsIid = Guid.Parse("94C92C4B-43F5-5CA3-BBBE-9F432C9546BC"); private static readonly Guid RepairOptionsIid = Guid.Parse("263F0546-2D7E-53A0-B8D1-75B74817FF18"); + private static readonly Guid PinPackageOptionsIid = Guid.Parse("8AB6949E-BB04-5F77-8B69-EF444D9C1635"); private static readonly IEnumerable ValidArchs = new Architecture[] { Architecture.X86, Architecture.X64 }; @@ -176,6 +181,15 @@ public RepairOptions CreateRepairOptions() return Create(RepairOptionsType, RepairOptionsIid); } + /// + /// Creates an instance of the class. + /// + /// A instance. + public PinPackageOptions CreatePinPackageOptions() + { + return Create(PinPackageOptionsType, PinPackageOptionsIid); + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static T Create(Type? type, in Guid iid) where T : new() diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs index 2228a1c1c4..45e7e368f5 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PSEnumHelpers.cs @@ -150,6 +150,22 @@ public static PackageRepairMode ToPackageRepairMode(string value) }; } + /// + /// Converts PSPackagePinType string value to PackagePinType. + /// + /// PSPackagePinType string value. + /// PackagePinType. + public static PackagePinType ToPackagePinType(string value) + { + return value switch + { + "Pinning" => PackagePinType.Pinning, + "Blocking" => PackagePinType.Blocking, + "Gating" => PackagePinType.Gating, + _ => throw new InvalidOperationException(), + }; + } + /// /// Converts PSWindowsPlatform string value to WindowsPlatform. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs index 6e34ab089d..aedf6c9402 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -148,6 +148,54 @@ public IReadOnlyList GetPins(CatalogPackage package) false); } + /// + /// Wrapper for GetAllPins. + /// + /// A read-only list of all PackagePin objects. + public IReadOnlyList GetAllPins() + { + return this.Execute( + () => this.packageManager.GetAllPins(), + false); + } + + /// + /// Wrapper for PinPackage. + /// + /// The package to pin. + /// The pin options. + /// A PinPackageResult. + public PinPackageResult PinPackage(CatalogPackage package, PinPackageOptions options) + { + return this.Execute( + () => this.packageManager.PinPackage(package, options), + false); + } + + /// + /// Wrapper for UnpinPackage. + /// + /// The package to unpin. + /// A PinPackageResult. + public PinPackageResult UnpinPackage(CatalogPackage package) + { + return this.Execute( + () => this.packageManager.UnpinPackage(package), + false); + } + + /// + /// Wrapper for ResetAllPins. + /// + /// The source name to reset pins for. Pass empty string to reset all. + /// A PinPackageResult. + public PinPackageResult ResetAllPins(string sourceName) + { + return this.Execute( + () => this.packageManager.ResetAllPins(sourceName), + false); + } + /// /// Gets the version of the package manager that is running. /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs new file mode 100644 index 0000000000..48ae65c8d9 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs @@ -0,0 +1,83 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using Microsoft.Management.Deployment; + + /// + /// PSPackagePin wraps a PackagePin COM object for PowerShell output. + /// + public sealed class PSPackagePin + { + private readonly PackagePin packagePin; + + /// + /// Initializes a new instance of the class. + /// + /// The PackagePin COM object. + internal PSPackagePin(PackagePin packagePin) + { + this.packagePin = packagePin; + } + + /// + /// Gets the package identifier. + /// + public string PackageId + { + get { return this.packagePin.PackageId; } + } + + /// + /// Gets the source identifier the pin applies to. + /// + public string SourceId + { + get { return this.packagePin.SourceId; } + } + + /// + /// Gets the pin type. + /// + public string Type + { + get { return this.packagePin.Type.ToString(); } + } + + /// + /// Gets the gated version range (for Gating pins only). + /// + public string GatedVersion + { + get { return this.packagePin.GatedVersion; } + } + + /// + /// Gets the date the pin was added (UTC ISO 8601 string). + /// + public string DateAdded + { + get { return this.packagePin.DateAdded; } + } + + /// + /// Gets the optional user note for this pin. + /// + public string Note + { + get { return this.packagePin.Note; } + } + + /// + /// Gets a value indicating whether this pin applies to an installed package. + /// + public bool IsForInstalledPackage + { + get { return this.packagePin.IsForInstalledPackage; } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs new file mode 100644 index 0000000000..fdfed20f94 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPinResult.cs @@ -0,0 +1,62 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.PSObjects +{ + using System; + using Microsoft.Management.Deployment; + + /// + /// PSPinResult wraps a PinPackageResult COM object for PowerShell output. + /// + public sealed class PSPinResult + { + private readonly PinPackageResult pinResult; + + /// + /// Initializes a new instance of the class. + /// + /// The PinPackageResult COM object. + internal PSPinResult(PinPackageResult pinResult) + { + this.pinResult = pinResult; + } + + /// + /// Gets the status of the pin operation. + /// + public string Status + { + get { return this.pinResult.Status.ToString(); } + } + + /// + /// Gets the extended error code of the pin operation. + /// + public Exception ExtendedErrorCode + { + get { return this.pinResult.ExtendedErrorCode; } + } + + /// + /// Returns whether the pin operation succeeded. + /// + /// True if the pin operation succeeded. + public bool Succeeded() + { + return this.pinResult.Status == PinResultStatus.Ok; + } + + /// + /// Returns a formatted error message. + /// + /// Error message string. + public string ErrorMessage() + { + return $"PinStatus: '{this.Status}' ExtendedError: '0x{this.ExtendedErrorCode.HResult:X8}'"; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 index 2603df2cfa..71e981fd0f 100644 --- a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 @@ -95,6 +95,10 @@ CmdletsToExport = @( 'Reset-WinGetSource' 'Export-WinGetPackage' 'Repair-WinGetPackage' + 'Get-WinGetPin' + 'Add-WinGetPin' + 'Remove-WinGetPin' + 'Reset-WinGetPin' ) # Variables to export from this module From 7ce3b272846e3e7e2e026924fc5826109e8ee768 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 13:00:18 -0500 Subject: [PATCH 06/46] Spelling --- .github/actions/spelling/expect.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 53175f0afa..8eb3002073 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -29,6 +29,7 @@ appdata appinstallertest applic appname +appone appshutdown APPTERMINATION archs @@ -339,6 +340,7 @@ megamorf microsoftentraid microsoftentraidforazureblobstorage midl +migratepintable minidump MINORVERSION missingdependency From 35322e72f6c01801074b4068f0d0164c1fcc6cd3 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 13:04:47 -0500 Subject: [PATCH 07/46] Inherit from 1.0 Interface --- .../Pinning_1_1/PinningIndexInterface.h | 6 ++--- .../Pinning_1_1/PinningIndexInterface_1_1.cpp | 24 ------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h index 5e6e58c441..8e9a078d17 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "Microsoft/Schema/IPinningIndex.h" +#include "Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h" namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { - struct PinningIndexInterface : public IPinningIndex + struct PinningIndexInterface : public Pinning_V1_0::PinningIndexInterface { // Version 1.1 SQLite::Version GetVersion() const override; @@ -15,9 +15,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 private: SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; - SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; std::vector GetAllPins(SQLite::Connection& connection) override; - bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index 0d65a8d3c0..247f945de0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -2,7 +2,6 @@ // Licensed under the MIT License. #include "pch.h" #include "Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h" -#include "Microsoft/Schema/Pinning_1_0/PinTable.h" #include "Microsoft/Schema/Pinning_1_1/PinTable.h" namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 @@ -76,20 +75,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return { status, existingPinId.value() }; } - SQLite::rowid_t PinningIndexInterface::RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) - { - auto existingPinId = GetExistingPinId(connection, pinKey); - - // If the pin doesn't exist, fail the remove - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); - - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "removepin_v1_1"); - PinTable::RemovePinById(connection, existingPinId.value()); - - savepoint.Commit(); - return existingPinId.value(); - } - std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) { auto existingPinId = GetExistingPinId(connection, pinKey); @@ -106,13 +91,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { return PinTable::GetAllPins(connection); } - - bool PinningIndexInterface::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) - { - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "resetpins_v1_1"); - bool result = PinTable::ResetAllPins(connection, sourceId); - savepoint.Commit(); - - return result; - } } From 236e667faa5eb4a2667bfcd941bd3e900327941e Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 29 Apr 2026 13:11:17 -0500 Subject: [PATCH 08/46] Contract 29 is not released yet --- .../PackageManager.idl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 6a5d6096ae..822d9c037b 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(30)] // For version 1.30 + [contractversion(29)] // For version 1.29 apicontract WindowsPackageManagerContract{}; /// State of the install @@ -1680,7 +1680,7 @@ namespace Microsoft.Management.Deployment EditPackageCatalogResult EditPackageCatalog(EditPackageCatalogOptions options); } - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] { /// Get all pins across all sources. Windows.Foundation.Collections.IVectorView GetAllPins(); @@ -1705,7 +1705,7 @@ namespace Microsoft.Management.Deployment } /// IMPLEMENTATION NOTE: Pinning::PinType from AppInstaller::Pinning - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] enum PackagePinType { /// Unknown pin type or not pinned. @@ -1724,7 +1724,7 @@ namespace Microsoft.Management.Deployment }; /// IMPLEMENTATION NOTE: Pinning::Pin from AppInstaller::Pinning - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] runtimeclass PackagePin { /// The package ID that the pin applies to (for available-package pins) or the @@ -1752,7 +1752,7 @@ namespace Microsoft.Management.Deployment }; /// Options for adding or updating a package pin. - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] runtimeclass PinPackageOptions { PinPackageOptions(); @@ -1779,7 +1779,7 @@ namespace Microsoft.Management.Deployment }; /// Status of a pin operation. - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] enum PinResultStatus { Ok, @@ -1794,7 +1794,7 @@ namespace Microsoft.Management.Deployment }; /// Result of a pin operation. - [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 30)] + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 29)] runtimeclass PinPackageResult { PinResultStatus Status { get; }; From 1c89957ccf526212db99d4b0c5339e9cd270b41e Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 20:48:52 -0500 Subject: [PATCH 09/46] Adddress comments about resource strings --- src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index bb3ea1e83d..a9c0f437a7 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1918,7 +1918,7 @@ Please specify one of them using the --source option to proceed. Table header for the version to which a package is pinned; meaning it should not update from that version. - Date added + Time Pinned Label shown in the pin show output for when the pin was added or last updated. @@ -1927,9 +1927,11 @@ Please specify one of them using the --source option to proceed. Show details about a pin + Short description of the 'winget pin show' subcommand, shown in help and usage text. - Show detailed information about a specific pin, including the package ID, version, type, date added, and any note stored with the pin. + Show detailed information about a specific pin, including the package identifier, version, type, date added, and any note stored with the pin. + Long description of the 'winget pin show' subcommand, shown in detailed help text. Date added: From fa617a69ed05f5cae38a60a53821fb7a88d4968a Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 20:52:18 -0500 Subject: [PATCH 10/46] Update comment to indicate the appropriate contract version --- src/Microsoft.Management.Deployment/PackageManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index d4c2ed5397..f1086736fb 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -54,7 +54,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::hstring Version() const; // Contract 28.0 winrt::Microsoft::Management::Deployment::EditPackageCatalogResult EditPackageCatalog(winrt::Microsoft::Management::Deployment::EditPackageCatalogOptions options); - // Contract 30.0 + // Contract 29.0 winrt::Windows::Foundation::Collections::IVectorView GetAllPins(); winrt::Windows::Foundation::Collections::IVectorView GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package); winrt::Microsoft::Management::Deployment::PinPackageResult PinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PinPackageOptions options); From 7f33f56e87cc726faa001f86cb8e4a92c63cffa0 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 21:04:08 -0500 Subject: [PATCH 11/46] Make methods public --- .../Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h index 8e9a078d17..1a13194076 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -11,8 +11,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Version GetVersion() const override; void CreateTables(SQLite::Connection& connection) override; bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; - - private: SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; From 3c7d4a6df75600516497a357c1f70bf56e03001c Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 21:11:25 -0500 Subject: [PATCH 12/46] Adjust how new schema is created --- src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp | 8 +------- src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h | 3 --- .../Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp | 6 +++--- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index c0b8a8150b..0bcddd1e62 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -185,12 +185,6 @@ namespace AppInstaller::Repository::Microsoft return m_interface->ResetAllPins(m_dbconn, sourceId); } - std::unique_ptr PinningIndex::CreateIPinningIndex() const - { - // Always return the latest interface; migration will handle version mismatches. - return std::make_unique(); - } - std::unique_ptr PinningIndex::CreateIPinningIndexForVersion(const SQLite::Version& version) { if (version == SQLite::Version{ 1, 0 }) @@ -212,7 +206,7 @@ namespace AppInstaller::Repository::Microsoft SQLiteStorageBase(target, disposition, std::move(indexFile)) { AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); - m_interface = CreateIPinningIndex(); + m_interface = CreateIPinningIndexForVersion(SQLite::Version::Latest()); if (m_version != m_interface->GetVersion()) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index 346766fd87..c015dd8366 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -67,9 +67,6 @@ namespace AppInstaller::Repository::Microsoft // Constructor used to create a new index. PinningIndex(const std::string& target, SQLite::Version version); - // Creates the IPinningIndex interface object for this version. - std::unique_ptr CreateIPinningIndex() const; - // Creates an IPinningIndex interface object for a specific version. static std::unique_ptr CreateIPinningIndexForVersion(const SQLite::Version& version); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index 247f945de0..cd6bd5be73 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -29,9 +29,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 void PinningIndexInterface::CreateTables(SQLite::Connection& connection) { - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); - Pinning_V1_1::PinTable::Create(connection); - savepoint.Commit(); + Pinning_V1_0::PinningIndexInterface base; + base.CreateTables(connection); + MigrateFrom(connection, &base); } bool PinningIndexInterface::MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) From 99a876004aec9247e5bb46c78ae863e14bfa93f2 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 21:25:56 -0500 Subject: [PATCH 13/46] Reduce duplicated code through inheritance --- .../Microsoft/Schema/Pinning_1_0/PinTable.h | 9 ++- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 74 ------------------- .../Microsoft/Schema/Pinning_1_1/PinTable.h | 19 +---- 3 files changed, 8 insertions(+), 94 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h index 4bfafd616a..8deb47e6d1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h @@ -13,9 +13,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Get the table name. static std::string_view TableName(); - // Creates the table with named indices. - static void Create(SQLite::Connection& connection); - // Gets the row ID for the pin, if it exists. static std::optional GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey); @@ -39,5 +36,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 // Resets all pins from a given source, or from all sources if none is specified. // Returns a value indicating whether there were any changes. static bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId = {}); + + protected: + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + friend struct PinningIndexInterface; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 88f819d9a7..56f1db51d1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -49,40 +49,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 static constexpr std::string_view s_PinTable_Version_Column = "version"sv; static constexpr std::string_view s_PinTable_DateAdded_Column = "date_added"sv; static constexpr std::string_view s_PinTable_Note_Column = "note"sv; - static constexpr std::string_view s_PinTable_Index = "pin_index"sv; - - std::string_view PinTable::TableName() - { - return s_PinTable_Table_Name; - } - - void PinTable::Create(SQLite::Connection& connection) - { - using namespace SQLite::Builder; - - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); - - StatementBuilder createTableBuilder; - createTableBuilder.CreateTable(s_PinTable_Table_Name).BeginColumns(); - - createTableBuilder.Column(ColumnBuilder(s_PinTable_PackageId_Column, Type::Text).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_SourceId_Column, Type::Text).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_Type_Column, Type::Int64).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_Version_Column, Type::Text).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_DateAdded_Column, Type::Text).NotNull()); - createTableBuilder.Column(ColumnBuilder(s_PinTable_Note_Column, Type::Text)); - - createTableBuilder.EndColumns(); - createTableBuilder.Execute(connection); - - // Create an index over the pairs package,source - StatementBuilder createIndexBuilder; - createIndexBuilder.CreateUniqueIndex(s_PinTable_Index).On(s_PinTable_Table_Name) - .Columns({ s_PinTable_PackageId_Column, s_PinTable_SourceId_Column }); - createIndexBuilder.Execute(connection); - - savepoint.Commit(); - } void PinTable::MigrateFrom1_0(SQLite::Connection& connection) { @@ -95,25 +61,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 addNote.Execute(); } - std::optional PinTable::GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey) - { - SQLite::Builder::StatementBuilder builder; - builder.Select(SQLite::RowIDName).From(s_PinTable_Table_Name) - .Where(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) - .And(s_PinTable_SourceId_Column).Equals((std::string_view)pinKey.SourceId); - - SQLite::Statement select = builder.Prepare(connection); - - if (select.Step()) - { - return select.GetColumn(0); - } - else - { - return {}; - } - } - SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { SQLite::Builder::StatementBuilder builder; @@ -165,13 +112,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return connection.GetChanges() != 0; } - void PinTable::RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId) - { - SQLite::Builder::StatementBuilder builder; - builder.DeleteFrom(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId); - builder.Execute(connection); - } - std::optional PinTable::GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId) { SQLite::Builder::StatementBuilder builder; @@ -225,18 +165,4 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return pins; } - bool PinTable::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) - { - SQLite::Builder::StatementBuilder builder; - builder.DeleteFrom(s_PinTable_Table_Name); - - if (!sourceId.empty()) - { - builder.Where(s_PinTable_SourceId_Column).Equals(sourceId); - } - - builder.Execute(connection); - - return connection.GetChanges() != 0; - } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h index b36c954bfd..dbd3a56638 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h @@ -4,24 +4,16 @@ #include #include #include "Microsoft/Schema/IPinningIndex.h" +#include "Microsoft/Schema/Pinning_1_0/PinTable.h" #include namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { - struct PinTable + struct PinTable : Pinning_V1_0::PinTable { - // Get the table name. - static std::string_view TableName(); - - // Creates the table with named indices. - static void Create(SQLite::Connection& connection); - // Migrates an existing v1.0 pin table by adding the date_added and note columns. static void MigrateFrom1_0(SQLite::Connection& connection); - // Gets the row ID for the pin, if it exists. - static std::optional GetIdByPinKey(SQLite::Connection& connection, const Pinning::PinKey& pinKey); - // Adds a new pin. Returns the row ID of the added pin. static SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin); @@ -29,18 +21,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 // Returns a value indicating whether there were any changes. static bool UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); - // Removes a pin given its row ID. - static void RemovePinById(SQLite::Connection& connection, SQLite::rowid_t pinId); - // Gets a pin by its row ID if it exists. // Used for testing static std::optional GetPinById(SQLite::Connection& connection, const SQLite::rowid_t pinId); // Gets all the currently existing pins. static std::vector GetAllPins(SQLite::Connection& connection); - - // Resets all pins from a given source, or from all sources if none is specified. - // Returns a value indicating whether there were any changes. - static bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId = {}); }; } From e336194eb622a1fa4e6b7017a60971ea78013f0f Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 21:45:41 -0500 Subject: [PATCH 14/46] Use statement builder, savepoints, and tuples for ownership --- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 56f1db51d1..800524dab2 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -9,32 +9,34 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { namespace { - std::optional GetPinFromRow( - std::string_view packageId, - std::string_view sourceId, - Pinning::PinType type, - std::string_view version, - std::string_view dateAdded, - std::optional note) + using PinRow = std::tuple>; + + std::optional GetPinFromRow(PinRow&& row) { + auto [packageId, sourceId, type, version, dateAdded, note] = std::move(row); + std::optional result; + Pinning::PinKey key; + key.PackageId = std::move(packageId); + key.SourceId = std::move(sourceId); + switch (type) { case Pinning::PinType::Blocking: - result = Pinning::Pin::CreateBlockingPin({ packageId, sourceId }); + result = Pinning::Pin::CreateBlockingPin(std::move(key)); break; case Pinning::PinType::Pinning: - result = Pinning::Pin::CreatePinningPin({ packageId, sourceId }); + result = Pinning::Pin::CreatePinningPin(std::move(key)); break; case Pinning::PinType::Gating: - result = Pinning::Pin::CreateGatingPin({ packageId, sourceId }, Utility::GatedVersion{ version }); + result = Pinning::Pin::CreateGatingPin(std::move(key), Utility::GatedVersion{ std::move(version) }); break; default: return {}; } - result->SetDateAdded(std::string{ dateAdded }); + result->SetDateAdded(std::move(dateAdded)); result->SetNote(std::move(note)); return result; @@ -52,13 +54,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 void PinTable::MigrateFrom1_0(SQLite::Connection& connection) { - SQLite::Statement addDateAdded = SQLite::Statement::Create(connection, - "ALTER TABLE pin ADD COLUMN date_added TEXT NOT NULL DEFAULT ''"); - addDateAdded.Execute(); + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migratepintable_v1_0_to_v1_1_pintable"); + + StatementBuilder addDateAdded; + addDateAdded.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_DateAdded_Column, Type::Text).NotNull().Default("''"sv); + addDateAdded.Execute(connection); + + StatementBuilder addNote; + addNote.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_Note_Column, Type::Text); + addNote.Execute(connection); - SQLite::Statement addNote = SQLite::Statement::Create(connection, - "ALTER TABLE pin ADD COLUMN note TEXT"); - addNote.Execute(); + savepoint.Commit(); } SQLite::rowid_t PinTable::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) @@ -74,11 +82,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 s_PinTable_DateAdded_Column, s_PinTable_Note_Column }) .Values( - (std::string_view)pinKey.PackageId, + pinKey.PackageId, pinKey.SourceId, pin.GetType(), pin.GetGatedVersion().ToString(), - (std::string_view)pin.GetDateAdded(), + pin.GetDateAdded(), pin.GetNote()); builder.Execute(connection); @@ -90,11 +98,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); builder.Update(s_PinTable_Table_Name).Set() - .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) + .Column(s_PinTable_PackageId_Column).Equals(pinKey.PackageId) .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) .Column(s_PinTable_Type_Column).Equals(pin.GetType()) .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) - .Column(s_PinTable_DateAdded_Column).Equals((std::string_view)pin.GetDateAdded()); + .Column(s_PinTable_DateAdded_Column).Equals(pin.GetDateAdded()); // Use Unbound (= ?) for null note so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. const auto& note = pin.GetNote(); @@ -131,9 +139,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return {}; } - auto [packageId, sourceId, pinType, gatedVersion, dateAdded, note] = - select.GetRow>(); - return GetPinFromRow(packageId, sourceId, pinType, gatedVersion, dateAdded, std::move(note)); + return GetPinFromRow(select.GetRow>()); } std::vector PinTable::GetAllPins(SQLite::Connection& connection) @@ -153,9 +159,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 std::vector pins; while (select.Step()) { - auto [packageId, sourceId, pinType, gatedVersion, dateAdded, note] = - select.GetRow>(); - auto pin = GetPinFromRow(packageId, sourceId, pinType, gatedVersion, dateAdded, std::move(note)); + auto pin = GetPinFromRow(select.GetRow>()); if (pin) { pins.push_back(std::move(pin.value())); From 0cb653b5eb38d27551480a0140491e1a79064f95 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:30:35 -0500 Subject: [PATCH 15/46] Use a time point instead of strings --- src/AppInstallerCLICore/Workflows/PinFlow.cpp | 14 +++---- src/AppInstallerCLITests/PinFlow.cpp | 19 ++++----- src/AppInstallerCLITests/PinTestCommon.h | 16 ++++++++ src/AppInstallerCLITests/PinningIndex.cpp | 20 +++++----- .../Public/winget/Pin.h | 7 ++-- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 39 +++++++++++++++---- .../PackageManager.cpp | 8 +--- .../PackageManager.idl | 4 +- .../PackagePin.cpp | 8 +++- .../PackagePin.h | 4 +- .../PSObjects/PSPackagePin.cs | 4 +- 11 files changed, 93 insertions(+), 50 deletions(-) create mode 100644 src/AppInstallerCLITests/PinTestCommon.h diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp index 91d863d5e9..3f7440e2a4 100644 --- a/src/AppInstallerCLICore/Workflows/PinFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp @@ -199,10 +199,7 @@ namespace AppInstaller::CLI::Workflow if (!pinsToAddOrUpdate.empty()) { - std::string dateAdded = Utility::TimePointToString( - std::chrono::system_clock::now(), - Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day | - Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second); + auto pinTime = std::chrono::system_clock::now(); std::optional note; if (context.Args.Contains(Execution::Args::Type::PinNote)) @@ -212,7 +209,7 @@ namespace AppInstaller::CLI::Workflow for (auto& pin : pinsToAddOrUpdate) { - pin.SetDateAdded(dateAdded); + pin.SetDateAdded(pinTime); pin.SetNote(note); pinningData.AddOrUpdatePin(pin); } @@ -435,9 +432,12 @@ namespace AppInstaller::CLI::Workflow // Date Added const auto& dateAdded = pin.GetDateAdded(); - if (!dateAdded.empty()) + if (dateAdded.has_value()) { - ShowSingleLineField(info, Resource::String::PinShowLabelDateAdded, Utility::LocIndView{ dateAdded }); + std::string dateAddedStr = Utility::TimePointToString(*dateAdded, + Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day | + Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second); + ShowSingleLineField(info, Resource::String::PinShowLabelDateAdded, Utility::LocIndView{ dateAddedStr }); } // Note (only shown if present) diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp index bf7687191c..d998cdb586 100644 --- a/src/AppInstallerCLITests/PinFlow.cpp +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -3,6 +3,8 @@ #include "pch.h" #include "WorkflowCommon.h" #include "TestHooks.h" +#include "PinTestCommon.h" +#include #include #include #include @@ -14,7 +16,6 @@ using namespace TestCommon; using namespace AppInstaller::CLI; using namespace AppInstaller::CLI::Workflow; using namespace AppInstaller::Repository::Microsoft; -using namespace AppInstaller::Utility; using namespace AppInstaller::Pinning; using namespace AppInstaller::SQLite; @@ -218,7 +219,7 @@ TEST_CASE("PinFlow_Add_SetsDateAdded", "[PinFlow][workflow]") auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); auto pins = index.GetAllPins(); REQUIRE(pins.size() == 1); - REQUIRE_FALSE(pins[0].GetDateAdded().empty()); + REQUIRE(pins[0].GetDateAdded().has_value()); } TEST_CASE("PinFlow_Add_WithNote", "[PinFlow][workflow]") @@ -283,7 +284,7 @@ TEST_CASE("PinFlow_Show_NoMatch", "[PinFlow][workflow]") TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); Pin existingPin = Pin::CreateBlockingPin({ "SomePackage.Id", "sourceId" }); - existingPin.SetDateAdded("2026-01-15 10:00:00"); + existingPin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1000)); PopulatePinIndexForShow(indexFile.GetPath(), { existingPin }); std::ostringstream showOutput; @@ -304,7 +305,7 @@ TEST_CASE("PinFlow_Show_MatchById", "[PinFlow][workflow]") TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); Pin pin = Pin::CreateBlockingPin({ "MyApp.Package", "sourceId" }); - pin.SetDateAdded("2026-06-01 09:00:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jun2026_01_0900)); pin.SetNote(std::string{ "keep this one" }); PopulatePinIndexForShow(indexFile.GetPath(), { pin }); @@ -319,7 +320,7 @@ TEST_CASE("PinFlow_Show_MatchById", "[PinFlow][workflow]") REQUIRE_FALSE(showContext.IsTerminated()); REQUIRE(showOutput.str().find("MyApp.Package") != std::string::npos); REQUIRE(showOutput.str().find("Blocking") != std::string::npos); - REQUIRE(showOutput.str().find("2026-06-01 09:00:00") != std::string::npos); + REQUIRE(showOutput.str().find("Date added:") != std::string::npos); REQUIRE(showOutput.str().find("keep this one") != std::string::npos); } @@ -329,7 +330,7 @@ TEST_CASE("PinFlow_Show_MatchByQuery", "[PinFlow][workflow]") TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); Pin pin = Pin::CreatePinningPin({ "Contoso.AppOne", "sourceId" }); - pin.SetDateAdded("2026-03-10 12:00:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Mar2026_10_1200)); PopulatePinIndexForShow(indexFile.GetPath(), { pin }); std::ostringstream showOutput; @@ -352,10 +353,10 @@ TEST_CASE("PinFlow_Show_ExactMatch", "[PinFlow][workflow]") // Two pins sharing a prefix Pin pinA = Pin::CreateBlockingPin({ "Vendor.App", "src" }); - pinA.SetDateAdded("2026-01-01 00:00:00"); + pinA.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_01_0000)); Pin pinB = Pin::CreateBlockingPin({ "Vendor.AppExtra", "src" }); - pinB.SetDateAdded("2026-01-01 00:00:00"); + pinB.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_01_0000)); PopulatePinIndexForShow(indexFile.GetPath(), { pinA, pinB }); @@ -381,7 +382,7 @@ TEST_CASE("PinFlow_Show_NoNote_DoesNotShowNoteLabel", "[PinFlow][workflow]") TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); Pin pin = Pin::CreatePinningPin({ "NoNote.Package", "src" }); - pin.SetDateAdded("2026-05-01 08:00:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::May2026_01_0800)); // note intentionally not set PopulatePinIndexForShow(indexFile.GetPath(), { pin }); diff --git a/src/AppInstallerCLITests/PinTestCommon.h b/src/AppInstallerCLITests/PinTestCommon.h new file mode 100644 index 0000000000..982737dc3b --- /dev/null +++ b/src/AppInstallerCLITests/PinTestCommon.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +// Named UTC epoch constants (seconds since Unix epoch) shared across pinning tests. +// Use with Utility::ConvertUnixEpochToSystemClock to obtain a time_point. +namespace PinTestEpoch +{ + constexpr int64_t Jan2026_01_0000 = 1767225600LL; // 2026-01-01 00:00:00 UTC + constexpr int64_t Jan2026_15_1000 = 1768471200LL; // 2026-01-15 10:00:00 UTC + constexpr int64_t Jan2026_15_1030 = 1768473000LL; // 2026-01-15 10:30:00 UTC + constexpr int64_t Jan2026_15_1100 = 1768474800LL; // 2026-01-15 11:00:00 UTC + constexpr int64_t Mar2026_10_1200 = 1773144000LL; // 2026-03-10 12:00:00 UTC + constexpr int64_t May2026_01_0800 = 1777622400LL; // 2026-05-01 08:00:00 UTC + constexpr int64_t Jun2026_01_0900 = 1780304400LL; // 2026-06-01 09:00:00 UTC +} diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index 00ea3945d2..d3127d8d9e 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -2,6 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" +#include "PinTestCommon.h" +#include #include #include #include @@ -181,7 +183,7 @@ TEST_CASE("PinningIndex_V1_1_AddPin_WithDateAndNote", "[pinningIndex]") INFO("Using temporary file named: " << tempFile.GetPath()); Pin pin = Pin::CreateBlockingPin({ "pkgId", "sourceId" }); - pin.SetDateAdded("2026-01-15 10:30:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); pin.SetNote(std::string{ "test note" }); { @@ -196,7 +198,7 @@ TEST_CASE("PinningIndex_V1_1_AddPin_WithDateAndNote", "[pinningIndex]") REQUIRE(pinFromIndex.has_value()); REQUIRE(pinFromIndex->GetType() == PinType::Blocking); REQUIRE(pinFromIndex->GetKey().PackageId == "pkgId"); - REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 10:30:00"); + REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); REQUIRE(pinFromIndex->GetNote().has_value()); REQUIRE(pinFromIndex->GetNote().value() == "test note"); } @@ -208,7 +210,7 @@ TEST_CASE("PinningIndex_V1_1_AddPin_WithoutNote", "[pinningIndex]") INFO("Using temporary file named: " << tempFile.GetPath()); Pin pin = Pin::CreatePinningPin({ "pkgId", "sourceId" }); - pin.SetDateAdded("2026-01-15 10:30:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); // note intentionally left unset { @@ -221,7 +223,7 @@ TEST_CASE("PinningIndex_V1_1_AddPin_WithoutNote", "[pinningIndex]") auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); REQUIRE(pinFromIndex.has_value()); - REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 10:30:00"); + REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); REQUIRE_FALSE(pinFromIndex->GetNote().has_value()); } } @@ -232,11 +234,11 @@ TEST_CASE("PinningIndex_V1_1_AddUpdateRemove", "[pinningIndex]") INFO("Using temporary file named: " << tempFile.GetPath()); Pin pin = Pin::CreateBlockingPin({ "pkgId", "srcId" }); - pin.SetDateAdded("2026-01-15 10:00:00"); + pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1000)); pin.SetNote(std::string{ "original note" }); Pin updatedPin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*"sv }); - updatedPin.SetDateAdded("2026-01-15 11:00:00"); + updatedPin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1100)); updatedPin.SetNote(std::string{ "updated note" }); { @@ -251,7 +253,7 @@ TEST_CASE("PinningIndex_V1_1_AddUpdateRemove", "[pinningIndex]") auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); REQUIRE(pinFromIndex.has_value()); REQUIRE(pinFromIndex->GetType() == PinType::Gating); - REQUIRE(pinFromIndex->GetDateAdded() == "2026-01-15 11:00:00"); + REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1100)); REQUIRE(pinFromIndex->GetNote().has_value()); REQUIRE(pinFromIndex->GetNote().value() == "updated note"); } @@ -294,8 +296,8 @@ TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1", "[pinningIndex]") for (const auto& pin : pins) { - // Migration adds columns with DEFAULT '' and NULL respectively - REQUIRE(pin.GetDateAdded() == ""); + // Migration adds nullable INTEGER and NULL respectively; existing rows get NULL for both + REQUIRE_FALSE(pin.GetDateAdded().has_value()); REQUIRE_FALSE(pin.GetNote().has_value()); } diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h index 752eb02d44..247b9d7176 100644 --- a/src/AppInstallerCommonCore/Public/winget/Pin.h +++ b/src/AppInstallerCommonCore/Public/winget/Pin.h @@ -3,6 +3,7 @@ #pragma once #include "winget/Manifest.h" #include "AppInstallerVersions.h" +#include #include #include @@ -99,10 +100,10 @@ namespace AppInstaller::Pinning PinType GetType() const { return m_type; } const PinKey& GetKey() const { return m_key; } const Utility::GatedVersion& GetGatedVersion() const { return m_gatedVersion; } - const std::string& GetDateAdded() const { return m_dateAdded; } + const std::optional& GetDateAdded() const { return m_dateAdded; } const std::optional& GetNote() const { return m_note; } - void SetDateAdded(std::string dateAdded) { m_dateAdded = std::move(dateAdded); } + void SetDateAdded(std::optional dateAdded) { m_dateAdded = std::move(dateAdded); } void SetNote(std::optional note) { m_note = std::move(note); } bool operator==(const Pin& other) const; @@ -121,7 +122,7 @@ namespace AppInstaller::Pinning PinType m_type = PinType::Unknown; PinKey m_key; Utility::GatedVersion m_gatedVersion; - std::string m_dateAdded; + std::optional m_dateAdded; std::optional m_note; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 800524dab2..4a49697add 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "PinTable.h" +#include #include #include "Microsoft/Schema/IPinningIndex.h" @@ -9,11 +10,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { namespace { - using PinRow = std::tuple>; + using PinRow = std::tuple, std::optional>; std::optional GetPinFromRow(PinRow&& row) { - auto [packageId, sourceId, type, version, dateAdded, note] = std::move(row); + auto [packageId, sourceId, type, version, epochOpt, note] = std::move(row); std::optional result; @@ -36,6 +37,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return {}; } + std::optional dateAdded; + if (epochOpt.has_value()) + { + dateAdded = Utility::ConvertUnixEpochToSystemClock(*epochOpt); + } + result->SetDateAdded(std::move(dateAdded)); result->SetNote(std::move(note)); @@ -59,7 +66,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "migratepintable_v1_0_to_v1_1_pintable"); StatementBuilder addDateAdded; - addDateAdded.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_DateAdded_Column, Type::Text).NotNull().Default("''"sv); + addDateAdded.AlterTable(s_PinTable_Table_Name).Add(s_PinTable_DateAdded_Column, Type::Integer); addDateAdded.Execute(connection); StatementBuilder addNote; @@ -73,6 +80,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); + + const auto& dateAdded = pin.GetDateAdded(); + std::optional epochOpt = dateAdded.has_value() + ? std::optional{ Utility::ConvertSystemClockToUnixEpoch(*dateAdded) } + : std::nullopt; + builder.InsertInto(s_PinTable_Table_Name) .Columns({ s_PinTable_PackageId_Column, @@ -86,7 +99,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 pinKey.SourceId, pin.GetType(), pin.GetGatedVersion().ToString(), - pin.GetDateAdded(), + epochOpt, pin.GetNote()); builder.Execute(connection); @@ -101,8 +114,18 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 .Column(s_PinTable_PackageId_Column).Equals(pinKey.PackageId) .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) .Column(s_PinTable_Type_Column).Equals(pin.GetType()) - .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) - .Column(s_PinTable_DateAdded_Column).Equals(pin.GetDateAdded()); + .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()); + + // Use Unbound (= ?) for null date so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. + const auto& dateAdded = pin.GetDateAdded(); + if (dateAdded.has_value()) + { + builder.Column(s_PinTable_DateAdded_Column).Equals(Utility::ConvertSystemClockToUnixEpoch(*dateAdded)); + } + else + { + builder.Column(s_PinTable_DateAdded_Column).Equals(SQLite::Builder::Unbound); + } // Use Unbound (= ?) for null note so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. const auto& note = pin.GetNote(); @@ -139,7 +162,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return {}; } - return GetPinFromRow(select.GetRow>()); + return GetPinFromRow(select.GetRow, std::optional>()); } std::vector PinTable::GetAllPins(SQLite::Connection& connection) @@ -159,7 +182,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 std::vector pins; while (select.Step()) { - auto pin = GetPinFromRow(select.GetRow>()); + auto pin = GetPinFromRow(select.GetRow, std::optional>()); if (pin) { pins.push_back(std::move(pin.value())); diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 5516ebefd6..cdb43a6479 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1658,11 +1658,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; - std::string dateAdded = ::AppInstaller::Utility::TimePointToString( - std::chrono::system_clock::now(), - ::AppInstaller::Utility::TimeFacet::Year | ::AppInstaller::Utility::TimeFacet::Month | - ::AppInstaller::Utility::TimeFacet::Day | ::AppInstaller::Utility::TimeFacet::Hour | - ::AppInstaller::Utility::TimeFacet::Minute | ::AppInstaller::Utility::TimeFacet::Second); + auto pinTime = std::chrono::system_clock::now(); std::optional note; if (!options.Note().empty()) @@ -1680,7 +1676,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS, !options.Force()); } - newPin.SetDateAdded(dateAdded); + newPin.SetDateAdded(pinTime); newPin.SetNote(note); pinningData.AddOrUpdatePin(newPin); } diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 822d9c037b..50f8b86c9e 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1740,8 +1740,8 @@ namespace Microsoft.Management.Deployment /// The gated version range. Only meaningful when Type is Gating; empty otherwise. String GatedVersion { get; }; - /// The UTC date/time when the pin was added (ISO 8601 format). - String DateAdded { get; }; + /// The UTC date/time when the pin was added. Null if not set. + Windows.Foundation.IReference DateAdded { get; }; /// Optional note associated with the pin. May be empty. String Note { get; }; diff --git a/src/Microsoft.Management.Deployment/PackagePin.cpp b/src/Microsoft.Management.Deployment/PackagePin.cpp index dd06b89ad1..b419320926 100644 --- a/src/Microsoft.Management.Deployment/PackagePin.cpp +++ b/src/Microsoft.Management.Deployment/PackagePin.cpp @@ -33,7 +33,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation m_sourceId = winrt::to_hstring(pin.GetKey().SourceId); m_type = ConvertPinType(pin.GetType()); m_gatedVersion = winrt::to_hstring(pin.GetGatedVersion().ToString()); - m_dateAdded = winrt::to_hstring(pin.GetDateAdded()); + const auto& dateAdded = pin.GetDateAdded(); + if (dateAdded.has_value()) + { + m_dateAdded = winrt::clock::from_sys(*dateAdded); + } m_note = pin.GetNote() ? winrt::to_hstring(*pin.GetNote()) : hstring{}; m_isForInstalledPackage = pin.GetKey().IsForInstalled(); } @@ -58,7 +62,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation return m_gatedVersion; } - hstring PackagePin::DateAdded() + winrt::Windows::Foundation::IReference PackagePin::DateAdded() { return m_dateAdded; } diff --git a/src/Microsoft.Management.Deployment/PackagePin.h b/src/Microsoft.Management.Deployment/PackagePin.h index 2d3c0f37ff..384170843e 100644 --- a/src/Microsoft.Management.Deployment/PackagePin.h +++ b/src/Microsoft.Management.Deployment/PackagePin.h @@ -18,7 +18,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring SourceId(); winrt::Microsoft::Management::Deployment::PackagePinType Type(); hstring GatedVersion(); - hstring DateAdded(); + winrt::Windows::Foundation::IReference DateAdded(); hstring Note(); bool IsForInstalledPackage(); @@ -28,7 +28,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation hstring m_sourceId; winrt::Microsoft::Management::Deployment::PackagePinType m_type = winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; hstring m_gatedVersion; - hstring m_dateAdded; + winrt::Windows::Foundation::IReference m_dateAdded{ nullptr }; hstring m_note; bool m_isForInstalledPackage = false; #endif diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs index 48ae65c8d9..7bc288acbf 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs @@ -57,9 +57,9 @@ public string GatedVersion } /// - /// Gets the date the pin was added (UTC ISO 8601 string). + /// Gets the UTC date/time when the pin was added. Null if not set. /// - public string DateAdded + public DateTimeOffset? DateAdded { get { return this.packagePin.DateAdded; } } From 29d62037415100d64c3e238dea2914d936ee65f5 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:33:05 -0500 Subject: [PATCH 16/46] Guard against non-existent source --- src/Microsoft.Management.Deployment/PackageManager.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index cdb43a6479..a34ddc5180 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1737,10 +1737,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation if (!sourceName.empty()) { auto matchingSource = GetMatchingSource(winrt::to_string(sourceName)); - if (matchingSource.has_value()) - { - sourceId = matchingSource->Identifier; - } + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !matchingSource.has_value()); + + sourceId = matchingSource->Identifier; } auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; From 184cc7abcc0745556cfb1cbf15ba1edaaaf38bf4 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:34:15 -0500 Subject: [PATCH 17/46] Cache IsPinned --- .../PSObjects/PSInstalledCatalogPackage.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs index d053cc204d..f8ade5e2bf 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -15,6 +15,8 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// public sealed class PSInstalledCatalogPackage : PSCatalogPackage { + private bool? isPinned; + /// /// Initializes a new instance of the class. /// @@ -37,7 +39,15 @@ public string InstalledVersion /// public bool IsPinned { - get { return PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; } + get + { + if (!this.isPinned.HasValue) + { + this.isPinned = PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; + } + + return this.isPinned.Value; + } } /// From 952e56ee06c1a466847c1693f6982390b07fd918 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:36:56 -0500 Subject: [PATCH 18/46] Merge default pin add behavior into single test --- src/AppInstallerCLITests/PinFlow.cpp | 43 ++-------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp index d998cdb586..baaaf7752a 100644 --- a/src/AppInstallerCLITests/PinFlow.cpp +++ b/src/AppInstallerCLITests/PinFlow.cpp @@ -43,6 +43,8 @@ TEST_CASE("PinFlow_Add", "[PinFlow][workflow]") REQUIRE(pins[0].GetGatedVersion().ToString() == ""); REQUIRE(pins[0].GetKey().PackageId == "AppInstallerCliTest.TestExeInstaller"); REQUIRE(pins[0].GetKey().SourceId == "*TestSource"); + REQUIRE(pins[0].GetDateAdded().has_value()); + REQUIRE_FALSE(pins[0].GetNote().has_value()); std::ostringstream pinListOutput; TestContext listContext{ pinListOutput, std::cin }; @@ -201,27 +203,6 @@ TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]") REQUIRE(pinResetOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos); } -TEST_CASE("PinFlow_Add_SetsDateAdded", "[PinFlow][workflow]") -{ - TempFile indexFile("pinningIndex", ".db"); - TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); - - std::ostringstream pinAddOutput; - TestContext addContext{ pinAddOutput, std::cin }; - OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); - addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); - addContext.Args.AddArg(Execution::Args::Type::BlockingPin); - - PinAddCommand pinAdd({}); - pinAdd.Execute(addContext); - INFO(pinAddOutput.str()); - - auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); - auto pins = index.GetAllPins(); - REQUIRE(pins.size() == 1); - REQUIRE(pins[0].GetDateAdded().has_value()); -} - TEST_CASE("PinFlow_Add_WithNote", "[PinFlow][workflow]") { TempFile indexFile("pinningIndex", ".db"); @@ -244,26 +225,6 @@ TEST_CASE("PinFlow_Add_WithNote", "[PinFlow][workflow]") REQUIRE(pins[0].GetNote().value() == "my test note"); } -TEST_CASE("PinFlow_Add_WithoutNote", "[PinFlow][workflow]") -{ - TempFile indexFile("pinningIndex", ".db"); - TestHook::SetPinningIndex_Override pinningIndexOverride(indexFile.GetPath()); - - std::ostringstream pinAddOutput; - TestContext addContext{ pinAddOutput, std::cin }; - OverrideForCompositeInstalledSource(addContext, CreateTestSource({ TSR::TestInstaller_Exe })); - addContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query); - - PinAddCommand pinAdd({}); - pinAdd.Execute(addContext); - INFO(pinAddOutput.str()); - - auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read); - auto pins = index.GetAllPins(); - REQUIRE(pins.size() == 1); - REQUIRE_FALSE(pins[0].GetNote().has_value()); -} - // Helper: Creates a v1.1 pinning index at the given path and adds the provided pins directly. // Each pin should already have date_added and note set as desired. namespace From e823b75f8433a975bb71cccfbc33425e9a5e21df Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:55:02 -0500 Subject: [PATCH 19/46] Ensure table is not created in a partial state --- .../Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index cd6bd5be73..606b51993f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -29,9 +29,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 void PinningIndexInterface::CreateTables(SQLite::Connection& connection) { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_1"); Pinning_V1_0::PinningIndexInterface base; base.CreateTables(connection); MigrateFrom(connection, &base); + savepoint.Commit(); } bool PinningIndexInterface::MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) From 195846f652a4f9dc0005d716bace56b5da816dbe Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 22:59:14 -0500 Subject: [PATCH 20/46] Add an alternate function for optional parms --- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 32 ++++++------------- .../Public/winget/SQLiteStatementBuilder.h | 18 +++++++++++ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 4a49697add..523c5a2938 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -110,33 +110,19 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); + + const auto& dateAdded = pin.GetDateAdded(); + std::optional epochOpt = dateAdded.has_value() + ? std::optional{ Utility::ConvertSystemClockToUnixEpoch(*dateAdded) } + : std::nullopt; + builder.Update(s_PinTable_Table_Name).Set() .Column(s_PinTable_PackageId_Column).Equals(pinKey.PackageId) .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) .Column(s_PinTable_Type_Column).Equals(pin.GetType()) - .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()); - - // Use Unbound (= ?) for null date so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. - const auto& dateAdded = pin.GetDateAdded(); - if (dateAdded.has_value()) - { - builder.Column(s_PinTable_DateAdded_Column).Equals(Utility::ConvertSystemClockToUnixEpoch(*dateAdded)); - } - else - { - builder.Column(s_PinTable_DateAdded_Column).Equals(SQLite::Builder::Unbound); - } - - // Use Unbound (= ?) for null note so SQLite stores NULL via = NULL, not the invalid SET syntax IS NULL. - const auto& note = pin.GetNote(); - if (note.has_value()) - { - builder.Column(s_PinTable_Note_Column).Equals(note.value()); - } - else - { - builder.Column(s_PinTable_Note_Column).Equals(SQLite::Builder::Unbound); - } + .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_DateAdded_Column).AssignValue(epochOpt) + .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()); builder.Where(SQLite::RowIDName).Equals(pinId); builder.Execute(connection); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 7de19998d7..c013b7ce9c 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -278,6 +278,24 @@ namespace AppInstaller::SQLite::Builder return IsNull(); } } + + // Assigns a value using "= ?" binding semantics. When the optional is empty, binds NULL + // via the "= ?" parameter rather than producing "IS NULL". Use this instead of + // Equals(optional) in UPDATE SET clauses, where "IS NULL" is invalid SQL. + template + StatementBuilder& AssignValue(const std::optional& value) + { + if (value) + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), value.value()); + } + else + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), nullptr); + } + return *this; + } + // The optional index value can be used to specify the parameter index. StatementBuilder& Equals(details::unbound_t, std::optional index = {}); StatementBuilder& Equals(std::nullptr_t); From 0468b3458529cb9ebe119a11434bec2461f8af72 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:08:06 -0500 Subject: [PATCH 21/46] Move to PackageCatalogReference --- .../PackageManager.cpp | 9 +++------ src/Microsoft.Management.Deployment/PackageManager.h | 2 +- .../PackageManager.idl | 6 +++--- .../Commands/ResetPinCommand.cs | 11 ++++++++++- .../Helpers/PackageManagerWrapper.cs | 6 +++--- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index a34ddc5180..73a68965ac 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1724,7 +1724,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation return MakePinPackageResult(terminationHR); } - winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::ResetAllPins(winrt::hstring const& sourceName) + winrt::Microsoft::Management::Deployment::PinPackageResult PackageManager::ResetAllPins(winrt::Microsoft::Management::Deployment::PackageCatalogReference packageCatalogReference) { LogStartupIfApplicable(); @@ -1734,12 +1734,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); std::string sourceId; - if (!sourceName.empty()) + if (packageCatalogReference) { - auto matchingSource = GetMatchingSource(winrt::to_string(sourceName)); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !matchingSource.has_value()); - - sourceId = matchingSource->Identifier; + sourceId = winrt::to_string(packageCatalogReference.Info().Id()); } auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; diff --git a/src/Microsoft.Management.Deployment/PackageManager.h b/src/Microsoft.Management.Deployment/PackageManager.h index f1086736fb..a61057fc28 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.h +++ b/src/Microsoft.Management.Deployment/PackageManager.h @@ -59,7 +59,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Windows::Foundation::Collections::IVectorView GetPins(winrt::Microsoft::Management::Deployment::CatalogPackage package); winrt::Microsoft::Management::Deployment::PinPackageResult PinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package, winrt::Microsoft::Management::Deployment::PinPackageOptions options); winrt::Microsoft::Management::Deployment::PinPackageResult UnpinPackage(winrt::Microsoft::Management::Deployment::CatalogPackage package); - winrt::Microsoft::Management::Deployment::PinPackageResult ResetAllPins(winrt::hstring const& sourceName); + winrt::Microsoft::Management::Deployment::PinPackageResult ResetAllPins(winrt::Microsoft::Management::Deployment::PackageCatalogReference packageCatalogReference); }; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index 50f8b86c9e..6a6340b9a4 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -1698,9 +1698,9 @@ namespace Microsoft.Management.Deployment /// Returns PackagePinNotFound if no pin exists for the package. PinPackageResult UnpinPackage(CatalogPackage package); - /// Reset (remove) all pins. If sourceName is non-empty, only pins from that - /// catalog source are removed; otherwise all pins are removed. - PinPackageResult ResetAllPins(String sourceName); + /// Reset (remove) all pins. If packageCatalogReference is non-null, only pins from that + /// catalog are removed; otherwise all pins are removed. + PinPackageResult ResetAllPins(PackageCatalogReference packageCatalogReference); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs index b25b9871e4..d5f10e5144 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/ResetPinCommand.cs @@ -7,7 +7,9 @@ namespace Microsoft.WinGet.Client.Engine.Commands { using System.Management.Automation; + using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.PSObjects; using Microsoft.WinGet.Common.Command; @@ -32,8 +34,15 @@ public ResetPinCommand(PSCmdlet psCmdlet) /// The source name to scope the reset. Pass null or empty to reset all sources. public void Reset(string sourceName) { + PackageCatalogReference? catalogReference = null; + if (!string.IsNullOrEmpty(sourceName)) + { + catalogReference = PackageManagerWrapper.Instance.GetPackageCatalogByName(sourceName) + ?? throw new InvalidSourceException(sourceName); + } + var result = this.Execute( - () => PackageManagerWrapper.Instance.ResetAllPins(sourceName ?? string.Empty)); + () => PackageManagerWrapper.Instance.ResetAllPins(catalogReference)); this.Write(StreamType.Object, new PSPinResult(result)); } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs index aedf6c9402..0d1c593c34 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/PackageManagerWrapper.cs @@ -187,12 +187,12 @@ public PinPackageResult UnpinPackage(CatalogPackage package) /// /// Wrapper for ResetAllPins. /// - /// The source name to reset pins for. Pass empty string to reset all. + /// The catalog reference to reset pins for. Pass null to reset all. /// A PinPackageResult. - public PinPackageResult ResetAllPins(string sourceName) + public PinPackageResult ResetAllPins(PackageCatalogReference? packageCatalogReference) { return this.Execute( - () => this.packageManager.ResetAllPins(sourceName), + () => this.packageManager.ResetAllPins(packageCatalogReference!), false); } From 4bfde99e7cac82fefad950f5093d982d7dca1bf0 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:10:53 -0500 Subject: [PATCH 22/46] Move functionality to converters --- .../Converters.cpp | 17 +++++++++++++++ .../Converters.h | 2 ++ .../PackagePin.cpp | 21 +------------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp index bbd82c6e2e..da6ae41e91 100644 --- a/src/Microsoft.Management.Deployment/Converters.cpp +++ b/src/Microsoft.Management.Deployment/Converters.cpp @@ -579,4 +579,21 @@ namespace winrt::Microsoft::Management::Deployment::implementation default: return AppInstaller::Manifest::PlatformEnum::Unknown; } } + + winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type) + { + switch (type) + { + case ::AppInstaller::Pinning::PinType::PinnedByManifest: + return winrt::Microsoft::Management::Deployment::PackagePinType::PinnedByManifest; + case ::AppInstaller::Pinning::PinType::Pinning: + return winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; + case ::AppInstaller::Pinning::PinType::Gating: + return winrt::Microsoft::Management::Deployment::PackagePinType::Gating; + case ::AppInstaller::Pinning::PinType::Blocking: + return winrt::Microsoft::Management::Deployment::PackagePinType::Blocking; + default: + return winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; + } + } } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h index 888bc72ff6..903a77e61e 100644 --- a/src/Microsoft.Management.Deployment/Converters.h +++ b/src/Microsoft.Management.Deployment/Converters.h @@ -3,6 +3,7 @@ #pragma once #include "PackageMatchFilter.g.h" #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::RemovePackageCatalogStatus GetRemovePackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::EditPackageCatalogStatus GetEditPackageCatalogOperationStatus(winrt::hresult hresult); winrt::Microsoft::Management::Deployment::PinResultStatus GetPinOperationStatus(winrt::hresult hresult); + winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type); ::AppInstaller::Manifest::PlatformEnum GetPlatformEnum(winrt::Microsoft::Management::Deployment::WindowsPlatform value); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_, _repairResultStatus_) \ diff --git a/src/Microsoft.Management.Deployment/PackagePin.cpp b/src/Microsoft.Management.Deployment/PackagePin.cpp index b419320926..d0b649da89 100644 --- a/src/Microsoft.Management.Deployment/PackagePin.cpp +++ b/src/Microsoft.Management.Deployment/PackagePin.cpp @@ -3,30 +3,11 @@ #include "pch.h" #include "PackagePin.h" #include "PackagePin.g.cpp" +#include "Converters.h" #include namespace winrt::Microsoft::Management::Deployment::implementation { - namespace - { - winrt::Microsoft::Management::Deployment::PackagePinType ConvertPinType(::AppInstaller::Pinning::PinType type) - { - switch (type) - { - case ::AppInstaller::Pinning::PinType::PinnedByManifest: - return winrt::Microsoft::Management::Deployment::PackagePinType::PinnedByManifest; - case ::AppInstaller::Pinning::PinType::Pinning: - return winrt::Microsoft::Management::Deployment::PackagePinType::Pinning; - case ::AppInstaller::Pinning::PinType::Gating: - return winrt::Microsoft::Management::Deployment::PackagePinType::Gating; - case ::AppInstaller::Pinning::PinType::Blocking: - return winrt::Microsoft::Management::Deployment::PackagePinType::Blocking; - default: - return winrt::Microsoft::Management::Deployment::PackagePinType::Unknown; - } - } - } - void PackagePin::Initialize(const ::AppInstaller::Pinning::Pin& pin) { m_packageId = winrt::to_hstring(pin.GetKey().PackageId); From 243798b67b536a7b9b03fc64d2be9750054f7fc0 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:17:01 -0500 Subject: [PATCH 23/46] Remove version comment It is no longer needed since the interface surface is so small --- .../Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h index 1a13194076..57ba50cc7d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -7,7 +7,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { struct PinningIndexInterface : public Pinning_V1_0::PinningIndexInterface { - // Version 1.1 SQLite::Version GetVersion() const override; void CreateTables(SQLite::Connection& connection) override; bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; From 4b7b8efdd7b7375d6746f1732255fe0d1fa94c64 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:20:44 -0500 Subject: [PATCH 24/46] Update filters so 1.1 schema is visible --- .../AppInstallerRepositoryCore.vcxproj.filters | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index d877dedeab..b5381284b1 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -82,6 +82,9 @@ {f05e19bb-2161-4ab0-9d04-2dfa2d3eb3c6} + + {a3e2f7c1-5d84-4b9e-8f02-1c6d3a7b0e45} + {21da32f5-b918-436e-96a9-465525f90259} @@ -375,6 +378,12 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + + + Microsoft\Schema\Pinning_1_1 + Public\winget @@ -695,6 +704,9 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + Source Files @@ -728,6 +740,9 @@ Microsoft\Schema\Pinning_1_0 + + Microsoft\Schema\Pinning_1_1 + Rest\Schema\1_7 From e93e3d1f041c63a83299b3640d49eda9b686cf01 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:29:04 -0500 Subject: [PATCH 25/46] Create the correct interface and migrate only if newer --- .../Microsoft/PinningIndex.cpp | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index 0bcddd1e62..e9081b3c6a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -206,27 +206,28 @@ namespace AppInstaller::Repository::Microsoft SQLiteStorageBase(target, disposition, std::move(indexFile)) { AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); - m_interface = CreateIPinningIndexForVersion(SQLite::Version::Latest()); - if (m_version != m_interface->GetVersion()) + // Create the correct interface for the stored schema version. + m_interface = CreateIPinningIndexForVersion(m_version); + + if (disposition == SQLiteStorageBase::OpenDisposition::ReadWrite) { - if (disposition == SQLiteStorageBase::OpenDisposition::ReadWrite) - { - // Attempt to migrate from the stored version to the current version. - AICLI_LOG(Repo, Info, << "Attempting to migrate Pinning Index from [" << m_version << "] to [" << m_interface->GetVersion() << "]"); + // For writable opens, create a latest interface and migrate if the stored version is older. + auto latestInterface = CreateIPinningIndexForVersion(SQLite::Version::Latest()); - // Create an interface representing the existing (older) schema so MigrateFrom can inspect it. - std::unique_ptr oldInterface = CreateIPinningIndexForVersion(m_version); + if (m_version != latestInterface->GetVersion()) + { + AICLI_LOG(Repo, Info, << "Attempting to migrate Pinning Index from [" << m_version << "] to [" << latestInterface->GetVersion() << "]"); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_migrate"); - bool migrated = m_interface->MigrateFrom(m_dbconn, oldInterface.get()); + bool migrated = latestInterface->MigrateFrom(m_dbconn, m_interface.get()); if (migrated) { - m_interface->GetVersion().SetSchemaVersion(m_dbconn); + latestInterface->GetVersion().SetSchemaVersion(m_dbconn); SetLastWriteTime(); savepoint.Commit(); - m_version = m_interface->GetVersion(); + m_version = latestInterface->GetVersion(); AICLI_LOG(Repo, Info, << "Migration successful"); } else @@ -235,12 +236,8 @@ namespace AppInstaller::Repository::Microsoft THROW_HR(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX); } } - else - { - // Read-only open: use an interface matching the stored schema version to avoid querying missing columns. - AICLI_LOG(Repo, Info, << "Read-only open with older schema [" << m_version << "]; using compatible interface"); - m_interface = CreateIPinningIndexForVersion(m_version); - } + + m_interface = std::move(latestInterface); } } From ae87ff17b457e5233029e2121ef53df377d5989f Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:31:40 -0500 Subject: [PATCH 26/46] Use correct version info --- src/Microsoft.Management.Deployment/PackageManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 73a68965ac..77c86b54f5 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1525,7 +1525,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation auto versionInfo = package.GetPackageVersionInfo(versionId); if (versionInfo) { - std::string packageId = winrt::to_string(package.Id()); + std::string packageId = winrt::to_string(versionInfo.Id()); std::string sourceId = winrt::to_string(versionInfo.PackageCatalog().Info().Id()); if (!packageId.empty() && !sourceId.empty()) { From 917db77865b6e46143eae516cbd9a15a2be95a64 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:35:00 -0500 Subject: [PATCH 27/46] Move invariants outside try catch --- .../PackageManager.cpp | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 77c86b54f5..200728d155 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1639,20 +1639,18 @@ namespace winrt::Microsoft::Management::Deployment::implementation { LogStartupIfApplicable(); + THROW_HR_IF_NULL(E_POINTER, package); + THROW_HR_IF_NULL(E_POINTER, options); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + + // Gating pins require a non-empty version range. + THROW_HR_IF(E_INVALIDARG, + options.PinType() == winrt::Microsoft::Management::Deployment::PackagePinType::Gating && + options.GatedVersion().empty()); + HRESULT terminationHR = S_OK; try { - THROW_HR_IF_NULL(E_POINTER, package); - THROW_HR_IF_NULL(E_POINTER, options); - THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); - - // Gating pins require a non-empty version range. - if (options.PinType() == winrt::Microsoft::Management::Deployment::PackagePinType::Gating && - options.GatedVersion().empty()) - { - THROW_HR(E_INVALIDARG); - } - auto pinKeys = GetPinKeysForCatalogPackage(package, options.PinInstalledPackage()); THROW_HR_IF(E_INVALIDARG, pinKeys.empty()); @@ -1694,12 +1692,12 @@ namespace winrt::Microsoft::Management::Deployment::implementation { LogStartupIfApplicable(); + THROW_HR_IF_NULL(E_POINTER, package); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + HRESULT terminationHR = S_OK; try { - THROW_HR_IF_NULL(E_POINTER, package); - THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); - auto pinKeys = GetPinKeysForCatalogPackage(package, /* includeInstalled */ true); auto pinningData = ::AppInstaller::Pinning::PinningData{ ::AppInstaller::Pinning::PinningData::Disposition::ReadWrite }; @@ -1728,11 +1726,11 @@ namespace winrt::Microsoft::Management::Deployment::implementation { LogStartupIfApplicable(); + THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); + HRESULT terminationHR = S_OK; try { - THROW_IF_FAILED(EnsureComCallerHasCapability(Capability::PackageManagement)); - std::string sourceId; if (packageCatalogReference) { From 34a727c1612ade9c84e3da17877b56c042af26be Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 30 Apr 2026 23:53:06 -0500 Subject: [PATCH 28/46] Add TryRemovePin for simplicity --- src/AppInstallerRepositoryCore/PinningData.cpp | 11 +++++++++++ .../Public/winget/PinningData.h | 2 ++ .../PackageManager.cpp | 7 +------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/AppInstallerRepositoryCore/PinningData.cpp b/src/AppInstallerRepositoryCore/PinningData.cpp index ac29bb0d0a..801f4f5fe1 100644 --- a/src/AppInstallerRepositoryCore/PinningData.cpp +++ b/src/AppInstallerRepositoryCore/PinningData.cpp @@ -93,6 +93,17 @@ namespace AppInstaller::Pinning m_database->RemovePin(pinKey); } + bool PinningData::TryRemovePin(const PinKey& pinKey) + { + THROW_HR_IF(E_NOT_VALID_STATE, !IsDatabaseConnected()); + if (!m_database->GetPin(pinKey)) + { + return false; + } + m_database->RemovePin(pinKey); + return true; + } + std::optional PinningData::GetPin(const PinKey& pinKey) { return IsDatabaseConnected() ? m_database->GetPin(pinKey) : std::nullopt; diff --git a/src/AppInstallerRepositoryCore/Public/winget/PinningData.h b/src/AppInstallerRepositoryCore/Public/winget/PinningData.h index ca1e3b36ce..d754611593 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/PinningData.h +++ b/src/AppInstallerRepositoryCore/Public/winget/PinningData.h @@ -58,6 +58,8 @@ namespace AppInstaller::Pinning // Pass through functions to the index itself void AddOrUpdatePin(const Pin& pin); void RemovePin(const PinKey& pinKey); + // Removes the pin if it exists. Returns true if a pin was removed, false if none was found. + bool TryRemovePin(const PinKey& pinKey); std::optional GetPin(const PinKey& pinKey); std::vector GetAllPins(); bool ResetAllPins(std::string_view sourceId = {}); diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 200728d155..44fdeab48f 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1704,12 +1704,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation bool anyRemoved = false; for (const auto& pinKey : pinKeys) { - auto existingPin = pinningData.GetPin(pinKey); - if (existingPin) - { - pinningData.RemovePin(pinKey); - anyRemoved = true; - } + anyRemoved |= pinningData.TryRemovePin(pinKey); } THROW_HR_IF(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST, !anyRemoved); From 01aad8e071cc782f24046186700006c47555974f Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 00:11:19 -0500 Subject: [PATCH 29/46] Refactor to use -Blocking and -GatedVersion --- .../Microsoft.WinGet.Client/Add-WinGetPin.md | 35 +++++++++---------- .../Cmdlets/AddPinCmdlet.cs | 33 ++++++++++++++--- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md index 2992b76ed8..b0bb276916 100644 --- a/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Add-WinGetPin.md @@ -15,7 +15,7 @@ Adds a WinGet package pin. ### FoundSet (Default) ``` -Add-WinGetPin [-PinType ] [-GatedVersion ] [-PinInstalledPackage] +Add-WinGetPin [-Blocking] [-GatedVersion ] [-PinInstalledPackage] [-Force] [-Note ] [-Id ] [-Name ] [-Moniker ] [-Source ] [[-Query] ] [-MatchOption ] [-ProgressAction ] [-WhatIf] [-Confirm] [] @@ -23,7 +23,7 @@ Add-WinGetPin [-PinType ] [-GatedVersion ] [-PinInstal ### GivenSet ``` -Add-WinGetPin [-PinType ] [-GatedVersion ] [-PinInstalledPackage] +Add-WinGetPin [-Blocking] [-GatedVersion ] [-PinInstalledPackage] [-Force] [-Note ] [[-PSCatalogPackage] ] [-ProgressAction ] [-WhatIf] [-Confirm] [] ``` @@ -33,10 +33,10 @@ This command adds a pin for a WinGet package, preventing automatic updates. Mirr of `winget pin add`. By default, all string-based searches are case-insensitive exact matches. Wildcards are not supported. You can change the search behavior using the **MatchOption** parameter. -Pin types: +Pin types are determined by the parameters you provide: - **Pinning** (default): Prevents automatic updates but allows manual upgrades. -- **Blocking**: Prevents all upgrades, including manual ones. -- **Gating**: Allows upgrades only to versions below the specified **GatedVersion**. +- **Blocking**: Use `-Blocking` to prevent all upgrades, including manual ones. +- **Gating**: Use `-GatedVersion` to allow upgrades only to versions satisfying the specified range. ## EXAMPLES @@ -48,13 +48,13 @@ This example adds a Pinning pin for `Microsoft.PowerShell`, preventing automatic ### Example 2: Add a blocking pin ```powershell -Add-WinGetPin -Id "Microsoft.PowerShell" -PinType Blocking +Add-WinGetPin -Id "Microsoft.PowerShell" -Blocking ``` This example adds a Blocking pin for `Microsoft.PowerShell`, preventing all upgrades. ### Example 3: Add a gating pin ```powershell -Add-WinGetPin -Id "Microsoft.PowerShell" -PinType Gating -GatedVersion "<7.5" +Add-WinGetPin -Id "Microsoft.PowerShell" -GatedVersion "<7.5" ``` This example adds a Gating pin that allows upgrades only to versions below 7.5. @@ -106,8 +106,9 @@ Accept wildcard characters: False ### -GatedVersion -Specify the version range for a Gating pin. This parameter is required when **PinType** is -`Gating`. The value uses WinGet version range syntax (e.g., `<7.5`, `>=7.0,<8.0`). +Specify the version range for a Gating pin. When provided, a gating pin is created that limits +upgrades to versions satisfying this range. Uses WinGet version range syntax (e.g., `<7.5`, +`>=7.0,<8.0`). Cannot be combined with **-Blocking**. ```yaml Type: System.String @@ -221,23 +222,19 @@ Accept pipeline input: True (ByPropertyName) Accept wildcard characters: False ``` -### -PinType +### -Blocking -Specify the type of pin to add. Accepted values: - -- **Pinning** (default): Prevents automatic upgrades. -- **Blocking**: Prevents all upgrades. -- **Gating**: Limits upgrades to versions below **GatedVersion**. +When specified, adds a Blocking pin that prevents all upgrades, including manual ones. Cannot be +combined with **-GatedVersion**. ```yaml -Type: Microsoft.WinGet.Client.PSObjects.PSPackagePinType +Type: System.Management.Automation.SwitchParameter Parameter Sets: (All) Aliases: -Accepted values: Pinning, Blocking, Gating Required: False Position: Named -Default value: Pinning +Default value: None Accept pipeline input: True (ByPropertyName) Accept wildcard characters: False ``` @@ -329,7 +326,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ### Microsoft.WinGet.Client.PSObjects.PSPackageFieldMatchOption -### Microsoft.WinGet.Client.PSObjects.PSPackagePinType +### System.Management.Automation.SwitchParameter ## OUTPUTS diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs index 8542dab160..1f4defdd9c 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs @@ -16,6 +16,14 @@ namespace Microsoft.WinGet.Client.Commands /// /// Adds a pin for a package. Mirrors the behavior of winget pin add. /// + /// + /// Pin type is determined by which parameters are provided, matching winget pin add behavior: + /// + /// present → Gating pin + /// present → Blocking pin + /// Neither → Pinning pin (default) + /// + /// [Cmdlet( VerbsCommon.Add, Constants.WinGetNouns.Pin, @@ -27,15 +35,19 @@ public sealed class AddPinCmdlet : PackageCmdlet private PinPackageCommand command = null; /// - /// Gets or sets the pin type. Defaults to . + /// Gets or sets a value indicating whether to add a blocking pin, which prevents all upgrades. + /// Cannot be combined with . /// [Parameter(ValueFromPipelineByPropertyName = true)] - public PSPackagePinType PinType { get; set; } = PSPackagePinType.Pinning; + public SwitchParameter Blocking { get; set; } /// - /// Gets or sets the gated version range. Required when is Gating. + /// Gets or sets the gated version range, which creates a gating pin that limits upgrades to + /// versions satisfying this range (e.g., <7.5, >=7.0,<8.0). + /// Cannot be combined with . /// [Parameter(ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] public string GatedVersion { get; set; } /// @@ -61,6 +73,19 @@ public sealed class AddPinCmdlet : PackageCmdlet /// protected override void ProcessRecord() { + if (this.Blocking && !string.IsNullOrEmpty(this.GatedVersion)) + { + this.ThrowTerminatingError(new ErrorRecord( + new System.ArgumentException("Cannot specify both -Blocking and -GatedVersion. Use -GatedVersion for a gating pin or -Blocking for a blocking pin."), + "ConflictingPinTypeParameters", + ErrorCategory.InvalidArgument, + null)); + } + + PSPackagePinType pinType = !string.IsNullOrEmpty(this.GatedVersion) ? PSPackagePinType.Gating + : this.Blocking ? PSPackagePinType.Blocking + : PSPackagePinType.Pinning; + this.command = new PinPackageCommand( this, this.PSCatalogPackage, @@ -71,7 +96,7 @@ protected override void ProcessRecord() this.Query); this.command.Add( this.MatchOption.ToString(), - this.PinType.ToString(), + pinType.ToString(), this.GatedVersion, this.PinInstalledPackage.ToBool(), this.Force.ToBool(), From ad9932fda2439eb03714423e75e47074e115d42a Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 08:43:39 -0500 Subject: [PATCH 30/46] Spelling --- .github/actions/spelling/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8eb3002073..982fc5b913 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -433,6 +433,7 @@ Pherson pid pidl pidlist +pintable PKCS pkgmgr pkindex From a96e43e0c7763aad2459b57277e323703b683de5 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 08:44:43 -0500 Subject: [PATCH 31/46] Add Pester Tests --- .../Cmdlets/AddPinCmdlet.cs | 11 ++ .../Cmdlets/RemovePinCmdlet.cs | 11 ++ .../tests/Microsoft.WinGet.Client.Tests.ps1 | 187 ++++++++++++++++++ 3 files changed, 209 insertions(+) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs index 1f4defdd9c..15e489b2b4 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddPinCmdlet.cs @@ -86,6 +86,17 @@ protected override void ProcessRecord() : this.Blocking ? PSPackagePinType.Blocking : PSPackagePinType.Pinning; + string target = this.PSCatalogPackage?.Id + ?? this.Id + ?? this.Name + ?? (this.Query != null ? string.Join(" ", this.Query) : null) + ?? "package"; + + if (!this.ShouldProcess(target)) + { + return; + } + this.command = new PinPackageCommand( this, this.PSCatalogPackage, diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs index 791de2b270..d00bf2070b 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemovePinCmdlet.cs @@ -52,6 +52,17 @@ protected override void ProcessRecord() id = this.PSPackagePin.PackageId; } + string target = id + ?? this.PSCatalogPackage?.Id + ?? this.Name + ?? (this.Query != null ? string.Join(" ", this.Query) : null) + ?? "package"; + + if (!this.ShouldProcess(target)) + { + return; + } + this.command = new PinPackageCommand( this, catalogPackage, diff --git a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 index f8042c2565..8fecf43d74 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Client.Tests.ps1 @@ -1019,6 +1019,193 @@ Describe 'WindowsPackageManagerServer' -Skip:($PSEdition -eq "Desktop") { } } +Describe 'Add|Get|Remove|Reset-WinGetPin' { + + BeforeAll { + AddTestSource + } + + AfterAll { + Reset-WinGetPin -Force | Out-Null + RemoveTestSource + } + + Context 'Add-WinGetPin' { + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + + It 'Add pinning pin by Id' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + $pin.Type | Should -Be 'Pinning' + } + + It 'Add blocking pin by Id' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Blocking + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Type | Should -Be 'Blocking' + } + + It 'Add gating pin with GatedVersion' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -GatedVersion '<2.0.0.0' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Type | Should -Be 'Gating' + $pin.GatedVersion | Should -Be '<2.0.0.0' + } + + It 'Add pin with Note' { + $result = Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Note 'test note' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + $pin.Note | Should -Be 'test note' + } + + It 'Add with -Blocking and -GatedVersion throws' { + { Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -Blocking -GatedVersion '<2.0.0.0' } | Should -Throw + } + + It 'Add with -WhatIf does not create pin' { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -WhatIf + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + } + + Context 'Get-WinGetPin' { + + BeforeAll { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + AfterAll { + Reset-WinGetPin -Force | Out-Null + } + + It 'Get all pins returns non-empty list' { + $pins = Get-WinGetPin + $pins | Should -Not -BeNullOrEmpty + } + + It 'Get pin by Id' { + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + } + + It 'Get pin scoped to Source' { + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + $pin.SourceId | Should -Not -BeNullOrEmpty + } + + It 'Get pin via pipeline from Find-WinGetPackage' { + $pin = Find-WinGetPackage -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -MatchOption Equals | Get-WinGetPin + + $pin | Should -Not -BeNullOrEmpty + $pin.PackageId | Should -Be 'AppInstallerTest.TestExeInstaller' + } + } + + Context 'Remove-WinGetPin' { + + BeforeEach { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + + It 'Remove pin by Id' { + $result = Remove-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Remove pin via pipeline from Get-WinGetPin' { + Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Remove-WinGetPin + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Remove with -WhatIf does not remove pin' { + Remove-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' -WhatIf + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -Not -BeNullOrEmpty + } + } + + Context 'Reset-WinGetPin' { + + BeforeEach { + Add-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' | Out-Null + } + + It 'Reset all pins with -Force' { + $result = Reset-WinGetPin -Force + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pins = Get-WinGetPin + $pins | Should -BeNullOrEmpty + } + + It 'Reset with -Source scopes to that source' { + $result = Reset-WinGetPin -Source 'TestSource' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Ok' + + $pin = Get-WinGetPin -Id 'AppInstallerTest.TestExeInstaller' -Source 'TestSource' + $pin | Should -BeNullOrEmpty + } + + It 'Reset with -WhatIf does not remove pins' { + Reset-WinGetPin -WhatIf + + $pins = Get-WinGetPin + $pins | Should -Not -BeNullOrEmpty + } + + AfterEach { + Reset-WinGetPin -Force | Out-Null + } + } +} + AfterAll { RestoreWinGetSettings RemoveTestSource From fde73827382cf9ec4b1763c745183b1839d29942 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 08:46:33 -0500 Subject: [PATCH 32/46] Tests should now be part of default creation test --- src/AppInstallerCLITests/PinningIndex.cpp | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index d3127d8d9e..6718f212e7 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -204,29 +204,6 @@ TEST_CASE("PinningIndex_V1_1_AddPin_WithDateAndNote", "[pinningIndex]") } } -TEST_CASE("PinningIndex_V1_1_AddPin_WithoutNote", "[pinningIndex]") -{ - TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; - INFO("Using temporary file named: " << tempFile.GetPath()); - - Pin pin = Pin::CreatePinningPin({ "pkgId", "sourceId" }); - pin.SetDateAdded(AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); - // note intentionally left unset - - { - PinningIndex index = PinningIndex::CreateNew(tempFile, Version::Latest()); - index.AddPin(pin); - } - - { - Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly); - - auto pinFromIndex = Pinning_V1_1::PinTable::GetPinById(connection, 1); - REQUIRE(pinFromIndex.has_value()); - REQUIRE(pinFromIndex->GetDateAdded() == AppInstaller::Utility::ConvertUnixEpochToSystemClock(PinTestEpoch::Jan2026_15_1030)); - REQUIRE_FALSE(pinFromIndex->GetNote().has_value()); - } -} TEST_CASE("PinningIndex_V1_1_AddUpdateRemove", "[pinningIndex]") { From d5f682c63334557bc3a70d0f61de37cc9adbdc52 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:08:37 -0500 Subject: [PATCH 33/46] Further abstract the interface --- .../Pinning_1_0/PinningIndexInterface.h | 6 +++ .../Pinning_1_0/PinningIndexInterface_1_0.cpp | 28 ++++++++-- .../Pinning_1_1/PinningIndexInterface.h | 10 ++-- .../Pinning_1_1/PinningIndexInterface_1_1.cpp | 53 +++---------------- 4 files changed, 43 insertions(+), 54 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h index 42f744c68d..8cc664e60c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h @@ -19,5 +19,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; std::vector GetAllPins(SQLite::Connection& connection) override; bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) override; + + protected: + virtual SQLite::rowid_t IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin); + virtual bool IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin); + virtual std::optional IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId); + virtual std::vector IGetAllPins(SQLite::Connection& connection); }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp index 9d81c5a087..38a95fcbc3 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp @@ -48,7 +48,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), existingPin.has_value()); SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_0"); - SQLite::rowid_t pinId = PinTable::AddPin(connection, pin); + SQLite::rowid_t pinId = IAddPin(connection, pin); savepoint.Commit(); return pinId; @@ -63,7 +63,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_0"); - bool status = PinTable::UpdatePinById(connection, existingPinId.value(), pin); + bool status = IUpdatePinById(connection, existingPinId.value(), pin); savepoint.Commit(); return { status, existingPinId.value() }; @@ -93,12 +93,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return {}; } - return PinTable::GetPinById(connection, existingPinId.value()); + return IGetPinById(connection, existingPinId.value()); } std::vector PinningIndexInterface::GetAllPins(SQLite::Connection& connection) { - return PinTable::GetAllPins(connection); + return IGetAllPins(connection); } bool PinningIndexInterface::ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) @@ -109,4 +109,24 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 return result; } + + SQLite::rowid_t PinningIndexInterface::IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + { + return PinTable::AddPin(connection, pin); + } + + bool PinningIndexInterface::IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) + { + return PinTable::UpdatePinById(connection, pinId, pin); + } + + std::optional PinningIndexInterface::IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) + { + return PinTable::GetPinById(connection, pinId); + } + + std::vector PinningIndexInterface::IGetAllPins(SQLite::Connection& connection) + { + return PinTable::GetAllPins(connection); + } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h index 57ba50cc7d..8c8820127f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h @@ -10,9 +10,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Version GetVersion() const override; void CreateTables(SQLite::Connection& connection) override; bool MigrateFrom(SQLite::Connection& connection, const IPinningIndex* current) override; - SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; - std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) override; - std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) override; - std::vector GetAllPins(SQLite::Connection& connection) override; + + protected: + SQLite::rowid_t IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) override; + bool IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) override; + std::optional IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) override; + std::vector IGetAllPins(SQLite::Connection& connection) override; }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index 606b51993f..21ff29cc22 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -6,21 +6,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 { - namespace - { - std::optional GetExistingPinId(SQLite::Connection& connection, const Pinning::PinKey& pinKey) - { - auto result = PinTable::GetIdByPinKey(connection, pinKey); - - if (!result) - { - AICLI_LOG(Repo, Verbose, << "Did not find pin " << pinKey.ToString()); - } - - return result; - } - } - // Version 1.1 SQLite::Version PinningIndexInterface::GetVersion() const { @@ -50,46 +35,22 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return true; } - SQLite::rowid_t PinningIndexInterface::AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) + SQLite::rowid_t PinningIndexInterface::IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { - auto existingPin = GetExistingPinId(connection, pin.GetKey()); - - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), existingPin.has_value()); - - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addpin_v1_1"); - SQLite::rowid_t pinId = PinTable::AddPin(connection, pin); - - savepoint.Commit(); - return pinId; + return PinTable::AddPin(connection, pin); } - std::pair PinningIndexInterface::UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) + bool PinningIndexInterface::IUpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin) { - auto existingPinId = GetExistingPinId(connection, pin.GetKey()); - - // If the pin doesn't exist, fail the update - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !existingPinId); - - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updatepin_v1_1"); - bool status = PinTable::UpdatePinById(connection, existingPinId.value(), pin); - - savepoint.Commit(); - return { status, existingPinId.value() }; + return PinTable::UpdatePinById(connection, pinId, pin); } - std::optional PinningIndexInterface::GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) + std::optional PinningIndexInterface::IGetPinById(SQLite::Connection& connection, SQLite::rowid_t pinId) { - auto existingPinId = GetExistingPinId(connection, pinKey); - - if (!existingPinId) - { - return {}; - } - - return PinTable::GetPinById(connection, existingPinId.value()); + return PinTable::GetPinById(connection, pinId); } - std::vector PinningIndexInterface::GetAllPins(SQLite::Connection& connection) + std::vector PinningIndexInterface::IGetAllPins(SQLite::Connection& connection) { return PinTable::GetAllPins(connection); } From 8f819c1e5695d18c748e4ca60393d7de3933166a Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:15:38 -0500 Subject: [PATCH 34/46] Update test name --- src/AppInstallerCLITests/PinningIndex.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index 6718f212e7..ef6a01c64b 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -168,7 +168,7 @@ TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]") REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS); } -TEST_CASE("PinningIndexCreateLatest_Is_V1_1", "[pinningIndex]") +TEST_CASE("PinningIndexCreateLatest_HasCorrectVersion", "[pinningIndex]") { TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; INFO("Using temporary file named: " << tempFile.GetPath()); @@ -313,4 +313,4 @@ TEST_CASE("PinningIndex_MigrateFrom_1_0_to_1_1_ReadOnly_Uses_OldInterface", "[pi REQUIRE(pins[0].GetType() == PinType::Pinning); REQUIRE(pins[0].GetKey() == pin.GetKey()); } -} \ No newline at end of file +} From 91f532a18b01989d8b923e042e81dbfb99b05881 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:20:41 -0500 Subject: [PATCH 35/46] Add comment --- .../Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index 21ff29cc22..48785c69cf 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -35,6 +35,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 return true; } + // Override the pin methods to use the correct PinTable methods for version 1.1 + SQLite::rowid_t PinningIndexInterface::IAddPin(SQLite::Connection& connection, const Pinning::Pin& pin) { return PinTable::AddPin(connection, pin); From 595abca17efb269f8b6762615f8bec49290410f4 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:30:54 -0500 Subject: [PATCH 36/46] Appease our AI overlords with an update to their tomes * Updated .github/copilot-instructions.md --- .github/copilot-instructions.md | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 77aabce02b..ea607123ce 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -133,6 +133,105 @@ void WorkflowTask(Execution::Context& context) - Parsing in `AppInstallerCommonCore/Manifest/` - Multi-file manifests: installer, locale, version, defaultLocale +## SQLite Statement Builder + +**Always use `AppInstaller::SQLite::Builder::StatementBuilder` when writing SQLite database code. Never write raw SQL strings.** + +The builder is in `` (namespace `AppInstaller::SQLite::Builder`). It generates type-safe, parameterized SQL and ensures symbolic names are used for tables and columns throughout. + +### Key conventions + +- Define table and column names as `constexpr std::string_view` constants, then pass them to the builder. +- Use `ColumnBuilder` with chained modifiers (`.NotNull()`, `.Unique()`, `.Default(value)`) when creating tables. +- Use `IntegerPrimaryKey()` for auto-increment rowid primary keys. +- Use `Unbound` as a placeholder and bind values later via `Statement::Bind()`, or pass values directly to have them bound automatically. + +### Common operations + +```cpp +using namespace AppInstaller::SQLite::Builder; + +// Symbolic names +static constexpr std::string_view s_MyTable = "my_table"sv; +static constexpr std::string_view s_Col_Id = "id"sv; +static constexpr std::string_view s_Col_Name = "name"sv; +static constexpr std::string_view s_Col_Value = "value"sv; + +// CREATE TABLE +StatementBuilder builder; +builder.CreateTable(s_MyTable).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(s_Col_Name, Type::Text).NotNull().Unique(), + ColumnBuilder(s_Col_Value, Type::Int64).NotNull().Default(0), +}); +builder.Execute(connection); + +// SELECT +StatementBuilder builder; +builder.Select({ s_Col_Id, s_Col_Name }) + .From(s_MyTable) + .Where(s_Col_Name).Equals(nameValue); +auto stmt = builder.Prepare(connection); +while (stmt.Step()) { /* stmt.GetColumn(index) */ } + +// INSERT +StatementBuilder builder; +builder.InsertInto(s_MyTable) + .Columns({ s_Col_Name, s_Col_Value }) + .Values(nameValue, intValue); +builder.Execute(connection); + +// UPDATE +StatementBuilder builder; +builder.Update(s_MyTable).Set() + .Column(s_Col_Value).Equals(newValue) + .Where(s_Col_Id).Equals(rowId); +builder.Execute(connection); + +// DELETE +StatementBuilder builder; +builder.DeleteFrom(s_MyTable) + .Where(s_Col_Id).Equals(rowId); +builder.Execute(connection); + +// ALTER TABLE – add a column +StatementBuilder builder; +builder.AlterTable(s_MyTable).Add(s_Col_NewCol, Type::Text).NotNull().Default(0); +builder.Execute(connection); +``` + +### Nullable values: `Equals()` vs `AssignValue()` + +Both accept `std::optional`, but they behave differently when the optional is empty and must be used in the right context: + +| Method | Empty optional emits | Use in | +|---|---|---| +| `Equals(optional)` | `IS NULL` | **WHERE** / filter clauses | +| `AssignValue(optional)` | `= ?` (binds NULL) | **UPDATE SET** assignments | + +Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col = IS NULL`. + +```cpp +// ✅ Correct: Equals for WHERE filter, AssignValue for SET assignment +std::optional maybeEpoch = ...; +std::optional maybeNote = ...; + +builder.Update(s_MyTable).Set() + .Column(s_Col_Name).Equals(requiredName) // non-optional: fine in SET + .Column(s_Col_Epoch).AssignValue(maybeEpoch) // nullable: must use AssignValue in SET + .Column(s_Col_Note).AssignValue(maybeNote) // nullable: must use AssignValue in SET + .Where(s_Col_Id).Equals(rowId); // filter: Equals is correct here + +// ✅ Correct: Equals(optional) in WHERE — emits "IS NULL" when empty +builder.Select(s_Col_Id).From(s_MyTable) + .Where(s_Col_Note).Equals(maybeNote); // → "WHERE note IS NULL" when empty +``` + +### Execution + +- `builder.Execute(connection)` — prepares, binds, and runs a statement that returns no rows. +- `builder.Prepare(connection)` — returns a `SQLite::Statement`; call `.Step()` to iterate, `.GetColumn(index)` to read values. + ## Naming Conventions - **Namespace structure**: `AppInstaller::[::]` From 7e83a832b5d34a72c506a29f4e279c0977ec4cb7 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:42:20 -0500 Subject: [PATCH 37/46] Unify on AssignValue to avoid future confusion --- src/AppInstallerCLITests/SQLiteWrapper.cpp | 2 +- .../Microsoft/Schema/1_0/OneToOneTable.cpp | 2 +- .../Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp | 4 ++-- .../Microsoft/Schema/1_1/ManifestMetadataTable.cpp | 2 +- .../Microsoft/Schema/2_0/OneToManyTableWithMap.cpp | 2 +- .../Schema/2_0/PackageUpdateTrackingTable.cpp | 6 +++--- .../Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp | 4 ++-- .../Microsoft/Schema/Pinning_1_0/PinTable.cpp | 8 ++++---- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 8 ++++---- .../Microsoft/Schema/Portable_1_0/PortableTable.cpp | 8 ++++---- .../Public/winget/SQLiteStatementBuilder.h | 10 ++++++++++ .../Database/Schema/0_1/SetInfoTable.cpp | 12 ++++++------ .../Database/Schema/0_2/QueueTable.cpp | 2 +- 13 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp index 7fa70aa955..01f9affc82 100644 --- a/src/AppInstallerCLITests/SQLiteWrapper.cpp +++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -54,7 +54,7 @@ void InsertIntoSimpleTestTable(Connection& connection, int firstVal, const std:: void UpdateSimpleTestTable(Connection& connection, int firstVal, const std::string& secondVal) { 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); update.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp index 2301d0d4b3..ed88f44f17 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp @@ -133,7 +133,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 if (tableValue.value() != value) { SQLite::Builder::StatementBuilder updateBuilder; - updateBuilder.Update(tableName).Set().Column(valueName).Equals(value).Where(SQLite::RowIDName).Equals(selectResult); + updateBuilder.Update(tableName).Set().Column(valueName).AssignValue(value).Where(SQLite::RowIDName).Equals(selectResult); updateBuilder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp index dd34ecbc57..d4fd017d64 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable_1_0.cpp @@ -135,7 +135,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { // Reset all filter values to unselected SQLite::Builder::StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(false); + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(false); builder.Execute(m_connection); } @@ -153,7 +153,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // ) // ) StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(true).Where(s_SearchResultsTable_Manifest).In().BeginParenthetical(). + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(true).Where(s_SearchResultsTable_Manifest).In().BeginParenthetical(). Select(s_SearchResultsTable_SubSelect_ManifestAlias).From().BeginParenthetical(); // Add the field specific portion diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp index dd461b5f41..dd52c5e8c8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/ManifestMetadataTable.cpp @@ -103,7 +103,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 // UPSERT (aka ON CONFLICT) is not available to us, as it was only introduced in 3.24.0 (2018-06-04), // and we need to support Windows 10 (17763) which was released in 2017. StatementBuilder updateBuilder; - updateBuilder.Update(s_ManifestMetadataTable_Table_Name).Set().Column(s_ManifestMetadataTable_Value_Column).Equals(value). + updateBuilder.Update(s_ManifestMetadataTable_Table_Name).Set().Column(s_ManifestMetadataTable_Value_Column).AssignValue(value). Where(s_ManifestMetadataTable_Manifest_Column).Equals(manifestId).And(s_ManifestMetadataTable_Metadata_Column).Equals(metadata); updateBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp index c0b2b75dde..8f0678427e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/OneToManyTableWithMap.cpp @@ -134,7 +134,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 if (tableValue.value() != value) { SQLite::Builder::StatementBuilder updateBuilder; - updateBuilder.Update(tableName).Set().Column(valueName).Equals(value).Where(SQLite::RowIDName).Equals(selectResult); + updateBuilder.Update(tableName).Set().Column(valueName).AssignValue(value).Where(SQLite::RowIDName).Equals(selectResult); updateBuilder.Execute(connection); } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp index 1728a5d6e7..fceea986ad 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/PackageUpdateTrackingTable.cpp @@ -123,9 +123,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // First attempt to update the row and then insert it if no modification occurred. Builder::StatementBuilder updateBuilder; updateBuilder.Update(s_PUTT_Table_Name).Set(). - Column(s_PUTT_WriteTime).Equals(currentTime). - Column(s_PUTT_Manifest).Equals(compressedManifest). - Column(s_PUTT_Hash).Equals(manifestHash). + Column(s_PUTT_WriteTime).AssignValue(currentTime). + Column(s_PUTT_Manifest).AssignValue(compressedManifest). + Column(s_PUTT_Hash).AssignValue(manifestHash). Where(s_PUTT_Package).LikeWithEscape(packageIdentifier); updateBuilder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp index 52dea8d7f0..735a34f9e9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/2_0/SearchResultsTable_2_0.cpp @@ -137,7 +137,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 { // Reset all filter values to unselected SQLite::Builder::StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(false); + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(false); builder.Execute(m_connection); } @@ -155,7 +155,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V2_0 // ) // ) StatementBuilder builder; - builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).Equals(true).Where(s_SearchResultsTable_Package).In().BeginParenthetical(). + builder.Update(GetQualifiedName()).Set().Column(s_SearchResultsTable_Filter).AssignValue(true).Where(s_SearchResultsTable_Package).In().BeginParenthetical(). Select(s_SearchResultsTable_SubSelect_PackageAlias).From().BeginParenthetical(); // Add the field specific portion diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp index 370c3f1e29..da89a2e1d7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp @@ -113,10 +113,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); builder.Update(s_PinTable_Table_Name).Set() - .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pinKey.PackageId) - .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) - .Column(s_PinTable_Type_Column).Equals(pin.GetType()) - .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_PackageId_Column).AssignValue((std::string_view)pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) + .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) + .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) .Where(SQLite::RowIDName).Equals(pinId); builder.Execute(connection); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 523c5a2938..2975bcd486 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -117,10 +117,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 : std::nullopt; builder.Update(s_PinTable_Table_Name).Set() - .Column(s_PinTable_PackageId_Column).Equals(pinKey.PackageId) - .Column(s_PinTable_SourceId_Column).Equals(pinKey.SourceId) - .Column(s_PinTable_Type_Column).Equals(pin.GetType()) - .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString()) + .Column(s_PinTable_PackageId_Column).AssignValue(pinKey.PackageId) + .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) + .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) + .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) .Column(s_PinTable_DateAdded_Column).AssignValue(epochOpt) .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp index 7557493f97..bcded14e6e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp @@ -82,10 +82,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0 { SQLite::Builder::StatementBuilder builder; builder.Update(s_PortableTable_Table_Name).Set() - .Column(s_PortableTable_FilePath_Column).Equals(file.GetFilePath().u8string()) - .Column(s_PortableTable_FileType_Column).Equals(file.FileType) - .Column(s_PortableTable_SHA256_Column).Equals(file.SHA256) - .Column(s_PortableTable_SymlinkTarget_Column).Equals(file.SymlinkTarget) + .Column(s_PortableTable_FilePath_Column).AssignValue(file.GetFilePath().u8string()) + .Column(s_PortableTable_FileType_Column).AssignValue(file.FileType) + .Column(s_PortableTable_SHA256_Column).AssignValue(file.SHA256) + .Column(s_PortableTable_SymlinkTarget_Column).AssignValue(file.SymlinkTarget) .Where(SQLite::RowIDName).Equals(id); builder.Execute(connection); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index c013b7ce9c..0452f4f5d0 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -296,6 +296,16 @@ namespace AppInstaller::SQLite::Builder return *this; } + // Assigns a non-nullable value using "= ?" binding semantics. Prefer this over Equals() + // in UPDATE SET clauses to clearly signal assignment intent and prevent future breakage + // if the type is later made optional. + template + StatementBuilder& AssignValue(const ValueType& value) + { + AddBindFunctor(AppendOpAndBinder(Op::Equals), value); + return *this; + } + // The optional index value can be used to specify the parameter index. StatementBuilder& Equals(details::unbound_t, std::optional index = {}); StatementBuilder& Equals(std::nullptr_t); diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp index e2003aa68f..669826fa86 100644 --- a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp @@ -169,12 +169,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: StatementBuilder builder; builder.Update(s_SetInfoTable_Table).Set(). - Column(s_SetInfoTable_Column_Name).Equals(ConvertToUTF8(configurationSet.Name())). - Column(s_SetInfoTable_Column_Origin).Equals(ConvertToUTF8(configurationSet.Origin())). - Column(s_SetInfoTable_Column_Path).Equals(ConvertToUTF8(configurationSet.Path())). - Column(s_SetInfoTable_Column_SchemaVersion).Equals(ConvertToUTF8(schemaVersion)). - Column(s_SetInfoTable_Column_Metadata).Equals(serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment())). - Column(s_SetInfoTable_Column_Variables).Equals(serializer->SerializeValueSet(configurationSet.Variables())). + Column(s_SetInfoTable_Column_Name).AssignValue(ConvertToUTF8(configurationSet.Name())). + Column(s_SetInfoTable_Column_Origin).AssignValue(ConvertToUTF8(configurationSet.Origin())). + Column(s_SetInfoTable_Column_Path).AssignValue(ConvertToUTF8(configurationSet.Path())). + Column(s_SetInfoTable_Column_SchemaVersion).AssignValue(ConvertToUTF8(schemaVersion)). + Column(s_SetInfoTable_Column_Metadata).AssignValue(serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment())). + Column(s_SetInfoTable_Column_Variables).AssignValue(serializer->SerializeValueSet(configurationSet.Variables())). Where(RowIDName).Equals(target); builder.Execute(m_connection); diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp index adcd41ccc4..7920aa8fc5 100644 --- a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp @@ -95,7 +95,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: void QueueTable::SetActiveQueueItem(const std::string& objectName) { StatementBuilder builder; - builder.Update(s_QueueTable_Table).Set().Column(s_QueueTable_Column_Active).Equals(true).Where(s_QueueTable_Column_ObjectName).Equals(objectName); + builder.Update(s_QueueTable_Table).Set().Column(s_QueueTable_Column_Active).AssignValue(true).Where(s_QueueTable_Column_ObjectName).Equals(objectName); builder.Execute(m_connection); } From 615ddca641ea0725f72c9d8d566958dca35b32b7 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:45:11 -0500 Subject: [PATCH 38/46] Ensure pin writes always record the last updated time --- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 2975bcd486..9786363a38 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -81,11 +81,6 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); - const auto& dateAdded = pin.GetDateAdded(); - std::optional epochOpt = dateAdded.has_value() - ? std::optional{ Utility::ConvertSystemClockToUnixEpoch(*dateAdded) } - : std::nullopt; - builder.InsertInto(s_PinTable_Table_Name) .Columns({ s_PinTable_PackageId_Column, @@ -99,7 +94,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 pinKey.SourceId, pin.GetType(), pin.GetGatedVersion().ToString(), - epochOpt, + Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()), pin.GetNote()); builder.Execute(connection); @@ -111,17 +106,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); - const auto& dateAdded = pin.GetDateAdded(); - std::optional epochOpt = dateAdded.has_value() - ? std::optional{ Utility::ConvertSystemClockToUnixEpoch(*dateAdded) } - : std::nullopt; - builder.Update(s_PinTable_Table_Name).Set() .Column(s_PinTable_PackageId_Column).AssignValue(pinKey.PackageId) .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) - .Column(s_PinTable_DateAdded_Column).AssignValue(epochOpt) + .Column(s_PinTable_DateAdded_Column).AssignValue(Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now())) .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()); builder.Where(SQLite::RowIDName).Equals(pinId); From 1f8932dc3caa4022d7da35f39fcce27072fc9cfa Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 09:48:15 -0500 Subject: [PATCH 39/46] Use original symbol for creation --- src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp | 8 ++++---- src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp index e9081b3c6a..bb03125e7c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp @@ -185,7 +185,7 @@ namespace AppInstaller::Repository::Microsoft return m_interface->ResetAllPins(m_dbconn, sourceId); } - std::unique_ptr PinningIndex::CreateIPinningIndexForVersion(const SQLite::Version& version) + std::unique_ptr PinningIndex::CreateIPinningIndex(const SQLite::Version& version) { if (version == SQLite::Version{ 1, 0 }) { @@ -208,12 +208,12 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Opened Pinning Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); // Create the correct interface for the stored schema version. - m_interface = CreateIPinningIndexForVersion(m_version); + m_interface = CreateIPinningIndex(m_version); if (disposition == SQLiteStorageBase::OpenDisposition::ReadWrite) { // For writable opens, create a latest interface and migrate if the stored version is older. - auto latestInterface = CreateIPinningIndexForVersion(SQLite::Version::Latest()); + auto latestInterface = CreateIPinningIndex(SQLite::Version::Latest()); if (m_version != latestInterface->GetVersion()) { @@ -243,7 +243,7 @@ namespace AppInstaller::Repository::Microsoft PinningIndex::PinningIndex(const std::string& target, SQLite::Version version) : SQLiteStorageBase(target, version) { - m_interface = CreateIPinningIndexForVersion(version); + m_interface = CreateIPinningIndex(version); m_version = m_interface->GetVersion(); } } \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h index c015dd8366..62a92a17e0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h @@ -68,7 +68,7 @@ namespace AppInstaller::Repository::Microsoft PinningIndex(const std::string& target, SQLite::Version version); // Creates an IPinningIndex interface object for a specific version. - static std::unique_ptr CreateIPinningIndexForVersion(const SQLite::Version& version); + static std::unique_ptr CreateIPinningIndex(const SQLite::Version& version); std::unique_ptr m_interface; }; From df5509625b040f9aef582b28201dd18f88ce338a Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:09:01 -0500 Subject: [PATCH 40/46] Add missing System namepace --- .../Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs index 7bc288acbf..27a11477aa 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSPackagePin.cs @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { + using System; using Microsoft.Management.Deployment; /// From 6d085dd4da785c973fd9422c73b217535d7b7b50 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:12:29 -0500 Subject: [PATCH 41/46] Restore conditional setting of date --- .../Microsoft/Schema/Pinning_1_1/PinTable.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp index 9786363a38..ac45dec42b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp @@ -81,6 +81,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); + int64_t epochToStore = pin.GetDateAdded().has_value() + ? Utility::ConvertSystemClockToUnixEpoch(*pin.GetDateAdded()) + : Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()); + builder.InsertInto(s_PinTable_Table_Name) .Columns({ s_PinTable_PackageId_Column, @@ -94,7 +98,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 pinKey.SourceId, pin.GetType(), pin.GetGatedVersion().ToString(), - Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()), + epochToStore, pin.GetNote()); builder.Execute(connection); @@ -106,12 +110,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 SQLite::Builder::StatementBuilder builder; const auto& pinKey = pin.GetKey(); + int64_t epochToStore = pin.GetDateAdded().has_value() + ? Utility::ConvertSystemClockToUnixEpoch(*pin.GetDateAdded()) + : Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now()); + builder.Update(s_PinTable_Table_Name).Set() .Column(s_PinTable_PackageId_Column).AssignValue(pinKey.PackageId) .Column(s_PinTable_SourceId_Column).AssignValue(pinKey.SourceId) .Column(s_PinTable_Type_Column).AssignValue(pin.GetType()) .Column(s_PinTable_Version_Column).AssignValue(pin.GetGatedVersion().ToString()) - .Column(s_PinTable_DateAdded_Column).AssignValue(Utility::ConvertSystemClockToUnixEpoch(std::chrono::system_clock::now())) + .Column(s_PinTable_DateAdded_Column).AssignValue(epochToStore) .Column(s_PinTable_Note_Column).AssignValue(pin.GetNote()); builder.Where(SQLite::RowIDName).Equals(pinId); From ea9f0d8e29334aff3aeb7c59b36b2e831b266009 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:15:16 -0500 Subject: [PATCH 42/46] Ensure ShouldProcess is always respected --- .../Cmdlets/ResetPinCmdlet.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs index b1e1360d2e..47543b6d9f 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetPinCmdlet.cs @@ -41,11 +41,22 @@ public sealed class ResetPinCmdlet : PSCmdlet protected override void ProcessRecord() { string target = string.IsNullOrEmpty(this.Source) ? "All sources" : this.Source; - if (this.Force || this.ShouldProcess(target)) + if (!this.ShouldProcess(target)) { - this.command = new ResetPinCommand(this); - this.command.Reset(this.Source); + return; } + + if (!this.Force && !this.ShouldContinue( + string.IsNullOrEmpty(this.Source) + ? "This will reset all pins for all sources." + : $"This will reset all pins for source '{this.Source}'.", + "Confirm")) + { + return; + } + + this.command = new ResetPinCommand(this); + this.command.Reset(this.Source); } /// From d207c7712e53aed1a3300ec7ce5d453b32a2a2af Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:16:38 -0500 Subject: [PATCH 43/46] Fix minor issue in copilot instructions --- .github/copilot-instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ea607123ce..f5ce805ac7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -184,7 +184,7 @@ builder.Execute(connection); // UPDATE StatementBuilder builder; builder.Update(s_MyTable).Set() - .Column(s_Col_Value).Equals(newValue) + .Column(s_Col_Value).AssignValue(newValue) .Where(s_Col_Id).Equals(rowId); builder.Execute(connection); @@ -217,7 +217,7 @@ std::optional maybeEpoch = ...; std::optional maybeNote = ...; builder.Update(s_MyTable).Set() - .Column(s_Col_Name).Equals(requiredName) // non-optional: fine in SET + .Column(s_Col_Name).AssignValue(requiredName) // non-optional: AssignValue in SET .Column(s_Col_Epoch).AssignValue(maybeEpoch) // nullable: must use AssignValue in SET .Column(s_Col_Note).AssignValue(maybeNote) // nullable: must use AssignValue in SET .Where(s_Col_Id).Equals(rowId); // filter: Equals is correct here From 349a0a13e1f7f244e098adb6e1f2d49253dec4e1 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:19:34 -0500 Subject: [PATCH 44/46] Validate provided pin types --- src/Microsoft.Management.Deployment/PackageManager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 44fdeab48f..50d83853a3 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -1586,6 +1586,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation { switch (options.PinType()) { + case winrt::Microsoft::Management::Deployment::PackagePinType::Pinning: + return ::AppInstaller::Pinning::Pin::CreatePinningPin(pinKey); case winrt::Microsoft::Management::Deployment::PackagePinType::Blocking: return ::AppInstaller::Pinning::Pin::CreateBlockingPin(pinKey); case winrt::Microsoft::Management::Deployment::PackagePinType::Gating: @@ -1593,7 +1595,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation pinKey, ::AppInstaller::Utility::GatedVersion{ winrt::to_string(options.GatedVersion()) }); default: - return ::AppInstaller::Pinning::Pin::CreatePinningPin(pinKey); + // Unknown, PinnedByManifest, and any future unrecognized values are not valid + // user-settable pin types. + THROW_HR(E_INVALIDARG); } } } From 840e5ae2089aeba84b3798dd90dff684360dea8c Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:27:56 -0500 Subject: [PATCH 45/46] Cache all pins and only fall back to get if set is empty --- .../Commands/FinderPackageCommand.cs | 16 +++++++++++++++- .../PSObjects/PSInstalledCatalogPackage.cs | 13 +++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs index fdfa06ce3e..4d5f1e06d1 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/FinderPackageCommand.cs @@ -6,6 +6,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands { + using System.Collections.Generic; + using System.Linq; using System.Management.Automation; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Commands.Common; @@ -82,9 +84,21 @@ public void Get(string psPackageFieldMatchOption) () => this.FindPackages( CompositeSearchBehavior.LocalCatalogs, PSEnumHelpers.ToPackageFieldMatchOption(psPackageFieldMatchOption))); + + if (results.Count == 0) + { + return; + } + + // Fetch all pins in a single COM call and build a lookup set to avoid + // one GetPins() roundtrip per package when IsPinned is accessed during output. + IReadOnlyList allPins = this.Execute( + () => PackageManagerWrapper.Instance.GetAllPins()); + var pinnedPackageIds = allPins.Select(p => p.PackageId).ToHashSet(); + for (var i = 0; i < results.Count; i++) { - this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage)); + this.Write(StreamType.Object, new PSInstalledCatalogPackage(results[i].CatalogPackage, pinnedPackageIds)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs index f8ade5e2bf..ef1d40a197 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs @@ -7,6 +7,7 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects { using System; + using System.Collections.Generic; using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Engine.Helpers; @@ -15,15 +16,21 @@ namespace Microsoft.WinGet.Client.Engine.PSObjects /// public sealed class PSInstalledCatalogPackage : PSCatalogPackage { + private readonly IReadOnlySet? pinnedPackageIds; private bool? isPinned; /// /// Initializes a new instance of the class. /// /// The catalog package COM object. - internal PSInstalledCatalogPackage(CatalogPackage catalogPackage) + /// + /// Optional pre-fetched set of pinned package IDs. When provided, + /// uses this set instead of issuing a per-package COM call. + /// + internal PSInstalledCatalogPackage(CatalogPackage catalogPackage, IReadOnlySet? pinnedPackageIds = null) : base(catalogPackage) { + this.pinnedPackageIds = pinnedPackageIds; } /// @@ -43,7 +50,9 @@ public bool IsPinned { if (!this.isPinned.HasValue) { - this.isPinned = PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; + this.isPinned = this.pinnedPackageIds != null + ? this.pinnedPackageIds.Contains(this.CatalogPackageCOM.Id) + : PackageManagerWrapper.Instance.GetPins(this.CatalogPackageCOM).Count > 0; } return this.isPinned.Value; From 65f8373041ff4b944ede269e30b474837b9630b6 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Fri, 1 May 2026 10:48:20 -0500 Subject: [PATCH 46/46] Change file name to remove override --- src/AppInstallerCLITests/PinningIndex.cpp | 2 +- .../AppInstallerRepositoryCore.vcxproj | 6 ++---- .../AppInstallerRepositoryCore.vcxproj.filters | 4 ++-- .../Schema/Pinning_1_1/{PinTable.cpp => PinTable_1_1.cpp} | 2 +- .../Schema/Pinning_1_1/{PinTable.h => PinTable_1_1.h} | 0 .../Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) rename src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/{PinTable.cpp => PinTable_1_1.cpp} (99%) rename src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/{PinTable.h => PinTable_1_1.h} (100%) diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp index ef6a01c64b..51f2738ef2 100644 --- a/src/AppInstallerCLITests/PinningIndex.cpp +++ b/src/AppInstallerCLITests/PinningIndex.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include using namespace std::string_literals; diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 9affa80d82..f18955f27e 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -339,7 +339,7 @@ - + @@ -452,9 +452,7 @@ - - $(IntDir)Schema\Pinning_1_1\ - + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index b5381284b1..897c8ab613 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -381,7 +381,7 @@ Microsoft\Schema\Pinning_1_1 - + Microsoft\Schema\Pinning_1_1 @@ -704,7 +704,7 @@ Microsoft\Schema\Pinning_1_0 - + Microsoft\Schema\Pinning_1_1 diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp similarity index 99% rename from src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp rename to src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp index ac45dec42b..77ba57d6fd 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.cpp @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "PinTable.h" +#include "PinTable_1_1.h" #include #include #include "Microsoft/Schema/IPinningIndex.h" diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.h similarity index 100% rename from src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable.h rename to src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinTable_1_1.h diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp index 48785c69cf..76fe9e209d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_1/PinningIndexInterface_1_1.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "Microsoft/Schema/Pinning_1_1/PinningIndexInterface.h" -#include "Microsoft/Schema/Pinning_1_1/PinTable.h" +#include "Microsoft/Schema/Pinning_1_1/PinTable_1_1.h" namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_1 {