Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link to commit of latest version if known #874

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""add ProjectVersion.commit_url

Revision ID: 16aa7da2764c
Revises: 314651690dc7
Create Date: 2019-12-19 14:54:56.036040
"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "16aa7da2764c"
down_revision = "314651690dc7"


def upgrade():
op.add_column(
"projects_versions",
sa.Column("commit_url", sa.String(length=200), nullable=True),
)


def downgrade():
with op.batch_alter_table("projects_versions") as batch_op:
batch_op.drop_column("commit_url")
14 changes: 14 additions & 0 deletions anitya/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ def create_version_objects(self, versions):
if isinstance(version, dict) and "cursor" in version
else None
),
commit_url=(
version["commit_url"]
if isinstance(version, dict) and "commit_url" in version
else None
),
)
for version in versions
]
Expand Down Expand Up @@ -385,12 +390,20 @@ def get_sorted_version_objects(self):
prefix=self.version_prefix,
created_on=v_obj.created_on,
pattern=self.version_pattern,
commit_url=v_obj.commit_url,
)
for v_obj in self.versions_obj
]
sorted_versions = list(reversed(sorted(versions)))
return sorted_versions

@property
def latest_version_object(self):
sorted_versions = self.get_sorted_version_objects()
if sorted_versions:
return sorted_versions[0]
return None

def get_version_class(self):
"""
Get the class for the version scheme used by this project.
Expand Down Expand Up @@ -633,6 +646,7 @@ class ProjectVersion(Base):
)
version = sa.Column(sa.String(50), primary_key=True)
created_on = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
commit_url = sa.Column(sa.String(200), nullable=True)

project = sa.orm.relationship(
"Project", backref=sa.orm.backref("versions_obj", cascade="all, delete-orphan")
Expand Down
126 changes: 66 additions & 60 deletions anitya/lib/backends/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,57 @@ def get_version_url(cls, project):

return url

@classmethod
def _retrieve_versions(cls, owner, repo, project, cursor=None):
query = prepare_query(owner, repo, project.releases_only, cursor=cursor)

try:
headers = REQUEST_HEADERS.copy()
token = config["GITHUB_ACCESS_TOKEN"]
if token:
headers["Authorization"] = "bearer %s" % token
resp = http_session.post(
API_URL,
json={"query": query},
headers=headers,
timeout=60,
verify=True,
)
except Exception as err:
_log.debug("%s ERROR: %s" % (project.name, str(err)))
raise AnityaPluginException(
'Could not call : "%s" of "%s", with error: %s'
% (API_URL, project.name, str(err))
) from err

if resp.ok:
json = resp.json()
elif resp.status_code == 403:
_log.info("Github API ratelimit reached.")
raise RateLimitException(reset_time)
else:
raise AnityaPluginException(
'%s: Server responded with status "%s": "%s"'
% (project.name, resp.status_code, resp.reason)
)

# Check for invalid cursor errors, we don't want to error out
# immediately in this case but repeat without specifying a cursor.
if (
cursor
and "errors" in json
and all(
elem.get("type") == "INVALID_CURSOR_ARGUMENTS"
or "invalid cursor" in elem.get("message", "").lower()
for elem in json["errors"]
)
):
return None

versions = parse_json(json, project)
_log.debug(f"Retrieved versions: {versions}")
return versions

@classmethod
def get_versions(cls, project):
""" Method called to retrieve all the versions (that can be found)
Expand Down Expand Up @@ -132,65 +183,17 @@ def get_versions(cls, project):
)
)

if project.latest_version_cursor:
# If we know about the cursor of the latest version, attempt to
# limit results to anything after it. Only if that fails, try
# without one.
cursor_attempts = (project.latest_version_cursor, None)
else:
cursor_attempts = (None,)

for cursor in cursor_attempts:
query = prepare_query(owner, repo, project.releases_only, cursor=cursor)

try:
headers = REQUEST_HEADERS.copy()
token = config["GITHUB_ACCESS_TOKEN"]
if token:
headers["Authorization"] = "bearer %s" % token
resp = http_session.post(
API_URL,
json={"query": query},
headers=headers,
timeout=60,
verify=True,
)
except Exception as err:
_log.debug("%s ERROR: %s" % (project.name, str(err)))
raise AnityaPluginException(
'Could not call : "%s" of "%s", with error: %s'
% (API_URL, project.name, str(err))
) from err

if resp.ok:
json = resp.json()
elif resp.status_code == 403:
_log.info("Github API ratelimit reached.")
raise RateLimitException(reset_time)
else:
raise AnityaPluginException(
'%s: Server responded with status "%s": "%s"'
% (project.name, resp.status_code, resp.reason)
)

# Check for invalid cursor errors, we don't want to error out
# immediately in this case but repeat without specifying a cursor.
if (
cursor
and "errors" in json
and all(
elem.get("type") == "INVALID_CURSOR_ARGUMENTS"
or "invalid cursor" in elem.get("message", "").lower()
for elem in json["errors"]
)
):
# unset faulty cursor for now
project.latest_version_cursor = None
continue
# If we know about the cursor of the latest version, attempt to
# limit results to anything after it.
versions = cls._retrieve_versions(
owner, repo, project, cursor=project.latest_version_cursor
)

versions = parse_json(json, project)
_log.debug(f"Retrieved versions: {versions}")
break
if versions is None:
# Either a previous version cursor wasn't known, or turned out to
# be invalid. Unset it for the latter case.
project.latest_version_cursor = None
versions = cls._retrieve_versions(owner, repo, project)

if len(versions) == 0:
raise AnityaPluginException(
Expand Down Expand Up @@ -262,9 +265,12 @@ def parse_json(json, project):
version = {"cursor": edge["cursor"]}

if project.releases_only:
version["version"] = edge["node"]["tag"]["name"]
hook = edge["node"]["tag"]
else:
version["version"] = edge["node"]["name"]
hook = edge["node"]

version["version"] = hook["name"]
version["commit_url"] = hook["target"]["commitUrl"]
versions.append(version)

return versions
Expand Down
4 changes: 3 additions & 1 deletion anitya/lib/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ def check_project_release(project, session, test=False):
if len(version.version) < version_column_len:
project.versions_obj.append(
models.ProjectVersion(
project_id=project.id, version=version.version
project_id=project.id,
version=version.version,
commit_url=version.commit_url,
)
)
else:
Expand Down
3 changes: 3 additions & 0 deletions anitya/lib/versions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(
created_on: Optional[datetime] = None,
pattern: Optional[str] = None,
cursor: Optional[str] = None,
commit_url: Optional[str] = None,
):
"""
Constructor of Version class.
Expand All @@ -57,6 +58,7 @@ def __init__(
pattern: Calendar version pattern.
See `Calendar version scheme_` for more information.
cursor: An opaque, backend-specific cursor pointing to the version.
commit_url: A URL pointing to the commit tagged as the version.

.. _Calendar version scheme:
https://calver.org/#scheme
Expand All @@ -79,6 +81,7 @@ def __init__(
else:
self.pattern = None
self.cursor = cursor
self.commit_url = commit_url

def __str__(self):
"""
Expand Down
6 changes: 5 additions & 1 deletion anitya/templates/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ <h3 class="panel-title inline" property="doap:name">{{ project.name }}</h3>
<h4 class="list-group-item-heading">Latest version</h4>
<div class="list-group-item-text">
{% if project.versions %}
<div property="doap:release" typeof="doap:Version">{{ project.latest_version }}</div>
{% if project.latest_version_object and project.latest_version_object.commit_url %}
<div property="doap:release" typeof="doap:Version">{{ project.latest_version }} (<a href="{{ project.latest_version_object.commit_url }}">commit</a>)</div>
{% else %}
<div property="doap:release" typeof="doap:Version">{{ project.latest_version }}</div>
{% endif %}
{% else %}
<p id="version_info">No version available for this package</p>
{% endif %}
Expand Down
34 changes: 34 additions & 0 deletions anitya/tests/db/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,40 @@ def test_get_sorted_version_objects(self):
self.assertEqual(versions[0].version, version_second.version)
self.assertEqual(versions[1].version, version_first.version)

def test_latest_version_object_with_versions(self):
"""Test the latest_version_object property with versions."""
project = models.Project(
name="test",
homepage="https://example.com",
backend="custom",
ecosystem_name="pypi",
version_scheme="RPM",
)
self.session.add(project)
self.session.flush()

version_first = models.ProjectVersion(project_id=project.id, version="0.8")
version_second = models.ProjectVersion(project_id=project.id, version="1.0")
self.session.add(version_first)
self.session.add(version_second)
self.session.flush()

self.assertEqual(project.latest_version_object.version, version_second.version)

def test_latest_version_object_without_versions(self):
"""Test the latest_version_object property without versions."""
project = models.Project(
name="test",
homepage="https://example.com",
backend="custom",
ecosystem_name="pypi",
version_scheme="RPM",
)
self.session.add(project)
self.session.flush()

self.assertIsNone(project.latest_version_object)

def test_get_version_class(self):
project = models.Project(
name="test",
Expand Down
20 changes: 18 additions & 2 deletions anitya/tests/lib/backends/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,29 @@ def test_parse_json(self):
"repository": {
"refs": {
"totalCount": 1,
"edges": [{"cursor": "cUrSoR", "node": {"name": "1.0"}}],
"edges": [
{
"cursor": "cUrSoR",
"node": {
"name": "1.0",
"target": {
"commitUrl": "https://foobar.com/foo/bar/commits/12345678"
},
},
}
],
},
},
"rateLimit": {"limit": 5000, "remaining": 5000, "resetAt": "dummy"},
}
}
exp = [{"cursor": "cUrSoR", "version": "1.0"}]
exp = [
{
"cursor": "cUrSoR",
"version": "1.0",
"commit_url": "https://foobar.com/foo/bar/commits/12345678",
}
]
obs = backend.parse_json(json, self.project)
self.assertEqual(exp, obs)

Expand Down
24 changes: 24 additions & 0 deletions anitya/tests/lib/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,30 @@ def test_check_project_release_versions_with_cursor(self, mock_method):

self.assertEqual(project.latest_version_cursor, "Hbgf")

@mock.patch(
"anitya.lib.backends.github.GithubBackend.get_versions",
return_value=[
{"version": "1.0.0", "commit_url": "https://example.com/tags/1.0.0"},
{"version": "0.9.9", "commit_url": "https://example.com/tags/0.9.9"},
{"version": "0.9.8", "commit_url": "https://example.com/tags/0.9.8"},
],
)
def test_check_project_release_with_versions_with_urls(self, mock_method):
"""Test check_project_release() with versions with URLs."""
with fml_testing.mock_sends(anitya_schema.ProjectCreated):
project = utilities.create_project(
self.session,
name="project_name",
homepage="https://not-a-real-homepage.com",
backend="GitHub",
user_id="[email protected]",
)
with fml_testing.mock_sends(anitya_schema.ProjectVersionUpdated):
utilities.check_project_release(project, self.session)

for vo in project.versions_obj:
self.assertEqual(vo.commit_url, "https://example.com/tags/" + vo.version)

@mock.patch(
"anitya.lib.backends.npmjs.NpmjsBackend.get_versions",
return_value=["1.0.0", "0.9.9", "0.9.8"],
Expand Down
1 change: 1 addition & 0 deletions news/PR874.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Link to commit of latest version if known