Skip to content

Instantly share code, notes, and snippets.

@rgoupil
Last active June 13, 2020 09:14
Show Gist options
  • Save rgoupil/ad05024f98db7be55ecc7c43322ac2d9 to your computer and use it in GitHub Desktop.
Save rgoupil/ad05024f98db7be55ecc7c43322ac2d9 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Bitbucket PR author filter
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Allow to filter PRs on bitbucket by author
// @author You
// @match https://bitbucket.org/dashboard/pullrequests
// @match https://bitbucket.org/dashboard/pullrequests?*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
// @connect api.bitbucket.org
// @run-at document-idle
// ==/UserScript==
/*
* Your bitbucket username
*/
const bitbucketUser = YOUR_USERNAME;
/*
* Go to https://bitbucket.org/account/settings/app-passwords/
* Create a new password with the following persmissions:
* - Workspace membership [READ]
* - Projects [READ]
* - Pull requests [READ]
* - Pipelines [READ]
*/
const bitbucketKey = YOUR_KEY;
/*
* Bitbucket workspace
*/
const bitbucketProject = 'backroom';
/*
* Butbucket team slug
*/
const bitbucketTeam = 'trauma-team';
/* eslint-env browser */
GM_xmlhttpRequest.async = async function(opts) {
return new Promise(resolve => GM_xmlhttpRequest(Object.assign(opts, { onload: resolve })));
}
GM_addStyle(`
.filter-container {
display: flex;
flex-direction: row;
align-items: center;
}
.filter-container > * {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.pullrequest-list td.date,
.pullrequest-list td.date > div {
width: 170px !important;
}
.iterable-item.dev-master[data-dev-master-idx="0"] {
border-top: 2px solid #DFE1E6;
}
`);
const bitbucketBasic = `Basic ${btoa(`${bitbucketUser}:${bitbucketKey}`)}`;
(function() {
'use strict';
fixWorkspaceFilters();
addAuthorFilter();
})();
function fixWorkspaceFilters() {
const groupEl = document.getElementById('team-filter');
if (!groupEl) {
return;
}
groupEl.style.display = 'flex';
const selectEl = groupEl.querySelector('.field-group.user-select');
if (!selectEl) {
return;
}
selectEl.style.padding = '0';
}
let originalTBodyHTML = '';
let pullRequests = [];
let pullRequestPromise;
async function getPullRequests(userIds) {
if (pullRequestPromise) {
return pullRequestPromise;
}
let tmpPRs = [];
pullRequestPromise = Promise.all(userIds.map(async userId => {
const res = await GM_xmlhttpRequest.async({
method: 'GET',
url: `https://api.bitbucket.org/2.0/pullrequests/${userId}?format=json&state=OPEN&limit=999`,
headers: { 'Authorization': bitbucketBasic },
});
const response = JSON.parse(res.response);
const prs = response.values;
await Promise.all(prs.map(async pr => {
{
const res = await GM_xmlhttpRequest.async({
method: 'GET',
url: `${pr.links.self.href}?format=json&sort=-updated_on`,
headers: { 'Authorization': bitbucketBasic },
});
const details = JSON.parse(res.response);
Object.assign(pr, details);
}
{
const res = await GM_xmlhttpRequest.async({
method: 'GET',
url: `${pr.links.self.href}/statuses?format=json&sort=-updated_on`,
headers: { 'Authorization': bitbucketBasic },
});
const results = JSON.parse(res.response);
pr.results = results.values;
}
pr.approuve_count = pr.participants.reduce((acc, cur) => acc + cur.approved ? 1 : 0, 0);
}));
tmpPRs = tmpPRs.concat(prs);
}));
await pullRequestPromise;
pullRequests = tmpPRs.sort((a, b) => {
a.dev_master = a.source.branch.name === 'dev' && a.destination.branch.name === 'master';
b.dev_master = b.source.branch.name === 'dev' && b.destination.branch.name === 'master';
if (a.dev_master || b.dev_master && a.dev_master !== b.dev_master) {
return a.dev_master ? 1 : -1;
}
return new Date(b.updated_on) - new Date(a.updated_on);
});
console.log('pullRequests', pullRequests);
pullRequestPromise = null;
}
async function addAuthorFilter() {
const url = `https://api.bitbucket.org/1.0/groups/${bitbucketProject}/${bitbucketTeam}/members?format=json`;
const res = await GM_xmlhttpRequest.async({
method: 'GET',
url,
headers: { 'Authorization': bitbucketBasic },
});
const users = JSON.parse(res.response);
await getPullRequests(users.map(u => u.uuid));
const groupEl = document.querySelector('.filter-status');
if (!groupEl) {
return;
}
const teamFilter = document.createElement('li');
teamFilter.select = function() {
[].slice.call(groupEl.querySelectorAll('[aria-pressed=true]')).map(el => el.removeAttribute('aria-pressed'));
teamFilter.setAttribute('aria-pressed', true);
}
if (new URLSearchParams(window.location.search).get('section') === 'nicecactus_teams') {
teamFilter.select();
}
teamFilter.style.cursor = 'pointer';
teamFilter.innerHTML = `<a data-status="nicecactus_teams">Teams</a>`;
teamFilter.onclick = async _ev => {
teamFilter.select();
history.pushState({}, 'Teams', '?section=nicecactus_teams');
await getPullRequests(users.map(u => u.uuid));
updatePullRequestList(true);
}
groupEl.appendChild(teamFilter);
waitForKeyElements('#pullrequests table tbody .iterable-item', () => {
const tbodyEl = document.querySelector('#pullrequests table tbody');
if (!tbodyEl) {
return;
}
if (!originalTBodyHTML) {
originalTBodyHTML = tbodyEl.innerHTML;
if (new URLSearchParams(window.location.search).get('section') !== 'nicecactus_teams') {
return;
}
updatePullRequestList(true);
}
}, true);
}
const PullRequestResultLUT = {
'SUCCESSFUL': {
label: 'Success',
class: 'commit-status--success',
icon: 'aui-iconfont-approve',
},
'INPROGRESS': {
label: 'In progress',
class: 'commit-status--inprogress',
icon: 'aui-iconfont-time',
},
'FAILED': {
label: 'Fail',
class: 'commit-status--fail',
icon: 'aui-iconfont-error',
},
};
function updatePullRequestList(enabled) {
const [ tbodyEl, paginationEl ] = [
document.querySelector('#pullrequests table tbody'),
document.querySelector('.aui-nav.aui-nav-pagination'),
];
if (!tbodyEl) {
return;
}
if (!enabled) {
tbodyEl.innerHTML = originalTBodyHTML;
if (paginationEl) {
paginationEl.style.display = null;
}
} else {
tbodyEl.innerHTML = '';
if (paginationEl) {
paginationEl.style.display = 'none';
}
let devMasterIdx = 0;
pullRequests.map(pr => {
const ticketRegex = /\w+\-\d+/;
const ticketIdMatch = ticketRegex.exec(pr.title);
const ticketId = ticketIdMatch && ticketIdMatch[0];
const titlePre = ticketIdMatch && pr.title.substr(0, ticketIdMatch.index);
const titlePost = ticketIdMatch && pr.title.substr(ticketIdMatch.index + ticketId.length);
const prEl = document.createElement('tr');
prEl.classList.add('iterable-item');
if (pr.dev_master) {
prEl.classList.add('dev-master');
prEl.setAttribute('data-dev-master-idx', devMasterIdx)
devMasterIdx++;
}
prEl.innerHTML = `
<td class="repo">
<div>
<span class="repository-avatar aui-avatar aui-avatar-squared aui-avatar-project aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="${pr.destination.repository.links.avatar.href}" alt="">
</span>
</span>
<a href="${pr.destination.repository.links.html.href}">
<span title="${pr.destination.repository.full_name}">${pr.destination.repository.name}</span>
</a>
</div>
</td>
<td class="user">
<div class="inline-user">
<span class="inline-user--avatar" data-module="components/user-profile-avatar">
<div class="sc-jqCOkK fYCWaK">
<div class="css-ziu9rg-AvatarContainer etjipaw0">
<div style="display: inline-block; position: relative; outline: 0px; height: 20px; width: 20px;">
<span class="sc-dxgOiQ hPmXGd" tabindex="0">
<span role="img" aria-label="${pr.author.display_name}" style="background-color: transparent; background-image: url('${pr.author.links.avatar.href}'); background-position: center center; background-repeat: no-repeat; background-size: cover; border-radius: 50%; display: flex; flex: 1 1 100%; height: 100%; width: 100%;">
</span>
</span>
</div>
</div>
</div>
</span>
<span class="inline-user--name" data-module="components/user-profile-name">
<div class="sc-jqCOkK fYCWaK"><span>${pr.author.display_name}</span></div>
</span>
</div>
</td>
<td class="title flex-content--column">
<div class="flex-content">
<div class="flex-content--primary">
<a class="execute" href="${pr.links.html.href}" target="_blank" title="#${pr.id}: ${pr.title}">#${pr.id}: </a>
${
ticketId ?
`${titlePre} <a href="https://backroom-esm.atlassian.net/browse/${ticketId}" target="_blank" class="detail-summary--item--link aui-button aui-button-text" resolved="">
<span class="aui-nav-item-label">${ticketId}</span>
</a> ${titlePost}` :
pr.title
}
</div>
<div class="flex-content--secondary">
<div class="list-stats pullrequest-stats">
${
pr.comment_count && pr.comment_count > 0 ?
`<div class="list-stat">
<a href="${pr.links.html.href}" class="comments-link" title="Comments">
<span class="aui-icon aui-icon-small aui-iconfont-comment">Comments</span>
</a>
<span class="count">${pr.comment_count}</span>
</div>` :
''
}
${
pr.approuve_count && pr.approuve_count > 0 ?
`<div class="list-stat">
<a href="${pr.links.html.href}" class="approval-link " title="Approvals">Approvals</a>
<span class="count">${pr.approuve_count}</span>
</div>` :
''
}
</div>
</div>
</div>
</td>
<td class="date">
<div>
<time datetime="${pr.updated_on}" title="${new Date(pr.updated_on).toLocaleDateString()} ${new Date(pr.updated_on).toLocaleTimeString()}">${new Date(pr.updated_on).toLocaleDateString()} ${new Date(pr.updated_on).toLocaleTimeString()}</time>
</div>
</td>
<td class="status icon-col">
${
pr.results && pr.results.length > 0 && PullRequestResultLUT[pr.results[0].state] ?
`<span class="commit-status--dialog-trigger">
<a href="${pr.results[0].url}" data-total-count="1"
target="_blank" rel="nofollow">
<span data-state="${pr.results[0].state}"></span>
<span class="aui-icon aui-icon-small ${PullRequestResultLUT[pr.results[0].state].class} ${PullRequestResultLUT[pr.results[0].state].icon} js-commit-status-icon">
${PullRequestResultLUT[pr.results[0].state].label}
</span>
</a>
</span>` :
''
}
</td>
<td class="actions">
<!--
<div>
<a href="/backroom/msapi/pull-requests/356/follow" data-template="pullrequest-watch-template"
data-type="pullrequest" class="follow following" title="Stop watching this pull request"> Stop watching
</a>
</div>
-->
</td>`;
tbodyEl.appendChild(prEl);
});
}
}
@Idlic
Copy link

Idlic commented Jun 11, 2020

If i can make a suggestion, we can't see dev->master pull request when it is not made by someone from our team

@rgoupil
Copy link
Author

rgoupil commented Jun 13, 2020

If i can make a suggestion, we can't see dev->master pull request when it is not made by someone from our team

Good point, thanks! I'll do it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment