From 6c104d4bace149347f60d353018208d58621b116 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 21 Nov 2024 23:26:10 +0100 Subject: [PATCH] Added submenues (#184) * Added submenu Added editor title commands * Handle internal processes --- README.md | 2 + package.json | 127 +++++++++++++---- src/ccScmProvider.ts | 178 +++++++++++++----------- src/clearcase.ts | 200 ++++++++++++++++++--------- src/extension.ts | 8 +- src/test/suite/cleartool.test.ts | 12 +- src/test/suite/outputchannel.test.ts | 4 +- 7 files changed, 353 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index 594db4d..e359804 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [![CodeFactor](https://www.codefactor.io/repository/github/openningia/vscode-clearcase/badge)](https://www.codefactor.io/repository/github/openningia/vscode-clearcase) [![.github/workflows/publish.yml](https://github.com/OpenNingia/vscode-clearcase/actions/workflows/publish.yml/badge.svg)](https://github.com/OpenNingia/vscode-clearcase/actions/workflows/publish.yml) [![Test on master](https://github.com/OpenNingia/vscode-clearcase/actions/workflows/integrate_test.yml/badge.svg)](https://github.com/OpenNingia/vscode-clearcase/actions/workflows/integrate_test.yml) +![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/OpenNingia.vscode-clearcase) +![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/OpenNingia.vscode-clearcase) # VS Code ClearCase diff --git a/package.json b/package.json index 0c3b0fa..2f760d2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-clearcase", "displayName": "Unofficial ClearCase SCM Commands", "description": "Unofficial support for IBM Rational ClearCase most common commands", - "version": "5.0.0", + "version": "5.1.0-beta.1", "publisher": "OpenNingia", "license": "MIT", "repository": { @@ -47,7 +47,8 @@ { "command": "extension.ccHijack", "title": "Hijack", - "category": "Clearcase" + "category": "Clearcase", + "icon": "$(go-to-editing-session)" }, { "command": "extension.ccUndoHijack", @@ -76,7 +77,8 @@ { "command": "extension.ccVersionTree", "title": "Version Tree", - "category": "Clearcase" + "category": "Clearcase", + "icon": "$(type-hierarchy-sub)" }, { "command": "extension.ccComparePrevious", @@ -86,7 +88,7 @@ }, { "command": "extension.ccCompareWithVersion", - "title": "Compare with version", + "title": "Compare with version ...", "category": "Clearcase", "icon": "$(diff-single)" }, @@ -159,7 +161,8 @@ { "command": "extension.ccAnnotate", "title": "Annotate", - "category": "Clearcase" + "category": "Clearcase", + "icon": "$(comment)" }, { "command": "extension.ccSelectActv", @@ -184,6 +187,18 @@ "icon": "$(add)" } ], + "submenus": [ + { + "id": "vscode-clearcase.explorer_context_submenu", + "icon": "$(source-control)", + "label": "Clearcase" + }, + { + "id": "vscode-clearcase.editor_context_submenu", + "icon": "$(source-control)", + "label": "Clearcase" + } + ], "menus": { "commandPalette": [ { @@ -192,7 +207,7 @@ }, { "command": "extension.ccCheckout", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects && resourcePath not in vscode-clearcase:CheckedoutObjects" }, { "command": "extension.ccCheckin", @@ -200,15 +215,15 @@ }, { "command": "extension.ccVersionTree", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccComparePrevious", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccCompareWithVersion", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccUndoCheckout", @@ -224,7 +239,7 @@ }, { "command": "extension.ccItemProperties", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccUpdateView", @@ -236,11 +251,11 @@ }, { "command": "extension.ccUpdateFile", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccAnnotate", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects && resourcePath not in vscode-clearcase:HijackedObjects && resourcePath not in vscode-clearcase:CheckedoutObjects" }, { "command": "extension.ccSelectActv", @@ -248,29 +263,40 @@ }, { "command": "extension.ccMkElement", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath in vscode-clearcase:ViewPrivateObjects" } ], "explorer/context": [ + { + "submenu": "vscode-clearcase.explorer_context_submenu", + "group": "clearcase" + } + ], + "vscode-clearcase.explorer_context_submenu": [ { "command": "extension.ccUpdateFile", "group": "cc@10", - "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView" + "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccMkElement", "group": "cc@5", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath in vscode-clearcase:ViewPrivateObjects" + }, + { + "command": "extension.ccCompareWithVersion", + "group": "cc@5", + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccVersionTree", "group": "cc@12", - "when": "vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects && resourcePath not in vscode-clearcase:HijackedObjects" }, { "command": "extension.ccCheckout", "group": "cc@2", - "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:CheckedoutObjects" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccItemProperties", @@ -290,7 +316,7 @@ { "command": "extension.ccHijack", "group": "cc@5", - "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:HijackedObjects" + "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:HijackedObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccUndoHijack", @@ -298,7 +324,60 @@ "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath in vscode-clearcase:HijackedObjects" } ], + "editor/title": [ + { + "command": "extension.ccAnnotate", + "group": "navigation@100", + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:HijackedObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccCompareWithVersion", + "group": "navigation@101", + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccCheckout", + "group": "navigation@102", + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccCheckin", + "group": "navigation@103", + "when": "vscode-clearcase:enabled && resourcePath in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccUndoCheckout", + "group": "navigation@104", + "when": "vscode-clearcase:enabled && resourcePath in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccHijack", + "group": "navigation@105", + "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath not in vscode-clearcase:HijackedObjects && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccUndoHijack", + "group": "navigation@106", + "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath in vscode-clearcase:HijackedObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccMkElement", + "group": "navigation@107", + "when": "vscode-clearcase:enabled && resourcePath in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + }, + { + "command": "extension.ccVersionTree", + "group": "navigation@108", + "when": "editorTextFocus && !inOutput && vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects && vscode-clearcase:editor == true" + } + ], "editor/context": [ + { + "submenu": "vscode-clearcase.editor_context_submenu", + "group": "clearcase" + } + ], + "vscode-clearcase.editor_context_submenu": [ { "command": "extension.ccUndoCheckout", "group": "cc@4", @@ -325,9 +404,9 @@ "when": "vscode-clearcase:enabled && !vscode-clearcase:DynView && resourcePath not in vscode-clearcase:CheckedoutObjects && resourcePath in vscode-clearcase:HijackedObjects" }, { - "command": "extension.ccComparePrevious", + "command": "extension.ccCompareWithVersion", "group": "cc@5", - "when": "editorTextFocus && !inOutput && vscode-clearcase:enabled" + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccVersionTree", @@ -409,9 +488,9 @@ "group": "cc_file" }, { - "command": "extension.ccComparePrevious", - "when": "scmProvider == cc && scmResourceGroup != cc_untracked && vscode-clearcase:enabled", - "group": "cc_file" + "command": "extension.ccCompareWithVersion", + "group": "cc_file", + "when": "vscode-clearcase:enabled && resourcePath not in vscode-clearcase:ViewPrivateObjects" }, { "command": "extension.ccVersionTree", @@ -530,7 +609,7 @@ }, "vscode-clearcase.annotation.showAnnotationCodeLens": { "type": "boolean", - "default": true, + "default": false, "markdownDescription": "Enable the 'Toggle Annotate' code lens.", "order": 20 }, diff --git a/src/ccScmProvider.ts b/src/ccScmProvider.ts index 63030e1..6a2b2b8 100644 --- a/src/ccScmProvider.ts +++ b/src/ccScmProvider.ts @@ -47,13 +47,15 @@ export class CCScmProvider implements IDisposable { private mCCCheckedoutGrp: SourceControlResourceGroup | null = null; private mCCUntrackedGrp: SourceControlResourceGroup | null = null; private mCCHijackedGrp: SourceControlResourceGroup | null = null; + private mCCUntrackedResource: SourceControlResourceState[] = []; + private mCCHijackedResource: SourceControlResourceState[] = []; private mIsUpdatingUntracked = false; private mIsUpdatingHijacked = false; private mListLock: Lock | null = null; private mDisposables: IDisposable[] = []; private mVersion = "0.0.0"; - private mWindowChangedEvent: EventEmitter = new EventEmitter(); + private mWindowChangedEvent: EventEmitter = new EventEmitter(); get root(): Uri | undefined { if (workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length > 0) { @@ -67,10 +69,19 @@ export class CCScmProvider implements IDisposable { private outputChannel: CCOutputChannel, private configHandler: CCConfigHandler ) { - this.configHandler.onDidChangeConfiguration(() => { + this.configHandler.onDidChangeConfiguration(async () => { if (this.configHandler.configuration.logLevel.changed) { this.outputChannel.logLevel = this.configHandler.configuration.logLevel.value; } + if (this.configHandler.configuration.showHijackedFiles.changed) { + this.updateHijackedResourceGroup(); + await this.createHijackedList(); + } + if (this.configHandler.configuration.showViewPrivateFiles.changed) { + this.updateUntrackedResourceGroup(); + await this.createViewPrivateList(); + } + this.updateContextResources(window.activeTextEditor !== undefined); }); outputChannel.logLevel = this.configHandler.configuration.logLevel.value; } @@ -222,14 +233,17 @@ export class CCScmProvider implements IDisposable { return this.clearCase?.checkIsView(window.activeTextEditor) ?? false; } - updateContextResources(): void { + updateContextResources(valid: boolean): void { const d = this.mCCHandler ? this.mCCHandler.viewType === ViewType.Dynamic : false; const files = this.getCheckedoutObjects(); const hijackedFiles = this.getHijackedObjects(); + const viewPrivateFiles = this.getUntrackedObjects(); commands.executeCommand("setContext", "vscode-clearcase:enabled", this.mCCHandler?.isView); commands.executeCommand("setContext", "vscode-clearcase:DynView", d); commands.executeCommand("setContext", "vscode-clearcase:CheckedoutObjects", files); commands.executeCommand("setContext", "vscode-clearcase:HijackedObjects", hijackedFiles); + commands.executeCommand("setContext", "vscode-clearcase:ViewPrivateObjects", viewPrivateFiles); + commands.executeCommand("setContext", "vscode-clearcase:editor", valid); } private async handleChangeFiles(fileObjs: Uri[]) { @@ -261,7 +275,7 @@ export class CCScmProvider implements IDisposable { this.mCCCheckedoutGrp.resourceStates = filteredCheckedout?.sort((a, b) => CCScmResource.sort(a, b)) || []; } } - this.updateContextResources(); + this.updateContextResources(window.activeTextEditor !== undefined); } catch (error) { this.outputChannel.appendLine( "Clearcase error: getVersionInformation: " + getErrorMessage(error), @@ -307,6 +321,7 @@ export class CCScmProvider implements IDisposable { let viewPrivate: CCScmResource[] = []; if (this.configHandler.configuration.showViewPrivateFiles.value) { + this.clearCase?.killUpdateFindViewPrivate(); this.clearCase?.findViewPrivate().then((files) => { viewPrivate = files .map((val) => { @@ -316,7 +331,8 @@ export class CCScmProvider implements IDisposable { return val1.resourceUri.fsPath.localeCompare(val2.resourceUri.fsPath); }); if (this.mCCUntrackedGrp) { - this.mCCUntrackedGrp.resourceStates = viewPrivate.sort((a, b) => CCScmResource.sort(a, b)); + this.mCCUntrackedResource = viewPrivate.sort((a, b) => CCScmResource.sort(a, b)); + this.mCCUntrackedGrp.resourceStates = this.mCCUntrackedResource; } }); } @@ -326,6 +342,7 @@ export class CCScmProvider implements IDisposable { let hijacked: CCScmResource[] = []; if (this.configHandler.configuration.showHijackedFiles.value) { + this.clearCase?.killUpdateFindHijacked(); this.clearCase?.findHijacked().then((files) => { hijacked = files .map((val) => { @@ -335,7 +352,8 @@ export class CCScmProvider implements IDisposable { return val1.resourceUri.fsPath.localeCompare(val2.resourceUri.fsPath); }); if (this.mCCHijackedGrp) { - this.mCCHijackedGrp.resourceStates = hijacked.sort((a, b) => CCScmResource.sort(a, b)); + this.mCCHijackedResource = hijacked.sort((a, b) => CCScmResource.sort(a, b)); + this.mCCHijackedGrp.resourceStates = this.mCCHijackedResource; } }); } @@ -348,13 +366,13 @@ export class CCScmProvider implements IDisposable { } getUntrackedObjects(): string[] | undefined { - return this.mCCUntrackedGrp?.resourceStates.map((value: SourceControlResourceState) => { + return this.mCCUntrackedResource.map((value: SourceControlResourceState) => { return value.resourceUri.fsPath; }); } getHijackedObjects(): string[] | undefined { - return this.mCCHijackedGrp?.resourceStates.map((value: SourceControlResourceState) => { + return this.mCCHijackedResource.map((value: SourceControlResourceState) => { return value.resourceUri.fsPath; }); } @@ -446,7 +464,7 @@ export class CCScmProvider implements IDisposable { }); } - get onWindowChanged(): Event { + get onWindowChanged(): Event { return this.mWindowChangedEvent.event; } @@ -462,6 +480,7 @@ export class CCScmProvider implements IDisposable { this.registerCommand("extension.ccMkElement", (fileObj) => this.clearCase?.createVersionedObject(fileObj)); this.registerCommand("extension.ccHijack", (fileObj) => this.clearCase?.createHijackedObject(fileObj)); this.registerCommand("extension.ccUndoHijack", (fileObj) => this.clearCase?.cancelHijackedObject(fileObj)); + this.registerCommand("extension.ccCompareWithVersion", (fileObj) => this.selectVersionAndCompare(fileObj)); this.mDisposables.push( commands.registerCommand( @@ -500,21 +519,6 @@ export class CCScmProvider implements IDisposable { ) ); - this.mDisposables.push( - commands.registerCommand( - "extension.ccCompareWithVersion", - (fileObj: Uri) => { - if (fileObj === undefined || fileObj === null) { - if (window?.activeTextEditor) { - fileObj = window.activeTextEditor.document.uri; - } - } - this.selectVersionAndCompare(fileObj); - }, - this - ) - ); - this.mDisposables.push( commands.registerCommand( "extension.ccFindModified", @@ -865,90 +869,104 @@ export class CCScmProvider implements IDisposable { } private async updateHijackedList(fileObj: Uri, version: CCVersionType): Promise { - if (this.configHandler.configuration.showHijackedFiles.value) { - if (this.clearCase && this.mCCHijackedGrp) { - const isHijacked = version.state === CCVersionState.Hijacked; - let hijackedExists = false; - const filteredHijacked = - this.mCCHijackedGrp?.resourceStates.filter((item) => { - if (item.resourceUri.fsPath === fileObj.fsPath) { - hijackedExists = true; - return isHijacked; - } - return true; - }) ?? []; - - if (isHijacked) { - if (this.clearCase.hijackedList.exists(fileObj.fsPath) === false) { - this.clearCase.hijackedList.addString(fileObj.fsPath); - this.mContext.workspaceState.update("hijackedfilecache", this.clearCase.hijackedList.stringify()); - } - if (!hijackedExists) { - filteredHijacked.push(new CCScmResource(ResourceGroupType.Index, fileObj, CCScmStatus.Hijacked)); + if (this.clearCase && this.mCCHijackedGrp) { + const isHijacked = version.state === CCVersionState.Hijacked; + let hijackedExists = false; + const filteredHijacked = + this.mCCHijackedResource.filter((item) => { + if (item.resourceUri.fsPath === fileObj.fsPath) { + hijackedExists = true; + return isHijacked; } + return true; + }) ?? []; + + if (isHijacked) { + if (this.clearCase.hijackedList.exists(fileObj.fsPath) === false) { + this.clearCase.hijackedList.addString(fileObj.fsPath); + this.mContext.workspaceState.update("hijackedfilecache", this.clearCase.hijackedList.stringify()); } - if ((isHijacked && !hijackedExists) || (!isHijacked && hijackedExists)) { - this.mCCHijackedGrp.resourceStates = filteredHijacked?.sort((a, b) => CCScmResource.sort(a, b)) || []; + if (!hijackedExists) { + filteredHijacked.push(new CCScmResource(ResourceGroupType.Index, fileObj, CCScmStatus.Hijacked)); } - return true; } + this.mCCHijackedResource = [...filteredHijacked]; + this.updateHijackedResourceGroup(); + return true; } return false; } private async updateViewPrivateList(fileObj: Uri, version: CCVersionType): Promise { - if (this.configHandler.configuration.showViewPrivateFiles.value) { - if (this.clearCase && this.mCCUntrackedGrp) { - const isPrivate = version.state === CCVersionState.Untracked; - let privateExists = false; - const filteredPrivate = - this.mCCUntrackedGrp.resourceStates.filter((item) => { - if (item.resourceUri.fsPath === fileObj.fsPath) { - privateExists = true; - return isPrivate; - } - return true; - }) ?? []; - - if (isPrivate) { - if (this.clearCase.untrackedList.exists(fileObj.fsPath) === false) { - this.clearCase.untrackedList.addString(fileObj.fsPath); - this.mContext.workspaceState.update("untrackedfilecache", this.clearCase.untrackedList.stringify()); - } - if (!privateExists) { - filteredPrivate.push(new CCScmResource(ResourceGroupType.Index, fileObj, CCScmStatus.Untracked)); + if (this.clearCase && this.mCCUntrackedGrp) { + const isPrivate = version.state === CCVersionState.Untracked; + let privateExists = false; + const filteredPrivate = + this.mCCUntrackedResource.filter((item) => { + if (item.resourceUri.fsPath === fileObj.fsPath) { + privateExists = true; + return isPrivate; } + return true; + }) ?? []; + + if (isPrivate) { + if (this.clearCase.untrackedList.exists(fileObj.fsPath) === false) { + this.clearCase.untrackedList.addString(fileObj.fsPath); + this.mContext.workspaceState.update("untrackedfilecache", this.clearCase.untrackedList.stringify()); } - if ((isPrivate && !privateExists) || (!isPrivate && privateExists)) { - this.mCCUntrackedGrp.resourceStates = filteredPrivate?.sort((a, b) => CCScmResource.sort(a, b)) || []; + if (!privateExists) { + filteredPrivate.push(new CCScmResource(ResourceGroupType.Index, fileObj, CCScmStatus.Untracked)); } - return true; } + this.mCCUntrackedResource = [...filteredPrivate]; + this.updateUntrackedResourceGroup(); + return true; } return false; } + private updateUntrackedResourceGroup() { + if (this.mCCUntrackedGrp) { + if (this.configHandler.configuration.showViewPrivateFiles.value) { + this.mCCUntrackedGrp.resourceStates = this.mCCUntrackedResource.sort((a, b) => CCScmResource.sort(a, b)) || []; + } else { + this.mCCUntrackedGrp.resourceStates = []; + this.clearCase?.killUpdateFindViewPrivate(); + } + } + } + + private updateHijackedResourceGroup() { + if (this.mCCHijackedGrp) { + if (this.configHandler.configuration.showHijackedFiles.value) { + this.mCCHijackedGrp.resourceStates = this.mCCHijackedResource.sort((a, b) => CCScmResource.sort(a, b)) || []; + } else { + this.mCCHijackedGrp.resourceStates = []; + this.clearCase?.killUpdateFindHijacked(); + } + } + } + private async onDidChangeTextEditor(editor: TextEditor | undefined): Promise { this.mCCContentProvider?.resetCache(); this.updateCheckedOutList(); + if (editor && this.clearCase && editor?.document.uri.scheme !== "output") { const version = await this.clearCase?.getVersionInformation(editor?.document.uri, true); this.updateHijackedList(editor?.document.uri, version); this.updateViewPrivateList(editor.document.uri, version); } - //if (editor?.document.uri.scheme !== "output") { - // if (editor?.document.uri) { - // this.updateUntrackedListWFile(editor.document.uri); - // } - // this.mWindowChangedEvent.fire(); - //} + if (editor?.document.uri.scheme !== "output") { + this.mWindowChangedEvent.fire(editor !== undefined); + } } - private async selectVersionAndCompare(file: Uri) { - if (this.clearCase) { - const selVersion = await CCUIControl.showVersionSelectQuickpick(this.clearCase.getVersionsOfFile(file)); + private async selectVersionAndCompare(file: Uri[]) { + if (this.clearCase && file.length > 0) { + const selVersion = await CCUIControl.showVersionSelectQuickpick(this.clearCase.getVersionsOfFile(file[0])); if (selVersion !== undefined && selVersion !== "") { - this.embeddedDiff(file, selVersion); + this.embeddedDiff(file[0], selVersion); } } } diff --git a/src/clearcase.ts b/src/clearcase.ts index 7a773c0..afef08c 100644 --- a/src/clearcase.ts +++ b/src/clearcase.ts @@ -1,7 +1,7 @@ -import { exec, ChildProcess, spawn } from "child_process"; +import { exec, ChildProcess, spawn, ChildProcessWithoutNullStreams } from "child_process"; import * as fs from "fs"; import { type } from "os"; -import { dirname, join } from "path"; +import { dirname } from "path"; import * as tmp from "tmp"; import { @@ -47,7 +47,7 @@ export class CCArgs { constructor(params: string[], file?: string[], version?: string) { this.params = [...params]; - if (file) { + if (file && file.length > 0) { this.mFiles = [...file]; } this.mVersion = version; @@ -134,6 +134,8 @@ export class ClearCase { private mWebviewPassword = ""; + private mRunningCommands = new Map(); + constructor(private configHandler: CCConfigHandler, private outputChannel: CCOutputChannel) { if (this.configHandler.configuration.useRemoteClient.value === true) { this.mExecCmd = new Cleartool( @@ -246,7 +248,7 @@ export class ClearCase { const args: CCArgs = new CCArgs(["login"].concat(this.mExecCmd.credentials())); const path: string = workspace.workspaceFolders !== undefined ? workspace.workspaceFolders[0].uri.fsPath : ""; - await this.runCleartoolCommand(args, path, (datas) => { + await this.runCleartoolCommand("loginWebview", args, path, (datas) => { this.outputChannel.appendLine(datas.join(" "), LogLevel.Information); return true; }); @@ -258,6 +260,7 @@ export class ClearCase { async execOnSCMFile(docs: Uri[], func: (arg: Uri[]) => void): Promise { await this.runCleartoolCommand( + "execOnScmFile", new CCArgs(["ls"], [docs[0]?.fsPath]), dirname(docs[0]?.fsPath), null, @@ -357,7 +360,9 @@ export class ClearCase { } try { - await this.runCleartoolCommand(cmd, dirname(docs[0]?.fsPath), null, () => this.mUpdateEvent.fire(docs)); + await this.runCleartoolCommand("checkoutFile", cmd, dirname(docs[0]?.fsPath), null, () => + this.mUpdateEvent.fire(docs) + ); } catch (error) { this.outputChannel.appendLine("Clearcase error: runCleartoolCommand: " + getErrorMessage(error), LogLevel.Error); window.showErrorMessage(`${getErrorMessage(error)}`, { modal: false }); @@ -441,8 +446,12 @@ export class ClearCase { return this.wslPath(d.fsPath, false); }); - await this.runCleartoolCommand(new CCArgs(["unco", rm], files), dirname(docs[0]?.fsPath), null, () => - this.mUpdateEvent.fire(docs) + await this.runCleartoolCommand( + "undoCheckoutFile", + new CCArgs(["unco", rm], files), + dirname(docs[0]?.fsPath), + null, + () => this.mUpdateEvent.fire(docs) ); } @@ -451,8 +460,12 @@ export class ClearCase { return this.wslPath(d.fsPath, false); }); try { - await this.runCleartoolCommand(new CCArgs(["mkelem", "-mkp", "-nc"], files), dirname(docs[0]?.fsPath), null, () => - this.mUpdateEvent.fire(docs) + await this.runCleartoolCommand( + "createVersionedObject", + new CCArgs(["mkelem", "-mkp", "-nc"], files), + dirname(docs[0]?.fsPath), + null, + () => this.mUpdateEvent.fire(docs) ); } catch (error) { window.showErrorMessage(`${getErrorMessage(error)}`, { modal: false }); @@ -473,8 +486,12 @@ export class ClearCase { async cancelHijackedObject(docs: Uri[]): Promise { if (this.mViewType === ViewType.Snapshot) { for (const d of docs) { - await this.runCleartoolCommand(new CCArgs(["update", "-overwrite"], [d.fsPath]), dirname(d.fsPath), null, () => - this.mUpdateEvent.fire([d]) + await this.runCleartoolCommand( + "cancelHijacked", + new CCArgs(["update", "-overwrite"], [d.fsPath]), + dirname(d.fsPath), + null, + () => this.mUpdateEvent.fire([d]) ); } } @@ -572,7 +589,9 @@ export class ClearCase { } private async doCheckinFiles(args: CCArgs, docs: Uri[]) { - await this.runCleartoolCommand(args, dirname(docs[0]?.fsPath), null, () => this.mUpdateEvent.fire(docs)); + await this.runCleartoolCommand("checkinFiles", args, dirname(docs[0]?.fsPath), null, () => + this.mUpdateEvent.fire(docs) + ); if (this.configHandler.configuration.useLabelAtCheckin.value) { const newLabel = await CCUIControl.showCreateLabelInput(); if (newLabel !== "") { @@ -586,13 +605,23 @@ export class ClearCase { versionTree(docs: Uri[]): void { for (const doc of docs) { - this.runCleartoolCommand(new CCArgs(["lsvtree", "-graphical"], [doc.fsPath]), dirname(doc.fsPath), null); + this.runCleartoolCommand( + "versionTree", + new CCArgs(["lsvtree", "-graphical"], [doc.fsPath]), + dirname(doc.fsPath), + null + ); } } diffWithPrevious(docs: Uri[]): void { for (const doc of docs) { - this.runCleartoolCommand(new CCArgs(["diff", "-graph", "-pred"], [doc.fsPath]), dirname(doc.fsPath), null); + this.runCleartoolCommand( + "diffWithPrevious", + new CCArgs(["diff", "-graph", "-pred"], [doc.fsPath]), + dirname(doc.fsPath), + null + ); } } @@ -610,7 +639,7 @@ export class ClearCase { const runInWsl = this.isRunningInWsl(); const cmdOpts = lscoArgTmpl.split(" "); const cmd: CCArgs = new CCArgs(["lsco", ...cmdOpts]); - await this.runCleartoolCommand(cmd, wsf, null, (_code: number, output: string) => { + await this.runCleartoolCommand("findCheckouts", cmd, wsf, null, (_code: number, output: string) => { if (output.length > 0) { const results: string[] = output.trim().split(/\r\n|\r|\n/); resNew = results.map((e) => { @@ -646,7 +675,7 @@ export class ClearCase { const runInWsl = this.isRunningInWsl(); const cmdOpts = lscoArgTmpl.split(" "); const cmd: CCArgs = new CCArgs([...cmdOpts]); - await this.runCleartoolCommand(cmd, wsf, null, (_code: number, output: string) => { + await this.runCleartoolCommand("findViewPrivate", cmd, wsf, null, (_code: number, output: string) => { if (output.length > 0) { const suff = this.configHandler.configuration.viewPrivateFileSuffixes.value; const suffRe = new RegExp(suff, "i"); @@ -690,7 +719,7 @@ export class ClearCase { const runInWsl = this.isRunningInWsl(); const cmdOpts = lscoArgTmpl.split(" "); const cmd: CCArgs = new CCArgs([...cmdOpts]); - await this.runCleartoolCommand(cmd, wsf, null, (_code: number, output: string) => { + await this.runCleartoolCommand("findHijacked", cmd, wsf, null, (_code: number, output: string) => { if (output.length > 0) { const results: string[] = output.trim().split(/\r\n|\r|\n/); resNew = results @@ -722,39 +751,6 @@ export class ClearCase { return resNew; } - /** - * Searching view private objects in all workspace folders of the current project. - * The result is filtered by the configuration 'ViewPrivateFileSuffixes' - */ - async findUntracked(pathObj: Uri | undefined): Promise { - try { - if (pathObj === undefined) { - return; - } - const cmd: CCArgs = new CCArgs(["ls", "-view_only", "-short", "-r"]); - await this.runCleartoolCommand(cmd, pathObj.fsPath, (data: string[]) => { - data.forEach((val) => { - let f = val; - if (val.match(/@@/g) !== null) { - const p = val.split("@@"); - if (p[1].match(/checkedout/gi) === null) { - f = p[0]; - } else { - f = ""; - } - } - if (f !== "") { - const p = join(pathObj.fsPath, f); - this.untrackedList.addStringByKey(p, pathObj.fsPath); - } - }); - }); - } catch (error) { - this.outputChannel.appendLine(getErrorMessage(error), LogLevel.Error); - window.showErrorMessage(`${getErrorMessage(error)}`, { modal: false }); - } - } - findCheckoutsGui(path: string): void { exec('clearfindco "' + path + '"'); } @@ -777,10 +773,16 @@ export class ClearCase { try { for (const p of workspace.workspaceFolders) { const cmd: CCArgs = new CCArgs(["catcs"]); - await this.runCleartoolCommand(cmd, p.uri.fsPath, null, (code: number, _output: string, error: string) => { - // Success only if command exit code is 0 and nothing on stderr - result = code === 0 && error.length === 0; - }); + await this.runCleartoolCommand( + "hasConfigspec", + cmd, + p.uri.fsPath, + null, + (code: number, _output: string, error: string) => { + // Success only if command exit code is 0 and nothing on stderr + result = code === 0 && error.length === 0; + } + ); if (result !== false) { break; } @@ -818,6 +820,7 @@ export class ClearCase { if (iUri !== undefined && this.isView === true) { const cwd = dirname(iUri.fsPath); await this.runCleartoolCommand( + "getVersionInformation", new CCArgs(["ls"], [iUri.fsPath]), cwd, null, @@ -902,6 +905,7 @@ export class ClearCase { let errorRes = ""; await this.runCleartoolCommand( + "updateObject", new CCArgs(["update"], [updateFsObj]), cwd, () => this.mUpdateEvent.fire([filePath]), @@ -943,6 +947,7 @@ export class ClearCase { const fileP = this.wslPath(filePath, false); await this.runCleartoolCommand( + "getAnnotationFileContent", new CCArgs(["annotate", "-out", "-", "-nhe", "-fmt", `"${fmt}${sep}"`, `${fileP}`]), workspace.workspaceFolders[0].uri.fsPath, null, @@ -974,6 +979,7 @@ export class ClearCase { if (workspace.workspaceFolders !== undefined) { let errorRes = ""; await this.runCleartoolCommand( + "getCurrentActivity", new CCArgs(["lsactivity", "-cac", "-fmt", `"%n"`]), workspace.workspaceFolders[0].uri.fsPath, null, @@ -995,6 +1001,7 @@ export class ClearCase { if (workspace.workspaceFolders !== undefined) { let errorRes = ""; await this.runCleartoolCommand( + "getQuickPickActivities", new CCArgs(["lsactivity"]), workspace.workspaceFolders[0].uri.fsPath, null, @@ -1056,6 +1063,7 @@ export class ClearCase { id = actvID; } await this.runCleartoolCommand( + "setViewActivities", new CCArgs(["setactivity", `${id}`]), workspace.workspaceFolders[0].uri.fsPath, null, @@ -1075,6 +1083,7 @@ export class ClearCase { const version = new CCVersionType(); if (fsPath !== "") { await this.runCleartoolCommand( + "getFilePredecessorVersion", new CCArgs(["describe", "-fmt", "%[version_predecessor]p", fsPath]), dirname(fsPath), null, @@ -1103,6 +1112,7 @@ export class ClearCase { } if (workspace.workspaceFolders !== undefined) { await this.runCleartoolCommand( + "readFileAtVersion", new CCArgs(["get", "-to", tempFile], [fsPath], version), workspace.workspaceFolders[0].uri.fsPath, null, @@ -1120,6 +1130,7 @@ export class ClearCase { } private async runCleartoolCommand( + cmdId: string, cmd: CCArgs, cwd: string, onData: ((data: string[]) => void) | null, @@ -1139,8 +1150,13 @@ export class ClearCase { const outputChannel = this.outputChannel; - outputChannel.appendLine(cmd.getCmd().toString(), LogLevel.Trace); - const command = spawn(executable, cmd.getCmd(), { cwd: cwd, env: process.env }); + outputChannel.appendLine(cmd.getCmd().toString(), LogLevel.Debug); + const command = spawn(executable, cmd.getCmd(), { cwd: cwd, env: process.env, detached: true }); + + // remove old running command + this.killRunningCommand(cmdId, command.pid); + this.mRunningCommands.set(cmdId, command); + outputChannel.appendLine(`Command ${cmdId} (${command.pid}) started`, LogLevel.Trace); let allData: Buffer = Buffer.alloc(0); let cmdErrMsg = ""; @@ -1163,17 +1179,25 @@ export class ClearCase { cmdErrMsg = `${cmdErrMsg}${msg}`; }); - command.on("close", (code) => { + command.on("close", (code, signal) => { + outputChannel.appendLine(`Command ${cmdId} (${command.pid}), with Signal (${signal}) deleted`, LogLevel.Trace); + if (this.mRunningCommands.has(cmdId)) { + if (this.mRunningCommands.get(cmdId)?.pid === command.pid) { + this.mRunningCommands.delete(cmdId); + outputChannel.appendLine(`Command ${cmdId} (${command.pid}) deleted`, LogLevel.Trace); + } + outputChannel.appendLine(`Command ${cmdId} (${command.pid}) finished`, LogLevel.Trace); + } if (cmdErrMsg !== "") { // If something was printed on stderr, log it, regardless of the exit code outputChannel.appendLine(`exit code ${code}, stderr: ${cmdErrMsg}`, LogLevel.Error); } else { - outputChannel.appendLine(`${allData.toString()}`, LogLevel.Trace); + outputChannel.appendLine(`${allData.toString()}`, LogLevel.Debug); } - if (code !== 0 && this.isView && cmdErrMsg !== "") { + if (code !== null && code !== 0 && this.isView && cmdErrMsg !== "") { reject(cmdErrMsg); } - if (typeof onFinished === "function") { + if (signal !== "SIGKILL" && typeof onFinished === "function") { onFinished(code, allData.toString(), cmdErrMsg); } resolve(undefined); @@ -1181,6 +1205,32 @@ export class ClearCase { }); } + private async killRunningCommand(cmdId: string, pid: number): Promise { + if (this.mRunningCommands.has(cmdId)) { + const cmd = this.mRunningCommands.get(cmdId); + if (cmd && (cmd.pid === pid || pid === 0)) { + const pidId = cmd.pid; + this.outputChannel.appendLine(`Going to kill ${cmdId} (${pidId})`, LogLevel.Trace); + if (process.platform === "win32") { + exec(`taskkill /PID ${pidId} /T /F`); + } else { + process.kill(-pidId, "SIGKILL"); + } + this.outputChannel.appendLine(`Command ${cmdId} (${pidId}) killed`, LogLevel.Trace); + this.mRunningCommands.delete(cmdId); + this.outputChannel.appendLine(`Command ${cmdId} (${pidId}) deleted`, LogLevel.Trace); + } + } + } + + public killUpdateFindViewPrivate(): void { + this.killRunningCommand("findViewPrivate", 0); + } + + public killUpdateFindHijacked(): void { + this.killRunningCommand("findHijacked", 0); + } + private async detectViewType(): Promise { let lines: string[] = []; let viewType: ViewType = ViewType.Unknown; @@ -1193,6 +1243,7 @@ export class ClearCase { }; if (workspace.workspaceFolders !== undefined) { await this.runCleartoolCommand( + "detectViewType", new CCArgs(this.lsView), workspace.workspaceFolders[0].uri.fsPath, null, @@ -1317,6 +1368,7 @@ export class ClearCase { const args = new CCArgs(["lstype", `lbtype:${newLabel}`]); try { await this.runCleartoolCommand( + "existsLabelType", args, dirname(doc.fsPath), null, @@ -1339,9 +1391,15 @@ export class ClearCase { if ((await this.existsLabelType(doc, newLabel)) === false) { const args = new CCArgs(["mklbtype", "-nc", newLabel]); try { - await this.runCleartoolCommand(args, dirname(doc.fsPath), null, (_code: number, output: string) => { - this.outputChannel.appendLine(output); - }); + await this.runCleartoolCommand( + "createLabelType", + args, + dirname(doc.fsPath), + null, + (_code: number, output: string) => { + this.outputChannel.appendLine(output); + } + ); } catch (e) { this.outputChannel.appendLine(getErrorMessage(e), LogLevel.Error); } @@ -1354,9 +1412,15 @@ export class ClearCase { if ((await this.existsLabelType(doc, newLabel)) === true) { const args = new CCArgs(["mklabel", "-replace", newLabel], [doc.fsPath]); try { - await this.runCleartoolCommand(args, dirname(doc.fsPath), null, (_code: number, output: string) => { - this.outputChannel.appendLine(output); - }); + await this.runCleartoolCommand( + "applyLabel", + args, + dirname(doc.fsPath), + null, + (_code: number, output: string) => { + this.outputChannel.appendLine(output); + } + ); } catch (e) { this.outputChannel.appendLine(getErrorMessage(e), LogLevel.Error); } @@ -1365,10 +1429,12 @@ export class ClearCase { } public async getVersionsOfFile(file: Uri): Promise { + const version = await this.getVersionInformation(file); let retVal: string[] = []; if (file && file.fsPath !== "") { await this.runCleartoolCommand( - new CCArgs(["lsvtree", "-short", file.fsPath]), + "getVersionsOfFile", + new CCArgs(["lsvtree", "-short"], [file.fsPath], version.version), dirname(file.fsPath), null, (_code: number, output: string) => { diff --git a/src/extension.ts b/src/extension.ts index f4da483..389ef94 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,12 +27,12 @@ async function _activate(context: ExtensionContext, disposables: Disposable[]) { provider.bindEvents(); provider.bindCommands(); - provider.onWindowChanged(() => { - provider.updateContextResources(); + provider.onWindowChanged((valid: boolean) => { + provider.updateContextResources(valid); }, provider); - provider.updateIsView().then(() => { - provider.updateContextResources(); + provider.updateIsView().then((valid: boolean) => { + provider.updateContextResources(valid); }); const uiInfo = new UIInformation(configHandler, window.activeTextEditor, provider.clearCase); diff --git a/src/test/suite/cleartool.test.ts b/src/test/suite/cleartool.test.ts index 7277d19..2bb7392 100644 --- a/src/test/suite/cleartool.test.ts +++ b/src/test/suite/cleartool.test.ts @@ -47,6 +47,11 @@ suite("Cleartool Commands Test Suite", () => { writeFileSync(path.join(testDir, "simple04.txt"), ""); writeFileSync(path.join(testDir, "simple04_ro.txt"), ""); chmodSync(path.join(testDir, "simple04_ro.txt"), 0o555); + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 800); + }); }); after(() => { @@ -63,11 +68,16 @@ suite("Cleartool Commands Test Suite", () => { configHandler = new CCConfigHandler(); configHandler.configuration.executable.value = path.join(__dirname, "../../../src/test/", "bin/cleartool.sh"); - configHandler.configuration.logLevel.value = LogLevel.Trace; + configHandler.configuration.logLevel.value = LogLevel.Debug; configHandler.configuration.useLabelAtCheckin.value = false; provider = new CCScmProvider(extensionContext, outputChannel, configHandler); await provider.init(); outputChannel.clear(); + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 100); + }); }); test("Cleartool change executable", () => { diff --git a/src/test/suite/outputchannel.test.ts b/src/test/suite/outputchannel.test.ts index 50ca10a..a20a114 100644 --- a/src/test/suite/outputchannel.test.ts +++ b/src/test/suite/outputchannel.test.ts @@ -83,7 +83,7 @@ suite("Outputchannel Test Suite", () => { await provider.clearCase?.checkinFile([file]); assert.strictEqual(outputChannelBase.getLine(0), `ci,-nc,${path.join(testDir, "simple01.txt")}\n`); assert.strictEqual( - outputChannelBase.getLine(1), + outputChannelBase.getLastLine(), `Checked in "${path.join(testDir, "simple01.txt")}" version "/main/dev_01/2".\n` ); }); @@ -125,7 +125,7 @@ suite("Outputchannel Test Suite", () => { const fileUri = vscode.Uri.parse(file); await provider.clearCase?.checkoutFile([fileUri]); assert.strictEqual( - outputChannelBase.getLine(1), + outputChannelBase.getLastLine(), `exit code 0, stderr: cleartool: Error: Element "${file}" is already checked out to view "myview".\n` ); });