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 @@ -88,6 +88,90 @@ internal GitHubGraphQLClient(HttpClient httpClient, string? graphqlEndpoint = nu
_ownsGraphQLClient = false;
}

/// <summary>
/// Gets all commits for a branch using GraphQL with pagination.
/// </summary>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <param name="branch">Branch name (e.g., 'main'). Will be automatically converted to fully qualified ref name.</param>
/// <returns>List of commit SHAs on the branch.</returns>
public async Task<List<string>> GetCommitsAsync(
string owner,
string repo,
string branch)
{
try
{
var allCommitShas = new List<string>();
string? afterCursor = null;
bool hasNextPage;

// Convert branch name to fully qualified ref name if needed
var qualifiedBranch = branch.StartsWith("refs/") ? branch : $"refs/heads/{branch}";

// Paginate through all commits on the branch
do
{
// Create GraphQL request to get commits for a branch with pagination support
var request = new GraphQLRequest
{
Query = @"
query($owner: String!, $repo: String!, $branch: String!, $after: String) {
repository(owner: $owner, name: $repo) {
ref(qualifiedName: $branch) {
target {
... on Commit {
history(first: 100, after: $after) {
nodes {
oid
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
}
}",
Variables = new
{
owner,
repo,
branch = qualifiedBranch,
after = afterCursor
}
};

// Execute GraphQL query
var response = await _graphqlClient.SendQueryAsync<GetCommitsResponse>(request);

// Extract commit SHAs from the GraphQL response, filtering out null or invalid values
var pageCommitShas = response.Data?.Repository?.Ref?.Target?.History?.Nodes?
.Where(n => !string.IsNullOrEmpty(n.Oid))
.Select(n => n.Oid!)
.ToList() ?? [];

allCommitShas.AddRange(pageCommitShas);

// Check if there are more pages
var pageInfo = response.Data?.Repository?.Ref?.Target?.History?.PageInfo;
hasNextPage = pageInfo?.HasNextPage ?? false;
afterCursor = pageInfo?.EndCursor;
}
while (hasNextPage);

// Return list of all commit SHAs
return allCommitShas;
}
catch
{
// If GraphQL query fails, return empty list
return [];
}
}

/// <summary>
/// Finds issue IDs linked to a pull request via closingIssuesReferences.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,47 @@ internal record IssueNode(
internal record PageInfo(
bool HasNextPage,
string? EndCursor);

/// <summary>
/// Response for getting commits from a repository.
/// </summary>
/// <param name="Repository">Repository data containing commit information.</param>
internal record GetCommitsResponse(
CommitRepositoryData? Repository);

/// <summary>
/// Repository data containing ref information for commits.
/// </summary>
/// <param name="Ref">Git reference data.</param>
internal record CommitRepositoryData(
RefData? Ref);

/// <summary>
/// Git reference data containing commit history.
/// </summary>
/// <param name="Target">Target commit information.</param>
internal record RefData(
TargetData? Target);

/// <summary>
/// Target commit data containing history.
/// </summary>
/// <param name="History">Commit history with pagination.</param>
internal record TargetData(
CommitHistoryData? History);

/// <summary>
/// Commit history data containing nodes and page info.
/// </summary>
/// <param name="Nodes">Commit nodes.</param>
/// <param name="PageInfo">Pagination information.</param>
internal record CommitHistoryData(
List<CommitNode>? Nodes,
PageInfo? PageInfo);

/// <summary>
/// Commit node containing commit SHA.
/// </summary>
/// <param name="Oid">Git object ID (SHA).</param>
internal record CommitNode(
string? Oid);
47 changes: 33 additions & 14 deletions src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ public override async Task<BuildInformation> GetBuildInformationAsync(Version? v
Credentials = new Credentials(token)
};

// Create GraphQL client
using var graphqlClient = new GitHubGraphQLClient(token);

// Fetch all data from GitHub
var gitHubData = await FetchGitHubDataAsync(client, owner, repo, branch.Trim());
var gitHubData = await FetchGitHubDataAsync(client, graphqlClient, owner, repo, branch.Trim());

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

/// <summary>
/// Simple commit representation containing only the SHA hash.
/// </summary>
internal sealed record Commit(
string Sha);

/// <summary>
/// Container for GitHub data fetched from the API.
/// </summary>
internal sealed record GitHubData(
IReadOnlyList<GitHubCommit> Commits,
IReadOnlyList<Commit> Commits,
IReadOnlyList<Release> Releases,
IReadOnlyList<RepositoryTag> Tags,
IReadOnlyList<PullRequest> PullRequests,
Expand All @@ -143,14 +152,20 @@ internal sealed record LookupData(
/// Fetches all required data from GitHub API in parallel.
/// </summary>
/// <param name="client">GitHub client.</param>
/// <param name="graphqlClient">GitHub GraphQL client.</param>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <param name="branch">Branch name.</param>
/// <returns>Container with all fetched GitHub data.</returns>
private static async Task<GitHubData> FetchGitHubDataAsync(GitHubClient client, string owner, string repo, string branch)
private static async Task<GitHubData> FetchGitHubDataAsync(
GitHubClient client,
GitHubGraphQLClient graphqlClient,
string owner,
string repo,
string branch)
{
// Fetch all data from GitHub in parallel
var commitsTask = GetAllCommitsAsync(client, owner, repo, branch);
var commitsTask = GetAllCommitsAsync(graphqlClient, owner, repo, branch);
var releasesTask = client.Repository.Release.GetAll(owner, repo);
var tagsTask = client.Repository.GetAllTags(owner, repo);
var pullRequestsTask = client.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest { State = ItemStateFilter.All });
Expand Down Expand Up @@ -401,7 +416,7 @@ private static int DetermineSearchStartIndex(int toIndex, int releaseCount)
/// <returns>Tuple of (bugs, nonBugChanges, allChangeIds).</returns>
private static async Task<(List<ItemInfo> bugs, List<ItemInfo> nonBugChanges, HashSet<string> allChangeIds)>
CollectChangesFromPullRequestsAsync(
List<GitHubCommit> commitsInRange,
List<Commit> commitsInRange,
LookupData lookupData,
string owner,
string repo,
Expand Down Expand Up @@ -537,20 +552,24 @@ private static List<ItemInfo> CollectKnownIssues(IReadOnlyList<Issue> issues, Ha
}

/// <summary>
/// Gets all commits for a branch using pagination.
/// Gets all commits for a branch using GraphQL pagination.
/// </summary>
/// <param name="client">GitHub client.</param>
/// <param name="graphqlClient">GitHub GraphQL client.</param>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <param name="branch">Branch name.</param>
/// <returns>List of all commits.</returns>
private static async Task<IReadOnlyList<GitHubCommit>> GetAllCommitsAsync(GitHubClient client, string owner, string repo, string branch)
private static async Task<IReadOnlyList<Commit>> GetAllCommitsAsync(
GitHubGraphQLClient graphqlClient,
string owner,
string repo,
string branch)
{
// Create request for branch commits
var request = new CommitRequest { Sha = branch };
// Fetch all commit SHAs for the branch using GraphQL
var commitShas = await graphqlClient.GetCommitsAsync(owner, repo, branch);

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

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

// Iterate through commits from newest to oldest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public void GitHubRepoConnector_GetTypeFromLabels_EmptyLabels_ReturnsOther()
public void GitHubRepoConnector_GetCommitsInRange_ToHashNotFound_ReturnsEmptyList()
{
// Arrange - empty list of commits
var commits = new List<GitHubCommit>();
var commits = new List<GitHubRepoConnector.Commit>();

// Act
var result = GitHubRepoConnector.GetCommitsInRange(commits, "hash1", "hash4");
Expand Down
Loading