| // 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>★</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(' ')); |
| 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 + ' <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(' ' + 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); |
| }); |