Skip to content

Commit 2e1f0fb

Browse files
Move commit querying from Octokit to GraphQL (#79)
* Initial plan * Implement GraphQL-based commit querying to replace Octokit Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Fix code review issues: add refs/heads prefix and null checks Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Fix spell check errors in test file Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
1 parent 82f54bd commit 2e1f0fb

File tree

5 files changed

+522
-16
lines changed

5 files changed

+522
-16
lines changed

src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,90 @@ internal GitHubGraphQLClient(HttpClient httpClient, string? graphqlEndpoint = nu
8888
_ownsGraphQLClient = false;
8989
}
9090

91+
/// <summary>
92+
/// Gets all commits for a branch using GraphQL with pagination.
93+
/// </summary>
94+
/// <param name="owner">Repository owner.</param>
95+
/// <param name="repo">Repository name.</param>
96+
/// <param name="branch">Branch name (e.g., 'main'). Will be automatically converted to fully qualified ref name.</param>
97+
/// <returns>List of commit SHAs on the branch.</returns>
98+
public async Task<List<string>> GetCommitsAsync(
99+
string owner,
100+
string repo,
101+
string branch)
102+
{
103+
try
104+
{
105+
var allCommitShas = new List<string>();
106+
string? afterCursor = null;
107+
bool hasNextPage;
108+
109+
// Convert branch name to fully qualified ref name if needed
110+
var qualifiedBranch = branch.StartsWith("refs/") ? branch : $"refs/heads/{branch}";
111+
112+
// Paginate through all commits on the branch
113+
do
114+
{
115+
// Create GraphQL request to get commits for a branch with pagination support
116+
var request = new GraphQLRequest
117+
{
118+
Query = @"
119+
query($owner: String!, $repo: String!, $branch: String!, $after: String) {
120+
repository(owner: $owner, name: $repo) {
121+
ref(qualifiedName: $branch) {
122+
target {
123+
... on Commit {
124+
history(first: 100, after: $after) {
125+
nodes {
126+
oid
127+
}
128+
pageInfo {
129+
hasNextPage
130+
endCursor
131+
}
132+
}
133+
}
134+
}
135+
}
136+
}
137+
}",
138+
Variables = new
139+
{
140+
owner,
141+
repo,
142+
branch = qualifiedBranch,
143+
after = afterCursor
144+
}
145+
};
146+
147+
// Execute GraphQL query
148+
var response = await _graphqlClient.SendQueryAsync<GetCommitsResponse>(request);
149+
150+
// Extract commit SHAs from the GraphQL response, filtering out null or invalid values
151+
var pageCommitShas = response.Data?.Repository?.Ref?.Target?.History?.Nodes?
152+
.Where(n => !string.IsNullOrEmpty(n.Oid))
153+
.Select(n => n.Oid!)
154+
.ToList() ?? [];
155+
156+
allCommitShas.AddRange(pageCommitShas);
157+
158+
// Check if there are more pages
159+
var pageInfo = response.Data?.Repository?.Ref?.Target?.History?.PageInfo;
160+
hasNextPage = pageInfo?.HasNextPage ?? false;
161+
afterCursor = pageInfo?.EndCursor;
162+
}
163+
while (hasNextPage);
164+
165+
// Return list of all commit SHAs
166+
return allCommitShas;
167+
}
168+
catch
169+
{
170+
// If GraphQL query fails, return empty list
171+
return [];
172+
}
173+
}
174+
91175
/// <summary>
92176
/// Finds issue IDs linked to a pull request via closingIssuesReferences.
93177
/// </summary>

src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,47 @@ internal record IssueNode(
6565
internal record PageInfo(
6666
bool HasNextPage,
6767
string? EndCursor);
68+
69+
/// <summary>
70+
/// Response for getting commits from a repository.
71+
/// </summary>
72+
/// <param name="Repository">Repository data containing commit information.</param>
73+
internal record GetCommitsResponse(
74+
CommitRepositoryData? Repository);
75+
76+
/// <summary>
77+
/// Repository data containing ref information for commits.
78+
/// </summary>
79+
/// <param name="Ref">Git reference data.</param>
80+
internal record CommitRepositoryData(
81+
RefData? Ref);
82+
83+
/// <summary>
84+
/// Git reference data containing commit history.
85+
/// </summary>
86+
/// <param name="Target">Target commit information.</param>
87+
internal record RefData(
88+
TargetData? Target);
89+
90+
/// <summary>
91+
/// Target commit data containing history.
92+
/// </summary>
93+
/// <param name="History">Commit history with pagination.</param>
94+
internal record TargetData(
95+
CommitHistoryData? History);
96+
97+
/// <summary>
98+
/// Commit history data containing nodes and page info.
99+
/// </summary>
100+
/// <param name="Nodes">Commit nodes.</param>
101+
/// <param name="PageInfo">Pagination information.</param>
102+
internal record CommitHistoryData(
103+
List<CommitNode>? Nodes,
104+
PageInfo? PageInfo);
105+
106+
/// <summary>
107+
/// Commit node containing commit SHA.
108+
/// </summary>
109+
/// <param name="Oid">Git object ID (SHA).</param>
110+
internal record CommitNode(
111+
string? Oid);

src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ public override async Task<BuildInformation> GetBuildInformationAsync(Version? v
6767
Credentials = new Credentials(token)
6868
};
6969

70+
// Create GraphQL client
71+
using var graphqlClient = new GitHubGraphQLClient(token);
72+
7073
// Fetch all data from GitHub
71-
var gitHubData = await FetchGitHubDataAsync(client, owner, repo, branch.Trim());
74+
var gitHubData = await FetchGitHubDataAsync(client, graphqlClient, owner, repo, branch.Trim());
7275

7376
// Build lookup dictionaries and mappings
7477
var lookupData = BuildLookupData(gitHubData);
@@ -117,11 +120,17 @@ public override async Task<BuildInformation> GetBuildInformationAsync(Version? v
117120
changelogLink);
118121
}
119122

123+
/// <summary>
124+
/// Simple commit representation containing only the SHA hash.
125+
/// </summary>
126+
internal sealed record Commit(
127+
string Sha);
128+
120129
/// <summary>
121130
/// Container for GitHub data fetched from the API.
122131
/// </summary>
123132
internal sealed record GitHubData(
124-
IReadOnlyList<GitHubCommit> Commits,
133+
IReadOnlyList<Commit> Commits,
125134
IReadOnlyList<Release> Releases,
126135
IReadOnlyList<RepositoryTag> Tags,
127136
IReadOnlyList<PullRequest> PullRequests,
@@ -143,14 +152,20 @@ internal sealed record LookupData(
143152
/// Fetches all required data from GitHub API in parallel.
144153
/// </summary>
145154
/// <param name="client">GitHub client.</param>
155+
/// <param name="graphqlClient">GitHub GraphQL client.</param>
146156
/// <param name="owner">Repository owner.</param>
147157
/// <param name="repo">Repository name.</param>
148158
/// <param name="branch">Branch name.</param>
149159
/// <returns>Container with all fetched GitHub data.</returns>
150-
private static async Task<GitHubData> FetchGitHubDataAsync(GitHubClient client, string owner, string repo, string branch)
160+
private static async Task<GitHubData> FetchGitHubDataAsync(
161+
GitHubClient client,
162+
GitHubGraphQLClient graphqlClient,
163+
string owner,
164+
string repo,
165+
string branch)
151166
{
152167
// Fetch all data from GitHub in parallel
153-
var commitsTask = GetAllCommitsAsync(client, owner, repo, branch);
168+
var commitsTask = GetAllCommitsAsync(graphqlClient, owner, repo, branch);
154169
var releasesTask = client.Repository.Release.GetAll(owner, repo);
155170
var tagsTask = client.Repository.GetAllTags(owner, repo);
156171
var pullRequestsTask = client.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest { State = ItemStateFilter.All });
@@ -401,7 +416,7 @@ private static int DetermineSearchStartIndex(int toIndex, int releaseCount)
401416
/// <returns>Tuple of (bugs, nonBugChanges, allChangeIds).</returns>
402417
private static async Task<(List<ItemInfo> bugs, List<ItemInfo> nonBugChanges, HashSet<string> allChangeIds)>
403418
CollectChangesFromPullRequestsAsync(
404-
List<GitHubCommit> commitsInRange,
419+
List<Commit> commitsInRange,
405420
LookupData lookupData,
406421
string owner,
407422
string repo,
@@ -537,20 +552,24 @@ private static List<ItemInfo> CollectKnownIssues(IReadOnlyList<Issue> issues, Ha
537552
}
538553

539554
/// <summary>
540-
/// Gets all commits for a branch using pagination.
555+
/// Gets all commits for a branch using GraphQL pagination.
541556
/// </summary>
542-
/// <param name="client">GitHub client.</param>
557+
/// <param name="graphqlClient">GitHub GraphQL client.</param>
543558
/// <param name="owner">Repository owner.</param>
544559
/// <param name="repo">Repository name.</param>
545560
/// <param name="branch">Branch name.</param>
546561
/// <returns>List of all commits.</returns>
547-
private static async Task<IReadOnlyList<GitHubCommit>> GetAllCommitsAsync(GitHubClient client, string owner, string repo, string branch)
562+
private static async Task<IReadOnlyList<Commit>> GetAllCommitsAsync(
563+
GitHubGraphQLClient graphqlClient,
564+
string owner,
565+
string repo,
566+
string branch)
548567
{
549-
// Create request for branch commits
550-
var request = new CommitRequest { Sha = branch };
568+
// Fetch all commit SHAs for the branch using GraphQL
569+
var commitShas = await graphqlClient.GetCommitsAsync(owner, repo, branch);
551570

552-
// Fetch and return all commits for the branch
553-
return await client.Repository.Commit.GetAll(owner, repo, request);
571+
// Convert SHAs to Commit objects and return
572+
return commitShas.Select(sha => new Commit(sha)).ToList();
554573
}
555574

556575
/// <summary>
@@ -560,10 +579,10 @@ private static async Task<IReadOnlyList<GitHubCommit>> GetAllCommitsAsync(GitHub
560579
/// <param name="fromHash">Starting commit hash (exclusive - not included in results; null for start of history).</param>
561580
/// <param name="toHash">Ending commit hash (inclusive - included in results).</param>
562581
/// <returns>List of commits in range, excluding fromHash but including toHash.</returns>
563-
internal static List<GitHubCommit> GetCommitsInRange(IReadOnlyList<GitHubCommit> commits, string? fromHash, string toHash)
582+
internal static List<Commit> GetCommitsInRange(IReadOnlyList<Commit> commits, string? fromHash, string toHash)
564583
{
565584
// Initialize collection and state tracking
566-
var result = new List<GitHubCommit>();
585+
var result = new List<Commit>();
567586
var foundTo = false;
568587

569588
// Iterate through commits from newest to oldest

test/DemaConsulting.BuildMark.Tests/GitHubRepoConnectorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public void GitHubRepoConnector_GetTypeFromLabels_EmptyLabels_ReturnsOther()
273273
public void GitHubRepoConnector_GetCommitsInRange_ToHashNotFound_ReturnsEmptyList()
274274
{
275275
// Arrange - empty list of commits
276-
var commits = new List<GitHubCommit>();
276+
var commits = new List<GitHubRepoConnector.Commit>();
277277

278278
// Act
279279
var result = GitHubRepoConnector.GetCommitsInRange(commits, "hash1", "hash4");

0 commit comments

Comments
 (0)