blob: 8122474428b1f8467bd1300b592dbbebad2c9570 [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Gerrit.install(function(self) {
function onFindOwners(c) {
const HTML_ALL_HAVE_OWNER_APPROVAL =
'<b>All files have owner approval.</b><br>';
const HTML_BULLET = '<small>&#x2605;</small>'; // a Black Star
const HTML_IS_EXEMPTED =
'<b>This commit is exempted from owner approval.</b><br>';
const HTML_NEED_REVIEWER_HEADER =
'<hr><b>Files without owner reviewer:</b><br>';
const HTML_NEED_APPROVAL_HEADER =
'<hr><b>Files without Code-Review vote from an owner:</b><br>';
const HTML_NO_OWNER =
'<b>No owner was found for changed files.</b><br>';
const HTML_OWNERS_HEADER = '<hr><b>Owners in alphabetical order:</b><br>';
const HTML_SELECT_REVIEWERS =
'<b>Check the box before owner names to select reviewers, ' +
'then click the "Apply" button.' +
'</b><br><small>Each file needs at least one owner. ' +
'Owners listed after a file are ordered by their importance. ' +
'(Or declare "<b><span style="font-size:80%;">' +
'Exempt-From-Owner-Approval:</span></b> ' +
'<i>reasons...</i>" in the Commit Message.)</small><br>';
const APPLY_BUTTON_ID = 'FindOwners:Apply';
const CHECKBOX_ID = 'FindOwners:CheckBox';
const HEADER_DIV_ID = 'FindOwners:Header';
const OWNERS_DIV_ID = 'FindOwners:Owners';
const NEED_APPROVAL_DIV_ID = 'FindOwners:NeedApproval';
const NEED_REVIEWER_DIV_ID = 'FindOwners:NeedReviewer';
// Aliases to values in the context.
const branch = c.change.branch;
const changeId = c.change._number;
const message = c.revision.commit.message;
const project = c.change.project;
var reviewerId = {}; // map from a reviewer's email to account id.
var reviewerVote = {}; // map from a reviewer's email to Code-Review vote.
// addList and removeList are used only under applySelections.
var addList = []; // remain emails to add to reviewers
var removeList = []; // remain emails to remove from reviewers
var needRefresh = false; // true if to refresh after checkAddRemoveLists
function getElement(id) {
return document.getElementById(id);
}
function getReviewers(change, callBack) {
Gerrit.get('changes/' + change + '/reviewers', callBack);
}
function setupReviewersMap(reviewerList) {
reviewerId = {};
reviewerVote = {};
reviewerList.forEach(function(reviewer) {
if ('email' in reviewer && '_account_id' in reviewer) {
reviewerId[reviewer.email] = reviewer._account_id;
reviewerVote[reviewer.email] = 0;
if ('approvals' in reviewer && 'Code-Review' in reviewer.approvals) {
reviewerVote[reviewer.email] =
parseInt(reviewer.approvals['Code-Review']);
// The 'Code-Review' values could be "-2", "-1", " 0", "+1", "+2"
}
}
});
}
function checkAddRemoveLists() {
// Gerrit.post and delete are asynchronous.
// Do one at a time, with checkAddRemoveLists as callBack.
for (var i = 0; i < addList.length; i++) {
var email = addList[i];
if (!(email in reviewerId)) {
addList = addList.slice(i + 1, addList.length);
// A post request can fail if given reviewer email is invalid.
// Gerrit core UI shows the error dialog and does not provide
// a way for plugins to handle the error yet.
needRefresh = true;
Gerrit.post('changes/' + changeId + '/reviewers',
{'reviewer': email},
checkAddRemoveLists);
return;
}
}
for (var i = 0; i < removeList.length; i++) {
var email = removeList[i];
if (email in reviewerId) {
removeList = removeList.slice(i + 1, removeList.length);
needRefresh = true;
Gerrit.delete('changes/' + changeId +
'/reviewers/' + reviewerId[email],
checkAddRemoveLists);
return;
}
}
c.hide();
if (needRefresh) {
needRefresh = false;
Gerrit.refresh();
}
callServer(showFindOwnersResults);
}
function applyGetReviewers(reviewerList) {
setupReviewersMap(reviewerList);
checkAddRemoveLists(); // update and pop up window at the end
}
function hasOwnerReviewer(reviewers, owners) {
return owners.some(function(owner) {
return (owner in reviewers || owner == '*');
});
}
function hasOwnerApproval(votes, minVoteLevel, owners) {
var foundApproval = false;
for (var j = 0; j < owners.length; j++) {
if (owners[j] in votes) {
var v = votes[owners[j]];
if (v < 0) {
return false; // cannot have any negative vote
}
// TODO: do not count if owners[j] is the patch committer.
foundApproval |= v >= minVoteLevel;
}
}
return foundApproval;
}
function isExemptedFromOwnerApproval() {
return message.match(/(Exempted|Exempt)-From-Owner-Approval:/);
}
function showDiv(div, text) {
div.style.display = 'inline';
div.innerHTML = text;
}
function strElement(s) {
var e = document.createElement('span');
e.innerHTML = s;
return e;
}
function showJsonLines(args, key, obj) {
showBoldKeyValueLines(args, key, JSON.stringify(obj, null, 2));
}
function showBoldKeyValueLines(args, key, value) {
args.push(c.hr(), strElement('<b>' + key + '</b>:'), c.br());
value.split('\n').forEach(function(line) {
args.push(c.msg(line), c.br());
});
}
function showDebugMessages(result, args) {
function addKeyValue(key, value) {
args.push(strElement('<b>' + key + '</b>: ' + value + '<br>'));
}
args.push(c.hr());
addKeyValue('changeId', changeId);
addKeyValue('project', project);
addKeyValue('branch', branch);
addKeyValue('Gerrit.url', Gerrit.url());
addKeyValue('self.url', self.url());
showJsonLines(args, 'changeOwner', c.change.owner);
showBoldKeyValueLines(args, 'commit.message', message);
showJsonLines(args, 'Client reviewers Ids', reviewerId);
showJsonLines(args, 'Client reviewers Votes', reviewerVote);
Object.keys(result).forEach(function(k) {
showJsonLines(args, 'Server.' + k, result[k]);
});
}
function showFilesAndOwners(result, args) {
var sortedOwners = result.owners.map(
function(ownerInfo) { return ownerInfo.email; });
var groups = {};
// group name ==> {needReviewer, needApproval, owners}
var groupSize = {};
// group name ==> number of files in group
var header = emptyDiv(HEADER_DIV_ID);
var needReviewerDiv = emptyDiv(NEED_REVIEWER_DIV_ID);
var needApprovalDiv = emptyDiv(NEED_APPROVAL_DIV_ID);
addApplyButton();
var ownersDiv = emptyDiv(OWNERS_DIV_ID);
var numCheckBoxes = 0;
var owner2boxes = {}; // owner name ==> array of checkbox id
var owner2email = {}; // owner name ==> email address
function addApplyButton() {
var apply = c.button('Apply', {onclick: doApplyButton});
apply.id = APPLY_BUTTON_ID;
apply.style.display = 'none';
args.push(apply);
}
function emptyDiv(id) {
var e = document.createElement('div');
e.id = id;
e.style.display = 'none';
args.push(e);
return e;
}
function doApplyButton() {
addList = [];
removeList = [];
// add each owner's email address to addList or removeList
Object.keys(owner2boxes).forEach(function(owner) {
(getElement(owner2boxes[owner][0]).checked ?
addList : removeList).push(owner2email[owner]);
});
getReviewers(changeId, applyGetReviewers);
}
function clickBox(event) {
var name = event.target.value;
var checked = event.target.checked;
var others = owner2boxes[name];
others.forEach(function(id) { getElement(id).checked = checked; });
getElement(APPLY_BUTTON_ID).style.display = 'inline';
}
function addGroupsToDiv(div, keys, title) {
if (keys.length <= 0) {
div.style.display = 'none';
return;
}
div.innerHTML = '';
div.style.display = 'inline';
div.appendChild(strElement(title));
function addOwner(ownerEmail) {
numCheckBoxes++;
var name = ownerEmail.replace(/@[^ ]*/g, '');
owner2email[name] = ownerEmail;
var id = CHECKBOX_ID + ':' + numCheckBoxes;
if (!(name in owner2boxes)) {
owner2boxes[name] = [];
}
owner2boxes[name].push(id);
var box = c.checkbox();
box.checked = (ownerEmail in reviewerId);
box.id = id;
box.value = name;
box.onclick = clickBox;
div.appendChild(strElement('&nbsp;&nbsp; '));
var nobr = document.createElement('nobr');
nobr.appendChild(box);
nobr.appendChild(strElement(name));
div.appendChild(nobr);
}
keys.forEach(function(key) {
var owners = groups[key].owners; // string of owner emails
var numFiles = groupSize[key];
var item = HTML_BULLET + '&nbsp;<b>' + key + '</b>' +
((numFiles > 1) ? (' (' + numFiles + ' files):') : ':');
var setOfOwners = new Set(owners.split(' '));
function add2list(list, email) {
if (setOfOwners.has(email)) {
list.push(email);
}
return list;
}
div.appendChild(strElement(item));
sortedOwners.reduce(add2list, []).forEach(addOwner);
div.appendChild(c.br());
});
}
function addOwnersDiv(div, title) {
div.innerHTML = '';
div.style.display = 'inline';
div.appendChild(strElement(title));
function compareOwnerInfo(o1, o2) {
return o1.email.localeCompare(o2.email);
}
result.owners.sort(compareOwnerInfo).forEach(function(ownerInfo) {
var email = ownerInfo.email;
var vote = reviewerVote[email];
if ((email in reviewerVote) && vote != 0) {
email += ' <font color="' +
((vote > 0) ? 'green">(+' : 'red">(') + vote + ')</font>';
}
div.appendChild(strElement('&nbsp;&nbsp;' + email + '<br>'));
});
}
function updateDivContent() {
var groupNeedReviewer = [];
var groupNeedApproval = [];
numCheckBoxes = 0;
owner2boxes = {};
Object.keys(groups).sort().forEach(function(key) {
var g = groups[key];
if (g.needReviewer) {
groupNeedReviewer.push(key);
} else if (g.needApproval) {
groupNeedApproval.push(key);
}
});
if (0 == groupNeedReviewer.length && 0 == groupNeedApproval.length) {
showDiv(header, HTML_ALL_HAVE_OWNER_APPROVAL);
} else {
showDiv(header, HTML_SELECT_REVIEWERS);
addGroupsToDiv(needReviewerDiv, groupNeedReviewer,
HTML_NEED_REVIEWER_HEADER);
addGroupsToDiv(needApprovalDiv, groupNeedApproval,
HTML_NEED_APPROVAL_HEADER);
addOwnersDiv(ownersDiv, HTML_OWNERS_HEADER);
}
}
function createGroups() {
var owners2group = {}; // owner list to group name
var minVoteLevel =
('minOwnerVoteLevel' in result ?
result['minOwnerVoteLevel'] : 1);
Object.keys(result.file2owners).sort().forEach(function(name) {
var splitOwners = result.file2owners[name];
var owners = splitOwners.join(' ');
if (owners in owners2group) {
groupSize[owners2group[owners]] += 1;
} else {
owners2group[owners] = name;
groupSize[name] = 1;
var needReviewer = !hasOwnerReviewer(reviewerId, splitOwners);
var needApproval = !needReviewer &&
!hasOwnerApproval(reviewerVote, minVoteLevel, splitOwners);
groups[name] = {
'needReviewer': needReviewer,
'needApproval': needApproval,
'owners': owners};
}
});
}
createGroups();
updateDivContent();
}
function showFindOwnersResults(result) {
function popupWindow(reviewerList) {
setupReviewersMap(reviewerList);
var args = [];
if (isExemptedFromOwnerApproval()) {
args.push(strElement(HTML_IS_EXEMPTED));
} else if (Object.keys(result.file2owners).length <= 0) {
args.push(strElement(HTML_NO_OWNER));
} else {
showFilesAndOwners(result, args);
}
if (result.addDebugMsg) {
showDebugMessages(result, args);
}
c.popup(c.div.apply(this, args));
}
getReviewers(changeId, popupWindow);
}
function callServer(callBack) {
// Use the plugin REST API; pass only changeId;
// let server get current patch set, project and branch info.
Gerrit.get('changes/' + changeId + '/owners', showFindOwnersResults);
}
callServer(showFindOwnersResults);
}
self.onAction('revision', 'find-owners', onFindOwners);
});