diff --git a/packages/zowe-explorer/.vscodeignore b/packages/zowe-explorer/.vscodeignore index 931447bc41..0a539b22c5 100644 --- a/packages/zowe-explorer/.vscodeignore +++ b/packages/zowe-explorer/.vscodeignore @@ -8,7 +8,7 @@ !resources/**/*.png !resources/**/*.svg !prebuilds/** -!webviews/*/dist/ +!src/webviews/dist/** !CHANGELOG.md !LICENSE diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 3c76988b82..b4815bcd01 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### Bug fixes - Fixed submitting local JCL using command pallet option `Zowe Explorer: Submit JCL` by adding a check for chosen profile returned to continue the action. [#1625](https://github.com/zowe/vscode-extension-for-zowe/issues/1625) +- Fixed conflict resolution being skipped if local and remote file have different contents but are the same size. [#2496](https://github.com/zowe/vscode-extension-for-zowe/issues/2496) ## `2.11.0` diff --git a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts index 63bb7cd047..07d17faaf5 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts @@ -293,12 +293,12 @@ export function createTextDocument(name: string, sessionNode?: ZoweDatasetNode | isDirty: null, isClosed: null, save: null, - eol: null, + eol: 1, lineCount: null, lineAt: null, offsetAt: null, - positionAt: null, - getText: jest.fn(), + positionAt: jest.fn(), + getText: jest.fn().mockReturnValue(""), getWordRangeAtPosition: null, validateRange: null, validatePosition: null, diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts index 09c1e79759..93aeba652e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts @@ -1454,6 +1454,74 @@ describe("Dataset Actions Unit Tests - Function saveFile", () => { logSpy.mockClear(); commandSpy.mockClear(); }); + + it("Checking common dataset saving failed due to conflict with server version when file size has not changed", async () => { + globals.defineGlobals(""); + createGlobalMocks(); + const blockMocks = createBlockMocks(); + const node = new ZoweDatasetNode( + "HLQ.TEST.AFILE", + vscode.TreeItemCollapsibleState.None, + blockMocks.datasetSessionNode, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + blockMocks.datasetSessionNode.children.push(node); + + mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); + blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); + mocked(zowe.List.dataSet).mockResolvedValue({ + success: true, + commandResponse: "", + apiResponse: { + items: [{ dsname: "HLQ.TEST.AFILE" }], + }, + }); + mocked(zowe.Upload.pathToDataSet).mockResolvedValueOnce({ + success: false, + commandResponse: "Rest API failure with HTTP(S) status 412", + apiResponse: [], + }); + + mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { + return callback(); + }); + const profile = blockMocks.imperativeProfile; + profile.profile.encoding = 1047; + blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + Object.defineProperty(wsUtils, "markDocumentUnsaved", { + value: jest.fn(), + configurable: true, + }); + Object.defineProperty(context, "isTypeUssTreeNode", { + value: jest.fn().mockReturnValueOnce(false), + configurable: true, + }); + Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { + value: jest.fn(), + configurable: true, + }); + + const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); + (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); + const logSpy = jest.spyOn(ZoweLogger, "warn"); + const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); + const applyEditSpy = jest.spyOn(vscode.workspace, "applyEdit"); + jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 0 } as any); + + await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); + + expect(logSpy).toBeCalledWith("Remote file has changed. Presenting with way to resolve file."); + expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); + expect(commandSpy).toBeCalledWith("workbench.files.action.compareWithSaved"); + expect(applyEditSpy).toHaveBeenCalledTimes(2); + logSpy.mockClear(); + commandSpy.mockClear(); + applyEditSpy.mockClear(); + }); }); describe("Dataset Actions Unit Tests - Function showAttributes", () => { diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index 98b6a293ad..354c9a6732 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -11,6 +11,7 @@ // Generic utility functions related to all node types. See ./src/utils.ts for other utility functions. +import * as fs from "fs"; import * as vscode from "vscode"; import * as path from "path"; import * as globals from "../globals"; @@ -383,8 +384,27 @@ export async function compareFileContent( responseTimeout: prof.profile?.responseTimeout, }); } + + // If local and remote file size are the same, then VS Code won't detect + // there is a conflict and remote changes may get overwritten. To work + // around this limitation of VS Code, when the sizes are identical we + // temporarily add a trailing newline byte to the local copy which forces + // the file size to be different. This is a terrible hack but it works. + // See https://github.com/microsoft/vscode/issues/119002 + const oldSize = doc.getText().length; + const newSize = fs.statSync(doc.fileName).size; + if (newSize === oldSize) { + const edits = new vscode.WorkspaceEdit(); + edits.insert(doc.uri, doc.positionAt(oldSize), doc.eol.toString()); + await vscode.workspace.applyEdit(edits); + } ZoweLogger.warn(localize("saveFile.etagMismatch.log.warning", "Remote file has changed. Presenting with way to resolve file.")); - vscode.commands.executeCommand("workbench.files.action.compareWithSaved"); + await vscode.commands.executeCommand("workbench.files.action.compareWithSaved"); + if (newSize === oldSize) { + const edits2 = new vscode.WorkspaceEdit(); + edits2.delete(doc.uri, new vscode.Range(doc.positionAt(oldSize), doc.positionAt(oldSize + doc.eol.toString().length))); + await vscode.workspace.applyEdit(edits2); + } // re-assign etag, so that it can be used with subsequent requests const downloadEtag = downloadResponse?.apiResponse?.etag; if (node && downloadEtag !== node.getEtag()) {