diff --git a/.gitignore b/.gitignore index 66ab88a2..4461211c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ /**/bin /**/obj /packages -/tools -!/tools/packages.config \ No newline at end of file +/tools \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index c43914f1..2ed08cdb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,9 @@ - 7.3 - 0.5.0 + 8.0 + enable + 0.6.0 diff --git a/Directory.Build.targets b/Directory.Build.targets index d81cd43f..d656d0ae 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,16 +1,17 @@ - - - - + + + + + - + - + diff --git a/GbUtil.ruleset b/GbUtil.ruleset index ea18c5e7..1c84e3f3 100644 --- a/GbUtil.ruleset +++ b/GbUtil.ruleset @@ -6,6 +6,7 @@ + @@ -22,10 +23,12 @@ + + diff --git a/GbUtil.sln b/GbUtil.sln index c373a691..7e804cdd 100644 --- a/GbUtil.sln +++ b/GbUtil.sln @@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt Directory.Build.targets = Directory.Build.targets global.json = global.json install.bat = install.bat - nuget.config = nuget.config tools\packages.config = tools\packages.config README.md = README.md EndProjectSection diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index f4a755ab..c4e13b70 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ Utilities for GitBucket [![Build status](https://ci.appveyor.com/api/projects/status/q1hfisqpa09662l5/?svg=true)](https://ci.appveyor.com/project/SIkebe/gitbucket-utility/) ## Requirements -* [.NET Core 2.2 SDK](https://www.microsoft.com/net/download/windows) -* GitBucket 4.30.0+ (using PostgreSQL as backend DB) +* [.NET Core 2.1.X SDK](https://www.microsoft.com/net/download/windows) +* GitBucket 4.31.X+ (using PostgreSQL as backend DB) ## Preparation ```cmd -dotnet tool install --global gbutil --version 0.5.0 +dotnet tool install --global gbutil --version 0.6.0 setx ConnectionStrings:GitBucketConnection Host=host;Username=username;Password=password;Database=gitbucket setx GitBucketUri http://localhost:8080/gitbucket/api/v3/ ``` @@ -18,13 +18,13 @@ setx GitBucketUri http://localhost:8080/gitbucket/api/v3/ ### `gbutil issue -t move` ```powershell -gbutil issue -t move [-s|--source] [-d|--destination] [-n|--number] +gbutil issue -s|--source -d|--destination -n|--number [-t move] ``` Move issues between repositories. ``` -> gbutil issue -t move -s root/test1 -d root/test2 -n 1 +> gbutil issue -s root/test1 -d root/test2 -n 1 -t move Enter your Username: root Enter your Password: **** The issue has been successfully moved to http://localhost:8080/gitbucket/root/test2/issues/35. @@ -37,15 +37,14 @@ Close the original one manually. |`-t`|`--type`|`false`|The type of issue options. Default value is "move".| |`-s`|`--source`|`true`|The source owner and repository to move from. Use "/" for separator like "root/repository1".| |`-d`|`--destination`|`true`|The destination owner and repository to move to. Use "/" for separator like "root/repository2".| -|`-n`|`--number`|`false`|The issue numbers to move. Use ":" for separator.| +|`-n`|`--number`|`true`|The issue numbers to move. Use ":" for separator.| ----- - ### `gbutil issue -t copy` ```powershell -gbutil issue -t copy [-s|--source] [-d|--destination] [-n|--number] +gbutil issue -t copy -s|--source -d|--destination -n|--number ``` Copy issues between repositories. @@ -63,13 +62,13 @@ The issue has been successfully copied to http://localhost:8080/gitbucket/root/t |`-t`|`--type`|`true`|The type of issue options. Default value is "move".| |`-s`|`--source`|`true`|The source owner and repository to copy from. Use "/" for separator like "root/repository1".| |`-d`|`--destination`|`true`|The destination owner and repository to copy to. Use "/" for separator like "root/repository2".| -|`-n`|`--number`|`false`|The issue numbers to copy. Use ":" for separator.| +|`-n`|`--number`|`true`|The issue numbers to copy. Use ":" for separator.| ----- ### `gbutil milestone` ```powershell -gbutil milestone [-o|--owner] [-r|--repository] [-c|--includeClosed] +gbutil milestone -o|--owner -r|--repository [-c|--includeClosed] ``` Show unclosed (by default) milestones. @@ -92,10 +91,12 @@ There are 3 open milestones. ----- ### `gbutil release` + ```powershell -gbutil release [-o|--owner] [-r|--repository] [-m|--miliestone] [-t|--target] +gbutil release -o|--owner -r|--repository -m|--miliestone [--from-pr] [--create-pr] ``` -Output a release note in markdown which lists issues or pull requests of the repository related to the milistone. +Output a release note in markdown which lists issues of the repository related to the milistone. +If `--create-pr` option is specified, gbutil automatically creates Pull Request instead of console output. ```powershell > gbutil release -o ikebe -r RepeatableTimer -m v0.1.0 @@ -109,6 +110,9 @@ The highest priority among them is "very high". ### Enhancement * Allow one time/repeat options #1 * Add validation #3 + +> gbutil release -o ikebe -r RepeatableTimer -m v0.1.0 --create-pr +A new pull request has been successfully created! ``` ### Options @@ -117,4 +121,8 @@ The highest priority among them is "very high". |`-o`|`--owner`|`true`|The owner name of the repository.| |`-r`|`--repository`|`true`|The repository name.| |`-m`|`--milestone`|`true`|The milestone to publish a release note.| -|`-t`|`--target`|`false`|The switch whether publish a release note based on issues or pull requests.
Predefined values are "issues" or "pullrequests".
Default value is "issues".| +|-|`--from-pr`|`false`|If specified, gbutil publish a release note based on pull requests.| +|-|`--create-pr`|`false`|If specified, gbutil automatically creates a pull request.| +|`-b`|`--base`|`false`|The name of the branch you want the changes pulled into. Default value is "master".| +|`-h`|`--head`|`false`|The name of the branch where your changes are implemented. Default value is "develop".| +|`-t`|`--title`|`false`|The title of the new pull request. Default value is the same as milestone.| \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 135765cd..044557ba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ init: # Build script build_script: - - ps: .\build.ps1 -target Run-Unit-Tests + - ps: .\build.ps1 --target="Run-Unit-Tests" # Tests test: off @@ -23,4 +23,4 @@ branches: # Build cache cache: -- tools -> build.cake \ No newline at end of file +- tools -> build.cake, build.config \ No newline at end of file diff --git a/build.cake b/build.cake index c04e98bf..76d35dee 100644 --- a/build.cake +++ b/build.cake @@ -1,4 +1,3 @@ -#addin nuget:?package=Cake.Incubator&version=3.0.0 ////////////////////////////////////////////////////////////////////// // ARGUMENTS ////////////////////////////////////////////////////////////////////// @@ -32,10 +31,10 @@ Task("Restore") Task("Build") .IsDependentOn("Clean") - .DoesForEach(GetFiles("./src/**/*.csproj"), (project) => + .Does(() => { DotNetCoreBuild( - project.FullPath, + "./GbUtil.sln", new DotNetCoreBuildSettings { Configuration = configuration @@ -44,10 +43,10 @@ Task("Build") Task("Run-Unit-Tests") .IsDependentOn("Clean") - .DoesForEach(GetFiles("./src/*.Tests/*.csproj"), (project) => + .Does(() => { DotNetCoreTest( - project.FullPath, + "./GbUtil.sln", new DotNetCoreTestSettings { Configuration = configuration @@ -78,7 +77,7 @@ Task("Publish") } DotNetCoreNuGetPush( - "./packages/GbUtil.0.5.0.nupkg", + "./packages/GbUtil.0.6.0.nupkg", new DotNetCoreNuGetPushSettings { ApiKey = apiKey, diff --git a/build.config b/build.config new file mode 100644 index 00000000..5a180f1e --- /dev/null +++ b/build.config @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +CAKE_VERSION=0.33.0 +DOTNET_VERSION=3.0.100-preview4-011223 \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index be4989d7..91d2eaae 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,235 +1,148 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER ShowDescription -Shows description about tasks. -.PARAMETER DryRun -Performs a dry run. -.PARAMETER Experimental -Uses the nightly builds of the Roslyn script engine. -.PARAMETER Mono -Uses the Mono Compiler rather than the Roslyn script engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -https://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [switch]$Experimental, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) +#!/usr/bin/env pwsh +$DotNetInstallerUri = 'https://dot.net/v1/dotnet-install.ps1'; +$DotNetUnixInstallerUri = 'https://dot.net/v1/dotnet-install.sh' +$DotNetChannel = 'LTS' +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +[string] $CakeVersion = '' +[string] $DotNetVersion= '' +foreach($line in Get-Content (Join-Path $PSScriptRoot 'build.config')) { - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } - - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } + if ($line -like 'CAKE_VERSION=*') { + $CakeVersion = $line.SubString(13) + } + elseif ($line -like 'DOTNET_VERSION=*') { + $DotNetVersion =$line.SubString(15) + } } -function GetProxyEnabledWebClient -{ - $wc = New-Object System.Net.WebClient - $proxy = [System.Net.WebRequest]::GetSystemWebProxy() - $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials - $wc.Proxy = $proxy - return $wc -} - -Write-Host "Preparing to run build script..." -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +if ([string]::IsNullOrEmpty($CakeVersion) -or [string]::IsNullOrEmpty($DotNetVersion)) { + 'Failed to parse Cake / .NET Core SDK Version' + exit 1 } -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" -$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" -$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" -$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" - # Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null +$ToolPath = Join-Path $PSScriptRoot "tools" +if (!(Test-Path $ToolPath)) { + Write-Verbose "Creating tools directory..." + New-Item -Path $ToolPath -Type Directory -Force | out-null } -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { - Throw "Could not download packages.config." - } -} -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select-Object -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." +if ($PSVersionTable.PSEdition -ne 'Core') { + # Attempt to set highest encryption available for SecurityProtocol. + # PowerShell will not set this by default (until maybe .NET 4.6.x). This + # will typically produce a message for PowerShell v2 (just an info + # message though) try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } + # Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48) + # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't + # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is + # installed (.NET 4.5 is an in-place upgrade). + [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 + } catch { + Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3' + } } -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | - Remove-Item -Recurse - } +########################################################################### +# INSTALL .NET CORE CLI +########################################################################### - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" +Function Remove-PathVariable([string]$VariableToRemove) +{ + $SplitChar = ';' + if ($IsMacOS -or $IsLinux) { + $SplitChar = ':' + } - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet tools." + $path = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($path -ne $null) + { + $newItems = $path.Split($SplitChar, [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove } + [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join($SplitChar, $newItems), "User") } - else + + $path = [Environment]::GetEnvironmentVariable("PATH", "Process") + if ($path -ne $null) { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + $newItems = $path.Split($SplitChar, [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove } + [Environment]::SetEnvironmentVariable("PATH", [System.String]::Join($SplitChar, $newItems), "Process") } - Write-Verbose -Message ($NuGetOutput | out-string) +} - Pop-Location +# Get .NET Core CLI path if installed. +$FoundDotNetCliVersion = $null; +if (Get-Command dotnet -ErrorAction SilentlyContinue) { + $FoundDotNetCliVersion = dotnet --version; } -# Restore addins from NuGet -if (Test-Path $ADDINS_PACKAGES_CONFIG) { - Push-Location - Set-Location $ADDINS_DIR +if($FoundDotNetCliVersion -ne $DotNetVersion) { + $InstallPath = Join-Path $PSScriptRoot ".dotnet" + if (!(Test-Path $InstallPath)) { + New-Item -Path $InstallPath -ItemType Directory -Force | Out-Null; + } - Write-Verbose -Message "Restoring addins from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" + if ($IsMacOS -or $IsLinux) { + $ScriptPath = Join-Path $InstallPath 'dotnet-install.sh' + (New-Object System.Net.WebClient).DownloadFile($DotNetUnixInstallerUri, $ScriptPath); + & bash $ScriptPath --version "$DotNetVersion" --install-dir "$InstallPath" --channel "$DotNetChannel" --no-path - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet addins." + Remove-PathVariable "$InstallPath" + $env:PATH = "$($InstallPath):$env:PATH" } + else { + $ScriptPath = Join-Path $InstallPath 'dotnet-install.ps1' + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallerUri, $ScriptPath); + & $ScriptPath -Channel $DotNetChannel -Version $DotNetVersion -InstallDir $InstallPath; - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location + Remove-PathVariable "$InstallPath" + $env:PATH = "$InstallPath;$env:PATH" + } } -# Restore modules from NuGet -if (Test-Path $MODULES_PACKAGES_CONFIG) { - Push-Location - Set-Location $MODULES_DIR +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT=1 - Write-Verbose -Message "Restoring modules from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet modules." - } +########################################################################### +# INSTALL CAKE +########################################################################### - Write-Verbose -Message ($NuGetOutput | out-string) +# Make sure Cake has been installed. +[string] $CakeExePath = '' +[string] $CakeInstalledVersion = Get-Command dotnet-cake -ErrorAction SilentlyContinue | ForEach-Object {&$_.Source --version} - Pop-Location +if ($CakeInstalledVersion -eq $CakeVersion) { + # Cake found locally + $CakeExePath = (Get-Command dotnet-cake).Source } +else { + $CakePath = [System.IO.Path]::Combine($ToolPath,'.store', 'cake.tool', $CakeVersion) # Old PowerShell versions Join-Path only supports one child path + + $CakeExePath = (Get-ChildItem -Path $ToolPath -Filter "dotnet-cake*" -File| ForEach-Object FullName | Select-Object -First 1) -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} + if ((!(Test-Path -Path $CakePath -PathType Container)) -or (!(Test-Path $CakeExePath -PathType Leaf))) { + if ((![string]::IsNullOrEmpty($CakeExePath)) -and (Test-Path $CakeExePath -PathType Leaf)) + { + & dotnet tool uninstall --tool-path $ToolPath Cake.Tool + } -# Build Cake arguments -$cakeArguments = @("$Script"); -if ($Target) { $cakeArguments += "-target=$Target" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -if ($Experimental) { $cakeArguments += "-experimental" } -if ($Mono) { $cakeArguments += "-mono" } -$cakeArguments += $ScriptArgs + & dotnet tool install --tool-path $ToolPath --version $CakeVersion Cake.Tool + if ($LASTEXITCODE -ne 0) + { + 'Failed to install cake' + exit 1 + } + $CakeExePath = (Get-ChildItem -Path $ToolPath -Filter "dotnet-cake*" -File| ForEach-Object FullName | Select-Object -First 1) + } +} -# Start Cake -Write-Host "Running build script..." -&$CAKE_EXE $cakeArguments -exit $LASTEXITCODE +########################################################################### +# RUN BUILD SCRIPT +########################################################################### +& "$CakeExePath" ./build.cake $args +exit $LASTEXITCODE \ No newline at end of file diff --git a/global.json b/global.json index 14c55b7d..19779d74 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "2.2.100" + "version": "3.0.100-preview4-011223" } } \ No newline at end of file diff --git a/install.bat b/install.bat index b98afe13..c1777303 100644 --- a/install.bat +++ b/install.bat @@ -1 +1 @@ -dotnet tool install gbutil -g --add-source .\packages\GbUtil.0.5.0.nupkg --version 0.5.0 \ No newline at end of file +dotnet tool update -g gbutil --add-source ./packages --version 0.6.0 \ No newline at end of file diff --git a/src/GbUtil/Extensions/IServiceCollectionExtensions.cs b/src/GbUtil/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..00ec09ac --- /dev/null +++ b/src/GbUtil/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace GbUtil.Extensions +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddScopedIf( + this IServiceCollection services, + bool condition, + Func implementationFactory) + where TService : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (implementationFactory == null) + { + throw new ArgumentNullException(nameof(implementationFactory)); + } + + if (condition) + { + return services.AddScoped(typeof(TService), implementationFactory); + } + + return services; + } + + public static IServiceCollection AddTransientIf( + this IServiceCollection services, + bool condition) + where TService : class + where TImplementation : class, TService + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (condition) + { + return services.AddTransient(typeof(TService), typeof(TImplementation)); + } + + return services; + } + } +} diff --git a/src/GbUtil/GbUtil.csproj b/src/GbUtil/GbUtil.csproj index 9d3a6776..957d5fc0 100644 --- a/src/GbUtil/GbUtil.csproj +++ b/src/GbUtil/GbUtil.csproj @@ -9,8 +9,7 @@ gitbucket-utility MIT false - https://github.com/SIkebe/gitbucket-utility/blob/master/LICENSE - https://github.com/SIkebe/gitbucket-utility/blob/master/README.md + LICENSE.txt true true true @@ -35,5 +34,9 @@ PreserveNewest - + + + + +
diff --git a/src/GbUtil/InvalidConfigurationException.cs b/src/GbUtil/InvalidConfigurationException.cs new file mode 100644 index 00000000..9b8d672c --- /dev/null +++ b/src/GbUtil/InvalidConfigurationException.cs @@ -0,0 +1,21 @@ +using System; + +namespace GbUtil +{ + public class InvalidConfigurationException : Exception + { + public InvalidConfigurationException() + { + } + + public InvalidConfigurationException(string message) + : base(message) + { + } + + public InvalidConfigurationException(string message, Exception inner) + : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/src/GbUtil/Program.cs b/src/GbUtil/Program.cs index 5cfbc41d..91d421bc 100644 --- a/src/GbUtil/Program.cs +++ b/src/GbUtil/Program.cs @@ -1,10 +1,11 @@ using System; using System.IO; +using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using CommandLine; +using GbUtil.Extensions; using GitBucket.Core; using GitBucket.Data.Repositories; using GitBucket.Service; @@ -18,7 +19,7 @@ namespace GbUtil { public sealed class Program { - public static async Task Main(string[] args) + public static async Task Main(string[] args) { IConfiguration configuration; IConsole console = new GbUtilConsole(); @@ -30,104 +31,120 @@ public static async Task Main(string[] args) .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build(); +#nullable disable - var connectionString = configuration.GetConnectionString("GitBucketConnection"); - if (string.IsNullOrEmpty(connectionString)) - { - console.WriteWarnLine("PostgreSQL ConnectionString is not configured. Add \"ConnectionStrings: GitBucketConnection\" environment variable."); - return; - } - - var gitbucketUri = configuration.GetSection("GitBucketUri")?.Value; - if (string.IsNullOrEmpty(gitbucketUri)) + // TODO: CommandLineOptionsBase? does not work here... + CommandLineOptionsBase options = Parser.Default.ParseArguments(args) + .WithNotParsed(errors => + { + if (errors.Any(e => + e.Tag != ErrorType.HelpVerbRequestedError && + e.Tag != ErrorType.VersionRequestedError && + e.Tag != ErrorType.NoVerbSelectedError)) + { + throw new InvalidConfigurationException($"Failed to parse arguments."); + } + }) + .MapResult( + (ReleaseOptions options) => (CommandLineOptionsBase)options, + (MilestoneOptions options) => options, + (IssueOptions options) => options, + _ => null + ); +#nullable restore + + // In case of default verbs (--help or --version) + if (options == null) { - console.WriteWarnLine("GitBucket URI is not configured. Add \"GitBucketUri\" environment variable."); - return; + return 0; } - var serviceProvider = new ServiceCollection() - .AddScoped(_ => new GitBucketDbContext(connectionString)) - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .BuildServiceProvider(); - - using (var scope = serviceProvider.CreateScope()) + var requireDbConnection = options is ReleaseOptions || options is MilestoneOptions; + using var scope = CreateServiceProvider(configuration, requireDbConnection).CreateScope(); + var result = options switch { - var provider = scope.ServiceProvider; - await Parser.Default.ParseArguments(args) - .MapResult( - (ReleaseOptions options) => provider.GetRequiredService().OutputReleaseNotes(options), - (MilestoneOptions options) => provider.GetRequiredService().ShowMilestones(options), - (IssueOptions options) => - { - console.Write("Enter your Username: "); - string user = console.ReadLine(); - if (string.IsNullOrEmpty(user)) - { - return Task.FromResult(1); - } - - console.Write("Enter your Password: "); - string password = GetPasswordFromConsole(); - if (string.IsNullOrEmpty(password)) - { - return Task.FromResult(1); - } - - var client = new GitHubClient( - new Connection( - new ProductHeaderValue("gbutil"), - new Uri(gitbucketUri), - new InMemoryCredentialStore(new Credentials(user, password)), - new HttpClientAdapter(() => new GitBucketMessageHandler()), - new SimpleJsonSerializer() - )); - - return provider.GetRequiredService().Execute(options, client); - }, - errs => Task.FromResult(-1)); - } + ReleaseOptions releaseOptions + => await scope.ServiceProvider.GetRequiredService().Execute(releaseOptions, CreateGitBucketClient(configuration, console)), + MilestoneOptions milestoneOptions + => await scope.ServiceProvider.GetRequiredService().ShowMilestones(milestoneOptions), + IssueOptions issueOptions + => await scope.ServiceProvider.GetRequiredService().Execute(issueOptions, CreateGitBucketClient(configuration, console)), + _ => 1 + }; + + return result; } + catch (InvalidConfigurationException ex) + { + console.WriteWarnLine(ex.Message); + return 1; + } +#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) { console.WriteErrorLine(ex.Message); console.WriteErrorLine(ex.StackTrace); + return 1; } +#pragma warning restore CA1031 // Do not catch general exception types } - private static string GetPasswordFromConsole() + private static ServiceProvider CreateServiceProvider( + IConfiguration configuration, + bool requireDbConnection = false) { - var builder = new StringBuilder(); - while (true) + string connectionString = ""; + if (requireDbConnection) { - var consoleKeyInfo = Console.ReadKey(true); - if (consoleKeyInfo.Key == ConsoleKey.Enter) + connectionString = configuration.GetConnectionString("GitBucketConnection"); + if (string.IsNullOrEmpty(connectionString)) { - Console.WriteLine(); - break; + throw new InvalidConfigurationException("PostgreSQL ConnectionString is not configured. Add \"ConnectionStrings: GitBucketConnection\" environment variable."); } + } - if (consoleKeyInfo.Key == ConsoleKey.Backspace && builder.Length > 0) - { - if (builder.Length > 0) - { - Console.Write("\b\0\b"); - builder.Length--; - } + return new ServiceCollection() + .AddScopedIf(requireDbConnection, _ => new GitBucketDbContext(connectionString)) + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransientIf(requireDbConnection) + .AddTransientIf(requireDbConnection) + .AddTransientIf(requireDbConnection) + .AddTransient() + .BuildServiceProvider(); + } - continue; - } + private static IGitHubClient CreateGitBucketClient(IConfiguration configuration, IConsole console) + { + var gitbucketUri = configuration.GetSection("GitBucketUri")?.Value; + if (string.IsNullOrEmpty(gitbucketUri)) + { + throw new InvalidConfigurationException("GitBucket URI is not configured. Add \"GitBucketUri\" environment variable."); + } - Console.Write('*'); - builder.Append(consoleKeyInfo.KeyChar); + console.Write("Enter your Username: "); + string user = console.ReadLine(); + if (string.IsNullOrEmpty(user)) + { + throw new InvalidConfigurationException("Username is required"); + } + + console.Write("Enter your Password: "); + string password = console.GetPassword(); + if (string.IsNullOrEmpty(password)) + { + throw new InvalidConfigurationException("Password is required"); } - return builder.ToString(); + return new GitHubClient( + new Connection( + new ProductHeaderValue("gbutil"), + new Uri(gitbucketUri), + new InMemoryCredentialStore(new Credentials(user, password)), + new HttpClientAdapter(() => new GitBucketMessageHandler()), + new SimpleJsonSerializer() + )); } } @@ -138,13 +155,17 @@ public GitBucketMessageHandler() : base(new HttpClientHandler()) } protected async override Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + HttpRequestMessage request, + CancellationToken cancellationToken = default) { - var contentType = request?.Content?.Headers.ContentType.MediaType; - if (contentType == "application/x-www-form-urlencoded") + if (request != null && request.Content != null) { - // GitBucket doesn't accept Content-Type: application/x-www-form-urlencoded - request.Content.Headers.ContentType.MediaType = "application/json"; + var contentType = request.Content.Headers.ContentType.MediaType; + if (contentType == "application/x-www-form-urlencoded") + { + // GitBucket doesn't accept Content-Type: application/x-www-form-urlencoded + request.Content.Headers.ContentType.MediaType = "application/json"; + } } return await base.SendAsync(request, cancellationToken); diff --git a/src/GbUtil/runtimeconfig.template.json b/src/GbUtil/runtimeconfig.template.json new file mode 100644 index 00000000..aea74491 --- /dev/null +++ b/src/GbUtil/runtimeconfig.template.json @@ -0,0 +1,6 @@ +{ + // Rollforward across major versions of .NET Core + // The default setting will only rollforward across minor versions + // https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/roll-forward-on-no-candidate-fx.md + "rollForwardOnNoCandidateFx": 2 +} \ No newline at end of file diff --git a/src/GitBucket.Core/GbUtilConsole.cs b/src/GitBucket.Core/GbUtilConsole.cs index 80349f1c..edd726e6 100644 --- a/src/GitBucket.Core/GbUtilConsole.cs +++ b/src/GitBucket.Core/GbUtilConsole.cs @@ -1,5 +1,6 @@ using System; - +using System.Text; + namespace GitBucket.Core { /// @@ -31,7 +32,6 @@ public void WriteLine(string value) public void WriteWarn(string value) { - var color = ForegroundColor; ForegroundColor = ConsoleColor.Yellow; Console.Write(value); ResetColor(); @@ -39,7 +39,6 @@ public void WriteWarn(string value) public void WriteWarnLine(string value) { - var color = ForegroundColor; ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(value); ResetColor(); @@ -47,7 +46,6 @@ public void WriteWarnLine(string value) public void WriteError(string value) { - var color = ForegroundColor; ForegroundColor = ConsoleColor.Red; Console.Write(value); ResetColor(); @@ -55,7 +53,6 @@ public void WriteError(string value) public void WriteErrorLine(string value) { - var color = ForegroundColor; ForegroundColor = ConsoleColor.Red; Console.WriteLine(value); ResetColor(); @@ -64,5 +61,35 @@ public void WriteErrorLine(string value) public void ResetColor() => Console.ResetColor(); public string ReadLine() => Console.ReadLine(); + + public string GetPassword() + { + var builder = new StringBuilder(); + while (true) + { + var consoleKeyInfo = Console.ReadKey(true); + if (consoleKeyInfo.Key == ConsoleKey.Enter) + { + Console.WriteLine(); + break; + } + + if (consoleKeyInfo.Key == ConsoleKey.Backspace && builder.Length > 0) + { + if (builder.Length > 0) + { + Console.Write("\b\0\b"); + builder.Length--; + } + + continue; + } + + Console.Write('*'); + builder.Append(consoleKeyInfo.KeyChar); + } + + return builder.ToString(); + } } } \ No newline at end of file diff --git a/src/GitBucket.Core/GitBucket.Core.csproj b/src/GitBucket.Core/GitBucket.Core.csproj index 907b0040..bd705e49 100644 --- a/src/GitBucket.Core/GitBucket.Core.csproj +++ b/src/GitBucket.Core/GitBucket.Core.csproj @@ -2,6 +2,9 @@ netstandard2.0 + + + disable CA2227 diff --git a/src/GitBucket.Core/GitBucketDbContext.cs b/src/GitBucket.Core/GitBucketDbContext.cs index b289b1f3..287eccf2 100644 --- a/src/GitBucket.Core/GitBucketDbContext.cs +++ b/src/GitBucket.Core/GitBucketDbContext.cs @@ -8,6 +8,11 @@ public partial class GitBucketDbContext : DbContext public GitBucketDbContext(string connectionString) => ConnectionString = connectionString; + public GitBucketDbContext(DbContextOptions options) + : base(options) + { + } + public virtual DbSet AccessToken { get; set; } public virtual DbSet Account { get; set; } public virtual DbSet AccountExtraMailAddress { get; set; } diff --git a/src/GitBucket.Core/IConsole.cs b/src/GitBucket.Core/IConsole.cs index a1c8d579..d83e0a6f 100644 --- a/src/GitBucket.Core/IConsole.cs +++ b/src/GitBucket.Core/IConsole.cs @@ -16,5 +16,6 @@ public interface IConsole void WriteErrorLine(string value); void ResetColor(); string ReadLine(); + string GetPassword(); } } \ No newline at end of file diff --git a/src/GitBucket.Core/ReleaseNoteTarget.cs b/src/GitBucket.Core/ReleaseNoteTarget.cs deleted file mode 100644 index c79d0f85..00000000 --- a/src/GitBucket.Core/ReleaseNoteTarget.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GitBucket.Core -{ - public enum ReleaseNoteTarget - { - Issues = 0, - PullRequests - } -} \ No newline at end of file diff --git a/src/GitBucket.Core/ReleaseOptions.cs b/src/GitBucket.Core/ReleaseOptions.cs index c29bc1bf..ac90f9d4 100644 --- a/src/GitBucket.Core/ReleaseOptions.cs +++ b/src/GitBucket.Core/ReleaseOptions.cs @@ -14,7 +14,19 @@ public class ReleaseOptions : CommandLineOptionsBase [Option('m', "milestone", Required = true, HelpText = "The milestone to publish a release note.")] public string MileStone { get; set; } - [Option('t', "target", Required = false, HelpText = "The options to publish a release note based on issues or pull requests.")] - public string Target { get; set; } = nameof(GitBucket.Core.ReleaseNoteTarget.Issues); + [Option("from-pr", Required = false, HelpText = "If specified, gbutil publish a release note based on pull requests.")] + public bool FromPullRequest { get; set; } + + [Option("create-pr", Required = false, Default = false, HelpText = "Whether create pull request based on the milestone.")] + public bool CreatePullRequest { get; set; } + + [Option("base", Required = false, Default = "master", HelpText = "The name of the branch you want the changes pulled into.")] + public string Base { get; set; } = "master"; + + [Option('h', "head", Required = false, Default = "develop", HelpText = "The name of the branch where your changes are implemented.")] + public string Head { get; set; } = "develop"; + + [Option("title", Required = false, HelpText = "The title of the new pull request. Default value is the same as milestone.")] + public string Title { get; set; } } } \ No newline at end of file diff --git a/src/GitBucket.Data/GitBucket.Data.csproj b/src/GitBucket.Data/GitBucket.Data.csproj index 32b13c75..7496dcf7 100644 --- a/src/GitBucket.Data/GitBucket.Data.csproj +++ b/src/GitBucket.Data/GitBucket.Data.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/GitBucket.Data/Repositories/BaseRepository.cs b/src/GitBucket.Data/Repositories/BaseRepository.cs index 14ed32b5..48905b13 100644 --- a/src/GitBucket.Data/Repositories/BaseRepository.cs +++ b/src/GitBucket.Data/Repositories/BaseRepository.cs @@ -52,7 +52,9 @@ protected virtual void Dispose(bool disposing) { Context.Dispose(); } +#pragma warning disable CA1031 // Do not catch general exception types catch { } +#pragma warning restore CA1031 // Do not catch general exception types } disposedValue = true; diff --git a/src/GitBucket.Data/Repositories/IssueRepository.cs b/src/GitBucket.Data/Repositories/IssueRepository.cs index b3a451bc..18c49e6f 100644 --- a/src/GitBucket.Data/Repositories/IssueRepository.cs +++ b/src/GitBucket.Data/Repositories/IssueRepository.cs @@ -29,7 +29,8 @@ public override IEnumerable FindIssueLabels(ReleaseOptions options, return Context.Set() .Where(l => l.UserName.Equals(options.Owner, StringComparison.OrdinalIgnoreCase)) .Where(l => l.RepositoryName.Equals(options.Repository, StringComparison.OrdinalIgnoreCase)) - .Where(l => issues.Select(i => i.IssueId).Contains(l.IssueId)); + .Where(l => issues.Select(i => i.IssueId).Contains(l.IssueId)) + .AsNoTracking(); } public async override Task> FindIssuesRelatedToMileStone(ReleaseOptions options) @@ -38,12 +39,10 @@ public async override Task> FindIssuesRelatedToMileStone(ReleaseOpti .Where(i => i.UserName.Equals(options.Owner, StringComparison.OrdinalIgnoreCase)) .Where(i => i.RepositoryName.Equals(options.Repository, StringComparison.OrdinalIgnoreCase)) .Where(i => i.Milestone.Title.Equals(options.MileStone, StringComparison.OrdinalIgnoreCase)) - .WhereIf(string.Equals(nameof(ReleaseNoteTarget.Issues), options.Target, StringComparison.OrdinalIgnoreCase), - i => !i.PullRequest) - .WhereIf(!string.Equals(nameof(ReleaseNoteTarget.Issues), options.Target, StringComparison.OrdinalIgnoreCase), - i => i.PullRequest) + .Where(i => i.PullRequest == options.FromPullRequest) .Include(i => i.Milestone) .Include(i => i.Priority) + .AsNoTracking() .ToListAsync(); } } diff --git a/src/GitBucket.Data/Repositories/MilestoneRepository.cs b/src/GitBucket.Data/Repositories/MilestoneRepository.cs index e0b0f3de..f389fbc3 100644 --- a/src/GitBucket.Data/Repositories/MilestoneRepository.cs +++ b/src/GitBucket.Data/Repositories/MilestoneRepository.cs @@ -32,6 +32,7 @@ public override async Task> FindMilestones(MilestoneOptions opti .OrderBy(m => m.DueDate) .ThenBy(m => m.UserName) .ThenBy(m => m.RepositoryName) + .AsNoTracking() .ToListAsync(); } } diff --git a/src/GitBucket.Service.Tests/FakeConsole.cs b/src/GitBucket.Service.Tests/FakeConsole.cs index ef1a709d..7d8b8490 100644 --- a/src/GitBucket.Service.Tests/FakeConsole.cs +++ b/src/GitBucket.Service.Tests/FakeConsole.cs @@ -1,137 +1,201 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GitBucket.Core; - -namespace GitBucket.Service.Tests -{ - /// - /// Implementation of a fake . - /// - public class FakeConsole : IConsole - { - private bool hasNewLineAtTheEndOfTheMessages = false; - public List Messages { get; } = new List(); - public List WarnMessages { get; } = new List(); - public List ErrorMessages { get; } = new List(); - public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray; - - public void Write(string value) - { - if (!Messages.Any()) - { - Messages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - Messages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else - { - Messages[Messages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = false; - } - } - - public void WriteLine(string value) - { - if (!Messages.Any()) - { - Messages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - Messages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else - { - Messages[Messages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = true; - } - } - - public void WriteWarn(string value) - { - if (!Messages.Any()) - { - WarnMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - WarnMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else - { - WarnMessages[WarnMessages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = false; - } - } - - public void WriteWarnLine(string value) - { - if (!Messages.Any()) - { - WarnMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - WarnMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else - { - WarnMessages[WarnMessages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = true; - } - } - - public void WriteError(string value) - { - if (!Messages.Any()) - { - ErrorMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - ErrorMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = false; - } - else - { - ErrorMessages[ErrorMessages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = false; - } - } - - public void WriteErrorLine(string value) - { - if (!Messages.Any()) - { - ErrorMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else if (hasNewLineAtTheEndOfTheMessages) - { - ErrorMessages.Add(value); - hasNewLineAtTheEndOfTheMessages = true; - } - else - { - ErrorMessages[ErrorMessages.Count - 1] += value; - hasNewLineAtTheEndOfTheMessages = true; - } - } - - public void ResetColor() => ForegroundColor = ConsoleColor.Gray; - - public virtual string ReadLine() => "test"; - } +using System; +using System.Collections.Generic; +using System.Linq; +using GitBucket.Core; + +namespace GitBucket.Service.Tests +{ + /// + /// Implementation of a fake . + /// + public class FakeConsole : IConsole + { + private readonly string _input; + private ConsoleKind _consoleKind = ConsoleKind.Normal; + private bool _hasNewLineAtTheEndOfTheMessages = false; + + public FakeConsole(string input = "test") => _input = input; + + private enum ConsoleKind + { + Normal, + Warn, + Error + } + + public List Messages { get; } = new List(); + public List WarnMessages { get; } = new List(); + public List ErrorMessages { get; } = new List(); + public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray; + + public void Write(string value) + { + if (!Messages.Any()) + { + Messages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + Messages.Add(value); + } + else + { + Messages[Messages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Normal; + _hasNewLineAtTheEndOfTheMessages = false; + } + + public void WriteLine(string value) + { + if (!Messages.Any()) + { + Messages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + Messages.Add(value); + } + else + { + Messages[Messages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Normal; + _hasNewLineAtTheEndOfTheMessages = true; + ResetColor(); + } + + public void WriteWarn(string value) + { + if (!WarnMessages.Any()) + { + WarnMessages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + WarnMessages.Add(value); + } + else + { + WarnMessages[WarnMessages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Warn; + _hasNewLineAtTheEndOfTheMessages = false; + } + + public void WriteWarnLine(string value) + { + if (!WarnMessages.Any()) + { + WarnMessages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + WarnMessages.Add(value); + } + else + { + WarnMessages[WarnMessages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Normal; + _hasNewLineAtTheEndOfTheMessages = true; + ResetColor(); + } + + public void WriteError(string value) + { + if (!ErrorMessages.Any()) + { + ErrorMessages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + ErrorMessages.Add(value); + } + else + { + ErrorMessages[ErrorMessages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Error; + _hasNewLineAtTheEndOfTheMessages = false; + } + + public void WriteErrorLine(string value) + { + if (!ErrorMessages.Any()) + { + ErrorMessages.Add(value); + } + else if (_hasNewLineAtTheEndOfTheMessages) + { + ErrorMessages.Add(value); + } + else + { + ErrorMessages[ErrorMessages.Count - 1] += value; + } + + _consoleKind = ConsoleKind.Normal; + _hasNewLineAtTheEndOfTheMessages = true; + ResetColor(); + } + + public void ResetColor() => ForegroundColor = ConsoleColor.Gray; + + public virtual string ReadLine() + { + switch (_consoleKind) + { + case ConsoleKind.Warn: + if (!_hasNewLineAtTheEndOfTheMessages) + { + WarnMessages[WarnMessages.Count - 1] += _input; + } + else + { + WarnMessages.Add(_input); + } + + break; + + case ConsoleKind.Error: + if (!_hasNewLineAtTheEndOfTheMessages) + { + ErrorMessages[ErrorMessages.Count - 1] += _input; + } + else + { + ErrorMessages.Add(_input); + } + + break; + + default: + if (!_hasNewLineAtTheEndOfTheMessages) + { + Messages[Messages.Count - 1] += _input; + } + else + { + Messages.Add(_input); + } + + break; + } + + _consoleKind = ConsoleKind.Normal; + _hasNewLineAtTheEndOfTheMessages = true; + return _input; + } + + public string GetPassword() + { + throw new NotImplementedException(); + } + } } \ No newline at end of file diff --git a/src/GitBucket.Service.Tests/GitBucket.Service.Tests.csproj b/src/GitBucket.Service.Tests/GitBucket.Service.Tests.csproj index bb262233..2ddce77e 100644 --- a/src/GitBucket.Service.Tests/GitBucket.Service.Tests.csproj +++ b/src/GitBucket.Service.Tests/GitBucket.Service.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 @@ -6,6 +6,7 @@ + diff --git a/src/GitBucket.Service.Tests/IssueServiceTest.cs b/src/GitBucket.Service.Tests/IssueServiceTest.cs index e6f42447..efc9dc24 100644 --- a/src/GitBucket.Service.Tests/IssueServiceTest.cs +++ b/src/GitBucket.Service.Tests/IssueServiceTest.cs @@ -592,46 +592,6 @@ public async Task Unsupported_Issue_Type() Assert.Equal(@"""Invalid Type"" is not supported.", console.WarnMessages.First()); } - [Fact] - public async Task Should_Throw_If_IssueOptions_Is_Null() - { - // Given - var mockGitBucketClient = new Mock(MockBehavior.Strict); - - var console = new FakeConsole(); - var service = new IssueService(console); - - // When - var ex = await Record.ExceptionAsync(() => service.Execute(null, mockGitBucketClient.Object)); - - // Then - Assert.IsType(ex); - Assert.Equal("Value cannot be null.\r\nParameter name: options", ex.Message); - } - - [Fact] - public async Task Should_Throw_If_Client_Is_Null() - { - // Given - var options = new IssueOptions - { - ExecutedDate = new DateTime(2018, 7, 1), - Source = new[] { "root", "test1" }, - Destination = new[] { "root", "test2" }, - IssueNumbers = new[] { 1 } - }; - - var console = new FakeConsole(); - var service = new IssueService(console); - - // When - var ex = await Record.ExceptionAsync(() => service.Execute(options, null)); - - // Then - Assert.IsType(ex); - Assert.Equal("Value cannot be null.\r\nParameter name: gitBucketClient", ex.Message); - } - [Fact] public async Task Should_Copy_Issue_To_Same_Owner_Repository() { diff --git a/src/GitBucket.Service.Tests/ReleaseServicetest.cs b/src/GitBucket.Service.Tests/ReleaseServicetest.cs new file mode 100644 index 00000000..fae33455 --- /dev/null +++ b/src/GitBucket.Service.Tests/ReleaseServicetest.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using GitBucket.Core; +using GitBucket.Data.Repositories; +using Microsoft.EntityFrameworkCore; +using Moq; +using Octokit; +using Xunit; + +namespace GitBucket.Service.Tests +{ + public class ReleaseServiceTest + { + public FakeConsole FakeConsole { get; } = new FakeConsole("yes"); + + [Fact] + public async void Milestone_Has_No_Issue() + { + // Given + var dbContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "Milestone_Has_No_Issue") + .Options; + + var dbContext = new GitBucketDbContext(dbContextOptions); + var options = new ReleaseOptions { MileStone = "v1.0.0" }; + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + + // When + var result = await service.Execute(options, new Mock().Object); + + // Then + Assert.Equal(1, result); + Assert.Empty(FakeConsole.Messages); + Assert.Single(FakeConsole.WarnMessages); + Assert.Empty(FakeConsole.ErrorMessages); + Assert.Equal("There are no issues related to \"v1.0.0\".", FakeConsole.WarnMessages[0]); + } + + [Fact] + public async void Milestone_Has_No_PullRequest() + { + // Given + var dbContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "Milestone_Has_No_PullRequest") + .Options; + + var dbContext = new GitBucketDbContext(dbContextOptions); + var options = new ReleaseOptions { FromPullRequest = true, MileStone = "v1.0.0" }; + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + + // When + var result = await service.Execute(options, new Mock().Object); + + // Then + Assert.Equal(1, result); + Assert.Empty(FakeConsole.Messages); + Assert.Single(FakeConsole.WarnMessages); + Assert.Empty(FakeConsole.ErrorMessages); + Assert.Equal("There are no pull requests related to \"v1.0.0\".", FakeConsole.WarnMessages[0]); + } + + [Fact] + public async void Milestone_Has_Unclosed_Issue() + { + // Given + var options = new ReleaseOptions { MileStone = "v1.0.0", Owner = "root", Repository = "test" }; + var dbContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "Milestone_Has_Unclosed_Issue") + .Options; + + var dbContext = new GitBucketDbContext(dbContextOptions); + dbContext.Issue.Add(new Core.Models.Issue + { + Closed = false, + Milestone = new Core.Models.Milestone + { + RepositoryName = options.Repository, + Title = options.MileStone, + UserName = options.Owner, + }, + RepositoryName = options.Repository, + UserName = options.Owner, + }); + + dbContext.SaveChanges(); + + var console = new FakeConsole("no"); + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), console); + + // When + var result = await service.Execute(options, new Mock().Object); + + // Then + Assert.Equal(1, result); + Assert.Empty(console.Messages); + Assert.Equal(2, console.WarnMessages.Count); + Assert.Empty(console.ErrorMessages); + Assert.Equal("There are unclosed issues in \"v1.0.0\".", console.WarnMessages[0]); + Assert.Equal("Do you want to continue?([Y]es/[N]o): no", console.WarnMessages[1]); + } + + [Fact] + public async void Milestone_Has_Issue_Without_Labels() + { + // Given + var options = new ReleaseOptions { MileStone = "v1.0.0", Owner = "root", Repository = "test" }; + var dbContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "Milestone_Has_Issue_Without_Labels") + .Options; + + var dbContext = new GitBucketDbContext(dbContextOptions); + dbContext.Issue.Add(new Core.Models.Issue + { + Milestone = new Core.Models.Milestone + { + RepositoryName = options.Repository, + Title = options.MileStone, + UserName = options.Owner, + }, + RepositoryName = options.Repository, + UserName = options.Owner, + }); + + dbContext.SaveChanges(); + + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + + // When + var result = await service.Execute(options, new Mock().Object); + + // Then + Assert.Equal(1, result); + Assert.Single(FakeConsole.Messages); + Assert.Equal("", FakeConsole.Messages[0]); + + Assert.Equal(3, FakeConsole.WarnMessages.Count); + Assert.Equal("There are unclosed issues in \"v1.0.0\".", FakeConsole.WarnMessages[0]); + Assert.Equal("Do you want to continue?([Y]es/[N]o): yes", FakeConsole.WarnMessages[1]); + Assert.Equal("There are issues which have no labels in \"v1.0.0\".", FakeConsole.WarnMessages[2]); + + Assert.Empty(FakeConsole.ErrorMessages); + } + + [Fact] + public async void PullRequest_Already_Exists() + { + // Given + var options = new ReleaseOptions { CreatePullRequest = true, MileStone = "v1.0.0", Owner = "root", Repository = "test" }; + var dbContext = EnsureDbCreated(options); + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + var gitbucketClient = new Mock(); + gitbucketClient + .Setup(g => g.PullRequest.GetAllForRepository(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReadOnlyCollection(new List + { + new FakePullRequest(new FakeGitReference("develop"), new FakeGitReference("master")) + })); + + // When + var result = await service.Execute(options, gitbucketClient.Object); + + // Then + Assert.Equal(1, result); + Assert.Empty(FakeConsole.Messages); + Assert.Single(FakeConsole.WarnMessages); + Assert.Equal("A pull request already exists for root:develop.", FakeConsole.WarnMessages[0]); + Assert.Empty(FakeConsole.ErrorMessages); + } + + [Fact] + public async void Should_Create_PullRequest() + { + // Given + var options = new ReleaseOptions { CreatePullRequest = true, MileStone = "v1.0.0", Owner = "root", Repository = "test" }; + var dbContext = EnsureDbCreated(options); + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + var gitbucketClient = new Mock(); + gitbucketClient + .Setup(g => g.PullRequest.GetAllForRepository(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReadOnlyCollection(new List())); + + gitbucketClient + .Setup(g => g.PullRequest.Create(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidCastException("Ignore InvalidCastException because of escaped response.")); + + // When + var result = await service.Execute(options, gitbucketClient.Object); + + // Then + Assert.Equal(0, result); + + gitbucketClient + .Verify(g => g.PullRequest.Create( + It.Is(o => o == "root"), + It.Is(r => r == "test"), + It.Is(p => + p.Title == "v1.0.0" && + p.Head == "develop" && + p.Base == "master" && + p.Body == @"As part of this release we had 3 issues closed. +The highest priority among them is ""high"". + +### Bug +* Found a bug! #1 +* Another bug #2 + +### Enhancement +* Some improvement on build #3 + +"))); + + Assert.Single(FakeConsole.Messages); + Assert.Equal("A new pull request has been successfully created!", FakeConsole.Messages[0]); + Assert.Empty(FakeConsole.WarnMessages); + Assert.Empty(FakeConsole.ErrorMessages); + } + + [Fact] + public async void Should_Create_PullRequest_With_Different_Options() + { + // Given + var options = new ReleaseOptions + { + Base = "release/v1.0.0", + CreatePullRequest = true, + FromPullRequest = true, + Head = "master2", + MileStone = "v1.0.0", + Owner = "root", + Repository = "test", + Title = "Amazing PR", + }; + + var dbContext = EnsureDbCreated(options); + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + var gitbucketClient = new Mock(); + gitbucketClient + .Setup(g => g.PullRequest.GetAllForRepository(It.IsAny(), It.IsAny())) + .ReturnsAsync(new ReadOnlyCollection(new List())); + + gitbucketClient + .Setup(g => g.PullRequest.Create(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidCastException("Ignore InvalidCastException because of escaped response.")); + + // When + var result = await service.Execute(options, gitbucketClient.Object); + + // Then + Assert.Equal(0, result); + + gitbucketClient + .Verify(g => g.PullRequest.Create( + It.Is(o => o == "root"), + It.Is(r => r == "test"), + It.Is(p => + p.Title == "Amazing PR" && + p.Head == "master2" && + p.Base == "release/v1.0.0" && + p.Body == @"As part of this release we had 1 pull requests closed. +The highest priority among them is ""default"". + +### Bug +* Fix a bug #4 + +"))); + + Assert.Single(FakeConsole.Messages); + Assert.Equal("A new pull request has been successfully created!", FakeConsole.Messages[0]); + Assert.Empty(FakeConsole.WarnMessages); + Assert.Empty(FakeConsole.ErrorMessages); + } + + [Fact] + public async void Should_Output_ReleaseNote() + { + // Given + var options = new ReleaseOptions { MileStone = "v1.0.0", Owner = "root", Repository = "test" }; + var dbContext = EnsureDbCreated(options); + var service = new ReleaseService(new IssueRepository(dbContext), new LabelRepository(dbContext), FakeConsole); + + // When + var result = await service.Execute(options, new Mock().Object); + + // Then + Assert.Equal(0, result); + Assert.Single(FakeConsole.Messages); + Assert.Equal( + @"As part of this release we had 3 issues closed. +The highest priority among them is ""high"". + +### Bug +* Found a bug! #1 +* Another bug #2 + +### Enhancement +* Some improvement on build #3 + +", FakeConsole.Messages[0]); + + Assert.Empty(FakeConsole.WarnMessages); + Assert.Empty(FakeConsole.ErrorMessages); + } + + private static GitBucketDbContext EnsureDbCreated(ReleaseOptions options) + { + var dbContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + var dbContext = new GitBucketDbContext(dbContextOptions); + + var milestone = new Core.Models.Milestone + { + RepositoryName = options.Repository, + Title = options.MileStone, + UserName = options.Owner, + }; + + var highPriority = new Core.Models.Priority + { + Ordering = 0, + PriorityName = "high", + RepositoryName = options.Repository, + UserName = options.Owner, + }; + + var defaultPriority = new Core.Models.Priority + { + Ordering = 1, + PriorityName = "default", + RepositoryName = options.Repository, + UserName = options.Owner, + }; + + dbContext.Issue.AddRange(new List + { + new Core.Models.Issue + { + Closed = true, + IssueId = 1, + Milestone = milestone, + Priority = highPriority, + RepositoryName = options.Repository, + Title = "Found a bug!", + UserName = options.Owner, + }, + new Core.Models.Issue + { + Closed = true, + IssueId = 2, + Milestone = milestone, + Priority = defaultPriority, + RepositoryName = options.Repository, + Title = "Another bug", + UserName = options.Owner, + }, + new Core.Models.Issue + { + Closed = true, + IssueId = 3, + Milestone = milestone, + Priority = defaultPriority, + RepositoryName = options.Repository, + Title = "Some improvement on build", + UserName = options.Owner, + }, + new Core.Models.Issue + { + Closed = true, + IssueId = 4, + Milestone = milestone, + Priority = defaultPriority, + PullRequest = true, + RepositoryName = options.Repository, + Title = "Fix a bug", + UserName = options.Owner, + } + }); + + dbContext.IssueLabel.AddRange(new List + { + new Core.Models.IssueLabel + { + IssueId = 1, + LabelId = 10, + RepositoryName = options.Repository, + UserName = options.Owner, + }, + new Core.Models.IssueLabel + { + IssueId = 2, + LabelId = 10, + RepositoryName = options.Repository, + UserName = options.Owner, + }, + new Core.Models.IssueLabel + { + IssueId = 3, + LabelId = 30, + RepositoryName = options.Repository, + UserName = options.Owner, + }, + new Core.Models.IssueLabel + { + IssueId = 4, + LabelId = 10, + RepositoryName = options.Repository, + UserName = options.Owner, + } + }); + + dbContext.Label.AddRange(new List + { + new Core.Models.Label + { + LabelId = 10, + LabelName = "Bug", + RepositoryName = options.Repository, + UserName = options.Owner, + }, + new Core.Models.Label + { + LabelId = 30, + LabelName = "Enhancement", + RepositoryName = options.Repository, + UserName = options.Owner, + } + }); + + dbContext.SaveChanges(); + return dbContext; + } + } + + public sealed class FakePullRequest : Octokit.PullRequest + { + public FakePullRequest(GitReference head, GitReference @base) + { + Head = head; + Base = @base; + } + } + + public sealed class FakeGitReference : Octokit.GitReference + { + public FakeGitReference(string @ref) => Ref = @ref; + } +} diff --git a/src/GitBucket.Data/Extensions/StringExtensions.cs b/src/GitBucket.Service/Extensions/StringExtensions.cs similarity index 100% rename from src/GitBucket.Data/Extensions/StringExtensions.cs rename to src/GitBucket.Service/Extensions/StringExtensions.cs diff --git a/src/GitBucket.Service/GitBucket.Service.csproj b/src/GitBucket.Service/GitBucket.Service.csproj index f8c89b42..f3cdff95 100644 --- a/src/GitBucket.Service/GitBucket.Service.csproj +++ b/src/GitBucket.Service/GitBucket.Service.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/GitBucket.Service/ReleaseNoteService.cs b/src/GitBucket.Service/ReleaseNoteService.cs deleted file mode 100644 index c76b38bf..00000000 --- a/src/GitBucket.Service/ReleaseNoteService.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using GitBucket.Core; -using GitBucket.Data.Repositories; -using Microsoft.EntityFrameworkCore; - -namespace GitBucket.Service -{ - public interface IReleaseNoteService - { - Task OutputReleaseNotes(ReleaseOptions options); - } - - public class ReleaseNoteService : IReleaseNoteService - { - private readonly IssueRepositoryBase _issueRepository; - private readonly LabelRepositoryBase _labelRepository; - private IConsole _console; - - public ReleaseNoteService( - IssueRepositoryBase issueRepository, - LabelRepositoryBase labelRepository, - IConsole console) - { - _issueRepository = issueRepository; - _labelRepository = labelRepository; - _console = console; - } - - public async Task OutputReleaseNotes(ReleaseOptions options) - { - var closedTargets = options.Target.ToLowerInvariant(); - var issues = await _issueRepository.FindIssuesRelatedToMileStone(options); - if (!issues.Any()) - { - _console.WriteWarnLine($"There are no {closedTargets} related to \"{options.MileStone}\"."); - return 1; - } - - if (issues.Any(i => !i.Closed)) - { - _console.WriteWarnLine($"There are unclosed {closedTargets} in \"{options.MileStone}\"."); - _console.WriteWarn("Do you want to continue?([Y]es/[N]o): "); - string yesOrNo = Console.ReadLine(); - - if (!string.Equals(yesOrNo, "y", StringComparison.OrdinalIgnoreCase) - && !string.Equals(yesOrNo, "yes", StringComparison.OrdinalIgnoreCase)) - { - return 1; - } - - _console.WriteLine(""); - } - - var issueLabels = _issueRepository.FindIssueLabels(options, issues).ToList(); - if (issues.Any(i => !issueLabels.Select(l => l.IssueId).Contains(i.IssueId))) - { - _console.WriteWarnLine($"There are issues which have no labels in \"{options.MileStone}\"."); - return 1; - } - - var labels = _labelRepository.FindBy(l => - l.UserName.Equals(options.Owner, StringComparison.OrdinalIgnoreCase) && - l.RepositoryName.Equals(options.Repository, StringComparison.OrdinalIgnoreCase) && - issueLabels.Select(i => i.LabelId).Contains(l.LabelId)); - - var highestPriority = issues - .OrderBy(i => i.Priority.Ordering) - .First() - .Priority.PriorityName; - - _console.WriteLine($"As part of this release we had {issues.Count} {closedTargets} closed."); - _console.WriteLine($"The highest priority among them is \"{highestPriority}\"."); - _console.WriteLine(""); - foreach (var label in labels) - { - _console.WriteLine($"### {label.LabelName.ConvertFirstCharToUpper()}"); - - var ids = issueLabels - .Where(l => l.LabelId == label.LabelId) - .Select(i => i.IssueId) - .OrderBy(i => i); - - foreach (var issueId in ids) - { - var issue = issues.Where(i => i.IssueId == issueId).Single(); - _console.WriteLine($"* {issue.Title} #{issue.IssueId}"); - } - - _console.WriteLine(""); - } - - return 0; - } - } -} diff --git a/src/GitBucket.Service/ReleaseService.cs b/src/GitBucket.Service/ReleaseService.cs new file mode 100644 index 00000000..7333a2e1 --- /dev/null +++ b/src/GitBucket.Service/ReleaseService.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GitBucket.Core; +using GitBucket.Data.Repositories; +using Microsoft.EntityFrameworkCore; +using Octokit; + +namespace GitBucket.Service +{ + public interface IReleaseService + { + Task Execute(ReleaseOptions options, IGitHubClient gitBucketClient); + } + + public class ReleaseService : IReleaseService + { + private readonly IssueRepositoryBase _issueRepository; + private readonly LabelRepositoryBase _labelRepository; + private readonly IConsole _console; + + public ReleaseService( + IssueRepositoryBase issueRepository, + LabelRepositoryBase labelRepository, + IConsole console) + { + _issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository)); + _labelRepository = labelRepository ?? throw new ArgumentNullException(nameof(labelRepository)); + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + public async Task Execute(ReleaseOptions options, IGitHubClient gitBucketClient) + { + var pullRequestSource = options.FromPullRequest ? "pull requests" : "issues"; + var issues = await _issueRepository.FindIssuesRelatedToMileStone(options); + if (!issues.Any()) + { + _console.WriteWarnLine($"There are no {pullRequestSource} related to \"{options.MileStone}\"."); + return await Task.FromResult(1); + } + + if (issues.Any(i => !i.Closed)) + { + _console.WriteWarnLine($"There are unclosed {pullRequestSource} in \"{options.MileStone}\"."); + _console.WriteWarn("Do you want to continue?([Y]es/[N]o): "); + string yesOrNo = _console.ReadLine(); + + if (!string.Equals(yesOrNo, "y", StringComparison.OrdinalIgnoreCase) + && !string.Equals(yesOrNo, "yes", StringComparison.OrdinalIgnoreCase)) + { + return await Task.FromResult(1); + } + + _console.WriteLine(""); + } + + var issueLabels = _issueRepository.FindIssueLabels(options, issues).ToList(); + if (issues.Any(i => !issueLabels.Select(l => l.IssueId).Contains(i.IssueId))) + { + _console.WriteWarnLine($"There are issues which have no labels in \"{options.MileStone}\"."); + return await Task.FromResult(1); + } + + if (options.CreatePullRequest) + { + return await CreatePullRequest(options, issues, issueLabels, pullRequestSource, gitBucketClient); + } + else + { + return await OutputReleaseNote(options, issues, issueLabels, pullRequestSource); + } + } + + private async Task CreatePullRequest( + ReleaseOptions options, + List issues, + List issueLabels, + string pullRequestSource, + IGitHubClient gitBucketClient) + { + // Check if specified pull request already exists + var pullRequests = await gitBucketClient.PullRequest.GetAllForRepository(options.Owner, options.Repository); + if (pullRequests.Any(p => p.Head.Ref == options.Head && p.Base.Ref == options.Base)) + { + _console.WriteWarnLine($"A pull request already exists for {options.Owner}:{options.Head}."); + return await Task.FromResult(1); + } + + var releaseNote = CreateReleaseNote(options, issues, issueLabels, pullRequestSource); + + try + { + // Create new pull request + await gitBucketClient.PullRequest.Create( + options.Owner, + options.Repository, + new NewPullRequest( + title: options.Title ?? options.MileStone, + head: options.Head, + baseRef: options.Base + ) + { Body = releaseNote }); + } + catch (InvalidCastException) + { + // Ignore InvalidCastException because of escaped response. + // https://github.com/gitbucket/gitbucket/issues/2306 + } + + _console.WriteLine($"A new pull request has been successfully created!"); + return await Task.FromResult(0); + } + + private async Task OutputReleaseNote( + ReleaseOptions options, + List issues, + List issueLabels, + string pullRequestSource) + { + var releaseNote = CreateReleaseNote(options, issues, issueLabels, pullRequestSource); + _console.WriteLine(releaseNote); + return await Task.FromResult(0); + } + + private string CreateReleaseNote( + ReleaseOptions options, + List issues, + List issueLabels, + string pullRequestSource) + { + var labels = _labelRepository + .FindBy(l => + l.UserName.Equals(options.Owner, StringComparison.OrdinalIgnoreCase) && + l.RepositoryName.Equals(options.Repository, StringComparison.OrdinalIgnoreCase) && + issueLabels.Select(i => i.LabelId).Contains(l.LabelId)); + + var highestPriority = issues + .OrderBy(i => i.Priority.Ordering) + .First() + .Priority.PriorityName; + + var builder = new StringBuilder(); + builder.AppendLine($"As part of this release we had {issues.Count} {pullRequestSource} closed."); + builder.AppendLine($"The highest priority among them is \"{highestPriority}\"."); + builder.AppendLine(""); + foreach (var label in labels) + { + builder.AppendLine($"### {label.LabelName.ConvertFirstCharToUpper()}"); + + var ids = issueLabels + .Where(l => l.LabelId == label.LabelId) + .Select(i => i.IssueId) + .OrderBy(i => i); + + foreach (var issueId in ids) + { + var issue = issues.Where(i => i.IssueId == issueId).Single(); + builder.AppendLine($"* {issue.Title} #{issue.IssueId}"); + } + + builder.AppendLine(""); + } + + return builder.ToString(); + } + } +} diff --git a/tools/packages.config b/tools/packages.config deleted file mode 100644 index 05018882..00000000 --- a/tools/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - -