// // Timestamp helpers. Gitea returns RFC3339 strings and uses the Go zero time // ("0001-01-01T00:00:00Z") to mean "never", which must not be shown as a date. // const ZERO_YEAR = 1; function parse(value) { if (!value) return null; if (typeof value === "number") { // Gitea occasionally hands back Unix seconds for job timestamps. return value > 0 ? new Date(value * 1000) : null; } const date = new Date(value); if (Number.isNaN(date.getTime()) || date.getUTCFullYear() <= ZERO_YEAR) return null; return date; } function relative(value) { const date = parse(value); if (!date) return ""; const seconds = Math.round((Date.now() - date.getTime()) / 1000); const future = seconds < 0; const abs = Math.abs(seconds); let text; if (abs < 45) text = "just now"; else if (abs < 90) text = "a minute"; else if (abs < 3600) text = `${Math.round(abs / 60)}m`; else if (abs < 86400) text = `${Math.round(abs / 3600)}h`; else if (abs < 2592000) text = `${Math.round(abs / 86400)}d`; else if (abs < 31536000) text = `${Math.round(abs / 2592000)}mo`; else text = `${Math.round(abs / 31536000)}y`; if (text === "just now") return text; return future ? `in ${text}` : `${text} ago`; } function duration(start, end) { const from = parse(start); if (!from) return ""; const to = parse(end) || new Date(); let seconds = Math.max(0, Math.round((to.getTime() - from.getTime()) / 1000)); const hours = Math.floor(seconds / 3600); seconds -= hours * 3600; const minutes = Math.floor(seconds / 60); seconds -= minutes * 60; if (hours) return `${hours}h ${minutes}m`; if (minutes) return `${minutes}m ${seconds}s`; return `${seconds}s`; } function absolute(value) { const date = parse(value); if (!date) return ""; return date.toLocaleString(); } exports.parse = parse; exports.relative = relative; exports.duration = duration; exports.absolute = absolute;