diff --git a/extension/src/Repository/DecorationProvider.ts b/extension/src/Repository/DecorationProvider.ts index 59eefaf75b..6a24801771 100644 --- a/extension/src/Repository/DecorationProvider.ts +++ b/extension/src/Repository/DecorationProvider.ts @@ -14,14 +14,21 @@ import { isStringInEnum } from '../util' export type DecorationState = Record> enum Status { + ADDED = 'added', DELETED = 'deleted', MODIFIED = 'modified', - NEW = 'new', NOT_IN_CACHE = 'notInCache', + STAGE_MODIFIED = 'stageModified', TRACKED = 'tracked' } export class DecorationProvider implements FileDecorationProvider { + private static DecorationAdded: FileDecoration = { + badge: 'A', + color: new ThemeColor('gitDecoration.addedResourceForeground'), + tooltip: 'DVC added' + } + private static DecorationDeleted: FileDecoration = { badge: 'D', color: new ThemeColor('gitDecoration.deletedResourceForeground'), @@ -34,10 +41,10 @@ export class DecorationProvider implements FileDecorationProvider { tooltip: 'DVC modified' } - private static DecorationNew: FileDecoration = { - badge: 'A', - color: new ThemeColor('gitDecoration.addedResourceForeground'), - tooltip: 'DVC added' + private static DecorationStageModified: FileDecoration = { + badge: 'M', + color: new ThemeColor('gitDecoration.stageModifiedResourceForeground'), + tooltip: 'DVC staged modified' } private static DecorationNotInCache: FileDecoration = { @@ -86,10 +93,11 @@ export class DecorationProvider implements FileDecorationProvider { } private decorationMapping: Partial> = { + added: DecorationProvider.DecorationAdded, deleted: DecorationProvider.DecorationDeleted, modified: DecorationProvider.DecorationModified, - new: DecorationProvider.DecorationNew, - notInCache: DecorationProvider.DecorationNotInCache + notInCache: DecorationProvider.DecorationNotInCache, + stageModified: DecorationProvider.DecorationStageModified } public provideFileDecoration(uri: Uri): FileDecoration | undefined { diff --git a/extension/src/Repository/State.test.ts b/extension/src/Repository/State.test.ts new file mode 100644 index 0000000000..c19e7891f1 --- /dev/null +++ b/extension/src/Repository/State.test.ts @@ -0,0 +1,79 @@ +import { Disposable, Disposer } from '@hediet/std/disposable' +import { join, sep } from 'path' +import { mocked } from 'ts-jest/utils' +import { RepositoryState, StatusOutput } from './State' + +jest.mock('@hediet/std/disposable') + +const mockedDisposable = mocked(Disposable) + +beforeEach(() => { + jest.resetAllMocks() + + mockedDisposable.fn.mockReturnValue(({ + track: function(disposable: T): T { + return disposable + } + } as unknown) as (() => void) & Disposer) +}) + +describe('RepositoryState', () => { + const dvcRoot = join(__dirname, '..', '..', 'demo') + + describe('update', () => { + it('should deal with the differences between diff and status', () => { + const file = join('data', 'MNIST', 'raw', 'train-labels-idx1-ubyte') + const predictions = 'predictions.json' + const diff = { + added: [], + deleted: [{ path: file }], + modified: [ + { path: join('data', 'MNIST', 'raw') + sep }, + { path: 'logs' + sep }, + { path: join('logs', 'acc.tsv') }, + { path: join('logs', 'loss.tsv') }, + { path: 'model.pt' }, + { path: predictions } + ], + renamed: [], + 'not in cache': [] + } + + const status = ({ + train: [ + { 'changed deps': { 'data/MNIST': 'modified' } }, + { + 'changed outs': { 'predictions.json': 'modified', logs: 'modified' } + }, + 'always changed' + ], + 'data/MNIST/raw.dvc': [ + { 'changed outs': { 'data/MNIST/raw': 'modified' } } + ] + } as unknown) as StatusOutput + + const repositoryState = new RepositoryState(dvcRoot) + repositoryState.updateStatus(diff, status) + + const emptySet = new Set() + + expect(repositoryState).toEqual({ + added: emptySet, + dispose: Disposable.fn(), + dvcRoot, + deleted: new Set([join(dvcRoot, file)]), + notInCache: emptySet, + stageModified: new Set([join(dvcRoot, 'model.pt')]), + modified: new Set([ + join(dvcRoot, 'data', 'MNIST', 'raw'), + join(dvcRoot, 'logs'), + join(dvcRoot, 'logs', 'acc.tsv'), + join(dvcRoot, 'logs', 'loss.tsv'), + join(dvcRoot, predictions) + ]), + tracked: emptySet, + untracked: emptySet + }) + }) + }) +}) diff --git a/extension/src/Repository/State.ts b/extension/src/Repository/State.ts new file mode 100644 index 0000000000..08e9750133 --- /dev/null +++ b/extension/src/Repository/State.ts @@ -0,0 +1,157 @@ +import { SourceControlManagementState } from './views/SourceControlManagement' +import { DecorationState } from './DecorationProvider' +import { DiffOutput, ListOutput } from '../cli/reader' +import { dirname, join, resolve } from 'path' +import { isDirectory } from '../fileSystem' +import { Disposable } from '@hediet/std/disposable' + +export enum Status { + ADDED = 'added', + DELETED = 'deleted', + MODIFIED = 'modified', + NOT_IN_CACHE = 'not in cache' +} + +enum ChangedType { + CHANGED_OUTS = 'changed outs', + CHANGED_DEPS = 'changed deps' +} + +type PathStatus = Record + +type StageOrFileStatuses = Record + +type StatusesOrAlwaysChanged = StageOrFileStatuses | 'always changed' + +export type StatusOutput = Record + +export class RepositoryState + implements DecorationState, SourceControlManagementState { + public dispose = Disposable.fn() + + private dvcRoot: string + + public added: Set = new Set() + public deleted: Set = new Set() + public modified: Set = new Set() + public notInCache: Set = new Set() + public stageModified: Set = new Set() + public tracked: Set = new Set() + public untracked: Set = new Set() + + private filterRootDir(dirs: string[] = []) { + return dirs.filter(dir => dir !== this.dvcRoot) + } + + private getAbsolutePath(files: string[] = []): string[] { + return files.map(file => join(this.dvcRoot, file)) + } + + private getAbsoluteParentPath(files: string[] = []): string[] { + return this.filterRootDir( + files.map(file => join(this.dvcRoot, dirname(file))) + ) + } + + public updateTracked(listOutput: ListOutput[]): void { + const trackedPaths = listOutput.map(tracked => tracked.path) + + const absoluteTrackedPaths = this.getAbsolutePath(trackedPaths) + + this.tracked = new Set([ + ...absoluteTrackedPaths, + ...this.getAbsoluteParentPath(trackedPaths) + ]) + } + + private getChangedOutsStatuses( + fileOrStage: StatusesOrAlwaysChanged[] + ): PathStatus[] { + return fileOrStage + .map(entry => (entry as StageOrFileStatuses)?.[ChangedType.CHANGED_OUTS]) + .filter(value => value) + } + + private reduceStatuses( + reducedStatus: Partial>>, + statuses: PathStatus[] + ) { + return statuses.map(entry => + Object.entries(entry).map(([relativePath, status]) => { + const absolutePath = join(this.dvcRoot, relativePath) + const existingPaths = reducedStatus[status] || new Set() + reducedStatus[status] = existingPaths.add(absolutePath) + }) + ) + } + + private reduceToChangedOutsStatuses( + filteredStatusOutput: StatusOutput + ): Partial>> { + const statusReducer = ( + reducedStatus: Partial>>, + entry: StatusesOrAlwaysChanged[] + ): Partial>> => { + const statuses = this.getChangedOutsStatuses(entry) + + this.reduceStatuses(reducedStatus, statuses) + + return reducedStatus + } + + return Object.values(filteredStatusOutput).reduce(statusReducer, {}) + } + + private getDiffFromDvc( + statusOutput: StatusOutput + ): Partial>> { + return this.reduceToChangedOutsStatuses(statusOutput) + } + + private mapStatusToState(status?: { path: string }[]): Set { + return new Set(status?.map(entry => join(this.dvcRoot, entry.path))) + } + + private getModified( + diff: { path: string }[] | undefined, + filter: (path: string) => boolean + ) { + return new Set( + diff?.map(entry => resolve(this.dvcRoot, entry.path)).filter(filter) + ) + } + + public updateStatus( + diffOutput: DiffOutput, + statusOutput: StatusOutput + ): void { + this.added = this.mapStatusToState(diffOutput.added) + this.deleted = this.mapStatusToState(diffOutput.deleted) + this.notInCache = this.mapStatusToState(diffOutput['not in cache']) + + const status = this.getDiffFromDvc(statusOutput) + + const pathMatchesDvc = (path: string): boolean => { + if (isDirectory(path)) { + return !status.modified?.has(path) + } + return !( + status.modified?.has(path) || status.modified?.has(dirname(path)) + ) + } + + this.modified = this.getModified( + diffOutput.modified, + path => !pathMatchesDvc(path) + ) + this.stageModified = this.getModified(diffOutput.modified, pathMatchesDvc) + } + + public updateUntracked(untracked: Set): void { + this.untracked = untracked + } + + constructor(dvcRoot: string) { + this.dvcRoot = dvcRoot + } +} diff --git a/extension/src/Repository/index.test.ts b/extension/src/Repository/index.test.ts index 6888fbe344..6f89198767 100644 --- a/extension/src/Repository/index.test.ts +++ b/extension/src/Repository/index.test.ts @@ -4,9 +4,16 @@ import { Config } from '../Config' import { SourceControlManagement } from './views/SourceControlManagement' import { mocked } from 'ts-jest/utils' import { DecorationProvider } from './DecorationProvider' -import { Repository, RepositoryState, Status } from '.' -import { listDvcOnlyRecursive, ListOutput, status } from '../cli/reader' +import { Repository } from '.' +import { + diff, + DiffOutput, + listDvcOnlyRecursive, + ListOutput, + status +} from '../cli/reader' import { getAllUntracked } from '../git' +import { RepositoryState } from './State' jest.mock('@hediet/std/disposable') jest.mock('./views/SourceControlManagement') @@ -15,6 +22,7 @@ jest.mock('../cli/reader') jest.mock('../git') jest.mock('../fileSystem') +const mockedDiff = mocked(diff) const mockedListDvcOnlyRecursive = mocked(listDvcOnlyRecursive) const mockedStatus = mocked(status) const mockedGetAllUntracked = mocked(getAllUntracked) @@ -42,16 +50,25 @@ beforeEach(() => { } as unknown) as DecorationProvider }) - mockedDisposable.fn.mockReturnValueOnce(({ + mockedDisposable.fn.mockReturnValue(({ track: function(disposable: T): T { return disposable } } as unknown) as (() => void) & Disposer) + mockedStatus.mockResolvedValue({}) }) describe('Repository', () => { const dvcRoot = resolve(__dirname, '..', '..', 'demo') + const emptyDiff = { + added: [], + modified: [], + deleted: [], + renamed: [], + 'not in cache': [] + } + describe('ready', () => { it('should wait for the state to be ready before resolving', async () => { const logDir = 'logs' @@ -67,16 +84,19 @@ describe('Repository', () => { { path: rawDataDir } ] as ListOutput[]) - mockedStatus.mockResolvedValueOnce({ - train: [ - { 'changed deps': { 'data/MNIST': 'modified' } }, - { 'changed outs': { 'model.pt': 'modified', logs: 'modified' } }, - 'always changed' + mockedDiff.mockResolvedValueOnce({ + added: [], + deleted: [], + modified: [ + { path: model }, + { path: logDir }, + { path: logAcc }, + { path: logLoss }, + { path: MNISTDataDir } ], - 'data/MNIST/raw.dvc': [ - { 'changed outs': { 'data/MNIST/raw': 'modified' } } - ] - } as Record> | string)[]>) + 'not in cache': [], + renamed: [] + } as DiffOutput) const untracked = new Set([ resolve(dvcRoot, 'some', 'untracked', 'python.py') @@ -91,7 +111,13 @@ describe('Repository', () => { const repository = new Repository(dvcRoot, config, decorationProvider) await repository.isReady() - const modified = new Set([resolve(dvcRoot, rawDataDir)]) + const modified = new Set([ + resolve(dvcRoot, rawDataDir), + resolve(dvcRoot, logDir), + resolve(dvcRoot, logAcc), + resolve(dvcRoot, logLoss), + resolve(dvcRoot, model) + ]) const tracked = new Set([ resolve(dvcRoot, logAcc), resolve(dvcRoot, logLoss), @@ -108,7 +134,7 @@ describe('Repository', () => { pythonBinPath: undefined } - expect(mockedStatus).toBeCalledWith(expectedExecutionOptions) + expect(mockedDiff).toBeCalledWith(expectedExecutionOptions) expect(mockedGetAllUntracked).toBeCalledWith(dvcRoot) expect(mockedListDvcOnlyRecursive).toBeCalledWith( expectedExecutionOptions @@ -116,10 +142,11 @@ describe('Repository', () => { expect(repository.getState()).toEqual( expect.objectContaining({ + added: emptySet, dispose: Disposable.fn(), + dvcRoot, deleted: emptySet, notInCache: emptySet, - new: emptySet, modified, tracked, untracked @@ -129,9 +156,9 @@ describe('Repository', () => { }) describe('resetState', () => { - it('will not exclude changed outs from stages that are always changed', async () => { + it('should map the output of diff to the correct statuses', async () => { mockedListDvcOnlyRecursive.mockResolvedValueOnce([]) - mockedStatus.mockResolvedValueOnce({}) + mockedDiff.mockResolvedValueOnce(emptyDiff) mockedGetAllUntracked.mockResolvedValueOnce(new Set()) const config = ({ @@ -150,18 +177,12 @@ describe('Repository', () => { const logLoss = join(logDir, 'loss.tsv') const model = 'model.pt' - mockedStatus.mockResolvedValueOnce({ - train: [ - { - 'changed deps': { 'data/MNIST': 'modified', 'train.py': 'modified' } - }, - { 'changed outs': { 'model.pt': 'deleted' } }, - 'always changed' - ], - 'data/MNIST/raw.dvc': [ - { 'changed outs': { 'data/MNIST/raw': 'deleted' } } - ] - } as Record> | string)[]>) + mockedDiff.mockResolvedValueOnce(({ + added: [], + deleted: [{ path: model }, { path: dataDir }], + modified: [], + 'not in cache': [] + } as unknown) as DiffOutput) const emptySet = new Set() @@ -175,7 +196,7 @@ describe('Repository', () => { { path: model } ] as ListOutput[]) - expect(repository.getState()).toEqual(new RepositoryState()) + expect(repository.getState()).toEqual(new RepositoryState(dvcRoot)) await repository.resetState() @@ -197,18 +218,94 @@ describe('Repository', () => { pythonBinPath: undefined } - expect(mockedStatus).toBeCalledWith(expectedExecutionOptions) + expect(mockedDiff).toBeCalledWith(expectedExecutionOptions) expect(mockedGetAllUntracked).toBeCalledWith(dvcRoot) expect(mockedListDvcOnlyRecursive).toBeCalledWith( expectedExecutionOptions ) expect(repository.getState()).toEqual({ + added: emptySet, + deleted, dispose: Disposable.fn(), - new: emptySet, + dvcRoot, modified: emptySet, notInCache: emptySet, - deleted, + stageModified: emptySet, + tracked, + untracked: emptySet + }) + }) + + it('should handle an empty diff output', async () => { + mockedListDvcOnlyRecursive.mockResolvedValueOnce([]) + mockedDiff.mockResolvedValueOnce(emptyDiff) + mockedGetAllUntracked.mockResolvedValueOnce(new Set()) + + const config = ({ + getCliPath: () => undefined + } as unknown) as Config + const decorationProvider = new DecorationProvider() + + const repository = new Repository(dvcRoot, config, decorationProvider) + await repository.isReady() + + const dataDir = 'data/MNIST/raw' + const compressedDataset = join(dataDir, 't10k-images-idx3-ubyte.gz') + const dataset = join(dataDir, 't10k-images-idx3-ubyte') + const logDir = 'logs' + const logAcc = join(logDir, 'acc.tsv') + const logLoss = join(logDir, 'loss.tsv') + const model = 'model.pt' + + mockedDiff.mockResolvedValueOnce({}) + + const emptySet = new Set() + + mockedGetAllUntracked.mockResolvedValueOnce(emptySet) + + mockedListDvcOnlyRecursive.mockResolvedValueOnce([ + { path: compressedDataset }, + { path: dataset }, + { path: logAcc }, + { path: logLoss }, + { path: model } + ] as ListOutput[]) + + expect(repository.getState()).toEqual(new RepositoryState(dvcRoot)) + + await repository.resetState() + + const tracked = new Set([ + resolve(dvcRoot, compressedDataset), + resolve(dvcRoot, dataset), + resolve(dvcRoot, logAcc), + resolve(dvcRoot, logLoss), + resolve(dvcRoot, model), + resolve(dvcRoot, dataDir), + resolve(dvcRoot, logDir) + ]) + + const expectedExecutionOptions = { + cliPath: undefined, + cwd: dvcRoot, + pythonBinPath: undefined + } + + expect(mockedDiff).toBeCalledWith(expectedExecutionOptions) + expect(mockedGetAllUntracked).toBeCalledWith(dvcRoot) + expect(mockedListDvcOnlyRecursive).toBeCalledWith( + expectedExecutionOptions + ) + + expect(repository.getState()).toEqual({ + added: emptySet, + deleted: emptySet, + dispose: Disposable.fn(), + dvcRoot, + modified: emptySet, + notInCache: emptySet, + stageModified: emptySet, tracked, untracked: emptySet }) @@ -216,7 +313,7 @@ describe('Repository', () => { it("should update the classes state and call it's dependents", async () => { mockedListDvcOnlyRecursive.mockResolvedValueOnce([]) - mockedStatus.mockResolvedValueOnce({}) + mockedDiff.mockResolvedValueOnce(emptyDiff) mockedGetAllUntracked.mockResolvedValueOnce(new Set()) const config = ({ @@ -231,7 +328,7 @@ describe('Repository', () => { const logAcc = join(logDir, 'acc.tsv') const logLoss = join(logDir, 'loss.tsv') const dataDir = 'data' - const model = 'model.pt' + const model = 'model.pkl' mockedListDvcOnlyRecursive.mockResolvedValueOnce([ { path: logAcc }, { path: logLoss }, @@ -239,31 +336,12 @@ describe('Repository', () => { { path: dataDir } ] as ListOutput[]) - mockedStatus.mockResolvedValueOnce({ - prepare: [ - { 'changed deps': { 'data/data.xml': Status.NOT_IN_CACHE } }, - { 'changed outs': { 'data/prepared': Status.NOT_IN_CACHE } } - ], - featurize: [ - { 'changed deps': { 'data/prepared': Status.NOT_IN_CACHE } }, - { 'changed outs': { 'data/features': 'modified' } } - ], - train: [ - { 'changed deps': { 'data/features': 'modified' } }, - { 'changed outs': { 'model.pkl': 'deleted' } } - ], - evaluate: [ - { - 'changed deps': { - 'data/features': 'modified', - 'model.pkl': 'deleted' - } - } - ], - 'data/data.xml.dvc': [ - { 'changed outs': { 'data/data.xml': Status.NOT_IN_CACHE } } - ] - } as Record> | string)[]>) + mockedDiff.mockResolvedValueOnce(({ + added: [], + modified: [{ path: 'data/features' }], + deleted: [{ path: model }], + 'not in cache': [{ path: 'data/data.xml' }, { path: 'data/prepared' }] + } as unknown) as DiffOutput) const untracked = new Set([ resolve(dvcRoot, 'some', 'untracked', 'python.py'), @@ -272,15 +350,15 @@ describe('Repository', () => { ]) mockedGetAllUntracked.mockResolvedValueOnce(untracked) - expect(repository.getState()).toEqual(new RepositoryState()) + expect(repository.getState()).toEqual(new RepositoryState(dvcRoot)) await repository.resetState() const deleted = new Set([join(dvcRoot, 'model.pkl')]) - const modified = new Set([join(dvcRoot, 'data/features')]) + const stageModified = new Set([join(dvcRoot, 'data/features')]) const notInCache = new Set([ - join(dvcRoot, 'data/data.xml'), - join(dvcRoot, 'data/prepared') + join(dvcRoot, 'data/prepared'), + join(dvcRoot, 'data/data.xml') ]) const tracked = new Set([ resolve(dvcRoot, logAcc), @@ -296,18 +374,22 @@ describe('Repository', () => { pythonBinPath: undefined } - expect(mockedStatus).toBeCalledWith(expectedExecutionOptions) + expect(mockedDiff).toBeCalledWith(expectedExecutionOptions) expect(mockedGetAllUntracked).toBeCalledWith(dvcRoot) expect(mockedListDvcOnlyRecursive).toBeCalledWith( expectedExecutionOptions ) + const emptySet = new Set() + expect(repository.getState()).toEqual({ + added: emptySet, + deleted, dispose: Disposable.fn(), - new: new Set(), - modified, + dvcRoot, + modified: emptySet, notInCache, - deleted, + stageModified, tracked, untracked }) diff --git a/extension/src/Repository/index.ts b/extension/src/Repository/index.ts index 70a153ecf8..299988846b 100644 --- a/extension/src/Repository/index.ts +++ b/extension/src/Repository/index.ts @@ -1,57 +1,13 @@ import { Config } from '../Config' import { Disposable } from '@hediet/std/disposable' import { getAllUntracked } from '../git' -import { - SourceControlManagementState, - SourceControlManagement -} from './views/SourceControlManagement' -import { DecorationProvider, DecorationState } from './DecorationProvider' +import { SourceControlManagement } from './views/SourceControlManagement' +import { DecorationProvider } from './DecorationProvider' import { Deferred } from '@hediet/std/synchronization' -import { status, listDvcOnlyRecursive } from '../cli/reader' -import { dirname, join } from 'path' +import { diff, listDvcOnlyRecursive, status } from '../cli/reader' import { observable, makeObservable } from 'mobx' import { getExecutionOptions } from '../cli/execution' - -export enum Status { - DELETED = 'deleted', - MODIFIED = 'modified', - NEW = 'new', - NOT_IN_CACHE = 'not in cache' -} - -enum ChangedType { - CHANGED_OUTS = 'changed outs', - CHANGED_DEPS = 'changed deps' -} - -type PathStatus = Record - -type StageOrFileStatuses = Record - -type StatusesOrAlwaysChanged = StageOrFileStatuses | 'always changed' - -type StatusOutput = Record - -export class RepositoryState - implements DecorationState, SourceControlManagementState { - public dispose = Disposable.fn() - - public tracked: Set - public deleted: Set - public modified: Set - public new: Set - public notInCache: Set - public untracked: Set - - constructor() { - this.tracked = new Set() - this.deleted = new Set() - this.modified = new Set() - this.new = new Set() - this.notInCache = new Set() - this.untracked = new Set() - } -} +import { RepositoryState, StatusOutput } from './State' export class Repository { public readonly dispose = Disposable.fn() @@ -79,89 +35,24 @@ export class Repository { private decorationProvider?: DecorationProvider private sourceControlManagement: SourceControlManagement - private filterRootDir(dirs: string[] = []) { - return dirs.filter(dir => dir !== this.dvcRoot) - } - - private getAbsolutePath(files: string[] = []): string[] { - return files.map(file => join(this.dvcRoot, file)) - } - - private getAbsoluteParentPath(files: string[] = []): string[] { - return this.filterRootDir( - files.map(file => join(this.dvcRoot, dirname(file))) - ) - } - - public async updateList(): Promise { + private async updateTracked(): Promise { const options = getExecutionOptions(this.config, this.dvcRoot) const listOutput = await listDvcOnlyRecursive(options) - const trackedPaths = listOutput.map(tracked => tracked.path) - - const absoluteTrackedPaths = this.getAbsolutePath(trackedPaths) - - this.state.tracked = new Set([ - ...absoluteTrackedPaths, - ...this.getAbsoluteParentPath(trackedPaths) - ]) - } - - private getChangedOutsStatuses( - fileOrStage: StatusesOrAlwaysChanged[] - ): PathStatus[] { - return fileOrStage - .map(entry => (entry as StageOrFileStatuses)?.[ChangedType.CHANGED_OUTS]) - .filter(value => value) - } - - private reduceStatuses( - reducedStatus: Partial>>, - statuses: PathStatus[] - ) { - return statuses.map(entry => - Object.entries(entry).map(([relativePath, status]) => { - const absolutePath = join(this.dvcRoot, relativePath) - const existingPaths = reducedStatus[status] || new Set() - reducedStatus[status] = existingPaths.add(absolutePath) - }) - ) - } - - private reduceToChangedOutsStatuses( - filteredStatusOutput: StatusOutput - ): Partial>> { - const statusReducer = ( - reducedStatus: Partial>>, - entry: StatusesOrAlwaysChanged[] - ): Partial>> => { - const statuses = this.getChangedOutsStatuses(entry) - - this.reduceStatuses(reducedStatus, statuses) - - return reducedStatus - } - - return Object.values(filteredStatusOutput).reduce(statusReducer, {}) - } - - private async getStatus(): Promise>>> { - const options = getExecutionOptions(this.config, this.dvcRoot) - const statusOutput = (await status(options)) as StatusOutput - - return this.reduceToChangedOutsStatuses(statusOutput) + this.state.updateTracked(listOutput) } public async updateStatus() { - const status = await this.getStatus() - - this.state.modified = status.modified || new Set() - this.state.deleted = status.deleted || new Set() - this.state.new = status.new || new Set() - this.state.notInCache = status['not in cache'] || new Set() + const options = getExecutionOptions(this.config, this.dvcRoot) + const [diffFromHead, diffFromDvc] = await Promise.all([ + diff(options), + status(options) as Promise + ]) + return this.state.updateStatus(diffFromHead, diffFromDvc) } - public async updateUntracked() { - this.state.untracked = await getAllUntracked(this.dvcRoot) + private async updateUntracked() { + const untracked = await getAllUntracked(this.dvcRoot) + this.state.updateUntracked(untracked) } private updateStatuses() { @@ -171,7 +62,7 @@ export class Repository { public async resetState() { const statusesUpdated = this.updateStatuses() - const slowerTrackedUpdated = this.updateList() + const slowerTrackedUpdated = this.updateTracked() await statusesUpdated this.sourceControlManagement.setState(this.state) @@ -204,7 +95,7 @@ export class Repository { this.config = config this.decorationProvider = decorationProvider this.dvcRoot = dvcRoot - this.state = this.dispose.track(new RepositoryState()) + this.state = this.dispose.track(new RepositoryState(this.dvcRoot)) this.sourceControlManagement = this.dispose.track( new SourceControlManagement(this.dvcRoot, this.state) diff --git a/extension/src/Repository/views/SourceControlManagement.test.ts b/extension/src/Repository/views/SourceControlManagement.test.ts index 9a0005cd75..63cdffe800 100644 --- a/extension/src/Repository/views/SourceControlManagement.test.ts +++ b/extension/src/Repository/views/SourceControlManagement.test.ts @@ -39,15 +39,16 @@ describe('SourceControlManagement', () => { expect(sourceControlManagement.getState()).toEqual([]) const updatedState = ({ + added: new Set(['/some/new/path']), deleted: new Set(['/some/deleted/path', '/some/other/deleted/path']), dispose: () => undefined, - new: new Set(['/some/new/path']), tracked: new Set(['/some/excluded/tracked/path']) } as unknown) as SourceControlManagementState sourceControlManagement.setState(updatedState) expect(sourceControlManagement.getState()).toEqual([ + { resourceUri: Uri.file('/some/new/path'), contextValue: 'added' }, { resourceUri: Uri.file('/some/deleted/path'), contextValue: 'deleted' @@ -55,8 +56,7 @@ describe('SourceControlManagement', () => { { resourceUri: Uri.file('/some/other/deleted/path'), contextValue: 'deleted' - }, - { resourceUri: Uri.file('/some/new/path'), contextValue: 'new' } + } ]) sourceControlManagement.setState(initialState) diff --git a/extension/src/Repository/views/SourceControlManagement.ts b/extension/src/Repository/views/SourceControlManagement.ts index 2490b82e37..8a4dffd4f9 100644 --- a/extension/src/Repository/views/SourceControlManagement.ts +++ b/extension/src/Repository/views/SourceControlManagement.ts @@ -7,10 +7,11 @@ import { isStringInEnum } from '../../util' export type SourceControlManagementState = Record> enum Status { + ADDED = 'added', DELETED = 'deleted', MODIFIED = 'modified', - NEW = 'new', NOT_IN_CACHE = 'notInCache', + STAGE_MODIFIED = 'stageModified', UNTRACKED = 'untracked' } diff --git a/extension/src/cli/reader.ts b/extension/src/cli/reader.ts index 738ac645ee..fd92444779 100644 --- a/extension/src/cli/reader.ts +++ b/extension/src/cli/reader.ts @@ -19,11 +19,11 @@ export const root = (options: ExecutionOptions): Promise => type Path = { path: string } export type DiffOutput = { - added: Path[] - deleted: Path[] - modified: Path[] - renamed: Path[] - 'not in cache': Path[] + added?: Path[] + deleted?: Path[] + modified?: Path[] + renamed?: Path[] + 'not in cache'?: Path[] } export const diff = (options: ExecutionOptions): Promise => diff --git a/extension/src/test/suite/extension.test.ts b/extension/src/test/suite/extension.test.ts index 3de0b23f4f..7081b242b8 100644 --- a/extension/src/test/suite/extension.test.ts +++ b/extension/src/test/suite/extension.test.ts @@ -108,6 +108,37 @@ suite('Extension Test Suite', () => { { isout: true, isdir: false, isexec: false, path: 'model.pt' } ]) + const mockDiff = stub(CliReader, 'diff').resolves({ + added: [], + deleted: [ + { + path: 'data/MNIST/raw/t10k-images-idx3-ubyte' + } + ], + modified: [ + { + path: 'data/MNIST/raw/' + }, + { + path: 'logs/' + }, + { + path: 'logs/acc.tsv' + }, + { + path: 'logs/loss.tsv' + }, + { + path: 'model.pt' + }, + { + path: 'predictions.json' + } + ], + renamed: [], + 'not in cache': [] + }) + const mockStatus = stub(CliReader, 'status').resolves({ train: [ { 'changed deps': { 'data/MNIST': 'modified' } }, @@ -137,7 +168,8 @@ suite('Extension Test Suite', () => { expect(mockOnDidChangeFileSystem).to.have.been.called expect(mockOnDidChangeFileType).to.have.been.called expect(mockListDvcOnlyRecursive).to.have.been.called - expect(mockStatus).to.have.been.called + expect(mockDiff).to.have.been.calledOnce + expect(mockStatus).to.have.been.calledOnce await configurationChangeEvent()