From 7d9353789392609545b31da3d5918497314a1a11 Mon Sep 17 00:00:00 2001 From: Nigel Westbury Date: Sun, 13 Oct 2019 18:48:42 +0100 Subject: [PATCH] Move Git history view to scm-extra package Signed-off-by: Nigel Westbury --- .travis.yml | 1 + CHANGELOG.md | 8 + configs/root-compilation.tsconfig.json | 3 + examples/browser/package.json | 1 + examples/electron/compile.tsconfig.json | 3 + examples/electron/package.json | 1 + .../src/browser/shell/application-shell.ts | 6 +- .../decorations/editor-decoration-style.ts | 2 +- packages/git/compile.tsconfig.json | 3 + packages/git/package.json | 1 + .../src/browser/blame/blame-contribution.ts | 4 +- .../src/browser/diff/git-diff-contribution.ts | 20 +- .../git/src/browser/diff/git-diff-widget.tsx | 83 +++-- packages/git/src/browser/git-contribution.ts | 4 +- .../browser/git-file-change-label-provider.ts | 75 ---- .../git/src/browser/git-frontend-module.ts | 26 +- .../browser/git-repository-provider.spec.ts | 7 +- .../src/browser/git-repository-provider.ts | 24 -- packages/git/src/browser/git-scm-provider.ts | 159 ++++++-- .../history/git-commit-detail-open-handler.ts | 38 +- .../history/git-commit-detail-widget.tsx | 49 +-- .../history/git-history-contribution.ts | 136 ------- .../history/git-history-frontend-module.ts | 23 +- .../browser/history/git-history-support.ts | 81 ++++ packages/git/src/browser/style/diff.css | 38 +- packages/git/src/browser/style/history.css | 167 --------- packages/git/src/browser/style/index.css | 75 +--- packages/git/src/common/git-model.ts | 9 +- packages/git/src/node/dugite-git.ts | 6 +- packages/keymaps/src/browser/style/index.css | 7 - packages/scm-extra/compile.tsconfig.json | 28 ++ packages/scm-extra/package.json | 49 +++ .../scm-extra/src/browser/history/index.ts | 29 ++ .../history/scm-history-contribution.ts | 107 ++++++ .../history/scm-history-frontend-module.ts | 36 ++ .../browser/history/scm-history-widget.tsx} | 346 ++++++++++-------- .../src/browser/scm-extra-contribution.ts} | 15 +- .../src/browser/scm-extra-frontend-module.ts | 27 ++ .../browser/scm-extra-layout-migrations.ts | 32 ++ .../browser/scm-file-change-label-provider.ts | 73 ++++ .../src/browser/scm-file-change-node.ts | 44 +++ .../browser/scm-navigable-list-widget.tsx} | 68 ++-- .../scm-extra/src/browser/style/history.css | 106 ++++++ packages/scm/src/browser/scm-contribution.ts | 2 +- .../scm/src/browser/scm-frontend-module.ts | 1 + packages/scm/src/browser/scm-input.ts | 1 - packages/scm/src/browser/scm-provider.ts | 12 +- packages/scm/src/browser/scm-service.ts | 8 + packages/scm/src/browser/style/diff.css | 40 ++ tsconfig.json | 3 + 50 files changed, 1254 insertions(+), 833 deletions(-) delete mode 100644 packages/git/src/browser/git-file-change-label-provider.ts delete mode 100644 packages/git/src/browser/history/git-history-contribution.ts create mode 100644 packages/git/src/browser/history/git-history-support.ts delete mode 100644 packages/git/src/browser/style/history.css create mode 100644 packages/scm-extra/compile.tsconfig.json create mode 100644 packages/scm-extra/package.json create mode 100644 packages/scm-extra/src/browser/history/index.ts create mode 100644 packages/scm-extra/src/browser/history/scm-history-contribution.ts create mode 100644 packages/scm-extra/src/browser/history/scm-history-frontend-module.ts rename packages/{git/src/browser/history/git-history-widget.tsx => scm-extra/src/browser/history/scm-history-widget.tsx} (61%) rename packages/{git/src/browser/git-file-change-node.ts => scm-extra/src/browser/scm-extra-contribution.ts} (66%) create mode 100644 packages/scm-extra/src/browser/scm-extra-frontend-module.ts create mode 100644 packages/scm-extra/src/browser/scm-extra-layout-migrations.ts create mode 100644 packages/scm-extra/src/browser/scm-file-change-label-provider.ts create mode 100644 packages/scm-extra/src/browser/scm-file-change-node.ts rename packages/{git/src/browser/git-navigable-list-widget.tsx => scm-extra/src/browser/scm-navigable-list-widget.tsx} (71%) create mode 100644 packages/scm-extra/src/browser/style/history.css create mode 100644 packages/scm/src/browser/style/diff.css diff --git a/.travis.yml b/.travis.yml index d93167634661b..152e90e0b0774 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,7 @@ cache: - packages/preferences/node_modules - packages/preview/node_modules - packages/process/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 5166e4e5f1be5..c9b2139fcd5e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## v0.17.0 + +Breaking changes: + +- [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. + CSS classes have been moved renamed accordingly. [6381](https://github.com/eclipse-theia/theia/pull/6381) + ## v0.16.0 - [cli] added an additional flag to the `download:plugins` script [#7123](https://github.com/eclipse-theia/theia/pull/7123) diff --git a/configs/root-compilation.tsconfig.json b/configs/root-compilation.tsconfig.json index ef8091ea6496d..3154544acd417 100644 --- a/configs/root-compilation.tsconfig.json +++ b/configs/root-compilation.tsconfig.json @@ -106,6 +106,9 @@ { "path": "../packages/scm/compile.tsconfig.json" }, + { + "path": "../packages/scm-extra/compile.tsconfig.json" + }, { "path": "../packages/search-in-workspace/compile.tsconfig.json" }, diff --git a/examples/browser/package.json b/examples/browser/package.json index d05d9dd71f2f5..bb8becec87883 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -44,6 +44,7 @@ "@theia/preview": "^0.16.0", "@theia/process": "^0.16.0", "@theia/scm": "^0.16.0", + "@theia/scm-extra": "^0.16.0", "@theia/search-in-workspace": "^0.16.0", "@theia/task": "^0.16.0", "@theia/terminal": "^0.16.0", diff --git a/examples/electron/compile.tsconfig.json b/examples/electron/compile.tsconfig.json index f19eb2dff29f7..3dff9bf742620 100644 --- a/examples/electron/compile.tsconfig.json +++ b/examples/electron/compile.tsconfig.json @@ -98,6 +98,9 @@ { "path": "../../packages/scm/compile.tsconfig.json" }, + { + "path": "../../packages/scm-extra/compile.tsconfig.json" + }, { "path": "../../packages/search-in-workspace/compile.tsconfig.json" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 7678d77a92838..c322cac083846 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -41,6 +41,7 @@ "@theia/preview": "^0.16.0", "@theia/process": "^0.16.0", "@theia/scm": "^0.16.0", + "@theia/scm-extra": "^0.16.0", "@theia/search-in-workspace": "^0.16.0", "@theia/task": "^0.16.0", "@theia/terminal": "^0.16.0", diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 614169c2e8aaf..ba340a395e884 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -52,12 +52,14 @@ export type ApplicationShellLayoutVersion = /** layout versioning is introduced, unversioned 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/editor/src/browser/decorations/editor-decoration-style.ts b/packages/editor/src/browser/decorations/editor-decoration-style.ts index 122f8ce559ac9..a43b76f536fc0 100644 --- a/packages/editor/src/browser/decorations/editor-decoration-style.ts +++ b/packages/editor/src/browser/decorations/editor-decoration-style.ts @@ -67,7 +67,7 @@ export namespace EditorDecorationStyle { } const index = styleSheet.insertRule('.' + selector + '{}', 0); const rules = styleSheet.cssRules || styleSheet.rules; - const rule = rules.item(index); + const rule = rules[index]; if (rule && rule.type === CSSRule.STYLE_RULE) { const styleRule = rule as CSSStyleRule; styleProvider(styleRule.style); diff --git a/packages/git/compile.tsconfig.json b/packages/git/compile.tsconfig.json index 27197774821cb..47e36ba03fd59 100644 --- a/packages/git/compile.tsconfig.json +++ b/packages/git/compile.tsconfig.json @@ -27,6 +27,9 @@ { "path": "../scm/compile.tsconfig.json" }, + { + "path": "../scm-extra/compile.tsconfig.json" + }, { "path": "../workspace/compile.tsconfig.json" } diff --git a/packages/git/package.json b/packages/git/package.json index 257fdd53a2baf..d8d16e2b24954 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -9,6 +9,7 @@ "@theia/languages": "^0.16.0", "@theia/navigator": "^0.16.0", "@theia/scm": "^0.16.0", + "@theia/scm-extra": "^0.16.0", "@theia/workspace": "^0.16.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 5f452e8abc5eb..464eb255017f8 100644 --- a/packages/git/src/browser/diff/git-diff-widget.tsx +++ b/packages/git/src/browser/diff/git-diff-widget.tsx @@ -18,20 +18,25 @@ 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 { 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'; /* eslint-disable no-null/no-null */ +type GitFileChangeNode = ScmFileChangeNode & { fileChange: GitScmFileChange }; + 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,6 +50,7 @@ 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; @@ -58,6 +64,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.title.closable = true; this.title.iconClass = 'theia-git-diff-icon'; + this.addClass('theia-scm'); this.addClass('theia-git'); } @@ -69,7 +76,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 +97,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,10 +143,10 @@ 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}
; + return
{commitishBar}{fileChangeList}
; } protected renderDiffListHeader(): React.ReactNode { @@ -159,7 +178,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 +260,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 +288,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 +311,7 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.openSelected(); } } else { - this.revealChange(selected); + this.revealChange(selected.fileChange.gitFileChange); } }); } @@ -300,19 +319,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 +342,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 +382,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 0253a667571ad..8e2b3140338d7 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..f9b9e8f9885b7 100644 --- a/packages/git/src/browser/git-frontend-module.ts +++ b/packages/git/src/browser/git-frontend-module.ts @@ -16,6 +16,7 @@ import '../../src/browser/style/index.css'; +import { interfaces } from 'inversify'; import { ContainerModule } from 'inversify'; import { CommandContribution, MenuContribution, ResourceResolver } from '@theia/core/lib/common'; import { @@ -41,9 +42,11 @@ import { GitRepositoryTracker } from './git-repository-tracker'; 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 { GitScmProvider, GitScmProviderOptions } from './git-scm-provider'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +import { ScmHistorySupport } from '@theia/scm-extra/lib/browser/history/scm-history-widget'; +import { ScmHistoryProvider } from '@theia/scm-extra/lib/browser/history'; +import { GitHistorySupport } from './history/git-history-support'; export default new ContainerModule(bind => { bindGitPreferences(bind); @@ -67,7 +70,7 @@ export default new ContainerModule(bind => { bind(GitResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(GitResourceResolver); - bind(GitScmProvider.Factory).toFactory(GitScmProvider.createFactory); + bind(GitScmProvider.Factory).toFactory(createGitScmProviderFactory); bind(GitRepositoryProvider).toSelf().inSingletonScope(); bind(GitQuickOpenService).toSelf().inSingletonScope(); @@ -78,7 +81,18 @@ export default new ContainerModule(bind => { bind(GitSyncService).toSelf().inSingletonScope(); bind(GitErrorHandler).toSelf().inSingletonScope(); - - bind(GitFileChangeLabelProvider).toSelf().inSingletonScope(); - bind(LabelProviderContribution).toService(GitFileChangeLabelProvider); }); + +export function createGitScmProviderFactory(ctx: interfaces.Context): GitScmProvider.Factory { + return (options: GitScmProviderOptions) => { + const container = ctx.container.createChild(); + container.bind(GitScmProviderOptions).toConstantValue(options); + container.bind(GitScmProvider).toSelf().inSingletonScope(); + container.bind(GitHistorySupport).toSelf().inSingletonScope(); + container.bind(ScmHistorySupport).toService(GitHistorySupport); + const provider = container.get(GitScmProvider); + const historySupport = container.get(GitHistorySupport); + (provider as ScmHistoryProvider).historySupport = historySupport; + return provider; + }; +} diff --git a/packages/git/src/browser/git-repository-provider.spec.ts b/packages/git/src/browser/git-repository-provider.spec.ts index 9ee30fa6348de..6414b062cd8bf 100644 --- a/packages/git/src/browser/git-repository-provider.spec.ts +++ b/packages/git/src/browser/git-repository-provider.spec.ts @@ -35,9 +35,11 @@ import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { ScmContextKeyService } from '@theia/scm/lib/browser/scm-context-key-service'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { GitScmProvider } from './git-scm-provider'; +import { createGitScmProviderFactory } from './git-frontend-module'; import { EditorManager } from '@theia/editor/lib/browser'; import { GitErrorHandler } from './git-error-handler'; import { GitPreferences } from './git-preferences'; +import { GitRepositoryTracker } from './git-repository-tracker'; const expect = chai.expect; disableJSDOM(); @@ -72,6 +74,7 @@ describe('GitRepositoryProvider', () => { let mockFilesystem: FileSystem; let mockFileSystemWatcher: FileSystemWatcher; let mockStorageService: StorageService; + let mockGitRepositoryTracker: GitRepositoryTracker; let gitRepositoryProvider: GitRepositoryProvider; const mockRootChangeEmitter: Emitter = new Emitter(); @@ -90,6 +93,7 @@ describe('GitRepositoryProvider', () => { mockFilesystem = sinon.createStubInstance(FileSystemNode); mockFileSystemWatcher = sinon.createStubInstance(FileSystemWatcher); mockStorageService = sinon.createStubInstance(LocalStorageService); + mockGitRepositoryTracker = sinon.createStubInstance(GitRepositoryTracker); testContainer = new Container(); testContainer.bind(GitRepositoryProvider).toSelf().inSingletonScope(); @@ -99,7 +103,7 @@ 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.Factory).toFactory(createGitScmProviderFactory); testContainer.bind(ScmContextKeyService).toSelf().inSingletonScope(); testContainer.bind(ContextKeyService).toSelf().inSingletonScope(); testContainer.bind(GitCommitMessageValidator).toSelf().inSingletonScope(); @@ -108,6 +112,7 @@ describe('GitRepositoryProvider', () => { testContainer.bind(CommandService).toConstantValue({}); testContainer.bind(LabelProvider).toConstantValue({}); testContainer.bind(GitPreferences).toConstantValue({}); + testContainer.bind(GitRepositoryTracker).toConstantValue(mockGitRepositoryTracker); sinon.stub(mockWorkspaceService, 'onWorkspaceChanged').value(mockRootChangeEmitter.event); sinon.stub(mockFileSystemWatcher, 'onFilesChanged').value(mockFileChangeEmitter.event); diff --git a/packages/git/src/browser/git-repository-provider.ts b/packages/git/src/browser/git-repository-provider.ts index 0d7dc94832092..9b2c46c430e89 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'; @@ -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[] = []; diff --git a/packages/git/src/browser/git-scm-provider.ts b/packages/git/src/browser/git-scm-provider.ts index db2a573e5f46e..2c3ef12ee5041 100644 --- a/packages/git/src/browser/git-scm-provider.ts +++ b/packages/git/src/browser/git-scm-provider.ts @@ -14,10 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject, postConstruct, interfaces } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; -import { Emitter } from '@theia/core/lib/common/event'; +import { Emitter } from '@theia/core'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { CommandService } from '@theia/core/lib/common/command'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; @@ -29,6 +29,9 @@ 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 { ScmHistoryCommit, ScmFileChange } from '@theia/scm-extra/lib/browser/scm-file-change-node'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { GitCommitDetailWidgetOptions } from './history/git-commit-detail-widget'; @injectable() export class GitScmProviderOptions { @@ -70,6 +73,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 +85,7 @@ export class GitScmProvider implements ScmProvider { @postConstruct() protected init(): void { - this._amendSupport = new GitAmendSupport(this.repository, this.git); + this._amendSupport = new GitAmendSupport(this, this.repository, this.git); } get repository(): Repository { @@ -344,8 +350,64 @@ export class GitScmProvider implements ScmProvider { await Promise.all(uris.map(uri => this.delete(new URI(uri)))); } + public createScmCommit(gitCommit: CommitWithChanges): ScmCommit { + const scmCommit: ScmCommit = { + id: gitCommit.sha, + summary: gitCommit.summary, + authorName: gitCommit.author.name, + authorEmail: gitCommit.author.email, + authorDateRelative: gitCommit.authorDateRelative, + }; + return scmCommit; + } + + public createScmHistoryCommit(gitCommit: CommitWithChanges): ScmHistoryCommit { + const range = { + fromRevision: gitCommit.sha + '~1', + toRevision: gitCommit.sha + }; + + const scmCommit: GitScmCommit = { + ...this.createScmCommit(gitCommit), + commitDetailUri: this.toCommitDetailUri(gitCommit.sha), + 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 +424,14 @@ export namespace GitScmProvider { groups: [] }; } + export const Factory = Symbol('GitScmProvider.Factory'); export type Factory = (options: GitScmProviderOptions) => GitScmProvider; - export function createFactory(ctx: interfaces.Context): Factory { - return (options: GitScmProviderOptions) => { - const container = ctx.container.createChild(); - container.bind(GitScmProviderOptions).toConstantValue(options); - container.bind(GitScmProvider).toSelf().inSingletonScope(); - return container.get(GitScmProvider); - }; - } } 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 +443,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 +466,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 ScmHistoryCommit { + 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 'git-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..4189fa66da4a2 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 { ScmHistoryCommit } from '@theia/scm-extra/lib/browser/scm-file-change-node'; -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: ScmHistoryCommit) => { 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.ts b/packages/git/src/browser/history/git-history-support.ts new file mode 100644 index 0000000000000..37b9fd848b928 --- /dev/null +++ b/packages/git/src/browser/history/git-history-support.ts @@ -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 { ScmHistoryCommit } from '@theia/scm-extra/lib/browser/scm-file-change-node'; +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.createScmHistoryCommit(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/browser/style/diff.css b/packages/git/src/browser/style/diff.css index e383cfe833b49..45380c9d23ae1 100644 --- a/packages/git/src/browser/style/diff.css +++ b/packages/git/src/browser/style/diff.css @@ -62,16 +62,6 @@ padding-right: 5px; } -.theia-git .gitItem.diff-file { - margin: 1px 0 !important; - padding: 0 !important; -} - -.theia-git .gitItem.diff-file:hover { - background-color: inherit !important; - cursor: inherit !important; -} - .theia-git .lrBtns { display:flex; align-items: center; @@ -88,3 +78,31 @@ flex: 1; overflow: auto; } + +.theia-git .scm-diff-container .commit-info { + padding-left: 10px; + box-sizing: border-box; + overflow: hidden; +} + +.theia-git .scm-diff-container .commit-info-row { + align-items: center; + margin-top: 10px; +} + +.theia-git .commit-info .header-row { + margin: 4px 0; +} + +.theia-git .commit-info .header-row .theia-header { + margin: 1px 0; +} + +.theia-git .commit-info .header-row .header-value { + margin: 0 0 0 5px; +} + +.theia-git .commit-info-row .image-container { + display: flex; +} + diff --git a/packages/git/src/browser/style/history.css b/packages/git/src/browser/style/history.css deleted file mode 100644 index 86b532d8d28cc..0000000000000 --- a/packages/git/src/browser/style/history.css +++ /dev/null @@ -1,167 +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 - ********************************************************************************/ - -.theia-git .commitListContainer .commitList .commitListElement { - margin: 3px 0; -} - -.theia-git .commitListElement.first .containerHead { - border: none; -} - -.theia-git .commitListElement .containerHead { - width: calc(100% - 5px); - height: 50px; - display: flex; - align-items: center; - border-top: 1px solid var(--theia-contrastBorder); -} - -.theia-git .commitListElement .containerHead:hover { - background-color: var(--theia-list-hoverBackground); - color: var(--theia-list-hoverForeground); - cursor: pointer; -} - -.theia-git:focus-within .commitListElement .containerHead.theia-mod-selected { - background: var(--theia-list-focusBackground); - color: var(--theia-list-focusForeground); -} - -.theia-git:not(:focus-within) .commitListElement .containerHead.theia-mod-selected { - background: var(--theia-list-inactiveFocusBackground); -} - -.theia-git .commitListElement .containerHead .headContent { - display: flex; - width: 100%; - box-sizing: border-box; - padding: 0 8px 0 2px; -} - -.theia-git .commitListElement .containerHead .headContent .image-container{ - margin-right: 5px; -} - -.theia-git .commitListElement .containerHead .headContent .image-container img{ - width: 27px; -} - -.theia-git .commitListElement .containerHead .headContent .headLabelContainer{ - min-width: calc(100% - 93px); -} - -.theia-git .commitListElement .containerHead .headContent .headLabelContainer.singleFileMode{ - width: 100%; -} - -.theia-git .commitListElement .containerHead .headContent .expansionToggle{ - display: flex; - align-items: center; -} - -.theia-git .commitListElement .containerHead .headContent .detailButton{ - display: flex; - align-items: center; - visibility: hidden; - margin: 0 5px; -} - -.theia-git .commitListElement .containerHead:hover .headContent .detailButton { - visibility: visible; -} - -.theia-git .commitListElement .containerHead .headContent .expansionToggle > .toggle { - display: flex; - background: var(--theia-list-focusBackground); - padding: 5px; - border-radius: 7px; - margin-left: 5px; - align-items: center; - justify-content: flex-end; - min-width: 30px; - color: var(--theia-theia-list-focusForeground); -} - -.theia-git .commitBody { - padding-bottom: 10px; -} - -.theia-git .commitFileList .theia-header { - margin-top: 5px; -} - -.theia-git .commitTime { - color: var(--theia-descriptionForeground); - font-size: smaller; -} - -.theia-git .git-diff-container .listContainer { - flex: 1; - position: relative; -} - -.theia-git .git-diff-container .listContainer .commitList { - height: 100%; -} - -.theia-git .git-diff-container .subject { - font-size: var(--theia-ui-font-size2); - font-weight: bold; -} - -.theia-git .git-diff-container .commit-info { - padding-left: 10px; - box-sizing: border-box; - overflow: hidden; -} - -.theia-git .git-diff-container .commit-info-row { - align-items: center; - margin-top: 10px; -} - -.theia-git .commit-info .header-row { - margin: 4px 0; -} - -.theia-git .commit-info .header-row .theia-header { - margin: 1px 0; -} - -.theia-git .commit-info .header-row .header-value { - margin: 0 0 0 5px; -} - -.theia-git .commit-info-row .image-container { - display: flex; -} - -.tab-git-icon { - width: 20px!important; - height: 20px; -} - -.theia-git .message-container { - height: 100%; - display: flex; - align-items: flex-start; - justify-content: center; -} - -.git-history-tab-icon::before { - content: "\f1da" -} diff --git a/packages/git/src/browser/style/index.css b/packages/git/src/browser/style/index.css index c8cc2a20dd233..e60d2494f7c2e 100644 --- a/packages/git/src/browser/style/index.css +++ b/packages/git/src/browser/style/index.css @@ -29,90 +29,39 @@ border: none; } -.theia-git .noWrapInfo { - width: 100%; -} - .theia-git .space-between { justify-content: space-between; } -.theia-git .gitItem { - font-size: var(--theia-ui-font-size1); - display: inline-flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: var(--theia-content-line-height); - line-height: var(--theia-content-line-height); - padding: 0px; - padding-right: var(--theia-ui-padding); -} - -.theia-git .gitItem:hover { - background-color: var(--theia-list-hoverBackground); - color: var(--theia-list-hoverForeground); - cursor: pointer; -} - -.theia-git:focus-within .gitItem:focus { - background: var(--theia-list-focusBackground); - color: var(--theia-list-focusForeground); -} - -.theia-git:not(:focus-within) .gitItem:not(:focus) { - background: var(--theia-list-inactiveFocusBackground); -} - -.theia-git:focus-within .gitItem.theia-mod-selected { - background: var(--theia-list-activeSelectionBackground); - color: var(--theia-list-activeSelectionForeground); -} - -.theia-git:not(:focus-within) .gitItem.theia-mod-selected { - background: var(--theia-list-inactiveSelectionBackground); - color: var(--theia-list-inactiveSelectionForeground); -} - -.theia-git .gitItem .file-icon { - display: initial; -} - -.theia-git .gitItem .path { - font-size: var(--theia-ui-font-size0); - margin-left: 3px; - opacity: .7; -} - -.theia-git .gitItem .status { - padding: 2px 4px; - font-size: var(--theia-ui-font-size0); -} - -.theia-git .gitItem .status.new { +.theia-scm .scmItem .git-status.new { color: var(--theia-gitDecoration-untrackedResourceForeground); } -.theia-git .gitItem .status.new.staged { +.theia-scm .scmItem .git-status.new.staged { color: var(--theia-gitDecoration-addedResourceForeground); } -.theia-git .gitItem .status.modified { +.theia-scm .scmItem .git-status.modified { color: var(--theia-gitDecoration-modifiedResourceForeground); } -.theia-git .gitItem .status.deleted { +.theia-scm .scmItem .git-status.deleted { color: var(--theia-gitDecoration-deletedResourceForeground); } -.theia-git .gitItem .status.renamed { +.theia-scm .scmItem .git-status.renamed { color: var(--theia-gitDecoration-untrackedResourceForeground); } -.theia-git .gitItem .status.conflicted { +.theia-scm .scmItem .git-status.conflicted { color: var(--theia-gitDecoration-conflictingResourceForeground); } -.theia-git .gitItem .status.copied { +.theia-scm .scmItem .git-status.copied { color: var(--theia-gitDecoration-modifiedResourceForeground); } + +.tab-git-icon { + width: 20px!important; + height: 20px; +} diff --git a/packages/git/src/common/git-model.ts b/packages/git/src/common/git-model.ts index 3b5a32bd30df9..a340db4f65352 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; } /** @@ -342,12 +341,12 @@ export interface Commit { export interface CommitWithChanges extends Commit { /** - * The date when the commit was authored. + * The date when the commit was authored (ISO format). */ 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 a87e1827c330a..6acb7d2a5e25e 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/keymaps/src/browser/style/index.css b/packages/keymaps/src/browser/style/index.css index 0755e95a22dad..5241c54410146 100644 --- a/packages/keymaps/src/browser/style/index.css +++ b/packages/keymaps/src/browser/style/index.css @@ -137,13 +137,6 @@ width: 25%; } -.message-container { - align-items: center; - display: flex; - height: 100%; - justify-content: center; -} - .no-kb { border: 1px solid var(--theia-editorWarning-foreground); } diff --git a/packages/scm-extra/compile.tsconfig.json b/packages/scm-extra/compile.tsconfig.json new file mode 100644 index 0000000000000..237f7fd625ab9 --- /dev/null +++ b/packages/scm-extra/compile.tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core/compile.tsconfig.json" + }, + { + "path": "../editor/compile.tsconfig.json" + }, + { + "path": "../filesystem/compile.tsconfig.json" + }, + { + "path": "../navigator/compile.tsconfig.json" + }, + { + "path": "../scm/compile.tsconfig.json" + } + ] +} diff --git a/packages/scm-extra/package.json b/packages/scm-extra/package.json new file mode 100644 index 0000000000000..5011bc1285233 --- /dev/null +++ b/packages/scm-extra/package.json @@ -0,0 +1,49 @@ +{ + "name": "@theia/scm-extra", + "version": "0.16.0", + "description": "Theia - Source control extras Extension", + "dependencies": { + "@theia/core": "^0.16.0", + "@theia/editor": "^0.16.0", + "@theia/filesystem": "^0.16.0", + "@theia/navigator": "^0.16.0", + "@theia/scm": "^0.16.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": { + "lint": "theiaext lint", + "clean": "theiaext clean", + "build": "theiaext build", + "watch": "theiaext watch", + "docs": "theiaext docs" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.16.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/scm-extra/src/browser/history/index.ts b/packages/scm-extra/src/browser/history/index.ts new file mode 100644 index 0000000000000..97ee93b1bd4b6 --- /dev/null +++ b/packages/scm-extra/src/browser/history/index.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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 { ScmProvider } from '@theia/scm/lib/browser/scm-provider'; +import { ScmHistorySupport } from './scm-history-widget'; + +export { ScmHistorySupport }; + +export interface ScmHistoryProvider extends ScmProvider { + historySupport?: ScmHistorySupport; +} +export namespace ScmHistoryProvider { + export function is(scmProvider: ScmProvider | undefined): scmProvider is ScmHistoryProvider { + return !!scmProvider && 'historySupport' in scmProvider; + } +} 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 1e0d45c9f286a..8623e40103efb 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,62 @@ ********************************************************************************/ import { injectable, inject, postConstruct } from 'inversify'; -import { DiffUris } from '@theia/core/lib/browser/diff-uris'; +import { Event as TheiaEvent, DisposableCollection } from '@theia/core'; import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell } from '@theia/core/lib/browser'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { Message } from '@phosphor/messaging'; import { AutoSizer, List, ListRowRenderer, ListRowProps, InfiniteLoader, IndexRange, ScrollParams, CellMeasurerCache, CellMeasurer } from 'react-virtualized'; -import { GIT_RESOURCE_SCHEME } from '../git-resource'; import URI from '@theia/core/lib/common/uri'; -import { GIT_HISTORY_ID, GIT_HISTORY_MAX_COUNT, GIT_HISTORY_LABEL } from './git-history-contribution'; -import { GitFileStatus, Git, GitFileChange, Repository } from '../../common'; +import { ScmService } from '@theia/scm/lib/browser/scm-service'; +import { ScmHistoryProvider } from '.'; +import { SCM_HISTORY_ID, SCM_HISTORY_MAX_COUNT, SCM_HISTORY_LABEL } from './scm-history-contribution'; +import { ScmHistoryCommit, ScmFileChange } from '../scm-file-change-node'; 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: TheiaEvent; +} + +export interface ScmCommitNode { + commitDetails: ScmHistoryCommit; + authorAvatar: string; + fileChangeNodes: ScmFileChangeNode[]; expanded: boolean; selected: boolean; } -export namespace GitCommitNode { +export namespace ScmCommitNode { // eslint-disable-next-line @typescript-eslint/no-explicit-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 interface HistoryWidgetOptions { + readonly range?: { + readonly toRevision?: string; + readonly fromRevision?: string; + }; + readonly uri?: string; + readonly maxCount?: number; +} + +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,45 +78,96 @@ 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.addClass('theia-scm'); + this.addClass('theia-scm-history'); this.resetState(); this.cancelIndicator = new CancellationTokenSource(); } @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 && ScmHistoryProvider.is(repository.provider)) { + this.historySupport = repository.provider.historySupport; + if (this.historySupport) { + this.toDisposeOnRepositoryChange.push(this.historySupport.onDidChangeHistory(() => this.setContent(this.options))); + } + } else { + this.historySupport = undefined; + } + this.setContent(this.options); + + // 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); // eslint-disable-next-line @typescript-eslint/no-explicit-any this.addEventListener(this.node, 'ps-scroll-y', (e: Event & { target: { scrollTop: number } }) => { if (this.listView && this.listView.list && this.listView.list.Grid) { @@ -117,7 +184,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,82 +192,79 @@ 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 { - const repository: Repository | undefined = this.repositoryProvider.findRepositoryOrSelected(options); + protected async addCommits(options?: HistoryWidgetOptions): Promise { + // const repository: Repository | undefined = this.repositoryProvider.findRepositoryOrSelected(options); + 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 { @@ -210,17 +274,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); } } @@ -241,7 +305,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(); } @@ -260,18 +324,18 @@ export class GitHistoryWidget extends GitNavigableListWidget const reason: React.ReactNode = this.status.errorMessage; let path: React.ReactNode = ''; 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; @@ -282,14 +346,14 @@ export class GitHistoryWidget extends GitNavigableListWidget ; break; } - return
+ return
{content}
; } 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
{ @@ -307,16 +371,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; @@ -332,12 +396,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; @@ -348,8 +412,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'; @@ -368,28 +432,29 @@ 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(); } - }> + } + >
- {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()}
@@ -399,25 +464,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) }} />; } @@ -425,8 +495,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 { @@ -434,7 +504,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]); } } @@ -444,7 +515,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(); @@ -456,49 +527,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 // eslint-disable-next-line @typescript-eslint/no-explicit-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); @@ -569,12 +629,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/git/src/browser/git-file-change-node.ts b/packages/scm-extra/src/browser/scm-extra-contribution.ts similarity index 66% rename from packages/git/src/browser/git-file-change-node.ts rename to packages/scm-extra/src/browser/scm-extra-contribution.ts index 8b108a4861007..6afcc3d4cb355 100644 --- a/packages/git/src/browser/git-file-change-node.ts +++ b/packages/scm-extra/src/browser/scm-extra-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Ericsson and others. + * 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 @@ -13,15 +13,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; -import { GitFileChange } from '../common/git-model'; - -export interface GitFileChangeNode extends GitFileChange { - readonly commitSha?: string; - selected?: boolean; -} -export namespace GitFileChangeNode { - export function is(node: Object | undefined): node is GitFileChangeNode { - return !!node && 'uri' in node && 'status' in node; - } -} +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/scm-extra/src/browser/scm-file-change-node.ts b/packages/scm-extra/src/browser/scm-file-change-node.ts new file mode 100644 index 0000000000000..bd5d95dd20983 --- /dev/null +++ b/packages/scm-extra/src/browser/scm-file-change-node.ts @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (C) 2018 Ericsson 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 { ScmCommit } from '@theia/scm/lib/browser/scm-provider'; +import URI from '@theia/core/lib/common/uri'; + +export interface ScmFileChangeNode { + readonly fileChange: ScmFileChange; + readonly commitId: string; + selected?: boolean; +} +export namespace ScmFileChangeNode { + export function is(node: Object | undefined): node is ScmFileChangeNode { + return !!node && 'fileChange' in node && 'commitId' in node; + } +} + +export interface ScmHistoryCommit extends ScmCommit { + readonly commitDetailUri: URI; + readonly fileChanges: ScmFileChange[]; + readonly commitDetailOptions: {}; +} + +export interface ScmFileChange { + readonly uri: string; + getCaption(): string; + getStatusCaption(): string; + getStatusAbbreviation(): string; + getClassNameForStatus(): string; + getUriToOpen(): URI; +} 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 71% 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..0d6ede01ed559 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,47 +137,47 @@ 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); - 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/scm-extra/src/browser/style/history.css b/packages/scm-extra/src/browser/style/history.css new file mode 100644 index 0000000000000..f980ec82f96ec --- /dev/null +++ b/packages/scm-extra/src/browser/style/history.css @@ -0,0 +1,106 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +.theia-scm-history .commitList .commitListElement { + margin: 3px 0; +} + +.theia-scm-history .commitListElement.first .containerHead { + border: none; +} + +.theia-scm-history .commitListElement .containerHead { + width: calc(100% - 5px); + height: 50px; + display: flex; + align-items: center; + border-top: 1px solid var(--theia-contrastBorder); +} + +.theia-scm-history .commitListElement .containerHead:hover { + background-color: var(--theia-list-hoverBackground); + color: var(--theia-list-hoverForeground); + cursor: pointer; +} + +.theia-scm-history:focus-within .commitListElement .containerHead.theia-mod-selected { + background: var(--theia-list-focusBackground); + color: var(--theia-list-focusForeground); +} + +.theia-scm-history:not(:focus-within) .commitListElement .containerHead.theia-mod-selected { + background: var(--theia-list-inactiveFocusBackground); +} + +.theia-scm-history .commitListElement .containerHead .headContent { + display: flex; + width: 100%; + box-sizing: border-box; + padding: 0 8px 0 2px; +} + +.theia-scm-history .commitListElement .containerHead .headContent .image-container{ + margin-right: 5px; +} + +.theia-scm-history .commitListElement .containerHead .headContent .image-container img{ + width: 27px; +} + +.theia-scm-history .commitListElement .containerHead .headContent .headLabelContainer{ + min-width: calc(100% - 93px); +} + +.theia-scm-history .commitListElement .containerHead .headContent .headLabelContainer.singleFileMode{ + width: 100%; +} + +.theia-scm-history .commitListElement .containerHead .headContent .expansionToggle{ + display: flex; + align-items: center; +} + +.theia-scm-history .commitListElement .containerHead .headContent .detailButton{ + display: flex; + align-items: center; + visibility: hidden; + margin: 0 5px; +} + +.theia-scm-history .commitListElement .containerHead:hover .headContent .detailButton { + visibility: visible; +} + +.theia-scm-history .commitListElement .containerHead .headContent .expansionToggle > .toggle { + display: flex; + background: var(--theia-list-focusBackground); + padding: 5px; + border-radius: 7px; + margin-left: 5px; + align-items: center; + justify-content: flex-end; + min-width: 30px; + color: var(--theia-theia-list-focusForeground); +} + +.theia-scm-history .commitTime { + color: var(--theia-descriptionForeground); + font-size: smaller; +} + +.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-frontend-module.ts b/packages/scm/src/browser/scm-frontend-module.ts index a2187f09b4347..2c7213a2fe1a1 100644 --- a/packages/scm/src/browser/scm-frontend-module.ts +++ b/packages/scm/src/browser/scm-frontend-module.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import '../../src/browser/style/index.css'; +import '../../src/browser/style/diff.css'; import { ContainerModule } from 'inversify'; import { diff --git a/packages/scm/src/browser/scm-input.ts b/packages/scm/src/browser/scm-input.ts index baba275686500..b1575f277b285 100644 --- a/packages/scm/src/browser/scm-input.ts +++ b/packages/scm/src/browser/scm-input.ts @@ -127,5 +127,4 @@ export class ScmInput implements Disposable { this.fireDidChange(); } } - } diff --git a/packages/scm/src/browser/scm-provider.ts b/packages/scm/src/browser/scm-provider.ts index 129a0f13ae3d3..6e04b836729ee 100644 --- a/packages/scm/src/browser/scm-provider.ts +++ b/packages/scm/src/browser/scm-provider.ts @@ -16,6 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { interfaces } from 'inversify'; import { Disposable, Event } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; @@ -33,6 +34,7 @@ export interface ScmProvider extends Disposable { readonly onDidChangeStatusBarCommands?: Event; readonly amendSupport?: ScmAmendSupport; + readonly get?: (id: interfaces.ServiceIdentifier) => T | undefined; } export interface ScmResourceGroup extends Disposable { @@ -69,11 +71,11 @@ export interface ScmCommand { } export interface ScmCommit { - id: string, // eg Git sha or Mercurial revision number - summary: string, - authorName: string, - authorEmail: string, - authorDateRelative: string + readonly id: string; // eg Git sha or Mercurial revision number + readonly summary: string; + readonly authorName: string; + readonly authorEmail: string; + readonly authorDateRelative: string; } export interface ScmAmendSupport { diff --git a/packages/scm/src/browser/scm-service.ts b/packages/scm/src/browser/scm-service.ts index d41d98a1373af..5240789fd1855 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/packages/scm/src/browser/style/diff.css b/packages/scm/src/browser/style/diff.css new file mode 100644 index 0000000000000..32cfc5ae7d1f6 --- /dev/null +++ b/packages/scm/src/browser/style/diff.css @@ -0,0 +1,40 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +.scm-diff-container { + display: flex; + flex-direction: column; + position: relative; + height: 100%; +} + +.scm-diff-container .listContainer { + flex: 1; + position: relative; +} + +.scm-diff-container .listContainer .commitList { + height: 100%; +} + +.scm-diff-container .subject { + font-size: var(--theia-ui-font-size2); + font-weight: bold; +} + +.scm-diff-container .noWrapInfo { + width: 100%; +} diff --git a/tsconfig.json b/tsconfig.json index e77b8e2c8339f..63b84501a0df6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -124,6 +124,9 @@ "@theia/scm/lib/*": [ "packages/scm/src/*" ], + "@theia/scm-extra/lib/*": [ + "packages/scm-extra/src/*" + ], "@theia/search-in-workspace/lib/*": [ "packages/search-in-workspace/src/*" ],