diff --git a/.editorconfig b/.editorconfig
index d9f8964..e516371 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,261 @@
-[*.cs]
+root = true
-# IDE0055: Fix formatting
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+max_line_length = 170
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = false # Override due to partial classes across folders
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:silent
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = false
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = false:suggestion
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:suggestion
+csharp_style_prefer_primary_constructors = true:warning
+csharp_style_prefer_top_level_statements = true:silent
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+# ==========================================================================
+# Diagnostic overrides
+# ==========================================================================
+
+# IDE0055: Fix formatting — disabled (handled by dotnet format on demand)
dotnet_diagnostic.IDE0055.severity = none
+
+# IDE0005: Using directive is unnecessary
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0130: Namespace does not match folder structure — this codebase uses
+# partial classes across folders, so namespace != folder is expected
+dotnet_diagnostic.IDE0130.severity = none
+
+# IDE* suggestions that can cause noisy or potentially breaking refactors when
+# running `dotnet format style --severity info` (e.g., renames). Keep formatting
+# verification focused on whitespace + explicit style preferences.
+dotnet_diagnostic.IDE1006.severity = none
+dotnet_diagnostic.IDE0028.severity = none
+dotnet_diagnostic.IDE0300.severity = none
+dotnet_diagnostic.IDE0305.severity = none
+
+# Meziantou/CA rules that have no auto code-fix — suppress so dotnet format
+# does not emit "Unable to fix" warnings
+dotnet_diagnostic.MA0011.severity = none
+dotnet_diagnostic.MA0015.severity = none
+dotnet_diagnostic.MA0016.severity = none
+dotnet_diagnostic.MA0048.severity = none
+dotnet_diagnostic.CA2024.severity = none
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fde4c26
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Normalize all text files to CRLF on checkout (matches .editorconfig end_of_line = crlf)
+* text=auto eol=crlf
+
+# Shell scripts must keep LF to run on Linux/macOS
+*.sh text eol=lf
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100644
index 0000000..6f7daf3
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Git pre-commit hook.
+#
+# To enable for this repo:
+# bash scripts/setup-githooks.sh
+# (or) ./scripts/setup-githooks.ps1
+#
+# To bypass once:
+# git commit --no-verify
+
+if [[ "${SKIP_DOTNET_FORMAT:-}" == "1" ]]; then
+ exit 0
+fi
+
+ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)"
+if [[ -z "$ROOT_DIR" ]]; then
+ echo "pre-commit: not inside a git repository" >&2
+ exit 1
+fi
+
+bash "$ROOT_DIR/scripts/verify-format.sh" --verify || {
+ echo "" >&2
+ echo "Commit aborted: formatting verification failed." >&2
+ echo "Fix by running: bash scripts/verify-format.sh --fix" >&2
+ exit 1
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d9dc4c..451b222 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,8 +31,21 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Verify formatting
+ run: bash scripts/verify-format.sh --verify
+
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
- run: dotnet test --no-build -c Release --verbosity normal
+ run: >
+ dotnet test --no-build -c Release --verbosity normal
+ --collect:"XPlat Code Coverage"
+ --results-directory ./coverage
+
+ - name: Upload coverage
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ uses: codecov/codecov-action@v4
+ with:
+ directory: ./coverage
+ fail_ci_if_error: false
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index f898bca..82abe25 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -33,13 +33,15 @@ jobs:
- name: Restore
run: dotnet restore
+ - name: Verify formatting
+ run: bash scripts/verify-format.sh --verify
+
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
run: dotnet test --no-build -c Release --verbosity normal
- # Extract version from tag (v0.1.0-beta.1 -> 0.1.0-beta.1)
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
@@ -48,7 +50,11 @@ jobs:
run: dotnet pack src/Bitbucket.Net/Bitbucket.Net.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.VERSION }}
- name: Push to NuGet.org
- run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
+ run: |
+ dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
+ dotnet nuget push ./artifacts/*.snupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Push to GitHub Packages
- run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate
+ run: |
+ dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate
+ dotnet nuget push ./artifacts/*.snupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..02f5b73
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "editor.formatOnSave": true,
+ "editor.formatOnSaveMode": "modificationsIfAvailable",
+ "editor.formatOnType": false,
+ "editor.rulers": [170],
+ "editor.wordWrapColumn": 170,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": "always",
+ "source.removeUnusedImports": "always",
+ "source.fixAll": "always"
+ }
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 53fc0fb..a054b60 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/Bitbucket.Net.sln",
+ "${workspaceFolder}/Bitbucket.Net.slnx",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
@@ -23,7 +23,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/Bitbucket.Net.sln",
+ "${workspaceFolder}/Bitbucket.Net.slnx",
"-c",
"Release",
"/property:GenerateFullPaths=true",
@@ -131,7 +131,7 @@
"type": "process",
"args": [
"clean",
- "${workspaceFolder}/Bitbucket.Net.sln"
+ "${workspaceFolder}/Bitbucket.Net.slnx"
],
"problemMatcher": "$msCompile"
},
diff --git a/Bitbucket.Net.sln b/Bitbucket.Net.sln
deleted file mode 100644
index 28ea8dc..0000000
--- a/Bitbucket.Net.sln
+++ /dev/null
@@ -1,62 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28803.156
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net", "src\Bitbucket.Net\Bitbucket.Net.csproj", "{51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CBBC7722-A73C-4504-81D7-E1FB82B851A5}"
- ProjectSection(SolutionItems) = preProject
- build\build.ps1 = build\build.ps1
- build\test.ps1 = build\test.ps1
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{778D1009-F274-4E3A-AC8C-D3B357F2DBE1}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- .gitignore = .gitignore
- appveyor.yml = appveyor.yml
- Bitbucket.Net.snk = Bitbucket.Net.snk
- global.json = global.json
- LICENSE = LICENSE
- README.md = README.md
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B7E00533-033F-48D3-A01C-40BD264F245A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net.Tests", "test\Bitbucket.Net.Tests\Bitbucket.Net.Tests.csproj", "{7775DD13-F980-4838-8FE4-6E8B96221298}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net.Benchmarks", "benchmarks\Bitbucket.Net.Benchmarks\Bitbucket.Net.Benchmarks.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Release|Any CPU.Build.0 = Release|Any CPU
- {7775DD13-F980-4838-8FE4-6E8B96221298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7775DD13-F980-4838-8FE4-6E8B96221298}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7775DD13-F980-4838-8FE4-6E8B96221298}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7775DD13-F980-4838-8FE4-6E8B96221298}.Release|Any CPU.Build.0 = Release|Any CPU
- {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Release|Any CPU
- {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Release|Any CPU
- {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {7775DD13-F980-4838-8FE4-6E8B96221298} = {B7E00533-033F-48D3-A01C-40BD264F245A}
- {B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {06DABD0E-1A16-4B84-9DF3-A1B8E73D18AF}
- EndGlobalSection
-EndGlobal
diff --git a/Bitbucket.Net.slnx b/Bitbucket.Net.slnx
new file mode 100644
index 0000000..f01e175
--- /dev/null
+++ b/Bitbucket.Net.slnx
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Bitbucket.Net.snk b/Bitbucket.Net.snk
deleted file mode 100644
index a5e5104..0000000
Binary files a/Bitbucket.Net.snk and /dev/null differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee69eff..665cd78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,19 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-## [0.1.0-beta.1] - 2026-02-06
+## [0.2.0] - 2026-02-08
-### Notes
+### Breaking Changes
+
+- **Exception handling**: The library now throws `BitbucketApiException` (and its typed subtypes) instead of `FlurlHttpException`. Consumers catching `FlurlHttpException` must update their catch blocks.
+- **`Comment` model**: No longer inherits from `PullRequestInfo`. Properties such as `Title`, `Description`, `FromRef`, `ToRef`, `Locked`, and `Reviewers` are removed from `Comment`. These were always null/default on comments and should not have been exposed.
+- **`Comment.State`**: Changed from `new string?` (hiding a `PullRequestStates` enum) to a plain `string?` property.
+- **Global Flurl configuration removed**: The library no longer calls `FlurlHttp.Clients.WithDefaults()`. Other Flurl consumers in the same process are no longer affected.
+
+### Added
+
+- **SourceLink**: Consumers can step into library source during debugging.
+- **Symbol packages**: `.snupkg` published alongside `.nupkg`.
+- **XML documentation**: IntelliSense documentation included in the NuGet package. Model classes now have comprehensive `` and `` XML docs.
+- **New streaming methods**:
+ - `GetPullRequestActivitiesStreamAsync`
+ - `GetPullRequestChangesStreamAsync`
+ - `GetPullRequestCommentsStreamAsync`
+ - `GetPullRequestParticipantsStreamAsync`
+ - `GetPullRequestTasksStreamAsync`
+ - `GetPullRequestBlockerCommentsStreamAsync`
+ - `GetDashboardPullRequestsStreamAsync`
+ - `GetInboxPullRequestsStreamAsync`
+ - `GetProjectRepositoryTagsStreamAsync`
+ - `GetChangesStreamAsync`
+ - `GetCommitChangesStreamAsync`
+- **`global.json`**: SDK version pinned for reproducible builds.
+- **`Directory.Build.props`**: Centralized build configuration (TFM, language version, nullable, warnings-as-errors).
+- **Code coverage**: CI collects and reports test coverage.
+- **File splitting**: Monolithic `Core/Projects/BitbucketClient.cs` (4 491 lines) split into 10 focused partial-class files by domain (projects, repositories, branches, commits, compare, pull requests, PR comments, PR details, tasks, repository settings).
+
+### Fixed
-This is the first public release of the modernized fork by
-[diomonogatari](https://github.com/diomonogatari).
-The version number intentionally starts at `0.x` to signal that the
-library is **not yet production-ready** — it is being dog-fooded
-in an MCP Server for on-prem Bitbucket Server but not every endpoint
-has been exhaustively tested.
+- **Typed exceptions now fire correctly** for all HTTP error responses. Previously, Flurl intercepted errors before the custom handling could run.
+- **`CancellationToken` propagation**: Helper methods now pass the token to underlying HTTP calls.
+- **`PullRequest.ToString()`**: No longer throws `NullReferenceException` when `Author` or `Author.User` is null.
+- **`Participant.ToString()`**: Same null-safety fix.
-The original [lvermeulen/Bitbucket.Net](https://github.com/lvermeulen/Bitbucket.Net)
-shipped up to 0.5.0 on NuGet; this fork is versioned independently.
+### Changed
+
+- Removed commented-out `Avatar` property from `ProjectDefinition`.
+- Fixed duplicate `` XML doc tag on `GetRepositoriesStreamAsync`.
+
+### Testing
+
+- Added streaming endpoint mock tests covering all 20 streaming methods (single-page, multi-page, empty result scenarios)
+- Added diff streaming tests for commit, repository, compare, and PR diffs (single, multiple, empty)
+- Added MCP extension method tests for `StreamDiffsWithLimitsAsync` and `TakeDiffsWithLimitsAsync`
+- Added cancellation token propagation tests (pre-cancelled tokens for buffered, streaming, and diff methods; mid-stream cancellation)
+- Added DI constructor integration tests for `HttpClient` and `IFlurlClient` injection paths (CRUD, error handling, streaming, auth header verification)
+- Introduced paginated fixture data (`projects-page1.json`, `projects-page2.json`, etc.) and `SetupPagedEndpoint` helper for multi-page mock tests
+- Total test count increased from 633 to 696 (+63 new tests)
+
+## [0.1.0-beta.1] - 2026-02-06 (pre-release)
+
+### Notes
+
+First public pre-release of the modernized fork. Superseded by 0.2.0.
## [2.0.0] - 2025-11-28 (internal)
@@ -35,11 +80,13 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
### Added
#### CancellationToken Support
+
- All async methods now accept an optional `CancellationToken` parameter
- Enables graceful cancellation of long-running operations
- Fully propagated to underlying HTTP calls
#### IAsyncEnumerable Streaming
+
- New streaming variants for paginated endpoints that yield items as they arrive:
- `GetProjectsStreamAsync()`
- `GetProjectRepositoriesStreamAsync()`
@@ -54,6 +101,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
- Native `await foreach` support
#### Diff and File Content Streaming
+
- New streaming methods for large diff responses:
- `GetCommitDiffStreamAsync()` - Stream diffs for a specific commit
- `GetRepositoryDiffStreamAsync()` - Stream repository diffs between refs
@@ -68,6 +116,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
- Reduced memory pressure for large file downloads
#### Dependency Injection Support
+
- New constructor: `BitbucketClient(HttpClient httpClient, string baseUrl, Func getToken = null)`
- Enables use with `IHttpClientFactory`
- Supports Polly resilience policies (retry, circuit breaker, etc.)
@@ -77,6 +126,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
- Supports `IFlurlClientCache` for named client management
#### Typed Exception Hierarchy
+
- New `BitbucketApiException` base class with rich error information:
- `StatusCode`: HTTP status code as `HttpStatusCode` enum
- `Context`: The field or resource that caused the error
@@ -93,12 +143,14 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
- `BitbucketServerException` (HTTP 5xx)
#### Code Quality Enforcement
+
- Added `Meziantou.Analyzer` to enforce library best practices
- ConfigureAwait(false) requirement enforced via MA0004 (warning level)
- Nullable reference types enabled project-wide
- EditorConfig configured with library-appropriate analyzer rules
#### Performance Benchmarks
+
- New benchmark project (`benchmarks/Bitbucket.Net.Benchmarks`) using BenchmarkDotNet
- Benchmark categories:
- **JSON Serialization**: Measure System.Text.Json performance for serialization/deserialization
@@ -126,6 +178,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
#### Updating from 1.x to 2.0.0
1. **Update Target Framework**
+
```xml
netstandard1.4
@@ -137,6 +190,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
2. **No Code Changes Required** for basic usage - the API remains backward compatible
3. **Optional: Use CancellationToken**
+
```csharp
// Before
var projects = await client.GetProjectsAsync();
@@ -147,6 +201,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
```
4. **Optional: Use Streaming for Large Results**
+
```csharp
// Before - buffers all results in memory
var allPRs = await client.GetPullRequestsAsync("PROJ", "repo");
@@ -159,6 +214,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
```
5. **Optional: Use Dependency Injection**
+
```csharp
// Configure with IHttpClientFactory + Polly
services.AddHttpClient()
@@ -173,6 +229,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently.
```
6. **Update Exception Handling** (Breaking Change)
+
```csharp
// Before - catching generic InvalidOperationException
try
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ee9797b
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+## Contributing
+
+Thanks for your interest in contributing! This repository enforces a consistent
+C# code style via `.editorconfig`, a pre-commit hook (opt-in), and CI checks.
+
+## Prerequisites
+
+- Git
+- .NET SDK 10.x
+- Bash (macOS/Linux: built-in; Windows: Git for Windows ships Git Bash)
+
+## Enable the pre-commit hook (recommended)
+
+Git hooks are not transferred by default when you clone a repository. This repo
+keeps hooks in `.githooks/` and uses `core.hooksPath` so they can be versioned.
+
+Run one of the setup scripts from the repo root:
+
+```powershell
+./scripts/setup-githooks.ps1
+```
+
+```bash
+bash scripts/setup-githooks.sh
+```
+
+This sets:
+
+- `git config core.hooksPath .githooks`
+
+After that, every `git commit` will run the formatting verification.
+
+### Bypass (when you really need to)
+
+- One-off bypass:
+
+```bash
+git commit --no-verify
+```
+
+- Disable the formatting hook for a command/session:
+
+```bash
+SKIP_DOTNET_FORMAT=1 git commit
+```
+
+## Formatting: verify vs fix
+
+The hook (and CI) run formatting in **verify** mode:
+
+- `dotnet format whitespace ... --verify-no-changes`
+- `dotnet format style ... --severity warn --verify-no-changes`
+
+To run the same checks manually:
+
+```powershell
+./scripts/verify-format.ps1
+```
+
+```bash
+bash scripts/verify-format.sh --verify
+```
+
+To apply fixes locally:
+
+```powershell
+./scripts/verify-format.ps1 -Fix
+```
+
+```bash
+bash scripts/verify-format.sh --fix
+```
+
+## Tests
+
+Please ensure tests pass before submitting a PR.
+
+```bash
+dotnet test ./test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj
+```
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..572bf01
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,15 @@
+
+
+ net10.0
+ latest
+ enable
+ enable
+ true
+ true
+ true
+ true
+ snupkg
+ true
+ true
+
+
diff --git a/README.md b/README.md
index 3c5ebc2..1af2ed3 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,27 @@
-
# Bitbucket.Net
[](https://www.nuget.org/packages/BitbucketServer.Net)
[](https://www.nuget.org/packages/BitbucketServer.Net)
[](https://github.com/diomonogatari/Bitbucket.Net/actions/workflows/ci.yml)
+[](https://codecov.io/gh/diomonogatari/Bitbucket.Net)
[](https://github.com/diomonogatari/Bitbucket.Net/blob/main/LICENSE)

-
+-orange.svg)
Modernized C# client for **Bitbucket Server** (Stash) REST API.
+## Contributing
+
+Development setup (including the pre-commit formatting hook) is documented in
+[CONTRIBUTING.md](CONTRIBUTING.md).
+
> **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 fork is **not production-ready** yet — it works well for the
-> author's own use case (an MCP Server for on-prem Bitbucket Server) but
-> not every endpoint has been fully tested.
+> 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.
### What changed from the original
@@ -35,7 +41,7 @@ If you're looking for Bitbucket Cloud API, try [this repository](https://github.
## Installation
```bash
-dotnet add package BitbucketServer.Net --prerelease
+dotnet add package BitbucketServer.Net
```
## Usage
@@ -112,6 +118,19 @@ await foreach (var pr in client.GetPullRequestsStreamAsync("PROJ", "repo", cance
{
await ProcessPullRequestAsync(pr);
}
+
+// Stream PR activities
+await foreach (var activity in client.GetPullRequestActivitiesStreamAsync(
+ "PROJ", "repo", pullRequestId: 42))
+{
+ ProcessActivity(activity);
+}
+
+// Stream dashboard PRs
+await foreach (var pr in client.GetDashboardPullRequestsStreamAsync())
+{
+ Console.WriteLine($"#{pr.Id}: {pr.Title}");
+}
```
### Exception Handling
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj b/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj
index aae63d2..267cf7f 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj
@@ -2,10 +2,6 @@
Exe
- net10.0
- enable
- latest
- enable
Release
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs b/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs
index ed0699c..cac839f 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs
@@ -72,20 +72,20 @@ public FullBenchmarkConfig()
{
AddJob(Job.Default);
AddDiagnoser(MemoryDiagnoser.Default);
-
+
AddColumn(StatisticColumn.Mean);
AddColumn(StatisticColumn.StdErr);
AddColumn(StatisticColumn.StdDev);
AddColumn(StatisticColumn.Median);
AddColumn(StatisticColumn.P95);
AddColumn(StatisticColumn.OperationsPerSecond);
-
+
AddExporter(MarkdownExporter.GitHub);
AddExporter(HtmlExporter.Default);
AddExporter(CsvExporter.Default);
-
+
AddLogger(ConsoleLogger.Default);
-
+
WithSummaryStyle(SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend));
}
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs
index 2483486..483cf4e 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs
@@ -66,4 +66,4 @@ private static void RunAllBenchmarks()
Console.WriteLine("All benchmarks completed!");
Console.WriteLine("Results are available in the BenchmarkDotNet.Artifacts folder.");
}
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs
index 9d0359e..e12f6d4 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs
@@ -1,6 +1,6 @@
-using System.Text;
using BenchmarkDotNet.Attributes;
using Bitbucket.Net.Benchmarks.Config;
+using System.Text;
namespace Bitbucket.Net.Benchmarks.Response;
@@ -57,7 +57,7 @@ public int ProcessLargeDiff()
[Benchmark(Description = "Buffered diff processing (1000 lines)")]
public List BufferedDiffProcessing()
{
- return _largeDiff.Split('\n').ToList();
+ return [.. _largeDiff.Split('\n')];
}
///
@@ -205,14 +205,14 @@ private static IEnumerable EnumerateLines(string content)
{
if (content[i] == '\n')
{
- yield return content.Substring(start, i - start);
+ yield return content[start..i];
start = i + 1;
}
}
if (start < content.Length)
{
- yield return content.Substring(start);
+ yield return content[start..];
}
}
@@ -282,4 +282,4 @@ private static string GenerateFileContent(int lines)
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs
index b68fe68..18a64af 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs
@@ -1,11 +1,11 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Metadata;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using Bitbucket.Net.Common.Models;
using Bitbucket.Net.Models.Core.Projects;
using Bitbucket.Net.Serialization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
namespace Bitbucket.Net.Benchmarks.Serialization;
@@ -164,4 +164,4 @@ private static PagedResults CreatePagedProjectsObject()
Start = 0
};
}
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs
index 66e37af..6259330 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs
@@ -1,9 +1,9 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
using BenchmarkDotNet.Attributes;
using Bitbucket.Net.Benchmarks.Config;
using Bitbucket.Net.Common.Models;
using Bitbucket.Net.Models.Core.Projects;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Bitbucket.Net.Benchmarks.Serialization;
@@ -307,4 +307,4 @@ private static string CreatePagedCommitsJson(int count)
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs
index bee70c1..7dd09ad 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs
@@ -1,12 +1,12 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Metadata;
using BenchmarkDotNet.Attributes;
using Bitbucket.Net.Benchmarks.Config;
using Bitbucket.Net.Common.Converters;
using Bitbucket.Net.Common.Models;
using Bitbucket.Net.Models.Core.Projects;
using Bitbucket.Net.Serialization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
namespace Bitbucket.Net.Benchmarks.Serialization;
@@ -605,4 +605,4 @@ private static string CreatePagedCommitsJson(int count)
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs
index dde68df..a0c86c3 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs
@@ -1,10 +1,10 @@
-using System.Runtime.CompilerServices;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using BenchmarkDotNet.Attributes;
using Bitbucket.Net.Benchmarks.Config;
using Bitbucket.Net.Common.Models;
using Bitbucket.Net.Models.Core.Projects;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Bitbucket.Net.Benchmarks.Streaming;
@@ -33,9 +33,7 @@ public class StreamingBenchmarks
[GlobalSetup]
public void Setup()
{
- _pagedResponses = Enumerable.Range(0, PageCount)
- .Select(pageIndex => CreatePagedRepositoriesJson(ItemsPerPage, pageIndex, pageIndex < PageCount - 1))
- .ToList();
+ _pagedResponses = [.. Enumerable.Range(0, PageCount).Select(pageIndex => CreatePagedRepositoriesJson(ItemsPerPage, pageIndex, pageIndex < PageCount - 1))];
}
///
@@ -129,7 +127,7 @@ public async Task> StreamingEarlyTermination()
public async Task> BufferedEarlyTermination()
{
var allResults = await BufferedApproach().ConfigureAwait(false);
- return allResults.Take(10).ToList();
+ return [.. allResults.Take(10)];
}
private async IAsyncEnumerable StreamItemsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -194,4 +192,4 @@ private static string CreatePagedRepositoriesJson(int count, int pageIndex, bool
}
""";
}
-}
+}
\ No newline at end of file
diff --git a/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs
index 667a704..81f3809 100644
--- a/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs
+++ b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs
@@ -1,9 +1,9 @@
+using BenchmarkDotNet.Attributes;
+using Bitbucket.Net.Benchmarks.Config;
using System.Buffers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-using BenchmarkDotNet.Attributes;
-using Bitbucket.Net.Benchmarks.Config;
namespace Bitbucket.Net.Benchmarks.ZeroCopy;
@@ -375,7 +375,7 @@ public sealed class BenchmarkPath
public string? Parent { get; set; }
public string? Name { get; set; }
public string? Extension { get; set; }
-
+
[JsonPropertyName("toString")]
public string? PathString { get; set; }
}
@@ -388,4 +388,4 @@ public sealed class BenchmarkDiffHunk
public int DestinationSpan { get; set; }
public List