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};
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 );
]
);
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 ) = @_;
$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};
}
);
- 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 {
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
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
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
},
},
},
+ qr{^/phabbugz/bug_revisions/(\d+)$}, {
+ GET => {
+ method => 'bug_revisions',
+ params => sub {
+ return { bug_id => $_[0] };
+ },
+ },
+ },
];
}
"authorPHID": "PHID-USER-4wigy3sh5fc5t74vapwm",
"dateCreated": 1507666113,
"dateModified": 1508514027,
+ "diffPHID": "PHID-DIFF-x5fnvkz5rpco2pogzcrf",
"policy": {
"view": "public",
"edit": "admin"
--- /dev/null
+[%# 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 %]
+
+<table class="phabricator-table">
+ <thead>
+ <tr>
+ <th>Phabricator Revisions</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td>
+ [% INCLUDE phabricator/table.html.tmpl %]
+ </td>
+ </tr>
+ </tbody>
+</table>
--- /dev/null
+[%# 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;
+%]
--- /dev/null
+[%# 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 %]
--- /dev/null
+[%# 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;
+%]
--- /dev/null
+[%# 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') %]
--- /dev/null
+[%# 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.
+ #%]
+
+<table class="phabricator-revisions"
+ data-phabricator-base-uri="[% Param('phabricator_base_uri') FILTER html %]">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Title</th>
+ <th>Author</th>
+ <th>Status</th>
+ <th>Reviewers</th>
+ </tr>
+ </thead>
+ <tbody class="phabricator-revision">
+ <tr class="phabricator-loading-row">
+ <td colspan="4">Loading...</td>
+ </tr>
+ <tr class="phabricator-loading-error-row bz_default_hidden">
+ <td colspan="4">Error loading Phabricator revisions:
+ <span class="phabricator-load-error-string"></span></td>
+ </tr>
+ </tbody>
+</table>
--- /dev/null
+/* 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 = $('<tr/>');
+ var td = $('<td/>');
+ var link = $('<a/>');
+ var span = $('<span/>');
+ var table = $('<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();
+});
--- /dev/null
+/* 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;
+}