48

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

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

4

4 回答 4

43

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

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

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

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

您缺少的关键是用于获取提交的 APIv3 使用参考提交(用于列出存储库sha上的提交的 API 调用的参数)。所以你需要确保当你收集分支时你也选择了他们最新的 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/bootstrap的master分支的最后 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 开始工作)。


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

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

于 2012-04-01T20:54:30.120 回答
32

我问了同样的问题以获得 GitHub 支持,他们回答了我这个问题:

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

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

文档还描述了如何使用分页来获取此分支的剩余提交。

只要您发出经过身份验证的请求,您每小时最多可以发出 5,000 个请求

我在我的应用程序中使用 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
于 2013-05-28T00:46:12.723 回答
19

使用 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": ""
}

在资源管理器中尝试

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

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

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

请注意,对于具有 < 19 个分支的 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
  }
}

在资源管理器中尝试

  • 使用的变量用于owner存储库的所有者和存储库name的名称。
  • 一个片段,以避免重复提交历史字段定义。

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

pageInfo.hasNextPage什么时候false,这个分支没有更多的页面,所以我们不会在下一个请求中包含它。

当最后一个分支必须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();

这也适用于有很多分支的仓库,例如这个有 700 多个分支的仓库

速率限制

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

于 2017-11-28T04:06:40.353 回答
0

没有访问令牌的纯 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

于 2020-05-01T18:13:12.460 回答