Skip to content

Commit ea4101a

Browse files
Restore commit logs for Git/GitHub projects
1 parent 7581ed1 commit ea4101a

4 files changed

Lines changed: 163 additions & 10 deletions

File tree

‎codespeed/commits/git.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,34 @@ def updaterepo(project, update=True):
4242
return [{'error': False}]
4343

4444

45+
def get_commit_date(project, commit_id):
46+
"""Author date of `commit_id` as a naive UTC datetime, or None."""
47+
updaterepo(project, update=False)
48+
fmt = '%ct' if getattr(settings, 'GIT_USE_COMMIT_DATE', False) else '%at'
49+
cmd = ['git', 'log', '-1', '--format=' + fmt, commit_id]
50+
p, stdout, stderr = execute_command(cmd, project.working_copy)
51+
if p.returncode != 0 or not stdout.strip():
52+
logger.warning("%s returned %s: %s", " ".join(cmd), p.returncode, stderr)
53+
return None
54+
return datetime.datetime.utcfromtimestamp(int(stdout.strip()))
55+
56+
57+
class Git:
58+
"""Git backend with the same interface as commits.mercurial.Mercurial."""
59+
60+
def __init__(self, project):
61+
self.project = project
62+
63+
def update_repo(self, update=True):
64+
return updaterepo(self.project, update=update)
65+
66+
def get_logs(self, endrev, startrev):
67+
return getlogs(endrev, startrev)
68+
69+
def get_commit_date(self, commit_id):
70+
return get_commit_date(self.project, commit_id)
71+
72+
4573
def getlogs(endrev, startrev):
4674
updaterepo(endrev.branch.project, update=False)
4775

‎codespeed/commits/github.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ def retrieve_revision(commit_id, username, project, revision=None):
126126
'tag': tag}
127127

128128

129+
def get_commit_date(project, commit_id):
130+
"""Committer date of `commit_id` from the GitHub API, or None."""
131+
m = GITHUB_URL_RE.match(project.repo_path.rstrip('/'))
132+
if not m:
133+
raise CommitLogError(
134+
"Unable to parse Github URL %s" % project.repo_path)
135+
data = retrieve_revision(commit_id, m.group("username"), m.group("project"))
136+
return data['date'] if data else None
137+
138+
129139
def getlogs(endrev, startrev):
130140
if endrev != startrev:
131141
revisions = endrev.branch.revisions.filter(

‎codespeed/commits/logs.py‎

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,54 @@
44
import datetime
55
import logging
66

7+
from .exceptions import CommitLogError
8+
79
logger = logging.getLogger(__name__)
810

911

12+
class FunctionBackend:
13+
"""Adapter giving the function-based backends (GitHub, Subversion) the
14+
same interface as the Mercurial and Git classes."""
15+
16+
def __init__(self, project, module):
17+
self.project = project
18+
self.module = module
19+
20+
def update_repo(self, update=True):
21+
# These backends talk to a remote service; there is no clone to
22+
# create, so only "update" means anything.
23+
if update:
24+
return self.module.updaterepo(self.project)
25+
26+
def get_logs(self, endrev, startrev):
27+
return self.module.getlogs(endrev, startrev)
28+
29+
def get_commit_date(self, commit_id):
30+
lookup = getattr(self.module, 'get_commit_date', None)
31+
if lookup is None:
32+
return None # not supported; callers fall back to today's date
33+
return lookup(self.project, commit_id)
34+
35+
1036
def get_scm(project):
11-
if project.repo_type == project.SUBVERSION:
12-
from .subversion import getlogs, updaterepo
13-
elif project.repo_type == project.MERCURIAL:
37+
"""Return a backend object for the project, or None if logs are off.
38+
39+
Every backend has update_repo(update), get_logs(endrev, startrev) and
40+
get_commit_date(commit_id).
41+
"""
42+
if project.repo_type == project.MERCURIAL:
1443
from .mercurial import Mercurial
1544
return Mercurial(project)
1645
elif project.repo_type == project.GIT:
17-
from .git import getlogs, updaterepo
46+
from .git import Git
47+
return Git(project)
1848
elif project.repo_type == project.GITHUB:
19-
from .github import getlogs, updaterepo # noqa: F401
20-
else:
21-
return None
49+
from . import github
50+
return FunctionBackend(project, github)
51+
elif project.repo_type == project.SUBVERSION:
52+
from . import subversion
53+
return FunctionBackend(project, subversion)
54+
return None
2255

2356

2457
def get_logs(rev, startrev, update=False):
@@ -33,7 +66,7 @@ def get_logs(rev, startrev, update=False):
3366
return logs
3467

3568
if update:
36-
scm.update_repo(project)
69+
scm.update_repo()
3770

3871
logs = scm.get_logs(rev, startrev)
3972

@@ -49,7 +82,13 @@ def _get_commit_date(project, commit_id):
4982
if scm is None:
5083
return None
5184

52-
return scm.get_commit_date(commit_id)
85+
try:
86+
return scm.get_commit_date(commit_id)
87+
except CommitLogError as e:
88+
# A missing clone or unknown commit must not break result submission
89+
logger.warning("Could not get the date of commit %r of project %s: %s",
90+
commit_id, project, e)
91+
return None
5392

5493

5594
def get_commit_date(project, commit_id):

‎codespeed/commits/tests/test_git.py‎

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from django.test import TestCase, override_settings
33
from unittest.mock import Mock, patch
44

5-
from codespeed.commits.git import getlogs
5+
from codespeed import commits
6+
from codespeed.commits.git import Git, getlogs
7+
from codespeed.commits.logs import FunctionBackend, get_scm
68
from codespeed.models import Project, Revision, Branch, Environment
79

810

@@ -58,3 +60,77 @@ def side_effect(cmd, *args, **kwargs):
5860
'tag': 'tag',
5961
}
6062
self.assertEqual([expected], logs)
63+
64+
@patch("codespeed.commits.git.Popen")
65+
def test_logs_through_dispatcher(self, popen):
66+
"""commits.get_logs must reach the git backend for a Git project."""
67+
ret = Mock()
68+
ret.returncode = 0
69+
ret.communicate.return_value = (
70+
b"id\x00long_id\x001583489681\x00author\x00email\x00msg\x00\x1e", b'')
71+
popen.return_value = ret
72+
73+
logs = commits.get_logs(self.revision, self.revision)
74+
75+
self.assertEqual(len(logs), 1)
76+
self.assertEqual(logs[0]['commitid'], 'long_id')
77+
78+
@patch("codespeed.commits.git.Popen")
79+
def test_get_commit_date(self, popen):
80+
ret = Mock()
81+
ret.returncode = 0
82+
ret.communicate.return_value = (b"1583489681\n", b'')
83+
popen.return_value = ret
84+
85+
date = commits.get_commit_date(self.project, 'long_id')
86+
87+
self.assertEqual(date, datetime(2020, 3, 6, 10, 14, 41))
88+
cmd = popen.call_args_list[-1][0][0]
89+
self.assertEqual(cmd[:3], ['git', 'log', '-1'])
90+
self.assertEqual(cmd[-1], 'long_id')
91+
92+
@patch("codespeed.commits.git.Popen")
93+
def test_get_commit_date_unknown_commit_falls_back(self, popen):
94+
ret = Mock()
95+
ret.returncode = 128
96+
ret.communicate.return_value = (b'', b'fatal: bad object')
97+
popen.return_value = ret
98+
99+
date = commits.get_commit_date(self.project, 'nope')
100+
101+
# Unknown commit: today rather than an exception, as before
102+
self.assertEqual(date.date(), datetime.today().date())
103+
104+
105+
class GetScmTest(TestCase):
106+
def make(self, repo_type, path='https://github.com/python/cpython'):
107+
return Project.objects.create(name='p-%s' % repo_type, repo_path=path,
108+
repo_type=repo_type)
109+
110+
def test_git(self):
111+
self.assertIsInstance(get_scm(self.make(Project.GIT)), Git)
112+
113+
def test_github_and_subversion_get_an_adapter(self):
114+
for repo_type in (Project.GITHUB, Project.SUBVERSION):
115+
self.assertIsInstance(get_scm(self.make(repo_type)), FunctionBackend)
116+
117+
def test_subversion_has_no_commit_date(self):
118+
self.assertIsNone(get_scm(self.make(Project.SUBVERSION)).get_commit_date('1'))
119+
120+
@patch("codespeed.commits.github.fetch_json")
121+
def test_github_commit_date_from_api(self, fetch_json):
122+
def fake(url):
123+
if url.endswith('/git/refs/tags'):
124+
return []
125+
return {'sha': 'abc', 'parents': [], 'message': 'm',
126+
'committer': {'date': '2020-03-06T10:14:41Z'},
127+
'author': {'name': 'a', 'email': 'a@example.com'}}
128+
fetch_json.side_effect = fake
129+
date = commits.get_commit_date(self.make(Project.GITHUB), 'abc')
130+
self.assertEqual(date.isoformat(), '2020-03-06T10:14:41+00:00')
131+
self.assertIn('/repos/python/cpython/git/commits/abc',
132+
fetch_json.call_args_list[0][0][0])
133+
134+
def test_no_logs(self):
135+
self.assertIsNone(get_scm(self.make(Project.NO_LOGS)))
136+
self.assertEqual(commits.get_logs.__name__, 'get_logs')

0 commit comments

Comments
 (0)