GitHub Date Formatter

A userscript to convert GitHub's relative timestamps and commit dates into standard local datetime formats.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         GitHub Date Formatter
// @name:zh-CN   GitHub 日期时间格式优化
// @namespace    https://github.com/abevol/github-date-formatter
// @version      1.0.2
// @description  A userscript to convert GitHub's relative timestamps and commit dates into standard local datetime formats.
// @description:zh-CN 将 GitHub 网页中的相对时间与英文日期统一转换为直观易读的本地标准日期格式。
// @author       Abevol
// @match        https://github.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 格式化函数:仅输出 YYYY-MM-DD hh:mm:ss,不要时区后缀
    function formatLocalTimeNoZone(isoString) {
        const date = new Date(isoString);
        if (isNaN(date.getTime())) return null;

        const pad = (num) => String(num).padStart(2, '0');

        const YYYY = date.getFullYear();
        const MM = pad(date.getMonth() + 1);
        const DD = pad(date.getDate());
        const hh = pad(date.getHours());
        const mm = pad(date.getMinutes());
        const ss = pad(date.getSeconds());

        return `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}`;
    }

    // 格式化函数:仅输出标题所需的 YYYY-MM-DD
    function formatPureDate(dateText) {
        if (!dateText) return null;
        const pad = (num) => String(num).padStart(2, '0');

        // 1. 中文/带分隔符的年月日格式匹配 (例如 2026年9月1日 / 2026-09-01)
        const matchYMD = dateText.match(/(\d{4})[年\-\/.]\s*(\d{1,2})[月\-\/.]\s*(\d{1,2})/);
        if (matchYMD) {
            return `${matchYMD[1]}-${pad(matchYMD[2])}-${pad(matchYMD[3])}`;
        }

        // 2. 英文形式 "Commits on Month DD, YYYY" / "Commits on DD Month YYYY"
        const cleanText = dateText.replace(/Commits\s+on\s+/i, '').trim();
        const date = new Date(cleanText);
        if (!isNaN(date.getTime())) {
            return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
        }

        return null;
    }

    function cleanGitHubContent() {
        // --- 1. 处理时间戳标签 ---
        const timeElements = document.querySelectorAll('relative-time, time-ago, local-time');
        timeElements.forEach(el => {
            if (el.getAttribute('data-time-fixed') === 'true') return;

            const isoStr = el.getAttribute('datetime');
            if (isoStr) {
                const formatted = formatLocalTimeNoZone(isoStr);
                if (formatted) {
                    const newSpan = document.createElement('span');
                    if (el.hasAttributes()) {
                        for (const attr of el.attributes) {
                            if (attr.name !== 'datetime' && attr.name !== 'prefix') {
                                newSpan.setAttribute(attr.name, attr.value);
                            }
                        }
                    }
                    newSpan.textContent = formatted;
                    newSpan.setAttribute('data-time-fixed', 'true');
                    el.parentNode.replaceChild(newSpan, el);
                }
            }
        });

        // --- 2. 处理 Commit 分组标题 ---
        const commitTitles = document.querySelectorAll('[data-testid="commit-group-title"], .commit-group-title');
        commitTitles.forEach(titleEl => {
            const originalText = titleEl.textContent ? titleEl.textContent.trim() : '';
            if (!originalText || /^\d{4}-\d{2}-\d{2}$/.test(originalText)) return;

            const cleanDate = formatPureDate(originalText);
            if (cleanDate && cleanDate !== originalText) {
                titleEl.textContent = cleanDate;
            }
        });
    }

    // 刚进入页面时跑一次
    cleanGitHubContent();

    // 持续监听单页应用的动态加载,随时清洗新蹦出来的节点
    const observer = new MutationObserver(cleanGitHubContent);
    observer.observe(document.body, { childList: true, subtree: true });
})();