commit 1c945795bfdf68aada08610e40875b9d6645d684 Author: Terence Carrera Date: Wed Dec 17 10:20:17 2025 +0800 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3185c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out +node_modules +.vscode-test \ No newline at end of file diff --git a/.hintrc b/.hintrc new file mode 100644 index 0000000..1f14328 --- /dev/null +++ b/.hintrc @@ -0,0 +1,8 @@ +{ + "extends": [ + "development" + ], + "hints": { + "typescript-config/consistent-casing": "off" + } +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9113471 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..77d204e --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,5 @@ +.vscode +.vscode-test +src +.gitignore +tsconfig.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a93dec2 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# WizGIT for VS Code + +![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/your-publisher.wizgit-in-vscode?style=for-the-badge&label=Marketplace) +![License](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge) + +Your intelligent coding companion for repository management, right inside Visual Studio Code. WizGIT simplifies repository creation and management with seamless API integration. + +## Features + +* **Repository Creation:** Easily create new repositories on WizGit directly from VS Code using the WizGit API. +* **Secure Authentication:** Safely authenticate using your WizGit Personal Access Token. +* **Progress Tracking:** Visual progress indication during repository creation with detailed status updates. +* **Error Handling:** Comprehensive error reporting for troubleshooting API issues. + +## Requirements + +* Visual Studio Code v1.74.0 or newer. +* A valid WizGit account with API access. +* Personal Access Token from your WizGit instance. + +## Usage + +1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`) +2. Run the command: `WizGIT: Create Repository` +3. Follow the prompts to enter: + - Your WizGit API endpoint (e.g., `https://wizgit.com/api/v1`) + - Your Personal Access Token + - Repository name + - Repository description (optional) + +The extension will create the repository and notify you of the result. + +## Commands + +* `WizGIT: Create Repository` - Creates a new repository on your WizGit instance + +## Extension Settings + +Currently, this extension does not contribute any settings. All configuration is done through the interactive prompts when creating a repository. + +## Known Issues + +* Network connectivity issues may cause repository creation to fail. +* Please report any bugs or feature requests on our [GitHub Issues](https://github.com/your-repo/wizgit-in-vscode/issues) page. + +## Release Notes + +### 0.0.1 + +Initial release of WizGIT. +* Core feature: Create repositories via WizGit API. +* Interactive prompts for API configuration. +* Progress tracking and error handling. + +--- + +**Enjoy streamlined repository management with WizGIT!** \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ea1b0fa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,142 @@ +{ + "name": "wizgit-in-vscode", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wizgit-in-vscode", + "version": "0.0.1", + "dependencies": { + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "engines": { + "vscode": "^1.74.0" + } + }, + "node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.107.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.107.0.tgz", + "integrity": "sha512-XS8YE1jlyTIowP64+HoN30OlC1H9xqSlq1eoLZUgFEC8oUTO6euYZxti1xRiLSfZocs4qytTzR6xCBYtioQTCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d8cd6c --- /dev/null +++ b/package.json @@ -0,0 +1,202 @@ +{ + "name": "wizgit-in-vscode", + "displayName": "WizGIT in VS Code", + "description": "A VS Code extension to interact with WizGIT API for repository management.", + "version": "0.0.1", + "engines": { + "vscode": "^1.74.0" + }, + "categories": [ + "SCM Providers", + "Other" + ], + "activationEvents": [ + "onStartupFinished", + "workspaceContains:.git" + ], + "main": "./out/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "wizgit", + "title": "WizGIT", + "icon": { + "light": "./resources/wizgit-logo.svg", + "dark": "./resources/wizgit-logo-dark.svg" + } + } + ] + }, + "views": { + "wizgit": [ + { + "id": "wizgitRepositories", + "name": "Repositories", + "when": "wizgit:enabled" + }, + { + "id": "wizgitIssues", + "name": "Issues", + "when": "wizgit:enabled" + }, + { + "id": "wizgitPullRequests", + "name": "Pull Requests", + "when": "wizgit:enabled" + } + ] + }, + "commands": [ + { + "command": "wizgit-in-vscode.configure", + "title": "Configure WizGIT", + "category": "WizGIT", + "icon": "$(gear)" + }, + { + "command": "wizgit-in-vscode.createRepository", + "title": "Create Repository", + "category": "WizGIT", + "icon": "$(repo-create)" + }, + { + "command": "wizgit-in-vscode.createIssue", + "title": "Create Issue", + "category": "WizGIT", + "icon": "$(issues)" + }, + { + "command": "wizgit-in-vscode.createPullRequest", + "title": "Create Pull Request", + "category": "WizGIT", + "icon": "$(git-pull-request)" + }, + { + "command": "wizgit-in-vscode.clearConfig", + "title": "Clear Configuration", + "category": "WizGIT", + "icon": "$(clear-all)" + }, + { + "command": "wizgit-in-vscode.refreshRepositories", + "title": "Refresh", + "category": "WizGIT", + "icon": "$(refresh)" + }, + { + "command": "wizgit-in-vscode.cloneRepository", + "title": "Clone Repository", + "category": "WizGIT", + "icon": "$(repo-clone)" + }, + { + "command": "wizgit-in-vscode.openIssue", + "title": "Open Issue", + "category": "WizGIT", + "icon": "$(link-external)" + }, + { + "command": "wizgit-in-vscode.openPullRequest", + "title": "Open Pull Request", + "category": "WizGIT", + "icon": "$(link-external)" + } + ], + "menus": { + "view/title": [ + { + "command": "wizgit-in-vscode.refreshRepositories", + "when": "view == wizgitRepositories", + "group": "navigation" + }, + { + "command": "wizgit-in-vscode.createRepository", + "when": "view == wizgitRepositories", + "group": "navigation" + }, + { + "command": "wizgit-in-vscode.createIssue", + "when": "view == wizgitIssues", + "group": "navigation" + }, + { + "command": "wizgit-in-vscode.createPullRequest", + "when": "view == wizgitPullRequests", + "group": "navigation" + } + ], + "explorer/context": [ + { + "command": "wizgit-in-vscode.createIssue", + "when": "explorerResourceIsFolder && wizgit:enabled", + "group": "7_modification" + } + ], + "editor/context": [ + { + "command": "wizgit-in-vscode.createIssue", + "when": "wizgit:enabled", + "group": "9_cutcopypaste" + } + ], + "commandPalette": [ + { + "command": "wizgit-in-vscode.refreshRepositories", + "when": "false" + }, + { + "command": "wizgit-in-vscode.openIssue", + "when": "false" + }, + { + "command": "wizgit-in-vscode.openPullRequest", + "when": "false" + } + ] + }, + "configuration": { + "title": "WizGIT", + "properties": { + "wizgit.apiEndpoint": { + "type": "string", + "description": "WizGIT API endpoint URL", + "default": "" + }, + "wizgit.autoDetect": { + "type": "boolean", + "description": "Automatically detect WizGIT repositories in workspace", + "default": true + }, + "wizgit.statusBar.enabled": { + "type": "boolean", + "description": "Show WizGIT information in status bar", + "default": true + }, + "wizgit.notifications.enabled": { + "type": "boolean", + "description": "Enable WizGIT notifications", + "default": true + }, + "wizgit.defaultBranch": { + "type": "string", + "description": "Default branch name for new repositories", + "default": "main" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/vscode": "^1.74.0", + "typescript": "^4.9.4" + }, + "dependencies": { + "node-fetch": "^3.3.2" + } +} \ No newline at end of file diff --git a/resources/wizgit-logo-dark.svg b/resources/wizgit-logo-dark.svg new file mode 100644 index 0000000..477bd15 --- /dev/null +++ b/resources/wizgit-logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/wizgit-logo.svg b/resources/wizgit-logo.svg new file mode 100644 index 0000000..477bd15 --- /dev/null +++ b/resources/wizgit-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..4e0b1aa --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,3024 @@ +import * as vscode from 'vscode'; +import fetch from 'node-fetch'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as child_process from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(child_process.exec); + +// Interfaces for API responses +interface WizGitRepoData { + id: number; + name: string; + full_name: string; + description: string; + private: boolean; + clone_url: string; + html_url: string; + default_branch: string; + open_issues_count: number; + language: string; + updated_at: string; +} + +interface WizGitIssueData { + id: number; + number: number; + title: string; + body: string; + state: 'open' | 'closed'; + user: { + login: string; + avatar_url: string; + }; + assignees: Array<{ + login: string; + avatar_url: string; + }>; + labels: Array<{ + name: string; + color: string; + }>; + created_at: string; + updated_at: string; + html_url: string; +} + +interface WizGitPRData { + id: number; + number: number; + title: string; + body: string; + state: 'open' | 'closed' | 'merged'; + head: { + ref: string; + sha: string; + }; + base: { + ref: string; + sha: string; + }; + user: { + login: string; + avatar_url: string; + }; + created_at: string; + updated_at: string; + html_url: string; + mergeable: boolean; + draft: boolean; +} + +interface WizGitCommentData { + id: number; + user: { + login: string; + avatar_url: string; + }; + body: string; + created_at: string; + updated_at: string; + html_url: string; +} + +interface WizGitCommentData { + id: number; + user: { + login: string; + avatar_url: string; + }; + body: string; + created_at: string; + updated_at: string; + html_url: string; +} + +// Tree Data Providers +class WizGitRepositoryProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(private context: vscode.ExtensionContext) { } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: WizGitRepository | WizGitRepoItem): vscode.TreeItem { + return element; + } + + async getChildren(element?: WizGitRepository | WizGitRepoItem): Promise<(WizGitRepository | WizGitRepoItem)[]> { + try { + const credentials = await getStoredCredentials(this.context); + if (!credentials) { + return [new WizGitRepoItem('No credentials', 'Configure WizGIT credentials first', 'configure')]; + } + + if (!element) { + // Return root level repositories + return this.getRepositories(credentials); + } else if (element instanceof WizGitRepository) { + // Return repository details (branches, issues, PRs) + return this.getRepositoryDetails(element, credentials); + } + } catch (error) { + console.error('Error fetching repositories:', error); + return [new WizGitRepoItem('Error', 'Failed to fetch repositories', 'error')]; + } + return []; + } + + private async getRepositories(credentials: { apiEndpoint: string, token: string }): Promise { + try { + const localRepos = await this.getLocalWizGitRepositories(credentials); + return localRepos; + } catch (error) { + console.error('Error fetching repositories:', error); + throw error; + } + } + + private async getLocalWizGitRepositories(credentials: { apiEndpoint: string, token: string }): Promise { + if (!vscode.workspace.workspaceFolders) { + return []; + } + + const wizgitRepos: WizGitRepository[] = []; + const apiUrl = new URL(credentials.apiEndpoint); + const wizgitDomain = apiUrl.hostname; + + for (const folder of vscode.workspace.workspaceFolders) { + const gitDir = path.join(folder.uri.fsPath, '.git'); + + try { + if (fs.existsSync(gitDir)) { + // Get git remote URL + const { stdout: remoteUrl } = await execAsync('git remote get-url origin', { + cwd: folder.uri.fsPath + }); + + // Get current branch + const { stdout: branch } = await execAsync('git branch --show-current', { + cwd: folder.uri.fsPath + }); + + const trimmedRemote = remoteUrl.trim(); + const trimmedBranch = branch.trim() || 'main'; + + // Check if remote URL matches WizGIT domain + if (this.isWizGitRepository(trimmedRemote, wizgitDomain)) { + const repoPath = this.extractRepoPath(trimmedRemote); + if (repoPath) { + // Fetch repository details from API + const repoData = await this.fetchRepositoryDetails(repoPath, credentials); + if (repoData) { + wizgitRepos.push(new WizGitRepository( + repoData.name, + `${repoData.description || 'Local workspace repository'} (${folder.name})`, + repoData, + vscode.TreeItemCollapsibleState.Collapsed + )); + } else { + // Create a basic repository object if API fetch fails + const basicRepoData: WizGitRepoData = { + id: 0, + name: folder.name, + full_name: repoPath, + description: 'Local workspace repository', + private: false, + clone_url: trimmedRemote, + html_url: this.getWebUrl(trimmedRemote), + default_branch: trimmedBranch, + open_issues_count: 0, + language: 'Unknown', + updated_at: new Date().toISOString() + }; + + wizgitRepos.push(new WizGitRepository( + folder.name, + `Local workspace repository (${folder.name})`, + basicRepoData, + vscode.TreeItemCollapsibleState.Collapsed + )); + } + } + } + } + } catch (error) { + console.log(`Could not process repository ${folder.name}:`, error); + // Continue with other repositories + } + } + + return wizgitRepos; + } + + private isWizGitRepository(remoteUrl: string, wizgitDomain: string): boolean { + try { + // Handle different URL formats + if (remoteUrl.startsWith('http')) { + const url = new URL(remoteUrl); + return url.hostname === wizgitDomain; + } else if (remoteUrl.startsWith('git@')) { + // Handle SSH format: git@domain.com:user/repo.git + const match = remoteUrl.match(/git@([^:]+):/); + return match ? match[1] === wizgitDomain : false; + } + } catch (error) { + console.log('Error parsing remote URL:', error); + } + return false; + } + + private extractRepoPath(remoteUrl: string): string | null { + // Extract owner/repo from various git URL formats + const patterns = [ + /https?:\/\/[^/]+\/(.+?\/[^/]+?)(?:\.git)?\/?$/, + /git@[^:]+:(.+?\/[^/]+?)(?:\.git)?$/ + ]; + + for (const pattern of patterns) { + const match = remoteUrl.match(pattern); + if (match) { + return match[1]; + } + } + return null; + } + + private getWebUrl(remoteUrl: string): string { + // Convert git URL to web URL + if (remoteUrl.startsWith('http')) { + return remoteUrl.replace(/\.git$/, ''); + } else if (remoteUrl.startsWith('git@')) { + // Convert git@domain.com:user/repo.git to https://domain.com/user/repo + const match = remoteUrl.match(/git@([^:]+):(.+?)(?:\.git)?$/); + if (match) { + return `https://${match[1]}/${match[2]}`; + } + } + return remoteUrl; + } + + private async fetchRepositoryDetails(repoPath: string, credentials: { apiEndpoint: string, token: string }): Promise { + try { + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + if (response.ok) { + return await response.json() as WizGitRepoData; + } + } catch (error) { + console.log('Could not fetch repository details:', error); + } + return null; + } + + private async getRepositoryDetails(repo: WizGitRepository, credentials: { apiEndpoint: string, token: string }): Promise { + const items: WizGitRepoItem[] = []; + + try { + // Get local repository information + const workspaceFolder = vscode.workspace.workspaceFolders?.find(folder => + folder.name === repo.name || repo.repoData.full_name.endsWith(folder.name) + ); + + if (workspaceFolder) { + try { + // Get current branch + const { stdout: currentBranch } = await execAsync('git branch --show-current', { + cwd: workspaceFolder.uri.fsPath + }); + + // Get git status + const { stdout: gitStatus } = await execAsync('git status --porcelain', { + cwd: workspaceFolder.uri.fsPath + }); + + const hasChanges = gitStatus.trim().length > 0; + const branch = currentBranch.trim() || repo.repoData.default_branch; + + items.push( + new WizGitRepoItem( + `Current Branch: ${branch}`, + hasChanges ? 'Working directory has changes' : 'Working directory clean', + 'current-branch', + new vscode.ThemeIcon('git-branch', hasChanges ? new vscode.ThemeColor('gitDecoration.modifiedResourceForeground') : undefined) + ), + new WizGitRepoItem( + `Workspace: ${workspaceFolder.name}`, + `Local path: ${workspaceFolder.uri.fsPath}`, + 'workspace', + new vscode.ThemeIcon('folder') + ) + ); + } catch (gitError) { + console.log('Could not get git information:', gitError); + } + } + + // Get issues count from API + const issuesResponse = await fetch(`${credentials.apiEndpoint}/repos/${repo.repoData.full_name}/issues?state=open`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + const issues = issuesResponse.ok ? await issuesResponse.json() as (WizGitIssueData & { pull_request?: any })[] : []; + const openIssues = issues.filter(issue => !issue.pull_request).length; + + // Get PRs count from API + const prsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repo.repoData.full_name}/pulls?state=open`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + const prs = prsResponse.ok ? await prsResponse.json() as WizGitPRData[] : []; + + items.push( + new WizGitRepoItem( + `Issues (${openIssues})`, + `${openIssues} open issues`, + 'issues', + new vscode.ThemeIcon('issues') + ), + new WizGitRepoItem( + `Pull Requests (${prs.length})`, + `${prs.length} open pull requests`, + 'pullrequests', + new vscode.ThemeIcon('git-pull-request') + ), + new WizGitRepoItem( + `Default Branch: ${repo.repoData.default_branch}`, + 'Default branch on remote', + 'default-branch', + new vscode.ThemeIcon('git-branch') + ) + ); + } catch (error) { + console.error('Error fetching repository details:', error); + items.push(new WizGitRepoItem('Error', 'Failed to fetch details', 'error')); + } + + return items; + } +} + +class WizGitRepository extends vscode.TreeItem { + constructor( + public readonly name: string, + public readonly description: string, + public readonly repoData: WizGitRepoData, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(name, collapsibleState); + this.tooltip = `${description}\n\nLanguage: ${repoData.language || 'Unknown'}\nOpen Issues: ${repoData.open_issues_count}\nPrivate: ${repoData.private ? 'Yes' : 'No'}\nUpdated: ${new Date(repoData.updated_at).toLocaleDateString()}`; + this.contextValue = 'repository'; + this.iconPath = new vscode.ThemeIcon(repoData.private ? 'lock' : 'repo'); + this.command = { + command: 'vscode.open', + title: 'Open Repository', + arguments: [vscode.Uri.parse(repoData.html_url)] + }; + } +} + +class WizGitRepoItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly description: string, + public readonly contextValue: string, + public readonly iconPath?: vscode.ThemeIcon + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this.tooltip = description; + } +} + +// Issue Provider +class WizGitIssueProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(private context: vscode.ExtensionContext) { } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: WizGitIssue): vscode.TreeItem { + return element; + } + + async getChildren(): Promise { + try { + const credentials = await getStoredCredentials(this.context); + if (!credentials) { + return []; + } + + const repos = await detectWizGitRepositories(this.context); + if (repos.length === 0) { + return []; + } + + // Get issues from the current repository + const currentRepo = repos[0]; // Use first detected repo + const repoPath = this.extractRepoPath(currentRepo.remote); + + if (!repoPath) return []; + + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues?state=open`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + return []; + } + + const issues: (WizGitIssueData & { pull_request?: any })[] = await response.json() as (WizGitIssueData & { pull_request?: any })[]; + return issues + .filter(issue => !issue.pull_request) // Filter out PRs + .map(issue => { + const wizgitIssue = new WizGitIssue(issue); + (wizgitIssue as any).repoPath = repoPath; + return wizgitIssue; + }); + } catch (error) { + console.error('Error fetching issues:', error); + return []; + } + } + + private extractRepoPath(remoteUrl: string): string | null { + // Extract owner/repo from various git URL formats + const patterns = [ + /https?:\/\/[^/]+\/(.+?\/[^/]+?)(?:\.git)?\/?$/, + /git@[^:]+:(.+?\/[^/]+?)(?:\.git)?$/ + ]; + + for (const pattern of patterns) { + const match = remoteUrl.match(pattern); + if (match) { + return match[1]; + } + } + return null; + } +} + +class WizGitIssue extends vscode.TreeItem { + constructor(public readonly issueData: WizGitIssueData) { + super(`#${issueData.number} ${issueData.title}`, vscode.TreeItemCollapsibleState.None); + + this.tooltip = `Issue #${issueData.number}\n${issueData.title}\n\nAuthor: ${issueData.user.login}\nState: ${issueData.state}\nCreated: ${new Date(issueData.created_at).toLocaleDateString()}`; + this.contextValue = 'issue'; + + // Set icon based on state + this.iconPath = new vscode.ThemeIcon( + issueData.state === 'open' ? 'issue-opened' : 'issue-closed', + issueData.state === 'open' ? new vscode.ThemeColor('issues.openForeground') : new vscode.ThemeColor('issues.closedForeground') + ); + + // Command to open issue in VS Code viewer + this.command = { + command: 'wizgit-in-vscode.openIssue', + title: 'Open Issue', + arguments: [issueData, this.getRepoPath()] + }; + + // Add assignee info to description + if (issueData.assignees && issueData.assignees.length > 0) { + this.description = `@${issueData.assignees[0].login}`; + } + } + + private getRepoPath(): string { + // This will be set by the provider when creating the issue + return (this as any).repoPath || ''; + } +} + +// Pull Request Provider +class WizGitPRProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(private context: vscode.ExtensionContext) { } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: WizGitPR): vscode.TreeItem { + return element; + } + + async getChildren(): Promise { + try { + const credentials = await getStoredCredentials(this.context); + if (!credentials) { + return []; + } + + const repos = await detectWizGitRepositories(this.context); + if (repos.length === 0) { + return []; + } + + const currentRepo = repos[0]; + const repoPath = this.extractRepoPath(currentRepo.remote); + + if (!repoPath) return []; + + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls?state=open`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + return []; + } + + const prs: WizGitPRData[] = await response.json() as WizGitPRData[]; + return prs.map(pr => { + const wizgitPR = new WizGitPR(pr); + (wizgitPR as any).repoPath = repoPath; + return wizgitPR; + }); + } catch (error) { + console.error('Error fetching pull requests:', error); + return []; + } + } + + private extractRepoPath(remoteUrl: string): string | null { + const patterns = [ + /https?:\/\/[^/]+\/(.+?\/[^/]+?)(?:\.git)?\/?$/, + /git@[^:]+:(.+?\/[^/]+?)(?:\.git)?$/ + ]; + + for (const pattern of patterns) { + const match = remoteUrl.match(pattern); + if (match) { + return match[1]; + } + } + return null; + } +} + +class WizGitPR extends vscode.TreeItem { + constructor(public readonly prData: WizGitPRData) { + super(`#${prData.number} ${prData.title}`, vscode.TreeItemCollapsibleState.None); + + this.tooltip = `Pull Request #${prData.number}\n${prData.title}\n\nAuthor: ${prData.user.login}\nState: ${prData.state}\nBranch: ${prData.head.ref} → ${prData.base.ref}\nCreated: ${new Date(prData.created_at).toLocaleDateString()}`; + this.contextValue = 'pullrequest'; + + // Set icon based on state and draft status + let iconId = 'git-pull-request'; + let color: vscode.ThemeColor | undefined; + + if (prData.draft) { + iconId = 'git-pull-request-draft'; + } else if (prData.state === 'merged') { + iconId = 'git-merge'; + color = new vscode.ThemeColor('gitDecoration.addedResourceForeground'); + } else if (prData.state === 'closed') { + iconId = 'git-pull-request-closed'; + color = new vscode.ThemeColor('gitDecoration.deletedResourceForeground'); + } + + this.iconPath = new vscode.ThemeIcon(iconId, color); + + // Command to open PR in VS Code viewer + this.command = { + command: 'wizgit-in-vscode.openPullRequest', + title: 'Open Pull Request', + arguments: [prData, this.getRepoPath()] + }; + + // Show branch info in description + this.description = `${prData.head.ref} → ${prData.base.ref}`; + } + + private getRepoPath(): string { + // This will be set by the provider when creating the PR + return (this as any).repoPath || ''; + } +} + +// Status Bar Manager +class WizGitStatusBar { + private statusBarItem: vscode.StatusBarItem; + + constructor() { + this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + this.statusBarItem.command = 'wizgit-in-vscode.configure'; + this.update(); + } + + update(repositoryName?: string, branch?: string): void { + if (repositoryName && branch) { + this.statusBarItem.text = `$(git-branch) ${repositoryName}:${branch}`; + this.statusBarItem.tooltip = `WizGIT Repository: ${repositoryName} (${branch})`; + } else { + this.statusBarItem.text = `$(git-branch) WizGIT`; + this.statusBarItem.tooltip = 'Click to configure WizGIT'; + } + this.statusBarItem.show(); + } + + dispose(): void { + this.statusBarItem.dispose(); + } +} + +export function activate(context: vscode.ExtensionContext) { + console.log('WizGIT in VS Code Extension is Now Active! 🚀'); + + // Initialize tree data providers + const repositoryProvider = new WizGitRepositoryProvider(context); + const issueProvider = new WizGitIssueProvider(context); + const prProvider = new WizGitPRProvider(context); + + vscode.window.createTreeView('wizgitRepositories', { + treeDataProvider: repositoryProvider, + showCollapseAll: true + }); + + vscode.window.createTreeView('wizgitIssues', { + treeDataProvider: issueProvider, + showCollapseAll: false + }); + + vscode.window.createTreeView('wizgitPullRequests', { + treeDataProvider: prProvider, + showCollapseAll: false + }); + + // Initialize status bar + const statusBar = new WizGitStatusBar(); + context.subscriptions.push(statusBar); + + // Set context for when clauses + vscode.commands.executeCommand('setContext', 'wizgit:enabled', true); + + // Listen for workspace changes + const workspaceWatcher = vscode.workspace.onDidChangeWorkspaceFolders(async () => { + const repos = await detectWizGitRepositories(context); + if (repos.length > 0) { + statusBar.update(repos[0].name, repos[0].branch); + repositoryProvider.refresh(); + issueProvider.refresh(); + prProvider.refresh(); + } else { + statusBar.update(); + } + }); + context.subscriptions.push(workspaceWatcher); + + // Initial detection + detectWizGitRepositories(context).then(repos => { + if (repos.length > 0) { + statusBar.update(repos[0].name, repos[0].branch); + } + }).catch(error => { + console.error('Initial repository detection failed:', error); + }); + + // Register commands + let configureDisposable = vscode.commands.registerCommand('wizgit-in-vscode.configure', () => { + configureWizGitCredentials(context); + }); + + let refreshDisposable = vscode.commands.registerCommand('wizgit-in-vscode.refreshRepositories', () => { + repositoryProvider.refresh(); + issueProvider.refresh(); + prProvider.refresh(); + vscode.window.showInformationMessage('WizGIT data refreshed!'); + }); + + let cloneDisposable = vscode.commands.registerCommand('wizgit-in-vscode.cloneRepository', async () => { + await cloneWizGitRepository(context); + }); + + let openIssueDisposable = vscode.commands.registerCommand('wizgit-in-vscode.openIssue', async (issueData: WizGitIssueData, repoPath: string) => { + await openIssueViewer(context, issueData, repoPath); + }); + + let openPRDisposable = vscode.commands.registerCommand('wizgit-in-vscode.openPullRequest', async (prData: WizGitPRData, repoPath: string) => { + await openPullRequestViewer(context, prData, repoPath); + }); + + let disposable = vscode.commands.registerCommand('wizgit-in-vscode.createRepository', async () => { + /** + * Creates a new repository on WizGit (self-hosted Gitea) using the user's API endpoint and personal access token. + * Follows Gitea's API authentication using 'Authorization: token ' header format. + */ + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Creating WizGit repository...", + cancellable: false + }, async (progress) => { + try { + // 1. Get credentials (stored or prompt user) + const credentials = await getCredentialsWithFallback(context); + if (!credentials) { + vscode.window.showErrorMessage('WizGIT credentials are required.'); + return; + } + + const { apiEndpoint, token: pat } = credentials; + + const repoName = await vscode.window.showInputBox({ + prompt: 'Enter the name for the new repository', + ignoreFocusOut: true, + }); + if (!repoName) { + vscode.window.showErrorMessage('Repository name is required.'); + return; + } + + const repoDescription = await vscode.window.showInputBox({ + prompt: 'Enter an optional description for the repository', + ignoreFocusOut: true, + }) || ''; // Default to empty string if no description is provided + + progress.report({ increment: 25, message: "Sending request..." }); + + // 2. Make the API call to create the repository using Gitea API + // Using Gitea's repository creation endpoint: /user/repos + const response = await fetch(`${apiEndpoint}/user/repos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${pat}` + }, + body: JSON.stringify({ + name: repoName, + description: repoDescription, + private: false, // Default to public, can be made configurable + auto_init: true // Initialize with README + }) + }); + + progress.report({ increment: 50, message: "Processing Response..." }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`API Request Failed with Status ${response.status}: ${errorBody}`); + } + + const responseData = await response.json() as { + name: string; + full_name: string; + html_url: string; + clone_url: string; + }; + + progress.report({ increment: 100, message: "Done!" }); + + // 3. Show success message with repository URL + const message = `Successfully created repository: ${responseData.name}`; + const action = await vscode.window.showInformationMessage( + message, + 'Open in Browser', + 'Copy Clone URL', + 'Open Repository in VS Code' + ); + + if (action === 'Open in Browser' && responseData.html_url) { + vscode.env.openExternal(vscode.Uri.parse(responseData.html_url)); + } else if (action === 'Copy Clone URL' && responseData.clone_url) { + vscode.env.clipboard.writeText(responseData.clone_url); + vscode.window.showInformationMessage('Clone URL copied to clipboard!'); + } else if (action === 'Open Repository in VS Code' && responseData.clone_url) { + vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.parse(responseData.clone_url), true); + } + + } catch (error) { + // 4. Show error message + if (error instanceof Error) { + vscode.window.showErrorMessage(`Failed to create repository: ${error.message}`); + } else { + vscode.window.showErrorMessage(`An unknown error occurred while creating the repository.`); + } + } + }); + }); + + // Register Create Issue command + let createIssueDisposable = vscode.commands.registerCommand('wizgit-in-vscode.createIssue', () => { + createIssueWebview(context); + }); + + // Register Create Pull Request command + let createPRDisposable = vscode.commands.registerCommand('wizgit-in-vscode.createPullRequest', () => { + createPullRequestWebview(context); + }); + + // Register Clear WizGIT Configuration command + let clearConfigDisposable = vscode.commands.registerCommand('wizgit-in-vscode.clearConfig', async () => { + await clearWizGitCredentials(context); + vscode.window.showInformationMessage('WizGIT credentials cleared successfully.'); + }); + + context.subscriptions.push( + disposable, + createIssueDisposable, + createPRDisposable, + configureDisposable, + clearConfigDisposable, + refreshDisposable, + cloneDisposable, + openIssueDisposable, + openPRDisposable + ); +} + +// Credential management functions +async function getStoredCredentials(context: vscode.ExtensionContext): Promise<{ apiEndpoint: string, token: string } | null> { + const apiEndpoint = await context.secrets.get('wizgit.apiEndpoint'); + const token = await context.secrets.get('wizgit.token'); + + if (apiEndpoint && token) { + return { apiEndpoint, token }; + } + return null; +} + +async function storeCredentials(context: vscode.ExtensionContext, apiEndpoint: string, token: string): Promise { + await context.secrets.store('wizgit.apiEndpoint', apiEndpoint); + await context.secrets.store('wizgit.token', token); +} + +async function clearWizGitCredentials(context: vscode.ExtensionContext): Promise { + await context.secrets.delete('wizgit.apiEndpoint'); + await context.secrets.delete('wizgit.token'); +} + +async function configureWizGitCredentials(context: vscode.ExtensionContext): Promise { + try { + const apiEndpoint = await vscode.window.showInputBox({ + prompt: 'Enter your WizGIT API endpoint (e.g., https://wizgit.your.host/api/v1)', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value) return 'API endpoint is required'; + try { + new URL(value); + return null; + } catch { + return 'Please enter a valid URL'; + } + } + }); + + if (!apiEndpoint) { + vscode.window.showErrorMessage('Configuration cancelled: API endpoint is required.'); + return; + } + + const token = await vscode.window.showInputBox({ + prompt: 'Enter your WizGIT Personal Access Token', + password: true, + ignoreFocusOut: true, + validateInput: (value) => { + if (!value) return 'Personal Access Token is required'; + if (value.length < 10) return 'Token appears to be too short'; + return null; + } + }); + + if (!token) { + vscode.window.showErrorMessage('Configuration cancelled: Personal Access Token is required.'); + return; + } + + // Test the credentials by making a simple API call + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Verifying WizGIT credentials...", + cancellable: false + }, async (progress) => { + try { + progress.report({ increment: 50, message: "Testing API connection..." }); + + const response = await fetch(`${apiEndpoint}/user`, { + headers: { + 'Authorization': `token ${token}` + } + }); + + if (!response.ok) { + throw new Error(`Authentication failed: ${response.status} ${response.statusText}`); + } + + const userData = await response.json() as { login: string, email?: string }; + + progress.report({ increment: 100, message: "Credentials verified!" }); + + // Store the credentials + await storeCredentials(context, apiEndpoint, token); + + vscode.window.showInformationMessage( + `WizGIT credentials configured successfully for user: ${userData.login}`, + 'OK' + ); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + vscode.window.showErrorMessage(`Failed to verify credentials: ${errorMessage}`); + } + }); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + vscode.window.showErrorMessage(`Configuration failed: ${errorMessage}`); + } +} + +async function getCredentialsWithFallback(context: vscode.ExtensionContext): Promise<{ apiEndpoint: string, token: string } | null> { + // Try to get stored credentials first + const stored = await getStoredCredentials(context); + if (stored) { + return stored; + } + + // If no stored credentials, prompt user to configure them + const configure = await vscode.window.showInformationMessage( + 'No WizGIT credentials found. Would you like to configure them now?', + 'Configure Now', + 'Enter Manually' + ); + + if (configure === 'Configure Now') { + await configureWizGitCredentials(context); + // Try to get stored credentials again after configuration + return await getStoredCredentials(context); + } else if (configure === 'Enter Manually') { + // Fallback to manual entry for this session only + const apiEndpoint = await vscode.window.showInputBox({ + prompt: 'Enter your WizGIT API endpoint (e.g., https://wizgit.your.host/api/v1)', + ignoreFocusOut: true, + }); + if (!apiEndpoint) return null; + + const token = await vscode.window.showInputBox({ + prompt: 'Enter your WizGIT Personal Access Token', + password: true, + ignoreFocusOut: true, + }); + if (!token) return null; + + return { apiEndpoint, token }; + } + + return null; +} + +async function createIssueWebview(context: vscode.ExtensionContext) { + const panel = vscode.window.createWebviewPanel( + 'wizgitCreateIssue', + 'WizGIT: Create Issue', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + // Get stored credentials, workspace info, and branches to pre-populate the form + const [credentials, repoInfo, branches] = await Promise.all([ + getStoredCredentials(context), + getWorkspaceRepoInfo(), + getAvailableBranches() + ]); + panel.webview.html = getIssueWebviewContent(credentials, repoInfo, branches); + + panel.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case 'createIssue': + await handleCreateIssue(message.data, panel, context); + break; + case 'configure': + await configureWizGitCredentials(context); + // Refresh the webview with updated credentials and workspace info + const [updatedCredentials, updatedRepoInfo, updatedBranches] = await Promise.all([ + getStoredCredentials(context), + getWorkspaceRepoInfo(), + getAvailableBranches() + ]); + panel.webview.html = getIssueWebviewContent(updatedCredentials, updatedRepoInfo, updatedBranches); + break; + } + }, undefined, context.subscriptions); +} + +async function createPullRequestWebview(context: vscode.ExtensionContext) { + const panel = vscode.window.createWebviewPanel( + 'wizgitCreatePR', + 'WizGIT: Create Pull Request', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + // Get stored credentials, workspace info, and branches to pre-populate the form + const [credentials, repoInfo, branches] = await Promise.all([ + getStoredCredentials(context), + getWorkspaceRepoInfo(), + getAvailableBranches() + ]); + panel.webview.html = getPullRequestWebviewContent(credentials, repoInfo, branches); + + panel.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case 'createPullRequest': + await handleCreatePullRequest(message.data, panel, context); + break; + case 'configure': + await configureWizGitCredentials(context); + // Refresh the webview with updated credentials and workspace info + const [updatedCredentials, updatedRepoInfo, updatedBranches] = await Promise.all([ + getStoredCredentials(context), + getWorkspaceRepoInfo(), + getAvailableBranches() + ]); + panel.webview.html = getPullRequestWebviewContent(updatedCredentials, updatedRepoInfo, updatedBranches); + break; + } + }, undefined, context.subscriptions); +} + +function getIssueWebviewContent(credentials?: { apiEndpoint: string, token: string } | null, repoInfo?: { owner: string, repo: string, currentBranch: string } | null, branches?: { local: string[], remote: string[], current: string }): string { + return ` + + + + + New Issue - WizGIT + + + + +
+
+
+ + + +

New Issue

+
+
+
+ +
+ ${!credentials ? ` +
+
+ + + +
+

Authentication Required

+

Please configure your WizGIT credentials to continue.

+
+
+
+ +
+
+ ` : ''} + +
+
+ +
+
+
+ + + +

Issue Information

+
+ +
+
+ + +
+ +
+ + +
+ Supports Markdown formatting +
+
+
+
+
+ + +
+
+

+ + + + Repository +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ + + +`; +} + +function getPullRequestWebviewContent(credentials?: { apiEndpoint: string, token: string } | null, repoInfo?: { owner: string, repo: string, currentBranch: string } | null, branches?: { local: string[], remote: string[], current: string }): string { + return ` + + + + + New Pull Request - WizGIT + + + + +
+
+
+ + + +

New Pull Request

+
+
+
+ +
+ ${!credentials ? ` +
+
+ + + +
+

Authentication Required

+

Please configure your WizGIT credentials to continue.

+
+
+
+ +
+
+ ` : ''} + +
+
+ +
+ +
+
+ + + +

Comparing Changes

+
+ +
+
+
+
+ + + + + +
+
+
+ + + +
+
+
+ + + + + +
+
+
+
+ +
+
+ + +
+ +
+ + +
+ Supports Markdown formatting +
+
+
+
+
+ + +
+
+

+ + + + Repository +

+ +
+
+ + +
+ +
+ + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ + + +`; +} + +async function handleCreateIssue(data: any, panel: vscode.WebviewPanel, context: vscode.ExtensionContext) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) { + throw new Error('No WizGIT credentials found. Please configure them first.'); + } + + const response = await fetch(`${credentials.apiEndpoint}/repos/${data.owner}/${data.repo}/issues`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${credentials.token}` + }, + body: JSON.stringify({ + title: data.title, + body: data.body + }) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`API request failed with status ${response.status}: ${errorBody}`); + } + + const responseData = await response.json() as { + title: string; + number: number; + html_url: string; + }; + + // Update webview with success message + panel.webview.postMessage({ + command: 'success', + message: `Successfully created issue #${responseData.number}: ${responseData.title}`, + url: responseData.html_url + }); + + // Show VS Code notification + const action = await vscode.window.showInformationMessage( + `Successfully created issue #${responseData.number}: ${responseData.title}`, + 'Open in Browser' + ); + + if (action === 'Open in Browser' && responseData.html_url) { + vscode.env.openExternal(vscode.Uri.parse(responseData.html_url)); + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + panel.webview.postMessage({ + command: 'error', + message: `Failed to create issue: ${errorMessage}` + }); + vscode.window.showErrorMessage(`Failed to create issue: ${errorMessage}`); + } +} + +async function handleCreatePullRequest(data: any, panel: vscode.WebviewPanel, context: vscode.ExtensionContext) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) { + throw new Error('No WizGIT credentials found. Please configure them first.'); + } + + const response = await fetch(`${credentials.apiEndpoint}/repos/${data.owner}/${data.repo}/pulls`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `token ${credentials.token}` + }, + body: JSON.stringify({ + title: data.title, + body: data.body, + head: data.head, + base: data.base + }) + }); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`API request failed with status ${response.status}: ${errorBody}`); + } + + const responseData = await response.json() as { + title: string; + number: number; + html_url: string; + }; + + // Update webview with success message + panel.webview.postMessage({ + command: 'success', + message: `Successfully created pull request #${responseData.number}: ${responseData.title}`, + url: responseData.html_url + }); + + // Show VS Code notification + const action = await vscode.window.showInformationMessage( + `Successfully created pull request #${responseData.number}: ${responseData.title}`, + 'Open in Browser' + ); + + if (action === 'Open in Browser' && responseData.html_url) { + vscode.env.openExternal(vscode.Uri.parse(responseData.html_url)); + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + panel.webview.postMessage({ + command: 'error', + message: `Failed to create pull request: ${errorMessage}` + }); + vscode.window.showErrorMessage(`Failed to create pull request: ${errorMessage}`); + } +} + +// Clone repository functionality +async function cloneWizGitRepository(context: vscode.ExtensionContext) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) { + vscode.window.showErrorMessage('WizGIT credentials are required. Please configure them first.'); + return; + } + + const repositoryUrl = await vscode.window.showInputBox({ + prompt: 'Enter the WizGIT repository URL to clone', + placeHolder: 'https://wizgit.your.host/user/repository.git', + ignoreFocusOut: true, + validateInput: (value) => { + if (!value) return 'Repository URL is required'; + try { + new URL(value); + return null; + } catch { + return 'Please enter a valid URL'; + } + } + }); + + if (!repositoryUrl) return; + + const folderUri = await vscode.window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select folder to clone into' + }); + + if (!folderUri || folderUri.length === 0) return; + + const cloneFolder = folderUri[0].fsPath; + const repoName = path.basename(repositoryUrl, '.git'); + const targetPath = path.join(cloneFolder, repoName); + + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: `Cloning ${repoName}...`, + cancellable: false + }, async (progress) => { + progress.report({ increment: 50, message: 'Cloning repository...' }); + + // Use VS Code's built-in git to clone + const terminal = vscode.window.createTerminal({ + name: 'WizGIT Clone', + cwd: cloneFolder + }); + + terminal.sendText(`git clone ${repositoryUrl}`); + terminal.show(); + + progress.report({ increment: 100, message: 'Clone completed!' }); + }); + + const openChoice = await vscode.window.showInformationMessage( + `Repository cloned successfully to ${targetPath}`, + 'Open in New Window', + 'Add to Workspace' + ); + + if (openChoice === 'Open in New Window') { + await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), true); + } else if (openChoice === 'Add to Workspace') { + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, 0, { + uri: vscode.Uri.file(targetPath), + name: repoName + }); + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + vscode.window.showErrorMessage(`Failed to clone repository: ${errorMessage}`); + } +} + +// Get workspace repository info for auto-population +async function getWorkspaceRepoInfo(): Promise<{ owner: string, repo: string, currentBranch: string } | null> { + const workspaces = vscode.workspace.workspaceFolders; + if (!workspaces || workspaces.length === 0) { + return null; + } + + // Use the first workspace folder + const workspaceRoot = workspaces[0].uri.fsPath; + const gitDir = path.join(workspaceRoot, '.git'); + + if (!fs.existsSync(gitDir)) { + return null; + } + + try { + // Get current branch + const { exec } = require('child_process'); + const getCurrentBranch = (): Promise => { + return new Promise((resolve, reject) => { + exec('git branch --show-current', { cwd: workspaceRoot }, (error: any, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }); + }); + }; + + // Get remote URL + const getRemoteUrl = (): Promise => { + return new Promise((resolve, reject) => { + exec('git config --get remote.origin.url', { cwd: workspaceRoot }, (error: any, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }); + }); + }; + + const [currentBranch, remoteUrl] = await Promise.all([ + getCurrentBranch(), + getRemoteUrl() + ]); + + // Parse owner/repo from remote URL + // Handle formats: https://domain.com/owner/repo.git or git@domain.com:owner/repo.git + let owner = '', repo = ''; + if (remoteUrl.includes('://')) { + // HTTPS format + const match = remoteUrl.match(/\/([^/]+)\/([^/]+?)(\.git)?$/); + if (match) { + owner = match[1]; + repo = match[2]; + } + } else if (remoteUrl.includes('@')) { + // SSH format + const match = remoteUrl.match(/@[^:]+:([^/]+)\/([^/]+?)(\.git)?$/); + if (match) { + owner = match[1]; + repo = match[2]; + } + } + + if (owner && repo) { + return { owner, repo, currentBranch }; + } + } catch (error) { + console.error('Failed to get workspace repo info:', error); + } + + return null; +} + +// Get all available branches (local and remote) +async function getAvailableBranches(): Promise<{ local: string[], remote: string[], current: string }> { + const workspaces = vscode.workspace.workspaceFolders; + if (!workspaces || workspaces.length === 0) { + return { local: [], remote: [], current: '' }; + } + + const workspaceRoot = workspaces[0].uri.fsPath; + const gitDir = path.join(workspaceRoot, '.git'); + + if (!fs.existsSync(gitDir)) { + return { local: [], remote: [], current: '' }; + } + + try { + const { exec } = require('child_process'); + + const getLocalBranches = (): Promise => { + return new Promise((resolve) => { + exec('git branch', { cwd: workspaceRoot }, (error: any, stdout: string) => { + if (error) { + resolve([]); + } else { + const branches = stdout + .split('\n') + .map(branch => branch.replace('*', '').trim()) + .filter(branch => branch && branch !== ''); + resolve(branches); + } + }); + }); + }; + + const getRemoteBranches = (): Promise => { + return new Promise((resolve) => { + exec('git branch -r', { cwd: workspaceRoot }, (error: any, stdout: string) => { + if (error) { + resolve([]); + } else { + const branches = stdout + .split('\n') + .map(branch => branch.trim()) + .filter(branch => branch && !branch.includes('HEAD')) + .map(branch => branch.replace('origin/', '')) + .filter(branch => branch && branch !== ''); + resolve([...new Set(branches)]); // Remove duplicates + } + }); + }); + }; + + const getCurrentBranch = (): Promise => { + return new Promise((resolve) => { + exec('git branch --show-current', { cwd: workspaceRoot }, (error: any, stdout: string) => { + if (error) { + resolve(''); + } else { + resolve(stdout.trim()); + } + }); + }); + }; + + const [local, remote, current] = await Promise.all([ + getLocalBranches(), + getRemoteBranches(), + getCurrentBranch() + ]); + + return { local, remote, current }; + } catch (error) { + console.error('Failed to get branches:', error); + return { local: [], remote: [], current: '' }; + } +} + +// Workspace detection and auto-configuration +async function detectWizGitRepositories(context: vscode.ExtensionContext): Promise<{ name: string, branch: string, remote: string }[]> { + if (!vscode.workspace.workspaceFolders) return []; + + const wizgitRepos: { name: string, branch: string, remote: string }[] = []; + const credentials = await getStoredCredentials(context); + + if (!credentials) return []; + + for (const folder of vscode.workspace.workspaceFolders) { + const gitDir = path.join(folder.uri.fsPath, '.git'); + try { + if (fs.existsSync(gitDir)) { + // Get git remote URL + const { stdout: remoteUrl } = await execAsync('git remote get-url origin', { + cwd: folder.uri.fsPath + }); + + // Get current branch + const { stdout: branch } = await execAsync('git branch --show-current', { + cwd: folder.uri.fsPath + }); + + const trimmedRemote = remoteUrl.trim(); + const trimmedBranch = branch.trim() || 'main'; + + // Check if remote URL matches WizGIT instance + const apiUrl = new URL(credentials.apiEndpoint); + if (trimmedRemote.includes(apiUrl.hostname)) { + wizgitRepos.push({ + name: folder.name, + branch: trimmedBranch, + remote: trimmedRemote + }); + } + } + } catch (error) { + // Ignore errors - repository might not have remotes set up + console.log(`Could not detect WizGIT repo for ${folder.name}:`, error); + } + } + + return wizgitRepos; +} + +// Issue Viewer +async function openIssueViewer(context: vscode.ExtensionContext, issueData: WizGitIssueData, repoPath: string) { + const panel = vscode.window.createWebviewPanel( + 'wizgitIssueViewer', + `Issue #${issueData.number}: ${issueData.title}`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + try { + const credentials = await getStoredCredentials(context); + if (!credentials) { + panel.webview.html = getErrorWebviewContent('No WizGIT credentials configured'); + return; + } + + // Fetch comments + const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${issueData.number}/comments`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + const comments: WizGitCommentData[] = commentsResponse.ok ? await commentsResponse.json() as WizGitCommentData[] : []; + + panel.webview.html = getIssueViewerContent(issueData, comments); + + panel.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case 'openInBrowser': + vscode.env.openExternal(vscode.Uri.parse(issueData.html_url)); + break; + case 'addComment': + await addIssueComment(context, repoPath, issueData.number, message.comment, panel); + break; + case 'toggleState': + await toggleIssueState(context, repoPath, issueData.number, issueData.state, panel); + break; + } + }, undefined, context.subscriptions); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + panel.webview.html = getErrorWebviewContent(`Failed to load issue: ${errorMessage}`); + } +} + +// Helper function to parse diff text into file objects +function parseDiffToFiles(diffText: string): any[] { + const files: any[] = []; + const lines = diffText.split('\n'); + let currentFile: any = null; + + console.log('Parsing diff text, total lines:', lines.length); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Match file headers like "diff --git a/file.js b/file.js" + if (line.startsWith('diff --git')) { + if (currentFile) { + files.push(currentFile); + } + const match = line.match(/diff --git a\/(.*?) b\/(.*?)$/); + if (match) { + currentFile = { + filename: match[2], + status: 'modified', + additions: 0, + deletions: 0, + changes: 0 + }; + console.log('Found file in diff:', match[2]); + } + } + // Also try to match simple file headers like "--- a/file.js" and "+++ b/file.js" + else if (line.startsWith('--- a/') && !currentFile) { + const match = line.match(/^--- a\/(.*?)$/); + if (match && i + 1 < lines.length && lines[i + 1].startsWith('+++ b/')) { + const nextMatch = lines[i + 1].match(/^\+\+\+ b\/(.*?)$/); + if (nextMatch) { + currentFile = { + filename: nextMatch[1], + status: 'modified', + additions: 0, + deletions: 0, + changes: 0 + }; + console.log('Found file from --- +++ headers:', nextMatch[1]); + } + } + } + else if (line.startsWith('new file mode')) { + if (currentFile) { + currentFile.status = 'added'; + } + } else if (line.startsWith('deleted file mode')) { + if (currentFile) { + currentFile.status = 'removed'; + } + } else if (line.startsWith('rename from')) { + if (currentFile) { + currentFile.status = 'renamed'; + } + } else if (line.startsWith('+') && !line.startsWith('+++') && !line.startsWith('+++ ')) { + if (currentFile) { + currentFile.additions++; + currentFile.changes++; + } + } else if (line.startsWith('-') && !line.startsWith('---') && !line.startsWith('--- ')) { + if (currentFile) { + currentFile.deletions++; + currentFile.changes++; + } + } + } + + if (currentFile) { + files.push(currentFile); + } + + console.log('Parsed files from diff:', files); + return files; +} + +// Pull Request Viewer +async function openPullRequestViewer(context: vscode.ExtensionContext, prData: WizGitPRData, repoPath: string) { + const panel = vscode.window.createWebviewPanel( + 'wizgitPRViewer', + `PR #${prData.number}: ${prData.title}`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + try { + const credentials = await getStoredCredentials(context); + if (!credentials) { + panel.webview.html = getErrorWebviewContent('No WizGIT credentials configured'); + return; + } + + // Fetch comments + const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${prData.number}/comments`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + const comments: WizGitCommentData[] = commentsResponse.ok ? await commentsResponse.json() as WizGitCommentData[] : []; + + // Fetch raw PR details first for debugging and fallback data + let rawPrData: any = null; + try { + const rawPrResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + if (rawPrResponse.ok) { + rawPrData = await rawPrResponse.json(); + console.log('Raw PR Data:', rawPrData); + } + } catch (rawError) { + console.error('Failed to fetch raw PR data:', rawError); + } + + // Fetch files changed using multiple methods + let files: any[] = []; + let filesLoadMethod = 'none'; + + // Check if files are embedded in the PR data first + if (rawPrData?.files && Array.isArray(rawPrData.files)) { + files = rawPrData.files; + filesLoadMethod = 'embedded-in-pr'; + console.log('Files found embedded in PR data:', files); + } + + // Method 1: Try standard files endpoint + if (files.length === 0) { + console.log('Attempting to fetch files from:', `${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}/files`); + try { + const filesResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}/files`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + console.log('Files endpoint response status:', filesResponse.status); + if (filesResponse.ok) { + const filesData = await filesResponse.json() as any[]; + console.log('Files endpoint raw response:', filesData); + if (filesData && Array.isArray(filesData) && filesData.length > 0) { + files = filesData; + filesLoadMethod = 'files-endpoint'; + console.log('PR Files loaded via files endpoint:', files); + } else { + console.warn('Files endpoint returned empty or invalid data:', filesData); + } + } else { + const errorText = await filesResponse.text(); + console.warn('Files endpoint failed:', filesResponse.status, filesResponse.statusText, errorText); + } + } catch (error) { + console.error('Files endpoint error:', error); + } + } + + // Method 2: Try diff endpoint if files are still empty + if (files.length === 0) { + console.log('Attempting diff endpoint:', `${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}.diff`); + try { + const diffResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}.diff`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'text/plain' + } + }); + + console.log('Diff endpoint response status:', diffResponse.status); + if (diffResponse.ok) { + const diffText = await diffResponse.text(); + console.log('Diff text preview (first 500 chars):', diffText.substring(0, 500)); + files = parseDiffToFiles(diffText); + filesLoadMethod = 'diff-endpoint'; + console.log('PR Files loaded via diff endpoint:', files); + } else { + const errorText = await diffResponse.text(); + console.warn('Diff endpoint failed:', diffResponse.status, diffResponse.statusText, errorText); + } + } catch (error) { + console.error('Diff endpoint error:', error); + } + } + + // Method 3: Try patch endpoint if files are still empty + if (files.length === 0) { + console.log('Attempting patch endpoint:', `${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}.patch`); + try { + const patchResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prData.number}.patch`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'text/plain' + } + }); + + console.log('Patch endpoint response status:', patchResponse.status); + if (patchResponse.ok) { + const patchText = await patchResponse.text(); + console.log('Patch text preview (first 500 chars):', patchText.substring(0, 500)); + files = parseDiffToFiles(patchText); + filesLoadMethod = 'patch-endpoint'; + console.log('PR Files loaded via patch endpoint:', files); + } else { + const errorText = await patchResponse.text(); + console.warn('Patch endpoint failed:', patchResponse.status, patchResponse.statusText, errorText); + } + } catch (error) { + console.error('Patch endpoint error:', error); + } + } + + // Method 4: Try commits endpoint to get file changes + if (files.length === 0 && rawPrData?.head?.sha) { + console.log('Attempting commits endpoint for files'); + try { + const commitResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/commits/${rawPrData.head.sha}`, { + headers: { + 'Authorization': `token ${credentials.token}`, + 'Accept': 'application/json' + } + }); + + if (commitResponse.ok) { + const commitData: any = await commitResponse.json(); + if (commitData.files && Array.isArray(commitData.files)) { + files = commitData.files; + filesLoadMethod = 'commit-files'; + console.log('Files loaded from commit data:', files); + } + } + } catch (error) { + console.error('Commit endpoint error:', error); + } + } + + // Final summary + console.log(`Files detection complete: ${files.length} files found using method: ${filesLoadMethod}`); + + panel.webview.html = getPullRequestViewerContent(prData, comments, files, rawPrData, filesLoadMethod); + + panel.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case 'openInBrowser': + vscode.env.openExternal(vscode.Uri.parse(prData.html_url)); + break; + case 'addComment': + await addIssueComment(context, repoPath, prData.number, message.comment, panel); + break; + case 'mergePR': + await mergePullRequest(context, repoPath, prData.number, panel); + break; + } + }, undefined, context.subscriptions); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + panel.webview.html = getErrorWebviewContent(`Failed to load pull request: ${errorMessage}`); + } +} + +// Helper functions for API operations +async function addIssueComment(context: vscode.ExtensionContext, repoPath: string, issueNumber: number, comment: string, panel: vscode.WebviewPanel) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) return; + + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${issueNumber}/comments`, { + method: 'POST', + headers: { + 'Authorization': `token ${credentials.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ body: comment }) + }); + + if (response.ok) { + panel.webview.postMessage({ command: 'commentAdded', success: true }); + vscode.window.showInformationMessage('Comment added successfully!'); + } else { + panel.webview.postMessage({ command: 'commentAdded', success: false, error: 'Failed to add comment' }); + } + } catch (error) { + panel.webview.postMessage({ command: 'commentAdded', success: false, error: 'Network error' }); + } +} + +async function toggleIssueState(context: vscode.ExtensionContext, repoPath: string, issueNumber: number, currentState: string, panel: vscode.WebviewPanel) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) return; + + const newState = currentState === 'open' ? 'closed' : 'open'; + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${issueNumber}`, { + method: 'PATCH', + headers: { + 'Authorization': `token ${credentials.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ state: newState }) + }); + + if (response.ok) { + panel.webview.postMessage({ command: 'stateChanged', success: true, newState }); + vscode.window.showInformationMessage(`Issue ${newState}!`); + } else { + panel.webview.postMessage({ command: 'stateChanged', success: false }); + } + } catch (error) { + panel.webview.postMessage({ command: 'stateChanged', success: false }); + } +} + +async function mergePullRequest(context: vscode.ExtensionContext, repoPath: string, prNumber: number, panel: vscode.WebviewPanel) { + try { + const credentials = await getStoredCredentials(context); + if (!credentials) return; + + const response = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/pulls/${prNumber}/merge`, { + method: 'PUT', + headers: { + 'Authorization': `token ${credentials.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + commit_title: `Merge pull request #${prNumber}`, + merge_method: 'merge' + }) + }); + + if (response.ok) { + panel.webview.postMessage({ command: 'merged', success: true }); + vscode.window.showInformationMessage('Pull request merged successfully!'); + } else { + panel.webview.postMessage({ command: 'merged', success: false }); + vscode.window.showErrorMessage('Failed to merge pull request'); + } + } catch (error) { + panel.webview.postMessage({ command: 'merged', success: false }); + vscode.window.showErrorMessage('Network error while merging'); + } +} + +// Webview Content Functions +function getIssueViewerContent(issue: WizGitIssueData, comments: WizGitCommentData[]): string { + const commentsHtml = comments.map(comment => ` +
+
+ ${comment.user.login} +
+ ${comment.user.login} + ${new Date(comment.created_at).toLocaleString()} +
+
+
${escapeHtml(comment.body)}
+
+ `).join(''); + + return ` + + + + + Issue #${issue.number} + + + +
+

#${issue.number} ${escapeHtml(issue.title)}

+
+ ${issue.state} + ${issue.user.login} + ${issue.user.login} opened this issue on ${new Date(issue.created_at).toLocaleDateString()} +
+ ${issue.labels && issue.labels.length > 0 ? ` +
+ ${issue.labels.map(label => `${escapeHtml(label.name)}`).join('')} +
+ ` : ''} +
+ +
+ + +
+ +
${escapeHtml(issue.body || 'No description provided.')}
+ +
+

Comments (${comments.length})

+ ${commentsHtml} +
+ +
+

Add a comment

+ +
+ +
+
+ + + +`; +} + +function getPullRequestViewerContent(pr: WizGitPRData, comments: WizGitCommentData[], files: any[], rawPrData?: any, filesLoadMethod?: string): string { + const commentsHtml = comments.map(comment => ` +
+
+ ${comment.user.login} +
+ ${comment.user.login} + ${new Date(comment.created_at).toLocaleString()} +
+
+
${escapeHtml(comment.body)}
+
+ `).join(''); + + const filesHtml = files && files.length > 0 ? files.map(file => { + const status = file.status || 'modified'; + const statusIcons: { [key: string]: string } = { + 'added': '📄', + 'removed': '🗑️', + 'modified': '✏️', + 'renamed': '📝' + }; + const statusIcon = statusIcons[status] || '📄'; + + const statusColors: { [key: string]: string } = { + 'added': '#28a745', + 'removed': '#dc3545', + 'modified': '#ffc107', + 'renamed': '#17a2b8' + }; + const statusColor = statusColors[status] || '#6c757d'; + + return ` +
+
+
+ ${statusIcon} + ${escapeHtml(file.filename || file.name || 'Unknown file')} +
+ + ${file.additions > 0 ? `+${file.additions}` : ''} + ${file.deletions > 0 ? `-${file.deletions}` : ''} + ${file.changes > 0 && !file.additions && !file.deletions ? `${file.changes} changes` : ''} + +
+
`; + }).join('') : '
No files changed or unable to load files.
'; + + return ` + + + + + PR #${pr.number} + + + +
+

#${pr.number} ${escapeHtml(pr.title)}

+
+ ${pr.state} + ${pr.user.login} + ${pr.user.login} wants to merge into ${pr.base.ref} from ${pr.head.ref} +
+
+ +
+ ${escapeHtml(pr.head.ref)} → ${escapeHtml(pr.base.ref)} +
+ +
+ ${pr.state === 'open' && pr.mergeable ? '' : ''} + +
+ +
${escapeHtml(pr.body || 'No description provided.')}
+ +
+

Files changed (${files.length})

+ ${filesHtml} +
+ Debug Info:
+ • Files loaded: ${files.length}
+ • Files load method: ${filesLoadMethod || 'unknown'}
+ • PR Number: ${pr.number}
+ • Raw PR Data:
Click to expand
${rawPrData}
+
+
+ +
+

Comments (${comments.length})

+ ${commentsHtml} +
+ +
+

Add a comment

+ +
+ +
+
+ + + +`; +} + +function getErrorWebviewContent(errorMessage: string): string { + return ` + + + + + Error + + + +
+

Error

+

${escapeHtml(errorMessage)}

+
+ +`; +} + +// Helper functions +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function getContrastColor(hexColor: string): string { + // Simple contrast calculation + const r = parseInt(hexColor.substr(0, 2), 16); + const g = parseInt(hexColor.substr(2, 2), 16); + const b = parseInt(hexColor.substr(4, 2), 16); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 128 ? '#000000' : '#ffffff'; +} + +export function deactivate() { } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..900ff46 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2021", + "outDir": "out", + "rootDir": "src", + "lib": [ + "ES2021", + "DOM" + ], + "sourceMap": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} \ No newline at end of file