diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0b8e7..8714156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog -## [2.4.1] - Current Changes +## [2.4.2] + +### Fixed + +- Enhanced handling for directories that are not Git repositories, eliminating unnecessary error notifications. + +## [2.4.1] ### Added diff --git a/package-lock.json b/package-lock.json index bca1317..b4822e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/mocha": "^10.0.9", "@types/mock-fs": "^4.13.4", - "@types/node": "22.7.5", + "@types/node": "22.7.6", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/vscode": "^1.93.1", @@ -52,14 +52,14 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.8.0.tgz", - "integrity": "sha512-YvFMowkXzLbXNM11yZtVLhUCmuG0ex7JKOH366ipjmHBhL3vpDcPAeWF+jf0X+jVXwFqo3UhsWUq4kH0ZPdu/g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", "dev": true, "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.1.0", + "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" }, "engines": { @@ -158,9 +158,9 @@ } }, "node_modules/@azure/core-util": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.10.0.tgz", - "integrity": "sha512-dqLWQsh9Nro1YQU+405POVtXnwrIVqPyfUzc4zXCbThTg7+vNNaiMkwbX9AMXKyoFYFClxmB3s25ZFr3+jZkww==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1092,9 +1092,9 @@ } }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", "dev": true, "license": "MIT", "dependencies": { @@ -1133,17 +1133,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.9.0.tgz", - "integrity": "sha512-Y1n621OCy4m7/vTXNlCbMVp87zSd7NH0L9cXD8aIpOaNlzeWxIK4+Q19A68gSmTNRZn92UjocVUWDthGxtqHFg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz", + "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/type-utils": "8.9.0", - "@typescript-eslint/utils": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/type-utils": "8.10.0", + "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1167,16 +1167,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.9.0.tgz", - "integrity": "sha512-U+BLn2rqTTHnc4FL3FJjxaXptTxmf9sNftJK62XLz4+GxG3hLHm/SUNaaXP5Y4uTiuYoL5YLy4JBCJe3+t8awQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz", + "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4" }, "engines": { @@ -1196,14 +1196,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.9.0.tgz", - "integrity": "sha512-bZu9bUud9ym1cabmOYH9S6TnbWRzpklVmwqICeOulTCZ9ue2/pczWzQvt/cGj2r2o1RdKoZbuEMalJJSYw3pHQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1214,14 +1214,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.9.0.tgz", - "integrity": "sha512-JD+/pCqlKqAk5961vxCluK+clkppHY07IbV3vett97KOV+8C6l+CPEPwpUuiMwgbOz/qrN3Ke4zzjqbT+ls+1Q==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz", + "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.9.0", - "@typescript-eslint/utils": "8.9.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/utils": "8.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1239,9 +1239,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.9.0.tgz", - "integrity": "sha512-SjgkvdYyt1FAPhU9c6FiYCXrldwYYlIQLkuc+LfAhCna6ggp96ACncdtlbn8FmnG72tUkXclrDExOpEYf1nfJQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", "dev": true, "license": "MIT", "engines": { @@ -1253,14 +1253,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.9.0.tgz", - "integrity": "sha512-9iJYTgKLDG6+iqegehc5+EqE6sqaee7kb8vWpmHZ86EqwDjmlqNNHeqDVqb9duh+BY6WCNHfIGvuVU3Tf9Db0g==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/visitor-keys": "8.9.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1282,16 +1282,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.9.0.tgz", - "integrity": "sha512-PKgMmaSo/Yg/F7kIZvrgrWa1+Vwn036CdNUvYFEkYbPwOH4i8xvkaRlu148W3vtheWK9ckKRIz7PBP5oUlkrvQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.9.0", - "@typescript-eslint/types": "8.9.0", - "@typescript-eslint/typescript-estree": "8.9.0" + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1305,13 +1305,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.9.0.tgz", - "integrity": "sha512-Ht4y38ubk4L5/U8xKUBfKNYGmvKvA1CANoxiTRMM+tOLk3lbF3DvzZCxJCRSE+2GdCMSh6zq9VZJc3asc1XuAA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.9.0", + "@typescript-eslint/types": "8.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1658,9 +1658,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "license": "MIT", "bin": { @@ -5379,9 +5379,9 @@ } }, "node_modules/node-abi": { - "version": "3.70.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.70.0.tgz", - "integrity": "sha512-xMTIZdvAyzGyxwOwxXv/8V/f/KAqKWNCeNIIFu2doEtQp9wvMUTam036At/iVtJqum6n5ljbAhUmXAUOhyivSA==", + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", "dev": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index 0b29029..9b6580c 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "devDependencies": { "@types/mocha": "^10.0.9", "@types/mock-fs": "^4.13.4", - "@types/node": "22.7.5", + "@types/node": "22.7.6", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/vscode": "^1.93.1", diff --git a/src/commands/pushAndCheckBuild.ts b/src/commands/pushAndCheckBuild.ts index 43d379b..02f63f6 100644 --- a/src/commands/pushAndCheckBuild.ts +++ b/src/commands/pushAndCheckBuild.ts @@ -86,7 +86,7 @@ export async function pollBuildStatusImmediate( } if (status === 'no_runs' && attempt === 0) { - vscode.window.showInformationMessage(`Waiting for CI to start for ${isTag ? 'tag' : 'branch'} ${ref} (${owner}/${repo})`); + vscode.window.showInformationMessage(`Waiting for ${ciType} to start build for ${isTag ? 'tag' : 'branch'} ${ref} (${owner}/${repo})`); } const interval = inProgressStatuses.includes(status) || status === 'no_runs' diff --git a/src/extension.ts b/src/extension.ts index 12c0d75..b1a9866 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,12 @@ import * as vscode from "vscode"; -import { GitService } from "./services/gitService"; -import { StatusBarService } from "./services/statusBarService"; -import { CIService } from "./services/ciService"; -import { createTag } from "./commands/createTag"; -import { createStatusBarUpdater } from "./utils/statusBarUpdater"; -import { pushAndCheckBuild } from "./commands/pushAndCheckBuild"; -import { Logger } from './utils/logger'; +import {GitService} from "./services/gitService"; +import {StatusBarService} from "./services/statusBarService"; +import {CIService} from "./services/ciService"; +import {createTag} from "./commands/createTag"; +import {createStatusBarUpdater} from "./utils/statusBarUpdater"; +import {pushAndCheckBuild} from "./commands/pushAndCheckBuild"; +import {Logger} from "./utils/logger"; +import {debounce} from "./utils/debounce"; let gitService: GitService | null = null; let statusBarService: StatusBarService | null = null; @@ -15,10 +16,13 @@ export async function activate(context: vscode.ExtensionContext) { // Initialize the logger Logger.initialize(context); - Logger.log("Activating extension...", 'INFO'); - + Logger.log("Activating extension...", "INFO"); + let isInitialized = false; // Function to initialize GitService and StatusBarService const initializeServices = async () => { + if (isInitialized) { + return; // Prevent reinitialization + } const activeEditor = vscode.window.activeTextEditor; if (activeEditor) { const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); @@ -27,24 +31,26 @@ export async function activate(context: vscode.ExtensionContext) { gitService = new GitService(context); const gitInitialized = await gitService.initialize(); if (!gitInitialized) { - Logger.log("GitService failed to initialize, continuing without it.", 'WARNING'); + Logger.log("GitService failed to initialize, continuing without it.", "WARNING"); gitService = null; // Ensure gitService is null if initialization fails + clearStatusBarAndState(); } else { context.subscriptions.push(gitService); setupStatusBarService(context); + isInitialized = true; } } } else { - Logger.log("No active repository detected. Please open a file within a Git repository.", 'WARNING'); + Logger.log("No active repository detected. Please open a file within a Git repository.", "WARNING"); } } }; // Add a listener for configuration changes context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration(async (event) => { - if (event.affectsConfiguration('gitTagReleaseTracker.ciProviders')) { - Logger.log("CI Provider configuration changed, validating...", 'INFO'); + vscode.workspace.onDidChangeConfiguration(async event => { + if (event.affectsConfiguration("gitTagReleaseTracker.ciProviders")) { + Logger.log("CI Provider configuration changed, validating...", "INFO"); await validateCIConfiguration(); } }) @@ -53,11 +59,14 @@ export async function activate(context: vscode.ExtensionContext) { // Initial attempt to initialize services await initializeServices(); - // Listen for changes in the active text editor - vscode.window.onDidChangeActiveTextEditor(async () => { - Logger.log("Active editor changed, attempting to initialize services...", 'INFO'); + // Debounce the function to handle active editor changes + const debouncedInitializeServices = debounce(async () => { + Logger.log("Active editor changed, attempting to initialize services...", "INFO"); await initializeServices(); - }); + }, 3000); // Adjust the debounce delay as needed + + // Listen for changes in the active text editor + vscode.window.onDidChangeActiveTextEditor(debouncedInitializeServices); // Register commands once registerCommands(context); @@ -75,7 +84,7 @@ function setupStatusBarService(context: vscode.ExtensionContext) { "extension.createMinorTag", "extension.createPatchTag", "extension.createInitialTag", - "extension.openCompareLink", + "extension.openCompareLink" ], context, gitService, @@ -89,7 +98,7 @@ function setupStatusBarService(context: vscode.ExtensionContext) { statusBarUpdater.debouncedUpdate(false); }); } else { - Logger.log("GitService is not initialized, skipping status bar setup.", 'WARNING'); + Logger.log("GitService is not initialized, skipping status bar setup.", "WARNING"); } } @@ -103,7 +112,7 @@ function registerCommands(context: vscode.ExtensionContext) { }; const commands = { - 'extension.createMajorTag': async () => { + "extension.createMajorTag": async () => { if (gitService && statusBarService && ciService) { const defaultBranch = await gitService.getDefaultBranch(); if (defaultBranch) { @@ -111,7 +120,7 @@ function registerCommands(context: vscode.ExtensionContext) { } } }, - 'extension.createMinorTag': async () => { + "extension.createMinorTag": async () => { if (gitService && statusBarService && ciService) { const defaultBranch = await gitService.getDefaultBranch(); if (defaultBranch) { @@ -119,7 +128,7 @@ function registerCommands(context: vscode.ExtensionContext) { } } }, - 'extension.createPatchTag': async () => { + "extension.createPatchTag": async () => { if (gitService && statusBarService && ciService) { const defaultBranch = await gitService.getDefaultBranch(); if (defaultBranch) { @@ -127,7 +136,7 @@ function registerCommands(context: vscode.ExtensionContext) { } } }, - 'extension.createInitialTag': async () => { + "extension.createInitialTag": async () => { if (gitService && statusBarService && ciService) { const defaultBranch = await gitService.getDefaultBranch(); if (defaultBranch) { @@ -135,44 +144,44 @@ function registerCommands(context: vscode.ExtensionContext) { } } }, - 'extension.openCompareLink': () => { + "extension.openCompareLink": () => { if (statusBarService) { const url = statusBarService.getCompareUrl(); if (url) { vscode.env.openExternal(vscode.Uri.parse(url)); } else { - vscode.window.showErrorMessage('No compare link available.'); + vscode.window.showErrorMessage("No compare link available."); } } }, - 'extension.openTagBuildStatus': () => { + "extension.openTagBuildStatus": () => { if (statusBarService) { const url = statusBarService.getTagBuildStatusUrl(); if (url) { vscode.env.openExternal(vscode.Uri.parse(url)); } else { - vscode.window.showErrorMessage('No tag build status URL available.'); + vscode.window.showErrorMessage("No tag build status URL available."); } } }, - 'extension.openBranchBuildStatus': () => { + "extension.openBranchBuildStatus": () => { if (statusBarService) { const url = statusBarService.getBranchBuildStatusUrl(); if (url) { vscode.env.openExternal(vscode.Uri.parse(url)); } else { - vscode.window.showErrorMessage('No branch build status URL available.'); + vscode.window.showErrorMessage("No branch build status URL available."); } } }, - 'extension.pushAndCheckBuild': refreshAfterPush, + "extension.pushAndCheckBuild": refreshAfterPush }; for (const [commandId, handler] of Object.entries(commands)) { context.subscriptions.push(vscode.commands.registerCommand(commandId, handler)); } - const showLogsCommand = vscode.commands.registerCommand('gitTagReleaseTracker.showLogs', () => { + const showLogsCommand = vscode.commands.registerCommand("gitTagReleaseTracker.showLogs", () => { Logger.show(); }); @@ -188,7 +197,7 @@ export function deactivate() { async function validateCIConfiguration() { // Check if gitService is initialized if (!gitService) { - Logger.log("GitService is not initialized, skipping CI configuration validation.", 'WARNING'); + Logger.log("GitService is not initialized, skipping CI configuration validation.", "WARNING"); return; // Exit if gitService is not initialized } @@ -198,13 +207,23 @@ async function validateCIConfiguration() { } const ciType = await gitService.detectCIType(); - const config = vscode.workspace.getConfiguration('gitTagReleaseTracker'); - const ciProviders = config.get<{ [key: string]: { token: string, apiUrl: string } }>('ciProviders', {}); + const config = vscode.workspace.getConfiguration("gitTagReleaseTracker"); + const ciProviders = config.get<{[key: string]: {token: string; apiUrl: string}}>("ciProviders", {}); if (ciType && (!ciProviders[ciType]?.token || !ciProviders[ciType]?.apiUrl)) { - Logger.log(`CI Provider ${ciType} is not properly configured.`, 'WARNING'); + Logger.log(`CI Provider ${ciType} is not properly configured.`, "WARNING"); vscode.window.showErrorMessage(`CI Provider ${ciType} is not properly configured.`); } else { - Logger.log(`CI Provider ${ciType} is properly configured.`, 'INFO'); + Logger.log(`CI Provider ${ciType} is properly configured.`, "INFO"); } -} \ No newline at end of file +} + +function clearStatusBarAndState() { + if (statusBarService) { + statusBarService.clearAllItems(); // Clear all status bar items + } + if (gitService) { + gitService.dispose(); // Dispose of gitService + gitService = null; // Reset gitService + } +} diff --git a/src/services/ciService.ts b/src/services/ciService.ts index 126ef98..2acfda9 100644 --- a/src/services/ciService.ts +++ b/src/services/ciService.ts @@ -1,6 +1,6 @@ -import * as vscode from 'vscode'; -import axios, { AxiosError, AxiosResponse } from 'axios'; -import { Logger } from '../utils/logger'; +import * as vscode from "vscode"; +import axios, {AxiosError, AxiosResponse} from "axios"; +import {Logger} from "../utils/logger"; interface CIProvider { token: string; @@ -8,19 +8,19 @@ interface CIProvider { } export class CIService { - private providers: { [key: string]: CIProvider }; - private buildStatusCache: { - [repoKey: string]: { - [cacheKey: string]: { - status: string; - url: string; + private providers: {[key: string]: CIProvider}; + private buildStatusCache: { + [repoKey: string]: { + [cacheKey: string]: { + status: string; + url: string; message?: string; - timestamp: number; - } - } + timestamp: number; + }; + }; } = {}; private rateLimitWarningThreshold = 0.95; - private inProgressStatuses = ['pending', 'in_progress', 'queued', 'requested', 'waiting', 'running']; + private inProgressStatuses = ["pending", "in_progress", "queued", "requested", "waiting", "running"]; private inProgressCacheDuration = 10000; // 10 seconds private cacheDuration = 60000; // 1 minute cache @@ -28,16 +28,23 @@ export class CIService { this.providers = this.loadProviders(); } - private loadProviders(): { [key: string]: CIProvider } { - const config = vscode.workspace.getConfiguration('gitTagReleaseTracker'); - const ciProviders = config.get<{ [key: string]: CIProvider }>('ciProviders', {}); - + private loadProviders(): {[key: string]: CIProvider} { + const config = vscode.workspace.getConfiguration("gitTagReleaseTracker"); + const ciProviders = config.get<{[key: string]: CIProvider}>("ciProviders", {}); + return ciProviders; } - async getBuildStatus(ref: string, owner: string, repo: string, ciType: 'github' | 'gitlab', isTag: boolean, forceRefresh: boolean = false): Promise<{ status: string, url: string, message?: string } | null> { + async getBuildStatus( + ref: string, + owner: string, + repo: string, + ciType: "github" | "gitlab", + isTag: boolean, + forceRefresh: boolean = false + ): Promise<{status: string; url: string; message?: string} | null> { if (!ref) { - Logger.log(`Skipping build status fetch due to empty ref for ${owner}/${repo}`, 'INFO'); + Logger.log(`Skipping build status fetch due to empty ref for ${owner}/${repo}`, "INFO"); return null; } @@ -52,31 +59,31 @@ export class CIService { const validCacheDuration = isInProgress ? this.inProgressCacheDuration : this.cacheDuration; if (cacheAge < validCacheDuration) { - Logger.log(`Returning cached build status for ${ref} (${owner}/${repo})`, 'INFO'); + Logger.log(`Returning cached build status for ${ref} (${owner}/${repo})`, "INFO"); return cachedResult; } } - Logger.log(`Fetching fresh build status for ${ref} (${owner}/${repo}) using ${ciType}`, 'INFO'); + Logger.log(`Fetching fresh build status for ${ref} (${owner}/${repo}) using ${ciType}`, "INFO"); try { const provider = this.providers[ciType]; if (!provider || !provider.token || !provider.apiUrl) { - Logger.log(`CI Provider ${ciType} is not properly configured.`, 'WARNING'); - return { status: 'unknown', url: '', message: `CI Provider ${ciType} is not properly configured.` }; + Logger.log(`CI Provider ${ciType} is not properly configured.`, "WARNING"); + return {status: "unknown", url: "", message: `CI Provider ${ciType} is not properly configured.`}; } if (!owner || !repo) { - Logger.log('Owner or repo is not provided', 'WARNING'); - return { status: 'unknown', url: '', message: 'Unable to determine owner and repo.' }; + Logger.log("Owner or repo is not provided", "WARNING"); + return {status: "unknown", url: "", message: "Unable to determine owner and repo."}; } - let result: { status: string, url: string, message?: string, response?: { headers: any } }; - if (ciType === 'github') { + let result: {status: string; url: string; message?: string; response?: {headers: any}}; + if (ciType === "github") { result = await this.getGitHubBuildStatus(ref, owner, repo, provider, isTag); - } else if (ciType === 'gitlab') { + } else if (ciType === "gitlab") { result = await this.getGitLabBuildStatus(ref, owner, repo, provider, isTag); } else { - throw new Error('Unsupported CI type'); + throw new Error("Unsupported CI type"); } // Check rate limit after successful API call, only if headers are available @@ -85,7 +92,7 @@ export class CIService { } // Remove the 'response' property before caching and returning - const { response, ...returnResult } = result; + const {response, ...returnResult} = result; this.cacheResult(owner, repo, ref, ciType, returnResult); return returnResult; } catch (error) { @@ -93,158 +100,197 @@ export class CIService { } } - private async getGitHubBuildStatus(ref: string, owner: string, repo: string, provider: CIProvider, isTag: boolean): Promise<{ status: string, url: string, message?: string, response?: { headers: any } }> { + private async getGitHubBuildStatus( + ref: string, + owner: string, + repo: string, + provider: CIProvider, + isTag: boolean + ): Promise<{status: string; url: string; message?: string; response?: {headers: any}}> { const runsUrl = `${provider.apiUrl}/repos/${owner}/${repo}/actions/runs`; - Logger.log(`Fetching workflow runs for ${ref} from: ${runsUrl}`, 'INFO'); + Logger.log(`Fetching workflow runs for ${ref} from: ${runsUrl}`, "INFO"); + + // If it's a tag and the ref is empty, return no_runs immediately + if (isTag && !ref) { + Logger.log("Empty tag provided, returning no_runs status", "INFO"); + return { + status: "no_runs", + url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines`, + message: "No tag provided" + }; + } try { const runsResponse = await axios.get(runsUrl, { headers: { Authorization: `Bearer ${provider.token}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28' + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28" }, params: { + branch: ref, per_page: 30, exclude_pull_requests: true } }); + //console.log("GitLab API Response:", runsResponse.data); + const runs = runsResponse.data.workflow_runs; const latestRun = runs.find((run: any) => run.head_branch === ref || run.head_sha === ref); if (!latestRun) { - return { - status: 'no_runs', + return { + status: "no_runs", url: `https://github.com/${owner}/${repo}/actions`, - message: `No workflow run found for ${isTag ? 'tag' : 'branch'} ${ref}`, - response: { headers: runsResponse.headers } + message: `No workflow run found for ${isTag ? "tag" : "branch"} ${ref}`, + response: {headers: runsResponse.headers} }; } let status = latestRun.status; const conclusion = latestRun.conclusion; - if (status === 'completed') { - status = conclusion === 'success' ? 'success' : 'failure'; + if (status === "completed") { + status = conclusion === "success" ? "success" : "failure"; } - const finalStatus = status === 'in_progress' ? 'pending' : status; + const finalStatus = status === "in_progress" ? "pending" : status; - Logger.log(`GitHub CI returning status: ${status}, conclusion: ${conclusion}, final status: ${finalStatus} for ${isTag ? 'tag' : 'branch'} ${ref}`, 'INFO'); - return { + Logger.log( + `GitHub CI returning status: ${status}, conclusion: ${conclusion}, final status: ${finalStatus} for ${ + isTag ? "tag" : "branch" + } ${ref}`, + "INFO" + ); + return { status: finalStatus, url: latestRun.html_url, - message: `GitHub CI returning status: ${finalStatus} for ${isTag ? 'tag' : 'branch'} ${ref}`, - response: { headers: runsResponse.headers } + message: `GitHub CI returning status: ${finalStatus} for ${isTag ? "tag" : "branch"} ${ref}`, + response: {headers: runsResponse.headers} }; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { - Logger.log(`No GitHub Actions found for ${owner}/${repo}`, 'INFO'); - return { - status: 'no_runs', + Logger.log(`No GitHub Actions found for ${owner}/${repo}`, "INFO"); + return { + status: "no_runs", url: `https://github.com/${owner}/${repo}/actions`, message: `No GitHub Actions configured for ${owner}/${repo}` }; } - Logger.log(`Error fetching GitHub workflow runs: ${this.getErrorMessage(error)}`, 'ERROR'); - return this.handleFetchError(error, owner, repo, 'github'); + Logger.log(`Error fetching GitHub workflow runs: ${this.getErrorMessage(error)}`, "ERROR"); + return this.handleFetchError(error, owner, repo, "github"); } } - private async getGitLabBuildStatus(ref: string, owner: string, repo: string, provider: CIProvider, isTag: boolean): Promise<{ status: string, url: string, message?: string, response?: { headers: any } }> { + private async getGitLabBuildStatus( + ref: string, + owner: string, + repo: string, + provider: CIProvider, + isTag: boolean + ): Promise<{status: string; url: string; message?: string; response?: {headers: any}}> { // Ensure the API URL includes the /api/v4 path - const apiUrl = provider.apiUrl.endsWith('/api/v4') ? provider.apiUrl : `${provider.apiUrl}/api/v4`; + const apiUrl = provider.apiUrl.endsWith("/api/v4") ? provider.apiUrl : `${provider.apiUrl}/api/v4`; const pipelinesUrl = `${apiUrl}/projects/${encodeURIComponent(`${owner}/${repo}`)}/pipelines`; - Logger.log(`Fetching pipelines for ${ref} from: ${pipelinesUrl}`, 'INFO'); + Logger.log(`Fetching pipelines for ${ref} from: ${pipelinesUrl}`, "INFO"); // If it's a tag and the ref is empty, return no_runs immediately if (isTag && !ref) { - Logger.log('Empty tag provided, returning no_runs status', 'INFO'); - return { - status: 'no_runs', + Logger.log("Empty tag provided, returning no_runs status", "INFO"); + return { + status: "no_runs", url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines`, - message: 'No tag provided' + message: "No tag provided" }; } try { const pipelinesResponse = await axios.get(pipelinesUrl, { - headers: { 'PRIVATE-TOKEN': provider.token }, + headers: {"PRIVATE-TOKEN": provider.token}, params: { ref: ref, - order_by: 'id', - sort: 'desc', + order_by: "id", + sort: "desc", per_page: 1 } }); - Logger.log(`GitLab API Response: ${JSON.stringify(pipelinesResponse.data)}`, 'DEBUG'); + //console.log("GitLab API Response:", pipelinesResponse.data); const pipelines = pipelinesResponse.data; if (pipelines.length === 0) { - Logger.log(`No pipelines found for ${isTag ? 'tag' : 'branch'} ${ref}`, 'INFO'); - return { - status: 'no_runs', + Logger.log(`No pipelines found for ${isTag ? "tag" : "branch"} ${ref}`, "INFO"); + return { + status: "no_runs", url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines`, - message: `No pipeline found for ${isTag ? 'tag' : 'branch'} ${ref}`, - response: { headers: pipelinesResponse.headers } + message: `No pipeline found for ${isTag ? "tag" : "branch"} ${ref}`, + response: {headers: pipelinesResponse.headers} }; } const latestPipeline = pipelines[0]; // Check if the returned pipeline matches the requested ref if (latestPipeline.ref !== ref) { - Logger.log(`Pipeline found but doesn't match the requested ref: ${ref}`, 'INFO'); - return { - status: 'no_runs', + Logger.log(`Pipeline found but doesn't match the requested ref: ${ref}`, "INFO"); + return { + status: "no_runs", url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines`, - message: `No pipeline found for ${isTag ? 'tag' : 'branch'} ${ref}`, - response: { headers: pipelinesResponse.headers } + message: `No pipeline found for ${isTag ? "tag" : "branch"} ${ref}`, + response: {headers: pipelinesResponse.headers} }; } let status = this.mapGitLabStatus(latestPipeline.status); - Logger.log(`GitLab CI returning status: ${status} for ${isTag ? 'tag' : 'branch'} ${ref}`, 'INFO'); - return { + Logger.log(`GitLab CI returning status: ${status} for ${isTag ? "tag" : "branch"} ${ref}`, "INFO"); + return { status: status, url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines/${latestPipeline.id}`, - message: `GitLab CI returning status: ${status} for ${isTag ? 'tag' : 'branch'} ${ref}`, - response: { headers: pipelinesResponse.headers } + message: `GitLab CI returning status: ${status} for ${isTag ? "tag" : "branch"} ${ref}`, + response: {headers: pipelinesResponse.headers} }; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { - Logger.log(`No GitLab Pipelines found for ${owner}/${repo}`, 'INFO'); - return { - status: 'no_runs', + Logger.log(`No GitLab Pipelines found for ${owner}/${repo}`, "INFO"); + return { + status: "no_runs", url: `${provider.apiUrl}/${owner}/${repo}/-/pipelines`, message: `No GitLab Pipelines configured for ${owner}/${repo}` }; } - Logger.log(`Error fetching GitLab pipelines: ${this.getErrorMessage(error)}`, 'ERROR'); - return this.handleFetchError(error, owner, repo, 'gitlab'); + Logger.log(`Error fetching GitLab pipelines: ${this.getErrorMessage(error)}`, "ERROR"); + return this.handleFetchError(error, owner, repo, "gitlab"); } } private mapGitLabStatus(gitlabStatus: string): string { switch (gitlabStatus) { - case 'success': return 'success'; - case 'failed': return 'failure'; - case 'canceled': return 'cancelled'; - case 'skipped': return 'skipped'; - case 'running': return 'in_progress'; - case 'pending': return 'pending'; - case 'created': return 'queued'; - case 'manual': return 'action_required'; - default: return 'unknown'; + case "success": + return "success"; + case "failed": + return "failure"; + case "canceled": + return "cancelled"; + case "skipped": + return "skipped"; + case "running": + return "in_progress"; + case "pending": + return "pending"; + case "created": + return "queued"; + case "manual": + return "action_required"; + default: + return "unknown"; } } clearCache() { this.buildStatusCache = {}; - Logger.log('CI Service cache cleared', 'INFO'); + Logger.log("CI Service cache cleared", "INFO"); } clearCacheForRepo(owner: string, repo: string) { @@ -252,7 +298,7 @@ export class CIService { delete this.buildStatusCache[repoKey]; } - clearCacheForBranch(branch: string, owner: string, repo: string, ciType: 'github' | 'gitlab') { + clearCacheForBranch(branch: string, owner: string, repo: string, ciType: "github" | "gitlab") { const repoKey = `${owner}/${repo}`; const cacheKey = `${branch}/${ciType}`; if (this.buildStatusCache[repoKey]) { @@ -260,10 +306,16 @@ export class CIService { } } - async getImmediateBuildStatus(ref: string, owner: string, repo: string, ciType: 'github' | 'gitlab', isTag: boolean): Promise<{ status: string, url: string, message?: string }> { + async getImmediateBuildStatus( + ref: string, + owner: string, + repo: string, + ciType: "github" | "gitlab", + isTag: boolean + ): Promise<{status: string; url: string; message?: string}> { const result = await this.getBuildStatus(ref, owner, repo, ciType, isTag, true); if (!result) { - return { status: 'unknown', url: '', message: 'Unable to fetch build status' }; + return {status: "unknown", url: "", message: "Unable to fetch build status"}; } // Cache the result @@ -280,33 +332,44 @@ export class CIService { return result; } - private checkRateLimit(headers: any, ciType: 'github' | 'gitlab') { + private checkRateLimit(headers: any, ciType: "github" | "gitlab") { let limit: number, remaining: number, reset: string | number; - if (ciType === 'github') { - limit = parseInt(headers['x-ratelimit-limit'] || '0'); - remaining = parseInt(headers['x-ratelimit-remaining'] || '0'); - reset = new Date(parseInt(headers['x-ratelimit-reset'] || '0') * 1000).toLocaleTimeString(); - } else if (ciType === 'gitlab') { - limit = parseInt(headers['ratelimit-limit'] || '0'); - remaining = parseInt(headers['ratelimit-remaining'] || '0'); - reset = headers['ratelimit-reset'] || 'unknown'; + if (ciType === "github") { + limit = parseInt(headers["x-ratelimit-limit"] || "0"); + remaining = parseInt(headers["x-ratelimit-remaining"] || "0"); + reset = new Date(parseInt(headers["x-ratelimit-reset"] || "0") * 1000).toLocaleTimeString(); + } else if (ciType === "gitlab") { + limit = parseInt(headers["ratelimit-limit"] || "0"); + remaining = parseInt(headers["ratelimit-remaining"] || "0"); + reset = headers["ratelimit-reset"] || "unknown"; } else { - Logger.log(`Unknown CI type: ${ciType}, skipping rate limit check`, 'WARNING'); + Logger.log(`Unknown CI type: ${ciType}, skipping rate limit check`, "WARNING"); return; } if (limit > 0) { const usagePercentage = (limit - remaining) / limit; - + Logger.log( + `Rate limit for ${ciType}: ${usagePercentage.toFixed(1)}% used, ${remaining} remaining out of ${limit}.`, + "INFO" + ); if (usagePercentage >= this.rateLimitWarningThreshold) { - const warningMessage = `${ciType.toUpperCase()} API rate limit is at ${(usagePercentage * 100).toFixed(1)}%. Limit resets at ${reset}. Please be cautious with further requests.`; + const warningMessage = `${ciType} API rate limit is at ${(usagePercentage * 100).toFixed( + 1 + )}%. Limit resets at ${reset}. Please be cautious with further requests.`; vscode.window.showWarningMessage(warningMessage); } } } - private cacheResult(owner: string, repo: string, ref: string, ciType: string, result: { status: string, url: string, message?: string }) { + private cacheResult( + owner: string, + repo: string, + ref: string, + ciType: string, + result: {status: string; url: string; message?: string} + ) { const repoKey = `${owner}/${repo}`; const cacheKey = `${ref}/${ciType}`; if (!this.buildStatusCache[repoKey]) { @@ -316,34 +379,39 @@ export class CIService { ...result, timestamp: Date.now() }; - Logger.log(`Cached build status for ${ref} (${owner}/${repo})`, 'INFO'); + Logger.log(`Cached build status for ${ref} (${owner}/${repo})`, "INFO"); } - private handleFetchError(error: unknown, owner: string, repo: string, ciType: string): { status: string, url: string, message?: string } { - let status = 'unknown'; + private handleFetchError( + error: unknown, + owner: string, + repo: string, + ciType: string + ): {status: string; url: string; message?: string} { + let status = "unknown"; let message: string | undefined; - + if (axios.isAxiosError(error)) { if (error.response?.status === 401) { - status = 'error'; - message = 'Authentication failed. Please check your CI token in the settings.'; + status = "error"; + message = "Authentication failed. Please check your CI token in the settings."; } else if (error.response?.status === 403) { - status = 'error'; - message = 'Access forbidden. Please check your token permissions.'; + status = "error"; + message = "Access forbidden. Please check your token permissions."; } else if (error.response?.status === 404) { - status = 'error'; - message = 'Resource not found. Please check your repository path and CI configuration.'; + status = "error"; + message = "Resource not found. Please check your repository path and CI configuration."; } } - // Log the error for debugging purposes, but don't include it in the user-facing message - Logger.log(`Error fetching build status: ${this.getErrorMessage(error)}`, 'ERROR'); + Logger.log(`Error fetching build status: ${this.getErrorMessage(error)}`, "WARNING"); - return { - status: status, - url: ciType === 'github' - ? `https://github.com/${owner}/${repo}/actions` - : `${this.providers[ciType].apiUrl}/${owner}/${repo}/-/pipelines`, + return { + status: status, + url: + ciType === "github" + ? `https://github.com/${owner}/${repo}/actions` + : `${this.providers[ciType].apiUrl}/${owner}/${repo}/-/pipelines`, message: message }; } @@ -352,7 +420,7 @@ export class CIService { if (error instanceof Error) { return error.message; } - if (error && typeof error === 'object' && 'message' in error) { + if (error && typeof error === "object" && "message" in error) { return String(error.message); } return String(error); @@ -364,6 +432,6 @@ export class CIService { public reloadProviders() { this.providers = this.loadProviders(); - Logger.log('CI Providers reloaded', 'INFO'); + Logger.log("CI Providers reloaded", "INFO"); } -} \ No newline at end of file +} diff --git a/src/services/gitService.ts b/src/services/gitService.ts index 6b6e964..fb6fe9c 100644 --- a/src/services/gitService.ts +++ b/src/services/gitService.ts @@ -1,10 +1,11 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; -import { EventEmitter } from 'vscode'; -import { Logger } from '../utils/logger'; -import simpleGit, { SimpleGit } from "simple-git"; -import { debounce } from '../utils/debounce'; +import {EventEmitter} from "vscode"; +import {Logger} from "../utils/logger"; +import simpleGit, {SimpleGit} from "simple-git"; +import {debounce} from "../utils/debounce"; +import {StatusBarService} from "./statusBarService"; export interface TagResult { latest: string | null; @@ -15,21 +16,26 @@ export class GitService { private activeRepository: string | null = null; private currentBranch: string | null = null; private initialized: boolean = false; - private _onRepoChanged = new EventEmitter<{ oldRepo: string | null, newRepo: string, oldBranch: string | null, newBranch: string | null }>(); + private _onRepoChanged = new EventEmitter<{ + oldRepo: string | null; + newRepo: string | null; + oldBranch: string | null; + newBranch: string | null; + }>(); readonly onRepoChanged = this._onRepoChanged.event; - private _onBranchChanged = new EventEmitter<{ oldBranch: string | null, newBranch: string | null }>(); + private _onBranchChanged = new EventEmitter<{oldBranch: string | null; newBranch: string | null}>(); readonly onBranchChanged = this._onBranchChanged.event; private _onGitPush = new vscode.EventEmitter(); readonly onGitPush = this._onGitPush.event; private defaultBranchCache: Map = new Map(); - private cachedCIType: 'github' | 'gitlab' | null = null; - private remoteUrlCache: { [key: string]: string } = {}; - private ownerRepoCache: { [key: string]: { owner: string, repo: string } } = {}; - private tagCache: { tags: TagResult | null, timestamp: number } = { tags: null, timestamp: 0 }; + private cachedCIType: "github" | "gitlab" | null = null; + private remoteUrlCache: {[key: string]: string} = {}; + private ownerRepoCache: {[key: string]: {owner: string; repo: string}} = {}; + private tagCache: {tags: TagResult | null; timestamp: number} = {tags: null, timestamp: 0}; private readonly tagCacheDuration = 60000; // 1 minute private pushCheckInterval: NodeJS.Timeout | null = null; private branchPollingInterval: NodeJS.Timeout | null = null; - private commitCountCache: { [key: string]: { count: number, timestamp: number } } = {}; + private commitCountCache: {[key: string]: {count: number; timestamp: number}} = {}; private readonly commitCountCacheDuration = 60000; // 1 minute private lastTagFetchRepo: string | null = null; private lastRepo: string | null = null; @@ -47,35 +53,55 @@ export class GitService { private async updateActiveRepository() { const editor = vscode.window.activeTextEditor; - if (editor) { - const filePath = editor.document.uri.fsPath; - const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri); - if (workspaceFolder) { - const relativePath = path.relative(workspaceFolder.uri.fsPath, filePath); - if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) { - const newRepo = workspaceFolder.uri.fsPath; - if (newRepo !== this.activeRepository) { - const oldRepo = this.activeRepository; - this.activeRepository = newRepo; - this.git = simpleGit(this.activeRepository); - this.initialized = false; - await this.initialize(); - Logger.log(`Active repository changed to: ${this.activeRepository}`, 'INFO'); - - // Clear caches when repository changes - this.clearCaches(); - - // Emit repository change event - const oldBranch = this.currentBranch; - this.currentBranch = await this.getCurrentBranchInternal(); - this._onRepoChanged.fire({ oldRepo, newRepo, oldBranch, newBranch: this.currentBranch }); - } - } - } else { - Logger.log("No active repository detected. Please open a file within a Git repository.", 'WARNING'); + if (!editor) { + Logger.log("No active editor. Skipping repository update.", "WARNING"); + return; + } + + const filePath = editor.document.uri.fsPath; + const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri); + if (!workspaceFolder) { + Logger.log("No workspace folder detected. Skipping repository update.", "WARNING"); + return; + } + + const relativePath = path.relative(workspaceFolder.uri.fsPath, filePath); + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + Logger.log("File is outside the workspace folder. Skipping repository update.", "WARNING"); + return; + } + + const newRepo = workspaceFolder.uri.fsPath; + if (!fs.existsSync(path.join(newRepo, ".git"))) { + if (this.activeRepository !== null) { + Logger.log(`The directory ${newRepo} is not a Git repository. Clearing status bar.`, "WARNING"); + this._onRepoChanged.fire({ + oldRepo: this.activeRepository, + newRepo: null, + oldBranch: this.currentBranch, + newBranch: null + }); + this.activeRepository = null; + this.git = null; // Clear the git instance } - } else { - Logger.log("No active editor. Please open a file to detect the repository.", 'WARNING'); + return; // Skip if not a Git repository + } + + if (newRepo !== this.activeRepository) { + const oldRepo = this.activeRepository; + this.activeRepository = newRepo; + this.git = simpleGit(this.activeRepository); + this.initialized = false; + await this.initialize(); + Logger.log(`Active repository changed to: ${this.activeRepository}`, "INFO"); + + // Clear caches when repository changes + this.clearCaches(); + + // Emit repository change event + const oldBranch = this.currentBranch; + this.currentBranch = await this.getCurrentBranchInternal(); + this._onRepoChanged.fire({oldRepo, newRepo, oldBranch, newBranch: this.currentBranch}); } } @@ -85,7 +111,7 @@ export class GitService { } if (!this.activeRepository) { - Logger.log("No active repository detected during initialization", 'WARNING'); + Logger.log("No active repository detected during initialization", "WARNING"); return false; } @@ -94,24 +120,29 @@ export class GitService { if (currentRepo !== this.lastRepo) { this.lastRepo = currentRepo; this.clearTagCache(); - Logger.log(`Repository changed to: ${currentRepo}. Tag cache cleared.`, 'INFO'); + Logger.log(`Repository changed to: ${currentRepo}. Tag cache cleared.`, "INFO"); } await this.git?.status(); this.initialized = true; this.currentBranch = await this.getCurrentBranchInternal(); - Logger.log(`Git initialized successfully for ${this.activeRepository}. Current branch: ${this.currentBranch}`, 'INFO'); + Logger.log( + `Git initialized successfully for ${this.activeRepository}. Current branch: ${this.currentBranch}`, + "INFO" + ); await this.watchGitChanges(); return true; } catch (error) { - Logger.log(`Failed to initialize Git: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Failed to initialize Git: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); if (error instanceof Error && error.stack) { - Logger.log(`Error stack: ${error.stack}`, 'ERROR'); + Logger.log(`Error stack: ${error.stack}`, "WARNING"); } return false; } } - + public getActiveRepository(): string | null { + return this.activeRepository; + } public isInitialized(): boolean { return this.initialized; } @@ -122,14 +153,14 @@ export class GitService { } try { - Logger.log(`Creating tag ${tag} locally...`, 'INFO'); + Logger.log(`Creating tag ${tag} locally...`, "INFO"); await this.git.addAnnotatedTag(tag, `Release ${tag}`); - Logger.log(`Tag ${tag} created locally`, 'INFO'); - + Logger.log(`Tag ${tag} created locally`, "INFO"); + // Force update of the local tag list - await this.git.tags(['--list']); + await this.git.tags(["--list"]); } catch (error) { - Logger.log(`Error creating tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error creating tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, "ERROR"); throw error; } } @@ -137,7 +168,7 @@ export class GitService { public async fetchAndTags(forceRefresh: boolean = false): Promise { const currentRepo = await this.getCurrentRepo(); const currentBranch = await this.getCurrentBranch(); - + if (currentRepo !== this.lastTagFetchRepo || currentBranch !== this.lastBranch) { this.lastTagFetchRepo = currentRepo; this.lastBranch = currentBranch; @@ -147,7 +178,7 @@ export class GitService { const now = Date.now(); if (!forceRefresh && this.tagCache.tags && now - this.tagCache.timestamp < this.tagCacheDuration) { - Logger.log('Returning cached latest tag', 'INFO'); + Logger.log("Returning cached latest tag", "INFO"); return this.tagCache.tags; } @@ -156,27 +187,27 @@ export class GitService { throw new Error("Git is not initialized"); } - Logger.log('Fetching latest tag from remote...', 'INFO'); - await this.git.fetch(['--tags', '--prune', '--prune-tags']); + Logger.log("Fetching latest tag from remote...", "INFO"); + await this.git.fetch(["--tags", "--prune", "--prune-tags"]); // Try to get the latest tag let latestTag: string | null = null; try { - latestTag = await this.git.raw(['describe', '--tags', '--abbrev=0']); + latestTag = await this.git.raw(["describe", "--tags", "--abbrev=0"]); latestTag = latestTag.trim(); } catch (error) { // If no tags are found, this command will throw an error - Logger.log('No tags found in the repository', 'INFO'); + Logger.log("No tags found in the repository", "INFO"); } - const result: TagResult = { latest: latestTag }; + const result: TagResult = {latest: latestTag}; - this.tagCache = { tags: result, timestamp: now }; - Logger.log(`Latest tag fetched and cached: ${JSON.stringify(result)}`, 'INFO'); + this.tagCache = {tags: result, timestamp: now}; + Logger.log(`Latest tag fetched and cached: ${JSON.stringify(result)}`, "INFO"); return result; } catch (error) { - Logger.log(`Error fetching tags: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); - return { latest: null }; + Logger.log(`Error fetching tags: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); + return {latest: null}; } } @@ -185,8 +216,12 @@ export class GitService { const now = Date.now(); // Check cache first, unless forceRefresh is true - if (!forceRefresh && this.commitCountCache[cacheKey] && now - this.commitCountCache[cacheKey].timestamp < this.commitCountCacheDuration) { - Logger.log(`Returning cached commit count for ${from} to ${to}`, 'INFO'); + if ( + !forceRefresh && + this.commitCountCache[cacheKey] && + now - this.commitCountCache[cacheKey].timestamp < this.commitCountCacheDuration + ) { + Logger.log(`Returning cached commit count for ${from} to ${to}`, "INFO"); return this.commitCountCache[cacheKey].count; } @@ -196,7 +231,7 @@ export class GitService { } // Fetch the latest changes without tags to improve performance - await this.git.fetch(['--no-tags']); + await this.git.fetch(["--no-tags"]); // Check if both 'from' and 'to' branches/refs exist const [fromExists, toExists] = await Promise.all([ @@ -204,32 +239,32 @@ export class GitService { this.refExists(to) ]); - Logger.log(`Checking ref existence - from: ${from} (${fromExists}), to: ${to} (${toExists})`, 'INFO'); + Logger.log(`Checking ref existence - from: ${from} (${fromExists}), to: ${to} (${toExists})`, "INFO"); // If either branch doesn't exist, return 0 if (!fromExists || !toExists) { - Logger.log(`Branch or ref does not exist: ${!fromExists ? from : to}`, 'INFO'); + Logger.log(`Branch or ref does not exist: ${!fromExists ? from : to}`, "INFO"); return 0; } const range = from ? `${from}..${to}` : to; - const result = await this.git.raw(['rev-list', '--count', range]); + const result = await this.git.raw(["rev-list", "--count", range]); const count = parseInt(result.trim(), 10); // Cache the result - this.commitCountCache[cacheKey] = { count, timestamp: now }; + this.commitCountCache[cacheKey] = {count, timestamp: now}; - Logger.log(`Commit count from ${from} to ${to}: ${count}`, 'INFO'); + Logger.log(`Commit count from ${from} to ${to}: ${count}`, "INFO"); return count; } catch (error) { - Logger.log(`Error getting commit count: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting commit count: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); return 0; } } private async refExists(ref: string): Promise { try { - await this.git?.revparse(['--verify', ref]); + await this.git?.revparse(["--verify", ref]); return true; } catch (error) { return false; @@ -238,10 +273,17 @@ export class GitService { public async getCurrentRepo(): Promise { const currentRepo = this.activeRepository; + if (!currentRepo || !fs.existsSync(path.join(currentRepo, ".git"))) { + Logger.log("No valid Git repository detected. Clearing caches.", "WARNING"); + this.clearCaches(); + this.activeRepository = null; + return null; + } + if (currentRepo !== this.lastRepo) { this.lastRepo = currentRepo; this.clearCaches(); - Logger.log(`Repository changed to: ${currentRepo}. Caches cleared.`, 'INFO'); + Logger.log(`Repository changed to: ${currentRepo}. Caches cleared.`, "INFO"); } return currentRepo; } @@ -251,7 +293,7 @@ export class GitService { return []; } const remotes = await this.git.getRemotes(true); - const remote = remotes.find((r) => r.name === "origin"); + const remote = remotes.find(r => r.name === "origin"); if (remote) { const remoteUrl = remote.refs.fetch.replace(".git", ""); this.activeRepository = remoteUrl.split("/").pop() || ""; @@ -259,62 +301,66 @@ export class GitService { return remotes; } - async pushTag(tag: string): Promise { + async pushTag(tag: string, timeout: number = 30000): Promise { if (!this.git) { throw new Error("Git is not initialized"); } + const timer = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Pushing tag ${tag} timed out after ${timeout / 1000} seconds`)), timeout) + ); + try { - Logger.log(`Verifying tag ${tag} exists locally...`, 'INFO'); + Logger.log(`Verifying tag ${tag} exists locally...`, "INFO"); const localTags = await this.git.tags(); - Logger.log(`Local tags: ${JSON.stringify(localTags)}`, 'INFO'); + Logger.log(`Local tags: ${JSON.stringify(localTags)}`, "INFO"); if (!localTags.all.includes(tag)) { throw new Error(`Tag ${tag} does not exist locally`); } - const tagCommit = await this.git.raw(['rev-list', '-n', '1', tag]); - Logger.log(`Tag ${tag} is associated with commit ${tagCommit.trim()}`, 'INFO'); + const tagCommit = await this.git.raw(["rev-list", "-n", "1", tag]); + Logger.log(`Tag ${tag} is associated with commit ${tagCommit.trim()}`, "INFO"); - Logger.log(`Pushing tag ${tag} to origin...`, 'INFO'); - await this.git.push('origin', tag); - Logger.log(`Tag ${tag} pushed successfully`, 'INFO'); + Logger.log(`Pushing tag ${tag} to origin...`, "INFO"); + await Promise.race([this.git.push("origin", tag), timer]); + Logger.log(`Tag ${tag} pushed successfully`, "INFO"); // Verify the tag was pushed - await this.git.fetch('origin', '--tags'); - const remoteTags = await this.git.tags(['--list', `${tag}`]); - Logger.log(`Remote tags: ${JSON.stringify(remoteTags)}`, 'INFO'); - + await this.git.fetch("origin", "--tags"); + const remoteTags = await this.git.tags(["--list", `${tag}`]); + Logger.log(`Remote tags: ${JSON.stringify(remoteTags)}`, "INFO"); + if (!remoteTags.all.includes(tag)) { - Logger.log(`Tag ${tag} not found in remote tags list. Waiting and retrying...`, 'WARNING'); + Logger.log(`Tag ${tag} not found in remote tags list. Waiting and retrying...`, "WARNING"); // Wait for 5 seconds and try again await new Promise(resolve => setTimeout(resolve, 5000)); - const retryRemoteTags = await this.git.tags(['--list', `${tag}`]); - Logger.log(`Retry remote tags: ${JSON.stringify(retryRemoteTags)}`, 'INFO'); - + const retryRemoteTags = await this.git.tags(["--list", `${tag}`]); + Logger.log(`Retry remote tags: ${JSON.stringify(retryRemoteTags)}`, "INFO"); + if (!retryRemoteTags.all.includes(tag)) { throw new Error(`Failed to verify tag ${tag} on remote after retry`); } } - Logger.log(`Tag ${tag} successfully pushed and verified on remote`, 'INFO'); + Logger.log(`Tag ${tag} successfully pushed and verified on remote`, "INFO"); } catch (error) { - Logger.log(`Error pushing tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error pushing tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, "ERROR"); if (error instanceof Error && error.stack) { - Logger.log(`Error stack: ${error.stack}`, 'ERROR'); + Logger.log(`Error stack: ${error.stack}`, "ERROR"); } throw error; } } async getRemoteUrl(): Promise { - const cacheKey = this.activeRepository || ''; + const cacheKey = this.activeRepository || ""; if (this.remoteUrlCache[cacheKey]) { return this.remoteUrlCache[cacheKey]; } if (!this.git) { - Logger.log('Git is not initialized, ignoring', 'WARNING'); + Logger.log("Git is not initialized, ignoring", "WARNING"); return undefined; } @@ -322,7 +368,7 @@ export class GitService { const remotes = await this.git.getRemotes(true); Logger.log(`All remotes: ${JSON.stringify(remotes)}`); - const originRemote = remotes.find(remote => remote.name === 'origin'); + const originRemote = remotes.find(remote => remote.name === "origin"); if (originRemote) { Logger.log(`Origin remote URL: ${originRemote.refs.fetch}`); const remoteUrl = originRemote.refs.fetch; @@ -332,34 +378,34 @@ export class GitService { return remoteUrl; } - Logger.log('No origin remote found, ignoring', 'WARNING'); + Logger.log("No origin remote found, ignoring", "WARNING"); return undefined; } catch (error) { - Logger.log(`Error getting remote URL: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting remote URL: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); return undefined; } } - async getOwnerAndRepo(): Promise<{ owner: string, repo: string } | undefined> { + async getOwnerAndRepo(): Promise<{owner: string; repo: string} | undefined> { const remoteUrl = await this.getRemoteUrl(); - if (!remoteUrl) { + if (!remoteUrl) { return undefined; } - + if (this.ownerRepoCache[remoteUrl]) { return this.ownerRepoCache[remoteUrl]; } try { if (!this.git) { - Logger.log('Git is not initialized, ignoring', 'WARNING'); + Logger.log("Git is not initialized, ignoring", "WARNING"); return undefined; } Logger.log(`Remote URL: ${remoteUrl}`); if (!remoteUrl) { - Logger.log('No remote URL found, ignoring', 'WARNING'); + Logger.log("No remote URL found, ignoring", "WARNING"); return undefined; } @@ -369,50 +415,50 @@ export class GitService { if (match) { const [, domain, fullPath, repo] = match; - + // Split the full path into parts - const pathParts = fullPath.split('/'); - + const pathParts = fullPath.split("/"); + // The repo name is the last part, and everything else is the owner/group path - const owner = pathParts.join('/'); + const owner = pathParts.join("/"); Logger.log(`Extracted owner and repo: ${owner}/${repo} (domain: ${domain})`); if (owner && repo) { - this.ownerRepoCache[remoteUrl] = { owner, repo }; + this.ownerRepoCache[remoteUrl] = {owner, repo}; } - return { owner, repo }; + return {owner, repo}; } - Logger.log(`Unable to extract owner and repo from remote URL: ${remoteUrl}`, 'ERROR'); + Logger.log(`Unable to extract owner and repo from remote URL: ${remoteUrl}`, "WARNING"); return undefined; } catch (error) { - Logger.log(`Error getting owner and repo: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting owner and repo: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); return undefined; } } - public async hasCIConfiguration(): Promise<'github' | 'gitlab' | null> { + public async hasCIConfiguration(): Promise<"github" | "gitlab" | null> { if (!this.activeRepository) { return null; } - const githubWorkflowsPath = path.join(this.activeRepository, '.github', 'workflows'); - const gitlabCIPath = path.join(this.activeRepository, '.gitlab-ci.yml'); + const githubWorkflowsPath = path.join(this.activeRepository, ".github", "workflows"); + const gitlabCIPath = path.join(this.activeRepository, ".gitlab-ci.yml"); try { if (fs.existsSync(githubWorkflowsPath) && fs.statSync(githubWorkflowsPath).isDirectory()) { - return 'github'; + return "github"; } else if (fs.existsSync(gitlabCIPath) && fs.statSync(gitlabCIPath).isFile()) { - return 'gitlab'; + return "gitlab"; } } catch (error) { - console.error('Error checking CI configuration:', error); + console.error("Error checking CI configuration:", error); } return null; } - public detectCIType(): 'github' | 'gitlab' | null { + public detectCIType(): "github" | "gitlab" | null { if (this.cachedCIType !== null) { return this.cachedCIType; } @@ -421,13 +467,13 @@ export class GitService { return null; } - const githubWorkflowsPath = path.join(this.activeRepository, '.github', 'workflows'); - const gitlabCIPath = path.join(this.activeRepository, '.gitlab-ci.yml'); + const githubWorkflowsPath = path.join(this.activeRepository, ".github", "workflows"); + const gitlabCIPath = path.join(this.activeRepository, ".gitlab-ci.yml"); if (fs.existsSync(githubWorkflowsPath) && fs.statSync(githubWorkflowsPath).isDirectory()) { - this.cachedCIType = 'github'; + this.cachedCIType = "github"; } else if (fs.existsSync(gitlabCIPath) && fs.statSync(gitlabCIPath).isFile()) { - this.cachedCIType = 'gitlab'; + this.cachedCIType = "gitlab"; } else { this.cachedCIType = null; } @@ -436,30 +482,32 @@ export class GitService { } async pushChanges(branch: string): Promise { - await this.git?.push('origin', branch); + await this.git?.push("origin", branch); } public async getDefaultBranch(): Promise { if (!this.git) { - Logger.log("Git is not initialized", 'INFO'); + Logger.log("Git is not initialized", "INFO"); return null; } const currentRepo = await this.getCurrentRepo(); if (!currentRepo) { - Logger.log("Current repository not detected", 'INFO'); + Logger.log("Current repository not detected", "WARNING"); return null; } // Check if the default branch is cached for the current repo const cachedDefaultBranch = this.defaultBranchCache.get(currentRepo); + console.log("Cached Default Branch:", cachedDefaultBranch); // Debug log if (cachedDefaultBranch) { return cachedDefaultBranch; } try { // First, try to get the default branch from the origin remote - const result = await this.git.raw(['remote', 'show', 'origin']); + const result = await this.git.raw(["remote", "show", "origin"]); + console.log("Remote Show Result:", result); // Debug log const match = result.match(/HEAD branch: (.+)/); if (match) { const defaultBranch = match[1].trim(); @@ -468,10 +516,10 @@ export class GitService { } // If that fails, fall back to checking common default branch names - const commonDefaultBranches = ['main', 'master', 'develop']; + const commonDefaultBranches = ["main", "master", "develop"]; for (const branch of commonDefaultBranches) { try { - await this.git.raw(['rev-parse', '--verify', `origin/${branch}`]); + await this.git.raw(["rev-parse", "--verify", `origin/${branch}`]); this.defaultBranchCache.set(currentRepo, branch); return branch; } catch (error) { @@ -481,16 +529,17 @@ export class GitService { // If all else fails, return the current branch as a default const currentBranch = await this.getCurrentBranchInternal(); + console.log("Current Branch:", currentBranch); // Debug log if (currentBranch) { - Logger.log(`Unable to determine default branch, falling back to current branch "${currentBranch}"`, 'WARNING'); + Logger.log(`Unable to determine default branch, falling back to current branch "${currentBranch}"`, "WARNING"); this.defaultBranchCache.set(currentRepo, currentBranch); return currentBranch; } else { - Logger.log("Unable to determine current branch", 'ERROR'); + Logger.log("Unable to determine current branch", "WARNING"); return null; } } catch (error) { - Logger.log(`Error getting default branch: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting default branch: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); throw error; } } @@ -515,10 +564,10 @@ export class GitService { throw new Error("Git is not initialized"); } try { - const result = await this.git.raw(['rev-list', '--max-parents=0', 'HEAD']); + const result = await this.git.raw(["rev-list", "--max-parents=0", "HEAD"]); return result.trim(); } catch (error) { - Logger.log(`Error getting initial commit: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting initial commit: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); throw error; } } @@ -532,7 +581,7 @@ export class GitService { if (newBranch !== this.currentBranch) { const oldBranch = this.currentBranch; this.currentBranch = newBranch; - this._onBranchChanged.fire({ oldBranch, newBranch }); + this._onBranchChanged.fire({oldBranch, newBranch}); } }, 6000); // Check every 6 seconds } @@ -541,7 +590,7 @@ export class GitService { const currentBranch = await this.getCurrentBranchInternal(); if (currentBranch !== this.lastBranch) { this.lastBranch = currentBranch; - Logger.log(`Branch changed to: ${currentBranch}`, 'INFO'); + Logger.log(`Branch changed to: ${currentBranch}`, "INFO"); } return currentBranch; } @@ -551,9 +600,9 @@ export class GitService { return null; } try { - return await this.git.revparse(['--abbrev-ref', 'HEAD']); + return await this.git.revparse(["--abbrev-ref", "HEAD"]); } catch (error) { - Logger.log(`Error getting current branch: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error getting current branch: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); return null; } } @@ -568,7 +617,7 @@ export class GitService { } private clearTagCache() { - this.tagCache = { tags: null, timestamp: 0 }; + this.tagCache = {tags: null, timestamp: 0}; } private clearCaches() { @@ -576,17 +625,19 @@ export class GitService { this.cachedCIType = null; this.remoteUrlCache = {}; this.ownerRepoCache = {}; - this.tagCache = { tags: null, timestamp: 0 }; + this.tagCache = {tags: null, timestamp: 0}; this.commitCountCache = {}; this.lastTagFetchRepo = null; this.lastBranch = null; } private async watchGitChanges() { - if (!this.activeRepository) {return;} + if (!this.activeRepository) { + return; + } const watcher = vscode.workspace.createFileSystemWatcher( - new vscode.RelativePattern(this.activeRepository, '.git/refs/remotes/origin/**') + new vscode.RelativePattern(this.activeRepository, ".git/refs/remotes/origin/**") ); watcher.onDidChange(() => this.debouncedHandleGitChange()); @@ -596,14 +647,18 @@ export class GitService { private async handleGitChange() { const currentBranch = await this.getCurrentBranch(); - if (!currentBranch) {return;} + if (!currentBranch) { + return; + } const localCommit = await this.git?.revparse([currentBranch]); const remoteCommit = await this.git?.revparse([`origin/${currentBranch}`]); if (localCommit === remoteCommit) { - Logger.log('Git push detected', 'INFO'); - this._onGitPush.fire(); + Logger.log("Git push detected", "INFO"); + setTimeout(() => { + this._onGitPush.fire(); + }, 5000); // 5 second delay } } -} \ No newline at end of file +} diff --git a/src/services/statusBarService.ts b/src/services/statusBarService.ts index 594c434..ccfe542 100644 --- a/src/services/statusBarService.ts +++ b/src/services/statusBarService.ts @@ -1,9 +1,9 @@ import * as vscode from "vscode"; -import { GitService, TagResult } from './gitService'; -import { CIService } from './ciService'; -import { Logger } from '../utils/logger'; -import semver from 'semver'; -import { debounce } from '../utils/debounce'; +import {GitService, TagResult} from "./gitService"; +import {CIService} from "./ciService"; +import {Logger} from "../utils/logger"; +import semver from "semver"; +import {debounce} from "../utils/debounce"; export class StatusBarService { private branchBuildStatusItem: vscode.StatusBarItem; @@ -11,21 +11,24 @@ export class StatusBarService { private buttons: vscode.StatusBarItem[]; private lastErrorTime: number = 0; private errorCooldownPeriod: number = 5 * 60 * 1000; // 5 minutes - private _onCIStatusUpdate = new vscode.EventEmitter<{ status: string, url: string, isTag: boolean }>(); + private _onCIStatusUpdate = new vscode.EventEmitter<{status: string; url: string; isTag: boolean}>(); readonly onCIStatusUpdate = this._onCIStatusUpdate.event; private branchBuildStatusUrl: string | undefined; private tagBuildStatusUrl: string | undefined; private compareUrl: string | undefined; - private debouncedUpdateEverything: (forceRefresh: boolean) => void; - private inProgressRefreshIntervals: { [key: string]: NodeJS.Timeout } = {}; - private lastKnownStatuses: { [key: string]: string } = {}; + private debouncedUpdateEverything = debounce(async (forceRefresh: boolean = false) => { + await this.updateEverything(forceRefresh); + }, 2000); + private inProgressRefreshIntervals: {[key: string]: NodeJS.Timeout} = {}; + private lastKnownStatuses: {[key: string]: string} = {}; private debouncedRefreshAfterPush: () => void; + private debouncedHandleActiveEditorChange = debounce(() => this.handleActiveEditorChange(), 300); private cachedData: { currentBranch: string | null; defaultBranch: string | null; latestTag: TagResult | null; unreleasedCount: number | null; - ownerAndRepo: { owner: string; repo: string } | null; + ownerAndRepo: {owner: string; repo: string} | null; } = { currentBranch: null, defaultBranch: null, @@ -46,12 +49,13 @@ export class StatusBarService { this.gitService.onRepoChanged(this.handleRepoChange.bind(this)); this.gitService.onBranchChanged(this.handleBranchChange.bind(this)); vscode.window.onDidChangeActiveTextEditor(() => this.handleActiveEditorChange()); - this.debouncedUpdateEverything = debounce(this.updateEverything.bind(this), 500); + this.debouncedUpdateEverything = debounce(this.updateEverything.bind(this), 5000); this.debouncedRefreshAfterPush = debounce(this.refreshAfterPush.bind(this), 1000); this.gitService.onGitPush(() => this.debouncedRefreshAfterPush()); + vscode.window.onDidChangeActiveTextEditor(() => this.debouncedHandleActiveEditorChange()); - Logger.log("StatusBarService constructor called", 'INFO'); - Logger.log(`Number of buttons created: ${this.buttons.length}`, 'INFO'); + Logger.log("StatusBarService constructor called", "INFO"); + Logger.log(`Number of buttons created: ${this.buttons.length}`, "INFO"); this.updateEverything(true); } @@ -78,35 +82,33 @@ export class StatusBarService { public async updateCIStatus(status: string, ref: string, url: string, isTag: boolean) { const statusItem = isTag ? this.tagBuildStatusItem : this.branchBuildStatusItem; - const statusType = isTag ? 'Tag' : 'Branch'; - const config = vscode.workspace.getConfiguration('gitTagReleaseTracker'); - const ciProviders = config.get<{ [key: string]: { token: string, apiUrl: string } }>('ciProviders', {}); + const statusType = isTag ? "Tag" : "Branch"; + const config = vscode.workspace.getConfiguration("gitTagReleaseTracker"); + const ciProviders = config.get<{[key: string]: {token: string; apiUrl: string}}>("ciProviders", {}); // Get the owner and repo using GitService const ownerAndRepo = await this.gitService.getOwnerAndRepo(); if (!ownerAndRepo) { - Logger.log('Unable to determine owner and repo', 'WARNING'); + Logger.log("Unable to determine owner and repo", "WARNING"); return; } - const { owner, repo: repoName } = ownerAndRepo; + const {owner, repo: repoName} = ownerAndRepo; - let newText = ''; - let newTooltip = ''; + let newText = ""; + let newTooltip = ""; - if (status === 'no_runs') { + if (status === "no_runs") { newText = `$(circle-slash) No ${statusType.toLowerCase()} builds found`; newTooltip = `No builds found for ${statusType.toLowerCase()} ${owner}/${repoName}/${ref}`; } else { const icon = this.getStatusIcon(status); - newText = isTag - ? `${icon} ${statusType} build ${ref} ${status}` - : `${icon} ${statusType} build ${status}`; + newText = isTag ? `${icon} ${statusType} build ${ref} ${status}` : `${icon} ${statusType} build ${status}`; newTooltip = `Click to open ${statusType.toLowerCase()} build status for ${owner}/${repoName}/${ref}`; } statusItem.text = newText; statusItem.tooltip = newTooltip; - statusItem.command = isTag ? 'extension.openTagBuildStatus' : 'extension.openBranchBuildStatus'; + statusItem.command = isTag ? "extension.openTagBuildStatus" : "extension.openBranchBuildStatus"; statusItem.show(); if (isTag) { @@ -115,27 +117,32 @@ export class StatusBarService { this.branchBuildStatusUrl = url; } - this._onCIStatusUpdate.fire({ status, url, isTag }); + this._onCIStatusUpdate.fire({status, url, isTag}); - if (status === 'error') { + if (status === "error") { const ciType = this.gitService.detectCIType(); if (ciType) { - const defaultUrl = ciType === 'github' - ? `https://github.com/${owner}/${repoName}/actions` - : `${ciProviders[ciType]?.apiUrl || `https://gitlab.com`}/${owner}/${repoName}/-/pipelines`; + const defaultUrl = + ciType === "github" + ? `https://github.com/${owner}/${repoName}/actions` + : `${ciProviders[ciType]?.apiUrl || `https://gitlab.com`}/${owner}/${repoName}/-/pipelines`; url = defaultUrl; const hasToken = ciProviders[ciType]?.token; const currentTime = Date.now(); if (!hasToken && currentTime - this.lastErrorTime > this.errorCooldownPeriod) { - vscode.window.showErrorMessage(`CI token for ${ciType} is not configured. Please set up your CI token in the extension settings.`); + vscode.window.showErrorMessage( + `CI token for ${ciType} is not configured. Please set up your CI token in the extension settings.` + ); this.lastErrorTime = currentTime; } else if (hasToken && currentTime - this.lastErrorTime > this.errorCooldownPeriod) { - vscode.window.showErrorMessage(`Error fetching ${statusType.toLowerCase()} build status. Please check your CI configuration and token.`); + vscode.window.showErrorMessage( + `Error fetching ${statusType.toLowerCase()} build status. Please check your CI configuration and token.` + ); this.lastErrorTime = currentTime; } } - Logger.log(`Error fetching ${statusType.toLowerCase()} build status for ${ref}`, 'ERROR'); + Logger.log(`Error fetching ${statusType.toLowerCase()} build status for ${ref}`, "ERROR"); } } @@ -156,36 +163,53 @@ export class StatusBarService { } private async updateVersionButtons() { - Logger.log("Entering updateVersionButtons method", 'DEBUG'); - - if (!this.cachedData.currentBranch || !this.cachedData.defaultBranch || this.cachedData.unreleasedCount === null || !this.cachedData.ownerAndRepo) { - Logger.log(`Missing data: currentBranch=${!!this.cachedData.currentBranch}, defaultBranch=${!!this.cachedData.defaultBranch}, unreleasedCount=${this.cachedData.unreleasedCount}, ownerAndRepo=${!!this.cachedData.ownerAndRepo}`, 'DEBUG'); + if ( + !this.cachedData.currentBranch || + !this.cachedData.defaultBranch || + this.cachedData.unreleasedCount === null || + !this.cachedData.ownerAndRepo + ) { + Logger.log( + `Missing data: currentBranch=${!!this.cachedData.currentBranch}, defaultBranch=${!!this.cachedData + .defaultBranch}, unreleasedCount=${this.cachedData.unreleasedCount}, ownerAndRepo=${!!this.cachedData + .ownerAndRepo}`, + "INFO" + ); return; } const isDefaultBranch = this.cachedData.currentBranch === this.cachedData.defaultBranch; - const { owner, repo } = this.cachedData.ownerAndRepo; + const {owner, repo} = this.cachedData.ownerAndRepo; - Logger.log(`Current branch: ${this.cachedData.currentBranch}, Default branch: ${this.cachedData.defaultBranch}`, 'DEBUG'); - Logger.log(`Is default branch: ${isDefaultBranch}, Unreleased count: ${this.cachedData.unreleasedCount}`, 'DEBUG'); - Logger.log(`Latest tag: ${this.cachedData.latestTag?.latest}`, 'DEBUG'); + Logger.log( + `Current branch: ${this.cachedData.currentBranch}, Default branch: ${this.cachedData.defaultBranch}`, + "INFO" + ); + Logger.log(`Is default branch: ${isDefaultBranch}, Unreleased count: ${this.cachedData.unreleasedCount}`, "INFO"); + Logger.log(`Latest tag: ${this.cachedData.latestTag?.latest}`, "INFO"); // Hide all version buttons initially this.hideAllVersionButtons(); if (isDefaultBranch && this.cachedData.unreleasedCount > 0) { if (!this.cachedData.latestTag?.latest) { - Logger.log("Showing initial version button", 'DEBUG'); + Logger.log("Showing initial version button", "INFO"); this.showInitialVersionButton(owner, repo); } else { - Logger.log("Showing increment buttons", 'DEBUG'); + Logger.log("Showing increment buttons", "INFO"); this.showIncrementButtons(this.cachedData.latestTag.latest, owner, repo); } } else { - Logger.log(`Not showing version buttons. isDefaultBranch=${isDefaultBranch}, unreleasedCount=${this.cachedData.unreleasedCount}`, 'DEBUG'); + Logger.log( + `Not showing version buttons. isDefaultBranch=${isDefaultBranch}, unreleasedCount=${this.cachedData.unreleasedCount}`, + "INFO" + ); } - Logger.log(`Version buttons updated. Is default branch: ${isDefaultBranch}, Latest tag: ${this.cachedData.latestTag?.latest}, Unreleased count: ${this.cachedData.unreleasedCount}`, 'INFO'); + Logger.log( + `Version buttons updated. Is default branch: ${isDefaultBranch}, Latest tag: ${this.cachedData.latestTag?.latest}, Unreleased count: ${this.cachedData.unreleasedCount}`, + "INFO" + ); } public hideAllVersionButtons() { @@ -195,18 +219,18 @@ export class StatusBarService { } private showInitialVersionButton(owner: string, repo: string) { - Logger.log(`Attempting to show initial version button for ${owner}/${repo}`, 'DEBUG'); + Logger.log(`Attempting to show initial version button for ${owner}/${repo}`, "INFO"); this.updateButton(3, "1.0.0", `Create initial version tag 1.0.0 for ${owner}/${repo}`); - this.buttons[3].command = 'extension.createInitialTag'; + this.buttons[3].command = "extension.createInitialTag"; this.buttons[3].show(); - Logger.log("Initial version button should now be visible", 'DEBUG'); + Logger.log("Initial version button should now be visible", "INFO"); } private showIncrementButtons(latestTag: string, owner: string, repo: string) { const match = latestTag.match(/^([^\d]*)(\d+\.\d+\.\d+)(.*)$/); if (match) { const [, prefix, version, suffix] = match; - ['major', 'minor', 'patch'].forEach((type, index) => { + ["major", "minor", "patch"].forEach((type, index) => { const newVersion = semver.inc(version, type as semver.ReleaseType); if (newVersion) { this.updateButton( @@ -219,17 +243,17 @@ export class StatusBarService { } }); } else { - Logger.log(`Invalid tag format: ${latestTag}`, 'WARNING'); + Logger.log(`Invalid tag format: ${latestTag}`, "WARNING"); } } public async updateCommitCountButton(forceRefresh: boolean = false) { if (!this.cachedData.currentBranch || !this.cachedData.defaultBranch || !this.cachedData.ownerAndRepo) { - Logger.log('Unable to update commit count button: missing branch or repo information', 'WARNING'); + Logger.log("Unable to update commit count button: missing branch or repo information", "WARNING"); return; } - const { owner, repo } = this.cachedData.ownerAndRepo; + const {owner, repo} = this.cachedData.ownerAndRepo; const isDefaultBranch = this.cachedData.currentBranch === this.cachedData.defaultBranch; let unreleasedCount = 0; @@ -241,19 +265,31 @@ export class StatusBarService { } if (isDefaultBranch && this.cachedData.latestTag?.latest) { - unreleasedCount = await this.gitService.getCommitCounts(this.cachedData.latestTag.latest, this.cachedData.defaultBranch, forceRefresh); + unreleasedCount = await this.gitService.getCommitCounts( + this.cachedData.latestTag.latest, + this.cachedData.defaultBranch, + forceRefresh + ); } else if (!isDefaultBranch) { - unmergedCount = await this.gitService.getCommitCounts(this.cachedData.defaultBranch, this.cachedData.currentBranch, forceRefresh); + unmergedCount = await this.gitService.getCommitCounts( + this.cachedData.defaultBranch, + this.cachedData.currentBranch, + forceRefresh + ); if (this.cachedData.latestTag?.latest) { - unreleasedCount = await this.gitService.getCommitCounts(this.cachedData.latestTag.latest, this.cachedData.defaultBranch, forceRefresh); + unreleasedCount = await this.gitService.getCommitCounts( + this.cachedData.latestTag.latest, + this.cachedData.defaultBranch, + forceRefresh + ); } } // Update the cached unreleased count this.cachedData.unreleasedCount = unreleasedCount; - let buttonText = ''; - let tooltipText = ''; + let buttonText = ""; + let tooltipText = ""; if (isDefaultBranch) { if (!this.cachedData.latestTag?.latest) { @@ -282,9 +318,9 @@ export class StatusBarService { } const buttonIndex = 4; - Logger.log(`Commit count button updated: ${buttonText}`, 'INFO'); + Logger.log(`Commit count button updated: ${buttonText}`, "INFO"); this.updateButton(buttonIndex, buttonText, tooltipText); - this.buttons[buttonIndex].command = 'extension.openCompareLink'; + this.buttons[buttonIndex].command = "extension.openCompareLink"; this.buttons[buttonIndex].show(); } @@ -299,42 +335,42 @@ export class StatusBarService { private getStatusIcon(status: string): string { switch (status) { - case 'success': - case 'completed': - return '$(check)'; - case 'failure': - case 'failed': - return '$(x)'; - case 'cancelled': - case 'canceled': - return '$(circle-slash)'; - case 'action_required': - case 'manual': - return '$(alert)'; - case 'in_progress': - case 'running': - return '$(sync~spin)'; - case 'queued': - case 'created': - case 'scheduled': - return '$(clock)'; - case 'requested': - case 'waiting': - case 'waiting_for_resource': - return '$(watch)'; - case 'pending': - case 'preparing': - return '$(clock)'; - case 'neutral': - return '$(dash)'; - case 'skipped': - return '$(skip)'; - case 'stale': - return '$(history)'; - case 'timed_out': - return '$(clock)'; + case "success": + case "completed": + return "$(check)"; + case "failure": + case "failed": + return "$(x)"; + case "cancelled": + case "canceled": + return "$(circle-slash)"; + case "action_required": + case "manual": + return "$(alert)"; + case "in_progress": + case "running": + return "$(sync~spin)"; + case "queued": + case "created": + case "scheduled": + return "$(clock)"; + case "requested": + case "waiting": + case "waiting_for_resource": + return "$(watch)"; + case "pending": + case "preparing": + return "$(clock)"; + case "neutral": + return "$(dash)"; + case "skipped": + return "$(skip)"; + case "stale": + return "$(history)"; + case "timed_out": + return "$(clock)"; default: - return '$(question)'; + return "$(question)"; } } @@ -344,28 +380,39 @@ export class StatusBarService { } public async updateEverything(forceRefresh: boolean = false): Promise { + const previousData = {...this.cachedData}; if (forceRefresh) { this.clearCache(); } await this.updateCachedData(); + // Check if there are actual changes in the data + if (!forceRefresh && JSON.stringify(previousData) === JSON.stringify(this.cachedData)) { + Logger.log("No changes detected, skipping update", "INFO"); + return; + } + if (this.cachedData.currentBranch && this.cachedData.ownerAndRepo && this.cachedData.defaultBranch) { - const { owner, repo } = this.cachedData.ownerAndRepo; + const {owner, repo} = this.cachedData.ownerAndRepo; const ciType = await this.gitService.detectCIType(); await Promise.all([ this.updateVersionButtons(), this.updateCommitCountButton(), this.updateCompareUrl(), - ...(ciType ? [ - this.updateBranchBuildStatus(owner, repo, ciType, forceRefresh), - this.updateTagBuildStatus(owner, repo, ciType, forceRefresh) - ] : []) + ...(ciType + ? [ + this.updateBuildStatus(this.cachedData.currentBranch, owner, repo, ciType, false, forceRefresh), + this.cachedData.latestTag?.latest + ? this.updateBuildStatus(this.cachedData.latestTag.latest, owner, repo, ciType, true, forceRefresh) + : Promise.resolve() + ] + : []) ]); if (!ciType) { - Logger.log("CI type not detected, skipping build status updates", 'WARNING'); + Logger.log("CI type not detected, skipping build status updates", "WARNING"); } } } @@ -383,15 +430,21 @@ export class StatusBarService { this.cachedData.ownerAndRepo = ownerAndRepo || null; this.cachedData.latestTag = latestTag; - Logger.log(`Cached data updated: currentBranch=${currentBranch}, defaultBranch=${defaultBranch}, latestTag=${latestTag?.latest}`, 'DEBUG'); + Logger.log( + `Cached data updated: currentBranch=${currentBranch}, defaultBranch=${defaultBranch}, latestTag=${latestTag?.latest}`, + "INFO" + ); if (currentBranch && defaultBranch) { - const fromRef = latestTag?.latest || await this.gitService.getInitialCommit(); + const fromRef = latestTag?.latest || (await this.gitService.getInitialCommit()); this.cachedData.unreleasedCount = await this.gitService.getCommitCounts(fromRef, currentBranch); - Logger.log(`Unreleased count calculated: ${this.cachedData.unreleasedCount} (from ${fromRef} to ${currentBranch})`, 'DEBUG'); + Logger.log( + `Unreleased count calculated: ${this.cachedData.unreleasedCount} (from ${fromRef} to ${currentBranch})`, + "INFO" + ); } else { this.cachedData.unreleasedCount = null; - Logger.log('Unable to calculate unreleased count: missing branch information', 'DEBUG'); + Logger.log("Unable to calculate unreleased count: missing branch information", "INFO"); } } @@ -414,49 +467,56 @@ export class StatusBarService { this.tagBuildStatusItem.hide(); } - private clearAllItems() { + public clearAllItems() { this.branchBuildStatusItem.hide(); this.tagBuildStatusItem.hide(); this.buttons.forEach(button => button.hide()); + this.cachedData = { + currentBranch: null, + defaultBranch: null, + latestTag: null, + unreleasedCount: null, + ownerAndRepo: null + }; } - private async updateBranchBuildStatus(owner: string, repo: string, ciType: 'github' | 'gitlab', forceRefresh: boolean = false) { + private async updateBuildStatus( + ref: string, + owner: string, + repo: string, + ciType: "github" | "gitlab", + isTag: boolean, + forceRefresh: boolean = false + ) { try { - const buildStatus = await this.ciService.getBuildStatus(this.cachedData.currentBranch!, owner, repo, ciType, false, forceRefresh); + const buildStatus = await this.ciService.getBuildStatus(ref, owner, repo, ciType, isTag, forceRefresh); if (buildStatus) { - await this.updateCIStatus(buildStatus.status, this.cachedData.currentBranch!, buildStatus.url, false); - this.lastKnownStatuses[this.cachedData.currentBranch!] = buildStatus.status; + await this.updateCIStatus(buildStatus.status, ref, buildStatus.url, isTag); + this.lastKnownStatuses[ref] = buildStatus.status; if (this.ciService.isInProgressStatus(buildStatus.status)) { - this.startRefreshingBranch(this.cachedData.currentBranch!); + this.startRefreshing(ref, isTag); } else { - this.stopRefreshingBranch(this.cachedData.currentBranch!); + this.stopRefreshing(ref, isTag); } } else { - this.clearBranchBuildStatus(); + if (isTag) { + this.clearTagBuildStatus(); + } else { + this.clearBranchBuildStatus(); + } } } catch (error) { - Logger.log(`Error updating branch build status: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); - this.clearBranchBuildStatus(); - } - } - - private async updateTagBuildStatus(owner: string, repo: string, ciType: 'github' | 'gitlab', forceRefresh: boolean = false) { - if (!this.cachedData.latestTag?.latest) { - Logger.log('No tags found, clearing tag build status', 'INFO'); - this.clearTagBuildStatus(); - return; - } - - try { - const buildStatus = await this.ciService.getBuildStatus(this.cachedData.latestTag.latest, owner, repo, ciType, true, forceRefresh); - if (buildStatus) { - await this.updateCIStatus(buildStatus.status, this.cachedData.latestTag.latest, buildStatus.url, true); - } else { + Logger.log( + `Error updating ${isTag ? "tag" : "branch"} build status: ${ + error instanceof Error ? error.message : String(error) + }`, + "ERROR" + ); + if (isTag) { this.clearTagBuildStatus(); + } else { + this.clearBranchBuildStatus(); } - } catch (error) { - Logger.log(`Error updating tag build status: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); - this.clearTagBuildStatus(); } } @@ -465,7 +525,10 @@ export class StatusBarService { if (initialized) { this.updateEverything(false); } else { - this.clearAllItems(); + // Only clear items if the repository is not valid + if (!this.gitService.getActiveRepository()) { + this.clearAllItems(); + } } } @@ -484,20 +547,20 @@ export class StatusBarService { private async updateCompareUrl(): Promise { try { if (!this.cachedData.currentBranch || !this.cachedData.defaultBranch || !this.cachedData.ownerAndRepo) { - Logger.log('Missing required information for compare URL', 'WARNING'); + Logger.log("Missing required information for compare URL", "WARNING"); this.compareUrl = undefined; return; } - const { owner, repo } = this.cachedData.ownerAndRepo; + const {owner, repo} = this.cachedData.ownerAndRepo; const repoInfo = await this.getBaseUrl(); if (!repoInfo) { - Logger.log('Unable to determine base URL for repository', 'WARNING'); + Logger.log("Unable to determine base URL for repository", "WARNING"); this.compareUrl = undefined; return; } - const { baseUrl, projectPath } = repoInfo; + const {baseUrl, projectPath} = repoInfo; if (this.cachedData.currentBranch !== this.cachedData.defaultBranch) { this.compareUrl = `${baseUrl}/${projectPath}/compare/${this.cachedData.defaultBranch}...${this.cachedData.currentBranch}`; @@ -509,22 +572,22 @@ export class StatusBarService { this.compareUrl = `${baseUrl}/${projectPath}/compare/${initialCommit}...${this.cachedData.currentBranch}`; } } - Logger.log(`Compare URL updated: ${this.compareUrl}`, 'INFO'); + Logger.log(`Compare URL updated: ${this.compareUrl}`, "INFO"); } catch (error) { - Logger.log(`Error generating compare URL: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error generating compare URL: ${error instanceof Error ? error.message : String(error)}`, "ERROR"); this.compareUrl = undefined; } } - private async getBaseUrl(): Promise<{ baseUrl: string, projectPath: string } | null> { + private async getBaseUrl(): Promise<{baseUrl: string; projectPath: string} | null> { const remoteUrl = await this.gitService.getRemoteUrl(); if (!remoteUrl) { - Logger.log('No remote URL found', 'WARNING'); + Logger.log("No remote URL found", "WARNING"); return null; } - + let match; - if (remoteUrl.startsWith('git@')) { + if (remoteUrl.startsWith("git@")) { // SSH URL match = remoteUrl.match(/git@([^:]+):(.+)\.git$/); if (match) { @@ -534,7 +597,7 @@ export class StatusBarService { projectPath: path }; } - } else if (remoteUrl.startsWith('https://')) { + } else if (remoteUrl.startsWith("https://")) { // HTTPS URL match = remoteUrl.match(/https:\/\/([^/]+)\/(.+)\.git$/); if (match) { @@ -545,51 +608,64 @@ export class StatusBarService { }; } } - - Logger.log(`Unable to parse remote URL: ${remoteUrl}`, 'WARNING'); + + Logger.log(`Unable to parse remote URL: ${remoteUrl}`, "WARNING"); return null; } - - private async handleRepoChange({ oldRepo, newRepo, oldBranch, newBranch }: { oldRepo: string | null, newRepo: string, oldBranch: string | null, newBranch: string | null }) { - if (oldRepo === newRepo) { - Logger.log(`Repository unchanged: ${newRepo}`, 'INFO'); + + private async handleRepoChange({ + oldRepo, + newRepo, + oldBranch, + newBranch + }: { + oldRepo: string | null; + newRepo: string | null; + oldBranch: string | null; + newBranch: string | null; + }) { + if (!newRepo) { + Logger.log("No valid repository detected, clearing status bar.", "INFO"); + this.clearAllItems(); return; } - Logger.log(`Repository changed from ${oldRepo} to ${newRepo}`, 'INFO'); - - // Clear existing status - this.clearAllItems(); + if (oldRepo !== newRepo) { + Logger.log(`Repository changed from ${oldRepo} to ${newRepo}`, "INFO"); - // Clear CI service cache - this.ciService.clearCache(); + // Clear existing status + this.clearAllItems(); - // Force refresh tags - await this.gitService.fetchAndTags(true); + // Clear CI service cache + this.ciService.clearCache(); - // Force refresh for the new repository - await this.updateEverything(true); + // Force refresh tags + await this.gitService.fetchAndTags(true); + + // Force refresh for the new repository + await this.updateEverything(true); + } // Handle branch change if it occurred during repo change if (oldBranch !== newBranch) { - await this.handleBranchChange({ oldBranch, newBranch }); + await this.handleBranchChange({oldBranch, newBranch}); } } - private async handleBranchChange({ oldBranch, newBranch }: { oldBranch: string | null, newBranch: string | null }) { + private async handleBranchChange({oldBranch, newBranch}: {oldBranch: string | null; newBranch: string | null}) { if (newBranch === null) { - Logger.log('Unable to determine current branch', 'WARNING'); + Logger.log("Unable to determine current branch", "WARNING"); this.clearAllItems(); return; } - Logger.log(`Branch changed from ${oldBranch} to ${newBranch}`, 'INFO'); - + Logger.log(`Branch changed from ${oldBranch} to ${newBranch}`, "INFO"); + // Stop refreshing the previous branch if it's not in progress if (oldBranch && oldBranch !== newBranch) { const previousStatus = this.lastKnownStatuses[oldBranch]; if (!this.ciService.isInProgressStatus(previousStatus)) { - this.stopRefreshingBranch(oldBranch); + this.stopRefreshing(oldBranch, false); } } @@ -602,51 +678,53 @@ export class StatusBarService { // Start refreshing if the new branch is in progress const currentStatus = this.lastKnownStatuses[newBranch]; if (this.ciService.isInProgressStatus(currentStatus)) { - this.startRefreshingBranch(newBranch); + this.startRefreshing(newBranch, false); } } - private startRefreshingBranch(branch: string) { - if (this.inProgressRefreshIntervals[branch]) { - clearInterval(this.inProgressRefreshIntervals[branch]); + private startRefreshing(ref: string, isTag: boolean) { + const key = `${isTag ? "tag" : "branch"}:${ref}`; + if (this.inProgressRefreshIntervals[key]) { + clearInterval(this.inProgressRefreshIntervals[key]); } - this.inProgressRefreshIntervals[branch] = setInterval(() => { - this.refreshBranchStatus(branch); - }, 6000); // Refresh every 6 seconds + this.inProgressRefreshIntervals[key] = setInterval(() => { + this.refreshStatus(ref, isTag); + }, 10000); // Refresh every 10 seconds } - private stopRefreshingBranch(branch: string) { - if (this.inProgressRefreshIntervals[branch]) { - clearInterval(this.inProgressRefreshIntervals[branch]); - delete this.inProgressRefreshIntervals[branch]; + private stopRefreshing(ref: string, isTag: boolean) { + const key = `${isTag ? "tag" : "branch"}:${ref}`; + if (this.inProgressRefreshIntervals[key]) { + clearInterval(this.inProgressRefreshIntervals[key]); + delete this.inProgressRefreshIntervals[key]; } } - private async refreshBranchStatus(branch: string) { + private async refreshStatus(ref: string, isTag: boolean) { const ownerAndRepo = await this.gitService.getOwnerAndRepo(); const ciType = this.gitService.detectCIType(); if (ownerAndRepo && ciType) { - const { owner, repo } = ownerAndRepo; - const buildStatus = await this.ciService.getBuildStatus(branch, owner, repo, ciType, false, true); + const {owner, repo} = ownerAndRepo; + const buildStatus = await this.ciService.getBuildStatus(ref, owner, repo, ciType, isTag, true); if (buildStatus) { - await this.updateCIStatus(buildStatus.status, branch, buildStatus.url, false); - this.lastKnownStatuses[branch] = buildStatus.status; + await this.updateCIStatus(buildStatus.status, ref, buildStatus.url, isTag); + this.lastKnownStatuses[ref] = buildStatus.status; if (!this.ciService.isInProgressStatus(buildStatus.status)) { - this.stopRefreshingBranch(branch); + this.stopRefreshing(ref, isTag); } } } } private async refreshAfterPush() { - Logger.log('Refreshing branch build status after push', 'INFO'); + Logger.log("Refreshing branch build status after push", "INFO"); const currentBranch = await this.gitService.getCurrentBranch(); const ownerAndRepo = await this.gitService.getOwnerAndRepo(); const ciType = this.gitService.detectCIType(); if (currentBranch && ownerAndRepo && ciType) { - const { owner, repo } = ownerAndRepo; - await this.updateBranchBuildStatus(owner, repo, ciType, true); + const {owner, repo} = ownerAndRepo; + await this.updateBuildStatus(currentBranch, owner, repo, ciType, false, true); await this.updateCommitCountButton(true); await this.updateVersionButtons(); await this.updateCompareUrl(); @@ -656,4 +734,4 @@ export class StatusBarService { public triggerUpdate(forceRefresh: boolean = false): void { this.debouncedUpdateEverything(forceRefresh); } -} \ No newline at end of file +} diff --git a/src/test/suite/ciService.test.ts b/src/test/suite/ciService.test.ts index 710d607..09a9fc9 100644 --- a/src/test/suite/ciService.test.ts +++ b/src/test/suite/ciService.test.ts @@ -1,240 +1,258 @@ -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { CIService } from '../../services/ciService'; -import axios from 'axios'; -import { setupTestEnvironment, teardownTestEnvironment } from './testSetup'; -import { Logger } from '../../utils/logger'; - -suite('CIService Test Suite', () => { - let sandbox: sinon.SinonSandbox; - let testEnv: ReturnType; - let ciService: CIService; - - setup(() => { - testEnv = setupTestEnvironment(); - sandbox = testEnv.sandbox; - ciService = new CIService(); - - // Stub Logger to prevent actual logging during tests - sandbox.stub(Logger, 'log'); - }); - - teardown(() => { - teardownTestEnvironment(sandbox); - }); - - test('getBuildStatus should return correct status for GitHub tag', async () => { - const axiosStub = sandbox.stub(axios, 'get').resolves({ - data: { - total_count: 1, - workflow_runs: [{ - id: 123, - status: 'completed', - conclusion: 'success', - html_url: 'https://github.com/owner/repo/actions/runs/123', - head_commit: { id: '1234567890abcdef' }, - head_branch: '1.0.0' - }], - }, - headers: { - 'x-ratelimit-limit': '5000', - 'x-ratelimit-remaining': '4999', - 'x-ratelimit-reset': '1609459200' - } - }); - - const result = await ciService.getBuildStatus('1.0.0', 'owner', 'repo', 'github', true); - assert.strictEqual(result?.status, 'success'); - assert.strictEqual(result?.url, 'https://github.com/owner/repo/actions/runs/123'); - }); - - test('getBuildStatus should return correct status for GitHub branch', async () => { - const axiosStub = sandbox.stub(axios, 'get').resolves({ - data: { - total_count: 1, - workflow_runs: [{ - id: 123, - status: 'completed', - conclusion: 'success', - html_url: 'https://github.com/owner/repo/actions/runs/123', - head_commit: { - id: '1234567890abcdef' - }, - head_branch: 'main' - }], - }, - headers: { - 'x-ratelimit-limit': '5000', - 'x-ratelimit-remaining': '4999', - 'x-ratelimit-reset': '1609459200', - }, - }); - - const result = await ciService.getBuildStatus('main', 'owner', 'repo', 'github', false); - assert.deepStrictEqual(result, { - status: 'success', - url: 'https://github.com/owner/repo/actions/runs/123', - message: 'GitHub CI returning status: success for branch main' - }); - }); - - test('getBuildStatus should return correct status for GitLab tag', async () => { - const axiosStub = sandbox.stub(axios, 'get').resolves({ - data: [{ - id: 123, - status: 'success', - web_url: 'https://gitlab.com/owner/repo/-/pipelines/123', - ref: '1.0.0' - }], - headers: { - 'ratelimit-limit': '5000', - 'ratelimit-remaining': '4999', - 'ratelimit-reset': '1609459200', - }, - }); - - const result = await ciService.getBuildStatus('1.0.0', 'owner', 'repo', 'gitlab', true); - assert.deepStrictEqual(result, { - status: 'success', - url: 'https://gitlab.com/api/v4/owner/repo/-/pipelines/123', - message: 'GitLab CI returning status: success for tag 1.0.0' - }); - }); - - test('getBuildStatus should return no_runs for GitLab when no matching pipeline is found', async () => { - const axiosStub = sandbox.stub(axios, 'get').resolves({ - data: [{ - id: 123, - status: 'success', - web_url: 'https://gitlab.com/owner/repo/-/pipelines/123', - ref: 'main' - }], - headers: { - 'ratelimit-limit': '5000', - 'ratelimit-remaining': '4999', - 'ratelimit-reset': '1609459200', - }, - }); - - const result = await ciService.getBuildStatus('1.0.0', 'owner', 'repo', 'gitlab', true); - assert.deepStrictEqual(result, { - status: 'no_runs', - url: 'https://gitlab.com/api/v4/owner/repo/-/pipelines', - message: 'No pipeline found for tag 1.0.0' - }); - }); - - test('getImmediateBuildStatus should return fresh status', async () => { - sandbox.stub(axios, 'get').resolves({ - data: { - total_count: 1, - workflow_runs: [{ - id: 123, - status: 'completed', - conclusion: 'success', - html_url: 'https://github.com/owner/repo/actions/runs/123', - head_commit: { id: '1234567890abcdef' }, - head_branch: 'main' - }], - }, - headers: { - 'x-ratelimit-limit': '5000', - 'x-ratelimit-remaining': '4999', - 'x-ratelimit-reset': '1609459200', - }, - }); - - const result = await ciService.getImmediateBuildStatus('main', 'owner', 'repo', 'github', false); - assert.strictEqual(result.status, 'success'); - }); - - test('Should handle errors gracefully when GitService fails', async () => { - sandbox.stub(axios, 'get').rejects(new Error('Network error')); - const result = await ciService.getBuildStatus('main', 'owner', 'repo', 'github', false); - assert.strictEqual(result?.status, 'unknown'); - assert.strictEqual(result?.message, undefined); - assert.strictEqual(result?.url, 'https://github.com/owner/repo/actions'); - }); - - test('Should handle GitLab CI type correctly', async () => { - sandbox.stub(axios, 'get').resolves({ - data: [{ - status: 'success', - web_url: 'https://gitlab.com/owner/repo/-/pipelines/123', - ref: 'main' - }], - headers: {} - }); - const result = await ciService.getBuildStatus('main', 'owner', 'repo', 'gitlab', false); - assert.strictEqual(result?.status, 'success'); - }); - - test('Should handle no CI configuration', async () => { - const result = await ciService.getBuildStatus('main', 'owner', 'repo', 'unknown' as any, false); - assert.strictEqual(result?.status, 'unknown'); - }); - - test('Should handle rate limiting', async () => { - sandbox.stub(axios, 'get').resolves({ - data: { message: 'API rate limit exceeded' }, - headers: { - 'x-ratelimit-limit': '60', - 'x-ratelimit-remaining': '0', - 'x-ratelimit-reset': '1609459200' - } - }); - const result = await ciService.getBuildStatus('main', 'owner', 'repo', 'github', false); - assert.strictEqual(result?.status, 'unknown'); - assert.strictEqual(result?.message, undefined); - assert.strictEqual(result?.url, 'https://github.com/owner/repo/actions'); - }); - - test('Should handle tag creation', async () => { - sandbox.stub(axios, 'get').resolves({ - data: { - total_count: 1, - workflow_runs: [{ - id: 123, - status: 'completed', - conclusion: 'success', - html_url: 'https://github.com/owner/repo/actions/runs/123', - head_branch: 'v1.0.1', - head_commit: { - id: 'v1.0.1' - } - }] - }, - headers: {} - }); - const result = await ciService.getBuildStatus('v1.0.1', 'owner', 'repo', 'github', true); - assert.strictEqual(result?.status, 'success'); - }); - - test('Should clear cache correctly', () => { - ciService.clearCache(); - // @ts-ignore: Accessing private property for testing - assert.deepStrictEqual(ciService.buildStatusCache, {}); - }); - - test('Should clear cache for specific repo', () => { - // @ts-ignore: Accessing private property for testing - ciService.buildStatusCache = { 'owner/repo': { 'main/github': { status: 'success', url: 'test', timestamp: Date.now() } } }; - ciService.clearCacheForRepo('owner', 'repo'); - // @ts-ignore: Accessing private property for testing - assert.deepStrictEqual(ciService.buildStatusCache, {}); - }); - - test('Should clear cache for specific branch', () => { - // @ts-ignore: Accessing private property for testing - ciService.buildStatusCache = { 'owner/repo': { 'main/github': { status: 'success', url: 'test', timestamp: Date.now() } } }; - ciService.clearCacheForBranch('main', 'owner', 'repo', 'github'); - // @ts-ignore: Accessing private property for testing - assert.deepStrictEqual(ciService.buildStatusCache, { 'owner/repo': {} }); - }); - - test('Should correctly identify in-progress status', () => { - assert.strictEqual(ciService.isInProgressStatus('pending'), true); - assert.strictEqual(ciService.isInProgressStatus('in_progress'), true); - assert.strictEqual(ciService.isInProgressStatus('success'), false); - }); +import * as assert from "assert"; +import * as sinon from "sinon"; +import {CIService} from "../../services/ciService"; +import axios from "axios"; +import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; +import {Logger} from "../../utils/logger"; + +suite("CIService Test Suite", () => { + let sandbox: sinon.SinonSandbox; + let testEnv: ReturnType; + let ciService: CIService; + + setup(() => { + testEnv = setupTestEnvironment(); + sandbox = testEnv.sandbox; + ciService = new CIService(); + + // Stub Logger to prevent actual logging during tests + sandbox.stub(Logger, "log"); + }); + + teardown(() => { + teardownTestEnvironment(sandbox); + }); + + test("getBuildStatus should return correct status for GitHub tag", async () => { + const axiosStub = sandbox.stub(axios, "get").resolves({ + data: { + total_count: 1, + workflow_runs: [ + { + id: 123, + status: "completed", + conclusion: "success", + html_url: "https://github.com/owner/repo/actions/runs/123", + head_commit: {id: "1234567890abcdef"}, + head_branch: "1.0.0" + } + ] + }, + headers: { + "x-ratelimit-limit": "5000", + "x-ratelimit-remaining": "4999", + "x-ratelimit-reset": "1609459200" + } + }); + + const result = await ciService.getBuildStatus("1.0.0", "owner", "repo", "github", true); + assert.strictEqual(result?.status, "success"); + assert.strictEqual(result?.url, "https://github.com/owner/repo/actions/runs/123"); + }); + + test("getBuildStatus should return correct status for GitHub branch", async () => { + const axiosStub = sandbox.stub(axios, "get").resolves({ + data: { + total_count: 1, + workflow_runs: [ + { + id: 123, + status: "completed", + conclusion: "success", + html_url: "https://github.com/owner/repo/actions/runs/123", + head_commit: { + id: "1234567890abcdef" + }, + head_branch: "main" + } + ] + }, + headers: { + "x-ratelimit-limit": "5000", + "x-ratelimit-remaining": "4999", + "x-ratelimit-reset": "1609459200" + } + }); + + const result = await ciService.getBuildStatus("main", "owner", "repo", "github", false); + assert.deepStrictEqual(result, { + status: "success", + url: "https://github.com/owner/repo/actions/runs/123", + message: "GitHub CI returning status: success for branch main" + }); + }); + + test("getBuildStatus should return correct status for GitLab tag", async () => { + const axiosStub = sandbox.stub(axios, "get").resolves({ + data: [ + { + id: 123, + status: "success", + web_url: "https://gitlab.com/owner/repo/-/pipelines/123", + ref: "1.0.0" + } + ], + headers: { + "ratelimit-limit": "5000", + "ratelimit-remaining": "4999", + "ratelimit-reset": "1609459200" + } + }); + + const result = await ciService.getBuildStatus("1.0.0", "owner", "repo", "gitlab", true); + assert.deepStrictEqual(result, { + status: "success", + url: "https://gitlab.com/api/v4/owner/repo/-/pipelines/123", + message: "GitLab CI returning status: success for tag 1.0.0" + }); + }); + + test("getBuildStatus should return no_runs for GitLab when no matching pipeline is found", async () => { + const axiosStub = sandbox.stub(axios, "get").resolves({ + data: [ + { + id: 123, + status: "success", + web_url: "https://gitlab.com/owner/repo/-/pipelines/123", + ref: "main" + } + ], + headers: { + "ratelimit-limit": "5000", + "ratelimit-remaining": "4999", + "ratelimit-reset": "1609459200" + } + }); + + const result = await ciService.getBuildStatus("1.0.0", "owner", "repo", "gitlab", true); + assert.deepStrictEqual(result, { + status: "no_runs", + url: "https://gitlab.com/api/v4/owner/repo/-/pipelines", + message: "No pipeline found for tag 1.0.0" + }); + }); + + test("getImmediateBuildStatus should return fresh status", async () => { + sandbox.stub(axios, "get").resolves({ + data: { + total_count: 1, + workflow_runs: [ + { + id: 123, + status: "completed", + conclusion: "success", + html_url: "https://github.com/owner/repo/actions/runs/123", + head_commit: {id: "1234567890abcdef"}, + head_branch: "main" + } + ] + }, + headers: { + "x-ratelimit-limit": "5000", + "x-ratelimit-remaining": "4999", + "x-ratelimit-reset": "1609459200" + } + }); + + const result = await ciService.getImmediateBuildStatus("main", "owner", "repo", "github", false); + assert.strictEqual(result.status, "success"); + }); + + test("Should handle errors gracefully when GitService fails", async () => { + sandbox.stub(axios, "get").rejects(new Error("Network error")); + const result = await ciService.getBuildStatus("main", "owner", "repo", "github", false); + assert.strictEqual(result?.status, "unknown"); + assert.strictEqual(result?.message, undefined); + assert.strictEqual(result?.url, "https://github.com/owner/repo/actions"); + }); + + test("Should handle GitLab CI type correctly", async () => { + sandbox.stub(axios, "get").resolves({ + data: [ + { + status: "success", + web_url: "https://gitlab.com/owner/repo/-/pipelines/123", + ref: "main" + } + ], + headers: {} + }); + const result = await ciService.getBuildStatus("main", "owner", "repo", "gitlab", false); + assert.strictEqual(result?.status, "success"); + }); + + test("Should handle no CI configuration", async () => { + const result = await ciService.getBuildStatus("main", "owner", "repo", "unknown" as any, false); + assert.strictEqual(result?.status, "unknown"); + }); + + test("Should handle rate limiting", async () => { + sandbox.stub(axios, "get").resolves({ + data: {message: "API rate limit exceeded"}, + headers: { + "x-ratelimit-limit": "60", + "x-ratelimit-remaining": "0", + "x-ratelimit-reset": "1609459200" + } + }); + const result = await ciService.getBuildStatus("main", "owner", "repo", "github", false); + assert.strictEqual(result?.status, "unknown"); + assert.strictEqual(result?.message, undefined); + assert.strictEqual(result?.url, "https://github.com/owner/repo/actions"); + }); + + test("Should handle tag creation", async () => { + sandbox.stub(axios, "get").resolves({ + data: { + total_count: 1, + workflow_runs: [ + { + id: 123, + status: "completed", + conclusion: "success", + html_url: "https://github.com/owner/repo/actions/runs/123", + head_branch: "v1.0.1", + head_commit: { + id: "v1.0.1" + } + } + ] + }, + headers: {} + }); + const result = await ciService.getBuildStatus("v1.0.1", "owner", "repo", "github", true); + assert.strictEqual(result?.status, "success"); + }); + + test("Should clear cache correctly", () => { + ciService.clearCache(); + // @ts-ignore: Accessing private property for testing + assert.deepStrictEqual(ciService.buildStatusCache, {}); + }); + + test("Should clear cache for specific repo", () => { + // @ts-ignore: Accessing private property for testing + ciService.buildStatusCache = { + "owner/repo": {"main/github": {status: "success", url: "test", timestamp: Date.now()}} + }; + ciService.clearCacheForRepo("owner", "repo"); + // @ts-ignore: Accessing private property for testing + assert.deepStrictEqual(ciService.buildStatusCache, {}); + }); + + test("Should clear cache for specific branch", () => { + // @ts-ignore: Accessing private property for testing + ciService.buildStatusCache = { + "owner/repo": {"main/github": {status: "success", url: "test", timestamp: Date.now()}} + }; + ciService.clearCacheForBranch("main", "owner", "repo", "github"); + // @ts-ignore: Accessing private property for testing + assert.deepStrictEqual(ciService.buildStatusCache, {"owner/repo": {}}); + }); + + test("Should correctly identify in-progress status", () => { + assert.strictEqual(ciService.isInProgressStatus("pending"), true); + assert.strictEqual(ciService.isInProgressStatus("in_progress"), true); + assert.strictEqual(ciService.isInProgressStatus("success"), false); + }); }); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 7be2bf2..623256c 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,58 +1,57 @@ -import * as sinon from 'sinon'; -import { GitService } from '../../services/gitService'; -import { CIService } from '../../services/ciService'; -import { StatusBarService } from '../../services/statusBarService'; -import { setupTestEnvironment, teardownTestEnvironment } from './testSetup'; -import { updateStatusBar } from '../../utils/statusBarUpdater'; - -suite('Extension Test Suite', () => { - let sandbox: sinon.SinonSandbox; - let testEnv: ReturnType; - let gitService: sinon.SinonStubbedInstance; - let ciService: sinon.SinonStubbedInstance; - let statusBarService: sinon.SinonStubbedInstance; - - setup(() => { - testEnv = setupTestEnvironment(); - sandbox = testEnv.sandbox; - - gitService = sandbox.createStubInstance(GitService); - ciService = sandbox.createStubInstance(CIService); - statusBarService = sandbox.createStubInstance(StatusBarService); - - // Setup default stub behaviors - gitService.initialize.resolves(true); - gitService.getCurrentRepo.resolves('test-repo'); - gitService.fetchAndTags.resolves({ latest: '1.0.0' }); - gitService.getCurrentBranch.resolves('main'); - gitService.detectCIType.returns('github'); - gitService.getOwnerAndRepo.resolves({ owner: 'testowner', repo: 'testrepo' }); - - ciService.getBuildStatus.resolves({ status: 'success', url: 'http://example.com' }); - }); - - teardown(() => { - teardownTestEnvironment(sandbox); - }); - - test('StatusBarService should update everything', async () => { - await updateStatusBar(gitService, statusBarService); - - sinon.assert.calledOnce(statusBarService.updateEverything); - }); - - test('StatusBarService should update status bar and CI status', async () => { - // Set up the stubs - gitService.isInitialized.returns(true); - gitService.getCurrentRepo.resolves('testrepo'); - gitService.getOwnerAndRepo.resolves({ owner: 'testowner', repo: 'testrepo' }); - gitService.detectCIType.returns('github'); - - // Call the function to update the status bar - await statusBarService.updateEverything(false); - - // Assertions - sinon.assert.calledOnce(statusBarService.updateEverything); - }); - +import * as sinon from "sinon"; +import {GitService} from "../../services/gitService"; +import {CIService} from "../../services/ciService"; +import {StatusBarService} from "../../services/statusBarService"; +import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; +import {updateStatusBar} from "../../utils/statusBarUpdater"; + +suite("Extension Test Suite", () => { + let sandbox: sinon.SinonSandbox; + let testEnv: ReturnType; + let gitService: sinon.SinonStubbedInstance; + let ciService: sinon.SinonStubbedInstance; + let statusBarService: sinon.SinonStubbedInstance; + + setup(() => { + testEnv = setupTestEnvironment(); + sandbox = testEnv.sandbox; + + gitService = sandbox.createStubInstance(GitService); + ciService = sandbox.createStubInstance(CIService); + statusBarService = sandbox.createStubInstance(StatusBarService); + + // Setup default stub behaviors + gitService.initialize.resolves(true); + gitService.getCurrentRepo.resolves("test-repo"); + gitService.fetchAndTags.resolves({latest: "1.0.0"}); + gitService.getCurrentBranch.resolves("main"); + gitService.detectCIType.returns("github"); + gitService.getOwnerAndRepo.resolves({owner: "testowner", repo: "testrepo"}); + + ciService.getBuildStatus.resolves({status: "success", url: "http://example.com"}); + }); + + teardown(() => { + teardownTestEnvironment(sandbox); + }); + + test("StatusBarService should update everything", async () => { + await updateStatusBar(gitService, statusBarService); + + sinon.assert.calledOnce(statusBarService.updateEverything); + }); + + test("StatusBarService should update status bar and CI status", async () => { + // Set up the stubs + gitService.isInitialized.returns(true); + gitService.getCurrentRepo.resolves("testrepo"); + gitService.getOwnerAndRepo.resolves({owner: "testowner", repo: "testrepo"}); + gitService.detectCIType.returns("github"); + + // Call the function to update the status bar + await statusBarService.updateEverything(false); + + // Assertions + sinon.assert.calledOnce(statusBarService.updateEverything); + }); }); diff --git a/src/test/suite/gitService.test.ts b/src/test/suite/gitService.test.ts index 11d87bb..d80abfa 100644 --- a/src/test/suite/gitService.test.ts +++ b/src/test/suite/gitService.test.ts @@ -1,209 +1,259 @@ -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { GitService } from '../../services/gitService'; -import * as vscode from 'vscode'; -import { SimpleGit } from 'simple-git'; -import mock from 'mock-fs'; -import { setupTestEnvironment, teardownTestEnvironment } from './testSetup'; - -suite('GitService Test Suite', () => { - let sandbox: sinon.SinonSandbox; - let testEnv: ReturnType; - let gitService: GitService; - let mockSimpleGit: SimpleGit; - - setup(() => { - testEnv = setupTestEnvironment(); - sandbox = testEnv.sandbox; - - // Mock vscode.workspace.workspaceFolders - sandbox.stub(vscode.workspace, 'workspaceFolders').value([ - { uri: { fsPath: '/mock/repo' } } - ] as any); - - // Mock simple-git - mockSimpleGit = { - checkIsRepo: sandbox.stub().resolves(true), - getRemotes: sandbox.stub().resolves([{ name: 'origin', refs: { fetch: 'https://github.com/owner/repo.git', push: 'https://github.com/owner/repo.git' } }]), - // Add other methods you need to mock - } as unknown as SimpleGit; - - // Mock the file system - mock({ - '/mock/repo': { - '.git': {}, // Simulate a git repository - }, - }); - - // Create a mock vscode.ExtensionContext - const mockContext: vscode.ExtensionContext = { - subscriptions: [], - workspaceState: { - get: sandbox.stub(), - update: sandbox.stub(), - }, - globalState: { - get: sandbox.stub(), - update: sandbox.stub(), - }, - extensionPath: '/mock/extension', - asAbsolutePath: (relativePath: string) => `/mock/extension/${relativePath}`, - storagePath: '/mock/storage', - globalStoragePath: '/mock/global-storage', - logPath: '/mock/log', - } as unknown as vscode.ExtensionContext; - - // Initialize GitService with the mocked simple-git and context - gitService = new GitService(mockContext); - gitService['git'] = mockSimpleGit; - }); - - teardown(() => { - teardownTestEnvironment(sandbox); - mock.restore(); - }); - - suite('getOwnerAndRepo', () => { - // Individual test cases - test('should return undefined for invalid URLs', async () => { - (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([{ name: 'origin', refs: { fetch: 'invalid-url', push: 'invalid-url' } }]); - - const result = await gitService.getOwnerAndRepo(); - assert.strictEqual(result, undefined); - }); - - test('should handle GitHub Enterprise URLs', async () => { - (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([{ name: 'origin', refs: { fetch: 'https://github.mycompany.com/owner/repo.git', push: 'https://github.mycompany.com/owner/repo.git' } }]); - - const result = await gitService.getOwnerAndRepo(); - assert.deepStrictEqual(result, { owner: 'owner', repo: 'repo' }); - }); - - test('should handle GitLab self-hosted URLs', async () => { - (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([{ name: 'origin', refs: { fetch: 'https://gitlab.mycompany.com/group/repo.git', push: 'https://gitlab.mycompany.com/group/repo.git' } }]); - - const result = await gitService.getOwnerAndRepo(); - assert.deepStrictEqual(result, { owner: 'group', repo: 'repo' }); - }); - - test('should return undefined when git is not initialized', async () => { - (mockSimpleGit.checkIsRepo as sinon.SinonStub).resolves(false); - (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([]); // Ensure no remotes are returned - - const result = await gitService.getOwnerAndRepo(); - assert.strictEqual(result, undefined); - }); - - // Array of test cases - const testCases = [ - { - name: 'GitHub HTTPS URL', - url: 'https://github.com/owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitHub SSH URL', - url: 'git@github.com:owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitHub HTTPS URL with credentials', - url: 'https://username:token@github.com/owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitLab HTTPS URL', - url: 'https://gitlab.com/owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitLab SSH URL', - url: 'git@gitlab.com:owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitLab HTTPS URL with credentials', - url: 'https://username:token@gitlab.com/owner/repo.git', - expected: { owner: 'owner', repo: 'repo' } - }, - { - name: 'GitLab HTTPS URL with subgroup', - url: 'https://gitlab.com/group/subgroup/repo.git', - expected: { owner: 'group/subgroup', repo: 'repo' } - }, - { - name: 'GitLab SSH URL with subgroup', - url: 'git@gitlab.com:group/subgroup/repo.git', - expected: { owner: 'group/subgroup', repo: 'repo' } - }, - { - name: 'GitLab HTTPS URL with multiple subgroups', - url: 'https://gitlab.com/group/subgroup1/subgroup2/repo.git', - expected: { owner: 'group/subgroup1/subgroup2', repo: 'repo' } - }, - { - name: 'GitLab SSH URL with multiple subgroups', - url: 'git@gitlab.com:group/subgroup1/subgroup2/repo.git', - expected: { owner: 'group/subgroup1/subgroup2', repo: 'repo' } - } - ]; - - testCases.forEach(({ name, url, expected }) => { - test(`getOwnerAndRepo should extract owner and repo from ${name}`, async () => { - (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([{ name: 'origin', refs: { fetch: url, push: url } }]); - - const result = await gitService.getOwnerAndRepo(); - assert.deepStrictEqual(result, expected); - }); - }); - }); - - test('getCommitCounts should return correct count for unreleased commits', async () => { - const gitStub = { - raw: sandbox.stub().resolves('5'), - fetch: sandbox.stub().resolves() - }; - (gitService as any).git = gitStub; - (gitService as any).refExists = sandbox.stub().resolves(true); - - const count = await gitService.getCommitCounts('v1.0.0', 'HEAD'); - assert.strictEqual(count, 5); - }); - - test('getCommitCounts should return total commit count when from is null', async () => { - const gitStub = { - raw: sandbox.stub().resolves('10'), - fetch: sandbox.stub().resolves() - }; - (gitService as any).git = gitStub; - (gitService as any).refExists = sandbox.stub().resolves(true); - - const count = await gitService.getCommitCounts(null, 'HEAD'); - assert.strictEqual(count, 10); - }); - - test('getDefaultBranch should return cached value if available', async () => { - (gitService as any).defaultBranchCache.set('testRepo', 'main'); - (gitService as any).activeRepository = 'testRepo'; - const gitStub = { - raw: sandbox.stub().resolves('origin/main') - }; - (gitService as any).git = gitStub; - - const branch = await gitService.getDefaultBranch(); - assert.strictEqual(branch, 'main'); - }); - - test('getDefaultBranch should fetch and cache default branch if not cached', async () => { - const gitStub = { - raw: sandbox.stub().resolves('origin/main') - }; - (gitService as any).git = gitStub; - (gitService as any).activeRepository = 'testRepo'; - - const branch = await gitService.getDefaultBranch(); - assert.strictEqual(branch, 'main'); - sinon.assert.calledWith(gitStub.raw, ['remote', 'show', 'origin']); - assert.strictEqual((gitService as any).defaultBranchCache.get('testRepo'), 'main'); - }); +import * as assert from "assert"; +import * as sinon from "sinon"; +import {GitService} from "../../services/gitService"; +import * as vscode from "vscode"; +import {SimpleGit} from "simple-git"; +import mock from "mock-fs"; +import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; + +suite("GitService Test Suite", () => { + let sandbox: sinon.SinonSandbox; + let testEnv: ReturnType; + let gitService: GitService; + let mockSimpleGit: SimpleGit; + + setup(() => { + testEnv = setupTestEnvironment(); + sandbox = testEnv.sandbox; + + // Mock vscode.workspace.workspaceFolders + sandbox.stub(vscode.workspace, "workspaceFolders").value([{uri: {fsPath: "/mock/repo"}}] as any); + + // Mock simple-git + mockSimpleGit = { + checkIsRepo: sandbox.stub().resolves(true), + getRemotes: sandbox.stub().resolves([ + { + name: "origin", + refs: {fetch: "https://github.com/owner/repo.git", push: "https://github.com/owner/repo.git"} + } + ]) + // Add other methods you need to mock + } as unknown as SimpleGit; + + // Mock the file system + mock({ + "/mock/repo": { + ".git": {} // Simulate a git repository + } + }); + + // Create a mock vscode.ExtensionContext + const mockContext: vscode.ExtensionContext = { + subscriptions: [], + workspaceState: { + get: sandbox.stub(), + update: sandbox.stub() + }, + globalState: { + get: sandbox.stub(), + update: sandbox.stub() + }, + extensionPath: "/mock/extension", + asAbsolutePath: (relativePath: string) => `/mock/extension/${relativePath}`, + storagePath: "/mock/storage", + globalStoragePath: "/mock/global-storage", + logPath: "/mock/log" + } as unknown as vscode.ExtensionContext; + + // Initialize GitService with the mocked simple-git and context + gitService = new GitService(mockContext); + gitService["git"] = mockSimpleGit; + }); + + teardown(() => { + teardownTestEnvironment(sandbox); + mock.restore(); + }); + + suite("getOwnerAndRepo", () => { + // Individual test cases + test("should return undefined for invalid URLs", async () => { + (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([ + {name: "origin", refs: {fetch: "invalid-url", push: "invalid-url"}} + ]); + + const result = await gitService.getOwnerAndRepo(); + assert.strictEqual(result, undefined); + }); + + test("should handle GitHub Enterprise URLs", async () => { + (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([ + { + name: "origin", + refs: { + fetch: "https://github.mycompany.com/owner/repo.git", + push: "https://github.mycompany.com/owner/repo.git" + } + } + ]); + + const result = await gitService.getOwnerAndRepo(); + assert.deepStrictEqual(result, {owner: "owner", repo: "repo"}); + }); + + test("should handle GitLab self-hosted URLs", async () => { + (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([ + { + name: "origin", + refs: { + fetch: "https://gitlab.mycompany.com/group/repo.git", + push: "https://gitlab.mycompany.com/group/repo.git" + } + } + ]); + + const result = await gitService.getOwnerAndRepo(); + assert.deepStrictEqual(result, {owner: "group", repo: "repo"}); + }); + + test("should return undefined when git is not initialized", async () => { + (mockSimpleGit.checkIsRepo as sinon.SinonStub).resolves(false); + (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([]); // Ensure no remotes are returned + + const result = await gitService.getOwnerAndRepo(); + assert.strictEqual(result, undefined); + }); + + // Array of test cases + const testCases = [ + { + name: "GitHub HTTPS URL", + url: "https://github.com/owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitHub SSH URL", + url: "git@github.com:owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitHub HTTPS URL with credentials", + url: "https://username:token@github.com/owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitLab HTTPS URL", + url: "https://gitlab.com/owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitLab SSH URL", + url: "git@gitlab.com:owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitLab HTTPS URL with credentials", + url: "https://username:token@gitlab.com/owner/repo.git", + expected: {owner: "owner", repo: "repo"} + }, + { + name: "GitLab HTTPS URL with subgroup", + url: "https://gitlab.com/group/subgroup/repo.git", + expected: {owner: "group/subgroup", repo: "repo"} + }, + { + name: "GitLab SSH URL with subgroup", + url: "git@gitlab.com:group/subgroup/repo.git", + expected: {owner: "group/subgroup", repo: "repo"} + }, + { + name: "GitLab HTTPS URL with multiple subgroups", + url: "https://gitlab.com/group/subgroup1/subgroup2/repo.git", + expected: {owner: "group/subgroup1/subgroup2", repo: "repo"} + }, + { + name: "GitLab SSH URL with multiple subgroups", + url: "git@gitlab.com:group/subgroup1/subgroup2/repo.git", + expected: {owner: "group/subgroup1/subgroup2", repo: "repo"} + } + ]; + + testCases.forEach(({name, url, expected}) => { + test(`getOwnerAndRepo should extract owner and repo from ${name}`, async () => { + (mockSimpleGit.getRemotes as sinon.SinonStub).resolves([{name: "origin", refs: {fetch: url, push: url}}]); + + const result = await gitService.getOwnerAndRepo(); + assert.deepStrictEqual(result, expected); + }); + }); + }); + + test("getCommitCounts should return correct count for unreleased commits", async () => { + const gitStub = { + raw: sandbox.stub().resolves("5"), + fetch: sandbox.stub().resolves() + }; + (gitService as any).git = gitStub; + (gitService as any).refExists = sandbox.stub().resolves(true); + + const count = await gitService.getCommitCounts("v1.0.0", "HEAD"); + assert.strictEqual(count, 5); + }); + + test("getCommitCounts should return total commit count when from is null", async () => { + const gitStub = { + raw: sandbox.stub().resolves("10"), + fetch: sandbox.stub().resolves() + }; + (gitService as any).git = gitStub; + (gitService as any).refExists = sandbox.stub().resolves(true); + + const count = await gitService.getCommitCounts(null, "HEAD"); + assert.strictEqual(count, 10); + }); + + suite("getDefaultBranch", () => { + test("should return cached value if available", async () => { + (gitService as any).defaultBranchCache.set("testRepo", "main"); + (gitService as any).activeRepository = "testRepo"; + sandbox.stub(gitService, "getCurrentRepo").resolves("testRepo"); + + const branch = await gitService.getDefaultBranch(); + assert.strictEqual(branch, "main"); + }); + + test("should fetch and cache default branch if not cached", async () => { + const gitStub = { + raw: sandbox.stub().resolves("HEAD branch: main") // Simulate fetching the default branch + }; + (gitService as any).git = gitStub; + (gitService as any).activeRepository = "testRepo"; + (gitService as any).defaultBranchCache.clear(); // Clear the cache + sandbox.stub(gitService, "getCurrentRepo").resolves("testRepo"); + + const branch = await gitService.getDefaultBranch(); + assert.strictEqual(branch, "main"); + sinon.assert.calledWith(gitStub.raw, ["remote", "show", "origin"]); + assert.strictEqual((gitService as any).defaultBranchCache.get("testRepo"), "main"); + }); + + + test("should return null if no default branch is found", async () => { + const gitStub = { + raw: sandbox.stub().resolves("HEAD branch: ") // Simulate no default branch found + }; + (gitService as any).git = gitStub; + (gitService as any).activeRepository = "testRepo"; + + const branch = await gitService.getDefaultBranch(); + assert.strictEqual(branch, null); + }); + + test("should return current branch if no default branch is found", async () => { + (gitService as any).activeRepository = "testRepo"; + (gitService as any).defaultBranchCache.set("testRepo", null); + + const gitStub = { + raw: sandbox.stub().resolves("HEAD branch: "), // Simulate no default branch found + revparse: sandbox.stub().resolves("main") // Simulate getting the current branch + }; + (gitService as any).git = gitStub; + sandbox.stub(gitService, "getCurrentRepo").resolves("testRepo"); + + const branch = await gitService.getDefaultBranch(); + assert.strictEqual(branch, "main"); + assert.strictEqual((gitService as any).defaultBranchCache.get("testRepo"), "main"); + }); + }); }); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index ca64a74..6d01753 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,23 +1,23 @@ -import path from 'path'; -import Mocha from 'mocha'; -import { glob } from 'glob'; -import { setupTestEnvironment, teardownTestEnvironment } from './testSetup'; +import path from "path"; +import Mocha from "mocha"; +import {glob} from "glob"; +import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; export async function run(): Promise { const mocha = new Mocha({ - ui: 'tdd', + ui: "tdd", color: true }); - const testsRoot = path.resolve(__dirname, '..'); + const testsRoot = path.resolve(__dirname, ".."); // Add root hooks mocha.rootHooks({ - beforeAll: function() { + beforeAll: function () { const testEnv = setupTestEnvironment(); (global as any).testSandbox = testEnv.sandbox; }, - afterAll: function() { + afterAll: function () { const sandbox = (global as any).testSandbox; if (sandbox) { teardownTestEnvironment(sandbox); @@ -26,7 +26,7 @@ export async function run(): Promise { }); try { - const files = await glob('**/**.test.js', { cwd: testsRoot }); + const files = await glob("**/**.test.js", {cwd: testsRoot}); // Add files to the test suite files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); @@ -42,7 +42,7 @@ export async function run(): Promise { }); }); } catch (err) { - console.error('Error loading test files:', err); + console.error("Error loading test files:", err); throw err; } -} \ No newline at end of file +} diff --git a/src/test/suite/statusBarService.test.ts b/src/test/suite/statusBarService.test.ts index dc3f28a..b48dc51 100644 --- a/src/test/suite/statusBarService.test.ts +++ b/src/test/suite/statusBarService.test.ts @@ -7,7 +7,8 @@ import {CIService} from "../../services/ciService"; import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; import {Logger} from "../../utils/logger"; -suite("StatusBarService Test Suite", () => { +suite("StatusBarService Test Suite", function () { + this.timeout(11000); // Extend the timeout to 11 seconds let sandbox: sinon.SinonSandbox; let testEnv: ReturnType; let statusBarService: StatusBarService; @@ -15,6 +16,7 @@ suite("StatusBarService Test Suite", () => { let ciServiceStub: sinon.SinonStubbedInstance; let contextStub: sinon.SinonStubbedInstance; let loggerSpy: sinon.SinonSpy; + let gitPushEmitter: vscode.EventEmitter; setup(() => { testEnv = setupTestEnvironment(); @@ -28,7 +30,7 @@ suite("StatusBarService Test Suite", () => { newBranch: string | null; }>(); const branchChangedEmitter = new vscode.EventEmitter<{oldBranch: string | null; newBranch: string | null}>(); - const gitPushEmitter = new vscode.EventEmitter(); + gitPushEmitter = new vscode.EventEmitter(); // Create the GitService stub gitServiceStub = sandbox.createStubInstance(GitService); @@ -285,7 +287,7 @@ suite("StatusBarService Test Suite", () => { // Set up the stubs for the unmerged commits scenario gitServiceStub.getCurrentBranch.resolves("feature/new-feature"); gitServiceStub.getDefaultBranch.resolves("main"); - gitServiceStub.getOwnerAndRepo.resolves({ owner: "testowner", repo: "testrepo" }); + gitServiceStub.getOwnerAndRepo.resolves({owner: "testowner", repo: "testrepo"}); // Simulate unmerged commits gitServiceStub.getCommitCounts.withArgs("main", "feature/new-feature", false).resolves(2); // Unmerged commits @@ -293,14 +295,14 @@ suite("StatusBarService Test Suite", () => { // Initialize the buttons array correctly (statusBarService as any).buttons = Array(5) - .fill({}) - .map(() => ({ - text: "", - tooltip: "", - command: "", - show: sinon.stub(), - hide: sinon.stub() - })); + .fill({}) + .map(() => ({ + text: "", + tooltip: "", + command: "", + show: sinon.stub(), + hide: sinon.stub() + })); // Call updateCachedData to ensure cachedData is populated await (statusBarService as any).updateCachedData(); @@ -310,17 +312,55 @@ suite("StatusBarService Test Suite", () => { // Assertions const buttons = (statusBarService as any).buttons; // Ensure buttons are accessed correctly - console.log("Tooltip:", buttons[4].tooltip); // Log the actual tooltip for debugging - console.log("Text:", buttons[4].text); // Log the actual text for debugging + //console.log("Tooltip:", buttons[4].tooltip); // Log the actual tooltip for debugging + //console.log("Text:", buttons[4].text); // Log the actual text for debugging // Check the cached data values - console.log("Cached Data:", (statusBarService as any).cachedData); + //console.log("Cached Data:", (statusBarService as any).cachedData); assert.strictEqual( - buttons[4].tooltip, - "2 unmerged commits in testowner/testrepo/feature/new-feature compared to main\nClick to open compare view" + buttons[4].tooltip, + "2 unmerged commits in testowner/testrepo/feature/new-feature compared to main\nClick to open compare view" ); assert.strictEqual(buttons[4].text, "2 unmerged commits"); assert.ok(buttons[4].show.called, "Commit count button should be shown"); }); -}); \ No newline at end of file + + test("should refresh build status after a commit is pushed", async () => { + // Set up the stubs + gitServiceStub.getCurrentBranch.resolves("main"); + gitServiceStub.getOwnerAndRepo.resolves({owner: "testowner", repo: "testrepo"}); + gitServiceStub.detectCIType.returns("github"); + gitServiceStub.getDefaultBranch.resolves("main"); + + // Mock the buttons array + const mockButtons = Array(5) + .fill({}) + .map(() => ({ + text: "", + tooltip: "", + command: "", + show: sinon.stub(), + hide: sinon.stub() + })); + (statusBarService as any).buttons = mockButtons; + + // Spy on the updateBuildStatus method + const updateBuildStatusSpy = sinon.spy(statusBarService as any, "updateBuildStatus"); + + // Call the function to update the branch build status + await statusBarService.updateEverything(false); + + // Fire the git push event + gitPushEmitter.fire(); + + // Wait for the asynchronous operations to complete + await new Promise(resolve => setTimeout(resolve, 6000)); + + // Assertions + assert.ok( + updateBuildStatusSpy.calledWith("main", "testowner", "testrepo", "github", false), + "updateBuildStatus should be called after a commit is pushed with the correct arguments" + ); + }); +}); diff --git a/src/test/suite/statusBarUpdater.test.ts b/src/test/suite/statusBarUpdater.test.ts index 879ac43..fe37cd4 100644 --- a/src/test/suite/statusBarUpdater.test.ts +++ b/src/test/suite/statusBarUpdater.test.ts @@ -1,13 +1,13 @@ -import assert from 'assert'; -import * as sinon from 'sinon'; -import { GitService } from '../../services/gitService'; -import { StatusBarService } from '../../services/statusBarService'; -import { CIService } from '../../services/ciService'; -import { updateStatusBar, createStatusBarUpdater } from '../../utils/statusBarUpdater'; -import { setupTestEnvironment, teardownTestEnvironment } from './testSetup'; -import { Logger } from '../../utils/logger'; - -suite('StatusBarUpdater Test Suite', () => { +import assert from "assert"; +import * as sinon from "sinon"; +import {GitService} from "../../services/gitService"; +import {StatusBarService} from "../../services/statusBarService"; +import {CIService} from "../../services/ciService"; +import {updateStatusBar, createStatusBarUpdater} from "../../utils/statusBarUpdater"; +import {setupTestEnvironment, teardownTestEnvironment} from "./testSetup"; +import {Logger} from "../../utils/logger"; + +suite("StatusBarUpdater Test Suite", () => { let sandbox: sinon.SinonSandbox; let testEnv: ReturnType; let gitService: sinon.SinonStubbedInstance; @@ -17,23 +17,23 @@ suite('StatusBarUpdater Test Suite', () => { setup(() => { testEnv = setupTestEnvironment(); sandbox = testEnv.sandbox; - + gitService = sandbox.createStubInstance(GitService); statusBarService = sandbox.createStubInstance(StatusBarService); ciService = sandbox.createStubInstance(CIService); // Stub Logger to prevent actual logging during tests - sandbox.stub(Logger, 'log'); + sandbox.stub(Logger, "log"); }); teardown(() => { teardownTestEnvironment(sandbox); }); - test('updateStatusBar should update status bar', async () => { + test("updateStatusBar should update status bar", async () => { // Set up the stubs gitService.isInitialized.returns(true); - gitService.getCurrentRepo.resolves('testrepo'); + gitService.getCurrentRepo.resolves("testrepo"); // Call the function await updateStatusBar(gitService, statusBarService); @@ -44,7 +44,7 @@ suite('StatusBarUpdater Test Suite', () => { sinon.assert.calledOnce(statusBarService.updateEverything); }); - test('updateStatusBar should not update when GitService is not initialized', async () => { + test("updateStatusBar should not update when GitService is not initialized", async () => { gitService.isInitialized.returns(false); gitService.initialize.resolves(false); @@ -55,7 +55,7 @@ suite('StatusBarUpdater Test Suite', () => { sinon.assert.notCalled(statusBarService.updateEverything); }); - test('updateStatusBar should not update when no Git repository is detected', async () => { + test("updateStatusBar should not update when no Git repository is detected", async () => { gitService.isInitialized.returns(true); gitService.getCurrentRepo.resolves(null); @@ -64,20 +64,20 @@ suite('StatusBarUpdater Test Suite', () => { sinon.assert.calledOnce(gitService.getCurrentRepo); }); - test('createStatusBarUpdater should return an object with updateNow and debouncedUpdate functions', () => { + test("createStatusBarUpdater should return an object with updateNow and debouncedUpdate functions", () => { const updater = createStatusBarUpdater(gitService, statusBarService); - assert.strictEqual(typeof updater.updateNow, 'function'); - assert.strictEqual(typeof updater.debouncedUpdate, 'function'); + assert.strictEqual(typeof updater.updateNow, "function"); + assert.strictEqual(typeof updater.debouncedUpdate, "function"); }); - test('createStatusBarUpdater.updateNow should call updateStatusBar', async () => { + test("createStatusBarUpdater.updateNow should call updateStatusBar", async () => { const updater = createStatusBarUpdater(gitService, statusBarService); gitService.isInitialized.returns(true); - gitService.getCurrentRepo.resolves('testrepo'); - gitService.getCurrentBranch.resolves('main'); - gitService.getOwnerAndRepo.resolves({ owner: 'testowner', repo: 'testrepo' }); - gitService.detectCIType.returns('github'); + gitService.getCurrentRepo.resolves("testrepo"); + gitService.getCurrentBranch.resolves("main"); + gitService.getOwnerAndRepo.resolves({owner: "testowner", repo: "testrepo"}); + gitService.detectCIType.returns("github"); await updater.updateNow(); diff --git a/src/test/suite/testSetup.ts b/src/test/suite/testSetup.ts index 7e98e5e..0542164 100644 --- a/src/test/suite/testSetup.ts +++ b/src/test/suite/testSetup.ts @@ -1,6 +1,6 @@ -import * as sinon from 'sinon'; -import * as vscode from 'vscode'; -import { Logger } from '../../utils/logger'; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import {Logger} from "../../utils/logger"; let isConfigurationStubbed = false; @@ -10,31 +10,31 @@ export function setupTestEnvironment() { // Mock the vscode API const outputChannelMock = { appendLine: sandbox.stub(), - show: sandbox.stub(), + show: sandbox.stub() }; - sandbox.stub(vscode.window, 'createOutputChannel').returns(outputChannelMock as any); + sandbox.stub(vscode.window, "createOutputChannel").returns(outputChannelMock as any); // Stub vscode.workspace.getConfiguration only if it hasn't been stubbed already if (!isConfigurationStubbed) { const mockConfig = { get: sandbox.stub().returns({ - github: { token: 'mock-token', apiUrl: 'https://api.github.com' }, - gitlab: { token: 'mock-token', apiUrl: 'https://gitlab.com/api/v4' } + github: {token: "mock-token", apiUrl: "https://api.github.com"}, + gitlab: {token: "mock-token", apiUrl: "https://gitlab.com/api/v4"} }), has: sandbox.stub(), inspect: sandbox.stub(), - update: sandbox.stub().resolves(), + update: sandbox.stub().resolves() }; - sandbox.stub(vscode.workspace, 'getConfiguration').returns(mockConfig as any); + sandbox.stub(vscode.workspace, "getConfiguration").returns(mockConfig as any); isConfigurationStubbed = true; } // Initialize the Logger with a mock context - const contextMock = { subscriptions: [] } as any; + const contextMock = {subscriptions: []} as any; Logger.initialize(contextMock); - return { sandbox, outputChannelMock }; + return {sandbox, outputChannelMock}; } export function teardownTestEnvironment(sandbox: sinon.SinonSandbox) { @@ -42,4 +42,4 @@ export function teardownTestEnvironment(sandbox: sinon.SinonSandbox) { sandbox.restore(); } isConfigurationStubbed = false; -} \ No newline at end of file +} diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index 71e3655..b015722 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -1,13 +1,13 @@ export function debounce any>( - func: F, - waitFor: number + func: F, + waitFor: number ): (...args: Parameters) => void { - let timeout: NodeJS.Timeout | null = null; - + let timeout: NodeJS.Timeout | null = null; + return (...args: Parameters): void => { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), waitFor); }; -} \ No newline at end of file +} diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts index 5afd6db..d96e7af 100644 --- a/src/utils/errorHandler.ts +++ b/src/utils/errorHandler.ts @@ -1,13 +1,13 @@ import * as vscode from "vscode"; -import { Logger } from './logger'; +import {Logger} from "./logger"; export function handleError(error: any, context: string) { - Logger.log(`${context}: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`${context}: ${error instanceof Error ? error.message : String(error)}`, "ERROR"); let errorMessage = error instanceof Error ? error.message : String(error); - + if (errorMessage.includes("src refspec") && errorMessage.includes("does not match any")) { errorMessage = "Failed to push the tag. Please try again in a few seconds."; } - + vscode.window.showErrorMessage(`${context}: ${errorMessage}`); } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 05a9e2b..729b334 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,23 +1,23 @@ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; export class Logger { private static outputChannel: vscode.OutputChannel; static initialize(context: vscode.ExtensionContext) { - this.outputChannel = vscode.window.createOutputChannel('Git Tag Release Tracker', 'log'); + this.outputChannel = vscode.window.createOutputChannel("Git Tag Release Tracker", "log"); context.subscriptions.push(this.outputChannel); } - static log(message: string, level: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' = 'INFO') { + static log(message: string, level: "DEBUG" | "INFO" | "WARNING" | "ERROR" = "INFO") { if (!this.outputChannel) { - console.warn('Logger not initialized'); + console.warn("Logger not initialized"); return; } const timestamp = new Date().toISOString(); this.outputChannel.appendLine(`[${timestamp}] [${level}] ${message}`); - if (level === 'ERROR') { + if (level === "ERROR") { vscode.window.showErrorMessage(message); } } @@ -27,4 +27,4 @@ export class Logger { this.outputChannel.show(); } } -} \ No newline at end of file +} diff --git a/src/utils/statusBarUpdater.ts b/src/utils/statusBarUpdater.ts index 30d03d3..45e8803 100644 --- a/src/utils/statusBarUpdater.ts +++ b/src/utils/statusBarUpdater.ts @@ -1,8 +1,8 @@ -import { GitService } from "../services/gitService"; -import { StatusBarService } from "../services/statusBarService"; -import { handleError } from "./errorHandler"; -import { debounce } from "./debounce"; -import { Logger } from './logger'; +import {GitService} from "../services/gitService"; +import {StatusBarService} from "../services/statusBarService"; +import {handleError} from "./errorHandler"; +import {debounce} from "./debounce"; +import {Logger} from "./logger"; let lastUpdateRepo: string | null = null; @@ -12,12 +12,12 @@ export async function updateStatusBar( forceRefresh: boolean = false ) { try { - Logger.log("Updating status bar...", 'INFO'); + Logger.log("Updating status bar...", "INFO"); if (!gitService.isInitialized()) { - Logger.log("GitService is not initialized, attempting to initialize...", 'INFO'); + Logger.log("GitService is not initialized, attempting to initialize...", "INFO"); const initialized = await gitService.initialize(); if (!initialized) { - Logger.log("Failed to initialize GitService, skipping status bar update", 'ERROR'); + Logger.log("Failed to initialize GitService, skipping status bar update", "WARNING"); return; } } @@ -26,33 +26,35 @@ export async function updateStatusBar( if (currentRepo !== lastUpdateRepo) { forceRefresh = true; lastUpdateRepo = currentRepo; - Logger.log(`Repository changed to: ${currentRepo}. Forcing refresh.`, 'INFO'); + Logger.log(`Repository changed to: ${currentRepo}. Forcing refresh.`, "INFO"); + + // Clear status bar items for the previous repository + statusBarService.clearAllItems(); } // Update everything in the status bar await statusBarService.updateEverything(forceRefresh); - Logger.log("Status bar updated successfully", 'INFO'); - + Logger.log("Status bar updated successfully", "INFO"); } catch (error) { - Logger.log(`Error updating status bar: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error updating status bar: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); handleError(error, "Error updating status bar"); } } -export function createStatusBarUpdater( - gitService: GitService, - statusBarService: StatusBarService -) { +export function createStatusBarUpdater(gitService: GitService, statusBarService: StatusBarService) { const updateStatusBarCallback = async (forceRefresh: boolean = false) => { try { await updateStatusBar(gitService, statusBarService, forceRefresh); } catch (error) { - Logger.log(`Error updating status bar: ${error instanceof Error ? error.message : String(error)}`, 'ERROR'); + Logger.log(`Error updating status bar: ${error instanceof Error ? error.message : String(error)}`, "WARNING"); handleError(error, "Error updating status bar"); } }; - const debouncedUpdateStatusBar = debounce((forceRefresh: boolean = false) => updateStatusBarCallback(forceRefresh), 2000); + const debouncedUpdateStatusBar = debounce( + (forceRefresh: boolean = false) => updateStatusBarCallback(forceRefresh), + 2000 + ); return { updateNow: updateStatusBarCallback, diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md deleted file mode 100644 index f518bb8..0000000 --- a/vsc-extension-quickstart.md +++ /dev/null @@ -1,48 +0,0 @@ -# Welcome to your VS Code Extension - -## What's in the folder - -* This folder contains all of the files necessary for your extension. -* `package.json` - this is the manifest file in which you declare your extension and command. - * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. -* `src/extension.ts` - this is the main file where you will provide the implementation of your command. - * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. - * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. - -## Setup - -* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) - - -## Get up and running straight away - -* Press `F5` to open a new window with your extension loaded. -* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. -* Set breakpoints in your code inside `src/extension.ts` to debug your extension. -* Find output from your extension in the debug console. - -## Make changes - -* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. -* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - - -## Explore the API - -* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. - -## Run tests - -* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) -* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. -* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` -* See the output of the test result in the Test Results view. -* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. - * The provided test runner will only consider files matching the name pattern `**.test.ts`. - * You can create folders inside the `test` folder to structure your tests any way you want. - -## Go further - -* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). -* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. -* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).