通过GitHub的v4 API,检索存储库中的所有提交的高效GraphQL查询?

4
我尝试构建一个GraphQL查询来检索给定仓库(无论分支如何)的所有提交,使用GitHub的v4 GraphQL API。以github/training-kit仓库为例,我目前需要按以下步骤进行操作:
1. 使用此查询检索仓库的所有分支列表(必要时使用pageInfo重复查询以获取所有分支):
{
  repository(owner: "github", name: "training-kit") {
    refs(first: 10, refPrefix: "refs/heads/", after: "") {
      totalCount
      edges {
        node {
          name
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
  1. 遍历分支列表,对于每个分支获取其提交历史记录。在每个分支内部,通常需要多次运行查询,因为存在分页限制。例如,这是获取master分支前100个提交的查询:
{
  repository(owner: "github", name: "training-kit") {
    refs(query: "master", refPrefix: "refs/heads/", first: 1) {
      nodes {
        target {
          ... on Commit {
            history(first: 100) {
              nodes {
                oid
              }
              pageInfo {
                hasNextPage
                endCursor
              }
            }
          }
        }
      }
    }
  }
}

对我来说,这个解决方案是低效的,特别是因为第二步。在第二步中,大多数提交将在许多分支之间重复(更不用说我必须做很多查询才能从一个分支中获取所有提交了)。一旦我获得来自每个分支的提交列表,我必须对它们进行去重。整个过程需要进行很多很多次查询和大量的重复努力。但是,由于有些提交只能通过某些分支到达,我认为除了详尽地查询每个分支,别无他法。 有人能建议更有效的策略,更好地利用GitHub的GraphQL API来检索存储库中的所有提交吗? 谢谢! P.S. 供参考,我查看了以下问题,但似乎没有回答我的问题:
a. Github GraphQL-获取存储库的提交列表-他们只想从存储库的默认分支获取最新的n个提交,而不是所有分支的提交。
b. 使用graphql从github获取提交统计信息 -这个问题只关心默认分支,可能不包括所有提交。
c. 使用GitHub GraphQL API v4在单个存储库中查询所有提交 -仅关心master分支以及如何进行分页,而不是存储库的所有提交。

奇怪的要求 - 简单而不被支持? - xadm
1
我认为这是一个非常好的问题,而且据我所见还没有得到答案。即使你简化了它,比如说你“只想检索分支内的所有提交”,你仍然需要遍历这100页的提交结果。我目前正在尝试检索一个包含670个提交的存储库的所有提交,每一页只需大约1200毫秒才能获取提交对象中的节点ID。所有这些加起来就是整个存储库需要8秒钟的时间。如果我尝试获取除ID之外的其他属性,每一页需要高达4000毫秒的时间。 - Armino
感谢@Armino,我同意你的评论,我经历的等待时间与你的大致相同。真的希望有人能想出更有效率的解决方案! - hpy
1
谢谢。我正在尝试以下想法:如果您获取所有页面光标(仅在结果中的页面光标),然后并行地发出“真正”的页面请求以获取所有提交数据,则应该显着减少持续时间。我能够在7秒内获取所有7个页面的完整提交数据(与之前的7 x 4秒相比)。这是在快速测试期间完成的,这些结果并不具有决定性,我不确定在大量请求等情况下的行为会是什么样子,但值得一试。 - Armino
1
那听起来像是一个相当大的改进,@Armino你可以把你的解决方案发布为答案,并包含GraphQL调用吗? - hpy
1
@hpy谢谢。我会尽快测试一下并发布答案。 - Armino
1个回答

2
这是我基于C#的想法,关于如何应对这个问题,也许不是要完全解决它,而是改进性能。下面显示的代码解决了“检索存储库默认分支中的所有提交”的问题,然而,它可以应用于几乎任何基于光标的GitHub GraphQL分页场景。我知道你的问题涉及“所有分支的所有提交,去重”,然而,我认为这种方法对你也可能有用。
查询大型存储库的固有问题是每页限制100个结果,并且您必须逐页迭代,因为每个页面包含指向下一页的光标。在我的解决方案中,我解决了光标识别问题,并通过同时发送所有页面请求并行降低了总体执行时间。
这个想法是创建一个对GitHub GraphQL API的初始请求,仅获取给定过滤器的总计数。我假设每页获取100个结果。由于GitHub提交页面光标始终处于格式“xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 99”,其中第一部分是第一个提交oid(第一页的第一个提交-所有页面上的所有光标都使用此oid-在迭代时不会更改),而99是前一页最后提交的顺序号(基于0的索引),因此只需进行“totalCount”请求就可以很容易地计算出670次提交存储库的每个页面的光标:
1. null 2. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 99" 3. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 199" 4. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 299" 5. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 399" 6. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 499" 7. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 599"
生成了每个页面开头的光标后,我们可以为每个页面准备一个单独的任务(Task),其中任务将包含一个请求到GitHub GraphQL以获取一页,并使用Task.WhenAll执行它们所有。
我在一个包含670个提交的存储库上进行了测试,所有7个页面一共在大约7秒内获取。如果我遍历每个页面,每页需要大约4秒,总计25-30秒。
应该注意的是,这没有在生产环境中进行测试,它不涉及错误处理,并且并发实现可能可以改进,因此它应该仅被视为概念验证。此外,我不确定GitHub API将如何处理您发送请求的具有100或1000页提交的存储库。
public async Task<List<Commit>> GetCommitsByPeriodAsync(Guid integrationId, DateTime since, string repositoryName, string repositoryOwner)
{
    string initialCursor = null;

    var firstPageInfo = await GetDefaultBranchCommitsFirstPageInfoAsync(since, initialCursor, repositoryOwner, repositoryName);
    var commitPagesCursors = GetCommitPagesCursors(firstPageInfo, initialCursor );

    var tasks = commitPagesCursors.Select(x => GetDefaultBranchCommitsPageByPeriodAsync(since, x, repositoryOwner, repositoryName));

    var results = await Task.WhenAll(tasks);
    var branchCommitsByPeriod = results.SelectMany(x => x.Commits)
                                       .ToList();

    return branchCommitsByPeriod;
}

private List<string> GetCommitPagesCursors(GetCommitsPageInfoResponse firstPageInfo, string initialCursor)
{
    // Two initial cursors will always be "null", and "oid 99" for 100 items pages
    var cursors = new List<string> { initialCursor, firstPageInfo.PageInfo.EndCursor };
    int totalCount = firstPageInfo.TotalCount;

    var firstCommitCursorSplit = firstPageInfo.PageInfo.EndCursor.Split(" ");
    var firstCommitId = firstCommitCursorSplit[0];

    var lastPageCommitNumberString = firstCommitCursorSplit[1];

    // TO DO: handling TryParse failure scenario
    int.TryParse(lastPageCommitNumberString, out int lastPageCommitNumber);

    // 100 is the max number of objects in a page
    lastPageCommitNumber += 100;

    while (lastPageCommitNumber < totalCount)
    {
        string nextPageCursor = $"{firstCommitId} {lastPageCommitNumber}";
        cursors.Add(nextPageCursor);

        lastPageCommitNumber += 100;
    }

    return cursors;
}

public async Task<GetCommitsPageInfoResponse> GetDefaultBranchCommitsFirstPageInfoAsync(DateTime since, string cursor, string repositoryOwner, string repositoryName)
{
    // Code omitted for brevity
    var commitsRequest = new GraphQLRequest
    {
        Query = @"
            query GetCommitsFirstPage($cursor: String, $commitsSince: GitTimestamp!, $repositoryName: String!, $repositoryOwner: String!) {
              repository(name: $repositoryName, owner: $repositoryOwner) {
                defaultBranchRef{
                  target {
                    ... on Commit {
                      history(after: $cursor, since: $commitsSince) {
                        totalCount
                        pageInfo {
                          endCursor
                          hasNextPage
                        }                      
                      }
                    }
                  }
                }
              }
            }",
        OperationName = "GetCommitsFirstPage",
        Variables = new
        {
            commitsSince = since.ToString("o"),
            cursor = cursor,
            repositoryOwner = repositoryOwner,
            repositoryName = repositoryName
        }
    };
    // Code omitted for brevity
}

public async Task<GetCommitsPageResponse> GetDefaultBranchCommitsPageByPeriodAsync(DateTime since, string cursor, string repositoryOwner, string repositoryName)
{
    
    // Code omitted for brevity

    var commitsRequest = new GraphQLRequest
    {
        Query = @"
            query GetCommitsSinceTimestamp($cursor: String, $commitsSince: GitTimestamp!, $repositoryName: String!, $repositoryOwner: String!) {
              repository(name: $repositoryName, owner: $repositoryOwner) {
                defaultBranchRef{
                  target {
                    ... on Commit {
                      history(after: $cursor, since: $commitsSince) {
                        pageInfo {
                          endCursor
                          hasNextPage
                        }
                        edges {
                          node {
                            oid
                            additions
                            deletions
                            commitUrl
                            url
                            committedDate
                            associatedPullRequests (first: 10) {
                                              nodes {
                                                id
                                                mergedAt
                                              }
                                            }
                            repository {
                              databaseId
                              nameWithOwner
                            }
                            author {
                              name
                              email
                              user {
                                login
                              }
                            }
                            message
                          }
                        }
                      }
                    }
                  }
                }
              }
            }",
        OperationName = "GetCommitsSinceTimestamp",
        Variables = new
        {
            commitsSince = since.ToString("o"),
            cursor = cursor,
            repositoryOwner = repositoryOwner,
            repositoryName = repositoryName
        }
    };
    // Code omitted for brevity
}

1
就像你所说的,这不是一个完整的解决方案,但仍然非常有前途。我不懂C#,但很快会尝试这个算法并回报结果。谢谢@Armino! - hpy
1
此外,还要扩展这个概念。“其中第一部分是第一个提交 oid(第一页的第一个提交 - 所有页面上的所有光标都使用此 oid - 在迭代时不会更改),”第一页上的第一个提交实际上是最后一个(最近的)提交。这遵循 git log 的顺序。 - shellscape

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接