diff --git a/CHANGELOG.md b/CHANGELOG.md index efc9d2f..658290a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ All notable changes to the "gops" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -## [Unreleased] +## [0.0.5] -- Initial release \ No newline at end of file +- Added ability to perform diff checks on changed files diff --git a/package-lock.json b/package-lock.json index 463f772..4f38d84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "22.x", + "@types/node": "^22.19.19", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", @@ -1210,9 +1210,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2ab2e20..ae6033e 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,11 @@ "command": "gops.tag", "title": "Create Tag", "icon": "$(tag)" + }, + { + "command": "gops.showDiff", + "title": "Show Diff", + "icon": "$(diff)" } ], "viewsContainers": { @@ -167,7 +172,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "22.x", + "@types/node": "^22.19.19", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", diff --git a/resources/gops-logo.png b/resources/gops-logo.png index 3196a1d..5b0835e 100644 Binary files a/resources/gops-logo.png and b/resources/gops-logo.png differ diff --git a/src/commands/CommandRegistrar.ts b/src/commands/CommandRegistrar.ts index 8da7f07..a702303 100644 --- a/src/commands/CommandRegistrar.ts +++ b/src/commands/CommandRegistrar.ts @@ -4,12 +4,15 @@ import { GitService } from "../services/GitService"; import { COMMANDS } from "./Commands"; import { GitTreeNode } from "../gopstree/types"; import { Logger } from "../logging/Logger"; +import { DiffService } from "../services/DiffService"; +import { ChangedFileNode } from "../gopstree/nodes/ChangedFileNode"; export class CommandRegistrar { constructor( private context: vscode.ExtensionContext, private treeDataProvider: TreeDataProvider, private gitService: GitService, + private diffService: DiffService, ) {} registerAll() { @@ -92,7 +95,28 @@ export class CommandRegistrar { this.treeDataProvider.refresh(node.parent); } }, - ); + ); + + this.register(COMMANDS.SHOW_DIFF, async (node: GitTreeNode) => { + console.debug(`executed command: ${COMMANDS.SHOW_DIFF}`); + if (!node || !(node instanceof ChangedFileNode) || !node.fileName) { + return; + } + const repoPath = this.gitService.getRepoPath(); + + await this.diffService.openDiff({ + left: { + repositoryPath: repoPath, + fileName: node.fileName, + ref: "HEAD", + }, + right: { + repositoryPath: repoPath, + fileName: node.fileName, + }, + title: `Diff: ${node.fileName}`, + }); + }); } private register( diff --git a/src/commands/Commands.ts b/src/commands/Commands.ts index af63440..d97f9d5 100644 --- a/src/commands/Commands.ts +++ b/src/commands/Commands.ts @@ -8,4 +8,5 @@ export const COMMANDS = { CREATE_BRANCH_FROM_CURRENT: "gops.branch.current", CREATE_BRANCH: "gops.branch", CREATE_TAG: "gops.tag", + SHOW_DIFF: "gops.showDiff", } as const; diff --git a/src/extension.ts b/src/extension.ts index 62c7105..5b904a8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,20 +2,26 @@ import * as vscode from "vscode"; import { TreeDataProvider } from "./gopstree/TreeDataProvider"; import { GitService } from "./services/GitService"; import { CommandRegistrar } from "./commands/CommandRegistrar"; +import { DiffService } from "./services/DiffService"; +import { FileService } from "./services/FileService"; export function activate(context: vscode.ExtensionContext) { const gitService = new GitService(); + const fileService = new FileService(context.globalStorageUri.fsPath); + const diffService = new DiffService(fileService, gitService); const treeDataProvider = new TreeDataProvider(gitService); - vscode.window.createTreeView("gitOpsTreeview", {treeDataProvider}); - + const treeView = vscode.window.createTreeView("gitOpsTreeview", { treeDataProvider }); + // Register commands const registrar = new CommandRegistrar( context, treeDataProvider, gitService, + diffService, ); registrar.registerAll(); - + + context.subscriptions.push(treeView); console.log("Gops extension activated."); } diff --git a/src/gopstree/TreeDataProvider.ts b/src/gopstree/TreeDataProvider.ts index 14de4b2..e896922 100644 --- a/src/gopstree/TreeDataProvider.ts +++ b/src/gopstree/TreeDataProvider.ts @@ -8,6 +8,7 @@ import { RemoteBranchNode } from "./nodes/RemoteBranchNode"; import { Constants } from "../constants/Constants"; import { GitTreeNode } from "./types"; import { Notifications } from "../notifications/Notifications"; +import { ChangedFileNode } from "./nodes/ChangedFileNode"; export class TreeDataProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter< @@ -81,9 +82,13 @@ export class TreeDataProvider implements vscode.TreeDataProvider { ...status.renamed.map((f) => f.to), ]; - return changedFiles.map( - (f) => new TreeItemModel({ label: f }, NodeType.File, vscode.TreeItemCollapsibleState.None) - ); + const allChangedFiles = changedFiles.map((f) => { + const node = new ChangedFileNode(f); + console.debug(node.toString()); + return node; + }); + + return allChangedFiles; } private async getTags(): Promise { diff --git a/src/gopstree/nodes/ChangedFileNode.ts b/src/gopstree/nodes/ChangedFileNode.ts new file mode 100644 index 0000000..c793c02 --- /dev/null +++ b/src/gopstree/nodes/ChangedFileNode.ts @@ -0,0 +1,37 @@ +import { ContextValue } from "../ContextValue"; +import { NodeType } from "./NodeType"; +import { TreeItemModel } from "../TreeItemModel"; +import * as vscode from 'vscode'; +import { createChangedFileTooltip, formatChangedFileLabel } from "./utils/nodeUtils"; +import { COMMANDS } from "../../commands/Commands"; + +export class ChangedFileNode extends TreeItemModel { + public override command?: vscode.Command; + constructor( + public readonly fileName: string, + ) { + const fomatted = formatChangedFileLabel(fileName); + super( + { + label: fomatted.label, + highlights: fomatted.highlights, + }, + NodeType.Changes, + vscode.TreeItemCollapsibleState.None, + ); + this.contextValue = ContextValue.Changes; + this.command = { + title: "Show Diff", + command: COMMANDS.SHOW_DIFF, + arguments: [this], + }; + + this.tooltip = createChangedFileTooltip( + fileName, + ); + } + + public toString(): string { + return `ChangedFileNode(${this.fileName}, contextValue=${this.contextValue})`; + } +} \ No newline at end of file diff --git a/src/gopstree/nodes/utils/nodeUtils.ts b/src/gopstree/nodes/utils/nodeUtils.ts index 5ca3d1d..728b7d0 100644 --- a/src/gopstree/nodes/utils/nodeUtils.ts +++ b/src/gopstree/nodes/utils/nodeUtils.ts @@ -75,3 +75,18 @@ export const createRemoteBranchTooltip = ( isTracking ? "Tracking enabled" : "Not tracking", ].join("\n"); }; + +export const formatChangedFileLabel = ( + fileName: string, +): LabelWithHighlights => { + return { + label: fileName, + highlights: [], + }; +}; + +export const createChangedFileTooltip = ( + fileName: string, +): string => { + return `File: ${fileName}`; +}; diff --git a/src/services/AheadBehindModel.ts b/src/models/AheadBehindModel.ts similarity index 100% rename from src/services/AheadBehindModel.ts rename to src/models/AheadBehindModel.ts diff --git a/src/models/DiffRequest.ts b/src/models/DiffRequest.ts new file mode 100644 index 0000000..d6152f7 --- /dev/null +++ b/src/models/DiffRequest.ts @@ -0,0 +1,7 @@ +import { FileRevision } from "./FileRevision"; + +export interface DiffRequest { + left: FileRevision; + right: FileRevision; + title?: string; +} diff --git a/src/models/FileRevision.ts b/src/models/FileRevision.ts new file mode 100644 index 0000000..29415df --- /dev/null +++ b/src/models/FileRevision.ts @@ -0,0 +1,5 @@ +export interface FileRevision { + repositoryPath: string; + fileName: string; + ref?: string; +} diff --git a/src/services/LocalBranchModel.ts b/src/models/LocalBranchModel.ts similarity index 100% rename from src/services/LocalBranchModel.ts rename to src/models/LocalBranchModel.ts diff --git a/src/services/RemoteBranchModel.ts b/src/models/RemoteBranchModel.ts similarity index 100% rename from src/services/RemoteBranchModel.ts rename to src/models/RemoteBranchModel.ts diff --git a/src/services/DiffService.ts b/src/services/DiffService.ts new file mode 100644 index 0000000..6a5654f --- /dev/null +++ b/src/services/DiffService.ts @@ -0,0 +1,35 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { DiffRequest } from "../models/DiffRequest"; +import { GitService } from "./GitService"; +import { FileService } from "./FileService"; + +export class DiffService { + constructor( + private readonly fileService: FileService, + private readonly gitService: GitService, + ) {} + + public async openDiff(request: DiffRequest): Promise { + const headContent = await this.gitService.getFileContent( + request.left.ref ?? "HEAD", + request.left.fileName, + ); + + const tempFile = await this.fileService.createTempFile( + request.left.fileName, + headContent, + ); + + const rightUri = vscode.Uri.file( + path.join(request.right.repositoryPath, request.right.fileName), + ); + + await vscode.commands.executeCommand( + "vscode.diff", + vscode.Uri.file(tempFile), + rightUri, + request.title, + ); + } +} diff --git a/src/services/FileService.ts b/src/services/FileService.ts new file mode 100644 index 0000000..96ec942 --- /dev/null +++ b/src/services/FileService.ts @@ -0,0 +1,27 @@ +import * as fs from "fs"; +import * as path from "path"; + +export class FileService { + constructor(private readonly storagePath: string) {} + + public async createTempFile( + filePath: string, + content: string, + ): Promise { + const tempDir = this.getTempDir(); + + await fs.promises.mkdir(tempDir, { recursive: true }); + + const safeName = filePath.replace(/[\/\\]/g, "_"); + + const tempFilePath = path.join(tempDir, `${Date.now()}_${safeName}`); + + await fs.promises.writeFile(tempFilePath, content, "utf8"); + + return tempFilePath; + } + + private getTempDir(): string { + return path.join(this.storagePath, "temp"); + } +} diff --git a/src/services/GitService.ts b/src/services/GitService.ts index 5a68d8c..32f4f55 100644 --- a/src/services/GitService.ts +++ b/src/services/GitService.ts @@ -3,9 +3,9 @@ import * as vscode from "vscode"; import * as path from "path"; import { Logger } from "../logging/Logger"; import { Notifications } from "../notifications/Notifications"; -import { LocalBranchModel } from "./LocalBranchModel"; -import { RemoteBranchModel } from "./RemoteBranchModel"; -import { AheadBehindModel } from "./AheadBehindModel"; +import { LocalBranchModel } from "../models/LocalBranchModel"; +import { RemoteBranchModel } from "../models/RemoteBranchModel"; +import { AheadBehindModel } from "../models/AheadBehindModel"; export class GitService { private git: SimpleGit; @@ -74,6 +74,10 @@ export class GitService { return log.all; } + public async getFileContent(ref: string, filePath: string): Promise { + return await this.git.show([`${ref}:${filePath}`]); + } + // #region [Branch Operations] async checkout(branch: string) { diff --git a/tsconfig.json b/tsconfig.json index 9bfc5f0..7ecbdf5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "sourceMap": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */, - "outDir": "dist" + "outDir": "dist", + "types": ["node"] /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */