Skip to content

Instantly share code, notes, and snippets.

@hsayed21
Created December 9, 2025 16:36
Show Gist options
  • Select an option

  • Save hsayed21/5535f6a946143a3519230c8bc42704e0 to your computer and use it in GitHub Desktop.

Select an option

Save hsayed21/5535f6a946143a3519230c8bc42704e0 to your computer and use it in GitHub Desktop.
Show badges in Azure DevOps PR list
// ==UserScript==
// @name Azure DevOps PR Reviewer Badges
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Show Approved / No Update / Assigned / Not Assigned badges in Azure DevOps PR list
// @author hsayed21
// @match https://dev.azure.com/*/*/_git/*/pullrequests*
// @grant GM_xmlhttpRequest
// @connect dev.azure.com
// ==/UserScript==
(function() {
'use strict';
const CURRENT_USER_EMAIL = "<EMAIL-HERE>";
function getOrgProjectRepo() {
const parts = window.location.pathname.split('/').filter(Boolean);
return {
organization: parts[0],
project: parts[1],
repo: decodeURIComponent(parts[3])
};
}
function addBadge(container, text, color) {
const badge = document.createElement("span");
badge.textContent = text;
badge.style.cssText = `
background:${color};
color:white;
padding:2px 6px;
border-radius:6px;
margin-left:8px;
font-size:11px;
font-weight:bold;
display:inline-block;
`;
container.appendChild(badge);
}
function api(url) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url,
headers: { "Content-Type": "application/json" },
onload: (r) => resolve(JSON.parse(r.responseText)),
onerror: () => resolve(null)
});
});
}
async function hasUpdates(org, project, repo, prId, myEmail) {
const { lastActivity } = await getMyLastActivity(org, project, repo, prId, myEmail);
// 1) Check new commits
const iterations = await api(`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/iterations?api-version=7.0`);
if (iterations?.value?.length) {
const lastIter = iterations.value[iterations.value.length - 1];
const lastCommitTime = lastIter.createdDate;
if (new Date(lastCommitTime) > new Date(lastActivity)) {
return true; // New commits added
}
}
// 2) Check new comments
const threads = await api(`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/threads?api-version=7.0`);
if (threads?.value?.length) {
for (const t of threads.value) {
for (const c of t.comments) {
if (new Date(c.publishedDate) > new Date(lastActivity)) {
return true; // New comment
}
}
}
}
return false;
}
async function getMyLastActivity(org, project, repo, prId, myEmail) {
let lastVoteDate = null;
let lastCommitDate = null;
let lastCommentDate = null;
// ---- 1) Reviewer Info (contains your vote timestamps)
const reviewers = await api(`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/reviewers?api-version=7.0`);
if (reviewers?.value?.length) {
const me = reviewers.value.find(r => r.uniqueName === myEmail);
if (me?.updatedDate) lastVoteDate = new Date(me.updatedDate);
}
// ---- 2) Commits
const commits = await api(`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/iterations?api-version=7.0`);
if (commits?.value?.length) {
for (const commit of commits.value) {
if (commit.author?.uniqueName === myEmail) {
const d = new Date(commit.createdDate);
if (!lastCommitDate || d > lastCommitDate) {
lastCommitDate = d;
}
}
}
}
// ---- 3) Commnets
const threads = await api(`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/threads?api-version=7.0`);
if (threads?.value?.length) {
for (const thread of threads.value) {
for (const c of thread.comments) {
if (c.author?.uniqueName === myEmail) {
const d = new Date(c.publishedDate);
if (!lastCommentDate || d > lastCommentDate) {
lastCommentDate = d;
}
}
}
}
}
// ---- 4) Determine the latest activity (largest datetime)
const lastActivity = getLatestDate(lastVoteDate, lastCommitDate, lastCommentDate);
return {
lastVoteDate,
lastCommitDate,
lastCommentDate,
lastActivity
};
}
function getLatestDate(...dates) {
const valid = dates.filter(d => d).map(d => new Date(d));
if (valid.length === 0) return null;
return new Date(Math.max(...valid));
}
async function processRow(row) {
if (row.dataset.badgeDone === "yes") return;
row.dataset.badgeDone = "yes";
// Get PR ID from href
const prMatch = row.href.match(/pullrequest\/(\d+)/);
if (!prMatch) return;
const prId = prMatch[1];
const { organization, project, repo } = getOrgProjectRepo();
const prInfo = await api(`https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}?api-version=7.0`);
const isDraft = prInfo?.isDraft === true;
if (isDraft)
{
row.style.display="none";
}
const isMyPR = prInfo?.createdBy?.uniqueName === CURRENT_USER_EMAIL;
if (isMyPR)
{
row.style.background = "rgb(17, 84, 87)";
return;
}
const container = row.querySelector(".bolt-table-two-line-cell-item.flex-row.scroll-hidden");
if (!container) return;
const reviewersData = await api(`https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repo}/pullRequests/${prId}/reviewers?api-version=7.0`);
if (!reviewersData || !reviewersData.value) return;
const reviewers = reviewersData.value || [];
const myReviewer = reviewers.find(r => r.uniqueName === CURRENT_USER_EMAIL);
if (!myReviewer) {
addBadge(container, "Not Assigned", "gray");
return;
}
// Reviewer status badges
const vote = myReviewer.vote;
if (vote === 10 || vote === 5) {
addBadge(container, "Approved", "green");
return;
}
else if (vote === -5) {
addBadge(container, "Waiting Author", "orange");
}
else {
addBadge(container, "Assigned", "rgb(5, 89, 166)"); // gray-ish
}
// ► Check new updates AFTER review
if (vote !== 0) {
const updated = await hasUpdates(organization, project, repo, prId, CURRENT_USER_EMAIL);
if (updated) {
addBadge(container, "Updated", "dodgerblue");
} else {
addBadge(container, "No Updates", "#555");
}
}
}
// Observe UI changes (Azure DevOps loads dynamically)
const observer = new MutationObserver(() => {
document.querySelectorAll("table.repos-pr-list > tbody > a").forEach(row => processRow(row));
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial run
setTimeout(() => {
document.querySelectorAll("table.repos-pr-list > tbody > a").forEach(row => processRow(row));
}, 1200);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment