Skip to content
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
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ API Changes
~~~~~~~~~~~

- Added avatar and avatarType to ``/organizations/{org}/`` endpoint.
- Provide commit and author information associated with a given release

Version 8.12
------------
Expand Down
117 changes: 115 additions & 2 deletions src/sentry/api/serializers/models/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,116 @@

from django.db.models import Sum


from collections import Counter, defaultdict

from sentry.api.serializers import Serializer, register, serialize
from sentry.models import Release, ReleaseProject, TagValue
from sentry.db.models.query import in_iexact
from sentry.models import Release, ReleaseCommit, ReleaseProject, TagValue, User, UserEmail


@register(Release)
class ReleaseSerializer(Serializer):
def _get_users_for_commits(self, release_commits, org_id):
"""
Returns a dictionary of author_id => user, if a Sentry
user object exists for that email. If there is no matching
Sentry user, a {user, email} dict representation of that
author is returned.

e.g.
{
1: serialized(<User id=1>),
2: {email: 'not-a-user@example.com', name: 'dunno'},
...
}
"""
authors = set(rc.commit.author for rc in release_commits if rc.commit.author is not None)
if not len(authors):
return {}

# Filter users based on the emails provided in the commits
user_emails = UserEmail.objects.filter(
in_iexact('email', [a.email for a in authors]),
).order_by('id')

# Filter users belonging to the organization associated with
# the release
users = User.objects.filter(
id__in=[ue.user_id for ue in user_emails],
sentry_orgmember_set__organization_id=org_id
)
users_by_id = dict((user.id, serialize(user)) for user in users)

# Figure out which email address matches to a user
users_by_email = {}
for user_email in user_emails:
if user_email.email in users_by_email:
pass

user = users_by_id.get(user_email.user_id)
if user:
users_by_email[user_email.email] = user

author_objs = {}
for author in authors:
author_objs[author.id] = users_by_email.get(author.email, {
"name": author.name,
"email": author.email
})

return author_objs

def _get_commit_metadata(self, item_list, user):
"""
Returns a dictionary of release_id => commit metadata,
where each commit metadata dict contains commit_count
and an array of authors.

e.g.
{
1: {
'commit_count': 3,
'authors': [<User id=1>, <User id=2>]
},
...
}

If there are no commits, returns None.
"""

release_commits = list(ReleaseCommit.objects.filter(
release__in=item_list).select_related("commit", "commit__author"))

if not len(release_commits):
return None

org_ids = set(item.organization_id for item in item_list)
assert len(org_ids) == 1
org_id = org_ids.pop()

users_by_email = self._get_users_for_commits(release_commits, org_id)
commit_count_by_release_id = Counter()
authors_by_release_id = defaultdict(dict)

for rc in release_commits:
# Accumulate authors per release
author = rc.commit.author
if author:
authors_by_release_id[rc.release_id][author.id] = \
users_by_email[author.id]

# Increment commit count per release
commit_count_by_release_id[rc.release_id] += 1

result = {}
for item in item_list:
result[item] = {
'commit_count': commit_count_by_release_id[item.id],
'authors': authors_by_release_id.get(item.id, {}).values(),
}
return result

def get_attrs(self, item_list, user, *args, **kwargs):
tags = {
tk.value: tk
Expand Down Expand Up @@ -41,13 +145,20 @@ def get_attrs(self, item_list, user, *args, **kwargs):
.values_list('release_id', 'new_groups')
)

release_metadata_attrs = self._get_commit_metadata(item_list, user)

result = {}
for item in item_list:
result[item] = {
'tag': tags.get(item.version),
'owner': owners[six.text_type(item.owner_id)] if item.owner_id else None,
'new_groups': group_counts_by_release.get(item.id) or 0
'new_groups': group_counts_by_release.get(item.id) or 0,
'commit_count': 0,
'authors': [],
}
if release_metadata_attrs:
result[item].update(release_metadata_attrs[item])

return result

def serialize(self, obj, attrs, user, *args, **kwargs):
Expand All @@ -62,6 +173,8 @@ def serialize(self, obj, attrs, user, *args, **kwargs):
'data': obj.data,
'newGroups': attrs['new_groups'],
'owner': attrs['owner'],
'commitCount': attrs.get('commit_count', 0),
'authors': attrs.get('authors', []),
}
if attrs['tag']:
d.update({
Expand Down
40 changes: 40 additions & 0 deletions src/sentry/static/sentry/app/components/releaseStats.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import Avatar from './avatar';
import TooltipMixin from '../mixins/tooltip';
import {t} from '../locale';

const ReleaseStats = React.createClass({
propTypes: {
release: React.PropTypes.object,
},

mixins: [
TooltipMixin({
selector: '.tip'
}),
],

render() {
let release = this.props.release;
let commitCount = release.commitCount;
let authorCount = release.authors.length;
if (commitCount === 0) {
return null;
}
return (
<div className="release-info">
<div><b>{commitCount}{t(' commits by ')}{authorCount}{t(' authors')}</b></div>
{release.authors.map(author => {
return (
<span className="assignee-selector tip"
title={author.name + ' ' + author.email}>
<Avatar user={author}/>
</span>
);
})}
</div>
);
}
});

export default ReleaseStats;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import ReleaseStats from '../../components/releaseStats';
import Count from '../../components/count';
import TimeSince from '../../components/timeSince';
import Version from '../../components/version';
Expand All @@ -19,12 +20,15 @@ const ReleaseList = React.createClass({
return (
<li className="release" key={release.version}>
<div className="row">
<div className="col-sm-8 col-xs-6">
<div className="col-sm-6 col-xs-4">
<h4><Version orgId={orgId} projectId={projectId} version={release.version} /></h4>
<div className="release-meta">
<span className="icon icon-clock"></span> <TimeSince date={release.dateCreated} />
</div>
</div>
<div className="col-sm-2 col-xs-2">
<ReleaseStats release={release}/>
</div>
<div className="col-sm-2 col-xs-3 release-stats stream-count">
<Count className="release-count" value={release.newGroups} />
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/sentry/static/sentry/app/views/releaseDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import DocumentTitle from 'react-document-title';
import ListLink from '../components/listLink';
import LoadingError from '../components/loadingError';
import LoadingIndicator from '../components/loadingIndicator';
import ReleaseStats from '../components/releaseStats';
import ProjectState from '../mixins/projectState';
import TimeSince from '../components/timeSince';
import Version from '../components/version';
Expand Down Expand Up @@ -92,18 +93,20 @@ const ReleaseDetails = React.createClass({

let release = this.state.release;
let {orgId, projectId} = this.props.params;

return (
<DocumentTitle title={this.getTitle()}>
<div>
<div className="release-details">
<div className="row">
<div className="col-sm-6 col-xs-12">
<div className="col-sm-4 col-xs-12">
<h3>{t('Release')} <strong><Version orgId={orgId} projectId={projectId} version={release.version} anchor={false} /></strong></h3>
<div className="release-meta">
<span className="icon icon-clock"></span> <TimeSince date={release.dateCreated} />
</div>
</div>
<div className="col-sm-2 hidden-xs">
<ReleaseStats release={release}/>
</div>
<div className="col-sm-2 hidden-xs">
<div className="release-stats">
<h6 className="nav-header">{t('New Issues')}</h6>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import LoadingIndicator from '../../components/loadingIndicator';
import LoadingError from '../../components/loadingError';

import ApiMixin from '../../mixins/apiMixin';

const ReleaseCommit = React.createClass({
Expand Down Expand Up @@ -72,8 +71,9 @@ const ReleaseCommits = React.createClass({
<div className="panel-heading panel-heading-bold">
<div className="row">
<div className="col-sm-2 col-xs-2">{'SHA'}</div>
<div className="col-sm-7 col-xs-7">{'Message'}</div>
<div className="col-sm-3 col-xs-3 align-right">{'Date'}</div>
<div className="col-sm-5 col-xs-5">{'Message'}</div>
<div className="col-sm-3 col-xs-3 align-right actions">{'Date'}</div>
<div className="col-sm-2 col-xs-2 align-right actions">{'Author'}</div>
</div>
</div>
<ul className="list-group commit-list">
Expand Down
Loading