From 4887be96f5ed42425e139f2ef327212e37f8669a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 30 Nov 2025 11:06:04 +0100 Subject: [PATCH] AboutTab: fix arch commit detection GitHubService: add optional TOKEN auth --- Modules/Panels/Settings/Tabs/AboutTab.qml | 13 ++- Services/Noctalia/GitHubService.qml | 117 ++++++++++++++++++---- 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index af1a1495..72f4abb7 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -25,13 +25,13 @@ ColumnLayout { spacing: Style.marginL Component.onCompleted: { + GitHubService.ensureDataFresh(); + if (root.isGitVersion) { - // First try to extract from Arch package version format (e.g., 3.4.0.r112.g3f00bec8-1) var archHash = root.extractCommitFromArchVersion(); if (archHash) { root.gitCommitHash = archHash; } else { - // Fall back to git command for git-cloned installations fetchGitCommit(); } } @@ -40,15 +40,18 @@ ColumnLayout { function hasArchGitVersion() { // Check if version matches Arch package format: X.Y.Z.rN.gHASH-REV // Pattern: version.r.g- - return /\.r\d+\.g[0-9a-fA-F]+-/.test(root.currentVersion); + // Handle both with and without "v" prefix: v3.4.0.r112.g3f00bec8-1 or 3.4.0.r112.g3f00bec8-1 + var version = root.currentVersion.replace(/^v/, ""); // Remove "v" prefix if present + return /\.r\d+\.g[0-9a-fA-F]+-/.test(version); } function extractCommitFromArchVersion() { // Extract commit hash from Arch package version format // Format: 3.4.0.r112.g3f00bec8-1 or v3.4.0.r112.g3f00bec8-1 // We want to extract the hash after ".g" and before "-" - var match = root.currentVersion.match(/\.g([0-9a-fA-F]+)-/i); - if (match && match[1]) { + var version = root.currentVersion.replace(/^v/, ""); // Remove "v" prefix if present + var match = version.match(/\.g([0-9a-fA-F]+)-/i); + if (match && match[1] && match[1].length >= 7) { // Return first 7 characters (short hash) return match[1].substring(0, 7); } diff --git a/Services/Noctalia/GitHubService.qml b/Services/Noctalia/GitHubService.qml index ea8d103a..bd285f17 100644 --- a/Services/Noctalia/GitHubService.qml +++ b/Services/Noctalia/GitHubService.qml @@ -14,6 +14,10 @@ Singleton { property bool isFetchingData: false readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy + // GitHub API authentication + readonly property string githubToken: Quickshell.env("GITHUB_TOKEN") || "" + readonly property bool hasAuth: githubToken !== "" + // Public properties for easy access property string latestVersion: I18n.tr("system.unknown-version") property var contributors: [] @@ -44,10 +48,11 @@ Singleton { } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { - // Fetch data after a short delay to ensure file is created - Qt.callLater(() => { - fetchFromGitHub(); - }); + // File doesn't exist - don't fetch on startup, just initialize empty + Logger.d("GitHub", "Cache file doesn't exist, will fetch when needed"); + data.version = I18n.tr("system.unknown-version"); + data.contributors = []; + data.timestamp = 0; } } @@ -72,7 +77,7 @@ Singleton { var needsRefetch = false; if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { needsRefetch = true; - Logger.d("GitHub", "Cache expired or missing, scheduling fetch"); + Logger.d("GitHub", "Cache expired or missing, will fetch when needed"); } else { Logger.d("GitHub", "Loading cached GitHub data (age:", Math.round((now - data.timestamp) / 60), "minutes)"); } @@ -84,7 +89,16 @@ Singleton { root.contributors = data.contributors; } - if (needsRefetch) { + // Don't fetch on startup - only fetch when explicitly requested (e.g., when About tab opens) + // This prevents rate limiting on startup + } + + // -------------------------------- + function ensureDataFresh() { + // Check if we need to fetch and do so if needed + const now = Time.timestamp; + if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { + Logger.d("GitHub", "Cache expired, fetching fresh data"); fetchFromGitHub(); } } @@ -136,6 +150,19 @@ Singleton { fetchFromGitHub(); } + // -------------------------------- + function handleRateLimit() { + Logger.w("GitHub", "Rate limit hit - extending cache expiration to avoid further requests"); + // Extend cache expiration by 1 hour to avoid hitting rate limit again + if (data.timestamp) { + data.timestamp = Time.timestamp + githubUpdateFrequency; + } + + if (!root.hasAuth) { + Logger.w("GitHub", "Consider setting GITHUB_TOKEN environment variable to increase rate limit from 60 to 5000 requests/hour"); + } + } + // -------------------------------- // Avatar Caching Functions // -------------------------------- @@ -419,14 +446,42 @@ Singleton { Process { id: versionProcess - command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest"] + command: root.hasAuth ? ["sh", "-c", "curl -s -w '\\n%{http_code}' -H 'Authorization: token " + root.githubToken + "' 'https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest'"] : ["sh", "-c", "curl -s -w '\\n%{http_code}' 'https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest'"] stdout: StdioCollector { onStreamFinished: { try { - const response = text; - if (response && response.trim()) { - const data = JSON.parse(response); + var response = text; + if (!response || !response.trim()) { + Logger.w("GitHub", "Empty response from GitHub API"); + checkAndSaveData(); + return; + } + + // Extract HTTP status code (last line) and response body + var lines = response.trim().split('\n'); + var statusCode = lines.length > 1 ? parseInt(lines[lines.length - 1]) : 200; + var responseBody = lines.length > 1 ? lines.slice(0, -1).join('\n') : response; + + // Check for rate limit status codes + if (statusCode === 403 || statusCode === 429) { + Logger.w("GitHub", "Rate limit hit (HTTP", statusCode + ")"); + root.handleRateLimit(); + checkAndSaveData(); + return; + } + + if (responseBody && responseBody.trim()) { + const data = JSON.parse(responseBody); + + // Check for rate limit in response message + if (data.message && (data.message.includes("API rate limit") || data.message.includes("rate limit"))) { + Logger.w("GitHub", "Rate limit exceeded:", data.message); + root.handleRateLimit(); + checkAndSaveData(); + return; + } + if (data.tag_name) { const version = data.tag_name; root.data.version = version; @@ -437,8 +492,6 @@ Singleton { } else { Logger.w("GitHub", "No tag_name in GitHub response"); } - } else { - Logger.w("GitHub", "Empty response from GitHub API"); } } catch (e) { Logger.e("GitHub", "Failed to parse version:", e); @@ -453,21 +506,51 @@ Singleton { Process { id: contributorsProcess - command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100"] + command: root.hasAuth ? ["sh", "-c", "curl -s -w '\\n%{http_code}' -H 'Authorization: token " + root.githubToken + "' 'https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100'"] : ["sh", "-c", "curl -s -w '\\n%{http_code}' 'https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100'"] stdout: StdioCollector { onStreamFinished: { try { - const response = text; + var response = text; Logger.d("GitHub", "Raw contributors response length:", response ? response.length : 0); - if (response && response.trim()) { - const data = JSON.parse(response); + + if (!response || !response.trim()) { + Logger.w("GitHub", "Empty response from GitHub API for contributors"); + root.data.contributors = []; + root.contributors = []; + checkAndSaveData(); + return; + } + + // Extract HTTP status code (last line) and response body + var lines = response.trim().split('\n'); + var statusCode = lines.length > 1 ? parseInt(lines[lines.length - 1]) : 200; + var responseBody = lines.length > 1 ? lines.slice(0, -1).join('\n') : response; + + // Check for rate limit status codes + if (statusCode === 403 || statusCode === 429) { + Logger.w("GitHub", "Rate limit hit (HTTP", statusCode + ")"); + root.handleRateLimit(); + checkAndSaveData(); + return; + } + + if (responseBody && responseBody.trim()) { + const data = JSON.parse(responseBody); + + // Check for rate limit in response message + if (data.message && (data.message.includes("API rate limit") || data.message.includes("rate limit"))) { + Logger.w("GitHub", "Rate limit exceeded:", data.message); + root.handleRateLimit(); + checkAndSaveData(); + return; + } + Logger.d("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array"); root.data.contributors = data || []; root.contributors = root.data.contributors; Logger.d("GitHub", "Contributors fetched from GitHub:", root.contributors.length); } else { - Logger.w("GitHub", "Empty response from GitHub API for contributors"); root.data.contributors = []; root.contributors = []; }