diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b123f4c110b..9a22ca4632d 100755 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -138,20 +138,22 @@ const ImageMimetypes = [ 'image/bmp' ]; -async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[] }> { +async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> { const selection = resources.filter(s => s instanceof Resource) as Resource[]; const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge); const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED; + const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US; const possibleUnresolved = merge.filter(isBothAddedOrModified); const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/)); const unresolvedBothModified = await Promise.all(promises); const resolved = possibleUnresolved.filter((s, i) => !unresolvedBothModified[i]); + const deletionConflicts = merge.filter(s => isAnyDeleted(s)); const unresolved = [ - ...merge.filter(s => !isBothAddedOrModified(s)), + ...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)), ...possibleUnresolved.filter((s, i) => unresolvedBothModified[i]) ]; - return { merge, resolved, unresolved }; + return { merge, resolved, unresolved, deletionConflicts }; } enum PushType { @@ -215,7 +217,10 @@ export class CommandCenter { right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root }); } } else { - left = await this.getLeftResource(resource); + if (resource.type !== Status.DELETED_BY_THEM) { + left = await this.getLeftResource(resource); + } + right = await this.getRightResource(resource); } @@ -242,7 +247,7 @@ export class CommandCenter { } if (!left) { - await commands.executeCommand('vscode.open', right, opts); + await commands.executeCommand('vscode.open', right, opts, title); } else { await commands.executeCommand('vscode.diff', left, right, title, opts); } @@ -310,10 +315,15 @@ export class CommandCenter { return this.getURI(resource.resourceUri, ''); case Status.INDEX_DELETED: - case Status.DELETED_BY_THEM: case Status.DELETED: return this.getURI(resource.resourceUri, 'HEAD'); + case Status.DELETED_BY_US: + return this.getURI(resource.resourceUri, '~3'); + + case Status.DELETED_BY_THEM: + return this.getURI(resource.resourceUri, '~2'); + case Status.MODIFIED: case Status.UNTRACKED: case Status.IGNORED: @@ -344,13 +354,18 @@ export class CommandCenter { switch (resource.type) { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: - case Status.DELETED_BY_THEM: return `${basename} (Index)`; case Status.MODIFIED: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return `${basename} (Working Tree)`; + + case Status.DELETED_BY_US: + return `${basename} (Theirs)`; + + case Status.DELETED_BY_THEM: + return `${basename} (Ours)`; } return ''; @@ -704,7 +719,7 @@ export class CommandCenter { } const selection = resourceStates.filter(s => s instanceof Resource) as Resource[]; - const { resolved, unresolved } = await categorizeResourceByResolution(selection); + const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection); if (unresolved.length > 0) { const message = unresolved.length > 1 @@ -719,6 +734,20 @@ export class CommandCenter { } } + try { + await this.runByRepository(deletionConflicts.map(r => r.resourceUri), async (repository, resources) => { + for (const resource of resources) { + await this._stageDeletionConflict(repository, resource); + } + }); + } catch (err) { + if (/Cancelled/.test(err.message)) { + return; + } + + throw err; + } + const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree); const scmResources = [...workingTree, ...resolved, ...unresolved]; @@ -734,7 +763,19 @@ export class CommandCenter { @command('git.stageAll', { repository: true }) async stageAll(repository: Repository): Promise { const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[]; - const { merge, unresolved } = await categorizeResourceByResolution(resources); + const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources); + + try { + for (const deletionConflict of deletionConflicts) { + await this._stageDeletionConflict(repository, deletionConflict.resourceUri); + } + } catch (err) { + if (/Cancelled/.test(err.message)) { + return; + } + + throw err; + } if (unresolved.length > 0) { const message = unresolved.length > 1 @@ -752,6 +793,41 @@ export class CommandCenter { await repository.add([]); } + private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise { + const uriString = uri.toString(); + const resource = repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]; + + if (!resource) { + return; + } + + if (resource.type === Status.DELETED_BY_THEM) { + const keepIt = localize('keep ours', "Keep Our Version"); + const deleteIt = localize('delete', "Delete File"); + const result = await window.showInformationMessage(localize('deleted by them', "File '{0}' was deleted by them and modified by us.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt); + + if (result === keepIt) { + await repository.add([uri]); + } else if (result === deleteIt) { + await repository.rm([uri]); + } else { + throw new Error('Cancelled'); + } + } else if (resource.type === Status.DELETED_BY_US) { + const keepIt = localize('keep theirs', "Keep Their Version"); + const deleteIt = localize('delete', "Delete File"); + const result = await window.showInformationMessage(localize('deleted by us', "File '{0}' was deleted by us and modified by them.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt); + + if (result === keepIt) { + await repository.add([uri]); + } else if (result === deleteIt) { + await repository.rm([uri]); + } else { + throw new Error('Cancelled'); + } + } + } + @command('git.stageChange') async stageChange(uri: Uri, changes: LineChange[], index: number): Promise { const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0]; diff --git a/extensions/git/src/contentProvider.ts b/extensions/git/src/contentProvider.ts index e03a97bd29b..18af502194a 100644 --- a/extensions/git/src/contentProvider.ts +++ b/extensions/git/src/contentProvider.ts @@ -116,6 +116,8 @@ export class GitContentProvider { const uriString = fileUri.toString(); const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString); ref = indexStatus ? '' : 'HEAD'; + } else if (/^~\d$/.test(ref)) { + ref = `:${ref[1]}`; } try { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index d0ffc580d5b..3fad3b51cf7 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -900,6 +900,18 @@ export class Repository { await this.run(args); } + async rm(paths: string[]): Promise { + const args = ['rm', '--']; + + if (!paths || !paths.length) { + return; + } + + args.push(...paths); + + await this.run(args); + } + async stage(path: string, data: string): Promise { const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] }); child.stdin.end(data, 'utf8'); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 9f8a4eb4330..e971588cf10 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -198,12 +198,14 @@ export class Resource implements SourceControlResourceState { return 'U'; case Status.IGNORED: return 'I'; + case Status.DELETED_BY_THEM: + return 'D'; + case Status.DELETED_BY_US: + return 'D'; case Status.INDEX_COPIED: case Status.BOTH_DELETED: case Status.ADDED_BY_US: - case Status.DELETED_BY_THEM: case Status.ADDED_BY_THEM: - case Status.DELETED_BY_US: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return 'C'; @@ -281,6 +283,7 @@ export const enum Operation { Diff = 'Diff', MergeBase = 'MergeBase', Add = 'Add', + Remove = 'Remove', RevertFiles = 'RevertFiles', Commit = 'Commit', Clean = 'Clean', @@ -755,6 +758,10 @@ export class Repository implements Disposable { await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath))); } + async rm(resources: Uri[]): Promise { + await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath))); + } + async stage(resource: Uri, contents: string): Promise { const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/'); await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents)); diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index d302c348567..2a4208d0d82 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -263,16 +263,16 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // --- commands -CommandsRegistry.registerCommand('_workbench.open', function (accessor: ServicesAccessor, args: [URI, IEditorOptions, EditorViewColumn]) { +CommandsRegistry.registerCommand('_workbench.open', function (accessor: ServicesAccessor, args: [URI, IEditorOptions, EditorViewColumn, string?]) { const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); const openerService = accessor.get(IOpenerService); - const [resource, options, position] = args; + const [resource, options, position, label] = args; if (options || typeof position === 'number') { // use editor options or editor view column as a hint to use the editor service for opening - return editorService.openEditor({ resource, options }, viewColumnToEditorGroup(editorGroupService, position)).then(_ => void 0); + return editorService.openEditor({ resource, options, label }, viewColumnToEditorGroup(editorGroupService, position)).then(_ => void 0); } if (resource && resource.scheme === 'command') { diff --git a/src/vs/workbench/api/node/apiCommands.ts b/src/vs/workbench/api/node/apiCommands.ts index 03dfaf4a9e1..adde6d38bcd 100644 --- a/src/vs/workbench/api/node/apiCommands.ts +++ b/src/vs/workbench/api/node/apiCommands.ts @@ -77,7 +77,7 @@ CommandsRegistry.registerCommand(DiffAPICommand.ID, adjustHandler(DiffAPICommand export class OpenAPICommand { public static ID = 'vscode.open'; - public static execute(executor: ICommandsExecutor, resource: URI, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions): Thenable { + public static execute(executor: ICommandsExecutor, resource: URI, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions, label?: string): Thenable { let options: ITextEditorOptions; let position: EditorViewColumn; @@ -93,7 +93,8 @@ export class OpenAPICommand { return executor.executeCommand('_workbench.open', [ resource, options, - position + position, + label ]); } }