diff --git a/CHANGELOG.md b/CHANGELOG.md index 665cd78..c9bdaae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,155 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0] - 2026-02-12 + +### Breaking Changes + +- **`IReadOnlyList` return types**: All buffered collection methods + now return `Task>` instead of `Task>`. + Consumers assigning results to `IEnumerable` are unaffected; + consumers assigning to `List` must add `.ToList()` or change the + variable type. +- **Init-only model properties**: 377 properties across 106 + response model classes converted from `{ get; set; }` to + `{ get; init; }`. Models used as request bodies (32 files, 98 + properties) retain mutable setters to allow consumer construction. + Consumers that assign model properties after construction must move + to object-initializer syntax. +- **Dedicated request DTOs**: Write operations now accept + purpose-built request DTOs instead of reusing response models or + inline parameters. 13 request DTOs created: + `CreateProjectRequest`, `UpdateProjectRequest`, + `CreateRepositoryRequest`, `ForkRepositoryRequest`, + `CreatePullRequestRequest`, `UpdatePullRequestRequest`, + `CreateBranchRequest`, `CreateTaskRequest`, `UpdateTaskRequest`, + `CreateWebHookRequest`, `UpdateWebHookRequest`, + `AssociateBuildStatusRequest`, `MergePullRequestRequest`. + Methods affected: `CreateProjectAsync`, `UpdateProjectAsync`, + `CreateProjectRepositoryAsync`, `CreateProjectRepositoryForkAsync`, + `CreatePullRequestAsync`, `UpdatePullRequestAsync`, + `MergePullRequestAsync`, `CreateBranchAsync`, `CreateTaskAsync`, + `UpdateTaskAsync`, `CreateProjectRepositoryWebHookAsync`, + `UpdateProjectRepositoryWebHookAsync`, + `AssociateBuildStatusWithCommitAsync`. + Request DTOs use `required` and `init`-only properties, + exposing only API-relevant fields. + +### Changed + +- **Source-gen-only deserialization**: Removed the + `JsonUnknownTypeHandling.JsonNode` reflection fallback from the + read-path `JsonSerializerOptions`. Deserialization now uses only + the source-generated `BitbucketJsonContext`, eliminating + reflection-based metadata and improving trim safety. A separate + `s_writeJsonOptions` retains a reflection fallback solely for + serializing anonymous types in request bodies. +- **Stream-based deserialization**: `ReadResponseContentAsync` + now calls `JsonSerializer.DeserializeAsync` directly on the HTTP + response stream instead of reading the body into a `string` first. + Eliminates a full UTF-16 string copy per response. +- **FrozenDictionary enum lookups**: All 25 enum-to-string + mapping dictionaries in `BitbucketHelpers` converted from + `Dictionary` to `FrozenDictionary`. + Added reverse `FrozenDictionary` with + `StringComparer.OrdinalIgnoreCase` for O(1) string-to-enum lookups. +- **Consolidated enum mappings**: Introduced generic + `EnumMap` as the single source of truth for all 25 + enum-to-string mappings. Replaced 13 individual converter + subclasses with a unified `BitbucketEnumConverterFactory`. + Slimmed `BitbucketHelpers.cs` from ~1,100 to ~490 lines. + Added public `ToApiString()` extension methods on all enum types. +- **Frozen `JsonSerializerOptions`**: Both `s_jsonOptions` + and `s_writeJsonOptions` are now explicitly frozen via + `MakeReadOnly()` at construction time, preventing accidental + mutation from any thread. +- **`IReadOnlyList` return types**: All 27 buffered + collection methods now return `Task>` instead of + `Task>`, communicating immutability and preventing + multiple-enumeration bugs. +- **Paginated endpoint helpers**: Introduced shared `GetPagedAsync` + and `GetPagedStreamAsync` methods, replacing ~300 lines of + duplicated pagination logic across 82 endpoints. +- **Dead code removal**: Removed unused `UnixDateTimeExtensions`, + `DictionaryExtensions`, uncalled `BitbucketHelpers` conversion + methods, and redundant `ExecuteAsync` overloads. + +### Added + +- **NuGet package metadata improvements**: Version bumped to + 1.0.0. Added `PackageIcon` (128×128 placeholder in `assets/icon.png`), + expanded `PackageTags` (`rest-api`, `api-client`, `atlassian`, `sdk`, + `dotnet`), and conditional icon inclusion with `Condition="Exists(…)"`. + `dotnet pack` now produces `BitbucketServer.Net.1.0.0.nupkg` with + README, icon, and full metadata. +- **Rate-limit headers on `BitbucketRateLimitException`**: + HTTP 429 exceptions now expose `RetryAfter`, `RateLimit`, + `RateLimitRemaining`, and `RateLimitReset` properties parsed from + standard rate-limit response headers. Gracefully returns `null` for + missing or unparseable headers. +- **`PagedResultsReader` zero-allocation metadata parser**: + Internal `Utf8JsonReader`-based parser that extracts pagination + metadata (`isLastPage`, `nextPageStart`, `start`, `limit`, `size`) + directly from UTF-8 bytes without deserializing the full payload. + Opt-in for hot-path streaming scenarios. +- **`IDisposable` on `BitbucketClient`**: The client now + implements `IDisposable` with ownership tracking. Clients created + via the `(string url, ...)` constructors own and dispose the + underlying `FlurlClient`. Clients created via `(IFlurlClient, ...)` + or `(HttpClient, ...)` do not dispose the injected client. All public + methods throw `ObjectDisposedException` after disposal. +- **`ExecuteAsync` centralised error handling**: New + `ExecuteAsync`, `ExecuteAsync` (bool), and + `ExecuteWithNoContentAsync` methods that wrap HTTP call + response + handling in a single call, reducing boilerplate in API methods. +- **Input validation guards**: ~130 public methods now + validate URL-path string parameters (`projectKey`, `repositorySlug`, + `commitId`, `hookKey`, `userSlug`, etc.) with + `ArgumentException.ThrowIfNullOrWhiteSpace()` at method entry. + Prevents malformed URLs and confusing server-side errors. +- **Fluent query builders**: New `PullRequestQueryBuilder`, + `CommitQueryBuilder`, `BranchQueryBuilder`, and `ProjectQueryBuilder` + classes providing a fluent API for complex queries. Entry points: + `client.PullRequests(...)`, `client.Commits(...)`, + `client.Branches(...)`, `client.Projects()`. Each builder supports + `GetAsync()` for buffered results and `StreamAsync()` for + `IAsyncEnumerable` streaming. Existing flat methods are unchanged. +- **OpenTelemetry tracing**: HTTP calls are traced via an internal + `ActivitySource` named `"Bitbucket.Net"`. Add it to your + `TracerProviderBuilder` to get per-request spans with method, URL, + and status code attributes. +- **`IBitbucketClient` interface**: Extracted from `BitbucketClient` + for dependency injection and unit testing. The interface is composed + of 12 domain-specific sub-interfaces (`IProjectOperations`, + `IRepositoryOperations`, `IPullRequestOperations`, etc.) so + consumers can depend on only the slice they need. +- **Code search**: `SearchCodeAsync` and `SearchCodeStreamAsync` + methods wrapping the Bitbucket Server code search REST API. + +### Testing + +- Added `SourceGenCoverageTests` validating all model types are + registered in `BitbucketJsonContext`. +- Added `BitbucketClientDisposeTests` (6 tests) covering disposal + semantics and ownership tracking. +- Added `FluentQueryBuilderMockTests` (12 tests) covering all four + query builders with default parameters, custom options, streaming, + and input validation. +- Added `ArchitecturalTests` verifying all HTTP calls have error + handlers (`HandleResponseAsync`, `ExecuteAsync`, or `StatusCode`), + that `JsonSerializerOptions` are explicitly frozen, and that every + `await` uses `ConfigureAwait(false)`. +- Added `InputValidationTests` (17 parameterized theories) covering + null/empty/whitespace rejection for key path-segment parameters. +- Total test count: 749. + +## [0.3.0] - 2026-02-010 + +### Added + +- **Code search**: `SearchCodeAsync` and `SearchCodeStreamAsync` + methods wrapping the Bitbucket Server code search REST API + (`/rest/search/latest/search`). ## [0.2.0] - 2026-02-08 diff --git a/README.md b/README.md index 1af2ed3..b4fa44c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![codecov](https://codecov.io/gh/diomonogatari/Bitbucket.Net/branch/main/graph/badge.svg)](https://codecov.io/gh/diomonogatari/Bitbucket.Net) [![license](https://img.shields.io/github/license/diomonogatari/Bitbucket.Net.svg?maxAge=2592000)](https://github.com/diomonogatari/Bitbucket.Net/blob/main/LICENSE) ![](https://img.shields.io/badge/.net-10.0-yellowgreen.svg) -![](https://img.shields.io/badge/status-0.x_(pre--stable)-orange.svg) +![](https://img.shields.io/badge/status-1.0.0_(stable)-brightgreen.svg) Modernized C# client for **Bitbucket Server** (Stash) REST API. @@ -15,24 +15,30 @@ Modernized C# client for **Bitbucket Server** (Stash) REST API. Development setup (including the pre-commit formatting hook) is documented in [CONTRIBUTING.md](CONTRIBUTING.md). -> **Fork notice** — This is an actively maintained fork of +> **Fork notice** — This is an actively maintained fork of > [lvermeulen/Bitbucket.Net](https://github.com/lvermeulen/Bitbucket.Net), > which appears to be abandoned (last release 2020). -> The library is at **0.x** — the API surface may still change -> between minor versions. It is used in production by the author (as the -> backend for an MCP Server talking to on-prem Bitbucket Server), but -> not every endpoint has been verified against a live instance. -> Contributions, bug reports, and feedback are very welcome. +> The 1.0.0 API surface is stable; breaking changes follow semver. +> The library is used in production by the author (as the backend for +> an MCP Server talking to on-prem Bitbucket Server), but not every +> endpoint has been verified against a live instance. +> Contributions, bug reports, and feedback are welcome. ### What changed from the original - .NET 10 target (dropped .NET Framework / .NET Standard) -- `System.Text.Json` instead of Newtonsoft.Json (2-3x faster, no CVEs) +- `System.Text.Json` with source generation (no runtime reflection) - `CancellationToken` on every async method - `IAsyncEnumerable` streaming for paginated endpoints - Streaming diffs and raw file content - Typed exception hierarchy (`BitbucketNotFoundException`, etc.) - `IHttpClientFactory` / DI-friendly constructors +- `IDisposable` with ownership tracking +- `IBitbucketClient` decomposed into 12 domain-specific sub-interfaces +- Fluent query builders for pull requests, commits, branches, and projects +- Dedicated request DTOs for write operations +- Input validation on all public API methods +- OpenTelemetry tracing via `ActivitySource` - Bitbucket Server 9.0+ blocker-comment (task) support with legacy fallback - Flurl.Http 4.x @@ -47,43 +53,100 @@ dotnet add package BitbucketServer.Net ## Usage ### Basic Authentication + ```csharp var client = new BitbucketClient("https://bitbucket.example.com", "username", "password"); ``` ### Token Authentication + ```csharp var client = new BitbucketClient("https://bitbucket.example.com", () => GetAccessToken()); ``` +### Resource management + +`BitbucketClient` implements `IDisposable`. Clients created with a URL +own the underlying HTTP connection and dispose it: + +```csharp +using var client = new BitbucketClient("https://bitbucket.example.com", "user", "pass"); +var projects = await client.GetProjectsAsync(); +``` + +When you inject an `HttpClient` or `IFlurlClient`, the caller retains +ownership; the client will not dispose it. + ### Dependency Injection with IHttpClientFactory -For production scenarios, you can inject an externally managed `HttpClient` to leverage `IHttpClientFactory` for connection pooling, resilience policies (via Polly), and centralized configuration: +For production scenarios, you can inject an externally managed `HttpClient` to leverage `IHttpClientFactory` for connection pooling, resilience policies, and centralized configuration. + +#### Standard resilience (recommended) + +The simplest approach uses `Microsoft.Extensions.Http.Resilience` which provides +retry, circuit breaker, and timeout out of the box: ```csharp -// In Program.cs or Startup.cs +// Requires: dotnet add package Microsoft.Extensions.Http.Resilience + services.AddHttpClient(client => { client.Timeout = TimeSpan.FromMinutes(2); }) -.AddTransientHttpErrorPolicy(p => - p.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))) -.AddTransientHttpErrorPolicy(p => - p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))); +.AddStandardResilienceHandler(options => +{ + options.Retry.MaxRetryAttempts = 3; + options.Retry.Delay = TimeSpan.FromSeconds(1); + options.Retry.BackoffType = DelayBackoffType.Exponential; + options.CircuitBreaker.FailureRatio = 0.5; + options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30); + options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(30); +}); -// Register BitbucketClient -services.AddSingleton(sp => +// Register IBitbucketClient for dependency injection +services.AddSingleton(sp => { var httpClientFactory = sp.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(BitbucketClient)); - + return new BitbucketClient( - httpClient, + httpClient, "https://bitbucket.example.com", () => sp.GetRequiredService().GetToken()); }); ``` +#### Custom resilience pipeline + +For fine-grained control over which responses trigger retries: + +```csharp +services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromMinutes(2); +}) +.AddResilienceHandler("bitbucket", builder => +{ + builder + .AddRetry(new HttpRetryStrategyOptions + { + MaxRetryAttempts = 3, + BackoffType = DelayBackoffType.Exponential, + Delay = TimeSpan.FromSeconds(1), + ShouldHandle = new PredicateBuilder() + .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests + || r.StatusCode >= HttpStatusCode.InternalServerError) + }) + .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions + { + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(30), + BreakDuration = TimeSpan.FromSeconds(15), + }) + .AddTimeout(TimeSpan.FromSeconds(30)); +}); +``` + ### Advanced: Using IFlurlClient For fine-grained control over Flurl's configuration: @@ -94,7 +157,7 @@ services.AddSingleton(sp => new FlurlClientCache() .WithSettings(s => s.Timeout = TimeSpan.FromMinutes(5)) .WithHeader("X-Custom-Header", "value"))); -services.AddSingleton(sp => +services.AddSingleton(sp => { var flurlClient = sp.GetRequiredService().Get("Bitbucket"); return new BitbucketClient(flurlClient, () => GetToken()); @@ -133,9 +196,33 @@ await foreach (var pr in client.GetDashboardPullRequestsStreamAsync()) } ``` -### Exception Handling +### Fluent query builders -The library provides typed exceptions for precise error handling: +For endpoints with many optional filters, query builders provide a typed +alternative to the flat method signatures: + +```csharp +var openPRs = await client.PullRequests("PROJ", "repo") + .InState(PullRequestStates.Open) + .OrderBy(PullRequestOrders.Newest) + .PageSize(25) + .GetAsync(); + +// Streaming variant +await foreach (var pr in client.PullRequests("PROJ", "repo") + .InState(PullRequestStates.Open) + .StreamAsync()) +{ + Console.WriteLine(pr.Title); +} +``` + +Builders are available for pull requests, commits, branches, and projects. +The original flat methods still work and are not deprecated. + +### Exception handling + +Typed exceptions give you precise control over error handling: ```csharp try @@ -172,74 +259,75 @@ dotnet run -c Release See [benchmarks/README.md](benchmarks/README.md) for detailed instructions. ## Features + * [X] Audit - * [X] Project Events - * [X] Repository Events -* [X] Branches - * [X] Create Branch - * [X] Delete Branch - * [X] Branch Info - * [X] Branch Model -* [X] Builds - * [X] Commits Build Stats - * [X] Commit Build Stats - * [X] Commit Build Status - * [X] Associate Build Status -* [X] Comment Likes - * [X] Repository Comment Likes - * [X] Pull Request Comment Likes -* [X] Core - * [X] Admin - * [X] Groups - * [X] Users - * [X] Cluster - * [X] License - * [X] Mail Server - * [X] Permissions - * [X] Pull Requests - * [X] Application Properties - * [X] Dashboard - * [X] Groups - * [X] Hooks - * [X] Inbox - * [X] Logs - * [X] Markup - * [X] Profile - * [X] Projects - * [X] Projects - * [X] Permissions - * [X] Repos - * [X] Repos - * [X] Branches - * [X] Browse - * [X] Changes - * [X] Commits - * [X] Compare - * [X] Diff - * [X] Files - * [X] Last Modified - * [X] Participants - * [X] Permissions - * [X] Pull Requests - * [X] Raw - * [X] Settings - * [X] Tags - * [X] Webhooks - * [X] Settings - * [X] Repos - * [X] Tasks - * [X] Users -* [X] Default Reviewers - * [X] Project Default Reviewers - * [X] Repository Default Reviewers -* [X] Git -* [X] JIRA - * [X] Create JIRA Issue - * [X] Get Commits For JIRA Issue - * [X] Get JIRA Issues For Commits -* [X] Personal Access Tokens -* [X] Ref Restrictions - * [X] Project Restrictions - * [X] Repository Restrictions -* [X] Repository Ref Synchronization -* [X] SSH + - [X] Project Events + - [X] Repository Events +- [X] Branches + - [X] Create Branch + - [X] Delete Branch + - [X] Branch Info + - [X] Branch Model +- [X] Builds + - [X] Commits Build Stats + - [X] Commit Build Stats + - [X] Commit Build Status + - [X] Associate Build Status +- [X] Comment Likes + - [X] Repository Comment Likes + - [X] Pull Request Comment Likes +- [X] Core + - [X] Admin + - [X] Groups + - [X] Users + - [X] Cluster + - [X] License + - [X] Mail Server + - [X] Permissions + - [X] Pull Requests + - [X] Application Properties + - [X] Dashboard + - [X] Groups + - [X] Hooks + - [X] Inbox + - [X] Logs + - [X] Markup + - [X] Profile + - [X] Projects + - [X] Projects + - [X] Permissions + - [X] Repos + - [X] Repos + - [X] Branches + - [X] Browse + - [X] Changes + - [X] Commits + - [X] Compare + - [X] Diff + - [X] Files + - [X] Last Modified + - [X] Participants + - [X] Permissions + - [X] Pull Requests + - [X] Raw + - [X] Settings + - [X] Tags + - [X] Webhooks + - [X] Settings + - [X] Repos + - [X] Tasks + - [X] Users +- [X] Default Reviewers + - [X] Project Default Reviewers + - [X] Repository Default Reviewers +- [X] Git +- [X] JIRA + - [X] Create JIRA Issue + - [X] Get Commits For JIRA Issue + - [X] Get JIRA Issues For Commits +- [X] Personal Access Tokens +- [X] Ref Restrictions + - [X] Project Restrictions + - [X] Repository Restrictions +- [X] Repository Ref Synchronization +- [X] SSH diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..c092ee9 Binary files /dev/null and b/assets/icon.png differ diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs index 483cf4e..b227621 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs @@ -62,6 +62,10 @@ private static void RunAllBenchmarks() Console.WriteLine("=== Zero-Copy Benchmarks ==="); BenchmarkRunner.Run(config); + Console.WriteLine(); + Console.WriteLine("=== PagedResultsReader Benchmarks ==="); + BenchmarkRunner.Run(config); + Console.WriteLine(); Console.WriteLine("All benchmarks completed!"); Console.WriteLine("Results are available in the BenchmarkDotNet.Artifacts folder."); diff --git a/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/PagedResultsReaderBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/PagedResultsReaderBenchmarks.cs new file mode 100644 index 0000000..ec7fa42 --- /dev/null +++ b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/PagedResultsReaderBenchmarks.cs @@ -0,0 +1,117 @@ +using BenchmarkDotNet.Attributes; +using Bitbucket.Net.Benchmarks.Config; +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Bitbucket.Net.Benchmarks.ZeroCopy; + +/// +/// Benchmarks comparing Utf8JsonReader-based metadata extraction +/// vs full JsonSerializer deserialization for paged API responses. +/// +[Config(typeof(DefaultBenchmarkConfig))] +[MemoryDiagnoser] +public class PagedResultsReaderBenchmarks +{ + private static readonly JsonSerializerOptions s_jsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private byte[] _emptyPayload = null!; + private byte[] _smallPayload = null!; + private byte[] _largePayload = null!; + + [GlobalSetup] + public void Setup() + { + _emptyPayload = Encoding.UTF8.GetBytes(CreatePagedJson(0)); + _smallPayload = Encoding.UTF8.GetBytes(CreatePagedJson(25)); + _largePayload = Encoding.UTF8.GetBytes(CreatePagedJson(100)); + } + + #region Empty payload (0 items) + + [Benchmark(Baseline = true, Description = "JsonSerializer - Empty")] + [BenchmarkCategory("Empty")] + public PagedResultsBase? JsonSerializer_Empty() + { + return JsonSerializer.Deserialize>(_emptyPayload, s_jsonOptions); + } + + [Benchmark(Description = "Utf8JsonReader - Empty")] + [BenchmarkCategory("Empty")] + public int Utf8JsonReader_Empty() + { + var m = PagedResultsReader.ReadMetadata(_emptyPayload); + return m.Size; + } + + #endregion + + #region Small payload (25 items) + + [Benchmark(Description = "JsonSerializer - 25 items")] + [BenchmarkCategory("Small")] + public PagedResultsBase? JsonSerializer_Small() + { + return JsonSerializer.Deserialize>(_smallPayload, s_jsonOptions); + } + + [Benchmark(Description = "Utf8JsonReader - 25 items")] + [BenchmarkCategory("Small")] + public int Utf8JsonReader_Small() + { + var m = PagedResultsReader.ReadMetadata(_smallPayload); + return m.Size; + } + + #endregion + + #region Large payload (100 items) + + [Benchmark(Description = "JsonSerializer - 100 items")] + [BenchmarkCategory("Large")] + public PagedResultsBase? JsonSerializer_Large() + { + return JsonSerializer.Deserialize>(_largePayload, s_jsonOptions); + } + + [Benchmark(Description = "Utf8JsonReader - 100 items")] + [BenchmarkCategory("Large")] + public int Utf8JsonReader_Large() + { + var m = PagedResultsReader.ReadMetadata(_largePayload); + return m.Size; + } + + #endregion + + #region Helper Methods + + private static string CreatePagedJson(int itemCount) + { + var items = Enumerable.Range(1, itemCount) + .Select(i => $$"""{"id":{{i}},"name":"item-{{i}}","description":"Description for item {{i}}","active":true,"tags":["tag1","tag2"]}"""); + + bool isLastPage = itemCount == 0; + int? nextPageStart = isLastPage ? null : 25; + + return $$"""{"size":{{itemCount}},"limit":25,"isLastPage":{{(isLastPage ? "true" : "false")}},"start":0{{(nextPageStart.HasValue ? $",\"nextPageStart\":{nextPageStart}" : "")}},"values":[{{string.Join(",", items)}}]}"""; + } + + #endregion +} + +public sealed class BenchmarkItem +{ + public int Id { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public bool Active { get; set; } + public List? Tags { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Audit/BitbucketClient.cs b/src/Bitbucket.Net/Audit/BitbucketClient.cs index c2969f8..a8fa8c3 100644 --- a/src/Bitbucket.Net/Audit/BitbucketClient.cs +++ b/src/Bitbucket.Net/Audit/BitbucketClient.cs @@ -1,5 +1,3 @@ -using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Audit; using Flurl.Http; @@ -31,13 +29,15 @@ private IFlurlRequest GetAuditUrl(string path) => GetAuditUrl() /// The size of user avatars to include in the response. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains a collection of objects. - public async Task> GetProjectAuditEventsAsync(string projectKey, + public Task> GetProjectAuditEventsAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -45,16 +45,8 @@ public async Task> GetProjectAuditEventsAsync(string pro ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAuditUrl($"/projects/{projectKey}/events") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAuditUrl($"/projects/{projectKey}/events"), queryParamValues, maxPages, cancellationToken); } /// @@ -68,13 +60,16 @@ public async Task> GetProjectAuditEventsAsync(string pro /// The size of user avatars to include in the response. /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation. The task result contains a collection of objects. - public async Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -82,15 +77,7 @@ public async Task> GetProjectRepoAuditEventsAsync(string ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAuditUrl($"/projects/{projectKey}/repos/{repositorySlug}/events") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAuditUrl($"/projects/{projectKey}/repos/{repositorySlug}/events"), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Bitbucket.Net.csproj b/src/Bitbucket.Net/Bitbucket.Net.csproj index d40f0a9..88f3141 100644 --- a/src/Bitbucket.Net/Bitbucket.Net.csproj +++ b/src/Bitbucket.Net/Bitbucket.Net.csproj @@ -2,15 +2,16 @@ BitbucketServer.Net - 0.2.0 + 1.0.0 Diogo Carvalho Modernized fork of Bitbucket.Net — C# client for Bitbucket Server (Stash) REST API. Adds streaming, CancellationToken support, System.Text.Json, typed exceptions, and Bitbucket Server 9.0+ compatibility. - bitbucket;bitbucket-server;stash;api;client;rest + bitbucket;bitbucket-server;stash;rest-api;api-client;atlassian;sdk;dotnet https://github.com/diomonogatari/Bitbucket.Net https://github.com/diomonogatari/Bitbucket.Net.git git MIT README.md + icon.png See https://github.com/diomonogatari/Bitbucket.Net/blob/main/CHANGELOG.md Copyright (c) Diogo Carvalho true @@ -19,6 +20,11 @@ true + + + + + @@ -35,6 +41,7 @@ + diff --git a/src/Bitbucket.Net/BitbucketClient.Tracing.cs b/src/Bitbucket.Net/BitbucketClient.Tracing.cs new file mode 100644 index 0000000..ae7f748 --- /dev/null +++ b/src/Bitbucket.Net/BitbucketClient.Tracing.cs @@ -0,0 +1,104 @@ +using Flurl.Http; +using System.Diagnostics; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + internal static readonly ActivitySource ActivitySource = new("Bitbucket.Net"); + + private static readonly HttpRequestOptionsKey s_activityKey = new("Bitbucket.Net.Activity"); + + private static void OnBeforeCall(FlurlCall call) + { + if (!ActivitySource.HasListeners()) + { + return; + } + + var httpMethod = call.Request.Verb.Method; + var activity = ActivitySource.StartActivity(httpMethod, ActivityKind.Client); + + if (activity is null) + { + return; + } + + var url = call.Request.Url; + activity.SetTag("http.request.method", httpMethod); + activity.SetTag("url.full", url.ToString()); + activity.SetTag("server.address", url.Host); + + if (url.Port is not null) + { + activity.SetTag("server.port", url.Port.Value); + } + + SetBitbucketTags(activity, url.Path); + + // Store the activity on the request message so we can retrieve it in AfterCall/OnError. + // Activity.Current is not reliable here because Flurl's RaiseEventAsync is async, + // and AsyncLocal changes inside async methods don't propagate to the caller. + call.HttpRequestMessage.Options.Set(s_activityKey, activity); + } + + private static void OnAfterCall(FlurlCall call) + { + if (!call.HttpRequestMessage.Options.TryGetValue(s_activityKey, out var activity)) + { + return; + } + + if (call.Response is not null) + { + var statusCode = call.Response.StatusCode; + activity.SetTag("http.response.status_code", statusCode); + + if (statusCode >= 400) + { + activity.SetStatus(ActivityStatusCode.Error); + activity.SetTag("error.type", statusCode.ToString()); + } + } + + activity.Stop(); + } + + private static void OnErrorCall(FlurlCall call) + { + if (!call.HttpRequestMessage.Options.TryGetValue(s_activityKey, out var activity)) + { + return; + } + + if (call.Exception is not null) + { + activity.SetStatus(ActivityStatusCode.Error, call.Exception.Message); + activity.SetTag("error.type", call.Exception.GetType().FullName); + activity.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection + { + { "exception.type", call.Exception.GetType().FullName }, + { "exception.message", call.Exception.Message }, + })); + } + + activity.Stop(); + } + + private static void SetBitbucketTags(Activity activity, string path) + { + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < segments.Length - 1; i++) + { + if (string.Equals(segments[i], "projects", StringComparison.OrdinalIgnoreCase)) + { + activity.SetTag("bitbucket.project_key", segments[i + 1]); + } + else if (string.Equals(segments[i], "repos", StringComparison.OrdinalIgnoreCase)) + { + activity.SetTag("bitbucket.repository_slug", segments[i + 1]); + } + } + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index d3ffb9c..9bdd0b7 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -16,37 +16,55 @@ namespace Bitbucket.Net; /// /// Client for interacting with Bitbucket Server REST APIs. +/// +/// The client implements . When created via the +/// constructor, +/// the client owns the internal wrapper and disposes it. +/// When created via the constructor, +/// the caller retains ownership of the and is responsible for its disposal. +/// /// -public partial class BitbucketClient +public partial class BitbucketClient : IBitbucketClient { - private static readonly JsonSerializerOptions s_jsonOptions = new() + private static readonly JsonSerializerOptions s_jsonOptions = CreateReadOptions(); + private static readonly JsonSerializerOptions s_writeJsonOptions = CreateWriteOptions(); + + private static JsonSerializerOptions CreateReadOptions() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + // Source-generated context only — no reflection fallback. + // Missing [JsonSerializable] registrations in BitbucketJsonContext will throw + // NotSupportedException at the call site, surfacing the problem immediately. + TypeInfoResolver = BitbucketJsonContext.Default, + Converters = + { + new UnixDateTimeOffsetConverter(), + new NullableUnixDateTimeOffsetConverter(), + new BitbucketEnumConverterFactory(), + }, + }; + options.MakeReadOnly(); + return options; + } + + // Write-only options for serializing outbound request bodies. + // Includes a reflection fallback for anonymous types used in API methods. + // Future improvement: replace anonymous types with typed request DTOs and remove this fallback. + private static JsonSerializerOptions CreateWriteOptions() { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - // Source-generated context with reflection fallback for edge cases - TypeInfoResolver = JsonTypeInfoResolver.Combine( - BitbucketJsonContext.Default, // Source-generated (fast path) - new DefaultJsonTypeInfoResolver() // Reflection fallback for unregistered types - ), - Converters = + var options = new JsonSerializerOptions(s_jsonOptions) { - new UnixDateTimeOffsetConverter(), - new NullableUnixDateTimeOffsetConverter(), - new PermissionsConverter(), - new RolesConverter(), - new FileTypesConverter(), - new LineTypesConverter(), - new ParticipantStatusConverter(), - new PullRequestStatesConverter(), - new HookTypesConverter(), - new ScopeTypesConverter(), - new WebHookOutcomesConverter(), - new RefRestrictionTypesConverter(), - new SynchronizeActionsConverter(), - new BlockerCommentStateConverter(), - new CommentSeverityConverter() - }, - }; + TypeInfoResolver = JsonTypeInfoResolver.Combine( + BitbucketJsonContext.Default, + new DefaultJsonTypeInfoResolver() + ), + }; + options.MakeReadOnly(); + return options; + } private static readonly ISerializer s_serializer = new DefaultJsonSerializer(s_jsonOptions); @@ -55,6 +73,8 @@ public partial class BitbucketClient private readonly string? _userName; private readonly string? _password; private readonly IFlurlClient? _injectedClient; + private readonly bool _ownsClient; + private bool _disposed; /// /// Initializes a new instance of the class with the specified base URL. @@ -127,6 +147,7 @@ public BitbucketClient(HttpClient httpClient, string baseUrl, Func? getT _getToken = getToken; _injectedClient = new FlurlClient(httpClient, baseUrl) .WithSettings(settings => settings.JsonSerializer = s_serializer); + _ownsClient = true; } /// @@ -146,14 +167,39 @@ public BitbucketClient(IFlurlClient flurlClient, Func? getToken = null) _getToken = getToken; } + /// + /// Releases the resources used by the . + /// When the client was created via the constructor, + /// the internal wrapper is disposed. + /// When created via the constructor, disposal is a no-op + /// since the caller retains ownership. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_ownsClient) + { + _injectedClient?.Dispose(); + } + } + /// /// Builds a Flurl request rooted at the Bitbucket REST API. /// /// The API root segment (default is /api). /// The API version segment (default is 1.0). /// An configured with authentication and serialization. + /// Thrown when the client has been disposed. private IFlurlRequest GetBaseUrl(string root = "/api", string version = "1.0") { + ObjectDisposedException.ThrowIf(_disposed, this); + IFlurlRequest request; // If using injected client, use it directly @@ -180,7 +226,10 @@ private IFlurlRequest GetBaseUrl(string root = "/api", string version = "1.0") return request .AllowAnyHttpStatus() - .WithSettings(settings => settings.JsonSerializer = s_serializer); + .WithSettings(settings => settings.JsonSerializer = s_serializer) + .BeforeCall(OnBeforeCall) + .AfterCall(OnAfterCall) + .OnError(OnErrorCall); } private static async Task ReadResponseStringAsync(IFlurlResponse response, CancellationToken cancellationToken) @@ -195,7 +244,7 @@ private static async Task ReadResponseStringAsync(IFlurlResponse respons private static StringContent CreateJsonContent(TValue value) { - var json = JsonSerializer.Serialize(value, s_jsonOptions); + var json = JsonSerializer.Serialize(value, s_writeJsonOptions); return new StringContent(json, Encoding.UTF8, "application/json"); } @@ -226,27 +275,34 @@ private static async Task ReadResponseStreamAsync(IFlurlResponse respons /// /// Reads the response content and deserializes it. + /// When no custom content handler is provided, deserializes directly from the response stream + /// to avoid intermediate string allocations (especially beneficial for large paged responses). /// /// The type of the result. /// The HTTP response. - /// Optional custom handler to parse the response content. + /// Optional custom handler to parse the response content as a string. /// Token to cancel the operation. /// The deserialized response content. private static async Task ReadResponseContentAsync(IFlurlResponse response, Func? contentHandler = null, CancellationToken cancellationToken = default) { - string content = await ReadResponseStringAsync(response, cancellationToken).ConfigureAwait(false); - + // Custom handler needs the raw string (used for non-JSON responses) if (contentHandler is not null) { + string content = await ReadResponseStringAsync(response, cancellationToken).ConfigureAwait(false); return contentHandler(content); } - if (string.IsNullOrWhiteSpace(content)) + // Deserialize directly from the stream — avoids intermediate string allocation + var stream = await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - return default!; - } + if (stream == Stream.Null) + { + return default!; + } - return JsonSerializer.Deserialize(content, s_jsonOptions)!; + return (await JsonSerializer.DeserializeAsync(stream, s_jsonOptions, cancellationToken).ConfigureAwait(false))!; + } } /// @@ -302,7 +358,8 @@ private static async Task HandleErrorsAsync(IFlurlResponse response, Cancellatio } } - throw BitbucketApiException.Create(response.StatusCode, errors, requestUrl); + var responseHeaders = response.ResponseMessage?.Headers; + throw BitbucketApiException.Create(response.StatusCode, errors, responseHeaders, requestUrl); } } @@ -332,6 +389,50 @@ private static async Task HandleResponseAsync(IFlurlResponse response, Can return await ReadResponseContentAsync(response, cancellationToken).ConfigureAwait(false); } + /// + /// Convenience wrapper that builds the standard GET + deserialize lambda + /// and delegates to . + /// + private Task> GetPagedAsync( + IFlurlRequest request, + IDictionary queryParams, + int? maxPages = null, + CancellationToken cancellationToken = default) + { + return GetPagedResultsAsync(maxPages, queryParams, async (qpv, ct) => + { + var response = await request + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct) + .ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Convenience wrapper that builds the standard GET + deserialize lambda + /// and delegates to . + /// + private IAsyncEnumerable GetPagedStreamAsync( + IFlurlRequest request, + IDictionary queryParams, + int? maxPages = null, + CancellationToken cancellationToken = default) + { + return GetPagedResultsStreamAsync(maxPages, queryParams, async (qpv, ct) => + { + var response = await request + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct) + .ConfigureAwait(false); + }, cancellationToken); + } + /// /// Retrieves paged results from a paginated endpoint. /// @@ -341,7 +442,7 @@ private static async Task HandleResponseAsync(IFlurlResponse response, Can /// A delegate that retrieves a page of results. /// Token to cancel the operation. /// All retrieved items. - private static async Task> GetPagedResultsAsync(int? maxPages, IDictionary queryParamValues, Func, CancellationToken, Task>> selector, CancellationToken cancellationToken = default) + private static async Task> GetPagedResultsAsync(int? maxPages, IDictionary queryParamValues, Func, CancellationToken, Task>> selector, CancellationToken cancellationToken = default) { var results = new List(); bool isLastPage = false; diff --git a/src/Bitbucket.Net/Branches/BitbucketClient.cs b/src/Bitbucket.Net/Branches/BitbucketClient.cs index e7fdb11..74c23a1 100644 --- a/src/Bitbucket.Net/Branches/BitbucketClient.cs +++ b/src/Bitbucket.Net/Branches/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Branches; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; @@ -38,28 +37,24 @@ private IFlurlRequest GetBranchUrl(string path) => GetBranchUrl() /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of branch information entries for the commit. - public async Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, + public Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(fullSha); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches/info/{fullSha}") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches/info/{fullSha}"), queryParamValues, maxPages, cancellationToken); } /// @@ -71,6 +66,9 @@ public async Task> GetCommitBranchInfoAsync(string proje /// The branch model configuration. public async Task GetRepoBranchModelAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branchmodel") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -89,6 +87,11 @@ public async Task GetRepoBranchModelAsync(string projectKey, string /// The created branch. public async Task CreateRepoBranchAsync(string projectKey, string repositorySlug, string branchName, string startPoint, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(branchName); + ArgumentException.ThrowIfNullOrWhiteSpace(startPoint); + var data = new { name = branchName, @@ -114,6 +117,10 @@ public async Task CreateRepoBranchAsync(string projectKey, string reposi /// true if the branch was deleted; otherwise, false. public async Task DeleteRepoBranchAsync(string projectKey, string repositorySlug, string branchName, bool dryRun, string? endPoint = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(branchName); + var data = new { name = branchName, @@ -121,7 +128,7 @@ public async Task DeleteRepoBranchAsync(string projectKey, string reposito endPoint, }; - var json = JsonSerializer.Serialize(data, s_jsonOptions); + var json = JsonSerializer.Serialize(data, s_writeJsonOptions); var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches") .WithHeader("Content-Type", "application/json") .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) diff --git a/src/Bitbucket.Net/Builders/BitbucketClient.Builders.cs b/src/Bitbucket.Net/Builders/BitbucketClient.Builders.cs new file mode 100644 index 0000000..2c999c5 --- /dev/null +++ b/src/Bitbucket.Net/Builders/BitbucketClient.Builders.cs @@ -0,0 +1,52 @@ +using Bitbucket.Net.Builders; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Returns a fluent builder for querying pull requests in a repository. + /// + /// The project key. + /// The repository slug. + public PullRequestQueryBuilder PullRequests(string projectKey, string repositorySlug) + { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + return new PullRequestQueryBuilder(this, projectKey, repositorySlug); + } + + /// + /// Returns a fluent builder for querying commits in a repository. + /// + /// The project key. + /// The repository slug. + /// The commit or ref to walk back from (required). + public CommitQueryBuilder Commits(string projectKey, string repositorySlug, string until) + { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(until); + return new CommitQueryBuilder(this, projectKey, repositorySlug, until); + } + + /// + /// Returns a fluent builder for querying branches in a repository. + /// + /// The project key. + /// The repository slug. + public BranchQueryBuilder Branches(string projectKey, string repositorySlug) + { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + return new BranchQueryBuilder(this, projectKey, repositorySlug); + } + + /// + /// Returns a fluent builder for querying projects. + /// + public ProjectQueryBuilder Projects() + { + return new ProjectQueryBuilder(this); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs b/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs new file mode 100644 index 0000000..dc66965 --- /dev/null +++ b/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs @@ -0,0 +1,61 @@ +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net.Builders; + +/// +/// Fluent builder for querying branches in a repository. +/// +public sealed class BranchQueryBuilder +{ + private readonly IBitbucketClient _client; + private readonly string _projectKey; + private readonly string _repositorySlug; + + private int? _maxPages; + private int? _limit; + private int? _start; + private string? _baseBranchOrTag; + private bool? _details; + private string? _filterText; + private BranchOrderBy? _orderBy; + + internal BranchQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug) + { + _client = client; + _projectKey = projectKey; + _repositorySlug = repositorySlug; + } + + /// Sets the base branch or tag for relative listing. + public BranchQueryBuilder Base(string baseBranchOrTag) { _baseBranchOrTag = baseBranchOrTag; return this; } + + /// Includes branch details (e.g. ahead/behind counts). + public BranchQueryBuilder WithDetails(bool details = true) { _details = details; return this; } + + /// Filters branches by display name prefix. + public BranchQueryBuilder FilterBy(string filterText) { _filterText = filterText; return this; } + + /// Sets the branch sort order. + public BranchQueryBuilder OrderBy(BranchOrderBy orderBy) { _orderBy = orderBy; return this; } + + /// Sets the page size (items per API call). + public BranchQueryBuilder PageSize(int limit) { _limit = limit; return this; } + + /// Sets the maximum number of pages to fetch. + public BranchQueryBuilder MaxPages(int pages) { _maxPages = pages; return this; } + + /// Sets the start index for pagination. + public BranchQueryBuilder StartAt(int start) { _start = start; return this; } + + /// Executes the query and returns all matching branches. + public Task> GetAsync(CancellationToken cancellationToken = default) + => _client.GetBranchesAsync(_projectKey, _repositorySlug, + _maxPages, _limit, _start, _baseBranchOrTag, _details, _filterText, + _orderBy, cancellationToken); + + /// Executes the query and streams matching branches one at a time. + public IAsyncEnumerable StreamAsync(CancellationToken cancellationToken = default) + => _client.GetBranchesStreamAsync(_projectKey, _repositorySlug, + _maxPages, _limit, _start, _baseBranchOrTag, _details, _filterText, + _orderBy, cancellationToken); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs b/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs new file mode 100644 index 0000000..c830070 --- /dev/null +++ b/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs @@ -0,0 +1,71 @@ +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net.Builders; + +/// +/// Fluent builder for querying commits in a repository. +/// +public sealed class CommitQueryBuilder +{ + private readonly IBitbucketClient _client; + private readonly string _projectKey; + private readonly string _repositorySlug; + private readonly string _until; + + private bool _followRenames; + private bool _ignoreMissing; + private MergeCommits _merges = MergeCommits.Exclude; + private string? _path; + private string? _since; + private bool _withCounts; + private int? _maxPages; + private int? _limit; + private int? _start; + + internal CommitQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug, string until) + { + _client = client; + _projectKey = projectKey; + _repositorySlug = repositorySlug; + _until = until; + } + + /// Follow file renames in history. + public CommitQueryBuilder FollowRenames(bool follow = true) { _followRenames = follow; return this; } + + /// Ignore missing commits instead of failing. + public CommitQueryBuilder IgnoreMissing(bool ignore = true) { _ignoreMissing = ignore; return this; } + + /// Controls inclusion of merge commits. + public CommitQueryBuilder Merges(MergeCommits merges) { _merges = merges; return this; } + + /// Filters commits that touch the given file path. + public CommitQueryBuilder AtPath(string path) { _path = path; return this; } + + /// Returns commits after (exclusive) the given commit or ref. + public CommitQueryBuilder Since(string since) { _since = since; return this; } + + /// Includes commit count metadata. + public CommitQueryBuilder WithCounts(bool include = true) { _withCounts = include; return this; } + + /// Sets the page size (items per API call). + public CommitQueryBuilder PageSize(int limit) { _limit = limit; return this; } + + /// Sets the maximum number of pages to fetch. + public CommitQueryBuilder MaxPages(int pages) { _maxPages = pages; return this; } + + /// Sets the start index for pagination. + public CommitQueryBuilder StartAt(int start) { _start = start; return this; } + + /// Executes the query and returns all matching commits. + public Task> GetAsync(CancellationToken cancellationToken = default) + => _client.GetCommitsAsync(_projectKey, _repositorySlug, _until, + _followRenames, _ignoreMissing, _merges, _path, _since, _withCounts, + _maxPages, _limit, _start, cancellationToken); + + /// Executes the query and streams matching commits one at a time. + public IAsyncEnumerable StreamAsync(CancellationToken cancellationToken = default) + => _client.GetCommitsStreamAsync(_projectKey, _repositorySlug, _until, + _followRenames, _ignoreMissing, _merges, _path, _since, _withCounts, + _maxPages, _limit, _start, cancellationToken); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs b/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs new file mode 100644 index 0000000..3dc26af --- /dev/null +++ b/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs @@ -0,0 +1,46 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net.Builders; + +/// +/// Fluent builder for querying projects. +/// +public sealed class ProjectQueryBuilder +{ + private readonly IBitbucketClient _client; + + private int? _maxPages; + private int? _limit; + private int? _start; + private string? _name; + private Permissions? _permission; + + internal ProjectQueryBuilder(IBitbucketClient client) + { + _client = client; + } + + /// Filters projects by name prefix. + public ProjectQueryBuilder NameFilter(string name) { _name = name; return this; } + + /// Filters projects by the current user's permission level. + public ProjectQueryBuilder WithPermission(Permissions permission) { _permission = permission; return this; } + + /// Sets the page size (items per API call). + public ProjectQueryBuilder PageSize(int limit) { _limit = limit; return this; } + + /// Sets the maximum number of pages to fetch. + public ProjectQueryBuilder MaxPages(int pages) { _maxPages = pages; return this; } + + /// Sets the start index for pagination. + public ProjectQueryBuilder StartAt(int start) { _start = start; return this; } + + /// Executes the query and returns all matching projects. + public Task> GetAsync(CancellationToken cancellationToken = default) + => _client.GetProjectsAsync(_maxPages, _limit, _start, _name, _permission, cancellationToken); + + /// Executes the query and streams matching projects one at a time. + public IAsyncEnumerable StreamAsync(CancellationToken cancellationToken = default) + => _client.GetProjectsStreamAsync(_maxPages, _limit, _start, _name, _permission, cancellationToken); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs b/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs new file mode 100644 index 0000000..74a80cb --- /dev/null +++ b/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs @@ -0,0 +1,69 @@ +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net.Builders; + +/// +/// Fluent builder for querying pull requests in a repository. +/// +public sealed class PullRequestQueryBuilder +{ + private readonly IBitbucketClient _client; + private readonly string _projectKey; + private readonly string _repositorySlug; + + private int? _maxPages; + private int? _limit; + private int? _start; + private PullRequestDirections _direction = PullRequestDirections.Incoming; + private string? _branchId; + private PullRequestStates _state = PullRequestStates.Open; + private PullRequestOrders _order = PullRequestOrders.Newest; + private bool _withAttributes = true; + private bool _withProperties = true; + + internal PullRequestQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug) + { + _client = client; + _projectKey = projectKey; + _repositorySlug = repositorySlug; + } + + /// Filters pull requests by state. + public PullRequestQueryBuilder InState(PullRequestStates state) { _state = state; return this; } + + /// Sets the sort order. + public PullRequestQueryBuilder OrderBy(PullRequestOrders order) { _order = order; return this; } + + /// Sets the direction filter (incoming/outgoing). + public PullRequestQueryBuilder WithDirection(PullRequestDirections direction) { _direction = direction; return this; } + + /// Filters by branch ref (e.g. "refs/heads/feature"). + public PullRequestQueryBuilder AtBranch(string branchId) { _branchId = branchId; return this; } + + /// Sets the page size (items per API call). + public PullRequestQueryBuilder PageSize(int limit) { _limit = limit; return this; } + + /// Sets the maximum number of pages to fetch. + public PullRequestQueryBuilder MaxPages(int pages) { _maxPages = pages; return this; } + + /// Sets the start index for pagination. + public PullRequestQueryBuilder StartAt(int start) { _start = start; return this; } + + /// Includes or excludes pull request attributes. + public PullRequestQueryBuilder IncludeAttributes(bool include = true) { _withAttributes = include; return this; } + + /// Includes or excludes pull request properties. + public PullRequestQueryBuilder IncludeProperties(bool include = true) { _withProperties = include; return this; } + + /// Executes the query and returns all matching pull requests. + public Task> GetAsync(CancellationToken cancellationToken = default) + => _client.GetPullRequestsAsync(_projectKey, _repositorySlug, + _maxPages, _limit, _start, _direction, _branchId, _state, _order, + _withAttributes, _withProperties, cancellationToken); + + /// Executes the query and streams matching pull requests one at a time. + public IAsyncEnumerable StreamAsync(CancellationToken cancellationToken = default) + => _client.GetPullRequestsStreamAsync(_projectKey, _repositorySlug, + _maxPages, _limit, _start, _direction, _branchId, _state, _order, + _withAttributes, _withProperties, cancellationToken); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index 591fabc..a6ccdce 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -1,6 +1,6 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Builds.Requests; using Flurl.Http; namespace Bitbucket.Net; @@ -33,6 +33,8 @@ private IFlurlRequest GetBuildsUrl(string path) => GetBuildsUrl() /// Build statistics for the commit. public async Task GetBuildStatsForCommitAsync(string commitId, bool includeUnique = false, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetBuildsUrl($"/commits/stats/{commitId}") .SetQueryParam("includeUnique", BitbucketHelpers.BoolToString(includeUnique)) .GetAsync(cancellationToken) @@ -75,40 +77,38 @@ public async Task> GetBuildStatsForCommitsAsync(p /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of build status entries. - public async Task> GetBuildStatusForCommitAsync(string commitId, + public Task> GetBuildStatusForCommitAsync(string commitId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetBuildsUrl($"/commits/{commitId}") - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetBuildsUrl($"/commits/{commitId}"), queryParamValues, maxPages, cancellationToken); } /// /// Associates a build status with a commit. /// /// The commit identifier. - /// The build status to associate. + /// The build status request to associate. /// Token to cancel the operation. /// true if the association was successful; otherwise, false. - public async Task AssociateBuildStatusWithCommitAsync(string commitId, BuildStatus buildStatus, CancellationToken cancellationToken = default) + public async Task AssociateBuildStatusWithCommitAsync(string commitId, AssociateBuildStatusRequest request, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + ArgumentNullException.ThrowIfNull(request); + var response = await GetBuildsUrl($"/commits/{commitId}") - .SendAsync(HttpMethod.Post, CreateJsonContent(buildStatus), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Builds/IBuildOperations.cs b/src/Bitbucket.Net/Builds/IBuildOperations.cs new file mode 100644 index 0000000..69eb1eb --- /dev/null +++ b/src/Bitbucket.Net/Builds/IBuildOperations.cs @@ -0,0 +1,16 @@ +using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Builds.Requests; + +namespace Bitbucket.Net; + +/// +/// Build status operations. +/// +public interface IBuildOperations +{ + Task GetBuildStatsForCommitAsync(string commitId, bool includeUnique = false, CancellationToken cancellationToken = default); + Task> GetBuildStatsForCommitsAsync(CancellationToken cancellationToken, params string[] commitIds); + Task> GetBuildStatsForCommitsAsync(params string[] commitIds); + Task> GetBuildStatusForCommitAsync(string commitId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task AssociateBuildStatusWithCommitAsync(string commitId, AssociateBuildStatusRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs index a09675b..4682b4a 100644 --- a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs +++ b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs @@ -1,5 +1,3 @@ -using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; @@ -37,13 +35,18 @@ private IFlurlRequest GetCommentLikesUrl(string path) => GetCommentLikesUrl() /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of users who liked the comment. - public async Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, + public Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -51,16 +54,8 @@ public async Task> GetCommitCommentLikesAsync(string projectKe ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes"), queryParamValues, maxPages, cancellationToken); } /// @@ -74,6 +69,11 @@ public async Task> GetCommitCommentLikesAsync(string projectKe /// true if the like was added; otherwise, false. public async Task LikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") .SendAsync(HttpMethod.Post, CreateEmptyJsonContent(), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -92,6 +92,11 @@ public async Task LikeCommitCommentAsync(string projectKey, string reposit /// true if the like was removed; otherwise, false. public async Task UnlikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -111,28 +116,25 @@ public async Task UnlikeCommitCommentAsync(string projectKey, string repos /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of users who liked the pull request comment. - public async Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, + public Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(pullRequestId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes"), queryParamValues, maxPages, cancellationToken); } /// @@ -146,6 +148,11 @@ public async Task> GetPullRequestCommentLikesAsync(string proj /// true if the like was added; otherwise, false. public async Task LikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(pullRequestId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") .SendAsync(HttpMethod.Post, CreateEmptyJsonContent(), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -164,6 +171,11 @@ public async Task LikePullRequestCommentAsync(string projectKey, string re /// true if the like was removed; otherwise, false. public async Task UnlikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(pullRequestId); + ArgumentException.ThrowIfNullOrWhiteSpace(commentId); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Common/BitbucketEnumMaps.cs b/src/Bitbucket.Net/Common/BitbucketEnumMaps.cs new file mode 100644 index 0000000..18ef231 --- /dev/null +++ b/src/Bitbucket.Net/Common/BitbucketEnumMaps.cs @@ -0,0 +1,221 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Logs; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Git; +using Bitbucket.Net.Models.RefRestrictions; +using Bitbucket.Net.Models.RefSync; + +namespace Bitbucket.Net.Common; + +/// +/// Central registry of all enum-to-string mappings used by the Bitbucket API. +/// Each is the single source of truth for both +/// JSON converters and query-parameter serialization. +/// +public static class BitbucketEnumMaps +{ + /// Mapping for . + public static EnumMap BranchOrderBy { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.BranchOrderBy.Alphabetical] = "ALPHABETICAL", + [Bitbucket.Net.Models.Core.Projects.BranchOrderBy.Modification] = "MODIFICATION", + }, createReverse: false); + + /// Mapping for . + public static EnumMap PullRequestDirections { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.PullRequestDirections.Incoming] = "INCOMING", + [Bitbucket.Net.Models.Core.Projects.PullRequestDirections.Outgoing] = "OUTGOING", + }, createReverse: false); + + /// Mapping for . + public static EnumMap PullRequestStates { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.PullRequestStates.Open] = "OPEN", + [Bitbucket.Net.Models.Core.Projects.PullRequestStates.Declined] = "DECLINED", + [Bitbucket.Net.Models.Core.Projects.PullRequestStates.Merged] = "MERGED", + [Bitbucket.Net.Models.Core.Projects.PullRequestStates.All] = "ALL", + }); + + /// Mapping for . + public static EnumMap PullRequestOrders { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.PullRequestOrders.Newest] = "NEWEST", + [Bitbucket.Net.Models.Core.Projects.PullRequestOrders.Oldest] = "OLDEST", + }, createReverse: false); + + /// Mapping for . + public static EnumMap PullRequestFromTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.PullRequestFromTypes.Comment] = "COMMENT", + [Bitbucket.Net.Models.Core.Projects.PullRequestFromTypes.Activity] = "ACTIVITY", + }, createReverse: false); + + /// Mapping for . + public static EnumMap Permissions { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Admin.Permissions.Admin] = "ADMIN", + [Bitbucket.Net.Models.Core.Admin.Permissions.LicensedUser] = "LICENSED_USER", + [Bitbucket.Net.Models.Core.Admin.Permissions.ProjectAdmin] = "PROJECT_ADMIN", + [Bitbucket.Net.Models.Core.Admin.Permissions.ProjectCreate] = "PROJECT_CREATE", + [Bitbucket.Net.Models.Core.Admin.Permissions.ProjectRead] = "PROJECT_READ", + [Bitbucket.Net.Models.Core.Admin.Permissions.ProjectView] = "PROJECT_VIEW", + [Bitbucket.Net.Models.Core.Admin.Permissions.ProjectWrite] = "PROJECT_WRITE", + [Bitbucket.Net.Models.Core.Admin.Permissions.RepoAdmin] = "REPO_ADMIN", + [Bitbucket.Net.Models.Core.Admin.Permissions.RepoRead] = "REPO_READ", + [Bitbucket.Net.Models.Core.Admin.Permissions.RepoWrite] = "REPO_WRITE", + [Bitbucket.Net.Models.Core.Admin.Permissions.SysAdmin] = "SYS_ADMIN", + }); + + /// Mapping for . + public static EnumMap MergeCommits { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.MergeCommits.Exclude] = "exclude", + [Bitbucket.Net.Models.Core.Projects.MergeCommits.Include] = "include", + [Bitbucket.Net.Models.Core.Projects.MergeCommits.Only] = "only", + }, createReverse: false); + + /// Mapping for . + public static EnumMap Roles { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.Roles.Author] = "AUTHOR", + [Bitbucket.Net.Models.Core.Projects.Roles.Reviewer] = "REVIEWER", + [Bitbucket.Net.Models.Core.Projects.Roles.Participant] = "PARTICIPANT", + }); + + /// Mapping for . + public static EnumMap LineTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.LineTypes.Added] = "ADDED", + [Bitbucket.Net.Models.Core.Projects.LineTypes.Removed] = "REMOVED", + [Bitbucket.Net.Models.Core.Projects.LineTypes.Context] = "CONTEXT", + }); + + /// Mapping for . + public static EnumMap FileTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.FileTypes.From] = "FROM", + [Bitbucket.Net.Models.Core.Projects.FileTypes.To] = "TO", + }); + + /// Mapping for . + public static EnumMap ChangeScopes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.ChangeScopes.All] = "ALL", + [Bitbucket.Net.Models.Core.Projects.ChangeScopes.Unreviewed] = "UNREVIEWED", + [Bitbucket.Net.Models.Core.Projects.ChangeScopes.Range] = "RANGE", + }, createReverse: false); + + /// Mapping for . + public static EnumMap LogLevels { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Logs.LogLevels.Trace] = "TRACE", + [Bitbucket.Net.Models.Core.Logs.LogLevels.Debug] = "DEBUG", + [Bitbucket.Net.Models.Core.Logs.LogLevels.Info] = "INFO", + [Bitbucket.Net.Models.Core.Logs.LogLevels.Warn] = "WARN", + [Bitbucket.Net.Models.Core.Logs.LogLevels.Error] = "ERROR", + }); + + /// Mapping for . + public static EnumMap ParticipantStatus { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.ParticipantStatus.Approved] = "APPROVED", + [Bitbucket.Net.Models.Core.Projects.ParticipantStatus.NeedsWork] = "NEEDS_WORK", + [Bitbucket.Net.Models.Core.Projects.ParticipantStatus.Unapproved] = "UNAPPROVED", + }); + + /// Mapping for . + public static EnumMap HookTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.HookTypes.PreReceive] = "PRE_RECEIVE", + [Bitbucket.Net.Models.Core.Projects.HookTypes.PostReceive] = "POST_RECEIVE", + [Bitbucket.Net.Models.Core.Projects.HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE", + }); + + /// Mapping for . + public static EnumMap ScopeTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.ScopeTypes.Global] = "GLOBAL", + [Bitbucket.Net.Models.Core.Projects.ScopeTypes.Project] = "PROJECT", + [Bitbucket.Net.Models.Core.Projects.ScopeTypes.Repository] = "REPOSITORY", + }); + + /// Mapping for . + public static EnumMap ArchiveFormats { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.ArchiveFormats.Zip] = "zip", + [Bitbucket.Net.Models.Core.Projects.ArchiveFormats.Tar] = "tar", + [Bitbucket.Net.Models.Core.Projects.ArchiveFormats.TarGz] = "tar.gz", + [Bitbucket.Net.Models.Core.Projects.ArchiveFormats.Tgz] = "tgz", + }, createReverse: false); + + /// Mapping for . + public static EnumMap WebHookOutcomes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.WebHookOutcomes.Success] = "SUCCESS", + [Bitbucket.Net.Models.Core.Projects.WebHookOutcomes.Failure] = "FAILURE", + [Bitbucket.Net.Models.Core.Projects.WebHookOutcomes.Error] = "ERROR", + }); + + /// Mapping for . + public static EnumMap AnchorStates { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.AnchorStates.Active] = "ACTIVE", + [Bitbucket.Net.Models.Core.Projects.AnchorStates.Orphaned] = "ORPHANED", + [Bitbucket.Net.Models.Core.Projects.AnchorStates.All] = "ALL", + }, createReverse: false); + + /// Mapping for . + public static EnumMap DiffTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.DiffTypes.Effective] = "EFFECTIVE", + [Bitbucket.Net.Models.Core.Projects.DiffTypes.Range] = "RANGE", + [Bitbucket.Net.Models.Core.Projects.DiffTypes.Commit] = "COMMIT", + }, createReverse: false); + + /// Mapping for . + public static EnumMap TagTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Git.TagTypes.LightWeight] = "LIGHTWEIGHT", + [Bitbucket.Net.Models.Git.TagTypes.Annotated] = "ANNOTATED", + }, createReverse: false); + + /// Mapping for . + public static EnumMap RefRestrictionTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.RefRestrictions.RefRestrictionTypes.AllChanges] = "read-only", + [Bitbucket.Net.Models.RefRestrictions.RefRestrictionTypes.RewritingHistory] = "fast-forward-only", + [Bitbucket.Net.Models.RefRestrictions.RefRestrictionTypes.Deletion] = "no-deletes", + [Bitbucket.Net.Models.RefRestrictions.RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only", + }); + + /// Mapping for . + public static EnumMap RefMatcherTypes { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.RefRestrictions.RefMatcherTypes.Branch] = "BRANCH", + [Bitbucket.Net.Models.RefRestrictions.RefMatcherTypes.Pattern] = "PATTERN", + [Bitbucket.Net.Models.RefRestrictions.RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", + [Bitbucket.Net.Models.RefRestrictions.RefMatcherTypes.ModelBranch] = "MODEL_BRANCH", + }, createReverse: false); + + /// Mapping for . + public static EnumMap SynchronizeActions { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.RefSync.SynchronizeActions.Merge] = "MERGE", + [Bitbucket.Net.Models.RefSync.SynchronizeActions.Discard] = "DISCARD", + }); + + /// Mapping for . + public static EnumMap BlockerCommentState { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.BlockerCommentState.Open] = "OPEN", + [Bitbucket.Net.Models.Core.Projects.BlockerCommentState.Resolved] = "RESOLVED", + }); + + /// Mapping for . + public static EnumMap CommentSeverity { get; } = new(new Dictionary + { + [Bitbucket.Net.Models.Core.Projects.CommentSeverity.Normal] = "NORMAL", + [Bitbucket.Net.Models.Core.Projects.CommentSeverity.Blocker] = "BLOCKER", + }); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/BitbucketHelpers.cs b/src/Bitbucket.Net/Common/BitbucketHelpers.cs index e6e3e9b..35c805f 100644 --- a/src/Bitbucket.Net/Common/BitbucketHelpers.cs +++ b/src/Bitbucket.Net/Common/BitbucketHelpers.cs @@ -3,12 +3,12 @@ using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Git; using Bitbucket.Net.Models.RefRestrictions; -using Bitbucket.Net.Models.RefSync; namespace Bitbucket.Net.Common; /// /// Helper methods for converting between Bitbucket enum values and their wire-format string representations. +/// Delegates to for all mappings. /// public static class BitbucketHelpers { @@ -32,23 +32,10 @@ public static string BoolToString(bool value) => value ? BoolToString(value.Value) : null; - /// - /// Parses a case-insensitive boolean string returned by the Bitbucket API. - /// - /// The string to parse. - /// when the value is "true"; otherwise . - public static bool StringToBool(string value) => value.Equals("true", StringComparison.OrdinalIgnoreCase); - #endregion #region BranchOrderBy - private static readonly Dictionary s_stringByBranchOrderBy = new() - { - [BranchOrderBy.Alphabetical] = "ALPHABETICAL", - [BranchOrderBy.Modification] = "MODIFICATION", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -56,25 +43,12 @@ public static string BoolToString(bool value) => value /// The API string representation. /// Thrown when the value is not recognized. public static string BranchOrderByToString(BranchOrderBy orderBy) - { - if (!s_stringByBranchOrderBy.TryGetValue(orderBy, out string? result)) - { - throw new ArgumentException($"Unknown branch order by: {orderBy}"); - } - - return result; - } + => BitbucketEnumMaps.BranchOrderBy.ToApiString(orderBy); #endregion #region PullRequestDirections - private static readonly Dictionary s_stringByPullRequestDirection = new() - { - [PullRequestDirections.Incoming] = "INCOMING", - [PullRequestDirections.Outgoing] = "OUTGOING", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -82,27 +56,12 @@ public static string BranchOrderByToString(BranchOrderBy orderBy) /// The API string representation. /// Thrown when the value is not recognized. public static string PullRequestDirectionToString(PullRequestDirections direction) - { - if (!s_stringByPullRequestDirection.TryGetValue(direction, out string? result)) - { - throw new ArgumentException($"Unknown pull request direction: {direction}"); - } - - return result; - } + => BitbucketEnumMaps.PullRequestDirections.ToApiString(direction); #endregion #region PullRequestStates - private static readonly Dictionary s_stringByPullRequestState = new() - { - [PullRequestStates.Open] = "OPEN", - [PullRequestStates.Declined] = "DECLINED", - [PullRequestStates.Merged] = "MERGED", - [PullRequestStates.All] = "ALL", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -110,52 +69,20 @@ public static string PullRequestDirectionToString(PullRequestDirections directio /// The API string representation. /// Thrown when the value is not recognized. public static string PullRequestStateToString(PullRequestStates state) - { - if (!s_stringByPullRequestState.TryGetValue(state, out string? result)) - { - throw new ArgumentException($"Unknown pull request state: {state}"); - } - - return result; - } + => BitbucketEnumMaps.PullRequestStates.ToApiString(state); /// /// Converts an optional value to the Bitbucket API string. /// /// The state to convert. /// The API string representation or when no state is provided. - public static string? PullRequestStateToString(PullRequestStates? state) => state.HasValue - ? PullRequestStateToString(state.Value) - : null; - - /// - /// Parses a Bitbucket pull request state string into a value. - /// - /// The string returned by the API. - /// The parsed state. - /// Thrown when the value is not recognized. - public static PullRequestStates StringToPullRequestState(string s) - { - var pair = s_stringByPullRequestState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown pull request state: {s}"); - } - - return pair.Key; - } + public static string? PullRequestStateToString(PullRequestStates? state) + => BitbucketEnumMaps.PullRequestStates.ToApiString(state); #endregion #region PullRequestOrders - private static readonly Dictionary s_stringByPullRequestOrder = new() - { - [PullRequestOrders.Newest] = "NEWEST", - [PullRequestOrders.Oldest] = "OLDEST", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -163,72 +90,41 @@ public static PullRequestStates StringToPullRequestState(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string PullRequestOrderToString(PullRequestOrders order) - { - if (!s_stringByPullRequestOrder.TryGetValue(order, out string? result)) - { - throw new ArgumentException($"Unknown pull request order: {order}"); - } - - return result; - } + => BitbucketEnumMaps.PullRequestOrders.ToApiString(order); /// /// Converts an optional value to the Bitbucket API string. /// /// The order to convert. /// The API string representation or when no order is provided. - public static string? PullRequestOrderToString(PullRequestOrders? order) => order.HasValue - ? PullRequestOrderToString(order.Value) - : null; + public static string? PullRequestOrderToString(PullRequestOrders? order) + => BitbucketEnumMaps.PullRequestOrders.ToApiString(order); #endregion #region PullRequestFromTypes - private static readonly Dictionary s_stringByPullRequestFromType = new() - { - [PullRequestFromTypes.Comment] = "COMMENT", - [PullRequestFromTypes.Activity] = "ACTIVITY", - }; - + /// + /// Converts a value to the Bitbucket API string. + /// + /// The source type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) - { - if (!s_stringByPullRequestFromType.TryGetValue(fromType, out string? result)) - { - throw new ArgumentException($"Unknown pull request from type: {fromType}"); - } - - return result; - } + => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(fromType); /// /// Converts an optional value to the Bitbucket API string. /// /// The source type to convert. /// The API string representation or when no source is provided. - public static string? PullRequestFromTypeToString(PullRequestFromTypes? fromType) => fromType.HasValue - ? PullRequestFromTypeToString(fromType.Value) - : null; + public static string? PullRequestFromTypeToString(PullRequestFromTypes? fromType) + => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(fromType); #endregion #region Permissions - private static readonly Dictionary s_stringByPermissions = new() - { - [Permissions.Admin] = "ADMIN", - [Permissions.LicensedUser] = "LICENSED_USER", - [Permissions.ProjectAdmin] = "PROJECT_ADMIN", - [Permissions.ProjectCreate] = "PROJECT_CREATE", - [Permissions.ProjectRead] = "PROJECT_READ", - [Permissions.ProjectView] = "PROJECT_VIEW", - [Permissions.ProjectWrite] = "PROJECT_WRITE", - [Permissions.RepoAdmin] = "REPO_ADMIN", - [Permissions.RepoRead] = "REPO_READ", - [Permissions.RepoWrite] = "REPO_WRITE", - [Permissions.SysAdmin] = "SYS_ADMIN", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -236,53 +132,20 @@ private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) /// The API string representation. /// Thrown when the value is not recognized. public static string PermissionToString(Permissions permission) - { - if (!s_stringByPermissions.TryGetValue(permission, out string? result)) - { - throw new ArgumentException($"Unknown permission: {permission}"); - } - - return result; - } + => BitbucketEnumMaps.Permissions.ToApiString(permission); /// /// Converts an optional value to the Bitbucket API string. /// /// The permission to convert. /// The API string representation or when not supplied. - public static string? PermissionToString(Permissions? permission) => permission.HasValue - ? PermissionToString(permission.Value) - : null; - - /// - /// Parses a Bitbucket permission string into a value. - /// - /// The string returned by the API. - /// The parsed permission. - /// Thrown when the value is not recognized. - public static Permissions StringToPermission(string s) - { - var pair = s_stringByPermissions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown permission: {s}"); - } - - return pair.Key; - } + public static string? PermissionToString(Permissions? permission) + => BitbucketEnumMaps.Permissions.ToApiString(permission); #endregion #region MergeCommits - private static readonly Dictionary s_stringByMergeCommits = new() - { - [MergeCommits.Exclude] = "exclude", - [MergeCommits.Include] = "include", - [MergeCommits.Only] = "only", - }; - /// /// Converts a preference to the Bitbucket API string. /// @@ -290,26 +153,12 @@ public static Permissions StringToPermission(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string MergeCommitsToString(MergeCommits mergeCommits) - { - if (!s_stringByMergeCommits.TryGetValue(mergeCommits, out string? result)) - { - throw new ArgumentException($"Unknown merge commit: {mergeCommits}"); - } - - return result; - } + => BitbucketEnumMaps.MergeCommits.ToApiString(mergeCommits); #endregion #region Roles - private static readonly Dictionary s_stringByRoles = new() - { - [Roles.Author] = "AUTHOR", - [Roles.Reviewer] = "REVIEWER", - [Roles.Participant] = "PARTICIPANT", - }; - /// /// Converts a pull request value to the Bitbucket API string. /// @@ -317,53 +166,20 @@ public static string MergeCommitsToString(MergeCommits mergeCommits) /// The API string representation. /// Thrown when the value is not recognized. public static string RoleToString(Roles role) - { - if (!s_stringByRoles.TryGetValue(role, out string? result)) - { - throw new ArgumentException($"Unknown role: {role}"); - } - - return result; - } + => BitbucketEnumMaps.Roles.ToApiString(role); /// /// Converts an optional pull request value to the Bitbucket API string. /// /// The role to convert. /// The API string representation or when not supplied. - public static string? RoleToString(Roles? role) => role.HasValue - ? RoleToString(role.Value) - : null; - - /// - /// Parses a pull request role string into a value. - /// - /// The string returned by the API. - /// The parsed role. - /// Thrown when the value is not recognized. - public static Roles StringToRole(string s) - { - var pair = s_stringByRoles.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown role: {s}"); - } - - return pair.Key; - } + public static string? RoleToString(Roles? role) + => BitbucketEnumMaps.Roles.ToApiString(role); #endregion #region LineTypes - private static readonly Dictionary s_stringByLineTypes = new() - { - [LineTypes.Added] = "ADDED", - [LineTypes.Removed] = "REMOVED", - [LineTypes.Context] = "CONTEXT", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -371,14 +187,7 @@ public static Roles StringToRole(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string LineTypeToString(LineTypes lineType) - { - if (!s_stringByLineTypes.TryGetValue(lineType, out string? result)) - { - throw new ArgumentException($"Unknown line type: {lineType}"); - } - - return result; - } + => BitbucketEnumMaps.LineTypes.ToApiString(lineType); /// /// Converts an optional value to the Bitbucket API string. @@ -386,40 +195,12 @@ public static string LineTypeToString(LineTypes lineType) /// The line type to convert. /// The API string representation or when not supplied. public static string? LineTypeToString(LineTypes? lineType) - { - return lineType.HasValue - ? LineTypeToString(lineType.Value) - : null; - } - - /// - /// Parses a line type string into a value. - /// - /// The string returned by the API. - /// The parsed line type. - /// Thrown when the value is not recognized. - public static LineTypes StringToLineType(string s) - { - var pair = s_stringByLineTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown line type: {s}"); - } - - return pair.Key; - } + => BitbucketEnumMaps.LineTypes.ToApiString(lineType); #endregion #region FileTypes - private static readonly Dictionary s_stringByFileTypes = new() - { - [FileTypes.From] = "FROM", - [FileTypes.To] = "TO", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -427,14 +208,7 @@ public static LineTypes StringToLineType(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string FileTypeToString(FileTypes fileType) - { - if (!s_stringByFileTypes.TryGetValue(fileType, out string? result)) - { - throw new ArgumentException($"Unknown file type: {fileType}"); - } - - return result; - } + => BitbucketEnumMaps.FileTypes.ToApiString(fileType); /// /// Converts an optional value to the Bitbucket API string. @@ -442,41 +216,12 @@ public static string FileTypeToString(FileTypes fileType) /// The file type to convert. /// The API string representation or when not supplied. public static string? FileTypeToString(FileTypes? fileType) - { - return fileType.HasValue - ? FileTypeToString(fileType.Value) - : null; - } - - /// - /// Parses a file type string into a value. - /// - /// The string returned by the API. - /// The parsed file type. - /// Thrown when the value is not recognized. - public static FileTypes StringToFileType(string s) - { - var pair = s_stringByFileTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown file type: {s}"); - } - - return pair.Key; - } + => BitbucketEnumMaps.FileTypes.ToApiString(fileType); #endregion #region ChangeScopes - private static readonly Dictionary s_stringByChangeScopes = new() - { - [ChangeScopes.All] = "ALL", - [ChangeScopes.Unreviewed] = "UNREVIEWED", - [ChangeScopes.Range] = "RANGE", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -484,28 +229,12 @@ public static FileTypes StringToFileType(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string ChangeScopeToString(ChangeScopes changeScope) - { - if (!s_stringByChangeScopes.TryGetValue(changeScope, out string? result)) - { - throw new ArgumentException($"Unknown change scope: {changeScope}"); - } - - return result; - } + => BitbucketEnumMaps.ChangeScopes.ToApiString(changeScope); #endregion #region LogLevels - private static readonly Dictionary s_stringByLogLevels = new() - { - [LogLevels.Trace] = "TRACE", - [LogLevels.Debug] = "DEBUG", - [LogLevels.Info] = "INFO", - [LogLevels.Warn] = "WARN", - [LogLevels.Error] = "ERROR", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -513,14 +242,7 @@ public static string ChangeScopeToString(ChangeScopes changeScope) /// The API string representation. /// Thrown when the value is not recognized. public static string LogLevelToString(LogLevels logLevel) - { - if (!s_stringByLogLevels.TryGetValue(logLevel, out string? result)) - { - throw new ArgumentException($"Unknown log level: {logLevel}"); - } - - return result; - } + => BitbucketEnumMaps.LogLevels.ToApiString(logLevel); /// /// Parses a log level string into a value. @@ -529,28 +251,12 @@ public static string LogLevelToString(LogLevels logLevel) /// The parsed log level. /// Thrown when the value is not recognized. public static LogLevels StringToLogLevel(string s) - { - var pair = s_stringByLogLevels.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown log level: {s}"); - } - - return pair.Key; - } + => BitbucketEnumMaps.LogLevels.FromApiString(s); #endregion #region ParticipantStatus - private static readonly Dictionary s_stringByParticipantStatus = new() - { - [ParticipantStatus.Approved] = "APPROVED", - [ParticipantStatus.NeedsWork] = "NEEDS_WORK", - [ParticipantStatus.Unapproved] = "UNAPPROVED", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -558,134 +264,12 @@ public static LogLevels StringToLogLevel(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string ParticipantStatusToString(ParticipantStatus participantStatus) - { - if (!s_stringByParticipantStatus.TryGetValue(participantStatus, out string? result)) - { - throw new ArgumentException($"Unknown participant status: {participantStatus}"); - } - - return result; - } - - /// - /// Parses a participant status string into a value. - /// - /// The string returned by the API. - /// The parsed status. - /// Thrown when the value is not recognized. - public static ParticipantStatus StringToParticipantStatus(string s) - { - var pair = s_stringByParticipantStatus.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown participant status: {s}"); - } - - return pair.Key; - } - - #endregion - - #region HookTypes - - private static readonly Dictionary s_stringByHookTypes = new() - { - [HookTypes.PreReceive] = "PRE_RECEIVE", - [HookTypes.PostReceive] = "POST_RECEIVE", - [HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE", - }; - - /// - /// Converts a hook value to the Bitbucket API string. - /// - /// The hook type to convert. - /// The API string representation. - /// Thrown when the value is not recognized. - public static string HookTypeToString(HookTypes hookType) - { - if (!s_stringByHookTypes.TryGetValue(hookType, out string? result)) - { - throw new ArgumentException($"Unknown hook type: {hookType}"); - } - - return result; - } - - /// - /// Parses a hook type string into a value. - /// - /// The string returned by the API. - /// The parsed hook type. - /// Thrown when the value is not recognized. - public static HookTypes StringToHookType(string s) - { - var pair = s_stringByHookTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown hook type: {s}"); - } - - return pair.Key; - } - - #endregion - - #region ScopeTypes - - private static readonly Dictionary s_stringByScopeTypes = new() - { - [ScopeTypes.Project] = "PROJECT", - [ScopeTypes.Repository] = "REPOSITORY", - }; - - /// - /// Converts a value to the Bitbucket API string. - /// - /// The scope type to convert. - /// The API string representation. - /// Thrown when the value is not recognized. - public static string ScopeTypeToString(ScopeTypes scopeType) - { - if (!s_stringByScopeTypes.TryGetValue(scopeType, out string? result)) - { - throw new ArgumentException($"Unknown scope type: {scopeType}"); - } - - return result; - } - - /// - /// Parses a scope type string into a value. - /// - /// The string returned by the API. - /// The parsed scope type. - /// Thrown when the value is not recognized. - public static ScopeTypes StringToScopeType(string s) - { - var pair = s_stringByScopeTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown scope type: {s}"); - } - - return pair.Key; - } + => BitbucketEnumMaps.ParticipantStatus.ToApiString(participantStatus); #endregion #region ArchiveFormats - private static readonly Dictionary s_stringByArchiveFormats = new() - { - [ArchiveFormats.Zip] = "zip", - [ArchiveFormats.Tar] = "tar", - [ArchiveFormats.TarGz] = "tar.gz", - [ArchiveFormats.Tgz] = "tgz", - }; - /// /// Converts an value to the Bitbucket API string. /// @@ -693,26 +277,12 @@ public static ScopeTypes StringToScopeType(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string ArchiveFormatToString(ArchiveFormats archiveFormat) - { - if (!s_stringByArchiveFormats.TryGetValue(archiveFormat, out string? result)) - { - throw new ArgumentException($"Unknown archive format: {archiveFormat}"); - } - - return result; - } + => BitbucketEnumMaps.ArchiveFormats.ToApiString(archiveFormat); #endregion #region WebHookOutcomes - private static readonly Dictionary s_stringByWebHookOutcomes = new() - { - [WebHookOutcomes.Success] = "SUCCESS", - [WebHookOutcomes.Failure] = "FAILURE", - [WebHookOutcomes.Error] = "ERROR", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -720,53 +290,20 @@ public static string ArchiveFormatToString(ArchiveFormats archiveFormat) /// The API string representation. /// Thrown when the value is not recognized. public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) - { - if (!s_stringByWebHookOutcomes.TryGetValue(webHookOutcome, out string? result)) - { - throw new ArgumentException($"Unknown web hook outcome: {webHookOutcome}"); - } - - return result; - } + => BitbucketEnumMaps.WebHookOutcomes.ToApiString(webHookOutcome); /// /// Converts an optional value to the Bitbucket API string. /// /// The outcome to convert. /// The API string representation or when not supplied. - public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) => webHookOutcome.HasValue - ? WebHookOutcomeToString(webHookOutcome.Value) - : null; - - /// - /// Parses a webhook outcome string into a value. - /// - /// The string returned by the API. - /// The parsed outcome. - /// Thrown when the value is not recognized. - public static WebHookOutcomes StringToWebHookOutcome(string s) - { - var pair = s_stringByWebHookOutcomes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown web hook outcome: {s}"); - } - - return pair.Key; - } + public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) + => BitbucketEnumMaps.WebHookOutcomes.ToApiString(webHookOutcome); #endregion #region AnchorStates - private static readonly Dictionary s_stringByAnchorStates = new() - { - [AnchorStates.Active] = "ACTIVE", - [AnchorStates.Orphaned] = "ORPHANED", - [AnchorStates.All] = "ALL", - }; - /// /// Converts an value to the Bitbucket API string. /// @@ -774,26 +311,12 @@ public static WebHookOutcomes StringToWebHookOutcome(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string AnchorStateToString(AnchorStates anchorState) - { - if (!s_stringByAnchorStates.TryGetValue(anchorState, out string? result)) - { - throw new ArgumentException($"Unknown anchor state: {anchorState}"); - } - - return result; - } + => BitbucketEnumMaps.AnchorStates.ToApiString(anchorState); #endregion #region DiffTypes - private static readonly Dictionary s_stringByDiffTypes = new() - { - [DiffTypes.Effective] = "EFFECTIVE", - [DiffTypes.Range] = "RANGE", - [DiffTypes.Commit] = "COMMIT", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -801,14 +324,7 @@ public static string AnchorStateToString(AnchorStates anchorState) /// The API string representation. /// Thrown when the value is not recognized. public static string DiffTypeToString(DiffTypes diffType) - { - if (!s_stringByDiffTypes.TryGetValue(diffType, out string? result)) - { - throw new ArgumentException($"Unknown diff type: {diffType}"); - } - - return result; - } + => BitbucketEnumMaps.DiffTypes.ToApiString(diffType); /// /// Converts an optional value to the Bitbucket API string. @@ -816,22 +332,12 @@ public static string DiffTypeToString(DiffTypes diffType) /// The diff type to convert. /// The API string representation or when not supplied. public static string? DiffTypeToString(DiffTypes? diffType) - { - return diffType.HasValue - ? DiffTypeToString(diffType.Value) - : null; - } + => BitbucketEnumMaps.DiffTypes.ToApiString(diffType); #endregion #region TagTypes - private static readonly Dictionary s_stringByTagTypes = new() - { - [TagTypes.LightWeight] = "LIGHTWEIGHT", - [TagTypes.Annotated] = "ANNOTATED", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -839,27 +345,12 @@ public static string DiffTypeToString(DiffTypes diffType) /// The API string representation. /// Thrown when the value is not recognized. public static string TagTypeToString(TagTypes tagType) - { - if (!s_stringByTagTypes.TryGetValue(tagType, out string? result)) - { - throw new ArgumentException($"Unknown tag type: {tagType}"); - } - - return result; - } + => BitbucketEnumMaps.TagTypes.ToApiString(tagType); #endregion #region RefRestrictionTypes - private static readonly Dictionary s_stringByRefRestrictionTypes = new() - { - [RefRestrictionTypes.AllChanges] = "read-only", - [RefRestrictionTypes.RewritingHistory] = "fast-forward-only", - [RefRestrictionTypes.Deletion] = "no-deletes", - [RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -867,14 +358,7 @@ public static string TagTypeToString(TagTypes tagType) /// The API string representation. /// Thrown when the value is not recognized. public static string RefRestrictionTypeToString(RefRestrictionTypes refRestrictionType) - { - if (!s_stringByRefRestrictionTypes.TryGetValue(refRestrictionType, out string? result)) - { - throw new ArgumentException($"Unknown ref restriction type: {refRestrictionType}"); - } - - return result; - } + => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(refRestrictionType); /// /// Converts an optional value to the Bitbucket API string. @@ -882,118 +366,33 @@ public static string RefRestrictionTypeToString(RefRestrictionTypes refRestricti /// The restriction to convert. /// The API string representation or when not supplied. public static string? RefRestrictionTypeToString(RefRestrictionTypes? refRestrictionType) - { - return refRestrictionType.HasValue - ? RefRestrictionTypeToString(refRestrictionType.Value) - : null; - } - - /// - /// Parses a ref restriction string into a value. - /// - /// The string returned by the API. - /// The parsed restriction. - /// Thrown when the value is not recognized. - public static RefRestrictionTypes StringToRefRestrictionType(string s) - { - var pair = s_stringByRefRestrictionTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown ref restriction type: {s}"); - } - - return pair.Key; - } + => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(refRestrictionType); #endregion #region RefMatcherTypes - private static readonly Dictionary s_stringByRefMatcherTypes = new() - { - [RefMatcherTypes.Branch] = "BRANCH", - [RefMatcherTypes.Pattern] = "PATTERN", - [RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", - [RefMatcherTypes.ModelBranch] = "MODEL_BRANCH", - }; - - private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) - { - if (!s_stringByRefMatcherTypes.TryGetValue(refMatcherType, out string? result)) - { - throw new ArgumentException($"Unknown ref matcher type: {refMatcherType}"); - } - - return result; - } - /// - /// Converts an optional value to the Bitbucket API string. + /// Converts a value to the Bitbucket API string. /// /// The matcher type to convert. - /// The API string representation or when not supplied. - public static string? RefMatcherTypeToString(RefMatcherTypes? refMatcherType) - { - return refMatcherType.HasValue - ? RefMatcherTypeToString(refMatcherType.Value) - : null; - } - - #endregion - - #region SynchronizeActions - - private static readonly Dictionary s_stringBySynchronizeActions = new() - { - [SynchronizeActions.Merge] = "MERGE", - [SynchronizeActions.Discard] = "DISCARD", - }; - - /// - /// Converts a value to the Bitbucket API string. - /// - /// The synchronization action to convert. /// The API string representation. /// Thrown when the value is not recognized. - public static string SynchronizeActionToString(SynchronizeActions synchronizeAction) - { - if (!s_stringBySynchronizeActions.TryGetValue(synchronizeAction, out string? result)) - { - throw new ArgumentException($"Unknown synchronize action: {synchronizeAction}"); - } - - return result; - } + private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(refMatcherType); /// - /// Parses a synchronization action string into a value. + /// Converts an optional value to the Bitbucket API string. /// - /// The string returned by the API. - /// The parsed action. - /// Thrown when the value is not recognized. - public static SynchronizeActions StringToSynchronizeAction(string s) - { - var pair = s_stringBySynchronizeActions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown synchronize action: {s}"); - } - - return pair.Key; - } + /// The matcher type to convert. + /// The API string representation or when not supplied. + public static string? RefMatcherTypeToString(RefMatcherTypes? refMatcherType) + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(refMatcherType); #endregion #region BlockerCommentState - private static readonly Dictionary s_stringByBlockerCommentState = new() - { - [BlockerCommentState.Open] = "OPEN", - [BlockerCommentState.Resolved] = "RESOLVED", - }; - /// /// Converts a value to the Bitbucket API string. /// @@ -1001,94 +400,15 @@ public static SynchronizeActions StringToSynchronizeAction(string s) /// The API string representation. /// Thrown when the value is not recognized. public static string BlockerCommentStateToString(BlockerCommentState state) - { - if (!s_stringByBlockerCommentState.TryGetValue(state, out string? result)) - { - throw new ArgumentException($"Unknown blocker comment state: {state}"); - } - - return result; - } + => BitbucketEnumMaps.BlockerCommentState.ToApiString(state); /// /// Converts an optional value to the Bitbucket API string. /// /// The blocker comment state to convert. /// The API string representation or when not supplied. - public static string? BlockerCommentStateToString(BlockerCommentState? state) => state.HasValue - ? BlockerCommentStateToString(state.Value) - : null; - - /// - /// Parses a blocker comment state string into a value. - /// - /// The string returned by the API. - /// The parsed state. - /// Thrown when the value is not recognized. - public static BlockerCommentState StringToBlockerCommentState(string s) - { - var pair = s_stringByBlockerCommentState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown blocker comment state: {s}"); - } - - return pair.Key; - } - - #endregion - - #region CommentSeverity - - private static readonly Dictionary s_stringByCommentSeverity = new() - { - [CommentSeverity.Normal] = "NORMAL", - [CommentSeverity.Blocker] = "BLOCKER", - }; - - /// - /// Converts a value to the Bitbucket API string. - /// - /// The comment severity to convert. - /// The API string representation. - /// Thrown when the value is not recognized. - public static string CommentSeverityToString(CommentSeverity severity) - { - if (!s_stringByCommentSeverity.TryGetValue(severity, out string? result)) - { - throw new ArgumentException($"Unknown comment severity: {severity}"); - } - - return result; - } - - /// - /// Converts an optional value to the Bitbucket API string. - /// - /// The comment severity to convert. - /// The API string representation or when not supplied. - public static string? CommentSeverityToString(CommentSeverity? severity) => severity.HasValue - ? CommentSeverityToString(severity.Value) - : null; - - /// - /// Parses a comment severity string into a value. - /// - /// The string returned by the API. - /// The parsed severity. - /// Thrown when the value is not recognized. - public static CommentSeverity StringToCommentSeverity(string s) - { - var pair = s_stringByCommentSeverity.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown comment severity: {s}"); - } - - return pair.Key; - } + public static string? BlockerCommentStateToString(BlockerCommentState? state) + => BitbucketEnumMaps.BlockerCommentState.ToApiString(state); #endregion } \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/BitbucketEnumConverterFactory.cs b/src/Bitbucket.Net/Common/Converters/BitbucketEnumConverterFactory.cs new file mode 100644 index 0000000..0f9c7af --- /dev/null +++ b/src/Bitbucket.Net/Common/Converters/BitbucketEnumConverterFactory.cs @@ -0,0 +1,48 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Logs; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.RefRestrictions; +using Bitbucket.Net.Models.RefSync; +using System.Collections.Frozen; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Bitbucket.Net.Common.Converters; + +/// +/// A that provides +/// instances for all Bitbucket enum types registered in . +/// +/// +/// This factory is parameterless so it can be referenced from +/// for source-generated contexts. +/// +public sealed class BitbucketEnumConverterFactory : JsonConverterFactory +{ + private static readonly FrozenDictionary s_converters = + new Dictionary + { + [typeof(PullRequestStates)] = new JsonEnumConverter(BitbucketEnumMaps.PullRequestStates), + [typeof(Permissions)] = new JsonEnumConverter(BitbucketEnumMaps.Permissions), + [typeof(Roles)] = new JsonEnumConverter(BitbucketEnumMaps.Roles), + [typeof(LineTypes)] = new JsonEnumConverter(BitbucketEnumMaps.LineTypes), + [typeof(FileTypes)] = new JsonEnumConverter(BitbucketEnumMaps.FileTypes), + [typeof(ParticipantStatus)] = new JsonEnumConverter(BitbucketEnumMaps.ParticipantStatus), + [typeof(HookTypes)] = new JsonEnumConverter(BitbucketEnumMaps.HookTypes), + [typeof(ScopeTypes)] = new JsonEnumConverter(BitbucketEnumMaps.ScopeTypes), + [typeof(WebHookOutcomes)] = new JsonEnumConverter(BitbucketEnumMaps.WebHookOutcomes), + [typeof(RefRestrictionTypes)] = new JsonEnumConverter(BitbucketEnumMaps.RefRestrictionTypes), + [typeof(SynchronizeActions)] = new JsonEnumConverter(BitbucketEnumMaps.SynchronizeActions), + [typeof(BlockerCommentState)] = new JsonEnumConverter(BitbucketEnumMaps.BlockerCommentState), + [typeof(CommentSeverity)] = new JsonEnumConverter(BitbucketEnumMaps.CommentSeverity), + [typeof(LogLevels)] = new JsonEnumConverter(BitbucketEnumMaps.LogLevels), + [typeof(List)] = new JsonEnumListConverter(BitbucketEnumMaps.Permissions), + }.ToFrozenDictionary(); + + /// + public override bool CanConvert(Type typeToConvert) => s_converters.ContainsKey(typeToConvert); + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + s_converters[typeToConvert]; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs b/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs deleted file mode 100644 index caf0338..0000000 --- a/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for enum values. -/// -public class BlockerCommentStateConverter : JsonEnumConverter -{ - protected override string ConvertToString(BlockerCommentState value) - { - return BitbucketHelpers.BlockerCommentStateToString(value); - } - - protected override BlockerCommentState ConvertFromString(string s) - { - return BitbucketHelpers.StringToBlockerCommentState(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs b/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs deleted file mode 100644 index b189903..0000000 --- a/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for enum values. -/// -public class CommentSeverityConverter : JsonEnumConverter -{ - protected override string ConvertToString(CommentSeverity value) - { - return BitbucketHelpers.CommentSeverityToString(value); - } - - protected override CommentSeverity ConvertFromString(string s) - { - return BitbucketHelpers.StringToCommentSeverity(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs deleted file mode 100644 index 0eaec6b..0000000 --- a/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket values. -/// -public class FileTypesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(FileTypes value) - { - return BitbucketHelpers.FileTypeToString(value); - } - - /// - protected override FileTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToFileType(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs deleted file mode 100644 index 44d41f0..0000000 --- a/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket hook type values. -/// -public class HookTypesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(HookTypes value) - { - return BitbucketHelpers.HookTypeToString(value); - } - - /// - protected override HookTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToHookType(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs b/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs index cc8ec6b..fa73f5b 100644 --- a/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs @@ -4,22 +4,11 @@ namespace Bitbucket.Net.Common.Converters; /// -/// Abstract base class for custom enum converters that convert between enum values and their string representations. +/// JSON converter for enums that use a for bidirectional mapping. /// -/// The enum type to convert. -public abstract class JsonEnumConverter : JsonConverter +public sealed class JsonEnumConverter(EnumMap map) : JsonConverter where TEnum : struct, Enum { - /// - /// Converts an enum value to its string representation. - /// - protected abstract string ConvertToString(TEnum value); - - /// - /// Converts a string representation to its enum value. - /// - protected abstract TEnum ConvertFromString(string s); - /// public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -30,8 +19,7 @@ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe if (reader.TokenType == JsonTokenType.String) { - string value = reader.GetString()!; - return ConvertFromString(value); + return map.FromApiString(reader.GetString()!); } throw new JsonException($"Unexpected token {reader.TokenType} when parsing enum {typeof(TEnum).Name}."); @@ -40,27 +28,16 @@ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe /// public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) { - writer.WriteStringValue(ConvertToString(value)); + writer.WriteStringValue(map.ToApiString(value)); } } /// -/// Abstract base class for custom enum list converters that convert between lists of enum values and their JSON array representations. +/// JSON converter for lists of enums using a for bidirectional mapping. /// -/// The enum type to convert. -public abstract class JsonEnumListConverter : JsonConverter?> +public sealed class JsonEnumListConverter(EnumMap map) : JsonConverter?> where TEnum : struct, Enum { - /// - /// Converts an enum value to its string representation. - /// - protected abstract string ConvertToString(TEnum value); - - /// - /// Converts a string representation to its enum value. - /// - protected abstract TEnum ConvertFromString(string s); - /// public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -84,7 +61,7 @@ public abstract class JsonEnumListConverter : JsonConverter?> if (reader.TokenType == JsonTokenType.String) { - items.Add(ConvertFromString(reader.GetString()!)); + items.Add(map.FromApiString(reader.GetString()!)); } } @@ -103,7 +80,7 @@ public override void Write(Utf8JsonWriter writer, List? value, JsonSerial writer.WriteStartArray(); foreach (var item in value) { - writer.WriteStringValue(ConvertToString(item)); + writer.WriteStringValue(map.ToApiString(item)); } writer.WriteEndArray(); } diff --git a/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs deleted file mode 100644 index f1bcf51..0000000 --- a/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket line classification values. -/// -public class LineTypesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(LineTypes value) - { - return BitbucketHelpers.LineTypeToString(value); - } - - /// - protected override LineTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToLineType(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs b/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs deleted file mode 100644 index f2d1b20..0000000 --- a/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket participant status values. -/// -public class ParticipantStatusConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(ParticipantStatus value) - { - return BitbucketHelpers.ParticipantStatusToString(value); - } - - /// - protected override ParticipantStatus ConvertFromString(string s) - { - return BitbucketHelpers.StringToParticipantStatus(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs b/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs deleted file mode 100644 index decbb44..0000000 --- a/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Bitbucket.Net.Models.Core.Admin; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket permission values. -/// -public class PermissionsConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(Permissions value) - { - return BitbucketHelpers.PermissionToString(value); - } - - /// - protected override Permissions ConvertFromString(string s) - { - return BitbucketHelpers.StringToPermission(s); - } -} - -/// -/// JSON converter for lists of Bitbucket permission values. -/// -public class PermissionsListConverter : JsonEnumListConverter -{ - /// - protected override string ConvertToString(Permissions value) - { - return BitbucketHelpers.PermissionToString(value); - } - - /// - protected override Permissions ConvertFromString(string s) - { - return BitbucketHelpers.StringToPermission(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs b/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs deleted file mode 100644 index f1ee423..0000000 --- a/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket pull request states. -/// -public class PullRequestStatesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(PullRequestStates value) - { - return BitbucketHelpers.PullRequestStateToString(value); - } - - /// - protected override PullRequestStates ConvertFromString(string s) - { - return BitbucketHelpers.StringToPullRequestState(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs deleted file mode 100644 index e0641f5..0000000 --- a/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.RefRestrictions; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for repository ref restriction types. -/// -public class RefRestrictionTypesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(RefRestrictionTypes value) - { - return BitbucketHelpers.RefRestrictionTypeToString(value); - } - - /// - protected override RefRestrictionTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToRefRestrictionType(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/RolesConverter.cs b/src/Bitbucket.Net/Common/Converters/RolesConverter.cs deleted file mode 100644 index b9f2431..0000000 --- a/src/Bitbucket.Net/Common/Converters/RolesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for pull request role values. -/// -public class RolesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(Roles value) - { - return BitbucketHelpers.RoleToString(value); - } - - /// - protected override Roles ConvertFromString(string s) - { - return BitbucketHelpers.StringToRole(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs deleted file mode 100644 index 7a3e859..0000000 --- a/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for Bitbucket permission scope types. -/// -public class ScopeTypesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(ScopeTypes value) - { - return BitbucketHelpers.ScopeTypeToString(value); - } - - /// - protected override ScopeTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToScopeType(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs b/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs deleted file mode 100644 index 9eb61bd..0000000 --- a/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.RefSync; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for repository synchronization actions. -/// -public class SynchronizeActionsConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(SynchronizeActions value) - { - return BitbucketHelpers.SynchronizeActionToString(value); - } - - /// - protected override SynchronizeActions ConvertFromString(string s) - { - return BitbucketHelpers.StringToSynchronizeAction(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs b/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs index 91e4a87..638ce4f 100644 --- a/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs @@ -15,8 +15,8 @@ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConver return reader.TokenType switch { JsonTokenType.Null => default, - JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeMilliseconds(), - JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => DateTimeOffset.FromUnixTimeMilliseconds(unixTime), + JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => DateTimeOffset.FromUnixTimeMilliseconds(unixTime), _ => throw new JsonException($"Cannot convert {reader.TokenType} to {nameof(DateTimeOffset)}."), }; } @@ -40,9 +40,9 @@ public sealed class NullableUnixDateTimeOffsetConverter : JsonConverter null, - JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => DateTimeOffset.FromUnixTimeMilliseconds(unixTime), JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => null, - JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => DateTimeOffset.FromUnixTimeMilliseconds(unixTime), _ => throw new JsonException($"Cannot convert {reader.TokenType} to nullable {nameof(DateTimeOffset)}."), }; } diff --git a/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs b/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs deleted file mode 100644 index 322a512..0000000 --- a/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; - -namespace Bitbucket.Net.Common.Converters; - -/// -/// JSON converter for webhook outcome values. -/// -public class WebHookOutcomesConverter : JsonEnumConverter -{ - /// - protected override string ConvertToString(WebHookOutcomes value) - { - return BitbucketHelpers.WebHookOutcomeToString(value); - } - - /// - protected override WebHookOutcomes ConvertFromString(string s) - { - return BitbucketHelpers.StringToWebHookOutcome(s); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/EnumMap.cs b/src/Bitbucket.Net/Common/EnumMap.cs new file mode 100644 index 0000000..5896f2f --- /dev/null +++ b/src/Bitbucket.Net/Common/EnumMap.cs @@ -0,0 +1,79 @@ +using System.Collections.Frozen; + +namespace Bitbucket.Net.Common; + +/// +/// Holds the canonical enum-to-string and string-to-enum mappings for a Bitbucket API enum type. +/// +/// The enum type. +public sealed class EnumMap where TEnum : struct, Enum +{ + /// + /// Enum-to-API-string lookup (forward mapping). + /// + public FrozenDictionary Forward { get; } + + /// + /// API-string-to-enum lookup (reverse mapping). May be empty for query-param-only enums. + /// + public FrozenDictionary Reverse { get; } + + /// + /// Creates a new enum map with bidirectional lookup. + /// + /// The enum-to-string mapping dictionary. + public EnumMap(Dictionary mappings) + { + Forward = mappings.ToFrozenDictionary(); + Reverse = mappings.ToFrozenDictionary( + kv => kv.Value, + kv => kv.Key, + StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a new enum map with forward-only lookup (no reverse). + /// + /// The enum-to-string mapping dictionary. + /// When false, no reverse dictionary is created. + public EnumMap(Dictionary mappings, bool createReverse) + { + Forward = mappings.ToFrozenDictionary(); + Reverse = createReverse + ? mappings.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase) + : FrozenDictionary.Empty; + } + + /// + /// Converts an enum value to its API string representation. + /// + /// The enum value has no known mapping. + public string ToApiString(TEnum value) + { + if (!Forward.TryGetValue(value, out string? result)) + { + throw new ArgumentException($"Unknown {typeof(TEnum).Name} value: {value}"); + } + + return result; + } + + /// + /// Converts a nullable enum value to its API string representation. + /// + public string? ToApiString(TEnum? value) => value.HasValue ? ToApiString(value.Value) : null; + + /// + /// Converts an API string to its enum value. + /// + /// The string has no known reverse mapping. + public TEnum FromApiString(string value) + { + if (!Reverse.TryGetValue(value, out TEnum result)) + { + throw new ArgumentException($"Unknown {typeof(TEnum).Name} string: {value}"); + } + + return result; + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs index 2406a2d..14dfeff 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs @@ -71,6 +71,24 @@ public BitbucketApiException(string message, HttpStatusCode statusCode, IReadOnl /// The request URL that caused the error. /// A typed exception matching the HTTP status code. public static BitbucketApiException Create(int statusCode, IReadOnlyList errors, string? requestUrl = null) + { + return Create(statusCode, errors, responseHeaders: null, requestUrl); + } + + /// + /// Creates the appropriate exception type based on the HTTP status code, + /// optionally extracting rate-limit metadata from response headers. + /// + /// The HTTP status code. + /// The collection of errors from the Bitbucket response. + /// The HTTP response headers (used for rate-limit metadata on 429). + /// The request URL that caused the error. + /// A typed exception matching the HTTP status code. + public static BitbucketApiException Create( + int statusCode, + IReadOnlyList errors, + System.Net.Http.Headers.HttpResponseHeaders? responseHeaders, + string? requestUrl = null) { var httpStatusCode = (HttpStatusCode)statusCode; string message = BuildErrorMessage(httpStatusCode, errors); @@ -83,12 +101,56 @@ public static BitbucketApiException Create(int statusCode, IReadOnlyList 404 => new BitbucketNotFoundException(message, errors, requestUrl), 409 => new BitbucketConflictException(message, errors, requestUrl), 422 => new BitbucketValidationException(message, errors, requestUrl), - 429 => new BitbucketRateLimitException(message, errors, requestUrl), + 429 => CreateRateLimitException(message, errors, responseHeaders, requestUrl), >= 500 and < 600 => new BitbucketServerException(message, httpStatusCode, errors, requestUrl), _ => new BitbucketApiException(message, httpStatusCode, errors, requestUrl), }; } + private static BitbucketRateLimitException CreateRateLimitException( + string message, + IReadOnlyList errors, + System.Net.Http.Headers.HttpResponseHeaders? responseHeaders, + string? requestUrl) + { + if (responseHeaders is null) + { + return new BitbucketRateLimitException(message, errors, requestUrl); + } + + var retryAfter = TryParseHeaderInt(responseHeaders, "Retry-After") is int retrySeconds + ? TimeSpan.FromSeconds(retrySeconds) + : (TimeSpan?)null; + + var rateLimit = TryParseHeaderInt(responseHeaders, "X-RateLimit-Limit"); + var rateLimitRemaining = TryParseHeaderInt(responseHeaders, "X-RateLimit-Remaining"); + + var rateLimitReset = TryParseHeaderLong(responseHeaders, "X-RateLimit-Reset") is long resetUnix + ? DateTimeOffset.FromUnixTimeSeconds(resetUnix) + : (DateTimeOffset?)null; + + return new BitbucketRateLimitException( + message, errors, retryAfter, rateLimit, rateLimitRemaining, rateLimitReset, requestUrl); + } + + private static int? TryParseHeaderInt( + System.Net.Http.Headers.HttpResponseHeaders headers, string name) + { + return headers.TryGetValues(name, out var values) + && int.TryParse(values.FirstOrDefault(), out int result) + ? result + : null; + } + + private static long? TryParseHeaderLong( + System.Net.Http.Headers.HttpResponseHeaders headers, string name) + { + return headers.TryGetValues(name, out var values) + && long.TryParse(values.FirstOrDefault(), out long result) + ? result + : null; + } + private static string BuildErrorMessage(HttpStatusCode statusCode, IReadOnlyList errors) { if (errors == null || errors.Count == 0) diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs index 5e72612..9650549 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs @@ -9,6 +9,18 @@ namespace Bitbucket.Net.Common.Exceptions; /// public class BitbucketRateLimitException : BitbucketApiException { + /// How long to wait before retrying (from Retry-After header). + public TimeSpan? RetryAfter { get; } + + /// Maximum rate limit (from X-RateLimit-Limit header). + public int? RateLimit { get; } + + /// Remaining requests in the current window (from X-RateLimit-Remaining header). + public int? RateLimitRemaining { get; } + + /// When the rate limit resets (from X-RateLimit-Reset header, Unix seconds). + public DateTimeOffset? RateLimitReset { get; } + /// /// Initializes a new instance of the class. /// @@ -20,6 +32,32 @@ public BitbucketRateLimitException(string message, IReadOnlyList errors, { } + /// + /// Initializes a new instance of the class with rate-limit headers. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// How long to wait before retrying. + /// Maximum rate limit. + /// Remaining requests in the current window. + /// When the rate limit resets. + /// The request URL that caused the error. + public BitbucketRateLimitException( + string message, + IReadOnlyList errors, + TimeSpan? retryAfter, + int? rateLimit, + int? rateLimitRemaining, + DateTimeOffset? rateLimitReset, + string? requestUrl = null) + : base(message, HttpStatusCode.TooManyRequests, errors, requestUrl) + { + RetryAfter = retryAfter; + RateLimit = rateLimit; + RateLimitRemaining = rateLimitRemaining; + RateLimitReset = rateLimitReset; + } + /// /// Initializes a new instance of the class with an inner exception. /// diff --git a/src/Bitbucket.Net/Common/PagedMetadata.cs b/src/Bitbucket.Net/Common/PagedMetadata.cs new file mode 100644 index 0000000..be52780 --- /dev/null +++ b/src/Bitbucket.Net/Common/PagedMetadata.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Bitbucket.Net.Common; + +/// +/// Lightweight pagination metadata extracted from a paged API response. +/// +[StructLayout(LayoutKind.Auto)] +internal readonly record struct PagedMetadata( + bool IsLastPage, + int? NextPageStart, + int? Start, + int? Limit, + int Size); \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/PagedResultsReader.cs b/src/Bitbucket.Net/Common/PagedResultsReader.cs new file mode 100644 index 0000000..7594786 --- /dev/null +++ b/src/Bitbucket.Net/Common/PagedResultsReader.cs @@ -0,0 +1,66 @@ +using System.Text.Json; + +namespace Bitbucket.Net.Common; + +/// +/// Zero-allocation reader for PagedResults pagination metadata. +/// Extracts isLastPage, nextPageStart, start, limit, +/// and size directly from UTF-8 bytes without full deserialization. +/// +internal static class PagedResultsReader +{ + /// + /// Reads pagination metadata from a UTF-8 JSON span. + /// + public static PagedMetadata ReadMetadata(ReadOnlySpan json) + { + var reader = new Utf8JsonReader(json); + bool isLastPage = true; + int? nextPageStart = null; + int? start = null; + int? limit = null; + int size = 0; + + while (reader.Read()) + { + if (reader.TokenType != JsonTokenType.PropertyName) + continue; + + if (reader.ValueTextEquals("isLastPage"u8)) + { + reader.Read(); + isLastPage = reader.GetBoolean(); + } + else if (reader.ValueTextEquals("nextPageStart"u8)) + { + reader.Read(); + nextPageStart = reader.GetInt32(); + } + else if (reader.ValueTextEquals("start"u8)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Number) + start = reader.GetInt32(); + } + else if (reader.ValueTextEquals("limit"u8)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Number) + limit = reader.GetInt32(); + } + else if (reader.ValueTextEquals("size"u8)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Number) + size = reader.GetInt32(); + } + else if (reader.ValueTextEquals("values"u8)) + { + reader.Read(); + reader.Skip(); + } + } + + return new PagedMetadata(isLastPage, nextPageStart, start, limit, size); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/TypeExtensions.cs b/src/Bitbucket.Net/Common/TypeExtensions.cs deleted file mode 100644 index 9dadc55..0000000 --- a/src/Bitbucket.Net/Common/TypeExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; - -namespace Bitbucket.Net.Common; - -/// -/// Provides reflection-based helpers for working with instances. -/// -public static class TypeExtensions -{ - /// - /// Determines whether the specified is a . - /// - /// The type to inspect. - /// when the type is a nullable value type; otherwise . - public static bool IsNullableType(Type type) => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs b/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs deleted file mode 100644 index b69a343..0000000 --- a/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Bitbucket.Net.Common; - -/// -/// Extension methods for converting between Unix epoch milliseconds and . -/// Bitbucket Server represents all timestamps as milliseconds since the Unix epoch (1970-01-01T00:00:00Z). -/// -public static class UnixDateTimeExtensions -{ - /// - /// Converts a Unix epoch millisecond timestamp to a UTC . - /// - /// The number of milliseconds since 1970-01-01T00:00:00Z. - /// A in UTC representing the given timestamp. - public static DateTimeOffset FromUnixTimeMilliseconds(this long value) - { - return DateTimeOffset.FromUnixTimeMilliseconds(value); - } - - /// - /// Converts a to Unix epoch milliseconds. - /// - /// The date and time to convert. - /// The number of milliseconds since 1970-01-01T00:00:00Z. - public static long ToUnixTimeMilliseconds(this DateTimeOffset dateTimeOffset) - { - return dateTimeOffset.ToUnixTimeMilliseconds(); - } -} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs index 8747b88..f8972e7 100644 --- a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; @@ -36,7 +35,7 @@ private IFlurlRequest GetAdminUrl(string path) => GetAdminUrl() /// Optional starting index for pagination. /// Cancellation token. /// A collection of groups. - public async Task> GetAdminGroupsAsync(string? filter = null, + public Task> GetAdminGroupsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -49,16 +48,8 @@ public async Task> GetAdminGroupsAsync(string? ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/groups") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/groups"), queryParamValues, maxPages, cancellationToken); } /// @@ -119,7 +110,7 @@ public async Task AddAdminGroupUsersAsync(GroupUsers groupUsers, Cancellat /// Optional avatar size. /// Cancellation token. /// A collection of group members. - public async Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, + public Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -135,16 +126,8 @@ public async Task> GetAdminGroupMoreMembersAsync(string co ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/groups/more-members") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/groups/more-members"), queryParamValues, maxPages, cancellationToken); } /// @@ -158,7 +141,7 @@ public async Task> GetAdminGroupMoreMembersAsync(string co /// Optional avatar size. /// Cancellation token. /// A collection of non-members. - public async Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, + public Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -174,16 +157,8 @@ public async Task> GetAdminGroupMoreNonMembersAsync(string ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/groups/more-non-members") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/groups/more-non-members"), queryParamValues, maxPages, cancellationToken); } /// @@ -196,7 +171,7 @@ public async Task> GetAdminGroupMoreNonMembersAsync(string /// Optional avatar size. /// Cancellation token. /// A collection of users. - public async Task> GetAdminUsersAsync(string? filter = null, + public Task> GetAdminUsersAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -211,16 +186,8 @@ public async Task> GetAdminUsersAsync(string? filter = nul ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/users") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/users"), queryParamValues, maxPages, cancellationToken); } /// @@ -351,7 +318,7 @@ public async Task UpdateAdminUserCredentialsAsync(PasswordChange passwordC /// Optional starting index for pagination. /// Cancellation token. /// A collection of group memberships. - public async Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, + public Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -365,16 +332,8 @@ public async Task> GetAdminUserMoreMembersAsyn ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/users/more-members") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/users/more-members"), queryParamValues, maxPages, cancellationToken); } /// @@ -387,7 +346,7 @@ public async Task> GetAdminUserMoreMembersAsyn /// Optional starting index for pagination. /// Cancellation token. /// A collection of non-member groups. - public async Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, + public Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -401,16 +360,8 @@ public async Task> GetAdminUserMoreNonMembersA ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/users/more-non-members") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/users/more-non-members"), queryParamValues, maxPages, cancellationToken); } /// @@ -590,7 +541,7 @@ public async Task DeleteAdminMailServerSenderAddressAsync(CancellationToke /// Optional starting index for pagination. /// Cancellation token. /// A collection of group permissions. - public async Task> GetAdminGroupPermissionsAsync(string? filter = null, + public Task> GetAdminGroupPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -603,16 +554,8 @@ public async Task> GetAdminGroupPermissionsAsync(st ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/permissions/groups") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/permissions/groups"), queryParamValues, maxPages, cancellationToken); } /// @@ -663,7 +606,7 @@ public async Task DeleteAdminGroupPermissionsAsync(string name, Cancellati /// Optional starting index for pagination. /// Cancellation token. /// A collection of groups without permissions. - public async Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, + public Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -676,16 +619,8 @@ public async Task> GetAdminGroupPermissionsNon ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/permissions/groups/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/permissions/groups/none"), queryParamValues, maxPages, cancellationToken); } /// @@ -698,7 +633,7 @@ public async Task> GetAdminGroupPermissionsNon /// Optional avatar size. /// Cancellation token. /// A collection of user permissions. - public async Task> GetAdminUserPermissionsAsync(string? filter = null, + public Task> GetAdminUserPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -713,16 +648,8 @@ public async Task> GetAdminUserPermissionsAsync(stri ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/permissions/users") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/permissions/users"), queryParamValues, maxPages, cancellationToken); } /// @@ -774,7 +701,7 @@ public async Task DeleteAdminUserPermissionsAsync(string name, Cancellatio /// Optional avatar size. /// Cancellation token. /// A collection of users without permissions. - public async Task> GetAdminUserPermissionsNoneAsync(string? filter = null, + public Task> GetAdminUserPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -789,16 +716,8 @@ public async Task> GetAdminUserPermissionsNoneAsync(string? fi ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetAdminUrl("/permissions/users/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetAdminUrl("/permissions/users/none"), queryParamValues, maxPages, cancellationToken); } /// @@ -809,6 +728,8 @@ public async Task> GetAdminUserPermissionsNoneAsync(string? fi /// The merge strategies configuration. public async Task GetAdminPullRequestsMergeStrategiesAsync(string scmId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(scmId); + var response = await GetAdminUrl($"/pull-requests/{scmId}") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -825,6 +746,8 @@ public async Task GetAdminPullRequestsMergeStrategiesAsync(stri /// The updated merge strategies. public async Task UpdateAdminPullRequestsMergeStrategiesAsync(string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(scmId); + var response = await GetAdminUrl($"/pull-requests/{scmId}") .SendAsync(HttpMethod.Post, CreateJsonContent(mergeStrategies), cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Core/Admin/IAdminOperations.cs b/src/Bitbucket.Net/Core/Admin/IAdminOperations.cs new file mode 100644 index 0000000..4219e92 --- /dev/null +++ b/src/Bitbucket.Net/Core/Admin/IAdminOperations.cs @@ -0,0 +1,47 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Users; + +namespace Bitbucket.Net; + +/// +/// Administration operations. +/// +public interface IAdminOperations +{ + Task> GetAdminGroupsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateAdminGroupAsync(string name, CancellationToken cancellationToken = default); + Task DeleteAdminGroupAsync(string name, CancellationToken cancellationToken = default); + Task AddAdminGroupUsersAsync(GroupUsers groupUsers, CancellationToken cancellationToken = default); + Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> GetAdminUsersAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task CreateAdminUserAsync(string name, string password, string displayName, string emailAddress, bool addToDefaultGroup = true, string notify = "false", CancellationToken cancellationToken = default); + Task UpdateAdminUserAsync(string? name = null, string? displayName = null, string? emailAddress = null, CancellationToken cancellationToken = default); + Task DeleteAdminUserAsync(string name, CancellationToken cancellationToken = default); + Task AddAdminUserGroupsAsync(UserGroups userGroups, CancellationToken cancellationToken = default); + Task DeleteAdminUserCaptcha(string name, CancellationToken cancellationToken = default); + Task UpdateAdminUserCredentialsAsync(Models.Core.Admin.PasswordChange passwordChange, CancellationToken cancellationToken = default); + Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task RemoveAdminUserFromGroupAsync(string userName, string groupName, CancellationToken cancellationToken = default); + Task RenameAdminUserAsync(UserRename userRename, int? avatarSize = null, CancellationToken cancellationToken = default); + Task GetAdminClusterAsync(CancellationToken cancellationToken = default); + Task GetAdminLicenseAsync(CancellationToken cancellationToken = default); + Task UpdateAdminLicenseAsync(LicenseInfo licenseInfo, CancellationToken cancellationToken = default); + Task GetAdminMailServerAsync(CancellationToken cancellationToken = default); + Task UpdateAdminMailServerAsync(MailServerConfiguration mailServerConfiguration, CancellationToken cancellationToken = default); + Task DeleteAdminMailServerAsync(CancellationToken cancellationToken = default); + Task GetAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default); + Task UpdateAdminMailServerSenderAddressAsync(string senderAddress, CancellationToken cancellationToken = default); + Task DeleteAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default); + Task> GetAdminGroupPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task UpdateAdminGroupPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default); + Task DeleteAdminGroupPermissionsAsync(string name, CancellationToken cancellationToken = default); + Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetAdminUserPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task UpdateAdminUserPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default); + Task DeleteAdminUserPermissionsAsync(string name, CancellationToken cancellationToken = default); + Task> GetAdminUserPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task GetAdminPullRequestsMergeStrategiesAsync(string scmId, CancellationToken cancellationToken = default); + Task UpdateAdminPullRequestsMergeStrategiesAsync(string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs index 3bf9725..d40bb63 100644 --- a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; @@ -38,7 +37,7 @@ private IFlurlRequest GetDashboardUrl(string path) => GetDashboardUrl() /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of pull requests. - public async Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, + public Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, Roles? role = null, List? status = null, PullRequestOrders? order = PullRequestOrders.Newest, @@ -59,16 +58,8 @@ public async Task> GetDashboardPullRequestsAsync(PullRe ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetDashboardUrl("/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetDashboardUrl("/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// @@ -105,15 +96,8 @@ public IAsyncEnumerable GetDashboardPullRequestsStreamAsync(PullReq ["start"] = start, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetDashboardUrl("/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetDashboardUrl("/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// @@ -125,7 +109,7 @@ public IAsyncEnumerable GetDashboardPullRequestsStreamAsync(PullReq /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of pull request suggestions. - public async Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, + public Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, int? maxPages = null, int? limit = 3, int? start = null, @@ -138,15 +122,7 @@ public async Task> GetDashboardPullRequestSug ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetDashboardUrl("/pull-request-suggestions") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetDashboardUrl("/pull-request-suggestions"), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs index a44e41c..bfdae71 100644 --- a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs @@ -1,5 +1,3 @@ -using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Flurl.Http; namespace Bitbucket.Net; @@ -25,7 +23,7 @@ private IFlurlRequest GetGroupsUrl() => GetBaseUrl() /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of group names. - public async Task> GetGroupNamesAsync(string? filter = null, + public Task> GetGroupNamesAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -38,15 +36,7 @@ public async Task> GetGroupNamesAsync(string? filter = null, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetGroupsUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetGroupsUrl(), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs b/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs index 878ea2b..f1c6a37 100644 --- a/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs @@ -24,6 +24,8 @@ private IFlurlRequest GetHooksUrl() => GetBaseUrl() /// The avatar image bytes. public async Task GetProjectHooksAvatarAsync(string hookKey, string? version = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetHooksUrl() .AppendPathSegment($"/{hookKey}/avatar") .SetQueryParam("version", version) diff --git a/src/Bitbucket.Net/Core/ISearchOperations.cs b/src/Bitbucket.Net/Core/ISearchOperations.cs new file mode 100644 index 0000000..c7254f6 --- /dev/null +++ b/src/Bitbucket.Net/Core/ISearchOperations.cs @@ -0,0 +1,16 @@ +using Bitbucket.Net.Common.Models.Search; +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net; + +/// +/// Search and repository listing operations. +/// +public interface ISearchOperations +{ + Task> GetRepositoriesAsync(int? maxPages = null, int? limit = null, int? start = null, string? name = null, string? projectName = null, Permissions? permission = null, bool isPublic = false, CancellationToken cancellationToken = default); + IAsyncEnumerable GetRepositoriesStreamAsync(int? maxPages = null, int? limit = null, int? start = null, string? name = null, string? projectName = null, Permissions? permission = null, bool isPublic = false, CancellationToken cancellationToken = default); + Task SearchCodeAsync(string query, int primaryLimit = 25, int secondaryLimit = 10, CancellationToken cancellationToken = default); + Task IsSearchAvailableAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs index 762e7bc..6fc66d5 100644 --- a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; using System.Text.Json; @@ -35,7 +34,7 @@ private IFlurlRequest GetInboxUrl(string path) => GetInboxUrl() /// The participant role filter (default reviewer). /// Token to cancel the operation. /// A collection of pull requests. - public async Task> GetInboxPullRequestsAsync( + public Task> GetInboxPullRequestsAsync( int? maxPages = null, int? limit = 25, int? start = 0, @@ -49,16 +48,8 @@ public async Task> GetInboxPullRequestsAsync( ["role"] = BitbucketHelpers.RoleToString(role), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetInboxUrl("/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetInboxUrl("/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// @@ -84,15 +75,8 @@ public IAsyncEnumerable GetInboxPullRequestsStreamAsync( ["role"] = BitbucketHelpers.RoleToString(role), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetInboxUrl("/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetInboxUrl("/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// diff --git a/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs b/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs index 69e42fd..e86b44f 100644 --- a/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs @@ -33,6 +33,8 @@ private IFlurlRequest GetLogsUrl(string path) => GetLogsUrl() /// The configured log level. public async Task GetLogLevelAsync(string loggerName, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(loggerName); + var response = await GetLogsUrl($"/logger/{loggerName}") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -54,6 +56,8 @@ public async Task GetLogLevelAsync(string loggerName, CancellationTok /// true if the update succeeded; otherwise, false. public async Task SetLogLevelAsync(string loggerName, LogLevels logLevel, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(loggerName); + var response = await GetLogsUrl($"/logger/{loggerName}/{BitbucketHelpers.LogLevelToString(logLevel)}") .PutAsync(new StringContent(""), cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs index 313445c..b1e1845 100644 --- a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; @@ -35,7 +34,7 @@ private IFlurlRequest GetProfileUrl(string path) => GetProfileUrl() /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of recent repositories. - public async Task> GetRecentReposAsync(Permissions? permission = null, + public Task> GetRecentReposAsync(Permissions? permission = null, int? maxPages = null, int? limit = null, int? start = null, @@ -48,15 +47,7 @@ public async Task> GetRecentReposAsync(Permissions? perm ["permission"] = BitbucketHelpers.PermissionToString(permission), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProfileUrl("/recent/repos") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProfileUrl("/recent/repos"), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs index 592ac82..399b5b5 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -1,6 +1,6 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Flurl.Http; using System.Buffers; using System.Runtime.CompilerServices; @@ -23,7 +23,7 @@ public partial class BitbucketClient /// Optional branch ordering. /// Cancellation token. /// A collection of branches. - public async Task> GetBranchesAsync(string projectKey, string repositorySlug, + public Task> GetBranchesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -43,16 +43,8 @@ public async Task> GetBranchesAsync(string projectKey, strin ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/branches"), queryParamValues, maxPages, cancellationToken); } /// @@ -78,15 +70,8 @@ public IAsyncEnumerable GetBranchesStreamAsync(string projectKey, string ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/branches"), queryParamValues, maxPages, cancellationToken); } /// @@ -94,13 +79,15 @@ public IAsyncEnumerable GetBranchesStreamAsync(string projectKey, string /// /// The project key. /// The repository slug. - /// The branch information. + /// The create branch request. /// Cancellation token. /// The created branch. - public async Task CreateBranchAsync(string projectKey, string repositorySlug, BranchInfo branchInfo, CancellationToken cancellationToken = default) + public async Task CreateBranchAsync(string projectKey, string repositorySlug, CreateBranchRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .SendAsync(HttpMethod.Post, CreateJsonContent(branchInfo), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -194,6 +181,8 @@ public async Task BrowseProjectRepositoryPathAsync(string projec bool noContent = false, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["at"] = at, @@ -229,6 +218,8 @@ public async Task GetRawFileContentStreamAsync(string projectKey, string string? at = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var request = GetProjectsReposUrl(projectKey, repositorySlug, $"/raw/{path}"); if (!string.IsNullOrEmpty(at)) @@ -257,6 +248,8 @@ public async IAsyncEnumerable GetRawFileContentLinesStreamAsync(string p string? at = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var stream = await GetRawFileContentStreamAsync(projectKey, repositorySlug, path, at, cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false)) @@ -286,6 +279,8 @@ public async Task UpdateProjectRepositoryPathAsync(string projectKey, st string? sourceBranch = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + if (!File.Exists(fileName)) { throw new ArgumentException($"File doesn't exist: {fileName}", nameof(fileName)); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs index d6f1b21..26e08f5 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; using System.Runtime.CompilerServices; @@ -20,7 +19,7 @@ public partial class BitbucketClient /// Optional starting index for pagination. /// Cancellation token. /// A collection of changes. - public async Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, + public Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, int? maxPages = null, int? limit = null, int? start = null, @@ -34,16 +33,8 @@ public async Task> GetChangesAsync(string projectKey, string ["until"] = until, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -72,15 +63,8 @@ public IAsyncEnumerable GetChangesStreamAsync(string projectKey, string ["until"] = until, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -100,7 +84,7 @@ public IAsyncEnumerable GetChangesStreamAsync(string projectKey, string /// Optional starting index for pagination. /// Cancellation token. /// A collection of commits. - public async Task> GetCommitsAsync(string projectKey, string repositorySlug, + public Task> GetCommitsAsync(string projectKey, string repositorySlug, string until, bool followRenames = false, bool ignoreMissing = false, @@ -126,16 +110,8 @@ public async Task> GetCommitsAsync(string projectKey, string ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -167,15 +143,8 @@ public IAsyncEnumerable GetCommitsStreamAsync(string projectKey, string ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -189,6 +158,8 @@ public IAsyncEnumerable GetCommitsStreamAsync(string projectKey, string /// The requested commit. public async Task GetCommitAsync(string projectKey, string repositorySlug, string commitId, string? path = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["path"] = path, @@ -215,7 +186,7 @@ public async Task GetCommitAsync(string projectKey, string repositorySlu /// Optional starting index for pagination. /// Cancellation token. /// A collection of changes. - public async Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, + public Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, string? since = null, bool withComments = true, int? maxPages = null, @@ -223,6 +194,8 @@ public async Task> GetCommitChangesAsync(string projectKey, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -231,16 +204,8 @@ public async Task> GetCommitChangesAsync(string projectKey, ["withComments"] = BitbucketHelpers.BoolToString(withComments), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -264,6 +229,8 @@ public IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, s int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -272,15 +239,8 @@ public IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, s ["withComments"] = BitbucketHelpers.BoolToString(withComments), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -296,7 +256,7 @@ public IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, s /// Optional starting index for pagination. /// Cancellation token. /// A collection of comments. - public async Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, + public Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, string path, string? since = null, int? maxPages = null, @@ -304,6 +264,8 @@ public async Task> GetCommitCommentsAsync(string projectKey int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -312,16 +274,8 @@ public async Task> GetCommitCommentsAsync(string projectKey ["since"] = since, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments"), queryParamValues, maxPages, cancellationToken); } /// @@ -337,6 +291,8 @@ public async Task> GetCommitCommentsAsync(string projectKey public async Task CreateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, CommentInfo commentInfo, string? since = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["since"] = since, @@ -364,6 +320,8 @@ public async Task GetCommitCommentAsync(string projectKey, string re int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken) @@ -385,6 +343,8 @@ public async Task GetCommitCommentAsync(string projectKey, string re public async Task UpdateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, CommentText commentText, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") .SendAsync(HttpMethod.Put, CreateJsonContent(commentText), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -406,6 +366,8 @@ public async Task DeleteCommitCommentAsync(string projectKey, string repos int version = -1, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["version"] = version, @@ -442,6 +404,8 @@ public async Task GetCommitDiffAsync(string projectKey, string repo bool withComments = true, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), @@ -484,6 +448,8 @@ public async IAsyncEnumerable GetCommitDiffStreamAsync(string projectKey, bool withComments = true, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), @@ -521,6 +487,8 @@ public async IAsyncEnumerable GetCommitDiffStreamAsync(string projectKey, /// true if watch was created; otherwise, false. public async Task CreateCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") .SendAsync(HttpMethod.Post, CreateEmptyJsonContent(), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -538,6 +506,8 @@ public async Task CreateCommitWatchAsync(string projectKey, string reposit /// true if the watch was removed; otherwise, false. public async Task DeleteCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs index fcdd17d..d34cd5d 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; using System.Runtime.CompilerServices; @@ -21,7 +20,7 @@ public partial class BitbucketClient /// Optional starting index for pagination. /// Cancellation token. /// A collection of changes between the refs. - public async Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, + public Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, @@ -37,16 +36,8 @@ public async Task> GetRepositoryCompareChangesAsync(string p ["fromRepo"] = fromRepo, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/compare/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -148,7 +139,7 @@ public async IAsyncEnumerable GetRepositoryCompareDiffStreamAsync(string p /// Optional starting index for pagination. /// Cancellation token. /// A collection of commits between the refs. - public async Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, + public Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, @@ -164,16 +155,8 @@ public async Task> GetRepositoryCompareCommitsAsync(string p ["fromRepo"] = fromRepo, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/compare/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -269,7 +252,7 @@ public async IAsyncEnumerable GetRepositoryDiffStreamAsync(string projectK /// Optional starting index for pagination. /// Cancellation token. /// A collection of file paths. - public async Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, + public Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, int? maxPages = null, int? limit = null, int? start = null, @@ -282,16 +265,8 @@ public async Task> GetRepositoryFilesAsync(string projectKey ["at"] = at, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/files") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/files"), queryParamValues, maxPages, cancellationToken); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs index d04b84d..c601058 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs @@ -1,7 +1,7 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Flurl.Http; using System.Text.Json; @@ -29,8 +29,11 @@ private IFlurlRequest GetProjectsUrl(string path) => GetProjectsUrl() /// /// The project key. /// An pointing to the project. - private IFlurlRequest GetProjectUrl(string projectKey) => GetProjectsUrl() - .AppendPathSegment($"/{projectKey}"); + private IFlurlRequest GetProjectUrl(string projectKey) + { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + return GetProjectsUrl().AppendPathSegment($"/{projectKey}"); + } /// /// Gets the URL for a repository within a project. @@ -38,7 +41,12 @@ private IFlurlRequest GetProjectUrl(string projectKey) => GetProjectsUrl() /// The project key. /// The repository slug. /// An pointing to the repository. - private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug) => GetProjectsUrl($"/{projectKey}/repos/{repositorySlug}"); + private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug) + { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + return GetProjectsUrl($"/{projectKey}/repos/{repositorySlug}"); + } /// /// Gets the URL for a specific path within a project repository. @@ -60,7 +68,7 @@ private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySl /// Optional permission filter. /// Token to cancel the operation. /// A collection of projects. - public async Task> GetProjectsAsync( + public Task> GetProjectsAsync( int? maxPages = null, int? limit = null, int? start = null, @@ -76,16 +84,8 @@ public async Task> GetProjectsAsync( ["permission"] = BitbucketHelpers.PermissionToString(permission), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl(), queryParamValues, maxPages, cancellationToken); } /// @@ -114,27 +114,22 @@ public IAsyncEnumerable GetProjectsStreamAsync( ["permission"] = BitbucketHelpers.PermissionToString(permission), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsUrl(), queryParamValues, maxPages, cancellationToken); } /// /// Creates a project. /// - /// The project definition. + /// The create project request. /// Token to cancel the operation. /// The created project. - public async Task CreateProjectAsync(ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) + public async Task CreateProjectAsync(CreateProjectRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsUrl() - .SendAsync(HttpMethod.Post, CreateJsonContent(projectDefinition), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -148,6 +143,8 @@ public async Task CreateProjectAsync(ProjectDefinition projectDefinitio /// true if deletion succeeded; otherwise, false. public async Task DeleteProjectAsync(string projectKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetProjectsUrl($"/{projectKey}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -159,13 +156,16 @@ public async Task DeleteProjectAsync(string projectKey, CancellationToken /// Updates a project. /// /// The project key. - /// The updated project definition. + /// The update project request. /// Token to cancel the operation. /// The updated project. - public async Task UpdateProjectAsync(string projectKey, ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) + public async Task UpdateProjectAsync(string projectKey, UpdateProjectRequest request, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsUrl($"/{projectKey}") - .SendAsync(HttpMethod.Put, CreateJsonContent(projectDefinition), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Put, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -179,6 +179,8 @@ public async Task UpdateProjectAsync(string projectKey, ProjectDefiniti /// The requested project. public async Task GetProjectAsync(string projectKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetProjectsUrl($"/{projectKey}") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -197,13 +199,15 @@ public async Task GetProjectAsync(string projectKey, CancellationToken /// Optional avatar size. /// Token to cancel the operation. /// A collection of user permissions. - public async Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, + public Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -212,16 +216,8 @@ public async Task> GetProjectUserPermissionsAsync(st ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/permissions/users") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl($"/{projectKey}/permissions/users"), queryParamValues, maxPages, cancellationToken); } /// @@ -233,6 +229,9 @@ public async Task> GetProjectUserPermissionsAsync(st /// true if removal succeeded; otherwise, false. public async Task DeleteProjectUserPermissionsAsync(string projectKey, string userName, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(userName); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["name"] = userName, @@ -256,6 +255,9 @@ public async Task DeleteProjectUserPermissionsAsync(string projectKey, str /// true if the update succeeded; otherwise, false. public async Task UpdateProjectUserPermissionsAsync(string projectKey, string userName, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(userName); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["name"] = userName, @@ -280,12 +282,14 @@ public async Task UpdateProjectUserPermissionsAsync(string projectKey, str /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of licensed users without project permissions. - public async Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, + public Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -293,16 +297,8 @@ public async Task> GetProjectUserPermissionsNoneAsync( ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/permissions/users/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl($"/{projectKey}/permissions/users/none"), queryParamValues, maxPages, cancellationToken); } /// @@ -315,12 +311,14 @@ public async Task> GetProjectUserPermissionsNoneAsync( /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of group permissions. - public async Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, + public Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -328,16 +326,8 @@ public async Task> GetProjectGroupPermissionsAsync( ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl($"/{projectKey}/permissions/groups"), queryParamValues, maxPages, cancellationToken); } /// @@ -349,6 +339,9 @@ public async Task> GetProjectGroupPermissionsAsync( /// true if the group permissions were removed; otherwise, false. public async Task DeleteProjectGroupPermissionsAsync(string projectKey, string groupName, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(groupName); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["name"] = groupName, @@ -372,6 +365,9 @@ public async Task DeleteProjectGroupPermissionsAsync(string projectKey, st /// true if the update succeeded; otherwise, false. public async Task UpdateProjectGroupPermissionsAsync(string projectKey, string groupName, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(groupName); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["name"] = groupName, @@ -396,12 +392,14 @@ public async Task UpdateProjectGroupPermissionsAsync(string projectKey, st /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of licensed users representing groups without permissions. - public async Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, + public Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -409,16 +407,8 @@ public async Task> GetProjectGroupPermissionsNoneAsync ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/permissions/groups/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl($"/{projectKey}/permissions/groups/none"), queryParamValues, maxPages, cancellationToken); } /// @@ -430,6 +420,8 @@ public async Task> GetProjectGroupPermissionsNoneAsync /// true if the permission is granted to all; otherwise, false. public async Task IsProjectDefaultPermissionAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetProjectsUrl($"/{projectKey}/permissions/{BitbucketHelpers.PermissionToString(permission)}/all") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -444,6 +436,8 @@ public async Task IsProjectDefaultPermissionAsync(string projectKey, Permi private async Task SetProjectDefaultPermissionAsync(string projectKey, Permissions permission, bool allow, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["allow"] = BitbucketHelpers.BoolToString(allow), diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs index 77a4dc9..e8a72c5 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; @@ -103,7 +102,7 @@ public async Task CreatePullRequestCommentAsync(string projectKey, s /// Optional avatar size. /// Cancellation token. /// A collection of pull request comments. - public async Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, string path, AnchorStates anchorState = AnchorStates.Active, DiffTypes diffType = DiffTypes.Effective, @@ -127,16 +126,8 @@ public async Task> GetPullRequestCommentsAsync(string pr ["toHash"] = toHash, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments"), queryParamValues, maxPages, cancellationToken); } /// @@ -180,15 +171,8 @@ public IAsyncEnumerable GetPullRequestCommentsStreamAsync(string pro ["toHash"] = toHash, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments"), queryParamValues, maxPages, cancellationToken); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs index 1f17f63..050fcd5 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; using System.Runtime.CompilerServices; @@ -23,7 +22,7 @@ public partial class BitbucketClient /// Optional avatar size. /// Cancellation token. /// A collection of pull request activities. - public async Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, long? fromId = null, PullRequestFromTypes? fromType = null, int? maxPages = null, @@ -41,16 +40,8 @@ public async Task> GetPullRequestActivitiesAsyn ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities"), queryParamValues, maxPages, cancellationToken); } /// @@ -85,15 +76,8 @@ public IAsyncEnumerable GetPullRequestActivitiesStreamAsync ["avatarSize"] = avatarSize, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities"), queryParamValues, maxPages, cancellationToken); } @@ -112,7 +96,7 @@ public IAsyncEnumerable GetPullRequestActivitiesStreamAsync /// Optional starting index for pagination. /// Cancellation token. /// A collection of changes. - public async Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, ChangeScopes changeScope = ChangeScopes.All, string? sinceId = null, string? untilId = null, @@ -132,16 +116,8 @@ public async Task> GetPullRequestChangesAsync(string project ["withComments"] = BitbucketHelpers.BoolToString(withComments), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes"), queryParamValues, maxPages, cancellationToken); } /// @@ -179,15 +155,8 @@ public IAsyncEnumerable GetPullRequestChangesStreamAsync(string projectK ["withComments"] = BitbucketHelpers.BoolToString(withComments), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes"), queryParamValues, maxPages, cancellationToken); } @@ -203,7 +172,7 @@ public IAsyncEnumerable GetPullRequestChangesStreamAsync(string projectK /// Optional starting index for pagination. /// Cancellation token. /// A collection of commits. - public async Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, bool withCounts = false, int? maxPages = null, int? limit = null, @@ -217,16 +186,8 @@ public async Task> GetPullRequestCommitsAsync(string project ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -246,15 +207,8 @@ public IAsyncEnumerable GetPullRequestCommitsStreamAsync(string projectK ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -368,6 +322,8 @@ public async Task GetPullRequestDiffPathAsync(string projectKey, st bool withComments = true, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); var response = await GetProjectsReposUrl(projectKey, repositorySlug) diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs index 6f2621e..b41e106 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -1,6 +1,6 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; @@ -21,7 +21,7 @@ public partial class BitbucketClient /// Optional starting index for pagination. /// Cancellation token. /// A collection of identities. - public async Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, + public Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, PullRequestDirections direction = PullRequestDirections.Incoming, string? filter = null, Roles? role = null, @@ -39,16 +39,8 @@ public async Task> GetRepositoryParticipantsAsync(string p ["role"] = BitbucketHelpers.RoleToString(role), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/participants") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/participants"), queryParamValues, maxPages, cancellationToken); } /// @@ -67,7 +59,7 @@ public async Task> GetRepositoryParticipantsAsync(string p /// Whether to include properties. /// Cancellation token. /// A collection of pull requests. - public async Task> GetPullRequestsAsync(string projectKey, string repositorySlug, + public Task> GetPullRequestsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -91,16 +83,8 @@ public async Task> GetPullRequestsAsync(string projectK ["withProperties"] = BitbucketHelpers.BoolToString(withProperties), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// @@ -130,15 +114,8 @@ public IAsyncEnumerable GetPullRequestsStreamAsync(string projectKe ["withProperties"] = BitbucketHelpers.BoolToString(withProperties), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests"), queryParamValues, maxPages, cancellationToken); } /// @@ -146,13 +123,15 @@ public IAsyncEnumerable GetPullRequestsStreamAsync(string projectKe /// /// The project key. /// The repository slug. - /// The pull request payload. + /// The create pull request payload. /// Cancellation token. /// The created pull request. - public async Task CreatePullRequestAsync(string projectKey, string repositorySlug, PullRequestInfo pullRequestInfo, CancellationToken cancellationToken = default) + public async Task CreatePullRequestAsync(string projectKey, string repositorySlug, CreatePullRequestRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .SendAsync(HttpMethod.Post, CreateJsonContent(pullRequestInfo), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -181,13 +160,15 @@ public async Task GetPullRequestAsync(string projectKey, string rep /// The project key. /// The repository slug. /// The pull request ID. - /// The update payload. + /// The update pull request payload. /// Cancellation token. /// The updated pull request. - public async Task UpdatePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, PullRequestUpdate pullRequestUpdate, CancellationToken cancellationToken = default) + public async Task UpdatePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, UpdatePullRequestRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") - .SendAsync(HttpMethod.Put, CreateJsonContent(pullRequestUpdate), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Put, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -294,19 +275,21 @@ public async Task GetPullRequestMergeStateAsync(string pr /// The project key. /// The repository slug. /// The pull request ID. - /// Optional version for optimistic concurrency. + /// Optional merge request. When null, a default request is used. /// Cancellation token. /// The merged pull request. - public async Task MergePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) + public async Task MergePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, MergePullRequestRequest? request = null, CancellationToken cancellationToken = default) { + var mergeRequest = request ?? new MergePullRequestRequest(); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - ["version"] = version, + ["version"] = mergeRequest.Version, }; var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge") .SetQueryParams(queryParamValues) - .SendAsync(HttpMethod.Post, CreateEmptyJsonContent(), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(mergeRequest), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -382,7 +365,7 @@ public async Task DeletePullRequestApprovalAsync(string projectKey, st /// Optional avatar size. /// Cancellation token. /// A collection of participants. - public async Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, @@ -396,17 +379,9 @@ public async Task> GetPullRequestParticipantsAsync(stri ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants"), queryParamValues, maxPages, cancellationToken); } /// @@ -435,16 +410,9 @@ public IAsyncEnumerable GetPullRequestParticipantsStreamAsync(strin ["avatarSize"] = avatarSize, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants"), queryParamValues, maxPages, cancellationToken); } /// @@ -515,6 +483,8 @@ public async Task UpdatePullRequestParticipantStatus(string project ParticipantStatus participantStatus, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var data = new { user = named, @@ -541,6 +511,8 @@ public async Task UpdatePullRequestParticipantStatus(string project /// true if removal succeeded; otherwise, false. public async Task UnassignUserFromPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, string userSlug, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetProjectsReposUrl(projectKey, repositorySlug) .AppendPathSegment($"/pull-requests/{pullRequestId}/participants/{userSlug}") .DeleteAsync(cancellationToken: cancellationToken) diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs index 517114b..ad7ede4 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs @@ -1,7 +1,7 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; @@ -18,28 +18,22 @@ public partial class BitbucketClient /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of repositories. - public async Task> GetProjectRepositoriesAsync(string projectKey, + public Task> GetProjectRepositoriesAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/repos") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsUrl($"/{projectKey}/repos"), queryParamValues, maxPages, cancellationToken); } /// @@ -51,41 +45,32 @@ public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string pro int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, ["start"] = start, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsUrl($"/{projectKey}/repos") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsUrl($"/{projectKey}/repos"), queryParamValues, maxPages, cancellationToken); } /// /// Creates a repository within a project. /// /// The project key. - /// The repository name. - /// Optional SCM identifier (default is git). + /// The create repository request. /// Token to cancel the operation. /// The created repository. - public async Task CreateProjectRepositoryAsync(string projectKey, string repositoryName, string scmId = "git", CancellationToken cancellationToken = default) + public async Task CreateProjectRepositoryAsync(string projectKey, CreateRepositoryRequest request, CancellationToken cancellationToken = default) { - var data = new - { - name = repositoryName, - scmId, - }; + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentNullException.ThrowIfNull(request); var response = await GetProjectsUrl($"/{projectKey}/repos") - .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -112,22 +97,13 @@ public async Task GetProjectRepositoryAsync(string projectKey, strin /// /// The source project key. /// The source repository slug. - /// Optional target project key for the fork. - /// Optional target repository slug. - /// Optional display name for the fork. + /// Optional fork repository request. When null, a default request is used. /// Token to cancel the operation. /// The created repository fork. - public async Task CreateProjectRepositoryForkAsync(string projectKey, string repositorySlug, string? targetProjectKey = null, string? targetSlug = null, string? targetName = null, CancellationToken cancellationToken = default) + public async Task CreateProjectRepositoryForkAsync(string projectKey, string repositorySlug, ForkRepositoryRequest? request = null, CancellationToken cancellationToken = default) { - var data = new - { - slug = targetSlug ?? repositorySlug, - name = targetName, - project = targetProjectKey == null ? null : new ProjectRef { Key = targetProjectKey }, - }; - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request ?? new ForkRepositoryRequest()), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -192,7 +168,7 @@ public async Task UpdateProjectRepositoryAsync(string projectKey, st /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of repository forks. - public async Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -204,16 +180,8 @@ public async Task> GetProjectRepositoryForksAsync(st ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/forks") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/forks"), queryParamValues, maxPages, cancellationToken); } /// @@ -242,7 +210,7 @@ public async Task RecreateProjectRepositoryAsync(string projectKey, /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of related repository forks. - public async Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, + public Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -254,16 +222,8 @@ public async Task> GetRelatedProjectRepositoriesAsyn ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/related") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/related"), queryParamValues, maxPages, cancellationToken); } /// @@ -315,7 +275,7 @@ public async Task GetProjectRepositoryArchiveAsync(string projectKey, st /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of group permissions. - public async Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -329,16 +289,8 @@ public async Task> GetProjectRepositoryGroupPermiss ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups"), queryParamValues, maxPages, cancellationToken); } /// @@ -395,7 +347,7 @@ public async Task DeleteProjectRepositoryGroupPermissionsAsync(string proj /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of removable group or user entries. - public async Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -409,16 +361,8 @@ public async Task> GetProjectRepositoryGroupPe ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups/none"), queryParamValues, maxPages, cancellationToken); } /// @@ -433,7 +377,7 @@ public async Task> GetProjectRepositoryGroupPe /// Optional avatar size. /// Token to cancel the operation. /// A collection of user permissions. - public async Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -449,16 +393,8 @@ public async Task> GetProjectRepositoryUserPermissio ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users"), queryParamValues, maxPages, cancellationToken); } /// @@ -519,7 +455,7 @@ public async Task DeleteProjectRepositoryUserPermissionsAsync(string proje /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of users without repository permissions. - public async Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -533,15 +469,7 @@ public async Task> GetProjectRepositoryUserPermissionsNoneAsyn ["filter"] = filter, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users/none") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users/none"), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs index 4c33059..2587800 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs @@ -1,7 +1,7 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Flurl.Http; namespace Bitbucket.Net; @@ -27,6 +27,8 @@ public async Task RetrieveRawContentAsync(string projectKey, string repo bool htmlEscape = true, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["at"] = at, @@ -90,7 +92,7 @@ public async Task UpdateProjectRepositoryPullRequestSetting /// Optional starting index for pagination. /// Cancellation token. /// A collection of hooks. - public async Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, HookTypes? hookType = null, int? maxPages = null, int? limit = null, @@ -104,16 +106,8 @@ public async Task> GetProjectRepositoryHooksSettingsAsync(stri ["type"] = hookType, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/hooks") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/settings/hooks"), queryParamValues, maxPages, cancellationToken); } /// @@ -126,6 +120,8 @@ public async Task> GetProjectRepositoryHooksSettingsAsync(stri /// The hook configuration. public async Task GetProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -143,6 +139,8 @@ public async Task GetProjectRepositoryHookSettingsAsync(string projectKey, /// true if deletion succeeded; otherwise, false. public async Task DeleteProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -161,6 +159,8 @@ public async Task DeleteProjectRepositoryHookSettingsAsync(string projectK /// The enabled hook. public async Task EnableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, object? hookSettings = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") .SendAsync(HttpMethod.Put, CreateJsonContent(hookSettings), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -178,6 +178,8 @@ public async Task EnableProjectRepositoryHookAsync(string projectKey, stri /// The disabled hook. public async Task DisableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -195,6 +197,8 @@ public async Task DisableProjectRepositoryHookAsync(string projectKey, str /// A dictionary of hook settings. public async Task> GetProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -214,6 +218,8 @@ public async Task DisableProjectRepositoryHookAsync(string projectKey, str public async Task> UpdateProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, Dictionary allSettings, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(hookKey); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") .SendAsync(HttpMethod.Put, CreateJsonContent(allSettings), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -230,6 +236,8 @@ public async Task DisableProjectRepositoryHookAsync(string projectKey, str /// The pull request settings. public async Task GetProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(scmId); + var response = await GetProjectUrl(projectKey) .AppendPathSegment($"/settings/pull-requests/{scmId}") .GetAsync(cancellationToken) @@ -248,6 +256,8 @@ public async Task GetProjectPullRequestsMergeStrategiesAsyn /// The updated merge strategies. public async Task UpdateProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(scmId); + var response = await GetProjectUrl(projectKey) .AppendPathSegment($"/settings/pull-requests/{scmId}") .SendAsync(HttpMethod.Post, CreateJsonContent(mergeStrategies), cancellationToken: cancellationToken) @@ -268,7 +278,7 @@ public async Task UpdateProjectPullRequestsMergeStrategiesAsync /// Optional starting index for pagination. /// Cancellation token. /// A collection of tags. - public async Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, string filterText, BranchOrderBy orderBy, int? maxPages = null, @@ -284,16 +294,8 @@ public async Task> GetProjectRepositoryTagsAsync(string project ["orderBy"] = BitbucketHelpers.BranchOrderByToString(orderBy), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/tags"), queryParamValues, maxPages, cancellationToken); } /// @@ -324,15 +326,8 @@ public IAsyncEnumerable GetProjectRepositoryTagsStreamAsync(string projectK ["orderBy"] = BitbucketHelpers.BranchOrderByToString(orderBy), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/tags"), queryParamValues, maxPages, cancellationToken); } /// @@ -375,6 +370,8 @@ public async Task CreateProjectRepositoryTagAsync(string projectKey, string /// The requested tag. public async Task GetProjectRepositoryTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(tagName); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/tags/{tagName}") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -394,7 +391,7 @@ public async Task GetProjectRepositoryTagAsync(string projectKey, string re /// Optional starting index for pagination. /// Cancellation token. /// A collection of webhooks. - public async Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, + public Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, string? @event = null, bool statistics = false, int? maxPages = null, @@ -410,16 +407,8 @@ public async Task> GetProjectRepositoryWebHooksAsync(string ["statistics"] = BitbucketHelpers.BoolToString(statistics), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks"), queryParamValues, maxPages, cancellationToken); } /// @@ -427,13 +416,15 @@ public async Task> GetProjectRepositoryWebHooksAsync(string /// /// The project key. /// The repository slug. - /// The webhook payload. + /// The create webhook request. /// Cancellation token. /// The created webhook. - public async Task CreateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, WebHook webHook, CancellationToken cancellationToken = default) + public async Task CreateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, CreateWebHookRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") - .SendAsync(HttpMethod.Post, CreateJsonContent(webHook), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -471,6 +462,8 @@ public async Task GetProjectRepositoryWebHookAsync(string projectKey, s bool statistics = false, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["statistics"] = BitbucketHelpers.BoolToString(statistics), @@ -490,14 +483,17 @@ public async Task GetProjectRepositoryWebHookAsync(string projectKey, s /// The project key. /// The repository slug. /// The webhook ID. - /// The webhook payload. + /// The update webhook request. /// Cancellation token. /// The updated webhook. public async Task UpdateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, - string webHookId, WebHook webHook, CancellationToken cancellationToken = default) + string webHookId, UpdateWebHookRequest request, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + ArgumentNullException.ThrowIfNull(request); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") - .SendAsync(HttpMethod.Put, CreateJsonContent(webHook), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Put, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -514,6 +510,8 @@ public async Task UpdateProjectRepositoryWebHookAsync(string projectKey public async Task DeleteProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string webHookId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -538,6 +536,8 @@ public async Task GetProjectRepositoryWebHookLatestAsync(string projectK WebHookOutcomes? outcome = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["event"] = @event, @@ -567,6 +567,8 @@ public async Task GetProjectRepositoryWebHookStatisticsAsync( string? @event = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics") .SetQueryParam("event", @event) .GetAsync(cancellationToken) @@ -586,6 +588,8 @@ public async Task GetProjectRepositoryWebHookStatisticsAsync( public async Task> GetProjectRepositoryWebHookStatisticsSummaryAsync(string projectKey, string repositorySlug, string webHookId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics/summary") .GetAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs index 7aadc85..08c8b4b 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs @@ -1,6 +1,5 @@ using Bitbucket.Net.Common; using Bitbucket.Net.Common.Exceptions; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Tasks; using Flurl.Http; @@ -31,7 +30,7 @@ public partial class BitbucketClient /// /// [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync for 9.0+ or GetPullRequestTasksWithFallbackAsync for cross-version compatibility.")] - public async Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, + public Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, @@ -45,16 +44,8 @@ public async Task> GetPullRequestTasksAsync(string pr ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks"), queryParamValues, maxPages, cancellationToken); } /// @@ -92,15 +83,8 @@ public IAsyncEnumerable GetPullRequestTasksStreamAsync(string pro ["avatarSize"] = avatarSize, }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks"), queryParamValues, maxPages, cancellationToken); } /// @@ -154,7 +138,7 @@ public async Task GetPullRequestTaskCountAsync(string projec /// For servers prior to 9.0, use instead. /// /// - public async Task> GetPullRequestBlockerCommentsAsync( + public Task> GetPullRequestBlockerCommentsAsync( string projectKey, string repositorySlug, long pullRequestId, @@ -171,16 +155,8 @@ public async Task> GetPullRequestBlockerCommentsAsyn ["state"] = BitbucketHelpers.BlockerCommentStateToString(state), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments"), queryParamValues, maxPages, cancellationToken); } /// @@ -213,15 +189,8 @@ public IAsyncEnumerable GetPullRequestBlockerCommentsStreamAsync ["state"] = BitbucketHelpers.BlockerCommentStateToString(state), }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments"), queryParamValues, maxPages, cancellationToken); } /// @@ -433,7 +402,7 @@ public async Task ReopenPullRequestBlockerCommentAsync( /// A collection of blocker comments () on Bitbucket 9.0+, /// or legacy tasks () on older versions. /// - public async Task> GetPullRequestTasksWithFallbackAsync( + public async Task> GetPullRequestTasksWithFallbackAsync( string projectKey, string repositorySlug, long pullRequestId, @@ -450,7 +419,7 @@ public async Task> GetPullRequestTasksWithFallbackAsync( maxPages: maxPages, limit: limit, start: start, cancellationToken: cancellationToken).ConfigureAwait(false); - return blockerComments.Cast(); + return blockerComments.Cast().ToList(); } catch (BitbucketNotFoundException) { @@ -462,7 +431,7 @@ public async Task> GetPullRequestTasksWithFallbackAsync( cancellationToken: cancellationToken).ConfigureAwait(false); #pragma warning restore CS0618 - return tasks.Cast(); + return tasks.Cast().ToList(); } } diff --git a/src/Bitbucket.Net/Core/Projects/IBranchOperations.cs b/src/Bitbucket.Net/Core/Projects/IBranchOperations.cs new file mode 100644 index 0000000..fc3d564 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/IBranchOperations.cs @@ -0,0 +1,26 @@ +using Bitbucket.Net.Models.Branches; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; + +namespace Bitbucket.Net; + +/// +/// Branch operations. +/// +public interface IBranchOperations +{ + Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetRepoBranchModelAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task CreateRepoBranchAsync(string projectKey, string repositorySlug, string branchName, string startPoint, CancellationToken cancellationToken = default); + Task DeleteRepoBranchAsync(string projectKey, string repositorySlug, string branchName, bool dryRun, string? endPoint = null, CancellationToken cancellationToken = default); + Task> GetBranchesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, string? baseBranchOrTag = null, bool? details = null, string? filterText = null, BranchOrderBy? orderBy = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetBranchesStreamAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, string? baseBranchOrTag = null, bool? details = null, string? filterText = null, BranchOrderBy? orderBy = null, CancellationToken cancellationToken = default); + Task CreateBranchAsync(string projectKey, string repositorySlug, CreateBranchRequest request, CancellationToken cancellationToken = default); + Task GetDefaultBranchAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task SetDefaultBranchAsync(string projectKey, string repositorySlug, BranchRef branchRef, CancellationToken cancellationToken = default); + Task BrowseProjectRepositoryAsync(string projectKey, string repositorySlug, string at, bool type = false, bool blame = false, bool noContent = false, CancellationToken cancellationToken = default); + Task BrowseProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, string at, bool type = false, bool blame = false, bool noContent = false, CancellationToken cancellationToken = default); + Task GetRawFileContentStreamAsync(string projectKey, string repositorySlug, string path, string? at = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetRawFileContentLinesStreamAsync(string projectKey, string repositorySlug, string path, string? at = null, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, string fileName, string branch, string? message = null, string? sourceCommitId = null, string? sourceBranch = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/ICommitOperations.cs b/src/Bitbucket.Net/Core/Projects/ICommitOperations.cs new file mode 100644 index 0000000..0e360f0 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/ICommitOperations.cs @@ -0,0 +1,34 @@ +using Bitbucket.Net.Models.Core.Projects; + +namespace Bitbucket.Net; + +/// +/// Commit and compare operations. +/// +public interface ICommitOperations +{ + Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetChangesStreamAsync(string projectKey, string repositorySlug, string until, string? since = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetCommitsAsync(string projectKey, string repositorySlug, string until, bool followRenames = false, bool ignoreMissing = false, MergeCommits merges = MergeCommits.Exclude, string? path = null, string? since = null, bool withCounts = false, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetCommitsStreamAsync(string projectKey, string repositorySlug, string until, bool followRenames = false, bool ignoreMissing = false, MergeCommits merges = MergeCommits.Exclude, string? path = null, string? since = null, bool withCounts = false, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetCommitAsync(string projectKey, string repositorySlug, string commitId, string? path = null, CancellationToken cancellationToken = default); + Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, string? since = null, bool withComments = true, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, string repositorySlug, string commitId, string? since = null, bool withComments = true, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, string path, string? since = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, CommentInfo commentInfo, string? since = null, CancellationToken cancellationToken = default); + Task GetCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task UpdateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, CommentText commentText, CancellationToken cancellationToken = default); + Task DeleteCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, int version = -1, CancellationToken cancellationToken = default); + Task GetCommitDiffAsync(string projectKey, string repositorySlug, string commitId, bool autoSrcPath = false, int contextLines = -1, string? since = null, string? srcPath = null, string whitespace = "ignore-all", bool withComments = true, CancellationToken cancellationToken = default); + IAsyncEnumerable GetCommitDiffStreamAsync(string projectKey, string repositorySlug, string commitId, bool autoSrcPath = false, int contextLines = -1, string? since = null, string? srcPath = null, string whitespace = "ignore-all", bool withComments = true, CancellationToken cancellationToken = default); + Task CreateCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default); + Task DeleteCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default); + Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetRepositoryCompareDiffAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, string? srcPath = null, int contextLines = -1, string whitespace = "ignore-all", CancellationToken cancellationToken = default); + IAsyncEnumerable GetRepositoryCompareDiffStreamAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, string? srcPath = null, int contextLines = -1, string whitespace = "ignore-all", CancellationToken cancellationToken = default); + Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetRepositoryDiffAsync(string projectKey, string repositorySlug, string until, int contextLines = -1, string? since = null, string? srcPath = null, string whitespace = "ignore-all", CancellationToken cancellationToken = default); + IAsyncEnumerable GetRepositoryDiffStreamAsync(string projectKey, string repositorySlug, string until, int contextLines = -1, string? since = null, string? srcPath = null, string whitespace = "ignore-all", CancellationToken cancellationToken = default); + Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetProjectRepositoryLastModifiedAsync(string projectKey, string repositorySlug, string at, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/IProjectOperations.cs b/src/Bitbucket.Net/Core/Projects/IProjectOperations.cs new file mode 100644 index 0000000..7b6e102 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/IProjectOperations.cs @@ -0,0 +1,29 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; + +namespace Bitbucket.Net; + +/// +/// Project management operations. +/// +public interface IProjectOperations +{ + Task> GetProjectsAsync(int? maxPages = null, int? limit = null, int? start = null, string? name = null, Permissions? permission = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetProjectsStreamAsync(int? maxPages = null, int? limit = null, int? start = null, string? name = null, Permissions? permission = null, CancellationToken cancellationToken = default); + Task CreateProjectAsync(CreateProjectRequest request, CancellationToken cancellationToken = default); + Task DeleteProjectAsync(string projectKey, CancellationToken cancellationToken = default); + Task UpdateProjectAsync(string projectKey, UpdateProjectRequest request, CancellationToken cancellationToken = default); + Task GetProjectAsync(string projectKey, CancellationToken cancellationToken = default); + Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task DeleteProjectUserPermissionsAsync(string projectKey, string userName, CancellationToken cancellationToken = default); + Task UpdateProjectUserPermissionsAsync(string projectKey, string userName, Permissions permission, CancellationToken cancellationToken = default); + Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task DeleteProjectGroupPermissionsAsync(string projectKey, string groupName, CancellationToken cancellationToken = default); + Task UpdateProjectGroupPermissionsAsync(string projectKey, string groupName, Permissions permission, CancellationToken cancellationToken = default); + Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task IsProjectDefaultPermissionAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default); + Task GrantProjectPermissionToAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default); + Task RevokeProjectPermissionFromAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/IPullRequestOperations.cs b/src/Bitbucket.Net/Core/Projects/IPullRequestOperations.cs new file mode 100644 index 0000000..ed74d1b --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/IPullRequestOperations.cs @@ -0,0 +1,69 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; +using Bitbucket.Net.Models.Core.Tasks; +using Bitbucket.Net.Models.Core.Users; + +namespace Bitbucket.Net; + +/// +/// Pull request operations. +/// +public interface IPullRequestOperations +{ + Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, PullRequestDirections direction = PullRequestDirections.Incoming, string? filter = null, Roles? role = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetPullRequestsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, PullRequestDirections direction = PullRequestDirections.Incoming, string? branchId = null, PullRequestStates state = PullRequestStates.Open, PullRequestOrders order = PullRequestOrders.Newest, bool withAttributes = true, bool withProperties = true, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestsStreamAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, PullRequestDirections direction = PullRequestDirections.Incoming, string? branchId = null, PullRequestStates state = PullRequestStates.Open, PullRequestOrders order = PullRequestOrders.Newest, bool withAttributes = true, bool withProperties = true, CancellationToken cancellationToken = default); + Task CreatePullRequestAsync(string projectKey, string repositorySlug, CreatePullRequestRequest request, CancellationToken cancellationToken = default); + Task GetPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task UpdatePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, UpdatePullRequestRequest request, CancellationToken cancellationToken = default); + Task DeletePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, VersionInfo versionInfo, CancellationToken cancellationToken = default); + Task DeclinePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default); + Task GetPullRequestMergeStateAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default); + Task GetPullRequestMergeBaseAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task MergePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, MergePullRequestRequest? request = null, CancellationToken cancellationToken = default); + Task ReopenPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default); + Task ApprovePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task DeletePullRequestApprovalAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestParticipantsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task AssignUserRoleToPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, Named named, Roles role, CancellationToken cancellationToken = default); + Task DeletePullRequestParticipantAsync(string projectKey, string repositorySlug, long pullRequestId, string userName, CancellationToken cancellationToken = default); + Task UpdatePullRequestParticipantStatus(string projectKey, string repositorySlug, long pullRequestId, string userSlug, Named named, bool approved, ParticipantStatus participantStatus, CancellationToken cancellationToken = default); + Task UnassignUserFromPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, string userSlug, CancellationToken cancellationToken = default); + Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, long? fromId = null, PullRequestFromTypes? fromType = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestActivitiesStreamAsync(string projectKey, string repositorySlug, long pullRequestId, long? fromId = null, PullRequestFromTypes? fromType = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, ChangeScopes changeScope = ChangeScopes.All, string? sinceId = null, string? untilId = null, bool withComments = true, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestChangesStreamAsync(string projectKey, string repositorySlug, long pullRequestId, ChangeScopes changeScope = ChangeScopes.All, string? sinceId = null, string? untilId = null, bool withComments = true, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, bool withCounts = false, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestCommitsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, bool withCounts = false, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetPullRequestDiffAsync(string projectKey, string repositorySlug, long pullRequestId, int contextLines = -1, DiffTypes diffType = DiffTypes.Effective, string? sinceId = null, string? srcPath = null, string? untilId = null, string whitespace = "ignore-all", bool withComments = true, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestDiffStreamAsync(string projectKey, string repositorySlug, long pullRequestId, int contextLines = -1, DiffTypes diffType = DiffTypes.Effective, string? sinceId = null, string? srcPath = null, string? untilId = null, string whitespace = "ignore-all", bool withComments = true, CancellationToken cancellationToken = default); + Task GetPullRequestDiffPathAsync(string projectKey, string repositorySlug, long pullRequestId, string path, int contextLines = -1, DiffTypes diffType = DiffTypes.Effective, string? sinceId = null, string? srcPath = null, string? untilId = null, string whitespace = "ignore-all", bool withComments = true, CancellationToken cancellationToken = default); + Task CreatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, string text, string? parentId = null, DiffTypes? diffType = null, string? fromHash = null, string? path = null, string? srcPath = null, string? toHash = null, int? line = null, FileTypes? fileType = null, LineTypes? lineType = null, CancellationToken cancellationToken = default); + Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, string path, AnchorStates anchorState = AnchorStates.Active, DiffTypes diffType = DiffTypes.Effective, string? fromHash = null, string? toHash = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestCommentsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, string path, AnchorStates anchorState = AnchorStates.Active, DiffTypes diffType = DiffTypes.Effective, string? fromHash = null, string? toHash = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task GetPullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task UpdatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, int version, string text, CancellationToken cancellationToken = default); + Task DeletePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, int version = -1, CancellationToken cancellationToken = default); + + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync for 9.0+ or GetPullRequestTasksWithFallbackAsync for cross-version compatibility.")] + Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsStreamAsync for 9.0+ compatibility.")] + IAsyncEnumerable GetPullRequestTasksStreamAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync and count the results for 9.0+ compatibility.")] + Task GetPullRequestTaskCountAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + + Task> GetPullRequestBlockerCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, BlockerCommentState? state = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetPullRequestBlockerCommentsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, BlockerCommentState? state = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetPullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long blockerCommentId, CancellationToken cancellationToken = default); + Task CreatePullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, string text, CommentAnchor? anchor = null, CancellationToken cancellationToken = default); + Task UpdatePullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long blockerCommentId, string text, int version, CancellationToken cancellationToken = default); + Task DeletePullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long blockerCommentId, int version, CancellationToken cancellationToken = default); + Task ResolvePullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long blockerCommentId, int version, CancellationToken cancellationToken = default); + Task ReopenPullRequestBlockerCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long blockerCommentId, int version, CancellationToken cancellationToken = default); + Task> GetPullRequestTasksWithFallbackAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task WatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task UnwatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/IRepositoryOperations.cs b/src/Bitbucket.Net/Core/Projects/IRepositoryOperations.cs new file mode 100644 index 0000000..f4adb63 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/IRepositoryOperations.cs @@ -0,0 +1,57 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; +using Bitbucket.Net.Models.Core.Users; + +namespace Bitbucket.Net; + +/// +/// Repository management operations. +/// +public interface IRepositoryOperations +{ + Task> GetProjectRepositoriesAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetProjectRepositoriesStreamAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateProjectRepositoryAsync(string projectKey, CreateRepositoryRequest request, CancellationToken cancellationToken = default); + Task GetProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task CreateProjectRepositoryForkAsync(string projectKey, string repositorySlug, ForkRepositoryRequest? request = null, CancellationToken cancellationToken = default); + Task ScheduleProjectRepositoryForDeletionAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryAsync(string projectKey, string repositorySlug, string? targetName = null, bool? isForkable = null, string? targetProjectKey = null, bool? isPublic = null, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task RecreateProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetProjectRepositoryArchiveAsync(string projectKey, string repositorySlug, string at, string fileName, ArchiveFormats archiveFormat, string path, string prefix, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default); + Task DeleteProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string name, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default); + Task DeleteProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string name, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task RetrieveRawContentAsync(string projectKey, string repositorySlug, string path, string? at = null, bool markup = false, bool hardWrap = true, bool htmlEscape = true, CancellationToken cancellationToken = default); + Task GetProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, PullRequestSettings pullRequestSettings, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, HookTypes? hookType = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task GetProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default); + Task DeleteProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default); + Task EnableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, object? hookSettings = null, CancellationToken cancellationToken = default); + Task DisableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default); + Task> UpdateProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, Dictionary allSettings, CancellationToken cancellationToken = default); + Task GetProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, CancellationToken cancellationToken = default); + Task UpdateProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, string filterText, BranchOrderBy orderBy, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetProjectRepositoryTagsStreamAsync(string projectKey, string repositorySlug, string filterText, BranchOrderBy orderBy, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateProjectRepositoryTagAsync(string projectKey, string repositorySlug, string name, string startPoint, string message, CancellationToken cancellationToken = default); + Task GetProjectRepositoryTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, string? @event = null, bool statistics = false, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, CreateWebHookRequest request, CancellationToken cancellationToken = default); + Task TestProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string url, CancellationToken cancellationToken = default); + Task GetProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string webHookId, bool statistics = false, CancellationToken cancellationToken = default); + Task UpdateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string webHookId, UpdateWebHookRequest request, CancellationToken cancellationToken = default); + Task DeleteProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string webHookId, CancellationToken cancellationToken = default); + Task GetProjectRepositoryWebHookLatestAsync(string projectKey, string repositorySlug, string webHookId, string? @event = null, WebHookOutcomes? outcome = null, CancellationToken cancellationToken = default); + Task GetProjectRepositoryWebHookStatisticsAsync(string projectKey, string repositorySlug, string webHookId, string? @event = null, CancellationToken cancellationToken = default); + Task> GetProjectRepositoryWebHookStatisticsSummaryAsync(string projectKey, string repositorySlug, string webHookId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs index e5e7e5e..3e50939 100644 --- a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; @@ -30,7 +29,7 @@ private IFlurlRequest GetReposUrl() => GetBaseUrl() /// Whether to include only public repositories. /// Token to cancel the operation. /// A collection of repositories. - public async Task> GetRepositoriesAsync( + public Task> GetRepositoriesAsync( int? maxPages = null, int? limit = null, int? start = null, @@ -50,16 +49,8 @@ public async Task> GetRepositoriesAsync( ["visibility"] = isPublic ? "public" : "private", }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetReposUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetReposUrl(), queryParamValues, maxPages, cancellationToken); } /// @@ -94,14 +85,7 @@ public IAsyncEnumerable GetRepositoriesStreamAsync( ["visibility"] = isPublic ? "public" : "private", }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetReposUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken); + return GetPagedStreamAsync( + GetReposUrl(), queryParamValues, maxPages, cancellationToken); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs b/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs index 839636e..5f9cefb 100644 --- a/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs @@ -1,4 +1,5 @@ using Bitbucket.Net.Common; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Models.Core.Tasks; using Flurl.Http; @@ -27,13 +28,15 @@ private IFlurlRequest GetTasksUrl(string path) => GetTasksUrl() /// /// Creates a task. /// - /// The task information. + /// The create task request. /// Token to cancel the operation. /// The created task. - public async Task CreateTaskAsync(TaskInfo taskInfo, CancellationToken cancellationToken = default) + public async Task CreateTaskAsync(CreateTaskRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var response = await GetTasksUrl() - .SendAsync(HttpMethod.Post, CreateJsonContent(taskInfo), cancellationToken: cancellationToken) + .SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken) .ConfigureAwait(false); return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -57,18 +60,21 @@ public async Task GetTaskAsync(long taskId, int? avatarSize = nul } /// - /// Updates a task's text. + /// Updates a task. /// /// The task identifier. - /// The updated task text. + /// The update task request. /// Token to cancel the operation. /// The updated task. - public async Task UpdateTaskAsync(long taskId, string text, CancellationToken cancellationToken = default) + public async Task UpdateTaskAsync(long taskId, UpdateTaskRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); + var obj = new { id = taskId, - text, + text = request.Text, + state = request.State, }; var response = await GetTasksUrl($"/{taskId}") diff --git a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs index b301b4d..f0070d4 100644 --- a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; @@ -38,7 +37,7 @@ private IFlurlRequest GetUsersUrl(string path) => GetUsersUrl() /// Token to cancel the operation. /// Additional permission filters. /// A collection of users. - public async Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, + public Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, int? maxPages = null, int? limit = null, int? start = null, @@ -63,16 +62,8 @@ public async Task> GetUsersAsync(string? filter = null, string queryParamValues.Add($"permission.{permissionNCounter}", perm); } - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetUsersUrl() - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetUsersUrl(), queryParamValues, maxPages, cancellationToken); } /// @@ -121,6 +112,8 @@ public async Task UpdateUserCredentialsAsync(PasswordChange passwordChange /// The requested user. public async Task GetUserAsync(string userSlug, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetUsersUrl($"/{userSlug}") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken) @@ -137,6 +130,8 @@ public async Task GetUserAsync(string userSlug, int? avatarSize = null, Ca /// true if the avatar was deleted; otherwise, false. public async Task DeleteUserAvatarAsync(string userSlug, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetUsersUrl($"/{userSlug}/avatar.png") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -152,6 +147,8 @@ public async Task DeleteUserAvatarAsync(string userSlug, CancellationToken /// A dictionary of user settings. public async Task> GetUserSettingsAsync(string userSlug, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetUsersUrl($"/{userSlug}/settings") .GetAsync(cancellationToken) .ConfigureAwait(false); @@ -168,6 +165,8 @@ public async Task DeleteUserAvatarAsync(string userSlug, CancellationToken /// true if the settings were updated; otherwise, false. public async Task UpdateUserSettingsAsync(string userSlug, IDictionary userSettings, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetUsersUrl($"/{userSlug}/settings") .SendAsync(HttpMethod.Post, CreateJsonContent(userSettings), cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs index 5af1890..bfb17e2 100644 --- a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs +++ b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs @@ -30,15 +30,18 @@ private IFlurlRequest GetDefaultReviewersUrl(string path) => GetDefaultReviewers /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of default reviewer conditions. - public async Task> GetDefaultReviewerConditionsAsync(string projectKey, + public async Task> GetDefaultReviewerConditionsAsync(string projectKey, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } /// @@ -50,6 +53,8 @@ public async Task> GetDefaultRe /// The created default reviewer condition. public async Task CreateDefaultReviewerConditionAsync(string projectKey, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") .SendAsync(HttpMethod.Post, CreateJsonContent(condition), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -67,6 +72,9 @@ public async Task CreateDefaultReviewerCond /// The updated default reviewer condition. public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(defaultReviewerPullRequestConditionId); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") .SendAsync(HttpMethod.Put, CreateJsonContent(condition), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -83,6 +91,9 @@ public async Task UpdateDefaultReviewerCond /// true if the condition was deleted; otherwise, false. public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(defaultReviewerPullRequestConditionId); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -98,15 +109,19 @@ public async Task DeleteDefaultReviewerConditionAsync(string projectKey, s /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of default reviewer conditions. - public async Task> GetDefaultReviewerConditionsAsync(string projectKey, string repositorySlug, + public async Task> GetDefaultReviewerConditionsAsync(string projectKey, string repositorySlug, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } /// @@ -119,6 +134,9 @@ public async Task> GetDefaultRe /// The created default reviewer condition. public async Task CreateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") .SendAsync(HttpMethod.Post, CreateJsonContent(condition), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -137,6 +155,10 @@ public async Task CreateDefaultReviewerCond /// The updated default reviewer condition. public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(defaultReviewerPullRequestConditionId); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") .SendAsync(HttpMethod.Put, CreateJsonContent(condition), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -154,6 +176,10 @@ public async Task UpdateDefaultReviewerCond /// true if the condition was deleted; otherwise, false. public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(defaultReviewerPullRequestConditionId); + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -173,7 +199,7 @@ public async Task DeleteDefaultReviewerConditionAsync(string projectKey, s /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of default reviewers. - public async Task> GetDefaultReviewersAsync(string projectKey, string repositorySlug, + public async Task> GetDefaultReviewersAsync(string projectKey, string repositorySlug, int? sourceRepoId = null, int? targetRepoId = null, string? sourceRefId = null, @@ -181,6 +207,9 @@ public async Task> GetDefaultReviewersAsync(string projectKey, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["sourceRepoId"] = sourceRepoId, @@ -195,6 +224,7 @@ public async Task> GetDefaultReviewersAsync(string projectKey, .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Git/BitbucketClient.cs b/src/Bitbucket.Net/Git/BitbucketClient.cs index 3e6a7b1..2316fe1 100644 --- a/src/Bitbucket.Net/Git/BitbucketClient.cs +++ b/src/Bitbucket.Net/Git/BitbucketClient.cs @@ -34,6 +34,9 @@ private IFlurlRequest GetGitUrl(string path) => GetGitUrl() /// The rebase eligibility details. public async Task GetCanRebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -52,6 +55,9 @@ public async Task GetCanRebasePullRequestAsync(strin /// The updated pull request. public async Task RebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var data = new { version }; var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) @@ -72,6 +78,11 @@ public async Task RebasePullRequestAsync(string projectKey, string /// The created tag. public async Task CreateTagAsync(string projectKey, string repositorySlug, TagTypes tagType, string tagName, string startPoint, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(tagName); + ArgumentException.ThrowIfNullOrWhiteSpace(startPoint); + var data = new { type = BitbucketHelpers.TagTypeToString(tagType), @@ -96,6 +107,10 @@ public async Task CreateTagAsync(string projectKey, string repositorySlug, /// true if the tag was deleted; otherwise, false. public async Task DeleteTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(tagName); + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/tags/{tagName}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Git/IGitOperations.cs b/src/Bitbucket.Net/Git/IGitOperations.cs new file mode 100644 index 0000000..3f3b087 --- /dev/null +++ b/src/Bitbucket.Net/Git/IGitOperations.cs @@ -0,0 +1,15 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Git; + +namespace Bitbucket.Net; + +/// +/// Git-specific operations. +/// +public interface IGitOperations +{ + Task GetCanRebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + Task RebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version, CancellationToken cancellationToken = default); + Task CreateTagAsync(string projectKey, string repositorySlug, TagTypes tagType, string tagName, string startPoint, CancellationToken cancellationToken = default); + Task DeleteTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/IBitbucketClient.cs b/src/Bitbucket.Net/IBitbucketClient.cs new file mode 100644 index 0000000..3953d9e --- /dev/null +++ b/src/Bitbucket.Net/IBitbucketClient.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net; + +/// +/// Abstraction over the Bitbucket Server REST API client, enabling +/// dependency injection, unit testing with mocks, and decorator patterns. +/// +public interface IBitbucketClient : + IProjectOperations, + IRepositoryOperations, + IPullRequestOperations, + ICommitOperations, + IBranchOperations, + IAdminOperations, + ISshOperations, + IRefRestrictionOperations, + IBuildOperations, + IGitOperations, + ISearchOperations, + IBitbucketMiscOperations, + IDisposable +{ +} \ No newline at end of file diff --git a/src/Bitbucket.Net/IBitbucketMiscOperations.cs b/src/Bitbucket.Net/IBitbucketMiscOperations.cs new file mode 100644 index 0000000..2d670e3 --- /dev/null +++ b/src/Bitbucket.Net/IBitbucketMiscOperations.cs @@ -0,0 +1,127 @@ +using Bitbucket.Net.Builders; +using Bitbucket.Net.Models.Audit; +using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Logs; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; +using Bitbucket.Net.Models.Core.Tasks; +using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.DefaultReviewers; +using Bitbucket.Net.Models.Jira; +using Bitbucket.Net.Models.PersonalAccessTokens; + +namespace Bitbucket.Net; + +/// +/// Miscellaneous operations not covered by domain-specific interfaces. +/// +public interface IBitbucketMiscOperations +{ + // ── Audit ──────────────────────────────────────────────────────── + + Task> GetProjectAuditEventsAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + + // ── Builders ───────────────────────────────────────────────────── + + PullRequestQueryBuilder PullRequests(string projectKey, string repositorySlug); + CommitQueryBuilder Commits(string projectKey, string repositorySlug, string until); + BranchQueryBuilder Branches(string projectKey, string repositorySlug); + ProjectQueryBuilder Projects(); + + // ── Comment Likes ──────────────────────────────────────────────── + + Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task LikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default); + Task UnlikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default); + Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task LikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default); + Task UnlikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default); + + // ── Default Reviewers ──────────────────────────────────────────── + + Task> GetDefaultReviewerConditionsAsync(string projectKey, int? avatarSize = null, CancellationToken cancellationToken = default); + Task CreateDefaultReviewerConditionAsync(string projectKey, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default); + Task UpdateDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default); + Task DeleteDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default); + Task> GetDefaultReviewerConditionsAsync(string projectKey, string repositorySlug, int? avatarSize = null, CancellationToken cancellationToken = default); + Task CreateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default); + Task UpdateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default); + Task DeleteDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default); + Task> GetDefaultReviewersAsync(string projectKey, string repositorySlug, int? sourceRepoId = null, int? targetRepoId = null, string? sourceRefId = null, string? targetRefId = null, int? avatarSize = null, CancellationToken cancellationToken = default); + + // ── Jira ───────────────────────────────────────────────────────── + + Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateJiraIssueAsync(string pullRequestCommentId, string applicationId, string title, string type, CancellationToken cancellationToken = default); + Task> GetJiraIssuesAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default); + + // ── Personal Access Tokens ─────────────────────────────────────── + + Task> GetUserAccessTokensAsync(string userSlug, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task CreateAccessTokenAsync(string userSlug, AccessTokenCreate accessToken, CancellationToken cancellationToken = default); + Task GetUserAccessTokenAsync(string userSlug, string tokenId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task ChangeUserAccessTokenAsync(string userSlug, string tokenId, AccessTokenCreate accessToken, CancellationToken cancellationToken = default); + Task DeleteUserAccessTokenAsync(string userSlug, string tokenId, CancellationToken cancellationToken = default); + + // ── Application Properties ─────────────────────────────────────── + + Task> GetApplicationPropertiesAsync(CancellationToken cancellationToken = default); + + // ── Dashboard ──────────────────────────────────────────────────── + + Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, Roles? role = null, List? status = null, PullRequestOrders? order = PullRequestOrders.Newest, int? closedSinceSeconds = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetDashboardPullRequestsStreamAsync(PullRequestStates? state = null, Roles? role = null, List? status = null, PullRequestOrders? order = PullRequestOrders.Newest, int? closedSinceSeconds = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, int? maxPages = null, int? limit = 3, int? start = null, CancellationToken cancellationToken = default); + + // ── Groups ─────────────────────────────────────────────────────── + + Task> GetGroupNamesAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + + // ── Hooks ──────────────────────────────────────────────────────── + + Task GetProjectHooksAvatarAsync(string hookKey, string? version = null, CancellationToken cancellationToken = default); + + // ── Inbox ──────────────────────────────────────────────────────── + + Task> GetInboxPullRequestsAsync(int? maxPages = null, int? limit = 25, int? start = 0, Roles role = Roles.Reviewer, CancellationToken cancellationToken = default); + IAsyncEnumerable GetInboxPullRequestsStreamAsync(int? maxPages = null, int? limit = 25, int? start = 0, Roles role = Roles.Reviewer, CancellationToken cancellationToken = default); + Task GetInboxPullRequestsCountAsync(CancellationToken cancellationToken = default); + + // ── Logs ───────────────────────────────────────────────────────── + + Task GetLogLevelAsync(string loggerName, CancellationToken cancellationToken = default); + Task SetLogLevelAsync(string loggerName, LogLevels logLevel, CancellationToken cancellationToken = default); + Task GetRootLogLevelAsync(CancellationToken cancellationToken = default); + Task SetRootLogLevelAsync(LogLevels logLevel, CancellationToken cancellationToken = default); + + // ── Markup ─────────────────────────────────────────────────────── + + Task PreviewMarkupAsync(string text, string? urlMode = null, bool? hardWrap = null, bool? htmlEscape = null, CancellationToken cancellationToken = default); + + // ── Profile ────────────────────────────────────────────────────── + + Task> GetRecentReposAsync(Permissions? permission = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + + // ── Tasks (global) ─────────────────────────────────────────────── + + Task CreateTaskAsync(CreateTaskRequest request, CancellationToken cancellationToken = default); + Task GetTaskAsync(long taskId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task UpdateTaskAsync(long taskId, UpdateTaskRequest request, CancellationToken cancellationToken = default); + Task DeleteTaskAsync(long taskId, CancellationToken cancellationToken = default); + + // ── Users ──────────────────────────────────────────────────────── + + Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default, params string[] permissionN); + Task UpdateUserAsync(string? email = null, string? displayName = null, CancellationToken cancellationToken = default); + Task UpdateUserCredentialsAsync(Models.Core.Users.PasswordChange passwordChange, CancellationToken cancellationToken = default); + Task GetUserAsync(string userSlug, int? avatarSize = null, CancellationToken cancellationToken = default); + Task DeleteUserAvatarAsync(string userSlug, CancellationToken cancellationToken = default); + Task> GetUserSettingsAsync(string userSlug, CancellationToken cancellationToken = default); + Task UpdateUserSettingsAsync(string userSlug, IDictionary userSettings, CancellationToken cancellationToken = default); + + // ── WhoAmI ─────────────────────────────────────────────────────── + + Task GetWhoAmIAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Jira/BitbucketClient.cs b/src/Bitbucket.Net/Jira/BitbucketClient.cs index 21f7dec..3c38fd5 100644 --- a/src/Bitbucket.Net/Jira/BitbucketClient.cs +++ b/src/Bitbucket.Net/Jira/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Models.Jira; using Flurl.Http; @@ -35,12 +34,14 @@ private IFlurlRequest GetJiraUrl(string path) => GetJiraUrl() /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of changesets. - public async Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, + public Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(issueKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -48,16 +49,8 @@ public async Task> GetChangeSetsAsync(string issueKey, in ["maxChanges"] = maxChanges, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetJiraUrl($"/issues/{issueKey}/commits") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetJiraUrl($"/issues/{issueKey}/commits"), queryParamValues, maxPages, cancellationToken); } /// @@ -71,6 +64,11 @@ public async Task> GetChangeSetsAsync(string issueKey, in /// The created Jira issue. public async Task CreateJiraIssueAsync(string pullRequestCommentId, string applicationId, string title, string type, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(pullRequestCommentId); + ArgumentException.ThrowIfNullOrWhiteSpace(applicationId); + ArgumentException.ThrowIfNullOrWhiteSpace(title); + ArgumentException.ThrowIfNullOrWhiteSpace(type); + var data = new { id = "https://docs.atlassian.com/jira/REST/schema/string#", @@ -94,12 +92,16 @@ public async Task CreateJiraIssueAsync(string pullRequestCommentId, s /// The pull request identifier. /// Token to cancel the operation. /// A collection of Jira issue links. - public async Task> GetJiraIssuesAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + public async Task> GetJiraIssuesAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetJiraUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/issues") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Audit/AuditEvent.cs b/src/Bitbucket.Net/Models/Audit/AuditEvent.cs index 30c925c..a3bd5ca 100644 --- a/src/Bitbucket.Net/Models/Audit/AuditEvent.cs +++ b/src/Bitbucket.Net/Models/Audit/AuditEvent.cs @@ -1,11 +1,11 @@ -using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.Core.Users; namespace Bitbucket.Net.Models.Audit; public class AuditEvent { - public string? Action { get; set; } - public long Timestamp { get; set; } - public string? Details { get; set; } - public User? User { get; set; } + public string? Action { get; init; } + public long Timestamp { get; init; } + public string? Details { get; init; } + public User? User { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Branches/BranchModel.cs b/src/Bitbucket.Net/Models/Branches/BranchModel.cs index 737cd00..7aa5488 100644 --- a/src/Bitbucket.Net/Models/Branches/BranchModel.cs +++ b/src/Bitbucket.Net/Models/Branches/BranchModel.cs @@ -1,10 +1,10 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.Branches; public class BranchModel { - public Branch? Development { get; set; } - public Branch? Production { get; set; } - public List? Types { get; set; } + public Branch? Development { get; init; } + public Branch? Production { get; init; } + public List? Types { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Branches/BranchModelType.cs b/src/Bitbucket.Net/Models/Branches/BranchModelType.cs index c894ab4..aa790d3 100644 --- a/src/Bitbucket.Net/Models/Branches/BranchModelType.cs +++ b/src/Bitbucket.Net/Models/Branches/BranchModelType.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Branches; +namespace Bitbucket.Net.Models.Branches; public class BranchModelType { - public string? Id { get; set; } - public string? DisplayName { get; set; } - public string? Prefix { get; set; } + public string? Id { get; init; } + public string? DisplayName { get; init; } + public string? Prefix { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Builds/BuildStats.cs b/src/Bitbucket.Net/Models/Builds/BuildStats.cs index 66439ae..79b6937 100644 --- a/src/Bitbucket.Net/Models/Builds/BuildStats.cs +++ b/src/Bitbucket.Net/Models/Builds/BuildStats.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Builds; +namespace Bitbucket.Net.Models.Builds; public class BuildStats { - public int Successful { get; set; } - public int InProgress { get; set; } - public int Failed { get; set; } + public int Successful { get; init; } + public int InProgress { get; init; } + public int Failed { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Builds/Requests/AssociateBuildStatusRequest.cs b/src/Bitbucket.Net/Models/Builds/Requests/AssociateBuildStatusRequest.cs new file mode 100644 index 0000000..a1848c3 --- /dev/null +++ b/src/Bitbucket.Net/Models/Builds/Requests/AssociateBuildStatusRequest.cs @@ -0,0 +1,32 @@ +namespace Bitbucket.Net.Models.Builds.Requests; + +/// +/// Request body for associating a build status with a commit. +/// +public sealed class AssociateBuildStatusRequest +{ + /// + /// The build state (e.g. "SUCCESSFUL", "FAILED", "INPROGRESS"). Required. + /// + public required string State { get; init; } + + /// + /// A unique key identifying the build (e.g. build plan ID). Required. + /// + public required string Key { get; init; } + + /// + /// The URL linking back to the build result in the CI system. Required. + /// + public required string Url { get; init; } + + /// + /// An optional human-readable build name. + /// + public string? Name { get; init; } + + /// + /// An optional build description. + /// + public string? Description { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Address.cs b/src/Bitbucket.Net/Models/Core/Admin/Address.cs index 822cc0a..49a1b02 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Address.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Address.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin; +namespace Bitbucket.Net.Models.Core.Admin; public class Address { - public string? HostName { get; set; } - public int Port { get; set; } + public string? HostName { get; init; } + public int Port { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs b/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs index 531f9cd..0622d3e 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Admin; +namespace Bitbucket.Net.Models.Core.Admin; public class Cluster { - public Node? LocalNode { get; set; } - public List? Nodes { get; set; } - public bool Running { get; set; } + public Node? LocalNode { get; init; } + public List? Nodes { get; init; } + public bool Running { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs b/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs index c2b72f2..0613b70 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs @@ -1,8 +1,8 @@ -using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.Core.Users; namespace Bitbucket.Net.Models.Core.Admin; public class DeletableGroupOrUser : Named { - public bool Deletable { get; set; } + public bool Deletable { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs index 6b83bdd..63f908b 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs @@ -1,14 +1,11 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.Core.Admin; public class GroupPermission { - public Named? Group { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } + public Named? Group { get; init; } + public Permissions Permission { get; init; } public override string ToString() => $"{Permission} - {Group}"; } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs b/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs index d50ee1f..7d5987e 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs @@ -6,21 +6,21 @@ namespace Bitbucket.Net.Models.Core.Admin; public class LicenseDetails : LicenseInfo { [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreationDate { get; set; } + public DateTimeOffset? CreationDate { get; init; } [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? PurchaseDate { get; set; } + public DateTimeOffset? PurchaseDate { get; init; } [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? ExpiryDate { get; set; } - public int NumberOfDaysBeforeExpiry { get; set; } + public DateTimeOffset? ExpiryDate { get; init; } + public int NumberOfDaysBeforeExpiry { get; init; } [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? MaintenanceExpiryDate { get; set; } - public int NumberOfDaysBeforeMaintenanceExpiry { get; set; } + public DateTimeOffset? MaintenanceExpiryDate { get; init; } + public int NumberOfDaysBeforeMaintenanceExpiry { get; init; } [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? GracePeriodEndDate { get; set; } - public int NumberOfDaysBeforeGracePeriodExpiry { get; set; } - public int MaximumNumberOfUsers { get; set; } - public bool UnlimitedNumberOfUsers { get; set; } - public string? ServerId { get; set; } - public string? SupportEntitlementNumber { get; set; } - public LicenseStatus? Status { get; set; } + public DateTimeOffset? GracePeriodEndDate { get; init; } + public int NumberOfDaysBeforeGracePeriodExpiry { get; init; } + public int MaximumNumberOfUsers { get; init; } + public bool UnlimitedNumberOfUsers { get; init; } + public string? ServerId { get; init; } + public string? SupportEntitlementNumber { get; init; } + public LicenseStatus? Status { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs b/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs index 4fb5606..da6a4a2 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin; +namespace Bitbucket.Net.Models.Core.Admin; public class LicenseStatus { - public string? ServerId { get; set; } - public int CurrentNumberOfUsers { get; set; } + public string? ServerId { get; init; } + public int CurrentNumberOfUsers { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs index 1b33f34..e6a973d 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs @@ -1,10 +1,10 @@ -namespace Bitbucket.Net.Models.Core.Admin; +namespace Bitbucket.Net.Models.Core.Admin; public class MergeStrategy { - public string? Description { get; set; } - public bool Enabled { get; set; } - public string? Flag { get; set; } - public string? Id { get; set; } - public string? Name { get; set; } + public string? Description { get; init; } + public bool Enabled { get; init; } + public string? Flag { get; init; } + public string? Id { get; init; } + public string? Name { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Node.cs b/src/Bitbucket.Net/Models/Core/Admin/Node.cs index df4e021..ccb63f1 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Node.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Node.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Admin; +namespace Bitbucket.Net.Models.Core.Admin; public class Node { - public string? Id { get; set; } - public string? Name { get; set; } - public Address? Address { get; set; } - public bool Local { get; set; } + public string? Id { get; init; } + public string? Name { get; init; } + public Address? Address { get; init; } + public bool Local { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs b/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs index c86c105..475751f 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs @@ -1,12 +1,12 @@ -using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.Core.Users; namespace Bitbucket.Net.Models.Core.Admin; public class UserInfo : User { - public string? DirectoryName { get; set; } - public bool Deletable { get; set; } - public long LastAuthenticationTimestamp { get; set; } - public bool MutableDetails { get; set; } - public bool MutableGroups { get; set; } + public string? DirectoryName { get; init; } + public bool Deletable { get; init; } + public long LastAuthenticationTimestamp { get; init; } + public bool MutableDetails { get; init; } + public bool MutableGroups { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs index 652e037..e612b3e 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs @@ -1,14 +1,11 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.Core.Admin; public class UserPermission { - public User? User { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } + public User? User { get; init; } + public Permissions Permission { get; init; } public override string ToString() => $"{Permission} - {User?.DisplayName}"; } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs b/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs index 05cf78c..ee115e3 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class AheadBehindMetaData { - public int Ahead { get; set; } - public int Behind { get; set; } + public int Ahead { get; init; } + public int Behind { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Author.cs b/src/Bitbucket.Net/Models/Core/Projects/Author.cs index 630e4ad..eba1e60 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Author.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Author.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Author { - public string? Name { get; set; } - public string? EmailAddress { get; set; } + public string? Name { get; init; } + public string? EmailAddress { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs index 3fca691..757e648 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs @@ -25,76 +25,74 @@ public class BlockerComment /// /// The unique identifier of the blocker comment. /// - public int Id { get; set; } + public int Id { get; init; } /// /// The version of the blocker comment, used for optimistic locking. /// - public int Version { get; set; } + public int Version { get; init; } /// /// The text content of the blocker comment. /// - public string Text { get; set; } = string.Empty; + public string Text { get; init; } = string.Empty; /// /// The user who created the blocker comment. /// - public User? Author { get; set; } + public User? Author { get; init; } /// /// When the blocker comment was created. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// When the blocker comment was last updated. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; init; } /// /// The severity level of the comment. For blocker comments, this is always . /// - [JsonConverter(typeof(CommentSeverityConverter))] - public CommentSeverity Severity { get; set; } = CommentSeverity.Blocker; + public CommentSeverity Severity { get; init; } = CommentSeverity.Blocker; /// /// The state of the blocker comment. /// - [JsonConverter(typeof(BlockerCommentStateConverter))] - public BlockerCommentState State { get; set; } = BlockerCommentState.Open; + public BlockerCommentState State { get; init; } = BlockerCommentState.Open; /// /// The user who resolved the blocker comment, if resolved. /// - public User? Resolver { get; set; } + public User? Resolver { get; init; } /// /// When the blocker comment was resolved. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? ResolvedDate { get; set; } + public DateTimeOffset? ResolvedDate { get; init; } /// /// The anchor point for the comment (file, line number, etc.). /// Null for general pull request-level blocker comments. /// - public CommentAnchor? Anchor { get; set; } + public CommentAnchor? Anchor { get; init; } /// /// The parent comment this blocker is attached to, if any. /// - public CommentRef? Parent { get; set; } + public CommentRef? Parent { get; init; } /// /// The permitted operations the current user can perform on this blocker comment. /// - public Permittedoperations? PermittedOperations { get; set; } + public Permittedoperations? PermittedOperations { get; init; } /// /// Additional properties associated with the blocker comment. /// - public Properties? Properties { get; set; } + public Properties? Properties { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Branch.cs b/src/Bitbucket.Net/Models/Core/Projects/Branch.cs index 904400a..b5803a4 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Branch.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Branch.cs @@ -18,17 +18,17 @@ public class Branch : BranchBase /// /// Gets or sets the SHA of the latest commit on this branch. /// - public string? LatestCommit { get; set; } + public string? LatestCommit { get; init; } /// /// Gets or sets the changeset identifier of the latest change on this branch. /// - public string? LatestChangeset { get; set; } + public string? LatestChangeset { get; init; } /// /// Gets or sets a value indicating whether this is the repository's default branch. /// - public bool IsDefault { get; set; } + public bool IsDefault { get; init; } /// /// Gets parsed branch metadata (ahead/behind counts, build status, outgoing pull requests) from the raw JSON. @@ -47,7 +47,9 @@ public BranchMetaData? BranchMetadata return null; } - _branchMetadata = new BranchMetaData(); + AheadBehindMetaData? aheadBehind = null; + BuildStatusMetadata? buildStatus = null; + PullRequestMetadata? outgoingPullRequest = null; foreach (var metadata in Metadata.Value.EnumerateArray()) { @@ -66,18 +68,25 @@ public BranchMetaData? BranchMetadata if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider", StringComparison.Ordinal)) { - _branchMetadata.AheadBehind = JsonSerializer.Deserialize(valueJson, s_jsonOptions); + aheadBehind = JsonSerializer.Deserialize(valueJson, s_jsonOptions); } else if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata", StringComparison.Ordinal)) { - _branchMetadata.BuildStatus = JsonSerializer.Deserialize(valueJson, s_jsonOptions); + buildStatus = JsonSerializer.Deserialize(valueJson, s_jsonOptions); } else if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata", StringComparison.Ordinal)) { - _branchMetadata.OutgoingPullRequest = JsonSerializer.Deserialize(valueJson, s_jsonOptions); + outgoingPullRequest = JsonSerializer.Deserialize(valueJson, s_jsonOptions); } } + _branchMetadata = new BranchMetaData + { + AheadBehind = aheadBehind, + BuildStatus = buildStatus, + OutgoingPullRequest = outgoingPullRequest, + }; + return _branchMetadata; } } @@ -86,7 +95,7 @@ public BranchMetaData? BranchMetadata /// Gets or sets the raw JSON metadata array returned by Bitbucket Server for this branch. /// [JsonPropertyName("metadata")] - public JsonElement? Metadata { get; set; } + public JsonElement? Metadata { get; init; } /// /// Returns the branch display identifier. diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs index 55577e2..269831e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Base branch reference. Extends with a display identifier and ref type. @@ -8,10 +8,10 @@ public class BranchBase : WithId /// /// Gets or sets the short display name of the branch (e.g. "main"). /// - public string? DisplayId { get; set; } + public string? DisplayId { get; init; } /// /// Gets or sets the ref type (e.g. "BRANCH" or "TAG"). /// - public string? Type { get; set; } + public string? Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs index 930379c..f91f62c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs @@ -5,11 +5,11 @@ namespace Bitbucket.Net.Models.Core.Projects; public class BranchMetaData { [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider")] - public AheadBehindMetaData? AheadBehind { get; set; } + public AheadBehindMetaData? AheadBehind { get; init; } [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata")] - public BuildStatusMetadata? BuildStatus { get; set; } + public BuildStatusMetadata? BuildStatus { get; init; } [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata")] - public PullRequestMetadata? OutgoingPullRequest { get; set; } + public PullRequestMetadata? OutgoingPullRequest { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs b/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs index 37143e4..bea78dc 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs @@ -1,10 +1,10 @@ -using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Common.Models; namespace Bitbucket.Net.Models.Core.Projects; public class BrowseItem { - public Path? Path { get; set; } - public string? Revision { get; set; } - public PagedResults? Children { get; set; } + public Path? Path { get; init; } + public string? Revision { get; init; } + public PagedResults? Children { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs b/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs index a7a8781..dff7487 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs @@ -1,8 +1,8 @@ -using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Common.Models; namespace Bitbucket.Net.Models.Core.Projects; public class BrowsePathItem : PagedResultsBase { - public List? Lines { get; set; } + public List? Lines { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs b/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs index cc10510..8703ede 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class BuildStatusMetadata { - public int Successful { get; set; } - public int InProgress { get; set; } - public int Failed { get; set; } + public int Successful { get; init; } + public int InProgress { get; init; } + public int Failed { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Change.cs b/src/Bitbucket.Net/Models/Core/Projects/Change.cs index a95b41b..c3ccf32 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Change.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Change.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Represents a file change in a Bitbucket commit or pull request. @@ -8,50 +8,50 @@ public class Change /// /// Gets or sets the content hash of the file after the change. /// - public string? ContentId { get; set; } + public string? ContentId { get; init; } /// /// Gets or sets the content hash of the file before the change. /// - public string? FromContentId { get; set; } + public string? FromContentId { get; init; } /// /// Gets or sets the file path after the change. /// - public Path? Path { get; set; } + public Path? Path { get; init; } /// /// Gets or sets a value indicating whether the file is executable after the change. /// - public bool Executable { get; set; } + public bool Executable { get; init; } /// /// Gets or sets the percentage of the file that is unchanged. /// - public int PercentUnchanged { get; set; } + public int PercentUnchanged { get; init; } /// /// Gets or sets the change type (e.g. "ADD", "MODIFY", "DELETE", "MOVE", "COPY"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the node type (e.g. "FILE" or "DIRECTORY"). /// - public string? NodeType { get; set; } + public string? NodeType { get; init; } /// /// Gets or sets the original file path before a move or copy. /// - public Path? SrcPath { get; set; } + public Path? SrcPath { get; init; } /// /// Gets or sets a value indicating whether the file was executable before the change. /// - public bool SrcExecutable { get; set; } + public bool SrcExecutable { get; init; } /// /// Gets or sets the hypermedia links for this change. /// - public Links? Links { get; set; } + public Links? Links { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs b/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs index cbd3129..2da637c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class CloneLink : Link { - public string? Name { get; set; } + public string? Name { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs b/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs index 09f8e3c..b2eeb03 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class CloneLinks : Links { - public List? Clone { get; set; } + public List? Clone { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Comment.cs b/src/Bitbucket.Net/Models/Core/Projects/Comment.cs index 8b24dba..5333647 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Comment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Comment.cs @@ -13,75 +13,75 @@ public class Comment /// /// Gets or sets the server-assigned comment identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the version number for optimistic locking on updates. /// - public int Version { get; set; } + public int Version { get; init; } /// /// Gets or sets the comment body text. /// - public string? Text { get; set; } + public string? Text { get; init; } /// /// Bitbucket Server comment state. /// Common values: OPEN, PENDING, RESOLVED. /// - public string? State { get; set; } + public string? State { get; init; } /// /// Indicates whether the whole comment thread is resolved. /// When true, Bitbucket UI will typically collapse/hide the thread as resolved. /// - public bool? ThreadResolved { get; set; } + public bool? ThreadResolved { get; init; } /// /// The user who resolved the comment thread (when resolved). /// - public User? Resolver { get; set; } + public User? Resolver { get; init; } /// /// When the comment thread was resolved (when resolved). /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? ResolvedDate { get; set; } + public DateTimeOffset? ResolvedDate { get; init; } /// /// Gets or sets the date and time when the comment was created. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// Gets or sets the date and time when the comment was last updated. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; init; } /// /// Gets or sets the user who authored the comment. /// - public User? Author { get; set; } + public User? Author { get; init; } /// /// Gets or sets the nested reply comments. /// - public List? Comments { get; set; } + public List? Comments { get; init; } /// /// Gets or sets the tasks associated with this comment. /// - public List? Tasks { get; set; } + public List? Tasks { get; init; } /// /// Gets or sets the participants in the comment thread. /// - public List? Participants { get; set; } + public List? Participants { get; init; } /// /// Gets or sets the hypermedia links for this comment. /// - public Links? Links { get; set; } + public Links? Links { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs index 05e6f6c..ae6e81e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs @@ -1,6 +1,3 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.Core.Projects; /// @@ -16,13 +13,11 @@ public class CommentAnchor /// /// Gets or sets the line type (e.g. ADDED, REMOVED, CONTEXT). /// - [JsonConverter(typeof(LineTypesConverter))] public LineTypes LineType { get; set; } /// /// Gets or sets the file type (e.g. FROM for source, TO for destination). /// - [JsonConverter(typeof(FileTypesConverter))] public FileTypes FileType { get; set; } /// diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs index 89db9cf..36f4958 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class CommentId { - public int? Id { get; set; } + public int? Id { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs index 826fbb5..b7b3cf2 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs @@ -13,52 +13,52 @@ public class CommentRef /// /// Gets or sets the additional properties bag. /// - public Properties? Properties { get; set; } + public Properties? Properties { get; init; } /// /// Gets or sets the server-assigned comment identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the version number for optimistic locking on updates. /// - public int Version { get; set; } + public int Version { get; init; } /// /// Gets or sets the comment body text. /// - public string? Text { get; set; } + public string? Text { get; init; } /// /// Gets or sets the user who authored the comment. /// - public User? Author { get; set; } + public User? Author { get; init; } /// /// Gets or sets the date and time when the comment was created. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// Gets or sets the date and time when the comment was last updated. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; init; } /// /// Gets or sets the nested reply comment references. /// - public List? Comments { get; set; } + public List? Comments { get; init; } /// /// Gets or sets the tasks associated with this comment. /// - public List? Tasks { get; set; } + public List? Tasks { get; init; } /// /// Gets or sets the operations the current user is permitted to perform on this comment. /// - public Permittedoperations? PermittedOperations { get; set; } + public Permittedoperations? PermittedOperations { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Commit.cs b/src/Bitbucket.Net/Models/Core/Projects/Commit.cs index fb6bc7d..159821e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Commit.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Commit.cs @@ -11,42 +11,42 @@ public class Commit : CommitParent /// /// Gets or sets the commit author. /// - public Author? Author { get; set; } + public Author? Author { get; init; } /// /// Gets or sets the author timestamp (Unix epoch milliseconds from the Bitbucket API). /// [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset AuthorTimestamp { get; set; } + public DateTimeOffset AuthorTimestamp { get; init; } /// /// Gets or sets the committer (may differ from author in cherry-picks or patches). /// - public Author? Committer { get; set; } + public Author? Committer { get; init; } /// /// Gets or sets the committer timestamp (Unix epoch milliseconds from the Bitbucket API). /// [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset CommitterTimestamp { get; set; } + public DateTimeOffset CommitterTimestamp { get; init; } /// /// Gets or sets the commit message. /// - public string? Message { get; set; } + public string? Message { get; init; } /// /// Gets or sets the parent commits. /// - public List? Parents { get; set; } + public List? Parents { get; init; } /// /// Gets or sets the number of unique authors in the commit range. /// - public int AuthorCount { get; set; } + public int AuthorCount { get; init; } /// /// Gets or sets the total number of commits in the range. /// - public int TotalCount { get; set; } + public int TotalCount { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs b/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs index 7f23edd..477a177 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Lightweight commit reference containing the full and abbreviated SHA. @@ -8,10 +8,10 @@ public class CommitParent /// /// Gets or sets the full commit SHA hash. /// - public string? Id { get; set; } + public string? Id { get; init; } /// /// Gets or sets the abbreviated commit SHA shown in the Bitbucket UI. /// - public string? DisplayId { get; set; } + public string? DisplayId { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs b/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs index 21dc427..e4dba1e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class ContentItem { - public Path? Path { get; set; } - public string? ContentId { get; set; } - public string? Type { get; set; } - public int Size { get; set; } + public Path? Path { get; init; } + public string? ContentId { get; init; } + public string? Type { get; init; } + public int Size { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Diff.cs b/src/Bitbucket.Net/Models/Core/Projects/Diff.cs index bb78453..4886274 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Diff.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Diff.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A file-level diff. Extends with source/destination paths and hunks. @@ -8,15 +8,15 @@ public class Diff : DiffInfo /// /// Gets or sets the source (before) file path. /// - public Path? Source { get; set; } + public Path? Source { get; init; } /// /// Gets or sets the destination (after) file path. /// - public Path? Destination { get; set; } + public Path? Destination { get; init; } /// /// Gets or sets the list of diff hunks containing the actual line changes. /// - public List? Hunks { get; set; } + public List? Hunks { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs b/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs index 17142a9..37345cc 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A hunk within a file diff, describing a contiguous block of changes. @@ -8,30 +8,30 @@ public class DiffHunk /// /// Gets or sets the starting line number in the source (before) file. /// - public int SourceLine { get; set; } + public int SourceLine { get; init; } /// /// Gets or sets the number of lines from the source file included in this hunk. /// - public int SourceSpan { get; set; } + public int SourceSpan { get; init; } /// /// Gets or sets the starting line number in the destination (after) file. /// - public int DestinationLine { get; set; } + public int DestinationLine { get; init; } /// /// Gets or sets the number of lines from the destination file included in this hunk. /// - public int DestinationSpan { get; set; } + public int DestinationSpan { get; init; } /// /// Gets or sets the segments (groups of added, removed, or context lines) in this hunk. /// - public List? Segments { get; set; } + public List? Segments { get; init; } /// /// Gets or sets a value indicating whether this hunk was truncated by the server. /// - public bool Truncated { get; set; } + public bool Truncated { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs index e928bdc..1a13054 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public abstract class DiffInfo { @@ -6,10 +6,10 @@ public abstract class DiffInfo /// Indicates whether the diff was truncated by the server. /// Note: Bitbucket Server 9.0+ returns boolean; older versions may return string. /// - public bool Truncated { get; set; } + public bool Truncated { get; init; } - public string? ContextLines { get; set; } - public string? FromHash { get; set; } - public string? ToHash { get; set; } - public string? WhiteSpace { get; set; } + public string? ContextLines { get; init; } + public string? FromHash { get; init; } + public string? ToHash { get; init; } + public string? WhiteSpace { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Differences.cs b/src/Bitbucket.Net/Models/Core/Projects/Differences.cs index 0ed6755..76d211b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Differences.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Differences.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Differences : DiffInfo { - public List? Diffs { get; set; } + public List? Diffs { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs b/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs index 5e48c3c..1821bc3 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Represents a reference (branch/tag) in a pull request's source or target. @@ -8,26 +8,26 @@ public class FromToRef /// /// The full ref ID (e.g., "refs/heads/feature-branch"). /// - public string? Id { get; set; } + public string? Id { get; init; } /// /// The display-friendly ref ID (e.g., "feature-branch"). /// - public string? DisplayId { get; set; } + public string? DisplayId { get; init; } /// /// The SHA of the latest commit on this ref. /// This is useful for creating line-specific comments on pull requests. /// - public string? LatestCommit { get; set; } + public string? LatestCommit { get; init; } /// /// The type of ref (e.g., "BRANCH", "TAG"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// The repository containing this ref. /// - public RepositoryRef? Repository { get; set; } + public RepositoryRef? Repository { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Hook.cs b/src/Bitbucket.Net/Models/Core/Projects/Hook.cs index 536de41..c77cfe2 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Hook.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Hook.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Hook { - public HookDetails? Details { get; set; } - public bool Enabled { get; set; } - public bool Configured { get; set; } - public HookScope? Scope { get; set; } + public HookDetails? Details { get; init; } + public bool Enabled { get; init; } + public bool Configured { get; init; } + public HookScope? Scope { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs index 43ffb54..81c2ad0 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs @@ -1,16 +1,12 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.Core.Projects; public class HookDetails { - public string? Key { get; set; } - public string? Name { get; set; } - [JsonConverter(typeof(HookTypesConverter))] - public HookTypes Type { get; set; } - public string? Description { get; set; } - public string? Version { get; set; } - public object? ConfigFormKey { get; set; } - public List? ScopeTypes { get; set; } + public string? Key { get; init; } + public string? Name { get; init; } + public HookTypes Type { get; init; } + public string? Description { get; init; } + public string? Version { get; init; } + public object? ConfigFormKey { get; init; } + public List? ScopeTypes { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs index a5634ac..da4aac8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs @@ -1,11 +1,7 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.Core.Projects; public class HookScope { - public int ResourceId { get; set; } - [JsonConverter(typeof(ScopeTypesConverter))] - public ScopeTypes Type { get; set; } + public int ResourceId { get; init; } + public ScopeTypes Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs b/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs index 0a68846..4f7e69d 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class LastModified { - public Dictionary? Files { get; set; } - public Commit? LatestCommit { get; set; } + public Dictionary? Files { get; init; } + public Commit? LatestCommit { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs b/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs index 447a901..f034226 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class LicensedUser { - public string? Name { get; set; } - public bool Deletable { get; set; } + public string? Name { get; init; } + public bool Deletable { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Line.cs b/src/Bitbucket.Net/Models/Core/Projects/Line.cs index 62bd824..33c596e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Line.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Line.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Line { - public string? Text { get; set; } + public string? Text { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs b/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs index ba111e8..8e98235 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A single line in a diff segment with source and destination line numbers. @@ -8,20 +8,20 @@ public class LineRef /// /// Gets or sets the line number in the destination (after) file. /// - public int Destination { get; set; } + public int Destination { get; init; } /// /// Gets or sets the line number in the source (before) file. /// - public int Source { get; set; } + public int Source { get; init; } /// /// Gets or sets the text content of the line. /// - public string? Line { get; set; } + public string? Line { get; init; } /// /// Gets or sets a value indicating whether this line was truncated by the server. /// - public bool Truncated { get; set; } + public bool Truncated { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Link.cs b/src/Bitbucket.Net/Models/Core/Projects/Link.cs index e357710..c99a4d7 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Link.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Link.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A single hyperlink in the Bitbucket REST API response. @@ -8,7 +8,7 @@ public class Link /// /// Gets or sets the URL of the link. /// - public string? Href { get; set; } + public string? Href { get; init; } /// /// Returns the link URL or an empty string when not set. diff --git a/src/Bitbucket.Net/Models/Core/Projects/Links.cs b/src/Bitbucket.Net/Models/Core/Projects/Links.cs index 7d8811c..cf9e019 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Links.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Links.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Hypermedia links for a Bitbucket resource. @@ -8,5 +8,5 @@ public class Links /// /// Gets or sets the self-referencing links. /// - public List? Self { get; set; } + public List? Self { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs b/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs index 51ae255..4b1ace0 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class MergeCheckRequiredBuilds { - public bool Enable { get; set; } - public int Count { get; set; } + public bool Enable { get; init; } + public int Count { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs b/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs index 5af97a6..5389083 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class MergeHookRequiredApprovers { - public bool Enable { get; set; } - public int Count { get; set; } + public bool Enable { get; init; } + public int Count { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Participant.cs b/src/Bitbucket.Net/Models/Core/Projects/Participant.cs index 1a89859..b9b9a7f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Participant.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Participant.cs @@ -1,6 +1,4 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.Core.Projects; @@ -12,24 +10,22 @@ public class Participant /// /// Gets or sets the participant's user details. /// - public User? User { get; set; } + public User? User { get; init; } /// /// Gets or sets the participant's role (e.g. AUTHOR, REVIEWER, PARTICIPANT). /// - [JsonConverter(typeof(RolesConverter))] - public Roles Role { get; set; } + public Roles Role { get; init; } /// /// Gets or sets a value indicating whether the participant has approved the pull request. /// - public bool Approved { get; set; } + public bool Approved { get; init; } /// /// Gets or sets the participant's review status (e.g. APPROVED, UNAPPROVED, NEEDS_WORK). /// - [JsonConverter(typeof(ParticipantStatusConverter))] - public ParticipantStatus Status { get; set; } + public ParticipantStatus Status { get; init; } /// /// Returns the participant's display name when available. diff --git a/src/Bitbucket.Net/Models/Core/Projects/Path.cs b/src/Bitbucket.Net/Models/Core/Projects/Path.cs index 78393e0..a7d9d39 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Path.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Path.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Represents a file path in a Bitbucket repository. @@ -8,29 +8,29 @@ public class Path /// /// The path components (directory and file name parts). /// - public List? Components { get; set; } + public List? Components { get; init; } /// /// The parent directory path. /// - public string? Parent { get; set; } + public string? Parent { get; init; } /// /// The file or directory name. /// - public string? Name { get; set; } + public string? Name { get; init; } /// /// The file extension (if any). /// - public string? Extension { get; set; } + public string? Extension { get; init; } /// /// The full path as a string, as returned by the Bitbucket API. /// Note: This property name is lowercase to match the JSON response. /// // ReSharper disable once InconsistentNaming - public string? toString { get; set; } + public string? toString { get; init; } /// /// Returns the full path string representation. diff --git a/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs b/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs index 4444410..dfcb58a 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Permittedoperations { - public bool Editable { get; set; } - public bool Deletable { get; set; } - public bool Transitionable { get; set; } + public bool Editable { get; init; } + public bool Deletable { get; init; } + public bool Transitionable { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Project.cs b/src/Bitbucket.Net/Models/Core/Projects/Project.cs index 998f60b..5a8a610 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Project.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Project.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Full Bitbucket project. Extends with server-assigned identity and metadata. @@ -8,22 +8,22 @@ public class Project : ProjectDefinition /// /// Gets or sets the server-assigned project identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets a value indicating whether the project is publicly accessible. /// - public bool Public { get; set; } + public bool Public { get; init; } /// /// Gets or sets the project type (e.g. "NORMAL" or "PERSONAL"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the hypermedia links for this project. /// - public Links? Links { get; set; } + public Links? Links { get; init; } /// /// Returns the project name, when available. diff --git a/src/Bitbucket.Net/Models/Core/Projects/Properties.cs b/src/Bitbucket.Net/Models/Core/Projects/Properties.cs index 820eb56..3b2e9ae 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Properties.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Properties.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Properties { - public string? Key { get; set; } + public string? Key { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs index 4123cc9..0c866fd 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs @@ -11,39 +11,39 @@ public class PullRequest : PullRequestInfo /// /// Gets or sets the server-assigned pull request identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the version number for optimistic locking on updates. /// - public int Version { get; set; } + public int Version { get; init; } /// /// Gets or sets the date and time when the pull request was created. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// Gets or sets the date and time when the pull request was last updated. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; init; } /// /// Gets or sets the pull request author. /// - public Participant? Author { get; set; } + public Participant? Author { get; init; } /// /// Gets or sets the list of participants (author, reviewers, and watchers). /// - public List? Participants { get; set; } + public List? Participants { get; init; } /// /// Gets or sets the hypermedia links for this pull request. /// - public Links? Links { get; set; } + public Links? Links { get; init; } /// /// Returns a human-readable label combining the author display name and title. diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs index a378678..979bdab 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs @@ -12,41 +12,41 @@ public class PullRequestActivity /// /// Gets or sets the server-assigned activity identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the date and time when the activity occurred. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// Gets or sets the user who performed the activity. /// - public User? User { get; set; } + public User? User { get; init; } /// /// Gets or sets the activity action type (e.g. "COMMENTED", "APPROVED", "MERGED", "OPENED"). /// - public string? Action { get; set; } + public string? Action { get; init; } /// /// Gets or sets the comment-specific action (e.g. "ADDED", "UPDATED", "DELETED") when the activity involves a comment. /// - public string? CommentAction { get; set; } + public string? CommentAction { get; init; } /// /// Gets or sets the comment associated with this activity, if any. /// - public Comment? Comment { get; set; } + public Comment? Comment { get; init; } /// /// Gets or sets the anchor location for an inline comment, if applicable. /// - public CommentAnchor? CommentAnchor { get; set; } + public CommentAnchor? CommentAnchor { get; init; } /// /// Gets or sets the commit associated with this activity, if any. /// - public Commit? Commit { get; set; } + public Commit? Commit { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs index 71cf44c..5710dfd 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs @@ -1,6 +1,3 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.Core.Projects; /// @@ -21,7 +18,6 @@ public class PullRequestInfo /// /// Gets or sets the pull request state (e.g. OPEN, MERGED, DECLINED). /// - [JsonConverter(typeof(PullRequestStatesConverter))] public PullRequestStates State { get; set; } /// diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs index f13b2c7..8bfa8b9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class PullRequestMergeState { - public bool CanMerge { get; set; } - public bool Conflicted { get; set; } - public string? Outcome { get; set; } - public List? Vetoes { get; set; } + public bool CanMerge { get; init; } + public bool Conflicted { get; init; } + public string? Outcome { get; init; } + public List? Vetoes { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs index b0c1ee6..ca4f2ba 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class PullRequestMetadata { - public PullRequest? PullRequest { get; set; } + public PullRequest? PullRequest { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs index 626a6ac..e86e838 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs @@ -6,9 +6,9 @@ namespace Bitbucket.Net.Models.Core.Projects; public class PullRequestSuggestion { [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset ChangeTime { get; set; } - public RefChange? RefChange { get; set; } - public Repository? Repository { get; set; } - public Ref? FromRef { get; set; } - public Ref? ToRef { get; set; } + public DateTimeOffset ChangeTime { get; init; } + public RefChange? RefChange { get; init; } + public Repository? Repository { get; init; } + public Ref? FromRef { get; init; } + public Ref? ToRef { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Ref.cs b/src/Bitbucket.Net/Models/Core/Projects/Ref.cs index e8fa224..8f464ee 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Ref.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Ref.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Ref { - public string? Id { get; set; } - public string? DisplayId { get; set; } - public string? Type { get; set; } + public string? Id { get; init; } + public string? DisplayId { get; init; } + public string? Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs b/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs index 5e1a82f..afb6c4d 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs @@ -1,10 +1,10 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class RefChange { - public Ref? Ref { get; set; } - public string? RefId { get; set; } - public string? FromHash { get; set; } - public string? ToHash { get; set; } - public string? Type { get; set; } + public Ref? Ref { get; init; } + public string? RefId { get; init; } + public string? FromHash { get; init; } + public string? ToHash { get; init; } + public string? Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Repository.cs b/src/Bitbucket.Net/Models/Core/Projects/Repository.cs index b901be7..61ac7e3 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Repository.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Repository.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// Full Bitbucket repository. Extends with server-assigned identity and metadata. @@ -8,37 +8,37 @@ public class Repository : RepositoryRef /// /// Gets or sets the server-assigned repository identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the SCM type identifier (e.g. "git"). /// - public string? ScmId { get; set; } + public string? ScmId { get; init; } /// /// Gets or sets the repository state (e.g. "AVAILABLE"). /// - public string? State { get; set; } + public string? State { get; init; } /// /// Gets or sets a human-readable status message for the repository. /// - public string? StatusMessage { get; set; } + public string? StatusMessage { get; init; } /// /// Gets or sets a value indicating whether the repository can be forked. /// - public bool Forkable { get; set; } + public bool Forkable { get; init; } /// /// Gets or sets a value indicating whether the repository is publicly accessible. /// - public bool Public { get; set; } + public bool Public { get; init; } /// /// Gets or sets the clone URLs and other hypermedia links for this repository. /// - public CloneLinks? Links { get; set; } + public CloneLinks? Links { get; init; } /// /// Returns the repository name, when available. diff --git a/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs b/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs index def5f2d..41bae7e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class RepositoryFork : RepositoryOrigin { - public RepositoryOrigin? Origin { get; set; } + public RepositoryOrigin? Origin { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs b/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs index 064eec8..8370f34 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs @@ -1,15 +1,15 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class RepositoryOrigin { - public string? Slug { get; set; } - public int Id { get; set; } - public string? Name { get; set; } - public string? ScmId { get; set; } - public string? State { get; set; } - public string? StatusMessage { get; set; } - public bool Forkable { get; set; } - public Project? Project { get; set; } - public bool Public { get; set; } - public Links? Links { get; set; } + public string? Slug { get; init; } + public int Id { get; init; } + public string? Name { get; init; } + public string? ScmId { get; init; } + public string? State { get; init; } + public string? StatusMessage { get; init; } + public bool Forkable { get; init; } + public Project? Project { get; init; } + public bool Public { get; init; } + public Links? Links { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateBranchRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateBranchRequest.cs new file mode 100644 index 0000000..8989171 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateBranchRequest.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new branch. +/// +public sealed class CreateBranchRequest +{ + /// + /// The branch name (e.g. "feature/my-feature"). Required. + /// + public required string Name { get; init; } + + /// + /// The starting point for the branch — a commit SHA, branch name, or tag. Required. + /// + public required string StartPoint { get; init; } + + /// + /// An optional message to associate with the branch creation. + /// + public string? Message { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateProjectRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateProjectRequest.cs new file mode 100644 index 0000000..a64d4dc --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateProjectRequest.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new Bitbucket project. +/// +public sealed class CreateProjectRequest +{ + /// + /// The unique project key (e.g. "PRJ"). Required. + /// + public required string Key { get; init; } + + /// + /// The human-readable project name. Required. + /// + public required string Name { get; init; } + + /// + /// An optional project description. + /// + public string? Description { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreatePullRequestRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreatePullRequestRequest.cs new file mode 100644 index 0000000..04293a8 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreatePullRequestRequest.cs @@ -0,0 +1,32 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new pull request. +/// +public sealed class CreatePullRequestRequest +{ + /// + /// The pull request title. Required. + /// + public required string Title { get; init; } + + /// + /// An optional description in Markdown format. + /// + public string? Description { get; init; } + + /// + /// The source branch reference. Required. + /// + public required FromToRef FromRef { get; init; } + + /// + /// The target branch reference. Required. + /// + public required FromToRef ToRef { get; init; } + + /// + /// An optional list of reviewers to add to the pull request. + /// + public IReadOnlyList? Reviewers { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateRepositoryRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateRepositoryRequest.cs new file mode 100644 index 0000000..1f489d1 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateRepositoryRequest.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new repository in a project. +/// +public sealed class CreateRepositoryRequest +{ + /// + /// The repository name. Required. + /// + public required string Name { get; init; } + + /// + /// The SCM type identifier. Defaults to "git". + /// + public string ScmId { get; init; } = "git"; + + /// + /// Whether the repository may be forked. + /// + public bool? Forkable { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateTaskRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateTaskRequest.cs new file mode 100644 index 0000000..185b4b4 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateTaskRequest.cs @@ -0,0 +1,19 @@ +using Bitbucket.Net.Models.Core.Tasks; + +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new task on a pull request comment. +/// +public sealed class CreateTaskRequest +{ + /// + /// The task description text. Required. + /// + public required string Text { get; init; } + + /// + /// The anchor comment to which this task is attached. + /// + public TaskBasicAnchor? Anchor { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateWebHookRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateWebHookRequest.cs new file mode 100644 index 0000000..af63563 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/CreateWebHookRequest.cs @@ -0,0 +1,32 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for creating a new webhook on a repository. +/// +public sealed class CreateWebHookRequest +{ + /// + /// The webhook name. Required. + /// + public required string Name { get; init; } + + /// + /// The list of Bitbucket event types that trigger this webhook (e.g. "repo:refs_changed"). + /// + public IReadOnlyList? Events { get; init; } + + /// + /// The URL that receives the webhook POST. Required. + /// + public required string Url { get; init; } + + /// + /// Whether the webhook is active. Defaults to true. + /// + public bool Active { get; init; } = true; + + /// + /// Additional configuration for the webhook (e.g. secret). + /// + public Dictionary? Configuration { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/ForkRepositoryRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/ForkRepositoryRequest.cs new file mode 100644 index 0000000..7f76065 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/ForkRepositoryRequest.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for forking a repository. +/// +public sealed class ForkRepositoryRequest +{ + /// + /// The slug for the forked repository. When omitted, defaults to the source repository slug. + /// + public string? Slug { get; init; } + + /// + /// The display name for the forked repository. + /// + public string? Name { get; init; } + + /// + /// The target project for the fork. When omitted, the fork is created in the current user's personal project. + /// + public ProjectRef? Project { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/MergePullRequestRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/MergePullRequestRequest.cs new file mode 100644 index 0000000..f2d9c71 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/MergePullRequestRequest.cs @@ -0,0 +1,24 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for merging a pull request. All fields are optional; +/// when omitted, the server uses default merge behaviour. +/// +public sealed class MergePullRequestRequest +{ + /// + /// The expected current version of the pull request for optimistic locking. + /// When set to -1 (the default), the version check is skipped. + /// + public int Version { get; init; } = -1; + + /// + /// An optional custom merge commit message. + /// + public string? Message { get; init; } + + /// + /// An optional merge strategy override (e.g. "squash", "no-ff"). + /// + public string? Strategy { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateProjectRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateProjectRequest.cs new file mode 100644 index 0000000..79bfd0f --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateProjectRequest.cs @@ -0,0 +1,22 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for updating an existing Bitbucket project. +/// +public sealed class UpdateProjectRequest +{ + /// + /// The project key. Optional — when omitted the key from the URL path is used. + /// + public string? Key { get; init; } + + /// + /// The new human-readable project name. + /// + public string? Name { get; init; } + + /// + /// The new project description. + /// + public string? Description { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdatePullRequestRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdatePullRequestRequest.cs new file mode 100644 index 0000000..a50cd30 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdatePullRequestRequest.cs @@ -0,0 +1,27 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for updating an existing pull request. +/// +public sealed class UpdatePullRequestRequest +{ + /// + /// The expected current version of the pull request for optimistic locking. Required. + /// + public required int Version { get; init; } + + /// + /// The new pull request title. + /// + public string? Title { get; init; } + + /// + /// The new pull request description in Markdown format. + /// + public string? Description { get; init; } + + /// + /// The updated list of reviewers. + /// + public IReadOnlyList? Reviewers { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateTaskRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateTaskRequest.cs new file mode 100644 index 0000000..1515a45 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateTaskRequest.cs @@ -0,0 +1,17 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for updating an existing task. +/// +public sealed class UpdateTaskRequest +{ + /// + /// The new task description text. Required. + /// + public required string Text { get; init; } + + /// + /// The desired task state (e.g. "OPEN", "RESOLVED"). + /// + public string? State { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateWebHookRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateWebHookRequest.cs new file mode 100644 index 0000000..d7d6833 --- /dev/null +++ b/src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateWebHookRequest.cs @@ -0,0 +1,32 @@ +namespace Bitbucket.Net.Models.Core.Projects.Requests; + +/// +/// Request body for updating an existing webhook on a repository. +/// +public sealed class UpdateWebHookRequest +{ + /// + /// The webhook name. + /// + public string? Name { get; init; } + + /// + /// The list of Bitbucket event types that trigger this webhook. + /// + public IReadOnlyList? Events { get; init; } + + /// + /// The URL that receives the webhook POST. + /// + public string? Url { get; init; } + + /// + /// Whether the webhook is active. + /// + public bool? Active { get; init; } + + /// + /// Additional configuration for the webhook (e.g. secret). + /// + public Dictionary? Configuration { get; init; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs b/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs index 6095f34..6a26796 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A pull request reviewer. Extends with the last-reviewed commit reference. @@ -8,5 +8,5 @@ public class Reviewer : Participant /// /// Gets or sets the SHA of the last commit the reviewer has reviewed. /// - public string? LastReviewedCommit { get; set; } + public string? LastReviewedCommit { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs index 6c73632..7947c22 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs @@ -2,6 +2,7 @@ public enum ScopeTypes { + Global, Project, Repository, } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Segment.cs b/src/Bitbucket.Net/Models/Core/Projects/Segment.cs index 4f0d314..9e0001a 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Segment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Segment.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A segment within a diff hunk, grouping consecutive lines of the same type. @@ -8,15 +8,15 @@ public class Segment /// /// Gets or sets the segment type (e.g. "ADDED", "REMOVED", or "CONTEXT"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the lines in this segment. /// - public List? Lines { get; set; } + public List? Lines { get; init; } /// /// Gets or sets a value indicating whether this segment was truncated by the server. /// - public bool Truncated { get; set; } + public bool Truncated { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Tag.cs b/src/Bitbucket.Net/Models/Core/Projects/Tag.cs index be724f5..3a91ae8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Tag.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Tag.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; /// /// A Git tag in a Bitbucket repository. @@ -8,30 +8,30 @@ public class Tag /// /// Gets or sets the full tag ref path (e.g. "refs/tags/v1.0"). /// - public string? Id { get; set; } + public string? Id { get; init; } /// /// Gets or sets the short tag name shown in the Bitbucket UI. /// - public string? DisplayId { get; set; } + public string? DisplayId { get; init; } /// /// Gets or sets the ref type (typically "TAG"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the SHA of the latest commit this tag points to. /// - public string? LatestCommit { get; set; } + public string? LatestCommit { get; init; } /// /// Gets or sets the changeset identifier of the latest change. /// - public string? LatestChangeset { get; set; } + public string? LatestChangeset { get; init; } /// /// Gets or sets the tag object hash (for annotated tags). /// - public string? Hash { get; set; } + public string? Hash { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs b/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs index 9d10a11..3ac8fc2 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs @@ -6,6 +6,6 @@ namespace Bitbucket.Net.Models.Core.Projects; public class TimeWindow { [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Start { get; set; } - public long Duration { get; set; } + public DateTimeOffset Start { get; init; } + public long Duration { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Veto.cs b/src/Bitbucket.Net/Models/Core/Projects/Veto.cs index a9cc7df..95f7c81 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Veto.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Veto.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class Veto { - public string? SummaryMessage { get; set; } - public string? DetailedMessage { get; set; } + public string? SummaryMessage { get; init; } + public string? DetailedMessage { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs index 3feec75..9287648 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs @@ -5,13 +5,13 @@ namespace Bitbucket.Net.Models.Core.Projects; public class WebHookInvocation { - public int Id { get; set; } - public string? Event { get; set; } - public int Duration { get; set; } + public int Id { get; init; } + public string? Event { get; init; } + public int Duration { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Start { get; set; } + public DateTimeOffset Start { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Finish { get; set; } - public WebHookRequest? Request { get; set; } - public WebHookResult? Result { get; set; } + public DateTimeOffset Finish { get; init; } + public WebHookRequest? Request { get; init; } + public WebHookResult? Result { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs index 6dd5c3e..67ad82b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookRequest { - public string? Url { get; set; } - public string? Method { get; set; } + public string? Url { get; init; } + public string? Method { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs index e8b7399..1765f98 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs @@ -1,11 +1,7 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.Core.Projects; public class WebHookResult { - public string? Description { get; set; } - [JsonConverter(typeof(WebHookOutcomesConverter))] - public WebHookOutcomes Outcome { get; set; } + public string? Description { get; init; } + public WebHookOutcomes Outcome { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs index 41aa17b..de2ff45 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookStatistics { - public WebHookStatisticsCounts? Counts { get; set; } + public WebHookStatisticsCounts? Counts { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs index ecc6cc6..21f5cd9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookStatisticsCounts { - public int Errors { get; set; } - public int Failures { get; set; } - public int Successes { get; set; } - public TimeWindow? Window { get; set; } + public int Errors { get; init; } + public int Failures { get; init; } + public int Successes { get; init; } + public TimeWindow? Window { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs index aae165f..d652856 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookStatisticsSummary { - public WebHookInvocation? LastSuccess { get; set; } - public WebHookInvocation? LastFailure { get; set; } - public WebHookInvocation? LastError { get; set; } - public int Counts { get; set; } + public WebHookInvocation? LastSuccess { get; init; } + public WebHookInvocation? LastFailure { get; init; } + public WebHookInvocation? LastError { get; init; } + public int Counts { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs index 5a09992..62b6479 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookTestRequest : WebHookRequest { - public string? Body { get; set; } - public List? Headers { get; set; } + public string? Body { get; init; } + public List? Headers { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs index 3588983..5e8c254 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookTestRequestResponse { - public WebHookTestRequest? Request { get; set; } - public WebHookTestResponse? Response { get; set; } + public WebHookTestRequest? Request { get; init; } + public WebHookTestResponse? Response { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs index 51cc559..88c1090 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects; +namespace Bitbucket.Net.Models.Core.Projects; public class WebHookTestResponse { - public int Status { get; set; } - public List? Headers { get; set; } - public string? Body { get; set; } + public int Status { get; init; } + public List? Headers { get; init; } + public string? Body { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs index 25449d6..f96f063 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Tasks; +namespace Bitbucket.Net.Models.Core.Tasks; /// /// A Bitbucket pull request task. Extends with an anchor and state. @@ -8,10 +8,10 @@ public class BitbucketTask : TaskRef /// /// Gets or sets the comment anchor this task is attached to. /// - public TaskAnchor? Anchor { get; set; } + public TaskAnchor? Anchor { get; init; } /// /// Gets or sets the task state (e.g. "OPEN" or "RESOLVED"). /// - public string? State { get; set; } + public string? State { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs index 1f1f749..67522b8 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Tasks; +namespace Bitbucket.Net.Models.Core.Tasks; public class BitbucketTaskCount { - public int Open { get; set; } - public int Resolved { get; set; } + public int Open { get; init; } + public int Resolved { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs index 2dae176..8482d1f 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs @@ -12,21 +12,21 @@ public class TaskAnchor : TaskRef /// /// Gets or sets the version number for optimistic locking on updates. /// - public int Version { get; set; } + public int Version { get; init; } /// /// Gets or sets the date and time when the anchor was last updated. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; init; } /// /// Gets or sets the nested comment references on this anchor. /// - public List? Comments { get; set; } + public List? Comments { get; init; } /// /// Gets or sets the tasks associated with this anchor. /// - public List? Tasks { get; set; } + public List? Tasks { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs index d2edc8f..1174f5c 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Tasks; +namespace Bitbucket.Net.Models.Core.Tasks; public class TaskBasicAnchor { - public int Id { get; set; } - public string? Type { get; set; } + public int Id { get; init; } + public string? Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs index a7ab03e..454d26a 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs @@ -13,31 +13,31 @@ public abstract class TaskRef /// /// Gets or sets the additional properties bag. /// - public Properties? Properties { get; set; } + public Properties? Properties { get; init; } /// /// Gets or sets the server-assigned task identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the task description text. /// - public string? Text { get; set; } + public string? Text { get; init; } /// /// Gets or sets the user who created the task. /// - public User? Author { get; set; } + public User? Author { get; init; } /// /// Gets or sets the date and time when the task was created. /// [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + public DateTimeOffset? CreatedDate { get; init; } /// /// Gets or sets the operations the current user is permitted to perform on this task. /// - public Permittedoperations? PermittedOperations { get; set; } + public Permittedoperations? PermittedOperations { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/Identity.cs b/src/Bitbucket.Net/Models/Core/Users/Identity.cs index a54e30a..626b290 100644 --- a/src/Bitbucket.Net/Models/Core/Users/Identity.cs +++ b/src/Bitbucket.Net/Models/Core/Users/Identity.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Users; +namespace Bitbucket.Net.Models.Core.Users; /// /// Extends with an email address. @@ -8,5 +8,5 @@ public class Identity : Named /// /// Gets or sets the email address associated with the identity. /// - public string? EmailAddress { get; set; } + public string? EmailAddress { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/User.cs b/src/Bitbucket.Net/Models/Core/Users/User.cs index a4390b1..9926938 100644 --- a/src/Bitbucket.Net/Models/Core/Users/User.cs +++ b/src/Bitbucket.Net/Models/Core/Users/User.cs @@ -1,4 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Users; +namespace Bitbucket.Net.Models.Core.Users; /// /// Full Bitbucket user. Extends with server-assigned identity, display name, and account state. @@ -8,32 +8,32 @@ public class User : Identity /// /// Gets or sets the server-assigned user identifier. /// - public int Id { get; set; } + public int Id { get; init; } /// /// Gets or sets the user's display name. /// - public string? DisplayName { get; set; } + public string? DisplayName { get; init; } /// /// Gets or sets a value indicating whether the user account is active. /// - public bool Active { get; set; } + public bool Active { get; init; } /// /// Gets or sets the URL-friendly user identifier. /// - public string? Slug { get; set; } + public string? Slug { get; init; } /// /// Gets or sets the user type (e.g. "NORMAL" or "SERVICE"). /// - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the URL of the user's avatar image. /// - public string? AvatarUrl { get; set; } + public string? AvatarUrl { get; init; } /// /// Returns the user's display name when available. diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs index f32bee2..ec04fdf 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.DefaultReviewers; +namespace Bitbucket.Net.Models.DefaultReviewers; public class DefaultReviewerPullRequestConditionScope { - public string? Type { get; set; } - public int ResourceId { get; set; } + public string? Type { get; init; } + public int ResourceId { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs index 9267530..c0026bc 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.DefaultReviewers; +namespace Bitbucket.Net.Models.DefaultReviewers; public class DefaultReviewerPullRequestConditionType { - public string? Id { get; set; } - public string? Name { get; set; } + public string? Id { get; init; } + public string? Name { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs b/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs index b852734..8d05e25 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs @@ -1,9 +1,9 @@ -namespace Bitbucket.Net.Models.DefaultReviewers; +namespace Bitbucket.Net.Models.DefaultReviewers; public class RefMatcher { - public bool Active { get; set; } - public string? Id { get; set; } - public string? DisplayId { get; set; } - public DefaultReviewerPullRequestConditionType? Type { get; set; } + public bool Active { get; init; } + public string? Id { get; init; } + public string? DisplayId { get; init; } + public DefaultReviewerPullRequestConditionType? Type { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs b/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs index 43607e6..52b8317 100644 --- a/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs +++ b/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.Git; +namespace Bitbucket.Net.Models.Git; public class RebasePullRequestCondition { - public bool CanRebase { get; set; } - public bool CanWrite { get; set; } - public List? Vetoes { get; set; } + public bool CanRebase { get; init; } + public bool CanWrite { get; init; } + public List? Vetoes { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Git/Veto.cs b/src/Bitbucket.Net/Models/Git/Veto.cs index 93d271c..56f3efa 100644 --- a/src/Bitbucket.Net/Models/Git/Veto.cs +++ b/src/Bitbucket.Net/Models/Git/Veto.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Git; +namespace Bitbucket.Net.Models.Git; public class Veto { - public string? SummaryMessage { get; set; } - public string? DetailedMessage { get; set; } + public string? SummaryMessage { get; init; } + public string? DetailedMessage { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/ChangeSet.cs b/src/Bitbucket.Net/Models/Jira/ChangeSet.cs index e6d86a8..4f722d1 100644 --- a/src/Bitbucket.Net/Models/Jira/ChangeSet.cs +++ b/src/Bitbucket.Net/Models/Jira/ChangeSet.cs @@ -1,12 +1,12 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.Jira; public class ChangeSet { - public CommitParent? FromCommit { get; set; } - public Commit? ToCommit { get; set; } - public Changes? Changes { get; set; } - public Links? Links { get; set; } - public Repository? Repository { get; set; } + public CommitParent? FromCommit { get; init; } + public Commit? ToCommit { get; init; } + public Changes? Changes { get; init; } + public Links? Links { get; init; } + public Repository? Repository { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/Changes.cs b/src/Bitbucket.Net/Models/Jira/Changes.cs index 223dcb7..bead2f9 100644 --- a/src/Bitbucket.Net/Models/Jira/Changes.cs +++ b/src/Bitbucket.Net/Models/Jira/Changes.cs @@ -1,12 +1,12 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.Jira; public class Changes { - public int Size { get; set; } - public int Limit { get; set; } - public bool IsLastPage { get; set; } - public List? Values { get; set; } - public int Start { get; set; } + public int Size { get; init; } + public int Limit { get; init; } + public bool IsLastPage { get; init; } + public List? Values { get; init; } + public int Start { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/JiraIssue.cs b/src/Bitbucket.Net/Models/Jira/JiraIssue.cs index 405b1ee..6ec8b9e 100644 --- a/src/Bitbucket.Net/Models/Jira/JiraIssue.cs +++ b/src/Bitbucket.Net/Models/Jira/JiraIssue.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Jira; +namespace Bitbucket.Net.Models.Jira; public class JiraIssue { - public int CommentId { get; set; } - public string? IssueKey { get; set; } + public int CommentId { get; init; } + public string? IssueKey { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs index 84a597a..af72167 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs @@ -6,12 +6,12 @@ namespace Bitbucket.Net.Models.PersonalAccessTokens; public class AccessToken : AccessTokenCreate { - public string? Id { get; set; } + public string? Id { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset CreatedDate { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset LastAuthenticated { get; set; } - public User? User { get; set; } + public DateTimeOffset LastAuthenticated { get; init; } + public User? User { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset ExpiryDate { get; set; } + public DateTimeOffset ExpiryDate { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs index 233e5bf..999690f 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs @@ -1,12 +1,9 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Admin; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.PersonalAccessTokens; public class AccessTokenCreate { public string? Name { get; set; } - [JsonConverter(typeof(PermissionsListConverter))] public List? Permissions { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs index e851c02..3e0f52c 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.PersonalAccessTokens; +namespace Bitbucket.Net.Models.PersonalAccessTokens; public class FullAccessToken : AccessToken { - public string? Token { get; set; } + public string? Token { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs b/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs index 1c3a694..51275e4 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.RefRestrictions; +namespace Bitbucket.Net.Models.RefRestrictions; public class AccessKey { - public Key? Key { get; set; } + public Key? Key { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/Key.cs b/src/Bitbucket.Net/Models/RefRestrictions/Key.cs index 1fb6c97..dfc795a 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/Key.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/Key.cs @@ -1,8 +1,8 @@ -namespace Bitbucket.Net.Models.RefRestrictions; +namespace Bitbucket.Net.Models.RefRestrictions; public class Key { - public int Id { get; set; } - public string? Text { get; set; } - public string? Label { get; set; } + public int Id { get; init; } + public string? Text { get; init; } + public string? Label { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs index 428c99d..20a3a4a 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs @@ -1,12 +1,12 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Users; namespace Bitbucket.Net.Models.RefRestrictions; public class RefRestriction : RefRestrictionBase { - public int Id { get; set; } - public HookScope? Scope { get; set; } - public List? Users { get; set; } - public List? AccessKeys { get; set; } + public int Id { get; init; } + public HookScope? Scope { get; init; } + public List? Users { get; init; } + public List? AccessKeys { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs index 63ecddc..a976cdd 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs @@ -1,12 +1,9 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.DefaultReviewers; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.RefRestrictions; public abstract class RefRestrictionBase { - [JsonConverter(typeof(RefRestrictionTypesConverter))] public RefRestrictionTypes Type { get; set; } public RefMatcher? Matcher { get; set; } public List? Groups { get; set; } diff --git a/src/Bitbucket.Net/Models/RefSync/FullRef.cs b/src/Bitbucket.Net/Models/RefSync/FullRef.cs index 82c3c44..8685f15 100644 --- a/src/Bitbucket.Net/Models/RefSync/FullRef.cs +++ b/src/Bitbucket.Net/Models/RefSync/FullRef.cs @@ -1,9 +1,9 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.RefSync; public class FullRef : Ref { - public string? State { get; set; } - public bool Tag { get; set; } + public string? State { get; init; } + public bool Tag { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs b/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs index a3b1f2e..0982829 100644 --- a/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs +++ b/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs @@ -5,11 +5,11 @@ namespace Bitbucket.Net.Models.RefSync; public class RepositorySynchronizationStatus { - public bool Available { get; set; } - public bool Enabled { get; set; } + public bool Available { get; init; } + public bool Enabled { get; init; } [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset LastSync { get; set; } - public List? AheadRefs { get; set; } - public List? DivergedRefs { get; set; } - public List? OrphanedRefs { get; set; } + public DateTimeOffset LastSync { get; init; } + public List? AheadRefs { get; init; } + public List? DivergedRefs { get; init; } + public List? OrphanedRefs { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/Synchronize.cs b/src/Bitbucket.Net/Models/RefSync/Synchronize.cs index 180930f..0b6d452 100644 --- a/src/Bitbucket.Net/Models/RefSync/Synchronize.cs +++ b/src/Bitbucket.Net/Models/RefSync/Synchronize.cs @@ -1,12 +1,8 @@ -using Bitbucket.Net.Common.Converters; -using System.Text.Json.Serialization; - namespace Bitbucket.Net.Models.RefSync; public class Synchronize { public string? RefId { get; set; } - [JsonConverter(typeof(SynchronizeActionsConverter))] public SynchronizeActions Action { get; set; } public SynchronizeContext? Context { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs b/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs index 2086aa4..297580e 100644 --- a/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs +++ b/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.RefSync; +namespace Bitbucket.Net.Models.RefSync; public class SynchronizeContext { - public string? CommitMessage { get; set; } + public string? CommitMessage { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs b/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs index f557845..7f51008 100644 --- a/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs +++ b/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs @@ -1,6 +1,6 @@ -namespace Bitbucket.Net.Models.Ssh; +namespace Bitbucket.Net.Models.Ssh; public class Accesskeys { - public bool Enabled { get; set; } + public bool Enabled { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs b/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs index b54158e..79f9e96 100644 --- a/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs +++ b/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs @@ -1,7 +1,7 @@ -namespace Bitbucket.Net.Models.Ssh; +namespace Bitbucket.Net.Models.Ssh; public class Fingerprint { - public string? Algorithm { get; set; } - public string? Value { get; set; } + public string? Algorithm { get; init; } + public string? Value { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/KeyBase.cs b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs index e477df5..0115c93 100644 --- a/src/Bitbucket.Net/Models/Ssh/KeyBase.cs +++ b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs @@ -1,13 +1,10 @@ -using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.RefRestrictions; -using System.Text.Json.Serialization; namespace Bitbucket.Net.Models.Ssh; public abstract class KeyBase { - public Key? Key { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } + public Key? Key { get; init; } + public Permissions Permission { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs b/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs index 0125d4d..f87fbd8 100644 --- a/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs +++ b/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs @@ -1,8 +1,8 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.Ssh; public class ProjectKey : KeyBase { - public Project? Project { get; set; } + public Project? Project { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs b/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs index 7d6fb27..ab716be 100644 --- a/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs +++ b/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs @@ -1,8 +1,8 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; namespace Bitbucket.Net.Models.Ssh; public class RepositoryKey : KeyBase { - public Repository? Repository { get; set; } + public Repository? Repository { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/SshSettings.cs b/src/Bitbucket.Net/Models/Ssh/SshSettings.cs index 928cc09..14016b0 100644 --- a/src/Bitbucket.Net/Models/Ssh/SshSettings.cs +++ b/src/Bitbucket.Net/Models/Ssh/SshSettings.cs @@ -1,10 +1,10 @@ -namespace Bitbucket.Net.Models.Ssh; +namespace Bitbucket.Net.Models.Ssh; public class SshSettings { - public Accesskeys? AccessKeys { get; set; } - public string? BaseUrl { get; set; } - public bool Enabled { get; set; } - public Fingerprint? Fingerprint { get; set; } - public int Port { get; set; } + public Accesskeys? AccessKeys { get; init; } + public string? BaseUrl { get; init; } + public bool Enabled { get; init; } + public Fingerprint? Fingerprint { get; init; } + public int Port { get; init; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs index 8843715..b832e93 100644 --- a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs +++ b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.PersonalAccessTokens; using Flurl.Http; @@ -34,13 +33,15 @@ private IFlurlRequest GetPatUrl(string path) => GetPatUrl() /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of access tokens. - public async Task> GetUserAccessTokensAsync(string userSlug, + public Task> GetUserAccessTokensAsync(string userSlug, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -48,16 +49,8 @@ public async Task> GetUserAccessTokensAsync(string user ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetPatUrl($"/users/{userSlug}") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetPatUrl($"/users/{userSlug}"), queryParamValues, maxPages, cancellationToken); } /// @@ -69,6 +62,8 @@ public async Task> GetUserAccessTokensAsync(string user /// The created access token including secret. public async Task CreateAccessTokenAsync(string userSlug, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var response = await GetPatUrl($"/users/{userSlug}") .SendAsync(HttpMethod.Put, CreateJsonContent(accessToken), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -86,6 +81,9 @@ public async Task CreateAccessTokenAsync(string userSlug, Acces /// The access token details. public async Task GetUserAccessTokenAsync(string userSlug, string tokenId, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + ArgumentException.ThrowIfNullOrWhiteSpace(tokenId); + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken) @@ -104,6 +102,9 @@ public async Task GetUserAccessTokenAsync(string userSlug, string t /// The updated access token details. public async Task ChangeUserAccessTokenAsync(string userSlug, string tokenId, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + ArgumentException.ThrowIfNullOrWhiteSpace(tokenId); + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") .SendAsync(HttpMethod.Post, CreateJsonContent(accessToken), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -120,6 +121,9 @@ public async Task ChangeUserAccessTokenAsync(string userSlug, strin /// true if the token was deleted; otherwise, false. public async Task DeleteUserAccessTokenAsync(string userSlug, string tokenId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + ArgumentException.ThrowIfNullOrWhiteSpace(tokenId); + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs index 9f13adf..534b775 100644 --- a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.RefRestrictions; using Flurl.Http; @@ -37,7 +36,7 @@ private IFlurlRequest GetRefRestrictionsUrl(string path) => GetRefRestrictionsUr /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of reference restrictions. - public async Task> GetProjectRefRestrictionsAsync(string projectKey, + public Task> GetProjectRefRestrictionsAsync(string projectKey, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, @@ -47,6 +46,8 @@ public async Task> GetProjectRefRestrictionsAsync(st int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), @@ -57,16 +58,8 @@ public async Task> GetProjectRefRestrictionsAsync(st ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions"), queryParamValues, maxPages, cancellationToken); } /// @@ -76,14 +69,17 @@ public async Task> GetProjectRefRestrictionsAsync(st /// Token to cancel the operation. /// The reference restrictions to create. /// The created reference restrictions. - public async Task> CreateProjectRefRestrictionsAsync(string projectKey, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) + public async Task> CreateProjectRefRestrictionsAsync(string projectKey, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") .SendAsync(HttpMethod.Post, CreateJsonContent(refRestrictions), cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } /// @@ -92,8 +88,10 @@ public async Task> CreateProjectRefRestrictionsAsync /// The project key. /// The reference restrictions to create. /// The created reference restrictions. - public async Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions) + public async Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + return await CreateProjectRefRestrictionsAsync(projectKey, default, refRestrictions).ConfigureAwait(false); } @@ -106,6 +104,8 @@ public async Task> CreateProjectRefRestrictionsAsync /// The created reference restriction. public async Task CreateProjectRefRestrictionAsync(string projectKey, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") .SendAsync(HttpMethod.Post, CreateJsonContent(refRestriction), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -123,6 +123,8 @@ public async Task CreateProjectRefRestrictionAsync(string projec /// The requested reference restriction. public async Task GetProjectRefRestrictionAsync(string projectKey, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken) @@ -140,6 +142,8 @@ public async Task GetProjectRefRestrictionAsync(string projectKe /// true if the restriction was deleted; otherwise, false. public async Task DeleteProjectRefRestrictionAsync(string projectKey, int refRestrictionId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -161,7 +165,7 @@ public async Task DeleteProjectRefRestrictionAsync(string projectKey, int /// Optional avatar size for returned users. /// Token to cancel the operation. /// A collection of reference restrictions. - public async Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, + public Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, @@ -171,6 +175,9 @@ public async Task> GetRepositoryRefRestrictionsAsync int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), @@ -181,16 +188,8 @@ public async Task> GetRepositoryRefRestrictionsAsync ["avatarSize"] = avatarSize, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions"), queryParamValues, maxPages, cancellationToken); } /// @@ -201,14 +200,18 @@ public async Task> GetRepositoryRefRestrictionsAsync /// Token to cancel the operation. /// The reference restrictions to create. /// The created reference restrictions. - public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) + public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") .SendAsync(HttpMethod.Post, CreateJsonContent(refRestrictions), cancellationToken: cancellationToken) .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + var items = await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + return items.ToList(); } /// @@ -218,8 +221,11 @@ public async Task> CreateRepositoryRefRestrictionsAs /// The repository slug. /// The reference restrictions to create. /// The created reference restrictions. - public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, params RefRestrictionCreate[] refRestrictions) + public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, params RefRestrictionCreate[] refRestrictions) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + return await CreateRepositoryRefRestrictionsAsync(projectKey, repositorySlug, default, refRestrictions).ConfigureAwait(false); } @@ -233,6 +239,9 @@ public async Task> CreateRepositoryRefRestrictionsAs /// The created reference restriction. public async Task CreateRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") .SendAsync(HttpMethod.Post, CreateJsonContent(refRestriction), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -252,6 +261,9 @@ public async Task CreateRepositoryRefRestrictionAsync(string pro public async Task GetRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") .SetQueryParam("avatarSize", avatarSize) .GetAsync(cancellationToken) @@ -270,6 +282,9 @@ public async Task GetRepositoryRefRestrictionAsync(string projec /// true if the restriction was deleted; otherwise, false. public async Task DeleteRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/RefRestrictions/IRefRestrictionOperations.cs b/src/Bitbucket.Net/RefRestrictions/IRefRestrictionOperations.cs new file mode 100644 index 0000000..321697b --- /dev/null +++ b/src/Bitbucket.Net/RefRestrictions/IRefRestrictionOperations.cs @@ -0,0 +1,26 @@ +using Bitbucket.Net.Models.RefRestrictions; +using Bitbucket.Net.Models.RefSync; + +namespace Bitbucket.Net; + +/// +/// Ref restriction and ref sync operations. +/// +public interface IRefRestrictionOperations +{ + Task> GetProjectRefRestrictionsAsync(string projectKey, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> CreateProjectRefRestrictionsAsync(string projectKey, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions); + Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions); + Task CreateProjectRefRestrictionAsync(string projectKey, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default); + Task GetProjectRefRestrictionAsync(string projectKey, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task DeleteProjectRefRestrictionAsync(string projectKey, int refRestrictionId, CancellationToken cancellationToken = default); + Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, int? maxPages = null, int? limit = null, int? start = null, int? avatarSize = null, CancellationToken cancellationToken = default); + Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions); + Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, params RefRestrictionCreate[] refRestrictions); + Task CreateRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default); + Task GetRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default); + Task DeleteRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, CancellationToken cancellationToken = default); + Task GetRepositorySynchronizationStatusAsync(string projectKey, string repositorySlug, string? at = null, CancellationToken cancellationToken = default); + Task EnableRepositorySynchronizationAsync(string projectKey, string repositorySlug, bool enabled, CancellationToken cancellationToken = default); + Task SynchronizeRepositoryAsync(string projectKey, string repositorySlug, Synchronize synchronize, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/RefSync/BitbucketClient.cs b/src/Bitbucket.Net/RefSync/BitbucketClient.cs index abbb0ab..c47d62c 100644 --- a/src/Bitbucket.Net/RefSync/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefSync/BitbucketClient.cs @@ -34,6 +34,9 @@ private IFlurlRequest GetRefSyncUrl(string path) => GetRefSyncUrl() public async Task GetRepositorySynchronizationStatusAsync(string projectKey, string repositorySlug, string? at = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") .SetQueryParam("at", at) .GetAsync(cancellationToken: cancellationToken) @@ -52,6 +55,9 @@ public async Task GetRepositorySynchronizationS /// The updated repository synchronization status. public async Task EnableRepositorySynchronizationAsync(string projectKey, string repositorySlug, bool enabled, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var data = new { enabled = BitbucketHelpers.BoolToString(enabled), @@ -74,6 +80,9 @@ public async Task EnableRepositorySynchronizati /// The result of the synchronization. public async Task SynchronizeRepositoryAsync(string projectKey, string repositorySlug, Synchronize synchronize, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") .SendAsync(HttpMethod.Post, CreateJsonContent(synchronize), cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs index 2ae2c2a..5bb65c1 100644 --- a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs +++ b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs @@ -1,3 +1,4 @@ +using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Common.Models; // Search using Bitbucket.Net.Common.Models.Search; @@ -7,11 +8,13 @@ using Bitbucket.Net.Models.Branches; // Builds using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Builds.Requests; // Core - Admin using Bitbucket.Net.Models.Core.Admin; // Core - Logs // Core - Projects using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; // Core - Tasks using Bitbucket.Net.Models.Core.Tasks; // Core - Users @@ -40,8 +43,9 @@ namespace Bitbucket.Net.Serialization; /// /// /// -/// This context is combined with -/// to provide a fallback for any types not explicitly registered (edge cases, future additions). +/// This context is the sole type-info resolver — there is no reflection fallback. +/// Any type not registered here will throw +/// at the call site, ensuring missing registrations are caught immediately. /// /// /// Custom converters (UnixDateTimeOffsetConverter, etc.) continue to work with source generation. @@ -50,7 +54,8 @@ namespace Bitbucket.Net.Serialization; [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization, + Converters = [typeof(UnixDateTimeOffsetConverter), typeof(NullableUnixDateTimeOffsetConverter), typeof(BitbucketEnumConverterFactory)])] // ============================================================================ // Common Models @@ -314,7 +319,12 @@ namespace Bitbucket.Net.Serialization; // ============================================================================ // Collection Types (for various API responses) // ============================================================================ +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -332,6 +342,23 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Dictionary))] +// ============================================================================ +// Request DTOs +// ============================================================================ +[JsonSerializable(typeof(AssociateBuildStatusRequest))] +[JsonSerializable(typeof(CreateBranchRequest))] +[JsonSerializable(typeof(CreateProjectRequest))] +[JsonSerializable(typeof(CreatePullRequestRequest))] +[JsonSerializable(typeof(CreateRepositoryRequest))] +[JsonSerializable(typeof(CreateTaskRequest))] +[JsonSerializable(typeof(CreateWebHookRequest))] +[JsonSerializable(typeof(ForkRepositoryRequest))] +[JsonSerializable(typeof(MergePullRequestRequest))] +[JsonSerializable(typeof(UpdateProjectRequest))] +[JsonSerializable(typeof(UpdatePullRequestRequest))] +[JsonSerializable(typeof(UpdateTaskRequest))] +[JsonSerializable(typeof(UpdateWebHookRequest))] + public partial class BitbucketJsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/Bitbucket.Net/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index fa1605f..5b35257 100644 --- a/src/Bitbucket.Net/Ssh/BitbucketClient.cs +++ b/src/Bitbucket.Net/Ssh/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Models.Ssh; @@ -51,7 +50,7 @@ private IFlurlRequest GetSshUrl(string path) => GetSshUrl() /// true if deletion succeeded; otherwise, false. public async Task DeleteProjectsReposKeysAsync(int keyId, CancellationToken cancellationToken, params string[] projectsOrRepos) { - var json = JsonSerializer.Serialize(projectsOrRepos); + var json = JsonSerializer.Serialize(projectsOrRepos, s_writeJsonOptions); var response = await GetKeysUrl($"/ssh/{keyId}") .WithHeader("Content-Type", "application/json") .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) @@ -80,7 +79,7 @@ public async Task DeleteProjectsReposKeysAsync(int keyId, params string[] /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of project keys. - public async Task> GetProjectKeysAsync(int keyId, + public Task> GetProjectKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, @@ -92,16 +91,8 @@ public async Task> GetProjectKeysAsync(int keyId, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetKeysUrl($"/ssh/{keyId}/projects") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetKeysUrl($"/ssh/{keyId}/projects"), queryParamValues, maxPages, cancellationToken); } /// @@ -115,7 +106,7 @@ public async Task> GetProjectKeysAsync(int keyId, /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of project keys. - public async Task> GetProjectKeysAsync(string projectKey, + public Task> GetProjectKeysAsync(string projectKey, string? filter = null, Permissions? permission = null, int? maxPages = null, @@ -123,6 +114,8 @@ public async Task> GetProjectKeysAsync(string projectKey int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -131,16 +124,8 @@ public async Task> GetProjectKeysAsync(string projectKey ["permission"] = BitbucketHelpers.PermissionToString(permission), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetKeysUrl($"/projects/{projectKey}/ssh") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetKeysUrl($"/projects/{projectKey}/ssh"), queryParamValues, maxPages, cancellationToken); } /// @@ -153,6 +138,9 @@ public async Task> GetProjectKeysAsync(string projectKey /// The created project key. public async Task CreateProjectKeyAsync(string projectKey, string keyText, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(keyText); + var data = new { key = new { text = keyText }, @@ -175,6 +163,8 @@ public async Task CreateProjectKeyAsync(string projectKey, string ke /// The requested project key. public async Task GetProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -191,6 +181,8 @@ public async Task GetProjectKeyAsync(string projectKey, int keyId, C /// true if the key was deleted; otherwise, false. public async Task DeleteProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -208,6 +200,8 @@ public async Task DeleteProjectKeyAsync(string projectKey, int keyId, Canc /// The updated project key. public async Task UpdateProjectKeyPermissionAsync(string projectKey, int keyId, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") .PutAsync(new StringContent(""), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -224,7 +218,7 @@ public async Task UpdateProjectKeyPermissionAsync(string projectKey, /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of repository keys. - public async Task> GetRepoKeysAsync(int keyId, + public Task> GetRepoKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, @@ -236,16 +230,8 @@ public async Task> GetRepoKeysAsync(int keyId, ["start"] = start, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetKeysUrl($"/ssh/{keyId}/repos") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetKeysUrl($"/ssh/{keyId}/repos"), queryParamValues, maxPages, cancellationToken); } /// @@ -261,7 +247,7 @@ public async Task> GetRepoKeysAsync(int keyId, /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of repository keys. - public async Task> GetRepoKeysAsync(string projectKey, string repositorySlug, + public Task> GetRepoKeysAsync(string projectKey, string repositorySlug, string? filter = null, bool? effective = null, Permissions? permission = null, @@ -270,6 +256,9 @@ public async Task> GetRepoKeysAsync(string projectKey int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -279,16 +268,8 @@ public async Task> GetRepoKeysAsync(string projectKey ["permission"] = BitbucketHelpers.PermissionToString(permission), }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh"), queryParamValues, maxPages, cancellationToken); } /// @@ -302,6 +283,10 @@ public async Task> GetRepoKeysAsync(string projectKey /// The created repository key. public async Task CreateRepoKeyAsync(string projectKey, string repositorySlug, string keyText, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(keyText); + var data = new { key = new { text = keyText }, @@ -325,6 +310,9 @@ public async Task CreateRepoKeyAsync(string projectKey, string re /// The requested repository key. public async Task GetRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") .GetAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -342,6 +330,9 @@ public async Task GetRepoKeyAsync(string projectKey, string repos /// true if the key was deleted; otherwise, false. public async Task DeleteRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") .DeleteAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -360,6 +351,9 @@ public async Task DeleteRepoKeyAsync(string projectKey, string repositoryS /// The updated repository key. public async Task UpdateRepoKeyPermissionAsync(string projectKey, string repositorySlug, int keyId, Permissions permission, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") .PutAsync(new StringContent(""), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -376,7 +370,7 @@ public async Task UpdateRepoKeyPermissionAsync(string projectKey, /// Optional starting index for pagination. /// Token to cancel the operation. /// A collection of SSH keys. - public async Task> GetUserKeysAsync(string? userSlug = null, + public Task> GetUserKeysAsync(string? userSlug = null, int? maxPages = null, int? limit = null, int? start = null, @@ -389,16 +383,8 @@ public async Task> GetUserKeysAsync(string? userSlug = null, ["user"] = userSlug, }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - { - var response = await GetSshUrl("/keys") - .SetQueryParams(qpv) - .GetAsync(ct) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); - }, cancellationToken) - .ConfigureAwait(false); + return GetPagedAsync( + GetSshUrl("/keys"), queryParamValues, maxPages, cancellationToken); } /// @@ -410,6 +396,8 @@ public async Task> GetUserKeysAsync(string? userSlug = null, /// The created SSH key. public async Task CreateUserKeyAsync(string keyText, string? userSlug = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(keyText); + var response = await GetSshUrl("/keys") .SetQueryParam("user", userSlug) .SendAsync(HttpMethod.Post, CreateJsonContent(new { text = keyText }), cancellationToken: cancellationToken) diff --git a/src/Bitbucket.Net/Ssh/ISshOperations.cs b/src/Bitbucket.Net/Ssh/ISshOperations.cs new file mode 100644 index 0000000..1f73c67 --- /dev/null +++ b/src/Bitbucket.Net/Ssh/ISshOperations.cs @@ -0,0 +1,31 @@ +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.RefRestrictions; +using Bitbucket.Net.Models.Ssh; + +namespace Bitbucket.Net; + +/// +/// SSH key operations. +/// +public interface ISshOperations +{ + Task DeleteProjectsReposKeysAsync(int keyId, CancellationToken cancellationToken, params string[] projectsOrRepos); + Task DeleteProjectsReposKeysAsync(int keyId, params string[] projectsOrRepos); + Task> GetProjectKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetProjectKeysAsync(string projectKey, string? filter = null, Permissions? permission = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateProjectKeyAsync(string projectKey, string keyText, Permissions permission, CancellationToken cancellationToken = default); + Task GetProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default); + Task DeleteProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default); + Task UpdateProjectKeyPermissionAsync(string projectKey, int keyId, Permissions permission, CancellationToken cancellationToken = default); + Task> GetRepoKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task> GetRepoKeysAsync(string projectKey, string repositorySlug, string? filter = null, bool? effective = null, Permissions? permission = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateRepoKeyAsync(string projectKey, string repositorySlug, string keyText, Permissions permission, CancellationToken cancellationToken = default); + Task GetRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default); + Task DeleteRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default); + Task UpdateRepoKeyPermissionAsync(string projectKey, string repositorySlug, int keyId, Permissions permission, CancellationToken cancellationToken = default); + Task> GetUserKeysAsync(string? userSlug = null, int? maxPages = null, int? limit = null, int? start = null, CancellationToken cancellationToken = default); + Task CreateUserKeyAsync(string keyText, string? userSlug = null, CancellationToken cancellationToken = default); + Task DeleteUserKeysAsync(string? userSlug = null, CancellationToken cancellationToken = default); + Task DeleteUserKeyAsync(int keyId, CancellationToken cancellationToken = default); + Task GetSshSettingsAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj b/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj index 7b08685..bb227a1 100644 --- a/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj +++ b/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj @@ -16,6 +16,7 @@ runtime; build; native; contentfiles; analyzers + diff --git a/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs b/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs index cdf59ce..ca51e62 100644 --- a/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs +++ b/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs @@ -246,6 +246,51 @@ public static WireMockServer SetupRateLimited(this WireMockServer server, string new Error { Message = "Rate limit exceeded" }); } + public static WireMockServer SetupRateLimitedWithHeaders( + this WireMockServer server, + string path, + string? retryAfter = null, + string? rateLimitLimit = null, + string? rateLimitRemaining = null, + string? rateLimitReset = null) + { + var json = JsonSerializer.Serialize( + new { errors = new[] { new Error { Message = "Rate limit exceeded" } } }, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + var responseBuilder = Response.Create() + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Content-Type", "application/json") + .WithBody(json); + + if (retryAfter is not null) + { + responseBuilder = responseBuilder.WithHeader("Retry-After", retryAfter); + } + + if (rateLimitLimit is not null) + { + responseBuilder = responseBuilder.WithHeader("X-RateLimit-Limit", rateLimitLimit); + } + + if (rateLimitRemaining is not null) + { + responseBuilder = responseBuilder.WithHeader("X-RateLimit-Remaining", rateLimitRemaining); + } + + if (rateLimitReset is not null) + { + responseBuilder = responseBuilder.WithHeader("X-RateLimit-Reset", rateLimitReset); + } + + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(responseBuilder); + + return server; + } + public static WireMockServer SetupErrorWithJsonBody(this WireMockServer server, string path, HttpStatusCode statusCode, params Error[] errors) { var json = JsonSerializer.Serialize(new { errors }, new JsonSerializerOptions diff --git a/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs index 6a92523..0815916 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs @@ -1,4 +1,5 @@ using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -49,7 +50,7 @@ public async Task CreateBranchAsync_ReturnsBranch() _fixture.Server.SetupCreateBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); var client = _fixture.CreateClient(); - var branchInfo = new BranchInfo + var request = new CreateBranchRequest { Name = "feature-test", StartPoint = "refs/heads/master" @@ -58,7 +59,7 @@ public async Task CreateBranchAsync_ReturnsBranch() var branch = await client.CreateBranchAsync( TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, - branchInfo); + request); Assert.NotNull(branch); Assert.Equal("refs/heads/feature-test", branch.Id); diff --git a/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs index 59b6ba5..37d872e 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs @@ -1,4 +1,4 @@ -using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Builds.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -46,7 +46,7 @@ public async Task AssociateBuildStatusWithCommitAsync_ReturnsTrue() _fixture.Server.SetupAssociateBuildStatus(TestConstants.TestCommitId); var client = _fixture.CreateClient(); - var buildStatus = new BuildStatus + var request = new AssociateBuildStatusRequest { Key = "build-125", State = "SUCCESSFUL", @@ -57,7 +57,7 @@ public async Task AssociateBuildStatusWithCommitAsync_ReturnsTrue() var result = await client.AssociateBuildStatusWithCommitAsync( TestConstants.TestCommitId, - buildStatus); + request); Assert.True(result); } diff --git a/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs index 599b2de..4524ba4 100644 --- a/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs @@ -1,4 +1,4 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Flurl.Http; using Xunit; @@ -46,7 +46,7 @@ public async Task CreateProjectAsync_PreCancelledToken_ThrowsOperationCanceled() using var cts = new CancellationTokenSource(); cts.Cancel(); - var definition = new ProjectDefinition { Key = TestConstants.TestProjectKey, Name = TestConstants.TestProjectName }; + var definition = new CreateProjectRequest { Key = TestConstants.TestProjectKey, Name = TestConstants.TestProjectName }; await AssertCancellationPropagatedAsync( () => client.CreateProjectAsync(definition, cts.Token)); diff --git a/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs index eb3ff38..f24a9f1 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs @@ -155,6 +155,65 @@ public async Task GetProjectAsync_WhenRateLimited_ThrowsException() Assert.Equal(HttpStatusCode.TooManyRequests, exception.StatusCode); } + [Fact] + public async Task GetProjectAsync_WhenRateLimitedWithHeaders_ExposesAllProperties() + { + _fixture.Reset(); + var projectKey = "RATELIMIT"; + _fixture.Server.SetupRateLimitedWithHeaders( + $"{ApiBasePath}/projects/{projectKey}", + retryAfter: "30", + rateLimitLimit: "100", + rateLimitRemaining: "0", + rateLimitReset: "1700000000"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.TooManyRequests, exception.StatusCode); + Assert.Equal(TimeSpan.FromSeconds(30), exception.RetryAfter); + Assert.Equal(100, exception.RateLimit); + Assert.Equal(0, exception.RateLimitRemaining); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1700000000), exception.RateLimitReset); + } + + [Fact] + public async Task GetProjectAsync_WhenRateLimitedWithPartialHeaders_MissingAreNull() + { + _fixture.Reset(); + var projectKey = "PARTIALRL"; + _fixture.Server.SetupRateLimitedWithHeaders( + $"{ApiBasePath}/projects/{projectKey}", + retryAfter: "60"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(TimeSpan.FromSeconds(60), exception.RetryAfter); + Assert.Null(exception.RateLimit); + Assert.Null(exception.RateLimitRemaining); + Assert.Null(exception.RateLimitReset); + } + + [Fact] + public async Task GetProjectAsync_WhenRateLimitedWithNoHeaders_PropertiesAreNull() + { + _fixture.Reset(); + var projectKey = "NORLHDR"; + _fixture.Server.SetupRateLimited($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Null(exception.RetryAfter); + Assert.Null(exception.RateLimit); + Assert.Null(exception.RateLimitRemaining); + Assert.Null(exception.RateLimitReset); + } + [Fact] public async Task GetProjectAsync_WhenErrorHasJsonBody_PopulatesErrorsAndContext() { diff --git a/test/Bitbucket.Net.Tests/MockTests/FluentQueryBuilderMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/FluentQueryBuilderMockTests.cs new file mode 100644 index 0000000..f79925e --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/FluentQueryBuilderMockTests.cs @@ -0,0 +1,234 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Tests.Infrastructure; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class FluentQueryBuilderMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; + + private BitbucketClient CreateClient() + { + _fixture.Reset(); + return _fixture.CreateClient(); + } + + private WireMock.Server.WireMockServer Server => _fixture.Server; + + [Fact] + public async Task PullRequestQueryBuilder_DefaultParams_ReturnsPullRequests() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/pull-requests") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":1,"limit":25,"isLastPage":true,"values":[{"id":1,"title":"Test PR","state":"OPEN","open":true,"closed":false}]}""")); + + var result = await client.PullRequests("PRJ", "my-repo").GetAsync(); + + Assert.NotEmpty(result); + Assert.Equal("Test PR", result[0].Title); + } + + [Fact] + public async Task PullRequestQueryBuilder_WithAllOptions_AppliesQueryParams() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/pull-requests") + .WithParam("state", "MERGED") + .WithParam("order", "OLDEST") + .WithParam("direction", "OUTGOING") + .WithParam("at", "refs/heads/feature") + .WithParam("limit", "50") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":0,"limit":50,"isLastPage":true,"values":[]}""")); + + var result = await client.PullRequests("PRJ", "my-repo") + .InState(PullRequestStates.Merged) + .OrderBy(PullRequestOrders.Oldest) + .WithDirection(PullRequestDirections.Outgoing) + .AtBranch("refs/heads/feature") + .PageSize(50) + .GetAsync(); + + Assert.Empty(result); + } + + [Fact] + public async Task PullRequestQueryBuilder_StreamAsync_YieldsItems() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/pull-requests") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":2,"limit":25,"isLastPage":true,"values":[{"id":1,"title":"PR 1","state":"OPEN","open":true,"closed":false},{"id":2,"title":"PR 2","state":"OPEN","open":true,"closed":false}]}""")); + + var items = new List(); + await foreach (var pr in client.PullRequests("PRJ", "my-repo").StreamAsync()) + { + items.Add(pr); + } + + Assert.Equal(2, items.Count); + Assert.Equal("PR 1", items[0].Title); + Assert.Equal("PR 2", items[1].Title); + } + + [Fact] + public async Task CommitQueryBuilder_DefaultParams_ReturnsCommits() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/commits") + .WithParam("until", "HEAD") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":1,"limit":25,"isLastPage":true,"values":[{"id":"abc123","message":"Initial commit"}]}""")); + + var result = await client.Commits("PRJ", "my-repo", "HEAD").GetAsync(); + + Assert.NotEmpty(result); + Assert.Equal("abc123", result[0].Id); + } + + [Fact] + public async Task CommitQueryBuilder_WithOptions_AppliesQueryParams() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/commits") + .WithParam("until", "main") + .WithParam("since", "v1.0") + .WithParam("path", "src/file.cs") + .WithParam("merges", "include") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":0,"limit":25,"isLastPage":true,"values":[]}""")); + + var result = await client.Commits("PRJ", "my-repo", "main") + .Since("v1.0") + .AtPath("src/file.cs") + .Merges(MergeCommits.Include) + .GetAsync(); + + Assert.Empty(result); + } + + [Fact] + public async Task BranchQueryBuilder_DefaultParams_ReturnsBranches() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/branches") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":1,"limit":25,"isLastPage":true,"values":[{"id":"refs/heads/main","displayId":"main","isDefault":true}]}""")); + + var result = await client.Branches("PRJ", "my-repo").GetAsync(); + + Assert.NotEmpty(result); + Assert.Equal("main", result[0].DisplayId); + } + + [Fact] + public async Task BranchQueryBuilder_WithOptions_AppliesQueryParams() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects/PRJ/repos/my-repo/branches") + .WithParam("filterText", "feature") + .WithParam("orderBy", "MODIFICATION") + .WithParam("details", "true") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":0,"limit":25,"isLastPage":true,"values":[]}""")); + + var result = await client.Branches("PRJ", "my-repo") + .FilterBy("feature") + .OrderBy(BranchOrderBy.Modification) + .WithDetails() + .GetAsync(); + + Assert.Empty(result); + } + + [Fact] + public async Task ProjectQueryBuilder_DefaultParams_ReturnsProjects() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":1,"limit":25,"isLastPage":true,"values":[{"key":"PRJ","name":"My Project"}]}""")); + + var result = await client.Projects().GetAsync(); + + Assert.NotEmpty(result); + Assert.Equal("PRJ", result[0].Key); + } + + [Fact] + public async Task ProjectQueryBuilder_WithNameFilter_AppliesQueryParam() + { + var client = CreateClient(); + Server + .Given(Request.Create() + .WithPath("/rest/api/1.0/projects") + .WithParam("name", "Test") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("""{"size":0,"limit":25,"isLastPage":true,"values":[]}""")); + + var result = await client.Projects() + .NameFilter("Test") + .GetAsync(); + + Assert.Empty(result); + } + + [Fact] + public void PullRequestQueryBuilder_NullProjectKey_Throws() + { + var client = CreateClient(); + Assert.Throws(() => client.PullRequests(null!, "repo")); + } + + [Fact] + public void CommitQueryBuilder_NullUntil_Throws() + { + var client = CreateClient(); + Assert.Throws(() => client.Commits("PRJ", "repo", null!)); + } + + [Fact] + public void BranchQueryBuilder_EmptyRepoSlug_Throws() + { + var client = CreateClient(); + Assert.Throws(() => client.Branches("PRJ", "")); + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs index 3a2ac0b..8b3f72e 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs @@ -1,4 +1,4 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -15,14 +15,14 @@ public async Task CreateProjectAsync_ReturnsCreatedProject() _fixture.Server.SetupCreateProject(); var client = _fixture.CreateClient(); - var projectDef = new ProjectDefinition + var request = new CreateProjectRequest { Key = TestConstants.TestProjectKey, Name = TestConstants.TestProjectName, Description = "Created by unit test" }; - var project = await client.CreateProjectAsync(projectDef); + var project = await client.CreateProjectAsync(request); Assert.NotNull(project); Assert.Equal(TestConstants.TestProjectKey, project.Key); @@ -36,13 +36,13 @@ public async Task UpdateProjectAsync_ReturnsUpdatedProject() _fixture.Server.SetupUpdateProject(TestConstants.TestProjectKey); var client = _fixture.CreateClient(); - var projectDef = new ProjectDefinition + var request = new UpdateProjectRequest { Name = "Updated Name", Description = "Updated by unit test" }; - var project = await client.UpdateProjectAsync(TestConstants.TestProjectKey, projectDef); + var project = await client.UpdateProjectAsync(TestConstants.TestProjectKey, request); Assert.NotNull(project); Assert.Equal(TestConstants.TestProjectKey, project.Key); diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs index 421a183..3f8c7cc 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs @@ -1,4 +1,5 @@ using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -15,7 +16,7 @@ public async Task CreatePullRequestAsync_ReturnsCreatedPullRequest() _fixture.Server.SetupCreatePullRequest(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); var client = _fixture.CreateClient(); - var prInfo = new PullRequestInfo + var request = new CreatePullRequestRequest { Title = "Test PR", Description = "Test description", @@ -42,7 +43,7 @@ public async Task CreatePullRequestAsync_ReturnsCreatedPullRequest() var pullRequest = await client.CreatePullRequestAsync( TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, - prInfo); + request); Assert.NotNull(pullRequest); Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); @@ -58,7 +59,7 @@ public async Task UpdatePullRequestAsync_ReturnsUpdatedPullRequest() TestConstants.TestPullRequestId); var client = _fixture.CreateClient(); - var prUpdate = new PullRequestUpdate + var request = new UpdatePullRequestRequest { Title = "Updated Title", Version = 0 @@ -68,7 +69,7 @@ public async Task UpdatePullRequestAsync_ReturnsUpdatedPullRequest() TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId, - prUpdate); + request); Assert.NotNull(pullRequest); Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs index a09f417..43ccfc3 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs @@ -1,4 +1,5 @@ using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -153,7 +154,7 @@ public async Task CreatePullRequestAsync_ReturnsPullRequest() TestConstants.TestRepositorySlug); var client = _fixture.CreateClient(); - var prInfo = new PullRequestInfo + var request = new CreatePullRequestRequest { Title = "New PR", Description = "Description", @@ -164,7 +165,7 @@ public async Task CreatePullRequestAsync_ReturnsPullRequest() var result = await client.CreatePullRequestAsync( TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, - prInfo); + request); Assert.NotNull(result); Assert.Equal(TestConstants.TestPullRequestId, result.Id); @@ -180,9 +181,8 @@ public async Task UpdatePullRequestAsync_ReturnsPullRequest() TestConstants.TestPullRequestId); var client = _fixture.CreateClient(); - var update = new PullRequestUpdate + var request = new UpdatePullRequestRequest { - Id = (int)TestConstants.TestPullRequestId, Version = 0, Title = "Updated Title" }; @@ -191,7 +191,7 @@ public async Task UpdatePullRequestAsync_ReturnsPullRequest() TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId, - update); + request); Assert.NotNull(result); } diff --git a/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs index b976e99..4c67b6e 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs @@ -1,3 +1,5 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -16,8 +18,7 @@ public async Task CreateProjectRepositoryAsync_CreatesAndReturnsRepository() var result = await client.CreateProjectRepositoryAsync( TestConstants.TestProjectKey, - "new-repo", - "git"); + new CreateRepositoryRequest { Name = "new-repo", ScmId = "git" }); Assert.NotNull(result); Assert.Equal("test-repo", result.Slug); @@ -63,8 +64,7 @@ public async Task CreateProjectRepositoryForkAsync_CreatesAndReturnsFork() var result = await client.CreateProjectRepositoryForkAsync( TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, - targetProjectKey: "FORK", - targetName: "forked-repo"); + new ForkRepositoryRequest { Name = "forked-repo", Project = new ProjectRef { Key = "FORK" } }); Assert.NotNull(result); } diff --git a/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs index 672b89a..91e56e0 100644 --- a/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs @@ -1,3 +1,4 @@ +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Models.Core.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -15,13 +16,13 @@ public async Task CreateTaskAsync_ReturnsCreatedTask() _fixture.Server.SetupCreateTask(); var client = _fixture.CreateClient(); - var taskInfo = new TaskInfo + var request = new CreateTaskRequest { Anchor = new TaskBasicAnchor { Id = 101, Type = "COMMENT" }, Text = "Fix the null pointer exception" }; - var task = await client.CreateTaskAsync(taskInfo); + var task = await client.CreateTaskAsync(request); Assert.NotNull(task); Assert.Equal(1, task.Id); @@ -54,7 +55,7 @@ public async Task UpdateTaskAsync_ReturnsUpdatedTask() _fixture.Server.SetupUpdateTask(1); var client = _fixture.CreateClient(); - var task = await client.UpdateTaskAsync(1, "Updated task text"); + var task = await client.UpdateTaskAsync(1, new UpdateTaskRequest { Text = "Updated task text" }); Assert.NotNull(task); Assert.Equal(1, task.Id); diff --git a/test/Bitbucket.Net.Tests/MockTests/TracingMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/TracingMockTests.cs new file mode 100644 index 0000000..47b0cbe --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/TracingMockTests.cs @@ -0,0 +1,133 @@ +using Bitbucket.Net.Common.Exceptions; +using Bitbucket.Net.Tests.Infrastructure; +using System.Diagnostics; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class TracingMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectAsync_CreatesActivityWithOTelTags() + { + _fixture.Reset(); + _fixture.Server.SetupGetProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + var activities = new List(); + + using var listener = CreateListener(activities); + + await client.GetProjectAsync(TestConstants.TestProjectKey); + + var activity = Assert.Single(activities); + Assert.Equal("GET", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("GET", activity.GetTagItem("http.request.method")); + Assert.Equal(200, activity.GetTagItem("http.response.status_code")); + Assert.NotNull(activity.GetTagItem("url.full")); + Assert.NotNull(activity.GetTagItem("server.address")); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + + [Fact] + public async Task GetProjectAsync_SetsBitbucketProjectKeyTag() + { + _fixture.Reset(); + _fixture.Server.SetupGetProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + var activities = new List(); + + using var listener = CreateListener(activities); + + await client.GetProjectAsync(TestConstants.TestProjectKey); + + var activity = Assert.Single(activities); + Assert.Equal(TestConstants.TestProjectKey, activity.GetTagItem("bitbucket.project_key")); + } + + [Fact] + public async Task GetRepositoryAsync_SetsBitbucketRepoSlugTag() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + var activities = new List(); + + using var listener = CreateListener(activities); + + await client.GetProjectRepositoryAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + + var activity = Assert.Single(activities); + Assert.Equal(TestConstants.TestProjectKey, activity.GetTagItem("bitbucket.project_key")); + Assert.Equal(TestConstants.TestRepositorySlug, activity.GetTagItem("bitbucket.repository_slug")); + } + + [Fact] + public async Task ApiCall_WhenErrorStatusCode_SetsActivityStatusToError() + { + _fixture.Reset(); + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/MISSING"); + var client = _fixture.CreateClient(); + var activities = new List(); + + using var listener = CreateListener(activities); + + await Assert.ThrowsAsync( + () => client.GetProjectAsync("MISSING")); + + var activity = Assert.Single(activities); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal(404, activity.GetTagItem("http.response.status_code")); + Assert.Equal("404", activity.GetTagItem("error.type")); + } + + [Fact] + public async Task ApiCall_WhenServerError_SetsActivityStatusToError() + { + _fixture.Reset(); + _fixture.Server.SetupInternalServerError($"{ApiBasePath}/projects/{TestConstants.TestProjectKey}"); + var client = _fixture.CreateClient(); + var activities = new List(); + + using var listener = CreateListener(activities); + + await Assert.ThrowsAsync( + () => client.GetProjectAsync(TestConstants.TestProjectKey)); + + var activity = Assert.Single(activities); + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal(500, activity.GetTagItem("http.response.status_code")); + Assert.Equal("500", activity.GetTagItem("error.type")); + } + + [Fact] + public void NoListeners_ActivitySourceHasNoOverhead() + { + var activity = BitbucketClient.ActivitySource.StartActivity("test"); + Assert.Null(activity); + } + + private ActivityListener CreateListener(List activities) + { + var baseUrl = _fixture.BaseUrl; + var listener = new ActivityListener + { + ShouldListenTo = source => source.Name == "Bitbucket.Net", + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => + { + // Filter to only activities targeting our fixture's server to avoid + // cross-contamination from parallel test classes using BitbucketClient. + if (activity.GetTagItem("url.full")?.ToString()?.StartsWith(baseUrl) == true) + { + activities.Add(activity); + } + }, + }; + ActivitySource.AddActivityListener(listener); + return listener; + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs index df64309..341dfd8 100644 --- a/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs @@ -1,4 +1,4 @@ -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects.Requests; using Bitbucket.Net.Tests.Infrastructure; using Xunit; @@ -32,7 +32,7 @@ public async Task CreateProjectRepositoryWebHookAsync_ReturnsWebhook() _fixture.Server.SetupCreateWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); var client = _fixture.CreateClient(); - var newWebhook = new WebHook + var request = new CreateWebHookRequest { Name = "Test Webhook", Url = "https://example.com/webhook", @@ -43,7 +43,7 @@ public async Task CreateProjectRepositoryWebHookAsync_ReturnsWebhook() var webhook = await client.CreateProjectRepositoryWebHookAsync( TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, - newWebhook); + request); Assert.NotNull(webhook); Assert.Equal(1, webhook.Id); diff --git a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs new file mode 100644 index 0000000..4b49f99 --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs @@ -0,0 +1,109 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +/// +/// Architectural tests that verify structural invariants across the codebase. +/// These tests catch unsafe patterns that cannot be detected at compile time. +/// +public class ArchitecturalTests +{ + private static readonly string s_sourceDir = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "src", "Bitbucket.Net")); + + /// + /// Verifies that the static JsonSerializerOptions instances are explicitly + /// frozen (read-only), preventing accidental mutation from any thread. + /// + [Fact] + public void JsonSerializerOptions_AreExplicitlyFrozen() + { + var clientType = typeof(BitbucketClient); + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Static; + + var readOptions = clientType.GetField("s_jsonOptions", bindingFlags)?.GetValue(null) as JsonSerializerOptions; + Assert.NotNull(readOptions); + Assert.True(readOptions.IsReadOnly, "s_jsonOptions should be explicitly frozen via MakeReadOnly()"); + + var writeOptions = clientType.GetField("s_writeJsonOptions", bindingFlags)?.GetValue(null) as JsonSerializerOptions; + Assert.NotNull(writeOptions); + Assert.True(writeOptions.IsReadOnly, "s_writeJsonOptions should be explicitly frozen via MakeReadOnly()"); + } + + /// + /// Verifies that every HTTP call in BitbucketClient partial class files + /// has a corresponding error handler (HandleResponseAsync, HandleErrorsAsync, + /// or ExecuteAsync). This prevents silent swallowing of HTTP errors since + /// BitbucketClient uses AllowAnyHttpStatus(). + /// + [Fact] + public void AllHttpCalls_HaveCorrespondingErrorHandlers() + { + var sourceFiles = Directory.GetFiles(s_sourceDir, "BitbucketClient*.cs", SearchOption.AllDirectories); + Assert.NotEmpty(sourceFiles); + + var httpCallPattern = new Regex(@"\.(Get|Post|Put|Delete|Patch|Send)Async\(", RegexOptions.Compiled); + var errorHandlerPattern = new Regex(@"(HandleResponseAsync|HandleErrorsAsync|ExecuteAsync|ExecuteWithNoContentAsync|response\.StatusCode)", RegexOptions.Compiled); + + var failures = new List(); + + foreach (var file in sourceFiles) + { + string content = File.ReadAllText(file); + int httpCalls = httpCallPattern.Matches(content).Count; + int errorHandlers = errorHandlerPattern.Matches(content).Count; + + if (httpCalls > errorHandlers) + { + string fileName = Path.GetRelativePath(s_sourceDir, file); + failures.Add($"{fileName}: {httpCalls} HTTP calls but only {errorHandlers} error handlers"); + } + } + + Assert.True( + failures.Count == 0, + $"Unhandled HTTP calls detected (every HTTP call must use HandleResponseAsync, HandleErrorsAsync, or ExecuteAsync):\n{string.Join('\n', failures)}"); + } + + /// + /// Verifies that every await expression in the production source uses + /// ConfigureAwait(false). This is a belt-and-suspenders safety net + /// alongside the Meziantou.Analyzer MA0004 build-time rule. Counts awaits + /// and ConfigureAwait(false) calls per file — they must be equal. + /// + [Fact] + public void AllAwaits_UseConfigureAwaitFalse() + { + var csFiles = Directory.GetFiles(s_sourceDir, "*.cs", SearchOption.AllDirectories); + Assert.NotEmpty(csFiles); + + var awaitPattern = new Regex(@"\bawait\s", RegexOptions.Compiled); + var configureAwaitPattern = new Regex(@"\.ConfigureAwait\(false\)", RegexOptions.Compiled); + + var failures = new List(); + + foreach (var file in csFiles) + { + string content = File.ReadAllText(file); + + // Strip single-line comments to avoid false positives + string strippedContent = Regex.Replace(content, @"//.*$", "", RegexOptions.Multiline); + + int awaitCount = awaitPattern.Matches(strippedContent).Count; + int configureAwaitCount = configureAwaitPattern.Matches(strippedContent).Count; + + if (awaitCount > configureAwaitCount) + { + string fileName = Path.GetRelativePath(s_sourceDir, file); + failures.Add($"{fileName}: {awaitCount} awaits but only {configureAwaitCount} ConfigureAwait(false) calls"); + } + } + + Assert.True( + failures.Count == 0, + $"await without ConfigureAwait(false) detected:\n{string.Join('\n', failures)}"); + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientDisposeTests.cs b/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientDisposeTests.cs new file mode 100644 index 0000000..feefb98 --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientDisposeTests.cs @@ -0,0 +1,68 @@ +#nullable enable + +using Flurl.Http; +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +public class BitbucketClientDisposeTests +{ + private const string TestUrl = "https://bitbucket.example.com"; + + [Fact] + public void Dispose_BasicAuth_DoesNotThrow() + { + var client = new BitbucketClient(TestUrl, "user", "pass"); + client.Dispose(); + } + + [Fact] + public void Dispose_TokenAuth_DoesNotThrow() + { + var client = new BitbucketClient(TestUrl, () => "token"); + client.Dispose(); + } + + [Fact] + public void Dispose_HttpClient_DisposesOwnedWrapper() + { + var httpClient = new HttpClient(); + var client = new BitbucketClient(httpClient, TestUrl); + + // Should not throw — disposes the internal FlurlClient wrapper + client.Dispose(); + } + + [Fact] + public void Dispose_FlurlClient_DoesNotDisposeExternalClient() + { + using var flurlClient = new FlurlClient(TestUrl); + var client = new BitbucketClient(flurlClient); + + // The BitbucketClient should NOT dispose the external FlurlClient + client.Dispose(); + + // The FlurlClient should still be usable after BitbucketClient disposal + Assert.NotNull(flurlClient.BaseUrl); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var httpClient = new HttpClient(); + var client = new BitbucketClient(httpClient, TestUrl); + + client.Dispose(); + client.Dispose(); // Should not throw on second call + } + + [Fact] + public async Task MethodAfterDispose_ThrowsObjectDisposedException() + { + var client = new BitbucketClient(TestUrl, "user", "pass"); + client.Dispose(); + + await Assert.ThrowsAsync( + () => client.GetProjectsAsync()); + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs b/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs index 3b72b29..0dbf04a 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs @@ -5,7 +5,6 @@ using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Git; using Bitbucket.Net.Models.RefRestrictions; -using Bitbucket.Net.Models.RefSync; using Xunit; namespace Bitbucket.Net.Tests.UnitTests; @@ -33,19 +32,6 @@ public void BoolToString_Nullable_ReturnsCorrectValue(bool? input, string? expec Assert.Equal(expected, result); } - [Theory] - [InlineData("true", true)] - [InlineData("TRUE", true)] - [InlineData("True", true)] - [InlineData("false", false)] - [InlineData("FALSE", false)] - [InlineData("anything", false)] - public void StringToBool_ReturnsCorrectValue(string input, bool expected) - { - var result = BitbucketHelpers.StringToBool(input); - Assert.Equal(expected, result); - } - #endregion #region BranchOrderBy Tests @@ -101,18 +87,6 @@ public void PullRequestStateToString_ReturnsCorrectValue(PullRequestStates input Assert.Equal(expected, result); } - [Theory] - [InlineData("OPEN", PullRequestStates.Open)] - [InlineData("open", PullRequestStates.Open)] - [InlineData("DECLINED", PullRequestStates.Declined)] - [InlineData("MERGED", PullRequestStates.Merged)] - [InlineData("ALL", PullRequestStates.All)] - public void StringToPullRequestState_ReturnsCorrectValue(string input, PullRequestStates expected) - { - var result = BitbucketHelpers.StringToPullRequestState(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(PullRequestStates.Open, "OPEN")] [InlineData(null, null)] @@ -187,19 +161,6 @@ public void PermissionToString_ReturnsCorrectValue(Permissions input, string exp Assert.Equal(expected, result); } - [Theory] - [InlineData("ADMIN", Permissions.Admin)] - [InlineData("admin", Permissions.Admin)] - [InlineData("LICENSED_USER", Permissions.LicensedUser)] - [InlineData("PROJECT_ADMIN", Permissions.ProjectAdmin)] - [InlineData("PROJECT_CREATE", Permissions.ProjectCreate)] - [InlineData("REPO_READ", Permissions.RepoRead)] - public void StringToPermission_ReturnsCorrectValue(string input, Permissions expected) - { - var result = BitbucketHelpers.StringToPermission(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(Permissions.Admin, "ADMIN")] [InlineData(null, null)] @@ -244,17 +205,6 @@ public void RoleToString_ReturnsCorrectValue(Roles input, string expected) Assert.Equal(expected, result); } - [Theory] - [InlineData("AUTHOR", Roles.Author)] - [InlineData("author", Roles.Author)] - [InlineData("REVIEWER", Roles.Reviewer)] - [InlineData("PARTICIPANT", Roles.Participant)] - public void StringToRole_ReturnsCorrectValue(string input, Roles expected) - { - var result = BitbucketHelpers.StringToRole(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(Roles.Author, "AUTHOR")] [InlineData(null, null)] @@ -278,17 +228,6 @@ public void LineTypeToString_ReturnsCorrectValue(LineTypes input, string expecte Assert.Equal(expected, result); } - [Theory] - [InlineData("ADDED", LineTypes.Added)] - [InlineData("added", LineTypes.Added)] - [InlineData("REMOVED", LineTypes.Removed)] - [InlineData("CONTEXT", LineTypes.Context)] - public void StringToLineType_ReturnsCorrectValue(string input, LineTypes expected) - { - var result = BitbucketHelpers.StringToLineType(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(LineTypes.Added, "ADDED")] [InlineData(null, null)] @@ -311,16 +250,6 @@ public void FileTypeToString_ReturnsCorrectValue(FileTypes input, string expecte Assert.Equal(expected, result); } - [Theory] - [InlineData("FROM", FileTypes.From)] - [InlineData("from", FileTypes.From)] - [InlineData("TO", FileTypes.To)] - public void StringToFileType_ReturnsCorrectValue(string input, FileTypes expected) - { - var result = BitbucketHelpers.StringToFileType(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(FileTypes.From, "FROM")] [InlineData(null, null)] @@ -365,65 +294,6 @@ public void ParticipantStatusToString_ReturnsCorrectValue(ParticipantStatus inpu Assert.Equal(expected, result); } - [Theory] - [InlineData("APPROVED", ParticipantStatus.Approved)] - [InlineData("approved", ParticipantStatus.Approved)] - [InlineData("NEEDS_WORK", ParticipantStatus.NeedsWork)] - [InlineData("UNAPPROVED", ParticipantStatus.Unapproved)] - public void StringToParticipantStatus_ReturnsCorrectValue(string input, ParticipantStatus expected) - { - var result = BitbucketHelpers.StringToParticipantStatus(input); - Assert.Equal(expected, result); - } - - #endregion - - #region HookTypes Tests - - [Theory] - [InlineData(HookTypes.PreReceive, "PRE_RECEIVE")] - [InlineData(HookTypes.PostReceive, "POST_RECEIVE")] - [InlineData(HookTypes.PrePullRequestMerge, "PRE_PULL_REQUEST_MERGE")] - public void HookTypeToString_ReturnsCorrectValue(HookTypes input, string expected) - { - var result = BitbucketHelpers.HookTypeToString(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("PRE_RECEIVE", HookTypes.PreReceive)] - [InlineData("pre_receive", HookTypes.PreReceive)] - [InlineData("POST_RECEIVE", HookTypes.PostReceive)] - [InlineData("PRE_PULL_REQUEST_MERGE", HookTypes.PrePullRequestMerge)] - public void StringToHookType_ReturnsCorrectValue(string input, HookTypes expected) - { - var result = BitbucketHelpers.StringToHookType(input); - Assert.Equal(expected, result); - } - - #endregion - - #region ScopeTypes Tests - - [Theory] - [InlineData(ScopeTypes.Project, "PROJECT")] - [InlineData(ScopeTypes.Repository, "REPOSITORY")] - public void ScopeTypeToString_ReturnsCorrectValue(ScopeTypes input, string expected) - { - var result = BitbucketHelpers.ScopeTypeToString(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("PROJECT", ScopeTypes.Project)] - [InlineData("project", ScopeTypes.Project)] - [InlineData("REPOSITORY", ScopeTypes.Repository)] - public void StringToScopeType_ReturnsCorrectValue(string input, ScopeTypes expected) - { - var result = BitbucketHelpers.StringToScopeType(input); - Assert.Equal(expected, result); - } - #endregion #region ArchiveFormats Tests @@ -460,17 +330,6 @@ public void WebHookOutcomeToString_ReturnsCorrectValue(WebHookOutcomes input, st Assert.Equal(expected, result); } - [Theory] - [InlineData("SUCCESS", WebHookOutcomes.Success)] - [InlineData("success", WebHookOutcomes.Success)] - [InlineData("FAILURE", WebHookOutcomes.Failure)] - [InlineData("ERROR", WebHookOutcomes.Error)] - public void StringToWebHookOutcome_ReturnsCorrectValue(string input, WebHookOutcomes expected) - { - var result = BitbucketHelpers.StringToWebHookOutcome(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(WebHookOutcomes.Success, "SUCCESS")] [InlineData(null, null)] @@ -566,18 +425,6 @@ public void RefRestrictionTypeToString_ReturnsCorrectValue(RefRestrictionTypes i Assert.Equal(expected, result); } - [Theory] - [InlineData("read-only", RefRestrictionTypes.AllChanges)] - [InlineData("READ-ONLY", RefRestrictionTypes.AllChanges)] - [InlineData("fast-forward-only", RefRestrictionTypes.RewritingHistory)] - [InlineData("no-deletes", RefRestrictionTypes.Deletion)] - [InlineData("pull-request-only", RefRestrictionTypes.ChangesWithoutPullRequest)] - public void StringToRefRestrictionType_ReturnsCorrectValue(string input, RefRestrictionTypes expected) - { - var result = BitbucketHelpers.StringToRefRestrictionType(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(RefRestrictionTypes.AllChanges, "read-only")] [InlineData(null, null)] @@ -589,29 +436,6 @@ public void RefRestrictionTypeToString_Nullable_ReturnsCorrectValue(RefRestricti #endregion - #region SynchronizeActions Tests - - [Theory] - [InlineData(SynchronizeActions.Merge, "MERGE")] - [InlineData(SynchronizeActions.Discard, "DISCARD")] - public void SynchronizeActionToString_ReturnsCorrectValue(SynchronizeActions input, string expected) - { - var result = BitbucketHelpers.SynchronizeActionToString(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("MERGE", SynchronizeActions.Merge)] - [InlineData("merge", SynchronizeActions.Merge)] - [InlineData("DISCARD", SynchronizeActions.Discard)] - public void StringToSynchronizeAction_ReturnsCorrectValue(string input, SynchronizeActions expected) - { - var result = BitbucketHelpers.StringToSynchronizeAction(input); - Assert.Equal(expected, result); - } - - #endregion - #region BlockerCommentState Tests [Theory] @@ -623,16 +447,6 @@ public void BlockerCommentStateToString_ReturnsCorrectValue(BlockerCommentState Assert.Equal(expected, result); } - [Theory] - [InlineData("OPEN", BlockerCommentState.Open)] - [InlineData("open", BlockerCommentState.Open)] - [InlineData("RESOLVED", BlockerCommentState.Resolved)] - public void StringToBlockerCommentState_ReturnsCorrectValue(string input, BlockerCommentState expected) - { - var result = BitbucketHelpers.StringToBlockerCommentState(input); - Assert.Equal(expected, result); - } - [Theory] [InlineData(BlockerCommentState.Open, "OPEN")] [InlineData(null, null)] @@ -643,36 +457,4 @@ public void BlockerCommentStateToString_Nullable_ReturnsCorrectValue(BlockerComm } #endregion - - #region CommentSeverity Tests - - [Theory] - [InlineData(CommentSeverity.Normal, "NORMAL")] - [InlineData(CommentSeverity.Blocker, "BLOCKER")] - public void CommentSeverityToString_ReturnsCorrectValue(CommentSeverity input, string expected) - { - var result = BitbucketHelpers.CommentSeverityToString(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("NORMAL", CommentSeverity.Normal)] - [InlineData("normal", CommentSeverity.Normal)] - [InlineData("BLOCKER", CommentSeverity.Blocker)] - public void StringToCommentSeverity_ReturnsCorrectValue(string input, CommentSeverity expected) - { - var result = BitbucketHelpers.StringToCommentSeverity(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData(CommentSeverity.Normal, "NORMAL")] - [InlineData(null, null)] - public void CommentSeverityToString_Nullable_ReturnsCorrectValue(CommentSeverity? input, string? expected) - { - var result = BitbucketHelpers.CommentSeverityToString(input); - Assert.Equal(expected, result); - } - - #endregion } \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs index fab23eb..6d5a5c0 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs @@ -1,6 +1,5 @@ #nullable enable -using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Serialization; using System.Text.Json; @@ -203,103 +202,4 @@ public void ErrorResponse_Serialization_RoundTrips() } #endregion - - #region TypeExtensions Tests - - [Fact] - public void IsNullableType_NullableInt_ReturnsTrue() - { - Assert.True(TypeExtensions.IsNullableType(typeof(int?))); - } - - [Fact] - public void IsNullableType_NullableDateTime_ReturnsTrue() - { - Assert.True(TypeExtensions.IsNullableType(typeof(DateTime?))); - } - - [Fact] - public void IsNullableType_NullableBool_ReturnsTrue() - { - Assert.True(TypeExtensions.IsNullableType(typeof(bool?))); - } - - [Fact] - public void IsNullableType_Int_ReturnsFalse() - { - Assert.False(TypeExtensions.IsNullableType(typeof(int))); - } - - [Fact] - public void IsNullableType_String_ReturnsFalse() - { - Assert.False(TypeExtensions.IsNullableType(typeof(string))); - } - - [Fact] - public void IsNullableType_Object_ReturnsFalse() - { - Assert.False(TypeExtensions.IsNullableType(typeof(object))); - } - - [Fact] - public void IsNullableType_ListOfInt_ReturnsFalse() - { - Assert.False(TypeExtensions.IsNullableType(typeof(List))); - } - - #endregion - - #region UnixDateTimeExtensions Tests - - [Fact] - public void FromUnixTimeMilliseconds_ZeroReturnsEpoch() - { - long timestamp = 0; - var result = timestamp.FromUnixTimeMilliseconds(); - - var expected = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - Assert.Equal(expected, result); - } - - [Fact] - public void FromUnixTimeMilliseconds_KnownTimestamp_ReturnsCorrectDate() - { - // 1609459200000 milliseconds = Jan 1, 2021 00:00:00 UTC - long timestamp = 1609459200000; - var result = timestamp.FromUnixTimeMilliseconds(); - - var expected = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); - Assert.Equal(expected, result); - } - - [Fact] - public void ToUnixTimeMilliseconds_Epoch_ReturnsZero() - { - var epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - var result = epoch.ToUnixTimeMilliseconds(); - - Assert.Equal(0, result); - } - - [Fact] - public void ToUnixTimeMilliseconds_KnownDate_ReturnsCorrectValue() - { - var dateTime = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); - var result = dateTime.ToUnixTimeMilliseconds(); - - Assert.Equal(1609459200000, result); - } - - [Fact] - public void UnixTimeMilliseconds_RoundTrip() - { - var original = new DateTimeOffset(2025, 6, 15, 12, 30, 45, 123, TimeSpan.Zero); - var milliseconds = original.ToUnixTimeMilliseconds(); - var restored = milliseconds.FromUnixTimeMilliseconds(); - - Assert.Equal(original, restored); - } - - #endregion } \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs index f093539..cdea0c4 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs @@ -86,6 +86,69 @@ public void Create_429_ReturnsBitbucketRateLimitException() Assert.Contains("429", exception.Message); } + [Fact] + public void Create_429_WithAllHeaders_ExposesRateLimitProperties() + { + using var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + response.Headers.TryAddWithoutValidation("Retry-After", "30"); + response.Headers.TryAddWithoutValidation("X-RateLimit-Limit", "100"); + response.Headers.TryAddWithoutValidation("X-RateLimit-Remaining", "0"); + response.Headers.TryAddWithoutValidation("X-RateLimit-Reset", "1700000000"); + + var exception = (BitbucketRateLimitException)BitbucketApiException.Create( + 429, SampleErrors, response.Headers, "https://test.com/api"); + + Assert.Equal(TimeSpan.FromSeconds(30), exception.RetryAfter); + Assert.Equal(100, exception.RateLimit); + Assert.Equal(0, exception.RateLimitRemaining); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1700000000), exception.RateLimitReset); + } + + [Fact] + public void Create_429_WithPartialHeaders_MissingValuesAreNull() + { + using var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + response.Headers.TryAddWithoutValidation("Retry-After", "60"); + + var exception = (BitbucketRateLimitException)BitbucketApiException.Create( + 429, SampleErrors, response.Headers); + + Assert.Equal(TimeSpan.FromSeconds(60), exception.RetryAfter); + Assert.Null(exception.RateLimit); + Assert.Null(exception.RateLimitRemaining); + Assert.Null(exception.RateLimitReset); + } + + [Fact] + public void Create_429_WithInvalidHeaders_ReturnsNullNotThrow() + { + using var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + response.Headers.TryAddWithoutValidation("Retry-After", "not-a-number"); + response.Headers.TryAddWithoutValidation("X-RateLimit-Limit", "abc"); + response.Headers.TryAddWithoutValidation("X-RateLimit-Remaining", ""); + response.Headers.TryAddWithoutValidation("X-RateLimit-Reset", "xyz"); + + var exception = (BitbucketRateLimitException)BitbucketApiException.Create( + 429, SampleErrors, response.Headers); + + Assert.Null(exception.RetryAfter); + Assert.Null(exception.RateLimit); + Assert.Null(exception.RateLimitRemaining); + Assert.Null(exception.RateLimitReset); + } + + [Fact] + public void Create_429_WithNullHeaders_ReturnsExceptionWithNullProperties() + { + var exception = (BitbucketRateLimitException)BitbucketApiException.Create( + 429, SampleErrors, responseHeaders: null); + + Assert.Null(exception.RetryAfter); + Assert.Null(exception.RateLimit); + Assert.Null(exception.RateLimitRemaining); + Assert.Null(exception.RateLimitReset); + } + [Theory] [InlineData(500)] [InlineData(502)] diff --git a/test/Bitbucket.Net.Tests/UnitTests/InputValidationTests.cs b/test/Bitbucket.Net.Tests/UnitTests/InputValidationTests.cs new file mode 100644 index 0000000..0bfbdf7 --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/InputValidationTests.cs @@ -0,0 +1,107 @@ +#nullable enable + +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +/// +/// Verifies that public methods validate URL-path string parameters +/// with ArgumentException for null, empty, and whitespace inputs. +/// +public class InputValidationTests +{ + private const string TestUrl = "https://bitbucket.example.com"; + private static BitbucketClient CreateClient() => new(TestUrl, "user", "pass"); + + #region ProjectKey validation + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetProjectAsync_InvalidProjectKey_ThrowsArgumentException(string? projectKey) + { + var client = CreateClient(); + await Assert.ThrowsAnyAsync( + () => client.GetProjectAsync(projectKey!)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetProjectRepositoriesAsync_InvalidProjectKey_ThrowsArgumentException(string? projectKey) + { + var client = CreateClient(); + await Assert.ThrowsAnyAsync( + () => client.GetProjectRepositoriesAsync(projectKey!)); + } + + #endregion + + #region RepositorySlug validation + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetBranchesAsync_InvalidRepositorySlug_ThrowsArgumentException(string? repositorySlug) + { + var client = CreateClient(); + await Assert.ThrowsAnyAsync( + () => client.GetBranchesAsync("PROJ", repositorySlug!)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetPullRequestsAsync_InvalidRepositorySlug_ThrowsArgumentException(string? repositorySlug) + { + var client = CreateClient(); + await Assert.ThrowsAnyAsync( + () => client.GetPullRequestsAsync("PROJ", repositorySlug!)); + } + + #endregion + + #region CommitId validation + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetCommitAsync_InvalidCommitId_ThrowsArgumentException(string? commitId) + { + var client = CreateClient(); + await Assert.ThrowsAnyAsync( + () => client.GetCommitAsync("PROJ", "repo", commitId!)); + } + + #endregion + + #region Combined path parameters + + [Fact] + public async Task GetPullRequestActivitiesAsync_NullProjectKey_ThrowsBeforeHttpCall() + { + var client = CreateClient(); + // Should throw immediately without making any HTTP call + var ex = await Assert.ThrowsAnyAsync( + () => client.GetPullRequestActivitiesAsync(null!, "repo", 1)); + + Assert.Contains("projectKey", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task GetPullRequestActivitiesAsync_NullRepoSlug_ThrowsBeforeHttpCall() + { + var client = CreateClient(); + var ex = await Assert.ThrowsAnyAsync( + () => client.GetPullRequestActivitiesAsync("PROJ", null!, 1)); + + Assert.Contains("repositorySlug", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + #endregion +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs b/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs index 748a9f5..cdc2688 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs @@ -1,3 +1,4 @@ +using Bitbucket.Net.Common; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Projects; using System.Text.Json; @@ -13,16 +14,16 @@ public class JsonConverterTests { new UnixDateTimeOffsetConverter(), new NullableUnixDateTimeOffsetConverter(), - new PullRequestStatesConverter(), - new ParticipantStatusConverter(), - new RolesConverter(), - new LineTypesConverter(), - new FileTypesConverter(), - new HookTypesConverter(), - new ScopeTypesConverter(), - new WebHookOutcomesConverter(), - new BlockerCommentStateConverter(), - new CommentSeverityConverter() + new JsonEnumConverter(BitbucketEnumMaps.PullRequestStates), + new JsonEnumConverter(BitbucketEnumMaps.ParticipantStatus), + new JsonEnumConverter(BitbucketEnumMaps.Roles), + new JsonEnumConverter(BitbucketEnumMaps.LineTypes), + new JsonEnumConverter(BitbucketEnumMaps.FileTypes), + new JsonEnumConverter(BitbucketEnumMaps.HookTypes), + new JsonEnumConverter(BitbucketEnumMaps.ScopeTypes), + new JsonEnumConverter(BitbucketEnumMaps.WebHookOutcomes), + new JsonEnumConverter(BitbucketEnumMaps.BlockerCommentState), + new JsonEnumConverter(BitbucketEnumMaps.CommentSeverity) } }; diff --git a/test/Bitbucket.Net.Tests/UnitTests/PagedResultsReaderTests.cs b/test/Bitbucket.Net.Tests/UnitTests/PagedResultsReaderTests.cs new file mode 100644 index 0000000..036a3ba --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/PagedResultsReaderTests.cs @@ -0,0 +1,65 @@ +using Bitbucket.Net.Common; +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +public class PagedResultsReaderTests +{ + [Fact] + public void ReadMetadata_FullPayload_ExtractsAllFields() + { + var json = """{"size":25,"limit":25,"isLastPage":false,"start":0,"nextPageStart":25,"values":[{"id":1},{"id":2}]}"""u8; + var metadata = PagedResultsReader.ReadMetadata(json); + + Assert.False(metadata.IsLastPage); + Assert.Equal(25, metadata.NextPageStart); + Assert.Equal(0, metadata.Start); + Assert.Equal(25, metadata.Limit); + Assert.Equal(25, metadata.Size); + } + + [Fact] + public void ReadMetadata_LastPage_ReturnsTrue() + { + var json = """{"size":10,"limit":25,"isLastPage":true,"start":0,"values":[]}"""u8; + var metadata = PagedResultsReader.ReadMetadata(json); + + Assert.True(metadata.IsLastPage); + Assert.Null(metadata.NextPageStart); + Assert.Equal(10, metadata.Size); + } + + [Fact] + public void ReadMetadata_EmptyValues_Works() + { + var json = """{"size":0,"limit":25,"isLastPage":true,"values":[]}"""u8; + var metadata = PagedResultsReader.ReadMetadata(json); + + Assert.True(metadata.IsLastPage); + Assert.Equal(0, metadata.Size); + } + + [Fact] + public void ReadMetadata_NestedValues_SkipsCorrectly() + { + var json = """{"size":2,"limit":25,"isLastPage":false,"nextPageStart":25,"values":[{"id":1,"title":"Test","nested":{"deep":true}},{"id":2,"title":"Another","tags":["a","b"]}]}"""u8; + var metadata = PagedResultsReader.ReadMetadata(json); + + Assert.False(metadata.IsLastPage); + Assert.Equal(25, metadata.NextPageStart); + Assert.Equal(2, metadata.Size); + } + + [Fact] + public void ReadMetadata_MissingOptionalFields_DefaultsCorrectly() + { + var json = """{"isLastPage":true,"values":[]}"""u8; + var metadata = PagedResultsReader.ReadMetadata(json); + + Assert.True(metadata.IsLastPage); + Assert.Null(metadata.NextPageStart); + Assert.Null(metadata.Start); + Assert.Null(metadata.Limit); + Assert.Equal(0, metadata.Size); + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/SourceGenCoverageTests.cs b/test/Bitbucket.Net.Tests/UnitTests/SourceGenCoverageTests.cs new file mode 100644 index 0000000..dc7b5ba --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/SourceGenCoverageTests.cs @@ -0,0 +1,133 @@ +#nullable enable + +using Bitbucket.Net.Serialization; +using System.Reflection; +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +/// +/// Verifies that all model types are registered in . +/// Without this test, a missing [JsonSerializable] attribute would cause a runtime +/// instead of being caught at build time. +/// +public class SourceGenCoverageTests +{ + private static readonly Assembly s_libraryAssembly = typeof(BitbucketClient).Assembly; + + /// + /// Enumerates every public, non-abstract, non-enum class in the Bitbucket.Net.Models + /// and Bitbucket.Net.Common.Models namespaces and asserts that the source-generated + /// context can resolve type info for each. + /// + [Fact] + public void AllModelTypesRegisteredInSourceGenContext() + { + var modelTypes = GetModelTypes(); + + Assert.NotEmpty(modelTypes); + + var missing = new List(); + + foreach (var type in modelTypes) + { + try + { + var typeInfo = BitbucketJsonContext.Default.GetTypeInfo(type); + if (typeInfo is null) + { + missing.Add(type.FullName!); + } + } + catch + { + missing.Add(type.FullName!); + } + } + + Assert.True( + missing.Count == 0, + $"The following model types are not registered in BitbucketJsonContext:\n{string.Join("\n", missing)}"); + } + + /// + /// Verifies that PagedResults<T> generic instantiations are registered + /// for the primary model types used in paginated API responses. + /// + [Theory] + [MemberData(nameof(PagedResultTypes))] + public void PagedResultsTypeIsRegistered(Type itemType) + { + var pagedType = typeof(Bitbucket.Net.Common.Models.PagedResults<>).MakeGenericType(itemType); + + var typeInfo = BitbucketJsonContext.Default.GetTypeInfo(pagedType); + + Assert.NotNull(typeInfo); + } + + public static TheoryData PagedResultTypes() + { + var data = new TheoryData(); + + foreach (var t in GetKnownPagedItemTypes()) + { + data.Add(t); + } + + return data; + } + + private static Type[] GetKnownPagedItemTypes() + { + return + [ + typeof(Bitbucket.Net.Models.PersonalAccessTokens.AccessToken), + typeof(Bitbucket.Net.Models.Audit.AuditEvent), + typeof(Bitbucket.Net.Models.Core.Tasks.BitbucketTask), + typeof(Bitbucket.Net.Models.Core.Projects.BlockerComment), + typeof(Bitbucket.Net.Models.Core.Projects.Branch), + typeof(Bitbucket.Net.Models.Core.Projects.BranchBase), + typeof(Bitbucket.Net.Models.Builds.BuildStatus), + typeof(Bitbucket.Net.Models.Core.Projects.Change), + typeof(Bitbucket.Net.Models.Jira.ChangeSet), + typeof(Bitbucket.Net.Models.Core.Projects.Comment), + typeof(Bitbucket.Net.Models.Core.Projects.CommentRef), + typeof(Bitbucket.Net.Models.Core.Projects.Commit), + typeof(Bitbucket.Net.Models.Core.Projects.ContentItem), + typeof(Bitbucket.Net.Models.Core.Admin.DeletableGroupOrUser), + typeof(Bitbucket.Net.Models.Core.Admin.GroupPermission), + typeof(Bitbucket.Net.Models.Core.Projects.Hook), + typeof(Bitbucket.Net.Models.Core.Users.Identity), + typeof(Bitbucket.Net.Models.RefRestrictions.Key), + typeof(Bitbucket.Net.Models.Core.Projects.LicensedUser), + typeof(Bitbucket.Net.Models.Core.Projects.Participant), + typeof(Bitbucket.Net.Models.Core.Projects.Project), + typeof(Bitbucket.Net.Models.Ssh.ProjectKey), + typeof(Bitbucket.Net.Models.Core.Projects.PullRequest), + typeof(Bitbucket.Net.Models.Core.Projects.PullRequestActivity), + typeof(Bitbucket.Net.Models.Core.Projects.PullRequestSuggestion), + typeof(Bitbucket.Net.Models.RefRestrictions.RefRestriction), + typeof(Bitbucket.Net.Models.Core.Projects.Repository), + typeof(Bitbucket.Net.Models.Core.Projects.RepositoryFork), + typeof(Bitbucket.Net.Models.Ssh.RepositoryKey), + typeof(string), + typeof(Bitbucket.Net.Models.Core.Projects.Tag), + typeof(Bitbucket.Net.Models.Core.Users.User), + typeof(Bitbucket.Net.Models.Core.Admin.UserInfo), + typeof(Bitbucket.Net.Models.Core.Admin.UserPermission), + typeof(Bitbucket.Net.Models.Core.Projects.WebHook), + ]; + } + + private static List GetModelTypes() + { + return s_libraryAssembly.GetExportedTypes() + .Where(t => + t is { IsClass: true, IsAbstract: false } && + !t.IsGenericTypeDefinition && + t.Namespace is not null && + (t.Namespace.StartsWith("Bitbucket.Net.Models", StringComparison.Ordinal) || + t.Namespace.StartsWith("Bitbucket.Net.Common.Models", StringComparison.Ordinal))) + .ToList(); + } +} \ No newline at end of file