如何使用GitHub V3 API获取存储库的提交计数?

33
我正在尝试使用API计算许多大型Github存储库的提交次数,因此我想避免获取所有提交的完整列表(例如这个链接:api.github.com/repos/jasonrudolph/keyboard/commits),然后对它们进行计数。
如果我有第一个(初始)提交的哈希值,我可以使用这种技术来比较第一个提交和最新的提交,并愉快地报告它们之间的总提交数(因此我需要添加一个)。不幸的是,我无法看到如何通过API优雅地获取第一个提交。
基本的repo URL确实为我提供了created_at (例如这个链接:api.github.com/repos/jasonrudolph/keyboard),所以我可以通过将提交限制为创建日期之前的提交(例如这个链接:api.github.com/repos/jasonrudolph/keyboard/commits?until=2013-03-30T16:01:43Z)来获得减少的提交集,并使用最早的提交(始终列在最后?)或者可能是没有父提交的提交(不确定是否forked项目有初始父提交)。
有更好的方法来获取存储库的第一个提交哈希值吗?
更好的是,这整个过程似乎对于一个简单的统计数据而言过于复杂,我想知道是否有什么遗漏的。有更好的使用API获取存储库提交计数的想法吗?
编辑:这个类似的问题正在尝试按某些文件过滤("并在其中选择特定的文件"),因此有一个不同的答案。

可能是重复的问题:github api:如何高效地查找存储库的提交数量? - Ciro Santilli OurBigBook.com
1
不是完全相同的问题。谢谢! - SteveCoffman
10个回答

16

https://api.github.com/repos/{username}/{repo}/commits?sha={branch}&per_page=1&page=1 上发起请求。

现在,只需要获取响应头的 Link 参数,并提取位于 rel="last" 前面的页数。

这个页面计数等于该分支中的总提交数!

诀窍是使用&per_page=1&page=1。它将 1 个提交分布到 1 个页面。因此,提交的总数将等于页面总数。


3
非常聪明,巧妙的技巧。 - Emsi
1
最佳答案我应该说。 - Underoos
这个能支持多个贡献者或多个代码仓库吗?我正在尝试在Github企业组织中实现这个,但我觉得这个解决方案可能行不通。 - Sridhar Sarnobat

15
你可以考虑使用GraphQL API v4来同时为多个代码库执行提交计数,使用别名。以下内容将获取3个不同代码库(每个代码库最多100个分支)所有分支的提交计数:
{
  gson: repository(owner: "google", name: "gson") {
    ...RepoFragment
  }
  martian: repository(owner: "google", name: "martian") {
    ...RepoFragment
  }
  keyboard: repository(owner: "jasonrudolph", name: "keyboard") {
    ...RepoFragment
  }
}

fragment RepoFragment on Repository {
  name
  refs(first: 100, refPrefix: "refs/heads/") {
    edges {
      node {
        name
        target {
          ... on Commit {
            id
            history(first: 0) {
              totalCount
            }
          }
        }
      }
    }
  }
}

在浏览器中尝试一下

RepoFragment是一个片段,它有助于避免为每个repo重复查询字段。

如果您只需要默认分支上的提交计数,则更加简单:

{
  gson: repository(owner: "google", name: "gson") {
    ...RepoFragment
  }
  martian: repository(owner: "google", name: "martian") {
    ...RepoFragment
  }
  keyboard: repository(owner: "jasonrudolph", name: "keyboard") {
    ...RepoFragment
  }
}

fragment RepoFragment on Repository {
  name
  defaultBranchRef {
    name
    target {
      ... on Commit {
        id
        history(first: 0) {
          totalCount
        }
      }
    }
  }
}

在浏览器中尝试一下


1
Bertrand,图形API允许在没有令牌的情况下进行查询吗?似乎这对于公共存储库来说是一个问题,因为早期的API可以在没有令牌的情况下工作。 - Mahesh
2
@Mahesh 是的,这是GraphQL API的一个很大的警告,如果你只想请求公共内容或者从Web客户端使用API。只有在你可以安全地存储访问令牌的环境中才能使用GraphQL API,否则请坚持使用REST API v3。 - Bertrand Martel
defaultBranchRef 是获取主分支提交次数的正确做法。谢谢! - Anton

12

如果您想要获取默认分支中的提交总数,您可能需要考虑不同的方法。

使用 Repo Contributors API 获取所有贡献者的列表:

https://developer.github.com/v3/repos/#list-contributors

列表中的每个项目都包含一个“contributions”字段,该字段告诉您用户在默认分支中编写了多少次提交。对所有贡献者的这些字段求和,您应该可以得到默认分支中提交的总数。

贡献者列表通常比提交列表要短得多,因此计算默认分支中提交的总数应该需要更少的请求。


1
谢谢。当我使用像这样的链接时(https://api.github.com/repos/jquery/jquery/contributors?anon=true),它似乎被限制为30个项目。我发现返回多个项目的请求默认会进行分页,每页30个项目。您可以使用“?page”参数指定更多页面。因此,如果您获得了30个项目,则需要检查是否有更多页面,并将它们添加到初始结果中。 - SteveCoffman
@SteveCoffman 是的,这是预期的行为:https://developer.github.com/v3/#pagination - Ivan Zuzak
看起来你和我两种方法都可行,但都不够优雅。除非有人提出了我们都忽略的东西,否则我会接受你的答案。谢谢。 - SteveCoffman
2
为什么GitHub不直接在API响应中包含提交计数?让人失望的是,我们必须不必要地遍历贡献者列表。 - Dan Dascalescu
3
请注意,如果您的存储库/组织/其他任何内容中删除了任何用户(例如员工离开公司),则此方法将返回错误的数字。 - snowe
这个答案讨论了如何获取默认分支的提交次数。请参考@Bertrand Martel的答案底部,了解如何使用GraphQL API实现这一点! - Anton

6

如果您在新项目中开始使用GraphQL API v4,则处理此问题的可能方式,但如果仍在使用REST API v3,则可以通过将请求限制为每页仅1个结果来解决分页问题。 通过设置该限制,最后一个链接中返回的pages数量将等于总数。

例如,使用python3和requests库:

def commit_count(project, sha='master', token=None):
    """
    Return the number of commits to a project
    """
    token = token or os.environ.get('GITHUB_API_TOKEN')
    url = f'https://api.github.com/repos/{project}/commits'
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': f'token {token}',
    }
    params = {
        'sha': sha,
        'per_page': 1,
    }
    resp = requests.request('GET', url, params=params, headers=headers)
    if (resp.status_code // 100) != 2:
        raise Exception(f'invalid github response: {resp.content}')
    # check the resp count, just in case there are 0 commits
    commit_count = len(resp.json())
    last_page = resp.links.get('last')
    # if there are no more pages, the count must be 0 or 1
    if last_page:
        # extract the query string from the last page url
        qs = urllib.parse.urlparse(last_page['url']).query
        # extract the page number from the query string
        commit_count = int(dict(urllib.parse.parse_qsl(qs))['page'])
    return commit_count

尝试在Java中运用相同的概念,效果完美,谢谢! - Grant Foster

6
简单的解决方案:查看页面编号。Github已经为您分页,因此您可以通过从Link头部获取最后一页的编号、减去1(您需要手动添加最后一页)并乘以页面大小来轻松计算提交次数,然后获取结果的最后一页并获取该数组的大小并将这两个数字相加。这是最多两个API调用!
以下是使用Ruby中的octokit gem抓取整个组织的总提交次数的实现:
@github = Octokit::Client.new access_token: key, auto_traversal: true, per_page: 100

Octokit.auto_paginate = true
repos = @github.org_repos('my_company', per_page: 100)

# * take the pagination number
# * get the last page
# * see how many items are on it
# * multiply the number of pages - 1 by the page size
# * and add the two together. Boom. Commit count in 2 api calls
def calc_total_commits(repos)
    total_sum_commits = 0

    repos.each do |e| 
        repo = Octokit::Repository.from_url(e.url)
        number_of_commits_in_first_page = @github.commits(repo).size
        repo_sum = 0
        if number_of_commits_in_first_page >= 100
            links = @github.last_response.rels

            unless links.empty?
                last_page_url = links[:last].href

                /.*page=(?<page_num>\d+)/ =~ last_page_url
                repo_sum += (page_num.to_i - 1) * 100 # we add the last page manually
                repo_sum += links[:last].get.data.size
            end
        else
            repo_sum += number_of_commits_in_first_page
        end
        puts "Commits for #{e.name} : #{repo_sum}"
        total_sum_commits += repo_sum
    end
    puts "TOTAL COMMITS #{total_sum_commits}"
end

是的,我知道代码很混乱,这只是几分钟内随意拼凑出来的。


2
没有使用你的代码,但是查看页眉链接中的页面编号的想法为我节省了许多 API 调用。谢谢。 - Marcino

3
我刚刚写了一个小脚本来实现这个功能。 由于它没有处理GitHub的速率限制,所以可能无法处理大型代码库。此外,它需要Python requests软件包。
#!/bin/env python3.4
import requests

GITHUB_API_BRANCHES = 'https://%(token)s@api.github.com/repos/%(namespace)s/%(repository)s/branches'
GUTHUB_API_COMMITS = 'https://%(token)s@api.github.com/repos/%(namespace)s/%(repository)s/commits?sha=%(sha)s&page=%(page)i'


def github_commit_counter(namespace, repository, access_token=''):
    commit_store = list()

    branches = requests.get(GITHUB_API_BRANCHES % {
        'token': access_token,
        'namespace': namespace,
        'repository': repository,
    }).json()

    print('Branch'.ljust(47), 'Commits')
    print('-' * 55)

    for branch in branches:
        page = 1
        branch_commits = 0

        while True:
            commits = requests.get(GUTHUB_API_COMMITS % {
                'token': access_token,
                'namespace': namespace,
                'repository': repository,
                'sha': branch['name'],
                'page': page
            }).json()

            page_commits = len(commits)

            for commit in commits:
                commit_store.append(commit['sha'])

            branch_commits += page_commits

            if page_commits == 0:
                break

            page += 1

        print(branch['name'].ljust(45), str(branch_commits).rjust(9))

    commit_store = set(commit_store)
    print('-' * 55)
    print('Total'.ljust(42), str(len(commit_store)).rjust(12))

# for private repositories, get your own token from
# https://github.com/settings/tokens
# github_commit_counter('github', 'gitignore', access_token='fnkr:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
github_commit_counter('github', 'gitignore')

发生了些变化,现在代码显示错误 line 36, in github_commit_counter: commit_store.append(commit['sha']) - Whitecat
我错了。脚本是有效的。我只是被限制频率了。 - Whitecat

3

以下是基于snowe的方法使用Fetch的JavaScript示例

Fetch示例

/**
 * @param {string} owner Owner of repo
 * @param {string} repo Name of repo
 * @returns {number} Number of total commits the repo contains on main master branch
 */
export const getTotalCommits = (owner, repo) => {
  let url = `https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`;
  let pages = 0;

  return fetch(url, {
    headers: {
      Accept: "application/vnd.github.v3+json",
    },
  })
    .then((data) => data.headers)
    .then(
      (result) =>
        result
          .get("link")
          .split(",")[1]
          .match(/.*page=(?<page_num>\d+)/).groups.page_num
    )
    .then((numberOfPages) => {
      pages = numberOfPages;
      return fetch(url + `&page=${numberOfPages}`, {
        headers: {
          Accept: "application/vnd.github.v3+json",
        },
      }).then((data) => data.json());
    })
    .then((data) => {
      return data.length + (pages - 1) * 100;
    })
    .catch((err) => {
      console.log(`ERROR: calling: ${url}`);
      console.log("See below for more info:");
      console.log(err);
    });
};

用法

getTotalCommits('facebook', 'react').then(commits => {
    console.log(commits);
});

不错的回答。然而,这可以只用一个请求完成。你可以在这里查看我的答案:https://dev59.com/d14c5IYBdhLWcg3wa56M#70610670 - Shashi
可以使用此示例在一次请求中执行,如@Shashi建议的stackoverflow.com/a/70610670/10266115,通过删除最后两个"then"块并用"?per_page=1&page=1"替换URL查询字符串。 - Damien Golding

1

可与Github Enterprise配合使用:

gh api https://github.myenterprise.com/api/v3/repos/myorg/myrepo/commits --paginate | jq length | datamash sum 1

如果你是Unix管道的支持者,你可以将其与存储库列表相结合,以获取组织中的所有提交。

设置说明

对于Mac OS:

brew install gh
brew install datamash
gh auth login

1
我用了@Shashi的技巧来编写一个JavaScript的fetch示例:
async function getTotalCommits(repo) {
    // Fetch data from GitHub API for the given repo
    const response = await fetch(`https://api.github.com/repos/${repo}/commits?per_page=1&page=1`);

    // Check for successful response
    if (!response.ok) {
        throw new Error(`GitHub API responded with status ${response.status}`);
    }

    // Extract Link header
    const linkHeader = response.headers.get('Link');

    // Use regular expression to find the number of pages (total commits)
    const lastPageMatch = linkHeader.match(/page=(\d+)>; rel="last"/);

    if (!lastPageMatch) {
        throw new Error('Cannot determine total commits from the Link header');
    }

    return parseInt(lastPageMatch[1], 10);
}

// Example usage:
getTotalCommits('microsoft/vscode')
    .then(totalCommits => {
        console.log(`Total commits: ${totalCommits}`);
    })
    .catch(error => {
        console.error(error.message);
    });

1
我使用Python创建了一个生成器,它返回一个贡献者列表,总结了提交次数,并检查其是否有效。如果提交次数少于给定值,则返回True;如果提交次数相同或更多,则返回False。你需要填写的唯一内容是使用你的凭据的请求会话。以下是我为你编写的代码:
from requests import session
def login()
    sess = session()

    # login here and return session with valid creds
    return sess

def generateList(link):
    # you need to login before you do anything
    sess = login()

    # because of the way that requests works, you must start out by creating an object to
    # imitate the response object. This will help you to cleanly while-loop through
    # github's pagination
    class response_immitator:
        links = {'next': {'url':link}}
    response = response_immitator() 
    while 'next' in response.links:
        response = sess.get(response.links['next']['url'])
        for repo in response.json():
            yield repo

def check_commit_count(baseurl, user_name, repo_name, max_commit_count=None):
    # login first
    sess = login()
    if max_commit_count != None:
        totalcommits = 0

        # construct url to paginate
        url = baseurl+"repos/" + user_name + '/' + repo_name + "/stats/contributors"
        for stats in generateList(url):
            totalcommits+=stats['total']

        if totalcommits >= max_commit_count:
            return False
        else:
            return True

def main():
    # what user do you want to check for commits
    user_name = "arcsector"

    # what repo do you want to check for commits
    repo_name = "EyeWitness"

    # github's base api url
    baseurl = "https://api.github.com/"

    # call function
    check_commit_count(baseurl, user_name, repo_name, 30)

if __name__ == "__main__":
    main()

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