Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/Bitbucket.Net/Common/Models/Search/CodeSearchRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Bitbucket.Net.Common.Models.Search;

/// <summary>
/// Request body for the Bitbucket Server code search API.
/// POST /rest/search/latest/search
/// </summary>
public class CodeSearchRequest
{
/// <summary>
/// The search query string. Supports Bitbucket search syntax:
/// repo:slug, project:KEY, lang:, ext:, path:
/// </summary>
public required string Query { get; set; }

/// <summary>
/// Entity types to search. Use <see cref="SearchEntities.CodeOnly"/> for code search.
/// </summary>
public required SearchEntities Entities { get; set; }

/// <summary>
/// Pagination limits for the search results.
/// </summary>
public required SearchLimits Limits { get; set; }
}

/// <summary>
/// Specifies which entity types to search for.
/// </summary>
public class SearchEntities
{
/// <summary>
/// Include code results. Set to an empty object to enable code search.
/// </summary>
public SearchEntityFilter? Code { get; set; }

/// <summary>
/// Creates entities for a code-only search.
/// </summary>
public static SearchEntities CodeOnly => new() { Code = new SearchEntityFilter() };
}

/// <summary>
/// Marker class representing an entity filter in search requests.
/// Serializes to an empty JSON object <c>{}</c>.
/// </summary>
public class SearchEntityFilter { }

/// <summary>
/// Pagination limits for search results.
/// </summary>
public class SearchLimits
{
/// <summary>
/// Maximum number of primary results to return. Default: 25.
/// </summary>
public int Primary { get; set; } = 25;

/// <summary>
/// Maximum number of secondary results per primary result. Default: 10.
/// </summary>
public int Secondary { get; set; } = 10;
}
146 changes: 146 additions & 0 deletions src/Bitbucket.Net/Common/Models/Search/CodeSearchResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using Bitbucket.Net.Models.Core.Projects;

namespace Bitbucket.Net.Common.Models.Search;

/// <summary>
/// Top-level response from the Bitbucket Server code search API.
/// </summary>
public class CodeSearchResponse
{
/// <summary>
/// The scope of the search (e.g., GLOBAL).
/// </summary>
public SearchScope? Scope { get; set; }

/// <summary>
/// Code search results.
/// </summary>
public CodeSearchCategory? Code { get; set; }

/// <summary>
/// Query metadata including whether query substitution occurred.
/// </summary>
public SearchQuery? Query { get; set; }
}

/// <summary>
/// Scope metadata for a search.
/// </summary>
public class SearchScope
{
/// <summary>
/// The scope type, e.g., "GLOBAL".
/// </summary>
public string? Type { get; set; }
}

/// <summary>
/// Query metadata.
/// </summary>
public class SearchQuery
{
/// <summary>
/// Whether the query was substituted (e.g., spell correction).
/// </summary>
public bool Substituted { get; set; }
}

/// <summary>
/// Category of code search results with pagination info.
/// </summary>
public class CodeSearchCategory
{
/// <summary>
/// Result category name (e.g., "primary").
/// </summary>
public string? Category { get; set; }

/// <summary>
/// Whether this is the last page of results.
/// </summary>
public bool IsLastPage { get; set; }

/// <summary>
/// Total number of results matching the query.
/// </summary>
public int Count { get; set; }

/// <summary>
/// Starting index of the current page.
/// </summary>
public int Start { get; set; }

/// <summary>
/// Starting index for the next page, if available.
/// </summary>
public int? NextStart { get; set; }

/// <summary>
/// The code search result items.
/// </summary>
public List<CodeSearchResult> Values { get; set; } = [];
}

/// <summary>
/// A single code search result representing a file with matching content.
/// </summary>
public class CodeSearchResult
{
/// <summary>
/// The repository containing the matching file.
/// </summary>
public Repository? Repository { get; set; }

/// <summary>
/// The file path within the repository.
/// </summary>
public string? File { get; set; }

/// <summary>
/// Groups of matching lines with surrounding context.
/// Each inner list represents a contiguous block of context lines.
/// </summary>
public List<List<CodeSearchHitLine>>? HitContexts { get; set; }

/// <summary>
/// Segments of the file path that matched the query.
/// </summary>
public List<SearchPathMatch>? PathMatches { get; set; }

/// <summary>
/// Total number of hits in this file.
/// </summary>
public int HitCount { get; set; }
}

/// <summary>
/// A single line in a code search hit context block.
/// </summary>
public class CodeSearchHitLine
{
/// <summary>
/// The 1-based line number.
/// </summary>
public int Line { get; set; }

/// <summary>
/// The line text content. May contain &lt;em&gt; tags highlighting matched terms.
/// </summary>
public string? Text { get; set; }
}

/// <summary>
/// Represents a matching segment in the file path.
/// </summary>
public class SearchPathMatch
{
/// <summary>
/// Starting character index of the match in the path.
/// </summary>
public int Start { get; set; }

/// <summary>
/// Length of the matching text.
/// </summary>
public int Length { get; set; }
}
92 changes: 92 additions & 0 deletions src/Bitbucket.Net/Core/Search/BitbucketClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Bitbucket.Net.Common.Models.Search;
using Flurl;
using Flurl.Http;

namespace Bitbucket.Net;

/// <summary>
/// Provides operations for the Bitbucket Server code search API (Elasticsearch-backed).
/// Requires the Bitbucket Code Search add-on to be installed on the server.
/// </summary>
public partial class BitbucketClient
{
private IFlurlRequest GetSearchUrl() => GetBaseUrl("/search", "latest");

/// <summary>
/// Performs a server-side code search using the Bitbucket Code Search API.
/// This is backed by Elasticsearch and significantly faster than client-side file scanning.
/// </summary>
/// <param name="query">
/// The search query string. Supports Bitbucket search syntax:
/// <list type="bullet">
/// <item><c>repo:slug</c> — filter to a specific repository</item>
/// <item><c>project:KEY</c> — filter to a specific project</item>
/// <item><c>lang:csharp</c> — filter by language</item>
/// <item><c>ext:cs</c> — filter by file extension</item>
/// <item><c>path:src/</c> — filter by file path</item>
/// </list>
/// </param>
/// <param name="primaryLimit">Maximum number of file results to return. Default: 25.</param>
/// <param name="secondaryLimit">Maximum number of hit contexts per file. Default: 10.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The code search response containing matching files and hit contexts.</returns>
/// <exception cref="Common.Exceptions.BitbucketApiException">
/// Thrown when the server returns an error (e.g., 404 if Code Search is not installed).
/// </exception>
public async Task<CodeSearchResponse> SearchCodeAsync(
string query,
int primaryLimit = 25,
int secondaryLimit = 10,
CancellationToken cancellationToken = default)
{
var request = new CodeSearchRequest
{
Query = query,
Entities = SearchEntities.CodeOnly,
Limits = new SearchLimits
{
Primary = primaryLimit,
Secondary = secondaryLimit
}
};

var response = await GetSearchUrl()
.AppendPathSegment("/search")
.SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken)
.ConfigureAwait(false);

return await HandleResponseAsync<CodeSearchResponse>(response, cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Checks whether the Bitbucket Code Search API is available on the server.
/// Returns true if the search endpoint responds successfully, false otherwise.
/// </summary>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>True if server-side search is available; false otherwise.</returns>
public async Task<bool> IsSearchAvailableAsync(CancellationToken cancellationToken = default)
{
try
{
// Perform a minimal search to probe the endpoint
var request = new CodeSearchRequest
{
Query = "test",
Entities = SearchEntities.CodeOnly,
Limits = new SearchLimits { Primary = 1, Secondary = 1 }
};

var response = await GetSearchUrl()
.AppendPathSegment("/search")
.SendAsync(HttpMethod.Post, CreateJsonContent(request), cancellationToken: cancellationToken)
.ConfigureAwait(false);

return response.StatusCode < 400;
}
catch
{
return false;
}
}
}
21 changes: 21 additions & 0 deletions src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Bitbucket.Net.Common.Models;
// Search
using Bitbucket.Net.Common.Models.Search;
// Audit
using Bitbucket.Net.Models.Audit;
// Branches
Expand Down Expand Up @@ -97,6 +99,25 @@ namespace Bitbucket.Net.Serialization;
[JsonSerializable(typeof(PagedResults<UserPermission>))]
[JsonSerializable(typeof(PagedResults<WebHook>))]

// ============================================================================
// Search Models
// ============================================================================
[JsonSerializable(typeof(CodeSearchRequest))]
[JsonSerializable(typeof(CodeSearchResponse))]
[JsonSerializable(typeof(CodeSearchCategory))]
[JsonSerializable(typeof(CodeSearchResult))]
[JsonSerializable(typeof(CodeSearchHitLine))]
[JsonSerializable(typeof(SearchEntities))]
[JsonSerializable(typeof(SearchEntityFilter))]
[JsonSerializable(typeof(SearchLimits))]
[JsonSerializable(typeof(SearchPathMatch))]
[JsonSerializable(typeof(SearchQuery))]
[JsonSerializable(typeof(SearchScope))]
[JsonSerializable(typeof(List<CodeSearchResult>))]
[JsonSerializable(typeof(List<List<CodeSearchHitLine>>))]
[JsonSerializable(typeof(List<CodeSearchHitLine>))]
[JsonSerializable(typeof(List<SearchPathMatch>))]

// ============================================================================
// Audit Models
// ============================================================================
Expand Down
15 changes: 15 additions & 0 deletions test/Bitbucket.Net.Tests/Fixtures/Search/code-search-empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"scope": {
"type": "GLOBAL"
},
"code": {
"category": "primary",
"isLastPage": true,
"count": 0,
"start": 0,
"values": []
},
"query": {
"substituted": false
}
}
Loading