diff --git a/doc/submit.md b/doc/submit.md index 945307ce..ed66d49a 100644 --- a/doc/submit.md +++ b/doc/submit.md @@ -16,6 +16,7 @@ The following arguments are available: | Argument | Description | |--------------|-------------| | **-p, --prtitle** | The title of the pull request submitted to GitHub. +| **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false. | **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. If you have provided your [GitHub token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) on the command line with the **submit** command and the device is registered with GitHub, **Winget-Create** will submit your PR to [Windows Package Manager repo](https://docs.microsoft.com/windows/package-manager/). diff --git a/src/WingetCreateCLI/Commands/SubmitCommand.cs b/src/WingetCreateCLI/Commands/SubmitCommand.cs index ad52adaa..58dc3313 100644 --- a/src/WingetCreateCLI/Commands/SubmitCommand.cs +++ b/src/WingetCreateCLI/Commands/SubmitCommand.cs @@ -44,10 +44,10 @@ public static IEnumerable Examples public string Path { get; set; } /// - /// Gets or sets the GitHub token used to submit a pull request on behalf of the user. + /// Gets or sets the previous version to replace from the Windows Package Manager repository. /// - [Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))] - public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; } + [Value(1, MetaName = "ReplaceVersion", Required = false, HelpText = "ReplaceVersion_HelpText", ResourceType = typeof(Resources))] + public string ReplaceVersion { get; set; } /// /// Gets or sets the title for the pull request. @@ -55,6 +55,18 @@ public static IEnumerable Examples [Option('p', "prtitle", Required = false, HelpText = "PullRequestTitle_HelpText", ResourceType = typeof(Resources))] public override string PRTitle { get => base.PRTitle; set => base.PRTitle = value; } + /// + /// Gets or sets a value indicating whether or not to replace a previous version of the manifest with the update. + /// + [Option('r', "replace", Required = false, HelpText = "ReplacePrevious_HelpText", ResourceType = typeof(Resources))] + public bool Replace { get; set; } + + /// + /// Gets or sets the GitHub token used to submit a pull request on behalf of the user. + /// + [Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))] + public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; } + /// /// Gets or sets the unbound arguments that exist after the first positional parameter. /// @@ -99,17 +111,30 @@ private async Task SubmitManifest() { string expandedPath = System.Environment.ExpandEnvironmentVariables(this.Path); + // TODO: Remove singleton support. if (File.Exists(expandedPath) && ValidateManifest(expandedPath)) { Manifests manifests = new Manifests(); manifests.SingletonManifest = Serialization.DeserializeFromPath(expandedPath); - return await this.GitHubSubmitManifests(manifests, this.PRTitle); + + if (this.Replace && !await this.ValidateReplaceArguments(manifests.SingletonManifest.PackageIdentifier, manifests.SingletonManifest.PackageVersion)) + { + return false; + } + + return await this.GitHubSubmitManifests(manifests, this.PRTitle, this.Replace, this.ReplaceVersion); } else if (Directory.Exists(expandedPath) && ValidateManifest(expandedPath)) { List manifestContents = Directory.GetFiles(expandedPath).Select(f => File.ReadAllText(f)).ToList(); Manifests manifests = Serialization.DeserializeManifestContents(manifestContents); - return await this.GitHubSubmitManifests(manifests, this.PRTitle); + + if (this.Replace && !await this.ValidateReplaceArguments(manifests.VersionManifest.PackageIdentifier, manifests.VersionManifest.PackageVersion)) + { + return false; + } + + return await this.GitHubSubmitManifests(manifests, this.PRTitle, this.Replace, this.ReplaceVersion); } else { @@ -117,5 +142,48 @@ private async Task SubmitManifest() return false; } } + + private async Task ValidateReplaceArguments(string packageId, string submitVersion) + { + string exactId; + try + { + exactId = await this.GitHubClient.FindPackageId(packageId); + } + catch (Octokit.RateLimitExceededException) + { + Logger.ErrorLocalized(nameof(Resources.RateLimitExceeded_Message)); + return false; + } + + if (string.IsNullOrEmpty(exactId)) + { + Logger.ErrorLocalized(nameof(Resources.ReplacePackageIdDoesNotExist_Error), packageId); + return false; + } + + if (!string.IsNullOrEmpty(this.ReplaceVersion)) + { + // If submit version is same as replace version, it's a regular update. + if (submitVersion == this.ReplaceVersion) + { + Logger.ErrorLocalized(nameof(Resources.ReplaceVersionEqualsSubmitVersion_ErrorMessage)); + return false; + } + + // Check if the replace version exists in the repository. + try + { + await this.GitHubClient.GetManifestContentAsync(packageId, this.ReplaceVersion); + } + catch (Octokit.NotFoundException) + { + Logger.ErrorLocalized(nameof(Resources.VersionDoesNotExist_Error), this.ReplaceVersion, packageId); + return false; + } + } + + return true; + } } } diff --git a/src/WingetCreateCLI/Commands/UpdateCommand.cs b/src/WingetCreateCLI/Commands/UpdateCommand.cs index a9961457..391b340e 100644 --- a/src/WingetCreateCLI/Commands/UpdateCommand.cs +++ b/src/WingetCreateCLI/Commands/UpdateCommand.cs @@ -138,6 +138,14 @@ public override async Task Execute() return false; } + bool submitFlagMissing = !this.SubmitToGitHub && (!string.IsNullOrEmpty(this.PRTitle) || this.Replace); + + if (submitFlagMissing) + { + Logger.WarnLocalized(nameof(Resources.SubmitFlagMissing_Warning)); + Console.WriteLine(); + } + Logger.DebugLocalized(nameof(Resources.RetrievingManifest_Message), this.Id); string exactId; @@ -172,7 +180,7 @@ public override async Task Execute() } catch (Octokit.NotFoundException) { - Logger.ErrorLocalized(nameof(Resources.VersionDoesNotExist_Error), this.Version, this.Id); + Logger.ErrorLocalized(nameof(Resources.VersionDoesNotExist_Error), this.ReplaceVersion, this.Id); return false; } } diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 68831f97..a3495ede 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -2265,6 +2265,15 @@ public static string RemoveLastItem_MenuItem { } } + /// + /// Looks up a localized string similar to Replace operation cannot be performed. Package identifier '{0}' does not exist in the Windows Package Manager repo.. + /// + public static string ReplacePackageIdDoesNotExist_Error { + get { + return ResourceManager.GetString("ReplacePackageIdDoesNotExist_Error", resourceCulture); + } + } + /// /// Looks up a localized string similar to Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false.. /// @@ -2283,6 +2292,15 @@ public static string ReplaceVersion_HelpText { } } + /// + /// Looks up a localized string similar to The replace version cannot be equal to the submit version.. + /// + public static string ReplaceVersionEqualsSubmitVersion_ErrorMessage { + get { + return ResourceManager.GetString("ReplaceVersionEqualsSubmitVersion_ErrorMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to The replace version cannot be equal to the update version.. /// @@ -2769,6 +2787,15 @@ public static string SubmitCommand_HelpText { } } + /// + /// Looks up a localized string similar to Submit arguments were provided. Did you forget to include the --submit, -s flag?. + /// + public static string SubmitFlagMissing_Warning { + get { + return ResourceManager.GetString("SubmitFlagMissing_Warning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Submitting pull request for manifest.... /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 68c9e23d..67a26139 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1237,4 +1237,15 @@ Indicates whether the installer is prohibited from being downloaded for offline installation + + Submit arguments were provided. Did you forget to include the --submit, -s flag? + '--submit, -s' refers to a command line switch argument + + + Replace operation cannot be performed. Package identifier '{0}' does not exist in the Windows Package Manager repo. + {0} - will be replaced with the ID of the package + + + The replace version cannot be equal to the submit version. + \ No newline at end of file diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs index 2db1b306..7610ffeb 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/UpdateCommandTests.cs @@ -212,6 +212,70 @@ public async Task UpdateFailsWithUnmatchedPackages() Assert.That(result, Does.Contain(Resources.NewInstallerUrlMustMatchExisting_Message), "New installer must match error should be thrown"); } + /// + /// Verify that update command warns if submit arguments are provided without submit flag being set. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task UpdateChecksMissingSubmitFlagWithReplace() + { + string packageId = "TestPublisher.TestPackageId"; + string version = "1.2.3.4"; + + UpdateCommand command = new UpdateCommand + { + Id = packageId, + Version = version, + InstallerUrls = new[] { "https://fakedomain.com/fakeinstaller.exe" }, + SubmitToGitHub = false, + Replace = true, + }; + + try + { + await command.Execute(); + } + catch (Exception) + { + // Expected exception + } + + string result = this.sw.ToString(); + Assert.That(result, Does.Contain(Resources.SubmitFlagMissing_Warning), "Submit flag missing warning should be shown"); + } + + /// + /// Verify that update command warns if submit arguments are provided without submit flag being set. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task UpdateChecksMissingSubmitFlagWithPRTitle() + { + string packageId = "TestPublisher.TestPackageId"; + string version = "1.2.3.4"; + + UpdateCommand command = new UpdateCommand + { + Id = packageId, + Version = version, + InstallerUrls = new[] { "https://fakedomain.com/fakeinstaller.exe" }, + SubmitToGitHub = false, + PRTitle = "Test PR Title", + }; + + try + { + await command.Execute(); + } + catch (Exception) + { + // Expected exception + } + + string result = this.sw.ToString(); + Assert.That(result, Does.Contain(Resources.SubmitFlagMissing_Warning), "Submit flag missing warning should be shown"); + } + /// /// Since some installers are incorrectly labeled on the manifest, resort to using the installer URL to find matches. /// This unit test uses a msi installer that is not an arm64 installer, but because the installer URL includes "arm64", it should find a match.