diff --git a/extensions/git-extended/package.json b/extensions/git-extended/package.json index a6b64fd6af9..3275792f62f 100644 --- a/extensions/git-extended/package.json +++ b/extensions/git-extended/package.json @@ -12,9 +12,7 @@ "Other" ], "activationEvents": [ - "onView:commits", - "onView:stash", - "onView:pr" + "*" ], "extensionDependencies": [ "vscode.git" diff --git a/extensions/git-extended/src/commentsProvider.ts b/extensions/git-extended/src/commentsProvider.ts new file mode 100644 index 00000000000..ba9b1887435 --- /dev/null +++ b/extensions/git-extended/src/commentsProvider.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as vscode from 'vscode'; +import * as _ from 'lodash'; +import { Comment } from './common/models/comment'; + +export interface ICommentsProvider { + provideComments(uri: vscode.Uri): Promise; +} + +export class CommentsProvider implements vscode.CommentProvider { + private providers: Map; + private _id: number; + constructor() { + // vscode.workspace.registerTextDocumentContentProvider('review', this); + this.providers = new Map(); + this._id = 0; + vscode.workspace.registerCommentProvider(this); + } + + registerCommentProvider(provider: ICommentsProvider): number { + this.providers.set(this._id, provider); + this._id++; + return this._id - 1; + } + + unregisterCommentProvider(id: number) { + this.providers.delete(id); + } + + async provideComments(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + let promises = []; + this.providers.forEach((value: ICommentsProvider) => { + promises.push(value.provideComments(document.uri)); + }); + + let matchingComments: Comment[] = []; + let allComments = await Promise.all(promises); + allComments.forEach(comments => { + if (comments) { + matchingComments.push(...comments); + } + }); + + if (!matchingComments || !matchingComments.length) { + return []; + } + + let sections = _.groupBy(matchingComments, comment => comment.position); + let ret = []; + + for (let i in sections) { + let comments = sections[i]; + + const comment = comments[0]; + const pos = new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0); + const range = new vscode.Range(pos, pos); + + ret.push({ + range, + comments: comments.map(comment => { + return { + body: new vscode.MarkdownString(comment.body), + userName: comment.user.login, + gravatar: comment.user.avatar_url + }; + }) + }); + } + + return ret; + } + + // async provideComments(uri: vscode.Uri, token?: vscode.CancellationToken): Promise { + // let promises = []; + // this.providers.forEach((value: ICommentsProvider) => { + // promises.push(value.provideComments(uri)); + // }); + + // let ret: Comment[] = []; + // (await Promise.all(promises)).forEach(comments => { + // ret.push(...comments); + // }); + + // return ret; + // } +} diff --git a/extensions/git-extended/src/common/comment.ts b/extensions/git-extended/src/common/comment.ts index 93707236e68..22a94959ec2 100644 --- a/extensions/git-extended/src/common/comment.ts +++ b/extensions/git-extended/src/common/comment.ts @@ -28,7 +28,7 @@ export function parseComments(comments: any[]): Comment[] { } export function getMatchingCommentsForDiffViewEditor(filePath: string, items: FileChange[], comments: Comment[]): Comment[] { - let fileChangeItem = items.filter(item => filePath === path.resolve(item.workspaceRoot, item.filePath)); + let fileChangeItem = items.filter(item => filePath === item.filePath.fsPath); if (fileChangeItem.length === 0) { return []; diff --git a/extensions/git-extended/src/common/models/model.ts b/extensions/git-extended/src/common/models/model.ts new file mode 100644 index 00000000000..d69cfbe6572 --- /dev/null +++ b/extensions/git-extended/src/common/models/model.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Repository } from './repository'; +export class Model { + constructor(repository: Repository) { + + } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/models/repository.ts b/extensions/git-extended/src/common/models/repository.ts index 4b0a5582d21..38c126e9cb1 100644 --- a/extensions/git-extended/src/common/models/repository.ts +++ b/extensions/git-extended/src/common/models/repository.ts @@ -3,15 +3,82 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { Remote } from './remote'; +import { getHEAD, getBranch, getRefs, getRemotes } from '../operation'; + +export enum RefType { + Head, + RemoteHead, + Tag +} + +export interface Ref { + type: RefType; + name?: string; + commit?: string; + remote?: string; +} + +export interface UpstreamRef { + remote: string; + name: string; +} + +export interface Branch extends Ref { + upstream?: UpstreamRef; + ahead?: number; + behind?: number; +} export class Repository { public path: string; - public remotes: Remote[]; + private _onDidRunGitStatus = new vscode.EventEmitter(); + readonly onDidRunGitStatus: vscode.Event = this._onDidRunGitStatus.event; - constructor(path: string, remotes: Remote[]) { + private _HEAD: Branch | undefined; + get HEAD(): Branch | undefined { + return this._HEAD; + } + + private _refs: Ref[] = []; + get refs(): Ref[] { + return this._refs; + } + + private _remotes: Remote[] = []; + get remotes(): Remote[] { + return this._remotes; + } + + constructor(path: string, workspaceState: vscode.Memento) { this.path = path; - this.remotes = remotes; + this.status(); + } + + async status() { + let HEAD: Branch | undefined; + + try { + HEAD = await getHEAD(this); + + if (HEAD.name) { + try { + HEAD = await getBranch(this, HEAD.name); + } catch (err) { + // noop + } + } + } catch (err) { + // noop + } + + const [refs, remotes] = await Promise.all([getRefs(this), getRemotes(this)]); + this._HEAD = HEAD; + this._refs = refs; + this._remotes = remotes; + + this._onDidRunGitStatus.fire(); } } \ No newline at end of file diff --git a/extensions/git-extended/src/common/operation.ts b/extensions/git-extended/src/common/operation.ts index 99d58cbf6e6..30e42b88149 100644 --- a/extensions/git-extended/src/common/operation.ts +++ b/extensions/git-extended/src/common/operation.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Repository } from './models/repository'; +import { Repository, Ref, RefType, Branch } from './models/repository'; import { GitProcess } from 'dugite'; +import { uniqBy } from './util'; +import { parseRemote } from './remote'; +import { Remote } from './models/remote'; export async function fetch(repository: Repository, remoteName: string, branch: string) { const result = await GitProcess.exec( @@ -34,3 +37,111 @@ export async function checkout(repository: Repository, branch: string) { throw (result.stderr); } } + +export async function getHEAD(repository: Repository): Promise { + try { + const result = await GitProcess.exec(['symbolic-ref', '--short', 'HEAD'], repository.path); + + if (!result.stdout) { + throw new Error('Not in a branch'); + } + + return { name: result.stdout.trim(), commit: void 0, type: RefType.Head }; + } catch (err) { + const result = await GitProcess.exec(['rev-parse', 'HEAD'], repository.path); + + if (!result.stdout) { + throw new Error('Error parsing HEAD'); + } + + return { name: void 0, commit: result.stdout.trim(), type: RefType.Head }; + } +} + +export async function getBranch(repository: Repository, name: string): Promise { + if (name === 'HEAD') { + return getHEAD(repository); + } + + const result = await GitProcess.exec(['rev-parse', name], repository.path); + + if (!result.stdout) { + return Promise.reject(new Error('No such branch')); + } + + const commit = result.stdout.trim(); + + try { + const res2 = await GitProcess.exec(['rev-parse', '--symbolic-full-name', name + '@{u}'], repository.path); + const fullUpstream = res2.stdout.trim(); + const match = /^refs\/remotes\/([^/]+)\/(.+)$/.exec(fullUpstream); + + if (!match) { + throw new Error(`Could not parse upstream branch: ${fullUpstream}`); + } + + const upstream = { remote: match[1], name: match[2] }; + const res3 = await GitProcess.exec(['rev-list', '--left-right', name + '...' + fullUpstream], repository.path); + + let ahead = 0, behind = 0; + let i = 0; + + while (i < res3.stdout.length) { + switch (res3.stdout.charAt(i)) { + case '<': ahead++; break; + case '>': behind++; break; + default: i++; break; + } + + while (res3.stdout.charAt(i++) !== '\n') { /* no-op */ } + } + + return { name, type: RefType.Head, commit, upstream, ahead, behind }; + } catch (err) { + return { name, type: RefType.Head, commit }; + } +} + +export async function getRefs(repository: Repository): Promise { + const result = await GitProcess.exec(['for-each-ref', '--format', '%(refname) %(objectname)', '--sort', '-committerdate'], repository.path); + + const fn = (line: string): Ref | null => { + let match: RegExpExecArray | null; + + if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { + return { name: match[1], commit: match[2], type: RefType.Head }; + } else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { + return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] }; + } else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { + return { name: match[1], commit: match[2], type: RefType.Tag }; + } + + return null; + }; + + return result.stdout.trim().split('\n') + .filter(line => !!line) + .map(fn) + .filter(ref => !!ref) as Ref[]; +} + +export async function getRemotes(repository: Repository): Promise { + const result = await GitProcess.exec(['remote', '--verbose'], repository.path); + const regex = /^([^\s]+)\s+([^\s]+)\s/; + const rawRemotes = result.stdout.trim().split('\n') + .filter(b => !!b) + .map(line => regex.exec(line) as RegExpExecArray) + .filter(g => !!g) + .map((groups: RegExpExecArray) => ({ name: groups[1], url: groups[2] })); + + return uniqBy(rawRemotes, remote => remote.name).map(remote => parseRemote(remote.name, remote.url)); +} + +export async function show(repository: Repository, filePath: string): Promise { + try { + const result = await GitProcess.exec(['show', filePath], repository.path); + return result.stdout.trim(); + } catch (e) { + return ''; + } +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/treeItems.ts b/extensions/git-extended/src/common/treeItems.ts index d2ac5532e23..3f403a477dc 100644 --- a/extensions/git-extended/src/common/treeItems.ts +++ b/extensions/git-extended/src/common/treeItems.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; import { GitChangeType } from './models/file'; import { Comment } from './models/comment'; import { Remote } from './models/remote'; @@ -55,21 +54,21 @@ export class FileChange implements vscode.TreeItem { public comments?: any[]; constructor( - public readonly rItem: any, + public readonly prItem: any, public readonly label: string, public readonly status: GitChangeType, public readonly context: vscode.ExtensionContext, public readonly fileName: string, - public readonly filePath: string, - public readonly parentFilePath: string, + public readonly filePath: vscode.Uri, + public readonly parentFilePath: vscode.Uri, public readonly workspaceRoot: string ) { this.command = { title: 'show diff', command: 'vscode.diff', arguments: [ - vscode.Uri.file(path.resolve(this.workspaceRoot, this.parentFilePath)), - vscode.Uri.file(path.resolve(this.workspaceRoot, this.filePath)), + this.parentFilePath, + this.filePath, this.fileName ] }; diff --git a/extensions/git-extended/src/common/uri.ts b/extensions/git-extended/src/common/uri.ts new file mode 100644 index 00000000000..2a77e668dfe --- /dev/null +++ b/extensions/git-extended/src/common/uri.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Uri } from 'vscode'; + +export interface GitUriParams { + path: string; + ref?: string; + commit?: 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, commit: string, options: GitUriOptions = {}): Uri { + const params: GitUriParams = { + path: uri.path, + ref, + commit: commit + }; + + let path = uri.path; + + if (options.replaceFileExtension) { + path = `${path}.git`; + } + + return uri.with({ + scheme: 'review', + path, + query: JSON.stringify(params) + }); +} \ No newline at end of file diff --git a/extensions/git-extended/src/common/util.ts b/extensions/git-extended/src/common/util.ts new file mode 100644 index 00000000000..5c04757b62a --- /dev/null +++ b/extensions/git-extended/src/common/util.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export function uniqBy(arr: T[], fn: (el: T) => string): T[] { + const seen = Object.create(null); + + return arr.filter(el => { + const key = fn(el); + + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }); +} \ No newline at end of file diff --git a/extensions/git-extended/src/contentProvider.ts b/extensions/git-extended/src/contentProvider.ts new file mode 100644 index 00000000000..6bb188d5b89 --- /dev/null +++ b/extensions/git-extended/src/contentProvider.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as vscode from 'vscode'; +import { Repository } from './common/models/repository'; +import { fromGitUri } from './common/uri'; +import { show } from './common/operation'; + +export class GitContentProvider implements vscode.TextDocumentContentProvider { + private _onDidChange = new vscode.EventEmitter(); + get onDidChange(): vscode.Event { return this._onDidChange.event; } + + constructor(private repository: Repository) { + vscode.workspace.registerTextDocumentContentProvider('review', this); + } + + async provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Promise { + let { path, commit } = fromGitUri(uri); + + if (!path || !commit) { + return ''; + } + + let ret = await show(this.repository, `${commit}:${path}`); + return ret; + } +} diff --git a/extensions/git-extended/src/extension.ts b/extensions/git-extended/src/extension.ts index 9a70dba0b67..1f3a6e2093f 100644 --- a/extensions/git-extended/src/extension.ts +++ b/extensions/git-extended/src/extension.ts @@ -7,16 +7,16 @@ import * as vscode from 'vscode'; import { PRProvider } from './prProvider'; import { Repository } from './common/models/repository'; -import { getRemotes, parseRemote } from './common/remote'; import { Configuration } from './configuration'; import { Resource } from './common/resources'; +import { GitContentProvider } from './contentProvider'; +import { CommentsProvider } from './commentsProvider'; export async function activate(context: vscode.ExtensionContext) { // initialize resources Resource.initialize(context); const rootPath = vscode.workspace.rootPath; - const remotes = await getRemotes(rootPath); const config = vscode.workspace.getConfiguration('github'); const configuration = new Configuration( @@ -51,7 +51,12 @@ export async function activate(context: vscode.ExtensionContext) { repo = repos[0]; } - const remoteInfos = remotes.map(remote => parseRemote(remote.name, remote.url)); - const repository = new Repository(rootPath, remoteInfos); - new PRProvider(configuration).activate(context, rootPath, repository, repo); + + const repository = new Repository(rootPath, context.workspaceState); + repository.onDidRunGitStatus(async e => { + let commentsProvider = new CommentsProvider(); + // tslint:disable-next-line:no-unused-expression + new GitContentProvider(repository); + await (new PRProvider(configuration)).activate(context, rootPath, repository, commentsProvider, repo); + }); } diff --git a/extensions/git-extended/src/prProvider.ts b/extensions/git-extended/src/prProvider.ts index 56942e1a236..b7e3c0ddab8 100644 --- a/extensions/git-extended/src/prProvider.ts +++ b/extensions/git-extended/src/prProvider.ts @@ -10,20 +10,20 @@ import { Comment } from './common/models/comment'; import * as _ from 'lodash'; import { Configuration } from './configuration'; import { CredentialStore } from './credentials'; -import { parseComments, getMatchingCommentsForDiffViewEditor, getMatchingCommentsForNormalEditor } from './common/comment'; -import { enterReviewMode } from './review'; +import { parseComments } from './common/comment'; +import { enterReviewMode, restoreReviewState } from './review'; import { PRGroup, PullRequest, FileChange, PRGroupType } from './common/treeItems'; import { Resource } from './common/resources'; +import { CommentsProvider } from './commentsProvider'; -export class PRProvider implements vscode.TreeDataProvider, vscode.CommentProvider { - private _fileChanges: FileChange[]; - private _comments?: Comment[]; +export class PRProvider implements vscode.TreeDataProvider { private context: vscode.ExtensionContext; private workspaceRoot: string; private repository: Repository; private gitRepo: any; private crendentialStore: CredentialStore; private configuration: Configuration; + private commentsProvider: CommentsProvider; private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; @@ -32,21 +32,22 @@ export class PRProvider implements vscode.TreeDataProvider('pr', this)); - this.context.subscriptions.push(vscode.commands.registerCommand('pr.pick', async (pr: PullRequest) => { - await enterReviewMode(this.workspaceRoot, this.repository, pr, this.gitRepo); + await enterReviewMode(context.workspaceState, this.repository, pr, this.gitRepo); })); - this.context.subscriptions.push(vscode.workspace.registerCommentProvider(this)); this.context.subscriptions.push(this.configuration.onDidChange(e => { this._onDidChangeTreeData.fire(); })); + + await restoreReviewState(this.repository, context.workspaceState, gitRepo, this.commentsProvider); } getTreeItem(element: PRGroup | PullRequest | FileChange): vscode.TreeItem { @@ -101,16 +102,22 @@ export class PRProvider implements vscode.TreeDataProvider(); let fileChanges = richContentChanges.map(change => { - let changedItem = new FileChange(element.prItem, change.fileName, change.status, this.context, change.fileName, change.filePath, change.originalFilePath, this.workspaceRoot); + let changedItem = new FileChange(element.prItem, change.fileName, change.status, this.context, change.fileName, vscode.Uri.file(change.filePath), vscode.Uri.file(change.originalFilePath), this.workspaceRoot); changedItem.comments = comments.filter(comment => comment.path === changedItem.fileName); + commentsCache.set(changedItem.filePath.toString(), changedItem.comments); return changedItem; }); + this.commentsProvider.registerCommentProvider({ + provideComments: async (uri: vscode.Uri) => { + let matchingComments = commentsCache.get(uri.toString()); + return matchingComments; + } + }); // fill in file changes and comments. element.comments = comments; element.fileChanges = fileChanges; - this._fileChanges = fileChanges; - this._comments = comments; return fileChanges; } } @@ -138,20 +145,20 @@ export class PRProvider implements vscode.TreeDataProvider new PullRequest(octo, remote, item)); - if (ret.length >= 100) { - let secondPage = await octo.pullRequests.getAll({ - owner: remote.owner, - repo: remote.name, - per_page: 100, - page: 2 - }); + // if (ret.length >= 100) { + // let secondPage = await octo.pullRequests.getAll({ + // owner: remote.owner, + // repo: remote.name, + // per_page: 100, + // page: 2 + // }); - ret.push(...secondPage.data.map(item => new PullRequest(octo, remote, item))); - } + // ret.push(...secondPage.data.map(item => new PullRequest(octo, remote, item))); + // } return ret; } }); @@ -188,42 +195,35 @@ export class PRProvider implements vscode.TreeDataProvider { - if (!this._comments) { - return []; - } + // async provideComments(document: vscode.TextDocument): Promise { + // let matchingComments = await this.commentsProvider.provideComments(document.uri) - let matchingComments = getMatchingCommentsForDiffViewEditor(document.fileName, this._fileChanges, this._comments); - if (!matchingComments || !matchingComments.length) { - matchingComments = getMatchingCommentsForNormalEditor(document.fileName, this.workspaceRoot, this._comments); - } + // if (!matchingComments || !matchingComments.length) { + // return []; + // } - if (!matchingComments || !matchingComments.length) { - return []; - } + // let sections = _.groupBy(matchingComments, comment => comment.position); + // let ret = []; - let sections = _.groupBy(matchingComments, comment => comment.position); - let ret = []; + // for (let i in sections) { + // let comments = sections[i]; - for (let i in sections) { - let comments = sections[i]; + // const comment = comments[0]; + // const pos = new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0); + // const range = new vscode.Range(pos, pos); - const comment = comments[0]; - const pos = new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0); - const range = new vscode.Range(pos, pos); + // ret.push({ + // range, + // comments: comments.map(comment => { + // return { + // body: new vscode.MarkdownString(comment.body), + // userName: comment.user.login, + // gravatar: comment.user.avatar_url + // }; + // }) + // }); + // } - ret.push({ - range, - comments: comments.map(comment => { - return { - body: new vscode.MarkdownString(comment.body), - userName: comment.user.login, - gravatar: comment.user.avatar_url - }; - }) - }); - } - - return ret; - } + // return ret; + // } } diff --git a/extensions/git-extended/src/review/fileComments.ts b/extensions/git-extended/src/review/fileComments.ts index 4dd427fe658..f7f17ccefbe 100644 --- a/extensions/git-extended/src/review/fileComments.ts +++ b/extensions/git-extended/src/review/fileComments.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; -import { PullRequest } from '../common/treeItems'; +import { FileChange } from '../common/treeItems'; +import { Comment } from '../common/models/comment'; const collection = vscode.languages.createDiagnosticCollection('reviews'); -export function populatePRDiagnostics(workspaceRoot: string, pr: PullRequest) { +export function populatePRDiagnostics(workspaceRoot: string, fileChanges: FileChange[], comments: Comment[]) { // todo, if we already have fileChanges locally, then we reuse it, otherwise, compare between // pr.prItem.base.sha and pr.prItem.head.sha collection.clear(); - pr.fileChanges.forEach(filechange => { - let comments = pr.comments.filter(comment => comment.path === filechange.fileName); - let diags = comments.map(comment => ({ + fileChanges.forEach(filechange => { + let matchedComments = comments.filter(comment => comment.path === filechange.fileName); + let diags = matchedComments.map(comment => ({ code: '', message: `@${comment.user.login}: ${comment.body}`, range: new vscode.Range(new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0), new vscode.Position(comment.diff_hunk_range.start + comment.position - 1 - 1, 0)), @@ -22,7 +22,7 @@ export function populatePRDiagnostics(workspaceRoot: string, pr: PullRequest) { source: '', relatedInformation: [] })); - collection.set(vscode.Uri.file(path.resolve(workspaceRoot, filechange.fileName)), diags); + collection.set(filechange.filePath, diags); }); } diff --git a/extensions/git-extended/src/review/index.ts b/extensions/git-extended/src/review/index.ts index 754fc15d69d..c65672f8ac3 100644 --- a/extensions/git-extended/src/review/index.ts +++ b/extensions/git-extended/src/review/index.ts @@ -6,29 +6,68 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { Repository } from '../common/models/repository'; -import { fetch, checkout } from '../common/operation'; import { populatePRDiagnostics } from './FileComments'; -import { PullRequest } from '../common/treeItems'; +import { PullRequest, FileChange } from '../common/treeItems'; +import { Comment } from '../common/models/comment'; +import { toGitUri } from '../common/uri'; +import { CommentsProvider } from '../commentsProvider'; -export async function enterReviewMode(workspaceRoot: string, repository: Repository, pr: PullRequest, gitRepo: any) { - // git fetch ${pr.remote.remoteName} pull/${pr.prItem.number}/head:pull-request-${pr.prItem.number} - await fetch(repository, pr.remote.remoteName, `pull/${pr.prItem.number}/head:pull-request-${pr.prItem.number}`); - await checkout(repository, `pull-request-${pr.prItem.number}`); +const REVIEW_STATE = 'git-extended.state'; - if (!pr.fileChanges || !pr.comments) { +export function parseCommitDiff(repository: Repository, head: string, base: string, fileChanges: FileChange[]): FileChange[] { + let ret = fileChanges.map(fileChange => { + let parentFilePath = toGitUri(vscode.Uri.parse(fileChange.fileName), null, base, {}); + let filePath = toGitUri(vscode.Uri.parse(fileChange.fileName), null, head, {}); + return new FileChange(fileChange.prItem, fileChange.label, fileChange.status, fileChange.context, fileChange.fileName, filePath, parentFilePath, fileChange.workspaceRoot); + }); + + return ret; +} + +export async function restoreReviewState(repository: Repository, workspaceState: vscode.Memento, gitRepo: any, commentsProvider: CommentsProvider) { + let branch = repository.HEAD.name; + + if (!branch) { return; } - populatePRDiagnostics(workspaceRoot, pr); + let state = workspaceState.get(`${REVIEW_STATE}:${branch}`); - let prChangeResources = pr.fileChanges.map(fileChange => ({ - resourceUri: vscode.Uri.file(path.resolve(workspaceRoot, fileChange.fileName)), + if (!state) { + return; + } + + // we are in review mode + let fileChanges: FileChange[] = state['fileChanges']; + let comments: Comment[] = state['comments']; + + if (!fileChanges || !comments) { + return; + } + + let localFileChanges = parseCommitDiff(repository, state['head'].sha, state['base'].sha, fileChanges); + + populatePRDiagnostics(repository.path, localFileChanges, comments); + const commentsCache = new Map(); + localFileChanges.forEach(changedItem => { + let matchingComments = comments.filter(comment => comment.path === changedItem.fileName); + commentsCache.set(changedItem.filePath.toString(), matchingComments); + }); + commentsProvider.registerCommentProvider({ + provideComments: async (uri: vscode.Uri) => { + let matchingComments = commentsCache.get(uri.toString()); + return matchingComments || []; + } + }); + + let prChangeResources = localFileChanges.map(fileChange => ({ + resourceUri: fileChange.filePath, command: { title: 'show diff', command: 'vscode.diff', arguments: [ - vscode.Uri.file(path.resolve(workspaceRoot, fileChange.parentFilePath)), - vscode.Uri.file(path.resolve(workspaceRoot, fileChange.filePath)), + fileChange.parentFilePath, + fileChange.filePath, fileChange.fileName ] } @@ -36,4 +75,43 @@ export async function enterReviewMode(workspaceRoot: string, repository: Reposit let prGroup: vscode.SourceControlResourceGroup = gitRepo._sourceControl.createResourceGroup('pr', 'Changes from PR'); prGroup.resourceStates = prChangeResources; +} + +export async function enterReviewMode(workspaceState: vscode.Memento, repository: Repository, pr: PullRequest, gitRepo: any) { + workspaceState.update(`${REVIEW_STATE}:pull-request-${pr.prItem.number}`, { + remote: pr.remote.remoteName, + prNumber: pr.prItem.number, + branch: `pull-request-${pr.prItem.number}`, + head: pr.prItem.head, + base: pr.prItem.base, + fileChanges: pr.fileChanges.map(filechange => ( + { + fileName: filechange.fileName, + parentFilePath: filechange.parentFilePath, + filePath: filechange.filePath + })), + comments: pr.comments + }).then(e => { + if (!pr.fileChanges || !pr.comments) { + return; + } + + populatePRDiagnostics(repository.path, pr.fileChanges, pr.comments); + + let prChangeResources = pr.fileChanges.map(fileChange => ({ + resourceUri: vscode.Uri.file(path.resolve(repository.path, fileChange.fileName)), + command: { + title: 'show diff', + command: 'vscode.diff', + arguments: [ + fileChange.parentFilePath, + fileChange.filePath, + fileChange.fileName + ] + } + })); + + let prGroup: vscode.SourceControlResourceGroup = gitRepo._sourceControl.createResourceGroup('pr', 'Changes from PR'); + prGroup.resourceStates = prChangeResources; + }); } \ No newline at end of file diff --git a/extensions/git-extended/yarn.lock b/extensions/git-extended/yarn.lock index 810e24c331c..5672fd701d9 100644 --- a/extensions/git-extended/yarn.lock +++ b/extensions/git-extended/yarn.lock @@ -15,6 +15,10 @@ node-fetch "^2.1.1" url-template "^2.0.8" +"@types/lodash@^4.14.106": + version "4.14.106" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd" + "@types/node@*": version "9.6.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.2.tgz#e49ac1adb458835e95ca6487bc20f916b37aff23"