diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 004c1c8540a..a95b2445f76 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -169,14 +169,33 @@ export class CommandCenter { } private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise { - const stat = await new Promise((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat))); + let stat: Stats | undefined; - if (stat.isDirectory()) { - return; + try { + stat = await new Promise((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat))); + } catch (err) { + // noop } - const left = await this.getLeftResource(resource); - const right = await this.getRightResource(resource); + let left: Uri | undefined; + let right: Uri | undefined; + + if (stat && stat.isDirectory()) { + outer: + for (const repository of this.model.repositories) { + for (const submodule of repository.submodules) { + const submodulePath = path.join(repository.root, submodule.path); + + if (submodulePath === resource.resourceUri.fsPath) { + right = toGitUri(Uri.file(submodulePath), resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root }); + break outer; + } + } + } + } else { + left = await this.getLeftResource(resource); + right = await this.getRightResource(resource); + } const title = this.getTitle(resource); @@ -1697,7 +1716,7 @@ export class CommandCenter { const isSingleResource = arg instanceof Uri; const groups = resources.reduce((result, resource) => { - const repository = this.model.getRepository(resource); + const repository = this.model.getRepository(resource, true); if (!repository) { console.warn('Could not find git repository for ', resource); diff --git a/extensions/git/src/contentProvider.ts b/extensions/git/src/contentProvider.ts index 9ab283dcd1e..67b36b3feca 100644 --- a/extensions/git/src/contentProvider.ts +++ b/extensions/git/src/contentProvider.ts @@ -52,7 +52,7 @@ export class GitContentProvider { return; } - this._onDidChange.fire(toGitUri(uri, '', true)); + this._onDidChange.fire(toGitUri(uri, '', { replaceFileExtension: true })); } @debounce(1100) @@ -83,6 +83,18 @@ export class GitContentProvider { } async provideTextDocumentContent(uri: Uri): Promise { + let { path, ref, submoduleOf } = fromGitUri(uri); + + if (submoduleOf) { + const repository = this.model.getRepository(submoduleOf); + + if (!repository) { + return ''; + } + + return await repository.diff(path, { cached: ref === 'index' }); + } + const repository = this.model.getRepository(uri); if (!repository) { @@ -95,8 +107,6 @@ export class GitContentProvider { this.cache[cacheKey] = cacheValue; - let { path, ref } = fromGitUri(uri); - if (ref === '~') { const fileUri = Uri.file(path); const uriString = fileUri.toString(); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index dbadd2003f6..bc9c120d338 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -597,6 +597,10 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } +export interface DiffOptions { + cached?: boolean; +} + export class Repository { constructor( @@ -735,6 +739,19 @@ export class Repository { } } + async diff(path: string, options: DiffOptions = {}): Promise { + const args = ['diff']; + + if (options.cached) { + args.push('--cached'); + } + + args.push('--', path); + + const result = await this.run(args); + return result.stdout; + } + async add(paths: string[]): Promise { const args = ['add', '-A', '--']; diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 1d40618a5e2..1a23c1795d2 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -265,19 +265,19 @@ export class Model { getRepository(sourceControl: SourceControl): Repository | undefined; getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined; - getRepository(path: string): Repository | undefined; - getRepository(resource: Uri): Repository | undefined; - getRepository(hint: any): Repository | undefined { - const liveRepository = this.getOpenRepository(hint); + getRepository(path: string, possibleSubmoduleRoot?: boolean): Repository | undefined; + getRepository(resource: Uri, possibleSubmoduleRoot?: boolean): Repository | undefined; + getRepository(hint: any, possibleSubmoduleRoot?: boolean): Repository | undefined { + const liveRepository = this.getOpenRepository(hint, possibleSubmoduleRoot); return liveRepository && liveRepository.repository; } private getOpenRepository(repository: Repository): OpenRepository | undefined; private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined; private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined; - private getOpenRepository(path: string): OpenRepository | undefined; - private getOpenRepository(resource: Uri): OpenRepository | undefined; - private getOpenRepository(hint: any): OpenRepository | undefined { + private getOpenRepository(path: string, possibleSubmoduleRoot?: boolean): OpenRepository | undefined; + private getOpenRepository(resource: Uri, possibleSubmoduleRoot?: boolean): OpenRepository | undefined; + private getOpenRepository(hint: any, possibleSubmoduleRoot?: boolean): OpenRepository | undefined { if (!hint) { return undefined; } @@ -295,15 +295,21 @@ export class Model { outer: for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) { + if (possibleSubmoduleRoot && liveRepository.repository.root === resourcePath) { + continue; + } + if (!isDescendant(liveRepository.repository.root, resourcePath)) { continue; } - for (const submodule of liveRepository.repository.submodules) { - const submoduleRoot = path.join(liveRepository.repository.root, submodule.path); + if (!possibleSubmoduleRoot) { + for (const submodule of liveRepository.repository.submodules) { + const submoduleRoot = path.join(liveRepository.repository.root, submodule.path); - if (isDescendant(submoduleRoot, resourcePath)) { - continue outer; + if (isDescendant(submoduleRoot, resourcePath)) { + continue outer; + } } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 332522c014d..9c5c07a9cc5 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -6,7 +6,7 @@ 'use strict'; import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento } from 'vscode'; -import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError, Submodule } from './git'; +import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError, Submodule, DiffOptions } from './git'; import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent } from './util'; import { memoize, throttle, debounce } from './decorators'; import { toGitUri } from './uri'; @@ -280,6 +280,7 @@ export class Resource implements SourceControlResourceState { export enum Operation { Status = 'Status', + Diff = 'Diff', Add = 'Add', RevertFiles = 'RevertFiles', Commit = 'Commit', @@ -557,7 +558,7 @@ export class Repository implements Disposable { return; } - return toGitUri(uri, '', true); + return toGitUri(uri, '', { replaceFileExtension: true }); } private async updateCommitTemplate(): Promise { @@ -573,6 +574,10 @@ export class Repository implements Disposable { await this.run(Operation.Status); } + diff(path: string, options: DiffOptions = {}): Promise { + return this.run(Operation.Diff, () => this.repository.diff(path, options)); + } + async add(resources: Uri[]): Promise { await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath))); } diff --git a/extensions/git/src/uri.ts b/extensions/git/src/uri.ts index 0a56c9a5a8e..0e96b3504b0 100644 --- a/extensions/git/src/uri.ts +++ b/extensions/git/src/uri.ts @@ -7,20 +7,45 @@ import { Uri } from 'vscode'; -export function fromGitUri(uri: Uri): { path: string; ref: string; } { +export interface GitUriParams { + path: string; + ref: string; + submoduleOf?: string; +} + +export function fromGitUri(uri: Uri): GitUriParams { return JSON.parse(uri.query); } +export interface GitUriOptions { + replaceFileExtension?: boolean; + submoduleOf?: string; +} + // As a mitigation for extensions like ESLint showing warnings and errors // for git URIs, let's change the file extension of these uris to .git, // when `replaceFileExtension` is true. -export function toGitUri(uri: Uri, ref: string, replaceFileExtension = false): Uri { +export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Uri { + const params: GitUriParams = { + path: uri.fsPath, + ref + }; + + if (options.submoduleOf) { + params.submoduleOf = options.submoduleOf; + } + + let path = uri.path; + + if (options.replaceFileExtension) { + path = path + `${path}.git`; + } else if (options.submoduleOf) { + path = path + `${path}.diff`; + } + return uri.with({ scheme: 'git', - path: replaceFileExtension ? `${uri.path}.git` : uri.path, - query: JSON.stringify({ - path: uri.fsPath, - ref - }) + path, + query: JSON.stringify(params) }); } \ No newline at end of file diff --git a/extensions/gitsyntax/OSSREADME.json b/extensions/gitsyntax/OSSREADME.json index 0e3ddd52faf..347ae77f6e5 100644 --- a/extensions/gitsyntax/OSSREADME.json +++ b/extensions/gitsyntax/OSSREADME.json @@ -1,29 +1,42 @@ // ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS: -[{ - "name": "textmate/git.tmbundle", - "version": "0.0.0", - "license": "MIT", - "repositoryURL": "https://github.com/textmate/git.tmbundle", - "licenseDetail": [ - "Copyright (c) 2008 Tim Harper", - "", - "Permission is hereby granted, free of charge, to any person obtaining", - "a copy of this software and associated documentation files (the\"", - "Software\"), to deal in the Software without restriction, including", - "without limitation the rights to use, copy, modify, merge, publish,", - "distribute, sublicense, and/or sell copies of the Software, and to", - "permit persons to whom the Software is furnished to do so, subject to", - "the following conditions:", - "", - "The above copyright notice and this permission notice shall be", - "included in all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,", - "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF", - "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND", - "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE", - "LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", - "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION", - "WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - ] -}] \ No newline at end of file +[ + { + "name": "textmate/git.tmbundle", + "version": "0.0.0", + "license": "MIT", + "repositoryURL": "https://github.com/textmate/git.tmbundle", + "licenseDetail": [ + "Copyright (c) 2008 Tim Harper", + "", + "Permission is hereby granted, free of charge, to any person obtaining", + "a copy of this software and associated documentation files (the\"", + "Software\"), to deal in the Software without restriction, including", + "without limitation the rights to use, copy, modify, merge, publish,", + "distribute, sublicense, and/or sell copies of the Software, and to", + "permit persons to whom the Software is furnished to do so, subject to", + "the following conditions:", + "", + "The above copyright notice and this permission notice shall be", + "included in all copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,", + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF", + "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND", + "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE", + "LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION", + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION", + "WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + ] + }, + { + "name": "textmate/diff.tmbundle", + "version": "0.0.0", + "repositoryURL": "https://github.com/textmate/diff.tmbundle", + "licenseDetail": [ + "Permission to copy, use, modify, sell and distribute this", + "software is granted. This software is provided \"as is\" without", + "express or implied warranty, and with no claim as to its", + "suitability for any purpose." + ] + } +] \ No newline at end of file diff --git a/extensions/gitsyntax/diff.language-configuration.json b/extensions/gitsyntax/diff.language-configuration.json new file mode 100644 index 00000000000..b61fbe63d34 --- /dev/null +++ b/extensions/gitsyntax/diff.language-configuration.json @@ -0,0 +1,11 @@ +{ + "comments": { + "lineComment": "#", + "blockComment": [ "#", " " ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ] +} \ No newline at end of file diff --git a/extensions/gitsyntax/package.json b/extensions/gitsyntax/package.json index 0040fe55473..d01893d002a 100644 --- a/extensions/gitsyntax/package.json +++ b/extensions/gitsyntax/package.json @@ -37,6 +37,19 @@ "git-rebase-todo" ], "configuration": "./git-rebase.language-configuration.json" + }, + { + "id": "diff", + "aliases": [ + "Diff", + "diff" + ], + "extensions": [ + ".patch", + ".diff", + ".rej" + ], + "configuration": "./diff.language-configuration.json" } ], "grammars": [ @@ -49,12 +62,19 @@ "language": "git-rebase", "scopeName": "text.git-rebase", "path": "./syntaxes/git-rebase.tmLanguage.json" + }, + { + "language": "diff", + "scopeName": "source.diff", + "path": "./syntaxes/diff.tmLanguage" } ], "configurationDefaults": { - "[git-commit]": { - "editor.rulers": [72] - } - } + "[git-commit]": { + "editor.rulers": [ + 72 + ] + } + } } } \ No newline at end of file diff --git a/extensions/gitsyntax/syntaxes/diff.tmLanguage b/extensions/gitsyntax/syntaxes/diff.tmLanguage new file mode 100644 index 00000000000..e71adc69fd1 --- /dev/null +++ b/extensions/gitsyntax/syntaxes/diff.tmLanguage @@ -0,0 +1,268 @@ + + + + + fileTypes + + patch + diff + rej + + firstLineMatch + (?x)^ + (===\ modified\ file + |==== \s* // .+ \s - \s .+ \s+ ==== + |Index:\ + |---\ [^%\n] + |\*\*\*.*\d{4}\s*$ + |\d+(,\d+)* (a|d|c) \d+(,\d+)* $ + |diff\ --git\ + |commit\ [0-9a-f]{40}$ + ) + keyEquivalent + ^~D + name + Diff + patterns + + + captures + + 1 + + name + punctuation.definition.separator.diff + + + match + ^((\*{15})|(={67})|(-{3}))$\n? + name + meta.separator.diff + + + match + ^\d+(,\d+)*(a|d|c)\d+(,\d+)*$\n? + name + meta.diff.range.normal + + + captures + + 1 + + name + punctuation.definition.range.diff + + 2 + + name + meta.toc-list.line-number.diff + + 3 + + name + punctuation.definition.range.diff + + + match + ^(@@)\s*(.+?)\s*(@@)($\n?)? + name + meta.diff.range.unified + + + captures + + 3 + + name + punctuation.definition.range.diff + + 4 + + name + punctuation.definition.range.diff + + 6 + + name + punctuation.definition.range.diff + + 7 + + name + punctuation.definition.range.diff + + + match + ^(((\-{3}) .+ (\-{4}))|((\*{3}) .+ (\*{4})))$\n? + name + meta.diff.range.context + + + match + ^diff --git a/.*$\n? + name + meta.diff.header.git + + + match + ^diff (-|\S+\s+\S+).*$\n? + name + meta.diff.header.command + + + captures + + 4 + + name + punctuation.definition.from-file.diff + + 6 + + name + punctuation.definition.from-file.diff + + 7 + + name + punctuation.definition.from-file.diff + + + match + (^(((-{3}) .+)|((\*{3}) .+))$\n?|^(={4}) .+(?= - )) + name + meta.diff.header.from-file + + + captures + + 2 + + name + punctuation.definition.to-file.diff + + 3 + + name + punctuation.definition.to-file.diff + + 4 + + name + punctuation.definition.to-file.diff + + + match + (^(\+{3}) .+$\n?| (-) .* (={4})$\n?) + name + meta.diff.header.to-file + + + captures + + 3 + + name + punctuation.definition.inserted.diff + + 6 + + name + punctuation.definition.inserted.diff + + + match + ^(((>)( .*)?)|((\+).*))$\n? + name + markup.inserted.diff + + + captures + + 1 + + name + punctuation.definition.changed.diff + + + match + ^(!).*$\n? + name + markup.changed.diff + + + captures + + 3 + + name + punctuation.definition.deleted.diff + + 6 + + name + punctuation.definition.deleted.diff + + + match + ^(((<)( .*)?)|((-).*))$\n? + name + markup.deleted.diff + + + begin + ^(#) + captures + + 1 + + name + punctuation.definition.comment.diff + + + comment + Git produces unified diffs with embedded comments" + end + \n + name + comment.line.number-sign.diff + + + match + ^index [0-9a-f]{7,40}\.\.[0-9a-f]{7,40}.*$\n? + name + meta.diff.index.git + + + captures + + 1 + + name + punctuation.separator.key-value.diff + + 2 + + name + meta.toc-list.file-name.diff + + + match + ^Index(:) (.+)$\n? + name + meta.diff.index + + + match + ^Only in .*: .*$\n? + name + meta.diff.only-in + + + scopeName + source.diff + uuid + 7E848FF4-708E-11D9-97B4-0011242E4184 + +