diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt
index 2b75ac9a91..7231a00be5 100644
--- a/.github/actions/spelling/allow.txt
+++ b/.github/actions/spelling/allow.txt
@@ -4,6 +4,7 @@ ACTIONDATA
ACTIONSTART
addfile
addmanifest
+addpin
addportablefile
addstore
admins
@@ -94,6 +95,7 @@ cpprestsdk
cppwinrt
CPRWL
createnew
+createpintable
createportabletable
createtables
cref
@@ -253,6 +255,7 @@ INVALIDARG
INVALIDSID
iomanip
iostream
+IPinning
IPortable
ipmo
ISAPPROVEDFOROUTPUT
@@ -389,6 +392,7 @@ pfp
PGP
php
PII
+pinningindex
pipssource
PKCS
placeholders
@@ -441,10 +445,12 @@ regex
regexp
removefile
removemanifest
+removepin
removeportablefile
repolibtest
requeue
rescap
+resetpins
resheader
resmimetype
RESOLVESOURCE
@@ -623,6 +629,7 @@ Unregisters
untimes
updatefile
updatemanifest
+updatepin
updateportablefile
UPLEVEL
upvote
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index effaa5d8b7..e43c58fb68 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -341,6 +341,7 @@
+
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
index 53e912fc82..f2983e3f44 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
@@ -349,8 +349,11 @@
Source Files
+
+ Workflows
+
- Source Files
+ Commands
diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp
index 207e636133..81a94755d1 100644
--- a/src/AppInstallerCLICore/Commands/PinCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/PinCommand.cpp
@@ -76,15 +76,26 @@ namespace AppInstaller::CLI
void PinAddCommand::Complete(Execution::Context& context, Args::Type valueType) const
{
+ context <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed);
+
switch (valueType)
{
- case Args::Type::Query:
- case Args::Type::Id:
- case Args::Type::Name:
- case Args::Type::Moniker:
- case Args::Type::Source:
+ case Execution::Args::Type::Query:
+ context <<
+ Workflow::RequireCompletionWordNonEmpty <<
+ Workflow::SearchSourceForManyCompletion <<
+ Workflow::CompleteWithMatchedField;
+ break;
+ case Execution::Args::Type::Id:
+ case Execution::Args::Type::Name:
+ case Execution::Args::Type::Moniker:
+ case Execution::Args::Type::Source:
+ case Execution::Args::Type::Tag:
+ case Execution::Args::Type::Command:
context <<
- Workflow::CompleteWithSingleSemanticsForValue(valueType);
+ Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType);
break;
}
}
@@ -105,8 +116,17 @@ namespace AppInstaller::CLI
void PinAddCommand::ExecuteInternal(Execution::Context& context) const
{
- // TODO
- Command::ExecuteInternal(context);
+ context <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) <<
+ Workflow::SearchSourceForSingle <<
+ Workflow::HandleSearchResultFailures <<
+ Workflow::EnsureOneMatchFromSearchResult(false) <<
+ Workflow::GetInstalledPackageVersion <<
+ Workflow::ReportPackageIdentity <<
+ Workflow::OpenPinningIndex() <<
+ Workflow::SearchPin <<
+ Workflow::AddPin;
}
std::vector PinRemoveCommand::GetArguments() const
@@ -137,15 +157,26 @@ namespace AppInstaller::CLI
void PinRemoveCommand::Complete(Execution::Context& context, Args::Type valueType) const
{
+ context <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed);
+
switch (valueType)
{
- case Args::Type::Query:
- case Args::Type::Id:
- case Args::Type::Name:
- case Args::Type::Moniker:
- case Args::Type::Source:
+ case Execution::Args::Type::Query:
context <<
- Workflow::CompleteWithSingleSemanticsForValue(valueType);
+ Workflow::RequireCompletionWordNonEmpty <<
+ Workflow::SearchSourceForManyCompletion <<
+ Workflow::CompleteWithMatchedField;
+ break;
+ case Execution::Args::Type::Id:
+ case Execution::Args::Type::Name:
+ case Execution::Args::Type::Moniker:
+ case Execution::Args::Type::Source:
+ case Execution::Args::Type::Tag:
+ case Execution::Args::Type::Command:
+ context <<
+ Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType);
break;
}
}
@@ -157,8 +188,17 @@ namespace AppInstaller::CLI
void PinRemoveCommand::ExecuteInternal(Execution::Context& context) const
{
- // TODO
- Command::ExecuteInternal(context);
+ context <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) <<
+ Workflow::SearchSourceForSingle <<
+ Workflow::HandleSearchResultFailures <<
+ Workflow::EnsureOneMatchFromSearchResult(false) <<
+ Workflow::GetInstalledPackageVersion <<
+ Workflow::ReportPackageIdentity <<
+ Workflow::OpenPinningIndex() <<
+ Workflow::SearchPin <<
+ Workflow::RemovePin;
}
std::vector PinListCommand::GetArguments() const
@@ -189,15 +229,26 @@ namespace AppInstaller::CLI
void PinListCommand::Complete(Execution::Context& context, Args::Type valueType) const
{
+ context <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed);
+
switch (valueType)
{
- case Args::Type::Query:
- case Args::Type::Id:
- case Args::Type::Name:
- case Args::Type::Moniker:
- case Args::Type::Source:
+ case Execution::Args::Type::Query:
+ context <<
+ Workflow::RequireCompletionWordNonEmpty <<
+ Workflow::SearchSourceForManyCompletion <<
+ Workflow::CompleteWithMatchedField;
+ break;
+ case Execution::Args::Type::Id:
+ case Execution::Args::Type::Name:
+ case Execution::Args::Type::Moniker:
+ case Execution::Args::Type::Source:
+ case Execution::Args::Type::Tag:
+ case Execution::Args::Type::Command:
context <<
- Workflow::CompleteWithSingleSemanticsForValue(valueType);
+ Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType);
break;
}
}
@@ -209,14 +260,20 @@ namespace AppInstaller::CLI
void PinListCommand::ExecuteInternal(Execution::Context& context) const
{
- // TODO
- Command::ExecuteInternal(context);
+ context <<
+ Workflow::OpenPinningIndex(/* readOnly */ true) <<
+ Workflow::GetAllPins <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) <<
+ Workflow::CrossReferencePinsWithSource <<
+ Workflow::ReportPins;
}
std::vector PinResetCommand::GetArguments() const
{
return {
Argument::ForType(Args::Type::Force),
+ Argument::ForType(Args::Type::Source),
};
}
@@ -237,7 +294,25 @@ namespace AppInstaller::CLI
void PinResetCommand::ExecuteInternal(Execution::Context& context) const
{
- // TODO
- Command::ExecuteInternal(context);
+
+ if (context.Args.Contains(Execution::Args::Type::Force))
+ {
+ context <<
+ Workflow::OpenPinningIndex() <<
+ Workflow::ResetAllPins;
+ }
+ else
+ {
+ AICLI_LOG(CLI, Info, << "--force argument is not present");
+ context.Reporter.Info() << Resource::String::PinResetUseForceArg << std::endl;
+
+ context <<
+ Workflow::OpenPinningIndex(/* readOnly */ true) <<
+ Workflow::GetAllPins <<
+ Workflow::OpenSource() <<
+ Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed) <<
+ Workflow::CrossReferencePinsWithSource <<
+ Workflow::ReportPins;
+ }
}
}
diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h
index 9ab78e1873..6011e4b397 100644
--- a/src/AppInstallerCLICore/ExecutionContextData.h
+++ b/src/AppInstallerCLICore/ExecutionContextData.h
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include "CompletionData.h"
#include "PackageCollection.h"
#include "PortableInstaller.h"
@@ -16,6 +17,10 @@
#include
#include
+namespace AppInstaller::Repository::Microsoft
+{
+ struct PinningIndex;
+}
namespace AppInstaller::CLI::Execution
{
@@ -55,6 +60,8 @@ namespace AppInstaller::CLI::Execution
AllowedArchitectures,
AllowUnknownScope,
PortableInstaller,
+ PinningIndex,
+ Pins,
Max
};
@@ -229,5 +236,17 @@ namespace AppInstaller::CLI::Execution
{
using value_t = CLI::Portable::PortableInstaller;
};
+
+ template <>
+ struct DataMapping
+ {
+ using value_t = std::shared_ptr;
+ };
+
+ template <>
+ struct DataMapping
+ {
+ using value_t = std::vector;
+ };
}
}
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index 66204f4735..432c77c270 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -244,14 +244,25 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(PinAddBlockingArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinAddCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinAdded);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinAlreadyExists);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinCannotOpenIndex);
WINGET_DEFINE_RESOURCE_STRINGID(PinCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg);
WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist);
WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinResetCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg);
+ WINGET_DEFINE_RESOURCE_STRINGID(PinType);
WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled);
WINGET_DEFINE_RESOURCE_STRINGID(PoliciesEnabled);
WINGET_DEFINE_RESOURCE_STRINGID(PoliciesPolicy);
diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.cpp b/src/AppInstallerCLICore/Workflows/PinFlow.cpp
new file mode 100644
index 0000000000..55f2294348
--- /dev/null
+++ b/src/AppInstallerCLICore/Workflows/PinFlow.cpp
@@ -0,0 +1,231 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "Resources.h"
+#include "PinFlow.h"
+#include "TableOutput.h"
+#include "Microsoft/PinningIndex.h"
+#include "Microsoft/SQLiteStorageBase.h"
+#include "winget/RepositorySearch.h"
+
+using namespace AppInstaller::Repository;
+using namespace AppInstaller::Repository::Microsoft;
+
+namespace AppInstaller::CLI::Workflow
+{
+ namespace
+ {
+ // Creates a Pin appropriate for the context based on the arguments provided
+ Pinning::Pin CreatePin(Execution::Context& context, std::string_view packageId, std::string_view sourceId)
+ {
+ if (context.Args.Contains(Execution::Args::Type::GatedVersion))
+ {
+ return Pinning::Pin::CreateGatingPin({ packageId, sourceId }, context.Args.GetArg(Execution::Args::Type::GatedVersion));
+ }
+ else if (context.Args.Contains(Execution::Args::Type::BlockingPin))
+ {
+ return Pinning::Pin::CreateBlockingPin({ packageId, sourceId });
+ }
+ else
+ {
+ return Pinning::Pin::CreatePinningPin({ packageId, sourceId });
+ }
+ }
+ }
+
+ void OpenPinningIndex::operator()(Execution::Context& context) const
+ {
+ auto openDisposition = m_readOnly ? SQLiteStorageBase::OpenDisposition::Read : SQLiteStorageBase::OpenDisposition::ReadWrite;
+ auto pinningIndex = PinningIndex::OpenOrCreateDefault(openDisposition);
+ if (!pinningIndex)
+ {
+ AICLI_LOG(CLI, Error, << "Unable to open pinning index.");
+ context.Reporter.Error() << Resource::String::PinCannotOpenIndex << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX);
+ }
+
+ context.Add(std::move(pinningIndex));
+ }
+
+ void GetAllPins(Execution::Context& context)
+ {
+ AICLI_LOG(CLI, Info, << "Getting all existing pins");
+ context.Add(context.Get()->GetAllPins());
+ }
+
+ void SearchPin(Execution::Context& context)
+ {
+ auto package = context.Get();
+ std::vector pins;
+
+ // TODO: We should support querying the multiple sources for a package, instead of just one
+ auto availableVersion = package->GetLatestAvailableVersion();
+
+ auto pinningIndex = context.Get();
+ auto pin = pinningIndex->GetPin({
+ availableVersion->GetProperty(PackageVersionProperty::Id).get(),
+ availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() });
+ if (pin)
+ {
+ pins.emplace_back(std::move(pin.value()));
+ }
+
+ context.Add(std::move(pins));
+ }
+
+ void AddPin(Execution::Context& context)
+ {
+ auto package = context.Get();
+ auto installedVersion = context.Get();
+
+ std::vector pins;
+
+ // TODO: We should support querying the multiple sources for a package, instead of just one
+ auto availableVersion = package->GetLatestAvailableVersion();
+
+ Pinning::PinKey pinKey{
+ availableVersion->GetProperty(PackageVersionProperty::Id).get(),
+ availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() };
+ auto pin = CreatePin(context, pinKey.PackageId, pinKey.SourceId);
+ AICLI_LOG(CLI, Info, << "Adding pin with type " << ToString(pin.GetType()) << " for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "]");
+
+
+ auto pinningIndex = context.Get();
+ auto existingPin = pinningIndex->GetPin(pinKey);
+
+ if (existingPin)
+ {
+ // Pin already exists.
+ // If it is the same, we do nothing. If it is different, check for the --force arg
+ if (pin == existingPin)
+ {
+ AICLI_LOG(CLI, Info, << "Pin already exists");
+ context.Reporter.Info() << Resource::String::PinAlreadyExists << std::endl;
+ return;
+ }
+
+ AICLI_LOG(CLI, Info, << "Another pin already exists for the package");
+ if (context.Args.Contains(Execution::Args::Type::Force))
+ {
+ AICLI_LOG(CLI, Info, << "Overwriting pin due to --force argument");
+ context.Reporter.Warn() << Resource::String::PinExistsOverwriting << std::endl;
+ pinningIndex->UpdatePin(pin);
+ }
+ else
+ {
+ context.Reporter.Error() << Resource::String::PinExistsUseForceArg << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS);
+ }
+ }
+ else
+ {
+ pinningIndex->AddPin(pin);
+ AICLI_LOG(CLI, Info, << "Finished adding pin");
+ context.Reporter.Info() << Resource::String::PinAdded << std::endl;
+ }
+ }
+
+ void RemovePin(Execution::Context& context)
+ {
+ auto package = context.Get();
+ std::vector pins;
+
+ // TODO: We should support querying the multiple sources for a package, instead of just one
+ auto availableVersion = package->GetLatestAvailableVersion();
+
+ auto pinningIndex = context.Get();
+ Pinning::PinKey pinKey{
+ availableVersion->GetProperty(PackageVersionProperty::Id).get(),
+ availableVersion->GetProperty(PackageVersionProperty::SourceIdentifier).get() };
+ AICLI_LOG(CLI, Info, << "Removing pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]");
+ if (!pinningIndex->GetPin(pinKey))
+ {
+ AICLI_LOG(CLI, Warning, << "Pin does not exist");
+ context.Reporter.Warn() << Resource::String::PinDoesNotExist(pinKey.PackageId) << std::endl;
+ AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST);
+ }
+
+ pinningIndex->RemovePin(pinKey);
+ }
+
+ void ReportPins(Execution::Context& context)
+ {
+ const auto& pins = context.Get();
+ if (pins.empty())
+ {
+ context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl;
+ return;
+ }
+
+ // TODO: Use package and source names
+ Execution::TableOutput<4> table(context.Reporter,
+ {
+ Resource::String::SearchId,
+ Resource::String::SearchSource,
+ Resource::String::SearchVersion,
+ Resource::String::PinType,
+ });
+
+ for (const auto& pin : pins)
+ {
+ // TODO: Avoid these conversions to string
+ table.OutputLine({
+ pin.GetPackageId(),
+ std::string{ pin.GetSourceId() },
+ pin.GetGatedVersion().ToString(),
+ std::string{ ToString(pin.GetType()) },
+ });
+ }
+
+ table.Complete();
+ }
+
+ void ResetAllPins(Execution::Context& context)
+ {
+ AICLI_LOG(CLI, Info, << "Resetting all pins");
+ context.Reporter.Info() << Resource::String::PinResettingAll << std::endl;
+
+ std::string sourceId;
+ if (context.Args.Contains(Execution::Args::Type::Source))
+ {
+ auto sourceName = context.Args.GetArg(Execution::Args::Type::Source);
+ auto sources = Source::GetCurrentSources();
+ for (const auto& source : sources)
+ {
+ if (Utility::CaseInsensitiveEquals(source.Name, sourceName))
+ {
+ sourceId = source.Identifier;
+ break;
+ }
+ }
+ }
+
+ if (context.Get()->ResetAllPins(sourceId))
+ {
+ context.Reporter.Info() << Resource::String::PinResetSuccessful << std::endl;
+ }
+ else
+ {
+ context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl;
+ }
+ }
+
+ void CrossReferencePinsWithSource(Execution::Context& context)
+ {
+ const auto& pins = context.Get();
+ const auto& source = context.Get();
+
+ std::vector matchingPins;
+ std::copy_if(pins.begin(), pins.end(), std::back_inserter(matchingPins), [&](Pinning::Pin pin) {
+ // TODO: Filter to source
+ SearchRequest searchRequest;
+ searchRequest.Filters.emplace_back(PackageMatchField::Id, MatchType::CaseInsensitive, pin.GetPackageId());
+ auto searchResult = source.Search(searchRequest);
+
+ return !searchResult.Matches.empty();
+ });
+
+ context.Add(std::move(matchingPins));
+ }
+
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLICore/Workflows/PinFlow.h b/src/AppInstallerCLICore/Workflows/PinFlow.h
index 504ed5782e..fd4a5fce2c 100644
--- a/src/AppInstallerCLICore/Workflows/PinFlow.h
+++ b/src/AppInstallerCLICore/Workflows/PinFlow.h
@@ -5,4 +5,61 @@
namespace AppInstaller::CLI::Workflow
{
+ // Opens the pinning index for use in future operations.
+ // Required Args: None
+ // Inputs: None
+ // Outputs: PinningIndex
+ struct OpenPinningIndex : public WorkflowTask
+ {
+ OpenPinningIndex(bool readOnly = false) : WorkflowTask("OpenPinningIndex"), m_readOnly(readOnly) {}
+
+ void operator()(Execution::Context& context) const override;
+
+ private:
+ bool m_readOnly;
+ };
+
+ // Gets all the pins from the index.
+ // Required Args: None
+ // Inputs: PinningIndex
+ // Outputs: Pins
+ void GetAllPins(Execution::Context& context);
+
+ // Searches for all the pins associated with a package.
+ // There may be several if a package is available from multiple sources.
+ // Required Args: None
+ // Inputs: PinningIndex, Package
+ // Outputs Pins
+ void SearchPin(Execution::Context& context);
+
+ // Adds a pin for the current package.
+ // A separate pin will be added for each source.
+ // Required Args: None
+ // Inputs: PinningIndex, Package
+ // Outputs: None
+ void AddPin(Execution::Context& context);
+
+ // Removes all the pins associated with a package.
+ // Required Args: None
+ // Inputs: PinningIndex, Package, InstalledPackageVersion
+ // Outputs: None
+ void RemovePin(Execution::Context& context);
+
+ // Report the pins in a table.
+ // Required Args: None
+ // Inputs: Pins
+ // Outputs: None
+ void ReportPins(Execution::Context& context);
+
+ // Resets all the existing pins.
+ // Required Args: None
+ // Inputs: None
+ // Outputs: None
+ void ResetAllPins(Execution::Context& context);
+
+ // Updates the list of pins to include only those matching the current open source
+ // Required Args: None
+ // Inputs: Pins, Source
+ // Outputs: None
+ void CrossReferencePinsWithSource(Execution::Context& context);
}
diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index 23bc85bf23..182670813d 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -1605,4 +1605,44 @@ Please specify one of them using the --source option to proceed.
Select installed package scope filter (user or machine)
This argument allows the user to select installed packages for just the user or for the entire machine.
-
+
+ Pin added successfully
+
+
+ The pin already exists
+
+
+ A pin already exists for this package. Overwriting due to the --force argument.
+ {Locked="--force"}
+
+
+ A pin already exists for this package. Use the --force argument to overwrite it.
+ {Locked="--force"}
+
+
+ Resetting all current pins.
+
+
+ Use the --force argument to reset all pins. The following pins would be removed:
+ {Locked="--force"}
+
+
+ Pin type
+
+
+ There is no pin for package {0}
+ {Locked="{0}"} {0} is a placeholder that will be replaced by a package name. The message is shown when attempting to delete a pin for a package that is not pinned.
+
+
+ There are no pins configured.
+ Shown when listing or modifying existing pins if there are none.
+
+
+ Pins reset successfully
+ Shown after resetting (deleting) all the pins
+
+
+ Unable to open pin database.
+ Error message for when we cannot open the database containing package pins.
+
+
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index ca1a736e72..f6c35bb3a9 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -219,6 +219,8 @@
+
+
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index f98d82be41..21072d3f32 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -275,6 +275,12 @@
Source Files\CLI
+
+ Source Files\Repository
+
+
+ Source Files\CLI
+
diff --git a/src/AppInstallerCLITests/PinFlow.cpp b/src/AppInstallerCLITests/PinFlow.cpp
new file mode 100644
index 0000000000..42989fc5da
--- /dev/null
+++ b/src/AppInstallerCLITests/PinFlow.cpp
@@ -0,0 +1,208 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "WorkflowCommon.h"
+#include
+#include
+#include
+#include
+
+using namespace TestCommon;
+using namespace AppInstaller::CLI;
+using namespace AppInstaller::CLI::Workflow;
+using namespace AppInstaller::Repository::Microsoft;
+using namespace AppInstaller::Pinning;
+
+void OverrideForOpenPinningIndex(TestContext& context, const std::filesystem::path& indexPath)
+{
+ context.Override({ "OpenPinningIndex", [=](TestContext& context)
+ {
+ auto pinningIndex = std::filesystem::exists(indexPath) ?
+ PinningIndex::Open(indexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite) :
+ PinningIndex::CreateNew(indexPath.u8string());
+ context.Add(std::make_shared(std::move(pinningIndex)));
+ } });
+}
+
+TEST_CASE("PinFlow_Add", "[PinFlow][workflow]")
+{
+ TempFile indexFile("pinningIndex", ".db");
+
+ std::ostringstream pinAddOutput;
+ TestContext addContext{ pinAddOutput, std::cin };
+ OverrideForOpenPinningIndex(addContext, indexFile.GetPath());
+ 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());
+
+ SECTION("Pin is saved")
+ {
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.size() == 1);
+ REQUIRE(pins[0].GetType() == PinType::Blocking);
+ REQUIRE(pins[0].GetPackageId() == "AppInstallerCliTest.TestExeInstaller");
+ REQUIRE(pins[0].GetSourceId() == "*TestSource");
+ REQUIRE(pins[0].GetGatedVersion().ToString() == "");
+
+ std::ostringstream pinListOutput;
+ TestContext listContext{ pinListOutput, std::cin };
+ OverrideForOpenPinningIndex(listContext, indexFile.GetPath());
+ OverrideForCompositeInstalledSource(listContext, CreateTestSource({ TSR::TestInstaller_Exe }));
+ listContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query);
+
+ PinListCommand pinList({});
+ pinList.Execute(listContext);
+
+ INFO(pinListOutput.str());
+ REQUIRE(pinListOutput.str().find("AppInstallerCliTest.TestExeInstaller"));
+ REQUIRE(pinListOutput.str().find("Blocking"));
+ }
+ SECTION("Remove pin")
+ {
+ std::ostringstream pinRemoveOutput;
+ TestContext removeContext{ pinRemoveOutput, std::cin };
+ OverrideForOpenPinningIndex(removeContext, indexFile.GetPath());
+ OverrideForCompositeInstalledSource(removeContext, CreateTestSource({ TSR::TestInstaller_Exe }));
+ removeContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query);
+
+ PinRemoveCommand pinRemove({});
+ pinRemove.Execute(removeContext);
+ INFO(pinRemoveOutput.str());
+
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.empty());
+ }
+ SECTION("Reset pins")
+ {
+ std::ostringstream pinResetOutput;
+ TestContext resetContext{ pinResetOutput, std::cin };
+ OverrideForOpenPinningIndex(resetContext, indexFile.GetPath());
+
+ SECTION("Without --force")
+ {
+ OverrideForCompositeInstalledSource(resetContext, CreateTestSource({ TSR::TestInstaller_Exe }));
+ PinResetCommand pinReset({});
+ pinReset.Execute(resetContext);
+ INFO(pinResetOutput.str());
+
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.size() == 1);
+ }
+ SECTION("With --force")
+ {
+ resetContext.Args.AddArg(Execution::Args::Type::Force);
+
+ PinResetCommand pinReset({});
+ pinReset.Execute(resetContext);
+ INFO(pinResetOutput.str());
+
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.empty());
+ }
+ }
+ SECTION("Update pin")
+ {
+ std::ostringstream pinUpdateOutput;
+ TestContext updateContext{ pinUpdateOutput, std::cin };
+ OverrideForOpenPinningIndex(updateContext, indexFile.GetPath());
+ OverrideForCompositeInstalledSource(updateContext, CreateTestSource({ TSR::TestInstaller_Exe }));
+ updateContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query);
+
+ SECTION("Without --force")
+ {
+ PinAddCommand pinUpdate({});
+ pinUpdate.Execute(updateContext);
+ INFO(pinUpdateOutput.str());
+ REQUIRE_TERMINATED_WITH(updateContext, APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS);
+
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.size() == 1);
+ REQUIRE(pins[0].GetType() == PinType::Blocking);
+ }
+ SECTION("With --force")
+ {
+ updateContext.Args.AddArg(Execution::Args::Type::Force);
+
+ PinAddCommand pinUpdate({});
+ pinUpdate.Execute(updateContext);
+ INFO(pinUpdateOutput.str());
+
+ auto index = PinningIndex::Open(indexFile.GetPath().u8string(), SQLiteStorageBase::OpenDisposition::Read);
+ auto pins = index.GetAllPins();
+ REQUIRE(pins.size() == 1);
+ REQUIRE(pins[0].GetType() == PinType::Pinning);
+ }
+ }
+}
+
+TEST_CASE("PinFlow_Add_NotFound", "[PinFlow][workflow]")
+{
+ std::ostringstream pinAddOutput;
+ TestContext addContext{ pinAddOutput, std::cin };
+ OverrideForCompositeInstalledSource(addContext, CreateTestSource({}));
+ addContext.Args.AddArg(Execution::Args::Type::Query, "This package doesn't exist"sv);
+
+ PinAddCommand pinAdd({});
+ pinAdd.Execute(addContext);
+ INFO(pinAddOutput.str());
+
+ REQUIRE_TERMINATED_WITH(addContext, APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND);
+}
+
+TEST_CASE("PinFlow_ListEmpty", "[PinFlow][workflow]")
+{
+ TempFile indexFile("pinningIndex", ".db");
+
+ std::ostringstream pinListOutput;
+ TestContext listContext{ pinListOutput, std::cin };
+ OverrideForOpenPinningIndex(listContext, indexFile.GetPath());
+ OverrideForCompositeInstalledSource(listContext, CreateTestSource({}));
+
+ PinListCommand pinList({});
+ pinList.Execute(listContext);
+ INFO(pinListOutput.str());
+
+ REQUIRE(pinListOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos);
+}
+
+TEST_CASE("PinFlow_RemoveNonExisting", "[PinFlow][workflow]")
+{
+ TempFile indexFile("pinningIndex", ".db");
+
+ std::ostringstream pinRemoveOutput;
+ TestContext removeContext{ pinRemoveOutput, std::cin };
+ OverrideForOpenPinningIndex(removeContext, indexFile.GetPath());
+ OverrideForCompositeInstalledSource(removeContext, CreateTestSource({ TSR::TestInstaller_Exe }));
+ removeContext.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe.Query);
+
+ PinRemoveCommand pinRemove({});
+ pinRemove.Execute(removeContext);
+ INFO(pinRemoveOutput.str());
+
+ REQUIRE_TERMINATED_WITH(removeContext, APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST);
+}
+
+TEST_CASE("PinFlow_ResetEmpty", "[PinFlow][workflow]")
+{
+ TempFile indexFile("pinningIndex", ".db");
+
+ std::ostringstream pinResetOutput;
+ TestContext resetContext{ pinResetOutput, std::cin };
+ OverrideForOpenPinningIndex(resetContext, indexFile.GetPath());
+ resetContext.Args.AddArg(Execution::Args::Type::Force);
+
+ PinResetCommand pinReset({});
+ pinReset.Execute(resetContext);
+ INFO(pinResetOutput.str());
+
+ REQUIRE(pinResetOutput.str().find(Resource::LocString(Resource::String::PinNoPinsExist)) != std::string::npos);
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/PinningIndex.cpp b/src/AppInstallerCLITests/PinningIndex.cpp
new file mode 100644
index 0000000000..b22e97c762
--- /dev/null
+++ b/src/AppInstallerCLITests/PinningIndex.cpp
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "TestCommon.h"
+#include
+#include
+#include
+#include
+
+using namespace std::string_literals;
+using namespace TestCommon;
+using namespace AppInstaller::Pinning;
+using namespace AppInstaller::Repository::Microsoft;
+using namespace AppInstaller::Repository::SQLite;
+using namespace AppInstaller::Repository::Microsoft::Schema;
+
+TEST_CASE("PinningIndexCreateLatestAndReopen", "[pinningIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Schema::Version versionCreated;
+
+ // Create the index
+ {
+ PinningIndex index = PinningIndex::CreateNew(tempFile, Schema::Version::Latest());
+ versionCreated = index.GetVersion();
+ }
+
+ // Reopen the index for read only
+ {
+ INFO("Trying with Read");
+ PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+
+ // Reopen the index for read/write
+ {
+ INFO("Trying with ReadWrite");
+ PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+
+ // Reopen the index for immutable read
+ {
+ INFO("Trying with Immutable");
+ PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Immutable);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+}
+
+TEST_CASE("PinningIndexAddEntryToTable", "[pinningIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Pin pin = Pin::CreateBlockingPin({ "pkgId", "sourceId" });
+
+ {
+ PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPin(pin);
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+
+ auto pins = Pinning_V1_0::PinTable::GetAllPins(connection);
+ REQUIRE(pins.size() == 1);
+ REQUIRE(pins[0] == pin);
+
+ auto pinFromIndex = Pinning_V1_0::PinTable::GetPinById(connection, 1);
+ REQUIRE(pinFromIndex.has_value());
+ REQUIRE(pinFromIndex.value() == pin);
+
+ REQUIRE(pinFromIndex->GetType() == pin.GetType());
+ REQUIRE(pinFromIndex->GetPackageId() == pin.GetPackageId());
+ REQUIRE(pinFromIndex->GetSourceId() == pin.GetSourceId());
+ }
+
+ {
+ PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ index.RemovePin(pin.GetKey());
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(Pinning_V1_0::PinTable::GetAllPins(connection).empty());
+ REQUIRE(!Pinning_V1_0::PinTable::GetPinById(connection, 1));
+ }
+}
+
+TEST_CASE("PinningIndex_AddUpdateRemove", "[pinningIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Pin pin = Pin::CreateGatingPin({ "pkgId", "srcId" }, { "1.0.*" });
+ Pin updatedPin = Pin::CreatePinningPin({ "pkgId", "srcId" });
+
+ {
+ PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPin(pin);
+ REQUIRE(index.UpdatePin(updatedPin));
+ }
+
+ {
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly);
+ auto pinFromIndex = Pinning_V1_0::PinTable::GetPinById(connection, 1);
+ REQUIRE(pinFromIndex.has_value());
+ REQUIRE(pinFromIndex.value() == updatedPin);
+ }
+
+ {
+ PinningIndex index = PinningIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ index.RemovePin(updatedPin.GetKey());
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(Pinning_V1_0::PinTable::GetAllPins(connection).empty());
+ REQUIRE(!Pinning_V1_0::PinTable::GetPinById(connection, 1));
+ }
+}
+
+TEST_CASE("PinningIndex_ResetAll", "[pinningIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Pin pin1 = Pin::CreateBlockingPin({ "pkg1", "src1" });
+ Pin pin2 = Pin::CreatePinningPin({ "pkg2", "src2" });
+
+ // Add two pins to the index, then check that they show up when queried
+ PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPin(pin1);
+ index.AddPin(pin2);
+
+ REQUIRE(index.GetAllPins().size() == 2);
+ REQUIRE(index.GetPin(pin1.GetKey()).has_value());
+ REQUIRE(index.GetPin(pin2.GetKey()).has_value());
+ REQUIRE(!index.GetPin({ "pkg", "src" }).has_value());
+
+ // Reset the index, then check that there are no pins
+ index.ResetAllPins();
+ REQUIRE(index.GetAllPins().empty());
+ REQUIRE(!index.GetPin(pin1.GetKey()).has_value());
+ REQUIRE(!index.GetPin(pin2.GetKey()).has_value());
+}
+
+TEST_CASE("PinningIndex_AddDuplicatePin", "[pinningIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Pin pin = Pin::CreateGatingPin({ "pkg", "src" }, { "1.*" });
+
+ PinningIndex index = PinningIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPin(pin);
+
+ REQUIRE_THROWS(index.AddPin(pin), ERROR_ALREADY_EXISTS);
+}
\ No newline at end of file
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
index 3b6d5a2f66..88ed0f5aa7 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
@@ -326,6 +326,7 @@
+
@@ -394,6 +395,7 @@
+
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
index 3f4da4c727..5a7a358352 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
@@ -222,6 +222,9 @@
Public\winget
+
+ Public\winget
+
@@ -395,6 +398,9 @@
Source Files
+
+ Source Files
+
diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp
index 0dce355f0e..524920f0fe 100644
--- a/src/AppInstallerCommonCore/Errors.cpp
+++ b/src/AppInstallerCommonCore/Errors.cpp
@@ -208,6 +208,12 @@ namespace AppInstaller
return "Archive malware scan failed.";
case APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED:
return "Found at least one version of the package installed.";
+ case APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS:
+ return "A pin already exists for the package.";
+ case APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST:
+ return "There is no pin for the package.";
+ case APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX:
+ return "Unable to open the pin database.";
// Install errors
case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE:
diff --git a/src/AppInstallerCommonCore/Pin.cpp b/src/AppInstallerCommonCore/Pin.cpp
new file mode 100644
index 0000000000..d76d0f4ea4
--- /dev/null
+++ b/src/AppInstallerCommonCore/Pin.cpp
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "winget/Pin.h"
+
+using namespace AppInstaller::Utility;
+
+namespace AppInstaller::Pinning
+{
+ using namespace std::string_view_literals;
+
+ std::string_view ToString(PinType type)
+ {
+ switch (type)
+ {
+ case PinType::Blocking:
+ return "Blocking"sv;
+ case PinType::Pinning:
+ return "Pinning"sv;
+ case PinType::Gating:
+ return "Gating"sv;
+ case PinType::Unknown:
+ default:
+ return "Unknown";
+ }
+ }
+
+ Pin Pin::CreateBlockingPin(PinKey&& pinKey)
+ {
+ return { PinType::Blocking, std::move(pinKey) };
+ }
+
+ Pin Pin::CreatePinningPin(PinKey&& pinKey)
+ {
+ return { PinType::Pinning, std::move(pinKey) };
+ }
+
+ Pin Pin::CreateGatingPin(PinKey&& pinKey, GatedVersion&& gatedVersion)
+ {
+ return { PinType::Gating, std::move(pinKey), std::move(gatedVersion) };
+ }
+
+ PinType Pin::GetType() const
+ {
+ return m_type;
+ }
+
+ const PinKey& Pin::GetKey() const
+ {
+ return m_key;
+ }
+
+ const AppInstaller::Manifest::Manifest::string_t& Pin::GetPackageId() const
+ {
+ return m_key.PackageId;
+ }
+
+ std::string_view Pin::GetSourceId() const
+ {
+ return m_key.SourceId;
+ }
+
+ Utility::GatedVersion Pin::GetGatedVersion() const
+ {
+ return m_gatedVersion;
+ }
+
+ bool Pin::operator==(const Pin& other) const
+ {
+ return m_type == other.m_type &&
+ m_key == other.m_key &&
+ m_gatedVersion == other.m_gatedVersion;
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
index 130a556b0b..f7864f751a 100644
--- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
+++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h
@@ -110,6 +110,9 @@
#define APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED ((HRESULT)0x8A15005F)
#define APPINSTALLER_CLI_ERROR_ARCHIVE_SCAN_FAILED ((HRESULT)0x8A150060)
#define APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED ((HRESULT)0x8A150061)
+#define APPINSTALLER_CLI_ERROR_PIN_ALREADY_EXISTS ((HRESULT)0x8A150062)
+#define APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST ((HRESULT)0x8A150063)
+#define APPINSTALLER_CLI_ERROR_CANNOT_OPEN_PINNING_INDEX ((HRESULT)0x8A150064)
// Install errors.
#define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101)
diff --git a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h
index 954c76a12b..6b4a01cd0c 100644
--- a/src/AppInstallerCommonCore/Public/AppInstallerVersions.h
+++ b/src/AppInstallerCommonCore/Public/AppInstallerVersions.h
@@ -169,6 +169,21 @@ namespace AppInstaller::Utility
bool m_isEmpty = false;
};
+ // A range of versions indicated by a version and optionally a wildcard at the end.
+ struct GatedVersion
+ {
+ // TODO
+ // For now, using dummy implementation that just holds a string
+ GatedVersion() {}
+ GatedVersion(std::string_view s) : m_tmp(s) {}
+ std::string ToString() const { return m_tmp; }
+
+ bool operator==(const GatedVersion& other) const { return m_tmp == other.m_tmp; }
+
+ private:
+ std::string m_tmp;
+ };
+
// A channel string; existing solely to give a type.
//
// Compared lexicographically.
diff --git a/src/AppInstallerCommonCore/Public/winget/Pin.h b/src/AppInstallerCommonCore/Public/winget/Pin.h
new file mode 100644
index 0000000000..3a617e18ba
--- /dev/null
+++ b/src/AppInstallerCommonCore/Public/winget/Pin.h
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "winget/Manifest.h"
+#include "AppInstallerVersions.h"
+
+namespace AppInstaller::Pinning
+{
+ enum class PinType
+ {
+ Unknown,
+ // The package is blocked from 'upgrade --all' and 'upgrade '.
+ // User has to unblock to allow update.
+ Blocking,
+ // 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.
+ Gating,
+ };
+
+ std::string_view ToString(PinType type);
+
+ // The set of values needed to uniquely identify a Pin
+ struct PinKey
+ {
+ PinKey() {}
+ PinKey(const Manifest::Manifest::string_t& packageId, std::string_view sourceId)
+ : PackageId(packageId), SourceId(sourceId) {}
+
+ bool operator==(const PinKey& other) const
+ {
+ return PackageId == other.PackageId && SourceId == other.SourceId;
+ }
+ bool operator!=(const PinKey& other) const
+ {
+ return !(*this == other);
+ }
+
+ Manifest::Manifest::string_t PackageId;
+ std::string SourceId;
+ };
+
+ struct Pin
+ {
+ static Pin CreateBlockingPin(PinKey&& pinKey);
+ static Pin CreatePinningPin(PinKey&& pinKey);
+ static Pin CreateGatingPin(PinKey&& pinKey, Utility::GatedVersion&& gatedVersion);
+
+ PinType GetType() const;
+ const PinKey& GetKey() const;
+ const Manifest::Manifest::string_t& GetPackageId() const;
+ std::string_view GetSourceId() const;
+ Utility::GatedVersion GetGatedVersion() const;
+
+ bool operator==(const Pin& other) const;
+
+ private:
+ Pin(PinType type, PinKey&& pinKey, Utility::GatedVersion&& gatedVersion = {})
+ : m_type(type), m_key(std::move(pinKey)), m_gatedVersion(std::move(gatedVersion)) {}
+
+ PinType m_type = PinType::Unknown;
+ PinKey m_key;
+ Utility::GatedVersion m_gatedVersion;
+ };
+}
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
index 2f97d0b62e..fad805b911 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
@@ -237,6 +237,7 @@
+
@@ -273,8 +274,11 @@
+
+
+
@@ -339,6 +343,7 @@
+
@@ -361,6 +366,8 @@
+
+
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
index 07bfc49bc3..8ffbdc73ca 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
@@ -76,6 +76,9 @@
{d9d70cf5-ce04-4db2-a0ec-970dd0ad22b6}
+
+ {f05e19bb-2161-4ab0-9d04-2dfa2d3eb3c6}
+
@@ -339,6 +342,18 @@
Rest\Schema
+
+ Microsoft\Schema
+
+
+ Microsoft
+
+
+ Microsoft\Schema\Pinning_1_0
+
+
+ Microsoft\Schema\Pinning_1_0
+
@@ -539,6 +554,15 @@
Source Files
+
+ Source Files
+
+
+ Source Files
+
+
+ Microsoft\Schema\Pinning_1_0
+
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp
new file mode 100644
index 0000000000..aa4688dc12
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.cpp
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "PinningIndex.h"
+#include "SQLiteStorageBase.h"
+#include "Schema/Pinning_1_0/PinningIndexInterface.h"
+
+namespace AppInstaller::Repository::Microsoft
+{
+ PinningIndex PinningIndex::CreateNew(const std::string& filePath, Schema::Version version)
+ {
+ AICLI_LOG(Repo, Info, << "Creating new Pinning Index [" << version << "] at '" << filePath << "'");
+ PinningIndex result{ filePath, version };
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "pinningindex_createnew");
+
+ // Use calculated version, as incoming version could be 'latest'
+ result.m_version.SetSchemaVersion(result.m_dbconn);
+
+ result.m_interface->CreateTables(result.m_dbconn);
+
+ result.SetLastWriteTime();
+
+ savepoint.Commit();
+
+ return result;
+ }
+
+ std::shared_ptr PinningIndex::OpenOrCreateDefault(OpenDisposition openDisposition)
+ {
+ auto indexPath = Runtime::GetPathTo(Runtime::PathName::LocalState) / "pinning.db";
+
+ try
+ {
+ if (std::filesystem::exists(indexPath))
+ {
+ if (std::filesystem::is_regular_file(indexPath))
+ {
+ try
+ {
+ AICLI_LOG(Repo, Info, << "Opening existing pinning index");
+ return std::make_shared(PinningIndex::Open(indexPath.u8string(), openDisposition));
+ }
+ CATCH_LOG();
+ }
+
+ AICLI_LOG(Repo, Info, << "Attempting to delete bad index file");
+ std::filesystem::remove_all(indexPath);
+ }
+
+ return std::make_shared(PinningIndex::CreateNew(indexPath.u8string()));
+ }
+ CATCH_LOG();
+
+ return {};
+ }
+
+ PinningIndex::IdType PinningIndex::AddPin(const Pinning::Pin& pin)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ AICLI_LOG(Repo, Verbose, << "Adding Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType()));
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_addpin");
+
+ IdType result = m_interface->AddPin(m_dbconn, pin);
+
+ SetLastWriteTime();
+
+ savepoint.Commit();
+
+ return result;
+ }
+
+ bool PinningIndex::UpdatePin(const Pinning::Pin& pin)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ AICLI_LOG(Repo, Verbose, << "Updating Pin for package [" << pin.GetPackageId() << "] from source [" << pin.GetSourceId() << "] with pin type " << Pinning::ToString(pin.GetType()));
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningindex_updatepin");
+
+ bool result = m_interface->UpdatePin(m_dbconn, pin).first;
+
+ if (result)
+ {
+ SetLastWriteTime();
+ savepoint.Commit();
+ }
+
+ return result;
+ }
+
+ void PinningIndex::RemovePin(const Pinning::PinKey& pinKey)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ AICLI_LOG(Repo, Verbose, << "Removing Pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]");
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "pinningIndex_removePin");
+
+ m_interface->RemovePin(m_dbconn, pinKey);
+
+ SetLastWriteTime();
+
+ savepoint.Commit();
+ }
+
+ std::optional PinningIndex::GetPin(const Pinning::PinKey& pinKey)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ return m_interface->GetPin(m_dbconn, pinKey);
+ }
+
+ std::vector PinningIndex::GetAllPins()
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ return m_interface->GetAllPins(m_dbconn);
+ }
+
+ bool PinningIndex::ResetAllPins(std::string_view sourceId)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ return m_interface->ResetAllPins(m_dbconn, sourceId);
+ }
+
+ std::unique_ptr PinningIndex::CreateIPinningIndex() const
+ {
+ if (m_version == Schema::Version{ 1, 0 } ||
+ m_version.MajorVersion == 1 ||
+ m_version.IsLatest())
+ {
+ return std::make_unique();
+ }
+
+ THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
+ }
+
+ PinningIndex::PinningIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) :
+ SQLiteStorageBase(target, disposition, std::move(indexFile))
+ {
+ 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());
+ }
+
+ PinningIndex::PinningIndex(const std::string& target, Schema::Version version) : SQLiteStorageBase(target, version)
+ {
+ m_interface = CreateIPinningIndex();
+ 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
new file mode 100644
index 0000000000..77180331b5
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/PinningIndex.h
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "Microsoft/Schema/IPinningIndex.h"
+#include "Microsoft/SQLiteStorageBase.h"
+#include "winget/Pin.h"
+#include
+
+namespace AppInstaller::Repository::Microsoft
+{
+ struct PinningIndex : SQLiteStorageBase
+ {
+ // An id that refers to a specific Pinning file.
+ using IdType = SQLite::rowid_t;
+
+ PinningIndex(const PinningIndex&) = delete;
+ PinningIndex& operator=(const PinningIndex&) = delete;
+
+ PinningIndex(PinningIndex&&) = default;
+ PinningIndex& operator=(PinningIndex&&) = default;
+
+ // Creates a new PinningIndex database of the given version.
+ static PinningIndex CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest());
+
+ // Opens an existing PinningIndex database.
+ static PinningIndex Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile = {})
+ {
+ return { filePath, disposition, std::move(indexFile) };
+ }
+
+ // Opens or creates a PinningIndex database on the default path.
+ // openDisposition is only used when opening an existing database.
+ // Returns nullptr in case of error.
+ static std::shared_ptr OpenOrCreateDefault(OpenDisposition openDisposition = OpenDisposition::ReadWrite);
+
+ // Adds a pin to the index.
+ IdType AddPin(const Pinning::Pin& pin);
+
+ // Updates a pin type, and gated version if needed
+ bool UpdatePin(const Pinning::Pin& pin);
+
+ // Removes a pin from the index.
+ void RemovePin(const Pinning::PinKey& pinKey);
+
+ // Returns the current pin for a given package if it exists.
+ std::optional GetPin(const Pinning::PinKey& pinKey);
+
+ // Returns a vector containing all the existing pins.
+ std::vector GetAllPins();
+
+ // Deletes all pins from a given source, or from all sources if none is specified
+ bool ResetAllPins(std::string_view sourceId = {});
+
+ private:
+ // Constructor used to open an existing index.
+ PinningIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile);
+
+ // Constructor used to create a new index.
+ PinningIndex(const std::string& target, Schema::Version version);
+
+ // Creates the IPinningIndex interface object for this version.
+ std::unique_ptr CreateIPinningIndex() const;
+
+ 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
new file mode 100644
index 0000000000..c4c96a73aa
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPinningIndex.h
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "Microsoft/Schema/Version.h"
+#include "winget/Pin.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema
+{
+ struct IPinningIndex
+ {
+ virtual ~IPinningIndex() = default;
+
+ // Gets the schema version that this index interface is built for.
+ virtual Schema::Version GetVersion() const = 0;
+
+ // Creates all of the version dependent tables within the database.
+ virtual void CreateTables(SQLite::Connection& connection) = 0;
+
+ // Version 1.0
+ // Adds a pin to the index.
+ virtual SQLite::rowid_t AddPin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0;
+
+ // Updates an existing pin in the index.
+ // The return value indicates whether the index was modified by the function.
+ virtual std::pair UpdatePin(SQLite::Connection& connection, const Pinning::Pin& pin) = 0;
+
+ // Removes a pin from the index.
+ virtual SQLite::rowid_t RemovePin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0;
+
+ // Returns the current pin for a given package if it exists.
+ virtual std::optional GetPin(SQLite::Connection& connection, const Pinning::PinKey& pinKey) = 0;
+
+ // Returns a vector containing all the existing pins.
+ virtual std::vector GetAllPins(SQLite::Connection& connection) = 0;
+
+ // Removes all the pins from a given source, or from all sources if none is specified.
+ // Returns a value indicating whether any pin was deleted.
+ virtual bool ResetAllPins(SQLite::Connection& connection, std::string_view sourceId) = 0;
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp
new file mode 100644
index 0000000000..5425556d5e
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.cpp
@@ -0,0 +1,187 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "PinTable.h"
+#include "SQLiteStatementBuilder.h"
+#include "Microsoft/Schema/IPinningIndex.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0
+{
+ namespace
+ {
+ std::optional GetPinFromRow(std::string_view packageId, std::string_view sourceId, Pinning::PinType type, std::string_view version)
+
+ {
+ switch (type)
+ {
+ case Pinning::PinType::Blocking:
+ return Pinning::Pin::CreateBlockingPin({ packageId, sourceId });
+ case Pinning::PinType::Pinning:
+ return Pinning::Pin::CreatePinningPin({ packageId, sourceId });
+ case Pinning::PinType::Gating:
+ return Pinning::Pin::CreateGatingPin({ packageId, sourceId }, Utility::GatedVersion{ version });
+ default:
+ return {};
+ }
+ }
+ }
+
+ 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_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_0");
+
+ 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));
+
+ 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();
+ }
+
+ 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;
+ builder.InsertInto(s_PinTable_Table_Name)
+ .Columns({
+ s_PinTable_PackageId_Column,
+ s_PinTable_SourceId_Column,
+ s_PinTable_Type_Column,
+ s_PinTable_Version_Column })
+ .Values(
+ (std::string_view)pin.GetPackageId(),
+ pin.GetSourceId(),
+ pin.GetType(),
+ pin.GetGatedVersion().ToString());
+
+ builder.Execute(connection);
+ return connection.GetLastInsertRowID();
+ }
+
+ bool PinTable::UpdatePinById(SQLite::Connection& connection, SQLite::rowid_t pinId, const Pinning::Pin& pin)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Update(s_PinTable_Table_Name).Set()
+ .Column(s_PinTable_PackageId_Column).Equals((std::string_view)pin.GetPackageId())
+ .Column(s_PinTable_SourceId_Column).Equals(pin.GetSourceId())
+ .Column(s_PinTable_Type_Column).Equals(pin.GetType())
+ .Column(s_PinTable_Version_Column).Equals(pin.GetGatedVersion().ToString())
+ .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 })
+ .From(s_PinTable_Table_Name).Where(SQLite::RowIDName).Equals(pinId);
+
+ SQLite::Statement select = builder.Prepare(connection);
+
+ if (!select.Step())
+ {
+ return {};
+ }
+
+ auto [packageId, sourceId, type, gatedVersion] = select.GetRow();
+ return GetPinFromRow(packageId, sourceId, type, gatedVersion);
+ }
+
+ 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 })
+ .From(s_PinTable_Table_Name);
+
+ SQLite::Statement select = builder.Prepare(connection);
+
+ std::vector pins;
+ while (select.Step())
+ {
+ auto [packageId, sourceId, type, gatedVersion] = select.GetRow();
+ auto pin = GetPinFromRow(packageId, sourceId, type, gatedVersion);
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h
new file mode 100644
index 0000000000..357ca2319b
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinTable.h
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "SQLiteStatementBuilder.h"
+#include "Microsoft/Schema/IPinningIndex.h"
+#include
+
+namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0
+{
+ struct PinTable
+ {
+ // 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);
+
+ // 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_0/PinningIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h
new file mode 100644
index 0000000000..cfe049ebad
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "Microsoft/Schema/IPinningIndex.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0
+{
+ struct PinningIndexInterface : public IPinningIndex
+ {
+ // Version 1.0
+ Schema::Version GetVersion() const override;
+ void CreateTables(SQLite::Connection& connection) 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_0/PinningIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp
new file mode 100644
index 0000000000..354dca15b2
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Pinning_1_0/PinningIndexInterface_1_0.cpp
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "Microsoft/Schema/Pinning_1_0/PinningIndexInterface.h"
+#include "Microsoft/Schema/Pinning_1_0/PinTable.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Pinning_V1_0
+{
+ 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 a pin for package [" << pinKey.PackageId << "] from source [" << pinKey.SourceId << "]");
+ }
+
+ return result;
+ }
+
+ }
+
+ // Version 1.0
+ Schema::Version PinningIndexInterface::GetVersion() const
+ {
+ return { 1, 0 };
+ }
+
+ void PinningIndexInterface::CreateTables(SQLite::Connection& connection)
+ {
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createpintable_v1_0");
+ Pinning_V1_0::PinTable::Create(connection);
+ savepoint.Commit();
+ }
+
+ 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_0");
+ 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_0");
+ 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_0");
+ 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_0");
+ bool result = PinTable::ResetAllPins(connection, sourceId);
+ savepoint.Commit();
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
index 2efca323fd..247ec33689 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
@@ -172,7 +172,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
portableFile.SymlinkTarget = std::move(symlinkTarget);
result.emplace_back(std::move(portableFile));
}
-
+
return result;
}
}
\ No newline at end of file