From 85bd25c67e62291bb6908e7e3093b18536feba6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:20:49 +0000 Subject: [PATCH 1/2] Initial plan From 4e297156cff46cd84f63aad8403e59a4d65c879f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:30:03 +0000 Subject: [PATCH 2/2] Add GraphQL support for fetching pull requests - Add PullRequestNode and related GraphQL types - Implement GetPullRequestsAsync in GitHubGraphQLClient - Create comprehensive tests for GetPullRequestsAsync - Add PullRequestInfo internal type to replace Octokit.PullRequest - Update GitHubRepoConnector to use GraphQL for pull requests - Replace Octokit PullRequest.GetAllForRepository with GraphQL Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- .../GitHub/GitHubGraphQLClient.cs | 83 +++ .../GitHub/GitHubGraphQLTypes.cs | 63 ++ .../RepoConnectors/GitHubRepoConnector.cs | 85 ++- ...GitHubGraphQLClientGetPullRequestsTests.cs | 546 ++++++++++++++++++ 4 files changed, 769 insertions(+), 8 deletions(-) create mode 100644 test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs index be75d4e..1f45ff1 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs @@ -394,6 +394,89 @@ public async Task> GetAllTagsAsync( } } + /// + /// Gets all pull requests for a repository using GraphQL with pagination. + /// + /// Repository owner. + /// Repository name. + /// List of pull request nodes. + public async Task> GetPullRequestsAsync( + string owner, + string repo) + { + try + { + var allPullRequestNodes = new List(); + string? afterCursor = null; + bool hasNextPage; + + // Paginate through all pull requests + do + { + // Create GraphQL request to get pull requests for a repository with pagination support + var request = new GraphQLRequest + { + Query = @" + query($owner: String!, $repo: String!, $after: String) { + repository(owner: $owner, name: $repo) { + pullRequests(first: 100, after: $after) { + nodes { + number + title + url + merged + mergeCommit { + oid + } + headRefOid + labels(first: 100) { + nodes { + name + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + }", + Variables = new + { + owner, + repo, + after = afterCursor + } + }; + + // Execute GraphQL query + var response = await _graphqlClient.SendQueryAsync(request); + + // Extract pull request nodes from the GraphQL response, filtering out null or invalid values + var pagePullRequestNodes = response.Data?.Repository?.PullRequests?.Nodes? + .Where(n => n.Number.HasValue && !string.IsNullOrEmpty(n.Title)) + .ToList() ?? []; + + allPullRequestNodes.AddRange(pagePullRequestNodes); + + // Check if there are more pages + var pageInfo = response.Data?.Repository?.PullRequests?.PageInfo; + hasNextPage = pageInfo?.HasNextPage ?? false; + afterCursor = pageInfo?.EndCursor; + } + while (hasNextPage); + + // Return list of all pull request nodes + return allPullRequestNodes; + } + catch + { + // If GraphQL query fails, return empty list + return []; + } + } + /// /// Disposes the GraphQL client if owned by this instance. /// diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs index e7c5d24..3db903d 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs @@ -178,3 +178,66 @@ internal record TagNode( /// Git object ID (SHA) of the commit. internal record TagTargetData( string? Oid); + +/// +/// Response for getting pull requests from a repository. +/// +/// Repository data containing pull requests information. +internal record GetPullRequestsResponse( + PullRequestRepositoryData? Repository); + +/// +/// Repository data containing pull requests information. +/// +/// Pull requests connection data. +internal record PullRequestRepositoryData( + PullRequestsConnectionData? PullRequests); + +/// +/// Pull requests connection data containing nodes and page info. +/// +/// Pull request nodes. +/// Pagination information. +internal record PullRequestsConnectionData( + List? Nodes, + PageInfo? PageInfo); + +/// +/// Pull request node containing pull request information. +/// +/// Pull request number. +/// Pull request title. +/// Pull request HTML URL. +/// Whether the pull request is merged. +/// Merge commit information. +/// Head reference commit SHA. +/// Labels assigned to the pull request. +internal record PullRequestNode( + int? Number, + string? Title, + string? Url, + bool Merged, + PullRequestMergeCommit? MergeCommit, + string? HeadRefOid, + PullRequestLabelsConnection? Labels); + +/// +/// Pull request merge commit information. +/// +/// Commit SHA of the merge commit. +internal record PullRequestMergeCommit( + string? Oid); + +/// +/// Pull request labels connection containing nodes. +/// +/// Label nodes. +internal record PullRequestLabelsConnection( + List? Nodes); + +/// +/// Pull request label information. +/// +/// Label name. +internal record PullRequestLabel( + string? Name); diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs index 8c481f6..a7eeb80 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs @@ -139,6 +139,24 @@ internal sealed record Tag( internal sealed record TagCommit( string Sha); + /// + /// Pull request information. + /// + internal sealed record PullRequestInfo( + int Number, + string Title, + string HtmlUrl, + bool Merged, + string? MergeCommitSha, + string? HeadSha, + IReadOnlyList Labels); + + /// + /// Pull request label information. + /// + internal sealed record PullRequestLabelInfo( + string Name); + /// /// Container for GitHub data fetched from the API. /// @@ -146,7 +164,7 @@ internal sealed record GitHubData( IReadOnlyList Commits, IReadOnlyList Releases, IReadOnlyList Tags, - IReadOnlyList PullRequests, + IReadOnlyList PullRequests, IReadOnlyList Issues); /// @@ -154,7 +172,7 @@ internal sealed record GitHubData( /// internal sealed record LookupData( Dictionary IssueById, - Dictionary CommitHashToPr, + Dictionary CommitHashToPr, List BranchReleases, Dictionary TagsByName, Dictionary TagToRelease, @@ -181,7 +199,7 @@ private static async Task FetchGitHubDataAsync( var commitsTask = GetAllCommitsAsync(graphqlClient, owner, repo, branch); var releasesTask = GetAllReleasesAsync(graphqlClient, owner, repo); var tagsTask = GetAllTagsAsync(graphqlClient, owner, repo); - var pullRequestsTask = client.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest { State = ItemStateFilter.All }); + var pullRequestsTask = GetAllPullRequestsAsync(graphqlClient, owner, repo); var issuesTask = client.Issue.GetAllForRepository(owner, repo, new RepositoryIssueRequest { State = ItemStateFilter.All }); // Wait for all parallel fetches to complete @@ -211,8 +229,8 @@ internal static LookupData BuildLookupData(GitHubData data) // This is used to associate commits with their pull requests for change tracking. // For merged PRs, use MergeCommitSha; for open PRs, use head SHA. var commitHashToPr = data.PullRequests - .Where(p => (p.Merged && p.MergeCommitSha != null) || (!p.Merged && p.Head?.Sha != null)) - .ToDictionary(p => p.Merged ? p.MergeCommitSha! : p.Head.Sha, p => p); + .Where(p => (p.Merged && p.MergeCommitSha != null) || (!p.Merged && p.HeadSha != null)) + .ToDictionary(p => p.Merged ? p.MergeCommitSha! : p.HeadSha!, p => p); // Build a set of commit SHAs in the current branch. // This is used for efficient filtering of branch-related tags. @@ -480,7 +498,7 @@ private static int DetermineSearchStartIndex(int toIndex, int releaseCount) private static void ProcessLinkedIssues( IReadOnlyList linkedIssueIds, Dictionary issueById, - PullRequest pr, + PullRequestInfo pr, HashSet allChangeIds, List bugs, List nonBugChanges) @@ -523,7 +541,7 @@ private static void ProcessLinkedIssues( /// List of bug changes (modified in place). /// List of non-bug changes (modified in place). private static void ProcessPullRequestWithoutIssues( - PullRequest pr, + PullRequestInfo pr, HashSet allChangeIds, List bugs, List nonBugChanges) @@ -623,6 +641,38 @@ private static async Task> GetAllTagsAsync( .ToList(); } + /// + /// Gets all pull requests for a repository using GraphQL pagination. + /// + /// GitHub GraphQL client. + /// Repository owner. + /// Repository name. + /// List of all pull requests. + private static async Task> GetAllPullRequestsAsync( + GitHubGraphQLClient graphqlClient, + string owner, + string repo) + { + // Fetch all pull requests for the repository using GraphQL + var prNodes = await graphqlClient.GetPullRequestsAsync(owner, repo); + + // Convert PullRequestNode objects to PullRequestInfo objects + return prNodes + .Where(pr => pr.Number.HasValue && !string.IsNullOrEmpty(pr.Title)) + .Select(pr => new PullRequestInfo( + pr.Number!.Value, + pr.Title!, + pr.Url ?? string.Empty, + pr.Merged, + pr.MergeCommit?.Oid, + pr.HeadRefOid, + pr.Labels?.Nodes? + .Where(l => !string.IsNullOrEmpty(l.Name)) + .Select(l => new PullRequestLabelInfo(l.Name!)) + .ToList() ?? [])) + .ToList(); + } + /// /// Gets commits in the range from fromHash (exclusive) to toHash (inclusive). /// @@ -687,7 +737,7 @@ internal static ItemInfo CreateItemInfoFromIssue(Issue issue, int index) /// /// GitHub pull request. /// ItemInfo instance. - internal static ItemInfo CreateItemInfoFromPullRequest(PullRequest pr) + internal static ItemInfo CreateItemInfoFromPullRequest(PullRequestInfo pr) { // Determine item type from PR labels var type = GetTypeFromLabels(pr.Labels); @@ -720,6 +770,25 @@ internal static string GetTypeFromLabels(IReadOnlyList