From: dklawren Date: Wed, 14 Nov 2018 22:58:39 +0000 (-0500) Subject: Bug 1489706 - Add section to show_bug displaying the status of associated revisions X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=70f89f1aff0350c2f81da19af67889a8e8dd5d36;p=thirdparty%2Fbugzilla.git Bug 1489706 - Add section to show_bug displaying the status of associated revisions --- diff --git a/extensions/PhabBugz/Extension.pm b/extensions/PhabBugz/Extension.pm index c857c60ab..af12985ce 100644 --- a/extensions/PhabBugz/Extension.pm +++ b/extensions/PhabBugz/Extension.pm @@ -14,10 +14,30 @@ use warnings; use parent qw(Bugzilla::Extension); use Bugzilla::Constants; -use Bugzilla::Extension::PhabBugz::Feed; +use Bugzilla::Extension::PhabBugz::Constants; our $VERSION = '0.01'; +sub template_before_process { + my ( $self, $args ) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + return unless Bugzilla->params->{phabricator_enabled}; + return unless Bugzilla->params->{phabricator_base_uri}; + return unless $file =~ /bug_modal\/(header|edit).html.tmpl$/; + + if ( my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'} ) { + my $has_revisions = 0; + foreach my $attachment ( @{ $bug->attachments } ) { + next if $attachment->contenttype ne PHAB_CONTENT_TYPE; + $has_revisions = 1; + last; + } + $vars->{phabricator_revisions} = $has_revisions; + } +} + sub config_add_panels { my ($self, $args) = @_; my $modules = $args->{panel_modules}; diff --git a/extensions/PhabBugz/lib/Revision.pm b/extensions/PhabBugz/lib/Revision.pm index 41a158ac8..6683b9348 100644 --- a/extensions/PhabBugz/lib/Revision.pm +++ b/extensions/PhabBugz/lib/Revision.pm @@ -36,6 +36,7 @@ has status => ( is => 'ro', isa => Str ); has creation_ts => ( is => 'ro', isa => Str ); has modification_ts => ( is => 'ro', isa => Str ); has author_phid => ( is => 'ro', isa => Str ); +has diff_phid => ( is => 'ro', isa => Str ); has bug_id => ( is => 'ro', isa => Str ); has view_policy => ( is => 'ro', isa => Str ); has edit_policy => ( is => 'ro', isa => Str ); @@ -65,11 +66,21 @@ has subscribers_raw => ( ] ); has projects_raw => ( - is => 'ro', + is => 'ro', isa => Dict [ projectPHIDs => ArrayRef [Str] ] ); +has reviewers_extra_raw => ( + is => 'ro', + isa => ArrayRef [ + Dict [ + reviewerPHID => Str, + voidedPHID => Maybe [Str], + diffPHID => Maybe [Str] + ] + ] +); sub new_from_query { my ( $class, $params ) = @_; @@ -109,12 +120,14 @@ sub BUILDARGS { $params->{creation_ts} = $params->{fields}->{dateCreated}; $params->{modification_ts} = $params->{fields}->{dateModified}; $params->{author_phid} = $params->{fields}->{authorPHID}; + $params->{diff_phid} = $params->{fields}->{diffPHID}; $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'}; $params->{view_policy} = $params->{fields}->{policy}->{view}; $params->{edit_policy} = $params->{fields}->{policy}->{edit}; $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers} // []; $params->{subscribers_raw} = $params->{attachments}->{subscribers}; $params->{projects_raw} = $params->{attachments}->{projects}; + $params->{reviewers_extra_raw} = $params->{attachments}->{'reviewers-extra'}->{'reviewers-extra'} // []; $params->{subscriber_count} = $params->{attachments}->{subscribers}->{subscriberCount}; @@ -321,14 +334,28 @@ sub _build_reviews { } ); - return [ - map { - { - user => $_, - status => $by_phid{ $_->phid }{status}, + my @reviewers; + foreach my $user (@{ $users }) { + my $reviewer_data = { + user => $user, + status => $by_phid{ $user->phid }{status} + }; + # Set to accepted-prior if the diffs reviewer are different and the reviewer status is accepted + foreach my $reviewer_extra (@{$self->reviewers_extra_raw}) { + if ($reviewer_extra->{reviewerPHID} eq $user->phid) { + if ($reviewer_extra->{diffPHID}) { + if ( $reviewer_data->{status} eq 'accepted' + && $reviewer_extra->{diffPHID} ne $self->diff_phid) + { + $reviewer_data->{status} = 'accepted-prior'; + } + } } - } @$users - ]; + } + push @reviewers, $reviewer_data; + } + + return \@reviewers; } sub _build_subscribers { diff --git a/extensions/PhabBugz/lib/WebService.pm b/extensions/PhabBugz/lib/WebService.pm index 19a758a70..36a3f0097 100644 --- a/extensions/PhabBugz/lib/WebService.pm +++ b/extensions/PhabBugz/lib/WebService.pm @@ -13,24 +13,31 @@ use warnings; use base qw(Bugzilla::WebService); +use Bugzilla::Bug; use Bugzilla::Constants; use Bugzilla::Error; +use Bugzilla::Logging; use Bugzilla::User; use Bugzilla::Util qw(detaint_natural trick_taint); use Bugzilla::WebService::Constants; +use Types::Standard qw(-types slurpy); +use Type::Params qw(compile); use Bugzilla::Extension::PhabBugz::Constants; +use Bugzilla::Extension::PhabBugz::Revision; +use Bugzilla::Extension::PhabBugz::Util qw(request); -use List::Util qw(first); -use List::MoreUtils qw(any); use MIME::Base64 qw(decode_base64); +use Try::Tiny; use constant READ_ONLY => qw( + bug_revisions check_user_enter_bug_permission check_user_permission_for_bug ); use constant PUBLIC_METHODS => qw( + bug_revisions check_user_enter_bug_permission check_user_permission_for_bug set_build_target @@ -122,7 +129,7 @@ sub set_build_target { trick_taint($build_target); Bugzilla->dbh->do( - "INSERT INTO phabbugz (name, value) VALUES (?, ?)", + 'INSERT INTO phabbugz (name, value) VALUES (?, ?)', undef, 'build_target_' . $revision_id, $build_target @@ -131,6 +138,112 @@ sub set_build_target { return { result => 1 }; } +sub bug_revisions { + state $check = compile(Object, Dict[bug_id => Int]); + my ( $self, $params ) = $check->(@_); + + $self->_check_phabricator(); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Validate that a bug id and user id are provided + ThrowUserError('phabricator_invalid_request_params') + unless $params->{bug_id}; + + # Validate that the user can see the bug itself + my $bug = Bugzilla::Bug->check( { id => $params->{bug_id}, cache => 1 } ); + + my @revision_ids; + foreach my $attachment ( @{ $bug->attachments } ) { + next if $attachment->contenttype ne PHAB_CONTENT_TYPE; + my ($revision_id) = ( $attachment->filename =~ PHAB_ATTACHMENT_PATTERN ); + next if !$revision_id; + push @revision_ids, int $revision_id; + } + + my $response = request( + 'differential.revision.search', + { + attachments => { + 'projects' => 1, + 'reviewers' => 1, + 'subscribers' => 1, + 'reviewers-extra' => 1, + }, + constraints => { + ids => \@revision_ids, + }, + order => 'newest', + } + ); + + state $SearchResult = Dict[ + result => Dict[ + # HashRef below could be better, + # but ::Revision takes a lot of options. + data => ArrayRef[ HashRef ], + slurpy Any, + ], + slurpy Any, + ]; + + my $error = $SearchResult->validate($response); + ThrowCodeError( 'phabricator_api_error', { reason => $error } ) + if defined $error; + + my $revision_status_map = { + 'abandoned' => 'Abandoned', + 'accepted' => 'Accepted', + 'changes-planned' => 'Changes Planned', + 'needs-review' => 'Needs Review', + 'needs-revision' => 'Needs Revision', + }; + + my $review_status_map = { + 'accepted' => 'Accepted', + 'accepted-prior' => 'Accepted Prior Diff', + 'added' => 'Review Requested', + 'blocking' => 'Blocking Review', + 'rejected' => 'Requested Changes', + 'resigned' => 'Resigned' + }; + + my @revisions; + foreach my $revision ( @{ $response->{result}{data} } ) { + my $revision_obj = Bugzilla::Extension::PhabBugz::Revision->new($revision); + my $revision_data = { + id => 'D' . $revision_obj->id, + author => $revision_obj->author->name, + status => $revision_obj->status, + long_status => $revision_status_map->{$revision_obj->status} || $revision_obj->status + }; + + my @reviews; + foreach my $review ( @{ $revision_obj->reviews } ) { + push @reviews, { + user => $review->{user}->name, + status => $review->{status}, + long_status => $review_status_map->{$review->{status}} || $review->{status} + }; + } + $revision_data->{reviews} = \@reviews; + + if ( $revision_obj->view_policy ne 'public' ) { + $revision_data->{title} = '(secured)'; + } + else { + $revision_data->{title} = $revision_obj->title; + } + + push @revisions, $revision_data; + } + + # sort by revision id + @revisions = sort { $a->{id} cmp $b->{id} } @revisions; + + return { revisions => \@revisions }; +} + sub rest_resources { return [ # Set build target in Phabricator @@ -162,6 +275,14 @@ sub rest_resources { }, }, }, + qr{^/phabbugz/bug_revisions/(\d+)$}, { + GET => { + method => 'bug_revisions', + params => sub { + return { bug_id => $_[0] }; + }, + }, + }, ]; } diff --git a/extensions/PhabBugz/t/basic.t b/extensions/PhabBugz/t/basic.t index af92dc64f..2f05496cd 100644 --- a/extensions/PhabBugz/t/basic.t +++ b/extensions/PhabBugz/t/basic.t @@ -186,6 +186,7 @@ do { "authorPHID": "PHID-USER-4wigy3sh5fc5t74vapwm", "dateCreated": 1507666113, "dateModified": 1508514027, + "diffPHID": "PHID-DIFF-x5fnvkz5rpco2pogzcrf", "policy": { "view": "public", "edit": "admin" diff --git a/extensions/PhabBugz/template/en/default/hook/bug/edit-after_bug_data.html.tmpl b/extensions/PhabBugz/template/en/default/hook/bug/edit-after_bug_data.html.tmpl new file mode 100644 index 000000000..7dc7c6e49 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/bug/edit-after_bug_data.html.tmpl @@ -0,0 +1,25 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% RETURN UNLESS phabricator_revisions %] + + + + + + + + + + + + + +
Phabricator Revisions
+ [% INCLUDE phabricator/table.html.tmpl %] +
diff --git a/extensions/PhabBugz/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/PhabBugz/template/en/default/hook/bug/show-header-end.html.tmpl new file mode 100644 index 000000000..9d6001a90 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/bug/show-header-end.html.tmpl @@ -0,0 +1,13 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% + IF phabricator_revisions; + PROCESS phabricator/header.html.tmpl; + END; +%] diff --git a/extensions/PhabBugz/template/en/default/hook/bug_modal/edit-module.html.tmpl b/extensions/PhabBugz/template/en/default/hook/bug_modal/edit-module.html.tmpl new file mode 100644 index 000000000..054cc72e8 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/bug_modal/edit-module.html.tmpl @@ -0,0 +1,16 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% RETURN UNLESS phabricator_revisions %] + +[% WRAPPER bug_modal/module.html.tmpl + title = "Phabricator Revisions" + collapsed = 0 +%] + [% INCLUDE phabricator/table.html.tmpl %] +[% END %] diff --git a/extensions/PhabBugz/template/en/default/hook/bug_modal/header-end.html.tmpl b/extensions/PhabBugz/template/en/default/hook/bug_modal/header-end.html.tmpl new file mode 100644 index 000000000..9d6001a90 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/bug_modal/header-end.html.tmpl @@ -0,0 +1,13 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% + IF phabricator_revisions; + PROCESS phabricator/header.html.tmpl; + END; +%] diff --git a/extensions/PhabBugz/template/en/default/phabricator/header.html.tmpl b/extensions/PhabBugz/template/en/default/phabricator/header.html.tmpl new file mode 100644 index 000000000..f4e96afa6 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/phabricator/header.html.tmpl @@ -0,0 +1,10 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% style_urls.push('extensions/PhabBugz/web/style/phabricator.css') %] +[% javascript_urls.push('extensions/PhabBugz/web/js/phabricator.js') %] diff --git a/extensions/PhabBugz/template/en/default/phabricator/table.html.tmpl b/extensions/PhabBugz/template/en/default/phabricator/table.html.tmpl new file mode 100644 index 000000000..3331c1991 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/phabricator/table.html.tmpl @@ -0,0 +1,29 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + + + + + + + + + + + + + + + + + + + +
IDTitleAuthorStatusReviewers
Loading...
Error loading Phabricator revisions: +
diff --git a/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff b/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff new file mode 100644 index 000000000..ffdc85c7b Binary files /dev/null and b/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff differ diff --git a/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff2 b/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff2 new file mode 100644 index 000000000..008f4f9ba Binary files /dev/null and b/extensions/PhabBugz/web/fonts/FontAwesome-DifferentialStatus.woff2 differ diff --git a/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff b/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff differ diff --git a/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff2 b/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/extensions/PhabBugz/web/fonts/fontawesome-webfont.woff2 differ diff --git a/extensions/PhabBugz/web/js/phabricator.js b/extensions/PhabBugz/web/js/phabricator.js new file mode 100644 index 000000000..a4ceff7c0 --- /dev/null +++ b/extensions/PhabBugz/web/js/phabricator.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + */ + +var Phabricator = {}; + +Phabricator.getBugRevisions = function() { + var phabUrl = $('.phabricator-revisions').data('phabricator-base-uri'); + var tr = $(''); + var td = $(''); + var link = $(''); + var span = $(''); + var table = $(''); + + function revisionRow(revision) { + var trRevision = tr.clone(); + var tdId = td.clone(); + var tdTitle = td.clone(); + var tdAuthor = td.clone(); + var tdRevisionStatus = td.clone(); + var tdReviewers = td.clone(); + var tableReviews = table.clone(); + + var spanRevisionStatus = span.clone(); + var spanRevisionStatusIcon = span.clone(); + var spanRevisionStatusText = span.clone(); + + var revLink = link.clone(); + revLink.attr('href', phabUrl + '/' + revision.id); + revLink.text(revision.id); + tdId.append(revLink); + + tdTitle.text(revision.title); + tdTitle.addClass('phabricator-title'); + + tdAuthor.text(revision.author); + + spanRevisionStatusIcon.addClass('revision-status-icon-' + revision.status); + spanRevisionStatus.append(spanRevisionStatusIcon); + spanRevisionStatusText.text(revision.long_status); + spanRevisionStatus.append(spanRevisionStatusText); + spanRevisionStatus.addClass('revision-status-box-' + revision.status); + tdRevisionStatus.append(spanRevisionStatus); + + var i = 0, l = revision.reviews.length; + for (; i < l; i++) { + var trReview = tr.clone(); + var tdReviewStatus = td.clone(); + var tdReviewer = td.clone(); + var spanReviewStatusIcon = span.clone(); + spanReviewStatusIcon.addClass('review-status-icon-' + revision.reviews[i].status); + spanReviewStatusIcon.prop('title', revision.reviews[i].long_status); + tdReviewStatus.append(spanReviewStatusIcon); + tdReviewer.text(revision.reviews[i].user); + tdReviewer.addClass('review-reviewer'); + trReview.append(tdReviewStatus, tdReviewer); + tableReviews.append(trReview); + } + tableReviews.addClass('phabricator-reviewers'); + tdReviewers.append(tableReviews); + + trRevision.append( + tdId, + tdTitle, + tdAuthor, + tdRevisionStatus, + tdReviewers + ); + + return trRevision; + } + + var tbody = $('tbody.phabricator-revision'); + + function displayLoadError(errStr) { + var errRow = tbody.find('.phabricator-loading-error-row'); + errRow.find('.phabricator-load-error-string').text(errStr); + errRow.removeClass('bz_default_hidden'); + } + + var $getUrl = '/rest/phabbugz/bug_revisions/' + BUGZILLA.bug_id + + '?Bugzilla_api_token=' + BUGZILLA.api_token; + + $.getJSON($getUrl, function(data) { + if (data.revisions.length === 0) { + displayLoadError('none returned from server'); + } else { + var i = 0; + for (; i < data.revisions.length; i++) { + tbody.append(revisionRow(data.revisions[i])); + } + } + tbody.find('.phabricator-loading-row').addClass('bz_default_hidden'); + }).fail(function(jqXHR, textStatus, errorThrown) { + var errStr; + if (jqXHR.responseJSON && jqXHR.responseJSON.err && + jqXHR.responseJSON.err.msg) { + errStr = jqXHR.responseJSON.err.msg; + } else if (errorThrown) { + errStr = errorThrown; + } else { + errStr = 'unknown'; + } + displayLoadError(errStr); + tbody.find('.phabricator-loading-row').addClass('bz_default_hidden'); + }); +}; + +$().ready(function() { + Phabricator.getBugRevisions(); +}); diff --git a/extensions/PhabBugz/web/style/phabricator.css b/extensions/PhabBugz/web/style/phabricator.css new file mode 100644 index 000000000..bbc865bf8 --- /dev/null +++ b/extensions/PhabBugz/web/style/phabricator.css @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +@font-face { + font-family: FontAwesome-DifferentialStatus; + font-style: normal; + font-weight: normal; + src: url(../fonts/FontAwesome-DifferentialStatus.woff2?v=4.7) format('woff2'), + url(../fonts/FontAwesome-DifferentialStatus.woff?v=4.7) format('woff'); +} + +.phabricator-table { + background: #fff; + border: none; + border-collapse: collapse; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} + +.phabricator-table th { + text-align: left; + padding: 4px; +} + +.phabricator-table td { + vertical-align: middle !important; + padding: 4px !important; +} + +.phabricator-table thead, .phabricator-table tfoot { + background-color: #eee; + color: #404040; +} + +.phabricator-revisions { + background: #fff; + border: none; + border-collapse: collapse; +} + +.phabricator-revisions th { + padding: 2px; +} + +.phabricator-revisions td { + padding: 2px; + vertical-align: top; +} + +.phabricator-revisions .phabricator-reviewers td { + padding: 1px; +} + +span[class^="revision-status-box-"] { + border: none; + font-weight: normal; + padding: 3px 9px; + border-radius: 3px; + white-space: nowrap; + font-size: 14px; + margin-bottom: 5px; +} + +span[class^="revision-status-icon-"]::before, +span[class^="review-status-icon-"]::before +{ + display: inline-block; + font-variant: normal; + text-rendering: auto; + font-family: FontAwesome-DifferentialStatus; +} + +.revision-status-icon-needs-review::before { + content: "\f121"; +} + +.revision-status-icon-needs-revision::before { + content: "\f021"; +} + +.revision-status-icon-changes-planned::before { + content: "\f025"; +} + +.revision-status-icon-accepted::before { + content: "\f00C"; +} + +.revision-status-icon-published::before { + content: "\f046"; +} + +.revision-status-icon-abandoned::before { + content: "\f072"; +} + +.revision-status-icon-draft::before { + content: "\f110"; +} + +.revision-status-box-needs-review { + background: rgba(71,87,120,0.1); + color: inherit; +} + +.revision-status-box-accepted { + background: #ddefdd; + color: #326d34; +} + +.revision-status-box-changes-planned, +.revision-status-box-needs-revision { + background: #f7e6e6; + color: #a53737; +} + +.revision-status-box-abandoned { + background: #eae6f7; + color: #6e5cb6; +} + +.review-status-icon-accepted::before { + color: green; + content: "\f058"; +} + +.review-status-icon-accepted-prior::before { + color: grey; + content: "\f058"; +} + +.review-status-icon-added::before { + color: grey; + content: "\f10c"; +} + +.review-status-icon-blocking::before { + color: red; + content: "\f056"; +} + +.review-status-icon-rejected::before { + color: red; + content: "\f05c"; +} + +.review-status-icon-resigned::before { + color: rgba(55,55,55,0.3); + content: "\f024"; +} + +/* bug-modal specific */ + +#module-phabricator-revisions .module-content { + padding: 0; +} + +.bug_modal .phabricator-table { + width: 100%; +} + +.bug_modal .phabricator-revision td { + padding: 8px; + vertical-align: top; + border-bottom: 1px dotted silver; +} + +.bug_modal .phabricator-revisions th { + text-align: left; + padding-left: 8px; +} + +.bug_modal .phabricator-revision .phabricator-reviewers td { + padding: 1px; + border: 0px; +}