GitHub Repo Age

Displays repository creation date/time/age.

As of 11/05/2025. See the latest version.

// ==UserScript==
// @name         GitHub Repo Age
// @description  Displays repository creation date/time/age.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.3
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://github.com/*/*
// @grant        GM_xmlhttpRequest
// @connect      api.codetabs.com
// @connect      api.github.com
// ==/UserScript==

(function () {
    'use strict';
    const githubApiBase = 'https://api.github.com/repos/';
    const fallbackApiBase = 'https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/';
    const CACHE_KEY_PREFIX = 'github_repo_created_';

    const selectors = {
        desktop: [
            '.BorderGrid-cell .hide-sm.hide-md .f4.my-3',
            '.BorderGrid-cell'
        ],
        mobile: [
            '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5 .f4.mb-3.color-fg-muted',
            '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5 .d-flex.gap-2.mt-n3.mb-3.flex-wrap',
            '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5'
        ]
    };

    let currentRepoPath = '';

    function formatDate(isoDateStr) {
        const createdDate = new Date(isoDateStr);
        const now = new Date();
        const diffTime = Math.abs(now - createdDate);

        const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
        const diffHours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
        const diffMinutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60));

        const diffMonths = Math.floor(diffDays / 30.44);
        const diffYears = Math.floor(diffMonths / 12);
        const remainingMonths = diffMonths % 12;
        const remainingDays = Math.floor(diffDays % 30.44);

        const datePart = createdDate.toLocaleDateString('en-GB', {
            day: '2-digit',
            month: 'short',
            year: 'numeric'
        });

        const timePart = createdDate.toLocaleTimeString('en-GB', {
            hour: '2-digit',
            minute: '2-digit',
            hour12: false
        });

        let ageText = '';

        if (diffYears > 0) {
            ageText = `${diffYears} year${diffYears !== 1 ? 's' : ''}`;
            if (remainingMonths > 0) {
                ageText += ` ${remainingMonths} month${remainingMonths !== 1 ? 's' : ''}`;
            }
        } else if (diffMonths > 0) {
            ageText = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`;
            if (remainingDays > 0) {
                ageText += ` ${remainingDays} day${remainingDays !== 1 ? 's' : ''}`;
            }
        } else if (diffDays > 0) {
            ageText = `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
            if (diffHours > 0) {
                ageText += ` ${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
            }
        } else if (diffHours > 0) {
            ageText = `${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
            if (diffMinutes > 0) {
                ageText += ` ${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`;
            }
        } else {
            ageText = `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`;
        }

        return `${datePart} - ${timePart} (${ageText} ago)`;
    }

    const cache = {
        getKey: function(user, repo) {
            return `${CACHE_KEY_PREFIX}${user}_${repo}`;
        },

        get: function(user, repo) {
            try {
                const key = this.getKey(user, repo);
                const cachedValue = localStorage.getItem(key);
                if (!cachedValue) return null;
                return JSON.parse(cachedValue);
            } catch (err) {
                return null;
            }
        },

        set: function(user, repo, value) {
            try {
                const key = this.getKey(user, repo);
                localStorage.setItem(key, JSON.stringify(value));
            } catch (err) {

            }
        }
    };

    async function fetchFromGitHubApi(user, repo) {
        const apiUrl = `${githubApiBase}${user}/${repo}`;
        
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl,
                headers: {
                    'Accept': 'application/vnd.github.v3+json'
                },
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            const createdAt = data.created_at;
                            if (createdAt) {
                                resolve({ success: true, data: createdAt });
                            } else {
                                resolve({ success: false, error: 'Missing creation date' });
                            }
                        } catch (e) {
                            resolve({ success: false, error: 'JSON parse error' });
                        }
                    } else {
                        resolve({ 
                            success: false, 
                            error: `Status ${response.status}`, 
                            useProxy: response.status === 403 || response.status === 429
                        });
                    }
                },
                onerror: function() {
                    resolve({ success: false, error: 'Network error', useProxy: true });
                },
                ontimeout: function() {
                    resolve({ success: false, error: 'Timeout', useProxy: true });
                }
            });
        });
    }

    async function fetchFromProxyApi(user, repo) {
        const apiUrl = `${fallbackApiBase}${user}/${repo}`;
        
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const data = JSON.parse(response.responseText);
                            const createdAt = data.created_at;
                            if (createdAt) {
                                resolve({ success: true, data: createdAt });
                            } else {
                                resolve({ success: false, error: 'Missing creation date' });
                            }
                        } catch (e) {
                            resolve({ success: false, error: 'JSON parse error' });
                        }
                    } else {
                        resolve({ success: false, error: `Status ${response.status}` });
                    }
                },
                onerror: function() {
                    resolve({ success: false, error: 'Network error' });
                },
                ontimeout: function() {
                    resolve({ success: false, error: 'Timeout' });
                }
            });
        });
    }

    async function getRepoCreationDate(user, repo) {
        const cachedDate = cache.get(user, repo);
        if (cachedDate) {
            return cachedDate;
        }

        const directResult = await fetchFromGitHubApi(user, repo);
        
        if (directResult.success) {
            cache.set(user, repo, directResult.data);
            return directResult.data;
        }
        
        if (directResult.useProxy) {
            console.log('GitHub Repo Age: Use Proxy');
            const proxyResult = await fetchFromProxyApi(user, repo);
            
            if (proxyResult.success) {
                cache.set(user, repo, proxyResult.data);
                return proxyResult.data;
            }
        }
        
        return null;
    }

    async function insertCreatedDate() {
        const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
        if (!match) return false;

        const [_, user, repo] = match;
        const repoPath = `${user}/${repo}`;

        currentRepoPath = repoPath;

        const createdAt = await getRepoCreationDate(user, repo);
        if (!createdAt) return false;

        const formattedDate = formatDate(createdAt);
        let insertedCount = 0;

        document.querySelectorAll('.repo-created-date').forEach(el => el.remove());

        for (const [view, selectorsList] of Object.entries(selectors)) {
            for (const selector of selectorsList) {
                const element = document.querySelector(selector);
                if (element && !element.querySelector(`.repo-created-${view}`)) {
                    insertDateElement(element, formattedDate, view);
                    insertedCount++;
                    break;
                }
            }
        }

        return insertedCount > 0;
    }

    function insertDateElement(targetElement, formattedDate, view) {
        const p = document.createElement('p');
        p.className = `f6 color-fg-muted repo-created-date repo-created-${view}`;
        p.style.marginTop = '4px';
        p.style.marginBottom = '8px';
        p.innerHTML = `<strong>Created</strong> ${formattedDate}`;

        if (view === 'mobile') {
            const flexWrap = targetElement.querySelector('.flex-wrap');
            if (flexWrap) {
                flexWrap.parentNode.insertBefore(p, flexWrap.nextSibling);
                return;
            }

            const dFlex = targetElement.querySelector('.d-flex');
            if (dFlex) {
                dFlex.parentNode.insertBefore(p, dFlex.nextSibling);
                return;
            }
        }

        targetElement.insertBefore(p, targetElement.firstChild);
    }

    function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) {
        insertCreatedDate().then(inserted => {
            if (!inserted && retryCount < maxRetries) {
                const delay = Math.pow(2, retryCount) * 500;
                setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay);
            }
        });
    }

    function checkForRepoChange() {
        const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
        if (!match) return;

        const [_, user, repo] = match;
        const repoPath = `${user}/${repo}`;

        if (repoPath !== currentRepoPath) {
            checkAndInsertWithRetry();
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => checkAndInsertWithRetry());
    } else {
        checkAndInsertWithRetry();
    }

    const originalPushState = history.pushState;
    history.pushState = function() {
        originalPushState.apply(this, arguments);
        setTimeout(checkForRepoChange, 100);
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function() {
        originalReplaceState.apply(this, arguments);
        setTimeout(checkForRepoChange, 100);
    };

    window.addEventListener('popstate', () => {
        setTimeout(checkForRepoChange, 100);
    });

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.type === 'childList' &&
                (mutation.target.id === 'js-repo-pjax-container' ||
                 mutation.target.id === 'repository-container-header')) {
                setTimeout(checkForRepoChange, 100);
                break;
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();