【问题标题】:Efficient GraphQL query to retrieve ALL commits in a repository via GitHub's v4 API?通过 GitHub 的 v4 API 检索存储库中所有提交的高效 GraphQL 查询?
【发布时间】:2020-11-20 16:19:25
【问题描述】:

我正在尝试为 GitHub 的 v4 GraphQL API 构建一个 GraphQL 查询,以从给定存储库(无论分支如何)检索所有提交。

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
              }
            }
          }
        }
      }
    }
  }
}

对我来说,这个解决方案效率低下,特别是因为第 2 步。其中大多数提交将在许多分支中重复(更不用说我必须进行许多查询才能从一个分支中获取所有提交)。一旦我从每个分支获得提交列表,我就必须对它们进行重复数据删除。整个过程需要许多许多个查询和大量重复工作。但是,由于有些提交只能由某些分支访问,除了详尽地查询每个分支之外,我看不到其他方法。

谁能提出更有效的策略,更好地利用 GitHub GraphQL API 从存储库中检索所有提交?

谢谢!

P.S.作为参考,我查看了以下问题,但似乎都没有回答我的问题:

一个。 Github GraphQL - Getting a repository's list of commits - 他们的目标只是从存储库的默认分支中获取最新的n 提交数量,而不是所有提交而不考虑分支。

b. Commits stats from github using graphql - 这个问题只对默认分支感兴趣,它可能不包括所有提交。

c。 Querying all commits in a single repository with the GitHub GraphQL API v4 - 只对master 分支和如何进行分页感兴趣,而不是对存储库的所有提交感兴趣。

【问题讨论】:

  • 奇怪的要求 - 根本不支持?
  • 我相信这是一个很好的问题,但我看到它还没有得到回答。即使您简化了它,并说您“只想检索一个分支中的所有提交”,您也必须遍历这 100 个页面的提交结果。我目前正在尝试检索 670 次提交存储库的所有提交,每页大约需要 1200 毫秒才能获取提交对象中的节点 ID。对于整个存储库,所有这些加起来最多需要 8 秒。如果我尝试获取除 id 之外的其他属性,则每页最多需要 4000 毫秒。
  • 谢谢。我正在尝试以下想法:如果您获取所有页面游标(结果中只有页面游标),然后触发“真实”页面请求以并行获取所有提交数据,您应该会显着减少持续时间。我能够在 7 秒内获取包含完整提交数据的所有 7 个页面(与之前的 7 x 4 秒相比)。这是在快速测试期间完成的,这些结果不是决定性的,我不确定在大量请求等情况下会出现什么行为,但值得一试。
  • 这听起来像是一个相当大的改进,@Armino 你能发布你的解决方案作为答案,包括 GraphQL 调用吗?
  • @hpy 谢谢。一旦我更彻底地测试它,我会尝试发布答案。

标签: api github version-control graphql github-api


【解决方案1】:

这是我基于 C# 的关于如何解决此问题的想法,可能不是完全解决它,而是提高性能。下面显示的代码解决了“在存储库的默认分支中检索所有提交”的问题,但是,它可以应用于 GitHub GraphQL 上几乎任何基于光标的分页场景。我知道您的问题是关于“所有分支的所有提交,重复数据删除”,但是,我相信这种方法也可能对您有用。

查询大型存储库的固有问题是每页限制为 100 个结果,并且您必须逐页迭代,因为每个页面都包含指向下一页的光标。我已经解决了我的解决方案中的光标识别问题,通过同时发送所有页面请求减少了整体执行时间。

这个想法是创建一个对 GitHub GraphQL API 的初始请求,只获取给定过滤器的总数。我假设我们每页会获取 100 个结果。由于 GitHub 提交页面游标始终采用“xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 99”格式,其中第一部分是第一次提交 oid(第一页的第一次提交 - 所有页面上的所有游标都使用此 oid - 它在迭代时不会改变),而 99 是上一页的最后一次提交的顺序号(基于 0 的索引),只需发出“totalCount”请求,就可以很容易地计算出 670 次提交存储库的每一页的游标是多少:

  1. “xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 99”
  2. “xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 199”
  3. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 299"
  4. "xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 399"
  5. “xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 499”
  6. “xX9XXXXXXX3961722145Xf39cc9617XXXXxxx 599”

在生成标识每个页面开头的游标后,我们可以为每个页面准备一个单独的Task,其中Task 将包含对GitHub GraphQL 获取一个页面的请求,并使用Task.WhenAll全部执行。

我已经在一个包含 670 个提交的存储库上对此进行了测试,所有 7 个页面总共在大约 7 秒内被获取。如果我遍历每一页,每页大约需要 4 秒,总共需要 25 - 30 秒。

需要注意的是,这不是在生产环境中测试的,它不涉及错误处理,并且并行/并发实现很可能可以改进,所以它只能被视为概念证明。此外,我不确定当您发送对具有 100 或 1000 个提交页面的存储库的请求时,GitHub API 将如何处理。

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
}

【讨论】:

  • 就像你说的不是一个完整的解决方案,但仍然很有希望。我不知道 C#,但很快就会尝试这个算法并报告。谢谢@Armino!
猜你喜欢
  • 2019-08-20
  • 2018-09-01
  • 2021-08-29
  • 2018-03-26
  • 2020-03-26
  • 2014-09-26
  • 2023-03-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多