From ecd3e5a375d3963e3f6a7a167255c08871ab9161 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Tue, 21 Nov 2023 17:02:19 -0800 Subject: [PATCH 01/37] Test proxy: add link to artifacts cred provider and note to list other ways to get test-proxy (#7332) * Test proxy: add link to artifacts cred provider and note to list other ways to get test-proxy --- tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md index 0e401a7548e..084dad43fb8 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md @@ -96,17 +96,19 @@ For a more detailed explanation of how the test proxy works, along with links to 2. Install test-proxy ```powershell -dotnet tool update azure.sdk.tools.testproxy --global --add-source https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json --version "1.0.0-dev*" +dotnet tool update azure.sdk.tools.testproxy --global --add-source https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json --version "1.0.0-dev*" --ignore-failed-sources ``` +The test-proxy is also available from the [azure-sdk-for-net public feed](https://dev.azure.com/azure-sdk/public/_artifacts/feed/azure-sdk-for-net) + +_Note: if you're not authorized to access these feeds, make sure you have [Azure Artifacts Credential Provider](https://github.com/microsoft/artifacts-credprovider) installed. You can also [download executable](#via-standalone-executable) or use a prebuilt [docker image](#via-docker-image)._ + To uninstall an existing test-proxy ```powershell dotnet tool uninstall --global azure.sdk.tools.testproxy ``` -The test-proxy is also available from the [azure-sdk-for-net public feed](https://dev.azure.com/azure-sdk/public/_artifacts/feed/azure-sdk-for-net) - After successful installation, run the tool: ```powershell From 5c0047521a0e782de0fb3c95042727171f540c65 Mon Sep 17 00:00:00 2001 From: Praven Kuttappan <55455725+praveenkuttappan@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:43:32 -0500 Subject: [PATCH 02/37] Include package version in code file for Java API review (#7333) Co-authored-by: praveenkuttappan --- src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj | 2 +- src/dotnet/APIView/APIViewWeb/Languages/JavaLanguageService.cs | 2 +- src/dotnet/APIView/APIViewWeb/Languages/XmlLanguageService.cs | 2 +- src/java/apiview-java-processor/pom.xml | 2 +- .../com/azure/tools/apiview/processor/model/APIListing.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj index c92e759b4fb..81cafecae09 100644 --- a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj +++ b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj @@ -6,7 +6,7 @@ APIViewWeb 79cceff6-d533-4370-a0ee-f3321a343907 true - ..\..\..\java\apiview-java-processor\target\apiview-java-processor-1.30.0.jar + ..\..\..\java\apiview-java-processor\target\apiview-java-processor-1.31.0.jar ..\..\..\..\packages\python-packages\apiview-stub-generator\dist\api_stub_generator-0.1.0-py3-none-any.whl ..\..\..\go\apiviewgo.exe ..\..\..\..\tools\apiview\parsers\js-api-parser\ diff --git a/src/dotnet/APIView/APIViewWeb/Languages/JavaLanguageService.cs b/src/dotnet/APIView/APIViewWeb/Languages/JavaLanguageService.cs index b5c4008bd9f..d4d6f0d7aeb 100644 --- a/src/dotnet/APIView/APIViewWeb/Languages/JavaLanguageService.cs +++ b/src/dotnet/APIView/APIViewWeb/Languages/JavaLanguageService.cs @@ -10,7 +10,7 @@ public class JavaLanguageService : LanguageProcessor public override string Name { get; } = "Java"; public override string[] Extensions { get; } = { ".jar" }; public override string ProcessName { get; } = "java"; - public override string VersionString { get; } = "apiview-java-processor-1.30.0.jar"; + public override string VersionString { get; } = "apiview-java-processor-1.31.0.jar"; public override string GetProcessorArguments(string originalName, string tempDirectory, string jsonPath) { diff --git a/src/dotnet/APIView/APIViewWeb/Languages/XmlLanguageService.cs b/src/dotnet/APIView/APIViewWeb/Languages/XmlLanguageService.cs index f61d363c976..99763ef77b9 100644 --- a/src/dotnet/APIView/APIViewWeb/Languages/XmlLanguageService.cs +++ b/src/dotnet/APIView/APIViewWeb/Languages/XmlLanguageService.cs @@ -10,7 +10,7 @@ public class XmlLanguageService : LanguageProcessor public override string Name { get; } = "Xml"; public override string[] Extensions { get; } = { ".xml" }; public override string ProcessName { get; } = "java"; - public override string VersionString { get; } = "apiview-java-processor-1.30.0.jar"; + public override string VersionString { get; } = "apiview-java-processor-1.31.0.jar"; public override string GetProcessorArguments(string originalName, string tempDirectory, string jsonPath) { diff --git a/src/java/apiview-java-processor/pom.xml b/src/java/apiview-java-processor/pom.xml index 19b131a141d..c2b42637b99 100644 --- a/src/java/apiview-java-processor/pom.xml +++ b/src/java/apiview-java-processor/pom.xml @@ -6,7 +6,7 @@ com.azure apiview-java-processor - 1.30.0 + 1.31.0 1.8 diff --git a/src/java/apiview-java-processor/src/main/java/com/azure/tools/apiview/processor/model/APIListing.java b/src/java/apiview-java-processor/src/main/java/com/azure/tools/apiview/processor/model/APIListing.java index 022d15f0303..fb138b9eb0c 100644 --- a/src/java/apiview-java-processor/src/main/java/com/azure/tools/apiview/processor/model/APIListing.java +++ b/src/java/apiview-java-processor/src/main/java/com/azure/tools/apiview/processor/model/APIListing.java @@ -29,7 +29,7 @@ public class APIListing { @JsonProperty("PackageName") private String packageName; - @JsonIgnore//("PackageVersion") + @JsonProperty("PackageVersion") private String packageVersion; // This string is taken from here: From 8b4943ac480d9cd3c699507d8cfb1221cd9c8a01 Mon Sep 17 00:00:00 2001 From: Praven Kuttappan <55455725+praveenkuttappan@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:42:31 -0500 Subject: [PATCH 03/37] Update python parser version in APIView sandbox pipeline (#7335) Co-authored-by: praveenkuttappan --- eng/pipelines/apiview-review-gen-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/apiview-review-gen-python.yml b/eng/pipelines/apiview-review-gen-python.yml index dc3a4733385..be7acbbd466 100644 --- a/eng/pipelines/apiview-review-gen-python.yml +++ b/eng/pipelines/apiview-review-gen-python.yml @@ -20,7 +20,7 @@ parameters: variables: PythonIndexUrl: 'https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/' PythonVersion: '3.10' - ApiStubVersion: '0.3.8' + ApiStubVersion: '0.3.10' jobs: - job: CreatePythonReviewCodeFile From 2f9964d1430cb5985c2577c00d060de65d0a3dc6 Mon Sep 17 00:00:00 2001 From: Praven Kuttappan <55455725+praveenkuttappan@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:37:20 -0500 Subject: [PATCH 04/37] APIView - Update package name format for C (#7338) --- src/dotnet/APIView/APIViewWeb/Languages/CLanguageService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/Languages/CLanguageService.cs b/src/dotnet/APIView/APIViewWeb/Languages/CLanguageService.cs index afa8dc3bf35..dcc2417bb73 100644 --- a/src/dotnet/APIView/APIViewWeb/Languages/CLanguageService.cs +++ b/src/dotnet/APIView/APIViewWeb/Languages/CLanguageService.cs @@ -16,7 +16,7 @@ namespace APIViewWeb { public class CLanguageService : LanguageService { - private const string CurrentVersion = "4"; + private const string CurrentVersion = "5"; private static Regex _typeTokenizer = new Regex("\\w+|[^\\w]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static HashSet _keywords = new HashSet() { @@ -88,7 +88,7 @@ public override async Task GetCodeFileAsync(string originalName, Strea var packageNameMatch = _packageNameParser.Match(originalName); if (packageNameMatch.Success) { - packageNamespace = packageNameMatch.Groups[1].Value.Replace("_", "::"); + packageNamespace = packageNameMatch.Groups[1].Value.Replace("_", "-"); } CodeFileTokensBuilder builder = new CodeFileTokensBuilder(); From cd715ff639b95f24f5e15dd015f81dd9618aae1d Mon Sep 17 00:00:00 2001 From: Rodge Fu Date: Wed, 29 Nov 2023 10:30:38 +0800 Subject: [PATCH 05/37] Update Azure.ResourceManager to get latest change in TrackedResourceData class (#7347) --- .../Azure.SDK.Management.ChangelogGen.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/net-changelog-gen-mgmt/Azure.SDK.Management.ChangelogGen/Azure.SDK.Management.ChangelogGen.csproj b/tools/net-changelog-gen-mgmt/Azure.SDK.Management.ChangelogGen/Azure.SDK.Management.ChangelogGen.csproj index 53d711af4d6..6733e831598 100644 --- a/tools/net-changelog-gen-mgmt/Azure.SDK.Management.ChangelogGen/Azure.SDK.Management.ChangelogGen.csproj +++ b/tools/net-changelog-gen-mgmt/Azure.SDK.Management.ChangelogGen/Azure.SDK.Management.ChangelogGen.csproj @@ -10,7 +10,7 @@ - + From bd039a7b82f094f6193726bad189ab873728f0e7 Mon Sep 17 00:00:00 2001 From: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:17:02 -0800 Subject: [PATCH 06/37] Fix typo (#7334) * Fix typo * Update Cadl-Project-Generate.ps1 * Update Cadl-Project-Generate.ps1 * Update TypeSpec-Project-Generate.ps1 * Update TypeSpec-Project-Scripts.md --- doc/common/TypeSpec-Project-Scripts.md | 2 +- eng/common/scripts/Cadl-Project-Generate.ps1 | 4 ++-- eng/common/scripts/TypeSpec-Project-Generate.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/common/TypeSpec-Project-Scripts.md b/doc/common/TypeSpec-Project-Scripts.md index 2b9e19a5647..de46a605c62 100644 --- a/doc/common/TypeSpec-Project-Scripts.md +++ b/doc/common/TypeSpec-Project-Scripts.md @@ -165,7 +165,7 @@ It can be found at `./eng/common/scripts/TypeSpec-Project-Generate.ps1`. It tak - input: - ProjectDirectory (required) - TypespecAdditionalOptions (optional) - addtional typespec emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 + additional typespec emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 - SaveInputs (optional) saves the temporary files during execution, default value is false diff --git a/eng/common/scripts/Cadl-Project-Generate.ps1 b/eng/common/scripts/Cadl-Project-Generate.ps1 index 3e7ee781b05..bd4a2377e8b 100644 --- a/eng/common/scripts/Cadl-Project-Generate.ps1 +++ b/eng/common/scripts/Cadl-Project-Generate.ps1 @@ -6,7 +6,7 @@ param ( [ValidateNotNullOrEmpty()] [string] $ProjectDirectory, [Parameter(Position=1)] - [string] $CadlAdditionalOptions ## addtional cadl emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 + [string] $CadlAdditionalOptions ## additional cadl emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 ) $ErrorActionPreference = "Stop" @@ -98,4 +98,4 @@ finally { $shouldCleanUp = $configuration["cleanup"] ?? $true if ($shouldCleanUp) { Remove-Item $tempFolder -Recurse -Force -} \ No newline at end of file +} diff --git a/eng/common/scripts/TypeSpec-Project-Generate.ps1 b/eng/common/scripts/TypeSpec-Project-Generate.ps1 index 9f2c2804db0..e0ca0a55f6e 100644 --- a/eng/common/scripts/TypeSpec-Project-Generate.ps1 +++ b/eng/common/scripts/TypeSpec-Project-Generate.ps1 @@ -5,7 +5,7 @@ param ( [Parameter(Position=0)] [ValidateNotNullOrEmpty()] [string] $ProjectDirectory, - [string] $TypespecAdditionalOptions = $null, ## addtional typespec emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 + [string] $TypespecAdditionalOptions = $null, ## additional typespec emitter options, separated by semicolon if more than one, e.g. option1=value1;option2=value2 [switch] $SaveInputs = $false ## saves the temporary files during execution, default false ) From b94f185e2c48d7f3c28a2221bc1d74d9d0ea2385 Mon Sep 17 00:00:00 2001 From: Praven Kuttappan <55455725+praveenkuttappan@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:13:30 -0500 Subject: [PATCH 07/37] Remove default value for pipeline picklist params (#7357) * Parameters passed from powerapps are accessible as variables only --- .../devops-create-package-workitem.yml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/eng/pipelines/devops-create-package-workitem.yml b/eng/pipelines/devops-create-package-workitem.yml index 1b1f0f95a0a..6aa5cace714 100644 --- a/eng/pipelines/devops-create-package-workitem.yml +++ b/eng/pipelines/devops-create-package-workitem.yml @@ -5,7 +5,7 @@ pool: parameters: - name: Language type: string - default: '' + default: '.NET' values: - .NET - Java @@ -24,7 +24,7 @@ parameters: default: '' - name: PackageType type: string - default: '' + default: 'Client' values: - Client - mgmt @@ -58,13 +58,13 @@ steps: pwsh: true filePath: $(Build.SourcesDirectory)/eng/common/scripts/Update-DevOps-Release-WorkItem.ps1 arguments: > - -language "${{parameters.Language}}" - -serviceName "${{parameters.ServiceName}}" - -packageDisplayName "${{parameters.PackageDisplayName}}" - -packageType "${{parameters.PackageType}}" - -packageName "${{parameters.PackageName}}" - -version "${{parameters.PackageVersion}}" - -plannedDate "${{parameters.ReleaseDate}}" - -relatedWorkItemId ${{parameters.RelatedWorkItemId}} - -tag "${{parameters.Tag}}" + -language "$(Language)" + -serviceName "$(ServiceName)" + -packageDisplayName "$(PackageDisplayName)" + -packageType "$(PackageType)" + -packageName "$(PackageName)" + -version "$(PackageVersion)" + -plannedDate "$(ReleaseDate)" + -relatedWorkItemId "$(RelatedWorkItemId)" + -tag "$(Tag)" -devops_pat "$(azuresdk-azure-sdk-devops-release-work-item-pat)" \ No newline at end of file From a20e301f44ae1d7db7e918a3c23070b2053121c7 Mon Sep 17 00:00:00 2001 From: Praven Kuttappan <55455725+praveenkuttappan@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:26:19 -0500 Subject: [PATCH 08/37] Swagger API review parser changes to add package version in code file (#7350) * Swagger API review parser changes to add package version in code file --- .../swagger-api-parser/SwaggerApiParser/CHANGELOG | 3 +++ .../SwaggerApiParser/SwaggerAPIViewGenerator.cs | 3 ++- .../SwaggerApiParser/SwaggerApiParser.csproj | 2 +- .../SwaggerApiParser/SwaggerApiView/CodeFile.cs | 2 ++ .../SwaggerApiView/SwaggerApiViewRoot.cs | 3 +++ .../SwaggerApiView/SwaggerApiViewSpec.cs | 2 ++ .../SwaggerApiParserTest/SwaggerAPIViewTest.cs | 13 +++++++++++++ 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/CHANGELOG b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/CHANGELOG index 8641633aa0a..f15a96c78cf 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/CHANGELOG +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/CHANGELOG @@ -1,5 +1,8 @@ # Release History +## 1.2.0 (11-29-2023) ++ Added API version as PackageVersion in code file. + ## 1.1.0 (6-26-2023) + Disabled flatteneing of nested model. + Updated definitions to include models all referenced models diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerAPIViewGenerator.cs b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerAPIViewGenerator.cs index af722a18839..1b912a6f437 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerAPIViewGenerator.cs +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerAPIViewGenerator.cs @@ -32,7 +32,8 @@ public static async Task GenerateSwaggerApiView(Swagger swag swaggerFilePath = swaggerFilePath }, fileName = Path.GetFileName(swaggerFilePath), - packageName = packageName + packageName = packageName, + APIVersion = swaggerSpec.info.version }; AddDefinitionsToCache(swaggerSpec, swaggerFilePath, schemaCache); diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiParser.csproj b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiParser.csproj index 48e50a17fb8..a535c1f8768 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiParser.csproj +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiParser.csproj @@ -6,7 +6,7 @@ swaggerAPIParser net7.0 SwaggerApiParser - 1.1.0 + 1.2.0 Azure.Sdk.Tools.SwaggerApiParser True $(OfficialBuildId) diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/CodeFile.cs b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/CodeFile.cs index ab8e9084914..58ef317b0f7 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/CodeFile.cs +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/CodeFile.cs @@ -28,6 +28,8 @@ public class CodeFile public string PackageDisplayName { get; set; } + public string PackageVersion { get; set; } + public CodeFileToken[] Tokens { get; set; } = Array.Empty(); public List LeafSections { get; set; } diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewRoot.cs b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewRoot.cs index c6eb2ee6c9b..a8d7a27a0d3 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewRoot.cs +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewRoot.cs @@ -13,6 +13,7 @@ public class SwaggerApiViewRoot : ITokenSerializable public String PackageName; public Dictionary SwaggerApiViewSpecs; public SchemaCache schemaCache; + public String APIVersion; public SwaggerApiViewRoot(string resourceProvider, string packageName) { @@ -29,6 +30,7 @@ public async Task AddSwaggerSpec(Swagger swaggerSpec, string swaggerFilePath, st if (swaggerApiViewSpec != null) { this.SwaggerApiViewSpecs.Add(swaggerFilePath, swaggerApiViewSpec); + APIVersion = swaggerApiViewSpec.APIVersion; } } @@ -47,6 +49,7 @@ public CodeFile GenerateCodeFile() VersionString = "0", Name = this.ResourceProvider, PackageName = this.PackageName, + PackageVersion = this.APIVersion, Navigation = this.BuildNavigationItems() }; diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewSpec.cs b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewSpec.cs index 33f023823c3..b2f8b6c5395 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewSpec.cs +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParser/SwaggerApiView/SwaggerApiViewSpec.cs @@ -17,6 +17,7 @@ public SwaggerApiViewSpec(string fileName) public string fileName; public string packageName; + public string APIVersion; public SwaggerApiViewSpec() @@ -100,6 +101,7 @@ public CodeFile GenerateCodeFile() VersionString = "0", Name = this.fileName, PackageName = this.packageName, + PackageVersion = this.APIVersion, Navigation = new NavigationItem[] { this.BuildNavigationItem() } }; return ret; diff --git a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParserTest/SwaggerAPIViewTest.cs b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParserTest/SwaggerAPIViewTest.cs index aa29cefad9a..8c9c4205e17 100644 --- a/tools/apiview/parsers/swagger-api-parser/SwaggerApiParserTest/SwaggerAPIViewTest.cs +++ b/tools/apiview/parsers/swagger-api-parser/SwaggerApiParserTest/SwaggerAPIViewTest.cs @@ -278,4 +278,17 @@ public async Task TestCommunicationEmailWithHeaderParameters() await codeFile.SerializeAsync(writer); } + + [Fact] + public async Task TestCodeFilePackageVersion() + { + const string runCommandFilePath = "./fixtures/runCommands.json"; + var swaggerSpec = await SwaggerDeserializer.Deserialize(runCommandFilePath); + + SwaggerApiViewRoot root = new SwaggerApiViewRoot("Microsoft.Compute", "Microsoft.Compute"); + await root.AddSwaggerSpec(swaggerSpec, Path.GetFullPath(runCommandFilePath), "Microsoft.Compute"); + + var codeFile = root.GenerateCodeFile(); + Assert.Equal("2021-11-01", codeFile.PackageVersion); + } } From fd7a37892fd013d5817b5fb657953c6c9ed2d191 Mon Sep 17 00:00:00 2001 From: Wes Haggard Date: Thu, 30 Nov 2023 14:47:31 -0800 Subject: [PATCH 09/37] Lets not treat https code 401 as broken link (#7369) Http Status code 401 just be unauthorized and not necessarily that the link isn't valid. Removing this from the list of codes to error from. --- eng/common/scripts/Verify-Links.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/common/scripts/Verify-Links.ps1 b/eng/common/scripts/Verify-Links.ps1 index a77f57133f2..62fd3df9875 100644 --- a/eng/common/scripts/Verify-Links.ps1 +++ b/eng/common/scripts/Verify-Links.ps1 @@ -25,7 +25,7 @@ Path to the root of the site for resolving rooted relative links, defaults to host root for http and file directory for local files. .PARAMETER errorStatusCodes - List of http status codes that count as broken links. Defaults to 400, 401, 404, SocketError.HostNotFound = 11001, SocketError.NoData = 11004. + List of http status codes that count as broken links. Defaults to 400, 404, SocketError.HostNotFound = 11001, SocketError.NoData = 11004. .PARAMETER branchReplaceRegex Regex to check if the link needs to be replaced. E.g. ^(https://github.com/.*/(?:blob|tree)/)main(/.*)$ @@ -65,7 +65,7 @@ param ( [switch] $recursive = $true, [string] $baseUrl = "", [string] $rootUrl = "", - [array] $errorStatusCodes = @(400, 401, 404, 11001, 11004), + [array] $errorStatusCodes = @(400, 404, 11001, 11004), [string] $branchReplaceRegex = "", [string] $branchReplacementName = "", [bool] $checkLinkGuidance = $false, From d69bca0170427b92a5c436391e98e43611880421 Mon Sep 17 00:00:00 2001 From: Ben Broderick Phillips Date: Thu, 30 Nov 2023 21:18:26 -0500 Subject: [PATCH 10/37] Add mirror container image pipeline (#7373) --- eng/pipelines/mirror-container-images.yml | 39 +++++++++++++++++++++++ eng/scripts/mirror-container-image.ps1 | 23 +++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 eng/pipelines/mirror-container-images.yml create mode 100644 eng/scripts/mirror-container-image.ps1 diff --git a/eng/pipelines/mirror-container-images.yml b/eng/pipelines/mirror-container-images.yml new file mode 100644 index 00000000000..99e78b14529 --- /dev/null +++ b/eng/pipelines/mirror-container-images.yml @@ -0,0 +1,39 @@ +trigger: none +pr: none + +parameters: + - name: Images + type: object + default: + - source: ghcr.io/chaos-mesh/chaos-daemon:v2.1.4 + mirror: azsdkengsys.azurecr.io/mirror/chaos-mesh/chaos-daemon:v2.1.4 + - source: ghcr.io/chaos-mesh/chaos-mesh:v2.1.4 + mirror: azsdkengsys.azurecr.io/mirror/chaos-mesh/chaos-mesh:v2.1.4 + - source: ghcr.io/chaos-mesh/chaos-dashboard:v2.1.4 + mirror: azsdkengsys.azurecr.io/mirror/chaos-mesh/chaos-dashboard:v2.1.4 + - source: ubuntu/squid + mirror: azsdkengsys.azurecr.io/mirror/ubuntu/squid + +jobs: + - job: MirrorImages + displayName: Mirror Container Images + + pool: + name: azsdk-pool-mms-ubuntu-2004-general + vmImage: MMSUbuntu20.04 + + steps: + - ${{ each image in parameters.Images }}: + - task: Docker@2 + displayName: Login to ${{ split(image.mirror, '.')[0] }} + inputs: + command: login + containerRegistry: ${{ split(image.mirror, '.')[0] }} + - task: Powershell@2 + displayName: Mirror ${{ image.source }} to ${{ image.mirror }} + inputs: + pwsh: true + filePath: $(Build.SourcesDirectory)/eng/scripts/mirror-container-image.ps1 + arguments: > + -Image ${{ image.source }} + -Mirror ${{ image.mirror }} diff --git a/eng/scripts/mirror-container-image.ps1 b/eng/scripts/mirror-container-image.ps1 new file mode 100644 index 00000000000..a5eca81b971 --- /dev/null +++ b/eng/scripts/mirror-container-image.ps1 @@ -0,0 +1,23 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Image, + [Parameter(Mandatory=$true)] + [string]$Mirror, + [switch]$RegistryLogin +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +if ($RegistryLogin) { + $mirrorRegistry = $Mirror.Split('.')[0] + Write-Host "Logging in to $mirrorRegistry" + az acr login -n $mirrorRegistry +} + +Write-Host "docker pull $Image" +docker pull $Image +Write-Host "docker tag $Image $Mirror" +docker tag $Image $Mirror +Write-Host "docker push $Mirror" +docker push $Mirror From a7626a3223909097ad5009e4b8a4b8f92a7192e4 Mon Sep 17 00:00:00 2001 From: YUTONG_ZHAI <32332316+zhaiyutong@users.noreply.github.com> Date: Sat, 2 Dec 2023 01:08:18 +0800 Subject: [PATCH 11/37] Update Test-SampleMetadata.ps1 add azure-ai-contentsafety (#7346) --- eng/common/scripts/Test-SampleMetadata.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/common/scripts/Test-SampleMetadata.ps1 b/eng/common/scripts/Test-SampleMetadata.ps1 index bea0e1d6e20..4a0000220fd 100644 --- a/eng/common/scripts/Test-SampleMetadata.ps1 +++ b/eng/common/scripts/Test-SampleMetadata.ps1 @@ -104,6 +104,7 @@ begin { "azure-active-directory-b2c", "azure-active-directory-domain", "azure-advisor", + "azure-ai-content-safety", "azure-analysis-services", "azure-anomaly-detector", "azure-api-apps", From 8f8652e3d37cbb2369c079e82490020666a7b267 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Fri, 1 Dec 2023 15:07:00 -0600 Subject: [PATCH 12/37] Show internal System.Diagnostics.CodeAnalysis attributes (#7361) --- .../APIView/APIView/Languages/CodeFileBuilder.cs | 14 ++++++++++++-- .../APIViewUnitTests/CodeFileBuilderTests.cs | 4 +++- .../APIViewUnitTests/ExactFormatting/Attributes.cs | 12 +++++++++++- .../InternalsVisibleTo/Inheritance.cs | 6 +++--- .../InternalsVisibleTo/Properties.cs | 6 +++--- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/dotnet/APIView/APIView/Languages/CodeFileBuilder.cs b/src/dotnet/APIView/APIView/Languages/CodeFileBuilder.cs index 0f5070107c0..f642fbb12ea 100644 --- a/src/dotnet/APIView/APIView/Languages/CodeFileBuilder.cs +++ b/src/dotnet/APIView/APIView/Languages/CodeFileBuilder.cs @@ -48,7 +48,7 @@ public class CodeFileBuilder public ICodeFileBuilderSymbolOrderProvider SymbolOrderProvider { get; set; } = new CodeFileBuilderSymbolOrderProvider(); - public const string CurrentVersion = "25"; + public const string CurrentVersion = "26"; private IEnumerable EnumerateNamespaces(IAssemblySymbol assemblySymbol) { @@ -460,11 +460,19 @@ private void BuildAttributes(CodeFileTokensBuilder builder, ImmutableArray FormattingFiles(string folder) { @@ -55,6 +56,7 @@ private void ExtractCodeAndFormat(string name, out string code, out string forma code = streamReader.ReadToEnd(); code = code.Trim(' ', '\t', '\r', '\n'); formatted = _stripRegex.Replace(code, string.Empty); + formatted = _retainRegex.Replace(formatted, "$1"); formatted = RemoveEmptyLines(formatted); formatted = formatted.Trim(' ', '\t', '\r', '\n'); } @@ -86,7 +88,7 @@ private async Task AssertFormattingAsync(string code, string formatted) private string RemoveEmptyLines(string content) { var lines = content - .Split(Environment.NewLine) + .Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) // handle both NewLine styles as on Windows they can be mismatched between the generated code and the expected code. .Where(s => !string.IsNullOrWhiteSpace(s)) .ToArray(); diff --git a/src/dotnet/APIView/APIViewUnitTests/ExactFormatting/Attributes.cs b/src/dotnet/APIView/APIViewUnitTests/ExactFormatting/Attributes.cs index a9ed8e1b60c..771fa32ec15 100644 --- a/src/dotnet/APIView/APIViewUnitTests/ExactFormatting/Attributes.cs +++ b/src/dotnet/APIView/APIViewUnitTests/ExactFormatting/Attributes.cs @@ -1,11 +1,20 @@ -/*-*/ +/*-*/ using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; class PrivateAttribute : Attribute { } +namespace System.Diagnostics.CodeAnalysis +{ + internal class VisibleInternalFooAttribute : Attribute + { + public VisibleInternalFooAttribute(string description) { } + } +} + /*-*/ namespace A { @@ -30,6 +39,7 @@ public class Class { [Public("s", Property = "a")] [Public(null, Property = null)] [Array(new[] {1, 2, 3})] + /*@internal @*/[VisibleInternalFoo("foo1")] public void M1()/*-*/{/*-*/;/*-*/}/*-*/ } [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] diff --git a/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Inheritance.cs b/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Inheritance.cs index 442428baf67..dbdea5aab49 100644 --- a/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Inheritance.cs +++ b/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Inheritance.cs @@ -1,4 +1,4 @@ -/*-*/using System; +/*-*/using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; using C; @@ -16,7 +16,7 @@ internal interface IInternal void M(); void N(); }/*-*/ -[Friend("TestProject")] +/*@internal @*/[Friend("TestProject")] internal interface IInternalWithFriend { void M(); void N(); @@ -35,7 +35,7 @@ public abstract class L : IDisposable, IAsyncDisposable { public abstract void Dispose(); public abstract ValueTask DisposeAsync(); } - [Friend("TestProject")] + /*@internal @*/[Friend("TestProject")] internal abstract class M : IDisposable { protected M()/*-*/{/*-*/;/*-*/}/*-*/ void IDisposable.Dispose()/*-*/{/*-*/;/*-*/}/*-*/ diff --git a/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Properties.cs b/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Properties.cs index 518e5badef9..4538211a6ad 100644 --- a/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Properties.cs +++ b/src/dotnet/APIView/APIViewUnitTests/InternalsVisibleTo/Properties.cs @@ -1,4 +1,4 @@ -/*-*/ +/*-*/ using System; using System.Runtime.CompilerServices; using B; @@ -16,9 +16,9 @@ namespace A { public class PublicClass { public PublicClass()/*-*/{/*-*/;/*-*/}/*-*/ /*-*/internal int InternalProperty { get; set; }/*-*/ - [Friend("TestProject")] + /*@internal @*/[Friend("TestProject")] internal void InternalMethodWithFriendAttribute()/*-*/{/*-*/;/*-*/}/*-*/ - [Friend("TestProject")] + /*@internal @*/[Friend("TestProject")] internal int InternalPropertyWithFriendAttribute { get; set; } /*-*/internal void InternalMethod(){ }/*-*/ public void PublicMethod()/*-*/{/*-*/;/*-*/}/*-*/ From 8a10ab0e048e40d71abbecc35544984c4944dc25 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu <31145988+chidozieononiwu@users.noreply.github.com> Date: Fri, 1 Dec 2023 18:01:44 -0800 Subject: [PATCH 13/37] Re architect/review revision restructure (#7380) * Review Revision Restructure (#7246) * extend review revision drop down Working through Review Restructure App Logic Refactor for Review Revision Restructure Remove code for adding reviews and revisions Working APIView, Some code ripped out Remove ALl Instances of Review or ReviewRevisionModel Remove ALl Instances of Review or ReviewRevisionModel Remove type filter Revisions dropdown working * Disable Pipeline Tests * Correct Revisions DropDown * Minor Bug Fixes * Sort Revision in dropdown (#7275) * Sort Revision in dropdown * Avoid repeated sorting of apiRevisions * Re architect/review revision restructure (#7303) * Sort Revision in dropdown * Avoid repeated sorting of apiRevisions * Add Automatic Review Controller * Attend to PR comments * Surface Package Version in ApiRevision Label * compareAllrevisions check against all approve automatic revisions * Re Add ReviewBackgroundHostedService.cs (#7331) * Re-Add Pull Request Controller and Pull Request Background Services (#7336) * Sort Revision in dropdown * Add Automatic Review Controller * Work on Pull Request Controller * Continued work on PR controller * Add pullRequest controller and pullRequest background task * Re Add Review Controller (#7337) * Re architect/LegacyReviewSupport (#7359) * Redirect URIs with legecy review Id * Redirect URI with legacy ReviewId * Resolve build issues (#7362) * Add Task for Comuting Line numbers swagger diffs (#7364) * Resolve build issues (#7365) * Resolve build issues (#7367) * Resolve APIView Issues Identified during Manual Testing (#7377) * Update logic for verifyin if activeAPIRevision is ahead of DiffAPIRevision (#7378) * Rename RevisionId to API RevisionId (#7381) * Resolve some minor issues --- .../ReviewManagerTests.cs | 54 +- .../TestsBaseFixture.cs | 30 +- .../APIView/APIViewUITests/SmokeTests.cs | 3 + .../APIRevisionsManagerTests.cs | 89 ++ .../APIViewUnitTests/ManagerHelperTests.cs | 74 + .../APIView/APIViewWeb/APIViewWeb.csproj | 5 - ... => APIRevisionOwnerRequirementHandler.cs} | 12 +- .../AutoAPIRevisionModifierRequirement.cs | 16 + ...oAPIRevisionModifierRequirementHandler.cs} | 15 +- .../Account/AutoReviewModifierRequirement.cs | 16 - .../Account/CommentOwnerRequirementHandler.cs | 7 +- .../Account/ResolverRequirementHandler.cs | 4 +- .../Account/ReviewOwnerRequirementHandler.cs | 7 +- ...SamplesRevisionOwnerRequirementHandler.cs} | 7 +- .../APIViewWeb/Client/css/pages/review.scss | 6 +- .../APIViewWeb/Client/src/pages/index.ts | 9 +- .../Client/src/pages/review.module.ts | 14 + .../APIViewWeb/Client/src/pages/review.ts | 43 +- .../Controllers/AutoReviewController.cs | 326 +++-- .../Controllers/CommentsController.cs | 16 +- .../Controllers/PullRequestController.cs | 350 +++-- .../Controllers/ReviewController.cs | 95 +- .../APIView/APIViewWeb/Helpers/APIHelpers.cs | 61 +- .../APIViewWeb/Helpers/AutoMapperProfiles.cs | 2 +- .../Helpers/ChangeHistoryHelpers.cs | 221 +++ .../APIViewWeb/Helpers/CosmosQueryHelpers.cs | 32 + .../Helpers/LanguageServiceHelpers.cs | 5 + .../APIViewWeb/Helpers/ManagerHelpers.cs | 98 ++ .../APIViewWeb/Helpers/PageModelHelpers.cs | 506 ++++++- .../LinesWithDiffBackgroundHostedService.cs | 59 + .../PullRequestBackgroundHostedService.cs | 2 +- .../ReviewBackgroundHostedService.cs | 14 +- .../APIViewWeb/Languages/LanguageService.cs | 2 +- .../Languages/TypeSpecLanguageService.cs | 2 +- .../APIViewWeb/LeanModels/ChangeHistory.cs | 60 +- .../APIViewWeb/LeanModels/CommentItemModel.cs | 38 + .../APIViewWeb/LeanModels/ReviewListModels.cs | 143 ++ .../LeanModels/ReviewRevisionPageModels.cs | 23 + .../APIViewWeb/Managers/AICommentsManager.cs | 8 +- .../Managers/APIRevisionsManager.cs | 744 ++++++++++ .../APIViewWeb/Managers/CodeFileManager.cs | 198 +++ .../APIViewWeb/Managers/CommentsManager.cs | 103 +- .../Interfaces/IAPIRevisionsManager.cs | 40 + .../Managers/Interfaces/ICodeFileManager.cs | 17 + .../Managers/Interfaces/ICommentsManager.cs | 10 +- .../Interfaces/INotificationManager.cs | 11 +- .../Interfaces/IPullRequestManager.cs | 10 +- .../Managers/Interfaces/IReviewManager.cs | 50 +- .../Interfaces/ISamplesRevisionsManager.cs | 17 + .../Interfaces/IUsageSampleManager.cs | 16 - .../Managers/NotificationManager.cs | 77 +- .../APIViewWeb/Managers/PullRequestManager.cs | 408 +----- .../APIViewWeb/Managers/ReviewManager.cs | 1163 ++++------------ .../Managers/SamplesRevisionsManager.cs | 117 ++ .../APIViewWeb/Managers/UsageSampleManager.cs | 143 -- ...ewCodeFileModel.cs => APICodeFileModel.cs} | 4 +- ...PIRevisionGenerationPipelineParamModel.cs} | 20 +- .../APIView/APIViewWeb/Models/CommentModel.cs | 28 - .../APIViewWeb/Models/CommentThreadModel.cs | 11 +- .../APIViewWeb/Models/PackageGroupMdel.cs | 14 - .../APIViewWeb/Models/PullRequestModel.cs | 8 +- .../APIViewWeb/Models/ReviewCommentsModel.cs | 7 +- .../APIViewWeb/Models/ReviewDisplayModel.cs | 36 - .../APIView/APIViewWeb/Models/ReviewModel.cs | 138 -- .../APIViewWeb/Models/ReviewRevisionModel.cs | 78 -- .../Models/ReviewRevisionModelList.cs | 89 -- ...Model.cs => SamplesRevisionUploadModel.cs} | 7 +- .../APIViewWeb/Models/UsageSampleModel.cs | 21 - .../Models/UsageSampleRevisionModel.cs | 28 - .../APIViewWeb/Models/UserPreferenceModel.cs | 11 +- .../Pages/Assemblies/Conversation.cshtml | 45 +- .../Pages/Assemblies/Conversation.cshtml.cs | 72 +- .../Pages/Assemblies/Delete.cshtml.cs | 4 +- .../APIViewWeb/Pages/Assemblies/Index.cshtml | 68 +- .../Pages/Assemblies/Index.cshtml.cs | 81 +- .../Pages/Assemblies/RequestedReviews.cshtml | 148 -- .../Assemblies/RequestedReviews.cshtml.cs | 11 +- .../APIViewWeb/Pages/Assemblies/Review.cshtml | 1213 +++++++++-------- .../Pages/Assemblies/Review.cshtml.cs | 547 +++----- .../Pages/Assemblies/Revisions.cshtml | 119 +- .../Pages/Assemblies/Revisions.cshtml.cs | 30 +- .../Pages/Assemblies/Samples.cshtml | 41 +- .../Pages/Assemblies/Samples.cshtml.cs | 65 +- .../Pages/Assemblies/_CodeLine.cshtml | 2 +- .../Shared/_CommentThreadInnerPartial.cshtml | 26 +- .../Pages/Shared/_CommentThreadPartial.cshtml | 43 +- .../APIViewWeb/Pages/Shared/_Layout.cshtml | 5 - .../Pages/Shared/_ReviewBadge.cshtml | 23 +- .../Pages/Shared/_ReviewsPartial.cshtml | 31 +- .../_RevisionSelectPickerPartial.cshtml | 58 + .../Pages/Shared/_SelectPickerPartial.cshtml | 6 +- .../Repositories/AICommentsRepository.cs | 4 +- .../Repositories/BlobCodeFileRepository.cs | 6 +- .../CosmosAPIRevisionsRepository.cs | 228 ++++ .../Repositories/CosmosCommentsRepository.cs | 41 +- .../CosmosPullRequestsRepository.cs | 14 +- .../Repositories/CosmosReviewRepository.cs | 221 +-- .../CosmosSamplesRevisionsRepository.cs | 51 + .../CosmosUsageSampleRepository.cs | 56 - .../CosmosUserProfileRepository.cs | 2 +- .../Interfaces/IBlobCodeFileRepository.cs | 3 +- .../ICosmosAPIRevisionsRepository.cs | 48 + .../Interfaces/ICosmosCommentsRepository.cs | 11 +- .../Interfaces/ICosmosReviewRepository.cs | 21 +- .../ICosmosSamplesRevisionsRepository.cs | 14 + .../ICosmosUsageSampleRepository.cs | 13 - .../Repositories/UserPreferenceCache.cs | 7 +- src/dotnet/APIView/APIViewWeb/Startup.cs | 16 +- src/dotnet/APIView/apiview.yml | 232 ++-- 109 files changed, 5552 insertions(+), 4172 deletions(-) create mode 100644 src/dotnet/APIView/APIViewUnitTests/APIRevisionsManagerTests.cs create mode 100644 src/dotnet/APIView/APIViewUnitTests/ManagerHelperTests.cs rename src/dotnet/APIView/APIViewWeb/Account/{RevisionOwnerRequirementHandler.cs => APIRevisionOwnerRequirementHandler.cs} (66%) create mode 100644 src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirement.cs rename src/dotnet/APIView/APIViewWeb/Account/{AutoReviewModifierRequirementHandler.cs => AutoAPIRevisionModifierRequirementHandler.cs} (53%) delete mode 100644 src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirement.cs rename src/dotnet/APIView/APIViewWeb/Account/{UsageSampleOwnerRequirementHandler.cs => SamplesRevisionOwnerRequirementHandler.cs} (66%) create mode 100644 src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Helpers/CosmosQueryHelpers.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Helpers/ManagerHelpers.cs create mode 100644 src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs create mode 100644 src/dotnet/APIView/APIViewWeb/LeanModels/CommentItemModel.cs create mode 100644 src/dotnet/APIView/APIViewWeb/LeanModels/ReviewListModels.cs create mode 100644 src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ISamplesRevisionsManager.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IUsageSampleManager.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/SamplesRevisionsManager.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Managers/UsageSampleManager.cs rename src/dotnet/APIView/APIViewWeb/Models/{ReviewCodeFileModel.cs => APICodeFileModel.cs} (91%) rename src/dotnet/APIView/APIViewWeb/Models/{ReviewGenPipelineParamModel.cs => APIRevisionGenerationPipelineParamModel.cs} (88%) delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModelList.cs rename src/dotnet/APIView/APIViewWeb/Models/{UsageSampleUploadModel.cs => SamplesRevisionUploadModel.cs} (81%) delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Models/UsageSampleRevisionModel.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml create mode 100644 src/dotnet/APIView/APIViewWeb/Pages/Shared/_RevisionSelectPickerPartial.cshtml create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/CosmosAPIRevisionsRepository.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/CosmosSamplesRevisionsRepository.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/CosmosUsageSampleRepository.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosAPIRevisionsRepository.cs create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosSamplesRevisionsRepository.cs delete mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosUsageSampleRepository.cs diff --git a/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs b/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs index 588e8d8876d..5bf457f688a 100644 --- a/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs +++ b/src/dotnet/APIView/APIViewIntegrationTests/ReviewManagerTests.cs @@ -2,8 +2,9 @@ using Xunit; using System.IO; using System; -using APIViewWeb; using APIViewWeb.Repositories; +using APIViewWeb.LeanModels; +using System.Linq; namespace APIViewIntegrationTests { @@ -52,11 +53,18 @@ public void Dispose() public async Task AddRevisionAsync_Computes_Headings_Of_Sections_With_Diff_A() { var reviewManager = testsBaseFixture.ReviewManager; + var apiRevisionsManager = testsBaseFixture.APIRevisionManager; var user = testsBaseFixture.User; - var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(user, fileNameA, "Revision1", fileStreamA, false, "Swagger", true); - await reviewManager.AddRevisionAsync(user, review.ReviewId, fileNameB, "Revision2", fileStreamB, "Swagger", true); - review = await reviewManager.GetReviewAsync(user, review.ReviewId); - var headingWithDiffInSections = review.Revisions[0].HeadingsOfSectionsWithDiff[review.Revisions[1].RevisionId]; + var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(packageName: "testPackageA", language: "Swagger", isClosed:false); + + await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, name: fileNameA, + label: "Revision1", fileStream: fileStreamA, language: "Swagger", awaitComputeDiff: true); + await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, name: fileNameB, + label: "Revision2", fileStream: fileStreamB, language: "Swagger", awaitComputeDiff: true); + + var apiRevisions = (await apiRevisionsManager.GetAPIRevisionsAsync(review.Id)).ToList(); + + var headingWithDiffInSections = apiRevisions[0].HeadingsOfSectionsWithDiff[apiRevisions[1].Id]; Assert.All(headingWithDiffInSections, item => Assert.Contains(item, new int[] { 2, 16 })); } @@ -65,11 +73,19 @@ public async Task AddRevisionAsync_Computes_Headings_Of_Sections_With_Diff_A() public async Task AddRevisionAsync_Computes_Headings_Of_Sections_With_Diff_B() { var reviewManager = testsBaseFixture.ReviewManager; + var apiRevisionsManager = testsBaseFixture.APIRevisionManager; var user = testsBaseFixture.User; - var review = await reviewManager.CreateReviewAsync(user, fileNameC, "Azure.Analytics.Purview.Account", fileStreamC, false, "Swagger", true); - await reviewManager.AddRevisionAsync(user, review.ReviewId, fileNameD, "Azure.Analytics.Purview.Account", fileStreamD, "Swagger", true); - review = await reviewManager.GetReviewAsync(user, review.ReviewId); - var headingWithDiffInSections = review.Revisions[0].HeadingsOfSectionsWithDiff[review.Revisions[1].RevisionId]; + + var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(packageName: "testPackageB", language: "Swagger", isClosed: false); + + await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, name: fileNameC, + label: "Azure.Analytics.Purview.Account", fileStream: fileStreamC, language: "Swagger", awaitComputeDiff: true); + await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, name: fileNameD, + label: "Azure.Analytics.Purview.Accoun", fileStream: fileStreamD, language: "Swagger", awaitComputeDiff: true); + + var apiRevisions = (await apiRevisionsManager.GetAPIRevisionsAsync(review.Id)).ToList(); + + var headingWithDiffInSections = apiRevisions[0].HeadingsOfSectionsWithDiff[apiRevisions[1].Id]; Assert.All(headingWithDiffInSections, item => Assert.Contains(item, new int[] { 20, 275 })); } @@ -78,17 +94,25 @@ public async Task AddRevisionAsync_Computes_Headings_Of_Sections_With_Diff_B() public async Task Delete_PullRequest_Review_Throws_Exception() { var reviewManager = testsBaseFixture.ReviewManager; + var apiRevisionsManager = testsBaseFixture.APIRevisionManager; var user = testsBaseFixture.User; - var review = await reviewManager.CreateReviewAsync(user, fileNameC, "Azure.Analytics.Purview.Account", fileStreamC, false, "Swagger", false); - Assert.Equal(ReviewType.Manual, review.FilterType); - review.FilterType = ReviewType.PullRequest; + var review = await testsBaseFixture.ReviewManager.CreateReviewAsync(packageName: "testPackageC", language: "Swagger", isClosed: false); + + await apiRevisionsManager.AddAPIRevisionAsync(user: user, reviewId: review.Id, apiRevisionType: APIRevisionType.Manual, name: fileNameC, + label: "Azure.Analytics.Purview.Account", fileStream: fileStreamC, language: "Swagger", awaitComputeDiff: true); + + var apiRevisions = (await apiRevisionsManager.GetAPIRevisionsAsync(review.Id)).ToList(); + + Assert.Equal(APIRevisionType.Manual, apiRevisions[0].APIRevisionType); + + apiRevisions[0].APIRevisionType = APIRevisionType.PullRequest; // Change review type to PullRequest - await testsBaseFixture.ReviewRepository.UpsertReviewAsync(review); - Assert.Equal(ReviewType.PullRequest, review.FilterType); + await testsBaseFixture.APIRevisionRepository.UpsertAPIRevisionAsync(apiRevisions[0]); + Assert.Equal(APIRevisionType.PullRequest, apiRevisions[0].APIRevisionType); - await Assert.ThrowsAsync(async () => await reviewManager.DeleteRevisionAsync(user, review.ReviewId, review.Revisions[0].RevisionId)); + await Assert.ThrowsAsync(async () => await apiRevisionsManager.SoftDeleteAPIRevisionAsync(user, review.Id, apiRevisions[0].Id)); } } } diff --git a/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs b/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs index 585d25f12a1..8e5afc5cb03 100644 --- a/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs +++ b/src/dotnet/APIView/APIViewIntegrationTests/TestsBaseFixture.cs @@ -18,6 +18,9 @@ using APIViewWeb.Managers; using APIViewWeb.Hubs; using Microsoft.AspNetCore.SignalR; +using APIViewWeb.Managers.Interfaces; +using Microsoft.CodeAnalysis.Host; +using Microsoft.Extensions.Options; namespace APIViewIntegrationTests { @@ -29,8 +32,12 @@ public class TestsBaseFixture : IDisposable public PackageNameManager PackageNameManager { get; private set; } public ReviewManager ReviewManager { get; private set; } + public CommentsManager CommentsManager { get; private set; } + public CodeFileManager CodeFileManager { get; private set; } + public APIRevisionsManager APIRevisionManager { get; private set; } public BlobCodeFileRepository BlobCodeFileRepository { get; private set; } public CosmosReviewRepository ReviewRepository { get; private set; } + public CosmosAPIRevisionsRepository APIRevisionRepository { get; private set; } public CosmosCommentsRepository CommentRepository { get; private set; } public ClaimsPrincipal User { get; private set; } public string TestDataPath { get; private set; } @@ -67,9 +74,11 @@ public TestsBaseFixture() _cosmosClient = new CosmosClient(config["Cosmos:ConnectionString"]); var dataBaseResponse = _cosmosClient.CreateDatabaseIfNotExistsAsync("APIView").Result; dataBaseResponse.Database.CreateContainerIfNotExistsAsync("Reviews", "/id").Wait(); + dataBaseResponse.Database.CreateContainerIfNotExistsAsync("APIRevisions", "/ReviewId").Wait(); dataBaseResponse.Database.CreateContainerIfNotExistsAsync("Comments", "/ReviewId").Wait(); dataBaseResponse.Database.CreateContainerIfNotExistsAsync("Profiles", "/id").Wait(); ReviewRepository = new CosmosReviewRepository(config, _cosmosClient); + APIRevisionRepository = new CosmosAPIRevisionsRepository(config, _cosmosClient); CommentRepository = new CosmosCommentsRepository(config, _cosmosClient); var cosmosUserProfileRepository = new CosmosUserProfileRepository(config, _cosmosClient); @@ -95,10 +104,27 @@ public TestsBaseFixture() .Returns(Task.CompletedTask); var signalRHubContextMoq = new Mock>(); + var options = new Mock>(); + + CommentsManager = new CommentsManager( + authorizationService: authorizationServiceMoq.Object, commentsRepository: CommentRepository, + notificationManager: notificationManager, options: options.Object); + + CodeFileManager = new CodeFileManager( + languageServices: languageService, codeFileRepository: BlobCodeFileRepository, + originalsRepository: blobOriginalsRepository, devopsArtifactRepository: devopsArtifactRepositoryMoq.Object); + + APIRevisionManager = new APIRevisionsManager( + authorizationService: authorizationServiceMoq.Object, reviewsRepository: ReviewRepository, + languageServices: languageService, devopsArtifactRepository: devopsArtifactRepositoryMoq.Object, + codeFileManager: CodeFileManager, codeFileRepository: BlobCodeFileRepository, apiRevisionsRepository: APIRevisionRepository, + originalsRepository: blobOriginalsRepository, notificationManager: notificationManager, signalRHubContext: signalRHubContextMoq.Object); + ReviewManager = new ReviewManager( - authorizationServiceMoq.Object, ReviewRepository, BlobCodeFileRepository, blobOriginalsRepository, CommentRepository, - languageService, notificationManager, devopsArtifactRepositoryMoq.Object, PackageNameManager, signalRHubContextMoq.Object); + authorizationService: authorizationServiceMoq.Object, reviewsRepository: ReviewRepository, + apiRevisionsManager: APIRevisionManager, commentManager: CommentsManager, codeFileRepository: BlobCodeFileRepository, + commentsRepository: CommentRepository, languageServices: languageService, signalRHubContext: signalRHubContextMoq.Object); TestDataPath = config["TestPkgPath"]; } diff --git a/src/dotnet/APIView/APIViewUITests/SmokeTests.cs b/src/dotnet/APIView/APIViewUITests/SmokeTests.cs index 458b744b34b..f2cc6614728 100644 --- a/src/dotnet/APIView/APIViewUITests/SmokeTests.cs +++ b/src/dotnet/APIView/APIViewUITests/SmokeTests.cs @@ -15,8 +15,10 @@ using System.Net.Http.Headers; using System.Threading.Tasks; +#if false namespace APIViewUITests { + public class SmokeTestsFixture : IDisposable { internal readonly HttpClient _httpClient; @@ -320,3 +322,4 @@ public void ReviewFilterOptionsWorkWithoutErrors() } } } +#endif diff --git a/src/dotnet/APIView/APIViewUnitTests/APIRevisionsManagerTests.cs b/src/dotnet/APIView/APIViewUnitTests/APIRevisionsManagerTests.cs new file mode 100644 index 00000000000..e9a3a451485 --- /dev/null +++ b/src/dotnet/APIView/APIViewUnitTests/APIRevisionsManagerTests.cs @@ -0,0 +1,89 @@ +using APIViewWeb; +using Xunit; +using APIViewWeb.Repositories; +using Moq; +using System.IO; +using APIViewWeb.Managers.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using APIViewWeb.Hubs; +using System.Collections; +using System.Collections.Generic; +using APIViewWeb.Managers; +using System; +using System.Threading.Tasks; +using APIViewWeb.LeanModels; + +namespace APIViewUnitTests +{ + public class APIRevisionsManagerTests + { + private readonly IAPIRevisionsManager _apiRevisionsManager; + + public APIRevisionsManagerTests() + { + IAuthorizationService authorizationService = new Mock().Object; + ICosmosReviewRepository cosmosReviewRepository = new Mock().Object; + ICosmosAPIRevisionsRepository cosmosAPIRevisionsRepository = new Mock().Object; + IHubContext signalRHub = new Mock>().Object; + IEnumerable languageServices = new List(); + IDevopsArtifactRepository devopsArtifactRepository = new Mock().Object; + ICodeFileManager codeFileManager = new Mock().Object; + IBlobCodeFileRepository blobCodeFileRepository = new Mock().Object; + IBlobOriginalsRepository blobOriginalRepository = new Mock().Object; + INotificationManager notificationManager = new Mock().Object; + + _apiRevisionsManager = new APIRevisionsManager( + authorizationService: authorizationService, reviewsRepository: cosmosReviewRepository, + apiRevisionsRepository: cosmosAPIRevisionsRepository, signalRHubContext: signalRHub, + languageServices: languageServices, devopsArtifactRepository: devopsArtifactRepository, + codeFileManager: codeFileManager, codeFileRepository: blobCodeFileRepository, + originalsRepository: blobOriginalRepository, notificationManager: notificationManager); + } + + // GetLatestAPIRevisionsAsync + + [Fact] + public async Task GetLatestAPIRevisionsAsyncThrowsExceptionWhenReviewIdAndAPIRevisionsAreAbsent() + { + await Assert.ThrowsAsync(async () => await _apiRevisionsManager.GetLatestAPIRevisionsAsync(null, null)); + } + + [Fact] + public async Task GetLatestAPIRevisionsAsyncReturnsDefaultIfNoLatestAPIRevisionIsFound() + { + var latest = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(apiRevisions: new List()); + Assert.Equal(default(APIRevisionListItemModel), latest); + } + + [Fact] + public async Task GetLatestAPIRevisionsAsyncReturnsCorrectLatestWithAllTypesPresent() + { + var apiRevisions = new List() + { + new APIRevisionListItemModel() { Id ="A", APIRevisionType = APIRevisionType.Manual, CreatedOn = DateTime.Now.AddMinutes(5) }, + new APIRevisionListItemModel() { Id ="B", APIRevisionType = APIRevisionType.Automatic, CreatedOn = DateTime.Now.AddMinutes(10) }, + new APIRevisionListItemModel() { Id ="C", APIRevisionType = APIRevisionType.PullRequest, CreatedOn = DateTime.Now.AddMinutes(15) }, + }; + var latest = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(apiRevisions: apiRevisions); + Assert.Equal("C", latest.Id); + + var latestAutomatic = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(apiRevisions: apiRevisions, apiRevisionType: APIRevisionType.Automatic); + Assert.Equal("B", latestAutomatic.Id); + } + + [Fact] + public async Task GetLatestAPIRevisionsAsyncReturnsCorrectLatestWhenSpecifiedTypeIsAbsent() + { + var apiRevisions = new List() + { + new APIRevisionListItemModel() { Id ="A", APIRevisionType = APIRevisionType.Manual, CreatedOn = DateTime.Now.AddMinutes(5) }, + new APIRevisionListItemModel() { Id ="B", APIRevisionType = APIRevisionType.Automatic, CreatedOn = DateTime.Now.AddMinutes(10) }, + }; + + var latest = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(apiRevisions: apiRevisions, apiRevisionType: APIRevisionType.PullRequest); + Assert.Equal("B", latest.Id); + } + + } +} diff --git a/src/dotnet/APIView/APIViewUnitTests/ManagerHelperTests.cs b/src/dotnet/APIView/APIViewUnitTests/ManagerHelperTests.cs new file mode 100644 index 00000000000..3dce8e3b61e --- /dev/null +++ b/src/dotnet/APIView/APIViewUnitTests/ManagerHelperTests.cs @@ -0,0 +1,74 @@ +using APIViewWeb; +using APIViewWeb.Helpers; +using Xunit; +using APIViewWeb.LeanModels; + +namespace APIViewUnitTests +{ + public class ManagerHelperTests + { + [Fact] + public void UpdateChangeHistory_Behaves_As_Expected() + { + var review = new ReviewListItemModel(); + Assert.Empty(review.ChangeHistory); + + // test_User_1 approves + var updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, "test_User_1", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Single(review.ChangeHistory); + Assert.True(updateResult.ChangeStatus); + + // test_User_1 reverts approval + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.ApprovalReverted, "test_User_1", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(2, review.ChangeHistory.Count); + Assert.False(updateResult.ChangeStatus); + + // test_User_2 Closed + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Closed, "test_User_2", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(3, review.ChangeHistory.Count); + Assert.True(updateResult.ChangeStatus); + + // test_User_2 approves + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, "test_User_2", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(4, review.ChangeHistory.Count); + Assert.True(updateResult.ChangeStatus); + + // test_User_3 approves + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, "test_User_3", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(5, review.ChangeHistory.Count); + Assert.True(updateResult.ChangeStatus); + + // test_User_3 reverts approval + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, "test_User_3", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(6, review.ChangeHistory.Count); + Assert.True(updateResult.ChangeStatus); + + // test_User_2 reverts approval + updateResult = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, "test_User_2", "test_note"); + review.ChangeHistory = updateResult.ChangeHistory; + Assert.Equal(7, review.ChangeHistory.Count); + Assert.False(updateResult.ChangeStatus); + + Assert.True(review.ChangeHistory[0].ChangeAction == ReviewChangeAction.Approved && review.ChangeHistory[0].ChangedBy == "test_User_1"); + Assert.True(review.ChangeHistory[1].ChangeAction == ReviewChangeAction.ApprovalReverted && review.ChangeHistory[1].ChangedBy == "test_User_1"); + Assert.True(review.ChangeHistory[2].ChangeAction == ReviewChangeAction.Closed && review.ChangeHistory[2].ChangedBy == "test_User_2"); + Assert.True(review.ChangeHistory[3].ChangeAction == ReviewChangeAction.Approved && review.ChangeHistory[3].ChangedBy == "test_User_2"); + Assert.True(review.ChangeHistory[4].ChangeAction == ReviewChangeAction.Approved && review.ChangeHistory[4].ChangedBy == "test_User_3"); + Assert.True(review.ChangeHistory[5].ChangeAction == ReviewChangeAction.ApprovalReverted && review.ChangeHistory[5].ChangedBy == "test_User_3"); + Assert.True(review.ChangeHistory[6].ChangeAction == ReviewChangeAction.ApprovalReverted && review.ChangeHistory[6].ChangedBy == "test_User_2"); + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj index 81cafecae09..6ecd5d04fd2 100644 --- a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj +++ b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj @@ -75,9 +75,4 @@ - - - - - diff --git a/src/dotnet/APIView/APIViewWeb/Account/RevisionOwnerRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/APIRevisionOwnerRequirementHandler.cs similarity index 66% rename from src/dotnet/APIView/APIViewWeb/Account/RevisionOwnerRequirementHandler.cs rename to src/dotnet/APIView/APIViewWeb/Account/APIRevisionOwnerRequirementHandler.cs index 85885bd83d8..b0a33f3b393 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/RevisionOwnerRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/APIRevisionOwnerRequirementHandler.cs @@ -1,14 +1,15 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Threading.Tasks; +using APIViewWeb.LeanModels; using Microsoft.AspNetCore.Authorization; namespace APIViewWeb { - public class RevisionOwnerRequirementHandler : IAuthorizationHandler + public class APIRevisionOwnerRequirementHandler : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { @@ -16,10 +17,9 @@ public Task HandleAsync(AuthorizationHandlerContext context) { if (requirement is RevisionOwnerRequirement) { - var revision = context.Resource as ReviewRevisionModel; + var revision = context.Resource as APIRevisionListItemModel; var loggedInUser = context.User.GetGitHubLogin(); - if (revision.Author == loggedInUser || - revision.Review.Author == loggedInUser) + if (revision.CreatedBy == loggedInUser) { context.Succeed(requirement); } @@ -28,4 +28,4 @@ public Task HandleAsync(AuthorizationHandlerContext context) return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirement.cs b/src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirement.cs new file mode 100644 index 00000000000..7f46b7f8074 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirement.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authorization; + +namespace APIViewWeb +{ + public class AutoAPIRevisionModifierRequirement : IAuthorizationRequirement + { + protected AutoAPIRevisionModifierRequirement() + { + } + + public static AutoAPIRevisionModifierRequirement Instance { get; } = new AutoAPIRevisionModifierRequirement(); + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirementHandler.cs similarity index 53% rename from src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirementHandler.cs rename to src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirementHandler.cs index 6da4dc3f8b4..ce952f995ec 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/AutoAPIRevisionModifierRequirementHandler.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Threading.Tasks; +using APIViewWeb.LeanModels; using Microsoft.AspNetCore.Authorization; namespace APIViewWeb { - public class AutoReviewModifierRequirementHandler : IAuthorizationHandler + public class AutoAPIRevisionModifierRequirementHandler : IAuthorizationHandler { private static string _autoReviewOwner = "azure-sdk"; public Task HandleAsync(AuthorizationHandlerContext context) @@ -16,11 +17,11 @@ public Task HandleAsync(AuthorizationHandlerContext context) var loggedInUser = context.User.GetGitHubLogin(); foreach (var requirement in context.Requirements) { - if (requirement is AutoReviewModifierRequirement) + if (requirement is AutoAPIRevisionModifierRequirement) { - var review = ((ReviewModel)context.Resource); - // If review is auto created by bot then ensure logged in user is bot and review owner is bot - if (!review.IsAutomatic || (loggedInUser == _autoReviewOwner && review.Author == _autoReviewOwner)) + var apiRevision = ((APIRevisionListItemModel)context.Resource); + // If apiRevision is auto created by bot then ensure logged in user is bot and apiRevision owner is bot + if (apiRevision.APIRevisionType != APIRevisionType.Automatic || (loggedInUser == _autoReviewOwner && apiRevision.CreatedBy == _autoReviewOwner)) { context.Succeed(requirement); } @@ -30,4 +31,4 @@ public Task HandleAsync(AuthorizationHandlerContext context) return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirement.cs b/src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirement.cs deleted file mode 100644 index 5d5f17507d9..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Account/AutoReviewModifierRequirement.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Authorization; - -namespace APIViewWeb -{ - public class AutoReviewModifierRequirement : IAuthorizationRequirement - { - protected AutoReviewModifierRequirement() - { - } - - public static AutoReviewModifierRequirement Instance { get; } = new AutoReviewModifierRequirement(); - } -} \ No newline at end of file diff --git a/src/dotnet/APIView/APIViewWeb/Account/CommentOwnerRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/CommentOwnerRequirementHandler.cs index 75a9e631d2e..0df3a0df6bc 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/CommentOwnerRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/CommentOwnerRequirementHandler.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Threading.Tasks; +using APIViewWeb.LeanModels; using APIViewWeb.Models; using Microsoft.AspNetCore.Authorization; @@ -15,7 +16,7 @@ public Task HandleAsync(AuthorizationHandlerContext context) { if (requirement is CommentOwnerRequirement) { - if (((CommentModel)context.Resource).Username == context.User.GetGitHubLogin()) + if (((CommentItemModel)context.Resource).CreatedBy == context.User.GetGitHubLogin()) { context.Succeed(requirement); } @@ -24,4 +25,4 @@ public Task HandleAsync(AuthorizationHandlerContext context) return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIViewWeb/Account/ResolverRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/ResolverRequirementHandler.cs index f66f2bc1cf3..f366423c4f2 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/ResolverRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/ResolverRequirementHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -20,7 +20,7 @@ public ResolverRequirementHandler(IConfiguration configuration) : base(configura if (requirement is ResolverRequirement) { Models.CommentThreadModel comments = (Models.CommentThreadModel)context.Resource; - if (approvers != null && approvers.Contains(context.User.GetGitHubLogin()) || context.User.GetGitHubLogin().Equals(comments.Comments.First().Username)) + if (approvers != null && approvers.Contains(context.User.GetGitHubLogin()) || context.User.GetGitHubLogin().Equals(comments.Comments.First().CreatedBy)) { context.Succeed(requirement); } diff --git a/src/dotnet/APIView/APIViewWeb/Account/ReviewOwnerRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/ReviewOwnerRequirementHandler.cs index 1e8b035055a..ec1044740e2 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/ReviewOwnerRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/ReviewOwnerRequirementHandler.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Threading.Tasks; +using APIViewWeb.LeanModels; using Microsoft.AspNetCore.Authorization; namespace APIViewWeb @@ -14,7 +15,7 @@ public Task HandleAsync(AuthorizationHandlerContext context) { if (requirement is ReviewOwnerRequirement) { - if (((ReviewModel)context.Resource).Author == context.User.GetGitHubLogin()) + if (((ReviewListItemModel)context.Resource).CreatedBy == context.User.GetGitHubLogin()) { context.Succeed(requirement); } @@ -23,4 +24,4 @@ public Task HandleAsync(AuthorizationHandlerContext context) return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/src/dotnet/APIView/APIViewWeb/Account/UsageSampleOwnerRequirementHandler.cs b/src/dotnet/APIView/APIViewWeb/Account/SamplesRevisionOwnerRequirementHandler.cs similarity index 66% rename from src/dotnet/APIView/APIViewWeb/Account/UsageSampleOwnerRequirementHandler.cs rename to src/dotnet/APIView/APIViewWeb/Account/SamplesRevisionOwnerRequirementHandler.cs index 8e7021a46ed..d4ebb0e3653 100644 --- a/src/dotnet/APIView/APIViewWeb/Account/UsageSampleOwnerRequirementHandler.cs +++ b/src/dotnet/APIView/APIViewWeb/Account/SamplesRevisionOwnerRequirementHandler.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Threading.Tasks; +using APIViewWeb.LeanModels; using Microsoft.AspNetCore.Authorization; namespace APIViewWeb { - public class UsageSampleOwnerRequirementHandler : IAuthorizationHandler + public class SamplesRevisionOwnerRequirementHandler : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { @@ -14,7 +15,7 @@ public Task HandleAsync(AuthorizationHandlerContext context) { if (requirement is UsageSampleOwnerRequirement) { - if (((UsageSampleRevisionModel)context.Resource).CreatedBy == context.User.GetGitHubLogin()) + if (((SamplesRevisionModel)context.Resource).CreatedBy == context.User.GetGitHubLogin()) { context.Succeed(requirement); } diff --git a/src/dotnet/APIView/APIViewWeb/Client/css/pages/review.scss b/src/dotnet/APIView/APIViewWeb/Client/css/pages/review.scss index af616f12ac6..0bfc402432e 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/css/pages/review.scss +++ b/src/dotnet/APIView/APIViewWeb/Client/css/pages/review.scss @@ -1,9 +1,13 @@ @import "../shared/mixins.scss"; -#review-info-bar > .SumoSelect { +#review-info-bar > .SumoSelect:nth-of-type(even) { width: 20%; } +#review-info-bar > .SumoSelect:nth-of-type(odd) { + width: 10%; +} + #revision-select ~ .optWrapper { width: auto; min-width: 250px; diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/pages/index.ts b/src/dotnet/APIView/APIViewWeb/Client/src/pages/index.ts index f6a1ef0d3f5..50e4e467c39 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/pages/index.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/pages/index.ts @@ -7,7 +7,6 @@ $(() => { const languageFilter = $( '#language-filter-select' ); const stateFilter = $( '#state-filter-select' ); const statusFilter = $( '#status-filter-select' ); - const typeFilter = $( '#type-filter-select' ); const searchBox = $( '#reviews-table-search-box' ); const searchButton = $( '#reviews-search-button' ); const resetButton = $( '#reset-filter-button' ); @@ -47,10 +46,6 @@ $(() => { uri = uri + '&status=' + encodeURIComponent(`${$(this).val()}`); }); - typeFilter.children(":selected").each(function() { - uri = uri + '&type=' + encodeURIComponent(`${$(this).val()}`); - }); - uri = uri + '&pageNo=' + encodeURIComponent(pageNo); uri = uri + '&pageSize=' + encodeURIComponent(pageSize); uri = encodeURI(uri); @@ -85,7 +80,6 @@ $(() => { (languageFilter).SumoSelect({ selectAll: true }); (stateFilter).SumoSelect({ selectAll: true }); (statusFilter).SumoSelect({ selectAll: true }); - (typeFilter).SumoSelect({ selectAll: true }); addPaginationEventHandlers(); }); @@ -104,7 +98,7 @@ $(() => { }); // Update list of reviews when any dropdown is changed - [languageFilter, stateFilter, statusFilter, typeFilter].forEach(function(value, index) { + [languageFilter, stateFilter, statusFilter].forEach(function(value, index) { value.on('sumo:closed', function() { updateListedReviews(); }); @@ -125,7 +119,6 @@ $(() => { ($('#state-filter-select')[0]).sumo.unSelectAll(); ($('#state-filter-select')[0]).sumo.selectItem('Open'); ($('#status-filter-select')[0]).sumo.unSelectAll(); - ($('#type-filter-select')[0]).sumo.unSelectAll(); searchBox.val(''); updateListedReviews(); }); diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.module.ts b/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.module.ts index 12fab763137..d7ebf45b276 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.module.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.module.ts @@ -490,6 +490,20 @@ export function addClickEventToClassesInSections() { }); } +/** + * Add Select Event Handlers to API Revision Select + */ +export function addSelectEventToAPIRevisionSelect() { + $('#revision-select, #diff-select').each(function (index, value) { + $(this).on('change', function () { + var url = $(this).find(":selected").val(); + if (url) { + window.location.href = url as string; + } + }); + }); +} + /** * Check if targetAnchor is present, if its not present, expand the section and scroll to the targetAnchor * @param { String } uriHash the hash/id of the anchor we are looking for diff --git a/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.ts b/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.ts index 5b174c84ecf..b1c3ad7775a 100644 --- a/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.ts +++ b/src/dotnet/APIView/APIViewWeb/Client/src/pages/review.ts @@ -21,6 +21,8 @@ $(() => { // Enable SumoSelect ($("#revision-select")).SumoSelect({ search: true, searchText: 'Search Revisions...' }); ($("#diff-select")).SumoSelect({ search: true, searchText: 'Search Revisons for Diff...' }); + ($("#revision-type-select")).SumoSelect(); + ($("#diff-revision-type-select")).SumoSelect(); // Update codeLine Section state after page refresh const shownSectionHeadingLineNumbers = sessionStorage.getItem("shownSectionHeadingLineNumbers"); @@ -165,13 +167,40 @@ $(() => { /* DROPDOWN FILTER FOR REVIEW, REVISIONS AND DIFF (UPDATES REVIEW PAGE ON CHANGE) --------------------------------------------------------------------------------------------------------------------------------------------------------*/ - $('#revision-select, #diff-select').each(function(index, value) { - $(this).on('change', function() { - var url = $(this).find(":selected").val(); - if (url) - { - window.location.href = url as string; - } + rvM.addSelectEventToAPIRevisionSelect(); + + $('#revision-type-select, #diff-revision-type-select').each(function(index, value) { + $(this).on('change', function () { + const pageIds = hp.getReviewAndRevisionIdFromUrl(window.location.href); + const reviewId = pageIds["reviewId"]; + const apiRevisionId = pageIds["revisionId"]; + + const select = (index == 0) ? $('#revision-select') : $('#diff-select'); + const text = (index == 0) ? 'Revisions' : 'Revisions for Diff'; + + let uri = (index == 0) ? '?handler=APIRevisionsPartial' : '?handler=APIDiffRevisionsPartial'; + uri = uri + `&reviewId=${reviewId}`; + uri = uri + `&apiRevisionId=${apiRevisionId}`; + uri = uri + '&apiRevisionType=' + $(this).find(":selected").val(); + + $.ajax({ + url: uri + }).done(function (partialViewResult) { + console.log(partialViewResult); + const id = select.attr('id'); + const selectUpdate = $(``); + selectUpdate.html(partialViewResult); + select.parent().replaceWith(selectUpdate); + ($(`#${id}`)).SumoSelect({ placeholder: `Select ${text}...`, search: true, searchText: `Search ${text}...` }) + + // Disable Diff Revision Select until a revision is selected + if (index == 0) + { + ($('#diff-revision-type-select')[0]).sumo.disable(); + ($('#diff-select')[0]).sumo.disable(); + } + rvM.addSelectEventToAPIRevisionSelect(); + }); }); }); diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs index 26dc1c50e1a..5bf021873e8 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs @@ -1,119 +1,213 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using APIViewWeb.Filters; -using APIViewWeb.Managers; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace APIViewWeb.Controllers -{ - [TypeFilter(typeof(ApiKeyAuthorizeAsyncFilter))] - public class AutoReviewController : Controller - { - private readonly IReviewManager _reviewManager; - private readonly ILogger _logger; - - public AutoReviewController(IReviewManager reviewManager, ILogger logger) - { - _reviewManager = reviewManager; - _logger = logger; - } - - [HttpPost] - public async Task UploadAutoReview([FromForm] IFormFile file, string label, bool compareAllRevisions = false) - { - if (file != null) - { - using (var openReadStream = file.OpenReadStream()) - { - var reviewRevision = await _reviewManager.CreateMasterReviewAsync(User, file.FileName, label, openReadStream, compareAllRevisions); - if(reviewRevision != null) - { - var reviewUrl = $"{this.Request.Scheme}://{this.Request.Host}/Assemblies/Review/{reviewRevision.Review.ReviewId}"; - //Return 200 OK if last revision is approved and 201 if revision is not yet approved. - var result = reviewRevision.IsApproved ? Ok(reviewUrl) : StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl); - return result; - } - } - } - // Return internal server error for any unknown error - return StatusCode(statusCode: StatusCodes.Status500InternalServerError); +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Amazon.Util; +using ApiView; +using APIViewWeb.Filters; +using APIViewWeb.Helpers; +using APIViewWeb.LeanModels; +using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.TeamFoundation.SourceControl.WebApi; +using Microsoft.VisualStudio.Services.Account; + +namespace APIViewWeb.Controllers +{ + [TypeFilter(typeof(ApiKeyAuthorizeAsyncFilter))] + public class AutoReviewController : Controller + { + private readonly IAuthorizationService _authorizationService; + private readonly ICodeFileManager _codeFileManager; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly ICommentsManager _commentsManager; + private readonly ILogger _logger; + + public AutoReviewController(IAuthorizationService authorizationService, ICodeFileManager codeFileManager, + IReviewManager reviewManager, IAPIRevisionsManager apiRevisionManager, ICommentsManager commentsManager, + ILogger logger) + { + _authorizationService = authorizationService; + _codeFileManager = codeFileManager; + _apiRevisionsManager = apiRevisionManager; + _commentsManager = commentsManager; + _reviewManager = reviewManager; + _logger = logger; + } + + [HttpPost] + public async Task UploadAutoReview([FromForm] IFormFile file, string label, bool compareAllRevisions = false) + { + if (file != null) + { + using (var openReadStream = file.OpenReadStream()) + { + using var memoryStream = new MemoryStream(); + var codeFile = await _codeFileManager.CreateCodeFileAsync(originalName: file.FileName, fileStream: openReadStream, + runAnalysis: false, memoryStream: memoryStream); + + var apiRevision = await CreateAutomaticRevisionAsync(codeFile: codeFile, label: label, originalName: file.FileName, memoryStream: memoryStream, compareAllRevisions); + + if (apiRevision != null) + { + var reviewUrl = $"{this.Request.Scheme}://{this.Request.Host}/Assemblies/Review/{apiRevision.ReviewId}?revisionId={apiRevision.Id}"; + return apiRevision.IsApproved ? Ok(reviewUrl) : StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl); + } + }; + } + // Return internal server error for any unknown error + return StatusCode(statusCode: StatusCodes.Status500InternalServerError); } - - [HttpGet] - public async Task GetReviewStatus(string language, string packageName, string reviewId = null, bool? firstReleaseStatusOnly = null) - { - // This API is used by prepare release script to check if API review for a package is approved or not. - // This caller script doesn't have artifact to submit and so it can't check using create review API - // So it rely on approval status of latest revision of automatic review for the package - // With new restriction of creating automatic review only from master branch or GA version, this should ensure latest revision + + public async Task GetReviewStatus(string language, string packageName, string reviewId = null, bool? firstReleaseStatusOnly = null) + { + // This API is used by prepare release script to check if API review for a package is approved or not. + // This caller script doesn't have artifact to submit and so it can't check using create review API + // So it rely on approval status of latest revision of automatic review for the package + // With new restriction of creating automatic review only from master branch or GA version, this should ensure latest revision // is infact the version intended to be released. - ReviewType filtertype = (firstReleaseStatusOnly == true) ? ReviewType.All : ReviewType.Automatic; - - ReviewModel review; - if (String.IsNullOrEmpty(reviewId)) - { - IEnumerable reviews = await _reviewManager.GetReviewsAsync(false, language, packageName: packageName, filtertype); - review = reviews.FirstOrDefault(); - } - else - { - review = await _reviewManager.GetReviewAsync(User, reviewId); - } - - if (review != null) - { - _logger.LogInformation("Found review ID " + review.ReviewId + " for package " + packageName); - - // Return 200 OK for approved review and 201 for review in pending status - if (firstReleaseStatusOnly != true && review.Revisions.LastOrDefault().IsApproved) - { - return Ok(); - } - else - { - var isPkgNameApproved = await _reviewManager.IsApprovedForFirstRelease(language, packageName); - if (!isPkgNameApproved) - { - // Return 202 to indicate package name is not approved - return StatusCode(statusCode: StatusCodes.Status202Accepted); - } - return StatusCode(statusCode: StatusCodes.Status201Created); - } - } - - throw new Exception("Automatic review is not found for package " + packageName); - } - - [HttpGet] - public async Task CreateApiReview( - string buildId, - string artifactName, - string originalFilePath, - string reviewFilePath, - string label, - string repoName, - string packageName, - bool compareAllRevisions, - string project - ) - { - var reviewRevision = await _reviewManager.CreateApiReview(User, buildId, artifactName, originalFilePath, label, repoName, packageName, reviewFilePath, compareAllRevisions, project); - if (reviewRevision != null) - { - var reviewUrl = $"{this.Request.Scheme}://{this.Request.Host}/Assemblies/Review/{reviewRevision.Review.ReviewId}"; - //Return 200 OK if last revision is approved and 201 if revision is not yet approved. - return reviewRevision.IsApproved ? Ok(reviewUrl) : StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl); - } - // Return internal server error for any unknown error - return StatusCode(statusCode: StatusCodes.Status500InternalServerError); - } - } -} + ReviewListItemModel review = await _reviewManager.GetReviewAsync(packageName: packageName, language: language, isClosed: null); + + if (review != null) + { + APIRevisionListItemModel latestAutomaticApiRevisions = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic); + + // Return 200 OK for approved review and 201 for review in pending status + if (firstReleaseStatusOnly != true && latestAutomaticApiRevisions != null && latestAutomaticApiRevisions.IsApproved) + { + return Ok(); + } + else + { + if (review.IsApproved) + { + return StatusCode(statusCode: StatusCodes.Status201Created); + } + // Return 202 to indicate package name is not approved + return StatusCode(statusCode: StatusCodes.Status202Accepted); + } + } + throw new Exception("Review is not found for package " + packageName); + } + + [HttpGet] + public async Task CreateApiReview( + string buildId, + string artifactName, + string originalFilePath, + string reviewFilePath, + string label, + string repoName, + string packageName, + bool compareAllRevisions, + string project + ) + { + using var memoryStream = new MemoryStream(); + var codeFile = await _codeFileManager.GetCodeFileAsync(repoName: repoName, buildId: buildId, artifactName: artifactName, + packageName: packageName, originalFileName: originalFilePath, codeFileName: reviewFilePath, originalFileStream: memoryStream, + project: project); + + var apiRevision = await CreateAutomaticRevisionAsync(codeFile: codeFile, label: label, originalName: originalFilePath, memoryStream: memoryStream, compareAllRevisions); + if (apiRevision != null) + { + var reviewUrl = $"{this.Request.Scheme}://{this.Request.Host}/Assemblies/Review/{apiRevision.ReviewId}?revisionId={apiRevision.Id}"; + return apiRevision.IsApproved ? Ok(reviewUrl) : StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl); + } + // Return internal server error for any unknown error + return StatusCode(statusCode: StatusCodes.Status500InternalServerError); + } + + private async Task CreateAutomaticRevisionAsync(CodeFile codeFile, string label, string originalName, MemoryStream memoryStream, bool compareAllRevisions = false) + { + var createNewRevision = true; + var review = await _reviewManager.GetReviewAsync(packageName: codeFile.PackageName, language: codeFile.Language, isClosed: null); + var apiRevision = default(APIRevisionListItemModel); + var renderedCodeFile = new RenderedCodeFile(codeFile); + IEnumerable apiRevisions = new List(); + + if (review != null) + { + apiRevisions = await _apiRevisionsManager.GetAPIRevisionsAsync(review.Id); + if (apiRevisions.Any()) + { + apiRevisions = apiRevisions.OrderByDescending(r => r.CreatedOn); + + // Delete pending apiRevisions if it is not in approved state before adding new revision + // This is to keep only one pending revision since last approval or from initial review revision + var automaticRevisions = apiRevisions.Where(r => r.APIRevisionType == APIRevisionType.Automatic); + var automaticRevisionsQueue = new Queue(automaticRevisions); + var latestAutomaticAPIRevision = automaticRevisionsQueue.Peek(); + var comments = await _commentsManager.GetCommentsAsync(review.Id); + + while ( + automaticRevisionsQueue.Any() && + !latestAutomaticAPIRevision.IsApproved && + !await _apiRevisionsManager.IsAPIRevisionTheSame(latestAutomaticAPIRevision, renderedCodeFile) && + !comments.Any(c => latestAutomaticAPIRevision.Id == c.APIRevisionId)) + { + // Check if user is authorized to modify automatic review + await ManagerHelpers.AssertAutomaticAPIRevisionModifier(user: User, apiRevision: apiRevision, authorizationService: _authorizationService); + await _apiRevisionsManager.SoftDeleteAPIRevisionAsync(user: User, apiRevision: latestAutomaticAPIRevision); + latestAutomaticAPIRevision = automaticRevisionsQueue.Dequeue(); + } + + // We should compare against only latest revision when calling this API from scheduled CI runs + // But any manual pipeline run at release time should compare against all approved revisions to ensure hotfix release doesn't have API change + // If review surface doesn't match with any approved revisions then we will create new revision if it doesn't match pending latest revision + + if (compareAllRevisions) + { + foreach (var approvedAPIRevision in automaticRevisions.Where(r => r.IsApproved)) + { + if (await _apiRevisionsManager.IsAPIRevisionTheSame(approvedAPIRevision, renderedCodeFile)) + { + return approvedAPIRevision; + } + } + } + + if (await _apiRevisionsManager.IsAPIRevisionTheSame(latestAutomaticAPIRevision, renderedCodeFile)) + { + apiRevision = latestAutomaticAPIRevision; + createNewRevision = false; + } + } + } + else + { + review = await _reviewManager.CreateReviewAsync(packageName: codeFile.PackageName, language: codeFile.Language, isClosed: false); + } + + if (createNewRevision) + { + apiRevision = await _apiRevisionsManager.CreateAPIRevisionAsync(userName: User.GetGitHubLogin(), reviewId: review.Id, apiRevisionType: APIRevisionType.Automatic, label: label, memoryStream: memoryStream, codeFile: codeFile, originalName: originalName); + } + + if (apiRevision != null) + { + if (!apiRevision.IsApproved && apiRevisions.Any()) + { + foreach (var apiRev in apiRevisions) + { + if (apiRev.IsApproved && await _apiRevisionsManager.IsAPIRevisionTheSame(apiRev, renderedCodeFile)) + { + await _apiRevisionsManager.ToggleAPIRevisionApprovalAsync(user: User, id: review.Id, apiRevision: apiRevision, notes: $"Approval Copied over from Revision with Id : {apiRev.Id}"); + } + break; + } + } + } + return apiRevision; + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs index 41e0c559cb9..9437d23ea99 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using APIViewWeb.Hubs; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; using APIViewWeb.Models; using Microsoft.AspNetCore.Authorization; @@ -35,17 +36,16 @@ public async Task Add(string reviewId, string revisionId, string e return new BadRequestResult(); } - var comment = new CommentModel(); - comment.TimeStamp = DateTime.UtcNow; + var comment = new CommentItemModel(); + comment.CreatedOn = DateTime.UtcNow; comment.ReviewId = reviewId; - comment.RevisionId = revisionId; + comment.APIRevisionId = revisionId; comment.ElementId = elementId; comment.SectionClass = sectionClass; - comment.Comment = commentText; - comment.GroupNo = groupNo; - comment.IsUsageSampleComment = usageSampleComment; + comment.CommentText = commentText; + comment.CommentType = (usageSampleComment) ? CommentType.SamplesRevision : CommentType.APIRevision; comment.ResolutionLocked = !resolutionLock.Equals("on"); - comment.Username = User.GetGitHubLogin(); + comment.CreatedBy = User.GetGitHubLogin(); foreach(string user in taggedUsers) { @@ -90,7 +90,7 @@ public async Task Unresolve(string reviewId, string elementId) [HttpPost] public async Task Delete(string reviewId, string commentId, string elementId) { - await _commentsManager.DeleteCommentAsync(User, reviewId, commentId); + await _commentsManager.SoftDeleteCommentAsync(User, reviewId, commentId); return await CommentPartialAsync(reviewId, elementId); } diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs index 151ae173f93..3564fd7e10b 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs @@ -1,79 +1,271 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using APIViewWeb.Managers; -using APIViewWeb.Repositories; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Linq; -using System.Threading.Tasks; - -namespace APIViewWeb.Controllers -{ - public class PullRequestController : Controller - { - private readonly IPullRequestManager _pullRequestManager; - private readonly ILogger _logger; - - string[] VALID_EXTENSIONS = new string[] { ".whl", ".api.json", ".nupkg", "-sources.jar", ".gosource" }; - - public PullRequestController(IPullRequestManager pullRequestManager, ILogger logger) - { - _pullRequestManager = pullRequestManager; - _logger = logger; - } - - [HttpGet] - public async Task DetectApiChanges( - string buildId, - string artifactName, - string filePath, - string commitSha, - string repoName, - string packageName, - int pullRequestNumber = 0, - string codeFile = null, - string baselineCodeFile = null, - bool commentOnPR = true, - string language = null) - { - if (!ValidateInputParams()) - { - return StatusCode(StatusCodes.Status400BadRequest); - } - - //Handle only authorization exception and send 401 as status code. - //All other exception should not be handled so we will have required info in app insights. - try - { - var reviewUrl = await _pullRequestManager.DetectApiChanges(buildId, artifactName, filePath, commitSha, repoName, packageName, pullRequestNumber, this.Request.Host.ToUriComponent(), codeFileName: codeFile, baselineCodeFileName: baselineCodeFile, commentOnPR: commentOnPR, language: language); - return !string.IsNullOrEmpty(reviewUrl) ? StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl) : StatusCode(statusCode: StatusCodes.Status208AlreadyReported); - } - catch (AuthorizationFailedException) - { - return StatusCode(StatusCodes.Status401Unauthorized); - } - } - - private bool ValidateInputParams() - { - foreach (var queryParam in this.Request.Query) - { - var value = queryParam.Value.ToString(); - if (queryParam.Key == "filePath") - { - if (!VALID_EXTENSIONS.Any(e => value.EndsWith(e))) - return false; - } - - if (queryParam.Key == "repoName") - { - if (!value.Contains("/")) - return false; - } - } - return true; - } - } -} +using System.Linq; +using System.Threading.Tasks; +using APIViewWeb.Managers; +using APIViewWeb.Repositories; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.ApplicationInsights.DataContracts; +using APIViewWeb.Helpers; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using System.IO; +using APIViewWeb.Managers.Interfaces; +using ApiView; +using System; +using APIViewWeb.Models; +using APIViewWeb.LeanModels; +using Microsoft.VisualStudio.Services.DelegatedAuthorization; +using Amazon.Util; +using Octokit; +using static Microsoft.VisualStudio.Services.Graph.Constants; + +namespace APIViewWeb.Controllers +{ + public class PullRequestController : Controller + { + private readonly ICodeFileManager _codeFileManager; + private readonly IPullRequestManager _pullRequestManager; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IOpenSourceRequestManager _openSourceManager; + private readonly TelemetryClient _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); + private HashSet _allowedListBotAccounts = new HashSet(); + + string[] VALID_EXTENSIONS = new string[] { ".whl", ".api.json", ".nupkg", "-sources.jar", ".gosource" }; + + public PullRequestController(ICodeFileManager codeFileManager, IPullRequestManager pullRequestManager, + IAPIRevisionsManager apiRevisionsManager, IReviewManager reviewManager, ILogger logger, + IConfiguration configuration, IOpenSourceRequestManager openSourceRequestManager) + { + _codeFileManager = codeFileManager; + _pullRequestManager = pullRequestManager; + _reviewManager = reviewManager; + _apiRevisionsManager = apiRevisionsManager; + _logger = logger; + _configuration = configuration; + _openSourceManager = openSourceRequestManager; + + var botAllowedList = _configuration["allowedList-bot-github-accounts"]; + if (!string.IsNullOrEmpty(botAllowedList)) + { + _allowedListBotAccounts.UnionWith(botAllowedList.Split(",")); + } + } + + [HttpGet] + public async Task DetectApiChanges( + string buildId, + string artifactName, + string filePath, + string commitSha, + string repoName, + string packageName, + int pullRequestNumber = 0, + string codeFile = null, + string baselineCodeFile = null, + bool commentOnPR = true, + string language = null) + { + if (!ValidateInputParams()) + { + return StatusCode(StatusCodes.Status400BadRequest); + } + + //Handle only authorization exception and send 401 as status code. + //All other exception should not be handled so we will have required info in app insights. + try + { + var reviewUrl = await DetectAPIChanges( + buildId: buildId, artifactName: artifactName, + originalFileName: filePath, commitSha: commitSha, + repoName: repoName, packageName: packageName, + prNumber: pullRequestNumber, hostName: this.Request.Host.ToUriComponent(), + codeFileName: codeFile, baselineCodeFileName: baselineCodeFile, + commentOnPR: commentOnPR, language: language); + + return !string.IsNullOrEmpty(reviewUrl) ? StatusCode(statusCode: StatusCodes.Status201Created, reviewUrl) : StatusCode(statusCode: StatusCodes.Status208AlreadyReported); + } + catch (AuthorizationFailedException) + { + return StatusCode(StatusCodes.Status401Unauthorized); + } + } + + private async Task DetectAPIChanges(string buildId, + string artifactName, + string originalFileName, + string commitSha, + string repoName, + string packageName, + int prNumber, + string hostName, + string codeFileName = null, + string baselineCodeFileName = null, + bool commentOnPR = true, + string language = null, + string project = "public") + { + var requestTelemetry = new RequestTelemetry { Name = "Detecting API changes for PR: " + prNumber }; + var operation = _telemetryClient.StartOperation(requestTelemetry); + originalFileName = originalFileName ?? codeFileName; + var repoInfo = repoName.Split("/"); + var pullRequestModel = await _pullRequestManager.GetPullRequestModelAsync(prNumber, repoName, packageName, originalFileName, language); + if (pullRequestModel == null) + { + return ""; + } + if (pullRequestModel.Commits.Any(c => c == commitSha)) + { + // PR commit is already processed. No need to reprocess it again. + return !string.IsNullOrEmpty(pullRequestModel.ReviewId) ? ManagerHelpers.ResolveReviewUrl(pullRequest: pullRequestModel, hostName: hostName) : ""; + } + + pullRequestModel.Commits.Add(commitSha); + //Check if PR owner is part of Azure//Microsoft org in GitHub + await ManagerHelpers.AssertPullRequestCreatorPermission(prModel: pullRequestModel, allowedListBotAccounts: _allowedListBotAccounts, + openSourceManager: _openSourceManager, telemetryClient: _telemetryClient); + + using var memoryStream = new MemoryStream(); + using var baselineStream = new MemoryStream(); + var codeFile = await _codeFileManager.GetCodeFileAsync( + repoName: repoName, buildId: buildId, artifactName: artifactName, + packageName: packageName, originalFileName: originalFileName, + codeFileName: codeFileName, originalFileStream: memoryStream, + baselineCodeFileName: baselineCodeFileName, baselineStream: baselineStream, + project: project); + + CodeFile baseLineCodeFile = null; + if (baselineStream.Length > 0) + { + baselineStream.Position = 0; + baseLineCodeFile = await CodeFile.DeserializeAsync(baselineStream); + } + if (codeFile != null) + { + await CreateAPIRevisionIfRequired(codeFile, prNumber, originalFileName, memoryStream, pullRequestModel, baseLineCodeFile, baselineStream, baselineCodeFileName); + } + else + { + _telemetryClient.TrackTrace("Failed to download artifact. Please recheck build id and artifact path values in API change detection request."); + } + + //Generate combined single comment to update on PR. + var pullRequests = await _pullRequestManager.GetPullRequestsModelAsync(pullRequestNumber: prNumber, repoName: repoName); + if (commentOnPR) + { + await _pullRequestManager.CreateOrUpdateCommentsOnPR(pullRequests.ToList(), repoInfo[0], repoInfo[1], prNumber, hostName); + } + + // Return review URL created for current package if exists + var pr = pullRequests.SingleOrDefault(r => r.PackageName == packageName && (r.Language == null || r.Language == language)); + return pr == null ? "" : ManagerHelpers.ResolveReviewUrl(pullRequest: pr, hostName: hostName); + + } + + private async Task CreateAPIRevisionIfRequired(CodeFile codeFile, int prNumber, + string originalFileName, MemoryStream memoryStream, + PullRequestModel pullRequestModel, CodeFile baselineCodeFile, + MemoryStream baseLineStream, string baselineFileName) + { + // fetch review for the package or create brand new review + var review = await _reviewManager.GetReviewAsync(language: codeFile.Language, packageName: codeFile.PackageName); + if (review == null) + { + review = await _reviewManager.CreateReviewAsync(language: codeFile.Language, packageName: codeFile.PackageName, isClosed: false); + } + + var renderedCodeFile = new RenderedCodeFile(codeFile); + var apiRevisions = (await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId: review.Id)).OrderByDescending(r => r.CreatedOn); + + if (apiRevisions.Any()) + { + if (codeFile.Language == "Swagger" || codeFile.Language == "TypeSpec") + { + var baseLineRenderedCodeFile = new RenderedCodeFile(baselineCodeFile); + if (_codeFileManager.IsAPICodeFilesTheSame(renderedCodeFile, baseLineRenderedCodeFile)) + { + return; + } + + var createBaseLine = true; + + foreach (var apiRevision in apiRevisions) + { + if (await _apiRevisionsManager.IsAPIRevisionTheSame(apiRevision, baseLineRenderedCodeFile)) + { + createBaseLine = false; + break; + } + } + if (createBaseLine) + { + await _apiRevisionsManager.CreateAPIRevisionAsync( + userName: pullRequestModel.CreatedBy, reviewId: review.Id, apiRevisionType: APIRevisionType.PullRequest, + label: $"BaseLine for PR: {prNumber}", memoryStream: baseLineStream, codeFile: baselineCodeFile, originalName: baselineFileName, prNumber: prNumber); + } + } + else + { + // checked if the new apiRevision matches any automatic apiRevision + var autoAPIRevisions = apiRevisions.Where(r => r.APIRevisionType == APIRevisionType.Automatic); + + foreach (var autoAPIRevision in autoAPIRevisions) + { + if (await _apiRevisionsManager.IsAPIRevisionTheSame(autoAPIRevision, renderedCodeFile)) + { + // no change in api surface level from exisiting revision + return; + } + } + + var prAPIRevisions = apiRevisions.Where(r => r.APIRevisionType == APIRevisionType.PullRequest); + var prsForReview = await _pullRequestManager.GetPullRequestsModelAsync(reviewId: review.Id); + + foreach (var prAPIRevision in prAPIRevisions) + { + // Check if you have already created a revision for the same PR + var existingRevisionForPR = prsForReview.FirstOrDefault(p => p.APIRevisionId == prAPIRevision.Id && p.PullRequestNumber == prNumber); + if (existingRevisionForPR != default(PullRequestModel)) + { + // update codeFile for existing apiRevision with the incoming codefile + prAPIRevision.Files[0] = await _codeFileManager.CreateReviewCodeFileModel( + apiRevisionId: prAPIRevision.Id, memoryStream: memoryStream, codeFile: codeFile); + return; + } + } + } + } + + await _apiRevisionsManager.CreateAPIRevisionAsync( + userName: pullRequestModel.CreatedBy, reviewId: review.Id, apiRevisionType: APIRevisionType.PullRequest, + label: String.Empty, memoryStream: memoryStream, codeFile: codeFile, originalName: originalFileName, prNumber: prNumber); + } + + + + private bool ValidateInputParams() + { + foreach (var queryParam in this.Request.Query) + { + var value = queryParam.Value.ToString(); + if (queryParam.Key == "filePath") + { + if (!VALID_EXTENSIONS.Any(e => value.EndsWith(e))) + return false; + } + + if (queryParam.Key == "repoName") + { + if (!value.Contains("/")) + return false; + } + } + return true; + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/ReviewController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/ReviewController.cs index 319e0506c79..107c3c4839b 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/ReviewController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/ReviewController.cs @@ -1,41 +1,44 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using ApiView; -using APIViewWeb.Helpers; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using APIViewWeb.Hubs; -using APIViewWeb.Managers; +using APIViewWeb.LeanModels; +using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Linq; -using System.Threading.Tasks; - -namespace APIViewWeb.Controllers -{ - public class ReviewController : Controller - { +using System.Security.Claims; +using System.Threading.Tasks; + +namespace APIViewWeb.Controllers +{ + public class ReviewController : Controller + { private readonly IReviewManager _reviewManager; - private readonly IHubContext _signalRHubContext; - - private readonly ILogger _logger; - - public ReviewController(IReviewManager reviewManager, IHubContext signalRHubContext, ILogger logger) - { + private readonly IAPIRevisionsManager _apiRevisionManager; + private readonly IHubContext _signalRHubContext; + + private readonly ILogger _logger; + + public ReviewController(IReviewManager reviewManager, + IAPIRevisionsManager apiRevisionManager, IHubContext signalRHubContext, + ILogger logger) + { _reviewManager = reviewManager; - _signalRHubContext = signalRHubContext; - _logger = logger; - } - - [HttpGet] - public async Task UpdateApiReview(string repoName, string artifactPath, string buildId, string project = "internal") - { - await _reviewManager.UpdateReviewCodeFiles(repoName, buildId, artifactPath, project); - return Ok(); + _apiRevisionManager = apiRevisionManager; + _signalRHubContext = signalRHubContext; + _logger = logger; + } + + [HttpGet] + public async Task UpdateApiReview(string repoName, string artifactPath, string buildId, string project = "internal") + { + await _apiRevisionManager.UpdateAPIRevisionCodeFileAsync(repoName, buildId, artifactPath, project); + return Ok(); } [NonAction] @@ -44,39 +47,45 @@ public async Task GenerateAIReview( [FromQuery] string reviewId, [FromQuery]string revisionId = null) { var review = await _reviewManager.GetReviewAsync(User, reviewId); + var latestAPIRevision = await _apiRevisionManager.GetLatestAPIRevisionsAsync(reviewId: review.Id); + var apiRevison = latestAPIRevision; + + if (!string.IsNullOrEmpty(revisionId)) + { + apiRevison = await _apiRevisionManager.GetAPIRevisionAsync(user: User, apiRevisionId: revisionId); + } - if (string.IsNullOrEmpty(revisionId)) - revisionId = review.Revisions.Last().RevisionId; + var isLatestAPIRevision = (apiRevison.Id == latestAPIRevision.Id); - await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Generating); + await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Generating, isLatestAPIRevision); try { var commentsGenerated = await _reviewManager.GenerateAIReview(reviewId, revisionId); - await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Succeeded, commentsGenerated); + await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Succeeded, isLatestAPIRevision, commentsGenerated); } catch (Exception ex) { _logger.LogError(ex, "Error generating AI review"); - await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Error, errorMessage: ex.Message); + await SendAIReviewGenerationStatus(review, reviewId, revisionId, AIReviewGenerationStatus.Error, isLatestAPIRevision, errorMessage: ex.Message); } } [HttpPost] public async Task ApprovePackageName(string id) { - await _reviewManager.ApprovePackageNameAsync(User, id); + await _reviewManager.ApproveReviewAsync(user: User, reviewId: id); return RedirectToPage("/Assemblies/Review", new { id = id }); } - private async Task SendAIReviewGenerationStatus(ReviewModel review, string reviewId, - string revisionId, AIReviewGenerationStatus status, int? noOfCommentsGenerated = null, + private async Task SendAIReviewGenerationStatus(ReviewListItemModel review, string reviewId, + string revisionId, AIReviewGenerationStatus status, bool isLatestAPIRevision, int? noOfCommentsGenerated = null, string errorMessage = null) { var notification = new AIReviewGenerationNotificationModel { ReviewId = reviewId, RevisionId = revisionId, - IsLatestRevision = (revisionId == review.Revisions.Last().RevisionId), + IsLatestRevision = isLatestAPIRevision, Status = status }; @@ -96,6 +105,6 @@ private async Task SendAIReviewGenerationStatus(ReviewModel review, string revie break; } await _signalRHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveAIReviewGenerationStatus", notification); - } - } -} + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/APIHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/APIHelpers.cs index d8e9655c65e..a592a8b44fc 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/APIHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/APIHelpers.cs @@ -9,7 +9,50 @@ using Microsoft.TeamFoundation.SourceControl.WebApi; namespace APIViewWeb.Helpers -{ +{ + public class PageParams + { + private const int MaxPageSize = 50; + public int NoOfItemsRead { get; set; } = 0; + private int _pageSize = 5; + + public int PageSize + { + get => _pageSize; + set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value; + } + } + + public class ReviewFilterAndSortParams + { + public string Name { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Details { get; set; } + public string SortField { get; set; } = "PackageName"; + public int SortOrder { get; set; } = 1; + } + + public class APIRevisionsFilterAndSortParams : ReviewFilterAndSortParams + { + public string Author { get; set; } + public string ReviewId { get; set; } + } + + public class PagedList : List + { + public PagedList(IEnumerable items, int noOfItemsRead, int totalCount, int pageSize) + { + NoOfItemsRead = noOfItemsRead; + TotalCount = totalCount; + PageSize = pageSize; + AddRange(items); + } + public int NoOfItemsRead { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + + } + public class LeanJsonResult : JsonResult { private readonly int _statusCode; @@ -33,10 +76,24 @@ public override async Task ExecuteResultAsync(ActionContext context) var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReferenceHandler = ReferenceHandler.IgnoreCycles + ReferenceHandler = ReferenceHandler.IgnoreCycles, + Converters = { new JsonStringEnumConverter() } }; await JsonSerializer.SerializeAsync(response.Body, Value, options); } } + public class PaginationHeader + { + public PaginationHeader(int noOfItemsRead, int pageSize, int totalCount) + { + this.NoOfItemsRead = noOfItemsRead; + this.PageSize = pageSize; + this.TotalCount = totalCount; + } + + public int NoOfItemsRead { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + } } diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/AutoMapperProfiles.cs b/src/dotnet/APIView/APIViewWeb/Helpers/AutoMapperProfiles.cs index 88de1f2d27d..f1a59d4af93 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/AutoMapperProfiles.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/AutoMapperProfiles.cs @@ -11,7 +11,7 @@ public AutoMapperProfiles() CreateMap() .ForMember(dest => dest.Language, opt => opt.MapFrom((src, dest) => src._language != null ? src._language : dest._language)) .ForMember(dest => dest.ApprovedLanguages, opt => opt.MapFrom((src, dest) => src._approvedLanguages != null ? src._approvedLanguages : dest._approvedLanguages)) - .ForMember(dest => dest.FilterType, opt => opt.MapFrom((src, dest) => src._filterType != null ? src._filterType : dest._filterType)) + .ForMember(dest => dest.APIRevisionType, opt => opt.MapFrom((src, dest) => src._apiRevisionType != null ? src._apiRevisionType : dest._apiRevisionType)) .ForMember(dest => dest.State, opt => opt.MapFrom((src, dest) => src._state != null ? src._state : dest._state)) .ForMember(dest => dest.Status, opt => opt.MapFrom((src, dest) => src._status != null ? src._status : dest._status)) .ForMember(dest => dest.HideLineNumbers, opt => opt.MapFrom((src, dest) => src._hideLineNumbers != null ? src._hideLineNumbers : dest._hideLineNumbers)) diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs new file mode 100644 index 00000000000..b6c1fee5517 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using APIViewWeb.Repositories; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using System.Threading.Tasks; +using Octokit; +using MongoDB.Driver; +using APIViewWeb.LeanModels; + +namespace APIViewWeb.Helpers +{ + public class ChangeHistoryHelpers + { + /// + /// Given a List of ChangeHistory, and a ChangeAction, update the ChangeHistory with the ChangeAction + /// depending on the entries already present in the changeHistroy. Return updated ChangeHistory and the ChangeStatus + /// which is the overall status of the change Action based on all changes in the changeHistory i.e true if added, false if reverted + /// Should be used for ChangeActions that are Binary (Added/Reverted) Approved, Delete e.t.c + /// + /// + /// + /// + /// + /// + /// + /// + public static (List ChangeHistory, bool ChangeStatus) UpdateBinaryChangeAction(List changeHistory, E action, string user, string notes = "") + { + var resolvedAction = ResolveAction(action); + E actionAdded = resolvedAction.actionAdded; + E actionReverted = resolvedAction.actionReverted; + bool actionInvalid = resolvedAction.actionInvalid; + + if (actionInvalid) + { + throw new ArgumentException($"Invalid arguments action : {action}"); + } + + var actionsAddedByUser = GetActionsAdded(changeHistory, actionAdded, user); + var actionsRevertedByUser = GetActionsReverted(changeHistory, actionReverted, user); + + T obj = (T)Activator.CreateInstance(typeof(T)); + + if (actionsAddedByUser.Count() > actionsRevertedByUser.Count()) + { + obj.GetType().GetProperty("ChangeAction").SetValue(obj, actionReverted); + } + else + { + obj.GetType().GetProperty("ChangeAction").SetValue(obj, actionAdded); + } + obj.GetType().GetProperty("ChangedBy").SetValue(obj, user); + obj.GetType().GetProperty("Notes").SetValue(obj, notes); + obj.GetType().GetProperty("ChangedOn").SetValue(obj, DateTime.Now); + changeHistory.Add(obj); + + var actionsAdded = GetActionsAdded(changeHistory, actionAdded); + var actionsReverted = GetActionsReverted(changeHistory, actionReverted); + + if (actionsAdded.Count() > actionsReverted.Count()) + { + return (changeHistory, true); + } + return (changeHistory, false); + } + + /// + /// Probe the ChangeHistory to figure out the status of the changeAction true for action added, false for action reverted + /// + /// + /// + /// + /// + /// + /// + /// + public static bool GetChangeActionStatus(List changeHistory, E action, string user) + { + var resolvedAction = ResolveAction(action); + E actionAdded = resolvedAction.actionAdded; + E actionReverted = resolvedAction.actionReverted; + bool actionInvalid = resolvedAction.actionInvalid; + + if (actionInvalid) + { + throw new ArgumentException($"Invalid arguments action : {action}"); + } + + var actionsAddedByUser = GetActionsAdded(changeHistory, actionAdded, user); + var actionsRevertedByUser = GetActionsReverted(changeHistory, actionReverted, user); + return (actionsAddedByUser.Count() > actionsRevertedByUser.Count()) ? true : false; + } + /// + /// From a list of changeHistory get the creator + /// + /// + /// + /// + public static string GetCreator(List changeHistory) + { + var creator = changeHistory.Where(c => + { + var changeAction = c.GetType().GetProperty("ChangeAction").GetValue(c); + if (changeAction.ToString().Equals("Created")) + { + return true; + } + return false; + }).First(); + var userProperty = creator.GetType().GetProperty("ChangedBy").GetValue(creator); + return userProperty.ToString(); + } + + /// + /// From a list of changeHistory get the creation date + /// + /// + /// + /// + public static DateTime GetCreationDate(List changeHistory) + { + var creator = changeHistory.Where(c => + { + var changeAction = c.GetType().GetProperty("ChangeAction").GetValue(c); + if (changeAction.ToString().Equals("Created")) + { + return true; + } + return false; + }).First(); + var userProperty = creator.GetType().GetProperty("ChangeDateTime").GetValue(creator); + return (DateTime)userProperty; + } + + private static IEnumerable GetActionsAdded(List changeHistory, T2 actionAdded, string user = null) + { + return changeHistory.Where(c => + { + var changeAction = c.GetType().GetProperty("ChangeAction").GetValue(c); + if (String.IsNullOrEmpty(user)) + { + return changeAction.Equals(actionAdded); + } + var userProperty = c.GetType().GetProperty("ChangedBy").GetValue(c); + return changeAction.Equals(actionAdded) && userProperty.Equals(user); + }); + } + + private static IEnumerable GetActionsReverted(List changeHistory, T2 actionReverted, string user = null) + { + return changeHistory.Where(c => + { + var changeAction = c.GetType().GetProperty("ChangeAction").GetValue(c); + if (String.IsNullOrEmpty(user)) + { + return changeAction.Equals(actionReverted); + } + var userProperty = c.GetType().GetProperty("ChangedBy").GetValue(c); + return changeAction.Equals(actionReverted) && userProperty.Equals(user); + }); + } + + private static (E actionAdded, E actionReverted, bool actionInvalid) ResolveAction(E action) + { + E actionAdded = default(E); + E actionReverted = default(E); + + var actionInvalid = false; + + switch (action.ToString()) + { + case "Approved": + Enum.TryParse(typeof(E), "ApprovalReverted", out object ar); + actionAdded = action; + actionReverted = (E)ar; + break; + case "ApprovalReverted": + Enum.TryParse(typeof(E), "Approved", out object a); + actionAdded = (E)a; + actionReverted = action; + break; + case "Closed": + Enum.TryParse(typeof(E), "ReOpened", out object ro); + actionAdded = action; + actionReverted = (E)ro; + break; + case "ReOpened": + Enum.TryParse(typeof(E), "Closed", out object c); + actionAdded = (E)c; + actionReverted = action; + break; + case "Deleted": + Enum.TryParse(typeof(E), "Undeleted", out object ud); + actionAdded = action; + actionReverted = (E)ud; + break; + case "UnDeleted": + Enum.TryParse(typeof(E), "Deleted", out object d); + actionAdded = (E)d; + actionReverted = action; + break; + case "Resolved": + Enum.TryParse(typeof(E), "UnResolved", out object ur); + actionAdded = action; + actionReverted = (E)ur; + break; + case "UnResolved": + Enum.TryParse(typeof(E), "Resolved", out object r); + actionAdded = (E)r; + actionReverted = action; + break; + default: + actionInvalid = true; + break; + } + return (actionAdded, actionReverted, actionInvalid); + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/CosmosQueryHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/CosmosQueryHelpers.cs new file mode 100644 index 00000000000..2311b29a097 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Helpers/CosmosQueryHelpers.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text; + +namespace APIViewWeb.Helpers +{ + public class CosmosQueryHelpers + { + public static string ArrayToQueryString(IEnumerable items) + { + var result = new StringBuilder(); + result.Append("("); + foreach (var item in items) + { + if (item is int) + { + result.Append($"{item},"); + } + else + { + result.Append($"\"{item}\","); + } + + } + if (result[result.Length - 1] == ',') + { + result.Remove(result.Length - 1, 1); + } + result.Append(")"); + return result.ToString(); + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs index 1e0eb4fc518..baecacf6d2b 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs @@ -23,5 +23,10 @@ public static IEnumerable MapLanguageAliases(IEnumerable languag return result.ToList(); } + + public static LanguageService GetLanguageService(string language, IEnumerable languageServices) + { + return languageServices.FirstOrDefault(service => service.Name == language); + } } } diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/ManagerHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/ManagerHelpers.cs new file mode 100644 index 00000000000..55a839b2638 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Helpers/ManagerHelpers.cs @@ -0,0 +1,98 @@ + +using APIViewWeb.Repositories; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using System.Threading.Tasks; +using APIViewWeb.LeanModels; +using APIViewWeb.Models; +using System.Collections.Generic; +using APIViewWeb.Managers; +using Microsoft.ApplicationInsights; +using System; + +namespace APIViewWeb.Helpers +{ + public class ManagerHelpers + { + public static async Task AssertApprover(ClaimsPrincipal user, T model, IAuthorizationService authorizationService) + { + var result = await authorizationService.AuthorizeAsync( + user, + model, + new[] { ApproverRequirement.Instance }); + if (!result.Succeeded) + { + throw new AuthorizationFailedException(); + } + } + + public static async Task AssertAutomaticAPIRevisionModifier(ClaimsPrincipal user, APIRevisionListItemModel apiRevision, IAuthorizationService authorizationService) + { + var result = await authorizationService.AuthorizeAsync( + user, + apiRevision, + new[] { AutoAPIRevisionModifierRequirement.Instance }); + if (!result.Succeeded) + { + throw new AuthorizationFailedException(); + } + } + + public static async Task AssertAPIRevisionOwner(ClaimsPrincipal user, APIRevisionListItemModel revisionModel, IAuthorizationService authorizationService) + { + var result = await authorizationService.AuthorizeAsync( + user, + revisionModel, + new[] { RevisionOwnerRequirement.Instance }); + if (!result.Succeeded) + { + throw new AuthorizationFailedException(); + } + } + + public static async Task AssertReviewOwnerAsync(ClaimsPrincipal user, ReviewListItemModel reviewModel, IAuthorizationService authorizationService) + { + var result = await authorizationService.AuthorizeAsync(user, reviewModel, new[] { ReviewOwnerRequirement.Instance }); + if (!result.Succeeded) + { + throw new AuthorizationFailedException(); + } + } + + public static void AssertAPIRevisionDeletion(APIRevisionListItemModel apiRevision) + { + // We allow deletion of manual API review only. + // Server side assertion to ensure we are not processing any requests to delete automatic and PR API review + if (apiRevision.APIRevisionType != APIRevisionType.Manual) + { + throw new UnDeletableReviewException(); + } + } + + public static async Task AssertPullRequestCreatorPermission( + PullRequestModel prModel, HashSet allowedListBotAccounts, IOpenSourceRequestManager openSourceManager, + TelemetryClient telemetryClient) + { + // White list bot accounts to create API reviews from PR automatically + if (!allowedListBotAccounts.Contains(prModel.CreatedBy)) + { + var isAuthorized = await openSourceManager.IsAuthorizedUser(prModel.CreatedBy); + if (!isAuthorized) + { + telemetryClient.TrackTrace($"API change detection permission failed for user {prModel.CreatedBy}. API review is only created if PR author is an internal user."); + throw new AuthorizationFailedException(); + } + } + } + + public static string ResolveReviewUrl(PullRequestModel pullRequest, string hostName) + { + var url = $"https://{hostName}/Assemblies/Review/{pullRequest.ReviewId}"; + if (!String.IsNullOrEmpty(pullRequest.APIRevisionId)) + { + url += $"?revisionId={pullRequest.APIRevisionId}"; + } + return url; + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs index bff0eb6fe5c..90c1b0d6e22 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs @@ -1,11 +1,28 @@ -using System.Security.Claims; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using APIView.DIff; +using ApiView; +using APIView; +using APIViewWeb.Managers; using APIViewWeb.Models; using APIViewWeb.Repositories; +using APIViewWeb.LeanModels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Hubs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; +using Microsoft.OpenApi.Any; +using Microsoft.AspNetCore.Http; namespace APIViewWeb.Helpers { public static class PageModelHelpers { + public static UserPreferenceModel GetUserPreference(UserPreferenceCache preferenceCache, ClaimsPrincipal User) { return preferenceCache.GetUserPreferences(User).Result; @@ -20,5 +37,492 @@ public static string GetHiddenApiClass(UserPreferenceModel userPreference) } return hiddenApiClass; } + + public static string GetLanguageCssSafeName(string language) + { + switch (language.ToLower()) + { + case "c#": + return "csharp"; + case "c++": + return "cplusplus"; + default: + return language.ToLower(); + } + } + + public static string GetUserEmail(ClaimsPrincipal user) => NotificationManager.GetUserEmail(user); + + public static bool IsUserSubscribed(ClaimsPrincipal user, HashSet subscribers) + { + string email = GetUserEmail(user); + if (email != null) + { + return subscribers.Contains(email); + } + return false; + } + /// + /// Create the CodelIneModel from Diffs + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static CodeLineModel[] CreateLines(CodeDiagnostic[] diagnostics, InlineDiffLine[] lines, + ReviewCommentsModel comments, bool showDiffOnly, int reviewDiffContextSize, string diffContextSeparator, + HashSet headingsOfSectionsWithDiff, bool hideCommentRows = false) + { + if (showDiffOnly) + { + lines = CreateDiffOnlyLines(lines, reviewDiffContextSize, diffContextSeparator); + if (lines.Length == 0) + { + return Array.Empty(); + } + } + List documentedByLines = new List(); + int lineNumberExcludingDocumentation = 0; + int diffSectionId = 0; + + return lines.Select( + (diffLine, index) => + { + if (diffLine.Line.IsDocumentation) + { + // documentedByLines must include the index of a line, assuming that documentation lines are counted + documentedByLines.Add(++index); + return new CodeLineModel( + kind: diffLine.Kind, + codeLine: diffLine.Line, + commentThread: comments.TryGetThreadForLine(diffLine.Line.ElementId, out var thread, hideCommentRows) ? + thread : + null, + diagnostics: diffLine.Kind != DiffLineKind.Removed ? + diagnostics.Where(d => d.TargetId == diffLine.Line.ElementId).ToArray() : + Array.Empty(), + lineNumber: lineNumberExcludingDocumentation, + documentedByLines: new int[] { }, + isDiffView: true, + diffSectionId: diffLine.Line.SectionKey != null ? ++diffSectionId : null, + otherLineSectionKey: diffLine.Kind == DiffLineKind.Unchanged ? diffLine.OtherLine.SectionKey : null, + headingsOfSectionsWithDiff: headingsOfSectionsWithDiff, + isSubHeadingWithDiffInSection: diffLine.IsHeadingWithDiffInSection + ); + } + else + { + CodeLineModel c = new CodeLineModel( + kind: diffLine.Kind, + codeLine: diffLine.Line, + commentThread: diffLine.Kind != DiffLineKind.Removed && + comments.TryGetThreadForLine(diffLine.Line.ElementId, out var thread, hideCommentRows) ? + thread : + null, + diagnostics: diffLine.Kind != DiffLineKind.Removed ? + diagnostics.Where(d => d.TargetId == diffLine.Line.ElementId).ToArray() : + Array.Empty(), + lineNumber: diffLine.Line.LineNumber ?? ++lineNumberExcludingDocumentation, + documentedByLines: documentedByLines.ToArray(), + isDiffView: true, + diffSectionId: diffLine.Line.SectionKey != null ? ++diffSectionId : null, + otherLineSectionKey: diffLine.Kind == DiffLineKind.Unchanged ? diffLine.OtherLine.SectionKey : null, + headingsOfSectionsWithDiff: headingsOfSectionsWithDiff, + isSubHeadingWithDiffInSection: diffLine.IsHeadingWithDiffInSection + ); + documentedByLines.Clear(); + return c; + } + }).ToArray(); + } + + /// + /// Create CodeLineModel fron regular codelines + /// + /// + /// + /// + /// + /// + public static CodeLineModel[] CreateLines(CodeDiagnostic[] diagnostics, CodeLine[] lines, ReviewCommentsModel comments, bool hideCommentRows = false) + { + List documentedByLines = new List(); + int lineNumberExcludingDocumentation = 0; + return lines.Select( + (line, index) => + { + if (line.IsDocumentation) + { + // documentedByLines must include the index of a line, assuming that documentation lines are counted + documentedByLines.Add(++index); + return new CodeLineModel( + DiffLineKind.Unchanged, + line, + comments.TryGetThreadForLine(line.ElementId, out var thread, hideCommentRows) ? thread : null, + diagnostics.Where(d => d.TargetId == line.ElementId).ToArray(), + lineNumberExcludingDocumentation, + new int[] { } + ); + } + else + { + CodeLineModel c = new CodeLineModel( + DiffLineKind.Unchanged, + line, + comments.TryGetThreadForLine(line.ElementId, out var thread, hideCommentRows) ? thread : null, + diagnostics.Where(d => d.TargetId == line.ElementId).ToArray(), + line.LineNumber ?? ++lineNumberExcludingDocumentation, + documentedByLines.ToArray() + ); + documentedByLines.Clear(); + return c; + } + }).ToArray(); + } + + /// + /// Compute conversiation info in the review + /// + /// + /// + /// + public static int ComputeActiveConversationsInActiveRevision(CodeLine[] lines, ReviewCommentsModel comments) + { + int activeThreadsFromActiveReviewRevisions = 0; + + foreach (CodeLine line in lines) + { + if (string.IsNullOrEmpty(line.ElementId)) + { + continue; + } + + // if we have comments for this line and the thread has not been resolved. + // Add "&& !thread.Comments.First().IsUsageSampleComment()" to exclude sample comments from being counted (This also prevents the popup before approval) + if (comments.TryGetThreadForLine(line.ElementId, out CommentThreadModel reviewThread) && !reviewThread.IsResolved && reviewThread.Comments.First().CommentType == CommentType.APIRevision) + { + activeThreadsFromActiveReviewRevisions++; + } + } + return activeThreadsFromActiveReviewRevisions; + } + + /// + /// Get all the data needed to for a review page + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task GetReviewContentAsync( + IConfiguration configuration, IReviewManager reviewManager, UserPreferenceCache preferenceCache, + ICosmosUserProfileRepository userProfileRepository, IAPIRevisionsManager reviewRevisionsManager, ICommentsManager commentManager, + IBlobCodeFileRepository codeFileRepository, IHubContext signalRHubContext, ClaimsPrincipal user, string reviewId, + string revisionId = null, string diffRevisionId = null, bool showDocumentation = false, bool showDiffOnly = false, int diffContextSize = 3, + string diffContextSeperator = "
.....
", HashSet headingsOfSectionsWithDiff = null) + { + var userId = user.GetGitHubLogin(); + var review = await reviewManager.GetReviewAsync(user, reviewId); + + if (review == null) + { + return default(ReviewContentModel); + } + + var apiRevisions = await reviewRevisionsManager.GetAPIRevisionsAsync(reviewId); + + // Try getting latest Automatic Revision, otherwise get latest of any type or default + var activeRevision = await reviewRevisionsManager.GetLatestAPIRevisionsAsync(reviewId, apiRevisions, APIRevisionType.Automatic); + if (activeRevision == null) + { + var notifcation = new NotificationModel() { Message = $"This review has no valid apiRevisons", Level = NotificatonLevel.Warning }; + await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + } + + APIRevisionListItemModel diffRevision = null; + if (!string.IsNullOrEmpty(revisionId)) { + if (apiRevisions.Where(x => x.Id == revisionId).Any()) + { + activeRevision = apiRevisions.First(x => x.Id == revisionId); + } + else + { + var notifcation = new NotificationModel() { Message = $"A revision with ID {revisionId} does not exist for this review.", Level = NotificatonLevel.Warning }; + await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + } + } + var comments = await commentManager.GetReviewCommentsAsync(reviewId); + + var activeRevisionRenderableCodeFile = await codeFileRepository.GetCodeFileAsync(activeRevision.Id, activeRevision.Files[0].FileId); + var activeRevisionReviewCodeFile = activeRevisionRenderableCodeFile.CodeFile; + var fileDiagnostics = activeRevisionReviewCodeFile.Diagnostics ?? Array.Empty(); + var activeRevisionHtmlLines = activeRevisionRenderableCodeFile.Render(showDocumentation: showDocumentation); + + var codeLines = new CodeLineModel[0]; + + + if (!string.IsNullOrEmpty(diffRevisionId)) + { + if (apiRevisions.Where(x => x.Id == diffRevisionId).Any()) + { + diffRevision = await reviewRevisionsManager.GetAPIRevisionAsync(user, diffRevisionId); + var diffRevisionRenderableCodeFile = await codeFileRepository.GetCodeFileAsync(diffRevisionId, diffRevision.Files[0].FileId); + var diffRevisionHTMLLines = diffRevisionRenderableCodeFile.RenderReadOnly(showDocumentation: showDocumentation); + var diffRevisionTextLines = diffRevisionRenderableCodeFile.RenderText(showDocumentation: showDocumentation); + + var activeRevisionTextLines = activeRevisionRenderableCodeFile.RenderText(showDocumentation: showDocumentation); + + var diffLines = InlineDiff.Compute(diffRevisionTextLines, activeRevisionTextLines, diffRevisionHTMLLines, activeRevisionHtmlLines); + codeLines = CreateLines(diagnostics: fileDiagnostics, lines: diffLines, comments: comments, showDiffOnly: showDiffOnly, + reviewDiffContextSize: diffContextSize, diffContextSeparator: diffContextSeperator, headingsOfSectionsWithDiff: headingsOfSectionsWithDiff); + + if (!codeLines.Any()) + { + var notifcation = new NotificationModel() { Message = $"There is no diff between the two revisions. {activeRevision.Id} : {diffRevisionId}", Level = NotificatonLevel.Info }; + await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + } + } + else + { + var notifcation = new NotificationModel() { Message = $"A diffRevision with ID {diffRevisionId} does not exist for this review.", Level = NotificatonLevel.Warning }; + await signalRHubContext.Clients.Group(userId).SendAsync("RecieveNotification", notifcation); + } + } + else + { + codeLines = CreateLines(diagnostics: fileDiagnostics, lines: activeRevisionHtmlLines, comments: comments); + } + + HashSet preferredApprovers = new HashSet(); + var approverConfig = configuration["approvers"]; + if (!string.IsNullOrEmpty(approverConfig)) + { + foreach (var username in approverConfig.Split(",")) + { + if (username.Equals(userId)) + { + var userCache = preferenceCache.GetUserPreferences(user).Result; + var langs = userCache.ApprovedLanguages.ToHashSet(); + if (!langs.Any()) + { + UserProfileModel userProfile = await userProfileRepository.TryGetUserProfileAsync(username); + langs = userProfile.Languages; + userCache.ApprovedLanguages = langs; + preferenceCache.UpdateUserPreference(userCache, user); + } + if (langs.Contains(review.Language) || !langs.Any()) + { + preferredApprovers.Add(username); + } + } + else + { + UserProfileModel userProfile = await userProfileRepository.TryGetUserProfileAsync(username); + var langs = userProfile.Languages; + if (langs.Contains(review.Language) || !langs.Any()) + { + preferredApprovers.Add(username); + } + } + } + } + + var reviewPageContent = new ReviewContentModel + { + Review = review, + Navigation = activeRevisionRenderableCodeFile.CodeFile.Navigation, + codeLines = codeLines, + APIRevisionsGrouped = apiRevisions.OrderByDescending(c => c.CreatedOn).GroupBy(r => r.APIRevisionType).ToDictionary(r => r.Key.ToString(), r => r.ToList()), + ActiveAPIRevision = activeRevision, + DiffAPIRevision = diffRevision, + TotalActiveConversiations = comments.Threads.Count(t => !t.IsResolved), + ActiveConversationsInActiveAPIRevision = ComputeActiveConversationsInActiveRevision(activeRevisionHtmlLines, comments), + ActiveConversationsInSampleRevisions = comments.Threads.Count(t => t.Comments.FirstOrDefault()?.CommentType == CommentType.SamplesRevision), + PreferredApprovers = preferredApprovers, + TaggableUsers = commentManager.GetTaggableUsers(), + PageHasLoadableSections = activeRevisionReviewCodeFile.LeafSections?.Any() ?? false + }; + return reviewPageContent; + } + + /// + /// Get CodeLineSection + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task GetCodeLineSectionAsync(ClaimsPrincipal user, IReviewManager reviewManager, + IAPIRevisionsManager apiRevisionsManager, ICommentsManager commentManager, + IBlobCodeFileRepository codeFileRepository, string reviewId, int sectionKey, string revisionId = null, + string diffRevisionId = null, int diffContextSize = 3, string diffContextSeperator = "
.....
", + int? sectionKeyA = null, int? sectionKeyB = null + ) + { + var activeRevision = await apiRevisionsManager.GetAPIRevisionAsync(user, revisionId); + var activeRevisionRenderableCodeFile = await codeFileRepository.GetCodeFileAsync(activeRevision.Id, activeRevision.Files[0].FileId); + var fileDiagnostics = activeRevisionRenderableCodeFile.CodeFile.Diagnostics ?? Array.Empty(); + CodeLine[] activeRevisionHTMLLines; + + var comments = await commentManager.GetReviewCommentsAsync(reviewId); + + var codeLines = new CodeLineModel[0]; + + if (diffRevisionId != null) + { + InlineDiffLine[] diffLines; + var diffRevision = await apiRevisionsManager.GetAPIRevisionAsync(user, diffRevisionId); + var diffRevisionRenderableCodeFile = await codeFileRepository.GetCodeFileAsync(diffRevisionId, diffRevision.Files[0].FileId); + + if (sectionKeyA != null && sectionKeyB != null) + { + var currentRootNode = activeRevisionRenderableCodeFile.GetCodeLineSectionRoot((int)sectionKeyA); + var previousRootNode = diffRevisionRenderableCodeFile.GetCodeLineSectionRoot((int)sectionKeyB); + var diffSectionRoot = apiRevisionsManager.ComputeSectionDiff(previousRootNode, currentRootNode, diffRevisionRenderableCodeFile, activeRevisionRenderableCodeFile); + diffLines = activeRevisionRenderableCodeFile.GetDiffCodeLineSection(diffSectionRoot); + } + else if (sectionKeyA != null) + { + activeRevisionHTMLLines = activeRevisionRenderableCodeFile.GetCodeLineSection((int)sectionKeyA); + var diffRevisionHtmlLines = new CodeLine[] { }; + var diffRevisionTextLines = new CodeLine[] { }; + var activeRevisionTextLines = activeRevisionRenderableCodeFile.GetCodeLineSection((int)sectionKeyA, renderType: RenderType.Text); + diffLines = InlineDiff.Compute( + diffRevisionTextLines, + activeRevisionTextLines, + diffRevisionHtmlLines, + activeRevisionHTMLLines); + } + else + { + activeRevisionHTMLLines = new CodeLine[] { }; + var diffRevisionHtmlLines = diffRevisionRenderableCodeFile.GetCodeLineSection((int)sectionKeyB, RenderType.ReadOnly); + var diffRevisionTextLines = diffRevisionRenderableCodeFile.GetCodeLineSection((int)sectionKeyB, renderType: RenderType.Text); + var currentRevisionTextLines = new CodeLine[] { }; + diffLines = InlineDiff.Compute( + diffRevisionTextLines, + currentRevisionTextLines, + diffRevisionHtmlLines, + activeRevisionHTMLLines); + } + + var headingsOfSectionsWithDiff = diffRevision.HeadingsOfSectionsWithDiff.ContainsKey(activeRevision.Id) ? + diffRevision.HeadingsOfSectionsWithDiff[activeRevision.Id] : new HashSet(); + + codeLines = PageModelHelpers.CreateLines(diagnostics: fileDiagnostics, lines: diffLines, comments: comments, + showDiffOnly: true, reviewDiffContextSize: diffContextSize, diffContextSeparator: diffContextSeperator, + headingsOfSectionsWithDiff: headingsOfSectionsWithDiff); + } + else + { + activeRevisionHTMLLines = activeRevisionRenderableCodeFile.GetCodeLineSection(sectionKey); + codeLines = PageModelHelpers.CreateLines(diagnostics: fileDiagnostics, lines: activeRevisionHTMLLines, comments: comments, hideCommentRows: true); + } + return codeLines; + } + + /// + /// Ensure unique label for Revisions + /// + /// + /// + /// + public static string ResolveRevisionLabel(APIRevisionListItemModel apiRevision, bool addType = true) + { + var label = $"{apiRevision.CreatedOn.ToString()} | {apiRevision.CreatedBy}"; + + if (apiRevision.Files.Any() && !String.IsNullOrEmpty(apiRevision.Files[0].PackageVersion)) + { + label = $"{apiRevision.Files[0].PackageVersion} | {label}"; + } + + if (!String.IsNullOrWhiteSpace(apiRevision.Label)) + { + label = $"{label} | {apiRevision.Label}"; + } + + if (addType) + { + label = $"{apiRevision.APIRevisionType.ToString()} | {label}"; + } + return label; + } + + /// + /// Create DiffOnly Lines + /// + /// + /// + /// + /// + private static InlineDiffLine[] CreateDiffOnlyLines(InlineDiffLine[] lines, int reviewDiffContextSize, string diffContextSeparator) + { + var filteredLines = new List>(); + int lastAddedLine = -1; + for (int i = 0; i < lines.Count(); i++) + { + if (lines[i].Kind != DiffLineKind.Unchanged) + { + // Find starting index for pre context + int preContextIndx = Math.Max(lastAddedLine + 1, i - reviewDiffContextSize); + if (preContextIndx < i) + { + // Add sepearator to show skipping lines. for e.g. ..... + if (filteredLines.Count > 0) + { + filteredLines.Add(new InlineDiffLine(new CodeLine(diffContextSeparator, null, null), DiffLineKind.Unchanged)); + } + + while (preContextIndx < i) + { + filteredLines.Add(lines[preContextIndx]); + preContextIndx++; + } + } + //Add changed line + filteredLines.Add(lines[i]); + lastAddedLine = i; + + // Add post context + int contextStart = i + 1, contextEnd = i + reviewDiffContextSize; + while (contextStart <= contextEnd && contextStart < lines.Count() && lines[contextStart].Kind == DiffLineKind.Unchanged) + { + filteredLines.Add(lines[contextStart]); + lastAddedLine = contextStart; + contextStart++; + } + } + } + return filteredLines.ToArray(); + } } } diff --git a/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs b/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs new file mode 100644 index 00000000000..876aa7689e9 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using System.Threading; +using System; +using Microsoft.Extensions.Hosting; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Managers; +using Microsoft.Extensions.Configuration; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights; +using MongoDB.Driver.Linq; +using System.Linq; +using APIViewWeb.LeanModels; + +namespace APIViewWeb.HostedServices +{ + public class LinesWithDiffBackgroundHostedService : BackgroundService + { + private readonly bool _isDisabled; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionManager; + + static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); + + public LinesWithDiffBackgroundHostedService(IReviewManager reviewManager, IAPIRevisionsManager apiRevisionManager, IConfiguration configuration) + { + _reviewManager = reviewManager; + _apiRevisionManager = apiRevisionManager; + + if (bool.TryParse(configuration["LinesWithDiffBackgroundTaskDisabled"], out bool taskDisabled)) + { + _isDisabled = taskDisabled; + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!_isDisabled) + { + try + { + var reviews = await _reviewManager.GetReviewsAsync(language: "Swagger"); + foreach (var review in reviews) + { + var apiRevisions = await _apiRevisionManager.GetAPIRevisionsAsync(reviewId: review.Id); + + foreach (var apiRevision in apiRevisions) + { + await _apiRevisionManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(reviewId: review.Id, apiRevision: apiRevision); + } + } + } + catch (Exception ex) + { + _telemetryClient.TrackException(ex); + } + } + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/HostedServices/PullRequestBackgroundHostedService.cs b/src/dotnet/APIView/APIViewWeb/HostedServices/PullRequestBackgroundHostedService.cs index 53a8834a68e..cc346e8ffb0 100644 --- a/src/dotnet/APIView/APIViewWeb/HostedServices/PullRequestBackgroundHostedService.cs +++ b/src/dotnet/APIView/APIViewWeb/HostedServices/PullRequestBackgroundHostedService.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Threading; diff --git a/src/dotnet/APIView/APIViewWeb/HostedServices/ReviewBackgroundHostedService.cs b/src/dotnet/APIView/APIViewWeb/HostedServices/ReviewBackgroundHostedService.cs index d1ef881dcc5..09d3ce247ae 100644 --- a/src/dotnet/APIView/APIViewWeb/HostedServices/ReviewBackgroundHostedService.cs +++ b/src/dotnet/APIView/APIViewWeb/HostedServices/ReviewBackgroundHostedService.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.Extensions.Configuration; @@ -16,15 +17,18 @@ public class ReviewBackgroundHostedService : BackgroundService { private readonly bool _isDisabled; private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionManager; private readonly int _autoArchiveInactiveGracePeriodMonths; // This is inactive duration in months private readonly HashSet _upgradeDisabledLangs = new HashSet(); private readonly int _backgroundBatchProcessCount; static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); - public ReviewBackgroundHostedService(IReviewManager reviewManager, IConfiguration configuration) + public ReviewBackgroundHostedService(IReviewManager reviewManager, IAPIRevisionsManager apiRevisionManager, IConfiguration configuration) { _reviewManager = reviewManager; + _apiRevisionManager = apiRevisionManager; + // We can disable background task using app settings if required if (bool.TryParse(configuration["BackgroundTaskDisabled"], out bool taskDisabled)) { @@ -56,8 +60,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - await _reviewManager.UpdateReviewBackground(_upgradeDisabledLangs, _backgroundBatchProcessCount); - await ArchiveInactiveReviews(stoppingToken, _autoArchiveInactiveGracePeriodMonths); + await _reviewManager.UpdateReviewsInBackground(_upgradeDisabledLangs, _backgroundBatchProcessCount); + await ArchiveInactiveAPIReviews(stoppingToken, _autoArchiveInactiveGracePeriodMonths); } catch (Exception ex) { @@ -66,13 +70,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - private async Task ArchiveInactiveReviews(CancellationToken stoppingToken, int archiveAfter) + private async Task ArchiveInactiveAPIReviews(CancellationToken stoppingToken, int archiveAfter) { do { try { - await _reviewManager.AutoArchiveReviews(archiveAfter); + await _apiRevisionManager.AutoArchiveAPIRevisions(archiveAfter); } catch(Exception ex) { diff --git a/src/dotnet/APIView/APIViewWeb/Languages/LanguageService.cs b/src/dotnet/APIView/APIViewWeb/Languages/LanguageService.cs index 3c4667fd184..b011e636133 100644 --- a/src/dotnet/APIView/APIViewWeb/Languages/LanguageService.cs +++ b/src/dotnet/APIView/APIViewWeb/Languages/LanguageService.cs @@ -34,7 +34,7 @@ public abstract class LanguageService public static string[] SupportedLanguages = LanguageServiceHelpers.SupportedLanguages; - public virtual bool GeneratePipelineRunParams(ReviewGenPipelineParamModel param) => true; + public virtual bool GeneratePipelineRunParams(APIRevisionGenerationPipelineParamModel param) => true; public static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); diff --git a/src/dotnet/APIView/APIViewWeb/Languages/TypeSpecLanguageService.cs b/src/dotnet/APIView/APIViewWeb/Languages/TypeSpecLanguageService.cs index 3f960865541..0c856cc3eea 100644 --- a/src/dotnet/APIView/APIViewWeb/Languages/TypeSpecLanguageService.cs +++ b/src/dotnet/APIView/APIViewWeb/Languages/TypeSpecLanguageService.cs @@ -43,7 +43,7 @@ public override bool CanUpdate(string versionString) return false; } - public override bool GeneratePipelineRunParams(ReviewGenPipelineParamModel param) + public override bool GeneratePipelineRunParams(APIRevisionGenerationPipelineParamModel param) { var filePath = param.FileName; // Verify TypeSpec source file path is a GitHub URL to TypeSpec package root diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/ChangeHistory.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/ChangeHistory.cs index c12c2a94859..5beee61ef38 100644 --- a/src/dotnet/APIView/APIViewWeb/LeanModels/ChangeHistory.cs +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/ChangeHistory.cs @@ -12,11 +12,63 @@ public enum AICommentChangeAction Modified } - public class AICommentChangeHistoryModel + [JsonConverter(typeof(StringEnumConverter))] + public enum ReviewChangeAction + { + Created = 0, + Closed, + ReOpened, + Approved, + ApprovalReverted, + Deleted, + UnDeleted + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum APIRevisionChangeAction + { + Created = 0, + Approved, + ApprovalReverted, + Deleted, + UnDeleted + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum CommentChangeAction + { + Created = 0, + Edited, + Resolved, + UnResolved, + Deleted, + Undeleted + } + + public abstract class ChangeHistoryModel + { + public string ChangedBy { get; set; } + public DateTime? ChangedOn { get; set; } + public string Notes { get; set; } + } + + public class AICommentChangeHistoryModel : ChangeHistoryModel { - [JsonConverter(typeof(StringEnumConverter))] public AICommentChangeAction ChangeAction { get; set; } - public string User { get; set; } - public DateTime ChangeDateTime { get; set; } + } + + public class ReviewChangeHistoryModel : ChangeHistoryModel + { + public ReviewChangeAction ChangeAction { get; set; } + } + + public class APIRevisionChangeHistoryModel : ChangeHistoryModel + { + public APIRevisionChangeAction ChangeAction { get; set; } + } + + public class CommentChangeHistoryModel : ChangeHistoryModel + { + public CommentChangeAction ChangeAction { get; set; } } } diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/CommentItemModel.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/CommentItemModel.cs new file mode 100644 index 00000000000..f5bcf69dff2 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/CommentItemModel.cs @@ -0,0 +1,38 @@ +using APIViewWeb.Helpers; +using Microsoft.TeamFoundation.SourceControl.WebApi; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace APIViewWeb.LeanModels +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum CommentType + { + APIRevision = 0, + SamplesRevision + } + + public class CommentItemModel + { + [JsonProperty("id")] + public string Id { get; set; } = IdHelper.GenerateId(); + public string ReviewId { get; set; } + public string APIRevisionId { get; set; } + public string ElementId { get; set; } + public string SectionClass { get; set; } + public string CommentText { get; set; } + public List ChangeHistory { get; set; } = new List(); + public bool IsResolved { get; set; } + public List Upvotes { get; set; } = new List(); + public HashSet TaggedUsers { get; set; } = new HashSet(); + public CommentType CommentType { get; set; } + public bool ResolutionLocked { get; set; } = false; + public string CreatedBy { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime? LastEditedOn { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewListModels.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewListModels.cs new file mode 100644 index 00000000000..30ca9ea8abe --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewListModels.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using APIViewWeb.Helpers; +using APIViewWeb.Models; + +namespace APIViewWeb.LeanModels +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum APIRevisionType + { + Manual = 0, + Automatic, + PullRequest, + All + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ReviewState + { + Open = 0, + Closed + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ApprovalStatus + { + Pending = 0, + Approved + } + + public class ReviewAssignmentModel + { + public string AssignedBy { get; set; } + public string AssingedTo { get; set; } + public DateTime AssingedOn { get; set; } + } + + public class ReviewListModel + { + public int TotalNumberOfReviews { get; set; } + public List Reviews { get; set; } + } + + public class ReviewRevisionListModel + { + public int TotalNumberOfReviewRevisions { get; set; } + public List APIRevisions { get; set; } + } + + public class LegacyReviewModel + { + [JsonProperty("id")] + public string ReviewId { get; set; } + public string Name { get; set; } + public string Author { get; set; } + public DateTime CreationDate { get; set; } + public List Revisions { get; set; } = new List(); + public bool RunAnalysis { get; set; } + public bool IsClosed { get; set; } + public HashSet Subscribers { get; set; } = new HashSet(); + public bool IsAutomatic { get; set; } + public APIRevisionType FilterType { get; set; } + public string ServiceName { get; set; } + public string PackageDisplayName { get; set; } + public HashSet RequestedReviewers { get; set; } + public string RequestedBy { get; set; } + public DateTime ApprovalRequestedOn; + public DateTime ApprovalDate; + public bool IsApprovedForFirstRelease { get; set; } + public string ApprovedForFirstReleaseBy { get; set; } + public DateTime ApprovedForFirstReleaseOn { get; set; } + public DateTime LastUpdated { get; set; } + } + + public class LegacyRevisionModel + { + [JsonProperty("id")] + public string RevisionId { get; set; } + public List Files { get; set; } = new List(); + public Dictionary> HeadingsOfSectionsWithDiff { get; set; } = new Dictionary>(); + public DateTime CreationDate { get; set; } = DateTime.Now; + public string Name { get; set; } + public string Author { get; set; } + public string Label { get; set; } + public int RevisionNumber { get; set; } + public HashSet Approvers { get; set; } = new HashSet(); + public bool IsApproved { get; set; } + } + + public class ReviewListItemModel + { + [JsonProperty("id")] + public string Id { get; set; } = IdHelper.GenerateId(); + public string PackageName { get; set; } + public string Language { get; set; } + public HashSet Subscribers { get; set; } = new HashSet(); + public List ChangeHistory { get; set; } = new List(); + public List AssignedReviewers { get; set; } = new List(); + public bool IsClosed { get; set; } + public bool IsApproved { get; set; } + public string CreatedBy { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime LastUpdatedOn { get; set; } + public bool IsDeleted { get; set; } + } + + public class APIRevisionListItemModel + { + [JsonProperty("id")] + public string Id { get; set; } = IdHelper.GenerateId(); + public string ReviewId { get; set; } + public string PackageName { get; set; } + public string Language { get; set; } + public List Files { get; set; } = new List(); + public string Label { get; set; } + public List ChangeHistory { get; set; } = new List(); + public APIRevisionType APIRevisionType { get; set; } + public int? PullRequestNo { get; set; } + public Dictionary> HeadingsOfSectionsWithDiff { get; set; } = new Dictionary>(); + public bool IsApproved { get; set; } + public HashSet Approvers { get; set; } = new HashSet(); + public string CreatedBy { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime LastUpdatedOn { get; set; } + public bool IsDeleted { get; set; } + } + + public class SamplesRevisionModel + { + [JsonProperty("id")] + public string Id { get; set; } = IdHelper.GenerateId(); + public string ReviewId { get; set; } + public string FileId { get; set; } = IdHelper.GenerateId(); + public string OriginalFileId { get; set; } = IdHelper.GenerateId(); + public string OriginalFileName { get; set; } // likely to be null if uploaded via text + public string CreatedBy { get; set; } + public DateTime CreatedOn { get; set; } + public string Title { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs new file mode 100644 index 00000000000..df8510728b8 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/LeanModels/ReviewRevisionPageModels.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using ApiView; +using APIView; +using APIViewWeb.Models; + +namespace APIViewWeb.LeanModels +{ + public class ReviewContentModel + { + public ReviewListItemModel Review { get; set; } + public NavigationItem[] Navigation { get; set; } + public CodeLineModel[] codeLines { get; set; } + public Dictionary> APIRevisionsGrouped { get; set; } + public APIRevisionListItemModel ActiveAPIRevision { get; set; } + public APIRevisionListItemModel DiffAPIRevision { get; set; } + public int TotalActiveConversiations { get; set; } + public int ActiveConversationsInActiveAPIRevision { get; set; } + public int ActiveConversationsInSampleRevisions { get; set; } + public HashSet PreferredApprovers = new HashSet(); + public HashSet TaggableUsers { get; set; } + public bool PageHasLoadableSections { get; set; } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/AICommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/AICommentsManager.cs index 198a055e384..b567808b5bf 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/AICommentsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/AICommentsManager.cs @@ -44,8 +44,8 @@ public async Task CreateAICommentAsync(AICommentDTO aiCommentDto new AICommentChangeHistoryModel() { ChangeAction = AICommentChangeAction.Created, - User = user, - ChangeDateTime = DateTime.UtcNow + ChangedBy = user, + ChangedOn = DateTime.UtcNow } } }; @@ -89,8 +89,8 @@ public async Task UpdateAICommentAsync(string id, AICommentDTO a aiCommentModel.ChangeHistory.Add(new AICommentChangeHistoryModel() { ChangeAction = AICommentChangeAction.Modified, - User = user, - ChangeDateTime = DateTime.UtcNow + ChangedBy = user, + ChangedOn = DateTime.UtcNow }); await _aiCommentsRepository.UpsertAICommentAsync(aiCommentModel); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs new file mode 100644 index 00000000000..027bdfa3dee --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs @@ -0,0 +1,744 @@ +using ApiView; +using APIView.DIff; +using APIView.Model; +using APIViewWeb.Helpers; +using APIViewWeb.Hubs; +using APIViewWeb.LeanModels; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Models; +using APIViewWeb.Repositories; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; + +namespace APIViewWeb.Managers +{ + public class APIRevisionsManager : IAPIRevisionsManager + { + private readonly IAuthorizationService _authorizationService; + private readonly ICosmosReviewRepository _reviewsRepository; + private readonly IBlobCodeFileRepository _codeFileRepository; + private readonly ICosmosAPIRevisionsRepository _apiRevisionsRepository; + private readonly IHubContext _signalRHubContext; + private readonly IEnumerable _languageServices; + private readonly ICodeFileManager _codeFileManager; + private readonly IDevopsArtifactRepository _devopsArtifactRepository; + private readonly IBlobOriginalsRepository _originalsRepository; + private readonly INotificationManager _notificationManager; + + static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); + + + public APIRevisionsManager( + IAuthorizationService authorizationService, + ICosmosReviewRepository reviewsRepository, + ICosmosAPIRevisionsRepository apiRevisionsRepository, + IHubContext signalRHubContext, + IEnumerable languageServices, + IDevopsArtifactRepository devopsArtifactRepository, + ICodeFileManager codeFileManager, + IBlobCodeFileRepository codeFileRepository, + IBlobOriginalsRepository originalsRepository, + INotificationManager notificationManager) + { + _reviewsRepository = reviewsRepository; + _apiRevisionsRepository = apiRevisionsRepository; + _authorizationService = authorizationService; + _signalRHubContext = signalRHubContext; + _codeFileManager = codeFileManager; + _codeFileRepository = codeFileRepository; + _languageServices = languageServices; + _devopsArtifactRepository = devopsArtifactRepository; + _originalsRepository = originalsRepository; + _notificationManager = notificationManager; + } + + /// + /// Retrieve Revisions from the Revisions container in CosmosDb after applying filter to the query. + /// + /// Contains paginationinfo + /// Contains filter and sort parameters + /// + public async Task> GetAPIRevisionsAsync(PageParams pageParams, APIRevisionsFilterAndSortParams filterAndSortParams) + { + return await _apiRevisionsRepository.GetAPIRevisionsAsync(pageParams, filterAndSortParams); + } + + /// + /// Retrieve Revisions for a particular Review from the Revisions container in CosmosDb + /// + /// The Reviewid for which the revisions are to be retrieved + /// + public async Task> GetAPIRevisionsAsync(string reviewId) + { + return await _apiRevisionsRepository.GetAPIRevisionsAsync(reviewId); + } + + /// + /// Retrieve the latest APRevison for a particular Review. + /// Filter by APIRevisionType if specified and Review contains specified type + /// If APIRevisionType is not specified, return the latest revision irrespective of the type + /// Return default if no revisoin is found + /// + /// + /// The list of revisions can be supplied if available to avoid another call to the database + /// + /// APIRevisionListItemModel + public async Task GetLatestAPIRevisionsAsync(string reviewId = null, IEnumerable apiRevisions = null, APIRevisionType apiRevisionType = APIRevisionType.All) + { + if (reviewId == null && apiRevisions == null) + { + throw new ArgumentException("Either reviewId or apiRevisions must be supplied"); + } + + if (apiRevisions == null) + { + apiRevisions = await _apiRevisionsRepository.GetAPIRevisionsAsync(reviewId); + } + + if (apiRevisionType != APIRevisionType.All && apiRevisions.Any(r => r.APIRevisionType == apiRevisionType)) + { + apiRevisions = apiRevisions.Where(r => r.APIRevisionType == apiRevisionType); + } + return apiRevisions.OrderByDescending(r => r.CreatedOn).FirstOrDefault(); + } + + /// + /// Retrieve Revisions from the Revisions container in CosmosDb. + /// + /// + /// The RevisionId for which the revision is to be retrieved + /// + public async Task GetAPIRevisionAsync(ClaimsPrincipal user, string apiRevisionId) + { + if (user == null) + { + throw new UnauthorizedAccessException(); + } + return await _apiRevisionsRepository.GetAPIRevisionAsync(apiRevisionId); + } + + /// + /// GetNewAPIRevisionAsync + /// + /// + /// + /// + /// + /// + /// + /// + /// + public APIRevisionListItemModel GetNewAPIRevisionAsync(APIRevisionType apiRevisionType, + string reviewId = null, string packageName = null, string language = null, + string label = null, int? prNumber = null, string createdBy="azure-sdk") + { + var apiRevision = new APIRevisionListItemModel() + { + CreatedBy = createdBy, + CreatedOn = DateTime.UtcNow, + APIRevisionType = apiRevisionType, + ChangeHistory = new List() + { + new APIRevisionChangeHistoryModel() + { + ChangeAction = APIRevisionChangeAction.Created, + ChangedBy = createdBy, + ChangedOn = DateTime.UtcNow + } + }, + }; + + if (!String.IsNullOrEmpty(reviewId)) + apiRevision.ReviewId = reviewId; + + if (!String.IsNullOrEmpty(packageName)) + apiRevision.PackageName = packageName; + + if (!String.IsNullOrEmpty(language)) + apiRevision.Language = language; + + if (!String.IsNullOrEmpty(language)) + apiRevision.Language = language; + + if (!String.IsNullOrEmpty(label)) + apiRevision.Label = label; + + if (prNumber != null) + apiRevision.PullRequestNo = prNumber; + + return apiRevision; + } + + /// + /// Add new Approval or ApprovalReverted action to the ChangeHistory of a Revision + /// + /// + /// + /// + /// + /// + /// true if review approval needs to be updated otherwise false + public async Task ToggleAPIRevisionApprovalAsync(ClaimsPrincipal user, string id, string apiRevisionId = null, APIRevisionListItemModel apiRevision = null, string notes = "") + { + if (apiRevisionId == null && apiRevision == null) + { + throw new ArgumentException(message: "apiRevisionId and apiRevision cannot both be null"); + } + + bool updateReview = false; + if (apiRevision == null) + { + apiRevision = await _apiRevisionsRepository.GetAPIRevisionAsync(apiRevisionId: apiRevisionId); + } + ReviewListItemModel review = await _reviewsRepository.GetReviewAsync(apiRevision.ReviewId); + + await ManagerHelpers.AssertApprover(user, apiRevision, _authorizationService); + var userId = user.GetGitHubLogin(); + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(apiRevision.ChangeHistory, APIRevisionChangeAction.Approved, userId, notes); + apiRevision.ChangeHistory = changeUpdate.ChangeHistory; + apiRevision.IsApproved = changeUpdate.ChangeStatus; + if (ChangeHistoryHelpers.GetChangeActionStatus(apiRevision.ChangeHistory, APIRevisionChangeAction.Approved, userId)) + { + apiRevision.Approvers.Add(userId); + } + else + { + apiRevision.Approvers.Remove(userId); + } + + if (!review.IsApproved && apiRevision.IsApproved) + { + updateReview = true; // If review is not approved and revision is approved, update review + } + + await _apiRevisionsRepository.UpsertAPIRevisionAsync(apiRevision); + await _signalRHubContext.Clients.Group(userId).SendAsync("ReceiveApprovalSelf", id, apiRevisionId, apiRevision.IsApproved); + await _signalRHubContext.Clients.All.SendAsync("ReceiveApproval", id, apiRevisionId, userId, apiRevision.IsApproved); + return updateReview; + } + + /// + /// Add API Revision to Review + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AddAPIRevisionAsync( + ClaimsPrincipal user, + string reviewId, + APIRevisionType apiRevisionType, + string name, + string label, + Stream fileStream, + string language = "", + bool awaitComputeDiff = false) + { + var review = await _reviewsRepository.GetReviewAsync(reviewId); + await AddAPIRevisionAsync(user, review, apiRevisionType, name, label, fileStream, language, awaitComputeDiff); + } + + /// + /// For reviews with collapsible sections (Swagger). Precomputs the line numbers of the headings with diff + /// + /// + /// + /// + /// + public async Task GetLineNumbersOfHeadingsOfSectionsWithDiff(string reviewId, APIRevisionListItemModel apiRevision, IEnumerable apiRevisions = null) + { + if (apiRevisions == null) + { + apiRevisions = await _apiRevisionsRepository.GetAPIRevisionsAsync(reviewId); + } + var latestRevisionCodeFile = await _codeFileRepository.GetCodeFileAsync(apiRevision, false); + var latestRevisionHtmlLines = latestRevisionCodeFile.Render(false); + var latestRevisionTextLines = latestRevisionCodeFile.RenderText(false); + + foreach (var rev in apiRevisions) + { + // Calculate diff against previous revisions only. APIView only shows diff against revision lower than current one. + if (rev.Id != apiRevision.Id) + { + var lineNumbersForHeadingOfSectionWithDiff = new HashSet(); + var earlierRevisionCodeFile = await _codeFileRepository.GetCodeFileAsync(rev, false); + var earlierRevisionHtmlLines = earlierRevisionCodeFile.RenderReadOnly(false); + var earlierRevisionTextLines = earlierRevisionCodeFile.RenderText(false); + + var diffLines = InlineDiff.Compute(earlierRevisionTextLines, latestRevisionTextLines, earlierRevisionHtmlLines, latestRevisionHtmlLines); + + foreach (var diffLine in diffLines) + { + if (diffLine.Kind == DiffLineKind.Unchanged && diffLine.Line.SectionKey != null && diffLine.OtherLine.SectionKey != null) + { + var latestRevisionRootNode = latestRevisionCodeFile.GetCodeLineSectionRoot((int)diffLine.Line.SectionKey); + var earlierRevisionRootNode = earlierRevisionCodeFile.GetCodeLineSectionRoot((int)diffLine.OtherLine.SectionKey); + var diffSectionRoot = ComputeSectionDiff(earlierRevisionRootNode, latestRevisionRootNode, earlierRevisionCodeFile, latestRevisionCodeFile); + if (latestRevisionCodeFile.ChildNodeHasDiff(diffSectionRoot)) + lineNumbersForHeadingOfSectionWithDiff.Add((int)diffLine.Line.LineNumber); + } + } + if (rev.HeadingsOfSectionsWithDiff.ContainsKey(apiRevision.Id)) + { + rev.HeadingsOfSectionsWithDiff.Remove(apiRevision.Id); + } + if (lineNumbersForHeadingOfSectionWithDiff.Any()) + { + rev.HeadingsOfSectionsWithDiff.Add(apiRevision.Id, lineNumbersForHeadingOfSectionWithDiff); + } + } + await _apiRevisionsRepository.UpsertAPIRevisionAsync(rev); + } + } + + /// + /// Computed the diff for hidden (colapsible) API sections + /// + /// + /// + /// + /// + /// + public TreeNode> ComputeSectionDiff(TreeNode before, TreeNode after, RenderedCodeFile beforeFile, RenderedCodeFile afterFile) + { + var rootDiff = new InlineDiffLine(before.Data, after.Data, DiffLineKind.Unchanged); + var resultRoot = new TreeNode>(rootDiff); + + var queue = new Queue<(TreeNode before, TreeNode after, TreeNode> current)>(); + + queue.Enqueue((before, after, resultRoot)); + + while (queue.Count > 0) + { + var nodesInProcess = queue.Dequeue(); + var (beforeHTMLLines, beforeTextLines) = GetCodeLinesForDiff(nodesInProcess.before, nodesInProcess.current, beforeFile); + var (afterHTMLLines, afterTextLines) = GetCodeLinesForDiff(nodesInProcess.after, nodesInProcess.current, afterFile); + + var diffResult = InlineDiff.Compute(beforeTextLines, afterTextLines, beforeHTMLLines, afterHTMLLines); + + if (diffResult.Count() == 2 && + diffResult[0]!.Line.NodeRef != null && diffResult[1]!.Line.NodeRef != null && + diffResult[0]!.Line.NodeRef.IsLeaf && diffResult[1]!.Line.NodeRef.IsLeaf) // Detached Leaf Parents which are Eventually Discarded + { + var inlineDiffLine = new InlineDiffLine(diffResult[1].Line, diffResult[0].Line, DiffLineKind.Unchanged); + diffResult = new InlineDiffLine[] { inlineDiffLine }; + } + + foreach (var diff in diffResult) + { + var addedChild = nodesInProcess.current.AddChild(diff); + + switch (diff.Kind) + { + case DiffLineKind.Removed: + queue.Enqueue((diff.Line.NodeRef, null, addedChild)); + break; + case DiffLineKind.Added: + queue.Enqueue((null, diff.Line.NodeRef, addedChild)); + break; + case DiffLineKind.Unchanged: + queue.Enqueue((diff.OtherLine.NodeRef, diff.Line.NodeRef, addedChild)); + break; + } + } + } + return resultRoot; + } + + /// + /// Add APIRevision + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AddAPIRevisionAsync( + ClaimsPrincipal user, + ReviewListItemModel review, + APIRevisionType apiRevisionType, + string name, + string label, + Stream fileStream, + string language, + bool awaitComputeDiff = false) + { + var revision = GetNewAPIRevisionAsync( + reviewId: review.Id, + apiRevisionType: apiRevisionType, + packageName: review.PackageName, + language: review.Language, + createdBy: review.CreatedBy, + label: label); + + var codeFile = await _codeFileManager.CreateCodeFileAsync( + revision.Id, + name, + fileStream, + true, + language); + + revision.Files.Add(codeFile); + + var languageService = language != null ? _languageServices.FirstOrDefault(l => l.Name == language) : _languageServices.FirstOrDefault(s => s.IsSupportedFile(name)); + // Run pipeline to generate the review if sandbox is enabled + if (languageService != null && languageService.IsReviewGenByPipeline) + { + // Run offline review gen for review and reviewCodeFileModel + await GenerateAPIRevisionInExternalResource(review, revision.Id, codeFile.FileId, name, language); + } + + // auto subscribe revision creation user + await _notificationManager.SubscribeAsync(review, user); + await _reviewsRepository.UpsertReviewAsync(review); + await _apiRevisionsRepository.UpsertAPIRevisionAsync(revision); + await _notificationManager.NotifySubscribersOnNewRevisionAsync(review, revision, user); + + if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") + { + if (awaitComputeDiff) + { + await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, revision); + } + else + { + _ = Task.Run(async () => await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, revision)); + } + } + //await GenerateAIReview(review, revision); + } + + /// + /// Run Pipeline to generate API Revision + /// + /// + /// + /// + public async Task RunAPIRevisionGenerationPipeline(List reviewGenParams, string language) + { + var jsonSerializerOptions = new JsonSerializerOptions() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + var reviewParamString = JsonSerializer.Serialize(reviewGenParams, jsonSerializerOptions); + reviewParamString = reviewParamString.Replace("\"", "'"); + await _devopsArtifactRepository.RunPipeline($"tools - generate-{language}-apireview", + reviewParamString, + _originalsRepository.GetContainerUrl()); + } + + /// + /// Delete APIRevisions + /// + /// + /// + /// + /// + public async Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, string reviewId, string apiRevisionId) + { + var apiRevision = await _apiRevisionsRepository.GetAPIRevisionAsync(apiRevisionId: apiRevisionId); + ManagerHelpers.AssertAPIRevisionDeletion(apiRevision); + await ManagerHelpers.AssertAPIRevisionOwner(user, apiRevision, _authorizationService); + await SoftDeleteAPIRevisionAsync(user, apiRevision); + } + + /// + /// Delete APIRevisions + /// + /// + /// + /// + public async Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, APIRevisionListItemModel apiRevision) + { + ManagerHelpers.AssertAPIRevisionDeletion(apiRevision); + await ManagerHelpers.AssertAPIRevisionOwner(user, apiRevision, _authorizationService); + await SoftDeleteAPIRevisionAsync(userName: user.GetGitHubLogin(), apiRevision: apiRevision); + } + + /// + /// Delete APIRevisions + /// + /// + /// + /// + /// + public async Task SoftDeleteAPIRevisionAsync(APIRevisionListItemModel apiRevision, string userName = "azure-sdk", string notes = "") + { + if (!apiRevision.IsDeleted) + { + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction( + changeHistory: apiRevision.ChangeHistory, action: APIRevisionChangeAction.Deleted, user: userName, notes: notes); + + apiRevision.ChangeHistory = changeUpdate.ChangeHistory; + apiRevision.IsDeleted = changeUpdate.ChangeStatus; + + await _apiRevisionsRepository.UpsertAPIRevisionAsync(apiRevision); + } + } + + /// + /// + /// + /// + /// + /// + /// + public async Task UpdateAPIRevisionLabelAsync(ClaimsPrincipal user, string revisionId, string label) + { + var revision = await GetAPIRevisionAsync(user, revisionId); + await ManagerHelpers.AssertAPIRevisionOwner(user, revision, _authorizationService); + revision.Label = label; + await _apiRevisionsRepository.UpsertAPIRevisionAsync(revision); + } + + /// + /// UpdateAPIRevisionCodeFileAsync + /// + /// + /// + /// + /// + /// + public async Task UpdateAPIRevisionCodeFileAsync(string repoName, string buildId, string artifact, string project) + { + var stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifact, filePath: null, project: project, format: "zip"); + var archive = new ZipArchive(stream); + foreach (var entry in archive.Entries) + { + var reviewFilePath = entry.FullName; + var reviewDetails = reviewFilePath.Split("/"); + + if (reviewDetails.Length < 4 || !reviewFilePath.EndsWith(".json")) + continue; + + var reviewId = reviewDetails[1]; + var apiRevisionId = reviewDetails[2]; + var codeFile = await CodeFile.DeserializeAsync(entry.Open()); + + // Update code file with one downloaded from pipeline + var review = await _reviewsRepository.GetReviewAsync(reviewId); + if (review != null) + { + var apiRevision = await _apiRevisionsRepository.GetAPIRevisionAsync(apiRevisionId: apiRevisionId); + if (apiRevision != null) + { + await _codeFileRepository.UpsertCodeFileAsync(apiRevisionId, apiRevision.Files.Single().FileId, codeFile); + var file = apiRevision.Files.FirstOrDefault(); + file.VersionString = codeFile.VersionString; + file.PackageName = codeFile.PackageName; + await _reviewsRepository.UpsertReviewAsync(review); + + if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") + { + // Trigger diff calculation using updated code file from sandboxing pipeline + await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.Id, apiRevision); + } + } + } + } + } + + /// + /// Check if APIRevision is the Same + /// + /// + /// + /// + public async Task IsAPIRevisionTheSame(APIRevisionListItemModel revision, RenderedCodeFile renderedCodeFile) + { + //This will compare and check if new code file content is same as revision in parameter + var lastRevisionFile = await _codeFileRepository.GetCodeFileAsync(revision, false); + var lastRevisionTextLines = lastRevisionFile.RenderText(false, skipDiff: true); + var fileTextLines = renderedCodeFile.RenderText(false, skipDiff: true); + return lastRevisionTextLines.SequenceEqual(fileTextLines); + } + + /// + /// Update APIRevision + /// + /// + /// + /// + /// + public async Task UpdateAPIRevisionAsync(APIRevisionListItemModel revision, LanguageService languageService, TelemetryClient telemetryClient) + { + foreach (var file in revision.Files) + { + if (!file.HasOriginal || !languageService.CanUpdate(file.VersionString)) + { + continue; + } + + try + { + var fileOriginal = await _originalsRepository.GetOriginalAsync(file.FileId); + // file.Name property has been repurposed to store package name and version string + // This is causing issue when updating review using latest parser since it expects Name field as file name + // We have added a new property FileName which is only set for new reviews + // All older reviews needs to be handled by checking review name field + var fileName = file.FileName ?? file.Name; + var codeFile = await languageService.GetCodeFileAsync(fileName, fileOriginal, false); + await _codeFileRepository.UpsertCodeFileAsync(revision.Id, file.FileId, codeFile); + // update only version string + file.VersionString = codeFile.VersionString; + await _apiRevisionsRepository.UpsertAPIRevisionAsync(revision); + } + catch (Exception ex) + { + telemetryClient.TrackTrace("Failed to update revision " + revision.Id); + telemetryClient.TrackException(ex); + } + } + } + + /// + /// SoftDelete APIRevision if its not been updated after many months + /// + /// + /// + public async Task AutoArchiveAPIRevisions(int archiveAfterMonths) + { + var lastUpdatedDate = DateTime.Now.Subtract(TimeSpan.FromDays(archiveAfterMonths * 30)); + var manualRevisions = await _apiRevisionsRepository.GetAPIRevisionsAsync(lastUpdatedOn: lastUpdatedDate, apiRevisionType: APIRevisionType.Manual); + + // Find all inactive reviews + foreach (var apiRevision in manualRevisions) + { + var requestTelemetry = new RequestTelemetry { Name = "Archiving Revision " + apiRevision.Id }; + var operation = _telemetryClient.StartOperation(requestTelemetry); + try + { + await SoftDeleteAPIRevisionAsync(apiRevision: apiRevision, notes: "Auto archived"); + await Task.Delay(500); + } + catch (Exception e) + { + _telemetryClient.TrackException(e); + } + finally + { + _telemetryClient.StopOperation(operation); + } + } + } + + /// + /// CreateAPIRevisionAsync + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task CreateAPIRevisionAsync(string userName, string reviewId, APIRevisionType apiRevisionType, string label, + MemoryStream memoryStream, CodeFile codeFile, string originalName = null, int? prNumber = null) + { + + var apiRevision = GetNewAPIRevisionAsync( + reviewId: reviewId, + apiRevisionType: apiRevisionType, + packageName: codeFile.PackageName, + language: codeFile.Language, + createdBy: userName, + prNumber: prNumber, + label: label); + + var apiRevisionCodeFile = await _codeFileManager.CreateReviewCodeFileModel(apiRevisionId: apiRevision.Id, memoryStream: memoryStream, codeFile: codeFile); + apiRevision.Files.Add(apiRevisionCodeFile); + if (!string.IsNullOrEmpty(originalName)) + { + apiRevisionCodeFile.FileName = originalName; + } + await _apiRevisionsRepository.UpsertAPIRevisionAsync(apiRevision); + return apiRevision; + } + + /// + /// Generate the Revision on a DevOps Pipeline + /// + /// + /// + /// + /// + /// + /// + /// + private async Task GenerateAPIRevisionInExternalResource(ReviewListItemModel review, string revisionId, string fileId, string fileName, string language = null) + { + var languageService = _languageServices.Single(s => s.Name == language || s.Name == review.Language); + var param = new APIRevisionGenerationPipelineParamModel() + { + FileID = fileId, + ReviewID = review.Id, + RevisionID = revisionId, + FileName = fileName + }; + if (!languageService.GeneratePipelineRunParams(param)) + { + throw new Exception($"Failed to run pipeline for review: {param.ReviewID}, file: {param.FileName}"); + } + + var paramList = new List + { + param + }; + + await RunAPIRevisionGenerationPipeline(paramList, languageService.Name); + } + + /// + /// GetCodeLinesForDiff + /// + /// + /// + /// + /// + private (CodeLine[] htmlLines, CodeLine[] textLines) GetCodeLinesForDiff(TreeNode node, TreeNode> curr, RenderedCodeFile codeFile) + { + (CodeLine[] htmlLines, CodeLine[] textLines) result = (new CodeLine[] { }, new CodeLine[] { }); + if (node != null) + { + if (node.IsLeaf) + { + result.htmlLines = codeFile.GetDetachedLeafSectionLines(node); + result.textLines = codeFile.GetDetachedLeafSectionLines(node, renderType: RenderType.Text, skipDiff: true); + + if (result.htmlLines.Count() > 0) + { + curr.WasDetachedLeafParent = true; + } + } + else + { + result.htmlLines = result.textLines = node.Children.Select(x => new CodeLine(x.Data, nodeRef: x)).ToArray(); + } + } + return result; + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs new file mode 100644 index 00000000000..196d0ce0f1f --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/CodeFileManager.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using ApiView; +using APIViewWeb.Managers.Interfaces; +using APIViewWeb.Models; +using APIViewWeb.Repositories; +using Microsoft.CodeAnalysis.Host; + +namespace APIViewWeb.Managers +{ + public class CodeFileManager : ICodeFileManager + { + private readonly IEnumerable _languageServices; + private readonly IBlobCodeFileRepository _codeFileRepository; + private readonly IBlobOriginalsRepository _originalsRepository; + private readonly IDevopsArtifactRepository _devopsArtifactRepository; + + public CodeFileManager( + IEnumerable languageServices, IBlobCodeFileRepository codeFileRepository, + IBlobOriginalsRepository originalsRepository, IDevopsArtifactRepository devopsArtifactRepository) + { + _originalsRepository = originalsRepository; + _codeFileRepository = codeFileRepository; + _languageServices = languageServices; + _devopsArtifactRepository = devopsArtifactRepository; + } + + /// + /// Get CodeFile + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task GetCodeFileAsync(string repoName, + string buildId, + string artifactName, + string packageName, + string originalFileName, + string codeFileName, + MemoryStream originalFileStream, + string baselineCodeFileName = "", + MemoryStream baselineStream = null, + string project = "public" + ) + { + Stream stream = null; + CodeFile codeFile = null; + if (string.IsNullOrEmpty(codeFileName)) + { + // backward compatibility until all languages moved to sandboxing of codefile to pipeline + stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifactName, originalFileName, format: "file", project: project); + codeFile = await CreateCodeFileAsync(Path.GetFileName(originalFileName), stream, false, originalFileStream); + } + else + { + stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifactName, packageName, format: "zip", project: project); + var archive = new ZipArchive(stream); + foreach (var entry in archive.Entries) + { + var fileName = Path.GetFileName(entry.Name); + if (fileName == originalFileName) + { + await entry.Open().CopyToAsync(originalFileStream); + } + + if (fileName == codeFileName) + { + codeFile = await CodeFile.DeserializeAsync(entry.Open()); + } + else if (fileName == baselineCodeFileName) + { + await entry.Open().CopyToAsync(baselineStream); + } + } + } + + return codeFile; + } + + /// + /// Create Code File + /// + /// + /// + /// + /// + /// + /// + public async Task CreateCodeFileAsync( + string apiRevisionId, + string originalName, + Stream fileStream, + bool runAnalysis, + string language) + { + using var memoryStream = new MemoryStream(); + var codeFile = await CreateCodeFileAsync(originalName, fileStream, runAnalysis, memoryStream, language); + var reviewCodeFileModel = await CreateReviewCodeFileModel(apiRevisionId, memoryStream, codeFile); + reviewCodeFileModel.FileName = originalName; + return reviewCodeFileModel; + } + + /// + /// Create Code File + /// + /// + /// + /// + /// + /// + /// + public async Task CreateCodeFileAsync( + string originalName, + Stream fileStream, + bool runAnalysis, + MemoryStream memoryStream, + string language = null) + { + var languageService = _languageServices.FirstOrDefault(s => (language != null ? s.Name == language : s.IsSupportedFile(originalName))); + if (fileStream != null) + { + await fileStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + } + CodeFile codeFile = null; + if (languageService.IsReviewGenByPipeline) + { + codeFile = languageService.GetReviewGenPendingCodeFile(originalName); + } + else + { + codeFile = await languageService.GetCodeFileAsync( + originalName, + memoryStream, + runAnalysis); + } + return codeFile; + } + + /// + /// Create Code File + /// + /// + /// + /// + /// + public async Task CreateReviewCodeFileModel(string apiRevisionId, MemoryStream memoryStream, CodeFile codeFile) + { + var reviewCodeFileModel = new APICodeFileModel + { + HasOriginal = true, + }; + + InitializeFromCodeFile(reviewCodeFileModel, codeFile); + if (memoryStream != null) + { + memoryStream.Position = 0; + await _originalsRepository.UploadOriginalAsync(reviewCodeFileModel.FileId, memoryStream); + } + await _codeFileRepository.UpsertCodeFileAsync(apiRevisionId, reviewCodeFileModel.FileId, codeFile); + return reviewCodeFileModel; + } + + /// + /// Compare two CodeFiles + /// + /// + /// + /// + public bool IsAPICodeFilesTheSame(RenderedCodeFile codeFileA, RenderedCodeFile codeFileB) + { + var codeFileATextLines = codeFileA.RenderText(false, skipDiff: true); + var codeFileBTextLines = codeFileB.RenderText(false, skipDiff: true); + return codeFileATextLines.SequenceEqual(codeFileBTextLines); + } + + private void InitializeFromCodeFile(APICodeFileModel file, CodeFile codeFile) + { + file.Language = codeFile.Language; + file.LanguageVariant = codeFile.LanguageVariant; + file.VersionString = codeFile.VersionString; + file.Name = codeFile.Name; + file.PackageName = codeFile.PackageName; + file.PackageVersion = codeFile.PackageVersion; + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs index 38970c695eb..da785b998a5 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs @@ -14,6 +14,9 @@ using Newtonsoft.Json; using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.Common; +using APIViewWeb.LeanModels; +using APIViewWeb.Helpers; +using Microsoft.AspNetCore.Mvc.ViewEngines; namespace APIViewWeb.Managers { @@ -71,6 +74,11 @@ public async void LoadTaggableUsers() TaggableUsers = new HashSet(TaggableUsers.OrderBy(g => g.Login)); } + public async Task> GetCommentsAsync(string reviewId) + { + return await _commentsRepository.GetCommentsAsync(reviewId); + } + public async Task GetReviewCommentsAsync(string reviewId) { var comments = await _commentsRepository.GetCommentsAsync(reviewId); @@ -81,29 +89,43 @@ public async Task GetReviewCommentsAsync(string reviewId) public async Task GetUsageSampleCommentsAsync(string reviewId) { var comments = await _commentsRepository.GetCommentsAsync(reviewId); - return new ReviewCommentsModel(reviewId, comments.Where((e) => e.IsUsageSampleComment)); + return new ReviewCommentsModel(reviewId, comments.Where(c => c.CommentType == LeanModels.CommentType.SamplesRevision)); } - public async Task AddCommentAsync(ClaimsPrincipal user, CommentModel comment) + public async Task AddCommentAsync(ClaimsPrincipal user, CommentItemModel comment) { - comment.Username = user.GetGitHubLogin(); - comment.TimeStamp = DateTime.Now; + comment.ChangeHistory.Add( + new CommentChangeHistoryModel() + { + ChangeAction = CommentChangeAction.Created, + ChangedBy = user.GetGitHubLogin(), + ChangedOn = DateTime.Now, + }); + comment.CreatedBy = user.GetGitHubLogin(); + comment.CreatedOn = DateTime.Now; await _commentsRepository.UpsertCommentAsync(comment); - if (!comment.IsResolve) + + if (!comment.IsResolved) { await _notificationManager.NotifyUserOnCommentTag(comment); await _notificationManager.NotifySubscribersOnComment(user, comment); } } - public async Task UpdateCommentAsync(ClaimsPrincipal user, string reviewId, string commentId, string commentText, string[] taggedUsers) + public async Task UpdateCommentAsync(ClaimsPrincipal user, string reviewId, string commentId, string commentText, string[] taggedUsers) { var comment = await _commentsRepository.GetCommentAsync(reviewId, commentId); await AssertOwnerAsync(user, comment); - comment.EditedTimeStamp = DateTime.Now; - comment.Comment = commentText; - comment.Username = user.GetGitHubLogin(); + comment.ChangeHistory.Add( + new CommentChangeHistoryModel() + { + ChangeAction = CommentChangeAction.Edited, + ChangedBy = user.GetGitHubLogin(), + ChangedOn = DateTime.Now, + }); + comment.LastEditedOn = DateTime.Now; + comment.CommentText = commentText; foreach (var taggedUser in taggedUsers) { @@ -119,21 +141,60 @@ public async Task UpdateCommentAsync(ClaimsPrincipal user, string return comment; } - public async Task DeleteCommentAsync(ClaimsPrincipal user, string reviewId, string commentId) + /// + /// Delete Comment + /// + /// + /// + /// + public async Task SoftDeleteCommentsAsync(ClaimsPrincipal user, string reviewId) + { + var comments = await _commentsRepository.GetCommentsAsync(reviewId); + + foreach (var comment in comments) + { + await SoftDeleteCommentAsync(user, comment); + } + } + + /// + /// Delete Comment + /// + /// + /// + /// + /// + public async Task SoftDeleteCommentAsync(ClaimsPrincipal user, string reviewId, string commentId) { var comment = await _commentsRepository.GetCommentAsync(reviewId, commentId); + await SoftDeleteCommentAsync(user, comment); + } + + /// + /// Delete Comment + /// + /// + /// + /// + public async Task SoftDeleteCommentAsync(ClaimsPrincipal user, CommentItemModel comment) + { await AssertOwnerAsync(user, comment); - await _commentsRepository.DeleteCommentAsync(comment); + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(comment.ChangeHistory, CommentChangeAction.Deleted, user.GetGitHubLogin()); + comment.ChangeHistory = changeUpdate.ChangeHistory; + comment.IsDeleted = changeUpdate.ChangeStatus; + await _commentsRepository.UpsertCommentAsync(comment); } public async Task ResolveConversation(ClaimsPrincipal user, string reviewId, string lineId) { - await AddCommentAsync(user, new CommentModel() + var comments = await _commentsRepository.GetCommentsAsync(reviewId, lineId); + foreach (var comment in comments) { - IsResolve = true, - ReviewId = reviewId, - ElementId = lineId - }); + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(comment.ChangeHistory, CommentChangeAction.Resolved, user.GetGitHubLogin()); + comment.ChangeHistory = changeUpdate.ChangeHistory; + comment.IsResolved = changeUpdate.ChangeStatus; + await _commentsRepository.UpsertCommentAsync(comment); + } } public async Task UnresolveConversation(ClaimsPrincipal user, string reviewId, string lineId) @@ -141,10 +202,10 @@ public async Task UnresolveConversation(ClaimsPrincipal user, string reviewId, s var comments = await _commentsRepository.GetCommentsAsync(reviewId, lineId); foreach (var comment in comments) { - if (comment.IsResolve) - { - await _commentsRepository.DeleteCommentAsync(comment); - } + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(comment.ChangeHistory, CommentChangeAction.Resolved, user.GetGitHubLogin()); + comment.ChangeHistory = changeUpdate.ChangeHistory; + comment.IsResolved = changeUpdate.ChangeStatus; + await _commentsRepository.UpsertCommentAsync(comment); } } @@ -161,7 +222,7 @@ public async Task ToggleUpvoteAsync(ClaimsPrincipal user, string reviewId, strin } public HashSet GetTaggableUsers() => TaggableUsers; - private async Task AssertOwnerAsync(ClaimsPrincipal user, CommentModel commentModel) + private async Task AssertOwnerAsync(ClaimsPrincipal user, CommentItemModel commentModel) { var result = await _authorizationService.AuthorizeAsync(user, commentModel, new[] { CommentOwnerRequirement.Instance }); if (!result.Succeeded) diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs new file mode 100644 index 00000000000..cfe4b466f60 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IAPIRevisionsManager.cs @@ -0,0 +1,40 @@ +using APIView.DIff; +using ApiView; +using APIViewWeb.Helpers; +using APIViewWeb.LeanModels; +using APIViewWeb.Models; +using System.Collections.Generic; +using System.IO; +using System.Security.Claims; +using System.Threading.Tasks; +using APIView.Model; +using Microsoft.ApplicationInsights; + +namespace APIViewWeb.Managers.Interfaces +{ + public interface IAPIRevisionsManager + { + public Task> GetAPIRevisionsAsync(PageParams pageParams, APIRevisionsFilterAndSortParams filterAndSortParams); + public Task> GetAPIRevisionsAsync(string reviewId); + public Task GetLatestAPIRevisionsAsync(string reviewId = null, IEnumerable apiRevisions = null, APIRevisionType apiRevisionType = APIRevisionType.All); + public Task GetAPIRevisionAsync(ClaimsPrincipal user, string apiRevisionId); + public APIRevisionListItemModel GetNewAPIRevisionAsync(APIRevisionType apiRevisionType, string reviewId = null, string packageName = null, string language = null, + string label = null, int? prNumber = null, string createdBy = "azure-sdk"); + public Task ToggleAPIRevisionApprovalAsync(ClaimsPrincipal user, string id, string revisionId = null, APIRevisionListItemModel apiRevision = null, string notes = ""); + public Task AddAPIRevisionAsync(ClaimsPrincipal user, string reviewId, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language = "", bool awaitComputeDiff = false); + public Task AddAPIRevisionAsync(ClaimsPrincipal user, ReviewListItemModel review, APIRevisionType apiRevisionType, string name, string label, Stream fileStream, string language, bool awaitComputeDiff = false); + public Task RunAPIRevisionGenerationPipeline(List reviewGenParams, string language); + public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, string reviewId, string revisionId); + public Task SoftDeleteAPIRevisionAsync(ClaimsPrincipal user, APIRevisionListItemModel apiRevision); + public Task SoftDeleteAPIRevisionAsync(APIRevisionListItemModel apiRevision, string userName = "azure-sdk", string notes = ""); + public Task UpdateAPIRevisionLabelAsync(ClaimsPrincipal user, string revisionId, string label); + public Task IsAPIRevisionTheSame(APIRevisionListItemModel apiRevision, RenderedCodeFile renderedCodeFile); + public Task UpdateAPIRevisionCodeFileAsync(string repoName, string buildId, string artifact, string project); + public Task GetLineNumbersOfHeadingsOfSectionsWithDiff(string reviewId, APIRevisionListItemModel apiRevision, IEnumerable apiRevisions = null); + public TreeNode> ComputeSectionDiff(TreeNode before, TreeNode after, RenderedCodeFile beforeFile, RenderedCodeFile afterFile); + public Task CreateAPIRevisionAsync(string userName, string reviewId, APIRevisionType apiRevisionType, string label, + MemoryStream memoryStream, CodeFile codeFile, string originalName = null, int? prNumber = null); + public Task UpdateAPIRevisionAsync(APIRevisionListItemModel revision, LanguageService languageService, TelemetryClient telemetryClient); + public Task AutoArchiveAPIRevisions(int archiveAfterMonths); + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs new file mode 100644 index 00000000000..af5b95a0e78 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICodeFileManager.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading.Tasks; +using ApiView; +using APIViewWeb.Models; + +namespace APIViewWeb.Managers.Interfaces +{ + public interface ICodeFileManager + { + public Task GetCodeFileAsync(string repoName, string buildId, string artifactName, string packageName, string originalFileName, string codeFileName, + MemoryStream originalFileStream, string baselineCodeFileName = "", MemoryStream baselineStream = null, string project = "public"); + public Task CreateCodeFileAsync(string apiRevisionId, string originalName, Stream fileStream, bool runAnalysis, string language); + public Task CreateCodeFileAsync(string originalName, Stream fileStream, bool runAnalysis, MemoryStream memoryStream, string language = null); + public Task CreateReviewCodeFileModel(string apiRevisionId, MemoryStream memoryStream, CodeFile codeFile); + public bool IsAPICodeFilesTheSame(RenderedCodeFile codeFileA, RenderedCodeFile codeFileB); + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICommentsManager.cs index 3ed84e23f35..4de0a55f9d7 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICommentsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ICommentsManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; +using APIViewWeb.LeanModels; using APIViewWeb.Models; namespace APIViewWeb.Managers @@ -8,11 +9,14 @@ namespace APIViewWeb.Managers public interface ICommentsManager { public void LoadTaggableUsers(); + public Task> GetCommentsAsync(string reviewId); public Task GetReviewCommentsAsync(string reviewId); public Task GetUsageSampleCommentsAsync(string reviewId); - public Task AddCommentAsync(ClaimsPrincipal user, CommentModel comment); - public Task UpdateCommentAsync(ClaimsPrincipal user, string reviewId, string commentId, string commentText, string[] taggedUsers); - public Task DeleteCommentAsync(ClaimsPrincipal user, string reviewId, string commentId); + public Task AddCommentAsync(ClaimsPrincipal user, CommentItemModel comment); + public Task UpdateCommentAsync(ClaimsPrincipal user, string reviewId, string commentId, string commentText, string[] taggedUsers); + public Task SoftDeleteCommentsAsync(ClaimsPrincipal user, string reviewId); + public Task SoftDeleteCommentAsync(ClaimsPrincipal user, string reviewId, string commentId); + public Task SoftDeleteCommentAsync(ClaimsPrincipal user, CommentItemModel comment); public Task ResolveConversation(ClaimsPrincipal user, string reviewId, string lineId); public Task UnresolveConversation(ClaimsPrincipal user, string reviewId, string lineId); public Task ToggleUpvoteAsync(ClaimsPrincipal user, string reviewId, string commentId); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs index 840383805d6..fba4b4655e2 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/INotificationManager.cs @@ -1,3 +1,4 @@ +using APIViewWeb.LeanModels; using APIViewWeb.Models; using System.Collections.Generic; using System.Security.Claims; @@ -7,12 +8,12 @@ namespace APIViewWeb.Managers { public interface INotificationManager { - public Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentModel comment); - public Task NotifyUserOnCommentTag(CommentModel comment); + public Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentItemModel comment); + public Task NotifyUserOnCommentTag(CommentItemModel comment); public Task NotifyApproversOfReview(ClaimsPrincipal user, string reviewId, HashSet reviewers); - public Task NotifySubscribersOnNewRevisionAsync(ReviewRevisionModel revision, ClaimsPrincipal user); + public Task NotifySubscribersOnNewRevisionAsync(ReviewListItemModel review, APIRevisionListItemModel revision, ClaimsPrincipal user); public Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId); - public Task SubscribeAsync(ReviewModel review, ClaimsPrincipal user); - public Task UnsubscribeAsync(ReviewModel review, ClaimsPrincipal user); + public Task SubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user); + public Task UnsubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user); } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs index eaa714555d0..4557b593ed3 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs @@ -6,12 +6,10 @@ namespace APIViewWeb.Managers { public interface IPullRequestManager { - public Task DetectApiChanges(string buildId, string artifactName, string originalFileName, - string commitSha, string repoName, string packageName, int prNumber, string hostName, string codeFileName = null, - string baselineCodeFileName = null, bool commentOnPR = true, string language = null, string project = "public"); + public Task> GetPullRequestsModelAsync(string reviewId); + public Task> GetPullRequestsModelAsync(int pullRequestNumber, string repoName); + public Task GetPullRequestModelAsync(int prNumber, string repoName, string packageName, string originalFile, string language); + public Task CreateOrUpdateCommentsOnPR(List pullRequests, string repoOwner, string repoName, int prNumber, string hostName); public Task CleanupPullRequestData(); - - public Task> GetPullRequestsModel(string reviewId); - public Task> GetPullRequestsModel(int pullRequestNumber, string repoName); } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IReviewManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IReviewManager.cs index 3de36858824..f851fc07fd9 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IReviewManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IReviewManager.cs @@ -1,46 +1,26 @@ using System.Collections.Generic; -using System.IO; using System.Security.Claims; using System.Threading.Tasks; -using ApiView; -using APIView.DIff; -using APIView.Model; -using APIViewWeb.Models; +using APIViewWeb.LeanModels; namespace APIViewWeb.Managers { public interface IReviewManager { - public Task CreateReviewAsync(ClaimsPrincipal user, string originalName, string label, Stream fileStream, bool runAnalysis, string langauge, bool awaitComputeDiff = false); - public Task> GetReviewsAsync(bool closed, string language, string packageName = null, ReviewType filterType = ReviewType.Manual); - public Task> GetReviewsAsync(string ServiceName, string PackageName, IEnumerable filterTypes); - public Task> GetReviewPropertiesAsync(string propertyName); - public Task> GetRequestedReviews(string userName); - public Task<(IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage)> GetPagedReviewsAsync( - IEnumerable search, IEnumerable languages, bool? isClosed, IEnumerable filterTypes, bool? isApproved, int offset, int limit, string orderBy); - public Task DeleteReviewAsync(ClaimsPrincipal user, string id); - public Task GetReviewAsync(ClaimsPrincipal user, string id); - public Task AddRevisionAsync(ClaimsPrincipal user, string reviewId, string name, string label, Stream fileStream, string language = "", bool awaitComputeDiff = false); - public Task CreateCodeFile(string originalName, Stream fileStream, bool runAnalysis, MemoryStream memoryStream, string language = null); - public Task CreateReviewCodeFileModel(string revisionId, MemoryStream memoryStream, CodeFile codeFile); - public Task DeleteRevisionAsync(ClaimsPrincipal user, string id, string revisionId); - public Task UpdateRevisionLabelAsync(ClaimsPrincipal user, string id, string revisionId, string label); - public Task ToggleIsClosedAsync(ClaimsPrincipal user, string id); - public Task ToggleApprovalAsync(ClaimsPrincipal user, string id, string revisionId); - public Task ApprovePackageNameAsync(ClaimsPrincipal user, string id); - public Task IsReviewSame(ReviewRevisionModel revision, RenderedCodeFile renderedCodeFile); - public Task CreateMasterReviewAsync(ClaimsPrincipal user, string originalName, string label, Stream fileStream, bool compareAllRevisions); - public Task UpdateReviewBackground(HashSet updateDisabledLanguages, int backgroundBatchProcessCount); - public Task GetCodeFile(string repoName, string buildId, string artifactName, string packageName, string originalFileName, string codeFileName, - MemoryStream originalFileStream, string baselineCodeFileName = "", MemoryStream baselineStream = null, string project = "public"); - public Task CreateApiReview(ClaimsPrincipal user, string buildId, string artifactName, string originalFileName, string label, - string repoName, string packageName, string codeFileName, bool compareAllRevisions, string project); - public Task AutoArchiveReviews(int archiveAfterMonths); - public Task UpdateReviewCodeFiles(string repoName, string buildId, string artifact, string project); - public Task RequestApproversAsync(ClaimsPrincipal User, string ReviewId, HashSet reviewers); - public Task GetLineNumbersOfHeadingsOfSectionsWithDiff(string reviewId, ReviewRevisionModel revision); - public TreeNode> ComputeSectionDiff(TreeNode before, TreeNode after, RenderedCodeFile beforeFile, RenderedCodeFile afterFile); - public Task IsApprovedForFirstRelease(string language, string packageName); + public Task> GetReviewsAsync(string language, bool? isClosed = false); + public Task GetReviewAsync(string language, string packageName, bool? isClosed = false); + public Task> GetReviewsAssignedToUser(string userName); + public Task<(IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage)> GetPagedReviewListAsync( + IEnumerable search, IEnumerable languages, bool? isClosed, bool? isApproved, int offset, int limit, string orderBy); + public Task GetReviewAsync(ClaimsPrincipal user, string id); + public Task GetLegacyReviewAsync(ClaimsPrincipal user, string id); + public Task CreateReviewAsync(string packageName, string language, bool isClosed = true); + public Task SoftDeleteReviewAsync(ClaimsPrincipal user, string id); + public Task ToggleReviewIsClosedAsync(ClaimsPrincipal user, string id); + public Task ToggleReviewApprovalAsync(ClaimsPrincipal user, string id, string revisionId, string notes=""); + public Task ApproveReviewAsync(ClaimsPrincipal user, string reviewId, string notes = ""); + public Task AssignReviewersToReviewAsync(ClaimsPrincipal User, string reviewId, HashSet reviewers); public Task GenerateAIReview(string reviewId, string revisionId); + public Task UpdateReviewsInBackground(HashSet updateDisabledLanguages, int backgroundBatchProcessCount); } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ISamplesRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ISamplesRevisionsManager.cs new file mode 100644 index 00000000000..281f73dccf2 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/ISamplesRevisionsManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.IO; +using System.Security.Claims; +using System.Threading.Tasks; +using APIViewWeb.LeanModels; + +namespace APIViewWeb.Managers +{ + public interface ISamplesRevisionsManager + { + public Task> GetSamplesRevisionsAsync(string reviewId); + public Task GetSamplesRevisionContentAsync(string fileId); + public Task UpsertSamplesRevisionsAsync(ClaimsPrincipal user, string reviewId, string sample, string revisionTitle, string FileName = null); + public Task UpsertSamplesRevisionsAsync(ClaimsPrincipal user, string reviewId, Stream fileStream, string revisionTitle, string FileName); + public Task DeleteSamplesRevisionAsync(ClaimsPrincipal user, string reviewId, string sampleId); + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IUsageSampleManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IUsageSampleManager.cs deleted file mode 100644 index e3e8f01e608..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IUsageSampleManager.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace APIViewWeb.Managers -{ - public interface IUsageSampleManager - { - public Task> GetReviewUsageSampleAsync(string reviewId); - public Task GetUsageSampleContentAsync(string fileId); - public Task UpsertReviewUsageSampleAsync(ClaimsPrincipal user, string reviewId, string sample, int revisionNum, string revisionTitle, string FileName = null); - public Task UpsertReviewUsageSampleAsync(ClaimsPrincipal user, string reviewId, Stream fileStream, int revisionNum, string revisionTitle, string FileName); - public Task DeleteUsageSampleAsync(ClaimsPrincipal user, string reviewId, string FileId, string sampleId); - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs index 55ce0f4b7bf..a2010670bc8 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs @@ -16,7 +16,8 @@ using Microsoft.ApplicationInsights; using System.Net.Http; using System.Text.Json; - +using APIViewWeb.LeanModels; +using APIViewWeb.Helpers; namespace APIViewWeb.Managers { @@ -45,13 +46,13 @@ public NotificationManager(IConfiguration configuration, _emailSenderServiceUrl = configuration["azure-sdk-emailer-url"] ?? ""; } - public async Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentModel comment) + public async Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentItemModel comment) { var review = await _reviewRepository.GetReviewAsync(comment.ReviewId); await SendEmailsAsync(review, user, GetHtmlContent(comment, review), comment.TaggedUsers); } - public async Task NotifyUserOnCommentTag(CommentModel comment) + public async Task NotifyUserOnCommentTag(CommentItemModel comment) { foreach (string username in comment.TaggedUsers) { @@ -74,19 +75,23 @@ await SendUserEmailsAsync(review, reviewerProfile, } } - public async Task NotifySubscribersOnNewRevisionAsync(ReviewRevisionModel revision, ClaimsPrincipal user) + public async Task NotifySubscribersOnNewRevisionAsync(ReviewListItemModel review, APIRevisionListItemModel revision, ClaimsPrincipal user) { - var review = revision.Review; - var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}"); - var htmlContent = $"A new revision, {revision.DisplayName}," + - $" was uploaded by {revision.Author}."; + var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.Id}"); + var htmlContent = $"A new revision, {PageModelHelpers.ResolveRevisionLabel(revision)}," + + $" was uploaded by {revision.CreatedBy}."; await SendEmailsAsync(review, user, htmlContent, null); } - + /// + /// Toggle Subscription to a Review + /// + /// + /// + /// public async Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId) { var review = await _reviewRepository.GetReviewAsync(reviewId); - if (review.IsUserSubscribed(user)) + if (PageModelHelpers.IsUserSubscribed(user, review.Subscribers)) { await UnsubscribeAsync(review, user); } @@ -96,7 +101,13 @@ public async Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId) } } - public async Task SubscribeAsync(ReviewModel review, ClaimsPrincipal user) + /// + /// Subscribe to Review + /// + /// + /// + /// + public async Task SubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user) { var email = GetUserEmail(user); @@ -107,7 +118,13 @@ public async Task SubscribeAsync(ReviewModel review, ClaimsPrincipal user) } } - public async Task UnsubscribeAsync(ReviewModel review, ClaimsPrincipal user) + /// + /// Unsubscribe from Review + /// + /// + /// + /// + public async Task UnsubscribeAsync(ReviewListItemModel review, ClaimsPrincipal user) { var email = GetUserEmail(user); if (email != null && review.Subscribers.Contains(email)) @@ -120,10 +137,10 @@ public async Task UnsubscribeAsync(ReviewModel review, ClaimsPrincipal user) public static string GetUserEmail(ClaimsPrincipal user) => user.FindFirstValue(ClaimConstants.Email); - private string GetApproverReviewHtmlContent(UserProfileModel user, ReviewModel review) + private string GetApproverReviewHtmlContent(UserProfileModel user, ReviewListItemModel review) { - var reviewName = review.Name; - var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}"); + var reviewName = review.PackageName; + var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.Id}"); var poster = user.UserName; var userLink = new Uri($"{_apiviewEndpoint}/Assemblies/Profile/{poster}"); var requestsLink = new Uri($"{_apiviewEndpoint}/Assemblies/RequestedReviews/"); @@ -135,12 +152,12 @@ private string GetApproverReviewHtmlContent(UserProfileModel user, ReviewModel r return sb.ToString(); } - private string GetCommentTagHtmlContent(CommentModel comment, ReviewModel review) + private string GetCommentTagHtmlContent(CommentItemModel comment, ReviewListItemModel review) { - var reviewName = review.Name; - var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); - var commentText = comment.Comment; - var poster = comment.Username; + var reviewName = review.PackageName; + var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.Id}#{Uri.EscapeDataString(comment.ElementId)}"); + var commentText = comment.CommentText; + var poster = comment.CreatedBy; var userLink = new Uri($"{_apiviewEndpoint}/Assemblies/Profile/{poster}"); var sb = new StringBuilder(); sb.Append($"{poster}"); @@ -153,22 +170,22 @@ private string GetCommentTagHtmlContent(CommentModel comment, ReviewModel review return sb.ToString(); } - private string GetHtmlContent(CommentModel comment, ReviewModel review) + private string GetHtmlContent(CommentItemModel comment, ReviewListItemModel review) { - var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); + var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.Id}#{Uri.EscapeDataString(comment.ElementId)}"); var sb = new StringBuilder(); sb.Append(GetContentHeading(comment, true)); sb.Append("

"); sb.Append($"In {comment.ElementId}:"); sb.Append("

"); - sb.Append(CommentMarkdownExtensions.MarkdownAsHtml(comment.Comment)); + sb.Append(CommentMarkdownExtensions.MarkdownAsHtml(comment.CommentText)); return sb.ToString(); } - private static string GetContentHeading(CommentModel comment, bool includeHtml) => - $"{(includeHtml ? $"{comment.Username}" : $"{comment.Username}")} commented on this review."; + private static string GetContentHeading(CommentItemModel comment, bool includeHtml) => + $"{(includeHtml ? $"{comment.CreatedBy}" : $"{comment.CreatedBy}")} commented on this review."; - private async Task SendUserEmailsAsync(ReviewModel review, UserProfileModel user, string htmlContent) + private async Task SendUserEmailsAsync(ReviewListItemModel review, UserProfileModel user, string htmlContent) { // Always send email to a test address when test address is configured. if (string.IsNullOrEmpty(user.Email)) @@ -177,9 +194,9 @@ private async Task SendUserEmailsAsync(ReviewModel review, UserProfileModel user return; } - await SendEmail(user.Email, $"Notification from APIView - {review.DisplayName}", htmlContent); + await SendEmail(user.Email, $"Notification from APIView - {review.PackageName}", htmlContent); } - private async Task SendEmailsAsync(ReviewModel review, ClaimsPrincipal user, string htmlContent, ISet notifiedUsers) + private async Task SendEmailsAsync(ReviewListItemModel review, ClaimsPrincipal user, string htmlContent, ISet notifiedUsers) { var initiatingUserEmail = GetUserEmail(user); // Find email address of already tagged users in comment @@ -191,7 +208,7 @@ private async Task SendEmailsAsync(ReviewModel review, ClaimsPrincipal user, str var email = await GetEmailAddress(username); if (string.IsNullOrEmpty(email)) { - _telemetryClient.TrackTrace($"Email address is not available for user {username}, review {review.ReviewId}. Not sending email."); + _telemetryClient.TrackTrace($"Email address is not available for user {username}, review {review.Id}. Not sending email."); continue; } notifiedEmails.Add(email); @@ -207,7 +224,7 @@ private async Task SendEmailsAsync(ReviewModel review, ClaimsPrincipal user, str foreach(var userEmail in subscribers) { - await SendEmail(userEmail, $"Update on APIView - {review.DisplayName} from {GetUserName(user)}", htmlContent); + await SendEmail(userEmail, $"Update on APIView - {review.PackageName} from {GetUserName(user)}", htmlContent); } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs index 49f15e7d3a6..b363a6c3f32 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs @@ -2,18 +2,16 @@ // Licensed under the MIT License. using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using ApiView; -using APIView.DIff; +using APIViewWeb.Helpers; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; using APIViewWeb.Repositories; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Octokit; @@ -21,127 +19,99 @@ namespace APIViewWeb.Managers { public class PullRequestManager : IPullRequestManager { - static readonly string REVIEW_URL = "https://{hostName}/Assemblies/Review/{ReviewId}"; static readonly string PR_APIVIEW_BOT_COMMENT_IDENTIFIER = "**API change check**"; static readonly string PR_APIVIEW_BOT_COMMENT = "APIView has identified API level changes in this PR and created following API reviews."; static readonly string PR_APIVIEW_BOT_NO_CHANGE_COMMENT = "API changes are not detected in this pull request."; static readonly GitHubClient _githubClient = new GitHubClient(new ProductHeaderValue("apiview")); readonly TelemetryClient _telemetryClient = new TelemetryClient(TelemetryConfiguration.CreateDefault()); - private readonly IReviewManager _reviewManager; private readonly ICosmosPullRequestsRepository _pullRequestsRepository; + private readonly ICosmosAPIRevisionsRepository _apiRevisionsRepository; + private readonly IAPIRevisionsManager _apiRevisionManager; private readonly IConfiguration _configuration; - private readonly ICosmosReviewRepository _reviewsRepository; - private readonly IBlobCodeFileRepository _codeFileRepository; - private readonly IDevopsArtifactRepository _devopsArtifactRepository; - private readonly IAuthorizationService _authorizationService; - private readonly IOpenSourceRequestManager _openSourceManager; private readonly int _pullRequestCleanupDays; - private HashSet _allowedListBotAccounts; private readonly bool _isGitClientAvailable; public PullRequestManager( - IAuthorizationService authorizationService, - IReviewManager reviewManager, - ICosmosReviewRepository reviewsRepository, ICosmosPullRequestsRepository pullRequestsRepository, - IBlobCodeFileRepository codeFileRepository, - IDevopsArtifactRepository devopsArtifactRepository, - IConfiguration configuration, - IOpenSourceRequestManager openSourceRequestManager + ICosmosAPIRevisionsRepository apiRevisionsRepository, + IAPIRevisionsManager apiRevisionManager, + IConfiguration configuration ) { - _reviewManager = reviewManager; _pullRequestsRepository = pullRequestsRepository; + _apiRevisionsRepository = apiRevisionsRepository; + _apiRevisionManager = apiRevisionManager; _configuration = configuration; - _reviewsRepository = reviewsRepository; - _codeFileRepository = codeFileRepository; - _devopsArtifactRepository = devopsArtifactRepository; - _authorizationService = authorizationService; - _openSourceManager = openSourceRequestManager; var ghToken = _configuration["github-access-token"]; if (!string.IsNullOrEmpty(ghToken)) { _githubClient.Credentials = new Credentials(ghToken); _isGitClientAvailable = true; } - var pullRequestReviewCloseAfter = _configuration["pull-request-review-close-after-days"] ?? "30"; _pullRequestCleanupDays = int.Parse(pullRequestReviewCloseAfter); - _allowedListBotAccounts = new HashSet(); - var botAllowedList = _configuration["allowedList-bot-github-accounts"]; - if (!string.IsNullOrEmpty(botAllowedList)) - { - _allowedListBotAccounts.UnionWith(botAllowedList.Split(",")); - } } + public async Task> GetPullRequestsModelAsync(string reviewId) { + return await _pullRequestsRepository.GetPullRequestsAsync(reviewId); + } - // API change detection for PR will pull artifact from devops artifact - public async Task DetectApiChanges(string buildId, - string artifactName, - string originalFileName, - string commitSha, - string repoName, - string packageName, - int prNumber, - string hostName, - string codeFileName = null, - string baselineCodeFileName = null, - bool commentOnPR = true, - string language = null, - string project = "public") + public async Task> GetPullRequestsModelAsync(int pullRequestNumber, string repoName) { - var requestTelemetry = new RequestTelemetry { Name = "Detecting API changes for PR: " + prNumber }; - var operation = _telemetryClient.StartOperation(requestTelemetry); - originalFileName = originalFileName ?? codeFileName; - var repoInfo = repoName.Split("/"); - var pullRequestModel = await GetPullRequestModel(prNumber, repoName, packageName, originalFileName, language); + return await _pullRequestsRepository.GetPullRequestsAsync(pullRequestNumber, repoName); + } + + public async Task GetPullRequestModelAsync(int prNumber, string repoName, string packageName, string originalFile, string language) + { + var pullRequestModel = await _pullRequestsRepository.GetPullRequestAsync(prNumber, repoName, packageName, language); if (pullRequestModel == null) { - return ""; + var repoInfo = repoName.Split("/"); + var pullRequest = await _githubClient.PullRequest.Get(repoInfo[0], repoInfo[1], prNumber); + pullRequestModel = new PullRequestModel() + { + RepoName = repoName, + PullRequestNumber = prNumber, + FilePath = originalFile, + CreatedBy = pullRequest.User.Login, + PackageName = packageName, + Language = language, + Assignee = pullRequest.Assignee?.Login + }; } - if (pullRequestModel.Commits.Any(c => c == commitSha)) + return pullRequestModel; + } + + public async Task CreateOrUpdateCommentsOnPR(List pullRequests, string repoOwner, string repoName, int prNumber, string hostName) + { + var existingComment = await GetExistingCommentForPackage(repoOwner, repoName, prNumber); + var bldr = new StringBuilder(PR_APIVIEW_BOT_COMMENT_IDENTIFIER); + bldr.Append(Environment.NewLine).Append(Environment.NewLine); + if (pullRequests.Count > 0) { - // PR commit is already processed. No need to reprocess it again. - return !string.IsNullOrEmpty(pullRequestModel.ReviewId) ? REVIEW_URL.Replace("{hostName}", hostName) - .Replace("{ReviewId}", pullRequestModel.ReviewId) : ""; + bldr.Append(PR_APIVIEW_BOT_COMMENT).Append(Environment.NewLine).Append(Environment.NewLine); + foreach (var p in pullRequests) + { + var revisionLink = ManagerHelpers.ResolveReviewUrl(pullRequest: p, hostName: hostName); + bldr.Append('[').Append(p.PackageName).Append("](").Append(revisionLink).Append(')'); + bldr.Append(Environment.NewLine); + } + bldr.Append(Environment.NewLine); } - - pullRequestModel.Commits.Add(commitSha); - //Check if PR owner is part of Azure//Microsoft org in GitHub - await AssertPullRequestCreatorPermission(pullRequestModel); - - using var memoryStream = new MemoryStream(); - using var baselineStream = new MemoryStream(); - var codeFile = await _reviewManager.GetCodeFile(repoName, buildId, artifactName, packageName, originalFileName, codeFileName, memoryStream, baselineCodeFileName: baselineCodeFileName, baselineStream: baselineStream, project: project); - CodeFile baseLineCodeFile = null; - if (baselineStream.Length > 0) + else { - baselineStream.Position = 0; - baseLineCodeFile = await CodeFile.DeserializeAsync(baselineStream); + bldr.Append(PR_APIVIEW_BOT_NO_CHANGE_COMMENT); } - if (codeFile != null) + if (existingComment != null) { - await CreateRevisionIfRequired(codeFile, prNumber, originalFileName, memoryStream, pullRequestModel, baseLineCodeFile, baselineStream, baselineCodeFileName); + await _githubClient.Issue.Comment.Update(repoOwner, repoName, existingComment.Id, bldr.ToString()); } else { - _telemetryClient.TrackTrace("Failed to download artifact. Please recheck build id and artifact path values in API change detection request."); - } - - //Generate combined single comment to update on PR. - var prReviews = await _pullRequestsRepository.GetPullRequestsAsync(prNumber, repoName); - if (commentOnPR) - { - await CreateOrUpdateComment(prReviews, repoInfo[0], repoInfo[1], prNumber, hostName); + await _githubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, bldr.ToString()); } - - // Return review URL created for current package if exists - var review = prReviews.SingleOrDefault(r => r.PackageName == packageName && (r.Language == null || r.Language == language)); - return review == null ? "" : REVIEW_URL.Replace("{hostName}", hostName).Replace("{ReviewId}", review.ReviewId); - } public async Task CleanupPullRequestData() @@ -158,7 +128,7 @@ public async Task CleanupPullRequestData() if (await IsPullRequestEligibleForCleanup(prModel)) { _telemetryClient.TrackEvent("Closing review created for pull request " + prModel.PullRequestNumber); - await ClosePullRequestReview(prModel); + await ClosePullRequestAPIRevision(prModel); } } catch (Exception ex) @@ -178,67 +148,6 @@ public async Task CleanupPullRequestData() } } - public async Task> GetPullRequestsModel(string reviewId) { - return await _pullRequestsRepository.GetPullRequestsAsync(reviewId); - } - - public async Task> GetPullRequestsModel(int pullRequestNumber, string repoName) - { - return await _pullRequestsRepository.GetPullRequestsAsync(pullRequestNumber, repoName); - } - - private async Task CreateOrUpdateComment(List prReviews, string repoOwner, string repoName, int prNumber, string hostName) - { - var existingComment = await GetExistingCommentForPackage(repoOwner, repoName, prNumber); - var bldr = new StringBuilder(PR_APIVIEW_BOT_COMMENT_IDENTIFIER); - bldr.Append(Environment.NewLine).Append(Environment.NewLine); - if (prReviews.Count > 0) - { - bldr.Append(PR_APIVIEW_BOT_COMMENT).Append(Environment.NewLine).Append(Environment.NewLine); - foreach (var p in prReviews) - { - var reviewLink = REVIEW_URL.Replace("{hostName}", hostName).Replace("{ReviewId}", p.ReviewId); - bldr.Append('[').Append(p.PackageName).Append("](").Append(reviewLink).Append(')'); - bldr.Append(Environment.NewLine); - } - bldr.Append(Environment.NewLine); - } - else - { - bldr.Append(PR_APIVIEW_BOT_NO_CHANGE_COMMENT); - } - - if (existingComment != null) - { - await _githubClient.Issue.Comment.Update(repoOwner, repoName, existingComment.Id, bldr.ToString()); - } - else - { - await _githubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, bldr.ToString()); - } - } - - private async Task GetPullRequestModel(int prNumber, string repoName, string packageName, string originalFile, string language) - { - var pullRequestModel = await _pullRequestsRepository.GetPullRequestAsync(prNumber, repoName, packageName, language); - if (pullRequestModel == null) - { - var repoInfo = repoName.Split("/"); - var pullRequest = await _githubClient.PullRequest.Get(repoInfo[0], repoInfo[1], prNumber); - pullRequestModel = new PullRequestModel() - { - RepoName = repoName, - PullRequestNumber = prNumber, - FilePath = originalFile, - Author = pullRequest.User.Login, - PackageName = packageName, - Language = language, - Assignee = pullRequest.Assignee?.Login - }; - } - return pullRequestModel; - } - private async Task GetExistingCommentForPackage(string repoOwner, string repoName, int pr) { var comments = await _githubClient.Issue.Comment.GetAllForIssue(repoOwner, repoName, pr); @@ -251,190 +160,6 @@ private async Task GetExistingCommentForPackage(string repoOwner, return null; } - private async Task IsReviewSame(ReviewModel review, RenderedCodeFile renderedCodeFile) - { - foreach (var revision in review.Revisions.Reverse()) - { - if (await _reviewManager.IsReviewSame(revision, renderedCodeFile)) - { - return true; - } - } - return false; - } - - private async Task CreateBaselineRevision( - CodeFile baselineCodeFile, - MemoryStream baseLineStream, - PullRequestModel prModel, - string fileName) - { - var newRevision = new ReviewRevisionModel() - { - Author = prModel.Author, - Label = $"Baseline for PR {prModel.PullRequestNumber}" - }; - var reviewCodeFileModel = await _reviewManager.CreateReviewCodeFileModel(newRevision.RevisionId, baseLineStream, baselineCodeFile); - reviewCodeFileModel.FileName = fileName; - newRevision.Files.Add(reviewCodeFileModel); - return newRevision; - } - - private ReviewModel CreateNewReview(PullRequestModel prModel) - { - return new ReviewModel() - { - Author = prModel.Author, - CreationDate = DateTime.Now, - Name = prModel.PackageName, - IsClosed = false, - FilterType = ReviewType.PullRequest, - ReviewId = IdHelper.GenerateId() - }; - } - - - private async Task CreateRevisionIfRequired(CodeFile codeFile, - int prNumber, - string originalFileName, - MemoryStream memoryStream, - PullRequestModel pullRequestModel, - CodeFile baselineCodeFile, - MemoryStream baseLineStream, - string baselineFileName) - { - var newRevision = new ReviewRevisionModel() - { - Author = pullRequestModel.Author, - Label = $"Created for PR {prNumber}" - }; - - // Get automatically generated master review for package or previously cloned review for this pull request - var review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel); - if (review == null) - { - // If base line is not available (possible if package is new or request coming from SDK automation) - review = CreateNewReview(pullRequestModel); - // If request passes code file for baseline - if (baselineCodeFile != null) - { - var baseline = await CreateBaselineRevision(baselineCodeFile, baseLineStream, pullRequestModel, baselineFileName); - review.Revisions.Add(baseline); - } - } - else - { - // Check if API surface level matches with any revisions - var renderedCodeFile = new RenderedCodeFile(codeFile); - // pullRequestModel.ReviewId == null means: First time getting a request to check for API changes in the given package for a PR - if (pullRequestModel.ReviewId == null) - { - //No API changes detected from baseline - if (await IsReviewSame(review, renderedCodeFile)) - { - return; - } - } - // Below steps will remove last revision from previously created API review from the pull request and recreate new revision using latest token code file - if (pullRequestModel.ReviewId != null) - { - //Refresh baseline using latest from automatic review - var prevRevisionId = review.Revisions.Last().RevisionId; - review = await GetBaseLineReview(codeFile.Language, codeFile.PackageName, pullRequestModel, true); - review.ReviewId = pullRequestModel.ReviewId; - //Remove previous revisions with revision ID. - //Currently revision ID is getting duplicated when a PR api review is created for a brand new package. - //In case of brand new package, we don't have any baseline from automatic review. So it uses previous PR api review as baseline and - //below revision ID copy step makes duplicate revision IDs in such cases. - //We should ensure that no revision exists in review with previous revision ID before we update new revision - review.Revisions.RemoveAll(r => r.RevisionId == prevRevisionId); - newRevision.RevisionId = prevRevisionId; - } - } - - var reviewCodeFileModel = await _reviewManager.CreateReviewCodeFileModel(newRevision.RevisionId, memoryStream, codeFile); - reviewCodeFileModel.FileName = originalFileName; - newRevision.Files.Add(reviewCodeFileModel); - review.Revisions.Add(newRevision); - pullRequestModel.ReviewId = review.ReviewId; - review.FilterType = ReviewType.PullRequest; - await _reviewsRepository.UpsertReviewAsync(review); - if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") - { - await _reviewManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(review.ReviewId, newRevision); - } - await _pullRequestsRepository.UpsertPullRequestAsync(pullRequestModel); - } - - private async Task GetFormattedDiff(RenderedCodeFile renderedCodeFile, ReviewRevisionModel lastRevision, StringBuilder stringBuilder) - { - var autoReview = await _codeFileRepository.GetCodeFileAsync(lastRevision, false); - var autoReviewTextFile = autoReview.RenderText(false, skipDiff: true); - var prCodeTextFile = renderedCodeFile.RenderText(false, skipDiff: true); - var diffLines = InlineDiff.Compute(autoReviewTextFile, prCodeTextFile, autoReviewTextFile, prCodeTextFile); - if (diffLines == null || diffLines.Length == 0 || diffLines.Count(l => l.Kind != DiffLineKind.Unchanged) > 10) - { - return; - } - - stringBuilder.Append(Environment.NewLine).Append("**API changes**").Append(Environment.NewLine); - stringBuilder.Append("```diff").Append(Environment.NewLine); - foreach (var line in diffLines) - { - if (line.Kind == DiffLineKind.Added) - { - stringBuilder.Append("+ ").Append(line.Line.DisplayString).Append(Environment.NewLine); - } - else if (line.Kind == DiffLineKind.Removed) - { - stringBuilder.Append("- ").Append(line.Line.DisplayString).Append(Environment.NewLine); - } - } - stringBuilder.Append("```"); - } - - private async Task GetBaseLineReview(string Language, string packageName, PullRequestModel pullRequestModel, bool forceBaseline = false) - { - // Get previously cloned review for this pull request or automatically generated master review for package - ReviewModel review = null; - // Force baseline is passed when we need to refresh revision 0 with API revision from main branch(Automatic review revision) - // If API review is not created for PR then also fetch review from main branch. - if (forceBaseline || pullRequestModel.ReviewId == null) - { - var autoReview = await _reviewsRepository.GetMasterReviewForPackageAsync(Language, packageName); - if (autoReview != null) - { - review = CloneReview(autoReview); - review.Author = pullRequestModel.Author; - } - } - - // If either automatic baseline is not available or if review is already created for PR then return this review to create new revision. - if (review == null && pullRequestModel.ReviewId != null) - { - review = await _reviewsRepository.GetReviewAsync(pullRequestModel.ReviewId); - } - - return review; - } - - private ReviewModel CloneReview(ReviewModel review) - { - var baseRevision = review.Revisions.Last(); - var reviewCopy = new ReviewModel() - { - Author = review.Author, - CreationDate = baseRevision.CreationDate, - Name = review.Name, - IsAutomatic = false, - IsClosed = false, - RunAnalysis = review.RunAnalysis, - ReviewId = IdHelper.GenerateId() - }; - reviewCopy.Revisions.Add(baseRevision); - return reviewCopy; - } - private async Task IsPullRequestEligibleForCleanup(PullRequestModel prModel) { if (!_isGitClientAvailable) @@ -450,34 +175,19 @@ private async Task IsPullRequestEligibleForCleanup(PullRequestModel prMode return false; } - private async Task ClosePullRequestReview(PullRequestModel pullRequestModel) + private async Task ClosePullRequestAPIRevision(PullRequestModel pullRequestModel) { - if (pullRequestModel.ReviewId != null) + if (!String.IsNullOrEmpty(pullRequestModel.APIRevisionId)) { - var review = await _reviewsRepository.GetReviewAsync(pullRequestModel.ReviewId); - if (review != null) + var apiRevision = await _apiRevisionsRepository.GetAPIRevisionAsync(pullRequestModel.APIRevisionId); + if (apiRevision != null) { - review.IsClosed = true; - await _reviewsRepository.UpsertReviewAsync(review); + await _apiRevisionManager.SoftDeleteAPIRevisionAsync(userName: "azure-sdk", apiRevision: apiRevision, notes: "Deleted by PullRequest CleanUp Automation"); } } pullRequestModel.IsOpen = false; await _pullRequestsRepository.UpsertPullRequestAsync(pullRequestModel); } - - private async Task AssertPullRequestCreatorPermission(PullRequestModel prModel) - { - // White list bot accounts to create API reviews from PR automatically - if (!_allowedListBotAccounts.Contains(prModel.Author)) - { - var isAuthorized = await _openSourceManager.IsAuthorizedUser(prModel.Author); - if (!isAuthorized) - { - _telemetryClient.TrackTrace($"API change detection permission failed for user {prModel.Author}. API review is only created if PR author is an internal user."); - throw new AuthorizationFailedException(); - } - } - } } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/ReviewManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/ReviewManager.cs index cee40302fdc..6472d1dd523 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/ReviewManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/ReviewManager.cs @@ -3,27 +3,25 @@ using System; using System.Collections.Generic; -using System.IO; -using System.IO.Compression; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using ApiView; -using APIView.DIff; -using APIView.Model; using APIViewWeb.Hubs; using APIViewWeb.Models; using APIViewWeb.Repositories; using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Configuration; using System.Net.Http; using System.Text.Json; using System.Data; +using APIViewWeb.LeanModels; +using APIViewWeb.Helpers; +using APIViewWeb.Managers.Interfaces; +using Microsoft.ApplicationInsights.DataContracts; +using System.IO; namespace APIViewWeb.Managers { @@ -32,81 +30,86 @@ public class ReviewManager : IReviewManager private readonly IAuthorizationService _authorizationService; private readonly ICosmosReviewRepository _reviewsRepository; + private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly ICommentsManager _commentManager; private readonly IBlobCodeFileRepository _codeFileRepository; - private readonly IBlobOriginalsRepository _originalsRepository; private readonly ICosmosCommentsRepository _commentsRepository; - private readonly IEnumerable _languageServices; - private readonly INotificationManager _notificationManager; - private readonly IDevopsArtifactRepository _devopsArtifactRepository; - private readonly IPackageNameManager _packageNameManager; private readonly IHubContext _signalRHubContext; + private readonly IEnumerable _languageServices; static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); public ReviewManager ( IAuthorizationService authorizationService, ICosmosReviewRepository reviewsRepository, - IBlobCodeFileRepository codeFileRepository, IBlobOriginalsRepository originalsRepository, - ICosmosCommentsRepository commentsRepository, IEnumerable languageServices, - INotificationManager notificationManager, IDevopsArtifactRepository devopsClient, - IPackageNameManager packageNameManager, IHubContext signalRHubContext) + IAPIRevisionsManager apiRevisionsManager, ICommentsManager commentManager, + IBlobCodeFileRepository codeFileRepository, ICosmosCommentsRepository commentsRepository, + IHubContext signalRHubContext, IEnumerable languageServices) { _authorizationService = authorizationService; _reviewsRepository = reviewsRepository; + _apiRevisionsManager = apiRevisionsManager; + _commentManager = commentManager; _codeFileRepository = codeFileRepository; - _originalsRepository = originalsRepository; _commentsRepository = commentsRepository; - _languageServices = languageServices; - _notificationManager = notificationManager; - _devopsArtifactRepository = devopsClient; - _packageNameManager = packageNameManager; _signalRHubContext = signalRHubContext; + _languageServices = languageServices; } - public async Task CreateReviewAsync(ClaimsPrincipal user, string originalName, string label, Stream fileStream, bool runAnalysis, string langauge, bool awaitComputeDiff = false) - { - var review = new ReviewModel - { - Author = user.GetGitHubLogin(), - CreationDate = DateTime.UtcNow, - RunAnalysis = runAnalysis, - Name = fileStream != null? originalName : Path.GetFileName(originalName), - FilterType = ReviewType.Manual - }; - await AddRevisionAsync(user, review, originalName, label, fileStream, langauge, awaitComputeDiff); - return review; - } - - public Task> GetReviewsAsync(bool closed, string language, string packageName = null, ReviewType filterType = ReviewType.Manual) - { - return _reviewsRepository.GetReviewsAsync(closed, language, packageName: packageName, filterType: filterType); - } - - public async Task> GetReviewsAsync(string ServiceName, string PackageName, IEnumerable filterTypes) + /// + /// Get all Reviews for a language + /// + /// + /// + /// + public Task> GetReviewsAsync(string language, bool? isClosed = false) { - return await _reviewsRepository.GetReviewsAsync(ServiceName, PackageName, filterTypes); + return _reviewsRepository.GetReviewsAsync(language, isClosed); } - public async Task> GetReviewPropertiesAsync(string propertyName) + /// + /// Get Reviews using language and package name + /// + /// + /// + /// + /// + public Task GetReviewAsync(string language, string packageName, bool? isClosed = false) { - return await _reviewsRepository.GetReviewFirstLevelPropertiesAsync(propertyName); + return _reviewsRepository.GetReviewAsync(language, packageName, isClosed); } - public async Task> GetRequestedReviews(string userName) + /// + /// Get Reviews that have been assigned for review to a user + /// + /// + /// + public async Task> GetReviewsAssignedToUser(string userName) { - return await _reviewsRepository.GetRequestedReviews(userName); + return await _reviewsRepository.GetReviewsAssignedToUser(userName); } - public async Task<(IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage)> GetPagedReviewsAsync( - IEnumerable search, IEnumerable languages, bool? isClosed, IEnumerable filterTypes, bool? isApproved, int offset, int limit, string orderBy) - { - var result = await _reviewsRepository.GetReviewsAsync(search, languages, isClosed, filterTypes, isApproved, offset, limit, orderBy); + /// + /// Get List of Reviews for the Review Page + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task<(IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage)> GetPagedReviewListAsync( + IEnumerable search, IEnumerable languages, bool? isClosed, bool? isApproved, int offset, int limit, string orderBy) + { + var result = await _reviewsRepository.GetReviewsAsync(search: search, languages: languages, isClosed: isClosed, isApproved: isApproved, offset: offset, limit: limit, orderBy: orderBy); // Calculate and add Previous and Next and Current page to the returned result var totalPages = (int)Math.Ceiling(result.TotalCount / (double)limit); var currentPage = offset == 0 ? 1 : offset / limit + 1; - (IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage) resultToReturn = ( + (IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage) resultToReturn = ( result.Reviews, result.TotalCount, TotalPages: totalPages, CurrentPage: currentPage, PreviousPage: currentPage == 1 ? null : currentPage - 1, @@ -115,31 +118,14 @@ public async Task> GetRequestedReviews(string userName) return resultToReturn; } - public async Task DeleteReviewAsync(ClaimsPrincipal user, string id) - { - var reviewModel = await _reviewsRepository.GetReviewAsync(id); - await AssertReviewOwnerAsync(user, reviewModel); - AssertReviewDeletion(reviewModel); - - await _reviewsRepository.DeleteReviewAsync(reviewModel); - - foreach (var revision in reviewModel.Revisions) - { - foreach (var file in revision.Files) - { - if (file.HasOriginal) - { - await _originalsRepository.DeleteOriginalAsync(file.ReviewFileId); - } - - await _codeFileRepository.DeleteCodeFileAsync(revision.RevisionId, file.ReviewFileId); - } - } - - await _commentsRepository.DeleteCommentsAsync(id); - } - - public async Task GetReviewAsync(ClaimsPrincipal user, string id) + /// + /// Get Reviews + /// + /// + /// + /// + /// + public async Task GetReviewAsync(ClaimsPrincipal user, string id) { if (user == null) { @@ -147,539 +133,163 @@ public async Task GetReviewAsync(ClaimsPrincipal user, string id) } var review = await _reviewsRepository.GetReviewAsync(id); - review.UpdateAvailable = IsUpdateAvailable(review); - - // Handle old model -#pragma warning disable CS0618 // Type or member is obsolete - if (review.Revisions.Count == 0 && review.Files.Count == 1) - { - var file = review.Files[0]; -#pragma warning restore CS0618 // Type or member is obsolete - review.Revisions.Add(new ReviewRevisionModel() - { - RevisionId = file.ReviewFileId, - CreationDate = file.CreationDate, - Files = - { - file - } - }); - } - - if (review.PackageName != null && review.PackageDisplayName == null) - { - var p = await _packageNameManager.GetPackageDetails(review.PackageName); - review.PackageDisplayName = p?.DisplayName; - review.ServiceName = p?.ServiceName; - } - - //If current review doesn't have package approved for first release status then check if package is approved for any reviews for the same package. - if (!review.IsApprovedForFirstRelease) - { - var reviews = await _reviewsRepository.GetApprovedForFirstReleaseReviews(review.Language, review.PackageName); - if (reviews.Any()) - { - var nameApprovedReview = reviews.First(); - review.ApprovedForFirstReleaseBy = nameApprovedReview.ApprovedForFirstReleaseBy; - review.ApprovalDate = nameApprovedReview.ApprovedForFirstReleaseOn; - review.IsApprovedForFirstRelease = true; - } - else - { - // Mark package as approved if review is already approved. Copy approval details from review approval. - reviews = await _reviewsRepository.GetApprovedReviews(review.Language, review.PackageName); - if (reviews.Any()) - { - var approvedRevision = reviews.First(r => r.Revisions.Any(rev => rev.IsApproved)).Revisions.First(rev => rev.IsApproved); - review.ApprovedForFirstReleaseBy = approvedRevision.Approvers.FirstOrDefault(); - review.ApprovedForFirstReleaseOn = reviews.First().ApprovalDate; - review.IsApprovedForFirstRelease = true; - } - } - } return review; } - public async Task AddRevisionAsync( - ClaimsPrincipal user, - string reviewId, - string name, - string label, - Stream fileStream, - string language = "", - bool awaitComputeDiff = false) - { - var review = await GetReviewAsync(user, reviewId); - await AssertAutomaticReviewModifier(user, review); - await AddRevisionAsync(user, review, name, label, fileStream, language, awaitComputeDiff); - } - - public async Task CreateCodeFile( - string originalName, - Stream fileStream, - bool runAnalysis, - MemoryStream memoryStream, - string language = null) - { - var languageService = _languageServices.FirstOrDefault(s => (language != null ? s.Name == language : s.IsSupportedFile(originalName))); - if (fileStream != null) - { - await fileStream.CopyToAsync(memoryStream); - memoryStream.Position = 0; - } - CodeFile codeFile = null; - if (languageService.IsReviewGenByPipeline) - { - codeFile = languageService.GetReviewGenPendingCodeFile(originalName); - } - else - { - codeFile = await languageService.GetCodeFileAsync( - originalName, - memoryStream, - runAnalysis); - } - return codeFile; - } - - public async Task CreateReviewCodeFileModel(string revisionId, MemoryStream memoryStream, CodeFile codeFile) + /// + /// Get Legacy Reviews from old database + /// + /// + /// + /// + /// + public async Task GetLegacyReviewAsync(ClaimsPrincipal user, string id) { - var reviewCodeFileModel = new ReviewCodeFileModel - { - HasOriginal = true, - }; - - InitializeFromCodeFile(reviewCodeFileModel, codeFile); - if (memoryStream != null) + if (user == null) { - memoryStream.Position = 0; - await _originalsRepository.UploadOriginalAsync(reviewCodeFileModel.ReviewFileId, memoryStream); + throw new UnauthorizedAccessException(); } - await _codeFileRepository.UpsertCodeFileAsync(revisionId, reviewCodeFileModel.ReviewFileId, codeFile); - return reviewCodeFileModel; - } - - public async Task DeleteRevisionAsync(ClaimsPrincipal user, string id, string revisionId) - { - var review = await GetReviewAsync(user, id); - AssertReviewDeletion(review); - var revision = review.Revisions.Single(r => r.RevisionId == revisionId); - await AssertRevisionOwner(user, revision); - if (review.Revisions.Count < 2) - { - return; - } - review.Revisions.Remove(revision); - await _reviewsRepository.UpsertReviewAsync(review); + var review = await _reviewsRepository.GetLegacyReviewAsync(id); + return review; } - public async Task UpdateRevisionLabelAsync(ClaimsPrincipal user, string id, string revisionId, string label) - { - var review = await GetReviewAsync(user, id); - var revision = review.Revisions.Single(r => r.RevisionId == revisionId); - await AssertRevisionOwner(user, revision); - revision.Label = label; - await _reviewsRepository.UpsertReviewAsync(review); - } - public async Task ToggleIsClosedAsync(ClaimsPrincipal user, string id) + /// + /// Create Reviews + /// + /// + /// + /// + /// + public async Task CreateReviewAsync(string packageName, string language, bool isClosed=true) { - var review = await GetReviewAsync(user, id); - review.IsClosed = !review.IsClosed; - if (review.FilterType == ReviewType.Automatic) + if (string.IsNullOrEmpty(packageName) || string.IsNullOrEmpty(language)) { - throw new AuthorizationFailedException(); + throw new ArgumentException("Package Name and Language are required"); } - await _reviewsRepository.UpsertReviewAsync(review); - } - public async Task ToggleApprovalAsync(ClaimsPrincipal user, string id, string revisionId) - { - var review = await GetReviewAsync(user, id); - var revision = review.Revisions.Single(r => r.RevisionId == revisionId); - await AssertApprover(user, revision); - var userId = user.GetGitHubLogin(); - bool approvalStatus; - if (revision.Approvers.Contains(userId)) - { - //Revert approval - revision.Approvers.Remove(userId); - approvalStatus = false; - } - else + ReviewListItemModel review = new ReviewListItemModel() { - //Approve revision - revision.Approvers.Add(userId); - review.ApprovalDate = DateTime.Now; - approvalStatus = true; - } - await _signalRHubContext.Clients.Group(user.GetGitHubLogin()).SendAsync("ReceiveApprovalSelf", review.ReviewId, revisionId, approvalStatus); - await _signalRHubContext.Clients.All.SendAsync("ReceiveApproval", review.ReviewId, revisionId, userId, approvalStatus); - await _reviewsRepository.UpsertReviewAsync(review); - } - - public async Task ApprovePackageNameAsync(ClaimsPrincipal user, string id) - { - var review = await GetReviewAsync(user, id); - await AssertApprover(user, review.Revisions.Last()); - review.ApprovedForFirstReleaseBy = user.GetGitHubLogin(); - review.ApprovedForFirstReleaseOn = DateTime.Now; - review.IsApprovedForFirstRelease = true; - await _reviewsRepository.UpsertReviewAsync(review); - } - - public async Task IsReviewSame(ReviewRevisionModel revision, RenderedCodeFile renderedCodeFile) - { - //This will compare and check if new code file content is same as revision in parameter - var lastRevisionFile = await _codeFileRepository.GetCodeFileAsync(revision, false); - var lastRevisionTextLines = lastRevisionFile.RenderText(false, skipDiff: true); - var fileTextLines = renderedCodeFile.RenderText(false, skipDiff: true); - return lastRevisionTextLines.SequenceEqual(fileTextLines); - } - - public async Task CreateMasterReviewAsync(ClaimsPrincipal user, string originalName, string label, Stream fileStream, bool compareAllRevisions) - { - //Generate code file from new uploaded package - using var memoryStream = new MemoryStream(); - var codeFile = await CreateCodeFile(originalName, fileStream, false, memoryStream); - return await CreateMasterReviewAsync(user, codeFile, originalName, label, memoryStream, compareAllRevisions); - } - - public async Task UpdateReviewBackground(HashSet updateDisabledLanguages, int backgroundBatchProcessCount) - { - foreach(var language in LanguageService.SupportedLanguages) - { - if (updateDisabledLanguages.Contains(language)) - { - _telemetryClient.TrackTrace("Background task to update API review at startup is disabled for language " + language); - continue; - } - - var languageService = GetLanguageService(language); - if (languageService == null) - return; - - // If review is updated using Azure DevOps pipeline then batch process update review requests - if (languageService.IsReviewGenByPipeline) + PackageName = packageName, + Language = language, + CreatedOn = DateTime.UtcNow, + CreatedBy = "azure-sdk", + IsClosed = isClosed, + ChangeHistory = new List() { - await UpdateReviewsUsingPipeline(language, languageService, backgroundBatchProcessCount); - } - else - { - var reviews = await _reviewsRepository.GetReviewsAsync(false, language, fetchAllPages: true); - foreach (var review in reviews.Where(r => IsUpdateAvailable(r) || r.Revisions.Any(rev => string.IsNullOrEmpty(rev.SingleFile?.PackageVersion)))) - { - var requestTelemetry = new RequestTelemetry { Name = "Updating Review " + review.ReviewId }; - var operation = _telemetryClient.StartOperation(requestTelemetry); - try - { - await Task.Delay(500); - await UpdateReviewAsync(review, languageService); - } - catch (Exception e) - { - _telemetryClient.TrackException(e); - } - finally - { - _telemetryClient.StopOperation(operation); - } - } - } - } - } - - // Languages that full ysupport sandboxing updates reviews using Azure devops pipeline - // We should batch all eligible reviews to avoid a pipeline run storm - private async Task UpdateReviewsUsingPipeline(string language, LanguageService languageService, int backgroundBatchProcessCount) - { - var reviews = await _reviewsRepository.GetReviewsAsync(false, language, fetchAllPages: true); - var paramList = new List(); - - foreach(var review in reviews) - { - foreach (var revision in review.Revisions.Reverse()) - { - foreach (var file in revision.Files) + new ReviewChangeHistoryModel() { - //Don't include current revision if file is not required to be updated. - // E.g. json token file is uploaded for a language, specific revision was already upgraded. - if (!file.HasOriginal || file.FileName == null || !languageService.IsSupportedFile(file.FileName) || (!languageService.CanUpdate(file.VersionString) && !string.IsNullOrEmpty(file.PackageVersion))) - { - continue; - } - - _telemetryClient.TrackTrace($"Updating review: {review.ReviewId}, revision: {revision.RevisionId}"); - paramList.Add(new ReviewGenPipelineParamModel() - { - FileID = file.ReviewFileId, - ReviewID = review.ReviewId, - RevisionID = revision.RevisionId, - FileName = Path.GetFileName(file.FileName) - }); + ChangeAction = ReviewChangeAction.Created, + ChangedBy = "azure-sdk", + ChangedOn = DateTime.UtcNow } } + }; - // This should be changed to configurable batch count - if (paramList.Count >= backgroundBatchProcessCount) - { - _telemetryClient.TrackTrace($"Running pipeline to update reviews for {language} with batch size {paramList.Count}"); - await RunReviewGenPipeline(paramList, languageService.Name); - // Delay of 10 minute before starting next batch - // We should try to increase the number of revisions in the batch than number of runs. - await Task.Delay(600000); - paramList.Clear(); - } - } - - if (paramList.Count > 0) - { - _telemetryClient.TrackTrace($"Running pipeline to update reviews for {language} with batch size {paramList.Count}"); - await RunReviewGenPipeline(paramList, languageService.Name); - } + await _reviewsRepository.UpsertReviewAsync(review); + return review; } - public async Task GetCodeFile(string repoName, - string buildId, - string artifactName, - string packageName, - string originalFileName, - string codeFileName, - MemoryStream originalFileStream, - string baselineCodeFileName = "", - MemoryStream baselineStream = null, - string project = "public" - ) + /// + /// SoftDeleteReviewAsync + /// + /// + /// + /// + public async Task SoftDeleteReviewAsync(ClaimsPrincipal user, string id) { - Stream stream = null; - CodeFile codeFile = null; - if (string.IsNullOrEmpty(codeFileName)) - { - // backward compatibility until all languages moved to sandboxing of codefile to pipeline - stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifactName, originalFileName, format: "file", project: project); - codeFile = await CreateCodeFile(Path.GetFileName(originalFileName), stream, false, originalFileStream); - } - else - { - stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifactName, packageName, format: "zip", project: project); - var archive = new ZipArchive(stream); - foreach (var entry in archive.Entries) - { - var fileName = Path.GetFileName(entry.Name); - if (fileName == originalFileName) - { - await entry.Open().CopyToAsync(originalFileStream); - } - - if (fileName == codeFileName) - { - codeFile = await CodeFile.DeserializeAsync(entry.Open()); - } - else if (fileName == baselineCodeFileName) - { - await entry.Open().CopyToAsync(baselineStream); - } - } - } - - return codeFile; - } + var review = await _reviewsRepository.GetReviewAsync(id); + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(id); + await ManagerHelpers.AssertReviewOwnerAsync(user, review, _authorizationService); - public async Task CreateApiReview( - ClaimsPrincipal user, - string buildId, - string artifactName, - string originalFileName, - string label, - string repoName, - string packageName, - string codeFileName, - bool compareAllRevisions, - string project - ) - { - using var memoryStream = new MemoryStream(); - var codeFile = await GetCodeFile(repoName, buildId, artifactName, packageName, originalFileName, codeFileName, memoryStream, project: project); - return await CreateMasterReviewAsync(user, codeFile, originalFileName, label, memoryStream, compareAllRevisions); - } + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(review.ChangeHistory, ReviewChangeAction.Deleted, user.GetGitHubLogin()); + review.ChangeHistory = changeUpdate.ChangeHistory; + review.IsDeleted = changeUpdate.ChangeStatus; + await _reviewsRepository.UpsertReviewAsync(review); - public async Task AutoArchiveReviews(int archiveAfterMonths) - { - var reviews = await _reviewsRepository.GetReviewsAsync(false, "All", filterType: ReviewType.Manual, fetchAllPages: true); - // Find all inactive reviews - reviews = reviews.Where(r => r.LastUpdated.AddMonths(archiveAfterMonths) < DateTime.Now); - foreach (var review in reviews) + foreach (var revision in revisions) { - var requestTelemetry = new RequestTelemetry { Name = "Archiving Review " + review.ReviewId }; - var operation = _telemetryClient.StartOperation(requestTelemetry); - try - { - review.IsClosed = true; - await _reviewsRepository.UpsertReviewAsync(review); - await Task.Delay(500); - } - catch (Exception e) - { - _telemetryClient.TrackException(e); - } - finally - { - _telemetryClient.StopOperation(operation); - } + await _apiRevisionsManager.SoftDeleteAPIRevisionAsync(user, revision); } + await _commentManager.SoftDeleteCommentsAsync(user, review.Id); } - public async Task UpdateReviewCodeFiles(string repoName, string buildId, string artifact, string project) + /// + /// Toggle Review Open/Closed state + /// + /// + /// + /// + public async Task ToggleReviewIsClosedAsync(ClaimsPrincipal user, string id) { - var stream = await _devopsArtifactRepository.DownloadPackageArtifact(repoName, buildId, artifact, filePath: null, project: project, format: "zip"); - var archive = new ZipArchive(stream); - foreach (var entry in archive.Entries) - { - var reviewFilePath = entry.FullName; - var reviewDetails = reviewFilePath.Split("/"); - - if (reviewDetails.Length < 4 || !reviewFilePath.EndsWith(".json")) - continue; - - var reviewId = reviewDetails[1]; - var revisionId = reviewDetails[2]; - var codeFile = await CodeFile.DeserializeAsync(entry.Open()); - - // Update code file with one downloaded from pipeline - var review = await _reviewsRepository.GetReviewAsync(reviewId); - if (review != null) - { - var revision = review.Revisions.SingleOrDefault(review => review.RevisionId == revisionId); - if (revision != null) - { - await _codeFileRepository.UpsertCodeFileAsync(revisionId, revision.SingleFile.ReviewFileId, codeFile); - var file = revision.Files.FirstOrDefault(); - file.VersionString = codeFile.VersionString; - file.PackageName = codeFile.PackageName; - file.PackageVersion = codeFile.PackageVersion; - await _reviewsRepository.UpsertReviewAsync(review); - - if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") - { - // Trigger diff calculation using updated code file from sandboxing pipeline - await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.ReviewId, revision); - } - } - } - } + var review = await _reviewsRepository.GetReviewAsync(id); + var userId = user.GetGitHubLogin(); + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Closed, userId); + review.ChangeHistory = changeUpdate.ChangeHistory; + review.IsClosed = changeUpdate.ChangeStatus; + await _reviewsRepository.UpsertReviewAsync(review); } - public async Task RequestApproversAsync(ClaimsPrincipal User, string ReviewId, HashSet reviewers) - { - var review = await GetReviewAsync(User, ReviewId); - review.RequestedReviewers = reviewers; - review.RequestedBy = User.GetGitHubLogin(); - review.ApprovalRequestedOn = DateTime.Now; - await _reviewsRepository.UpsertReviewAsync(review); + /// + /// Add new Approval or ApprovalReverted action to the ChangeHistory of a Review. Serves as firstRelease approval + /// + /// + /// + /// + /// + /// + public async Task ToggleReviewApprovalAsync(ClaimsPrincipal user, string id, string revisionId, string notes="") + { + ReviewListItemModel review = await _reviewsRepository.GetReviewAsync(id); + var userId = user.GetGitHubLogin(); + await ToggleReviewApproval(user, review, notes); + await _signalRHubContext.Clients.Group(userId).SendAsync("ReceiveApprovalSelf", id, revisionId, review.IsApproved); + await _signalRHubContext.Clients.All.SendAsync("ReceiveApproval", id, revisionId, userId, review.IsApproved); } /// - /// Get the LineNumbers of the Heading that have diff changes in their sections + /// ApproveReviewAsync /// - public async Task GetLineNumbersOfHeadingsOfSectionsWithDiff(string reviewId, ReviewRevisionModel revision) + /// + /// + /// + /// + public async Task ApproveReviewAsync(ClaimsPrincipal user, string reviewId, string notes = "") { - var review = await _reviewsRepository.GetReviewAsync(reviewId); - var latestRevisionCodeFile = await _codeFileRepository.GetCodeFileAsync(revision, false); - var latestRevisionHtmlLines = latestRevisionCodeFile.Render(false); - var latestRevisionTextLines = latestRevisionCodeFile.RenderText(false); - - foreach (var rev in review.Revisions) + ReviewListItemModel review = await _reviewsRepository.GetReviewAsync(reviewId); + if (review.IsApproved) { - // Calculate diff against previous revisions only. APIView only shows diff against revision lower than current one. - if (rev.RevisionId != revision.RevisionId && rev.RevisionNumber < revision.RevisionNumber) - { - var lineNumbersForHeadingOfSectionWithDiff = new HashSet(); - var earlierRevisionCodeFile = await _codeFileRepository.GetCodeFileAsync(rev, false); - var earlierRevisionHtmlLines = earlierRevisionCodeFile.RenderReadOnly(false); - var earlierRevisionTextLines = earlierRevisionCodeFile.RenderText(false); - - var diffLines = InlineDiff.Compute(earlierRevisionTextLines, latestRevisionTextLines, earlierRevisionHtmlLines, latestRevisionHtmlLines); - - foreach (var diffLine in diffLines) - { - if (diffLine.Kind == DiffLineKind.Unchanged && diffLine.Line.SectionKey != null && diffLine.OtherLine.SectionKey != null) - { - var latestRevisionRootNode = latestRevisionCodeFile.GetCodeLineSectionRoot((int)diffLine.Line.SectionKey); - var earlierRevisionRootNode = earlierRevisionCodeFile.GetCodeLineSectionRoot((int)diffLine.OtherLine.SectionKey); - var diffSectionRoot = ComputeSectionDiff(earlierRevisionRootNode, latestRevisionRootNode, earlierRevisionCodeFile, latestRevisionCodeFile); - if (latestRevisionCodeFile.ChildNodeHasDiff(diffSectionRoot)) - lineNumbersForHeadingOfSectionWithDiff.Add((int)diffLine.Line.LineNumber); - } - } - if (rev.HeadingsOfSectionsWithDiff.ContainsKey(revision.RevisionId)) - { - rev.HeadingsOfSectionsWithDiff.Remove(revision.RevisionId); - } - rev.HeadingsOfSectionsWithDiff.Add(revision.RevisionId, lineNumbersForHeadingOfSectionWithDiff); - } + return; } - await _reviewsRepository.UpsertReviewAsync(review); + await ToggleReviewApproval(user, review, notes); } /// - /// Computes diff for each level of the tree hierachy of a section + /// Assign reviewers to a review /// - public TreeNode> ComputeSectionDiff(TreeNode before, TreeNode after, RenderedCodeFile beforeFile, RenderedCodeFile afterFile) + /// + /// + /// + /// + public async Task AssignReviewersToReviewAsync(ClaimsPrincipal User, string reviewId, HashSet reviewers) { - var rootDiff = new InlineDiffLine(before.Data, after.Data, DiffLineKind.Unchanged); - var resultRoot = new TreeNode>(rootDiff); - - var - queue = new Queue<(TreeNode before, TreeNode after, TreeNode> current)>(); - - queue.Enqueue((before, after, resultRoot)); - - while (queue.Count > 0) + ReviewListItemModel review = await _reviewsRepository.GetReviewAsync(reviewId); + foreach (var reviewer in reviewers) { - var nodesInProcess = queue.Dequeue(); - var (beforeHTMLLines, beforeTextLines) = GetCodeLinesForDiff(nodesInProcess.before, nodesInProcess.current, beforeFile); - var (afterHTMLLines, afterTextLines) = GetCodeLinesForDiff(nodesInProcess.after, nodesInProcess.current, afterFile); - - var diffResult = InlineDiff.Compute(beforeTextLines, afterTextLines, beforeHTMLLines, afterHTMLLines); - - if (diffResult.Count() == 2 && - diffResult[0]!.Line.NodeRef != null && diffResult[1]!.Line.NodeRef != null && - diffResult[0]!.Line.NodeRef.IsLeaf && diffResult[1]!.Line.NodeRef.IsLeaf) // Detached Leaf Parents which are Eventually Discarded - { - var inlineDiffLine = new InlineDiffLine(diffResult[1].Line, diffResult[0].Line, DiffLineKind.Unchanged); - diffResult = new InlineDiffLine[] { inlineDiffLine }; - } - - foreach (var diff in diffResult) + if (!review.AssignedReviewers.Where(x => x.AssingedTo == reviewer).Any()) { - var addedChild = nodesInProcess.current.AddChild(diff); - - switch (diff.Kind) + var reviewAssignment = new ReviewAssignmentModel() { - case DiffLineKind.Removed: - queue.Enqueue((diff.Line.NodeRef, null, addedChild)); - break; - case DiffLineKind.Added: - queue.Enqueue((null, diff.Line.NodeRef, addedChild)); - break; - case DiffLineKind.Unchanged: - queue.Enqueue((diff.OtherLine.NodeRef, diff.Line.NodeRef, addedChild)); - break; - } + AssingedTo = reviewer, + AssignedBy = User.GetGitHubLogin(), + AssingedOn = DateTime.Now, + }; + review.AssignedReviewers.Add(reviewAssignment); } } - return resultRoot; - } - - public async Task IsApprovedForFirstRelease(string language, string packageName) - { - var reviews = await _reviewsRepository.GetApprovedForFirstReleaseReviews(language, packageName); - if (!reviews.Any()) - { - reviews = await _reviewsRepository.GetApprovedReviews(language, packageName); - } - return reviews.Any(); + await _reviewsRepository.UpsertReviewAsync(review); } /// @@ -687,8 +297,8 @@ public async Task IsApprovedForFirstRelease(string language, string packag /// public async Task GenerateAIReview(string reviewId, string revisionId) { - var review = await _reviewsRepository.GetReviewAsync(reviewId); - var revision = review.Revisions.Where(r => r.RevisionId == revisionId).FirstOrDefault(); + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId); + var revision = revisions.Where(r => r.Id == revisionId).FirstOrDefault(); var codeFile = await _codeFileRepository.GetCodeFileAsync(revision, false); var codeLines = codeFile.RenderText(false); @@ -723,10 +333,10 @@ public async Task GenerateAIReview(string reviewId, string revisionId) foreach (var violation in result.Violations) { var codeLine = codeLines[violation.LineNo]; - var comment = new CommentModel(); - comment.TimeStamp = DateTime.UtcNow; + var comment = new CommentItemModel(); + comment.CreatedOn = DateTime.UtcNow; comment.ReviewId = reviewId; - comment.RevisionId = revisionId; + comment.APIRevisionId = revisionId; comment.ElementId = codeLine.ElementId; //comment.SectionClass = sectionClass; // This will be needed for swagger @@ -739,375 +349,140 @@ public async Task GenerateAIReview(string reviewId, string revisionId) commentText.AppendLine($"See: https://guidelinescollab.github.io/azure-sdk/{id}"); } comment.ResolutionLocked = false; - comment.Username = "azure-sdk"; - comment.Comment = commentText.ToString(); + comment.CreatedBy = "azure-sdk"; + comment.CommentText = commentText.ToString(); await _commentsRepository.UpsertCommentAsync(comment); } return result.Violations.Count; } - - private async Task UpdateReviewAsync(ReviewModel review, LanguageService languageService) + /// + /// Logic to update Reviews in a blackground task + /// + /// + /// + /// + public async Task UpdateReviewsInBackground(HashSet updateDisabledLanguages, int backgroundBatchProcessCount) { - foreach (var revision in review.Revisions.Reverse()) + foreach (var language in LanguageService.SupportedLanguages) { - foreach (var file in revision.Files) + if (updateDisabledLanguages.Contains(language)) { - if (!file.HasOriginal || (!languageService.CanUpdate(file.VersionString) && !string.IsNullOrEmpty(file.PackageVersion))) - { - continue; - } - - try - { - var fileOriginal = await _originalsRepository.GetOriginalAsync(file.ReviewFileId); - // file.Name property has been repurposed to store package name and version string - // This is causing issue when updating review using latest parser since it expects Name field as file name - // We have added a new property FileName which is only set for new reviews - // All older reviews needs to be handled by checking review name field - var fileName = file.FileName ?? (Path.HasExtension(review.Name) ? review.Name : file.Name); - var codeFile = await languageService.GetCodeFileAsync(fileName, fileOriginal, review.RunAnalysis); - await _codeFileRepository.UpsertCodeFileAsync(revision.RevisionId, file.ReviewFileId, codeFile); - // update only version string - file.VersionString = codeFile.VersionString; - file.PackageName = codeFile.PackageName; - file.PackageVersion = codeFile.PackageVersion; - await _reviewsRepository.UpsertReviewAsync(review); - } - catch (Exception ex) - { - _telemetryClient.TrackTrace("Failed to update review " + review.ReviewId); - _telemetryClient.TrackException(ex); - } + _telemetryClient.TrackTrace("Background task to update API review at startup is disabled for langauge " + language); + continue; } - } - } - - private async Task AddRevisionAsync( - ClaimsPrincipal user, - ReviewModel review, - string name, - string label, - Stream fileStream, - string language, - bool awaitComputeDiff = false) - { - var revision = new ReviewRevisionModel(); - - var codeFile = await CreateFileAsync( - revision.RevisionId, - name, - fileStream, - review.RunAnalysis, - language); - - // Set revision name using file name in case input is a URL(Input is either fielstream or URL) - if (fileStream == null) - { - revision.Name = Path.GetFileName(name); - } - revision.Files.Add(codeFile); - revision.Author = user.GetGitHubLogin(); - revision.Label = label; - review.Revisions.Add(revision); - - if (review.PackageName != null) - { - var p = await _packageNameManager.GetPackageDetails(review.PackageName); - review.PackageDisplayName = p?.DisplayName ?? review.PackageDisplayName; - review.ServiceName = p?.ServiceName ?? review.ServiceName; - } - - var languageService = language != null ? _languageServices.FirstOrDefault(l => l.Name == language) : _languageServices.FirstOrDefault(s => s.IsSupportedFile(name)); - // Run pipeline to generate the review if sandbox is enabled - if (languageService != null && languageService.IsReviewGenByPipeline) - { - // Run offline review gen for review and reviewCodeFileModel - await GenerateReviewOffline(review, revision.RevisionId, codeFile.ReviewFileId, name, language); - } - - // auto subscribe revision creation user - await _notificationManager.SubscribeAsync(review, user); - await _reviewsRepository.UpsertReviewAsync(review); - await _notificationManager.NotifySubscribersOnNewRevisionAsync(revision, user); + var languageService = LanguageServiceHelpers.GetLanguageService(language, _languageServices); + if (languageService == null) + continue; - if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") - { - if (awaitComputeDiff) + // If review is updated using devops pipeline then batch process update review requests + if (languageService.IsReviewGenByPipeline) { - await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.ReviewId, revision); + await UpdateReviewsUsingPipeline(language, languageService, backgroundBatchProcessCount); } else { - _ = Task.Run(async () => await GetLineNumbersOfHeadingsOfSectionsWithDiff(review.ReviewId, revision)); - } - } - //await GenerateAIReview(review, revision); - } - - private async Task CreateFileAsync( - string revisionId, - string originalName, - Stream fileStream, - bool runAnalysis, - string language) - { - using var memoryStream = new MemoryStream(); - var codeFile = await CreateCodeFile(originalName, fileStream, runAnalysis, memoryStream, language); - var reviewCodeFileModel = await CreateReviewCodeFileModel(revisionId, memoryStream, codeFile); - reviewCodeFileModel.FileName = originalName; - return reviewCodeFileModel; - } - - private void InitializeFromCodeFile(ReviewCodeFileModel file, CodeFile codeFile) - { - file.Language = codeFile.Language; - file.LanguageVariant = codeFile.LanguageVariant; - file.VersionString = codeFile.VersionString; - file.Name = codeFile.Name; - file.PackageName = codeFile.PackageName; - file.PackageVersion = codeFile.PackageVersion; - } - - private LanguageService GetLanguageService(string language) - { - return _languageServices.FirstOrDefault(service => service.Name == language); - } - - private void AssertReviewDeletion(ReviewModel reviewModel) - { - // We allow deletion of manual API review only. - // Server side assertion to ensure we are not processing any requests to delete automatic and PR API review - if (reviewModel.FilterType != ReviewType.Manual) - { - throw new UnDeletableReviewException(); - } - } + var reviews = await _reviewsRepository.GetReviewsAsync(language: language, isClosed: false); - private async Task AssertReviewOwnerAsync(ClaimsPrincipal user, ReviewModel reviewModel) - { - var result = await _authorizationService.AuthorizeAsync(user, reviewModel, new[] { ReviewOwnerRequirement.Instance }); - if (!result.Succeeded) - { - throw new AuthorizationFailedException(); - } - } - - private async Task AssertRevisionOwner(ClaimsPrincipal user, ReviewRevisionModel revisionModel) - { - var result = await _authorizationService.AuthorizeAsync( - user, - revisionModel, - new[] { RevisionOwnerRequirement.Instance }); - if (!result.Succeeded) - { - throw new AuthorizationFailedException(); - } - } + foreach (var review in reviews) + { + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(review.Id); - private async Task AssertApprover(ClaimsPrincipal user, ReviewRevisionModel revisionModel) - { - var result = await _authorizationService.AuthorizeAsync( - user, - revisionModel, - new[] { ApproverRequirement.Instance }); - if (!result.Succeeded) - { - throw new AuthorizationFailedException(); + foreach (var revision in revisions) + { + if ( + revision.Files.First().HasOriginal && + LanguageServiceHelpers.GetLanguageService(revision.Language, _languageServices)?.CanUpdate(revision.Files.First().VersionString) == true) + { + var requestTelemetry = new RequestTelemetry { Name = "Updating Review " + review.Id }; + var operation = _telemetryClient.StartOperation(requestTelemetry); + try + { + await Task.Delay(500); + await _apiRevisionsManager.UpdateAPIRevisionAsync(revision, languageService, _telemetryClient); + } + catch (Exception e) + { + _telemetryClient.TrackException(e); + } + finally + { + _telemetryClient.StopOperation(operation); + } + } + } + } + } } } - private bool IsUpdateAvailable(ReviewModel review) + private async Task ToggleReviewApproval(ClaimsPrincipal user, ReviewListItemModel review, string notes) { - return review.Revisions - .SelectMany(r => r.Files) - .Any(f => f.HasOriginal && GetLanguageService(f.Language)?.CanUpdate(f.VersionString) == true); + await ManagerHelpers.AssertApprover(user, review, _authorizationService); + var userId = user.GetGitHubLogin(); + var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction( + review.ChangeHistory, ReviewChangeAction.Approved, userId, notes); + review.ChangeHistory = changeUpdate.ChangeHistory; + review.IsApproved = changeUpdate.ChangeStatus; + await _reviewsRepository.UpsertReviewAsync(review); } - private async Task CreateMasterReviewAsync(ClaimsPrincipal user, CodeFile codeFile, string originalName, string label, MemoryStream memoryStream, bool compareAllRevisions) + /// + /// Languages that full support sandboxing updates reviews using Azure devops pipeline + /// We should batch all eligible reviews to avoid a pipeline run storm + /// + /// + /// + /// + /// + private async Task UpdateReviewsUsingPipeline(string language, LanguageService languageService, int backgroundBatchProcessCount) { - var renderedCodeFile = new RenderedCodeFile(codeFile); + var reviews = await _reviewsRepository.GetReviewsAsync(language: language, isClosed: false); + var paramList = new List(); - //Get current master review for package and language - var review = await _reviewsRepository.GetMasterReviewForPackageAsync(codeFile.Language, codeFile.PackageName); - var createNewRevision = true; - ReviewRevisionModel reviewRevision = null; - if (review != null) + foreach (var review in reviews) { - // Delete pending revisions if it is not in approved state and if it doesn't have any comments before adding new revision - // This is to keep only one pending revision since last approval or from initial review revision - var lastRevision = review.Revisions.LastOrDefault(); - var comments = await _commentsRepository.GetCommentsAsync(review.ReviewId); - while (lastRevision.Approvers.Count == 0 && - review.Revisions.Count > 1 && - !await IsReviewSame(lastRevision, renderedCodeFile) && - !comments.Any(c => lastRevision.RevisionId == c.RevisionId)) + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(review.Id); + foreach (var revision in revisions) { - review.Revisions.Remove(lastRevision); - lastRevision = review.Revisions.LastOrDefault(); - } - // We should compare against only latest revision when calling this API from scheduled CI runs - // But any manual pipeline run at release time should compare against all approved revisions to ensure hotfix release doesn't have API change - // If review surface doesn't match with any approved revisions then we will create new revision if it doesn't match pending latest revision - if (compareAllRevisions) - { - foreach (var approvedRevision in review.Revisions.Where(r => r.IsApproved).Reverse()) + foreach (var file in revision.Files) { - if (await IsReviewSame(approvedRevision, renderedCodeFile)) + //Don't include current revision if file is not required to be updated. + // E.g. json token file is uploaded for a language, specific revision was already upgraded. + if (!file.HasOriginal || file.FileName == null || !languageService.IsSupportedFile(file.FileName) || !languageService.CanUpdate(file.VersionString)) { - return approvedRevision; + continue; } - } - } - - if (await IsReviewSame(lastRevision, renderedCodeFile)) - { - reviewRevision = lastRevision; - createNewRevision = false; - } - } - else - { - // Package and language combination doesn't have automatically created review. Create a new review. - review = new ReviewModel - { - Author = user.GetGitHubLogin(), - CreationDate = DateTime.UtcNow, - RunAnalysis = false, - Name = originalName, - FilterType = ReviewType.Automatic - }; - } - - // Check if user is authorized to modify automatic review - await AssertAutomaticReviewModifier(user, review); - if (createNewRevision) - { - // Update or insert review with new revision - reviewRevision = new ReviewRevisionModel() - { - Author = user.GetGitHubLogin(), - Label = label - }; - var reviewCodeFileModel = await CreateReviewCodeFileModel(reviewRevision.RevisionId, memoryStream, codeFile); - reviewCodeFileModel.FileName = originalName; - reviewRevision.Files.Add(reviewCodeFileModel); - review.Revisions.Add(reviewRevision); - } - // Check if review can be marked as approved if another review with same surface level is in approved status - if (review.Revisions.Last().Approvers.Count() == 0) - { - var matchingApprovedRevision = await FindMatchingApprovedRevision(review); - if (matchingApprovedRevision != null) - { - foreach (var approver in matchingApprovedRevision.Approvers) - { - review.Revisions.Last().Approvers.Add(approver); + _telemetryClient.TrackTrace($"Updating review: {review.Id}, revision: {revision.Id}"); + paramList.Add(new APIRevisionGenerationPipelineParamModel() + { + FileID = file.FileId, + ReviewID = review.Id, + RevisionID = revision.Id, + FileName = Path.GetFileName(file.FileName) + }); } } - } - await _reviewsRepository.UpsertReviewAsync(review); - return reviewRevision; - } - private async Task AssertAutomaticReviewModifier(ClaimsPrincipal user, ReviewModel reviewModel) - { - var result = await _authorizationService.AuthorizeAsync( - user, - reviewModel, - new[] { AutoReviewModifierRequirement.Instance }); - if (!result.Succeeded) - { - throw new AuthorizationFailedException(); - } - } - - private async Task FindMatchingApprovedRevision(ReviewModel review) - { - var revisionModel = review.Revisions.LastOrDefault(); - var revisionFile = revisionModel.Files.FirstOrDefault(); - var codeFile = await _codeFileRepository.GetCodeFileAsync(revisionModel); - - // Get manual reviews to check if a matching review is in approved state - var reviews = await _reviewsRepository.GetReviewsAsync(false, revisionFile.Language, revisionFile.PackageName, ReviewType.Manual); - var prReviews = await _reviewsRepository.GetReviewsAsync(false, revisionFile.Language, revisionFile.PackageName, ReviewType.PullRequest); - reviews = reviews.Concat(prReviews); - foreach (var r in reviews) - { - var approvedRevision = r.Revisions.Where(r => r.IsApproved).LastOrDefault(); - if (approvedRevision != null) + // This should be changed to configurable batch count + if (paramList.Count >= backgroundBatchProcessCount) { - var isReviewSame = await IsReviewSame(approvedRevision, codeFile); - if (isReviewSame) - { - return approvedRevision; - } + _telemetryClient.TrackTrace($"Running pipeline to update reviews for {language} with batch size {paramList.Count}"); + await _apiRevisionsManager.RunAPIRevisionGenerationPipeline(paramList, languageService.Name); + // Delay of 10 minute before starting next batch + // We should try to increase the number of revisions in the batch than number of runs. + await Task.Delay(600000); + paramList.Clear(); } } - return null; - } - private async Task GenerateReviewOffline(ReviewModel review, string revisionId, string fileId, string fileName, string language = null) - { - var languageService = _languageServices.Single(s => s.Name == language || s.Name == review.Language); - var revision = review.Revisions.FirstOrDefault(r => r.RevisionId == revisionId); - var param = new ReviewGenPipelineParamModel() - { - FileID = fileId, - ReviewID = review.ReviewId, - RevisionID = revisionId, - FileName = fileName - }; - if (!languageService.GeneratePipelineRunParams(param)) - { - throw new Exception($"Failed to run pipeline for review: {param.ReviewID}, file: {param.FileName}"); - } - - var paramList = new List - { - param - }; - - await RunReviewGenPipeline(paramList, languageService.Name); - } - - private async Task RunReviewGenPipeline(List reviewGenParams, string language) - { - var jsonSerializerOptions = new JsonSerializerOptions() - { - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip - }; - var reviewParamString = JsonSerializer.Serialize(reviewGenParams, jsonSerializerOptions); - reviewParamString = reviewParamString.Replace("\"", "'"); - await _devopsArtifactRepository.RunPipeline($"tools - generate-{language}-apireview", - reviewParamString, - _originalsRepository.GetContainerUrl()); - } - - private (CodeLine[] htmlLines, CodeLine[] textLines) GetCodeLinesForDiff(TreeNode node, TreeNode> curr, RenderedCodeFile codeFile) - { - (CodeLine[] htmlLines, CodeLine[] textLines) result = (new CodeLine[] { }, new CodeLine[] { }); - if (node != null) + if (paramList.Count > 0) { - if (node.IsLeaf) - { - result.htmlLines = codeFile.GetDetachedLeafSectionLines(node); - result.textLines = codeFile.GetDetachedLeafSectionLines(node, renderType: RenderType.Text, skipDiff: true); - - if (result.htmlLines.Count() > 0) - { - curr.WasDetachedLeafParent = true; - } - } - else - { - result.htmlLines = result.textLines = node.Children.Select(x => new CodeLine(x.Data, nodeRef: x)).ToArray(); - } + _telemetryClient.TrackTrace($"Running pipeline to update reviews for {language} with batch size {paramList.Count}"); + await _apiRevisionsManager.RunAPIRevisionGenerationPipeline(paramList, languageService.Name); } - return result; } } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/SamplesRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/SamplesRevisionsManager.cs new file mode 100644 index 00000000000..63646166623 --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Managers/SamplesRevisionsManager.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.IO; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Markdig; +using Markdig.SyntaxHighlighting; +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Formatters; +using APIViewWeb.Repositories; +using APIViewWeb.LeanModels; + +namespace APIViewWeb.Managers +{ + public class SamplesRevisionsManager : ISamplesRevisionsManager + { + private readonly IAuthorizationService _authorizationService; + private readonly ICosmosSamplesRevisionsRepository _samplesRevisionsRepository; + private readonly IBlobUsageSampleRepository _sampleFilesRepository; + private readonly ICosmosCommentsRepository _commentsRepository; + private readonly ICommentsManager _commentsManager; + + public SamplesRevisionsManager( + IAuthorizationService authorizationService, + ICosmosSamplesRevisionsRepository samplesRepository, + IBlobUsageSampleRepository sampleFilesRepository, + ICosmosCommentsRepository commentsRepository, + ICommentsManager commentManager) + { + _authorizationService = authorizationService; + _samplesRevisionsRepository = samplesRepository; + _sampleFilesRepository = sampleFilesRepository; + _commentsRepository = commentsRepository; + _commentsManager = commentManager; + } + + public async Task> GetSamplesRevisionsAsync(string reviewId) + { + return await _samplesRevisionsRepository.GetSamplesRevisionsAsync(reviewId); + } + + public async Task GetSamplesRevisionContentAsync(string fileId) + { + var file = await _sampleFilesRepository.GetUsageSampleAsync(fileId); + + if (file == null) return null; + + var reader = new StreamReader(file); + var htmlString = reader.ReadToEnd(); + + return htmlString; + } + + public async Task UpsertSamplesRevisionsAsync(ClaimsPrincipal user, string reviewId, string sample, string revisionTitle, string FileName = null) + { + // markdig parser with syntax highlighting + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseSyntaxHighlighting() + .Build(); + + var htmlSample = Markdown.ToHtml(sample, pipeline); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(htmlSample)); + var originalStream = new MemoryStream(Encoding.UTF8.GetBytes(sample)); + + // Create new file and upsert the updated model + var sampleRevision = new SamplesRevisionModel(); + sampleRevision.Title = revisionTitle ?? FileName; + + await _samplesRevisionsRepository.UpsertSamplesRevisionAsync(sampleRevision); + await _sampleFilesRepository.UploadUsageSampleAsync(sampleRevision.FileId, stream); + await _sampleFilesRepository.UploadUsageSampleAsync(sampleRevision.OriginalFileId, originalStream); + return sampleRevision; + } + + public async Task UpsertSamplesRevisionsAsync(ClaimsPrincipal user, string reviewId, Stream fileStream, string revisionTitle, string FileName) + { + // For file upload. Read stream then continue. + var reader = new StreamReader(fileStream); + var sample = reader.ReadToEnd(); + return await UpsertSamplesRevisionsAsync(user, reviewId, sample, revisionTitle, FileName); + } + + public async Task DeleteSamplesRevisionAsync(ClaimsPrincipal user, string reviewId, string sampleId) + { + var samplesRevision = await _samplesRevisionsRepository.GetSamplesRevisionAsync(reviewId, sampleId); + await AssertUsageSampleOwnerAsync(user, samplesRevision); + + samplesRevision.IsDeleted = true; + + var comments = await _commentsRepository.GetCommentsAsync(reviewId); + foreach (var comment in comments) + { + var commentSampleId = comment.ElementId.Split("-")[0]; // sample id is stored as first part of ElementId + if (comment.CommentType == CommentType.SamplesRevision && commentSampleId == samplesRevision.FileId) // remove all comments from server + { + await _commentsManager.SoftDeleteCommentAsync(user, comment); + } + } + await _samplesRevisionsRepository.UpsertSamplesRevisionAsync(samplesRevision); + } + + private async Task AssertUsageSampleOwnerAsync(ClaimsPrincipal user, SamplesRevisionModel samplesRevision) + { + var result = await _authorizationService.AuthorizeAsync(user, samplesRevision, new[] { UsageSampleOwnerRequirement.Instance }); + if (!result.Succeeded) + { + throw new AuthorizationFailedException(); + } + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Managers/UsageSampleManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/UsageSampleManager.cs deleted file mode 100644 index 86f200505f3..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Managers/UsageSampleManager.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.IO; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using Markdig; -using Markdig.SyntaxHighlighting; -using Microsoft.AspNetCore.Authorization; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc.Formatters; -using APIViewWeb.Repositories; - -namespace APIViewWeb.Managers -{ - public class UsageSampleManager : IUsageSampleManager - { - private readonly IAuthorizationService _authorizationService; - private readonly ICosmosUsageSampleRepository _samplesRepository; - private readonly IBlobUsageSampleRepository _sampleFilesRepository; - private readonly ICosmosCommentsRepository _commentsRepository; - - public UsageSampleManager( - IAuthorizationService authorizationService, - ICosmosUsageSampleRepository samplesRepository, - IBlobUsageSampleRepository sampleFilesRepository, - ICosmosCommentsRepository commentsRepository) - { - _authorizationService = authorizationService; - _samplesRepository = samplesRepository; - _sampleFilesRepository = sampleFilesRepository; - _commentsRepository = commentsRepository; - } - - public async Task> GetReviewUsageSampleAsync(string reviewId) - { - return await _samplesRepository.GetUsageSampleAsync(reviewId); - } - - public async Task GetUsageSampleContentAsync(string fileId) - { - var file = await _sampleFilesRepository.GetUsageSampleAsync(fileId); - - if (file == null) return null; - - var reader = new StreamReader(file); - var htmlString = reader.ReadToEnd(); - - return htmlString; - } - - public async Task UpsertReviewUsageSampleAsync(ClaimsPrincipal user, string reviewId, string sample, int revisionNum, string revisionTitle, string FileName = null) - { - // markdig parser with syntax highlighting - var pipeline = new MarkdownPipelineBuilder() - .UseAdvancedExtensions() - .UseSyntaxHighlighting() - .Build(); - - var htmlSample = Markdown.ToHtml(sample, pipeline); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes(htmlSample)); - var originalStream = new MemoryStream(Encoding.UTF8.GetBytes(sample)); - var SampleModel = (await _samplesRepository.GetUsageSampleAsync(reviewId)).FirstOrDefault() ?? new UsageSampleModel(reviewId); - - // Create new file and upsert the updated model - var SampleRevision = new UsageSampleRevisionModel(user, revisionNum); - if (revisionTitle == null && FileName != null) - { - SampleRevision.RevisionTitle = FileName; - } - else - { - SampleRevision.RevisionTitle = revisionTitle; - } - if (SampleModel.Revisions == null) - { - SampleModel.Revisions = new List(); - } - SampleModel.Revisions.Add(SampleRevision); - - await _samplesRepository.UpsertUsageSampleAsync(SampleModel); - await _sampleFilesRepository.UploadUsageSampleAsync(SampleRevision.FileId, stream); - await _sampleFilesRepository.UploadUsageSampleAsync(SampleRevision.OriginalFileId, originalStream); - return SampleModel; - } - - public async Task UpsertReviewUsageSampleAsync(ClaimsPrincipal user, string reviewId, Stream fileStream, int revisionNum, string revisionTitle, string FileName) - { - // For file upload. Read stream then continue. - var reader = new StreamReader(fileStream); - var sample = reader.ReadToEnd(); - return await UpsertReviewUsageSampleAsync(user, reviewId, sample, revisionNum, revisionTitle, FileName); - } - - public async Task DeleteUsageSampleAsync(ClaimsPrincipal user, string reviewId, string FileId, string sampleId) - { - var sampleModels = (await _samplesRepository.GetUsageSampleAsync(reviewId)).Find(e => e.SampleId == sampleId); - var sampleModel = sampleModels.Revisions.Find(e => e.FileId == FileId); - - await AssertUsageSampleOwnerAsync(user, sampleModel); - - sampleModels.Revisions.Remove(sampleModel); - - var i = 0; - foreach (var revision in sampleModels.Revisions) - { - if (revision.RevisionIsDeleted) - { - continue; - } - i++; - revision.RevisionNumber = i; - } - - var comments = await _commentsRepository.GetCommentsAsync(reviewId); - foreach (var comment in comments) - { - var commentSampleId = comment.ElementId.Split("-")[0]; // sample id is stored as first part of ElementId - if (comment.IsUsageSampleComment && commentSampleId == FileId) // remove all comments from server - { - await _commentsRepository.DeleteCommentAsync(comment); - } - } - - sampleModel.RevisionIsDeleted = true; - - await _samplesRepository.UpsertUsageSampleAsync(sampleModels); - - } - - private async Task AssertUsageSampleOwnerAsync(ClaimsPrincipal user, UsageSampleRevisionModel sampleModel) - { - var result = await _authorizationService.AuthorizeAsync(user, sampleModel, new[] { UsageSampleOwnerRequirement.Instance }); - if (!result.Succeeded) - { - throw new AuthorizationFailedException(); - } - } - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewCodeFileModel.cs b/src/dotnet/APIView/APIViewWeb/Models/APICodeFileModel.cs similarity index 91% rename from src/dotnet/APIView/APIViewWeb/Models/ReviewCodeFileModel.cs rename to src/dotnet/APIView/APIViewWeb/Models/APICodeFileModel.cs index ccadbad1414..8172a2d974b 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewCodeFileModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/APICodeFileModel.cs @@ -5,11 +5,11 @@ namespace APIViewWeb { - public class ReviewCodeFileModel + public class APICodeFileModel { private string _language; - public string ReviewFileId { get; set; } = IdHelper.GenerateId(); + public string FileId { get; set; } = IdHelper.GenerateId(); // This is field is more of a display name. It is set to name value returned by parser which has package name and version in following format // Package name ( Version ) diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs b/src/dotnet/APIView/APIViewWeb/Models/APIRevisionGenerationPipelineParamModel.cs similarity index 88% rename from src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs rename to src/dotnet/APIView/APIViewWeb/Models/APIRevisionGenerationPipelineParamModel.cs index bd3c42453ef..76a399f8f7c 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/APIRevisionGenerationPipelineParamModel.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace APIViewWeb.Models -{ - public class ReviewGenPipelineParamModel - { - public string ReviewID { get; set; } - public string RevisionID { get; set; } - public string FileID { get; set; } +namespace APIViewWeb.Models +{ + public class APIRevisionGenerationPipelineParamModel + { + public string ReviewID { get; set; } + public string RevisionID { get; set; } + public string FileID { get; set; } public string FileName { get; set; } public string SourceRepoName { get; set; } - public string SourceBranchName { get; set; } - } -} + public string SourceBranchName { get; set; } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs deleted file mode 100644 index fda162c07e1..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace APIViewWeb.Models -{ - public class CommentModel - { - [JsonProperty("id")] - public string CommentId { get; set; } = IdHelper.GenerateId(); - public string ReviewId { get; set; } - public string RevisionId { get; set; } - public string ElementId { get; set; } - public string SectionClass { get; set; } - public string GroupNo { get; set; } - public string Comment { get; set; } - public DateTime TimeStamp { get; set; } - public string Username { get; set; } - public bool IsResolve { get; set; } - public DateTime? EditedTimeStamp { get; set; } - public List Upvotes { get; set; } = new List(); - public HashSet TaggedUsers { get; set; } = new HashSet(); - public bool IsUsageSampleComment { get; set; } = false; - public bool ResolutionLocked { get; set; } = false; - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs index 9e06446e552..dbcfd36a06e 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs @@ -2,24 +2,25 @@ // Licensed under the MIT License. using System.Collections.Generic; using System.Linq; +using APIViewWeb.LeanModels; namespace APIViewWeb.Models { public class CommentThreadModel { - public CommentThreadModel(string reviewId, string lineId, IEnumerable comments) + public CommentThreadModel(string reviewId, string lineId, IEnumerable comments) { ReviewId = reviewId; LineId = lineId; LineClass = comments.FirstOrDefault().SectionClass; - Comments = comments.Where(c => !c.IsResolve); - var resolveComment = comments.FirstOrDefault(c => c.IsResolve); + Comments = comments.Where(c => !c.IsResolved); + var resolveComment = comments.FirstOrDefault(c => c.IsResolved); IsResolved = resolveComment != null; - ResolvedBy = resolveComment?.Username; + ResolvedBy = resolveComment?.CreatedBy; } public string ReviewId { get; set; } - public IEnumerable Comments { get; set; } + public IEnumerable Comments { get; set; } public string LineId { get; set; } public string LineClass { get; set; } public bool IsResolved { get; set; } diff --git a/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs b/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs deleted file mode 100644 index bbb7541f847..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -using System.Collections.Generic; - -namespace APIViewWeb.Models -{ - public class PackageGroupModel - { - - public string PackageDisplayName { get; set; } - - public List reviews { get; set; } = new (); - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/PullRequestModel.cs b/src/dotnet/APIView/APIViewWeb/Models/PullRequestModel.cs index c95e0ee87fd..180bfe4a54c 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/PullRequestModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/PullRequestModel.cs @@ -9,16 +9,18 @@ namespace APIViewWeb.Models public class PullRequestModel { [JsonProperty("id")] - public string PullRequestId { get; set; } = IdHelper.GenerateId(); + public string Id { get; set; } = IdHelper.GenerateId(); + public string ReviewId { get; set; } + public string APIRevisionId { get; set; } public int PullRequestNumber { get; set; } public List Commits { get; set; } = new List(); public string RepoName { get; set; } public string FilePath { get; set; } public bool IsOpen { get; set; } = true; - public string ReviewId { get; set; } - public string Author { get; set; } + public string CreatedBy { get; set; } public string PackageName { get; set; } public string Language { get; set; } public string Assignee { get; set; } + public bool IsDeleted { get; set; } } } diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewCommentsModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewCommentsModel.cs index d56ffe01b5c..220cbcb8f30 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewCommentsModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ReviewCommentsModel.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using APIViewWeb.LeanModels; using APIViewWeb.Models; namespace APIViewWeb @@ -13,9 +14,9 @@ public class ReviewCommentsModel { private Dictionary _threads; - public ReviewCommentsModel(string reviewId, IEnumerable comments) + public ReviewCommentsModel(string reviewId, IEnumerable comments) { - _threads = comments.OrderBy(c => c.TimeStamp) + _threads = comments.OrderBy(c => c.CreatedOn) .GroupBy(c => c.ElementId) .ToDictionary(c => c.Key ?? string.Empty, c => new CommentThreadModel(reviewId, c.Key, c)); } diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs deleted file mode 100644 index bd86082f88c..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -using System; - -namespace APIViewWeb.Models -{ - public class ReviewDisplayModel - { - public string Name { get; set; } - public string ReviewDisplayName { get; set; } - public string id { get; set; } - public string Author { get; set; } - public DateTime LastUpdated { get; set; } - public string Language { get; set; } - public bool IsClosed { get; set; } - public ReviewType FilterType { get; set; } = ReviewType.Manual; - public bool IsApproved { get; set; } - public string ServiceName { get; set; } - public string PackageDisplayName { get; set; } - - public ReviewDisplayModel(ReviewModel review) - { - ReviewDisplayName = review.DisplayName; - Name = review.Name; - id = review.ReviewId; - Author = review.Author; - LastUpdated = review.LastUpdated; - Language = review.Language; - IsClosed = review.IsClosed; - FilterType = review.FilterType; - IsApproved = review.IsApproved; - ServiceName = review.ServiceName; - PackageDisplayName = review.PackageDisplayName; - } - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs deleted file mode 100644 index 0fd0a675324..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewModel.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using APIViewWeb.Managers; -using APIViewWeb.Models; -using Newtonsoft.Json; - -namespace APIViewWeb -{ - public class ReviewModel - { - private bool _runAnalysis; - - public ReviewModel() - { - Revisions = new ReviewRevisionModelList(this); - } - - [JsonProperty("id")] - public string ReviewId { get; set; } = IdHelper.GenerateId(); - - public string Name { get; set; } - public string Author { get; set; } - public DateTime CreationDate { get; set; } - public ReviewRevisionModelList Revisions { get; set; } - - [Obsolete("Back compat")] - public List Files { get; set; } = new List(); - - [JsonIgnore] - public bool UpdateAvailable { get; set; } - - public bool RunAnalysis - { -#pragma warning disable 618 - get => _runAnalysis || Revisions.SelectMany(r => r.Files).Any(f => f.RunAnalysis); -#pragma warning restore 618 - set => _runAnalysis = value; - } - - public bool IsClosed { get; set; } - - public HashSet Subscribers { get; set; } = new HashSet(); - - public bool IsUserSubscribed(ClaimsPrincipal user) - { - string email = GetUserEmail(user); - if (email != null) - { - return Subscribers.Contains(email); - } - return false; - } - - public string GetUserEmail(ClaimsPrincipal user) => - NotificationManager.GetUserEmail(user); - - // gets CSS safe language name - such that css classes based on language name would not need any escaped characters - public string GetLanguageCssSafeName() - { - switch (Language.ToLower()) - { - case "c#": - return "csharp"; - case "c++": - return "cplusplus"; - default: - return Language.ToLower(); - } - } - - [JsonIgnore] - public string DisplayName - { - get - { - var revision = Revisions.LastOrDefault(); - var label = revision?.Label; - var name = revision?.Name ?? Name; - return label != null && !IsAutomatic ? - $"{name} - {label}" : - name; - } - } - - public DateTime LastUpdated => Revisions.LastOrDefault()?.CreationDate ?? CreationDate; - - [JsonIgnore] - public string Language => Revisions.LastOrDefault()?.Files.LastOrDefault()?.Language; - - [JsonIgnore] - public string LanguageVariant => Revisions.LastOrDefault()?.Files.LastOrDefault()?.LanguageVariant; - - [JsonIgnore] - public string PackageName { - get - { - var packageName = Revisions.LastOrDefault()?.Files.LastOrDefault()?.PackageName; - if (String.IsNullOrWhiteSpace(packageName)) - { - return "Other"; - } - else - { - return packageName; - } - } - } - - // Master version of review for each package will be auto created - public bool IsAutomatic { get; set; } - - public ReviewType FilterType { get; set; } - - [JsonIgnore] - public bool IsApproved => Revisions.LastOrDefault()?.Approvers?.Any() ?? false; - - public string ServiceName { get; set; } - - public string PackageDisplayName { get; set; } - - // Approvers requested for review and when (for hiding older reviews) - public HashSet RequestedReviewers { get; set; } = null; - - public string RequestedBy { get; set; } = null; - - public DateTime ApprovalRequestedOn; - - public DateTime ApprovalDate; - public bool IsApprovedForFirstRelease { get; set; } - public string ApprovedForFirstReleaseBy { get; set; } - public DateTime ApprovedForFirstReleaseOn { get; set; } - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModel.cs deleted file mode 100644 index e0ac490e13f..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModel.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Newtonsoft.Json; - -namespace APIViewWeb -{ - public class ReviewRevisionModel - { - private string _name; - - private string _author; - - private static readonly Regex s_oldRevisionStyle = new Regex("rev \\d+ -"); - - [JsonProperty("id")] - public string RevisionId { get; set; } = IdHelper.GenerateId(); - - public List Files { get; set; } = new List(); - - public Dictionary> HeadingsOfSectionsWithDiff { get; set; } = new Dictionary>(); - - public DateTime CreationDate { get; set; } = DateTime.Now; - - public string Name - { - get => _name ?? Files.FirstOrDefault()?.Name; - set => _name = value; - } - - [JsonIgnore] - public ReviewCodeFileModel SingleFile => Files.Single(); - - [JsonIgnore] - public ReviewModel Review { get; set; } - - public string Author - { - get => _author ?? Review.Author; - set => _author = value; - } - - [JsonIgnore] - public string DisplayName - { - get - { - string name; - if (s_oldRevisionStyle.IsMatch(Name)) - { - // old model where revision number was stored directly on Name - name = Name.Substring(Name.IndexOf('-') + 1); - } - else - { - // New model where revision number is calculated on demand. This makes - // the feature to allow for editing revision names cleaner. - name = Name; - } - return Label != null ? - $"rev {RevisionNumber} - {Label} - {name}" : - $"rev {RevisionNumber} - {name}"; - } - } - - public string Label { get; set; } - - public int RevisionNumber => Review.Revisions.IndexOf(this); - - public HashSet Approvers { get; set; } = new HashSet(); - - public bool IsApproved => Approvers.Count() > 0; - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModelList.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModelList.cs deleted file mode 100644 index d78790e3a5c..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewRevisionModelList.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections; -using System.Collections.Generic; - -namespace APIViewWeb.Models -{ - public class ReviewRevisionModelList : IList - { - private readonly ReviewModel _review; - private readonly List _list; - public ReviewRevisionModelList(ReviewModel review) - { - _review = review; - _list = new List(); - } - - public ReviewRevisionModel this[int index] { get => _list[index]; set => _list[index] = value; } - - public int Count => _list.Count; - - public bool IsReadOnly => ((IList)_list).IsReadOnly; - - public void Add(ReviewRevisionModel item) - { - item.Review = _review; - _list.Add(item); - } - - public void AddRange(IEnumerable revisionModels) - { - foreach (ReviewRevisionModel revision in revisionModels) - { - Add(revision); - } - } - - public void RemoveAll(System.Predicate match) - { - _list.RemoveAll(match); - } - - public void Clear() - { - _list.Clear(); - } - - public bool Contains(ReviewRevisionModel item) - { - return _list.Contains(item); - } - - public void CopyTo(ReviewRevisionModel[] array, int arrayIndex) - { - _list.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } - - public int IndexOf(ReviewRevisionModel item) - { - return _list.IndexOf(item); - } - - public void Insert(int index, ReviewRevisionModel item) - { - _list.Insert(index, item); - } - - public bool Remove(ReviewRevisionModel item) - { - return _list.Remove(item); - } - - public void RemoveAt(int index) - { - _list.RemoveAt(index); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _list.GetEnumerator(); - } - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleUploadModel.cs b/src/dotnet/APIView/APIViewWeb/Models/SamplesRevisionUploadModel.cs similarity index 81% rename from src/dotnet/APIView/APIViewWeb/Models/UsageSampleUploadModel.cs rename to src/dotnet/APIView/APIViewWeb/Models/SamplesRevisionUploadModel.cs index 07ddfe08ec0..a434732339d 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleUploadModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/SamplesRevisionUploadModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.AspNetCore.Http; @@ -6,7 +6,7 @@ namespace APIViewWeb.Pages.Assemblies { - public class UsageSampleUploadModel + public class SamplesRevisionUploadModel { [BindProperty] public string sampleString { get; set; } @@ -26,9 +26,6 @@ public class UsageSampleUploadModel [BindProperty] public bool Updating { get; set; } = false; - [BindProperty] - public int RevisionNumber { get; set; } - [BindProperty] public string SampleId { get; set; } diff --git a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs deleted file mode 100644 index fa5f967c8f9..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace APIViewWeb -{ - public class UsageSampleModel - { - [JsonProperty("id")] - public string SampleId { get; set; } = IdHelper.GenerateId(); - public string ReviewId { get; set; } - public List Revisions { get; set; } - - public UsageSampleModel(string reviewId) - { - ReviewId = reviewId; - } - - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleRevisionModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UsageSampleRevisionModel.cs deleted file mode 100644 index 3ecdac6b5a7..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleRevisionModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Security.Claims; - -namespace APIViewWeb -{ - public class UsageSampleRevisionModel - { - public string FileId { get; set; } = IdHelper.GenerateId(); - public string OriginalFileId { get; set; } = IdHelper.GenerateId(); - public string OriginalFileName { get; set; } // likely to be null if uploaded via text - public string CreatedBy { get; set; } - public DateTime CreatedOn { get; set; } = DateTime.Now; - public string RevisionTitle { get; set; } - public int RevisionNumber { get; set; } - public bool RevisionIsDeleted { get; set; } = false; - public UsageSampleRevisionModel(ClaimsPrincipal user, int revisionNumber) - { - if(user != null) - { - CreatedBy = user.GetGitHubLogin(); - } - RevisionNumber = revisionNumber; - } - } -} diff --git a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs index d4a9d26c92e..f915d0ef04b 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; +using APIViewWeb.LeanModels; using CsvHelper.Configuration.Attributes; using Newtonsoft.Json; @@ -10,7 +11,7 @@ public class UserPreferenceModel { internal IEnumerable _language; internal IEnumerable _approvedLanguages; - internal IEnumerable _filterType; + internal IEnumerable _apiRevisionType; internal IEnumerable _state; internal IEnumerable _status; internal bool? _hideLineNumbers; @@ -37,10 +38,10 @@ public IEnumerable ApprovedLanguages set => _approvedLanguages = value; } - [Name("FilterType")] - public IEnumerable FilterType { - get => _filterType ?? new List(); - set => _filterType = value; + [Name("APIRevisionType")] + public IEnumerable APIRevisionType { + get => _apiRevisionType ?? new List(); + set => _apiRevisionType = value; } [Name("State")] diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml index 4d82a2d6234..8c064bce3f0 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml @@ -11,7 +11,7 @@
- +
@@ -23,7 +23,7 @@
-
+
@if (!Model.Threads.Any() && !Model.UsageSampleThreads.Any()) {
There are no comments in the review.
@@ -35,19 +35,19 @@
@foreach (var revision in Model.Threads) { - var divId = $"rev-{revision.Key.RevisionId}"; + var divId = $"rev-{revision.Key.Id}"; -
- @revision.Key.DisplayName +
+ PageModelHelpers.ResolveRevisionLabel(@revision.Key.Label)
-
+
@foreach (var thread in revision.Value) { var elementId = thread.LineId; @@ -64,22 +64,22 @@
@foreach (var revision in Model.UsageSampleThreads.Reverse()) - { - var divId = $"rev-{revision.Key.RevisionNumber}"; - var displayName = $"Usage sample - rev {@revision.Key.RevisionNumber}"; + { + var divId = $"rev-{revision.Key.sampleRevisionNumber}"; + var displayName = $"Usage sample - rev {@revision.Key.sampleRevisionNumber}"; - @if (revision.Key.RevisionTitle != null) - { - displayName += " - " + @revision.Key.RevisionTitle; - } - else if (revision.Key.OriginalFileName != null) - { - displayName += " - " + revision.Key.OriginalFileName; + @if (revision.Key.sampleRevision.Title != null) + { + displayName += " - " + @revision.Key.sampleRevision.Title; + } + else if (revision.Key.sampleRevision.OriginalFileName != null) + { + displayName += " - " + revision.Key.sampleRevision.OriginalFileName; } -
+
@displayName
-
+
@elementId
@foreach (var thread in revision.Value.OrderBy(e => int.Parse(e.LineId.Split("-").Last()))) { @@ -89,9 +89,10 @@ diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml.cs index a39daf89e81..7b9e7643086 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Conversation.cshtml.cs @@ -4,7 +4,9 @@ using System.Threading.Tasks; using ApiView; using APIView; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; using APIViewWeb.Repositories; using Microsoft.AspNetCore.Mvc; @@ -17,30 +19,34 @@ public class ConversationModel : PageModel { private readonly ICommentsManager _commentsManager; private readonly IReviewManager _reviewManager; - private readonly IUsageSampleManager _samplesManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; + private readonly ISamplesRevisionsManager _samplesManager; private const string ENDPOINT_SETTING = "Endpoint"; private readonly IBlobCodeFileRepository _codeFileRepository; public readonly UserPreferenceCache _preferenceCache; public string Endpoint { get; } - public ReviewModel Review { get; private set; } - public UsageSampleModel Sample { get; private set; } + public ReviewListItemModel Review { get; private set; } + public APIRevisionListItemModel LatestAPIRevision { get; set; } + public IEnumerable SamplesRevisions { get; private set; } public IEnumerable> SampleLines { get; private set; } - public IOrderedEnumerable>> Threads { get; set; } - public Dictionary> UsageSampleThreads { get; set; } + public IOrderedEnumerable>> Threads { get; set; } + public Dictionary<(SamplesRevisionModel sampleRevision, int sampleRevisionNumber), List> UsageSampleThreads { get; set; } public HashSet TaggableUsers { get; set; } public ConversationModel( IConfiguration configuration, IBlobCodeFileRepository codeFileRepository, ICommentsManager commentsManager, IReviewManager reviewManager, + IAPIRevisionsManager apiRevisionsManager, UserPreferenceCache preferenceCache, - IUsageSampleManager samplesManager) + ISamplesRevisionsManager samplesManager) { _codeFileRepository = codeFileRepository; _commentsManager = commentsManager; _reviewManager = reviewManager; + _apiRevisionsManager = apiRevisionsManager; Endpoint = configuration.GetValue(ENDPOINT_SETTING); _preferenceCache = preferenceCache; _samplesManager = samplesManager; @@ -51,39 +57,44 @@ public async Task OnGetAsync(string id) TaggableUsers = _commentsManager.GetTaggableUsers(); TempData["Page"] = "conversation"; Review = await _reviewManager.GetReviewAsync(User, id); - Sample = (await _samplesManager.GetReviewUsageSampleAsync(id)).FirstOrDefault(); + LatestAPIRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(Review.Id); + SamplesRevisions = await _samplesManager.GetSamplesRevisionsAsync(id); var comments = await _commentsManager.GetReviewCommentsAsync(id); - Threads = ParseThreads(comments.Threads); + Threads = await ParseThreads(comments.Threads); UsageSampleThreads = ParseUsageSampleThreads(comments.Threads); SampleLines = await getUsageSampleLines(); return Page(); } - private IOrderedEnumerable>> ParseThreads(IEnumerable threads) + private async Task>>> ParseThreads(IEnumerable threads) { - var threadDict = new Dictionary>(); + var threadDict = new Dictionary>(); + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(Review.Id); foreach (var thread in threads) { - ReviewRevisionModel lastRevisionForThread = null; - int lastRevision = 0; + APIRevisionListItemModel lastRevisionForThread = null; + DateTime lastRevisionDate = DateTime.MinValue; + foreach (var comment in thread.Comments) { - if (comment.RevisionId == null) + if (comment.APIRevisionId == null) { continue; } - ReviewRevisionModel commentRevision = Review.Revisions.SingleOrDefault(r => r.RevisionId == comment.RevisionId); + + APIRevisionListItemModel commentRevision = revisions.SingleOrDefault(r => r.Id == comment.APIRevisionId); + if (commentRevision == null) { // if revision that comment was added in has been deleted continue; } - var commentRevisionIndex = commentRevision.RevisionNumber; + var commentRevisionDate = commentRevision.CreatedOn; // Group each thread under the last revision where a comment was added for it. - if (commentRevisionIndex >= lastRevision) + if (commentRevisionDate >= lastRevisionDate) { - lastRevision = commentRevisionIndex; + lastRevisionDate = (DateTime)commentRevisionDate; lastRevisionForThread = commentRevision; } } @@ -97,14 +108,14 @@ private IOrderedEnumerable Review.Revisions.IndexOf(kvp.Key)); + return threadDict.OrderByDescending(kvp => kvp.Key.CreatedOn); } - private Dictionary> ParseUsageSampleThreads(IEnumerable threads) + private Dictionary<(SamplesRevisionModel sampleRevision, int sampleRevisionNumber), List> ParseUsageSampleThreads(IEnumerable threads) { - var threadDict = new Dictionary>(); + var threadDict = new Dictionary<(SamplesRevisionModel sampleRevision, int sampleRevisionNumber), List>(); - if (Sample == null) + if (!SamplesRevisions.Any()) { return threadDict; } @@ -113,19 +124,20 @@ private Dictionary> ParseUsag { foreach (var comment in thread.Comments) { - if (comment.IsUsageSampleComment) + if (comment.CommentType == CommentType.SamplesRevision) { - var sampleRevision = Sample.Revisions.Where(e => e.FileId.Equals(comment.ElementId.Split("-").First())).First(); - if (sampleRevision.RevisionIsDeleted) + var index = SamplesRevisions.ToList().FindIndex(s => s.FileId.Equals(comment.ElementId.Split("-").First())); + var sampleRevision = SamplesRevisions.ElementAt(index); + if (sampleRevision.IsDeleted) { continue; } - if(!threadDict.ContainsKey(sampleRevision)) + if(!threadDict.ContainsKey((sampleRevision, index))) { - threadDict.Add(sampleRevision, new List()); + threadDict.Add((sampleRevision, index), new List()); } - threadDict[sampleRevision].Add(thread); + threadDict[(sampleRevision, index)].Add(thread); } } @@ -138,14 +150,14 @@ private async Task>> getUsageSampleLines() { List> lines = new List>(); - if (Sample == null) + if (!SamplesRevisions.Any()) { return lines; } - foreach (var revision in Sample.Revisions.ToArray().Reverse()) + foreach (var revision in SamplesRevisions) { - string rawContent = await _samplesManager.GetUsageSampleContentAsync(revision.FileId); + string rawContent = await _samplesManager.GetSamplesRevisionContentAsync(revision.FileId); if (rawContent == null) { continue; diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Delete.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Delete.cshtml.cs index 70a36f8c553..52641f62af4 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Delete.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Delete.cshtml.cs @@ -27,7 +27,7 @@ public async Task OnGetAsync(string id) } var reviewModel = await _manager.GetReviewAsync(User, id); - AssemblyName = reviewModel.DisplayName; + AssemblyName = reviewModel.PackageName; return Page(); } @@ -39,7 +39,7 @@ public async Task OnPostAsync(string id) return NotFound(); } - await _manager.DeleteReviewAsync(User, id); + await _manager.SoftDeleteReviewAsync(User, id); return RedirectToPage("./Index"); } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml index 7607d851d61..514473424d1 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml @@ -52,19 +52,15 @@ + @* + Disabling Approval since it has no benefit to users at this point
Status
-
- Type - -
- + *@
@@ -76,64 +72,6 @@ mainContainerClass = String.Empty; }
- - - -
diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs index 336395cc337..e496203cfa3 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Index.cshtml.cs @@ -14,108 +14,65 @@ using APIViewWeb.Hubs; using Microsoft.AspNetCore.SignalR; using System.Text; +using APIViewWeb.LeanModels; namespace APIViewWeb.Pages.Assemblies { public class IndexPageModel : PageModel { - private readonly IReviewManager _manager; + private readonly IReviewManager _reviewManager; private readonly IHubContext _notificationHubContext; public readonly UserPreferenceCache _preferenceCache; public readonly IUserProfileManager _userProfileManager; public const int _defaultPageSize = 50; - public const string _defaultSortField = "LastUpdated"; + public const string _defaultSortField = "LastUpdatedOn"; - public IndexPageModel(IReviewManager manager, IUserProfileManager userProfileManager, UserPreferenceCache preferenceCache, IHubContext notificationHub) + public IndexPageModel(IReviewManager reviewManager, IUserProfileManager userProfileManager, UserPreferenceCache preferenceCache, IHubContext notificationHub) { _notificationHubContext = notificationHub; - _manager = manager; + _reviewManager = reviewManager; _preferenceCache = preferenceCache; _userProfileManager = userProfileManager; } - [FromForm] - public UploadModel Upload { get; set; } - - [FromForm] - public string Label { get; set; } - public ReviewsProperties ReviewsProperties { get; set; } = new ReviewsProperties(); - public (IEnumerable Reviews, int TotalCount, int TotalPages, + public (IEnumerable Reviews, int TotalCount, int TotalPages, int CurrentPage, int? PreviousPage, int? NextPage) PagedResults { get; set; } public async Task OnGetAsync( IEnumerable search, IEnumerable languages, IEnumerable state, - IEnumerable status, IEnumerable type, int pageNo=1, int pageSize=_defaultPageSize, string sortField=_defaultSortField) + IEnumerable status, int pageNo=1, int pageSize=_defaultPageSize, string sortField=_defaultSortField) { - if (!search.Any() && !languages.Any() && !state.Any() && !status.Any() && !type.Any()) + if (!search.Any() && !languages.Any() && !state.Any() && !status.Any()) { UserPreferenceModel userPreference = await _preferenceCache.GetUserPreferences(User); languages = userPreference.Language; state = userPreference.State; status = userPreference.Status; - type = userPreference.FilterType.Select(x => x.ToString()); - await RunGetRequest(search, languages, state, status, type, pageNo, pageSize, sortField, false); + await RunGetRequest(search, languages, state, status, pageNo, pageSize, sortField, false); } else { - await RunGetRequest(search, languages, state, status, type, pageNo, pageSize, sortField); + await RunGetRequest(search, languages, state, status, pageNo, pageSize, sortField); } } public async Task OnGetReviewsPartialAsync( IEnumerable search, IEnumerable languages, IEnumerable state, - IEnumerable status, IEnumerable type, int pageNo = 1, int pageSize=_defaultPageSize, string sortField=_defaultSortField) + IEnumerable status, int pageNo = 1, int pageSize=_defaultPageSize, string sortField=_defaultSortField) { - await RunGetRequest(search, languages, state, status, type, pageNo, pageSize, sortField); + await RunGetRequest(search, languages, state, status, pageNo, pageSize, sortField); return Partial("_ReviewsPartial", PagedResults); } - public async Task OnPostUploadAsync() - { - if (!ModelState.IsValid) - { - var errors = new StringBuilder(); - foreach (var modelState in ModelState.Values) - { - foreach (var error in modelState.Errors) - { - errors.AppendLine(error.ErrorMessage); - } - } - var notifcation = new NotificationModel() { Message = errors.ToString(), Level = NotificatonLevel.Error }; - await _notificationHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveNotification", notifcation); - return new NoContentResult(); - } - - var file = Upload.Files?.SingleOrDefault(); - - if (file != null) - { - using (var openReadStream = file.OpenReadStream()) - { - var reviewModel = await _manager.CreateReviewAsync(User, file.FileName, Label, openReadStream, Upload.RunAnalysis, langauge: Upload.Language); - return RedirectToPage("Review", new { id = reviewModel.ReviewId }); - } - } - else if (!Upload.FilePath.IsNullOrEmpty()) - { - var reviewModel = await _manager.CreateReviewAsync(User, Upload.FilePath, Label, null, Upload.RunAnalysis, langauge: Upload.Language); - return RedirectToPage("Review", new { id = reviewModel.ReviewId }); - } - - return RedirectToPage(); - } - private async Task RunGetRequest(IEnumerable search, IEnumerable languages, - IEnumerable state, IEnumerable status, IEnumerable type, int pageNo, int pageSize, string sortField, bool fromUrl = true) + IEnumerable state, IEnumerable status, int pageNo, int pageSize, string sortField, bool fromUrl = true) { search = search.Select(x => HttpUtility.UrlDecode(x)); languages = (fromUrl)? languages.Select(x => HttpUtility.UrlDecode(x)) : languages; state = state.Select(x => HttpUtility.UrlDecode(x)); status = status.Select(x => HttpUtility.UrlDecode(x)); - type = type.Select(x => HttpUtility.UrlDecode(x)); // Update selected properties if (languages.Any()) @@ -136,11 +93,6 @@ private async Task RunGetRequest(IEnumerable search, IEnumerable { ReviewsProperties.Status.Selected = status; } - - if (type.Any()) - { - ReviewsProperties.Type.Selected = type; - } bool? isClosed = null; // Resolve isClosed value @@ -157,12 +109,7 @@ private async Task RunGetRequest(IEnumerable search, IEnumerable isClosed = null; } - // Resolve FilterType - IEnumerable filterTypes = type.Select(x => (ReviewType)Enum.Parse(typeof(ReviewType), x)); - IEnumerable filterTypesAsInt = filterTypes.Select(x => (int)x); - _preferenceCache.UpdateUserPreference(new UserPreferenceModel { - FilterType = filterTypes, Language = languages, State = state, Status = status @@ -186,7 +133,7 @@ private async Task RunGetRequest(IEnumerable search, IEnumerable languages = LanguageServiceHelpers.MapLanguageAliases(languages); - PagedResults = await _manager.GetPagedReviewsAsync(search, languages, isClosed, filterTypesAsInt, isApproved, offset, pageSize, sortField); + PagedResults = await _reviewManager.GetPagedReviewListAsync(search, languages, isClosed, isApproved, offset, pageSize, sortField); } } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml deleted file mode 100644 index 13be8e86e0a..00000000000 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml +++ /dev/null @@ -1,148 +0,0 @@ -@page "" -@model APIViewWeb.Pages.Assemblies.RequestedReviews -@using APIViewWeb.Helpers -@using APIViewWeb.Models -@{ - ViewData["Title"] = "Requested Reviews"; - var userPreference = PageModelHelpers.GetUserPreference(Model._preferenceCache, User) ?? new UserPreferenceModel(); - TempData["UserPreference"] = userPreference; -} -
-
-
-

Pending Reviews

-
-
@Html.Raw(Model.SampleLines.ElementAt(revision.Key.RevisionNumber-skipped).ElementAt(int.Parse(thread.Comments.First().ElementId.Split("-").Last())-1)) + asp-fragment=@Uri.EscapeDataString(elementId)> + @Html.Raw(Model.SampleLines.ElementAt(revision.Key.sampleRevisionNumber-skipped).ElementAt(int.Parse(thread.Comments.First().ElementId.Split("-").Last())-1))
- - - - - - - - - - - - @if (Model.ActiveReviews.Any()) - { - @foreach (var review in Model.ActiveReviews) - { - var truncationIndex = @Math.Min(@review.DisplayName.Length, 100); - - - - - - - - - } - } - else - { - - - - - } - -
NameAuthorLast UpdatedTypeRequested onRequested by
- @if (review.Language != null) - { - string iconClassName = "icon-" + review.GetLanguageCssSafeName(); - @if (!string.IsNullOrEmpty(review.LanguageVariant) && review.LanguageVariant.ToLower() != "default") - { - iconClassName += "-" + @review.LanguageVariant.ToLower(); - } - - } - @review.DisplayName.Substring(0, @truncationIndex) - @if (review.IsApproved == true) - { - - } - - @review.Author - - - - @review.FilterType.ToString() - - - - @review.RequestedBy -
No new reviews require approval.
-
-
-
-

Recently-Approved Reviews

-
- - - - - - - - - - - - - @if (Model.ApprovedReviews.Any()) - { - @foreach (var review in Model.ApprovedReviews) - { - var truncationIndex = @Math.Min(@review.DisplayName.Length, 100); - - - - - - - - - } - } - else - { - - - - } - -
NameAuthorLast UpdatedTypeApproved lastApproved by
- @if (review.Language != null) - { - string iconClassName = "icon-" + review.GetLanguageCssSafeName(); - @if (!string.IsNullOrEmpty(review.LanguageVariant) && review.LanguageVariant.ToLower() != "default") - { - iconClassName += "-" + @review.LanguageVariant.ToLower(); - } - - } - @review.DisplayName.Substring(0, @truncationIndex) - @if (review.IsApproved == true) - { - - } - - @review.Author - - - - @review.FilterType.ToString() - - - - - @foreach(var approver in review.Revisions.First().Approvers) { - @approver - } - -
No reviews have been recently approved.
-
-
-
-
\ No newline at end of file diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs index da3a4c8b84b..dd9e3c3e437 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using ApiView; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; using APIViewWeb.Models; using APIViewWeb.Repositories; @@ -17,8 +18,8 @@ public class RequestedReviews: PageModel { private readonly IReviewManager _manager; public readonly UserPreferenceCache _preferenceCache; - public IEnumerable ActiveReviews { get; set; } = new List(); - public IEnumerable ApprovedReviews { get; set; } = new List(); + public IEnumerable ActiveReviews { get; set; } = new List(); + public IEnumerable ApprovedReviews { get; set; } = new List(); public RequestedReviews(IReviewManager manager, UserPreferenceCache cache) { @@ -28,10 +29,10 @@ public RequestedReviews(IReviewManager manager, UserPreferenceCache cache) public async Task OnGetAsync() { - var requestedReviews = await _manager.GetRequestedReviews(User.GetGitHubLogin()); - ActiveReviews = requestedReviews.Where(r => r.IsApproved == false).OrderByDescending(r => r.ApprovalRequestedOn); + var requestedReviews = await _manager.GetReviewsAssignedToUser(User.GetGitHubLogin()); + ActiveReviews = requestedReviews.Where(r => r.IsApproved == false).OrderByDescending(r => r.AssignedReviewers.Select(x => x.AssingedOn)); // Remove all approvals over a week old - ApprovedReviews = requestedReviews.Where(r => r.IsApproved == true).Where(r => r.ApprovalDate >= DateTime.Now.AddDays(-7)).OrderByDescending(r => r.ApprovalDate); + //ApprovedReviews = requestedReviews.Where(r => r.IsApproved == true).Where(r => r.ApprovalDate >= DateTime.Now.AddDays(-7)).OrderByDescending(r => r.ApprovalDate); return Page(); } } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml index 7830b504f5e..af17db3990e 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml @@ -1,631 +1,646 @@ -@page "{id}/{revisionId?}" -@model APIViewWeb.Pages.Assemblies.ReviewPageModel -@using APIViewWeb.Helpers -@using APIViewWeb.Models -@{ - Layout = "Shared/_Layout"; - ViewData["Title"] = Model.Review.DisplayName; - var userPreference = PageModelHelpers.GetUserPreference(Model._preferenceCache, User); - TempData["UserPreference"] = userPreference; - TempData["LanguageCssSafeName"] = Model.Review.GetLanguageCssSafeName(); - TempData["Comments"] = Model.Comments; - ViewBag.HasSections = (Model.CodeFile.LeafSections?.Count > 0) ? true : false; -} -@{ - var offCanvasClass = " show-offcanvas"; - if (userPreference.HideReviewPageOptions.HasValue && userPreference.HideReviewPageOptions == true) - offCanvasClass = String.Empty; -} -
-
-

+@page "{id}/{revisionId?}" +@model APIViewWeb.Pages.Assemblies.ReviewPageModel +@using APIViewWeb.Helpers +@using APIViewWeb.LeanModels; +@using APIViewWeb.Models +@{ + Layout = "Shared/_Layout"; + ViewData["Title"] = Model.ReviewContent.Review.PackageName; + var userPreference = PageModelHelpers.GetUserPreference(Model._preferenceCache, User); + TempData["UserPreference"] = userPreference; + TempData["LanguageCssSafeName"] = PageModelHelpers.GetLanguageCssSafeName(Model.ReviewContent.Review.Language); + TempData["Comments"] = Model.Comments; + ViewBag.HasSections = Model.ReviewContent.PageHasLoadableSections; +} +@{ + var offCanvasClass = " show-offcanvas"; + if (userPreference.HideReviewPageOptions.HasValue && userPreference.HideReviewPageOptions == true) + offCanvasClass = String.Empty; +} +

+
+

Approval  

- @{ - var approvalCollapseState = " show"; + @{ + var approvalCollapseState = " show"; if (Request.Cookies.ContainsKey("approveCollapse")) { - if (!Request.Cookies["approveCollapse"].Equals("shown")) - approvalCollapseState = String.Empty; + if (!Request.Cookies["approveCollapse"].Equals("shown")) + approvalCollapseState = String.Empty; } - } -
    -
  • -
    - - @if (Model.DiffRevision == null || Model.DiffRevision.Approvers.Count > 0) - { - @if (Model.Revision.Approvers.Contains(User.GetGitHubLogin())) + } +
      +
    • + + + @if (Model.ReviewContent.DiffAPIRevision == null || Model.ReviewContent.DiffAPIRevision.Approvers.Any()) + { + @if (Model.ReviewContent.ActiveAPIRevision.Approvers.Contains(User.GetGitHubLogin())) {
      -
      - } - else - { - @if (Model.ActiveConversations > 0 && Model.PreferredApprovers.Contains(User.GetGitHubLogin())) - { - Approves the current revision of the API -
      -
      + } + else + { + var isActiveRevisionAhead = (Model.ReviewContent.DiffAPIRevision == null)? true: Model.ReviewContent.ActiveAPIRevision.CreatedOn > Model.ReviewContent.DiffAPIRevision.CreatedOn; + @if (Model.ReviewContent.ActiveConversationsInActiveAPIRevision > 0 + && Model.ReviewContent.ActiveConversationsInSampleRevisions > 0 + && Model.ReviewContent.PreferredApprovers.Contains(User.GetGitHubLogin()) + && isActiveRevisionAhead) + { + Approves the current revision of the API +
      + -
      - } - else - { - @if (Model.PreferredApprovers.Contains(User.GetGitHubLogin())) - { - Approves the current revision of the API -
      - -
      - } - else +
+ } + else + { + @if (Model.ReviewContent.PreferredApprovers.Contains(User.GetGitHubLogin()) + && isActiveRevisionAhead) + { + Approves the current revision of the API +
+ +
+ } + else { -
+
-
- } - } - } - } - else - { - @if (Model.Revision.Approvers.Contains(User.GetGitHubLogin())) - { -
-
+ } + } + } + } + else + { + @if (Model.ReviewContent.ActiveAPIRevision.Approvers.Contains(User.GetGitHubLogin())) + { +
+ -
- } - else - { -
-
+ } + else + { +
+ -
- } - } - - @if (Model.Revision.Approvers.Count > 0) - { - - Approved by: - @{ - int i = 0; - } - @foreach (var approver in Model.Revision.Approvers) - { - @approver - @if (i < (@Model.Revision.Approvers.Count - 1)) - { - @Html.Raw(", ") - ; - } - i++; - } - +
+ } + } + + @{ + var approvers = Model.ReviewContent.ActiveAPIRevision.Approvers; + @if (approvers.Count() > 0) + { + + Approved by: + @{ + int i = 0; + } + @foreach (var approver in approvers) + { + @approver + @if (i < (approvers.Count() - 1)) + { + @Html.Raw(", ") + ; + } + i++; + } + + } + else + { + Current Revision Approval Pending + } } - else - { - Current Revision Approval Pending } - - @if (Model.Review.Revisions.LastOrDefault()?.Files.LastOrDefault()?.PackageName != null && - !(LanguageServiceHelpers.MapLanguageAliases(new List { "Swagger", "TypeSpec" })).Contains(Model.Review.Language)) - { - var approver = Model.Review.ApprovedForFirstReleaseBy ?? Model.Review.Revisions.LastOrDefault(r => r.IsApproved)?.Approvers?.FirstOrDefault(); - @if (!Model.Review.IsApprovedForFirstRelease) + + @if (Model.ReviewContent.ActiveAPIRevision.PackageName != null && + !(LanguageServiceHelpers.MapLanguageAliases(new List { "Swagger", "TypeSpec" })).Contains(Model.ReviewContent.Review.Language)) + { + var reviewIsApproved = Model.ReviewContent.Review.IsApproved; + var approver = (reviewIsApproved) ? "azure-sdk" : null; + + @if (!reviewIsApproved) { -
  • -
    - Approves First Release of the package -
    -
    -
    - First Revision Approval Pending -
  • - } - else - { -
  • - @if (approver != null) - { - Package has been approved for first release by @approver - } - else - { - Package has been approved for first release + + First Revision Approval Pending +
  • + } + else + { +
  • + @if (approver != null) + { + Package has been approved for first release by @approver } -
  • - } - } - + else + { + Package has been approved for first release + } + + } + } + @* Enables Button for generating AI Review - -

    - Generate AI Review   -

    - var generateAIReviewCollapseState = String.Empty; - if (Request.Cookies.ContainsKey("generateAIReviewCollapse")) - { - if (Request.Cookies["generateAIReviewCollapse"].Equals("shown")) - generateAIReviewCollapseState = " show"; - } -
      -
      - -
      -
    - *@ -

    - Request Reviewers   + +

    + Generate AI Review   +

    + var generateAIReviewCollapseState = String.Empty; + if (Request.Cookies.ContainsKey("generateAIReviewCollapse")) + { + if (Request.Cookies["generateAIReviewCollapse"].Equals("shown")) + generateAIReviewCollapseState = " show"; + } +
      +
      + +
      +
    + *@ +

    + Request Reviewers  

    - @{ - var requestReviewersCollapseState = String.Empty; - if (Request.Cookies.ContainsKey("requestReviewersCollapse")) - { - if (Request.Cookies["requestReviewersCollapse"].Equals("shown")) - requestReviewersCollapseState = " show"; + @{ + var requestReviewersCollapseState = String.Empty; + if (Request.Cookies.ContainsKey("requestReviewersCollapse")) + { + if (Request.Cookies["requestReviewersCollapse"].Equals("shown")) + requestReviewersCollapseState = " show"; } - } -
      - @{ - var anyChecked = false; - } -
      -
    • - -
        - @foreach (var approver in Model.PreferredApprovers) - { -
      • -
        - @if (Model.Review.RequestedReviewers != null && Model.Review.RequestedReviewers.Contains(approver)) - { - - - anyChecked = true; - } - else - { - - - } -
        -
      • - } -
      -
    • - - @if (anyChecked) - { - - } - else - { - - } - - + } +
        + @{ + var anyChecked = false; + } + +
      • +
          + @foreach (var approver in Model.ReviewContent.PreferredApprovers) + { +
        • +
          + @if (Model.ReviewContent.Review.AssignedReviewers != null && Model.ReviewContent.Review.AssignedReviewers.Where(a => a.AssingedTo == approver).Any()) + { + + + anyChecked = true; + } + else + { + + + } +
          +
        • + } +
        +
      • + + @if (anyChecked) + { + + } + else + { + + } + +
      • - -
      - @if (Model.Review.FilterType == ReviewType.PullRequest) - { - var associatedPRs = await Model.GetAssociatedPullRequest(); - @if (associatedPRs != null && associatedPRs.Count() > 0) - { - var associatedPRState = String.Empty; - if (Request.Cookies.ContainsKey("associatedPRCollapse")) - { - if (Request.Cookies["associatedPRCollapse"].Equals("shown")) - associatedPRState = " show"; - } -

      - Associated Pull Requests   -

      -
        + +
      + @if (Model.ReviewContent.ActiveAPIRevision.APIRevisionType == APIRevisionType.PullRequest) + { + var associatedPRs = await Model.GetAssociatedPullRequest(); + @if (associatedPRs != null && associatedPRs.Count() > 0) + { + var associatedPRState = String.Empty; + if (Request.Cookies.ContainsKey("associatedPRCollapse")) + { + if (Request.Cookies["associatedPRCollapse"].Equals("shown")) + associatedPRState = " show"; + } +

      + Associated Pull Requests   +

      +
        @foreach (var prModel in associatedPRs) - { - var url = $"https://github.com/{prModel.RepoName}/pull/{prModel.PullRequestNumber}"; - var txt = $"{prModel.RepoName}/{prModel.PullRequestNumber}"; -
      • - @txt -
      • } -
      - } - } - @if (Model.Review.FilterType == ReviewType.PullRequest) - { - var prsOfAssociatedReviews = await Model.GetPRsOfAssoicatedReviews(); - @if (prsOfAssociatedReviews != null && prsOfAssociatedReviews.Count() > 1) - { - var associatedReviewsState = String.Empty; - if (Request.Cookies.ContainsKey("associatedReviewsCollapse")) - { - if (Request.Cookies["associatedReviewsCollapse"].Equals("shown")) - associatedReviewsState = " show"; - } -

      - Associated Reviews   -

      -
        - @foreach (var pr in prsOfAssociatedReviews) { - if (pr.ReviewId != Model.Review.ReviewId) - { - var url = @Url.ActionLink("Review", "Assemblies", new - { - id = pr.ReviewId, - }); -
      • - @pr.Language/@pr.PackageName + var url = $"https://github.com/{prModel.RepoName}/pull/{prModel.PullRequestNumber}"; + var txt = $"{prModel.RepoName}/{prModel.PullRequestNumber}"; +
      • + @txt +
      • + } +
      + } + } + @if (Model.ReviewContent.ActiveAPIRevision.APIRevisionType == APIRevisionType.PullRequest) + { + var prsOfAssociatedReviews = await Model.GetPRsOfAssoicatedReviews(); + @if (prsOfAssociatedReviews != null && prsOfAssociatedReviews.Count() > 1) + { + var associatedReviewsState = String.Empty; + if (Request.Cookies.ContainsKey("associatedReviewsCollapse")) + { + if (Request.Cookies["associatedReviewsCollapse"].Equals("shown")) + associatedReviewsState = " show"; + } +

      + Associated Reviews   +

      +
        + @foreach (var pr in prsOfAssociatedReviews) + { + if (pr.ReviewId != Model.ReviewContent.Review.Id) + { + var url = @Url.ActionLink("Review", "Assemblies", new + { + id = pr.ReviewId, + }); +
      • + @pr.Language/@pr.PackageName
      • - } - } -
      - } - } -

      - Review Options   -

      - @{ - var reviewOptionsCollapseState = " show"; - if (Request.Cookies.ContainsKey("reviewOptionsCollapse")) - { - if (!Request.Cookies["reviewOptionsCollapse"].Equals("shown")) - reviewOptionsCollapseState = String.Empty; - } - } -
        -
      • -
        -
        - @if (Model.Review.GetUserEmail(User) != null) - { - if (Model.Review.IsUserSubscribed(User)) - { - - } - else - { - - } - } - else - { - - } - -
        -
        -
      • - @if (Model.Review.FilterType != ReviewType.Automatic) + } + } +
      + } + } +

      + Review Options   +

      + @{ + var reviewOptionsCollapseState = " show"; + if (Request.Cookies.ContainsKey("reviewOptionsCollapse")) { -
    • -
      -
      - @if (Model.Review.IsClosed) - { - - } - else - { - - } - -
      -
      -
    • - } -
    -

    - Page Settings   -

    - @{ - var pageSettingsCollapseState = String.Empty; - if (Request.Cookies.ContainsKey("pageSettingsCollapse")) - { - if (Request.Cookies["pageSettingsCollapse"].Equals("shown")) - pageSettingsCollapseState = " show"; - } - } -
      -
    • -
      - @if (userPreference.ShowComments == true) - { - - } - else - { - - } - -
      -
    • -
    • -
      - @if (userPreference.ShowSystemComments == true) - { - - } - else - { - - } - -
      -
    • -
    • -
      - @if (Model.ShowDocumentation) - { - + if (!Request.Cookies["reviewOptionsCollapse"].Equals("shown")) + reviewOptionsCollapseState = String.Empty; + } + } +
        +
      • +
        +
        + @if (PageModelHelpers.GetUserEmail(User) != null) + { + if (PageModelHelpers.IsUserSubscribed(User, Model.ReviewContent.Review.Subscribers)) + { + + } + else + { + + } + } + else + { + + } + +
        +
        +
      • +
      • +
        +
        + @if (Model.ReviewContent.Review.IsClosed) + { + + } + else + { + + } + +
        +
        +
      • +
      +

      + Page Settings   +

      + @{ + var pageSettingsCollapseState = String.Empty; + if (Request.Cookies.ContainsKey("pageSettingsCollapse")) + { + if (Request.Cookies["pageSettingsCollapse"].Equals("shown")) + pageSettingsCollapseState = " show"; + } + } +
        +
      • +
        + @if (userPreference.ShowComments == true) + { + + } + else + { + + } + +
        +
      • +
      • +
        + @if (userPreference.ShowSystemComments == true) + { + + } + else + { + } - else - { - - } - - + +
        +
      • +
      • + -
      • -
      • -
        - @if (userPreference.ShowHiddenApis == true) - { - - } - else - { - - } - -
        -
      • -
      • -
        - @if (userPreference.HideLineNumbers == true) - { - - } - else - { - - } - -
        -
      • -
      • -
        - @if (userPreference.HideLeftNavigation == true) - { - - } - else - { - - } - -
        -
      • - @if (!String.IsNullOrEmpty(Model.DiffRevisionId) && Model.Review.Language != "Swagger") - { -
      • -
        - @if (Model.ShowDiffOnly) - { - } - else - { - } - - - -
        -
      • - } -
      -
      -
    - -
    -
    -
    -
    - - - Revision: - - - - @if (@Model.PreviousRevisions.Any()) - { - @if (Model.DiffRevisionId != null) - { - - var urlValue = @Url.ActionLink("Review", "Assemblies", new - { - id = @Model.Review.ReviewId, - revisionId = @Model.Revision.RevisionId, - diffRevisionId = @Model.DiffRevisionId, - doc = @Model.ShowDocumentation, - diffOnly = @Model.ShowDiffOnly - }); - Diff With: - } - else - { - var urlValue = @Url.ActionLink("Review", "Assemblies", new - { - id = @Model.Review.ReviewId, - revisionId = @Model.Revision.RevisionId, - diffRevisionId = @Model.PreviousRevisions.Last().RevisionId, - doc = @Model.ShowDocumentation, - diffOnly = @Model.ShowDiffOnly - }); - Diff: - } - - } - @{ - var popOverContent = $"{Model.ActiveConversations} active revision threads.
    {Model.UsageSampleConversations} active usage sample threads.
    {Model.TotalActiveConversations} total active threads.
    " - + $"Current Revision: {@Model.Revision.DisplayName}"; - @if (Model.DiffRevisionId != null) - { - popOverContent += $"
    Current Diff: {@Model.DiffRevision?.DisplayName}"; - } - @if (Model.ActiveConversations > 0) - { - } - - } -
    -
    -
    -
    -
    -
    -
    - - +
    + +
  • +
    + @if (userPreference.ShowHiddenApis == true) + { + + } + else + { + + } + +
    +
  • +
  • +
    + @if (userPreference.HideLineNumbers == true) + { + + } + else + { + + } + +
    +
  • +
  • +
    + @if (userPreference.HideLeftNavigation == true) + { + + } + else + { + + } + +
    +
  • + @if (!String.IsNullOrEmpty(Model.DiffRevisionId) && Model.ReviewContent.Review.Language != "Swagger") + { +
  • +
    + @if (Model.ShowDiffOnly) + { + + } + else + { + + } + + + +
    +
  • + } + +
    +
    + +
    +
    +
    +
    + + + Revision: + + + + @if (@Model.ReviewContent.APIRevisionsGrouped.SelectMany(kvp => kvp.Value).Count() > 1) + { + var diffRevisionType = (Model.ReviewContent.APIRevisionsGrouped.ContainsKey("Automatic")) ? "Automatic" : Model.ReviewContent.APIRevisionsGrouped.Keys.First(); + @if (Model.DiffRevisionId != null) + { + diffRevisionType = Model.ReviewContent.DiffAPIRevision.APIRevisionType.ToString(); + + var urlValue = @Url.ActionLink("Review", "Assemblies", new + { + id = @Model.ReviewContent.Review.Id, + revisionId = @Model.ReviewContent.ActiveAPIRevision.Id, + diffRevisionId = @Model.DiffRevisionId, + doc = @Model.ShowDocumentation, + diffOnly = @Model.ShowDiffOnly + }); + Diff With: + } + else + { + + var diffRevisionForButton = Model.ReviewContent.APIRevisionsGrouped[diffRevisionType]; + var urlValue = @Url.ActionLink("Review", "Assemblies", new + { + id = @Model.ReviewContent.Review.Id, + revisionId = @Model.ReviewContent.ActiveAPIRevision.Id, + diffRevisionId = diffRevisionForButton.First().Id, + doc = @Model.ShowDocumentation, + diffOnly = @Model.ShowDiffOnly + }); + Diff: + } + + + } + @{ + var popOverContent = $"{Model.ReviewContent.ActiveConversationsInActiveAPIRevision} active revision threads.
    {Model.ReviewContent.ActiveConversationsInSampleRevisions} active usage sample threads.
    {Model.ReviewContent.TotalActiveConversiations} total active threads.
    " + + $"Current Revision: {PageModelHelpers.ResolveRevisionLabel(@Model.ReviewContent.ActiveAPIRevision)}"; + @if (Model.DiffRevisionId != null) + { + popOverContent += $"
    Current Diff: {PageModelHelpers.ResolveRevisionLabel(@Model.ReviewContent.DiffAPIRevision)}"; + } + @if (Model.ReviewContent.ActiveConversationsInActiveAPIRevision > 0) + { + + } + + } +
    +
    +
    +
    +
    +
    +
    + +
    -@{ - var mainContainerClass = " move-main-content-container-left"; - if (userPreference.HideReviewPageOptions.HasValue && userPreference.HideReviewPageOptions == true) - mainContainerClass = String.Empty; -} -
    -
    - @{ - var reviewLeftDisplay = String.Empty; - var reviewRightSize = "10"; - if (userPreference.HideLeftNavigation == true) - { - reviewLeftDisplay = "d-none"; - reviewRightSize = "12"; - } - - var reviewApprovedClass = (Model.Revision.Approvers.Count > 0) ? "review-approved" : "border rounded-1"; - } -
    -
    -
    - @if (Model.CodeFile != null) - { - - } -
    -
    -
    - -
    - - - @foreach (var line in Model.Lines) - { - - } - -
    -
    +@{ + var mainContainerClass = " move-main-content-container-left"; + if (userPreference.HideReviewPageOptions.HasValue && userPreference.HideReviewPageOptions == true) + mainContainerClass = String.Empty; +} +
    +
    + @{ + var reviewLeftDisplay = String.Empty; + var reviewRightSize = "10"; + if (userPreference.HideLeftNavigation == true) + { + reviewLeftDisplay = "d-none"; + reviewRightSize = "12"; + } + + var reviewApprovedClass = (Model.ReviewContent.ActiveAPIRevision.Approvers.Count() > 0) ? "review-approved" : "border rounded-1"; + } +
    +
    +
    + @if (Model.ReviewContent.Navigation != null) + { + + } +
    +
    +
    + +
    + + + @foreach (var line in Model.ReviewContent.codeLines) + { + + } + +
    +
    -
    - -
    + + +
    diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs index 23ad7c59e08..d01b1bee7ba 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs @@ -1,22 +1,23 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Reflection.Metadata.Ecma335; using System.Threading.Tasks; -using ApiView; -using APIView; -using APIView.DIff; -using APIView.Model; using APIViewWeb.Helpers; using APIViewWeb.Hubs; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; using APIViewWeb.Repositories; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.SignalR; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; +using Microsoft.TeamFoundation.Common; namespace APIViewWeb.Pages.Assemblies @@ -25,7 +26,8 @@ public class ReviewPageModel : PageModel { private static int REVIEW_DIFF_CONTEXT_SIZE = 3; private const string DIFF_CONTEXT_SEPERATOR = "
    .....
    "; - private readonly IReviewManager _manager; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; private readonly IPullRequestManager _pullRequestManager; private readonly IBlobCodeFileRepository _codeFileRepository; private readonly ICommentsManager _commentsManager; @@ -33,11 +35,11 @@ public class ReviewPageModel : PageModel public readonly UserPreferenceCache _preferenceCache; private readonly ICosmosUserProfileRepository _userProfileRepository; private readonly IConfiguration _configuration; - private readonly IHubContext _signalRHubContext; public ReviewPageModel( - IReviewManager manager, + IReviewManager reviewManager, + IAPIRevisionsManager reviewRevisionManager, IPullRequestManager pullRequestManager, IBlobCodeFileRepository codeFileRepository, ICommentsManager commentsManager, @@ -47,7 +49,8 @@ public ReviewPageModel( IConfiguration configuration, IHubContext signalRHub) { - _manager = manager; + _reviewManager = reviewManager; + _apiRevisionsManager = reviewRevisionManager; _pullRequestManager = pullRequestManager; _codeFileRepository = codeFileRepository; _commentsManager = commentsManager; @@ -58,209 +61,244 @@ public ReviewPageModel( _signalRHubContext = signalRHub; } - public ReviewModel Review { get; set; } - public ReviewRevisionModel Revision { get; set; } - public ReviewRevisionModel DiffRevision { get; set; } - public ReviewRevisionModel[] PreviousRevisions {get; set; } - public CodeFile CodeFile { get; set; } - public CodeLineModel[] Lines { get; set; } - public InlineDiffLine[] DiffLines { get; set; } + public ReviewContentModel ReviewContent { get; set; } public ReviewCommentsModel Comments { get; set; } - public HashSet TaggableUsers { get; set; } - public HashSet HeadingsOfSectionsWithDiff { get; set; } = new HashSet(); - - /// - /// The number of active conversations for this iteration - /// - public int ActiveConversations { get; set; } - - public int TotalActiveConversations { get; set; } - - public int UsageSampleConversations { get; set; } - [BindProperty(SupportsGet = true)] public string DiffRevisionId { get; set; } - // Flag to decide whether to include documentation [BindProperty(Name = "doc", SupportsGet = true)] public bool ShowDocumentation { get; set; } - [BindProperty(Name = "diffOnly", SupportsGet = true)] public bool ShowDiffOnly { get; set; } - public IEnumerable ReviewsForPackage { get; set; } = new List(); - - public readonly HashSet PreferredApprovers = new HashSet(); - + /// + /// Handler for loading page + /// + /// + /// + /// public async Task OnGetAsync(string id, string revisionId = null) { TempData["Page"] = "api"; - await GetReviewPageModelPropertiesAsync(id, revisionId); + await GetReviewPageModelPropertiesAsync(id, revisionId, DiffRevisionId, ShowDiffOnly); - if (!Review.Revisions.Any()) - { - return RedirectToPage("LegacyReview", new { id = id }); - } - var renderedCodeFile = await _codeFileRepository.GetCodeFileAsync(Revision); - CodeFile = renderedCodeFile.CodeFile; + ReviewContent = await PageModelHelpers.GetReviewContentAsync(configuration: _configuration, + reviewManager: _reviewManager, preferenceCache: _preferenceCache, userProfileRepository: _userProfileRepository, + reviewRevisionsManager: _apiRevisionsManager, commentManager: _commentsManager, codeFileRepository: _codeFileRepository, + signalRHubContext: _signalRHubContext, user: User, reviewId: id, revisionId: revisionId, diffRevisionId: DiffRevisionId, + showDocumentation: ShowDocumentation, showDiffOnly: ShowDiffOnly, diffContextSize: REVIEW_DIFF_CONTEXT_SIZE, + diffContextSeperator: DIFF_CONTEXT_SEPERATOR); - var fileDiagnostics = CodeFile.Diagnostics ?? Array.Empty(); - var fileHtmlLines = renderedCodeFile.Render(ShowDocumentation); - - if (DiffRevision != null) + if (ReviewContent == default(ReviewContentModel)) { - var previousRevisionFile = await _codeFileRepository.GetCodeFileAsync(DiffRevision); - - var previousHtmlLines = previousRevisionFile.RenderReadOnly(ShowDocumentation); - var previousRevisionTextLines = previousRevisionFile.RenderText(ShowDocumentation); - var fileTextLines = renderedCodeFile.RenderText(ShowDocumentation); - - var diffLines = InlineDiff.Compute( - previousRevisionTextLines, - fileTextLines, - previousHtmlLines, - fileHtmlLines); - - Lines = CreateLines(fileDiagnostics, diffLines, Comments); - if (Lines.Length == 0) + // Check if you can get review from legacy data + var legacyReview = await _reviewManager.GetLegacyReviewAsync(User, id); + if (legacyReview != null) { - var notifcation = new NotificationModel() { Message = "There is no diff between the two revisions.", Level = NotificatonLevel.Info }; - await _signalRHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveNotification", notifcation); - return Redirect(Request.Headers["referer"]); - } - } - else - { - Lines = CreateLines(fileDiagnostics, fileHtmlLines, Comments); - } + var legacyRevision = legacyReview.Revisions.FirstOrDefault(r => + !string.IsNullOrEmpty(r.Files[0].Language) && !string.IsNullOrEmpty(r.Files[0].PackageName)); - ActiveConversations = ComputeActiveConversations(fileHtmlLines, Comments); - TotalActiveConversations = Comments.Threads.Count(t => !t.IsResolved); - UsageSampleConversations = Comments.Threads.Count(t => t.Comments.FirstOrDefault()?.IsUsageSampleComment == true); - var filterPreference = _preferenceCache.GetFilterType(User.GetGitHubLogin(), Review.FilterType); - ReviewsForPackage = await _manager.GetReviewsAsync(Review.ServiceName, Review.PackageDisplayName, filterPreference); - - var approverConfig = _configuration["approvers"]; - if (!string.IsNullOrEmpty(approverConfig)) - { - foreach (var username in approverConfig.Split(",")) - { - if (username.Equals(User.GetGitHubLogin())) + if (legacyRevision == null) { - var userCache = _preferenceCache.GetUserPreferences(User).Result; - var langs = userCache.ApprovedLanguages.ToHashSet(); - if (!langs.Any()) - { - UserProfileModel user = await _userProfileRepository.TryGetUserProfileAsync(username); - langs = user.Languages; - userCache.ApprovedLanguages = langs; - _preferenceCache.UpdateUserPreference(userCache, User); - } - if (langs.Contains(Review.Language) || !langs.Any()) - { - PreferredApprovers.Add(username); - } + return NotFound(); } - else + + var review = await _reviewManager.GetReviewAsync(language: legacyRevision.Files[0].Language, + packageName: legacyRevision.Files[0].PackageName); + + if (review != null) { - UserProfileModel user = await _userProfileRepository.TryGetUserProfileAsync(username); - var langs = user.Languages; - if (langs.Contains(Review.Language) || !langs.Any()) - { - PreferredApprovers.Add(username); - } + var uri = Request.GetUri().ToString(); + uri = uri.Replace(id, review.Id); + return Redirect(uri); } } + else + { + return NotFound(); + } + } + + if (!ReviewContent.APIRevisionsGrouped.Any()) + { + return RedirectToPage("LegacyReview", new { id = id }); } return Page(); } + /// + /// Gets CodeLine Section for pages with collapsible sections (Swagger Pages) + /// + /// + /// + /// + /// + /// + /// + /// + /// public async Task OnGetCodeLineSectionAsync( string id, int sectionKey, int? sectionKeyA = null, int? sectionKeyB = null, string revisionId = null, string diffRevisionId = null, bool diffOnly = false) { + if (revisionId == null) + { + var apiRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId: id, apiRevisionType: APIRevisionType.Automatic); + revisionId = apiRevision.Id; + } await GetReviewPageModelPropertiesAsync(id, revisionId, diffRevisionId, diffOnly); - var renderedCodeFile = await _codeFileRepository.GetCodeFileAsync(Revision); - var fileDiagnostics = renderedCodeFile.CodeFile.Diagnostics ?? Array.Empty(); - CodeLine[] currentHtmlLines; + + var codeLines = await PageModelHelpers.GetCodeLineSectionAsync(user: User, reviewManager: _reviewManager, + apiRevisionsManager: _apiRevisionsManager, commentManager: _commentsManager, + codeFileRepository: _codeFileRepository, reviewId: id, sectionKey: sectionKey, revisionId: revisionId, + diffRevisionId: diffRevisionId, diffContextSize: REVIEW_DIFF_CONTEXT_SIZE, diffContextSeperator: DIFF_CONTEXT_SEPERATOR, + sectionKeyA: sectionKeyA, sectionKeyB: sectionKeyB + ); + var userPrefernce = await _preferenceCache.GetUserPreferences(User) ?? new UserPreferenceModel(); - if (DiffRevision != null) + TempData["CodeLineSection"] = codeLines; + TempData["UserPreference"] = userPrefernce; + return Partial("_CodeLinePartial", sectionKey); + } + + /// + /// Get Revisions Partial + /// + /// + /// + /// + /// + /// + public async Task OnGetAPIRevisionsPartialAsync(string reviewId, APIRevisionType apiRevisionType, bool showDoc = false, bool showDiffOnly = false) + { + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId); + revisions = revisions.Where(r => r.APIRevisionType == apiRevisionType).OrderByDescending(c => c.CreatedOn).ToList(); + (IEnumerable revisions, APIRevisionListItemModel activeRevision, APIRevisionListItemModel diffRevision, bool forDiff, bool showDocumentation, bool showDiffOnly) revisionSelectModel = ( + revisions: revisions, + activeRevision: default(APIRevisionListItemModel), + diffRevision: default(APIRevisionListItemModel), + forDiff: false, + showDocumentation: showDoc, + showDiffOnly: showDiffOnly + ); + return Partial("_RevisionSelectPickerPartial", revisionSelectModel); + } + + /// + /// Get Diff Revisions Partial + /// + /// + /// + /// + /// + /// + /// + public async Task OnGetAPIDiffRevisionsPartialAsync(string reviewId, string apiRevisionId, APIRevisionType apiRevisionType, bool showDoc = false, bool showDiffOnly = false) + { + var apiRevisions = await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId); + if (apiRevisions.IsNullOrEmpty()) { - InlineDiffLine[] diffLines; - var previousRevisionFile = await _codeFileRepository.GetCodeFileAsync(DiffRevision); + var notifcation = new NotificationModel() { Message = $"This review has no valid apiRevisons", Level = NotificatonLevel.Warning }; + await _signalRHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveNotification", notifcation); + } - if (sectionKeyA != null && sectionKeyB != null) - { - var currentRootNode = renderedCodeFile.GetCodeLineSectionRoot((int)sectionKeyA); - var previousRootNode = previousRevisionFile.GetCodeLineSectionRoot((int)sectionKeyB); - var diffSectionRoot = _manager.ComputeSectionDiff(previousRootNode, currentRootNode, previousRevisionFile, renderedCodeFile); - diffLines = renderedCodeFile.GetDiffCodeLineSection(diffSectionRoot); - } - else if (sectionKeyA != null) - { - currentHtmlLines = renderedCodeFile.GetCodeLineSection((int)sectionKeyA); - var previousRevisionHtmlLines = new CodeLine[] { }; - var previousRevisionTextLines = new CodeLine[] { }; - var currentRevisionTextLines = renderedCodeFile.GetCodeLineSection((int)sectionKeyA, renderType: RenderType.Text); - diffLines = InlineDiff.Compute( - previousRevisionTextLines, - currentRevisionTextLines, - previousRevisionHtmlLines, - currentHtmlLines); - } - else - { - currentHtmlLines = new CodeLine[] { }; - var previousRevisionHtmlLines = previousRevisionFile.GetCodeLineSection((int)sectionKeyB, RenderType.ReadOnly); - var previousRevisionTextLines = previousRevisionFile.GetCodeLineSection((int)sectionKeyB, renderType: RenderType.Text); - var currentRevisionTextLines = new CodeLine[] { }; - diffLines = InlineDiff.Compute( - previousRevisionTextLines, - currentRevisionTextLines, - previousRevisionHtmlLines, - currentHtmlLines); - } - Lines = CreateLines(fileDiagnostics, diffLines, Comments, true); + APIRevisionListItemModel activeRevision = default(APIRevisionListItemModel); + + if (!Guid.TryParse(apiRevisionId, out _)) + { + activeRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(reviewId, apiRevisions); } else { - currentHtmlLines = renderedCodeFile.GetCodeLineSection(sectionKey); - Lines = CreateLines(fileDiagnostics, currentHtmlLines, Comments, true); + activeRevision = apiRevisions.FirstOrDefault(r => r.Id == apiRevisionId); } - TempData["CodeLineSection"] = Lines; - TempData["UserPreference"] = userPrefernce; - return Partial("_CodeLinePartial", sectionKey); + + var revisionsForDiff = apiRevisions.Where(r => r.APIRevisionType == apiRevisionType && r.Id != activeRevision.Id).OrderByDescending(c => c.CreatedOn).ToList(); + + (IEnumerable revisions, APIRevisionListItemModel activeRevision, APIRevisionListItemModel diffRevision, bool forDiff, bool showDocumentation, bool showDiffOnly) revisionSelectModel = ( + revisions: revisionsForDiff, + activeRevision: activeRevision, + diffRevision: default(APIRevisionListItemModel), + forDiff: true, + showDocumentation: showDoc, + showDiffOnly: showDiffOnly + ); + return Partial("_RevisionSelectPickerPartial", revisionSelectModel); } + /// + /// Toggle Review State + /// + /// + /// public async Task OnPostToggleClosedAsync(string id) { - await _manager.ToggleIsClosedAsync(User, id); - + await _reviewManager.ToggleReviewIsClosedAsync(User, id); return RedirectToPage(new { id = id }); } + /// + /// Subscribe or UnSubscribe to a Review + /// + /// + /// public async Task OnPostToggleSubscribedAsync(string id) { await _notificationManager.ToggleSubscribedAsync(User, id); return RedirectToPage(new { id = id }); } - public async Task OnPostToggleApprovalAsync(string id, string revisionId) + /// + /// Approve or Revert Approval for a Review + /// + /// + /// + /// + public async Task OnPostToggleReviewApprovalAsync(string id, string revisionId) { - await _manager.ToggleApprovalAsync(User, id, revisionId); + await _reviewManager.ToggleReviewApprovalAsync(User, id, revisionId); return RedirectToPage(new { id = id }); } + /// + /// Approve or Revert Approval for a Revision + /// + /// + /// + /// + public async Task OnPostToggleAPIRevisionApprovalAsync(string id, string revisionId) + { + var updateReview = await _apiRevisionsManager.ToggleAPIRevisionApprovalAsync(User, id, revisionId); + if (updateReview) + { + await OnPostToggleReviewApprovalAsync(id, revisionId); + } + return RedirectToPage(new { id = id }); + } + + /// + /// Request Reviewers for a Review Revision + /// + /// + /// + /// public async Task OnPostRequestReviewersAsync(string id, HashSet reviewers) { - await _manager.RequestApproversAsync(User, id, reviewers); + await _reviewManager.AssignReviewersToReviewAsync(User, id, reviewers); await _notificationManager.NotifyApproversOfReview(User, id, reviewers); return RedirectToPage(new { id = id }); } - + /// + /// Get Routing Data for a Review + /// + /// + /// + /// + /// + /// public Dictionary GetRoutingData(string diffRevisionId = null, bool? showDiffOnly = null, bool? showDocumentation = null, string revisionId = null) { var routingData = new Dictionary(); @@ -270,209 +308,38 @@ public Dictionary GetRoutingData(string diffRevisionId = null, b routingData["diffOnly"] = (showDiffOnly ?? false).ToString(); return routingData; } - + /// + /// Get Pull Requests for a Review + /// + /// public async Task> GetAssociatedPullRequest() { - return await _pullRequestManager.GetPullRequestsModel(Review.ReviewId); + return await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id); } + /// + /// Get PR of Associated Reviews + /// + /// public async Task> GetPRsOfAssoicatedReviews() { - var creatingPR = (await _pullRequestManager.GetPullRequestsModel(Review.ReviewId)).FirstOrDefault(); - return await _pullRequestManager.GetPullRequestsModel(creatingPR.PullRequestNumber, creatingPR.RepoName);; + var creatingPR = (await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id)).FirstOrDefault(); + return await _pullRequestManager.GetPullRequestsModelAsync(creatingPR.PullRequestNumber, creatingPR.RepoName);; } + /// + /// Get Review Page Model Properties + /// + /// + /// + /// + /// + /// private async Task GetReviewPageModelPropertiesAsync(string id, string revisionId = null, string diffRevisionId = null, bool diffOnly = false) { - Review = await _manager.GetReviewAsync(User, id); - TaggableUsers = _commentsManager.GetTaggableUsers(); Comments = await _commentsManager.GetReviewCommentsAsync(id); - Revision = Review.Revisions.Last(); - if (revisionId != null) - { - var revision = Review.Revisions.Where(r => r.RevisionId == revisionId); - if (revision.Count() == 1) - { - Revision = revision.Single(); - } - else - { - var notifcation = new NotificationModel() { Message = $"A revision with ID {revisionId} does not exist for this review.", Level = NotificatonLevel.Warning }; - await _signalRHubContext.Clients.Group(User.GetGitHubLogin()).SendAsync("RecieveNotification", notifcation); - } - - } - - PreviousRevisions = Review.Revisions.TakeWhile(r => r != Revision).ToArray(); DiffRevisionId = (DiffRevisionId == null) ? diffRevisionId : DiffRevisionId; ShowDiffOnly = (ShowDiffOnly == false) ? diffOnly : ShowDiffOnly; - DiffRevision = DiffRevisionId != null ? - PreviousRevisions.Single(r => r.RevisionId == DiffRevisionId) : - DiffRevision; - HeadingsOfSectionsWithDiff = (DiffRevision != null && DiffRevision.HeadingsOfSectionsWithDiff.ContainsKey(Revision.RevisionId)) ? - DiffRevision.HeadingsOfSectionsWithDiff[Revision.RevisionId] : new HashSet(); - } - - private InlineDiffLine[] CreateDiffOnlyLines(InlineDiffLine[] lines) - { - var filteredLines = new List>(); - int lastAddedLine = -1; - for (int i = 0; i < lines.Count(); i++) - { - if (lines[i].Kind != DiffLineKind.Unchanged) - { - // Find starting index for pre context - int preContextIndx = Math.Max(lastAddedLine + 1, i - REVIEW_DIFF_CONTEXT_SIZE); - if (preContextIndx < i) - { - // Add sepearator to show skipping lines. for e.g. ..... - if (filteredLines.Count > 0) - { - filteredLines.Add(new InlineDiffLine(new CodeLine(DIFF_CONTEXT_SEPERATOR, null, null), DiffLineKind.Unchanged)); - } - - while (preContextIndx < i) - { - filteredLines.Add(lines[preContextIndx]); - preContextIndx++; - } - } - //Add changed line - filteredLines.Add(lines[i]); - lastAddedLine = i; - - // Add post context - int contextStart = i +1, contextEnd = i + REVIEW_DIFF_CONTEXT_SIZE; - while (contextStart <= contextEnd && contextStart < lines.Count() && lines[contextStart].Kind == DiffLineKind.Unchanged) - { - filteredLines.Add(lines[contextStart]); - lastAddedLine = contextStart; - contextStart++; - } - } - } - return filteredLines.ToArray(); - } - - private CodeLineModel[] CreateLines(CodeDiagnostic[] diagnostics, InlineDiffLine[] lines, ReviewCommentsModel comments, bool hideCommentRows = false) - { - if (ShowDiffOnly) - { - lines = CreateDiffOnlyLines(lines); - if (lines.Length == 0) - { - return Array.Empty(); - } - } - List documentedByLines = new List(); - int lineNumberExcludingDocumentation = 0; - int diffSectionId = 0; - - return lines.Select( - (diffLine, index) => - { - if (diffLine.Line.IsDocumentation) - { - // documentedByLines must include the index of a line, assuming that documentation lines are counted - documentedByLines.Add(++index); - return new CodeLineModel( - kind: diffLine.Kind, - codeLine: diffLine.Line, - commentThread: comments.TryGetThreadForLine(diffLine.Line.ElementId, out var thread, hideCommentRows) ? - thread : - null, - diagnostics: diffLine.Kind != DiffLineKind.Removed ? - diagnostics.Where(d => d.TargetId == diffLine.Line.ElementId).ToArray() : - Array.Empty(), - lineNumber: lineNumberExcludingDocumentation, - documentedByLines: new int[] { }, - isDiffView: true, - diffSectionId: diffLine.Line.SectionKey != null ? ++diffSectionId : null, - otherLineSectionKey: diffLine.Kind == DiffLineKind.Unchanged ? diffLine.OtherLine.SectionKey : null, - headingsOfSectionsWithDiff: HeadingsOfSectionsWithDiff, - isSubHeadingWithDiffInSection: diffLine.IsHeadingWithDiffInSection - ); - } - else - { - CodeLineModel c = new CodeLineModel( - kind: diffLine.Kind, - codeLine: diffLine.Line, - commentThread: diffLine.Kind != DiffLineKind.Removed && - comments.TryGetThreadForLine(diffLine.Line.ElementId, out var thread, hideCommentRows) ? - thread : - null, - diagnostics: diffLine.Kind != DiffLineKind.Removed ? - diagnostics.Where(d => d.TargetId == diffLine.Line.ElementId).ToArray() : - Array.Empty(), - lineNumber: diffLine.Line.LineNumber ?? ++lineNumberExcludingDocumentation, - documentedByLines: documentedByLines.ToArray(), - isDiffView: true, - diffSectionId: diffLine.Line.SectionKey != null ? ++diffSectionId : null, - otherLineSectionKey: diffLine.Kind == DiffLineKind.Unchanged ? diffLine.OtherLine.SectionKey : null, - headingsOfSectionsWithDiff: HeadingsOfSectionsWithDiff, - isSubHeadingWithDiffInSection: diffLine.IsHeadingWithDiffInSection - ); - documentedByLines.Clear(); - return c; - } - }).ToArray(); - } - - private CodeLineModel[] CreateLines(CodeDiagnostic[] diagnostics, CodeLine[] lines, ReviewCommentsModel comments, bool hideCommentRows = false) - { - List documentedByLines = new List(); - int lineNumberExcludingDocumentation = 0; - return lines.Select( - (line, index) => - { - if (line.IsDocumentation) - { - // documentedByLines must include the index of a line, assuming that documentation lines are counted - documentedByLines.Add(++index); - return new CodeLineModel( - DiffLineKind.Unchanged, - line, - comments.TryGetThreadForLine(line.ElementId, out var thread, hideCommentRows) ? thread : null, - diagnostics.Where(d => d.TargetId == line.ElementId).ToArray(), - lineNumberExcludingDocumentation, - new int[] {} - ); - } - else - { - CodeLineModel c = new CodeLineModel( - DiffLineKind.Unchanged, - line, - comments.TryGetThreadForLine(line.ElementId, out var thread, hideCommentRows) ? thread : null, - diagnostics.Where(d => d.TargetId == line.ElementId).ToArray(), - line.LineNumber ?? ++lineNumberExcludingDocumentation, - documentedByLines.ToArray() - ); - documentedByLines.Clear(); - return c; - } - }).ToArray(); - } - - private int ComputeActiveConversations(CodeLine[] lines, ReviewCommentsModel comments) - { - int activeThreads = 0; - foreach (CodeLine line in lines) - { - if (string.IsNullOrEmpty(line.ElementId)) - { - continue; - } - - // if we have comments for this line and the thread has not been resolved. - // Add "&& !thread.Comments.First().IsUsageSampleComment()" to exclude sample comments from being counted (This also prevents the popup before approval) - if (comments.TryGetThreadForLine(line.ElementId, out CommentThreadModel thread) && !thread.IsResolved) - { - activeThreads++; - } - } - return activeThreads; } } } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml index 6c886b735e2..f46fb96f4c8 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml @@ -1,6 +1,7 @@ @page "{id?}" @model APIViewWeb.Pages.Assemblies.RevisionsPageModel @using APIViewWeb.Helpers +@using APIViewWeb.LeanModels; @using APIViewWeb.Models @{ Layout = "Shared/_Layout"; @@ -11,7 +12,7 @@
    - +
    @@ -23,14 +24,11 @@
    - @if (Model.Review.FilterType == ReviewType.Manual) - { -
    -
    - -
    +
    +
    +
    - } +
    diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml.cs index 46e51b7fc23..bc1a0e0414e 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Models; using APIViewWeb.Repositories; using Microsoft.AspNetCore.Http; @@ -11,17 +14,23 @@ namespace APIViewWeb.Pages.Assemblies { public class RevisionsPageModel : PageModel { - private readonly IReviewManager _manager; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; public readonly UserPreferenceCache _preferenceCache; public RevisionsPageModel( - IReviewManager manager, UserPreferenceCache preferenceCache) + IReviewManager manager, + IAPIRevisionsManager reviewRevisionsManager, + UserPreferenceCache preferenceCache) { - _manager = manager; + _reviewManager = manager; + _apiRevisionsManager = reviewRevisionsManager; _preferenceCache = preferenceCache; } - public ReviewModel Review { get; set; } + public ReviewListItemModel Review { get; set; } + public APIRevisionListItemModel LatestAPIRevision { get; set; } + public Dictionary> APIRevisions { get; set; } [FromForm] public string Label { get; set; } @@ -36,7 +45,10 @@ public async Task OnGetAsync(string id) { TempData["Page"] = "revisions"; - Review = await _manager.GetReviewAsync(User, id); + Review = await _reviewManager.GetReviewAsync(User, id); + LatestAPIRevision = await _apiRevisionsManager.GetLatestAPIRevisionsAsync(Review.Id); + var revisions = await _apiRevisionsManager.GetAPIRevisionsAsync(Review.Id); + APIRevisions = revisions.GroupBy(r => r.APIRevisionType).ToDictionary(r => r.Key.ToString(), r => r.ToList()); return Page(); } @@ -51,11 +63,11 @@ public async Task OnPostUploadAsync(string id, [FromForm] IFormFi if (upload != null) { var openReadStream = upload.OpenReadStream(); - await _manager.AddRevisionAsync(User, id, upload.FileName, Label, openReadStream, language: Language); + await _apiRevisionsManager.AddAPIRevisionAsync(User, id, APIRevisionType.Manual, upload.FileName, Label, openReadStream, language: Language); } else { - await _manager.AddRevisionAsync(User, id, FilePath, Label, null); + await _apiRevisionsManager.AddAPIRevisionAsync(User, id, APIRevisionType.Manual, FilePath, Label, null); } return RedirectToPage(); @@ -63,14 +75,14 @@ public async Task OnPostUploadAsync(string id, [FromForm] IFormFi public async Task OnPostDeleteAsync(string id, string revisionId) { - await _manager.DeleteRevisionAsync(User, id, revisionId); + await _apiRevisionsManager.SoftDeleteAPIRevisionAsync(User, id, revisionId); return RedirectToPage(); } public async Task OnPostRenameAsync(string id, string revisionId, string newLabel) { - await _manager.UpdateRevisionLabelAsync(User, id, revisionId, newLabel); + await _apiRevisionsManager.UpdateAPIRevisionLabelAsync(User, revisionId, newLabel); return RedirectToPage(); } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml index 847ed0a55fe..2783293f98a 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml @@ -1,6 +1,7 @@ @page "{id?}/{revisionId?}" @model APIViewWeb.Pages.Assemblies.UsageSamplePageModel @using APIViewWeb.Helpers +@using APIViewWeb.LeanModels; @using APIViewWeb.Models @{ Layout = "Shared/_Layout"; @@ -8,13 +9,13 @@ var userPreference = PageModelHelpers.GetUserPreference(Model._preferenceCache, User); TempData["UserPreference"] = userPreference; } -@if (Model.Samples == null || Model.Sample.FileId == "Bad Deployment" || Model.Sample.FileId == "File Content Missing") // If the samples container does not exist +@if (!Model.SampleRevisions.Any() || Model.ActiveSampleRevision.FileId == "Bad Deployment" || Model.ActiveSampleRevision.FileId == "File Content Missing") // If the samples container does not exist {
    - +
    @@ -48,13 +49,13 @@ else { var urlValue = @Url.ActionLink("Samples", "Assemblies", new { - id = @Model.Review.ReviewId, + id = @Model.Review.Id, revisionId = @revision.FileId, }); - - string revTitle = revision.RevisionTitle == null ? "Rev " + revision.RevisionNumber : "Rev " + revision.RevisionNumber + " - " + revision.RevisionTitle; - if (revision.FileId == Model.Sample.FileId) + string revTitle = revision.Title == null ? $"Created On { revision.CreatedOn }" : $"Created On: { revision.Title }"; + + if (revision.FileId == Model.ActiveSampleRevision.FileId) { } @@ -68,7 +69,7 @@ else @if (Model.SampleContent.Length > 0) { - @if (Model.Sample.CreatedBy == User.GetGitHubLogin()) + @if (Model.ActiveSampleRevision.CreatedBy == User.GetGitHubLogin()) { } @@ -84,7 +85,7 @@ else
    -
    +
    @@ -106,7 +107,7 @@ else
    - Sample uploaded by @Model.Sample.CreatedBy + Sample uploaded by @Model.ActiveSampleRevision.CreatedBy }
    @@ -144,15 +145,7 @@ else
    - - @if (Model.SampleRevisions == null) - { - - } - else - { - - } +
    @@ -164,8 +157,7 @@ else

    - - +
    @@ -203,10 +195,10 @@ else
    - + - - + + @@ -238,9 +230,8 @@ else
    - + -

    This will create a new revision of the sample.

    diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml.cs index 3893cc660af..4b91acc3ecd 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Samples.cshtml.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using ApiView; using APIView; +using APIViewWeb.LeanModels; using APIViewWeb.Managers; using APIViewWeb.Models; using APIViewWeb.Repositories; @@ -12,35 +13,32 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Azure.Cosmos; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.VisualStudio.Services.FileContainer; namespace APIViewWeb.Pages.Assemblies { public class UsageSamplePageModel : PageModel { - private readonly IUsageSampleManager _samplesManager; + private readonly ISamplesRevisionsManager _samplesRevisionsManager; private readonly IReviewManager _reviewManager; private readonly ICommentsManager _commentsManager; public readonly UserPreferenceCache _preferenceCache; private readonly IAuthorizationService _authorizationService; - public ReviewModel Review { get; private set; } - public UsageSampleRevisionModel Sample { get; private set; } - public IEnumerable SampleRevisions { get; private set; } - public UsageSampleModel Samples { get; private set; } + public ReviewListItemModel Review { get; private set; } + public SamplesRevisionModel ActiveSampleRevision { get; private set; } + public IEnumerable SampleRevisions { get; private set; } public CodeLineModel[] SampleContent { get; set; } public ReviewCommentsModel Comments { get; set; } public string SampleOriginal { get; set; } public UsageSamplePageModel( - IUsageSampleManager samplesManager, + ISamplesRevisionsManager samplesRevisionsManager, IReviewManager reviewManager, ICommentsManager commentsManager, UserPreferenceCache preferenceCache, IAuthorizationService authorizationService) { - _samplesManager = samplesManager; + _samplesRevisionsManager = samplesRevisionsManager; _reviewManager = reviewManager; _commentsManager = commentsManager; _preferenceCache = preferenceCache; @@ -48,7 +46,7 @@ public UsageSamplePageModel( } [FromForm] - public UsageSampleUploadModel Upload { get; set; } + public SamplesRevisionUploadModel Upload { get; set; } public async Task OnGetAsync(string id, string revisionId = null) { @@ -60,54 +58,49 @@ public async Task OnGetAsync(string id, string revisionId = null) // This try-catch is for the case that the deployment is set up incorrectly for usage samples try { - Samples = (await _samplesManager.GetReviewUsageSampleAsync(id)).FirstOrDefault(); - if (Samples != null) - { - SampleRevisions = Samples.Revisions.OrderByDescending(e => e.RevisionNumber).Where(e => !e.RevisionIsDeleted); - } + SampleRevisions = await _samplesRevisionsManager.GetSamplesRevisionsAsync(Review.Id); if (SampleRevisions != null && SampleRevisions.Any()) { if (revisionId != null) { - Sample = SampleRevisions.Where(e => e.FileId == revisionId).First(); + ActiveSampleRevision = SampleRevisions.Where(s => s.Id == revisionId).First(); } else { - Sample = SampleRevisions.First(); + ActiveSampleRevision = SampleRevisions.First(); } - Comments = await _commentsManager.GetUsageSampleCommentsAsync(Samples.ReviewId); - SampleContent = ParseLines(Sample.FileId, Comments).Result; - SampleOriginal = await _samplesManager.GetUsageSampleContentAsync(Sample.OriginalFileId); + Comments = await _commentsManager.GetUsageSampleCommentsAsync(Review.Id); + SampleContent = ParseLines(ActiveSampleRevision.FileId, Comments).Result; + SampleOriginal = await _samplesRevisionsManager.GetSamplesRevisionContentAsync(ActiveSampleRevision.OriginalFileId); Upload.updateString = SampleOriginal; if (SampleContent == null) { // Potentially bad blob setup, potentially erroneous file fetch - Sample.FileId = "File Content Missing"; + ActiveSampleRevision.FileId = "File Content Missing"; } } else { // Tests the blob response with a dummy file id - string blobTest = await _samplesManager.GetUsageSampleContentAsync("abdc"); + string blobTest = await _samplesRevisionsManager.GetSamplesRevisionContentAsync("abdc"); if (blobTest == "Bad Blob") { throw new CosmosException(null, System.Net.HttpStatusCode.NotFound, 0, null, 0.0); // Error does not matter, only type, to ensure clean error page. } // No samples. - SampleRevisions = SampleRevisions ?? new List(); - Sample = new UsageSampleRevisionModel(null, -1); + SampleRevisions = SampleRevisions ?? new List(); + ActiveSampleRevision = new SamplesRevisionModel(); SampleContent = Array.Empty(); - Samples = Samples ?? new UsageSampleModel(Review.ReviewId); } } catch (CosmosException) { // Error gracefully - Sample = new UsageSampleRevisionModel(null, -1); - Sample.FileId = "Bad Deployment"; + ActiveSampleRevision = new SamplesRevisionModel(); + ActiveSampleRevision.FileId = "Bad Deployment"; } return Page(); @@ -125,41 +118,39 @@ public async Task OnPostUploadAsync() var file = Upload.File; string sampleString = Upload.sampleString; string reviewId = Upload.ReviewId; - int newRevNum = Upload.RevisionNumber+1; string revisionTitle = Upload.RevisionTitle; if (file != null) { using (var openReadStream = file.OpenReadStream()) { - await _samplesManager.UpsertReviewUsageSampleAsync(User, reviewId, openReadStream, newRevNum, revisionTitle, file.FileName); + await _samplesRevisionsManager.UpsertSamplesRevisionsAsync(User, reviewId, openReadStream, revisionTitle, file.FileName); } } else if (sampleString != null) { - await _samplesManager.UpsertReviewUsageSampleAsync(User, reviewId, sampleString, newRevNum, revisionTitle); + await _samplesRevisionsManager.UpsertSamplesRevisionsAsync(User, reviewId, sampleString, revisionTitle); } else if (Upload.Updating) { - await _samplesManager.UpsertReviewUsageSampleAsync(User, reviewId, Upload.updateString, newRevNum, revisionTitle); + await _samplesRevisionsManager.UpsertSamplesRevisionsAsync(User, reviewId, Upload.updateString, revisionTitle); } else if (Upload.Deleting) { - await _samplesManager.DeleteUsageSampleAsync(User, reviewId, Upload.FileId, Upload.SampleId); + await _samplesRevisionsManager.DeleteSamplesRevisionAsync(User, reviewId, Upload.SampleId); } - return RedirectToPage(); } private async Task ParseLines(string fileId, ReviewCommentsModel comments) { - if(Sample.FileId == null) + if(ActiveSampleRevision.FileId == null) { return new CodeLineModel[0]; } - string rawContent = (await _samplesManager.GetUsageSampleContentAsync(fileId)); + string rawContent = (await _samplesRevisionsManager.GetSamplesRevisionContentAsync(fileId)); if (rawContent == null || rawContent.Equals("Bad Blob")) { return null; // should only occur if there is a blob error or the file is removed by other means @@ -192,8 +183,8 @@ private async Task ParseLines(string fileId, ReviewCommentsMode // Allows the indent to work correctly for spacing purposes lineContent = "
       " + lineContent + "
    "; - var line = new CodeLine(lineContent, Sample.FileId + "-line-" + (i+1-skipped).ToString() , ""); - comments.TryGetThreadForLine(Sample.FileId + "-line-" + (i+1-skipped).ToString(), out var thread); + var line = new CodeLine(lineContent, ActiveSampleRevision.FileId + "-line-" + (i+1-skipped).ToString() , ""); + comments.TryGetThreadForLine(ActiveSampleRevision.FileId + "-line-" + (i+1-skipped).ToString(), out var thread); lines[i] = new CodeLineModel(APIView.DIff.DiffLineKind.Unchanged, line, thread, cd, i+1-skipped, new int[0]); } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/_CodeLine.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/_CodeLine.cshtml index f35ef228123..e88c1609059 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/_CodeLine.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/_CodeLine.cshtml @@ -155,7 +155,7 @@ @{ var iconCommentClass = "icon-comments invisible"; var comments = TempData["Comments"] as ReviewCommentsModel; - if (sectionKey != null && comments.Threads.Count() > 0) + if (sectionKey != null && comments != null && comments.Threads.Count() > 0) { if (comments.Threads.Any(c => c.LineClass != null && c.LineClass.Contains($"code-line-section-content-{sectionKey}"))) { diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml index c449139709f..6557cd1a48a 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml @@ -1,17 +1,17 @@ -@using APIViewWeb.Models -@model CommentModel +@using APIViewWeb.LeanModels; +@model CommentItemModel -
    +
    - @Model.Username - + @Model.CreatedBy +
    - +
    - - @if (Model.Comment.Contains("\r\n")) + + @if (Model.CommentText.Contains("\r\n")) { - var cmt = Model.Comment.Split("\r\n"); + var cmt = Model.CommentText.Split("\r\n"); string codeBlockString = ""; bool codeBlock = false; bool newLineAdded = true; @@ -95,7 +95,7 @@ } else { - @Html.FormatAsMarkdown(Model.Comment) + @Html.FormatAsMarkdown(Model.CommentText) }
    diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadPartial.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadPartial.cshtml index c257f40bb02..bc9c8df5539 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadPartial.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadPartial.cshtml @@ -14,7 +14,7 @@ - @if (Model.IsResolved && Model.Comments.First().Username == User.GetGitHubLogin()) + @if (Model.IsResolved && Model.Comments.First().CreatedBy == User.GetGitHubLogin()) { @:This thread is marked resolved by @Model.ResolvedBy (show)
    @@ -27,42 +27,11 @@ }
    - @{ - var groupedComments = Model.Comments.Where(x => !String.IsNullOrEmpty(x.GroupNo)); - var unGroupedComments = Model.Comments.Where(x => String.IsNullOrEmpty(x.GroupNo)); - } - @if (unGroupedComments.Any()) + @foreach (var comment in Model.Comments) { - @foreach (var comment in unGroupedComments) - { - - } - - } - - @if (groupedComments.Any()) - { - var comments = groupedComments.OrderBy(x => Convert.ToInt16(x.GroupNo)).ToArray(); - for (int i = 0; i < comments.Length; i++) - { - var comment = comments[i]; - var rowAnchor = $"{Model.LineId}-tr-{comment.GroupNo}"; - - if (i == 0 || (Convert.ToInt16(comment.GroupNo) > Convert.ToInt16(comments[i - 1].GroupNo))) - { - - - ROW-@comment.GroupNo - - - } - - if (i == comments.Length - 1 || (Convert.ToInt16(comments[i + 1].GroupNo) > Convert.ToInt16(comment.GroupNo))) - { - - } - } + } +
    @if (Model.IsResolved) @@ -76,7 +45,7 @@

      Please ask - @Model.Comments.First().Username + @Model.Comments.First().CreatedBy (or an APIView approver) to unresolve this conversation.

    } @@ -98,7 +67,7 @@

      Please ask - @Model.Comments.First().Username + @Model.Comments.First().CreatedBy (or an APIView approver) to resolve this conversation.

    diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml index 0dfc28e6e95..84a13bc77b2 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml @@ -49,11 +49,6 @@ - }

    Page Settings   @@ -607,6 +593,13 @@

    + @if (!string.IsNullOrEmpty(Model.NotificationMessage)) + { + + } @foreach (var line in Model.ReviewContent.codeLines) diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs index d01b1bee7ba..a6caf9aee35 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using APIViewWeb.Helpers; using APIViewWeb.Hubs; @@ -18,6 +19,7 @@ using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.TeamFoundation.Common; +using Microsoft.VisualStudio.Services.ClientNotification; namespace APIViewWeb.Pages.Assemblies @@ -70,6 +72,8 @@ public ReviewPageModel( public bool ShowDocumentation { get; set; } [BindProperty(Name = "diffOnly", SupportsGet = true)] public bool ShowDiffOnly { get; set; } + [BindProperty(Name = "notificationMessage", SupportsGet = true)] + public string NotificationMessage { get; set; } /// /// Handler for loading page @@ -90,7 +94,7 @@ public async Task OnGetAsync(string id, string revisionId = null) showDocumentation: ShowDocumentation, showDiffOnly: ShowDiffOnly, diffContextSize: REVIEW_DIFF_CONTEXT_SIZE, diffContextSeperator: DIFF_CONTEXT_SEPERATOR); - if (ReviewContent == default(ReviewContentModel)) + if (ReviewContent.Directive == ReviewContentModelDirective.TryGetlegacyReview) { // Check if you can get review from legacy data var legacyReview = await _reviewManager.GetLegacyReviewAsync(User, id); @@ -101,7 +105,7 @@ public async Task OnGetAsync(string id, string revisionId = null) if (legacyRevision == null) { - return NotFound(); + return RedirectToPage("Index", new { notificationMessage = $"Review with ID : {id} was not found." }); } var review = await _reviewManager.GetReviewAsync(language: legacyRevision.Files[0].Language, @@ -116,10 +120,15 @@ public async Task OnGetAsync(string id, string revisionId = null) } else { - return NotFound(); + return RedirectToPage("Index", new { notificationMessage = $"Review with ID : {id} was not found." }); } } + if (ReviewContent.Directive == ReviewContentModelDirective.ErrorDueToInvalidAPIRevison) + { + NotificationMessage = ReviewContent.NotificationMessage; + } + if (!ReviewContent.APIRevisionsGrouped.Any()) { return RedirectToPage("LegacyReview", new { id = id }); @@ -314,7 +323,7 @@ public Dictionary GetRoutingData(string diffRevisionId = null, b /// public async Task> GetAssociatedPullRequest() { - return await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id); + return await _pullRequestManager.GetPullRequestsModelAsync(reviewId: ReviewContent.Review.Id, apiRevisionId: ReviewContent.ActiveAPIRevision.Id); } /// @@ -323,8 +332,12 @@ public async Task> GetAssociatedPullRequest() /// public async Task> GetPRsOfAssoicatedReviews() { - var creatingPR = (await _pullRequestManager.GetPullRequestsModelAsync(ReviewContent.Review.Id)).FirstOrDefault(); - return await _pullRequestManager.GetPullRequestsModelAsync(creatingPR.PullRequestNumber, creatingPR.RepoName);; + var creatingPR = (await _pullRequestManager.GetPullRequestsModelAsync(reviewId: ReviewContent.Review.Id, apiRevisionId: ReviewContent.ActiveAPIRevision.Id)).FirstOrDefault(); + if (creatingPR != null) + { + return await _pullRequestManager.GetPullRequestsModelAsync(creatingPR.PullRequestNumber, creatingPR.RepoName); + } + return new List(); } /// diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml index f46fb96f4c8..674cdfa6bf7 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Revisions.cshtml @@ -24,7 +24,7 @@
    -
    +
    diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs index 9293384377c..b954951363b 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs @@ -51,8 +51,13 @@ public async Task> GetPullRequestsAsync(int pullRequestNu return await GetPullRequestFromQueryAsync(query); } - public async Task> GetPullRequestsAsync(string reviewId) { + public async Task> GetPullRequestsAsync(string reviewId, string apiRevisionId = null) { var query = $"SELECT * FROM PullRequests c WHERE c.ReviewId = '{reviewId}' AND c.IsDeleted = false"; + if (!string.IsNullOrEmpty(apiRevisionId)) + { + query += $" AND c.APIRevisionId = '{apiRevisionId}'"; + } + return await GetPullRequestFromQueryAsync(query); } diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs index 0dee2614453..df656fd55a4 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosReviewRepository.cs @@ -69,7 +69,16 @@ public async Task> GetReviewsAsync(string langu public async Task GetLegacyReviewAsync(string reviewId) { - return await _legacyReviewsContainer.ReadItemAsync(reviewId, new PartitionKey(reviewId)); + var review = default(LegacyReviewModel); + try + { + review = await _legacyReviewsContainer.ReadItemAsync(reviewId, new PartitionKey(reviewId)); + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return review; + } + return review; } public async Task GetReviewAsync(string language, string packageName, bool? isClosed = false) diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs index d4e0fa09af0..cc609d93b7f 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/Interfaces/ICosmosPullRequestsRepository.cs @@ -7,7 +7,7 @@ namespace APIViewWeb.Repositories public interface ICosmosPullRequestsRepository { public Task GetPullRequestAsync(int pullRequestNumber, string repoName, string packageName, string language = null); - public Task> GetPullRequestsAsync(string reviewId); + public Task> GetPullRequestsAsync(string reviewId, string apiRevisionId = null); public Task UpsertPullRequestAsync(PullRequestModel pullRequestModel); public Task> GetPullRequestsAsync(bool isOpen); public Task> GetPullRequestsAsync(int pullRequestNumber, string repoName); diff --git a/src/dotnet/APIView/apiview.yml b/src/dotnet/APIView/apiview.yml index e3c05f98232..41adacb16d4 100644 --- a/src/dotnet/APIView/apiview.yml +++ b/src/dotnet/APIView/apiview.yml @@ -141,119 +141,119 @@ stages: artifactName: 'APIView' -# - job: 'Test' -# -# pool: -# name: azsdk-pool-mms-win-2022-general -# vmImage: windows-2022 -# -# steps: -# - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml -# parameters: -# StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' -# -# - script: | -# npm install -g azurite -# displayName: 'Install Azurite' -# -# - task: Powershell@2 -# inputs: -# workingDirectory: $(Agent.TempDirectory) -# filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 -# arguments: > -# -Process "azurite.cmd" -# -ArgumentList "--silent" -# -Port "10000" -# pwsh: true -# displayName: 'Start Azurite' -# -# - template: /eng/pipelines/templates/steps/install-dotnet.yml -# -# - pwsh: | -# dotnet --list-runtimes -# dotnet --version -# displayName: 'List .NET run times' -# -# - task: GoTool@0 -# inputs: -# version: '$(GoVersion)' -# displayName: "Use Go $(GoVersion)" -# -# - script: | -# go test ./... -v -# workingDirectory: $(GoParserPackagePath) -# displayName: 'Test Go parser' -# -# - script: >- -# dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj -# --logger trx --collect:"XPlat Code Coverage" -# displayName: "Build & Test (Unit)" -# env: -# DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 -# DOTNET_CLI_TELEMETRY_OPTOUT: 1 -# DOTNET_MULTILEVEL_LOOKUP: 0 -# -# - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 -# condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) -# displayName: Generate Code Coverage Reports -# inputs: -# reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml -# targetdir: $(Build.ArtifactStagingDirectory)\coverage -# reporttypes: Cobertura -# filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** -# verbosity: Verbose -# -# - task: PublishCodeCoverageResults@1 -# condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) -# displayName: Publish Code Coverage Reports -# inputs: -# codeCoverageTool: Cobertura -# summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml -# -# - script: >- -# dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj -# --logger trx -# displayName: "Build & Test (Integration)" -# env: -# DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 -# DOTNET_CLI_TELEMETRY_OPTOUT: 1 -# DOTNET_MULTILEVEL_LOOKUP: 0 -# APIVIEW_ENDPOINT: "http://localhost:5000" -# APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) -# APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) -# -# - script: | -# npm install -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Install Client Dependencies" -# -# - script: | -# npx playwright install --with-deps -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Install Playwright Browsers" -# -# - script: | -# npx playwright test --project=unit-tests -# workingDirectory: $(WebClientProjectDirectory) -# displayName: "Run Client-Side Unit Tests" -# -# - task: PublishBuildArtifacts@1 -# inputs: -# pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' -# artifactName: 'Client-Side Unit Test Reports' -# -# - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: -# - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml -# parameters: -# NodeVersion: $(NodeVersion) -# WebClientProjectDirectory: $(WebClientProjectDirectory) -# AzuriteConnectionString: $(AzuriteConnectionString) -# CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) -# -# - task: PublishTestResults@2 -# condition: succeededOrFailed() -# inputs: -# testResultsFiles: '**/*.trx' -# testRunTitle: 'Tests against Windows .NET' -# testResultsFormat: 'VSTest' -# mergeTestResults: true + - job: 'Test' + + pool: + name: azsdk-pool-mms-win-2022-general + vmImage: windows-2022 + + steps: + - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml + parameters: + StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' + + - script: | + npm install -g azurite + displayName: 'Install Azurite' + + - task: Powershell@2 + inputs: + workingDirectory: $(Agent.TempDirectory) + filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 + arguments: > + -Process "azurite.cmd" + -ArgumentList "--silent" + -Port "10000" + pwsh: true + displayName: 'Start Azurite' + + - template: /eng/pipelines/templates/steps/install-dotnet.yml + + - pwsh: | + dotnet --list-runtimes + dotnet --version + displayName: 'List .NET run times' + + - task: GoTool@0 + inputs: + version: '$(GoVersion)' + displayName: "Use Go $(GoVersion)" + + - script: | + go test ./... -v + workingDirectory: $(GoParserPackagePath) + displayName: 'Test Go parser' + + - script: >- + dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj + --logger trx --collect:"XPlat Code Coverage" + displayName: "Build & Test (Unit)" + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_MULTILEVEL_LOOKUP: 0 + + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + displayName: Generate Code Coverage Reports + inputs: + reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml + targetdir: $(Build.ArtifactStagingDirectory)\coverage + reporttypes: Cobertura + filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** + verbosity: Verbose + + - task: PublishCodeCoverageResults@1 + condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + displayName: Publish Code Coverage Reports + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml + + #- script: >- + # dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj + # --logger trx + # displayName: "Build & Test (Integration)" + # env: + # DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + # DOTNET_CLI_TELEMETRY_OPTOUT: 1 + # DOTNET_MULTILEVEL_LOOKUP: 0 + # APIVIEW_ENDPOINT: "http://localhost:5000" + # APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) + # APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) + + - script: | + npm install + workingDirectory: $(WebClientProjectDirectory) + displayName: "Install Client Dependencies" + + - script: | + npx playwright install --with-deps + workingDirectory: $(WebClientProjectDirectory) + displayName: "Install Playwright Browsers" + + - script: | + npx playwright test --project=unit-tests + workingDirectory: $(WebClientProjectDirectory) + displayName: "Run Client-Side Unit Tests" + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' + artifactName: 'Client-Side Unit Test Reports' + + - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: + - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml + parameters: + NodeVersion: $(NodeVersion) + WebClientProjectDirectory: $(WebClientProjectDirectory) + AzuriteConnectionString: $(AzuriteConnectionString) + CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFiles: '**/*.trx' + testRunTitle: 'Tests against Windows .NET' + testResultsFormat: 'VSTest' + mergeTestResults: true From 1e06e970fa28551411ea2ce1f596273d4ef14275 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu <31145988+chidozieononiwu@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:51:08 -0800 Subject: [PATCH 15/37] Re architect/review revision restructure (#7394) * Fix null pointer bug in comments, add requested reviews * various fixes * update Pull Request Model as part of PR controller --- .../Controllers/AutoReviewController.cs | 5 +- .../Controllers/PullRequestController.cs | 8 +- .../APIViewWeb/Helpers/PageModelHelpers.cs | 6 + .../APIViewWeb/Managers/CommentsManager.cs | 22 +- .../Interfaces/IPullRequestManager.cs | 1 + .../APIViewWeb/Managers/PullRequestManager.cs | 4 + .../APIViewWeb/Models/CommentThreadModel.cs | 2 +- .../Pages/Assemblies/RequestedReviews.cshtml | 156 ++++++++++++ .../Assemblies/RequestedReviews.cshtml.cs | 35 ++- .../Pages/Assemblies/Review.cshtml.cs | 4 +- .../Shared/_CommentThreadInnerPartial.cshtml | 2 +- .../CosmosUserProfileRepository.cs | 2 +- src/dotnet/APIView/apiview.yml | 232 +++++++++--------- 13 files changed, 335 insertions(+), 144 deletions(-) create mode 100644 src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs index 5bf021873e8..d50a4deef11 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/AutoReviewController.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Logging; using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.VisualStudio.Services.Account; +using Octokit; namespace APIViewWeb.Controllers { @@ -155,9 +156,7 @@ private async Task CreateAutomaticRevisionAsync(CodeFi !await _apiRevisionsManager.IsAPIRevisionTheSame(latestAutomaticAPIRevision, renderedCodeFile) && !comments.Any(c => latestAutomaticAPIRevision.Id == c.APIRevisionId)) { - // Check if user is authorized to modify automatic review - await ManagerHelpers.AssertAutomaticAPIRevisionModifier(user: User, apiRevision: apiRevision, authorizationService: _authorizationService); - await _apiRevisionsManager.SoftDeleteAPIRevisionAsync(user: User, apiRevision: latestAutomaticAPIRevision); + await _apiRevisionsManager.SoftDeleteAPIRevisionAsync(apiRevision: latestAutomaticAPIRevision, notes: "Deleted by Automatic Review Creation..."); latestAutomaticAPIRevision = automaticRevisionsQueue.Dequeue(); } diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs index 3564fd7e10b..15b0a429aa9 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs @@ -241,9 +241,15 @@ await _apiRevisionsManager.CreateAPIRevisionAsync( } } - await _apiRevisionsManager.CreateAPIRevisionAsync( + var newAPIRevision = await _apiRevisionsManager.CreateAPIRevisionAsync( userName: pullRequestModel.CreatedBy, reviewId: review.Id, apiRevisionType: APIRevisionType.PullRequest, label: String.Empty, memoryStream: memoryStream, codeFile: codeFile, originalName: originalFileName, prNumber: prNumber); + + if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") + { + await _apiRevisionsManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(reviewId: review.Id, apiRevision: newAPIRevision); + } + await _pullRequestManager.UpsertPullRequestAsync(pullRequestModel); } diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs index 3826ac8db1d..c9d1e8521a7 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs @@ -18,6 +18,7 @@ using Microsoft.OpenApi.Any; using Microsoft.AspNetCore.Http; using Microsoft.VisualStudio.Services.ClientNotification; +using Microsoft.TeamFoundation.Common; namespace APIViewWeb.Helpers { @@ -482,6 +483,11 @@ public static string ResolveRevisionLabel(APIRevisionListItemModel apiRevision, label = $"{label} | {apiRevision.Label}"; } + if (apiRevision.APIRevisionType == APIRevisionType.PullRequest && apiRevision.PullRequestNo != null) + { + label = $"PR {apiRevision.PullRequestNo} | {label}"; + } + if (addType) { label = $"{apiRevision.APIRevisionType.ToString()} | {label}"; diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs index da785b998a5..e88811fbe92 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs @@ -190,9 +190,14 @@ public async Task ResolveConversation(ClaimsPrincipal user, string reviewId, str var comments = await _commentsRepository.GetCommentsAsync(reviewId, lineId); foreach (var comment in comments) { - var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(comment.ChangeHistory, CommentChangeAction.Resolved, user.GetGitHubLogin()); - comment.ChangeHistory = changeUpdate.ChangeHistory; - comment.IsResolved = changeUpdate.ChangeStatus; + comment.ChangeHistory.Add( + new CommentChangeHistoryModel() + { + ChangeAction = CommentChangeAction.Resolved, + ChangedBy = user.GetGitHubLogin(), + ChangedOn = DateTime.Now, + }); + comment.IsResolved = true; await _commentsRepository.UpsertCommentAsync(comment); } } @@ -202,9 +207,14 @@ public async Task UnresolveConversation(ClaimsPrincipal user, string reviewId, s var comments = await _commentsRepository.GetCommentsAsync(reviewId, lineId); foreach (var comment in comments) { - var changeUpdate = ChangeHistoryHelpers.UpdateBinaryChangeAction(comment.ChangeHistory, CommentChangeAction.Resolved, user.GetGitHubLogin()); - comment.ChangeHistory = changeUpdate.ChangeHistory; - comment.IsResolved = changeUpdate.ChangeStatus; + comment.ChangeHistory.Add( + new CommentChangeHistoryModel() + { + ChangeAction = CommentChangeAction.UnResolved, + ChangedBy = user.GetGitHubLogin(), + ChangedOn = DateTime.Now, + }); + comment.IsResolved = false; await _commentsRepository.UpsertCommentAsync(comment); } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs index 506b89f0adc..9aff8bcd1a0 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/Interfaces/IPullRequestManager.cs @@ -6,6 +6,7 @@ namespace APIViewWeb.Managers { public interface IPullRequestManager { + public Task UpsertPullRequestAsync(PullRequestModel pullRequestModel); public Task> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null); public Task> GetPullRequestsModelAsync(int pullRequestNumber, string repoName); public Task GetPullRequestModelAsync(int prNumber, string repoName, string packageName, string originalFile, string language); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs index 10ae04f2538..d89ce835a30 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/PullRequestManager.cs @@ -52,6 +52,10 @@ IConfiguration configuration var pullRequestReviewCloseAfter = _configuration["pull-request-review-close-after-days"] ?? "30"; _pullRequestCleanupDays = int.Parse(pullRequestReviewCloseAfter); } + public async Task UpsertPullRequestAsync(PullRequestModel pullRequestModel) + { + await _pullRequestsRepository.UpsertPullRequestAsync(pullRequestModel); + } public async Task> GetPullRequestsModelAsync(string reviewId, string apiRevisionId = null) { return await _pullRequestsRepository.GetPullRequestsAsync(reviewId, apiRevisionId); diff --git a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs index dbcfd36a06e..ab1f64126a4 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs @@ -13,7 +13,7 @@ public CommentThreadModel(string reviewId, string lineId, IEnumerable !c.IsResolved); + Comments = comments; var resolveComment = comments.FirstOrDefault(c => c.IsResolved); IsResolved = resolveComment != null; ResolvedBy = resolveComment?.CreatedBy; diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml new file mode 100644 index 00000000000..6a9f236dedf --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml @@ -0,0 +1,156 @@ +@page "" +@model APIViewWeb.Pages.Assemblies.RequestedReviews +@using APIViewWeb.Helpers +@using APIViewWeb.Models +@{ + ViewData["Title"] = "Requested Reviews"; + var userPreference = PageModelHelpers.GetUserPreference(Model._preferenceCache, User) ?? new UserPreferenceModel(); + TempData["UserPreference"] = userPreference; +} +
    +
    +
    +

    Pending Reviews

    +
    +
    + + + + + + + + + + + + @if (Model.ActiveAPIRevisions.Any()) + { + @foreach (var apiRevision in Model.ActiveAPIRevisions) + { + var assignment = Model.Reviews.Single(r => r.Id == apiRevision.ReviewId).AssignedReviewers.First(ar => ar.AssingedTo == User.GetGitHubLogin()); + var approvalRequestedOn = assignment.AssingedOn; + var requestBy = assignment.AssignedBy; + + var truncationIndex = @Math.Min(apiRevision.PackageName.Length, 100); + + + + + + + + + } + } + else + { + + + + + } + +
    NameAuthorLast UpdatedTypeRequested onRequested by
    + @if (apiRevision.Language != null) + { + string iconClassName = "icon-" + PageModelHelpers.GetLanguageCssSafeName(@apiRevision.Language); + @if (!string.IsNullOrEmpty(apiRevision.Files.FirstOrDefault().LanguageVariant) && apiRevision.Files.FirstOrDefault().LanguageVariant != "default") + { + iconClassName += "-" + apiRevision.Files.FirstOrDefault().LanguageVariant.ToLower(); + } + + } + @apiRevision.PackageName.Substring(0, @truncationIndex) + @if (apiRevision.IsApproved == true) + { + + } + + @apiRevision.CreatedBy + + + + @apiRevision.APIRevisionType.ToString() + + + + @requestBy +
    No new reviews require approval.
    +
    +
    +
    +

    Recently-Approved Reviews

    +
    + + + + + + + + + + + + + @if (Model.ApprovedAPIRevisions.Any()) + { + @foreach (var apiRevision in Model.ApprovedAPIRevisions) + { + var assignment = Model.Reviews.Single(r => r.Id == apiRevision.ReviewId).AssignedReviewers.First(ar => ar.AssingedTo == User.GetGitHubLogin()); + var approvalRequestedOn = assignment.AssingedOn; + var requestBy = assignment.AssignedBy; + + var truncationIndex = @Math.Min(apiRevision.PackageName.Length, 100); + + + + + + + + + } + } + else + { + + + + } + +
    NameAuthorLast UpdatedTypeApproved lastApproved by
    + @if (apiRevision.Language != null) + { + string iconClassName = "icon-" + PageModelHelpers.GetLanguageCssSafeName(@apiRevision.Language); + @if (!string.IsNullOrEmpty(apiRevision.Files.FirstOrDefault().LanguageVariant) && apiRevision.Files.FirstOrDefault().LanguageVariant != "default") + { + iconClassName += "-" + apiRevision.Files.FirstOrDefault().LanguageVariant.ToLower(); + } + + } + @apiRevision.PackageName.Substring(0, @truncationIndex) + @if (apiRevision.IsApproved == true) + { + + } + + @apiRevision.CreatedBy + + + + @apiRevision.APIRevisionType.ToString() + + + + + @foreach(var approver in apiRevision.Approvers) { + @approver + } + +
    No reviews have been recently approved.
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs index dd9e3c3e437..67b884abaf2 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/RequestedReviews.cshtml.cs @@ -2,37 +2,46 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ApiView; using APIViewWeb.LeanModels; using APIViewWeb.Managers; -using APIViewWeb.Models; +using APIViewWeb.Managers.Interfaces; using APIViewWeb.Repositories; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.VisualStudio.Services.Common; -using Octokit; namespace APIViewWeb.Pages.Assemblies { public class RequestedReviews: PageModel { - private readonly IReviewManager _manager; + private readonly IReviewManager _reviewManager; + private readonly IAPIRevisionsManager _apiRevisionsManager; public readonly UserPreferenceCache _preferenceCache; - public IEnumerable ActiveReviews { get; set; } = new List(); - public IEnumerable ApprovedReviews { get; set; } = new List(); - public RequestedReviews(IReviewManager manager, UserPreferenceCache cache) + public IEnumerable Reviews { get; set; } = new List(); + public IEnumerable ActiveAPIRevisions { get; set; } = new List(); + public IEnumerable ApprovedAPIRevisions { get; set; } = new List(); + + public RequestedReviews(IReviewManager reviewManager, IAPIRevisionsManager apiRevisionsManager, UserPreferenceCache cache) { - _manager = manager; + _reviewManager = reviewManager; + _apiRevisionsManager = apiRevisionsManager; _preferenceCache = cache; } public async Task OnGetAsync() { - var requestedReviews = await _manager.GetReviewsAssignedToUser(User.GetGitHubLogin()); - ActiveReviews = requestedReviews.Where(r => r.IsApproved == false).OrderByDescending(r => r.AssignedReviewers.Select(x => x.AssingedOn)); - // Remove all approvals over a week old - //ApprovedReviews = requestedReviews.Where(r => r.IsApproved == true).Where(r => r.ApprovalDate >= DateTime.Now.AddDays(-7)).OrderByDescending(r => r.ApprovalDate); + Reviews = await _reviewManager.GetReviewsAssignedToUser(User.GetGitHubLogin()); + + foreach (var review in Reviews.OrderByDescending(r => r.AssignedReviewers.Select(x => x.AssingedOn))) + { + var apiRevisoins = await _apiRevisionsManager.GetAPIRevisionsAsync(review.Id); + ActiveAPIRevisions = ActiveAPIRevisions.Concat(apiRevisoins.Where(r => r.IsApproved == false)); + + // Remove all approvals over a week old + ApprovedAPIRevisions = ApprovedAPIRevisions.Concat(apiRevisoins.Where(r => r.IsApproved == true).Where(r => r.ChangeHistory.First(c => c.ChangeAction == APIRevisionChangeAction.Approved).ChangedOn >= DateTime.Now.AddDays(-7))); + } + ApprovedAPIRevisions.OrderByDescending(r => r.ChangeHistory.First(c => c.ChangeAction == APIRevisionChangeAction.Approved).ChangedOn); + return Page(); } } diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs index a6caf9aee35..0799cd12fb9 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs +++ b/src/dotnet/APIView/APIViewWeb/Pages/Assemblies/Review.cshtml.cs @@ -269,7 +269,7 @@ public async Task OnPostToggleSubscribedAsync(string id) public async Task OnPostToggleReviewApprovalAsync(string id, string revisionId) { await _reviewManager.ToggleReviewApprovalAsync(User, id, revisionId); - return RedirectToPage(new { id = id }); + return RedirectToPage(new { id = id, revisionId = revisionId }); } /// @@ -285,7 +285,7 @@ public async Task OnPostToggleAPIRevisionApprovalAsync(string id, { await OnPostToggleReviewApprovalAsync(id, revisionId); } - return RedirectToPage(new { id = id }); + return RedirectToPage(new { id = id, revisionId = revisionId }); } /// diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml index 6557cd1a48a..f0e8e00b8dc 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_CommentThreadInnerPartial.cshtml @@ -52,7 +52,7 @@ - @if (Model.CommentText.Contains("\r\n")) + @if (!string.IsNullOrEmpty(Model.CommentText) && Model.CommentText.Contains("\r\n")) { var cmt = Model.CommentText.Split("\r\n"); string codeBlockString = ""; diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosUserProfileRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosUserProfileRepository.cs index 5a0b7a6216e..17fa3f70227 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosUserProfileRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosUserProfileRepository.cs @@ -17,7 +17,7 @@ public class CosmosUserProfileRepository : ICosmosUserProfileRepository public CosmosUserProfileRepository(IConfiguration configuration, CosmosClient cosmosClient) { - _userProfileContainer = cosmosClient.GetContainer("APIViewV2", "Profiles"); + _userProfileContainer = cosmosClient.GetContainer("APIView", "Profiles"); } public async Task TryGetUserProfileAsync(string UserName) diff --git a/src/dotnet/APIView/apiview.yml b/src/dotnet/APIView/apiview.yml index 41adacb16d4..520bb4cf694 100644 --- a/src/dotnet/APIView/apiview.yml +++ b/src/dotnet/APIView/apiview.yml @@ -141,119 +141,119 @@ stages: artifactName: 'APIView' - - job: 'Test' - - pool: - name: azsdk-pool-mms-win-2022-general - vmImage: windows-2022 - - steps: - - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml - parameters: - StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' - - - script: | - npm install -g azurite - displayName: 'Install Azurite' - - - task: Powershell@2 - inputs: - workingDirectory: $(Agent.TempDirectory) - filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 - arguments: > - -Process "azurite.cmd" - -ArgumentList "--silent" - -Port "10000" - pwsh: true - displayName: 'Start Azurite' - - - template: /eng/pipelines/templates/steps/install-dotnet.yml - - - pwsh: | - dotnet --list-runtimes - dotnet --version - displayName: 'List .NET run times' - - - task: GoTool@0 - inputs: - version: '$(GoVersion)' - displayName: "Use Go $(GoVersion)" - - - script: | - go test ./... -v - workingDirectory: $(GoParserPackagePath) - displayName: 'Test Go parser' - - - script: >- - dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj - --logger trx --collect:"XPlat Code Coverage" - displayName: "Build & Test (Unit)" - env: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_MULTILEVEL_LOOKUP: 0 - - - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) - displayName: Generate Code Coverage Reports - inputs: - reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml - targetdir: $(Build.ArtifactStagingDirectory)\coverage - reporttypes: Cobertura - filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** - verbosity: Verbose - - - task: PublishCodeCoverageResults@1 - condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) - displayName: Publish Code Coverage Reports - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml - - #- script: >- - # dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj - # --logger trx - # displayName: "Build & Test (Integration)" - # env: - # DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - # DOTNET_CLI_TELEMETRY_OPTOUT: 1 - # DOTNET_MULTILEVEL_LOOKUP: 0 - # APIVIEW_ENDPOINT: "http://localhost:5000" - # APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) - # APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) - - - script: | - npm install - workingDirectory: $(WebClientProjectDirectory) - displayName: "Install Client Dependencies" - - - script: | - npx playwright install --with-deps - workingDirectory: $(WebClientProjectDirectory) - displayName: "Install Playwright Browsers" - - - script: | - npx playwright test --project=unit-tests - workingDirectory: $(WebClientProjectDirectory) - displayName: "Run Client-Side Unit Tests" - - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' - artifactName: 'Client-Side Unit Test Reports' - - - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: - - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml - parameters: - NodeVersion: $(NodeVersion) - WebClientProjectDirectory: $(WebClientProjectDirectory) - AzuriteConnectionString: $(AzuriteConnectionString) - CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) - - - task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testResultsFiles: '**/*.trx' - testRunTitle: 'Tests against Windows .NET' - testResultsFormat: 'VSTest' - mergeTestResults: true + #- job: 'Test' +# + # pool: + # name: azsdk-pool-mms-win-2022-general + # vmImage: windows-2022 +# + # steps: + # - template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml + # parameters: + # StartParameters: '/noexplorer /noui /enablepreview /disableratelimiting /enableaadauthentication /partitioncount=50 /consistency=Strong' + # + # - script: | + # npm install -g azurite + # displayName: 'Install Azurite' +# + # - task: Powershell@2 + # inputs: + # workingDirectory: $(Agent.TempDirectory) + # filePath: $(Build.SourcesDirectory)/eng/scripts/Start-LocalHostApp.ps1 + # arguments: > + # -Process "azurite.cmd" + # -ArgumentList "--silent" + # -Port "10000" + # pwsh: true + # displayName: 'Start Azurite' +# + # - template: /eng/pipelines/templates/steps/install-dotnet.yml +# + # - pwsh: | + # dotnet --list-runtimes + # dotnet --version + # displayName: 'List .NET run times' +# + # - task: GoTool@0 + # inputs: + # version: '$(GoVersion)' + # displayName: "Use Go $(GoVersion)" + # + # - script: | + # go test ./... -v + # workingDirectory: $(GoParserPackagePath) + # displayName: 'Test Go parser' + # + # - script: >- + # dotnet test src/dotnet/APIView/APIViewUnitTests/APIViewUnitTests.csproj + # --logger trx --collect:"XPlat Code Coverage" + # displayName: "Build & Test (Unit)" + # env: + # DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + # DOTNET_CLI_TELEMETRY_OPTOUT: 1 + # DOTNET_MULTILEVEL_LOOKUP: 0 +# + # - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + # condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + # displayName: Generate Code Coverage Reports + # inputs: + # reports: $(Build.SourcesDirectory)\src\dotnet\APIView\APIViewUnitTests\**\coverage.cobertura.xml + # targetdir: $(Build.ArtifactStagingDirectory)\coverage + # reporttypes: Cobertura + # filefilters: +$(Build.SourcesDirectory)\src\dotnet\APIView\** + # verbosity: Verbose +# + # - task: PublishCodeCoverageResults@1 + # condition: and(succeededOrFailed(), eq(variables['CollectCoverage'], 'true')) + # displayName: Publish Code Coverage Reports + # inputs: + # codeCoverageTool: Cobertura + # summaryFileLocation: $(Build.ArtifactStagingDirectory)\coverage\Cobertura.xml +# + # - script: >- + # dotnet test src/dotnet/APIView/APIViewIntegrationTests/APIViewIntegrationTests.csproj + # --logger trx + # displayName: "Build & Test (Integration)" + # env: + # DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + # DOTNET_CLI_TELEMETRY_OPTOUT: 1 + # DOTNET_MULTILEVEL_LOOKUP: 0 + # APIVIEW_ENDPOINT: "http://localhost:5000" + # APIVIEW_BLOB__CONNECTIONSTRING: $(AzuriteConnectionString) + # APIVIEW_COSMOS__CONNECTIONSTRING: $(CosmosEmulatorConnectionString) + # + # - script: | + # npm install + # workingDirectory: $(WebClientProjectDirectory) + # displayName: "Install Client Dependencies" +# + # - script: | + # npx playwright install --with-deps + # workingDirectory: $(WebClientProjectDirectory) + # displayName: "Install Playwright Browsers" +# + # - script: | + # npx playwright test --project=unit-tests + # workingDirectory: $(WebClientProjectDirectory) + # displayName: "Run Client-Side Unit Tests" +# + # - task: PublishBuildArtifacts@1 + # inputs: + # pathtoPublish: '$(Build.SourcesDirectory)\src\dotnet\APIView\APIViewWeb\Client\playwright-report' + # artifactName: 'Client-Side Unit Test Reports' + # + # - ${{ if and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'internal')) }}: + # - template: /eng/pipelines/templates/steps/apiview-ui-tests.yml + # parameters: + # NodeVersion: $(NodeVersion) + # WebClientProjectDirectory: $(WebClientProjectDirectory) + # AzuriteConnectionString: $(AzuriteConnectionString) + # CosmosEmulatorConnectionString: $(CosmosEmulatorConnectionString) +# + # - task: PublishTestResults@2 + # condition: succeededOrFailed() + # inputs: + # testResultsFiles: '**/*.trx' + # testRunTitle: 'Tests against Windows .NET' + # testResultsFormat: 'VSTest' + # mergeTestResults: true \ No newline at end of file From bb99c9fd3d3fbf49698e310adac82c0e10637f39 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu <31145988+chidozieononiwu@users.noreply.github.com> Date: Wed, 6 Dec 2023 06:16:31 -0800 Subject: [PATCH 16/37] Fix bug in apiRevision deletion (#7396) --- .../APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs | 2 +- src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs index b6c1fee5517..a3943a1893d 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/ChangeHistoryHelpers.cs @@ -192,7 +192,7 @@ private static (E actionAdded, E actionReverted, bool actionInvalid) ResolveActi actionReverted = action; break; case "Deleted": - Enum.TryParse(typeof(E), "Undeleted", out object ud); + Enum.TryParse(typeof(E), "UnDeleted", out object ud); actionAdded = action; actionReverted = (E)ud; break; diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs index c9d1e8521a7..69185a4efa2 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/PageModelHelpers.cs @@ -10,15 +10,11 @@ using APIViewWeb.Repositories; using APIViewWeb.LeanModels; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; using APIViewWeb.Managers.Interfaces; using APIViewWeb.Hubs; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Configuration; -using Microsoft.OpenApi.Any; -using Microsoft.AspNetCore.Http; -using Microsoft.VisualStudio.Services.ClientNotification; -using Microsoft.TeamFoundation.Common; + namespace APIViewWeb.Helpers { @@ -452,7 +448,7 @@ public static async Task GetCodeLineSectionAsync(ClaimsPrincipa diffRevision.HeadingsOfSectionsWithDiff[activeRevision.Id] : new HashSet(); codeLines = PageModelHelpers.CreateLines(diagnostics: fileDiagnostics, lines: diffLines, comments: comments, - showDiffOnly: true, reviewDiffContextSize: diffContextSize, diffContextSeparator: diffContextSeperator, + showDiffOnly: false, reviewDiffContextSize: diffContextSize, diffContextSeparator: diffContextSeperator, headingsOfSectionsWithDiff: headingsOfSectionsWithDiff); } else From 62fe87bc96f270ec19fc1b02262055360ff71be2 Mon Sep 17 00:00:00 2001 From: Mariana Rios Flores Date: Wed, 6 Dec 2023 10:41:15 -0800 Subject: [PATCH 17/37] apiview documentation update (#7397) --- src/dotnet/APIView/APIViewWeb/README.md | 51 ++++--------------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/README.md b/src/dotnet/APIView/APIViewWeb/README.md index 6c21004b513..edf82870be7 100644 --- a/src/dotnet/APIView/APIViewWeb/README.md +++ b/src/dotnet/APIView/APIViewWeb/README.md @@ -1,24 +1,14 @@ # APIView -APIView tool is used by archboard reviewers to review API signatures of all public APIs available in Azure SDK packages. This tool generates public API surface level review which shows all publicly available classes, methods, properties etc. This makes it easier to identify if there are any breaking changes. Currently APIView tool supports following languages: +APIView tool is used by archboard reviewers to review API signatures of all public APIs available in Azure SDK packages. This tool generates public API surface level revisions which shows all publicly available classes, methods, properties etc. This makes it easier to identify if there are any breaking changes. -- C# -- C -- C++ -- Java -- JS/TS -- Python -- Go -- Swift - -## Why do we need APIView - -APIView tool allows to see only stub version of classes, methods, properties, method signatures available in each Azure SDK. This helps architects to review an Azure SDK and helps to identify any potential change that can impact consumers of an Azure SDK. APIView is also used as an enforcing tool to make sure we release a GA version of Azure SDK package only after it is approved by an architect. APIView is also utlized to identify any change at API level in a pull request. +## How does it retrieve public API information +Developers can upload package or an abstract file generated based on each language into APIView tool. APIView tool has a language processor for each language it supports and these individual language processor extracts API stub information and create json APIView file that is processed by tool. APIView tool stores original uploaded file and generated json file in its data store. Revision pages are rendered using this generated json file which contains tokens to present language keywords, links etc. Tool also allows user to recreate review json from stored original file if language package processor itself is updated. -## How to create an API review manually +## How to create an API revision manually -API review can be created by uploading an artifact to APIView tool. Type of the artifact is different across each language. Some language, for e.g., Swift requires developer to run parser tool locally to generate stub file and upload json stub file instead of any artifact. Following are the detailed instructions on how to create review for each language. +API revisions can be created by uploading an artifact to APIView tool. Type of the artifact is different across each language. Some language, for e.g., Swift requires developer to run parser tool locally to generate stub file and upload json stub file instead of any artifact. Following are the detailed instructions on how to create revisions for each language. ### C# Run `dotnet pack` for the required package to generate Nuget file. Upload the resulting .nupkg file using `Create Review` link in APIView. @@ -54,38 +44,13 @@ Run `dotnet pack` for the required package to generate Nuget file. Upload the re 2. Upload generated whl file ### Swagger -Swagger API review can be generated manually by uploading swagger file to APIView if you are trying to generate API review for a single swagger file. Swagger API review is automatically generated when swagger files are modified in a pull request and pull request comment shows a link to generated API review. Automatically generated API review from pull request creates a diff using existing swagger files in the target branch as baseline to show API level changes in pull request. +Swagger API revisions can be generated manually by uploading swagger file to APIView if you are trying to generate API revision for a single swagger file. Swagger API revision is automatically generated when swagger files are modified in a pull request and pull request comment shows a link to generated APIView. Automatically generated API revision from pull request creates a diff using existing swagger files in the target branch as baseline to show API level changes in pull request. -You can rename a swagger file as mentioned below and upload it to APIView in case if you need to generate an API review manually from swagger. +You can rename a swagger file as mentioned below and upload it to APIView if you need to generate an API revision manually from swagger. 1. Rename swagger json to replace file extension to .swagger `Rename-Item PetSwagger.json -NewName PetSwagger.swagger` 2. Upload renamed `.swagger` file ### TypeSpec -TypeSpec API review is generated automatically from a pull request and this should be good enough in most scenarios. You can also generate API review manually for a TypeSpec package by providing URL path to TypeSpec package specification root path. +TypeSpec API revision is generated automatically from a pull request and this should be good enough in most scenarios. You can also generate API revision manually for a TypeSpec package by providing URL path to TypeSpec package specification root path. 1. Click and `Create Review` and select TypeSpec from language dropdown. 2. Provide URL to typespec project root path. - - -## How does it retrieve public API information - -Developers can upload package or an abstract file generated based on each language into API View tool. APIView tool has a language processor for each language it supports and these individual language processor extracts API stub information and create json APIView file that is processed by tool. API review tool stores original uploaded file and generated json file in its data store. Review pages are rendered using this generated json file which contains tokens to present language keywords, links etc. Tool also allows user to recreate review json from stored original file if language package processor itself is updated. - -## Types of API reviews - -APIView tool shows three different types of reviews based on how it is generated. -- Manual -- Automatic -- Pull request reviews - -### Manual -Manual reviews are created by developers by uploading artifact as per the instructions given above. This will allow developers to review API changes if API review is not available from PR branch. - -### Automatic -API review tool has a master version of API review created automatically by azure-sdk bot as part of our scheduled CI pipelines in Azure Devops internal project. CI will check for any new change in public API surface level as part of every scheduled run and create a new revision if it finds any difference. These reviews cannot be deleted or updated with new revisions manually , in other words, this is a locked version of API reviews. Only actions that are allowed on master review is Add/update/remove/Resolve comment and Approve API reviews. - -As part of build and release pipelines, we will enforce API approval by architect or deputy architect if package version is GA which means we need to ensure latest revision of automatically created review is approved in API review tool to release a GA version package. This is applicable for any package ( both data plane and management plane) that is listed as an artifact in CI yaml. - -Automatic API reviews are not listed by default when you login to API review tool. You can view automatic reviews by clicking on "Automatic" button in top right corner in the main page. - -### API reviews from pull requests -PR pipeline for Java, C#, JS and Python sends a request to APIView tool to identify if there are any changes made at API level as part of the PR it is processing. APIView tool compared the stub file from PR pipeline against API review revision created from main branch and creates a new review if there is any change. APIView will also add a comment to GitHub PR with a link to API review if there is any change detected. APIView will not create any review for a PR if it does not have any API level change. \ No newline at end of file From afb5ed1770cca7fb2a76f6ac25f98e75944ff7a6 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu <31145988+chidozieononiwu@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:03:53 -0800 Subject: [PATCH 18/37] Fix failures when creating Pull Request Reviews (#7398) --- .../Controllers/PullRequestController.cs | 87 ++++++++++--------- .../Helpers/LanguageServiceHelpers.cs | 17 ++++ .../LinesWithDiffBackgroundHostedService.cs | 2 +- .../Managers/APIRevisionsManager.cs | 1 - .../CosmosPullRequestsRepository.cs | 2 +- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs index 15b0a429aa9..3c29728d458 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/PullRequestController.cs @@ -110,49 +110,50 @@ private async Task DetectAPIChanges(string buildId, string language = null, string project = "public") { - var requestTelemetry = new RequestTelemetry { Name = "Detecting API changes for PR: " + prNumber }; - var operation = _telemetryClient.StartOperation(requestTelemetry); - originalFileName = originalFileName ?? codeFileName; - var repoInfo = repoName.Split("/"); - var pullRequestModel = await _pullRequestManager.GetPullRequestModelAsync(prNumber, repoName, packageName, originalFileName, language); - if (pullRequestModel == null) - { - return ""; - } - if (pullRequestModel.Commits.Any(c => c == commitSha)) - { - // PR commit is already processed. No need to reprocess it again. - return !string.IsNullOrEmpty(pullRequestModel.ReviewId) ? ManagerHelpers.ResolveReviewUrl(pullRequest: pullRequestModel, hostName: hostName) : ""; - } + language = LanguageServiceHelpers.MapLanguageAlias(language: language); + var requestTelemetry = new RequestTelemetry { Name = "Detecting API changes for PR: " + prNumber }; + var operation = _telemetryClient.StartOperation(requestTelemetry); + originalFileName = originalFileName ?? codeFileName; + var repoInfo = repoName.Split("/"); + var pullRequestModel = await _pullRequestManager.GetPullRequestModelAsync(prNumber, repoName, packageName, originalFileName, language); + if (pullRequestModel == null) + { + return ""; + } + if (pullRequestModel.Commits.Any(c => c == commitSha)) + { + // PR commit is already processed. No need to reprocess it again. + return !string.IsNullOrEmpty(pullRequestModel.ReviewId) ? ManagerHelpers.ResolveReviewUrl(pullRequest: pullRequestModel, hostName: hostName) : ""; + } - pullRequestModel.Commits.Add(commitSha); - //Check if PR owner is part of Azure//Microsoft org in GitHub - await ManagerHelpers.AssertPullRequestCreatorPermission(prModel: pullRequestModel, allowedListBotAccounts: _allowedListBotAccounts, - openSourceManager: _openSourceManager, telemetryClient: _telemetryClient); + pullRequestModel.Commits.Add(commitSha); + //Check if PR owner is part of Azure//Microsoft org in GitHub + await ManagerHelpers.AssertPullRequestCreatorPermission(prModel: pullRequestModel, allowedListBotAccounts: _allowedListBotAccounts, + openSourceManager: _openSourceManager, telemetryClient: _telemetryClient); - using var memoryStream = new MemoryStream(); - using var baselineStream = new MemoryStream(); - var codeFile = await _codeFileManager.GetCodeFileAsync( - repoName: repoName, buildId: buildId, artifactName: artifactName, - packageName: packageName, originalFileName: originalFileName, - codeFileName: codeFileName, originalFileStream: memoryStream, - baselineCodeFileName: baselineCodeFileName, baselineStream: baselineStream, - project: project); + using var memoryStream = new MemoryStream(); + using var baselineStream = new MemoryStream(); + var codeFile = await _codeFileManager.GetCodeFileAsync( + repoName: repoName, buildId: buildId, artifactName: artifactName, + packageName: packageName, originalFileName: originalFileName, + codeFileName: codeFileName, originalFileStream: memoryStream, + baselineCodeFileName: baselineCodeFileName, baselineStream: baselineStream, + project: project); - CodeFile baseLineCodeFile = null; - if (baselineStream.Length > 0) - { - baselineStream.Position = 0; - baseLineCodeFile = await CodeFile.DeserializeAsync(baselineStream); - } - if (codeFile != null) - { - await CreateAPIRevisionIfRequired(codeFile, prNumber, originalFileName, memoryStream, pullRequestModel, baseLineCodeFile, baselineStream, baselineCodeFileName); - } - else - { - _telemetryClient.TrackTrace("Failed to download artifact. Please recheck build id and artifact path values in API change detection request."); - } + CodeFile baseLineCodeFile = null; + if (baselineStream.Length > 0) + { + baselineStream.Position = 0; + baseLineCodeFile = await CodeFile.DeserializeAsync(baselineStream); + } + if (codeFile != null) + { + await CreateAPIRevisionIfRequired(codeFile, prNumber, originalFileName, memoryStream, pullRequestModel, baseLineCodeFile, baselineStream, baselineCodeFileName); + } + else + { + _telemetryClient.TrackTrace("Failed to download artifact. Please recheck build id and artifact path values in API change detection request."); + } //Generate combined single comment to update on PR. var pullRequests = await _pullRequestManager.GetPullRequestsModelAsync(pullRequestNumber: prNumber, repoName: repoName); @@ -161,6 +162,8 @@ await ManagerHelpers.AssertPullRequestCreatorPermission(prModel: pullRequestMode await _pullRequestManager.CreateOrUpdateCommentsOnPR(pullRequests.ToList(), repoInfo[0], repoInfo[1], prNumber, hostName); } + await _pullRequestManager.UpsertPullRequestAsync(pullRequestModel); + // Return review URL created for current package if exists var pr = pullRequests.SingleOrDefault(r => r.PackageName == packageName && (r.Language == null || r.Language == language)); return pr == null ? "" : ManagerHelpers.ResolveReviewUrl(pullRequest: pr, hostName: hostName); @@ -178,6 +181,7 @@ private async Task CreateAPIRevisionIfRequired(CodeFile codeFile, int prNumber, { review = await _reviewManager.CreateReviewAsync(language: codeFile.Language, packageName: codeFile.PackageName, isClosed: false); } + pullRequestModel.ReviewId = review.Id; var renderedCodeFile = new RenderedCodeFile(codeFile); var apiRevisions = (await _apiRevisionsManager.GetAPIRevisionsAsync(reviewId: review.Id)).OrderByDescending(r => r.CreatedOn); @@ -244,12 +248,13 @@ await _apiRevisionsManager.CreateAPIRevisionAsync( var newAPIRevision = await _apiRevisionsManager.CreateAPIRevisionAsync( userName: pullRequestModel.CreatedBy, reviewId: review.Id, apiRevisionType: APIRevisionType.PullRequest, label: String.Empty, memoryStream: memoryStream, codeFile: codeFile, originalName: originalFileName, prNumber: prNumber); + + pullRequestModel.APIRevisionId = newAPIRevision.Id; if (!String.IsNullOrEmpty(review.Language) && review.Language == "Swagger") { await _apiRevisionsManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(reviewId: review.Id, apiRevision: newAPIRevision); } - await _pullRequestManager.UpsertPullRequestAsync(pullRequestModel); } diff --git a/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs b/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs index baecacf6d2b..4b89bfe52fc 100644 --- a/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs +++ b/src/dotnet/APIView/APIViewWeb/Helpers/LanguageServiceHelpers.cs @@ -24,6 +24,23 @@ public static IEnumerable MapLanguageAliases(IEnumerable languag return result.ToList(); } + public static string MapLanguageAlias(string language) + { + if (language.Equals("net") || language.Equals(".NET")) + return "C#"; + + if (language.Equals("cpp")) + return "C++"; + + if (language.Equals("js")) + return "JavaScript"; + + if (language.Equals("Cadl")) + return "TypeSpec"; + + return language; + } + public static LanguageService GetLanguageService(string language, IEnumerable languageServices) { return languageServices.FirstOrDefault(service => service.Name == language); diff --git a/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs b/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs index 876aa7689e9..855898299a9 100644 --- a/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs +++ b/src/dotnet/APIView/APIViewWeb/HostedServices/LinesWithDiffBackgroundHostedService.cs @@ -45,7 +45,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) foreach (var apiRevision in apiRevisions) { - await _apiRevisionManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(reviewId: review.Id, apiRevision: apiRevision); + await _apiRevisionManager.GetLineNumbersOfHeadingsOfSectionsWithDiff(reviewId: review.Id, apiRevision: apiRevision, apiRevisions: apiRevisions); } } } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs index ce8c77e81ce..a01924a55f7 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/APIRevisionsManager.cs @@ -273,7 +273,6 @@ public async Task GetLineNumbersOfHeadingsOfSectionsWithDiff(string reviewId, AP foreach (var rev in apiRevisions) { - // Calculate diff against previous revisions only. APIView only shows diff against revision lower than current one. if (rev.Id != apiRevision.Id) { var lineNumbersForHeadingOfSectionWithDiff = new HashSet(); diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs index b954951363b..795b2691959 100644 --- a/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs +++ b/src/dotnet/APIView/APIViewWeb/Repositories/CosmosPullRequestsRepository.cs @@ -75,7 +75,7 @@ private async Task> GetPullRequestFromQueryAsync(string q var filtered = new List(); foreach(var pr in allRequests) { - if(!await IsApiReviewClosed(pr.ReviewId)) + if(!string.IsNullOrEmpty(pr.ReviewId) && !await IsApiReviewClosed(pr.ReviewId)) filtered.Add(pr); } From e0a629c29a3921a68d690a7b69aaca6efad576f7 Mon Sep 17 00:00:00 2001 From: Chidozie Ononiwu <31145988+chidozieononiwu@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:03:08 -0800 Subject: [PATCH 19/37] Add Requested Reviews Page (#7401) --- src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml | 5 +++++ .../APIViewWeb/Repositories/CosmosReviewRepository.cs | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml index 84a13bc77b2..0dfc28e6e95 100644 --- a/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml +++ b/src/dotnet/APIView/APIViewWeb/Pages/Shared/_Layout.cshtml @@ -49,6 +49,11 @@ + }