diff --git a/.travis.yml b/.travis.yml index 3883b53806037..ab6f48a9a0cb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,7 @@ cache: - packages/preview/node_modules - packages/process/node_modules - packages/python/node_modules + - packages/scm-extra/node_modules - packages/scm/node_modules - packages/search-in-workspace/node_modules - packages/task/node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index e8e3ec689b3c7..224f26b616ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Breaking changes: Most browsers define a page as served from secure context if its url has `https` scheme. For local testing `localhost` is treated as a secure context as well. Unfortunately, it does not work nicely in FireFox, since it does not treat subdomains of localhost as secure as well, compare to Chrome. If you want to test with FireFox you can configure it as described [here](https://github.com/eclipse-theia/theia/pull/6465#issuecomment-556443218). +- [scm][git] the History view (GitHistoryWidget) has moved from the git package to a new package, scm-extra, and renamed to ScmHistoryWidget. GitNavigableListWidget has also moved. [6381](https://github.com/eclipse-theia/theia/pull/6381) ## v0.12.0 diff --git a/examples/browser/package.json b/examples/browser/package.json index 1f3a45df3706f..69a13c43eeea1 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -49,7 +49,7 @@ "@theia/preview": "^0.12.0", "@theia/process": "^0.12.0", "@theia/python": "^0.12.0", - "@theia/scm": "^0.12.0", + "@theia/scm-extra": "^0.12.0", "@theia/search-in-workspace": "^0.12.0", "@theia/task": "^0.12.0", "@theia/terminal": "^0.12.0", diff --git a/examples/browser/test/left-panel/left-panel.ts b/examples/browser/test/left-panel/left-panel.ts index ae444495f5c0f..ec26eb667392a 100644 --- a/examples/browser/test/left-panel/left-panel.ts +++ b/examples/browser/test/left-panel/left-panel.ts @@ -90,12 +90,12 @@ export class LeftPanel { } isGitHistoryContainerVisible(): boolean { - return (this.driver.isExisting('#git-history') && this.driver.element('#git-history').getAttribute('class').split(' ').indexOf('p-mod-hidden') === -1 + return (this.driver.isExisting('#scm-history') && this.driver.element('#scm-history').getAttribute('class').split(' ').indexOf('p-mod-hidden') === -1 && this.isPanelVisible()); } waitForGitHistoryViewVisible(): void { - this.driver.waitForVisible('#git-history'); + this.driver.waitForVisible('#scm-history'); // Wait for animations to finish this.driver.pause(300); } diff --git a/examples/browser/test/top-panel/top-panel.ts b/examples/browser/test/top-panel/top-panel.ts index ca2c8f377127b..00e922a7cc19c 100644 --- a/examples/browser/test/top-panel/top-panel.ts +++ b/examples/browser/test/top-panel/top-panel.ts @@ -41,12 +41,12 @@ export class TopPanel { toggleScmView(): void { this.clickMenuTab('View'); - this.clickSubMenu('SCM'); + this.clickSubMenu('Source Control'); } toggleGitHistoryView(): void { this.clickMenuTab('View'); - this.clickSubMenu('Git History'); + this.clickSubMenu('Source History'); } toggleOutlineView(): void { diff --git a/examples/electron/package.json b/examples/electron/package.json index c9836b3d413a7..cea1f86d36e6c 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -46,6 +46,7 @@ "@theia/preview": "^0.12.0", "@theia/process": "^0.12.0", "@theia/python": "^0.12.0", + "@theia/scm-extra": "^0.12.0", "@theia/search-in-workspace": "^0.12.0", "@theia/task": "^0.12.0", "@theia/terminal": "^0.12.0", diff --git a/packages/git/package.json b/packages/git/package.json index 02006fab637a9..d707a68772e78 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -9,6 +9,7 @@ "@theia/languages": "^0.12.0", "@theia/navigator": "^0.12.0", "@theia/scm": "^0.12.0", + "@theia/scm-extra": "^0.12.0", "@theia/workspace": "^0.12.0", "@types/diff": "^3.2.2", "@types/p-queue": "^2.3.1", diff --git a/packages/git/src/browser/blame/blame-contribution.ts b/packages/git/src/browser/blame/blame-contribution.ts index 07806ca6ac83f..9de4206697eea 100644 --- a/packages/git/src/browser/blame/blame-contribution.ts +++ b/packages/git/src/browser/blame/blame-contribution.ts @@ -21,7 +21,7 @@ import { BlameDecorator } from './blame-decorator'; import { EditorManager, EditorKeybindingContexts, EditorWidget, EditorTextFocusContext, StrictEditorTextFocusContext } from '@theia/editor/lib/browser'; import { BlameManager } from './blame-manager'; import URI from '@theia/core/lib/common/uri'; -import { EDITOR_CONTEXT_MENU_GIT } from '../git-contribution'; +import { EDITOR_CONTEXT_MENU_SCM } from '@theia/scm-extra/lib/browser/scm-extra-contribution'; import debounce = require('lodash.debounce'); @@ -136,7 +136,7 @@ export class BlameContribution implements CommandContribution, KeybindingContrib } registerMenus(menus: MenuModelRegistry): void { - menus.registerMenuAction(EDITOR_CONTEXT_MENU_GIT, { + menus.registerMenuAction(EDITOR_CONTEXT_MENU_SCM, { commandId: BlameCommands.TOGGLE_GIT_ANNOTATIONS.id, }); } diff --git a/packages/git/src/browser/diff/git-diff-contribution.ts b/packages/git/src/browser/diff/git-diff-contribution.ts index ad6363a3dd626..082a4368b612a 100644 --- a/packages/git/src/browser/diff/git-diff-contribution.ts +++ b/packages/git/src/browser/diff/git-diff-contribution.ts @@ -19,6 +19,7 @@ import { FrontendApplication, AbstractViewContribution } from '@theia/core/lib/b import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; import { injectable, inject } from 'inversify'; import { GitDiffWidget, GIT_DIFF } from './git-diff-widget'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { open, OpenerService } from '@theia/core/lib/browser'; import { NavigatorContextMenu, FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution'; import { UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; @@ -27,8 +28,7 @@ import { FileSystem } from '@theia/filesystem/lib/common'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import URI from '@theia/core/lib/common/uri'; import { GIT_RESOURCE_SCHEME } from '../git-resource'; -import { Git } from '../../common'; -import { GitRepositoryProvider } from '../git-repository-provider'; +import { Git, Repository } from '../../common'; import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; @@ -65,7 +65,7 @@ export class GitDiffContribution extends AbstractViewContribution @inject(FileSystem) protected readonly fileSystem: FileSystem, @inject(OpenerService) protected openerService: OpenerService, @inject(MessageService) protected readonly notifications: MessageService, - @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider + @inject(ScmService) protected readonly scmService: ScmService ) { super({ widgetId: GIT_DIFF, @@ -85,8 +85,8 @@ export class GitDiffContribution extends AbstractViewContribution registerCommands(commands: CommandRegistry): void { commands.registerCommand(GitDiffCommands.OPEN_FILE_DIFF, this.newWorkspaceRootUriAwareCommandHandler({ - isVisible: uri => !!this.repositoryProvider.findRepository(uri), - isEnabled: uri => !!this.repositoryProvider.findRepository(uri), + isVisible: uri => !!this.findGitRepository(uri), + isEnabled: uri => !!this.findGitRepository(uri), execute: async fileUri => { await this.quickOpenService.chooseTagsAndBranches( async (fromRevision, toRevision) => { @@ -112,7 +112,7 @@ export class GitDiffContribution extends AbstractViewContribution } } } - }, this.repositoryProvider.findRepository(fileUri)); + }, this.findGitRepository(fileUri)); } })); } @@ -126,6 +126,14 @@ export class GitDiffContribution extends AbstractViewContribution }); } + protected findGitRepository(uri: URI): Repository | undefined { + const repo = this.scmService.findRepository(uri); + if (repo && repo.provider.id === 'git') { + return { localUri: repo.provider.rootUri }; + } + return undefined; + } + async showWidget(options: Git.Options.Diff): Promise { const widget = await this.widget; await widget.setContent(options); diff --git a/packages/git/src/browser/diff/git-diff-widget.tsx b/packages/git/src/browser/diff/git-diff-widget.tsx index 71e0dc4c68e5d..cd3e07c427d4f 100644 --- a/packages/git/src/browser/diff/git-diff-widget.tsx +++ b/packages/git/src/browser/diff/git-diff-widget.tsx @@ -18,12 +18,16 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { StatefulWidget, SELECTED_CLASS, DiffUris, Message } from '@theia/core/lib/browser'; import { EditorManager, EditorOpenerOptions, EditorWidget, DiffNavigatorProvider, DiffNavigator } from '@theia/editor/lib/browser'; +import { ScmRepository } from '@theia/scm/lib/browser/scm-repository'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { GitFileChange, GitFileStatus, Git, WorkingDirectoryStatus } from '../../common'; +import { GitScmProvider, GitScmFileChange } from '../git-scm-provider'; import { GitWatcher } from '../../common'; import { GIT_RESOURCE_SCHEME } from '../git-resource'; -import { GitNavigableListWidget } from '../git-navigable-list-widget'; +import { ScmNavigableListWidget } from '@theia/scm-extra/lib/browser/scm-navigable-list-widget'; import { GitFileChangeNode } from '../git-file-change-node'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { GitRepositoryProvider } from '../git-repository-provider'; import * as React from 'react'; import { MaybePromise } from '@theia/core/lib/common/types'; @@ -31,7 +35,7 @@ import { MaybePromise } from '@theia/core/lib/common/types'; export const GIT_DIFF = 'git-diff'; @injectable() -export class GitDiffWidget extends GitNavigableListWidget implements StatefulWidget { +export class GitDiffWidget extends ScmNavigableListWidget implements StatefulWidget { protected readonly GIT_DIFF_TITLE = 'Diff'; @@ -45,9 +49,11 @@ export class GitDiffWidget extends GitNavigableListWidget imp protected deferredListContainer = new Deferred(); @inject(Git) protected readonly git: Git; + @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider; @inject(DiffNavigatorProvider) protected readonly diffNavigatorProvider: DiffNavigatorProvider; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(GitWatcher) protected readonly gitWatcher: GitWatcher; + @inject(ScmService) protected readonly sucmService: ScmService; constructor() { super(); @@ -90,8 +96,10 @@ export class GitDiffWidget extends GitNavigableListWidget imp async setContent(options: Git.Options.Diff): Promise { this.options = options; - const repository = this.repositoryProvider.findRepositoryOrSelected(options); - if (repository) { + const scmRepository = this.findRepositoryOrSelected(options.uri); + if (scmRepository && scmRepository.provider.id === 'git') { + const provider = scmRepository.provider as GitScmProvider; + const repository = { localUri: scmRepository.provider.rootUri }; const fileChanges: GitFileChange[] = await this.git.diff(repository, { range: options.range, uri: options.uri @@ -105,9 +113,11 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.relativePath(fileChangeUri.parent) ]); - const caption = this.computeCaption(fileChange); + const gitScmFileChange = new GitScmFileChange(fileChange, provider, options.range); + const caption = this.computeCaption(gitScmFileChange); + const statusCaption = gitScmFileChange.getStatusCaption(); fileChangeNodes.push({ - ...fileChange, icon, label, description, caption + ...fileChange, icon, label, description, caption, statusCaption }); } this.fileChangeNodes = fileChangeNodes; @@ -115,6 +125,13 @@ export class GitDiffWidget extends GitNavigableListWidget imp } } + protected findRepositoryOrSelected(uri?: string): ScmRepository | undefined { + if (uri) { + return this.scmService.findRepository(new URI(uri)); + } + return this.scmService.selectedRepository; + } + storeState(): object { const { fileChangeNodes, options } = this; return { @@ -138,7 +155,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp } protected render(): React.ReactNode { - this.gitNodes = this.fileChangeNodes; + this.scmNodes = this.fileChangeNodes; const commitishBar = this.renderDiffListHeader(); const fileChangeList = this.renderFileChangeList(); return
{commitishBar}{fileChangeList}
; @@ -255,7 +272,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp protected doAddGitDiffListKeyListeners(id: string): void { const container = document.getElementById(id); if (container) { - this.addGitListNavigationKeyListeners(container); + this.addListNavigationKeyListeners(container); } } @@ -283,7 +300,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp
- {this.getStatusCaption(change.status, true).charAt(0)} + {change.statusCaption ? change.statusCaption.charAt(0) : undefined}
; } @@ -305,8 +322,8 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.revealChange(selected); } }); - } else if (this.gitNodes.length > 0) { - this.selectNode(this.gitNodes[0]); + } else if (this.scmNodes.length > 0) { + this.selectNode(this.scmNodes[0]); this.openSelected(); } } @@ -333,19 +350,19 @@ export class GitDiffWidget extends GitNavigableListWidget imp protected selectNextNode(): void { const idx = this.indexOfSelected; - if (idx >= 0 && idx < this.gitNodes.length - 1) { - this.selectNode(this.gitNodes[idx + 1]); - } else if (this.gitNodes.length > 0 && (idx === -1 || idx === this.gitNodes.length - 1)) { - this.selectNode(this.gitNodes[0]); + if (idx >= 0 && idx < this.scmNodes.length - 1) { + this.selectNode(this.scmNodes[idx + 1]); + } else if (this.scmNodes.length > 0 && (idx === -1 || idx === this.scmNodes.length - 1)) { + this.selectNode(this.scmNodes[0]); } } protected selectPreviousNode(): void { const idx = this.indexOfSelected; if (idx > 0) { - this.selectNode(this.gitNodes[idx - 1]); + this.selectNode(this.scmNodes[idx - 1]); } else if (idx === 0) { - this.selectNode(this.gitNodes[this.gitNodes.length - 1]); + this.selectNode(this.scmNodes[this.scmNodes.length - 1]); } } diff --git a/packages/git/src/browser/git-contribution.ts b/packages/git/src/browser/git-contribution.ts index 56ae33284a6fc..21672f63a3372 100644 --- a/packages/git/src/browser/git-contribution.ts +++ b/packages/git/src/browser/git-contribution.ts @@ -18,7 +18,7 @@ import URI from '@theia/core/lib/common/uri'; import { Command, CommandContribution, CommandRegistry, DisposableCollection, MenuContribution, MenuModelRegistry, Mutable, MenuAction } from '@theia/core'; import { DiffUris, Widget } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { EDITOR_CONTEXT_MENU, EditorContextMenu, EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser'; +import { EditorContextMenu, EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser'; import { Git, GitFileChange, GitFileStatus } from '../common'; import { GitRepositoryTracker } from './git-repository-tracker'; import { GitAction, GitQuickOpenService } from './git-quick-open-service'; @@ -31,8 +31,6 @@ import { ScmResource, ScmCommand } from '@theia/scm/lib/browser/scm-provider'; import { ProgressService } from '@theia/core/lib/common/progress-service'; import { GitPreferences } from './git-preferences'; -export const EDITOR_CONTEXT_MENU_GIT = [...EDITOR_CONTEXT_MENU, '3_git']; - export namespace GIT_COMMANDS { export const CLONE = { id: 'git.clone', diff --git a/packages/git/src/browser/git-file-change-node.ts b/packages/git/src/browser/git-file-change-node.ts index d1adeb7e8ac6b..c8c5f51a4f3b4 100644 --- a/packages/git/src/browser/git-file-change-node.ts +++ b/packages/git/src/browser/git-file-change-node.ts @@ -21,6 +21,7 @@ export interface GitFileChangeNode extends GitFileChange { readonly label: string; readonly description: string; readonly caption?: string; + readonly statusCaption?: string; readonly extraIconClassName?: string; readonly commitSha?: string; selected?: boolean; diff --git a/packages/git/src/browser/git-frontend-module.ts b/packages/git/src/browser/git-frontend-module.ts index a71b0f71c5a80..70c3497cb2ada 100644 --- a/packages/git/src/browser/git-frontend-module.ts +++ b/packages/git/src/browser/git-frontend-module.ts @@ -42,6 +42,8 @@ import { GitCommitMessageValidator } from './git-commit-message-validator'; import { GitSyncService } from './git-sync-service'; import { GitErrorHandler } from './git-error-handler'; import { GitScmProvider } from './git-scm-provider'; +import { GitHistorySupport } from './history/git-history-support'; +import { ScmHistorySupport } from '@theia/scm-extra/lib/browser/history/scm-history-widget'; export default new ContainerModule(bind => { bindGitPreferences(bind); @@ -64,9 +66,16 @@ export default new ContainerModule(bind => { bind(GitResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(GitResourceResolver); - bind(GitScmProvider.Factory).toFactory(GitScmProvider.createFactory); + bind(GitScmProvider.ContainerFactory).toFactory(GitScmProvider.createFactory); bind(GitRepositoryProvider).toSelf().inSingletonScope(); bind(GitQuickOpenService).toSelf().inSingletonScope(); + bind(GitScmProvider.ScmTypeContainer).toDynamicValue(({ container }) => { + const child = container.createChild(); + child.bind(GitScmProvider).toSelf().inTransientScope(); + child.bind(GitHistorySupport).toSelf().inTransientScope(); + child.bind(ScmHistorySupport).toService(GitHistorySupport); + return child; + }).inSingletonScope(); bind(LabelProviderContribution).to(GitUriLabelProviderContribution).inSingletonScope(); bind(NavigatorTreeDecorator).to(GitDecorator).inSingletonScope(); diff --git a/packages/git/src/browser/git-repository-provider.spec.ts b/packages/git/src/browser/git-repository-provider.spec.ts index 0c2c3085a2e69..9a5db6122b3ae 100644 --- a/packages/git/src/browser/git-repository-provider.spec.ts +++ b/packages/git/src/browser/git-repository-provider.spec.ts @@ -99,7 +99,12 @@ describe('GitRepositoryProvider', () => { testContainer.bind(FileSystemWatcher).toConstantValue(mockFileSystemWatcher); testContainer.bind(StorageService).toConstantValue(mockStorageService); testContainer.bind(ScmService).toSelf().inSingletonScope(); - testContainer.bind(GitScmProvider.Factory).toFactory(GitScmProvider.createFactory); + testContainer.bind(GitScmProvider.ContainerFactory).toFactory(GitScmProvider.createFactory); + testContainer.bind(GitScmProvider.ScmTypeContainer).toDynamicValue(({ container }) => { + const child = container.createChild(); + child.bind(GitScmProvider).toSelf().inTransientScope(); + return child; + }).inSingletonScope(); testContainer.bind(ScmContextKeyService).toSelf().inSingletonScope(); testContainer.bind(ContextKeyService).toSelf().inSingletonScope(); testContainer.bind(GitCommitMessageValidator).toSelf().inSingletonScope(); diff --git a/packages/git/src/browser/git-repository-provider.ts b/packages/git/src/browser/git-repository-provider.ts index 0d7dc94832092..d8ffe3de136f4 100644 --- a/packages/git/src/browser/git-repository-provider.ts +++ b/packages/git/src/browser/git-repository-provider.ts @@ -21,7 +21,6 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service import { FileSystem } from '@theia/filesystem/lib/common'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { StorageService } from '@theia/core/lib/browser/storage-service'; -import URI from '@theia/core/lib/common/uri'; import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { Git, Repository } from '../common'; import { GitCommitMessageValidator } from './git-commit-message-validator'; @@ -40,8 +39,8 @@ export class GitRepositoryProvider { protected readonly selectedRepoStorageKey = 'theia-git-selected-repository'; protected readonly allRepoStorageKey = 'theia-git-all-repositories'; - @inject(GitScmProvider.Factory) - protected readonly scmProviderFactory: GitScmProvider.Factory; + @inject(GitScmProvider.ContainerFactory) + protected readonly scmProviderFactory: GitScmProvider.ContainerFactory; @inject(GitCommitMessageValidator) protected readonly commitMessageValidator: GitCommitMessageValidator; @@ -128,29 +127,6 @@ export class GitRepositoryProvider { return repositories; } - findRepository(uri: URI): Repository | undefined { - const reposSorted = this.allRepositories.sort(Repository.sortComparator); - return reposSorted.find(repo => new URI(repo.localUri).isEqualOrParent(uri)); - } - - findRepositoryOrSelected(arg: URI | string | { uri?: string | URI } | undefined): Repository | undefined { - let uri: URI | string | undefined; - if (arg) { - if (arg instanceof URI || typeof arg === 'string') { - uri = arg; - } else if (typeof arg === 'object' && 'uri' in arg && arg.uri) { - uri = arg.uri; - } - if (uri) { - if (typeof uri === 'string') { - uri = new URI(uri); - } - return this.findRepository(uri); - } - } - return this.selectedRepository; - } - async refresh(options?: GitRefreshOptions): Promise { const repositories: Repository[] = []; const refreshing: Promise[] = []; @@ -191,7 +167,8 @@ export class GitRepositoryProvider { } protected registerScmProvider(repository: Repository): void { - const provider = this.scmProviderFactory({ repository }); + const providerContainer = this.scmProviderFactory({ repository }); + const provider = providerContainer.get(GitScmProvider); this.scmService.registerScmProvider(provider, { input: { placeholder: 'Message (press {0} to commit)', @@ -201,7 +178,8 @@ export class GitRepositoryProvider { message: issue.message, type: issue.status }; - } + }, + providerContainer } }); } diff --git a/packages/git/src/browser/git-scm-provider.ts b/packages/git/src/browser/git-scm-provider.ts index a8ac340e6aa1b..53a47f8f07709 100644 --- a/packages/git/src/browser/git-scm-provider.ts +++ b/packages/git/src/browser/git-scm-provider.ts @@ -30,7 +30,8 @@ import { Repository, Git, CommitWithChanges, GitFileChange, WorkingDirectoryStat import { GIT_RESOURCE_SCHEME } from './git-resource'; import { GitErrorHandler } from './git-error-handler'; import { EditorWidget } from '@theia/editor/lib/browser'; -import { ScmProvider, ScmCommand, ScmResourceGroup, ScmAmendSupport, ScmCommit } from '@theia/scm/lib/browser/scm-provider'; +import { ScmProvider, ScmCommand, ScmResourceGroup, ScmAmendSupport, ScmCommit, ScmFileChange } from '@theia/scm/lib/browser/scm-provider'; +import { GitCommitDetailWidgetOptions } from './history/git-commit-detail-widget'; @injectable() export class GitScmProviderOptions { @@ -84,7 +85,7 @@ export class GitScmProvider implements ScmProvider { @postConstruct() protected init(): void { - this._amendSupport = new GitAmendSupport(this.repository, this.git); + this._amendSupport = new GitAmendSupport(this, this.repository, this.git); } get repository(): Repository { @@ -362,8 +363,59 @@ export class GitScmProvider implements ScmProvider { await Promise.all(uris.map(uri => this.delete(new URI(uri)))); } + public createScmCommit(gitCommit: CommitWithChanges): ScmCommit { + const range = { + fromRevision: gitCommit.sha + '~1', + toRevision: gitCommit.sha + }; + + const scmCommit: GitScmCommit = { + id: gitCommit.sha, + commitDetailUri: this.toCommitDetailUri(gitCommit.sha), + summary: gitCommit.summary, + messageBody: gitCommit.body, + authorName: gitCommit.author.name, + authorEmail: gitCommit.author.email, + authorTimestamp: gitCommit.author.timestamp, + authorDateRelative: gitCommit.authorDateRelative, + scmProvider: this, + gitFileChanges: gitCommit.fileChanges.map(change => new GitScmFileChange(change, this, range)), + get fileChanges(): ScmFileChange[] { + return this.gitFileChanges; + }, + get commitDetailOptions(): GitCommitDetailWidgetOptions { + return { + commitSha: gitCommit.sha, + commitMessage: gitCommit.summary, + messageBody: gitCommit.body, + authorName: gitCommit.author.name, + authorEmail: gitCommit.author.email, + authorDate: gitCommit.author.timestamp, + authorDateRelative: gitCommit.authorDateRelative, + }; + } + }; + return scmCommit; + } + + public relativePath(uri: string): string { + const parsedUri = new URI(uri); + const gitRepo = { localUri: this.rootUri }; + const relativePath = Repository.relativePath(gitRepo, parsedUri); + if (relativePath) { + return relativePath.toString(); + } + return this.labelProvider.getLongName(parsedUri); + } + + protected toCommitDetailUri(commitSha: string): URI { + return new URI('').withScheme(GitScmProvider.GIT_COMMIT_DETAIL).withFragment(commitSha); + } + } export namespace GitScmProvider { + export const GIT_COMMIT_DETAIL = 'git-commit-detail-widget'; + export interface State { status?: WorkingDirectoryStatus stagedChanges: GitFileChange[] @@ -380,21 +432,22 @@ export namespace GitScmProvider { groups: [] }; } - export const Factory = Symbol('GitScmProvider.Factory'); - export type Factory = (options: GitScmProviderOptions) => GitScmProvider; - export function createFactory(ctx: interfaces.Context): Factory { + export type ContainerFactory = (options: GitScmProviderOptions) => interfaces.Container; + export function createFactory(ctx: interfaces.Context): ContainerFactory { + const typeContainer = ctx.container.get(GitScmProvider.ScmTypeContainer as interfaces.ServiceIdentifier); return (options: GitScmProviderOptions) => { - const container = ctx.container.createChild(); + const container = typeContainer.createChild(); container.bind(GitScmProviderOptions).toConstantValue(options); - container.bind(GitScmProvider).toSelf().inSingletonScope(); - return container.get(GitScmProvider); + return container; }; } + export const ScmTypeContainer = Symbol('GitScmProvider.TypeContainer'); + export const ContainerFactory = Symbol('GitScmProvider.ProviderContainer'); } export class GitAmendSupport implements ScmAmendSupport { - constructor(protected readonly repository: Repository, protected readonly git: Git) { } + constructor(protected readonly provider: GitScmProvider, protected readonly repository: Repository, protected readonly git: Git) { } public async getInitialAmendingCommits(amendingHeadCommitSha: string, latestCommitSha: string | undefined): Promise { const commits = await this.git.log( @@ -406,7 +459,7 @@ export class GitAmendSupport implements ScmAmendSupport { } ); - return commits.map(this.createScmCommit); + return commits.map(commit => this.provider.createScmCommit(commit)); } public async getMessage(commit: string): Promise { @@ -429,23 +482,67 @@ export class GitAmendSupport implements ScmAmendSupport { public async getLastCommit(): Promise { const commits = await this.git.log(this.repository, { maxCount: 1 }); if (commits.length > 0) { - return this.createScmCommit(commits[0]); + return this.provider.createScmCommit(commits[0]); } } +} - private createScmCommit(gitCommit: CommitWithChanges): { - id: string; - summary: string; - authorName: string; - authorEmail: string; - authorDateRelative: string; - } { - return { - id: gitCommit.sha, - summary: gitCommit.summary, - authorName: gitCommit.author.name, - authorEmail: gitCommit.author.email, - authorDateRelative: gitCommit.authorDateRelative - }; +export interface GitScmCommit extends ScmCommit { + scmProvider: GitScmProvider; + gitFileChanges: GitScmFileChange[]; +} + +export class GitScmFileChange implements ScmFileChange { + + constructor( + protected readonly fileChange: GitFileChange, + protected readonly scmProvider: GitScmProvider, + protected readonly range?: Git.Options.Range + ) { } + + get uri(): string { + return this.fileChange.uri; + } + + getCaption(): string { + const provider = this.scmProvider; + let result = `${provider.relativePath(this.fileChange.uri)} - ${GitFileStatus.toString(this.fileChange.status, true)}`; + if (this.fileChange.oldUri) { + result = `${provider.relativePath(this.fileChange.oldUri)} -> ${result}`; + } + return result; + } + + getStatusCaption(): string { + return GitFileStatus.toString(this.fileChange.status, this.fileChange.staged); + } + + getStatusAbbreviation(): string { + return GitFileStatus.toAbbreviation(this.fileChange.status, this.fileChange.staged); + } + + getClassNameForStatus(): string { + return GitFileStatus[this.fileChange.status]; + } + + getUriToOpen(): URI { + const uri: URI = new URI(this.fileChange.uri); + const fromFileURI = this.fileChange.oldUri ? new URI(this.fileChange.oldUri) : uri; // set oldUri on renamed and copied + if (!this.range) { + return uri; + } + const fromRevision = this.range.fromRevision || 'HEAD'; + const toRevision = this.range.toRevision || 'HEAD'; + const fromURI = fromFileURI.withScheme(GIT_RESOURCE_SCHEME).withQuery(fromRevision.toString()); + const toURI = uri.withScheme(GIT_RESOURCE_SCHEME).withQuery(toRevision.toString()); + let uriToOpen = uri; + if (this.fileChange.status === GitFileStatus.Deleted) { + uriToOpen = fromURI; + } else if (this.fileChange.status === GitFileStatus.New) { + uriToOpen = toURI; + } else { + uriToOpen = DiffUris.encode(fromURI, toURI); + } + return uriToOpen; } } diff --git a/packages/git/src/browser/history/git-commit-detail-open-handler.ts b/packages/git/src/browser/history/git-commit-detail-open-handler.ts index 079c930167619..1e584f42fda84 100644 --- a/packages/git/src/browser/history/git-commit-detail-open-handler.ts +++ b/packages/git/src/browser/history/git-commit-detail-open-handler.ts @@ -17,13 +17,11 @@ import { injectable } from 'inversify'; import { WidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; -import { GIT_COMMIT_DETAIL, GitCommitDetailWidgetOptions, GitCommitDetailWidget, GitCommitDetails } from './git-commit-detail-widget'; +import { GitCommitDetailWidgetOptions, GitCommitDetailWidget } from './git-commit-detail-widget'; +import { GitScmProvider } from '../git-scm-provider'; export namespace GitCommitDetailUri { - export const scheme = GIT_COMMIT_DETAIL; - export function toUri(commitSha: string): URI { - return new URI('').withScheme(scheme).withFragment(commitSha); - } + export const scheme = GitScmProvider.GIT_COMMIT_DETAIL; export function toCommitSha(uri: URI): string { if (uri.scheme === scheme) { return uri.fragment; @@ -36,7 +34,7 @@ export type GitCommitDetailOpenerOptions = WidgetOpenerOptions & GitCommitDetail @injectable() export class GitCommitDetailOpenHandler extends WidgetOpenHandler { - readonly id = GIT_COMMIT_DETAIL; + readonly id = GitScmProvider.GIT_COMMIT_DETAIL; canHandle(uri: URI): number { try { @@ -48,31 +46,17 @@ export class GitCommitDetailOpenHandler extends WidgetOpenHandler { - widget.setContent({ range: options.range }); + widget.setContent({ + range: { + fromRevision: options.commitSha + '~1', + toRevision: options.commitSha + } + }); await super.doOpen(widget, options); } protected createWidgetOptions(uri: URI, commit: GitCommitDetailOpenerOptions): GitCommitDetailWidgetOptions { - return this.getCommitDetailWidgetOptions(commit); - } - - getCommitDetailWidgetOptions(commit: GitCommitDetails): GitCommitDetailWidgetOptions { - const range = { - fromRevision: commit.commitSha + '~1', - toRevision: commit.commitSha - }; - return { - range, - authorAvatar: commit.authorAvatar, - authorDate: commit.authorDate, - authorDateRelative: commit.authorDateRelative, - authorEmail: commit.authorEmail, - authorName: commit.authorName, - commitMessage: commit.commitMessage, - fileChangeNodes: commit.fileChangeNodes, - messageBody: commit.messageBody, - commitSha: commit.commitSha - }; + return commit; } } diff --git a/packages/git/src/browser/history/git-commit-detail-widget.tsx b/packages/git/src/browser/history/git-commit-detail-widget.tsx index 8e7e7b2d6fb91..445e79274761f 100644 --- a/packages/git/src/browser/history/git-commit-detail-widget.tsx +++ b/packages/git/src/browser/history/git-commit-detail-widget.tsx @@ -14,50 +14,55 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import { Widget } from '@phosphor/widgets'; import { LabelProvider } from '@theia/core/lib/browser'; -import { Git, GitFileChange } from '../../common'; +import { GitFileChange } from '../../common'; import { GitDiffWidget } from '../diff/git-diff-widget'; import { GitRepositoryProvider } from '../git-repository-provider'; -import { GitFileChangeNode } from '../git-file-change-node'; +import { ScmAvatarService } from '@theia/scm/lib/browser/scm-avatar-service'; import * as React from 'react'; -export const GIT_COMMIT_DETAIL = 'git-commit-detail-widget'; - -export interface GitCommitDetails { - readonly authorName: string; - readonly authorEmail: string; - readonly authorDate: Date; - readonly authorDateRelative: string; - readonly authorAvatar: string; - readonly commitMessage: string; - readonly messageBody?: string; - readonly fileChangeNodes: GitFileChangeNode[]; - readonly commitSha: string; -} - export const GitCommitDetailWidgetOptions = Symbol('GitCommitDetailWidgetOptions'); -export interface GitCommitDetailWidgetOptions extends GitCommitDetails { - readonly range: Git.Options.Range +export interface GitCommitDetailWidgetOptions { + commitSha: string; + commitMessage: string; + messageBody?: string; + authorName: string; + authorEmail: string; + authorDate: string; + authorDateRelative: string; } @injectable() export class GitCommitDetailWidget extends GitDiffWidget { + protected authorAvatar: string; + constructor( @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider, @inject(LabelProvider) protected readonly labelProvider: LabelProvider, + @inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService, @inject(GitCommitDetailWidgetOptions) protected readonly commitDetailOptions: GitCommitDetailWidgetOptions ) { super(); this.id = 'commit' + commitDetailOptions.commitSha; - this.title.label = commitDetailOptions.commitSha; - this.options = { range: commitDetailOptions.range }; + this.title.label = commitDetailOptions.commitSha.substr(0, 8); + this.options = { + range: { + fromRevision: commitDetailOptions.commitSha + '~1', + toRevision: commitDetailOptions.commitSha + } + }; this.title.closable = true; this.title.iconClass = 'icon-git-commit tab-git-icon'; } + @postConstruct() + protected async init(): Promise { + this.authorAvatar = await this.avatarService.getAvatar(this.commitDetailOptions.authorEmail); + } + protected renderDiffListHeader(): React.ReactNode { const authorEMail = this.commitDetailOptions.authorEmail; const subject =
{this.commitDetailOptions.commitMessage}
; @@ -83,7 +88,7 @@ export class GitCommitDetailWidget extends GitDiffWidget {
{this.commitDetailOptions.commitSha}
; const gravatar =
-
; + ; const commitInfo =
{gravatar}
{authorRow}{mailRow}{dateRow}{revisionRow}
; const header =
Files changed
; diff --git a/packages/git/src/browser/history/git-history-contribution.ts b/packages/git/src/browser/history/git-history-contribution.ts deleted file mode 100644 index 4a23bfa852f17..0000000000000 --- a/packages/git/src/browser/history/git-history-contribution.ts +++ /dev/null @@ -1,136 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { MenuModelRegistry, CommandRegistry, Command, SelectionService } from '@theia/core'; -import { AbstractViewContribution, OpenViewArguments } from '@theia/core/lib/browser'; -import { injectable, inject, postConstruct } from 'inversify'; -import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution'; -import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; -import URI from '@theia/core/lib/common/uri'; -import { GitHistoryWidget } from './git-history-widget'; -import { Git } from '../../common'; -import { GitRepositoryTracker } from '../git-repository-tracker'; -import { GitRepositoryProvider } from '../git-repository-provider'; -import { EDITOR_CONTEXT_MENU_GIT } from '../git-contribution'; - -export const GIT_HISTORY_ID = 'git-history'; -export const GIT_HISTORY_LABEL = 'Git History'; -export const GIT_HISTORY_TOGGLE_KEYBINDING = 'alt+h'; -export const GIT_HISTORY_MAX_COUNT = 100; - -export namespace GitHistoryCommands { - export const OPEN_FILE_HISTORY: Command = { - id: 'git-history:open-file-history', - }; - export const OPEN_BRANCH_HISTORY: Command = { - id: 'git-history:open-branch-history', - label: GIT_HISTORY_LABEL - }; -} - -export interface GitHistoryOpenViewArguments extends OpenViewArguments { - uri: string | undefined; -} - -@injectable() -export class GitHistoryContribution extends AbstractViewContribution { - - @inject(SelectionService) - protected readonly selectionService: SelectionService; - @inject(GitRepositoryTracker) - protected readonly repositoryTracker: GitRepositoryTracker; - @inject(GitRepositoryProvider) - protected readonly repositoryProvider: GitRepositoryProvider; - - constructor() { - super({ - widgetId: GIT_HISTORY_ID, - widgetName: GIT_HISTORY_LABEL, - defaultWidgetOptions: { - area: 'left', - rank: 500 - }, - toggleCommandId: GitHistoryCommands.OPEN_BRANCH_HISTORY.id, - toggleKeybinding: GIT_HISTORY_TOGGLE_KEYBINDING - }); - } - - @postConstruct() - protected init(): void { - this.repositoryTracker.onDidChangeRepository(async repository => { - this.refreshWidget(repository ? repository.localUri : undefined); - } - ); - this.repositoryTracker.onGitEvent(event => { - const { source, status, oldStatus } = event || { source: undefined, status: undefined, oldStatus: undefined }; - let isBranchChanged = false; - let isHeaderChanged = false; - if (oldStatus) { - isBranchChanged = !!status && status.branch !== oldStatus.branch; - isHeaderChanged = !!status && status.currentHead !== oldStatus.currentHead; - } - if (isBranchChanged || isHeaderChanged || oldStatus === undefined) { - this.refreshWidget(source && source.localUri); - } - }); - } - - async openView(args?: Partial): Promise { - const widget = await super.openView(args); - this.refreshWidget(args!.uri); - return widget; - } - - registerMenus(menus: MenuModelRegistry): void { - menus.registerMenuAction(NavigatorContextMenu.SEARCH, { - commandId: GitHistoryCommands.OPEN_FILE_HISTORY.id, - label: GIT_HISTORY_LABEL - }); - menus.registerMenuAction(EDITOR_CONTEXT_MENU_GIT, { - commandId: GitHistoryCommands.OPEN_FILE_HISTORY.id, - label: GIT_HISTORY_LABEL - }); - super.registerMenus(menus); - } - - registerCommands(commands: CommandRegistry): void { - commands.registerCommand(GitHistoryCommands.OPEN_FILE_HISTORY, this.newUriAwareCommandHandler({ - isEnabled: (uri: URI) => !!this.repositoryProvider.findRepository(uri), - isVisible: (uri: URI) => !!this.repositoryProvider.findRepository(uri), - execute: async uri => this.openView({ activate: true, uri: uri.toString() }), - })); - super.registerCommands(commands); - } - - protected async refreshWidget(uri: string | undefined): Promise { - const widget = this.tryGetWidget(); - if (!widget) { - // the widget doesn't exist, so don't wake it up - return; - } - const options: Git.Options.Log = { - uri, - maxCount: GIT_HISTORY_MAX_COUNT, - shortSha: true - }; - await widget.setContent(options); - } - - protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { - return new UriAwareCommandHandler(this.selectionService, handler); - } - -} diff --git a/packages/git/src/browser/history/git-history-frontend-module.ts b/packages/git/src/browser/history/git-history-frontend-module.ts index a9ae42b4a4814..7526691e4cb84 100644 --- a/packages/git/src/browser/history/git-history-frontend-module.ts +++ b/packages/git/src/browser/history/git-history-frontend-module.ts @@ -14,27 +14,20 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GitHistoryContribution, GIT_HISTORY_ID } from './git-history-contribution'; import { interfaces, Container } from 'inversify'; -import { WidgetFactory, OpenHandler, bindViewContribution } from '@theia/core/lib/browser'; -import { GitHistoryWidget } from './git-history-widget'; -import { GIT_COMMIT_DETAIL, GitCommitDetailWidget, GitCommitDetails, GitCommitDetailWidgetOptions } from './git-commit-detail-widget'; +import { WidgetFactory, OpenHandler } from '@theia/core/lib/browser'; +import { GitCommitDetailWidget, GitCommitDetailWidgetOptions } from './git-commit-detail-widget'; +import { GitCommitDetailOpenHandler } from './git-commit-detail-open-handler'; +import { GitScmProvider } from '../git-scm-provider'; +import { ScmCommitNode } from '@theia/scm-extra/lib/browser/history/scm-history-widget'; -import '../../../src/browser/style/history.css'; import '../../../src/browser/style/git-icons.css'; -import { GitCommitDetailOpenHandler } from './git-commit-detail-open-handler'; export function bindGitHistoryModule(bind: interfaces.Bind): void { - bind(GitHistoryWidget).toSelf(); - bind(WidgetFactory).toDynamicValue(ctx => ({ - id: GIT_HISTORY_ID, - createWidget: () => ctx.container.get(GitHistoryWidget) - })); - bind(WidgetFactory).toDynamicValue(ctx => ({ - id: GIT_COMMIT_DETAIL, - createWidget: (options: GitCommitDetails) => { + id: GitScmProvider.GIT_COMMIT_DETAIL, + createWidget: (options: ScmCommitNode) => { const child = new Container({ defaultScope: 'Singleton' }); child.parent = ctx.container; child.bind(GitCommitDetailWidget).toSelf(); @@ -46,6 +39,4 @@ export function bindGitHistoryModule(bind: interfaces.Bind): void { bind(GitCommitDetailOpenHandler).toSelf(); bind(OpenHandler).toService(GitCommitDetailOpenHandler); - bindViewContribution(bind, GitHistoryContribution); - } diff --git a/packages/git/src/browser/history/git-history-support.tsx b/packages/git/src/browser/history/git-history-support.tsx new file mode 100644 index 0000000000000..2cb49a5b5e70c --- /dev/null +++ b/packages/git/src/browser/history/git-history-support.tsx @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { Emitter, Disposable } from '@theia/core'; +import { Git } from '../../common'; +import { ScmHistorySupport, HistoryWidgetOptions } from '@theia/scm-extra/lib/browser/history/scm-history-widget'; +import { ScmCommit } from '@theia/scm/lib/browser/scm-provider'; +import { GitScmProvider } from '../git-scm-provider'; +import { GitRepositoryTracker } from '../git-repository-tracker'; + +@injectable() +export class GitHistorySupport implements ScmHistorySupport { + + @inject(GitScmProvider) protected readonly provider: GitScmProvider; + @inject(Git) protected readonly git: Git; + @inject(GitRepositoryTracker) protected readonly repositoryTracker: GitRepositoryTracker; + + async getCommitHistory(options?: HistoryWidgetOptions): Promise { + const repository = this.provider.repository; + const gitOptions: Git.Options.Log = { + uri: options ? options.uri : undefined, + maxCount: options ? options.maxCount : undefined, + shortSha: true + }; + + const commits = await this.git.log(repository, gitOptions); + if (commits.length > 0) { + return commits.map(commit => this.provider.createScmCommit(commit)); + } else { + const pathIsUnderVersionControl = !options || !options.uri || await this.git.lsFiles(repository, options.uri, { errorUnmatch: true }); + if (!pathIsUnderVersionControl) { + throw new Error('It is not under version control.'); + } else { + throw new Error('No commits have been committed.'); + } + } + } + + protected readonly onDidChangeHistoryEmitter = new Emitter({ + onFirstListenerAdd: () => this.onFirstListenerAdd(), + onLastListenerRemove: () => this.onLastListenerRemove() + }); + readonly onDidChangeHistory = this.onDidChangeHistoryEmitter.event; + + protected onGitEventDisposable: Disposable | undefined; + protected onFirstListenerAdd(): void { + this.onGitEventDisposable = this.repositoryTracker.onGitEvent(event => { + const { status, oldStatus } = event || { status: undefined, oldStatus: undefined }; + let isBranchChanged = false; + let isHeaderChanged = false; + if (oldStatus) { + isBranchChanged = !!status && status.branch !== oldStatus.branch; + isHeaderChanged = !!status && status.currentHead !== oldStatus.currentHead; + } + if (isBranchChanged || isHeaderChanged || oldStatus === undefined) { + this.onDidChangeHistoryEmitter.fire(undefined); + } + }); + } + + protected onLastListenerRemove(): void { + if (this.onGitEventDisposable) { + this.onGitEventDisposable.dispose(); + this.onGitEventDisposable = undefined; + } + } +} diff --git a/packages/git/src/common/git-model.ts b/packages/git/src/common/git-model.ts index 60bc971940ab1..35579d5c3b10b 100644 --- a/packages/git/src/common/git-model.ts +++ b/packages/git/src/common/git-model.ts @@ -153,7 +153,7 @@ export interface GitFileChange { readonly oldUri?: string; /** - * `true` if the file is staged, otherwise `false`. If absent, it means, not staged. + * `true` if the file is staged or committed, `false` if not staged. If absent, it means not staged. */ readonly staged?: boolean; @@ -195,11 +195,10 @@ export namespace Repository { export function is(repository: Object | undefined): repository is Repository { return !!repository && 'localUri' in repository; } - export function relativePath(repository: Repository | URI | string, uri: URI | string): Path | undefined { + export function relativePath(repository: Repository | URI, uri: URI | string): Path | undefined { const repositoryUri = new URI(Repository.is(repository) ? repository.localUri : String(repository)); return repositoryUri.relative(new URI(String(uri))); } - export const sortComparator = (ra: Repository, rb: Repository) => rb.localUri.length - ra.localUri.length; } /** @@ -339,7 +338,7 @@ export interface CommitWithChanges extends Commit { readonly authorDateRelative: string; /** - * The number of file changes per commit. + * The file changes in the commit. */ readonly fileChanges: GitFileChange[]; } diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index f1ff64aab230d..9c06c9593c06f 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -90,14 +90,16 @@ export class NameStatusParser extends OutputParser { changes.push({ status, uri, - oldUri + oldUri, + staged: true }); index = index + 3; } else { const uri = this.toUri(repositoryUri, items[index + 1]); changes.push({ status, - uri + uri, + staged: true }); index = index + 2; } diff --git a/packages/scm-extra/compile.tsconfig.json b/packages/scm-extra/compile.tsconfig.json new file mode 100644 index 0000000000000..a23513b5e6b13 --- /dev/null +++ b/packages/scm-extra/compile.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/packages/scm-extra/package.json b/packages/scm-extra/package.json new file mode 100644 index 0000000000000..201e9eee52cb3 --- /dev/null +++ b/packages/scm-extra/package.json @@ -0,0 +1,57 @@ +{ + "name": "@theia/scm-extra", + "version": "0.12.0", + "description": "Theia - Source control extras Extension", + "dependencies": { + "@theia/core": "^0.12.0", + "@theia/editor": "^0.12.0", + "@theia/filesystem": "^0.12.0", + "@theia/navigator": "^0.12.0", + "@theia/scm": "^0.12.0", + "@types/diff": "^3.2.2", + "@types/p-debounce": "^1.0.1", + "diff": "^3.4.0", + "p-debounce": "^2.1.0", + "react-autosize-textarea": "^7.0.0", + "ts-md5": "^1.2.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/scm-extra-frontend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "theiaext clean", + "build": "theiaext build", + "watch": "theiaext watch", + "test": "theiaext test --no-cluster", + "test:watch": "theiaext test:watch", + "docs": "theiaext docs" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.12.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/scm-extra/src/browser/history/scm-history-contribution.ts b/packages/scm-extra/src/browser/history/scm-history-contribution.ts new file mode 100644 index 0000000000000..6942981aae35e --- /dev/null +++ b/packages/scm-extra/src/browser/history/scm-history-contribution.ts @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (C) 2018 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { MenuModelRegistry, CommandRegistry, Command, SelectionService } from '@theia/core'; +import { AbstractViewContribution, OpenViewArguments } from '@theia/core/lib/browser'; +import { injectable, inject } from 'inversify'; +import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution'; +import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import URI from '@theia/core/lib/common/uri'; +import { ScmHistoryWidget } from './scm-history-widget'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; +import { EDITOR_CONTEXT_MENU_SCM } from '../scm-extra-contribution'; + +export const SCM_HISTORY_ID = 'scm-history'; +export const SCM_HISTORY_LABEL = 'Source History'; +export const SCM_HISTORY_TOGGLE_KEYBINDING = 'alt+h'; +export const SCM_HISTORY_MAX_COUNT = 100; + +export namespace ScmHistoryCommands { + export const OPEN_FILE_HISTORY: Command = { + id: 'scm-history:open-file-history', + }; + export const OPEN_BRANCH_HISTORY: Command = { + id: 'scm-history:open-branch-history', + label: SCM_HISTORY_LABEL + }; +} + +export interface ScmHistoryOpenViewArguments extends OpenViewArguments { + uri: string | undefined; +} + +@injectable() +export class ScmHistoryContribution extends AbstractViewContribution { + + @inject(SelectionService) + protected readonly selectionService: SelectionService; + @inject(ScmService) + protected readonly scmService: ScmService; + + constructor() { + super({ + widgetId: SCM_HISTORY_ID, + widgetName: SCM_HISTORY_LABEL, + defaultWidgetOptions: { + area: 'left', + rank: 500 + }, + toggleCommandId: ScmHistoryCommands.OPEN_BRANCH_HISTORY.id, + toggleKeybinding: SCM_HISTORY_TOGGLE_KEYBINDING + }); + } + + async openView(args?: Partial): Promise { + const widget = await super.openView(args); + this.refreshWidget(args!.uri); + return widget; + } + + registerMenus(menus: MenuModelRegistry): void { + menus.registerMenuAction(NavigatorContextMenu.SEARCH, { + commandId: ScmHistoryCommands.OPEN_FILE_HISTORY.id, + label: SCM_HISTORY_LABEL + }); + menus.registerMenuAction(EDITOR_CONTEXT_MENU_SCM, { + commandId: ScmHistoryCommands.OPEN_FILE_HISTORY.id, + label: SCM_HISTORY_LABEL + }); + super.registerMenus(menus); + } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(ScmHistoryCommands.OPEN_FILE_HISTORY, this.newUriAwareCommandHandler({ + isEnabled: (uri: URI) => !!this.scmService.findRepository(uri), + isVisible: (uri: URI) => !!this.scmService.findRepository(uri), + execute: async uri => this.openView({ activate: true, uri: uri.toString() }), + })); + super.registerCommands(commands); + } + + protected async refreshWidget(uri: string | undefined): Promise { + const widget = this.tryGetWidget(); + if (!widget) { + // the widget doesn't exist, so don't wake it up + return; + } + await widget.setContent({ uri }); + } + + protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { + return new UriAwareCommandHandler(this.selectionService, handler); + } + +} diff --git a/packages/scm-extra/src/browser/history/scm-history-frontend-module.ts b/packages/scm-extra/src/browser/history/scm-history-frontend-module.ts new file mode 100644 index 0000000000000..27990073ecb52 --- /dev/null +++ b/packages/scm-extra/src/browser/history/scm-history-frontend-module.ts @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { interfaces } from 'inversify'; +import { ScmHistoryContribution, SCM_HISTORY_ID } from './scm-history-contribution'; +import { WidgetFactory, bindViewContribution } from '@theia/core/lib/browser'; +import { ScmHistoryWidget } from './scm-history-widget'; + +import '../../../src/browser/style/history.css'; + +export function bindScmHistoryModule(bind: interfaces.Bind): void { + + bind(ScmHistoryWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(ctx => ({ + id: SCM_HISTORY_ID, + createWidget: () => ctx.container.get(ScmHistoryWidget) + })); + + bindViewContribution(bind, ScmHistoryContribution); + +} diff --git a/packages/git/src/browser/history/git-history-widget.tsx b/packages/scm-extra/src/browser/history/scm-history-widget.tsx similarity index 60% rename from packages/git/src/browser/history/git-history-widget.tsx rename to packages/scm-extra/src/browser/history/scm-history-widget.tsx index 6052f7438bd93..a243e5ff4d780 100644 --- a/packages/git/src/browser/history/git-history-widget.tsx +++ b/packages/scm-extra/src/browser/history/scm-history-widget.tsx @@ -15,47 +15,77 @@ ********************************************************************************/ import { injectable, inject, postConstruct } from 'inversify'; -import { DiffUris } from '@theia/core/lib/browser/diff-uris'; +import { Event as TheiaEvent, DisposableCollection } from '@theia/core'; import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell } from '@theia/core/lib/browser'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { Message } from '@phosphor/messaging'; import { AutoSizer, List, ListRowRenderer, ListRowProps, InfiniteLoader, IndexRange, ScrollParams, CellMeasurerCache, CellMeasurer } from 'react-virtualized'; -import { GIT_RESOURCE_SCHEME } from '../git-resource'; import URI from '@theia/core/lib/common/uri'; -import { GIT_HISTORY_ID, GIT_HISTORY_MAX_COUNT, GIT_HISTORY_LABEL } from './git-history-contribution'; -import { GitFileStatus, Git, GitFileChange, Repository } from '../../common'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; +import { SCM_HISTORY_ID, SCM_HISTORY_MAX_COUNT, SCM_HISTORY_LABEL } from './scm-history-contribution'; +import { ScmCommit, ScmFileChange } from '@theia/scm/lib/browser/scm-provider'; import { FileSystem } from '@theia/filesystem/lib/common'; -import { GitDiffContribution } from '../diff/git-diff-contribution'; import { ScmAvatarService } from '@theia/scm/lib/browser/scm-avatar-service'; -import { GitCommitDetailUri, GitCommitDetailOpenerOptions, GitCommitDetailOpenHandler } from './git-commit-detail-open-handler'; -import { GitCommitDetails } from './git-commit-detail-widget'; -import { GitNavigableListWidget } from '../git-navigable-list-widget'; -import { GitFileChangeNode } from '../git-file-change-node'; +import { ScmNavigableListWidget } from '../scm-navigable-list-widget'; import * as React from 'react'; import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message'; import { DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider'; -export interface GitCommitNode extends GitCommitDetails { - fileChanges?: GitFileChange[]; +export const ScmHistorySupport = Symbol('scm-history-support'); +export interface ScmHistorySupport { + getCommitHistory(options?: HistoryWidgetOptions): Promise; + readonly onDidChangeHistory: TheiaEvent; +} + +export interface ScmCommitNode { + commitDetails: ScmCommit; + authorAvatar: string; + fileChangeNodes?: ScmFileChangeNode[]; expanded: boolean; selected: boolean; } +export interface ScmFileChangeNode { + readonly fileChange: ScmFileChange; + readonly icon: string; + readonly label: string; + readonly description: string; + readonly caption?: string; + readonly extraIconClassName?: string; + readonly commitSha?: string; + selected?: boolean; +} + +export interface HistoryWidgetOptions { + readonly range?: { + readonly toRevision?: string; + readonly fromRevision?: string; + }; + readonly uri?: string; + readonly maxCount?: number; +} + +export namespace ScmCommitNode { + // tslint:disable-next-line:no-any + export function is(node: any): node is ScmCommitNode { + return !!node && 'commitDetails' in node && 'expanded' in node && 'selected' in node; + } +} -export namespace GitCommitNode { +export namespace ScmFileChangeNode { // tslint:disable-next-line:no-any - export function is(node: any): node is GitCommitNode { - return !!node && 'commitSha' in node && 'commitMessage' in node && 'fileChangeNodes' in node; + export function is(node: any): node is ScmFileChangeNode { + return !!node && 'fileChange' in node && 'icon' in node && 'label' in node && 'description' in node; } } -export type GitHistoryListNode = (GitCommitNode | GitFileChangeNode); +export type ScmHistoryListNode = (ScmCommitNode | ScmFileChangeNode); @injectable() -export class GitHistoryWidget extends GitNavigableListWidget implements StatefulWidget { - protected options: Git.Options.Log; +export class ScmHistoryWidget extends ScmNavigableListWidget implements StatefulWidget { + protected options: HistoryWidgetOptions; protected singleFileMode: boolean; private cancelIndicator: CancellationTokenSource; - protected listView: GitHistoryList | undefined; + protected listView: ScmHistoryList | undefined; protected hasMoreCommits: boolean; protected allowScrollToSelected: boolean; @@ -63,27 +93,30 @@ export class GitHistoryWidget extends GitNavigableListWidget state: 'loading', } | { state: 'ready', - commits: GitCommitNode[]; + commits: ScmCommitNode[]; } | { state: 'error', errorMessage: React.ReactNode }; + protected readonly toDisposeOnRepositoryChange = new DisposableCollection(); + + protected historySupport: ScmHistorySupport | undefined; + constructor( + @inject(ScmService) protected readonly scmService: ScmService, @inject(OpenerService) protected readonly openerService: OpenerService, - @inject(GitCommitDetailOpenHandler) protected readonly detailOpenHandler: GitCommitDetailOpenHandler, @inject(ApplicationShell) protected readonly shell: ApplicationShell, @inject(FileSystem) protected readonly fileSystem: FileSystem, - @inject(Git) protected readonly git: Git, @inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService, @inject(WidgetManager) protected readonly widgetManager: WidgetManager, - @inject(GitDiffContribution) protected readonly diffContribution: GitDiffContribution) { + ) { super(); - this.id = GIT_HISTORY_ID; + this.id = SCM_HISTORY_ID; this.scrollContainer = 'git-history-list-container'; - this.title.label = GIT_HISTORY_LABEL; - this.title.caption = GIT_HISTORY_LABEL; - this.title.iconClass = 'fa git-history-tab-icon'; + this.title.label = SCM_HISTORY_LABEL; + this.title.caption = SCM_HISTORY_LABEL; + this.title.iconClass = 'fa scm-history-tab-icon'; this.title.closable = true; this.addClass('theia-git'); this.resetState(); @@ -92,19 +125,21 @@ export class GitHistoryWidget extends GitNavigableListWidget @postConstruct() protected init(): void { + this.refreshOnRepositoryChange(); + this.toDispose.push(this.scmService.onDidChangeSelectedRepository(() => this.refreshOnRepositoryChange())); this.toDispose.push(this.labelProvider.onDidChange(event => this.refreshLabels(event))); } protected async refreshLabels(event: DidChangeLabelEvent): Promise { let isAnyAffectedNodes = false; - for (let i = 0; i < this.gitNodes.length; i++) { - const gitNode = this.gitNodes[i]; - if (GitFileChangeNode.is(gitNode)) { - const uri = new URI(gitNode.uri); + for (let i = 0; i < this.scmNodes.length; i++) { + const scmNode = this.scmNodes[i]; + if (ScmFileChangeNode.is(scmNode)) { + const uri = new URI(scmNode.fileChange.uri); if (event.affects(uri)) { const label = this.labelProvider.getName(uri); const icon = await this.labelProvider.getIcon(uri); - this.gitNodes[i] = { ...gitNode, label, icon }; + this.scmNodes[i] = { ...scmNode, label, icon }; isAnyAffectedNodes = true; } } @@ -114,9 +149,54 @@ export class GitHistoryWidget extends GitNavigableListWidget } } + protected refreshOnRepositoryChange(): void { + this.toDisposeOnRepositoryChange.dispose(); + + const repository = this.scmService.selectedRepository; + if (repository) { + this.historySupport = repository.input.get(ScmHistorySupport); + this.setContent(this.options); + if (this.historySupport) { + this.toDisposeOnRepositoryChange.push(this.historySupport.onDidChangeHistory(() => this.setContent(this.options))); + } + } else { + this.historySupport = undefined; + } + + // If switching repository, discard options because they are specific to a repository + this.options = {}; + + this.refresh(); + } + + protected readonly toDisposeOnRefresh = new DisposableCollection(); + protected refresh(): void { + this.toDisposeOnRefresh.dispose(); + this.toDispose.push(this.toDisposeOnRefresh); + const repository = this.scmService.selectedRepository; + this.title.label = SCM_HISTORY_LABEL; + if (repository) { + this.title.label += ': ' + repository.provider.label; + } + const area = this.shell.getAreaFor(this); + if (area === 'left') { + this.shell.leftPanelHandler.refresh(); + } else if (area === 'right') { + this.shell.rightPanelHandler.refresh(); + } + this.update(); + + if (repository) { + this.toDisposeOnRefresh.push(repository.onDidChange(() => this.update())); + // render synchronously to avoid cursor jumping + // see https://stackoverflow.com/questions/28922275/in-reactjs-why-does-setstate-behave-differently-when-called-synchronously/28922465#28922465 + this.toDisposeOnRefresh.push(repository.input.onDidChange(() => this.setContent(this.options))); + } + } + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); - this.addGitListNavigationKeyListeners(this.node); + this.addListNavigationKeyListeners(this.node); // tslint:disable-next-line:no-any this.addEventListener(this.node, 'ps-scroll-y', (e: Event & { target: { scrollTop: number } }) => { if (this.listView && this.listView.list && this.listView.list.Grid) { @@ -133,7 +213,7 @@ export class GitHistoryWidget extends GitNavigableListWidget super.update(); } - async setContent(options?: Git.Options.Log): Promise { + async setContent(options?: HistoryWidgetOptions): Promise { this.resetState(options); if (options && options.uri) { const fileStat = await this.fileSystem.getFileStat(options.uri); @@ -141,83 +221,70 @@ export class GitHistoryWidget extends GitNavigableListWidget } await this.addCommits(options); this.onDataReady(); - if (this.gitNodes.length > 0) { - this.selectNode(this.gitNodes[0]); + if (this.scmNodes.length > 0) { + this.selectNode(this.scmNodes[0]); } } - protected resetState(options?: Git.Options.Log): void { + protected resetState(options?: HistoryWidgetOptions): void { this.options = options || {}; this.status = { state: 'loading' }; - this.gitNodes = []; + this.scmNodes = []; this.hasMoreCommits = true; this.allowScrollToSelected = true; } - protected async addCommits(options?: Git.Options.Log): Promise { - let repository: Repository | undefined; - repository = this.repositoryProvider.findRepositoryOrSelected(options); + protected async addCommits(options?: HistoryWidgetOptions): Promise { + const repository = this.scmService.selectedRepository; this.cancelIndicator.cancel(); this.cancelIndicator = new CancellationTokenSource(); const token = this.cancelIndicator.token; if (repository) { - try { - const currentCommits = this.status.state === 'ready' ? this.status.commits : []; + if (this.historySupport) { + try { + const currentCommits = this.status.state === 'ready' ? this.status.commits : []; - let changes = await this.git.log(repository, options); - if (token.isCancellationRequested || !this.hasMoreCommits) { - return; - } - if (options && ((options.maxCount && changes.length < options.maxCount) || (!options.maxCount && currentCommits))) { - this.hasMoreCommits = false; - } - if (currentCommits.length > 0) { - changes = changes.slice(1); - } - if (changes.length > 0) { - const commits: GitCommitNode[] = []; - for (const commit of changes) { - const fileChangeNodes: GitFileChangeNode[] = []; - const avatarUrl = await this.avatarService.getAvatar(commit.author.email); + let history = await this.historySupport.getCommitHistory(options); + if (token.isCancellationRequested || !this.hasMoreCommits) { + return; + } + + if (options && ((options.maxCount && history.length < options.maxCount) || (!options.maxCount && currentCommits))) { + this.hasMoreCommits = false; + } + if (currentCommits.length > 0) { + history = history.slice(1); + } + const commits: ScmCommitNode[] = []; + for (const commit of history) { + const avatarUrl = await this.avatarService.getAvatar(commit.authorEmail); commits.push({ - authorName: commit.author.name, - authorDate: new Date(commit.author.timestamp), - authorEmail: commit.author.email, - authorDateRelative: commit.authorDateRelative, + commitDetails: commit, authorAvatar: avatarUrl, - commitSha: commit.sha, - commitMessage: commit.summary, - messageBody: commit.body, - fileChangeNodes, - fileChanges: commit.fileChanges, expanded: false, selected: false }); } currentCommits.push(...commits); this.status = { state: 'ready', commits: currentCommits }; - } else if (options && options.uri && repository) { - const pathIsUnderVersionControl = await this.git.lsFiles(repository, options.uri, { errorUnmatch: true }); - if (!pathIsUnderVersionControl) { - this.status = { state: 'error', errorMessage: It is not under version control. }; - } else { - this.status = { state: 'error', errorMessage: No commits have been committed. }; + } catch (error) { + if (options && options.uri && repository) { + this.hasMoreCommits = false; } + this.status = { state: 'error', errorMessage: {error.message} }; } - - } catch (error) { - this.status = { state: 'error', errorMessage: error.message }; + } else { + this.status = { state: 'error', errorMessage: History is not supported for {repository.provider.label} source control. }; } - } else { this.status = { state: 'error', errorMessage: There is no repository selected in this workspace. }; } } - protected async addOrRemoveFileChangeNodes(commit: GitCommitNode): Promise { - const id = this.gitNodes.findIndex(node => node === commit); + protected async addOrRemoveFileChangeNodes(commit: ScmCommitNode): Promise { + const id = this.scmNodes.findIndex(node => node === commit); if (commit.expanded) { this.removeFileChangeNodes(commit, id); } else { @@ -227,27 +294,32 @@ export class GitHistoryWidget extends GitNavigableListWidget this.update(); } - protected async addFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number): Promise { - if (commit.fileChanges) { - const fileChangeNodes: GitFileChangeNode[] = []; - await Promise.all(commit.fileChanges.map(async fileChange => { + protected async addFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): Promise { + this.scmNodes.splice(scmNodesArrayIndex + 1, 0, ...await this.getFileChangeNodes(commit)); + } + + protected removeFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): void { + if (commit.fileChangeNodes) { + this.scmNodes.splice(scmNodesArrayIndex + 1, commit.fileChangeNodes.length); + } + } + + protected async getFileChangeNodes(commitNode: ScmCommitNode): Promise { + if (!commitNode.fileChangeNodes) { + const fileChangeNodes: ScmFileChangeNode[] = []; + await Promise.all(commitNode.commitDetails.fileChanges.map(async fileChange => { const fileChangeUri = new URI(fileChange.uri); const icon = await this.labelProvider.getIcon(fileChangeUri); const label = this.labelProvider.getName(fileChangeUri); const description = this.relativePath(fileChangeUri.parent); const caption = this.computeCaption(fileChange); fileChangeNodes.push({ - ...fileChange, icon, label, description, caption, commitSha: commit.commitSha + fileChange, icon, label, description, caption, commitSha: commitNode.commitDetails.id }); })); - this.gitNodes.splice(gitNodesArrayIndex + 1, 0, ...fileChangeNodes); - } - } - - protected removeFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number): void { - if (commit.fileChanges) { - this.gitNodes.splice(gitNodesArrayIndex + 1, commit.fileChanges.length); + commitNode.fileChangeNodes = fileChangeNodes; } + return commitNode.fileChangeNodes; } storeState(): object { @@ -267,7 +339,7 @@ export class GitHistoryWidget extends GitNavigableListWidget protected onDataReady(): void { if (this.status.state === 'ready') { - this.gitNodes = this.status.commits; + this.scmNodes = this.status.commits; } this.update(); } @@ -290,15 +362,15 @@ export class GitHistoryWidget extends GitNavigableListWidget const relPathEncoded = this.relativePath(this.options.uri); const relPath = relPathEncoded ? `${decodeURIComponent(relPathEncoded)}` : ''; - const repo = this.repositoryProvider.findRepository(new URI(this.options.uri)); - const repoName = repo ? `${new URI(repo.localUri).displayName}` : ''; + const repo = this.scmService.selectedRepository; + const repoName = repo ? `${new URI(repo.provider.rootUri).displayName}` : ''; const relPathAndRepo = [relPath, repoName].filter(Boolean).join(' in '); path = ` for ${relPathAndRepo}`; } content = + header={`There is no history available${path}.`}> {reason} ; break; @@ -334,16 +406,16 @@ export class GitHistoryWidget extends GitNavigableListWidget protected renderCommitList(): React.ReactNode { const list =
- this.listView = (listView || undefined)} - rows={this.gitNodes} + rows={this.scmNodes} hasMoreRows={this.hasMoreCommits} indexOfSelected={this.allowScrollToSelected ? this.indexOfSelected : -1} handleScroll={this.handleScroll} loadMoreRows={this.loadMoreRows} renderCommit={this.renderCommit} renderFileChangeList={this.renderFileChangeList} - > + >
; this.allowScrollToSelected = true; return list; @@ -359,12 +431,12 @@ export class GitHistoryWidget extends GitNavigableListWidget protected doLoadMoreRows(params: IndexRange): Promise { let resolver: () => void; const promise = new Promise(resolve => resolver = resolve); - const lastRow = this.gitNodes[params.stopIndex - 1]; - if (GitCommitNode.is(lastRow)) { - const toRevision = lastRow.commitSha; + const lastRow = this.scmNodes[params.stopIndex - 1]; + if (ScmCommitNode.is(lastRow)) { + const toRevision = lastRow.commitDetails.id; this.addCommits({ range: { toRevision }, - maxCount: GIT_HISTORY_MAX_COUNT, + maxCount: SCM_HISTORY_MAX_COUNT, uri: this.options.uri }).then(() => { this.allowScrollToSelected = false; @@ -375,8 +447,8 @@ export class GitHistoryWidget extends GitNavigableListWidget return promise; } - protected readonly renderCommit = (commit: GitCommitNode) => this.doRenderCommit(commit); - protected doRenderCommit(commit: GitCommitNode): React.ReactNode { + protected readonly renderCommit = (commit: ScmCommitNode) => this.doRenderCommit(commit); + protected doRenderCommit(commit: ScmCommitNode): React.ReactNode { let expansionToggleIcon = 'caret-right'; if (commit && commit.expanded) { expansionToggleIcon = 'caret-down'; @@ -395,8 +467,8 @@ export class GitHistoryWidget extends GitNavigableListWidget } onDoubleClick={ e => { - if (this.singleFileMode && commit.fileChanges && commit.fileChanges.length > 0) { - this.openFile(commit.fileChanges[0], commit.commitSha); + if (this.singleFileMode && commit.fileChangeNodes && commit.fileChangeNodes.length > 0) { + this.openFile(commit.fileChangeNodes[0].fileChange); } e.preventDefault(); } @@ -406,17 +478,17 @@ export class GitHistoryWidget extends GitNavigableListWidget
- {commit.commitMessage} + {commit.commitDetails.summary}
- {commit.authorDateRelative + ' by ' + commit.authorName} + {commit.commitDetails.authorDateRelative + ' by ' + commit.commitDetails.authorName}
this.openDetailWidget(commit)}>
{ !this.singleFileMode ?
-
{(commit.fileChanges && commit.fileChanges.length || commit.fileChangeNodes.length).toString()}
+
{commit.commitDetails.fileChanges.length.toString()}
@@ -426,44 +498,49 @@ export class GitHistoryWidget extends GitNavigableListWidget ; } - protected async openDetailWidget(commit: GitCommitNode): Promise { - const commitDetails = this.detailOpenHandler.getCommitDetailWidgetOptions(commit); - this.detailOpenHandler.open(GitCommitDetailUri.toUri(commit.commitSha), { - ...commitDetails - } as GitCommitDetailOpenerOptions); + protected async openDetailWidget(commitNode: ScmCommitNode): Promise { + const options = { + ...commitNode.commitDetails.commitDetailOptions, + mode: 'reveal' + }; + open( + this.openerService, + commitNode.commitDetails.commitDetailUri, + options + ); } - protected readonly renderFileChangeList = (fileChange: GitFileChangeNode) => this.doRenderFileChangeList(fileChange); - protected doRenderFileChangeList(fileChange: GitFileChangeNode): React.ReactNode { - const fileChangeElement: React.ReactNode = this.renderGitItem(fileChange, fileChange.commitSha || ''); + protected readonly renderFileChangeList = (fileChange: ScmFileChangeNode) => this.doRenderFileChangeList(fileChange); + protected doRenderFileChangeList(fileChange: ScmFileChangeNode): React.ReactNode { + const fileChangeElement: React.ReactNode = this.renderScmItem(fileChange, fileChange.commitSha || ''); return fileChangeElement; } - protected renderGitItem(change: GitFileChangeNode, commitSha: string): React.ReactNode { - return
+ protected renderScmItem(changeNode: ScmFileChangeNode, commitSha: string): React.ReactNode { + return
{ - this.openFile(change, commitSha); + this.openFile(changeNode.fileChange); }} onClick={() => { - this.selectNode(change); + this.selectNode(changeNode); }}> - - {change.label + ' '} - {change.description} + + {changeNode.label + ' '} + {changeNode.description}
{ - change.extraIconClassName ?
+ changeNode.extraIconClassName ?
: '' }
- {this.getStatusCaption(change.status, true).charAt(0)} + title={changeNode.caption} + className={'status staged ' + changeNode.fileChange.getClassNameForStatus().toLowerCase()}> + {changeNode.fileChange.getStatusCaption().charAt(0)}
; } @@ -471,8 +548,8 @@ export class GitHistoryWidget extends GitNavigableListWidget protected navigateLeft(): void { const selected = this.getSelected(); if (selected && this.status.state === 'ready') { - const idx = this.status.commits.findIndex(c => c.commitSha === selected.commitSha); - if (GitCommitNode.is(selected)) { + if (ScmCommitNode.is(selected)) { + const idx = this.status.commits.findIndex(c => c.commitDetails.id === selected.commitDetails.id); if (selected.expanded) { this.addOrRemoveFileChangeNodes(selected); } else { @@ -480,7 +557,8 @@ export class GitHistoryWidget extends GitNavigableListWidget this.selectNode(this.status.commits[idx - 1]); } } - } else if (GitFileChangeNode.is(selected)) { + } else if (ScmFileChangeNode.is(selected)) { + const idx = this.status.commits.findIndex(c => c.commitDetails.id === selected.commitSha); this.selectNode(this.status.commits[idx]); } } @@ -490,7 +568,7 @@ export class GitHistoryWidget extends GitNavigableListWidget protected navigateRight(): void { const selected = this.getSelected(); if (selected) { - if (GitCommitNode.is(selected) && !selected.expanded && !this.singleFileMode) { + if (ScmCommitNode.is(selected) && !selected.expanded && !this.singleFileMode) { this.addOrRemoveFileChangeNodes(selected); } else { this.selectNextNode(); @@ -499,52 +577,42 @@ export class GitHistoryWidget extends GitNavigableListWidget this.update(); } - protected handleListEnter(): void { + protected async handleListEnter(): Promise { const selected = this.getSelected(); if (selected) { - if (GitCommitNode.is(selected)) { + if (ScmCommitNode.is(selected)) { if (this.singleFileMode) { - this.openFile(selected.fileChangeNodes[0], selected.commitSha); + const fileChangeNodes = await this.getFileChangeNodes(selected); + this.openFile(fileChangeNodes[0].fileChange); } else { this.openDetailWidget(selected); } - } else if (GitFileChangeNode.is(selected)) { - this.openFile(selected, selected.commitSha || ''); + } else if (ScmFileChangeNode.is(selected)) { + this.openFile(selected.fileChange); } } this.update(); } - protected openFile(change: GitFileChange, commitSha: string): void { - const uri: URI = new URI(change.uri); - let fromURI = change.oldUri ? new URI(change.oldUri) : uri; // set oldUri on renamed and copied - fromURI = fromURI.withScheme(GIT_RESOURCE_SCHEME).withQuery(commitSha + '~1'); - const toURI = uri.withScheme(GIT_RESOURCE_SCHEME).withQuery(commitSha); - let uriToOpen = uri; - if (change.status === GitFileStatus.Deleted) { - uriToOpen = fromURI; - } else if (change.status === GitFileStatus.New) { - uriToOpen = toURI; - } else { - uriToOpen = DiffUris.encode(fromURI, toURI); - } + protected openFile(change: ScmFileChange): void { + const uriToOpen = change.getUriToOpen(); open(this.openerService, uriToOpen, { mode: 'reveal' }); } } -export namespace GitHistoryList { +export namespace ScmHistoryList { export interface Props { - readonly rows: GitHistoryListNode[] + readonly rows: ScmHistoryListNode[] readonly indexOfSelected: number readonly hasMoreRows: boolean readonly handleScroll: (info: { clientHeight: number; scrollHeight: number; scrollTop: number }) => void // tslint:disable-next-line:no-any readonly loadMoreRows: (params: IndexRange) => Promise - readonly renderCommit: (commit: GitCommitNode) => React.ReactNode - readonly renderFileChangeList: (fileChange: GitFileChangeNode) => React.ReactNode + readonly renderCommit: (commit: ScmCommitNode) => React.ReactNode + readonly renderFileChangeList: (fileChange: ScmFileChangeNode) => React.ReactNode } } -export class GitHistoryList extends React.Component { +export class ScmHistoryList extends React.Component { list: List | undefined; protected readonly checkIfRowIsLoaded = (opts: { index: number }) => this.doCheckIfRowIsLoaded(opts); @@ -615,12 +683,12 @@ export class GitHistoryList extends React.Component { protected renderRow: ListRowRenderer = ({ index, key, style }) => { if (this.checkIfRowIsLoaded({ index })) { const row = this.props.rows[index]; - if (GitCommitNode.is(row)) { + if (ScmCommitNode.is(row)) { const head = this.props.renderCommit(row); return
{head}
; - } else if (GitFileChangeNode.is(row)) { + } else if (ScmFileChangeNode.is(row)) { return
{this.props.renderFileChangeList(row)}
; diff --git a/packages/scm-extra/src/browser/scm-extra-contribution.ts b/packages/scm-extra/src/browser/scm-extra-contribution.ts new file mode 100644 index 0000000000000..6afcc3d4cb355 --- /dev/null +++ b/packages/scm-extra/src/browser/scm-extra-contribution.ts @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; + +export const EDITOR_CONTEXT_MENU_SCM = [...EDITOR_CONTEXT_MENU, '3_scm']; diff --git a/packages/scm-extra/src/browser/scm-extra-frontend-module.ts b/packages/scm-extra/src/browser/scm-extra-frontend-module.ts new file mode 100644 index 0000000000000..e8c847b92de00 --- /dev/null +++ b/packages/scm-extra/src/browser/scm-extra-frontend-module.ts @@ -0,0 +1,22 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { bindScmHistoryModule } from './history/scm-history-frontend-module'; + +export default new ContainerModule(bind => { + bindScmHistoryModule(bind); +}); diff --git a/packages/git/src/browser/git-navigable-list-widget.tsx b/packages/scm-extra/src/browser/scm-navigable-list-widget.tsx similarity index 70% rename from packages/git/src/browser/git-navigable-list-widget.tsx rename to packages/scm-extra/src/browser/scm-navigable-list-widget.tsx index e28f3a4d5a0e8..e170a42d22588 100644 --- a/packages/git/src/browser/git-navigable-list-widget.tsx +++ b/packages/scm-extra/src/browser/scm-navigable-list-widget.tsx @@ -15,9 +15,9 @@ ********************************************************************************/ import { SELECTED_CLASS, Key, Widget } from '@theia/core/lib/browser'; -import { GitFileStatus, Repository, GitFileChange } from '../common'; +import { ScmFileChange } from '@theia/scm/lib/browser/scm-provider'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; import URI from '@theia/core/lib/common/uri'; -import { GitRepositoryProvider } from './git-repository-provider'; import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { Message } from '@phosphor/messaging'; import { ElementExt } from '@phosphor/domutils'; @@ -26,12 +26,12 @@ import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import * as React from 'react'; @injectable() -export abstract class GitNavigableListWidget extends ReactWidget { +export abstract class ScmNavigableListWidget extends ReactWidget { - protected gitNodes: T[]; + protected scmNodes: T[]; private _scrollContainer: string; - @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider; + @inject(ScmService) protected readonly scmService: ScmService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; constructor() { @@ -74,36 +74,35 @@ export abstract class GitNavigableListWidget e this.update(); } - protected getStatusCaption(status: GitFileStatus, staged?: boolean): string { - return GitFileStatus.toString(status, staged); + protected getStatusCaption(status: ScmFileChange): string { + return status.getStatusCaption(); } - protected getAbbreviatedStatusCaption(status: GitFileStatus, staged?: boolean): string { - return GitFileStatus.toAbbreviation(status, staged); + protected getAbbreviatedStatusCaption(status: ScmFileChange): string { + return status.getStatusAbbreviation(); } protected relativePath(uri: URI | string): string { const parsedUri = typeof uri === 'string' ? new URI(uri) : uri; - const repo = this.repositoryProvider.findRepository(parsedUri); - const relativePath = repo && Repository.relativePath(repo, parsedUri); - if (relativePath) { - return relativePath.toString(); + const repository = this.scmService.selectedRepository; + if (repository) { + const repositoryUri = new URI(repository.provider.rootUri); + const relativePath = repositoryUri.relative(new URI(String(uri))); + if (relativePath) { + return relativePath.toString(); + } } return this.labelProvider.getLongName(parsedUri); } protected getRepositoryLabel(uri: string): string | undefined { - const repository = this.repositoryProvider.findRepository(new URI(uri)); - const isSelectedRepo = this.repositoryProvider.selectedRepository && repository && this.repositoryProvider.selectedRepository.localUri === repository.localUri; - return repository && !isSelectedRepo ? this.labelProvider.getLongName(new URI(repository.localUri)) : undefined; + const repository = this.scmService.findRepository(new URI(uri)); + const isSelectedRepo = this.scmService.selectedRepository && repository && this.scmService.selectedRepository.provider.rootUri === repository.provider.rootUri; + return repository && !isSelectedRepo ? this.labelProvider.getLongName(new URI(repository.provider.rootUri)) : undefined; } - protected computeCaption(fileChange: GitFileChange): string { - let result = `${this.relativePath(fileChange.uri)} - ${this.getStatusCaption(fileChange.status, true)}`; - if (fileChange.oldUri) { - result = `${this.relativePath(fileChange.oldUri)} -> ${result}`; - } - return result; + protected computeCaption(fileChange: ScmFileChange): string { + return fileChange.getCaption(); } protected renderHeaderRow({ name, value, classNames, title }: { name: string, value: React.ReactNode, classNames?: string[], title?: string }): React.ReactNode { @@ -117,7 +116,7 @@ export abstract class GitNavigableListWidget e
; } - protected addGitListNavigationKeyListeners(container: HTMLElement): void { + protected addListNavigationKeyListeners(container: HTMLElement): void { this.addKeyListener(container, Key.ARROW_LEFT, () => this.navigateLeft()); this.addKeyListener(container, Key.ARROW_RIGHT, () => this.navigateRight()); this.addKeyListener(container, Key.ARROW_UP, () => this.navigateUp()); @@ -146,7 +145,7 @@ export abstract class GitNavigableListWidget e } protected getSelected(): T | undefined { - return this.gitNodes ? this.gitNodes.find(c => c.selected || false) : undefined; + return this.scmNodes ? this.scmNodes.find(c => c.selected || false) : undefined; } protected selectNode(node: T): void { @@ -160,23 +159,23 @@ export abstract class GitNavigableListWidget e protected selectNextNode(): void { const idx = this.indexOfSelected; - if (idx >= 0 && idx < this.gitNodes.length - 1) { - this.selectNode(this.gitNodes[idx + 1]); - } else if (this.gitNodes.length > 0 && idx === -1) { - this.selectNode(this.gitNodes[0]); + if (idx >= 0 && idx < this.scmNodes.length - 1) { + this.selectNode(this.scmNodes[idx + 1]); + } else if (this.scmNodes.length > 0 && idx === -1) { + this.selectNode(this.scmNodes[0]); } } protected selectPreviousNode(): void { const idx = this.indexOfSelected; if (idx > 0) { - this.selectNode(this.gitNodes[idx - 1]); + this.selectNode(this.scmNodes[idx - 1]); } } protected get indexOfSelected(): number { - if (this.gitNodes && this.gitNodes.length > 0) { - return this.gitNodes.findIndex(c => c.selected || false); + if (this.scmNodes && this.scmNodes.length > 0) { + return this.scmNodes.findIndex(c => c.selected || false); } return -1; } diff --git a/packages/git/src/browser/style/history.css b/packages/scm-extra/src/browser/style/history.css similarity index 99% rename from packages/git/src/browser/style/history.css rename to packages/scm-extra/src/browser/style/history.css index 6f7368efa0831..7df898d1d5bd4 100644 --- a/packages/git/src/browser/style/history.css +++ b/packages/scm-extra/src/browser/style/history.css @@ -202,6 +202,6 @@ justify-content: center; } -.git-history-tab-icon::before { +.scm-history-tab-icon::before { content: "\f1da" } diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index afee96b6b60fd..03a69c2153197 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -68,7 +68,7 @@ export class ScmContribution extends AbstractViewContribution impleme super({ viewContainerId: SCM_VIEW_CONTAINER_ID, widgetId: SCM_WIDGET_FACTORY_ID, - widgetName: 'SCM', + widgetName: 'Source Control', defaultWidgetOptions: { area: 'left', rank: 300 diff --git a/packages/scm/src/browser/scm-input.ts b/packages/scm/src/browser/scm-input.ts index fb2792973aae6..70f62cbd5c7a9 100644 --- a/packages/scm/src/browser/scm-input.ts +++ b/packages/scm/src/browser/scm-input.ts @@ -19,6 +19,7 @@ import * as debounce from 'p-debounce'; import { Disposable, DisposableCollection, Emitter } from '@theia/core/lib/common'; import { JSONExt, JSONObject } from '@phosphor/coreutils/lib/json'; +import { interfaces } from 'inversify'; export interface ScmInputIssue { message: string; @@ -32,6 +33,7 @@ export interface ScmInputValidator { export interface ScmInputOptions { placeholder?: string validator?: ScmInputValidator + providerContainer?: interfaces.Container } export interface ScmInputData { @@ -128,4 +130,9 @@ export class ScmInput implements Disposable { } } + get(serviceIdentifier: interfaces.ServiceIdentifier): T | undefined { + return this.options.providerContainer + ? this.options.providerContainer.get(serviceIdentifier) + : undefined; + } } diff --git a/packages/scm/src/browser/scm-provider.ts b/packages/scm/src/browser/scm-provider.ts index 6c370bda7c02f..389eb1186a792 100644 --- a/packages/scm/src/browser/scm-provider.ts +++ b/packages/scm/src/browser/scm-provider.ts @@ -70,11 +70,28 @@ export interface ScmCommand { } export interface ScmCommit { - id: string, // eg Git sha or Mercurial revision number - summary: string, - authorName: string, - authorEmail: string, - authorDateRelative: string + id: string; // eg Git sha or Mercurial revision number + commitDetailUri: URI; + summary: string; + messageBody?: string; + authorName: string; + authorEmail: string; + /** + * The date of the commit in ISO format. + */ + authorTimestamp: string; + authorDateRelative: string; + fileChanges: ScmFileChange[]; + commitDetailOptions: {}; +} + +export interface ScmFileChange { + uri: string; + getCaption(): string; + getStatusCaption(): string; + getStatusAbbreviation(): string; + getClassNameForStatus(): string; + getUriToOpen(): URI; } export interface ScmAmendSupport { diff --git a/packages/scm/src/browser/scm-service.ts b/packages/scm/src/browser/scm-service.ts index 89429527ade6c..a2e65c5840ac0 100644 --- a/packages/scm/src/browser/scm-service.ts +++ b/packages/scm/src/browser/scm-service.ts @@ -21,6 +21,7 @@ import { injectable, inject } from 'inversify'; import { ScmContextKeyService } from './scm-context-key-service'; import { ScmRepository, ScmProviderOptions } from './scm-repository'; import { ScmCommand, ScmProvider } from './scm-provider'; +import URI from '@theia/core/lib/common/uri'; @injectable() export class ScmService { @@ -76,6 +77,13 @@ export class ScmService { this.fireDidChangeStatusBarCommands(); } + findRepository(uri: URI): ScmRepository | undefined { + const reposSorted = this.repositories.sort( + (ra: ScmRepository, rb: ScmRepository) => rb.provider.rootUri.length - ra.provider.rootUri.length + ); + return reposSorted.find(repo => new URI(repo.provider.rootUri).isEqualOrParent(uri)); + } + registerScmProvider(provider: ScmProvider, options: ScmProviderOptions = {}): ScmRepository { const key = provider.id + ':' + provider.rootUri; if (this._repositories.has(key)) { diff --git a/tsconfig.json b/tsconfig.json index 42d04f1a9c071..8d9156c5e60bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -106,6 +106,9 @@ "@theia/scm/lib/*": [ "packages/scm/src/*" ], + "@theia/scm-extra/lib/*": [ + "packages/scm-extra/src/*" + ], "@theia/getting-started/lib/*": [ "packages/getting-started/src/*" ],