diff --git a/.travis.yml b/.travis.yml index f5f6552332632..330dcfb1235b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,6 +53,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 0e8ae43dcbb4e..6ba45b71200f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Breaking changes: Before these attributes have to be computed for all nodes and stored as a part of the layout. From now on they will be computed only on demand for visible nodes. It decreases requirements to the local storage and allows to invalidate node appearance by simply rerendering a tree. +- [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.14.0 diff --git a/examples/browser/package.json b/examples/browser/package.json index f890769ca85f5..5c81cb2008344 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -50,6 +50,7 @@ "@theia/process": "^0.14.0", "@theia/python": "^0.14.0", "@theia/scm": "^0.14.0", + "@theia/scm-extra": "^0.14.0", "@theia/search-in-workspace": "^0.14.0", "@theia/task": "^0.14.0", "@theia/terminal": "^0.14.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 cff097033782b..560d5ca409b65 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -46,6 +46,8 @@ "@theia/preview": "^0.14.0", "@theia/process": "^0.14.0", "@theia/python": "^0.14.0", + "@theia/scm": "^0.14.0", + "@theia/scm-extra": "^0.14.0", "@theia/search-in-workspace": "^0.14.0", "@theia/task": "^0.14.0", "@theia/terminal": "^0.14.0", diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 27f63b3034c98..796e0af31391b 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -50,12 +50,14 @@ export type ApplicationShellLayoutVersion = /** layout versioning is introduced, unversiouned layout are not compatible */ 2.0 | /** view containers are introduced, backward compatible to 2.0 */ - 3.0; + 3.0 | + /** git history view is replaced by a more generic scm history view, backward compatible to 3.0 */ + 4.0; /** * When a version is increased, make sure to introduce a migration (ApplicationShellLayoutMigration) to this version. */ -export const applicationShellLayoutVersion: ApplicationShellLayoutVersion = 3.0; +export const applicationShellLayoutVersion: ApplicationShellLayoutVersion = 4.0; export const ApplicationShellOptions = Symbol('ApplicationShellOptions'); export const DockPanelRendererFactory = Symbol('DockPanelRendererFactory'); diff --git a/packages/git/package.json b/packages/git/package.json index 33fb310d1fca9..243e6632f8eee 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -9,6 +9,7 @@ "@theia/languages": "^0.14.0", "@theia/navigator": "^0.14.0", "@theia/scm": "^0.14.0", + "@theia/scm-extra": "^0.14.0", "@theia/workspace": "^0.14.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 4aad153daa8b3..5dc2748a1f156 100644 --- a/packages/git/src/browser/diff/git-diff-widget.tsx +++ b/packages/git/src/browser/diff/git-diff-widget.tsx @@ -18,20 +18,26 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { StatefulWidget, 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, GitItemComponent } from '../git-navigable-list-widget'; -import { GitFileChangeNode } from '../git-file-change-node'; +import { ScmNavigableListWidget, ScmItemComponent } from '@theia/scm-extra/lib/browser/scm-navigable-list-widget'; 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'; +import { ScmFileChangeNode } from '@theia/scm-extra/lib/browser/scm-file-change-node'; // tslint:disable:no-null-keyword +type GitFileChangeNode = ScmFileChangeNode & { fileChange: GitScmFileChange, selected?: boolean }; + 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 +51,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(); @@ -69,7 +77,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp } })); this.toDispose.push(this.labelProvider.onDidChange(event => { - const affectsFiles = this.fileChangeNodes.some(node => event.affects(new URI(node.uri))); + const affectsFiles = this.fileChangeNodes.some(node => event.affects(new URI(node.fileChange.uri))); if (this.options && affectsFiles) { this.setContent(this.options); } @@ -90,17 +98,29 @@ 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 fileChanges: GitFileChange[] = await this.git.diff(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 gitFileChanges = await this.git.diff(repository, { range: options.range, uri: options.uri }); - this.fileChangeNodes = fileChanges; + const scmFileChanges: GitFileChangeNode[] = gitFileChanges + .map(change => new GitScmFileChange(change, provider, options.range)) + .map(fileChange => ({ fileChange, commitId: fileChange.gitFileChange.uri })); + this.fileChangeNodes = scmFileChanges; this.update(); } } + 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 { @@ -124,7 +144,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}
; @@ -159,7 +179,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp } protected renderPath(): React.ReactNode { if (this.options.uri) { - const path = this.gitLabelProvider.relativePath(this.options.uri); + const path = this.scmLabelProvider.relativePath(this.options.uri); if (path.length > 0) { return '/' + path; } else { @@ -241,24 +261,24 @@ export class GitDiffWidget extends GitNavigableListWidget imp protected doAddGitDiffListKeyListeners(id: string): void { const container = document.getElementById(id); if (container) { - this.addGitListNavigationKeyListeners(container); + this.addListNavigationKeyListeners(container); } } protected renderGitItem(change: GitFileChangeNode): React.ReactNode { - return this.revealChange(change), + revealChange: () => this.revealChange(change.fileChange.gitFileChange), selectNode: () => this.selectNode(change) }} />; } protected navigateRight(): void { const selected = this.getSelected(); - if (selected && GitFileChangeNode.is(selected)) { - const uri = this.getUriToOpen(selected); + if (selected) { + const uri = this.getUriToOpen(selected.fileChange.gitFileChange); this.editorManager.getByUri(uri).then(widget => { if (widget) { const diffNavigator: DiffNavigator = this.diffNavigatorProvider(widget.editor); @@ -269,19 +289,19 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.openSelected(); } } else { - this.revealChange(selected); + this.revealChange(selected.fileChange.gitFileChange); } }); - } else if (this.gitNodes.length > 0) { - this.selectNode(this.gitNodes[0]); + } else if (this.scmNodes.length > 0) { + this.selectNode(this.scmNodes[0]); this.openSelected(); } } protected navigateLeft(): void { const selected = this.getSelected(); - if (GitFileChangeNode.is(selected)) { - const uri = this.getUriToOpen(selected); + if (selected) { + const uri = this.getUriToOpen(selected.fileChange.gitFileChange); this.editorManager.getByUri(uri).then(widget => { if (widget) { const diffNavigator: DiffNavigator = this.diffNavigatorProvider(widget.editor); @@ -292,7 +312,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.openSelected(); } } else { - this.revealChange(selected); + this.revealChange(selected.fileChange.gitFileChange); } }); } @@ -300,19 +320,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]); } } @@ -323,7 +343,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp protected openSelected(): void { const selected = this.getSelected(); if (selected) { - this.revealChange(selected); + this.revealChange(selected.fileChange.gitFileChange); } } @@ -363,8 +383,8 @@ export class GitDiffWidget extends GitNavigableListWidget imp async openChanges(uri: URI, options?: EditorOpenerOptions): Promise { const stringUri = uri.toString(); - const change = this.fileChangeNodes.find(n => n.uri.toString() === stringUri); - return change && this.openChange(change, options); + const change = this.fileChangeNodes.find(n => n.fileChange.uri.toString() === stringUri); + return change && this.openChange(change.fileChange.gitFileChange, options); } openChange(change: GitFileChange, options?: EditorOpenerOptions): Promise { diff --git a/packages/git/src/browser/git-contribution.ts b/packages/git/src/browser/git-contribution.ts index 1fb292399b463..ab5dbfee91cf6 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'; @@ -33,8 +33,6 @@ import { GitPreferences } from './git-preferences'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; -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-label-provider.ts b/packages/git/src/browser/git-file-change-label-provider.ts deleted file mode 100644 index d89a2f19820d8..0000000000000 --- a/packages/git/src/browser/git-file-change-label-provider.ts +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 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 { injectable, inject } from 'inversify'; -import { LabelProviderContribution, DidChangeLabelEvent, LabelProvider } from '@theia/core/lib/browser/label-provider'; -import { GitFileChangeNode } from './git-file-change-node'; -import URI from '@theia/core/lib/common/uri'; -import { GitRepositoryProvider } from './git-repository-provider'; -import { Repository, GitFileStatus } from '../common'; - -@injectable() -export class GitFileChangeLabelProvider implements LabelProviderContribution { - - @inject(LabelProvider) - protected readonly labelProvider: LabelProvider; - - @inject(GitRepositoryProvider) - protected readonly repositoryProvider: GitRepositoryProvider; - - canHandle(element: object): number { - return GitFileChangeNode.is(element) ? 100 : 0; - } - - getIcon(node: GitFileChangeNode): string { - return this.labelProvider.getIcon(new URI(node.uri)); - } - - getName(node: GitFileChangeNode): string { - return this.labelProvider.getName(new URI(node.uri)); - } - - getDescription(node: GitFileChangeNode): string { - return this.relativePath(new URI(node.uri).parent); - } - - affects(node: GitFileChangeNode, event: DidChangeLabelEvent): boolean { - return event.affects(new URI(node.uri)); - } - - getCaption(fileChange: GitFileChangeNode): string { - let result = `${this.relativePath(fileChange.uri)} - ${this.getStatusCaption(fileChange.status, true)}`; - if (fileChange.oldUri) { - result = `${this.relativePath(fileChange.oldUri)} -> ${result}`; - } - return result; - } - - 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(); - } - return this.labelProvider.getLongName(parsedUri); - } - - getStatusCaption(status: GitFileStatus, staged?: boolean): string { - return GitFileStatus.toString(status, staged); - } - -} diff --git a/packages/git/src/browser/git-frontend-module.ts b/packages/git/src/browser/git-frontend-module.ts index c375efb77edde..48b3f93bfaeb8 100644 --- a/packages/git/src/browser/git-frontend-module.ts +++ b/packages/git/src/browser/git-frontend-module.ts @@ -42,8 +42,9 @@ 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 { GitFileChangeLabelProvider } from './git-file-change-label-provider'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +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); @@ -67,9 +68,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(); @@ -78,7 +86,4 @@ export default new ContainerModule(bind => { bind(GitSyncService).toSelf().inSingletonScope(); bind(GitErrorHandler).toSelf().inSingletonScope(); - - bind(GitFileChangeLabelProvider).toSelf().inSingletonScope(); - bind(LabelProviderContribution).toService(GitFileChangeLabelProvider); }); 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 db2a573e5f46e..1f68f6813aad6 100644 --- a/packages/git/src/browser/git-scm-provider.ts +++ b/packages/git/src/browser/git-scm-provider.ts @@ -28,7 +28,9 @@ 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'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; @injectable() export class GitScmProviderOptions { @@ -70,6 +72,9 @@ export class GitScmProvider implements ScmProvider { @inject(GitScmProviderOptions) protected readonly options: GitScmProviderOptions; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + readonly id = 'git'; readonly label = 'Git'; @@ -79,7 +84,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 { @@ -344,8 +349,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[] @@ -362,21 +418,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( @@ -388,7 +445,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 { @@ -411,23 +468,71 @@ 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 gitFileChange(): GitFileChange { + return this.fileChange; + } + + 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, true); + } + + getStatusAbbreviation(): string { + return GitFileStatus.toAbbreviation(this.fileChange.status, this.fileChange.staged); + } + + getClassNameForStatus(): string { + return 'status staged ' + GitFileStatus[this.fileChange.status].toLowerCase(); + } + + 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 3b5a32bd30df9..e67985b04544b 100644 --- a/packages/git/src/common/git-model.ts +++ b/packages/git/src/common/git-model.ts @@ -161,7 +161,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; @@ -203,11 +203,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; } /** @@ -347,7 +346,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..258de7aa05088 --- /dev/null +++ b/packages/scm-extra/package.json @@ -0,0 +1,51 @@ +{ + "name": "@theia/scm-extra", + "version": "0.14.0", + "description": "Theia - Source control extras Extension", + "dependencies": { + "@theia/core": "^0.14.0", + "@theia/editor": "^0.14.0", + "@theia/filesystem": "^0.14.0", + "@theia/navigator": "^0.14.0", + "@theia/scm": "^0.14.0" + }, + "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.14.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..fde4c0fc9d6d2 --- /dev/null +++ b/packages/scm-extra/src/browser/history/scm-history-frontend-module.ts @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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, ApplicationShellLayoutMigration } from '@theia/core/lib/browser'; +import { ScmHistoryWidget } from './scm-history-widget'; +import { ScmExtraLayoutVersion4Migration } from '../scm-extra-layout-migrations'; + +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); + + bind(ApplicationShellLayoutMigration).to(ScmExtraLayoutVersion4Migration).inSingletonScope(); +} 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 61% rename from packages/git/src/browser/history/git-history-widget.tsx rename to packages/scm-extra/src/browser/history/scm-history-widget.tsx index ae19a65151baf..c0f13ed970716 100644 --- a/packages/git/src/browser/history/git-history-widget.tsx +++ b/packages/scm-extra/src/browser/history/scm-history-widget.tsx @@ -15,46 +15,61 @@ ********************************************************************************/ import { injectable, inject, postConstruct } from 'inversify'; -import { DiffUris } from '@theia/core/lib/browser/diff-uris'; +import { Event, 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, GitItemComponent } from '../git-navigable-list-widget'; -import { GitFileChangeNode } from '../git-file-change-node'; +import { ScmItemComponent } from '../scm-navigable-list-widget'; +import { ScmFileChangeNode } from '../scm-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'; -export interface GitCommitNode extends GitCommitDetails { - fileChanges?: GitFileChange[]; +export const ScmHistorySupport = Symbol('scm-history-support'); +export interface ScmHistorySupport { + getCommitHistory(options?: HistoryWidgetOptions): Promise; + readonly onDidChangeHistory: Event; +} + +export interface ScmCommitNode { + commitDetails: ScmCommit; + authorAvatar: string; + fileChangeNodes: ScmFileChangeNode[]; expanded: boolean; selected: boolean; } -export namespace GitCommitNode { +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 GitCommitNode { - return !!node && 'commitSha' in node && 'commitMessage' in node && 'fileChangeNodes' in node; + export function is(node: any): node is ScmCommitNode { + return !!node && 'commitDetails' in node && 'expanded' in node && 'selected' 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; @@ -62,27 +77,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.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.id = SCM_HISTORY_ID; + this.scrollContainer = 'scm-history-list-container'; + 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(); @@ -91,20 +109,67 @@ 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 => { - if (this.gitNodes.some(node => GitFileChangeNode.is(node) && event.affects(new URI(node.uri)))) { + if (this.scmNodes.some(node => ScmFileChangeNode.is(node) && event.affects(new URI(node.fileChange.uri)))) { this.update(); } })); } + 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 } }) => { + this.addEventListener(this.node, 'ps-scroll-y', event => { if (this.listView && this.listView.list && this.listView.list.Grid) { - const { scrollTop } = e.target; + const { scrollTop } = event.target; this.listView.list.Grid.handleScrollEvent({ scrollTop }); } }); @@ -117,7 +182,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); @@ -125,83 +190,78 @@ 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 fileChangeNodes: ScmFileChangeNode[] = []; + await Promise.all(commit.fileChanges.map(async fileChange => { + fileChangeNodes.push({ + fileChange, commitId: commit.id + }); + })); + + 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 { @@ -211,17 +271,17 @@ export class GitHistoryWidget extends GitNavigableListWidget this.update(); } - protected async addFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number): Promise { - if (commit.fileChanges) { - this.gitNodes.splice(gitNodesArrayIndex + 1, 0, ...commit.fileChanges.map(fileChange => - Object.assign(fileChange, { commitSha: commit.commitSha }) + protected async addFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): Promise { + if (commit.fileChangeNodes) { + this.scmNodes.splice(scmNodesArrayIndex + 1, 0, ...commit.fileChangeNodes.map(node => + Object.assign(node, { commitSha: commit.commitDetails.id }) )); } } - protected removeFileChangeNodes(commit: GitCommitNode, gitNodesArrayIndex: number): void { - if (commit.fileChanges) { - this.gitNodes.splice(gitNodesArrayIndex + 1, commit.fileChanges.length); + protected removeFileChangeNodes(commit: ScmCommitNode, scmNodesArrayIndex: number): void { + if (commit.fileChangeNodes) { + this.scmNodes.splice(scmNodesArrayIndex + 1, commit.fileChangeNodes.length); } } @@ -242,7 +302,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(); } @@ -262,18 +322,18 @@ export class GitHistoryWidget extends GitNavigableListWidget let reason: React.ReactNode; reason = this.status.errorMessage; if (this.options.uri) { - const relPathEncoded = this.gitLabelProvider.relativePath(this.options.uri); + const relPathEncoded = this.scmLabelProvider.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.findRepository(new URI(this.options.uri)); + 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; @@ -291,7 +351,7 @@ export class GitHistoryWidget extends GitNavigableListWidget protected renderHistoryHeader(): React.ReactNode { if (this.options.uri) { - const path = this.gitLabelProvider.relativePath(this.options.uri); + const path = this.scmLabelProvider.relativePath(this.options.uri); const fileName = path.split('/').pop(); return
{ @@ -309,16 +369,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; @@ -334,12 +394,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; @@ -350,8 +410,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'; @@ -370,8 +430,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.length > 0) { + this.openFile(commit.fileChangeNodes[0].fileChange); } e.preventDefault(); } @@ -381,17 +441,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()}
@@ -401,25 +461,30 @@ 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.commitId); return fileChangeElement; } - protected renderGitItem(change: GitFileChangeNode, commitSha: string): React.ReactNode { - return this.openFile(change, commitSha), + revealChange: () => this.openFile(change.fileChange), selectNode: () => this.selectNode(change) }} />; } @@ -427,8 +492,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 { @@ -436,7 +501,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.commitId); this.selectNode(this.status.commits[idx]); } } @@ -446,7 +512,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(); @@ -458,49 +524,38 @@ export class GitHistoryWidget extends GitNavigableListWidget protected handleListEnter(): void { const selected = this.getSelected(); if (selected) { - if (GitCommitNode.is(selected)) { + if (ScmCommitNode.is(selected)) { if (this.singleFileMode) { - this.openFile(selected.fileChangeNodes[0], selected.commitSha); + this.openFile(selected.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); @@ -571,12 +626,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..80efcd0b6a021 --- /dev/null +++ b/packages/scm-extra/src/browser/scm-extra-frontend-module.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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'; +import { ScmFileChangeLabelProvider } from './scm-file-change-label-provider'; +import { LabelProviderContribution } from '@theia/core/lib/browser'; + +export default new ContainerModule(bind => { + bindScmHistoryModule(bind); + + bind(ScmFileChangeLabelProvider).toSelf().inSingletonScope(); + bind(LabelProviderContribution).toService(ScmFileChangeLabelProvider); +}); diff --git a/packages/scm-extra/src/browser/scm-extra-layout-migrations.ts b/packages/scm-extra/src/browser/scm-extra-layout-migrations.ts new file mode 100644 index 0000000000000..e29d378b5889b --- /dev/null +++ b/packages/scm-extra/src/browser/scm-extra-layout-migrations.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (C) 2020 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 { injectable } from 'inversify'; +import { ApplicationShellLayoutMigration, WidgetDescription, ApplicationShellLayoutMigrationContext } from '@theia/core/lib/browser/shell/shell-layout-restorer'; +import { SCM_HISTORY_ID } from './history/scm-history-contribution'; + +@injectable() +export class ScmExtraLayoutVersion4Migration implements ApplicationShellLayoutMigration { + readonly layoutVersion = 4.0; + onWillInflateWidget(desc: WidgetDescription, { parent }: ApplicationShellLayoutMigrationContext): WidgetDescription | undefined { + if (desc.constructionOptions.factoryId === 'git-history') { + desc.constructionOptions.factoryId = SCM_HISTORY_ID; + return desc; + } + + return undefined; + } +} diff --git a/packages/scm-extra/src/browser/scm-file-change-label-provider.ts b/packages/scm-extra/src/browser/scm-file-change-label-provider.ts new file mode 100644 index 0000000000000..08a0518b68861 --- /dev/null +++ b/packages/scm-extra/src/browser/scm-file-change-label-provider.ts @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (C) 2019 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 { injectable, inject } from 'inversify'; +import { LabelProviderContribution, DidChangeLabelEvent, LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { ScmFileChangeNode } from './scm-file-change-node'; +import URI from '@theia/core/lib/common/uri'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; + +@injectable() +export class ScmFileChangeLabelProvider implements LabelProviderContribution { + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(ScmService) + protected readonly scmService: ScmService; + + canHandle(element: object): number { + return ScmFileChangeNode.is(element) ? 100 : 0; + } + + getIcon(node: ScmFileChangeNode): string { + return this.labelProvider.getIcon(new URI(node.fileChange.uri)); + } + + getName(node: ScmFileChangeNode): string { + return this.labelProvider.getName(new URI(node.fileChange.uri)); + } + + getDescription(node: ScmFileChangeNode): string { + return this.relativePath(new URI(node.fileChange.uri).parent); + } + + affects(node: ScmFileChangeNode, event: DidChangeLabelEvent): boolean { + return event.affects(new URI(node.fileChange.uri)); + } + + getCaption(node: ScmFileChangeNode): string { + return node.fileChange.getCaption(); + } + + relativePath(uri: URI | string): string { + const parsedUri = typeof uri === 'string' ? new URI(uri) : uri; + const repo = this.scmService.findRepository(parsedUri); + if (repo) { + const repositoryUri = new URI(repo.provider.rootUri); + const relativePath = repositoryUri.relative(parsedUri); + if (relativePath) { + return relativePath.toString(); + } + } + return this.labelProvider.getLongName(parsedUri); + } + + getStatusCaption(node: ScmFileChangeNode): string { + return node.fileChange.getStatusCaption(); + } + +} diff --git a/packages/git/src/browser/git-file-change-node.ts b/packages/scm-extra/src/browser/scm-file-change-node.ts similarity index 71% rename from packages/git/src/browser/git-file-change-node.ts rename to packages/scm-extra/src/browser/scm-file-change-node.ts index 8b108a4861007..92b9a9f8bbfab 100644 --- a/packages/git/src/browser/git-file-change-node.ts +++ b/packages/scm-extra/src/browser/scm-file-change-node.ts @@ -14,14 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GitFileChange } from '../common/git-model'; +import { ScmFileChange } from '@theia/scm/lib/browser/scm-provider'; -export interface GitFileChangeNode extends GitFileChange { - readonly commitSha?: string; +export interface ScmFileChangeNode { + readonly fileChange: ScmFileChange; + readonly commitId: string; selected?: boolean; } -export namespace GitFileChangeNode { - export function is(node: Object | undefined): node is GitFileChangeNode { - return !!node && 'uri' in node && 'status' in node; +export namespace ScmFileChangeNode { + export function is(node: Object | undefined): node is ScmFileChangeNode { + return !!node && 'fileChange' in node && 'commitId' in node; } } 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 72% rename from packages/git/src/browser/git-navigable-list-widget.tsx rename to packages/scm-extra/src/browser/scm-navigable-list-widget.tsx index 8533904eb9c8c..cb9958aa9ecf7 100644 --- a/packages/git/src/browser/git-navigable-list-widget.tsx +++ b/packages/scm-extra/src/browser/scm-navigable-list-widget.tsx @@ -15,28 +15,27 @@ ********************************************************************************/ import { SELECTED_CLASS, Key, Widget } from '@theia/core/lib/browser'; -import { GitFileStatus } from '../common'; +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'; import { inject, injectable } from 'inversify'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import * as React from 'react'; -import { GitFileChangeLabelProvider } from './git-file-change-label-provider'; -import { GitFileChangeNode } from './git-file-change-node'; +import { ScmFileChangeLabelProvider } from './scm-file-change-label-provider'; +import { ScmFileChangeNode } from './scm-file-change-node'; @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; - @inject(GitFileChangeLabelProvider) - protected readonly gitLabelProvider: GitFileChangeLabelProvider; + @inject(ScmFileChangeLabelProvider) + protected readonly scmLabelProvider: ScmFileChangeLabelProvider; constructor() { super(); @@ -78,13 +77,10 @@ export abstract class GitNavigableListWidget e this.update(); } - protected getAbbreviatedStatusCaption(status: GitFileStatus, staged?: boolean): string { - return GitFileStatus.toAbbreviation(status, staged); - } 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 renderHeaderRow({ name, value, classNames, title }: { name: string, value: React.ReactNode, classNames?: string[], title?: string }): React.ReactNode { @@ -98,7 +94,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()); @@ -127,7 +123,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 { @@ -141,46 +137,46 @@ 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; } } -export namespace GitItemComponent { +export namespace ScmItemComponent { export interface Props { labelProvider: LabelProvider; - gitLabelProvider: GitFileChangeLabelProvider; - change: GitFileChangeNode; - revealChange: (change: GitFileChangeNode) => void - selectNode: (change: GitFileChangeNode) => void + scmLabelProvider: ScmFileChangeLabelProvider; + change: ScmFileChangeNode; + revealChange: (change: ScmFileChangeNode) => void + selectNode: (change: ScmFileChangeNode) => void } } -export class GitItemComponent extends React.Component { +export class ScmItemComponent extends React.Component { render(): JSX.Element { - const { labelProvider, gitLabelProvider, change } = this.props; + const { labelProvider, scmLabelProvider, change } = this.props; const icon = labelProvider.getIcon(change); const label = labelProvider.getName(change); const description = labelProvider.getLongName(change); - const caption = gitLabelProvider.getCaption(change); - const statusCaption = gitLabelProvider.getStatusCaption(change.status, true); + const caption = scmLabelProvider.getCaption(change); + const statusCaption = scmLabelProvider.getStatusCaption(change); return
@@ -191,10 +187,10 @@ export class GitItemComponent extends React.Component {
+ className={change.fileChange.getClassNameForStatus()}> {statusCaption.charAt(0)}
- ; + ; } protected readonly revealChange = () => this.props.revealChange(this.props.change); 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 86b532d8d28cc..b81f62abe80b8 100644 --- a/packages/git/src/browser/style/history.css +++ b/packages/scm-extra/src/browser/style/history.css @@ -162,6 +162,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 e715aa973cecb..19563e10d99e2 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -70,7 +70,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 65dd792632494..5e91f97a895cb 100644 --- a/packages/scm/src/browser/scm-provider.ts +++ b/packages/scm/src/browser/scm-provider.ts @@ -69,11 +69,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 e10ed4853e031..a00cd0d6d9430 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -103,6 +103,9 @@ "@theia/scm/lib/*": [ "packages/scm/src/*" ], + "@theia/scm-extra/lib/*": [ + "packages/scm-extra/src/*" + ], "@theia/getting-started/lib/*": [ "packages/getting-started/src/*" ],