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
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,89 @@ public async Task<List<TagNode>> GetAllTagsAsync(
}
}

/// <summary>
/// Gets all pull requests for a repository using GraphQL with pagination.
/// </summary>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <returns>List of pull request nodes.</returns>
public async Task<List<PullRequestNode>> GetPullRequestsAsync(
string owner,
string repo)
{
try
{
var allPullRequestNodes = new List<PullRequestNode>();
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<GetPullRequestsResponse>(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 [];
}
}

/// <summary>
/// Disposes the GraphQL client if owned by this instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,66 @@ internal record TagNode(
/// <param name="Oid">Git object ID (SHA) of the commit.</param>
internal record TagTargetData(
string? Oid);

/// <summary>
/// Response for getting pull requests from a repository.
/// </summary>
/// <param name="Repository">Repository data containing pull requests information.</param>
internal record GetPullRequestsResponse(
PullRequestRepositoryData? Repository);

/// <summary>
/// Repository data containing pull requests information.
/// </summary>
/// <param name="PullRequests">Pull requests connection data.</param>
internal record PullRequestRepositoryData(
PullRequestsConnectionData? PullRequests);

/// <summary>
/// Pull requests connection data containing nodes and page info.
/// </summary>
/// <param name="Nodes">Pull request nodes.</param>
/// <param name="PageInfo">Pagination information.</param>
internal record PullRequestsConnectionData(
List<PullRequestNode>? Nodes,
PageInfo? PageInfo);

/// <summary>
/// Pull request node containing pull request information.
/// </summary>
/// <param name="Number">Pull request number.</param>
/// <param name="Title">Pull request title.</param>
/// <param name="Url">Pull request HTML URL.</param>
/// <param name="Merged">Whether the pull request is merged.</param>
/// <param name="MergeCommit">Merge commit information.</param>
/// <param name="HeadRefOid">Head reference commit SHA.</param>
/// <param name="Labels">Labels assigned to the pull request.</param>
internal record PullRequestNode(
int? Number,
string? Title,
string? Url,
bool Merged,
PullRequestMergeCommit? MergeCommit,
string? HeadRefOid,
PullRequestLabelsConnection? Labels);

/// <summary>
/// Pull request merge commit information.
/// </summary>
/// <param name="Oid">Commit SHA of the merge commit.</param>
internal record PullRequestMergeCommit(
string? Oid);

/// <summary>
/// Pull request labels connection containing nodes.
/// </summary>
/// <param name="Nodes">Label nodes.</param>
internal record PullRequestLabelsConnection(
List<PullRequestLabel>? Nodes);

/// <summary>
/// Pull request label information.
/// </summary>
/// <param name="Name">Label name.</param>
internal record PullRequestLabel(
string? Name);
85 changes: 77 additions & 8 deletions src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,40 @@ internal sealed record Tag(
internal sealed record TagCommit(
string Sha);

/// <summary>
/// Pull request information.
/// </summary>
internal sealed record PullRequestInfo(
int Number,
string Title,
string HtmlUrl,
bool Merged,
string? MergeCommitSha,
string? HeadSha,
IReadOnlyList<PullRequestLabelInfo> Labels);

/// <summary>
/// Pull request label information.
/// </summary>
internal sealed record PullRequestLabelInfo(
string Name);

/// <summary>
/// Container for GitHub data fetched from the API.
/// </summary>
internal sealed record GitHubData(
IReadOnlyList<Commit> Commits,
IReadOnlyList<ReleaseNode> Releases,
IReadOnlyList<Tag> Tags,
IReadOnlyList<PullRequest> PullRequests,
IReadOnlyList<PullRequestInfo> PullRequests,
IReadOnlyList<Issue> Issues);

/// <summary>
/// Container for lookup data structures built from GitHub data.
/// </summary>
internal sealed record LookupData(
Dictionary<int, Issue> IssueById,
Dictionary<string, PullRequest> CommitHashToPr,
Dictionary<string, PullRequestInfo> CommitHashToPr,
List<ReleaseNode> BranchReleases,
Dictionary<string, Tag> TagsByName,
Dictionary<string, ReleaseNode> TagToRelease,
Expand All @@ -181,7 +199,7 @@ private static async Task<GitHubData> 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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -480,7 +498,7 @@ private static int DetermineSearchStartIndex(int toIndex, int releaseCount)
private static void ProcessLinkedIssues(
IReadOnlyList<int> linkedIssueIds,
Dictionary<int, Issue> issueById,
PullRequest pr,
PullRequestInfo pr,
HashSet<string> allChangeIds,
List<ItemInfo> bugs,
List<ItemInfo> nonBugChanges)
Expand Down Expand Up @@ -523,7 +541,7 @@ private static void ProcessLinkedIssues(
/// <param name="bugs">List of bug changes (modified in place).</param>
/// <param name="nonBugChanges">List of non-bug changes (modified in place).</param>
private static void ProcessPullRequestWithoutIssues(
PullRequest pr,
PullRequestInfo pr,
HashSet<string> allChangeIds,
List<ItemInfo> bugs,
List<ItemInfo> nonBugChanges)
Expand Down Expand Up @@ -623,6 +641,38 @@ private static async Task<IReadOnlyList<Tag>> GetAllTagsAsync(
.ToList();
}

/// <summary>
/// Gets all pull requests for a repository using GraphQL pagination.
/// </summary>
/// <param name="graphqlClient">GitHub GraphQL client.</param>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <returns>List of all pull requests.</returns>
private static async Task<IReadOnlyList<PullRequestInfo>> 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();
}

/// <summary>
/// Gets commits in the range from fromHash (exclusive) to toHash (inclusive).
/// </summary>
Expand Down Expand Up @@ -687,7 +737,7 @@ internal static ItemInfo CreateItemInfoFromIssue(Issue issue, int index)
/// </summary>
/// <param name="pr">GitHub pull request.</param>
/// <returns>ItemInfo instance.</returns>
internal static ItemInfo CreateItemInfoFromPullRequest(PullRequest pr)
internal static ItemInfo CreateItemInfoFromPullRequest(PullRequestInfo pr)
{
// Determine item type from PR labels
var type = GetTypeFromLabels(pr.Labels);
Expand Down Expand Up @@ -720,6 +770,25 @@ internal static string GetTypeFromLabels(IReadOnlyList<Label> labels)
return matchingType ?? "other";
}

/// <summary>
/// Determines item type from pull request labels.
/// </summary>
/// <param name="labels">List of pull request labels.</param>
/// <returns>Item type string.</returns>
internal static string GetTypeFromLabels(IReadOnlyList<PullRequestLabelInfo> labels)
{
// Find first matching label type by checking label names against the type map
var matchingType = labels
.Select(label => label.Name.ToLowerInvariant())
.SelectMany(lowerLabel => LabelTypeMap
.Where(kvp => lowerLabel.Contains(kvp.Key))
.Select(kvp => kvp.Value))
.FirstOrDefault();

// Return matched type or default to "other"
return matchingType ?? "other";
}

/// <summary>
/// Gets GitHub token from environment or gh CLI.
/// </summary>
Expand Down
Loading