winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 128fa1ae9f0b3462303a316fec16e5e2d4d76d69
parent ce587d46e0ae12ff648d036600ac5c4dd3a76405
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Mon, 10 Aug 2020 14:19:49 -0700

Search across multiple sources (#527)

Search across multiple sources
Diffstat:
Msrc/AppInstallerCLICore/Resources.h | 1+
Msrc/AppInstallerCLICore/Workflows/ManifestComparator.cpp | 17++++++++---------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 4++--
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 3+++
Msrc/AppInstallerCLITests/Sources.cpp | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/AppInstallerCLITests/WorkFlow.cpp | 227++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 1-
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 3---
Asrc/AppInstallerRepositoryCore/AggregatedSource.cpp | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/AggregatedSource.h | 37+++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 2++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 6++++++
Msrc/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h | 15++++++++++-----
Msrc/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h | 3+++
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 48++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerRepositoryCore/pch.h | 2++
16 files changed, 428 insertions(+), 193 deletions(-)

diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -127,6 +127,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SearchId); WINGET_DEFINE_RESOURCE_STRINGID(SearchMatch); WINGET_DEFINE_RESOURCE_STRINGID(SearchName); + WINGET_DEFINE_RESOURCE_STRINGID(SearchSource); WINGET_DEFINE_RESOURCE_STRINGID(SearchTruncated); WINGET_DEFINE_RESOURCE_STRINGID(SearchVersion); WINGET_DEFINE_RESOURCE_STRINGID(SettingLoadFailure); diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -11,29 +11,28 @@ namespace AppInstaller::CLI::Workflow bool InstallerComparator::operator() (const ManifestInstaller& installer1, const ManifestInstaller& installer2) { // Todo: Compare only architecture for now. Need more work and spec. - if (Utility::IsApplicableArchitecture(installer1.Arch) < Utility::IsApplicableArchitecture(installer2.Arch)) + if (Utility::IsApplicableArchitecture(installer1.Arch) > Utility::IsApplicableArchitecture(installer2.Arch)) { - return false; + return true; } - return true; + return false; } bool LocalizationComparator::operator() (const ManifestLocalization& loc1, const ManifestLocalization& loc2) { - UNREFERENCED_PARAMETER(loc2); - // Todo: Compare simple language for now. Need more work and spec. std::string userPreferredLocale = std::locale("").name(); - auto found = userPreferredLocale.find(loc1.Language); + auto foundLoc1 = userPreferredLocale.find(loc1.Language); + auto foundLoc2 = userPreferredLocale.find(loc2.Language); - if (found != std::string::npos) + if (foundLoc1 != std::string::npos && foundLoc2 == std::string::npos) { - return false; + return true; } - return true; + return false; } std::optional<Manifest::ManifestInstaller> ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -251,14 +251,14 @@ namespace AppInstaller::CLI::Workflow auto& searchResult = context.Get<Execution::Data::SearchResult>(); Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); - Execution::TableOutput<4> table(context.Reporter, { Resource::String::SearchName, Resource::String::SearchId, Resource::String::SearchVersion, Resource::String::SearchMatch }); + Execution::TableOutput<5> table(context.Reporter, { Resource::String::SearchName, Resource::String::SearchId, Resource::String::SearchVersion, Resource::String::SearchMatch, Resource::String::SearchSource }); for (size_t i = 0; i < searchResult.Matches.size(); ++i) { auto app = searchResult.Matches[i].Application.get(); auto allVersions = app->GetVersions(); - table.OutputLine({ app->GetName(), app->GetId(), allVersions.at(0).GetVersion().ToString(), GetMatchCriteriaDescriptor(searchResult.Matches[i]) }); + table.OutputLine({ app->GetName(), app->GetId(), allVersions.at(0).GetVersion().ToString(), GetMatchCriteriaDescriptor(searchResult.Matches[i]), searchResult.Matches[i].SourceName }); } table.Complete(); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -435,6 +435,9 @@ They can be configured through the settings file 'winget settings'.</value> <data name="SearchName" xml:space="preserve"> <value>Name</value> </data> + <data name="SearchSource" xml:space="preserve"> + <value>Source</value> + </data> <data name="SearchTruncated" xml:space="preserve"> <value>additional entries truncated due to result limit</value> </data> diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -92,76 +92,101 @@ Sources: IsTombstone: false )"sv; -// Helper to create a simple source. -struct TestSource : public ISource -{ - TestSource() = default; - TestSource(const SourceDetails& details) : m_details(details) {} - - static std::shared_ptr<ISource> Create(const SourceDetails& details) - { - // using return std::make_shared<TestSource>(details); will crash the x86 test during destruction. - return std::shared_ptr<ISource>(new TestSource(details)); - } +constexpr std::string_view s_TwoSource_AggregateSourceTest = R"( +Sources: + - Name: winget + Type: testType + Arg: testArg + Data: testData + IsTombstone: false + - Name: msstore + Type: testType + Arg: testArg + Data: testData + IsTombstone: false +)"sv; - // ISource - const SourceDetails& GetDetails() const override +namespace +{ + // Helper to create a simple source. + struct TestSource : public ISource { - return m_details; - } + TestSource() = default; + TestSource(const SourceDetails& details) : m_details(details) {} - SearchResult Search(const SearchRequest& request) override - { - UNREFERENCED_PARAMETER(request); - return {}; - } + static std::shared_ptr<ISource> Create(const SourceDetails& details) + { + // using return std::make_shared<TestSource>(details); will crash the x86 test during destruction. + return std::shared_ptr<ISource>(new TestSource(details)); + } - SourceDetails m_details; -}; + // ISource + const SourceDetails& GetDetails() const override + { + return m_details; + } -// Helper that allows some lambdas to be wrapped into a source factory. -struct TestSourceFactory : public ISourceFactory -{ - using CreateFunctor = std::function<std::shared_ptr<ISource>(const SourceDetails&)>; - using AddFunctor = std::function<void(SourceDetails&)>; - using UpdateFunctor = std::function<void(const SourceDetails&)>; - using RemoveFunctor = std::function<void(const SourceDetails&)>; + SearchResult Search(const SearchRequest& request) override + { + UNREFERENCED_PARAMETER(request); + + SearchResult result; + ApplicationMatchFilter testMatchFilter1{ ApplicationMatchField::Id, MatchType::Exact, "test" }; + ApplicationMatchFilter testMatchFilter2{ ApplicationMatchField::Name, MatchType::Exact, "test" }; + ApplicationMatchFilter testMatchFilter3{ ApplicationMatchField::Id, MatchType::CaseInsensitive, "test" }; + result.Matches.emplace_back(std::unique_ptr<IApplication>(), testMatchFilter1); + result.Matches.emplace_back(std::unique_ptr<IApplication>(), testMatchFilter2); + result.Matches.emplace_back(std::unique_ptr<IApplication>(), testMatchFilter3); + return result; + } - TestSourceFactory() : - m_Create(TestSource::Create), m_Add([](SourceDetails&) {}), m_Update([](const SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} + SourceDetails m_details; + }; - // ISourceFactory - std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback&) override + // Helper that allows some lambdas to be wrapped into a source factory. + struct TestSourceFactory : public ISourceFactory { - return m_Create(details); - } + using CreateFunctor = std::function<std::shared_ptr<ISource>(const SourceDetails&)>; + using AddFunctor = std::function<void(SourceDetails&)>; + using UpdateFunctor = std::function<void(const SourceDetails&)>; + using RemoveFunctor = std::function<void(const SourceDetails&)>; - void Add(SourceDetails& details, IProgressCallback&) override - { - m_Add(details); - } + TestSourceFactory() : + m_Create(TestSource::Create), m_Add([](SourceDetails&) {}), m_Update([](const SourceDetails&) {}), m_Remove([](const SourceDetails&) {}) {} - void Update(const SourceDetails& details, IProgressCallback&) override - { - m_Update(details); - } + // ISourceFactory + std::shared_ptr<ISource> Create(const SourceDetails& details, IProgressCallback&) override + { + return m_Create(details); + } - void Remove(const SourceDetails& details, IProgressCallback&) override - { - m_Remove(details); - } + void Add(SourceDetails& details, IProgressCallback&) override + { + m_Add(details); + } - // Make copies of self when requested. - operator std::function<std::unique_ptr<ISourceFactory>()>() - { - return [this]() { return std::make_unique<TestSourceFactory>(*this); }; - } + void Update(const SourceDetails& details, IProgressCallback&) override + { + m_Update(details); + } + + void Remove(const SourceDetails& details, IProgressCallback&) override + { + m_Remove(details); + } - CreateFunctor m_Create; - AddFunctor m_Add; - UpdateFunctor m_Update; - RemoveFunctor m_Remove; -}; + // Make copies of self when requested. + operator std::function<std::unique_ptr<ISourceFactory>()>() + { + return [this]() { return std::make_unique<TestSourceFactory>(*this); }; + } + + CreateFunctor m_Create; + AddFunctor m_Add; + UpdateFunctor m_Update; + RemoveFunctor m_Remove; + }; +} TEST_CASE("RepoSources_UserSettingDoesNotExist", "[sources]") @@ -547,3 +572,39 @@ TEST_CASE("RepoSources_DropAllSources", "[sources]") REQUIRE(sources.size() == 1); REQUIRE(sources[0].Origin == SourceOrigin::Default); } + +TEST_CASE("RepoSources_SearchAcrossMultipleSources", "[sources]") +{ + TestHook_ClearSourceFactoryOverrides(); + TestSourceFactory factory; + TestHook_SetSourceFactoryOverride("testType", factory); + + SetSetting(Streams::UserSources, s_TwoSource_AggregateSourceTest); + + ProgressCallback progress; + auto source = OpenSource("", progress); + + REQUIRE(source->GetDetails().IsAggregated); + + SearchRequest request; + auto result = source->Search(request); + REQUIRE(result.Matches.size() == 6); + REQUIRE_FALSE(result.Truncated); + // matches are sorted in expected order + REQUIRE((result.Matches[0].MatchCriteria.Type == MatchType::Exact && result.Matches[0].MatchCriteria.Field == ApplicationMatchField::Id)); + REQUIRE((result.Matches[1].MatchCriteria.Type == MatchType::Exact && result.Matches[1].MatchCriteria.Field == ApplicationMatchField::Id)); + REQUIRE((result.Matches[2].MatchCriteria.Type == MatchType::Exact && result.Matches[2].MatchCriteria.Field == ApplicationMatchField::Name)); + REQUIRE((result.Matches[3].MatchCriteria.Type == MatchType::Exact && result.Matches[3].MatchCriteria.Field == ApplicationMatchField::Name)); + REQUIRE((result.Matches[4].MatchCriteria.Type == MatchType::CaseInsensitive && result.Matches[4].MatchCriteria.Field == ApplicationMatchField::Id)); + REQUIRE((result.Matches[5].MatchCriteria.Type == MatchType::CaseInsensitive && result.Matches[5].MatchCriteria.Field == ApplicationMatchField::Id)); + + // when truncate required + request.MaximumResults = 3; + result = source->Search(request); + REQUIRE(result.Matches.size() == 3); + REQUIRE(result.Truncated); + // matches are sorted in expected order + REQUIRE((result.Matches[0].MatchCriteria.Type == MatchType::Exact && result.Matches[0].MatchCriteria.Field == ApplicationMatchField::Id)); + REQUIRE((result.Matches[1].MatchCriteria.Type == MatchType::Exact && result.Matches[1].MatchCriteria.Field == ApplicationMatchField::Id)); + REQUIRE((result.Matches[2].MatchCriteria.Type == MatchType::Exact && result.Matches[2].MatchCriteria.Field == ApplicationMatchField::Name)); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -32,151 +32,154 @@ using namespace AppInstaller::Utility; REQUIRE(_context_.IsTerminated()); \ REQUIRE(_hr_ == _context_.GetTerminationHR()) -struct TestSource : public ISource +namespace { - struct TestApplication : public IApplication + struct TestSource : public ISource { - TestApplication(const Manifest manifest) : m_manifest(manifest) {} - - std::optional<Manifest> GetManifest(const NormalizedString&, const NormalizedString&) override + struct TestApplication : public IApplication { - return m_manifest; - } - - LocIndString GetId() override - { - return LocIndString{ m_manifest.Id }; - } + TestApplication(const Manifest manifest) : m_manifest(manifest) {} - LocIndString GetName() override - { - return LocIndString{ m_manifest.Name }; - } + std::optional<Manifest> GetManifest(const NormalizedString&, const NormalizedString&) override + { + return m_manifest; + } - std::vector<VersionAndChannel> GetVersions() override - { - std::vector<VersionAndChannel> result; - result.emplace_back(Version(m_manifest.Version), Channel(m_manifest.Channel)); - return result; - } + LocIndString GetId() override + { + return LocIndString{ m_manifest.Id }; + } - Manifest m_manifest; - }; + LocIndString GetName() override + { + return LocIndString{ m_manifest.Name }; + } - SearchResult Search(const SearchRequest& request) override - { - SearchResult result; + std::vector<VersionAndChannel> GetVersions() override + { + std::vector<VersionAndChannel> result; + result.emplace_back(Version(m_manifest.Version), Channel(m_manifest.Channel)); + return result; + } - std::string input; + Manifest m_manifest; + }; - if (request.Query) - { - input = request.Query->Value; - } - else if (!request.Inclusions.empty()) + SearchResult Search(const SearchRequest& request) override { - input = request.Inclusions[0].Value; - } + SearchResult result; - if (input == "TestQueryReturnOne") - { - auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); - } - else if (input == "TestQueryReturnTwo") - { - auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); - - auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest2), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); - } + std::string input; - return result; - } + if (request.Query) + { + input = request.Query->Value; + } + else if (!request.Inclusions.empty()) + { + input = request.Inclusions[0].Value; + } + + if (input == "TestQueryReturnOne") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); + } + else if (input == "TestQueryReturnTwo") + { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); + + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("Manifest-Good.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest2), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); + } - const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } -}; + return result; + } -struct TestContext; + const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } + }; -struct WorkflowTaskOverride -{ - WorkflowTaskOverride(WorkflowTask::Func f, const std::function<void(TestContext&)>& o) : - Target(f), Override(o) {} + struct TestContext; - WorkflowTaskOverride(std::string_view n, const std::function<void(TestContext&)>& o) : - Target(n), Override(o) {} + struct WorkflowTaskOverride + { + WorkflowTaskOverride(WorkflowTask::Func f, const std::function<void(TestContext&)>& o) : + Target(f), Override(o) {} - WorkflowTaskOverride(const WorkflowTask& t, const std::function<void(TestContext&)>& o) : - Target(t), Override(o) {} + WorkflowTaskOverride(std::string_view n, const std::function<void(TestContext&)>& o) : + Target(n), Override(o) {} - bool Used = false; - WorkflowTask Target; - std::function<void(TestContext&)> Override; -}; + WorkflowTaskOverride(const WorkflowTask& t, const std::function<void(TestContext&)>& o) : + Target(t), Override(o) {} -// Enables overriding the behavior of specific workflow tasks. -struct TestContext : public Context -{ - TestContext(std::ostream& out, std::istream& in) : Context(out, in) + bool Used = false; + WorkflowTask Target; + std::function<void(TestContext&)> Override; + }; + + // Enables overriding the behavior of specific workflow tasks. + struct TestContext : public Context { - WorkflowTaskOverride wto - { RemoveInstaller, [](TestContext&) - { - // Do nothing; we never want to remove the test files. - } }; + TestContext(std::ostream& out, std::istream& in) : Context(out, in) + { + WorkflowTaskOverride wto + { RemoveInstaller, [](TestContext&) + { + // Do nothing; we never want to remove the test files. + } }; - // Mark this one as used so that it doesn't anger the destructor. - wto.Used = true; + // Mark this one as used so that it doesn't anger the destructor. + wto.Used = true; - Override(wto); - } + Override(wto); + } - ~TestContext() - { - for (const auto& wto : m_overrides) + ~TestContext() { - if (!wto.Used) + for (const auto& wto : m_overrides) { - FAIL("Unused override"); + if (!wto.Used) + { + FAIL("Unused override"); + } } } - } - - bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task) override - { - auto itr = std::find_if(m_overrides.begin(), m_overrides.end(), [&](const WorkflowTaskOverride& wto) { return wto.Target == task; }); - if (itr == m_overrides.end()) + bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task) override { - return true; + auto itr = std::find_if(m_overrides.begin(), m_overrides.end(), [&](const WorkflowTaskOverride& wto) { return wto.Target == task; }); + + if (itr == m_overrides.end()) + { + return true; + } + else + { + itr->Used = true; + itr->Override(*this); + return false; + } } - else + + void Override(const WorkflowTaskOverride& wto) { - itr->Used = true; - itr->Override(*this); - return false; + m_overrides.emplace_back(wto); } - } - - void Override(const WorkflowTaskOverride& wto) - { - m_overrides.emplace_back(wto); - } -private: - std::vector<WorkflowTaskOverride> m_overrides; -}; + private: + std::vector<WorkflowTaskOverride> m_overrides; + }; +} void OverrideForOpenSource(TestContext& context) { diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -201,7 +201,6 @@ <ClInclude Include="Telemetry\MicrosoftTelemetry.h" /> <ClInclude Include="Telemetry\TraceLogging.h" /> <ClInclude Include="Telemetry\WinEventLogLevels.h" /> - <ClInclude Include="winget\settings\Setting.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="AppInstallerLogging.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -108,9 +108,6 @@ <ClInclude Include="Public\winget\UserSettings.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="winget\settings\Setting.h"> - <Filter>Header Files</Filter> - </ClInclude> <ClInclude Include="JsonUtil.h"> <Filter>Header Files</Filter> </ClInclude> diff --git a/src/AppInstallerRepositoryCore/AggregatedSource.cpp b/src/AppInstallerRepositoryCore/AggregatedSource.cpp @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AggregatedSource.h" + +namespace AppInstaller::Repository +{ + AggregatedSource::AggregatedSource() + { + m_details.Name = "AggregatedSource"; + m_details.IsAggregated = true; + } + + const SourceDetails& AppInstaller::Repository::AggregatedSource::GetDetails() const + { + return m_details; + } + + void AggregatedSource::AddSource(std::shared_ptr<ISource> source) + { + m_sources.emplace_back(std::move(source)); + } + + SearchResult AggregatedSource::Search(const SearchRequest& request) + { + SearchResult result; + + for (auto& source : m_sources) + { + auto oneSourceResult = source->Search(request); + + for (auto& r : oneSourceResult.Matches) + { + r.SourceName = source->GetDetails().Name; + result.Matches.emplace_back(std::move(r)); + } + } + + SortResultMatches(result.Matches); + + if (request.MaximumResults > 0 && result.Matches.size() > request.MaximumResults) + { + result.Truncated = true; + result.Matches.erase(result.Matches.begin() + request.MaximumResults, result.Matches.end()); + } + + return result; + } + + void AggregatedSource::SortResultMatches(std::vector<ResultMatch>& matches) + { + struct ResultMatchComparator + { + // The comparator compares the ResultMatch by MatchType first, then Field in a predefined order. + bool operator() ( + const ResultMatch& match1, + const ResultMatch& match2) + { + if (match1.MatchCriteria.Type != match2.MatchCriteria.Type) + { + return match2.MatchCriteria.Type > match1.MatchCriteria.Type; + } + + if (match1.MatchCriteria.Field != match2.MatchCriteria.Field) + { + return match2.MatchCriteria.Field > match1.MatchCriteria.Field; + } + + return false; + } + }; + + std::stable_sort(matches.begin(), matches.end(), ResultMatchComparator()); + } +} + diff --git a/src/AppInstallerRepositoryCore/AggregatedSource.h b/src/AppInstallerRepositoryCore/AggregatedSource.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "AppInstallerRepositorySource.h" + +namespace AppInstaller::Repository +{ + struct AggregatedSource : public ISource + { + AggregatedSource(); + + AggregatedSource(const AggregatedSource&) = delete; + AggregatedSource& operator=(const AggregatedSource&) = delete; + + AggregatedSource(AggregatedSource&&) = default; + AggregatedSource& operator=(AggregatedSource&&) = default; + + ~AggregatedSource() = default; + + // Get the source's details. + const SourceDetails& GetDetails() const override; + + // Execute a search on the source. + SearchResult Search(const SearchRequest & request) override; + + void AddSource(std::shared_ptr<ISource> source); + + private: + std::vector<std::shared_ptr<ISource>> m_sources; + SourceDetails m_details; + + void SortResultMatches(std::vector<ResultMatch>& matches); + }; +} + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -172,6 +172,7 @@ </Link> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="AggregatedSource.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> <ClInclude Include="Manifest\Manifest.h" /> <ClInclude Include="Manifest\ManifestInstaller.h" /> @@ -206,6 +207,7 @@ <ClInclude Include="SQLiteWrapper.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="AggregatedSource.cpp" /> <ClCompile Include="ICU\SQLiteICU.c"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -129,6 +129,9 @@ <ClInclude Include="Manifest\YamlParser.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="AggregatedSource.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -194,6 +197,9 @@ <ClCompile Include="Manifest\YamlParser.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="AggregatedSource.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -16,25 +16,27 @@ namespace AppInstaller::Repository { // The type of matching to perform during a search. + // The values must be declared in order of preference in search results. enum class MatchType { - Exact, + Exact = 0, CaseInsensitive, StartsWith, - Substring, - Wildcard, Fuzzy, + Substring, FuzzySubstring, + Wildcard, }; // The field to match on. + // The values must be declared in order of preference in search results. enum class ApplicationMatchField { - Id, + Id = 0, Name, Moniker, - Tag, Command, + Tag, }; // A single match to be performed during a search. @@ -111,6 +113,9 @@ namespace AppInstaller::Repository // The highest order field on which the application matched the search. ApplicationMatchFilter MatchCriteria; + // The name of the source where the result is from. Used in aggregated source scenario. + std::string SourceName = {}; + ResultMatch(std::unique_ptr<IApplication>&& a, ApplicationMatchFilter f) : Application(std::move(a)), MatchCriteria(std::move(f)) {} }; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -43,6 +43,9 @@ namespace AppInstaller::Repository // The origin of the source. SourceOrigin Origin = SourceOrigin::Default; + + // If the source is an aggregated source + bool IsAggregated = false; }; // Interface for interacting with a source from outside of the repository lib. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -2,8 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/AppInstallerRepositorySource.h" -#include <winget/UserSettings.h> +#include "AggregatedSource.h" #include "SourceFactory.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" @@ -512,12 +512,38 @@ namespace AppInstaller::Repository AICLI_LOG(Repo, Info, << "Default source requested, but no sources configured"); return {}; } - else + else if(currentSources.size() == 1) { - // TODO: Create aggregate source here. For now, just get the first in the list. - AICLI_LOG(Repo, Info, << "Default source requested, using first source: " << currentSources[0].Name); + AICLI_LOG(Repo, Info, << "Default source requested, only 1 source available, using the only source: " << currentSources[0].Name); return OpenSource(currentSources[0].Name, progress); } + else + { + AICLI_LOG(Repo, Info, << "Default source requested, multiple sources available, creating aggregated source."); + auto aggregatedSource = std::make_shared<AggregatedSource>(); + + bool sourceUpdated = false; + for (auto& source : currentSources) + { + AICLI_LOG(Repo, Info, << "Adding to aggregated source: " << source.Name); + + if (ShouldUpdateBeforeOpen(source)) + { + // TODO: Consider adding a context callback to indicate we are doing the same action + // to avoid the progress bar fill up multiple times. + UpdateSourceFromDetails(source, progress); + sourceUpdated = true; + } + aggregatedSource->AddSource(CreateSourceFromDetails(source, progress)); + } + + if (sourceUpdated) + { + SetMetadata(currentSources); + } + + return aggregatedSource; + } } else { @@ -598,6 +624,20 @@ namespace AppInstaller::Repository THROW_HR(E_UNEXPECTED); } + // Add back tombstoned default sources, otherwise the info will be lost by SetSourcesByOrigin + auto defaultSources = GetSourcesByOrigin(SourceOrigin::Default); + for (const auto& defaultSource : defaultSources) + { + if (FindSourceByName(currentSources, defaultSource.Name) == currentSources.end()) + { + SourceDetailsInternal tombstone; + tombstone.Name = defaultSource.Name; + tombstone.IsTombstone = true; + tombstone.Origin = SourceOrigin::User; + currentSources.emplace_back(std::move(tombstone)); + } + } + SetSourcesByOrigin(SourceOrigin::User, currentSources); return true; diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -18,7 +18,9 @@ #include <AppInstallerSynchronization.h> #include <AppInstallerVersions.h> #include <winget/ExtensionCatalog.h> +#include <winget/ExperimentalFeature.h> #include <winget/Settings.h> +#include <winget/UserSettings.h> #include <yaml-cpp/yaml.h> #include <wil/result_macros.h>