From 6d5cb198606f00c1513e10e16d8a6667c07b7ac9 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:01:25 +0000 Subject: [PATCH 01/31] refactor: remove reflection fallback from JSON deserialization Remove DefaultJsonTypeInfoResolver from s_jsonOptions so deserialization uses only the source-generated BitbucketJsonContext. This ensures missing type registrations fail fast with NotSupportedException rather than silently falling back to slower reflection-based serialization. - Separate s_writeJsonOptions retains reflection fallback for outbound request bodies that use anonymous types (to be replaced with typed DTOs in a future change) - Register missing collection types in BitbucketJsonContext: Dictionary, IEnumerable, IEnumerable, IEnumerable, IEnumerable - Fix direct JsonSerializer.Serialize calls in Branches and Ssh clients to use s_writeJsonOptions - Add SourceGenCoverageTests verifying all model types and PagedResults instantiations are registered All 773 tests pass. --- src/Bitbucket.Net/BitbucketClient.cs | 22 ++- src/Bitbucket.Net/Branches/BitbucketClient.cs | 2 +- .../Serialization/BitbucketJsonContext.cs | 10 +- src/Bitbucket.Net/Ssh/BitbucketClient.cs | 2 +- .../UnitTests/SourceGenCoverageTests.cs | 133 ++++++++++++++++++ 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 test/Bitbucket.Net.Tests/UnitTests/SourceGenCoverageTests.cs diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index d3ffb9c..854cdc0 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -23,11 +23,10 @@ public partial class BitbucketClient { 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 - ), + // 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(), @@ -48,6 +47,17 @@ public partial class BitbucketClient }, }; + // 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 readonly JsonSerializerOptions s_writeJsonOptions = new(s_jsonOptions) + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + BitbucketJsonContext.Default, + new DefaultJsonTypeInfoResolver() + ), + }; + private static readonly ISerializer s_serializer = new DefaultJsonSerializer(s_jsonOptions); private readonly Url _url; @@ -195,7 +205,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"); } diff --git a/src/Bitbucket.Net/Branches/BitbucketClient.cs b/src/Bitbucket.Net/Branches/BitbucketClient.cs index e7fdb11..5c14c53 100644 --- a/src/Bitbucket.Net/Branches/BitbucketClient.cs +++ b/src/Bitbucket.Net/Branches/BitbucketClient.cs @@ -121,7 +121,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/Serialization/BitbucketJsonContext.cs b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs index 2ae2c2a..c539169 100644 --- a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs +++ b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs @@ -40,8 +40,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. @@ -314,7 +315,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))] diff --git a/src/Bitbucket.Net/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index fa1605f..94ce24e 100644 --- a/src/Bitbucket.Net/Ssh/BitbucketClient.cs +++ b/src/Bitbucket.Net/Ssh/BitbucketClient.cs @@ -51,7 +51,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) 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 From 9995837f604ff73b624acd74c5ef4827f20ecffc Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:02:51 +0000 Subject: [PATCH 02/31] perf: deserialize JSON responses directly from stream Replace string-based deserialization in ReadResponseContentAsync with stream-based JsonSerializer.DeserializeAsync. This avoids allocating an intermediate UTF-16 string for every API response, which is especially beneficial for large paged responses (100+ items). The contentHandler overload path still reads as string for non-JSON responses. Error handling continues to read the body as string since error bodies are small and need to be included in exception messages. All 773 tests pass. --- src/Bitbucket.Net/BitbucketClient.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 854cdc0..3a35682 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -236,27 +236,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))!; + } } /// From 1e30035f3b1be4483d2682a13d26e00ed2b11b0e Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:10:48 +0000 Subject: [PATCH 03/31] perf: migrate enum lookup dictionaries to FrozenDictionary - Convert all 25 Dictionary fields to FrozenDictionary for optimal read-only lookup performance - Add 14 reverse FrozenDictionary fields with StringComparer.OrdinalIgnoreCase for O(1) string-to-enum lookups - Replace O(n) FirstOrDefault reverse scans with O(1) TryGetValue - Add missing Global value to ScopeTypes enum (Bitbucket API returns GLOBAL for hooks configured at server level) - Improve XML documentation across all helper methods --- src/Bitbucket.Net/Common/BitbucketHelpers.cs | 229 ++++++++++-------- .../Models/Core/Projects/ScopeTypes.cs | 1 + 2 files changed, 124 insertions(+), 106 deletions(-) diff --git a/src/Bitbucket.Net/Common/BitbucketHelpers.cs b/src/Bitbucket.Net/Common/BitbucketHelpers.cs index e6e3e9b..2f77a80 100644 --- a/src/Bitbucket.Net/Common/BitbucketHelpers.cs +++ b/src/Bitbucket.Net/Common/BitbucketHelpers.cs @@ -4,11 +4,13 @@ using Bitbucket.Net.Models.Git; using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Models.RefSync; +using System.Collections.Frozen; namespace Bitbucket.Net.Common; /// /// Helper methods for converting between Bitbucket enum values and their wire-format string representations. +/// Uses for optimal read-only lookup performance. /// public static class BitbucketHelpers { @@ -43,11 +45,11 @@ public static string BoolToString(bool value) => value #region BranchOrderBy - private static readonly Dictionary s_stringByBranchOrderBy = new() + private static readonly FrozenDictionary s_stringByBranchOrderBy = new Dictionary { [BranchOrderBy.Alphabetical] = "ALPHABETICAL", [BranchOrderBy.Modification] = "MODIFICATION", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -69,11 +71,11 @@ public static string BranchOrderByToString(BranchOrderBy orderBy) #region PullRequestDirections - private static readonly Dictionary s_stringByPullRequestDirection = new() + private static readonly FrozenDictionary s_stringByPullRequestDirection = new Dictionary { [PullRequestDirections.Incoming] = "INCOMING", [PullRequestDirections.Outgoing] = "OUTGOING", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -95,13 +97,16 @@ public static string PullRequestDirectionToString(PullRequestDirections directio #region PullRequestStates - private static readonly Dictionary s_stringByPullRequestState = new() + private static readonly FrozenDictionary s_stringByPullRequestState = new Dictionary { [PullRequestStates.Open] = "OPEN", [PullRequestStates.Declined] = "DECLINED", [PullRequestStates.Merged] = "MERGED", [PullRequestStates.All] = "ALL", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_pullRequestStateByString = + s_stringByPullRequestState.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -136,25 +141,23 @@ public static string PullRequestStateToString(PullRequestStates 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)) + if (!s_pullRequestStateByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown pull request state: {s}"); } - return pair.Key; + return result; } #endregion #region PullRequestOrders - private static readonly Dictionary s_stringByPullRequestOrder = new() + private static readonly FrozenDictionary s_stringByPullRequestOrder = new Dictionary { [PullRequestOrders.Newest] = "NEWEST", [PullRequestOrders.Oldest] = "OLDEST", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -185,11 +188,11 @@ public static string PullRequestOrderToString(PullRequestOrders order) #region PullRequestFromTypes - private static readonly Dictionary s_stringByPullRequestFromType = new() + private static readonly FrozenDictionary s_stringByPullRequestFromType = new Dictionary { [PullRequestFromTypes.Comment] = "COMMENT", [PullRequestFromTypes.Activity] = "ACTIVITY", - }; + }.ToFrozenDictionary(); private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) { @@ -214,7 +217,7 @@ private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) #region Permissions - private static readonly Dictionary s_stringByPermissions = new() + private static readonly FrozenDictionary s_stringByPermissions = new Dictionary { [Permissions.Admin] = "ADMIN", [Permissions.LicensedUser] = "LICENSED_USER", @@ -227,7 +230,10 @@ private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) [Permissions.RepoRead] = "REPO_READ", [Permissions.RepoWrite] = "REPO_WRITE", [Permissions.SysAdmin] = "SYS_ADMIN", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_permissionByString = + s_stringByPermissions.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -262,26 +268,24 @@ public static string PermissionToString(Permissions 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)) + if (!s_permissionByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown permission: {s}"); } - return pair.Key; + return result; } #endregion #region MergeCommits - private static readonly Dictionary s_stringByMergeCommits = new() + private static readonly FrozenDictionary s_stringByMergeCommits = new Dictionary { [MergeCommits.Exclude] = "exclude", [MergeCommits.Include] = "include", [MergeCommits.Only] = "only", - }; + }.ToFrozenDictionary(); /// /// Converts a preference to the Bitbucket API string. @@ -303,12 +307,15 @@ public static string MergeCommitsToString(MergeCommits mergeCommits) #region Roles - private static readonly Dictionary s_stringByRoles = new() + private static readonly FrozenDictionary s_stringByRoles = new Dictionary { [Roles.Author] = "AUTHOR", [Roles.Reviewer] = "REVIEWER", [Roles.Participant] = "PARTICIPANT", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_roleByString = + s_stringByRoles.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a pull request value to the Bitbucket API string. @@ -343,26 +350,27 @@ public static string RoleToString(Roles 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)) + if (!s_roleByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown role: {s}"); } - return pair.Key; + return result; } #endregion #region LineTypes - private static readonly Dictionary s_stringByLineTypes = new() + private static readonly FrozenDictionary s_stringByLineTypes = new Dictionary { [LineTypes.Added] = "ADDED", [LineTypes.Removed] = "REMOVED", [LineTypes.Context] = "CONTEXT", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_lineTypeByString = + s_stringByLineTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -400,25 +408,26 @@ public static string LineTypeToString(LineTypes lineType) /// 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)) + if (!s_lineTypeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown line type: {s}"); } - return pair.Key; + return result; } #endregion #region FileTypes - private static readonly Dictionary s_stringByFileTypes = new() + private static readonly FrozenDictionary s_stringByFileTypes = new Dictionary { [FileTypes.From] = "FROM", [FileTypes.To] = "TO", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_fileTypeByString = + s_stringByFileTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -456,26 +465,24 @@ public static string FileTypeToString(FileTypes fileType) /// 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)) + if (!s_fileTypeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown file type: {s}"); } - return pair.Key; + return result; } #endregion #region ChangeScopes - private static readonly Dictionary s_stringByChangeScopes = new() + private static readonly FrozenDictionary s_stringByChangeScopes = new Dictionary { [ChangeScopes.All] = "ALL", [ChangeScopes.Unreviewed] = "UNREVIEWED", [ChangeScopes.Range] = "RANGE", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -497,14 +504,17 @@ public static string ChangeScopeToString(ChangeScopes changeScope) #region LogLevels - private static readonly Dictionary s_stringByLogLevels = new() + private static readonly FrozenDictionary s_stringByLogLevels = new Dictionary { [LogLevels.Trace] = "TRACE", [LogLevels.Debug] = "DEBUG", [LogLevels.Info] = "INFO", [LogLevels.Warn] = "WARN", [LogLevels.Error] = "ERROR", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_logLevelByString = + s_stringByLogLevels.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -530,26 +540,27 @@ public static string LogLevelToString(LogLevels logLevel) /// 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)) + if (!s_logLevelByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown log level: {s}"); } - return pair.Key; + return result; } #endregion #region ParticipantStatus - private static readonly Dictionary s_stringByParticipantStatus = new() + private static readonly FrozenDictionary s_stringByParticipantStatus = new Dictionary { [ParticipantStatus.Approved] = "APPROVED", [ParticipantStatus.NeedsWork] = "NEEDS_WORK", [ParticipantStatus.Unapproved] = "UNAPPROVED", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_participantStatusByString = + s_stringByParticipantStatus.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -575,26 +586,27 @@ public static string ParticipantStatusToString(ParticipantStatus participantStat /// 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)) + if (!s_participantStatusByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown participant status: {s}"); } - return pair.Key; + return result; } #endregion #region HookTypes - private static readonly Dictionary s_stringByHookTypes = new() + private static readonly FrozenDictionary s_stringByHookTypes = new Dictionary { [HookTypes.PreReceive] = "PRE_RECEIVE", [HookTypes.PostReceive] = "POST_RECEIVE", [HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_hookTypeByString = + s_stringByHookTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a hook value to the Bitbucket API string. @@ -620,25 +632,27 @@ public static string HookTypeToString(HookTypes hookType) /// 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)) + if (!s_hookTypeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown hook type: {s}"); } - return pair.Key; + return result; } #endregion #region ScopeTypes - private static readonly Dictionary s_stringByScopeTypes = new() + private static readonly FrozenDictionary s_stringByScopeTypes = new Dictionary { + [ScopeTypes.Global] = "GLOBAL", [ScopeTypes.Project] = "PROJECT", [ScopeTypes.Repository] = "REPOSITORY", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_scopeTypeByString = + s_stringByScopeTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -664,27 +678,25 @@ public static string ScopeTypeToString(ScopeTypes scopeType) /// 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)) + if (!s_scopeTypeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown scope type: {s}"); } - return pair.Key; + return result; } #endregion #region ArchiveFormats - private static readonly Dictionary s_stringByArchiveFormats = new() + private static readonly FrozenDictionary s_stringByArchiveFormats = new Dictionary { [ArchiveFormats.Zip] = "zip", [ArchiveFormats.Tar] = "tar", [ArchiveFormats.TarGz] = "tar.gz", [ArchiveFormats.Tgz] = "tgz", - }; + }.ToFrozenDictionary(); /// /// Converts an value to the Bitbucket API string. @@ -706,12 +718,15 @@ public static string ArchiveFormatToString(ArchiveFormats archiveFormat) #region WebHookOutcomes - private static readonly Dictionary s_stringByWebHookOutcomes = new() + private static readonly FrozenDictionary s_stringByWebHookOutcomes = new Dictionary { [WebHookOutcomes.Success] = "SUCCESS", [WebHookOutcomes.Failure] = "FAILURE", [WebHookOutcomes.Error] = "ERROR", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_webHookOutcomeByString = + s_stringByWebHookOutcomes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -746,26 +761,24 @@ public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) /// 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)) + if (!s_webHookOutcomeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown web hook outcome: {s}"); } - return pair.Key; + return result; } #endregion #region AnchorStates - private static readonly Dictionary s_stringByAnchorStates = new() + private static readonly FrozenDictionary s_stringByAnchorStates = new Dictionary { [AnchorStates.Active] = "ACTIVE", [AnchorStates.Orphaned] = "ORPHANED", [AnchorStates.All] = "ALL", - }; + }.ToFrozenDictionary(); /// /// Converts an value to the Bitbucket API string. @@ -787,12 +800,12 @@ public static string AnchorStateToString(AnchorStates anchorState) #region DiffTypes - private static readonly Dictionary s_stringByDiffTypes = new() + private static readonly FrozenDictionary s_stringByDiffTypes = new Dictionary { [DiffTypes.Effective] = "EFFECTIVE", [DiffTypes.Range] = "RANGE", [DiffTypes.Commit] = "COMMIT", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -826,11 +839,11 @@ public static string DiffTypeToString(DiffTypes diffType) #region TagTypes - private static readonly Dictionary s_stringByTagTypes = new() + private static readonly FrozenDictionary s_stringByTagTypes = new Dictionary { [TagTypes.LightWeight] = "LIGHTWEIGHT", [TagTypes.Annotated] = "ANNOTATED", - }; + }.ToFrozenDictionary(); /// /// Converts a value to the Bitbucket API string. @@ -852,13 +865,16 @@ public static string TagTypeToString(TagTypes tagType) #region RefRestrictionTypes - private static readonly Dictionary s_stringByRefRestrictionTypes = new() + private static readonly FrozenDictionary s_stringByRefRestrictionTypes = new Dictionary { [RefRestrictionTypes.AllChanges] = "read-only", [RefRestrictionTypes.RewritingHistory] = "fast-forward-only", [RefRestrictionTypes.Deletion] = "no-deletes", [RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_refRestrictionTypeByString = + s_stringByRefRestrictionTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -896,27 +912,25 @@ public static string RefRestrictionTypeToString(RefRestrictionTypes refRestricti /// 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)) + if (!s_refRestrictionTypeByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown ref restriction type: {s}"); } - return pair.Key; + return result; } #endregion #region RefMatcherTypes - private static readonly Dictionary s_stringByRefMatcherTypes = new() + private static readonly FrozenDictionary s_stringByRefMatcherTypes = new Dictionary { [RefMatcherTypes.Branch] = "BRANCH", [RefMatcherTypes.Pattern] = "PATTERN", [RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", [RefMatcherTypes.ModelBranch] = "MODEL_BRANCH", - }; + }.ToFrozenDictionary(); private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) { @@ -944,11 +958,14 @@ private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) #region SynchronizeActions - private static readonly Dictionary s_stringBySynchronizeActions = new() + private static readonly FrozenDictionary s_stringBySynchronizeActions = new Dictionary { [SynchronizeActions.Merge] = "MERGE", [SynchronizeActions.Discard] = "DISCARD", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_synchronizeActionByString = + s_stringBySynchronizeActions.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -974,25 +991,26 @@ public static string SynchronizeActionToString(SynchronizeActions synchronizeAct /// 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)) + if (!s_synchronizeActionByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown synchronize action: {s}"); } - return pair.Key; + return result; } #endregion #region BlockerCommentState - private static readonly Dictionary s_stringByBlockerCommentState = new() + private static readonly FrozenDictionary s_stringByBlockerCommentState = new Dictionary { [BlockerCommentState.Open] = "OPEN", [BlockerCommentState.Resolved] = "RESOLVED", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_blockerCommentStateByString = + s_stringByBlockerCommentState.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -1027,25 +1045,26 @@ public static string BlockerCommentStateToString(BlockerCommentState 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)) + if (!s_blockerCommentStateByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown blocker comment state: {s}"); } - return pair.Key; + return result; } #endregion #region CommentSeverity - private static readonly Dictionary s_stringByCommentSeverity = new() + private static readonly FrozenDictionary s_stringByCommentSeverity = new Dictionary { [CommentSeverity.Normal] = "NORMAL", [CommentSeverity.Blocker] = "BLOCKER", - }; + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary s_commentSeverityByString = + s_stringByCommentSeverity.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); /// /// Converts a value to the Bitbucket API string. @@ -1080,14 +1099,12 @@ public static string CommentSeverityToString(CommentSeverity 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)) + if (!s_commentSeverityByString.TryGetValue(s, out var result)) { throw new ArgumentException($"Unknown comment severity: {s}"); } - return pair.Key; + return result; } #endregion 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 From ce74b025b58b3ae116810d61b09ea259af1190f3 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:13:16 +0000 Subject: [PATCH 04/31] feat: implement IDisposable on BitbucketClient - Add IDisposable with ownership tracking via _ownsClient flag - HttpClient constructor owns the FlurlClient wrapper and disposes it - IFlurlClient constructor does not own, disposal is a no-op - Basic/token auth constructors have no disposable resources - Add ObjectDisposedException guard in GetBaseUrl() to cover all API methods - Dispose is idempotent (safe to call multiple times) - Add 6 unit tests covering all constructor variants and disposal semantics --- src/Bitbucket.Net/BitbucketClient.cs | 37 +++++++++- .../UnitTests/BitbucketClientDisposeTests.cs | 68 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/Bitbucket.Net.Tests/UnitTests/BitbucketClientDisposeTests.cs diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 3a35682..3295e66 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -16,8 +16,15 @@ 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 : IDisposable { private static readonly JsonSerializerOptions s_jsonOptions = new() { @@ -65,6 +72,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. @@ -137,6 +146,7 @@ public BitbucketClient(HttpClient httpClient, string baseUrl, Func? getT _getToken = getToken; _injectedClient = new FlurlClient(httpClient, baseUrl) .WithSettings(settings => settings.JsonSerializer = s_serializer); + _ownsClient = true; } /// @@ -156,14 +166,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 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 From 4b88cf04abfc93a58fdc1cefc941d53007167e99 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:16:14 +0000 Subject: [PATCH 05/31] feat: add ExecuteAsync methods and architectural error-handling test - Add ExecuteAsync, ExecuteAsync (bool), and ExecuteWithNoContentAsync methods that unify HTTP call + error handling - New methods make it impossible to forget error handling in future code - Add architectural test verifying all BitbucketClient HTTP calls have corresponding error handlers (HandleResponseAsync, HandleErrorsAsync, ExecuteAsync, or explicit StatusCode checks) - Existing methods continue using HandleResponseAsync/HandleErrorsAsync; new methods should prefer ExecuteAsync for safety --- src/Bitbucket.Net/BitbucketClient.cs | 53 +++++++++++++++++++ .../UnitTests/ArchitecturalTests.cs | 49 +++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 3295e66..ad511c6 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -384,6 +384,59 @@ private static async Task HandleResponseAsync(IFlurlResponse response, Can return await ReadResponseContentAsync(response, cancellationToken).ConfigureAwait(false); } + /// + /// Executes an HTTP request with unified error handling and deserialization. + /// Use this method for all new API methods to ensure errors are never silently ignored. + /// + /// The expected deserialized result type. + /// The configured Flurl request. + /// A delegate that performs the HTTP verb (e.g., static (r, ct) => r.GetAsync(ct)). + /// Optional custom handler for non-JSON response bodies. + /// Token to cancel the operation. + /// The deserialized response content. + private static async Task ExecuteAsync( + IFlurlRequest request, + Func> httpMethod, + Func? contentHandler = null, + CancellationToken cancellationToken = default) + { + var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); + return await HandleResponseAsync(response, contentHandler, cancellationToken).ConfigureAwait(false); + } + + /// + /// Executes an HTTP request with unified error handling and returns a boolean success indicator. + /// Use this method for API methods that return success/failure based on an empty response body. + /// + /// The configured Flurl request. + /// A delegate that performs the HTTP verb. + /// Token to cancel the operation. + /// true if the response body is empty (indicating success); otherwise, false. + private static async Task ExecuteAsync( + IFlurlRequest request, + Func> httpMethod, + CancellationToken cancellationToken = default) + { + var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Executes an HTTP request with unified error handling without returning content. + /// Use this method for API methods that only need to verify the request succeeded. + /// + /// The configured Flurl request. + /// A delegate that performs the HTTP verb. + /// Token to cancel the operation. + private static async Task ExecuteWithNoContentAsync( + IFlurlRequest request, + Func> httpMethod, + CancellationToken cancellationToken = default) + { + var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + } + /// /// Retrieves paged results from a paginated endpoint. /// diff --git a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs new file mode 100644 index 0000000..0c0731e --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs @@ -0,0 +1,49 @@ +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 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)}"); + } +} \ No newline at end of file From 78e55fa3f4c6e3e40c6b6e559b63cc086f2b0164 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:19:39 +0000 Subject: [PATCH 06/31] refactor!: return IReadOnlyList instead of IEnumerable BREAKING CHANGE: All buffered collection methods now return Task> instead of Task>. This accurately reflects that results are fully materialized and enables direct Count/indexer access without LINQ. - Change GetPagedResultsAsync return type to IReadOnlyList - Update ~70 public method signatures across all API domains - Methods not using GetPagedResultsAsync add .ToList() to convert IEnumerable results to List (implements IReadOnlyList) - Streaming methods (IAsyncEnumerable) are unaffected - Consumers using explicit IEnumerable types will get compile errors; switch to var or IReadOnlyList --- src/Bitbucket.Net/Audit/BitbucketClient.cs | 4 ++-- src/Bitbucket.Net/BitbucketClient.cs | 2 +- src/Bitbucket.Net/Branches/BitbucketClient.cs | 2 +- src/Bitbucket.Net/Builds/BitbucketClient.cs | 2 +- .../CommentLikes/BitbucketClient.cs | 4 ++-- .../Core/Admin/BitbucketClient.cs | 20 +++++++++---------- .../Core/Dashboard/BitbucketClient.cs | 4 ++-- .../Core/Groups/BitbucketClient.cs | 2 +- .../Core/Inbox/BitbucketClient.cs | 2 +- .../Core/Profile/BitbucketClient.cs | 2 +- .../Core/Projects/BitbucketClient.Branches.cs | 2 +- .../Core/Projects/BitbucketClient.Commits.cs | 8 ++++---- .../Core/Projects/BitbucketClient.Compare.cs | 6 +++--- .../Core/Projects/BitbucketClient.Projects.cs | 10 +++++----- .../BitbucketClient.PullRequestComments.cs | 2 +- .../BitbucketClient.PullRequestDetails.cs | 6 +++--- .../Projects/BitbucketClient.PullRequests.cs | 6 +++--- .../Projects/BitbucketClient.Repositories.cs | 14 ++++++------- .../BitbucketClient.RepositorySettings.cs | 6 +++--- .../Core/Projects/BitbucketClient.Tasks.cs | 10 +++++----- .../Core/Repos/BitbucketClient.cs | 2 +- .../Core/Users/BitbucketClient.cs | 2 +- .../DefaultReviewers/BitbucketClient.cs | 15 ++++++++------ src/Bitbucket.Net/Jira/BitbucketClient.cs | 7 ++++--- .../PersonalAccessTokens/BitbucketClient.cs | 2 +- .../RefRestrictions/BitbucketClient.cs | 18 +++++++++-------- src/Bitbucket.Net/Ssh/BitbucketClient.cs | 10 +++++----- 27 files changed, 88 insertions(+), 82 deletions(-) diff --git a/src/Bitbucket.Net/Audit/BitbucketClient.cs b/src/Bitbucket.Net/Audit/BitbucketClient.cs index c2969f8..9a14221 100644 --- a/src/Bitbucket.Net/Audit/BitbucketClient.cs +++ b/src/Bitbucket.Net/Audit/BitbucketClient.cs @@ -31,7 +31,7 @@ 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 async Task> GetProjectAuditEventsAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, @@ -68,7 +68,7 @@ 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 async Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index ad511c6..940e05a 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -446,7 +446,7 @@ private static async Task ExecuteWithNoContentAsync( /// 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 5c14c53..39f7d59 100644 --- a/src/Bitbucket.Net/Branches/BitbucketClient.cs +++ b/src/Bitbucket.Net/Branches/BitbucketClient.cs @@ -38,7 +38,7 @@ 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 async Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index 591fabc..d36f258 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -75,7 +75,7 @@ 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 async Task> GetBuildStatusForCommitAsync(string commitId, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs index a09675b..a241a6d 100644 --- a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs +++ b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs @@ -37,7 +37,7 @@ 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 async Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, int? maxPages = null, int? limit = null, int? start = null, @@ -111,7 +111,7 @@ 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 async Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs index 8747b88..f6de7a4 100644 --- a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs @@ -36,7 +36,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 async Task> GetAdminGroupsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -119,7 +119,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 async Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -158,7 +158,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 async Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -196,7 +196,7 @@ public async Task> GetAdminGroupMoreNonMembersAsync(string /// Optional avatar size. /// Cancellation token. /// A collection of users. - public async Task> GetAdminUsersAsync(string? filter = null, + public async Task> GetAdminUsersAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -351,7 +351,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 async Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -387,7 +387,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 async Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -590,7 +590,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 async Task> GetAdminGroupPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -663,7 +663,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 async Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -698,7 +698,7 @@ public async Task> GetAdminGroupPermissionsNon /// Optional avatar size. /// Cancellation token. /// A collection of user permissions. - public async Task> GetAdminUserPermissionsAsync(string? filter = null, + public async Task> GetAdminUserPermissionsAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -774,7 +774,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 async Task> GetAdminUserPermissionsNoneAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs index 3bf9725..2abdd56 100644 --- a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs @@ -38,7 +38,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 async Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, Roles? role = null, List? status = null, PullRequestOrders? order = PullRequestOrders.Newest, @@ -125,7 +125,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 async Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, int? maxPages = null, int? limit = 3, int? start = null, diff --git a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs index a44e41c..0ea436f 100644 --- a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs @@ -25,7 +25,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 async Task> GetGroupNamesAsync(string? filter = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs index 762e7bc..8b33851 100644 --- a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs @@ -35,7 +35,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 async Task> GetInboxPullRequestsAsync( int? maxPages = null, int? limit = 25, int? start = 0, diff --git a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs index 313445c..a2436d9 100644 --- a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs @@ -35,7 +35,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 async Task> GetRecentReposAsync(Permissions? permission = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs index 592ac82..a0e573d 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -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 async Task> GetBranchesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs index d6f1b21..ab33a78 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs @@ -20,7 +20,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 async Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, int? maxPages = null, int? limit = null, int? start = null, @@ -100,7 +100,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 async Task> GetCommitsAsync(string projectKey, string repositorySlug, string until, bool followRenames = false, bool ignoreMissing = false, @@ -215,7 +215,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 async Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, string? since = null, bool withComments = true, int? maxPages = null, @@ -296,7 +296,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 async Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, string path, string? since = null, int? maxPages = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs index fcdd17d..5305f66 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs @@ -21,7 +21,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 async Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, @@ -148,7 +148,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 async Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, string? fromRepo = null, int? maxPages = null, int? limit = null, @@ -269,7 +269,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 async Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs index d04b84d..4f3943c 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs @@ -60,7 +60,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 async Task> GetProjectsAsync( int? maxPages = null, int? limit = null, int? start = null, @@ -197,7 +197,7 @@ 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 async Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -280,7 +280,7 @@ 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 async Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -315,7 +315,7 @@ 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 async Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, @@ -396,7 +396,7 @@ 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 async Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs index 77a4dc9..2f468e6 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs @@ -103,7 +103,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 async Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, string path, AnchorStates anchorState = AnchorStates.Active, DiffTypes diffType = DiffTypes.Effective, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs index 1f17f63..aa93821 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs @@ -23,7 +23,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 async Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, long? fromId = null, PullRequestFromTypes? fromType = null, int? maxPages = null, @@ -112,7 +112,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 async Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, ChangeScopes changeScope = ChangeScopes.All, string? sinceId = null, string? untilId = null, @@ -203,7 +203,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 async Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, bool withCounts = false, int? maxPages = null, int? limit = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs index 6f2621e..e46e259 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -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 async Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, PullRequestDirections direction = PullRequestDirections.Incoming, string? filter = null, Roles? role = null, @@ -67,7 +67,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 async Task> GetPullRequestsAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -382,7 +382,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 async Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs index 517114b..fedc90c 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs @@ -18,7 +18,7 @@ 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 async Task> GetProjectRepositoriesAsync(string projectKey, int? maxPages = null, int? limit = null, int? start = null, @@ -192,7 +192,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 async Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -242,7 +242,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 async Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, int? maxPages = null, int? limit = null, int? start = null, @@ -315,7 +315,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 async Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -395,7 +395,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 async Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -433,7 +433,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 async Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, @@ -519,7 +519,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 async Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, string? filter = null, int? maxPages = null, int? limit = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs index 4c33059..818f000 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs @@ -90,7 +90,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 async Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, HookTypes? hookType = null, int? maxPages = null, int? limit = null, @@ -268,7 +268,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 async Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, string filterText, BranchOrderBy orderBy, int? maxPages = null, @@ -394,7 +394,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 async Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, string? @event = null, bool statistics = false, int? maxPages = null, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs index 7aadc85..87930fb 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs @@ -31,7 +31,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 async Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, int? maxPages = null, int? limit = null, int? start = null, @@ -154,7 +154,7 @@ public async Task GetPullRequestTaskCountAsync(string projec /// For servers prior to 9.0, use instead. /// /// - public async Task> GetPullRequestBlockerCommentsAsync( + public async Task> GetPullRequestBlockerCommentsAsync( string projectKey, string repositorySlug, long pullRequestId, @@ -433,7 +433,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 +450,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 +462,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/Repos/BitbucketClient.cs b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs index e5e7e5e..46464a4 100644 --- a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs @@ -30,7 +30,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 async Task> GetRepositoriesAsync( int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs index b301b4d..efa227f 100644 --- a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs @@ -38,7 +38,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 async Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs index 5af1890..976ee09 100644 --- a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs +++ b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs @@ -30,7 +30,7 @@ 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) { var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") @@ -38,7 +38,8 @@ public async Task> GetDefaultRe .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(); } /// @@ -98,7 +99,7 @@ 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) { var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") @@ -106,7 +107,8 @@ public async Task> GetDefaultRe .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(); } /// @@ -173,7 +175,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, @@ -195,6 +197,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/Jira/BitbucketClient.cs b/src/Bitbucket.Net/Jira/BitbucketClient.cs index 21f7dec..f6619c8 100644 --- a/src/Bitbucket.Net/Jira/BitbucketClient.cs +++ b/src/Bitbucket.Net/Jira/BitbucketClient.cs @@ -35,7 +35,7 @@ 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 async Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, int? maxPages = null, int? limit = null, int? start = null, @@ -94,12 +94,13 @@ 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) { 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/PersonalAccessTokens/BitbucketClient.cs b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs index 8843715..30b2867 100644 --- a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs +++ b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs @@ -34,7 +34,7 @@ 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 async Task> GetUserAccessTokensAsync(string userSlug, int? maxPages = null, int? limit = null, int? start = null, diff --git a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs index 9f13adf..d9d58f9 100644 --- a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs @@ -37,7 +37,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 async Task> GetProjectRefRestrictionsAsync(string projectKey, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, @@ -76,14 +76,15 @@ 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) { 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,7 +93,7 @@ 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) { return await CreateProjectRefRestrictionsAsync(projectKey, default, refRestrictions).ConfigureAwait(false); } @@ -161,7 +162,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 async Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, RefRestrictionTypes? type = null, RefMatcherTypes? matcherType = null, string? matcherId = null, @@ -201,14 +202,15 @@ 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) { 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,7 +220,7 @@ 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) { return await CreateRepositoryRefRestrictionsAsync(projectKey, repositorySlug, default, refRestrictions).ConfigureAwait(false); } diff --git a/src/Bitbucket.Net/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index 94ce24e..dd14ade 100644 --- a/src/Bitbucket.Net/Ssh/BitbucketClient.cs +++ b/src/Bitbucket.Net/Ssh/BitbucketClient.cs @@ -80,7 +80,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 async Task> GetProjectKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, @@ -115,7 +115,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 async Task> GetProjectKeysAsync(string projectKey, string? filter = null, Permissions? permission = null, int? maxPages = null, @@ -224,7 +224,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 async Task> GetRepoKeysAsync(int keyId, int? maxPages = null, int? limit = null, int? start = null, @@ -261,7 +261,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 async Task> GetRepoKeysAsync(string projectKey, string repositorySlug, string? filter = null, bool? effective = null, Permissions? permission = null, @@ -376,7 +376,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 async Task> GetUserKeysAsync(string? userSlug = null, int? maxPages = null, int? limit = null, int? start = null, From 7f618ed6324404bb7c850da608389e266b03b1f2 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:42:33 +0000 Subject: [PATCH 07/31] feat: add input validation guards on public API methods - Add ArgumentException.ThrowIfNullOrWhiteSpace() guards on ~130 public methods for URL-path parameters - Validate projectKey, repositorySlug, commitId, hookKey, userSlug, scmId, loggerName and other path segment parameters - Cover methods that bypass guarded URL-builder helpers - Add InputValidationTests with parameterized theories - Prevents malformed URLs and confusing server-side errors --- src/Bitbucket.Net/Audit/BitbucketClient.cs | 5 + src/Bitbucket.Net/Branches/BitbucketClient.cs | 16 +++ src/Bitbucket.Net/Builds/BitbucketClient.cs | 6 + .../CommentLikes/BitbucketClient.cs | 30 +++++ .../Core/Admin/BitbucketClient.cs | 4 + .../Core/Hooks/BitbucketClient.cs | 2 + .../Core/Logs/BitbucketClient.cs | 4 + .../Core/Projects/BitbucketClient.Branches.cs | 8 ++ .../Core/Projects/BitbucketClient.Commits.cs | 24 ++++ .../Core/Projects/BitbucketClient.Projects.cs | 44 ++++++- .../BitbucketClient.PullRequestDetails.cs | 2 + .../Projects/BitbucketClient.PullRequests.cs | 4 + .../Projects/BitbucketClient.Repositories.cs | 7 ++ .../BitbucketClient.RepositorySettings.cs | 32 ++++++ .../Core/Users/BitbucketClient.cs | 8 ++ .../DefaultReviewers/BitbucketClient.cs | 27 +++++ src/Bitbucket.Net/Git/BitbucketClient.cs | 15 +++ src/Bitbucket.Net/Jira/BitbucketClient.cs | 10 ++ .../PersonalAccessTokens/BitbucketClient.cs | 13 +++ .../RefRestrictions/BitbucketClient.cs | 30 +++++ src/Bitbucket.Net/RefSync/BitbucketClient.cs | 9 ++ src/Bitbucket.Net/Ssh/BitbucketClient.cs | 29 +++++ .../UnitTests/InputValidationTests.cs | 107 ++++++++++++++++++ 23 files changed, 433 insertions(+), 3 deletions(-) create mode 100644 test/Bitbucket.Net.Tests/UnitTests/InputValidationTests.cs diff --git a/src/Bitbucket.Net/Audit/BitbucketClient.cs b/src/Bitbucket.Net/Audit/BitbucketClient.cs index 9a14221..8a6de7a 100644 --- a/src/Bitbucket.Net/Audit/BitbucketClient.cs +++ b/src/Bitbucket.Net/Audit/BitbucketClient.cs @@ -38,6 +38,8 @@ public async Task> GetProjectAuditEventsAsync(string p int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -75,6 +77,9 @@ public async Task> GetProjectRepoAuditEventsAsync(stri int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, diff --git a/src/Bitbucket.Net/Branches/BitbucketClient.cs b/src/Bitbucket.Net/Branches/BitbucketClient.cs index 39f7d59..88d2736 100644 --- a/src/Bitbucket.Net/Branches/BitbucketClient.cs +++ b/src/Bitbucket.Net/Branches/BitbucketClient.cs @@ -44,6 +44,10 @@ public async Task> GetCommitBranchInfoAsync(string pro int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + ArgumentException.ThrowIfNullOrWhiteSpace(fullSha); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -71,6 +75,9 @@ public async Task> GetCommitBranchInfoAsync(string pro /// 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 +96,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 +126,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, diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index d36f258..b33161a 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -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) @@ -81,6 +83,8 @@ public async Task> GetBuildStatusForCommitAsync(strin int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -107,6 +111,8 @@ public async Task> GetBuildStatusForCommitAsync(strin /// true if the association was successful; otherwise, false. public async Task AssociateBuildStatusWithCommitAsync(string commitId, BuildStatus buildStatus, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var response = await GetBuildsUrl($"/commits/{commitId}") .SendAsync(HttpMethod.Post, CreateJsonContent(buildStatus), cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs index a241a6d..219e3c5 100644 --- a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs +++ b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs @@ -44,6 +44,11 @@ public async Task> GetCommitCommentLikesAsync(string project 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, @@ -74,6 +79,11 @@ public async Task> GetCommitCommentLikesAsync(string project /// 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 +102,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); @@ -117,6 +132,11 @@ public async Task> GetPullRequestCommentLikesAsync(string pr 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, @@ -146,6 +166,11 @@ public async Task> GetPullRequestCommentLikesAsync(string pr /// 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 +189,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/Core/Admin/BitbucketClient.cs b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs index f6de7a4..8829d07 100644 --- a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs @@ -809,6 +809,8 @@ public async Task> GetAdminUserPermissionsNoneAsync(string? /// 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 +827,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/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/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/Projects/BitbucketClient.Branches.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs index a0e573d..417efc1 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -194,6 +194,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 +231,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 +261,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 +292,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 ab33a78..2fe2d49 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs @@ -189,6 +189,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, @@ -223,6 +225,8 @@ public async Task> GetCommitChangesAsync(string projectKey int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -264,6 +268,8 @@ public IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, s int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -304,6 +310,8 @@ public async Task> GetCommitCommentsAsync(string projectK int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(commitId); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -337,6 +345,8 @@ public async Task> GetCommitCommentsAsync(string projectK 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 +374,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 +397,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 +420,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 +458,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 +502,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 +541,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 +560,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.Projects.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs index 4f3943c..8aa0d91 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs @@ -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. @@ -148,6 +156,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); @@ -164,6 +174,8 @@ public async Task DeleteProjectAsync(string projectKey, CancellationToken /// The updated project. public async Task UpdateProjectAsync(string projectKey, ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var response = await GetProjectsUrl($"/{projectKey}") .SendAsync(HttpMethod.Put, CreateJsonContent(projectDefinition), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -179,6 +191,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); @@ -204,6 +218,8 @@ public async Task> GetProjectUserPermissionsAsync( int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -233,6 +249,9 @@ public async Task> GetProjectUserPermissionsAsync( /// 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 +275,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, @@ -286,6 +308,8 @@ public async Task> GetProjectUserPermissionsNoneAsyn int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -321,6 +345,8 @@ public async Task> GetProjectGroupPermissionsAsyn int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -349,6 +375,9 @@ public async Task> GetProjectGroupPermissionsAsyn /// 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 +401,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, @@ -402,6 +434,8 @@ public async Task> GetProjectGroupPermissionsNoneAsy int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -430,6 +464,8 @@ public async Task> GetProjectGroupPermissionsNoneAsy /// 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 +480,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.PullRequestDetails.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs index aa93821..205e41e 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs @@ -368,6 +368,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 e46e259..2bc575d 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -515,6 +515,8 @@ public async Task UpdatePullRequestParticipantStatus(string project ParticipantStatus participantStatus, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var data = new { user = named, @@ -541,6 +543,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 fedc90c..818cacc 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs @@ -24,6 +24,8 @@ public async Task> GetProjectRepositoriesAsync(string int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -51,6 +53,8 @@ public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string pro int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -78,6 +82,9 @@ public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string pro /// The created repository. public async Task CreateProjectRepositoryAsync(string projectKey, string repositoryName, string scmId = "git", CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositoryName); + var data = new { name = repositoryName, diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs index 818f000..de1b111 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs @@ -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, @@ -126,6 +128,8 @@ public async Task> GetProjectRepositoryHooksSettingsAsync(st /// 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 +147,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 +167,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 +186,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 +205,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 +226,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 +244,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 +264,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) @@ -375,6 +393,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); @@ -471,6 +491,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), @@ -496,6 +518,8 @@ public async Task GetProjectRepositoryWebHookAsync(string projectKey, s public async Task UpdateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string webHookId, WebHook webHook, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(webHookId); + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") .SendAsync(HttpMethod.Put, CreateJsonContent(webHook), cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -514,6 +538,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 +564,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 +595,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 +616,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/Users/BitbucketClient.cs b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs index efa227f..209e443 100644 --- a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs @@ -121,6 +121,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 +139,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 +156,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 +174,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 976ee09..bfb17e2 100644 --- a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs +++ b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs @@ -33,6 +33,8 @@ private IFlurlRequest GetDefaultReviewersUrl(string path) => GetDefaultReviewers 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) @@ -51,6 +53,8 @@ public async Task> GetDefault /// 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); @@ -68,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); @@ -84,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); @@ -102,6 +112,9 @@ public async Task DeleteDefaultReviewerConditionAsync(string projectKey, s 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) @@ -121,6 +134,9 @@ public async Task> GetDefault /// 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); @@ -139,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); @@ -156,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); @@ -183,6 +207,9 @@ public async Task> GetDefaultReviewersAsync(string projectKe int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["sourceRepoId"] = sourceRepoId, 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/Jira/BitbucketClient.cs b/src/Bitbucket.Net/Jira/BitbucketClient.cs index f6619c8..c94c86e 100644 --- a/src/Bitbucket.Net/Jira/BitbucketClient.cs +++ b/src/Bitbucket.Net/Jira/BitbucketClient.cs @@ -41,6 +41,8 @@ public async Task> GetChangeSetsAsync(string issueKey, int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(issueKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -71,6 +73,11 @@ public async Task> GetChangeSetsAsync(string issueKey, /// 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#", @@ -96,6 +103,9 @@ public async Task CreateJiraIssueAsync(string pullRequestCommentId, s /// A collection of Jira issue links. 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); diff --git a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs index 30b2867..1df17d9 100644 --- a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs +++ b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs @@ -41,6 +41,8 @@ public async Task> GetUserAccessTokensAsync(string us int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(userSlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -69,6 +71,8 @@ public async Task> GetUserAccessTokensAsync(string us /// 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 +90,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 +111,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 +130,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 d9d58f9..299ec51 100644 --- a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs @@ -47,6 +47,8 @@ public async Task> GetProjectRefRestrictionsAsync( int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), @@ -78,6 +80,8 @@ public async Task> GetProjectRefRestrictionsAsync( /// The created reference restrictions. 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) @@ -95,6 +99,8 @@ public async Task> CreateProjectRefRestrictionsAsy /// The created reference restrictions. public async Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + return await CreateProjectRefRestrictionsAsync(projectKey, default, refRestrictions).ConfigureAwait(false); } @@ -107,6 +113,8 @@ public async Task> CreateProjectRefRestrictionsAsy /// 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); @@ -124,6 +132,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) @@ -141,6 +151,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); @@ -172,6 +184,9 @@ public async Task> GetRepositoryRefRestrictionsAsy int? avatarSize = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), @@ -204,6 +219,9 @@ public async Task> GetRepositoryRefRestrictionsAsy /// The created reference restrictions. 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) @@ -222,6 +240,9 @@ public async Task> CreateRepositoryRefRestrictions /// The created reference restrictions. 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); } @@ -235,6 +256,9 @@ public async Task> CreateRepositoryRefRestrictions /// 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); @@ -254,6 +278,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) @@ -272,6 +299,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/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/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index dd14ade..bb617c7 100644 --- a/src/Bitbucket.Net/Ssh/BitbucketClient.cs +++ b/src/Bitbucket.Net/Ssh/BitbucketClient.cs @@ -123,6 +123,8 @@ public async Task> GetProjectKeysAsync(string projectK int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -153,6 +155,9 @@ public async Task> GetProjectKeysAsync(string projectK /// 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 +180,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 +198,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 +217,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); @@ -270,6 +281,9 @@ public async Task> GetRepoKeysAsync(string projectK int? start = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); + ArgumentException.ThrowIfNullOrWhiteSpace(repositorySlug); + var queryParamValues = new Dictionary(StringComparer.Ordinal) { ["limit"] = limit, @@ -302,6 +316,10 @@ public async Task> GetRepoKeysAsync(string projectK /// 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 +343,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 +363,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 +384,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); @@ -410,6 +437,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/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 From caf157ab0af4689fe44800ff19e84c92b6845697 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:09:13 +0000 Subject: [PATCH 08/31] docs: add P1+P2 changes to changelog for 1.0.0 release --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 665cd78..ef13d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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. + +### Changed + +- **Source-gen-only deserialization** (Spec 001): 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** (Spec 002): `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** (Spec 003): 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. +- **`IReadOnlyList` return types** (Spec 006): All 27 buffered + collection methods now return `Task>` instead of + `Task>`, communicating immutability and preventing + multiple-enumeration bugs. + +### Added + +- **`IDisposable` on `BitbucketClient`** (Spec 004): 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** (Spec 005): 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** (Spec 007): ~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. + +### Testing + +- Added `SourceGenCoverageTests` validating all model types are + registered in `BitbucketJsonContext`. +- Added `BitbucketClientDisposeTests` (6 tests) covering disposal + semantics and ownership tracking. +- Added `ArchitecturalTests` verifying all HTTP calls have error + handlers (`HandleResponseAsync`, `ExecuteAsync`, or `StatusCode`). +- Added `InputValidationTests` (17 parameterized theories) covering + null/empty/whitespace rejection for key path-segment parameters. +- Total test count increased from 696 to 797 (+101 new tests). + ## [0.2.0] - 2026-02-08 ### Breaking Changes From ecef7a63bfbd430d36709cc3398ddc6a2bc9b8a3 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:13:03 +0000 Subject: [PATCH 09/31] feat: freeze JsonSerializerOptions explicitly via MakeReadOnly() - Extract s_jsonOptions and s_writeJsonOptions initialization into CreateReadOptions() and CreateWriteOptions() factory methods - Call MakeReadOnly() after configuration to prevent accidental mutation from any thread - Add JsonSerializerOptions_AreExplicitlyFrozen architectural test - Update CHANGELOG.md --- CHANGELOG.md | 9 ++- src/Bitbucket.Net/BitbucketClient.cs | 75 +++++++++++-------- .../UnitTests/ArchitecturalTests.cs | 21 ++++++ 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef13d5f..fe0e100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Dictionary` to `FrozenDictionary`. Added reverse `FrozenDictionary` with `StringComparer.OrdinalIgnoreCase` for O(1) string-to-enum lookups. +- **Frozen `JsonSerializerOptions`** (Spec 013): Both `s_jsonOptions` + and `s_writeJsonOptions` are now explicitly frozen via + `MakeReadOnly()` at construction time, preventing accidental + mutation from any thread. - **`IReadOnlyList` return types** (Spec 006): All 27 buffered collection methods now return `Task>` instead of `Task>`, communicating immutability and preventing @@ -63,10 +67,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `BitbucketClientDisposeTests` (6 tests) covering disposal semantics and ownership tracking. - Added `ArchitecturalTests` verifying all HTTP calls have error - handlers (`HandleResponseAsync`, `ExecuteAsync`, or `StatusCode`). + handlers (`HandleResponseAsync`, `ExecuteAsync`, or `StatusCode`) + and that `JsonSerializerOptions` are explicitly frozen. - Added `InputValidationTests` (17 parameterized theories) covering null/empty/whitespace rejection for key path-segment parameters. -- Total test count increased from 696 to 797 (+101 new tests). +- Total test count increased from 696 to 798 (+102 new tests). ## [0.2.0] - 2026-02-08 diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 940e05a..c38bd02 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -26,44 +26,57 @@ namespace Bitbucket.Net; /// public partial class BitbucketClient : IDisposable { - 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() { - 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 = + var options = new JsonSerializerOptions { - 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() - }, - }; + 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 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() + }, + }; + 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 readonly JsonSerializerOptions s_writeJsonOptions = new(s_jsonOptions) + private static JsonSerializerOptions CreateWriteOptions() { - TypeInfoResolver = JsonTypeInfoResolver.Combine( - BitbucketJsonContext.Default, - new DefaultJsonTypeInfoResolver() - ), - }; + var options = new JsonSerializerOptions(s_jsonOptions) + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + BitbucketJsonContext.Default, + new DefaultJsonTypeInfoResolver() + ), + }; + options.MakeReadOnly(); + return options; + } private static readonly ISerializer s_serializer = new DefaultJsonSerializer(s_jsonOptions); diff --git a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs index 0c0731e..f14b0a4 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System.Text.Json; using System.Text.RegularExpressions; using Xunit; @@ -12,6 +14,25 @@ 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, From 1d9341f9de5529264be948303839948beebb7023 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:37:02 +0000 Subject: [PATCH 10/31] refactor: consolidate enum mappings into generic EnumMap - Introduce EnumMap as single source of truth for all 25 enum-to-string mappings (BitbucketEnumMaps static registry) - Replace 13 individual converter subclasses with unified BitbucketEnumConverterFactory using generic JsonEnumConverter - Slim BitbucketHelpers.cs from ~1100 to ~490 lines; methods now delegate to EnumMap instances - Add public ToApiString() extension methods on all enum types - Update CHANGELOG.md --- CHANGELOG.md | 6 + src/Bitbucket.Net/BitbucketClient.cs | 14 +- .../Common/BitbucketEnumExtensions.cs | 128 ++++ src/Bitbucket.Net/Common/BitbucketEnumMaps.cs | 221 ++++++ src/Bitbucket.Net/Common/BitbucketHelpers.cs | 658 ++---------------- .../BitbucketEnumConverterFactory.cs | 48 ++ .../BlockerCommentStateConverter.cs | 19 - .../Converters/CommentSeverityConverter.cs | 19 - .../Common/Converters/FileTypesConverter.cs | 21 - .../Common/Converters/HookTypesConverter.cs | 21 - .../Common/Converters/JsonEnumConverter.cs | 39 +- .../Common/Converters/LineTypesConverter.cs | 21 - .../Converters/ParticipantStatusConverter.cs | 21 - .../Common/Converters/PermissionsConverter.cs | 39 -- .../Converters/PullRequestStatesConverter.cs | 21 - .../RefRestrictionTypesConverter.cs | 21 - .../Common/Converters/RolesConverter.cs | 21 - .../Common/Converters/ScopeTypesConverter.cs | 21 - .../Converters/SynchronizeActionsConverter.cs | 21 - .../Converters/WebHookOutcomesConverter.cs | 21 - src/Bitbucket.Net/Common/EnumMap.cs | 79 +++ .../Models/Core/Admin/GroupPermission.cs | 3 - .../Models/Core/Admin/UserPermission.cs | 3 - .../Models/Core/Projects/BlockerComment.cs | 2 - .../Models/Core/Projects/CommentAnchor.cs | 5 - .../Models/Core/Projects/HookDetails.cs | 4 - .../Models/Core/Projects/HookScope.cs | 4 - .../Models/Core/Projects/Participant.cs | 4 - .../Models/Core/Projects/PullRequestInfo.cs | 4 - .../Models/Core/Projects/WebHookResult.cs | 4 - .../PersonalAccessTokens/AccessTokenCreate.cs | 3 - .../RefRestrictions/RefRestrictionBase.cs | 3 - .../Models/RefSync/Synchronize.cs | 4 - src/Bitbucket.Net/Models/Ssh/KeyBase.cs | 3 - .../Serialization/BitbucketJsonContext.cs | 4 +- .../UnitTests/JsonConverterTests.cs | 21 +- 36 files changed, 578 insertions(+), 973 deletions(-) create mode 100644 src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs create mode 100644 src/Bitbucket.Net/Common/BitbucketEnumMaps.cs create mode 100644 src/Bitbucket.Net/Common/Converters/BitbucketEnumConverterFactory.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/RolesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs delete mode 100644 src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs create mode 100644 src/Bitbucket.Net/Common/EnumMap.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0e100..1a6829a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Dictionary` to `FrozenDictionary`. Added reverse `FrozenDictionary` with `StringComparer.OrdinalIgnoreCase` for O(1) string-to-enum lookups. +- **Consolidated enum mappings** (Spec 008): 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`** (Spec 013): Both `s_jsonOptions` and `s_writeJsonOptions` are now explicitly frozen via `MakeReadOnly()` at construction time, preventing accidental diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index c38bd02..8d6a988 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -43,19 +43,7 @@ private static JsonSerializerOptions CreateReadOptions() { 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() + new BitbucketEnumConverterFactory(), }, }; options.MakeReadOnly(); diff --git a/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs b/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs new file mode 100644 index 0000000..823b1a9 --- /dev/null +++ b/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs @@ -0,0 +1,128 @@ +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; + +/// +/// Extension methods for converting Bitbucket API enums to their wire-format strings. +/// +public static class BitbucketEnumExtensions +{ + public static string ToApiString(this BranchOrderBy value) + => BitbucketEnumMaps.BranchOrderBy.ToApiString(value); + + public static string ToApiString(this PullRequestDirections value) + => BitbucketEnumMaps.PullRequestDirections.ToApiString(value); + + public static string ToApiString(this PullRequestStates value) + => BitbucketEnumMaps.PullRequestStates.ToApiString(value); + + public static string? ToApiString(this PullRequestStates? value) + => BitbucketEnumMaps.PullRequestStates.ToApiString(value); + + public static string ToApiString(this PullRequestOrders value) + => BitbucketEnumMaps.PullRequestOrders.ToApiString(value); + + public static string? ToApiString(this PullRequestOrders? value) + => BitbucketEnumMaps.PullRequestOrders.ToApiString(value); + + public static string ToApiString(this PullRequestFromTypes value) + => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(value); + + public static string? ToApiString(this PullRequestFromTypes? value) + => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(value); + + public static string ToApiString(this Permissions value) + => BitbucketEnumMaps.Permissions.ToApiString(value); + + public static string? ToApiString(this Permissions? value) + => BitbucketEnumMaps.Permissions.ToApiString(value); + + public static string ToApiString(this MergeCommits value) + => BitbucketEnumMaps.MergeCommits.ToApiString(value); + + public static string ToApiString(this Roles value) + => BitbucketEnumMaps.Roles.ToApiString(value); + + public static string? ToApiString(this Roles? value) + => BitbucketEnumMaps.Roles.ToApiString(value); + + public static string ToApiString(this LineTypes value) + => BitbucketEnumMaps.LineTypes.ToApiString(value); + + public static string? ToApiString(this LineTypes? value) + => BitbucketEnumMaps.LineTypes.ToApiString(value); + + public static string ToApiString(this FileTypes value) + => BitbucketEnumMaps.FileTypes.ToApiString(value); + + public static string? ToApiString(this FileTypes? value) + => BitbucketEnumMaps.FileTypes.ToApiString(value); + + public static string ToApiString(this ChangeScopes value) + => BitbucketEnumMaps.ChangeScopes.ToApiString(value); + + public static string ToApiString(this LogLevels value) + => BitbucketEnumMaps.LogLevels.ToApiString(value); + + public static string ToApiString(this ParticipantStatus value) + => BitbucketEnumMaps.ParticipantStatus.ToApiString(value); + + public static string ToApiString(this HookTypes value) + => BitbucketEnumMaps.HookTypes.ToApiString(value); + + public static string ToApiString(this ScopeTypes value) + => BitbucketEnumMaps.ScopeTypes.ToApiString(value); + + public static string ToApiString(this ArchiveFormats value) + => BitbucketEnumMaps.ArchiveFormats.ToApiString(value); + + public static string ToApiString(this WebHookOutcomes value) + => BitbucketEnumMaps.WebHookOutcomes.ToApiString(value); + + public static string? ToApiString(this WebHookOutcomes? value) + => BitbucketEnumMaps.WebHookOutcomes.ToApiString(value); + + public static string ToApiString(this AnchorStates value) + => BitbucketEnumMaps.AnchorStates.ToApiString(value); + + public static string ToApiString(this DiffTypes value) + => BitbucketEnumMaps.DiffTypes.ToApiString(value); + + public static string? ToApiString(this DiffTypes? value) + => BitbucketEnumMaps.DiffTypes.ToApiString(value); + + public static string ToApiString(this TagTypes value) + => BitbucketEnumMaps.TagTypes.ToApiString(value); + + public static string ToApiString(this RefRestrictionTypes value) + => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(value); + + public static string? ToApiString(this RefRestrictionTypes? value) + => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(value); + + public static string ToApiString(this RefMatcherTypes value) + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(value); + + public static string? ToApiString(this RefMatcherTypes? value) + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(value); + + public static string ToApiString(this SynchronizeActions value) + => BitbucketEnumMaps.SynchronizeActions.ToApiString(value); + + public static string ToApiString(this BlockerCommentState value) + => BitbucketEnumMaps.BlockerCommentState.ToApiString(value); + + public static string? ToApiString(this BlockerCommentState? value) + => BitbucketEnumMaps.BlockerCommentState.ToApiString(value); + + public static string ToApiString(this CommentSeverity value) + => BitbucketEnumMaps.CommentSeverity.ToApiString(value); + + public static string? ToApiString(this CommentSeverity? value) + => BitbucketEnumMaps.CommentSeverity.ToApiString(value); +} \ No newline at end of file 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 2f77a80..0b3ac84 100644 --- a/src/Bitbucket.Net/Common/BitbucketHelpers.cs +++ b/src/Bitbucket.Net/Common/BitbucketHelpers.cs @@ -4,13 +4,12 @@ using Bitbucket.Net.Models.Git; using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Models.RefSync; -using System.Collections.Frozen; namespace Bitbucket.Net.Common; /// /// Helper methods for converting between Bitbucket enum values and their wire-format string representations. -/// Uses for optimal read-only lookup performance. +/// Delegates to for all mappings. /// public static class BitbucketHelpers { @@ -45,12 +44,6 @@ public static string BoolToString(bool value) => value #region BranchOrderBy - private static readonly FrozenDictionary s_stringByBranchOrderBy = new Dictionary - { - [BranchOrderBy.Alphabetical] = "ALPHABETICAL", - [BranchOrderBy.Modification] = "MODIFICATION", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -58,25 +51,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 FrozenDictionary s_stringByPullRequestDirection = new Dictionary - { - [PullRequestDirections.Incoming] = "INCOMING", - [PullRequestDirections.Outgoing] = "OUTGOING", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -84,30 +64,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 FrozenDictionary s_stringByPullRequestState = new Dictionary - { - [PullRequestStates.Open] = "OPEN", - [PullRequestStates.Declined] = "DECLINED", - [PullRequestStates.Merged] = "MERGED", - [PullRequestStates.All] = "ALL", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_pullRequestStateByString = - s_stringByPullRequestState.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -115,23 +77,15 @@ 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; + public static string? PullRequestStateToString(PullRequestStates? state) + => BitbucketEnumMaps.PullRequestStates.ToApiString(state); /// /// Parses a Bitbucket pull request state string into a value. @@ -140,25 +94,12 @@ public static string PullRequestStateToString(PullRequestStates state) /// The parsed state. /// Thrown when the value is not recognized. public static PullRequestStates StringToPullRequestState(string s) - { - if (!s_pullRequestStateByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown pull request state: {s}"); - } - - return result; - } + => BitbucketEnumMaps.PullRequestStates.FromApiString(s); #endregion #region PullRequestOrders - private static readonly FrozenDictionary s_stringByPullRequestOrder = new Dictionary - { - [PullRequestOrders.Newest] = "NEWEST", - [PullRequestOrders.Oldest] = "OLDEST", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -166,75 +107,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 FrozenDictionary s_stringByPullRequestFromType = new Dictionary - { - [PullRequestFromTypes.Comment] = "COMMENT", - [PullRequestFromTypes.Activity] = "ACTIVITY", - }.ToFrozenDictionary(); - + /// + /// 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 FrozenDictionary s_stringByPermissions = new Dictionary - { - [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", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_permissionByString = - s_stringByPermissions.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -242,23 +149,15 @@ 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; + public static string? PermissionToString(Permissions? permission) + => BitbucketEnumMaps.Permissions.ToApiString(permission); /// /// Parses a Bitbucket permission string into a value. @@ -267,26 +166,12 @@ public static string PermissionToString(Permissions permission) /// The parsed permission. /// Thrown when the value is not recognized. public static Permissions StringToPermission(string s) - { - if (!s_permissionByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown permission: {s}"); - } - - return result; - } + => BitbucketEnumMaps.Permissions.FromApiString(s); #endregion #region MergeCommits - private static readonly FrozenDictionary s_stringByMergeCommits = new Dictionary - { - [MergeCommits.Exclude] = "exclude", - [MergeCommits.Include] = "include", - [MergeCommits.Only] = "only", - }.ToFrozenDictionary(); - /// /// Converts a preference to the Bitbucket API string. /// @@ -294,29 +179,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 FrozenDictionary s_stringByRoles = new Dictionary - { - [Roles.Author] = "AUTHOR", - [Roles.Reviewer] = "REVIEWER", - [Roles.Participant] = "PARTICIPANT", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_roleByString = - s_stringByRoles.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a pull request value to the Bitbucket API string. /// @@ -324,23 +192,15 @@ 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; + public static string? RoleToString(Roles? role) + => BitbucketEnumMaps.Roles.ToApiString(role); /// /// Parses a pull request role string into a value. @@ -349,29 +209,12 @@ public static string RoleToString(Roles role) /// The parsed role. /// Thrown when the value is not recognized. public static Roles StringToRole(string s) - { - if (!s_roleByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown role: {s}"); - } - - return result; - } + => BitbucketEnumMaps.Roles.FromApiString(s); #endregion #region LineTypes - private static readonly FrozenDictionary s_stringByLineTypes = new Dictionary - { - [LineTypes.Added] = "ADDED", - [LineTypes.Removed] = "REMOVED", - [LineTypes.Context] = "CONTEXT", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_lineTypeByString = - s_stringByLineTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -379,14 +222,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. @@ -394,11 +230,7 @@ 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; - } + => BitbucketEnumMaps.LineTypes.ToApiString(lineType); /// /// Parses a line type string into a value. @@ -407,28 +239,12 @@ public static string LineTypeToString(LineTypes lineType) /// The parsed line type. /// Thrown when the value is not recognized. public static LineTypes StringToLineType(string s) - { - if (!s_lineTypeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown line type: {s}"); - } - - return result; - } + => BitbucketEnumMaps.LineTypes.FromApiString(s); #endregion #region FileTypes - private static readonly FrozenDictionary s_stringByFileTypes = new Dictionary - { - [FileTypes.From] = "FROM", - [FileTypes.To] = "TO", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_fileTypeByString = - s_stringByFileTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -436,14 +252,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. @@ -451,11 +260,7 @@ 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; - } + => BitbucketEnumMaps.FileTypes.ToApiString(fileType); /// /// Parses a file type string into a value. @@ -464,26 +269,12 @@ public static string FileTypeToString(FileTypes fileType) /// The parsed file type. /// Thrown when the value is not recognized. public static FileTypes StringToFileType(string s) - { - if (!s_fileTypeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown file type: {s}"); - } - - return result; - } + => BitbucketEnumMaps.FileTypes.FromApiString(s); #endregion #region ChangeScopes - private static readonly FrozenDictionary s_stringByChangeScopes = new Dictionary - { - [ChangeScopes.All] = "ALL", - [ChangeScopes.Unreviewed] = "UNREVIEWED", - [ChangeScopes.Range] = "RANGE", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -491,31 +282,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 FrozenDictionary s_stringByLogLevels = new Dictionary - { - [LogLevels.Trace] = "TRACE", - [LogLevels.Debug] = "DEBUG", - [LogLevels.Info] = "INFO", - [LogLevels.Warn] = "WARN", - [LogLevels.Error] = "ERROR", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_logLevelByString = - s_stringByLogLevels.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -523,14 +295,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. @@ -539,29 +304,12 @@ public static string LogLevelToString(LogLevels logLevel) /// The parsed log level. /// Thrown when the value is not recognized. public static LogLevels StringToLogLevel(string s) - { - if (!s_logLevelByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown log level: {s}"); - } - - return result; - } + => BitbucketEnumMaps.LogLevels.FromApiString(s); #endregion #region ParticipantStatus - private static readonly FrozenDictionary s_stringByParticipantStatus = new Dictionary - { - [ParticipantStatus.Approved] = "APPROVED", - [ParticipantStatus.NeedsWork] = "NEEDS_WORK", - [ParticipantStatus.Unapproved] = "UNAPPROVED", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_participantStatusByString = - s_stringByParticipantStatus.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -569,14 +317,7 @@ 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; - } + => BitbucketEnumMaps.ParticipantStatus.ToApiString(participantStatus); /// /// Parses a participant status string into a value. @@ -585,29 +326,12 @@ public static string ParticipantStatusToString(ParticipantStatus participantStat /// The parsed status. /// Thrown when the value is not recognized. public static ParticipantStatus StringToParticipantStatus(string s) - { - if (!s_participantStatusByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown participant status: {s}"); - } - - return result; - } + => BitbucketEnumMaps.ParticipantStatus.FromApiString(s); #endregion #region HookTypes - private static readonly FrozenDictionary s_stringByHookTypes = new Dictionary - { - [HookTypes.PreReceive] = "PRE_RECEIVE", - [HookTypes.PostReceive] = "POST_RECEIVE", - [HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_hookTypeByString = - s_stringByHookTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a hook value to the Bitbucket API string. /// @@ -615,14 +339,7 @@ public static ParticipantStatus StringToParticipantStatus(string s) /// 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; - } + => BitbucketEnumMaps.HookTypes.ToApiString(hookType); /// /// Parses a hook type string into a value. @@ -631,29 +348,12 @@ public static string HookTypeToString(HookTypes hookType) /// The parsed hook type. /// Thrown when the value is not recognized. public static HookTypes StringToHookType(string s) - { - if (!s_hookTypeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown hook type: {s}"); - } - - return result; - } + => BitbucketEnumMaps.HookTypes.FromApiString(s); #endregion #region ScopeTypes - private static readonly FrozenDictionary s_stringByScopeTypes = new Dictionary - { - [ScopeTypes.Global] = "GLOBAL", - [ScopeTypes.Project] = "PROJECT", - [ScopeTypes.Repository] = "REPOSITORY", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_scopeTypeByString = - s_stringByScopeTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -661,14 +361,7 @@ public static HookTypes StringToHookType(string s) /// 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; - } + => BitbucketEnumMaps.ScopeTypes.ToApiString(scopeType); /// /// Parses a scope type string into a value. @@ -677,27 +370,12 @@ public static string ScopeTypeToString(ScopeTypes scopeType) /// The parsed scope type. /// Thrown when the value is not recognized. public static ScopeTypes StringToScopeType(string s) - { - if (!s_scopeTypeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown scope type: {s}"); - } - - return result; - } + => BitbucketEnumMaps.ScopeTypes.FromApiString(s); #endregion #region ArchiveFormats - private static readonly FrozenDictionary s_stringByArchiveFormats = new Dictionary - { - [ArchiveFormats.Zip] = "zip", - [ArchiveFormats.Tar] = "tar", - [ArchiveFormats.TarGz] = "tar.gz", - [ArchiveFormats.Tgz] = "tgz", - }.ToFrozenDictionary(); - /// /// Converts an value to the Bitbucket API string. /// @@ -705,29 +383,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 FrozenDictionary s_stringByWebHookOutcomes = new Dictionary - { - [WebHookOutcomes.Success] = "SUCCESS", - [WebHookOutcomes.Failure] = "FAILURE", - [WebHookOutcomes.Error] = "ERROR", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_webHookOutcomeByString = - s_stringByWebHookOutcomes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -735,23 +396,15 @@ 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; + public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) + => BitbucketEnumMaps.WebHookOutcomes.ToApiString(webHookOutcome); /// /// Parses a webhook outcome string into a value. @@ -760,26 +413,12 @@ public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) /// The parsed outcome. /// Thrown when the value is not recognized. public static WebHookOutcomes StringToWebHookOutcome(string s) - { - if (!s_webHookOutcomeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown web hook outcome: {s}"); - } - - return result; - } + => BitbucketEnumMaps.WebHookOutcomes.FromApiString(s); #endregion #region AnchorStates - private static readonly FrozenDictionary s_stringByAnchorStates = new Dictionary - { - [AnchorStates.Active] = "ACTIVE", - [AnchorStates.Orphaned] = "ORPHANED", - [AnchorStates.All] = "ALL", - }.ToFrozenDictionary(); - /// /// Converts an value to the Bitbucket API string. /// @@ -787,26 +426,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 FrozenDictionary s_stringByDiffTypes = new Dictionary - { - [DiffTypes.Effective] = "EFFECTIVE", - [DiffTypes.Range] = "RANGE", - [DiffTypes.Commit] = "COMMIT", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -814,14 +439,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. @@ -829,22 +447,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 FrozenDictionary s_stringByTagTypes = new Dictionary - { - [TagTypes.LightWeight] = "LIGHTWEIGHT", - [TagTypes.Annotated] = "ANNOTATED", - }.ToFrozenDictionary(); - /// /// Converts a value to the Bitbucket API string. /// @@ -852,30 +460,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 FrozenDictionary s_stringByRefRestrictionTypes = new Dictionary - { - [RefRestrictionTypes.AllChanges] = "read-only", - [RefRestrictionTypes.RewritingHistory] = "fast-forward-only", - [RefRestrictionTypes.Deletion] = "no-deletes", - [RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_refRestrictionTypeByString = - s_stringByRefRestrictionTypes.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -883,14 +473,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. @@ -898,11 +481,7 @@ 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; - } + => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(refRestrictionType); /// /// Parses a ref restriction string into a value. @@ -911,36 +490,20 @@ public static string RefRestrictionTypeToString(RefRestrictionTypes refRestricti /// The parsed restriction. /// Thrown when the value is not recognized. public static RefRestrictionTypes StringToRefRestrictionType(string s) - { - if (!s_refRestrictionTypeByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown ref restriction type: {s}"); - } - - return result; - } + => BitbucketEnumMaps.RefRestrictionTypes.FromApiString(s); #endregion #region RefMatcherTypes - private static readonly FrozenDictionary s_stringByRefMatcherTypes = new Dictionary - { - [RefMatcherTypes.Branch] = "BRANCH", - [RefMatcherTypes.Pattern] = "PATTERN", - [RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", - [RefMatcherTypes.ModelBranch] = "MODEL_BRANCH", - }.ToFrozenDictionary(); - + /// + /// Converts a value to the Bitbucket API string. + /// + /// The matcher type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) - { - if (!s_stringByRefMatcherTypes.TryGetValue(refMatcherType, out string? result)) - { - throw new ArgumentException($"Unknown ref matcher type: {refMatcherType}"); - } - - return result; - } + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(refMatcherType); /// /// Converts an optional value to the Bitbucket API string. @@ -948,25 +511,12 @@ private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) /// 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; - } + => BitbucketEnumMaps.RefMatcherTypes.ToApiString(refMatcherType); #endregion #region SynchronizeActions - private static readonly FrozenDictionary s_stringBySynchronizeActions = new Dictionary - { - [SynchronizeActions.Merge] = "MERGE", - [SynchronizeActions.Discard] = "DISCARD", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_synchronizeActionByString = - s_stringBySynchronizeActions.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -974,14 +524,7 @@ private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) /// 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; - } + => BitbucketEnumMaps.SynchronizeActions.ToApiString(synchronizeAction); /// /// Parses a synchronization action string into a value. @@ -990,28 +533,12 @@ public static string SynchronizeActionToString(SynchronizeActions synchronizeAct /// The parsed action. /// Thrown when the value is not recognized. public static SynchronizeActions StringToSynchronizeAction(string s) - { - if (!s_synchronizeActionByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown synchronize action: {s}"); - } - - return result; - } + => BitbucketEnumMaps.SynchronizeActions.FromApiString(s); #endregion #region BlockerCommentState - private static readonly FrozenDictionary s_stringByBlockerCommentState = new Dictionary - { - [BlockerCommentState.Open] = "OPEN", - [BlockerCommentState.Resolved] = "RESOLVED", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_blockerCommentStateByString = - s_stringByBlockerCommentState.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -1019,23 +546,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; + public static string? BlockerCommentStateToString(BlockerCommentState? state) + => BitbucketEnumMaps.BlockerCommentState.ToApiString(state); /// /// Parses a blocker comment state string into a value. @@ -1044,28 +563,12 @@ public static string BlockerCommentStateToString(BlockerCommentState state) /// The parsed state. /// Thrown when the value is not recognized. public static BlockerCommentState StringToBlockerCommentState(string s) - { - if (!s_blockerCommentStateByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown blocker comment state: {s}"); - } - - return result; - } + => BitbucketEnumMaps.BlockerCommentState.FromApiString(s); #endregion #region CommentSeverity - private static readonly FrozenDictionary s_stringByCommentSeverity = new Dictionary - { - [CommentSeverity.Normal] = "NORMAL", - [CommentSeverity.Blocker] = "BLOCKER", - }.ToFrozenDictionary(); - - private static readonly FrozenDictionary s_commentSeverityByString = - s_stringByCommentSeverity.ToFrozenDictionary(kv => kv.Value, kv => kv.Key, StringComparer.OrdinalIgnoreCase); - /// /// Converts a value to the Bitbucket API string. /// @@ -1073,23 +576,15 @@ public static BlockerCommentState StringToBlockerCommentState(string s) /// 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; - } + => BitbucketEnumMaps.CommentSeverity.ToApiString(severity); /// /// 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; + public static string? CommentSeverityToString(CommentSeverity? severity) + => BitbucketEnumMaps.CommentSeverity.ToApiString(severity); /// /// Parses a comment severity string into a value. @@ -1098,14 +593,7 @@ public static string CommentSeverityToString(CommentSeverity severity) /// The parsed severity. /// Thrown when the value is not recognized. public static CommentSeverity StringToCommentSeverity(string s) - { - if (!s_commentSeverityByString.TryGetValue(s, out var result)) - { - throw new ArgumentException($"Unknown comment severity: {s}"); - } - - return result; - } + => BitbucketEnumMaps.CommentSeverity.FromApiString(s); #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/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/Models/Core/Admin/GroupPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs index 6b83bdd..7c60637 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs @@ -1,13 +1,10 @@ -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 override string ToString() => $"{Permission} - {Group}"; diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs index 652e037..f1b88ce 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs @@ -1,13 +1,10 @@ -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 override string ToString() => $"{Permission} - {User?.DisplayName}"; diff --git a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs index 3fca691..b37c589 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs @@ -57,13 +57,11 @@ public class BlockerComment /// /// The severity level of the comment. For blocker comments, this is always . /// - [JsonConverter(typeof(CommentSeverityConverter))] public CommentSeverity Severity { get; set; } = CommentSeverity.Blocker; /// /// The state of the blocker comment. /// - [JsonConverter(typeof(BlockerCommentStateConverter))] public BlockerCommentState State { get; set; } = BlockerCommentState.Open; /// 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/HookDetails.cs b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs index 43ffb54..9889bb4 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs @@ -1,13 +1,9 @@ -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; } diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs index a5634ac..9ef2870 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; } } \ 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..0b0bd3d 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; @@ -17,7 +15,6 @@ public class Participant /// /// Gets or sets the participant's role (e.g. AUTHOR, REVIEWER, PARTICIPANT). /// - [JsonConverter(typeof(RolesConverter))] public Roles Role { get; set; } /// @@ -28,7 +25,6 @@ public class Participant /// /// Gets or sets the participant's review status (e.g. APPROVED, UNAPPROVED, NEEDS_WORK). /// - [JsonConverter(typeof(ParticipantStatusConverter))] public ParticipantStatus Status { get; set; } /// 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/WebHookResult.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs index e8b7399..5f5f4ab 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; } } \ 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/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/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/Ssh/KeyBase.cs b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs index e477df5..444fcf5 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; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs index c539169..ff9b942 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; @@ -51,7 +52,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 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) } }; From e38a2a5c0092aabe3e3242ea04319f4d5657abc3 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:54:02 +0000 Subject: [PATCH 11/31] test: add ConfigureAwait(false) verification architectural test - Count await vs ConfigureAwait(false) calls per source file - Catches suppressions via #pragma or files excluded from analyzer - Update CHANGELOG.md --- CHANGELOG.md | 7 ++-- .../UnitTests/ArchitecturalTests.cs | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6829a..913103e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,11 +73,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `BitbucketClientDisposeTests` (6 tests) covering disposal semantics and ownership tracking. - Added `ArchitecturalTests` verifying all HTTP calls have error - handlers (`HandleResponseAsync`, `ExecuteAsync`, or `StatusCode`) - and that `JsonSerializerOptions` are explicitly frozen. + 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 increased from 696 to 798 (+102 new tests). +- Total test count increased from 696 to 799 (+103 new tests). ## [0.2.0] - 2026-02-08 diff --git a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs index f14b0a4..4b49f99 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/ArchitecturalTests.cs @@ -67,4 +67,43 @@ public void AllHttpCalls_HaveCorrespondingErrorHandlers() 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 From f926433bc3903b7aef478a174a391f8ac5b46d3d Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:05:13 +0000 Subject: [PATCH 12/31] refactor: convert 377 response model properties to init-only Convert { get; set; } to { get; init; } on 106 response model classes. 32 request body model files (98 properties) retain mutable setters. Refactored BranchMetadata getter to use object initializer. Breaking change: consumers that assign model properties after construction must move to object-initializer syntax. All 799 tests pass. Zero warnings. --- CHANGELOG.md | 6 ++++ src/Bitbucket.Net/Models/Audit/AuditEvent.cs | 10 +++---- .../Models/Branches/BranchModel.cs | 8 +++--- .../Models/Branches/BranchModelType.cs | 8 +++--- src/Bitbucket.Net/Models/Builds/BuildStats.cs | 8 +++--- .../Models/Core/Admin/Address.cs | 6 ++-- .../Models/Core/Admin/Cluster.cs | 8 +++--- .../Models/Core/Admin/DeletableGroupOrUser.cs | 4 +-- .../Models/Core/Admin/GroupPermission.cs | 4 +-- .../Models/Core/Admin/LicenseDetails.cs | 26 ++++++++--------- .../Models/Core/Admin/LicenseStatus.cs | 6 ++-- .../Models/Core/Admin/MergeStrategy.cs | 12 ++++---- src/Bitbucket.Net/Models/Core/Admin/Node.cs | 10 +++---- .../Models/Core/Admin/UserInfo.cs | 12 ++++---- .../Models/Core/Admin/UserPermission.cs | 4 +-- .../Core/Projects/AheadBehindMetaData.cs | 6 ++-- .../Models/Core/Projects/Author.cs | 6 ++-- .../Models/Core/Projects/BlockerComment.cs | 28 +++++++++---------- .../Models/Core/Projects/Branch.cs | 25 +++++++++++------ .../Models/Core/Projects/BranchBase.cs | 6 ++-- .../Models/Core/Projects/BranchMetaData.cs | 6 ++-- .../Models/Core/Projects/BrowseItem.cs | 8 +++--- .../Models/Core/Projects/BrowsePathItem.cs | 4 +-- .../Core/Projects/BuildStatusMetadata.cs | 8 +++--- .../Models/Core/Projects/Change.cs | 22 +++++++-------- .../Models/Core/Projects/CloneLink.cs | 4 +-- .../Models/Core/Projects/CloneLinks.cs | 4 +-- .../Models/Core/Projects/Comment.cs | 28 +++++++++---------- .../Models/Core/Projects/CommentId.cs | 4 +-- .../Models/Core/Projects/CommentRef.cs | 20 ++++++------- .../Models/Core/Projects/Commit.cs | 16 +++++------ .../Models/Core/Projects/CommitParent.cs | 6 ++-- .../Models/Core/Projects/ContentItem.cs | 10 +++---- .../Models/Core/Projects/Diff.cs | 8 +++--- .../Models/Core/Projects/DiffHunk.cs | 14 +++++----- .../Models/Core/Projects/DiffInfo.cs | 12 ++++---- .../Models/Core/Projects/Differences.cs | 4 +-- .../Models/Core/Projects/FromToRef.cs | 12 ++++---- .../Models/Core/Projects/Hook.cs | 10 +++---- .../Models/Core/Projects/HookDetails.cs | 14 +++++----- .../Models/Core/Projects/HookScope.cs | 4 +-- .../Models/Core/Projects/LastModified.cs | 6 ++-- .../Models/Core/Projects/LicensedUser.cs | 6 ++-- .../Models/Core/Projects/Line.cs | 4 +-- .../Models/Core/Projects/LineRef.cs | 10 +++---- .../Models/Core/Projects/Link.cs | 4 +-- .../Models/Core/Projects/Links.cs | 4 +-- .../Core/Projects/MergeCheckRequiredBuilds.cs | 6 ++-- .../Projects/MergeHookRequiredApprovers.cs | 6 ++-- .../Models/Core/Projects/Participant.cs | 8 +++--- .../Models/Core/Projects/Path.cs | 12 ++++---- .../Core/Projects/Permittedoperations.cs | 8 +++--- .../Models/Core/Projects/Project.cs | 10 +++---- .../Models/Core/Projects/Properties.cs | 4 +-- .../Models/Core/Projects/PullRequest.cs | 14 +++++----- .../Core/Projects/PullRequestActivity.cs | 16 +++++------ .../Core/Projects/PullRequestMergeState.cs | 10 +++---- .../Core/Projects/PullRequestMetadata.cs | 4 +-- .../Core/Projects/PullRequestSuggestion.cs | 10 +++---- src/Bitbucket.Net/Models/Core/Projects/Ref.cs | 8 +++--- .../Models/Core/Projects/RefChange.cs | 12 ++++---- .../Models/Core/Projects/Repository.cs | 16 +++++------ .../Models/Core/Projects/RepositoryFork.cs | 4 +-- .../Models/Core/Projects/RepositoryOrigin.cs | 22 +++++++-------- .../Models/Core/Projects/Reviewer.cs | 4 +-- .../Models/Core/Projects/Segment.cs | 8 +++--- src/Bitbucket.Net/Models/Core/Projects/Tag.cs | 14 +++++----- .../Models/Core/Projects/TimeWindow.cs | 4 +-- .../Models/Core/Projects/Veto.cs | 6 ++-- .../Models/Core/Projects/WebHookInvocation.cs | 14 +++++----- .../Models/Core/Projects/WebHookRequest.cs | 6 ++-- .../Models/Core/Projects/WebHookResult.cs | 4 +-- .../Models/Core/Projects/WebHookStatistics.cs | 4 +-- .../Core/Projects/WebHookStatisticsCounts.cs | 10 +++---- .../Core/Projects/WebHookStatisticsSummary.cs | 10 +++---- .../Core/Projects/WebHookTestRequest.cs | 6 ++-- .../Projects/WebHookTestRequestResponse.cs | 6 ++-- .../Core/Projects/WebHookTestResponse.cs | 8 +++--- .../Models/Core/Tasks/BitbucketTask.cs | 6 ++-- .../Models/Core/Tasks/BitbucketTaskCount.cs | 6 ++-- .../Models/Core/Tasks/TaskAnchor.cs | 8 +++--- .../Models/Core/Tasks/TaskBasicAnchor.cs | 6 ++-- .../Models/Core/Tasks/TaskRef.cs | 12 ++++---- .../Models/Core/Users/Identity.cs | 4 +-- src/Bitbucket.Net/Models/Core/Users/User.cs | 14 +++++----- ...efaultReviewerPullRequestConditionScope.cs | 6 ++-- ...DefaultReviewerPullRequestConditionType.cs | 6 ++-- .../Models/DefaultReviewers/RefMatcher.cs | 10 +++---- .../Models/Git/RebasePullRequestCondition.cs | 8 +++--- src/Bitbucket.Net/Models/Git/Veto.cs | 6 ++-- src/Bitbucket.Net/Models/Jira/ChangeSet.cs | 12 ++++---- src/Bitbucket.Net/Models/Jira/Changes.cs | 12 ++++---- src/Bitbucket.Net/Models/Jira/JiraIssue.cs | 6 ++-- .../PersonalAccessTokens/AccessToken.cs | 10 +++---- .../PersonalAccessTokens/FullAccessToken.cs | 4 +-- .../Models/RefRestrictions/AccessKey.cs | 4 +-- .../Models/RefRestrictions/Key.cs | 8 +++--- .../Models/RefRestrictions/RefRestriction.cs | 10 +++---- src/Bitbucket.Net/Models/RefSync/FullRef.cs | 6 ++-- .../RepositorySynchronizationStatus.cs | 12 ++++---- .../Models/RefSync/SynchronizeContext.cs | 4 +-- src/Bitbucket.Net/Models/Ssh/Accesskeys.cs | 4 +-- src/Bitbucket.Net/Models/Ssh/Fingerprint.cs | 6 ++-- src/Bitbucket.Net/Models/Ssh/KeyBase.cs | 4 +-- src/Bitbucket.Net/Models/Ssh/ProjectKey.cs | 4 +-- src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs | 4 +-- src/Bitbucket.Net/Models/Ssh/SshSettings.cs | 12 ++++---- 107 files changed, 479 insertions(+), 464 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 913103e..51e5138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Consumers assigning results to `IEnumerable` are unaffected; consumers assigning to `List` must add `.ToList()` or change the variable type. +- **Init-only model properties** (Spec 009): 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. ### Changed 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/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 7c60637..63f908b 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs @@ -4,8 +4,8 @@ namespace Bitbucket.Net.Models.Core.Admin; public class GroupPermission { - public Named? Group { get; set; } - 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 f1b88ce..e612b3e 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs @@ -4,8 +4,8 @@ namespace Bitbucket.Net.Models.Core.Admin; public class UserPermission { - public User? User { get; set; } - 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 b37c589..757e648 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs @@ -25,74 +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 . /// - public CommentSeverity Severity { get; set; } = CommentSeverity.Blocker; + public CommentSeverity Severity { get; init; } = CommentSeverity.Blocker; /// /// The state of the blocker comment. /// - 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/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 9889bb4..81c2ad0 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs @@ -2,11 +2,11 @@ namespace Bitbucket.Net.Models.Core.Projects; public class HookDetails { - public string? Key { get; set; } - public string? Name { get; set; } - 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 9ef2870..da4aac8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs @@ -2,6 +2,6 @@ namespace Bitbucket.Net.Models.Core.Projects; public class HookScope { - public int ResourceId { get; set; } - 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 0b0bd3d..b9b9a7f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Participant.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Participant.cs @@ -10,22 +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). /// - 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). /// - 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/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/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/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 5f5f4ab..1765f98 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs @@ -2,6 +2,6 @@ namespace Bitbucket.Net.Models.Core.Projects; public class WebHookResult { - public string? Description { get; set; } - 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/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/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/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 444fcf5..0115c93 100644 --- a/src/Bitbucket.Net/Models/Ssh/KeyBase.cs +++ b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs @@ -5,6 +5,6 @@ namespace Bitbucket.Net.Models.Ssh; public abstract class KeyBase { - public Key? Key { get; set; } - 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 From 656f58d6c27228a8dc592b898422ad1765ace016 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:25:19 +0000 Subject: [PATCH 13/31] feat: introduce 13 dedicated request DTOs for write operations Create purpose-built request DTOs for all write API methods, replacing reused response models and anonymous types. DTOs use required/init-only properties and expose only API-relevant fields. New types in Models/Core/Projects/Requests/: CreateProjectRequest, UpdateProjectRequest, CreateRepositoryRequest, ForkRepositoryRequest, CreatePullRequestRequest, UpdatePullRequestRequest, CreateBranchRequest, CreateTaskRequest, UpdateTaskRequest, CreateWebHookRequest, UpdateWebHookRequest, MergePullRequestRequest New type in Models/Builds/Requests/: AssociateBuildStatusRequest 13 method signatures updated. All tests updated to use new DTOs. Breaking change: method parameter types changed. All 799 tests pass. Zero warnings. --- CHANGELOG.md | 18 +++++++++++ src/Bitbucket.Net/Builds/BitbucketClient.cs | 8 +++-- .../Core/Projects/BitbucketClient.Branches.cs | 9 ++++-- .../Core/Projects/BitbucketClient.Projects.cs | 16 ++++++---- .../Projects/BitbucketClient.PullRequests.cs | 27 ++++++++++------ .../Projects/BitbucketClient.Repositories.cs | 31 +++++------------- .../BitbucketClient.RepositorySettings.cs | 16 ++++++---- .../Core/Tasks/BitbucketClient.cs | 20 ++++++++---- .../Requests/AssociateBuildStatusRequest.cs | 32 +++++++++++++++++++ .../Projects/Requests/CreateBranchRequest.cs | 22 +++++++++++++ .../Projects/Requests/CreateProjectRequest.cs | 22 +++++++++++++ .../Requests/CreatePullRequestRequest.cs | 32 +++++++++++++++++++ .../Requests/CreateRepositoryRequest.cs | 22 +++++++++++++ .../Projects/Requests/CreateTaskRequest.cs | 19 +++++++++++ .../Projects/Requests/CreateWebHookRequest.cs | 32 +++++++++++++++++++ .../Requests/ForkRepositoryRequest.cs | 22 +++++++++++++ .../Requests/MergePullRequestRequest.cs | 24 ++++++++++++++ .../Projects/Requests/UpdateProjectRequest.cs | 22 +++++++++++++ .../Requests/UpdatePullRequestRequest.cs | 27 ++++++++++++++++ .../Projects/Requests/UpdateTaskRequest.cs | 17 ++++++++++ .../Projects/Requests/UpdateWebHookRequest.cs | 32 +++++++++++++++++++ .../Serialization/BitbucketJsonContext.cs | 19 +++++++++++ .../MockTests/BranchMockTests.cs | 5 +-- .../MockTests/BuildMockTests.cs | 6 ++-- .../MockTests/CancellationMockTests.cs | 4 +-- .../MockTests/ProjectCrudMockTests.cs | 10 +++--- .../MockTests/PullRequestCrudMockTests.cs | 9 +++--- .../MockTests/PullRequestExtendedMockTests.cs | 10 +++--- .../MockTests/RepositoryCrudMockTests.cs | 8 ++--- .../MockTests/TasksMockTests.cs | 7 ++-- .../MockTests/WebhookMockTests.cs | 6 ++-- 31 files changed, 465 insertions(+), 89 deletions(-) create mode 100644 src/Bitbucket.Net/Models/Builds/Requests/AssociateBuildStatusRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreateBranchRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreateProjectRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreatePullRequestRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreateRepositoryRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreateTaskRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/CreateWebHookRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/ForkRepositoryRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/MergePullRequestRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateProjectRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/UpdatePullRequestRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateTaskRequest.cs create mode 100644 src/Bitbucket.Net/Models/Core/Projects/Requests/UpdateWebHookRequest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e5138..0085672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 properties) retain mutable setters to allow consumer construction. Consumers that assign model properties after construction must move to object-initializer syntax. +- **Dedicated request DTOs** (Spec 010): 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 diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index b33161a..af97c24 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -1,6 +1,7 @@ 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; @@ -106,15 +107,16 @@ public async Task> GetBuildStatusForCommitAsync(strin /// 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/Core/Projects/BitbucketClient.Branches.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs index 417efc1..82dec63 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -1,6 +1,7 @@ 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; @@ -94,13 +95,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); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs index 8aa0d91..e78794d 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs @@ -2,6 +2,7 @@ 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; @@ -136,13 +137,15 @@ public IAsyncEnumerable GetProjectsStreamAsync( /// /// 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); @@ -169,15 +172,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); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs index 2bc575d..d1acfdf 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -1,6 +1,7 @@ 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; @@ -146,13 +147,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 +184,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 +299,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); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs index 818cacc..f60ea8f 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs @@ -2,6 +2,7 @@ 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; @@ -76,23 +77,16 @@ public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string pro /// 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) { ArgumentException.ThrowIfNullOrWhiteSpace(projectKey); - ArgumentException.ThrowIfNullOrWhiteSpace(repositoryName); - - var data = new - { - name = repositoryName, - scmId, - }; + 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); @@ -119,22 +113,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); diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs index de1b111..bd954f4 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs @@ -2,6 +2,7 @@ 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; @@ -447,13 +448,15 @@ public async Task> GetProjectRepositoryWebHooksAsync(stri /// /// 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); @@ -512,16 +515,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); 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/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/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/Serialization/BitbucketJsonContext.cs b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs index ff9b942..5bb65c1 100644 --- a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs +++ b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs @@ -8,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 @@ -340,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/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/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/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); From b37846ac317a59cc82383bdf41d13fb68115c88b Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:48:06 +0000 Subject: [PATCH 14/31] feat: add fluent query builders for complex endpoints Introduce PullRequestQueryBuilder, CommitQueryBuilder, BranchQueryBuilder, and ProjectQueryBuilder with fluent API methods. Entry points: client.PullRequests(), client.Commits(), client.Branches(), client.Projects(). Each builder supports GetAsync() for buffered results and StreamAsync() for IAsyncEnumerable streaming. Builders delegate to existing flat methods. Existing flat methods are unchanged (additive, non-breaking). 12 new tests covering all builders. 811 total tests pass. Zero warnings. --- CHANGELOG.md | 10 + .../Builders/BitbucketClient.Builders.cs | 52 ++++ .../Builders/BranchQueryBuilder.cs | 61 +++++ .../Builders/CommitQueryBuilder.cs | 71 ++++++ .../Builders/ProjectQueryBuilder.cs | 46 ++++ .../Builders/PullRequestQueryBuilder.cs | 69 ++++++ .../MockTests/FluentQueryBuilderMockTests.cs | 234 ++++++++++++++++++ 7 files changed, 543 insertions(+) create mode 100644 src/Bitbucket.Net/Builders/BitbucketClient.Builders.cs create mode 100644 src/Bitbucket.Net/Builders/BranchQueryBuilder.cs create mode 100644 src/Bitbucket.Net/Builders/CommitQueryBuilder.cs create mode 100644 src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs create mode 100644 src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs create mode 100644 test/Bitbucket.Net.Tests/MockTests/FluentQueryBuilderMockTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0085672..dfa2ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `commitId`, `hookKey`, `userSlug`, etc.) with `ArgumentException.ThrowIfNullOrWhiteSpace()` at method entry. Prevents malformed URLs and confusing server-side errors. +- **Fluent query builders** (Spec 011): 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. ### Testing @@ -96,6 +103,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 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..99ed861 --- /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 BitbucketClient _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(BitbucketClient 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..844507b --- /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 BitbucketClient _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(BitbucketClient 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..190abfe --- /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 BitbucketClient _client; + + private int? _maxPages; + private int? _limit; + private int? _start; + private string? _name; + private Permissions? _permission; + + internal ProjectQueryBuilder(BitbucketClient 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..92a02c2 --- /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 BitbucketClient _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(BitbucketClient 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/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 From 8887bcc03b733294f37e6828d44fa3cd5792dc0b Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:55:49 +0000 Subject: [PATCH 15/31] perf: add Utf8JsonReader-based PagedResultsReader for hot paths Internal zero-allocation reader extracts pagination metadata (isLastPage, nextPageStart, start, limit, size) directly from UTF-8 bytes using Utf8JsonReader, skipping full deserialization. New files: Common/PagedResultsReader.cs, Common/PagedMetadata.cs PagedResultsReaderTests.cs (5 tests) PagedResultsReaderBenchmarks.cs (6 benchmarks) Added InternalsVisibleTo for test and benchmark projects. 816 tests pass. Zero warnings. --- CHANGELOG.md | 5 + .../Bitbucket.Net.Benchmarks/Program.cs | 4 + .../ZeroCopy/PagedResultsReaderBenchmarks.cs | 117 ++++++++++++++++++ src/Bitbucket.Net/Bitbucket.Net.csproj | 5 + src/Bitbucket.Net/Common/PagedMetadata.cs | 14 +++ .../Common/PagedResultsReader.cs | 66 ++++++++++ .../UnitTests/PagedResultsReaderTests.cs | 65 ++++++++++ 7 files changed, 276 insertions(+) create mode 100644 benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/PagedResultsReaderBenchmarks.cs create mode 100644 src/Bitbucket.Net/Common/PagedMetadata.cs create mode 100644 src/Bitbucket.Net/Common/PagedResultsReader.cs create mode 100644 test/Bitbucket.Net.Tests/UnitTests/PagedResultsReaderTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa2ea2..42a44af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`PagedResultsReader` zero-allocation metadata parser** (Spec 012): + 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`** (Spec 004): The client now implements `IDisposable` with ownership tracking. Clients created via the `(string url, ...)` constructors own and dispose the 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/Bitbucket.Net.csproj b/src/Bitbucket.Net/Bitbucket.Net.csproj index d40f0a9..2eb29e9 100644 --- a/src/Bitbucket.Net/Bitbucket.Net.csproj +++ b/src/Bitbucket.Net/Bitbucket.Net.csproj @@ -19,6 +19,11 @@ true + + + + + 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/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 From fb46d794e20dba7743fdbc4f6e13ce0cb23f1827 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:12:37 +0000 Subject: [PATCH 16/31] feat: expose rate-limit headers on BitbucketRateLimitException MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add RetryAfter, RateLimit, RateLimitRemaining, and RateLimitReset properties to BitbucketRateLimitException, parsed from standard HTTP rate-limit response headers (Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset). - New Create overload on BitbucketApiException accepts HttpResponseHeaders - Graceful null on missing/unparseable headers - 7 new tests (5 unit + 2 mock) β€” 823 total pass - Updated CHANGELOG.md --- CHANGELOG.md | 5 ++ src/Bitbucket.Net/BitbucketClient.cs | 3 +- .../Exceptions/BitbucketApiException.cs | 64 ++++++++++++++++++- .../Exceptions/BitbucketRateLimitException.cs | 38 +++++++++++ .../Infrastructure/MockSetupExtensions.cs | 45 +++++++++++++ .../MockTests/ErrorHandlingMockTests.cs | 59 +++++++++++++++++ .../UnitTests/ExceptionTests.cs | 63 ++++++++++++++++++ 7 files changed, 275 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a44af..8794e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Rate-limit headers on `BitbucketRateLimitException`** (Spec 014): + 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** (Spec 012): Internal `Utf8JsonReader`-based parser that extracts pagination metadata (`isLastPage`, `nextPageStart`, `start`, `limit`, `size`) diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 8d6a988..a4d6fb2 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -355,7 +355,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); } } 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/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/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/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)] From 2f3cb03c12c1edbb053cc70e0fce67a214e6b89c Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:14:57 +0000 Subject: [PATCH 17/31] chore: improve NuGet package metadata and bump to 1.0.0 - Version 0.2.0 -> 1.0.0 - Add PackageIcon with 128x128 placeholder (assets/icon.png) - Expand PackageTags: rest-api, api-client, atlassian, sdk, dotnet - Conditional icon inclusion (Condition=Exists) - Verified dotnet pack produces BitbucketServer.Net.1.0.0.nupkg - Updated CHANGELOG.md --- CHANGELOG.md | 6 ++++++ assets/icon.png | Bin 0 -> 875 bytes src/Bitbucket.Net/Bitbucket.Net.csproj | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 assets/icon.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 8794e37..16b103c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **NuGet package metadata improvements** (Spec 016): 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`** (Spec 014): HTTP 429 exceptions now expose `RetryAfter`, `RateLimit`, `RateLimitRemaining`, and `RateLimitReset` properties parsed from diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c092ee93ff56883d39b5b414df097b85acf8a5c5 GIT binary patch literal 875 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-49pCkE{-7;ac^%K`aO1FXniQtKWp=Wz!w&* z;t!ZIGX+DvqP&#fpJMu>p4ej}x4$xf$@-s5XH5OW(e~zHLAA^w38xB24tx%YNbHZic2QpBQcI z&&2=U{`!x_^2-Iy*`1CPg6bZx<4^cxWph5h;%mtVgARq%w~Rg0dK8#5)*NH#JAAkB z-z#r*AmwJe?mUuONZKEOBS-Wpcsii-LBzx;Xm zaCd!+Lh4h-Dfj-!$^BE~2)ZWKaC3d#eMJvV`2$wVH_x_rny~3E!@B*=9L^Jhsu||m zyo_&FSo)1&pWi*kvdrC#KF{C2e0pZTvqaOBx9lJOmwW*_Yw1;nW0y~I0DURrV8?T- z@e|_|n4Fk8G(m95xl@<*)%o;098$P}6bN`jk z2vQXCli}2Q%ekMj-wS}$e*NvMwg1}r$@VO!U#`#Ic5h 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 @@ -40,6 +41,7 @@ + From 678e636454cf7d77584e9f77947148bcf16b797d Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:38:49 +0000 Subject: [PATCH 18/31] docs: update DI examples to Polly v8 ResiliencePipeline syntax Replace deprecated Polly v7 AddTransientHttpErrorPolicy with Polly v8 AddStandardResilienceHandler and custom AddResilienceHandler examples. - Standard resilience handler with retry, circuit breaker, and timeout - Custom resilience pipeline for fine-grained control over retried status codes - Requires Microsoft.Extensions.Http.Resilience NuGet package --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1af2ed3..68a7888 100644 --- a/README.md +++ b/README.md @@ -58,32 +58,74 @@ var client = new BitbucketClient("https://bitbucket.example.com", () => GetAcces ### 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 => { 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: From 39d81cdcbcb19b8ac54f6d9c420c61b9c69377da Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:48:17 +0000 Subject: [PATCH 19/31] chore: whitespace cleanup on readme.md --- README.md | 143 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 68a7888..7f5785a 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,13 @@ 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()); ``` @@ -214,74 +216,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 From e4adeabfd633865940e0dd0716b4cd40c2d57089 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:04:56 +0000 Subject: [PATCH 20/31] feat: extract IBitbucketClient interface for DI and testability - Define IBitbucketClient with all ~230 public method signatures - Update BitbucketClient to implement IBitbucketClient - Update query builders to accept IBitbucketClient instead of concrete type - Add NSubstitute 5.3.0 for mock-based testing - Add InterfaceTests verifying parity, obsolete attributes, and mockability - Update README DI examples to register IBitbucketClient --- README.md | 6 +- src/Bitbucket.Net/BitbucketClient.cs | 2 +- .../Builders/BranchQueryBuilder.cs | 4 +- .../Builders/CommitQueryBuilder.cs | 4 +- .../Builders/ProjectQueryBuilder.cs | 4 +- .../Builders/PullRequestQueryBuilder.cs | 4 +- src/Bitbucket.Net/IBitbucketClient.cs | 429 ++++++++++++++++++ .../Bitbucket.Net.Tests.csproj | 1 + .../UnitTests/InterfaceTests.cs | 95 ++++ 9 files changed, 537 insertions(+), 12 deletions(-) create mode 100644 src/Bitbucket.Net/IBitbucketClient.cs create mode 100644 test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs diff --git a/README.md b/README.md index 7f5785a..99f7571 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ services.AddHttpClient(client => 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)); @@ -138,7 +138,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()); diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index a4d6fb2..8a9109d 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -24,7 +24,7 @@ namespace Bitbucket.Net; /// the caller retains ownership of the and is responsible for its disposal. /// /// -public partial class BitbucketClient : IDisposable +public partial class BitbucketClient : IBitbucketClient { private static readonly JsonSerializerOptions s_jsonOptions = CreateReadOptions(); private static readonly JsonSerializerOptions s_writeJsonOptions = CreateWriteOptions(); diff --git a/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs b/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs index 99ed861..dc66965 100644 --- a/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs +++ b/src/Bitbucket.Net/Builders/BranchQueryBuilder.cs @@ -7,7 +7,7 @@ namespace Bitbucket.Net.Builders; /// public sealed class BranchQueryBuilder { - private readonly BitbucketClient _client; + private readonly IBitbucketClient _client; private readonly string _projectKey; private readonly string _repositorySlug; @@ -19,7 +19,7 @@ public sealed class BranchQueryBuilder private string? _filterText; private BranchOrderBy? _orderBy; - internal BranchQueryBuilder(BitbucketClient client, string projectKey, string repositorySlug) + internal BranchQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug) { _client = client; _projectKey = projectKey; diff --git a/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs b/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs index 844507b..c830070 100644 --- a/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs +++ b/src/Bitbucket.Net/Builders/CommitQueryBuilder.cs @@ -7,7 +7,7 @@ namespace Bitbucket.Net.Builders; /// public sealed class CommitQueryBuilder { - private readonly BitbucketClient _client; + private readonly IBitbucketClient _client; private readonly string _projectKey; private readonly string _repositorySlug; private readonly string _until; @@ -22,7 +22,7 @@ public sealed class CommitQueryBuilder private int? _limit; private int? _start; - internal CommitQueryBuilder(BitbucketClient client, string projectKey, string repositorySlug, string until) + internal CommitQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug, string until) { _client = client; _projectKey = projectKey; diff --git a/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs b/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs index 190abfe..3dc26af 100644 --- a/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs +++ b/src/Bitbucket.Net/Builders/ProjectQueryBuilder.cs @@ -8,7 +8,7 @@ namespace Bitbucket.Net.Builders; /// public sealed class ProjectQueryBuilder { - private readonly BitbucketClient _client; + private readonly IBitbucketClient _client; private int? _maxPages; private int? _limit; @@ -16,7 +16,7 @@ public sealed class ProjectQueryBuilder private string? _name; private Permissions? _permission; - internal ProjectQueryBuilder(BitbucketClient client) + internal ProjectQueryBuilder(IBitbucketClient client) { _client = client; } diff --git a/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs b/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs index 92a02c2..74a80cb 100644 --- a/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs +++ b/src/Bitbucket.Net/Builders/PullRequestQueryBuilder.cs @@ -7,7 +7,7 @@ namespace Bitbucket.Net.Builders; /// public sealed class PullRequestQueryBuilder { - private readonly BitbucketClient _client; + private readonly IBitbucketClient _client; private readonly string _projectKey; private readonly string _repositorySlug; @@ -21,7 +21,7 @@ public sealed class PullRequestQueryBuilder private bool _withAttributes = true; private bool _withProperties = true; - internal PullRequestQueryBuilder(BitbucketClient client, string projectKey, string repositorySlug) + internal PullRequestQueryBuilder(IBitbucketClient client, string projectKey, string repositorySlug) { _client = client; _projectKey = projectKey; diff --git a/src/Bitbucket.Net/IBitbucketClient.cs b/src/Bitbucket.Net/IBitbucketClient.cs new file mode 100644 index 0000000..c9c83d1 --- /dev/null +++ b/src/Bitbucket.Net/IBitbucketClient.cs @@ -0,0 +1,429 @@ +using Bitbucket.Net.Builders; +using Bitbucket.Net.Common.Models.Search; +using Bitbucket.Net.Models.Audit; +using Bitbucket.Net.Models.Branches; +using Bitbucket.Net.Models.Builds; +using Bitbucket.Net.Models.Builds.Requests; +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.Git; +using Bitbucket.Net.Models.Jira; +using Bitbucket.Net.Models.PersonalAccessTokens; +using Bitbucket.Net.Models.RefRestrictions; +using Bitbucket.Net.Models.RefSync; +using Bitbucket.Net.Models.Ssh; + +namespace Bitbucket.Net; + +/// +/// Abstraction over the Bitbucket Server REST API client, enabling +/// dependency injection, unit testing with mocks, and decorator patterns. +/// +public interface IBitbucketClient : IDisposable +{ + // ── 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); + + // ── Branches ───────────────────────────────────────────────────── + + 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); + + // ── Builders ───────────────────────────────────────────────────── + + PullRequestQueryBuilder PullRequests(string projectKey, string repositorySlug); + CommitQueryBuilder Commits(string projectKey, string repositorySlug, string until); + BranchQueryBuilder Branches(string projectKey, string repositorySlug); + ProjectQueryBuilder Projects(); + + // ── Builds ─────────────────────────────────────────────────────── + + 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); + + // ── 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); + + // ── Git ────────────────────────────────────────────────────────── + + 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); + + // ── 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); + + // ── Ref Restrictions ───────────────────────────────────────────── + + 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); + + // ── Ref Sync ───────────────────────────────────────────────────── + + 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); + + // ── SSH ────────────────────────────────────────────────────────── + + 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); + + // ── Admin ──────────────────────────────────────────────────────── + + 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); + + // ── 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); + + // ── Repos ──────────────────────────────────────────────────────── + + 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); + + // ── Search ─────────────────────────────────────────────────────── + + Task SearchCodeAsync(string query, int primaryLimit = 25, int secondaryLimit = 10, CancellationToken cancellationToken = default); + Task IsSearchAvailableAsync(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); + + // ── Projects ───────────────────────────────────────────────────── + + 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); + + // ── Repositories ───────────────────────────────────────────────── + + 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); + + // ── Repository Settings ────────────────────────────────────────── + + 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); + + // ── Compare ────────────────────────────────────────────────────── + + 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); + + // ── Commits ────────────────────────────────────────────────────── + + 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); + + // ── Branches (project-level) ───────────────────────────────────── + + 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); + + // ── Pull Requests ──────────────────────────────────────────────── + + 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); + + // ── Pull Request Details ───────────────────────────────────────── + + 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); + + // ── Pull Request Comments ──────────────────────────────────────── + + 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); + + // ── Pull Request Tasks ─────────────────────────────────────────── + + [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/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/UnitTests/InterfaceTests.cs b/test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs new file mode 100644 index 0000000..0d68c98 --- /dev/null +++ b/test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs @@ -0,0 +1,95 @@ +using Bitbucket.Net.Models.Core.Projects; +using NSubstitute; +using System.Reflection; +using Xunit; + +namespace Bitbucket.Net.Tests.UnitTests; + +public class InterfaceTests +{ + private static readonly Type s_interfaceType = typeof(IBitbucketClient); + private static readonly Type s_concreteType = typeof(BitbucketClient); + + [Fact] + public void IBitbucketClient_HasAllPublicMethodsFromBitbucketClient() + { + var concretePublicMethods = s_concreteType + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(m => !m.IsSpecialName) // exclude property accessors + .Where(m => m.Name is not "Dispose") + .Select(m => GetMethodSignature(m)) + .ToHashSet(StringComparer.Ordinal); + + var interfaceMethods = s_interfaceType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => !m.IsSpecialName) + .Where(m => m.DeclaringType != typeof(IDisposable)) + .Select(m => GetMethodSignature(m)) + .ToHashSet(StringComparer.Ordinal); + + var missingFromInterface = concretePublicMethods.Except(interfaceMethods).ToList(); + var extraOnInterface = interfaceMethods.Except(concretePublicMethods).ToList(); + + Assert.True( + missingFromInterface.Count == 0, + $"Methods on BitbucketClient missing from IBitbucketClient:\n{string.Join('\n', missingFromInterface)}"); + + Assert.True( + extraOnInterface.Count == 0, + $"Methods on IBitbucketClient not found on BitbucketClient:\n{string.Join('\n', extraOnInterface)}"); + } + + [Fact] + public void IBitbucketClient_PreservesObsoleteAttributes() + { + var obsoleteMethods = s_interfaceType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.GetCustomAttribute() is not null) + .Select(m => m.Name) + .OrderBy(n => n, StringComparer.Ordinal) + .ToList(); + + Assert.Equal( + ["GetPullRequestTaskCountAsync", "GetPullRequestTasksAsync", "GetPullRequestTasksStreamAsync"], + obsoleteMethods); + } + + [Fact] + public async Task IBitbucketClient_IsMockableWithNSubstitute() + { + var mock = Substitute.For(); + + IReadOnlyList expected = [new Project { Key = "TEST", Name = "Test Project" }]; + mock.GetProjectsAsync().Returns(Task.FromResult(expected)); + + var result = await mock.GetProjectsAsync(); + + Assert.Single(result); + Assert.Equal("TEST", result[0].Key); + } + + [Fact] + public void BitbucketClient_IsAssignableToIBitbucketClient() + { + Assert.True(s_interfaceType.IsAssignableFrom(s_concreteType)); + } + + private static string GetMethodSignature(MethodInfo m) + { + var parameters = string.Join(", ", m.GetParameters().Select(p => $"{FormatType(p.ParameterType)} {p.Name}")); + return $"{FormatType(m.ReturnType)} {m.Name}({parameters})"; + } + + private static string FormatType(Type t) + { + if (!t.IsGenericType) + { + return t.FullName ?? t.Name; + } + + var genericDef = t.GetGenericTypeDefinition().FullName!; + var baseName = genericDef[..genericDef.IndexOf('`')]; + var args = string.Join(", ", t.GetGenericArguments().Select(FormatType)); + return $"{baseName}<{args}>"; + } +} \ No newline at end of file From 537dff6192962e7f3ddb3b094d826bdf58915549 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:33:35 +0000 Subject: [PATCH 21/31] test: remove useless InterfaceTests The parity and mockability tests were meta-assertions about the interface shape, not meaningful behavioral tests. --- .../UnitTests/InterfaceTests.cs | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs diff --git a/test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs b/test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs deleted file mode 100644 index 0d68c98..0000000 --- a/test/Bitbucket.Net.Tests/UnitTests/InterfaceTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Bitbucket.Net.Models.Core.Projects; -using NSubstitute; -using System.Reflection; -using Xunit; - -namespace Bitbucket.Net.Tests.UnitTests; - -public class InterfaceTests -{ - private static readonly Type s_interfaceType = typeof(IBitbucketClient); - private static readonly Type s_concreteType = typeof(BitbucketClient); - - [Fact] - public void IBitbucketClient_HasAllPublicMethodsFromBitbucketClient() - { - var concretePublicMethods = s_concreteType - .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Where(m => !m.IsSpecialName) // exclude property accessors - .Where(m => m.Name is not "Dispose") - .Select(m => GetMethodSignature(m)) - .ToHashSet(StringComparer.Ordinal); - - var interfaceMethods = s_interfaceType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(m => !m.IsSpecialName) - .Where(m => m.DeclaringType != typeof(IDisposable)) - .Select(m => GetMethodSignature(m)) - .ToHashSet(StringComparer.Ordinal); - - var missingFromInterface = concretePublicMethods.Except(interfaceMethods).ToList(); - var extraOnInterface = interfaceMethods.Except(concretePublicMethods).ToList(); - - Assert.True( - missingFromInterface.Count == 0, - $"Methods on BitbucketClient missing from IBitbucketClient:\n{string.Join('\n', missingFromInterface)}"); - - Assert.True( - extraOnInterface.Count == 0, - $"Methods on IBitbucketClient not found on BitbucketClient:\n{string.Join('\n', extraOnInterface)}"); - } - - [Fact] - public void IBitbucketClient_PreservesObsoleteAttributes() - { - var obsoleteMethods = s_interfaceType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(m => m.GetCustomAttribute() is not null) - .Select(m => m.Name) - .OrderBy(n => n, StringComparer.Ordinal) - .ToList(); - - Assert.Equal( - ["GetPullRequestTaskCountAsync", "GetPullRequestTasksAsync", "GetPullRequestTasksStreamAsync"], - obsoleteMethods); - } - - [Fact] - public async Task IBitbucketClient_IsMockableWithNSubstitute() - { - var mock = Substitute.For(); - - IReadOnlyList expected = [new Project { Key = "TEST", Name = "Test Project" }]; - mock.GetProjectsAsync().Returns(Task.FromResult(expected)); - - var result = await mock.GetProjectsAsync(); - - Assert.Single(result); - Assert.Equal("TEST", result[0].Key); - } - - [Fact] - public void BitbucketClient_IsAssignableToIBitbucketClient() - { - Assert.True(s_interfaceType.IsAssignableFrom(s_concreteType)); - } - - private static string GetMethodSignature(MethodInfo m) - { - var parameters = string.Join(", ", m.GetParameters().Select(p => $"{FormatType(p.ParameterType)} {p.Name}")); - return $"{FormatType(m.ReturnType)} {m.Name}({parameters})"; - } - - private static string FormatType(Type t) - { - if (!t.IsGenericType) - { - return t.FullName ?? t.Name; - } - - var genericDef = t.GetGenericTypeDefinition().FullName!; - var baseName = genericDef[..genericDef.IndexOf('`')]; - var args = string.Join(", ", t.GetGenericArguments().Select(FormatType)); - return $"{baseName}<{args}>"; - } -} \ No newline at end of file From 2459706f85a008a9be2b916ac909f045e965bec4 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:07:23 +0000 Subject: [PATCH 22/31] feat: add OpenTelemetry tracing via ActivitySource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add distributed tracing instrumentation using System.Diagnostics.Activity, following OTel stable HTTP client semantic conventions. No dependency on OpenTelemetry SDK β€” pure DiagnosticSource (best practice for libraries). - Create BitbucketClient.Tracing.cs partial class with ActivitySource - Hook BeforeCall/AfterCall/OnError via Flurl 4 event handlers on GetBaseUrl() - Tag spans with http.request.method, url.full, server.address, server.port, http.response.status_code, error.type, and custom bitbucket.project_key / bitbucket.repository_slug attributes - Set ActivityStatusCode.Error for 4xx/5xx responses - Record exception events with type and message on transport errors - Store Activity ref on HttpRequestMessage.Options (AsyncLocal does not propagate back from Flurl's async RaiseEventAsync) - Near-zero overhead when no listeners are subscribed - Add TracingMockTests with ActivityListener-based assertions --- src/Bitbucket.Net/BitbucketClient.Tracing.cs | 104 ++++++++++++++ src/Bitbucket.Net/BitbucketClient.cs | 5 +- .../MockTests/TracingMockTests.cs | 133 ++++++++++++++++++ 3 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/Bitbucket.Net/BitbucketClient.Tracing.cs create mode 100644 test/Bitbucket.Net.Tests/MockTests/TracingMockTests.cs 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 8a9109d..20567e2 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -226,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) 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 From 5925a1f8d3d3fc59c00f724d08e0424865266338 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:41:29 +0000 Subject: [PATCH 23/31] refactor: remove unused extension classes Delete BitbucketEnumExtensions (38 dead enum wrappers) and TypeExtensions (dead IsNullableType helper), along with their tests. --- .../Common/BitbucketEnumExtensions.cs | 128 ------------------ src/Bitbucket.Net/Common/TypeExtensions.cs | 16 --- .../UnitTests/CommonModelTests.cs | 46 ------- 3 files changed, 190 deletions(-) delete mode 100644 src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs delete mode 100644 src/Bitbucket.Net/Common/TypeExtensions.cs diff --git a/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs b/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs deleted file mode 100644 index 823b1a9..0000000 --- a/src/Bitbucket.Net/Common/BitbucketEnumExtensions.cs +++ /dev/null @@ -1,128 +0,0 @@ -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; - -/// -/// Extension methods for converting Bitbucket API enums to their wire-format strings. -/// -public static class BitbucketEnumExtensions -{ - public static string ToApiString(this BranchOrderBy value) - => BitbucketEnumMaps.BranchOrderBy.ToApiString(value); - - public static string ToApiString(this PullRequestDirections value) - => BitbucketEnumMaps.PullRequestDirections.ToApiString(value); - - public static string ToApiString(this PullRequestStates value) - => BitbucketEnumMaps.PullRequestStates.ToApiString(value); - - public static string? ToApiString(this PullRequestStates? value) - => BitbucketEnumMaps.PullRequestStates.ToApiString(value); - - public static string ToApiString(this PullRequestOrders value) - => BitbucketEnumMaps.PullRequestOrders.ToApiString(value); - - public static string? ToApiString(this PullRequestOrders? value) - => BitbucketEnumMaps.PullRequestOrders.ToApiString(value); - - public static string ToApiString(this PullRequestFromTypes value) - => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(value); - - public static string? ToApiString(this PullRequestFromTypes? value) - => BitbucketEnumMaps.PullRequestFromTypes.ToApiString(value); - - public static string ToApiString(this Permissions value) - => BitbucketEnumMaps.Permissions.ToApiString(value); - - public static string? ToApiString(this Permissions? value) - => BitbucketEnumMaps.Permissions.ToApiString(value); - - public static string ToApiString(this MergeCommits value) - => BitbucketEnumMaps.MergeCommits.ToApiString(value); - - public static string ToApiString(this Roles value) - => BitbucketEnumMaps.Roles.ToApiString(value); - - public static string? ToApiString(this Roles? value) - => BitbucketEnumMaps.Roles.ToApiString(value); - - public static string ToApiString(this LineTypes value) - => BitbucketEnumMaps.LineTypes.ToApiString(value); - - public static string? ToApiString(this LineTypes? value) - => BitbucketEnumMaps.LineTypes.ToApiString(value); - - public static string ToApiString(this FileTypes value) - => BitbucketEnumMaps.FileTypes.ToApiString(value); - - public static string? ToApiString(this FileTypes? value) - => BitbucketEnumMaps.FileTypes.ToApiString(value); - - public static string ToApiString(this ChangeScopes value) - => BitbucketEnumMaps.ChangeScopes.ToApiString(value); - - public static string ToApiString(this LogLevels value) - => BitbucketEnumMaps.LogLevels.ToApiString(value); - - public static string ToApiString(this ParticipantStatus value) - => BitbucketEnumMaps.ParticipantStatus.ToApiString(value); - - public static string ToApiString(this HookTypes value) - => BitbucketEnumMaps.HookTypes.ToApiString(value); - - public static string ToApiString(this ScopeTypes value) - => BitbucketEnumMaps.ScopeTypes.ToApiString(value); - - public static string ToApiString(this ArchiveFormats value) - => BitbucketEnumMaps.ArchiveFormats.ToApiString(value); - - public static string ToApiString(this WebHookOutcomes value) - => BitbucketEnumMaps.WebHookOutcomes.ToApiString(value); - - public static string? ToApiString(this WebHookOutcomes? value) - => BitbucketEnumMaps.WebHookOutcomes.ToApiString(value); - - public static string ToApiString(this AnchorStates value) - => BitbucketEnumMaps.AnchorStates.ToApiString(value); - - public static string ToApiString(this DiffTypes value) - => BitbucketEnumMaps.DiffTypes.ToApiString(value); - - public static string? ToApiString(this DiffTypes? value) - => BitbucketEnumMaps.DiffTypes.ToApiString(value); - - public static string ToApiString(this TagTypes value) - => BitbucketEnumMaps.TagTypes.ToApiString(value); - - public static string ToApiString(this RefRestrictionTypes value) - => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(value); - - public static string? ToApiString(this RefRestrictionTypes? value) - => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(value); - - public static string ToApiString(this RefMatcherTypes value) - => BitbucketEnumMaps.RefMatcherTypes.ToApiString(value); - - public static string? ToApiString(this RefMatcherTypes? value) - => BitbucketEnumMaps.RefMatcherTypes.ToApiString(value); - - public static string ToApiString(this SynchronizeActions value) - => BitbucketEnumMaps.SynchronizeActions.ToApiString(value); - - public static string ToApiString(this BlockerCommentState value) - => BitbucketEnumMaps.BlockerCommentState.ToApiString(value); - - public static string? ToApiString(this BlockerCommentState? value) - => BitbucketEnumMaps.BlockerCommentState.ToApiString(value); - - public static string ToApiString(this CommentSeverity value) - => BitbucketEnumMaps.CommentSeverity.ToApiString(value); - - public static string? ToApiString(this CommentSeverity? value) - => BitbucketEnumMaps.CommentSeverity.ToApiString(value); -} \ 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/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs index fab23eb..430679d 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs @@ -204,52 +204,6 @@ 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] From ab638093420215316158550888751c79c34fff49 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:41:48 +0000 Subject: [PATCH 24/31] refactor: remove uncalled conversion methods from BitbucketHelpers Delete 15 StringTo* methods (keep only StringToLogLevel which has callers), StringToBool, and 4 *ToString methods that had zero call sites. Remove corresponding tests. --- src/Bitbucket.Net/Common/BitbucketHelpers.cs | 185 --------------- .../UnitTests/BitbucketHelpersTests.cs | 218 ------------------ 2 files changed, 403 deletions(-) diff --git a/src/Bitbucket.Net/Common/BitbucketHelpers.cs b/src/Bitbucket.Net/Common/BitbucketHelpers.cs index 0b3ac84..35c805f 100644 --- a/src/Bitbucket.Net/Common/BitbucketHelpers.cs +++ b/src/Bitbucket.Net/Common/BitbucketHelpers.cs @@ -3,7 +3,6 @@ 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; @@ -33,13 +32,6 @@ 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 @@ -87,15 +79,6 @@ public static string PullRequestStateToString(PullRequestStates state) public static string? PullRequestStateToString(PullRequestStates? state) => BitbucketEnumMaps.PullRequestStates.ToApiString(state); - /// - /// 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) - => BitbucketEnumMaps.PullRequestStates.FromApiString(s); - #endregion #region PullRequestOrders @@ -159,15 +142,6 @@ public static string PermissionToString(Permissions permission) public static string? PermissionToString(Permissions? permission) => BitbucketEnumMaps.Permissions.ToApiString(permission); - /// - /// 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) - => BitbucketEnumMaps.Permissions.FromApiString(s); - #endregion #region MergeCommits @@ -202,15 +176,6 @@ public static string RoleToString(Roles role) public static string? RoleToString(Roles? role) => BitbucketEnumMaps.Roles.ToApiString(role); - /// - /// 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) - => BitbucketEnumMaps.Roles.FromApiString(s); - #endregion #region LineTypes @@ -232,15 +197,6 @@ public static string LineTypeToString(LineTypes lineType) public static string? LineTypeToString(LineTypes? lineType) => BitbucketEnumMaps.LineTypes.ToApiString(lineType); - /// - /// 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) - => BitbucketEnumMaps.LineTypes.FromApiString(s); - #endregion #region FileTypes @@ -262,15 +218,6 @@ public static string FileTypeToString(FileTypes fileType) public static string? FileTypeToString(FileTypes? fileType) => BitbucketEnumMaps.FileTypes.ToApiString(fileType); - /// - /// 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) - => BitbucketEnumMaps.FileTypes.FromApiString(s); - #endregion #region ChangeScopes @@ -319,59 +266,6 @@ public static LogLevels StringToLogLevel(string s) public static string ParticipantStatusToString(ParticipantStatus participantStatus) => BitbucketEnumMaps.ParticipantStatus.ToApiString(participantStatus); - /// - /// 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) - => BitbucketEnumMaps.ParticipantStatus.FromApiString(s); - - #endregion - - #region HookTypes - - /// - /// 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) - => BitbucketEnumMaps.HookTypes.ToApiString(hookType); - - /// - /// 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) - => BitbucketEnumMaps.HookTypes.FromApiString(s); - - #endregion - - #region ScopeTypes - - /// - /// 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) - => BitbucketEnumMaps.ScopeTypes.ToApiString(scopeType); - - /// - /// 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) - => BitbucketEnumMaps.ScopeTypes.FromApiString(s); - #endregion #region ArchiveFormats @@ -406,15 +300,6 @@ public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) => BitbucketEnumMaps.WebHookOutcomes.ToApiString(webHookOutcome); - /// - /// 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) - => BitbucketEnumMaps.WebHookOutcomes.FromApiString(s); - #endregion #region AnchorStates @@ -483,15 +368,6 @@ public static string RefRestrictionTypeToString(RefRestrictionTypes refRestricti public static string? RefRestrictionTypeToString(RefRestrictionTypes? refRestrictionType) => BitbucketEnumMaps.RefRestrictionTypes.ToApiString(refRestrictionType); - /// - /// 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) - => BitbucketEnumMaps.RefRestrictionTypes.FromApiString(s); - #endregion #region RefMatcherTypes @@ -515,28 +391,6 @@ private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) #endregion - #region SynchronizeActions - - /// - /// 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) - => BitbucketEnumMaps.SynchronizeActions.ToApiString(synchronizeAction); - - /// - /// Parses a synchronization action string into a value. - /// - /// The string returned by the API. - /// The parsed action. - /// Thrown when the value is not recognized. - public static SynchronizeActions StringToSynchronizeAction(string s) - => BitbucketEnumMaps.SynchronizeActions.FromApiString(s); - - #endregion - #region BlockerCommentState /// @@ -556,44 +410,5 @@ public static string BlockerCommentStateToString(BlockerCommentState state) public static string? BlockerCommentStateToString(BlockerCommentState? state) => BitbucketEnumMaps.BlockerCommentState.ToApiString(state); - /// - /// 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) - => BitbucketEnumMaps.BlockerCommentState.FromApiString(s); - - #endregion - - #region CommentSeverity - - /// - /// 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) - => BitbucketEnumMaps.CommentSeverity.ToApiString(severity); - - /// - /// 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) - => BitbucketEnumMaps.CommentSeverity.ToApiString(severity); - - /// - /// 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) - => BitbucketEnumMaps.CommentSeverity.FromApiString(s); - #endregion } \ 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 From 8b866c9a9bb231013146f5eecb61b3a9b71ab9bd Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:42:05 +0000 Subject: [PATCH 25/31] refactor: remove unused ExecuteAsync overloads Delete 3 private static overloads (ExecuteAsync, ExecuteAsync, ExecuteWithNoContentAsync) with zero call sites. The stream-based Execute*StreamAsync methods remain in use. --- src/Bitbucket.Net/BitbucketClient.cs | 53 ---------------------------- 1 file changed, 53 deletions(-) diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index 20567e2..a5dcca3 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -389,59 +389,6 @@ private static async Task HandleResponseAsync(IFlurlResponse response, Can return await ReadResponseContentAsync(response, cancellationToken).ConfigureAwait(false); } - /// - /// Executes an HTTP request with unified error handling and deserialization. - /// Use this method for all new API methods to ensure errors are never silently ignored. - /// - /// The expected deserialized result type. - /// The configured Flurl request. - /// A delegate that performs the HTTP verb (e.g., static (r, ct) => r.GetAsync(ct)). - /// Optional custom handler for non-JSON response bodies. - /// Token to cancel the operation. - /// The deserialized response content. - private static async Task ExecuteAsync( - IFlurlRequest request, - Func> httpMethod, - Func? contentHandler = null, - CancellationToken cancellationToken = default) - { - var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); - return await HandleResponseAsync(response, contentHandler, cancellationToken).ConfigureAwait(false); - } - - /// - /// Executes an HTTP request with unified error handling and returns a boolean success indicator. - /// Use this method for API methods that return success/failure based on an empty response body. - /// - /// The configured Flurl request. - /// A delegate that performs the HTTP verb. - /// Token to cancel the operation. - /// true if the response body is empty (indicating success); otherwise, false. - private static async Task ExecuteAsync( - IFlurlRequest request, - Func> httpMethod, - CancellationToken cancellationToken = default) - { - var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - /// - /// Executes an HTTP request with unified error handling without returning content. - /// Use this method for API methods that only need to verify the request succeeded. - /// - /// The configured Flurl request. - /// A delegate that performs the HTTP verb. - /// Token to cancel the operation. - private static async Task ExecuteWithNoContentAsync( - IFlurlRequest request, - Func> httpMethod, - CancellationToken cancellationToken = default) - { - var response = await httpMethod(request, cancellationToken).ConfigureAwait(false); - await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); - } - /// /// Retrieves paged results from a paginated endpoint. /// From af88fbc56955c3a3388f987e74327a065c54356f Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:01:45 +0000 Subject: [PATCH 26/31] refactor: inline UnixDateTimeExtensions into converter Replace thin extension wrappers with direct BCL calls to DateTimeOffset.FromUnixTimeMilliseconds in the JSON converters. Delete UnixDateTimeExtensions.cs and its tests. --- .../Converters/UnixDateTimeOffsetConverter.cs | 8 +-- .../Common/UnixDateTimeExtensions.cs | 28 ---------- .../UnitTests/CommonModelTests.cs | 54 ------------------- 3 files changed, 4 insertions(+), 86 deletions(-) delete mode 100644 src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs 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/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/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs index 430679d..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,57 +202,4 @@ public void ErrorResponse_Serialization_RoundTrips() } #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 From eaff4aa6543bcdfdbc6327f043438568d3a8d03a Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:56:34 +0000 Subject: [PATCH 27/31] refactor: add GetPagedAsync and GetPagedStreamAsync helpers Introduce two convenience wrappers that absorb the repeated GET + deserialize lambda used by all paginated endpoints. The underlying GetPagedResultsAsync/StreamAsync loop methods remain unchanged. --- src/Bitbucket.Net/BitbucketClient.cs | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index a5dcca3..9bdd0b7 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -389,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. /// From be5965df3e2e831fd8eade293da876566b328249 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:58:28 +0000 Subject: [PATCH 28/31] refactor: migrate 82 paginated endpoints to shared helpers Replace inline GET + deserialize lambdas in all 64 list and 18 stream paginated methods with calls to GetPagedAsync and GetPagedStreamAsync. Remove async from method signatures where the only await was the paged call. --- src/Bitbucket.Net/Audit/BitbucketClient.cs | 30 +--- src/Bitbucket.Net/Branches/BitbucketClient.cs | 15 +- src/Bitbucket.Net/Builds/BitbucketClient.cs | 14 +- .../CommentLikes/BitbucketClient.cs | 30 +--- .../Core/Admin/BitbucketClient.cs | 141 ++++-------------- .../Core/Dashboard/BitbucketClient.cs | 40 +---- .../Core/Groups/BitbucketClient.cs | 16 +- .../Core/Inbox/BitbucketClient.cs | 26 +--- .../Core/Profile/BitbucketClient.cs | 15 +- .../Core/Projects/BitbucketClient.Branches.cs | 26 +--- .../Core/Projects/BitbucketClient.Commits.cs | 90 +++-------- .../Core/Projects/BitbucketClient.Compare.cs | 43 ++---- .../Core/Projects/BitbucketClient.Projects.cs | 82 +++------- .../BitbucketClient.PullRequestComments.cs | 26 +--- .../BitbucketClient.PullRequestDetails.cs | 76 ++-------- .../Projects/BitbucketClient.PullRequests.cs | 69 ++------- .../Projects/BitbucketClient.Repositories.cs | 110 +++----------- .../BitbucketClient.RepositorySettings.cs | 54 ++----- .../Core/Projects/BitbucketClient.Tasks.cs | 51 ++----- .../Core/Repos/BitbucketClient.cs | 26 +--- .../Core/Users/BitbucketClient.cs | 15 +- src/Bitbucket.Net/Jira/BitbucketClient.cs | 15 +- .../PersonalAccessTokens/BitbucketClient.cs | 15 +- .../RefRestrictions/BitbucketClient.cs | 29 +--- src/Bitbucket.Net/Ssh/BitbucketClient.cs | 71 ++------- 25 files changed, 230 insertions(+), 895 deletions(-) diff --git a/src/Bitbucket.Net/Audit/BitbucketClient.cs b/src/Bitbucket.Net/Audit/BitbucketClient.cs index 8a6de7a..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,7 +29,7 @@ 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, @@ -47,16 +45,8 @@ public async Task> GetProjectAuditEventsAsync(string p ["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); } /// @@ -70,7 +60,7 @@ public async Task> GetProjectAuditEventsAsync(string p /// 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, @@ -87,15 +77,7 @@ public async Task> GetProjectRepoAuditEventsAsync(stri ["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/Branches/BitbucketClient.cs b/src/Bitbucket.Net/Branches/BitbucketClient.cs index 88d2736..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,7 +37,7 @@ 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, @@ -54,16 +53,8 @@ public async Task> GetCommitBranchInfoAsync(string pro ["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); } /// diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index af97c24..a6ccdce 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -1,5 +1,4 @@ using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Models.Builds.Requests; using Flurl.Http; @@ -78,7 +77,7 @@ 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, @@ -92,15 +91,8 @@ public async Task> GetBuildStatusForCommitAsync(strin ["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); } /// diff --git a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs index 219e3c5..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,7 +35,7 @@ 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, @@ -56,16 +54,8 @@ public async Task> GetCommitCommentLikesAsync(string project ["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); } /// @@ -126,7 +116,7 @@ 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, @@ -143,16 +133,8 @@ public async Task> GetPullRequestCommentLikesAsync(string pr ["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); } /// diff --git a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs index 8829d07..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(strin ["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 ["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 /// 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(stri ["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(stri /// 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 = n ["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> GetAdminUserMoreMembersAs ["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> GetAdminUserMoreMembersAs /// 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> GetAdminUserMoreNonMember ["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( ["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> GetAdminGroupPermissionsN ["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> GetAdminGroupPermissionsN /// 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(st ["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? ["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); } /// diff --git a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs index 2abdd56..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(Pull ["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> GetDashboardPullRequestS ["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 0ea436f..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 = nul ["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/Inbox/BitbucketClient.cs b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs index 8b33851..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/Profile/BitbucketClient.cs b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs index a2436d9..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? pe ["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 82dec63..399b5b5 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -1,5 +1,4 @@ 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; @@ -24,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, @@ -44,16 +43,8 @@ public async Task> GetBranchesAsync(string projectKey, str ["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); } /// @@ -79,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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs index 2fe2d49..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, stri ["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, stri ["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); } /// @@ -217,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, @@ -235,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); } /// @@ -278,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); } /// @@ -302,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, @@ -320,16 +274,8 @@ public async Task> GetCommitCommentsAsync(string projectK ["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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs index 5305f66..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 ["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 ["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 projectK ["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 e78794d..c601058 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.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 Bitbucket.Net.Models.Core.Projects.Requests; @@ -69,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, @@ -85,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); } /// @@ -123,15 +114,8 @@ 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); } /// @@ -215,7 +199,7 @@ 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, @@ -232,16 +216,8 @@ public async Task> GetProjectUserPermissionsAsync( ["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); } /// @@ -306,7 +282,7 @@ 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, @@ -321,16 +297,8 @@ public async Task> GetProjectUserPermissionsNoneAsyn ["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); } /// @@ -343,7 +311,7 @@ public async Task> GetProjectUserPermissionsNoneAsyn /// 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, @@ -358,16 +326,8 @@ public async Task> GetProjectGroupPermissionsAsyn ["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); } /// @@ -432,7 +392,7 @@ 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, @@ -447,16 +407,8 @@ public async Task> GetProjectGroupPermissionsNoneAsy ["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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs index 2f468e6..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 ["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 205e41e..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> GetPullRequestActivitiesAs ["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 proje ["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 proje ["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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs index d1acfdf..b41e106 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -1,5 +1,4 @@ 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; @@ -22,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, @@ -40,16 +39,8 @@ public async Task> GetRepositoryParticipantsAsync(string ["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); } /// @@ -68,7 +59,7 @@ public async Task> GetRepositoryParticipantsAsync(string /// 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, @@ -92,16 +83,8 @@ public async Task> GetPullRequestsAsync(string projec ["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); } /// @@ -131,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); } /// @@ -389,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, @@ -403,17 +379,9 @@ public async Task> GetPullRequestParticipantsAsync(st ["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); } /// @@ -442,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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs index f60ea8f..ad7ede4 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.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 Bitbucket.Net.Models.Core.Projects.Requests; @@ -19,7 +18,7 @@ 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, @@ -33,16 +32,8 @@ public async Task> GetProjectRepositoriesAsync(string ["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); } /// @@ -62,15 +53,8 @@ public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string pro ["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); } /// @@ -184,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, @@ -196,16 +180,8 @@ public async Task> GetProjectRepositoryForksAsync( ["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); } /// @@ -234,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, @@ -246,16 +222,8 @@ public async Task> GetRelatedProjectRepositoriesAs ["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); } /// @@ -307,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, @@ -321,16 +289,8 @@ public async Task> GetProjectRepositoryGroupPermi ["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); } /// @@ -387,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, @@ -401,16 +361,8 @@ public async Task> GetProjectRepositoryGroup ["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); } /// @@ -425,7 +377,7 @@ public async Task> GetProjectRepositoryGroup /// 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, @@ -441,16 +393,8 @@ public async Task> GetProjectRepositoryUserPermiss ["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); } /// @@ -511,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, @@ -525,15 +469,7 @@ public async Task> GetProjectRepositoryUserPermissionsNoneAs ["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 bd954f4..2587800 100644 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.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 Bitbucket.Net.Models.Core.Projects.Requests; @@ -93,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, @@ -107,16 +106,8 @@ public async Task> GetProjectRepositoryHooksSettingsAsync(st ["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); } /// @@ -287,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, @@ -303,16 +294,8 @@ public async Task> GetProjectRepositoryTagsAsync(string proje ["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); } /// @@ -343,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); } /// @@ -415,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, @@ -431,16 +407,8 @@ public async Task> GetProjectRepositoryWebHooksAsync(stri ["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); } /// diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs index 87930fb..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 ["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> GetPullRequestBlockerCommentsAs ["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); } /// diff --git a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs index 46464a4..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/Users/BitbucketClient.cs b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs index 209e443..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, stri 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); } /// diff --git a/src/Bitbucket.Net/Jira/BitbucketClient.cs b/src/Bitbucket.Net/Jira/BitbucketClient.cs index c94c86e..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,7 +34,7 @@ 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, @@ -50,16 +49,8 @@ public async Task> GetChangeSetsAsync(string issueKey, ["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); } /// diff --git a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs index 1df17d9..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,7 +33,7 @@ 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, @@ -50,16 +49,8 @@ public async Task> GetUserAccessTokensAsync(string us ["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); } /// diff --git a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs index 299ec51..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, @@ -59,16 +58,8 @@ public async Task> GetProjectRefRestrictionsAsync( ["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); } /// @@ -174,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, @@ -197,16 +188,8 @@ public async Task> GetRepositoryRefRestrictionsAsy ["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); } /// diff --git a/src/Bitbucket.Net/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index bb617c7..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; @@ -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, @@ -133,16 +124,8 @@ public async Task> GetProjectKeysAsync(string projectK ["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); } /// @@ -235,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, @@ -247,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); } /// @@ -272,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, @@ -293,16 +268,8 @@ public async Task> GetRepoKeysAsync(string projectK ["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); } /// @@ -403,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, @@ -416,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); } /// From b85d2330a9fdd2d1188fd1283d90e806fda6d98c Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:51:20 +0000 Subject: [PATCH 29/31] refactor: decompose IBitbucketClient into domain-specific sub-interfaces Split the monolithic IBitbucketClient (287 methods) into 12 focused sub-interfaces co-located with their implementations: - IProjectOperations, IRepositoryOperations, IPullRequestOperations - ICommitOperations, IBranchOperations, IAdminOperations - ISshOperations, IRefRestrictionOperations, IBuildOperations - IGitOperations, ISearchOperations, IBitbucketMiscOperations IBitbucketClient becomes a composition root inheriting all sub-interfaces. Non-breaking change: all existing code using IBitbucketClient continues to work. Consumers can now depend on narrower interfaces (ISP). --- src/Bitbucket.Net/Builds/IBuildOperations.cs | 16 + .../Core/Admin/IAdminOperations.cs | 47 ++ src/Bitbucket.Net/Core/ISearchOperations.cs | 16 + .../Core/Projects/IBranchOperations.cs | 26 ++ .../Core/Projects/ICommitOperations.cs | 34 ++ .../Core/Projects/IProjectOperations.cs | 29 ++ .../Core/Projects/IPullRequestOperations.cs | 69 +++ .../Core/Projects/IRepositoryOperations.cs | 57 +++ src/Bitbucket.Net/Git/IGitOperations.cs | 15 + src/Bitbucket.Net/IBitbucketClient.cs | 435 +----------------- src/Bitbucket.Net/IBitbucketMiscOperations.cs | 127 +++++ .../IRefRestrictionOperations.cs | 26 ++ src/Bitbucket.Net/Ssh/ISshOperations.cs | 31 ++ 13 files changed, 507 insertions(+), 421 deletions(-) create mode 100644 src/Bitbucket.Net/Builds/IBuildOperations.cs create mode 100644 src/Bitbucket.Net/Core/Admin/IAdminOperations.cs create mode 100644 src/Bitbucket.Net/Core/ISearchOperations.cs create mode 100644 src/Bitbucket.Net/Core/Projects/IBranchOperations.cs create mode 100644 src/Bitbucket.Net/Core/Projects/ICommitOperations.cs create mode 100644 src/Bitbucket.Net/Core/Projects/IProjectOperations.cs create mode 100644 src/Bitbucket.Net/Core/Projects/IPullRequestOperations.cs create mode 100644 src/Bitbucket.Net/Core/Projects/IRepositoryOperations.cs create mode 100644 src/Bitbucket.Net/Git/IGitOperations.cs create mode 100644 src/Bitbucket.Net/IBitbucketMiscOperations.cs create mode 100644 src/Bitbucket.Net/RefRestrictions/IRefRestrictionOperations.cs create mode 100644 src/Bitbucket.Net/Ssh/ISshOperations.cs 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/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/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/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/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 index c9c83d1..3953d9e 100644 --- a/src/Bitbucket.Net/IBitbucketClient.cs +++ b/src/Bitbucket.Net/IBitbucketClient.cs @@ -1,429 +1,22 @@ -using Bitbucket.Net.Builders; -using Bitbucket.Net.Common.Models.Search; -using Bitbucket.Net.Models.Audit; -using Bitbucket.Net.Models.Branches; -using Bitbucket.Net.Models.Builds; -using Bitbucket.Net.Models.Builds.Requests; -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.Git; -using Bitbucket.Net.Models.Jira; -using Bitbucket.Net.Models.PersonalAccessTokens; -using Bitbucket.Net.Models.RefRestrictions; -using Bitbucket.Net.Models.RefSync; -using Bitbucket.Net.Models.Ssh; - namespace Bitbucket.Net; /// /// Abstraction over the Bitbucket Server REST API client, enabling /// dependency injection, unit testing with mocks, and decorator patterns. /// -public interface IBitbucketClient : IDisposable +public interface IBitbucketClient : + IProjectOperations, + IRepositoryOperations, + IPullRequestOperations, + ICommitOperations, + IBranchOperations, + IAdminOperations, + ISshOperations, + IRefRestrictionOperations, + IBuildOperations, + IGitOperations, + ISearchOperations, + IBitbucketMiscOperations, + IDisposable { - // ── 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); - - // ── Branches ───────────────────────────────────────────────────── - - 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); - - // ── Builders ───────────────────────────────────────────────────── - - PullRequestQueryBuilder PullRequests(string projectKey, string repositorySlug); - CommitQueryBuilder Commits(string projectKey, string repositorySlug, string until); - BranchQueryBuilder Branches(string projectKey, string repositorySlug); - ProjectQueryBuilder Projects(); - - // ── Builds ─────────────────────────────────────────────────────── - - 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); - - // ── 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); - - // ── Git ────────────────────────────────────────────────────────── - - 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); - - // ── 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); - - // ── Ref Restrictions ───────────────────────────────────────────── - - 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); - - // ── Ref Sync ───────────────────────────────────────────────────── - - 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); - - // ── SSH ────────────────────────────────────────────────────────── - - 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); - - // ── Admin ──────────────────────────────────────────────────────── - - 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); - - // ── 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); - - // ── Repos ──────────────────────────────────────────────────────── - - 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); - - // ── Search ─────────────────────────────────────────────────────── - - Task SearchCodeAsync(string query, int primaryLimit = 25, int secondaryLimit = 10, CancellationToken cancellationToken = default); - Task IsSearchAvailableAsync(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); - - // ── Projects ───────────────────────────────────────────────────── - - 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); - - // ── Repositories ───────────────────────────────────────────────── - - 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); - - // ── Repository Settings ────────────────────────────────────────── - - 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); - - // ── Compare ────────────────────────────────────────────────────── - - 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); - - // ── Commits ────────────────────────────────────────────────────── - - 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); - - // ── Branches (project-level) ───────────────────────────────────── - - 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); - - // ── Pull Requests ──────────────────────────────────────────────── - - 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); - - // ── Pull Request Details ───────────────────────────────────────── - - 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); - - // ── Pull Request Comments ──────────────────────────────────────── - - 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); - - // ── Pull Request Tasks ─────────────────────────────────────────── - - [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/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/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/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 From d1e8a8fd0f74f393dcfbbe7b25845dd0f14680fe Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:02:31 +0000 Subject: [PATCH 30/31] docs: update README and CHANGELOG for 1.0.0 stable release --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++-------------- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b103c..1570120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 @@ -14,13 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Consumers assigning results to `IEnumerable` are unaffected; consumers assigning to `List` must add `.ToList()` or change the variable type. -- **Init-only model properties** (Spec 009): 377 properties across 106 +- **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** (Spec 010): Write operations now accept +- **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`, @@ -41,77 +41,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Source-gen-only deserialization** (Spec 001): Removed the +- **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** (Spec 002): `ReadResponseContentAsync` +- **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** (Spec 003): All 25 enum-to-string +- **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** (Spec 008): Introduced generic +- **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`** (Spec 013): Both `s_jsonOptions` +- **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** (Spec 006): All 27 buffered +- **`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** (Spec 016): Version bumped to +- **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`** (Spec 014): +- **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** (Spec 012): +- **`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`** (Spec 004): The client now +- **`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** (Spec 005): New +- **`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** (Spec 007): ~130 public methods now +- **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** (Spec 011): New `PullRequestQueryBuilder`, +- **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 @@ -128,7 +145,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `await` uses `ConfigureAwait(false)`. - Added `InputValidationTests` (17 parameterized theories) covering null/empty/whitespace rejection for key path-segment parameters. -- Total test count increased from 696 to 799 (+103 new tests). +- Total test count: 749. + +## [0.3.0] - 2026-02-09 + +### 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 99f7571..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 @@ -58,6 +64,19 @@ var client = new BitbucketClient("https://bitbucket.example.com", "username", "p 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, and centralized configuration. @@ -177,9 +196,33 @@ await foreach (var pr in client.GetDashboardPullRequestsStreamAsync()) } ``` -### Exception Handling +### Fluent query builders + +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 -The library provides typed exceptions for precise error handling: +Typed exceptions give you precise control over error handling: ```csharp try From f193ba02b1a5f4e780d901e96e09d215cd192545 Mon Sep 17 00:00:00 2001 From: diomonogatari <17832206+diomonogatari@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:25:21 +0000 Subject: [PATCH 31/31] docs: correct date format for version 0.3.0 in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1570120..c9bdaae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,7 +147,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 null/empty/whitespace rejection for key path-segment parameters. - Total test count: 749. -## [0.3.0] - 2026-02-09 +## [0.3.0] - 2026-02-010 ### Added