【问题标题】:Github API: Retrieve all commits for all branches for a repoGithub API:检索所有分支的所有提交以进行 repo
【发布时间】:2012-02-07 16:21:33
【问题描述】:

根据 V2 文档,您可以列出分支的所有提交:

commits/list/:user_id/:repository/:branch

我在 V3 文档中没有看到相同的功能。

我想使用以下方式收集所有分支:

https://api.github.com/repos/:user/:repo/branches

然后遍历它们,为每个提取所有提交。或者,如果有一种方法可以直接提取所有分支的所有提交以进行 repo,那么即使不是更好,它也会同样有效。有任何想法吗?

更新:我尝试将分支 :sha 作为参数传递,如下所示:

params = {:page => 1, :per_page => 100, :sha => b}

问题是当我这样做时,它没有正确分页结果。我觉得我们正在错误地处理这个问题。有什么想法吗?

【问题讨论】:

  • 你能描述一下“它没有正确分页结果”的意思吗?
  • 顺便说一下,如果你只需要提交的哈希值,你可以这样做git log --pretty="%h"

标签: github-api


【解决方案1】:

我遇到了完全相同的问题。我确实设法获取了存储库中所有分支的所有提交(由于 API 的原因,可能没有那么高效)。

检索存储库中所有分支的所有提交的方法

正如你所说,首先你收集所有的分支:

# https://api.github.com/repos/:user/:repo/branches
https://api.github.com/repos/twitter/bootstrap/branches

您缺少的关键是用于获取提交的 APIv3 使用参考提交(用于列出存储库上的提交的 API 调用参数 sha)。所以你需要确保当你收集分支时你也选择了他们最新的sha:

twitter/bootstrap 的分支 API 调用的修剪结果

[
  {
    "commit": {
      "url": "https://api.github.com/repos/twitter/bootstrap/commits/8b19016c3bec59acb74d95a50efce70af2117382",
      "sha": "8b19016c3bec59acb74d95a50efce70af2117382"
    },
    "name": "gh-pages"
  },
  {
    "commit": {
      "url": "https://api.github.com/repos/twitter/bootstrap/commits/d335adf644b213a5ebc9cee3f37f781ad55194ef",
      "sha": "d335adf644b213a5ebc9cee3f37f781ad55194ef"
    },
    "name": "master"
  }
]

使用最后一次提交的 sha

所以我们看到这里的两个分支有不同的 sha,这些是这些分支上的最新提交 sha。你现在可以做的是从他们最新的 sha 中遍历每个分支:

# With sha parameter of the branch's lastest sha
# https://api.github.com/repos/:user/:repo/commits
https://api.github.com/repos/twitter/bootstrap/commits?per_page=100&sha=d335adf644b213a5ebc9cee3f37f781ad55194ef

所以上面的 API 调用将列出 twitter/bootstrapmaster 分支的最后 100 次提交。使用 API,您必须指定下一个提交的 sha 才能获得下 100 个提交。我们可以使用最后一次提交的 sha(在当前示例中为 7a8d6b19767a92b1c4ea45d88d4eedc2b29bf1fa)作为下一个 API 调用的输入:

# Next API call for commits (use the last commit's sha)
# https://api.github.com/repos/:user/:repo/commits
https://api.github.com/repos/twitter/bootstrap/commits?per_page=100&sha=7a8d6b19767a92b1c4ea45d88d4eedc2b29bf1fa

重复这个过程,直到最后一次提交的 sha 与 API 的调用 sha 参数相同。

下一个分支

这就是一个分支。现在,您对另一个分支应用相同的方法(从最新的 sha 开始工作)。


这种方法存在一个大问题...由于分支共享一些相同的提交,当您移动到另一个分支时,您会一遍又一遍地看到相同的提交。

我可以想象有一种更有效的方法可以实现这一点,但这对我有用。

【讨论】:

  • 如果我想从自己的私有仓库中获取所有提交,我应该怎么做?你认为 authing 会给我必要的权限,我也会在 repos 列表中获得私有 repos 吗?
  • @hungryWolf 我怀疑使用您的身份验证令牌的 API 调用应该可以解决问题——前提是您对该令牌有足够的权限。试一试,让我们知道:)
  • 是的身份验证令牌有效,顺便说一句,您的回答真的很有帮助
【解决方案2】:

我向 GitHub 支持提出了同样的问题,他们回答了我这个问题:

GETing /repos/:owner/:repo/commits 应该可以解决问题。您可以在sha 参数中传递分支名称。例如,要从 the twitter/bootstrap repository 的 '3.0.0-wip' 分支获取提交的第一页,您可以使用以下 curl 请求:

curl https://api.github.com/repos/twitter/bootstrap/commits?sha=3.0.0-wip

该分支的文档还 describe how to use pagination to get the remaining commits

只要你是making authenticated requests,就可以发up to 5,000 requests per hour

我在我的应用程序中使用 rails github-api 如下(使用https://github.com/peter-murach/github gem):

github_connection = Github.new :client_id => 'your_id', :client_secret => 'your_secret', :oauth_token => 'your_oath_token'
branches_info = {}
all_branches = git_connection.repos.list_branches owner,repo_name
all_branches.body.each do |branch|
    branches_info["#{branch.name}".to_s] = "#{branch.commit.url}"
end
branches_info.keys.each do |branch|
    commits_list.push (git_connection.repos.commits.list owner,repo_name, start_date,      end_date, :sha => "branch_name")
end

【讨论】:

  • 这正是我想要的。谢谢@Gerson
  • 我认为“all_branches = git_connection.repos”应该是“all_branches = github_connection”。
  • 这是不正确的,返回所有提交而不过滤分支名称。
【解决方案3】:

使用 GraphQL API v4

您可以使用GraphQL API v4 来优化每个分支的提交下载。在以下方法中,我设法在一个请求中下载了 1900 个提交(19 个不同分支中的每个分支 100 个提交),这大大减少了请求的数量(与使用 REST api 相比)。

1 - 获取所有分支

如果您有超过 100 个分支,则必须获取所有分支并进行分页:

查询:

query($owner:String!, $name:String!, $branchCursor: String!) {
  repository(owner: $owner, name: $name) {
    refs(first: 100, refPrefix: "refs/heads/",after: $branchCursor) {
      totalCount
      edges {
        node {
          name
          target {
            ...on Commit {
              history(first:0){
                totalCount
              }
            }
          }
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

变量:

{
  "owner": "google",
  "name": "gson",
  "branchCursor": ""
}

Try it in the explorer

请注意,branchCursor 变量在您有超过 100 个分支时使用,并且在这种情况下,在上一个请求中具有 pageInfo.endCursor 的值。

2 - 将分支数组拆分为最多 19 个分支的数组

每个节点的请求数量有一些限制,这会阻止我们对每个节点进行过多的查询。在这里,我执行的一些测试表明,我们不能在单个查询中超过 19*100 次提交。

请注意,如果 repo 有

3 - 每个分支按 100 块查询提交

然后,您可以动态创建查询,以获取所有分支上的 100 个下一个提交。有 2 个分支的示例:

query ($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    branch0: ref(qualifiedName: "JsonArrayImplementsList") {
      target {
        ... on Commit {
          history(first: 100) {
            ...CommitFragment
          }
        }
      }
    }
    branch1: ref(qualifiedName: "master") {
      target {
        ... on Commit {
          history(first: 100) {
            ...CommitFragment
          }
        }
      }
    }
  }
}

fragment CommitFragment on CommitHistoryConnection {
  totalCount
  nodes {
    oid
    message
    committedDate
    author {
      name
      email
    }
  }
  pageInfo {
    hasNextPage
    endCursor
  }
}

Try it in the explorer

  • 使用的变量是owner 代表repo 的所有者,name 代表repo 的名称。
  • fragment 以避免重复提交历史字段定义。

您可以看到pageInfo.hasNextpage & pageInfo.endCursor 将用于对每个分支进行分页。分页发生在history(first: 100) 中,并指定遇到的最后一个游标。例如下一个请求将有history(first: 100, after: "6e2fcdcaf252c54a151ce6a4441280e4c54153ae 99")。对于每个分支,我们必须使用最后一个 endCursor 值更新请求,以查询 100 次提交。

pageInfo.hasNextPagefalse 时,此分支没有更多页面,因此我们不会将其包含在下一个请求中。

当最后一个分支有pageInfo.hasNextPagefalse 时,我们已经检索到所有提交

示例实现

这是使用github-graphql-client 在 NodeJS 中的示例实现。可以用任何其他语言实现相同的方法。以下还将提交存储在文件 commitsX.json 中:

var client = require('github-graphql-client');
var fs = require("fs");

const owner = "google";
const repo = "gson";
const accessToken = "YOUR_ACCESS_TOKEN";

const branchQuery = `
query($owner:String!, $name:String!, $branchCursor: String!) {
  repository(owner: $owner, name: $name) {
    refs(first: 100, refPrefix: "refs/heads/",after: $branchCursor) {
      totalCount
      edges {
        node {
          name
          target {
            ...on Commit {
              history(first:0){
                totalCount
              }
            }
          }
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}`;

function buildCommitQuery(branches){
    var query = `
        query ($owner: String!, $name: String!) {
          repository(owner: $owner, name: $name) {`;
    for (var key in branches) {
        if (branches.hasOwnProperty(key) && branches[key].hasNextPage) {
          query+=`
            ${key}: ref(qualifiedName: "${branches[key].name}") {
              target {
                ... on Commit {
                  history(first: 100, after: ${branches[key].cursor ? '"' + branches[key].cursor + '"': null}) {
                    ...CommitFragment
                  }
                }
              }
            }`;
        }
    }
    query+=`
          }
        }`;
    query+= commitFragment;
    return query;
}

const commitFragment = `
fragment CommitFragment on CommitHistoryConnection {
  totalCount
  nodes {
    oid
    message
    committedDate
    author {
      name
      email
    }
  }
  pageInfo {
    hasNextPage
    endCursor
  }
}`;

function doRequest(query, variables) {
  return new Promise(function (resolve, reject) {
    client({
        token: accessToken,
        query: query,
        variables: variables
    }, function (err, res) {
      if (!err) {
        resolve(res);
      } else {
        console.log(JSON.stringify(err, null, 2));
        reject(err);
      }
    });
  });
}

function buildBranchObject(branch){
    var refs = {};

    for (var i = 0; i < branch.length; i++) {
        console.log("branch " + branch[i].node.name);
        refs["branch" + i] = {
            name: branch[i].node.name,
            totalCount: branch[i].node.target.history.totalCount,
            cursor: null,
            hasNextPage : true,
            commits: []
        };
    }
    return refs;
}

async function requestGraphql() {
    var iterateBranch = true;
    var branches = [];
    var cursor = "";

    // get all branches
    while (iterateBranch) {
        let res = await doRequest(branchQuery,{
          "owner": owner,
          "name": repo,
          "branchCursor": cursor
        });
        iterateBranch = res.data.repository.refs.pageInfo.hasNextPage;
        cursor = res.data.repository.refs.pageInfo.endCursor;
        branches = branches.concat(res.data.repository.refs.edges);
    }

    //split the branch array into smaller array of 19 items
    var refChunk = [], size = 19;

    while (branches.length > 0){
        refChunk.push(branches.splice(0, size));
    }

    for (var j = 0; j < refChunk.length; j++) {

        //1) store branches in a format that makes it easy to concat commit when receiving the query result
        var refs = buildBranchObject(refChunk[j]);

        //2) query commits while there are some pages existing. Note that branches that don't have pages are not 
        //added in subsequent request. When there are no more page, the loop exit
        var hasNextPage = true;
        var count = 0;

        while (hasNextPage) {
            var commitQuery = buildCommitQuery(refs);
            console.log("request : " + count);
            let commitResult = await doRequest(commitQuery, {
              "owner": owner,
              "name": repo
            });
            hasNextPage = false;
            for (var key in refs) {
                if (refs.hasOwnProperty(key) && commitResult.data.repository[key]) {
                    isEmpty = false;
                    let history = commitResult.data.repository[key].target.history;
                    refs[key].commits = refs[key].commits.concat(history.nodes);
                    refs[key].cursor = (history.pageInfo.hasNextPage) ? history.pageInfo.endCursor : '';
                    refs[key].hasNextPage = history.pageInfo.hasNextPage;
                    console.log(key + " : " + refs[key].commits.length + "/" + refs[key].totalCount + " : " + refs[key].hasNextPage + " : " + refs[key].cursor + " : " + refs[key].name);
                    if (refs[key].hasNextPage){
                        hasNextPage = true;
                    }
                }
            }
            count++;
            console.log("------------------------------------");
        }
        for (var key in refs) {
            if (refs.hasOwnProperty(key)) {
                console.log(refs[key].totalCount + " : " + refs[key].commits.length + " : " + refs[key].name);
            }
        }

        //3) write commits chunk (up to 19 branches) in a single json file
        fs.writeFile("commits" + j + ".json", JSON.stringify(refs, null, 4), "utf8", function(err){
            if (err){
                console.log(err);
            }
            console.log("done");
        });
    }
}

requestGraphql();

这也适用于有很多分支的 repo,例如 this one 有超过 700 个分支

速率限制

请注意,虽然使用 GraphQL 确实可以减少请求数量,但它不一定会提高您的速率限制,因为速率限制是基于点数而不是有限数量的请求:检查GraphQL API rate limit

【讨论】:

    【解决方案4】:

    没有访问令牌的纯 JS 实现(未经授权的使用)

    const base_url = 'https://api.github.com';
    
        function httpGet(theUrl, return_headers) {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.open("GET", theUrl, false); // false for synchronous request
            xmlHttp.send(null);
            if (return_headers) {
                return xmlHttp
            }
            return xmlHttp.responseText;
        }
    
        function get_all_commits_count(owner, repo, sha) {
            let first_commit = get_first_commit(owner, repo);
            let compare_url = base_url + '/repos/' + owner + '/' + repo + '/compare/' + first_commit + '...' + sha;
            let commit_req = httpGet(compare_url);
            let commit_count = JSON.parse(commit_req)['total_commits'] + 1;
            console.log('Commit Count: ', commit_count);
            return commit_count
        }
    
        function get_first_commit(owner, repo) {
            let url = base_url + '/repos/' + owner + '/' + repo + '/commits';
            let req = httpGet(url, true);
            let first_commit_hash = '';
            if (req.getResponseHeader('Link')) {
                let page_url = req.getResponseHeader('Link').split(',')[1].split(';')[0].split('<')[1].split('>')[0];
                let req_last_commit = httpGet(page_url);
                let first_commit = JSON.parse(req_last_commit);
                first_commit_hash = first_commit[first_commit.length - 1]['sha']
            } else {
                let first_commit = JSON.parse(req.responseText);
                first_commit_hash = first_commit[first_commit.length - 1]['sha'];
            }
            return first_commit_hash;
        }
    
        let owner = 'getredash';
        let repo = 'redash';
        let sha = 'master';
        get_all_commits_count(owner, repo, sha);
    

    学分 - https://gist.github.com/yershalom/a7c08f9441d1aadb13777bce4c7cdc3b

    【讨论】:

      猜你喜欢
      • 2016-02-22
      • 2021-09-04
      • 1970-01-01
      • 2016-02-28
      • 1970-01-01
      • 2014-02-22
      • 1970-01-01
      • 1970-01-01
      • 2023-03-03
      相关资源
      最近更新 更多