// ==UserScript==
// @name GitHub Search Commit Dates
// @description Add commit dates to GitHub search results using GitHub token.
// @icon https://github.githubassets.com/favicons/favicon-dark.svg
// @version 1.1
// @author afkarxyz
// @namespace https://github.com/afkarxyz/misc-scripts/
// @supportURL https://github.com/afkarxyz/misc-scripts/issues
// @license MIT
// @match https://github.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
HARDCODED_TOKEN: '',
SELECTORS: {
SEARCH_RESULTS: '.Box-sc-g0xbh4-0.dufIPq.notranslate:not([data-processed])',
FILE_LINK: 'a[data-testid="link-to-search-result"]'
},
STYLES: `
.duplicate-element {
border-width: 0px 1px 1px 1px !important;
border-style: solid !important;
border-color: var(--borderColor-default, #d0d7de) !important;
border-radius: 0 !important;
}
.duplicate-element * {
border-radius: 0 !important;
}
.duplicate-element .Box-sc-g0xbh4-0.cJmQqW {
visibility: hidden !important;
pointer-events: none !important;
}
.duplicate-element .Box-sc-g0xbh4-0.bPbmFy {
display: inline-flex !important;
align-items: center !important;
gap: 4px !important;
padding: 3px 0 !important;
min-height: 20px !important;
}
.duplicate-element .custom-calendar-icon {
fill: var(--color-fg-default, #24292f) !important;
display: block !important;
flex-shrink: 0 !important;
position: relative !important;
top: -1px !important;
}
.duplicate-element .search-title {
font-weight: normal !important;
margin: 0 !important;
padding: 0 !important;
line-height: 1.25 !important;
color: var(--color-fg-default, #24292f) !important;
}
.duplicate-element .Box-sc-g0xbh4-0.bPbmFy > * {
flex: 0 0 auto !important;
}
@media (prefers-color-scheme: dark) {
.duplicate-element .custom-calendar-icon {
fill: var(--color-fg-default, #9198a1) !important;
}
.duplicate-element .search-title {
color: var(--color-fg-default, #9198a1) !important;
}
}
`
};
class GitHubDateAdder {
constructor() {
this.token = CONFIG.HARDCODED_TOKEN || GM_getValue('github_token');
this.setupTokenManager();
this.setupStyles();
this.setupObserver();
this.processSearchResults();
}
setupTokenManager() {
GM_registerMenuCommand('Set GitHub Token', () => {
const newToken = prompt('Enter your GitHub token:', this.token || '');
if (newToken) {
this.token = newToken;
GM_setValue('github_token', newToken);
}
});
}
setupStyles() {
if (!document.querySelector('#duplicate-style')) {
const style = document.createElement('style');
style.id = 'duplicate-style';
style.textContent = CONFIG.STYLES;
document.head.appendChild(style);
}
}
setupObserver() {
const observer = new MutationObserver(mutations => {
const hasNewElements = mutations.some(mutation =>
mutation.type === 'childList' &&
Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
node.querySelector(CONFIG.SELECTORS.SEARCH_RESULTS)
)
);
if (hasNewElements) {
this.processSearchResults();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
getTimeAgo(date) {
const intervals = [
{ label: 'year', seconds: 31536000 },
{ label: 'month', seconds: 2592000 },
{ label: 'week', seconds: 604800 },
{ label: 'day', seconds: 86400 },
{ label: 'hour', seconds: 3600 },
{ label: 'minute', seconds: 60 },
{ label: 'second', seconds: 1 }
];
const seconds = Math.floor((new Date() - date) / 1000);
const interval = intervals.find(int => Math.floor(seconds / int.seconds) >= 1);
if (interval) {
const count = Math.floor(seconds / interval.seconds);
return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`;
}
return 'just now';
}
createDateElement(element, index) {
if (document.querySelector(`[data-duplicate-id="duplicate-${index}"]`)) return null;
const newContainer = document.createElement('div');
newContainer.className = 'Box-sc-g0xbh4-0 dufIPq notranslate duplicate-element';
newContainer.setAttribute('data-duplicate-id', `duplicate-${index}`);
const button = element.querySelector('button');
const searchTitle = element.querySelector('.search-title');
const parentContainer = searchTitle?.parentElement;
if (button && parentContainer) {
newContainer.appendChild(button.cloneNode(true));
newContainer.appendChild(parentContainer.cloneNode(true));
}
element.parentNode.insertBefore(newContainer, element.nextSibling);
const iconElement = newContainer.querySelector('.Box-sc-g0xbh4-0.bPbmFy');
if (iconElement) {
iconElement.innerHTML = this.getCalendarIconSVG();
}
const titleElement = newContainer.querySelector('.search-title');
if (titleElement) {
titleElement.textContent = 'Loading...';
}
return newContainer;
}
getCalendarIconSVG() {
return `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
style="width: 16px; height: 16px" class="custom-calendar-icon">
<path d="M128 0c13.3 0 24 10.7 24 24l0 40 144 0 0-40c0-13.3 10.7-24 24-24s24 10.7 24 24l0 40 40 0c35.3 0 64 28.7 64 64l0 16 0 48-16 0-32 0-112 0L48 192l0 256c0 8.8 7.2 16 16 16l220.5 0c12.3 18.8 28 35.1 46.3 48L64 512c-35.3 0-64-28.7-64-64L0 192l0-48 0-16C0 92.7 28.7 64 64 64l40 0 0-40c0-13.3 10.7-24 24-24zM288 368a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-80c-8.8 0-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16l48 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0 0-48c0-8.8-7.2-16-16-16z"/>
</svg>
`;
}
async fetchCommitDate(pathInfo) {
const response = await fetch(
`https://api.github.com/repos/${pathInfo.owner}/${pathInfo.repo}/commits?path=${pathInfo.path}&per_page=1`,
{
headers: {
'Authorization': `token ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
}
}
);
if (response.status === 403) {
throw new Error('Rate limit exceeded or invalid token');
}
const data = await response.json();
return new Date(data[0]?.commit?.author?.date);
}
extractPathInfo(element) {
const fileLink = element.querySelector(CONFIG.SELECTORS.FILE_LINK);
if (!fileLink) return null;
const href = fileLink.getAttribute('href');
const match = href.match(/\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+?)(?:\?.*)?(?:#.*)?$/);
return match ? {
owner: match[1],
repo: match[2],
path: match[3]
} : null;
}
updateElementWithDate(element, date) {
const titleElement = element.querySelector('.search-title');
if (titleElement && date) {
const fullDate = `${date.getDate()} ${date.toLocaleString('en-US', { month: 'short' })} ${date.getFullYear()}`;
titleElement.textContent = `${fullDate} • ${this.getTimeAgo(date)}`;
}
}
async processSearchResults() {
if (!this.token) {
console.warn('GitHub token not set. Please set it from the userscript menu or hardcode it in the script.');
return;
}
const searchResults = document.querySelectorAll(CONFIG.SELECTORS.SEARCH_RESULTS);
const elements = Array.from(searchResults).map((element, index) => {
element.setAttribute('data-processed', '1');
return this.createDateElement(element, index);
}).filter(Boolean);
await Promise.all(elements.map(async (newElement, index) => {
const originalElement = searchResults[index];
const pathInfo = this.extractPathInfo(originalElement);
if (!pathInfo) {
console.log('Could not extract path info from:', originalElement);
return;
}
try {
const date = await this.fetchCommitDate(pathInfo);
if (date) {
this.updateElementWithDate(newElement, date);
}
} catch (error) {
console.error('Error for path:', pathInfo, error);
const titleElement = newElement.querySelector('.search-title');
if (titleElement) {
titleElement.textContent = 'Error fetching date';
}
}
}));
}
}
new GitHubDateAdder();
})();