Skip to content

Commit ba0dc07

Browse files
committed
Add support for searching commits
Adds the `search_commits` method to the `GitHub` class in order to be able to search for commits that match a given query. Closes sigmavirus24#748
1 parent 37b3cf9 commit ba0dc07

File tree

6 files changed

+186
-2
lines changed

6 files changed

+186
-2
lines changed

src/github3/github.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,87 @@ def search_code(self, query, sort=None, order=None, per_page=None,
16691669
number, url, search.CodeSearchResult, self, params, etag, headers
16701670
)
16711671

1672+
def search_commits(self, query, sort=None, order=None, per_page=None,
1673+
text_match=False, number=-1, etag=None):
1674+
"""Find commits via the commits search API.
1675+
1676+
The query can contain any combination of the following supported
1677+
qualifiers:
1678+
1679+
- ``author`` Matches commits authored by the given username.
1680+
Example: ``author:defunkt``.
1681+
- ``committer`` Matches commits committed by the given username.
1682+
Example: ``committer:defunkt``.
1683+
- ``author-name`` Matches commits authored by a user with the given
1684+
name. Example: ``author-name:wanstrath``.
1685+
- ``committer-name`` Matches commits committed by a user with the given
1686+
name. Example: ``committer-name:wanstrath``.
1687+
- ``author-email`` Matches commits authored by a user with the given
1688+
email. Example: ``author-email:chris@github.com``.
1689+
- ``committer-email`` Matches commits committed by a user with the
1690+
given email. Example: ``committer-email:chris@github.com``.
1691+
- ``author-date`` Matches commits authored within the specified date
1692+
range. Example: ``author-date:<2016-01-01``.
1693+
- ``committer-date`` Matches commits committed within the specified
1694+
date range. Example: ``committer-date:>2016-01-01``.
1695+
- ``merge`` Matches merge commits when set to to ``true``, excludes
1696+
them when set to ``false``.
1697+
- ``hash`` Matches commits with the specified hash. Example:
1698+
``hash:124a9a0ee1d8f1e15e833aff432fbb3b02632105``.
1699+
- ``parent`` Matches commits whose parent has the specified hash.
1700+
Example: ``parent:124a9a0ee1d8f1e15e833aff432fbb3b02632105``.
1701+
- ``tree`` Matches commits with the specified tree hash. Example:
1702+
``tree:99ca967``.
1703+
- ``is`` Matches public repositories when set to ``public``, private
1704+
repositories when set to ``private``.
1705+
- ``user`` or ``org`` or ``repo`` Limits the search to a specific user,
1706+
organization, or repository.
1707+
1708+
For more information about these qualifiers, see: https://git.io/vb7XQ
1709+
1710+
:param str query:
1711+
(required), a valid query as described above, e.g.,
1712+
``css repo:octocat/Spoon-Knife``
1713+
:param str sort:
1714+
(optional), how the results should be sorted;
1715+
options: ``author-date``, ``committer-date``;
1716+
default: best match
1717+
:param str order:
1718+
(optional), the direction of the sorted results,
1719+
options: ``asc``, ``desc``; default: ``desc``
1720+
:param int per_page:
1721+
(optional)
1722+
:param int number:
1723+
(optional), number of commits to return.
1724+
Default: -1, returns all available commits
1725+
:param str etag:
1726+
(optional), previous ETag header value
1727+
:return:
1728+
generator of commit search results
1729+
:rtype:
1730+
:class:`~github3.search.commits.CommitSearchResult`
1731+
"""
1732+
params = {'q': query}
1733+
headers = {'Accept': 'application/vnd.github.cloak-preview'}
1734+
1735+
if sort in ('author-date', 'committer-date'):
1736+
params['sort'] = sort
1737+
1738+
if sort and order in ('asc', 'desc'):
1739+
params['order'] = order
1740+
1741+
if text_match:
1742+
headers['Accept'] = ', '.join([
1743+
headers['Accept'],
1744+
'application/vnd.github.v3.full.text-match+json'
1745+
])
1746+
1747+
url = self._build_url('search', 'commits')
1748+
return structs.SearchIterator(
1749+
number, url, search.CommitSearchResult,
1750+
self, params, etag, headers
1751+
)
1752+
16721753
def search_issues(self, query, sort=None, order=None, per_page=None,
16731754
text_match=False, number=-1, etag=None):
16741755
"""Find issues by state and keyword.

src/github3/search/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from .code import CodeSearchResult
2+
from .commit import CommitSearchResult
23
from .issue import IssueSearchResult
34
from .repository import RepositorySearchResult
45
from .user import UserSearchResult
56

67

7-
__all__ = [CodeSearchResult, IssueSearchResult, RepositorySearchResult,
8-
UserSearchResult]
8+
__all__ = (
9+
'CodeSearchResult',
10+
'CommitSearchResult',
11+
'IssueSearchResult',
12+
'RepositorySearchResult',
13+
'UserSearchResult',
14+
)

src/github3/search/commit.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
"""Commit search results implementation."""
3+
from __future__ import unicode_literals
4+
5+
from .. import git
6+
from .. import models
7+
from .. import repos
8+
from .. import users
9+
10+
11+
class CommitSearchResult(models.GitHubCore):
12+
"""A representation of a commit search result from the API.
13+
14+
This object has the following attributes:
15+
16+
.. attribute:: author
17+
18+
A :class:`~github3.users.ShortUser` representing the user who
19+
authored the found commit.
20+
21+
.. attribute:: comments_url
22+
23+
The URL to retrieve the comments on the found commit from the API.
24+
25+
.. attribute:: commit
26+
27+
A :class:`~github3.git.ShortCommit` representing the found commit.
28+
29+
.. attribute:: committer
30+
31+
A :class:`~github3.users.ShortUser` representing the user who
32+
committed the found commit.
33+
34+
.. attribute:: html_url
35+
36+
The URL to view the found commit in a browser.
37+
38+
.. attribute:: repository
39+
40+
A :class:`~github3.repos.repo.ShortRepository` representing the
41+
repository in which the commit was found.
42+
43+
.. attribute:: score
44+
45+
The confidence score assigned to the result.
46+
47+
.. attribute:: sha
48+
49+
The SHA1 of the found commit.
50+
51+
.. attribute:: text_matches
52+
53+
A list of the text matches in the commit that generated this result.
54+
55+
.. note::
56+
57+
To receive these, you must pass ``text_match=True`` to
58+
:meth:`~github3.github.GitHub.search_commit`.
59+
"""
60+
61+
def _update_attributes(self, data):
62+
self._api = data['url']
63+
self.author = users.ShortUser(data['author'], self)
64+
self.comments_url = data['comments_url']
65+
self.commit = git.ShortCommit(data['commit'], self)
66+
self.committer = users.ShortUser(data['committer'], self)
67+
self.html_url = data['html_url']
68+
self.repository = repos.ShortRepository(data['repository'], self)
69+
self.score = data['score']
70+
self.sha = data['sha']
71+
self.text_matches = data.get('text_matches', [])
72+
73+
def _repr(self):
74+
return '<CommitSearchResult [{0}]>'.format(self.sha[:7])
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.cloak-preview"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA+1ZXW/iRhT9K4jXJtjGhthI0XbVpKttRaKk7HaTaoXG4zEesD3WzDgUrPz33hkbA1EIYLbtS15CMD5n7sfM9bnXRVsyieIxZnkq2wPrrE1TzJIsJpKMORF5LEV7EKJYEPhJkgS+/VW0cx63B+1IykwMDANltDOhMsr9DmANTjImDIYlw0gaf2SMpee/pzQkBvyaUCkM33cwdgPb7xLL8W0U9oKw73nuRT8InDC0EQ5M078I22dtESFY6QhAygIypgGAhlcP/buuJ/0/Y3M4Gs6HV9e94TSjD9P76cMymg27nxePo2vzYXoTPU6/zh6nD73b0cS+Se5nN58ek2H31+nj1cf5wzRIwJJIJvF42/ENp3e7e4y3KkAkleLFOv9agHVG1ILgX5mc9qBxdiEaTTKMchkxrtYNkCSQt65pOedm99x0RpYzsN2B3T833YFpgpEpStQto4i0bssNBhdJgqjaj1UOfk7ZPCKcqM3Yfl45Jslra1jdkdUbWO6g1ztpjYQIgSbKtF84ATdaQi5iIjpYiBZKg1aeKeeC1v31x6vhNdgsOYG7T4q1ohAG6tse8fqhZ9ruhXcR+qhPTKfvhZ6DHN/E2Ol5Ieq79Vk6GFCFDrZHXR4suLbOV8wmNF3HHVZQ567n2l0bCsnmQbzrf/12E+PpF2e4/Lwc3l1ews3oCUnEX251fVHYVUHJBeGYpRKM0LUlN0r6D0+XDlBMeEWiDzxceLMwKbK6MMHNB51ouC9kcczmgH1p63bd26I3alBNQNPJ8QQAKgwmYT+PFf+zcpqKfRVi2xQNKAz1AZVRUQiIMSfBMeZUEDBmnoIdhS7zmiv3BeY0k5SlR0VoCwhEjE9QSpfoaCIAqgKmnzvHuKQBACRP+2vudkRLRGFknD4hvFBh4AQT+gQxPZ7tBRTI5CJTteQLZFxFGB68YxQk6rDpR/HLovZ+ELcEyPtBfD+I/9lBzBDXAuoHyGJkm9jyiB1a9kW/h2zfcd2ud+G6Praw7/oO8XqYEPXgO+jR9Yr2No5Yo5TeBwOev1clmErGF0rbqIeyZZum5XW35cD1/Db+LcafvCX6dv+E09nfw+XH+XA0W4BrlcTb6BvgYpjH8bj65RWpDXfox5Ja9b0YvhfDd1Xy/6gSrYdUE1dNDI4uU3CQA1LLSd3pUdFSQq0FnyHjrYAkoDMl1zKxleUcNBwRLZbGi44qFIzP6uXf7AZ2jikqlj1ScicclD+fKVk5I4vGHApbGPC3EuwYWhDkM/Ca7WtDdhu2RVJAr77mVApWEpQ0NliDgSRibNaYRIOBhAqRk4OE9G5nNYcwVko9zRO/bJ8O0ee7aUs02IiEoJMUOvC3O47dTDVBYaw6O5+jFEfNKVf4wij/01lFk8YmKixQ+DHzG3OoqZAmKAwQFGX/KsenWKUYFX6LkJPwJBMVvibUk5WmedXmKYKaDlpnCSluzLjCG0UVwRilkxzGTY0ZawLIrmrsJ2i5d7yxeyevGYBOTWs49fPTCtWaQ1lYThjg/DZ2eINiTahHFk3TvDm90G7r0XZTtgq+taVPpNwYx9a0P2K+rLqdwljX07JYV8xN/a+q9cq+Tf5q/HdCaPX4UBjFTxmSkapAsIxq25oaW8GNwkcwi+l0OkVEkJ6rJYSfcCpLNNAgjiOYJTW1r1jhQYkkSOpZXajMC6BJihkKGseyJgCyMmVNbSzRm3nOoMVrbJgGb7IlFMbvkqXNa+SaYZM3ZZKGFF5s7Z927i6YWyTFBwHv3MgZiuMz2JWSYgr7FOa+KmMg+kjzqJRoMB/empQzypjAlm0cZU5KfGGUY+SAZDFbnPTGaoNCvasRmHFoH+yO0+1Z8B7h+fvzP+6CgBWjHAAA", "encoding": "utf-8"}, "headers": {"Status": ["200 OK"], "X-RateLimit-Remaining": ["9"], "X-GitHub-Media-Type": ["github.cloak-preview"], "X-Runtime-rack": ["0.066329"], "Content-Security-Policy": ["default-src 'none'"], "X-Content-Type-Options": ["nosniff"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "X-GitHub-Request-Id": ["BFE0:5172:2C4F2:5A0E2:5B64C901"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-XSS-Protection": ["1; mode=block"], "Server": ["GitHub.com"], "X-RateLimit-Limit": ["10"], "Cache-Control": ["no-cache"], "Date": ["Fri, 03 Aug 2018 21:28:33 GMT"], "Access-Control-Allow-Origin": ["*"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Frame-Options": ["deny"], "Content-Encoding": ["gzip"], "X-RateLimit-Reset": ["1533331773"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "recorded_at": "2018-08-03T21:28:33"}], "recorded_with": "betamax/0.8.1"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.cloak-preview, application/vnd.github.v3.full.text-match+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA+1ZbW/bNhD+K4a/LrEkS7YlA0VXNFnRDUrRzO2aDIVBUZRFWxIFkoprC/nvO1KybAd1Y8vd9iUIECeynof3wjveHcuuZBIlU8yKTHbH1kWXZpileUIkmXIiikSK7jhCiSDwlSQp/Pd32S140h13YylzMTYMlNPejMq4CHqANTjJmTAYlgwjafyZM5Zd/pHRiBjwbUqlMILAwdgN7aBPLCewUTQIo6HnuaNhGDpRZCMcmmYwiroXXREjWOkEQMZCMqUhgPyru+HHvieDvxLTn/hL/+p64M9zeje/nd+t44Xff7+6n1ybd/Ob+H7+eXE/vxt8mMzsm/R2cfPuPvX7v83vr94s7+ZhCpLEMk2m+4rvKH1Y3VO0VQYimRRP1vnXDKw9ohYE/SrndMetvQvWaONhVMiYcbVuiCQBv/VNy7k0+5emM7Gcse2O7eGl6Y5NE4TMUKpemcSk86HaYPCQpIiq/Vj74NeMLWPCidqM3ceNYpJ8bw2rP7EGY8sdDwZnrZESIdBMifaWE1CjI+QqIaKHheigLOwUuVIu7Nxev7nyr0FmyQm8fZatFYUw0ND2iDeMPNN2R94oCtCQmM7QizwHOYGJsTPwIjR0m1g6GlCbDrZHkx4seLb1V8JmNNvaHVZQcTdw7b4NiWQ3ED8OP3+5SfD8k+Ov36/9j69ewcvoAUnEn251/VDYdUIpBOGYZRKE0LmlMCr61w+vHKCY8ZpEBzw8+GFiUmRNYoKXj4poeC9iScKWgH0q637e26M3GlBDQLPZ6QQAKg0mYT9PFf+jUpqK5zLEvigaUBrqAzKjohBgY07CU8SpISDMMgM5Sp3mNVcRCMxpLinLTrLQHhCIGJ+hjK7RyUQAVAlMnzunqKQBACQPz+fcfYtWiNLIOX1AeKXMwAkm9AFsejrbEyiQyVWucskn8LiyMBy8UxSmKtj0Ufw0qb0E4l4B8hKIL4H4nwVijrguoH5CWYxsE1sesSPLHg0HyA4c1+17I9cNsIUDN3CIN8CEqIPvqKPrO7W3ccIaVel9NODxa52CqWR8pWobdShbtmlaXn+/HLhefkh+T/A7b42+3D7gbPHNX79Z+pPFClSrS7ydvgEeRkWSTOtvvlNqwxv6WFKrviTDl2T4UpX8P1WJrodUE1dPDE5OUxDIIWnKSd3pUdFRhVoHPiPGOyFJoc6UXJeJnbzgUMMR0WFZsuqpRMH4oln+h93AwTFFzfJMKXkQDpU/X6iyckFWrTkUtjTgd12wY2hBUMBAa/ZcG3JYsD2SEnr1LaeqYCVBaWuBNRhIYsYWrUk0GEioEAU5qpA+rKzmEMamUs+KNKjap2Pq88O0FRpkRELQWQYd+I87jsNMDUFpbDq7gKMMx+0pN/jSqP7SXkWz1iIqLFAECQtac6ipkCYoDSgoqv5VTs+RSjEq/B4hJ9FZIip8Q6gnK239qsVTBA0dtM4SXNyacYM3ytqCCcpmBYybWjM2BOBd1djP0PrZ8cbhnbxlADo1reE0KM5LVFsOJWE1YYD4ba3wDsWWUI8s2rp5d3qh1daj7bZsNXxvS59JuTOObWh/xnxZdTulsc2nVbKumdvqX2frjXy7/PX47wzT6vGhMMpfciRjlYFgGdW2tRW2hhtlgGAW0+v1ypggPVdLCT8jKis00CCOY5gltZWv3OChEkmR1LO6SIkXQpOUMBS2tmVDAGSVy9rKWKF3/ZxDi9daMA3eZUspjN8ly9rnyC3DLm/GJI0oXGw9P+08nDD3SMrXAu7cyAVKkgvYlZJiCvsU5r7KY1D0kfZWqdAgPtyaVDPKhMCWbW1lTip8aVRj5JDkCVuddWO1Q6HuagRmHNoHu+f0hwO3b6uq9Jucwi5WdZEesrBgTrBsu+/qpHrKpVy9YD2TfasTvV/f91x0c85ywiUMPCD6q0sg6EI4mimzHH8ftKuhUhmQcHekwiwLKdaqW86FNfr6WP38AyuRIHW5HQAA", "encoding": "utf-8"}, "headers": {"Status": ["200 OK"], "X-RateLimit-Remaining": ["8"], "X-GitHub-Media-Type": ["github.cloak-preview; param=full.text-match"], "X-Runtime-rack": ["0.043624"], "Content-Security-Policy": ["default-src 'none'"], "X-Content-Type-Options": ["nosniff"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "X-GitHub-Request-Id": ["B258:516C:12CE5:2A7F2:5B64C902"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-XSS-Protection": ["1; mode=block"], "Server": ["GitHub.com"], "X-RateLimit-Limit": ["10"], "Cache-Control": ["no-cache"], "Date": ["Fri, 03 Aug 2018 21:28:34 GMT"], "Access-Control-Allow-Origin": ["*"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Frame-Options": ["deny"], "Content-Encoding": ["gzip"], "X-RateLimit-Reset": ["1533331773"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "recorded_at": "2018-08-03T21:28:34"}], "recorded_with": "betamax/0.8.1"}

tests/integration/test_github.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,27 @@ def test_search_code_with_text_match(self):
523523
assert isinstance(code_result, github3.search.CodeSearchResult)
524524
assert len(code_result.text_matches) > 0
525525

526+
def test_search_commits(self):
527+
"""Test the ability to search for commits."""
528+
cassette_name = self.cassette_name('search_commits')
529+
with self.recorder.use_cassette(cassette_name):
530+
result_iterator = self.gh.search_commits(
531+
'css repo:octocat/Spoon-Knife')
532+
commit_result = next(result_iterator)
533+
534+
assert isinstance(commit_result, github3.search.CommitSearchResult)
535+
536+
def test_search_commits_with_text_match(self):
537+
"""Test the ability to search for commits with text matches."""
538+
cassette_name = self.cassette_name('search_commits_with_text_match')
539+
with self.recorder.use_cassette(cassette_name):
540+
result_iterator = self.gh.search_commits(
541+
'css repo:octocat/Spoon-Knife', text_match=True)
542+
commit_result = next(result_iterator)
543+
544+
assert isinstance(commit_result, github3.search.CommitSearchResult)
545+
assert len(commit_result.text_matches) > 0
546+
526547
def test_search_users(self):
527548
"""Test the ability to use the user search endpoint."""
528549
cassette_name = self.cassette_name('search_users')

0 commit comments

Comments
 (0)