Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## New in v1.28

# Experimental Feature: 'sourceEdit'
New feature that adds an 'edit' subcommand to the 'source' command. This can be used to set an explicit source to be implicit and vice-versa. For example, with this feature you can make the 'winget-font' source an implicit source instead of explicit source.

To enable this feature, add the 'sourceEdit' experimental feature to your settings.
```
"experimentalFeatures": {
"sourceEdit": true
},
```
To use the feature, try `winget source edit winget-font` to set the Explicit state to the default.

<!-- Nothing yet! -->
10 changes: 10 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,13 @@ This feature enables support for fonts via `winget settings`. The `winget font l
"fonts": true
},
```

### sourceEdit

This feature enables support for additional source command improvements via `winget settings`. The `winget source edit` command will become available with this feature.

```json
"experimentalFeatures": {
"sourceEdit": true
},
```
35 changes: 35 additions & 0 deletions src/AppInstallerCLICore/Commands/SourceCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace AppInstaller::CLI
std::make_unique<SourceListCommand>(FullName()),
std::make_unique<SourceUpdateCommand>(FullName()),
std::make_unique<SourceRemoveCommand>(FullName()),
std::make_unique<SourceEditCommand>(FullName()),
std::make_unique<SourceResetCommand>(FullName()),
std::make_unique<SourceExportCommand>(FullName()),
});
Expand Down Expand Up @@ -312,4 +313,38 @@ namespace AppInstaller::CLI
Workflow::GetSourceListWithFilter <<
Workflow::ExportSourceList;
}

// Source Edit Command

std::vector<Argument> SourceEditCommand::GetArguments() const
{
return {
Argument::ForType(Args::Type::SourceName).SetRequired(true),
Argument::ForType(Args::Type::SourceExplicit),
};
}

Resource::LocString SourceEditCommand::ShortDescription() const
{
return { Resource::String::SourceEditCommandShortDescription };
}

Resource::LocString SourceEditCommand::LongDescription() const
{
return { Resource::String::SourceEditCommandLongDescription };
}

Utility::LocIndView SourceEditCommand::HelpLink() const
{
return s_SourceCommand_HelpLink;
}

void SourceEditCommand::ExecuteInternal(Context& context) const
{
context <<
Workflow::EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::SourceEdit) <<
Workflow::EnsureRunningAsAdmin <<
Workflow::GetSourceListWithFilter <<
Workflow::EditSources;
}
}
15 changes: 15 additions & 0 deletions src/AppInstallerCLICore/Commands/SourceCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,19 @@ namespace AppInstaller::CLI
protected:
void ExecuteInternal(Execution::Context& context) const override;
};

struct SourceEditCommand final : public Command
{
SourceEditCommand(std::string_view parent) : Command("edit", {}, parent, Settings::ExperimentalFeature::Feature::SourceEdit) {}

std::vector<Argument> GetArguments() const override;

Resource::LocString ShortDescription() const override;
Resource::LocString LongDescription() const override;

Utility::LocIndView HelpLink() const override;

protected:
void ExecuteInternal(Execution::Context& context) const override;
};
}
4 changes: 4 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,10 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditOne);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditNoChanges);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription);
Expand Down
33 changes: 33 additions & 0 deletions src/AppInstallerCLICore/Workflows/SourceFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,39 @@ namespace AppInstaller::CLI::Workflow
}
}

void EditSources(Execution::Context& context)
{
// We are assuming there is only one match, as SourceName is a required parameter.
const std::vector<Repository::SourceDetails>& sources = context.Get<Data::SourceList>();
bool isExplicit = context.Args.Contains(Args::Type::SourceExplicit);
for (const auto& sd : sources)
{
// Get the current source with this name.
Repository::Source currentSource{ sd.Name };

// Only permitting Explicit to be edited at this time.
Repository::Source source{ sd.Name, isExplicit };

// Is anything being changed??
if (!source.IsEditOfSource(currentSource))
{
context.Reporter.Info() << Resource::String::SourceEditNoChanges(Utility::LocIndView{ sd.Name }) << std::endl;
continue;
}

context.Reporter.Info() << Resource::String::SourceEditOne(Utility::LocIndView{ sd.Name }, Utility::LocIndView{ Utility::ConvertBoolToString(isExplicit) }) << std::endl;
auto editFunction = [&](IProgressCallback& progress)->bool { return source.Edit(progress); };
if (context.Reporter.ExecuteWithProgress(editFunction))
{
context.Reporter.Info() << Resource::String::Done << std::endl;
}
else
{
context.Reporter.Info() << Resource::String::Cancelled << std::endl;
}
}
}

void QueryUserForSourceReset(Execution::Context& context)
{
if (!context.Args.Contains(Execution::Args::Type::ForceSourceReset))
Expand Down
6 changes: 6 additions & 0 deletions src/AppInstallerCLICore/Workflows/SourceFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,10 @@ namespace AppInstaller::CLI::Workflow
// Inputs: None
// Outputs: None
void ForceInstalledCacheUpdate(Execution::Context& context);

// Edits a source in SourceList.
// Required Args: SourceName
// Inputs: SourceList
// Outputs: None
void EditSources(Execution::Context& context);
}
87 changes: 87 additions & 0 deletions src/AppInstallerCLIE2ETests/SourceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ namespace AppInstallerCLIE2ETests
/// </summary>
public class SourceCommand : BaseCommand
{
/// <summary>
/// One time set up.
/// </summary>
[OneTimeSetUp]
public void OneTimeSetup()
{
WinGetSettingsHelper.ConfigureFeature("sourceEdit", true);
}

/// <summary>
/// Test set up.
/// </summary>
Expand Down Expand Up @@ -258,5 +267,83 @@ public void SourceForceReset()
Assert.False(result.StdOut.Contains(Constants.TestSourceName));
Assert.False(result.StdOut.Contains(Constants.TestSourceUrl));
}

/// <summary>
/// Test source add with explicit flag, edit the source to not be explicit.
/// </summary>
[Test]
public void SourceEdit()
{
// Remove the test source.
TestCommon.RunAICLICommand("source remove", Constants.TestSourceName);

// Add source as explicit and verify it is explicit.
var addResult = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl} --trust-level trusted --explicit");
Assert.AreEqual(Constants.ErrorCode.S_OK, addResult.ExitCode);
Assert.True(addResult.StdOut.Contains("Done"));

var searchResult = TestCommon.RunAICLICommand("search", "TestExampleInstaller");
Assert.AreEqual(Constants.ErrorCode.ERROR_NO_SOURCES_DEFINED, searchResult.ExitCode);
Assert.True(searchResult.StdOut.Contains("No sources defined; add one with 'source add' or reset to defaults with 'source reset'"));

// Run the edit, this should be S_OK with "Done" as it changed the state to not-explicit.
var editResult = TestCommon.RunAICLICommand("source edit", $"SourceTest");
Assert.AreEqual(Constants.ErrorCode.S_OK, editResult.ExitCode);
Assert.True(editResult.StdOut.Contains("Done"));

// Run it again, this should result in S_OK with no changes and a message that the source is already in that state.
var editResult2 = TestCommon.RunAICLICommand("source edit", $"SourceTest");
Assert.AreEqual(Constants.ErrorCode.S_OK, editResult2.ExitCode);
Assert.True(editResult2.StdOut.Contains("The source named 'SourceTest' is already in that desired state."));

// Now verify it is no longer explicit by running the search again without adding the source parameter.
var searchResult2 = TestCommon.RunAICLICommand("search", "TestExampleInstaller");
Assert.AreEqual(Constants.ErrorCode.S_OK, searchResult2.ExitCode);
Assert.True(searchResult2.StdOut.Contains("TestExampleInstaller"));
Assert.True(searchResult2.StdOut.Contains("AppInstallerTest.TestExampleInstaller"));
TestCommon.RunAICLICommand("source remove", $"-n SourceTest");
}

/// <summary>
/// Test override of a default source via edit command.
/// </summary>
[Test]
public void SourceEditOverrideDefault()
{
// Force Reset Sources
var resetResult = TestCommon.RunAICLICommand("source reset", "--force");
Assert.AreEqual(Constants.ErrorCode.S_OK, resetResult.ExitCode);

// Verify it is explicit true. Explicit is the only boolean value in the output.
var listResult = TestCommon.RunAICLICommand("source list", "winget-font");
Assert.AreEqual(Constants.ErrorCode.S_OK, listResult.ExitCode);
Assert.True(listResult.StdOut.Contains("true"));

var editResult = TestCommon.RunAICLICommand("source edit", "winget-font");
Assert.AreEqual(Constants.ErrorCode.S_OK, editResult.ExitCode);
Assert.True(editResult.StdOut.Contains("Done"));

// Verify that after edit it is now explicit false.
var listResult2 = TestCommon.RunAICLICommand("source list", "winget-font");
Assert.AreEqual(Constants.ErrorCode.S_OK, listResult2.ExitCode);
Assert.True(listResult2.StdOut.Contains("false"));

// Remove the source. This should correctly tombstone it, even though it is overridden.
var removeResult = TestCommon.RunAICLICommand("source remove", "winget-font");
Assert.AreEqual(Constants.ErrorCode.S_OK, removeResult.ExitCode);
Assert.True(removeResult.StdOut.Contains("Done"));

var listResult3 = TestCommon.RunAICLICommand("source list", "winget-font");
Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, listResult3.ExitCode);

// Force Reset Sources
var resetResult2 = TestCommon.RunAICLICommand("source reset", "--force");
Assert.AreEqual(Constants.ErrorCode.S_OK, resetResult2.ExitCode);

// Verify it is back to being explicit true.
var listResult4 = TestCommon.RunAICLICommand("source list", "winget-font");
Assert.AreEqual(Constants.ErrorCode.S_OK, listResult4.ExitCode);
Assert.True(listResult4.StdOut.Contains("true"));
}
}
}
14 changes: 14 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,20 @@ They can be configured through the settings file 'winget settings'.</value>
<data name="SourceCommandShortDescription" xml:space="preserve">
<value>Manage sources of packages</value>
</data>
<data name="SourceEditCommandLongDescription" xml:space="preserve">
<value>Edit properties of an existing source. A source provides the data for you to discover and install packages. Only add a new source if you trust it as a secure location.</value>
</data>
<data name="SourceEditCommandShortDescription" xml:space="preserve">
<value>Edit properties of a source</value>
</data>
<data name="SourceEditOne" xml:space="preserve">
<value>Changing source: {0} to Explicit={1}...</value>
<comment>{Locked="{0}","{1}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name. {1} is a placeholder replaced by the changed value.</comment>
</data>
<data name="SourceEditNoChanges" xml:space="preserve">
<value>The source named '{0}' is already in that desired state.</value>
<comment>{Locked="{0}"} Message displayed to inform the user about a registered repository source that is currently being edited. {0} is a placeholder replaced by the repository source name.</comment>
</data>
<data name="SourceListArg" xml:space="preserve">
<value>Argument</value>
<comment>Value given to source.</comment>
Expand Down
45 changes: 45 additions & 0 deletions src/AppInstallerCLITests/Sources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ constexpr std::string_view s_SingleSource = R"(
IsTombstone: false
)"sv;

constexpr std::string_view s_SingleSourceOverride = R"(
Sources:
- Name: winget-font
Type: ""
Arg: ""
Data: ""
IsTombstone: false
IsOverride: true
Explicit: false
)"sv;

constexpr std::string_view s_SingleSourceMetadata = R"(
Sources:
- Name: testName
Expand Down Expand Up @@ -291,6 +302,40 @@ TEST_CASE("RepoSources_DefaultSourcesTombstoned", "[sources]")
REQUIRE(sources.empty());
}


TEST_CASE("RepoSources_DefaultSourceOverride", "[sources]")
{
SetSetting(Stream::UserSources, s_EmptySources);

// Default font has explicit to true.
// Font is at index 2 as it is the third one added.
auto beforeOverride = GetSources();
REQUIRE(beforeOverride.size() == c_DefaultSourceCount);
REQUIRE(beforeOverride[2].Name == "winget-font");
REQUIRE(beforeOverride[2].Arg == "https://cdn.winget.microsoft.com/fonts");
REQUIRE(beforeOverride[2].Data == "Microsoft.Winget.Fonts.Source_8wekyb3d8bbwe");
REQUIRE(beforeOverride[2].Type == "Microsoft.PreIndexed.Package");
REQUIRE(beforeOverride[2].Origin == SourceOrigin::Default);
REQUIRE(beforeOverride[2].Explicit == true);

SetSetting(Stream::UserSources, s_SingleSourceOverride);
auto afterOverride = GetSources();

// The override will change the index value as the Default will be replaced by the override.
// User sources have higher priority so the override will be at index 0.
// We expect the same count, and the Name, Arg, Data, and Type properties to all be identical.
// Only the name is defined in the override setting so all others should be properly populated.
REQUIRE(afterOverride.size() == c_DefaultSourceCount);
REQUIRE(afterOverride[0].Name == beforeOverride[2].Name);
REQUIRE(afterOverride[0].Arg == beforeOverride[2].Arg);
REQUIRE(afterOverride[0].Data == beforeOverride[2].Data);
REQUIRE(afterOverride[0].Type == beforeOverride[2].Type);

// The only properties we expect to be different are the Origin, which is now User, and Explicit.
REQUIRE(afterOverride[0].Origin == SourceOrigin::User);
REQUIRE(afterOverride[0].Explicit == false);
}

TEST_CASE("RepoSources_SingleSource", "[sources]")
{
SetSetting(Stream::UserSources, s_SingleSource);
Expand Down
15 changes: 15 additions & 0 deletions src/AppInstallerCLITests/TestSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,15 @@ namespace TestCommon
return true;
}

bool TestSourceFactory::Edit(const SourceDetails& details, IProgressCallback&)
{
if (OnEdit)
{
OnEdit(details);
}
return true;
}

// Make copies of self when requested.
TestSourceFactory::operator std::function<std::unique_ptr<ISourceFactory>()>()
{
Expand All @@ -466,6 +475,12 @@ namespace TestCommon
return source.Add(progress);
}

bool EditSource(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback& progress)
{
Repository::Source source{ details.Name, details.Explicit };
return source.Edit(progress);
}

bool UpdateSource(std::string_view name, AppInstaller::IProgressCallback& progress)
{
Repository::Source source{ name };
Expand Down
4 changes: 4 additions & 0 deletions src/AppInstallerCLITests/TestSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ namespace TestCommon
using AddFunctor = std::function<void(AppInstaller::Repository::SourceDetails&)>;
using UpdateFunctor = std::function<void(const AppInstaller::Repository::SourceDetails&)>;
using RemoveFunctor = std::function<void(const AppInstaller::Repository::SourceDetails&)>;
using EditFunctor = std::function<void(const AppInstaller::Repository::SourceDetails&)>;

TestSourceFactory(OpenFunctor open) : OnOpen(std::move(open)) {}
TestSourceFactory(OpenFunctorWithCustomHeader open) : OnOpenWithCustomHeader(std::move(open)) {}
Expand All @@ -197,6 +198,7 @@ namespace TestCommon
bool Add(AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override;
bool Update(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override;
bool Remove(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override;
bool Edit(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback&) override;

// Make copies of self when requested.
operator std::function<std::unique_ptr<AppInstaller::Repository::ISourceFactory>()>();
Expand All @@ -207,9 +209,11 @@ namespace TestCommon
AddFunctor OnAdd;
UpdateFunctor OnUpdate;
RemoveFunctor OnRemove;
EditFunctor OnEdit;
};

bool AddSource(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback& progress);
bool EditSource(const AppInstaller::Repository::SourceDetails& details, AppInstaller::IProgressCallback& progress);
bool UpdateSource(std::string_view name, AppInstaller::IProgressCallback& progress);
bool RemoveSource(std::string_view name, AppInstaller::IProgressCallback& progress);
AppInstaller::Repository::Source OpenSource(std::string_view name, AppInstaller::IProgressCallback& progress);
Expand Down
Loading