diff --git a/extension/package.json b/extension/package.json index 82b2b72ea9..b5bc525cce 100644 --- a/extension/package.json +++ b/extension/package.json @@ -202,6 +202,12 @@ "command": "dvc.renameTarget", "category": "DVC" }, + { + "title": "%command.resetWorkspace%", + "command": "dvc.resetWorkspace", + "category": "DVC", + "icon": "$(discard)" + }, { "title": "%command.runExperiment%", "command": "dvc.runExperiment", @@ -395,6 +401,10 @@ "command": "dvc.renameTarget", "when": "false" }, + { + "command": "dvc.resetWorkspace", + "when": "false" + }, { "command": "dvc.runExperiment", "when": "dvc.commands.available == true" @@ -442,23 +452,28 @@ ], "scm/title": [ { - "command": "dvc.checkout", - "group": "navigation", + "command": "dvc.resetWorkspace", + "group": "navigation@1", "when": "scmProvider == dvc && dvc.commands.available == true" }, { "command": "dvc.commit", - "group": "navigation", + "group": "navigation@2", "when": "scmProvider == dvc && dvc.commands.available == true" }, { "command": "dvc.pull", - "group": "navigation", + "group": "navigation@3", "when": "scmProvider == dvc && dvc.commands.available == true" }, { "command": "dvc.push", - "group": "navigation", + "group": "navigation@4", + "when": "scmProvider == dvc && dvc.commands.available == true" + }, + { + "command": "dvc.checkout", + "group": "DVC", "when": "scmProvider == dvc && dvc.commands.available == true" } ], diff --git a/extension/package.nls.json b/extension/package.nls.json index fe9aa68803..e74b4f9557 100644 --- a/extension/package.nls.json +++ b/extension/package.nls.json @@ -31,6 +31,7 @@ "command.removeExperimentsTableSorts": "Remove Sort(s) From Experiments Table", "command.removeTarget": "Remove", "command.renameTarget": "Rename", + "command.resetWorkspace": "Reset the Workspace", "command.runExperiment": "Run Experiment", "command.runQueuedExperiments": "Run All Queued Experiments", "command.runResetExperiment": "Run and Reset Experiment", diff --git a/extension/src/commands/external.ts b/extension/src/commands/external.ts index a0d6bf3306..d7357a1809 100644 --- a/extension/src/commands/external.ts +++ b/extension/src/commands/external.ts @@ -42,6 +42,8 @@ export enum RegisteredCommands { DELETE_TARGET = 'dvc.deleteTarget', MOVE_TARGETS = 'dvc.moveTargets', + RESET_WORKSPACE = 'dvc.resetWorkspace', + TRACKED_EXPLORER_OPEN_FILE = 'dvc.views.trackedExplorerTree.openFile', TRACKED_EXPLORER_COMPARE_SELECTED = 'dvc.compareSelected', TRACKED_EXPLORER_COPY_FILE_PATH = 'dvc.copyFilePath', diff --git a/extension/src/git.ts b/extension/src/git.ts index 079262f298..b4ef07179b 100644 --- a/extension/src/git.ts +++ b/extension/src/git.ts @@ -43,3 +43,18 @@ export const getAllUntracked = async ( ]) return new Set([...files, ...dirs]) } + +export const gitResetWorkspace = async ( + repositoryRoot: string +): Promise => { + await executeProcess({ + args: ['reset', '--hard', 'HEAD'], + cwd: repositoryRoot, + executable: 'git' + }) + await executeProcess({ + args: ['clean', '-f', '-d', '-q'], + cwd: repositoryRoot, + executable: 'git' + }) +} diff --git a/extension/src/repository/commands/index.ts b/extension/src/repository/commands/index.ts index c5cb4de85a..5010d225a6 100644 --- a/extension/src/repository/commands/index.ts +++ b/extension/src/repository/commands/index.ts @@ -1,7 +1,14 @@ import { relative } from 'path' import { Uri } from 'vscode' import { tryThenMaybeForce } from '../../cli/actions' -import { CommandId, InternalCommands } from '../../commands/internal' +import { Flag } from '../../cli/args' +import { + AvailableCommands, + CommandId, + InternalCommands +} from '../../commands/internal' +import { gitResetWorkspace } from '../../git' +import { getWarningResponse } from '../../vscode/modal' export type Resource = { dvcRoot: string @@ -40,3 +47,28 @@ export const getRootCommand = return tryThenMaybeForce(internalCommands, commandId, cwd) } + +export const getResetRootCommand = + (internalCommands: InternalCommands): RootCommand => + async ({ rootUri }) => { + const cwd = rootUri.fsPath + + const response = await getWarningResponse( + 'Are you sure you want to discard ALL workspace changes?\n' + + 'This is IRREVERSIBLE!\n' + + 'Your current working set will be FOREVER LOST if you proceed.', + 'Discard Changes' + ) + + if (response !== 'Discard Changes') { + return + } + + await gitResetWorkspace(cwd) + + return internalCommands.executeCommand( + AvailableCommands.CHECKOUT, + cwd, + Flag.FORCE + ) + } diff --git a/extension/src/repository/commands/register.ts b/extension/src/repository/commands/register.ts index 1dd407dbfa..7f092a5ff1 100644 --- a/extension/src/repository/commands/register.ts +++ b/extension/src/repository/commands/register.ts @@ -1,5 +1,6 @@ import { commands } from 'vscode' import { + getResetRootCommand, getResourceCommand, getRootCommand, getSimpleResourceCommand, @@ -7,7 +8,10 @@ import { Root } from '.' import { tryThenMaybeForce } from '../../cli/actions' -import { RegisteredCliCommands } from '../../commands/external' +import { + RegisteredCliCommands, + RegisteredCommands +} from '../../commands/external' import { AvailableCommands, InternalCommands } from '../../commands/internal' const registerResourceCommands = (internalCommands: InternalCommands): void => { @@ -52,6 +56,11 @@ const registerRootCommands = (internalCommands: InternalCommands) => { RegisteredCliCommands.PUSH, getRootCommand(internalCommands, AvailableCommands.PUSH) ) + + internalCommands.registerExternalCommand( + RegisteredCommands.RESET_WORKSPACE, + getResetRootCommand(internalCommands) + ) } export const registerRepositoryCommands = ( diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index 62db3cc4bf..ead76492b5 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -93,6 +93,7 @@ export interface IEventNamePropertyMapping { [EventName.PUSH]: undefined [EventName.REMOVE_TARGET]: undefined [EventName.RENAME_TARGET]: undefined + [EventName.RESET_WORKSPACE]: undefined [EventName.TRACKED_EXPLORER_COMPARE_SELECTED]: undefined [EventName.TRACKED_EXPLORER_COPY_FILE_PATH]: undefined diff --git a/extension/src/test/suite/repository/sourceControlManagement.test.ts b/extension/src/test/suite/repository/sourceControlManagement.test.ts index 854a48e94a..2ddf8f9d72 100644 --- a/extension/src/test/suite/repository/sourceControlManagement.test.ts +++ b/extension/src/test/suite/repository/sourceControlManagement.test.ts @@ -6,7 +6,11 @@ import { window, commands, Uri, MessageItem } from 'vscode' import { Disposable } from '../../../extension' import { CliExecutor } from '../../../cli/executor' import { dvcDemoPath } from '../util' -import { RegisteredCliCommands } from '../../../commands/external' +import { + RegisteredCliCommands, + RegisteredCommands +} from '../../../commands/external' +import * as ProcessExecution from '../../../processExecution' suite('Source Control Management Test Suite', () => { const disposable = Disposable.fn() @@ -179,5 +183,51 @@ suite('Source Control Management Test Suite', () => { expect(mockPush).to.be.calledOnce expect(mockShowErrorMessage).to.be.calledOnce }) + + it('should not reset the workspace if the user does not confirm', async () => { + const mockCheckout = stub(CliExecutor.prototype, 'checkout').resolves('') + const mockGitReset = stub(ProcessExecution, 'executeProcess').resolves('') + + const mockShowWarningMessage = stub( + window, + 'showWarningMessage' + ).resolves('' as unknown as MessageItem) + + await commands.executeCommand(RegisteredCommands.RESET_WORKSPACE, { + rootUri + }) + + expect(mockShowWarningMessage).to.be.calledOnce + expect(mockCheckout).not.to.be.called + expect(mockGitReset).not.to.be.called + }) + + it('should reset the workspace if the user confirms they want to', async () => { + const mockCheckout = stub(CliExecutor.prototype, 'checkout').resolves('') + const mockGitReset = stub(ProcessExecution, 'executeProcess').resolves('') + + const mockShowWarningMessage = stub( + window, + 'showWarningMessage' + ).resolves('Discard Changes' as unknown as MessageItem) + + await commands.executeCommand(RegisteredCommands.RESET_WORKSPACE, { + rootUri + }) + + expect(mockShowWarningMessage).to.be.calledOnce + expect(mockCheckout).to.be.calledOnce + expect(mockGitReset).to.be.calledTwice + expect(mockGitReset).to.be.calledWith({ + args: ['reset', '--hard', 'HEAD'], + cwd: dvcDemoPath, + executable: 'git' + }) + expect(mockGitReset).to.be.calledWith({ + args: ['clean', '-f', '-d', '-q'], + cwd: dvcDemoPath, + executable: 'git' + }) + }) }) })