Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -393,3 +393,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
},
```
4 changes: 4 additions & 0 deletions src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ namespace AppInstaller::CLI
return { type, "explicit"_liv };
case Execution::Args::Type::SourceTrustLevel:
return { type, "trust-level"_liv };
case Execution::Args::Type::SourceEditExplicit:
return { type, "explicit"_liv, 'e' };

//Hash Command
case Execution::Args::Type::HashFile:
Expand Down Expand Up @@ -410,6 +412,8 @@ namespace AppInstaller::CLI
return Argument{ type, Resource::String::SourceTypeArgumentDescription, ArgumentType::Positional };
case Args::Type::SourceExplicit:
return Argument{ type, Resource::String::SourceExplicitArgumentDescription, ArgumentType::Flag };
case Args::Type::SourceEditExplicit:
return Argument{ type, Resource::String::SourceEditExplicitArgumentDescription, ArgumentType::Positional };
case Args::Type::SourceTrustLevel:
return Argument{ type, Resource::String::SourceTrustLevelArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help };
case Args::Type::ValidateManifest:
Expand Down
52 changes: 52 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,55 @@ 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::SourceEditExplicit),
};
}

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::ValidateArgumentsInternal(Execution::Args& execArgs) const
{
if (execArgs.Contains(Execution::Args::Type::SourceEditExplicit))
{
std::string_view explicitArg = execArgs.GetArg(Execution::Args::Type::SourceEditExplicit);
auto convertedArg = Utility::TryConvertStringToBool(explicitArg);
if (!convertedArg.has_value())
{
auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{
"true"_lis,
"false"_lis,
});
throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::SourceEditExplicit).Name(), validOptions));
}
}
}

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

struct SourceEditCommand final : public Command
{
SourceEditCommand(std::string_view parent) : Command("edit", { "config", "set" }, 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 ValidateArgumentsInternal(Execution::Args& execArgs) const override;
void ExecuteInternal(Execution::Context& context) const override;
};
}
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/ExecutionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ namespace AppInstaller::CLI::Execution
ForceSourceReset,
SourceExplicit,
SourceTrustLevel,
SourceEditExplicit,

//Hash Command
HashFile,
Expand Down
7 changes: 7 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,13 @@ 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(SourceEditExplicitArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditNewValue);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditNoChanges);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditOldValue);
WINGET_DEFINE_RESOURCE_STRINGID(SourceEditOne);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription);
Expand Down
34 changes: 34 additions & 0 deletions src/AppInstallerCLICore/Workflows/SourceFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,40 @@ 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>();

for (const auto& sd : sources)
{
// Get the current source with this name.
Repository::Source targetSource{ sd.Name };
auto oldExplicitValue = sd.Explicit;

std::optional<bool> isExplicit;
if (context.Args.Contains(Execution::Args::Type::SourceEditExplicit))
{
isExplicit = Utility::TryConvertStringToBool(context.Args.GetArg(Execution::Args::Type::SourceEditExplicit));
}

Repository::SourceEdit edits{ isExplicit };
if (!targetSource.RequiresChanges(edits))
{
context.Reporter.Info() << Resource::String::SourceEditNoChanges(Utility::LocIndView{ sd.Name }) << std::endl;
continue;
}

context.Reporter.Info() << Resource::String::SourceEditOne(Utility::LocIndView{ sd.Name }) << std::endl;
targetSource.Edit(edits);

// Output updated source information. Since only Explicit is editable, we will only list that field. The name of the source being edited is listed prior to the edits.
Execution::TableOutput<3> table(context.Reporter, { Resource::String::SourceListField, Resource::String::SourceEditOldValue, Resource::String::SourceEditNewValue });
table.OutputLine({ Resource::LocString(Resource::String::SourceListExplicit), std::string{ Utility::ConvertBoolToString(oldExplicitValue) }, std::string{ Utility::ConvertBoolToString(isExplicit.value()) } });
table.Complete();
}
}

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public void DisableWinGetPolicy()
Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message);
Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult);

groupPolicyException = Assert.Catch<GroupPolicyException>(() => { EditPackageCatalogOptions packageManagerSettings = this.TestFactory.CreateEditPackageCatalogOptions(); });
Assert.AreEqual(Constants.BlockByWinGetPolicyErrorMessage, groupPolicyException.Message);
Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, groupPolicyException.HResult);

// PackageManagerSettings is not implemented in context OutOfProcDev
if (this.TestFactory.Context == ClsidContext.InProc)
{
Expand Down
77 changes: 77 additions & 0 deletions src/AppInstallerCLIE2ETests/Interop/PackageCatalogInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,63 @@ public async Task RemoveNonExistingPackageCatalog()
await this.RemoveAndValidatePackageCatalogAsync(removePackageCatalogOptions, RemovePackageCatalogStatus.InvalidOptions, Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST);
}

/// <summary>
/// Edit package catalog with invalid options.
/// </summary>
[Test]
public void EditPackageCatalogWithInvalidOptions()
{
// Edit package catalog with null options.
Assert.Throws<NullReferenceException>(() => this.packageManager.EditPackageCatalog(null));

// Edit package catalog with empty options.
Assert.Throws<ArgumentException>(() => this.packageManager.EditPackageCatalog(this.TestFactory.CreateEditPackageCatalogOptions()));
}

/// <summary>
/// Edit a package catalog that is not present.
/// </summary>
[Test]
public void EditNonExistingPackageCatalog()
{
EditPackageCatalogOptions editPackageCatalogOptions = this.TestFactory.CreateEditPackageCatalogOptions();
editPackageCatalogOptions.Name = Constants.TestSourceName;

this.EditAndValidatePackageCatalog(editPackageCatalogOptions, EditPackageCatalogStatus.InvalidOptions, Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST);
}

/// <summary>
/// Edit a package catalog that is not present.
/// </summary>
/// <returns>representing the asynchronous unit test.</returns>
[Test]
public async Task AddEditRemovePackageCatalog()
{
// Add
AddPackageCatalogOptions options = this.TestFactory.CreateAddPackageCatalogOptions();
options.SourceUri = Constants.TestSourceUrl;
options.Name = Constants.TestSourceName;
options.TrustLevel = PackageCatalogTrustLevel.Trusted;

await this.AddAndValidatePackageCatalogAsync(options, AddPackageCatalogStatus.Ok);

// Edit
EditPackageCatalogOptions editOptions = this.TestFactory.CreateEditPackageCatalogOptions();
editOptions.Name = Constants.TestSourceName;
editOptions.Explicit = OptionalBoolean.False;
this.EditAndValidatePackageCatalog(editOptions, EditPackageCatalogStatus.Ok);

// Remove
RemovePackageCatalogOptions removePackageCatalogOptions = this.TestFactory.CreateRemovePackageCatalogOptions();
removePackageCatalogOptions.Name = Constants.TestSourceName;
var removeCatalogResult = await this.packageManager.RemovePackageCatalogAsync(removePackageCatalogOptions);
Assert.IsNotNull(removeCatalogResult);
Assert.AreEqual(RemovePackageCatalogStatus.Ok, removeCatalogResult.Status);

var testSource = this.packageManager.GetPackageCatalogByName(Constants.TestSourceName);
Assert.IsNull(testSource);
}

/// <summary>
/// Test class Tear down.
/// </summary>
Expand Down Expand Up @@ -324,5 +381,25 @@ private async Task RemoveAndValidatePackageCatalogAsync(RemovePackageCatalogOpti
var packageCatalog = this.packageManager.GetPackageCatalogByName(removePackageCatalogOptions.Name);
Assert.IsNull(packageCatalog);
}

private void EditAndValidatePackageCatalog(EditPackageCatalogOptions editPackageCatalogOptions, EditPackageCatalogStatus expectedStatus, int expectedErrorCode = 0)
{
var editCatalogResult = this.packageManager.EditPackageCatalog(editPackageCatalogOptions);
Assert.IsNotNull(editCatalogResult);
Assert.AreEqual(expectedStatus, editCatalogResult.Status);

if (expectedStatus != EditPackageCatalogStatus.Ok && expectedErrorCode != 0)
{
Assert.AreEqual(expectedErrorCode, editCatalogResult.ExtendedErrorCode.HResult);
return;
}

// Verify edits are correct.
var packageCatalog = this.packageManager.GetPackageCatalogByName(editPackageCatalogOptions.Name);
if (editPackageCatalogOptions.Explicit != OptionalBoolean.Unspecified)
{
Assert.AreEqual(packageCatalog.Info.Explicit, editPackageCatalogOptions.Explicit == OptionalBoolean.True);
}
}
}
}
Loading
Loading