Github API:检索存储库所有分支的所有提交记录

58
根据V2文档,您可以使用以下代码列出分支的所有提交记录:
commits/list/:user_id/:repository/:branch

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

我想使用类似以下代码来收集所有分支:

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

然后遍历它们,获取每个分支的所有提交。或者,如果有一种直接为存储库中的所有分支拉取所有提交的方法,那么这样做同样有效,甚至更好。有什么想法吗?

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

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

问题在于,当我这样做时,它无法正确分页结果。我觉得我们的方法可能有误。有什么想法吗?


2
你能描述一下“它没有正确地分页结果”是什么意思吗? - Kevin Sawicki
顺便提一下,如果你只需要提交的哈希值,可以使用 git log --pretty="%h" - George Pligoropoulos
4个回答

52

我遇到了完全相同的问题。我确实成功地获取了一个存储库中所有分支的所有提交记录(由于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开始工作)。


但是,这种方法存在一个大问题...由于分支共享一些相同的提交,因此当您转移到另一个分支时,您会反复看到相同的提交。

我可以想象有一种更有效的方法来完成这个任务,但对我来说这样做已经足够了。


1
如果我想从自己的私有仓库中获取所有提交记录,该怎么办?您认为进行身份验证是否会给我必要的权限,并且我将在仓库列表中获得私有仓库? - hungryWolf
1
@hungryWolf 我猜测,使用你的授权令牌进行 API 调用应该可以解决问题——前提是你对该令牌拥有足够的权限。尝试一下,然后告诉我们结果 :) - Kevin Jalbert
是的,身份验证令牌有效,顺便说一下,你的回答真的很有帮助。 - hungryWolf
在您的修剪结果中,用于显示提交的 API 调用中不再存在带有 branchname 键。 - Timo
如果您已经知道名称,还可以针对特定分支进行操作 - https://api.github.com/repos/twbs/bootstrap/branches/main,其中main是分支名称。 - Gangula

39

我向GitHub支持团队提出了同样的问题,他们回答我如下:

GETing /repos/:owner/:repo/commits should do the trick. You can pass the branch name in the sha parameter. For example, to get the first page of commits from the '3.0.0-wip' branch of the twitter/bootstrap repository, you would use the following curl request:

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

The docs also describe how to use pagination to get the remaining commits for this branch.

As long as you are making authenticated requests, you can make 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 - Pradeep
我认为 "all_branches = git_connection.repos" 应该改为 "all_branches = github_connection." - user1724295
2
这不正确,它返回所有提交而没有过滤分支名称。 - Afshin Mehrabani

22

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

在浏览器中尝试一下

请注意,当您有超过100个分支和特性时,变量branchCursor会在该情况下使用前一个请求中的pageInfo.endCursor值。
2-将分支数组拆分为最多19个分支的数组
每个节点的请求次数存在一定限制,防止我们对每个节点进行过多的查询。在这里,我进行了一些测试,发现单个查询中不能超过19 * 100个提交。
请注意,在具有<19个分支的仓库的情况下,您不需要担心这个问题。
3-按照每个分支的100个一组查询提交
然后,您可以动态地创建查询,以获取所有分支的下100个提交。以下是两个分支的示例:
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.hasNextpagepageInfo.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();

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

速率限制

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

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


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